Version: 0.9.78.dev.260506
This commit is contained in:
329
backend/services/taskclassforum/adapter/taskclass_rpc.go
Normal file
329
backend/services/taskclassforum/adapter/taskclass_rpc.go
Normal file
@@ -0,0 +1,329 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
taskclassclient "github.com/LoveLosita/smartflow/backend/client/taskclass"
|
||||
forumsv "github.com/LoveLosita/smartflow/backend/services/taskclassforum/sv"
|
||||
taskclasscontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/taskclass"
|
||||
"github.com/LoveLosita/smartflow/backend/shared/respond"
|
||||
)
|
||||
|
||||
var errTaskClassRPCAdapterNotReady = errors.New("taskclassforum task-class rpc adapter is not initialized")
|
||||
|
||||
// TaskClassRPCAdapter 负责把 task-class 独立服务适配成计划广场需要的快照端口。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只通过 task-class zrpc 读取/创建任务类,不直连 task_classes / task_items 物理表;
|
||||
// 2. 只暴露论坛导入/发布需要的白名单快照语义,不透传 schedule 写入能力;
|
||||
// 3. 论坛业务层只依赖快照端口,后续 task-class 契约继续演进时只改这一层。
|
||||
type TaskClassRPCAdapter struct {
|
||||
client *taskclassclient.Client
|
||||
}
|
||||
|
||||
var _ forumsv.TaskClassSnapshotPort = (*TaskClassRPCAdapter)(nil)
|
||||
|
||||
// NewTaskClassRPCAdapter 创建基于 task-class zrpc 的论坛快照适配器。
|
||||
func NewTaskClassRPCAdapter(client *taskclassclient.Client) *TaskClassRPCAdapter {
|
||||
return &TaskClassRPCAdapter{client: client}
|
||||
}
|
||||
|
||||
// GetOwnedTaskClassSnapshot 读取当前用户自己的 TaskClass,并投影为论坛可分享快照。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只读取当前用户可见的单个 TaskClass;
|
||||
// 2. 只返回论坛白名单字段和条目 source id/order/content;
|
||||
// 3. 不透传 embedded_time、status 和任何 schedule 绑定细节。
|
||||
func (a *TaskClassRPCAdapter) GetOwnedTaskClassSnapshot(ctx context.Context, userID uint64, taskClassID uint64) (*forumsv.TaskClassSnapshot, error) {
|
||||
if err := a.ensureReady(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userIDInt, err := toUserID(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
taskClassIDInt, err := toTaskClassID(taskClassID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw, err := a.client.GetAgentTaskClasses(ctx, taskclasscontracts.AgentTaskClassesRequest{
|
||||
UserID: userIDInt,
|
||||
TaskClassIDs: []int{taskClassIDInt},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp taskclasscontracts.AgentTaskClassesResponse
|
||||
if err := json.Unmarshal(raw, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(resp.TaskClasses) == 0 {
|
||||
return nil, respond.UserTaskClassNotFound
|
||||
}
|
||||
|
||||
snapshot, err := snapshotFromTaskClass(resp.TaskClasses[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &snapshot, nil
|
||||
}
|
||||
|
||||
// CreateTaskClassFromSnapshot 基于论坛模板快照为当前用户创建 task-class 服务里的副本。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 只创建 task-class 主体与 items,不写 forum_imports;
|
||||
// 2. 所有 item 都作为新记录创建,不沿用原任务条目的 ID;
|
||||
// 3. 不写 schedule,导入后仍保持“当前用户自己的未安排副本”语义。
|
||||
func (a *TaskClassRPCAdapter) CreateTaskClassFromSnapshot(ctx context.Context, userID uint64, snapshot forumsv.TaskClassSnapshot, targetTitle string) (*forumsv.CreatedTaskClass, error) {
|
||||
if err := a.ensureReady(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userIDInt, err := toUserID(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
title := strings.TrimSpace(targetTitle)
|
||||
if title == "" {
|
||||
title = strings.TrimSpace(snapshot.Title)
|
||||
}
|
||||
if title == "" || strings.TrimSpace(snapshot.Mode) == "" {
|
||||
return nil, respond.MissingParam
|
||||
}
|
||||
|
||||
if _, _, err := parseSnapshotDateRange(snapshot.Mode, snapshot.StartDate, snapshot.EndDate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw, err := a.client.AddTaskClass(ctx, buildUpsertTaskClassRequest(userIDInt, title, snapshot))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var created taskclasscontracts.UpsertTaskClassResponse
|
||||
if err := json.Unmarshal(raw, &created); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if created.TaskClassID <= 0 {
|
||||
return nil, respond.InternalError(errors.New("task-class rpc add response missing task_class_id"))
|
||||
}
|
||||
return &forumsv.CreatedTaskClass{
|
||||
TaskClassID: uint64(created.TaskClassID),
|
||||
Title: title,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func snapshotFromTaskClass(taskClass taskclasscontracts.AgentTaskClass) (forumsv.TaskClassSnapshot, error) {
|
||||
items := snapshotItemsFromTaskClassItems(taskClass.Items)
|
||||
snapshot := forumsv.TaskClassSnapshot{
|
||||
TaskClassID: uint64(taskClass.ID),
|
||||
Title: strings.TrimSpace(taskClass.Name),
|
||||
Mode: strings.TrimSpace(taskClass.Mode),
|
||||
StartDate: strings.TrimSpace(taskClass.StartDate),
|
||||
EndDate: strings.TrimSpace(taskClass.EndDate),
|
||||
SubjectType: strings.TrimSpace(taskClass.SubjectType),
|
||||
DifficultyLevel: strings.TrimSpace(taskClass.DifficultyLevel),
|
||||
CognitiveIntensity: strings.TrimSpace(taskClass.CognitiveIntensity),
|
||||
TotalSlots: taskClass.TotalSlots,
|
||||
AllowFillerCourse: taskClass.AllowFillerCourse,
|
||||
Strategy: strings.TrimSpace(taskClass.Strategy),
|
||||
ExcludedSlots: cloneIntSlice(taskClass.ExcludedSlots),
|
||||
ExcludedDaysOfWeek: cloneIntSlice(taskClass.ExcludedDaysOfWeek),
|
||||
StrategyLabels: strategyLabels(taskClass.Strategy),
|
||||
Items: items,
|
||||
}
|
||||
|
||||
configJSON, err := buildConfigSnapshotJSON(snapshot)
|
||||
if err != nil {
|
||||
return forumsv.TaskClassSnapshot{}, err
|
||||
}
|
||||
snapshot.ConfigSnapshotJSON = configJSON
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
func snapshotItemsFromTaskClassItems(items []taskclasscontracts.AgentTaskClassItem) []forumsv.TaskClassSnapshotItem {
|
||||
if len(items) == 0 {
|
||||
return []forumsv.TaskClassSnapshotItem{}
|
||||
}
|
||||
|
||||
sorted := append([]taskclasscontracts.AgentTaskClassItem(nil), items...)
|
||||
sort.SliceStable(sorted, func(i, j int) bool {
|
||||
leftOrder := derefInt(sorted[i].Order)
|
||||
rightOrder := derefInt(sorted[j].Order)
|
||||
if leftOrder != rightOrder {
|
||||
return leftOrder < rightOrder
|
||||
}
|
||||
return sorted[i].ID < sorted[j].ID
|
||||
})
|
||||
|
||||
result := make([]forumsv.TaskClassSnapshotItem, 0, len(sorted))
|
||||
for _, item := range sorted {
|
||||
content := strings.TrimSpace(item.Content)
|
||||
if content == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, forumsv.TaskClassSnapshotItem{
|
||||
TaskItemID: uint64(item.ID),
|
||||
Order: derefInt(item.Order),
|
||||
Content: content,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func buildUpsertTaskClassRequest(userID int, title string, snapshot forumsv.TaskClassSnapshot) taskclasscontracts.UpsertTaskClassRequest {
|
||||
items := make([]taskclasscontracts.UpsertTaskClassItemConfig, 0, len(snapshot.Items))
|
||||
sortedItems := append([]forumsv.TaskClassSnapshotItem(nil), snapshot.Items...)
|
||||
sort.SliceStable(sortedItems, func(i, j int) bool {
|
||||
if sortedItems[i].Order != sortedItems[j].Order {
|
||||
return sortedItems[i].Order < sortedItems[j].Order
|
||||
}
|
||||
return sortedItems[i].TaskItemID < sortedItems[j].TaskItemID
|
||||
})
|
||||
for _, item := range sortedItems {
|
||||
content := strings.TrimSpace(item.Content)
|
||||
if content == "" {
|
||||
continue
|
||||
}
|
||||
items = append(items, taskclasscontracts.UpsertTaskClassItemConfig{
|
||||
Order: item.Order,
|
||||
Content: content,
|
||||
})
|
||||
}
|
||||
|
||||
return taskclasscontracts.UpsertTaskClassRequest{
|
||||
UserID: userID,
|
||||
Name: title,
|
||||
StartDate: strings.TrimSpace(snapshot.StartDate),
|
||||
EndDate: strings.TrimSpace(snapshot.EndDate),
|
||||
Mode: strings.TrimSpace(snapshot.Mode),
|
||||
SubjectType: strings.TrimSpace(snapshot.SubjectType),
|
||||
DifficultyLevel: strings.TrimSpace(snapshot.DifficultyLevel),
|
||||
CognitiveIntensity: strings.TrimSpace(snapshot.CognitiveIntensity),
|
||||
Config: taskclasscontracts.UpsertTaskClassConfig{
|
||||
TotalSlots: snapshot.TotalSlots,
|
||||
AllowFillerCourse: snapshot.AllowFillerCourse,
|
||||
Strategy: strings.TrimSpace(snapshot.Strategy),
|
||||
ExcludedSlots: cloneIntSlice(snapshot.ExcludedSlots),
|
||||
ExcludedDaysOfWeek: cloneIntSlice(snapshot.ExcludedDaysOfWeek),
|
||||
},
|
||||
Items: items,
|
||||
}
|
||||
}
|
||||
|
||||
func strategyLabels(strategy string) []string {
|
||||
trimmed := strings.TrimSpace(strategy)
|
||||
if trimmed == "" {
|
||||
return []string{}
|
||||
}
|
||||
return []string{trimmed}
|
||||
}
|
||||
|
||||
func (a *TaskClassRPCAdapter) ensureReady() error {
|
||||
if a == nil || a.client == nil {
|
||||
return errTaskClassRPCAdapterNotReady
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func toUserID(value uint64) (int, error) {
|
||||
if value == 0 || value > uint64(maxIntValue()) {
|
||||
return 0, respond.WrongUserID
|
||||
}
|
||||
return int(value), nil
|
||||
}
|
||||
|
||||
func toTaskClassID(value uint64) (int, error) {
|
||||
if value == 0 || value > uint64(maxIntValue()) {
|
||||
return 0, respond.WrongTaskClassID
|
||||
}
|
||||
return int(value), nil
|
||||
}
|
||||
|
||||
func maxIntValue() int {
|
||||
return int(^uint(0) >> 1)
|
||||
}
|
||||
|
||||
func derefInt(value *int) int {
|
||||
if value == nil {
|
||||
return 0
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func parseSnapshotDateRange(mode string, startDate string, endDate string) (*time.Time, *time.Time, error) {
|
||||
parsedStart, err := parseDatePtr(startDate)
|
||||
if err != nil {
|
||||
return nil, nil, respond.WrongParamType
|
||||
}
|
||||
parsedEnd, err := parseDatePtr(endDate)
|
||||
if err != nil {
|
||||
return nil, nil, respond.WrongParamType
|
||||
}
|
||||
|
||||
if strings.TrimSpace(mode) != "auto" {
|
||||
return parsedStart, parsedEnd, nil
|
||||
}
|
||||
if parsedStart == nil || parsedEnd == nil {
|
||||
return nil, nil, respond.MissingParamForAutoScheduling
|
||||
}
|
||||
if parsedStart.After(*parsedEnd) {
|
||||
return nil, nil, respond.InvalidDateRange
|
||||
}
|
||||
return parsedStart, parsedEnd, nil
|
||||
}
|
||||
|
||||
func buildConfigSnapshotJSON(snapshot forumsv.TaskClassSnapshot) (string, error) {
|
||||
raw, err := json.Marshal(map[string]any{
|
||||
"mode": snapshot.Mode,
|
||||
"start_date": snapshot.StartDate,
|
||||
"end_date": snapshot.EndDate,
|
||||
"subject_type": snapshot.SubjectType,
|
||||
"difficulty_level": snapshot.DifficultyLevel,
|
||||
"cognitive_intensity": snapshot.CognitiveIntensity,
|
||||
"total_slots": snapshot.TotalSlots,
|
||||
"allow_filler_course": snapshot.AllowFillerCourse,
|
||||
"strategy": snapshot.Strategy,
|
||||
"excluded_slots": cloneIntSlice(snapshot.ExcludedSlots),
|
||||
"excluded_days_of_week": cloneIntSlice(snapshot.ExcludedDaysOfWeek),
|
||||
"strategy_labels": append([]string(nil), snapshot.StrategyLabels...),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(raw), nil
|
||||
}
|
||||
|
||||
func parseDatePtr(value string) (*time.Time, error) {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parsed, err := time.ParseInLocation("2006-01-02", trimmed, time.Local)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &parsed, nil
|
||||
}
|
||||
|
||||
func cloneIntSlice(values []int) []int {
|
||||
if len(values) == 0 {
|
||||
return []int{}
|
||||
}
|
||||
return append([]int(nil), values...)
|
||||
}
|
||||
Reference in New Issue
Block a user