feat(P11-P12): 落地治理开关与移动端MVP(G0-G6/H1/M1-M4)
- 新增 Feature Flag 模型、评估服务与管理 API(erp_sync/invoice_verification)\n- 完成 ERP 与验真灰度收口(路由/服务/作业统一判定)\n- 补齐 pay 与手动 sync 的 department_id 灰度上下文\n- 增加管理后台开关页、PWA 基础设施与移动拍照/审批页面\n- 增加回归脚本、安全基线与压测脚本,更新文档与执行合同\n\nIssue: G0,G1,G2,G3,G4,G5,G6,H1,M1,M2,M3,M4
这个提交包含在:
@@ -5,6 +5,7 @@ const { sendData } = require("../utils/reply");
|
||||
const { buildAuditLogData } = require("../utils/audit");
|
||||
const { enqueueIntegrationEvent } = require("../services/integrations/outbox");
|
||||
const { buildErpPayload } = require("../services/integrations/erp");
|
||||
const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureFlags");
|
||||
const { runIntegrationOutboxOnce } = require("../jobs/integrationOutboxJob");
|
||||
|
||||
async function integrationsRoutes(fastify) {
|
||||
@@ -14,15 +15,6 @@ async function integrationsRoutes(fastify) {
|
||||
preHandler: fastify.requirePermission("integration.manage"),
|
||||
},
|
||||
async (request, reply) => {
|
||||
if (!fastify.config.erpSyncEnabled) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 409,
|
||||
details: { integration: "erp_sync_disabled" },
|
||||
});
|
||||
}
|
||||
|
||||
const parsedId = z.string().uuid().safeParse(request.params?.reimbursementId);
|
||||
if (!parsedId.success) {
|
||||
throw new AppError({
|
||||
@@ -42,6 +34,7 @@ async function integrationsRoutes(fastify) {
|
||||
form_number: true,
|
||||
owner_id: true,
|
||||
submitter_id: true,
|
||||
department_id: true,
|
||||
status: true,
|
||||
total_amount: true,
|
||||
payment_voucher_no: true,
|
||||
@@ -67,6 +60,27 @@ async function integrationsRoutes(fastify) {
|
||||
});
|
||||
}
|
||||
|
||||
const erpGate = await evaluateFeatureFlag({
|
||||
prisma: fastify.prisma,
|
||||
config: fastify.config,
|
||||
flagKey: FEATURE_FLAG_KEYS.erpSync,
|
||||
departmentId: reimbursement.department_id ?? request.employee.department_id ?? null,
|
||||
strictDepartment: true,
|
||||
});
|
||||
|
||||
if (!erpGate.enabled) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 409,
|
||||
details: {
|
||||
integration: "feature_flag_blocked",
|
||||
featureFlag: FEATURE_FLAG_KEYS.erpSync,
|
||||
reason: erpGate.reason,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const payload = buildErpPayload({ reimbursement });
|
||||
const enqueued = await enqueueIntegrationEvent(fastify.prisma, {
|
||||
eventType: "erp.reimbursement.paid",
|
||||
@@ -85,6 +99,9 @@ async function integrationsRoutes(fastify) {
|
||||
extra: {
|
||||
outboxId: enqueued.id,
|
||||
duplicated: enqueued.duplicated,
|
||||
featureFlag: FEATURE_FLAG_KEYS.erpSync,
|
||||
departmentId: reimbursement.department_id ?? null,
|
||||
featureFlagReason: erpGate.reason,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户