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

207 lines
6.6 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 (
"encoding/json"
"fmt"
"math"
"net/http"
"os"
"time"
"soul-api/internal/database"
"soul-api/internal/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// computeAvailableWithdraw 与小程序 / referral 页可提现逻辑一致:可提现 = 累计佣金 - 已提现 - 待审核
// 用于 referral/data 展示与 withdraw 接口二次查库校验(不信任前端传参)
func computeAvailableWithdraw(db *gorm.DB, userID string) (available, totalCommission, withdrawn, pending float64, minAmount float64) {
distributorShare := 0.9
minAmount = 10
var cfg model.SystemConfig
if err := db.Where("config_key = ?", "referral_config").First(&cfg).Error; err == nil {
var config map[string]interface{}
if _ = json.Unmarshal(cfg.ConfigValue, &config); config != nil {
if share, ok := config["distributorShare"].(float64); ok {
distributorShare = share / 100
}
if m, ok := config["minWithdrawAmount"].(float64); ok {
minAmount = m
}
}
}
var sumOrder struct{ Total float64 }
db.Model(&model.Order{}).Where("referrer_id = ? AND status = ?", userID, "paid").
Select("COALESCE(SUM(amount), 0) as total").Scan(&sumOrder)
totalCommission = sumOrder.Total * distributorShare
var w struct{ Total float64 }
db.Model(&model.Withdrawal{}).Where("user_id = ? AND status = ?", userID, "success").
Select("COALESCE(SUM(amount), 0)").Scan(&w)
withdrawn = w.Total
db.Model(&model.Withdrawal{}).Where("user_id = ? AND status IN ?", userID, []string{"pending", "processing", "pending_confirm"}).
Select("COALESCE(SUM(amount), 0)").Scan(&w)
pending = w.Total
available = math.Max(0, totalCommission-withdrawn-pending)
return available, totalCommission, withdrawn, pending, minAmount
}
// generateWithdrawID 生成提现单号(不依赖 wechat 包)
func generateWithdrawID() string {
return fmt.Sprintf("WD%d%06d", time.Now().Unix(), time.Now().UnixNano()%1000000)
}
// WithdrawPost POST /api/withdraw 创建提现申请(仅落库待审核,不调用微信打款接口)
// 可提现逻辑与小程序 referral 页一致;二次查库校验防止超额。打款由管理端审核后手动/后续接入官方接口再处理。
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
}
db := database.DB()
available, _, _, _, minWithdrawAmount := computeAvailableWithdraw(db, req.UserID)
if req.Amount > available {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": fmt.Sprintf("可提现金额不足(当前可提现:%.2f元)", available),
})
return
}
if req.Amount < minWithdrawAmount {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": fmt.Sprintf("最低提现金额为%.0f元", minWithdrawAmount),
})
return
}
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
}
withdrawID := generateWithdrawID()
status := "pending"
remark := req.Remark
if remark == "" {
remark = "提现"
}
withdrawal := model.Withdrawal{
ID: withdrawID,
UserID: req.UserID,
Amount: req.Amount,
Status: &status,
}
if err := db.Select("ID", "UserID", "Amount", "Status").Create(&withdrawal).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "创建提现记录失败",
"error": err.Error(),
})
return
}
_ = db.Model(&withdrawal).Updates(map[string]interface{}{
"remark": remark,
"wechat_openid": user.OpenID,
}).Error
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "提现申请已提交,审核通过后将打款至您的微信零钱",
"data": map[string]interface{}{
"id": withdrawal.ID,
"amount": req.Amount,
"status": "pending",
"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,
},
})
}