duidui_fiber/internal/payment/handler.go
2026-03-27 10:34:03 +08:00

422 lines
13 KiB
Go
Raw Permalink 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 payment
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"reflect"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
camp_dao "dd_fiber_api/internal/camp/dao"
"dd_fiber_api/internal/order"
order_dao "dd_fiber_api/internal/order/dao"
"dd_fiber_api/pkg/snowflake"
)
// Handler 支付处理器
type Handler struct {
wechatPayV3Service *WechatPayV3Service
orderDAO *order_dao.OrderDAO
accessDAO *camp_dao.UserSectionAccessDAO // 用户小节访问记录 DAO
userCampDAO *camp_dao.UserCampDAO // 用户打卡营 DAO用于更新当前小节
}
// NewHandler 创建支付处理器
func NewHandler(wechatPayV3Service *WechatPayV3Service) *Handler {
return &Handler{
wechatPayV3Service: wechatPayV3Service,
}
}
// SetOrderDAO 设置订单DAO用于支付回调时更新订单状态
func (h *Handler) SetOrderDAO(orderDAO *order_dao.OrderDAO) {
h.orderDAO = orderDAO
}
// SetAccessDAO 设置访问记录 DAO用于支付完成后创建访问记录
func (h *Handler) SetAccessDAO(accessDAO *camp_dao.UserSectionAccessDAO) {
h.accessDAO = accessDAO
}
// SetUserCampDAO 设置用户打卡营 DAO用于更新当前小节
func (h *Handler) SetUserCampDAO(userCampDAO *camp_dao.UserCampDAO) {
h.userCampDAO = userCampDAO
}
// CreateWechatPayV3 创建微信支付V3订单
// POST /api/v1/payment/wechat/v3
func (h *Handler) CreateWechatPayV3(c *fiber.Ctx) error {
var req CreateWechatPayV3Request
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{
"error": "请求参数解析失败: " + err.Error(),
})
}
// 验证必填字段
if req.OrderID == "" {
return c.Status(400).JSON(fiber.Map{
"error": "order_id 是必需的",
})
}
if req.Description == "" {
return c.Status(400).JSON(fiber.Map{
"error": "description 是必需的",
})
}
if req.OpenID == "" {
return c.Status(400).JSON(fiber.Map{
"error": "openid 是必需的",
})
}
if req.Amount <= 0 {
return c.Status(400).JSON(fiber.Map{
"error": "amount 必须大于0",
})
}
// 调用服务
resp, err := h.wechatPayV3Service.CreateWechatPayV3(c.Context(), &req)
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": "创建支付订单失败: " + err.Error(),
})
}
// 返回响应
if !resp.Success {
return c.Status(400).JSON(resp)
}
return c.JSON(resp)
}
// HandleWechatPayV3Notify 处理微信支付V3通知使用官方SDK的notify handler
// POST /api/v1/payment/wechat/v3/notify
func (h *Handler) HandleWechatPayV3Notify(c *fiber.Ctx) error {
// 保存原始请求体(用于测试和调试)
rawBody := c.Body()
if len(rawBody) == 0 {
return c.Status(200).JSON(fiber.Map{
"code": "FAIL",
"message": "请求体为空",
})
}
// 复制 body用于保存
rawBodyCopy := make([]byte, len(rawBody))
copy(rawBodyCopy, rawBody)
// 保存请求头信息
headers := make(map[string]string)
c.Request().Header.VisitAll(func(key, val []byte) {
headers[string(key)] = string(val)
})
// 先保存原始数据
h.saveNotifyData("raw", rawBodyCopy, headers, nil)
// 使用官方SDK的notify handler处理回调通知
// 官方SDK会自动处理签名验证和解密
apiKeyV3 := h.wechatPayV3Service.GetAPIKeyV3()
// 将Fiber的请求转换为标准http.Request用于notify handler
// 注意Fiber使用fasthttp需要手动构造http.Request
httpReq, err := http.NewRequestWithContext(c.Context(), "POST", c.OriginalURL(), bytes.NewReader(rawBodyCopy))
if err != nil {
h.saveNotifyData("parse_error", rawBodyCopy, headers, nil)
return c.Status(200).JSON(fiber.Map{
"code": "FAIL",
"message": "创建http.Request失败: " + err.Error(),
})
}
// 复制请求头
c.Request().Header.VisitAll(func(key, val []byte) {
httpReq.Header.Set(string(key), string(val))
})
// 创建notify handler使用NewRSANotifyHandler它包含AES-GCM解密能力
// 获取verifier用于验证签名
verifier, err := h.wechatPayV3Service.GetVerifier()
if err != nil {
h.saveNotifyData("parse_error", rawBodyCopy, headers, nil)
return c.Status(200).JSON(fiber.Map{
"code": "FAIL",
"message": "获取verifier失败: " + err.Error(),
})
}
handler, err := notify.NewRSANotifyHandler(apiKeyV3, verifier)
if err != nil {
h.saveNotifyData("parse_error", rawBodyCopy, headers, nil)
return c.Status(200).JSON(fiber.Map{
"code": "FAIL",
"message": "创建notify handler失败: " + err.Error(),
})
}
// 使用官方SDK解析通知自动验证签名和解密
// content参数可以是map[string]interface{}来接收解密后的数据
content := make(map[string]interface{})
notification, err := handler.ParseNotifyRequest(c.Context(), httpReq, content)
if err != nil {
// 检查是否是时间戳过期错误
errMsg := err.Error()
if strings.Contains(errMsg, "timestamp") && strings.Contains(errMsg, "expires") {
log.Printf("⚠️ 解析微信支付回调通知失败: 时间戳已过期(可能是使用旧测试数据导致): %v", err)
} else {
log.Printf("⚠️ 解析微信支付回调通知失败: %v", err)
}
h.saveNotifyData("parse_error", rawBodyCopy, headers, nil)
// 返回 200 状态码,避免微信重复推送
return c.Status(200).JSON(fiber.Map{
"code": "FAIL",
"message": "解析通知失败: " + err.Error(),
})
}
// 保存解析后的数据
h.saveNotifyData("success", rawBodyCopy, headers, notification)
// 从notification.Resource.Plaintext中获取解密后的业务数据
// 官方SDK会将解密后的数据放在Resource.Plaintext字段中JSON字符串格式
var transactionData map[string]interface{}
// 使用反射获取Resource.Plaintext字段
notificationValue := reflect.ValueOf(notification)
if notificationValue.Kind() == reflect.Ptr {
notificationValue = notificationValue.Elem()
}
resourceField := notificationValue.FieldByName("Resource")
if !resourceField.IsValid() || resourceField.IsNil() {
log.Printf("❌ notification.Resource字段无效或为空")
return c.Status(200).JSON(fiber.Map{
"code": "FAIL",
"message": "无法获取Resource字段",
})
}
resourceValue := resourceField.Interface()
// 尝试将Resource转换为map
resourceMap, ok := resourceValue.(map[string]interface{})
if !ok {
// 如果不是map尝试使用反射获取Plaintext字段
resourceReflectValue := reflect.ValueOf(resourceValue)
if resourceReflectValue.Kind() == reflect.Ptr {
resourceReflectValue = resourceReflectValue.Elem()
}
plaintextField := resourceReflectValue.FieldByName("Plaintext")
if !plaintextField.IsValid() || plaintextField.Kind() != reflect.String {
return c.Status(200).JSON(fiber.Map{
"code": "FAIL",
"message": "无法获取Plaintext字段",
})
}
plaintextStr := plaintextField.String()
if err := json.Unmarshal([]byte(plaintextStr), &transactionData); err != nil {
return c.Status(200).JSON(fiber.Map{
"code": "FAIL",
"message": "解析解密数据失败: " + err.Error(),
})
}
} else {
// 从map中获取Plaintext
plaintext, exists := resourceMap["Plaintext"]
if !exists {
return c.Status(200).JSON(fiber.Map{
"code": "FAIL",
"message": "Resource中不存在Plaintext字段",
})
}
plaintextStr, ok := plaintext.(string)
if !ok {
return c.Status(200).JSON(fiber.Map{
"code": "FAIL",
"message": "Plaintext不是字符串类型",
})
}
// 解析Plaintext中的JSON字符串
if err := json.Unmarshal([]byte(plaintextStr), &transactionData); err != nil {
return c.Status(200).JSON(fiber.Map{
"code": "FAIL",
"message": "解析解密数据失败: " + err.Error(),
})
}
}
// 获取交易状态和订单号
tradeState, _ := transactionData["trade_state"].(string)
outTradeNo, _ := transactionData["out_trade_no"].(string)
transactionID, _ := transactionData["transaction_id"].(string)
successTime, _ := transactionData["success_time"].(string)
// 更新订单状态
if h.orderDAO != nil && outTradeNo != "" {
// 先查询当前订单状态(幂等性检查)
currentOrder, err := h.orderDAO.GetOrderByOrderNo(outTradeNo)
if err == nil && currentOrder != nil {
// 如果订单已经是最终状态PAID, REFUNDED, CANCELLED且交易状态也是成功则跳过更新
if currentOrder.Status == order.OrderStatusPaid && tradeState == "SUCCESS" {
// 仍然返回成功,避免微信重复推送
return c.Status(200).JSON(fiber.Map{
"code": "SUCCESS",
"message": "订单已处理",
})
}
}
// 将微信支付的交易状态映射到订单状态
var orderStatus order.OrderStatus
switch tradeState {
case "SUCCESS":
orderStatus = order.OrderStatusPaid
case "NOTPAY", "USERPAYING":
orderStatus = order.OrderStatusPending
case "PAYERROR", "CLOSED":
orderStatus = order.OrderStatusFailed
case "REVOKED":
orderStatus = order.OrderStatusCancelled
case "REFUND":
orderStatus = order.OrderStatusRefunded
default:
// 如果事件类型是 TRANSACTION.SUCCESS则认为是支付成功
if notification.EventType == "TRANSACTION.SUCCESS" {
orderStatus = order.OrderStatusPaid
} else {
// 其他情况不更新订单状态
orderStatus = ""
}
}
// 只有当订单状态确定时才更新
if orderStatus != "" {
// 解析支付时间
var paymentTime *time.Time
if successTime != "" {
// 尝试解析 RFC3339 格式(微信支付返回的格式)
if t, err := time.Parse(time.RFC3339, successTime); err == nil {
paymentTime = &t
} else if t, err := time.Parse("2006-01-02T15:04:05+08:00", successTime); err == nil {
paymentTime = &t
}
}
// 直接使用 orderDAO 更新订单状态
err := h.orderDAO.UpdateOrderStatus(
"", // orderID 为空,使用 orderNo
outTradeNo,
orderStatus,
order.PaymentMethodWechat,
transactionID,
paymentTime,
)
if err != nil {
log.Printf("更新订单状态失败: %v", err)
} else if orderStatus == order.OrderStatusPaid && h.accessDAO != nil {
// 订单支付成功后,创建访问记录
// 查询订单信息(包括业务数据)
orderObj, err := h.orderDAO.GetOrderByOrderNo(outTradeNo)
if err == nil && orderObj != nil && orderObj.SectionID != "" {
// 检查是否已存在访问记录(幂等性检查,避免重复创建)
hasAccess, checkErr := h.accessDAO.GetByUserAndSection(orderObj.UserID, orderObj.SectionID)
if checkErr == nil && !hasAccess {
// 生成访问记录ID使用雪花算法
accessID := snowflake.GetInstance().NextIDString()
// 创建访问记录(只保留访问权限相关字段,订单信息从 orders 表查询)
paidPriceFen := int32(orderObj.ActualAmount)
if paidPriceFen < 0 {
paidPriceFen = 0
}
err = h.accessDAO.Create(
accessID,
orderObj.UserID,
orderObj.CampID,
orderObj.SectionID,
paidPriceFen,
camp_dao.AccessSourcePurchase,
)
if err != nil {
log.Printf("创建访问记录失败: %v", err)
} else {
log.Printf("✅ 访问记录创建成功: user_id=%s, section_id=%s", orderObj.UserID, orderObj.SectionID)
// 更新当前小节
if h.userCampDAO != nil && orderObj.CampID != "" {
if err := h.userCampDAO.UpdateCurrentSection(orderObj.UserID, orderObj.CampID, orderObj.SectionID); err != nil {
log.Printf("更新当前小节失败: %v", err)
} else {
log.Printf("✅ 当前小节更新成功: user_id=%s, camp_id=%s, section_id=%s", orderObj.UserID, orderObj.CampID, orderObj.SectionID)
}
}
}
} else if hasAccess {
log.Printf("访问记录已存在,跳过创建: user_id=%s, section_id=%s", orderObj.UserID, orderObj.SectionID)
}
}
}
}
} else if h.orderDAO == nil {
log.Printf("⚠️ 订单DAO未初始化无法更新订单状态")
} else if outTradeNo == "" {
log.Printf("⚠️ 订单号为空,无法更新订单状态")
}
// 返回 200 状态码和成功响应
return c.Status(200).JSON(fiber.Map{
"code": "SUCCESS",
"message": "成功",
})
}
// saveNotifyData 保存支付回调数据到文件(用于测试和调试)
func (h *Handler) saveNotifyData(status string, rawBody []byte, headers map[string]string, parsedReq interface{}) {
// 获取当前工作目录
workDir, err := os.Getwd()
if err != nil {
workDir = "." // 使用当前目录
}
// 创建保存目录(使用绝对路径)
saveDir := filepath.Join(workDir, "storage", "payment_notify")
if err := os.MkdirAll(saveDir, 0755); err != nil {
return
}
// 构建保存的数据
saveData := map[string]interface{}{
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
"status": status,
"raw_body": string(rawBody),
"headers": headers,
}
if parsedReq != nil {
saveData["parsed_request"] = parsedReq
}
// 转换为 JSON
jsonData, err := json.MarshalIndent(saveData, "", " ")
if err != nil {
return
}
// 生成文件名(使用时间戳和状态)
fileName := fmt.Sprintf("notify_%s_%s.json", time.Now().Format("20060102_150405"), status)
filePath := filepath.Join(saveDir, fileName)
// 保存到文件
_ = os.WriteFile(filePath, jsonData, 0644)
}