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:
@@ -6,7 +6,7 @@ import { groupResourcesByProductId, mapProductMedia } from './catalog.mapper';
|
||||
export type CatalogViewer = {
|
||||
/** C 端用户手机号;无则无法看到白名单商品 */
|
||||
phone?: string | null;
|
||||
/** 合伙人代下单等内部场景跳过白名单 */
|
||||
/** 仅总部代下单等运营场景跳过白名单;合伙人端必须遵守白名单 */
|
||||
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 { 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()
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) },
|
||||
|
||||
Reference in New Issue
Block a user