引言

真正有技术含量的系统,不是看框架堆砌得有多漂亮,而是看复杂业务逻辑的设计是否经得起推敲。

前面三篇完整梳理了项目规划、整体架构与AI协作开发流程。这一篇进入技术深水区,云山HRM 中复杂度最高、最能体现业务设计能力的三大核心模块:考勤规则引擎、员工异动状态机、薪资公式引擎。
这三类模块和普通CRUD业务有着本质区别:不只是简单的增删改查,核心围绕多状态判定、规则校验、数据联动,属于典型的“能跑起来很简单,跑对、跑稳、覆盖全场景很难”的业务。

员工状态机的架构选型(6 状态 12 规则、静态 Map 替代 GoF 状态模式)见第2篇,本篇第二部分只补实现细节,不重复架构论证。单篇转载的读者从本篇读起不影响理解,涉及前文处均有提示。

在这里插入图片描述


第一部分:考勤规则引擎

考勤是 HRM 系统的第一个“硬骨头”。看起来只是记一下上下班时间,实际上要解决一连串边界问题:什么叫迟到?迟到了但下班晚了怎么算?一天打了四次卡怎么办?夜班跨天了怎么算?

1.1 设计理念:打卡即算,不等定时任务

传统考勤系统的做法是每天凌晨跑一个定时任务,批量计算前一天的考勤结果。这个方案有个天然缺陷,员工打完了卡,不知道今天的考勤状态是“正常”还是“迟到”,要等到第二天才能看到。

Trae 给出的设计方案不同,采用实时重算的方式。
AttPunchRecordServiceImpl.punch() 方法的关键逻辑:

// 1. 写入打卡记录
punchRecordMapper.insert(record);
// 2. 同步重算当日结果
AttDailyResult result = recalculateDailyResult(empId, punchDate);
// 3. 更新日结果
dailyResultMapper.insertOrUpdate(result);

打卡动作本身触发了考勤结果的重算。员工打卡的同时,他的当日考勤状态就已经确定了,正常、迟到、早退、缺卡、旷工状态实时可见。

对比维度 定时任务方案 实时重算方案(Trae)
结果可见性 次日才能看到 打卡后立即可见
系统依赖 依赖定时任务不挂 无额外依赖
打卡链路 短(只写记录) 长(写记录+重算+更新)
适合场景 大规模批量处理 中小规模+体验优先

此方案的代价是单次打卡链路变长了,多了一次重算逻辑和一次数据库写入。对于这个项目规模(100 个员工),这个代价完全可以接受。后续如果需要优化,可把重算改为异步消息处理。


1.2 判定算法:一张决策树说清楚

考勤判定的核心逻辑,Trae 收敛成了一棵清晰的决策树:

在这里插入图片描述

这个算法有三个关键设计点值得展开:

只校验迟到早退,不校验早到晚走:判定规则只看「晚到」和「早走」两个方向。firstIn < startTime(提前到岗)和 lastOut > endTime(加班晚走)不影响考勤状态,早到和加班不属于违规,自然不进入判定逻辑。

状态可以组合"迟到,早退" 是一个合法的状态。一个员工可能早上起晚了迟到、下午有急事早退,两种违规在同一天发生,状态应该如实记录,而不是取「最严重的那个」。

多打卡记录处理:一天可能打多次卡,中午出去吃饭打了一次出门卡、吃完饭回来又打了一次进门卡。引擎取第一条 IN 记录和最后一条 OUT 记录作为当日有效打卡时间,中间反复出入不算。该边界场景为初版遗漏,在后续的代码审查环节补充完善。如果后期业务上需要扣除中途外出的时间,也只需要在这条计算规则上扩展即可,决策树的其他分支不受影响。

List<AttPunchRecord> records = punchRecordMapper.selectByEmployeeAndDate(empId, date);
// 取当日第一条上班打卡记录
AttPunchRecord firstIn = records.stream()
    .filter(r -> r.getPunchType() == PunchType.IN)
    .min(Comparator.comparing(AttPunchRecord::getPunchTime)).orElse(null);
// 取当日最后一条下班打卡记录
AttPunchRecord lastOut = records.stream()
    .filter(r -> r.getPunchType() == PunchType.OUT)
    .max(Comparator.comparing(AttPunchRecord::getPunchTime)).orElse(null);

