Skip to content

feat(timetable): 课表底座——pku_account 北大账号绑定 + timetable 课表/导入/ICS + 小程序 API - #1012

Draft
tangmc0210 wants to merge 15 commits into
Yuanpei-Intelligence:developfrom
tangmc0210:feat/timetable
Draft

feat(timetable): 课表底座——pku_account 北大账号绑定 + timetable 课表/导入/ICS + 小程序 API#1012
tangmc0210 wants to merge 15 commits into
Yuanpei-Intelligence:developfrom
tangmc0210:feat/timetable

Conversation

@tangmc0210

@tangmc0210 tangmc0210 commented Sep 9, 2026

Copy link
Copy Markdown

概要

面向小程序的课表功能后端(需求池「课表」及其下游「晒课表」「课表活动」的底座),按可插拔方式拆成三个新 app,契约见 timetable/README.md

  • pku_account — 北大账号绑定:IAAA/门户客户端(requests,无业务依赖)、PkuAccount(YPPF 用户 ↔ 北大账号,一对一、含课表/成绩授权项)、PkuPortalSession(Fernet 加密的门户 cookie,不存密码)、失败计数与锁定、授权变更信号、/api/v2/pku/{binding,login,unbind,consents}/
  • timetableAcademicTerm / TimetableEntry / ImportLog / TimetableSettings;纯标准库解析器(门户 getCourseInfo.do JSON、门户「我的课表」HTML、选课系统「选课结果」表格与纯文本);通过 config.json → timetable.sources 注册的事件源(已存条目、书院课 Course/CourseTime/Activity、已报名活动、地下室预约、考试安排);周视图 + 冲突检测;门户/粘贴导入(按 (person, term, source) upsert,不动其它来源与历史学期);RFC 5545 ICS 订阅 GET /timetable/ics/<token>.ics上课提醒(微信订阅消息 + 站内通知/企微推送回退,5 分钟 job);全校课程库import_course_catalog 命令吃 PKU-Course-Crawler 的 xlsx,检索 API 供手动添加联想);/api/v2/timetable/ 路由。
  • academic_record — 成绩:门户成绩解析、GradeRecord 仅在学生勾选成绩授权后落库,撤销授权或解绑即删除;/api/v2/grades/ 三个端点。
  • 首页日程与四源勾选GET /api/v2/timetable/agenda/?from=&days=(跨学期连续日期、带校历标签,供小程序首页「我的日程」与单日视图);TimetableSettings.show_courses 使学校课表/书院课/活动/预约四个来源在周视图、日程、ICS 导出、上课提醒中统一可勾选。
  • 错误信封对齐 Unify exception handling in miniprogram API #1014:三个新模块的校验错误已输出 errors: {字段: [{code, message}]}(与 api/exceptions.py 及小程序 Develop #10 的解析器一致);Unify exception handling in miniprogram API #1014 合并后改用 StandardizedExceptionHandlerMixin 即可。
  • 校历(semester.CalendarEvent + semester.calendar — 全局、按日期的放假 / 考试周 / 调休 / 标注事件表,admin 可随时改、import_academic_calendar 可按学期 JSON 重复导入覆盖(附 2026–2027 两学期从官方 PDF 转录的种子文件);semester.calendar 是全平台读接口(is_class_day / effective_weekday),课表周视图、ICS、提醒在读取时实时查询,放假日不出课、调休日按被换星期出课;活动等其它模块以后也从这里读,不再各自写死。

依赖方向单向:pku_accounttimetable ← 适配器(懒导入 app/Appointment),academic_recordpku_accountapp/Appointment/ 不引用新 app。取消一个数据源、提醒通道或整个门户登录只需改 config.json

#999 的关系:本 PR 取代其「教务课表」部分(并修正了节次时间表:北大为 50 分钟制),已在 #999 留言请作者收窄为日程表页与手动日程,改为读本 app 的数据。

第三轮(2026-09-10,README §8)

  • 课程库关联与旁听:导入时按 课程号+班号 → 课程号 → 课程名(+教师) 把条目关联到 CourseCatalogEntry(只补空字段,学生自己的值优先,重导入不改学生的标注);POST catalog/<id>/add/ 一键把课程库课程加为旁听(每个时段一条,role=audit);条目新增 role / category / tag,备注改长文本;GET catalog/ 返回院系 / 类别 / 学分 / 周次与 added
  • 单次 / 以后 / 全部编辑TimetableEntryOverride(JSON 覆盖字段 + canceled + 周范围,窄范围优先);PATCH entries/<id>/scope / week,门户 / 粘贴条目的修改存为整段覆盖、重导入不丢;GET entries/<id>/ 详情(课程库信息、覆盖记录、考试),DELETE entries/<id>/overrides/[<oid>/] 恢复;周视图 / 日程 / ICS / 提醒统一消费覆盖后的日程(Occurrence.role/tag/modified)。
  • 标签与来源清单settings/ 返回已注册来源(含控制它的 setting 键)与我用过的标签,接受 hidden_tags / show_exams——小程序的筛选弹层不再写死四个来源。
  • 考试周与考试安排AcademicTerm.exam_week_start + teaching_weekstotal_weeks 改为含考试周(秋 19 / 春 18,校历种子已更新);CourseExam + import_exam_schedule(教务部 xlsx/csv,识别多种时间写法)+ exam 源自动按课程号 / 课程名匹配到学生课表;条目详情带 exam。门户是否有个人考试接口未知(需真实账号探测),暂无按学生导入。
  • 海报素材GET share/assets/wxacode.getUnlimited 小程序码 30 天文件缓存 + 公众号二维码 + 口号)。公众号二维码随仓库提供(static/assets/img/yppf_official_qrcode.png,模板默认值;配置值以 / 开头视为站内路径拼 base_url,裸文件名走 MEDIA_URL)。
  • 部署自检python manage.py timetable_check 按 README §9 清单逐项输出 [OK]/[WARN]/[FAIL](迁移、配置、来源、订阅模板、海报素材、定时任务、学期 / 校历 / 课程库 / 考试表),有 FAIL 时非零退出(--warn-only 可只报告)。
  • 顺带:SPECTACULAR_SETTINGS['ENUM_NAME_OVERRIDES'] 固定新增枚举的组件名(否则与活动 category 冲突产生告警)。

