Vue+Node.js+ThinkPHP重构人事工资管理系统实践与踩坑指南
从6月开始我一直在重构公司内部一套缝缝补补又三年的人事工资管理系统。旧系统是纯PHP写的页面还是jQuery 模板渲染功能倒是能用但每次改工资计算规则都要动核心文件考勤、绩效、福利全堆在一起谁都不敢动。这次彻底重写技术选型定了 Vue Node.js ThinkPHP 的组合花了几周时间把员工档案、考勤、绩效、工资、福利一套流程捋顺平稳上线。今天把这套系统的设计思路、核心模块实现和踩坑记录整理出来给准备做人事管理系统或者正被类似老系统折磨的朋友一个参考。这套系统本质上是典型的业务管理系统前端交互多、表单复杂、权限细碎后端既要处理业务数据又要做大量计算工资核算、考勤汇总、绩效打分。我的分工方案是Vue负责全部界面交互ThinkPHP 8 负责业务API和数据库操作Node.js 则单独拆出来扛计算密集型的活——月度工资批量计算、考勤原始记录清洗、薪资报表导出。看起来多加了一层中间件但实际跑下来这层非常值后面我会详细说为什么这么拆。整套方案适合50到500人规模的中小企业也适合正在做毕设或练手项目的开发者参考。对于用过 Vue 但没试过 Node.js 和 PHP 混合架构的人这篇文章尤其有用因为里面每一节都是我实际跑通后沉淀下来的东西不是照搬文档。1. 为什么非要把系统拆成 Vue Node.js ThinkPHP 三层1.1 三个组件各自的边界划在哪先说结论Vue 管交互ThinkPHP 管业务数据和规则Node.js 管计算和杂活。这个分工不是说谁先进就用谁而是根据这段代码到底重在哪决定的。Vue 这边负责的是员工档案页、考勤日历、绩效评分表、工资条预览这类界面。为什么不用原来的服务端模板渲染因为人事系统的前端交互实在太碎——表格要排序筛选、树形组织架构要懒加载、工资明细要做折叠展开、权限不同看到的菜单还不一样。用 jQuery 硬写也能出来但维护成本会随着功能膨胀直线上升。换成 Vue Element Plus 后这些全都变成了组件化的拖拽配置开发效率和体验完全是两个级别。ThinkPHP 8 承担的是店长角色接收前端请求校验权限读写 MySQL跑核心业务流水线。选它而不是换成 Node.js 或 Go一方面 PHP 在中小型项目的开发效率仍然是第一梯队部署也简单一个 Nginx PHP-FPM 就能跑另一方面公司有存量 PHP 开发经验后续维护不会断层。项目的段落、部门组织、员工基本信息、薪资档案模板这些重数据逻辑的部分都归 ThinkPHP 管。Node.js 在这里不是替代 PHP而是当工头接到 ThinkPHP 派发的工资计算任务后用脚本批量跑完再写回数据库。为什么计算活要单独拎出来我实测过同样的月度工资批量核算600人规模PHP 单进程要跑十几秒Node.js 起几个 Worker 并行处理最快能把时间压到 3 秒左右还把主业务的数据库连接释放了。考勤原始数据的清洗也是同理打卡机导出的 Excel 或 CSV 数据往往一天好几千行用 Node.js 的流式解析加异步批量写入效率比逐条 insert 高一个数量级。1.2 这套拆法解决了老系统的哪几个实际问题第一解耦。老系统里工资计算和考勤汇总的代码是揉在同一个 Controller 里的想改个加班费规则得把整个文件从头读一遍。拆开后工资计算逻辑独立成 Node.js 服务改计算规则只要动一个文件跑完测试就能发风险面大大缩小。第二性能隔离。工资核算这类任务一旦跑起来会占满数据库连接和 CPU如果和日常的员工信息查询共用一套服务高峰期系统会明显卡顿。拆出来以后日常查询走 ThinkPHP计算任务走 Node.js 队列两个进程互不干扰。第三生态互补。PHP 的数组和字符串处理非常适合拼接业务逻辑但说到处理 Excel、做并发请求、解析各种格式的原始文件Node.js 的 npm 生态明显更顺手。两个各干各的擅长活比单用一种语言硬扛所有需求舒服得多。在具体落地上这套方案对部署的要求就是多一个 Node.js 进程和一个代理转发规则并没有引入 Kafka 那种重量级中间件。如果你的公司规模和我类似任务量没有到需要消息队列的程度一条 HTTP 请求触发任务、轮询状态的方式完全够用。2. 开发环境搭建Node.js、Vue 和 ThinkPHP 的磨合期2.1 Node.js 安装与版本管理Windows 和 macOS 两套做法这一步看起来简单但团队里几乎每个人都踩过坑。我建议不要直接去官网下载最新版安装包而是用 nvm 做版本管理。Windows 用户去 GitHub 下载nvm-windows的安装包装完后在命令行里输入nvm install 20.11.0 nvm use 20.11.0macOS 用户直接装官方 nvmcurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash nvm install 20.11.0为什么非要用 nvm因为不同项目依赖的 Node.js 版本经常不一样老项目可能锁在 16新项目要求 20。用 nvm 可以随时切换不会出现这个项目跑不起来因为我的 Node 太新这种问题。版本本身我推荐用 LTS 版本比如 20.x不要追奇奇怪怪的新版本稳定压倒一切。装完以后命令node -v和npm -v能看到版本号就说明环境没问题。如果npm报错说找不到命令多半是环境变量没配上检查一下系统变量里的NODE_HOME或PATH是否指向了 Node 安装目录。2.2 解决高频报错npm.ps1 禁止运行脚本这是 Windows 用户几乎必踩的坑。你在 PowerShell 里执行npm install结果蹦出一行npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本注意不只是 C 盘D 盘装 Node 也经常报d:\program files\nodejs\npm.ps1或者d:\nodejs\npm.ps1本质原因完全一样Windows 的 PowerShell 执行策略默认限制.ps1脚本运行而npm.ps1就是 npm 的命令入口脚本。解法很简单以管理员身份打开 PowerShell执行Set-ExecutionPolicy RemoteSigned输入Y确认再重新打开终端就好。RemoteSigned的意思是本地脚本可以直接跑从网上下载的脚本必须带可信签名。这是最通用也最稳妥的设置不用图省事选Unrestricted那个会让所有脚本都能跑安全边界太松。如果是公司电脑不想动全局策略也可以只对当前用户生效Set-ExecutionPolicy -Scope CurrentUser RemoteSigned这个方案不需要管理员权限效果一样。我在给团队写的开发环境文档里第一条就放了这段命令和说明之后再也没有人卡在这一步。2.3 Vue 项目创建与依赖安装别再用 vue create 了现在创建 Vue 项目推荐用 Vite命令简单清晰npm create vitelatest hr-front然后选Vue模板语言选TypeScript或者JavaScript都行。我这边因为团队已经习惯了 JS选的是 JavaScript省一层类型心智负担。创建完成后进入目录cd hr-front npm install npm install element-plus axios pinia vue-router4这里用到 Element Plus 做 UI 组件库axios 做 HTTP 请求Pinia 做状态管理vue-router 4 做路由。装依赖的时候有个小经验第一次npm install如果慢得离谱大概率是官方源网络问题可以临时切到国内镜像npm config set registry https://registry.npmmirror.com注意这个源是给下载加速用的发布包还是走官方所以不会带来什么副作用。装完以后npm run dev能启动页面就说明基础通了。2.4 ThinkPHP 环境跑通与接口约定ThinkPHP 端我直接用的 Composer 创建项目composer create-project topthink/think tp-hr然后在.env文件里配置数据库连接。关键是要把数据库的字符集设为utf8mb4不然存员工名字里的生僻字或者 emoji 会出问题。前后端联调前我先统一了接口返回格式这是磨合并行的关键。约定所有接口返回都是这个样子{ code: 0, message: success, data: {} }code为 0 是成功非 0 是业务异常业务异常的信息放在message里。前端 axios 封装一层拦截器统一处理 code 和 HTTP 状态页面里只需要写业务逻辑不用每个接口都做错误分支。另一个约定是金额和日期格式金额统一精确到分整数存储日期统一YYYY-MM-DD HH:mm:ss避免出现前端传2024-06-01后端读成2024年06月01日这类尴尬问题。Vite 开发环境下的代理也一并配好不然前后端联调会被 CORS 折磨server: { proxy: { /api/php: { target: http://localhost:8000, changeOrigin: true }, /api/node: { target: http://localhost:3000, changeOrigin: true } } }这样前端代码里请求/api/php/employee/list会被转发到 ThinkPHP请求/api/node/salary/calc会被转发到 Node.js 工资计算服务。生产环境则靠 Nginx 做同样的事。3. 核心业务模块设计与实现细节3.1 员工档案和组织架构拆表比存一棵大树实在员工管理模块是所有其他模块的数据底座。一开始我想偷懒把部门、岗位、员工全部塞进一张表用一个字段parent_id表示层级关系后来发现查询某个部门下面的所有员工时SQL 写得极其痛苦而且一个员工调岗后历史数据还要保留一张表根本说不清。最终落地的表结构是department部门表包含id、parent_id、name、leader_idposition岗位表包含id、name、department_id、job_levelemployee员工主表包含基础信息、所属部门、岗位、入职日期、状态employee_salary_profile薪资档案表包含基本工资、岗位工资、社保基数、公积金基数等员工和部门、岗位都用外键关联这样调整岗位时只需要更新employee表的position_id薪资计算时通过关联关系去取最新的部门和岗位系数。调岗历史我单独加了一张employee_transfer_log记录生效日期和变更前后值既方便审计也方便将来做人力分析。组织架构树不需要一次加载全年部门前端用el-tree 懒加载点击展开时再请求子部门列表接口返回的字段带上hasChildren标记体验和性能都兼顾。3.2 考勤数据清洗与迟到扣款计算考勤是整个工资计算的数据源头也是最容易出问题的环节。我们用的考勤机每天导出一份 Excel里面是一整天的打卡原始记录每条记录包含工号、日期、打点时间、设备编号。这些数据直接塞进 SQL 没用必须先按规则清洗成某员工某天应该几点上下班、实际几点上下班、状态是什么的考勤明细。清洗逻辑我放在 Node.js 服务里因为处理 Excel 和字符串匹配时 npm 的工具链太舒服了。我用了xlsx库解析 Excel然后用worker_threads开多线程处理伪代码如下const XLSX require(xlsx); const { Worker } require(worker_threads); async function parseAttendance(filePath) { const workbook XLSX.readFile(filePath); const sheet workbook.Sheets[workbook.SheetNames[0]]; const rawRows XLSX.utils.sheet_to_json(sheet); // 清洗规则去重、补全工号、剔除无效设备记录 const cleaned deduplicate(rawRows); // 按工号分组异步写入考勤明细表 await batchInsert(cleaned); }清洗完以后每条考勤明细要判定规则晚于上班时间 30 分钟内算迟到超过 30 分钟算事假半天下班早于规定时间算早退加班需要自助申请并关联真实打卡记录超时没申请的不算加班时长。这些规则全部配在数据库的一张规则表里而不是写死在代码中这样 HR 改规则不用找开发发版本。再套一个生活类比考勤清洗就像会计做账前的凭证整理打卡记录相当于原始票据你必须先摆平这张票算不算数后面的账才做得下去。规则不清晰时宁愿退回重导数据也不要带病计算工资。3.3 月度工资计算的两级流水线工资计算是我们整个系统的核心重头活。每到月底HR 在后台点开始核算系统就自动开启一条流水线。第一步ThinkPHP 端把员工薪资档案、考勤月度汇总、绩效评分结果、社保公积金基数等数据全部从库里捞出来按照员工 ID 组装成一个 JSON 请求体POST 到 Node.js 工资计算服务。这一步等于把所有原料统一打包不让计算服务直接碰数据库一是隔离 IO二是让计算服务保持无状态以后扩展到多实例部署也容易。第二步Node.js 服务收到请求后按员工分片并行计算。计算逻辑包括应发工资汇总、各类扣款、个税预扣预缴。个税部分我用的是速算扣除数法每月减除费用 5000 元再叠加专项附加扣除数据子女教育、房贷、赡养老人等这部分数据 HR 月初维护在系统里计算时直接读取。第三步计算完成后结果写回到 MySQL 的salary_batch和salary_detail两张表。salary_batch记录这次核算的生成批次、总人数、总金额、核算状态salary_detail存每个员工每个工资项的名称和金额工资条就是从这张明细表渲染出来的。这里说一个关键细节工资明细里每个项目是独立一行比如基本工资岗位工资加班费迟到扣款社保个人部分不是一整条 JSON 塞进一个字段。这样做的好处是工资条打印时可以灵活分组展示HR 审核时也能逐项核对后续如果要接财务系统做科目映射也有明确的数据粒度。计算完以后还不能直接发布系统会生成一个异常项检查报告比如某个员工本月工资为零可能是离职未办理、社保基数低于下限、考勤汇总和工资考勤对不上等等HR 逐条确认无问题后才点确认发布。这一步强制人工复核虽然多了流程但防住了好几起因为数据错误可能引发的薪资投诉。3.4 绩效评分与奖金联动绩效模块在这个系统里和工资是强关联的。每个季度 HR 配置一次考核模板包含考核维度工作业绩、团队协作、创新能力、权重和评分标准。员工自评后直属上级评部门负责人再审最后系统按权重算加权总分。为了让绩效结果更贴合实际我加了强制分布机制部门内按分数拉通强制分出 A/B/C/D 四档比例默认 20%/40%/30%/10%可配置。每个档位对应一个绩效系数A 档系数 1.2B 档 1.0C 档 0.8D 档 0.5。这个系数最终与月度绩效工资和年终奖联动。绩效结果的展示和确认环节所有评分记录都不可直接篡改留痕可追溯。如果有争议系统支持派发复议流程由部门上级重新评估后更新分数但修改日志会完整记录在案。这是因为我管理过程中发现没有留痕的绩效系统最终一定会变成人情分和口水账。3.5 福利模块五险一金基数和节日补贴的规则化福利模块看着不难但细节巨多。五险一金各城市缴费比例不同且每年会有基数上下限调整节日补贴按员工状态、工龄、部门有不同的发放标准。如果这些规则全部写死在代码里每次政策调整都得发版本非常被动。所以我把福利规则全部做成了配置表welfare_rule记录某个时间区间内某个城市的养老、医疗、失业、工伤、生育、公积金比例。计算工资时直接读取规则表按员工参保城市匹配。节日补贴则按福利项目 触发条件 发放金额设计。比如中秋节福利全员发 500 元购物卡当年入职满 6 个月发 800 元。条件表达式存成 JSON 结构Node.js 里解析后计算。这样 HR 自己就能维护规则下个节日不用找开发。4. 前端交互实战路由、插槽、文件预览那些躲不过的坑4.1 动态路由与 RBAC 权限控制人事系统的权限是出了名的细不同角色登录后看到的菜单、能点开的页面、能操作的按钮完全不一样。我用的是 RBAC 模型角色表、权限点表、角色权限关联表、员工角色关联表。前端拿到登录用户信息时同时拿到他拥有的路由权限列表。Vue Router 不在一开始把全部路由注册完而是在登录后动态添加const dynamicRoutes buildRoutes(menuList); router.addRoute(dynamicRoutes);配合菜单权限后端返回的路由表是树形结构前端根据这个结构渲染侧边栏菜单再注册对应组件。这样天然就实现了没权限的人压根看不到那个页面的效果。按钮级的权限我用自定义指令v-permission控制没有对应权限码的按钮会被移除app.directive(permission, { mounted(el, binding) { if (!hasPermission(binding.value)) { el.parentNode?.removeChild(el); } } });用动态路由而不是在模板里做一堆v-if判断核心原因是安全。前端隐藏菜单不等于安全真正拦截请求是后端权限校验但动态路由让用户连入口都找不到减少了不少点到灰色按钮报错的工单。4.2 表格里的插槽和折叠展开人事系统的表格交互比普通后台管理系统高很多。以员工列表为例一行员工点展开能看到该员工的薪资档案、考勤汇总、最近绩效评分的分栏展示同时操作列根据当前用户权限动态渲染编辑、调整岗位、停用按钮。Element Plus 的el-table操作列就这么写el-table-column label操作 width220 template #default{ row } el-button v-ifhasPerm(employee:edit) clickopenEdit(row)编辑/el-button el-button v-ifhasPerm(employee:salary) clickviewSalary(row)薪资/el-button /template /el-table-column这里的作用域插槽#default{ row }是个大杀器它让你在渲染每一行时拿到当前行的完整数据不用自己去维护 selectedRow。工资批次列表我用了el-collapse做手风琴折叠点开一层是本月所有部门再点一层才展开员工工资明细层级清晰也不会一屏塞满表格。我在这个模块最大的省力技巧是把所有弹窗表单统一封装成表单引擎——用配置数组描述表单控件类型、字段名、校验规则渲染成统一的弹窗。新增员工和编辑员工的表单共用一套配置只是初始化值不同。以后加一个字段改配置数组就行不用复制十遍模板。4.3 工资单 PDF 预览图片组件不能渲染 PDF 的解决办法这是前端一个典型误区。很多同事以为用了img就能显示 PDF结果发现浏览器要么直接下载、要么只弹原生预览完全不可控。vue image组件确实显示不了 PDF原因是img标签的渲染能力仅限位图和 SVG 等栅格格式PDF 是复合文档格式浏览器的原生扩展里只有 PDF Viewer 能接管它。我们的处理方案是工资条生成后转成 PDF 存入对象存储前端预览时用pdfjs-dist库直接渲染到 Canvas 上。这样有两个好处一是不依赖浏览器原生 PDF 插件的样式差异二是可以精确控制预览工具栏比如禁用下载按钮只允许查看。核心代码如下import * as pdfjsLib from pdfjs-dist; async function previewPdf(url) { const loadingTask pdfjsLib.getDocument(url); const pdf await loadingTask.promise; const page await pdf.getPage(1); const viewport page.getViewport({ scale: 1.2 }); const canvas document.getElementById(pdfCanvas); canvas.width viewport.width; canvas.height viewport.height; await page.render({ canvasContext: canvas.getContext(2d), viewport }).promise; }顺带提一句如果项目里有音频视频文件比如考勤培训资料要注意播放 m3u8 这种流媒体格式时 vue 原生组件玩不动需要引入hls.jsimport Hls from hls.js; if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(videoUrl); hls.attachMedia(videoElement); }不考虑浏览器兼容的话也可以靠原生 video 标签支持 mp4但碰到 m3u8 就必须上 hls.js 或 flv.js这是流媒体播放的老规矩。4.4 打包部署与跨框架托管前端构建后就一个dist目录里面全是静态文件。部署我推荐用 Nginx 统一托管做一个最简单的反向代理server { listen 80; server_name hr.example.com; root /var/www/hr-front/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8000/; proxy_set_header Host $host; } location /api-node/ { proxy_pass http://127.0.0.1:3000/; proxy_set_header Host $host; } location / { try_files $uri $uri/ /index.html; } }这里最后一行try_files是 SPA 路由的核心让所有前端路由都回退到 index.html由 Vue Router 接手。网上还能看到很多人问vue 打包怎么放进 springboot 里面本质就是把dist文件夹复制到 springboot 的 static 目录或 thinkphp 的 public 目录让后端服务接管前端静态资源。这种方案能用但跨框架部署容易碰到路由冲突和缓存问题我还是建议哪怕是单机部署也用 Nginx 把动静分离做好省心得多。生产环境的 Node.js 进程我推荐用 PM2 托管一条命令搞定守护、日志、自动重启pm2 start server.js --name hr-salary-worker5. 高频报错与排查速查表这几天我逐个填过的坑5.1 npm.ps1 禁止运行脚本C盘、D盘都有前面已经写了完整解法这里再补充一个重要提示这个问题会反复出现。如果你在安装 nvm、切换 Node 版本、换了一个设备以后又遇到类似报错先检查当前 PowerShell 的执行策略Get-ExecutionPolicy -List看看CurrentUser和MachinePolicy有没有被组策略覆盖。如果MachinePolicy是Restricted光设CurrentUser没用要联系管理员处理。我在两台公司电脑上实测过不同情况这个问题不是镜像源的问题不是 npm 包损坏的问题就是权限策略的问题别绕弯路。5.2 VSCode 里 Vue 标签跳转失效明明是这个组件点 Ctrl 跳转却提示找不到定义大概率是 VSCode 的 Vue 语言服务出问题了。新版本 VSCode 必须用Vue OfficialVolar 的新名字插件如果同时装了旧的Vetur两个插件会打架。记得先禁用 Vetur只保留 Vue Official重启 VSCode。还有一种是路径别名导致的跳转失败。项目里配置了指向src但 VSCode 不知道这个别名需要在jsconfig.jsonJS 项目或tsconfig.jsonTS 项目里加{ compilerOptions: { baseUrl: ., paths: { /*: [src/*] } } }最后还要注意一个点如果你用 TypeScript 开发Vue 文件里自己的类型声明可能会被报错或忽略经常碰到failed to load tsconfig vue/tsconfig/tsconfig.web.json这类问题。这是因为 Vue 模板需要独立的 tsconfig 解析检查tsconfig.json里的extends路径是否正确实在不行把extends改成本地完整配置。5.3 Node.js 直接运行 TypeScript我有一部分脚本如考勤清洗、工资计算是用 TypeScript 写的但在 Node 里直接跑会报语法错误。传统做法是用ts-node转译npm install -D ts-node typescript npx ts-node src/salary-calc.ts如果你用的是 Node 22 以上版本可以尝鲜原生停掉类型检查直接跑 TS 的特性node --experimental-strip-types src/salary-calc.ts不过生产环境我还是建议用ts-node或者配合tsx更稳。注意只 strip 类型不做转译遇到enum、namespace这类 TS 特有语法仍会报错写脚本时尽量用标准 ES 语法配合类型注解。5.4 前后端联调的经典坑跨域、端口、代理链开发时前端在 5173 端口后端在 8000 和 3000只要代理没配置好报错一定是一片红。最常见的问题是代理 target 写成了http://localhost:8000/但请求路径里带着/api/php导致后端收到的 URL 里还残留/api/php前缀路由匹配失败。Vite 代理默认不改写路径需要rewrite/api/php: { target: http://localhost:8000, changeOrigin: true, rewrite: (path) path.replace(/^\/api\/php/, ) }后端路由自己规范了api前缀的话也可以不 rewrite前后端约定一致就行。最忌惮的是前端一个样子、后端一个样子排查的人会很痛苦。如果项目挂在 Nginx 下同样要先确认代理转发后前缀是否符合后端路由预期这个规则我建议写进团队约定文档每次新环境搭建都有人踩。5.5 数据库时区与金额精度两个怕出事的细节考勤和工资都是时间敏感业务前端传2024-06-01 00:00:00后端如果时区配置不对经常变成前一天或者后一天。MySQL 连接串里务必加上时区参数charset utf8mb4, collation utf8mb4_unicode_ci,PHP 侧date_default_timezone_set(Asia/Shanghai)Node 侧做计算时统一用 UTC 存储、显示时转发成东八区字符串。金额精度是另一个大坑。MySQL 里存万元等浮点数PHP 浮点运算会出现0.1 0.2 0.30000000000000004这种经典问题。工资表的salary、deduction、social_insurance一律用decimal(10,2)PHP 里用bcadd()而不是运算Node.js 里用decimal.js库。我从一开始就定了这个规矩后面几轮核算没有出现过金额对不上账的情况。6. 上线后的那几天我又补了哪些课系统上线不是一切结束反而是问题的开始。第一周我密集收到各种异常报告同一个员工在两个部门都出现了离职员工的工资单还在生成绩效评分卡在某个环节没人审核。这些都是因为我在做数据处理时没有把员工状态字段贯穿到所有查询里。后来统一在员工表的查询条件下加了一层过滤status active才进入月度核算离职和停薪留职的员工自动跳过只在审计报表里保留记录。工资条发布后员工的反馈渠道也很重要。我们给每个员工开通了自助登录登录后可以查看自己的工资条和考勤记录发现有异议可以发起申诉申诉工单自动推到人事的待办里。这个小功能上线后工资不对来找HR的口头沟通少了大半因为流程留痕处理效率也高了。还有一个经验任何批量操作批量导入、批量调薪、批量发放福利都要做预演和假数据验证。我在 Node.js 服务里加了一个dryRun模式跑计算时只输出结果不写库HR 可以先看预览报表确认无误后再正式执行。这个开关看似只是个小功能却救了我至少两次——一次是导入的 Excel 里有两列错位了另一次是新绩效系数忘记乘权重如果直接跑完写库改回来可就是一大摊子事。最后分享一个小技巧考勤和工资模块的数据我都做了月快照。每个月生成一份 JSON 存到归档表里面有当月所有参与计算的原始参数和计算结果。为什么这么做因为几个月后如果有人回来质疑某月工资你可以直接调出当时的快照一清二楚地告诉他那个月的算法、参数、结果分别是什么。人事系统最怕的就是扯皮和说不清楚快照是成本最低的审计底稿。这一招比任何日志系统都直观强烈推荐给所有做财务人事相关系统的朋友。