接入无感发货流程
This commit is contained in:
@@ -54,6 +54,7 @@ func main() {
|
||||
callbackSvc := service.NewCallbackService(db, codec)
|
||||
fulfillmentSvc := service.NewFulfillmentService(db, callbackSvc)
|
||||
merchantSvc := service.NewMerchantService(db, codec, tenantSvc)
|
||||
deliverySvc := service.NewDeliveryService(fulfillmentSvc, cfg.DeliveryBFFBaseURL, cfg.DeliveryChannel)
|
||||
|
||||
if err := authSvc.EnsureAdmin(); err != nil {
|
||||
log.Fatalf("ensure admin: %v", err)
|
||||
@@ -64,6 +65,7 @@ func main() {
|
||||
h := &router.Handlers{
|
||||
Auth: handler.NewAuthHandler(authSvc),
|
||||
Dashboard: handler.NewDashboardHandler(fulfillmentSvc),
|
||||
Delivery: handler.NewDeliveryHandler(deliverySvc),
|
||||
User: handler.NewUserHandler(userSvc),
|
||||
Open: handler.NewOpenV1Handler(merchantSvc, fulfillmentSvc),
|
||||
SourceOpen: handler.NewOpenHandler(fulfillmentSvc),
|
||||
|
||||
@@ -26,6 +26,12 @@ type Config struct {
|
||||
OpenAPIDebug bool
|
||||
// LogFile 日志文件路径;非空时同时写控制台与文件
|
||||
LogFile string
|
||||
// DeliveryBaseURL 用户侧发货页面基础地址;为空时前端可自行按当前域名拼接。
|
||||
DeliveryBaseURL string
|
||||
// DeliveryBFFBaseURL 上游测试发货 BFF 地址。
|
||||
DeliveryBFFBaseURL string
|
||||
// DeliveryChannel 上游测试发货渠道前缀,如 dlc。
|
||||
DeliveryChannel string
|
||||
}
|
||||
|
||||
// Load 加载配置:先尝试读取 .env,再读系统环境变量(已存在的系统环境变量优先级更高)
|
||||
@@ -35,16 +41,19 @@ func Load() *Config {
|
||||
// OPEN_API_DEBUG 优先;未设置时 debug 模式默认开启
|
||||
debugOpen := getEnvBool("OPEN_API_DEBUG", mode == "debug" || mode == "")
|
||||
return &Config{
|
||||
Port: getEnv("PORT", "8080"),
|
||||
JWTSecret: getEnv("JWT_SECRET", "affiliate-dash-dev-secret-change-me"),
|
||||
DatabaseURL: getEnv("DATABASE_URL", "postgres://affiliate:affiliate_dev_password@127.0.0.1:5432/affiliate_dash?sslmode=disable"),
|
||||
Mode: mode,
|
||||
DataEncryptionKey: getEnv("DATA_ENCRYPTION_KEY", getEnv("JWT_SECRET", "affiliate-dash-dev-secret-change-me")),
|
||||
OpenAPIKey: getEnv("OPEN_API_KEY", "sk_source_dev_key_change_me"),
|
||||
OpenAPISecret: getEnv("OPEN_API_SECRET", "sk_source_dev_secret_change_me"),
|
||||
OpenSignSkew: int64(getEnvInt("OPEN_SIGN_SKEW", 300)),
|
||||
OpenAPIDebug: debugOpen,
|
||||
LogFile: getEnv("LOG_FILE", "logs/app.log"),
|
||||
Port: getEnv("PORT", "8080"),
|
||||
JWTSecret: getEnv("JWT_SECRET", "affiliate-dash-dev-secret-change-me"),
|
||||
DatabaseURL: getEnv("DATABASE_URL", "postgres://affiliate:affiliate_dev_password@127.0.0.1:5432/affiliate_dash?sslmode=disable"),
|
||||
Mode: mode,
|
||||
DataEncryptionKey: getEnv("DATA_ENCRYPTION_KEY", getEnv("JWT_SECRET", "affiliate-dash-dev-secret-change-me")),
|
||||
OpenAPIKey: getEnv("OPEN_API_KEY", "sk_source_dev_key_change_me"),
|
||||
OpenAPISecret: getEnv("OPEN_API_SECRET", "sk_source_dev_secret_change_me"),
|
||||
OpenSignSkew: int64(getEnvInt("OPEN_SIGN_SKEW", 300)),
|
||||
OpenAPIDebug: debugOpen,
|
||||
LogFile: getEnv("LOG_FILE", "logs/app.log"),
|
||||
DeliveryBaseURL: getEnv("DELIVERY_BASE_URL", ""),
|
||||
DeliveryBFFBaseURL: getEnv("DELIVERY_BFF_BASE_URL", "https://www.jxya.top/bff-stg"),
|
||||
DeliveryChannel: getEnv("DELIVERY_CHANNEL", "dlc"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"affiliate_dash/internal/pkg/response"
|
||||
"affiliate_dash/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DeliveryHandler 提供用户侧公开发货页面使用的接口。
|
||||
type DeliveryHandler struct {
|
||||
deliverySvc *service.DeliveryService
|
||||
}
|
||||
|
||||
func NewDeliveryHandler(deliverySvc *service.DeliveryService) *DeliveryHandler {
|
||||
return &DeliveryHandler{deliverySvc: deliverySvc}
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) GetOrder(c *gin.Context) {
|
||||
order, err := h.deliverySvc.GetOrder(c.Param("order_no"))
|
||||
if err != nil {
|
||||
writeDeliveryError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, order)
|
||||
}
|
||||
|
||||
type deliveryBindReq struct {
|
||||
GameAccount string `json:"game_account" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) Bind(c *gin.Context) {
|
||||
var req deliveryBindReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "请输入玩家编号")
|
||||
return
|
||||
}
|
||||
result, err := h.deliverySvc.Bind(c.Param("order_no"), req.GameAccount)
|
||||
if err != nil {
|
||||
writeDeliveryError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
type deliverySubmitReq struct {
|
||||
GameAccount string `json:"game_account" binding:"required"`
|
||||
BindUUID string `json:"bind_uuid" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *DeliveryHandler) Submit(c *gin.Context) {
|
||||
var req deliverySubmitReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "玩家编号和绑定凭证不能为空")
|
||||
return
|
||||
}
|
||||
result, err := h.deliverySvc.Submit(c.Param("order_no"), req.GameAccount, req.BindUUID)
|
||||
if err != nil {
|
||||
writeDeliveryError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
func writeDeliveryError(c *gin.Context, err error) {
|
||||
msg := strings.TrimSpace(err.Error())
|
||||
if msg == "订单不存在" {
|
||||
response.NotFound(c, msg)
|
||||
return
|
||||
}
|
||||
response.BadRequest(c, msg)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
type Handlers struct {
|
||||
Auth *handler.AuthHandler
|
||||
Dashboard *handler.DashboardHandler
|
||||
Delivery *handler.DeliveryHandler
|
||||
User *handler.UserHandler
|
||||
Open *handler.OpenV1Handler
|
||||
SourceOpen *handler.OpenHandler
|
||||
@@ -48,6 +49,14 @@ func Setup(h *Handlers) *gin.Engine {
|
||||
{
|
||||
api.POST("/auth/login", h.Auth.Login)
|
||||
|
||||
// 用户侧公开发货页接口:商户把发货链接给用户,用户只填写游戏 UID。
|
||||
delivery := api.Group("/delivery/v1")
|
||||
{
|
||||
delivery.GET("/orders/:order_no", h.Delivery.GetOrder)
|
||||
delivery.POST("/orders/:order_no/bind", h.Delivery.Bind)
|
||||
delivery.POST("/orders/:order_no/submit", h.Delivery.Submit)
|
||||
}
|
||||
|
||||
// 源头侧接口:上游发货平台查询订单并回传发货结果。
|
||||
sourceOpen := api.Group("/open/v1")
|
||||
sourceOpen.Use(middleware.SourceOpenAuth(middleware.SourceOpenAuthConfig{
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultDeliveryBFFBaseURL = "https://www.jxya.top/bff-stg"
|
||||
|
||||
type DeliveryService struct {
|
||||
fulfillment *FulfillmentService
|
||||
bffBaseURL string
|
||||
channel string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewDeliveryService(fulfillment *FulfillmentService, bffBaseURL, channel string) *DeliveryService {
|
||||
bffBaseURL = strings.TrimRight(strings.TrimSpace(bffBaseURL), "/")
|
||||
if bffBaseURL == "" {
|
||||
bffBaseURL = defaultDeliveryBFFBaseURL
|
||||
}
|
||||
channel = strings.TrimSpace(channel)
|
||||
if channel == "" {
|
||||
channel = "dlc"
|
||||
}
|
||||
return &DeliveryService{
|
||||
fulfillment: fulfillment,
|
||||
bffBaseURL: bffBaseURL,
|
||||
channel: channel,
|
||||
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 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"`
|
||||
}
|
||||
|
||||
func (s *DeliveryService) GetOrder(orderNo string) (*DeliveryOrderInfo, error) {
|
||||
info, _, _, err := s.prepareOrder(orderNo, false)
|
||||
return info, err
|
||||
}
|
||||
|
||||
func (s *DeliveryService) Bind(orderNo, gameAccount string) (*DeliveryBindResult, error) {
|
||||
gameAccount = strings.TrimSpace(gameAccount)
|
||||
if gameAccount == "" {
|
||||
return nil, errors.New("请输入玩家编号")
|
||||
}
|
||||
_, _, goodID, err := s.prepareOrder(orderNo, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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) (*DeliverySubmitResult, error) {
|
||||
gameAccount = strings.TrimSpace(gameAccount)
|
||||
bindUUID = strings.TrimSpace(bindUUID)
|
||||
orderNo = strings.TrimSpace(orderNo)
|
||||
if gameAccount == "" || bindUUID == "" {
|
||||
return nil, errors.New("玩家编号和绑定凭证不能为空")
|
||||
}
|
||||
_, order, goodID, err := s.prepareOrder(orderNo, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
boundAccount, err := s.accountBound(bindUUID, goodID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if boundGameAccount := stringFromMap(boundAccount, "game_account"); boundGameAccount != "" && boundGameAccount != gameAccount {
|
||||
return nil, errors.New("玩家编号与绑定的游戏账号不一致,请重新绑定")
|
||||
}
|
||||
queueOrderID, err := s.createOrderQueue(goodID, order.OrderNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.patchOrderQueue(queueOrderID, gameAccount, bindUUID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
upstreamOrder, err := s.createUpstreamOrder(queueOrderID, gameAccount, goodID, order.OrderNo)
|
||||
if err != nil {
|
||||
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: 0,
|
||||
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) prepareOrder(orderNo string, requireCanShip bool) (*DeliveryOrderInfo, *FulfillmentOrderSnapshot, 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
|
||||
}
|
||||
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, "", errors.New(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, &FulfillmentOrderSnapshot{
|
||||
ID: order.ID,
|
||||
MerchantID: order.MerchantID,
|
||||
OrderNo: order.OrderNo,
|
||||
}, goodID, 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
|
||||
}
|
||||
|
||||
type FulfillmentOrderSnapshot struct {
|
||||
ID uint
|
||||
MerchantID uint
|
||||
OrderNo string
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
Reference in New Issue
Block a user