拆分 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:
@@ -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]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user