恢复 ship_failed 可重试 UI;DLC 断点续传 queue;乐玩未绑定禁止进 delivering;Webhook 原子 nonce 与业务拒绝回 200。 Co-authored-by: Cursor <cursoragent@cursor.com>
207 lines
4.9 KiB
Go
207 lines
4.9 KiB
Go
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
|
||
}
|