Files
dukang/server/dukang-api/prisma/seed-stats-mock.ts
T
2026-08-04 21:38:49 +08:00

490 lines
16 KiB
TypeScript

/**
* 概览页统计 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<string, number>();
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<string, typeof createdStores>();
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();
});