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 行删除
+233
查看文件
@@ -0,0 +1,233 @@
require("dotenv/config");
const { createPrismaClient } = require("../prismaClient");
const ENABLED_EXPENSE_TYPES = [
{ code: "travel_airfare", name: "机票", category: "travel" },
{ code: "travel_train", name: "火车票", category: "travel" },
{ code: "travel_taxi", name: "打车费", category: "travel" },
{ code: "travel_hotel", name: "住宿费", category: "travel" },
{ code: "travel_bus", name: "市内交通", category: "travel" },
{ code: "travel_meal", name: "差旅餐补", category: "travel" },
{ code: "travel_fuel", name: "油费", category: "travel" },
{ code: "travel_toll", name: "过路费", category: "travel" },
{ code: "travel_parking", name: "停车费", category: "travel" },
{ code: "travel_visa", name: "签证费", category: "travel" },
{ code: "rd_materials", name: "研发材料", category: "rd" },
{ code: "rd_testing", name: "测试费用", category: "rd" },
{ code: "rd_prototype", name: "样机制作", category: "rd" },
{ code: "rd_software", name: "软件订阅", category: "rd" },
{ code: "rd_external_service", name: "外部技术服务", category: "rd" },
{ code: "production_raw_material", name: "生产原材料", category: "production" },
{ code: "production_processing", name: "加工费", category: "production" },
{ code: "production_quality", name: "质量检测", category: "production" },
{ code: "production_maintenance", name: "设备维护", category: "production" },
{ code: "production_energy", name: "生产能耗", category: "production" },
{ code: "office_stationery", name: "文具耗材", category: "office" },
{ code: "office_supplies", name: "办公用品", category: "office" },
{ code: "office_property_fee", name: "物业费用", category: "office" },
{ code: "office_communication", name: "通讯费用", category: "office" },
{ code: "office_postal", name: "邮寄快递", category: "office" },
{ code: "office_cleaning", name: "保洁服务", category: "office" },
{ code: "business_client_meal", name: "客户招待", category: "business" },
{ code: "business_gift", name: "商务礼品", category: "business" },
{ code: "business_event", name: "活动执行", category: "business" },
{ code: "business_marketing", name: "营销投放", category: "business" },
{ code: "business_training", name: "培训会议", category: "business" },
{ code: "assets_equipment", name: "设备采购", category: "assets" },
{ code: "assets_furniture", name: "家具采购", category: "assets" },
{ code: "assets_it_hardware", name: "IT 硬件", category: "assets" },
{ code: "utilities", name: "水电费", category: "other" },
{ code: "rent", name: "房租租赁费", category: "other" },
{ code: "other_misc", name: "其他杂项", category: "other" },
];
const DISABLED_LEGACY_EXPENSE_TYPES = [
{ code: "transport", name: "legacy-交通费", category: "travel" },
{ code: "meal", name: "legacy-餐费", category: "business" },
{ code: "rd", name: "legacy-研发费", category: "rd" },
];
const DEFAULT_APPROVAL_RULES = [
{
name: "小额",
min_amount: "0",
max_amount: "500",
level1_role: "manager",
level2_role: "finance",
level3_role: null,
priority: 10,
},
{
name: "中额",
min_amount: "500",
max_amount: "5000",
level1_role: "manager",
level2_role: "finance_mgr",
level3_role: null,
priority: 20,
},
{
name: "大额",
min_amount: "5000",
max_amount: null,
level1_role: "manager",
level2_role: "ceo",
level3_role: "finance_mgr",
priority: 30,
},
];
const DEFAULT_WORKFLOW_DEFINITION = {
steps: [
{
id: "ceo_review",
name: "总经理审批",
type: "approval",
assignee_rule: {
type: "role",
value: "ceo",
},
},
],
};
async function seedExpenseTypes(prisma) {
const now = new Date();
const allTypes = [
...ENABLED_EXPENSE_TYPES.map((item) => ({ ...item, enabled: true })),
...DISABLED_LEGACY_EXPENSE_TYPES.map((item) => ({ ...item, enabled: false })),
];
for (const [index, item] of allTypes.entries()) {
await prisma.expenseType.upsert({
where: { code: item.code },
update: {
name: item.name,
description: null,
category: item.category,
sort_order: index + 1,
account_code_default: null,
is_system_default: true,
enabled: item.enabled,
updated_at: now,
updated_by: null,
},
create: {
code: item.code,
name: item.name,
description: null,
category: item.category,
sort_order: index + 1,
account_code_default: null,
is_system_default: true,
enabled: item.enabled,
created_at: now,
created_by: null,
updated_at: now,
updated_by: null,
},
});
}
}
async function seedApprovalRules(prisma) {
await prisma.approvalRule.deleteMany({});
await prisma.approvalRule.createMany({
data: DEFAULT_APPROVAL_RULES.map((item) => ({
...item,
enabled: true,
created_by: null,
updated_by: null,
})),
});
}
async function seedDocumentSequence(prisma) {
await prisma.documentSequence.upsert({
where: { document_type: "reimbursement" },
update: {
prefix: "BX",
date_format: "YYYYMMDD",
separator: "-",
number_length: 4,
reset_period: "none",
},
create: {
document_type: "reimbursement",
prefix: "BX",
date_format: "YYYYMMDD",
separator: "-",
number_length: 4,
reset_period: "none",
current_number: 0,
updated_by: null,
},
});
}
async function seedWorkflowDefinition(prisma) {
await prisma.workflowDefinition.updateMany({
where: {
entity_type: "reimbursement",
},
data: {
is_active: false,
},
});
await prisma.workflowDefinition.upsert({
where: {
entity_type_version: {
entity_type: "reimbursement",
version: 1,
},
},
update: {
name: "报销审批流程(MVP)",
definition_json: DEFAULT_WORKFLOW_DEFINITION,
is_active: true,
created_by: null,
},
create: {
name: "报销审批流程(MVP)",
entity_type: "reimbursement",
definition_json: DEFAULT_WORKFLOW_DEFINITION,
version: 1,
is_active: true,
created_by: null,
},
});
}
async function main() {
const { prisma, pool } = createPrismaClient();
await prisma.$connect();
try {
await seedExpenseTypes(prisma);
await seedApprovalRules(prisma);
await seedDocumentSequence(prisma);
await seedWorkflowDefinition(prisma);
// eslint-disable-next-line no-console
console.log("Seeded config data:");
// eslint-disable-next-line no-console
console.log(`- expense types: ${ENABLED_EXPENSE_TYPES.length} enabled + ${DISABLED_LEGACY_EXPENSE_TYPES.length} disabled`);
// eslint-disable-next-line no-console
console.log("- approval rules: 3");
// eslint-disable-next-line no-console
console.log("- document sequence: reimbursement(reset_period=none)");
// eslint-disable-next-line no-console
console.log("- workflow definition: reimbursement v1 (1-step role=ceo)");
} finally {
await prisma.$disconnect();
await pool.end();
}
}
main().catch((error) => {
// eslint-disable-next-line no-console
console.error(error);
process.exit(1);
});