怎么用 这是格式与深度的参考,不是让你照抄的模板。数字全部是示例,直接抄进简历一问就穿——必须换成你自己的真实数据,而且要能说清是怎么测出来的。下面每个模块都有「数字是怎么测的」和「面试追问」两段,写不出那两段的模块就不要往简历上写

面试题库(电商 / 短视频 / 社区 / 本地生活 / 旅游 / 企业服务 多业务场景)

项目背景设定 多业务线共用的运营管理后台,Vue 3 单页应用。使用方是运营、客服、财务、风控几类角色,共几十号人。核心场景是各业务线的配置管理订单与内容审核数据报表
为什么选这个背景 后台项目最容易写成「用 element-plus 搭了几十个增删改查页面」,那样毫无区分度。真正有含金量的是三件事:权限体系怎么做到既细又不失控怎么让重复的表单和列表不用一个个手写大数据量下前端怎么不卡死。这三个都能讲出设计取舍,也都能测出数字。

模块一:四层权限体系

  1. 四层权限体系(菜单 / 路由 / 操作 / 数据范围)★★★
    简历这样写 后台权限体系设计与落地(Vue 3 + Pinia + 动态路由 + 自定义指令):按菜单可见、路由可达、操作可用、数据可见四层拆分权限,前端按登录后下发的权限码动态注册路由并用指令控制按钮,数据范围由后端按角色绑定的组织/业务线维度强制过滤;权限点变更由改代码发版改为后台配置即时生效。上线后清理了 约 40 个历史遗留的越权入口。
    展开完整拆解
    为什么要这么设计

    接手时后台的权限是「按角色写死在前端」——代码里到处是 if (role === 'admin')。问题很明显:

    一是加一个角色要改一堆文件。运营说「要给客服主管开退款审核权限但不能改价格」,这在当时的实现里得翻遍所有页面找判断条件。二是前端判断压根不是安全边界,把 role 改成 admin 就能看到所有菜单,接口也真的能调通——因为后端没做对应校验。三是数据范围完全没有概念,东南亚区的运营能看到全球所有订单。

    第三点是最严重的,它不是使用不便的问题而是数据越权。所以重做时把「权限」这个笼统的词拆开:菜单可见(左侧显示什么)、路由可达(直接输 URL 能不能进)、操作可用(页面里的按钮)、数据可见(同一个列表页不同人看到的行不同)。这四层的实现位置和责任方完全不同,混在一起谈必然出漏洞。

    整体链路
    登录 │ ├─ 后端返回:用户信息 + 权限码列表 + 数据范围(组织/业务线) │ ["order:view","order:refund","report:export", ...] │ ├─ 存入 Pinia(不落 localStorage,刷新重新拉) │ ├─ 按权限码过滤路由表 ──▶ addRoute 动态注册 │ │ │ │ └─ 菜单树同步生成 └─ 未注册的路由 → 404(而不是无权限页) │ ├─ 页面内按钮:v-perm="'order:refund'" → 无权限则移除节点 │ └─ 数据范围:前端不传、也不可信 └─▶ 后端从会话取当前用户的范围,强制拼进查询条件 关键:四层里前三层是「体验」,第四层和接口校验才是「安全」 前端权限只负责不让用户看到点不了的东西,拦截必须在后端
    分步拆解
    1. 权限码用「资源:操作」的字符串。比如 order:refund。不用位运算、不用角色名。字符串虽然占空间但可读、可搜索、可组合——排查「谁能退款」的时候直接全局搜这个码就行。角色只是权限码的集合,是配置层的概念,代码里不出现任何角色名。
    2. 路由分静态和动态两部分。登录页、404 这类静态注册;业务路由等拿到权限后用 router.addRoute 注册。没有权限的路由压根不注册,用户输 URL 直接落到 404,而不是进去再弹「无权限」——后者会暴露系统里有哪些功能。
    3. 刷新丢路由是必踩的坑。动态注册的路由存在内存里,刷新页面就没了,此时如果当前 URL 是动态路由会直接白屏或 404。解决办法是在全局前置守卫里判断「权限还没拉过」就先拉权限、注册路由,然后next({ ...to, replace: true }) 重新进入一次当前路由。这个写法的细节要能讲——直接 next() 会因为路由表刚变还没生效而匹配失败。
    4. 按钮用自定义指令,且要移除节点而不是隐藏。v-perm 在 mounted 时判断,无权限就把元素从 DOM 里删掉。用 display:none 隐藏的话,改一下样式就能点出来。
    5. 数据范围绝不能由前端传。前端如果传「我要查东南亚区的数据」,那把参数改成全球就越权了。正确做法是后端从会话里取当前用户的数据范围,强制拼进 SQL 的 where 条件,前端传的范围参数只能在自己权限内做进一步收窄。
    6. 每个接口都要独立校验。前端权限只是体验优化,真正的拦截在后端——用注解或者拦截器在接口入口校验权限码。这句话在面试里必须主动说出来,只讲前端怎么控制而不提后端校验,会被直接判定为不懂安全。
    关键决策与取舍

    为什么不做菜单和按钮的可视化配置到底。理论上可以做到「运营自己在后台拖拽配置每个按钮的权限」,但那需要前端把所有按钮都注册到权限中心,维护成本极高而且容易漏。折中方案是权限码在代码里声明、角色与权限码的绑定关系在后台配置。加新功能还是要发版(因为要写新的权限码),但调整谁能用什么不需要发版——后者的变更频率是前者的十倍以上,所以这个折中很划算。

    权限数据不存 localStorage。存了刷新更快,但存在本地就意味着可以被改。虽然改了也过不了后端校验,但会造成「界面显示有按钮点了却报无权限」的困惑。所以每次刷新重新拉,多一次请求换取状态可信。

    踩过的坑:权限变更不即时。管理员把某人的权限收回了,但那人的页面还开着,仍然能看到按钮并且点了会调接口。后端会拦住所以没有安全问题,但体验很差。做法是在接口返回里带一个权限版本号,前端发现版本变了就提示刷新。这比用长连接推送权限变更简单得多,代价是最长延迟一次请求。

    数字是怎么测的

    「约 40 个越权入口」是怎么来的:把新的权限码体系建好之后,写脚本把旧代码里所有 if (role === ...) 的判断点列出来,逐个对照「这个功能应该谁能用」,找出判断缺失或者判断错误的地方。数量是数出来的,不是估的。被追问时要能举一两个具体例子——比如「订单导出按钮任何登录用户都能看到」、「跨区域的数据没有过滤」。举不出具体例子的数字就是编的。

    这个模块本身不适合硬凑性能数字。权限体系的价值是安全性和可维护性,硬写「权限校验耗时降低 X ms」反而可疑(权限判断本来就是内存里的字符串比对,压根不是瓶颈)。该定性的地方就定性,用「变更从发版到配置即时生效」这种可验证的描述,比假的性能数字强。

    面试追问
    Q:前端做权限控制有什么意义?反正能被绕过。 A:前端权限解决的是体验和信息暴露,不是安全。体验上,用户不应该看到一堆点了就报错的按钮;信息上,菜单本身会暴露系统有哪些功能模块,对外部账号来说这也是信息泄露。安全边界只在后端——每个接口独立校验权限码,数据范围强制从会话取。两者是不同层面的事,都要做,但不能拿前端当安全措施。
    Q:动态路由刷新后丢失,你具体怎么解决的? A:在 router.beforeEach 里判断权限是否已加载。没加载就 await 拉权限、循环 addRoute 注册,然后 next({ ...to, replace: true })。这里有两个细节:一是必须重新进入,因为守卫执行时的路由匹配结果是旧路由表算出来的,直接 next() 会匹配不到;二是 replace: true 是为了不在历史记录里留下一条重复的。还有个容易漏的点——404 的兜底路由必须在动态路由之后注册,否则它会先匹配,所有动态路由都进不去。
    Q:数据范围(比如只能看自己区域的订单)在后端具体怎么落地? A:两种做法。简单的是在业务层手动拼条件,但容易漏——几十个查询接口只要有一个忘了拼就是越权。更稳的是在数据访问层统一拦截,比如 MyBatis 的拦截器解析 SQL 并自动追加范围条件,业务代码不用关心。后者的坑是要有白名单,因为有些查询(比如系统配置表)压根没有范围概念,无脑追加会报错。选哪种取决于接口数量和团队规范程度。
    Q:如果一个用户有多个角色,权限怎么合并?有冲突怎么办? A:多角色取权限码的并集,这是最常见也最好理解的模型。至于「冲突」——如果只有「允许」类权限码就不存在冲突,并集就是答案。真正的麻烦是引入「拒绝」类规则之后(比如角色 A 允许退款、角色 B 明确禁止退款),那就得定优先级,通常是拒绝优先。我的选择是不引入拒绝规则,把模型保持成纯白名单,需要限制就不给那个码。规则模型越简单,出错的可能越小——这类系统里可理解性比表达能力更重要。

