代下单功能
This commit is contained in:
@@ -11,13 +11,15 @@ import {
|
||||
orderTabToStatuses,
|
||||
validateMinPurchase,
|
||||
} from '@dukang/domain';
|
||||
import { loadAppConfig, WECHAT_AUTH_REQUIRED } from '@dukang/shared-types';
|
||||
import { loadAppConfig, ClientApp, SmsScene, 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';
|
||||
import { CatalogService } from '../catalog/catalog.service';
|
||||
import { BenefitService } from '../benefit/benefit.service';
|
||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||
import { AuthService } from '../iam/auth.service';
|
||||
import { PromoCodeService } from '../promo/promo-code.service';
|
||||
import { TicketService } from '../common/ticket.service';
|
||||
import { PAY_PROVIDER, DELIVERY_PROVIDER } from '../../integrations/integrations.constants';
|
||||
import { IPayProvider } from '../../integrations/pay/pay.interface';
|
||||
@@ -41,6 +43,8 @@ export class TradeService {
|
||||
@Inject(DELIVERY_PROVIDER) private readonly deliveryProvider: IDeliveryProvider,
|
||||
private readonly analyticsService: AnalyticsService,
|
||||
private readonly partnerCityService: PartnerCityService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly promoCodeService: PromoCodeService,
|
||||
) {}
|
||||
|
||||
async preview(userId: bigint, body: { productId: string; quantity: number; addressId?: string }) {
|
||||
@@ -584,4 +588,225 @@ export class TradeService {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getPartnerProxyOrderOptions() {
|
||||
const [products, promoCodes] = await Promise.all([
|
||||
this.catalogService.listProducts(),
|
||||
this.promoCodeService.listActiveOptions(),
|
||||
]);
|
||||
return serializeBigInt({
|
||||
products: products.map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
spec: p.spec,
|
||||
price: Number(p.price),
|
||||
benefitAmount: p.benefitAmount != null ? Number(p.benefitAmount) : null,
|
||||
})),
|
||||
promoCodes,
|
||||
});
|
||||
}
|
||||
|
||||
async previewPartnerProxyOrder(body: {
|
||||
productId: string;
|
||||
quantity: number;
|
||||
receiverCity?: string;
|
||||
receiverDistrict?: string;
|
||||
}) {
|
||||
const product = await this.catalogService.getProduct(BigInt(body.productId));
|
||||
if (!product || product.status !== 'ON_SALE') {
|
||||
throw new BadRequestException('商品不可购买');
|
||||
}
|
||||
const city = await this.prisma.commonCity.findFirst({ where: { status: 'ACTIVE' } });
|
||||
if (!city) throw new BadRequestException('暂无开城城市');
|
||||
|
||||
let deliveryType: 'LOCAL' | 'CROSS_CITY' = 'LOCAL';
|
||||
const receiverCity = body.receiverCity?.trim();
|
||||
if (receiverCity && receiverCity !== city.name && receiverCity !== '郑州市') {
|
||||
deliveryType = 'CROSS_CITY';
|
||||
}
|
||||
|
||||
const check = validateMinPurchase(
|
||||
deliveryType,
|
||||
body.quantity,
|
||||
city.localMinQty,
|
||||
city.crossMinQty,
|
||||
);
|
||||
if (!check.ok) throw new BadRequestException(check.message);
|
||||
|
||||
const unitPrice = Number(product.price);
|
||||
const productAmount = unitPrice * body.quantity;
|
||||
const benefitPerUnit = calcBenefitAmount({
|
||||
price: unitPrice,
|
||||
benefitAmount: product.benefitAmount ? Number(product.benefitAmount) : null,
|
||||
});
|
||||
|
||||
return {
|
||||
productAmount,
|
||||
payAmount: productAmount,
|
||||
benefitAmount: benefitPerUnit * body.quantity,
|
||||
deliveryType,
|
||||
unitPrice,
|
||||
};
|
||||
}
|
||||
|
||||
async sendPartnerProxyOrderSms(phone: string) {
|
||||
const normalizedPhone = phone.trim();
|
||||
await this.authService.sendSms(normalizedPhone, SmsScene.PARTNER_PROXY_ORDER, {
|
||||
clientApp: ClientApp.PARTNER_H5,
|
||||
});
|
||||
const masked =
|
||||
normalizedPhone.length >= 7
|
||||
? `${normalizedPhone.slice(0, 3)}****${normalizedPhone.slice(-4)}`
|
||||
: normalizedPhone;
|
||||
return { ok: true, maskedPhone: masked };
|
||||
}
|
||||
|
||||
async createPartnerProxyOrder(
|
||||
partnerAccountId: bigint,
|
||||
body: {
|
||||
phone: string;
|
||||
smsCode: string;
|
||||
receiverName?: string;
|
||||
province: string;
|
||||
city: string;
|
||||
district: string;
|
||||
addressDetail: string;
|
||||
productId: string;
|
||||
quantity: number;
|
||||
promoCodeId?: string;
|
||||
},
|
||||
req: Request,
|
||||
) {
|
||||
const normalizedPhone = body.phone.trim();
|
||||
await this.authService.verifySmsCode(
|
||||
normalizedPhone,
|
||||
body.smsCode.trim(),
|
||||
SmsScene.PARTNER_PROXY_ORDER,
|
||||
);
|
||||
|
||||
const user = await this.authService.findOrCreateUserByPhone(normalizedPhone);
|
||||
const preview = await this.previewPartnerProxyOrder({
|
||||
productId: body.productId,
|
||||
quantity: body.quantity,
|
||||
receiverCity: body.city,
|
||||
receiverDistrict: body.district,
|
||||
});
|
||||
|
||||
const product = await this.prisma.commonProductItem.findUniqueOrThrow({
|
||||
where: { id: BigInt(body.productId) },
|
||||
});
|
||||
const city = await this.prisma.commonCity.findFirstOrThrow({ where: { status: 'ACTIVE' } });
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const paySnapshot = await this.partnerCityService.resolveForOrder(city.id, body.district);
|
||||
|
||||
let promoCodeId: bigint | undefined;
|
||||
if (body.promoCodeId?.trim()) {
|
||||
promoCodeId = BigInt(body.promoCodeId.trim());
|
||||
await this.promoCodeService.attributeUserToPromo(user.id, promoCodeId);
|
||||
}
|
||||
|
||||
const receiverName = body.receiverName?.trim() || `用户${normalizedPhone.slice(-4)}`;
|
||||
const receiverAddress = `${body.province}${body.city}${body.district}${body.addressDetail}`;
|
||||
const orderNo = generateOrderNo();
|
||||
const now = new Date();
|
||||
|
||||
const location = buildOrderClientLocationSnapshot(
|
||||
req,
|
||||
this.ipGeoService.resolve(extractClientIp(req)),
|
||||
undefined,
|
||||
);
|
||||
|
||||
const order = await this.prisma.$transaction(async (tx) => {
|
||||
const created = await tx.order.create({
|
||||
data: {
|
||||
orderNo,
|
||||
orderType: 'PROXY',
|
||||
userId: user.id,
|
||||
cityId: city.id,
|
||||
status: 'COMPLETED',
|
||||
payStatus: 'PAID',
|
||||
deliveryType: preview.deliveryType,
|
||||
channelSource: 'OFFLINE_PROXY',
|
||||
promoCodeId,
|
||||
productId: product.id,
|
||||
barcode69: product.barcode69,
|
||||
productName: product.name,
|
||||
productSpec: product.spec,
|
||||
imageResourceId: product.coverResourceId,
|
||||
quantity: body.quantity,
|
||||
listUnitPrice: product.price,
|
||||
listAmount: preview.productAmount,
|
||||
productAmount: preview.productAmount,
|
||||
payAmount: preview.payAmount,
|
||||
benefitAmount: preview.benefitAmount,
|
||||
freightAmount: 0,
|
||||
freightPayType: preview.deliveryType === 'CROSS_CITY' ? 'COD' : null,
|
||||
receiverName,
|
||||
receiverPhone: normalizedPhone,
|
||||
receiverAddress,
|
||||
receiverProvince: body.province,
|
||||
receiverCity: body.city,
|
||||
receiverDistrict: body.district,
|
||||
clientIp: location.clientIp,
|
||||
ipProvince: location.ipProvince,
|
||||
ipCity: location.ipCity,
|
||||
ipDistrict: location.ipDistrict,
|
||||
paidAt: now,
|
||||
shippedAt: now,
|
||||
completedAt: now,
|
||||
partnerAccountIdAtPay: paySnapshot?.partnerAccountId ?? primary.id,
|
||||
orderCommissionRateAtPay: paySnapshot?.orderCommissionRate ?? null,
|
||||
remark: `合伙人代下单 partnerAccountId=${primary.id}`,
|
||||
},
|
||||
include: { product: true, imageResource: true },
|
||||
});
|
||||
|
||||
await tx.orderDelivery.create({
|
||||
data: {
|
||||
orderId: created.id,
|
||||
provider: 'MANUAL',
|
||||
outWarehouseAt: now,
|
||||
shippingAt: now,
|
||||
deliveredAt: now,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.commonEvent.create({
|
||||
data: buildOrderStatusEvent({
|
||||
orderId: created.id,
|
||||
fromStatus: 'PENDING_PAY',
|
||||
toStatus: 'COMPLETED',
|
||||
operator: 'PARTNER_PROXY',
|
||||
remark: '合伙人线下代下单',
|
||||
}),
|
||||
});
|
||||
|
||||
if (promoCodeId) {
|
||||
await tx.commonPromoCode.update({
|
||||
where: { id: promoCodeId },
|
||||
data: { orderCount: { increment: 1 } },
|
||||
});
|
||||
}
|
||||
|
||||
return created;
|
||||
});
|
||||
|
||||
await this.benefitService.grantOnOrderPaid(order.id);
|
||||
|
||||
this.analyticsService.trackPartnerOneSafe(partnerAccountId, 'PARTNER_H5', {
|
||||
partnerAccountId: primary.id,
|
||||
eventName: 'partner_proxy_order_create',
|
||||
refType: 'ORDER',
|
||||
refId: order.id,
|
||||
extraJson: {
|
||||
orderId: order.id.toString(),
|
||||
userId: user.id.toString(),
|
||||
productId: body.productId,
|
||||
quantity: body.quantity,
|
||||
promoCodeId: promoCodeId?.toString() ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
return this.getPartnerOrder(partnerAccountId, order.id);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user