拆分 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
-325
View File
@@ -1,17 +1,9 @@
package service
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
@@ -616,67 +608,6 @@ func (s *DeliveryService) buildDeliveryState(openOrder *OpenOrderQuery, order *m
return info, order, goodID, nil
}
func buildExistingDeliverySubmitResult(order *model.FulfillmentOrder) *DeliverySubmitResult {
if order == nil {
return nil
}
switch normalizeOrderStatus(order) {
case model.OrderStatusDelivering, model.OrderStatusDelivered:
default:
return nil
}
var resultData map[string]interface{}
if json.Valid([]byte(order.ResultData)) {
_ = json.Unmarshal([]byte(order.ResultData), &resultData)
}
providerOrderNo := firstNonEmpty(
order.ProviderOrderNo,
stringFromMap(resultData, "provider_order_no"),
)
message := "订单暂时正在发货中,请稍后查询"
status := normalizeOrderStatus(order)
if status == model.OrderStatusDelivered {
message = "订单已交付"
}
return &DeliverySubmitResult{
OrderNo: order.OrderNo,
Status: status,
Message: message,
ProviderOrderNo: providerOrderNo,
UpstreamOrder: resultData,
}
}
func upstreamDeliverySucceeded(order map[string]interface{}) bool {
return strings.EqualFold(stringFromMap(order, "status"), "FINISHED") ||
strings.EqualFold(stringFromMap(order, "send_status"), "SUCCESS")
}
func deliverySubmissionInProgress(stage string) bool {
return stage == deliveryStageClaimed || stage == deliveryStageQueueCreated
}
func deliverySubmissionStale(order *model.FulfillmentOrder, now time.Time) bool {
if order == nil {
return false
}
startedAtRaw := resultDataString(order.ResultData, "submit_started_at")
if startedAtRaw != "" {
if startedAt, err := time.Parse(timeutil.APITimeLayout, startedAtRaw); err == nil {
return now.Sub(startedAt) >= deliverySubmissionStaleTimeout
}
}
return !order.UpdatedAt.IsZero() && now.Sub(order.UpdatedAt) >= deliverySubmissionStaleTimeout
}
func optionalUint(value uint) *uint {
if value == 0 {
return nil
}
out := value
return &out
}
func (s *DeliveryService) GetOrCreateDeliveryLink(merchantID uint, orderNo, requestBaseURL string) (*DeliveryLinkResult, error) {
orderNo = strings.TrimSpace(orderNo)
if orderNo == "" {
@@ -826,207 +757,6 @@ func (s *DeliveryService) prepareOrder(orderNo string, requireCanShip bool, auth
return info, order, goodID, nil
}
func (s *DeliveryService) authorizeDeliveryLink(order *model.FulfillmentOrder, auth DeliveryLinkAuth) error {
if order == nil {
return newDeliveryHTTPError(http.StatusNotFound, "订单不存在")
}
if auth.Exp <= 0 || strings.TrimSpace(auth.Sign) == "" {
return newDeliveryHTTPError(http.StatusForbidden, "链接参数缺失")
}
if order.DeliveryLinkRevokedAt != nil {
return newDeliveryHTTPError(http.StatusForbidden, "发货链接已作废")
}
if order.DeliveryLinkExpiresAt == nil {
return newDeliveryHTTPError(http.StatusForbidden, "发货链接未生成")
}
expiresAt := order.DeliveryLinkExpiresAt.UTC().Truncate(time.Second)
linkExpires := time.Unix(auth.Exp, 0).UTC()
if !linkExpires.Equal(expiresAt) {
return newDeliveryHTTPError(http.StatusForbidden, "发货链接已失效")
}
if time.Now().UTC().After(linkExpires) {
return newDeliveryHTTPError(http.StatusForbidden, "发货链接已过期")
}
expected := s.signDeliveryLink(order.OrderNo, auth.Exp)
if !hmac.Equal([]byte(strings.ToLower(strings.TrimSpace(auth.Sign))), []byte(expected)) {
return newDeliveryHTTPError(http.StatusForbidden, "发货链接签名无效")
}
return nil
}
func (s *DeliveryService) buildDeliveryLinkResult(orderNo string, expiresAt time.Time, requestBaseURL string) *DeliveryLinkResult {
expiresAt = expiresAt.UTC()
exp := expiresAt.Unix()
sign := s.signDeliveryLink(orderNo, exp)
return &DeliveryLinkResult{
OrderNo: orderNo,
DeliveryURL: s.buildDeliveryURL(orderNo, exp, sign, requestBaseURL),
ExpiresAt: expiresAt,
Exp: exp,
Sign: sign,
}
}
func (s *DeliveryService) buildDeliveryURL(orderNo string, exp int64, sign, requestBaseURL string) string {
path := fmt.Sprintf("/delivery/%s/%s?exp=%d&sign=%s", url.PathEscape(s.channel), url.PathEscape(orderNo), exp, url.QueryEscape(sign))
base := strings.TrimRight(s.linkBaseURL, "/")
if base == "" {
base = strings.TrimRight(requestBaseURL, "/")
}
if base == "" {
return path
}
return base + path
}
func (s *DeliveryService) signDeliveryLink(orderNo string, exp int64) string {
mac := hmac.New(sha256.New, []byte(s.linkSecret))
_, _ = mac.Write([]byte(orderNo + "|" + strconv.FormatInt(exp, 10)))
return hex.EncodeToString(mac.Sum(nil))
}
func buildDeliveryProduct(openProduct *OpenOrderProduct, good map[string]interface{}) *DeliveryProduct {
if openProduct == nil {
return nil
}
product := &DeliveryProduct{
Name: openProduct.Name,
SKU: openProduct.SKU,
Game: openProduct.Game,
}
if title := stringFromMap(good, "title"); title != "" {
product.Name = title
}
product.Image = stringFromMap(good, "image")
return product
}
func (s *DeliveryService) goodsDetail(goodID string) (map[string]interface{}, error) {
var out struct {
Good map[string]interface{} `json:"good"`
}
if err := s.signProxy("/public/goods/detail", "POST", map[string]interface{}{
"good_id": goodID,
}, &out); err != nil {
return nil, err
}
return out.Good, nil
}
func (s *DeliveryService) accountBound(bindUUID, goodID string) (map[string]interface{}, error) {
var out struct {
GameAccount map[string]interface{} `json:"gameAccount"`
Snake map[string]interface{} `json:"game_account"`
}
if err := s.signProxy("/public/games/account-bound", "POST", map[string]interface{}{
"bind_uuid": bindUUID,
"bindUuid": bindUUID,
"goodId": goodID,
"good_id": goodID,
}, &out); err != nil {
return nil, err
}
if out.GameAccount == nil {
out.GameAccount = out.Snake
}
if out.GameAccount == nil {
return nil, errors.New("账号尚未绑定,请扫码完成绑定后再提交")
}
return out.GameAccount, nil
}
func (s *DeliveryService) createOrderQueue(goodID, orderNo string) (string, error) {
var out struct {
Orders []map[string]interface{} `json:"orders"`
}
if err := s.signProxy("/public/users/orders-queue", "POST", map[string]interface{}{
"good_id": goodID,
"quantity": 1,
"order_sn": orderNo,
}, &out); err != nil {
return "", err
}
if len(out.Orders) == 0 {
return "", errors.New("发货服务未返回队列订单")
}
orderID := firstNonEmpty(stringFromMap(out.Orders[0], "_id"), stringFromMap(out.Orders[0], "id"))
if orderID == "" {
return "", errors.New("发货服务未返回队列订单 ID")
}
return orderID, nil
}
func (s *DeliveryService) patchOrderQueue(orderID, gameAccount, bindUUID string) error {
var out map[string]interface{}
return s.signProxy("/public/users/orders-queue", "PATCH", map[string]interface{}{
"order_id": orderID,
"game_account": gameAccount,
"bind_uuid": bindUUID,
}, &out)
}
func (s *DeliveryService) createUpstreamOrder(orderID, gameAccount, goodID, orderNo string) (map[string]interface{}, error) {
var out struct {
Order map[string]interface{} `json:"order"`
}
if err := s.signProxy("/public/users/orders", "POST", map[string]interface{}{
"order_id": orderID,
"game_account": gameAccount,
"good_id": goodID,
"h5_prefix": s.channel,
"order_sn": orderNo,
}, &out); err != nil {
return nil, err
}
if out.Order == nil {
out.Order = map[string]interface{}{"order_id": orderID, "order_sn": orderNo}
}
return out.Order, nil
}
func (s *DeliveryService) signProxy(path, method string, data interface{}, out interface{}) error {
payload, err := json.Marshal(map[string]interface{}{
"path": path,
"method": strings.ToUpper(method),
"data": data,
})
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, s.bffBaseURL+"/sign-proxy", bytes.NewReader(payload))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.httpClient.Do(req)
if err != nil {
return fmt.Errorf("发货服务请求失败:%w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
var envelope struct {
Code interface{} `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data"`
}
if err := json.Unmarshal(body, &envelope); err != nil {
return fmt.Errorf("发货服务响应无法解析:%s", truncateDeliveryText(string(body)))
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices || !isZeroCode(envelope.Code) {
if envelope.Message != "" {
return errors.New(envelope.Message)
}
return fmt.Errorf("发货服务请求失败:HTTP %d", resp.StatusCode)
}
if out == nil || len(envelope.Data) == 0 || string(envelope.Data) == "null" {
return nil
}
if err := json.Unmarshal(envelope.Data, out); err != nil {
return fmt.Errorf("发货服务数据无法解析:%w", err)
}
return nil
}
func deliveryGoodID(channel, sku string) string {
if channel != "dlc" {
return ""
@@ -1067,58 +797,3 @@ func deliveryGoodID(channel, sku string) string {
"lucky_coin_x90": "682ef39ca8f40c4234c59f42",
}[sku]
}
func isZeroCode(code interface{}) bool {
switch v := code.(type) {
case nil:
return true
case float64:
return v == 0
case string:
return v == "0"
default:
return false
}
}
func stringFromMap(m map[string]interface{}, key string) string {
if m == nil {
return ""
}
switch v := m[key].(type) {
case string:
return v
case float64:
return fmt.Sprintf("%.0f", v)
default:
if v != nil {
return fmt.Sprint(v)
}
return ""
}
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func gameChannelText(account map[string]interface{}) string {
area := stringFromMap(account, "game_account_area")
plat := stringFromMap(account, "game_account_plat")
if area == "" && plat == "" {
return ""
}
return strings.Trim(strings.Join([]string{area, plat}, "-"), "-")
}
func truncateDeliveryText(value string) string {
if len(value) <= 300 {
return value
}
return value[:300]
}