推广码后端页面优化
This commit is contained in:
@@ -145,6 +145,7 @@ export default function PromoCodesPage() {
|
|||||||
scene: values.scene,
|
scene: values.scene,
|
||||||
ownerUserId: values.ownerUserId?.trim() || undefined,
|
ownerUserId: values.ownerUserId?.trim() || undefined,
|
||||||
remark: values.remark?.trim() || undefined,
|
remark: values.remark?.trim() || undefined,
|
||||||
|
page: values.page?.trim() || undefined,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
message.success('推广码已创建');
|
message.success('推广码已创建');
|
||||||
@@ -220,6 +221,13 @@ export default function PromoCodesPage() {
|
|||||||
<Form.Item name="code" label="自定义码值(选填)">
|
<Form.Item name="code" label="自定义码值(选填)">
|
||||||
<Input placeholder="留空自动生成,如 DKDEMO1" />
|
<Input placeholder="留空自动生成,如 DKDEMO1" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="page"
|
||||||
|
label="小程序落地页(选填)"
|
||||||
|
extra="如 pages/home/index;留空则使用服务端环境变量 WX_MINI_PROMO_PAGE"
|
||||||
|
>
|
||||||
|
<Input placeholder="pages/home/index" />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item name="ownerUserId" label="关联用户 ID(选填)">
|
<Form.Item name="ownerUserId" label="关联用户 ID(选填)">
|
||||||
<Input placeholder="渠道负责人,填写用户数据库 ID" />
|
<Input placeholder="渠道负责人,填写用户数据库 ID" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -155,9 +155,6 @@ export default function PromoCodeDetailPage() {
|
|||||||
<Descriptions.Item label="二维码 ID" span={2}>
|
<Descriptions.Item label="二维码 ID" span={2}>
|
||||||
<Typography.Text copyable={{ text: detail.qrcodeId }}>{detail.qrcodeId}</Typography.Text>
|
<Typography.Text copyable={{ text: detail.qrcodeId }}>{detail.qrcodeId}</Typography.Text>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="H5 落地链接" span={2}>
|
|
||||||
<Typography.Text copyable={{ text: detail.landingUrl }}>{detail.landingUrl}</Typography.Text>
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="小程序码 OSS" span={2}>
|
<Descriptions.Item label="小程序码 OSS" span={2}>
|
||||||
{detail.qrcodeUrl ? (
|
{detail.qrcodeUrl ? (
|
||||||
<Typography.Text copyable={{ text: detail.qrcodeUrl }} ellipsis>
|
<Typography.Text copyable={{ text: detail.qrcodeUrl }} ellipsis>
|
||||||
@@ -179,7 +176,15 @@ export default function PromoCodeDetailPage() {
|
|||||||
<Row gutter={16} style={{ marginTop: 16 }}>
|
<Row gutter={16} style={{ marginTop: 16 }}>
|
||||||
<Col xs={12} sm={6}>
|
<Col xs={12} sm={6}>
|
||||||
<Card size="small">
|
<Card size="small">
|
||||||
<Statistic title="扫码次数" value={stats?.scanCount ?? detail.scanCount} />
|
<Statistic title="扫码进入次数" value={stats?.scanCount ?? detail.scanCount} />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={12} sm={6}>
|
||||||
|
<Card size="small">
|
||||||
|
<Statistic
|
||||||
|
title="扫码注册用户数"
|
||||||
|
value={stats?.registerCount ?? stats?.sourceMarkedCount ?? 0}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={6}>
|
<Col xs={12} sm={6}>
|
||||||
@@ -195,11 +200,6 @@ export default function PromoCodeDetailPage() {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={6}>
|
|
||||||
<Card size="small">
|
|
||||||
<Statistic title="来源标记用户" value={stats?.sourceMarkedCount ?? '—'} />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { captureIosJssdkEntryUrl } from '@dukang/weixin-sdk';
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { finishLoginNavigate, forceReloadAfterAccountMerge, goLogin } from '../lib/auth-nav';
|
import { finishLoginNavigate, forceReloadAfterAccountMerge, goLogin } from '../lib/auth-nav';
|
||||||
import { toast } from '../lib/api';
|
import { toast } from '../lib/api';
|
||||||
|
import { capturePromoSceneAndTouchScan } from '../lib/promo';
|
||||||
import { saveWechatLoginResult } from '../lib/pay-wechat';
|
import { saveWechatLoginResult } from '../lib/pay-wechat';
|
||||||
import { applyWechatShare } from '../lib/wechat-share';
|
import { applyWechatShare } from '../lib/wechat-share';
|
||||||
import { handleWechatAuthCallback } from '../lib/wechat-auth';
|
import { handleWechatAuthCallback } from '../lib/wechat-auth';
|
||||||
@@ -29,13 +30,16 @@ function currentPagePathWithQuery(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* H5 App 根节点专用:不可用 useDidShow(App 无页面 Context)。
|
* H5 App 根节点:iOS 签名 URL + 默认分享 + OAuth code 回调。
|
||||||
* - 首次进页:捕获 iOS 签名 URL + 默认分享 + OAuth code 回调
|
* 小程序:冷启动时捕获推广码 scene 并回传扫码埋点。
|
||||||
* - 路由/回前台:刷新默认分享卡片
|
|
||||||
*/
|
*/
|
||||||
export default function WechatShareBootstrap() {
|
export default function WechatShareBootstrap() {
|
||||||
const handlingCode = useRef(false);
|
const handlingCode = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void capturePromoSceneAndTouchScan();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (process.env.TARO_ENV !== 'h5') return;
|
if (process.env.TARO_ENV !== 'h5') return;
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import { request } from './api';
|
||||||
|
|
||||||
|
const PROMO_ID_KEY = 'dukang_promo_id';
|
||||||
|
|
||||||
|
/** 同一次进入只 touch 一次扫码计数,避免首页反复 onShow 刷量 */
|
||||||
|
let lastScanTouchKey = '';
|
||||||
|
|
||||||
|
function safeDecode(raw: string): string {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(raw);
|
||||||
|
} catch {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePromoId(raw: unknown): string | null {
|
||||||
|
if (raw == null || raw === '') return null;
|
||||||
|
const s = safeDecode(String(raw)).trim();
|
||||||
|
// 小程序码 scene 写入的是推广活动数字 ID
|
||||||
|
if (!/^\d+$/.test(s)) return null;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
type EnterOptionsLike = {
|
||||||
|
scene?: string | number;
|
||||||
|
query?: Record<string, string | undefined>;
|
||||||
|
path?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 从启动/进入参数解析推广活动 ID(优先 query.scene,与 getwxacodeunlimit 一致) */
|
||||||
|
export function extractPromoIdFromEnterOptions(opts?: EnterOptionsLike | null): string | null {
|
||||||
|
if (!opts) return null;
|
||||||
|
const q = opts.query ?? {};
|
||||||
|
return (
|
||||||
|
normalizePromoId(q.scene) ||
|
||||||
|
normalizePromoId(q.promoId) ||
|
||||||
|
normalizePromoId(q.pid) ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStoredPromoId(): string | null {
|
||||||
|
try {
|
||||||
|
const v = Taro.getStorageSync(PROMO_ID_KEY);
|
||||||
|
return normalizePromoId(v);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setStoredPromoId(promoId: string) {
|
||||||
|
const id = normalizePromoId(promoId);
|
||||||
|
if (!id) return;
|
||||||
|
try {
|
||||||
|
Taro.setStorageSync(PROMO_ID_KEY, id);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readEnterOptions(): EnterOptionsLike | null {
|
||||||
|
try {
|
||||||
|
if (typeof Taro.getEnterOptionsSync === 'function') {
|
||||||
|
return Taro.getEnterOptionsSync() as EnterOptionsLike;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (typeof Taro.getLaunchOptionsSync === 'function') {
|
||||||
|
return Taro.getLaunchOptionsSync() as EnterOptionsLike;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
// H5:从 URL query 读取
|
||||||
|
if (process.env.TARO_ENV === 'h5' && typeof window !== 'undefined') {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
return {
|
||||||
|
query: {
|
||||||
|
scene: params.get('scene') || undefined,
|
||||||
|
promoId: params.get('promoId') || undefined,
|
||||||
|
pid: params.get('pid') || undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主页面进入时:取出 scene(活动 ID)本地缓存,并回传 /promo/touch 累加扫码次数。
|
||||||
|
* 同一进入会话只计一次扫码。
|
||||||
|
*/
|
||||||
|
export async function capturePromoSceneAndTouchScan(): Promise<void> {
|
||||||
|
const opts = readEnterOptions();
|
||||||
|
const fromEnter = extractPromoIdFromEnterOptions(opts);
|
||||||
|
if (fromEnter) {
|
||||||
|
setStoredPromoId(fromEnter);
|
||||||
|
const touchKey = `${fromEnter}|${opts?.path || ''}|${JSON.stringify(opts?.query || {})}|${String(opts?.scene ?? '')}`;
|
||||||
|
if (touchKey === lastScanTouchKey) return;
|
||||||
|
lastScanTouchKey = touchKey;
|
||||||
|
await touchPromo({ promoId: fromEnter, countScan: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无新 scene 时不重复扫码计数
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 登录成功后:用已缓存的活动 ID 做归因(不重复加扫码次数) */
|
||||||
|
export async function touchStoredPromoAfterLogin(): Promise<void> {
|
||||||
|
const promoId = getStoredPromoId();
|
||||||
|
if (!promoId) return;
|
||||||
|
await touchPromo({ promoId, countScan: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function touchPromo(input: { promoId: string; countScan: boolean }): Promise<void> {
|
||||||
|
try {
|
||||||
|
await request('/promo/touch', {
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
promoId: input.promoId,
|
||||||
|
countScan: input.countScan,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* 静默失败,不阻断浏览 */
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import CouponBadge from '../../components/CouponBadge';
|
|||||||
import ProductCarousel from '../../components/ProductCarousel';
|
import ProductCarousel from '../../components/ProductCarousel';
|
||||||
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
|
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
|
||||||
import { request, toast } from '../../lib/api';
|
import { request, toast } from '../../lib/api';
|
||||||
|
import { capturePromoSceneAndTouchScan } from '../../lib/promo';
|
||||||
import { getProductImages } from '../../lib/product-images';
|
import { getProductImages } from '../../lib/product-images';
|
||||||
import { getCityCodeForCatalog, resolveUserCity } from '../../lib/user-location';
|
import { getCityCodeForCatalog, resolveUserCity } from '../../lib/user-location';
|
||||||
type Product = {
|
type Product = {
|
||||||
@@ -35,6 +36,7 @@ export default function HomePage() {
|
|||||||
|
|
||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
syncTabBarSelected(0);
|
syncTabBarSelected(0);
|
||||||
|
void capturePromoSceneAndTouchScan();
|
||||||
void resolveUserCity().then((resolved) => {
|
void resolveUserCity().then((resolved) => {
|
||||||
setDisplayCity(resolved.displayCity);
|
setDisplayCity(resolved.displayCity);
|
||||||
setCityCode(getCityCodeForCatalog(resolved));
|
setCityCode(getCityCodeForCatalog(resolved));
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
type MiniWechatProfile,
|
type MiniWechatProfile,
|
||||||
} from '../../lib/mini-wechat-profile';
|
} from '../../lib/mini-wechat-profile';
|
||||||
import { isLoggedIn, request, saveAuth, toast, type SessionPayload } from '../../lib/api';
|
import { isLoggedIn, request, saveAuth, toast, type SessionPayload } from '../../lib/api';
|
||||||
|
import { touchStoredPromoAfterLogin } from '../../lib/promo';
|
||||||
|
|
||||||
const IS_WEAPP = process.env.TARO_ENV === 'weapp';
|
const IS_WEAPP = process.env.TARO_ENV === 'weapp';
|
||||||
|
|
||||||
@@ -176,6 +177,7 @@ export default function LoginPage() {
|
|||||||
refreshToken: data.refreshToken,
|
refreshToken: data.refreshToken,
|
||||||
});
|
});
|
||||||
void syncMiniWechatProfile(wxInfo ?? getCachedWxProfile());
|
void syncMiniWechatProfile(wxInfo ?? getCachedWxProfile());
|
||||||
|
void touchStoredPromoAfterLogin();
|
||||||
if (!phoneValue) {
|
if (!phoneValue) {
|
||||||
void fetchUserProfile()
|
void fetchUserProfile()
|
||||||
.then((me) => resolveDefaultUserPhone(me))
|
.then((me) => resolveDefaultUserPhone(me))
|
||||||
|
|||||||
@@ -45,12 +45,16 @@ export type PromoCodeItem = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type PromoCodeStats = {
|
export type PromoCodeStats = {
|
||||||
|
/** 扫码进入次数 */
|
||||||
scanCount: number;
|
scanCount: number;
|
||||||
orderCount: number;
|
orderCount: number;
|
||||||
conversionRate: number;
|
conversionRate: number;
|
||||||
|
/** 归因用户数(user_promo_attribution) */
|
||||||
attributionCount?: number;
|
attributionCount?: number;
|
||||||
/** 用户表 source_ref_id 指向本推广码的用户数 */
|
/** 扫码注册用户数:用户来源标记为本推广码 */
|
||||||
sourceMarkedCount?: number;
|
sourceMarkedCount?: number;
|
||||||
|
/** @deprecated 同 sourceMarkedCount,兼容旧字段名 */
|
||||||
|
registerCount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PromoCodeAttributedUser = {
|
export type PromoCodeAttributedUser = {
|
||||||
|
|||||||
@@ -26,7 +26,12 @@ export class PromoController {
|
|||||||
touch(@CurrentUser() user: AuthUser | undefined, @Body() dto: PromoTouchDto) {
|
touch(@CurrentUser() user: AuthUser | undefined, @Body() dto: PromoTouchDto) {
|
||||||
const userId = user?.actorType === ActorType.USER ? user.actorId : undefined;
|
const userId = user?.actorType === ActorType.USER ? user.actorId : undefined;
|
||||||
return this.promoCodeService.touch(
|
return this.promoCodeService.touch(
|
||||||
{ promoCode: dto.promoCode, qrcodeId: dto.qrcodeId, promoId: dto.promoId },
|
{
|
||||||
|
promoCode: dto.promoCode,
|
||||||
|
qrcodeId: dto.qrcodeId,
|
||||||
|
promoId: dto.promoId,
|
||||||
|
countScan: dto.countScan,
|
||||||
|
},
|
||||||
userId,
|
userId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { IsOptional, IsString } from 'class-validator';
|
import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
export class PromoTouchDto {
|
export class PromoTouchDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -13,4 +14,17 @@ export class PromoTouchDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
promoId?: string;
|
promoId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否累加扫码次数。扫码进入为 true;登录后归因可传 false,避免重复计数。
|
||||||
|
* 默认 true。
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => {
|
||||||
|
if (value === false || value === 'false' || value === 0 || value === '0') return false;
|
||||||
|
if (value === true || value === 'true' || value === 1 || value === '1') return true;
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
@IsBoolean()
|
||||||
|
countScan?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ export class CreatePromoCodeDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(256)
|
@MaxLength(256)
|
||||||
remark?: string;
|
remark?: string;
|
||||||
|
|
||||||
|
/** 小程序码落地页路径,如 pages/home/index;留空用环境变量 WX_MINI_PROMO_PAGE */
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(128)
|
||||||
|
page?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdatePromoCodeDto {
|
export class UpdatePromoCodeDto {
|
||||||
|
|||||||
@@ -235,14 +235,19 @@ export class PromoCodeService {
|
|||||||
/**
|
/**
|
||||||
* 调用微信 getwxacodeunlimit,scene=推广活动 ID,PNG 上传 OSS uploads/qrcode/
|
* 调用微信 getwxacodeunlimit,scene=推广活动 ID,PNG 上传 OSS uploads/qrcode/
|
||||||
*/
|
*/
|
||||||
private async createQrcodeResource(promoId: bigint, code: string) {
|
private async createQrcodeResource(promoId: bigint, code: string, pagePath?: string) {
|
||||||
const scene = promoId.toString();
|
const scene = promoId.toString();
|
||||||
if (scene.length > 32) {
|
if (scene.length > 32) {
|
||||||
throw new BadRequestException('推广活动 ID 过长,无法写入小程序码 scene');
|
throw new BadRequestException('推广活动 ID 过长,无法写入小程序码 scene');
|
||||||
}
|
}
|
||||||
|
const page = (
|
||||||
|
pagePath?.trim() ||
|
||||||
|
process.env.WX_MINI_PROMO_PAGE ||
|
||||||
|
'pages/home/index'
|
||||||
|
).replace(/^\//, '');
|
||||||
const pngBuffer = await this.wechat.getWxaCodeUnlimited({
|
const pngBuffer = await this.wechat.getWxaCodeUnlimited({
|
||||||
scene,
|
scene,
|
||||||
page: process.env.WX_MINI_PROMO_PAGE || 'pages/home/index',
|
page,
|
||||||
width: 430,
|
width: 430,
|
||||||
checkPath: false,
|
checkPath: false,
|
||||||
});
|
});
|
||||||
@@ -290,7 +295,7 @@ export class PromoCodeService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resource = await this.createQrcodeResource(row.id, code);
|
const resource = await this.createQrcodeResource(row.id, code, dto.page);
|
||||||
const updated = await this.prisma.commonPromoCode.update({
|
const updated = await this.prisma.commonPromoCode.update({
|
||||||
where: { id: row.id },
|
where: { id: row.id },
|
||||||
data: { qrcodeResourceId: resource.id },
|
data: { qrcodeResourceId: resource.id },
|
||||||
@@ -380,7 +385,13 @@ export class PromoCodeService {
|
|||||||
|
|
||||||
/** C 端扫码/带参进入:累加 scan_count、归因、标记用户来源 */
|
/** C 端扫码/带参进入:累加 scan_count、归因、标记用户来源 */
|
||||||
async touch(
|
async touch(
|
||||||
input: { promoCode?: string; qrcodeId?: string; promoId?: string },
|
input: {
|
||||||
|
promoCode?: string;
|
||||||
|
qrcodeId?: string;
|
||||||
|
promoId?: string;
|
||||||
|
/** 默认 true;登录后归因传 false 避免重复计扫码 */
|
||||||
|
countScan?: boolean;
|
||||||
|
},
|
||||||
userId?: bigint,
|
userId?: bigint,
|
||||||
) {
|
) {
|
||||||
const promoCode = input.promoCode?.trim().toUpperCase();
|
const promoCode = input.promoCode?.trim().toUpperCase();
|
||||||
@@ -395,10 +406,13 @@ export class PromoCodeService {
|
|||||||
throw new NotFoundException('推广码无效或已停用');
|
throw new NotFoundException('推广码无效或已停用');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prisma.commonPromoCode.update({
|
const shouldCountScan = input.countScan !== false;
|
||||||
where: { id: promo.id },
|
if (shouldCountScan) {
|
||||||
data: { scanCount: { increment: 1 } },
|
await this.prisma.commonPromoCode.update({
|
||||||
});
|
where: { id: promo.id },
|
||||||
|
data: { scanCount: { increment: 1 } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let attributed = false;
|
let attributed = false;
|
||||||
let sourceApplied = false;
|
let sourceApplied = false;
|
||||||
@@ -428,6 +442,7 @@ export class PromoCodeService {
|
|||||||
channelName: promo.name,
|
channelName: promo.name,
|
||||||
attributed,
|
attributed,
|
||||||
sourceApplied,
|
sourceApplied,
|
||||||
|
scanCounted: shouldCountScan,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,7 +487,14 @@ export class PromoCodeService {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
return { scanCount, orderCount, conversionRate, attributionCount, sourceMarkedCount };
|
return {
|
||||||
|
scanCount,
|
||||||
|
orderCount,
|
||||||
|
conversionRate,
|
||||||
|
attributionCount,
|
||||||
|
sourceMarkedCount,
|
||||||
|
registerCount: sourceMarkedCount,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 推广码关联用户:归因记录或用户来源指向本码 */
|
/** 推广码关联用户:归因记录或用户来源指向本码 */
|
||||||
|
|||||||
Reference in New Issue
Block a user