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 页可提现逻辑一致:可提现 = 累计佣金 - 已提现 - 待审核 // 佣金按订单逐条 computeOrderCommission 求和(会员订单 20%/10%,内容订单 90%) func computeAvailableWithdraw(db *gorm.DB, userID string) (available, totalCommission, withdrawn, pending float64, minAmount float64) { 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 m, ok := config["minWithdrawAmount"].(float64); ok { minAmount = m } } } var orders []model.Order db.Where("referrer_id = ? AND status = ?", userID, "paid").Find(&orders) for i := range orders { totalCommission += computeOrderCommission(db, &orders[i], nil) } 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" // 根据 user_id 已查到的用户信息,填充提现表所需字段;仅写入表中存在的列,避免 remark 等列不存在报错 wechatID := user.WechatID if (wechatID == nil || *wechatID == "") && user.OpenID != nil && *user.OpenID != "" { wechatID = user.OpenID } withdrawal := model.Withdrawal{ ID: withdrawID, UserID: req.UserID, Amount: req.Amount, Status: &status, WechatOpenid: user.OpenID, WechatID: wechatID, } if err := db.Select("ID", "UserID", "Amount", "Status", "WechatOpenid", "WechatID").Create(&withdrawal).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "创建提现记录失败", "error": err.Error(), }) return } 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, }, }) } // AdminWithdrawTest GET/POST /api/admin/withdraw-test 提现测试接口,供 curl 等调试用 // 参数:userId(默认 ogpTW5fmXRGNpoUbXB3UEqnVe5Tg)、amount(默认 1) // 测试时忽略最低提现额限制,仅校验可提现余额与用户存在 func AdminWithdrawTest(c *gin.Context) { userID := c.DefaultQuery("userId", "ogpTW5fmXRGNpoUbXB3UEqnVe5Tg") amountStr := c.DefaultQuery("amount", "1") var amount float64 if _, err := fmt.Sscanf(amountStr, "%f", &amount); err != nil || amount <= 0 { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "amount 须为正数"}) return } db := database.DB() available, _, _, _, _ := computeAvailableWithdraw(db, userID) if amount > available { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": fmt.Sprintf("可提现金额不足(当前可提现:%.2f元)", available), }) return } var user model.User if err := db.Where("id = ?", 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" wechatID := user.WechatID if (wechatID == nil || *wechatID == "") && user.OpenID != nil && *user.OpenID != "" { wechatID = user.OpenID } withdrawal := model.Withdrawal{ ID: withdrawID, UserID: userID, Amount: amount, Status: &status, WechatOpenid: user.OpenID, WechatID: wechatID, } if err := db.Select("ID", "UserID", "Amount", "Status", "WechatOpenid", "WechatID").Create(&withdrawal).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "创建提现记录失败", "error": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "提现测试已提交", "data": map[string]interface{}{ "id": withdrawal.ID, "userId": userID, "amount": 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" } packageInfo := "" if w.PackageInfo != nil && *w.PackageInfo != "" { packageInfo = *w.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= 待确认收款列表(仅审核通过后) // 只返回 processing、pending_confirm,供「我的」页「待确认收款」展示;pending 为待审核,不在此列表 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 // 仅审核已通过、等待用户确认收款的:processing(微信处理中)、pending_confirm(待用户点确认收款) if err := db.Where("user_id = ? AND status IN ?", userId, []string{"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, } if w.PackageInfo != nil && *w.PackageInfo != "" { item["package"] = *w.PackageInfo } else { item["package"] = "" } if w.UserConfirmedAt != nil && !w.UserConfirmedAt.IsZero() { item["userConfirmedAt"] = w.UserConfirmedAt.Format("2006-01-02 15:04:05") } else { item["userConfirmedAt"] = nil } 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, }, }) } // WithdrawConfirmReceived POST /api/miniprogram/withdraw/confirm-received 用户确认收款(记录已点击确认) // body: { "withdrawalId": "xxx", "userId": "xxx" },仅本人可操作;更新 user_confirmed_at 并将状态置为 success,该条不再出现在待确认收款列表 func WithdrawConfirmReceived(c *gin.Context) { var req struct { WithdrawalID string `json:"withdrawalId" binding:"required"` UserID string `json:"userId" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "缺少 withdrawalId 或 userId"}) return } db := database.DB() var w model.Withdrawal if err := db.Where("id = ? AND user_id = ?", req.WithdrawalID, req.UserID).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" && st != "success" { c.JSON(http.StatusOK, gin.H{"success": false, "message": "当前状态不可确认收款"}) return } if w.UserConfirmedAt != nil && !w.UserConfirmedAt.IsZero() { c.JSON(http.StatusOK, gin.H{"success": true, "message": "已确认过"}) return } now := time.Now() // 更新为已确认收款,并将状态置为 success,待确认列表只含 processing/pending_confirm,故该条会从列表中移除 up := map[string]interface{}{"user_confirmed_at": now, "status": "success"} if err := db.Model(&w).Updates(up).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "message": "更新失败"}) return } c.JSON(http.StatusOK, gin.H{"success": true, "message": "已记录确认收款"}) }