/** * 概览页统计 Mock(用户/订单/合伙人/门店/核销,幂等可重复跑) * 用法:cd server/dukang-api && pnpm prisma:seed-stats */ import { PrismaClient, type OrderStatus, type PayStatus } from '@prisma/client'; import { createHash, randomBytes } from 'crypto'; const prisma = new PrismaClient(); const USER_PREFIX = 'STAT'; const ORDER_PREFIX = 'STAT'; const COUPON_PREFIX = 'STATCPN'; const REDEEM_PREFIX = 'STATRD'; const STORE_NAME_PREFIX = 'STAT门店'; const PARTNER_PHONE_PREFIX = '13788'; const PROMO_CODES = [ { code: 'STAT_A', name: '统计演示·品鉴会A', scene: 'EVENT' as const }, { code: 'STAT_B', name: '统计演示·线下提货B', scene: 'OFFLINE_PICKUP' as const }, { code: 'STAT_C', name: '统计演示·线上渠道C', scene: 'ONLINE_LINK' as const }, ]; const CITY_DEFS = [ { code: '410100', name: '郑州市', province: '河南省', district: '金水区' }, { code: '410300', name: '洛阳市', province: '河南省', district: '涧西区' }, ]; function startOfDay(d: Date) { return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0); } function qrcodeIdFor(code: string) { return createHash('sha256').update(`stats-mock:${code}`).digest('hex'); } function mulberry32(seed: number) { return () => { let t = (seed += 0x6d2b79f5); t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } async function cleanup() { console.log('Cleaning previous STAT* mock...'); const redeemIds = ( await prisma.redeemRecord.findMany({ where: { redeemNo: { startsWith: REDEEM_PREFIX } }, select: { id: true }, }) ).map((r) => r.id); if (redeemIds.length) { await prisma.redeemRecordAllocation.deleteMany({ where: { redeemRecordId: { in: redeemIds } }, }); await prisma.storeRating.deleteMany({ where: { redeemRecordId: { in: redeemIds } } }); await prisma.storePayout.deleteMany({ where: { redeemRecordId: { in: redeemIds } } }); await prisma.redeemPendingRecord.deleteMany({ where: { redeemRecordId: { in: redeemIds } }, }); await prisma.redeemRecord.deleteMany({ where: { id: { in: redeemIds } } }); } await prisma.benefitCoupon.deleteMany({ where: { couponNo: { startsWith: COUPON_PREFIX } }, }); const statOrders = await prisma.order.findMany({ where: { orderNo: { startsWith: ORDER_PREFIX } }, select: { id: true }, }); const orderIds = statOrders.map((o) => o.id); if (orderIds.length) { await prisma.benefitCoupon.deleteMany({ where: { orderId: { in: orderIds } } }); await prisma.orderDelivery.deleteMany({ where: { orderId: { in: orderIds } } }); await prisma.userInvoice.deleteMany({ where: { orderId: { in: orderIds } } }); await prisma.order.deleteMany({ where: { id: { in: orderIds } } }); } const statUsers = await prisma.user.findMany({ where: { userNo: { startsWith: USER_PREFIX } }, select: { id: true }, }); const userIds = statUsers.map((u) => u.id); if (userIds.length) { await prisma.userPromoAttribution.deleteMany({ where: { userId: { in: userIds } } }); await prisma.userCityPreference.deleteMany({ where: { userId: { in: userIds } } }); await prisma.userAddress.deleteMany({ where: { userId: { in: userIds } } }); await prisma.benefitCoupon.deleteMany({ where: { userId: { in: userIds } } }); await prisma.user.deleteMany({ where: { id: { in: userIds } } }); } await prisma.commonPromoCode.deleteMany({ where: { code: { in: PROMO_CODES.map((p) => p.code) } }, }); const statStores = await prisma.store.findMany({ where: { name: { startsWith: STORE_NAME_PREFIX } }, select: { id: true }, }); const storeIds = statStores.map((s) => s.id); if (storeIds.length) { await prisma.storeAccountStore.deleteMany({ where: { storeId: { in: storeIds } } }); await prisma.storeBill.deleteMany({ where: { storeId: { in: storeIds } } }); await prisma.store.deleteMany({ where: { id: { in: storeIds } } }); } const statPartners = await prisma.partnerAccount.findMany({ where: { phone: { startsWith: PARTNER_PHONE_PREFIX } }, select: { id: true }, }); const partnerIds = statPartners.map((p) => p.id); if (partnerIds.length) { await prisma.partnerBill.deleteMany({ where: { partnerAccountId: { in: partnerIds } } }); await prisma.partnerAccount.deleteMany({ where: { OR: [{ id: { in: partnerIds } }, { parentAccountId: { in: partnerIds } }] }, }); } } async function ensureCities() { const cities = []; for (const def of CITY_DEFS) { const city = await prisma.commonCity.upsert({ where: { code: def.code }, create: { code: def.code, name: def.name, province: def.province, status: 'ACTIVE', }, update: { name: def.name, province: def.province, status: 'ACTIVE', }, }); cities.push({ ...city, district: def.district }); } return cities; } async function main() { console.log('Seeding stats mock (users/orders/partners/stores/redeems)...'); await cleanup(); const cities = await ensureCities(); const product = await prisma.commonProductItem.findFirst({ orderBy: { id: 'asc' }, select: { id: true, barcode69: true, name: true, spec: true, price: true, }, }); if (!product) throw new Error('没有商品,请先 pnpm prisma:seed'); const category = await prisma.commonStoreCategory.findFirst({ orderBy: { id: 'asc' } }); const promos = []; for (const def of PROMO_CODES) { const promo = await prisma.commonPromoCode.create({ data: { code: def.code, name: def.name, scene: def.scene, qrcodeId: qrcodeIdFor(def.code), status: 'ACTIVE', scanCount: 0, orderCount: 0, }, }); promos.push(promo); } const rand = mulberry32(20260731); const today = startOfDay(new Date()); // ── 城市合伙人 + 门店(近 30 天分散创建) ── const createdPartners: Array<{ id: bigint; cityId: bigint; city: (typeof cities)[0]; }> = []; const createdStores: Array<{ id: bigint; cityId: bigint; partnerAccountId: bigint; settlementRate: number; }> = []; for (let i = 0; i < cities.length; i++) { const city = cities[i]; // 每城 2 个主合伙人 for (let j = 0; j < 2; j++) { const dayOffset = Math.floor(rand() * 28); const createdAt = new Date(today); createdAt.setDate(createdAt.getDate() - dayOffset); createdAt.setHours(9, 0, 0, 0); const phone = `${PARTNER_PHONE_PREFIX}${String(i * 10 + j + 1).padStart(5, '0')}`; const partner = await prisma.partnerAccount.create({ data: { phone, name: `${city.name}统计合伙人${j + 1}`, companyName: `STAT${city.name}合伙人${j + 1}`, isPrimary: 1, status: 'ACTIVE', cityId: city.id, scopeType: 'CITY_WIDE', bindingStatus: 'ACTIVE', orderCommissionRate: 0, redeemCommissionRate: 0.03, bankAccountName: `${city.name}统计合伙人${j + 1}`, bankAccountNo: `622202${String(1000000000 + i * 10 + j)}`, bankBranch: `${city.name}工商银行`, createdAt, updatedAt: createdAt, }, }); createdPartners.push({ id: partner.id, cityId: city.id, city }); // 每位合伙人 3~5 家门店 const storeN = 3 + Math.floor(rand() * 3); for (let k = 0; k < storeN; k++) { const sDay = Math.floor(rand() * Math.max(1, dayOffset + 1)); const sCreated = new Date(today); sCreated.setDate(sCreated.getDate() - sDay); sCreated.setHours(11, Math.floor(rand() * 40), 0, 0); const rate = 0.6; const store = await prisma.store.create({ data: { cityId: city.id, partnerAccountId: partner.id, categoryId: category?.id, name: `${STORE_NAME_PREFIX}-${city.name}-${j + 1}-${k + 1}`, phone: `1399${String(100000 + i * 100 + j * 10 + k).slice(-7)}`, province: city.province, cityName: city.name, district: city.district, address: `统计路${k + 1}号`, settlementRate: rate, status: 'OPEN', auditStatus: 'APPROVED', openTime: '10:00', closeTime: '22:00', createdAt: sCreated, updatedAt: sCreated, }, }); createdStores.push({ id: store.id, cityId: city.id, partnerAccountId: partner.id, settlementRate: rate, }); } } } // ── 用户 + 订单 ── const userCount = 100; const createdUsers: Array<{ id: bigint; cityId: bigint; city: (typeof cities)[0]; promoId: bigint | null; phone: string; }> = []; const paidOrders: Array<{ id: bigint; userId: bigint; cityId: bigint; payAmount: number; productName: string; }> = []; for (let i = 0; i < userCount; i++) { const dayOffset = Math.floor(rand() * 30); const createdAt = new Date(today); createdAt.setDate(createdAt.getDate() - dayOffset); createdAt.setHours(8 + Math.floor(rand() * 12), Math.floor(rand() * 60), 0, 0); const city = cities[Math.floor(rand() * cities.length)]; const promo = rand() < 0.7 ? promos[Math.floor(rand() * promos.length)] : null; const seq = String(i + 1).padStart(4, '0'); const phone = `13888${String(10000 + i).slice(-5)}`; const user = await prisma.user.create({ data: { userNo: `${USER_PREFIX}${seq}`, phone, phoneVerifiedAt: createdAt, nickname: `统计用户${seq}`, deviceKey: `stat-device-${seq}-${randomBytes(4).toString('hex')}`, sourceType: promo ? 'PROMO_CODE' : 'ORGANIC', sourceRefId: promo?.id ?? null, sourceLabel: promo?.name ?? null, createdAt, updatedAt: createdAt, cityPreference: { create: { selectedCityCode: city.code, selectedDistrict: city.district, locateCityCode: city.code, locateDistrict: city.district, updatedAt: createdAt, }, }, ...(promo ? { promoTouch: { create: { promoCodeId: promo.id, channelName: promo.name, firstTouchAt: createdAt, }, }, } : {}), }, }); createdUsers.push({ id: user.id, cityId: city.id, city, promoId: promo?.id ?? null, phone, }); } const statuses: Array<{ status: OrderStatus; payStatus: PayStatus }> = [ { status: 'COMPLETED', payStatus: 'PAID' }, { status: 'COMPLETED', payStatus: 'PAID' }, { status: 'PENDING_SHIP', payStatus: 'PAID' }, { status: 'SHIPPING', payStatus: 'PAID' }, { status: 'PENDING_PAY', payStatus: 'UNPAID' }, { status: 'CANCELLED', payStatus: 'UNPAID' }, ]; let orderCount = 0; const promoOrderInc = new Map(); for (const u of createdUsers) { const n = 1 + Math.floor(rand() * 3); for (let j = 0; j < n; j++) { const dayOffset = Math.floor(rand() * 30); const createdAt = new Date(today); createdAt.setDate(createdAt.getDate() - dayOffset); createdAt.setHours(10 + Math.floor(rand() * 10), Math.floor(rand() * 60), 0, 0); const qty = 2 + Math.floor(rand() * 3); const unit = Number(product.price); const listAmount = unit * qty; const st = statuses[Math.floor(rand() * statuses.length)]; const paid = st.payStatus === 'PAID'; orderCount += 1; const orderNo = `${ORDER_PREFIX}${String(orderCount).padStart(6, '0')}`; const order = await prisma.order.create({ data: { orderNo, userId: u.id, cityId: u.cityId, promoCodeId: u.promoId, channelSource: u.promoId ? 'STATS_MOCK' : null, status: st.status, payStatus: st.payStatus, deliveryType: 'LOCAL', productId: product.id, barcode69: product.barcode69, productName: product.name, productSpec: product.spec, quantity: qty, listUnitPrice: unit, listAmount, productAmount: listAmount, payAmount: listAmount, benefitAmount: listAmount, receiverName: `统计用户`, receiverPhone: u.phone, receiverAddress: `${u.city.name}${u.city.district}统计路1号`, receiverProvince: u.city.province, receiverCity: u.city.name, receiverDistrict: u.city.district, paidAt: paid ? createdAt : null, completedAt: st.status === 'COMPLETED' ? createdAt : null, cancelledAt: st.status === 'CANCELLED' ? createdAt : null, createdAt, updatedAt: createdAt, }, }); if (paid) { paidOrders.push({ id: order.id, userId: u.id, cityId: u.cityId, payAmount: listAmount, productName: product.name, }); } if (u.promoId) { const key = u.promoId.toString(); promoOrderInc.set(key, (promoOrderInc.get(key) ?? 0) + 1); } } } for (const [id, inc] of promoOrderInc) { await prisma.commonPromoCode.update({ where: { id: BigInt(id) }, data: { orderCount: { increment: inc }, scanCount: { increment: Math.floor(inc * 1.5) } }, }); } // ── 权益券 + 核销(约 80 笔,分散近 30 天) ── const storesByCity = new Map(); for (const s of createdStores) { const key = s.cityId.toString(); const list = storesByCity.get(key) ?? []; list.push(s); storesByCity.set(key, list); } let redeemCount = 0; const redeemTarget = Math.min(80, paidOrders.length); for (let i = 0; i < redeemTarget; i++) { const order = paidOrders[i]; const cityStores = storesByCity.get(order.cityId.toString()) ?? createdStores; if (!cityStores.length) continue; const store = cityStores[Math.floor(rand() * cityStores.length)]; const dayOffset = Math.floor(rand() * 30); const createdAt = new Date(today); createdAt.setDate(createdAt.getDate() - dayOffset); createdAt.setHours(14 + Math.floor(rand() * 6), Math.floor(rand() * 60), 0, 0); const amount = Math.round((40 + rand() * 200) * 100) / 100; const settleAmount = Math.round(amount * store.settlementRate * 100) / 100; const coupon = await prisma.benefitCoupon.create({ data: { couponNo: `${COUPON_PREFIX}${String(i + 1).padStart(5, '0')}`, userId: order.userId, orderId: order.id, totalAmount: order.payAmount, usedAmount: amount, balance: Math.max(0, order.payAmount - amount), status: amount >= order.payAmount ? 'USED_UP' : 'ACTIVE', sourceProduct: order.productName, createdAt, updatedAt: createdAt, }, }); redeemCount += 1; await prisma.redeemRecord.create({ data: { redeemNo: `${REDEEM_PREFIX}${String(redeemCount).padStart(5, '0')}`, userId: order.userId, couponId: coupon.id, storeId: store.id, amount, settleAmount, createdAt, }, }); } console.log( `Done: cities=${cities.length}, promos=${promos.length}, partners=${createdPartners.length}, ` + `stores=${createdStores.length}, users=${createdUsers.length}, orders=${orderCount}, redeems=${redeemCount}`, ); } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });