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 } canReceive := st == "processing" || st == "pending_confirm" out = append(out, gin.H{ "id": w.ID, "amount": w.Amount, "status": st, "createdAt": w.CreatedAt, "processedAt": w.ProcessedAt, "canReceive": canReceive, }) } c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"list": out}}) } // WithdrawConfirmInfo GET /api/miniprogram/withdraw/confirm-info?id= 获取某条提现的领取零钱参数(mchId/appId/package),供 wx.requestMerchantTransfer 使用 func WithdrawConfirmInfo(c *gin.Context) { id := c.Query("id") if id == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "缺少 id"}) return } db := database.DB() var w model.Withdrawal if err := db.Where("id = ?", id).First(&w).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": "提现记录不存在"}) return } st := "" if w.Status != nil { st = *w.Status } if st != "processing" && st != "pending_confirm" { c.JSON(http.StatusOK, gin.H{"success": false, "message": "当前状态不可领取"}) return } mchId := os.Getenv("WECHAT_MCH_ID") if mchId == "" { mchId = "1318592501" } appId := os.Getenv("WECHAT_APPID") if appId == "" { appId = "wxb8bbb2b10dec74aa" } // package 需由「用户确认模式」转账接口返回并落库,当前批量转账无 package,返回空;有值时可调 wx.requestMerchantTransfer packageInfo := "" c.JSON(http.StatusOK, gin.H{ "success": true, "data": gin.H{ "mchId": mchId, "appId": appId, "package": packageInfo, }, }) } // 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, }, }) }