引言

架构是图纸,流程是施工。上一篇讲清楚了图纸长什么样,这一篇回到工地现场:看一个真人和一个 AI,5 天怎么把 24 个任务推进完毕。

这一篇不打算写成逐天流水账。先一张表概览全貌,再深入两个剖面:TDD 流程怎么在“人+AI”模式下落地、分歧和 Bug 怎么发现和修复。最后聊聊那些你不容易感知到的:规范执行带来的隐形价值。


在这里插入图片描述

第一部分:5 天 24 个任务

1.1 真实进度总览

以下是一份逐天真实记录。表格中的数据来源是 tasks.md 和实际开发节奏:

天数 Phase 完成任务 任务数 Trae 做了什么 我做了什么 发现的问题
Day 1 P0 + P1 项目框架搭建、Swagger 配置、用户管理 CRUD、角色管理、数据权限 5 生成 Spring Boot 骨架、Security 配置、JWT 认证、BCrypt 加密 调整包结构命名、确认版本号兼容性 Swagger 的页面请求被拦截
Day 2 P1 + P2 员工状态机、岗位管理、部门管理、考勤组与班次、种子数据生成 5 生成 Entity/Mapper/Service/Controller 全套、5 状态 9 规则状态机、10 部门+100 员工种子数据 校验状态转换规则是否完备、“已离职→待入职” 的重新入职路径需要单独补充 种子数据生成时,先插员工再插部门导致外键约束失败
员工状态流转路径缺失
Day 3 P2 日考勤计算引擎(含打卡与考勤统计)、考勤日历、假期额度管理、请假审批、加班管理、月考勤汇总 6 日考勤判定算法(迟到/早退/旷工/缺卡/正常五种状态)、假期额度自动生成、请假审批流程 补充打卡的边界 case(一天多次打卡取首条 IN+末条 OUT)、调整节假日的计算逻辑 月考勤汇总的批量重算存在 N+1 查询:每个员工单独查一次日结果,100 个员工就是 100 次查询
Day 4 P3 薪资科目管理、薪资公式引擎、工资单生成、职位管理、候选人管理、招聘看板 6 科目+模板双层薪资模型、公式引擎、候选人状态机、拖拽式招聘看板 调整薪资计算链路的数据联动(考勤→薪资)、补充公式引擎的 BigDecimal 精度处理 公式引擎对“空值”的处理不当,某员工当月没有加班记录,公式里引用加班字段直接抛 NPE
Day 5 P4 数据看板与移动端适配、性能优化 2 ECharts 图表组件封装、响应式布局 调整看板图表的数据聚合口径、优化首屏加载 基础优化后 Lighthouse 仍只有 70,Element Plus 全量引入导致首屏 JS 体积过大,常规优化手段无效

1.2 节奏分析

如果仔细看上表的任务数分布,我们会发现:

  • 前 2 天(10 个任务):搭骨架。Phase 1 的 10 个 P0 任务是整个系统的地基。这个阶段的特点是“代码量大但处理逻辑相对少”:CRUD 为主,Trae 生成速度极快,我的工作主要是审查和微调。
  • 第 3 天(6 个任务):跑业务闭环。Phase 2 的任务数量多但每个都是“小闭环”——日考勤引擎、请假审批流、月考勤汇总,每个任务写完就能独立验证。这个阶段是出 Bug 最多的一天。
  • 第 4 天(6 个任务):打硬仗。薪资公式引擎和候选人状态机是技术密度最高的模块,任务数少了但每个任务的复杂度翻倍。
  • 第 5 天(2 个任务):收尾打磨。图表、适配、优化,属于“锦上添花”类任务。如果时间不够,这几个任务是可裁减的。

1.3 一个典型任务的完整时间线

以 Day 2 的“员工状态机”任务为例,拆解从启动到验收的全过程:

步骤 谁主导 具体动作
1. 创建 Git 分支 Trae git checkout -b task-4-employee-state-machine
2. 编写测试 Trae 生成 EmployeeTransitionServiceTest,覆盖 9 条合法转换和 5 条非法转换
3. 测试失败(Red) Trae 14 个测试全部标红:实体类和 Service 还不存在
4. 编写实现 Trae 生成状态枚举、转换规则 Map、Service、Controller
5. 测试通过(Green) Trae 14 个测试全部绿色
6. 审查调整 发现“已离职→待入职”重新入职路径缺失,补充转换规则;调整异动日志的 JSON 存储格式
7. 前端页面 Trae 生成状态转换按钮、Timeline 组件、确认弹窗
8. 联调验证 Swagger 调接口验证完整流转,确认前端正确渲染
9. Git 提交 Trae git commit -m "feat: 完成员工状态机功能"

