文件
hyb-oa/server/src/routes/migrationRuns.js
T
wangsli e39e41287e 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
2026-03-21 18:03:20 +08:00

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;