部署注意

  • config.json 新增 pku_portaltimetablewx_miniapp.subscribe_templateswx_miniapp.share 段(见 config_template.json)。pku_portal.enabled 模板默认 false,生产需显式设为 truesession_key 留空则由 SECRET_KEY 派生;timetable.sources 需加 timetable.sources.exam.ExamSource
  • 迁移:pku_account.0001timetable.0001–0004(0004 含手动条目类别的数据步骤)、academic_record.0001
  • 建学期与校历:python manage.py import_academic_calendar timetable/data/calendar_26-27-1.json(同时建/更新 AcademicTerm 26-27-1total_weeks/exam_week_start 与校历事件;春季用 calendar_26-27-2.json;元旦 2027 待国办公布后在 admin 加一行)。考试安排:python manage.py import_exam_schedule <教务部表格> --term 26-27-1
  • 提醒 job:collect_jobs 会自动发现 timetable/jobs.pyrunscheduler 照常;订阅消息需在小程序后台申请模板后把 id 填进 wx_miniapp.subscribe_templates.class_reminder.id(留空则只走站内通知 + 企微)。
  • 海报二维码:MEDIA_ROOT 需对外可访问,小程序后台把后端域名加进 downloadFile 合法域名;公众号二维码已随仓库提供,wx_miniapp.share.official_qrcode_url 保持模板默认值即可。
  • 部署完成后跑 python manage.py timetable_check 看清单(README §9)。
  • 门户校外登录自 2026-03-30 起需短信二次验证;服务器在校内预期不触发,上线前请用真实账号验证一次 POST /api/v2/pku/login/
  • 政策面:IAAA 账号不应交给第三方——建议知会学院 / 计算中心,导入页已有明确授权文案。

验证

  • 容器内 python manage.py test 全量 934 OK(三轮共新增约 580 个用例:pku_accounttimetable(含 overrides / exams / share / catalog 匹配)、academic_record 及各自 API),manage.py check / makemigrations --check 干净,spectacular 无新增告警。
  • 多轮独立代码复审(凭证泄露路径、IDOR、迁移一致性、周次展开、ICS 合规、提醒配额并发、成绩授权信号),复审问题均已修。
  • DevTools 冒烟(样例库 + S000063,后端跑在 dev container):真实微信登录 → 绑定 → 课表;粘贴导入 → 周视图 → 详情 → 海报;成绩授权门;课程库「添加为旁听」→ 201 → 课表出现「旁」格子;「仅本次」编辑 → PATCH scope=single → 格子显示新地点;「本次停课」→ 格子消失;「恢复默认」→ DELETE overrides 204;首页筛选关掉课程 → 日程清空、角标 4/5;第 19 周显示匹配到的考试红块;四套海报渲染。对 pku.edu.cn / weixin 的请求在测试中全部 mock。

小程序前端见配套 PR Yuanpei-Intelligence/YPPF-mini#9

🤖 Generated with Claude Code

https://claude.ai/code/session_012t4BD5H1P5nn2zF7oU6q9e

tangmc0210 and others added 14 commits September 10, 2026 11:16
…contract

Add empty pku_account and timetable apps, register them in INSTALLED_APPS, mount /api/v2/pku/, /api/v2/timetable/ and the /timetable/ ICS route, add pku_portal and timetable config sections, and document the models, source registry and API contract in timetable/README.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012t4BD5H1P5nn2zF7oU6q9e
…session vault

Add the pku_account app: requests-based IAAA login and portal client (course info, scores, liveness probe), PkuAccount/PkuPortalSession models, Fernet-encrypted cookie storage, brute-force lock, consent flags, and the /api/v2/pku/ binding, login, unbind and consents endpoints with tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012t4BD5H1P5nn2zF7oU6q9e
…S feed and mini-program API

