Files
soul-yongping/soul-api/internal/handler/admin_withdrawals.go

225 lines
6.3 KiB
Go
Raw Normal View History

package handler
import (
"fmt"
"net/http"
"time"
"soul-api/internal/database"
"soul-api/internal/model"
"soul-api/internal/wechat"
"github.com/gin-gonic/gin"
)
// AdminWithdrawalsList GET /api/admin/withdrawals
func AdminWithdrawalsList(c *gin.Context) {
statusFilter := c.Query("status")
var list []model.Withdrawal
q := database.DB().Order("created_at DESC").Limit(100)
if statusFilter != "" {
q = q.Where("status = ?", statusFilter)
}
if err := q.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"
}
}
withdrawals = append(withdrawals, gin.H{
"id": w.ID, "userId": w.UserID, "userName": userName, "userAvatar": userAvatar,
"amount": w.Amount, "status": st, "createdAt": w.CreatedAt,
"method": "wechat", "account": account,
})
}
c.JSON(http.StatusOK, gin.H{"success": true, "withdrawals": withdrawals, "stats": gin.H{"total": len(withdrawals)}})
}
// 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":
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
}
st := ""
if w.Status != nil {
st = *w.Status
}
if st != "pending" && st != "processing" && st != "pending_confirm" {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "当前状态不允许批准"})
return
}
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 == "" {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户未绑定微信 openid无法打款"})
return
}
// 调用微信转账接口;未初始化时仅标记为已打款(线下打款)
outBatchNo := wechat.GenerateTransferBatchNo()
outDetailNo := wechat.GenerateTransferDetailNo()
remark := "提现"
if w.Remark != nil && *w.Remark != "" {
remark = *w.Remark
}
amountFen := int(w.Amount * 100)
if amountFen < 1 {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "提现金额异常"})
return
}
params := wechat.TransferParams{
OutBatchNo: outBatchNo,
OutDetailNo: outDetailNo,
OpenID: openID,
Amount: amountFen,
Remark: remark,
BatchName: "用户提现",
BatchRemark: fmt.Sprintf("用户 %s 提现 %.2f 元", w.UserID, w.Amount),
}
result, err := wechat.InitiateTransfer(params)
if err != nil {
// 未初始化转账客户端:仅标记为 success提示线下打款
if err.Error() == "转账客户端未初始化" {
_ = db.Model(&w).Updates(map[string]interface{}{
"status": "success",
"processed_at": now,
}).Error
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "已标记为已打款。当前未接入微信转账,请线下打款。",
})
return
}
// 其他打款失败:记失败原因
failMsg := err.Error()
_ = db.Model(&w).Updates(map[string]interface{}{
"status": "failed",
"fail_reason": failMsg,
"error_message": failMsg,
"processed_at": now,
}).Error
c.JSON(http.StatusOK, gin.H{
"success": false,
"error": "发起打款失败",
"message": failMsg,
})
return
}
// 打款已受理,更新为处理中并保存微信批次号
processingStatus := "processing"
batchID := result.BatchID
_ = db.Model(&w).Updates(map[string]interface{}{
"status": processingStatus,
"batch_no": outBatchNo,
"detail_no": outDetailNo,
"batch_id": batchID,
"processed_at": now,
}).Error
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "已发起打款,微信处理中",
"data": gin.H{"batch_id": batchID},
})
return
default:
c.JSON(http.StatusOK, gin.H{"success": false, "error": "action 须为 approve 或 reject"})
}
}