934 lines
16 KiB
TypeScript
934 lines
16 KiB
TypeScript
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,
|
||
|
||
},
|
||
|
||
});
|
||
|
||
|
||
|
||
const xfxProvider = 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: 'API_AUTO',
|
||
|
||
fulfillmentProviderId: xfxProvider.id,
|
||
|
||
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 categories = await Promise.all([
|
||
|
||
prisma.commonStoreCategory.create({ data: { code: 'HOTPOT', name: '火锅', sort: 1 } }),
|
||
|
||
prisma.commonStoreCategory.create({ data: { code: 'LOCAL', name: '地方菜', sort: 2 } }),
|
||
|
||
]);
|
||
|
||
|
||
|
||
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: 'CONFIRMED',
|
||
|
||
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();
|
||
|
||
});
|
||
|