import { BadRequestException, Inject, Injectable, NotFoundException, } from '@nestjs/common'; import { calcBenefitAmount, generateOrderNo, orderTabToStatuses, validateMinPurchase, } from '@dukang/domain'; import { PrismaService } from '../../common/prisma/prisma.module'; import { serializeBigInt } from '../../common/decorators/current-user.decorator'; import { BenefitService } from '../benefit/benefit.service'; import { PAY_PROVIDER, DELIVERY_PROVIDER } from '../../integrations/integrations.constants'; import { IPayProvider } from '../../integrations/pay/pay.interface'; import { IDeliveryProvider } from '../../integrations/delivery/delivery.interface'; @Injectable() export class TradeService { constructor( private readonly prisma: PrismaService, private readonly benefitService: BenefitService, @Inject(PAY_PROVIDER) private readonly payProvider: IPayProvider, @Inject(DELIVERY_PROVIDER) private readonly deliveryProvider: IDeliveryProvider, ) {} async preview(userId: bigint, body: { productId: string; quantity: number; addressId?: string }) { const product = await this.prisma.product.findUnique({ where: { id: BigInt(body.productId) }, }); if (!product || product.status !== 'ON_SALE') { throw new BadRequestException('商品不可购买'); } const city = await this.prisma.city.findFirst({ where: { status: 'ACTIVE' } }); if (!city) throw new BadRequestException('暂无开城城市'); let deliveryType: 'LOCAL' | 'CROSS_CITY' = 'LOCAL'; if (body.addressId) { const address = await this.prisma.userAddress.findFirst({ where: { id: BigInt(body.addressId), userId }, }); if (address && address.city !== city.name && address.city !== '郑州市') { deliveryType = 'CROSS_CITY'; } } const check = validateMinPurchase( deliveryType, body.quantity, city.localMinQty, city.crossMinQty, ); if (!check.ok) throw new BadRequestException(check.message); const unitPrice = Number(product.price); const productAmount = unitPrice * body.quantity; const benefitPerUnit = calcBenefitAmount({ price: unitPrice, benefitAmount: product.benefitAmount ? Number(product.benefitAmount) : null, }); return { product: serializeBigInt(product), quantity: body.quantity, deliveryType, productAmount, freightAmount: deliveryType === 'CROSS_CITY' ? 0 : 0, freightPayType: deliveryType === 'CROSS_CITY' ? 'COD' : null, payAmount: productAmount, benefitAmount: benefitPerUnit * body.quantity, city: serializeBigInt(city), }; } async createOrder( userId: bigint, body: { productId: string; quantity: number; addressId: string; }, ) { const preview = await this.preview(userId, body); const address = await this.prisma.userAddress.findFirst({ where: { id: BigInt(body.addressId), userId }, }); if (!address) throw new BadRequestException('请选择收货地址'); const product = await this.prisma.product.findUniqueOrThrow({ where: { id: BigInt(body.productId) }, }); const city = await this.prisma.city.findFirstOrThrow({ where: { status: 'ACTIVE' } }); const orderNo = generateOrderNo(); const payExpireAt = new Date(Date.now() + 30 * 60 * 1000); const order = await this.prisma.order.create({ data: { orderNo, userId, cityId: city.id, status: 'PENDING_PAY', deliveryType: preview.deliveryType as 'LOCAL' | 'CROSS_CITY', receiverName: address.receiverName, receiverPhone: address.phone, receiverAddress: `${address.province}${address.city}${address.district}${address.detail}`, receiverProvince: address.province, receiverCity: address.city, receiverDistrict: address.district, productAmount: preview.productAmount, freightAmount: preview.freightAmount, freightPayType: preview.freightPayType, payAmount: preview.payAmount, benefitAmount: preview.benefitAmount, payExpireAt, items: { create: { productId: product.id, productName: product.name, productSpec: product.spec, productImage: product.mainImageUrl, unitPrice: product.price, quantity: body.quantity, subtotal: preview.productAmount, }, }, payment: { create: { paymentNo: `PAY${orderNo}`, amount: preview.payAmount, status: 'PENDING', }, }, }, include: { items: true, payment: true }, }); return serializeBigInt(order); } async payOrder(userId: bigint, orderId: bigint) { const order = await this.prisma.order.findFirst({ where: { id: orderId, userId }, include: { items: true, payment: true }, }); if (!order) throw new NotFoundException('订单不存在'); if (order.status !== 'PENDING_PAY') { throw new BadRequestException('订单状态不可支付'); } const { externalNo } = await this.payProvider.payOrder(orderId); const now = new Date(); await this.prisma.$transaction(async (tx) => { await tx.payment.update({ where: { orderId: order.id }, data: { status: 'SUCCESS', paidAt: now, wxTransactionId: externalNo, }, }); await tx.order.update({ where: { id: order.id }, data: { status: 'PENDING_SHIP', paidAt: now }, }); await tx.orderStatusLog.create({ data: { orderId: order.id, fromStatus: 'PENDING_PAY', toStatus: 'PENDING_SHIP', operator: 'MOCK_PAY', }, }); await tx.orderDelivery.create({ data: { orderId: order.id, provider: 'MOCK' }, }); }); await this.benefitService.grantOnOrderPaid(order.id); await this.deliveryProvider.scheduleAutoAdvance(order.id); return this.getOrder(userId, orderId); } async listOrders(userId: bigint, tab = 'all', page = 1, pageSize = 20) { const statuses = orderTabToStatuses(tab); const where = { userId, ...(statuses ? { status: { in: statuses as never[] } } : {}), }; const [list, total] = await Promise.all([ this.prisma.order.findMany({ where, include: { items: true, benefitCoupons: true }, orderBy: { createdAt: 'desc' }, skip: (page - 1) * pageSize, take: pageSize, }), this.prisma.order.count({ where }), ]); return { list: serializeBigInt(list), total, page, pageSize }; } async getOrder(userId: bigint, orderId: bigint) { const order = await this.prisma.order.findFirst({ where: { id: orderId, userId }, include: { items: true, delivery: true, payment: true, benefitCoupons: true, statusLogs: { orderBy: { createdAt: 'desc' } }, }, }); if (!order) throw new NotFoundException('订单不存在'); return serializeBigInt(order); } async updateAddress(userId: bigint, orderId: bigint, body: Record) { const order = await this.prisma.order.findFirst({ where: { id: orderId, userId } }); if (!order) throw new NotFoundException('订单不存在'); if (!['PENDING_PAY', 'PENDING_SHIP', 'OUT_WAREHOUSE'].includes(order.status)) { throw new BadRequestException('当前状态不可修改地址'); } const updated = await this.prisma.order.update({ where: { id: orderId }, data: { receiverName: String(body.receiverName ?? order.receiverName), receiverPhone: String(body.receiverPhone ?? order.receiverPhone), receiverProvince: String(body.receiverProvince ?? order.receiverProvince), receiverCity: String(body.receiverCity ?? order.receiverCity), receiverDistrict: String(body.receiverDistrict ?? order.receiverDistrict), receiverAddress: String(body.receiverAddress ?? order.receiverAddress), }, }); await this.prisma.orderStatusLog.create({ data: { orderId, fromStatus: order.status, toStatus: order.status, operator: 'USER', remark: '修改收货地址', }, }); return serializeBigInt(updated); } async confirmReceive(userId: bigint, orderId: bigint) { const order = await this.prisma.order.findFirst({ where: { id: orderId, userId } }); if (!order) throw new NotFoundException('订单不存在'); if (order.status !== 'PENDING_RECEIVE') { throw new BadRequestException('当前状态不可确认收货'); } await this.applyStatusTransition(order.id, order.status, 'COMPLETED', 'USER'); return this.getOrder(userId, orderId); } async listPartnerOrders(partnerAccountId: bigint, page = 1, pageSize = 20) { const account = await this.prisma.partnerAccount.findUniqueOrThrow({ where: { id: partnerAccountId }, }); const cities = await this.prisma.city.findMany({ where: { partnerId: account.partnerId } }); const cityIds = cities.map((c) => c.id); const where = { cityId: { in: cityIds } }; const [list, total] = await Promise.all([ this.prisma.order.findMany({ where, include: { items: true, delivery: true, user: { select: { phone: true, nickname: true } } }, orderBy: { createdAt: 'desc' }, skip: (page - 1) * pageSize, take: pageSize, }), this.prisma.order.count({ where }), ]); return { list: serializeBigInt(list), total, page, pageSize }; } async getPartnerOrder(partnerAccountId: bigint, orderId: bigint) { const account = await this.prisma.partnerAccount.findUniqueOrThrow({ where: { id: partnerAccountId }, }); const cities = await this.prisma.city.findMany({ where: { partnerId: account.partnerId } }); const order = await this.prisma.order.findFirst({ where: { id: orderId, cityId: { in: cities.map((c) => c.id) } }, include: { items: true, delivery: true, statusLogs: true, user: true }, }); if (!order) throw new NotFoundException('订单不存在'); return serializeBigInt(order); } async advanceDelivery(partnerAccountId: bigint, orderId: bigint, targetStatus: string) { const account = await this.prisma.partnerAccount.findUniqueOrThrow({ where: { id: partnerAccountId }, }); const order = await this.prisma.order.findFirst({ where: { id: orderId, city: { partnerId: account.partnerId }, }, include: { delivery: true }, }); if (!order) throw new NotFoundException('订单不存在'); await this.applyStatusTransition(order.id, order.status, targetStatus); return this.getPartnerOrder(partnerAccountId, orderId); } async applyStatusTransition( orderId: bigint, fromStatus: string, targetStatus: string, operator = 'MOCK', ) { const order = await this.prisma.order.findUnique({ where: { id: orderId } }); if (!order) return; const currentStatus = fromStatus || order.status; const now = new Date(); const data: Record = { status: targetStatus }; const deliveryData: Record = {}; if (targetStatus === 'OUT_WAREHOUSE') deliveryData.outWarehouseAt = now; if (targetStatus === 'SHIPPING') { deliveryData.shippingAt = now; data.shippedAt = now; } if (targetStatus === 'COMPLETED') { deliveryData.deliveredAt = now; data.completedAt = now; } await this.prisma.$transaction(async (tx) => { await tx.order.update({ where: { id: orderId }, data: data as never }); if (Object.keys(deliveryData).length) { await tx.orderDelivery.update({ where: { orderId }, data: deliveryData as never }); } await tx.orderStatusLog.create({ data: { orderId, fromStatus: currentStatus, toStatus: targetStatus, operator, }, }); }); } }