package handler import ( "crypto/rand" "encoding/hex" "fmt" "net/http" "time" "soul-api/internal/database" "soul-api/internal/model" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // GET /api/miniprogram/balance 小程序-查询余额 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 bal model.UserBalance if err := db.Where("user_id = ?", userID).First(&bal).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"userId": userID, "balance": 0, "totalRecharged": 0, "totalGifted": 0, "totalRefunded": 0}}) return } c.JSON(http.StatusOK, gin.H{"success": true, "data": bal}) } // POST /api/miniprogram/balance/recharge 小程序-充值(创建充值订单) func BalanceRecharge(c *gin.Context) { var body struct { UserID string `json:"userId" binding:"required"` Amount float64 `json:"amount" binding:"required,gt=0"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数错误: " + err.Error()}) return } db := database.DB() orderSN := fmt.Sprintf("BAL_%d", time.Now().UnixNano()) order := model.Order{ ID: orderSN, OrderSN: orderSN, UserID: body.UserID, ProductType: "balance_recharge", Amount: body.Amount, } desc := fmt.Sprintf("余额充值 ¥%.2f", body.Amount) status := "pending" order.Description = &desc order.Status = &status if err := db.Create(&order).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "创建充值订单失败"}) return } c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"orderSn": orderSN, "amount": body.Amount}}) } // POST /api/miniprogram/balance/recharge/confirm 充值完成回调(内部或手动确认) func BalanceRechargeConfirm(c *gin.Context) { var body struct { OrderSN string `json:"orderSn" binding:"required"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数错误"}) return } db := database.DB() var order model.Order if err := db.Where("order_sn = ? AND product_type = ?", body.OrderSN, "balance_recharge").First(&order).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "订单不存在"}) return } if order.Status != nil && *order.Status == "paid" { c.JSON(http.StatusOK, gin.H{"success": true, "message": "已确认"}) return } err := db.Transaction(func(tx *gorm.DB) error { paid := "paid" now := time.Now() if err := tx.Model(&order).Updates(map[string]interface{}{"status": paid, "pay_time": now}).Error; err != nil { return err } var bal model.UserBalance if err := tx.Where("user_id = ?", order.UserID).First(&bal).Error; err != nil { bal = model.UserBalance{UserID: order.UserID} tx.Create(&bal) } if err := tx.Model(&bal).Updates(map[string]interface{}{ "balance": gorm.Expr("balance + ?", order.Amount), "total_recharged": gorm.Expr("total_recharged + ?", order.Amount), }).Error; err != nil { return err } tx.Create(&model.BalanceTransaction{ UserID: order.UserID, Type: "recharge", Amount: order.Amount, BalanceAfter: bal.Balance + order.Amount, RelatedOrder: &order.OrderSN, Description: fmt.Sprintf("充值 ¥%.2f", order.Amount), }) return nil }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "确认失败"}) return } c.JSON(http.StatusOK, gin.H{"success": true, "message": "充值成功"}) } // POST /api/miniprogram/balance/gift 小程序-代付解锁(用余额帮他人解锁章节) func BalanceGift(c *gin.Context) { var body struct { GiverID string `json:"giverId" binding:"required"` SectionID string `json:"sectionId" binding:"required"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数错误"}) return } db := database.DB() var chapter model.Chapter if err := db.Where("id = ?", body.SectionID).First(&chapter).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "章节不存在"}) return } price := float64(1) if chapter.Price != nil { price = *chapter.Price } if price <= 0 { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "该章节免费,无需代付"}) return } var giftCode string err := db.Transaction(func(tx *gorm.DB) error { var bal model.UserBalance if err := tx.Where("user_id = ?", body.GiverID).First(&bal).Error; err != nil || bal.Balance < price { return fmt.Errorf("余额不足,当前 ¥%.2f,需要 ¥%.2f", bal.Balance, price) } if err := tx.Model(&bal).Updates(map[string]interface{}{ "balance": gorm.Expr("balance - ?", price), "total_gifted": gorm.Expr("total_gifted + ?", price), }).Error; err != nil { return err } code := make([]byte, 16) rand.Read(code) giftCode = hex.EncodeToString(code) tx.Create(&model.GiftUnlock{ GiftCode: giftCode, GiverID: body.GiverID, SectionID: body.SectionID, Amount: price, Status: "pending", }) tx.Create(&model.BalanceTransaction{ UserID: body.GiverID, Type: "gift", Amount: -price, BalanceAfter: bal.Balance - price, SectionID: &body.SectionID, Description: fmt.Sprintf("代付章节 %s (¥%.2f)", body.SectionID, price), }) return nil }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{ "giftCode": giftCode, "sectionId": body.SectionID, "amount": price, }}) } // POST /api/miniprogram/balance/gift/redeem 领取代付礼物 func BalanceGiftRedeem(c *gin.Context) { var body struct { GiftCode string `json:"giftCode" binding:"required"` ReceiverID string `json:"receiverId" binding:"required"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数错误"}) return } db := database.DB() var gift model.GiftUnlock if err := db.Where("gift_code = ?", body.GiftCode).First(&gift).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "礼物码无效"}) return } if gift.Status != "pending" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "礼物已被领取"}) return } err := db.Transaction(func(tx *gorm.DB) error { now := time.Now() tx.Model(&gift).Updates(map[string]interface{}{ "receiver_id": body.ReceiverID, "status": "redeemed", "redeemed_at": now, }) orderSN := fmt.Sprintf("GIFT_%s", body.GiftCode[:8]) paid := "paid" desc := fmt.Sprintf("来自好友的代付解锁") tx.Create(&model.Order{ ID: orderSN, OrderSN: orderSN, UserID: body.ReceiverID, ProductType: "section", ProductID: &gift.SectionID, Amount: 0, Description: &desc, Status: &paid, PayTime: &now, ReferrerID: &gift.GiverID, }) return nil }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "领取失败"}) return } c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{ "sectionId": gift.SectionID, "message": "解锁成功!", }}) } // POST /api/miniprogram/balance/refund 申请余额退款(9折) func BalanceRefund(c *gin.Context) { var body struct { UserID string `json:"userId" binding:"required"` Amount float64 `json:"amount" binding:"required,gt=0"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数错误"}) return } db := database.DB() refundAmount := body.Amount * 0.9 err := db.Transaction(func(tx *gorm.DB) error { var bal model.UserBalance if err := tx.Where("user_id = ?", body.UserID).First(&bal).Error; err != nil || bal.Balance < body.Amount { return fmt.Errorf("余额不足") } if err := tx.Model(&bal).Updates(map[string]interface{}{ "balance": gorm.Expr("balance - ?", body.Amount), "total_refunded": gorm.Expr("total_refunded + ?", body.Amount), }).Error; err != nil { return err } tx.Create(&model.BalanceTransaction{ UserID: body.UserID, Type: "refund", Amount: -body.Amount, BalanceAfter: bal.Balance - body.Amount, Description: fmt.Sprintf("退款 ¥%.2f(原额 ¥%.2f,9折退回 ¥%.2f)", body.Amount, body.Amount, refundAmount), }) return nil }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{ "deducted": body.Amount, "refundAmount": refundAmount, "message": fmt.Sprintf("退款成功,实际退回 ¥%.2f", refundAmount), }}) } // GET /api/miniprogram/balance/transactions 交易记录(含余额变动 + 阅读消费订单) func BalanceTransactions(c *gin.Context) { userID := c.Query("userId") if userID == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 userId"}) return } db := database.DB() var txns []model.BalanceTransaction db.Where("user_id = ?", userID).Order("created_at DESC").Limit(50).Find(&txns) var orders []model.Order db.Where("user_id = ? AND product_type = ? AND status IN ?", userID, "section", []string{"paid", "completed", "success"}). Order("created_at DESC").Limit(50).Find(&orders) type txRow struct { ID string `json:"id"` Type string `json:"type"` Amount float64 `json:"amount"` Description string `json:"description"` CreatedAt interface{} `json:"createdAt"` } merged := make([]txRow, 0, len(txns)+len(orders)) for _, t := range txns { merged = append(merged, txRow{ ID: fmt.Sprintf("bal_%d", t.ID), Type: t.Type, Amount: t.Amount, Description: t.Description, CreatedAt: t.CreatedAt, }) } for _, o := range orders { desc := "阅读消费" if o.Description != nil && *o.Description != "" { desc = *o.Description } merged = append(merged, txRow{ ID: o.ID, Type: "consume", Amount: -o.Amount, Description: desc, CreatedAt: o.CreatedAt, }) } // 按时间倒序排列 for i := 0; i < len(merged); i++ { for j := i + 1; j < len(merged); j++ { ti := fmt.Sprintf("%v", merged[i].CreatedAt) tj := fmt.Sprintf("%v", merged[j].CreatedAt) if ti < tj { merged[i], merged[j] = merged[j], merged[i] } } } if len(merged) > 50 { merged = merged[:50] } c.JSON(http.StatusOK, gin.H{"success": true, "data": merged}) } // GET /api/admin/balance/summary 管理端-余额统计 func BalanceSummary(c *gin.Context) { db := database.DB() type Summary struct { TotalUsers int64 `json:"totalUsers"` TotalBalance float64 `json:"totalBalance"` TotalRecharged float64 `json:"totalRecharged"` TotalGifted float64 `json:"totalGifted"` TotalRefunded float64 `json:"totalRefunded"` GiftCount int64 `json:"giftCount"` PendingGifts int64 `json:"pendingGifts"` } var s Summary db.Model(&model.UserBalance{}).Count(&s.TotalUsers) db.Model(&model.UserBalance{}).Select("COALESCE(SUM(balance),0)").Scan(&s.TotalBalance) db.Model(&model.UserBalance{}).Select("COALESCE(SUM(total_recharged),0)").Scan(&s.TotalRecharged) db.Model(&model.UserBalance{}).Select("COALESCE(SUM(total_gifted),0)").Scan(&s.TotalGifted) db.Model(&model.UserBalance{}).Select("COALESCE(SUM(total_refunded),0)").Scan(&s.TotalRefunded) db.Model(&model.GiftUnlock{}).Count(&s.GiftCount) db.Model(&model.GiftUnlock{}).Where("status = ?", "pending").Count(&s.PendingGifts) c.JSON(200, gin.H{"success": true, "data": s}) } // GET /api/miniprogram/balance/gift/info 查询礼物码信息 func BalanceGiftInfo(c *gin.Context) { code := c.Query("code") if code == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 code"}) return } db := database.DB() var gift model.GiftUnlock if err := db.Where("gift_code = ?", code).First(&gift).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "礼物码无效"}) return } var chapter model.Chapter db.Where("id = ?", gift.SectionID).First(&chapter) c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{ "giftCode": gift.GiftCode, "sectionId": gift.SectionID, "sectionTitle": chapter.SectionTitle, "amount": gift.Amount, "status": gift.Status, "giverId": gift.GiverID, }}) }