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

207 lines
6.6 KiB
Go
Raw Normal View History

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