Scert/backend/internal/repository/recovery_disposition_completion.go

120 lines
5.8 KiB
Go

package repository
import (
"context"
"database/sql"
"time"
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
)
// FinishRecoveryDisposition 仅由完成现场核验的内部执行器调用,不接受公网客户端结果。
// 不确定结果保留占用;成功结果、必要的原事务终态与结束审计在同一事务内提交。
func (r *SQLDeploymentTransactionRepository) FinishRecoveryDisposition(ctx context.Context, id, resultCode string) (deployment.RecoveryDisposition, error) {
if r == nil || r.db == nil {
return deployment.RecoveryDisposition{}, dispositionError(ctx)
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return deployment.RecoveryDisposition{}, dispositionError(ctx)
}
defer tx.Rollback()
query := `SELECT ` + recoveryDispositionColumns + ` FROM recovery_dispositions WHERE id = ?`
if r.driver != "sqlite" {
query += " FOR UPDATE"
}
item, err := scanRecoveryDisposition(tx.QueryRowContext(ctx, r.query(query), id))
if err != nil {
return deployment.RecoveryDisposition{}, dispositionError(ctx)
}
if !item.Intent.AllowsResult(resultCode) {
return deployment.RecoveryDisposition{}, deployment.ErrRecoveryDispositionInvalid
}
if item.Status != "running" {
if item.ResultCode == resultCode {
return item, nil
}
return deployment.RecoveryDisposition{}, deployment.ErrRecoveryDispositionConflict
}
var claim, start string
if err := tx.QueryRowContext(ctx, r.query(`SELECT disposition_id FROM recovery_claims WHERE transaction_id = ?`), item.Intent.TransactionID).Scan(&claim); err != nil || claim != id {
return deployment.RecoveryDisposition{}, deployment.ErrRecoveryDispositionConflict
}
if err := tx.QueryRowContext(ctx, r.query(`SELECT event_type FROM recovery_events WHERE disposition_id = ? AND event_type = 'started'`), id).Scan(&start); err != nil {
return deployment.RecoveryDisposition{}, dispositionError(ctx)
}
if item.Intent.SourceDispositionID != "" {
if err := r.validateReconciliationSource(ctx, tx, item.Intent); err != nil {
return deployment.RecoveryDisposition{}, err
}
}
now := time.Now().UTC()
if err := r.finishRecoveryOriginal(ctx, tx, item.Intent, resultCode, now); err != nil {
return deployment.RecoveryDisposition{}, err
}
item.Status = "succeeded"
if resultCode == deployment.RecoveryResultRejected || resultCode == deployment.RecoveryResultUncertain {
item.Status = resultCode
}
result, err := tx.ExecContext(ctx, r.query(`UPDATE recovery_dispositions SET status = ?, result_code = ?, updated_at = ? WHERE id = ? AND status = 'running'`), item.Status, resultCode, now, id)
if err != nil || !oneRecoveryRow(result) {
return deployment.RecoveryDisposition{}, dispositionError(ctx)
}
if _, err := tx.ExecContext(ctx, r.query(`INSERT INTO recovery_events (disposition_id, event_type, result_code, created_at) VALUES (?, 'finished', ?, ?)`), id, resultCode, now); err != nil {
return deployment.RecoveryDisposition{}, dispositionError(ctx)
}
if item.Intent.SourceDispositionID != "" && (resultCode == deployment.RecoveryResultRejected || resultCode == deployment.RecoveryResultUncertain) {
// 只读核查未通过时归还原占用,保留两次审计;绝不解锁原事务。
result, err := tx.ExecContext(ctx, r.query(`UPDATE recovery_claims SET disposition_id = ?, created_at = ? WHERE transaction_id = ? AND disposition_id = ?`), item.Intent.SourceDispositionID, now, item.Intent.TransactionID, id)
if err != nil || !oneRecoveryRow(result) {
return deployment.RecoveryDisposition{}, dispositionError(ctx)
}
} else if resultCode != deployment.RecoveryResultUncertain {
result, err := tx.ExecContext(ctx, r.query(`DELETE FROM recovery_claims WHERE transaction_id = ? AND disposition_id = ?`), item.Intent.TransactionID, id)
if err != nil || !oneRecoveryRow(result) {
return deployment.RecoveryDisposition{}, dispositionError(ctx)
}
}
if err := tx.Commit(); err != nil {
return deployment.RecoveryDisposition{}, dispositionError(ctx)
}
item.ResultCode, item.UpdatedAt = resultCode, now
return item, nil
}
func oneRecoveryRow(result sql.Result) bool {
if result == nil {
return false
}
count, err := result.RowsAffected()
return err == nil && count == 1
}
func (r *SQLDeploymentTransactionRepository) finishRecoveryOriginal(ctx context.Context, tx *sql.Tx, intent deployment.RecoveryDispositionIntent, resultCode string, now time.Time) error {
if resultCode != deployment.RecoveryResultCommitted && resultCode != deployment.RecoveryResultRolledBack {
// 只记录核查、核验拒绝与不确定结果均不改变原事务;不能以已阅读代替恢复成功。
return nil
}
record, err := r.lockRecoveryTransaction(ctx, tx, intent.TransactionID)
if err != nil {
return dispositionError(ctx)
}
phase, status, message := deployment.TransactionPhaseCommitVerified, deployment.TransactionStatusCommitted, "人工处置已完成提交核验"
if resultCode == deployment.RecoveryResultRolledBack {
phase, status, message = deployment.TransactionPhaseRollbackVerified, deployment.TransactionStatusRolledBack, "人工处置已完成回滚核验"
}
if record.ServerID != intent.ServerID || record.Evidence == nil || record.Phase != phase ||
(record.Status != deployment.TransactionStatusPending && record.Status != deployment.TransactionStatusRecoveryRequired) {
return deployment.ErrRecoveryDispositionConflict
}
if intent.SourceDispositionID != "" && !intent.MatchesRecord(record) {
return deployment.ErrRecoveryReviewChanged
}
result, err := tx.ExecContext(ctx, r.query(`UPDATE deployment_transactions SET status = ?, phase = ?, message = ?, updated_at = ?
WHERE id = ? AND phase = ? AND status IN ('pending', 'recovery_required')`), status, status, message, now, intent.TransactionID, phase)
if err != nil || !oneRecoveryRow(result) {
return dispositionError(ctx)
}
return nil
}