news 2026/8/26 15:09:10

SpringBoot3+Vue3 企业算薪引擎:SalaryCalcContext 驱动规则、公式与数据源解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot3+Vue3 企业算薪引擎:SalaryCalcContext 驱动规则、公式与数据源解析

SpringBoot3+Vue3 企业算薪引擎:SalaryCalcContext 驱动规则、公式与数据源解析

🌐文档地址:https://ruoyioffice.com
📦源码1·GitHub:https://github.com/yuqing2026/ruoyi-office
📦源码2·GitCode:https://gitcode.com/zhouzhongyan/ruoyi-office
📦源码3·Gitee:https://gitee.com/yqzy1688/ruoyi-office
💬微信:17156169080(备注「RuoYi Office」)

一句话:算薪引擎要验收的不是「能不能加出总数」,而是每一次试算能不能说清楚每个数字从哪来。RuoYi Office 把档案、岗位、绩效、考勤先装进SalaryCalcContext,规则只认三种calcType,项目金额按编码回填,整份上下文打成 JSON 快照。

▲ 先装上下文,再算应发,考勤按应出勤日入账,社保个税夹紧基数,最后按方案项目回填并打异常标签


引言:配置页配完了,真正难的是「算这一跳」

薪酬模块很容易被拆成三堆菜单:方案怎么配、个税怎么累计、工资条怎么发。这三件事都重要,但现场真正炸掉的往往是中间那一跳——点「试算」之后,引擎按什么顺序取数、公式引用哪个键、缺勤天数从哪来、算完的数怎么对回项目编码

现场原话引擎必须回答
「基本工资到底取档案还是取岗位?」规则dataSourceConfig是哪个键
「绩效没出结果,工资还发不发?」正式结果 → 岗位绩效基数 → 规则固定值,哪一层兜底
「这个月没打卡,为什么应发被扣光?」有没有考勤规则;没有规则则扣款为 0
「公式里写了『基本工资*0.1』,为什么是 0?」上下文里这个键当时有没有已经 put 进去

方案工作台、累计预扣个税、发薪发条这三件事,已有专文拆过。本文只写试算引擎本身:上下文、三种计算方式、考勤入账、项目回填与快照。

技术栈锚点(2026):Spring Boot 3.5 + Java 17,PC 为 Vue3 方案工作台配置规则,试算入口在薪资批次。


一、SalaryCalcContext 是什么

结论:SalaryCalcContext是一次员工试算的只读工作台,规则、公式、取数全部问它,不允许再去翻 Mapper。

它不是一个独立 Spring Bean,而是SalaryBatchServiceImpl里的内部类。构造时一次性塞进:

成员来源试算时干什么
batch薪资批次提供期间年、月
archive/version员工薪资档案及生效版本固定薪、补贴、社保个税城市
employee员工档案部门、是否已生成系统用户
positionRate岗位薪资规则最优匹配岗位工资、绩效基数
performanceResult已发布且期间相交的绩效结果金额 / 分数 / 系数
attendanceSummary打卡记录 + 部门考勤配置应出勤、缺勤、迟到早退缺卡
amounts引擎自己边算边 put公式和系统取数都从这张表读

键名是中英双语都写一份的。basicSalary和「基本工资」指向同一个数,这样公式既可以写basicSalary*0.1,也可以写基本工资*0.1

