更新输入框边距规范,增加资源对接弹窗的布局修正,确保在小程序开发中避免文字贴边问题。补充相关口诀以提升开发一致性,并在经验清单中记录最新最佳实践。调整项目索引以反映最新进展,增强文档的可用性与可追溯性。
This commit is contained in:
@@ -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` 结构。
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
|
||||
| 日期 | 摘要 | 文件 |
|
||||
|------|------|------|
|
||||
| 2026-02-28 | input 边距口诀、match 资源对接弹窗修正 | [2026-02-28.md](./2026-02-28.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
|
||||
|
||||
@@ -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,状态用:已完成 / 进行中 / 待续 / 搁置
|
||||
|
||||
|
||||
@@ -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 工具读取**绝对路径**的完整文件内容后执行,不可跳过或仅凭记忆。
|
||||
|
||||
@@ -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。
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ description: Soul 创业派对开发团队多角色会议。语义化触发:
|
||||
|
||||
当触发会议关键词时,使用本 Skill 主持多角色会议,确保各角色充分发言、形成决议、橙子生成会议纪要。
|
||||
|
||||
**主持人约定**:**乘风 = 老板分身**。会议中乘风(老板分身)为主持人,负责定议题、执行质疑轮、形成决议;橙子为书记员,负责会议结束后的存档与经验入库。
|
||||
|
||||
---
|
||||
|
||||
## 1. 触发词(语义化,理解意图即可)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -186,6 +186,17 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 头像弹窗:必须点击 button 才能获取微信头像(隐私规范) -->
|
||||
<view class="modal-overlay" wx:if="{{showAvatarModal}}" bindtap="closeAvatarModal">
|
||||
<view class="modal-content avatar-modal" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeAvatarModal">✕</view>
|
||||
<text class="avatar-modal-title">获取微信头像</text>
|
||||
<text class="avatar-modal-desc">点击下方按钮使用你的微信头像</text>
|
||||
<button class="btn-choose-avatar" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">使用微信头像</button>
|
||||
<view class="avatar-modal-cancel" bindtap="closeAvatarModal">取消</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 修改昵称弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showNicknameModal}}" bindtap="closeNicknameModal">
|
||||
<view class="modal-content nickname-modal" catchtap="stopPropagation">
|
||||
|
||||
@@ -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)); }
|
||||
|
||||
@@ -48,11 +48,11 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 底部固定购买按钮(非 VIP 时显示) -->
|
||||
<!-- 底部固定购买按钮(非 VIP 时显示,用 view 避让 button 默认 margin) -->
|
||||
<view class="buy-footer" wx:if="{{!isVip}}">
|
||||
<button class="buy-btn-fixed" bindtap="handlePurchase" disabled="{{purchasing}}">
|
||||
<view class="buy-btn-fixed {{purchasing ? 'buy-btn-disabled' : ''}}" bindtap="handlePurchase">
|
||||
{{purchasing ? "处理中..." : "¥" + price + "/年 加入创业派对"}}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bottom-spacer" wx:if="{{!isVip}}"></view>
|
||||
<!-- VIP资料填写(仅VIP可见) -->
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user