Skip to content

Commit 8f2ddd9

Browse files
committed
feat: 添加RenewService,重构文件结构,重构Course
1 parent f3eeb91 commit 8f2ddd9

19 files changed

+255
-96
lines changed

eslint.config.js

+17-8
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,24 @@ import pluginReactHooks from 'eslint-plugin-react-hooks'
66

77
/** @type {import('eslint').Linter.Config[]} */
88
export default [
9-
pluginJs.configs.recommended,
9+
{ name: 'globalIgnore', ignores: ['dist/', 'android/', 'eslint.config.js'] },
10+
{ name: 'pluginJs.configs.recommended', ...pluginJs.configs.recommended },
1011
...tseslint.configs.recommendedTypeChecked,
11-
pluginReact.configs.flat.recommended,
12-
pluginReact.configs.flat['jsx-runtime'],
13-
{ plugins: { 'react-hooks': pluginReactHooks } },
14-
{ rules: pluginReactHooks.configs.recommended.rules },
1512
{
13+
name: 'pluginReact.configs.flat.recommended',
14+
...pluginReact.configs.flat.recommended,
15+
},
16+
{
17+
name: "pluginReact.configs.flat['jsx-runtime']",
18+
...pluginReact.configs.flat['jsx-runtime'],
19+
},
20+
{
21+
name: 'pluginReactHooks',
22+
plugins: { 'react-hooks': pluginReactHooks },
23+
rules: pluginReactHooks.configs.recommended.rules,
24+
},
25+
{
26+
name: 'customConfig',
1627
settings: { react: { version: 'detect' } },
1728
languageOptions: {
1829
globals: globals.browser,
@@ -21,8 +32,6 @@ export default [
2132
tsconfigRootDir: import.meta.dirname,
2233
},
2334
},
24-
},
25-
{
26-
ignores: ['dist/', 'android/', 'eslint.config.js'],
35+
rules: { eqeqeq: ['error', 'always'] },
2736
},
2837
]

src/extension/ExtensionWidgetCapability.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { WidgetExtension } from './Extension'
77
import { z } from 'zod'
88
import { afterDone, PromiseAwaited } from '../utils/func'
99
import { BorrowedHandle, BorrowManager } from './BorrowManager'
10-
import { ZjuamService } from '../interop/zjuam'
10+
import { ZjuamService } from '../services/ZjuamService'
1111
import { encodeReturn } from './ExtensionIO'
1212

1313
type ResolveHandle<H> = H extends BorrowedHandle<infer O> ? O : H

src/interop/credential.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { env } from './env'
2-
import { ZjuamService } from './zjuam'
2+
import { ZjuamService } from '../services/ZjuamService'
33

44
export type Credential = {
55
username: string

src/models/Course.ts

-44
This file was deleted.

src/models/CourseBase.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Semester } from './shared'
2+
3+
/**课程最基础的识别信息。选课课号为唯一标识符。
4+
*
5+
* 某些上游不返回学期字段,此时semester字段通过选课课号解析,可能不准确(如实际秋学期的课,该字段为秋冬)。
6+
*/
7+
export interface CourseBase {
8+
/**学年&学期 */
9+
semester: Semester
10+
/**选课号 如(2024-2025-1)-761T0060-0017687-4 */
11+
id: string
12+
/**课程名称(中文) */
13+
name: string
14+
}

src/models/CourseClassInfo.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { WeekType, DayOfWeek } from './shared'
2+
3+
/**课程教学信息,包括教师和上课时间地点。 */
4+
export interface CourseClassInfo {
5+
/**教师姓名 */
6+
teacherName: string
7+
/**上课时间地点 */
8+
classes: ClassArrangement[]
9+
}
10+
/**相对于学期的上课时间、地点 */
11+
12+
export interface ClassArrangement {
13+
/**上课周次 */
14+
weekType: WeekType
15+
dayOfWeek: DayOfWeek
16+
/**从第几节开始 =djj */
17+
startSection: number
18+
/**持续节数 =kccd */
19+
sectionCount: number
20+
/**地点 =skdd */
21+
location: string
22+
}

src/models/CourseCombined.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { CourseBase } from '@/models/CourseBase'
2+
import { CourseTodoInfo } from './CourseTodoInfo'
3+
import { CourseGradeInfo } from './CourseGradeInfo'
4+
import { CourseExamInfo } from './CourseExamInfo'
5+
import { CourseClassInfo } from './CourseClassInfo'
6+
import { Maybe } from '@/utils/type'
7+
8+
export type DataOrigin = 'class' | 'exam' | 'grade' | 'xzzdTodo'
9+
/**从课程表、考试、成绩、学在浙大多方来源合并的课程信息。
10+
*
11+
* 注意:如果没有来自'class'的信息(如实践课),则学期可能不准确(单个学期的课可能被解析为长学期)
12+
*/
13+
export type CourseCombined = CourseBase &
14+
Maybe<CourseClassInfo> &
15+
Maybe<CourseExamInfo> &
16+
Maybe<CourseGradeInfo> &
17+
Maybe<CourseTodoInfo> & { origin: DataOrigin[] }

