文件
hyb-oa/server/src/routes/cutover.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

81 行
3.9 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 SCOPE_TYPE_SCHEMA = z.enum(["department", "legal_entity", "domain"]);
const WRITE_SCHEMA = z.object({
scopeType: SCOPE_TYPE_SCHEMA,
scopeKey: z.string().trim().min(1).max(64),
enabled: z.boolean().optional(),
startsAt: z.string().datetime({ offset: true }).nullable().optional(),
notes: z.string().trim().max(500).nullable().optional(),
}).strict();
const PATCH_SCHEMA = WRITE_SCHEMA.partial().refine((value) => Object.keys(value).length > 0, { message: "empty_patch" });
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; }
function parseBody(schema, body) { const parsed = schema.safeParse(body ?? {}); if (!parsed.success) throw buildValidationError(parsed.error.flatten()); 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 mapScope(item) {
return {
id: item.id,
scopeType: item.scope_type,
scopeKey: item.scope_key,
enabled: item.enabled,
startsAt: item.starts_at?.toISOString?.() ?? null,
notes: item.notes ?? null,
createdAt: item.created_at?.toISOString?.() ?? null,
updatedAt: item.updated_at?.toISOString?.() ?? null,
updatedBy: item.updated_by ?? null,
};
}
async function cutoverRoutes(fastify) {
fastify.get("/cutover/scopes", { preHandler: fastify.requirePermission("system.settings") }, async (request, reply) => {
await ensureFeatureEnabled(fastify);
const items = await fastify.prisma.cutoverScope.findMany({ orderBy: [{ scope_type: "asc" }, { scope_key: "asc" }] });
return sendData(reply, request, { items: items.map(mapScope) });
});
fastify.post("/cutover/scopes", { preHandler: fastify.requirePermission("system.settings") }, async (request, reply) => {
await ensureFeatureEnabled(fastify);
const body = parseBody(WRITE_SCHEMA, request.body);
const created = await fastify.prisma.cutoverScope.create({
data: {
scope_type: body.scopeType,
scope_key: body.scopeKey,
enabled: body.enabled ?? false,
starts_at: body.startsAt ? new Date(body.startsAt) : null,
notes: body.notes ?? null,
created_by: request.employee.id,
updated_by: request.employee.id,
},
});
return reply.code(201).send({ requestId: request.id, data: { item: mapScope(created) } });
});
fastify.patch("/cutover/scopes/:id", { preHandler: fastify.requirePermission("system.settings") }, async (request, reply) => {
await ensureFeatureEnabled(fastify);
const scopeId = parseId(request.params?.id);
const body = parseBody(PATCH_SCHEMA, request.body);
const updated = await fastify.prisma.cutoverScope.update({
where: { id: scopeId },
data: {
...(body.scopeType !== undefined ? { scope_type: body.scopeType } : {}),
...(body.scopeKey !== undefined ? { scope_key: body.scopeKey } : {}),
...(body.enabled !== undefined ? { enabled: body.enabled } : {}),
...(body.startsAt !== undefined ? { starts_at: body.startsAt ? new Date(body.startsAt) : null } : {}),
...(body.notes !== undefined ? { notes: body.notes ?? null } : {}),
updated_by: request.employee.id,
},
});
return sendData(reply, request, { item: mapScope(updated) });
});
}
module.exports = cutoverRoutes;