/** * 数据字典共享契约(A4) * 依据:PRD §5.4.5、R-10;技术架构 §5.3 / §6.3.7;开发计划 A4-BE-01 / A4-BE-04。 * * 约定: * - 一张通用枚举表同时表达二级树(parent_code)与词表; * - 类型编码 / 条目编码创建后不可修改; * - 条目按「通用 → 父行业 → 本行业」三层合并,同 code 以更具体层覆盖; * - 历史数据保存填写时快照(toDictItemSnapshot),字典改名不影响历史。 */ import { z } from 'zod'; import { cursorPaginationQuerySchema } from './api.js'; import { isoDateTimeSchema, ulidSchema } from './common.js'; // --------------------------------------------------------------------------- // 枚举 / 分组 // --------------------------------------------------------------------------- export const DICT_GROUP_KEYS = ['content', 'platform', 'quality', 'audience'] as const; export const dictGroupKeySchema = z.enum(DICT_GROUP_KEYS); export type DictGroupKey = z.infer; export const DICT_GROUPS: readonly { key: DictGroupKey; name: string; description: string; }[] = [ { key: 'content', name: '内容生产', description: '脚本、素材与投放的结构化选项' }, { key: 'platform', name: '平台与规格', description: '投放平台、尺寸比例与光影风格' }, { key: 'quality', name: '质量与合规', description: '失败原因、淘汰理由与合规词表' }, { key: 'audience', name: '人群与投放', description: '人群标签与素材形态' }, ]; export const DICT_TYPE_KINDS = ['tree', 'list'] as const; export const dictTypeKindSchema = z.enum(DICT_TYPE_KINDS); export type DictTypeKind = z.infer; export const DICT_STATUSES = ['active', 'disabled'] as const; export const dictStatusSchema = z.enum(DICT_STATUSES); export type DictStatus = z.infer; /** 生效条目的来源层(不是存储字段;由三层合并计算) */ export const DICT_LAYER_SOURCES = ['generic', 'parent', 'current'] as const; export const dictLayerSourceSchema = z.enum(DICT_LAYER_SOURCES); export type DictLayerSource = z.infer; export const DICT_ITEM_SOURCES = DICT_LAYER_SOURCES; export const dictItemSourceSchema = dictLayerSourceSchema; export type DictItemSource = z.infer; // --------------------------------------------------------------------------- // 编码 / 名称 // --------------------------------------------------------------------------- /** 类型编码:小写 snake_case,创建后不可改 */ export const dictTypeCodeSchema = z .string() .trim() .regex(/^[a-z][a-z0-9_]{1,48}$/, '类型编码只能使用小写字母、数字与下划线,且以字母开头'); /** 条目编码:字母 / 数字 / 下划线 / 短横线,创建后不可改 */ export const dictItemCodeSchema = z .string() .trim() .regex( /^[A-Za-z0-9][A-Za-z0-9_-]{1,48}$/, '条目编码只能使用字母、数字、下划线或短横线,长度 2–49', ); export const dictNameSchema = z .string() .trim() .min(1, '名称不能为空') .max(64, '名称最多 64 个字符'); export const dictDescriptionSchema = z.string().trim().max(255, '说明最多 255 个字符'); export const dictItemRemarkSchema = z.string().trim().max(255, '备注最多 255 个字符'); export const dictSortOrderSchema = z.coerce.number().int().min(0).max(9999); // --------------------------------------------------------------------------- // 响应资源 // --------------------------------------------------------------------------- export const dictTypeSummarySchema = z.object({ code: z.string().min(1), name: z.string().min(1), group: dictGroupKeySchema, kind: dictTypeKindSchema, description: z.string(), industryScoped: z.boolean(), builtin: z.boolean(), status: dictStatusSchema, sortOrder: z.number().int(), itemCount: z.number().int().nonnegative(), activeItemCount: z.number().int().nonnegative(), industryCount: z.number().int().nonnegative(), updatedAt: isoDateTimeSchema, updatedBy: z.string().nullable(), }); export type DictTypeSummary = z.infer; export const dictTypeDetailSchema = dictTypeSummarySchema.extend({ /** 类型下所有条目(不合并),用于编辑 / 诊断;列表与生效项走 items 接口 */ items: z.lazy(() => z.array(dictItemSchema)).optional(), }); export type DictTypeDetail = z.infer; export const dictItemSchema = z.object({ id: ulidSchema, typeCode: z.string().min(1), code: z.string().min(1), name: z.string().min(1), parentCode: z.string().nullable(), parentName: z.string().nullable(), industryId: ulidSchema.nullable(), industryPath: z.string().nullable(), sortOrder: z.number().int(), status: dictStatusSchema, remark: z.string(), source: dictItemSourceSchema, overridden: z.boolean(), refCount: z.number().int().nonnegative(), updatedAt: isoDateTimeSchema, updatedBy: z.string().nullable(), }); export type DictItem = z.infer; export const dictLayerSchema = z.object({ source: dictLayerSourceSchema, industryId: ulidSchema.nullable(), label: z.string().min(1), count: z.number().int().nonnegative(), }); export type DictLayer = z.infer; export const dictItemListResponseSchema = z.object({ items: z.array(dictItemSchema), next_cursor: z.string().nullable(), layers: z.array(dictLayerSchema), effectiveCount: z.number().int().nonnegative(), overriddenCount: z.number().int().nonnegative(), disabledCount: z.number().int().nonnegative(), }); export type DictItemListResponse = z.infer; // --------------------------------------------------------------------------- // 查询 / 写接口 // --------------------------------------------------------------------------- export const dictTypeListQuerySchema = cursorPaginationQuerySchema.extend({ keyword: z.string().trim().max(64).optional(), group: dictGroupKeySchema.optional(), status: z.enum(['all', 'active', 'disabled']).default('all'), }); export type DictTypeListQuery = z.infer; export const dictItemListQuerySchema = z.object({ /** 不传 = 全平台(仅通用层);传一级行业 = 通用 + 该行业;传二级 = 通用 + 父行业 + 该行业 */ industry_id: ulidSchema.optional(), }); export type DictItemListQuery = z.infer; export const createDictTypeInputSchema = z .object({ code: dictTypeCodeSchema, name: dictNameSchema, group: dictGroupKeySchema, kind: dictTypeKindSchema.default('list'), description: dictDescriptionSchema.default(''), industryScoped: z.boolean().default(false), sortOrder: dictSortOrderSchema.default(10), }) .strict(); export type CreateDictTypeInput = z.infer; /** 类型编码 / 形态 / 行业作用域创建后不可改;名称、说明、分组、排序可改 */ export const updateDictTypeInputSchema = z .object({ name: dictNameSchema.optional(), group: dictGroupKeySchema.optional(), description: dictDescriptionSchema.optional(), sortOrder: dictSortOrderSchema.optional(), }) .strict(); export type UpdateDictTypeInput = z.infer; export const setDictTypeStatusInputSchema = z .object({ status: dictStatusSchema, }) .strict(); export type SetDictTypeStatusInput = z.infer; export const createDictItemInputSchema = z .object({ code: dictItemCodeSchema, name: dictNameSchema, parentCode: z.string().trim().max(64).nullable().optional(), industryId: ulidSchema.nullable().optional(), sortOrder: dictSortOrderSchema.default(10), status: dictStatusSchema.default('active'), remark: dictItemRemarkSchema.default(''), }) .strict(); export type CreateDictItemInput = z.infer; /** 编辑条目:编码、父级、所属行业创建后不可改;名称、排序、启停、备注可改 */ export const updateDictItemInputSchema = z .object({ name: dictNameSchema.optional(), sortOrder: dictSortOrderSchema.optional(), status: dictStatusSchema.optional(), remark: dictItemRemarkSchema.optional(), }) .strict(); export type UpdateDictItemInput = z.infer; // --------------------------------------------------------------------------- // 快照(R-10):业务记录引用字典时保存填写时名称 / 编码 / 作用域 // --------------------------------------------------------------------------- export const dictItemSnapshotSchema = z.object({ typeCode: z.string().min(1), code: z.string().min(1), name: z.string().min(1), parentCode: z.string().nullable(), industryId: ulidSchema.nullable(), capturedAt: isoDateTimeSchema, }); export type DictItemSnapshot = z.infer; /** 生成填写时快照;A4 只提供契约与工具,真正落库由 B0 / M1 业务记录执行 */ export function toDictItemSnapshot( item: Pick, capturedAt: string, ): DictItemSnapshot { return { typeCode: item.typeCode, code: item.code, name: item.name, parentCode: item.parentCode, industryId: item.industryId, capturedAt, }; }