diff --git a/backend/internal/modules/order/admin_actions.go b/backend/internal/modules/order/admin_actions.go index 6f151aa..533202f 100644 --- a/backend/internal/modules/order/admin_actions.go +++ b/backend/internal/modules/order/admin_actions.go @@ -173,6 +173,7 @@ type adminResetTarget struct { RenterContent string AuditAction string RefreshStageTime bool + RecalculateEnd bool } // AdminResetHandoff 将超时卡住的订单恢复到对应待办阶段,并刷新阶段计时, @@ -195,6 +196,21 @@ func (r *Repository) AdminResetHandoff(ctx context.Context, adminID uint64, orde if target.RefreshStageTime { order.HandoffStartedAt = &now } + if target.RecalculateEnd { + durationHours := estimateOrderDurationHours(order.AccountSnapshot) + if durationHours > 0 { + order.EstimatedDurationHours = durationHours + } + if order.RentedAt == nil { + order.RentedAt = &now + } + if order.RentedAt != nil { + estimatedEndAt := order.RentedAt.Add(time.Duration(order.EstimatedDurationHours) * time.Hour) + if !estimatedEndAt.After(now) { + order.RentedAt = &now + } + } + } if err := notification.Append(tx, notification.Entry{ UserID: order.OwnerID, @@ -272,6 +288,19 @@ func resetTargetForOrder(order model.RentalOrder) (adminResetTarget, bool) { RefreshStageTime: true, }, true } + if order.Status == orderStatusOverdue && order.HandoffStatus == handoffStatusReturnOverdue { + return adminResetTarget{ + OrderStatus: orderStatusRenting, + HandoffStatus: handoffStatusReceived, + Label: "重置归还逾期", + OwnerTitle: "订单归还逾期已重置", + OwnerContent: "客服已重置归还逾期状态,订单恢复为使用中。", + RenterTitle: "请及时发起结账", + RenterContent: "客服已重置归还逾期状态,请尽快发起结账。", + AuditAction: "order.reset_return_overdue", + RecalculateEnd: true, + }, true + } return adminResetTarget{}, false } diff --git a/backend/internal/modules/order/repository_integration_test.go b/backend/internal/modules/order/repository_integration_test.go index dddde45..68bcf9a 100644 --- a/backend/internal/modules/order/repository_integration_test.go +++ b/backend/internal/modules/order/repository_integration_test.go @@ -395,6 +395,20 @@ func TestEstimateOrderDurationHoursFromSnapshot(t *testing.T) { } } +func TestEstimateOrderDurationHoursFromServerSnapshot(t *testing.T) { + snapshot := datatypes.JSON([]byte(`{ + "haf_coin_amount": 105000000, + "asset_summary": { + "daily_loss_m": 10 + } + }`)) + + hours := estimateOrderDurationHours(snapshot) + if hours != 252 { + t.Fatalf("hours = %d, want 252", hours) + } +} + func TestEstimateOrderDurationHoursUsesDefaultWithoutDailyLoss(t *testing.T) { snapshot := datatypes.JSON([]byte(`{ "haf_coin_amount": 165000000, @@ -604,6 +618,62 @@ func TestAdminResetCheckoutConfirmTimeout(t *testing.T) { } } +func TestAdminResetReturnOverdueRecalculatesDuration(t *testing.T) { + db := setupOrderTestDB(t) + repo := NewRepository(db) + + owner := model.User{Phone: "13800003005"} + renter := model.User{Phone: "13900003005"} + if err := db.Create(&owner).Error; err != nil { + t.Fatalf("create owner failed: %v", err) + } + if err := db.Create(&renter).Error; err != nil { + t.Fatalf("create renter failed: %v", err) + } + + rentedAt := time.Now().Add(-72 * time.Hour) + order := model.RentalOrder{ + OrderNo: "ORD-RESET-RETURN-001", + ListingID: 1, + AccountID: 1, + OwnerID: owner.ID, + RenterID: renter.ID, + RentedAt: &rentedAt, + EstimatedDurationHours: 24, + AccountSnapshot: datatypes.JSON([]byte(`{ + "haf_coin_amount": 105000000, + "asset_summary": {"daily_loss_m": 10} + }`)), + Status: orderStatusOverdue, + HandoffStatus: handoffStatusReturnOverdue, + } + if err := db.Create(&order).Error; err != nil { + t.Fatalf("create order failed: %v", err) + } + + err := repo.AdminResetHandoff(t.Context(), 1, order.ID, AdminActionRequest{Reason: "预计时长计算有误"}, AuditMeta{}) + if err != nil { + t.Fatalf("AdminResetHandoff() error = %v", err) + } + + var saved model.RentalOrder + if err := db.First(&saved, order.ID).Error; err != nil { + t.Fatalf("load order failed: %v", err) + } + if saved.Status != orderStatusRenting { + t.Fatalf("status = %q, want %q", saved.Status, orderStatusRenting) + } + if saved.HandoffStatus != handoffStatusReceived { + t.Fatalf("handoff status = %q, want %q", saved.HandoffStatus, handoffStatusReceived) + } + if saved.EstimatedDurationHours != 252 { + t.Fatalf("EstimatedDurationHours = %d, want 252", saved.EstimatedDurationHours) + } + if saved.RentedAt == nil || !saved.RentedAt.Equal(rentedAt) { + t.Fatalf("RentedAt = %#v, want original %v", saved.RentedAt, rentedAt) + } +} + func TestAdminOrderDTOIncludesResetAction(t *testing.T) { order := model.RentalOrder{ Status: orderStatusAbnormal, @@ -621,6 +691,23 @@ func TestAdminOrderDTOIncludesResetAction(t *testing.T) { } } +func TestAdminOrderDTOIncludesReturnOverdueResetAction(t *testing.T) { + order := model.RentalOrder{ + Status: orderStatusOverdue, + HandoffStatus: handoffStatusReturnOverdue, + } + dto := orderRow{RentalOrder: order}.toAdminDTO() + if dto.AdminActions == nil || dto.AdminActions.ResetHandoff == nil { + t.Fatal("return overdue reset action should be present") + } + if !dto.AdminActions.ResetHandoff.Enabled { + t.Fatal("return overdue reset action should be enabled") + } + if dto.AdminActions.ResetHandoff.Label != "重置归还逾期" { + t.Fatalf("label = %q, want 重置归还逾期", dto.AdminActions.ResetHandoff.Label) + } +} + func TestUserOrderDTODoesNotIncludeAdminActions(t *testing.T) { order := model.RentalOrder{ OwnerID: 1,