Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9f3ac5c
feat: sync custom course
Eksin-4eva Jul 31, 2026
fc4e989
fix: 代码格式优化
Eksin-4eva Jul 31, 2026
785a9c6
feat: sync custom course
Eksin-4eva Jul 31, 2026
d64c5a2
feat: sync custom course
Eksin-4eva Jul 31, 2026
c601973
fix: 代码格式优化
Eksin-4eva Jul 31, 2026
24a0977
chore: 新增测试用例
Eksin-4eva Jul 31, 2026
3da9e1d
chore: run go mod tidy to sync go.mod and go.sum
Eksin-4eva Jul 31, 2026
4bff562
Merge branch 'main' into syncCustom
Penty-d Aug 2, 2026
876bd37
fix: ci
Penty-d Aug 2, 2026
117ca96
feat(api): 完善网关层
Eksin-4eva Aug 15, 2026
5aa65ec
fix: ci
Eksin-4eva Aug 15, 2026
890fb4a
feat: sql
Eksin-4eva Aug 17, 2026
9fcac2c
fix(course): 自定义课程用提取后的纯学号作为 DB key,与登录链路对齐
Eksin-4eva Aug 17, 2026
7eebee1
Update api/pack/course.go
Eksin-4eva Aug 17, 2026
47ea291
Update api/pack/course.go
Eksin-4eva Aug 17, 2026
5c06e95
Merge branch 'main' into syncCustom
Eksin-4eva Aug 18, 2026
408d585
feat: apiv2
Eksin-4eva Aug 18, 2026
69cdd0b
fix: ci
Eksin-4eva Aug 18, 2026
657860c
fix: remove gorm
Eksin-4eva Aug 18, 2026
62525e4
fix: upsert
Eksin-4eva Aug 18, 2026
b8bfaef
fix(upsert): 走PUT时带上全字段
Eksin-4eva Aug 18, 2026
cea347e
fix(upsert): 注释
Eksin-4eva Aug 18, 2026
850339a
fix: remove custom course biz code
Eksin-4eva Aug 19, 2026
d9b08de
fix: remove custom course biz code
Eksin-4eva Aug 19, 2026
205fc29
fix: ci
Eksin-4eva Aug 19, 2026
4870ab5
fix(course): 增加单双周判断,返回已有 courseId,handler 统一旧风格
Eksin-4eva Aug 19, 2026
a132285
fix: unit test
Eksin-4eva Aug 20, 2026
e340138
refactor: custom course
Eksin-4eva Aug 21, 2026
76471b5
fix: test cases
Eksin-4eva Aug 21, 2026
a30b3f9
feat: test cases
Eksin-4eva Aug 23, 2026
98ee8a4
fix(mcp): 修改课程列表取值方式,courseList 改为 courseList.Data
Eksin-4eva Aug 23, 2026
565041f
fix(custom course): test cases
Eksin-4eva Aug 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 81 additions & 1 deletion api/handler/api/course_service.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