1.3 从「日结果」到「月汇总」的数据流

日考勤结果存在 att_daily_result 表中,精确到每个员工每一天。但薪酬计算需要的是月度汇总:一个月正常出勤了多少天、迟到了几次、旷工了多少天。

在这里插入图片描述

两张表的分工很清晰:

粒度 字段示例 作用
att_daily_result 员工 × 日期 考勤状态、首次打卡时间、末次打卡时间 精确记录每天的考勤状态
att_month_summary 员工 × 年月 正常出勤天数、迟到次数、早退次数、旷工天数、请假天数、加班时长 月度聚合,薪酬计算的数据源

月汇总的生成逻辑很直接,按员工 + 年月分组,对日结果做聚合统计:

monthly.setNormalDays((int) dailyResults.stream()
    .filter(d -> d.getStatus() == AttendanceStatus.NORMAL).count());
monthly.setLateCount((int) dailyResults.stream()
    .filter(d -> d.getStatus().toString().contains("LATE")).count());
monthly.setEarlyCount((int) dailyResults.stream()
    .filter(d -> d.getStatus().toString().contains("EARLY")).count());
monthly.setAbsentDays((int) dailyResults.stream()
    .filter(d -> d.getStatus() == AttendanceStatus.ABSENT).count());

关键设计细节:统计迟到、早退次数时,采用 contains 模糊匹配而非精确匹配,核心是兼容迟到+早退的组合状态,确保单日双重违规时,两类异常次数均能正常累计,保证月度统计数据无遗漏。代码层展示为英文枚举,中文状态仅用于前端展示与阅读。

除此之外,系统设计了月度考勤锁定机制is_locked 字段用于标记当月考勤数据状态。锁定前,HR可补录打卡记录、修正异常考勤并重新聚合月度数据;锁定后,当月数据冻结为薪资核算快照,禁止任何修改,彻底避免“薪资已发放、考勤数据异动”的业务事故。


1.4 可改进的短板

真实的项目设计不存在完美方案,正视短板、记录可优化点,才是真实的技术复盘,也为后续迭代留存清晰方向。

宽限期是硬编码的 5 分钟AttShift 表有 flexStartflexEnd 字段,设计意图是让每个班次可以自定义宽限期,但当前代码中这个字段没有被读取,所有班次用的都是硬编码的 5 分钟。

跨天班次未实现。夜班场景(如 22:00 - 06:00)在现实中很常见。AttShift 表预留了 isCrossDay 字段,但考勤引擎的判定逻辑没有处理跨天情况。22:00-次日06:00等跨天班次打卡记录,会被拆分至两个自然日统计,无法精准核算夜班考勤。

批量重算的 N+1 问题。早期月度批量重算逻辑中,遍历员工后单独查询单人员考勤数据,100名员工即产生100次数据库查询,单次核算耗时5秒。后续通过批量查询优化,耗时降至1秒,但该优化为人工兜底修复,并非初始设计最优解。

月度出勤天数未剔除节假日、周末。当前出勤天数统计直接取用当月自然天数,未剔除周六、周日及法定节假日,会导致出勤率、有效出勤统计存在偏差,影响薪资与考勤报表精准度。


第二部分:员工异动状态机

在这里插入图片描述

第2篇 已介绍员工状态机的核心架构:6 个状态、12 条规则、静态 Map 替代 GoF 状态模式,以及初版5状态9规则的迭代优化过程。本节将补齐核心实现细节,详解一次 transition() 状态调用背后的完整执行逻辑。

员工状态机是组织人事模块的核心,管控员工从入职、转正、在职、离职的全生命周期状态。核心是杜绝非法状态流转:已离职员工无法直接恢复在职状态、已取消的入职Offer无法直接复活,所有状态变更均有规则可依、有记录可查。

2.1 一次状态转换的五步链路

HR 在界面上点下“转正”按钮,后端 EmployeeTransitionServiceImpl.transition() 方法严格执行五步链路,保证状态变更安全、合规、可追溯:

① 参数校验:校验员工信息有效性、目标状态合法性,拦截无效请求
② 规则校验:匹配预设流转规则,仅允许合法状态切换
③ 类型推断:根据前后状态自动判定异动类型,无需前端传参
④ 状态更新:数据库落地最新员工状态
⑤ 审计落库:留存状态变更前后完整数据快照,用于溯源审计

