Version: 0.9.69.dev.260504

后端:
1. 阶段 4 active-scheduler 服务边界落地,新增 `cmd/active-scheduler`、`services/active_scheduler`、`shared/contracts/activescheduler` 和 active-scheduler port,迁移 dry-run、trigger、preview、confirm zrpc 能力
2. active-scheduler outbox consumer、relay、retry loop 和 due job scanner 迁入独立服务入口,gateway `/active-schedule/*` 改为通过 zrpc client 调用
3. gateway 目录收口为 `gateway/api` + `gateway/client`,统一归档 userauth、notification、active-scheduler 的 HTTP 门面和 zrpc client
4. 将旧 `backend/active_scheduler` 领域核心下沉到 `services/active_scheduler/core`,清退旧根目录活跃实现,并补充 active-scheduler 启动期跨域依赖表检查
5. 调整单体启动与 outbox 归属,`cmd/all` 不再启动 active-scheduler workflow、scanner 或 handler

文档:
1. 更新微服务迁移计划,将阶段 4 active-scheduler 标记为首轮收口完成,并明确下一阶段进入 schedule / task / course / task-class
This commit is contained in:
Losita
2026-05-04 21:01:00 +08:00
parent abe3b4960e
commit 4d9a5c4d30
66 changed files with 2048 additions and 466 deletions

View File

@@ -0,0 +1,118 @@
package activescheduler
import (
"encoding/json"
"fmt"
"strings"
"time"
)
// ActiveScheduleRequest 是 gateway 调用 active-scheduler 的通用触发请求。
//
// 职责边界:
// 1. 只承载 dry-run / trigger / preview 三个入口共享的触发事实;
// 2. user_id 由 gateway 从 JWT 上下文补齐,不信任前端传入;
// 3. payload 保留原始 JSON由服务侧按 trigger_type 再解释,避免 gateway 承担领域解析。
type ActiveScheduleRequest struct {
UserID int `json:"user_id,omitempty"`
TriggerType string `json:"trigger_type" binding:"required"`
TargetType string `json:"target_type" binding:"required"`
TargetID int `json:"target_id"`
FeedbackID string `json:"feedback_id,omitempty"`
IdempotencyKey string `json:"idempotency_key,omitempty"`
MockNow *time.Time `json:"mock_now,omitempty"`
Payload json.RawMessage `json:"payload,omitempty"`
}
// TriggerResponse 是正式触发写入后的跨进程响应。
type TriggerResponse struct {
TriggerID string `json:"trigger_id"`
Status string `json:"status"`
PreviewID *string `json:"preview_id,omitempty"`
DedupeHit bool `json:"dedupe_hit"`
TraceID string `json:"trace_id,omitempty"`
}
// GetPreviewRequest 是查询主动调度预览详情的跨进程请求。
type GetPreviewRequest struct {
UserID int `json:"user_id,omitempty"`
PreviewID string `json:"preview_id"`
}
// ConfirmPreviewRequest 是确认主动调度预览的跨进程请求。
//
// 职责边界:
// 1. edited_changes 保留原始 JSON由 active-scheduler 服务侧反序列化为 apply.Change
// 2. gateway 只补 user_id / preview_id不理解正式写库命令
// 3. action / idempotency_key 继续保持现有前端请求语义。
type ConfirmPreviewRequest struct {
PreviewID string `json:"preview_id,omitempty"`
UserID int `json:"user_id,omitempty"`
CandidateID string `json:"candidate_id"`
Action string `json:"action"`
EditedChanges json.RawMessage `json:"edited_changes,omitempty"`
IdempotencyKey string `json:"idempotency_key"`
RequestedAt time.Time `json:"requested_at,omitempty"`
TraceID string `json:"trace_id,omitempty"`
}
type ApplyErrorCode string
const (
ApplyErrorCodeExpired ApplyErrorCode = "expired"
ApplyErrorCodeIdempotencyConflict ApplyErrorCode = "idempotency_conflict"
ApplyErrorCodeBaseVersionChanged ApplyErrorCode = "base_version_changed"
ApplyErrorCodeTargetNotFound ApplyErrorCode = "target_not_found"
ApplyErrorCodeTargetCompleted ApplyErrorCode = "target_completed"
ApplyErrorCodeTargetAlreadySchedule ApplyErrorCode = "target_already_scheduled"
ApplyErrorCodeSlotConflict ApplyErrorCode = "slot_conflict"
ApplyErrorCodeInvalidEditedChanges ApplyErrorCode = "invalid_edited_changes"
ApplyErrorCodeUnsupportedChangeType ApplyErrorCode = "unsupported_change_type"
ApplyErrorCodeDBError ApplyErrorCode = "db_error"
ApplyErrorCodeInvalidRequest ApplyErrorCode = "invalid_request"
ApplyErrorCodeForbidden ApplyErrorCode = "forbidden"
ApplyErrorCodeAlreadyApplied ApplyErrorCode = "already_applied"
)
const (
ApplyStatusRejected = "rejected"
ApplyStatusExpired = "expired"
ApplyStatusFailed = "failed"
)
// ApplyError 是 active-scheduler confirm/apply 业务拒绝的跨进程错误。
//
// 职责边界:
// 1. 只承载可映射到 HTTP 4xx/5xx 的错误码与展示文案;
// 2. 不暴露服务内部 applyadapter、DAO 或事务错误类型;
// 3. cause 仅用于 gateway 日志串联,响应体只使用 code/message。
type ApplyError struct {
Code ApplyErrorCode
Message string
Cause error
}
func (e *ApplyError) Error() string {
if e == nil {
return ""
}
message := strings.TrimSpace(e.Message)
if message == "" {
return string(e.Code)
}
return fmt.Sprintf("%s: %s", e.Code, message)
}
func (e *ApplyError) Unwrap() error {
if e == nil {
return nil
}
return e.Cause
}
// ConfirmErrorResult 是 confirm/apply 失败时仍返回给前端的稳定数据形状。
type ConfirmErrorResult struct {
ApplyStatus string `json:"apply_status"`
ErrorCode ApplyErrorCode `json:"error_code,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}

View File

@@ -0,0 +1,22 @@
package ports
import (
"context"
"encoding/json"
contracts "github.com/LoveLosita/smartflow/backend/shared/contracts/activescheduler"
)
// ActiveSchedulerCommandClient 是 gateway 调用 active-scheduler 服务的最小能力集合。
//
// 职责边界:
// 1. 只覆盖当前 HTTP 入口需要的 dry-run / trigger / preview / confirm
// 2. 不暴露 active-scheduler 的 DAO、graph、selection、job scanner 或 outbox consumer
// 3. 复杂响应先以原始 JSON 透传,避免 gateway 重建一套主动调度 DTO。
type ActiveSchedulerCommandClient interface {
DryRun(ctx context.Context, req contracts.ActiveScheduleRequest) (json.RawMessage, error)
Trigger(ctx context.Context, req contracts.ActiveScheduleRequest) (*contracts.TriggerResponse, error)
CreatePreview(ctx context.Context, req contracts.ActiveScheduleRequest) (json.RawMessage, error)
GetPreview(ctx context.Context, req contracts.GetPreviewRequest) (json.RawMessage, error)
ConfirmPreview(ctx context.Context, req contracts.ConfirmPreviewRequest) (json.RawMessage, error)
}