feat(server): 票据增强闭环(F9/F10/F11/F12/F13/F28)
- /invoice-pool 新增 unbind、PATCH 修正、manual archive、OCR retry、判重例外 override(含 VALID_004/006/007) - signed-url 签发/下载写入审计日志(脱敏 extra) - 新增未绑定票据自动归档作业 + 一次性执行脚本 - 回归脚本增强:workflow-actions/dedup/audit/ocr 覆盖上述场景;v3 CSV 状态推进至 DONE
这个提交包含在:
@@ -7,11 +7,11 @@ F5,报销明细CRUD与总金额重算,"新增报销明细 items 的增改删;
|
||||
F6,前端报销编辑器(明细+费用类型),"在报销详情页补齐明细编辑能力(费用类型下拉+日期+金额+说明),并保持现有提交/撤回/支付闭环","employee 可完整填写明细并提交;列表/详情金额一致;typecheck/build/e2e 通过","cd client && npm run typecheck && npm run build && npm run test:e2e",manual,TODO,TODO,TODO,"client/src/pages/ReimbursementDetailPage.tsx | client/src/api/reimbursements.ts | client/src/api/expenseTypes.ts","[""F3"",""F5""]","UI继续复用现有 shadcn 风格组件;加 data-testid"
|
||||
F7,代提交(expense.proxy_submit),"允许 administration 为员工 owner 创建/提交报销(submitter=行政,owner=员工):POST /api/reimbursements 支持 ownerEmployeeId;强制 delegation_grants 有效期校验;workflow 解析以 owner 为基准;必要时补齐 administration 角色最小权限(expense.create/submit/withdraw/invoice.upload)","无授权时 403;有授权可 create/submit 且审批人按 owner.direct_manager 解析;own 口径包含 submitter_id=me;audit_logs 记录 proxy 行为","cd server && ./scripts/regression-workflow-actions.sh",manual,DONE,DONE,DONE,"server/src/routes/reimbursements.js | server/src/utils/delegation.js | server/src/routes/workflow.js | server/src/constants/permissions.js | server/scripts/regression-workflow-actions.sh","[""F5""]","前端可后置:先保证后端权限链/审计链正确"
|
||||
F8,行政代上传(owner≠uploaded_by),"放开 invoice-pool/upload 跨 owner 上传:system.users 允许;administration 需具备 expense.proxy_submit 且存在有效 delegation_grants(scope=reimbursement_submit/all);并写 audit_logs","行政可为已授权员工上传;未授权拒绝;owner/uploaded_by 记录正确;发票归属与可见性规则不被破坏;审计可追踪","cd server && ./scripts/regression-ocr.sh && ./scripts/regression-audit.sh",manual,DONE,DONE,DONE,"server/src/routes/invoicePool.js | server/src/utils/delegation.js | server/src/routes/audit.js | server/src/constants/permissions.js","[""F7""]","保持上传不阻塞 OCR;ownerScope 查询语义不变"
|
||||
F9,票据解绑/重绑(bound->available),"新增 unbind 接口:仅当报销单处于 draft/rejected 时允许解绑;解绑后票据回到 available 并清理 linked_*","解绑权限与状态校验正确;解绑后可重新绑定其他报销草稿;不破坏判重约束","cd server && ./scripts/regression-workflow-actions.sh",manual,TODO,TODO,TODO,"server/src/routes/invoicePool.js | server/scripts/regression-workflow-actions.sh","[""F1""]","与 invoice_pool_status_invariants_chk 保持一致"
|
||||
F10,判重例外(override duplicate),"新增 finance_mgr 判重例外接口:标记 invoice_pool.is_duplicate_override=true 并记录引用原单号+原因;写 audit_logs","命中判重时普通用户仍 VALID_003;finance_mgr 可 override 后成功 bind/submit;若原单 status=approved/paid 则 VALID_004","cd server && ./scripts/regression-dedup.sh && ./scripts/regression-audit.sh",manual,TODO,TODO,TODO,"server/src/routes/invoicePool.js | server/src/routes/reimbursements.js | server/scripts/regression-dedup.sh | server/src/routes/audit.js","[""F9""]","严格遵守规格书:例外必须可审计可追溯"
|
||||
F11,OCR失败重试API(次数/频控),"新增 POST /api/invoice-pool/:id/retry-ocr:attempt<=3 且 60 秒频控;超限返回 VALID_006/VALID_007;写 audit_logs","ocr_failed 可重试回到 pending_ocr 并由 job 处理;频控与次数校验正确;mock 模式可回归","cd server && ./scripts/regression-ocr.sh && ./scripts/regression-audit.sh",manual,TODO,TODO,TODO,"server/src/routes/invoicePool.js | server/src/jobs/ocrJob.js | server/prisma/schema.prisma | server/prisma/migrations/* | server/scripts/regression-ocr.sh","[""F1""]","错误码新增需同步 client 错误文案映射"
|
||||
F12,90天未绑定自动归档(unused_timeout),"新增作业:available/ocr_failed 超过 90 天未绑定自动归档为 archived_reason=unused_timeout;并保证不参与判重","作业可重复运行幂等;归档后仍可查询/预览;不影响全局判重历史口径","cd server && ./scripts/regression-ocr.sh && manual: 调整 created_at 模拟超期后跑 job 断言",manual,TODO,TODO,TODO,"server/src/jobs/invoiceArchiveJob.js | server/src/app.js | server/README.md","[""F11""]","后续迁移到 pg-boss;当前可先 node-cron 实现"
|
||||
F13,signed-url审计与下载行为记录,"对 signed-url 签发与下载两类事件写 audit_logs(不记录敏感 token 明文)","无权限仍 403;有权限获取 signed-url 与下载时均可在 audit_logs 检索到对应记录","cd server && ./scripts/regression-audit.sh",manual,TODO,TODO,TODO,"server/src/routes/invoicePool.js | server/src/routes/audit.js","[""F1""]","extra 仅保存 invoiceId/actorId/expireAt 等脱敏信息"
|
||||
F9,票据解绑/重绑(bound->available),"新增 unbind 接口:仅当报销单处于 draft/rejected 时允许解绑;解绑后票据回到 available 并清理 linked_*","解绑权限与状态校验正确;解绑后可重新绑定其他报销草稿;不破坏判重约束","cd server && ./scripts/regression-workflow-actions.sh",manual,DONE,DONE,DONE,"server/src/routes/invoicePool.js | server/scripts/regression-workflow-actions.sh","[""F1""]","与 invoice_pool_status_invariants_chk 保持一致"
|
||||
F10,判重例外(override duplicate),"新增 finance_mgr 判重例外接口:标记 invoice_pool.is_duplicate_override=true 并记录引用原单号+原因;写 audit_logs","命中判重时普通用户仍 VALID_003;finance_mgr 可 override 后成功 bind/submit;若原单 status=approved/paid 则 VALID_004","cd server && ./scripts/regression-dedup.sh && ./scripts/regression-audit.sh",manual,DONE,DONE,DONE,"server/src/routes/invoicePool.js | server/src/routes/reimbursements.js | server/scripts/regression-dedup.sh | server/src/routes/audit.js","[""F9""]","严格遵守规格书:例外必须可审计可追溯"
|
||||
F11,OCR失败重试API(次数/频控),"新增 POST /api/invoice-pool/:id/retry-ocr:attempt<=3 且 60 秒频控;超限返回 VALID_006/VALID_007;写 audit_logs","ocr_failed 可重试回到 pending_ocr 并由 job 处理;频控与次数校验正确;mock 模式可回归","cd server && ./scripts/regression-ocr.sh && ./scripts/regression-audit.sh",manual,DONE,DONE,DONE,"server/src/routes/invoicePool.js | server/src/jobs/ocrJob.js | server/prisma/schema.prisma | server/prisma/migrations/* | server/scripts/regression-ocr.sh","[""F1""]","错误码新增需同步 client 错误文案映射"
|
||||
F12,90天未绑定自动归档(unused_timeout),"新增作业:available/ocr_failed 超过 90 天未绑定自动归档为 archived_reason=unused_timeout;并保证不参与判重","作业可重复运行幂等;归档后仍可查询/预览;不影响全局判重历史口径","cd server && ./scripts/regression-ocr.sh",manual,DONE,DONE,DONE,"server/src/jobs/invoiceArchiveJob.js | server/src/app.js | server/README.md","[""F11""]","后续迁移到 pg-boss;当前可先 node-cron 实现"
|
||||
F13,signed-url审计与下载行为记录,"对 signed-url 签发与下载两类事件写 audit_logs(不记录敏感 token 明文)","无权限仍 403;有权限获取 signed-url 与下载时均可在 audit_logs 检索到对应记录","cd server && ./scripts/regression-audit.sh",manual,DONE,DONE,DONE,"server/src/routes/invoicePool.js | server/src/routes/audit.js","[""F1""]","extra 仅保存 invoiceId/actorId/expireAt 等脱敏信息"
|
||||
F14,站内通知基础设施(DB+API),"新增 notifications 表与 API(list/markRead),用于工作流与系统事件通知","可分页读取;可标记已读;权限隔离正确;最小索引满足试运行","cd server && npm run prisma:migrate:dev && ./scripts/regression-all.sh",manual,TODO,TODO,TODO,"server/prisma/schema.prisma | server/src/routes/notifications.js | server/src/app.js","[""F1""]","只做最小字段:title/body/type/targetId/readAt"
|
||||
F15,邮件通知(SMTP/Nodemailer),"引入 nodemailer 发送邮件:新待办/审批结果/付款完成/密码重置等;失败降级不阻塞主流程;可配置开关","EMAIL_ENABLED=false 时不发送;开启后可发出测试邮件;失败仅记录 audit/日志不影响业务","manual: 配置 SMTP 并触发一次待办通知/重置密码邮件",manual,TODO,TODO,TODO,"server/src/services/email/* | server/src/config.js | server/package.json","[""F14""]","优先复用成熟 nodemailer;写回滚说明"
|
||||
F16,前端通知入口(Bell),"新增通知列表页/弹层与未读角标;支持标记已读","四角色均可见自己的通知;读写后角标正确;e2e 覆盖核心路径","cd client && npm run test:e2e",manual,TODO,TODO,TODO,"client/src/components/navigation/Topbar.tsx | client/src/pages/NotificationsPage.tsx | client/src/api/notifications.ts","[""F14""]","保持 UI 轻量,不做复杂订阅推送"
|
||||
@@ -29,7 +29,7 @@ F24,工作流扩展到其他实体(P7+),"抽象 entity_type 扩展到请假/
|
||||
F25,敏感字段加密与脱敏(P6+),"落实员工银行卡/身份证等敏感字段的可逆加密与脱敏输出,并对查看/变更写审计","对外响应仅脱敏;解密仅限特权权限并写 audit;不在日志/错误中泄露明文","manual: 创建员工并查看返回脱敏;用特权接口取明文并在 audit 检索记录",manual,TODO,TODO,TODO,"server/src/routes/users.js | server/src/services/crypto/* | server/src/routes/audit.js","[""F1""]","优先用成熟库(libsodium/crypto),并明确密钥轮换策略"
|
||||
F26,部门API(departments),"补齐部门树/列表与管理接口:GET /api/departments(默认隐藏归档,可 query 开启);POST/PATCH;POST /:id/archive(无子部门/无在职员工约束);写 audit_logs","部门树可用且过滤正确;归档约束正确;system.users 才可写;审计可追踪","manual: 启动 server;以 [email protected]/AdminPassword1234 登录拿 accessToken;curl -sS -H 'authorization: Bearer <TOKEN>' 'http://127.0.0.1:3000/api/departments' | jq;curl -sS -X POST -H 'authorization: Bearer <TOKEN>' -H 'content-type: application/json' 'http://127.0.0.1:3000/api/departments' -d '{name:...}';curl -sS -X PATCH -H 'authorization: Bearer <TOKEN>' -H 'content-type: application/json' 'http://127.0.0.1:3000/api/departments/<ID>' -d '{name:...}';curl -sS -X POST -H 'authorization: Bearer <TOKEN>' 'http://127.0.0.1:3000/api/departments/<ID>/archive';可选 curl -sS -H 'authorization: Bearer <TOKEN>' 'http://127.0.0.1:3000/api/audit-logs?action=DEPARTMENT_*' | jq",manual,DONE,DONE,DONE,"server/src/routes/departments.js | server/src/app.js | server/README.md","[""F1""]","单机试运行优先;树构造按 parent_id 组装"
|
||||
F27,员工API对齐(新增别名 /api/employees),"对齐规格书:在保留 /api/users 的同时新增 /api/employees 别名路由;补齐 GET详情/PATCH更新/POST archive;并保证本人可读自己的详情","/api/employees 与 /api/users 行为一致;system.users 可增改归档;本人可 GET 自己;审计完整","manual: 启动 server;以 [email protected]/AdminPassword1234 登录拿 accessToken;curl -sS -H 'authorization: Bearer <TOKEN>' 'http://127.0.0.1:3000/api/employees?page=1&pageSize=1' | jq;取 employeeId 后 curl -sS -H 'authorization: Bearer <TOKEN>' 'http://127.0.0.1:3000/api/employees/<ID>' | jq;再用普通 employee 登录拿 <EMP_TOKEN> 验证仅可 GET 自己且 GET 他人返回 403 PERM_001",manual,DONE,DONE,DONE,"server/src/routes/users.js | server/src/routes/employees.js | server/src/app.js | server/README.md","[""F26""]","优先复用既有 usersRoutes 实现,避免重复逻辑"
|
||||
F28,票据人工修正与手工归档(invoice-pool patch/archive),"补齐 PATCH /api/invoice-pool/:id(remarks/人工修正关键字段,允许 ocr_failed -> available);补齐 POST /api/invoice-pool/:id/archive(manual,未绑定才允许);写 audit_logs(INVOICE_POOL_EDITED/INVOICE_POOL_ARCHIVED)","服务端鉴权正确(owner/uploader/invoice.manage);人工修正后可参与 bind/判重;手工归档后不影响历史判重口径;审计可检索","cd server && ./scripts/regression-audit.sh",manual,TODO,TODO,TODO,"server/src/routes/invoicePool.js | server/scripts/regression-audit.sh | server/README.md","[""F1""]","extra.changes 仅记录必要字段的前后快照(避免敏感/大字段)"
|
||||
F28,票据人工修正与手工归档(invoice-pool patch/archive),"补齐 PATCH /api/invoice-pool/:id(remarks/人工修正关键字段,允许 ocr_failed -> available);补齐 POST /api/invoice-pool/:id/archive(manual,未绑定才允许);写 audit_logs(INVOICE_POOL_EDITED/INVOICE_POOL_ARCHIVED)","服务端鉴权正确(owner/uploader/invoice.manage);人工修正后可参与 bind/判重;手工归档后不影响历史判重口径;审计可检索","cd server && ./scripts/regression-audit.sh",manual,DONE,DONE,DONE,"server/src/routes/invoicePool.js | server/scripts/regression-audit.sh | server/README.md","[""F1""]","extra.changes 仅记录必要字段的前后快照(避免敏感/大字段)"
|
||||
F29,配置只读API(approval-rules/document-sequences),"补齐只读配置接口:GET /api/approval-rules?enabled=true(登录即可);GET /api/document-sequences/reimbursement(system.settings 可选)","两接口返回结构稳定且不暴露敏感;不影响现有 submit 生成单号逻辑","manual: 启动 server;任意登录拿 accessToken;curl -sS -H 'authorization: Bearer <TOKEN>' 'http://127.0.0.1:3000/api/approval-rules?enabled=true' | jq;以 admin 登录拿 <ADMIN_TOKEN>;curl -sS -H 'authorization: Bearer <ADMIN_TOKEN>' 'http://127.0.0.1:3000/api/document-sequences/reimbursement' | jq",manual,DONE,DONE,DONE,"server/src/routes/configRead.js | server/src/app.js | server/README.md","[""F1""]","仅只读;写接口与更复杂可视化后置"
|
||||
F30,通知触发集成(workflow/pay/auth),"把关键事件写入 notifications 并可选触发邮件:新待办/审批结果/付款完成/密码重置等;失败降级不阻塞主流程","提交后审批人收到通知;审批后 owner/submitter 收到结果;付款后收到通知;EMAIL_ENABLED=false 时仅站内通知;审计/日志可追踪","manual: 走一条 submit->approve->pay 链路并验证 notifications 与(可选)邮件",manual,TODO,TODO,TODO,"server/src/routes/workflow.js | server/src/routes/reimbursements.js | server/src/services/notifications/* | server/src/services/email/*","[""F14""]","先做站内通知,邮件作为可选增强(复用 nodemailer)"
|
||||
F31,Delegation Grants API对齐(新增别名/增强响应),"新增 /api/delegation-grants 别名路由并增强现有 delegation API:响应内嵌 principal/delegate 员工摘要;system.users 可代维护 principal;delegate 资格校验:scope=reimbursement_submit 需 expense.proxy_submit(administration);scope=all 需 expense.approve;支持 includeInactive","/api/delegation-grants 与 /api/delegations 兼容;返回包含员工摘要;无权限阻断;代维护审计完整","manual: 启动 server;以 [email protected]/AdminPassword1234 登录拿 accessToken;curl -sS -H 'authorization: Bearer <TOKEN>' 'http://127.0.0.1:3000/api/delegation-grants' | jq;curl -sS -X POST -H 'authorization: Bearer <TOKEN>' -H 'content-type: application/json' 'http://127.0.0.1:3000/api/delegation-grants' -d '{delegateEmployeeId:... scope:... validTo:...}';验证 /api/delegations/mine 与 /api/delegation-grants 返回一致且包含 principal/delegate 摘要",manual,DONE,DONE,DONE,"server/src/routes/delegations.js | server/src/app.js | server/README.md","[""F27""]","不破坏既有 /api/delegations 客户端;新增别名优先"
|
||||
|
||||
|
在新工单中引用
屏蔽一个用户