216 changes: 213 additions & 3 deletions api/handler/api/course_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func TestGetCourseList(t *testing.T) {
type testCase struct {
name string
url string
mockResp []*model.Course
mockResp *course.CourseListResponse
mockErr error
expectContains string
}
Expand All @@ -64,7 +64,7 @@ func TestGetCourseList(t *testing.T) {
{
name: "success",
url: "/api/v1/jwch/course/list?term=202401",
mockResp: []*model.Course{},
mockResp: &course.CourseListResponse{Data: []*model.Course{}},
expectContains: `{"code":"10000","message":"ok","data":[]}`,
},
{
Expand All @@ -86,7 +86,7 @@ func TestGetCourseList(t *testing.T) {
defer mockey.UnPatchAll()
for _, tc := range testCases {
mockey.PatchConvey(tc.name, t, func() {
mockey.Mock(rpc.GetCourseListRPC).To(func(ctx context.Context, req *course.CourseListRequest) ([]*model.Course, error) {
mockey.Mock(rpc.GetCourseListRPC).To(func(ctx context.Context, req *course.CourseListRequest) (*course.CourseListResponse, error) {
return tc.mockResp, tc.mockErr
}).Build()

Expand All @@ -97,6 +97,216 @@ func TestGetCourseList(t *testing.T) {
}
}

func TestGetCourseListV2(t *testing.T) {
type testCase struct {
name string
url string
mockResp *course.CourseListResponse
mockErr error
expectContains string
expectAbsence string
}

testCases := []testCase{
{
name: "success",
url: "/api/v2/course/list?term=202401",
mockResp: &course.CourseListResponse{Data: []*model.Course{}},
expectContains: `{"code":"10000","message":"ok","data":{"base":null,"data":[],"custom_courses":[]}}`,
},
{
name: "success",
url: "/api/v2/course/list?term=202401",
mockResp: &course.CourseListResponse{
Data: []*model.Course{},
CustomCourses: []*course.CustomCourseItem{
{Name: "x", Location: "y", StartClass: 1, EndClass: 2, StartWeek: 1, EndWeek: 2, Weekday: 1},
},
},
expectContains: `"custom_courses":[{"name":"x"`,
},
{
name: "rpc error",
url: "/api/v2/course/list?term=202401",
mockErr: errno.InternalServiceError,
expectContains: `{"code":"50001","message":"内部服务错误"}`,
},
{
name: "bind error",
url: "/api/v2/course/list",
expectContains: `{"code":"20001","message":"参数错误,`,
},
}

router := route.NewEngine(&config.Options{})
router.GET("/api/v2/course/list", GetCourseListV2)

defer mockey.UnPatchAll()
for _, tc := range testCases {
mockey.PatchConvey(tc.name, t, func() {
mockey.Mock(rpc.GetCourseListRPC).To(func(ctx context.Context, req *course.CourseListRequest) (*course.CourseListResponse, error) {
return tc.mockResp, tc.mockErr
}).Build()

res := ut.PerformRequest(router, consts.MethodGet, tc.url, nil)
assert.Equal(t, consts.StatusOK, res.Result().StatusCode())
body := string(res.Result().Body())
assert.Contains(t, body, tc.expectContains)
if tc.expectAbsence != "" {
assert.NotContains(t, body, tc.expectAbsence)
}
})
}
}

func TestDeleteCustomCourse(t *testing.T) {
type testCase struct {
name string
url string
body string
mockErr error
expectContains string
}

testCases := []testCase{
{
name: "success",
url: "/api/v1/course/custom",
body: `{"term":"202401","course_id":"114514"}`,
expectContains: `{"code":"10000","message":"ok"}`,
},
{
name: "rpc error",
url: "/api/v1/course/custom",
body: `{"term":"202401","course_id":"114514"}`,
mockErr: errno.InternalServiceError,
expectContains: `{"code":"50001","message":"内部服务错误"}`,
},
{
name: "bind error",
url: "/api/v1/course/custom",
body: `{"course_id":"114514"}`,
expectContains: `{"code":"20001","message":"参数错误,`,
},
}

router := route.NewEngine(&config.Options{})
router.DELETE("/api/v1/course/custom", DeleteCustomCourse)

defer mockey.UnPatchAll()
for _, tc := range testCases {
mockey.PatchConvey(tc.name, t, func() {
mockey.Mock(rpc.DeleteCustomCourseRPC).To(func(ctx context.Context, req *course.DeleteCustomCourseRequest) error {
return tc.mockErr
}).Build()

body := &ut.Body{
Body: bytes.NewBufferString(tc.body),
Len: len(tc.body),
}
res := ut.PerformRequest(router, consts.MethodDelete, tc.url, body, ut.Header{
Key: "Content-Type",
Value: "application/json",
})
assert.Equal(t, consts.StatusOK, res.Result().StatusCode())
assert.Contains(t, string(res.Result().Body()), tc.expectContains)
})
}
}

func TestUpsertCustomCourse(t *testing.T) {
type testCase struct {
name string
url string
body string
mockResp *course.UpsertCustomCourseResponse
mockErr error
expectContains string
}

testCases := []testCase{
{
name: "success",
url: "/api/v1/course/custom",
body: `{"term":"202401","course":` +
`{"name":"x","location":"y","start_class":1,"end_class":2,` +
`"start_week":1,"end_week":2,"weekday":1,"single":false,"double_":false}}`,
mockResp: &course.UpsertCustomCourseResponse{},
expectContains: `{"code":"10000","message":"ok","data":`,
},
{
name: "rpc error",
url: "/api/v1/course/custom",
body: `{"term":"202401","course":` +
`{"name":"x","location":"y","start_class":1,"end_class":2,` +
`"start_week":1,"end_week":2,"weekday":1,"single":false,"double_":false}}`,
mockErr: errno.InternalServiceError,
expectContains: `{"code":"50001","message":"内部服务错误"}`,
},
{
name: "missing course rejected at bind",
url: "/api/v1/course/custom",
body: `{"term":"202401"}`,
expectContains: `{"code":"20001","message":"参数错误,`,
},
{
name: "bind error",
url: "/api/v1/course/custom",
body: `{"term":"202401","course":{"name":"x"}}`,
expectContains: `{"code":"20001","message":"参数错误,`,
},
}

router := route.NewEngine(&config.Options{})
router.POST("/api/v1/course/custom", UpsertCustomCourse)

defer mockey.UnPatchAll()
for _, tc := range testCases {
mockey.PatchConvey(tc.name, t, func() {
mockey.Mock(rpc.UpsertCustomCourseRPC).To(func(ctx context.Context, req *course.UpsertCustomCourseRequest) (*course.UpsertCustomCourseResponse, error) {
return tc.mockResp, tc.mockErr
}).Build()

body := &ut.Body{
Body: bytes.NewBufferString(tc.body),
Len: len(tc.body),
}
res := ut.PerformRequest(router, consts.MethodPost, tc.url, body, ut.Header{
Key: "Content-Type",
Value: "application/json",
})
assert.Equal(t, consts.StatusOK, res.Result().StatusCode())
assert.Contains(t, string(res.Result().Body()), tc.expectContains)
})
}
}

func TestUpsertCustomCourseCourseNil(t *testing.T) {
router := route.NewEngine(&config.Options{})
router.POST("/api/v1/course/custom", UpsertCustomCourse)

defer mockey.UnPatchAll()
mockey.PatchConvey("course nil reaches nil check", t, func() {
// 绕过 hertz 的 required 校验,使 req.Course 保持 nil,覆盖 handler 中的防御性检查
mockey.Mock((*app.RequestContext).BindAndValidate).To(
func(c *app.RequestContext, req interface{}) error {
return nil
},
).Build()

body := &ut.Body{
Body: bytes.NewBufferString(`{"term":"202401"}`),
Len: len(`{"term":"202401"}`),
}
res := ut.PerformRequest(router, consts.MethodPost, "/api/v1/course/custom", body, ut.Header{
Key: "Content-Type",
Value: "application/json",
})
assert.Equal(t, consts.StatusOK, res.Result().StatusCode())
assert.Contains(t, string(res.Result().Body()), `{"code":"20001","message":"参数错误"}`)
})
}

func TestGetTermList(t *testing.T) {
type testCase struct {
name string
Expand Down
2 changes: 1 addition & 1 deletion api/mcp/course.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func handleGetCourse(ctx context.Context, request mcp.CallToolRequest) (*mcp.Cal
// 包装成JSON,JSON数组直接返回时不合法的
resp := map[string]any{
"term": term,
"courses": courseList,
"courses": courseList.Data,
}

return mcp.NewToolResultJSON(resp)
Expand Down
Loading