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"}) } }