增强用户隐私保护,新增昵称授权功能。更新头像选择逻辑,用户可直接通过按钮选择微信头像或相册图片。优化个人资料页面,强化手机号必填提示,提升用户体验。调整多个页面以支持新隐私授权机制,确保符合最新隐私规范。
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user