v3.5.4版本提交,修改api
CI / verify (pull_request) Waiting to run

This commit is contained in:
2026-08-21 16:22:53 +08:00
parent a01217539c
commit 62e61a9e63
21 changed files with 594 additions and 142 deletions
@@ -38,7 +38,7 @@ import { AlertService } from '../../common/alert/alert.service';
import { PayRedeemAnomalyService } from '../../common/alert/pay-redeem-anomaly.service';
import { WecomMessagePushService } from '../../integrations/wecom/wecom-message-push.service';
import type { Request } from 'express';
import type { CommonProductSku } from '@prisma/client';
import type { ProductSaleUnit } from '@prisma/client';
@Injectable()
export class TradeService {
@@ -64,23 +64,34 @@ export class TradeService {
private readonly logger = new Logger(TradeService.name);
private overlayProductWithSku(
private overlayProductWithSale(
productDto: Record<string, unknown>,
sku: CommonProductSku,
sale: {
skuId: bigint | null;
skuCode: string;
specText: string;
price: unknown;
benefitAmount: unknown;
allowOnSitePickup: boolean;
allowOnlinePurchase: boolean;
allowCrossCityDelivery: boolean;
saleUnit: ProductSaleUnit;
bottlesPerUnit: number;
},
): Record<string, unknown> {
return {
...productDto,
skuCode: sku.skuCode,
spec: sku.specText || productDto.spec,
price: Number(sku.price),
benefitAmount: Number(sku.benefitAmount ?? sku.price),
benefitDisplay: Number(sku.benefitAmount ?? sku.price),
allowOnSitePickup: sku.allowOnSitePickup,
allowOnlinePurchase: sku.allowOnlinePurchase,
allowCrossCityDelivery: sku.allowCrossCityDelivery,
saleUnit: sku.saleUnit,
bottlesPerUnit: sku.bottlesPerUnit,
selectedSkuId: sku.id.toString(),
skuCode: sale.skuCode,
spec: sale.specText || productDto.spec,
price: Number(sale.price),
benefitAmount: Number(sale.benefitAmount ?? sale.price),
benefitDisplay: Number(sale.benefitAmount ?? sale.price),
allowOnSitePickup: sale.allowOnSitePickup,
allowOnlinePurchase: sale.allowOnlinePurchase,
allowCrossCityDelivery: sale.allowCrossCityDelivery,
saleUnit: sale.saleUnit,
bottlesPerUnit: sale.bottlesPerUnit,
...(sale.skuId ? { selectedSkuId: sale.skuId.toString() } : {}),
};
}
@@ -95,7 +106,7 @@ export class TradeService {
},
) {
const viewerPhone = await this.catalogService.resolveUserPhone(userId);
const { product: spu, sku } = await this.catalogService.assertPurchasable(
const { product: spu, sale } = await this.catalogService.assertPurchasable(
BigInt(body.productId),
viewerPhone,
body.skuId,
@@ -104,13 +115,13 @@ export class TradeService {
if (!productDto || productDto.status !== 'ON_SALE') {
throw new BadRequestException('商品不可购买');
}
const product = this.overlayProductWithSku(productDto as Record<string, unknown>, sku);
const product = this.overlayProductWithSale(productDto as Record<string, unknown>, sale);
const city = await this.prisma.commonCity.findFirst({ where: { status: 'ACTIVE' } });
if (!city) throw new BadRequestException('暂无开城城市');
const onSitePickup = !!body.onSitePickup;
if (onSitePickup && !sku.allowOnSitePickup) {
if (onSitePickup && !sale.allowOnSitePickup) {
throw new BadRequestException('该规格不支持现场取货');
}
@@ -129,8 +140,8 @@ export class TradeService {
let addressOk = true;
let addressMessage: string | null = null;
if (!onSitePickup) {
const allowOnline = sku.allowOnlinePurchase !== false;
const allowCross = sku.allowCrossCityDelivery !== false;
const allowOnline = sale.allowOnlinePurchase !== false;
const allowCross = sale.allowCrossCityDelivery !== false;
if (deliveryType === 'LOCAL' && !allowOnline) {
addressOk = false;
addressMessage = '该规格不支持线上购买';
@@ -145,20 +156,20 @@ export class TradeService {
}
}
const bottlesPerUnit = sku.bottlesPerUnit > 0 ? sku.bottlesPerUnit : 1;
const bottlesPerUnit = sale.bottlesPerUnit > 0 ? sale.bottlesPerUnit : 1;
const check = validateMinPurchase(
deliveryType,
body.quantity,
city.localMinQty,
city.crossMinQty,
{ bottlesPerUnit, saleUnit: sku.saleUnit },
{ bottlesPerUnit, saleUnit: sale.saleUnit },
);
const unitPrice = Number(sku.price);
const unitPrice = Number(sale.price);
const productAmount = unitPrice * body.quantity;
const benefitPerUnit = calcBenefitAmount({
price: unitPrice,
benefitAmount: sku.benefitAmount != null ? Number(sku.benefitAmount) : null,
benefitAmount: sale.benefitAmount != null ? Number(sale.benefitAmount) : null,
});
const freightPayType: FreightPayType | null = deliveryType === 'CROSS_CITY' ? 'COD' : null;
@@ -186,10 +197,13 @@ export class TradeService {
addressMessage,
minQty,
onSitePickup,
allowCrossCityDelivery: sku.allowCrossCityDelivery !== false,
allowOnlinePurchase: sku.allowOnlinePurchase !== false,
skuId: sku.id.toString(),
saleUnit: sku.saleUnit,
allowCrossCityDelivery: sale.allowCrossCityDelivery !== false,
allowOnlinePurchase: sale.allowOnlinePurchase !== false,
skuId: sale.skuId?.toString(),
barcode69: sale.barcode69,
productSpec: sale.specText,
unitPrice,
saleUnit: sale.saleUnit,
bottlesPerUnit,
bottleQuantity: body.quantity * bottlesPerUnit,
};
@@ -247,9 +261,6 @@ export class TradeService {
const product = await this.prisma.commonProductItem.findUniqueOrThrow({
where: { id: BigInt(body.productId) },
});
const sku = await this.prisma.commonProductSku.findUniqueOrThrow({
where: { id: BigInt(preview.skuId) },
});
const city = await this.prisma.commonCity.findFirstOrThrow({ where: { status: 'ACTIVE' } });
const orderNo = generateOrderNo();
const payExpireAt = new Date(Date.now() + 30 * 60 * 1000);
@@ -282,15 +293,15 @@ export class TradeService {
payStatus: 'UNPAID',
deliveryType: preview.deliveryType as 'LOCAL' | 'CROSS_CITY' | 'ON_SITE_PICKUP',
productId: product.id,
skuId: sku.id,
barcode69: sku.barcode69,
skuId: preview.skuId ? BigInt(preview.skuId) : null,
barcode69: preview.barcode69 || product.barcode69,
productName: product.name,
productSpec: sku.specText || product.spec,
productSpec: preview.productSpec || product.spec,
imageResourceId: product.coverResourceId,
quantity: body.quantity,
saleUnit: sku.saleUnit,
saleUnit: preview.saleUnit,
bottlesPerUnit: preview.bottlesPerUnit,
listUnitPrice: sku.price,
listUnitPrice: preview.unitPrice,
listAmount: preview.productAmount,
productAmount: preview.productAmount,
receiverName,
@@ -346,7 +357,7 @@ export class TradeService {
extraJson: {
orderId: order.id.toString(),
productId: body.productId,
skuId: sku.id.toString(),
skuId: preview.skuId,
quantity: body.quantity,
onSitePickup,
},
@@ -1080,6 +1091,7 @@ export class TradeService {
titleId?: string;
titleType?: string;
invoiceKind?: string;
invoiceCategory?: string;
titleName?: string;
taxNo?: string | null;
addressPhone?: string | null;
@@ -1088,6 +1100,7 @@ export class TradeService {
phone?: string;
remark?: string;
},
opts?: { allowSpecialKind?: boolean },
) {
const order = await this.prisma.order.findFirst({ where: { id: orderId, userId } });
if (!order) throw new NotFoundException('订单不存在');
@@ -1137,7 +1150,8 @@ export class TradeService {
if (resolved.titleType === 'ENTERPRISE' && !resolved.taxNo?.trim()) {
throw new BadRequestException('企业抬头须填写税号');
}
const invoiceKind = body.invoiceKind || 'NORMAL';
const invoiceKind =
opts?.allowSpecialKind && body.invoiceKind === 'SPECIAL' ? 'SPECIAL' : 'NORMAL';
if (invoiceKind === 'SPECIAL') {
if (resolved.titleType !== 'ENTERPRISE') {
throw new BadRequestException('专用发票仅支持企业抬头');
@@ -1146,6 +1160,7 @@ export class TradeService {
throw new BadRequestException('专用发票须填写税号、地址电话与开户行账号');
}
}
const invoiceCategory = body.invoiceCategory === 'CATERING' ? 'CATERING' : 'LIQUOR';
const invoice = await this.prisma.userInvoice.create({
data: {
@@ -1154,6 +1169,7 @@ export class TradeService {
userId,
titleType: resolved.titleType as never,
invoiceKind: invoiceKind as never,
invoiceCategory: invoiceCategory as never,
titleName: resolved.titleName.trim(),
taxNo: resolved.taxNo?.trim() || null,
addressPhone: resolved.addressPhone?.trim() || null,
@@ -1161,7 +1177,7 @@ export class TradeService {
email: resolved.email.trim(),
phone: resolved.phone.trim(),
remark: body.remark?.trim() || null,
},
} as never,
});
if (!order.isTest) {
@@ -1254,6 +1270,7 @@ export class TradeService {
titleId?: string;
titleType?: string;
invoiceKind?: string;
invoiceCategory?: string;
titleName?: string;
taxNo?: string;
addressPhone?: string;
@@ -1267,7 +1284,7 @@ export class TradeService {
if (!orderNo) throw new BadRequestException('请填写订单号');
const order = await this.prisma.order.findFirst({ where: { orderNo } });
if (!order) throw new NotFoundException('订单不存在');
return this.createInvoice(order.userId, order.id, body);
return this.createInvoice(order.userId, order.id, body, { allowSpecialKind: true });
}
async adminListInvoices(query: {
@@ -1598,7 +1615,7 @@ export class TradeService {
},
viewer?: { phone?: string | null; bypassWhitelist?: boolean },
) {
const { product: spu, sku } = await this.catalogService.assertPurchasable(
const { product: spu, sale } = await this.catalogService.assertPurchasable(
BigInt(body.productId),
viewer?.phone,
body.skuId,
@@ -1620,7 +1637,7 @@ export class TradeService {
let deliveryType: 'LOCAL' | 'CROSS_CITY' | 'ON_SITE_PICKUP' = 'LOCAL';
if (deliveryMode === 'ON_SITE_PICKUP') {
if (!sku.allowOnSitePickup) {
if (!sale.allowOnSitePickup) {
throw new BadRequestException('该规格不支持现场提货');
}
deliveryType = 'ON_SITE_PICKUP';
@@ -1629,8 +1646,8 @@ export class TradeService {
if (receiverCity && receiverCity !== city.name && receiverCity !== '郑州市') {
deliveryType = 'CROSS_CITY';
}
const allowOnline = sku.allowOnlinePurchase !== false;
const allowCross = sku.allowCrossCityDelivery !== false;
const allowOnline = sale.allowOnlinePurchase !== false;
const allowCross = sale.allowCrossCityDelivery !== false;
if (deliveryType === 'LOCAL' && !allowOnline) {
throw new BadRequestException('该规格不支持线上购买');
}
@@ -1644,21 +1661,21 @@ export class TradeService {
}
}
const bottlesPerUnit = sku.bottlesPerUnit > 0 ? sku.bottlesPerUnit : 1;
const bottlesPerUnit = sale.bottlesPerUnit > 0 ? sale.bottlesPerUnit : 1;
const check = validateMinPurchase(
deliveryType === 'CROSS_CITY' ? 'CROSS_CITY' : deliveryType === 'ON_SITE_PICKUP' ? 'ON_SITE_PICKUP' : 'LOCAL',
body.quantity,
city.localMinQty,
city.crossMinQty,
{ bottlesPerUnit, saleUnit: sku.saleUnit },
{ bottlesPerUnit, saleUnit: sale.saleUnit },
);
if (!check.ok) throw new BadRequestException(check.message);
const unitPrice = Number(sku.price);
const unitPrice = Number(sale.price);
const productAmount = unitPrice * body.quantity;
const benefitPerUnit = calcBenefitAmount({
price: unitPrice,
benefitAmount: sku.benefitAmount != null ? Number(sku.benefitAmount) : null,
benefitAmount: sale.benefitAmount != null ? Number(sale.benefitAmount) : null,
});
return {
@@ -1667,8 +1684,10 @@ export class TradeService {
benefitAmount: benefitPerUnit * body.quantity,
deliveryType,
unitPrice,
skuId: sku.id.toString(),
saleUnit: sku.saleUnit,
skuId: sale.skuId?.toString(),
barcode69: sale.barcode69,
productSpec: sale.specText,
saleUnit: sale.saleUnit,
bottlesPerUnit,
bottleQuantity: body.quantity * bottlesPerUnit,
minQuantity: toMinSaleQuantity(
@@ -1755,9 +1774,6 @@ export class TradeService {
const product = await this.prisma.commonProductItem.findUniqueOrThrow({
where: { id: BigInt(body.productId) },
});
const sku = await this.prisma.commonProductSku.findUniqueOrThrow({
where: { id: BigInt(preview.skuId) },
});
const city = await this.prisma.commonCity.findFirstOrThrow({ where: { status: 'ACTIVE' } });
let receiverName = body.receiverName?.trim() || `用户${normalizedPhone.slice(-4)}`;
@@ -1815,15 +1831,15 @@ export class TradeService {
channelSource: 'PROXY_ONLINE',
promoCodeId,
productId: product.id,
skuId: sku.id,
barcode69: sku.barcode69,
skuId: preview.skuId ? BigInt(preview.skuId) : null,
barcode69: preview.barcode69 || product.barcode69,
productName: product.name,
productSpec: sku.specText || product.spec,
productSpec: preview.productSpec || product.spec,
imageResourceId: product.coverResourceId,
quantity: body.quantity,
saleUnit: sku.saleUnit,
saleUnit: preview.saleUnit,
bottlesPerUnit: preview.bottlesPerUnit,
listUnitPrice: sku.price,
listUnitPrice: preview.unitPrice,
listAmount: preview.productAmount,
productAmount: preview.productAmount,
payAmount: preview.payAmount,
@@ -2171,9 +2187,6 @@ export class TradeService {
const product = await this.prisma.commonProductItem.findUniqueOrThrow({
where: { id: BigInt(body.productId) },
});
const sku = await this.prisma.commonProductSku.findUniqueOrThrow({
where: { id: BigInt(preview.skuId) },
});
const city = await this.prisma.commonCity.findFirstOrThrow({ where: { status: 'ACTIVE' } });
let receiverName = body.receiverName?.trim() || `用户${normalizedPhone.slice(-4)}`;
@@ -2231,15 +2244,15 @@ export class TradeService {
channelSource: 'PROXY_ONLINE',
promoCodeId,
productId: product.id,
skuId: sku.id,
barcode69: sku.barcode69,
skuId: preview.skuId ? BigInt(preview.skuId) : null,
barcode69: preview.barcode69 || product.barcode69,
productName: product.name,
productSpec: sku.specText || product.spec,
productSpec: preview.productSpec || product.spec,
imageResourceId: product.coverResourceId,
quantity: body.quantity,
saleUnit: sku.saleUnit,
saleUnit: preview.saleUnit,
bottlesPerUnit: preview.bottlesPerUnit,
listUnitPrice: sku.price,
listUnitPrice: preview.unitPrice,
listAmount: preview.productAmount,
productAmount: preview.productAmount,
payAmount: preview.payAmount,