From ff0f5e6ed54cabe300e0ff91eefc81d23fbd140c Mon Sep 17 00:00:00 2001 From: Pine Date: Wed, 2 Sep 2026 02:18:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(mini):=20=E5=B0=8F=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E7=AB=AF=E9=82=80=E8=AF=B7=E5=BD=92=E5=9B=A0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - utils/invite.js: 邀请API封装(我的邀请码/小程序码/分享记录/扫码记录/邀请列表) - utils/auth.js: wxLogin自动从启动参数提取inviter,登录时关联邀请人 - app.js: 启动时缓存邀请人参数(小程序码scene或分享query) - 测评结果页: 分享带邀请人参数,分享卡片用后端动态小程序码替换静态二维码 - 分享时记录分享行为(share_app/share_timeline) --- miniprogram/src/app.js | 36 ++++++++++++++++++++++ miniprogram/src/pages-extra/test/index.jsx | 33 ++++++++++++++++---- miniprogram/src/utils/auth.js | 31 +++++++++++++++++-- miniprogram/src/utils/invite.js | 26 ++++++++++++++++ 4 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 miniprogram/src/utils/invite.js diff --git a/miniprogram/src/app.js b/miniprogram/src/app.js index 1d0c91a..1194eda 100644 --- a/miniprogram/src/app.js +++ b/miniprogram/src/app.js @@ -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 ( diff --git a/miniprogram/src/pages-extra/test/index.jsx b/miniprogram/src/pages-extra/test/index.jsx index 52cbc2e..d04208a 100644 --- a/miniprogram/src/pages-extra/test/index.jsx +++ b/miniprogram/src/pages-extra/test/index.jsx @@ -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 画占位 + }); }); }; diff --git a/miniprogram/src/utils/auth.js b/miniprogram/src/utils/auth.js index e5d5250..7e5aa3c 100644 --- a/miniprogram/src/utils/auth.js +++ b/miniprogram/src/utils/auth.js @@ -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; diff --git a/miniprogram/src/utils/invite.js b/miniprogram/src/utils/invite.js new file mode 100644 index 0000000..a82c556 --- /dev/null +++ b/miniprogram/src/utils/invite.js @@ -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); +}