Files
smartmate/backend/client/course/client.go
Losita 7d324b77aa Version: 0.9.79.dev.260506
后端:
1. 本地后端启动体系收口到 `backend/scripts`,移除 `cmd/all` 聚合入口,并将仓库根兼容启动语义收敛为 `StartAPI` 别名;新增 dev-up / dev-down / services-up / services-down / dev-status / dev-logs / service-restart 脚本,统一托管多服务进程、日志、PID 与基础设施启动。
2. 课表服务超时口径统一放宽到 5 分钟,覆盖 gateway / client / rpc server / config example,避免课表导入与图片识别在长耗时场景下被内层提前截断。
3. `today` 课表查询修正为读取真实当前日期,不再使用硬编码测试日期;同时剔除旧缓存与返回结果里的 `empty` 占位事件,后端只返回真实日程,空档改由前端时间轴自行补齐。

前端:
4. 首页路由切回改为复用 `DashboardView` 实例,补 `keep-alive`、`onActivated` 与双帧缩放重算,修复从侧栏返回首页时首帧布局放大与重复加载闪动问题。
5. 首页加载态与今日时间线口径收口:移除额外 800ms `pageLoading` 人为延迟,task / schedule 改为分开驱动;时间线忽略 `empty` 事件,并统一空档文案为“无课”。
6. 收敛助手页与首页若干进场/弹性动画,降低结果卡片、微调弹窗、思考区与面板切换时的抖动感。

仓库:
7. README 补充后端本地快速启动说明,`.gitignore` 忽略 `backend/.dev` 脚本运行态产物。
2026-05-06 12:59:29 +08:00

164 lines
4.6 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 course
import (
"context"
"encoding/json"
"errors"
"strings"
"time"
coursepb "github.com/LoveLosita/smartflow/backend/services/course/rpc/pb"
coursecontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/course"
"github.com/zeromicro/go-zero/zrpc"
"google.golang.org/grpc"
)
const (
defaultEndpoint = "127.0.0.1:9087"
// 课表导入可能一次展开大量周次与节次RPC 默认超时与网关保持一致,避免内层先被截断。
defaultTimeout = 5 * time.Minute
defaultMaxRPCMessageSize = 8 * 1024 * 1024
rpcMessageSizePadding = 1024 * 1024
)
type ClientConfig struct {
Endpoints []string
Target string
Timeout time.Duration
MaxImageBytes int64
}
// Client 是 gateway 访问 course zrpc 的最小适配层。
//
// 职责边界:
// 1. 只负责跨进程 gRPC 调用和 JSON/bytes 透传,不触碰 DAO
// 2. HTTP 入参仍由 gateway/api 做基础绑定,业务校验交给 course 服务;
// 3. import 冲突通过 CourseImportConflictError 返回,让 HTTP 层保留 409 + conflicts 数据。
type Client struct {
rpc coursepb.CourseClient
}
func NewClient(cfg ClientConfig) (*Client, error) {
timeout := cfg.Timeout
if timeout <= 0 {
timeout = defaultTimeout
}
endpoints := normalizeEndpoints(cfg.Endpoints)
target := strings.TrimSpace(cfg.Target)
if len(endpoints) == 0 && target == "" {
endpoints = []string{defaultEndpoint}
}
maxMessageSize := normalizeMaxRPCMessageSize(cfg.MaxImageBytes)
zclient, err := zrpc.NewClient(zrpc.RpcClientConf{
Endpoints: endpoints,
Target: target,
NonBlock: true,
Timeout: int64(timeout / time.Millisecond),
}, zrpc.WithDialOption(grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(maxMessageSize),
grpc.MaxCallSendMsgSize(maxMessageSize),
)))
if err != nil {
return nil, err
}
client := &Client{rpc: coursepb.NewCourseClient(zclient.Conn())}
if err := client.ping(timeout); err != nil {
return nil, err
}
return client, nil
}
func (c *Client) ValidateCourse(ctx context.Context, req coursecontracts.UserCheckCourseRequest) error {
_, err := c.callJSON(ctx, c.rpc.ValidateCourse, req)
return responseFromRPCError(err)
}
func (c *Client) ImportCourses(ctx context.Context, req coursecontracts.UserImportCoursesRequest) (json.RawMessage, error) {
resp, err := c.callJSON(ctx, c.rpc.ImportCourses, req)
raw, err := jsonFromResponse(resp, err)
if err != nil {
return nil, err
}
var result coursecontracts.ImportCoursesResult
if err := json.Unmarshal(raw, &result); err != nil {
return nil, err
}
if result.Conflict {
return nil, CourseImportConflictError{conflicts: result.Conflicts}
}
return raw, nil
}
func (c *Client) ParseCourseTableImage(ctx context.Context, req coursecontracts.CourseImageParseRequest) (json.RawMessage, error) {
resp, err := c.rpc.ParseCourseImage(ctx, &coursepb.CourseImageRequest{
Filename: req.Filename,
MimeType: req.MIMEType,
ImageBytes: req.ImageBytes,
})
return jsonFromResponse(resp, err)
}
func (c *Client) ensureReady() error {
if c == nil || c.rpc == nil {
return errors.New("course zrpc client is not initialized")
}
return nil
}
func (c *Client) ping(timeout time.Duration) error {
if err := c.ensureReady(); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
_, err := c.rpc.Ping(ctx, &coursepb.StatusResponse{})
return responseFromRPCError(err)
}
func (c *Client) callJSON(ctx context.Context, fn func(context.Context, *coursepb.JSONRequest, ...grpc.CallOption) (*coursepb.JSONResponse, error), payload any) (*coursepb.JSONResponse, error) {
if err := c.ensureReady(); err != nil {
return nil, err
}
raw, err := json.Marshal(payload)
if err != nil {
return nil, err
}
return fn(ctx, &coursepb.JSONRequest{PayloadJson: raw})
}
func jsonFromResponse(resp *coursepb.JSONResponse, rpcErr error) (json.RawMessage, error) {
if rpcErr != nil {
return nil, responseFromRPCError(rpcErr)
}
if resp == nil {
return nil, errors.New("course zrpc service returned empty JSON response")
}
if len(resp.DataJson) == 0 {
return json.RawMessage("null"), nil
}
return json.RawMessage(resp.DataJson), nil
}
func normalizeEndpoints(values []string) []string {
endpoints := make([]string, 0, len(values))
for _, value := range values {
trimmed := strings.TrimSpace(value)
if trimmed != "" {
endpoints = append(endpoints, trimmed)
}
}
return endpoints
}
func normalizeMaxRPCMessageSize(maxImageBytes int64) int {
if maxImageBytes <= 0 {
return defaultMaxRPCMessageSize
}
size := maxImageBytes + rpcMessageSizePadding
if size < defaultMaxRPCMessageSize {
return defaultMaxRPCMessageSize
}
return int(size)
}