package handler import ( "fmt" "net/http" "strconv" "time" "soul-api/internal/database" "soul-api/internal/model" "soul-api/internal/wechat" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // BalanceGet GET /api/miniprogram/balance?userId= func BalanceGet(c *gin.Context) { userID := c.Query("userId") if userID == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 userId"}) return } db := database.DB() var ub model.UserBalance if err := db.Where("user_id = ?", userID).First(&ub).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"balance": 0}}) return } c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"balance": ub.Balance}}) } // BalanceTransactionsGet GET /api/miniprogram/balance/transactions?userId=&page=&pageSize= func BalanceTransactionsGet(c *gin.Context) { userID := c.Query("userId") if userID == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 userId"}) return } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20")) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 20 } db := database.DB() var total int64 db.Model(&model.BalanceTransaction{}).Where("user_id = ?", userID).Count(&total) var list []model.BalanceTransaction if err := db.Where("user_id = ?", userID).Order("created_at DESC"). Offset((page - 1) * pageSize).Limit(pageSize).Find(&list).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "data": []interface{}{}, "total": 0}) return } out := make([]gin.H, 0, len(list)) for _, t := range list { orderID := "" if t.OrderID != nil { orderID = *t.OrderID } out = append(out, gin.H{ "id": t.ID, "type": t.Type, "amount": t.Amount, "orderId": orderID, "createdAt": t.CreatedAt, }) } c.JSON(http.StatusOK, gin.H{"success": true, "data": out, "total": total}) } // BalanceRechargePost POST /api/miniprogram/balance/recharge func BalanceRechargePost(c *gin.Context) { var req struct { UserID string `json:"userId" binding:"required"` Amount float64 `json:"amount" binding:"required,gte=0.01"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数无效"}) return } orderSn := wechat.GenerateOrderSn() db := database.DB() desc := fmt.Sprintf("余额充值 ¥%.2f", req.Amount) status := "created" order := model.Order{ ID: orderSn, OrderSN: orderSn, UserID: req.UserID, ProductType: "balance_recharge", ProductID: &orderSn, Amount: req.Amount, Description: &desc, Status: &status, } if err := db.Create(&order).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "创建订单失败"}) return } c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"orderSn": orderSn}}) } // BalanceRechargeConfirmPost POST /api/miniprogram/balance/recharge/confirm func BalanceRechargeConfirmPost(c *gin.Context) { var req struct { OrderSn string `json:"orderSn" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 orderSn"}) return } db := database.DB() err := db.Transaction(func(tx *gorm.DB) error { var order model.Order if err := tx.Where("order_sn = ? AND product_type = ?", req.OrderSn, "balance_recharge").First(&order).Error; err != nil { return err } status := "" if order.Status != nil { status = *order.Status } if status != "paid" { return fmt.Errorf("订单未支付") } // 幂等:检查是否已处理 var cnt int64 tx.Model(&model.BalanceTransaction{}).Where("order_id = ? AND type = ?", req.OrderSn, "recharge").Count(&cnt) if cnt > 0 { return nil // 已处理,直接成功 } // 增加余额 tx.Exec("INSERT INTO user_balances (user_id, balance, updated_at) VALUES (?, 0, NOW()) ON DUPLICATE KEY UPDATE balance = balance + ?, updated_at = NOW()", order.UserID, order.Amount) txID := fmt.Sprintf("bt_%d", time.Now().UnixNano()%100000000000) tx.Create(&model.BalanceTransaction{ ID: txID, UserID: order.UserID, Type: "recharge", Amount: order.Amount, OrderID: &req.OrderSn, CreatedAt: time.Now(), }) return nil }) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true}) } // BalanceConsumePost POST /api/miniprogram/balance/consume func BalanceConsumePost(c *gin.Context) { var req struct { UserID string `json:"userId" binding:"required"` ProductType string `json:"productType" binding:"required"` ProductID string `json:"productId"` Amount float64 `json:"amount" binding:"required,gte=0.01"` ReferralCode string `json:"referralCode"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数无效"}) return } db := database.DB() // 后端价格校验 standardPrice, priceErr := getStandardPrice(db, req.ProductType, req.ProductID) if priceErr != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": priceErr.Error()}) return } if req.Amount < standardPrice-0.01 || req.Amount > standardPrice+0.01 { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "金额校验失败"}) return } amount := standardPrice // 解析推荐人 var referrerID *string if req.ReferralCode != "" { var refUser model.User if err := db.Where("referral_code = ?", req.ReferralCode).First(&refUser).Error; err == nil { referrerID = &refUser.ID } } if referrerID == nil { var binding struct { ReferrerID string `gorm:"column:referrer_id"` } _ = db.Raw("SELECT referrer_id FROM referral_bindings WHERE referee_id = ? AND status = 'active' AND expiry_date > NOW() ORDER BY binding_date DESC LIMIT 1", req.UserID).Scan(&binding).Error if binding.ReferrerID != "" { referrerID = &binding.ReferrerID } } productID := req.ProductID if productID == "" { switch req.ProductType { case "vip": productID = "vip_annual" case "fullbook": productID = "fullbook" default: productID = req.ProductID } } err := db.Transaction(func(tx *gorm.DB) error { var ub model.UserBalance if err := tx.Where("user_id = ?", req.UserID).First(&ub).Error; err != nil { if err == gorm.ErrRecordNotFound { return fmt.Errorf("余额不足") } return err } if ub.Balance < amount { return fmt.Errorf("余额不足") } tx.Model(&model.UserBalance{}).Where("user_id = ?", req.UserID).Update("balance", gorm.Expr("balance - ?", amount)) orderSn := wechat.GenerateOrderSn() desc := "" switch req.ProductType { case "section": desc = "章节购买-" + productID case "fullbook": desc = "《一场Soul的创业实验》全书" case "vip": desc = "卡若创业派对VIP年度会员(365天)" default: desc = "余额消费" } pm := "balance" status := "paid" now := time.Now() order := model.Order{ ID: orderSn, OrderSN: orderSn, UserID: req.UserID, ProductType: req.ProductType, ProductID: &productID, Amount: amount, Description: &desc, Status: &status, PaymentMethod: &pm, ReferrerID: referrerID, PayTime: &now, } if err := tx.Create(&order).Error; err != nil { return err } txID := fmt.Sprintf("bt_%d", time.Now().UnixNano()%100000000000) tx.Create(&model.BalanceTransaction{ ID: txID, UserID: req.UserID, Type: "consume", Amount: -amount, OrderID: &orderSn, CreatedAt: now, }) // 激活权益 if req.ProductType == "fullbook" { tx.Model(&model.User{}).Where("id = ?", req.UserID).Update("has_full_book", true) } else if req.ProductType == "vip" { activateVIP(tx, req.UserID, 365, now) } // 分佣 if referrerID != nil { processReferralCommission(tx, req.UserID, amount, orderSn, &order) } return nil }) if err != nil { if err.Error() == "余额不足" { c.JSON(http.StatusOK, gin.H{"success": false, "error": "余额不足"}) return } c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true}) } // BalanceRefundPost POST /api/miniprogram/balance/refund func BalanceRefundPost(c *gin.Context) { var req struct { UserID string `json:"userId" binding:"required"` Amount float64 `json:"amount" binding:"required,gte=0.01"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数无效"}) return } // 首版简化:暂不实现微信原路退,仅扣减余额并记录 db := database.DB() err := db.Transaction(func(tx *gorm.DB) error { var ub model.UserBalance if err := tx.Where("user_id = ?", req.UserID).First(&ub).Error; err != nil { if err == gorm.ErrRecordNotFound { return fmt.Errorf("余额为零") } return err } if ub.Balance < req.Amount { return fmt.Errorf("余额不足") } tx.Model(&model.UserBalance{}).Where("user_id = ?", req.UserID).Update("balance", gorm.Expr("balance - ?", req.Amount)) txID := fmt.Sprintf("bt_%d", time.Now().UnixNano()%100000000000) tx.Create(&model.BalanceTransaction{ ID: txID, UserID: req.UserID, Type: "refund", Amount: -req.Amount, CreatedAt: time.Now(), }) return nil }) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"message": "退款申请已提交,1-3个工作日内原路返回"}}) } // AdminUserBalanceGet GET /api/admin/users/:id/balance 管理端-用户余额与最近交易 func AdminUserBalanceGet(c *gin.Context) { userID := c.Param("id") if userID == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少用户ID"}) return } db := database.DB() var ub model.UserBalance balance := 0.0 if err := db.Where("user_id = ?", userID).First(&ub).Error; err == nil { balance = ub.Balance } var list []model.BalanceTransaction db.Where("user_id = ?", userID).Order("created_at DESC").Limit(20).Find(&list) transactions := make([]gin.H, 0, len(list)) for _, t := range list { orderID := "" if t.OrderID != nil { orderID = *t.OrderID } transactions = append(transactions, gin.H{ "id": t.ID, "type": t.Type, "amount": t.Amount, "orderId": orderID, "createdAt": t.CreatedAt, }) } c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"balance": balance, "transactions": transactions}}) } // AdminUserBalanceAdjust POST /api/admin/users/:id/balance/adjust 管理端-人工调整用户余额 func AdminUserBalanceAdjust(c *gin.Context) { userID := c.Param("id") if userID == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少用户ID"}) return } var req struct { Amount float64 `json:"amount" binding:"required"` // 正数增加,负数扣减 Remark string `json:"remark"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数无效"}) return } if req.Amount == 0 { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "调整金额不能为 0"}) return } db := database.DB() err := db.Transaction(func(tx *gorm.DB) error { var ub model.UserBalance if err := tx.Where("user_id = ?", userID).First(&ub).Error; err != nil { if err == gorm.ErrRecordNotFound { ub = model.UserBalance{UserID: userID, Balance: 0} } else { return err } } newBalance := ub.Balance + req.Amount if newBalance < 0 { return fmt.Errorf("调整后余额不能为负,当前余额 %.2f", ub.Balance) } tx.Exec("INSERT INTO user_balances (user_id, balance, updated_at) VALUES (?, 0, NOW()) ON DUPLICATE KEY UPDATE balance = ?, updated_at = NOW()", userID, newBalance) txID := fmt.Sprintf("bt_adj_%d", time.Now().UnixNano()%100000000000) return tx.Create(&model.BalanceTransaction{ ID: txID, UserID: userID, Type: "admin_adjust", Amount: req.Amount, CreatedAt: time.Now(), }).Error }) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true, "message": "余额已调整"}) } // ConfirmBalanceRechargeByOrder 支付成功后确认充值(幂等),供 PayNotify 和 activateOrderBenefits 调用 func ConfirmBalanceRechargeByOrder(db *gorm.DB, order *model.Order) error { if order == nil || order.ProductType != "balance_recharge" { return nil } orderSn := order.OrderSN return db.Transaction(func(tx *gorm.DB) error { var cnt int64 tx.Model(&model.BalanceTransaction{}).Where("order_id = ? AND type = ?", orderSn, "recharge").Count(&cnt) if cnt > 0 { return nil // 已处理,幂等 } tx.Exec("INSERT INTO user_balances (user_id, balance, updated_at) VALUES (?, 0, NOW()) ON DUPLICATE KEY UPDATE balance = balance + ?, updated_at = NOW()", order.UserID, order.Amount) txID := fmt.Sprintf("bt_%d", time.Now().UnixNano()%100000000000) return tx.Create(&model.BalanceTransaction{ ID: txID, UserID: order.UserID, Type: "recharge", Amount: order.Amount, OrderID: &orderSn, CreatedAt: time.Now(), }).Error }) }