422 lines
13 KiB
Go
422 lines
13 KiB
Go
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)
|
||
}
|