更新文档,新增输入框样式最佳实践,强调在小程序和管理端开发中使用容器包裹输入框以避免布局问题。调整经验库,记录相关最佳实践和开发进度,确保团队共享经验的一致性和可追溯性。优化小程序页面,增加分享功能,提升用户体验。

This commit is contained in:
Alex-larget
2026-02-27 14:22:58 +08:00
parent 31f4e37345
commit 8655dca7b4
34 changed files with 514 additions and 142 deletions

View File

@@ -58,10 +58,18 @@ description: Soul 创业派对小程序开发规范。在 miniprogram/ 下编辑
---
## 6. 何时使用本 Skill
## 6. 表单与输入框
- **输入框 padding**:设置 padding、背景、边框时`<view>` 包裹 `<input>`padding/背景/边框写在 view 上input 设置 `width: 100%``font-size``color``background: transparent`。可避免光标截断、布局异常。
- **示例**`<view class="form-input"><input ... /></view>``.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 接口混用。

View File

@@ -61,7 +61,11 @@ description: Soul 创业派对管理端开发规范。在 soul-admin/ 下编辑
**检查清单**分页、搜索防抖、刷新、loading、空状态、错误条、仅 admin/db 路径。详见 `开发文档/列表标准与角色分工.md`
### 4.1 表单弹窗与「可选择+可手动填写」(吸收 SetVipModal 经验
### 4.1 输入框 padding前端通用
设置 input 的 padding、背景、边框时用 div 包裹 inputpadding 写在容器上input 仅做文字样式。可避免光标截断、布局异常。React`<div className="form-input"><input ... /></div>`
### 4.2 表单弹窗与「可选择+可手动填写」(吸收 SetVipModal 经验)
当表单需支持「从预设选项选择」或「手动填写自定义值」时:

View File

@@ -3,3 +3,8 @@
> 本日跨角色共享的架构决策、业务规则。格式:类型 | 摘要 | 关联 Skill
---
## 输入框样式(前端通用)
- **最佳实践 | 输入框 padding**:设置 input 的 padding/背景/边框时,用 div 或 view 包裹 inputpadding 写在容器上input 仅做文字样式,可避免光标截断、布局异常。
- **适用**小程序、管理端React input 同理)

View File

@@ -0,0 +1,19 @@
# 小程序开发工程师 经验记录 - 2026-02-27
---
## 输入框 padding 最佳实践
- **场景**:设置 input 的 padding、背景、边框等样式时直接作用于 input 可能导致光标被截断、布局异常。
- **方案**:用 `<view>` 包裹 `<input>`padding/背景/边框写在 view 上input 仅设置 width、font-size、color 等文字相关样式。
- **示例**
```xml
<view class="form-input">
<input placeholder="提示文字" value="{{value}}" bindinput="onInput" />
</view>
```
```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 表单与输入框

View File

@@ -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状态用已完成 / 进行中 / 待续 / 搁置

View File

@@ -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

View File

@@ -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}` : '' }
}
})

View File

@@ -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}` : '' }
}
})

View File

@@ -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}` : '' }
}
})

View File

@@ -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}` : '' }
}
})

View File

@@ -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}` : '' }
}
})

View File

@@ -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}` : '' }
}
})

View File

@@ -90,7 +90,17 @@
<text class="more-arrow"></text>
</view>
</view>
<scroll-view wx:if="{{superMembers.length > 0}}" class="super-scroll" scroll-x>
<!-- 加载中:骨架动画 -->
<view wx:if="{{superMembersLoading}}" class="super-loading">
<view class="super-loading-inner">
<view class="super-loading-item" wx:for="{{[1,2,3,4]}}" wx:key="*this">
<view class="super-loading-avatar"></view>
<view class="super-loading-name"></view>
</view>
</view>
</view>
<!-- 已加载有数据 -->
<scroll-view wx:elif="{{superMembers.length > 0}}" class="super-scroll" scroll-x>
<view class="super-scroll-inner">
<view
class="super-item-h"
@@ -107,7 +117,8 @@
</view>
</view>
</scroll-view>
<view class="super-empty" wx:else>
<!-- 已加载无数据 -->
<view wx:else class="super-empty">
<text class="super-empty-text">成为会员,展示你的项目</text>
<view class="super-empty-btn" bindtap="goToVip">加入创业派对 →</view>
</view>