其中规则校验、自动类型推断、审计快照是核心设计亮点,也是区别于普通状态更新的关键。

2.2 规则校验:静态 Map 锁死所有合法流转路径

系统通过 Map<EmployeeStatus, Set<EmployeeStatus>> 静态规则表,统一管控所有合法状态流转,核心校验逻辑仅一行,简洁且高效:

Set<EmployeeStatus> allowed = TRANSITION_RULES.get(from);
if (allowed == null || !allowed.contains(to)) {
    throw new BusinessException("非法的状态转换: " + from + " → " + to);
}

状态机的核心本质,不是支持多少种合法流转,而是彻底杜绝所有非法流转。所有不在规则表中的状态变更,都会直接抛出明确的业务异常,避免静默数据异常。

这张规则表还有两个值得注意的细节:

  • 规则表被 static final 修饰,类加载时一次性初始化,运行期不可修改,保证规则稳定性;
  • OFFER_CANCELLED(已取消入职) 为终态,未作为任何流转规则的前置状态,Offer取消后无法复活,需重新发起流程。

2.3 三个工程亮点

在这里插入图片描述

异动类型自动推断,减少传参出错风险

系统无需前端传入异动类型,仅通过变更前后的状态,自动匹配对应的人事异动场景,8行代码全覆盖12条流转规则,精简且严谨:

private String determineTransitionType(EmployeeStatus from, EmployeeStatus to) {
    if (to == OFFER_CANCELLED) return "OFFER_CANCELLED";
    if (from == PENDING_HIRE) return "HIRED";
    if (from == PROBATION && to == ACTIVE) return "CONFIRMED";
    if (to == RESIGNATION_PENDING) return "RESIGNED";
    if (from == RESIGNATION_PENDING && to == ACTIVE) return "WITHDRAWN";
    if (to == TERMINATED) return "TERMINATED";
    if (from == TERMINATED) return "REHIRED";
    return "TRANSFERRED";
}

通过状态范围兜底,一条逻辑可覆盖多类相似场景,大幅减少接口参数,降低前后端联调出错概率。

多格式参数兼容,提升接口容错性

适配前端不同传参习惯,支持英文枚举名、数字编码、中文全称、中文别名四种输入格式,自动解析匹配对应状态,大幅提升生产环境接口稳定性:

public static EmployeeStatus fromString(String status) {
    String trimmed = status.trim().toUpperCase();
    try { return EmployeeStatus.valueOf(trimmed); }        // 兼容英文枚举名,如:"PROBATION"
    catch (Exception e) { /* continue */ }
    try { return fromCode(Integer.parseInt(trimmed)); }     // 兼容数字编码,如:"1"
    catch (Exception e) { /* continue */ }
    return fromDescription(trimmed);                        // 兼容中文名称/别名,如:"试用期" / "试用"
}

注:文中通用异常捕获为行文精简,实际代码已优化为精准捕获对应异常,避免静默吞掉未知错误,保证异常可监控、可排查。

全事务审计快照,变更全程可追溯

第2篇提过为什么选 JSON 快照而不是字段级日志,员工数据结构复杂,快照能完整还原变更前后的全貌。通过 hr_employee_change_log 表留存每一次状态变更的完整信息:

字段 类型 说明
employee_id BIGINT 被操作的员工
operator_id BIGINT 操作人
from_status VARCHAR 变更前状态
to_status VARCHAR 变更后状态
change_reason VARCHAR 变更原因
old_data TEXT (JSON) 变更前员工信息快照
new_data TEXT (JSON) 变更后员工信息快照
created_at DATETIME 操作时间

状态更新与日志落库绑定同一事务,保证数据一致性,杜绝“状态改了无日志、日志写了状态未更新”的问题。

2.4 测试用例兜底,守住所有流转规则

状态机属于规则密集型模块,任何规则变更都必须由测试用例兜底,避免迭代引入Bug。项目采用「正向全覆盖、反向抓典型」的测试策略:

用例类型 覆盖范围 数量
正向用例 12 条合法路径,每条一个 @Test 12
反向用例 典型非法转换(待入职→已离职、已离职→正式、已取消入职→任意状态) 5+