Add the timetable app: AcademicTerm/TimetableEntry/ImportLog/TimetableSettings, stdlib parsers for the portal course JSON/HTML and elective 选课结果 (HTML and plain text), config-registered event sources (stored entries, 书院课 via Course/CourseTime/Activity, applied activities, basement appointments), week view with conflict detection, portal/text import with upsert-by-source semantics, an RFC 5545 ICS feed, the timetable_seed_terms command, and the /api/v2/timetable/ endpoints with tests. Update the README contract with the agreed refinements.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012t4BD5H1P5nn2zF7oU6q9e
Serialize imports per person with a NaturalPerson row lock so a first import cannot race on the entry unique key; show canceled/aborted 书院课 activities as status 'canceled' instead of dropping them and ignore them in conflict detection; require a valid person account in the timetable API like the pku_account API; default pku_portal.enabled to false in config_template.json (enable per deployment).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012t4BD5H1P5nn2zF7oU6q9e
Add the academic_record app: portal score parser (nested and flattened shapes, non-numeric scores), GradeRecord model stored only with the student's grades consent, GPA summaries, the grades/sync/delete endpoints with the {code, message} error envelope, a pku_account consent_changed signal emitted after commit, and receivers that delete stored grades when consent is revoked or the binding is removed. Document the as-built behaviour in the timetable README.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012t4BD5H1P5nn2zF7oU6q9e
…talog import/search

Add SubscribeQuota/ReminderLog/CourseCatalogEntry, the subscribe-message sender (extern/wx_miniapp.py), the 5-minute reminder job with notification fallback, quota grant/templates endpoints, catalog parsing, the import_course_catalog command and catalog search API with tests; document the new config keys in config_template.json and the as-built notes in the README; list the new API modules in api/AGENTS.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012t4BD5H1P5nn2zF7oU6q9e
…art, keep appsecret out of logs

Look back three job intervals (ReminderLog keeps re-scans idempotent) so a short scheduler outage does not drop reminders, skip occurrences whose class already started, and log only the exception class when the WeChat access-token request fails because requests embeds the URL (with the appsecret) in the message. Document the academic-calendar contract (README §6.4).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012t4BD5H1P5nn2zF7oU6q9e
… timetable expansion

Add semester.CalendarEvent (global, keyed by date; editable in admin) and semester.calendar, the platform-wide read API (is_class_day, effective_weekday, AcademicCalendar) that is queried fresh at use time; make timetable expansion, week view (days labels), Term.calendar and the ICS feed honour it; add the import_academic_calendar command with the 2026-2027 fall and spring seed files transcribed from the official PKU calendar.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012t4BD5H1P5nn2zF7oU6q9e
…four-source toggles

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012t4BD5H1P5nn2zF7oU6q9e
…ng all source toggles

Add TimetableSettings.show_courses, make every source (stored entries, 书院课, activities, appointments) respect the settings in the ICS feed as well, add date-span queries to the live sources and the GET /api/v2/timetable/agenda/ endpoint (consecutive days across term boundaries with calendar labels) for the home-page agenda, with tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012t4BD5H1P5nn2zF7oU6q9e
…nt envelopes

Validation errors now carry errors: {field: [{code, message}]}, the shape of the platform-wide envelope introduced by api/exceptions.py (PR Yuanpei-Intelligence#1014) and parsed by the mini-program's unified RequestError (YPPF-mini Yuanpei-Intelligence#10). Switching these modules to StandardizedExceptionHandlerMixin is a mechanical follow-up once Yuanpei-Intelligence#1014 lands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012t4BD5H1P5nn2zF7oU6q9e
… and share assets

Iteration 3 of the timetable (README §8):

- Entries link to the course catalog on import (blank fields filled, the
  student's values win) and carry role (已选/旁听), category (course/exam/
  other) and a free tag; POST catalog/<id>/add/ adds a catalog course as 旁听.
- Per-week overrides (TimetableEntryOverride) back PATCH entries/<id>/ with
  scope all/single/following and a canceled flag; imported entries keep the
  student's edits across re-imports; GET entries/<id>/ and DELETE overrides.
- Settings expose the registered sources, the student's tags and hidden_tags;
  a new exam source matches the term's CourseExam rows (import_exam_schedule)
  to the student's courses; terms gain exam_week_start / teaching_weeks and
  the seed calendars cover the exam weeks.
- GET share/assets/ serves the mini-program code (wxacode, cached) and the
  official-account QR code for the poster.
- Spectacular enum names pinned for the new choice sets.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012t4BD5H1P5nn2zF7oU6q9e
…d a deployment checklist

- static/assets/img/yppf_official_qrcode.png is the default
  wx_miniapp.share.official_qrcode_url; a leading slash now means a site
  path joined with global.base_url (MEDIA_URL only for bare names).
- New management command `timetable_check` prints the README §9
  deployment checklist as [OK]/[WARN]/[FAIL] lines (migrations, config,
  sources, subscribe template, share assets, jobs, terms / calendar /
  catalog / exams) and exits non-zero on failures unless --warn-only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012t4BD5H1P5nn2zF7oU6q9e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant