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