优化提现记录页面,新增“领取零钱”按钮以支持用户提现操作,并更新相关API以获取转账信息。调整页面布局以提升用户体验,同时确保状态字段的一致性和可读性。
This commit is contained in:
@@ -211,10 +211,14 @@ func AdminWithdrawalsAction(c *gin.Context) {
|
||||
"batch_id": batchID,
|
||||
"processed_at": now,
|
||||
}).Error
|
||||
// 始终返回 out_batch_no 便于追踪;batch_id 为微信返回,可能为空
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "已发起打款,微信处理中",
|
||||
"data": gin.H{"batch_id": batchID},
|
||||
"data": gin.H{
|
||||
"batch_id": batchID,
|
||||
"out_batch_no": outBatchNo,
|
||||
},
|
||||
})
|
||||
return
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package handler
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -287,33 +288,19 @@ func miniprogramPayPost(c *gin.Context) {
|
||||
fmt.Printf("[MiniprogramPay] 插入订单失败: %v\n", err)
|
||||
}
|
||||
|
||||
// 调用微信统一下单
|
||||
params := map[string]string{
|
||||
"body": description,
|
||||
"out_trade_no": orderSn,
|
||||
"total_fee": fmt.Sprintf("%d", totalFee),
|
||||
"spbill_create_ip": clientIP,
|
||||
"notify_url": "https://soul.quwanzhi.com/api/miniprogram/pay/notify",
|
||||
"trade_type": "JSAPI",
|
||||
"openid": req.OpenID,
|
||||
"attach": fmt.Sprintf(`{"productType":"%s","productId":"%s","userId":"%s"}`, req.ProductType, req.ProductID, userID),
|
||||
}
|
||||
|
||||
result, err := wechat.PayV2UnifiedOrder(params)
|
||||
attach := fmt.Sprintf(`{"productType":"%s","productId":"%s","userId":"%s"}`, req.ProductType, req.ProductID, userID)
|
||||
ctx := c.Request.Context()
|
||||
prepayID, err := wechat.PayJSAPIOrder(ctx, req.OpenID, orderSn, totalFee, description, attach)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": fmt.Sprintf("微信支付请求失败: %v", err)})
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": fmt.Sprintf("微信支付请求失败: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
prepayID := result["prepay_id"]
|
||||
if prepayID == "" {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "微信支付返回数据异常"})
|
||||
payParams, err := wechat.GetJSAPIPayParams(prepayID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": fmt.Sprintf("生成支付参数失败: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成小程序支付参数
|
||||
payParams := wechat.GenerateJSAPIPayParams(prepayID)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": map[string]interface{}{
|
||||
@@ -332,7 +319,8 @@ func miniprogramPayGet(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := wechat.PayV2OrderQuery(orderSn)
|
||||
ctx := c.Request.Context()
|
||||
tradeState, transactionID, totalFee, err := wechat.QueryOrderByOutTradeNo(ctx, orderSn)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
@@ -344,10 +332,7 @@ func miniprogramPayGet(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 映射微信支付状态
|
||||
tradeState := result["trade_state"]
|
||||
status := "paying"
|
||||
|
||||
switch tradeState {
|
||||
case "SUCCESS":
|
||||
status = "paid"
|
||||
@@ -357,175 +342,130 @@ func miniprogramPayGet(c *gin.Context) {
|
||||
status = "refunded"
|
||||
}
|
||||
|
||||
totalFee := 0
|
||||
if result["total_fee"] != "" {
|
||||
fmt.Sscanf(result["total_fee"], "%d", &totalFee)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": map[string]interface{}{
|
||||
"status": status,
|
||||
"orderSn": orderSn,
|
||||
"transactionId": result["transaction_id"],
|
||||
"transactionId": transactionID,
|
||||
"totalFee": totalFee,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// MiniprogramPayNotify POST /api/miniprogram/pay/notify
|
||||
// MiniprogramPayNotify POST /api/miniprogram/pay/notify(v3 支付回调,PowerWeChat 验签解密)
|
||||
func MiniprogramPayNotify(c *gin.Context) {
|
||||
// 读取 XML body
|
||||
body, err := c.GetRawData()
|
||||
if err != nil {
|
||||
c.String(http.StatusBadRequest, failResponse())
|
||||
return
|
||||
}
|
||||
resp, err := wechat.HandlePayNotify(c.Request, func(orderSn, transactionID string, totalFee int, attachStr, openID string) error {
|
||||
totalAmount := float64(totalFee) / 100
|
||||
fmt.Printf("[PayNotify] 支付成功: orderSn=%s, transactionId=%s, amount=%.2f\n", orderSn, transactionID, totalAmount)
|
||||
|
||||
// 解析 XML
|
||||
data := wechat.XMLToMap(string(body))
|
||||
|
||||
// 验证签名
|
||||
if !wechat.VerifyPayNotify(data) {
|
||||
fmt.Println("[PayNotify] 签名验证失败")
|
||||
var attach struct {
|
||||
ProductType string `json:"productType"`
|
||||
ProductID string `json:"productId"`
|
||||
UserID string `json:"userId"`
|
||||
}
|
||||
if attachStr != "" {
|
||||
_ = json.Unmarshal([]byte(attachStr), &attach)
|
||||
}
|
||||
|
||||
db := database.DB()
|
||||
buyerUserID := attach.UserID
|
||||
if openID != "" {
|
||||
var user model.User
|
||||
if err := db.Where("open_id = ?", openID).First(&user).Error; err == nil {
|
||||
if attach.UserID != "" && user.ID != attach.UserID {
|
||||
fmt.Printf("[PayNotify] 买家身份校验: attach.userId 与 openId 解析不一致,以 openId 为准\n")
|
||||
}
|
||||
buyerUserID = user.ID
|
||||
}
|
||||
}
|
||||
if buyerUserID == "" && attach.UserID != "" {
|
||||
buyerUserID = attach.UserID
|
||||
}
|
||||
|
||||
var order model.Order
|
||||
result := db.Where("order_sn = ?", orderSn).First(&order)
|
||||
if result.Error != nil {
|
||||
fmt.Printf("[PayNotify] 订单不存在,补记订单: %s\n", orderSn)
|
||||
productID := attach.ProductID
|
||||
if productID == "" {
|
||||
productID = "fullbook"
|
||||
}
|
||||
productType := attach.ProductType
|
||||
if productType == "" {
|
||||
productType = "unknown"
|
||||
}
|
||||
desc := "支付回调补记订单"
|
||||
status := "paid"
|
||||
now := time.Now()
|
||||
order = model.Order{
|
||||
ID: orderSn,
|
||||
OrderSN: orderSn,
|
||||
UserID: buyerUserID,
|
||||
OpenID: openID,
|
||||
ProductType: productType,
|
||||
ProductID: &productID,
|
||||
Amount: totalAmount,
|
||||
Description: &desc,
|
||||
Status: &status,
|
||||
TransactionID: &transactionID,
|
||||
PayTime: &now,
|
||||
}
|
||||
db.Create(&order)
|
||||
} else if *order.Status != "paid" {
|
||||
status := "paid"
|
||||
now := time.Now()
|
||||
db.Model(&order).Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
"transaction_id": transactionID,
|
||||
"pay_time": now,
|
||||
})
|
||||
fmt.Printf("[PayNotify] 订单状态已更新为已支付: %s\n", orderSn)
|
||||
} else {
|
||||
fmt.Printf("[PayNotify] 订单已支付,跳过更新: %s\n", orderSn)
|
||||
}
|
||||
|
||||
if buyerUserID != "" && attach.ProductType != "" {
|
||||
if attach.ProductType == "fullbook" {
|
||||
db.Model(&model.User{}).Where("id = ?", buyerUserID).Update("has_full_book", true)
|
||||
fmt.Printf("[PayNotify] 用户已购全书: %s\n", buyerUserID)
|
||||
} else if attach.ProductType == "section" && attach.ProductID != "" {
|
||||
var count int64
|
||||
db.Model(&model.Order{}).Where(
|
||||
"user_id = ? AND product_type = 'section' AND product_id = ? AND status = 'paid' AND order_sn != ?",
|
||||
buyerUserID, attach.ProductID, orderSn,
|
||||
).Count(&count)
|
||||
if count == 0 {
|
||||
fmt.Printf("[PayNotify] 用户首次购买章节: %s - %s\n", buyerUserID, attach.ProductID)
|
||||
} else {
|
||||
fmt.Printf("[PayNotify] 用户已有该章节的其他已支付订单: %s - %s\n", buyerUserID, attach.ProductID)
|
||||
}
|
||||
}
|
||||
productID := attach.ProductID
|
||||
if productID == "" {
|
||||
productID = "fullbook"
|
||||
}
|
||||
db.Where(
|
||||
"user_id = ? AND product_type = ? AND product_id = ? AND status = 'created' AND order_sn != ?",
|
||||
buyerUserID, attach.ProductType, productID, orderSn,
|
||||
).Delete(&model.Order{})
|
||||
processReferralCommission(db, buyerUserID, totalAmount, orderSn)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("[PayNotify] 处理回调失败: %v\n", err)
|
||||
c.String(http.StatusOK, failResponse())
|
||||
return
|
||||
}
|
||||
|
||||
// 检查支付结果
|
||||
if data["return_code"] != "SUCCESS" || data["result_code"] != "SUCCESS" {
|
||||
fmt.Printf("[PayNotify] 支付未成功: %s\n", data["err_code"])
|
||||
c.String(http.StatusOK, successResponse())
|
||||
return
|
||||
}
|
||||
|
||||
orderSn := data["out_trade_no"]
|
||||
transactionID := data["transaction_id"]
|
||||
totalFee := 0
|
||||
fmt.Sscanf(data["total_fee"], "%d", &totalFee)
|
||||
totalAmount := float64(totalFee) / 100
|
||||
openID := data["openid"]
|
||||
|
||||
fmt.Printf("[PayNotify] 支付成功: orderSn=%s, transactionId=%s, amount=%.2f\n", orderSn, transactionID, totalAmount)
|
||||
|
||||
// 解析附加数据
|
||||
var attach struct {
|
||||
ProductType string `json:"productType"`
|
||||
ProductID string `json:"productId"`
|
||||
UserID string `json:"userId"`
|
||||
}
|
||||
if data["attach"] != "" {
|
||||
json.Unmarshal([]byte(data["attach"]), &attach)
|
||||
}
|
||||
|
||||
db := database.DB()
|
||||
|
||||
// 用 openID 解析真实买家身份
|
||||
buyerUserID := attach.UserID
|
||||
if openID != "" {
|
||||
var user model.User
|
||||
if err := db.Where("open_id = ?", openID).First(&user).Error; err == nil {
|
||||
if attach.UserID != "" && user.ID != attach.UserID {
|
||||
fmt.Printf("[PayNotify] 买家身份校验: attach.userId 与 openId 解析不一致,以 openId 为准\n")
|
||||
}
|
||||
buyerUserID = user.ID
|
||||
defer resp.Body.Close()
|
||||
for k, v := range resp.Header {
|
||||
if len(v) > 0 {
|
||||
c.Header(k, v[0])
|
||||
}
|
||||
}
|
||||
if buyerUserID == "" && attach.UserID != "" {
|
||||
buyerUserID = attach.UserID
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
var order model.Order
|
||||
result := db.Where("order_sn = ?", orderSn).First(&order)
|
||||
|
||||
if result.Error != nil {
|
||||
// 订单不存在,补记订单
|
||||
fmt.Printf("[PayNotify] 订单不存在,补记订单: %s\n", orderSn)
|
||||
|
||||
productID := attach.ProductID
|
||||
if productID == "" {
|
||||
productID = "fullbook"
|
||||
}
|
||||
productType := attach.ProductType
|
||||
if productType == "" {
|
||||
productType = "unknown"
|
||||
}
|
||||
desc := "支付回调补记订单"
|
||||
status := "paid"
|
||||
now := time.Now()
|
||||
|
||||
order = model.Order{
|
||||
ID: orderSn,
|
||||
OrderSN: orderSn,
|
||||
UserID: buyerUserID,
|
||||
OpenID: openID,
|
||||
ProductType: productType,
|
||||
ProductID: &productID,
|
||||
Amount: totalAmount,
|
||||
Description: &desc,
|
||||
Status: &status,
|
||||
TransactionID: &transactionID,
|
||||
PayTime: &now,
|
||||
}
|
||||
|
||||
db.Create(&order)
|
||||
} else if *order.Status != "paid" {
|
||||
// 更新订单状态
|
||||
status := "paid"
|
||||
now := time.Now()
|
||||
db.Model(&order).Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
"transaction_id": transactionID,
|
||||
"pay_time": now,
|
||||
})
|
||||
fmt.Printf("[PayNotify] 订单状态已更新为已支付: %s\n", orderSn)
|
||||
} else {
|
||||
fmt.Printf("[PayNotify] 订单已支付,跳过更新: %s\n", orderSn)
|
||||
}
|
||||
|
||||
// 更新用户购买记录
|
||||
if buyerUserID != "" && attach.ProductType != "" {
|
||||
if attach.ProductType == "fullbook" {
|
||||
// 全书购买
|
||||
db.Model(&model.User{}).Where("id = ?", buyerUserID).Update("has_full_book", true)
|
||||
fmt.Printf("[PayNotify] 用户已购全书: %s\n", buyerUserID)
|
||||
} else if attach.ProductType == "section" && attach.ProductID != "" {
|
||||
// 检查是否已有该章节的其他已支付订单
|
||||
var count int64
|
||||
db.Model(&model.Order{}).Where(
|
||||
"user_id = ? AND product_type = 'section' AND product_id = ? AND status = 'paid' AND order_sn != ?",
|
||||
buyerUserID, attach.ProductID, orderSn,
|
||||
).Count(&count)
|
||||
|
||||
if count == 0 {
|
||||
// 首次购买该章节,这里不需要更新 purchased_sections,因为查询时会从 orders 表读取
|
||||
fmt.Printf("[PayNotify] 用户首次购买章节: %s - %s\n", buyerUserID, attach.ProductID)
|
||||
} else {
|
||||
fmt.Printf("[PayNotify] 用户已有该章节的其他已支付订单: %s - %s\n", buyerUserID, attach.ProductID)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理相同产品的无效订单
|
||||
productID := attach.ProductID
|
||||
if productID == "" {
|
||||
productID = "fullbook"
|
||||
}
|
||||
|
||||
result := db.Where(
|
||||
"user_id = ? AND product_type = ? AND product_id = ? AND status = 'created' AND order_sn != ?",
|
||||
buyerUserID, attach.ProductType, productID, orderSn,
|
||||
).Delete(&model.Order{})
|
||||
|
||||
if result.RowsAffected > 0 {
|
||||
fmt.Printf("[PayNotify] 已清理无效订单: %d 个\n", result.RowsAffected)
|
||||
}
|
||||
|
||||
// 处理分销佣金
|
||||
processReferralCommission(db, buyerUserID, totalAmount, orderSn)
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, successResponse())
|
||||
c.Status(resp.StatusCode)
|
||||
io.Copy(c.Writer, resp.Body)
|
||||
}
|
||||
|
||||
// 处理分销佣金
|
||||
|
||||
@@ -152,14 +152,57 @@ func WithdrawRecords(c *gin.Context) {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user