Elder 项目 · 面试案例集
配合《elder开发手册问题.md》使用:每条案例按「遇到什么问题 → 怎么解决的 → 学到了什么」组织,适合面试口述。先讲现象和影响,再讲方案,最后落在认知升级——这是面试官最想听的结构。
案例索引
| # | 案例 | 一句话亮点 |
|---|---|---|
| 1 | 雪花 ID 到前端精度丢失 | 后端一行 Jackson 配置解决前端 JS 的天花板 |
| 2 | 时间范围查询查不到数据 | 前后端时区链路排查,不盲改配置 |
| 3 | String 转 Date 多格式兼容 | Converter + Jackson 两条链路各管各的 |
| 4 | Vue 作用域插槽 | <template #default="{row}"> 的底层原理 |
| 5 | 刷新后 Pinia 数据丢失 | 内存态 → localStorage 持久化 |
| 6 | MP 参数为什么是 Serializable | 框架源码里的面向接口设计 |
| 7 | 逻辑删除撞唯一索引 | 4 种方案演进,NULL 不参与唯一校验 |
| 8 | 输 URL 绕过权限 | 菜单级 → 路由级鉴权 |
| 9 | 切页面数据不更新 | keep-alive 改变生命周期模型 |
| 10 | 什么类不能交给 Spring | 单例 + 有状态 = 并发事故 |
| 11 | 同域名 token 串号 | localStorage 按源隔离的坑 |
| 12 | AI 回复显示原始 Markdown | marked + DOMPurify 安全渲染 |
| 13 | 密钥明文进 git | 环境变量注入 + 密钥轮换 |
| 14 | 护工看到别人的任务 | 功能权限 ≠ 数据权限 |
案例 1:雪花 ID 到前端精度丢失
遇到什么问题 列表页删除/编辑按 id 请求时对不上记录。排查发现前端拿到的 id 末几位变成了 0——雪花 ID 是 19 位,而 JS 的 Number 基于 IEEE 754 双精度,只能精确表示 2^53-1(16 位)以内的整数。
怎么解决的 后端加一个 JacksonConfig 配置类,注册 ToStringSerializer,全局把 Long 序列化成字符串返回:
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
objectMapper.registerModule(simpleModule);
学到了什么
- JS 的 Number 有安全整数上限(
Number.MAX_SAFE_INTEGER),跨语言传输大整数要转字符串; - Jackson 全局序列化可以统一解决,不用每个 VO 手动改类型;
- 反方向(前端字符串传回后端转 Long)Jackson 自动兼容,不用额外处理。
案例 2:时间范围查询查不到数据,时间偏移 16 小时
遇到什么问题 按创建时间筛「8-26 到 8-28」查不到任何数据。打点排查发现 SQL 里的实际时间比用户选的提前了约 16 小时。
怎么解决的(排查过程是重点) 逐段排查链路:el-date-picker 返回本地 Date → axios 自动 toISOString() 转成 UTC 带 Z 的串(2026-08-25T16:00:00.000Z)→ 后端解析时忽略了 Z 按本地时间处理 → JDBC 又偏一次。最终方案:前端封装 formatDate 把 Date 转成 yyyy-MM-dd HH:mm:ss 本地字符串再传,后端无歧义解析。
学到了什么
- 排查时区问题要分段打点,定位到底偏在哪一段,而不是盲改配置;
- 这条链路不归 Jackson 管(它只管 JSON 体),也不归 JDBC 管——yml 里的
time-zone配置救不了 query 参数; - 附带解决了当天边界问题:结束时间补到
23:59:59,否则between闭区间会漏掉最后一天的数据。
案例 3:前台 String 向后台传 Date 多格式兼容
遇到什么问题 前端 query 参数传日期字符串,Spring 默认转换格式有限,部分格式直接绑定失败。
怎么解决的 在 WebConfig 里注册 Converter<String, Date>,依次尝试 yyyy-MM-dd HH:mm:ss、yyyy-MM-dd、两种 ISO 带 Z 格式,全不匹配抛出带原始值的异常。同时 yml 里配 Jackson 的 time-zone/date-format 管 JSON 体。
学到了什么
- Converter 管 query/表单绑定,Jackson 管 JSON 请求/响应体,两条链路互补都要配;
- 转换失败要抛带原始内容的明确异常,方便定位是哪个字段格式不对。
案例 4:自定义表格列为什么要包 <template>
遇到什么问题 el-table 状态列里放 el-switch,不理解为什么必须套一层 <template #default="{row}">。
怎么解决的 理解了 Vue 作用域插槽机制:row 是子组件(el-table-column)渲染每行时才有的数据,只有通过作用域插槽才能「子传父、渲染权在父」;<template> 本身不渲染 DOM,只是接收插槽参数的容器,#default 是 v-slot:default 的缩写且只能写在 template 上。
学到了什么
- 作用域插槽解决了「数据在子组件、模板写在父组件」的矛盾;
- 四种等价写法(
#default/#/v-slot:default/v-slot)本质相同,读别人代码不迷惑。
案例 5:刷新页面后 Pinia 数据丢失
遇到什么问题 登录后刷新浏览器就被踢回登录页——Pinia 是内存存储,刷新即清空。
怎么解决的 引入 pinia-plugin-persistedstate:main.js 里 pinia.use() 注册,需要持久化的 store 加 persist: { enabled: true },数据自动同步 localStorage、刷新自动恢复。
学到了什么
- 状态管理库默认都是内存态,「刷新丢失」不是 bug 而是设计;
- persist 默认 key 是 store 的 id,这里埋下了后面同域名串号的坑(案例 11)。
案例 6:MyBatis-Plus 的方法参数为什么是 Serializable
遇到什么问题 读 MP 源码发现 deleteById(Serializable id) 参数不是 Long,不理解为什么。
怎么解决的 从框架设计角度理解:MP 是通用框架,主键可以是 Long/Integer/String/UUID,这些类型全部实现了 Serializable 标记接口,用公共父接口做参数就能兼容所有主键类型。
学到了什么
- 面向接口编程在框架源码里的实际应用;
- 顺带记住一个易错点:MP 的
updateById只忽略 null 字段,setPassword("")会把密码真的更新成空——想跳过字段必须置 null。
案例 7:逻辑删除后同名无法新增(唯一约束设计)
遇到什么问题 name 加了唯一索引,逻辑删除「张三」后无法再新增「张三」——因为逻辑删除只是 UPDATE 标记,数据物理还在,仍占着索引。
怎么解决的(4 种方案演进)
UNIQUE(name):❌ 删除后永远无法新增;UNIQUE(name, deleted):✅ 能解决,但有致命缺陷——同名最多删 1 次,第二次删除时两条(张三,1)撞索引;- MySQL 8.0 表达式唯一索引:
CASE WHEN deleted=0 THEN name ELSE NULL END,利用 NULL 不参与唯一校验,无次数限制; - MySQL 5.7 兼容:虚拟生成列(Generated Column)+ 唯一索引,效果同 3。
代码层再兜底:捕获 DuplicateKeyException 返回友好提示。
学到了什么
- 逻辑删除和唯一索引天然冲突,方案选型取决于业务是否会反复删除同名;
- MySQL 唯一索引「多条 NULL 共存」的特性是这类问题的万能钥匙;
- 这是一道完整的设计演进题,面试可以讲出权衡过程。
案例 8:登录后直接输 URL 绕过权限
遇到什么问题 权限系统只有菜单级 + 按钮级控制,路由守卫只校验登录——登录后手输 /user 照样能进无权页面。
怎么解决的 把后端返回的可见菜单树存进 Pinia,getter 递归展开成路径集合 allowedPaths;守卫在登录校验后追加路径鉴权,不在集合内拦截回首页。两个细节:守卫里权限未加载时先 await 拉取(覆盖直接刷新场景);退出登录必须 resetAll() 清空权限,防止下一账号沿用上一账号的缓存越权。
学到了什么
- 权限体系三层:菜单级(看得见)→ 路由级(进得来)→ 按钮级(点得动);
- 前端拦截只是第一道防线,接口层数据安全靠后端(引出案例 14)。
案例 9:A 页面添加数据,切回 B 页面看不到
遇到什么问题 在护理计划页新增计划后,切回护理任务页看不到新生成的任务,刷新浏览器才正常。
怎么解决的 根因是 Index.vue 的 router-view 包了 keep-alive:组件切走不销毁、切回复用缓存实例,setup 里的数据加载只执行一次。解决:把数据加载放进 Vue 为 keep-alive 专属的 onActivated 钩子,每次切回都重新拉取。注意它首次挂载也会触发,所以 setup 顶层的加载调用要避免重复。
学到了什么
- keep-alive 改变了生命周期模型:
setup/onMounted只跑一次,切回走「激活」不走「重新挂载」; - 判断口诀:本页操作刷新用回调里
loadData();其他页面改了数据要看到,用onActivated; - 排查口诀:「刷新才正常、切换就不对」→ 先查 keep-alive。
案例 10:哪些类不能 @Component 交给 Spring 管理
遇到什么问题 AI 工具类 ElderTools 里的 elderId 每次对话都不同,一开始想加 @Component 注入使用。
怎么解决的 没有纳入容器,改为调用方 new + 构造传参。核心判断:Spring 默认单例,无状态的 Service/Util 才适合;ElderTools 是有状态对象(任务级可变成员变量),单例下并发请求会互相覆盖成员变量,导致数据串号。
学到了什么 快速判断四条:有状态的不行、生命周期和单次任务绑定的不行、框架要求每次新建的不行(如 EasyExcel 监听器)、需要动态传运行时参数的不行。技术上 prototype 作用域能强行塞进容器,但手动 new 更清晰安全。
案例 11:前后台部署同一域名后 token 串号
遇到什么问题 本地开发一切正常,部署到同一域名后,登录后台再打开前台,前台读到的是管理员的 token,返回了管理员的信息。
怎么解决的 根因:两个项目的 persist 都用默认 key token,往同一个 localStorage['token'] 里写。本地端口不同(5173/5174)是两个源所以没事,同域名就是同一个源。解决:显式指定独立 key——后台 elder-admin-token、前台 elder-app-token(userInfo 同理)。
学到了什么
- localStorage 按「协议+域名+端口」的源隔离,端口不同就是不同源——典型的「开发环境掩盖线上问题」;
- 排查口诀:前端串号先开 DevTools → Application → Local Storage 看 key 是否撞名。
案例 12:AI 助手回复显示原始 Markdown 符号
遇到什么问题 聊天页面 AI 回复里 加粗、### 标题 原样显示——大模型默认输出 Markdown 源文本,而 Vue 的 {{ }} 插值是纯文本输出。
怎么解决的 引入 marked(Markdown→HTML)+ DOMPurify(消毒),只对 AI 消息 v-html 渲染,用户消息保持纯文本。两个关键点:AI 输出属于不可信内容,不消毒直接 v-html 就是 XSS 漏洞;v-html 插入的 DOM 不带 scoped 属性,样式必须 :deep() 穿透。
学到了什么
- 「不可信内容 + v-html」必须过 DOMPurify,这是安全底线不是可选项;
- 为什么用户消息不用渲染:纯文本无 Markdown 语义,且渲染用户输入同样有注入风险。
案例 13:API Key 明文写在代码里进了 git
遇到什么问题 自查发现 DeepSeek 的 sk-xxx 明文在 yml、阿里云 AccessKey 硬编码在工具类,且已随 git 提交——历史提交里删掉也翻得到,等于永久泄露。
怎么解决的
- yml 改占位符
${DEEPSEEK_API_KEY:},值由环境变量注入(IDEA 运行配置 / Linux export / 宝塔环境变量框); - 静态工具类
AliOSSUtil拿不到配置注入,加@Component改造成 Bean 用@Value注入,调用处从静态调用改为注入调用; - 最关键一步:去云平台轮换已泄露的 key——删代码不等于作废。
学到了什么
- 配置与代码分离:代码留结构,环境给值;
- 静态工具类要用 Spring 配置,标准做法是改造成 Bean;
- 进过 git 的密钥必须视为已泄露,轮换 + 清理历史(filter-repo)。
案例 14:护工角色能看到其他护工的任务(数据权限)
遇到什么问题 护工登录管理端,预约任务列表能看到所有护工的任务。接口只按前端传参分页,token 里明明有用户 id 但查询没用——菜单隐藏只是假象,手工调接口能看全量。
怎么解决的 服务端强制数据过滤,不信任前端传参:列表接口解析 token 拿当前用户,是护工就覆盖(不是采纳)userId 查询条件,XML 里 user_id = #{query.userId} 生效;详情和打卡执行接口补归属校验——拿别人的任务 id 直接返回「无权」。角色判断复用 hasRole(userId, "HUGONG")。
学到了什么
- 功能权限 ≠ 数据权限:RBAC 管菜单按钮,行级隔离必须落在后端 SQL 条件上;
- 前端隐藏是体验,后端过滤才是安全;服务端要「覆盖」前端传的过滤条件而不是采纳;
- 改进方向:自定义
@DataScope注解 + AOP 统一织入,避免每个接口手写。
一句话总纲(面试收尾用)
这个项目里我踩的坑基本围绕三条主线:前后端边界(数据格式、时区、精度、权限分工)、框架原理(Jackson/MP/keep-alive/Spring 单例的底层机制)、安全意识(XSS 消毒、密钥管理、越权防护)。每个问题我都按「现象 → 定位 → 修复 → 沉淀原则」的方式复盘过,细节可以看我的开发手册笔记。