224 lines
6.3 KiB
Go
224 lines
6.3 KiB
Go
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,
|
||
},
|
||
})
|
||
}
|