feat(mini): 小程序端邀请归因功能

- utils/invite.js: 邀请API封装(我的邀请码/小程序码/分享记录/扫码记录/邀请列表)
- utils/auth.js: wxLogin自动从启动参数提取inviter,登录时关联邀请人
- app.js: 启动时缓存邀请人参数(小程序码scene或分享query)
- 测评结果页: 分享带邀请人参数,分享卡片用后端动态小程序码替换静态二维码
- 分享时记录分享行为(share_app/share_timeline)
This commit is contained in:
Pine
2026-09-02 02:18:51 +08:00
parent e5166c4061
commit ff0f5e6ed5
4 changed files with 118 additions and 8 deletions
+36
View File
@@ -1,8 +1,44 @@
import { Component } from 'react';
import { View } from '@tarojs/components';
import Taro from '@tarojs/taro';
import './app.scss';
class App extends Component {
componentDidMount() {
// 启动时缓存邀请人参数(小程序码 scene 或分享 query),登录时自动关联
try {
const opts = Taro.getLaunchOptionsSync?.() || {};
const scene = opts.scene || '';
const query = opts.query || {};
let inviter = '';
if (scene && typeof scene === 'string' && scene.includes('i=')) {
const match = scene.match(/i=([^&]+)/);
if (match) inviter = match[1];
}
if (!inviter && query.inviter) inviter = query.inviter;
if (!inviter && query.i) inviter = query.i;
if (inviter) {
Taro.setStorageSync('pending_inviter', inviter);
}
} catch (e) {
// ignore
}
}
componentDidShow() {
// 每次显示时也检查邀请参数(通过分享卡片进入时)
try {
const opts = Taro.getLaunchOptionsSync?.() || {};
const query = opts.query || {};
const inviter = query.inviter || query.i || '';
if (inviter && !Taro.getStorageSync('pending_inviter')) {
Taro.setStorageSync('pending_inviter', inviter);
}
} catch (e) {
// ignore
}
}
render() {
return (
<View className="app">
+27 -6
View File
@@ -4,9 +4,9 @@ import { useState, useRef } from 'react';
import { useTabIndex } from '@/utils/tab';
import { request } from '@/utils/api';
import { getUser } from '@/utils/auth';
import { getInviteQrcode, recordShare } from '@/utils/invite';
import './index.scss';
import { PageHeader } from '@/components/TopInset';
import qrcodeImg from '@/assets/qrcode-test.png';
const adaptColor = (s) => (s >= 78 ? '#a8f0c8' : s >= 55 ? '#f5d9a0' : '#f0b8b8');
@@ -27,20 +27,30 @@ export default function Test() {
/* 分享配置:微信好友 */
useShareAppMessage(() => {
if (stage !== 'result' || !result) return { title: 'OPC 创业基因测评', path: '/pages-extra/test/index' };
const user = getUser() || {};
const inviter = user.invite_code || user.id || '';
if (stage !== 'result' || !result) {
return { title: 'OPC 创业基因测评', path: `/pages-extra/test/index?inviter=${inviter}` };
}
const persona = result.persona || {};
// 记录分享
recordShare({ channel: 'share_app', scene: 'test_result' }).catch(() => {});
return {
title: `我的 OPC 画像是 ${result.typeCode} · ${persona.name || ''},契合度 ${result.adaptIndex}`,
path: '/pages-extra/test/index',
path: `/pages-extra/test/index?inviter=${inviter}`,
};
});
/* 分享配置:朋友圈 */
useShareTimeline(() => {
if (stage !== 'result' || !result) return { title: 'OPC 创业基因测评' };
const user = getUser() || {};
const inviter = user.invite_code || user.id || '';
if (stage !== 'result' || !result) return { title: 'OPC 创业基因测评', query: `inviter=${inviter}` };
const persona = result.persona || {};
recordShare({ channel: 'share_timeline', scene: 'test_result' }).catch(() => {});
return {
title: `我的 OPC 画像是 ${result.typeCode} · ${persona.name || ''},契合度 ${result.adaptIndex}`,
query: `inviter=${inviter}`,
};
});
@@ -174,7 +184,7 @@ export default function Test() {
ctx.setFillStyle('#a3abc9');
ctx.fillText('云超服 OPC · 为进化而设计', 200, qrY + 105);
// 绘制二维码图片
// 绘制二维码图片(从后端获取带邀请参数的小程序码)
return new Promise((resolve) => {
const img = Taro.createImage();
img.onload = () => {
@@ -192,7 +202,18 @@ export default function Test() {
ctx.draw();
setTimeout(resolve, 300);
};
img.src = qrcodeImg;
// 先尝试从后端获取动态小程序码,失败则用占位
getInviteQrcode({ scene: 'test_result', page: 'pages-extra/test/index', env: 'trial' })
.then((res) => {
if (res && res.image) {
img.src = res.image; // base64 data URL
} else {
img.src = ''; // 触发 onerror 画占位
}
})
.catch(() => {
img.src = ''; // 触发 onerror 画占位
});
});
};
+29 -2
View File
@@ -54,12 +54,39 @@ export function logout() {
}
/* ---------------- 微信登录 / 我的资料(平台 /auth ---------------- */
/** 从启动参数中提取邀请人(小程序码 scene 格式:i=邀请码&s=场景) */
function _extractInviterFromLaunch() {
try {
const opts = Taro.getLaunchOptionsSync?.() || {};
const scene = opts.scene || '';
const query = opts.query || {};
// 小程序码 scene 参数
if (scene && typeof scene === 'string' && scene.includes('i=')) {
const match = scene.match(/i=([^&]+)/);
if (match) return match[1];
}
// query 中的 inviter 参数
if (query.inviter) return query.inviter;
if (query.i) return query.i;
// 缓存的邀请人(扫码后未登录时缓存)
const cached = Taro.getStorageSync('pending_inviter');
if (cached) return cached;
return '';
} catch (e) {
return '';
}
}
/** 微信一键登录:wx.login 拿 code → 平台 /auth/wx-login(需后端配 PINEAGENTS_WECHAT_LOGIN=true */
export async function wxLogin({ code, nickName, avatarUrl }) {
const data = await request('POST', '/auth/wx-login', { code, nickName, avatarUrl });
export async function wxLogin({ code, nickName, avatarUrl, inviter = '', scene = '' }) {
// 自动从启动参数提取邀请人(调用方未显式传入时)
const finalInviter = inviter || _extractInviterFromLaunch();
const data = await request('POST', '/auth/wx-login', { code, nickName, avatarUrl, inviter: finalInviter, scene });
if (data.token) {
Taro.setStorageSync('pine_token', data.token);
saveUser(data);
// 登录成功后清除缓存的邀请人
Taro.removeStorageSync('pending_inviter');
return data;
}
return data;
+26
View File
@@ -0,0 +1,26 @@
import { request } from './api';
/** 获取我的邀请码和统计 */
export async function getMyInvite() {
return request('GET', '/invite/me', {}, true);
}
/** 生成带邀请参数的小程序码 */
export async function getInviteQrcode({ scene = 'home', page = 'pages/index/index', env = 'trial', width = 430 } = {}) {
return request('GET', `/invite/qrcode?scene=${scene}&page=${encodeURIComponent(page)}&env=${env}&width=${width}`, {}, true);
}
/** 记录分享行为 */
export async function recordShare({ channel = 'share_app', scene = '', env = 'trial' } = {}) {
return request('POST', '/invite/share', { channel, scene, env }, true);
}
/** 记录扫码行为(无需登录) */
export async function recordScan({ inviter, scene = '', env = 'trial' } = {}) {
return request('POST', '/invite/scan', { inviter, scene, env });
}
/** 获取我邀请的用户列表 */
export async function getInvitees() {
return request('GET', '/invite/invitees', {}, true);
}