第2篇:分层架构、四层权限、状态机——我和 Trae 一起做了哪些架构决策
这是我与Trae SOLO协作开发HRM系统的第二篇。本文记录了我与AI共同设计HRM系统架构的全过程。经典的前后端分层架构、纵深的四层权限控制、规范的数据库建模,以及严谨的员工状态机流转。我通过与AI工具Trae的协作,在保证系统可扩展性的同时,避免了过度设计,体现了"够用、清晰、可扩展"的架构原则。
引言
架构本身并无绝对的对错之分,关键在于是否契合当下的场景。 一个个人作品项目的架构,没必要一上来就照着企业级分布式系统去搭,它只需要在可预见的范围内做出合理的选择,并留好演进的路径。
上一篇完成了环境搭建、PRD 制定以及计划安排。这一篇,将深入探讨这套系统的核心骨架,包括如何进行分层设计、权限控制以及状态转换。这些设计决策不是 Trae 单方面拍板的,而是我确定了基本方向之后,Trae 参与讨论、给出建议、帮我落地,几轮推敲之后才最终定下来的。
第一部分:整体架构——前后端怎么分、各层怎么划
1.1 后端四层:Controller → Service → Mapper → DB
Trae SOLO 搭建项目骨架时,采用经典的 MVC 四层架构。它帮我绘制目录结构,并将每一层的职责边界写入项目的 Rule 文件,以确保后续代码生成严格遵循该约定。不花哨、不创新,但每一层都做了严格隔离:Controller → Service → Mapper → DB,详见下文图 [前后端分层架构图]。
每一层都有明确的职责边界:
| 层 | 允许的行为 | 禁止的行为 |
|---|---|---|
| Controller | 参数校验、调用 Service、返回 Result | 直接操作 Mapper、执行业务逻辑 |
| Service | 业务编排、事务控制、调用 Mapper | 直接处理 HTTP 请求/响应 |
| Mapper | 数据库操作、SQL 映射 | 编写业务逻辑、跨表事务编排 |
分层这事很常见,但难就难在一直都严格按分层规则来做。 在审查时,从没发现 Controller 偷偷写 SQL,也没发现 Service 直接去操作 HttpServletRequest,因为分层规范一开始就写进了规则里,Trae 每次生成代码都会自动照着这个规则来。
后端包结构按照业务模块组织:
| 模块 | 核心实体 | 职责 |
|---|---|---|
modules/system |
SysUser, SysRole, SysMenu | 用户、角色、菜单管理 |
modules/org |
HrEmployee, OrgDepartment | 组织架构、员工管理、状态机 |
modules/attendance |
LeaveType, LeaveQuota, AttPunchRecord, AttDailyResult | 假期类型、假期额度、打卡、日考勤、月考勤汇总 |
modules/salary |
SalaryItem, SalaryRecord | 薪资科目、核算、公式引擎 |
modules/recruitment |
RecruitmentCandidate, Position | 候选人管理、招聘看板 |
再加上 common 包下的统一返回体 Result<T>、全局异常处理、Excel 导入导出、安全过滤器等公共组件,一个标准的 Spring Boot 企业级项目骨架就出来了。
1.2 前端四层:表现层 → 业务逻辑层 → 数据访问层 → 基础设施层
前端分层方面,Trae 最初给出的是五层方案,即 Views → Components → API → Stores → Utils,呈线性排列。乍一看职责分得很细,但实际写代码的时候发现不对劲:Views 和 Components 本质上都属于表现层,API 负责数据,Stores 管理状态,二者归根结底都是业务逻辑的载体。硬拆成五层不仅未使架构更清晰,反而让依赖方向显得牵强。 比如那条 Views → Components → API → Stores → Utils 的链,Components 什么时候会直接调 API 了?API 和 Stores 之间明明应该是 Services 调 API、Stores 存状态,不是什么线性上下游。
于是我把它纠正回了经典四层,这是前端架构中最基础也最经得起推敲的模型:表现层 → 业务逻辑层 → 数据访问层 → 基础设施层,详见下文图 [前后端分层架构图]。
四层之间的依赖方向清晰、不绕弯:
| 层 | 允许的行为 | 禁止的行为 |
|---|---|---|
| 表现层 | 页面布局、UI 呈现、调用业务逻辑层 | 直接调 API、写业务逻辑 |
| 业务逻辑层 | 业务编排、数据转换、状态管理、调 API 层 | 直接操作 DOM、处理 HTTP 拦截 |
| 数据访问层 | HTTP 请求封装、对接后端接口 | 写业务逻辑、操作状态 |
| 基础设施层 | 纯工具函数、指令、拦截器、配置 | 写业务逻辑、持有状态 |
Trae 为何会给出五层方案?这实际上反映了 AI 的一种典型倾向,即倾向于将概念细化,认为层越多越专业。但架构不是乐高,不是拆得越散越好。Views 和 Components 本来就是表现层的两种粒度,Services 和 Stores 本来就是业务逻辑层的两种载体,四层已经足够把职责边界划清楚,再多就是过度设计了。 这件事上,我做了减法。
前端路由结构直接映射业务模块:
| 路由 | 对应模块 | 页面数 |
|---|---|---|
/dashboard |
数据看板 | 1 |
/org/* |
组织人事(员工/部门/岗位/组织架构) | 8 |
/attendance/* |
考勤管理(打卡/请假/加班/班次/日结果/月汇总/日历) | 8 |
/salary/* |
薪酬管理(科目/模板/核算/工资单) | 5 |
/recruitment/* |
招聘管理(职位/候选人/看板) | 3 |
/system/* |
系统管理(用户/角色/设置) | 3 |

1.3 我为什么选了这套分层?
前后端均采用了最为经典的方案,未涉及 DDD、微服务及 CQRS。这是我和 Trae 讨论之后,根据项目规范和业界最佳实践一起定下来的。
核心考量很简单:这是一个个人作品项目,不需要复杂架构来撑场面。 够用、清晰、可扩展,这三条就够了。
Trae 最初给出的方案更为复杂,建议引入 Redis 作为缓存,RabbitMQ 作为消息队列,理由是 这是企业级项目的标配。但我把它砍掉了。一个单实例部署的 HRM 演示项目,引入 Redis 和 MQ 只会增加部署复杂度,对实际价值几乎没有提升。架构不是越全越好,而是在当前场景下做出最合适的选择,同时为未来的演进留好空间。
所以在设计阶段,我和 Trae 一起梳理了可能的扩展路线,比如后续如果需要缓存,在哪里加 Redis 最合适;如果需要异步处理,哪些业务适合引入消息队列。这些预留接口不会增加当前的代码量,但确保了未来需要升级时,不会因为架构限制而束手束脚。
回过头来看,许多项目之所以后期变得混乱,并非架构选择有误,而是前期未做好规划与边界定义。 哪怕是一个简单的系统,前期把分层规则、命名规范、职责边界写清楚,后面就能避免大量返工和偏离轨道。Trae 在这里的价值不是想出了这些规则,而是帮我把规则写进了项目的 Rule 文件,并且在后续每一次代码生成中严格执行了它们。
第二部分:权限系统——四层设计,分阶段落地
架构搭建好后,权限系统就是保障 HRM 系统安全的关键,我和 Trae 讨论确定了一套四层设计且分阶段落地的权限体系。一个普通员工不应该看到全公司薪资,一个部门经理不能随意修改组织架构。最后确定的权限体系是完整的四层设计:API 认证、路由过滤、组件隐藏、数据隔离。
但在实际开发中,我并没有一次性全部实现。先做能立刻保护系统的(API 层),再做改善体验的(路由+组件层),最后做最复杂但对 MVP 价值最小的(数据权限层)。 下面按实现的优先级,从已完成的到还在设计中的,逐层展开。

2.1 第一优先级·已完成:API 层 — JWT + 拦截器 + Spring Security
此乃整个安全体系的基石,若无它,后续几层便如空中楼阁般无从谈起。整个后端安全体系基于 Spring Security + JWT 无状态认证:
请求流程:
浏览器
→ [axios 请求拦截器:自动附加 Bearer Token]
→ [Spring Security Filter Chain]
→ [JwtAuthenticationFilter:解析 Token,设置 SecurityContext]
→ [Controller:@Tag + @Operation 注解标注的接口]
→ [Service 层业务逻辑]
→ [响应拦截器:code≠200 → Message.error + 401→强制登出]
关键组件:
| 组件 | 文件 | 职责 |
|---|---|---|
| JWT 生成/验证 | JwtTokenProvider.java |
签发含 userId/username/roles 的 Token |
| JWT 过滤器 | JwtAuthenticationFilter.java |
每次请求从 Header 提取 Bearer Token,解析后注入 SecurityContext |
| 安全配置 | SecurityConfig.java |
禁用 CSRF、启用 CORS、STATELESS 会话、BCrypt 密码加密、放行登录/健康检查/Swagger |
| 前端拦截器 | request.ts |
axios 请求拦截自动加 Token,响应拦截统一处理 401 → 强制登出跳转 |
SecurityConfig 中放行的路径:
// 登录路径,允许所有用户访问
.requestMatchers("/api/auth/login").permitAll()
// 健康检查路径,用于监控系统状态,允许所有访问
.requestMatchers("/actuator/**").permitAll()
// Swagger相关路径,用于展示API文档,允许所有访问
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
// 其他所有请求,均需经过认证
.anyRequest().authenticated()
401 处理链路:后端返回 401 后,前端request.ts响应拦截器捕获该状态,调用userStore.logout()清空本地状态,随后通过router.push('/login')跳转至登录页。
FilterChain 顺序设计:在SecurityConfig中,Filter 的排列顺序至关重要。JwtAuthenticationFilter置于UsernamePasswordAuthenticationFilter之前,以保证 JWT 无状态认证优先于表单登录。同时,将SecurityContextHolder的策略设为MODE_INHERITABLETHREADLOCAL,为后续异步处理预留空间。整个 FilterChain 职责明确,仅从 Token 中提取身份并注入 SecurityContext 后放行,不涉及权限与角色校验,边界清晰。
状态:✅ 已完整实现。这是首要实现的部分,没有它,后续设计就无法开展。
设计预留:JWT 的扩展空间。 当前 JWT 中只存储了 userId、username、roles 三个字段,但 JwtTokenProvider.generateToken() 接受 Map<String, Object> 作为额外 claims,未来可随时向 Token 注入 dataScope、deptId 等字段,无需改动生成和验证逻辑。架构不是一步到位,而是在关键节点留好扩展口。
2.2 第二优先级·基础版已完成:路由层 — beforeEach 守卫
前端在router/index.ts中定义了路由守卫,逻辑如下:若访问路径为/login,直接放行;若不是且用户无 token,则强制跳转至登录页,否则放行。对应的代码如下:
router.beforeEach((to, _from, next) => {
const userStore = useUserStore()
if (to.path === '/login') {
next()
} else {
if (!userStore.token) {
next('/login')
} else {
next()
}
}
})
当前版本限制: 目前侧边栏导航采用硬编码方式,所有用户看到的菜单相同。完整方案是将菜单数据存储于sys_menu表,后端依据用户角色查询权限菜单,前端动态渲染侧边栏。该方案设计已完成,但在 MVP 阶段,鉴于系统角色数量有限,硬编码菜单结合 API 层鉴权已能保障安全。
状态:✅ 基础版已完成(路由守卫 + 硬编码菜单)。动态菜单方案已设计完成,计划在需要支持多角色差异化导航时实现。
2.3 第三优先级·代码就绪:组件层 — v-permission 指令
directives/permission.ts 实现了一个简洁的自定义指令:
export const permission: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const userStore = useUserStore()
// 获取指令绑定的值,即所需权限
const requiredPerm = binding.value as string
// 如果所需权限存在,且当前用户不是超级管理员,且用户菜单中不包含该权限
if (requiredPerm && !userStore.roles.includes('SUPER_ADMIN')
&& !userStore.menus.some(m => m.perms === requiredPerm)) {
el.parentNode?.removeChild(el)
}
}
}
用法很简单:<el-button v-permission="'attendance:punch'">打卡</el-button>。如果当前用户没有 attendance:punch 权限,这个按钮直接从 DOM 中移除,连 v-if 都不用手动写。
这个设计的巧妙之处在于:SUPER_ADMIN 角色绕过所有组件级权限检查,管理员永远能看到所有按钮,不需要逐项配置。
当前版本限制: 指令代码已完成,但权限判断依赖userStore.menus。因动态菜单(2.2 的升级版)尚未实现,menus当前为空数组,导致非管理员用户的所有带v - permission按钮均会被隐藏。待动态菜单接入后,该功能即可正常使用。
状态:⚠️ 代码已完成,待动态菜单数据接入后即可正常工作。当前依赖 SUPER_ADMIN 豁免逻辑保证管理员可用。
2.4 第四优先级·设计中:数据权限 — ALL / DEPT_AND_CHILD / SELF
API、路由与组件三层解决了能否访问的问题,然而,还有一个更深入的问题,即能看到哪些数据。这一层在权限系统中最为复杂,也是 MVP 阶段唯一未实现执行逻辑的部分,不过其设计方案已完全确定。
DataPermissionContext 定义了三种数据范围:
// 代表可访问全部数据
public static final String DATA_SCOPE_ALL = "ALL";
// 代表可访问本部门及子部门数据
public static final String DATA_SCOPE_DEPT_AND_CHILD = "DEPT_AND_CHILD";
// 代表仅可访问本人数据
public static final String DATA_SCOPE_SELF = "SELF";
这个数据范围绑定在角色表 sys_role 的 data_scope 字段上(字段和种子数据已就绪)。一个拥有「部门经理」角色的用户,data_scope 值为 DEPT_AND_CHILD,那么查询员工列表时,通过 MyBatis 拦截器动态注入 WHERE dept_id IN (本部门ID, 子部门ID1, 子部门ID2 ...) 的条件。
| 角色 | data_scope | 能看到的员工数据 |
|---|---|---|
| HR 管理员 | ALL | 全公司所有员工 |
| 部门经理 | DEPT_AND_CHILD | 本部门及下属部门员工 |
| 普通员工 | SELF | 仅自己的信息 |
这个机制妙就妙在它是声明式的。 无需在每个 Service 方法中编写if (role == SUPER_ADMIN) { … } else { … }这样的判断逻辑,只需配置一次数据范围,注入一次 SQL,整个模块的查询便会自动应用该规则。
为何 MVP 阶段未实现? 数据权限的实现涉及 MyBatis 拦截器、SQL 解析、部门树递归查询以及多表字段映射,是四层中最为复杂的一层。在 MVP 阶段,角色种类较少(仅管理员、部门经理、普通员工三种),数据隔离需求并不强烈。因此,相较于在早期耗费大量时间实现复杂的通用方案,先确定设计并预留接口,待多角色上线时再进行开发更为明智。
状态:📋 设计方案已完成(DataPermissionContext + sys_role.data_scope + MyBatis 拦截器方案),执行逻辑计划在后续迭代中实现。

第三部分:数据库设计精要
3.1 核心实体关系
云山HRM 共有 32 张业务表,按模块组织:
| 模块 | 前缀 | 核心表 | 数量 |
|---|---|---|---|
| 系统权限 | sys_ |
sys_user, sys_role, sys_menu, sys_user_role, sys_role_menu, sys_login_log, sys_config | 7 |
| 组织人事 | org_ / hr_ |
hr_employee, org_department, org_job_category, org_job_position, org_dept_position, hr_employee_change_log | 6 |
| 考勤管理 | att_ |
att_punch_record, att_daily_result, att_month_summary, att_shift, att_group, att_leave_type, att_leave_quota 等 | 11 |
| 薪酬管理 | sal_ |
sal_salary_item, sal_salary_template, sal_template_item, sal_salary_record, sal_salary_record_item | 5 |
| 招聘管理 | recruitment_ |
recruitment_position, recruitment_candidate, recruitment_candidate_status_log | 3 |
核心实体关系简要版:

3.2 命名规范
| 命名对象 | 规范 | 示例 |
|---|---|---|
| 表名 | {module}_{entity} |
hr_employee, att_daily_result |
| 字段名 | snake_case |
employee_no, direct_leader_id |
| 主键 | id BIGINT AUTO_INCREMENT |
所有表统一 |
| 唯一索引 | uk_{field} |
uk_employee_no, uk_username |
| 普通索引 | idx_{field} |
idx_dept_id, idx_status |
| 时间字段 | DATETIME DEFAULT CURRENT_TIMESTAMP |
created_at, updated_at |
3.3 脚本管理策略
数据库脚本没有堆在一个文件里,而是按用途拆成了 7个文件:
| 文件 | 类别 | 说明 |
|---|---|---|
db_table.sql |
建表 | 用于创建 32 张业务表,采用 InnoDB 存储引擎,字符集为 utf8mb4 |
db_data.sql |
初始数据 | 用于初始化公司、部门、员工、用户、角色、菜单、考勤种子数据 |
db_table_config.sql |
系统配置 | 用于初始化sys_config 表 + 加班换算规则 |
db_table_update.sql |
增量更新 | 后续迭代新增的表,通过 CREATE TABLE IF NOT EXISTS 安全追加 |
db_seed_salary.sql |
薪资测试数据 | 薪资科目、模板、核算记录、明细的测试数据 |
db_test_attendance.sql |
考勤测试数据 | 请假、加班、班次、考勤组、打卡记录测试数据 |
db_query_attendance.sql |
查询参考 | 10 个常用考勤查询 SQL(日考勤、月汇总、异常统计等) |
要重建表结构跑 db_table.sql,要测考勤跑 db_test_attendance.sql。每个文件职责单一,出问题也容易定位。
3.4 关键设计决策
为何选择 InnoDB + 自增主键?
InnoDB 是 MySQL 中广泛使用的存储引擎,具备事务处理、行级锁等功能,可有效保障数据的完整性与一致性,这对于 HRM 系统至关重要。
| 对比项 | InnoDB + 自增 | UUID | 雪花ID |
|---|---|---|---|
| 插入性能 | ⭐⭐⭐ 顺序写入 | ⭐ 随机写入 | ⭐⭐ |
| 索引大小 | 8字节 | 16字节 | 8字节 |
| 可读性 | ⭐⭐⭐ 直观 | ⭐ 难以辨认 | ⭐⭐ |
| 分布式兼容 | ⭐ 单机友好 | ⭐⭐⭐ | ⭐⭐⭐ |
| 选择理由 | HRM 单实例部署 + 可读性优先 → 自增主键是最佳选择 |
对于用于个人作品展示的 HRM 系统,无需考虑分库分表,自增主键凭借其简洁性与直观性占据优势。
第四部分:两个状态机的设计对比
HRM 系统的核心是状态流转。员工从入职到离职,候选人从沟通到入职,每一步都涉及状态的合法转换。
起初,我并未计划采用状态机方案。最初想法很简单,即在员工表中设置一个status字段,通过setStatus()方法更改状态。然而,Trae 在编写员工状态变更代码时提出建议,指出这种直接调用setStatus()方法缺乏约束,一旦代码出错,员工状态可能从 待入职 直接变为 已离职,导致数据异常。因此,Trae 建议使用状态机锁定合法转换路径,对非法转换直接抛出异常。
我一听有道理,就让它继续展开了。后面我们又讨论了候选人模块,发现候选人的状态流转不需要这么强的约束,于是做了两种不同风格的设计,对照看很有意思。
4.1 员工状态机:6 状态 12 规则
EmployeeStatus 枚举定义了 6 种状态:待入职、试用期、正式、待离职、已离职、已取消入职。各状态之间的合法流转路径共 12 条,详见下文图 [员工状态机流转图]。
状态转换规则用一张静态 Map 定义:
private static final Map<EmployeeStatus, Set<EmployeeStatus>> TRANSITION_RULES = new HashMap<>();
static {
TRANSITION_RULES.put(EmployeeStatus.PENDING_HIRE,
Set.of(EmployeeStatus.PROBATION, EmployeeStatus.ACTIVE, EmployeeStatus.OFFER_CANCELLED));
TRANSITION_RULES.put(EmployeeStatus.PROBATION,
Set.of(EmployeeStatus.ACTIVE, EmployeeStatus.TERMINATED, EmployeeStatus.RESIGNATION_PENDING));
TRANSITION_RULES.put(EmployeeStatus.ACTIVE,
Set.of(EmployeeStatus.RESIGNATION_PENDING, EmployeeStatus.TERMINATED, EmployeeStatus.PROBATION));
TRANSITION_RULES.put(EmployeeStatus.RESIGNATION_PENDING,
Set.of(EmployeeStatus.ACTIVE, EmployeeStatus.TERMINATED));
TRANSITION_RULES.put(EmployeeStatus.TERMINATED,
Set.of(EmployeeStatus.PENDING_HIRE));
}
变量命名说明: 上述代码为最终修正版本。Trae 最初生成的枚举名存在中式英语问题,例如
PENDING(待入职,语义不明)、REGULAR(意为常规的,并非正式的)、PENDING_LEAVE(待离职,中式直译)、LEFT(已离职,left为动词过去式,作为形容词不专业)。我将其修正为PENDING_HIRE、ACTIVE、RESIGNATION_PENDING、TERMINATED,这些命名更易于开发者理解。可见,Trae 生成代码质量尚可,但命名方面需人为把关。
此外,这套规则并非 Trae 一次编写正确,其最初版本存在两个问题:
- 待入职 → 已离职:将拿到 offer 但尚未正式入职的人员标记为"已离职",这种情况并不合理。因为"离职"意味着员工已经在公司任职过,而待入职人员并未开始工作。修正方案是新增
OFFER_CANCELLED(已取消入职)状态,当 offer 撤回时,状态变更为PENDING_HIRE→OFFER_CANCELLED,以此与真正的离职状态彻底区分开来。 - 待离职缺少撤销路径:Trae 最初遗漏了待离职状态撤销离职并回到正式状态的路径,我补充了
RESIGNATION_PENDING → ACTIVE这一状态转换路径。
经过修正,最终确定为 6 个状态、12 条规则,每一条规则在业务层面都经得起仔细考量。以下是最终确认的完整转换表:
| from → to | 含义 | 触发的异动类型 |
|---|---|---|
| 待入职 → 试用期 | 员工正式入职 | HIRED(入职) |
| 待入职 → 正式 | 直接入职为正式员工 | HIRED(入职) |
| 待入职 → 已取消入职 | 撤销 offer | OFFER_CANCELLED(取消入职) |
| 试用期 → 正式 | 转正 | CONFIRMED(转正) |
| 试用期 → 待离职 | 试用期发起离职 | RESIGNED(发起离职) |
| 试用期 → 已离职 | 试用期离职 | TERMINATED(离职) |
| 正式 → 正式 | 员工异动 | TRANSFERRED(异动) |
| 正式 → 待离职 | 发起辞职 | RESIGNED(发起离职) |
| 正式 → 已离职 | 解聘 | TERMINATED(离职) |
| 待离职 → 正式 | 撤销离职 | WITHDRAWN(撤销离职) |
| 待离职 → 已离职 | 离职生效 | TERMINATED(离职) |
| 已离职 → 待入职 | 重新入职 | REHIRED(重新入职) |

4.2 为什么不用 GoF 状态模式?
GoF 状态模式将对象状态封装为独立类,通过类间切换改变对象行为。但对于仅有 6 个状态和 12 条规则的员工状态机而言,使用该模式实属杀鸡用牛刀。
| 方案 | 类数量 | 代码量 | 新人理解时间 |
|---|---|---|---|
| GoF 状态模式 | 6 个状态类 + 1 个 Context | ~240 行 | 15 分钟 |
| 静态 Map | 1 个枚举 + 1 个 Map | ~40 行 | 30 秒 |

4.3 审计与可追溯性
状态机不仅需具备防错功能,还应具有可追溯性。每次状态转换时,EmployeeTransitionServiceImpl.transition() 方法在更新状态后,会将变更前后的数据以 JSON 格式写入 hr_employee_change_log 表。如此一来,任何时候都能够追溯任意员工的状态变更历史,包括操作人、操作时间以及状态变化情况。
之所以选择 JSON 快照而非字段级日志,是由于员工数据结构复杂,涵盖部门、职位、薪酬等多维度信息。JSON 能够完整还原变更前后的全貌,相比之下,字段级日志只能呈现零散的变更点。
状态机的具体实现细节,将在后面的文章中详细阐述。此处仅探讨架构层面的设计决策。
第五部分:Trae 在架构决策中的表现点评
至此,对第二篇中 Trae 在架构决策过程中的真实表现进行总结:
采纳的部分 ✅
| 决策 | Trae 做了什么 | 为什么直接采纳 |
|---|---|---|
| 后端四层 MVC | 严格按照规范生成 Controller-Service-Mapper 分层骨架 | 标准做法,没有歧义 |
| JWT + Spring Security | 标准 JWT 无状态认证 + BCrypt 加密 | 安全实践成熟,无需调整 |
| 数据库命名 | module_entity / snake_case / 索引命名 |
规范先行,一致性高 |
| 状态机 Map 方案 | 用静态 Map 替代 GoF 状态模式 | 够用就好 |
| 审计日志 JSON 快照 | oldData/newData 存为 JSON | 结构化追溯,比字段级日志更灵活 |
调整了的部分 ⚠️
| 调整点 | Trae 原来写的 | 我改成了什么 | 原因 |
|---|---|---|---|
| 整体架构方案 | 提议加入 Redis 缓存 + RabbitMQ 消息队列 | 全部砍掉,保持单实例部署的简洁性 | 个人作品无需中间件,增加部署复杂度无实际价值 |
v-permission 指令 |
初始版本对所有角色生效 | 加了 SUPER_ADMIN 豁免逻辑 |
管理员不应被按钮权限限制 |
| 前端请求响应拦截 | 初始只处理了 token 附加 | 补充了 responseType === 'blob' 判断和 401 强制登出 |
Excel 下载是 blob 流,不走统一返回体 |
| 员工状态枚举命名 | PENDING / REGULAR / PENDING_LEAVE / LEFT |
修正为 PENDING_HIRE / ACTIVE / RESIGNATION_PENDING / TERMINATED,并新增 OFFER_CANCELLED |
Trae 生成的变量名中式英语严重,LEFT 等命名不专业,需人工修正 |
| 前端分层 | 给出了五层结构(Views → Components → API → Stores → Utils),线性箭头暗示严格的依赖链 | 纠正为经典四层(表现层 / 业务逻辑层 / 数据访问层 / 基础设施层),每层内可包含多个子模块 | 五层是对经典四层的过度拆分,且线性箭头在实际开发中经不起推敲,Components 不会直接调 API,API 和 Stores 也不是上下游关系 |
架构层面 Trae 的能力边界
就本次项目来看,Trae 在架构层面呈现出三个显著优势与两个不足
优势:
- 规范执行力:只要规范明确,Trae 便能严格遵循,不会遗忘、懈怠或偏离。例如,Controller 均按要求添加了
@Tag注解,SQL 命名也全部采用了snake_case格式。。 - 模式识别:Trae 能够依据项目规模,自动挑选适宜的设计模式。如针对仅有 6 个状态的状态机,选用静态 Map 而非 GoF 状态模式,避免了不考虑实际场景而盲目套用复杂模式的情况。
- 一致性保障:在 4 个模块、32 张表以及 100 + 个 API 的整个项目中,Trae 确保了命名风格、包结构以及分层边界始终保持统一。
不足:
- 业务理解不够深入:Trae 虽知晓如何搭建架构,却未能充分理解为何此业务场景需要更严格的约束。例如员工状态机中,像待入职直接标记为已离职这种非法转换路径,便是由我发现并纠正的,Trae 起初并未察觉到该业务问题。
- 方案易过度设计:Trae 倾向于一开始就提供一套企业级标配方案,诸如 Redis、MQ、分布式锁等一应俱全。然而,在实际项目的起步阶段,这些往往并非必需。这就需要人为判断并筛选,去除冗余部分,前面提及的 Redis 和 RabbitMQ 便是典型示例。
结尾
至此,架构篇已讲解完毕,我们探讨了这套系统的核心架构内容,包括前后端的分层方式、权限的四层纵深控制机制、数据库的建模方法以及状态的流转过程。
回顾整个过程,Trae 在这些架构决策中并非主导者,却是最佳执行者。但 Trae 在确保架构一致性、维持命名规范性以及实现安全配置完整性等方面,发挥了重要作用。
下一篇,我们将聚焦于 5 天内我与 Trae 推进 24 个任务的过程,探讨 TDD 如何落地实施、遇到分歧怎样解决,以及哪些陷阱必须亲身经历才能避免。
更多推荐


所有评论(0)