View File

@@ -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%;

View File

@@ -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}` : '' }
}
})

View File

@@ -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 }
}
})

View File

@@ -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}` : '' }
}
})

View File

@@ -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}` : '' }
}
})

View File

@@ -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}` : '' }
}
})

View File

@@ -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 }
},
// 显示登录弹窗(每次打开协议未勾选,符合审核要求)

View File

@@ -69,6 +69,8 @@ Page({
onLoad() {
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
this.initData()
// 启用分享到朋友圈(需同时有 onShareAppMessage 和 onShareTimelinemenus 在 Android 支持iOS 为 Beta
wx.showShareMenu({ menus: ['shareAppMessage', 'shareTimeline'] })
},
onShow() {

View File

@@ -191,7 +191,7 @@
</view>
<view class="share-info">
<text class="share-title">更多分享方式</text>
<text class="share-desc">使用系统分享功能</text>
<text class="share-desc">复制链接发给好友;点击右上角 ⋮ 可分享到朋友圈(带推荐码)</text>
</view>
<image class="share-arrow-icon" src="/assets/icons/arrow-right.svg" mode="aspectFit"></image>
</view>

View File

@@ -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}` : '' }
}
})

View File

@@ -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}` : '' }
}
})

View File

@@ -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}` : '' }
}
})

View File

@@ -1,44 +1,50 @@
<!--VIP会员页-->
<!-- VIP会员页 -->
<view class="page">
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-back" bindtap="goBack"><text class="back-icon"></text></view>
<view class="nav-back" bindtap="goBack">
<text class="back-icon"></text>
</view>
<text class="nav-title">卡若创业派对</text>
<view class="nav-placeholder-r"></view>
</view>
<view style="height: {{statusBarHeight + 44}}px;"></view>
<!-- 会员状态 -->
<view class="vip-hero {{isVip ? 'vip-hero-active' : ''}}">
<text class="vip-hero-tag">卡若创业派对</text>
<text class="vip-hero-title">加入卡若的<text class="gold">创业派对</text>会员</text>
<text class="vip-hero-title">
加入卡若的
<text class="gold">创业派对</text>
会员
</text>
<text class="vip-hero-sub" wx:if="{{isVip}}">有效期至 {{expireDateStr}}(剩余{{daysRemaining}}天)</text>
<text class="vip-hero-sub" wx:else>专属会员尊享权益</text>
</view>
<!-- 内容权益 -->
<view class="rights-card">
<text class="rights-section-title">内容权益</text>
<view class="rights-item" wx:for="{{contentRights}}" wx:key="title">
<view class="rights-check-wrap"><text class="rights-check">✓</text></view>
<view class="rights-check-wrap">
<text class="rights-check">✓</text>
</view>
<view class="rights-info">
<text class="rights-title">{{item.title}}</text>
<text class="rights-desc">{{item.desc}}</text>
</view>
</view>
</view>
<!-- 社交权益 -->
<view class="rights-card">
<text class="rights-section-title">社交权益</text>
<view class="rights-item" wx:for="{{socialRights}}" wx:key="title">
<view class="rights-check-wrap"><text class="rights-check">✓</text></view>
<view class="rights-check-wrap">
<text class="rights-check">✓</text>
</view>
<view class="rights-info">
<text class="rights-title">{{item.title}}</text>
<text class="rights-desc">{{item.desc}}</text>
</view>
</view>
</view>
<!-- 价格区 + 购买按钮 -->
<view class="buy-area" wx:if="{{!isVip}}">
<view class="price-row">
@@ -51,28 +57,46 @@
</button>
<text class="buy-sub">加入卡若创业派对,获取创业资讯与优质人脉资源</text>
</view>
<!-- VIP资料填写仅VIP可见 -->
<view class="profile-card" wx:if="{{isVip}}">
<text class="profile-title">会员资料(展示在创业老板排行)</text>
<view class="avatar-row">
<view class="avatar-label">头像</view>
<view class="avatar-slot" bindtap="onChooseVipAvatar">
<image wx:if="{{profile.vipAvatar}}" class="avatar-img" src="{{profile.vipAvatar}}" mode="aspectFill"/>
<view wx:else class="avatar-placeholder">
<text class="avatar-add">+</text>
<text class="avatar-hint">上传头像</text>
</view>
</view>
</view>
<view class="form-group">
<text class="form-label">姓名</text>
<input class="form-input" placeholder="您的真实姓名" value="{{profile.name}}" bindinput="onNameInput"/>
<view class="form-input">
<input type="nickname" placeholder="您的真实姓名" placeholder-class="form-placeholder" value="{{profile.vipName}}" bindinput="onVipNameInput" />
</view>
</view>
<view class="form-group">
<text class="form-label">项目名称</text>
<input class="form-input" placeholder="您的项目/公司名称" value="{{profile.project}}" bindinput="onProjectInput"/>
<view class="form-input">
<input placeholder="您的项目/公司名称" placeholder-class="form-placeholder" value="{{profile.vipProject}}" bindinput="onVipProjectInput" />
</view>
</view>
<view class="form-group">
<text class="form-label">联系方式</text>
<input class="form-input" placeholder="微信号或手机号" value="{{profile.contact}}" bindinput="onContactInput"/>
<view class="form-input">
<input placeholder="微信号或手机号" placeholder-class="form-placeholder" value="{{profile.vipContact}}" bindinput="onVipContactInput" />
</view>
</view>
<view class="form-group">
<text class="form-label">一句话简介</text>
<input class="form-input" placeholder="简要描述您的业务" value="{{profile.bio}}" bindinput="onBioInput"/>
<view class="form-input">
<input placeholder="简要描述您的业务" placeholder-class="form-placeholder" value="{{profile.vipBio}}" bindinput="onVipBioInput" />
</view>
</view>
<view class="save-btn-wrap">
<button class="save-btn" bindtap="saveProfile">保存资料</button>
</view>
<button class="save-btn" bindtap="saveProfile">保存资料</button>
</view>
<view class="bottom-space"></view>
</view>
</view>

View File

@@ -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; }

View File

@@ -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}` : '' }
}
})

