diff --git a/apps/admin-web/src/components/ProxyOrderModal.tsx b/apps/admin-web/src/components/ProxyOrderModal.tsx index 96b151b..f7423e1 100644 --- a/apps/admin-web/src/components/ProxyOrderModal.tsx +++ b/apps/admin-web/src/components/ProxyOrderModal.tsx @@ -37,8 +37,6 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder const [options, setOptions] = useState(null); const [loadingOptions, setLoadingOptions] = useState(false); const [phone, setPhone] = useState(''); - const [customerSmsCode, setCustomerSmsCode] = useState(''); - const [operatorSmsCode, setOperatorSmsCode] = useState(''); const [receiverName, setReceiverName] = useState(''); const [regionCodes, setRegionCodes] = useState([]); const [addressDetail, setAddressDetail] = useState(''); @@ -47,11 +45,8 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder const [promoCodeId, setPromoCodeId] = useState(); const [deliveryMode, setDeliveryMode] = useState('ADDRESS'); const [autoReceive, setAutoReceive] = useState(false); - const [confirmStep, setConfirmStep] = useState(false); const [preview, setPreview] = useState(null); const [previewLoading, setPreviewLoading] = useState(false); - const [customerCooldown, setCustomerCooldown] = useState(0); - const [operatorCooldown, setOperatorCooldown] = useState(0); const [submitting, setSubmitting] = useState(false); const region = useMemo(() => parseRegionCodes(regionCodes), [regionCodes]); @@ -94,8 +89,6 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder function resetForm() { setPhone(''); - setCustomerSmsCode(''); - setOperatorSmsCode(''); setReceiverName(''); setRegionCodes([]); setAddressDetail(''); @@ -103,10 +96,7 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder setPromoCodeId(undefined); setDeliveryMode('ADDRESS'); setAutoReceive(false); - setConfirmStep(false); setPreview(null); - setCustomerCooldown(0); - setOperatorCooldown(0); setProductId(options?.products[0]?.id); } @@ -115,52 +105,8 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder onClose(); } - function startCooldown(setter: (n: number | ((s: number) => number)) => void) { - setter(60); - const timer = window.setInterval(() => { - setter((s) => { - if (s <= 1) { - window.clearInterval(timer); - return 0; - } - return s - 1; - }); - }, 1000); - } - - async function sendCustomerSms() { - if (!/^1\d{10}$/.test(phone.trim())) { - message.warning('请输入有效手机号'); - return; - } - try { - const res = await request<{ maskedPhone: string }>('/admin/proxy-orders/send-customer-sms', { - method: 'POST', - body: JSON.stringify({ phone: phone.trim() }), - }); - message.success(`客户验证码已发送至 ${res.maskedPhone}`); - startCooldown(setCustomerCooldown); - } catch (e) { - message.error(e instanceof Error ? e.message : '发送失败'); - } - } - - async function sendOperatorSms() { - try { - const res = await request<{ maskedPhone: string }>('/admin/proxy-orders/send-operator-sms', { - method: 'POST', - body: '{}', - }); - message.success(`确认验证码已发送至总部手机 ${res.maskedPhone}`); - startCooldown(setOperatorCooldown); - } catch (e) { - message.error(e instanceof Error ? e.message : '发送失败'); - } - } - function validateForm(): string | null { if (!/^1\d{10}$/.test(phone.trim())) return '请输入有效手机号'; - if (!customerSmsCode.trim()) return '请输入客户验证码'; if (!productId) return '请选择商品'; if (deliveryMode === 'ADDRESS') { if (!region?.province || !region?.city || !region?.district) return '请选择省市区'; @@ -174,32 +120,15 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder return null; } - async function openConfirmStep() { - const err = validateForm(); - if (err) { - message.warning(err); - return; - } - setConfirmStep(true); - setOperatorSmsCode(''); - await sendOperatorSms(); - } - async function submit() { const err = validateForm(); if (err) { message.warning(err); return; } - if (!operatorSmsCode.trim()) { - message.warning('请输入总部确认验证码'); - return; - } const payload: HqProxyOrderCreateRequest = { phone: phone.trim(), - customerSmsCode: customerSmsCode.trim(), - operatorSmsCode: operatorSmsCode.trim(), deliveryMode, autoReceive: deliveryMode === 'ADDRESS' ? true : undefined, receiverName: receiverName.trim() || undefined, @@ -243,186 +172,132 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder width={640} destroyOnClose footer={ - confirmStep ? ( - - - - - ) : ( - - - - - ) + + + + } > - {!confirmStep ? ( -
- - - setPhone(e.target.value.replace(/\D/g, '').slice(0, 11))} - /> - - - + + + setPhone(e.target.value.replace(/\D/g, '').slice(0, 11))} + /> + - - setCustomerSmsCode(e.target.value.replace(/\D/g, '').slice(0, 6))} - /> - + + ({ + allowClear + placeholder="不绑定" + value={promoCodeId} + onChange={setPromoCodeId} + options={(options?.promoCodes ?? []).map((p) => ({ value: p.id, - label: `${p.name} · ${p.spec} · ¥${fmtMoney(p.price)}`, + label: `${p.code} · ${p.name}`, }))} /> + ) : null} - - setQuantity(Math.max(1, Number(v) || 1))} style={{ width: '100%' }} /> - + + { + setDeliveryMode(e.target.value); + if (e.target.value !== 'ADDRESS') setAutoReceive(false); + }} + options={[ + { value: 'ADDRESS', label: '配送到址' }, + { value: 'ON_SITE_PICKUP', label: '现场提货' }, + ]} + /> + - {(options?.promoCodes.length ?? 0) > 0 ? ( - - setReceiverName(e.target.value)} /> - ) : null} - - - { - setDeliveryMode(e.target.value); - if (e.target.value !== 'ADDRESS') setAutoReceive(false); - }} - options={[ - { value: 'ADDRESS', label: '配送到址' }, - { value: 'ON_SITE_PICKUP', label: '现场提货' }, - ]} - /> - - - {deliveryMode === 'ADDRESS' ? ( - <> - - setReceiverName(e.target.value)} - /> - - - - - - setAddressDetail(e.target.value)} - /> - - - setAutoReceive(e.target.checked)}> - 同意自动收货(配送到址必选) - - - - ) : null} - -
- 费用预览 - {previewLoading ? ( -
计算中…
- ) : preview ? ( - -
履约:{deliveryLabel}
-
商品金额:¥{fmtMoney(preview.productAmount)}
-
实付:¥{fmtMoney(preview.payAmount)}
-
好客权益:¥{fmtMoney(preview.benefitAmount)}
-
- ) : ( -
- 请完善商品与数量 -
- )} -
-
- ) : ( -
- - - - setOperatorSmsCode(e.target.value.replace(/\D/g, '').slice(0, 6))} + + + + + setAddressDetail(e.target.value)} /> - - - - {preview ? ( - - {deliveryLabel} · 实付 ¥{fmtMoney(preview.payAmount)} · 权益 ¥{fmtMoney(preview.benefitAmount)} - - ) : null} - - )} + + + setAutoReceive(e.target.checked)}> + 同意自动收货(配送到址必选) + + + + ) : null} + +
+ 费用预览 + {previewLoading ? ( +
计算中…
+ ) : preview ? ( + +
履约:{deliveryLabel}
+
商品金额:¥{fmtMoney(preview.productAmount)}
+
实付:¥{fmtMoney(preview.payAmount)}
+
好客权益:¥{fmtMoney(preview.benefitAmount)}
+
+ ) : ( +
+ 请完善商品与数量 +
+ )} +
+ ); } diff --git a/apps/h5-partner/src/lib/proxyOrderDraft.ts b/apps/h5-partner/src/lib/proxyOrderDraft.ts new file mode 100644 index 0000000..50e1156 --- /dev/null +++ b/apps/h5-partner/src/lib/proxyOrderDraft.ts @@ -0,0 +1,56 @@ +import type { PartnerProxyDeliveryMode } from '@dukang/shared-types'; + +const PROXY_ORDER_DRAFT_KEY = 'partner_proxy_order_draft_v2'; + +export type ProxyOrderDraft = { + phone: string; + receiverName: string; + regionCodes: string[]; + addressDetail: string; + productId: string; + quantity: number; + promoCodeId: string; + deliveryMode: PartnerProxyDeliveryMode; + autoReceive: boolean; +}; + +export function saveProxyOrderDraft(draft: ProxyOrderDraft) { + try { + sessionStorage.setItem(PROXY_ORDER_DRAFT_KEY, JSON.stringify(draft)); + } catch { + /* ignore quota / private mode */ + } +} + +export function loadProxyOrderDraft(): ProxyOrderDraft | null { + try { + const raw = sessionStorage.getItem(PROXY_ORDER_DRAFT_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw) as Partial; + if (!parsed || typeof parsed !== 'object') return null; + return { + phone: typeof parsed.phone === 'string' ? parsed.phone : '', + receiverName: typeof parsed.receiverName === 'string' ? parsed.receiverName : '', + regionCodes: Array.isArray(parsed.regionCodes) + ? parsed.regionCodes.filter((c): c is string => typeof c === 'string') + : [], + addressDetail: typeof parsed.addressDetail === 'string' ? parsed.addressDetail : '', + productId: typeof parsed.productId === 'string' ? parsed.productId : '', + quantity: typeof parsed.quantity === 'number' && parsed.quantity >= 1 ? parsed.quantity : 2, + promoCodeId: typeof parsed.promoCodeId === 'string' ? parsed.promoCodeId : '', + deliveryMode: parsed.deliveryMode === 'ON_SITE_PICKUP' ? 'ON_SITE_PICKUP' : 'ADDRESS', + autoReceive: parsed.autoReceive === true, + }; + } catch { + return null; + } +} + +export function clearProxyOrderDraft() { + try { + sessionStorage.removeItem(PROXY_ORDER_DRAFT_KEY); + sessionStorage.removeItem('partner_proxy_order_draft_v1'); + } catch { + /* ignore */ + } +} diff --git a/apps/h5-partner/src/pages/ProxyOrderPage.tsx b/apps/h5-partner/src/pages/ProxyOrderPage.tsx index b522b1e..d6afe75 100644 --- a/apps/h5-partner/src/pages/ProxyOrderPage.tsx +++ b/apps/h5-partner/src/pages/ProxyOrderPage.tsx @@ -4,6 +4,12 @@ import PageHeader from '@dukang/shared-ui/PageHeader'; import ChinaRegionPicker from '../components/ChinaRegionPicker'; import { request } from '../lib/api'; import { formatRegionLabel, parseRegionCodes } from '../lib/china-region'; +import { + clearProxyOrderDraft, + loadProxyOrderDraft, + saveProxyOrderDraft, + type ProxyOrderDraft, +} from '../lib/proxyOrderDraft'; import { toastError, toastSuccess } from '../lib/toast'; import type { PartnerProxyDeliveryMode, @@ -16,33 +22,60 @@ function fmtMoney(n: number) { return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } +function initialDraft(): ProxyOrderDraft { + return ( + loadProxyOrderDraft() ?? { + phone: '', + receiverName: '', + regionCodes: [], + addressDetail: '', + productId: '', + quantity: 2, + promoCodeId: '', + deliveryMode: 'ADDRESS', + autoReceive: false, + } + ); +} + export default function ProxyOrderPage() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); + const draft0 = useMemo(() => initialDraft(), []); const [options, setOptions] = useState(null); const [loadingOptions, setLoadingOptions] = useState(true); - const [phone, setPhone] = useState(''); - const [customerSmsCode, setCustomerSmsCode] = useState(''); - const [partnerSmsCode, setPartnerSmsCode] = useState(''); - const [receiverName, setReceiverName] = useState(''); - const [regionCodes, setRegionCodes] = useState([]); - const [addressDetail, setAddressDetail] = useState(''); - const [productId, setProductId] = useState(''); - const [quantity, setQuantity] = useState(2); - const [promoCodeId, setPromoCodeId] = useState(''); - const [deliveryMode, setDeliveryMode] = useState('ADDRESS'); - const [autoReceive, setAutoReceive] = useState(false); - const [confirmStep, setConfirmStep] = useState(false); + const [phone, setPhone] = useState(draft0.phone); + const [receiverName, setReceiverName] = useState(draft0.receiverName); + const [regionCodes, setRegionCodes] = useState(draft0.regionCodes); + const [addressDetail, setAddressDetail] = useState(draft0.addressDetail); + const [productId, setProductId] = useState(draft0.productId); + const [quantity, setQuantity] = useState(draft0.quantity); + const [promoCodeId, setPromoCodeId] = useState(draft0.promoCodeId); + const [deliveryMode, setDeliveryMode] = useState(draft0.deliveryMode); + const [autoReceive, setAutoReceive] = useState(draft0.autoReceive); const [preview, setPreview] = useState(null); const [previewLoading, setPreviewLoading] = useState(false); - const [customerCooldown, setCustomerCooldown] = useState(0); - const [partnerCooldown, setPartnerCooldown] = useState(0); const [submitting, setSubmitting] = useState(false); const [msg, setMsg] = useState(''); const region = useMemo(() => parseRegionCodes(regionCodes), [regionCodes]); const regionLabel = region ? formatRegionLabel(region) : ''; + function persistDraft(overrides?: Partial) { + saveProxyOrderDraft({ + phone, + receiverName, + regionCodes, + addressDetail, + productId, + quantity, + promoCodeId, + deliveryMode, + autoReceive, + ...overrides, + }); + } + useEffect(() => { request('PARTNER_H5', '/partner/proxy-orders/options') .then((data) => { @@ -53,6 +86,7 @@ export default function ProxyOrderPage() { const fromQuery = searchParams.get('productId'); if (fromQuery && data.products.some((p) => p.id === fromQuery)) { setProductId(fromQuery); + persistDraft({ productId: fromQuery }); } else if (data.products[0] && !productId) { setProductId(data.products[0].id); } @@ -64,9 +98,28 @@ export default function ProxyOrderPage() { useEffect(() => { const fromQuery = searchParams.get('productId'); - if (fromQuery) setProductId(fromQuery); + if (fromQuery) { + setProductId(fromQuery); + persistDraft({ productId: fromQuery }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps -- only sync productId from URL }, [searchParams]); + useEffect(() => { + persistDraft(); + // eslint-disable-next-line react-hooks/exhaustive-deps -- keep draft in sync for product-picker roundtrip + }, [ + phone, + receiverName, + regionCodes, + addressDetail, + productId, + quantity, + promoCodeId, + deliveryMode, + autoReceive, + ]); + useEffect(() => { if (!productId || quantity < 1) { setPreview(null); @@ -92,64 +145,10 @@ export default function ProxyOrderPage() { return () => clearTimeout(timer); }, [productId, quantity, deliveryMode, region?.city, region?.district]); - function startCooldown(setter: (n: number | ((s: number) => number)) => void) { - setter(60); - const timer = setInterval(() => { - setter((s) => { - if (s <= 1) { - clearInterval(timer); - return 0; - } - return s - 1; - }); - }, 1000); - } - - async function sendCustomerSms() { - setMsg(''); - if (!/^1\d{10}$/.test(phone.trim())) { - setMsg('请输入有效手机号'); - return; - } - try { - const res = await request<{ maskedPhone: string }>( - 'PARTNER_H5', - '/partner/proxy-orders/send-customer-sms', - { - method: 'POST', - body: JSON.stringify({ phone: phone.trim() }), - silent: true, - }, - ); - setMsg(`客户验证码已发送至 ${res.maskedPhone}`); - startCooldown(setCustomerCooldown); - } catch (e) { - setMsg(e instanceof Error ? e.message : '发送失败'); - } - } - - async function sendPartnerSms() { - setMsg(''); - try { - const res = await request<{ maskedPhone: string }>( - 'PARTNER_H5', - '/partner/proxy-orders/send-partner-sms', - { method: 'POST', body: '{}', silent: true }, - ); - setMsg(`确认验证码已发送至合伙人手机 ${res.maskedPhone}`); - startCooldown(setPartnerCooldown); - } catch (e) { - setMsg(e instanceof Error ? e.message : '发送失败'); - } - } - function validateForm(): string | null { if (!/^1\d{10}$/.test(phone.trim())) return '请输入有效手机号'; - if (!customerSmsCode.trim()) return '请输入客户验证码'; if (!productId) return '请选择商品'; - if (deliveryMode !== 'ADDRESS') { - // 现场提货不需要地址与门店 - } else { + if (deliveryMode === 'ADDRESS') { if (!region?.province || !region?.city || !region?.district) return '请选择省市区'; if (!addressDetail.trim()) return '请填写详细地址'; if (!autoReceive) return '配送到址须勾选同意自动收货'; @@ -158,18 +157,6 @@ export default function ProxyOrderPage() { return null; } - async function openConfirmStep() { - setMsg(''); - const err = validateForm(); - if (err) { - setMsg(err); - return; - } - setConfirmStep(true); - setPartnerSmsCode(''); - await sendPartnerSms(); - } - async function submit() { setMsg(''); const err = validateForm(); @@ -177,15 +164,9 @@ export default function ProxyOrderPage() { setMsg(err); return; } - if (!partnerSmsCode.trim()) { - setMsg('请输入合伙人确认验证码'); - return; - } const payload: PartnerProxyOrderCreateRequest = { phone: phone.trim(), - customerSmsCode: customerSmsCode.trim(), - partnerSmsCode: partnerSmsCode.trim(), deliveryMode, autoReceive: deliveryMode === 'ADDRESS' ? true : undefined, receiverName: receiverName.trim() || undefined, @@ -206,6 +187,7 @@ export default function ProxyOrderPage() { silent: true, }); toastSuccess(`代下单成功:${order.orderNo}`); + clearProxyOrderDraft(); navigate(`/orders/${order.id}`); } catch (e) { setMsg(e instanceof Error ? e.message : '下单失败'); @@ -233,34 +215,12 @@ export default function ProxyOrderPage() { <>
-
-
- setPhone(e.target.value.replace(/\D/g, '').slice(0, 11))} - /> -
- -
-
- -
-
setCustomerSmsCode(e.target.value.replace(/\D/g, '').slice(0, 6))} + placeholder="11 位手机号" + value={phone} + onChange={(e) => setPhone(e.target.value.replace(/\D/g, '').slice(0, 11))} />
@@ -270,11 +230,12 @@ export default function ProxyOrderPage() { - - - ) : null} - {msg ? (

{msg}

) : null} - {!confirmStep ? ( - - ) : ( -
- - -
- )} +

- 线下已收款确认后,将自动创建/关联用户,订单标记为代下单并直接完成,同时发放对应权益。客户订单列表会显示您的姓名。 + 确认线下已收款后提交:将自动创建/关联用户,订单标记为代下单并直接完成,同时发放对应权益。客户订单列表会显示您的姓名。

)} diff --git a/packages/shared-types/src/trade.ts b/packages/shared-types/src/trade.ts index f0dcbcd..99da032 100644 --- a/packages/shared-types/src/trade.ts +++ b/packages/shared-types/src/trade.ts @@ -96,8 +96,6 @@ export type PartnerProxyOrderPreviewResult = { export type PartnerProxyOrderCreateRequest = { phone: string; - customerSmsCode: string; - partnerSmsCode: string; deliveryMode: PartnerProxyDeliveryMode; /** 地址配送时必须为 true */ autoReceive?: boolean; @@ -112,11 +110,9 @@ export type PartnerProxyOrderCreateRequest = { promoCodeId?: string; }; -/** 总部代下单(对齐合伙人双短信流程;确认码发至 HQ 登录手机) */ +/** 总部代下单(线下完成发权益,无需短信验证) */ export type HqProxyOrderCreateRequest = { phone: string; - customerSmsCode: string; - operatorSmsCode: string; deliveryMode: PartnerProxyDeliveryMode; autoReceive?: boolean; receiverName?: string; diff --git a/server/dukang-api/src/modules/catalog/catalog.service.ts b/server/dukang-api/src/modules/catalog/catalog.service.ts index 77c0f45..2430397 100644 --- a/server/dukang-api/src/modules/catalog/catalog.service.ts +++ b/server/dukang-api/src/modules/catalog/catalog.service.ts @@ -6,7 +6,7 @@ import { groupResourcesByProductId, mapProductMedia } from './catalog.mapper'; export type CatalogViewer = { /** C 端用户手机号;无则无法看到白名单商品 */ phone?: string | null; - /** 合伙人代下单等内部场景跳过白名单 */ + /** 仅总部代下单等运营场景跳过白名单;合伙人端必须遵守白名单 */ bypassWhitelist?: boolean; }; diff --git a/server/dukang-api/src/modules/ops/admin-proxy-orders.controller.ts b/server/dukang-api/src/modules/ops/admin-proxy-orders.controller.ts index 585e98f..59ecb9e 100644 --- a/server/dukang-api/src/modules/ops/admin-proxy-orders.controller.ts +++ b/server/dukang-api/src/modules/ops/admin-proxy-orders.controller.ts @@ -10,11 +10,7 @@ import type { AuthUser } from '../../common/guards/jwt-auth.guard'; import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { TradeService } from '../trade/trade.service'; -import { - HqProxyOrderCreateDto, - HqProxyOrderPreviewDto, - HqProxyOrderSendCustomerSmsDto, -} from './dto/hq-proxy-order.dto'; +import { HqProxyOrderCreateDto, HqProxyOrderPreviewDto } from './dto/hq-proxy-order.dto'; @Controller('admin/proxy-orders') @UseGuards(HqAuthGuard, HqPermissionGuard) @@ -29,17 +25,7 @@ export class AdminProxyOrdersController { @Post('preview') preview(@Body() dto: HqProxyOrderPreviewDto) { - return this.tradeService.previewPartnerProxyOrder(dto); - } - - @Post('send-customer-sms') - sendCustomerSms(@Body() dto: HqProxyOrderSendCustomerSmsDto) { - return this.tradeService.sendHqProxyCustomerSms(dto.phone); - } - - @Post('send-operator-sms') - sendOperatorSms(@CurrentUser() user: AuthUser) { - return this.tradeService.sendHqProxyOperatorSms(user.actorId); + return this.tradeService.previewPartnerProxyOrder(dto, { bypassWhitelist: true }); } @Post() diff --git a/server/dukang-api/src/modules/ops/dto/hq-proxy-order.dto.ts b/server/dukang-api/src/modules/ops/dto/hq-proxy-order.dto.ts index b21d75b..80f4417 100644 --- a/server/dukang-api/src/modules/ops/dto/hq-proxy-order.dto.ts +++ b/server/dukang-api/src/modules/ops/dto/hq-proxy-order.dto.ts @@ -35,26 +35,11 @@ export class HqProxyOrderPreviewDto { receiverDistrict?: string; } -export class HqProxyOrderSendCustomerSmsDto { - @IsString() - @Matches(/^1\d{10}$/, { message: '请输入有效手机号' }) - phone: string; -} - export class HqProxyOrderCreateDto { @IsString() @Matches(/^1\d{10}$/, { message: '请输入有效手机号' }) phone: string; - @IsString() - @IsNotEmpty() - customerSmsCode: string; - - /** 发至当前 HQ 登录手机号的确认验证码 */ - @IsString() - @IsNotEmpty() - operatorSmsCode: string; - @IsIn(['ADDRESS', 'ON_SITE_PICKUP']) deliveryMode: 'ADDRESS' | 'ON_SITE_PICKUP'; diff --git a/server/dukang-api/src/modules/trade/dto/partner-proxy-order.dto.ts b/server/dukang-api/src/modules/trade/dto/partner-proxy-order.dto.ts index 6272e85..984c00e 100644 --- a/server/dukang-api/src/modules/trade/dto/partner-proxy-order.dto.ts +++ b/server/dukang-api/src/modules/trade/dto/partner-proxy-order.dto.ts @@ -39,25 +39,11 @@ export class PartnerProxyOrderPreviewDto { receiverDistrict?: string; } -export class PartnerProxyOrderSendCustomerSmsDto { - @IsString() - @Matches(/^1\d{10}$/, { message: '请输入有效手机号' }) - phone: string; -} - export class PartnerProxyOrderCreateDto { @IsString() @Matches(/^1\d{10}$/, { message: '请输入有效手机号' }) phone: string; - @IsString() - @IsNotEmpty() - customerSmsCode: string; - - @IsString() - @IsNotEmpty() - partnerSmsCode: string; - @IsIn(['ADDRESS', 'ON_SITE_PICKUP']) deliveryMode: 'ADDRESS' | 'ON_SITE_PICKUP'; diff --git a/server/dukang-api/src/modules/trade/trade.controller.ts b/server/dukang-api/src/modules/trade/trade.controller.ts index 7dc004b..6e724ac 100644 --- a/server/dukang-api/src/modules/trade/trade.controller.ts +++ b/server/dukang-api/src/modules/trade/trade.controller.ts @@ -9,7 +9,6 @@ import { CurrentUser } from '../../common/decorators/current-user.decorator'; import { PartnerProxyOrderCreateDto, PartnerProxyOrderPreviewDto, - PartnerProxyOrderSendCustomerSmsDto, } from './dto/partner-proxy-order.dto'; import { ManualShipOrderDto } from '../ops/dto/admin-mutate.dto'; import { CreateAfterSaleTicketDto, CreateInvoiceDto } from './dto/after-sale.dto'; @@ -210,24 +209,8 @@ export class PartnerProxyOrderController { } @Post('preview') - preview(@Body() dto: PartnerProxyOrderPreviewDto) { - return this.tradeService.previewPartnerProxyOrder(dto); - } - - /** @deprecated 兼容:转发客户短信 */ - @Post('send-sms') - sendSms(@Body() dto: PartnerProxyOrderSendCustomerSmsDto) { - return this.tradeService.sendPartnerProxyCustomerSms(dto.phone); - } - - @Post('send-customer-sms') - sendCustomerSms(@Body() dto: PartnerProxyOrderSendCustomerSmsDto) { - return this.tradeService.sendPartnerProxyCustomerSms(dto.phone); - } - - @Post('send-partner-sms') - sendPartnerSms(@CurrentUser() user: AuthUser) { - return this.tradeService.sendPartnerProxyPartnerSms(user.actorId); + preview(@CurrentUser() user: AuthUser, @Body() dto: PartnerProxyOrderPreviewDto) { + return this.tradeService.previewPartnerProxyOrderForPartner(user.actorId, dto); } @Post() diff --git a/server/dukang-api/src/modules/trade/trade.service.ts b/server/dukang-api/src/modules/trade/trade.service.ts index 7647ee4..bd3efb4 100644 --- a/server/dukang-api/src/modules/trade/trade.service.ts +++ b/server/dukang-api/src/modules/trade/trade.service.ts @@ -12,7 +12,7 @@ import { orderTabToStatuses, validateMinPurchase, } from '@dukang/domain'; -import { loadAppConfig, ClientApp, SmsScene, WECHAT_AUTH_REQUIRED } from '@dukang/shared-types'; +import { loadAppConfig, ClientApp, WECHAT_AUTH_REQUIRED } from '@dukang/shared-types'; import { PrismaService } from '../../common/prisma/prisma.module'; import { serializeBigInt } from '../../common/decorators/current-user.decorator'; import { AnalyticsService } from '../analytics/analytics.service'; @@ -1050,7 +1050,8 @@ export class TradeService { async getPartnerProxyOrderOptions(partnerAccountId: bigint) { const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId); const [products, promoCodes, stores] = await Promise.all([ - this.catalogService.listProducts(undefined, undefined, { bypassWhitelist: true }), + // 遵守商品白名单:未开放的测试酒不对合伙人可见 + this.catalogService.listProducts(undefined, undefined, { phone: primary.phone }), this.promoCodeService.listActiveOptions(), this.prisma.store.findMany({ where: { partnerAccountId: primary.id }, @@ -1090,17 +1091,18 @@ export class TradeService { }); } - async previewPartnerProxyOrder(body: { - productId: string; - quantity: number; - deliveryMode?: 'ADDRESS' | 'ON_SITE_PICKUP'; - storeId?: string; - receiverCity?: string; - receiverDistrict?: string; - }) { - const product = await this.catalogService.getProduct(BigInt(body.productId), { - bypassWhitelist: true, - }); + async previewPartnerProxyOrder( + body: { + productId: string; + quantity: number; + deliveryMode?: 'ADDRESS' | 'ON_SITE_PICKUP'; + storeId?: string; + receiverCity?: string; + receiverDistrict?: string; + }, + viewer?: { phone?: string | null; bypassWhitelist?: boolean }, + ) { + const product = await this.catalogService.getProduct(BigInt(body.productId), viewer ?? {}); if (!product || product.status !== 'ON_SALE') { throw new BadRequestException('商品不可购买'); } @@ -1146,45 +1148,26 @@ export class TradeService { }; } - async sendPartnerProxyCustomerSms(phone: string) { - const normalizedPhone = phone.trim(); - await this.authService.sendSms(normalizedPhone, SmsScene.PARTNER_PROXY_CUSTOMER, { - clientApp: ClientApp.PARTNER_H5, - }); - const masked = - normalizedPhone.length >= 7 - ? `${normalizedPhone.slice(0, 3)}****${normalizedPhone.slice(-4)}` - : normalizedPhone; - return { ok: true, maskedPhone: masked }; - } - - /** @deprecated 兼容旧前端:转发为客户短信 */ - async sendPartnerProxyOrderSms(phone: string) { - return this.sendPartnerProxyCustomerSms(phone); - } - - async sendPartnerProxyPartnerSms(partnerAccountId: bigint) { + /** 合伙人代下单预览:按合伙人手机号遵守商品白名单 */ + async previewPartnerProxyOrderForPartner( + partnerAccountId: bigint, + body: { + productId: string; + quantity: number; + deliveryMode?: 'ADDRESS' | 'ON_SITE_PICKUP'; + storeId?: string; + receiverCity?: string; + receiverDistrict?: string; + }, + ) { const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId); - const partnerPhone = primary.phone?.trim(); - if (!partnerPhone || !/^1\d{10}$/.test(partnerPhone)) { - throw new BadRequestException('合伙人手机号无效,无法发送确认验证码'); - } - await this.authService.sendSms(partnerPhone, SmsScene.PARTNER_PROXY_ORDER, { - clientApp: ClientApp.PARTNER_H5, - }); - const masked = - partnerPhone.length >= 7 - ? `${partnerPhone.slice(0, 3)}****${partnerPhone.slice(-4)}` - : partnerPhone; - return { ok: true, maskedPhone: masked }; + return this.previewPartnerProxyOrder(body, { phone: primary.phone }); } async createPartnerProxyOrder( partnerAccountId: bigint, body: { phone: string; - customerSmsCode: string; - partnerSmsCode: string; deliveryMode: 'ADDRESS' | 'ON_SITE_PICKUP'; autoReceive?: boolean; storeId?: string; @@ -1200,23 +1183,15 @@ export class TradeService { req: Request, ) { const normalizedPhone = body.phone.trim(); + if (!/^1\d{10}$/.test(normalizedPhone)) { + throw new BadRequestException('请输入有效手机号'); + } const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId); const partnerPhone = primary.phone?.trim(); if (!partnerPhone || !/^1\d{10}$/.test(partnerPhone)) { throw new BadRequestException('合伙人手机号无效'); } - await this.authService.verifySmsCode( - normalizedPhone, - body.customerSmsCode.trim(), - SmsScene.PARTNER_PROXY_CUSTOMER, - ); - await this.authService.verifySmsCode( - partnerPhone, - body.partnerSmsCode.trim(), - SmsScene.PARTNER_PROXY_ORDER, - ); - if (body.deliveryMode === 'ADDRESS' && body.autoReceive !== true) { throw new BadRequestException('配送到址须勾选同意自动收货'); } @@ -1232,14 +1207,17 @@ export class TradeService { sourceLabel: `代下单·${maskedPartnerPhone}`, }); - const preview = await this.previewPartnerProxyOrder({ - productId: body.productId, - quantity: body.quantity, - deliveryMode: body.deliveryMode, - storeId: body.storeId, - receiverCity: body.city, - receiverDistrict: body.district, - }); + const preview = await this.previewPartnerProxyOrder( + { + productId: body.productId, + quantity: body.quantity, + deliveryMode: body.deliveryMode, + storeId: body.storeId, + receiverCity: body.city, + receiverDistrict: body.district, + }, + { phone: primary.phone }, + ); const product = await this.prisma.commonProductItem.findUniqueOrThrow({ where: { id: BigInt(body.productId) }, @@ -1384,7 +1362,7 @@ export class TradeService { return this.getPartnerOrder(partnerAccountId, order.id); } - /** HQ 代下单:商品/推广码选项(不绑定合伙人门店) */ + /** HQ 代下单:商品/推广码选项(运营侧可看白名单测试酒) */ async getHqProxyOrderOptions() { const [products, promoCodes] = await Promise.all([ this.catalogService.listProducts(undefined, undefined, { bypassWhitelist: true }), @@ -1405,43 +1383,10 @@ export class TradeService { }); } - async sendHqProxyCustomerSms(phone: string) { - const normalizedPhone = phone.trim(); - await this.authService.sendSms(normalizedPhone, SmsScene.PARTNER_PROXY_CUSTOMER, { - clientApp: ClientApp.HQ_WEB, - }); - const masked = - normalizedPhone.length >= 7 - ? `${normalizedPhone.slice(0, 3)}****${normalizedPhone.slice(-4)}` - : normalizedPhone; - return { ok: true, maskedPhone: masked }; - } - - async sendHqProxyOperatorSms(hqAccountId: bigint) { - const hq = await this.prisma.hqAccount.findUnique({ where: { id: hqAccountId } }); - if (!hq || hq.status !== 'ACTIVE') { - throw new BadRequestException('总部账号无效'); - } - const operatorPhone = hq.phone?.trim(); - if (!operatorPhone || !/^1\d{10}$/.test(operatorPhone)) { - throw new BadRequestException('总部账号手机号无效,无法发送确认验证码'); - } - await this.authService.sendSms(operatorPhone, SmsScene.PARTNER_PROXY_ORDER, { - clientApp: ClientApp.HQ_WEB, - }); - const masked = - operatorPhone.length >= 7 - ? `${operatorPhone.slice(0, 3)}****${operatorPhone.slice(-4)}` - : operatorPhone; - return { ok: true, maskedPhone: masked }; - } - async createHqProxyOrder( hqAccountId: bigint, body: { phone: string; - customerSmsCode: string; - operatorSmsCode: string; deliveryMode: 'ADDRESS' | 'ON_SITE_PICKUP'; autoReceive?: boolean; receiverName?: string; @@ -1456,6 +1401,9 @@ export class TradeService { req: Request, ) { const normalizedPhone = body.phone.trim(); + if (!/^1\d{10}$/.test(normalizedPhone)) { + throw new BadRequestException('请输入有效手机号'); + } const hq = await this.prisma.hqAccount.findUnique({ where: { id: hqAccountId } }); if (!hq || hq.status !== 'ACTIVE') { throw new BadRequestException('总部账号无效'); @@ -1465,17 +1413,6 @@ export class TradeService { throw new BadRequestException('总部账号手机号无效'); } - await this.authService.verifySmsCode( - normalizedPhone, - body.customerSmsCode.trim(), - SmsScene.PARTNER_PROXY_CUSTOMER, - ); - await this.authService.verifySmsCode( - operatorPhone, - body.operatorSmsCode.trim(), - SmsScene.PARTNER_PROXY_ORDER, - ); - if (body.deliveryMode === 'ADDRESS' && body.autoReceive !== true) { throw new BadRequestException('配送到址须勾选同意自动收货'); } @@ -1492,13 +1429,16 @@ export class TradeService { sourceLabel: `总部代下单·${maskedOperatorPhone}`, }); - const preview = await this.previewPartnerProxyOrder({ - productId: body.productId, - quantity: body.quantity, - deliveryMode: body.deliveryMode, - receiverCity: body.city, - receiverDistrict: body.district, - }); + const preview = await this.previewPartnerProxyOrder( + { + productId: body.productId, + quantity: body.quantity, + deliveryMode: body.deliveryMode, + receiverCity: body.city, + receiverDistrict: body.district, + }, + { bypassWhitelist: true }, + ); const product = await this.prisma.commonProductItem.findUniqueOrThrow({ where: { id: BigInt(body.productId) },