From 302111c8affd6c31c6d616eed45a2a8cee42843c Mon Sep 17 00:00:00 2001 From: xuans Date: Thu, 1 Oct 2026 16:54:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AF=81=E4=B9=A6=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E9=87=8D=E8=AF=95=E8=87=AA=E5=86=B2=E7=AA=81=E5=B9=B6?= =?UTF-8?q?=E8=A1=A5=E5=85=85=E7=9C=9F=E5=AE=9E=E8=A3=85=E9=85=8D=E5=9B=9E?= =?UTF-8?q?=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/app/app.go | 4 + .../app/certificate_retry_api_test.go | 132 ++++++++++++++++++ backend/internal/httpapi/server.go | 50 ++++--- backend/internal/httpapi/task_api.go | 4 +- backend/internal/httpapi/task_api_test.go | 34 ++++- doc/Linux生产流程测试记录.md | 16 +++ doc/Scert整改任务执行清单.md | 16 +++ doc/Scert整改合并优化清单.md | 16 +++ 8 files changed, 245 insertions(+), 27 deletions(-) create mode 100644 backend/internal/app/certificate_retry_api_test.go diff --git a/backend/internal/app/app.go b/backend/internal/app/app.go index a1c93d7..8f0f1b6 100644 --- a/backend/internal/app/app.go +++ b/backend/internal/app/app.go @@ -199,6 +199,10 @@ func NewWithConfigPath(ctx context.Context, cfg config.Config, configPath string result, err := trackedWorkflow.DeployCurrent(ctx, domainID) return result.Certificate, result.Deployment, err }, + DeployCertificateTask: func(ctx context.Context, domainID string) (certificate.IssueResult, deployment.Report, error) { + result, err := workflow.DeployCurrent(ctx, domainID) + return result.Certificate, result.Deployment, err + }, RestoreCertificateVersion: func(ctx context.Context, domainID, version string) (certificate.IssueResult, error) { result, err := trackedWorkflow.Restore(ctx, domainID, version) return result.Certificate, err diff --git a/backend/internal/app/certificate_retry_api_test.go b/backend/internal/app/certificate_retry_api_test.go new file mode 100644 index 0000000..d900a1e --- /dev/null +++ b/backend/internal/app/certificate_retry_api_test.go @@ -0,0 +1,132 @@ +package app + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "strings" + "sync/atomic" + "testing" + "time" + + "git.pcux.cn/xuans/Scert/backend/internal/certificate" + "git.pcux.cn/xuans/Scert/backend/internal/deployment" +) + +func TestApplicationDeployRetryUsesExistingTask(t *testing.T) { + _, application := newCertificateTaskFixture(t) + saved := workflowSavedCertificate(t) + var calls atomic.Int32 + started := make(chan struct{}) + release := make(chan struct{}) + defer close(release) + transactions := make(chan deployment.TransactionContext, 2) + application.Workflow.CurrentCertificate = func(ctx context.Context, id string) (certificate.IssueResult, error) { + transaction, _ := deployment.TransactionContextFromContext(ctx) + transactions <- transaction + if calls.Add(1) == 1 { + return certificate.IssueResult{}, errors.New("首次部署故障") + } + close(started) + select { + case <-release: + case <-ctx.Done(): + return certificate.IssueResult{}, ctx.Err() + } + return certificate.IssueResult{DomainID: id, Domains: []string{"example.com"}, Saved: saved}, nil + } + application.Workflow.Servers = workflowServerLister{servers: []deployment.Server{{ID: "server-1"}, {ID: "server-2"}}} + application.Workflow.Deployment = &workflowDeployer{report: deployment.Report{ + Total: 2, Success: 2, Status: "success", + Results: []deployment.Result{{ServerID: "server-1", Status: "success"}, {ServerID: "server-2", Status: "success"}}, + }} + application.Workflow.Notifier = nil + application.Workflow.RemoteVerifierForServer = func(context.Context, deployment.Server, string, []string) (certificate.ValidationResult, error) { + return certificate.ValidationResult{FingerprintSHA256: saved.FingerprintSHA256}, nil + } + var cookies []*http.Cookie + request := func(path, body string) *httptest.ResponseRecorder { + req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + for _, cookie := range cookies { + req.AddCookie(cookie) + } + response := httptest.NewRecorder() + application.HTTP.Handler().ServeHTTP(response, req) + return response + } + login := request("/api/v1/auth/login", `{"identifier":"admin","password":"Test1234!"}`) + if login.Code != http.StatusOK { + t.Fatalf("真实应用登录失败:%d %s", login.Code, login.Body.String()) + } + cookies = login.Result().Cookies() + initial := request("/api/v1/domains/task-domain/deploy", "") + if initial.Code != http.StatusBadGateway || calls.Load() != 1 { + t.Fatalf("首次故障未经过真实部署接线:%d %s", initial.Code, initial.Body.String()) + } + tasks, err := application.Tasks.ListTasks(t.Context(), "task-domain", 10) + if err != nil || len(tasks) != 1 || tasks[0].Status != "failed" { + t.Fatalf("首次部署未生成唯一失败任务:%+v %v", tasks, err) + } + task := tasks[0] + retryPath := "/api/v1/tasks/" + task.ID + "/retry" + done := make(chan *httptest.ResponseRecorder, 1) + go func() { done <- request(retryPath, "") }() + select { + case <-started: + case response := <-done: + t.Fatalf("重试未进入原始部署工作流:%d %s", response.Code, response.Body.String()) + case <-time.After(5 * time.Second): + t.Fatal("重试工作流启动超时") + } + concurrent := request(retryPath, "") + if concurrent.Code != http.StatusConflict || calls.Load() != 2 { + t.Fatalf("并发重试未被阻止:%d %s", concurrent.Code, concurrent.Body.String()) + } + release <- struct{}{} + select { + case response := <-done: + if response.Code != http.StatusOK { + t.Fatalf("真实应用部署重试失败:%d %s", response.Code, response.Body.String()) + } + case <-time.After(5 * time.Second): + t.Fatal("重试工作流收尾超时") + } + tasks, err = application.Tasks.ListTasks(t.Context(), "task-domain", 10) + if err != nil || len(tasks) != 1 || tasks[0].ID != task.ID || tasks[0].Status != "success" { + t.Fatalf("重试重复创建任务或未成功:%+v %v", tasks, err) + } + attempts, err := application.Tasks.ListAttempts(t.Context(), task.ID) + if err != nil || len(attempts) != 2 { + t.Fatalf("尝试数量错误:%+v %v", attempts, err) + } + for i, status := range []string{"failed", "success"} { + attempt := attempts[i] + transaction := <-transactions + if attempt.Status != status || attempt.AttemptNumber != i+1 || attempt.FinishedAt == nil || + transaction.TaskID != task.ID || transaction.AttemptID != attempt.ID || transaction.AttemptNumber != i+1 { + t.Fatalf("重试丢失原任务事务关联:%+v %+v", attempt, transaction) + } + } + repeated := request(retryPath, "") + results, err := application.Tasks.ListTaskDeploymentResults(t.Context(), task.ID) + if err != nil || len(results) != 2 { + t.Fatalf("双服务器结果未保存:%+v %v", results, err) + } + for _, result := range results { + if result.AttemptID != attempts[1].ID || result.AttemptNumber != 2 || result.Status != "success" { + t.Fatalf("服务器结果未关联重试尝试:%+v", result) + } + } + var auditCount int + err = application.Database.DB.QueryRowContext(t.Context(), + "SELECT COUNT(*) FROM audit_logs WHERE task_id = ? AND action = ? AND operator_id = ? AND status IN ('running', 'success')", + task.ID, "certificate_deploy_retry", task.OperatorID).Scan(&auditCount) + if err != nil || auditCount != 2 { + t.Fatalf("重试审计与操作者关联不完整:%d %v", auditCount, err) + } + if repeated.Code != http.StatusConflict || calls.Load() != 2 { + t.Fatalf("成功任务仍被重复执行:%d %s", repeated.Code, repeated.Body.String()) + } +} diff --git a/backend/internal/httpapi/server.go b/backend/internal/httpapi/server.go index 5b421e6..d2b4940 100644 --- a/backend/internal/httpapi/server.go +++ b/backend/internal/httpapi/server.go @@ -40,29 +40,31 @@ type readinessSecretReader interface { } type Dependencies struct { - Config config.Config - ConfigPath string - FrontendDir string - Logger *logging.Logger - Repository repository.DomainRepository - Servers repository.ServerRepository - Users repository.UserRepository - ProjectMembers repository.ProjectMemberRepository - ProjectDirectory repository.ProjectDirectoryRepository - ProjectLifecycle repository.ProjectLifecycleRepository - ProjectResources repository.ProjectResourceReader - ProjectBindings repository.ProjectBindingRepository - ProjectDomains repository.ProjectDomainRepository - SSHKeys *sshkey.Service - DeploymentFactory deployment.ClientFactory - SetupDNS setupDNSVerifier - Secrets setupSecretWriter - Firewall firewall.Manager - IssueCertificate func(context.Context, string) (certificate.IssueResult, deployment.Report, error) - IssueSetupCertificate func(context.Context, certificate.SetupCertificateRequest) (certificate.SavedResource, error) - RollbackSetupCertificate func(context.Context, certificate.SavedResource) error - ImportCertificate func(context.Context, certificate.ImportRequest) (certificate.IssueResult, deployment.Report, error) - DeployCertificate func(context.Context, string) (certificate.IssueResult, deployment.Report, error) + Config config.Config + ConfigPath string + FrontendDir string + Logger *logging.Logger + Repository repository.DomainRepository + Servers repository.ServerRepository + Users repository.UserRepository + ProjectMembers repository.ProjectMemberRepository + ProjectDirectory repository.ProjectDirectoryRepository + ProjectLifecycle repository.ProjectLifecycleRepository + ProjectResources repository.ProjectResourceReader + ProjectBindings repository.ProjectBindingRepository + ProjectDomains repository.ProjectDomainRepository + SSHKeys *sshkey.Service + DeploymentFactory deployment.ClientFactory + SetupDNS setupDNSVerifier + Secrets setupSecretWriter + Firewall firewall.Manager + IssueCertificate func(context.Context, string) (certificate.IssueResult, deployment.Report, error) + IssueSetupCertificate func(context.Context, certificate.SetupCertificateRequest) (certificate.SavedResource, error) + RollbackSetupCertificate func(context.Context, certificate.SavedResource) error + ImportCertificate func(context.Context, certificate.ImportRequest) (certificate.IssueResult, deployment.Report, error) + DeployCertificate func(context.Context, string) (certificate.IssueResult, deployment.Report, error) + // DeployCertificateTask 执行已有任务,不得重复创建任务或尝试记录。 + DeployCertificateTask func(context.Context, string) (certificate.IssueResult, deployment.Report, error) RestoreCertificateVersion func(context.Context, string, string) (certificate.IssueResult, error) ValidateCertificate func(context.Context, string) (certificate.IssueResult, certificate.ValidationResult, error) @@ -101,6 +103,7 @@ type Server struct { rollbackSetupCertificate func(context.Context, certificate.SavedResource) error importCertificate func(context.Context, certificate.ImportRequest) (certificate.IssueResult, deployment.Report, error) deployCertificate func(context.Context, string) (certificate.IssueResult, deployment.Report, error) + deployCertificateTask func(context.Context, string) (certificate.IssueResult, deployment.Report, error) restoreVersion func(context.Context, string, string) (certificate.IssueResult, error) validateCertificateWorkflow func(context.Context, string) (certificate.IssueResult, certificate.ValidationResult, error) @@ -165,6 +168,7 @@ func New(deps Dependencies) *Server { rollbackSetupCertificate: deps.RollbackSetupCertificate, importCertificate: deps.ImportCertificate, deployCertificate: deps.DeployCertificate, + deployCertificateTask: deps.DeployCertificateTask, restoreVersion: deps.RestoreCertificateVersion, validateCertificateWorkflow: deps.ValidateCertificate, runRenewal: deps.RunRenewal, diff --git a/backend/internal/httpapi/task_api.go b/backend/internal/httpapi/task_api.go index 914d05e..45a2e1d 100644 --- a/backend/internal/httpapi/task_api.go +++ b/backend/internal/httpapi/task_api.go @@ -141,7 +141,7 @@ func (s *Server) executeCertificateIssueTask(c *gin.Context, task repository.Tas } func (s *Server) executeCertificateDeployTask(c *gin.Context, task repository.Task, attempt repository.TaskAttempt, auditAction string) { - s.executeCertificateTask(c, task, attempt, auditAction, s.deployCertificate, "certificate_deploy", "正在部署证书", "证书部署完成") + s.executeCertificateTask(c, task, attempt, auditAction, s.deployCertificateTask, "certificate_deploy", "正在部署证书", "证书部署完成") } func (s *Server) executeCertificateTask(c *gin.Context, task repository.Task, attempt repository.TaskAttempt, auditAction string, operation func(context.Context, string) (certificate.IssueResult, deployment.Report, error), stage, runningMessage, successMessage string) { @@ -423,7 +423,7 @@ func (s *Server) retryTask(c *gin.Context) { } var runTask func(*gin.Context, repository.Task, repository.TaskAttempt, string) if item.TaskType == "certificate_deploy" { - if s.deployCertificate == nil { + if s.deployCertificateTask == nil { s.writeError(c, http.StatusNotImplemented, "证书部署接口尚未配置", errors.New("证书部署工作流未配置")) return } diff --git a/backend/internal/httpapi/task_api_test.go b/backend/internal/httpapi/task_api_test.go index eb2881c..83e4c0f 100644 --- a/backend/internal/httpapi/task_api_test.go +++ b/backend/internal/httpapi/task_api_test.go @@ -749,7 +749,7 @@ func TestTaskAPIRetriesFailedCertificateDeploymentWithoutDNSCredential(t *testin return certificate.IssueResult{}, deployment.Report{}, nil }) defer closeDB() - server.deployCertificate = func(_ context.Context, id string) (certificate.IssueResult, deployment.Report, error) { + server.deployCertificateTask = func(_ context.Context, id string) (certificate.IssueResult, deployment.Report, error) { calls++ return certificate.IssueResult{DomainID: id}, deployment.Report{Status: "success"}, nil } @@ -792,12 +792,42 @@ func TestTaskAPIRetriesFailedCertificateDeploymentWithoutDNSCredential(t *testin } } +func TestTaskAPIDeployRetryRequiresUntrackedExecutor(t *testing.T) { + server, tasks, _, closeDB := newTaskAPITestServer(t, nil) + defer closeDB() + server.deployCertificate = func(context.Context, string) (certificate.IssueResult, deployment.Report, error) { + t.Fatal("已有任务不得回退到创建新任务的部署入口") + return certificate.IssueResult{}, deployment.Report{}, nil + } + item := repository.Task{ + HistoryScope: repository.HistoryScope{ScopeType: repository.ScopeProject, ProjectID: domain.DefaultProjectID}, + ID: "deploy-missing-executor", DomainID: "domain-1", TaskType: "certificate_deploy", + Status: "failed", Stage: "certificate_deploy", + } + if err := tasks.CreateTask(t.Context(), item); err != nil { + t.Fatal(err) + } + response := httptest.NewRecorder() + serveAdminHTTP(server, response, httptest.NewRequest(http.MethodPost, "/api/v1/tasks/"+item.ID+"/retry", nil)) + if response.Code != http.StatusNotImplemented { + t.Fatalf("缺少已有任务执行器时应拒绝重试:%d %s", response.Code, response.Body.String()) + } + stored, err := tasks.GetTask(t.Context(), item.ID) + if err != nil || stored.Status != "failed" { + t.Fatalf("执行器未配置时不应改变任务状态:%+v %v", stored, err) + } + attempts, err := tasks.ListAttempts(t.Context(), item.ID) + if err != nil || len(attempts) != 0 { + t.Fatalf("执行器未配置时不应增加尝试:%+v %v", attempts, err) + } +} + func TestTaskAPIRetriesPartialCertificateDeployment(t *testing.T) { server, taskRepo, _, closeDB := newTaskAPITestServer(t, func(context.Context, string) (certificate.IssueResult, deployment.Report, error) { return certificate.IssueResult{}, deployment.Report{}, nil }) defer closeDB() - server.deployCertificate = func(_ context.Context, id string) (certificate.IssueResult, deployment.Report, error) { + server.deployCertificateTask = func(_ context.Context, id string) (certificate.IssueResult, deployment.Report, error) { return certificate.IssueResult{DomainID: id}, deployment.Report{Status: "success"}, nil } finished := time.Now().UTC() diff --git a/doc/Linux生产流程测试记录.md b/doc/Linux生产流程测试记录.md index b74f10e..301b3b5 100644 --- a/doc/Linux生产流程测试记录.md +++ b/doc/Linux生产流程测试记录.md @@ -1217,3 +1217,19 @@ git diff --check - [x] 已完成,等待复审:两台服务器交叉探测对方公网 `8443` 均超时;结合实际监听地址确认该内部端口未对外监听,未据此声称其他已有服务端口全部关闭。 - [!] 边界说明:第一台内部管理服务沿用原有 IP 自签证书,使用测试域名和系统 CA 直连会失败;通过密码 SSH 核查证书及 IP SAN 后,以该证书作为显式信任文件访问回环上游返回 200,未关闭 TLS 校验。这仅是该机内部健康核验,不是第一台管理入口的公网受信 TLS 验收。 - [!] 边界说明:本轮保留数据库,没有执行清库首次流程;未额外制作数据备份,官方安装器自身保留升级前发布文件以支持失败回退。完整生产验收仍为 **P0-03:完成,待复审**。 + +### 部署重试阻塞修复进度(2026-10-01 16:51,北京时间) + +本节为本轮当前状态说明,前文保留为历史记录。P0-03 的局部代码复审状态不代表整个项目的生产验收状态;本轮上线范围仅 Linux,不将 Windows 列为阻塞。 + +- [✓] 复审通过:双机服务保持回环监听 127.0.0.1:8443,由 Nginx 80/443 对外提供服务;本轮未新增公网业务端口。 +- [x] 已完成,等待复审:通过正式部署接口制造第一台 Nginx 校验命令失败,两台部署报告整体失败,恢复报告为 success;随后已恢复正常校验命令,双机 nginx -t 通过。该验证使用同一证书重部署,不代表不同新旧证书切换验收。 +- [x] 已完成,等待复审:定位部署重试自冲突。HTTP 重试已创建尝试并标记原任务运行,但应用装配再次调用创建任务的工作流,将自身判为同类活动任务。新增真实应用登录与路由装配回归,修复前稳定返回 502。 +- [x] 已完成,等待复审:拆分普通部署与已有任务执行回调,普通部署继续创建任务,重试只执行原始证书部署流程,保留权限、审计、尝试记录与事务关联。 +- [x] 已完成,等待复审:定向回归验证唯一父任务、失败后成功的两次尝试、原任务事务关联、并发重试 409、成功后重复重试 409;后端 go test ./... 与 go vet ./... 通过。 +- [~] 进行中:竞态测试、干净提交构建、双机升级和真实失败任务重试复核。当前尚未取得修复后的实机成功证据。 +- [!] 暂缓或存在阻塞:历史恢复事务、Smail SMTP、数据库生产矩阵、备份恢复与最终清库首次部署门禁仍未闭环,不宣称生产验收通过,不给缺乏依据的完成百分比。 + +现场失败任务标识:683ef23a-c912-4f46-b2a6-3ec11908808e。本轮保留其数据用于复现与修复验证,不属于清库首次流程;没有直接改库伪造状态,也没有创建辅助验收脚本。 + +本轮补充回归:应用装配测试同时核对双服务器结果归属于第二次尝试,以及开始和成功审计的操作者关联;缺少已有任务执行器时返回 501,不回退到创建任务入口,不改变原任务状态。后端全量测试与 vet 再次通过;app/httpapi/repository 竞态测试通过,补充用例的定向竞态测试通过;前端 241 项通过。实机复测尚待新包部署。 diff --git a/doc/Scert整改任务执行清单.md b/doc/Scert整改任务执行清单.md index a9256fc..195ef1c 100644 --- a/doc/Scert整改任务执行清单.md +++ b/doc/Scert整改任务执行清单.md @@ -1529,3 +1529,19 @@ P0-05 从 `[!]` 调整为 `[~]`,并未标记完成;P0-04 剩余门禁和全 - [x] 已完成,等待复审:两台服务器交叉探测对方公网 `8443` 均超时;结合实际监听地址确认该内部端口未对外监听,未据此声称其他已有服务端口全部关闭。 - [!] 边界说明:第一台内部管理服务沿用原有 IP 自签证书,使用测试域名和系统 CA 直连会失败;通过密码 SSH 核查证书及 IP SAN 后,以该证书作为显式信任文件访问回环上游返回 200,未关闭 TLS 校验。这仅是该机内部健康核验,不是第一台管理入口的公网受信 TLS 验收。 - [!] 边界说明:本轮保留数据库,没有执行清库首次流程;未额外制作数据备份,官方安装器自身保留升级前发布文件以支持失败回退。完整生产验收仍为 **P0-03:完成,待复审**。 + +### 部署重试阻塞修复进度(2026-10-01 16:51,北京时间) + +本节为本轮当前状态说明,前文保留为历史记录。P0-03 的局部代码复审状态不代表整个项目的生产验收状态;本轮上线范围仅 Linux,不将 Windows 列为阻塞。 + +- [✓] 复审通过:双机服务保持回环监听 127.0.0.1:8443,由 Nginx 80/443 对外提供服务;本轮未新增公网业务端口。 +- [x] 已完成,等待复审:通过正式部署接口制造第一台 Nginx 校验命令失败,两台部署报告整体失败,恢复报告为 success;随后已恢复正常校验命令,双机 nginx -t 通过。该验证使用同一证书重部署,不代表不同新旧证书切换验收。 +- [x] 已完成,等待复审:定位部署重试自冲突。HTTP 重试已创建尝试并标记原任务运行,但应用装配再次调用创建任务的工作流,将自身判为同类活动任务。新增真实应用登录与路由装配回归,修复前稳定返回 502。 +- [x] 已完成,等待复审:拆分普通部署与已有任务执行回调,普通部署继续创建任务,重试只执行原始证书部署流程,保留权限、审计、尝试记录与事务关联。 +- [x] 已完成,等待复审:定向回归验证唯一父任务、失败后成功的两次尝试、原任务事务关联、并发重试 409、成功后重复重试 409;后端 go test ./... 与 go vet ./... 通过。 +- [~] 进行中:竞态测试、干净提交构建、双机升级和真实失败任务重试复核。当前尚未取得修复后的实机成功证据。 +- [!] 暂缓或存在阻塞:历史恢复事务、Smail SMTP、数据库生产矩阵、备份恢复与最终清库首次部署门禁仍未闭环,不宣称生产验收通过,不给缺乏依据的完成百分比。 + +现场失败任务标识:683ef23a-c912-4f46-b2a6-3ec11908808e。本轮保留其数据用于复现与修复验证,不属于清库首次流程;没有直接改库伪造状态,也没有创建辅助验收脚本。 + +本轮补充回归:应用装配测试同时核对双服务器结果归属于第二次尝试,以及开始和成功审计的操作者关联;缺少已有任务执行器时返回 501,不回退到创建任务入口,不改变原任务状态。后端全量测试与 vet 再次通过;app/httpapi/repository 竞态测试通过,补充用例的定向竞态测试通过;前端 241 项通过。实机复测尚待新包部署。 diff --git a/doc/Scert整改合并优化清单.md b/doc/Scert整改合并优化清单.md index f225c8a..14c90da 100644 --- a/doc/Scert整改合并优化清单.md +++ b/doc/Scert整改合并优化清单.md @@ -2159,3 +2159,19 @@ P0-11 生产验收门禁 - [x] 已完成,等待复审:两台服务器交叉探测对方公网 `8443` 均超时;结合实际监听地址确认该内部端口未对外监听,未据此声称其他已有服务端口全部关闭。 - [!] 边界说明:第一台内部管理服务沿用原有 IP 自签证书,使用测试域名和系统 CA 直连会失败;通过密码 SSH 核查证书及 IP SAN 后,以该证书作为显式信任文件访问回环上游返回 200,未关闭 TLS 校验。这仅是该机内部健康核验,不是第一台管理入口的公网受信 TLS 验收。 - [!] 边界说明:本轮保留数据库,没有执行清库首次流程;未额外制作数据备份,官方安装器自身保留升级前发布文件以支持失败回退。完整生产验收仍为 **P0-03:完成,待复审**。 + +### 部署重试阻塞修复进度(2026-10-01 16:51,北京时间) + +本节为本轮当前状态说明,前文保留为历史记录。P0-03 的局部代码复审状态不代表整个项目的生产验收状态;本轮上线范围仅 Linux,不将 Windows 列为阻塞。 + +- [✓] 复审通过:双机服务保持回环监听 127.0.0.1:8443,由 Nginx 80/443 对外提供服务;本轮未新增公网业务端口。 +- [x] 已完成,等待复审:通过正式部署接口制造第一台 Nginx 校验命令失败,两台部署报告整体失败,恢复报告为 success;随后已恢复正常校验命令,双机 nginx -t 通过。该验证使用同一证书重部署,不代表不同新旧证书切换验收。 +- [x] 已完成,等待复审:定位部署重试自冲突。HTTP 重试已创建尝试并标记原任务运行,但应用装配再次调用创建任务的工作流,将自身判为同类活动任务。新增真实应用登录与路由装配回归,修复前稳定返回 502。 +- [x] 已完成,等待复审:拆分普通部署与已有任务执行回调,普通部署继续创建任务,重试只执行原始证书部署流程,保留权限、审计、尝试记录与事务关联。 +- [x] 已完成,等待复审:定向回归验证唯一父任务、失败后成功的两次尝试、原任务事务关联、并发重试 409、成功后重复重试 409;后端 go test ./... 与 go vet ./... 通过。 +- [~] 进行中:竞态测试、干净提交构建、双机升级和真实失败任务重试复核。当前尚未取得修复后的实机成功证据。 +- [!] 暂缓或存在阻塞:历史恢复事务、Smail SMTP、数据库生产矩阵、备份恢复与最终清库首次部署门禁仍未闭环,不宣称生产验收通过,不给缺乏依据的完成百分比。 + +现场失败任务标识:683ef23a-c912-4f46-b2a6-3ec11908808e。本轮保留其数据用于复现与修复验证,不属于清库首次流程;没有直接改库伪造状态,也没有创建辅助验收脚本。 + +本轮补充回归:应用装配测试同时核对双服务器结果归属于第二次尝试,以及开始和成功审计的操作者关联;缺少已有任务执行器时返回 501,不回退到创建任务入口,不改变原任务状态。后端全量测试与 vet 再次通过;app/httpapi/repository 竞态测试通过,补充用例的定向竞态测试通过;前端 241 项通过。实机复测尚待新包部署。