361 行
39 KiB
Markdown
361 行
39 KiB
Markdown
# 杨俊报销就绪:技术设计
|
||
|
||
## 1. 设计目标
|
||
|
||
以最小生产变更打通“杨俊企微登录 → 杨俊提交真实报销 → 马建勇审批 → 王芝玲财务处理”,并在真实员工使用前由两个既有企微测试成员完成一次端到端体验验收。本任务不新增产品能力,复用现有通讯录导入、用户/角色管理、报销工作流和异机备份能力。
|
||
|
||
## 2. 已确认边界
|
||
|
||
- 企业微信成员与 OA 员工是两个独立事实:杨俊已在企微通讯录,但生产尚无 OA 员工记录和企微绑定。
|
||
- 单人通讯录输入可唯一筛出杨俊;手机号为空符合现有 nullable 合同,企业邮箱存在,`incomplete=0`。
|
||
- 生产已有 37 个 active 费用类型和 1 个 active 报销工作流定义。
|
||
- active 法体为 0,但当前报销创建、提交和费用控制不以法体为硬门禁,本任务不创建法体。
|
||
- 杨俊的真实直属主管由用户确认是马建勇。
|
||
- 自动备份 daily/watchdog/drill-reminder 与 WARCH3 service 的 `BUILD_SHA` 均已漂移,必须先修复。
|
||
- 通讯录中已有两个状态为已激活的专用企微测试成员,分别命名为“测试员工”和“测试主管”;生产 OA 尚未导入它们,因此不存在可直接登录的测试 OA 账号。
|
||
|
||
## 3. 数据流与执行门禁
|
||
|
||
```text
|
||
现场只读复核活动构建与 4 个 unit
|
||
→ 重装 automatic-backup 三项 timer/service 与 WARCH3 timer
|
||
→ 验证 unit BUILD_SHA、enabled、active
|
||
→ 手工触发一份真实自动异机备份
|
||
→ 验证源端成功与 ARM 恢复点完整/新鲜
|
||
→ 从管理员导出通讯录在内存中仅筛选杨俊
|
||
→ production dry-run(create=1、conflict/skip/fail=0)
|
||
→ production apply(初始角色仅 employee)
|
||
→ 验证 OA 员工 active、企微绑定唯一、其他成员未导入
|
||
→ 通过现有用户管理接口设置 direct_manager_id=马建勇
|
||
→ 验证主管 active、不同人、具备 expense.approve,并核对审计
|
||
→ 从同一通讯录仅筛选两个既有测试成员并 dry-run/apply
|
||
→ 赋予最小角色并设置“测试员工 → 测试主管”的 OA 主管关系
|
||
→ 两个测试成员通过企微分别登录
|
||
→ 创建一条受控测试报销并由测试主管退回
|
||
→ 删除测试报销、归档测试票据、停用测试 OA 账号并撤销会话
|
||
→ 杨俊本人企微登录并提交真实报销
|
||
→ 马建勇审批,随后进入王芝玲财务处理链路
|
||
```
|
||
|
||
任一门禁失败即停止后续步骤,不用后续成功掩盖前序失败。
|
||
|
||
## 4. 复用合同
|
||
|
||
### 4.1 通讯录导入
|
||
|
||
- 复用 `server/src/scripts/import-wecom-directory-export.js` 的 stdin 输入,不把筛选后的 XLSX 或成员原始 payload 写入磁盘、日志、Trellis 或聊天。
|
||
- dry-run 只记录人数与结果计数。
|
||
- apply 必须同时满足脚本 `--apply` 参数和容器内 `WECOM_DIRECTORY_EXPORT_APPLY=1`。当前 Compose v1 调用必须显式使用 `docker-compose exec -T -e WECOM_DIRECTORY_EXPORT_APPLY ...`,不能只在宿主 shell export。
|
||
- 输入严格只有杨俊一人;成功创建后的初始角色集合严格为 `employee`,不导入其他通讯录成员。
|
||
- 此路径是“管理员导出企微通讯录 → OA 员工/身份绑定”的单向导入。OA 侧主管字段仅服务 OA 工作流;不调用企微成员 create/update/disable,也不把 OA 主管、姓名、部门、手机或邮箱写回企微。
|
||
- 企微成员资料如需变化,由成员本人在企微允许的入口修改,或由企微管理员在管理后台修改;OA 后续只通过重新导出/受控同步读取变化。
|
||
|
||
### 4.2 直属主管
|
||
|
||
- 复用 `POST /api/users/batch/set-manager`,不直写数据库。
|
||
- 写入目标仅杨俊,主管仅马建勇;接口负责 active、自指等现有校验。
|
||
- 写后核对 `SYSTEM_USERS_BATCH_SET_MANAGER` 审计增量,不记录完整账号或原始请求体。
|
||
- 马建勇必须是 active、与杨俊不同且按有效角色解析出 `expense.approve`。
|
||
|
||
### 4.3 报销业务
|
||
|
||
- 杨俊无需先存在差旅申请;`linked_travel_request_id` 可空。
|
||
- 提交仍须满足既有硬门禁:至少一条费用明细、至少一张已绑定且状态为 `bound` 的真实票据、可匹配工作流并能解析审批人。
|
||
- 第一笔真实报销必须由杨俊本人操作;Codex 不代建生产报销、发票、付款或测试业务数据。
|
||
|
||
### 4.4 timer 与备份
|
||
|
||
- 使用现有 `install-automatic-backup-timer.sh` 和 `install-warch3-ops-timer.sh`,从活动部署现场解析并显式传入合同参数;不在任务文档中固化 Secret、宿主路径或完整账号。
|
||
- 重装后核对 4 个 service 的 `BUILD_SHA` 与活动 server 一致,并确认对应 timers 为 enabled/active。
|
||
- 随后手工触发 daily 自动备份路径,验证源端 service 成功、ARM 批次完成且 dead-man/新鲜度检查通过。
|
||
- 新鲜异机恢复点是人员数据写入的前置条件,但不会用于回滚单个人员变更。
|
||
|
||
### 4.5 两个既有测试成员
|
||
|
||
- 不创建新的 OA-only 测试身份,也不调用企微成员创建接口;复用管理员导出表中的两个专用企微测试成员,通过通讯录导入建立 OA 员工与企微身份绑定。
|
||
- 导入输入只含“测试员工”和“测试主管”,手机号、邮箱、企微 userid 等原始字段不进入任务证据、聊天或日志。
|
||
- “测试员工”角色集合严格为 `employee`;“测试主管”角色集合严格为 `employee + manager`。后者只用于验证 `expense.approve` 和主管待办,不授予财务、薪资、银行资料或系统管理权限。
|
||
- 通过现有主管接口把测试员工的 `direct_manager_id` 指向测试主管;所有 OA 主管与角色写入均保留现有审计,不回写企微。
|
||
- 两个账号必须分别从企微入口登录,以同时验证通讯录导入后的身份绑定和实际页面权限;无需生成或传递临时密码。
|
||
|
||
### 4.6 受控测试报销生命周期
|
||
|
||
```text
|
||
测试员工创建报销
|
||
→ 使用 0.01 元最小测试值和“系统测试、非真实业务、禁止付款”描述
|
||
→ 上传不含真实票据/个人资料的安全 PDF
|
||
→ OCR 成功则继续;OCR 失败则在票据池人工填入唯一测试字段并确认可用
|
||
→ 绑定测试票据并提交
|
||
→ 测试主管确认待办可见并执行“退回”
|
||
→ 报销恢复 draft
|
||
→ 测试员工删除 draft(票据释放为 available)
|
||
→ 测试员工归档已释放票据
|
||
→ 管理员停用两个测试 OA 账号并核对会话撤销
|
||
```
|
||
|
||
- 测试主管不得点击“通过”;这保证测试单不会进入真实财务列表或付款批次。
|
||
- 删除仅允许发生在退回后的 `draft`;`POST /api/workflow/tasks/:taskId/return` 会在同一事务中把报销明确改回 `draft` 并终止 workflow instance,因此无需再执行申请人撤回。现有草稿删除接口会把已绑定票据释放为 `available`。
|
||
- 票据仅在解绑且为 `available`/`ocr_failed` 时手工归档;归档后保留最小审计痕迹但不再是可用业务票据。
|
||
- 账号收口使用现有 OA 停用接口,递增 token version 并撤销活动会话;企微侧两个测试成员保持原状。
|
||
|
||
### 4.7 受控测试发现与真实使用门禁
|
||
|
||
- 测试实际确认:生产当前使用 `OCR_PROVIDER=mock`,未配置腾讯 OCR 凭证;上传的合成 PDF 被写入明显的 Mock 发票号码、销售方、日期和金额并标记为可用。
|
||
- mock 只能作为合成测试依赖,不能让真实员工把其结果误认为真实识别。杨俊真实使用前必须二选一并再次复验:配置腾讯 OCR;或关闭 mock,明确采用人工录入/核对流程。
|
||
- 用户选择并批准关闭 mock。现场拓扑没有独立 worker,`JOBS_ENABLED=true`,因此只重建实际承载 Web 与后台任务的 server;不得为了“同时重启 worker”而凭空创建第二个作业执行器。
|
||
- `OCR_PROVIDER=off` 复验确认:新 PDF 直接进入 `available`,发票代码、号码、日期、购销方和金额字段均为空,OCR 尝试为 0、请求/完成时间为空;票据可以绑定到单明细 0.01 元测试报销并正常提交、退回和清理。
|
||
- `InvoicePool.ocr_provider` 是非空枚举且 schema 默认 `tencent`;off 路径传入 `undefined` 后数据库仍写入该默认值。该字段只是历史 schema 默认,不代表发生过 OCR 调用;当前 UI 不展示 provider。是否在后续迁移中改为 nullable 应与腾讯 OCR/人工录入产品决策一起规划,不阻断本次真实报销。
|
||
- `InvoicePool` 当前使用 `@@unique([file_sha256, ocr_provider, ocr_type])`。后续若把 provider 改成 nullable 或新增 `off` 枚举,必须同步回填历史行或重设计唯一键;只改新行 provider 会把同一文件拆进两个查重命名空间。生产只读核查显示历史 `mock` 票据共 1 张且仅由测试账号上传,非测试员工上传数为 0。
|
||
- off 模式下发票号为空,基于 `invoice_code + invoice_number` 的查重不可用,但文件 SHA-256 查重仍生效;当前小团队短期由财务查看 PDF 原件并人工核对,不在本任务增加强制补录。
|
||
- 草稿删除成功后,详情页在导航完成前仍短暂请求已删除的详情与 precheck,浏览器产生预期 404 控制台噪声,但用户已回到空草稿列表。这是非阻断前端时序 finding,不影响数据清理结果。
|
||
|
||
## 5. 失败与回退
|
||
|
||
- timer 重装或异机备份失败:不写杨俊数据,保留本地诊断并走既有脱敏群告警。
|
||
- dry-run 不满足 `target=1 / active=1 / incomplete=0 / planned create=1 / conflicts=0 / skipped=0 / failed=0`:不 apply,重新核对输入或生产现状。
|
||
- apply 失败:停止;按脚本结果诊断,不手工拼接残缺账号。
|
||
- apply 成功但主管写入失败:保留已创建的 OA 员工和企微绑定,不删除、不恢复整库;主管仍为空时不能提交报销,可安全重试同一主管接口。
|
||
- 主管写错:通过同一接口恢复写入前的 `direct_manager_id`,不恢复整库。
|
||
- 真实报销提交失败:保留草稿和真实票据状态,按返回的业务错误定位;不得通过扩权、代批或伪造数据绕过。
|
||
- 测试导入或角色/主管设置失败:停止测试,不借用真实员工账号;按单步接口恢复两名测试成员原 OA 状态。
|
||
- 测试提交失败:保留 draft 和测试票据定位问题;不得改成“审批通过”绕过。修复后从同一测试草稿重试或删除后重建。
|
||
- 测试退回后删除失败:禁止停用账号,先保留两名测试账号以完成清理;不得直写数据库删除业务记录。
|
||
- 测试账号误被赋予超范围角色:立即通过现有角色接口恢复最小角色集合并撤销会话,不恢复整库。
|
||
- OCR 仍为 mock:保持测试账号停用,不让杨俊上传真实票据;不得用“人工记住忽略 Mock 字段”作为默认生产口径。当前已切换为 off,此回退项不再触发。
|
||
|
||
## 6. 隐私与审计
|
||
|
||
- 终端与任务证据只保留布尔值、计数、角色 slug、审计 action 和脱敏状态。
|
||
- 不输出手机号、企业邮箱、企微 userid、完整账号、通讯录原始行、票据内容、银行资料、Secret 或完整宿主路径。
|
||
- 账号导入与主管写入分别保留现有系统审计,确保每一步可独立追踪和撤销。
|
||
|
||
## 7. 不做的设计扩展
|
||
|
||
- 不新增导入 API、主管兜底规则、CEO 自审批能力或报销状态。
|
||
- 不启用 OA → 企微通讯录写回,不修改任何企微成员资料。
|
||
- 不创建法体、不补银行账号、不导入其余成员。
|
||
- 不把首次真实业务扩成管理员代办的生产自动化测试。
|
||
- 不创建新的企微测试成员,不给测试主管财务或管理员权限,不让测试单通过审批或进入付款链路。
|
||
|
||
## 8. 真实首跑补充风险复核
|
||
|
||
- 生产三名真实参与人均 active 且企微绑定唯一,但尚无 `AUTH_WECOM_LOGIN_SUCCESS` 记录;企微客户端入口仍需本人首登验证。
|
||
- 当前 active 费用标准为 0、杨俊部门适用预算池为 0;真实金额不会触发费用标准或预算阻断。active 法体为 0、杨俊未绑定法体,但现有报销提交和付款批次候选不以法体为硬门禁。
|
||
- 金额审批规则不是同一条:`[0,500)` 为 `manager → finance`,`[500,5000)` 为 `manager → finance_mgr`,`[5000,+∞)` 为 `manager → ceo → finance_mgr`。
|
||
- 当前 active 角色持有人:`finance=1`、`ceo=1`、`finance_mgr=0`。因此低于 500 元可路由到马建勇后再到王芝玲;达到 500 元的单据会生成无人可处理的 `finance_mgr` 角色待办。修复应调整审批规则到既有最小角色,不给王芝玲扩为权限过宽的 `finance_mgr`。
|
||
- 用户确认当前公司人数少且由马建勇统一负责,不需要金额分级。生产规则收敛为唯一 `[0,+∞)` 规则:`department_manager → finance`;删除另外两档,不保留 CEO 重复节点。
|
||
- 后续只有出现第二名实际审批主管,或有人基于真实职责承担 `finance_mgr`,并且业务证明需要金额升级时才重开分级设计。大额关注使用风险标记或通知,不通过同一自然人重复审批实现。
|
||
- 生产变更复用 `PATCH /api/approval-rules/:id`:先把低档规则扩为 `[0,+∞)`,再停用中、高两档,避免出现无人可匹配的短暂金额空窗;每步保留 `APPROVAL_RULE_UPDATED` 审计,失败则通过同一接口补偿恢复原三档。
|
||
- 最终审批只把报销置为 `approved`,不会自动创建付款批次或付款记录;付款批次仍由王芝玲主动创建并由马建勇复核。
|
||
|
||
## 9. 审批任务到完整报销单的可达性
|
||
|
||
- 根因不在待办列表卡片:移动待办列表已经通过 `handleTaskSelection` 进入 `/mobile/approvals/:taskId`。真实阻断发生在审批任务详情,其报销摘要只有单号、申请人和金额,没有费用明细、发票和附件,也没有进入既有报销详情的出口。
|
||
- 移动端只在审批任务详情的报销摘要区增加明确的“查看完整报销单”入口,目标复用 `/mobile/reimbursements/:reimbursementId`;导航继续使用当前移动页面通用的 `{ state: { from: currentMobileSource } }` 约定,不新增 `returnTo` 参数或另一套回跳状态。
|
||
- PC 端在 `WorkflowTaskDetailPage` 的报销摘要区提供“查看报销单”入口,目标复用 `/reimbursements/:reimbursementId`。
|
||
- 现有 `manager`、`ceo` 和 `finance` 角色已经包含 `expense.view_all`,因此本轮不调整角色、权限矩阵、后端报销详情鉴权或 workflow 数据结构。
|
||
- 不把整个摘要卡片改造成无提示的隐式链接;使用明确命名的链接按钮,避免与待办列表卡片的既有任务选择交互混淆。
|
||
- 请假、采购、差旅审批详情存在同类业务详情缺口,但本轮只修复真实报销链;其他类型另轮统一处理。
|
||
|
||
## 10. 报销详情时区与 KISS 收敛
|
||
|
||
- 后端继续用 ISO 8601 UTC 字符串传输时间,前端不得用 `slice(0, 19)` 假装本地时间。PC 报销详情、移动报销详情和 PC 审批任务详情复用 `formatDisplayDateTime`;共享函数显式使用 `Asia/Shanghai`,使公司内部 OA 不依赖浏览器所在时区。
|
||
- 不新增页面级时间工具。合成 UTC 样例通过页面文本断言验证上海时间,覆盖附件上传时间和审批任务时间。
|
||
- PC 将“当前状态与下一步”和“业务摘要”收敛为单一首屏概况,保留总额、状态、编号、日期、报销人、说明与真正可执行入口;差旅概要并入该单头,删除解释同一事实的二级标题和辅助句。
|
||
- PC 与移动只保留单头概况、费用明细、报销材料、处理进度四个可扫描标题。附件上传限制只在可编辑时出现;关联事项作为单头行、预算/风险作为就近提示、付款作为进度节点次级事实,均不形成永久空区域。
|
||
- 移动端头部已经包含状态,因此概况卡不重复状态;只保留金额、日期、报销人和说明。只读状态不显示编辑教学或空“操作”卡,编辑态仍保留新增、上传、删除和底部提交能力。
|
||
- 采用当前设计系统和现有组件,不引入新颜色、字体、动效、布局系统或后端字段;新版 `ui-ux-pro-max` 仅用于内容优先、密度和渐进披露判断。
|
||
|
||
## 11. 企微 WebView 旧包刷新护栏
|
||
|
||
- 诊断依据:生产构建的移动审批详情把报销日期和“查看完整报销单”放在同一个 `selectedTask.reimbursement` 条件块;真机截图只出现日期,说明 WebView 仍运行部署前 JS。公网 `/ready` 与当前移动 chunk 均已核对为正确构建,不把问题误判为权限、后端报销关联或列表未进入详情。
|
||
- 复用现有构建合同:前端使用 `VITE_BUILD_SHA`,公网 `/ready` 返回 server `buildSha`。页面只在 `visibilitychange` 回到 `visible` 时用 `cache: no-store` 请求 `/ready`,规范化为 12 位后比较;开发/unknown、空值、请求失败和解析失败均静默。
|
||
- 构建不一致时继续复用 `hyb-oa:pwa-update-ready` 事件及 `usePwaUpdatePrompt`,在已登录 AppShell 顶部显示一条用户控制的刷新提示;不自动刷新,避免打断审批意见、表单或其他未保存操作。
|
||
- 用户点击刷新后复用 `clearPwaRuntimeCaches()` 并重载当前 URL;同一次页面生命周期只提示一次,避免页面多次前后台切换反复提示。
|
||
- 护栏只能保护已经加载过本次新代码的页面,不能反向唤醒当前仍运行 `d2841b1` 之前旧包的 WebView;首次部署本护栏时仍需主动通知马建勇手工刷新,后续部署才由版本比对接管。
|
||
- 不新增后端 API、数据库字段、定时轮询、更新中心或强制升级状态;登录/改密页面继续沿用既有用户确认刷新逻辑。
|
||
|
||
## 9. 企业微信原生材料预览
|
||
|
||
### 9.1 运行时分流
|
||
|
||
- 企业微信 iOS/Android WebView:使用腾讯维护的 `@wecom/jssdk` 调用 `previewFile({ url, name, size })`,由企业微信原生层预览文件。
|
||
- 普通 PC 浏览器:发票使用现有短时签名 URL,附件继续使用认证下载 Blob 后由浏览器打开;两端复用同一个“查看原件”业务动作,但不强求相同底层机制。
|
||
- 非企微移动浏览器或 JS-SDK 调用失败:给出自然中文错误并保留下载/PC 查看退路;不静默宣称已预览。
|
||
|
||
### 9.2 JS-SDK 签名链
|
||
|
||
- 新增已认证的自建应用 JS-SDK 签名接口,输入为当前页面去掉 fragment 的完整 URL。
|
||
- 服务端只允许 `APP_ORIGIN` 或当前受信请求 origin,拒绝跨 origin、带凭据 URL、非 HTTP(S) 和 fragment;复用企微应用 access token,在服务端获取并缓存 JSAPI ticket。
|
||
- 返回仅包含 `corpId`、`timestamp`、`nonceStr`、`signature`;ticket、access token、Agent Secret 和企微原始响应不得返回或记录。
|
||
- 客户端仅在 `isWecomWebView()` 为真时按需注册 SDK,`jsApiList` 只包含 `previewFile`,避免把 JS-SDK 加到登录页或普通浏览器主路径。
|
||
|
||
### 9.3 文件 URL 与授权
|
||
|
||
- 发票复用 `/invoice-pool/:id/signed-url`,响应补准确 `fileSize`、`originalName` 和 `mimeType`;签发沿用票据所有者/报销参与人/当前待审批人对象级授权。
|
||
- 报销附件新增同形短时签名 URL:签发前复用 `ensureOwnOrCanViewAll`,执行治理状态与下载理由门禁;token 绑定 attachment、reimbursement、actor 和用途,TTL 取现有签名 TTL 与 120 秒中的较小值。
|
||
- 原生层取件时重新校验 token、对象关系和过期时间,实际打开前写下载/预览审计。短时 URL 是企业微信原生层无法携带 OA 认证头时的窄例外,不扩展为通用 query-token 下载机制。
|
||
- 不改角色或权限矩阵。审批人获得的只是当前报销材料只读能力,不能编辑、解绑、删除或管理票据池。
|
||
|
||
### 9.4 安全与兼容
|
||
|
||
- `name` 使用数据库原始显示名但必须确保与实际 MIME 匹配并带安全扩展名;`size` 来自持久化的准确字节数。
|
||
- 短时 URL 不写入 Trellis、聊天、前端持久化存储、错误详情或普通日志;页面关闭/刷新后重新签发。
|
||
- 生产首次验收先用无业务数据 PDF 证明 `config + previewFile`,再查看真实材料。部署、企微真机调用和真实审批继续单独获批。
|
||
|
||
## 12. 报销明细与材料的 KISS 对齐
|
||
|
||
- 交互采用“一个入口、一次通览、语义保留”:费用明细只展示费用事实,不再渲染关联发票;`linkedItemId`、票据/附件类型、对象权限、审计和生命周期均不改变。付款回单仍属于付款阶段材料,不提前混入本次提交材料。
|
||
- 报销材料首屏显示“发票 N · 补充附件 M”和一个明确的“查看材料合并预览”按钮,下方始终显示有序原件列表。每份材料只使用一个语义化整行按钮打开原件,不再叠加“查看”按钮;草稿/驳回编辑态在同一行外保留发票移除和附件删除动作,只读审批人不显示数据变更动作。
|
||
- 服务端新增报销级合并 PDF grant:授权后读取当前绑定的发票 PDF 与补充附件,PDF 原页全部复制,JPG/PNG/WebP 经 Sharp 规范化后各嵌入一页。企微调用一次 `previewFile({ url, name, size })`,PC/普通浏览器打开同一短时 PDF URL;不引入 PDF.js,不在客户端渲染 PDF。
|
||
- 预览顺序固定为发票 `created_at, id`、补充附件 `created_at, id`、PDF 内页顺序;不根据 `linkedItemId`、金额或文件名猜测排序。有序材料集合哈希覆盖材料类型、ID、文件哈希/大小/MIME,缓存键由报销单 ID、集合哈希和合并器版本组成。
|
||
- 合并在 Node `worker_threads` 中执行,不启用 Compose worker profile。worker 限制材料数、源文件总字节、PDF 总页数、图片像素、输出字节和执行时间;`pdf-lib` 不使用 `ignoreEncryption`,加密、损坏或不可复制 PDF 导致整包失败。任一材料失败都不能跳过,客户端展开现有逐份原件路径。
|
||
- 合并 PDF 固定 CreationDate、ModificationDate、Creator、Producer 和保存选项;相同有序输入必须生成完全相同字节。合并 Buffer 生成一次后缓存并原样返回,签发 `size` 取该 Buffer 长度。缓存缺失时原签发 URL 返回拒绝,不现场生成另一份字节;用户重新操作会重新签发。
|
||
- 合并件不进入业务表、不形成原件,只在有界短时缓存中保存;页面明确“报销材料合并预览,仅供审批核对;原始文件另行保留”。grant 绑定 purpose、reimbursement、actor、bundle 与过期时间,实际 GET 验签并写整包预览审计。
|
||
- 后端从已脱敏持久化 OCR `result_json` 只映射 `documentCategory` / `documentSubtype` 到报销详情,不下发原始腾讯响应。前端共享格式化函数按 `vat_invoice`、`full_vat_invoice`、`electronic_train_ticket` 选择票种名称和适用字段;未知票种使用中性“发票 + 号码/文件名”,不显示“无代码”或对不适用字段宣称“未识别”。
|
||
- “查看材料合并预览”和降级行均使用语义化 button、可见焦点、至少 44px 触控目标;超过 300ms 时按钮禁用并显示“正在准备材料预览…”。材料区提示“打开后上下滚动,连续查看全部材料”;不再提示左右滑动。
|
||
|
||
## 13. 生产验收修正:材料可读性与轻量差旅概要
|
||
|
||
### 13.1 按钮层级
|
||
|
||
- PC 现有入口已在“报销材料” `SectionPanel`,保持不变。
|
||
- 移动端将入口从 `mobile-reimbursement-items` 移到现有“报销材料” card 标题/计数之后;明细区只保留费用明细及其编辑动作。
|
||
|
||
### 13.2 合并 PDF 与确定性合同
|
||
|
||
- 生产脱敏计时基线保留为旧图片包证据;新主路径不再使用 Poppler 或图片数组。
|
||
- `createBundle` 先按显式排序取得材料并计算集合哈希,再把源字节交给受限 `worker_threads`。worker 复制全部 PDF 页、把图片嵌为页面,并返回单个 Buffer、页数和精确大小。
|
||
- 固定 PDF 元数据和序列化选项;单测对相同输入分别启动两次合并,断言 Buffer 全等。签发响应的 `fileSize` 必须等于随后 GET 的 `content-length` 和响应字节数。
|
||
- 合并缓存按报销单 ID、集合哈希、合并器版本复用;actor grant 仍独立。缓存淘汰或进程重启后旧 URL 失败,不以重新生成结果冒充旧签发字节。
|
||
- 移除旧图片 bundle 页路由与 `previewImage` 主路径;若 Poppler 无其他用途,从镜像移除 `poppler-utils`。保留单份发票/附件原件预览作为失败降级与电子签名事实源。
|
||
|
||
### 13.3 轻量差旅概要数据流
|
||
|
||
```text
|
||
草稿/驳回报销的差旅明细
|
||
-> 编辑 travelPurpose / travelLocation / travelStartDate / travelEndDate
|
||
-> PATCH 入口集中验证长度与日期顺序
|
||
-> reimbursement_forms 可空列持久化
|
||
-> 详情 API 返回轻量 travelSummary
|
||
-> PC/移动首部同一信息层级显示
|
||
-> 存在 travel category 明细时,submit precheck 拒绝缺失字段
|
||
```
|
||
|
||
- Prisma 在 `ReimbursementForm` 增加四个 nullable 列:事由、地点、开始日期、结束日期。迁移只加可空列,不回填、不改历史单、不修改审批状态;回滚时代码先回滚,列可保留至受控后续清理。
|
||
- 差旅单判定使用“任一明细的 `expenseType.category === travel`”或存在关联差旅申请,不新建另一套报销类型字典。
|
||
- 关联差旅申请时,概要可从已批准申请的 purpose、segments 城市、start/end date 读取;普通差旅报销使用自身四字段。历史数据只读降级为“description 作事由、明细最小/最大日期作起止日”,地点缺失如实显示,禁止从文件名、OCR 金额或自由文本猜地点。
|
||
- 四字段只允许申请人/受权代办在 `draft/rejected` 更改。审批中单不原地改业务事实;当前真实单如需完整地点,必须由主管退回后申请人补齐并重新提交。
|
||
- 详情页把差旅概要并入金额/报销人所在的单头概况;只有差旅单显示差旅行,不让普通费用报销承担空字段或独立空卡。
|
||
- 共享 `ReimbursementTravelSummary` 以“可编辑且概要不完整”作为初始填写态条件,直接呈现四个可见标签输入框;保存成功后切回摘要态,保存失败停留在表单并显示现有错误反馈。完整概要仍由用户点击“编辑”进入表单,不引入新的步骤、弹窗或报销类型。
|
||
|
||
### 13.4 四块式 KISS 交互边界
|
||
|
||
- 材料列表与合并预览复用现有单份签名 URL、企微 `previewFile`、PC 打开方式、下载理由、权限和审计;本轮只移除只读态的列表可见性条件,不增加后端接口。
|
||
- 列表顺序沿用详情 API 的发票、补充附件顺序;不按金额、文件名或关联明细重新排序。稳定 ID 作为 React key,整行按钮保留可见焦点和至少 44px 触控高度。
|
||
- PC/移动沿用现有详情路由和返回路径,只重排当前内容为四块纯滚动;不做 Tab、折叠、抽屉、普通/差旅二选一、费用日期交叉校验、补助自动计算或审批页重构。
|
||
- 真实风险以内联提示贴近单头/明细,关联项目或差旅申请以单头一行链接呈现;草稿/驳回动作留在页面底部命令区,均不计入稳定内容块。
|
||
|
||
### 13.5 五阶段处理进度 DTO
|
||
|
||
`GET /api/reimbursements/:id` 新增只读 `processingProgress`:
|
||
|
||
```text
|
||
processingProgress.stages[] = {
|
||
key,
|
||
label,
|
||
state, // completed | current | pending | exception
|
||
actorName?,
|
||
occurredAt?,
|
||
result?,
|
||
comment?,
|
||
detail?,
|
||
targetHref?
|
||
}
|
||
```
|
||
|
||
- 服务端固定生成“申请提交”以及“付款批次复核、付款与回单”首尾阶段;中间审批段先按查询的 `created_at,id` 顺序逐项生成真实 task,每个 task 拥有独立 stage key,不再用职责 Map 合并;再从提交时 `workflow_steps_snapshot` 的当前节点之后补齐尚未创建 task 的未来 pending 节点。当前两步审批规则从刚提交起就返回五个阶段,新增或重复角色节点会自然增加时间轴节点,不得丢失、覆盖或因延迟建任务而暂时消失。
|
||
- 审批标题优先使用非空且不属于“步骤/节点/step + 序号”的 `step_name`;否则把 `manager` 映射为“主管审批”、`finance|finance_mgr` 映射为“财务复核”、`ceo` 映射为“负责人复核”,其余统一为“审批”。标题和详情均不暴露原始 step ID、角色码或序号。
|
||
- workflow 实例已创建但 `workflow_tasks` 暂为空时,中间审批段按提交快照生成无姓名 pending 节点;workflow 整体不存在时才回退为“主管审批、财务复核”默认占位,避免申请刚提交时出现空白进度。
|
||
- workflow task 提供实际 assignee/completer/outcome,history 提供动作、意见和时间。已完成取实际 completer/actor,当前取真实 pending assignee;未来 task 尚未创建时 `actorName=null`,严禁按当前角色持有人猜姓名。
|
||
- 付款汇总读取与当前报销关联的最新 `PaymentBatchItem` 及其 batch,只选择状态、制单/复核/执行人员与时间和未归档回单计数,不返回批次其他报销、金额或材料。只有具备现有付款批次读取权限时才返回可跳转的 batch href。
|
||
- 没有批次时付款复核 detail 为“待财务创建付款批次”;draft 为“所在付款批次待非制单人复核”;reviewed 为“所在付款批次已复核,待执行付款”;failed/cancelled 如实表达失败或待重新创建。不得写成“您的报销待复核”。
|
||
- `paid` 后最后阶段完成;回单计数为 0 显示“付款已完成,回单待留存”,大于 0 显示“付款与回单已留存”。回单是最后阶段次级状态,不增加第六块。
|
||
- workflow、batch、receipt 三个来源分别 try/normalize;合法缺失不是错误,缺失来源只让对应及下游阶段保持 pending。数据库或映射异常仍按现有 5xx 处理并记录服务端错误,不用伪造“已完成”。
|
||
- PC 与移动复用一个时间轴组件。已完成无意见节点压成单行;当前、exception、returned/rejected 或有 comment 的节点展开。语义状态同时使用图标/文字/线条,不只依赖颜色。
|
||
|
||
### 13.6 验证与回退
|
||
|
||
- 后端覆盖迁移、PATCH/GET/submit 字段合同、日期倒置、非差旅报销不误拦截、关联差旅申请摘要与旧单降级。
|
||
- 后端补充进度 DTO 的 pending/approved/paid、退回/驳回、batch 五种状态、回单有无、三个来源独立缺失、未来节点无姓名和付款链接权限测试;另覆盖第三个未知职责节点、两个同角色节点互不覆盖、未来快照节点尚无 task、实例存在但 tasks 为空四条边界。
|
||
- 前端覆盖 PC/移动四块顺序、单头差旅填写/只读、材料列表与合并入口、变长审批节点紧凑时间轴、加载反馈、企微原件预览和 375px/横屏;组件只按数组 `map` 渲染,不按固定长度或位置解释职责。
|
||
- 若 JPEG/并发修复未通过目标镜像与合成火车票验收,回滚应用代码到现有 PNG 串行路径,保留原件降级;不删生产数据或回滚整库。
|
||
|
||
### 13.7 报销 UX 最小收口
|
||
|
||
- 报销列表把当前 `pathname + search` 作为 `location.state.from` 传给报销详情或审批任务;审批任务进入报销详情时把当前任务路径作为来源。PC 详情仅接受 `/reimbursements` 列表和 `/workflow/tasks/:id` 两类内部来源,拒绝绝对 URL、协议相对 URL、hash、当前页和其他模块路径。
|
||
- 报销详情在 `EntityOverviewHero` 之前渲染一个文字明确的左侧返回控件;返回采用 replace,避免“返回后浏览器后退又回详情”的循环。无可信来源时降级 `/reimbursements`。
|
||
- `displayFormat.ts` 新增可接受 `string | number | null | undefined` 的人民币格式化函数;PC/移动详情删除各自金额 formatter,统一复用。列表页本轮不做横向重构,避免扩大已批准范围。
|
||
- 移动材料区继续承接发票添加;固定底栏删除同名次级按钮,只渲染提交主动作。固定栏的 safe-area、loading、禁用和提交校验保持不变。
|
||
- 回退只需还原前端提交;不涉及 API、数据库、权限、workflow、生产配置或真实数据。
|
||
|
||
## 14. 生产部署防重复事故合同
|
||
|
||
### 14.1 根因与边界
|
||
|
||
- 正确发布提交 `5dac336` 前,首次尝试把 Compose `working_dir` 误当仓库根目录。该标签实际指向 `deploy/trialrun` 配置目录;现有 `deploy.sh` 又到 rsync 之后才检查远端 `.env`,因此错误目标先被同步,随后才因重复的 env 路径停止。旧容器未重建,但远端 `.env` 需从仍运行容器的项目允许键原子恢复。
|
||
- 发布慢的主因不是 Vite;现场 Vite 生产构建约 11 秒,而 server `npm ci` 约 8 分钟。Dockerfile 在依赖层之前注入每次变化的 build args,使 `apk add` 与两套依赖层随提交 SHA 一起失效。
|
||
- 当前加固只改部署脚本、Dockerfile、脱敏输出、测试和既有 runbook/spec;不修改数据库、业务状态、生产 `.env` 值或真实报销数据。加固完成后需另行批准才再次部署。
|
||
|
||
### 14.2 同步前只读 preflight
|
||
|
||
- `DEPLOY_DATA_MODE=existing` 时,`deploy.sh` 在创建目录或 rsync 前通过 SSH 验证:`REMOTE_DIR` 已存在且同时含 `server/package.json` 与 `deploy/trialrun/docker-compose.yml`;PGDATA 含 `PG_VERSION`;附件目录存在。
|
||
- `SYNC_ENV=0` 时,同一 preflight 必须确认 `REMOTE_ENV_FILE` 是普通文件;若位于 `REMOTE_DIR` 内,继续使用当前精确相对路径保护。任何验证失败均在 rsync 前退出。
|
||
- 已有生产部署的参数只从受控 automatic-backup systemd unit 的 allowlist 环境读取,并交叉核对活动 Compose project 与挂载;禁止从 `com.docker.compose.project.working_dir` 猜仓库根目录。首次部署继续使用人工审核的显式参数。
|
||
|
||
### 14.3 Docker 缓存与构建身份
|
||
|
||
- `base` 只安装稳定系统依赖;client/server dependency stages 只由各自 lockfile(以及 Prisma generate 所需 schema)决定。
|
||
- `VITE_BUILD_SHA` 只在 `client_build` 执行 Vite 前注入;`BUILD_SHA` 只在 `server_build`/最终运行层注入。改变 SHA 必须重建应用产物,但不得重跑基础包和 `npm ci`。
|
||
- 静态合同测试断言 Dockerfile 中 `apk add`、client `npm ci`、server `npm ci` 均早于对应 build arg;既有 deploy 来源、`/ready` 和公网短 SHA 检查继续作为运行时证明。
|
||
|
||
### 14.4 日志与回退
|
||
|
||
- seed/smoke 只输出 readiness、计数和布尔状态,不输出完整邮箱、电话、企业微信 corp/agent 标识、redirect URI 或一次性密码。需要首次凭据时必须通过受控配置渠道提供,不从部署日志交付。
|
||
- preflight 失败不需要回滚,因为尚未同步。镜像构建或 smoke 失败时沿用旧容器/既有发布回退合同,不通过重建 `.env` 作为常规路径。
|
||
- 回归至少覆盖错误仓库根、缺失远端 env、无效 existing 数据目录都不会调用 rsync;正确合同可进入 rsync;Docker 缓存顺序与日志禁敏片段均有静态断言。
|
||
|
||
## 15. 五人推广前的采用扩展就绪检查
|
||
|
||
### 15.1 目标与证据层级
|
||
|
||
- 本阶段回答“当前生产是否具备让第二位及更多真实员工开始走报销的前置条件”,不再回答“mock 自动化能否走通界面”。代码测试证明合同实现,生产只读事实证明账号、权限、配置和历史业务当前状态,两类证据不得互相替代。
|
||
- 既有任务证据已确认至少一笔真实报销完成提交、主管审批、财务复核与付款批次复核;采用扩展查询须现场判断它是否继续完成到付款、回单,以及其他记录的实际停点,不能沿用旧快照假定完整闭环。
|
||
- `people_lifecycle` 仅是采用度模块口径;人员分母从 `employees` 及其认证/绑定关系读取。法体配置只作为现状字段输出,不进入 go/no-go 硬门禁。
|
||
|
||
### 15.2 只读数据合同
|
||
|
||
只读结果按六组输出,所有分组使用各自实体独立计数:
|
||
|
||
1. **人员与登录**:active 员工数、具备至少一种可登录标识的 active 员工数、无可登录标识数;只输出计数与布尔,不输出标识本身。
|
||
2. **关系与权限**:普通员工主管关系覆盖数;`expense.submit`、`invoice.upload`、`expense.approve` 分别按适用人群统计具备/缺失数量,不用角色名称代替有效权限计算。
|
||
3. **主数据与门禁**:active 费用类型数、active 报销 workflow 数、相关 feature flag/effective state、提交前检查所需配置;active 法体数量单列为观察项。
|
||
4. **票据能力**:上传路径是否启用、OCR provider/mode 是否为受支持现场值、必要 worker/队列状态是否可观察;只输出枚举、布尔和计数,不输出 Secret、文件或 OCR payload。
|
||
5. **生命周期漏斗**:报销单按 `draft/reviewing/approved/paid/rejected/cancelled`,票据池按 `pending_ocr/ocr_failed/available/bound/archived`,workflow instance/task 按各自枚举,付款批次按 `draft/reviewed/executed/failed/cancelled`,回单按存在/缺失独立聚合。
|
||
6. **闭环判定**:以关联关系证明是否存在至少一条“已提交 → workflow 完成 → 付款批次已执行 → 回单存在”的完整链;同时把其他记录归入明确停点。不得通过总量相等或时间接近推定关联。
|
||
|
||
本地汇总器必须 fail closed:预期枚举、模块或查询结果缺失,字段名不匹配,或一个预期有分类输入的过滤器产出空集合时,报告失败并保留原因;禁止将解析失败序列化为全零。
|
||
|
||
### 15.3 执行边界与停止条件
|
||
|
||
- 先完成本地静态合同复核,再单独申请生产只读 PRE。PRE 仅核对受控身份、运行 SHA、migration、回滚窗口及查询能力,不读取业务正文。
|
||
- 正式 readiness 查询严格只读,输出经本地脱敏后只保留计数、布尔和状态分布;姓名、邮箱、手机号、员工号、金额、单号、UUID、票据/附件内容和原始 payload 均不得进入任务文档、日志或对话。
|
||
- 查询失败、数据模型与预期不一致、闭环无法可靠关联或脱敏断言失败时停止;允许如实标记 supplemental read,但不得把补读包装成首次严格执行。
|
||
- 发现账号、权限、主数据、OCR 或历史状态缺口时仅形成 remediation 清单。任何修复、登录或业务 mutation 都需要新的明确批准,不因“只差一项”顺手执行。
|
||
|
||
### 15.4 真人采用验证
|
||
|
||
- readiness 通过后也不自动制造测试数据。等待第二位真实员工出现真实报销需求,由其本人使用真实账号和真实票据完成正常流程;观察者只记录,不代替操作。
|
||
- 若现实条件迫使熟练管理员代走,结果必须标记为“真人摩擦下限”,不能作为五人自然采用已验证的证据。
|
||
- 摩擦点全部记录,包含发生阶段、可复现现象和是否阻断;本轮最多选择三个进入后续处理。认证、授权或数据安全问题无论排序如何都立即停止并另批。
|