348 lines
12 KiB
TypeScript
348 lines
12 KiB
TypeScript
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 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<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,
|
|
},
|
|
});
|
|
});
|
|
}
|
|
}
|