diff --git a/backend/internal/app/certificate_workflow.go b/backend/internal/app/certificate_workflow.go index 3f67427..2cd7f66 100644 --- a/backend/internal/app/certificate_workflow.go +++ b/backend/internal/app/certificate_workflow.go @@ -231,6 +231,9 @@ func (w *CertificateWorkflow) deploy(ctx context.Context, result certificate.Iss if err != nil { return deployment.Report{}, fmt.Errorf("查询域名部署服务器失败:%w", err) } + if len(servers) == 0 { + return deployment.Report{Status: "failed"}, errors.New("\u57df\u540d\u5c1a\u672a\u5173\u8054\u4efb\u4f55\u90e8\u7f72\u670d\u52a1\u5668") + } targets := make([]deployment.Target, 0, len(servers)) for _, server := range servers { resolved, resolveErr := deployment.ResolveServerPaths(server, domainItem.Name, string(domainItem.CertificateType)) diff --git a/backend/internal/app/certificate_workflow_test.go b/backend/internal/app/certificate_workflow_test.go index e5fcf82..7eba5e4 100644 --- a/backend/internal/app/certificate_workflow_test.go +++ b/backend/internal/app/certificate_workflow_test.go @@ -125,6 +125,24 @@ func TestCertificateWorkflowDeploysIssuedCertificateToAssociatedServers(t *testi } } +func TestCertificateWorkflowRejectsDomainWithoutDeploymentServers(t *testing.T) { + deployer := &workflowDeployer{report: deployment.Report{Total: 1, Success: 1, Status: "success"}} + state := &workflowDomainState{item: domain.Domain{ID: "domain-1", Name: "example.com", CertificateType: domain.CertificateSingle}} + workflow := &CertificateWorkflow{ + Domains: state, + Servers: workflowServerLister{}, + Deployment: deployer, + } + + report, err := workflow.deploy(context.Background(), certificate.IssueResult{DomainID: "domain-1"}) + if err == nil || !strings.Contains(err.Error(), "\u5c1a\u672a\u5173\u8054\u4efb\u4f55\u90e8\u7f72\u670d\u52a1\u5668") { + t.Fatalf("\u672a\u5173\u8054\u670d\u52a1\u5668\u65f6\u5e94\u8fd4\u56de\u660e\u786e\u9519\u8bef\xef\xbc\x9a%v", err) + } + if report.Status != "failed" || deployer.targets != nil { + t.Fatalf("\u672a\u5173\u8054\u670d\u52a1\u5668\u65f6\u4e0d\u5f97\u6267\u884c\u8fdc\u7a0b\u90e8\u7f72\xef\xbc\x9a%+v %+v", report, deployer.targets) + } +} + func TestCertificateWorkflowReturnsPartialDeploymentError(t *testing.T) { deployer := &workflowDeployer{report: deployment.Report{ Total: 2, Success: 1, Failed: 1, Status: "partial", diff --git a/backend/internal/deployment/service.go b/backend/internal/deployment/service.go index c97089b..413f05b 100644 --- a/backend/internal/deployment/service.go +++ b/backend/internal/deployment/service.go @@ -57,6 +57,10 @@ func NewService(factory ClientFactory) *Service { func (s *Service) Deploy(ctx context.Context, targets []Target, artifact Artifact) Report { report := Report{Total: len(targets), Results: make([]Result, 0, len(targets))} + if len(targets) == 0 { + report.Status = "failed" + return report + } if s == nil || s.factory == nil { report.Status = "failed" report.Failed = len(targets) diff --git a/backend/internal/deployment/service_test.go b/backend/internal/deployment/service_test.go index f096b1f..fbd667d 100644 --- a/backend/internal/deployment/service_test.go +++ b/backend/internal/deployment/service_test.go @@ -269,6 +269,14 @@ func TestDeployReportsPartialSuccess(t *testing.T) { } } +func TestDeployRejectsEmptyTargets(t *testing.T) { + service := NewService(fixedClientFactory{client: newMemoryClient()}) + report := service.Deploy(context.Background(), nil, testArtifact(t)) + if report.Status != "failed" || report.Total != 0 || report.Success != 0 || report.Failed != 0 { + t.Fatalf("\u7a7a\u90e8\u7f72\u76ee\u6807\u4e0d\u5f97\u88ab\u89c6\u4e3a\u6210\u529f\xef\xbc\x9a%+v", report) + } +} + func TestDeployCleansPartialPrivateKeyUploadWithoutReplacingCurrentFiles(t *testing.T) { client := newMemoryClient() client.uploadError = "key.pem" diff --git a/doc/Scert开发任务清单.md b/doc/Scert开发任务清单.md index 76f0d39..d3b3440 100644 --- a/doc/Scert开发任务清单.md +++ b/doc/Scert开发任务清单.md @@ -3702,3 +3702,14 @@ Linux Ubuntu 22.04 上的 `Scert-admin` 手动启停、无开机自启动、无 - [~] 进行中:P0-10 其他任务入口、跨数据库验证、崩溃恢复、重试幂等和统一复审。 - [ ] 未开始:P0-12 备份恢复、密码 SSH、浏览器清缓存、远端部署及最终生产门禁。 - [!] 总体验收仍为 **P0-03:完成,待复审**,不得把本地自动化结果标记为生产验收通过。 + + +## 本轮开发进度(2026-09-29,P0-10 空部署目标防护) + +总体验收位置继续保持 **P0-03:完成,待复审**;本轮仅继续完成 P0-10 批量部署边界防护,不代表 P0-10 统一复审通过或生产部署验收通过。 +- [x] 已完成,等待复审:批量部署目标为空时显式返回失败,不再把 0 台服务器错判为部署成功。 +- [x] 已完成,等待复审:证书工作流在域名未关联任何部署服务器时提前失败,不调用远程部署。 +- [x] 已完成:新增应用层与部署层回归测试;`go test ./...`、`go vet ./...` 和 `git diff --check` 均通过。 +- [~] 进行中:P0-10 其余任务入口、失败回滚结果持久化、跨数据库矩阵、崩溃恢复与统一复审仍需继续。 +- [ ] 未开始:P0-12 备份恢复、浏览器清缓存、密码 SSH、远端部署、真实 DNS/ACME 和最终生产门禁仍未完成。 +- [!] 总体验收不得升级:当前仍为 **P0-03:完成,待复审**,本轮没有新增 `[✓]` 复审通过项。 diff --git a/doc/Scert整改任务执行清单.md b/doc/Scert整改任务执行清单.md index 244c17e..2ce3786 100644 --- a/doc/Scert整改任务执行清单.md +++ b/doc/Scert整改任务执行清单.md @@ -1006,3 +1006,14 @@ P0-05 从 `[!]` 调整为 `[~]`,并未标记完成;P0-04 剩余门禁和全 - [~] 进行中:DNS 验证任务、批量部署结果、崩溃恢复、重试幂等、三数据库实机矩阵和统一复审。 - [ ] 未开始:P0-12 备份恢复、浏览器清缓存、密码 SSH、远端部署及最终生产门禁。 - [!] 总体验收不得升级,仍为 **P0-03:完成,待复审**,没有新增 `[✓]`。 + + +## 本轮开发进度(2026-09-29,P0-10 空部署目标防护) + +总体验收位置继续保持 **P0-03:完成,待复审**;本轮仅继续完成 P0-10 批量部署边界防护,不代表 P0-10 统一复审通过或生产部署验收通过。 +- [x] 已完成,等待复审:批量部署目标为空时显式返回失败,不再把 0 台服务器错判为部署成功。 +- [x] 已完成,等待复审:证书工作流在域名未关联任何部署服务器时提前失败,不调用远程部署。 +- [x] 已完成:新增应用层与部署层回归测试;`go test ./...`、`go vet ./...` 和 `git diff --check` 均通过。 +- [~] 进行中:P0-10 其余任务入口、失败回滚结果持久化、跨数据库矩阵、崩溃恢复与统一复审仍需继续。 +- [ ] 未开始:P0-12 备份恢复、浏览器清缓存、密码 SSH、远端部署、真实 DNS/ACME 和最终生产门禁仍未完成。 +- [!] 总体验收不得升级:当前仍为 **P0-03:完成,待复审**,本轮没有新增 `[✓]` 复审通过项。 diff --git a/doc/Scert整改合并优化清单.md b/doc/Scert整改合并优化清单.md index d43dfd2..ff1937f 100644 --- a/doc/Scert整改合并优化清单.md +++ b/doc/Scert整改合并优化清单.md @@ -1653,3 +1653,14 @@ P0-11 生产验收门禁 - [~] 进行中:P0-10 其他任务入口、跨数据库实机矩阵、崩溃恢复、重试幂等和统一复审仍需继续。 - [ ] 未开始:P0-12 备份恢复及最终生产门禁仍未完成。 - [!] 总体验收不得升级,当前仍为 **P0-03:完成,待复审**,没有新增 `[✓]` 复审通过项。 + + +## 本轮开发进度(2026-09-29,P0-10 空部署目标防护) + +总体验收位置继续保持 **P0-03:完成,待复审**;本轮仅继续完成 P0-10 批量部署边界防护,不代表 P0-10 统一复审通过或生产部署验收通过。 +- [x] 已完成,等待复审:批量部署目标为空时显式返回失败,不再把 0 台服务器错判为部署成功。 +- [x] 已完成,等待复审:证书工作流在域名未关联任何部署服务器时提前失败,不调用远程部署。 +- [x] 已完成:新增应用层与部署层回归测试;`go test ./...`、`go vet ./...` 和 `git diff --check` 均通过。 +- [~] 进行中:P0-10 其余任务入口、失败回滚结果持久化、跨数据库矩阵、崩溃恢复与统一复审仍需继续。 +- [ ] 未开始:P0-12 备份恢复、浏览器清缓存、密码 SSH、远端部署、真实 DNS/ACME 和最终生产门禁仍未完成。 +- [!] 总体验收不得升级:当前仍为 **P0-03:完成,待复审**,本轮没有新增 `[✓]` 复审通过项。 diff --git a/doc/基础整改任务规格.md b/doc/基础整改任务规格.md index 554af76..0bfd0d7 100644 --- a/doc/基础整改任务规格.md +++ b/doc/基础整改任务规格.md @@ -583,3 +583,14 @@ P0-06 与 P0-07 均维持 `[~]`,六阶段中的隔离、生产和最终验收 - [x] 本轮专项测试、静态检查通过,代码提交 `9741721` 已推送。 - [~] P0-10 其他任务链、跨数据库实机行为、崩溃恢复、重试幂等和统一复审继续进行。 - [!] 本地测试结果不得替代真实 DNS/ACME、多服务器推送、密码 SSH 和最终生产门禁。 + + +## 本轮开发进度(2026-09-29,P0-10 空部署目标防护) + +总体验收位置继续保持 **P0-03:完成,待复审**;本轮仅继续完成 P0-10 批量部署边界防护,不代表 P0-10 统一复审通过或生产部署验收通过。 +- [x] 已完成,等待复审:批量部署目标为空时显式返回失败,不再把 0 台服务器错判为部署成功。 +- [x] 已完成,等待复审:证书工作流在域名未关联任何部署服务器时提前失败,不调用远程部署。 +- [x] 已完成:新增应用层与部署层回归测试;`go test ./...`、`go vet ./...` 和 `git diff --check` 均通过。 +- [~] 进行中:P0-10 其余任务入口、失败回滚结果持久化、跨数据库矩阵、崩溃恢复与统一复审仍需继续。 +- [ ] 未开始:P0-12 备份恢复、浏览器清缓存、密码 SSH、远端部署、真实 DNS/ACME 和最终生产门禁仍未完成。 +- [!] 总体验收不得升级:当前仍为 **P0-03:完成,待复审**,本轮没有新增 `[✓]` 复审通过项。