feat: 小程序阅读记录与资料链路、管理端用户规则、API/VIP/推荐与运营脚本

- miniprogram: reading-records、imageUrl/mpNavigate、多页资料与 VIP 展示调整
- soul-admin: Users/Settings/UserDetailModal、dist 构建产物更新
- soul-api: user/vip/referral/ckb/db、MBTI 头像管理、user_rule_completion、迁移 SQL
- .cursor: karuo-party 与飞书文档;.gitignore 忽略 .tmp_skill_bundle

Made-with: Cursor
This commit is contained in:
卡若
2026-03-23 18:38:23 +08:00
parent cb6e2bff56
commit fa3da12b16
82 changed files with 5621 additions and 2723 deletions

View File

@@ -13,6 +13,9 @@ const { toAvatarPath } = require('../../utils/util.js')
const MBTI_OPTIONS = ['INTJ', 'INFP', 'INTP', 'ENTP', 'ENFP', 'ENTJ', 'ENFJ', 'INFJ', 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ', 'ISTP', 'ISFP', 'ESTP', 'ESFP']
/** 首次分步完善完成后写入;与手机号+昵称齐全时自动写入,老用户免向导 */
const PROFILE_WIZARD_DONE_KEY = 'profile_wizard_v1_done'
Page({
data: {
statusBarHeight: 44,
@@ -41,9 +44,17 @@ Page({
loading: true,
showPrivacyModal: false,
nicknameInputFocus: false,
/** 首次完善:分 3 步full=1 或未达标资料时为单页 */
wizardMode: false,
wizardStep: 1,
totalWizardSteps: 3,
},
onLoad(options) {
this._wizardRouteOpts = {
full: options?.full === '1',
wizardOff: options?.wizard === '0',
}
this.setData({
statusBarHeight: app.globalData.statusBarHeight || 44,
fromVip: options?.from === 'vip',
@@ -98,6 +109,7 @@ Page({
projectIntro: v('projectIntro'),
loading: false,
})
this._applyWizardModeFromProfile(d)
setTimeout(() => this.generateShareCard(), 200)
} else {
this.setData({ loading: false })
@@ -109,6 +121,49 @@ Page({
goBack() { getApp().goBackOrToHome() },
/**
* 是否走三步向导:资料未「手机号+昵称」齐全且未标记完成,且非 full=1、非 VIP 开通页强制单页。
* 老用户已齐全则自动写 DONE避免重复向导。
*/
_applyWizardModeFromProfile(d) {
const ro = this._wizardRouteOpts || {}
const forceFull = ro.full === true
const forceNoWizard = ro.wizardOff === true
const phoneNum = String(d.phone || '').replace(/\D/g, '')
const phoneOk = /^1[3-9]\d{9}$/.test(phoneNum)
const nickOk = !!(d.nickname && String(d.nickname).trim())
if (phoneOk && nickOk && !wx.getStorageSync(PROFILE_WIZARD_DONE_KEY)) {
wx.setStorageSync(PROFILE_WIZARD_DONE_KEY, '1')
}
const wizardMode = !forceFull && !forceNoWizard && !wx.getStorageSync(PROFILE_WIZARD_DONE_KEY)
this.setData({ wizardMode, wizardStep: 1 })
},
onWizardPrev() {
if (this.data.wizardStep > 1) {
this.setData({ wizardStep: this.data.wizardStep - 1 })
}
},
onWizardNext() {
if (this.data.saving) return
const { wizardMode, wizardStep } = this.data
if (!wizardMode) return
if (wizardStep === 1) {
if (!(this.data.nickname || '').trim()) {
wx.showToast({ title: '请填写昵称', icon: 'none' })
return
}
this.setData({ wizardStep: 2 })
return
}
if (wizardStep === 2) {
this.setData({ wizardStep: 3 })
return
}
this._doSaveProfile({ wizardComplete: true })
},
onNicknameAreaTouch() {
if (typeof wx.requirePrivacyAuthorize !== 'function') return
wx.requirePrivacyAuthorize({
@@ -365,7 +420,11 @@ Page({
}
},
async saveProfile() {
saveProfile() {
this._doSaveProfile({ wizardComplete: false })
},
async _doSaveProfile({ wizardComplete }) {
const userId = app.globalData.userInfo?.id
if (!userId) {
wx.showToast({ title: '请先登录', icon: 'none' })
@@ -373,7 +432,6 @@ Page({
}
const s = (v) => (v || '').toString().trim()
const isVip = this.data.isVip
// 手机号必填,格式校验(支持带空格/连字符输入)
const phoneRaw = s(this.data.phone)
if (!phoneRaw) {
wx.showToast({ title: '请输入手机号', icon: 'none' })
@@ -428,8 +486,17 @@ Page({
if (app.globalData.userInfo) {
if (payload.nickname) app.globalData.userInfo.nickname = payload.nickname
if (res?.data?.avatar) app.globalData.userInfo.avatar = res.data.avatar
if (payload.phone) app.globalData.userInfo.phone = payload.phone
wx.setStorageSync('userInfo', app.globalData.userInfo)
}
if (wizardComplete) {
wx.setStorageSync(PROFILE_WIZARD_DONE_KEY, '1')
this.setData({ wizardMode: false, saving: false })
setTimeout(() => {
wx.redirectTo({ url: `/pages/member-detail/member-detail?id=${userId}` })
}, 400)
return
}
setTimeout(() => getApp().goBackOrToHome(), 800)
} catch (e) {
wx.showToast({ title: e.message || '保存失败', icon: 'none' })