模块二:可配置化的表单与列表

  1. 可配置化的表单与列表(JSON Schema 驱动 + 动态渲染)★★★
    简历这样写 后台表单与列表的配置化改造(Vue 3 + 动态组件 + JSON Schema + 组件注册表):把重复的增删改查页面抽象为由 JSON 配置驱动的渲染引擎,字段类型、校验规则、联动关系、列表列与筛选项全部由配置描述,业务代码只保留特殊逻辑;配套可视化配置页供运营维护。新增一个标准业务表单由改代码发版(约半天)变为后台配置(十几分钟),重复模板代码减少 约 3000 行
    展开完整拆解
    为什么要这么设计

    后台有四十多个页面,结构高度雷同:上面一排筛选条件,中间一个表格,右上角新增按钮,点开是个表单弹窗。每次新业务要加一个管理页,就是把上一个页面复制过来改字段——复制出来的代码里连注释和变量名都是上一个业务的,改漏一处就是隐性 bug。

    更麻烦的是变更。运营说「这个字段加个必填校验」、「这两个下拉要联动」,都得走前端排期、改代码、发版。一个五分钟的改动走完流程要一两天,运营和前端都在这上面消耗。

    所以核心判断是:这些页面的差异其实只在「有哪些字段、每个字段什么类型、什么校验」,这部分完全可以数据化。把它抽成配置之后,改配置不需要发版,而且新页面不用写代码。

    整体链路
    配置存储(后端表 / JSON) │ │ { key:"price", label:"售价", type:"number", required:true, │ rules:[{min:0}], showWhen:{ field:"type", eq:"paid" } } │ ├─▶ 页面加载:拉配置 ──▶ 渲染引擎 │ │ │ ├─ 遍历字段 → 查组件注册表 │ │ type → 对应组件 │ │ <component :is="map[type]"> │ │ │ ├─ 校验规则 → 生成校验配置 │ ├─ showWhen → 计算属性控制显隐 │ └─ 未知 type → 降级为文本框 + 告警 │ └─▶ 提交:按配置收集值 → 统一提交接口 逃生舱:配置里可指定 slot 名,业务页面用插槽覆盖某个字段的渲染 (不是所有需求都能配置化,必须留手写的口子)

    最后那个「逃生舱」是这类方案能不能落地的关键。纯配置化一定会遇到配不出来的需求,如果没有退回手写的口子,团队就会绕过整套机制自己复制页面,方案很快作废。

    分步拆解
    1. 先归纳字段类型,不要一开始就抽象。把现有四十多个页面的所有字段列出来分类,发现其实就十来种类型覆盖了九成以上:文本、数字、下拉、多选、日期、日期区间、开关、图片上传、富文本、级联选择。剩下的一成是特殊的。先有归纳再有抽象,凭想象设计的配置结构一定会跑偏。
    2. 组件注册表把类型映射到组件。一个 type -> 组件 的映射表,渲染时用 <component :is> 动态渲染。新增类型就是往表里加一项,不用改引擎。遇到未知 type 要降级成文本框并在控制台告警,不能直接白屏——配置是运营在改的,出错必须能撑住。
    3. 校验规则用声明式描述。requiredminmaxpattern 这些直接对应表单库的校验配置。正则要谨慎——让运营在后台填正则风险很大,可能填出灾难性回溯的表达式把页面卡死。做法是提供预置的常用校验(手机号、邮箱、URL),复杂的走特殊字段。
    4. 联动用声明式条件而不是回调。showWhen: { field: 'type', eq: 'paid' } 这种结构能被 JSON 表达,转成计算属性求值。如果允许配置里写函数字符串然后 eval,那就是把代码注入的口子开在了后台配置页上,绝对不能做。
    5. 列表侧同样配置化。列定义(字段、标题、宽度、格式化方式)、筛选项、行操作按钮(含权限码)都是配置。格式化方式用预置的枚举(金额、日期、枚举映射、图片缩略图),不开放自定义函数。
    6. 配置要有版本和回滚。运营改错配置会直接让页面不可用。所以配置的每次修改存一个版本,出问题能一键回滚,并且发布前有预览。这一步是从事故里学来的——有次改错了必填项导致整个页面提交不了。
    关键决策与取舍

    为什么不用现成的低代码平台。评估过,但两个问题:一是它们的产出物往往和自己的组件库、权限体系不通,接进来的成本不比自己做低;二是能力过剩——我们只需要「配置化的增删改查」,不需要页面拖拽、流程编排这些。引入一个大平台会带来学习成本和不可控的黑盒。所以选了「小范围自研 + 留手写逃生舱」。

    配置化的边界必须画清楚。标准的增删改查配置化收益极高;但一旦某个页面有复杂交互(比如订单详情页里的多步骤操作、带实时计算的表单),配置化的成本会超过手写。我的判断标准是「配置能不能在半小时内配完」,超过就直接手写。硬把所有页面塞进配置化框架,最后会做出一个谁都看不懂的巨型配置。

    代价:调试变难了。手写页面出问题直接看代码,配置驱动的页面出问题要先看配置对不对、再看引擎解析对不对,链路长了一层。所以配套做了配置校验和错误提示——配置保存时先校验结构,运行时遇到异常配置在页面上显示明确的错误而不是崩掉。这个代价要主动承认,说配置化只有好处没有坏处,一听就是没真做过。

    数字是怎么测的

    「约半天变十几分钟」:拿改造后真实新增的几个页面计时。改造前的「半天」是从 git 记录估的——找几个历史上新增管理页的提交,看从第一次提交到合并的时间(去掉等待评审的部分)。要说清这半天包含什么:复制页面、改字段、对接接口、自测、发版。改造后的「十几分钟」是在配置页操作的实际耗时,不含需求沟通。

    「减少约 3000 行」:git diff --stat 看改造涉及的提交,删除行数减去新增行数(引擎本身的代码要算进新增里,不能只报删除数)。这个数字是净减少,如果只报「删了 5000 行」而不提「引擎写了 2000 行」,是不诚实的。

    面试追问
    Q:配置里怎么支持复杂的字段联动?比如 A 选了某个值,B 的下拉选项要重新请求。 A:简单的显隐和禁用用声明式条件就够。「重新请求选项」这类需要副作用的联动,我的做法是配置里声明「依赖字段」和「选项来源接口」,引擎监听依赖字段变化后带上它的值去请求。这覆盖了级联下拉这个最常见的场景。再复杂的(比如三个字段互相计算)就走逃生舱手写,不硬塞进配置。判断标准是:配置结构如果需要表达控制流,就说明该手写了。
    Q:让运营改配置,你怎么防止他们把页面改坏? A:四层防护。一是配置页本身做约束,字段类型是下拉选而不是手填,校验规则是勾选预置项;二是保存时做结构校验,比如必填的属性缺失直接拒绝保存;三是有预览,发布前能看到渲染结果;四是版本与回滚,改坏了能立刻退回上一版。另外权限上配置修改权只给少数人,不是所有运营都能改。这几层里回滚是最兜底的——前面三层都可能漏,回滚保证坏了能马上恢复。
    Q:这套东西你觉得最大的风险是什么? A:被滥用导致失控。配置化好用之后,团队会倾向于把所有需求都往配置里塞,配置结构不断膨胀,最后出现嵌套五层、带条件表达式的配置——那时候它已经是一门没有类型检查、没有 IDE 支持、没法调试的自制语言了,比手写代码糟糕得多。所以需要有人守住边界,明确哪些场景不用配置化。另一个风险是引擎成了单点,引擎的 bug 会同时影响所有页面,所以引擎的改动要比业务代码更谨慎。

