Files
lingxi/prisma/seed.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

920 lines
27 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.
// 灵犀 · 种子数据(A2)
// 原则:
// 1) 幂等:按业务唯一键 upsert,重复执行不重复插入;已存在的密码 / 人工调整值不被覆盖;
// 2) 权限点目录、角色-权限点、角色-数据范围、用户-角色为种子可重建映射:先清后建
// (限定在种子角色 / 种子用户范围内;生产禁止直接跑 seed);
// 3) 权限点编码唯一事实源是 packages/shared/src/permissions.ts,本文件不复制编码字符串;
// 4) 超管、部门、示例角色 / 用户口径见 prisma/SEED.md。
import { randomBytes } from 'node:crypto';
import { existsSync } from 'node:fs';
import { resolve } from 'node:path';
import type { Prisma } from '@prisma/client';
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);
if (existsSync(path)) {
loadDotenv({ path });
}
}
const prisma = new PrismaClient();
const ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
function ulid(): string {
let time = Date.now();
const timeChars = new Array<string>(10);
for (let i = 9; i >= 0; i -= 1) {
timeChars[i] = ENCODING[time % 32] ?? '0';
time = Math.floor(time / 32);
}
const random = randomBytes(16);
const randomChars = Array.from(random, (byte) => ENCODING[byte & 31] ?? '0');
return `${timeChars.join('')}${randomChars.join('')}`;
}
/** 示例账号统一初始密码:lingxi2026(Argon2id,首登强制修改) */
const SEED_PASSWORD_HASH =
'$argon2id$v=19$m=65536,p=4,t=3$Osdgg3Fz53AwcoC2kyGiGg$QVbxW7Ok/SlCNbML95go2J/6GMSfE1EG3jBTJgR4zO4';
interface SeedDepartment {
code: string;
name: string;
parentCode: string | null;
leaderAccount: string | null;
sortOrder: number;
}
const DEPARTMENTS: SeedDepartment[] = [
{
code: 'content',
name: '内容生产中心',
parentCode: null,
leaderAccount: 'chen.mo',
sortOrder: 1,
},
{ code: 'delivery', name: '交付中心', parentCode: null, leaderAccount: 'gao.cen', sortOrder: 2 },
{
code: 'tech',
name: '平台技术部',
parentCode: null,
leaderAccount: 'shen.zhiyuan',
sortOrder: 3,
},
{
code: 'content-g1',
name: '素材一组',
parentCode: 'content',
leaderAccount: null,
sortOrder: 1,
},
{
code: 'content-g2',
name: '素材二组',
parentCode: 'content',
leaderAccount: null,
sortOrder: 2,
},
{ code: 'content-g3', name: '设计组', parentCode: 'content', leaderAccount: null, sortOrder: 3 },
{
code: 'delivery-cs',
name: '客户成功组',
parentCode: 'delivery',
leaderAccount: null,
sortOrder: 1,
},
{ code: 'tech-ai', name: 'AI 工程组', parentCode: 'tech', leaderAccount: null, sortOrder: 1 },
{ code: 'tech-ops', name: '平台运维组', parentCode: 'tech', leaderAccount: null, sortOrder: 2 },
];
interface SeedRole {
code: string;
name: string;
description: string;
isSystem: boolean;
status?: RoleStatus;
/** 普通权限点(不得包含特殊权限;特殊权限必须放 specialPermissions 显式单独授予) */
permissions: string[];
/** 特殊权限点:默认关闭,仅在种子里显式授予(原型口径:超管 6 + 平台管理员 1 + AI 工程师 1) */
specialPermissions?: string[];
/** 数据范围声明(一个角色只允许一种类型;A3 前「指定客户组」targetIds 为空) */
dataScope: SeedDataScope;
}
interface SeedDataScope {
type: string;
targetIds?: string[];
}
/**
* 种子角色与权限映射:与 design/a2-ui-v1/brief.md §4 的共享角色数据对齐
* (各角色普通权限数 12 / 9 / 4 / 2 / 4 / 3 / 5 / 4 / 5,特殊权限授予 8 项)。
*/
const ROLES: SeedRole[] = [
{
code: 'super_admin',
name: '超级管理员',
description: '内置角色,拥有系统全部权限',
isSystem: true,
permissions: PERMISSION_CATALOG.filter((item) => !item.isSpecial).map((item) => item.code),
specialPermissions: PERMISSION_CATALOG.filter((item) => item.isSpecial).map(
(item) => item.code,
),
dataScope: { type: 'all' },
},
{
code: 'platform_admin',
name: '平台管理员',
description: '组织、角色与平台配置管理',
isSystem: false,
permissions: [
Permission.USER_VIEW,
Permission.USER_MANAGE,
Permission.ROLE_VIEW,
Permission.ROLE_MANAGE,
Permission.SYSTEM_CONFIGURE,
Permission.INDUSTRY_MANAGE,
Permission.DICT_MANAGE,
Permission.FORM_DESIGN,
Permission.CUSTOMER_READ,
Permission.CUSTOMER_GROUP_MANAGE,
Permission.DELIVERY_READ,
Permission.ASSET_READ,
],
specialPermissions: [Permission.AUDIT_READ],
dataScope: { type: 'all' },
},
{
code: 'material_operator',
name: '素材运营',
description: '素材生产、快筛、导出与归档',
isSystem: false,
permissions: [
Permission.SCRIPT_READ,
Permission.SCRIPT_WRITE,
Permission.MATERIAL_READ,
Permission.MATERIAL_WRITE,
Permission.MATERIAL_RENDER,
Permission.ASSET_READ,
Permission.ASSET_WRITE,
Permission.DELIVERY_READ,
Permission.CUSTOMER_READ,
],
dataScope: { type: 'owned_customers' },
},
{
code: 'designer',
name: '设计师',
description: '素材设计与资产维护',
isSystem: false,
permissions: [
Permission.MATERIAL_READ,
Permission.MATERIAL_WRITE,
Permission.ASSET_READ,
Permission.ASSET_WRITE,
],
dataScope: { type: 'owned_customers' },
},
{
code: 'copywriter',
name: '文案策划',
description: '脚本撰写与要素卡维护',
isSystem: false,
permissions: [Permission.SCRIPT_READ, Permission.SCRIPT_WRITE],
dataScope: { type: 'department' },
},
{
code: 'reviewer',
name: '审核员',
description: '脚本与素材审核',
isSystem: false,
permissions: [
Permission.SCRIPT_READ,
Permission.SCRIPT_REVIEW,
Permission.MATERIAL_READ,
Permission.MATERIAL_REVIEW,
],
dataScope: { type: 'department_and_children' },
},
{
code: 'ai_engineer',
name: 'AI 工程师',
description: '模型接入与生成链路维护',
isSystem: false,
permissions: [
Permission.MATERIAL_READ,
Permission.MATERIAL_RENDER,
Permission.SYSTEM_CONFIGURE,
],
specialPermissions: [Permission.AI_CONFIGURE],
dataScope: { type: 'all' },
},
{
code: 'account_owner',
name: '客户负责人',
description: '客户资料、脚本审核与交付验收',
isSystem: false,
permissions: [
Permission.CUSTOMER_READ,
Permission.CUSTOMER_MANAGE,
Permission.CUSTOMER_TRANSFER,
Permission.DELIVERY_READ,
Permission.SCRIPT_REVIEW,
],
dataScope: { type: 'customer_groups', targetIds: [] },
},
{
code: 'industry_config',
name: '行业配置',
description: '行业包、表单与字典维护',
isSystem: false,
permissions: [
Permission.INDUSTRY_MANAGE,
Permission.FORM_DESIGN,
Permission.DICT_MANAGE,
Permission.CUSTOMER_READ,
],
dataScope: { type: 'all' },
},
{
code: 'review_lead',
name: '审核主管',
description: '原审核组主管角色,已并入审核员',
isSystem: false,
status: 'DISABLED',
permissions: [
Permission.SCRIPT_READ,
Permission.SCRIPT_REVIEW,
Permission.MATERIAL_READ,
Permission.MATERIAL_REVIEW,
Permission.CUSTOMER_READ,
],
dataScope: { type: 'department_and_children' },
},
];
interface SeedUser {
name: string;
account: string;
phone: string;
jobTitle: string;
departmentCode: string;
roleCodes: string[];
status: UserStatus;
}
const USERS: SeedUser[] = [
{
name: '灵犀管理员',
account: 'admin',
phone: '13000000001',
jobTitle: '平台管理员',
departmentCode: 'tech',
roleCodes: ['super_admin'],
status: 'ACTIVE',
},
{
name: '陈默',
account: 'chen.mo',
phone: '13866202046',
jobTitle: '内容生产中心主管',
departmentCode: 'content-g1',
roleCodes: ['material_operator', 'account_owner'],
status: 'ACTIVE',
},
{
name: '林清',
account: 'lin.qing',
phone: '13912347712',
jobTitle: '设计师',
departmentCode: 'content-g3',
roleCodes: ['designer'],
status: 'ACTIVE',
},
{
name: '周岸',
account: 'zhou.an',
phone: '13755205520',
jobTitle: '文案策划',
departmentCode: 'content-g2',
roleCodes: ['copywriter'],
status: 'ACTIVE',
},
{
name: '赵一诺',
account: 'zhao.yinuo',
phone: '13633183188',
jobTitle: '审核员',
departmentCode: 'content-g2',
roleCodes: ['reviewer'],
status: 'ACTIVE',
},
{
name: '高岑',
account: 'gao.cen',
phone: '18644024402',
jobTitle: '交付中心主管',
departmentCode: 'delivery-cs',
roleCodes: ['account_owner'],
status: 'ACTIVE',
},
{
name: '苏禾',
account: 'su.he',
phone: '18877657765',
jobTitle: 'AI 工程师',
departmentCode: 'tech-ai',
roleCodes: ['ai_engineer'],
status: 'ACTIVE',
},
{
name: '沈知远',
account: 'shen.zhiyuan',
phone: '13300000071',
jobTitle: '平台技术部主管',
departmentCode: 'tech',
roleCodes: ['super_admin'],
status: 'ACTIVE',
},
{
name: '江岚',
account: 'jiang.lan',
phone: '15933203320',
jobTitle: '平台管理员',
departmentCode: 'tech-ops',
roleCodes: ['platform_admin'],
status: 'ACTIVE',
},
{
name: '唐嘉',
account: 'tang.jia',
phone: '13566416641',
jobTitle: '素材运营',
departmentCode: 'content-g1',
roleCodes: ['material_operator'],
status: 'DISABLED',
},
{
name: '何岸',
account: 'he.an',
phone: '18711931193',
jobTitle: '客户负责人',
departmentCode: 'delivery-cs',
roleCodes: ['account_owner'],
status: 'DISABLED',
},
];
async function seedConfigs(): Promise<void> {
for (const definition of SYSTEM_CONFIG_DEFINITIONS) {
await prisma.systemConfig.upsert({
where: { configKey: definition.key },
// 只同步元数据,不覆盖人工调整过的值 / 版本
update: {
description: definition.description,
valueType: systemConfigValueType(definition),
isEditable: definition.editable,
},
create: {
id: ulid(),
configKey: definition.key,
configValue: definition.defaultValue,
valueType: systemConfigValueType(definition),
description: definition.description,
isEditable: definition.editable,
},
});
}
console.log(`[seed] system_configs 就绪:${String(SYSTEM_CONFIG_DEFINITIONS.length)} 项(幂等)`);
}
async function seedDepartments(): Promise<void> {
for (const item of DEPARTMENTS) {
const parent =
item.parentCode === null
? null
: await prisma.department.findUnique({ where: { code: item.parentCode } });
await prisma.department.upsert({
where: { code: item.code },
update: {},
create: {
id: ulid(),
code: item.code,
name: item.name,
parentId: parent?.id ?? null,
sortOrder: item.sortOrder,
},
});
}
console.log(`[seed] departments 就绪:${String(DEPARTMENTS.length)} 个(幂等)`);
}
async function seedPermissions(): Promise<void> {
for (const item of PERMISSION_CATALOG) {
await prisma.permission.upsert({
where: { code: item.code },
update: {
module: item.module,
moduleLabel: item.moduleLabel,
name: item.name,
description: item.description,
isSpecial: item.isSpecial,
sortOrder: item.sortOrder,
},
create: {
code: item.code,
module: item.module,
moduleLabel: item.moduleLabel,
name: item.name,
description: item.description,
isSpecial: item.isSpecial,
sortOrder: item.sortOrder,
},
});
}
console.log(`[seed] permissions 就绪:${String(PERMISSION_CATALOG.length)} 个(幂等,只增不删)`);
}
async function seedRoleAccess(roleId: string, item: SeedRole): Promise<void> {
const normal = splitPermissions(item.permissions);
if (normal.specialPermissions.length > 0) {
throw new Error(
`种子角色 ${item.code} 的 permissions 混入特殊权限(必须放 specialPermissions):${normal.specialPermissions.join(',')}`,
);
}
const special = splitPermissions(item.specialPermissions ?? []);
if (special.permissions.length > 0) {
throw new Error(
`种子角色 ${item.code} 的 specialPermissions 混入普通权限:${special.permissions.join(',')}`,
);
}
const conflicts = item.isSystem
? []
: findSodConflicts([...normal.permissions, ...special.specialPermissions]);
if (conflicts.length > 0) {
throw new Error(
`种子角色 ${item.code} 违反职责互斥:${conflicts.map((c) => c.label).join(';')}`,
);
}
await prisma.rolePermission.deleteMany({ where: { roleId } });
await prisma.rolePermission.createMany({
data: [...normal.permissions, ...special.specialPermissions].map((permissionCode) => ({
roleId,
permissionCode,
})),
skipDuplicates: true,
});
await prisma.dataScope.deleteMany({ where: { roleId } });
// 无授权对象(含 A3 前空客户组)也保留一行 target_id = NULL,确保范围类型不丢失(不降级为最小权限)
const targetIds = item.dataScope.targetIds?.length ? item.dataScope.targetIds : [null];
await prisma.dataScope.createMany({
data: targetIds.map((targetId) => ({
id: ulid(),
roleId,
scopeType: item.dataScope.type,
targetId,
})),
skipDuplicates: true,
});
}
async function seedRoles(): Promise<void> {
for (const item of ROLES) {
const role = await prisma.role.upsert({
where: { code: item.code },
update: {},
create: {
id: ulid(),
code: item.code,
name: item.name,
description: item.description,
isSystem: item.isSystem,
status: item.status ?? 'ACTIVE',
},
});
await seedRoleAccess(role.id, item);
}
console.log(
`[seed] roles 就绪:${String(ROLES.length)} 个 + 权限点 / 数据范围映射(幂等,特殊权限按原型显式授予)`,
);
}
async function seedUsers(): Promise<void> {
for (const item of USERS) {
const department = await prisma.department.findUnique({ where: { code: item.departmentCode } });
if (department === null) {
throw new Error(`部门不存在:${item.departmentCode}`);
}
const user = await prisma.user.upsert({
where: { account: item.account },
update: {},
create: {
id: ulid(),
name: item.name,
account: item.account,
email: `${item.account}@lingxi.cn`,
phone: item.phone,
passwordHash: SEED_PASSWORD_HASH,
departmentId: department.id,
jobTitle: item.jobTitle,
status: item.status,
mustChangePassword: true,
...(item.status === 'DISABLED' ? { disabledAt: new Date() } : {}),
},
});
await prisma.userRole.deleteMany({ where: { userId: user.id } });
for (const code of item.roleCodes) {
const role = await prisma.role.findUnique({ where: { code } });
if (role === null) throw new Error(`角色不存在:${code}`);
await prisma.userRole.create({ data: { userId: user.id, roleId: role.id } });
}
}
console.log(
`[seed] users 就绪:${String(USERS.length)} 个(幂等,初始密码 lingxi2026,首登强制修改)`,
);
}
async function seedDepartmentLeaders(): Promise<void> {
for (const item of DEPARTMENTS) {
if (item.leaderAccount === null) continue;
const [department, leader] = await Promise.all([
prisma.department.findUnique({ where: { code: item.code } }),
prisma.user.findUnique({ where: { account: item.leaderAccount } }),
]);
if (department !== null && leader !== null) {
await prisma.department.update({
where: { id: department.id },
data: { leaderUserId: leader.id },
});
}
}
console.log('[seed] 部门负责人就绪(幂等)');
}
/**
* A3 客户与行业种子(幂等):
* - 两级行业:电商(服饰鞋包 / 美妆个护)、金融(信贷);
* - 2 个客户(花间集、素研)与 1 个客户组(电商核心组);
* - 花间集 3 条客户级字段:已生效 2(1 条进入生成 + 1 条仅检索)、待审批 1。
* 不覆盖人工调整值:按业务唯一键查找,存在则只补齐缺失关系。
*/
/* eslint-disable max-lines-per-function -- A3 种子按行业 / 客户 / 客户组 / 字段顺序幂等写入 */
async function seedClient(): Promise<void> {
const admin = await prisma.user.findUnique({ where: { account: 'admin' }, select: { id: true } });
const chen = await prisma.user.findUnique({
where: { account: 'chen.mo' },
select: { id: true },
});
const lin = await prisma.user.findUnique({
where: { account: 'lin.qing' },
select: { id: true },
});
const jiang = await prisma.user.findUnique({
where: { account: 'jiang.lan' },
select: { id: true },
});
if (admin === null || chen === null || lin === null || jiang === null) {
throw new Error('A3 种子依赖 admin / chen.mo / lin.qing / jiang.lan 账号,请先执行 A1 种子');
}
const adminId = admin.id;
async function upsertIndustry(
name: string,
parentId: string | null,
level: number,
sortOrder: number,
remark: string,
): Promise<string> {
const existing = await prisma.industry.findFirst({
where: { name, parentId },
select: { id: true },
});
if (existing !== null) return existing.id;
const id = ulid();
await prisma.industry.create({
data: { id, name, parentId, level, sortOrder, remark, status: 'ACTIVE' },
});
return id;
}
const ec = await upsertIndustry('电商', null, 1, 10, '电商行业客户,覆盖服饰、美妆等类目');
const apparel = await upsertIndustry('服饰鞋包', ec, 2, 10, '服饰、鞋履、箱包类目');
const beauty = await upsertIndustry('美妆个护', ec, 2, 20, '护肤、彩妆、个护类目');
const fin = await upsertIndustry('金融', null, 1, 20, '金融行业客户,合规要求高');
await upsertIndustry('信贷', fin, 2, 10, '消费贷、经营贷、信用卡');
async function upsertCustomer(input: {
name: string;
shortName: string;
industryId: string;
ownerUserId: string;
contactName: string;
contactPhone: string;
remark: string;
preferences: Prisma.InputJsonValue;
customFields: Prisma.InputJsonValue;
}): Promise<string> {
const existing = await prisma.customer.findFirst({
where: { industryId: input.industryId, name: input.name, deletedAt: null },
select: { id: true },
});
if (existing !== null) return existing.id;
const id = ulid();
await prisma.customer.create({
data: {
id,
industryId: input.industryId,
name: input.name,
shortName: input.shortName,
status: 'ACTIVE',
ownerUserId: input.ownerUserId,
contactName: input.contactName,
contactPhone: input.contactPhone,
preferences: input.preferences,
customFields: input.customFields,
remark: input.remark,
createdBy: adminId,
},
});
return id;
}
const huajianji = await upsertCustomer({
name: '花间集',
shortName: 'HJ',
industryId: apparel,
ownerUserId: chen.id,
contactName: '林晓',
contactPhone: '138****2210',
remark: '秋冬新品主推,素材需保留品牌金色描边。',
preferences: {
naming: '客户名_方向_日期_序号',
directory: '花间集/2026/秋冬',
specs: ['1080×1920', '1080×1080'],
taboo: ['绝对化用语', '未授权肖像'],
},
customFields: [
{ key: '门店编号', value: '沪-012' },
{ key: '内部产品代码', value: 'HJ-2026' },
],
});
const suyan = await upsertCustomer({
name: '素研',
shortName: 'SY',
industryId: beauty,
ownerUserId: lin.id,
contactName: '周航',
contactPhone: '139****8832',
remark: '以成分卖点为主,需规避医疗功效表述。',
preferences: {
naming: '客户名_产品线_日期_序号',
directory: '素研/2026/Q4',
specs: ['1080×1920'],
taboo: ['医疗功效表述'],
},
customFields: [{ key: '门店编号', value: '杭-008' }],
});
async function upsertGroup(): Promise<void> {
const existing = await prisma.customerGroup.findUnique({
where: { name: '电商核心组' },
select: { id: true },
});
const groupId = existing?.id ?? ulid();
if (existing === null) {
await prisma.customerGroup.create({
data: {
id: groupId,
name: '电商核心组',
description: '电商行业核心客户,作为数据范围授权单位',
createdBy: adminId,
},
});
}
await prisma.customerGroupMember.createMany({
data: [huajianji, suyan].map((customerId) => ({ groupId, customerId })),
skipDuplicates: true,
});
}
await upsertGroup();
const fieldSeeds = [
{
customerId: huajianji,
name: '商品系列',
code: 'series',
type: 'select',
options: ['秋冬', '春夏'],
purpose: '卖点与脚本分组,按系列匹配脚本模板',
entersGeneration: true,
status: 'ACTIVE' as const,
approverUserId: jiang.id,
},
{
customerId: huajianji,
name: '门店编号',
code: 'store_code',
type: 'text',
options: [],
purpose: '门店素材检索与报表拆分',
entersGeneration: false,
status: 'ACTIVE' as const,
approverUserId: jiang.id,
},
{
customerId: huajianji,
name: '活动编码',
code: 'campaign_code',
type: 'text',
options: [],
purpose: '活动素材命名与检索,用于区分不同投放批次',
entersGeneration: true,
status: 'PENDING' as const,
approverUserId: null,
},
{
customerId: suyan,
name: '产品线',
code: 'product_line',
type: 'select',
options: ['精华', '面霜'],
purpose: '按产品线匹配脚本方向与卖点',
entersGeneration: true,
status: 'ACTIVE' as const,
approverUserId: jiang.id,
},
];
for (const field of fieldSeeds) {
const existing = await prisma.customerField.findUnique({
where: { customerId_code: { customerId: field.customerId, code: field.code } },
select: { id: true },
});
if (existing !== null) continue;
await prisma.customerField.create({
data: {
id: ulid(),
customerId: field.customerId,
name: field.name,
code: field.code,
type: field.type,
options: field.options,
purpose: field.purpose,
entersGeneration: field.entersGeneration,
status: field.status,
applicantUserId: chen.id,
approverUserId: field.approverUserId,
approvedAt: field.status === 'ACTIVE' ? new Date() : null,
},
});
}
console.log('[seed] A3 客户与行业就绪(幂等)');
}
/* 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();
await seedDepartments();
await seedRoles();
await seedUsers();
await seedDepartmentLeaders();
await seedClient();
await seedDicts();
}
void main()
.catch((error: unknown) => {
console.error('[seed] 失败:', error);
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});