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), ¬ify); err != nil { return nil, fmt.Errorf("XML 解析回调数据失败: %w", err) } return ¬ify, 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), ¬ify); 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 ¬ify, fmt.Errorf("回调结果非成功: %s - %s", notify.ResultCode, notify.ResultMsg) } return ¬ify, 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 构建回调响应 XML(GBK 编码) func buildNotifyResponse(code, msg string) []byte { resp := NotifyResponse{ ResultCode: code, ResultMsg: msg, } xmlBytes, err := xml.Marshal(resp) if err != nil { return []byte(`999999internal error`) } xmlStr := `` + string(xmlBytes) gbkBytes, err := utf8ToGBK([]byte(xmlStr)) if err != nil { return []byte(xmlStr) } return gbkBytes }