Files
lingxi/prisma/seed.ts
T
Stev_Wang 140dc386c1 feat(client): A3 客户与行业目录(两级行业、客户组、字段审批)
背景:A3 需把行业 / 客户 / 客户组 / 客户级字段立为资产根节点;用户确认两级行业树与混搭 UI(行业 B / 客户列表 A / 详情 A / 新建 B / 客户组 B / 字段 A)。

改动:
- 新增 ADR-002,行业改为两级树,客户挂末级行业;同步 PRD / 技术架构 / 开发计划。
- 新增 client 模块:industries / customers / customer-groups / customer-fields 接口、真实 PrismaCustomerDirectory、客户状态机与客户转交、客户组数据范围解析、跨客户 404。
- 新增迁移与幂等种子(电商/金融两级行业、2 客户、1 客户组、4 条字段)。
- 前端新增行业/客户/客户组/字段审批页面与路由、导航更名(客户管理 / 行业管理 / 客户组),复用现有 token。
- 补充 shared 契约单测、client 集成用例、A3 浏览器 e2e。

影响面:client 模块与 client 表、shared 契约、web 路由/导航、ScopeContext 数据范围、OpenAPI 与种子;A1 客户转交占位由 client 实现替换。
回滚:回滚本提交;数据库保留新增表或按报告执行 down SQL;CUSTOMER_DIRECTORY 可切回空集实现。
验证:format:check / lint / typecheck / test 全绿(shared 131 / api 90 / web 125,共 346);test:int 5 文件 39 条;A3 e2e 1/1;build 8/8;真实浏览器六页验证通过。
2026-11-03 21:45:38 +08:00

849 lines
25 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 {
findSodConflicts,
Permission,
PERMISSION_CATALOG,
splitPermissions,
} from '../packages/shared/src/permissions.js';
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 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;
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 item of SYSTEM_CONFIGS) {
await prisma.systemConfig.upsert({
where: { configKey: item.key },
update: {},
create: {
id: ulid(),
configKey: item.key,
configValue: item.value,
valueType: item.valueType,
description: item.description,
},
});
}
console.log(`[seed] system_configs 就绪:${String(SYSTEM_CONFIGS.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 */
async function main(): Promise<void> {
await seedConfigs();
await seedPermissions();
await seedDepartments();
await seedRoles();
await seedUsers();
await seedDepartmentLeaders();
await seedClient();
}
void main()
.catch((error: unknown) => {
console.error('[seed] 失败:', error);
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});