后端:
1.阶段 6 CP4/CP5 目录收口与共享边界纯化
- 将 backend 根目录收口为 services、client、gateway、cmd、shared 五个一级目录
- 收拢 bootstrap、inits、infra/kafka、infra/outbox、conv、respond、pkg、middleware,移除根目录旧实现与空目录
- 将 utils 下沉到 services/userauth/internal/auth,将 logic 下沉到 services/schedule/core/planning
- 将迁移期 runtime 桥接实现统一收拢到 services/runtime/{conv,dao,eventsvc,model},删除 shared/legacy 与未再被 import 的旧 service 实现
- 将 gateway/shared/respond 收口为 HTTP/Gin 错误写回适配,shared/respond 仅保留共享错误语义与状态映射
- 将 HTTP IdempotencyMiddleware 与 RateLimitMiddleware 收口到 gateway/middleware
- 将 GormCachePlugin 下沉到 shared/infra/gormcache,将共享 RateLimiter 下沉到 shared/infra/ratelimit,将 agent token budget 下沉到 services/agent/shared
- 删除 InitEino 兼容壳,收缩 cmd/internal/coreinit 仅保留旧组合壳残留域初始化语义
- 更新微服务迁移计划与桌面 checklist,补齐 CP4/CP5 当前切流点、目录终态与验证结果
- 完成 go test ./...、git diff --check 与最终真实 smoke;health、register/login、task/create+get、schedule/today、task-class/list、memory/items、agent chat/meta/timeline/context-stats 全部 200,SSE 合并结果为 CP5_OK 且 [DONE] 只有 1 个
131 lines
4.6 KiB
Go
131 lines
4.6 KiB
Go
package sv
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
|
|
agentshared "github.com/LoveLosita/smartflow/backend/services/agent/shared"
|
|
"github.com/LoveLosita/smartflow/backend/services/runtime/model"
|
|
"github.com/LoveLosita/smartflow/backend/shared/respond"
|
|
)
|
|
|
|
// GetSchedulePlanPreview 按 conversation_id 读取结构化排程预览。
|
|
//
|
|
// 职责边界:
|
|
// 1. 负责参数归一化、缓存优先读取、会话归属校验和 DB 兜底。
|
|
// 2. 负责把缓存/快照 DTO 转成接口响应 DTO。
|
|
// 3. 不负责触发排程,不负责补算结果,也不负责消息链路落库。
|
|
func (s *AgentService) GetSchedulePlanPreview(ctx context.Context, userID int, chatID string) (*model.GetSchedulePlanPreviewResponse, error) {
|
|
// 1. 先校验会话参数,避免无效请求打到缓存或数据库。
|
|
normalizedChatID := strings.TrimSpace(chatID)
|
|
if normalizedChatID == "" {
|
|
return nil, respond.MissingParam
|
|
}
|
|
if s == nil {
|
|
return nil, errors.New("agent service is not initialized")
|
|
}
|
|
|
|
// 2. 优先查 Redis。
|
|
if s.cacheDAO != nil {
|
|
preview, err := s.cacheDAO.GetSchedulePlanPreviewFromCache(ctx, userID, normalizedChatID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if preview != nil {
|
|
if preview.UserID > 0 && preview.UserID != userID {
|
|
return nil, respond.SchedulePlanPreviewNotFound
|
|
}
|
|
plans := agentshared.CloneWeekSchedules(preview.CandidatePlans)
|
|
if plans == nil {
|
|
plans = make([]model.UserWeekSchedule, 0)
|
|
}
|
|
return &model.GetSchedulePlanPreviewResponse{
|
|
ConversationID: normalizedChatID,
|
|
TraceID: strings.TrimSpace(preview.TraceID),
|
|
Summary: strings.TrimSpace(preview.Summary),
|
|
CandidatePlans: plans,
|
|
HybridEntries: agentshared.CloneHybridEntries(preview.HybridEntries),
|
|
TaskClassIDs: preview.TaskClassIDs,
|
|
GeneratedAt: preview.GeneratedAt,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// 3. Redis 未命中时回源 MySQL。
|
|
if s.repo != nil {
|
|
snapshot, err := s.repo.GetScheduleStateSnapshot(ctx, userID, normalizedChatID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if snapshot != nil {
|
|
response := snapshotToSchedulePlanPreviewResponse(snapshot)
|
|
if s.cacheDAO != nil {
|
|
cachePreview := snapshotToSchedulePlanPreviewCache(snapshot)
|
|
if setErr := s.cacheDAO.SetSchedulePlanPreviewToCache(ctx, userID, normalizedChatID, cachePreview); setErr != nil {
|
|
log.Printf("回填排程预览缓存失败 chat_id=%s: %v", normalizedChatID, setErr)
|
|
}
|
|
}
|
|
return response, nil
|
|
}
|
|
}
|
|
|
|
return nil, respond.SchedulePlanPreviewNotFound
|
|
}
|
|
|
|
// snapshotToSchedulePlanPreviewCache 把 MySQL 快照映射成 Redis 预览缓存结构。
|
|
func snapshotToSchedulePlanPreviewCache(snapshot *model.SchedulePlanStateSnapshot) *model.SchedulePlanPreviewCache {
|
|
if snapshot == nil {
|
|
return nil
|
|
}
|
|
generatedAt := snapshot.UpdatedAt
|
|
if generatedAt.IsZero() {
|
|
generatedAt = time.Now()
|
|
}
|
|
return &model.SchedulePlanPreviewCache{
|
|
UserID: snapshot.UserID,
|
|
ConversationID: snapshot.ConversationID,
|
|
TraceID: strings.TrimSpace(snapshot.TraceID),
|
|
Summary: schedulePlanSummaryOrFallback(strings.TrimSpace(snapshot.FinalSummary)),
|
|
CandidatePlans: agentshared.CloneWeekSchedules(snapshot.CandidatePlans),
|
|
TaskClassIDs: append([]int(nil), snapshot.TaskClassIDs...),
|
|
HybridEntries: agentshared.CloneHybridEntries(snapshot.HybridEntries),
|
|
AllocatedItems: agentshared.CloneTaskClassItems(snapshot.AllocatedItems),
|
|
GeneratedAt: generatedAt,
|
|
}
|
|
}
|
|
|
|
// snapshotToSchedulePlanPreviewResponse 把 MySQL 快照映射成查询接口响应结构。
|
|
func snapshotToSchedulePlanPreviewResponse(snapshot *model.SchedulePlanStateSnapshot) *model.GetSchedulePlanPreviewResponse {
|
|
if snapshot == nil {
|
|
return nil
|
|
}
|
|
plans := agentshared.CloneWeekSchedules(snapshot.CandidatePlans)
|
|
if plans == nil {
|
|
plans = make([]model.UserWeekSchedule, 0)
|
|
}
|
|
generatedAt := snapshot.UpdatedAt
|
|
if generatedAt.IsZero() {
|
|
generatedAt = time.Now()
|
|
}
|
|
return &model.GetSchedulePlanPreviewResponse{
|
|
ConversationID: snapshot.ConversationID,
|
|
TraceID: strings.TrimSpace(snapshot.TraceID),
|
|
Summary: schedulePlanSummaryOrFallback(strings.TrimSpace(snapshot.FinalSummary)),
|
|
CandidatePlans: plans,
|
|
HybridEntries: agentshared.CloneHybridEntries(snapshot.HybridEntries),
|
|
TaskClassIDs: snapshot.TaskClassIDs,
|
|
GeneratedAt: generatedAt,
|
|
}
|
|
}
|
|
|
|
// schedulePlanSummaryOrFallback 统一收口排程摘要兜底文案,避免各处重复维护默认值。
|
|
func schedulePlanSummaryOrFallback(summary string) string {
|
|
if strings.TrimSpace(summary) == "" {
|
|
return "排程流程已完成,但未生成结果摘要。"
|
|
}
|
|
return summary
|
|
}
|