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); +}