测试用例与规则表完全镜像同步:新增一条流转规则,必新增对应正向用例;删除一条规则,对应用例立即失效标红。整套用例从初版9条规则迭代至今,完美适配状态机的每一次优化升级,是模块稳定运行的核心保障。

这块也有已知边界:TRANSITION_RULES 是类加载时写死的静态表,业务规则变更(比如新增「停薪留职」状态)仍需改代码重新发版,没有热更新能力;规则校验只是单线程读静态 Map,当前无性能压力,但也意味着没有动态扩展的口子。对人员规模百人级的场景够用,再往上需要重新评估。


第三部分:薪资公式引擎

3.1 薪资计算的核心难点

我们经常会认为,薪资计算只是简单的“基本工资+绩效-扣款”,但真实企业薪资场景复杂度极高,核心存在三大痛点,也是公式引擎的设计初衷:

薪酬结构不固定。每个公司、甚至同一个公司的不同岗位,薪酬结构都可能不同。技术岗可能是“基本 + 绩效 + 项目奖金”,销售岗可能是”底薪 + 提成”,高管可能是“年薪制分月发放”。如果把公式写死在代码里,新增/修改薪酬结构必须改代码、发版本,扩展性为零。

数据来源多。单笔薪资核算需要联动员工档案、考勤汇总、假期扣款、社保配置等多模块数据,数据源零散,手动聚合极易出错。

计算过程必须可追溯。员工薪资疑问是企业高频场景,HR需要清晰、明细的计算依据,能够逐条解释薪资构成、公式规则、扣款来源,无法追溯的黑盒计算完全不满足业务需求。


3.2 四层架构设计:科目、模板、公式、工资单

项目采用四层分层架构,自上而下逐层解耦,实现零代码配置薪酬结构,避免为调整薪酬结构而改代码、发版本:

在这里插入图片描述

薪资科目(SalaryItem) 所有薪资构成的基础单位,不可拆分,包含五大核心属性,支持自定义公式:

属性 说明 示例
name 科目名称 基本工资、绩效奖金、交通补贴
code 唯一编码 BASE_SALARY, PERFORMANCE_BONUS
type 收入/扣款 INCOME(加项)/ DEDUCTION(减项)
isDefault 是否默认科目 基本工资 = 默认;高温补贴 = 非默认
formula 计算公式 baseSalary * 0.3(绩效 = 基本工资 × 30%)

薪资模板(SalaryTemplate) 将多个薪资科目组合为完整的岗位薪酬体系,按需适配不同岗位:

模板名称 包含科目
技术岗薪酬模板 基本工资、绩效奖金、交通补贴、加班费、缺勤扣款、事假扣款、社保代扣
销售岗薪酬模板 基本工资、销售提成、交通补贴、加班费、缺勤扣款、事假扣款、社保代扣

核心价值:新增薪酬结构、调整薪资构成,仅需在数据库新增模板、关联科目,完全实现业务配置化、代码零改动


3.3 Aviator 公式引擎的设计取舍

在这里插入图片描述

选 Aviator 的核心原因有三个:

一、安全性是引擎原生内置,而非人工配置兜底。SpEL 的安全依赖开发者手动切换 SimpleEvaluationContext,默认上下文存在 T() 类型引用、方法调用漏洞,一旦用户恶意配置公式可触发任意代码执行;Groovy 脚本自由度更高、注入风险更大。
Aviator 是纯表达式语言而非脚本语言,原生不支持 Java 类型引用、方法调用、反射操作,公式能做的只有算术、比较和函数运算。薪酬公式跑的是用户配置的字符串,安全边界靠引擎自身保证,比”靠使用者的记忆”可靠得多。

二、编译缓存开箱即用,天然解决批量核算性能问题。通过 AviatorEvaluator.compile(expr, true) 开启缓存,相同公式仅编译一次,批量核算百人、千人薪资时,避免大量重复 Parse 开销,性能远优于每次实时解析的 SpEL。

三、表达能力边界刚刚好,不会过度冗余也不会能力不足。三元表达式、数学函数、常规四则运算开箱即用,HR 可读懂、业务可配置;同时不支持复杂脚本逻辑,避免公式迭代后变成不可维护的“黑盒代码”。

核心计算入口逻辑简洁清晰,统一封装科目计算、变量注入、结果精度处理:

