Files
smartmate/backend/active_scheduler/apply/validate.go
LoveLosita e945578fbf Version: 0.9.59.dev.260430
后端:
1. 主动调度预览确认主链路落地——新增主动调度数据模型、DAO 与事件契约;接入 dry-run pipeline 与任务触发的 job upsert/cancel;新增 preview 查询与 confirm API,支持 apply_id 幂等确认并同步写入 task_pool 日程
2. 同步更新主动调度实施文档的阶段状态与验收记录

前端:
3. AssistantPanel 脚本层继续解耦——私有类型迁移到独立类型文件,并抽离会话、工具轨迹、思考摘要、任务表单等纯函数辅助逻辑;保持助手面板模板与样式不变,降低表现层回归风险
2026-04-30 12:05:15 +08:00

99 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package apply
import (
"strings"
"time"
"github.com/LoveLosita/smartflow/backend/model"
)
// IsPreviewExpired 判断 preview 是否已经超过确认有效期。
//
// 职责边界:
// 1. 只比较 expires_at 与调用方传入的 now
// 2. 不读取数据库,也不更新 preview.status
// 3. now 为空时按“不能安全确认”处理,避免调用方误放过过期预览。
func IsPreviewExpired(preview model.ActiveSchedulePreview, now time.Time) bool {
if now.IsZero() || preview.ExpiresAt.IsZero() {
return true
}
return !now.Before(preview.ExpiresAt)
}
// IsPreviewOwnedByUser 判断 preview 是否归属当前用户。
//
// 职责边界:
// 1. 只做 user_id 等值判断;
// 2. userID 非法时直接返回 false
// 3. 不判断用户是否仍存在,该事实应由 API 鉴权或接入层保证。
func IsPreviewOwnedByUser(preview model.ActiveSchedulePreview, userID int) bool {
return userID > 0 && preview.UserID == userID
}
// IsPreviewAlreadyApplied 判断 preview 是否已经成功应用过。
//
// 职责边界:
// 1. 同时兼容 preview.status 与 apply_status 两个字段;
// 2. 只识别“已成功应用”,不把 failed/rejected 视为成功;
// 3. 返回 true 时主线程应避免再次写正式日程。
func IsPreviewAlreadyApplied(preview model.ActiveSchedulePreview) bool {
return preview.Status == model.ActiveSchedulePreviewStatusApplied ||
preview.ApplyStatus == model.ActiveScheduleApplyStatusApplied
}
// ValidatePreviewConfirmable 执行 confirm 入口的基础 preview 判断。
//
// 职责边界:
// 1. 只校验预览归属、过期、状态与已应用等轻量规则;
// 2. 不校验 task/schedule 当前真值,也不判断冲突,正式重校验由 apply port 完成;
// 3. 返回 nil 表示可以继续做候选转换,返回 ApplyError 表示本次 confirm 应被拒绝。
func ValidatePreviewConfirmable(preview model.ActiveSchedulePreview, userID int, now time.Time) error {
if preview.ID == "" {
return newApplyError(ErrorCodeTargetNotFound, "预览不存在或未加载", nil)
}
if !IsPreviewOwnedByUser(preview, userID) {
return newApplyError(ErrorCodeForbidden, "预览不属于当前用户", nil)
}
if IsPreviewExpired(preview, now) || preview.Status == model.ActiveSchedulePreviewStatusExpired || preview.ApplyStatus == model.ActiveScheduleApplyStatusExpired {
return newApplyError(ErrorCodeExpired, "预览已过期,请重新生成建议", nil)
}
if IsPreviewAlreadyApplied(preview) {
return newApplyError(ErrorCodeAlreadyApplied, "该预览已经应用过,不能重复写入日程", nil)
}
if preview.Status == model.ActiveSchedulePreviewStatusIgnored {
return newApplyError(ErrorCodeInvalidRequest, "该预览已被忽略,不能继续确认", nil)
}
if preview.Status == model.ActiveSchedulePreviewStatusFailed {
return newApplyError(ErrorCodeInvalidRequest, "该预览生成失败,不能继续确认", nil)
}
if preview.Status != "" && preview.Status != model.ActiveSchedulePreviewStatusReady && preview.Status != model.ActiveSchedulePreviewStatusPending {
return newApplyError(ErrorCodeInvalidRequest, "预览状态不允许确认", nil)
}
if preview.ApplyStatus != "" &&
preview.ApplyStatus != model.ActiveScheduleApplyStatusNone &&
preview.ApplyStatus != model.ActiveScheduleApplyStatusFailed &&
preview.ApplyStatus != model.ActiveScheduleApplyStatusRejected {
return newApplyError(ErrorCodeInvalidRequest, "当前 apply 状态不允许重新确认", nil)
}
return nil
}
// DetectIdempotencyConflict 判断同一个 preview_id + idempotency_key 是否被复用于不同请求体。
//
// 职责边界:
// 1. 只比较当前请求和 preview 已记录的 apply_idempotency_key / apply_request_hash
// 2. 不查询数据库唯一约束,主线程仍需要在事务或行锁内调用;
// 3. 返回 true 表示必须拒绝,避免同 key 不同内容污染审计链路。
func DetectIdempotencyConflict(preview model.ActiveSchedulePreview, requestHash string, idempotencyKey string) bool {
if strings.TrimSpace(idempotencyKey) == "" || strings.TrimSpace(preview.ApplyIdempotencyKey) == "" {
return false
}
if preview.ApplyIdempotencyKey != idempotencyKey {
return false
}
if preview.ApplyRequestHash == "" {
return false
}
return preview.ApplyRequestHash != requestHash
}