import { PrismaClient, ResourceBizType, ResourceMediaType, ResourceOwnerType } from '@prisma/client'; import { hashPassword } from '../src/common/crypto/password.util'; import { DEFAULT_PRODUCT_DETAIL_TEMPLATES } from './seeds/product-detail-templates.default'; const prisma = new PrismaClient(); async function createMockResource( ownerType: ResourceOwnerType, ownerId: bigint, bizType: ResourceBizType, url: string, ) { return prisma.commonResource.create({ data: { ownerType, ownerId, bizType, mediaType: ResourceMediaType.IMAGE, ossBucket: 'mock-dukang', ossKey: `mock/${ownerType.toLowerCase()}/${ownerId}/${bizType.toLowerCase()}`, url, }, }); } async function main() { console.log('Seeding v3.1 data...'); await prisma.logUserAnalytics.deleteMany(); await prisma.logPartnerAnalytics.deleteMany(); await prisma.logThirdParty.deleteMany(); await prisma.storePayout.deleteMany(); await prisma.storeRating.deleteMany(); await prisma.redeemRecord.deleteMany(); await prisma.benefitCoupon.deleteMany(); await prisma.orderDelivery.deleteMany(); await prisma.order.deleteMany(); await prisma.commonEvent.deleteMany(); await prisma.commonTicket.deleteMany(); await prisma.userPromoAttribution.deleteMany(); await prisma.userCityPreference.deleteMany(); await prisma.userAddress.deleteMany(); await prisma.user.deleteMany(); await prisma.storeAccount.deleteMany(); await prisma.store.deleteMany(); await prisma.partnerBill.deleteMany(); await prisma.cityWarehouse.deleteMany(); await prisma.fulfillmentProvider.deleteMany(); await prisma.partnerAccount.deleteMany(); await prisma.commonCity.deleteMany(); await prisma.commonProductItem.deleteMany(); await prisma.commonProductDetailTemplate.deleteMany(); await prisma.commonStoreCategory.deleteMany(); await prisma.commonPromoCode.deleteMany(); await prisma.commonResource.deleteMany(); await prisma.hqAccount.deleteMany(); const city = await prisma.commonCity.create({ data: { code: '410100', name: '郑州市', province: '河南省', status: 'ACTIVE', localMinQty: 2, crossMinQty: 6, }, }); const primaryAccount = await prisma.partnerAccount.create({ data: { phone: '13700000001', name: '郑州合伙人主账号', isPrimary: 1, staffRole: 'PARTNER', status: 'ACTIVE', cityId: city.id, scopeType: 'CITY_WIDE', orderCommissionRate: 0, redeemCommissionRate: 0.03, bindingStatus: 'ACTIVE', companyName: '郑州城市合伙人', address: '河南省郑州市金水区', contactPhone: '13700000001', bankAccountName: '郑州合伙人公司', bankAccountNo: '6222021234567890', bankBranch: '工商银行郑州分行', weeklyStoreTarget: 20, }, }); await prisma.fulfillmentProvider.create({ data: { code: 'XFX', name: '小飞侠', type: 'API', status: 'ACTIVE', capabilitiesJson: JSON.stringify({ createShipment: true, getTrack: true, callback: true, cancel: true, }), configJson: JSON.stringify({ apiUrl: 'https://beta.51xiaoju.cn/app/api/interface.do', mchId: '', apiKey: '', signType: 'MD5', }), }, }); const warehouse = await prisma.cityWarehouse.create({ data: { cityId: city.id, name: '郑州中央仓', address: '河南省郑州市金水区物流园1号', contactName: '仓管张', contactPhone: '13700000009', managerType: 'PARTNER', partnerAccountId: primaryAccount.id, status: 'ACTIVE', fulfillmentMode: 'MANUAL', fulfillmentProviderId: null, lng: 113.665, lat: 34.757, }, }); await prisma.partnerAccount.update({ where: { id: primaryAccount.id }, data: { managedWarehouseId: warehouse.id }, }); await prisma.partnerAccount.create({ data: { phone: '13700000002', name: '拓店员小李', isPrimary: 0, parentAccountId: primaryAccount.id, staffRole: 'INTERNAL', permissions: ['store:create', 'store:view'], status: 'DISABLED', }, }); const { DEFAULT_STORE_CATEGORY_TREE } = await import('../src/modules/store/store-category.defaults'); const categoryByCode = new Map(); for (const root of DEFAULT_STORE_CATEGORY_TREE) { const parent = await prisma.commonStoreCategory.create({ data: { code: root.code, name: root.name, sort: root.sort, parentId: null, status: 'ACTIVE', }, }); categoryByCode.set(root.code, parent); for (const child of root.children) { const row = await prisma.commonStoreCategory.create({ data: { code: child.code, name: child.name, sort: child.sort, parentId: parent.id, status: 'ACTIVE', }, }); categoryByCode.set(child.code, row); } } const categories = [ categoryByCode.get('HOTPOT')!, categoryByCode.get('LOCAL')!, ]; for (const tpl of DEFAULT_PRODUCT_DETAIL_TEMPLATES) { await prisma.commonProductDetailTemplate.create({ data: { code: tpl.code, name: tpl.name, description: tpl.description, aromaType: tpl.aromaType as 'QINGXIANG' | 'JIANGXIANG' | 'NONGXIANG' | null, storyTitle: tpl.storyTitle, storyText: tpl.storyText, features: tpl.features, detailImageUrls: [], suggestedDetailImageCount: tpl.suggestedDetailImageCount, sortOrder: tpl.sortOrder, status: 'ACTIVE', }, }); } const productDefs = [ { skuCode: 'JZ-10', name: '酒祖杜康(国标特级10)', subtitle: '清香型 53度', price: 128, sortOrder: 1, img: 'https://picsum.photos/seed/jiuzu10/400/400' }, { skuCode: 'JZ-15', name: '酒祖杜康(国标特级15)', subtitle: '清香型 53度', price: 168, sortOrder: 2, img: 'https://picsum.photos/seed/jiuzu15/400/400' }, { skuCode: 'JZ-20', name: '酒祖杜康(国标特级20)', subtitle: '清香型 53度', price: 298, sortOrder: 3, img: 'https://picsum.photos/seed/jiuzu20/400/400' }, { skuCode: 'JZ-30', name: '酒祖杜康(国标特级30)', subtitle: '清香型 53度', price: 498, sortOrder: 4, img: 'https://picsum.photos/seed/jiuzu30/400/400' }, ]; const products = []; for (const [i, def] of productDefs.entries()) { const product = await prisma.commonProductItem.create({ data: { skuCode: def.skuCode, barcode69: `69000000000${i + 1}`, name: def.name, subtitle: def.subtitle, aromaType: 'QINGXIANG', spec: '500ml | 53度', price: def.price, benefitAmount: def.price, status: 'ON_SALE', sortOrder: def.sortOrder, }, }); const cover = await createMockResource(ResourceOwnerType.PRODUCT, product.id, ResourceBizType.COVER, def.img); await prisma.commonProductItem.update({ where: { id: product.id }, data: { coverResourceId: cover.id }, }); products.push(product); } const storeDefs = [ { name: '郑州老城店', phone: '13910000001', district: '金水区', address: '花园路100号', intro: '正宗河南菜,欢迎核销好客权益', img: 'https://picsum.photos/seed/store1/400/300', categoryId: categories[0].id, settlementRate: 0.6, withAccount: true, }, { name: '郑州美食城店', phone: '13910000002', district: '二七区', address: '大学路200号', intro: '地方特色餐饮', img: 'https://picsum.photos/seed/store2/400/300', categoryId: categories[1].id, settlementRate: 0.65, withAccount: false, }, ]; const createdStores: { id: bigint; name: string }[] = []; for (const def of storeDefs) { const store = await prisma.store.create({ data: { cityId: city.id, partnerAccountId: primaryAccount.id, categoryId: def.categoryId, name: def.name, phone: def.phone, province: '河南省', cityName: '郑州市', district: def.district, address: def.address, intro: def.intro, settlementRate: def.settlementRate, status: 'OPEN', openTime: '10:00', closeTime: '22:00', }, }); const cover = await createMockResource(ResourceOwnerType.STORE, store.id, ResourceBizType.COVER, def.img); await prisma.store.update({ where: { id: store.id }, data: { coverResourceId: cover.id } }); createdStores.push({ id: store.id, name: def.name }); } // 一号两店:主账号 13910000001 绑定老城店 + 美食城店,便于测选店 const multiStorePrimary = await prisma.storeAccount.create({ data: { phone: '13910000001', name: '郑州老城店主', isPrimary: 1, bankAccountName: '郑州老城店主', bankAccountNo: '6222029876543210', bankBranch: '建设银行郑州分行', bindings: { create: createdStores.map((s) => ({ storeId: s.id })), }, }, }); // 子账号样例:仅绑定第一家店 await prisma.storeAccount.create({ data: { phone: '13910000011', name: '老城店收银员', isPrimary: 0, parentAccountId: multiStorePrimary.id, staffRole: 'CASHIER', permissions: ['redeem', 'records'], status: 'ACTIVE', bindings: { create: [{ storeId: createdStores[0].id }], }, }, }); const weekStart = (() => { const d = new Date(); d.setHours(12, 0, 0, 0); const day = d.getDay(); const diff = day === 0 ? 6 : day - 1; d.setDate(d.getDate() - diff); d.setHours(10, 0, 0, 0); return d; })(); const newWeekStore = await prisma.store.create({ data: { cityId: city.id, partnerAccountId: primaryAccount.id, categoryId: categories[0].id, name: '本周新签体验店', phone: '13910000003', province: '河南省', cityName: '郑州市', district: '中原区', address: '建设路88号', intro: '本周新签约门店', settlementRate: 0.6, status: 'OPEN', openTime: '10:00', closeTime: '22:00', createdAt: new Date(weekStart.getTime() + 2 * 24 * 60 * 60 * 1000), }, }); createdStores.push({ id: newWeekStore.id, name: newWeekStore.name }); await prisma.storeAccount.create({ data: { phone: '13910000003', name: '本周新签体验店', isPrimary: 1, bankAccountName: '本周新签体验店', bankAccountNo: '6222029876543211', bankBranch: '农业银行郑州分行', bindings: { create: [{ storeId: newWeekStore.id }] }, }, }); await prisma.user.create({ data: { userNo: 'DK88293401', phone: '13800000001', phoneVerifiedAt: new Date(), nickname: '测试用户', cityPreference: { create: { selectedCityCode: '410100', selectedDistrict: '郑州市', locateCityCode: '410100', locateDistrict: '金水区', }, }, }, }); await prisma.hqAccount.create({ data: { phone: '13600000001', loginName: 'admin', passwordHash: hashPassword('dukang@123!'), name: '总部管理员', adminRole: 'SUPER_ADMIN', }, }); await prisma.commonPromoCode.create({ data: { code: 'DKHQ001', name: '总部品鉴会', scene: 'EVENT', qrcodeId: 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456', status: 'ACTIVE', }, }); await prisma.commonPromoCode.create({ data: { code: 'DKDEMO1', name: '郑州品鉴会演示', scene: 'OFFLINE_PICKUP', qrcodeId: 'b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef12345678', status: 'ACTIVE', }, }); const user = await prisma.user.findUniqueOrThrow({ where: { phone: '13800000001' } }); const product = products[0]; const orderDefs = [ { dayOffset: 0, payAmount: 1198, quantity: 2 }, { dayOffset: 1, payAmount: 599, quantity: 1 }, { dayOffset: 2, payAmount: 1760, quantity: 2 }, { dayOffset: 4, payAmount: 1299, quantity: 1 }, { dayOffset: 5, payAmount: 880, quantity: 1 }, ]; const coupons: { id: bigint; balance: number }[] = []; for (const [index, def] of orderDefs.entries()) { const paidAt = new Date(weekStart.getTime() + def.dayOffset * 24 * 60 * 60 * 1000 + 14 * 60 * 60 * 1000); const listAmount = def.payAmount; const order = await prisma.order.create({ data: { orderNo: `WK${Date.now()}${index}`, userId: user.id, cityId: city.id, status: 'COMPLETED', payStatus: 'PAID', deliveryType: 'LOCAL', productId: product.id, barcode69: product.barcode69, productName: product.name, productSpec: product.spec, quantity: def.quantity, listUnitPrice: product.price, listAmount, productAmount: listAmount, payAmount: listAmount, benefitAmount: listAmount, receiverName: '测试用户', receiverPhone: user.phone!, receiverAddress: '郑州市金水区测试路1号', receiverProvince: '河南省', receiverCity: '郑州市', receiverDistrict: '金水区', paidAt, completedAt: paidAt, partnerAccountIdAtPay: primaryAccount.id, orderCommissionRateAtPay: 0, }, }); const coupon = await prisma.benefitCoupon.create({ data: { couponNo: `CPN${Date.now()}${index}`, userId: user.id, orderId: order.id, totalAmount: listAmount, balance: listAmount, sourceProduct: product.name, }, }); coupons.push({ id: coupon.id, balance: listAmount }); } const redeemDefs = [ { storeIndex: 0, amount: 42800, dayOffset: 1, settlementRate: 0.6 }, { storeIndex: 1, amount: 38400, dayOffset: 2, settlementRate: 0.65 }, { storeIndex: 2, amount: 31200, dayOffset: 3, settlementRate: 0.6 }, ]; for (const [index, def] of redeemDefs.entries()) { const coupon = coupons[index]; const store = createdStores[def.storeIndex]; const createdAt = new Date(weekStart.getTime() + def.dayOffset * 24 * 60 * 60 * 1000 + 16 * 60 * 60 * 1000); const settleAmount = Math.round(def.amount * def.settlementRate * 100) / 100; await prisma.redeemRecord.create({ data: { redeemNo: `RD${Date.now()}${index}`, userId: user.id, couponId: coupon.id, storeId: store.id, amount: def.amount, settleAmount, createdAt, }, }); await prisma.benefitCoupon.update({ where: { id: coupon.id }, data: { usedAmount: def.amount, balance: Math.max(0, coupon.balance - def.amount), }, }); } const now = new Date(); const periodStart = new Date(now.getFullYear(), now.getMonth(), 1); const periodEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0); await prisma.partnerBill.create({ data: { billNo: `PB${Date.now()}`, partnerAccountId: primaryAccount.id, periodStart, periodEnd, orderCommission: 1200, redeemCommission: 800, totalAmount: 2000, status: 'UNPAID', confirmedAt: now, }, }); console.log('Seed complete:', { city: city.name, warehouse: warehouse.name, primaryAccount: primaryAccount.phone, detailTemplates: DEFAULT_PRODUCT_DETAIL_TEMPLATES.length, products: products.length, stores: createdStores.length, testPhones: { user: '13800000001', partner: '13700000001', partnerStaff: '13700000002', hq: '13600000001', }, }); } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });