保留管理与业务续期调度,增加系统级幂等任务、取消传播和统计审计原子收尾;前端跟踪真实任务状态。原生 Linux 全后端、静态检查、相关专项二十次重复及前端二百七十二项测试和生产构建通过,未进行正式生产部署。
162 lines
6.9 KiB
Go
162 lines
6.9 KiB
Go
package httpapi
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"errors"
|
||
"fmt"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
|
||
"git.pcux.cn/xuans/Scert/backend/internal/domain"
|
||
"git.pcux.cn/xuans/Scert/backend/internal/repository"
|
||
)
|
||
|
||
// 全局检查不是业务域名,也不是单次管理证书签发;子续期仍由原调度器负责。
|
||
func (s *Server) submitRenewalTask(c *gin.Context) {
|
||
operatorID := s.currentOperatorID(c)
|
||
if err := s.authorizeRenewalOperator(c.Request.Context(), operatorID); err != nil {
|
||
s.writeError(c, http.StatusForbidden, "只有当前有效的超级管理员可以执行全局续期检查", nil)
|
||
return
|
||
}
|
||
key := strings.TrimSpace(c.GetHeader("Idempotency-Key"))
|
||
if key != "" && !validTaskIdempotencyKey(key) {
|
||
s.writeError(c, http.StatusBadRequest, "幂等键格式无效", nil)
|
||
return
|
||
}
|
||
s.issueTaskMu.Lock()
|
||
defer s.issueTaskMu.Unlock()
|
||
if key != "" {
|
||
item, err := s.tasks.FindTaskByIdempotencyKey(c.Request.Context(), repository.RenewalCheckResourceID, key)
|
||
if err == nil {
|
||
if item.TaskType != "renewal_check" || item.ScopeType != repository.ScopeSystem {
|
||
s.writeError(c, http.StatusConflict, "幂等键已用于另一种操作", nil)
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"task": item, "replayed": true})
|
||
return
|
||
}
|
||
if !errors.Is(err, sql.ErrNoRows) {
|
||
s.writeError(c, http.StatusInternalServerError, "检查续期幂等任务失败", err)
|
||
return
|
||
}
|
||
}
|
||
if active, err := s.tasks.FindActiveTaskByDomain(c.Request.Context(), repository.RenewalCheckResourceID, "renewal_check"); err == nil {
|
||
c.JSON(http.StatusConflict, gin.H{"error": "全局续期检查已在执行,请查看现有任务", "task": active, "replayed": true})
|
||
return
|
||
} else if !errors.Is(err, sql.ErrNoRows) {
|
||
s.writeError(c, http.StatusInternalServerError, "检查活动续期任务失败", err)
|
||
return
|
||
}
|
||
now := time.Now().UTC()
|
||
task := repository.Task{
|
||
HistoryScope: repository.HistoryScope{ScopeType: repository.ScopeSystem},
|
||
ID: domain.NewID(), DomainID: repository.RenewalCheckResourceID, TaskType: "renewal_check",
|
||
Status: "queued", Stage: "queued", Message: "等待检查管理证书和业务域名续期",
|
||
OperatorID: operatorID, IdempotencyKey: key, CreatedAt: now, UpdatedAt: now,
|
||
}
|
||
if err := s.tasks.CreateTask(c.Request.Context(), task); err != nil {
|
||
s.writeError(c, http.StatusInternalServerError, "创建续期检查任务失败", err)
|
||
return
|
||
}
|
||
attempt := repository.TaskAttempt{
|
||
ID: domain.NewID(), TaskID: task.ID, AttemptNumber: 1, Stage: "renewal_check",
|
||
Status: "running", Message: task.Message, CreatedAt: now,
|
||
}
|
||
if err := s.tasks.CreateAttempt(c.Request.Context(), attempt); err != nil {
|
||
finishCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
defer cancel()
|
||
if finishErr := s.tasks.FinishTaskAttempt(finishCtx, repository.TaskCompletion{
|
||
TaskID: task.ID, Status: "failed", Stage: "record", Message: "创建续期尝试失败,未执行续期",
|
||
FinishedAt: time.Now().UTC(), WithoutAttempt: true, AuditAction: task.TaskType,
|
||
}); finishErr != nil && s.logger != nil {
|
||
s.logger.Error("续期任务收尾失败,保留启动恢复状态", "task_id", task.ID, "error", finishErr)
|
||
}
|
||
s.writeError(c, http.StatusInternalServerError, "创建续期尝试失败", err)
|
||
return
|
||
}
|
||
if s.enqueueTaskWorker(c, task, attempt, func(ctx context.Context) {
|
||
s.runRenewalTask(ctx, task, attempt)
|
||
}, nil) {
|
||
writeAcceptedCertificateTask(c, task)
|
||
}
|
||
}
|
||
|
||
func (s *Server) authorizeRenewalOperator(ctx context.Context, operatorID string) error {
|
||
if s.users == nil || operatorID == "" {
|
||
return repository.ErrProjectAccessDenied
|
||
}
|
||
user, err := s.users.GetUserByID(ctx, operatorID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if user.Role != repository.UserRoleSuperAdmin || user.Status != repository.UserStatusActive {
|
||
return repository.ErrProjectAccessDenied
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *Server) runRenewalTask(ctx context.Context, task repository.Task, attempt repository.TaskAttempt) {
|
||
if s.finishCancelledTask(task, attempt, ctx) {
|
||
return
|
||
}
|
||
if err := s.authorizeRenewalOperator(ctx, task.OperatorID); err != nil {
|
||
attempt.Stage = "authorize"
|
||
s.finishTaskBeforeExecution(task, attempt, "failed", "续期执行前超级管理员权限校验未通过,未执行续期")
|
||
return
|
||
}
|
||
now := time.Now().UTC()
|
||
if err := s.tasks.UpdateTask(ctx, task.ID, "running", "renewal_check", "正在检查管理证书和业务域名续期", &now, nil); err != nil {
|
||
if !s.finishCancelledTask(task, attempt, ctx) {
|
||
s.finishTaskBeforeExecution(task, attempt, "failed", "续期检查启动失败,未执行续期")
|
||
}
|
||
return
|
||
}
|
||
if s.audit != nil {
|
||
if err := s.audit.CreateAuditLog(ctx, repository.AuditLog{
|
||
TaskID: task.ID, OperatorID: task.OperatorID, Action: task.TaskType,
|
||
ResourceType: "task", ResourceID: task.ID, Status: "running", Message: "开始全局续期检查",
|
||
}); err != nil {
|
||
s.finishTaskBeforeExecution(task, attempt, "failed", "记录续期开始审计失败,未执行续期")
|
||
return
|
||
}
|
||
}
|
||
if s.finishCancelledTask(task, attempt, ctx) {
|
||
return
|
||
}
|
||
// 不继承父任务事务编号,原续期工作流自行生成子任务及部署事务。
|
||
executionCtx := repository.WithTaskOperator(ctx, task.OperatorID)
|
||
executionCtx = repository.WithTaskCancellationHooks(executionCtx, repository.TaskCancellationHooks{
|
||
Register: s.registerTaskCancel, Unregister: s.unregisterTaskCancel,
|
||
})
|
||
report, runErr := s.runRenewal(executionCtx)
|
||
s.taskMu.Lock()
|
||
defer s.taskMu.Unlock()
|
||
status, prefix := "success", "续期检查完成"
|
||
if runErr != nil || report.Failed > 0 || len(report.Errors) > 0 {
|
||
status, prefix = "failed", "续期检查未全部完成"
|
||
if report.Succeeded > 0 {
|
||
status = "partial_success"
|
||
}
|
||
}
|
||
// 即使下游忽略取消并返回成功,也不得将取消或超时伪装为整体成功。
|
||
if errors.Is(ctx.Err(), context.DeadlineExceeded) || errors.Is(runErr, context.DeadlineExceeded) {
|
||
status, prefix = "failed", "续期检查超时,以下为已执行统计"
|
||
} else if errors.Is(ctx.Err(), context.Canceled) || errors.Is(runErr, context.Canceled) {
|
||
status, prefix = "cancelled", "续期检查已取消,以下为已执行统计"
|
||
}
|
||
message := fmt.Sprintf("%s:检查 %d 项,临期 %d 项,选中 %d 项,紧急 %d 项,成功 %d 项,失败 %d 项,异常 %d 项;逐项结果请查看续期任务",
|
||
prefix, report.Checked, report.NearExpiry, report.Selected, report.Urgent, report.Succeeded, report.Failed, len(report.Errors))
|
||
finishCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
defer cancel()
|
||
if err := s.tasks.FinishTaskAttempt(finishCtx, repository.TaskCompletion{
|
||
TaskID: task.ID, AttemptID: attempt.ID, Status: status, Stage: "renewal_check",
|
||
Message: message, StartedAt: &now, FinishedAt: time.Now().UTC(), AuditAction: task.TaskType,
|
||
}); err != nil && s.logger != nil {
|
||
s.logger.Error("保存续期检查终态失败,保留启动恢复状态", "task_id", task.ID, "error", err)
|
||
}
|
||
}
|