forked from gxwebsoft/mp-10550
- 切换API基础URL到生产环境地址 - 在商品详情页添加邀请参数解析和存储逻辑 - 实现分享链接携带邀请者ID和来源信息 - 新增商品分享来源类型标识 - 在短信登录成功后处理待绑定的邀请关系 - 添加邀请关系跟踪和统计功能
486 lines
13 KiB
TypeScript
486 lines
13 KiB
TypeScript
import Taro from '@tarojs/taro'
|
||
import { bindRefereeRelation } from '@/api/invite'
|
||
|
||
/**
|
||
* 邀请参数接口
|
||
*/
|
||
export interface InviteParams {
|
||
inviter?: string;
|
||
source?: string;
|
||
t?: string;
|
||
}
|
||
|
||
/**
|
||
* 解析小程序启动参数中的邀请信息
|
||
*/
|
||
export function parseInviteParams(options: any): InviteParams | null {
|
||
try {
|
||
// 优先从 query.scene 参数中解析邀请信息
|
||
let sceneStr = null
|
||
if (options.query && options.query.scene) {
|
||
sceneStr = typeof options.query.scene === 'string' ? options.query.scene : String(options.query.scene)
|
||
} else if (options.scene) {
|
||
// 兼容直接从 scene 参数解析
|
||
sceneStr = typeof options.scene === 'string' ? options.scene : String(options.scene)
|
||
}
|
||
|
||
// 从 scene 参数中解析邀请信息
|
||
if (sceneStr) {
|
||
// 处理 uid_xxx 格式的邀请码
|
||
if (sceneStr.startsWith('uid_')) {
|
||
const inviterId = sceneStr.replace('uid_', '')
|
||
|
||
if (inviterId && !isNaN(parseInt(inviterId))) {
|
||
return {
|
||
inviter: inviterId,
|
||
source: 'qrcode',
|
||
t: Date.now().toString()
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理传统的 key=value&key=value 格式
|
||
const params: InviteParams = {}
|
||
const pairs = sceneStr.split('&')
|
||
|
||
pairs.forEach((pair: string) => {
|
||
const [key, value] = pair.split('=')
|
||
if (key && value) {
|
||
switch (key) {
|
||
case 'inviter':
|
||
params.inviter = decodeURIComponent(value)
|
||
break
|
||
case 'source':
|
||
params.source = decodeURIComponent(value)
|
||
break
|
||
case 't':
|
||
params.t = decodeURIComponent(value)
|
||
break
|
||
}
|
||
}
|
||
})
|
||
|
||
if (params.inviter) {
|
||
return params
|
||
}
|
||
}
|
||
|
||
// 从 query 参数中解析邀请信息(处理首页分享链接)
|
||
if (options.query) {
|
||
const query = options.query
|
||
if (query.inviter) {
|
||
return {
|
||
inviter: query.inviter,
|
||
source: query.source || 'share',
|
||
t: query.t
|
||
}
|
||
}
|
||
|
||
// 兼容旧版本
|
||
if (query.referrer) {
|
||
return {
|
||
inviter: query.referrer,
|
||
source: 'link'
|
||
}
|
||
}
|
||
}
|
||
|
||
return null
|
||
} catch (error) {
|
||
console.error('解析邀请参数失败:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 保存邀请信息到本地存储
|
||
*/
|
||
export function saveInviteParams(params: InviteParams) {
|
||
try {
|
||
const saveData = {
|
||
...params,
|
||
timestamp: Date.now()
|
||
}
|
||
|
||
Taro.setStorageSync('invite_params', saveData)
|
||
} catch (error) {
|
||
console.error('保存邀请参数失败:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取本地存储的邀请信息
|
||
*/
|
||
export function getStoredInviteParams(): InviteParams | null {
|
||
try {
|
||
const stored = Taro.getStorageSync('invite_params')
|
||
|
||
if (stored && stored.inviter) {
|
||
// 检查是否过期(24小时)
|
||
const now = Date.now()
|
||
const expireTime = 24 * 60 * 60 * 1000 // 24小时
|
||
|
||
if (now - stored.timestamp < expireTime) {
|
||
return {
|
||
inviter: stored.inviter,
|
||
source: stored.source || 'unknown',
|
||
t: stored.t
|
||
}
|
||
} else {
|
||
// 过期则清除
|
||
clearInviteParams()
|
||
}
|
||
}
|
||
return null
|
||
} catch (error) {
|
||
console.error('获取邀请参数失败:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清除本地存储的邀请信息
|
||
*/
|
||
export function clearInviteParams() {
|
||
try {
|
||
Taro.removeStorageSync('invite_params')
|
||
} catch (error) {
|
||
console.error('清除邀请参数失败:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理邀请关系建立
|
||
*/
|
||
export async function handleInviteRelation(userId: number): Promise<boolean> {
|
||
try {
|
||
const inviteParams = getStoredInviteParams()
|
||
if (!inviteParams || !inviteParams.inviter) {
|
||
return false
|
||
}
|
||
|
||
const inviterId = parseInt(inviteParams.inviter)
|
||
if (isNaN(inviterId) || inviterId === userId) {
|
||
// 邀请人ID无效或自己邀请自己
|
||
clearInviteParams()
|
||
return false
|
||
}
|
||
|
||
// 防重复检查:检查是否已经处理过这个邀请关系
|
||
const relationKey = `invite_relation_${inviterId}_${userId}`
|
||
const existingRelation = Taro.getStorageSync(relationKey)
|
||
|
||
if (existingRelation) {
|
||
clearInviteParams() // 清除邀请参数
|
||
return true // 返回true表示关系已存在
|
||
}
|
||
|
||
// 设置API调用超时
|
||
const timeoutPromise = new Promise((_, reject) =>
|
||
setTimeout(() => reject(new Error('API调用超时')), 5000)
|
||
);
|
||
|
||
// 使用新的绑定推荐关系接口
|
||
const apiPromise = bindRefereeRelation({
|
||
dealerId: inviterId,
|
||
userId: userId,
|
||
source: inviteParams.source || 'qrcode',
|
||
scene: inviteParams.source === 'qrcode' ? `uid_${inviterId}` : `inviter=${inviterId}&source=${inviteParams.source}&t=${inviteParams.t}`
|
||
});
|
||
|
||
// 等待API调用完成或超时
|
||
await Promise.race([apiPromise, timeoutPromise]);
|
||
|
||
// 标记邀请关系已处理(设置过期时间为7天)
|
||
Taro.setStorageSync(relationKey, {
|
||
inviterId,
|
||
userId,
|
||
timestamp: Date.now(),
|
||
source: inviteParams.source || 'qrcode'
|
||
})
|
||
|
||
// 清除本地存储的邀请参数
|
||
clearInviteParams()
|
||
|
||
return true
|
||
} catch (error) {
|
||
console.error('建立邀请关系失败:', error)
|
||
|
||
// 如果是网络错误或超时,不清除邀请参数,允许稍后重试
|
||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||
if (errorMessage.includes('超时') || errorMessage.includes('网络')) {
|
||
console.log('网络问题,保留邀请参数供稍后重试')
|
||
return false
|
||
}
|
||
|
||
// 其他错误(如业务逻辑错误),清除邀请参数
|
||
clearInviteParams()
|
||
return false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查是否有待处理的邀请
|
||
*/
|
||
export function hasPendingInvite(): boolean {
|
||
const params = getStoredInviteParams()
|
||
return !!(params && params.inviter)
|
||
}
|
||
|
||
/**
|
||
* 获取邀请来源的显示名称
|
||
*/
|
||
export function getSourceDisplayName(source: string): string {
|
||
const sourceMap: Record<string, string> = {
|
||
'qrcode': '小程序码',
|
||
'link': '分享链接',
|
||
'share': '好友分享',
|
||
'goods_share': '商品分享',
|
||
'poster': '海报分享',
|
||
'unknown': '未知来源'
|
||
}
|
||
|
||
return sourceMap[source] || source
|
||
}
|
||
|
||
/**
|
||
* 验证邀请码格式
|
||
*/
|
||
export function validateInviteCode(scene: string): boolean {
|
||
try {
|
||
if (!scene) return false
|
||
|
||
// 检查是否包含必要的参数
|
||
const hasInviter = scene.includes('inviter=')
|
||
const hasSource = scene.includes('source=')
|
||
|
||
return hasInviter && hasSource
|
||
} catch (error) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成邀请场景值
|
||
*/
|
||
export function generateInviteScene(inviterId: number, source: string): string {
|
||
const timestamp = Date.now()
|
||
return `inviter=${inviterId}&source=${source}&t=${timestamp}`
|
||
}
|
||
|
||
/**
|
||
* 统计邀请来源
|
||
*/
|
||
export function trackInviteSource(source: string, inviterId?: number) {
|
||
try {
|
||
// 记录邀请来源统计
|
||
const trackData = {
|
||
source,
|
||
inviterId,
|
||
timestamp: Date.now(),
|
||
userAgent: Taro.getSystemInfoSync()
|
||
}
|
||
|
||
// 可以发送到统计服务
|
||
console.log('邀请来源统计:', trackData)
|
||
|
||
// 暂存到本地,后续可批量上报
|
||
const existingTracks = Taro.getStorageSync('invite_tracks') || []
|
||
existingTracks.push(trackData)
|
||
|
||
// 只保留最近100条记录
|
||
if (existingTracks.length > 100) {
|
||
existingTracks.splice(0, existingTracks.length - 100)
|
||
}
|
||
|
||
Taro.setStorageSync('invite_tracks', existingTracks)
|
||
} catch (error) {
|
||
console.error('统计邀请来源失败:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 调试工具:打印所有邀请相关的存储信息
|
||
*/
|
||
export function debugInviteInfo() {
|
||
try {
|
||
console.log('=== 邀请参数调试信息 ===')
|
||
|
||
// 获取启动参数
|
||
const launchOptions = Taro.getLaunchOptionsSync()
|
||
console.log('启动参数:', JSON.stringify(launchOptions, null, 2))
|
||
|
||
// 获取存储的邀请参数
|
||
const storedParams = Taro.getStorageSync('invite_params')
|
||
console.log('存储的邀请参数:', JSON.stringify(storedParams, null, 2))
|
||
|
||
// 获取用户信息
|
||
const userId = Taro.getStorageSync('UserId')
|
||
const userInfo = Taro.getStorageSync('userInfo')
|
||
console.log('用户ID:', userId)
|
||
console.log('用户信息:', JSON.stringify(userInfo, null, 2))
|
||
|
||
// 获取邀请统计
|
||
const inviteTracks = Taro.getStorageSync('invite_tracks')
|
||
console.log('邀请统计:', JSON.stringify(inviteTracks, null, 2))
|
||
|
||
console.log('=== 调试信息结束 ===')
|
||
|
||
return {
|
||
launchOptions,
|
||
storedParams,
|
||
userId,
|
||
userInfo,
|
||
inviteTracks
|
||
}
|
||
} catch (error) {
|
||
console.error('获取调试信息失败:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查并处理当前用户的邀请关系
|
||
* 用于在用户登录后立即检查是否需要建立邀请关系
|
||
*/
|
||
export async function checkAndHandleInviteRelation(): Promise<boolean> {
|
||
try {
|
||
// 清理过期的防重记录
|
||
cleanExpiredInviteRelations()
|
||
|
||
// 获取当前用户信息
|
||
const userInfo = Taro.getStorageSync('userInfo')
|
||
const userId = Taro.getStorageSync('UserId')
|
||
|
||
const finalUserId = userId || userInfo?.userId
|
||
|
||
if (!finalUserId) {
|
||
console.log('用户未登录,无法处理邀请关系')
|
||
return false
|
||
}
|
||
|
||
console.log('使用用户ID处理邀请关系:', finalUserId)
|
||
|
||
// 设置整体超时保护
|
||
const timeoutPromise = new Promise<boolean>((_, reject) =>
|
||
setTimeout(() => reject(new Error('邀请关系处理整体超时')), 6000)
|
||
);
|
||
|
||
const handlePromise = handleInviteRelation(parseInt(finalUserId));
|
||
|
||
return await Promise.race([handlePromise, timeoutPromise]);
|
||
} catch (error) {
|
||
console.error('检查邀请关系失败:', error)
|
||
|
||
// 记录失败次数,避免无限重试
|
||
const failKey = 'invite_handle_fail_count'
|
||
const failCount = Taro.getStorageSync(failKey) || 0
|
||
|
||
if (failCount >= 3) {
|
||
console.log('邀请关系处理失败次数过多,清除邀请参数')
|
||
clearInviteParams()
|
||
Taro.removeStorageSync(failKey)
|
||
} else {
|
||
Taro.setStorageSync(failKey, failCount + 1)
|
||
}
|
||
|
||
return false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 手动触发邀请关系建立
|
||
* 用于在特定页面或时机手动建立邀请关系
|
||
*/
|
||
export async function manualHandleInviteRelation(userId: number): Promise<boolean> {
|
||
try {
|
||
console.log('手动触发邀请关系建立,用户ID:', userId)
|
||
|
||
const inviteParams = getStoredInviteParams()
|
||
if (!inviteParams || !inviteParams.inviter) {
|
||
console.log('没有待处理的邀请参数')
|
||
return false
|
||
}
|
||
|
||
const result = await handleInviteRelation(userId)
|
||
|
||
if (result) {
|
||
// 显示成功提示
|
||
Taro.showModal({
|
||
title: '邀请成功',
|
||
content: '您已成功加入邀请人的团队!',
|
||
showCancel: false,
|
||
confirmText: '知道了'
|
||
})
|
||
}
|
||
|
||
return result
|
||
} catch (error) {
|
||
console.error('手动处理邀请关系失败:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清理过期的邀请关系防重记录
|
||
*/
|
||
export function cleanExpiredInviteRelations() {
|
||
try {
|
||
const keys = Taro.getStorageInfoSync().keys
|
||
const expireTime = 7 * 24 * 60 * 60 * 1000 // 7天
|
||
const now = Date.now()
|
||
|
||
keys.forEach(key => {
|
||
if (key.startsWith('invite_relation_')) {
|
||
try {
|
||
const data = Taro.getStorageSync(key)
|
||
if (data && data.timestamp && (now - data.timestamp > expireTime)) {
|
||
Taro.removeStorageSync(key)
|
||
}
|
||
} catch (error) {
|
||
// 如果读取失败,直接删除
|
||
Taro.removeStorageSync(key)
|
||
}
|
||
}
|
||
})
|
||
} catch (error) {
|
||
console.error('清理过期邀请关系记录失败:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 直接绑定推荐关系
|
||
* 用于直接调用绑定推荐关系接口
|
||
*/
|
||
export async function bindReferee(refereeId: number, userId?: number, source: string = 'qrcode'): Promise<boolean> {
|
||
try {
|
||
// 如果没有传入userId,尝试从本地存储获取
|
||
let targetUserId = userId
|
||
if (!targetUserId) {
|
||
const userInfo = Taro.getStorageSync('userInfo')
|
||
if (userInfo && userInfo.userId) {
|
||
targetUserId = userInfo.userId
|
||
} else {
|
||
throw new Error('无法获取用户ID')
|
||
}
|
||
}
|
||
|
||
// 防止自己推荐自己
|
||
if (refereeId === targetUserId) {
|
||
throw new Error('不能推荐自己')
|
||
}
|
||
|
||
await bindRefereeRelation({
|
||
dealerId: refereeId,
|
||
userId: targetUserId,
|
||
source: source,
|
||
scene: source === 'qrcode' ? `uid_${refereeId}` : undefined
|
||
})
|
||
|
||
return true
|
||
} catch (error: any) {
|
||
console.error('绑定推荐关系失败:', error)
|
||
return false
|
||
}
|
||
}
|