feat(assoc): v4.0.1 合伙人关联码、分佣账单与 H5 用户管理

订单佣金只认关联用户;合伙人备注写入独立表;H5 增加用户管理与首页统计。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-30 14:35:39 +08:00
parent 3b669f7e38
commit 9c8d5f2cad
125 changed files with 6355 additions and 1436 deletions
+74 -1
View File
@@ -1,7 +1,8 @@
import Taro from '@tarojs/taro';
import { request } from './api';
import { request, isLoggedIn, toast } from './api';
const PROMO_ID_KEY = 'dukang_promo_id';
const ASSOC_SCENE_KEY = 'dukang_partner_assoc_scene';
/** 同一次进入只 touch 一次扫码计数,避免首页反复 onShow 刷量 */
let lastScanTouchKey = '';
@@ -28,10 +29,54 @@ type EnterOptionsLike = {
path?: string;
};
function normalizeAssocScene(raw: unknown): string | null {
if (raw == null || raw === '') return null;
const s = safeDecode(String(raw)).trim();
return /^pa_\d+$/.test(s) ? s : null;
}
function extractAssocSceneFromEnterOptions(opts?: EnterOptionsLike | null): string | null {
if (!opts) return null;
const q = opts.query ?? {};
return (
normalizeAssocScene(q.scene) ||
normalizeAssocScene(opts.scene) ||
normalizeAssocScene(q.partnerId) ||
null
);
}
export function getStoredAssocScene(): string | null {
try {
return normalizeAssocScene(Taro.getStorageSync(ASSOC_SCENE_KEY));
} catch {
return null;
}
}
export function setStoredAssocScene(scene: string) {
const id = normalizeAssocScene(scene);
if (!id) return;
try {
Taro.setStorageSync(ASSOC_SCENE_KEY, id);
} catch {
/* ignore */
}
}
export function clearStoredAssocScene() {
try {
Taro.removeStorageSync(ASSOC_SCENE_KEY);
} catch {
/* ignore */
}
}
/** 从启动/进入参数解析推广活动 ID(优先 query.scene,与 getwxacodeunlimit 一致) */
export function extractPromoIdFromEnterOptions(opts?: EnterOptionsLike | null): string | null {
if (!opts) return null;
const q = opts.query ?? {};
if (extractAssocSceneFromEnterOptions(opts)) return null;
return (
normalizePromoId(q.scene) ||
normalizePromoId(q.promoId) ||
@@ -92,8 +137,35 @@ function readEnterOptions(): EnterOptionsLike | null {
* 主页面进入时:取出 scene(活动 ID)本地缓存,并回传 /promo/touch 累加扫码次数。
* 同一进入会话只计一次扫码。
*/
async function bindStoredAssocIfLoggedIn(): Promise<void> {
const scene = getStoredAssocScene();
if (!scene || !isLoggedIn()) return;
try {
const result = await request<{ alreadyBound?: boolean; bound?: boolean }>('/user/partner-assoc/bind', {
method: 'POST',
data: { scene },
});
clearStoredAssocScene();
if (result.alreadyBound) toast('已关联');
else if (result.bound) toast('关联成功', 'success');
} catch (e) {
const msg = e instanceof Error ? e.message : '';
if (msg.includes('已关联')) {
clearStoredAssocScene();
toast('已关联');
}
}
}
export async function capturePromoSceneAndTouchScan(): Promise<void> {
const opts = readEnterOptions();
const assocScene = extractAssocSceneFromEnterOptions(opts);
if (assocScene) {
setStoredAssocScene(assocScene);
await bindStoredAssocIfLoggedIn();
return;
}
const fromEnter = extractPromoIdFromEnterOptions(opts);
if (fromEnter) {
setStoredPromoId(fromEnter);
@@ -109,6 +181,7 @@ export async function capturePromoSceneAndTouchScan(): Promise<void> {
/** 登录成功后:用已缓存的活动 ID 做归因(不重复加扫码次数) */
export async function touchStoredPromoAfterLogin(): Promise<void> {
await bindStoredAssocIfLoggedIn();
const promoId = getStoredPromoId();
if (!promoId) return;
await touchPromo({ promoId, countScan: false });