后端:
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 个
222 lines
7.7 KiB
Go
222 lines
7.7 KiB
Go
package sv
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
|
||
rootmodel "github.com/LoveLosita/smartflow/backend/services/runtime/model"
|
||
"github.com/LoveLosita/smartflow/backend/services/schedule/core/applyadapter"
|
||
schedulecontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/schedule"
|
||
"github.com/LoveLosita/smartflow/backend/shared/respond"
|
||
)
|
||
|
||
// DeleteScheduleEventByContract 把跨进程删除契约转换为既有 schedule 核心逻辑入参。
|
||
func (ss *ScheduleService) DeleteScheduleEventByContract(ctx context.Context, req schedulecontracts.DeleteScheduleEventsRequest) error {
|
||
events := make([]rootmodel.UserDeleteScheduleEvent, 0, len(req.Events))
|
||
for _, event := range req.Events {
|
||
events = append(events, rootmodel.UserDeleteScheduleEvent{
|
||
ID: event.ID,
|
||
DeleteCourse: event.DeleteCourse,
|
||
DeleteEmbeddedTask: event.DeleteEmbeddedTask,
|
||
})
|
||
}
|
||
return ss.DeleteScheduleEvent(ctx, events, req.UserID)
|
||
}
|
||
|
||
// GetScheduleFactsByWindow 暴露主动调度需要的滚动窗口日程事实。
|
||
func (ss *ScheduleService) GetScheduleFactsByWindow(ctx context.Context, req schedulecontracts.ScheduleWindowRequest) (schedulecontracts.ScheduleWindowFacts, error) {
|
||
if ss == nil || ss.scheduleDAO == nil {
|
||
return schedulecontracts.ScheduleWindowFacts{}, errors.New("schedule facts service 未初始化")
|
||
}
|
||
return ss.scheduleDAO.GetScheduleFactsByWindow(ctx, req)
|
||
}
|
||
|
||
// GetAgentWeekSchedule 为 agent provider 暴露原始周日程槽位事实。
|
||
//
|
||
// 职责边界:
|
||
// 1. 只读取 schedule 服务拥有的 schedules / schedule_events 数据;
|
||
// 2. 保留 embedded_task_id 和 can_be_embedded,避免 agent 用前端 DTO 还原时丢语义;
|
||
// 3. 不做缓存、不做前端展示聚合,调用侧负责组装 ScheduleState。
|
||
func (ss *ScheduleService) GetAgentWeekSchedule(ctx context.Context, req schedulecontracts.AgentScheduleWeekRequest) (schedulecontracts.AgentScheduleWeekResponse, error) {
|
||
if ss == nil || ss.scheduleDAO == nil {
|
||
return schedulecontracts.AgentScheduleWeekResponse{}, errors.New("schedule agent week service 未初始化")
|
||
}
|
||
if req.Week < 0 || req.Week > 25 {
|
||
return schedulecontracts.AgentScheduleWeekResponse{}, respond.WeekOutOfRange
|
||
}
|
||
schedules, err := ss.scheduleDAO.GetUserWeeklySchedule(ctx, req.UserID, req.Week)
|
||
if err != nil {
|
||
return schedulecontracts.AgentScheduleWeekResponse{}, err
|
||
}
|
||
return schedulesToAgentWeekContract(schedules), nil
|
||
}
|
||
|
||
// GetFeedbackSignal 暴露主动调度 unfinished_feedback 的日程目标定位事实。
|
||
func (ss *ScheduleService) GetFeedbackSignal(ctx context.Context, req schedulecontracts.FeedbackRequest) (schedulecontracts.FeedbackFact, bool, error) {
|
||
if ss == nil || ss.scheduleDAO == nil {
|
||
return schedulecontracts.FeedbackFact{}, false, errors.New("schedule feedback service 未初始化")
|
||
}
|
||
return ss.scheduleDAO.GetFeedbackSignal(ctx, req)
|
||
}
|
||
|
||
// ApplyActiveScheduleChanges 在 schedule 服务内执行主动调度正式写入。
|
||
//
|
||
// 职责边界:
|
||
// 1. 只把 shared 契约转换为 schedule 私有 applyadapter 入参;
|
||
// 2. 具体事务、锁定、冲突检查和写库仍由搬运后的 applyadapter 负责;
|
||
// 3. 返回结果只包含正式落库 ID,不回写 active-scheduler preview 状态。
|
||
func (ss *ScheduleService) ApplyActiveScheduleChanges(ctx context.Context, req schedulecontracts.ApplyActiveScheduleRequest) (schedulecontracts.ApplyActiveScheduleResult, error) {
|
||
if ss == nil || ss.applyAdapter == nil {
|
||
return schedulecontracts.ApplyActiveScheduleResult{}, errors.New("schedule apply adapter 未初始化")
|
||
}
|
||
result, err := ss.applyAdapter.ApplyActiveScheduleChanges(ctx, toAdapterApplyRequest(req))
|
||
if err != nil {
|
||
return schedulecontracts.ApplyActiveScheduleResult{}, err
|
||
}
|
||
return schedulecontracts.ApplyActiveScheduleResult{
|
||
ApplyID: result.ApplyID,
|
||
AppliedEventIDs: result.AppliedEventIDs,
|
||
AppliedScheduleIDs: result.AppliedScheduleIDs,
|
||
}, nil
|
||
}
|
||
|
||
func toAdapterApplyRequest(req schedulecontracts.ApplyActiveScheduleRequest) applyadapter.ApplyActiveScheduleRequest {
|
||
changes := make([]applyadapter.ApplyChange, 0, len(req.Changes))
|
||
for _, change := range req.Changes {
|
||
changes = append(changes, applyadapter.ApplyChange{
|
||
ChangeID: change.ChangeID,
|
||
ChangeType: change.ChangeType,
|
||
TargetType: change.TargetType,
|
||
TargetID: change.TargetID,
|
||
ToSlot: toAdapterSlotSpan(change.ToSlot),
|
||
DurationSections: change.DurationSections,
|
||
Metadata: cloneStringMap(change.Metadata),
|
||
})
|
||
}
|
||
return applyadapter.ApplyActiveScheduleRequest{
|
||
PreviewID: req.PreviewID,
|
||
ApplyID: req.ApplyID,
|
||
UserID: req.UserID,
|
||
CandidateID: req.CandidateID,
|
||
Changes: changes,
|
||
RequestedAt: req.RequestedAt,
|
||
TraceID: req.TraceID,
|
||
}
|
||
}
|
||
|
||
func schedulesToAgentWeekContract(schedules []rootmodel.Schedule) schedulecontracts.AgentScheduleWeekResponse {
|
||
out := make([]schedulecontracts.AgentScheduleSlot, 0, len(schedules))
|
||
for _, item := range schedules {
|
||
out = append(out, schedulecontracts.AgentScheduleSlot{
|
||
ID: item.ID,
|
||
EventID: item.EventID,
|
||
UserID: item.UserID,
|
||
Week: item.Week,
|
||
DayOfWeek: item.DayOfWeek,
|
||
Section: item.Section,
|
||
EmbeddedTaskID: cloneIntPtr(item.EmbeddedTaskID),
|
||
Status: item.Status,
|
||
Event: scheduleEventToAgentContract(item.Event),
|
||
EmbeddedTask: scheduleTaskItemToAgentContract(item.EmbeddedTask),
|
||
})
|
||
}
|
||
return schedulecontracts.AgentScheduleWeekResponse{Schedules: out}
|
||
}
|
||
|
||
func scheduleEventToAgentContract(event *rootmodel.ScheduleEvent) *schedulecontracts.AgentScheduleEvent {
|
||
if event == nil {
|
||
return nil
|
||
}
|
||
return &schedulecontracts.AgentScheduleEvent{
|
||
ID: event.ID,
|
||
UserID: event.UserID,
|
||
Name: event.Name,
|
||
Location: cloneStringPtr(event.Location),
|
||
Type: event.Type,
|
||
RelID: cloneIntPtr(event.RelID),
|
||
TaskSourceType: event.TaskSourceType,
|
||
CanBeEmbedded: event.CanBeEmbedded,
|
||
StartTime: event.StartTime,
|
||
EndTime: event.EndTime,
|
||
}
|
||
}
|
||
|
||
func scheduleTaskItemToAgentContract(item *rootmodel.TaskClassItem) *schedulecontracts.AgentScheduleTaskItem {
|
||
if item == nil {
|
||
return nil
|
||
}
|
||
return &schedulecontracts.AgentScheduleTaskItem{
|
||
ID: item.ID,
|
||
CategoryID: cloneIntPtr(item.CategoryID),
|
||
Order: cloneIntPtr(item.Order),
|
||
Content: derefString(item.Content),
|
||
EmbeddedTime: scheduleTargetTimeToAgentContract(item.EmbeddedTime),
|
||
Status: cloneIntPtr(item.Status),
|
||
}
|
||
}
|
||
|
||
func scheduleTargetTimeToAgentContract(value *rootmodel.TargetTime) *schedulecontracts.AgentScheduleTargetTime {
|
||
if value == nil {
|
||
return nil
|
||
}
|
||
return &schedulecontracts.AgentScheduleTargetTime{
|
||
Week: value.Week,
|
||
DayOfWeek: value.DayOfWeek,
|
||
SectionFrom: value.SectionFrom,
|
||
SectionTo: value.SectionTo,
|
||
}
|
||
}
|
||
|
||
func cloneIntPtr(value *int) *int {
|
||
if value == nil {
|
||
return nil
|
||
}
|
||
copied := *value
|
||
return &copied
|
||
}
|
||
|
||
func cloneStringPtr(value *string) *string {
|
||
if value == nil {
|
||
return nil
|
||
}
|
||
copied := *value
|
||
return &copied
|
||
}
|
||
|
||
func derefString(value *string) string {
|
||
if value == nil {
|
||
return ""
|
||
}
|
||
return *value
|
||
}
|
||
|
||
func toAdapterSlotSpan(span *schedulecontracts.SlotSpan) *applyadapter.SlotSpan {
|
||
if span == nil {
|
||
return nil
|
||
}
|
||
return &applyadapter.SlotSpan{
|
||
Start: applyadapter.Slot{
|
||
Week: span.Start.Week,
|
||
DayOfWeek: span.Start.DayOfWeek,
|
||
Section: span.Start.Section,
|
||
},
|
||
End: applyadapter.Slot{
|
||
Week: span.End.Week,
|
||
DayOfWeek: span.End.DayOfWeek,
|
||
Section: span.End.Section,
|
||
},
|
||
DurationSections: span.DurationSections,
|
||
}
|
||
}
|
||
|
||
func cloneStringMap(input map[string]string) map[string]string {
|
||
if len(input) == 0 {
|
||
return nil
|
||
}
|
||
output := make(map[string]string, len(input))
|
||
for key, value := range input {
|
||
output[key] = value
|
||
}
|
||
return output
|
||
}
|