import { BadRequestException, ConflictException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; import { DesignListStatus, OrderStatus, Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import { CreateOrderDto } from './dto/create-order.dto'; import { OrderQueryDto } from './dto/order-query.dto'; const PAYMENT_TIMEOUT_MS = 30 * 60 * 1000; function generateOrderNo(): string { const now = new Date(); const ymd = now.getUTCFullYear().toString() + String(now.getUTCMonth() + 1).padStart(2, '0') + String(now.getUTCDate()).padStart(2, '0'); const rand = Math.random().toString(36).slice(2, 8).toUpperCase(); return `${ymd}${rand}`; } @Injectable() export class OrdersService { constructor(private readonly prisma: PrismaService) {} private serialize(order: any) { return { ...order, totalAmount: Number(order.totalAmount), paidAt: order.paidAt?.toISOString?.() ?? null, paymentExpiresAt: order.paymentExpiresAt?.toISOString?.() ?? null, createdAt: order.createdAt.toISOString(), updatedAt: order.updatedAt.toISOString(), items: order.items?.map((item: any) => ({ ...item, price: Number(item.price), })), }; } async listByUser(userId: string, query: OrderQueryDto) { await this.expirePendingOrders({ userId }); const where = { userId, ...(query.status ? { status: query.status } : {}) }; const [rows, total] = await this.prisma.$transaction([ this.prisma.order.findMany({ where, include: { items: true, payment: true }, orderBy: { createdAt: 'desc' }, skip: (query.page - 1) * query.pageSize, take: query.pageSize, }), this.prisma.order.count({ where }), ]); return { list: rows.map((row) => this.serialize(row)), total, page: query.page, pageSize: query.pageSize }; } async findOne(userId: string, id: string) { await this.expirePendingOrders({ id, userId }); const order = await this.prisma.order.findUnique({ where: { id }, include: { items: true, payment: true } }); if (!order) throw new NotFoundException('订单不存在'); if (order.userId !== userId) throw new ForbiddenException('无权访问该订单'); return this.serialize(order); } async create(userId: string, dto: CreateOrderDto) { if (dto.requestId) { const existing = await this.prisma.order.findFirst({ where: { userId, requestId: dto.requestId }, include: { items: true, payment: true }, }); if (existing) return this.serialize(existing); } const address = await this.prisma.address.findUnique({ where: { id: dto.addressId } }); if (!address) throw new NotFoundException('地址不存在'); if (address.userId !== userId) throw new ForbiddenException('无权使用该地址'); const design = dto.designListId ? await this.prisma.designList.findUnique({ where: { id: dto.designListId } }) : null; if (dto.designListId && !design) throw new NotFoundException('设计清单不存在'); if (design && design.userId !== userId) throw new ForbiddenException('无权使用该设计清单'); const products = await this.prisma.product.findMany({ where: { id: { in: dto.items.map((item) => item.productId) }, status: 'ON_SALE' }, }); if (products.length !== new Set(dto.items.map((item) => item.productId)).size) { throw new BadRequestException('存在无效或已下架商品'); } const byId = new Map(products.map((product) => [product.id, product])); const total = dto.items.reduce( (sum, item) => sum.plus(new Prisma.Decimal(byId.get(item.productId)!.price).mul(item.quantity)), new Prisma.Decimal(0), ); const addressSnapshot = { id: address.id, name: address.name, phone: address.phone, province: address.province, city: address.city, district: address.district, detail: address.detail, }; const paymentExpiresAt = new Date(Date.now() + PAYMENT_TIMEOUT_MS); try { const created = await this.prisma.$transaction(async (tx) => { let orderNo = generateOrderNo(); for (let attempt = 0; attempt < 3; attempt += 1) { const collision = await tx.order.findUnique({ where: { orderNo }, select: { id: true } }); if (!collision) break; orderNo = generateOrderNo(); } const order = await tx.order.create({ data: { orderNo, user: { connect: { id: userId } }, requestId: dto.requestId, totalAmount: total, paymentExpiresAt, addressSnapshot: addressSnapshot as Prisma.InputJsonValue, designList: dto.designListId ? { connect: { id: dto.designListId } } : undefined, items: { create: dto.items.map((item) => ({ productId: item.productId, name: byId.get(item.productId)!.name, price: byId.get(item.productId)!.price, quantity: item.quantity, })), }, payment: { create: { status: 'PENDING' } }, }, include: { items: true, payment: true }, }); if (dto.designListId) { await tx.designList.updateMany({ where: { id: dto.designListId, status: { in: [DesignListStatus.DRAFT, DesignListStatus.SUBMITTED] }, }, data: { status: DesignListStatus.PROCESSING }, }); } return order; }); return this.serialize(created); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') { const existing = dto.requestId ? await this.prisma.order.findFirst({ where: { userId, requestId: dto.requestId }, include: { items: true, payment: true }, }) : null; if (existing) return this.serialize(existing); throw new ConflictException('订单号冲突,请重试'); } throw error; } } async cancel(userId: string, id: string) { const order = await this.getOwned(userId, id); if (order.status !== OrderStatus.PENDING) throw new ConflictException('仅待付款订单可取消'); const updated = await this.prisma.order.update({ where: { id }, data: { status: OrderStatus.CANCELLED }, include: { items: true, payment: true }, }); return this.serialize(updated); } async confirm(userId: string, id: string) { const order = await this.getOwned(userId, id); if (order.status !== OrderStatus.SHIPPED) throw new ConflictException('仅待收货订单可确认收货'); const updated = await this.prisma.$transaction(async (tx) => { const completed = await tx.order.update({ where: { id }, data: { status: OrderStatus.COMPLETED }, include: { items: true, payment: true }, }); if (order.designListId) { await tx.designList.updateMany({ where: { id: order.designListId, status: DesignListStatus.PROCESSING }, data: { status: DesignListStatus.DONE }, }); } return completed; }); return this.serialize(updated); } private async getOwned(userId: string, id: string) { await this.expirePendingOrders({ id, userId }); const order = await this.prisma.order.findUnique({ where: { id }, include: { items: true, payment: true } }); if (!order) throw new NotFoundException('订单不存在'); if (order.userId !== userId) throw new ForbiddenException('无权操作该订单'); return order; } private async expirePendingOrders(where: Prisma.OrderWhereInput) { await this.prisma.order.updateMany({ where: { ...where, status: OrderStatus.PENDING, paymentExpiresAt: { lte: new Date() }, }, data: { status: OrderStatus.PAYMENT_EXPIRED }, }); } }