public BigDecimal calculate(Long itemId, Long employeeId, int year, int month) {
    SalaryItem item = salaryItemMapper.selectById(itemId);
    Map<String, Object> variables = buildVariableMap(employeeId, year, month);

    formulaEngine.setVariables(variables);
    Object result = formulaEngine.execute(item.getFormula());
    return ((BigDecimal) result).setScale(2, RoundingMode.HALF_UP);
}

真正的公式执行在 FormulaEngine.execute() 内部完成,包含多重生产级兜底设计,也是本段最核心的工程细节:

public Object execute(String formula) {
    if (formula == null || formula.trim().isEmpty()) {
        return BigDecimal.ZERO;
    }
    // 统一语法预处理
    String aviatorExpr = preprocessFormula(formula);   // ${baseSalary} → baseSalary
    // 扫描公式依赖变量
    Set<String> missingVars = findMissingVariables(aviatorExpr);
    Map<String, Object> varsWithDefaults = new HashMap<>(variables);
    // 缺失变量强制兜底 0,避免计算空值
    for (String var : missingVars) {
        if (!varsWithDefaults.containsKey(var)) {
            varsWithDefaults.put(var, BigDecimal.ZERO);
        }
    }

    try {
        Expression compiledExpr = AviatorEvaluator.compile(aviatorExpr, true);
        Object result = compiledExpr.execute(varsWithDefaults);
        return convertToNumber(result);
    } catch (Exception e) {
        throw new FormulaException("Formula execution error: " + e.getMessage());
    }
}

双语法兼容preprocessFormula 会把 ${baseSalary} 统一剥成裸变量 baseSalary,前端配置、后台解析互不冲突,统一内部执行口径。

缺失变量兜底:通过词法分析扫描公式依赖的全部变量,对未注入、不存在的变量默认填充 0,同时内置关键字白名单,避免函数、保留字被误判为缺失变量。

全程 BigDecimal 高精度计算:所有入参和出参都经 convertToNumber 归一成 BigDecimal,彻底杜绝浮点精度丢失,解决薪资计算中 0.1+0.2 精度问题,满足财务级精度要求。

变量数据源聚合方法 buildVariableMap 统一汇总五大数据源,并对所有可空字段做默认值兜底,解决 NULL 导致公式结果为空的异常:

private Map<String, Object> buildVariableMap(Long empId, int year, int month) {
    HrEmployee employee = employeeMapper.selectById(empId);
    AttMonthSummary monthly = monthSummaryMapper.selectByEmpAndPeriod(empId, year, month);
    BigDecimal leaveDeduction = leaveService.calcDeduction(empId, year, month);
    BigDecimal socialSecurity = socialSecurityService.calc(employee);

    Map<String, Object> vars = new HashMap<>();
    vars.put("baseSalary", employee.getBaseSalary());
    vars.put("normalDays", Objects.requireNonNullElse(monthly.getNormalDays(), 0));
    vars.put("overtimeHours", Objects.requireNonNullElse(monthly.getOvertimeHours(), 0));
    vars.put("lateCount", Objects.requireNonNullElse(monthly.getLateCount(), 0));
    vars.put("earlyCount", Objects.requireNonNullElse(monthly.getEarlyCount(), 0));
    vars.put("absentDays", Objects.requireNonNullElse(monthly.getAbsentDays(), 0));
    vars.put("leaveDeduction", Objects.requireNonNullElse(leaveDeduction, BigDecimal.ZERO));
    vars.put("socialSecurity", Objects.requireNonNullElse(socialSecurity, BigDecimal.ZERO));
    return vars;
}

这层空值兜底,正是第三篇复盘提到的线上问题修复方案:全勤员工部分考勤字段为 NULL 导致整月薪资为空,数据层 requireNonNullElse + 引擎层 findMissingVariables 双重兜底已解决字段级空值;至于”当月整条考勤汇总记录缺失”的记录级 NULL,仍会在 buildVariableMap 触发 NPE。


3.4 数据联动:五大来源汇聚一处

在这里插入图片描述

公式引擎的核心价值,不止是“表达式计算”,而是全维度薪资数据聚合中枢。它自动汇聚员工档案、月度考勤、加班数据、假期扣款、社保数据五大独立模块,完成统一计算、统一落库、统一明细追溯。

