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) }