View File

@@ -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

View File

@@ -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,
}
}

View File

@@ -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 '一句话简介';

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -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_id11 位数字→phone否则→wechat_id
- 中期:产品确认是否在资料页/VIP 页拆分为「手机号」「微信号」两个输入框,与规则完全一致
### 2.3 找伙伴「我能帮」「需要什么」与 VIP 资料分离
**现状**
- VIP 页:姓名、项目、联系方式、一句话简介 → 用于创业老板排行
- 找伙伴-资源对接:在 match 页填写「我能帮到你什么」「我需要什么帮助」→ 与 VIP 资料分离
**规则**:资源对接需填写「联系方式 + 我能帮到大家什么 + 我需要什么帮助」。
**结论**:✅ **用途不同,分离合理**。VIP 资料侧重排行展示,找伙伴侧重匹配信息。但需确保「联系方式」统一:若在 VIP 页填了 contact应能作为找伙伴/提现的前置条件。
### 2.4 填写后「自动同步保存到资料页」
**规则**:提现弹窗填写手机号、微信号后,应自动同步保存到资料页(用户信息)。
**现状**
- VIP 页保存 → 只更新 vip_*(会员资料),**不**写入 user/profile
- 设置页保存 → 更新 user/profilephone、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 | 参与角色:产品、后端、管理端、小程序*

View File

@@ -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 分离 |
---