工资单中的 SalaryRecordItem 记录了每一个科目的计算明细:

字段 说明 示例值
salary_item_name 科目名称 基本工资
formula 使用的公式 baseSalary
variables 公式变量的实际值 {baseSalary: 15000}
result 计算结果 15000.00
type 收入/扣款 INCOME

这套明细机制,让任何一笔薪资都可以完整溯源:公式是什么、变量取的哪条数据、结果如何得出,HR 无需后台查库,即可直接向员工答疑。


3.5 跟着一个员工算一遍工资

我们用一条真实月度数据,完整走一遍核算链路,直观体现「可追溯、可校验、可解释」的能力。以技术岗员工张三 2026 年 5 月薪资为例:

数据源 变量 实际值
员工档案 baseSalary 15000
月考勤汇总 normalDays / lateCount / absentDays 22 / 0 / 1
月考勤汇总 overtimeHours 12
假期模块 leaveDeduction 800
社保扣款 socialSecurity 1575

逐科目代入技术岗模板的公式:

科目 公式 变量实际值 结果
基本工资 baseSalary {baseSalary: 15000} +15000.00
绩效奖金 baseSalary * 0.3 {baseSalary: 15000} +4500.00
加班费 overtimeHours * 100 {overtimeHours: 12} +1200.00
缺勤扣款 absentDays * baseSalary / 21.75 {absentDays: 1, baseSalary: 15000} -689.66
事假扣款 leaveDeduction {leaveDeduction: 800} -800.00
社保代扣 socialSecurity {socialSecurity: 1575} -1575.00
实发工资

17635.34

缺勤扣款是按日薪折算,不是扣固定金额:21.75 为法定月计薪天数(劳社部发〔2008〕3号),月薪 15000 折算日薪 689.66 元。公式跨科目引用了 baseSalary,换个月薪 30000 的员工,日薪自动变 1379.31,公式零改动。反例是加班费的 overtimeHours * 100:单位加班费写死在公式里,应抽成变量或公式。

当员工询问薪资差异时,HR 可以直接展开明细:本月旷工1天扣 689.66、事假扣 800,所有异常有据可查、所有计算有源可溯。这就是可追溯从技术设计落地为业务体验的真实体现。

在这里插入图片描述


3.6 三步核算流程:计算→预览→确认

薪资涉及真实资金发放,绝对不允许“一键生成直接生效”。项目设计了计算-预览-锁定三段式严谨流程,规避批量错算风险:

第一步:批量计算(草稿态 DRAFT)
遍历全员、匹配岗位薪资模板、逐科目解析公式、聚合多源数据,批量生成草稿工资单,不落地最终财务数据。

第二步:人工预览校验
HR 统一核查异常数据(实发为负、薪资骤变、零出勤异常等),提前修正原始考勤、请假、社保数据。

第三步:确认锁定(最终态 FINAL)
校验无误后批量锁定工资单,状态固化为 FINAL,禁止任何修改,仅保留查询、导出、打印能力。

状态 说明 允许的操作
DRAFT 草稿,计算完成但未确认 修改、重新计算
FINAL 已确认,不可修改 仅查询、导出、打印

3.7 Trae 贡献了什么、我补充了什么

公式引擎是本次 AI 开发项目中完成度最高、架构最稳的模块,但依旧存在 AI 天然的细节盲区。

Trae AI 擅长的架构与框架设计:

方面 具体表现
四层架构设计 科目-模板-公式-工资单的分离是自发设计的,层次清晰,职责明确
Aviator 选型 在多个表达式引擎中选对了最合适的,安全优先于灵活,这对薪酬计算是对的
数据注入设计 buildVariableMap 自动从员工、考勤、假期等多个源收集变量
计算明细存储 工资单中记录了每个科目的公式、变量值、计算结果,审计友好

人工补充、修正的生产级细节:

问题 Trae 原始实现 人工优化方案
公式校验 公式保存时不做语法校验,仅在月度核算时报错,排查成本高 引擎增加 validate() 保存前校验 + dryRun() 配置试算(试算完自动还原变量),错误在配置阶段就暴露
空值处理 仅数据层简单判空,引擎层缺失兜底 所有变量加 Objects.requireNonNullElse(..., 0) 默认值;引擎侧再用 findMissingVariables 给未注入变量兜底 0,双保险
计算精度 未处理小数精度 setScale(2, RoundingMode.HALF_UP) + BigDecimal 全程计算
公式联动校验 公式可以自由引用任何变量,无合规检查 增加科目依赖校验,禁止引用模板外非法变量
确认流程 计算后直接终审,无人工校验窗口 引入草稿-预览-确认三步流程,规避批量错发风险

