feat(mini): 小程序端邀请归因功能
- utils/invite.js: 邀请API封装(我的邀请码/小程序码/分享记录/扫码记录/邀请列表) - utils/auth.js: wxLogin自动从启动参数提取inviter,登录时关联邀请人 - app.js: 启动时缓存邀请人参数(小程序码scene或分享query) - 测评结果页: 分享带邀请人参数,分享卡片用后端动态小程序码替换静态二维码 - 分享时记录分享行为(share_app/share_timeline)
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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 画占位
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user