基于ruoyi——vue3
Q1「简单介绍一下这个项目」标准应答2分钟版「这是一个基于若依 RuoYi-Vue3 框架二次开发的智能售货机运营管理平台。业务场景是一家公司在全国投放了几千台智能售货机需要一套系统来管理设备从投放到撤机的全生命周期。核心业务分三层基础数据层管区域、点位、合作商、人员设备核心层管设备本身、设备型号、货道和商品运营流程层管工单——这也是最复杂的模块。工单分两种运营补货工单由 Quartz 定时轮询自动生成运维工单手动创建。设备故障从发现到派单维修原来靠电话微信沟通要两天系统上线后平均 2 小时内解决。后端一共 10 个业务模块代码生成器生了 60% 的 CRUD 骨架剩下 40% 的业务逻辑、状态机、权限改造、前端交互都是手写的。」Q2「你在里面做了什么哪些是你写的哪些是框架生成的」标准应答「代码生成器生成了每个模块的 Entity / Mapper / Service / Controller 基础方法和前端 CRUD 页面——这部分是骨架。我手写了五块工单状态机流转逻辑——Controller 里校验状态变更是否合法Service 里更新状态 记录操作日志Quartz 定时任务——轮询货道库存自动创建补货工单包含库存阈值判断、工单编号生成、工单明细写入前端列表改造——比如点位列表要展示区域名和合作商名后端多表联查前端联动下拉框权限扩展——给生成的代码加DataScope注解运维只看自己的工单Excel 导入导出——EasyExcel 批量导入点位台账加校验逻辑异常行自动标记返回」Q3「讲一下整个项目的数据流从用户登录到创建一条工单」标准应答「登录时前端发用户名密码 → Spring Security 过滤器链 → UserDetailsService 查数据库 → 密码匹配 → 生成 JWT Token 返给前端存入 localStorage → 前端路由守卫拿 Token 调getInfo获取用户角色和权限菜单。创建工单时前端页面根据已加载的菜单权限渲染「创建工单」按钮 → 运营人员点创建 → 选设备编号前端三级联动加载区域→点位→设备→ 选工单类型补货自动生成这一步跳过 → 填工单详情 → 调后端/task/create接口 → 后端生成工单编号日期Redis INCR → 写入 tb_task tb_task_details → 返回工单编号给前端 → 运维 App 刷新看到新工单。」二、工单模块深挖必挖Q4「补货工单自动生成的具体逻辑是什么」标准应答「用 Quartz 写了一个定时任务每 5 分钟执行一次第一步查所有运营中状态的设备 第二步遍历每个设备的所有货道查到货道的当前库存和补货阈值 第三步如果当前库存 补货阈值就创建一条运营补货工单 第四步工单编号格式是年月日 当天序号比如20250315001。当天序号通过 RedisINCR task:code:20250315原子自增获得防止并发重复 第五步一条工单对应多条工单明细——每个缺货的货道一条明细记录货道编号、缺货商品、建议补货量。这里有个优化点不能每次定时任务扫全部设备全部货道我加了设备状态过滤只扫运营中的设备而且补货阈值不是硬编码是在策略管理里可配置的。」追问「如果货道库存是假的、不准的怎么办」「库存准确性确实是个问题。实际场景里货道有物理传感器出货一次传感器计数减一。系统显示的库存是传感器上报的值。我们在定时任务里增加了容错——如果连续三次定时任务都检测到同一个货道缺货但运维反馈实际有货会生成一条「传感器校准」运维工单提醒运维去现场检查硬件。」Q5「工单编号并发问题具体怎么解决的」标准应答「最朴素的做法是先查当天最大序号再 1但并发下多个线程可能查到同一个值。我的方案是 Redis INCRString today LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); String key task:code: today; Long seq redisTemplate.opsForValue().increment(key); // 第一个请求 seq1第二个 seq2原子递增 redisTemplate.expire(key, 1, TimeUnit.DAYS); // 第二天自动过期 String taskCode today String.format(%03d, seq);不用数据库自增主键的原因自增 ID 是按所有工单全局递增的不是按天重置。也不用SELECT FOR UPDATE行锁因为锁粒度太大数据库压力大。REDIS INCR 天然原子性能最好。就算 Redis 挂了重启序列号丢了下一个并发请求重新 INCR 也不会重复只是序号会断档——这对业务没影响。」追问「INCR 到 999 超出 3 位怎么办」「每天工单量不可能到 999运营补货一天几十次维修工单一天十几单。但如果真有极端情况我会用 4 位格式。另外还有一个兜底如果 Redis 不可用降级到数据库FOR UPDATE查当日最大序号。」Q6「工单状态机怎么实现的怎么防止非法状态流转」标准应答「状态机和流转规则在后端硬编码状态流转规则 待处理(0) → 进行中(1) ✅ 允许 待处理(0) → 已取消(3) ✅ 允许 进行中(1) → 已完成(2) ✅ 允许 进行中(1) → 已取消(3) ✅ 允许 已完成(2) → 任何状态 ❌ 禁止 已取消(3) → 任何状态 ❌ 禁止实现Service 层写了一个validateTaskStatusTransition(currentStatus, newStatus)方法用一个MapInteger, SetInteger存允许的流转规则。每次更新工单状态前先校验不允许的流转直接抛业务异常TaskStatusException。防止并发改同一个工单用Transactional保证原子性更新 SQL 加WHERE task_status #{oldStatus}做乐观锁。」Q7「工单分配到运维人员的逻辑是什么」标准应答「管理员在后台可以把工单指派给指定运维——手动指派。自动分单功能我做了简化实现根据工单关联的区域查该区域下的所有运维人员选当前工单数最少的一个——用 Redis ZSet 存区域ID → 运维人员工单计数按工单数升序取第一个。更完善的方案应该考虑运维人员的在离线状态、技能标签维修 vs 投放、位置距离GPS但教程项目没做到这个细粒度。」三、权限模块深挖Q8「DataScope 注解到底做了什么」标准应答「DataScope 是若依封装的 MyBatis 插件原理是在切面里拦截加了注解的方法把数据权限范围注入到参数里然后 MyBatis SQL 拼接时自动加上过滤条件。以查看工单列表为例Controller 方法上加DataScope(deptAlias t, userAlias t)AOP 切面从 SecurityContext 拿到当前用户的数据权限范围比如「本区域及以下」把 dept_id 范围 set 到 BaseEntity 的params里MyBatis XML 里对应的 SQL 末尾会自动拼接AND t.user_id IN (本区域运维人员ID列表)我加了这个注解后运维登录查工单SQL 自动变成SELECT * FROM tb_task t WHERE ... AND t.user_id IN (3, 5, 7) -- 3/5/7 是当前运维所在区域的运维人员ID这样运维只能看到自己或同区域同事的工单合作商只能看到自己名下的点位实现了行级别的数据隔离。」Q9「四角色权限怎么区分的」标准应答「角色设计角色核心权限数据范围管理员全部模块全部数据运维人员工单(查看/操作) 设备(查看)所在区域 已指派工单运营人员工单(查看/操作) 商品货道(查看)所在区域 已指派工单合作商点位(查看) 收益(查看)仅名下数据前端路由守卫router.beforeEach根据角色动态加载菜单按钮用v-hasPermi指令控制显隐。 后端Controller 方法加PreAuthorize(ss.hasPermi(manage:task:list))做接口级鉴权。数据权限层加DataScope做行级过滤。」四、技术选型深挖Q10「为什么用若依而不是直接用 Spring Boot 从零开发」标准应答「三个理由省时间——登录认证、RBAC 权限、操作日志、数据字典、定时任务、代码生成器这些通用能力若依都现成的。从零写至少多花两个礼拜而且不一定写得比它稳。学框架设计——若依的代码分层controller → service → mapper和 MyBatis 插件机制DataScope、操作日志都是 MyBatis 插件实现本身就是很好的工程实践参考。面试优势——面试官一听「基于若依二次开发」会默认你有读源码的能力和工程规范意识。比「手写了一个 Spring Boot 增删改查」高出不止一档。前提是——我必须能把框架做了什么、我做了什么分得清清楚楚。这也是今天面试我想讲清楚的重点。」Q11「为什么不直接用 ruoyi-vue-pro和它比这个项目少了什么」标准应答「Pro 版多了 SaaS 多租户、Flowable 工作流、商城、CRM、ERP、支付系统这些重模块。我的项目是一个垂直领域的运营场景不需要这些。业务规模也只有一家公司几千台设备不需要多租户。区别核心三点Pro 有多租户行级隔离TenantLineInnerInterceptor我没有租户概念直接按角色做数据权限Pro 有完整的 Flowable 工作流引擎会签/或签/驳回/加签 20 特性我的工单流转是手写的简化状态机Pro 代码更重11 万行我这个项目裁到只保留核心框架 10 个业务模块理解成本更低」追问「那如果需求变复杂了这个简化状态机撑不住怎么办」「迁移到 Flowable。工单表的 task_code 和 task_status 字段保留把审批流程交给 Flowable 的 BPMN 流程定义——请假审批、会签、转办这些高级操作直接调 Flowable API。Ruoyi-Vue 和 ruoyi-vue-pro 底层都是 Spring Boot迁移成本不高。」Q12「如果设备数量从千级变百万级哪些地方会出问题怎么解」标准应答「四个瓶颈定时任务扫全表——设备百万级每 5 分钟扫一轮货道库存光 SQL 就崩溃。解法改 CDCChange Data Capture货道库存变化时发 MQ 消息消费者判断是否低于阈值异步创建工单。不用定时扫。分页列表查询——百万设备的分页下拉框直接崩。解法干掉下拉列表改成远程搜索Select 组件输名字模糊搜索后端 ES。单库单表——tb_task 日增万级工单一年几百万。解法按月分表 冷数据归档。Redis 单机扛不住——区域树、字典、设备状态全在 Redis。解法Cluster 分片热点 key 打散到多个分片。」五、数据库深挖Q13「区域-点位-设备-货道-商品这条链路表怎么设计的联查会不会慢」标准应答「表关系tb_region (区域) 1:N → tb_node (点位) 1:N → tb_vending_machine (设备) 1:N → tb_channel (货道) N:1 → tb_product (商品)设备详情页需要展示货道商品最多联查 3-4 张表。我的方案是 MyBatis 一条 SQL JOIN 全查回来用resultMap嵌套映射到 VOLEFT JOIN tb_channel ON vm.id ch.vm_id LEFT JOIN tb_product ON ch.product_id p.id设备下的货道通常不超过 50 个一个售货机 50 个货道算很多了数据量极小一条 JOIN 毫秒级返回。不需要分次查询也不需要缓存。必须避免 MyBatis 的 N1 问题如果在 resultMap 里用selectselectChannelsByVmId做子查询每个设备都会额外执行一次 SQL。我用一条 JOIN 直接映射没有这个问题。」Q14「SQL 优化举个具体例子」标准应答「点位列表页展示三个字段点位名称、所属区域名、合作商名称。代码生成器默认只查点位表不关联区域和合作商。原来的做法是前端渲染时逐行去查区域名和合作商名——这就是典型的 N1 问题列表 20 行 1 20 20 41 次查询。我改造了 PointMapper.xml一条 LEFT JOIN 三张表全查回来SQL 变成SELECT n.*, r.region_name, p.partner_name FROM tb_node n LEFT JOIN tb_region r ON n.region_id r.id LEFT JOIN tb_partner p ON n.partner_id p.id从 41 次查询降到 1 次分页查询从 2.3s 降到 380ms。」六、缓存深挖Q15「这个项目 Redis 存了什么怎么保证一致性」标准应答「存了三类数据数据类型Redis 结构刷新策略区域树Hash增删改时清除 Hash下次查询重建设备类型字典Hash同上——主动删除设备状态String在线/离线/故障设备 App 心跳上报实时更新缓存一致性用了「先更新数据库再删除缓存」策略Transactional public void updateRegion(Region region) { regionMapper.updateById(region); // 1. 更新 DB redisTemplate.delete(cache:regionTree); // 2. 删缓存 }极端情况下删缓存失败用CacheEvict注解配合若依框架自带的定时任务每 30 分钟全量刷新一次缓存做最终一致兜底。」七、工程化深挖Q16「批量导入 2000 条点位数据怎么做的异常数据怎么处理」标准应答「用 EasyExcel 的同步读取 逐行校验EasyExcel.read(file.getInputStream(), NodeExcelVO.class, new ReadListenerNodeExcelVO() { ListNodeExcelVO successList new ArrayList(); ListErrorRow errorList new ArrayList(); Override public void invoke(NodeExcelVO data, AnalysisContext context) { // 逐行校验 String error validateRow(data); // 区域名是否存在 / 合作商是否存在 / 地址非空 if (error ! null) { errorList.add(new ErrorRow(context.readRowHolder().getRowIndex(), error)); } else { successList.add(data); } } Override public void doAfterAllAnalysed(AnalysisContext context) { // 批量写库 返回错误明细 nodeService.batchInsert(successList); } }).sheet().doRead();关键设计成功和失败分开返回——前端展示「导入完成成功 1950 条失败 50 条点击下载错误明细」。2000 条数据校验 入库约 5s瓶颈在校验逻辑里每次要查数据库确认区域名/合作商名是否存在。优化方案先在 Redis 里缓存区域名→ID 的映射校验全走内存。」Q17「AOP 操作日志怎么做的记录了什么」标准应答「若依框架自带基于 MyBatis 插件的操作日志——通过Log注解 AOP 切面拦截记录操作人、操作时间、请求参数、返回值。但 MyBatis 插件层面的日志只记录了 SQL 执行不记录「数据到底改了什么」。我额外加了一层在关键业务操作创建工单、修改设备状态、更新库存的 Service 方法上用 AOP 拦截参数和返回值把变更前后的数据快照JSON 格式存入操作日志表的 extra_data 字段方便运维排查问题。AOP 切面核心代码Around(annotation(com.dkd.common.annotation.DataLog)) public Object around(ProceedingJoinPoint point) throws Throwable { // 执行前查旧数据 Object oldData getOldData(point.getArgs()); // 执行业务 Object result point.proceed(); // 执行后diff 对比 DataLog log buildLog(oldData, result); asyncLogService.save(log); // 异步写入不阻塞主线程 return result; }异步写入是关键——日志不能阻塞主业务。」八、项目和 ruoyi-vue-pro 的关联追问Q18「所以你两个若依版本都接触过你觉得核心区别是什么」标准应答「接触过。RuoYi-Vue3 是原版这个帝可得项目就是基于它做的。ruoyi-vue-pro 是芋道维护的增强版我读了源码但没在上面做项目。核心区别维度RuoYi-Vue3RuoYi-Vue-Pro多租户无有TenantLine 行级隔离工作流无Flowable 完整引擎代码生成器单表/主子表/树表同但模板更丰富权限Spring Security JWTSSO单点登录 OAuth2.0模块系统管理基础功能系统商城CRMERPAI先学原版的好处是吃透了底层原理Spring Security 过滤器链怎么走的、代码生成器 Velocity 模板怎么渲染的再去读 Pro 的源码就有框架感了知道每个模块是在哪个基础上扩展的。」九、快速自查面试前必练序号面试官问这个你能不看笔记答出来吗1工单状态流转规则是什么✅ / ❌2工单编号生成怎么防并发✅ / ❌3Quartz 补货工单自动生成流程✅ / ❌4DataScope 做了什么、SQL 怎么变的✅ / ❌5设备-货道-商品三级联查怎么避免 N1✅ / ❌6Redis 存了哪些数据、怎么保证一致性✅ / ❌7EasyExcel 导入异常行怎么处理✅ / ❌8代码生成器生成了什么、你写了什么✅ / ❌9百万设备级别怎么优化✅ / ❌10和 ruoyi-vue-pro 的区别✅ / ❌