privateclassSalaryCalcContext{privatefinalMap<String,BigDecimal>amounts=newHashMap<>();privateSalaryCalcContext(...,AttendanceSummaryattendanceSummary){putAmount("periodYear",newBigDecimal(batch.getPeriodYear()));putAmount("periodMonth",newBigDecimal(batch.getPeriodMonth()));putAmount("performance.result_score",scoreOrZero(performanceResult));putAmount("performance.result_coefficient",coefOrZero(performanceResult));putAmount("performance.result_amount",amountOrZero(performanceResult));refreshAttendanceAmounts();}privatevoidrefreshAttendanceAmounts(){putAmount("attendance.work_days",attendanceSummary.workDays);putAmount("attendance.absent_days",attendanceSummary.absentDays);putAmount("attendance.absence_deduction",attendanceSummary.absenceDeduct);// 迟到 / 早退 / 缺卡次数同样 put 进表,供公式引用}}

顺序约束:应发四项(基本、岗位、绩效、补贴)必须先算完并putAmount,考勤扣款才能用「日薪 = 应发合计 / 应出勤日」。考勤汇总刷新过一次之后,社保个人、公积金、个税再 put。公式如果提前引用还没 put 的键,得到的是0,不是报错——这是设计取舍,试算不能因为一条自定义公式写错就把整批人打挂。


二、三种 calcType:固定值、公式、系统取数

结论:计薪规则只有三种算法。系统取数只认单个键名,加减乘除必须改走公式。

前端工作台把这三种算法展示给 HR:

▲ 深圳研发中心月薪方案:配置完整度六项打勾;「基本工资」计算方式是系统取数,来源说明指向薪资档案固定薪资。高级配置里才改公式或条件

对应后端SalaryRuleDO.calcType

calcType页面文案求值
1固定值rule.fixedValue
2公式计算中缀转后缀,再按上下文取标识符
3系统取数dataSourceConfig单个键

条件表达式先于求值:conditionExpression不满足,该项直接0,不再跑公式。上下限、小数位、舍入模式在算出裸值之后再夹。

privateBigDecimalcalculateByRule(SalaryRuleDOrule,SalaryCalcContextcontext,BigDecimalfallback){if(rule==null){returnscale(fallback);}if(!matchesCondition(rule.getConditionExpression(),context)){returnBigDecimal.ZERO;}BigDecimalvalue;if(Objects.equals(rule.getCalcType(),1)){value=defaultValue(rule.getFixedValue());}elseif(Objects.equals(rule.getCalcType(),2)){value=evaluateFormula(rule.getFormulaExpression(),context);}elseif(Objects.equals(rule.getCalcType(),3)){value=resolveDataSource(rule.getDataSourceConfig(),context,fallback);}else{value=resolveDataSource(rule.getDataSourceConfig(),context,fallback);}returnapplyRuleBoundsAndRound(value,rule);}

前端有一句硬提示:系统取数不能写archive.fixed_salary+archive.allowance_amount键名解析是精确匹配字符串,加号会被当成键名的一部分,永远 miss,结果落到 fallback。要「固定薪 + 补贴 − 缺勤」,把calcType改成 2,公式写成archive.fixed_salary+archive.allowance_amount-attendance.absence_deduction


三、系统取数:白名单键,而不是随意拼 SQL

结论:系统取数是一张白名单。引擎不会按字符串去反射档案对象,更不会拼 SQL。

resolveDataSource认这些键(与工作台下拉SALARY_DATA_SOURCE_OPTIONS对齐):

含义miss 时
archive.fixed_salary档案固定薪,兼看转正薪、试用薪0
archive.allowance_amount档案固定补贴0
position.post_salary岗位匹配结果岗位未命中则 0
performance.base正式绩效金额,否则岗位绩效基数0
performance.result_amount/result_score/result_coefficient已发布绩效无结果则 0
attendance.absence_deduction已按考勤规则算出的缺勤扣款刷新前是 0

认不出的字符串,退回context.amount(source),也就是中间金额表。所以自定义项目可以把键写成基本工资payableAmount,前提是引擎已经 put 过。

开发库里现有规则绝大多数是calcType=3。公式和固定值是给「这个部门有特殊津贴」留的口子,不是默认路径。


四、公式:中缀转后缀,标识符允许汉字

结论:公式引擎是手写的调度场算法,不是 Groovy、不是 Aviator。标识符允许字母、数字、下划线、点号和汉字。

这是为了让 HR 能写基本工资*0.08,而不必先学英文变量。全角括号会先替换成半角。除数为 0 时结果是 0,不抛异常。

比较运算符挂在条件表达式上:>=<===!=><左右两边各自再走一遍公式求值。没有运算符时,公式结果非 0 视为条件成立。

privateBigDecimalparseFormulaValue(Stringtoken,SalaryCalcContextcontext){try{returnnewBigDecimal(token);}catch(NumberFormatExceptionignored){returncontext.amount(token);}}privateBigDecimalcalculate(BigDecimalleft,BigDecimalright,charoperator){returnswitch(operator){case'+'->left.add(right);case'-'->left.subtract(right);case'*'->left.multiply(right);case'/'->right.compareTo(BigDecimal.ZERO)==0?BigDecimal.ZERO:left.divide(right,8,RoundingMode.HALF_UP);default->BigDecimal.ZERO;};}

边界要讲清楚:没有函数、没有IF()、没有日期。复杂分支用conditionExpression拆成多条规则,而不是在一条公式里写脚本。这和「把 Excel 整张表贴进系统」不是一条路。


五、考勤入账:先数应出勤日,再谈扣款

结论:缺勤天数不是「日历 30 天 − 打卡天数」,是考勤规则引擎逐日判定的应出勤日,减去有进或出记录的工作日。

buildAttendanceSummary做了三件容易被忽略的事:

  1. 按员工部门取考勤配置,沿部门树向上继承,跟打卡页是同一套getAttendanceConfigByDeptId
  2. isWorkDay逐日数应出勤,接入大小周、节假日、夏冬令时;休息日加班打卡不计入出勤天数。
  3. 员工还没生成系统用户,汇总标记available=false,后面扣款直接为 0,并打异常标签,而不是按全月缺勤把应发扣光。

扣款公式在AttendanceSummary.calculateDeduct。方案如果没配考勤规则,扣款为 0——这是有意的,避免「规则没配等于按默认比例扣光」。

▲ 深圳研发考勤扣款规则:缺勤按日薪比例 1 扣,迟到/早退每次 20,缺卡每次 50。这些数进入试算上下文的attendance.absence_deduction

privatevoidcalculateDeduct(BigDecimalpayableBase,SalaryAttendanceRuleDOattendanceRule){if(!available||attendanceRule==null){absenceDeduct=BigDecimal.ZERO;return;}BigDecimaldailySalary=workDays.compareTo(BigDecimal.ZERO)==0?BigDecimal.ZERO:payableBase.divide(workDays,8,RoundingMode.HALF_UP);BigDecimalabsentDeduct=Objects.equals(attendanceRule.getAbsenceDeductType(),2)?absenceDayAmount.multiply(absentDays):dailySalary.multiply(absentDays).multiply(absenceDayRatio);BigDecimallateDeduct=newBigDecimal(lateCount).multiply(lateAmount);BigDecimalearlyLeaveDeduct=newBigDecimal(earlyLeaveCount).multiply(earlyAmount);BigDecimalmissingDeduct=newBigDecimal(missingPunchCount).multiply(missingAmount);absenceDeduct=scale(absentDeduct.add(lateDeduct).add(earlyLeaveDeduct).add(missingDeduct));}

缺勤类型 1 是「日薪 × 缺勤天数 × 比例」,类型 2 是「固定额 × 缺勤天数」。迟到早退缺卡是按次加。全部加完才putAmount("attendance.absence_deduction"),缺勤项目用系统取数去读这个键。


六、按方案项目回填,并留下快照

结论:引擎内部先算「应发合计 / 应扣合计 / 实发 / 人工成本」四个汇总,再按方案勾选的每一个薪资项目把金额填回去。工资条上看到的每一行,都能对上itemCode

resolveItemAmount用编码关键字认标准项:BASICPOSTPERFORMANCEALLOWATTENDANCESOCIAL_PERSONALFUND_PERSONALTAXNETSOCIAL_COMPANYFUND_COMPANYLABOR_COST。认不出的自定义项,再走一遍calculateByRule

每个员工一行结果写入hrm_salary_batch_employee,分项写入hrm_salary_batch_employee_item,整份上下文打进calc_snapshot_json:批次号、档案版本、岗位匹配、绩效结果、考勤汇总、异常标签、个税累计口径。

▲ 试算结果弹窗写明绩效工资的取值优先级;黄标签是配置提示,红标签是实发为负、缺方案、缺社保规则、员工公司与薪酬主体不一致这类硬异常

社保基数按基本工资夹在上下限之间,个税走累计预扣(专文已写,这里只强调引擎把taxAmountput 进上下文后,个税项目按编码取走)。非居民规则会打「未支持累计预扣」标签,税额按 0 处理,不假装算过。


七、异常标签:能继续算,但必须看见

结论:试算默认不中断整批。缺配置用标签标出来,HR 决定是补数据再跑,还是带着提示送确认审批。

标签含义
期间无生效版本档案版本生效日晚于本期结束
未匹配岗位工资规则方案勾了岗位项,但岗位规则没命中
绩效模块未接入期间没有已发布结果,用了岗位基数或规则兜底
无考勤数据,缺勤扣款按全月缺勤测算有用户、有应出勤日、打卡行为 0
缺少社保公积金规则 / 缺少个税规则城市规则 miss
累计期初缺失2 月及以后首次跑,历史台账为空
实发为负数扣项大于应发,硬异常

硬异常在弹窗里用红色标。配置提示用黄色,允许继续。这和「试算失败就整批回滚」不同:企业发薪经常是 90% 的人没问题,剩下的人缺一张社保城市,HR 需要看见名单而不是整批重来。


八、数据结构(试算相关)

角色
hrm_salary_plan/hrm_salary_plan_item方案及勾选的薪资项目
hrm_salary_rulecalcType、公式、取数键、条件、上下限
hrm_salary_attendance_rule缺勤类型、迟到早退缺卡金额
hrm_salary_social_fund_rule/hrm_salary_tax_rule按城市匹配
hrm_salary_batch期间批次,确认审批后禁止重试算
hrm_salary_batch_employee人级应发应扣实发 +calc_snapshot_json
hrm_salary_batch_employee_item项目级金额、来源类型、备注

设计要点:快照是试算当时的事实,发薪后档案再改不影响已确认批次;来源备注会写成「来自打卡汇总:缺勤 x 天,迟到 y 次」,对账时不用再反推。


九、技术亮点

设计要点实现价值
一次装上下文内部类SalaryCalcContext规则不再各自查库
三种 calcType固定值 / 公式 / 系统取数HR 能配,工程师能审计
取数白名单精确匹配键名禁止在取数框里写加减
中文公式标识符含汉字基本工资*0.1可跑
考勤按应出勤日复用打卡规则引擎大小周不会算错缺勤
无规则不扣款attendanceRule == null则 0避免无配置扣光应发
绩效三层兜底正式结果 → 岗位基数 → 规则绩效晚出也能先发工资
快照可对账JSON 含分项与异常工资条每一行有出处
标签不中断整批黄提示 / 红硬异常少数人缺配置不影响多数人

十、快速体验

在线演示:https://ruoyioffice.com/web/(账号 admin / admin123)

建议按这条路径看引擎,而不是只看方案列表:

  1. 人力 → 薪酬管理 → 薪酬方案,打开「深圳研发中心月薪方案」工作台。
  2. 在「项目与计薪」看BASIC_SALARY的计算方式是否为系统取数。
  3. 点某一行「高级」,看calcType、取数键、条件表达式。
  4. 切到「考勤」,看缺勤比例和迟到早退缺卡金额。
  5. 打开薪资批次,对已有批次点「查看结果」,看绩效来源和异常色。
  6. 若批次尚未确认,可再点「试算」(确认审批后会被后端拒绝重算)。

源码仓库:GitHub:https://github.com/yuqing2026/ruoyi-office | GitCode:https://gitcode.com/zhouzhongyan/ruoyi-office | Gitee:https://gitee.com/yqzy1688/ruoyi-office


常见问题(FAQ)

算薪引擎和薪酬方案工作台是一回事吗?

不是。工作台负责把项目、岗位、社保、个税、考勤配齐并生成默认规则;引擎负责批次试算时按SalaryCalcContext求值。工作台配错,引擎会算出带标签的结果,而不是在配置页假装已经发薪。

系统取数能不能写「固定薪加补贴」?

不能。系统取数只匹配单个键,例如archive.fixed_salary。加减乘除把calcType改成公式计算。

员工这个月完全没打卡,会不会把应发扣成 0?

分两层。还没生成系统用户:考勤available=false,扣款为 0,打标签。已经有用户但打卡记录为 0:按应出勤日计全月缺勤,再乘方案里的缺勤比例。方案若没配考勤规则,扣款仍是 0。

公式支持 Excel 那种 IF 吗?

不支持函数。分支用规则上的条件表达式拆开。标识符可以写中文键名,四则运算和括号可以,IF/SUM/VLOOKUP不行。

试算过的批次还能改方案再算一遍吗?

发起确认审批、已确认、已发薪或已发布工资条后,后端拒绝重试算。要改口径,走新批次,不要改已经送审的快照。


结语

企业算薪引擎的核心不是「把四则运算写进 Java」,而是给每一次试算一个封闭的上下文:档案版本、岗位匹配、绩效结果、考勤汇总进同一个SalaryCalcContext;规则只暴露固定值、公式、系统取数三种算法;项目按编码把金额领走;算不清的用标签说出来,而不是静默填 0 或整批失败。

方案怎么配、个税怎么累计、工资条怎么发,可以各自成文。中间这一跳如果讲不清,前面配得再漂亮,发下去的数也经不起对账。

你们团队现在的算薪,是脚本拍脑袋,还是每次试算都能打开快照看每个项目的来源?欢迎在评论里说说卡在哪一层。


💡想要体验 RuoYi Office 的强大功能?

🌐在线演示:https://ruoyioffice.com/web/(账号 admin / admin123)

📦源码仓库:GitHub:https://github.com/yuqing2026/ruoyi-office | GitCode:https://gitcode.com/zhouzhongyan/ruoyi-office | Gitee:https://gitee.com/yqzy1688/ruoyi-office

💬技术咨询:添加微信17156169080,备注「RuoYi Office」

如果觉得不错,请给个 Star 支持一下!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/26 15:05:53

YiShaAdmin 权限管理系统部署指南:4 个高频卡点一次打通

YiShaAdmin 权限管理系统部署指南&#xff1a;4 个高频卡点一次打通 【免费下载链接】YiShaAdmin 基于 .NET Core MVC 的权限管理系统&#xff0c;代码易读易懂、界面简洁美观 项目地址: https://gitcode.com/GitHub_Trending/yi/YiShaAdmin YiShaAdmin 是一个基于 .NET…

作者头像 李华
网站建设 2026/8/26 15:05:44

产品教程|Memmy 如何接续任务查询,并处理本地检索超时

本文经 AI 创作者 Berryxia.AI 授权&#xff0c;根据其实际测试过程整理与改写。 SalesScout 是 Barry 的本地项目&#xff0c;文中的文件名称、查询条件和返回结果来自他的设备和真实项目案例。 Memmy 是运行在本地的个人 AI Agent&#xff0c;同时也是多个 Agent 共用的个人记…

作者头像 李华
网站建设 2026/8/26 15:05:30

EasyMocap 实时 3D 可视化 3 步跑通

EasyMocap 实时 3D 可视化 3 步跑通 【免费下载链接】EasyMocap Make human motion capture easier. 项目地址: https://gitcode.com/gh_mirrors/ea/EasyMocap 算完几百帧姿态&#xff0c;还得翻 json 逐帧核对&#xff1f;EasyMocap 的实时 3D 可视化把关键点直接送进…

作者头像 李华