diff --git a/.cursor/skills/SKILL-小程序开发.md b/.cursor/skills/SKILL-小程序开发.md index a6202c83..ff027dba 100644 --- a/.cursor/skills/SKILL-小程序开发.md +++ b/.cursor/skills/SKILL-小程序开发.md @@ -58,10 +58,18 @@ description: Soul 创业派对小程序开发规范。在 miniprogram/ 下编辑 --- -## 6. 何时使用本 Skill +## 6. 表单与输入框 + +- **输入框 padding**:设置 padding、背景、边框时,用 `` 包裹 ``;padding/背景/边框写在 view 上,input 设置 `width: 100%`、`font-size`、`color`、`background: transparent`。可避免光标截断、布局异常。 +- **示例**:``,`.form-input { padding: 16rpx 24rpx; ... }`,`.form-input input { width: 100%; font-size: 28rpx; ... }` + +--- + +## 7. 何时使用本 Skill - 在 **miniprogram/** 下新增或修改页面、组件、utils 时。 - 在小程序内新增或修改任何网络请求路径时(必须保持 `/api/miniprogram/...`)。 - 做阅读、支付、推荐、提现等与 soul-api 对接的功能时。 +- 做表单、输入框样式时(遵循 §6 包裹 input 的写法)。 遵循本 Skill 可保证小程序只与 soul-api 的 miniprogram 路由组对接,避免与管理端或 next-project 接口混用。 diff --git a/.cursor/skills/SKILL-管理端开发.md b/.cursor/skills/SKILL-管理端开发.md index 56c1a76d..dbee184e 100644 --- a/.cursor/skills/SKILL-管理端开发.md +++ b/.cursor/skills/SKILL-管理端开发.md @@ -61,7 +61,11 @@ description: Soul 创业派对管理端开发规范。在 soul-admin/ 下编辑 **检查清单**:分页、搜索防抖、刷新、loading、空状态、错误条、仅 admin/db 路径。详见 `开发文档/列表标准与角色分工.md`。 -### 4.1 表单弹窗与「可选择+可手动填写」(吸收 SetVipModal 经验) +### 4.1 输入框 padding(前端通用) + +设置 input 的 padding、背景、边框时,用 div 包裹 input;padding 写在容器上,input 仅做文字样式。可避免光标截断、布局异常。React:`
`。 + +### 4.2 表单弹窗与「可选择+可手动填写」(吸收 SetVipModal 经验) 当表单需支持「从预设选项选择」或「手动填写自定义值」时: diff --git a/.cursor/经验库/团队/2026-02-27.md b/.cursor/经验库/团队/2026-02-27.md index 33823626..f552fafb 100644 --- a/.cursor/经验库/团队/2026-02-27.md +++ b/.cursor/经验库/团队/2026-02-27.md @@ -3,3 +3,8 @@ > 本日跨角色共享的架构决策、业务规则。格式:类型 | 摘要 | 关联 Skill --- + +## 输入框样式(前端通用) + +- **最佳实践 | 输入框 padding**:设置 input 的 padding/背景/边框时,用 div 或 view 包裹 input;padding 写在容器上,input 仅做文字样式,可避免光标截断、布局异常。 +- **适用**:小程序、管理端(React input 同理) diff --git a/.cursor/经验库/小程序/2026-02-27.md b/.cursor/经验库/小程序/2026-02-27.md new file mode 100644 index 00000000..eae48339 --- /dev/null +++ b/.cursor/经验库/小程序/2026-02-27.md @@ -0,0 +1,19 @@ +# 小程序开发工程师 经验记录 - 2026-02-27 + +--- + +## 输入框 padding 最佳实践 + +- **场景**:设置 input 的 padding、背景、边框等样式时,直接作用于 input 可能导致光标被截断、布局异常。 +- **方案**:用 `` 包裹 ``,padding/背景/边框写在 view 上,input 仅设置 width、font-size、color 等文字相关样式。 +- **示例**: + ```xml + + + + ``` + ```css + .form-input { padding: 16rpx 24rpx; background: rgba(255,255,255,0.06); border: 1rpx solid rgba(255,255,255,0.1); border-radius: 12rpx; } + .form-input input { width: 100%; font-size: 28rpx; color: #fff; background: transparent; } + ``` +- **升级**:已写入 SKILL-小程序开发 §6 表单与输入框 diff --git a/.cursor/经验库/小程序/项目索引.md b/.cursor/经验库/小程序/项目索引.md index 8f45c455..52c0dada 100644 --- a/.cursor/经验库/小程序/项目索引.md +++ b/.cursor/经验库/小程序/项目索引.md @@ -17,6 +17,7 @@ | 2026-02-26 | 项目索引初始化,.cursor 规则优化完成 | 已完成 | | 2026-02-27 | 开发进度汇报:永平落地已完成(海报 scene、我的收益、推广中心、VIP 相关);找伙伴、提现、阅读、分销核心功能已上线 | 已完成 | | 2026-02-27 | 开发进度同步会议:进度已同步至开发文档,待办资料完善弹窗、≥3 章弹窗 | 已完成 | +| 2026-02-27 | 吸收经验:输入框 padding 用 view 包裹,已升级 SKILL-小程序开发 §6 | 已完成 | > **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置 diff --git a/.cursor/经验库/经验清单.md b/.cursor/经验库/经验清单.md index a2c23799..bb4f8b77 100644 --- a/.cursor/经验库/经验清单.md +++ b/.cursor/经验库/经验清单.md @@ -21,7 +21,7 @@ | 日期 | 角色 | 类型 | 升级 Skill | 摘要 | |------|------|------|------------|------| -| (待补充) | | | | | +| 2026-02-27 | 小程序、团队 | 最佳实践 | SKILL-小程序开发 §6、SKILL-管理端开发 §4.1 | 输入框 padding 用 view/div 包裹 | --- @@ -32,4 +32,4 @@ --- -**最后更新**:2026-02-26 +**最后更新**:2026-02-27 diff --git a/miniprogram/pages/about/about.js b/miniprogram/pages/about/about.js index b337a620..ff164288 100644 --- a/miniprogram/pages/about/about.js +++ b/miniprogram/pages/about/about.js @@ -40,6 +40,7 @@ Page({ }, onLoad() { + wx.showShareMenu({ withShareTimeline: true }) this.setData({ statusBarHeight: app.globalData.statusBarHeight }) @@ -85,5 +86,10 @@ Page({ title: 'Soul创业派对 - 关于', path: ref ? `/pages/about/about?ref=${ref}` : '/pages/about/about' } + }, + + onShareTimeline() { + const ref = app.getMyReferralCode() + return { title: 'Soul创业派对 - 关于', query: ref ? `ref=${ref}` : '' } } }) diff --git a/miniprogram/pages/addresses/addresses.js b/miniprogram/pages/addresses/addresses.js index c3dfda61..1d4a50e1 100644 --- a/miniprogram/pages/addresses/addresses.js +++ b/miniprogram/pages/addresses/addresses.js @@ -14,6 +14,7 @@ Page({ }, onLoad() { + wx.showShareMenu({ withShareTimeline: true }) this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 }) @@ -127,5 +128,10 @@ Page({ title: 'Soul创业派对 - 地址管理', path: ref ? `/pages/addresses/addresses?ref=${ref}` : '/pages/addresses/addresses' } + }, + + onShareTimeline() { + const ref = app.getMyReferralCode() + return { title: 'Soul创业派对 - 地址管理', query: ref ? `ref=${ref}` : '' } } }) diff --git a/miniprogram/pages/addresses/edit.js b/miniprogram/pages/addresses/edit.js index 4f45893c..efacd000 100644 --- a/miniprogram/pages/addresses/edit.js +++ b/miniprogram/pages/addresses/edit.js @@ -27,6 +27,7 @@ Page({ }, onLoad(options) { + wx.showShareMenu({ withShareTimeline: true }) this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 }) @@ -205,5 +206,10 @@ Page({ title: 'Soul创业派对 - 编辑地址', path: ref ? `/pages/addresses/edit?ref=${ref}` : '/pages/addresses/edit' } + }, + + onShareTimeline() { + const ref = app.getMyReferralCode() + return { title: 'Soul创业派对 - 编辑地址', query: ref ? `ref=${ref}` : '' } } }) diff --git a/miniprogram/pages/agreement/agreement.js b/miniprogram/pages/agreement/agreement.js index cff31e3b..ea413a87 100644 --- a/miniprogram/pages/agreement/agreement.js +++ b/miniprogram/pages/agreement/agreement.js @@ -10,6 +10,7 @@ Page({ }, onLoad() { + wx.showShareMenu({ withShareTimeline: true }) this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 }) @@ -25,5 +26,10 @@ Page({ title: 'Soul创业派对 - 用户协议', path: ref ? `/pages/agreement/agreement?ref=${ref}` : '/pages/agreement/agreement' } + }, + + onShareTimeline() { + const ref = app.getMyReferralCode() + return { title: 'Soul创业派对 - 用户协议', query: ref ? `ref=${ref}` : '' } } }) diff --git a/miniprogram/pages/chapters/chapters.js b/miniprogram/pages/chapters/chapters.js index ff1c4787..4425b0ed 100644 --- a/miniprogram/pages/chapters/chapters.js +++ b/miniprogram/pages/chapters/chapters.js @@ -206,6 +206,7 @@ Page({ }, onLoad() { + wx.showShareMenu({ withShareTimeline: true }) this.setData({ statusBarHeight: app.globalData.statusBarHeight, navBarHeight: app.globalData.navBarHeight @@ -302,5 +303,10 @@ Page({ title: 'Soul创业派对 - 目录', path: ref ? `/pages/chapters/chapters?ref=${ref}` : '/pages/chapters/chapters' } + }, + + onShareTimeline() { + const ref = app.getMyReferralCode() + return { title: 'Soul创业派对 - 真实商业故事', query: ref ? `ref=${ref}` : '' } } }) diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js index ad4d0145..937c3254 100644 --- a/miniprogram/pages/index/index.js +++ b/miniprogram/pages/index/index.js @@ -45,6 +45,7 @@ Page({ // 超级个体(VIP会员) superMembers: [], + superMembersLoading: true, // 最新新增章节 latestChapters: [], @@ -71,7 +72,7 @@ Page({ app.handleReferralCode({ query: options }) } - // 初始化数据 + wx.showShareMenu({ withShareTimeline: true }) this.initData() }, @@ -119,6 +120,7 @@ Page({ }, async loadSuperMembers() { + this.setData({ superMembersLoading: true }) try { // 优先加载 VIP 会员(购买 1980 fullbook/vip 订单的用户) let members = [] @@ -128,8 +130,8 @@ Page({ // 不再过滤无头像用户,无头像时用首字母展示 members = (Array.isArray(res.data) ? res.data : []).slice(0, 4).map(u => ({ id: u.id, - name: u.vip_name || u.nickname || '会员', - avatar: u.vip_avatar || u.avatar || '', + name: u.vipName || u.vip_name || u.nickname || '会员', + avatar: u.vipAvatar || u.vip_avatar || u.avatar || '', isVip: true })) if (members.length > 0) { @@ -153,8 +155,11 @@ Page({ } } catch (e) {} } - this.setData({ superMembers: members }) - } catch (e) { console.log('[Index] 加载超级个体失败:', e) } + this.setData({ superMembers: members, superMembersLoading: false }) + } catch (e) { + console.log('[Index] 加载超级个体失败:', e) + this.setData({ superMembersLoading: false }) + } }, // 从服务端获取精选推荐(加权算法:阅读量50% + 时效30% + 付款率20%)和最新更新 @@ -336,5 +341,10 @@ Page({ title: 'Soul创业派对 - 真实商业故事', path: ref ? `/pages/index/index?ref=${ref}` : '/pages/index/index' } + }, + + onShareTimeline() { + const ref = app.getMyReferralCode() + return { title: 'Soul创业派对 - 真实商业故事', query: ref ? `ref=${ref}` : '' } } }) diff --git a/miniprogram/pages/index/index.wxml b/miniprogram/pages/index/index.wxml index b04cc314..e0536412 100644 --- a/miniprogram/pages/index/index.wxml +++ b/miniprogram/pages/index/index.wxml @@ -90,7 +90,17 @@
- + + + + + + + + + + + - + + 成为会员,展示你的项目 加入创业派对 → diff --git a/miniprogram/pages/index/index.wxss b/miniprogram/pages/index/index.wxss index a26506b1..861d0990 100644 --- a/miniprogram/pages/index/index.wxss +++ b/miniprogram/pages/index/index.wxss @@ -547,6 +547,45 @@ } /* ===== 超级个体(横向滚动) ===== */ +/* 加载骨架动画 */ +.super-loading { + width: 100%; + margin: 0 -32rpx; + padding: 0 32rpx; +} +.super-loading-inner { + display: flex; + gap: 32rpx; + padding-bottom: 16rpx; +} +.super-loading-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 16rpx; + min-width: 140rpx; +} +.super-loading-avatar { + width: 112rpx; + height: 112rpx; + border-radius: 50%; + background: linear-gradient(90deg, #2c2c2e 25%, #3a3a3c 50%, #2c2c2e 75%); + background-size: 200% 100%; + animation: super-shimmer 1.2s ease-in-out infinite; +} +.super-loading-name { + width: 80rpx; + height: 24rpx; + border-radius: 8rpx; + background: linear-gradient(90deg, #2c2c2e 25%, #3a3a3c 50%, #2c2c2e 75%); + background-size: 200% 100%; + animation: super-shimmer 1.2s ease-in-out infinite 0.2s; +} +@keyframes super-shimmer { + 0% { background-position: 100% 0; } + 100% { background-position: -100% 0; } +} + .super-scroll { white-space: nowrap; width: 100%; diff --git a/miniprogram/pages/match/match.js b/miniprogram/pages/match/match.js index eff8b841..63fde49a 100644 --- a/miniprogram/pages/match/match.js +++ b/miniprogram/pages/match/match.js @@ -72,6 +72,7 @@ Page({ }, onLoad() { + wx.showShareMenu({ withShareTimeline: true }) this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 }) @@ -667,5 +668,10 @@ Page({ title: 'Soul创业派对 - 找伙伴', path: ref ? `/pages/match/match?ref=${ref}` : '/pages/match/match' } + }, + + onShareTimeline() { + const ref = app.getMyReferralCode() + return { title: 'Soul创业派对 - 找伙伴', query: ref ? `ref=${ref}` : '' } } }) diff --git a/miniprogram/pages/member-detail/member-detail.js b/miniprogram/pages/member-detail/member-detail.js index 8eeb6396..7af4e8e7 100644 --- a/miniprogram/pages/member-detail/member-detail.js +++ b/miniprogram/pages/member-detail/member-detail.js @@ -11,6 +11,7 @@ Page({ data: { statusBarHeight: 44, member: null, loading: true }, onLoad(options) { + wx.showShareMenu({ withShareTimeline: true }) this.setData({ statusBarHeight: app.globalData.statusBarHeight }) if (options.id) this.loadMember(options.id) }, @@ -30,10 +31,10 @@ Page({ const u = Array.isArray(dbRes.data) ? dbRes.data[0] : dbRes.data if (u) { this.setData({ member: this.enrichAndFormat({ - id: u.id, name: u.vip_name || u.nickname || '创业者', - avatar: u.vip_avatar || u.avatar || '', isVip: u.is_vip === 1, - contactRaw: u.vip_contact || u.phone || '', project: u.vip_project || '', - bio: u.vip_bio || '', mbti: '', region: '', skills: '', + id: u.id, name: u.vipName || u.vip_name || u.nickname || '创业者', + avatar: u.vipAvatar || u.vip_avatar || u.avatar || '', isVip: u.is_vip === 1, + contactRaw: u.vipContact || u.vip_contact || u.phone || '', project: u.vipProject || u.vip_project || '', + bio: u.vipBio || u.vip_bio || '', mbti: '', region: '', skills: '', }), loading: false }) return } @@ -48,19 +49,19 @@ Page({ const merged = { id: raw.id, - name: raw.name || raw.vip_name || raw.nickname || '创业者', - avatar: raw.avatar || raw.vip_avatar || '', + name: raw.name || raw.vipName || raw.vip_name || raw.nickname || '创业者', + avatar: raw.avatar || raw.vipAvatar || raw.vip_avatar || '', isVip: raw.isVip || raw.is_vip === 1, mbti: raw.mbti || mock.mbti, region: raw.region || mock.region, skills: raw.skills || mock.skills, - contactRaw: raw.contactRaw || raw.vip_contact || mock.contactRaw, + contactRaw: raw.contactRaw || raw.vipContact || raw.vip_contact || mock.contactRaw, bestMonth: raw.bestMonth || mock.bestMonth, achievement: raw.achievement || mock.achievement, turningPoint: raw.turningPoint || mock.turningPoint, canHelp: raw.canHelp || mock.canHelp, needHelp: raw.needHelp || mock.needHelp, - project: raw.project || raw.vip_project || mock.project + project: raw.project || raw.vipProject || raw.vip_project || mock.project } const contact = merged.contactRaw || '' @@ -96,5 +97,12 @@ Page({ title: 'Soul创业派对 - 创业者详情', path: id && ref ? `/pages/member-detail/member-detail?id=${id}&ref=${ref}` : id ? `/pages/member-detail/member-detail?id=${id}` : ref ? `/pages/member-detail/member-detail?ref=${ref}` : '/pages/member-detail/member-detail' } + }, + + onShareTimeline() { + const ref = app.getMyReferralCode() + const id = this.data.member?.id + const q = id ? (ref ? `id=${id}&ref=${ref}` : `id=${id}`) : (ref ? `ref=${ref}` : '') + return { title: 'Soul创业派对 - 创业者详情', query: q } } }) diff --git a/miniprogram/pages/my/my.js b/miniprogram/pages/my/my.js index c857e4d0..2a01da3d 100644 --- a/miniprogram/pages/my/my.js +++ b/miniprogram/pages/my/my.js @@ -60,6 +60,7 @@ Page({ }, onLoad() { + wx.showShareMenu({ withShareTimeline: true }) this.setData({ statusBarHeight: app.globalData.statusBarHeight, navBarHeight: app.globalData.navBarHeight @@ -719,5 +720,10 @@ Page({ title: 'Soul创业派对 - 我的', path: ref ? `/pages/my/my?ref=${ref}` : '/pages/my/my' } + }, + + onShareTimeline() { + const ref = app.getMyReferralCode() + return { title: 'Soul创业派对 - 我的', query: ref ? `ref=${ref}` : '' } } }) diff --git a/miniprogram/pages/privacy/privacy.js b/miniprogram/pages/privacy/privacy.js index 0c95c06e..a12def28 100644 --- a/miniprogram/pages/privacy/privacy.js +++ b/miniprogram/pages/privacy/privacy.js @@ -10,6 +10,7 @@ Page({ }, onLoad() { + wx.showShareMenu({ withShareTimeline: true }) this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 }) @@ -25,5 +26,10 @@ Page({ title: 'Soul创业派对 - 隐私政策', path: ref ? `/pages/privacy/privacy?ref=${ref}` : '/pages/privacy/privacy' } + }, + + onShareTimeline() { + const ref = app.getMyReferralCode() + return { title: 'Soul创业派对 - 隐私政策', query: ref ? `ref=${ref}` : '' } } }) diff --git a/miniprogram/pages/purchases/purchases.js b/miniprogram/pages/purchases/purchases.js index a332a03c..86293d66 100644 --- a/miniprogram/pages/purchases/purchases.js +++ b/miniprogram/pages/purchases/purchases.js @@ -11,6 +11,7 @@ Page({ }, onLoad() { + wx.showShareMenu({ withShareTimeline: true }) this.setData({ statusBarHeight: app.globalData.statusBarHeight }) this.loadOrders() }, @@ -71,5 +72,10 @@ Page({ title: 'Soul创业派对 - 购买记录', path: ref ? `/pages/purchases/purchases?ref=${ref}` : '/pages/purchases/purchases' } + }, + + onShareTimeline() { + const ref = app.getMyReferralCode() + return { title: 'Soul创业派对 - 购买记录', query: ref ? `ref=${ref}` : '' } } }) diff --git a/miniprogram/pages/read/read.js b/miniprogram/pages/read/read.js index 9b0aa041..7d6981b8 100644 --- a/miniprogram/pages/read/read.js +++ b/miniprogram/pages/read/read.js @@ -74,6 +74,7 @@ Page({ }, async onLoad(options) { + wx.showShareMenu({ withShareTimeline: true }) // 支持 scene(扫码)、mid、id、ref const sceneStr = (options && options.scene) || '' const parsed = parseScene(sceneStr) @@ -505,10 +506,7 @@ Page({ const { section, sectionId, sectionMid } = this.data const ref = app.getMyReferralCode() const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}` - return { - title: `${section?.title || 'Soul创业派对'} - 来自派对房的真实故事`, - query: ref ? `${q}&ref=${ref}` : q - } + return { title: section?.title ? `📚 ${section.title}` : 'Soul创业派对 - 真实商业故事', query: ref ? `${q}&ref=${ref}` : q } }, // 显示登录弹窗(每次打开协议未勾选,符合审核要求) diff --git a/miniprogram/pages/referral/referral.js b/miniprogram/pages/referral/referral.js index 9985baff..9685e2e7 100644 --- a/miniprogram/pages/referral/referral.js +++ b/miniprogram/pages/referral/referral.js @@ -69,6 +69,8 @@ Page({ onLoad() { this.setData({ statusBarHeight: app.globalData.statusBarHeight }) this.initData() + // 启用分享到朋友圈(需同时有 onShareAppMessage 和 onShareTimeline;menus 在 Android 支持,iOS 为 Beta) + wx.showShareMenu({ menus: ['shareAppMessage', 'shareTimeline'] }) }, onShow() { diff --git a/miniprogram/pages/referral/referral.wxml b/miniprogram/pages/referral/referral.wxml index 38c587aa..b8536499 100644 --- a/miniprogram/pages/referral/referral.wxml +++ b/miniprogram/pages/referral/referral.wxml @@ -191,7 +191,7 @@ - + diff --git a/miniprogram/pages/search/search.js b/miniprogram/pages/search/search.js index 0eeb383c..485b7fcc 100644 --- a/miniprogram/pages/search/search.js +++ b/miniprogram/pages/search/search.js @@ -25,6 +25,7 @@ Page({ }, onLoad() { + wx.showShareMenu({ withShareTimeline: true }) this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 }) @@ -113,5 +114,10 @@ Page({ title: 'Soul创业派对 - 搜索', path: ref ? `/pages/search/search?ref=${ref}` : '/pages/search/search' } + }, + + onShareTimeline() { + const ref = app.getMyReferralCode() + return { title: 'Soul创业派对 - 搜索', query: ref ? `ref=${ref}` : '' } } }) diff --git a/miniprogram/pages/settings/settings.js b/miniprogram/pages/settings/settings.js index c46ab852..3f2cb8b1 100644 --- a/miniprogram/pages/settings/settings.js +++ b/miniprogram/pages/settings/settings.js @@ -27,6 +27,7 @@ Page({ }, onLoad() { + wx.showShareMenu({ withShareTimeline: true }) this.setData({ statusBarHeight: app.globalData.statusBarHeight, isLoggedIn: app.globalData.isLoggedIn, @@ -501,5 +502,10 @@ Page({ title: 'Soul创业派对 - 设置', path: ref ? `/pages/settings/settings?ref=${ref}` : '/pages/settings/settings' } + }, + + onShareTimeline() { + const ref = app.getMyReferralCode() + return { title: 'Soul创业派对 - 设置', query: ref ? `ref=${ref}` : '' } } }) diff --git a/miniprogram/pages/vip/vip.js b/miniprogram/pages/vip/vip.js index f28cf200..22ccd79d 100644 --- a/miniprogram/pages/vip/vip.js +++ b/miniprogram/pages/vip/vip.js @@ -20,11 +20,12 @@ Page({ { title: '链接资源', desc: '进群聊天、链接资源的权利' }, { title: '专属VIP标识', desc: '头像金色VIP光圈' } ], - profile: { name: '', project: '', contact: '', bio: '' }, + profile: { vipName: '', vipProject: '', vipContact: '', vipAvatar: '', vipBio: '' }, purchasing: false }, onLoad() { + wx.showShareMenu({ withShareTimeline: true }) this.setData({ statusBarHeight: app.globalData.statusBarHeight }) this.loadVipInfo() }, @@ -55,10 +56,54 @@ Page({ async loadProfile(userId) { try { const res = await app.request(`/api/miniprogram/vip/profile?userId=${userId}`) - if (res?.success) this.setData({ profile: res.data }) + if (res?.success) { + const p = res.data + // 头像若为相对路径则补全 + if (p.vipAvatar && !p.vipAvatar.startsWith('http')) { + p.vipAvatar = app.globalData.baseUrl.replace(/\/$/, '') + (p.vipAvatar.startsWith('/') ? p.vipAvatar : '/' + p.vipAvatar) + } + this.setData({ profile: p }) + } } catch (e) { console.log('[VIP] 资料加载失败', e) } }, + async onChooseVipAvatar() { + wx.chooseMedia({ + count: 1, + mediaType: ['image'], + sourceType: ['album', 'camera'], + success: async (res) => { + const tempPath = res.tempFiles[0].tempFilePath + wx.showLoading({ title: '上传中...', mask: true }) + try { + const uploadRes = await new Promise((resolve, reject) => { + wx.uploadFile({ + url: app.globalData.baseUrl + '/api/miniprogram/upload', + filePath: tempPath, + name: 'file', + formData: { folder: 'avatars' }, + success: (r) => { + try { + const d = JSON.parse(r.data) + d.success ? resolve(d) : reject(new Error(d.error || '上传失败')) + } catch { reject(new Error('解析失败')) } + }, + fail: reject + }) + }) + const path = uploadRes.url || uploadRes.data?.url || '' + const avatarUrl = path.startsWith('http') ? path : (app.globalData.baseUrl.replace(/\/$/, '') + (path.startsWith('/') ? path : '/' + path)) + this.setData({ 'profile.vipAvatar': avatarUrl }) + wx.hideLoading() + wx.showToast({ title: '头像已更新', icon: 'success' }) + } catch (e) { + wx.hideLoading() + wx.showToast({ title: e.message || '上传失败', icon: 'none' }) + } + } + }) + }, + async handlePurchase() { let userId = app.globalData.userInfo?.id let openId = app.globalData.openId || app.globalData.userInfo?.open_id @@ -110,10 +155,10 @@ Page({ } finally { this.setData({ purchasing: false }) } }, - onNameInput(e) { this.setData({ 'profile.name': e.detail.value }) }, - onProjectInput(e) { this.setData({ 'profile.project': e.detail.value }) }, - onContactInput(e) { this.setData({ 'profile.contact': e.detail.value }) }, - onBioInput(e) { this.setData({ 'profile.bio': e.detail.value }) }, + onVipNameInput(e) { this.setData({ 'profile.vipName': e.detail.value }) }, + onVipProjectInput(e) { this.setData({ 'profile.vipProject': e.detail.value }) }, + onVipContactInput(e) { this.setData({ 'profile.vipContact': e.detail.value }) }, + onVipBioInput(e) { this.setData({ 'profile.vipBio': e.detail.value }) }, async saveProfile() { const userId = app.globalData.userInfo?.id @@ -121,7 +166,7 @@ Page({ const p = this.data.profile try { const res = await app.request('/api/miniprogram/vip/profile', { - method: 'POST', data: { userId, name: p.name, project: p.project, contact: p.contact, bio: p.bio } + method: 'POST', data: { userId, vipName: p.vipName, vipProject: p.vipProject, vipContact: p.vipContact, vipAvatar: p.vipAvatar, vipBio: p.vipBio } }) if (res?.success) wx.showToast({ title: '资料已保存', icon: 'success' }) else wx.showToast({ title: res?.error || '保存失败', icon: 'none' }) @@ -136,5 +181,10 @@ Page({ title: 'Soul创业派对 - VIP会员', path: ref ? `/pages/vip/vip?ref=${ref}` : '/pages/vip/vip' } + }, + + onShareTimeline() { + const ref = app.getMyReferralCode() + return { title: 'Soul创业派对 - VIP会员', query: ref ? `ref=${ref}` : '' } } }) diff --git a/miniprogram/pages/vip/vip.wxml b/miniprogram/pages/vip/vip.wxml index c7ebfb09..b7fde33a 100644 --- a/miniprogram/pages/vip/vip.wxml +++ b/miniprogram/pages/vip/vip.wxml @@ -1,44 +1,50 @@ - + - + + + 卡若创业派对 - 卡若创业派对 - 加入卡若的创业派对会员 + + 加入卡若的 + 创业派对 + 会员 + 有效期至 {{expireDateStr}}(剩余{{daysRemaining}}天) 专属会员尊享权益 - 内容权益 - + + + {{item.title}} {{item.desc}} - 社交权益 - + + + {{item.title}} {{item.desc}} - @@ -51,28 +57,46 @@ 加入卡若创业派对,获取创业资讯与优质人脉资源 - 会员资料(展示在创业老板排行) + + 头像 + + + + + + 上传头像 + + + 姓名 - + + + 项目名称 - + + + 联系方式 - + + + 一句话简介 - + + + + + + - - - + \ No newline at end of file diff --git a/miniprogram/pages/vip/vip.wxss b/miniprogram/pages/vip/vip.wxss index 01071efe..be547857 100644 --- a/miniprogram/pages/vip/vip.wxss +++ b/miniprogram/pages/vip/vip.wxss @@ -12,15 +12,16 @@ .gold { color: #FFD700; } .vip-hero-sub { display: block; font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 12rpx; } -.rights-card { margin: 24rpx; } -.rights-item { display: flex; align-items: flex-start; gap: 20rpx; padding: 24rpx; margin-bottom: 16rpx; background: rgba(255,255,255,0.04); border: 1rpx solid rgba(255,255,255,0.06); border-radius: 16rpx; } +.rights-card { margin: 24rpx; padding: 0 8rpx; } +.rights-item { display: flex; align-items: flex-start; padding: 24rpx; margin-bottom: 16rpx; background: rgba(255,255,255,0.04); border: 1rpx solid rgba(255,255,255,0.06); border-radius: 16rpx; } +.rights-item .rights-check-wrap { margin-right: 20rpx; } .rights-check-wrap { width: 44rpx; height: 44rpx; border-radius: 50%; background: rgba(0,206,209,0.15); display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-top: 4rpx; } .rights-check { color: #00CED1; font-size: 24rpx; font-weight: bold; } .rights-info { display: flex; flex-direction: column; } .rights-title { font-size: 30rpx; font-weight: 600; color: rgba(255,255,255,0.95); } .rights-desc { font-size: 24rpx; color: rgba(255,255,255,0.45); margin-top: 6rpx; } -.rights-section-title { display: block; font-size: 26rpx; color: #00CED1; font-weight: 600; margin-bottom: 16rpx; padding-bottom: 12rpx; border-bottom: 1rpx solid rgba(0,206,209,0.15); } +.rights-section-title { display: block; font-size: 26rpx; color: #00CED1; font-weight: 600; margin-bottom: 16rpx; margin-left: 16rpx; padding-bottom: 12rpx; border-bottom: 1rpx solid rgba(0,206,209,0.15); } .buy-area { margin: 24rpx; padding: 32rpx; text-align: center; background: rgba(255,255,255,0.03); border-radius: 20rpx; } .price-row { display: flex; align-items: baseline; justify-content: center; gap: 12rpx; margin-bottom: 24rpx; } @@ -32,12 +33,23 @@ .buy-btn[disabled] { opacity: 0.5; } .buy-sub { display: block; font-size: 22rpx; color: rgba(255,255,255,0.4); margin-top: 16rpx; } -.profile-card { margin: 24rpx; padding: 28rpx; background: #1c1c1e; border-radius: 20rpx; } +.profile-card { margin: 24rpx; padding: 32rpx; background: rgba(255,255,255,0.04); border: 1rpx solid rgba(255,255,255,0.08); border-radius: 20rpx; } .profile-title { font-size: 30rpx; font-weight: 600; color: rgba(255,255,255,0.9); display: block; margin-bottom: 24rpx; } -.form-group { margin-bottom: 20rpx; } -.form-label { font-size: 24rpx; color: rgba(255,255,255,0.5); display: block; margin-bottom: 8rpx; } -.form-input { background: rgba(255,255,255,0.06); border: 1rpx solid rgba(255,255,255,0.1); border-radius: 12rpx; padding: 16rpx 20rpx; font-size: 28rpx; color: #fff; } -.save-btn { margin-top: 24rpx; width: 100%; height: 80rpx; line-height: 80rpx; background: #00CED1; color: #000; font-size: 30rpx; font-weight: 600; border-radius: 40rpx; border: none; } +.avatar-row { display: flex; align-items: center; margin-bottom: 24rpx; } +.avatar-label { font-size: 26rpx; color: rgba(255,255,255,0.6); width: 140rpx; flex-shrink: 0; } +.avatar-slot { width: 128rpx; height: 128rpx; border-radius: 50%; background: rgba(255,255,255,0.06); border: 2rpx dashed rgba(255,255,255,0.2); overflow: hidden; display: flex; align-items: center; justify-content: center; } +.avatar-slot .avatar-img { width: 100%; height: 100%; } +.avatar-placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; color: rgba(255,255,255,0.4); } +.avatar-add { font-size: 48rpx; line-height: 1; } +.avatar-hint { font-size: 20rpx; margin-top: 8rpx; } +.form-group { margin-bottom: 24rpx; } +.form-label { font-size: 26rpx; color: rgba(255,255,255,0.6); display: block; margin-bottom: 10rpx; } +.form-input { box-sizing: border-box; width: 100%; min-height: 76rpx; background: rgba(255,255,255,0.06); border: 1rpx solid rgba(255,255,255,0.1); border-radius: 12rpx; padding: 16rpx 24rpx; } +.form-input input { width: 100%; font-size: 28rpx; color: #fff; line-height: 1.5; background: transparent; } +.form-placeholder { color: rgba(255,255,255,0.35); } +.save-btn-wrap { margin-top: 32rpx; width: 100%; display: flex; justify-content: center; } +.save-btn { display: block; margin: 0; padding: 0; width: 100%; height: 88rpx; line-height: 88rpx; text-align: center; background: linear-gradient(135deg, #00CED1, #20B2AA); color: #000; font-size: 32rpx; font-weight: 600; border-radius: 44rpx; border: none; box-sizing: border-box; } +.save-btn::after { border: none; margin: 0; padding: 0; } .author-section { margin: 32rpx 24rpx; padding: 24rpx; border-top: 1rpx solid rgba(255,255,255,0.08); } .author-row { display: flex; justify-content: space-between; padding: 8rpx 0; } diff --git a/miniprogram/pages/withdraw-records/withdraw-records.js b/miniprogram/pages/withdraw-records/withdraw-records.js index 9757efac..22000ecd 100644 --- a/miniprogram/pages/withdraw-records/withdraw-records.js +++ b/miniprogram/pages/withdraw-records/withdraw-records.js @@ -11,6 +11,7 @@ Page({ }, onLoad() { + wx.showShareMenu({ withShareTimeline: true }) this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 }) this.loadRecords() }, @@ -127,5 +128,10 @@ Page({ title: 'Soul创业派对 - 提现记录', path: ref ? `/pages/withdraw-records/withdraw-records?ref=${ref}` : '/pages/withdraw-records/withdraw-records' } + }, + + onShareTimeline() { + const ref = app.getMyReferralCode() + return { title: 'Soul创业派对 - 提现记录', query: ref ? `ref=${ref}` : '' } } }) diff --git a/soul-api/internal/handler/miniprogram.go b/soul-api/internal/handler/miniprogram.go index 862e32b2..12feef1a 100644 --- a/soul-api/internal/handler/miniprogram.go +++ b/soul-api/internal/handler/miniprogram.go @@ -757,22 +757,17 @@ func MiniprogramUsers(c *gin.Context) { var cnt int64 db.Model(&model.Order{}).Where("user_id = ? AND status = ? AND (product_type = ? OR product_type = ?)", id, "paid", "fullbook", "vip").Count(&cnt) - nick := getStringValue(user.Nickname) - avatar := getStringValue(user.Avatar) - contact := getStringValue(user.Phone) - if contact == "" { - contact = getStringValue(user.WechatID) - } + // 用户信息(nickname/avatar)与会员资料(vip*)区分,字段与 model 一致(camelCase) item := gin.H{ - "id": user.ID, - "nickname": nick, - "avatar": avatar, - "vip_name": nick, - "vip_avatar": avatar, - "vip_contact": contact, - "vip_project": "", - "vip_bio": "", - "is_vip": cnt > 0, + "id": user.ID, + "nickname": getStringValue(user.Nickname), + "avatar": getStringValue(user.Avatar), + "vipName": getStringValue(user.VipName), + "vipAvatar": getStringValue(user.VipAvatar), + "vipContact": getStringValue(user.VipContact), + "vipProject": getStringValue(user.VipProject), + "vipBio": getStringValue(user.VipBio), + "is_vip": cnt > 0, } c.JSON(http.StatusOK, gin.H{"success": true, "data": item}) return diff --git a/soul-api/internal/handler/vip.go b/soul-api/internal/handler/vip.go index b5a4d64a..112a31aa 100644 --- a/soul-api/internal/handler/vip.go +++ b/soul-api/internal/handler/vip.go @@ -80,7 +80,7 @@ func VipStatus(c *gin.Context) { "isVip": false, "daysRemaining": 0, "expireDate": "", - "profile": gin.H{"name": "", "project": "", "contact": "", "avatar": "", "bio": ""}, + "profile": gin.H{"vipName": "", "vipProject": "", "vipContact": "", "vipAvatar": "", "vipBio": ""}, "price": float64(defaultVipPrice), "rights": defaultVipRights, }, @@ -117,36 +117,23 @@ func VipStatus(c *gin.Context) { }) } +// buildVipProfile 仅从 vip_* 字段构建会员资料,不混入用户信息(nickname/avatar/phone/wechat_id) +// 返回字段与 users 表 vip_* 对应,统一 vipName/vipProject/vipContact/vipAvatar/vipBio func buildVipProfile(u *model.User) gin.H { - name, project, contact, avatar, bio := "", "", "", "", "" - if u.VipName != nil && *u.VipName != "" { - name = *u.VipName + return gin.H{ + "vipName": getStr(u.VipName), + "vipProject": getStr(u.VipProject), + "vipContact": getStr(u.VipContact), + "vipAvatar": getStr(u.VipAvatar), + "vipBio": getStr(u.VipBio), } - if name == "" && u.Nickname != nil { - name = *u.Nickname +} + +func getStr(s *string) string { + if s == nil { + return "" } - if u.VipProject != nil { - project = *u.VipProject - } - if u.VipContact != nil { - contact = *u.VipContact - } - if contact == "" && u.Phone != nil { - contact = *u.Phone - } - if contact == "" && u.WechatID != nil { - contact = *u.WechatID - } - if u.VipAvatar != nil && *u.VipAvatar != "" { - avatar = *u.VipAvatar - } - if avatar == "" && u.Avatar != nil { - avatar = *u.Avatar - } - if u.VipBio != nil { - bio = *u.VipBio - } - return gin.H{"name": name, "project": project, "contact": contact, "avatar": avatar, "bio": bio} + return *s } // VipProfileGet GET /api/miniprogram/vip/profile 小程序-获取 VIP 资料 @@ -159,7 +146,7 @@ func VipProfileGet(c *gin.Context) { db := database.DB() var user model.User if err := db.Where("id = ?", userID).First(&user).Error; err != nil { - c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"name": "", "project": "", "contact": "", "avatar": "", "bio": ""}}) + c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"vipName": "", "vipProject": "", "vipContact": "", "vipAvatar": "", "vipBio": ""}}) return } c.JSON(http.StatusOK, gin.H{ @@ -169,15 +156,15 @@ func VipProfileGet(c *gin.Context) { } // VipProfilePost POST /api/miniprogram/vip/profile 小程序-更新 VIP 资料 -// 仅 VIP 会员可更新,更新 vip_name/vip_avatar/vip_project/vip_contact/vip_bio +// 请求/响应字段与 users 表 vip_* 一致:vipName/vipProject/vipContact/vipAvatar/vipBio func VipProfilePost(c *gin.Context) { var req struct { - UserID string `json:"userId" binding:"required"` - Name string `json:"name"` - Project string `json:"project"` - Contact string `json:"contact"` - Avatar string `json:"avatar"` - Bio string `json:"bio"` + UserID string `json:"userId" binding:"required"` + VipName string `json:"vipName"` + VipProject string `json:"vipProject"` + VipContact string `json:"vipContact"` + VipAvatar string `json:"vipAvatar"` + VipBio string `json:"vipBio"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"}) @@ -196,20 +183,20 @@ func VipProfilePost(c *gin.Context) { } updates := map[string]interface{}{} - if req.Name != "" { - updates["vip_name"] = req.Name + if req.VipName != "" { + updates["vip_name"] = req.VipName } - if req.Project != "" { - updates["vip_project"] = req.Project + if req.VipProject != "" { + updates["vip_project"] = req.VipProject } - if req.Contact != "" { - updates["vip_contact"] = req.Contact + if req.VipContact != "" { + updates["vip_contact"] = req.VipContact } - if req.Avatar != "" { - updates["vip_avatar"] = req.Avatar + if req.VipAvatar != "" { + updates["vip_avatar"] = req.VipAvatar } - if req.Bio != "" { - updates["vip_bio"] = req.Bio + if req.VipBio != "" { + updates["vip_bio"] = req.VipBio } if len(updates) == 0 { c.JSON(http.StatusOK, gin.H{"success": false, "error": "无更新内容"}) @@ -284,24 +271,20 @@ func VipMembers(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true, "data": list, "total": len(list)}) } +// formatVipMember 仅从 vip_* 字段构建会员展示数据,不混入用户信息 +// 用于创业老板排行等场景;未填会员资料时 name 显示「创业者」占位 func formatVipMember(u *model.User, isVip bool) gin.H { name := "" - if u.VipName != nil && *u.VipName != "" { + if u.VipName != nil { name = *u.VipName } - if name == "" && u.Nickname != nil { - name = *u.Nickname - } if name == "" { name = "创业者" } avatar := "" - if u.VipAvatar != nil && *u.VipAvatar != "" { + if u.VipAvatar != nil { avatar = *u.VipAvatar } - if avatar == "" && u.Avatar != nil { - avatar = *u.Avatar - } project := "" if u.VipProject != nil { project = *u.VipProject @@ -314,28 +297,22 @@ func formatVipMember(u *model.User, isVip bool) gin.H { if u.VipContact != nil { contact = *u.VipContact } - if contact == "" && u.Phone != nil { - contact = *u.Phone - } - if contact == "" && u.WechatID != nil { - contact = *u.WechatID - } vipRole := "" if u.VipRole != nil { vipRole = *u.VipRole } return gin.H{ - "id": u.ID, - "name": name, - "nickname": name, - "avatar": avatar, - "vip_name": name, - "vip_role": vipRole, - "vip_avatar": avatar, - "vip_project": project, - "vip_contact": contact, - "vip_bio": bio, - "is_vip": isVip, + "id": u.ID, + "name": name, + "nickname": name, + "avatar": avatar, + "vipName": name, + "vipRole": vipRole, + "vipAvatar": avatar, + "vipProject": project, + "vipContact": contact, + "vipBio": bio, + "is_vip": isVip, } } diff --git a/soul-api/scripts/add-vip-profile-fields.sql b/soul-api/scripts/add-vip-profile-fields.sql new file mode 100644 index 00000000..335a460b --- /dev/null +++ b/soul-api/scripts/add-vip-profile-fields.sql @@ -0,0 +1,20 @@ +-- ============================================================ +-- 会员资料字段 - users 表 +-- 用途:VIP 页保存资料、创业老板排行展示(与用户信息 phone/wechat_id 分离) +-- 执行前请先备份数据库! +-- ============================================================ + +-- 1. 检查:查看 users 表是否已有这些列(可选执行) +-- SHOW COLUMNS FROM users LIKE 'vip_name'; +-- SHOW COLUMNS FROM users LIKE 'vip_avatar'; +-- SHOW COLUMNS FROM users LIKE 'vip_project'; +-- SHOW COLUMNS FROM users LIKE 'vip_contact'; +-- SHOW COLUMNS FROM users LIKE 'vip_bio'; + +-- 2. 新增会员资料字段(若列已存在会报 Duplicate column name,可忽略该条) +-- -------------------------------------------------------- +ALTER TABLE users ADD COLUMN vip_name VARCHAR(100) NULL COMMENT '会员姓名(创业老板排行)'; +ALTER TABLE users ADD COLUMN vip_avatar VARCHAR(500) NULL COMMENT '会员头像'; +ALTER TABLE users ADD COLUMN vip_project VARCHAR(200) NULL COMMENT '项目名称'; +ALTER TABLE users ADD COLUMN vip_contact VARCHAR(100) NULL COMMENT '会员联系方式(展示用,与 phone/wechat_id 分离)'; +ALTER TABLE users ADD COLUMN vip_bio TEXT NULL COMMENT '一句话简介'; diff --git a/soul-api/uploads/avatars/1772165051417228100_qpc606.png b/soul-api/uploads/avatars/1772165051417228100_qpc606.png new file mode 100644 index 00000000..fabb67db Binary files /dev/null and b/soul-api/uploads/avatars/1772165051417228100_qpc606.png differ diff --git a/临时需求池/VIP页保存资料-开发团队分析.md b/临时需求池/VIP页保存资料-开发团队分析.md new file mode 100644 index 00000000..bfc1209d --- /dev/null +++ b/临时需求池/VIP页保存资料-开发团队分析.md @@ -0,0 +1,112 @@ +# VIP 页保存资料 - 开发团队分析 + +> 针对 `miniprogram/pages/vip` 的「保存资料」逻辑,与《规则说明》、提现、找伙伴等业务规则的对齐分析。 + +--- + +## 一、当前实现梳理 + +### 1.1 VIP 页资料字段 + +| 字段 | 存库字段 | 说明 | +|------|----------|------| +| 姓名 | vip_name | 展示在创业老板排行 | +| 项目名称 | vip_project | | +| 联系方式 | vip_contact | placeholder: 微信号或手机号 | +| 一句话简介 | vip_bio | | + +保存时调用:`POST /api/miniprogram/vip/profile`,仅更新 `vip_*` 字段。 + +### 1.2 规则说明中的资料要求 + +| 场景 | 规则要求 | +|------|----------| +| 提现前 | 必须填写**手机号、微信号**,有单独弹窗,填写完后**自动同步保存到资料页** | +| 找伙伴前 | 需要引导完善个人资料 | +| 资源对接 | 需填写**联系方式** + **我能帮到大家什么** + **我需要什么帮助** | +| 导师顾问/团队招募 | 需填写**联系方式** | + +### 1.3 相关资料存储位置 + +| 入口 | 接口 | 更新字段 | 用途 | +|------|------|----------|------| +| 设置页 | POST /api/miniprogram/user/profile | phone, wechatId | 提现校验、资料页展示 | +| 提现弹窗 | (待实现)填写手机号、微信号 | phone, wechat_id | 提现到账 | +| VIP 页 | POST /api/miniprogram/vip/profile | vip_name, vip_project, vip_contact, vip_bio | 创业老板排行、VIP 展示 | +| 找伙伴-资源对接 | match 页表单 | 需核对后端存储 | 我能帮、需要什么、联系方式 | + +--- + +## 二、对齐问题分析 + +### 2.1 会员资料与用户信息已区分(设计正确) + +**users 表字段划分**: +- **用户信息**(phone、wechat_id、nickname、avatar):用于登录、提现、找伙伴前置校验 +- **会员资料**(vip_name、vip_project、vip_contact、vip_avatar、vip_bio):仅用于创业老板排行展示 + +**结论**:✅ **已区分**。VIP 页保存的「联系方式」只写入 `vip_contact`,不混入 `phone`/`wechat_id`。提现、找伙伴所需手机号/微信号需在设置页或提现弹窗单独填写并更新用户信息。 + +### 2.2 规则要求「手机号、微信号」分开 + +**现状**:VIP 页、设置页均为单一「联系方式」或混合输入。 + +**规则说明**:明确要求「手机号、微信号」两项。 + +**建议**: +- 短期:保持 VIP 页单一「联系方式」框,但保存时后端需将 contact 解析并同步到 phone/wechat_id(如:11 位数字→phone,否则→wechat_id) +- 中期:产品确认是否在资料页/VIP 页拆分为「手机号」「微信号」两个输入框,与规则完全一致 + +### 2.3 找伙伴「我能帮」「需要什么」与 VIP 资料分离 + +**现状**: +- VIP 页:姓名、项目、联系方式、一句话简介 → 用于创业老板排行 +- 找伙伴-资源对接:在 match 页填写「我能帮到你什么」「我需要什么帮助」→ 与 VIP 资料分离 + +**规则**:资源对接需填写「联系方式 + 我能帮到大家什么 + 我需要什么帮助」。 + +**结论**:✅ **用途不同,分离合理**。VIP 资料侧重排行展示,找伙伴侧重匹配信息。但需确保「联系方式」统一:若在 VIP 页填了 contact,应能作为找伙伴/提现的前置条件。 + +### 2.4 填写后「自动同步保存到资料页」 + +**规则**:提现弹窗填写手机号、微信号后,应自动同步保存到资料页(用户信息)。 + +**现状**: +- VIP 页保存 → 只更新 vip_*(会员资料),**不**写入 user/profile +- 设置页保存 → 更新 user/profile(phone、wechatId 等用户信息) +- 提现弹窗(待实现)→ 应写入 phone、wechat_id,并同步到用户资料 + +**结论**:✅ **职责正确**。会员资料与用户信息分离;提现所需手机号/微信号通过设置页或提现弹窗更新用户信息即可。 + +--- + +## 三、开发团队决议(已确认) + +### 3.1 会员资料与用户信息分离 + +- **VipProfilePost**:仅更新 `vip_name`、`vip_project`、`vip_contact`、`vip_avatar`、`vip_bio` +- **buildVipProfile**:仅读取 vip_* 字段,不再 fallback 到 nickname/phone/wechat_id/avatar +- 提现、找伙伴所需手机号/微信号:通过设置页或提现弹窗 → `POST /api/miniprogram/user/profile` 更新 + +### 3.2 产品确认项(可选) + +| 项目 | 说明 | +|------|------| +| 提现弹窗填写后同步到资料页的具体交互 | 是否直接更新 users.phone/wechat_id + 刷新设置页展示 | + +--- + +## 四、总结表 + +| 维度 | 状态 | 说明 | +|------|------|------| +| 会员资料与用户信息分离 | ✅ 已确认 | vip_* 仅用于排行;phone/wechat_id 用于提现、找伙伴 | +| buildVipProfile 不再混入用户信息 | ✅ 已实现 | 仅读 vip_* 字段 | +| 规则「手机号、微信号」分开 | ⚠️ 待产品确认 | 设置页/提现弹窗可拆分 | +| 找伙伴资料与 VIP 资料分离 | ✅ 合理 | 用途不同 | +| 提现弹窗填写后同步资料页 | 待实现 | 见运营与变更待办 | +| VIP 页样式 | ✅ 已修复 | 表单、按钮、权益卡片 | + +--- + +*分析日期:2026-02-27 | 参与角色:产品、后端、管理端、小程序* diff --git a/开发文档/8、部署/VIP功能-数据库迁移说明.md b/开发文档/8、部署/VIP功能-数据库迁移说明.md index c27ba77f..66b09558 100644 --- a/开发文档/8、部署/VIP功能-数据库迁移说明.md +++ b/开发文档/8、部署/VIP功能-数据库迁移说明.md @@ -10,6 +10,7 @@ |------|------| | `soul-api/scripts/add-vip-activated-at.sql` | 新增 `users.vip_activated_at`(成为 VIP 时间,排序用) | | `soul-api/scripts/add-vip-roles-and-fields.sql` | 新建 `vip_roles` 表;新增 `users.vip_sort`、`users.vip_role` | +| `soul-api/scripts/add-vip-profile-fields.sql` | 新增 `users.vip_name`、`vip_avatar`、`vip_project`、`vip_contact`、`vip_bio`(会员资料,与用户信息分离) | --- @@ -21,6 +22,9 @@ mysql -u user -p database < soul-api/scripts/add-vip-activated-at.sql # 2. vip_roles 表 + users 新字段 mysql -u user -p database < soul-api/scripts/add-vip-roles-and-fields.sql + +# 3. 会员资料字段(若尚未执行;列已存在会报 Duplicate column,可忽略) +mysql -u user -p database < soul-api/scripts/add-vip-profile-fields.sql ``` 若 `vip_sort`、`vip_role` 已存在,对应 `ALTER` 会报错,可忽略或单独执行未执行过的语句。 @@ -35,6 +39,7 @@ mysql -u user -p database < soul-api/scripts/add-vip-roles-and-fields.sql | `vip_sort` | 手动排序,数字越小越靠前;NULL 时按 vip_activated_at | | `vip_role` | 角色:从 vip_roles 选或手动填写 | | `vip_roles` | 预设角色表(创始人、投资人、产品经理等),管理端可 CRUD | +| `vip_name`、`vip_avatar`、`vip_project`、`vip_contact`、`vip_bio` | 会员资料(创业老板排行),与用户信息 phone/wechat_id 分离 | ---