2026-02-09 14:33:41 +08:00
|
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-02-09 19:26:19 +08:00
|
|
|
|
"encoding/json"
|
2026-02-09 18:19:12 +08:00
|
|
|
|
"fmt"
|
2026-02-09 19:26:19 +08:00
|
|
|
|
"math"
|
2026-02-09 14:33:41 +08:00
|
|
|
|
"net/http"
|
2026-02-09 18:19:12 +08:00
|
|
|
|
"os"
|
2026-02-09 19:26:19 +08:00
|
|
|
|
"time"
|
2026-02-09 14:33:41 +08:00
|
|
|
|
|
|
|
|
|
|
"soul-api/internal/database"
|
|
|
|
|
|
"soul-api/internal/model"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
2026-02-09 19:26:19 +08:00
|
|
|
|
"gorm.io/gorm"
|
2026-02-09 14:33:41 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-09 19:26:19 +08:00
|
|
|
|
// 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 页一致;二次查库校验防止超额。打款由管理端审核后手动/后续接入官方接口再处理。
|
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"`
|
2026-02-09 19:26:19 +08:00
|
|
|
|
UserName string `json:"userName"`
|
2026-02-09 18:19:12 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-02-09 19:26:19 +08:00
|
|
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
|
})
|
2026-02-09 18:19:12 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 19:26:19 +08:00
|
|
|
|
withdrawID := generateWithdrawID()
|
2026-02-09 18:19:12 +08:00
|
|
|
|
status := "pending"
|
|
|
|
|
|
remark := req.Remark
|
|
|
|
|
|
if remark == "" {
|
|
|
|
|
|
remark = "提现"
|
|
|
|
|
|
}
|
|
|
|
|
|
withdrawal := model.Withdrawal{
|
2026-02-09 19:26:19 +08:00
|
|
|
|
ID: withdrawID,
|
|
|
|
|
|
UserID: req.UserID,
|
|
|
|
|
|
Amount: req.Amount,
|
|
|
|
|
|
Status: &status,
|
2026-02-09 18:19:12 +08:00
|
|
|
|
}
|
2026-02-09 19:26:19 +08:00
|
|
|
|
if err := db.Select("ID", "UserID", "Amount", "Status").Create(&withdrawal).Error; err != nil {
|
2026-02-09 18:19:12 +08:00
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
|
|
|
|
"success": false,
|
2026-02-09 19:26:19 +08:00
|
|
|
|
"message": "创建提现记录失败",
|
2026-02-09 18:19:12 +08:00
|
|
|
|
"error": err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-02-09 19:26:19 +08:00
|
|
|
|
_ = db.Model(&withdrawal).Updates(map[string]interface{}{
|
|
|
|
|
|
"remark": remark,
|
|
|
|
|
|
"wechat_openid": user.OpenID,
|
|
|
|
|
|
}).Error
|
2026-02-09 18:19:12 +08:00
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
2026-02-09 19:26:19 +08:00
|
|
|
|
"message": "提现申请已提交,审核通过后将打款至您的微信零钱",
|
2026-02-09 18:19:12 +08:00
|
|
|
|
"data": map[string]interface{}{
|
2026-02-09 19:26:19 +08:00
|
|
|
|
"id": withdrawal.ID,
|
|
|
|
|
|
"amount": req.Amount,
|
|
|
|
|
|
"status": "pending",
|
|
|
|
|
|
"created_at": withdrawal.CreatedAt,
|
2026-02-09 18:19:12 +08:00
|
|
|
|
},
|
|
|
|
|
|
})
|
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
|
|
|
|
}
|