268 lines
7.8 KiB
Go
268 lines
7.8 KiB
Go
|
|
package handler
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"encoding/json"
|
|||
|
|
"net/http"
|
|||
|
|
"strconv"
|
|||
|
|
"strings"
|
|||
|
|
"sync"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"soul-api/internal/database"
|
|||
|
|
"soul-api/internal/model"
|
|||
|
|
"soul-api/internal/wechat"
|
|||
|
|
|
|||
|
|
"github.com/gin-gonic/gin"
|
|||
|
|
"gorm.io/gorm"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// OrdersList GET /api/orders(带用户昵称/头像/手机号,分销佣金按配置比例计算;支持分页 page、pageSize,筛选 status,搜索 search)
|
|||
|
|
func OrdersList(c *gin.Context) {
|
|||
|
|
db := database.DB()
|
|||
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|||
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
|||
|
|
statusFilter := c.Query("status")
|
|||
|
|
search := strings.TrimSpace(c.Query("search"))
|
|||
|
|
if page < 1 {
|
|||
|
|
page = 1
|
|||
|
|
}
|
|||
|
|
if pageSize < 1 || pageSize > 100 {
|
|||
|
|
pageSize = 10
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 预加载 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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var total int64
|
|||
|
|
var totalRevenue, todayRevenue float64
|
|||
|
|
var orders []model.Order
|
|||
|
|
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
|
|||
|
|
if int(total)%pageSize > 0 {
|
|||
|
|
totalPages++
|
|||
|
|
}
|
|||
|
|
if len(orders) == 0 {
|
|||
|
|
c.JSON(http.StatusOK, gin.H{
|
|||
|
|
"success": true, "orders": []interface{}{},
|
|||
|
|
"total": total, "page": page, "pageSize": pageSize, "totalPages": totalPages,
|
|||
|
|
})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 收集订单中的 user_id、referrer_id,查用户信息
|
|||
|
|
userIDs := make(map[string]bool)
|
|||
|
|
for _, o := range orders {
|
|||
|
|
if o.UserID != "" {
|
|||
|
|
userIDs[o.UserID] = true
|
|||
|
|
}
|
|||
|
|
if o.ReferrerID != nil && *o.ReferrerID != "" {
|
|||
|
|
userIDs[*o.ReferrerID] = true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
ids := make([]string, 0, len(userIDs))
|
|||
|
|
for id := range userIDs {
|
|||
|
|
ids = append(ids, id)
|
|||
|
|
}
|
|||
|
|
var users []model.User
|
|||
|
|
if len(ids) > 0 {
|
|||
|
|
db.Where("id IN ?", ids).Find(&users)
|
|||
|
|
}
|
|||
|
|
userMap := make(map[string]*model.User)
|
|||
|
|
for i := range users {
|
|||
|
|
userMap[users[i].ID] = &users[i]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getStr := func(s *string) string {
|
|||
|
|
if s == nil || *s == "" {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
return *s
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
out := make([]gin.H, 0, len(orders))
|
|||
|
|
for _, o := range orders {
|
|||
|
|
// 序列化订单为基础字段
|
|||
|
|
b, _ := json.Marshal(o)
|
|||
|
|
var m map[string]interface{}
|
|||
|
|
_ = json.Unmarshal(b, &m)
|
|||
|
|
// 用户信息
|
|||
|
|
if u := userMap[o.UserID]; u != nil {
|
|||
|
|
m["userNickname"] = getStr(u.Nickname)
|
|||
|
|
m["userPhone"] = getStr(u.Phone)
|
|||
|
|
m["userAvatar"] = getStr(u.Avatar)
|
|||
|
|
} else {
|
|||
|
|
m["userNickname"] = ""
|
|||
|
|
m["userPhone"] = ""
|
|||
|
|
m["userAvatar"] = ""
|
|||
|
|
}
|
|||
|
|
// 推荐人信息
|
|||
|
|
if o.ReferrerID != nil && *o.ReferrerID != "" {
|
|||
|
|
if u := userMap[*o.ReferrerID]; u != nil {
|
|||
|
|
m["referrerNickname"] = getStr(u.Nickname)
|
|||
|
|
m["referrerCode"] = getStr(u.ReferralCode)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 分销佣金:仅对已支付且存在推荐人的订单,按 computeOrderCommission(会员 20%/10%,内容 90%)
|
|||
|
|
status := getStr(o.Status)
|
|||
|
|
if status == "paid" && o.ReferrerID != nil && *o.ReferrerID != "" {
|
|||
|
|
var refUser *model.User
|
|||
|
|
if u := userMap[*o.ReferrerID]; u != nil {
|
|||
|
|
refUser = u
|
|||
|
|
}
|
|||
|
|
m["referrerEarnings"] = computeOrderCommission(db, &o, refUser, refCfg)
|
|||
|
|
} else {
|
|||
|
|
m["referrerEarnings"] = nil
|
|||
|
|
}
|
|||
|
|
out = append(out, m)
|
|||
|
|
}
|
|||
|
|
c.JSON(http.StatusOK, gin.H{
|
|||
|
|
"success": true, "orders": out,
|
|||
|
|
"total": total, "page": page, "pageSize": pageSize, "totalPages": totalPages,
|
|||
|
|
"totalRevenue": totalRevenue, "todayRevenue": todayRevenue,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MiniprogramOrders GET /api/miniprogram/orders 小程序-当前用户订单列表(按 userId 过滤,返回 data)
|
|||
|
|
func MiniprogramOrders(c *gin.Context) {
|
|||
|
|
userID := c.Query("userId")
|
|||
|
|
if userID == "" {
|
|||
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
db := database.DB()
|
|||
|
|
var orders []model.Order
|
|||
|
|
if err := db.Where("user_id = ?", userID).Order("created_at DESC").Find(&orders).Error; err != nil {
|
|||
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
out := make([]gin.H, 0, len(orders))
|
|||
|
|
for _, o := range orders {
|
|||
|
|
desc := ""
|
|||
|
|
if o.Description != nil {
|
|||
|
|
desc = *o.Description
|
|||
|
|
}
|
|||
|
|
productID := ""
|
|||
|
|
if o.ProductID != nil {
|
|||
|
|
productID = *o.ProductID
|
|||
|
|
}
|
|||
|
|
status := "created"
|
|||
|
|
if o.Status != nil {
|
|||
|
|
status = *o.Status
|
|||
|
|
}
|
|||
|
|
out = append(out, gin.H{
|
|||
|
|
"id": o.ID, "order_sn": o.OrderSN, "user_id": o.UserID,
|
|||
|
|
"product_id": productID, "product_type": o.ProductType,
|
|||
|
|
"product_name": desc, "section_id": productID,
|
|||
|
|
"amount": o.Amount, "status": status,
|
|||
|
|
"created_at": o.CreatedAt,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": out})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AdminOrderRefund PUT /api/admin/orders/refund 管理端-订单退款(仅支持已支付订单,调用微信支付退款)
|
|||
|
|
func AdminOrderRefund(c *gin.Context) {
|
|||
|
|
var req struct {
|
|||
|
|
OrderSn string `json:"orderSn" binding:"required"`
|
|||
|
|
Reason string `json:"reason"`
|
|||
|
|
}
|
|||
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|||
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少订单号"})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
db := database.DB()
|
|||
|
|
var order model.Order
|
|||
|
|
if err := db.Where("order_sn = ?", req.OrderSn).First(&order).Error; err != nil {
|
|||
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "订单不存在"})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
status := ""
|
|||
|
|
if order.Status != nil {
|
|||
|
|
status = *order.Status
|
|||
|
|
}
|
|||
|
|
if status != "paid" && status != "completed" {
|
|||
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "仅支持已支付订单退款"})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
transactionID := ""
|
|||
|
|
if order.TransactionID != nil {
|
|||
|
|
transactionID = *order.TransactionID
|
|||
|
|
}
|
|||
|
|
if transactionID == "" {
|
|||
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "订单缺少微信支付单号,无法退款"})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
totalCents := int(order.Amount * 100)
|
|||
|
|
if totalCents < 1 {
|
|||
|
|
totalCents = 1
|
|||
|
|
}
|
|||
|
|
if err := wechat.RefundOrder(context.Background(), order.OrderSN, transactionID, totalCents, req.Reason); err != nil {
|
|||
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "微信退款失败: " + err.Error()})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
refunded := "refunded"
|
|||
|
|
updates := map[string]interface{}{"status": refunded}
|
|||
|
|
if req.Reason != "" {
|
|||
|
|
updates["refund_reason"] = req.Reason
|
|||
|
|
}
|
|||
|
|
if err := db.Model(&order).Updates(updates).Error; err != nil {
|
|||
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "退款成功但更新订单状态失败: " + err.Error()})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "退款成功"})
|
|||
|
|
}
|