platform-assistant/apps/api/prisma/seed.ts
Sisyphus 58ac140802 feat: 品类方案C - AI分类 + 关键词表反哺
- 新增 ProductCategory 表(schema + migration + seed 15个品类)
- 新增 product-category CRUD API(GET/POST/PUT/DELETE /api/categories)
- 新增 CategoryInferenceService:关键词匹配优先,失败时调 DeepSeek AI 分类,结果反写品类表
- 更新 CrawlerService:采集入库时使用 batchInfer 替代硬编码 inferCategory
- 更新 LeadService:品类筛选改为 DB 关键词匹配,移除硬编码 CATEGORY_KEYWORDS
- 修复 LeadModule/CrawlerModule 依赖关系,避免循环依赖
2026-06-30 18:37:23 +08:00

297 lines
18 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.

import { PrismaClient } from '@prisma/client'
import * as bcrypt from 'bcrypt'
const prisma = new PrismaClient()
async function main() {
const hashed = await bcrypt.hash('admin123', 10)
// ---- 租户 ----
const demoCompany = await prisma.tenant.upsert({
where: { id: 'demo-tenant-001' },
update: {},
create: {
id: 'demo-tenant-001',
companyName: '示例科技公司', contactName: '张三', contactEmail: 'contact@example.com',
plan: 'enterprise', status: 'active',
validFrom: new Date('2026-01-01'), validUntil: new Date('2027-01-01'),
platformType: 'jubian', platformUrl: 'https://jubian.example.com',
features: JSON.stringify({ inquiry: true, imageGen: true, product: true, comparison: true }),
usageQuotas: JSON.stringify({ monthlyLeads: 10000, monthlyImages: 500, monthlyProducts: 5000, monthlyComparisons: 200, maxUsers: 50 }),
},
})
const farEast = await prisma.tenant.upsert({
where: { id: 'demo-tenant-002' },
update: {},
create: {
id: 'demo-tenant-002',
companyName: '远东电缆有限公司', contactName: '李四', contactEmail: 'li@fareast-cable.com',
plan: 'pro', status: 'active',
validFrom: new Date('2026-03-01'), validUntil: new Date('2026-05-31'),
platformType: 'custom', platformUrl: 'https://erp.fareast-cable.com',
features: JSON.stringify({ inquiry: true, imageGen: false, product: true, comparison: true }),
usageQuotas: JSON.stringify({ monthlyLeads: 5000, monthlyImages: 0, monthlyProducts: 2000, monthlyComparisons: 100, maxUsers: 20 }),
},
})
const shangshang = await prisma.tenant.upsert({
where: { id: 'demo-tenant-003' },
update: {},
create: {
id: 'demo-tenant-003',
companyName: '上上电缆集团', contactName: '王五', contactEmail: 'wang@shangshang.com',
plan: 'basic', status: 'active',
validFrom: new Date('2026-04-01'), validUntil: new Date('2027-04-01'),
features: JSON.stringify({ inquiry: true, imageGen: false, product: true, comparison: false }),
usageQuotas: JSON.stringify({ monthlyLeads: 1000, monthlyImages: 0, monthlyProducts: 500, monthlyComparisons: 0, maxUsers: 5 }),
},
})
const minxing = await prisma.tenant.upsert({
where: { id: 'demo-tenant-004' },
update: {},
create: {
id: 'demo-tenant-004',
companyName: '民兴电缆科技', contactName: '赵六', contactEmail: 'zhao@minxing.com',
plan: 'basic', status: 'expired',
validFrom: new Date('2025-01-01'), validUntil: new Date('2026-01-01'),
features: JSON.stringify({ inquiry: true, imageGen: false, product: false, comparison: false }),
usageQuotas: JSON.stringify({ monthlyLeads: 500, monthlyImages: 0, monthlyProducts: 0, monthlyComparisons: 0, maxUsers: 3 }),
},
})
// ---- 用户 ----
await prisma.user.upsert({
where: { email: 'admin@platform.com' },
update: {},
create: { email: 'admin@platform.com', username: 'admin', password: hashed, name: '管理员', role: 'admin', tenantId: demoCompany.id },
})
await prisma.user.upsert({
where: { email: 'user@example.com' },
update: {},
create: { email: 'user@example.com', username: 'user', password: hashed, name: '测试用户', role: 'user', tenantId: demoCompany.id },
})
await prisma.user.upsert({
where: { email: 'li@fareast-cable.com' },
update: {},
create: { email: 'li@fareast-cable.com', username: 'li.fareast', password: hashed, name: '李四', role: 'user', tenantId: farEast.id },
})
await prisma.user.upsert({
where: { email: 'wang@shangshang.com' },
update: {},
create: { email: 'wang@shangshang.com', username: 'wang', password: hashed, name: '王五', role: 'user', tenantId: shangshang.id },
})
await prisma.user.upsert({
where: { email: 'zhao@minxing.com' },
update: {},
create: { email: 'zhao@minxing.com', username: 'zhao', password: hashed, name: '赵六', role: 'user', tenantId: minxing.id },
})
await prisma.rule.createMany({
data: [
{ name: '综合评分权重', type: 'scoring', config: JSON.stringify({ weights: [{ field: '预估金额', weight: 30 }, { field: '匹配度', weight: 25 }, { field: '甲方级别', weight: 20 }, { field: '时效性', weight: 15 }, { field: '历史成交', weight: 10 }] }), enabled: true },
{ name: '防水材料关键词库', type: 'keyword', config: JSON.stringify({ keywords: ['防水卷材', '防水涂料', '聚氨酯', 'SBS改性沥青', '高分子防水'], category: '防水材料', priority: 8 }), enabled: true },
{ name: '电力设备关键词库', type: 'keyword', config: JSON.stringify({ keywords: ['变压器', '断路器', '电缆', '开关柜', 'GIS'], category: '电力设备', priority: 9 }), enabled: true },
{ name: '钢材关键词库', type: 'keyword', config: JSON.stringify({ keywords: ['钢板', '钢管', '型钢', '螺纹钢', 'H型钢'], category: '钢材', priority: 7 }), enabled: true },
{ name: '甲方分级标准', type: 'client_level', config: JSON.stringify({ levels: [{ name: 'S级', minAmount: 1000000, minCooperations: 3 }, { name: 'A级', minAmount: 500000, minCooperations: 1 }, { name: 'B级', minAmount: 100000 }, { name: 'C级', minAmount: 10000 }, { name: 'D级' }] }), enabled: true },
],
})
await prisma.report.createMany({
data: [
{ title: '5月第3周线索数据报告', type: '线索', status: '已完成', config: JSON.stringify({ dateRange: '2024-05-13~2024-05-19', sources: ['中国采购与招标网', '中国电建阳光采购网'], categories: ['电力设备', '钢材', 'IT设备'] }), channels: JSON.stringify(['飞书', '邮箱']) },
{ title: '5月询价发布统计', type: '询价', status: '已完成', config: JSON.stringify({ dateRange: '2024-05-01~2024-05-20' }) },
{ title: '本周商品上架报告', type: '商品', status: '生成中', config: JSON.stringify({ dateRange: '2024-05-20~2024-05-22' }) },
],
})
await prisma.auditLog.createMany({
data: [
{ action: '登录', detail: '用户 admin 登录系统', operator: 'admin', ip: '192.168.1.100' },
{ action: '修改规则', detail: '修改了评分权重配置', operator: 'admin', ip: '192.168.1.100' },
{ action: '导出报告', detail: '导出了5月第3周线索数据报告', operator: 'admin', ip: '192.168.1.100' },
{ action: '删除线索', detail: '删除了2条无效线索', operator: 'admin', ip: '192.168.1.100' },
{ action: '批量上架', detail: '批量上架了5个商品', operator: 'admin', ip: '192.168.1.100' },
{ action: '修改规则', detail: '新增了钢材品类关键词', operator: 'admin', ip: '192.168.1.100' },
{ action: '导出报告', detail: '导出了5月询价发布统计', operator: 'admin', ip: '192.168.1.100' },
{ action: '登录', detail: '用户 admin 登录系统', operator: 'admin', ip: '192.168.1.100' },
],
})
await prisma.crawlerConfig.deleteMany()
await prisma.crawlerConfig.createMany({
data: [
{ name: '中国电建阳光采购网', targetUrl: 'https://bid.powerchina.cn', type: 'bid_powerchina', sourceName: '中电建', status: 'active', schedule: '0 8 * * 1-5', totalRuns: 286, successRuns: 275, failedRuns: 11, keywords: '["电缆","防火门","配电箱","钢材"]' },
{ name: '新矿集团供应链平台', targetUrl: 'https://xkgroup.kdcloud.com', type: 'bid_xkgroup', sourceName: '新矿集团', scriptPath: 'fetch-xkgroup.py', status: 'active', schedule: '5 8 * * 1-5', totalRuns: 142, successRuns: 138, failedRuns: 4, keywords: '["电缆","钢材","阀门","管材"]' },
{ name: '中核集团电子采购平台', targetUrl: 'https://www.cnncecp.com', type: 'bid_cnncecp', sourceName: '中核集团', scriptPath: 'fetch-cnncecp.py', status: 'paused', totalRuns: 0, successRuns: 0, failedRuns: 0, keywords: '["电缆","防火门","钢材","阀门"]' },
],
})
await prisma.inquiry.createMany({
data: [
{ title: '2024年电力电缆框架采购项目', source: '中国电建阳光采购网', url: 'https://example.com/inquiry/001', budget: '500万元', deadline: '2024-12-31', buyer: '中国电力建设集团', category: '电力设备', status: 'pending', level: 'S', star: 5, region: '北京' },
{ title: '钢结构桥梁用钢板采购招标', source: '中国中铁采购电子商务平台', url: 'https://example.com/inquiry/002', budget: '1200万元', deadline: '2024-11-15', buyer: '中铁大桥局', category: '钢材', status: 'published', level: 'A', star: 4, region: '湖北武汉' },
{ title: '数据中心服务器及网络设备采购', source: '中国移动采购与招标网', url: 'https://example.com/inquiry/003', budget: '800万元', deadline: '2024-10-20', buyer: '中国移动通信集团', category: 'IT设备', status: 'pending', level: 'S', star: 5, region: '北京' },
],
})
const cableSKUs = [
{ name: 'YJV-0.6/1kV 3×4 电力电缆', spec: '0.6/1kV 3×4mm²', unit: '米', category: '电力电缆' },
{ name: 'YJV-0.6/1kV 3×6 电力电缆', spec: '0.6/1kV 3×6mm²', unit: '米', category: '电力电缆' },
{ name: 'YJV-0.6/1kV 3×10 电力电缆', spec: '0.6/1kV 3×10mm²', unit: '米', category: '电力电缆' },
{ name: 'YJV22-8.7/15kV 3×70 电力电缆', spec: '8.7/15kV 3×70mm²', unit: '米', category: '电力电缆' },
{ name: 'YJV22-8.7/15kV 3×95 电力电缆', spec: '8.7/15kV 3×95mm²', unit: '米', category: '电力电缆' },
{ name: 'YJV22-8.7/15kV 3×120 电力电缆', spec: '8.7/15kV 3×120mm²', unit: '米', category: '电力电缆' },
{ name: 'YJV22-8.7/15kV 3×150 电力电缆', spec: '8.7/15kV 3×150mm²', unit: '米', category: '电力电缆' },
{ name: 'YJV22-8.7/15kV 3×185 电力电缆', spec: '8.7/15kV 3×185mm²', unit: '米', category: '电力电缆' },
{ name: 'YJV22-8.7/15kV 3×240 电力电缆', spec: '8.7/15kV 3×240mm²', unit: '米', category: '电力电缆' },
{ name: 'YJV22-8.7/15kV 3×300 电力电缆', spec: '8.7/15kV 3×300mm²', unit: '米', category: '电力电缆' },
{ name: 'BV-450/750V 2.5mm² 家装电线', spec: '450/750V 2.5mm²', unit: '米', category: '家装电线' },
{ name: 'BV-450/750V 4mm² 家装电线', spec: '450/750V 4mm²', unit: '米', category: '家装电线' },
{ name: 'BV-450/750V 6mm² 家装电线', spec: '450/750V 6mm²', unit: '米', category: '家装电线' },
{ name: 'KVVP-0.45/0.75kV 14×1.5 控制电缆', spec: '0.45/0.75kV 14×1.5mm²', unit: '米', category: '控制电缆' },
{ name: 'RVVP-300/300V 4×0.75 屏蔽软电缆', spec: '300/300V 4×0.75mm²', unit: '米', category: '控制电缆' },
]
const suppliers = [
{ name: '远东电缆', brand: '远东电缆' },
{ name: '上上电缆', brand: '上上电缆' },
{ name: '民兴电缆', brand: '民兴电缆' },
{ name: '金杯电缆', brand: '金杯电缆' },
{ name: '金龙羽', brand: '金龙羽' },
]
// 价格矩阵:每行是一个商品,每列是对应供应商的价格
// 设计逻辑:每个供应商在不同商品上有价格优势,使得 2/3/4 家组合的最优方案不同
// 远东: 商品0-2最便宜 | 上上: 商品3-5最便宜 | 民兴: 商品6-8最便宜 | 金杯: 商品9-11最便宜 | 金龙羽: 商品12-14最便宜
const priceMatrix: number[][] = [
// SKU 远东 上上 民兴 金杯 金龙羽
/* YJV 3×4 电力电缆 */ [12.00, 12.80, 13.50, 13.20, 14.00],
/* YJV 3×6 电力电缆 */ [18.00, 19.20, 20.10, 19.50, 21.00],
/* YJV 3×10 电力电缆 */ [27.50, 29.00, 30.50, 29.80, 32.00],
/* YJV22 3×70 电力电缆 */ [190.00, 182.00, 195.00, 188.00, 200.00],
/* YJV22 3×95 电力电缆 */ [250.00, 238.00, 260.00, 248.00, 265.00],
/* YJV22 3×120 电力电缆 */[320.00, 305.00, 330.00, 315.00, 340.00],
/* YJV22 3×150 电力电缆 */[410.00, 395.00, 385.00, 400.00, 420.00],
/* YJV22 3×185 电力电缆 */[500.00, 480.00, 465.00, 475.00, 510.00],
/* YJV22 3×240 电力电缆 */[650.00, 630.00, 615.00, 625.00, 670.00],
/* YJV22 3×300 电力电缆 */[810.00, 790.00, 780.00, 770.00, 830.00],
/* BV 2.5mm² 家装电线 */ [1.85, 1.80, 1.75, 1.68, 1.90],
/* BV 4mm² 家装电线 */ [2.70, 2.60, 2.55, 2.45, 2.80],
/* BV 6mm² 家装电线 */ [4.30, 4.15, 4.10, 3.95, 4.50],
/* KVVP 控制电缆 */ [40.00, 38.50, 37.00, 36.00, 42.00],
/* RVVP 屏蔽软电缆 */ [23.00, 22.00, 21.50, 21.00, 24.00],
]
// 每个商品的数量
const quantities = [3000, 2500, 2000, 1500, 1200, 1000, 800, 600, 500, 400, 5000, 4000, 3000, 800, 600]
const productData: any[] = []
for (let si = 0; si < suppliers.length; si++) {
const s = suppliers[si]
for (let pi = 0; pi < cableSKUs.length; pi++) {
productData.push({
name: cableSKUs[pi].name,
spec: cableSKUs[pi].spec,
unit: cableSKUs[pi].unit,
category: cableSKUs[pi].category,
price: priceMatrix[pi][si],
quantity: quantities[pi],
status: 'draft',
brand: s.brand,
supplier: s.name,
minOrder: 100,
})
}
}
await prisma.product.deleteMany()
await prisma.product.createMany({ data: productData })
// Default system configs
const defaultConfigs = [
{ key: 'llm_provider', value: 'minimax', group: 'ai', description: '大模型供应商(minimax / openai)' },
{ key: 'llm_model', value: 'abab-6.5s-chat', group: 'ai', description: '对话模型名称' },
{ key: 'image_provider', value: 'minimax', group: 'ai', description: '图片生成供应商(minimax)' },
{ key: 'mmx_group_id', value: 'mg-20260301-abc123', group: 'ai', description: 'MiniMax 分组 ID' },
{ key: 'mmx_api_key', value: 'sk-mmx-xxxxxxxxxxxxxxxx', group: 'ai', description: 'MiniMax API 密钥' },
{ key: 'jubian_api_url', value: 'https://api.jubian.com/v1', group: 'platform', description: '巨变平台 API 地址' },
{ key: 'jubian_api_key', value: 'jb-sk-xxxxxxxxxxxxxxxx', group: 'platform', description: '巨变平台 API 密钥' },
{ key: 'custom_api_url', value: '', group: 'platform', description: '自建平台 API 地址' },
{ key: 'custom_api_key', value: '', group: 'platform', description: '自建平台 API 密钥' },
{ key: 'upload_max_size', value: '20', group: 'system', description: '上传文件大小限制(MB)' },
{ key: 'session_timeout', value: '120', group: 'system', description: '会话超时时间(分钟)' },
{ key: 'maintenance_mode', value: 'false', group: 'system', description: '系统维护模式(true/false)' },
]
for (const cfg of defaultConfigs) {
await prisma.systemConfig.upsert({
where: { key: cfg.key },
update: {},
create: cfg,
})
}
// Default plan config
await prisma.systemConfig.upsert({
where: { key: 'plan_config' },
update: {},
create: {
key: 'plan_config',
value: JSON.stringify({
basic: {
label: '基础版', price: 3000, description: '小型团队起步',
features: { inquiry: true, imageGen: false, product: true, comparison: false },
quotas: { monthlyLeads: 100, monthlyImages: 0, monthlyProducts: 100, monthlyComparisons: 0, maxUsers: 5 },
platformCount: 1, supportLevel: '社区支持',
},
pro: {
label: '专业版', price: 6000, description: '中型企业',
features: { inquiry: true, imageGen: true, product: true, comparison: true },
quotas: { monthlyLeads: 500, monthlyImages: 50, monthlyProducts: 500, monthlyComparisons: 100, maxUsers: 20 },
platformCount: 3, supportLevel: '标准支持',
},
enterprise: {
label: '旗舰版', price: 9000, description: '大型企业/多租户',
features: { inquiry: true, imageGen: true, product: true, comparison: true },
quotas: { monthlyLeads: 99999, monthlyImages: 99999, monthlyProducts: 99999, monthlyComparisons: 99999, maxUsers: 99999 },
platformCount: 999, supportLevel: '优先技术支持',
},
}),
group: 'system',
description: '订阅方案配置(JSON)',
},
})
console.log('Seed data created')
const categories = [
{ name: '防火门', keywords: ['防火门', '防火卷帘', '防火窗', '钢质防火', '木质防火'] },
{ name: '配电箱', keywords: ['配电箱', '配电柜', '配电盘', '照明配电箱', '动力配电箱', '电表箱'] },
{ name: '电线电缆', keywords: ['电线', '电缆', '导线', '电力线', '控制电缆', '光缆', '架空线'] },
{ name: '阀门', keywords: ['阀门', '闸阀', '球阀', '蝶阀', '截止阀', '止回阀', '调节阀'] },
{ name: '钢材', keywords: ['钢材', '钢管', '钢板', '钢筋', '型钢', '槽钢', '角钢', '工字钢', 'H型钢', '螺纹钢'] },
{ name: '涂料', keywords: ['涂料', '油漆', '防腐漆', '防火涂料', '乳胶漆', '防水涂料'] },
{ name: '混凝土', keywords: ['混凝土', '商混', '商品砼', '水泥', '砂浆', '砼'] },
{ name: '变压器', keywords: ['变压器', '箱变', '干式变', '油浸变', '电力变压器'] },
{ name: '水泵', keywords: ['水泵', '离心泵', '潜污泵', '消防泵', '排污泵', '循环泵'] },
{ name: '风机', keywords: ['风机', '通风机', '排风机', '轴流风机', '离心风机'] },
{ name: '管材', keywords: ['管材', '管道', 'PVC管', 'PE管', '镀锌管', '无缝管', '螺旋管', '给水管'] },
{ name: '开关', keywords: ['开关', '断路器', '接触器', '继电器', '熔断器', '隔离开关'] },
{ name: '灯具', keywords: ['灯具', '照明灯', '路灯', '防爆灯', 'LED灯', '太阳能灯'] },
{ name: '仪表', keywords: ['仪表', '仪器', '传感器', '变送器', '流量计', '压力表', '温度计'] },
{ name: '电机', keywords: ['电机', '电动机', '减速机', '减速器', '变频电机'] },
]
for (const cat of categories) {
await prisma.productCategory.upsert({
where: { name: cat.name },
update: { keywords: JSON.stringify(cat.keywords) },
create: { name: cat.name, keywords: JSON.stringify(cat.keywords) },
})
}
console.log('ProductCategory seed data created')
}
main()
.catch((e) => console.error(e))
.finally(() => prisma.$disconnect())