问题:上游 0666.pro 兑换返回 HTTP 200 + status=pending(官方处理中), 旧逻辑仅凭「没有 error 字段」就判定成功并写 redeemed 终态,导致本地显示 已发货而上游仍是 bound,且 redeemed 为硬终态无法自愈。 后端: - RedeemSkin 改为三态判定(success / pending / failed),不再无 error 即成功 - 新增 redeeming「发货处理中」状态;pending 不写 redeemed_at/fulfill_id - 成功落库前二次 refresh 确认上游 redeemed 或 redeemed_at 才置终态 - 新增确认 worker:20s 轮询、30s→5m 指数退避、110001 全局冷却 60s、 15 分钟未确认仅告警不回退,确认后同步订单为 delivered 并触发回调 - pending 期间禁止重复兑换,订单保持 delivering,不下发发货成功回调 - 新增 rock_delivery_logs 表与记录器:入站参数、内部状态、上游请求/响应 原文全部留档,保留 180 天 - 分配/绑定校验/刷新/导入/删除/回收站/统计/客服查询全部适配 redeeming - 迁移 026:redeeming 跟踪字段、部分索引与报文日志表 前端: - Token 列表新增「发货处理中」筛选项、标签与超时提示 - 删除/回收站纳入 redeeming 危险状态 - 历史抽屉拆为「状态历史 / 发货报文」双页签,按 trace 展示报文详情 - 买家发货页在 pending 时提示官方处理中,无需重复提交 测试:新增 pending 不落终态、异常响应不误判、重复提交不重复兑换、 确认后同步订单、保持 pending 并退避 5 个用例;go build/vet/test 与 前端构建均通过。
94 lines
2.5 KiB
Go
94 lines
2.5 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
|
|
"affiliate_dash/internal/model"
|
|
"affiliate_dash/internal/pkg/timeutil"
|
|
)
|
|
|
|
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 = "订单已交付"
|
|
} else if boolFromMap(resultData, "rock_redeem_pending") {
|
|
message = "官方处理中,请稍后刷新查看结果(系统会自动确认发货状态)"
|
|
}
|
|
return &DeliverySubmitResult{
|
|
OrderNo: order.OrderNo,
|
|
Status: status,
|
|
Pending: boolFromMap(resultData, "rock_redeem_pending"),
|
|
Message: message,
|
|
ProviderOrderNo: providerOrderNo,
|
|
UpstreamOrder: resultData,
|
|
}
|
|
}
|
|
|
|
func boolFromMap(values map[string]interface{}, key string) bool {
|
|
if values == nil {
|
|
return false
|
|
}
|
|
switch value := values[key].(type) {
|
|
case bool:
|
|
return value
|
|
case string:
|
|
return strings.EqualFold(strings.TrimSpace(value), "true") || strings.TrimSpace(value) == "1"
|
|
case float64:
|
|
return value != 0
|
|
case json.Number:
|
|
parsed, err := value.Int64()
|
|
return err == nil && parsed != 0
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|