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 行删除
+105
查看文件
@@ -0,0 +1,105 @@
const fastifyJwt = require("@fastify/jwt");
const fp = require("fastify-plugin");
const { AppError } = require("../utils/AppError");
const { resolvePermissionsForRoles, hasPermission } = require("../constants/permissions");
async function authPlugin(fastify) {
const { jwtSecret, accessTokenTtl } = fastify.config;
await fastify.register(fastifyJwt, {
secret: jwtSecret,
});
fastify.decorateRequest("employee", null);
fastify.decorate("issueAccessToken", (employee) => {
return fastify.jwt.sign(
{
sub: employee.id,
tokenVersion: employee.token_version,
},
{ expiresIn: accessTokenTtl },
);
});
fastify.decorate("hasPermission", (employee, permissionCode) => {
return hasPermission(employee?.permissionCodes ?? [], permissionCode);
});
fastify.decorate("requirePermission", (permissionCode) => {
return async (request) => {
await fastify.authenticate(request);
if (!fastify.hasPermission(request.employee, permissionCode)) {
throw new AppError({
code: "PERM_001",
message: "无权限",
statusCode: 403,
details: { permission: permissionCode },
});
}
};
});
fastify.decorate("authenticate", async (request) => {
try {
await request.jwtVerify();
} catch {
throw new AppError({
code: "AUTH_004",
message: "Access Token 已过期/无效",
statusCode: 401,
});
}
const employeeId = request.user?.sub;
const tokenVersion = request.user?.tokenVersion;
if (typeof employeeId !== "string" || typeof tokenVersion !== "number") {
throw new AppError({
code: "AUTH_004",
message: "Access Token 已过期/无效",
statusCode: 401,
});
}
const employee = await fastify.prisma.employee.findUnique({
where: { id: employeeId },
select: {
id: true,
status: true,
token_version: true,
department_id: true,
roles: { select: { role_code: true } },
},
});
if (!employee || employee.status !== "active") {
throw new AppError({
code: "AUTH_003",
message: "账号已离职/禁用",
statusCode: 403,
});
}
if (tokenVersion < employee.token_version) {
throw new AppError({
code: "AUTH_004",
message: "Access Token 已过期/无效",
statusCode: 401,
});
}
const roleCodes = employee.roles.map((item) => item.role_code);
const permissionCodes = resolvePermissionsForRoles(roleCodes);
request.employee = {
id: employee.id,
status: employee.status,
token_version: employee.token_version,
department_id: employee.department_id,
roleCodes,
permissionCodes,
};
});
}
module.exports = fp(authPlugin, { name: "auth" });