有效天数问题,充值单回调问题

This commit is contained in:
2026-05-08 11:46:34 +08:00
parent 7c85a8cf29
commit a93acc0784
12 changed files with 402 additions and 87 deletions

View File

@@ -3,6 +3,7 @@ package fuiou
import (
"encoding/xml"
"fmt"
"io"
"net/url"
"strings"
)
@@ -10,19 +11,10 @@ import (
// ParseNotify 解析回调通知(不验签)
// 用于在验签前提取订单号以确定使用哪套支付配置
func ParseNotify(rawBody []byte) (*NotifyRequest, error) {
content := string(rawBody)
if strings.HasPrefix(content, "req=") {
content = content[4:]
}
decoded, err := url.QueryUnescape(content)
utf8Str, err := decodeNotifyXML(rawBody)
if err != nil {
return nil, fmt.Errorf("URL 解码回调数据失败: %w", err)
return nil, err
}
utf8Bytes, err := GBKToUTF8([]byte(decoded))
if err != nil {
return nil, fmt.Errorf("GBK 转 UTF-8 失败: %w", err)
}
utf8Str := strings.Replace(string(utf8Bytes), `encoding="GBK"`, `encoding="UTF-8"`, 1)
var notify NotifyRequest
if err := xml.Unmarshal([]byte(utf8Str), &notify); err != nil {
return nil, fmt.Errorf("XML 解析回调数据失败: %w", err)
@@ -33,36 +25,22 @@ func ParseNotify(rawBody []byte) (*NotifyRequest, error) {
// VerifyNotify 验证回调通知并解析数据
// rawBody: 原始请求 body可能包含 req= 前缀的 URL 编码 GBK XML
func (c *Client) VerifyNotify(rawBody []byte) (*NotifyRequest, error) {
content := string(rawBody)
// 处理 req= 前缀(富友回调可能以 form 格式发送)
if strings.HasPrefix(content, "req=") {
content = content[4:]
}
// URL 解码
decoded, err := url.QueryUnescape(content)
utf8Str, err := decodeNotifyXML(rawBody)
if err != nil {
return nil, fmt.Errorf("URL 解码回调数据失败: %w", err)
return nil, err
}
// GBK → UTF-8
utf8Bytes, err := GBKToUTF8([]byte(decoded))
if err != nil {
return nil, fmt.Errorf("GBK 转 UTF-8 失败: %w", err)
}
// 替换编码声明
utf8Str := strings.Replace(string(utf8Bytes), `encoding="GBK"`, `encoding="UTF-8"`, 1)
// XML 解析
var notify NotifyRequest
if err := xml.Unmarshal([]byte(utf8Str), &notify); err != nil {
return nil, fmt.Errorf("XML 解析回调数据失败: %w", err)
}
// 验证签名
if err := c.Verify(&notify, notify.Sign); err != nil {
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)
}
@@ -74,14 +52,162 @@ func (c *Client) VerifyNotify(rawBody []byte) (*NotifyRequest, error) {
return &notify, nil
}
// BuildNotifySuccessResponse 构建成功响应GBK 编码的 XML
func BuildNotifySuccessResponse() []byte {
return buildNotifyResponse("000000", "success")
// buildNotifySignString 从回调 XML 实际字段构建验签原文。
func buildNotifySignString(utf8XML string) (string, error) {
fields, err := extractNotifySignFields(utf8XML)
if err != nil {
return "", err
}
return buildSignStringFromMap(fields), nil
}
// BuildNotifyFailResponse 构建失败响应GBK 编码的 XML
func BuildNotifyFailResponse(msg string) []byte {
return buildNotifyResponse("999999", msg)
// 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 编码)