fix(trade): simplify proxy order and respect product whitelist

Drop SMS verification for HQ/partner proxy orders; keep form draft across product picker; hide whitelist-only products from partner catalog.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-31 09:43:16 +08:00
parent 2cd4e25682
commit 51f2b5fbfa
10 changed files with 311 additions and 592 deletions
+106 -231
View File
@@ -37,8 +37,6 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder
const [options, setOptions] = useState<PartnerProxyOrderOptions | null>(null); const [options, setOptions] = useState<PartnerProxyOrderOptions | null>(null);
const [loadingOptions, setLoadingOptions] = useState(false); const [loadingOptions, setLoadingOptions] = useState(false);
const [phone, setPhone] = useState(''); const [phone, setPhone] = useState('');
const [customerSmsCode, setCustomerSmsCode] = useState('');
const [operatorSmsCode, setOperatorSmsCode] = useState('');
const [receiverName, setReceiverName] = useState(''); const [receiverName, setReceiverName] = useState('');
const [regionCodes, setRegionCodes] = useState<string[]>([]); const [regionCodes, setRegionCodes] = useState<string[]>([]);
const [addressDetail, setAddressDetail] = useState(''); const [addressDetail, setAddressDetail] = useState('');
@@ -47,11 +45,8 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder
const [promoCodeId, setPromoCodeId] = useState<string>(); const [promoCodeId, setPromoCodeId] = useState<string>();
const [deliveryMode, setDeliveryMode] = useState<PartnerProxyDeliveryMode>('ADDRESS'); const [deliveryMode, setDeliveryMode] = useState<PartnerProxyDeliveryMode>('ADDRESS');
const [autoReceive, setAutoReceive] = useState(false); const [autoReceive, setAutoReceive] = useState(false);
const [confirmStep, setConfirmStep] = useState(false);
const [preview, setPreview] = useState<PartnerProxyOrderPreviewResult | null>(null); const [preview, setPreview] = useState<PartnerProxyOrderPreviewResult | null>(null);
const [previewLoading, setPreviewLoading] = useState(false); const [previewLoading, setPreviewLoading] = useState(false);
const [customerCooldown, setCustomerCooldown] = useState(0);
const [operatorCooldown, setOperatorCooldown] = useState(0);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const region = useMemo(() => parseRegionCodes(regionCodes), [regionCodes]); const region = useMemo(() => parseRegionCodes(regionCodes), [regionCodes]);
@@ -94,8 +89,6 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder
function resetForm() { function resetForm() {
setPhone(''); setPhone('');
setCustomerSmsCode('');
setOperatorSmsCode('');
setReceiverName(''); setReceiverName('');
setRegionCodes([]); setRegionCodes([]);
setAddressDetail(''); setAddressDetail('');
@@ -103,10 +96,7 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder
setPromoCodeId(undefined); setPromoCodeId(undefined);
setDeliveryMode('ADDRESS'); setDeliveryMode('ADDRESS');
setAutoReceive(false); setAutoReceive(false);
setConfirmStep(false);
setPreview(null); setPreview(null);
setCustomerCooldown(0);
setOperatorCooldown(0);
setProductId(options?.products[0]?.id); setProductId(options?.products[0]?.id);
} }
@@ -115,52 +105,8 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder
onClose(); 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 { function validateForm(): string | null {
if (!/^1\d{10}$/.test(phone.trim())) return '请输入有效手机号'; if (!/^1\d{10}$/.test(phone.trim())) return '请输入有效手机号';
if (!customerSmsCode.trim()) return '请输入客户验证码';
if (!productId) return '请选择商品'; if (!productId) return '请选择商品';
if (deliveryMode === 'ADDRESS') { if (deliveryMode === 'ADDRESS') {
if (!region?.province || !region?.city || !region?.district) return '请选择省市区'; if (!region?.province || !region?.city || !region?.district) return '请选择省市区';
@@ -174,32 +120,15 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder
return null; return null;
} }
async function openConfirmStep() {
const err = validateForm();
if (err) {
message.warning(err);
return;
}
setConfirmStep(true);
setOperatorSmsCode('');
await sendOperatorSms();
}
async function submit() { async function submit() {
const err = validateForm(); const err = validateForm();
if (err) { if (err) {
message.warning(err); message.warning(err);
return; return;
} }
if (!operatorSmsCode.trim()) {
message.warning('请输入总部确认验证码');
return;
}
const payload: HqProxyOrderCreateRequest = { const payload: HqProxyOrderCreateRequest = {
phone: phone.trim(), phone: phone.trim(),
customerSmsCode: customerSmsCode.trim(),
operatorSmsCode: operatorSmsCode.trim(),
deliveryMode, deliveryMode,
autoReceive: deliveryMode === 'ADDRESS' ? true : undefined, autoReceive: deliveryMode === 'ADDRESS' ? true : undefined,
receiverName: receiverName.trim() || undefined, receiverName: receiverName.trim() || undefined,
@@ -243,186 +172,132 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder
width={640} width={640}
destroyOnClose destroyOnClose
footer={ footer={
confirmStep ? ( <Space>
<Space> <Button onClick={handleClose}></Button>
<Button <Button type="primary" loading={submitting || loadingOptions} onClick={() => void submit()}>
onClick={() => {
setConfirmStep(false); </Button>
setOperatorSmsCode(''); </Space>
}}
>
</Button>
<Button type="primary" loading={submitting} onClick={() => void submit()}>
</Button>
</Space>
) : (
<Space>
<Button onClick={handleClose}></Button>
<Button type="primary" loading={loadingOptions} onClick={() => void openConfirmStep()}>
</Button>
</Space>
)
} }
> >
<Alert <Alert
type="info" type="info"
showIcon showIcon
style={{ marginBottom: 16 }} style={{ marginBottom: 16 }}
message="线下已收款:提交后订单直接完成并发放好客权益(客户验证码 + 总部确认码)" message="线下已收款:提交后订单直接完成并发放好客权益"
/> />
{!confirmStep ? ( <Form layout="vertical">
<Form layout="vertical"> <Form.Item label="用户手机号" required>
<Form.Item label="用户手机号" required> <Input
<Space.Compact style={{ width: '100%' }}> placeholder="11 位手机号"
<Input value={phone}
placeholder="11 位手机号" maxLength={11}
value={phone} onChange={(e) => setPhone(e.target.value.replace(/\D/g, '').slice(0, 11))}
maxLength={11} />
onChange={(e) => setPhone(e.target.value.replace(/\D/g, '').slice(0, 11))} </Form.Item>
/>
<Button disabled={customerCooldown > 0} onClick={() => void sendCustomerSms()}>
{customerCooldown > 0 ? `${customerCooldown}s` : '获取验证码'}
</Button>
</Space.Compact>
</Form.Item>
<Form.Item label="客户验证码" required> <Form.Item label="酒品" required>
<Input <Select
placeholder="发至客户手机" showSearch
value={customerSmsCode} optionFilterProp="label"
maxLength={6} loading={loadingOptions}
onChange={(e) => setCustomerSmsCode(e.target.value.replace(/\D/g, '').slice(0, 6))} placeholder="选择商品"
/> value={productId}
</Form.Item> onChange={setProductId}
options={(options?.products ?? []).map((p) => ({
value: p.id,
label: `${p.name} · ${p.spec} · ¥${fmtMoney(p.price)}`,
}))}
/>
</Form.Item>
<Form.Item label="酒品" required> <Form.Item label="数量" required>
<InputNumber
min={1}
value={quantity}
onChange={(v) => setQuantity(Math.max(1, Number(v) || 1))}
style={{ width: '100%' }}
/>
</Form.Item>
{(options?.promoCodes.length ?? 0) > 0 ? (
<Form.Item label="绑定推广码(选填)">
<Select <Select
showSearch allowClear
optionFilterProp="label" placeholder="不绑定"
loading={loadingOptions} value={promoCodeId}
placeholder="选择商品" onChange={setPromoCodeId}
value={productId} options={(options?.promoCodes ?? []).map((p) => ({
onChange={setProductId}
options={(options?.products ?? []).map((p) => ({
value: p.id, value: p.id,
label: `${p.name} · ${p.spec} · ¥${fmtMoney(p.price)}`, label: `${p.code} · ${p.name}`,
}))} }))}
/> />
</Form.Item> </Form.Item>
) : null}
<Form.Item label="数量" required> <Form.Item label="履约方式" required>
<InputNumber min={1} value={quantity} onChange={(v) => setQuantity(Math.max(1, Number(v) || 1))} style={{ width: '100%' }} /> <Radio.Group
</Form.Item> value={deliveryMode}
onChange={(e) => {
setDeliveryMode(e.target.value);
if (e.target.value !== 'ADDRESS') setAutoReceive(false);
}}
options={[
{ value: 'ADDRESS', label: '配送到址' },
{ value: 'ON_SITE_PICKUP', label: '现场提货' },
]}
/>
</Form.Item>
{(options?.promoCodes.length ?? 0) > 0 ? ( {deliveryMode === 'ADDRESS' ? (
<Form.Item label="绑定推广码(选填)"> <>
<Select <Form.Item label="收货人(选填)">
allowClear <Input
placeholder="不绑定" placeholder="默认:用户+手机尾号"
value={promoCodeId} value={receiverName}
onChange={setPromoCodeId} maxLength={32}
options={(options?.promoCodes ?? []).map((p) => ({ onChange={(e) => setReceiverName(e.target.value)}
value: p.id,
label: `${p.code} · ${p.name}`,
}))}
/> />
</Form.Item> </Form.Item>
) : null} <Form.Item label="省市区" required>
<ChinaRegionCascader value={regionCodes} onChange={setRegionCodes} />
<Form.Item label="履约方式" required> </Form.Item>
<Radio.Group <Form.Item label="详细地址" required>
value={deliveryMode} <Input.TextArea
onChange={(e) => { rows={2}
setDeliveryMode(e.target.value); placeholder="街道门牌等"
if (e.target.value !== 'ADDRESS') setAutoReceive(false); value={addressDetail}
}} maxLength={256}
options={[ onChange={(e) => setAddressDetail(e.target.value)}
{ value: 'ADDRESS', label: '配送到址' },
{ value: 'ON_SITE_PICKUP', label: '现场提货' },
]}
/>
</Form.Item>
{deliveryMode === 'ADDRESS' ? (
<>
<Form.Item label="收货人(选填)">
<Input
placeholder="默认:用户+手机尾号"
value={receiverName}
maxLength={32}
onChange={(e) => setReceiverName(e.target.value)}
/>
</Form.Item>
<Form.Item label="省市区" required>
<ChinaRegionCascader value={regionCodes} onChange={setRegionCodes} />
</Form.Item>
<Form.Item label="详细地址" required>
<Input.TextArea
rows={2}
placeholder="街道门牌等"
value={addressDetail}
maxLength={256}
onChange={(e) => setAddressDetail(e.target.value)}
/>
</Form.Item>
<Form.Item>
<Checkbox checked={autoReceive} onChange={(e) => setAutoReceive(e.target.checked)}>
</Checkbox>
</Form.Item>
</>
) : null}
<div style={{ background: '#fafafa', borderRadius: 8, padding: 12 }}>
<Typography.Text type="secondary"></Typography.Text>
{previewLoading ? (
<div></div>
) : preview ? (
<Space direction="vertical" size={2} style={{ width: '100%', marginTop: 8 }}>
<div>{deliveryLabel}</div>
<div>¥{fmtMoney(preview.productAmount)}</div>
<div>¥{fmtMoney(preview.payAmount)}</div>
<div>¥{fmtMoney(preview.benefitAmount)}</div>
</Space>
) : (
<div style={{ marginTop: 8 }}>
<Typography.Text type="secondary"></Typography.Text>
</div>
)}
</div>
</Form>
) : (
<Form layout="vertical">
<Alert
type="warning"
showIcon
style={{ marginBottom: 16 }}
message={`即将为 ${phone} 创建已完成订单并发放权益,请输入发至总部账号手机的确认验证码`}
/>
<Form.Item label="总部确认验证码" required>
<Space.Compact style={{ width: '100%' }}>
<Input
placeholder="发至当前登录 HQ 手机"
value={operatorSmsCode}
maxLength={6}
onChange={(e) => setOperatorSmsCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
/> />
<Button disabled={operatorCooldown > 0} onClick={() => void sendOperatorSms()}> </Form.Item>
{operatorCooldown > 0 ? `${operatorCooldown}s` : '重新发送'} <Form.Item>
</Button> <Checkbox checked={autoReceive} onChange={(e) => setAutoReceive(e.target.checked)}>
</Space.Compact>
</Form.Item> </Checkbox>
{preview ? ( </Form.Item>
<Typography.Paragraph type="secondary" style={{ marginBottom: 0 }}> </>
{deliveryLabel} · ¥{fmtMoney(preview.payAmount)} · ¥{fmtMoney(preview.benefitAmount)} ) : null}
</Typography.Paragraph>
) : null} <div style={{ background: '#fafafa', borderRadius: 8, padding: 12 }}>
</Form> <Typography.Text type="secondary"></Typography.Text>
)} {previewLoading ? (
<div></div>
) : preview ? (
<Space direction="vertical" size={2} style={{ width: '100%', marginTop: 8 }}>
<div>{deliveryLabel}</div>
<div>¥{fmtMoney(preview.productAmount)}</div>
<div>¥{fmtMoney(preview.payAmount)}</div>
<div>¥{fmtMoney(preview.benefitAmount)}</div>
</Space>
) : (
<div style={{ marginTop: 8 }}>
<Typography.Text type="secondary"></Typography.Text>
</div>
)}
</div>
</Form>
</Modal> </Modal>
); );
} }
@@ -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<ProxyOrderDraft>;
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 */
}
}
+87 -175
View File
@@ -4,6 +4,12 @@ import PageHeader from '@dukang/shared-ui/PageHeader';
import ChinaRegionPicker from '../components/ChinaRegionPicker'; import ChinaRegionPicker from '../components/ChinaRegionPicker';
import { request } from '../lib/api'; import { request } from '../lib/api';
import { formatRegionLabel, parseRegionCodes } from '../lib/china-region'; import { formatRegionLabel, parseRegionCodes } from '../lib/china-region';
import {
clearProxyOrderDraft,
loadProxyOrderDraft,
saveProxyOrderDraft,
type ProxyOrderDraft,
} from '../lib/proxyOrderDraft';
import { toastError, toastSuccess } from '../lib/toast'; import { toastError, toastSuccess } from '../lib/toast';
import type { import type {
PartnerProxyDeliveryMode, PartnerProxyDeliveryMode,
@@ -16,33 +22,60 @@ function fmtMoney(n: number) {
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); 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() { export default function ProxyOrderPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const draft0 = useMemo(() => initialDraft(), []);
const [options, setOptions] = useState<PartnerProxyOrderOptions | null>(null); const [options, setOptions] = useState<PartnerProxyOrderOptions | null>(null);
const [loadingOptions, setLoadingOptions] = useState(true); const [loadingOptions, setLoadingOptions] = useState(true);
const [phone, setPhone] = useState(''); const [phone, setPhone] = useState(draft0.phone);
const [customerSmsCode, setCustomerSmsCode] = useState(''); const [receiverName, setReceiverName] = useState(draft0.receiverName);
const [partnerSmsCode, setPartnerSmsCode] = useState(''); const [regionCodes, setRegionCodes] = useState<string[]>(draft0.regionCodes);
const [receiverName, setReceiverName] = useState(''); const [addressDetail, setAddressDetail] = useState(draft0.addressDetail);
const [regionCodes, setRegionCodes] = useState<string[]>([]); const [productId, setProductId] = useState(draft0.productId);
const [addressDetail, setAddressDetail] = useState(''); const [quantity, setQuantity] = useState(draft0.quantity);
const [productId, setProductId] = useState(''); const [promoCodeId, setPromoCodeId] = useState(draft0.promoCodeId);
const [quantity, setQuantity] = useState(2); const [deliveryMode, setDeliveryMode] = useState<PartnerProxyDeliveryMode>(draft0.deliveryMode);
const [promoCodeId, setPromoCodeId] = useState(''); const [autoReceive, setAutoReceive] = useState(draft0.autoReceive);
const [deliveryMode, setDeliveryMode] = useState<PartnerProxyDeliveryMode>('ADDRESS');
const [autoReceive, setAutoReceive] = useState(false);
const [confirmStep, setConfirmStep] = useState(false);
const [preview, setPreview] = useState<PartnerProxyOrderPreviewResult | null>(null); const [preview, setPreview] = useState<PartnerProxyOrderPreviewResult | null>(null);
const [previewLoading, setPreviewLoading] = useState(false); const [previewLoading, setPreviewLoading] = useState(false);
const [customerCooldown, setCustomerCooldown] = useState(0);
const [partnerCooldown, setPartnerCooldown] = useState(0);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [msg, setMsg] = useState(''); const [msg, setMsg] = useState('');
const region = useMemo(() => parseRegionCodes(regionCodes), [regionCodes]); const region = useMemo(() => parseRegionCodes(regionCodes), [regionCodes]);
const regionLabel = region ? formatRegionLabel(region) : ''; const regionLabel = region ? formatRegionLabel(region) : '';
function persistDraft(overrides?: Partial<ProxyOrderDraft>) {
saveProxyOrderDraft({
phone,
receiverName,
regionCodes,
addressDetail,
productId,
quantity,
promoCodeId,
deliveryMode,
autoReceive,
...overrides,
});
}
useEffect(() => { useEffect(() => {
request<PartnerProxyOrderOptions>('PARTNER_H5', '/partner/proxy-orders/options') request<PartnerProxyOrderOptions>('PARTNER_H5', '/partner/proxy-orders/options')
.then((data) => { .then((data) => {
@@ -53,6 +86,7 @@ export default function ProxyOrderPage() {
const fromQuery = searchParams.get('productId'); const fromQuery = searchParams.get('productId');
if (fromQuery && data.products.some((p) => p.id === fromQuery)) { if (fromQuery && data.products.some((p) => p.id === fromQuery)) {
setProductId(fromQuery); setProductId(fromQuery);
persistDraft({ productId: fromQuery });
} else if (data.products[0] && !productId) { } else if (data.products[0] && !productId) {
setProductId(data.products[0].id); setProductId(data.products[0].id);
} }
@@ -64,9 +98,28 @@ export default function ProxyOrderPage() {
useEffect(() => { useEffect(() => {
const fromQuery = searchParams.get('productId'); 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]); }, [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(() => { useEffect(() => {
if (!productId || quantity < 1) { if (!productId || quantity < 1) {
setPreview(null); setPreview(null);
@@ -92,64 +145,10 @@ export default function ProxyOrderPage() {
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [productId, quantity, deliveryMode, region?.city, region?.district]); }, [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 { function validateForm(): string | null {
if (!/^1\d{10}$/.test(phone.trim())) return '请输入有效手机号'; if (!/^1\d{10}$/.test(phone.trim())) return '请输入有效手机号';
if (!customerSmsCode.trim()) return '请输入客户验证码';
if (!productId) return '请选择商品'; if (!productId) return '请选择商品';
if (deliveryMode !== 'ADDRESS') { if (deliveryMode === 'ADDRESS') {
// 现场提货不需要地址与门店
} else {
if (!region?.province || !region?.city || !region?.district) return '请选择省市区'; if (!region?.province || !region?.city || !region?.district) return '请选择省市区';
if (!addressDetail.trim()) return '请填写详细地址'; if (!addressDetail.trim()) return '请填写详细地址';
if (!autoReceive) return '配送到址须勾选同意自动收货'; if (!autoReceive) return '配送到址须勾选同意自动收货';
@@ -158,18 +157,6 @@ export default function ProxyOrderPage() {
return null; return null;
} }
async function openConfirmStep() {
setMsg('');
const err = validateForm();
if (err) {
setMsg(err);
return;
}
setConfirmStep(true);
setPartnerSmsCode('');
await sendPartnerSms();
}
async function submit() { async function submit() {
setMsg(''); setMsg('');
const err = validateForm(); const err = validateForm();
@@ -177,15 +164,9 @@ export default function ProxyOrderPage() {
setMsg(err); setMsg(err);
return; return;
} }
if (!partnerSmsCode.trim()) {
setMsg('请输入合伙人确认验证码');
return;
}
const payload: PartnerProxyOrderCreateRequest = { const payload: PartnerProxyOrderCreateRequest = {
phone: phone.trim(), phone: phone.trim(),
customerSmsCode: customerSmsCode.trim(),
partnerSmsCode: partnerSmsCode.trim(),
deliveryMode, deliveryMode,
autoReceive: deliveryMode === 'ADDRESS' ? true : undefined, autoReceive: deliveryMode === 'ADDRESS' ? true : undefined,
receiverName: receiverName.trim() || undefined, receiverName: receiverName.trim() || undefined,
@@ -206,6 +187,7 @@ export default function ProxyOrderPage() {
silent: true, silent: true,
}); });
toastSuccess(`代下单成功:${order.orderNo}`); toastSuccess(`代下单成功:${order.orderNo}`);
clearProxyOrderDraft();
navigate(`/orders/${order.id}`); navigate(`/orders/${order.id}`);
} catch (e) { } catch (e) {
setMsg(e instanceof Error ? e.message : '下单失败'); setMsg(e instanceof Error ? e.message : '下单失败');
@@ -233,34 +215,12 @@ export default function ProxyOrderPage() {
<> <>
<section className="partner-form-section"> <section className="partner-form-section">
<label className="partner-form-label"></label> <label className="partner-form-label"></label>
<div className="partner-input-row">
<div className="partner-input-wrap" style={{ flex: 1 }}>
<input
className="partner-input"
placeholder="11 位手机号"
value={phone}
onChange={(e) => setPhone(e.target.value.replace(/\D/g, '').slice(0, 11))}
/>
</div>
<button
type="button"
className="btn btn-secondary"
disabled={customerCooldown > 0}
onClick={() => void sendCustomerSms()}
>
{customerCooldown > 0 ? `${customerCooldown}s` : '获取验证码'}
</button>
</div>
</section>
<section className="partner-form-section">
<label className="partner-form-label"></label>
<div className="partner-input-wrap"> <div className="partner-input-wrap">
<input <input
className="partner-input" className="partner-input"
placeholder="发至客户手机" placeholder="11 位手机"
value={customerSmsCode} value={phone}
onChange={(e) => setCustomerSmsCode(e.target.value.replace(/\D/g, '').slice(0, 6))} onChange={(e) => setPhone(e.target.value.replace(/\D/g, '').slice(0, 11))}
/> />
</div> </div>
</section> </section>
@@ -270,11 +230,12 @@ export default function ProxyOrderPage() {
<button <button
type="button" type="button"
className="partner-proxy-product-picker" className="partner-proxy-product-picker"
onClick={() => onClick={() => {
persistDraft();
navigate( navigate(
`/proxy-order/products${productId ? `?selected=${encodeURIComponent(productId)}` : ''}`, `/proxy-order/products${productId ? `?selected=${encodeURIComponent(productId)}` : ''}`,
) );
} }}
> >
{selectedProduct ? ( {selectedProduct ? (
<> <>
@@ -428,73 +389,24 @@ export default function ProxyOrderPage() {
)} )}
</section> </section>
{confirmStep ? (
<section className="partner-form-section partner-proxy-confirm">
<label className="partner-form-label"></label>
<p className="label-md text-muted" style={{ marginBottom: 8 }}>
线
</p>
<div className="partner-input-row">
<div className="partner-input-wrap" style={{ flex: 1 }}>
<input
className="partner-input"
placeholder="发至合伙人手机"
value={partnerSmsCode}
onChange={(e) => setPartnerSmsCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
/>
</div>
<button
type="button"
className="btn btn-secondary"
disabled={partnerCooldown > 0}
onClick={() => void sendPartnerSms()}
>
{partnerCooldown > 0 ? `${partnerCooldown}s` : '重新发送'}
</button>
</div>
</section>
) : null}
{msg ? ( {msg ? (
<p className="partner-form-error" role="alert"> <p className="partner-form-error" role="alert">
{msg} {msg}
</p> </p>
) : null} ) : null}
{!confirmStep ? ( <button
<button type="button"
type="button" className="btn btn-primary btn-block"
className="btn btn-primary btn-block" disabled={submitting || !preview}
disabled={!preview} onClick={() => void submit()}
onClick={() => void openConfirmStep()} style={{ marginTop: 16 }}
style={{ marginTop: 16 }} >
> {submitting ? '提交中…' : '确认代下单'}
</button>
</button>
) : (
<div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
<button
type="button"
className="btn btn-secondary"
style={{ flex: 1 }}
onClick={() => setConfirmStep(false)}
>
</button>
<button
type="button"
className="btn btn-primary"
style={{ flex: 1 }}
disabled={submitting || !preview}
onClick={() => void submit()}
>
{submitting ? '提交中…' : '验证并提交'}
</button>
</div>
)}
<p className="label-md text-muted" style={{ marginTop: 12, lineHeight: 1.5 }}> <p className="label-md text-muted" style={{ marginTop: 12, lineHeight: 1.5 }}>
线/ 线/
</p> </p>
</> </>
)} )}
+1 -5
View File
@@ -96,8 +96,6 @@ export type PartnerProxyOrderPreviewResult = {
export type PartnerProxyOrderCreateRequest = { export type PartnerProxyOrderCreateRequest = {
phone: string; phone: string;
customerSmsCode: string;
partnerSmsCode: string;
deliveryMode: PartnerProxyDeliveryMode; deliveryMode: PartnerProxyDeliveryMode;
/** 地址配送时必须为 true */ /** 地址配送时必须为 true */
autoReceive?: boolean; autoReceive?: boolean;
@@ -112,11 +110,9 @@ export type PartnerProxyOrderCreateRequest = {
promoCodeId?: string; promoCodeId?: string;
}; };
/** 总部代下单(对齐合伙人双短信流程;确认码发至 HQ 登录手机 */ /** 总部代下单(线下完成发权益,无需短信验证 */
export type HqProxyOrderCreateRequest = { export type HqProxyOrderCreateRequest = {
phone: string; phone: string;
customerSmsCode: string;
operatorSmsCode: string;
deliveryMode: PartnerProxyDeliveryMode; deliveryMode: PartnerProxyDeliveryMode;
autoReceive?: boolean; autoReceive?: boolean;
receiverName?: string; receiverName?: string;
@@ -6,7 +6,7 @@ import { groupResourcesByProductId, mapProductMedia } from './catalog.mapper';
export type CatalogViewer = { export type CatalogViewer = {
/** C 端用户手机号;无则无法看到白名单商品 */ /** C 端用户手机号;无则无法看到白名单商品 */
phone?: string | null; phone?: string | null;
/** 合伙人代下单等内部场景跳过白名单 */ /** 仅总部代下单等运营场景跳过白名单;合伙人端必须遵守白名单 */
bypassWhitelist?: boolean; bypassWhitelist?: boolean;
}; };
@@ -10,11 +10,7 @@ import type { AuthUser } from '../../common/guards/jwt-auth.guard';
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
import { TradeService } from '../trade/trade.service'; import { TradeService } from '../trade/trade.service';
import { import { HqProxyOrderCreateDto, HqProxyOrderPreviewDto } from './dto/hq-proxy-order.dto';
HqProxyOrderCreateDto,
HqProxyOrderPreviewDto,
HqProxyOrderSendCustomerSmsDto,
} from './dto/hq-proxy-order.dto';
@Controller('admin/proxy-orders') @Controller('admin/proxy-orders')
@UseGuards(HqAuthGuard, HqPermissionGuard) @UseGuards(HqAuthGuard, HqPermissionGuard)
@@ -29,17 +25,7 @@ export class AdminProxyOrdersController {
@Post('preview') @Post('preview')
preview(@Body() dto: HqProxyOrderPreviewDto) { preview(@Body() dto: HqProxyOrderPreviewDto) {
return this.tradeService.previewPartnerProxyOrder(dto); return this.tradeService.previewPartnerProxyOrder(dto, { bypassWhitelist: true });
}
@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);
} }
@Post() @Post()
@@ -35,26 +35,11 @@ export class HqProxyOrderPreviewDto {
receiverDistrict?: string; receiverDistrict?: string;
} }
export class HqProxyOrderSendCustomerSmsDto {
@IsString()
@Matches(/^1\d{10}$/, { message: '请输入有效手机号' })
phone: string;
}
export class HqProxyOrderCreateDto { export class HqProxyOrderCreateDto {
@IsString() @IsString()
@Matches(/^1\d{10}$/, { message: '请输入有效手机号' }) @Matches(/^1\d{10}$/, { message: '请输入有效手机号' })
phone: string; phone: string;
@IsString()
@IsNotEmpty()
customerSmsCode: string;
/** 发至当前 HQ 登录手机号的确认验证码 */
@IsString()
@IsNotEmpty()
operatorSmsCode: string;
@IsIn(['ADDRESS', 'ON_SITE_PICKUP']) @IsIn(['ADDRESS', 'ON_SITE_PICKUP'])
deliveryMode: 'ADDRESS' | 'ON_SITE_PICKUP'; deliveryMode: 'ADDRESS' | 'ON_SITE_PICKUP';
@@ -39,25 +39,11 @@ export class PartnerProxyOrderPreviewDto {
receiverDistrict?: string; receiverDistrict?: string;
} }
export class PartnerProxyOrderSendCustomerSmsDto {
@IsString()
@Matches(/^1\d{10}$/, { message: '请输入有效手机号' })
phone: string;
}
export class PartnerProxyOrderCreateDto { export class PartnerProxyOrderCreateDto {
@IsString() @IsString()
@Matches(/^1\d{10}$/, { message: '请输入有效手机号' }) @Matches(/^1\d{10}$/, { message: '请输入有效手机号' })
phone: string; phone: string;
@IsString()
@IsNotEmpty()
customerSmsCode: string;
@IsString()
@IsNotEmpty()
partnerSmsCode: string;
@IsIn(['ADDRESS', 'ON_SITE_PICKUP']) @IsIn(['ADDRESS', 'ON_SITE_PICKUP'])
deliveryMode: 'ADDRESS' | 'ON_SITE_PICKUP'; deliveryMode: 'ADDRESS' | 'ON_SITE_PICKUP';
@@ -9,7 +9,6 @@ import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { import {
PartnerProxyOrderCreateDto, PartnerProxyOrderCreateDto,
PartnerProxyOrderPreviewDto, PartnerProxyOrderPreviewDto,
PartnerProxyOrderSendCustomerSmsDto,
} from './dto/partner-proxy-order.dto'; } from './dto/partner-proxy-order.dto';
import { ManualShipOrderDto } from '../ops/dto/admin-mutate.dto'; import { ManualShipOrderDto } from '../ops/dto/admin-mutate.dto';
import { CreateAfterSaleTicketDto, CreateInvoiceDto } from './dto/after-sale.dto'; import { CreateAfterSaleTicketDto, CreateInvoiceDto } from './dto/after-sale.dto';
@@ -210,24 +209,8 @@ export class PartnerProxyOrderController {
} }
@Post('preview') @Post('preview')
preview(@Body() dto: PartnerProxyOrderPreviewDto) { preview(@CurrentUser() user: AuthUser, @Body() dto: PartnerProxyOrderPreviewDto) {
return this.tradeService.previewPartnerProxyOrder(dto); return this.tradeService.previewPartnerProxyOrderForPartner(user.actorId, 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);
} }
@Post() @Post()
@@ -12,7 +12,7 @@ import {
orderTabToStatuses, orderTabToStatuses,
validateMinPurchase, validateMinPurchase,
} from '@dukang/domain'; } 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 { PrismaService } from '../../common/prisma/prisma.module';
import { serializeBigInt } from '../../common/decorators/current-user.decorator'; import { serializeBigInt } from '../../common/decorators/current-user.decorator';
import { AnalyticsService } from '../analytics/analytics.service'; import { AnalyticsService } from '../analytics/analytics.service';
@@ -1050,7 +1050,8 @@ export class TradeService {
async getPartnerProxyOrderOptions(partnerAccountId: bigint) { async getPartnerProxyOrderOptions(partnerAccountId: bigint) {
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId); const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
const [products, promoCodes, stores] = await Promise.all([ 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.promoCodeService.listActiveOptions(),
this.prisma.store.findMany({ this.prisma.store.findMany({
where: { partnerAccountId: primary.id }, where: { partnerAccountId: primary.id },
@@ -1090,17 +1091,18 @@ export class TradeService {
}); });
} }
async previewPartnerProxyOrder(body: { async previewPartnerProxyOrder(
productId: string; body: {
quantity: number; productId: string;
deliveryMode?: 'ADDRESS' | 'ON_SITE_PICKUP'; quantity: number;
storeId?: string; deliveryMode?: 'ADDRESS' | 'ON_SITE_PICKUP';
receiverCity?: string; storeId?: string;
receiverDistrict?: string; receiverCity?: string;
}) { receiverDistrict?: string;
const product = await this.catalogService.getProduct(BigInt(body.productId), { },
bypassWhitelist: true, viewer?: { phone?: string | null; bypassWhitelist?: boolean },
}); ) {
const product = await this.catalogService.getProduct(BigInt(body.productId), viewer ?? {});
if (!product || product.status !== 'ON_SALE') { if (!product || product.status !== 'ON_SALE') {
throw new BadRequestException('商品不可购买'); throw new BadRequestException('商品不可购买');
} }
@@ -1146,45 +1148,26 @@ export class TradeService {
}; };
} }
async sendPartnerProxyCustomerSms(phone: string) { /** 合伙人代下单预览:按合伙人手机号遵守商品白名单 */
const normalizedPhone = phone.trim(); async previewPartnerProxyOrderForPartner(
await this.authService.sendSms(normalizedPhone, SmsScene.PARTNER_PROXY_CUSTOMER, { partnerAccountId: bigint,
clientApp: ClientApp.PARTNER_H5, body: {
}); productId: string;
const masked = quantity: number;
normalizedPhone.length >= 7 deliveryMode?: 'ADDRESS' | 'ON_SITE_PICKUP';
? `${normalizedPhone.slice(0, 3)}****${normalizedPhone.slice(-4)}` storeId?: string;
: normalizedPhone; receiverCity?: string;
return { ok: true, maskedPhone: masked }; receiverDistrict?: string;
} },
) {
/** @deprecated 兼容旧前端:转发为客户短信 */
async sendPartnerProxyOrderSms(phone: string) {
return this.sendPartnerProxyCustomerSms(phone);
}
async sendPartnerProxyPartnerSms(partnerAccountId: bigint) {
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId); const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
const partnerPhone = primary.phone?.trim(); return this.previewPartnerProxyOrder(body, { phone: primary.phone });
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 };
} }
async createPartnerProxyOrder( async createPartnerProxyOrder(
partnerAccountId: bigint, partnerAccountId: bigint,
body: { body: {
phone: string; phone: string;
customerSmsCode: string;
partnerSmsCode: string;
deliveryMode: 'ADDRESS' | 'ON_SITE_PICKUP'; deliveryMode: 'ADDRESS' | 'ON_SITE_PICKUP';
autoReceive?: boolean; autoReceive?: boolean;
storeId?: string; storeId?: string;
@@ -1200,23 +1183,15 @@ export class TradeService {
req: Request, req: Request,
) { ) {
const normalizedPhone = body.phone.trim(); const normalizedPhone = body.phone.trim();
if (!/^1\d{10}$/.test(normalizedPhone)) {
throw new BadRequestException('请输入有效手机号');
}
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId); const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
const partnerPhone = primary.phone?.trim(); const partnerPhone = primary.phone?.trim();
if (!partnerPhone || !/^1\d{10}$/.test(partnerPhone)) { if (!partnerPhone || !/^1\d{10}$/.test(partnerPhone)) {
throw new BadRequestException('合伙人手机号无效'); 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) { if (body.deliveryMode === 'ADDRESS' && body.autoReceive !== true) {
throw new BadRequestException('配送到址须勾选同意自动收货'); throw new BadRequestException('配送到址须勾选同意自动收货');
} }
@@ -1232,14 +1207,17 @@ export class TradeService {
sourceLabel: `代下单·${maskedPartnerPhone}`, sourceLabel: `代下单·${maskedPartnerPhone}`,
}); });
const preview = await this.previewPartnerProxyOrder({ const preview = await this.previewPartnerProxyOrder(
productId: body.productId, {
quantity: body.quantity, productId: body.productId,
deliveryMode: body.deliveryMode, quantity: body.quantity,
storeId: body.storeId, deliveryMode: body.deliveryMode,
receiverCity: body.city, storeId: body.storeId,
receiverDistrict: body.district, receiverCity: body.city,
}); receiverDistrict: body.district,
},
{ phone: primary.phone },
);
const product = await this.prisma.commonProductItem.findUniqueOrThrow({ const product = await this.prisma.commonProductItem.findUniqueOrThrow({
where: { id: BigInt(body.productId) }, where: { id: BigInt(body.productId) },
@@ -1384,7 +1362,7 @@ export class TradeService {
return this.getPartnerOrder(partnerAccountId, order.id); return this.getPartnerOrder(partnerAccountId, order.id);
} }
/** HQ 代下单:商品/推广码选项(不绑定合伙人门店 */ /** HQ 代下单:商品/推广码选项(运营侧可看白名单测试酒 */
async getHqProxyOrderOptions() { async getHqProxyOrderOptions() {
const [products, promoCodes] = await Promise.all([ const [products, promoCodes] = await Promise.all([
this.catalogService.listProducts(undefined, undefined, { bypassWhitelist: true }), 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( async createHqProxyOrder(
hqAccountId: bigint, hqAccountId: bigint,
body: { body: {
phone: string; phone: string;
customerSmsCode: string;
operatorSmsCode: string;
deliveryMode: 'ADDRESS' | 'ON_SITE_PICKUP'; deliveryMode: 'ADDRESS' | 'ON_SITE_PICKUP';
autoReceive?: boolean; autoReceive?: boolean;
receiverName?: string; receiverName?: string;
@@ -1456,6 +1401,9 @@ export class TradeService {
req: Request, req: Request,
) { ) {
const normalizedPhone = body.phone.trim(); const normalizedPhone = body.phone.trim();
if (!/^1\d{10}$/.test(normalizedPhone)) {
throw new BadRequestException('请输入有效手机号');
}
const hq = await this.prisma.hqAccount.findUnique({ where: { id: hqAccountId } }); const hq = await this.prisma.hqAccount.findUnique({ where: { id: hqAccountId } });
if (!hq || hq.status !== 'ACTIVE') { if (!hq || hq.status !== 'ACTIVE') {
throw new BadRequestException('总部账号无效'); throw new BadRequestException('总部账号无效');
@@ -1465,17 +1413,6 @@ export class TradeService {
throw new BadRequestException('总部账号手机号无效'); 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) { if (body.deliveryMode === 'ADDRESS' && body.autoReceive !== true) {
throw new BadRequestException('配送到址须勾选同意自动收货'); throw new BadRequestException('配送到址须勾选同意自动收货');
} }
@@ -1492,13 +1429,16 @@ export class TradeService {
sourceLabel: `总部代下单·${maskedOperatorPhone}`, sourceLabel: `总部代下单·${maskedOperatorPhone}`,
}); });
const preview = await this.previewPartnerProxyOrder({ const preview = await this.previewPartnerProxyOrder(
productId: body.productId, {
quantity: body.quantity, productId: body.productId,
deliveryMode: body.deliveryMode, quantity: body.quantity,
receiverCity: body.city, deliveryMode: body.deliveryMode,
receiverDistrict: body.district, receiverCity: body.city,
}); receiverDistrict: body.district,
},
{ bypassWhitelist: true },
);
const product = await this.prisma.commonProductItem.findUniqueOrThrow({ const product = await this.prisma.commonProductItem.findUniqueOrThrow({
where: { id: BigInt(body.productId) }, where: { id: BigInt(body.productId) },