功能:发货链接支持永久有效,默认 TTL=0
- DELIVERY_LINK_TTL_MINUTES 设为 0 或负数时链接永久有效(expires_at 存 NULL), 默认值由 120 改为 0,本地 .env 同步改为永久 - 新增 delivery_link_generated_at 列区分从未生成与永久链接,兼容历史数据 - 永久链接授权仅校验 HMAC 签名、不校验过期(exp=0),作废/恢复语义不变 - 前端链接有效期列对永久链接显示"永久有效"
This commit is contained in:
+2
-1
@@ -27,7 +27,8 @@ DELIVERY_BASE_URL=
|
|||||||
DELIVERY_BFF_BASE_URL=https://www.jxya.top/bff
|
DELIVERY_BFF_BASE_URL=https://www.jxya.top/bff
|
||||||
DELIVERY_CHANNEL=dlc
|
DELIVERY_CHANNEL=dlc
|
||||||
DELIVERY_LINK_SECRET=change_me_for_delivery_link_sign
|
DELIVERY_LINK_SECRET=change_me_for_delivery_link_sign
|
||||||
DELIVERY_LINK_TTL_MINUTES=120
|
# 发货链接有效期(分钟),0 或留空表示永久有效
|
||||||
|
DELIVERY_LINK_TTL_MINUTES=0
|
||||||
FULFILLMENT_PROCESSING_TIMEOUT_MINUTES=30
|
FULFILLMENT_PROCESSING_TIMEOUT_MINUTES=30
|
||||||
FULFILLMENT_TIMEOUT_SCAN_INTERVAL_SECONDS=60
|
FULFILLMENT_TIMEOUT_SCAN_INTERVAL_SECONDS=60
|
||||||
|
|
||||||
|
|||||||
@@ -26,3 +26,4 @@ frontend/dist/
|
|||||||
|
|
||||||
# reasonix 本地会话数据
|
# reasonix 本地会话数据
|
||||||
.reasonix/
|
.reasonix/
|
||||||
|
backend/.gocache
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ type Config struct {
|
|||||||
DeliveryChannel string
|
DeliveryChannel string
|
||||||
// DeliveryLinkSecret 发货链接签名密钥。
|
// DeliveryLinkSecret 发货链接签名密钥。
|
||||||
DeliveryLinkSecret string
|
DeliveryLinkSecret string
|
||||||
// DeliveryLinkTTLMinutes 发货链接默认有效分钟数。
|
// DeliveryLinkTTLMinutes 发货链接有效分钟数;<=0 表示永久有效。
|
||||||
DeliveryLinkTTLMinutes int
|
DeliveryLinkTTLMinutes int
|
||||||
// FulfillmentProcessingTimeoutMinutes 发货中订单超过该分钟数自动标记失败;<=0 表示关闭。
|
// FulfillmentProcessingTimeoutMinutes 发货中订单超过该分钟数自动标记失败;<=0 表示关闭。
|
||||||
FulfillmentProcessingTimeoutMinutes int
|
FulfillmentProcessingTimeoutMinutes int
|
||||||
@@ -78,7 +78,7 @@ func Load() *Config {
|
|||||||
DeliveryBFFBaseURL: getEnv("DELIVERY_BFF_BASE_URL", "https://www.jxya.top/bff"),
|
DeliveryBFFBaseURL: getEnv("DELIVERY_BFF_BASE_URL", "https://www.jxya.top/bff"),
|
||||||
DeliveryChannel: getEnv("DELIVERY_CHANNEL", "dlc"),
|
DeliveryChannel: getEnv("DELIVERY_CHANNEL", "dlc"),
|
||||||
DeliveryLinkSecret: getEnv("DELIVERY_LINK_SECRET", getEnv("OPEN_API_SECRET", getEnv("JWT_SECRET", "affiliate-dash-dev-secret-change-me"))),
|
DeliveryLinkSecret: getEnv("DELIVERY_LINK_SECRET", getEnv("OPEN_API_SECRET", getEnv("JWT_SECRET", "affiliate-dash-dev-secret-change-me"))),
|
||||||
DeliveryLinkTTLMinutes: getEnvInt("DELIVERY_LINK_TTL_MINUTES", 120),
|
DeliveryLinkTTLMinutes: getEnvInt("DELIVERY_LINK_TTL_MINUTES", 0),
|
||||||
FulfillmentProcessingTimeoutMinutes: getEnvInt("FULFILLMENT_PROCESSING_TIMEOUT_MINUTES", 30),
|
FulfillmentProcessingTimeoutMinutes: getEnvInt("FULFILLMENT_PROCESSING_TIMEOUT_MINUTES", 30),
|
||||||
FulfillmentTimeoutScanIntervalSeconds: getEnvInt("FULFILLMENT_TIMEOUT_SCAN_INTERVAL_SECONDS", 60),
|
FulfillmentTimeoutScanIntervalSeconds: getEnvInt("FULFILLMENT_TIMEOUT_SCAN_INTERVAL_SECONDS", 60),
|
||||||
CallbackMaxAttempts: getEnvInt("CALLBACK_MAX_ATTEMPTS", 16),
|
CallbackMaxAttempts: getEnvInt("CALLBACK_MAX_ATTEMPTS", 16),
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- 记录发货链接的生成时间,用于区分"从未生成"与"永久有效链接
|
||||||
|
-- (永久链接 delivery_link_expires_at 为 NULL,需要该列避免与未生成状态混淆)。
|
||||||
|
ALTER TABLE fulfillment_orders ADD COLUMN IF NOT EXISTS delivery_link_generated_at timestamptz;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_delivery_link_generated_at
|
||||||
|
ON fulfillment_orders (delivery_link_generated_at);
|
||||||
@@ -242,6 +242,8 @@ type FulfillmentOrder struct {
|
|||||||
DeliveredAt *time.Time `json:"delivered_at"`
|
DeliveredAt *time.Time `json:"delivered_at"`
|
||||||
DeliveryLinkExpiresAt *time.Time `json:"delivery_link_expires_at"`
|
DeliveryLinkExpiresAt *time.Time `json:"delivery_link_expires_at"`
|
||||||
DeliveryLinkRevokedAt *time.Time `json:"delivery_link_revoked_at"`
|
DeliveryLinkRevokedAt *time.Time `json:"delivery_link_revoked_at"`
|
||||||
|
// DeliveryLinkGeneratedAt 记录发货链接的生成/恢复时间;expires_at 为 NULL 时表示永久有效。
|
||||||
|
DeliveryLinkGeneratedAt *time.Time `json:"delivery_link_generated_at"`
|
||||||
|
|
||||||
MerchantProduct *MerchantProduct `gorm:"foreignKey:MerchantProductID" json:"merchant_product,omitempty"`
|
MerchantProduct *MerchantProduct `gorm:"foreignKey:MerchantProductID" json:"merchant_product,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,8 +73,9 @@ func NewDeliveryService(fulfillment *FulfillmentService, bffBaseURL, channel, li
|
|||||||
if linkSecret == "" {
|
if linkSecret == "" {
|
||||||
linkSecret = "affiliate-dash-delivery-link-secret"
|
linkSecret = "affiliate-dash-delivery-link-secret"
|
||||||
}
|
}
|
||||||
if linkTTLMinutes <= 0 {
|
// linkTTLMinutes <= 0 表示链接永久有效(0 为默认值)。
|
||||||
linkTTLMinutes = 120
|
if linkTTLMinutes < 0 {
|
||||||
|
linkTTLMinutes = 0
|
||||||
}
|
}
|
||||||
return &DeliveryService{
|
return &DeliveryService{
|
||||||
fulfillment: fulfillment,
|
fulfillment: fulfillment,
|
||||||
@@ -135,7 +136,7 @@ type DeliveryBindResult struct {
|
|||||||
type DeliveryLinkResult struct {
|
type DeliveryLinkResult struct {
|
||||||
OrderNo string `json:"order_no"`
|
OrderNo string `json:"order_no"`
|
||||||
DeliveryURL string `json:"delivery_url"`
|
DeliveryURL string `json:"delivery_url"`
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
ExpiresAt *time.Time `json:"expires_at,omitempty"` // null 表示永久有效
|
||||||
Exp int64 `json:"exp"`
|
Exp int64 `json:"exp"`
|
||||||
Sign string `json:"sign"`
|
Sign string `json:"sign"`
|
||||||
}
|
}
|
||||||
@@ -627,7 +628,12 @@ func (s *DeliveryService) GetOrCreateDeliveryLink(merchantID uint, orderNo, requ
|
|||||||
}
|
}
|
||||||
// 复用未过期链接:已交付/发货中订单也允许查看兑换结果或进度,仅停止发放新链接。
|
// 复用未过期链接:已交付/发货中订单也允许查看兑换结果或进度,仅停止发放新链接。
|
||||||
if order.DeliveryLinkExpiresAt != nil && order.DeliveryLinkExpiresAt.After(now) {
|
if order.DeliveryLinkExpiresAt != nil && order.DeliveryLinkExpiresAt.After(now) {
|
||||||
return s.buildDeliveryLinkResult(order.OrderNo, order.DeliveryLinkExpiresAt.UTC().Truncate(time.Second), requestBaseURL), nil
|
expiresAt := order.DeliveryLinkExpiresAt.UTC().Truncate(time.Second)
|
||||||
|
return s.buildDeliveryLinkResult(order.OrderNo, &expiresAt, requestBaseURL), nil
|
||||||
|
}
|
||||||
|
// 永久有效链接(已生成、未作废、无有效期)直接复用,不重复生成。
|
||||||
|
if order.DeliveryLinkGeneratedAt != nil && order.DeliveryLinkExpiresAt == nil {
|
||||||
|
return s.buildDeliveryLinkResult(order.OrderNo, nil, requestBaseURL), nil
|
||||||
}
|
}
|
||||||
// 仅待发货(含可重试的发货失败)订单能生成或续期新链接。
|
// 仅待发货(含可重试的发货失败)订单能生成或续期新链接。
|
||||||
canShip, reason := CanFulfill(order)
|
canShip, reason := CanFulfill(order)
|
||||||
@@ -637,12 +643,17 @@ func (s *DeliveryService) GetOrCreateDeliveryLink(merchantID uint, orderNo, requ
|
|||||||
}
|
}
|
||||||
return nil, newDeliveryHTTPError(http.StatusBadRequest, reason)
|
return nil, newDeliveryHTTPError(http.StatusBadRequest, reason)
|
||||||
}
|
}
|
||||||
expiresAt := now.Add(s.linkTTL).UTC().Truncate(time.Second)
|
var expiresAt *time.Time
|
||||||
|
if s.linkTTL > 0 {
|
||||||
|
value := now.Add(s.linkTTL).UTC().Truncate(time.Second)
|
||||||
|
expiresAt = &value
|
||||||
|
}
|
||||||
if err := s.fulfillment.db.Model(&model.FulfillmentOrder{}).
|
if err := s.fulfillment.db.Model(&model.FulfillmentOrder{}).
|
||||||
Where("id = ?", order.ID).
|
Where("id = ?", order.ID).
|
||||||
Updates(map[string]interface{}{
|
Updates(map[string]interface{}{
|
||||||
"delivery_link_expires_at": expiresAt,
|
"delivery_link_expires_at": expiresAt,
|
||||||
"delivery_link_revoked_at": nil,
|
"delivery_link_revoked_at": nil,
|
||||||
|
"delivery_link_generated_at": now,
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -689,9 +700,14 @@ func (s *DeliveryService) RestoreDeliveryLink(merchantID, actorUserID uint, orde
|
|||||||
}
|
}
|
||||||
return nil, newDeliveryHTTPError(http.StatusBadRequest, reason)
|
return nil, newDeliveryHTTPError(http.StatusBadRequest, reason)
|
||||||
}
|
}
|
||||||
expiresAt := time.Now().UTC().Add(s.linkTTL).Truncate(time.Second)
|
now := time.Now().UTC()
|
||||||
if order.DeliveryLinkExpiresAt != nil && expiresAt.Equal(order.DeliveryLinkExpiresAt.UTC().Truncate(time.Second)) {
|
var expiresAt *time.Time
|
||||||
expiresAt = expiresAt.Add(time.Second)
|
if s.linkTTL > 0 {
|
||||||
|
value := now.Add(s.linkTTL).Truncate(time.Second)
|
||||||
|
if order.DeliveryLinkExpiresAt != nil && value.Equal(order.DeliveryLinkExpiresAt.UTC().Truncate(time.Second)) {
|
||||||
|
value = value.Add(time.Second)
|
||||||
|
}
|
||||||
|
expiresAt = &value
|
||||||
}
|
}
|
||||||
if err := s.fulfillment.db.Transaction(func(tx *gorm.DB) error {
|
if err := s.fulfillment.db.Transaction(func(tx *gorm.DB) error {
|
||||||
if err := tx.Model(&model.FulfillmentOrder{}).
|
if err := tx.Model(&model.FulfillmentOrder{}).
|
||||||
@@ -699,12 +715,19 @@ func (s *DeliveryService) RestoreDeliveryLink(merchantID, actorUserID uint, orde
|
|||||||
Updates(map[string]interface{}{
|
Updates(map[string]interface{}{
|
||||||
"delivery_link_expires_at": expiresAt,
|
"delivery_link_expires_at": expiresAt,
|
||||||
"delivery_link_revoked_at": nil,
|
"delivery_link_revoked_at": nil,
|
||||||
|
"delivery_link_generated_at": now,
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return writeAudit(tx, &merchantID, optionalUint(actorUserID), nil, "delivery_link.restore", "fulfillment_order", order.OrderNo, map[string]interface{}{
|
metadata := map[string]interface{}{
|
||||||
"expires_at": timeutil.FormatAPITime(expiresAt),
|
"generated_at": timeutil.FormatAPITime(now),
|
||||||
})
|
}
|
||||||
|
if expiresAt != nil {
|
||||||
|
metadata["expires_at"] = timeutil.FormatAPITime(*expiresAt)
|
||||||
|
} else {
|
||||||
|
metadata["permanent"] = true
|
||||||
|
}
|
||||||
|
return writeAudit(tx, &merchantID, optionalUint(actorUserID), nil, "delivery_link.restore", "fulfillment_order", order.OrderNo, metadata)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func (s *DeliveryService) authorizeDeliveryLink(order *model.FulfillmentOrder, a
|
|||||||
if order == nil {
|
if order == nil {
|
||||||
return newDeliveryHTTPError(http.StatusNotFound, "订单不存在")
|
return newDeliveryHTTPError(http.StatusNotFound, "订单不存在")
|
||||||
}
|
}
|
||||||
if auth.Exp <= 0 || strings.TrimSpace(auth.Sign) == "" {
|
if strings.TrimSpace(auth.Sign) == "" {
|
||||||
return newDeliveryHTTPError(http.StatusForbidden, "链接参数缺失")
|
return newDeliveryHTTPError(http.StatusForbidden, "链接参数缺失")
|
||||||
}
|
}
|
||||||
if order.DeliveryLinkRevokedAt != nil {
|
if order.DeliveryLinkRevokedAt != nil {
|
||||||
@@ -28,9 +28,18 @@ func (s *DeliveryService) authorizeDeliveryLink(order *model.FulfillmentOrder, a
|
|||||||
if normalizeOrderStatus(order) == model.OrderStatusCancelled {
|
if normalizeOrderStatus(order) == model.OrderStatusCancelled {
|
||||||
return newDeliveryHTTPError(http.StatusForbidden, "订单已取消,发货链接不可用")
|
return newDeliveryHTTPError(http.StatusForbidden, "订单已取消,发货链接不可用")
|
||||||
}
|
}
|
||||||
if order.DeliveryLinkExpiresAt == nil {
|
// 从未生成过链接(兼容历史数据:本次改动前生成的有时效链接没有 generated_at 记录)。
|
||||||
|
if order.DeliveryLinkGeneratedAt == nil && order.DeliveryLinkExpiresAt == nil {
|
||||||
return newDeliveryHTTPError(http.StatusForbidden, "发货链接未生成")
|
return newDeliveryHTTPError(http.StatusForbidden, "发货链接未生成")
|
||||||
}
|
}
|
||||||
|
// 永久有效链接(expires_at 为 NULL):仅校验签名(exp 固定为 0),不检查过期。
|
||||||
|
if order.DeliveryLinkExpiresAt == nil {
|
||||||
|
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
|
||||||
|
}
|
||||||
expiresAt := order.DeliveryLinkExpiresAt.UTC().Truncate(time.Second)
|
expiresAt := order.DeliveryLinkExpiresAt.UTC().Truncate(time.Second)
|
||||||
linkExpires := time.Unix(auth.Exp, 0).UTC()
|
linkExpires := time.Unix(auth.Exp, 0).UTC()
|
||||||
if !linkExpires.Equal(expiresAt) {
|
if !linkExpires.Equal(expiresAt) {
|
||||||
@@ -46,9 +55,12 @@ func (s *DeliveryService) authorizeDeliveryLink(order *model.FulfillmentOrder, a
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DeliveryService) buildDeliveryLinkResult(orderNo string, expiresAt time.Time, requestBaseURL string) *DeliveryLinkResult {
|
// buildDeliveryLinkResult 组装链接结果;expiresAt 为 nil 表示永久有效(exp 固定为 0)。
|
||||||
expiresAt = expiresAt.UTC()
|
func (s *DeliveryService) buildDeliveryLinkResult(orderNo string, expiresAt *time.Time, requestBaseURL string) *DeliveryLinkResult {
|
||||||
exp := expiresAt.Unix()
|
exp := int64(0)
|
||||||
|
if expiresAt != nil {
|
||||||
|
exp = expiresAt.UTC().Unix()
|
||||||
|
}
|
||||||
sign := s.signDeliveryLink(orderNo, exp)
|
sign := s.signDeliveryLink(orderNo, exp)
|
||||||
return &DeliveryLinkResult{
|
return &DeliveryLinkResult{
|
||||||
OrderNo: orderNo,
|
OrderNo: orderNo,
|
||||||
|
|||||||
@@ -179,6 +179,71 @@ func TestDeliveryLinkAfterDeliveredReusesButNeverGenerates(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeliveryLinkPermanentWhenTTLZero(t *testing.T) {
|
||||||
|
db := newServiceTestDB(t)
|
||||||
|
merchantID, product := seedFulfillmentMerchant(t, db, "delivery-permanent", 5000, 5, 100)
|
||||||
|
fulfillmentSvc := NewFulfillmentService(db, nil)
|
||||||
|
deliverySvc := NewDeliveryService(fulfillmentSvc, "https://bff.example", "dlc", "https://shop.example", "link-secret", 0)
|
||||||
|
|
||||||
|
created, err := fulfillmentSvc.CreateOrder(CreateFulfillmentOrderInput{
|
||||||
|
MerchantID: merchantID,
|
||||||
|
APIClientID: 1,
|
||||||
|
ClientOrderNo: "delivery-permanent-001",
|
||||||
|
SKU: product.SKU,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create order: %v", err)
|
||||||
|
}
|
||||||
|
link, err := deliverySvc.GetOrCreateDeliveryLink(merchantID, created.Order.OrderNo, "https://shop.example")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get delivery link: %v", err)
|
||||||
|
}
|
||||||
|
if link.ExpiresAt != nil || link.Exp != 0 {
|
||||||
|
t.Fatalf("permanent link should have nil expires_at and exp=0, got %+v", link)
|
||||||
|
}
|
||||||
|
// 永久链接授权通过(不校验过期)。
|
||||||
|
if _, err := deliverySvc.GetOrder(created.Order.OrderNo, DeliveryLinkAuth{Exp: link.Exp, Sign: link.Sign}); err != nil {
|
||||||
|
t.Fatalf("permanent link should authorize: %v", err)
|
||||||
|
}
|
||||||
|
// 数据库中 expires_at 为 NULL、generated_at 已记录。
|
||||||
|
var order model.FulfillmentOrder
|
||||||
|
if err := db.Where("order_no = ?", created.Order.OrderNo).First(&order).Error; err != nil {
|
||||||
|
t.Fatalf("reload order: %v", err)
|
||||||
|
}
|
||||||
|
if order.DeliveryLinkExpiresAt != nil {
|
||||||
|
t.Fatalf("permanent link should store NULL expires_at, got %v", *order.DeliveryLinkExpiresAt)
|
||||||
|
}
|
||||||
|
if order.DeliveryLinkGeneratedAt == nil {
|
||||||
|
t.Fatalf("permanent link should record generated_at")
|
||||||
|
}
|
||||||
|
// 再次获取直接复用,URL 与签名保持稳定。
|
||||||
|
reused, err := deliverySvc.GetOrCreateDeliveryLink(merchantID, created.Order.OrderNo, "https://shop.example")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reuse permanent link: %v", err)
|
||||||
|
}
|
||||||
|
if reused.DeliveryURL != link.DeliveryURL || reused.Exp != link.Exp {
|
||||||
|
t.Fatalf("permanent link should be stable, got %+v", reused)
|
||||||
|
}
|
||||||
|
// 作废后永久链接立即失效。
|
||||||
|
if err := deliverySvc.RevokeDeliveryLink(merchantID, 1, created.Order.OrderNo); err != nil {
|
||||||
|
t.Fatalf("revoke: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := deliverySvc.GetOrder(created.Order.OrderNo, DeliveryLinkAuth{Exp: link.Exp, Sign: link.Sign}); err == nil {
|
||||||
|
t.Fatalf("revoked permanent link should be rejected")
|
||||||
|
}
|
||||||
|
// 恢复后重新变为永久有效。
|
||||||
|
restored, err := deliverySvc.RestoreDeliveryLink(merchantID, 1, created.Order.OrderNo, "https://shop.example")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("restore permanent link: %v", err)
|
||||||
|
}
|
||||||
|
if restored.ExpiresAt != nil {
|
||||||
|
t.Fatalf("restored permanent link should keep nil expires_at, got %v", restored.ExpiresAt)
|
||||||
|
}
|
||||||
|
if _, err := deliverySvc.GetOrder(created.Order.OrderNo, DeliveryLinkAuth{Exp: restored.Exp, Sign: restored.Sign}); err != nil {
|
||||||
|
t.Fatalf("restored permanent link should authorize: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeliveryGoodIDIncludesMedalAndCoinProducts(t *testing.T) {
|
func TestDeliveryGoodIDIncludesMedalAndCoinProducts(t *testing.T) {
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
"honor_medal_x2": "682ef39ca8f40c4234c59f45",
|
"honor_medal_x2": "682ef39ca8f40c4234c59f45",
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ services:
|
|||||||
- DELIVERY_BFF_BASE_URL=${DELIVERY_BFF_BASE_URL:-https://www.jxya.top/bff}
|
- DELIVERY_BFF_BASE_URL=${DELIVERY_BFF_BASE_URL:-https://www.jxya.top/bff}
|
||||||
- DELIVERY_CHANNEL=${DELIVERY_CHANNEL:-dlc}
|
- DELIVERY_CHANNEL=${DELIVERY_CHANNEL:-dlc}
|
||||||
- DELIVERY_LINK_SECRET=${DELIVERY_LINK_SECRET:-}
|
- DELIVERY_LINK_SECRET=${DELIVERY_LINK_SECRET:-}
|
||||||
- DELIVERY_LINK_TTL_MINUTES=${DELIVERY_LINK_TTL_MINUTES:-120}
|
- DELIVERY_LINK_TTL_MINUTES=${DELIVERY_LINK_TTL_MINUTES:-0}
|
||||||
- FULFILLMENT_PROCESSING_TIMEOUT_MINUTES=${FULFILLMENT_PROCESSING_TIMEOUT_MINUTES:-30}
|
- FULFILLMENT_PROCESSING_TIMEOUT_MINUTES=${FULFILLMENT_PROCESSING_TIMEOUT_MINUTES:-30}
|
||||||
- FULFILLMENT_TIMEOUT_SCAN_INTERVAL_SECONDS=${FULFILLMENT_TIMEOUT_SCAN_INTERVAL_SECONDS:-60}
|
- FULFILLMENT_TIMEOUT_SCAN_INTERVAL_SECONDS=${FULFILLMENT_TIMEOUT_SCAN_INTERVAL_SECONDS:-60}
|
||||||
- CALLBACK_MAX_ATTEMPTS=${CALLBACK_MAX_ATTEMPTS:-16}
|
- CALLBACK_MAX_ATTEMPTS=${CALLBACK_MAX_ATTEMPTS:-16}
|
||||||
|
|||||||
@@ -252,7 +252,11 @@ export function OrdersTab({
|
|||||||
title: '链接有效期',
|
title: '链接有效期',
|
||||||
dataIndex: 'delivery_link_expires_at',
|
dataIndex: 'delivery_link_expires_at',
|
||||||
width: 140,
|
width: 140,
|
||||||
render: (_, record) => record.delivery_link_revoked_at ? <Tag color="red">已作废</Tag> : formatDateTime(record.delivery_link_expires_at),
|
render: (_, record) => {
|
||||||
|
if (record.delivery_link_revoked_at) return <Tag color="red">已作废</Tag>
|
||||||
|
if (!record.delivery_link_expires_at) return <Tag color="green">永久有效</Tag>
|
||||||
|
return formatDateTime(record.delivery_link_expires_at)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{ title: '时间', dataIndex: 'created_at', width: 150, render: formatDateTime },
|
{ title: '时间', dataIndex: 'created_at', width: 150, render: formatDateTime },
|
||||||
{
|
{
|
||||||
@@ -271,8 +275,9 @@ export function OrdersTab({
|
|||||||
if (record.delivery_link_revoked_at) {
|
if (record.delivery_link_revoked_at) {
|
||||||
return <Tag color="red">已作废</Tag>
|
return <Tag color="red">已作废</Tag>
|
||||||
}
|
}
|
||||||
const expired = !record.delivery_link_expires_at
|
// expires_at 为空表示永久有效,不视为过期
|
||||||
|| new Date(record.delivery_link_expires_at).getTime() < Date.now()
|
const expired = !!record.delivery_link_expires_at
|
||||||
|
&& new Date(record.delivery_link_expires_at).getTime() < Date.now()
|
||||||
if (expired) {
|
if (expired) {
|
||||||
return <Typography.Text type="secondary">已过期</Typography.Text>
|
return <Typography.Text type="secondary">已过期</Typography.Text>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ export interface FulfillmentOrder {
|
|||||||
cancelled_at?: string | null
|
cancelled_at?: string | null
|
||||||
delivery_link_expires_at?: string | null
|
delivery_link_expires_at?: string | null
|
||||||
delivery_link_revoked_at?: string | null
|
delivery_link_revoked_at?: string | null
|
||||||
|
delivery_link_generated_at?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateTestOrderResult {
|
export interface CreateTestOrderResult {
|
||||||
@@ -201,7 +202,7 @@ export interface DeliverySubmitResult {
|
|||||||
export interface DeliveryLinkResult {
|
export interface DeliveryLinkResult {
|
||||||
order_no: string
|
order_no: string
|
||||||
delivery_url: string
|
delivery_url: string
|
||||||
expires_at: string
|
expires_at: string | null // null 表示永久有效
|
||||||
exp: number
|
exp: number
|
||||||
sign: string
|
sign: string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user