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

364 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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": "已记录确认收款"})
}