从启动到提交,一个完整功能约 39 分钟。 其中 Trae 编码加调整共用了约 20 分钟,我审查调整和验证共用了约 19 分钟。接近五五开。

这不是特例。在整个 24 个任务中,这个比例非常稳定:Trae 负责“写出来”,我负责“写对了”


第二部分:TDD 实战剖面

上一节是广度,这一节是深度。用一个真实案例,还原 Red-Green-Refactor 在“人+AI”协作模式下的真实样貌。

在这里插入图片描述

2.1 Red:测试先行

任务开始,我在执行计划里写道:“实现用户管理,密码必须 BCrypt 加密存储”。

Trae 在执行时先生成了测试:

@Test
void create_ValidUser_PasswordIsBCryptEncoded() {
    UserDTO dto = new UserDTO();
    dto.setUsername("testuser");
    dto.setPassword("123456");
    dto.setStatus(1);

    userService.create(dto);

    SysUser saved = userMapper.selectOne(
        new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, "testuser")
    );
    assertNotEquals("123456", saved.getPassword());
    assertTrue(saved.getPassword().startsWith("$2a$"));
    assertNotNull(saved);
}

测试有三个断言:

  1. 密码不能是明文 "123456"
  2. 密码必须是 BCrypt 格式(以 $2a$ 开头)
  3. 用户能被正常查询到

此时 UserService 还不存在,测试自然是红的。这个阶段只花了不到 2 分钟,Trae 写测试比人快得多,因为在设计的时候已经定义了测试场景“应该覆盖什么”,它会自动填充边界条件和异常情况。


2.2 Green:最小实现

测试通过后,Trae 开始写实现:

@Override
@Transactional(rollbackFor = Exception.class)
public void create(UserDTO dto) {
    if (!StringUtils.hasText(dto.getPassword())) {
        throw new BusinessException("密码不能为空");
    }
    LambdaQueryWrapper<SysUser> query = new LambdaQueryWrapper<>();
    query.eq(SysUser::getUsername, dto.getUsername());
    if (userMapper.exists(query)) {
        throw new BusinessException("用户名已存在");
    }
    SysUser user = new SysUser();
    user.setUsername(dto.getUsername());
    user.setPassword(passwordEncoder.encode(dto.getPassword()));
    user.setEmployeeId(dto.getEmployeeId());
    user.setStatus(dto.getStatus());
    user.setPwdReset(0);
    userMapper.insert(user);
}

这段代码有几个值得注意的点:

密码加密在 Service 层,而非 Controller 层。 这是一个关键点,Service 层是业务逻辑的边界,密码加密是业务行为而非 HTTP 行为。Trae 在这一点上判断正确。

用户名唯一性校验在插入前完成。 没有依赖数据库唯一索引报错来兜底,而是主动查询+抛业务异常。这意味着前端能拿到明确的错误信息("用户名已存在"),而不是一个 SQL 异常。

pwdReset 字段初始化为 0。 标记该密码未被强制重置,这点在实际开发的时候有可能被忽略,但 Trae 没忘。因为它读了项目的实体类定义,知道有这个字段。


2.3 Refactor:我发现的问题

测试全部通过后,我开始审查。发现了两个问题:

问题一:更新用户时,密码不应该被静默修改。

Trae 最初生成的 update() 方法中包含了密码设置逻辑,如果 DTO 传了 password 字段就更新密码。这是不合理的:用户信息的编辑页面不应该同时承担“改密码”的职责。密码修改应该有独立的接口(resetPassword),有独立的审计日志。

我的调整:从 update() 中移除密码字段的处理,让 update 只修改基本信息,如用户名、关联员工和状态。密码修改走独立的 changePassword 流程。

问题二:密码强度没有服务端校验。

Trae 的测试密码都是 "123456",测试通过不代表实际上线没问题。我要求增加密码复杂度校验: 8 位、包含字母和数字。这个校验逻辑放在 create()resetPassword() 两个入口中。

if (dto.getPassword().length() < 8 || 
    !dto.getPassword().matches(".*[a-zA-Z].*") || 
    !dto.getPassword().matches(".*[0-9].*")) {
    throw new BusinessException("密码长度至少8位,需包含字母和数字");
}

