Files
junhong_cmp_fiber/pkg/fuiou/notify.go

234 lines
5.9 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 fuiou
import (
"encoding/xml"
"fmt"
"io"
"net/url"
"strings"
)
// ParseNotify 解析回调通知(不验签)
// 用于在验签前提取订单号以确定使用哪套支付配置
func ParseNotify(rawBody []byte) (*NotifyRequest, error) {
utf8Str, err := decodeNotifyXML(rawBody)
if err != nil {
return nil, err
}
var notify NotifyRequest
if err := xml.Unmarshal([]byte(utf8Str), &notify); err != nil {
return nil, fmt.Errorf("XML 解析回调数据失败: %w", err)
}
return &notify, nil
}
// VerifyNotify 验证回调通知并解析数据
// rawBody: 原始请求 body可能包含 req= 前缀的 URL 编码 GBK XML
func (c *Client) VerifyNotify(rawBody []byte) (*NotifyRequest, error) {
utf8Str, err := decodeNotifyXML(rawBody)
if err != nil {
return nil, err
}
var notify NotifyRequest
if err := xml.Unmarshal([]byte(utf8Str), &notify); err != nil {
return nil, fmt.Errorf("XML 解析回调数据失败: %w", err)
}
signStr, err := buildNotifySignString(utf8Str)
if err != nil {
return nil, err
}
// 富友回调字段会随支付通道扩展,验签必须以 XML 实际出现的字段为准。
if err := c.verifySignString(signStr, notify.Sign); err != nil {
return nil, fmt.Errorf("回调签名验证失败: %w", err)
}
// 检查结果码
if notify.ResultCode != "000000" {
return &notify, fmt.Errorf("回调结果非成功: %s - %s", notify.ResultCode, notify.ResultMsg)
}
return &notify, nil
}
// buildNotifySignString 从回调 XML 实际字段构建验签原文。
func buildNotifySignString(utf8XML string) (string, error) {
fields, err := extractNotifySignFields(utf8XML)
if err != nil {
return "", err
}
return buildSignStringFromMap(fields), nil
}
// extractNotifySignFields 提取 XML 根节点下参与验签的字段。
func extractNotifySignFields(utf8XML string) (map[string]string, error) {
fields := make(map[string]string)
decoder := xml.NewDecoder(strings.NewReader(utf8XML))
depth := 0
currentField := ""
for {
token, err := decoder.Token()
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("XML 提取验签字段失败: %w", err)
}
switch t := token.(type) {
case xml.StartElement:
depth++
if depth == 2 {
currentField = t.Name.Local
fields[currentField] = ""
}
case xml.CharData:
if depth == 2 && currentField != "" {
fields[currentField] += string(t)
}
case xml.EndElement:
if depth == 2 {
currentField = ""
}
depth--
}
}
for key := range fields {
if key == "sign" || strings.HasPrefix(key, "reserved") {
delete(fields, key)
}
}
return fields, nil
}
// decodeNotifyXML 将富友回调报文统一解码为 UTF-8 XML。
func decodeNotifyXML(rawBody []byte) (string, error) {
content, err := extractNotifyContent(rawBody)
if err != nil {
return "", err
}
decoded, err := urlDecodeNotifyContent(content)
if err != nil {
return "", err
}
if strings.TrimSpace(decoded) == "" {
return "", fmt.Errorf("回调 XML 内容为空")
}
return notifyXMLToUTF8(decoded)
}
// extractNotifyContent 提取 req 字段或原始 XML 内容。
func extractNotifyContent(rawBody []byte) (string, error) {
content := strings.TrimSpace(string(rawBody))
if content == "" {
return "", fmt.Errorf("回调数据为空")
}
if strings.HasPrefix(content, "<") {
return content, nil
}
if values, err := url.ParseQuery(content); err == nil {
if reqValues, ok := values["req"]; ok {
if len(reqValues) == 0 || strings.TrimSpace(reqValues[0]) == "" {
return "", fmt.Errorf("回调 req 内容为空")
}
return reqValues[0], nil
}
}
if strings.HasPrefix(content, "req=") {
content = strings.TrimPrefix(content, "req=")
}
if strings.TrimSpace(content) == "" {
return "", fmt.Errorf("回调 req 内容为空")
}
return content, nil
}
// urlDecodeNotifyContent 兼容富友单次或双次 URL 编码的 XML。
func urlDecodeNotifyContent(content string) (string, error) {
decoded := strings.TrimSpace(content)
for i := 0; i < 2; i++ {
if strings.HasPrefix(decoded, "<") {
return decoded, nil
}
if !strings.ContainsAny(decoded, "%+") {
return decoded, nil
}
next, err := url.QueryUnescape(decoded)
if err != nil {
return "", fmt.Errorf("URL 解码回调数据失败: %w", err)
}
decoded = strings.TrimSpace(next)
}
return decoded, nil
}
// notifyXMLToUTF8 将 XML 内容转换为 UTF-8 并修正编码声明。
func notifyXMLToUTF8(content string) (string, error) {
if hasUTF8XMLDeclaration(content) {
return content, nil
}
utf8Bytes, err := GBKToUTF8([]byte(content))
if err != nil {
return "", fmt.Errorf("GBK 转 UTF-8 失败: %w", err)
}
replacer := strings.NewReplacer(
`encoding="GBK"`, `encoding="UTF-8"`,
`encoding="gbk"`, `encoding="UTF-8"`,
`encoding='GBK'`, `encoding='UTF-8'`,
`encoding='gbk'`, `encoding='UTF-8'`,
)
return replacer.Replace(string(utf8Bytes)), nil
}
// hasUTF8XMLDeclaration 判断 XML 声明是否已标明 UTF-8。
func hasUTF8XMLDeclaration(content string) bool {
head := content
if len(head) > 128 {
head = head[:128]
}
head = strings.ToLower(head)
return strings.Contains(head, `encoding="utf-8"`) || strings.Contains(head, `encoding='utf-8'`)
}
// BuildNotifySuccessResponse 构建成功响应。
func BuildNotifySuccessResponse() []byte {
return []byte("1")
}
// BuildNotifyFailResponse 构建失败响应。
func BuildNotifyFailResponse(_ string) []byte {
return []byte("0")
}
// buildNotifyResponse 构建回调响应 XMLGBK 编码)
func buildNotifyResponse(code, msg string) []byte {
resp := NotifyResponse{
ResultCode: code,
ResultMsg: msg,
}
xmlBytes, err := xml.Marshal(resp)
if err != nil {
return []byte(`<?xml version="1.0" encoding="GBK"?><xml><result_code>999999</result_code><result_msg>internal error</result_msg></xml>`)
}
xmlStr := `<?xml version="1.0" encoding="GBK"?>` + string(xmlBytes)
gbkBytes, err := utf8ToGBK([]byte(xmlStr))
if err != nil {
return []byte(xmlStr)
}
return gbkBytes
}