import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../../common/prisma/prisma.module'; import { serializeBigInt } from '../../common/decorators/current-user.decorator'; import { orderStatusLogWhere } from '../../common/event/event.helpers'; import { mapOrderCompat, mapStatusLogCompat } from '../../common/compat/v31-compat'; import { TradeService } from '../trade/trade.service'; import { AdminXiaofeixiaService } from './admin-xiaofeixia.service'; import type { AdminOrdersQueryDto } from './dto/admin-query.dto'; import type { AdminShipOrderDto } from './dto/admin-mutate.dto'; import type { XiaofeixiaCreateShipmentDto } from './dto/admin-courier.dto'; @Injectable() export class AdminOrdersService { constructor( private readonly prisma: PrismaService, private readonly tradeService: TradeService, private readonly xiaofeixiaService: AdminXiaofeixiaService, private readonly config: ConfigService, ) {} async list(query: AdminOrdersQueryDto) { const page = query.page ?? 1; const pageSize = query.pageSize ?? 20; const where: Prisma.OrderWhereInput = {}; if (query.orderNo) where.orderNo = { contains: query.orderNo }; if (query.status) where.status = query.status as Prisma.EnumOrderStatusFilter['equals']; if (query.userId) where.userId = BigInt(query.userId); if (query.receiverPhone) where.receiverPhone = { contains: query.receiverPhone }; if (query.createdFrom || query.createdTo) { where.createdAt = {}; if (query.createdFrom) where.createdAt.gte = new Date(query.createdFrom); if (query.createdTo) where.createdAt.lte = new Date(query.createdTo); } const [items, total] = await Promise.all([ this.prisma.order.findMany({ where, orderBy: { createdAt: 'desc' }, skip: (page - 1) * pageSize, take: pageSize, include: { user: { select: { id: true, userNo: true, phone: true, nickname: true } }, delivery: { select: { provider: true, trackingNo: true, providerOrderNo: true } }, }, }), this.prisma.order.count({ where }), ]); return serializeBigInt({ items, total, page, pageSize }); } async detail(id: bigint) { const order = await this.prisma.order.findUnique({ where: { id }, include: { user: { select: { id: true, userNo: true, phone: true, nickname: true, deviceKey: true, phoneVerifiedAt: true, }, }, delivery: true, benefitCoupon: { select: { id: true, couponNo: true, balance: true, status: true }, }, city: { select: { id: true, name: true, code: true } }, product: { select: { id: true, name: true, skuCode: true, barcode69: true } }, imageResource: { select: { id: true, url: true } }, }, }); if (!order) throw new NotFoundException('订单不存在'); const statusLogs = await this.prisma.commonEvent.findMany({ where: orderStatusLogWhere(id), orderBy: { createdAt: 'asc' }, }); return serializeBigInt(mapOrderCompat({ ...order, statusLogs: mapStatusLogCompat(statusLogs), benefitCoupons: order.benefitCoupon ? [order.benefitCoupon] : [], })); } async updateStatusDebug(id: bigint, status: string) { const order = await this.prisma.order.findUnique({ where: { id } }); if (!order) throw new NotFoundException('订单不存在'); await this.tradeService.applyStatusTransition(id, order.status, status, 'HQ_DEBUG'); return this.detail(id); } getShipDefaults() { return { provider: 'XFX', providerLabel: '小飞侠', fromName: this.config.get('SHIP_FROM_NAME') || '杜康仓库', fromMobile: this.config.get('SHIP_FROM_MOBILE') || '13800000000', fromAddress: this.config.get('SHIP_FROM_ADDRESS') || '河南省郑州市金水区', fromAddressDetail: this.config.get('SHIP_FROM_ADDRESS_DETAIL') || '杜康酒业仓', fromLng: Number(this.config.get('SHIP_FROM_LNG') || 113.665), fromLat: Number(this.config.get('SHIP_FROM_LAT') || 34.757), weight: 2, payMode: '1', }; } async shipOrder(id: bigint, dto: AdminShipOrderDto) { if (dto.provider !== 'XFX') { throw new BadRequestException('暂仅支持小飞侠配送'); } const order = await this.prisma.order.findUnique({ where: { id }, include: { delivery: true }, }); if (!order) throw new NotFoundException('订单不存在'); if (!['PENDING_SHIP', 'OUT_WAREHOUSE'].includes(order.status)) { throw new BadRequestException('当前订单状态不可发货'); } if (order.delivery?.trackingNo) { throw new BadRequestException('该订单已有运单号,请勿重复发货'); } const defaults = this.getShipDefaults(); const shipmentDto: XiaofeixiaCreateShipmentDto = { outNumber: order.orderNo, fromName: dto.fromName || defaults.fromName, fromMobile: dto.fromMobile || defaults.fromMobile, fromAddress: dto.fromAddress || defaults.fromAddress, fromAddressDetail: dto.fromAddressDetail || defaults.fromAddressDetail, fromLng: dto.fromLng ?? defaults.fromLng, fromLat: dto.fromLat ?? defaults.fromLat, toName: order.receiverName, toMobile: order.receiverPhone, toAddress: `${order.receiverProvince}${order.receiverCity}${order.receiverDistrict}`, toAddressDetail: order.receiverAddress, goodsName: order.productName, goodsNum: order.quantity, weight: dto.weight ?? defaults.weight, payMode: dto.payMode || defaults.payMode, remark: dto.remark || `HQ发货 ${order.orderNo}`, }; const result = await this.xiaofeixiaService.createShipment(shipmentDto); if (!result.ok || !result.data) { throw new BadRequestException(result.error || '小飞侠创建运单失败'); } const { providerShipmentId, trackingNumber } = result.data; const now = new Date(); await this.prisma.$transaction(async (tx) => { if (order.delivery) { await tx.orderDelivery.update({ where: { orderId: id }, data: { provider: 'XFX', trackingNo: trackingNumber, providerOrderNo: String(providerShipmentId), shippingAt: now, }, }); } else { await tx.orderDelivery.create({ data: { orderId: id, provider: 'XFX', trackingNo: trackingNumber, providerOrderNo: String(providerShipmentId), shippingAt: now, }, }); } }); await this.tradeService.applyStatusTransition(id, order.status, 'SHIPPING', 'HQ_SHIP'); return this.detail(id); } async batchDeleteOrders(ids: bigint[]) { const uniqueIds = [...new Set(ids)]; if (!uniqueIds.length) { return { ok: true, deleted: 0, message: '未选择订单' }; } const orders = await this.prisma.order.findMany({ where: { id: { in: uniqueIds } }, select: { id: true, orderNo: true }, }); if (!orders.length) throw new NotFoundException('订单不存在'); const orderIds = orders.map((o) => o.id); await this.prisma.$transaction(async (tx) => { await this.deleteOrdersInTx(tx, orderIds); }); return { ok: true, deleted: orderIds.length, orderNos: orders.map((o) => o.orderNo), message: '订单及关联业务数据已删除,状态流转等业务日志已保留', }; } private async deleteOrdersInTx(tx: Prisma.TransactionClient, orderIds: bigint[]) { if (!orderIds.length) return; const couponIds = ( await tx.benefitCoupon.findMany({ where: { orderId: { in: orderIds } }, select: { id: true }, }) ).map((c) => c.id); if (couponIds.length) { const redeemIds = ( await tx.redeemRecord.findMany({ where: { couponId: { in: couponIds } }, select: { id: true }, }) ).map((r) => r.id); if (redeemIds.length) { await tx.storePayout.deleteMany({ where: { redeemRecordId: { in: redeemIds } } }); await tx.storeRating.deleteMany({ where: { redeemRecordId: { in: redeemIds } } }); await tx.redeemRecord.deleteMany({ where: { id: { in: redeemIds } } }); } await tx.benefitCoupon.deleteMany({ where: { id: { in: couponIds } } }); } await tx.order.updateMany({ where: { originOrderId: { in: orderIds } }, data: { originOrderId: null }, }); await tx.commonTicket.deleteMany({ where: { refType: 'ORDER', refId: { in: orderIds } }, }); await tx.order.deleteMany({ where: { id: { in: orderIds } } }); } }