feat(P25-1/P25-5/P26-1/P26-3/P24-1/P29-4): 落地内部OA够用版后端与试运行能力
关联 Issue: P25-1, P25-2, P25-3, P25-4, P25-5, P26-1, P26-3, P24-1, P24-2, P29-1, P29-2, P29-3, P29-4
这个提交包含在:
@@ -0,0 +1,125 @@
|
||||
const { AppError } = require("../utils/AppError");
|
||||
const { sendData } = require("../utils/reply");
|
||||
const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureFlags");
|
||||
const { buildPersistedRiskFlag, toMoneyString } = require("../services/feeControl");
|
||||
|
||||
function ensureReadPermission(fastify, request) {
|
||||
if (
|
||||
!fastify.hasPermission(request.employee, "payment.batch.manage") &&
|
||||
!fastify.hasPermission(request.employee, "risk.review.manage")
|
||||
) {
|
||||
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureFeatureEnabled(fastify) {
|
||||
const gate = await evaluateFeatureFlag({
|
||||
prisma: fastify.prisma,
|
||||
config: fastify.config,
|
||||
flagKey: FEATURE_FLAG_KEYS.paymentBatch,
|
||||
strictDepartment: false,
|
||||
});
|
||||
if (!gate.enabled) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 409,
|
||||
details: { featureFlag: "payment_batch_v1_disabled", reason: gate.reason },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function mapBatchSummary(item) {
|
||||
return {
|
||||
id: item.id,
|
||||
batchNo: item.batch_no,
|
||||
entityType: item.entity_type,
|
||||
name: item.name,
|
||||
status: item.status,
|
||||
createdAt: item.created_at?.toISOString?.() ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async function apOperationsRoutes(fastify) {
|
||||
fastify.get("/ap-operations", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||
await ensureFeatureEnabled(fastify);
|
||||
ensureReadPermission(fastify, request);
|
||||
|
||||
const [batches, failedItems, openRiskFlags, anomalies] = await Promise.all([
|
||||
fastify.prisma.paymentBatch.findMany({
|
||||
where: { status: { in: ["draft", "reviewed", "failed"] } },
|
||||
orderBy: [{ created_at: "desc" }, { id: "desc" }],
|
||||
take: 20,
|
||||
}),
|
||||
fastify.prisma.paymentBatchItem.findMany({
|
||||
where: { status: "failed" },
|
||||
orderBy: [{ created_at: "desc" }, { id: "desc" }],
|
||||
take: 50,
|
||||
}),
|
||||
fastify.prisma.entityRiskFlag.findMany({
|
||||
where: { status: "open" },
|
||||
orderBy: [{ created_at: "desc" }, { id: "desc" }],
|
||||
take: 50,
|
||||
}),
|
||||
fastify.prisma.travelReconciliationItem.findMany({
|
||||
where: {
|
||||
resolution_status: "pending",
|
||||
match_status: { not: "matched" },
|
||||
},
|
||||
orderBy: [{ created_at: "desc" }, { id: "desc" }],
|
||||
take: 50,
|
||||
select: {
|
||||
id: true,
|
||||
travel_reconciliation_batch_id: true,
|
||||
travel_order_id: true,
|
||||
travel_request_id: true,
|
||||
match_status: true,
|
||||
resolution_status: true,
|
||||
anomaly_code: true,
|
||||
order_amount: true,
|
||||
transaction_amount: true,
|
||||
currency: true,
|
||||
created_at: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return sendData(reply, request, {
|
||||
summary: {
|
||||
openPaymentBatchCount: batches.length,
|
||||
failedPaymentBatchItemCount: failedItems.length,
|
||||
openRiskFlagCount: openRiskFlags.length,
|
||||
reconciliationAnomalyCount: anomalies.length,
|
||||
},
|
||||
openPaymentBatches: batches.map(mapBatchSummary),
|
||||
failedPaymentBatchItems: failedItems.map((item) => ({
|
||||
id: item.id,
|
||||
paymentBatchId: item.payment_batch_id,
|
||||
entityId: item.entity_id,
|
||||
entityCode: item.entity_code,
|
||||
entityTitle: item.entity_title,
|
||||
amount: toMoneyString(item.amount),
|
||||
currencyCode: item.currency_code,
|
||||
failureCode: item.failure_code ?? null,
|
||||
failureMessage: item.failure_message ?? null,
|
||||
createdAt: item.created_at?.toISOString?.() ?? null,
|
||||
})),
|
||||
openRiskFlags: openRiskFlags.map(buildPersistedRiskFlag),
|
||||
reconciliationAnomalies: anomalies.map((item) => ({
|
||||
id: item.id,
|
||||
travelReconciliationBatchId: item.travel_reconciliation_batch_id,
|
||||
travelOrderId: item.travel_order_id ?? null,
|
||||
travelRequestId: item.travel_request_id ?? null,
|
||||
matchStatus: item.match_status,
|
||||
resolutionStatus: item.resolution_status,
|
||||
anomalyCode: item.anomaly_code ?? null,
|
||||
orderAmount: item.order_amount != null ? toMoneyString(item.order_amount) : null,
|
||||
transactionAmount: item.transaction_amount != null ? toMoneyString(item.transaction_amount) : null,
|
||||
currency: item.currency,
|
||||
createdAt: item.created_at?.toISOString?.() ?? null,
|
||||
})),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = apOperationsRoutes;
|
||||
在新工单中引用
屏蔽一个用户