package service import ( "context" "encoding/json" "errors" "testing" "affiliate_dash/internal/model" ) func createDeleteTestToken(t *testing.T, svc *RockService, upstreamID, status string) model.RockToken { t.Helper() token := model.RockToken{ UpstreamTokenID: upstreamID, Token: "tk-" + upstreamID, RedeemCode: "rc-" + upstreamID, Color: model.RockColorBlack, Status: status, } if status == model.RockStatusUsing || status == model.RockStatusBound || status == model.RockStatusMismatch { token.AssignedOrderNo = "ORDER-" + upstreamID } if err := svc.db.Create(&token).Error; err != nil { t.Fatalf("创建 Token 失败: %v", err) } return token } func TestDeleteRockTokens_SoftDeletesAndHidesFromList(t *testing.T) { db := newRockTestDB(t) svc := NewRockService(db, NewRockClient("https://example.com")) ctx := context.Background() available := createDeleteTestToken(t, svc, "90001", model.RockStatusAvailable) redeemed := createDeleteTestToken(t, svc, "90002", model.RockStatusRedeemed) result, err := svc.DeleteRockTokens(ctx, []uint{available.ID}, false, 7) if err != nil { t.Fatalf("删除 Token 失败: %v", err) } if result.Deleted != 1 || result.Requested != 1 { t.Fatalf("删除数量不符: %+v", result) } _, total, err := svc.ListTokens(ctx, 1, 20, "", "") if err != nil { t.Fatalf("查询列表失败: %v", err) } if total != 1 { t.Fatalf("软删除后列表应只剩 1 条,实际 %d", total) } stats, err := svc.GetStats(ctx) if err != nil { t.Fatalf("查询统计失败: %v", err) } if stats.AvailableStock != 0 || stats.RedeemedStock != 1 { t.Fatalf("统计未排除软删除: available=%d redeemed=%d", stats.AvailableStock, stats.RedeemedStock) } var all []model.RockToken if err := db.Unscoped().Order("id ASC").Find(&all).Error; err != nil { t.Fatalf("Unscoped 查询失败: %v", err) } if len(all) != 2 { t.Fatalf("软删除不应物理删除,实际记录数 %d", len(all)) } var deleted model.RockToken if err := db.Unscoped().First(&deleted, available.ID).Error; err != nil { t.Fatalf("读取已删除记录失败: %v", err) } if !deleted.DeletedAt.Valid { t.Fatal("available Token 未写入 deleted_at") } var active model.RockToken if err := db.First(&active, available.ID).Error; err == nil { t.Fatal("普通查询不应返回已软删除 Token") } _ = redeemed } func TestDeleteRockTokens_RequiresForceForDangerousStatuses(t *testing.T) { db := newRockTestDB(t) svc := NewRockService(db, NewRockClient("https://example.com")) ctx := context.Background() using := createDeleteTestToken(t, svc, "91001", model.RockStatusUsing) bound := createDeleteTestToken(t, svc, "91002", model.RockStatusBound) mismatch := createDeleteTestToken(t, svc, "91003", model.RockStatusMismatch) ids := []uint{using.ID, bound.ID, mismatch.ID} if _, err := svc.DeleteRockTokens(ctx, ids, false, 7); err == nil { t.Fatal("包含危险状态时未强制确认应失败") } else { var forceErr *RockTokenForceRequiredError if !errors.As(err, &forceErr) { t.Fatalf("错误类型不是 ForceRequired: %T %v", err, err) } if forceErr.Dangerous != 3 { t.Fatalf("危险数量不符: %d", forceErr.Dangerous) } } var activeCount int64 if err := db.Model(&model.RockToken{}).Count(&activeCount).Error; err != nil { t.Fatalf("统计失败: %v", err) } if activeCount != 3 { t.Fatalf("强制确认失败时不应删除,实际剩余 %d", activeCount) } result, err := svc.DeleteRockTokens(ctx, ids, true, 7) if err != nil { t.Fatalf("强制删除失败: %v", err) } if result.Deleted != 3 { t.Fatalf("强制删除数量不符: %+v", result) } } func TestPreviewAndDeleteAllRockTokens_Filtered(t *testing.T) { db := newRockTestDB(t) svc := NewRockService(db, NewRockClient("https://example.com")) ctx := context.Background() createDeleteTestToken(t, svc, "92001", model.RockStatusRedeemed) createDeleteTestToken(t, svc, "92002", model.RockStatusRedeemed) createDeleteTestToken(t, svc, "92003", model.RockStatusAvailable) preview, err := svc.PreviewRockTokenDelete(ctx, model.RockStatusRedeemed, "") if err != nil { t.Fatalf("预览失败: %v", err) } if preview.Total != 2 || preview.Dangerous != 0 || preview.Available != 0 { t.Fatalf("预览数量不符: %+v", preview) } // 预览数量与当前数据不一致时必须拒绝,避免误删。 if _, err := svc.DeleteAllRockTokens(ctx, model.RockStatusRedeemed, "", 1, false, 7); err == nil { t.Fatal("expected_count 不一致时应拒绝") } else { var conflict *RockTokenDeleteConflictError if !errors.As(err, &conflict) { t.Fatalf("错误类型不是 Conflict: %T %v", err, err) } } result, err := svc.DeleteAllRockTokens(ctx, model.RockStatusRedeemed, "", 2, false, 7) if err != nil { t.Fatalf("删除全部失败: %v", err) } if result.Deleted != 2 { t.Fatalf("删除数量不符: %+v", result) } _, total, err := svc.ListTokens(ctx, 1, 20, "", "") if err != nil { t.Fatalf("查询列表失败: %v", err) } if total != 1 { t.Fatalf("按状态删除后应剩 1 条 available,实际 %d", total) } } func TestPreviewDeleteAll_ReportsDangerousAndAvailable(t *testing.T) { db := newRockTestDB(t) svc := NewRockService(db, NewRockClient("https://example.com")) ctx := context.Background() createDeleteTestToken(t, svc, "93001", model.RockStatusAvailable) createDeleteTestToken(t, svc, "93002", model.RockStatusBound) createDeleteTestToken(t, svc, "93003", model.RockStatusRedeemed) preview, err := svc.PreviewRockTokenDelete(ctx, "", "") if err != nil { t.Fatalf("预览失败: %v", err) } if preview.Total != 3 || preview.Dangerous != 1 || preview.Available != 1 || !preview.RequiresForce { t.Fatalf("预览结果不符: %+v", preview) } if len(preview.DangerousItems) != 1 || preview.DangerousItems[0].Status != model.RockStatusBound { t.Fatalf("危险明细不符: %+v", preview.DangerousItems) } } func TestImportPackageDetail_DoesNotResurrectDeletedToken(t *testing.T) { db := newRockTestDB(t) svc := NewRockService(db, NewRockClient("https://example.com")) ctx := context.Background() packageID := int64(12345) token := model.RockToken{ UpstreamTokenID: "94001", Token: "tk-94001", RedeemCode: "rc-94001", Color: model.RockColorBlack, Status: model.RockStatusAvailable, PackageID: &packageID, Source: "package", } if err := db.Create(&token).Error; err != nil { t.Fatalf("创建 Token 失败: %v", err) } if _, err := svc.DeleteRockTokens(ctx, []uint{token.ID}, false, 7); err != nil { t.Fatalf("软删除失败: %v", err) } var detail RockPackageDetailResponse raw := `{ "package": {"id": 12345, "share_code": "share-test"}, "tokens": [ {"id": 94001, "token": "tk-94001", "redeem_code": "rc-94001", "color": "black", "status": "available", "created_at": "2026-10-31 10:00:00"} ] }` if err := json.Unmarshal([]byte(raw), &detail); err != nil { t.Fatalf("解析包详情失败: %v", err) } inserted, updated, skipped, err := svc.importPackageDetail(ctx, &detail, nil) if err != nil { t.Fatalf("导入包详情失败: %v", err) } if inserted != 0 || updated != 0 || skipped != 1 { t.Fatalf("已删除 Token 不应被导入: inserted=%d updated=%d skipped=%d", inserted, updated, skipped) } var activeCount int64 if err := db.Model(&model.RockToken{}).Where("upstream_token_id = ?", "94001").Count(&activeCount).Error; err != nil { t.Fatalf("统计失败: %v", err) } if activeCount != 0 { t.Fatalf("已删除 Token 被重新创建,active=%d", activeCount) } var total int64 if err := db.Unscoped().Model(&model.RockToken{}).Where("upstream_token_id = ?", "94001").Count(&total).Error; err != nil { t.Fatalf("Unscoped 统计失败: %v", err) } if total != 1 { t.Fatalf("软删除记录应保留一条,实际 %d", total) } }