功能:发货链接支持永久有效,默认 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:
yml2213
2026-08-14 14:26:06 +08:00
parent 11bce9c87e
commit b76d8dc257
11 changed files with 147 additions and 32 deletions
+42 -19
View File
@@ -73,8 +73,9 @@ func NewDeliveryService(fulfillment *FulfillmentService, bffBaseURL, channel, li
if linkSecret == "" {
linkSecret = "affiliate-dash-delivery-link-secret"
}
if linkTTLMinutes <= 0 {
linkTTLMinutes = 120
// linkTTLMinutes <= 0 表示链接永久有效(0 为默认值)。
if linkTTLMinutes < 0 {
linkTTLMinutes = 0
}
return &DeliveryService{
fulfillment: fulfillment,
@@ -133,11 +134,11 @@ type DeliveryBindResult struct {
}
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"`
OrderNo string `json:"order_no"`
DeliveryURL string `json:"delivery_url"`
ExpiresAt *time.Time `json:"expires_at,omitempty"` // null 表示永久有效
Exp int64 `json:"exp"`
Sign string `json:"sign"`
}
type DeliverySubmitResult struct {
@@ -627,7 +628,12 @@ func (s *DeliveryService) GetOrCreateDeliveryLink(merchantID uint, orderNo, requ
}
// 复用未过期链接:已交付/发货中订单也允许查看兑换结果或进度,仅停止发放新链接。
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)
@@ -637,12 +643,17 @@ func (s *DeliveryService) GetOrCreateDeliveryLink(merchantID uint, orderNo, requ
}
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{}).
Where("id = ?", order.ID).
Updates(map[string]interface{}{
"delivery_link_expires_at": expiresAt,
"delivery_link_revoked_at": nil,
"delivery_link_expires_at": expiresAt,
"delivery_link_revoked_at": nil,
"delivery_link_generated_at": now,
}).Error; err != nil {
return nil, err
}
@@ -689,22 +700,34 @@ func (s *DeliveryService) RestoreDeliveryLink(merchantID, actorUserID uint, orde
}
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)
now := time.Now().UTC()
var expiresAt *time.Time
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 := tx.Model(&model.FulfillmentOrder{}).
Where("id = ?", order.ID).
Updates(map[string]interface{}{
"delivery_link_expires_at": expiresAt,
"delivery_link_revoked_at": nil,
"delivery_link_expires_at": expiresAt,
"delivery_link_revoked_at": nil,
"delivery_link_generated_at": now,
}).Error; err != nil {
return err
}
return writeAudit(tx, &merchantID, optionalUint(actorUserID), nil, "delivery_link.restore", "fulfillment_order", order.OrderNo, map[string]interface{}{
"expires_at": timeutil.FormatAPITime(expiresAt),
})
metadata := map[string]interface{}{
"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 {
return nil, err
}