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
+10
-9
@@ -6,15 +6,16 @@ A0 只 seed「已建表」的 `system_configs`;A1 补齐组织、账号与角
|
||||
|
||||
## 已 seed(`prisma/seed.ts`)
|
||||
|
||||
| 数据 | 来源 | 幂等键 | 说明 |
|
||||
| ------------------------------ | ---------------------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 平台系统配置 7 项 | `system_configs` | `config_key` | 平台名、导出链接有效期、上传上限、分页大小、AI 预算比例等,A0 起延续 |
|
||||
| 部门 / 小组 9 个(两级) | `departments` | `code` | 内容生产中心(素材一组 / 素材二组 / 设计组)、交付中心(客户成功组)、平台技术部(AI 工程组 / 平台运维组);一级部门负责人取 `leader_account`,用户种子之后回填 `leader_user_id` |
|
||||
| 权限点目录 30 个 | `permissions` | `code`(只增不删) | 10 个模块(script / material / asset / delivery / customer / customer_group / identity / industry / platform / audit);启动时由 `PermissionCatalogService` 幂等 upsert,seed 同步一次 |
|
||||
| 角色 10 个 + 角色权限点 | `roles` / `role_permissions` | 角色 `code`;权限映射先清后建 | 与 `design/a2-ui-v1/brief.md` §4 对齐:超级管理员(隐式全集,映射 24 普通 + 6 特殊)、平台管理员 12+1(`audit:read`)、素材运营 9、设计师 4、文案策划 2、审核员 4、AI 工程师 3+1(`ai:configure`)、客户负责人 5、行业配置 4、审核主管 5(停用);非超管角色合计 50 条映射,特殊授予共 8 项;新建角色仍默认全空、特殊权限单独授予 |
|
||||
| 角色数据范围 | `data_scopes` | 角色 `code`;范围先清后建 | 一个角色只允许一种范围类型:超管 / 平台管理员 / AI 工程师 / 行业配置 = `all`;素材运营 / 设计师 = `owned_customers`;文案策划 = `department`;审核员 / 审核主管 = `department_and_children`;客户负责人 = `customer_groups`(A3 前无客户组,保留一行 `target_id = NULL` 以固定范围类型,不降级为最小权限) |
|
||||
| 用户 11 个(超管 1 + 示例 10) | `users` / `user_roles` | `account`;用户角色先清后建 | 超管 `admin`,示例用户与 A1 原型口径一致(陈默、林清、周岸、赵一诺、高岑、苏禾、沈知远、江岚、唐嘉、何岸);唐嘉 / 何岸为停用态,用于「停用即时失效」联调 |
|
||||
| 用户初始密码 | `users.password_hash` | — | 统一 `lingxi2026`(Argon2id),`must_change_password=true`:首登强制改密 |
|
||||
| 数据 | 来源 | 幂等键 | 说明 |
|
||||
| ------------------------------ | ---------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 平台系统配置 26 项 | `system_configs` | `config_key` | 定义唯一事实源为 `packages/shared/src/config/system-configs.ts`:基础信息 2、命名规则 5、导出与归档 4、通知开关 4、阈值与上限 5、业务阈值 6;A0 的 7 项继续保留,A4 补齐其余配置;重复执行不覆盖人工调整值 / 版本 |
|
||||
| 部门 / 小组 9 个(两级) | `departments` | `code` | 内容生产中心(素材一组 / 素材二组 / 设计组)、交付中心(客户成功组)、平台技术部(AI 工程组 / 平台运维组);一级部门负责人取 `leader_account`,用户种子之后回填 `leader_user_id` |
|
||||
| 权限点目录 30 个 | `permissions` | `code`(只增不删) | 10 个模块(script / material / asset / delivery / customer / customer_group / identity / industry / platform / audit);启动时由 `PermissionCatalogService` 幂等 upsert,seed 同步一次 |
|
||||
| 角色 10 个 + 角色权限点 | `roles` / `role_permissions` | 角色 `code`;权限映射先清后建 | 与 `design/a2-ui-v1/brief.md` §4 对齐:超级管理员(隐式全集,映射 24 普通 + 6 特殊)、平台管理员 12+1(`audit:read`)、素材运营 9、设计师 4、文案策划 2、审核员 4、AI 工程师 3+1(`ai:configure`)、客户负责人 5、行业配置 4、审核主管 5(停用);非超管角色合计 50 条映射,特殊授予共 8 项;新建角色仍默认全空、特殊权限单独授予 |
|
||||
| 角色数据范围 | `data_scopes` | 角色 `code`;范围先清后建 | 一个角色只允许一种范围类型:超管 / 平台管理员 / AI 工程师 / 行业配置 = `all`;素材运营 / 设计师 = `owned_customers`;文案策划 = `department`;审核员 / 审核主管 = `department_and_children`;客户负责人 = `customer_groups`(A3 前无客户组,保留一行 `target_id = NULL` 以固定范围类型,不降级为最小权限) |
|
||||
| 用户 11 个(超管 1 + 示例 10) | `users` / `user_roles` | `account`;用户角色先清后建 | 超管 `admin`,示例用户与 A1 原型口径一致(陈默、林清、周岸、赵一诺、高岑、苏禾、沈知远、江岚、唐嘉、何岸);唐嘉 / 何岸为停用态,用于「停用即时失效」联调 |
|
||||
| 用户初始密码 | `users.password_hash` | — | 统一 `lingxi2026`(Argon2id),`must_change_password=true`:首登强制改密 |
|
||||
| A4 数据字典 13 类 / 96 条 | `dict_types` / `dict_items` | 类型 `code`;条目 `(type, code, industry)` | 13 类:平台与规格、光影与风格、时长档、钩子类型、景别、脚本方向、情绪与风格、素材形态、警示语类型、禁用词库、失败原因、淘汰理由、人群标签;4 分组;行业作用域使用 A3 的「电商 / 服饰鞋包 / 金融 / 信贷」等行业 ID;内置类型不可删除,条目编码不可改 |
|
||||
|
||||
## 登录账号(本地联调)
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "DictStatus" AS ENUM ('ACTIVE', 'DISABLED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "DictKind" AS ENUM ('TREE', 'LIST');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "system_config_versions" (
|
||||
"id" CHAR(26) NOT NULL,
|
||||
"config_key" VARCHAR(128) NOT NULL,
|
||||
"version" INTEGER NOT NULL,
|
||||
"old_value" JSONB,
|
||||
"new_value" JSONB NOT NULL,
|
||||
"reason" VARCHAR(255),
|
||||
"changed_by" CHAR(26),
|
||||
"changed_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "system_config_versions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "dict_types" (
|
||||
"id" CHAR(26) NOT NULL,
|
||||
"code" VARCHAR(64) NOT NULL,
|
||||
"name" VARCHAR(64) NOT NULL,
|
||||
"group_key" VARCHAR(32) NOT NULL,
|
||||
"kind" "DictKind" NOT NULL DEFAULT 'LIST',
|
||||
"description" VARCHAR(255) NOT NULL DEFAULT '',
|
||||
"industry_scoped" BOOLEAN NOT NULL DEFAULT false,
|
||||
"builtin" BOOLEAN NOT NULL DEFAULT false,
|
||||
"status" "DictStatus" NOT NULL DEFAULT 'ACTIVE',
|
||||
"sort_order" INTEGER NOT NULL DEFAULT 0,
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
"updated_by" VARCHAR(64),
|
||||
|
||||
CONSTRAINT "dict_types_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "dict_items" (
|
||||
"id" CHAR(26) NOT NULL,
|
||||
"type_id" CHAR(26) NOT NULL,
|
||||
"code" VARCHAR(64) NOT NULL,
|
||||
"name" VARCHAR(64) NOT NULL,
|
||||
"parent_code" VARCHAR(64),
|
||||
"industry_id" CHAR(26),
|
||||
"sort_order" INTEGER NOT NULL DEFAULT 0,
|
||||
"status" "DictStatus" NOT NULL DEFAULT 'ACTIVE',
|
||||
"remark" VARCHAR(255) NOT NULL DEFAULT '',
|
||||
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(3) NOT NULL,
|
||||
"updated_by" VARCHAR(64),
|
||||
|
||||
CONSTRAINT "dict_items_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "system_config_versions_changed_at_idx" ON "system_config_versions"("changed_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "system_config_versions_config_key_version_key" ON "system_config_versions"("config_key", "version");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "dict_types_code_key" ON "dict_types"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "dict_types_group_key_sort_order_idx" ON "dict_types"("group_key", "sort_order");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "dict_items_type_id_industry_id_sort_order_idx" ON "dict_items"("type_id", "industry_id", "sort_order");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "dict_items_type_id_parent_code_idx" ON "dict_items"("type_id", "parent_code");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "system_config_versions" ADD CONSTRAINT "system_config_versions_config_key_fkey" FOREIGN KEY ("config_key") REFERENCES "system_configs"("config_key") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "dict_items" ADD CONSTRAINT "dict_items_type_id_fkey" FOREIGN KEY ("type_id") REFERENCES "dict_types"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "dict_items" ADD CONSTRAINT "dict_items_industry_id_fkey" FOREIGN KEY ("industry_id") REFERENCES "industries"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- A4 / R-10:同 type + code + 行业层唯一;industry_id 为 NULL 的通用层也必须唯一
|
||||
CREATE UNIQUE INDEX "dict_items_type_code_generic_key" ON "dict_items"("type_id", "code") WHERE "industry_id" IS NULL;
|
||||
CREATE UNIQUE INDEX "dict_items_type_code_industry_key" ON "dict_items"("type_id", "code", "industry_id") WHERE "industry_id" IS NOT NULL;
|
||||
|
||||
-- 一条条目不能把自己作为父级
|
||||
ALTER TABLE "dict_items" ADD CONSTRAINT "dict_items_no_self_parent_check" CHECK ("parent_code" IS NULL OR "parent_code" <> "code");
|
||||
@@ -226,9 +226,29 @@ model SystemConfig {
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
updatedBy String? @map("updated_by") @db.Char(26)
|
||||
|
||||
versions SystemConfigVersion[]
|
||||
|
||||
@@map("system_configs")
|
||||
}
|
||||
|
||||
/// 系统配置版本历史(append-only):每次变更留前后值、原因、操作人(A4-BE-02)
|
||||
model SystemConfigVersion {
|
||||
id String @id @db.Char(26)
|
||||
configKey String @map("config_key") @db.VarChar(128)
|
||||
version Int
|
||||
oldValue Json? @map("old_value") @db.JsonB
|
||||
newValue Json @map("new_value") @db.JsonB
|
||||
reason String? @db.VarChar(255)
|
||||
changedBy String? @map("changed_by") @db.Char(26)
|
||||
changedAt DateTime @default(now()) @map("changed_at") @db.Timestamptz(3)
|
||||
|
||||
config SystemConfig @relation(fields: [configKey], references: [configKey], onDelete: Cascade)
|
||||
|
||||
@@unique([configKey, version])
|
||||
@@index([changedAt])
|
||||
@@map("system_config_versions")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// A3 客户与行业(client 模块;ADR-002 两级行业)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -254,6 +274,7 @@ model Industry {
|
||||
parent Industry? @relation("IndustryTree", fields: [parentId], references: [id], onDelete: Restrict)
|
||||
children Industry[] @relation("IndustryTree")
|
||||
customers Customer[]
|
||||
dictItems DictItem[]
|
||||
|
||||
@@index([parentId, sortOrder])
|
||||
@@index([level, status])
|
||||
@@ -361,3 +382,63 @@ model CustomerField {
|
||||
@@index([customerId, status])
|
||||
@@map("customer_fields")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// A4 数据字典(catalog 模块):一张通用枚举表同时表达二级树与词表
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum DictStatus {
|
||||
ACTIVE
|
||||
DISABLED
|
||||
}
|
||||
|
||||
enum DictKind {
|
||||
TREE
|
||||
LIST
|
||||
}
|
||||
|
||||
/// 字典类型:编码唯一且创建后不可改;内置类型不可删除
|
||||
model DictType {
|
||||
id String @id @db.Char(26)
|
||||
code String @unique @db.VarChar(64)
|
||||
name String @db.VarChar(64)
|
||||
groupKey String @map("group_key") @db.VarChar(32)
|
||||
kind DictKind @default(LIST)
|
||||
description String @default("") @db.VarChar(255)
|
||||
industryScoped Boolean @default(false) @map("industry_scoped")
|
||||
builtin Boolean @default(false)
|
||||
status DictStatus @default(ACTIVE)
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
updatedBy String? @map("updated_by") @db.VarChar(64)
|
||||
|
||||
items DictItem[]
|
||||
|
||||
@@index([groupKey, sortOrder])
|
||||
@@map("dict_types")
|
||||
}
|
||||
|
||||
/// 字典条目:industry_id 为空 = 通用层;parent_code 表达二级树;
|
||||
/// 同 type + code + industry 唯一由迁移中的部分唯一索引保证(NULL 也唯一)。
|
||||
model DictItem {
|
||||
id String @id @db.Char(26)
|
||||
typeId String @map("type_id") @db.Char(26)
|
||||
code String @db.VarChar(64)
|
||||
name String @db.VarChar(64)
|
||||
parentCode String? @map("parent_code") @db.VarChar(64)
|
||||
industryId String? @map("industry_id") @db.Char(26)
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
status DictStatus @default(ACTIVE)
|
||||
remark String @default("") @db.VarChar(255)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
updatedBy String? @map("updated_by") @db.VarChar(64)
|
||||
|
||||
type DictType @relation(fields: [typeId], references: [id], onDelete: Cascade)
|
||||
industry Industry? @relation(fields: [industryId], references: [id], onDelete: Restrict)
|
||||
|
||||
@@index([typeId, industryId, sortOrder])
|
||||
@@index([typeId, parentCode])
|
||||
@@map("dict_items")
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+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