关联 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
87 行
4.3 KiB
JavaScript
87 行
4.3 KiB
JavaScript
const { z } = require("zod");
|
|
|
|
const { AppError } = require("../utils/AppError");
|
|
const { sendData } = require("../utils/reply");
|
|
const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureFlags");
|
|
|
|
const ID_SCHEMA = z.string().uuid();
|
|
const RUN_TYPE_SCHEMA = z.enum(["employees", "departments", "budgets", "masterdata_validation"]);
|
|
|
|
function buildValidationError(details, statusCode = 400) { return new AppError({ code: "VALID_001", message: "参数校验失败", statusCode, details }); }
|
|
function parseId(value) { const parsed = ID_SCHEMA.safeParse(value); if (!parsed.success) throw buildValidationError({ id: "invalid_uuid" }); return parsed.data; }
|
|
async function ensureFeatureEnabled(fastify) {
|
|
const gate = await evaluateFeatureFlag({ prisma: fastify.prisma, config: fastify.config, flagKey: FEATURE_FLAG_KEYS.cutoverConsole, strictDepartment: false });
|
|
if (!gate.enabled) throw buildValidationError({ featureFlag: "cutover_console_v1_disabled", reason: gate.reason }, 409);
|
|
}
|
|
function parseCsv(content) {
|
|
const rows = String(content ?? "").trim().split(/\r?\n/).filter(Boolean);
|
|
if (!rows.length) return { headers: [], records: [] };
|
|
const headers = rows[0].split(",").map((item) => item.trim());
|
|
const records = rows.slice(1).map((line) => {
|
|
const cells = line.split(",");
|
|
const record = {};
|
|
headers.forEach((header, index) => { record[header] = (cells[index] ?? "").trim(); });
|
|
return record;
|
|
});
|
|
return { headers, records };
|
|
}
|
|
function requiredHeaders(runType) {
|
|
if (runType === "employees") return ["employee_no", "name", "department_name"];
|
|
if (runType === "departments") return ["name"];
|
|
if (runType === "budgets") return ["code", "name"];
|
|
return ["type", "code"];
|
|
}
|
|
function mapRun(item) {
|
|
return {
|
|
id: item.id,
|
|
runType: item.run_type,
|
|
status: item.status,
|
|
sourceFilename: item.source_filename,
|
|
summaryJson: item.summary_json ?? null,
|
|
errorJson: item.error_json ?? null,
|
|
createdAt: item.created_at?.toISOString?.() ?? null,
|
|
createdBy: item.created_by ?? null,
|
|
};
|
|
}
|
|
async function migrationRunsRoutes(fastify) {
|
|
fastify.post("/migration-runs/dry-run", { preHandler: fastify.requirePermission("system.settings") }, async (request, reply) => {
|
|
await ensureFeatureEnabled(fastify);
|
|
const part = await request.file();
|
|
if (!part) throw buildValidationError({ file: "required" });
|
|
const runType = RUN_TYPE_SCHEMA.safeParse(part.fields?.runType?.value ?? request.query?.runType ?? request.body?.runType);
|
|
if (!runType.success) throw buildValidationError({ runType: "invalid" });
|
|
const content = await part.toBuffer();
|
|
const parsed = parseCsv(content.toString("utf8"));
|
|
const required = requiredHeaders(runType.data);
|
|
const missingHeaders = required.filter((header) => !parsed.headers.includes(header));
|
|
const summaryJson = { rowCount: parsed.records.length, headers: parsed.headers, requiredHeaders: required };
|
|
const errorJson = missingHeaders.length ? { missingHeaders } : null;
|
|
const status = missingHeaders.length || parsed.records.length === 0 ? "dry_run_failed" : "dry_run_ok";
|
|
const created = await fastify.prisma.migrationRun.create({
|
|
data: {
|
|
run_type: runType.data,
|
|
status,
|
|
source_filename: part.filename,
|
|
summary_json: summaryJson,
|
|
error_json: errorJson,
|
|
created_by: request.employee.id,
|
|
},
|
|
});
|
|
return reply.code(201).send({ requestId: request.id, data: { item: mapRun(created) } });
|
|
});
|
|
|
|
fastify.get("/migration-runs", { preHandler: fastify.requirePermission("system.settings") }, async (request, reply) => {
|
|
await ensureFeatureEnabled(fastify);
|
|
const items = await fastify.prisma.migrationRun.findMany({ orderBy: [{ created_at: "desc" }, { id: "desc" }] });
|
|
return sendData(reply, request, { items: items.map(mapRun) });
|
|
});
|
|
|
|
fastify.get("/migration-runs/:id", { preHandler: fastify.requirePermission("system.settings") }, async (request, reply) => {
|
|
await ensureFeatureEnabled(fastify);
|
|
const run = await fastify.prisma.migrationRun.findUnique({ where: { id: parseId(request.params?.id) } });
|
|
if (!run) throw new AppError({ code: "SYS_404", message: "Not Found", statusCode: 404 });
|
|
return sendData(reply, request, { item: mapRun(run) });
|
|
});
|
|
}
|
|
module.exports = migrationRunsRoutes;
|