增强用户隐私保护,新增昵称授权功能。更新头像选择逻辑,用户可直接通过按钮选择微信头像或相册图片。优化个人资料页面,强化手机号必填提示,提升用户体验。调整多个页面以支持新隐私授权机制,确保符合最新隐私规范。

This commit is contained in:
Alex-larget
2026-03-19 18:26:45 +08:00
parent 35aecdcd8c
commit 588dad2518
124 changed files with 4093 additions and 38827 deletions

View File

@@ -80,10 +80,28 @@ App({
supportWechat: '',
// config 统一缓存5min减少重复请求
configCache: null,
configCacheExpires: 0
configCacheExpires: 0,
// VIP 联系方式检测上次检测时间戳onShow 节流 5 分钟
lastVipContactCheck: 0,
// 头像昵称检测上次检测时间戳onShow 节流 5 分钟
lastAvatarNicknameCheck: 0,
},
onLaunch(options) {
// 昵称等隐私组件需先授权input type="nickname" 不会主动触发,需配合 wx.requirePrivacyAuthorize 使用
if (typeof wx.onNeedPrivacyAuthorization === 'function') {
wx.onNeedPrivacyAuthorization((resolve) => {
this._privacyResolve = resolve
const pages = getCurrentPages()
const cur = pages[pages.length - 1]
if (cur && typeof cur.setData === 'function' && cur.route && (cur.route.includes('avatar-nickname') || cur.route.includes('profile-edit'))) {
cur.setData({ showPrivacyModal: true })
} else {
resolve({ event: 'disagree' })
}
})
}
this.globalData.readSectionIds = wx.getStorageSync('readSectionIds') || []
// 加载 iconfont字体图标。注意小程序不支持在 wxss 里用本地 @font-face 引用字体文件,
// 需使用 loadFontFace 动态加载(字体文件建议走 https CDN
@@ -103,6 +121,11 @@ App({
// 检查登录状态
this.checkLoginStatus()
// 每次进入:先获取 VIP 状态VIP 走 profile-edit非 VIP 走头像/昵称引导(由 checkVipContactRequiredAndGuide 内部链式调用)
if (this.globalData.isLoggedIn && this.globalData.userInfo?.id) {
setTimeout(() => this.checkVipContactRequiredAndGuide(), 1500)
setTimeout(() => this.connectWsHeartbeat(), 2000)
}
// 加载书籍数据
this.loadBookData()
@@ -143,6 +166,23 @@ App({
this.globalData.lastMpConfigCheck = now
this.getAuditMode()
}
// 从后台切回:先 VIP 强制跳转,再头像/昵称,节流 5 分钟
const throttle = 5 * 60 * 1000
if (this.globalData.isLoggedIn && this.globalData.userInfo?.id) {
if (!this.globalData.lastVipContactCheck || now - this.globalData.lastVipContactCheck > throttle) {
this.globalData.lastVipContactCheck = now
this.globalData.lastAvatarNicknameCheck = now
setTimeout(() => this.checkVipContactRequiredAndGuide(), 500)
}
// 从后台切回:若 WSS 已断开则重连(微信后台可能回收连接)
try {
const need = !this._wsSocketTask || (this._wsSocketTask.readyState !== 0 && this._wsSocketTask.readyState !== 1)
if (need) {
this.clearWsReconnect()
setTimeout(() => this.connectWsHeartbeat(), 1000)
}
} catch (_) {}
}
},
// 处理推荐码绑定:官方以 options.scene 接收扫码参数(可同时带 mid/id + ref与 utils/scene 解析闭环
@@ -322,6 +362,44 @@ App({
return false
},
/** 判断头像/昵称是否未完善(默认状态) */
_needsAvatarNickname(user) {
const u = user || this.globalData.userInfo || {}
const avatar = (u.avatar || u.avatarUrl || '').trim()
const nickname = (u.nickname || u.nickName || '').trim()
return !avatar || avatar.includes('default') || !nickname || nickname === '微信用户' || nickname.startsWith('微信用户')
},
/**
* 头像/昵称未改则引导:老用户弹窗后跳 avatar-nickname新用户由登录处强制 redirectTo
* VIP 用户不在此处理,统一走 checkVipContactRequiredAndGuide 只跳 profile-edit避免乱跳
*/
checkAvatarNicknameAndGuide() {
if (!this.globalData.isLoggedIn || !this.globalData.userInfo?.id) return
if (this.globalData.isVip) return // VIP 统一走 profile-edit此处不触发
if (!this._needsAvatarNickname()) return
try {
const pages = getCurrentPages()
const last = pages[pages.length - 1]
const route = (last && last.route) || ''
if (route.indexOf('profile-edit') !== -1 || route.indexOf('avatar-nickname') !== -1) return
} catch (_) {}
// 老用户:弹窗提示后跳转
const today = new Date().toISOString().slice(0, 10)
const lastDate = wx.getStorageSync('lastAvatarGuideDate') || ''
if (lastDate === today) return
wx.setStorageSync('lastAvatarGuideDate', today)
wx.showModal({
title: '完善个人资料',
content: '请设置头像和昵称,让其他创业者更好地认识你',
confirmText: '去完善',
cancelText: '稍后',
success: (res) => {
if (res.confirm) wx.navigateTo({ url: '/pages/avatar-nickname/avatar-nickname' })
}
})
},
// 检查登录状态
checkLoginStatus() {
try {
@@ -341,6 +419,168 @@ App({
}
},
/**
* WSS 在线心跳(占位):登录后连接 ws发送 auth + 心跳,供管理端统计在线人数
* 容错任意异常均不向外抛出不影响登录、API 请求等核心功能
*/
clearWsReconnect() {
try {
if (this._wsReconnectTimerId) {
clearTimeout(this._wsReconnectTimerId)
this._wsReconnectTimerId = null
}
this._wsReconnectDelay = 3000
} catch (_) {}
},
scheduleWsReconnect() {
try {
if (!this.globalData.isLoggedIn || !this.globalData.userInfo?.id) return
if (this._wsReconnectTimerId) return
const delay = this._wsReconnectDelay || 3000
this._wsReconnectTimerId = setTimeout(() => {
this._wsReconnectTimerId = null
this._wsReconnectDelay = Math.min(60000, (this._wsReconnectDelay || 3000) * 2)
this.connectWsHeartbeat()
}, delay)
} catch (_) {}
},
connectWsHeartbeat() {
try {
this.clearWsReconnect()
if (!this.globalData.isLoggedIn || !this.globalData.userInfo?.id) return
const userId = this.globalData.userInfo.id
const base = (this.globalData.baseUrl || '').replace(/\/$/, '')
if (!base) return
const wsUrl = base.replace(/^http/, 'ws') + '/ws/miniprogram'
if (this._wsHeartbeatTimer) {
clearInterval(this._wsHeartbeatTimer)
this._wsHeartbeatTimer = null
}
if (this._wsSocketTask) {
try { this._wsSocketTask.close() } catch (_) {}
this._wsSocketTask = null
}
let task
try {
task = wx.connectSocket({
url: wsUrl,
fail: () => { try { this.scheduleWsReconnect() } catch (_) {} }
})
} catch (e) {
if (typeof console !== 'undefined' && console.warn) console.warn('[WS] 连接失败(静默):', e?.message || e)
try { this.scheduleWsReconnect() } catch (_) {}
return
}
task.onOpen(() => {
try {
this.clearWsReconnect()
task.send({ data: JSON.stringify({ type: 'auth', userId }) })
this._wsHeartbeatTimer = setInterval(() => {
try {
if (task && task.readyState === 1) task.send({ data: JSON.stringify({ type: 'heartbeat' }) })
} catch (_) {}
}, 30000)
} catch (_) {}
})
task.onClose(() => {
try {
if (this._wsHeartbeatTimer) { clearInterval(this._wsHeartbeatTimer); this._wsHeartbeatTimer = null }
this._wsSocketTask = null
this.scheduleWsReconnect()
} catch (_) {}
})
task.onError(() => {
try {
if (this._wsHeartbeatTimer) { clearInterval(this._wsHeartbeatTimer); this._wsHeartbeatTimer = null }
this._wsSocketTask = null
this.scheduleWsReconnect()
} catch (_) {}
})
this._wsSocketTask = task
} catch (e) {
if (typeof console !== 'undefined' && console.warn) console.warn('[WS] 心跳异常(静默,不影响业务):', e?.message || e)
}
},
/**
* VIP 用户登录后检测:手机号/头像昵称等需完善时,统一只跳 profile-edit避免与 avatar-nickname 乱跳。
* 旧数据VIP 但头像昵称未改):弹窗「为了更好服务,请完善资料」→ redirectTo profile-edit
*/
async checkVipContactRequiredAndGuide() {
if (!this.globalData.isLoggedIn || !this.globalData.userInfo?.id) return
const now = Date.now()
if (this._lastVipGuideRun && now - this._lastVipGuideRun < 3000) return // 3 秒内不重复执行,避免 onLaunch+onShow 双重触发
this._lastVipGuideRun = now
const userId = this.globalData.userInfo.id
try {
const pages = getCurrentPages()
const last = pages[pages.length - 1]
const route = (last && last.route) || ''
if (route.indexOf('profile-edit') !== -1 || route.indexOf('avatar-nickname') !== -1) return
} catch (_) {}
try {
const [vipRes, profileRes] = await Promise.all([
this.request({ url: `/api/miniprogram/vip/status?userId=${userId}`, silent: true }).catch(() => null),
this.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true }).catch(() => null)
])
const isVip = vipRes?.data?.isVip || this.globalData.isVip || false
this.globalData.isVip = isVip
if (!isVip) {
this.checkAvatarNicknameAndGuide()
return
}
const profileData = profileRes?.data || this.globalData.userInfo || {}
const phone = (profileData.phone || this.globalData.userInfo?.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
const wechatId = (profileData.wechatId || profileData.wechat_id || this.globalData.userInfo?.wechatId || this.globalData.userInfo?.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
const needsAvatarNickname = this._needsAvatarNickname(profileData)
// VIP 头像/昵称未改(含旧数据):统一只跳 profile-edit弹窗「为了更好服务请完善资料」
if (needsAvatarNickname) {
wx.showModal({
title: '完善资料',
content: '为了更好为您服务,请完善资料',
confirmText: '去完善',
showCancel: false,
success: () => {
wx.redirectTo({ url: '/pages/profile-edit/profile-edit' })
}
})
return
}
if (phone && wechatId) return
// VIP 无手机号:弹窗说明后跳转
if (!phone) {
wx.showModal({
title: '完善资料',
content: 'VIP会员需完善手机号以便使用找伙伴、提现等功能',
confirmText: '去完善',
showCancel: false,
success: () => {
wx.redirectTo({ url: '/pages/profile-edit/profile-edit' })
}
})
return
}
// 有手机号但缺微信号:弹窗引导(非强制)
wx.showModal({
title: '完善联系方式',
content: '请到资料页完善微信号,便于他人联系您',
confirmText: '去完善',
cancelText: '稍后',
success: (res) => {
if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
}
})
} catch (e) {
console.log('[App] checkVipContactRequiredAndGuide 失败:', e?.message)
}
},
// 加载书籍元数据totalSections不再预加载 all-chapters
async loadBookData() {
try {
@@ -659,8 +899,17 @@ App({
this.bindReferralCode(pendingRef)
}
// 登录后引导完善资料(规则引擎接管,完善头像吸收到规则引擎
checkAndExecute('after_login', null)
// 同步 isVip与 checkLoginStatus 一致
this.globalData.isVip = user.isVip || false
this.globalData.vipExpireDate = user.vipExpireDate || ''
// 首次登录注册:强制跳转 avatar-nickname 修改头像昵称(不弹窗)
if (res.isNewUser === true && this._needsAvatarNickname(user)) {
setTimeout(() => wx.redirectTo({ url: '/pages/avatar-nickname/avatar-nickname' }), 1000)
} else {
checkAndExecute('after_login', null)
setTimeout(() => this.checkVipContactRequiredAndGuide(), 1200)
setTimeout(() => this.connectWsHeartbeat(), 2000)
}
}
return res.data
@@ -721,8 +970,16 @@ App({
this.bindReferralCode(pendingRef)
}
// 登录后引导完善资料(规则引擎接管)
checkAndExecute('after_login', null)
// 同步 isVip
this.globalData.isVip = user.isVip || false
this.globalData.vipExpireDate = user.vipExpireDate || ''
// 首次登录注册:强制跳转 avatar-nickname
if (res.isNewUser === true && this._needsAvatarNickname(user)) {
setTimeout(() => wx.redirectTo({ url: '/pages/avatar-nickname/avatar-nickname' }), 1000)
} else {
checkAndExecute('after_login', null)
setTimeout(() => this.checkVipContactRequiredAndGuide(), 1200)
}
}
return res.data.openId
}
@@ -764,6 +1021,8 @@ App({
this.globalData.isLoggedIn = true
this.globalData.purchasedSections = user.purchasedSections || []
this.globalData.hasFullBook = user.hasFullBook || false
this.globalData.isVip = user.isVip || false
this.globalData.vipExpireDate = user.vipExpireDate || ''
wx.setStorageSync('userInfo', user)
wx.setStorageSync('token', res.data.token)
@@ -775,9 +1034,14 @@ App({
this.bindReferralCode(pendingRef)
}
// 登录后引导完善资料(规则引擎接管)
checkAndExecute('after_login', null)
// 首次登录注册:强制跳转 avatar-nickname
if (res.isNewUser === true && this._needsAvatarNickname(user)) {
setTimeout(() => wx.redirectTo({ url: '/pages/avatar-nickname/avatar-nickname' }), 1000)
} else {
checkAndExecute('after_login', null)
setTimeout(() => this.checkVipContactRequiredAndGuide(), 1200)
}
return res.data
}
} catch (e) {