package ordertimeout import ( "testing" "time" "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/modules/chat" "go.uber.org/zap" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) func setupOrderTimeoutTestDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { t.Fatalf("无法创建测试数据库: %v", err) } if err := db.AutoMigrate( &model.RentalOrder{}, &model.HandoffRecord{}, &model.Notification{}, &model.ChatConversation{}, &model.ChatParticipant{}, &model.ChatMessage{}, &model.SystemConfig{}, &model.AuditLog{}, ); err != nil { t.Fatalf("数据库迁移失败: %v", err) } return db } func TestHandleEndedOrderRenterRetention(t *testing.T) { db := setupOrderTimeoutTestDB(t) job := New(db, nil, zap.NewNop()) now := time.Date(2026, 6, 18, 12, 0, 0, 0, time.UTC) oldSettledAt := now.AddDate(0, 0, -6) recentSettledAt := now.AddDate(0, 0, -2) oldOrder := model.RentalOrder{ OrderNo: "ORDER-OLD", ListingID: 101, AccountID: 201, OwnerID: 301, RenterID: 401, Status: "completed", SettledAt: &oldSettledAt, } recentOrder := model.RentalOrder{ OrderNo: "ORDER-RECENT", ListingID: 102, AccountID: 202, OwnerID: 302, RenterID: 402, Status: "completed", SettledAt: &recentSettledAt, } if err := db.Create(&oldOrder).Error; err != nil { t.Fatalf("创建旧订单失败: %v", err) } if err := db.Create(&recentOrder).Error; err != nil { t.Fatalf("创建新订单失败: %v", err) } oldConv := model.ChatConversation{ ListingID: &oldOrder.ListingID, Type: chat.ConversationTypeListingGroup, Title: "旧发布群", Status: "active", } recentConv := model.ChatConversation{ ListingID: &recentOrder.ListingID, Type: chat.ConversationTypeListingGroup, Title: "新发布群", Status: "active", } if err := db.Create(&oldConv).Error; err != nil { t.Fatalf("创建旧会话失败: %v", err) } if err := db.Create(&recentConv).Error; err != nil { t.Fatalf("创建新会话失败: %v", err) } participants := []model.ChatParticipant{ {ConversationID: oldConv.ID, ParticipantType: "user", ParticipantID: oldOrder.RenterID, Role: "renter", JoinedAt: oldSettledAt}, {ConversationID: recentConv.ID, ParticipantType: "user", ParticipantID: recentOrder.RenterID, Role: "renter", JoinedAt: recentSettledAt}, } if err := db.Create(&participants).Error; err != nil { t.Fatalf("创建成员失败: %v", err) } count, err := job.handleEndedOrderRenterRetention(t.Context(), now, thresholds{ RenterRetentionDaysAfterOrderEnd: 5, }) if err != nil { t.Fatalf("清退任务失败: %v", err) } if count != 1 { t.Fatalf("清退数量 = %d, want 1", count) } var oldCount int64 if err := db.Model(&model.ChatParticipant{}). Where("conversation_id = ? AND participant_id = ? AND role = ?", oldConv.ID, oldOrder.RenterID, "renter"). Count(&oldCount).Error; err != nil { t.Fatalf("统计旧租客失败: %v", err) } if oldCount != 0 { t.Fatalf("旧租客成员数 = %d, want 0", oldCount) } var recentCount int64 if err := db.Model(&model.ChatParticipant{}). Where("conversation_id = ? AND participant_id = ? AND role = ?", recentConv.ID, recentOrder.RenterID, "renter"). Count(&recentCount).Error; err != nil { t.Fatalf("统计新租客失败: %v", err) } if recentCount != 1 { t.Fatalf("新租客成员数 = %d, want 1", recentCount) } var auditCount int64 if err := db.Model(&model.AuditLog{}). Where("action = ?", "chat.renter_retention.remove"). Count(&auditCount).Error; err != nil { t.Fatalf("统计清退审计失败: %v", err) } if auditCount != 1 { t.Fatalf("清退审计数 = %d, want 1", auditCount) } count, err = job.handleEndedOrderRenterRetention(t.Context(), now, thresholds{ RenterRetentionDaysAfterOrderEnd: 5, }) if err != nil { t.Fatalf("重复清退任务失败: %v", err) } if count != 0 { t.Fatalf("重复清退数量 = %d, want 0", count) } if err := db.Model(&model.AuditLog{}). Where("action = ?", "chat.renter_retention.remove"). Count(&auditCount).Error; err != nil { t.Fatalf("重复统计清退审计失败: %v", err) } if auditCount != 1 { t.Fatalf("重复清退审计数 = %d, want 1", auditCount) } } func TestHandleRenterConfirmTimeoutUsesRefreshedStageTime(t *testing.T) { db := setupOrderTimeoutTestDB(t) job := New(db, nil, zap.NewNop()) now := time.Date(2026, 6, 18, 12, 0, 0, 0, time.UTC) oldHandoffAt := now.Add(-2 * time.Hour) refreshedAt := now.Add(-5 * time.Minute) order := model.RentalOrder{ OrderNo: "ORDER-RENTER-CONFIRM-RESET", ListingID: 101, AccountID: 201, OwnerID: 301, RenterID: 401, Status: "pending_handoff", HandoffStatus: "pending_renter_confirm", HandoffStartedAt: &refreshedAt, } if err := db.Create(&order).Error; err != nil { t.Fatalf("创建订单失败: %v", err) } record := model.HandoffRecord{ OrderID: order.ID, FromUserID: order.OwnerID, ToUserID: order.RenterID, Type: "owner_handoff", Content: "交接说明", CreatedAt: oldHandoffAt, } if err := db.Create(&record).Error; err != nil { t.Fatalf("创建交接记录失败: %v", err) } count, err := job.handleRenterConfirmTimeout(t.Context(), now, thresholds{ RenterConfirmTimeoutMinutes: 30, }) if err != nil { t.Fatalf("确认收号超时任务失败: %v", err) } if count != 0 { t.Fatalf("处理数量 = %d, want 0", count) } var saved model.RentalOrder if err := db.First(&saved, order.ID).Error; err != nil { t.Fatalf("读取订单失败: %v", err) } if saved.Status != "pending_handoff" || saved.HandoffStatus != "pending_renter_confirm" { t.Fatalf("订单状态 = %s/%s, want pending_handoff/pending_renter_confirm", saved.Status, saved.HandoffStatus) } } func TestHandleOwnerReturnConfirmTimeoutUsesStageTime(t *testing.T) { db := setupOrderTimeoutTestDB(t) job := New(db, nil, zap.NewNop()) now := time.Date(2026, 6, 18, 12, 0, 0, 0, time.UTC) oldUpdatedAt := now.Add(-2 * time.Hour) refreshedAt := now.Add(-5 * time.Minute) order := model.RentalOrder{ OrderNo: "ORDER-CHECKOUT-CONFIRM-RESET", ListingID: 102, AccountID: 202, OwnerID: 302, RenterID: 402, Status: "pending_checkout_confirm", HandoffStatus: "pending_owner_checkout", HandoffStartedAt: &refreshedAt, UpdatedAt: oldUpdatedAt, } if err := db.Create(&order).Error; err != nil { t.Fatalf("创建订单失败: %v", err) } if err := db.Model(&model.RentalOrder{}).Where("id = ?", order.ID).Update("updated_at", oldUpdatedAt).Error; err != nil { t.Fatalf("回写旧更新时间失败: %v", err) } count, err := job.handleOwnerReturnConfirmTimeout(t.Context(), now, thresholds{ OwnerReturnConfirmTimeoutMinutes: 30, }) if err != nil { t.Fatalf("号主确认结账超时任务失败: %v", err) } if count != 0 { t.Fatalf("处理数量 = %d, want 0", count) } var saved model.RentalOrder if err := db.First(&saved, order.ID).Error; err != nil { t.Fatalf("读取订单失败: %v", err) } if saved.Status != "pending_checkout_confirm" || saved.HandoffStatus != "pending_owner_checkout" { t.Fatalf("订单状态 = %s/%s, want pending_checkout_confirm/pending_owner_checkout", saved.Status, saved.HandoffStatus) } } func TestClampRenterRetentionDays(t *testing.T) { tests := []struct { value int want int }{ {value: 1, want: 3}, {value: 5, want: 5}, {value: 9, want: 7}, } for _, tt := range tests { if got := clampRenterRetentionDays(tt.value); got != tt.want { t.Fatalf("clampRenterRetentionDays(%d) = %d, want %d", tt.value, got, tt.want) } } }