Files
affiliate_dash/backend/internal/service/fulfillment_data.go
T
yml2213andCursor c2d8228512 fix(delivery): 硬化自动发货可靠性,避免双发与无限 Webhook 重试
恢复 ship_failed 可重试 UI;DLC 断点续传 queue;乐玩未绑定禁止进 delivering;Webhook 原子 nonce 与业务拒绝回 200。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 20:13:29 +08:00

207 lines
4.9 KiB
Go
Raw 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 service
import (
"bytes"
"encoding/json"
"strconv"
"strings"
"time"
"affiliate_dash/internal/pkg/timeutil"
)
// buildShipNotifyResultData 把推送结果与游戏字段合并进 ResultData JSON。
func buildShipNotifyResultData(existing string, in ShipNotifyInput, shippedAt *time.Time) string {
m := map[string]interface{}{}
if existing != "" && json.Valid([]byte(existing)) {
_ = json.Unmarshal([]byte(existing), &m)
}
m["ship_status"] = in.ShipStatus
if in.ProviderOrderNo != "" {
m["provider_order_no"] = in.ProviderOrderNo
}
if in.ShipStatus == "success" {
delete(m, "fail_reason")
} else if in.FailReason != "" {
m["fail_reason"] = in.FailReason
}
if shippedAt != nil {
m["shipped_at"] = timeutil.FormatAPITime(*shippedAt)
}
if in.GameChannel != nil {
m["game_channel"] = *in.GameChannel
}
if in.GameUID != nil {
m["game_uid"] = *in.GameUID
}
if in.RoleName != nil {
m["role_name"] = *in.RoleName
}
if in.PayScore != nil {
m["pay_score"] = *in.PayScore
}
raw, err := json.Marshal(m)
if err != nil {
return existing
}
return string(raw)
}
// mergeResultData 保留已有 JSON 字段,仅覆盖或新增 patch 中的字段,避免各发货阶段互相清空上下文。
func mergeResultData(existing string, patch map[string]interface{}) string {
if len(patch) == 0 {
return existing
}
m := resultDataMap(existing)
for k, v := range patch {
m[k] = v
}
raw, err := json.Marshal(m)
if err != nil {
return existing
}
return string(raw)
}
// encodeFulfillmentResultData 写入 result_data:已是 JSON 对象/数组字符串时不再二次 Marshal,避免双重编码。
func encodeFulfillmentResultData(v interface{}) (string, error) {
switch t := v.(type) {
case nil:
return "", nil
case string:
trimmed := strings.TrimSpace(t)
if trimmed == "" {
return "", nil
}
if json.Valid([]byte(trimmed)) && (strings.HasPrefix(trimmed, "{") || strings.HasPrefix(trimmed, "[")) {
return trimmed, nil
}
raw, err := json.Marshal(t)
return string(raw), err
case []byte:
trimmed := bytes.TrimSpace(t)
if len(trimmed) == 0 {
return "", nil
}
if json.Valid(trimmed) && (trimmed[0] == '{' || trimmed[0] == '[') {
return string(trimmed), nil
}
raw, err := json.Marshal(t)
return string(raw), err
default:
raw, err := json.Marshal(v)
if err != nil {
return "", err
}
return string(raw), nil
}
}
func resultDataMap(raw string) map[string]interface{} {
m := map[string]interface{}{}
if raw == "" {
return m
}
if err := json.Unmarshal([]byte(raw), &m); err != nil {
// 容错:如果 raw 被双重序列化为 string(如 "\"{\\\"provider\\\":...}\"")
var inner string
if json.Unmarshal([]byte(raw), &inner) == nil && inner != "" {
_ = json.Unmarshal([]byte(inner), &m)
}
}
return m
}
// requestDataMap 解析下单透传的 data(RequestData),非法或为空时返回空 map。
func requestDataMap(raw string) map[string]interface{} {
m := map[string]interface{}{}
if raw == "" {
return m
}
if err := json.Unmarshal([]byte(raw), &m); err != nil {
var inner string
if json.Unmarshal([]byte(raw), &inner) == nil && inner != "" {
_ = json.Unmarshal([]byte(inner), &m)
}
}
return m
}
// requestDataString 读取下单透传 data 中的字符串字段。
func requestDataString(raw, key string) string {
if v, ok := requestDataMap(raw)[key].(string); ok {
return strings.TrimSpace(v)
}
return ""
}
func resultDataString(raw, key string) string {
if v, ok := resultDataMap(raw)[key].(string); ok {
return v
}
return ""
}
func resultDataNumber(raw, key string) int64 {
switch v := resultDataMap(raw)[key].(type) {
case float64:
return int64(v)
case int64:
return v
case int:
return int64(v)
case string:
if n, err := strconv.ParseInt(v, 10, 64); err == nil {
return n
}
}
return 0
}
// extractGameFields 从 JSON 文本中还原游戏相关字段(仅填充当前为空的字段)。
func extractGameFields(raw string, out *OpenOrderQuery) {
if raw == "" || !json.Valid([]byte(raw)) {
return
}
var m map[string]interface{}
if err := json.Unmarshal([]byte(raw), &m); err != nil {
return
}
if out.GameChannel == "" {
if v, ok := m["game_channel"].(string); ok {
out.GameChannel = v
}
}
if out.GameUID == "" {
if v, ok := m["game_uid"].(string); ok && v != "" {
out.GameUID = v
} else if v, ok := m["game_account"].(string); ok && v != "" {
out.GameUID = v
}
}
if out.RoleName == "" {
if v, ok := m["role_name"].(string); ok && v != "" {
out.RoleName = v
} else if v, ok := m["game_account_role_name"].(string); ok && v != "" {
out.RoleName = v
}
}
if out.PayScore == 0 {
if v, ok := toInt(m["pay_score"]); ok {
out.PayScore = v
}
}
}
func toInt(v interface{}) (int, bool) {
switch n := v.(type) {
case float64:
return int(n), true
case int:
return n, true
case int64:
return int(n), true
}
return 0, false
}