2.4 这个案例说明了什么?

  1. Trae 严格遵循 TDD 流程。 测试→实现→通过,没有跳步、没有偷懒。我们有时候会想“这个太简单了不用写测试”,但只要你告诉 AI 说我们要按照 TDD 的流程执行开发, AI 就不会偷懒。
  2. Trae 不会主动思考“这个功能该不该放在这里”。 它按规范写代码,但“update 方法该不该改密码”这种设计层面的判断,仍然需要人来做。
  3. Refactor 阶段的审查,是人的核心价值所在。 Trae 给你的是一个“功能正确”的版本,而你要把它变成“设计合理”的版本。

第三部分:分歧与纠错

如果说 TDD 展示的是“正常流程下的协作模式”,那这一节展示的是“出问题的时候怎么办”。

3.1 场景一:“太复杂了,简化它”

问题:Day 3 做“请假审批流程”时,Trae 生成了一个完整的工作流引擎:LeaveApprovalWorkflow 类包含了多级审批链、条件分支、会签/或签、超时自动处理等逻辑。

我跟它说:“太复杂。

实际情况是:这个系统的请假审批只需要“提交 → 直属上级审批 → 通过/拒绝”三段式。不需要多级审批链,不需要会签,不需要超时自动处理。

我的处理:让 Trae 把 LeaveApprovalWorkflow 整个类删掉,审批逻辑简化为一个 approve() 方法和一个 reject() 方法,放在 LeaveService 中。

// 简化后的审批逻辑,只有一个方法
public void approve(Long applicationId, Long approverId) {
    LeaveApplication app = getById(applicationId);
    if (app.getStatus() != LeaveStatus.PENDING) {
        throw new BusinessException("该申请已处理");
    }
    app.setStatus(LeaveStatus.APPROVED);
    app.setApproverId(approverId);
    app.setApprovedAt(LocalDateTime.now());
    leaveApplicationMapper.updateById(app);
    leaveQuotaService.deduct(app.getEmployeeId(), app.getLeaveType(), app.getDuration());
}

教训:AI 倾向于“完整方案”。你让它做审批,它给你工作流引擎。你让它做权限,它给你 RBAC+ABAC+ACL 全套。我们要会掌舵。
在这里插入图片描述


3.2 场景二:“跑不通,一起 debug”

问题:Day 4 做薪资公式引擎时,公式 "基本工资 + 绩效 - 缺勤扣款" 在界面操作正常,在单元测试中也正常,但批量计算 100 个员工时,有 3 个员工的结果是 null

排查过程:

  1. 先看数据库,这 3 个员工的 att_month_summary 记录存在,但 late_countearly_count 字段都是 NULL 而非 0
  2. 再看种子数据生成逻辑,Trae 生成的 SQL 中对于“全勤员工”的 late_countearly_count 设为了 NULL,而不是 0
  3. 公式引擎在解析时,NULL 被原样代入 SpEL 表达式,导致整个计算结果变成 null

修复:两处改动:种子数据 SQL 中加 COALESCE(late_count, 0),公式引擎中加空值保护:

// 公式引擎中加空值保护
variables.put("lateCount", Objects.requireNonNullElse(monthly.getLateCount(), 0));
variables.put("earlyCount", Objects.requireNonNullElse(monthly.getEarlyCount(), 0));
variables.put("absentDays", Objects.requireNonNullElse(monthly.getAbsentDays(), 0));

教训:AI 生成代码时对“缺失数据”的处理有时候会假设数据是干净的。生产环境数据很有可能存在异常,NULL、空字符串、负数、超长文本,什么都有。边界条件的防护,需要我们来提醒它要注意。


3.3 场景三:“为什么不用批量操作?”

问题:Day 3 做“月考勤汇总”时,批量重算 100 个员工的月度考勤耗时 5 秒,超过了我的预期。

排查发现,MonthlyAttendanceService 的批量计算方法长这样(简化版):

// 问题代码:N+1 查询
for (Long empId : employeeIds) {
    List<AttDailyResult> dailyResults = dailyResultMapper.selectByEmployeeAndMonth(empId, year, month);
    // 对每个员工单独查一次数据库
    MonthlyAttendance summary = calculateSummary(dailyResults);
    monthSummaryMapper.insert(summary);
}

100 个员工 = 100 次数据库查询 + 100 次插入。这就是典型的 N+1 问题。

修复:改为批量查询 + 批量插入:

