拆分 service 与前端大文件,修复 CORS 配置与格式问题

- 后端 internal/service 按职责拆分:
  fulfillment.go(1397→527)拆出 wallet/timeout/data/order/query/dashboard/shipnotify
  delivery.go(1124→801)拆出 upstream/link/state/helpers
  merchant.go(855→251)拆出 member/product/api_client/catalog/helpers
- 前端 MerchantCenter.tsx(1327→606)拆出 merchantCenterTabs/merchantCenterUtils
- docker-compose backend 透传 CORS_ALLOWED_ORIGINS
- CORS 白名单实现(config/router/README/.env.example 配套)
- 修复 gofmt 与文件尾部多余空行
This commit is contained in:
yml2213
2026-08-05 13:32:11 +08:00
parent 569109cd92
commit 2264851d5d
29 changed files with 2990 additions and 2606 deletions
@@ -0,0 +1,157 @@
package service
import (
"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 := map[string]interface{}{}
if existing != "" && json.Valid([]byte(existing)) {
_ = json.Unmarshal([]byte(existing), &m)
}
for k, v := range patch {
m[k] = v
}
raw, err := json.Marshal(m)
if err != nil {
return existing
}
return string(raw)
}
func resultDataMap(raw string) map[string]interface{} {
m := map[string]interface{}{}
if raw != "" && json.Valid([]byte(raw)) {
_ = json.Unmarshal([]byte(raw), &m)
}
return m
}
// requestDataMap 解析下单透传的 dataRequestData),非法或为空时返回空 map。
func requestDataMap(raw string) map[string]interface{} {
m := map[string]interface{}{}
if raw != "" && json.Valid([]byte(raw)) {
_ = json.Unmarshal([]byte(raw), &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 {
out.GameUID = v
}
}
if out.RoleName == "" {
if v, ok := m["role_name"].(string); ok {
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
}