v3.5.4版本提交
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-08-21 15:48:15 +08:00
parent 26334ed072
commit a01217539c
32 changed files with 2278 additions and 147 deletions
+26
View File
@@ -7,6 +7,8 @@ import {
calcOrderBoxCount,
shouldHoldAutoCourierDispatch,
validateMinPurchase,
toBottleQuantity,
toMinSaleQuantity,
validateBusinessHours,
formatBusinessHours,
validateRedeemAmount,
@@ -47,6 +49,30 @@ describe('validateMinPurchase', () => {
expect(validateMinPurchase('ON_SITE_PICKUP', 1, 2, 6).ok).toBe(false);
expect(validateMinPurchase('ON_SITE_PICKUP', 2, 2, 6).ok).toBe(true);
});
it('box SKU: 1 box (6 bottles) satisfies local and cross mins', () => {
const opts = { bottlesPerUnit: 6, saleUnit: 'BOX' as const };
expect(validateMinPurchase('LOCAL', 1, 2, 6, opts).ok).toBe(true);
expect(validateMinPurchase('CROSS_CITY', 1, 2, 6, opts).ok).toBe(true);
expect(validateMinPurchase('ON_SITE_PICKUP', 1, 2, 6, opts).ok).toBe(true);
});
it('box SKU message uses 箱', () => {
const r = validateMinPurchase('LOCAL', 0, 2, 6, { bottlesPerUnit: 6, saleUnit: 'BOX' });
expect(r.ok).toBe(false);
expect(r.message).toContain('箱');
});
});
describe('toBottleQuantity / toMinSaleQuantity', () => {
it('converts sale qty to bottles', () => {
expect(toBottleQuantity(2, 1)).toBe(2);
expect(toBottleQuantity(1, 6)).toBe(6);
expect(toMinSaleQuantity(2, 1)).toBe(2);
expect(toMinSaleQuantity(2, 6)).toBe(1);
expect(toMinSaleQuantity(6, 6)).toBe(1);
expect(toMinSaleQuantity(7, 6)).toBe(2);
});
});
describe('validateBusinessHours', () => {
+41 -7
View File
@@ -7,27 +7,61 @@ export function calcBenefitAmount(product: ProductPricing): number {
return product.benefitAmount ?? product.price;
}
export type ProductSaleUnit = 'BOTTLE' | 'BOX';
/** 销售数量 → 瓶当量 */
export function toBottleQuantity(quantity: number, bottlesPerUnit = 1): number {
const qty = Math.floor(Number(quantity) || 0);
const per = Math.floor(Number(bottlesPerUnit) || 0);
if (qty <= 0 || per <= 0) return 0;
return qty * per;
}
/**
* 城市起购瓶数 → 最少销售单位数量(向上取整)
* 例:同城 2 瓶、整箱 6 瓶/箱 → minSaleQty = 1
*/
export function toMinSaleQuantity(minBottleQty: number, bottlesPerUnit = 1): number {
const minBottles = Math.floor(Number(minBottleQty) || 0);
const per = Math.floor(Number(bottlesPerUnit) || 0);
if (minBottles <= 0) return 1;
if (per <= 0) return minBottles;
return Math.max(1, Math.ceil(minBottles / per));
}
export function saleUnitLabel(saleUnit: ProductSaleUnit | string | null | undefined): string {
return saleUnit === 'BOX' ? '箱' : '瓶';
}
export function validateMinPurchase(
deliveryType: 'LOCAL' | 'CROSS_CITY' | 'ON_SITE_PICKUP',
quantity: number,
localMinQty: number,
crossMinQty: number,
options?: { bottlesPerUnit?: number; saleUnit?: ProductSaleUnit },
): { ok: boolean; message?: string } {
const bottlesPerUnit = options?.bottlesPerUnit ?? 1;
const saleUnit = options?.saleUnit ?? (bottlesPerUnit > 1 ? 'BOX' : 'BOTTLE');
const bottleQty = toBottleQuantity(quantity, bottlesPerUnit);
const unit = saleUnitLabel(saleUnit);
if (deliveryType === 'ON_SITE_PICKUP') {
const min = localMinQty > 0 ? localMinQty : 2;
if (quantity < min) {
return { ok: false, message: `现场提货至少购买 ${min}` };
const minBottles = localMinQty > 0 ? localMinQty : 2;
if (bottleQty < minBottles) {
const minSale = toMinSaleQuantity(minBottles, bottlesPerUnit);
return { ok: false, message: `现场提货至少购买 ${minSale}${unit}` };
}
return { ok: true };
}
const min = deliveryType === 'LOCAL' ? localMinQty : crossMinQty;
if (quantity < min) {
const minBottles = deliveryType === 'LOCAL' ? localMinQty : crossMinQty;
if (bottleQty < minBottles) {
const minSale = toMinSaleQuantity(minBottles, bottlesPerUnit);
return {
ok: false,
message:
deliveryType === 'LOCAL'
? `同城配送至少购买 ${min}`
: `跨城配送至少购买 ${min} 瓶(1箱)`,
? `同城配送至少购买 ${minSale}${unit}`
: `跨城配送至少购买 ${minSale}${unit}`,
};
}
return { ok: true };
+75 -1
View File
@@ -8,16 +8,48 @@ export interface CityDto {
maxPartnerCommissionRate?: number;
}
export type ProductSaleUnit = 'BOTTLE' | 'BOX';
export interface ProductSpecValueDto {
id: string;
name: string;
}
export interface ProductSpecAttrDto {
id: string;
name: string;
values: ProductSpecValueDto[];
}
export interface ProductSkuDto {
id: string;
/** 规格值 id 列表(与 specAttrs 轴顺序对应) */
specValueIds: string[];
specText: string;
price: number;
benefitAmount: number;
status: string;
allowOnlinePurchase: boolean;
allowCrossCityDelivery: boolean;
allowOnSitePickup: boolean;
saleUnit: ProductSaleUnit;
bottlesPerUnit: number;
isDefault: boolean;
}
export interface ProductDto {
id: string;
skuCode: string;
name: string;
subtitle?: string | null;
/** 拍平价:可售 SKU 最低价 / 默认 SKU 价 */
price: number;
benefitAmount: number;
benefitDisplay?: number;
aromaType: string;
status: string;
/** 规格文案(默认/展示 SKU) */
spec?: string;
/** 封面图(common_resource COVER / cover_resource_id */
mainImageUrl?: string | null;
/** 轮播图(bizType=CAROUSEL;无则回退封面) */
@@ -25,12 +57,21 @@ export interface ProductDto {
/** 详情长图(bizType=DETAIL 或 detailContent JSON */
detailImageUrls?: string[];
detailContent?: ProductDetailContentDto | null;
/** 是否允许现场取货下单 */
/** 是否允许现场取货下单(拍平自展示 SKU */
allowOnSitePickup?: boolean;
/** 是否允许配送到址(同城线上购买) */
allowOnlinePurchase?: boolean;
/** 是否允许跨城配送 */
allowCrossCityDelivery?: boolean;
/** v3.5.4:是否配置了销售规格(多 SKU) */
specEnabled?: boolean;
/** 列表轻量:展示 SKU 销售单位 */
saleUnit?: ProductSaleUnit;
/** 详情:规格轴 */
specAttrs?: ProductSpecAttrDto[];
/** 详情:可售 SKU(含 OFF_SALE 供前端灰置时可带 status */
skus?: ProductSkuDto[];
defaultSkuId?: string;
}
export interface ProductDetailFeatureDto {
@@ -68,3 +109,36 @@ export interface ProductDetailTemplateDto {
createdAt: string;
updatedAt: string;
}
/** Admin 保存规格轴 */
export interface AdminProductSpecValueInput {
/** 已有值 id;新建可省略 */
id?: string;
name: string;
sortOrder?: number;
}
export interface AdminProductSpecAttrInput {
id?: string;
name: string;
sortOrder?: number;
values: AdminProductSpecValueInput[];
}
export interface AdminProductSkuInput {
id?: string;
/** 规格值 id;无规格时为空数组 */
specValueIds?: string[];
skuCode?: string;
barcode69: string;
price: number;
benefitAmount?: number;
status?: string;
allowOnSitePickup?: boolean;
allowOnlinePurchase?: boolean;
allowCrossCityDelivery?: boolean;
saleUnit?: ProductSaleUnit;
bottlesPerUnit?: number;
isDefault?: boolean;
sortOrder?: number;
}
+24
View File
@@ -114,6 +114,22 @@ export type PartnerProxyOrderProductOption = {
allowOnSitePickup?: boolean;
allowOnlinePurchase?: boolean;
allowCrossCityDelivery?: boolean;
specEnabled?: boolean;
saleUnit?: 'BOTTLE' | 'BOX';
defaultSkuId?: string;
skus?: Array<{
id: string;
specText: string;
price: number;
benefitAmount: number;
status: string;
allowOnSitePickup: boolean;
allowOnlinePurchase: boolean;
allowCrossCityDelivery: boolean;
saleUnit: 'BOTTLE' | 'BOX';
bottlesPerUnit: number;
isDefault: boolean;
}>;
};
export type PartnerProxyOrderPromoOption = {
@@ -145,6 +161,7 @@ export type PartnerProxyOrderPreviewRequest = {
storeId?: string;
receiverCity?: string;
receiverDistrict?: string;
skuId?: string;
};
export type PartnerProxyOrderPreviewResult = {
@@ -153,6 +170,11 @@ export type PartnerProxyOrderPreviewResult = {
benefitAmount: number;
deliveryType: 'LOCAL' | 'CROSS_CITY' | 'ON_SITE_PICKUP';
unitPrice: number;
skuId?: string;
saleUnit?: 'BOTTLE' | 'BOX';
bottlesPerUnit?: number;
bottleQuantity?: number;
minQuantity?: number;
};
export type PartnerProxyOrderCreateRequest = {
@@ -169,6 +191,7 @@ export type PartnerProxyOrderCreateRequest = {
productId: string;
quantity: number;
promoCodeId?: string;
skuId?: string;
};
/** 合伙人代下单列表项(与 OrderDto 兼容,附带收货信息) */
@@ -203,4 +226,5 @@ export type HqProxyOrderCreateRequest = {
productId: string;
quantity: number;
promoCodeId?: string;
skuId?: string;
};