// 批量查询所有员工的日结果
List<AttDailyResult> allDailyResults = dailyResultMapper.selectByMonth(year, month);
// 按员工 ID 分组
Map<Long, List<AttDailyResult>> grouped = allDailyResults.stream()
    .collect(Collectors.groupingBy(AttDailyResult::getEmployeeId));
// 批量计算
List<MonthlyAttendance> summaries = grouped.entrySet().stream()
    .map(e -> calculateSummary(e.getValue()))
    .toList();
// 批量插入(自定义扩展方法,BaseMapper 不提供 insertBatch)
monthSummaryMapper.insertBatch(summaries);

改完后,100 个员工的批量计算从 5 秒降到 1 秒

教训:Trae 写业务逻辑时,有时候没有“性能意识”。它的关注点是“逻辑正确”,至于这个循环体里是一次 SQL 还是两次 SQL,它不关心。我们要发现问题,然后让它改正。
在这里插入图片描述


第四部分:规范执行的“隐形价值”

前三部分讲的是“看得见的协作”,任务怎么推、TDD 怎么走、Bug 怎么修。这一节想聊聊那些“你不容易感觉到,但一旦没有就会很痛苦”的事。

4.1 为什么你不需要检查 Swagger 注解漏没漏?

在以往的团队开发中,Code Review 有一项固定动作:“这个接口的 Swagger 注解写了吗?”

云山HR 有 100+ 个 API 接口,按照传统开发模式,总有那么几个接口会漏掉 @Operation 注解。然后测试跑来跟你说“这个接口文档里怎么没有?”,你翻开代码一看,确实忘了,然后再补上。

但在 Trae 开发模式下,这个问题不存在。项目规范中写了:

“所有 Controller 类必须添加 @Tag 注解,所有 API 方法必须添加 @Operation 注解”

Trae 每次生成 Controller 时,都会自动补齐这些注解。不是“大部分时候补齐”,每次都补齐。因为规范对 AI 来说是“指令”而不是“建议”。


4.2 为什么代码风格始终一致?

一个 24 个任务的项目,如果分给 3 个开发人员写,大概率会出现三种不同的代码风格:有人用 @Autowired 有人用构造器注入,有人返回 Result<T> 有人返回 ApiResponse<T>,有人异常抛 RuntimeException 有人抛 BusinessException

但云山HR 的代码风格从头到尾完全一致,因为只有“一个开发人员”在写:Trae。它每次写代码前都加载同一份规范,都使用同一个模板。这种一致性不是靠 Code Review 追回来的,是从源头上就保证了的。

以上其实是一些被忽略的隐形价值。开发结束后,你可能会觉得“这代码写得很符合我制定的规范”,但你可能意识不到,这些规范是因为 Trae 强制执行才没成为后来的“技术债”。


4.3 500 行检查清单不是摆设

开发过程中,我每天都会对照一次 checklist.md。不是人工逐条核对,而是让 Trae 扫描一遍,报告哪些项还没完成。

举几个印象最深的检查项:

“密码 BCrypt 加密实现完成,确认数据库中不存储明文密码”

“分页接口:确认入参 page 从 1 开始,size 默认 10 最大 100”

“日考勤计算引擎 TDD 流程完成:包含正常打卡、迟到早退、旷工缺卡、多打卡记录、节假日五种场景”

如果没有这个清单,后两条大概率会被遗漏,人在高强度开发中,总会忘记某些边界条件。但清单在那里,对着逐一扫描一遍就知道缺了什么。


在这里插入图片描述


结语

总结一下这 5 天的核心感受:

维度 Trae 擅长的 我们必须做的
速度 代码生成极快,CRUD 出码迅速 设计决策:功能该不该放在这里
一致性 严格按规范写,风格统一 边界条件:NULL、空值、异常输入怎么办
完整性 不会忘记注解、不会漏测试步骤 简化:去掉 AI 过度设计的部分
性能 逻辑正确优先 性能优化:N+1、缓存、SQL 调优
规范执行 100% 执行,不会偷懒 规范本身的设计和迭代

AI 写得快,但我们在审查、调试和优化中的作用不可替代。 这个结论不是感悟,是 5 天 24 个任务反复验证的结果。

下一篇文章将进入业务和技术的深水区:考勤引擎、状态机和公式引擎,这三个模块是云山HR 最复杂的业务逻辑,也是最能体现 Trae 的能力和短板的地方。

Logo

AtomGit AI 社区提供模型库、数据集、Agent、Token等资源

更多推荐