From 5b52acdd6cdb5836d17cd9b2e999f3352b35212d Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 12 Jul 2026 15:31:01 +0800 Subject: [PATCH] refactor(zone): remove legacy route domain storage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 第二阶段清理:删除 of_managed_domains 与 of_proxy_routes 冗余域名/证书列, 移除 ManagedDomain 模型与 API、路由侧 legacy 字段维护,以及前端 WebsiteService。 ImportLegacy 在旧列/旧表缺失时跳过对应源,保持幂等。 --- docs/docs.go | 83 ------ docs/plan/20260712-zone-domain-refactor.md | 20 +- docs/swagger.json | 83 ------ docs/swagger.yaml | 54 ---- .../components/certificate-editor-dialog.tsx | 3 +- .../components/certificate-import-dialog.tsx | 3 +- frontend/lib/services/index.ts | 4 - frontend/lib/services/openflare/index.ts | 9 +- frontend/lib/services/openflare/types.ts | 28 -- .../lib/services/openflare/website.service.ts | 35 --- .../certificate_snapshot_test.go | 2 +- .../openflare/config_version/logics_test.go | 6 - .../config_version/pages_snapshot_test.go | 2 - .../openflare/proxy_route/build_helpers.go | 2 - internal/apps/openflare/proxy_route/logics.go | 32 --- internal/apps/openflare/tls/errs.go | 5 - internal/apps/openflare/tls/logics_test.go | 49 ---- internal/apps/openflare/tls/managed_domain.go | 242 ------------------ internal/apps/openflare/tls/routers.go | 129 +--------- .../apps/openflare/uptimekuma/sync_test.go | 10 - internal/apps/openflare/zone/legacy_import.go | 84 ++++-- ...30001_drop_legacy_route_domain_columns.sql | 35 +++ ...30001_drop_legacy_route_domain_columns.sql | 138 ++++++++++ internal/db/migrator/migrator_test.go | 18 ++ internal/model/openflare_managed_domain.go | 94 ------- internal/model/openflare_proxy_route.go | 17 +- 26 files changed, 272 insertions(+), 915 deletions(-) delete mode 100644 frontend/lib/services/openflare/website.service.ts delete mode 100644 internal/apps/openflare/tls/managed_domain.go create mode 100644 internal/db/migrator/goose/postgres/202607130001_drop_legacy_route_domain_columns.sql create mode 100644 internal/db/migrator/goose/sqlite/202607130001_drop_legacy_route_domain_columns.sql delete mode 100644 internal/model/openflare_managed_domain.go diff --git a/docs/docs.go b/docs/docs.go index ceb1a18e..789eaea2 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -14404,32 +14404,6 @@ const docTemplate = `{ } } }, - "model.ManagedDomain": { - "type": "object", - "properties": { - "cert_id": { - "type": "integer" - }, - "created_at": { - "type": "string" - }, - "domain": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "id": { - "type": "integer" - }, - "remark": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, "model.OpenFlareApplyLog": { "type": "object", "properties": { @@ -17959,63 +17933,6 @@ const docTemplate = `{ } } }, - "tls.ManagedDomainInput": { - "type": "object", - "properties": { - "cert_id": { - "type": "integer" - }, - "domain": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "remark": { - "type": "string" - } - } - }, - "tls.ManagedDomainMatchCandidate": { - "type": "object", - "properties": { - "certificate_id": { - "type": "integer" - }, - "certificate_name": { - "type": "string" - }, - "domain": { - "type": "string" - }, - "managed_domain_id": { - "type": "integer" - }, - "match_type": { - "type": "string" - } - } - }, - "tls.ManagedDomainMatchResult": { - "type": "object", - "properties": { - "candidate": { - "$ref": "#/definitions/tls.ManagedDomainMatchCandidate" - }, - "candidates": { - "type": "array", - "items": { - "$ref": "#/definitions/tls.ManagedDomainMatchCandidate" - } - }, - "domain": { - "type": "string" - }, - "matched": { - "type": "boolean" - } - } - }, "updater.Status": { "type": "object", "properties": { diff --git a/docs/plan/20260712-zone-domain-refactor.md b/docs/plan/20260712-zone-domain-refactor.md index 0912d32a..615f97f0 100644 --- a/docs/plan/20260712-zone-domain-refactor.md +++ b/docs/plan/20260712-zone-domain-refactor.md @@ -416,27 +416,27 @@ git commit -m "refactor(web): select route domains from zones" - Modify: generated `docs/{docs.go,swagger.json,swagger.yaml}` - Create: `docs/guide/zone-domain-migration.md` -- [ ] **Step 1: 为导入命令写可操作迁移指南** +- [x] **Step 1: 为导入命令写可操作迁移指南** 文档写明备份、执行 `wavelet migrate-zones`、读取导入报告、发布预览、比较 `server_name`/证书支持文件、发布激活和回滚步骤;不允许在报告有冲突时继续。 -- [ ] **Step 2: 生成 Swagger 和更新未发布变更** +- [x] **Step 2: 生成 Swagger 和更新未发布变更** Run: `make swagger` 在 `[Unreleased]` 记录 Zone 管理、反代路由域名正规化和移除 managed-domain API。 -- [ ] **Step 3: 运行全量质量门禁** +- [x] **Step 3: 运行全量质量门禁** Run: `go test ./... && make code-check` Expected: PASS。 -- [ ] **Step 4: 做快照等价性验收** +- [x] **Step 4: 做快照等价性验收** 在升级前导出活动版本,在导入后生成预览;逐个比较所有路由的明确 `server_name` 集合、证书路径、WAF RouteID 绑定与 Pages 部署引用。只允许旧快照的域名/证书冗余 JSON 消失,不允许数据面语义变化。 -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add docs @@ -455,7 +455,7 @@ git commit -m "docs(zone): add migration and release verification guide" - Delete: `internal/apps/openflare/tls/helpers.go` 中仅用于旧路由证书数组的函数 - Modify: legacy迁移相关测试、模型测试与 `docs/design/zone-design.md` -- [ ] **Step 1: 写空库与升级库清理失败测试** +- [x] **Step 1: 写空库与升级库清理失败测试** ```go func TestLegacyRouteColumnsAreAbsentAfterCleanup(t *testing.T) { @@ -463,21 +463,21 @@ func TestLegacyRouteColumnsAreAbsentAfterCleanup(t *testing.T) { } ``` -- [ ] **Step 2: 编写双方言清理 DDL** +- [x] **Step 2: 编写双方言清理 DDL** PostgreSQL 删除旧唯一索引和 `domain`、`domains`、`cert_id`、`cert_ids`、`domain_cert_ids`,再删除 `of_managed_domains`;SQLite 使用重建 `of_proxy_routes` 表的迁移方式保留所有非旧字段与索引。Down 仅在开发数据库恢复旧结构,不回填历史数据。 -- [ ] **Step 3: 删除旧读取代码与测试 fixture** +- [x] **Step 3: 删除旧读取代码与测试 fixture** 删除所有 `route.Domain`、`route.Domains`、`route.CertID`、`route.CertIDs`、`route.DomainCertIDs` 的持久化引用;让编译器、Uptime Kuma、Flared、来源摘要及 API 只使用 ZoneDomain 查询结果。 -- [ ] **Step 4: 验证升级和完整回归** +- [x] **Step 4: 验证升级和完整回归** Run: `go test ./internal/db/migrator ./internal/model ./internal/apps/openflare/... ./pkg/render/openresty -count=1 && make code-check` Expected: PASS;全仓搜索不再发现旧 `ManagedDomain` 业务代码、`ProxyRoute` 持久化字段或管理端 API;渲染快照中的临时 `DomainCertIDs` 类型允许保留。 -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add internal/db/migrator internal/model internal/apps frontend docs diff --git a/docs/swagger.json b/docs/swagger.json index a98e7191..d3af4c5b 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -14397,32 +14397,6 @@ } } }, - "model.ManagedDomain": { - "type": "object", - "properties": { - "cert_id": { - "type": "integer" - }, - "created_at": { - "type": "string" - }, - "domain": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "id": { - "type": "integer" - }, - "remark": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, "model.OpenFlareApplyLog": { "type": "object", "properties": { @@ -17952,63 +17926,6 @@ } } }, - "tls.ManagedDomainInput": { - "type": "object", - "properties": { - "cert_id": { - "type": "integer" - }, - "domain": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "remark": { - "type": "string" - } - } - }, - "tls.ManagedDomainMatchCandidate": { - "type": "object", - "properties": { - "certificate_id": { - "type": "integer" - }, - "certificate_name": { - "type": "string" - }, - "domain": { - "type": "string" - }, - "managed_domain_id": { - "type": "integer" - }, - "match_type": { - "type": "string" - } - } - }, - "tls.ManagedDomainMatchResult": { - "type": "object", - "properties": { - "candidate": { - "$ref": "#/definitions/tls.ManagedDomainMatchCandidate" - }, - "candidates": { - "type": "array", - "items": { - "$ref": "#/definitions/tls.ManagedDomainMatchCandidate" - } - }, - "domain": { - "type": "string" - }, - "matched": { - "type": "boolean" - } - } - }, "updater.Status": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index f2795d8d..da3e679d 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1104,23 +1104,6 @@ definitions: id: type: integer type: object - model.ManagedDomain: - properties: - cert_id: - type: integer - created_at: - type: string - domain: - type: string - enabled: - type: boolean - id: - type: integer - remark: - type: string - updated_at: - type: string - type: object model.OpenFlareApplyLog: properties: checksum: @@ -3471,43 +3454,6 @@ definitions: type: type: string type: object - tls.ManagedDomainInput: - properties: - cert_id: - type: integer - domain: - type: string - enabled: - type: boolean - remark: - type: string - type: object - tls.ManagedDomainMatchCandidate: - properties: - certificate_id: - type: integer - certificate_name: - type: string - domain: - type: string - managed_domain_id: - type: integer - match_type: - type: string - type: object - tls.ManagedDomainMatchResult: - properties: - candidate: - $ref: '#/definitions/tls.ManagedDomainMatchCandidate' - candidates: - items: - $ref: '#/definitions/tls.ManagedDomainMatchCandidate' - type: array - domain: - type: string - matched: - type: boolean - type: object updater.Status: properties: asset_name: diff --git a/frontend/app/(main)/websites/components/certificate-editor-dialog.tsx b/frontend/app/(main)/websites/components/certificate-editor-dialog.tsx index 532b7433..351d68f0 100644 --- a/frontend/app/(main)/websites/components/certificate-editor-dialog.tsx +++ b/frontend/app/(main)/websites/components/certificate-editor-dialog.tsx @@ -62,8 +62,7 @@ export function CertificateEditorDialog({ onSuccess: async (certificate) => { await Promise.all([ queryClient.invalidateQueries({queryKey: certificatesQueryKey}), - queryClient.invalidateQueries({queryKey: ['openflare', 'managed-domains']}), - ]); + ]); onSaved?.(certificate); handleClose(); }, diff --git a/frontend/app/(main)/websites/components/certificate-import-dialog.tsx b/frontend/app/(main)/websites/components/certificate-import-dialog.tsx index 23bbbe31..748c34a2 100644 --- a/frontend/app/(main)/websites/components/certificate-import-dialog.tsx +++ b/frontend/app/(main)/websites/components/certificate-import-dialog.tsx @@ -58,8 +58,7 @@ export function CertificateImportDialog({ const invalidateQueries = async () => { await Promise.all([ queryClient.invalidateQueries({queryKey: certificatesQueryKey}), - queryClient.invalidateQueries({queryKey: ['openflare', 'managed-domains']}), - ]); + ]); }; const resetFileForm = () => { diff --git a/frontend/lib/services/index.ts b/frontend/lib/services/index.ts index 52553230..10ee668a 100644 --- a/frontend/lib/services/index.ts +++ b/frontend/lib/services/index.ts @@ -41,7 +41,6 @@ import { TlsCertificateService, UptimeKumaService, WafService, - WebsiteService, ZoneDomainService, ZoneService, } from './openflare'; @@ -68,7 +67,6 @@ const services = { openflareApplyLog: ApplyLogService, openflareDashboard: DashboardService, openflareWaf: WafService, - openflareWebsite: WebsiteService, openflareZone: ZoneService, openflareZoneDomain: ZoneDomainService, openflareTls: TlsCertificateService, @@ -191,7 +189,6 @@ export { ApplyLogService, DashboardService, WafService, - WebsiteService, TlsCertificateService, DnsAccountService, PagesService, @@ -216,7 +213,6 @@ export type { WAFIPGroup, WAFRuleGroup, WAFSiteRuleGroups, - ManagedDomainItem, TlsCertificateItem, DnsAccountItem, PagesProject, diff --git a/frontend/lib/services/openflare/index.ts b/frontend/lib/services/openflare/index.ts index 76a88d73..4f18fe15 100644 --- a/frontend/lib/services/openflare/index.ts +++ b/frontend/lib/services/openflare/index.ts @@ -6,7 +6,6 @@ export { ConfigVersionService } from './config-version.service'; export { ApplyLogService } from './apply-log.service'; export { DashboardService } from './dashboard.service'; export { WafService } from './waf.service'; -export { WebsiteService } from './website.service'; export { ZoneDomainService, ZoneService, zoneQueryKey } from './zone.service'; export { TlsCertificateService } from './tls-certificate.service'; export { DnsAccountService } from './dns-account.service'; @@ -104,9 +103,6 @@ export type { AcmeAccountItem, DnsAccountItem, DnsAccountMutationPayload, - ManagedDomainItem, - ManagedDomainMatchResult, - ManagedDomainMutationPayload, ZoneDomainItem, ZoneDomainMutationPayload, ZoneItem, @@ -134,7 +130,7 @@ import {PagesService} from './pages.service'; import {ProxyRouteService} from './proxy-route.service'; import {TlsCertificateService} from './tls-certificate.service'; import {WafService} from './waf.service'; -import {WebsiteService} from './website.service'; +import {ZoneDomainService, ZoneService} from './zone.service'; export const openflareServices = { node: NodeService, @@ -143,7 +139,8 @@ export const openflareServices = { applyLog: ApplyLogService, dashboard: DashboardService, waf: WafService, - website: WebsiteService, + zone: ZoneService, + zoneDomain: ZoneDomainService, tlsCertificate: TlsCertificateService, dnsAccount: DnsAccountService, pages: PagesService, diff --git a/frontend/lib/services/openflare/types.ts b/frontend/lib/services/openflare/types.ts index 7a0f56f7..679a1ee9 100644 --- a/frontend/lib/services/openflare/types.ts +++ b/frontend/lib/services/openflare/types.ts @@ -998,37 +998,9 @@ export interface ZoneOverview { domains: ZoneDomainItem[]; } -export interface ManagedDomainItem { - id: number; - domain: string; - cert_id: number | null; - enabled: boolean; - remark: string; - created_at: string; - updated_at: string; -} -export interface ManagedDomainMutationPayload { - domain: string; - cert_id: number | null; - enabled: boolean; - remark: string; -} -export interface ManagedDomainMatchCandidate { - managed_domain_id: number; - domain: string; - match_type: 'exact' | 'wildcard' | string; - certificate_id: number; - certificate_name: string; -} -export interface ManagedDomainMatchResult { - domain: string; - matched: boolean; - candidate?: ManagedDomainMatchCandidate; - candidates: ManagedDomainMatchCandidate[]; -} export interface TlsCertificateItem { id: number; diff --git a/frontend/lib/services/openflare/website.service.ts b/frontend/lib/services/openflare/website.service.ts deleted file mode 100644 index 1617b7d2..00000000 --- a/frontend/lib/services/openflare/website.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import {OpenFlareBaseService} from './base.service'; -import type { - ManagedDomainItem, - ManagedDomainMatchResult, - ManagedDomainMutationPayload, -} from './types'; - -export class WebsiteService extends OpenFlareBaseService { - protected static override readonly basePath: string = '/api/v1/d/managed-domains'; - - static async list(): Promise { - return this.get('/'); - } - - static async create( - payload: ManagedDomainMutationPayload, - ): Promise { - return this.post('/', payload); - } - - static async update( - id: number, - payload: ManagedDomainMutationPayload, - ): Promise { - return this.post(`/${id}/update`, payload); - } - - static async deleteById(id: number): Promise { - return this.post(`/${id}/delete`); - } - - static async match(domain: string): Promise { - return this.get('/match', {domain}); - } -} \ No newline at end of file diff --git a/internal/apps/openflare/config_version/certificate_snapshot_test.go b/internal/apps/openflare/config_version/certificate_snapshot_test.go index f4a4e048..519e55aa 100644 --- a/internal/apps/openflare/config_version/certificate_snapshot_test.go +++ b/internal/apps/openflare/config_version/certificate_snapshot_test.go @@ -75,7 +75,7 @@ func TestBuildSnapshotReadsZoneDomainCertificates(t *testing.T) { second, err := oftls.CreateCertificate(ctx, oftls.CertificateInput{Name: "second", CertPEM: secondCertPEM, KeyPEM: secondKeyPEM}) require.NoError(t, err) - route := &model.ProxyRoute{SiteName: "tls-site", Domain: "legacy.invalid", OriginURL: "http://origin:8080", Upstreams: `["http://origin:8080"]`, Enabled: true, EnableHTTPS: true} + route := &model.ProxyRoute{SiteName: "tls-site", OriginURL: "http://origin:8080", Upstreams: `["http://origin:8080"]`, Enabled: true, EnableHTTPS: true} require.NoError(t, model.CreateProxyRouteRecord(ctx, route)) zone := &model.Zone{Domain: "example.com"} require.NoError(t, db.DB(ctx).Create(zone).Error) diff --git a/internal/apps/openflare/config_version/logics_test.go b/internal/apps/openflare/config_version/logics_test.go index 0981634f..d2127b1f 100644 --- a/internal/apps/openflare/config_version/logics_test.go +++ b/internal/apps/openflare/config_version/logics_test.go @@ -96,8 +96,6 @@ func TestPublishConfigVersionCreatesVersion(t *testing.T) { route := &model.ProxyRoute{ SiteName: "publish-site", - Domain: "publish.example.com", - Domains: `["publish.example.com"]`, OriginURL: "http://origin.publish.example.com:8080", Upstreams: `["http://origin.publish.example.com:8080"]`, Enabled: true, @@ -142,8 +140,6 @@ func TestBuildSnapshotWAFDocumentUsesNormalizedSiteNames(t *testing.T) { route := &model.ProxyRoute{ SiteName: "example.com", - Domain: "Example.COM", - Domains: `["example.com","www.example.com"]`, OriginURL: "http://origin.example.com:8080", Upstreams: `["http://origin.example.com:8080"]`, Enabled: true, @@ -204,8 +200,6 @@ func TestBuildCurrentConfigBundleEnablesGlobalPoWWithoutExplicitBinding(t *testi route := &model.ProxyRoute{ SiteName: "pow-global.example.com", - Domain: "pow-global.example.com", - Domains: `["pow-global.example.com"]`, OriginURL: "http://origin.example.com:8080", Upstreams: `["http://origin.example.com:8080"]`, Enabled: true, diff --git a/internal/apps/openflare/config_version/pages_snapshot_test.go b/internal/apps/openflare/config_version/pages_snapshot_test.go index dcf1fb45..bff94153 100644 --- a/internal/apps/openflare/config_version/pages_snapshot_test.go +++ b/internal/apps/openflare/config_version/pages_snapshot_test.go @@ -45,8 +45,6 @@ func TestBuildSnapshotRoutesPages(t *testing.T) { route := &model.ProxyRoute{ SiteName: "speedtest", - Domain: "speedtest.arctel.net", - Domains: `["speedtest.arctel.net"]`, OriginURL: "openflare-pages://project/1", Upstreams: `["openflare-pages://project/1"]`, Enabled: true, diff --git a/internal/apps/openflare/proxy_route/build_helpers.go b/internal/apps/openflare/proxy_route/build_helpers.go index a667afc8..ec618646 100644 --- a/internal/apps/openflare/proxy_route/build_helpers.go +++ b/internal/apps/openflare/proxy_route/build_helpers.go @@ -145,8 +145,6 @@ func applyProxyRouteUpstreamType(ctx context.Context, route *model.ProxyRoute, u func updateProxyRouteRecord(tx *gorm.DB, route *model.ProxyRoute) error { return tx.Model(&model.ProxyRoute{}).Where("id = ?", route.ID).Updates(map[string]any{ "site_name": route.SiteName, "origin_id": route.OriginID, "origin_url": route.OriginURL, - "domain": route.Domain, "domains": route.Domains, "cert_id": route.CertID, - "cert_ids": route.CertIDs, "domain_cert_ids": route.DomainCertIDs, "origin_host": route.OriginHost, "upstreams": route.Upstreams, "enabled": route.Enabled, "enable_https": route.EnableHTTPS, "redirect_http": route.RedirectHTTP, "limit_conn_per_server": route.LimitConnPerServer, "limit_conn_per_ip": route.LimitConnPerIP, diff --git a/internal/apps/openflare/proxy_route/logics.go b/internal/apps/openflare/proxy_route/logics.go index dfd1c921..e227090c 100644 --- a/internal/apps/openflare/proxy_route/logics.go +++ b/internal/apps/openflare/proxy_route/logics.go @@ -5,7 +5,6 @@ package proxy_route import ( "context" - "encoding/json" "errors" "strings" "time" @@ -253,43 +252,12 @@ func buildProxyRoute(ctx context.Context, route *model.ProxyRoute, input Input) limitRate, upstreamType, ) - // Preserve legacy columns until the second-phase schema cleanup. They are - // derived solely from ZoneDomain bindings and are not exposed by this API. - populateLegacyZoneDomainFields(route, domains) if err := applyProxyRouteUpstreamType(ctx, route, upstreamType, input); err != nil { return nil, nil, err } return route, domains, nil } -func mustMarshalProxyRouteLegacy(value any) string { - encoded, err := json.Marshal(value) - if err != nil { - panic(err) - } - return string(encoded) -} - -func populateLegacyZoneDomainFields(route *model.ProxyRoute, domains []model.ZoneDomain) { - legacyDomains := make([]string, 0, len(domains)) - legacyCertIDs := make([]uint, 0, len(domains)) - for _, domain := range domains { - legacyDomains = append(legacyDomains, domain.Domain) - if domain.CertID != nil { - legacyCertIDs = append(legacyCertIDs, *domain.CertID) - } - } - route.Domain = legacyDomains[0] - route.Domains = mustMarshalProxyRouteLegacy(legacyDomains) - route.CertIDs = mustMarshalProxyRouteLegacy(legacyCertIDs) - if len(legacyCertIDs) > 0 { - route.CertID = &legacyCertIDs[0] - } else { - route.CertID = nil - } - route.DomainCertIDs = route.CertIDs -} - func buildProxyRouteViews(ctx context.Context, routes []*model.ProxyRoute) ([]*View, error) { views := make([]*View, 0, len(routes)) for _, route := range routes { diff --git a/internal/apps/openflare/tls/errs.go b/internal/apps/openflare/tls/errs.go index 17df3709..98151a6e 100644 --- a/internal/apps/openflare/tls/errs.go +++ b/internal/apps/openflare/tls/errs.go @@ -17,11 +17,6 @@ const ( errCertificateFilesRequired = "certificate file and key file cannot be empty" errCertificatePEMInvalid = "证书 PEM 内容不合法" - errManagedDomainRequired = "域名不能为空" - errManagedDomainInvalid = "域名格式不合法" - errManagedDomainWildcardInvalid = "通配符域名仅支持 *.example.com 格式" - errManagedDomainExists = "域名已存在" - errManagedDomainCertNotFound = "所选证书不存在" errDNSAccountInUse = "该 DNS 账号已被证书使用,无法删除" ) diff --git a/internal/apps/openflare/tls/logics_test.go b/internal/apps/openflare/tls/logics_test.go index 79997c8e..89e2f24f 100644 --- a/internal/apps/openflare/tls/logics_test.go +++ b/internal/apps/openflare/tls/logics_test.go @@ -42,7 +42,6 @@ func setupTLSTestDB(t *testing.T) func() { &model.TLSCertificate{}, &model.Zone{}, &model.ZoneDomain{}, - &model.ManagedDomain{}, &model.DNSAccount{}, &model.AcmeAccount{}, &model.TaskExecution{}, // 异步任务执行记录也需要 migrate @@ -111,54 +110,6 @@ func generateTestCertificatePair(t *testing.T, dnsNames []string) (string, strin return string(certPEM), string(keyPEM) } -func TestCreateManagedDomain(t *testing.T) { - cleanup := setupTLSTestDB(t) - defer cleanup() - ctx := context.Background() - - certPEM, keyPEM := generateTestCertificatePair(t, []string{"api.example.com"}) - certificate, err := CreateCertificate(ctx, CertificateInput{ - Name: "api-cert", - CertPEM: certPEM, - KeyPEM: keyPEM, - }) - require.NoError(t, err) - - certID := certificate.ID - domain, err := CreateManagedDomain(ctx, ManagedDomainInput{ - Domain: "api.example.com", - CertID: &certID, - Enabled: true, - Remark: "primary api", - }) - require.NoError(t, err) - assert.NotZero(t, domain.ID) - assert.Equal(t, "api.example.com", domain.Domain) - assert.Equal(t, certID, *domain.CertID) - assert.True(t, domain.Enabled) - assert.Equal(t, "primary api", domain.Remark) - - _, err = CreateManagedDomain(ctx, ManagedDomainInput{ - Domain: "api.example.com", - Enabled: true, - }) - require.Error(t, err) - assert.Equal(t, errManagedDomainExists, err.Error()) -} - -func TestCreateManagedDomainRejectsInvalidWildcard(t *testing.T) { - cleanup := setupTLSTestDB(t) - defer cleanup() - ctx := context.Background() - - _, err := CreateManagedDomain(ctx, ManagedDomainInput{ - Domain: "*.*.example.com", - Enabled: true, - }) - require.Error(t, err) - assert.Equal(t, errManagedDomainWildcardInvalid, err.Error()) -} - func TestCreateCertificateEncryptsPrivateKey(t *testing.T) { cleanup := setupTLSTestDB(t) defer cleanup() diff --git a/internal/apps/openflare/tls/managed_domain.go b/internal/apps/openflare/tls/managed_domain.go deleted file mode 100644 index 14ca3295..00000000 --- a/internal/apps/openflare/tls/managed_domain.go +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package tls - -import ( - "context" - "errors" - "fmt" - "sort" - "strings" - "unicode" - - "github.com/Rain-kl/Wavelet/internal/model" -) - -const ( - managedDomainMatchTypeExact = "exact" - managedDomainMatchTypeWildcard = "wildcard" - - maxManagedDomainLength = 253 - minManagedDomainLabelCount = 2 -) - -// ManagedDomainInput 托管域名创建/更新请求。 -type ManagedDomainInput struct { - Domain string `json:"domain"` - CertID *uint `json:"cert_id"` - Enabled bool `json:"enabled"` - Remark string `json:"remark"` -} - -// ManagedDomainMatchCandidate 证书匹配候选。 -type ManagedDomainMatchCandidate struct { - ManagedDomainID uint `json:"managed_domain_id"` - Domain string `json:"domain"` - MatchType string `json:"match_type"` - CertificateID uint `json:"certificate_id"` - CertificateName string `json:"certificate_name"` -} - -// ManagedDomainMatchResult 证书匹配结果。 -type ManagedDomainMatchResult struct { - Domain string `json:"domain"` - Matched bool `json:"matched"` - Candidate *ManagedDomainMatchCandidate `json:"candidate,omitempty"` - Candidates []ManagedDomainMatchCandidate `json:"candidates"` -} - -// ListManagedDomains 列出托管域名。 -func ListManagedDomains(ctx context.Context) ([]model.ManagedDomain, error) { - return model.ListManagedDomains(ctx) -} - -// CreateManagedDomain 创建托管域名。 -func CreateManagedDomain(ctx context.Context, input ManagedDomainInput) (*model.ManagedDomain, error) { - domain, err := buildManagedDomain(ctx, nil, input) - if err != nil { - return nil, err - } - if err = model.CreateManagedDomainRecord(ctx, domain); err != nil { - if isUniqueConstraintError(err) { - return nil, errors.New(errManagedDomainExists) - } - return nil, err - } - return domain, nil -} - -// UpdateManagedDomain 更新托管域名。 -func UpdateManagedDomain(ctx context.Context, id uint, input ManagedDomainInput) (*model.ManagedDomain, error) { - domain, err := model.GetManagedDomainByID(ctx, id) - if err != nil { - return nil, err - } - domain, err = buildManagedDomain(ctx, domain, input) - if err != nil { - return nil, err - } - if err = model.SaveManagedDomain(ctx, domain); err != nil { - if isUniqueConstraintError(err) { - return nil, errors.New(errManagedDomainExists) - } - return nil, err - } - return domain, nil -} - -// DeleteManagedDomain 删除托管域名。 -func DeleteManagedDomain(ctx context.Context, id uint) error { - if _, err := model.GetManagedDomainByID(ctx, id); err != nil { - return err - } - return model.DeleteManagedDomainRecord(ctx, id) -} - -// MatchManagedDomainCertificate 为域名匹配证书。 -func MatchManagedDomainCertificate(ctx context.Context, rawDomain string) (*ManagedDomainMatchResult, error) { - domain := normalizeManagedDomain(rawDomain) - if err := validateManagedDomainPattern(domain); err != nil { - return nil, err - } - managedDomains, err := model.ListEnabledManagedDomainsWithCertificate(ctx) - if err != nil { - return nil, err - } - candidates := make([]ManagedDomainMatchCandidate, 0) - for _, item := range managedDomains { - if item.CertID == nil || *item.CertID == 0 { - continue - } - matchType := detectManagedDomainMatchType(item.Domain, domain) - if matchType == "" { - continue - } - certificate, err := model.GetTLSCertificateByID(ctx, *item.CertID) - if err != nil { - return nil, fmt.Errorf("托管域名 %s 关联证书不存在", item.Domain) - } - candidates = append(candidates, ManagedDomainMatchCandidate{ - ManagedDomainID: item.ID, - Domain: item.Domain, - MatchType: matchType, - CertificateID: certificate.ID, - CertificateName: certificate.Name, - }) - } - sortManagedDomainCandidates(candidates) - result := &ManagedDomainMatchResult{ - Domain: domain, - Matched: len(candidates) > 0, - Candidates: candidates, - } - if len(candidates) > 0 { - candidate := candidates[0] - result.Candidate = &candidate - } - return result, nil -} - -func buildManagedDomain(ctx context.Context, existing *model.ManagedDomain, input ManagedDomainInput) (*model.ManagedDomain, error) { - domain := normalizeManagedDomain(input.Domain) - remark := strings.TrimSpace(input.Remark) - if err := validateManagedDomainPattern(domain); err != nil { - return nil, err - } - if input.CertID != nil && *input.CertID != 0 { - if _, err := model.GetTLSCertificateByID(ctx, *input.CertID); err != nil { - return nil, errors.New(errManagedDomainCertNotFound) - } - } else { - input.CertID = nil - } - if existing == nil { - existing = &model.ManagedDomain{} - } - existing.Domain = domain - existing.CertID = input.CertID - existing.Enabled = input.Enabled - existing.Remark = remark - return existing, nil -} - -func normalizeManagedDomain(domain string) string { - return strings.ToLower(strings.TrimSpace(domain)) -} - -func validateManagedDomainPattern(domain string) error { - if domain == "" { - return errors.New(errManagedDomainRequired) - } - if strings.Contains(domain, "://") || strings.Contains(domain, "/") { - return errors.New(errManagedDomainInvalid) - } - if strings.Contains(domain, "*") { - if !strings.HasPrefix(domain, "*.") || strings.Count(domain, "*") != 1 { - return errors.New(errManagedDomainWildcardInvalid) - } - return validateHostname(strings.TrimPrefix(domain, "*.")) - } - return validateHostname(domain) -} - -func validateHostname(domain string) error { - if domain == "" { - return errors.New(errManagedDomainRequired) - } - if len(domain) > maxManagedDomainLength { - return errors.New(errManagedDomainInvalid) - } - labels := strings.Split(domain, ".") - if len(labels) < minManagedDomainLabelCount { - return errors.New(errManagedDomainInvalid) - } - for _, label := range labels { - if len(label) == 0 || len(label) > 63 { - return errors.New(errManagedDomainInvalid) - } - if label[0] == '-' || label[len(label)-1] == '-' { - return errors.New(errManagedDomainInvalid) - } - for _, r := range label { - if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-' { - continue - } - return errors.New(errManagedDomainInvalid) - } - } - return nil -} - -func detectManagedDomainMatchType(pattern string, domain string) string { - if pattern == domain { - return managedDomainMatchTypeExact - } - if !strings.HasPrefix(pattern, "*.") { - return "" - } - suffix := strings.TrimPrefix(pattern, "*.") - if !strings.HasSuffix(domain, "."+suffix) { - return "" - } - prefix := strings.TrimSuffix(domain, "."+suffix) - if prefix == "" || strings.Contains(prefix, ".") { - return "" - } - return managedDomainMatchTypeWildcard -} - -func sortManagedDomainCandidates(candidates []ManagedDomainMatchCandidate) { - sort.Slice(candidates, func(i int, j int) bool { - left := candidates[i] - right := candidates[j] - if left.MatchType != right.MatchType { - return left.MatchType == managedDomainMatchTypeExact - } - if len(left.Domain) != len(right.Domain) { - return len(left.Domain) > len(right.Domain) - } - return left.ManagedDomainID < right.ManagedDomainID - }) -} diff --git a/internal/apps/openflare/tls/routers.go b/internal/apps/openflare/tls/routers.go index 0136cad4..58c5c56c 100644 --- a/internal/apps/openflare/tls/routers.go +++ b/internal/apps/openflare/tls/routers.go @@ -4,12 +4,11 @@ package tls import ( - "net/http" - "strings" + "net/http" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" - "github.com/Rain-kl/Wavelet/internal/common/response" - "github.com/gin-gonic/gin" + "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" + "github.com/Rain-kl/Wavelet/internal/common/response" + "github.com/gin-gonic/gin" ) @@ -328,126 +327,6 @@ func RenewCertificateHandler(c *gin.Context) { c.JSON(http.StatusOK, response.OK(certificate)) } -// GetManagedDomains 列出托管域名。 -// @Summary 列出托管域名 -// @Description 返回全部托管域名及关联证书,需要管理员权限 -// @Tags openflare-tls -// @Produce json -// @Security SessionCookie -// @Success 200 {object} response.Any{data=[]model.ManagedDomain} "托管域名列表" -// @Failure 400 {object} response.Any "参数错误" -// @Failure 401 {object} response.Any "未登录" -// @Failure 404 {object} response.Any "无权限或不存在" -// @Failure 500 {object} response.Any "内部错误" -func GetManagedDomains(c *gin.Context) { - domains, err := ListManagedDomains(c.Request.Context()) - if handleLogicError(c, err) { - return - } - c.JSON(http.StatusOK, response.OK(domains)) -} - -// CreateManagedDomainHandler 创建托管域名。 -// @Summary 创建托管域名 -// @Description 创建新的托管域名记录,需要管理员权限 -// @Tags openflare-tls -// @Accept json -// @Produce json -// @Security SessionCookie -// @Param request body tls.ManagedDomainInput true "托管域名参数" -// @Success 200 {object} response.Any{data=model.ManagedDomain} "创建成功的托管域名" -// @Failure 400 {object} response.Any "参数错误" -// @Failure 401 {object} response.Any "未登录" -// @Failure 404 {object} response.Any "无权限或不存在" -// @Failure 500 {object} response.Any "内部错误" -func CreateManagedDomainHandler(c *gin.Context) { - var input ManagedDomainInput - if !apiutil.BindJSON(c, &input) { - return - } - domain, err := CreateManagedDomain(c.Request.Context(), input) - if handleLogicError(c, err) { - return - } - c.JSON(http.StatusOK, response.OK(domain)) -} - -// UpdateManagedDomainHandler 更新托管域名。 -// @Summary 更新托管域名 -// @Description 按 ID 更新托管域名,需要管理员权限 -// @Tags openflare-tls -// @Accept json -// @Produce json -// @Security SessionCookie -// @Param id path int true "托管域名 ID" -// @Param request body tls.ManagedDomainInput true "托管域名参数" -// @Success 200 {object} response.Any{data=model.ManagedDomain} "更新后的托管域名" -// @Failure 400 {object} response.Any "参数错误" -// @Failure 401 {object} response.Any "未登录" -// @Failure 404 {object} response.Any "无权限或不存在" -// @Failure 404 {object} response.Any "记录不存在" -// @Failure 500 {object} response.Any "内部错误" -func UpdateManagedDomainHandler(c *gin.Context) { - id, ok := apiutil.IDParam(c) - if !ok { - return - } - var input ManagedDomainInput - if !apiutil.BindJSON(c, &input) { - return - } - domain, err := UpdateManagedDomain(c.Request.Context(), id, input) - if handleLogicError(c, err) { - return - } - c.JSON(http.StatusOK, response.OK(domain)) -} - -// DeleteManagedDomainHandler 删除托管域名。 -// @Summary 删除托管域名 -// @Description 按 ID 删除托管域名,需要管理员权限 -// @Tags openflare-tls -// @Produce json -// @Security SessionCookie -// @Param id path int true "托管域名 ID" -// @Success 200 {object} response.Any "删除成功" -// @Failure 400 {object} response.Any "参数错误" -// @Failure 401 {object} response.Any "未登录" -// @Failure 404 {object} response.Any "无权限或不存在" -// @Failure 404 {object} response.Any "记录不存在" -// @Failure 500 {object} response.Any "内部错误" -func DeleteManagedDomainHandler(c *gin.Context) { - id, ok := apiutil.IDParam(c) - if !ok { - return - } - if err := DeleteManagedDomain(c.Request.Context(), id); handleLogicError(c, err) { - return - } - c.JSON(http.StatusOK, response.OKNil()) -} - -// MatchManagedDomainCertificateHandler 匹配域名证书。 -// @Summary 匹配托管域名证书 -// @Description 按域名查询可用的证书匹配候选,需要管理员权限 -// @Tags openflare-tls -// @Produce json -// @Security SessionCookie -// @Param domain query string true "域名" -// @Success 200 {object} response.Any{data=tls.ManagedDomainMatchResult} "证书匹配结果" -// @Failure 400 {object} response.Any "参数错误" -// @Failure 401 {object} response.Any "未登录" -// @Failure 404 {object} response.Any "无权限或不存在" -// @Failure 500 {object} response.Any "内部错误" -func MatchManagedDomainCertificateHandler(c *gin.Context) { - domain := strings.TrimSpace(c.Query("domain")) - result, err := MatchManagedDomainCertificate(c.Request.Context(), domain) - if handleLogicError(c, err) { - return - } - c.JSON(http.StatusOK, response.OK(result)) -} - // GetDNSAccounts 列出 DNS 账号。 // @Summary 列出 DNS 账号 // @Description 返回全部 DNS 提供商账号,需要管理员权限 diff --git a/internal/apps/openflare/uptimekuma/sync_test.go b/internal/apps/openflare/uptimekuma/sync_test.go index 537b01ab..60411fe1 100644 --- a/internal/apps/openflare/uptimekuma/sync_test.go +++ b/internal/apps/openflare/uptimekuma/sync_test.go @@ -200,24 +200,18 @@ func TestSyncToUptimeKumaSuccess(t *testing.T) { routeA := &model.ProxyRoute{ SiteName: "site-a", - Domain: "site-a.com", - Domains: `["site-a.com"]`, OriginURL: "http://10.0.0.1", Enabled: true, EnableHTTPS: false, } routeB := &model.ProxyRoute{ SiteName: "site-b", - Domain: "site-b.com", - Domains: `["site-b.com"]`, OriginURL: "https://10.0.0.2", Enabled: true, EnableHTTPS: true, } routeC := &model.ProxyRoute{ SiteName: "site-c", - Domain: "site-c.com", - Domains: `["site-c.com"]`, OriginURL: "http://10.0.0.3", Enabled: false, EnableHTTPS: false, @@ -315,16 +309,12 @@ func TestSyncToUptimeKumaSelectedScope(t *testing.T) { routeA := &model.ProxyRoute{ SiteName: "site-a", - Domain: "site-a.com", - Domains: `["site-a.com"]`, OriginURL: "http://10.0.0.1", Enabled: true, EnableHTTPS: false, } routeB := &model.ProxyRoute{ SiteName: "site-b", - Domain: "site-b.com", - Domains: `["site-b.com"]`, OriginURL: "http://10.0.0.2", Enabled: true, EnableHTTPS: false, diff --git a/internal/apps/openflare/zone/legacy_import.go b/internal/apps/openflare/zone/legacy_import.go index 3dcff6c9..1f665c89 100644 --- a/internal/apps/openflare/zone/legacy_import.go +++ b/internal/apps/openflare/zone/legacy_import.go @@ -36,8 +36,24 @@ type legacyDomain struct { Remark string } -// ImportLegacy imports legacy proxy-route names first, and managed domains only when routes contain no domains. -// ImportLegacy imports legacy records atomically after collecting validation conflicts. +// legacyRouteRow reads pre-cleanup of_proxy_routes columns via raw scan. +type legacyRouteRow struct { + ID uint `gorm:"column:id"` + Domain string `gorm:"column:domain"` + Domains string `gorm:"column:domains"` + DomainCertIDs string `gorm:"column:domain_cert_ids"` + Remark string `gorm:"column:remark"` +} + +// legacyManagedRow reads of_managed_domains while the table still exists. +type legacyManagedRow struct { + Domain string `gorm:"column:domain"` + CertID *uint `gorm:"column:cert_id"` + Remark string `gorm:"column:remark"` +} + +// ImportLegacy imports legacy proxy-route / managed-domain rows into Zone tables. +// After the phase-2 schema cleanup, missing legacy columns or tables are skipped. // //nolint:cyclop // the transactional importer intentionally validates every legacy source in one pass. func ImportLegacy(ctx context.Context) (report ImportReport, resultErr error) { @@ -46,40 +62,50 @@ func ImportLegacy(ctx context.Context) (report ImportReport, resultErr error) { return report, fmt.Errorf("database is not initialized") } resultErr = conn.Transaction(func(tx *gorm.DB) error { - var routes []model.ProxyRoute - if err := tx.Find(&routes).Error; err != nil { - return err - } items := make([]legacyDomain, 0) hasRouteDomains := false - for _, route := range routes { - domains, err := routeidentity.DecodeDomains(route.Domains, route.Domain) - if err != nil { - report.Conflicts = append(report.Conflicts, fmt.Sprintf("route %d: %v", route.ID, err)) - continue + + if tx.Migrator().HasColumn("of_proxy_routes", "domain") && + tx.Migrator().HasColumn("of_proxy_routes", "domains") { + var routes []legacyRouteRow + if err := tx.Table("of_proxy_routes"). + Select("id, domain, domains, domain_cert_ids, remark"). + Find(&routes).Error; err != nil { + return err } - if len(domains) > 0 { - hasRouteDomains = true - } - certIDs := decodeLegacyCertIDs(route.DomainCertIDs, len(domains)) - for i, domain := range domains { - var certID *uint - if i < len(certIDs) && certIDs[i] > 0 { - v := certIDs[i] - certID = &v + for _, route := range routes { + domains, err := routeidentity.DecodeDomains(route.Domains, route.Domain) + if err != nil { + report.Conflicts = append(report.Conflicts, fmt.Sprintf("route %d: %v", route.ID, err)) + continue + } + if len(domains) > 0 { + hasRouteDomains = true + } + certIDs := decodeLegacyCertIDs(route.DomainCertIDs, len(domains)) + for i, domain := range domains { + var certID *uint + if i < len(certIDs) && certIDs[i] > 0 { + v := certIDs[i] + certID = &v + } + items = append(items, legacyDomain{Domain: domain, CertID: certID, Remark: route.Remark}) } - items = append(items, legacyDomain{Domain: domain, CertID: certID, Remark: route.Remark}) } } - if !hasRouteDomains { - var legacy []model.ManagedDomain - if err := tx.Find(&legacy).Error; err != nil { + + if !hasRouteDomains && tx.Migrator().HasTable("of_managed_domains") { + var legacy []legacyManagedRow + if err := tx.Table("of_managed_domains"). + Select("domain, cert_id, remark"). + Find(&legacy).Error; err != nil { return err } for _, item := range legacy { - items = append(items, legacyDomain{Domain: item.Domain, CertID: item.CertID, Remark: item.Remark}) + items = append(items, legacyDomain(item)) } } + for _, item := range items { domain, err := normalizeDomain(item.Domain) if err != nil { @@ -121,7 +147,12 @@ func ImportLegacy(ctx context.Context) (report ImportReport, resultErr error) { continue } } - if err = tx.Create(&model.ZoneDomain{ZoneID: zone.ID, Domain: domain, CertID: item.CertID, Remark: item.Remark}).Error; err != nil { + if err = tx.Create(&model.ZoneDomain{ + ZoneID: zone.ID, + Domain: domain, + CertID: item.CertID, + Remark: item.Remark, + }).Error; err != nil { return err } report.Domains++ @@ -144,4 +175,5 @@ func decodeLegacyCertIDs(raw string, count int) []uint { } return values } + func isNotFound(err error) bool { return err == gorm.ErrRecordNotFound } diff --git a/internal/db/migrator/goose/postgres/202607130001_drop_legacy_route_domain_columns.sql b/internal/db/migrator/goose/postgres/202607130001_drop_legacy_route_domain_columns.sql new file mode 100644 index 00000000..7fe1d6cb --- /dev/null +++ b/internal/db/migrator/goose/postgres/202607130001_drop_legacy_route_domain_columns.sql @@ -0,0 +1,35 @@ +-- +goose Up +-- 第二阶段:删除反代路由冗余域名/证书列与托管域名表。 +-- 执行前必须完成 migrate-zones 且配置预览验收通过。 + +DROP INDEX IF EXISTS idx_of_proxy_routes_domain; + +ALTER TABLE of_proxy_routes DROP COLUMN IF EXISTS domain; +ALTER TABLE of_proxy_routes DROP COLUMN IF EXISTS domains; +ALTER TABLE of_proxy_routes DROP COLUMN IF EXISTS cert_id; +ALTER TABLE of_proxy_routes DROP COLUMN IF EXISTS cert_ids; +ALTER TABLE of_proxy_routes DROP COLUMN IF EXISTS domain_cert_ids; + +DROP TABLE IF EXISTS of_managed_domains; + +-- +goose Down +-- 仅恢复开发库结构,不回填历史域名/证书数据。 + +CREATE TABLE IF NOT EXISTS of_managed_domains ( + id BIGSERIAL PRIMARY KEY, + domain VARCHAR(255) NOT NULL, + cert_id BIGINT, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + remark VARCHAR(255) NOT NULL DEFAULT '', + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_of_managed_domains_domain ON of_managed_domains (domain); + +ALTER TABLE of_proxy_routes ADD COLUMN IF NOT EXISTS domain VARCHAR(255) NOT NULL DEFAULT ''; +ALTER TABLE of_proxy_routes ADD COLUMN IF NOT EXISTS domains TEXT NOT NULL DEFAULT '[]'; +ALTER TABLE of_proxy_routes ADD COLUMN IF NOT EXISTS cert_id BIGINT; +ALTER TABLE of_proxy_routes ADD COLUMN IF NOT EXISTS cert_ids TEXT NOT NULL DEFAULT '[]'; +ALTER TABLE of_proxy_routes ADD COLUMN IF NOT EXISTS domain_cert_ids TEXT NOT NULL DEFAULT '[]'; + +CREATE UNIQUE INDEX IF NOT EXISTS idx_of_proxy_routes_domain ON of_proxy_routes (domain); diff --git a/internal/db/migrator/goose/sqlite/202607130001_drop_legacy_route_domain_columns.sql b/internal/db/migrator/goose/sqlite/202607130001_drop_legacy_route_domain_columns.sql new file mode 100644 index 00000000..69adac5f --- /dev/null +++ b/internal/db/migrator/goose/sqlite/202607130001_drop_legacy_route_domain_columns.sql @@ -0,0 +1,138 @@ +-- +goose Up +-- 第二阶段:重建 of_proxy_routes(去掉冗余域名/证书列)并删除 of_managed_domains。 + +PRAGMA foreign_keys=OFF; + +CREATE TABLE of_proxy_routes_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_name TEXT NOT NULL DEFAULT '', + origin_id INTEGER, + origin_url TEXT NOT NULL, + origin_host TEXT NOT NULL DEFAULT '', + upstreams TEXT NOT NULL DEFAULT '[]', + enabled INTEGER NOT NULL DEFAULT 1, + enable_https INTEGER NOT NULL DEFAULT 0, + redirect_http INTEGER NOT NULL DEFAULT 0, + limit_conn_per_server INTEGER NOT NULL DEFAULT 0, + limit_conn_per_ip INTEGER NOT NULL DEFAULT 0, + limit_rate TEXT NOT NULL DEFAULT '', + cache_enabled INTEGER NOT NULL DEFAULT 0, + cache_policy TEXT NOT NULL DEFAULT '', + cache_rules TEXT NOT NULL DEFAULT '[]', + custom_headers TEXT NOT NULL DEFAULT '[]', + basic_auth_enabled INTEGER NOT NULL DEFAULT 0, + basic_auth_username TEXT NOT NULL DEFAULT '', + basic_auth_password TEXT NOT NULL DEFAULT '', + remark TEXT NOT NULL DEFAULT '', + upstream_type TEXT NOT NULL DEFAULT 'direct', + tunnel_node_id INTEGER, + tunnel_target_addr TEXT NOT NULL DEFAULT '', + tunnel_target_protocol TEXT NOT NULL DEFAULT '', + pages_project_id INTEGER, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO of_proxy_routes_new ( + id, site_name, origin_id, origin_url, origin_host, upstreams, enabled, + enable_https, redirect_http, limit_conn_per_server, limit_conn_per_ip, limit_rate, + cache_enabled, cache_policy, cache_rules, custom_headers, basic_auth_enabled, + basic_auth_username, basic_auth_password, remark, upstream_type, tunnel_node_id, + tunnel_target_addr, tunnel_target_protocol, pages_project_id, created_at, updated_at +) +SELECT + id, site_name, origin_id, origin_url, origin_host, upstreams, enabled, + enable_https, redirect_http, limit_conn_per_server, limit_conn_per_ip, limit_rate, + cache_enabled, cache_policy, cache_rules, custom_headers, basic_auth_enabled, + basic_auth_username, basic_auth_password, remark, upstream_type, tunnel_node_id, + tunnel_target_addr, tunnel_target_protocol, pages_project_id, created_at, updated_at +FROM of_proxy_routes; + +DROP TABLE of_proxy_routes; +ALTER TABLE of_proxy_routes_new RENAME TO of_proxy_routes; + +CREATE UNIQUE INDEX IF NOT EXISTS idx_of_proxy_routes_site_name ON of_proxy_routes (site_name); +CREATE INDEX IF NOT EXISTS idx_of_proxy_routes_origin_id ON of_proxy_routes (origin_id); +CREATE INDEX IF NOT EXISTS idx_of_proxy_routes_tunnel_node_id ON of_proxy_routes (tunnel_node_id); +CREATE INDEX IF NOT EXISTS idx_of_proxy_routes_pages_project_id ON of_proxy_routes (pages_project_id); + +DROP TABLE IF EXISTS of_managed_domains; + +PRAGMA foreign_keys=ON; + +-- +goose Down +-- 仅恢复开发库结构,不回填历史域名/证书数据。 + +PRAGMA foreign_keys=OFF; + +CREATE TABLE IF NOT EXISTS of_managed_domains ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + domain TEXT NOT NULL, + cert_id INTEGER, + enabled INTEGER NOT NULL DEFAULT 1, + remark TEXT NOT NULL DEFAULT '', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_of_managed_domains_domain ON of_managed_domains (domain); + +CREATE TABLE of_proxy_routes_old ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_name TEXT NOT NULL DEFAULT '', + domain TEXT NOT NULL DEFAULT '', + domains TEXT NOT NULL DEFAULT '[]', + origin_id INTEGER, + origin_url TEXT NOT NULL, + origin_host TEXT NOT NULL DEFAULT '', + upstreams TEXT NOT NULL DEFAULT '[]', + enabled INTEGER NOT NULL DEFAULT 1, + enable_https INTEGER NOT NULL DEFAULT 0, + cert_id INTEGER, + cert_ids TEXT NOT NULL DEFAULT '[]', + domain_cert_ids TEXT NOT NULL DEFAULT '[]', + redirect_http INTEGER NOT NULL DEFAULT 0, + limit_conn_per_server INTEGER NOT NULL DEFAULT 0, + limit_conn_per_ip INTEGER NOT NULL DEFAULT 0, + limit_rate TEXT NOT NULL DEFAULT '', + cache_enabled INTEGER NOT NULL DEFAULT 0, + cache_policy TEXT NOT NULL DEFAULT '', + cache_rules TEXT NOT NULL DEFAULT '[]', + custom_headers TEXT NOT NULL DEFAULT '[]', + basic_auth_enabled INTEGER NOT NULL DEFAULT 0, + basic_auth_username TEXT NOT NULL DEFAULT '', + basic_auth_password TEXT NOT NULL DEFAULT '', + remark TEXT NOT NULL DEFAULT '', + upstream_type TEXT NOT NULL DEFAULT 'direct', + tunnel_node_id INTEGER, + tunnel_target_addr TEXT NOT NULL DEFAULT '', + tunnel_target_protocol TEXT NOT NULL DEFAULT '', + pages_project_id INTEGER, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO of_proxy_routes_old ( + id, site_name, domain, domains, origin_id, origin_url, origin_host, upstreams, enabled, + enable_https, cert_id, cert_ids, domain_cert_ids, redirect_http, limit_conn_per_server, + limit_conn_per_ip, limit_rate, cache_enabled, cache_policy, cache_rules, custom_headers, + basic_auth_enabled, basic_auth_username, basic_auth_password, remark, upstream_type, + tunnel_node_id, tunnel_target_addr, tunnel_target_protocol, pages_project_id, created_at, updated_at +) +SELECT + id, site_name, '', '[]', origin_id, origin_url, origin_host, upstreams, enabled, + enable_https, NULL, '[]', '[]', redirect_http, limit_conn_per_server, + limit_conn_per_ip, limit_rate, cache_enabled, cache_policy, cache_rules, custom_headers, + basic_auth_enabled, basic_auth_username, basic_auth_password, remark, upstream_type, + tunnel_node_id, tunnel_target_addr, tunnel_target_protocol, pages_project_id, created_at, updated_at +FROM of_proxy_routes; + +DROP TABLE of_proxy_routes; +ALTER TABLE of_proxy_routes_old RENAME TO of_proxy_routes; + +CREATE UNIQUE INDEX IF NOT EXISTS idx_of_proxy_routes_domain ON of_proxy_routes (domain); +CREATE UNIQUE INDEX IF NOT EXISTS idx_of_proxy_routes_site_name ON of_proxy_routes (site_name); +CREATE INDEX IF NOT EXISTS idx_of_proxy_routes_origin_id ON of_proxy_routes (origin_id); +CREATE INDEX IF NOT EXISTS idx_of_proxy_routes_tunnel_node_id ON of_proxy_routes (tunnel_node_id); +CREATE INDEX IF NOT EXISTS idx_of_proxy_routes_pages_project_id ON of_proxy_routes (pages_project_id); + +PRAGMA foreign_keys=ON; diff --git a/internal/db/migrator/migrator_test.go b/internal/db/migrator/migrator_test.go index 27238aa4..ae9a0b88 100644 --- a/internal/db/migrator/migrator_test.go +++ b/internal/db/migrator/migrator_test.go @@ -83,6 +83,24 @@ func TestMigrateInitializesSQLiteDatabase(t *testing.T) { if !sqliteDB.Migrator().HasTable("of_zone_domains") { t.Error("Migrate() did not create of_zone_domains") } + if sqliteDB.Migrator().HasTable("of_managed_domains") { + t.Error("Migrate() should drop of_managed_domains after phase-2 cleanup") + } + if sqliteDB.Migrator().HasColumn(&model.ProxyRoute{}, "domain") { + t.Error("Migrate() should drop of_proxy_routes.domain after phase-2 cleanup") + } + if sqliteDB.Migrator().HasColumn(&model.ProxyRoute{}, "domains") { + t.Error("Migrate() should drop of_proxy_routes.domains after phase-2 cleanup") + } + if sqliteDB.Migrator().HasColumn(&model.ProxyRoute{}, "cert_id") { + t.Error("Migrate() should drop of_proxy_routes.cert_id after phase-2 cleanup") + } + if sqliteDB.Migrator().HasColumn(&model.ProxyRoute{}, "cert_ids") { + t.Error("Migrate() should drop of_proxy_routes.cert_ids after phase-2 cleanup") + } + if sqliteDB.Migrator().HasColumn(&model.ProxyRoute{}, "domain_cert_ids") { + t.Error("Migrate() should drop of_proxy_routes.domain_cert_ids after phase-2 cleanup") + } zone := model.Zone{Domain: "example.com"} if err := sqliteDB.Create(&zone).Error; err != nil { diff --git a/internal/model/openflare_managed_domain.go b/internal/model/openflare_managed_domain.go deleted file mode 100644 index 1ebcb599..00000000 --- a/internal/model/openflare_managed_domain.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package model - -import ( - "context" - "errors" - "time" - - "github.com/Rain-kl/Wavelet/internal/db" -) - -// ManagedDomain OpenFlare 托管域名实体。 -type ManagedDomain struct { - ID uint `json:"id" gorm:"primaryKey;autoIncrement"` - Domain string `json:"domain" gorm:"uniqueIndex;size:255;not null"` - CertID *uint `json:"cert_id"` - Enabled bool `json:"enabled" gorm:"not null;default:true"` - Remark string `json:"remark" gorm:"size:255"` - CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` - UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` -} - -// TableName 表名。 -func (ManagedDomain) TableName() string { - return "of_managed_domains" -} - -// ListManagedDomains 列出全部托管域名。 -func ListManagedDomains(ctx context.Context) ([]ManagedDomain, error) { - conn := db.DB(ctx) - if conn == nil { - return nil, errors.New(errDatabaseNotInitialized) - } - var domains []ManagedDomain - if err := conn.Order("id desc").Find(&domains).Error; err != nil { - return nil, err - } - return domains, nil -} - -// ListEnabledManagedDomainsWithCertificate 列出已启用且绑定证书的托管域名。 -func ListEnabledManagedDomainsWithCertificate(ctx context.Context) ([]ManagedDomain, error) { - conn := db.DB(ctx) - if conn == nil { - return nil, errors.New(errDatabaseNotInitialized) - } - var domains []ManagedDomain - if err := conn.Where("enabled = ? AND cert_id IS NOT NULL", true).Order("id desc").Find(&domains).Error; err != nil { - return nil, err - } - return domains, nil -} - -// GetManagedDomainByID 按 ID 查询托管域名。 -func GetManagedDomainByID(ctx context.Context, id uint) (*ManagedDomain, error) { - conn := db.DB(ctx) - if conn == nil { - return nil, errors.New(errDatabaseNotInitialized) - } - var domain ManagedDomain - if err := conn.First(&domain, id).Error; err != nil { - return nil, err - } - return &domain, nil -} - -// CreateManagedDomainRecord 创建托管域名。 -func CreateManagedDomainRecord(ctx context.Context, domain *ManagedDomain) error { - conn := db.DB(ctx) - if conn == nil { - return errors.New(errDatabaseNotInitialized) - } - return conn.Create(domain).Error -} - -// SaveManagedDomain 保存托管域名。 -func SaveManagedDomain(ctx context.Context, domain *ManagedDomain) error { - conn := db.DB(ctx) - if conn == nil { - return errors.New(errDatabaseNotInitialized) - } - return conn.Save(domain).Error -} - -// DeleteManagedDomainRecord 删除托管域名。 -func DeleteManagedDomainRecord(ctx context.Context, id uint) error { - conn := db.DB(ctx) - if conn == nil { - return errors.New(errDatabaseNotInitialized) - } - return conn.Delete(&ManagedDomain{}, id).Error -} diff --git a/internal/model/openflare_proxy_route.go b/internal/model/openflare_proxy_route.go index c2aa899c..1e37a133 100644 --- a/internal/model/openflare_proxy_route.go +++ b/internal/model/openflare_proxy_route.go @@ -11,22 +11,16 @@ import ( ) // ProxyRoute OpenFlare 代理规则实体。 +// 域名与证书仅通过 of_zone_domains 关联,不再持久化在本表。 type ProxyRoute struct { - ID uint `json:"id" gorm:"primaryKey;autoIncrement"` - SiteName string `json:"site_name" gorm:"size:255;not null;default:''"` - // Legacy mirrors are maintained from ZoneDomain bindings until the staged - // schema cleanup. They are not route API fields. - Domain string `json:"-" gorm:"uniqueIndex;size:255;not null"` - Domains string `json:"-" gorm:"type:text;not null;default:'[]'"` + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + SiteName string `json:"site_name" gorm:"size:255;not null;default:''"` OriginID *uint `json:"origin_id" gorm:"index"` OriginURL string `json:"origin_url" gorm:"size:2048;not null"` OriginHost string `json:"origin_host" gorm:"size:255"` Upstreams string `json:"upstreams" gorm:"type:text;not null;default:'[]'"` Enabled bool `json:"enabled" gorm:"not null;default:true"` EnableHTTPS bool `json:"enable_https" gorm:"column:enable_https;not null;default:false"` - CertID *uint `json:"-"` - CertIDs string `json:"-" gorm:"type:text;not null;default:'[]'"` - DomainCertIDs string `json:"-" gorm:"type:text;not null;default:'[]'"` RedirectHTTP bool `json:"redirect_http" gorm:"not null;default:false"` LimitConnPerServer int `json:"limit_conn_per_server" gorm:"not null;default:0"` LimitConnPerIP int `json:"limit_conn_per_ip" gorm:"not null;default:0"` @@ -81,17 +75,12 @@ func CreateProxyRouteRecord(ctx context.Context, route *ProxyRoute) error { func UpdateProxyRouteRecord(ctx context.Context, route *ProxyRoute) error { return db.DB(ctx).Model(&ProxyRoute{}).Where("id = ?", route.ID).Updates(map[string]any{ "site_name": route.SiteName, - "domain": route.Domain, - "domains": route.Domains, "origin_id": route.OriginID, "origin_url": route.OriginURL, "origin_host": route.OriginHost, "upstreams": route.Upstreams, colEnabled: route.Enabled, "enable_https": route.EnableHTTPS, - "cert_id": route.CertID, - "cert_ids": route.CertIDs, - "domain_cert_ids": route.DomainCertIDs, "redirect_http": route.RedirectHTTP, "limit_conn_per_server": route.LimitConnPerServer, "limit_conn_per_ip": route.LimitConnPerIP,