feat(server): 完成 P0+P1 后端能力(工作流、RBAC、付款归档)

P0 MVP:

- 认证:login/refresh/logout/forgot/reset + tokenVersion

- RBAC:角色到权限映射 + requirePermission

- 发票池:upload/bind/signed-url + 判重(VALID_003/VALID_005)

- 报销:create/view/submit + 单号生成

- 工作流:approve + department_manager 解析

P1 扩展:

- 动作:reject/return/withdraw(含 comment)

- transfer:任务转审 + 链路上限 3

- pay:approved -> paid + 发票归档 used_complete

- 动态审批链:approval_rules 驱动

回归脚本:

- smoke-test.sh / regression-dedup.sh / regression-workflow-actions.sh
这个提交包含在:
2026-02-07 16:43:31 +08:00
当前提交 f527633716
共修改 44 个文件,包含 11100 行新增和 0 行删除
@@ -0,0 +1,186 @@
{
"info": {
"_postman_id": "d5575dd2-7e6f-4675-9f82-b52f22de8de0",
"name": "hyb-oa P0 MVP",
"description": "P0 vertical slice: login -> draft -> upload -> bind -> submit -> approve",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "1. Login Employee",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "x-device-fingerprint", "value": "{{deviceFingerprint}}" }
],
"url": "{{baseUrl}}/api/auth/login",
"body": {
"mode": "raw",
"raw": "{\n \"identifier\": \"{{employeeIdentifier}}\",\n \"password\": \"{{employeePassword}}\"\n}"
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.environment.set('employeeAccessToken', json.data.accessToken);"
]
}
}
]
},
{
"name": "2. Create Reimbursement",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Authorization", "value": "Bearer {{employeeAccessToken}}" }
],
"url": "{{baseUrl}}/api/reimbursements",
"body": {
"mode": "raw",
"raw": "{\n \"description\": \"P0 冒烟报销单\"\n}"
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.environment.set('reimbursementId', json.data.id);"
]
}
}
]
},
{
"name": "3. Upload Invoice",
"request": {
"method": "POST",
"header": [
{ "key": "Authorization", "value": "Bearer {{employeeAccessToken}}" }
],
"url": "{{baseUrl}}/api/invoice-pool/upload",
"body": {
"mode": "formdata",
"formdata": [
{
"key": "file",
"type": "file",
"src": "{{sampleInvoiceFile}}"
},
{
"key": "ocr_type",
"type": "text",
"value": "general"
}
]
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.environment.set('invoicePoolId', json.data.invoicePoolId);"
]
}
}
]
},
{
"name": "4. Bind Invoice",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Authorization", "value": "Bearer {{employeeAccessToken}}" }
],
"url": "{{baseUrl}}/api/invoice-pool/{{invoicePoolId}}/bind",
"body": {
"mode": "raw",
"raw": "{\n \"reimbursementId\": \"{{reimbursementId}}\"\n}"
}
}
},
{
"name": "5. Submit Reimbursement",
"request": {
"method": "POST",
"header": [
{ "key": "Authorization", "value": "Bearer {{employeeAccessToken}}" }
],
"url": "{{baseUrl}}/api/reimbursements/{{reimbursementId}}/submit"
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.environment.set('workflowTaskId', json.data.currentTaskId);",
"pm.environment.set('workflowInstanceId', json.data.workflowInstanceId);"
]
}
}
]
},
{
"name": "6. Login CEO",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "x-device-fingerprint", "value": "{{deviceFingerprint}}" }
],
"url": "{{baseUrl}}/api/auth/login",
"body": {
"mode": "raw",
"raw": "{\n \"identifier\": \"{{ceoIdentifier}}\",\n \"password\": \"{{ceoPassword}}\"\n}"
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.environment.set('ceoAccessToken', json.data.accessToken);"
]
}
}
]
},
{
"name": "7. Approve Workflow Task",
"request": {
"method": "POST",
"header": [
{ "key": "Authorization", "value": "Bearer {{ceoAccessToken}}" }
],
"url": "{{baseUrl}}/api/workflow/tasks/{{workflowTaskId}}/approve"
}
},
{
"name": "8. Get Signed URL",
"request": {
"method": "POST",
"header": [
{ "key": "Authorization", "value": "Bearer {{employeeAccessToken}}" }
],
"url": "{{baseUrl}}/api/invoice-pool/{{invoicePoolId}}/signed-url"
}
}
]
}
@@ -0,0 +1,24 @@
{
"id": "6d11aa57-5444-4cc2-b9ff-b8dcfa5d44ee",
"name": "hyb-oa local",
"values": [
{ "key": "baseUrl", "value": "http://localhost:3000", "type": "default", "enabled": true },
{ "key": "deviceFingerprint", "value": "dev-local-fingerprint", "type": "default", "enabled": true },
{ "key": "sampleInvoiceFile", "value": "/tmp/hyb-oa-sample-invoice.pdf", "type": "default", "enabled": true },
{ "key": "employeeIdentifier", "value": "[email protected]", "type": "default", "enabled": true },
{ "key": "employeePassword", "value": "EmployeePassword1234", "type": "secret", "enabled": true },
{ "key": "ceoIdentifier", "value": "[email protected]", "type": "default", "enabled": true },
{ "key": "ceoPassword", "value": "CeoPassword1234", "type": "secret", "enabled": true },
{ "key": "employeeAccessToken", "value": "", "type": "default", "enabled": true },
{ "key": "ceoAccessToken", "value": "", "type": "default", "enabled": true },
{ "key": "reimbursementId", "value": "", "type": "default", "enabled": true },
{ "key": "invoicePoolId", "value": "", "type": "default", "enabled": true },
{ "key": "workflowTaskId", "value": "", "type": "default", "enabled": true },
{ "key": "workflowInstanceId", "value": "", "type": "default", "enabled": true }
],
"_postman_variable_scope": "environment",
"_postman_exported_at": "2026-02-06T00:00:00.000Z",
"_postman_exported_using": "Codex CLI"
}