2026-02-09 14:33:41 +08:00
|
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-02-09 18:19:12 +08:00
|
|
|
|
"fmt"
|
2026-02-09 14:33:41 +08:00
|
|
|
|
"net/http"
|
2026-02-09 18:19:12 +08:00
|
|
|
|
"os"
|
2026-02-09 14:33:41 +08:00
|
|
|
|
|
|
|
|
|
|
"soul-api/internal/database"
|
|
|
|
|
|
"soul-api/internal/model"
|
2026-02-09 18:19:12 +08:00
|
|
|
|
"soul-api/internal/wechat"
|
2026-02-09 14:33:41 +08:00
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-09 18:19:12 +08:00
|
|
|
|
// WithdrawPost POST /api/withdraw 创建提现申请并发起微信转账
|
2026-02-09 14:33:41 +08:00
|
|
|
|
func WithdrawPost(c *gin.Context) {
|
2026-02-09 18:19:12 +08:00
|
|
|
|
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,
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
2026-02-09 14:33:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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}})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 18:19:12 +08:00
|
|
|
|
// WithdrawPendingConfirm GET /api/withdraw/pending-confirm?userId= 待确认/处理中收款列表
|
|
|
|
|
|
// 返回 pending、processing、pending_confirm 的提现,供小程序展示;并返回 mchId、appId 供确认收款用
|
2026-02-09 14:33:41 +08:00
|
|
|
|
func WithdrawPendingConfirm(c *gin.Context) {
|
|
|
|
|
|
userId := c.Query("userId")
|
|
|
|
|
|
if userId == "" {
|
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "缺少 userId"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-02-09 18:19:12 +08:00
|
|
|
|
db := database.DB()
|
2026-02-09 14:33:41 +08:00
|
|
|
|
var list []model.Withdrawal
|
2026-02-09 18:19:12 +08:00
|
|
|
|
// 进行中的提现:待处理、处理中、待确认收款(与 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
|
2026-02-09 14:33:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
out := make([]gin.H, 0, len(list))
|
|
|
|
|
|
for _, w := range list {
|
2026-02-09 18:19:12 +08:00
|
|
|
|
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"
|
2026-02-09 14:33:41 +08:00
|
|
|
|
}
|
2026-02-09 18:19:12 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"data": gin.H{
|
|
|
|
|
|
"list": out,
|
|
|
|
|
|
"mchId": mchId,
|
|
|
|
|
|
"appId": appId,
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
2026-02-09 14:33:41 +08:00
|
|
|
|
}
|