Files
lingxi/packages/shared/src/schemas/dict.ts
T
Stev_Wang cc6e84cdbc feat(shared): A4 数据字典与系统配置契约
背景:A4 需要字典三层合并、配置版本与命名规则的共享契约。\n改动:新增 dict/config 契约、system-configs 定义与校验、命名模板解析;新增 dict_types/dict_items/system_config_versions 迁移与 13 类/96 条字典、26 项配置种子。\n影响面:shared 新增导出;prisma 新增表与迁移;客户基础字段 DTO 硬上界调整为 50(实际生效上限仍由配置动态校验)。\n回滚:回滚本提交并保留新增表(不删数据)。\n验证:shared typecheck/test 147;seed 幂等;prisma migrate dev 已应用。
2026-11-04 11:48:38 +08:00

259 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 数据字典共享契约(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<typeof dictGroupKeySchema>;
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<typeof dictTypeKindSchema>;
export const DICT_STATUSES = ['active', 'disabled'] as const;
export const dictStatusSchema = z.enum(DICT_STATUSES);
export type DictStatus = z.infer<typeof dictStatusSchema>;
/** 生效条目的来源层(不是存储字段;由三层合并计算) */
export const DICT_LAYER_SOURCES = ['generic', 'parent', 'current'] as const;
export const dictLayerSourceSchema = z.enum(DICT_LAYER_SOURCES);
export type DictLayerSource = z.infer<typeof dictLayerSourceSchema>;
export const DICT_ITEM_SOURCES = DICT_LAYER_SOURCES;
export const dictItemSourceSchema = dictLayerSourceSchema;
export type DictItemSource = z.infer<typeof dictItemSourceSchema>;
// ---------------------------------------------------------------------------
// 编码 / 名称
// ---------------------------------------------------------------------------
/** 类型编码:小写 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<typeof dictTypeSummarySchema>;
export const dictTypeDetailSchema = dictTypeSummarySchema.extend({
/** 类型下所有条目(不合并),用于编辑 / 诊断;列表与生效项走 items 接口 */
items: z.lazy(() => z.array(dictItemSchema)).optional(),
});
export type DictTypeDetail = z.infer<typeof dictTypeDetailSchema>;
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<typeof dictItemSchema>;
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<typeof dictLayerSchema>;
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<typeof dictItemListResponseSchema>;
// ---------------------------------------------------------------------------
// 查询 / 写接口
// ---------------------------------------------------------------------------
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<typeof dictTypeListQuerySchema>;
export const dictItemListQuerySchema = z.object({
/** 不传 = 全平台(仅通用层);传一级行业 = 通用 + 该行业;传二级 = 通用 + 父行业 + 该行业 */
industry_id: ulidSchema.optional(),
});
export type DictItemListQuery = z.infer<typeof dictItemListQuerySchema>;
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<typeof createDictTypeInputSchema>;
/** 类型编码 / 形态 / 行业作用域创建后不可改;名称、说明、分组、排序可改 */
export const updateDictTypeInputSchema = z
.object({
name: dictNameSchema.optional(),
group: dictGroupKeySchema.optional(),
description: dictDescriptionSchema.optional(),
sortOrder: dictSortOrderSchema.optional(),
})
.strict();
export type UpdateDictTypeInput = z.infer<typeof updateDictTypeInputSchema>;
export const setDictTypeStatusInputSchema = z
.object({
status: dictStatusSchema,
})
.strict();
export type SetDictTypeStatusInput = z.infer<typeof setDictTypeStatusInputSchema>;
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<typeof createDictItemInputSchema>;
/** 编辑条目:编码、父级、所属行业创建后不可改;名称、排序、启停、备注可改 */
export const updateDictItemInputSchema = z
.object({
name: dictNameSchema.optional(),
sortOrder: dictSortOrderSchema.optional(),
status: dictStatusSchema.optional(),
remark: dictItemRemarkSchema.optional(),
})
.strict();
export type UpdateDictItemInput = z.infer<typeof updateDictItemInputSchema>;
// ---------------------------------------------------------------------------
// 快照(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<typeof dictItemSnapshotSchema>;
/** 生成填写时快照;A4 只提供契约与工具,真正落库由 B0 / M1 业务记录执行 */
export function toDictItemSnapshot(
item: Pick<DictItem, 'typeCode' | 'code' | 'name' | 'parentCode' | 'industryId'>,
capturedAt: string,
): DictItemSnapshot {
return {
typeCode: item.typeCode,
code: item.code,
name: item.name,
parentCode: item.parentCode,
industryId: item.industryId,
capturedAt,
};
}