207 lines
6.6 KiB
Go
207 lines
6.6 KiB
Go
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,
|
||
},
|
||
})
|
||
}
|