package service import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strconv" "strings" "time" "affiliate_dash/internal/model" "affiliate_dash/internal/pkg/timeutil" "gorm.io/gorm" "gorm.io/gorm/clause" ) const defaultDeliveryBFFBaseURL = "https://www.jxya.top/bff-stg" type DeliveryService struct { fulfillment *FulfillmentService bffBaseURL string channel string linkBaseURL string linkSecret string linkTTL time.Duration httpClient *http.Client } func NewDeliveryService(fulfillment *FulfillmentService, bffBaseURL, channel, linkBaseURL, linkSecret string, linkTTLMinutes int) *DeliveryService { bffBaseURL = strings.TrimRight(strings.TrimSpace(bffBaseURL), "/") if bffBaseURL == "" { bffBaseURL = defaultDeliveryBFFBaseURL } channel = strings.TrimSpace(channel) if channel == "" { channel = "dlc" } linkBaseURL = strings.TrimRight(strings.TrimSpace(linkBaseURL), "/") linkSecret = strings.TrimSpace(linkSecret) if linkSecret == "" { linkSecret = "affiliate-dash-delivery-link-secret" } if linkTTLMinutes <= 0 { linkTTLMinutes = 120 } return &DeliveryService{ fulfillment: fulfillment, bffBaseURL: bffBaseURL, channel: channel, linkBaseURL: linkBaseURL, linkSecret: linkSecret, linkTTL: time.Duration(linkTTLMinutes) * time.Minute, httpClient: &http.Client{Timeout: 15 * time.Second}, } } func (s *DeliveryService) SetHTTPClient(client *http.Client) { if client != nil { s.httpClient = client } } type DeliveryOrderInfo struct { OrderNo string `json:"order_no"` Status string `json:"status"` CanShip bool `json:"can_ship"` CannotShipReason string `json:"cannot_ship_reason,omitempty"` Product *DeliveryProduct `json:"product,omitempty"` BuyerName string `json:"buyer_name"` Amount int64 `json:"amount"` CreatedAt time.Time `json:"created_at"` ShippedAt *time.Time `json:"shipped_at"` ShipFailReason string `json:"ship_fail_reason,omitempty"` GameChannel string `json:"game_channel,omitempty"` GameUID string `json:"game_uid,omitempty"` RoleName string `json:"role_name,omitempty"` PayScore int `json:"pay_score,omitempty"` Good map[string]interface{} `json:"good,omitempty"` } type DeliveryProduct struct { Name string `json:"name"` SKU string `json:"sku"` Game string `json:"game"` Image string `json:"image,omitempty"` } type DeliveryBindResult struct { BindUUID string `json:"bind_uuid"` BindURL string `json:"bind_url"` QRURL string `json:"qr_url"` } type DeliveryLinkResult struct { OrderNo string `json:"order_no"` DeliveryURL string `json:"delivery_url"` ExpiresAt time.Time `json:"expires_at"` Exp int64 `json:"exp"` Sign string `json:"sign"` } type DeliverySubmitResult struct { OrderNo string `json:"order_no"` Status string `json:"status"` Message string `json:"message"` ProviderOrderNo string `json:"provider_order_no,omitempty"` GameAccount map[string]interface{} `json:"game_account,omitempty"` UpstreamOrder interface{} `json:"upstream_order,omitempty"` } type DeliveryLinkAuth struct { Exp int64 Sign string } type deliveryHTTPError struct { status int message string } func (e deliveryHTTPError) Error() string { return e.message } func (e deliveryHTTPError) HTTPStatus() int { return e.status } func newDeliveryHTTPError(status int, message string) error { return deliveryHTTPError{status: status, message: message} } func (s *DeliveryService) GetOrder(orderNo string, auth DeliveryLinkAuth) (*DeliveryOrderInfo, error) { info, _, _, err := s.prepareOrder(orderNo, false, &auth) return info, err } func (s *DeliveryService) GetMerchantOrder(merchantID uint, orderNo string) (*DeliveryOrderInfo, error) { openOrder, order, goodID, err := s.loadMerchantOrder(merchantID, orderNo) if err != nil { return nil, err } info, _, _, err := s.buildDeliveryState(openOrder, order, goodID, false) return info, err } func (s *DeliveryService) Bind(orderNo, gameAccount string, auth DeliveryLinkAuth) (*DeliveryBindResult, error) { return s.bindWithOrder(orderNo, gameAccount, &auth, 0) } func (s *DeliveryService) BindForMerchant(merchantID uint, orderNo, gameAccount string) (*DeliveryBindResult, error) { return s.bindWithOrder(orderNo, gameAccount, nil, merchantID) } func (s *DeliveryService) bindWithOrder(orderNo, gameAccount string, auth *DeliveryLinkAuth, merchantID uint) (*DeliveryBindResult, error) { gameAccount = strings.TrimSpace(gameAccount) if gameAccount == "" { return nil, errors.New("请输入玩家编号") } var ( openOrder *OpenOrderQuery order *model.FulfillmentOrder goodID string err error ) if merchantID > 0 { openOrder, order, goodID, err = s.loadMerchantOrder(merchantID, orderNo) } else { openOrder, order, goodID, err = s.loadLinkedOrder(orderNo, auth) } if err != nil { return nil, err } _, _, _, err = s.buildDeliveryState(openOrder, order, goodID, true) if err != nil { return nil, err } return s.bindGood(gameAccount, goodID) } func (s *DeliveryService) bindGood(gameAccount, goodID string) (*DeliveryBindResult, error) { var out struct { BindUUID string `json:"bind_uuid"` URL string `json:"url"` } if err := s.signProxy("/public/games/bind-account", "POST", map[string]interface{}{ "game_account": gameAccount, "gameAccount": gameAccount, "goodId": goodID, "good_id": goodID, }, &out); err != nil { return nil, err } if out.BindUUID == "" || out.URL == "" { return nil, errors.New("上游未返回绑定二维码") } return &DeliveryBindResult{ BindUUID: out.BindUUID, BindURL: out.URL, QRURL: "https://api.qrserver.com/v1/create-qr-code/?size=240x240&data=" + url.QueryEscape(out.URL), }, nil } func (s *DeliveryService) Submit(orderNo, gameAccount, bindUUID string, auth DeliveryLinkAuth) (*DeliverySubmitResult, error) { return s.submit(orderNo, gameAccount, bindUUID, 0, &auth, 0) } func (s *DeliveryService) SubmitForMerchant(merchantID, apiClientID uint, orderNo, gameAccount, bindUUID string) (*DeliverySubmitResult, error) { return s.submit(orderNo, gameAccount, bindUUID, apiClientID, nil, merchantID) } func (s *DeliveryService) submit(orderNo, gameAccount, bindUUID string, apiClientID uint, auth *DeliveryLinkAuth, merchantID uint) (*DeliverySubmitResult, error) { gameAccount = strings.TrimSpace(gameAccount) bindUUID = strings.TrimSpace(bindUUID) orderNo = strings.TrimSpace(orderNo) if gameAccount == "" || bindUUID == "" { return nil, errors.New("玩家编号和绑定凭证不能为空") } var ( openOrder *OpenOrderQuery order *model.FulfillmentOrder goodID string err error ) if merchantID > 0 { openOrder, order, goodID, err = s.loadMerchantOrder(merchantID, orderNo) } else { openOrder, order, goodID, err = s.loadLinkedOrder(orderNo, auth) } if err != nil { return nil, err } info, _, _, err := s.buildDeliveryState(openOrder, order, goodID, false) if err != nil { return nil, err } if order.FulfillmentStatus == model.FulfillmentStatusProcessing || order.FulfillmentStatus == model.FulfillmentStatusSucceeded || strings.TrimSpace(order.ProviderOrderNo) != "" || strings.Contains(order.ResultData, "\"queue_order_id\"") { if existing := buildExistingDeliverySubmitResult(order); existing != nil { return existing, nil } } if !info.CanShip { reason := info.CannotShipReason if reason == "" { reason = "订单暂不可发货" } return nil, newDeliveryHTTPError(http.StatusBadRequest, reason) } claimed, claimedNow, err := s.claimDeliverySubmission(order.MerchantID, apiClientID, order.OrderNo) if err != nil { return nil, err } if !claimedNow { if existing := buildExistingDeliverySubmitResult(claimed); existing != nil { return existing, nil } return nil, newDeliveryHTTPError(http.StatusConflict, "订单暂时正在履约中,请稍后查询") } boundAccount, err := s.accountBound(bindUUID, goodID) if err != nil { s.markDeliverySubmissionFailed(claimed, apiClientID, err.Error()) return nil, err } if boundGameAccount := stringFromMap(boundAccount, "game_account"); boundGameAccount != "" && boundGameAccount != gameAccount { err := errors.New("玩家编号与绑定的游戏账号不一致,请重新绑定") s.markDeliverySubmissionFailed(claimed, apiClientID, err.Error()) return nil, err } queueOrderID, err := s.createOrderQueue(goodID, order.OrderNo) if err != nil { s.markDeliverySubmissionFailed(claimed, apiClientID, err.Error()) return nil, err } if err := s.patchOrderQueue(queueOrderID, gameAccount, bindUUID); err != nil { s.markDeliverySubmissionFailed(claimed, apiClientID, err.Error()) return nil, err } upstreamOrder, err := s.createUpstreamOrder(queueOrderID, gameAccount, goodID, order.OrderNo) if err != nil { s.markDeliverySubmissionFailed(claimed, apiClientID, err.Error()) return nil, err } providerOrderNo := firstNonEmpty( stringFromMap(upstreamOrder, "order_no"), stringFromMap(upstreamOrder, "order_sn"), stringFromMap(upstreamOrder, "_id"), stringFromMap(upstreamOrder, "id"), queueOrderID, ) resultData := map[string]interface{}{ "source": "delivery_proxy", "queue_order_id": queueOrderID, "provider_order_no": providerOrderNo, "game_uid": gameAccount, "role_name": stringFromMap(boundAccount, "game_account_role_name"), "game_channel": gameChannelText(boundAccount), "upstream_order": upstreamOrder, } updated, err := s.fulfillment.UpdateFulfillment(FulfillmentUpdateInput{ MerchantID: order.MerchantID, APIClientID: apiClientID, OrderNo: order.OrderNo, Status: "processing", ProviderOrderNo: providerOrderNo, ResultData: resultData, }) if err != nil { return nil, err } return &DeliverySubmitResult{ OrderNo: updated.OrderNo, Status: updated.FulfillmentStatus, Message: "已提交上游发货,等待发货结果回传", ProviderOrderNo: providerOrderNo, GameAccount: boundAccount, UpstreamOrder: upstreamOrder, }, nil } func (s *DeliveryService) claimDeliverySubmission(merchantID, apiClientID uint, orderNo string) (*model.FulfillmentOrder, bool, error) { var updated model.FulfillmentOrder claimedNow := false err := s.fulfillment.db.Transaction(func(tx *gorm.DB) error { var order model.FulfillmentOrder if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("merchant_id = ? AND order_no = ?", merchantID, orderNo). First(&order).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("订单不存在") } return err } if order.FulfillmentStatus == model.FulfillmentStatusProcessing || order.FulfillmentStatus == model.FulfillmentStatusSucceeded { updated = order return nil } if order.FulfillmentStatus != model.FulfillmentStatusPending && order.FulfillmentStatus != model.FulfillmentStatusFailed { return newDeliveryHTTPError(http.StatusConflict, "订单暂不可发货") } raw, _ := json.Marshal(map[string]interface{}{ "source": "delivery_proxy", "submit_started_at": timeutil.FormatAPITime(time.Now()), "provider_order_stage": "claimed", }) if err := tx.Model(&order).Updates(map[string]interface{}{ "fulfillment_status": model.FulfillmentStatusProcessing, "result_data": string(raw), "failure_reason": "", }).Error; err != nil { return err } claimedNow = true if err := tx.First(&updated, order.ID).Error; err != nil { return err } apiClientIDPtr := optionalUint(apiClientID) if err := writeAudit(tx, &merchantID, nil, apiClientIDPtr, "delivery.submit.claim", "fulfillment_order", order.OrderNo, nil); err != nil { return err } if s.fulfillment.callbacks != nil { if err := s.fulfillment.callbacks.Enqueue(tx, merchantID, "order.fulfillment.updated", orderCallbackData(&updated)); err != nil { return err } } return nil }) if err != nil { return nil, false, err } return &updated, claimedNow, nil } func (s *DeliveryService) markDeliverySubmissionFailed(order *model.FulfillmentOrder, apiClientID uint, reason string) { if order == nil { return } _, _ = s.fulfillment.UpdateFulfillment(FulfillmentUpdateInput{ MerchantID: order.MerchantID, APIClientID: apiClientID, OrderNo: order.OrderNo, Status: model.FulfillmentStatusFailed, FailureReason: reason, ResultData: map[string]interface{}{ "source": "delivery_proxy", "fail_reason": reason, }, }) } func (s *DeliveryService) loadLinkedOrder(orderNo string, auth *DeliveryLinkAuth) (*OpenOrderQuery, *model.FulfillmentOrder, string, error) { openOrder, err := s.fulfillment.QueryOpenOrder(orderNo) if err != nil { return nil, nil, "", err } order, err := s.fulfillment.GetByOrderNo(orderNo) if err != nil { return nil, nil, "", err } if auth != nil { if err := s.authorizeDeliveryLink(order, *auth); err != nil { return nil, nil, "", err } } goodID := "" if openOrder.Product != nil { goodID = deliveryGoodID(s.channel, openOrder.Product.SKU) } return openOrder, order, goodID, nil } func (s *DeliveryService) loadMerchantOrder(merchantID uint, orderNo string) (*OpenOrderQuery, *model.FulfillmentOrder, string, error) { if merchantID == 0 { return nil, nil, "", errors.New("无效的商户") } openOrder, err := s.fulfillment.QueryOpenOrder(orderNo) if err != nil { return nil, nil, "", err } order, err := s.fulfillment.GetOrder(merchantID, orderNo) if err != nil { return nil, nil, "", err } goodID := "" if openOrder.Product != nil { goodID = deliveryGoodID(s.channel, openOrder.Product.SKU) } return openOrder, order, goodID, nil } func (s *DeliveryService) buildDeliveryState(openOrder *OpenOrderQuery, order *model.FulfillmentOrder, goodID string, requireCanShip bool) (*DeliveryOrderInfo, *model.FulfillmentOrder, string, error) { if openOrder == nil || order == nil { return nil, nil, "", errors.New("订单不存在") } canShip, reason := CanFulfill(order) if !openOrder.CanShip { reason = openOrder.CannotShipReason canShip = false } if !canShip && reason == "" { reason = "订单暂不可发货" } if openOrder.Product == nil || openOrder.Product.SKU == "" { canShip = false reason = "订单缺少商品 SKU" } else if goodID == "" { goodID = deliveryGoodID(s.channel, openOrder.Product.SKU) if goodID == "" { canShip = false reason = "未找到对应的上游商品配置" } } if requireCanShip && !canShip { return nil, nil, "", newDeliveryHTTPError(http.StatusBadRequest, reason) } var good map[string]interface{} if goodID != "" { good, _ = s.goodsDetail(goodID) } product := buildDeliveryProduct(openOrder.Product, good) info := &DeliveryOrderInfo{ OrderNo: openOrder.OrderNo, Status: openOrder.Status, CanShip: canShip, CannotShipReason: reason, Product: product, BuyerName: openOrder.BuyerName, Amount: openOrder.Amount, CreatedAt: openOrder.CreatedAt, ShippedAt: openOrder.ShippedAt, ShipFailReason: openOrder.ShipFailReason, GameChannel: openOrder.GameChannel, GameUID: openOrder.GameUID, RoleName: openOrder.RoleName, PayScore: openOrder.PayScore, Good: good, } return info, order, goodID, nil } func buildExistingDeliverySubmitResult(order *model.FulfillmentOrder) *DeliverySubmitResult { if order == nil { return nil } switch order.FulfillmentStatus { case model.FulfillmentStatusProcessing, model.FulfillmentStatusSucceeded: 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 := "订单暂时正在履约中,请稍后查询" if order.FulfillmentStatus == model.FulfillmentStatusSucceeded { message = "订单已履约成功" } return &DeliverySubmitResult{ OrderNo: order.OrderNo, Status: order.FulfillmentStatus, Message: message, ProviderOrderNo: providerOrderNo, UpstreamOrder: resultData, } } func optionalUint(value uint) *uint { if value == 0 { return nil } out := value return &out } func (s *DeliveryService) GetOrCreateDeliveryLink(merchantID uint, orderNo string) (*DeliveryLinkResult, error) { orderNo = strings.TrimSpace(orderNo) if orderNo == "" { return nil, errors.New("订单号不能为空") } order, err := s.fulfillment.GetOrder(merchantID, orderNo) if err != nil { return nil, err } now := time.Now().UTC() if order.DeliveryLinkRevokedAt != nil { return nil, newDeliveryHTTPError(http.StatusForbidden, "发货链接已作废") } if order.DeliveryLinkExpiresAt != nil && order.DeliveryLinkExpiresAt.After(now) { return s.buildDeliveryLinkResult(order.OrderNo, order.DeliveryLinkExpiresAt.UTC().Truncate(time.Second)), nil } canShip, reason := CanFulfill(order) if !canShip { if reason == "" { reason = "订单暂不可发货" } return nil, newDeliveryHTTPError(http.StatusBadRequest, reason) } expiresAt := now.Add(s.linkTTL).UTC().Truncate(time.Second) if err := s.fulfillment.db.Model(&model.FulfillmentOrder{}). Where("id = ?", order.ID). Updates(map[string]interface{}{ "delivery_link_expires_at": expiresAt, "delivery_link_revoked_at": nil, }).Error; err != nil { return nil, err } return s.buildDeliveryLinkResult(order.OrderNo, expiresAt), nil } func (s *DeliveryService) RevokeDeliveryLink(merchantID uint, orderNo string) error { orderNo = strings.TrimSpace(orderNo) if orderNo == "" { return errors.New("订单号不能为空") } order, err := s.fulfillment.GetOrder(merchantID, orderNo) if err != nil { return err } now := time.Now().UTC().Truncate(time.Second) return s.fulfillment.db.Model(&model.FulfillmentOrder{}). Where("id = ?", order.ID). Updates(map[string]interface{}{ "delivery_link_revoked_at": now, }).Error } func (s *DeliveryService) RestoreDeliveryLink(merchantID uint, orderNo string) (*DeliveryLinkResult, error) { orderNo = strings.TrimSpace(orderNo) if orderNo == "" { return nil, errors.New("订单号不能为空") } order, err := s.fulfillment.GetOrder(merchantID, orderNo) if err != nil { return nil, err } canShip, reason := CanFulfill(order) if !canShip { if reason == "" { reason = "订单暂不可发货" } return nil, newDeliveryHTTPError(http.StatusBadRequest, reason) } expiresAt := time.Now().UTC().Add(s.linkTTL).Truncate(time.Second) if order.DeliveryLinkExpiresAt != nil && expiresAt.Equal(order.DeliveryLinkExpiresAt.UTC().Truncate(time.Second)) { expiresAt = expiresAt.Add(time.Second) } if err := s.fulfillment.db.Model(&model.FulfillmentOrder{}). Where("id = ?", order.ID). Updates(map[string]interface{}{ "delivery_link_expires_at": expiresAt, "delivery_link_revoked_at": nil, }).Error; err != nil { return nil, err } return s.buildDeliveryLinkResult(order.OrderNo, expiresAt), nil } func (s *DeliveryService) prepareOrder(orderNo string, requireCanShip bool, auth *DeliveryLinkAuth) (*DeliveryOrderInfo, *model.FulfillmentOrder, string, error) { orderNo = strings.TrimSpace(orderNo) if orderNo == "" { return nil, nil, "", errors.New("订单号不能为空") } openOrder, err := s.fulfillment.QueryOpenOrder(orderNo) if err != nil { return nil, nil, "", err } order, err := s.fulfillment.GetByOrderNo(orderNo) if err != nil { return nil, nil, "", err } if auth != nil { if err := s.authorizeDeliveryLink(order, *auth); err != nil { return nil, nil, "", err } } canShip, reason := CanFulfill(order) if !openOrder.CanShip { reason = openOrder.CannotShipReason canShip = false } if !canShip && reason == "" { reason = "订单暂不可发货" } goodID := "" if openOrder.Product == nil || openOrder.Product.SKU == "" { canShip = false reason = "订单缺少商品 SKU" } else { goodID = deliveryGoodID(s.channel, openOrder.Product.SKU) if goodID == "" { canShip = false reason = "未找到对应的上游商品配置" } } if requireCanShip && !canShip { return nil, nil, "", newDeliveryHTTPError(http.StatusBadRequest, reason) } var good map[string]interface{} if goodID != "" { good, _ = s.goodsDetail(goodID) } product := buildDeliveryProduct(openOrder.Product, good) info := &DeliveryOrderInfo{ OrderNo: openOrder.OrderNo, Status: openOrder.Status, CanShip: canShip, CannotShipReason: reason, Product: product, BuyerName: openOrder.BuyerName, Amount: openOrder.Amount, CreatedAt: openOrder.CreatedAt, ShippedAt: openOrder.ShippedAt, ShipFailReason: openOrder.ShipFailReason, GameChannel: openOrder.GameChannel, GameUID: openOrder.GameUID, RoleName: openOrder.RoleName, PayScore: openOrder.PayScore, Good: good, } 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) *DeliveryLinkResult { expiresAt = expiresAt.UTC() exp := expiresAt.Unix() sign := s.signDeliveryLink(orderNo, exp) return &DeliveryLinkResult{ OrderNo: orderNo, DeliveryURL: s.buildDeliveryURL(orderNo, exp, sign), ExpiresAt: expiresAt, Exp: exp, Sign: sign, } } func (s *DeliveryService) buildDeliveryURL(orderNo string, exp int64, sign string) string { path := fmt.Sprintf("/delivery/%s/%s?exp=%d&sign=%s", url.PathEscape(s.channel), url.PathEscape(orderNo), exp, url.QueryEscape(sign)) if s.linkBaseURL == "" { return path } return s.linkBaseURL + 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 "" } return map[string]string{ "suit_alan_walker": "682ef3c5a8f40c4234c59f47", "suit_shadow_gothic": "682ef39ca8f40c4234c59f36", "top_black_elite_trainer": "682ef39ca8f40c4234c59f37", "m416_hamster_gray": "682ef3c5a8f40c4234c59f52", "bag_cute_bear": "682ef3c5a8f40c4234c59f4a", "suit_dual_fluffy": "682ef3c5a8f40c4234c59f49", "suit_pink_sheep": "682ef3c5a8f40c4234c59f4d", "suit_first_peach": "682ef3c5a8f40c4234c59f48", "suit_romantic_destiny": "682ef39ca8f40c4234c59f38", "pack_western_cowboy": "682ef3c5a8f40c4234c59f4c", "smoke_pink_sheep": "682ef3c5a8f40c4234c59f50", "frag_pink_sheep": "682ef3c5a8f40c4234c59f51", "suit_hamster_gray": "682ef3c5a8f40c4234c59f53", "suit_cute_bear": "682ef3c5a8f40c4234c59f4b", "bag_pink_sheep": "682ef3c5a8f40c4234c59f4f", "helmet_pink_sheep": "682ef3c5a8f40c4234c59f4e", "bag_hamster_gray": "682ef3c5a8f40c4234c59f55", "helmet_hamster_gray": "682ef3c5a8f40c4234c59f54", "suit_western_mystery": "682ef39ca8f40c4234c59f35", "helmet_panda_treasure": "682ef39ca8f40c4234c59f39", "suit_panda_round": "682ef39ca8f40c4234c59f3a", "suit_panda_tuan": "682ef39ca8f40c4234c59f3b", "pack_lava_ranger": "682ef39ca8f40c4234c59f3c", "suit_sand_dancer": "682ef39ca8f40c4234c59f3d", "pack_star_roam_outfit": "682ef39ca8f40c4234c59f3e", "pack_star_roam_weapon": "682ef39ca8f40c4234c59f3f", "suit_cloud_bear": "682ef39ca8f40c4234c59f40", }[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] }