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
+131
-60
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user