This commit is contained in:
2026-06-30 10:33:56 +08:00
commit 6e047dc0a5
607 changed files with 65966 additions and 0 deletions
@@ -0,0 +1,332 @@
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<string, unknown>) {
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 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) {
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<string, unknown> = { status: targetStatus };
const deliveryData: Record<string, unknown> = {};
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: 'MOCK',
},
});
});
}
}