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
@@ -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) },