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 行删除
+274
查看文件
@@ -0,0 +1,274 @@
require("dotenv/config");
const { createPrismaClient } = require("../prismaClient");
const { hashPassword } = require("../utils/crypto");
async function ensureDepartment(prisma, payload) {
const now = new Date();
const existing = await prisma.department.findFirst({
where: {
name: payload.name,
parent_id: payload.parent_id ?? null,
},
});
if (existing) {
return prisma.department.update({
where: { id: existing.id },
data: {
parent_id: payload.parent_id ?? null,
archived_at: null,
updated_at: now,
},
});
}
return prisma.department.create({
data: {
name: payload.name,
parent_id: payload.parent_id ?? null,
created_at: now,
updated_at: now,
},
});
}
async function ensureEmployee(prisma, payload) {
const now = new Date();
const passwordHash = await hashPassword(payload.password);
const existing = await prisma.employee.findFirst({
where: { email: payload.email },
select: { id: true },
});
if (existing) {
const employee = await prisma.employee.update({
where: { id: existing.id },
data: {
employee_no: payload.employeeNo,
name: payload.name,
phone: payload.phone,
department_id: payload.departmentId,
title: payload.title,
direct_manager_id: payload.directManagerId,
hire_date: payload.hireDate,
status: "active",
bank_account_encrypted: "ENC_DEV_ONLY",
bank_name: "DEV_BANK",
password_hash: passwordHash,
updated_at: now,
},
select: { id: true, email: true },
});
await prisma.employeeRole.deleteMany({
where: { employee_id: employee.id },
});
await prisma.employeeRole.createMany({
data: payload.roles.map((roleCode) => ({
employee_id: employee.id,
role_code: roleCode,
created_at: now,
})),
skipDuplicates: true,
});
return employee;
}
const employee = await prisma.employee.create({
data: {
employee_no: payload.employeeNo,
name: payload.name,
phone: payload.phone,
email: payload.email,
department_id: payload.departmentId,
title: payload.title,
direct_manager_id: payload.directManagerId,
hire_date: payload.hireDate,
status: "active",
bank_account_encrypted: "ENC_DEV_ONLY",
bank_name: "DEV_BANK",
password_hash: passwordHash,
created_at: now,
updated_at: now,
},
select: { id: true, email: true },
});
await prisma.employeeRole.createMany({
data: payload.roles.map((roleCode) => ({
employee_id: employee.id,
role_code: roleCode,
created_at: now,
})),
skipDuplicates: true,
});
return employee;
}
async function main() {
const { prisma, pool } = createPrismaClient();
await prisma.$connect();
const hireDate = new Date("2020-01-01");
try {
const rootDepartment = await ensureDepartment(prisma, { name: "公司" });
const managementDepartment = await ensureDepartment(prisma, {
name: "管理部",
parent_id: rootDepartment.id,
});
const financeDepartment = await ensureDepartment(prisma, {
name: "财务部",
parent_id: rootDepartment.id,
});
const hrDepartment = await ensureDepartment(prisma, {
name: "人事部",
parent_id: rootDepartment.id,
});
const administrationDepartment = await ensureDepartment(prisma, {
name: "行政部",
parent_id: rootDepartment.id,
});
const rdDepartment = await ensureDepartment(prisma, {
name: "研发部",
parent_id: rootDepartment.id,
});
const ceo = await ensureEmployee(prisma, {
employeeNo: process.env.SEED_CEO_EMPLOYEE_NO ?? "E0001",
name: "CEO",
phone: process.env.SEED_CEO_PHONE ?? "13900000000",
email: process.env.SEED_CEO_EMAIL ?? "[email protected]",
password: process.env.SEED_CEO_PASSWORD ?? "CeoPassword1234",
departmentId: rootDepartment.id,
title: "总经理",
directManagerId: null,
roles: ["ceo", "employee"],
hireDate,
});
const admin = await ensureEmployee(prisma, {
employeeNo: process.env.SEED_ADMIN_EMPLOYEE_NO ?? "E0000",
name: "Admin",
phone: process.env.SEED_ADMIN_PHONE ?? "13800000000",
email: process.env.SEED_ADMIN_EMAIL ?? "[email protected]",
password: process.env.SEED_ADMIN_PASSWORD ?? "AdminPassword1234",
departmentId: managementDepartment.id,
title: "系统管理员",
directManagerId: ceo.id,
roles: ["admin", "employee"],
hireDate,
});
const manager = await ensureEmployee(prisma, {
employeeNo: process.env.SEED_MANAGER_EMPLOYEE_NO ?? "E0100",
name: "Manager",
phone: process.env.SEED_MANAGER_PHONE ?? "13700000000",
email: process.env.SEED_MANAGER_EMAIL ?? "[email protected]",
password: process.env.SEED_MANAGER_PASSWORD ?? "ManagerPassword1234",
departmentId: rdDepartment.id,
title: "研发经理",
directManagerId: ceo.id,
roles: ["manager", "employee"],
hireDate,
});
const employee = await ensureEmployee(prisma, {
employeeNo: process.env.SEED_EMPLOYEE_EMPLOYEE_NO ?? "E0101",
name: "Employee",
phone: process.env.SEED_EMPLOYEE_PHONE ?? "13600000000",
email: process.env.SEED_EMPLOYEE_EMAIL ?? "[email protected]",
password: process.env.SEED_EMPLOYEE_PASSWORD ?? "EmployeePassword1234",
departmentId: rdDepartment.id,
title: "研发工程师",
directManagerId: manager.id,
roles: ["employee"],
hireDate,
});
const finance = await ensureEmployee(prisma, {
employeeNo: process.env.SEED_FINANCE_EMPLOYEE_NO ?? "E0200",
name: "Finance",
phone: process.env.SEED_FINANCE_PHONE ?? "13500000000",
email: process.env.SEED_FINANCE_EMAIL ?? "[email protected]",
password: process.env.SEED_FINANCE_PASSWORD ?? "FinancePassword1234",
departmentId: financeDepartment.id,
title: "财务专员",
directManagerId: ceo.id,
roles: ["finance", "employee"],
hireDate,
});
const financeMgr = await ensureEmployee(prisma, {
employeeNo: process.env.SEED_FINANCE_MGR_EMPLOYEE_NO ?? "E0201",
name: "Finance Manager",
phone: process.env.SEED_FINANCE_MGR_PHONE ?? "13400000000",
email: process.env.SEED_FINANCE_MGR_EMAIL ?? "[email protected]",
password: process.env.SEED_FINANCE_MGR_PASSWORD ?? "FinanceMgrPassword1234",
departmentId: financeDepartment.id,
title: "财务主管",
directManagerId: ceo.id,
roles: ["finance", "finance_mgr", "employee"],
hireDate,
});
const hr = await ensureEmployee(prisma, {
employeeNo: process.env.SEED_HR_EMPLOYEE_NO ?? "E0300",
name: "HR",
phone: process.env.SEED_HR_PHONE ?? "13300000000",
email: process.env.SEED_HR_EMAIL ?? "[email protected]",
password: process.env.SEED_HR_PASSWORD ?? "HrPassword1234",
departmentId: hrDepartment.id,
title: "人事专员",
directManagerId: ceo.id,
roles: ["hr"],
hireDate,
});
const administration = await ensureEmployee(prisma, {
employeeNo: process.env.SEED_ADMINISTRATION_EMPLOYEE_NO ?? "E0400",
name: "Administration",
phone: process.env.SEED_ADMINISTRATION_PHONE ?? "13200000000",
email: process.env.SEED_ADMINISTRATION_EMAIL ?? "[email protected]",
password: process.env.SEED_ADMINISTRATION_PASSWORD ?? "AdministrationPassword1234",
departmentId: administrationDepartment.id,
title: "行政专员",
directManagerId: manager.id,
roles: ["administration"],
hireDate,
});
// eslint-disable-next-line no-console
console.log("Seeded dev users:");
// eslint-disable-next-line no-console
console.log(`- CEO: ${ceo.email} / ${process.env.SEED_CEO_PASSWORD ?? "CeoPassword1234"}`);
// eslint-disable-next-line no-console
console.log(`- Admin: ${admin.email} / ${process.env.SEED_ADMIN_PASSWORD ?? "AdminPassword1234"}`);
// eslint-disable-next-line no-console
console.log(`- Manager: ${manager.email} / ${process.env.SEED_MANAGER_PASSWORD ?? "ManagerPassword1234"}`);
// eslint-disable-next-line no-console
console.log(`- Employee: ${employee.email} / ${process.env.SEED_EMPLOYEE_PASSWORD ?? "EmployeePassword1234"}`);
// eslint-disable-next-line no-console
console.log(`- Finance: ${finance.email} / ${process.env.SEED_FINANCE_PASSWORD ?? "FinancePassword1234"}`);
// eslint-disable-next-line no-console
console.log(`- Finance Manager: ${financeMgr.email} / ${process.env.SEED_FINANCE_MGR_PASSWORD ?? "FinanceMgrPassword1234"}`);
// eslint-disable-next-line no-console
console.log(`- HR: ${hr.email} / ${process.env.SEED_HR_PASSWORD ?? "HrPassword1234"}`);
// eslint-disable-next-line no-console
console.log(`- Administration: ${administration.email} / ${process.env.SEED_ADMINISTRATION_PASSWORD ?? "AdministrationPassword1234"}`);
} finally {
await prisma.$disconnect();
await pool.end();
}
}
main().catch((error) => {
// eslint-disable-next-line no-console
console.error(error);
process.exit(1);
});