优化提现功能,更新提现审核逻辑以处理API响应,确保在操作失败时提供用户反馈。同时,调整页面数据加载逻辑,提升用户体验和代码可读性。
This commit is contained in:
@@ -201,16 +201,20 @@ func AdminWithdrawalsAction(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 打款已受理,更新为处理中并保存微信批次号
|
||||
// 打款已受理,更新为处理中并保存商家批次/明细单号及微信批次号
|
||||
processingStatus := "processing"
|
||||
batchID := result.BatchID
|
||||
_ = db.Model(&w).Updates(map[string]interface{}{
|
||||
if err := db.Model(&w).Updates(map[string]interface{}{
|
||||
"status": processingStatus,
|
||||
"batch_no": outBatchNo,
|
||||
"detail_no": outDetailNo,
|
||||
"batch_id": batchID,
|
||||
"processed_at": now,
|
||||
}).Error
|
||||
}).Error; err != nil {
|
||||
fmt.Printf("[AdminWithdrawals] 更新提现状态失败 id=%s: %v\n", body.ID, err)
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "更新状态失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
// 始终返回 out_batch_no 便于追踪;batch_id 为微信返回,可能为空
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
|
||||
@@ -2,7 +2,13 @@ package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
"soul-api/internal/wechat"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -48,38 +54,54 @@ func PaymentWechatNotify(c *gin.Context) {
|
||||
}
|
||||
|
||||
// PaymentWechatTransferNotify POST /api/payment/wechat/transfer/notify
|
||||
// 使用 PowerWeChat 验签、解密密文后更新提现状态,并返回微信要求的应答
|
||||
func PaymentWechatTransferNotify(c *gin.Context) {
|
||||
// 微信转账回调处理
|
||||
// 注意:实际生产环境需要验证签名,这里简化处理
|
||||
|
||||
var req struct {
|
||||
ID string `json:"id"`
|
||||
CreateTime string `json:"create_time"`
|
||||
EventType string `json:"event_type"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
Summary string `json:"summary"`
|
||||
Resource struct {
|
||||
Algorithm string `json:"algorithm"`
|
||||
Ciphertext string `json:"ciphertext"`
|
||||
AssociatedData string `json:"associated_data"`
|
||||
Nonce string `json:"nonce"`
|
||||
} `json:"resource"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
fmt.Printf("[TransferNotify] 解析请求失败: %v\n", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": "FAIL", "message": "请求格式错误"})
|
||||
resp, err := wechat.HandleTransferNotify(c.Request, func(outBillNo, transferBillNo, state, failReason string) error {
|
||||
fmt.Printf("[TransferNotify] 解密成功: out_bill_no=%s, transfer_bill_no=%s, state=%s\n", outBillNo, transferBillNo, state)
|
||||
db := database.DB()
|
||||
var w model.Withdrawal
|
||||
if err := db.Where("detail_no = ?", outBillNo).First(&w).Error; err != nil {
|
||||
fmt.Printf("[TransferNotify] 未找到 detail_no=%s 的提现记录: %v\n", outBillNo, err)
|
||||
return nil // 找不到也返回成功,避免微信重复推送
|
||||
}
|
||||
// 幂等:仅当当前为处理中/待确认时更新
|
||||
cur := ""
|
||||
if w.Status != nil {
|
||||
cur = *w.Status
|
||||
}
|
||||
if cur != "processing" && cur != "pending_confirm" {
|
||||
return nil
|
||||
}
|
||||
now := time.Now()
|
||||
up := map[string]interface{}{"processed_at": now}
|
||||
switch state {
|
||||
case "SUCCESS":
|
||||
up["status"] = "success"
|
||||
case "FAIL", "CANCELLED":
|
||||
up["status"] = "failed"
|
||||
if failReason != "" {
|
||||
up["fail_reason"] = failReason
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
if err := db.Model(&w).Updates(up).Error; err != nil {
|
||||
return fmt.Errorf("更新提现状态失败: %w", err)
|
||||
}
|
||||
fmt.Printf("[TransferNotify] 已更新提现 id=%s -> status=%s\n", w.ID, up["status"])
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("[TransferNotify] 验签/解密/处理失败: %v\n", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": "FAIL", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("[TransferNotify] 收到转账回调: event_type=%s\n", req.EventType)
|
||||
|
||||
// TODO: 使用 APIv3 密钥解密 resource.ciphertext
|
||||
// 解密后可以获取转账详情(outBatchNo、outDetailNo、detailStatus等)
|
||||
|
||||
// 暂时记录日志,实际处理需要解密后进行
|
||||
fmt.Printf("[TransferNotify] 转账回调数据: %+v\n", req)
|
||||
|
||||
// 返回成功响应
|
||||
c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "OK"})
|
||||
defer resp.Body.Close()
|
||||
for k, v := range resp.Header {
|
||||
if len(v) > 0 {
|
||||
c.Header(k, v[0])
|
||||
}
|
||||
}
|
||||
c.Status(resp.StatusCode)
|
||||
io.Copy(c.Writer, resp.Body)
|
||||
}
|
||||
|
||||
@@ -364,6 +364,29 @@ func HandlePayNotify(req *http.Request, handler func(orderSn, transactionID stri
|
||||
})
|
||||
}
|
||||
|
||||
// HandleTransferNotify 处理商家转账结果回调:验签并解密后调用 handler,返回应写回微信的 HTTP 响应
|
||||
// handler 参数:outBillNo(商户单号/即我们存的 detail_no)、transferBillNo、state(SUCCESS/FAIL/CANCELLED)、failReason
|
||||
func HandleTransferNotify(req *http.Request, handler func(outBillNo, transferBillNo, state, failReason string) error) (*http.Response, error) {
|
||||
if paymentApp == nil {
|
||||
return nil, fmt.Errorf("支付/转账未初始化")
|
||||
}
|
||||
return paymentApp.HandleTransferBillsNotify(req, func(_ *notifyrequest.RequestNotify, bill *models.TransferBills, fail func(string)) interface{} {
|
||||
if bill == nil {
|
||||
fail("bill is nil")
|
||||
return nil
|
||||
}
|
||||
outBillNo := bill.OutBillNo
|
||||
transferBillNo := bill.TransferBillNo
|
||||
state := bill.State
|
||||
failReason := bill.FailReason
|
||||
if err := handler(outBillNo, transferBillNo, state, failReason); err != nil {
|
||||
fail(err.Error())
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GenerateOrderSn 生成订单号
|
||||
func GenerateOrderSn() string {
|
||||
now := time.Now()
|
||||
|
||||
Reference in New Issue
Block a user