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

224 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handler
import (
"fmt"
"net/http"
"os"
"soul-api/internal/database"
"soul-api/internal/model"
"soul-api/internal/wechat"
"github.com/gin-gonic/gin"
)
// WithdrawPost POST /api/withdraw 创建提现申请并发起微信转账
func WithdrawPost(c *gin.Context) {
var req struct {
UserID string `json:"userId" binding:"required"`
Amount float64 `json:"amount" binding:"required"`
UserName string `json:"userName"` // 可选,实名校验用
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "参数错误"})
return
}
// 金额验证
if req.Amount <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "提现金额必须大于0"})
return
}
if req.Amount < 1 {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "最低提现金额为1元"})
return
}
db := database.DB()
// 查询用户信息,获取 openid 和待提现金额
var user model.User
if err := db.Where("id = ?", req.UserID).First(&user).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "用户不存在"})
return
}
if user.OpenID == nil || *user.OpenID == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "用户未绑定微信"})
return
}
// 检查待结算收益是否足够
pendingEarnings := 0.0
if user.PendingEarnings != nil {
pendingEarnings = *user.PendingEarnings
}
if pendingEarnings < req.Amount {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": fmt.Sprintf("可提现金额不足(当前:%.2f元)", pendingEarnings),
})
return
}
// 生成转账单号
outBatchNo := wechat.GenerateTransferBatchNo()
outDetailNo := wechat.GenerateTransferDetailNo()
// 创建提现记录
status := "pending"
remark := req.Remark
if remark == "" {
remark = "提现"
}
withdrawal := model.Withdrawal{
ID: outDetailNo,
UserID: req.UserID,
Amount: req.Amount,
Status: &status,
BatchNo: &outBatchNo,
DetailNo: &outDetailNo,
Remark: &remark,
}
if err := db.Create(&withdrawal).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "创建提现记录失败"})
return
}
// 发起微信转账
transferAmount := int(req.Amount * 100) // 转为分
transferParams := wechat.TransferParams{
OutBatchNo: outBatchNo,
OutDetailNo: outDetailNo,
OpenID: *user.OpenID,
Amount: transferAmount,
UserName: req.UserName,
Remark: remark,
BatchName: "用户提现",
BatchRemark: fmt.Sprintf("用户 %s 提现 %.2f 元", req.UserID, req.Amount),
}
result, err := wechat.InitiateTransfer(transferParams)
if err != nil {
// 转账失败,更新提现状态为失败
failedStatus := "failed"
failReason := fmt.Sprintf("发起转账失败: %v", err)
db.Model(&withdrawal).Updates(map[string]interface{}{
"status": failedStatus,
"fail_reason": failReason,
})
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "发起转账失败,请稍后重试",
"error": err.Error(),
})
return
}
// 更新提现记录状态
processingStatus := "processing"
batchID := result.BatchID
db.Model(&withdrawal).Updates(map[string]interface{}{
"status": processingStatus,
"batch_id": batchID,
})
// 扣减用户的待结算收益,增加已提现金额
db.Model(&user).Updates(map[string]interface{}{
"pending_earnings": db.Raw("pending_earnings - ?", req.Amount),
"withdrawn_earnings": db.Raw("COALESCE(withdrawn_earnings, 0) + ?", req.Amount),
})
fmt.Printf("[Withdraw] 用户 %s 提现 %.2f 元,转账批次号: %s\n", req.UserID, req.Amount, outBatchNo)
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "提现申请已提交预计2小时内到账",
"data": map[string]interface{}{
"id": withdrawal.ID,
"amount": req.Amount,
"status": processingStatus,
"out_batch_no": outBatchNo,
"batch_id": result.BatchID,
"created_at": withdrawal.CreatedAt,
},
})
}
// WithdrawRecords GET /api/withdraw/records?userId= 当前用户提现记录GORM
func WithdrawRecords(c *gin.Context) {
userId := c.Query("userId")
if userId == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "缺少 userId"})
return
}
var list []model.Withdrawal
if err := database.DB().Where("user_id = ?", userId).Order("created_at DESC").Limit(100).Find(&list).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"list": []interface{}{}}})
return
}
out := make([]gin.H, 0, len(list))
for _, w := range list {
st := ""
if w.Status != nil {
st = *w.Status
}
out = append(out, gin.H{
"id": w.ID, "amount": w.Amount, "status": st,
"createdAt": w.CreatedAt, "processedAt": w.ProcessedAt,
})
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"list": out}})
}
// WithdrawPendingConfirm GET /api/withdraw/pending-confirm?userId= 待确认/处理中收款列表
// 返回 pending、processing、pending_confirm 的提现,供小程序展示;并返回 mchId、appId 供确认收款用
func WithdrawPendingConfirm(c *gin.Context) {
userId := c.Query("userId")
if userId == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "缺少 userId"})
return
}
db := database.DB()
var list []model.Withdrawal
// 进行中的提现:待处理、处理中、待确认收款(与 next 的 pending_confirm 兼容)
if err := db.Where("user_id = ? AND status IN ?", userId, []string{"pending", "processing", "pending_confirm"}).
Order("created_at DESC").
Find(&list).Error; err != nil {
list = nil
}
out := make([]gin.H, 0, len(list))
for _, w := range list {
item := gin.H{
"id": w.ID,
"amount": w.Amount,
"createdAt": w.CreatedAt,
}
// 若有 package 信息requestMerchantTransfer 用),一并返回;当前直接打款无 package给空字符串
item["package"] = ""
out = append(out, item)
}
mchId := os.Getenv("WECHAT_MCH_ID")
if mchId == "" {
mchId = "1318592501"
}
appId := os.Getenv("WECHAT_APPID")
if appId == "" {
appId = "wxb8bbb2b10dec74aa"
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"list": out,
"mchId": mchId,
"appId": appId,
},
})
}