init
This commit is contained in:
@@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user