diff --git a/.cursor/agent/小程序开发工程师/evolution/2026-02-28.md b/.cursor/agent/小程序开发工程师/evolution/2026-02-28.md index 2d189bbe..5b2b6a03 100644 --- a/.cursor/agent/小程序开发工程师/evolution/2026-02-28.md +++ b/.cursor/agent/小程序开发工程师/evolution/2026-02-28.md @@ -15,4 +15,11 @@ ## input/textarea padding 规范 - **原则**:给 input 或 textarea 设置 padding 时,必须用 view 包裹,padding 写在 view 上;不在 input/textarea 自身上设 padding,避免原生组件光标截断、布局异常。 -- **已升级**:miniprogram-dev SKILL §6。 +- **口诀**:外边包 view,内部 input width 100%。 +- **已升级**:miniprogram-dev SKILL §6 加入口诀;admin-dev §4.1 同步补充。 + +## 找伙伴-资源对接弹窗 input 边距修正 + +- **问题**:弹窗内 input 文字贴边,padding 直接写在 input 上导致布局异常。 +- **正确做法**:按 Skill §6,用 view 包裹,padding 写 view,内部 input 设 `width: 100%`。 +- **修改**:match 页资源对接两个输入框 + 联系方式输入框,统一改为 `form-input-wrap` + `form-input-inner` / `input-field-wrap` + `input-field-inner` 结构。 diff --git a/.cursor/agent/小程序开发工程师/evolution/索引.md b/.cursor/agent/小程序开发工程师/evolution/索引.md index 1e6e8433..eaf1ff56 100644 --- a/.cursor/agent/小程序开发工程师/evolution/索引.md +++ b/.cursor/agent/小程序开发工程师/evolution/索引.md @@ -2,3 +2,4 @@ | 日期 | 摘要 | 文件 | |------|------|------| +| 2026-02-28 | input 边距口诀、match 资源对接弹窗修正 | [2026-02-28.md](./2026-02-28.md) | diff --git a/.cursor/agent/开发助理/经验清单.md b/.cursor/agent/开发助理/经验清单.md index aee847d7..f8697f96 100644 --- a/.cursor/agent/开发助理/经验清单.md +++ b/.cursor/agent/开发助理/经验清单.md @@ -23,6 +23,7 @@ | 日期 | 角色 | 类型 | 升级 Skill | 摘要 | |------|------|------|------------|------| | 2026-02-27 | 小程序、团队 | 最佳实践 | SKILL-小程序开发 §6、SKILL-管理端开发 §4.1 | 输入框 padding 用 view/div 包裹 | +| 2026-02-28 | 小程序、管理端 | 最佳实践 | miniprogram §6、admin §4.1 | input 边距口诀「外边包 view、内部 width 100%」;match 弹窗已修正 | --- @@ -33,4 +34,4 @@ --- -**最后更新**:2026-02-27 +**最后更新**:2026-02-28 diff --git a/.cursor/agent/开发助理/项目索引/小程序.md b/.cursor/agent/开发助理/项目索引/小程序.md index 25a8421c..9874bcd4 100644 --- a/.cursor/agent/开发助理/项目索引/小程序.md +++ b/.cursor/agent/开发助理/项目索引/小程序.md @@ -19,6 +19,7 @@ | 2026-02-27 | 开发进度同步会议:进度已同步至开发文档,待办资料完善弹窗、≥3 章弹窗 | 已完成 | | 2026-02-27 | 吸收经验:输入框 padding 用 view 包裹,已升级 SKILL-小程序开发 §6 | 已完成 | | 2026-02-28 | stitch_soul 需求评审:首页/目录/导师/会员/资料五类页面,待需求与接口确定后分阶段实现 | 待续 | +| 2026-02-28 | 吸收经验:input 边距口诀「外边包 view、内部 width 100%」写入 Skill §6;match 资源对接弹窗已按规范修正 | 已完成 | > **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置 diff --git a/.cursor/rules/soul-project-boundary.mdc b/.cursor/rules/soul-project-boundary.mdc index d19727ac..772afcd0 100644 --- a/.cursor/rules/soul-project-boundary.mdc +++ b/.cursor/rules/soul-project-boundary.mdc @@ -58,7 +58,7 @@ alwaysApply: true | 吸收经验、升级 skills、记录经验、保存开发进度、更新项目索引、记录开发进度、任务完成、搞定了、完成了 | `e:\Gongsi\Mycontent\.cursor\skills\assistant-doc-sync\SKILL.md` | | 跨端功能开发 | `e:\Gongsi\Mycontent\.cursor\skills\role-flow-control\SKILL.md` | | 变更完成、检查一下、准备提交 | `e:\Gongsi\Mycontent\.cursor\skills\change-checklist\SKILL.md` | -| 开个会、团队会议、需求评审、方案讨论、大家一起讨论 | `e:\Gongsi\Mycontent\.cursor\skills\team-meeting\SKILL.md` | +| 开个会、开会、团队会议、乘风开会、需求评审、方案讨论、大家一起讨论 | `e:\Gongsi\Mycontent\.cursor\skills\team-meeting\SKILL.md`(老板分身/乘风主持) | | 会议结束、散会、会开完了 | `e:\Gongsi\Mycontent\.cursor\skills\assistant-doc-sync\SKILL.md`(会议收尾) | **注意**:「必须 Read」= 使用 Read 工具读取**绝对路径**的完整文件内容后执行,不可跳过或仅凭记忆。 diff --git a/.cursor/rules/老板分身-索引.mdc b/.cursor/rules/老板分身-索引.mdc index 4a9f62b4..96d32b1c 100644 --- a/.cursor/rules/老板分身-索引.mdc +++ b/.cursor/rules/老板分身-索引.mdc @@ -6,7 +6,7 @@ alwaysApply: true # 老板分身 - 能力与约束(Soul 创业派对) > **老板分身权限最高**:协调所有智能体(小程序开发工程师、管理端开发工程师、后端工程师、产品经理、开发助理等)。其他 agent 执行任务时遵循本规则;老板分身可调度、协调、指派任一角色。 -> **激活方式**:用户说「老板」「分身」「架构」「帮我协调」时,从旁观者转为主动参与。 +> **激活方式**:用户说「老板」「分身」「乘风」「架构」「帮我协调」时,从旁观者转为主动参与。**开会时**:用户说「开会」「开个会」「团队会议」「乘风开会」等,老板分身(乘风)作为主持人自动读取并执行 `.cursor/skills/team-meeting/SKILL.md` 中的会议协议。 > **会话自检**:仅沿用本项目 `.cursor/` 下的 rules、skills、agent;忽略与本项目无关的全局 rules/skills。 > **角色驱动**:Soul 角色与 agent 映射见 `config/paths.py` 的 ROLE_TO_AGENT。 diff --git a/.cursor/skills/team-meeting/SKILL.md b/.cursor/skills/team-meeting/SKILL.md index 20d50442..b1003d93 100644 --- a/.cursor/skills/team-meeting/SKILL.md +++ b/.cursor/skills/team-meeting/SKILL.md @@ -5,6 +5,8 @@ description: Soul 创业派对开发团队多角色会议。语义化触发: 当触发会议关键词时,使用本 Skill 主持多角色会议,确保各角色充分发言、形成决议、橙子生成会议纪要。 +**主持人约定**:**乘风 = 老板分身**。会议中乘风(老板分身)为主持人,负责定议题、执行质疑轮、形成决议;橙子为书记员,负责会议结束后的存档与经验入库。 + --- ## 1. 触发词(语义化,理解意图即可) diff --git a/miniprogram/app.js b/miniprogram/app.js index 1a92cf58..aacfcca8 100644 --- a/miniprogram/app.js +++ b/miniprogram/app.js @@ -8,9 +8,9 @@ const { parseScene } = require('./utils/scene.js') App({ globalData: { // API基础地址 - 连接真实后端 - // baseUrl: 'https://soulapi.quwanzhi.com', + baseUrl: 'https://soulapi.quwanzhi.com', // baseUrl: 'https://souldev.quwanzhi.com', - baseUrl: 'http://localhost:8080', + // baseUrl: 'http://localhost:8080', // 小程序配置 - 真实AppID diff --git a/miniprogram/pages/my/my.js b/miniprogram/pages/my/my.js index ba873f4b..b1a3889e 100644 --- a/miniprogram/pages/my/my.js +++ b/miniprogram/pages/my/my.js @@ -58,6 +58,9 @@ Page({ showNicknameModal: false, editingNickname: '', + // 头像弹窗(含 chooseAvatar 按钮,必须用户点击才可获取微信头像) + showAvatarModal: false, + // 手机/微信号弹窗(stitch_soul comprehensive_profile_editor_v1_2) showContactModal: false, contactPhone: '', @@ -290,11 +293,11 @@ Page({ wx.showToast({ title: '已刷新', icon: 'success' }) }, - // 微信原生获取头像(button open-type="chooseAvatar" 回调) + // 微信原生获取头像(button open-type="chooseAvatar" 回调,真正获取微信头像) async onChooseAvatar(e) { - const tempAvatarUrl = e.detail.avatarUrl + const tempAvatarUrl = e.detail?.avatarUrl + this.setData({ showAvatarModal: false }) if (!tempAvatarUrl) return - wx.showLoading({ title: '上传中...', mask: true }) try { @@ -659,32 +662,59 @@ Page({ } catch (e) { console.log('[My] VIP查询失败', e) } }, - // 头像点击:已登录弹出选项(改头像/进VIP) + // 头像点击:已登录弹出选项(微信头像 / 相册 / VIP) onAvatarTap() { if (!this.data.isLoggedIn) { this.showLogin(); return } wx.showActionSheet({ - itemList: ['获取微信头像', '开通/管理VIP'], + itemList: ['获取微信头像', '从相册选择', '开通/管理VIP'], success: (res) => { - if (res.tapIndex === 0) this.chooseAvatarFallback() - if (res.tapIndex === 1) this.goToVip() + if (res.tapIndex === 0) this.setData({ showAvatarModal: true }) + if (res.tapIndex === 1) this.chooseAvatarFromAlbum() + if (res.tapIndex === 2) this.goToVip() } }) }, - chooseAvatarFallback() { + closeAvatarModal() { + this.setData({ showAvatarModal: false }) + }, + + // 从相册/相机选择(自定义图片) + chooseAvatarFromAlbum() { wx.chooseMedia({ count: 1, mediaType: ['image'], sourceType: ['album', 'camera'], success: async (res) => { const tempPath = res.tempFiles[0].tempFilePath - const userInfo = this.data.userInfo - userInfo.avatar = tempPath - this.setData({ userInfo }) - app.globalData.userInfo = userInfo - wx.setStorageSync('userInfo', userInfo) + wx.showLoading({ title: '上传中...', mask: true }) try { - await app.request('/api/miniprogram/user/update', { method: 'POST', data: { userId: userInfo.id, avatar: tempPath } }) - } catch (e) { console.log('头像同步失败', e) } - wx.showToast({ title: '头像已更新', icon: 'success' }) + 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 data = JSON.parse(r.data) + data.success ? resolve(data) : reject(new Error(data.error || '上传失败')) + } catch (e) { reject(new Error('解析失败')) } + }, + fail: (e) => reject(e) + }) + }) + const avatarUrl = app.globalData.baseUrl + uploadRes.data.url + const userInfo = this.data.userInfo + userInfo.avatar = avatarUrl + this.setData({ userInfo }) + app.globalData.userInfo = userInfo + wx.setStorageSync('userInfo', userInfo) + await app.request('/api/miniprogram/user/update', { method: 'POST', data: { userId: userInfo.id, avatar: avatarUrl } }) + wx.hideLoading() + wx.showToast({ title: '头像已更新', icon: 'success' }) + } catch (e) { + wx.hideLoading() + wx.showToast({ title: e.message || '上传失败,请重试', icon: 'none' }) + } } }) }, diff --git a/miniprogram/pages/my/my.wxml b/miniprogram/pages/my/my.wxml index 9094cf50..c333a623 100644 --- a/miniprogram/pages/my/my.wxml +++ b/miniprogram/pages/my/my.wxml @@ -186,6 +186,17 @@ + + + + + 获取微信头像 + 点击下方按钮使用你的微信头像 + + 取消 + + + diff --git a/miniprogram/pages/my/my.wxss b/miniprogram/pages/my/my.wxss index 4e81abc4..91026a52 100644 --- a/miniprogram/pages/my/my.wxss +++ b/miniprogram/pages/my/my.wxss @@ -3,14 +3,20 @@ * 设计稿:primary #4FD1C5, vip-gold #C8A146, card-dark #1A1A1A, card-inner #252525 */ -.page { min-height: 100vh; background: #121212; padding-bottom: 220rpx; } +/* 真机适配:底部留足 TabBar + 安全区,避免「我的订单」被遮挡 */ +.page { + min-height: 100vh; + background: #121212; + padding-bottom: calc(220rpx + env(safe-area-inset-bottom, 0px)); +} -/* ===== 导航栏 ===== */ +/* ===== 导航栏(避让右上角系统胶囊) ===== */ .nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(18,18,18,0.9); backdrop-filter: blur(8rpx); display: flex; align-items: center; - min-height: 44px; padding: 0 32rpx; + min-height: 44px; + padding: 0 200rpx 0 32rpx; /* 右侧 200rpx 避让真机右上角胶囊 */ border-bottom: 1rpx solid rgba(255,255,255,0.05); } .nav-title { font-size: 40rpx; font-weight: bold; color: #4FD1C5; } @@ -155,6 +161,18 @@ .agree-link { color: #4FD1C5; text-decoration: underline; padding: 0 4rpx; } .btn-wechat-disabled { opacity: 0.6; } +/* 头像弹窗 */ +.avatar-modal .avatar-modal-title { display: block; font-size: 36rpx; font-weight: bold; color: #fff; text-align: center; margin-bottom: 16rpx; } +.avatar-modal .avatar-modal-desc { display: block; font-size: 26rpx; color: rgba(255,255,255,0.6); text-align: center; margin-bottom: 32rpx; } +.avatar-modal .btn-choose-avatar { + width: 100%; height: 88rpx; margin: 0 0 24rpx 0; padding: 0; + display: flex; align-items: center; justify-content: center; + background: #4FD1C5; color: #000; font-size: 30rpx; font-weight: 600; + border-radius: 44rpx; border: none; +} +.avatar-modal .btn-choose-avatar::after { border: none; } +.avatar-modal .avatar-modal-cancel { display: block; text-align: center; font-size: 28rpx; color: rgba(255,255,255,0.5); padding: 16rpx; } + /* 手机/微信号弹窗 */ .contact-modal-overlay { background: rgba(0,0,0,0.85); backdrop-filter: blur(8rpx); } .contact-modal { width: 90%; max-width: 600rpx; background: #1A1A1A; border-radius: 48rpx; padding: 48rpx 40rpx; border: 1rpx solid rgba(255,255,255,0.1); } @@ -181,4 +199,5 @@ .modal-btn-cancel { background: rgba(255,255,255,0.1); color: #fff; } .modal-btn-confirm { background: #4FD1C5; color: #000; font-weight: 600; } -.bottom-space { height: 80rpx; } +/* 底部留白:配合 page padding-bottom,避免内容被 TabBar 遮挡 */ +.bottom-space { height: calc(80rpx + env(safe-area-inset-bottom, 0px)); } diff --git a/miniprogram/pages/vip/vip.wxml b/miniprogram/pages/vip/vip.wxml index 02ccc6b2..ff304a30 100644 --- a/miniprogram/pages/vip/vip.wxml +++ b/miniprogram/pages/vip/vip.wxml @@ -48,11 +48,11 @@ - + - + diff --git a/miniprogram/pages/vip/vip.wxss b/miniprogram/pages/vip/vip.wxss index 31e3123b..d8258be0 100644 --- a/miniprogram/pages/vip/vip.wxss +++ b/miniprogram/pages/vip/vip.wxss @@ -28,11 +28,17 @@ .benefit-title { font-size: 26rpx; font-weight: bold; color: #fff; } .benefit-desc { font-size: 20rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; line-height: 1.4; } -/* 底部固定购买按钮 - 设计稿 */ -.buy-footer { position: fixed; bottom: 0; left: 0; right: 0; padding: 24rpx 32rpx; padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); background: rgba(0,0,0,0.95); border-top: 1rpx solid rgba(255,255,255,0.05); z-index: 50; } -.buy-btn-fixed { width: 100%; height: 96rpx; padding: 0; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #FFD700, #FFB000); color: #000; font-size: 32rpx; font-weight: bold; border-radius: 48rpx; border: none; box-shadow: 0 8rpx 32rpx rgba(255,188,46,0.2); } -.buy-btn-fixed::after { border: none; } -.buy-btn-fixed[disabled] { opacity: 0.6; } +/* 底部固定购买按钮 - 宽度拉满屏幕(用 view 替代 button 避让默认 margin) */ +.buy-footer { position: fixed; bottom: 0; left: 0; right: 0; padding: 24rpx 20rpx; padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); background: rgba(0,0,0,0.95); border-top: 1rpx solid rgba(255,255,255,0.05); z-index: 50; box-sizing: border-box; } +.buy-btn-fixed { + width: 100%; + height: 96rpx; + display: flex; align-items: center; justify-content: center; + background: linear-gradient(135deg, #FFD700, #FFB000); color: #000; + font-size: 32rpx; font-weight: bold; border-radius: 48rpx; + box-shadow: 0 8rpx 32rpx rgba(255,188,46,0.2); +} +.buy-btn-disabled { opacity: 0.6; pointer-events: none; } .bottom-spacer { height: 180rpx; } .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; } diff --git a/miniprogram/project.private.config.json b/miniprogram/project.private.config.json index 2947b963..fa7f85a9 100644 --- a/miniprogram/project.private.config.json +++ b/miniprogram/project.private.config.json @@ -24,12 +24,26 @@ "miniprogram": { "list": [ { - "name": "pages/profile-edit/profile-edit", - "pathName": "pages/profile-edit/profile-edit", + "name": "pages/about/about", + "pathName": "pages/about/about", "query": "", "scene": null, "launchMode": "default" }, + { + "name": "pages/vip/vip", + "pathName": "pages/vip/vip", + "query": "", + "launchMode": "default", + "scene": null + }, + { + "name": "pages/profile-edit/profile-edit", + "pathName": "pages/profile-edit/profile-edit", + "query": "", + "launchMode": "default", + "scene": null + }, { "name": "个人资料", "pathName": "pages/profile-show/profile-show", diff --git a/soul-api/internal/handler/orders.go b/soul-api/internal/handler/orders.go index 27a34391..fb47a71c 100644 --- a/soul-api/internal/handler/orders.go +++ b/soul-api/internal/handler/orders.go @@ -6,6 +6,7 @@ import ( "net/http" "strconv" "strings" + "sync" "time" "soul-api/internal/database" @@ -13,6 +14,7 @@ import ( "soul-api/internal/wechat" "github.com/gin-gonic/gin" + "gorm.io/gorm" ) // OrdersList GET /api/orders(带用户昵称/头像/手机号,分销佣金按配置比例计算;支持分页 page、pageSize,筛选 status,搜索 search) @@ -29,50 +31,64 @@ func OrdersList(c *gin.Context) { pageSize = 10 } - q := db.Model(&model.Order{}) - if statusFilter != "" && statusFilter != "all" { - if statusFilter == "completed" { - q = q.Where("status IN ?", []string{"paid", "completed"}) - } else { - q = q.Where("status = ?", statusFilter) // 含 refunded、pending、created、failed + // 预加载 referral_config,避免订单循环内 N+1 查询 + var refCfgRow model.SystemConfig + refCfg := (*model.SystemConfig)(nil) + if err := db.Where("config_key = ?", "referral_config").First(&refCfgRow).Error; err == nil { + refCfg = &refCfgRow + } + + // 构建带筛选的查询(count 与 list 共用条件) + applyOrdersFilter := func(q *gorm.DB) *gorm.DB { + if statusFilter != "" && statusFilter != "all" { + if statusFilter == "completed" { + q = q.Where("status IN ?", []string{"paid", "completed"}) + } else { + q = q.Where("status = ?", statusFilter) + } } + if search != "" { + pattern := "%" + search + "%" + q = q.Where("order_sn LIKE ? OR id LIKE ? OR user_id IN (SELECT id FROM users WHERE COALESCE(nickname,'') LIKE ? OR COALESCE(phone,'') LIKE ? OR id LIKE ?)", + pattern, pattern, pattern, pattern, pattern) + } + return q } - if search != "" { - pattern := "%" + search + "%" - q = q.Where("order_sn LIKE ? OR id LIKE ? OR user_id IN (SELECT id FROM users WHERE COALESCE(nickname,'') LIKE ? OR COALESCE(phone,'') LIKE ? OR id LIKE ?)", - pattern, pattern, pattern, pattern, pattern) - } + var total int64 - q.Count(&total) - var totalRevenue, todayRevenue float64 - db.Model(&model.Order{}).Select("COALESCE(SUM(amount), 0)"). - Where("status IN ?", []string{"paid", "completed"}).Scan(&totalRevenue) - todayStart := time.Now().Truncate(24 * time.Hour) - todayEnd := todayStart.Add(24 * time.Hour) - db.Model(&model.Order{}).Select("COALESCE(SUM(amount), 0)"). - Where("status IN ? AND created_at >= ? AND created_at < ?", []string{"paid", "completed"}, todayStart, todayEnd). - Scan(&todayRevenue) - var orders []model.Order - query := db.Model(&model.Order{}) - if statusFilter != "" && statusFilter != "all" { - if statusFilter == "completed" { - query = query.Where("status IN ?", []string{"paid", "completed"}) - } else { - query = query.Where("status = ?", statusFilter) - } - } - if search != "" { - pattern := "%" + search + "%" - query = query.Where("order_sn LIKE ? OR id LIKE ? OR user_id IN (SELECT id FROM users WHERE COALESCE(nickname,'') LIKE ? OR COALESCE(phone,'') LIKE ? OR id LIKE ?)", - pattern, pattern, pattern, pattern, pattern) - } - if err := query.Order("created_at DESC"). - Offset((page - 1) * pageSize). - Limit(pageSize). - Find(&orders).Error; err != nil { - c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "orders": []interface{}{}, "total": 0}) + var ordersErr error + var wg sync.WaitGroup + + // 并行:count、营收统计、订单列表 + wg.Add(3) + go func() { + defer wg.Done() + applyOrdersFilter(db.Model(&model.Order{})).Count(&total) + }() + go func() { + defer wg.Done() + db.Model(&model.Order{}).Select("COALESCE(SUM(amount), 0)"). + Where("status IN ?", []string{"paid", "completed"}).Scan(&totalRevenue) + todayStart := time.Now().Truncate(24 * time.Hour) + todayEnd := todayStart.Add(24 * time.Hour) + db.Model(&model.Order{}).Select("COALESCE(SUM(amount), 0)"). + Where("status IN ? AND created_at >= ? AND created_at < ?", []string{"paid", "completed"}, todayStart, todayEnd). + Scan(&todayRevenue) + }() + go func() { + defer wg.Done() + query := applyOrdersFilter(db.Model(&model.Order{})) + ordersErr = query.Order("created_at DESC"). + Offset((page - 1) * pageSize). + Limit(pageSize). + Find(&orders).Error + }() + wg.Wait() + + if ordersErr != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "error": ordersErr.Error(), "orders": []interface{}{}, "total": 0}) return } totalPages := int(total) / pageSize @@ -147,7 +163,7 @@ func OrdersList(c *gin.Context) { if u := userMap[*o.ReferrerID]; u != nil { refUser = u } - m["referrerEarnings"] = computeOrderCommission(db, &o, refUser) + m["referrerEarnings"] = computeOrderCommission(db, &o, refUser, refCfg) } else { m["referrerEarnings"] = nil } diff --git a/soul-api/internal/handler/referral_commission.go b/soul-api/internal/handler/referral_commission.go index 7394e830..5011da4a 100644 --- a/soul-api/internal/handler/referral_commission.go +++ b/soul-api/internal/handler/referral_commission.go @@ -13,7 +13,8 @@ import ( // 会员订单:推广者会员 20%、非会员 10%;内容订单:90%(好友优惠 5% 仅针对内容) // order: 已支付订单,需有 product_type、amount、referrer_id // referrerUser: 推广者用户信息,用于判断 is_vip(可为 nil,会查库) -func computeOrderCommission(db *gorm.DB, order *model.Order, referrerUser *model.User) float64 { +// preloadConfig: 可选,预加载的 referral_config,避免 N+1 查询 +func computeOrderCommission(db *gorm.DB, order *model.Order, referrerUser *model.User, preloadConfig ...*model.SystemConfig) float64 { if order == nil || order.ReferrerID == nil || *order.ReferrerID == "" { return 0 } @@ -22,8 +23,17 @@ func computeOrderCommission(db *gorm.DB, order *model.Order, referrerUser *model userDiscount := 0.0 vipOrderShareVip := 20.0 vipOrderShareNonVip := 10.0 - var cfg model.SystemConfig - if err := db.Where("config_key = ?", "referral_config").First(&cfg).Error; err == nil { + var cfg *model.SystemConfig + if len(preloadConfig) > 0 && preloadConfig[0] != nil { + cfg = preloadConfig[0] + } else if row, err := (func() (*model.SystemConfig, error) { + var r model.SystemConfig + e := db.Where("config_key = ?", "referral_config").First(&r).Error + return &r, e + })(); err == nil { + cfg = row + } + if cfg != nil { var config map[string]interface{} if err := json.Unmarshal(cfg.ConfigValue, &config); err == nil { if share, ok := config["distributorShare"].(float64); ok { diff --git a/soul-api/internal/handler/vip.go b/soul-api/internal/handler/vip.go index 112a31aa..7ad501f6 100644 --- a/soul-api/internal/handler/vip.go +++ b/soul-api/internal/handler/vip.go @@ -271,20 +271,26 @@ func VipMembers(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true, "data": list, "total": len(list)}) } -// formatVipMember 仅从 vip_* 字段构建会员展示数据,不混入用户信息 -// 用于创业老板排行等场景;未填会员资料时 name 显示「创业者」占位 +// formatVipMember 构建会员展示数据;优先 vip_*,无则回退到用户 nickname/avatar +// 用于首页超级个体、创业老板排行等场景,展示真实用户头像和昵称 func formatVipMember(u *model.User, isVip bool) gin.H { name := "" - if u.VipName != nil { + if u.VipName != nil && *u.VipName != "" { name = *u.VipName } + if name == "" && u.Nickname != nil && *u.Nickname != "" { + name = *u.Nickname + } if name == "" { name = "创业者" } avatar := "" - if u.VipAvatar != nil { + if u.VipAvatar != nil && *u.VipAvatar != "" { avatar = *u.VipAvatar } + if avatar == "" && u.Avatar != nil && *u.Avatar != "" { + avatar = *u.Avatar + } project := "" if u.VipProject != nil { project = *u.VipProject @@ -302,17 +308,19 @@ func formatVipMember(u *model.User, isVip bool) gin.H { vipRole = *u.VipRole } return gin.H{ - "id": u.ID, - "name": name, - "nickname": name, - "avatar": avatar, - "vipName": name, - "vipRole": vipRole, - "vipAvatar": avatar, - "vipProject": project, - "vipContact": contact, - "vipBio": bio, - "is_vip": isVip, + "id": u.ID, + "name": name, + "nickname": name, + "avatar": avatar, + "vip_name": name, + "vipName": name, + "vipRole": vipRole, + "vip_avatar": avatar, + "vipAvatar": avatar, + "vipProject": project, + "vipContact": contact, + "vipBio": bio, + "is_vip": isVip, } } diff --git a/soul-api/soul-api b/soul-api/soul-api index 113f21a4..4b2abc42 100755 Binary files a/soul-api/soul-api and b/soul-api/soul-api differ