feat(promo): HQ 推广码订单只计已完成并补扫码订单快链
列表与详情的订单数/转化率按 COMPLETED 实时统计;扫码、订单、事件 ID 可跳到对应页。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -100,6 +100,27 @@ export class WechatTradeManageService {
|
||||
remark,
|
||||
}),
|
||||
});
|
||||
if (order.promoCodeId) {
|
||||
const already = await tx.logPromoEvent.findFirst({
|
||||
where: { orderId: order.id, eventType: 'ORDER' },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!already) {
|
||||
await tx.commonPromoCode.update({
|
||||
where: { id: order.promoCodeId },
|
||||
data: { orderCount: { increment: 1 } },
|
||||
});
|
||||
await tx.logPromoEvent.create({
|
||||
data: {
|
||||
promoCodeId: order.promoCodeId,
|
||||
eventType: 'ORDER',
|
||||
userId: order.userId,
|
||||
orderId: order.id,
|
||||
clientIp: order.clientIp,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await this.logEvent(evt, order.id, 'SUCCESS', methodLabel);
|
||||
|
||||
@@ -37,6 +37,7 @@ type OrderFilterInput = Pick<
|
||||
| 'createdTo'
|
||||
| 'excludeTest'
|
||||
| 'deliveryType'
|
||||
| 'promoCodeId'
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
@@ -94,7 +95,7 @@ export class AdminOrdersService {
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
user: { select: { id: true, userNo: true, phone: true, nickname: true } },
|
||||
user: { select: { id: true, userNo: true, phone: true, nickname: true, hqRemark: true } },
|
||||
delivery: {
|
||||
select: {
|
||||
provider: true,
|
||||
@@ -190,6 +191,10 @@ export class AdminOrdersService {
|
||||
if (query.createdTo) where.createdAt.lte = this.endOfDay(query.createdTo);
|
||||
}
|
||||
if (query.excludeTest) where.isTest = false;
|
||||
const promoCodeId = query.promoCodeId?.trim();
|
||||
if (promoCodeId && /^\d+$/.test(promoCodeId)) {
|
||||
where.promoCodeId = BigInt(promoCodeId);
|
||||
}
|
||||
|
||||
return where;
|
||||
}
|
||||
@@ -252,6 +257,7 @@ export class AdminOrdersService {
|
||||
userNo: true,
|
||||
phone: true,
|
||||
nickname: true,
|
||||
hqRemark: true,
|
||||
deviceKey: true,
|
||||
phoneVerifiedAt: true,
|
||||
},
|
||||
|
||||
@@ -81,6 +81,13 @@ export class AdminUsersService {
|
||||
|
||||
if (query.phone) where.phone = { contains: query.phone };
|
||||
if (query.userNo) where.userNo = { contains: query.userNo };
|
||||
const userId = query.userId?.trim();
|
||||
if (userId) {
|
||||
if (!/^\d+$/.test(userId)) {
|
||||
return serializeBigInt({ items: [], total: 0, page, pageSize });
|
||||
}
|
||||
where.id = BigInt(userId);
|
||||
}
|
||||
if (query.deviceKey) where.deviceKey = query.deviceKey;
|
||||
if (query.status !== undefined) where.status = query.status;
|
||||
if (query.phoneVerified === '1') where.phoneVerifiedAt = { not: null };
|
||||
|
||||
@@ -48,6 +48,11 @@ export class AdminUsersQueryDto extends PaginationQueryDto {
|
||||
@IsString()
|
||||
userNo?: string;
|
||||
|
||||
/** 精确匹配用户主键,供订单等页的用户快链带入筛选 */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
deviceKey?: string;
|
||||
@@ -116,6 +121,11 @@ export class AdminOrdersQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsIn(['LOCAL', 'CROSS_CITY', 'ON_SITE_PICKUP'])
|
||||
deliveryType?: string;
|
||||
|
||||
/** 按下单时绑定的推广码筛选 */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
promoCodeId?: string;
|
||||
}
|
||||
|
||||
/** HQ 订单导出(筛选 + 勾选范围) */
|
||||
@@ -174,6 +184,10 @@ export class AdminOrdersExportDto {
|
||||
@IsOptional()
|
||||
@IsIn(['LOCAL', 'CROSS_CITY', 'ON_SITE_PICKUP'])
|
||||
deliveryType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
promoCodeId?: string;
|
||||
}
|
||||
|
||||
/** 概览页用户/订单 ECharts 聚合筛选 */
|
||||
|
||||
@@ -151,7 +151,7 @@ export class PromoCodeService {
|
||||
});
|
||||
}
|
||||
|
||||
private mapRow(row: PromoRow) {
|
||||
private mapRow(row: PromoRow, orderCount?: number) {
|
||||
return serializeBigInt({
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
@@ -161,7 +161,7 @@ export class PromoCodeService {
|
||||
status: row.status,
|
||||
remark: row.remark,
|
||||
scanCount: row.scanCount,
|
||||
orderCount: row.orderCount,
|
||||
orderCount: orderCount ?? row.orderCount,
|
||||
landingUrl: buildLandingUrl(row.code, row.qrcodeId),
|
||||
qrcodeUrl: row.qrcodeResource?.url ?? null,
|
||||
ownerUser: this.mapOwnerUser(row.ownerUser),
|
||||
@@ -207,9 +207,10 @@ export class PromoCodeService {
|
||||
}),
|
||||
this.prisma.commonPromoCode.count({ where }),
|
||||
]);
|
||||
const completedByPromo = await this.completedOrderCounts(items.map((r) => r.id));
|
||||
|
||||
return serializeBigInt({
|
||||
items: items.map((r) => this.mapRow(r)),
|
||||
items: items.map((r) => this.mapRow(r, completedByPromo.get(r.id.toString()) ?? 0)),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
@@ -223,7 +224,7 @@ export class PromoCodeService {
|
||||
});
|
||||
if (!row) throw new NotFoundException('推广码不存在');
|
||||
const stats = await this.statsFromRow(row);
|
||||
return serializeBigInt({ ...this.mapRow(row), stats });
|
||||
return serializeBigInt({ ...this.mapRow(row, stats.orderCount), stats });
|
||||
}
|
||||
|
||||
private async resolveOwnerUserId(ownerUserId?: string) {
|
||||
@@ -519,6 +520,40 @@ export class PromoCodeService {
|
||||
this.logPromoMetric(promoCodeId, 'ORDER', { clientIp }, { userId, orderId });
|
||||
}
|
||||
|
||||
/** 订单完成时计入推广码订单数(下单/待付款不计入) */
|
||||
async recordCompletedOrder(
|
||||
promoCodeId: bigint,
|
||||
orderId: bigint,
|
||||
userId: bigint,
|
||||
clientIp?: string,
|
||||
): Promise<void> {
|
||||
const already = await this.prisma.logPromoEvent.findFirst({
|
||||
where: { orderId, eventType: 'ORDER' },
|
||||
select: { id: true },
|
||||
});
|
||||
if (already) return;
|
||||
await this.prisma.commonPromoCode.update({
|
||||
where: { id: promoCodeId },
|
||||
data: { orderCount: { increment: 1 } },
|
||||
});
|
||||
this.logPromoOrderEvent(promoCodeId, orderId, userId, clientIp);
|
||||
}
|
||||
|
||||
private async completedOrderCounts(promoIds: bigint[]): Promise<Map<string, number>> {
|
||||
const map = new Map<string, number>();
|
||||
if (!promoIds.length) return map;
|
||||
const rows = await this.prisma.order.groupBy({
|
||||
by: ['promoCodeId'],
|
||||
where: { promoCodeId: { in: promoIds }, status: 'COMPLETED' },
|
||||
_count: { _all: true },
|
||||
});
|
||||
for (const row of rows) {
|
||||
if (row.promoCodeId == null) continue;
|
||||
map.set(row.promoCodeId.toString(), row._count._all);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private logPromoMetric(
|
||||
promoCodeId: bigint,
|
||||
eventType: PromoMetricEventType,
|
||||
@@ -610,12 +645,37 @@ export class PromoCodeService {
|
||||
this.prisma.logPromoEvent.count({ where }),
|
||||
]);
|
||||
|
||||
const userIds = [...new Set(items.map((row) => row.userId).filter((id): id is bigint => id != null))];
|
||||
const orderIds = [...new Set(items.map((row) => row.orderId).filter((id): id is bigint => id != null))];
|
||||
const [users, orders] = await Promise.all([
|
||||
userIds.length
|
||||
? this.prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, userNo: true },
|
||||
})
|
||||
: [],
|
||||
orderIds.length
|
||||
? this.prisma.order.findMany({
|
||||
where: { id: { in: orderIds } },
|
||||
select: { id: true, orderNo: true },
|
||||
})
|
||||
: [],
|
||||
]);
|
||||
const userNoById = new Map<string, string | null>(
|
||||
users.map((u): [string, string | null] => [u.id.toString(), u.userNo]),
|
||||
);
|
||||
const orderNoById = new Map<string, string>(
|
||||
orders.map((o): [string, string] => [o.id.toString(), o.orderNo]),
|
||||
);
|
||||
|
||||
return serializeBigInt({
|
||||
items: items.map((row) => ({
|
||||
id: row.id,
|
||||
eventType: row.eventType,
|
||||
userId: row.userId,
|
||||
userNo: row.userId ? userNoById.get(row.userId.toString()) ?? null : null,
|
||||
orderId: row.orderId,
|
||||
orderNo: row.orderId ? orderNoById.get(row.orderId.toString()) ?? null : null,
|
||||
sessionId: row.sessionId,
|
||||
clientIp: row.clientIp,
|
||||
ipProvince: row.ipProvince,
|
||||
@@ -636,12 +696,9 @@ export class PromoCodeService {
|
||||
if (!promo) throw new NotFoundException('推广码不存在');
|
||||
}
|
||||
|
||||
private async statsFromRow(row: { id: bigint; scanCount: number; orderCount: number }) {
|
||||
private async statsFromRow(row: { id: bigint; scanCount: number }) {
|
||||
const scanCount = row.scanCount;
|
||||
const orderCount = row.orderCount;
|
||||
const conversionRate =
|
||||
scanCount > 0 ? Math.round((orderCount / scanCount) * 1000) / 10 : 0;
|
||||
const [attributionCount, sourceMarkedCount] = await Promise.all([
|
||||
const [attributionCount, sourceMarkedCount, orderCount] = await Promise.all([
|
||||
this.prisma.userPromoAttribution.count({
|
||||
where: { promoCodeId: row.id },
|
||||
}),
|
||||
@@ -652,7 +709,12 @@ export class PromoCodeService {
|
||||
mergedIntoUserId: null,
|
||||
},
|
||||
}),
|
||||
this.prisma.order.count({
|
||||
where: { promoCodeId: row.id, status: 'COMPLETED' },
|
||||
}),
|
||||
]);
|
||||
const conversionRate =
|
||||
scanCount > 0 ? Math.round((orderCount / scanCount) * 1000) / 10 : 0;
|
||||
return {
|
||||
scanCount,
|
||||
orderCount,
|
||||
@@ -695,7 +757,7 @@ export class PromoCodeService {
|
||||
sourceRefId: true,
|
||||
createdAt: true,
|
||||
promoTouch: { select: { firstTouchAt: true, promoCodeId: true } },
|
||||
_count: { select: { orders: true } },
|
||||
_count: { select: { orders: { where: { status: 'COMPLETED' } } } },
|
||||
},
|
||||
}),
|
||||
this.prisma.user.count({ where }),
|
||||
|
||||
@@ -357,25 +357,9 @@ export class TradeService {
|
||||
include: { product: true, imageResource: true },
|
||||
});
|
||||
|
||||
if (promoCodeId) {
|
||||
await tx.commonPromoCode.update({
|
||||
where: { id: promoCodeId },
|
||||
data: { orderCount: { increment: 1 } },
|
||||
});
|
||||
}
|
||||
|
||||
return created;
|
||||
});
|
||||
|
||||
if (promoCodeId) {
|
||||
this.promoCodeService.logPromoOrderEvent(
|
||||
promoCodeId,
|
||||
order.id,
|
||||
userId,
|
||||
location.clientIp ?? undefined,
|
||||
);
|
||||
}
|
||||
|
||||
this.analyticsService.trackOneSafe(userId, this.resolveTrackedUserClientApp(clientApp), {
|
||||
eventName: 'order_submit',
|
||||
refType: 'ORDER',
|
||||
@@ -526,6 +510,15 @@ export class TradeService {
|
||||
});
|
||||
if (!order) return;
|
||||
|
||||
if (order.status === 'COMPLETED' && order.promoCodeId) {
|
||||
await this.promoCodeService.recordCompletedOrder(
|
||||
order.promoCodeId,
|
||||
order.id,
|
||||
order.userId,
|
||||
order.clientIp ?? undefined,
|
||||
);
|
||||
}
|
||||
|
||||
if (!order.isTest) {
|
||||
const unit = order.saleUnit === 'BOX' ? '箱' : '瓶';
|
||||
const spec = (order.productSpec || '').trim();
|
||||
@@ -1593,6 +1586,15 @@ export class TradeService {
|
||||
});
|
||||
});
|
||||
|
||||
if (targetStatus === 'COMPLETED' && currentStatus !== 'COMPLETED' && order.promoCodeId) {
|
||||
await this.promoCodeService.recordCompletedOrder(
|
||||
order.promoCodeId,
|
||||
order.id,
|
||||
order.userId,
|
||||
order.clientIp ?? undefined,
|
||||
);
|
||||
}
|
||||
|
||||
// 发货信息管理:进入 SHIPPING 时向微信录入(解冻结算前置)
|
||||
if (targetStatus === 'SHIPPING' && currentStatus !== 'SHIPPING') {
|
||||
this.wechatOrderShipping.uploadForOrderSafe(orderId);
|
||||
@@ -1934,25 +1936,9 @@ export class TradeService {
|
||||
}),
|
||||
});
|
||||
|
||||
if (promoCodeId) {
|
||||
await tx.commonPromoCode.update({
|
||||
where: { id: promoCodeId },
|
||||
data: { orderCount: { increment: 1 } },
|
||||
});
|
||||
}
|
||||
|
||||
return created;
|
||||
});
|
||||
|
||||
if (promoCodeId) {
|
||||
this.promoCodeService.logPromoOrderEvent(
|
||||
promoCodeId,
|
||||
order.id,
|
||||
user.id,
|
||||
location.clientIp ?? undefined,
|
||||
);
|
||||
}
|
||||
|
||||
this.analyticsService.trackPartnerOneSafe(partnerAccountId, 'PARTNER_H5', {
|
||||
partnerAccountId: primary.id,
|
||||
eventName: 'partner_proxy_order_create',
|
||||
@@ -2347,25 +2333,9 @@ export class TradeService {
|
||||
}),
|
||||
});
|
||||
|
||||
if (promoCodeId) {
|
||||
await tx.commonPromoCode.update({
|
||||
where: { id: promoCodeId },
|
||||
data: { orderCount: { increment: 1 } },
|
||||
});
|
||||
}
|
||||
|
||||
return created;
|
||||
});
|
||||
|
||||
if (promoCodeId) {
|
||||
this.promoCodeService.logPromoOrderEvent(
|
||||
promoCodeId,
|
||||
order.id,
|
||||
user.id,
|
||||
location.clientIp ?? undefined,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
id: order.id.toString(),
|
||||
orderNo: order.orderNo,
|
||||
|
||||
Reference in New Issue
Block a user