feat: 优化发货交付页面与凭据生成,支持一键复制结果图片及官方商品图片本地缓存

- 交付界面重构:隐藏内部订单号、流水号与发货渠道,展示聚焦于买家游戏UID、角色名与交付时间
- 发货结果凭据生成:采用 Canvas 生成高清发货存证图,优化存证说明文案与自适应排版
- 一键复制图片:支持直接将结果凭证图片写入系统剪贴板,方便客服在聊天窗口快捷粘贴
- 官方图片本地化缓存:全量下载乐玩商品官方主图并建立本地持久化路由,规避外部源站跨域及破损问题
- 修复乐玩渠道角色UID提取与履约状态幂等回填逻辑
This commit is contained in:
yml2213
2026-09-14 17:19:12 +08:00
parent f7f48d0feb
commit 5f642eb7a6
34 changed files with 1428 additions and 66 deletions
+92
View File
@@ -0,0 +1,92 @@
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"
"affiliate_dash/internal/config"
"affiliate_dash/internal/pkg/lewan"
)
func main() {
cfg := config.Load()
client := lewan.NewClient(lewan.Config{
BaseURL: cfg.LewanBaseURL,
AccessKey: cfg.LewanAccessKey,
SecretKey: cfg.LewanSecretKey,
ApplicationID: cfg.LewanApplicationID,
}, nil)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
list, err := client.ListProducts(ctx, 1, 100, "")
if err != nil {
fmt.Printf("ListProducts error: %v\n", err)
return
}
fmt.Printf("Found %d products (total: %d)\n", len(list.Items), list.Total)
uploadDir := cfg.UploadDir
if uploadDir == "" {
uploadDir = "data/uploads"
}
cacheDir := filepath.Join(uploadDir, "products")
_ = os.MkdirAll(cacheDir, 0755)
httpClient := &http.Client{Timeout: 15 * time.Second}
for i, item := range list.Items {
fmt.Printf("[%d/%d] ID: %s, Name: %s, Thumb: %s\n", i+1, len(list.Items), item.ID, item.Name, item.ThumbnailUrl)
if item.ThumbnailUrl == "" {
continue
}
// 存储为 item.ID + 原扩展名
ext := filepath.Ext(item.ThumbnailUrl)
if ext == "" || len(ext) > 5 {
ext = ".webp"
}
destPath := filepath.Join(cacheDir, item.ID+ext)
// 同时也用 url hash 存一个软链或备份,以便 proxy 按 url 精准命中
urlHash := sha256.Sum256([]byte(item.ThumbnailUrl))
hashPath := filepath.Join(cacheDir, hex.EncodeToString(urlHash[:])+ext)
if _, err := os.Stat(destPath); err == nil {
fmt.Printf(" -> 已存在缓存: %s\n", destPath)
continue
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, item.ThumbnailUrl, nil)
if err != nil {
fmt.Printf(" -> 构建请求失败: %v\n", err)
continue
}
resp, err := httpClient.Do(req)
if err != nil {
fmt.Printf(" -> 下载失败: %v\n", err)
continue
}
if resp.StatusCode != http.StatusOK {
fmt.Printf(" -> 状态码错误: %d\n", resp.StatusCode)
resp.Body.Close()
continue
}
data, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Printf(" -> 读取失败: %v\n", err)
continue
}
_ = os.WriteFile(destPath, data, 0644)
_ = os.WriteFile(hashPath, data, 0644)
fmt.Printf(" -> 成功缓存: %s (大小: %d 字节)\n", destPath, len(data))
}
}
+1
View File
@@ -96,6 +96,7 @@ func main() {
lewanProvider := service.NewLewanProvider(lewanClient)
deliverySvc.RegisterProvider(lewanProvider)
fulfillmentSvc.SetUpstreamCancel(deliverySvc.CancelUpstream)
fulfillmentSvc.SetUpstreamSync(deliverySvc.SyncDeliveringOrder)
openlog.Init(cfg.OpenAPIDebug)
+89
View File
@@ -1,8 +1,15 @@
package handler
import (
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"affiliate_dash/internal/pkg/response"
"affiliate_dash/internal/service"
@@ -65,6 +72,88 @@ func (h *DeliveryHandler) Submit(c *gin.Context) {
response.OK(c, result)
}
// ImageProxy 代理图片并支持本地持久化缓存,杜绝跨域污染和远端加载失败。
func (h *DeliveryHandler) ImageProxy(c *gin.Context) {
rawURL := strings.TrimSpace(c.Query("url"))
if rawURL == "" || (!strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://")) {
response.BadRequest(c, "无效的图片地址")
return
}
// 1. 优先检查本地官方商品图片缓存
for _, base := range []string{"data/uploads/products", "../data/uploads/products", "uploads/products"} {
if strings.Contains(rawURL, "/products/") {
parts := strings.Split(rawURL, "/products/")
if len(parts) > 1 {
sub := strings.Split(parts[1], "/")
if len(sub) > 0 && sub[0] != "" {
localPath := filepath.Join(base, sub[0]+".webp")
if info, err := os.Stat(localPath); err == nil && info.Size() > 0 {
c.Header("Cache-Control", "public, max-age=2592000, immutable")
c.File(localPath)
return
}
}
}
}
// 按 URL SHA256 哈希查本地缓存
hash := sha256.Sum256([]byte(rawURL))
hashPath := filepath.Join(base, hex.EncodeToString(hash[:])+".webp")
if info, err := os.Stat(hashPath); err == nil && info.Size() > 0 {
c.Header("Cache-Control", "public, max-age=2592000, immutable")
c.File(hashPath)
return
}
}
// 2. 本地无缓存时请求远端
req, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, rawURL, nil)
if err != nil {
response.BadRequest(c, "构建请求失败")
return
}
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
response.Fail(c, http.StatusBadGateway, http.StatusBadGateway, "图片下载失败")
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
response.Fail(c, resp.StatusCode, resp.StatusCode, "远端图片响应异常")
return
}
data, err := io.ReadAll(resp.Body)
if err != nil {
response.Fail(c, http.StatusBadGateway, http.StatusBadGateway, "读取图片内容失败")
return
}
// 3. 异步/同步回写到本地磁盘缓存
cacheDir := "data/uploads/products"
_ = os.MkdirAll(cacheDir, 0755)
if strings.Contains(rawURL, "/products/") {
parts := strings.Split(rawURL, "/products/")
if len(parts) > 1 {
sub := strings.Split(parts[1], "/")
if len(sub) > 0 && sub[0] != "" {
_ = os.WriteFile(filepath.Join(cacheDir, sub[0]+".webp"), data, 0644)
}
}
}
hash := sha256.Sum256([]byte(rawURL))
_ = os.WriteFile(filepath.Join(cacheDir, hex.EncodeToString(hash[:])+".webp"), data, 0644)
contentType := resp.Header.Get("Content-Type")
if contentType == "" {
contentType = "image/webp"
}
c.Header("Cache-Control", "public, max-age=2592000, immutable")
c.Data(http.StatusOK, contentType, data)
}
func parseDeliveryAuth(c *gin.Context) service.DeliveryLinkAuth {
exp := int64(0)
if value := strings.TrimSpace(c.Query("exp")); value != "" {
@@ -118,3 +118,38 @@ func (h *PlatformLewanHandler) SyncLewanProduct(c *gin.Context) {
}
response.WithStatus(c, http.StatusCreated, item)
}
type batchSyncLewanReq struct {
Products []syncLewanProductReq `json:"products" binding:"required,min=1"`
}
// BatchSyncLewanProducts POST /api/platform/lewan/batch-sync-catalog
// 批量同步/添加乐玩商品到自营商品目录
func (h *PlatformLewanHandler) BatchSyncLewanProducts(c *gin.Context) {
var req batchSyncLewanReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误:products 列表不能为空")
return
}
actorUserID := middleware.GetUserID(c)
inputs := make([]service.SyncLewanProductInput, len(req.Products))
for i, p := range req.Products {
inputs[i] = service.SyncLewanProductInput{
LewanProductID: p.LewanProductID,
Name: p.Name,
Category: p.Category,
SKU: p.SKU,
Price: p.Price,
DeliveryType: p.DeliveryType,
}
}
count, err := h.merchantSvc.BatchSyncLewanProductsToCatalog(inputs, actorUserID)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, gin.H{"synced_count": count})
}
+3
View File
@@ -132,6 +132,8 @@ type LewanOrder struct {
Status string `json:"status"` // WAITING_BINDING, QUEUED, PROCESSING, RECONCILING, SUCCEEDED, PARTIAL_SUCCESS, FAILED, CANCELLED
BindingDeadline *string `json:"bindingDeadline"`
BindingAction *BindingAction `json:"bindingAction,omitempty"`
Binding *BindingInfo `json:"binding,omitempty"`
CompletedAt *string `json:"completedAt,omitempty"`
}
// CreateOrderResult 下单响应
@@ -148,6 +150,7 @@ type BindingInfo struct {
Matched bool `json:"matched"`
RoleID string `json:"roleId"`
RoleName string `json:"roleName"`
RoleArea string `json:"roleArea,omitempty"`
ServerName string `json:"serverName"`
}
+2
View File
@@ -112,6 +112,7 @@ func Setup(h *Handlers) *gin.Engine {
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)
delivery.GET("/image-proxy", h.Delivery.ImageProxy)
}
// 源头侧接口:上游发货平台查询订单并回传发货结果。
@@ -228,6 +229,7 @@ func Setup(h *Handlers) *gin.Engine {
if h.PlatformLewan != nil {
admin.GET("/platform/lewan/products", h.PlatformLewan.ListLewanProducts)
admin.POST("/platform/lewan/sync-catalog", h.PlatformLewan.SyncLewanProduct)
admin.POST("/platform/lewan/batch-sync-catalog", h.PlatformLewan.BatchSyncLewanProducts)
admin.PUT("/platform/products/:id/routing", h.PlatformLewan.UpdateProductRouting)
}
}
+106 -1
View File
@@ -135,6 +135,77 @@ func (s *DeliveryService) CancelUpstream(order *model.FulfillmentOrder) error {
return nil
}
// SyncDeliveringOrder 主动向上游渠道查询处于 delivering 状态的订单结果,以补充 Webhook 丢失或延迟。
func (s *DeliveryService) SyncDeliveringOrder(ctx context.Context, order *model.FulfillmentOrder) (bool, error) {
if order == nil || s.router == nil || order.OrderStatus != model.OrderStatusDelivering {
return false, nil
}
if ctx == nil {
ctx = context.Background()
}
providerName := resultDataString(order.ResultData, "provider")
if providerName == "" && (resultDataString(order.ResultData, "lewan_order_id") != "" || resultDataString(order.ResultData, "lewan_order_no") != "" || strings.HasPrefix(order.ProviderOrderNo, "OMU")) {
providerName = ProviderLewan
}
providers := make([]FulfillmentProvider, 0, 2)
if providerName != "" {
if p := s.router.GetProvider(providerName); p != nil {
providers = append(providers, p)
}
}
if len(providers) == 0 {
resolved, _ := s.router.ResolveProviders(order)
providers = resolved
}
for _, p := range providers {
res, err := p.SyncStatus(ctx, order)
if err != nil || res == nil {
continue
}
if res.Status == "success" || res.Status == "failed" {
input := ShipNotifyInput{
OrderNo: order.OrderNo,
ShipStatus: res.Status,
ProviderOrderNo: res.ProviderOrderNo,
FailReason: res.FailReason,
RawPayload: res.RawPayload,
}
if res.GameUID != "" {
input.GameUID = &res.GameUID
}
if res.RoleName != "" {
input.RoleName = &res.RoleName
}
if res.RoleArea != "" {
input.GameChannel = &res.RoleArea
}
if _, err := s.fulfillment.HandleShipNotify(input); err != nil {
return false, err
}
return true, nil
}
// 如果仍在发货中,且获得了游戏UID/角色名/区服,补充写入 ResultData
patch := map[string]interface{}{}
if res.GameUID != "" && resultDataString(order.ResultData, "game_uid") == "" {
patch["game_uid"] = res.GameUID
}
if res.RoleName != "" && resultDataString(order.ResultData, "role_name") == "" {
patch["role_name"] = res.RoleName
}
if res.RoleArea != "" && resultDataString(order.ResultData, "game_channel") == "" {
patch["game_channel"] = res.RoleArea
}
if len(patch) > 0 {
_ = s.persistOrderResultPatch(order, patch)
}
return false, nil
}
return false, nil
}
// persistOrderResultPatch 合并写入订单 result_data,不改变发货 stage。
func (s *DeliveryService) persistOrderResultPatch(order *model.FulfillmentOrder, patch map[string]interface{}) error {
if order == nil || len(patch) == 0 {
@@ -181,6 +252,8 @@ type DeliveryOrderInfo struct {
Product *DeliveryProduct `json:"product,omitempty"`
BuyerName string `json:"buyer_name"`
Amount int64 `json:"amount"`
ProviderOrderNo string `json:"provider_order_no,omitempty"`
ProviderName string `json:"provider_name,omitempty"`
CreatedAt time.Time `json:"created_at"`
ShippedAt *time.Time `json:"shipped_at"`
ShipFailReason string `json:"ship_fail_reason,omitempty"`
@@ -633,8 +706,14 @@ func (s *DeliveryService) loadMerchantOrder(merchantID uint, orderNo string) (*O
if err != nil {
return nil, nil, "", err
}
if order.OrderStatus == model.OrderStatusDelivering {
if synced, _ := s.SyncDeliveringOrder(context.Background(), order); synced {
openOrder, _ = s.fulfillment.QueryOpenOrder(orderNo)
order, _ = s.fulfillment.GetOrder(merchantID, orderNo)
}
}
goodID := ""
if openOrder.Product != nil {
if openOrder != nil && openOrder.Product != nil {
goodID = deliveryGoodID(s.channel, openOrder.Product.SKU)
}
return openOrder, order, goodID, nil
@@ -701,6 +780,24 @@ func (s *DeliveryService) buildDeliveryState(openOrder *OpenOrderQuery, order *m
return nil, nil, "", newDeliveryHTTPError(http.StatusBadRequest, reason)
}
providerOrderNo := strings.TrimSpace(openOrder.ProviderOrderNo)
if providerOrderNo == "" {
providerOrderNo = strings.TrimSpace(order.ProviderOrderNo)
}
if providerOrderNo == "" {
providerOrderNo = resultDataString(order.ResultData, "lewan_order_no")
}
if providerOrderNo == "" {
providerOrderNo = resultDataString(order.ResultData, "provider_order_no")
}
providerName := resultDataString(order.ResultData, "provider")
if providerName == "" {
if strings.TrimSpace(order.ProviderOrderNo) != "" || resultDataString(order.ResultData, "lewan_order_id") != "" {
providerName = "lewan"
}
}
info := &DeliveryOrderInfo{
OrderNo: openOrder.OrderNo,
Status: openOrder.Status,
@@ -709,6 +806,8 @@ func (s *DeliveryService) buildDeliveryState(openOrder *OpenOrderQuery, order *m
Product: deliveryProd,
BuyerName: openOrder.BuyerName,
Amount: openOrder.Amount,
ProviderOrderNo: providerOrderNo,
ProviderName: providerName,
CreatedAt: openOrder.CreatedAt,
ShippedAt: openOrder.ShippedAt,
ShipFailReason: openOrder.ShipFailReason,
@@ -886,6 +985,12 @@ func (s *DeliveryService) prepareOrder(orderNo string, requireCanShip bool, auth
return nil, nil, "", err
}
}
if order.OrderStatus == model.OrderStatusDelivering {
if synced, _ := s.SyncDeliveringOrder(context.Background(), order); synced {
openOrder, _ = s.fulfillment.QueryOpenOrder(orderNo)
order, _ = s.fulfillment.GetByOrderNo(orderNo)
}
}
return s.buildDeliveryState(openOrder, order, "", requireCanShip)
}
+7
View File
@@ -1,6 +1,7 @@
package service
import (
"context"
"encoding/json"
"errors"
"math"
@@ -19,6 +20,7 @@ type FulfillmentService struct {
db *gorm.DB
callbacks *CallbackService
upstreamCancel func(order *model.FulfillmentOrder) error
upstreamSync func(ctx context.Context, order *model.FulfillmentOrder) (bool, error)
}
func NewFulfillmentService(db *gorm.DB, callbacks *CallbackService) *FulfillmentService {
@@ -30,6 +32,11 @@ func (s *FulfillmentService) SetUpstreamCancel(fn func(order *model.FulfillmentO
s.upstreamCancel = fn
}
// SetUpstreamSync 注册主动查单同步上游状态的钩子(如乐玩订单补偿)。
func (s *FulfillmentService) SetUpstreamSync(fn func(ctx context.Context, order *model.FulfillmentOrder) (bool, error)) {
s.upstreamSync = fn
}
type CreateFulfillmentOrderInput struct {
MerchantID uint
APIClientID uint
+18 -8
View File
@@ -51,10 +51,7 @@ 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)
}
m := resultDataMap(existing)
for k, v := range patch {
m[k] = v
}
@@ -67,8 +64,15 @@ func mergeResultData(existing string, patch map[string]interface{}) string {
func resultDataMap(raw string) map[string]interface{} {
m := map[string]interface{}{}
if raw != "" && json.Valid([]byte(raw)) {
_ = json.Unmarshal([]byte(raw), &m)
if raw == "" {
return m
}
if err := json.Unmarshal([]byte(raw), &m); err != nil {
// 容错:如果 raw 被双重序列化为 string(如 "\"{\\\"provider\\\":...}\"")
var inner string
if json.Unmarshal([]byte(raw), &inner) == nil && inner != "" {
_ = json.Unmarshal([]byte(inner), &m)
}
}
return m
}
@@ -76,8 +80,14 @@ func resultDataMap(raw string) map[string]interface{} {
// requestDataMap 解析下单透传的 data(RequestData),非法或为空时返回空 map。
func requestDataMap(raw string) map[string]interface{} {
m := map[string]interface{}{}
if raw != "" && json.Valid([]byte(raw)) {
_ = json.Unmarshal([]byte(raw), &m)
if raw == "" {
return m
}
if err := json.Unmarshal([]byte(raw), &m); err != nil {
var inner string
if json.Unmarshal([]byte(raw), &inner) == nil && inner != "" {
_ = json.Unmarshal([]byte(inner), &m)
}
}
return m
}
@@ -72,8 +72,18 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe
return err
}
now := time.Now()
shippedAt := in.ShippedAt
if shippedAt == nil && in.ShipStatus == "success" {
shippedAt = &now
}
// 已交付:success 推送幂等成功。状态读取和后续更新必须在同一把行锁内完成。
if normalizeOrderStatus(&order) == model.OrderStatusDelivered && in.ShipStatus == "success" {
newResultData := buildShipNotifyResultData(order.ResultData, in, shippedAt)
if newResultData != order.ResultData {
_ = tx.Model(&model.FulfillmentOrder{}).Where("id = ?", order.ID).Update("result_data", newResultData).Error
}
result = ShipNotifyResult{
OrderNo: order.OrderNo,
Status: normalizeOrderStatus(&order),
@@ -91,12 +101,6 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe
return nil
}
now := time.Now()
shippedAt := in.ShippedAt
if shippedAt == nil && in.ShipStatus == "success" {
shippedAt = &now
}
updates := map[string]interface{}{
"order_status": nextStatus,
}
@@ -33,6 +33,14 @@ func (s *FulfillmentService) MarkProcessingTimeouts(timeout time.Duration, limit
}
changed := 0
for _, id := range ids {
if s.upstreamSync != nil {
var ord model.FulfillmentOrder
if err := s.db.First(&ord, id).Error; err == nil {
if synced, _ := s.upstreamSync(context.Background(), &ord); synced {
continue
}
}
}
updated, err := s.markProcessingTimeout(id, timeout, now)
if err != nil {
return changed, err
@@ -456,3 +456,18 @@ func (s *MerchantService) SyncLewanProductToCatalog(in SyncLewanProductInput, ac
}
return res, nil
}
// BatchSyncLewanProductsToCatalog 批量导入或关联乐玩商品至平台自营商品目录
func (s *MerchantService) BatchSyncLewanProductsToCatalog(items []SyncLewanProductInput, actorUserID uint) (int, error) {
if len(items) == 0 {
return 0, nil
}
count := 0
for _, item := range items {
if _, err := s.SyncLewanProductToCatalog(item, actorUserID); err == nil {
count++
}
}
return count, nil
}
+12
View File
@@ -115,6 +115,17 @@ type ProviderSubmitResult struct {
PatchResultData map[string]interface{} `json:"patch_result_data,omitempty"`
}
// ProviderSyncResult 抽象主动查单同步状态返回
type ProviderSyncResult struct {
Status string // success / failed / delivering / ""
ProviderOrderNo string
FailReason string
GameUID string
RoleName string
RoleArea string
RawPayload string
}
// FulfillmentProvider 统一履约提供商接口
type FulfillmentProvider interface {
Name() string
@@ -122,6 +133,7 @@ type FulfillmentProvider interface {
Bind(ctx context.Context, order *model.FulfillmentOrder, gameAccount string) (*ProviderBindResult, error)
CheckBind(ctx context.Context, order *model.FulfillmentOrder, bindUUID string) (*ProviderBindResult, error)
Submit(ctx context.Context, order *model.FulfillmentOrder, gameAccount, bindUUID string) (*ProviderSubmitResult, error)
SyncStatus(ctx context.Context, order *model.FulfillmentOrder) (*ProviderSyncResult, error)
Cancel(ctx context.Context, order *model.FulfillmentOrder) error
}
+4
View File
@@ -350,6 +350,10 @@ func (p *DLCProvider) createUpstreamOrder(orderID, gameAccount, goodID, orderNo
return out.Order, nil
}
func (p *DLCProvider) SyncStatus(ctx context.Context, order *model.FulfillmentOrder) (*ProviderSyncResult, error) {
return nil, nil
}
func (p *DLCProvider) signProxy(path, method string, data interface{}, out interface{}) error {
payload, err := json.Marshal(map[string]interface{}{
"path": path,
+112
View File
@@ -2,6 +2,7 @@ package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
@@ -356,3 +357,114 @@ func (p *LewanProvider) Cancel(ctx context.Context, order *model.FulfillmentOrde
_, err := p.client.CancelOrder(ctx, lewanOrderID)
return err
}
func (p *LewanProvider) SyncStatus(ctx context.Context, order *model.FulfillmentOrder) (*ProviderSyncResult, error) {
if p.client == nil || order == nil {
return nil, nil
}
callerOrderID := order.OrderNo
lewanOrderID := resultDataString(order.ResultData, "lewan_order_id")
var (
lewanOrder *lewan.LewanOrder
err error
)
if callerOrderID != "" {
lewanOrder, err = p.client.GetOrderByCallerOrder(ctx, callerOrderID)
}
if lewanOrder == nil && lewanOrderID != "" {
lewanOrder, err = p.client.GetOrder(ctx, lewanOrderID)
}
if err != nil || lewanOrder == nil {
return nil, err
}
roleName := ""
roleArea := ""
gameUID := lewanOrder.RoleID
if lewanOrder.Binding != nil {
if lewanOrder.Binding.RoleID != "" {
gameUID = lewanOrder.Binding.RoleID
}
roleName = lewanOrder.Binding.RoleName
roleArea = lewanOrder.Binding.RoleArea
}
if gameUID == "" {
gameUID = resultDataString(order.ResultData, "game_uid")
}
raw, _ := json.Marshal(lewanOrder)
providerOrderNo := lewanOrder.OrderNo
if providerOrderNo == "" {
providerOrderNo = lewanOrder.ID
}
switch lewanOrder.Status {
case "SUCCEEDED":
return &ProviderSyncResult{
Status: "success",
ProviderOrderNo: providerOrderNo,
GameUID: gameUID,
RoleName: roleName,
RoleArea: roleArea,
RawPayload: string(raw),
}, nil
case "FAILED":
reason := strings.Join(lewanOrder.FailureCodes, "; ")
if reason == "" {
reason = "乐玩平台发货失败"
}
return &ProviderSyncResult{
Status: "failed",
ProviderOrderNo: providerOrderNo,
FailReason: reason,
GameUID: gameUID,
RoleName: roleName,
RoleArea: roleArea,
RawPayload: string(raw),
}, nil
case "PARTIAL_SUCCESS":
if lewanOrder.SucceededQuantity > 0 {
return &ProviderSyncResult{
Status: "success",
ProviderOrderNo: providerOrderNo,
GameUID: gameUID,
RoleName: roleName,
RoleArea: roleArea,
RawPayload: string(raw),
}, nil
}
reason := strings.Join(lewanOrder.FailureCodes, "; ")
if reason == "" {
reason = "乐玩部分单件被停止或释放"
}
return &ProviderSyncResult{
Status: "failed",
ProviderOrderNo: providerOrderNo,
FailReason: reason,
GameUID: gameUID,
RoleName: roleName,
RoleArea: roleArea,
RawPayload: string(raw),
}, nil
case "ORDER_CANCELLED", "BINDING_TIMEOUT":
return &ProviderSyncResult{
Status: "failed",
ProviderOrderNo: providerOrderNo,
FailReason: "乐玩订单超时或已取消",
GameUID: gameUID,
RoleName: roleName,
RoleArea: roleArea,
RawPayload: string(raw),
}, nil
default:
return &ProviderSyncResult{
Status: "delivering",
ProviderOrderNo: providerOrderNo,
GameUID: gameUID,
RoleName: roleName,
RoleArea: roleArea,
RawPayload: string(raw),
}, nil
}
}