feat(trade): v4.0.13 收货禁全市与小飞侠拒单可感知
CI / verify (push) Has been cancelled

拦截伪区县写入与下单;推单失败挂 fulfillmentHold,HQ 可见超区/推单失败。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-02 17:20:40 +08:00
parent 63fcf33416
commit c6b01def4c
18 changed files with 613 additions and 76 deletions
+1
View File
@@ -410,3 +410,4 @@ export * from './dev-plan';
export * from './support-ticket';
export * from './phone';
export * from './shanghai-date';
export * from './shipping-address';
@@ -0,0 +1,50 @@
import { describe, expect, it } from 'vitest';
import {
isConcreteShippingDistrict,
isPseudoShippingDistrict,
shippingDetailCityWideHint,
validateShippingAddress,
} from './shipping-address';
describe('shipping address', () => {
it('rejects pseudo districts', () => {
expect(isPseudoShippingDistrict('全市')).toBe(true);
expect(isPseudoShippingDistrict('全部')).toBe(true);
expect(isPseudoShippingDistrict('')).toBe(true);
expect(isConcreteShippingDistrict('金水区')).toBe(true);
});
it('requires concrete district and detail length', () => {
expect(
validateShippingAddress({
province: '河南省',
city: '郑州市',
district: '全市',
detail: '上馆子信阳菜尚购生活广场店',
}).ok,
).toBe(false);
expect(
validateShippingAddress({
province: '河南省',
city: '郑州市',
district: '金水区',
detail: '短',
}).message,
).toMatch(/详细/);
expect(
validateShippingAddress({
province: '河南省',
city: '郑州市',
district: '金水区',
detail: '沙口路8号院尚购生活广场',
}).ok,
).toBe(true);
});
it('soft-hints bare 全市 in detail', () => {
expect(shippingDetailCityWideHint('河南省郑州市全市上馆子')).toBeTruthy();
expect(shippingDetailCityWideHint('金水区沙口路8号')).toBeNull();
});
});
+75
View File
@@ -0,0 +1,75 @@
/** 门店筛选用伪区县,禁止写入收货地址 */
export const PSEUDO_SHIPPING_DISTRICTS = ['全市', '全部'] as const;
export const SHIPPING_DETAIL_MIN_LEN = 8;
export const SHIPPING_REGION_REQUIRED_MSG = '请选择具体区县';
export const SHIPPING_DETAIL_REQUIRED_MSG = '请填写详细地址';
export const SHIPPING_DETAIL_TOO_SHORT_MSG = '请填写更详细的收货地址(含街道门牌)';
export type ShippingAddressFields = {
province?: string | null;
city?: string | null;
district?: string | null;
detail?: string | null;
};
export type ShippingAddressValidation = {
ok: boolean;
message?: string;
};
function trim(v: string | null | undefined): string {
return String(v ?? '').trim();
}
export function isPseudoShippingDistrict(district: string | null | undefined): boolean {
const d = trim(district);
if (!d) return true;
return (PSEUDO_SHIPPING_DISTRICTS as readonly string[]).includes(d);
}
/** 区县是否可作为收货地址(非空且非全市/全部) */
export function isConcreteShippingDistrict(district: string | null | undefined): boolean {
return !isPseudoShippingDistrict(district);
}
/**
* 校验收货省市区 + 详细地址。
* - 现场取货等特殊值请勿调用本函数
* - 不依赖完整省市区树;前端另做树内白名单
*/
export function validateShippingAddress(
input: ShippingAddressFields,
options?: { requireDetail?: boolean },
): ShippingAddressValidation {
const province = trim(input.province);
const city = trim(input.city);
const district = trim(input.district);
const detail = trim(input.detail);
const requireDetail = options?.requireDetail !== false;
if (!province || !city) {
return { ok: false, message: '请选择所在地区' };
}
if (!isConcreteShippingDistrict(district)) {
return { ok: false, message: SHIPPING_REGION_REQUIRED_MSG };
}
if (requireDetail) {
if (!detail) {
return { ok: false, message: SHIPPING_DETAIL_REQUIRED_MSG };
}
if (detail.length < SHIPPING_DETAIL_MIN_LEN) {
return { ok: false, message: SHIPPING_DETAIL_TOO_SHORT_MSG };
}
}
return { ok: true };
}
/** 详细地址含「全市」时的软提示(不单独作为硬失败条件) */
export function shippingDetailCityWideHint(detail: string | null | undefined): string | null {
const d = trim(detail);
if (!d.includes('全市')) return null;
if (/路|街|巷|号|大厦|广场|小区|村|镇|乡/.test(d)) return null;
return '详细地址含「全市」,建议改为具体街道门牌,以免配送拒单';
}
+6 -1
View File
@@ -99,11 +99,16 @@ export interface OrderTrackDto {
queryError?: string | null;
}
/** 大单拦截原因:≥10 箱不自动推小飞侠 */
/** 履约拦截原因(大单 / 承运商拒单等) */
export const FULFILLMENT_HOLD_REASON_LABELS: Record<string, string> = {
LARGE_ORDER_GE_10_BOXES: '大单≥10箱,待总部确认推单/自配送',
COURIER_OUT_OF_SERVICE: '小飞侠超出服务区,待改址后重推或自配送',
COURIER_DISPATCH_FAILED: '自动推配送失败,待总部确认重推或自配送',
};
export const FULFILLMENT_HOLD_COURIER_OUT_OF_SERVICE = 'COURIER_OUT_OF_SERVICE';
export const FULFILLMENT_HOLD_COURIER_DISPATCH_FAILED = 'COURIER_DISPATCH_FAILED';
export type PartnerProxyDeliveryMode = 'ADDRESS' | 'ON_SITE_PICKUP';
export type PartnerProxyOrderProductOption = {