3.8 可改进的方向

当前 Aviator 公式引擎已完全适配中小规模企业生产使用,但距离大规模、高可用、全场景薪资系统仍有明确优化空间:

性能瓶颈。当前是单线程逐员工计算。100 个员工还好,但如果是 10000 个员工,串行计算会很慢。优化方向:利用员工薪资计算天然无依赖的特性,改造多线程并行批量计算。

有状态引擎的并发隐患FormulaEngine 是个单例 Spring Bean,内部却持有可变的 variables 成员,setVariables 写入、execute 读取都发生在同一份状态上。当前单线程逐员工计算没问题,可一旦按上面的方向做并行化,多个线程会同时读写这张变量表、互相覆盖。改进方向是把变量改为 execute(formula, variables) 参数传入,引擎彻底无状态,并行才安全。
补充:Aviator 自身编译缓存线程安全、开箱即用,无需额外处理。

复杂公式支持。普通算术、三元表达式完全够用,但阶梯计税、分段补贴、累进扣款等复杂查表规则,硬写在表达式中会极度臃肿、不可维护。后续可单独封装规则函数、规则表,隔离复杂业务逻辑。

加班单价硬编码overtimeHours * 100 把 100 元/小时写死在公式字符串里,与「零代码配置薪酬结构」的主张有张力。改进方向:抽成独立变量 overtimeRate(按员工或岗位维护)或独立科目,公式改为 overtimeHours * overtimeRate,调整单价只改配置,不动公式。

税率计算。当前版本没有实现个人所得税的计算。完整的薪酬系统需要根据税前工资、社保扣除、专项附加扣除计算应纳税额,这不是公式引擎的问题,而是需要新增一个完整的税务计算模块。

汇总行缺失兜底。当前仅兜底字段级 NULL,未处理员工当月无考勤汇总记录的顶层 NULL 场景,存在极小概率 NPE,上线前需补充空实例默认兜底。


结语:三个设计的共性思考

回头看这三个模块,有一条共同的主线贯穿始终:用不可变性换确定性。状态机用一张 static final 的规则表锁死所有合法路径,杜绝非法状态变更;考勤月汇总用 is_locked 冻结数据快照,防止事后数据篡改;工资单用 DRAFT → FINAL 锁定核算结果,保证发薪数据绝对可信。三者本质都在对抗同一件事:业务数据被意外、非法、静默修改。规则在初始化时固定、数据在确认后冻结、状态流转全程可追溯,系统行为因此变得可预测、可审计、可复盘。

这也是复杂业务系统和普通 CRUD 系统的本质区别:CRUD 拼的是开发速度,复杂业务引擎拼的是边界覆盖、安全兜底、状态可控、数据一致。AI 可以快速搭建规范的分层架构、定义数据模型、实现主流程逻辑,轻松做到 80 分的标准化实现;但真正决定系统能否上线、能否稳定跑一年、能否扛住真实业务各种奇葩场景的,还是要靠人补充的 20%:空值兜底、精度控制、并发安全、权限合规、边界异常、流程校验。

维度 Trae 擅长的 人必须做的
架构方向 ✅ 分层清晰、模式恰当 ⚠️ 判断“这个场景值不值得这样设计”
规则定义 ✅ 覆盖所有正常路径 ⚠️ 边界条件、异常输入、NULL 值
代码生成 ✅ 快速、准确、无遗漏 ⚠️ 性能优化、设计简化
数据联动 ✅ 自动识别数据来源 ⚠️ 联动链路中的一致性校验

最好的人机协作模式,不是 AI 替代人,而是 AI 快速搭建高质量基础框架,人专注打磨业务细节与工程稳定性,把 80 分的标准实现,打磨成 95 分的生产级稳定系统。

下一篇是系列的收官,不再讲技术细节,做一次全局反思:Trae 真正擅长什么、做不到什么、人机该怎么分工。

Logo

一站式 AI 云服务平台

更多推荐