diff --git a/backend/internal/service/delivery.go b/backend/internal/service/delivery.go index 4e36ab3..cf9c5b0 100644 --- a/backend/internal/service/delivery.go +++ b/backend/internal/service/delivery.go @@ -617,7 +617,7 @@ func (s *DeliveryService) GetOrCreateDeliveryLink(merchantID uint, orderNo, requ if err != nil { return nil, err } - // 订单已取消后禁止获取发货链接(含复用未过期链接的路径),与 authorizeDeliveryLink 保持一致。 + // 已取消订单即使存在未过期链接也不再发放(买家侧授权同样拦截已取消订单)。 if normalizeOrderStatus(order) == model.OrderStatusCancelled { return nil, newDeliveryHTTPError(http.StatusBadRequest, "订单已取消,无法获取发货链接") } @@ -625,9 +625,11 @@ func (s *DeliveryService) GetOrCreateDeliveryLink(merchantID uint, orderNo, requ if order.DeliveryLinkRevokedAt != nil { return nil, newDeliveryHTTPError(http.StatusForbidden, "发货链接已作废") } + // 复用未过期链接:已交付/发货中订单也允许查看兑换结果或进度,仅停止发放新链接。 if order.DeliveryLinkExpiresAt != nil && order.DeliveryLinkExpiresAt.After(now) { return s.buildDeliveryLinkResult(order.OrderNo, order.DeliveryLinkExpiresAt.UTC().Truncate(time.Second), requestBaseURL), nil } + // 仅待发货(含可重试的发货失败)订单能生成或续期新链接。 canShip, reason := CanFulfill(order) if !canShip { if reason == "" { diff --git a/backend/internal/service/delivery_test.go b/backend/internal/service/delivery_test.go index f4af162..1ea06ed 100644 --- a/backend/internal/service/delivery_test.go +++ b/backend/internal/service/delivery_test.go @@ -116,6 +116,69 @@ func TestDeliveryLinkRejectedAfterOrderCancelled(t *testing.T) { } } +func TestDeliveryLinkAfterDeliveredReusesButNeverGenerates(t *testing.T) { + db := newServiceTestDB(t) + merchantID, product := seedFulfillmentMerchant(t, db, "delivery-done", 5000, 5, 100) + fulfillmentSvc := NewFulfillmentService(db, nil) + deliverySvc := NewDeliveryService(fulfillmentSvc, "https://bff.example", "dlc", "https://shop.example", "link-secret", 60) + + created, err := fulfillmentSvc.CreateOrder(CreateFulfillmentOrderInput{ + MerchantID: merchantID, + APIClientID: 1, + ClientOrderNo: "delivery-done-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 _, err := fulfillmentSvc.UpdateFulfillment(FulfillmentUpdateInput{ + MerchantID: merchantID, + APIClientID: 1, + OrderNo: created.Order.OrderNo, + Status: model.OrderStatusDelivered, + }); err != nil { + t.Fatalf("mark delivered: %v", err) + } + // 已交付订单仍可复用未过期的既有链接(查看兑换结果),但不会生成新链接。 + reused, err := deliverySvc.GetOrCreateDeliveryLink(merchantID, created.Order.OrderNo, "https://shop.example") + if err != nil { + t.Fatalf("delivered order should reuse existing link: %v", err) + } + if reused.DeliveryURL != link.DeliveryURL || reused.Exp != link.Exp { + t.Fatalf("reused link should match original, got %+v", reused) + } + // 买家侧仍可通过既有链接只读查看交付结果。 + if _, err := deliverySvc.GetOrder(created.Order.OrderNo, DeliveryLinkAuth{Exp: link.Exp, Sign: link.Sign}); err != nil { + t.Fatalf("delivered order link should stay readable for the buyer: %v", err) + } + + // 从未生成过链接的已交付订单,不能再获取新链接。 + second, err := fulfillmentSvc.CreateOrder(CreateFulfillmentOrderInput{ + MerchantID: merchantID, + APIClientID: 1, + ClientOrderNo: "delivery-done-002", + SKU: product.SKU, + }) + if err != nil { + t.Fatalf("create second order: %v", err) + } + if _, err := fulfillmentSvc.UpdateFulfillment(FulfillmentUpdateInput{ + MerchantID: merchantID, + APIClientID: 1, + OrderNo: second.Order.OrderNo, + Status: model.OrderStatusDelivered, + }); err != nil { + t.Fatalf("mark second delivered: %v", err) + } + if _, err := deliverySvc.GetOrCreateDeliveryLink(merchantID, second.Order.OrderNo, "https://shop.example"); err == nil { + t.Fatalf("delivered order without existing link should not generate a new one") + } +} + func TestDeliveryGoodIDIncludesMedalAndCoinProducts(t *testing.T) { cases := map[string]string{ "honor_medal_x2": "682ef39ca8f40c4234c59f45", diff --git a/frontend/src/pages/merchantCenterTabs.tsx b/frontend/src/pages/merchantCenterTabs.tsx index 4e67445..32fad9e 100644 --- a/frontend/src/pages/merchantCenterTabs.tsx +++ b/frontend/src/pages/merchantCenterTabs.tsx @@ -260,9 +260,29 @@ export function OrdersTab({ key: 'delivery_link', width: 160, render: (_, record) => { + // 非可发货状态不再展示链接作废等操作;已交付订单保留复制/打开,便于查看兑换结果。 if (record.order_status === 'cancelled') { return 订单已取消 } + if (record.order_status === 'delivering') { + return 发货中 + } + if (record.order_status === 'delivered') { + if (record.delivery_link_revoked_at) { + return 已作废 + } + const expired = !record.delivery_link_expires_at + || new Date(record.delivery_link_expires_at).getTime() < Date.now() + if (expired) { + return 已过期 + } + return ( + + + + + ) + } if (record.delivery_link_revoked_at) { return }