代码审查工作流
两阶段审查:本地检查(规范 + 逻辑) + Codex 深度审查(可选)。
Phase 1: 收集变更范围
git diff --name-only HEAD git diff --cached --name-only
如果没有变更文件,提示"没有检测到代码变更"并终止。
将变更文件按类型分组:
-
Java 文件:Controller / Business / Service / Mapper / Entity / VO / DTO
-
XML 文件:Mapper XML
-
SQL 文件:建表/变更脚本
-
其他:配置文件等
Phase 2: 本地检查(必做)
分两步执行:Step A 用 Grep 批量扫描规范问题(秒级),Step B 用 Read 逐文件审查代码逻辑。
Step A: 项目规范扫描(Grep 批量检查)
🔴 A1. 包名规范
Grep pattern: "package org.dromara." path: [目标目录] glob: "*.java"
- ❌ package org.dromara.xxx → ✅ package net.xnzn.core.xxx
🔴 A2. 禁止 RuoYi 工具类
Grep pattern: "MapstructUtils" path: [目标目录] glob: ".java" Grep pattern: "ServiceException" path: [目标目录] glob: ".java" Grep pattern: "import javax.validation" path: [目标目录] glob: "*.java"
-
❌ MapstructUtils → ✅ BeanUtil.copyProperties()
-
❌ ServiceException → ✅ LeException
-
❌ javax.validation.* → ✅ jakarta.validation.* (JDK 21)
🔴 A3. 审计字段名称
Grep pattern: "private.*createBy|private.*updateBy|private.*createTime|private.updateTime" path: [目标目录] glob: ".java"
- ❌ createBy/updateBy/createTime/updateTime → ✅ crby/upby/crtime/uptime
🔴 A4. del_flag 值语义
Grep pattern: "delFlag.*=.0|del_flag.=.0" path: [目标目录] glob: ".java"
- ❌ delFlag = 0 (RuoYi 正常值) → ✅ delFlag = 2 (leniu 正常值,1=删除)
🔴 A5. Entity 不含 tenant_id
Grep pattern: "tenantId|tenant_id" path: [目标目录] glob: "*.java"
- ❌ 双库物理隔离,无需 tenant_id 字段
🔴 A6. 禁止 Map 传递业务数据
Grep pattern: "Map<String,\sObject>" path: [目标目录] glob: ".java"
- ❌ Map<String, Object> → ✅ 使用 VO/DTO 类
🟡 A7. 事务注解缺少 rollbackFor
Grep pattern: "@Transactional\b" path: [目标目录] glob: "*.java"
对命中文件二次检查:是否缺少 rollbackFor
Grep pattern: "@Transactional((?!.*rollbackFor)" path: [命中文件]
- ❌ @Transactional → ✅ @Transactional(rollbackFor = Exception.class)
🔴 A7b. selectOne 无唯一保障
Grep pattern: "selectOne(" path: [目标目录] glob: "*.java"
命中行检查:是否有 LIMIT 1 或注释说明唯一索引
- ❌ 无 LIMIT 且无唯一索引 → ✅ 加 .last("LIMIT 1") 或确保有唯一索引
🟡 A7c. selectCount 用于存在性判断
Grep pattern: "selectCount" path: [目标目录] glob: "*.java"
检查命中行是否用于 > 0 判断(存在性),而非真正计数
- ❌ selectCount(w) > 0 → ✅ mapper.exists(w) 或 selectList(w.last("LIMIT 1"))
🔴 A7d. Redis KEYS 命令
Grep pattern: "keysByPattern|.keys(" path: [目标目录] glob: "*.java"
- ❌ redisTemplate.keys() / keysByPattern() → ✅ 使用 Redisson deleteByPattern() (内部 SCAN + UNLINK)
🟡 A7e. Optional.of 误用
Grep pattern: "Optional.of(" path: [目标目录] glob: "*.java"
排除 ofNullable 的命中
- ❌ Optional.of(可能为null) → ✅ Optional.ofNullable(value)
🟡 A7f. 布尔字段类型错误
Grep pattern: "private Integer (if|is|has)" path: [目标目录] glob: "*.java"
- ❌ private Integer isEnabled → ✅ private Boolean enabled
🟡 A7g. Wrapper 嵌套过深
人工审查:Read 文件时检查 Wrapper 嵌套是否超过 2 层
- ❌ Wrapper 嵌套 >2 层 → ✅ 迁移到 XML 原生 SQL
🟡 A8. 请求体封装
Grep pattern: "@RequestBody [^L]" path: [目标目录] glob: "*Controller.java"
- 建议 POST 请求使用 @RequestBody LeRequest<T> 封装
🟡 A9. 金额类型错误
Grep pattern: "Double|Float|double|float" path: [目标目录] glob: "*.java"
在命中行中检查是否涉及金额字段(amount/price/money/fee/cost)
- ❌ Double/Float 处理金额 → ✅ Long (分)或 BigDecimal
🟡 A10. BigDecimal 比较错误
Grep pattern: "BigDecimal.*==|==.BigDecimal" path: [目标目录] glob: ".java"
- ❌ bigDecimal1 == bigDecimal2 → ✅ bigDecimal1.compareTo(bigDecimal2) == 0
🟡 A11. SELECT * 查询
Grep pattern: "SELECT *|select *" path: [目标目录] glob: "*.xml"
- ❌ SELECT * → ✅ 明确列出需要的字段
🟡 A12. SQL 注入风险
Grep pattern: '${' path: [目标目录] glob: "*.xml"
- ❌ ${} 拼接参数 → ✅ #{} 参数化查询(ORDER BY 等特殊场景除外)
🟡 A13. 国际化异常
Grep pattern: 'new LeException("[^"][\u4e00-\u9fa5]' path: [目标目录] glob: ".java"
- 建议使用 I18n.getMessage() 替代硬编码中文
Step B: 代码逻辑审查(Read 逐文件检查)
对每个变更的 Java 文件执行 Read,按以下清单逐项审查。
Java 必检项(所有 Java 文件)
检查项 检查要点 严重级
B1 空指针风险 selectOne /getOne /selectById 返回值是否有 null 判断;Optional 是否正确处理 🔴
B2 参数校验 入参是否校验非空和合法性(@NotNull /@NotEmpty /Objects.requireNonNull /手动 if 判断) 🔴
B3 并发安全 查询+新增/查询+更新的组合操作是否有竞态条件;是否需要分布式锁 🔴
B4 事务边界 多表写操作是否加了 @Transactional(rollbackFor = Exception.class)
🔴
B5 资源关闭 Stream/IO流/数据库连接是否用 try-with-resources 或 finally 关闭 🔴
B6 集合并发修改 forEach 内是否有 remove/add 操作;应使用 Iterator 或 removeIf
🔴
B7 分页泄漏 PageMethod.startPage() 是否紧贴查询语句,中间不能有其他 SQL 查询 🔴
Java 安全项
检查项 检查要点 严重级
B8 租户隔离(仅定时任务) 定时任务中是否使用 Executors.doInTenant() /doInAllTenant() 切换到商户库;普通接口默认在商户库,无需额外处理 🔴
B9 SQL 注入 动态 SQL 是否使用参数化查询;拼接 SQL 是否转义 🔴
B10 越权访问 删除/修改操作是否校验数据归属(检查 where 条件是否包含归属字段) 🔴
B11 敏感信息 日志中是否打印了密码、token、身份证、银行卡等敏感信息 🔴
B12 批量操作限制 批量删除/更新是否限制了最大数量,防止误操作全表 🟡
Java 跨模块调用项
检查项 检查要点 严重级
B13 返回值兜底 返回 List /Map 时是否有空集合兜底(Collections.emptyList() ),避免调用方 NPE 🟡
B14 集合参数防御 集合入参(List /Set )是否判空,空集合的 IN() 会导致 SQL 异常 🔴
B15 异常透传 是否吞掉异常不抛出(空 catch 块);跨模块调用需要明确的异常传递 🔴
B16 日志追踪 关键操作或异常分支是否有日志(log.info /log.error ),便于跨模块问题排查 🟡
Java 代码规范项
检查项 检查要点 严重级
B17 魔法值 是否存在未定义的常量(状态码 1/2/3 必须用枚举,字符串必须定义常量) 🟡
B18 方法长度 方法是否过长(>50 行),需要拆分为子方法 🔵
B19 注释完整性 公共 API(Controller/Business 方法)是否有 JavaDoc 注释 🔵
B20 空 catch 块 catch 块是否为空或仅打印日志而不处理/不抛出 🟡
B21 过时 API 是否使用了 @Deprecated 的方法或类 🔵
B22 返回值一致性 Controller 层是否统一返回 LeResponse<T> ,不能裸返回 🟡
MyBatis XML 检查(仅 XML 文件)
检查项 检查要点 严重级
X1 SQL 注入 使用 ${} 而非 #{} 进行参数拼接(ORDER BY 等场景需白名单校验) 🔴
X2 IN 查询防护 IN 查询是否用 <foreach> 且考虑集合为空的情况(空 IN 会 SQL 异常) 🔴
X3 动态 SQL 语法 <if> /<where> /<choose> 标签使用是否正确,是否会产生多余的 AND/OR 🟡
X4 **SELECT *** 是否使用 SELECT * ,应明确列出需要的字段 🟡
X5 缺少 WHERE UPDATE/DELETE 是否缺少 WHERE 条件(全表操作风险) 🔴
X6 索引失效 WHERE 条件是否对索引字段使用了函数(DATE(crtime) )或隐式类型转换 🟡
X7 LIKE 前模糊 LIKE '%xxx%' 或 LIKE CONCAT('%', #{}, '%') 前模糊导致全表扫描 🟡
X8 大表无分页 大表查询是否遗漏分页,可能造成 OOM 🟡
X9 namespace 匹配 namespace 是否与 Mapper 接口全限定名完全匹配 🔴
X10 resultMap 映射 是否正确定义 resultMap,字段名和属性名是否对应 🟡
本地检查结果展示
代码审查报告
审查范围: [变更文件列表]
🔴 严重问题(X 项)
-
[B1 空指针风险] 文件: OrderServiceImpl.java:42 问题: selectById 返回值未做 null 判断 修复: if (ObjectUtil.isNull(entity)) throw new LeException("数据不存在");
-
[A2 禁止 RuoYi 工具类] 文件: UserServiceImpl.java:15 问题: 使用了 MapstructUtils 修复: 替换为 BeanUtil.copyProperties()
🟡 警告问题(X 项)
...
🔵 建议(X 项)
...
✅ 通过项
- A1 包名规范 (net.xnzn.core.*)
- A3 审计字段正确 (crby/crtime/upby/uptime)
- A4 del_flag 语义正确 (2=正常)
- B8 租户隔离正确 ...
结论: ✅ 通过 / ⚠️ 需修复 X 项 / ❌ 不通过
如果全部通过 → 展示"本地检查通过",询问是否需要 Codex 深度审查。 如果有问题 → 先修复严重问题,修复后再询问是否需要 Codex 深度审查。
Phase 3: Codex 深度审查(可选)
依赖 collaborating-with-codex skill 的 codex_bridge.py 脚本。 Codex 擅长发现本地规则难以覆盖的逻辑 Bug、复杂并发问题、架构缺陷。
Phase 2 完成后询问用户:"是否需要 Codex 深度审查?"
-
用户同意 → 执行以下流程
-
用户拒绝 → 跳到 Phase 5
python3 .claude/skills/collaborating-with-codex/scripts/codex_bridge.py
--cd .
--sandbox read-only
--PROMPT "Review the following changed files for code quality issues:
FILES TO REVIEW: {变更文件列表,含相对路径}
REVIEW FOCUS (beyond basic lint):
- Logic bugs: race conditions, off-by-one, incorrect state transitions, edge cases
- Security: privilege escalation, data leakage across tenants, missing auth checks
- Architecture: Controller calling Mapper directly, Business layer bypassed, circular dependencies
- Performance: N+1 queries, missing pagination on large tables, unnecessary DB calls in loops
- Concurrency: check-then-act without locking, shared mutable state, CompletableFuture error handling
PROJECT CONTEXT:
- Package: net.xnzn.core.* (NOT org.dromara.*)
- 4-layer: Controller → Business → Service → Mapper
- Dual-database: tenant DB is default (by MERCHANT-ID header); only scheduled tasks need Executors.doInTenant()/doInAllTenant() to switch; Executors.doInSystem() for system DB access
- Audit fields: crby/crtime/upby/uptime
- del_flag: 1=deleted, 2=normal
- Exception: LeException (NOT ServiceException)
- Object copy: BeanUtil.copyProperties() (NOT MapstructUtils)
- Amount: stored as Long (fen/cents), NOT Double/Float
- Pagination: PageMethod.startPage() must be immediately before query
OUTPUT FORMAT: For each issue:
- [SEVERITY] CRITICAL / WARNING / SUGGESTION
- [FILE] filepath:line_number
- [ISSUE] Description
- [FIX] Recommended fix
If no issues: ALL CLEAR
IMPORTANT: All comments in Chinese, code/paths in English."
关键约束:
-
始终使用 --sandbox read-only ,Codex 不直接修改文件
-
变更文件过多时(>10 个),按模块分批审查
-
使用 run_in_background 避免阻塞
Phase 4: 用户确认后修复
合并 Phase 2 和 Phase 3 的所有问题,等待用户确认:
-
全部修复:"修复所有问题" → 逐个修复所有 🔴 + 🟡
-
选择性修复:"只修复严重问题" → 仅修复 🔴
-
跳过:"不需要修复" → 终止
修复时:
-
按文件逐个修复,使用 Edit 工具
-
每修复一个文件,简要说明改动
-
🔵 建议级别默认跳过,除非用户明确要求
-
修复完成后运行 git diff 展示所有变更
Phase 5: 最终确认
修复完成后询问:"是否需要再次审查确认?"
-
如果用户同意 → 回到 Phase 2 重新审查
-
如果用户拒绝 → 输出最终结论并终止
结论: ✅ 通过 / ⚠️ 需修复 / ❌ 不通过
前端审查(如涉及前端文件变更)
前端项目路径:/Users/xujiajun/Developer/frontProj/web
检查项 检查要点
F1 响应码 成功码是 10000 (不是 200)
F2 Token Admin-Token (localStorage)
F3 租户头 MERCHANT-ID (请求头)
F4 金额显示 后端返回分,前端用 money() 转元
F5 权限指令 按钮权限使用 v-hasPerm
F6 加密 敏感字段使用 SM4 加密