144 lines
3.6 KiB
Go
144 lines
3.6 KiB
Go
package payment
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"math"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
// 依次尝试:微信等常带 ¥;支付宝等到账文案多为「收款x.xx元」无货币符号。
|
||
var mengyaNoticeAmountPatterns = []*regexp.Regexp{
|
||
regexp.MustCompile(`到账[¥¥]\s*([\d]+(?:\.[\d]+)?)`),
|
||
regexp.MustCompile(`到账\s*([\d]+(?:\.[\d]+)?)\s*元`),
|
||
regexp.MustCompile(`收款[¥¥]\s*([\d]+(?:\.[\d]+)?)(?:\s*元)?`),
|
||
regexp.MustCompile(`收款\s*([\d]+(?:\.[\d]+)?)\s*元`),
|
||
}
|
||
|
||
// AmountEpsilon:Webhook 到账金额与订单快照金额对比时的容差。
|
||
const AmountEpsilon = 0.005
|
||
|
||
func AmountAlmostEqual(expected, actual float64) bool {
|
||
return math.Abs(expected-actual) < AmountEpsilon
|
||
}
|
||
|
||
// ParseWebhookJSON 从转发通知体中解析支付金额:
|
||
// 优先 body.notice,其次顶层 notice,否则在序列化后的 JSON 字符串中扫描。
|
||
func ParseWebhookJSON(raw []byte) (float64, bool) {
|
||
if len(bytes.TrimSpace(raw)) == 0 {
|
||
return 0, false
|
||
}
|
||
var top map[string]json.RawMessage
|
||
if err := json.Unmarshal(raw, &top); err != nil {
|
||
return scanAmount(string(raw))
|
||
}
|
||
if v, ok := top["notice"]; ok {
|
||
if s := jsonString(v); s != "" {
|
||
return scanAmount(s)
|
||
}
|
||
}
|
||
if inner, ok := top["body"]; ok {
|
||
var nested map[string]json.RawMessage
|
||
if json.Unmarshal(inner, &nested) == nil {
|
||
if v, ok := nested["notice"]; ok {
|
||
if s := jsonString(v); s != "" {
|
||
return scanAmount(s)
|
||
}
|
||
}
|
||
if amt, ok := scanAmount(jsonString(inner)); ok {
|
||
return amt, ok
|
||
}
|
||
return scanAmount(string(inner))
|
||
}
|
||
if amt, ok := scanAmount(strings.Trim(strings.Trim(string(inner), "\""), `"`)); ok {
|
||
return amt, ok
|
||
}
|
||
return scanAmount(string(inner))
|
||
}
|
||
buf, _ := json.Marshal(top)
|
||
return scanAmount(string(buf))
|
||
}
|
||
|
||
func jsonString(raw json.RawMessage) string {
|
||
var s string
|
||
if json.Unmarshal(raw, &s) == nil {
|
||
return s
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func scanAmount(text string) (float64, bool) {
|
||
if amt, ok := parseAmountRegex(text); ok {
|
||
return amt, true
|
||
}
|
||
return parseAmountRegex(strings.ReplaceAll(text, `\n`, "\n"))
|
||
}
|
||
|
||
func parseAmountRegex(text string) (float64, bool) {
|
||
for _, re := range mengyaNoticeAmountPatterns {
|
||
match := re.FindStringSubmatch(text)
|
||
if len(match) < 2 {
|
||
continue
|
||
}
|
||
s := strings.ReplaceAll(match[1], ",", "")
|
||
yuan, err := strconv.ParseFloat(s, 64)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
return yuan, true
|
||
}
|
||
return 0, false
|
||
}
|
||
|
||
// NoticeFieldsForLog 从常见萌芽/转发 JSON 里抽出 notice 等字段,仅供后台日志排查(如微信/支付宝文案差异)。
|
||
func NoticeFieldsForLog(raw []byte) string {
|
||
if len(bytes.TrimSpace(raw)) == 0 {
|
||
return ""
|
||
}
|
||
var top map[string]json.RawMessage
|
||
if err := json.Unmarshal(raw, &top); err != nil {
|
||
return ""
|
||
}
|
||
var parts []string
|
||
add := func(k, v string) {
|
||
if v == "" {
|
||
return
|
||
}
|
||
if len(v) > 600 {
|
||
v = v[:600] + "…"
|
||
}
|
||
parts = append(parts, fmt.Sprintf("%s=%s", k, strconv.Quote(v)))
|
||
}
|
||
if v, ok := top["notice"]; ok {
|
||
add("notice", jsonString(v))
|
||
}
|
||
if inner, ok := top["body"]; ok {
|
||
var nested map[string]json.RawMessage
|
||
if json.Unmarshal(inner, &nested) == nil {
|
||
if v, ok := nested["notice"]; ok {
|
||
add("body.notice", jsonString(v))
|
||
}
|
||
if v, ok := nested["content"]; ok {
|
||
add("body.content", jsonString(v))
|
||
}
|
||
if v, ok := nested["text"]; ok {
|
||
add("body.text", jsonString(v))
|
||
}
|
||
} else if s := jsonString(inner); s != "" {
|
||
add("body(string)", s)
|
||
}
|
||
}
|
||
for _, k := range []string{"title", "msg", "message", "desc", "description"} {
|
||
if v, ok := top[k]; ok {
|
||
add(k, jsonString(v))
|
||
}
|
||
}
|
||
if len(parts) == 0 {
|
||
return ""
|
||
}
|
||
return strings.Join(parts, "; ")
|
||
}
|