From b3f27e88df0f1976b21d846881ba4ac0c9c98f4e Mon Sep 17 00:00:00 2001 From: xuans Date: Sat, 3 Oct 2026 20:18:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5=E5=AE=8C=E6=95=B4=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E4=BA=92=E6=96=A5=E4=B8=8E=E6=8C=81=E4=B9=85=E5=8C=96?= =?UTF-8?q?=E4=BA=BA=E5=B7=A5=E5=8D=A0=E7=94=A8=E9=98=B2=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/deployment/operation.go | 85 ++++++ backend/internal/deployment/operation_test.go | 243 ++++++++++++++++++ backend/internal/deployment/service.go | 22 ++ backend/internal/deployment/verified.go | 5 + backend/internal/deployment/verified_test.go | 5 +- .../recovery_disposition_repository_test.go | 17 ++ doc/Linux生产流程测试记录.md | 9 + doc/Scert开发任务清单.md | 8 + doc/Scert整改任务执行清单.md | 8 + doc/Scert整改合并优化清单.md | 13 + doc/部署事务人工处置设计.md | 9 + doc/部署事务人工核查任务清单.md | 8 + 12 files changed, 430 insertions(+), 2 deletions(-) create mode 100644 backend/internal/deployment/operation.go create mode 100644 backend/internal/deployment/operation_test.go diff --git a/backend/internal/deployment/operation.go b/backend/internal/deployment/operation.go new file mode 100644 index 0000000..7263c6c --- /dev/null +++ b/backend/internal/deployment/operation.go @@ -0,0 +1,85 @@ +package deployment + +import ( + "context" + "fmt" +) + +// acquireOperation 覆盖整个部署、站点核验及收尾;人工执行器必须共用此入口。 +// 它不替代数据库持久化占用和远端文件锁,等待可以取消,不能提前释放。 +func (s *Service) acquireOperation(ctx context.Context) (func(), error) { + if s == nil { + return nil, ErrRecoveryDispositionUnavailable + } + if err := ctx.Err(); err != nil { + return nil, err + } + s.operationOnce.Do(func() { s.operationGate = make(chan struct{}, 1) }) + select { + case <-ctx.Done(): + return nil, ctx.Err() + case s.operationGate <- struct{}{}: + if err := ctx.Err(); err != nil { + <-s.operationGate + return nil, err + } + return func() { <-s.operationGate }, nil + } +} + +func operationFailure(targets []Target, err error) Report { + report := Report{Total: len(targets), Failed: len(targets), Status: "failed", Results: make([]Result, 0, len(targets))} + for _, target := range targets { + report.Results = append(report.Results, Result{ + ServerID: target.Server.ID, Server: target.Server.Name, Status: "failed", + FailureCode: FailureUnavailable, Message: fmt.Sprintf("未获得部署操作许可:%v", err), + }) + } + return report +} + +type recoveryClaimReader interface { + HasRecoveryClaim(context.Context, string) (bool, error) +} + +func (s *Service) checkRecoveryClaim(ctx context.Context, id string) error { + if err := ctx.Err(); err != nil { + return err + } + reader, ok := s.journal.(recoveryClaimReader) + if !ok { + return nil + } + claimed, err := reader.HasRecoveryClaim(ctx, id) + if err != nil { + return ErrRecoveryDispositionUnavailable + } + if claimed { + return ErrRecoveryDispositionBusy + } + return ctx.Err() +} + +// checkDeploymentClaims 在连接远端前拦截仍有人工占用的服务器。 +// 同地址同端口的别名也须检查;凭据或路径修改不能绕过原服务器占用。 +func (s *Service) checkDeploymentClaims(ctx context.Context, server Server) error { + if _, ok := s.journal.(recoveryClaimReader); !ok { + return nil + } + records, err := s.journal.ListPendingDeploymentTransactions(ctx) + if err != nil { + return ErrRecoveryDispositionUnavailable + } + for _, record := range records { + sameServer := record.ServerID == server.ID + if record.Evidence != nil { + sameServer = sameServer || (record.Evidence.Address == server.Address && record.Evidence.Port == server.Port) + } + if sameServer { + if err := s.checkRecoveryClaim(ctx, record.ID); err != nil { + return err + } + } + } + return ctx.Err() +} diff --git a/backend/internal/deployment/operation_test.go b/backend/internal/deployment/operation_test.go new file mode 100644 index 0000000..3d531b4 --- /dev/null +++ b/backend/internal/deployment/operation_test.go @@ -0,0 +1,243 @@ +package deployment + +import ( + "context" + "errors" + "maps" + "reflect" + "sync" + "sync/atomic" + "testing" + "time" +) + +type operationWitnessContext struct { + context.Context + once sync.Once + waiting chan struct{} +} + +func (c *operationWitnessContext) Done() <-chan struct{} { + c.once.Do(func() { close(c.waiting) }) + return c.Context.Done() +} + +type operationFactory struct { + client Client + calls atomic.Int32 +} + +func (f *operationFactory) Connect(context.Context, Server) (Client, error) { + f.calls.Add(1) + return f.client, nil +} + +type operationCloseClient struct { + *memoryClient + closeHook func() +} + +func (c *operationCloseClient) Close() error { + if c.closeHook != nil { + c.closeHook() + } + return nil +} + +type operationJournal struct { + memoryTransactionJournal + commitHook func() +} + +func (j *operationJournal) UpdateDeploymentTransaction(ctx context.Context, item TransactionRecord) error { + if item.Phase == TransactionPhaseCommitting && j.commitHook != nil { + j.commitHook() + } + return j.memoryTransactionJournal.UpdateDeploymentTransaction(ctx, item) +} + +func awaitOperationSignal(t *testing.T, signal <-chan struct{}) { + t.Helper() + select { + case <-signal: + case <-time.After(5 * time.Second): + t.Fatal("operation did not reach expected stage") + } +} + +func TestOperationGateCoversVerifiedLifecycle(t *testing.T) { + for _, stage := range []string{"verify", "commit", "restore", "close"} { + t.Run(stage, func(t *testing.T) { + entered, resume, done := make(chan struct{}), make(chan struct{}), make(chan struct{}) + var unblock sync.Once + release := func() { unblock.Do(func() { close(resume) }) } + defer release() + var hookOnce sync.Once + hook := func() { hookOnce.Do(func() { close(entered); <-resume }) } + server, memory := testServer("gate"), newMemoryClient() + seedVerifiedFiles(memory, server) + client := &operationCloseClient{memoryClient: memory} + journal := &operationJournal{} + if stage == "close" { + client.closeHook = hook + } + if stage == "commit" { + journal.commitHook = hook + } + factory := &operationFactory{client: client} + service := NewServiceWithJournal(factory, journal) + ctx := WithTransactionContext(t.Context(), TransactionContext{TaskID: "task", AttemptID: "attempt", AttemptNumber: 1}) + artifact := testArtifact(t) + var report Report + var runErr error + go func() { + defer close(done) + report, runErr = service.DeployVerified(ctx, []Target{{Server: server}}, artifact, Verification{ + Verify: func(context.Context) error { + if stage == "verify" { + hook() + } + if stage == "restore" { + return errors.New("injected verification failure") + } + return nil + }, + VerifyRecovery: func(context.Context) error { hook(); return nil }, + }) + }() + awaitOperationSignal(t, entered) + calls := factory.calls.Load() + for _, operation := range []string{"deploy", "verified", "recover"} { + waitCtx, cancel := context.WithCancel(t.Context()) + witness := &operationWitnessContext{Context: waitCtx, waiting: make(chan struct{})} + otherDone := make(chan struct{}) + var otherErr error + var otherReport Report + go func() { + defer close(otherDone) + if operation == "recover" { + otherErr = service.RecoverPending(witness, func(context.Context, string) (Server, error) { return server, nil }) + } else if operation == "verified" { + otherReport, otherErr = service.DeployVerified(witness, []Target{{Server: server}}, artifact, Verification{Verify: func(context.Context) error { return nil }}) + } else { + otherReport = service.Deploy(witness, []Target{{Server: server}}, artifact) + } + }() + awaitOperationSignal(t, witness.waiting) + cancel() + awaitOperationSignal(t, otherDone) + if factory.calls.Load() != calls || (operation != "deploy" && !errors.Is(otherErr, context.Canceled)) || (operation != "recover" && otherReport.Status != "failed") { + t.Fatalf("concurrent %s entered during %s: %+v %v", operation, stage, otherReport, otherErr) + } + } + release() + awaitOperationSignal(t, done) + if stage == "restore" { + if runErr == nil || report.RecoveryStatus != "success" { + t.Fatalf("restore failed: %+v %v", report, runErr) + } + } else if runErr != nil || report.Status != "success" { + t.Fatalf("deployment failed: %+v %v", report, runErr) + } + unlock, err := service.acquireOperation(t.Context()) + if err != nil { + t.Fatal(err) + } + unlock() + }) + } +} + +type claimedOperationJournal struct { + memoryTransactionJournal + claimed bool + claimErr error + listErr error +} + +func (j *claimedOperationJournal) HasRecoveryClaim(context.Context, string) (bool, error) { + return j.claimed, j.claimErr +} +func (j *claimedOperationJournal) ListPendingDeploymentTransactions(ctx context.Context) ([]TransactionRecord, error) { + if j.listErr != nil { + return nil, j.listErr + } + return j.memoryTransactionJournal.ListPendingDeploymentTransactions(ctx) +} + +func TestRecoveryClaimPreventsAutomaticMutation(t *testing.T) { + for _, failure := range []bool{false, true} { + t.Run(map[bool]string{false: "claimed", true: "read_failure"}[failure], func(t *testing.T) { + server, client, record := recoveryPreflightFixture() + original := maps.Clone(client.files) + journal := &claimedOperationJournal{memoryTransactionJournal: memoryTransactionJournal{pending: []TransactionRecord{record}}, claimed: true} + want := ErrRecoveryDispositionBusy + if failure { + journal.claimErr = errors.New("private database error") + want = ErrRecoveryDispositionUnavailable + } + factory := &operationFactory{client: client} + service := NewServiceWithJournal(factory, journal) + err := service.RecoverPending(t.Context(), func(context.Context, string) (Server, error) { t.Error("claimed record resolved"); return server, nil }) + if !errors.Is(err, want) || factory.calls.Load() != 0 || len(journal.updated) != 0 || len(journal.finished) != 0 || !reflect.DeepEqual(original, client.files) { + t.Fatalf("claimed recovery changed state: %v", err) + } + }) + } +} + +func TestRecoveryClaimPreventsDeploymentBeforeConnection(t *testing.T) { + for _, scenario := range []string{"same_server", "alias", "claim_failure", "list_failure"} { + t.Run(scenario, func(t *testing.T) { + server, client, record := recoveryPreflightFixture() + journal := &claimedOperationJournal{memoryTransactionJournal: memoryTransactionJournal{pending: []TransactionRecord{record}}, claimed: true} + if scenario == "alias" { + server.ID = "alias" + } + if scenario == "claim_failure" { + journal.claimErr = errors.New("private claim error") + } + if scenario == "list_failure" { + journal.listErr = errors.New("private list error") + } + factory := &operationFactory{client: client} + service := NewServiceWithJournal(factory, journal) + report := service.Deploy(t.Context(), []Target{{Server: server}}, testArtifact(t)) + if report.Status != "failed" || report.Failed != 1 || factory.calls.Load() != 0 || len(journal.pending) != 1 { + t.Fatalf("claimed deployment entered remote: %+v", report) + } + }) + } +} + +func TestUnclaimedRecoveryStillRuns(t *testing.T) { + server, client, record := recoveryPreflightFixture() + journal := &claimedOperationJournal{memoryTransactionJournal: memoryTransactionJournal{pending: []TransactionRecord{record}}} + factory := &operationFactory{client: client} + service := NewServiceWithJournal(factory, journal) + service.VerifyRecoveryTarget = func(context.Context, Server, RecoveryBaseline, bool) error { return nil } + if err := service.RecoverPending(t.Context(), func(context.Context, string) (Server, error) { return server, nil }); err != nil { + t.Fatal(err) + } + if factory.calls.Load() != 1 || len(journal.finished) != 1 || journal.finished[0].status != TransactionStatusRolledBack { + t.Fatal("unclaimed recovery was suppressed") + } +} + +func TestCanceledOperationDoesNotLeakGate(t *testing.T) { + service := NewService(nil) + canceled, cancel := context.WithCancel(t.Context()) + cancel() + if _, err := service.acquireOperation(canceled); !errors.Is(err, context.Canceled) { + t.Fatal(err) + } + for range 3 { + ctx, stop := context.WithTimeout(t.Context(), time.Second) + unlock, err := service.acquireOperation(ctx) + stop() + if err != nil { + t.Fatal(err) + } + unlock() + } +} diff --git a/backend/internal/deployment/service.go b/backend/internal/deployment/service.go index 06a7efc..3be3970 100644 --- a/backend/internal/deployment/service.go +++ b/backend/internal/deployment/service.go @@ -11,6 +11,7 @@ import ( "os" "sort" "strings" + "sync" "time" ) @@ -57,6 +58,8 @@ type Report struct { } type Service struct { + operationOnce sync.Once + operationGate chan struct{} factory ClientFactory journal TransactionJournal now func() time.Time @@ -72,6 +75,11 @@ func NewServiceWithJournal(factory ClientFactory, journal TransactionJournal) *S } func (s *Service) Deploy(ctx context.Context, targets []Target, artifact Artifact) Report { + release, err := s.acquireOperation(ctx) + if err != nil { + return operationFailure(targets, err) + } + defer release() return s.deploy(ctx, targets, artifact, nil) } @@ -150,6 +158,11 @@ func (s *Service) deployOne(ctx context.Context, server Server, domainName strin result.Message = err.Error() return result } + if err := s.checkDeploymentClaims(ctx, server); err != nil { + result.FailureCode = FailureUnavailable + result.Message = err.Error() + return result + } client, err := s.factory.Connect(ctx, server) if err != nil { result.FailureCode = FailureConnection @@ -752,6 +765,11 @@ func (s *Service) RecoverPending(ctx context.Context, resolve func(context.Conte if s == nil || s.journal == nil { return nil } + release, err := s.acquireOperation(ctx) + if err != nil { + return err + } + defer release() if resolve == nil { return errors.New("部署事务恢复缺少服务器解析器") } @@ -761,6 +779,10 @@ func (s *Service) RecoverPending(ctx context.Context, resolve func(context.Conte } var recoveryErrors []error for _, record := range records { + if err := s.checkRecoveryClaim(ctx, record.ID); err != nil { + recoveryErrors = append(recoveryErrors, fmt.Errorf("事务 %s 未执行自动恢复:%w", record.ID, err)) + continue + } if err := s.recoverTransaction(ctx, &record, resolve); err != nil { recoveryErrors = append(recoveryErrors, fmt.Errorf("事务 %s 恢复失败:%w", record.ID, err)) record.Status = TransactionStatusRecoveryRequired diff --git a/backend/internal/deployment/verified.go b/backend/internal/deployment/verified.go index 8900b86..59f7e81 100644 --- a/backend/internal/deployment/verified.go +++ b/backend/internal/deployment/verified.go @@ -31,6 +31,11 @@ func (s *Service) DeployVerified(ctx context.Context, targets []Target, artifact if verification.Verify == nil { return Report{Status: "failed"}, errors.New("缺少部署后站点校验,未执行推送") } + release, err := s.acquireOperation(ctx) + if err != nil { + return operationFailure(targets, err), err + } + defer release() pending := make([]*pendingDeployment, 0, len(targets)) // 异常退出时关闭连接但保留现场;不得以释放锁掩盖未确认的事务。 defer func() { diff --git a/backend/internal/deployment/verified_test.go b/backend/internal/deployment/verified_test.go index ae91279..f156c74 100644 --- a/backend/internal/deployment/verified_test.go +++ b/backend/internal/deployment/verified_test.go @@ -12,6 +12,7 @@ import ( type lateCancellationContext struct { context.Context + armed atomic.Bool errCalls atomic.Int32 } @@ -20,7 +21,7 @@ func (c *lateCancellationContext) Done() <-chan struct{} { } func (c *lateCancellationContext) Err() error { - if c.errCalls.Add(1) > 2 { + if c.armed.Load() && c.errCalls.Add(1) > 1 { return context.Canceled } return nil @@ -49,7 +50,7 @@ func TestVerifiedDeploymentCommitUsesIndependentContextAfterCancellationRace(t * artifact := testArtifact(t) service := NewServiceWithJournal(fixedClientFactory{client}, journal) report, err := service.DeployVerified(ctx, []Target{{Server: server}}, artifact, Verification{ - Verify: func(context.Context) error { return nil }, + Verify: func(context.Context) error { parent.armed.Store(true); return nil }, }) if err != nil || report.Status != "success" || !journal.commitContextOK { t.Fatalf("unexpected deployment result: %+v, %v", report, err) diff --git a/backend/internal/repository/recovery_disposition_repository_test.go b/backend/internal/repository/recovery_disposition_repository_test.go index 62f23ed..a91c137 100644 --- a/backend/internal/repository/recovery_disposition_repository_test.go +++ b/backend/internal/repository/recovery_disposition_repository_test.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "reflect" "strings" "sync" "sync/atomic" @@ -244,6 +245,22 @@ func TestRecoveryDispositionClaimSurvivesDatabaseReopen(t *testing.T) { } defer reopened.Close() repo = NewSQLDeploymentTransactionRepository(reopened) + before, err := repo.ListPendingDeploymentTransactions(t.Context()) + if err != nil { + t.Fatal(err) + } + service := deployment.NewServiceWithJournal(nil, repo) + err = service.RecoverPending(t.Context(), func(context.Context, string) (deployment.Server, error) { + t.Error("restart attempted automatic recovery of claimed transaction") + return deployment.Server{}, errors.New("unexpected server lookup") + }) + if !errors.Is(err, deployment.ErrRecoveryDispositionBusy) { + t.Fatalf("restart did not honor durable claim: %v", err) + } + after, err := repo.ListPendingDeploymentTransactions(t.Context()) + if err != nil || !reflect.DeepEqual(before, after) { + t.Fatal("restart modified claimed original transaction", err) + } if claimed, err := repo.HasRecoveryClaim(t.Context(), intent.TransactionID); err != nil || !claimed { t.Fatal("incomplete operation lost its persistent claim", err) } diff --git a/doc/Linux生产流程测试记录.md b/doc/Linux生产流程测试记录.md index 7eca998..42d674f 100644 --- a/doc/Linux生产流程测试记录.md +++ b/doc/Linux生产流程测试记录.md @@ -1,5 +1,14 @@ # Linux 生产流程测试记录 +## 2026年10月3日20:17 完整操作互斥回归 + +- [x] `scert-operation-20261003-2011.service` 成功结束;`operation-race-vet.log` 中部署包竞态约 3.189 秒,`OPERATION_RACE_EXIT=0`、`OPERATION_VET_EXIT=0`,整组约 43.506 秒。 +- [x] 补充竞态:部署约 1.086 秒、仓储约 2.016 秒,`OPERATION_EXTRA_RACE_EXIT=0`;实际覆盖 SQLite 关库重开后服务启动识别占用及原事务不变。 +- [~] `scert-operation-full-20261003-2012.service` 正执行最新后端全量;日志为第一台隔离目录中的 `operation-full.log`。未修改正在运行任务的源码。 +- [x] `service.go`、`operation.go`、`operation_test.go`、`recovery_disposition_repository_test.go` 的本地与远端 SHA-256 一致;测试目录 Git 索引仅用于源码边界测试,不是正式部署。 +- [ ] 本轮未替换线上正式包,不代表公共入口、首次 Web 流程或生产验收通过。 + + ## 2026年10月3日20:04 MySQL 修复复测与磁盘复核 - [x] 原任务 `scert-disposition-extra-20261003-1945.service` 已失败终止:补充竞态通过,MySQL 迁移十二场景通过;仓储三个审计注入因错误 1419 失败。原日志保留,不记整组通过。 diff --git a/doc/Scert开发任务清单.md b/doc/Scert开发任务清单.md index 98c3fc4..a99588c 100644 --- a/doc/Scert开发任务清单.md +++ b/doc/Scert开发任务清单.md @@ -1,5 +1,13 @@ # Scert 开发任务清单 +## 2026年10月3日20:17 当前开发进度 + +- [x] 开发完整部署操作互斥及持久化占用检查;部署竞态、完整静态检查和补充并发/取消/数据库重开专项通过。 +- [~] 原生 Linux 最新后端全量仍运行中,不把专项替代全量或实机验收。 +- [ ] 下一开发任务:一次性本机确认、三类动作执行器、受保护本机协议及终端接入;复用已建立的持久化审计与共同互斥。 +- [ ] PostgreSQL 实库及正式候选包生产门禁仍待完成;Linux 范围不变,未改线上业务数据库。 + + ## 2026年10月3日20:04 开发与回归状态 - [x] MySQL 审计故障注入改用隔离实例管理连接,受限业务连接仍执行全部仓储操作;六场景复测通过。 diff --git a/doc/Scert整改任务执行清单.md b/doc/Scert整改任务执行清单.md index 90f9723..7de3b0c 100644 --- a/doc/Scert整改任务执行清单.md +++ b/doc/Scert整改任务执行清单.md @@ -1,5 +1,13 @@ # Scert 整改任务执行清单 +## 2026年10月3日20:17 当前执行状态 + +- [x] 完整部署互斥和人工占用防护已编码;部署竞态、完整静态检查、补充竞态均通过。 +- [x] SQLite 实际关库重开后由真实部署服务识别遗留占用,不连接远端、不改原事务;无占用仍正常恢复。 +- [~] 最新后端全量运行中。 +- [ ] 一次性本机确认、动作执行器及本机交互、PostgreSQL 实库、正式包与生产门禁仍未完成。 + + ## 2026年10月3日20:04 当前执行状态 - [x] 隔离 MySQL 迁移十二场景通过,仓储三动作及三个审计失败回滚场景修复后通过。 diff --git a/doc/Scert整改合并优化清单.md b/doc/Scert整改合并优化清单.md index 50237da..062250e 100644 --- a/doc/Scert整改合并优化清单.md +++ b/doc/Scert整改合并优化清单.md @@ -1,5 +1,18 @@ # Scert 整改、合并与优化清单 +## 2026年10月3日20:17 最新执行进度 + +本节覆盖下方历史状态;仅 Linux,第一台部署测试、第二台证书推送,Smail 为 `web.cdis.cn`。总体生产验收未通过。 + +- [x] R4 完整进程内互斥已接入 `Deploy`、`DeployVerified`、`RecoverPending`,覆盖推送、HTTPS、提交、回滚与连接收尾;等待可取消,不提前释放。 +- [x] 自动恢复识别持久化人工占用,查询失败或已有占用均不连接远端、不更新原事务;普通部署在连接前检查同服务器及同地址端口别名。 +- [x] 原生 Linux 部署包竞态和后端完整静态检查通过;补充并发、取消、未占用正常恢复、SQLite 关闭重开后的真实服务占用拦截竞态通过。 +- [~] 最新后端全量正在 `scert-operation-full-20261003-2012.service` 执行,尚未据此记通过;四个关键源文件本地与测试机 SHA-256 一致。 +- [x] 前一批 MySQL 迁移十二场景及仓储六场景通过;代码与文档检查点 `d801fb3` 已推送。 +- [ ] R3 下一步:一次性确认绑定受保护本机身份和准备摘要,在共同互斥及持久化占用内重新核验,执行明确的记录、提交或回滚动作;再接入本机协议和终端。 +- [ ] R4 PostgreSQL 实库、执行器并发/重启边界,以及 R5 正式候选包和三数据库首次 Web、系统邮件、DNS/ACME、公共 HTTPS、多对多部署、失败回滚等生产门禁仍待完成。 + + ## 2026年10月3日20:04 最新执行进度 本节覆盖下方历史状态;仍仅处理 Linux,生产验收未通过。 diff --git a/doc/部署事务人工处置设计.md b/doc/部署事务人工处置设计.md index 8ce4b60..a6c06b9 100644 --- a/doc/部署事务人工处置设计.md +++ b/doc/部署事务人工处置设计.md @@ -1,5 +1,14 @@ # 部署事务人工处置设计 +## 2026年10月3日20:17 互斥接入状态 + +- [x] `Service` 的共同操作门覆盖普通部署、站点核验、异常回滚、提交收尾及自动恢复;等待可取消,异常退出先收尾再释放。 +- [x] 自动恢复在解析服务器之前查询持久化占用;失败关闭且不改原事务。普通部署在连接之前检查同服务器及同地址端口别名占用。 +- [x] 竞态、静态检查及数据库重开集成专项通过,最新全量运行中。 +- [ ] 人工执行器必须加入同一操作门,消费一次性确认后建立持久化意图、重新核验准备绑定,并按动作执行真实文件/锁/严格 HTTPS 核验;失败不静默切换动作。 +- [ ] 本机协议、终端、动作级不确定结果与重启矩阵、PostgreSQL 实库及正式生产门禁仍未闭环。 + + ## 2026年10月3日20:04 实现与待接入边界 - [x] 持久化 MySQL 迁移十二场景、仓储三动作及审计失败回滚六场景通过。 diff --git a/doc/部署事务人工核查任务清单.md b/doc/部署事务人工核查任务清单.md index 8f67e53..e318612 100644 --- a/doc/部署事务人工核查任务清单.md +++ b/doc/部署事务人工核查任务清单.md @@ -1,5 +1,13 @@ # 部署事务人工核查任务清单 +## 2026年10月3日20:17 最新任务状态 + +- [x] R4-A 部署及自动恢复进程内互斥、持久化占用识别已接入;涵盖站点核验、提交、恢复核验和连接收尾,等待可取消。 +- [x] 部署竞态、完整静态检查及数据库关闭重开后真实服务拒绝自动重放通过。 +- [~] 最新后端全量仍运行中;R4-A 整体还需人工执行器接入同一互斥,不能标整项复审通过。 +- [ ] R3 一次性本机确认、记录/提交/回滚三动作及本机协议与终端,R4 PostgreSQL 实库,R5 正式包验收。 + + ## 2026年10月3日20:04 最新任务状态 - [x] 持久化补充竞态、MySQL 迁移十二场景和仓储六场景通过;仓储审计故障注入权限问题已在测试夹具层修复。