2026-03-07 22:58:43 +08:00
package handler
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
2026-03-20 13:40:13 +08:00
"soul-api/internal/cache"
2026-03-07 22:58:43 +08:00
"soul-api/internal/database"
"soul-api/internal/model"
"soul-api/internal/wechat"
"github.com/gin-gonic/gin"
2026-03-20 13:40:13 +08:00
"gorm.io/gorm"
2026-03-07 22:58:43 +08:00
)
2026-03-20 13:40:13 +08:00
// AdminWithdrawalsAutoApproveGet GET /api/admin/withdrawals/auto-approve 获取自动审批开关状态
func AdminWithdrawalsAutoApproveGet ( c * gin . Context ) {
db := database . DB ( )
enabled := false
var refCfg model . SystemConfig
if err := db . Where ( "config_key = ?" , "referral_config" ) . First ( & refCfg ) . Error ; err == nil {
var val map [ string ] interface { }
if err := json . Unmarshal ( refCfg . ConfigValue , & val ) ; err == nil {
if v , ok := val [ "enableAutoWithdraw" ] . ( bool ) ; ok {
enabled = v
}
}
}
c . JSON ( http . StatusOK , gin . H { "success" : true , "enableAutoApprove" : enabled } )
}
// AdminWithdrawalsAutoApprovePut PUT /api/admin/withdrawals/auto-approve 设置自动审批开关
func AdminWithdrawalsAutoApprovePut ( c * gin . Context ) {
var body struct {
EnableAutoApprove bool ` json:"enableAutoApprove" `
}
if err := c . ShouldBindJSON ( & body ) ; err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : "请求体无效" } )
return
}
db := database . DB ( )
var refCfg model . SystemConfig
val := map [ string ] interface { } {
"distributorShare" : float64 ( 90 ) , "minWithdrawAmount" : float64 ( 10 ) , "bindingDays" : float64 ( 30 ) ,
"userDiscount" : float64 ( 5 ) , "withdrawFee" : float64 ( 5 ) , "enableAutoWithdraw" : body . EnableAutoApprove ,
"vipOrderShareVip" : float64 ( 20 ) , "vipOrderShareNonVip" : float64 ( 10 ) ,
}
if err := db . Where ( "config_key = ?" , "referral_config" ) . First ( & refCfg ) . Error ; err == nil {
if err := json . Unmarshal ( refCfg . ConfigValue , & val ) ; err == nil {
val [ "enableAutoWithdraw" ] = body . EnableAutoApprove
}
}
valBytes , _ := json . Marshal ( val )
desc := "分销 / 推广规则配置"
if err := db . Where ( "config_key = ?" , "referral_config" ) . First ( & refCfg ) . Error ; err != nil {
refCfg = model . SystemConfig { ConfigKey : "referral_config" , ConfigValue : valBytes , Description : & desc }
_ = db . Create ( & refCfg )
} else {
refCfg . ConfigValue = valBytes
refCfg . Description = & desc
_ = db . Save ( & refCfg )
}
cache . InvalidateConfig ( )
c . JSON ( http . StatusOK , gin . H { "success" : true , "enableAutoApprove" : body . EnableAutoApprove , "message" : "已更新" } )
}
2026-03-07 22:58:43 +08:00
// AdminWithdrawalsList GET /api/admin/withdrawals( 支持分页 page、pageSize, 筛选 status)
func AdminWithdrawalsList ( c * gin . Context ) {
statusFilter := c . Query ( "status" )
page , _ := strconv . Atoi ( c . DefaultQuery ( "page" , "1" ) )
pageSize , _ := strconv . Atoi ( c . DefaultQuery ( "pageSize" , "10" ) )
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
db := database . DB ( )
q := db . Model ( & model . Withdrawal { } )
if statusFilter != "" && statusFilter != "all" {
q = q . Where ( "status = ?" , statusFilter )
}
var total int64
q . Count ( & total )
var list [ ] model . Withdrawal
query := db . Order ( "created_at DESC" )
if statusFilter != "" && statusFilter != "all" {
query = query . Where ( "status = ?" , statusFilter )
}
if err := query . Offset ( ( page - 1 ) * pageSize ) . Limit ( pageSize ) . Find ( & list ) . Error ; err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : err . Error ( ) , "withdrawals" : [ ] interface { } { } , "stats" : gin . H { "total" : 0 } } )
return
}
userIds := make ( [ ] string , 0 , len ( list ) )
seen := make ( map [ string ] bool )
for _ , w := range list {
if ! seen [ w . UserID ] {
seen [ w . UserID ] = true
userIds = append ( userIds , w . UserID )
}
}
var users [ ] model . User
if len ( userIds ) > 0 {
database . DB ( ) . Where ( "id IN ?" , userIds ) . Find ( & users )
}
userMap := make ( map [ string ] * model . User )
for i := range users {
userMap [ users [ i ] . ID ] = & users [ i ]
}
withdrawals := make ( [ ] gin . H , 0 , len ( list ) )
for _ , w := range list {
u := userMap [ w . UserID ]
userName := "未知用户"
var userAvatar * string
account := "未绑定微信号"
if w . WechatID != nil && * w . WechatID != "" {
account = * w . WechatID
}
if u != nil {
if u . Nickname != nil {
userName = * u . Nickname
}
userAvatar = u . Avatar
if u . WechatID != nil && * u . WechatID != "" {
account = * u . WechatID
}
}
st := "pending"
if w . Status != nil {
st = * w . Status
if st == "success" {
st = "completed"
} else if st == "failed" {
st = "rejected"
} else if st == "pending_confirm" {
st = "pending_confirm"
}
}
userConfirmedAt := interface { } ( nil )
if w . UserConfirmedAt != nil && ! w . UserConfirmedAt . IsZero ( ) {
userConfirmedAt = w . UserConfirmedAt . Format ( "2006-01-02 15:04:05" )
}
2026-03-19 18:26:45 +08:00
avStr := ""
if userAvatar != nil {
avStr = resolveAvatarURL ( * userAvatar )
}
2026-03-20 13:40:13 +08:00
// 备注:失败时显示 failReason/errorMessage, 否则显示用户 remark
remark := ""
if st == "rejected" || st == "failed" {
if w . FailReason != nil && * w . FailReason != "" {
remark = * w . FailReason
} else if w . ErrorMessage != nil && * w . ErrorMessage != "" {
remark = * w . ErrorMessage
}
}
if remark == "" && w . Remark != nil && * w . Remark != "" {
remark = * w . Remark
}
2026-03-07 22:58:43 +08:00
withdrawals = append ( withdrawals , gin . H {
2026-03-19 18:26:45 +08:00
"id" : w . ID , "userId" : w . UserID , "userName" : userName , "userAvatar" : avStr ,
2026-03-07 22:58:43 +08:00
"amount" : w . Amount , "status" : st , "createdAt" : w . CreatedAt ,
"method" : "wechat" , "account" : account ,
"userConfirmedAt" : userConfirmedAt ,
2026-03-20 13:40:13 +08:00
"remark" : remark ,
2026-03-07 22:58:43 +08:00
} )
}
totalPages := int ( total ) / pageSize
if int ( total ) % pageSize > 0 {
totalPages ++
}
var pendingCount , successCount , failedCount int64
var pendingAmount , successAmount float64
db . Model ( & model . Withdrawal { } ) . Where ( "status IN ?" , [ ] string { "pending" , "pending_confirm" , "processing" } ) . Count ( & pendingCount )
db . Model ( & model . Withdrawal { } ) . Where ( "status IN ?" , [ ] string { "pending" , "pending_confirm" , "processing" } ) . Select ( "COALESCE(SUM(amount), 0)" ) . Scan ( & pendingAmount )
db . Model ( & model . Withdrawal { } ) . Where ( "status IN ?" , [ ] string { "success" , "completed" } ) . Count ( & successCount )
db . Model ( & model . Withdrawal { } ) . Where ( "status IN ?" , [ ] string { "success" , "completed" } ) . Select ( "COALESCE(SUM(amount), 0)" ) . Scan ( & successAmount )
db . Model ( & model . Withdrawal { } ) . Where ( "status IN ?" , [ ] string { "failed" , "rejected" } ) . Count ( & failedCount )
c . JSON ( http . StatusOK , gin . H {
"success" : true , "withdrawals" : withdrawals ,
"total" : total , "page" : page , "pageSize" : pageSize , "totalPages" : totalPages ,
"stats" : gin . H {
"total" : total , "pendingCount" : pendingCount , "pendingAmount" : pendingAmount ,
"successCount" : successCount , "successAmount" : successAmount , "failedCount" : failedCount ,
} ,
} )
}
2026-03-20 13:40:13 +08:00
// doApproveWithdrawal 执行提现审批逻辑(打款),供 AdminWithdrawalsAction 与自动审批共用
// 返回 (successMessage, error),成功时 err 为 nil
func doApproveWithdrawal ( db * gorm . DB , id string ) ( string , error ) {
now := time . Now ( )
var w model . Withdrawal
if err := db . Where ( "id = ?" , id ) . First ( & w ) . Error ; err != nil {
return "" , fmt . Errorf ( "提现记录不存在" )
}
st := ""
if w . Status != nil {
st = * w . Status
}
if st != "pending" && st != "processing" && st != "pending_confirm" {
return "" , fmt . Errorf ( "当前状态不允许批准" )
}
openID := ""
if w . WechatOpenid != nil && * w . WechatOpenid != "" {
openID = * w . WechatOpenid
}
if openID == "" {
var u model . User
if err := db . Where ( "id = ?" , w . UserID ) . First ( & u ) . Error ; err == nil && u . OpenID != nil {
openID = * u . OpenID
}
}
if openID == "" {
return "" , fmt . Errorf ( "用户未绑定微信 openid, 无法打款" )
}
_ , totalCommission , withdrawn , pending , _ := computeAvailableWithdraw ( db , w . UserID )
availableRaw := totalCommission - withdrawn - pending
if availableRaw < - 0.01 {
return "" , fmt . Errorf ( "用户当前可提现不足,无法批准" )
}
remark := "提现"
if w . Remark != nil && * w . Remark != "" {
remark = * w . Remark
}
withdrawFee := 0.0
var refCfg model . SystemConfig
if err := db . Where ( "config_key = ?" , "referral_config" ) . First ( & refCfg ) . Error ; err == nil {
var refVal map [ string ] interface { }
if err := json . Unmarshal ( refCfg . ConfigValue , & refVal ) ; err == nil {
if v , ok := refVal [ "withdrawFee" ] . ( float64 ) ; ok {
withdrawFee = v / 100
}
}
}
actualAmount := w . Amount * ( 1 - withdrawFee )
if actualAmount < 0.01 {
actualAmount = 0.01
}
amountFen := int ( actualAmount * 100 )
if amountFen < 1 {
return "" , fmt . Errorf ( "提现金额异常" )
}
params := wechat . FundAppTransferParams {
OutBillNo : w . ID , OpenID : openID , Amount : amountFen , Remark : remark ,
NotifyURL : "" , TransferSceneId : "1005" ,
}
result , err := wechat . InitiateTransferByFundApp ( params )
if err != nil {
errMsg := err . Error ( )
if errMsg == "支付/转账未初始化,请先调用 wechat.Init" || errMsg == "转账客户端未初始化" {
_ = db . Model ( & w ) . Updates ( map [ string ] interface { } { "status" : "success" , "processed_at" : now } ) . Error
return "已标记为已打款。当前未接入微信转账,请线下打款。" , nil
}
_ = db . Model ( & w ) . Updates ( map [ string ] interface { } {
"status" : "failed" , "fail_reason" : errMsg , "error_message" : errMsg , "processed_at" : now ,
} ) . Error
return "" , fmt . Errorf ( "%s" , errMsg )
}
if result . OutBillNo == "" {
failMsg := "微信未返回商户单号,请检查商户平台(如 IP 白名单)或查看服务端日志"
_ = db . Model ( & w ) . Updates ( map [ string ] interface { } {
"status" : "failed" , "fail_reason" : failMsg , "error_message" : failMsg , "processed_at" : now ,
} ) . Error
return "" , fmt . Errorf ( "%s" , failMsg )
}
rowStatus := "processing"
if result . State == "WAIT_USER_CONFIRM" {
rowStatus = "pending_confirm"
}
upd := map [ string ] interface { } {
"status" : rowStatus , "detail_no" : result . OutBillNo , "batch_no" : result . OutBillNo ,
"batch_id" : result . TransferBillNo , "processed_at" : now ,
}
if result . PackageInfo != "" {
upd [ "package_info" ] = result . PackageInfo
}
if err := db . Model ( & w ) . Updates ( upd ) . Error ; err != nil {
return "" , fmt . Errorf ( "更新状态失败: %w" , err )
}
if openID != "" {
go func ( ) {
ctx := context . Background ( )
if e := wechat . SendWithdrawSubscribeMessage ( ctx , openID , w . Amount , true ) ; e != nil {
fmt . Printf ( "[AdminWithdrawals] 订阅消息发送失败 id=%s: %v\n" , id , e )
}
} ( )
}
return "已发起打款,微信处理中" , nil
}
2026-03-07 22:58:43 +08:00
// AdminWithdrawalsAction PUT /api/admin/withdrawals 审核/打款
// approve: 先调微信转账接口打款, 成功则标为 processing, 失败则标为 failed 并返回错误。
// 若未初始化微信转账客户端,则仅将状态标为 success( 线下打款后批准) 。
// reject: 直接标为 failed。
func AdminWithdrawalsAction ( c * gin . Context ) {
var body struct {
ID string ` json:"id" `
Action string ` json:"action" `
ErrorMessage string ` json:"errorMessage" `
Reason string ` json:"reason" `
}
if err := c . ShouldBindJSON ( & body ) ; err != nil || body . ID == "" {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : "缺少 id 或请求体无效" } )
return
}
reason := body . ErrorMessage
if reason == "" {
reason = body . Reason
}
if reason == "" && body . Action == "reject" {
reason = "管理员拒绝"
}
db := database . DB ( )
now := time . Now ( )
switch body . Action {
case "reject" :
err := db . Model ( & model . Withdrawal { } ) . Where ( "id = ?" , body . ID ) . Updates ( map [ string ] interface { } {
"status" : "failed" ,
"error_message" : reason ,
"fail_reason" : reason ,
"processed_at" : 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" : "已拒绝" } )
return
case "approve" :
2026-03-20 13:40:13 +08:00
msg , err := doApproveWithdrawal ( db , body . ID )
2026-03-07 22:58:43 +08:00
if err != nil {
2026-03-20 13:40:13 +08:00
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : err . Error ( ) , "message" : err . Error ( ) } )
2026-03-07 22:58:43 +08:00
return
}
2026-03-20 13:40:13 +08:00
c . JSON ( http . StatusOK , gin . H { "success" : true , "message" : msg } )
2026-03-07 22:58:43 +08:00
return
default :
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : "action 须为 approve 或 reject" } )
}
}
// AdminWithdrawalsSync POST /api/admin/withdrawals/sync 主动向微信查询转账结果并更新状态(无回调时的备选)
// body: { "id": "提现记录id" } 同步单条;不传 id 或 id 为空则同步所有 processing/pending_confirm
func AdminWithdrawalsSync ( c * gin . Context ) {
var body struct {
ID string ` json:"id" `
}
_ = c . ShouldBindJSON ( & body )
db := database . DB ( )
var list [ ] model . Withdrawal
if body . ID != "" {
var w model . Withdrawal
if err := db . Where ( "id = ?" , body . ID ) . First ( & w ) . Error ; err != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "error" : "提现记录不存在" } )
return
}
list = [ ] model . Withdrawal { w }
} else {
if err := db . Where ( "status IN ?" , [ ] string { "processing" , "pending_confirm" } ) .
Find ( & list ) . Error ; err != nil || len ( list ) == 0 {
c . JSON ( http . StatusOK , gin . H { "success" : true , "message" : "暂无待同步记录" , "synced" : 0 } )
return
}
}
now := time . Now ( )
synced := 0
for _ , w := range list {
batchNo := ""
detailNo := ""
if w . BatchNo != nil {
batchNo = * w . BatchNo
}
if w . DetailNo != nil {
detailNo = * w . DetailNo
}
if detailNo == "" {
continue
}
var status , failReason string
// FundApp 单笔: batch_no == detail_no 时用商户单号查询
if batchNo == detailNo {
state , _ , fail , err := wechat . QueryTransferByOutBill ( detailNo )
if err != nil {
continue
}
status = state
failReason = fail
} else {
res , err := wechat . QueryTransfer ( batchNo , detailNo )
if err != nil {
continue
}
if s , ok := res [ "detail_status" ] . ( string ) ; ok {
status = s
}
if s , ok := res [ "fail_reason" ] . ( string ) ; ok {
failReason = s
}
}
up := map [ string ] interface { } { "processed_at" : now }
switch status {
case "SUCCESS" :
up [ "status" ] = "success"
case "FAIL" :
up [ "status" ] = "failed"
if failReason != "" {
up [ "fail_reason" ] = failReason
}
default :
continue
}
if err := db . Model ( & model . Withdrawal { } ) . Where ( "id = ?" , w . ID ) . Updates ( up ) . Error ; err != nil {
continue
}
synced ++
fmt . Printf ( "[AdminWithdrawals] 同步状态 id=%s -> %s\n" , w . ID , up [ "status" ] )
}
c . JSON ( http . StatusOK , gin . H {
"success" : true ,
"message" : "已向微信查询并更新" ,
"synced" : synced ,
"total" : len ( list ) ,
} )
}