From d1070b699e4edf1fdb48e8256f97bb7c06fb8a86 Mon Sep 17 00:00:00 2001 From: xuans Date: Wed, 30 Sep 2026 02:55:42 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E8=AF=81=E4=B9=A6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=BC=80=E5=A7=8B=E5=AE=A1=E8=AE=A1=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 HTTP 证书申请任务增加开始执行审计;审计写入失败时收敛任务和尝试状态,补充回归测试并更新四份整改清单,验收位置继续保持 P0-03 完成待复审。 --- backend/internal/httpapi/task_api.go | 14 ++++++ backend/internal/httpapi/task_api_test.go | 53 ++++++++++++++++++++--- doc/Scert开发任务清单.md | 11 +++++ doc/Scert整改任务执行清单.md | 11 +++++ doc/Scert整改合并优化清单.md | 11 +++++ doc/基础整改任务规格.md | 11 +++++ 6 files changed, 105 insertions(+), 6 deletions(-) diff --git a/backend/internal/httpapi/task_api.go b/backend/internal/httpapi/task_api.go index 9f07632..e9af3d5 100644 --- a/backend/internal/httpapi/task_api.go +++ b/backend/internal/httpapi/task_api.go @@ -147,6 +147,20 @@ func (s *Server) executeCertificateIssueTask(c *gin.Context, task repository.Tas s.writeError(c, http.StatusInternalServerError, "更新任务状态失败", err) return } + if s.audit != nil { + if err := s.audit.CreateAuditLog(c.Request.Context(), repository.AuditLog{ + TaskID: task.ID, OperatorID: task.OperatorID, + Action: auditAction, ResourceType: "domain", ResourceID: task.DomainID, + Status: "running", Message: "开始申请证书并部署", + }); err != nil { + finished := time.Now().UTC() + failureMessage := "任务启动审计记录失败" + _ = s.tasks.UpdateTask(context.Background(), task.ID, "failed", "record", failureMessage, &now, &finished) + _ = s.tasks.FinishAttempt(context.Background(), attempt.ID, "failed", failureMessage, &finished) + s.writeError(c, http.StatusInternalServerError, failureMessage, err) + return + } + } executionCtx, cancel := context.WithTimeout(c.Request.Context(), certificateTaskTimeout) s.registerTaskCancel(task.ID, cancel) defer func() { diff --git a/backend/internal/httpapi/task_api_test.go b/backend/internal/httpapi/task_api_test.go index 126b559..1ec09e3 100644 --- a/backend/internal/httpapi/task_api_test.go +++ b/backend/internal/httpapi/task_api_test.go @@ -84,7 +84,7 @@ func TestTaskAPIRecordsSuccessfulIssueAndAudit(t *testing.T) { t.Fatalf("任务尝试记录错误:%+v %v", attempts, err) } logs, err := auditRepo.ListAuditLogs(context.Background(), 10) - if err != nil || len(logs) != 1 || logs[0].Status != "success" { + if err != nil || len(logs) != 2 || logs[0].Status != "success" || logs[1].Status != "running" { t.Fatalf("审计日志记录错误:%+v %v", logs, err) } wantScope := repository.HistoryScope{ScopeType: repository.ScopeProject, ProjectID: domain.DefaultProjectID} @@ -168,6 +168,47 @@ func (r *failingTaskStartRepository) UpdateTask(ctx context.Context, id, status, return r.TaskRepository.UpdateTask(ctx, id, status, stage, message, startedAt, finishedAt) } +type failingTaskStartAuditRepository struct { + repository.AuditRepository + failure error +} + +func (r failingTaskStartAuditRepository) CreateAuditLog(ctx context.Context, item repository.AuditLog) error { + if item.Status == "running" { + return r.failure + } + return r.AuditRepository.CreateAuditLog(ctx, item) +} + +func TestTaskAPIClosesTaskWhenStartAuditFails(t *testing.T) { + server, taskRepo, auditRepo, closeDB := newTaskAPITestServer(t, func(context.Context, string) (certificate.IssueResult, deployment.Report, error) { + t.Fatal("任务启动审计失败时不应执行证书申请") + return certificate.IssueResult{}, deployment.Report{}, nil + }) + defer closeDB() + server.audit = failingTaskStartAuditRepository{ + AuditRepository: auditRepo, + failure: errors.New("模拟任务启动审计失败"), + } + + response := httptest.NewRecorder() + serveAdminHTTP(server, response, httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/issue", nil)) + if response.Code != http.StatusInternalServerError || !strings.Contains(response.Body.String(), "任务启动审计记录失败") { + t.Fatalf("任务启动审计失败响应错误:%d %s", response.Code, response.Body.String()) + } + items, err := taskRepo.ListTasks(context.Background(), "", 10) + if err != nil || len(items) != 1 || items[0].Status != "failed" || items[0].Stage != "record" { + t.Fatalf("任务启动审计失败后任务未收敛:%+v %v", items, err) + } + attempts, err := taskRepo.ListAttempts(context.Background(), items[0].ID) + if err != nil || len(attempts) != 1 || attempts[0].Status != "failed" { + t.Fatalf("任务启动审计失败后尝试未收敛:%+v %v", attempts, err) + } + logs, err := auditRepo.ListAuditLogs(context.Background(), 10) + if err != nil || len(logs) != 0 { + t.Fatalf("启动审计写入失败时不应生成伪造终态审计:%+v %v", logs, err) + } +} func TestTaskAPIClosesTaskWhenStartStateUpdateFails(t *testing.T) { server, taskRepo, auditRepo, closeDB := newTaskAPITestServer(t, func(context.Context, string) (certificate.IssueResult, deployment.Report, error) { t.Fatal("任务启动失败时不应执行证书申请") @@ -237,7 +278,7 @@ func TestTaskAPIReturnsFailureAndKeepsTaskTrace(t *testing.T) { t.Fatalf("失败任务状态错误:%+v %v", items, err) } logs, err := auditRepo.ListAuditLogs(context.Background(), 10) - if err != nil || len(logs) != 1 || logs[0].Status != "error" { + if err != nil || len(logs) != 2 || logs[0].Status != "error" || logs[1].Status != "running" { t.Fatalf("失败审计状态错误:%+v %v", logs, err) } } @@ -312,7 +353,7 @@ func TestTaskAPICanCancelRunningIssue(t *testing.T) { t.Fatalf("任务取消状态没有持久化:%+v %v", item, err) } logs, err := auditRepo.ListAuditLogs(t.Context(), 10) - if err != nil || len(logs) != 2 { + if err != nil || len(logs) != 3 { t.Fatalf("取消和申请收尾审计缺失:%+v,%v", logs, err) } for _, entry := range logs { @@ -528,7 +569,7 @@ func TestTaskAPIRetriesFailedCertificateIssue(t *testing.T) { t.Fatalf("retry attempts were not recorded correctly: %+v %v", attempts, err) } logs, err := auditRepo.ListAuditLogs(context.Background(), 10) - if err != nil || len(logs) != 2 || logs[0].Action != "certificate_issue_retry" || logs[0].OperatorID != "test-admin" { + if err != nil || len(logs) != 4 || logs[0].Action != "certificate_issue_retry" || logs[0].Status != "success" || logs[1].Action != "certificate_issue_retry" || logs[1].Status != "running" || logs[0].OperatorID != "test-admin" { t.Fatalf("retry audit was not recorded correctly: %+v %v", logs, err) } } @@ -559,7 +600,7 @@ func TestTaskAPIRecordsPartialDeploymentSuccess(t *testing.T) { t.Fatalf("partial attempt status: %+v %v", attempts, err) } logs, err := auditRepo.ListAuditLogs(context.Background(), 10) - if err != nil || len(logs) != 1 || logs[0].Status != "partial_success" { + if err != nil || len(logs) != 2 || logs[0].Status != "partial_success" || logs[1].Status != "running" { t.Fatalf("partial audit status: %+v %v", logs, err) } } @@ -586,7 +627,7 @@ func TestTaskAPIRecordsPartialDeploymentFailureAsPartialSuccess(t *testing.T) { t.Fatalf("partial failure attempt status: %+v %v", attempts, err) } logs, err := auditRepo.ListAuditLogs(context.Background(), 10) - if err != nil || len(logs) != 1 || logs[0].Status != "partial_success" { + if err != nil || len(logs) != 2 || logs[0].Status != "partial_success" || logs[1].Status != "running" { t.Fatalf("partial failure audit status: %+v %v", logs, err) } } diff --git a/doc/Scert开发任务清单.md b/doc/Scert开发任务清单.md index bcfe16a..ee40411 100644 --- a/doc/Scert开发任务清单.md +++ b/doc/Scert开发任务清单.md @@ -1,5 +1,16 @@ # Scert 开发任务清单 +## 最新进度摘要(2026-09-29 继续开发) + +**总体验收位置仍为 P0-03 完成,待复审。** 本轮继续推进 P0-10 任务事务链,不把局部代码或专项测试视为整项复审通过。 + +- [x] HTTP 证书申请任务补齐“开始执行”审计;开始审计与终态审计均关联任务创建人、项目和任务 ID。 +- [x] 开始审计写入失败时,任务和任务尝试收敛为失败并停止证书申请;专项回归测试已覆盖该异常路径。 +- [x] `go test ./internal/httpapi` 已通过;尚未据此升级 P0-10 或总体验收状态。 +- [~] P0-10 的 DNS 验证任务、批量部署结果、崩溃恢复、重试幂等及其他长耗时入口仍需继续审查。 +- [ ] P0-12 备份恢复、三数据库实库矩阵、浏览器清缓存、密码 SSH、远端部署和生产验收尚未执行。 +- [!] 不新增 `[✓]` 复审通过项,总体验收继续保持 **P0-03 完成,待复审**。 + ## 最新执行摘要(2026-09-29 继续开发) 总体验收位置继续保持 **P0-03 完成,待复审**;本轮继续先开发、后统一测试,不进入远端生产验收。 diff --git a/doc/Scert整改任务执行清单.md b/doc/Scert整改任务执行清单.md index 976fede..921a819 100644 --- a/doc/Scert整改任务执行清单.md +++ b/doc/Scert整改任务执行清单.md @@ -1,5 +1,16 @@ # Scert 整改任务执行清单 +## 最新进度摘要(2026-09-29 继续开发) + +**总体验收位置仍为 P0-03 完成,待复审。** 本轮继续推进 P0-10 任务事务链,不把局部代码或专项测试视为整项复审通过。 + +- [x] HTTP 证书申请任务补齐“开始执行”审计;开始审计与终态审计均关联任务创建人、项目和任务 ID。 +- [x] 开始审计写入失败时,任务和任务尝试收敛为失败并停止证书申请;专项回归测试已覆盖该异常路径。 +- [x] `go test ./internal/httpapi` 已通过;尚未据此升级 P0-10 或总体验收状态。 +- [~] P0-10 的 DNS 验证任务、批量部署结果、崩溃恢复、重试幂等及其他长耗时入口仍需继续审查。 +- [ ] P0-12 备份恢复、三数据库实库矩阵、浏览器清缓存、密码 SSH、远端部署和生产验收尚未执行。 +- [!] 不新增 `[✓]` 复审通过项,总体验收继续保持 **P0-03 完成,待复审**。 + ## 最新任务批次(2026-09-29 继续开发) 总体验收仍保持 **P0-03 完成,待复审**;本批只补齐 P0-10 任务创建人和审计归属,不代表生产部署验收通过。 diff --git a/doc/Scert整改合并优化清单.md b/doc/Scert整改合并优化清单.md index befc581..8ebf090 100644 --- a/doc/Scert整改合并优化清单.md +++ b/doc/Scert整改合并优化清单.md @@ -2,6 +2,17 @@ ## 最新进度摘要(2026-09-29 继续开发) +**总体验收位置仍为 P0-03 完成,待复审。** 本轮继续推进 P0-10 任务事务链,不把局部代码或专项测试视为整项复审通过。 + +- [x] HTTP 证书申请任务补齐“开始执行”审计;开始审计与终态审计均关联任务创建人、项目和任务 ID。 +- [x] 开始审计写入失败时,任务和任务尝试收敛为失败并停止证书申请;专项回归测试已覆盖该异常路径。 +- [x] `go test ./internal/httpapi` 已通过;尚未据此升级 P0-10 或总体验收状态。 +- [~] P0-10 的 DNS 验证任务、批量部署结果、崩溃恢复、重试幂等及其他长耗时入口仍需继续审查。 +- [ ] P0-12 备份恢复、三数据库实库矩阵、浏览器清缓存、密码 SSH、远端部署和生产验收尚未执行。 +- [!] 不新增 `[✓]` 复审通过项,总体验收继续保持 **P0-03 完成,待复审**。 + +## 最新进度摘要(2026-09-29 继续开发) + **总体验收位置仍为 P0-03 完成,待复审。** 本轮只推进 P0-10 任务归属链,不把局部代码或专项测试结果视为整项复审通过。 - [x] 任务工作流从认证请求上下文读取创建人,并写入证书任务的 `operator_id`。 diff --git a/doc/基础整改任务规格.md b/doc/基础整改任务规格.md index 41bf378..b5119e9 100644 --- a/doc/基础整改任务规格.md +++ b/doc/基础整改任务规格.md @@ -1,5 +1,16 @@ # 基础整改任务规格 +## 最新进度摘要(2026-09-29 继续开发) + +**总体验收位置仍为 P0-03 完成,待复审。** 本轮继续推进 P0-10 任务事务链,不把局部代码或专项测试视为整项复审通过。 + +- [x] HTTP 证书申请任务补齐“开始执行”审计;开始审计与终态审计均关联任务创建人、项目和任务 ID。 +- [x] 开始审计写入失败时,任务和任务尝试收敛为失败并停止证书申请;专项回归测试已覆盖该异常路径。 +- [x] `go test ./internal/httpapi` 已通过;尚未据此升级 P0-10 或总体验收状态。 +- [~] P0-10 的 DNS 验证任务、批量部署结果、崩溃恢复、重试幂等及其他长耗时入口仍需继续审查。 +- [ ] P0-12 备份恢复、三数据库实库矩阵、浏览器清缓存、密码 SSH、远端部署和生产验收尚未执行。 +- [!] 不新增 `[✓]` 复审通过项,总体验收继续保持 **P0-03 完成,待复审**。 + ## 最新执行与补充约束(2026-09-29 继续开发) 总体验收位置继续保持 **P0-03 完成,待复审**。本轮补充 P0-10 任务归属不变量,不改变原始 44 项范围,也不提前进入生产验收。