2026-03-17 11:44:36 +08:00
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 := ""
2026-03-17 16:12:04 +08:00
if t . RelatedOrder != nil {
orderID = * t . RelatedOrder
2026-03-17 11:44:36 +08:00
}
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 {
2026-03-22 08:34:28 +08:00
ID : orderSn ,
OrderSN : orderSn ,
UserID : req . UserID ,
ProductType : "balance_recharge" ,
ProductID : & orderSn ,
Amount : req . Amount ,
Description : & desc ,
Status : & status ,
2026-03-17 11:44:36 +08:00
}
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
2026-03-17 16:12:04 +08:00
tx . Model ( & model . BalanceTransaction { } ) . Where ( "related_order = ? AND type = ?" , req . OrderSn , "recharge" ) . Count ( & cnt )
2026-03-17 11:44:36 +08:00
if cnt > 0 {
2026-03-17 16:12:04 +08:00
return nil
2026-03-17 11:44:36 +08:00
}
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 )
tx . Create ( & model . BalanceTransaction {
2026-03-17 16:12:04 +08:00
UserID : order . UserID , Type : "recharge" , Amount : order . Amount ,
RelatedOrder : & req . OrderSn , CreatedAt : time . Now ( ) ,
2026-03-17 11:44:36 +08:00
} )
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 {
2026-03-22 08:34:28 +08:00
ID : orderSn ,
OrderSN : orderSn ,
UserID : req . UserID ,
ProductType : req . ProductType ,
ProductID : & productID ,
Amount : amount ,
Description : & desc ,
Status : & status ,
2026-03-17 11:44:36 +08:00
PaymentMethod : & pm ,
2026-03-22 08:34:28 +08:00
ReferrerID : referrerID ,
PayTime : & now ,
2026-03-17 11:44:36 +08:00
}
if err := tx . Create ( & order ) . Error ; err != nil {
return err
}
tx . Create ( & model . BalanceTransaction {
2026-03-17 16:12:04 +08:00
UserID : req . UserID , Type : "consume" , Amount : - amount ,
RelatedOrder : & orderSn , CreatedAt : now ,
2026-03-17 11:44:36 +08:00
} )
// 激活权益
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 ) )
tx . Create ( & model . BalanceTransaction {
2026-03-17 16:12:04 +08:00
UserID : req . UserID , Type : "refund" , Amount : - req . Amount ,
2026-03-17 11:44:36 +08:00
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 := ""
2026-03-17 16:12:04 +08:00
if t . RelatedOrder != nil {
orderID = * t . RelatedOrder
2026-03-17 11:44:36 +08:00
}
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 } } )
}
2026-03-17 13:17:49 +08:00
// 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 )
return tx . Create ( & model . BalanceTransaction {
2026-03-17 16:12:04 +08:00
UserID : userID , Type : "admin_adjust" , Amount : req . Amount ,
2026-03-17 13:17:49 +08:00
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" : "余额已调整" } )
}
2026-03-17 11:44:36 +08:00
// 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 )
return tx . Create ( & model . BalanceTransaction {
2026-03-17 16:12:04 +08:00
UserID : order . UserID , Type : "recharge" , Amount : order . Amount ,
RelatedOrder : & orderSn , CreatedAt : time . Now ( ) ,
2026-03-17 11:44:36 +08:00
} ) . Error
} )
}