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 已应用。
This commit is contained in:
1 parent
32b44bdeec
commit
cc6e84cdbc
15 files changed
+2899
-75
No files matched your search
@@ -84,8 +84,8 @@ describe('客户契约', () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('新建:基础字段 ≤ 10,接受完整偏好', () => {
|
||||
const baseFields = Array.from({ length: 11 }, (_, index) => ({
|
||||
it('新建:基础字段受 DTO 硬上限 50 保护,实际上限由 system_configs 动态校验', () => {
|
||||
const baseFields = Array.from({ length: 51 }, (_, index) => ({
|
||||
key: `k${index}`,
|
||||
value: `v${index}`,
|
||||
}));
|
||||
|
||||
@@ -37,10 +37,16 @@ export const CUSTOMER_FIELD_STATUSES = ['pending', 'active', 'rejected', 'disabl
|
||||
export const customerFieldStatusSchema = z.enum(CUSTOMER_FIELD_STATUSES);
|
||||
export type CustomerFieldStatus = z.infer<typeof customerFieldStatusSchema>;
|
||||
|
||||
/** 上限:只存系统配置前的共享常量(A4 迁入 system_configs,R-26) */
|
||||
/** 默认上限:A4 起权威来源为 system_configs;常量仅作前端兜底与默认值 */
|
||||
export const CUSTOMER_BASE_FIELD_LIMIT = 10;
|
||||
export const CUSTOMER_GEN_FIELD_LIMIT = 5;
|
||||
|
||||
/**
|
||||
* 共享 DTO 的硬上限(防御性上界,与 system_configs 定义的最大值一致)。
|
||||
* 实际生效上限由后端 service 读取 system_configs 动态校验(R-26)。
|
||||
*/
|
||||
export const CUSTOMER_BASE_FIELD_HARD_LIMIT = 50;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 行业
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -258,7 +264,7 @@ export const createCustomerInputSchema = z
|
||||
contactName: customerContactNameSchema,
|
||||
contactPhone: customerContactPhoneSchema,
|
||||
preferences: customerPreferencesSchema.optional(),
|
||||
baseFields: z.array(baseFieldSchema).max(CUSTOMER_BASE_FIELD_LIMIT).optional(),
|
||||
baseFields: z.array(baseFieldSchema).max(CUSTOMER_BASE_FIELD_HARD_LIMIT).optional(),
|
||||
remark: customerRemarkSchema,
|
||||
})
|
||||
.strict();
|
||||
@@ -274,7 +280,7 @@ export const updateCustomerInputSchema = z
|
||||
contactName: customerContactNameSchema,
|
||||
contactPhone: customerContactPhoneSchema,
|
||||
preferences: customerPreferencesSchema.optional(),
|
||||
baseFields: z.array(baseFieldSchema).max(CUSTOMER_BASE_FIELD_LIMIT).optional(),
|
||||
baseFields: z.array(baseFieldSchema).max(CUSTOMER_BASE_FIELD_HARD_LIMIT).optional(),
|
||||
remark: customerRemarkSchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
SYSTEM_CONFIG_DEFINITIONS,
|
||||
SYSTEM_CONFIG_GROUPS,
|
||||
systemConfigDefinition,
|
||||
validateSystemConfigValue,
|
||||
} from '../config/system-configs.js';
|
||||
import {
|
||||
effectiveSystemConfigsResponseSchema,
|
||||
systemConfigSchema,
|
||||
updateSystemConfigsInputSchema,
|
||||
} from './config.js';
|
||||
|
||||
describe('系统配置契约(A4-BE-02 / R-26)', () => {
|
||||
it('配置键唯一、分组合法、默认值可通过校验', () => {
|
||||
const keys = SYSTEM_CONFIG_DEFINITIONS.map((item) => item.key);
|
||||
expect(new Set(keys).size).toBe(keys.length);
|
||||
expect(keys.length).toBeGreaterThanOrEqual(20);
|
||||
const groupKeys = new Set(SYSTEM_CONFIG_GROUPS.map((group) => group.key));
|
||||
for (const definition of SYSTEM_CONFIG_DEFINITIONS) {
|
||||
expect(groupKeys.has(definition.group)).toBe(true);
|
||||
expect(validateSystemConfigValue(definition, definition.defaultValue).ok).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('阈值 / 上限类配置向上限读取接口公开', () => {
|
||||
expect(systemConfigDefinition('limit.customer.base_field')?.public).toBe(true);
|
||||
expect(systemConfigDefinition('limit.customer.gen_field')?.public).toBe(true);
|
||||
expect(systemConfigDefinition('platform.page.default_limit')?.public).toBe(true);
|
||||
expect(systemConfigDefinition('naming.script.pattern')?.public).toBe(false);
|
||||
});
|
||||
|
||||
it('数字边界与布尔类型校验', () => {
|
||||
const limit = systemConfigDefinition('limit.customer.base_field');
|
||||
expect(limit).toBeDefined();
|
||||
if (limit === undefined) return;
|
||||
expect(validateSystemConfigValue(limit, 10).ok).toBe(true);
|
||||
expect(validateSystemConfigValue(limit, 1).ok).toBe(true);
|
||||
expect(validateSystemConfigValue(limit, 0).ok).toBe(false);
|
||||
expect(validateSystemConfigValue(limit, 51).ok).toBe(false);
|
||||
expect(validateSystemConfigValue(limit, '10').ok).toBe(false);
|
||||
|
||||
const notify = systemConfigDefinition('notify.task.completed');
|
||||
expect(notify).toBeDefined();
|
||||
if (notify === undefined) return;
|
||||
expect(validateSystemConfigValue(notify, true).ok).toBe(true);
|
||||
expect(validateSystemConfigValue(notify, 'true').ok).toBe(false);
|
||||
});
|
||||
|
||||
it('命名模板配置校验占位符与非法字符', () => {
|
||||
const naming = systemConfigDefinition('naming.script.pattern');
|
||||
expect(naming).toBeDefined();
|
||||
if (naming === undefined) return;
|
||||
expect(validateSystemConfigValue(naming, '{行业}-{客户}-{序号}').ok).toBe(true);
|
||||
expect(validateSystemConfigValue(naming, '{行业}*{客户}').ok).toBe(false);
|
||||
expect(validateSystemConfigValue(naming, '{未知}').ok).toBe(false);
|
||||
});
|
||||
|
||||
it('系统配置响应与保存入参可解析', () => {
|
||||
const config = {
|
||||
key: 'limit.customer.base_field',
|
||||
group: 'limits' as const,
|
||||
label: '客户基础字段上限',
|
||||
description: 'desc',
|
||||
impact: 'impact',
|
||||
type: 'number' as const,
|
||||
value: 10,
|
||||
valueType: 'number' as const,
|
||||
unit: '个',
|
||||
version: 1,
|
||||
updatedAt: '2026-11-04T00:00:00.000Z',
|
||||
updatedBy: '江岚',
|
||||
editable: true,
|
||||
isPublic: true,
|
||||
min: 1,
|
||||
max: 50,
|
||||
step: null,
|
||||
maxLength: null,
|
||||
};
|
||||
expect(systemConfigSchema.parse(config).key).toBe('limit.customer.base_field');
|
||||
expect(
|
||||
updateSystemConfigsInputSchema.parse({
|
||||
changes: [
|
||||
{ key: 'limit.customer.base_field', value: 12, expectedVersion: 1, reason: 'A3 对齐' },
|
||||
],
|
||||
}).changes,
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
effectiveSystemConfigsResponseSchema.parse({
|
||||
items: [{ key: 'limit.customer.base_field', value: 10 }],
|
||||
}).items[0]?.value,
|
||||
).toBe(10);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 系统配置共享契约(A4-BE-02)
|
||||
* 依据:PRD §5.4.4 / R-26;技术架构 §5.7 / §6.3.7;开发计划 A4-BE-02。
|
||||
*
|
||||
* 系统配置是阈值 / 上限唯一来源;本契约的键、分组、类型与默认值来自
|
||||
* `config/system-configs.ts`,业务代码不得再写阈值常量。
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
SYSTEM_CONFIG_DEFINITIONS,
|
||||
SYSTEM_CONFIG_GROUPS,
|
||||
type SystemConfigDefinition,
|
||||
systemConfigDefinition,
|
||||
type SystemConfigGroupKey,
|
||||
type SystemConfigInputType,
|
||||
} from '../config/system-configs.js';
|
||||
import { isoDateTimeSchema } from './common.js';
|
||||
|
||||
export const systemConfigGroupKeySchema = z.enum(
|
||||
SYSTEM_CONFIG_GROUPS.map((group) => group.key) as [
|
||||
SystemConfigGroupKey,
|
||||
...SystemConfigGroupKey[],
|
||||
],
|
||||
);
|
||||
|
||||
export const systemConfigInputTypeSchema = z.enum(['text', 'number', 'boolean', 'pattern']);
|
||||
export const systemConfigValueSchema = z.union([z.string(), z.number(), z.boolean()]);
|
||||
|
||||
/** 单条配置(含展示元数据与版本) */
|
||||
export const systemConfigSchema = z.object({
|
||||
key: z.string().min(1),
|
||||
group: systemConfigGroupKeySchema,
|
||||
label: z.string().min(1),
|
||||
description: z.string(),
|
||||
impact: z.string(),
|
||||
type: systemConfigInputTypeSchema,
|
||||
value: systemConfigValueSchema,
|
||||
valueType: z.enum(['string', 'number', 'boolean']),
|
||||
unit: z.string().nullable(),
|
||||
version: z.number().int().positive(),
|
||||
updatedAt: isoDateTimeSchema,
|
||||
updatedBy: z.string().nullable(),
|
||||
editable: z.boolean(),
|
||||
isPublic: z.boolean(),
|
||||
min: z.number().nullable(),
|
||||
max: z.number().nullable(),
|
||||
step: z.number().nullable(),
|
||||
maxLength: z.number().int().nullable(),
|
||||
});
|
||||
|
||||
export type SystemConfig = z.infer<typeof systemConfigSchema>;
|
||||
|
||||
export const systemConfigListResponseSchema = z.object({
|
||||
items: z.array(systemConfigSchema),
|
||||
});
|
||||
|
||||
export type SystemConfigListResponse = z.infer<typeof systemConfigListResponseSchema>;
|
||||
|
||||
export const systemConfigChangeInputSchema = z
|
||||
.object({
|
||||
key: z.string().min(1),
|
||||
value: z.unknown(),
|
||||
/** 乐观锁:前端读取时的版本;不一致返回 40901 */
|
||||
expectedVersion: z.number().int().positive().optional(),
|
||||
reason: z.string().trim().max(255).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type SystemConfigChangeInput = z.infer<typeof systemConfigChangeInputSchema>;
|
||||
|
||||
export const updateSystemConfigsInputSchema = z
|
||||
.object({
|
||||
changes: z.array(systemConfigChangeInputSchema).min(1).max(50),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type UpdateSystemConfigsInput = z.infer<typeof updateSystemConfigsInputSchema>;
|
||||
|
||||
export const systemConfigChangeSchema = z.object({
|
||||
key: z.string().min(1),
|
||||
label: z.string().min(1),
|
||||
version: z.number().int().positive(),
|
||||
oldValue: z.unknown().nullable(),
|
||||
newValue: z.unknown(),
|
||||
reason: z.string().nullable(),
|
||||
operator: z.string().nullable(),
|
||||
changedAt: isoDateTimeSchema,
|
||||
result: z.string(),
|
||||
});
|
||||
|
||||
export type SystemConfigChange = z.infer<typeof systemConfigChangeSchema>;
|
||||
|
||||
export const systemConfigHistoryQuerySchema = z.object({
|
||||
config_key: z.string().min(1).optional(),
|
||||
limit: z.coerce.number().int().min(1).max(200).default(50),
|
||||
});
|
||||
|
||||
export type SystemConfigHistoryQuery = z.infer<typeof systemConfigHistoryQuerySchema>;
|
||||
|
||||
export const systemConfigHistoryResponseSchema = z.object({
|
||||
items: z.array(systemConfigChangeSchema),
|
||||
next_cursor: z.string().nullable(),
|
||||
});
|
||||
|
||||
export type SystemConfigHistoryResponse = z.infer<typeof systemConfigHistoryResponseSchema>;
|
||||
|
||||
export const effectiveSystemConfigSchema = z.object({
|
||||
key: z.string().min(1),
|
||||
value: systemConfigValueSchema,
|
||||
});
|
||||
|
||||
export type EffectiveSystemConfig = z.infer<typeof effectiveSystemConfigSchema>;
|
||||
|
||||
export const effectiveSystemConfigsResponseSchema = z.object({
|
||||
items: z.array(effectiveSystemConfigSchema),
|
||||
});
|
||||
|
||||
export type EffectiveSystemConfigsResponse = z.infer<typeof effectiveSystemConfigsResponseSchema>;
|
||||
|
||||
/** 供后端 / 前端按 key 查找定义 */
|
||||
export function systemConfigMetaOf(key: string): SystemConfigDefinition | undefined {
|
||||
return systemConfigDefinition(key);
|
||||
}
|
||||
|
||||
export type { SystemConfigDefinition, SystemConfigGroupKey, SystemConfigInputType };
|
||||
|
||||
/** 配置定义清单(前端设置页分组、种子脚本共用) */
|
||||
export const SYSTEM_CONFIG_META = SYSTEM_CONFIG_DEFINITIONS;
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
createDictItemInputSchema,
|
||||
createDictTypeInputSchema,
|
||||
dictItemSnapshotSchema,
|
||||
toDictItemSnapshot,
|
||||
updateDictItemInputSchema,
|
||||
} from './dict.js';
|
||||
|
||||
describe('数据字典契约(A4-BE-01 / A4-BE-04)', () => {
|
||||
it('类型编码只允许小写 snake_case,名称必填', () => {
|
||||
expect(
|
||||
createDictTypeInputSchema.parse({
|
||||
code: 'script_direction',
|
||||
name: '脚本方向',
|
||||
group: 'content',
|
||||
}),
|
||||
).toMatchObject({ code: 'script_direction', kind: 'list', industryScoped: false });
|
||||
expect(() =>
|
||||
createDictTypeInputSchema.parse({
|
||||
code: 'ScriptDirection',
|
||||
name: '脚本方向',
|
||||
group: 'content',
|
||||
}),
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
createDictTypeInputSchema.parse({ code: 'script_direction', name: '', group: 'content' }),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it('条目编码允许数字开头(如 15s)与短横线', () => {
|
||||
expect(
|
||||
createDictItemInputSchema.parse({
|
||||
code: '15s',
|
||||
name: '15 秒',
|
||||
}),
|
||||
).toMatchObject({ code: '15s', status: 'active' });
|
||||
expect(
|
||||
createDictItemInputSchema.parse({
|
||||
code: 'light-style',
|
||||
name: '光影',
|
||||
parentCode: null,
|
||||
industryId: null,
|
||||
}),
|
||||
).toMatchObject({ code: 'light-style' });
|
||||
});
|
||||
|
||||
it('编辑条目只接收名称 / 排序 / 启停 / 备注,不接收编码与行业', () => {
|
||||
expect(
|
||||
updateDictItemInputSchema.parse({ name: '口播', sortOrder: 10, status: 'disabled' }),
|
||||
).toEqual({ name: '口播', sortOrder: 10, status: 'disabled' });
|
||||
expect(() => updateDictItemInputSchema.parse({ code: 'new_code' })).toThrow();
|
||||
expect(() => updateDictItemInputSchema.parse({ industryId: null })).toThrow();
|
||||
});
|
||||
|
||||
it('快照保留填写时名称,之后改字典不影响历史(R-10)', () => {
|
||||
const item = {
|
||||
id: '01HZZZZZZZZZZZZZZZZZZZZZZZ',
|
||||
typeCode: 'script_direction',
|
||||
code: 'oral',
|
||||
name: '口播',
|
||||
parentCode: null,
|
||||
parentName: null,
|
||||
industryId: null,
|
||||
industryPath: null,
|
||||
sortOrder: 10,
|
||||
status: 'active' as const,
|
||||
remark: '',
|
||||
source: 'generic' as const,
|
||||
overridden: false,
|
||||
refCount: 0,
|
||||
updatedAt: '2026-11-04T00:00:00.000Z',
|
||||
updatedBy: '江岚',
|
||||
};
|
||||
const snapshot = toDictItemSnapshot(item, '2026-11-04T10:00:00.000Z');
|
||||
item.name = '口播(改名后)';
|
||||
expect(snapshot.name).toBe('口播');
|
||||
expect(dictItemSnapshotSchema.parse(snapshot)).toEqual(snapshot);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* 数据字典共享契约(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,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user