364 lines
12 KiB
Go
364 lines
12 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 页可提现逻辑一致:可提现 = 累计佣金 - 已提现 - 待审核
|
||
// 佣金按订单逐条 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": "已记录确认收款"})
|
||
}
|