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:
Stev_Wang committed 2026-11-04 11:48:38 +08:00
1 parent 32b44bdeec
commit cc6e84cdbc
15 files changed
+2899 -75

No files matched your search

+131 -60
View File
@@ -14,12 +14,17 @@ import { PrismaClient, type RoleStatus, type UserStatus } from '@prisma/client';
import { config as loadDotenv } from 'dotenv';
// 直接引用 shared 源码目录(seed 由 tsx 执行;shared 是权限点唯一事实源,避免两处编码漂移)
import {
SYSTEM_CONFIG_DEFINITIONS,
systemConfigValueType,
} from '../packages/shared/src/config/system-configs.js';
import {
findSodConflicts,
Permission,
PERMISSION_CATALOG,
splitPermissions,
} from '../packages/shared/src/permissions.js';
import dictSeed from './seed-data/dicts.json' with { type: 'json' };
for (const file of ['.env.local', '.env']) {
const path = resolve(process.cwd(), file);
@@ -48,58 +53,6 @@ function ulid(): string {
const SEED_PASSWORD_HASH =
'$argon2id$v=19$m=65536,p=4,t=3$Osdgg3Fz53AwcoC2kyGiGg$QVbxW7Ok/SlCNbML95go2J/6GMSfE1EG3jBTJgR4zO4';
interface SeedConfig {
key: string;
value: string | number | boolean;
valueType: 'string' | 'number' | 'boolean';
description: string;
}
const SYSTEM_CONFIGS: SeedConfig[] = [
{
key: 'platform.name',
value: '灵犀',
valueType: 'string',
description: '平台名称(展示用)',
},
{
key: 'platform.export.signed_url_ttl_seconds',
value: 900,
valueType: 'number',
description: '导出产物签名 URL 有效期(秒),短时效',
},
{
key: 'platform.export.archive_retention_days',
value: 3,
valueType: 'number',
description: '导出打包产物默认保留天数',
},
{
key: 'platform.upload.max_upload_mb',
value: 500,
valueType: 'number',
description: '单文件上传上限(MB)',
},
{
key: 'platform.page.default_limit',
value: 20,
valueType: 'number',
description: '列表默认每页数量',
},
{
key: 'platform.ai.cost_soft_limit_ratio',
value: 0.8,
valueType: 'number',
description: 'AI 成本软上限触发比例',
},
{
key: 'platform.ai.cost_hard_limit_ratio',
value: 1,
valueType: 'number',
description: 'AI 成本硬上限触发比例(达到即拒绝新任务)',
},
];
interface SeedDepartment {
code: string;
name: string;
@@ -429,20 +382,26 @@ const USERS: SeedUser[] = [
];
async function seedConfigs(): Promise<void> {
for (const item of SYSTEM_CONFIGS) {
for (const definition of SYSTEM_CONFIG_DEFINITIONS) {
await prisma.systemConfig.upsert({
where: { configKey: item.key },
update: {},
where: { configKey: definition.key },
// 只同步元数据,不覆盖人工调整过的值 / 版本
update: {
description: definition.description,
valueType: systemConfigValueType(definition),
isEditable: definition.editable,
},
create: {
id: ulid(),
configKey: item.key,
configValue: item.value,
valueType: item.valueType,
description: item.description,
configKey: definition.key,
configValue: definition.defaultValue,
valueType: systemConfigValueType(definition),
description: definition.description,
isEditable: definition.editable,
},
});
}
console.log(`[seed] system_configs 就绪:${String(SYSTEM_CONFIGS.length)} 项(幂等)`);
console.log(`[seed] system_configs 就绪:${String(SYSTEM_CONFIG_DEFINITIONS.length)} 项(幂等)`);
}
async function seedDepartments(): Promise<void> {
@@ -828,6 +787,117 @@ async function seedClient(): Promise<void> {
}
/* eslint-enable max-lines-per-function */
interface DictSeedType {
code: string;
name: string;
group: string;
kind: 'tree' | 'list';
description: string;
industryScoped: boolean;
builtin: boolean;
status: 'active' | 'disabled';
sortOrder: number;
updatedBy: string;
}
interface DictSeedItem {
typeCode: string;
code: string;
name: string;
parentCode: string | null;
industryId: string | null;
sortOrder: number;
status: 'active' | 'disabled';
remark: string;
updatedBy: string;
}
const DICT_TYPES = dictSeed.types as DictSeedType[];
const DICT_ITEMS = dictSeed.items as DictSeedItem[];
/** 原型示例数据用行业 code 表示作用域;种子落库时映射为 A3 行业名称 */
const DICT_INDUSTRY_CODE_TO_NAME: Readonly<Record<string, string>> = {
ec: '电商',
ec_apparel: '服饰鞋包',
ec_beauty: '美妆个护',
fin: '金融',
fin_credit: '信贷',
local: '本地生活',
local_food: '餐饮到店',
};
/** A4 数据字典种子:13 类 / 96 条;行业作用域用 A3 的行业 ID 解析;幂等,不覆盖人工调整 */
// eslint-disable-next-line max-lines-per-function -- 类型 + 条目两段种子共用行业映射,集中便于阅读
async function seedDicts(): Promise<void> {
const industries = await prisma.industry.findMany({
select: { id: true, name: true, parentId: true },
});
const industryByName = new Map(industries.map((industry) => [industry.name, industry.id]));
const typeIdByCode = new Map<string, string>();
for (const item of DICT_TYPES) {
const status = item.status === 'active' ? 'ACTIVE' : 'DISABLED';
const existing = await prisma.dictType.findUnique({
where: { code: item.code },
select: { id: true },
});
if (existing !== null) {
typeIdByCode.set(item.code, existing.id);
continue;
}
const id = ulid();
await prisma.dictType.create({
data: {
id,
code: item.code,
name: item.name,
groupKey: item.group,
kind: item.kind === 'tree' ? 'TREE' : 'LIST',
description: item.description,
industryScoped: item.industryScoped,
builtin: item.builtin,
status,
sortOrder: item.sortOrder,
updatedBy: item.updatedBy,
},
});
typeIdByCode.set(item.code, id);
}
for (const item of DICT_ITEMS) {
const typeId = typeIdByCode.get(item.typeCode);
if (typeId === undefined) continue;
const industryName =
item.industryId === null
? null
: (DICT_INDUSTRY_CODE_TO_NAME[item.industryId] ?? item.industryId);
const industryId = industryName === null ? null : (industryByName.get(industryName) ?? null);
const existing = await prisma.dictItem.findFirst({
where: { typeId, code: item.code, industryId },
select: { id: true },
});
if (existing !== null) continue;
await prisma.dictItem.create({
data: {
id: ulid(),
typeId,
code: item.code,
name: item.name,
parentCode: item.parentCode,
industryId,
sortOrder: item.sortOrder,
status: item.status === 'active' ? 'ACTIVE' : 'DISABLED',
remark: item.remark,
updatedBy: item.updatedBy,
},
});
}
console.log(
`[seed] A4 数据字典就绪:${String(DICT_TYPES.length)} 类 / ${String(DICT_ITEMS.length)} 条(幂等)`,
);
}
async function main(): Promise<void> {
await seedConfigs();
await seedPermissions();
@@ -836,6 +906,7 @@ async function main(): Promise<void> {
await seedUsers();
await seedDepartmentLeaders();
await seedClient();
await seedDicts();
}
void main()