Version: 0.9.60.dev.260430
后端: 1.接入主动调度 worker 与飞书通知链路 - 新增 due job scanner 与 active_schedule.triggered workflow - 接入 notification.feishu.requested handler、飞书 webhook provider 和用户通知配置接口 - 支持 notification_records 去重、重试、skipped/dead 状态流转 - 完成 api / worker / all 启动模式装配与主动调度验收记录 2.后续要做的就是补全从异常发生到给用户推送消息之间的逻辑缺口
This commit is contained in:
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
activeapply "github.com/LoveLosita/smartflow/backend/active_scheduler/apply"
|
||||
@@ -12,6 +11,7 @@ import (
|
||||
activepreview "github.com/LoveLosita/smartflow/backend/active_scheduler/preview"
|
||||
"github.com/LoveLosita/smartflow/backend/dao"
|
||||
"github.com/LoveLosita/smartflow/backend/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PreviewConfirmService 编排第三阶段的预览生成、查询和确认应用。
|
||||
@@ -88,16 +88,19 @@ func (s *PreviewConfirmService) ConfirmPreview(ctx context.Context, req activeap
|
||||
}
|
||||
previewRow, err := s.activeDAO.GetPreviewByID(ctx, req.PreviewID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, activeapply.NewApplyError(activeapply.ErrorCodeTargetNotFound, "预览不存在或已被删除", err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if previewRow.UserID != req.UserID {
|
||||
return nil, fmt.Errorf("preview 不属于当前用户")
|
||||
return nil, activeapply.NewApplyError(activeapply.ErrorCodeForbidden, "预览不属于当前用户", nil)
|
||||
}
|
||||
if previewRow.ApplyStatus == model.ActiveScheduleApplyStatusApplied {
|
||||
if previewRow.ApplyIdempotencyKey == req.IdempotencyKey {
|
||||
return alreadyAppliedResult(*previewRow), nil
|
||||
}
|
||||
return nil, fmt.Errorf("preview 已应用,不能使用新的幂等键重复确认")
|
||||
return nil, activeapply.NewApplyError(activeapply.ErrorCodeAlreadyApplied, "预览已经应用,不能使用新的幂等键重复确认", nil)
|
||||
}
|
||||
|
||||
applyReq, err := activeapply.ConvertConfirmToApplyRequest(*previewRow, req, now)
|
||||
@@ -106,7 +109,7 @@ func (s *PreviewConfirmService) ConfirmPreview(ctx context.Context, req activeap
|
||||
return nil, err
|
||||
}
|
||||
if len(applyReq.Commands) == 0 {
|
||||
return nil, fmt.Errorf("当前候选没有可正式应用的日程变更")
|
||||
return s.markNoopApplied(ctx, *applyReq)
|
||||
}
|
||||
if err = s.markApplying(ctx, *applyReq); err != nil {
|
||||
return nil, err
|
||||
@@ -115,8 +118,9 @@ func (s *PreviewConfirmService) ConfirmPreview(ctx context.Context, req activeap
|
||||
adapterReq := toAdapterRequest(*applyReq)
|
||||
adapterResult, err := s.applyAdapter.ApplyActiveScheduleChanges(ctx, adapterReq)
|
||||
if err != nil {
|
||||
_ = s.markApplyFailed(ctx, previewRow.ID, applyReq.ApplyID, err)
|
||||
return nil, err
|
||||
classifiedErr := classifyAdapterApplyError(err)
|
||||
_ = s.markApplyFailed(ctx, previewRow.ID, applyReq.ApplyID, classifiedErr)
|
||||
return nil, classifiedErr
|
||||
}
|
||||
|
||||
result := activeapply.ApplyActiveScheduleResult{
|
||||
@@ -155,13 +159,48 @@ func (s *PreviewConfirmService) markApplying(ctx context.Context, req activeappl
|
||||
})
|
||||
}
|
||||
|
||||
// markNoopApplied 处理 notify_only / ask_user / close 这类“确认成功但不写正式日程”的候选。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只把 preview 标记为已处理,并保留幂等字段,便于同 key 重试直接命中历史结果;
|
||||
// 2. 不调用 apply adapter,因为这些 change 在转换阶段已经被归类为 skipped_changes;
|
||||
// 3. 失败时直接返回数据库错误,调用方应按系统错误处理,避免前端误以为确认成功。
|
||||
func (s *PreviewConfirmService) markNoopApplied(ctx context.Context, req activeapply.ApplyActiveScheduleRequest) (*activeapply.ConfirmResult, error) {
|
||||
result := activeapply.ApplyActiveScheduleResult{
|
||||
ApplyID: req.ApplyID,
|
||||
ApplyStatus: activeapply.ApplyStatusApplied,
|
||||
AppliedChanges: []activeapply.ApplyChange{},
|
||||
SkippedChanges: req.SkippedChanges,
|
||||
RequestHash: req.RequestHash,
|
||||
NormalizedChangeHash: req.NormalizedChangesHash,
|
||||
}
|
||||
if err := s.markApplied(ctx, req, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &activeapply.ConfirmResult{
|
||||
PreviewID: req.PreviewID,
|
||||
ApplyID: req.ApplyID,
|
||||
ApplyStatus: activeapply.ApplyStatusApplied,
|
||||
CandidateID: req.CandidateID,
|
||||
RequestHash: req.RequestHash,
|
||||
RequestBodyHash: req.RequestBodyHash,
|
||||
ApplyRequest: &req,
|
||||
ApplyResult: &result,
|
||||
SkippedChanges: req.SkippedChanges,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *PreviewConfirmService) markApplied(ctx context.Context, req activeapply.ApplyActiveScheduleRequest, result activeapply.ApplyActiveScheduleResult) error {
|
||||
now := s.now()
|
||||
appliedChangesJSON := mustJSON(result.AppliedChanges)
|
||||
appliedEventIDsJSON := mustJSON(result.AppliedEventIDs)
|
||||
return s.activeDAO.UpdatePreviewFields(ctx, req.PreviewID, map[string]any{
|
||||
"status": model.ActiveSchedulePreviewStatusApplied,
|
||||
"apply_id": req.ApplyID,
|
||||
"apply_status": model.ActiveScheduleApplyStatusApplied,
|
||||
"apply_candidate_id": req.CandidateID,
|
||||
"apply_idempotency_key": req.IdempotencyKey,
|
||||
"apply_request_hash": req.RequestHash,
|
||||
"applied_changes_json": &appliedChangesJSON,
|
||||
"applied_event_ids_json": &appliedEventIDsJSON,
|
||||
"apply_error": nil,
|
||||
@@ -177,8 +216,19 @@ func (s *PreviewConfirmService) markApplyFailed(ctx context.Context, previewID s
|
||||
if err != nil {
|
||||
message = err.Error()
|
||||
}
|
||||
status := model.ActiveScheduleApplyStatusFailed
|
||||
if applyErr, ok := activeapply.AsApplyError(err); ok {
|
||||
switch applyErr.Code {
|
||||
case activeapply.ErrorCodeExpired:
|
||||
status = model.ActiveScheduleApplyStatusExpired
|
||||
case activeapply.ErrorCodeDBError:
|
||||
status = model.ActiveScheduleApplyStatusFailed
|
||||
default:
|
||||
status = model.ActiveScheduleApplyStatusRejected
|
||||
}
|
||||
}
|
||||
updates := map[string]any{
|
||||
"apply_status": model.ActiveScheduleApplyStatusFailed,
|
||||
"apply_status": status,
|
||||
"apply_error": &message,
|
||||
}
|
||||
if applyID != "" {
|
||||
@@ -187,6 +237,40 @@ func (s *PreviewConfirmService) markApplyFailed(ctx context.Context, previewID s
|
||||
return s.activeDAO.UpdatePreviewFields(ctx, previewID, updates)
|
||||
}
|
||||
|
||||
// classifyAdapterApplyError 把正式写库 adapter 的错误转换为 confirm 层统一错误码。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只处理 applyadapter 已声明的业务错误码,保持 API 层只理解 active_scheduler/apply 包;
|
||||
// 2. 未知错误统一归为 db_error,避免把真实系统故障错误映射为用户可修正的 4xx;
|
||||
// 3. 原始错误作为 cause 保留,日志和 apply_error 仍能追到 adapter 返回的完整信息。
|
||||
func classifyAdapterApplyError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var adapterErr *applyadapter.ApplyError
|
||||
if !errors.As(err, &adapterErr) {
|
||||
return activeapply.NewApplyError(activeapply.ErrorCodeDBError, "主动调度正式写库失败", err)
|
||||
}
|
||||
switch adapterErr.Code {
|
||||
case applyadapter.ErrorCodeInvalidRequest:
|
||||
return activeapply.NewApplyError(activeapply.ErrorCodeInvalidRequest, adapterErr.Message, err)
|
||||
case applyadapter.ErrorCodeUnsupportedChangeType:
|
||||
return activeapply.NewApplyError(activeapply.ErrorCodeUnsupportedChangeType, adapterErr.Message, err)
|
||||
case applyadapter.ErrorCodeTargetNotFound:
|
||||
return activeapply.NewApplyError(activeapply.ErrorCodeTargetNotFound, adapterErr.Message, err)
|
||||
case applyadapter.ErrorCodeTargetCompleted:
|
||||
return activeapply.NewApplyError(activeapply.ErrorCodeTargetCompleted, adapterErr.Message, err)
|
||||
case applyadapter.ErrorCodeTargetAlreadyScheduled:
|
||||
return activeapply.NewApplyError(activeapply.ErrorCodeTargetAlreadySchedule, adapterErr.Message, err)
|
||||
case applyadapter.ErrorCodeSlotConflict:
|
||||
return activeapply.NewApplyError(activeapply.ErrorCodeSlotConflict, adapterErr.Message, err)
|
||||
case applyadapter.ErrorCodeInvalidEditedChanges:
|
||||
return activeapply.NewApplyError(activeapply.ErrorCodeInvalidEditedChanges, adapterErr.Message, err)
|
||||
default:
|
||||
return activeapply.NewApplyError(activeapply.ErrorCodeDBError, adapterErr.Message, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PreviewConfirmService) now() time.Time {
|
||||
if s == nil || s.clock == nil {
|
||||
return time.Now()
|
||||
|
||||
Reference in New Issue
Block a user