模块三:大数据量报表与导出

  1. 大数据量报表与导出(虚拟滚动 + 服务端异步导出)★★★
    简历这样写 报表页大数据量渲染与导出改造(Vue 3 + 虚拟滚动 + 服务端异步导出 + 任务轮询):表格改为虚拟滚动只渲染可视区行,配合列冻结与合计行的独立渲染;导出由前端拼装改为服务端异步生成 + 任务状态轮询 + 链接下载,前端不再持有全量数据。一万行十余列的报表首屏渲染由约 3.2s 降至 350ms 上下;十万行导出由前端页面卡死不可用变为后台约 40 秒产出文件。
    展开完整拆解
    为什么要这么设计

    财务和运营的报表页是投诉最多的地方。两类问题。

    一是渲染卡死。报表默认查一个月的数据,一万多行,每行十几列,还有几列带格式化和条件样式。一次性渲染出来是十几万个 DOM 节点,浏览器要卡三四秒,滚动也不流畅。运营的反馈是「点了查询以为没反应,又点了一次」。

    二是导出直接崩。原来的导出是前端把所有数据拉下来、在浏览器里拼 Excel。几千行还行,十万行时页面直接无响应——因为要把全量数据放进内存再构造出一个巨大的数组和字符串。

    这两个问题的解法方向是相反的:渲染问题是「少渲染」(虚拟滚动),导出问题是「别在前端做」(挪到服务端异步)。共同点是都不再让前端持有全量数据

    整体链路
    渲染侧 查询 ──▶ 分页拉数据(或全量拉但只渲染可视区) │ ├─ 虚拟滚动:计算 startIndex / endIndex │ 只渲染约 40 行 + 上下缓冲各 5 行 │ 用 transform 偏移,撑高用占位元素 │ ├─ 冻结列独立渲染层,滚动时同步位置 └─ 合计行单独请求(不在前端累加全量) 导出侧 点击导出 ──▶ 提交导出任务(带当前筛选条件) │ ├─ 服务端返回 taskId,前端进入「生成中」 │ ├─ 服务端流式查询 + 分批写文件 → 上传对象存储 │ (不把结果集一次读进内存) │ ├─ 前端轮询任务状态(退避间隔) └─ 完成 → 返回下载链接 → 触发下载 要点:导出请求带的是「筛选条件」,不是「数据」 服务端重新查一遍,前端全程不持有全量结果
    分步拆解
    1. 虚拟滚动的核心是三个量:可视区高度、行高、滚动位置。由此算出该渲染哪一段。行高固定的情况很简单;报表里如果有换行导致的不定高,就要缓存每行实测高度并做增量修正——这部分复杂度高,所以我的选择是强制单行显示、超出省略号 + 悬浮查看全文,把不定高问题从源头消掉。
    2. 缓冲区要留。只渲染精确的可视区,快速滚动时会看到空白。上下各多渲染几行作为缓冲。缓冲太大又失去意义,几行到十行是合适的量级。
    3. 冻结列单独一层渲染。左侧固定的几列做成独立的 DOM 层,横向滚动时不动,纵向滚动位置和主体同步。坑是行高对不齐——两层各自渲染,如果内容高度有差异就会错位,这也是强制单行显示的另一个理由。
    4. 合计行不在前端算。原来是把全量数据拉下来在前端 reduce 求和,这直接决定了前端必须持有全量数据。改成单独请求一个合计接口,服务端用 SQL 聚合返回。这一步是「前端不持有全量数据」得以成立的前提。
    5. 导出改成异步任务。前端提交筛选条件拿到 taskId,然后轮询状态。轮询间隔要退避(比如从 1 秒逐步拉到 5 秒),不能固定高频轮询。任务完成返回下载链接,前端触发下载。用户在等待期间可以离开页面,回来还能看到任务列表。
    6. 服务端导出必须流式。不能 select * 全查进内存再写文件,那只是把 OOM 从前端搬到了后端。用游标或者分页循环,查一批写一批,写完刷盘。生成的文件上传对象存储,通过时效链接下载,避免占用应用服务器带宽。
    7. 导出要有并发和量级限制。不限制的话几个人同时导出全量数据能把数据库拖垮。做法是限制同时进行的导出任务数、超过行数上限要求缩小筛选范围、导出查询走只读库。
    关键决策与取舍

    为什么不直接分页,非要虚拟滚动。产品上确实先建议了分页,但财务的使用习惯是上下对比不同行的数据,翻页会打断。折中方案是「后端分页 + 前端虚拟滚动」的组合——一次拉一大页(比如两千行)在前端虚拟滚动,滚到底部自动拉下一批。既没有一次十万行的压力,也没有翻页的割裂感。纯技术最优不一定是产品最优,要能说出这个权衡过程。

    异步导出的体验代价。原来点了立刻下载(虽然会卡),现在要等任务完成。对小数据量来说体验反而变差了。所以做了分流:行数小于某个阈值走同步导出立刻返回,超过才走异步任务。这个阈值是试出来的——同步导出的耗时控制在几秒内是可接受的。

    踩过的坑:导出的数据和页面看到的不一致。因为导出是服务端重新查的,而用户看到的是几分钟前查询的结果,期间数据变了。解决办法是导出的文件里带上「数据截止时间」,并且导出请求带上查询时的时间戳作为数据边界。这类一致性细节容易被忽略,但财务对账时对不上会引发很严重的质疑。

    数字是怎么测的

    渲染耗时:用 Performance 面板录制,量「点击查询后数据返回」到「表格渲染完成、页面可交互」这段。要固定数据量(一万行)和列数,并说明机器配置——同样的代码在开发机和运营的办公电脑上差别很大,性能数字必须带测试环境。改造前约 3.2s,改造后 350ms 上下。

    为什么改造后不是更小的数字:350ms 里包含了数据处理和四十行的渲染,这已经接近这个量级的合理下限。报出 50ms 之类的数字反而不可信。

    导出耗时:十万行约 40 秒是服务端任务的执行时间,从任务日志里取。改造前的「不可用」不是估的——实测在浏览器里跑十万行导出,页面无响应到需要强制关闭标签页,这个结论比编一个「原来要 5 分钟」更真实。做不到的事情就说做不到,不要为了对比强行造一个数字。

    面试追问
    Q:虚拟滚动的滚动条高度怎么撑起来的? A:外层容器固定高度且 overflow: auto,里面放一个占位元素,高度设成「总行数 × 行高」。真实渲染的那几十行用 transform: translateY() 偏移到正确位置。用 transform 而不是改 top 或 margin,因为 transform 不触发重排,滚动时性能好很多。行高不定的情况就要维护一个高度累积表,滚动位置反查行索引,复杂度高一个档次。
    Q:导出的时候用户关掉了浏览器,任务怎么办? A:任务在服务端跑,和前端断连无关,会继续执行完并把文件存到对象存储。所以要有一个「我的导出任务」列表页,用户下次进来能看到历史任务和下载链接。这也是异步导出相比同步的一个额外好处。文件要设置过期清理,不然对象存储会越堆越多——通常保留几天。
    Q:几个人同时导出全量数据,数据库会不会被打挂? A:会,这是必须防的。几层措施:导出走只读库不影响主库;限制并发任务数,超过就排队;限制单次导出的行数上限,超了要求用户缩小时间范围;查询用游标流式读取,避免单个查询占用大量内存和长时间持有连接。另外导出的 SQL 要确保走索引——全量导出很容易写成全表扫描,加上排序就更慢。
    Q:为什么合计行要单独请求?前端加一下不行吗? A:前端加的前提是前端有全量数据,而整个改造的方向就是让前端不再持有全量数据。分页之后前端手里只有当前批次,加出来的合计是错的。所以合计必须服务端用 SQL 聚合。这里有个容易忽略的点——合计接口和列表接口要用同一套筛选条件,否则会出现「明细加起来和合计对不上」,财务场景下这是致命的。我的做法是把筛选条件的构造抽成公共方法,两个接口共用。

没有匹配的内容,换个关键词试试。

项目拆解 · 运营管理后台(Vue)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据