476 lines
14 KiB
Go
476 lines
14 KiB
Go
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"`
|
|
PaidViaWechat bool `json:"paidViaWechat"`
|
|
}
|
|
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
|
|
code := make([]byte, 16)
|
|
rand.Read(code)
|
|
giftCode = hex.EncodeToString(code)
|
|
|
|
if body.PaidViaWechat {
|
|
db.Create(&model.GiftUnlock{
|
|
GiftCode: giftCode,
|
|
GiverID: body.GiverID,
|
|
SectionID: body.SectionID,
|
|
Amount: price,
|
|
Status: "pending",
|
|
})
|
|
} else {
|
|
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
|
|
}
|
|
|
|
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 申请余额退款(全额原路返回)
|
|
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()
|
|
|
|
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", body.Amount),
|
|
})
|
|
|
|
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": body.Amount,
|
|
"message": fmt.Sprintf("退款成功,¥%.2f 将原路返回", body.Amount),
|
|
}})
|
|
}
|
|
|
|
// 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,
|
|
"mid": chapter.MID,
|
|
}})
|
|
}
|
|
|
|
// GET /api/miniprogram/balance/gifts?userId=xxx 我的代付列表
|
|
func BalanceGiftList(c *gin.Context) {
|
|
userId := c.Query("userId")
|
|
if userId == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 userId"})
|
|
return
|
|
}
|
|
|
|
db := database.DB()
|
|
var gifts []model.GiftUnlock
|
|
db.Where("giver_id = ?", userId).Order("created_at DESC").Limit(50).Find(&gifts)
|
|
|
|
type giftItem struct {
|
|
GiftCode string `json:"giftCode"`
|
|
SectionID string `json:"sectionId"`
|
|
SectionTitle string `json:"sectionTitle"`
|
|
Amount float64 `json:"amount"`
|
|
Status string `json:"status"`
|
|
CreatedAt string `json:"createdAt"`
|
|
}
|
|
|
|
var result []giftItem
|
|
for _, g := range gifts {
|
|
var ch model.Chapter
|
|
title := g.SectionID
|
|
if db.Where("id = ?", g.SectionID).First(&ch).Error == nil && ch.SectionTitle != "" {
|
|
title = ch.SectionTitle
|
|
}
|
|
result = append(result, giftItem{
|
|
GiftCode: g.GiftCode,
|
|
SectionID: g.SectionID,
|
|
SectionTitle: title,
|
|
Amount: g.Amount,
|
|
Status: g.Status,
|
|
CreatedAt: g.CreatedAt.Format("2006-01-02 15:04"),
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"gifts": result}})
|
|
}
|