src/models/CourseExamInfo.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**课程考试信息。 */
2+
export interface CourseExamInfo {
3+
/**考试 */
4+
exams: ExamArrangement[]
5+
}
6+
7+
export type ExamType = 'midterm' | 'final'
8+
/**单场考试安排 */
9+
export interface ExamArrangement {
10+
type: ExamType
11+
startAt: Date
12+
endAt: Date
13+
/**考试地点,可空 */
14+
location?: string
15+
/**座位号,可空,原样保留上游结果,不需要转Number再转字符串 */
16+
seat?: string
17+
}

src/models/Grade.ts renamed to src/models/CourseGradeInfo.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import type { Course } from './Course'
2-
3-
export interface Grade {
4-
course: Pick<Course, 'semester' | 'id' | 'name' | 'credit'>
1+
export interface CourseGradeInfo {
2+
/**学分 */
3+
credit: number
54
/**原始成绩。包括“缺考”、“缓考”等 */
65
rawScore: string
76
/**原始绩点(原样保留上游数据) */

src/models/CourseTodoInfo.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { XzzdTodoType } from '@/spiders/XzzdSpider'
2+
3+
export interface CourseTodoInfo {
4+
todos: CourseTodo[]
5+
}
6+
7+
export interface CourseTodo {
8+
endAt: Date
9+
/**待办名称 */
10+
title: string
11+
type: XzzdTodoType
12+
}

src/services/RenewService.ts

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { CourseBase } from '@/models/CourseBase'
2+
import { CourseTodoInfo } from '@/models/CourseTodoInfo'
3+
import { Semester, Term } from '@/models/shared'
4+
import { CourseSpider } from '@/spiders/CourseSpider'
5+
import { ExamSpider } from '@/spiders/ExamSpider'
6+
import { GradeSpider } from '@/spiders/GradeSpider'
7+
import { XzzdSpider } from '@/spiders/XzzdSpider'
8+
import { toSemester } from '@/utils/stringUtils'
9+
import { CourseCombined, DataOrigin } from '../models/CourseCombined'
10+
11+
/**批量获取并解析所有上游数据的服务。 */
12+
export class RenewService {
13+
courseSpider = new CourseSpider()
14+
examSpider = new ExamSpider()
15+
gradeSpider = new GradeSpider()
16+
xxzdSpider = new XzzdSpider()
17+
18+
/**立即重新获取所有数据并解析。若任何步骤发生致命错误将reject。 */
19+
async renewAll(): Promise<CourseCombined[]> {
20+
/** 对term做按位交,合并两个Semester */
21+
function combineSemester(sem1: Semester, sem2: Semester): Semester {
22+
const { year: year1, term: term1 } = sem1,
23+
{ year: year2, term: term2 } = sem2
24+
if (year1 !== year2) throw new Error('尝试跨学年合并学期')
25+
if (term1 === Term.Short || term2 === Term.Short)
26+
//如果含短学期,总是返回短学期
27+
return { year: year1, term: Term.Short }
28+
const term = term1 & term2
29+
if (term === 0) throw new Error('欲合并的学期无交集')
30+
return { year: year1, term }
31+
}
32+
/** 返回一个函数。该函数将`append`与接收到的首个参数进行课程信息合并。直接修改接收到的参数,不返回值。 */
33+
function combineF(
34+
append: Omit<CourseCombined, 'origin'>,
35+
origin: DataOrigin,
36+
): (base: CourseCombined) => void {
37+
return (base) => {
38+
//合并课程信息。除了semester取区间较短者(秋 总覆盖 秋冬),其它全部后出现者覆盖
39+
const { semester: appendSemester } = append,
40+
{ semester: baseSemester, origin: baseOrigin } = base
41+
if (!baseOrigin.includes(origin)) baseOrigin.push(origin)
42+
Object.assign(base, {
43+
...append,
44+
semester: combineSemester(baseSemester, appendSemester),
45+
})
46+
}
47+
}
48+
const [courses, exams, grades, todos] = await Promise.all([
49+
this.courseSpider.getAllCourses(),
50+
this.examSpider.getAllExams(),
51+
this.gradeSpider.getAllGrades(),
52+
this.xxzdSpider.getTodos(),
53+
])
54+
const r: Map<CourseBase['id'], CourseCombined> = new Map()
55+
courses.forEach((item) =>
56+
r.ensure(
57+
item.id,
58+
() => ({ ...item, origin: ['class'] }),
59+
combineF(item, 'class'),
60+
),
61+
)
62+
exams.forEach((item) =>
63+
r.ensure(
64+
item.id,
65+
() => ({ ...item, origin: ['exam'] }),
66+
combineF(item, 'exam'),
67+
),
68+
)
69+
grades.forEach((item) =>
70+
r.ensure(
71+
item.id,
72+
() => ({ ...item, origin: ['grade'] }),
73+
combineF(item, 'grade'),
74+
),
75+
)
76+
todos.forEach((item) =>
77+
r.ensure(
78+
item.courseCode,
79+
() => {
80+
return {
81+
semester: toSemester(item.courseCode),
82+
id: item.courseCode,
83+
name: item.courseName,
84+
todos: [item],
85+
origin: ['xzzdTodo'],
86+
}
87+
},
88+
(prevItem: CourseCombined & Partial<CourseTodoInfo>) => {
89+
if (!prevItem.todos) {
90+
prevItem.todos = []
91+
prevItem.origin.push('xzzdTodo')
92+
}
93+
prevItem.todos.push(item)
94+
},
95+
),
96+
)
97+
const coll = new Intl.Collator('zh-Hans-CN', {
98+
usage: 'sort',
99+
sensitivity: 'variant',
100+
})
101+
return [...r.values()].sort((a, b) => coll.compare(a.id, b.id))
102+
}
103+
}

src/interop/zjuam.ts renamed to src/services/ZjuamService.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { modPow } from 'bigint-mod-arith'
2-
import { getRawUrl, nxFetch } from './fetch'
3-
import { requestCredential } from './credential'
2+
import { getRawUrl, nxFetch } from '../interop/fetch'
3+
import { requestCredential } from '../interop/credential'
44
import { z } from 'zod'
55
import { init } from '@/utils/func'
66

src/spiders/CourseSpider.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { DayOfWeek, Term, WeekType } from '../models/shared'
2-
import { Course, ClassArrangement } from '../models/Course'
2+
import { CourseBase } from '../models/CourseBase'
3+
import { ClassArrangement, CourseClassInfo } from '@/models/CourseClassInfo'
34
import { parseCourseSelectionId } from '@/utils/stringUtils'
45
import { sharedZjuamService } from './sharedZjuamService'
56

6-
/**课程表中的课程信息,无学分、考试 */
7-
type CourseInSchedule = Omit<Course, 'credit' | 'exams'>
7+
/**课程表中的课程信息 */
8+
type CourseWithClassInfo = CourseBase & CourseClassInfo
89
type RawCourseResp = {
910
/**显示在课表的课,与实践课相对 */
1011
kbList: {
@@ -29,8 +30,8 @@ export class CourseSpider {
2930
private readonly zjuamService = sharedZjuamService
3031

3132
/**一次性获取全部课程信息。未查短学期的课,未查实践课。 */
32-
public async getAllCourses(): Promise<CourseInSchedule[]> {
33-
const cMap = new Map<string, CourseInSchedule[]>()
33+
public async getAllCourses(): Promise<CourseWithClassInfo[]> {
34+
const cMap = new Map<string, CourseWithClassInfo[]>()
3435
const response = await this.zjuamService.nxFetch.postUrlEncoded(
3536
'http://zdbk.zju.edu.cn/jwglxt/kbcx/xskbcx_cxXsKb.html?gnmkdm=N253508',
3637
{
@@ -131,7 +132,7 @@ export class CourseSpider {
131132
sectionCount: secCount,
132133
})
133134
}
134-
while (secFlags != 0) {
135+
while (secFlags !== 0) {
135136
const curSecValid = Boolean(secFlags & 0b1)
136137
if (curSecValid) {
137138
//当前sec有课
@@ -145,7 +146,7 @@ export class CourseSpider {
145146
secFlags >>= 1
146147
curSec++
147148
}
148-
if (startSec != 0) finishCurSection()
149+
if (startSec !== 0) finishCurSection()
149150
return sections
150151
}),
151152
),

src/spiders/ExamSpider.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import { Course } from '@/models/Course'
2-
import { parseZdbkDate } from '@/utils/stringUtils'
1+
import { parseZdbkDate, toSemester } from '@/utils/stringUtils'
32
import { Maybe } from '@/utils/type'
43
import { sharedZjuamService } from './sharedZjuamService'
4+
import { CourseBase } from '@/models/CourseBase'
5+
import { CourseExamInfo, ExamArrangement } from '@/models/CourseExamInfo'
56

7+
type CourseWithExamInfo = CourseBase & CourseExamInfo
68
export class ExamSpider {
79
private readonly zjuamService = sharedZjuamService
810

911
/**一次性获取全部考试信息。 */
10-
async getAllExams(): Promise<Pick<Course, 'id' | 'name' | 'exams'>[]> {
12+
async getAllExams(): Promise<CourseWithExamInfo[]> {
1113
const { items } = (await (
1214
await this.zjuamService.nxFetch.postUrlEncoded(
1315
'http://zdbk.zju.edu.cn/jwglxt/xskscx/kscx_cxXsgrksIndex.html?doType=query&gnmkdm=N509070',
@@ -46,11 +48,13 @@ export class ExamSpider {
4648
}
4749

4850
return items.map((item) => {
51+
const selectionId = item.xkkh
4952
const course = {
50-
id: item.xkkh,
53+
semester: toSemester(selectionId),
54+
id: selectionId,
5155
name: item.kcmc,
52-
exams: [],
53-
} as Pick<Course, 'id' | 'name' | 'exams'>
56+
exams: [] as ExamArrangement[],
57+
}
5458
if ('qzkssj' in item)
5559
course.exams.push({
5660
type: 'midterm',

0 commit comments

Comments
 (0)