一个面向同济大学学生的开源 iOS App,仅支持 iPhone(不支持 iPad / Mac / watchOS)。当前为 v0.1.0 早期版本,按"逐步开发"的方式从最小可用功能开始迭代。
Jinitaimei 在架构与 UI 设计上重度参考了复旦大学的优秀开源项目 DanXi(Flutter)与 DanXi-swift(SwiftUI 重写版);同济业务数据抓取逻辑参考了作者所在团队的 .NET MAUI 原型 wish_drom。在此致谢上游项目。
| Tab | 功能 | 数据来源 | 状态 |
|---|---|---|---|
| 日程 | 周课表查看(按周切换、点击节次看详情、显示本周考试、导出课表到系统日历) | 一系统 1.tongji.edu.cn findStudentTimetab + currentTermCalendar + 考试查询 |
v0.1 可用 |
| 校园 | 卓越星活动列表、筛选排序、个人星值摘要 | STAR 平台 star.tongji.edu.cn |
v0.1 可用 |
| 校园 | 教学管理信息系统通知公告(列表、详情、附件按需下载与预览、置顶服务卡片) | 一系统 1.tongji.edu.cn commonMsgPublish + obsfile |
v0.1 可用 |
| 校园 | 校园卡余额、消费记录、最近 7 天每日消费曲线、低余额提醒 | 同济校园卡 pay-yikatong.tongji.edu.cn + all4u.tongji.edu.cn |
v0.1 可用 |
| 校园 | 考试安排(考试/考察列表、置顶卡片、导出到系统日历、同步到 App 日程) | 一系统 1.tongji.edu.cn undergraduateExamQuery |
v0.1 可用 |
| 校园 | 课程成绩(总绩点、学分、不及格统计、分学期成绩、课程详情) | 一系统 1.tongji.edu.cn scoreGrades + queryCourseTag |
v0.1 可用 |
| 校园 | 图书馆座位(四个重点馆座位占用概览、区域座位图、研习室三天空闲时段、单人单座本地标签) | 图书馆空间系统 space.tongji.edu.cn |
v0.1 可用 |
| 校园 | 智能控水(分组、控水器空闲 / 使用中 / 异常只读状态、置顶摘要) | 智能控水 ks.tongji.edu.cn + 校园卡 pay-yikatong.tongji.edu.cn |
v0.1 可用 |
| 校园 | 宿舍洗衣机(附近洗衣房、单房间机器空闲 / 运行中 / 离线状态、常用洗衣房摘要) | CoolEasy wx2.cooleasy.net |
v0.1 可用 |
| 设置 | 校园账户登录、账户信息、退出登录、自动登录开关、邮件推送与低余额阈值 | 同济统一身份认证 + 一系统 session 用户信息 | v0.1 可用 |
登录方式:仅同济统一身份认证(iam/ids.tongji.edu.cn)。用户从 设置 → 校园账户 进入登录页,在 WebView 内完成一次 SSO 后,App 会在同一个 WebView 内于遮罩下后台抓取凭证(Cookie + sessionid + AES 加密 studentCode)。STAR 平台的活动列表为公开接口,无需额外登录;个人星值用独立的 Bearer Token,由 StarAuthCoordinator 单独维护。
邮件推送:设置 → 通知 中可填写接收邮箱并同步到 https://tjpush.mikezhuang.cn。如果用户开启“离线邮件推送”并保存同济统一身份账号密码,服务端会加密保存这组凭据,用于低频轮询教务通知;卓越星新活动和报名提醒优先使用公开活动接口,不上传 STAR Token。关闭邮件推送会请求服务器删除订阅和凭据。SMTP 密码、服务端加密主密钥不写入 App 或仓库。
校园卡低余额提醒:设置 → 通知 中可单独开启“校园卡低余额提醒”,默认阈值为 50 元,也可手动改成 9 / 20 / 30 等。App 本地通知与服务端邮件提醒统一使用这一个阈值;首次只建立基线,不补发历史低余额状态,后续只有在“余额从高于阈值变为低于或等于阈值”时才提醒一次。若余额一直处于低位,不会反复打扰;只有余额恢复到阈值以上后再次跌破,才会重新提醒。
为了避免"半夜被踢、早起重登"的体验,App 实现了三层兜底续期:
- L1 静默续期:业务 API 收到 401/403 时,由
AuthRecoveryManager通过隐藏在根视图上的 0×0WKWebView重新访问1.tongji.edu.cn/workbench。只要 IAM SSO cookie 仍有效,浏览器会自动跳回 workbench 并写入新的sessiondata,全过程对用户无感。 - L2 密码自动回填(严格 opt-in):用户在
设置 → 自动登录中开启后,账号密码会以kSecAttrAccessControl(biometryCurrentSet)写入 Keychain,密钥锁进 Secure Enclave。L1 失败时,框架会触发一次 Face ID / Touch ID 校验,然后用 JS 注入填充 IAM 登录表单提交。若学校 SSO 当时要求图形验证码 / 短信码 / 二次验证,会自动检测并降级到 L3。 - L3 交互登录:上述全部失败 →
CampusModel.authState转requiresInteractiveLogin,日程页 / 卓越星页顶部出现非阻塞 banner 引导手动登录。本地课表 / 活动缓存不会被清空。
并发请求安全:AuthRecoveryManager 通过 single-flight Task<Bool, Never> 把多个 API 并发遇到 401 的情况合并成一次续期,不会出现"同一时间触发多次重登"。
Cookie 处理:所有响应里的 Set-Cookie 会被合并回 CookieJar.shared(结构化存储,按 domain / path / expires 过滤、持久化到 Keychain tongji_cookies_jar),让服务端滚动刷新的 sessionId 立即生效。
Cookie 双向同步:参考 Android 下游项目更稳定的“全局 CookieJar + SSO 预热 + 统一请求注入”思路,iOS 端现在在静默续期、密码回填和回前台会话检查前,会把 Keychain 中仍有效的同济 Cookie 回灌到隐藏 WKWebView;登录成功、续期成功和 all.tongji.edu.cn 预热完成后,再从 WKHTTPCookieStore 回写 CookieJar。这样 URLSession 和 WebView 不再各自持有一套割裂的登录态。
SSO 预热:登录成功或回前台发现 all.tongji.edu.cn Cookie 不完整时,App 会非阻塞访问 https://all.tongji.edu.cn/new/index.html,用于补齐同济门户域 Cookie。预热失败只写脱敏诊断日志,不会直接判定一系统掉线。
安全边界:不会照搬 Android 端把 session cookie 强制延长 24 小时、BODY 级网络日志、Cookie / Token / sessiondata 明文日志、或用 uid 兜底充当 X-Token 的做法。iOS 端继续保留真实过期时间和 Face ID / Touch ID 保护的自动登录策略。
日程页的校历接口只用于缓存"本学期第几周对应哪一段日期":calendarId、beginDay、endDay、weekNum 等。当前显示第几周由本机系统日期和缓存的 beginDay 即时计算,同一学期内不会每次进入日程页都请求校历接口。学期选择只展示从学生入学学年第一学期开始,到当前学期加下一学期为止;入学年份优先取一系统个人资料中的年级,缺失时用学号前两位兜底推断。
课表导出到系统日历时,导出 Sheet 内完成课程选择、考试开关、提醒和目标日历选择。若系统只授予写入权限、不能列出全部日历,则默认写入系统默认日历。
考试安排与课程成绩拆成两个独立校园服务入口:
- 考试安排:同步当前学期考试与考察条目。本地缓存成功后,具有明确日期和起止时间的考试会同步显示在日程页对应周的"本周考试"区域,也可从考试安排页写入系统日历。考察、论文、汇报等无明确时间条目只在 App 内展示,不导出。
- 课程成绩:展示总平均绩点、实修学分、不及格统计、分学期课程列表和课程详情。列表保持克制,不直接复刻网页大表格。
一系统课程类别存在两套编号:网页底部红圈序号与后端 queryCourseTag.id。App 会动态拉取 queryCourseTag,同时按两种编号映射到 shortName。例如:
| 原始字段 | App 展示 |
|---|---|
科学探索与生命关怀[2] |
科学探索与生命关怀 · 精品类(核心) |
人文经典与审美素养[125] |
人文经典与审美素养 · 大学美育 |
人文经典与审美素养 |
人文经典与审美素养 |
courseNature 中的 SJ 等值属于一系统内部代码,不作为用户可见课程类别展示。
- 校园卡续期优先走同济校园卡登录中转页;必要时使用
all4u入口获取可用凭证。余额接口失效时会清理校园卡专属旧凭证并重新续期,不影响一系统登录态。 - 校园卡详情页展示余额、更新时间、最近 7 天每日消费曲线和精简后的消费记录。卡号、账户号、机位号以及重复泛化文案不在列表中堆叠展示。
- 教务通知附件只在用户点击时按需下载。下载请求复用一系统 Cookie 与
X-Token,凭证失效时自动续期并重试一次;下载后的文件仅写入临时目录,用于系统预览或分享。
图书馆座位功能只读展示,不提交预约、取消预约或签到。首版重点展示四个图书馆:
- 四平路校区图书馆
- 德文图书馆
- 东区图书馆
- 嘉定校区图书馆
当前抓包未发现真实门禁入馆人数接口,因此页面明确使用“座位占用口径”:已占用座位 / 总座位 与 可用座位 / 总座位。若后续拿到真实入馆人数接口,再在同一页面升级为双口径展示。
座位详情按区域懒加载学校返回的坐标与状态,并绘制为平面图,不展示几千条座位列表。筛选支持学校标签(靠窗、带电源、安静等)和本地 单人单座 标签。本地规则来自人工确认:
- 四平路二楼:
113-128、191-214 - 四平路 6 / 8 / 10 楼南北:
001-007、032-040、073-081 - 东区二楼:
57-69 - 东区三楼:
65-79
研习室展示今天、明天、后天三天的空闲时段时间轴,并显示人数限制;东区图书馆若接口暂无研习室数据,会显示空状态。
智能控水功能只读展示,不提交开关水、预约、锁定或控制请求。首版展示:
- 分组列表
- 每组控水器空闲 / 使用中 / 离线 / 报警状态
- 校园首页置顶摘要
- 本地缓存与按需刷新
鉴权链路复用校园卡 synjones-auth:App 先通过校园卡登录态进入 pay-yikatong.tongji.edu.cn 的水控跳转入口,再到 ks.tongji.edu.cn 页面提取 sessionStorage.ano、前端 JS 中的 AES key 和接口 password。水控参数、Cookie 只写入 Keychain,日志只输出来源、长度、状态和 Cookie 名称,不输出 token、Cookie、AES key、password 或完整一卡通号。
水控 API 位于:
GET https://ks.tongji.edu.cn/waterapi/api/UseHzWatch
GET https://ks.tongji.edu.cn/waterapi/api/GetToken?info=...
GET https://ks.tongji.edu.cn/waterapi/api/AccUseHzWatch?info=...&token=...
info 使用与 Android 下游一致的 AES/ECB/PKCS7 加密;分组页先拉分组,展开某个分组时才懒加载该组控水器,避免一次性请求所有设备。
洗衣机功能只读展示,不提交预约、排队、支付、扫码、启动或停止洗衣请求。数据来自 https://wx2.cooleasy.net 的 CoolEasy 微信网页接口,不依赖同济统一认证,但依赖 CoolEasy 自己的微信网页会话 Cookie(关键为 loginStatusId)。App 会保存并滚动更新 CoolEasy 同站 Cookie;有可用会话时,先打开洗衣房页面抽取网页 __RequestVerificationToken,再请求附近洗衣房列表;展开某个洗衣房时再抽取该详情页 token 并拉取该房间机器状态。
首版默认使用四平路附近坐标 121.498886, 31.28323 拉取附近洗衣房,不请求系统定位权限。页面支持把常用洗衣房置顶,首页卡片和详情页会优先同步这个洗衣房;未展开的宿舍不会批量拉取机器列表。
更多协议与行为约定见 docs/PROTOCOLS.md 和 docs/FEATURE-BEHAVIOR.md。
已删除 / 不创建(参考 DanXi 但本项目不实现):树洞社区、课评、AI 助手、教师日程、食堂排队、巴士、电费、运动数据、钱包、宿舍预约…… 后续按需独立 module 增量加入。
Jinitaimei/
├── project.yml # XcodeGen 项目描述(唯一事实来源;修改后跑 `xcodegen generate`)
├── Jinitaimei.xcodeproj/ # Xcode 壳工程(不入仓,clone 后由 xcodegen 生成)
├── App/ # iOS App 入口
│ ├── JinitaimeiApp.swift # @main 入口,注册 SwiftData ModelContainer
│ ├── Info.plist
│ └── Assets.xcassets/
├── TongjiKit/ # 业务 SDK(本地 SPM Package)
│ └── Sources/TongjiKit/
│ ├── Authentication/ # SSO 登录 / 静默续期 / 密码回填 / AuthRecoveryManager / CookieJar / Keychain / studentCode AES-CBC / StarAuthCoordinator
│ ├── Course/ # 一系统课表 API + 解析 + SwiftData 仓储
│ ├── ExamScore/ # 一系统考试安排 / 课程成绩 API + SwiftData 缓存 + 考试日历导出
│ ├── Activity/ # STAR 活动 API + 解析 + SwiftData 仓储
│ ├── TeachingNotice/ # 一系统通知公告 API + 模型
│ ├── Wallet/ # 校园卡余额与消费记录 API / 登录续期 / SwiftData 快照
│ ├── LibrarySpace/ # 图书馆空间系统 API / JWT 续期 / 座位与研习室模型 / SwiftData 概览缓存
│ ├── WaterControl/ # 智能控水 API / AES-ECB 加密 / 参数提取 / SwiftData 快照
│ ├── Laundry/ # 宿舍洗衣机网页 token / 洗衣房与机器状态 / SwiftData 快照
│ ├── Notification/ # 本地通知、偏好与变化检测
│ ├── Push/ # 邮件推送订阅 API
│ ├── Profile/ # 一系统 session 用户信息
│ ├── CampusModel.swift # 全局登录态
│ └── Util/ # JSON 等工具
└── TongjiUI/ # UI 层(本地 SPM Package,依赖 TongjiKit)
└── Sources/TongjiUI/
├── Navigation/ # RootView(TabBar + 隐藏续期 WebView + scenePhase 节流检查) / CampusHome
├── Components/ # AuthStateBanner(renewing / requiresInteractiveLogin 非阻塞 banner)
├── Pages/ # LoginPage / AutoLoginSettingsView / ActivityListPage / TeachingNoticePage / CampusCardPage / ExamSchedulePage / GradeReportPage / LibrarySpacePage / WaterControlPage / SettingsPage
└── Calendar/ # CoursePage(周课表 + 本周考试)
模块依赖方向(单向):
Jinitaimei (App) → TongjiUI → TongjiKit
↘ SwiftData / WebKit / SafariServices / CommonCrypto / Security
- macOS 14 Sonoma 或更新
- Xcode 15.0+(含 iOS 17.0 SDK)
- 必装 XcodeGen:
brew install xcodegen本仓库不提交Jinitaimei.xcodeproj/(已写进.gitignore),project.yml才是工程描述的唯一事实来源。 - 一个 Apple ID(免费即可,真机 7 天证书;如有付费开发者账号可直接长期签)
# 1. 安装 XcodeGen(如已装跳过)
brew install xcodegen
# 2. 在仓库 Jinitaimei/ 目录下生成 Xcode 工程
cd Jinitaimei
xcodegen generate
# 3. 打开
open Jinitaimei.xcodeproj任何时候修改了
project.yml(增删源文件目录、改 bundle id、调 build setting 等)后,都要重新跑xcodegen generate。也别试图把 Xcode 里手动加的文件/设置写回,下一次 generate 会被覆盖——所有改动应该写到project.yml。
- 打开
Jinitaimei.xcodeproj。 - 顶部 scheme 选 Jinitaimei。
- 顶部 device 选择任意 iPhone 模拟器(例如
iPhone 15/iPhone 15 Pro,iOS 17.x)。 ⌘R运行。首次启动不会自动弹出登录页;进入设置 → 校园账户后,用学号 + 统一身份密码登录即可。登录完成后 App 会询问是否保存账号密码用于自动续期登录(Face ID / Touch ID 保护,默认拒绝;也可随时在设置 → 自动登录中关闭)。- 模拟器内 WebView 调试:在 macOS Safari 菜单中开启
开发 → 模拟器 → <你的 WebView>,可像调试网页一样调试 SSO 流程。
- 用数据线把 iPhone 接到 Mac,iPhone 端点击 信任此电脑。
- iPhone:
设置 → 隐私与安全性 → 开发者模式打开(仅 iOS 16+ 需要),重启 iPhone。 - Xcode 顶部 device 列表里选你的 iPhone(不要选模拟器)。
- 选中项目 → Signing & Capabilities 标签:
- Team 改为你自己的 Apple ID(在 Xcode → Settings → Accounts 里先登录账号)。
- Bundle Identifier 改成全局唯一值,例如
com.<yourname>.jinitaimei(默认com.jinitaimei.app可能被占用)。
⌘R安装并运行。首次安装后 iPhone 会弹"不受信任的开发者",去设置 → 通用 → VPN与设备管理 → 开发者 App → 你的 Apple ID → 信任一次即可。- 免费 Apple ID 的真机证书 7 天后失效;到期后重新
⌘R一次即可重签。
- iPhone:
设置 → Safari → 高级 → Web 检查器打开。 - iPhone 接到 Mac,启动 App 并停留在登录页。
- Mac Safari:
开发 → <你的 iPhone> → <WebView>即可。
# 1. 生成工程(首次或修改 project.yml 后)
xcodegen generate
# 2. 仅编译(不签名),快速校验代码无误
xcodebuild \
-project Jinitaimei.xcodeproj \
-scheme Jinitaimei \
-destination 'generic/platform=iOS Simulator' \
-configuration Debug \
build CODE_SIGNING_ALLOWED=NO- App 界面只占屏幕中间,上下两条黑边 ——
iOS 没识别到 LaunchScreen 配置,被强制按"iPhone 4 兼容模式"渲染。检查 App/Info.plist 里有
UILaunchScreen键,然后在 Xcode 里Product → Clean Build Folder(⇧⌘K) 一次再 ⌘R。 - 登录页一闪而过自动关闭 ——
当前实现在每次进入
LoginPage时调用TongjiAuthCoordinator.startFreshInteractiveLogin()主动清空 WKWebView 的 cookie/localStorage,强制重新走 SSO;如果你修改这条路径去掉清理逻辑,残留 cookie 会让 SSO 静默通过、登录页会瞬间关闭。(注意:静默续期路径attemptSilentRenew()故意不清数据,目的就是复用 IAM cookie,请勿混淆。) - 第二天打开 App 又被踢出登录 ——
正常情况下应当只看到顶部短暂出现"登录状态刷新中…"的 banner,几秒后自动消失。如果反复落到
requiresInteractiveLogin,看控制台[AuthRecovery]/[Auth]日志:- L1 失败 + 用户未开自动登录 → 直接进 L3,属于预期。建议在
设置 → 自动登录中开启 Face ID 保护的自动登录。 - L1 失败 + L2 也失败 → 多半学校 SSO 当时在弹验证码 / 短信码,搜索
mfaRequired日志即可确认;只能手动登录一次。 - L1 一直超时 → 检查根视图是否正确挂载了
AuthRecoveryManager.shared.silentCoordinator.webView(0×0 隐藏视图)。WKWebView 不挂在视图树上时 SPA JS 会被系统暂停,导致抽取失败。
- L1 失败 + 用户未开自动登录 → 直接进 L3,属于预期。建议在
- 修改 project.yml 后 Xcode 编译不到新文件 —— 重新跑
xcodegen generate。 - 修改了 SPM Package(TongjiKit / TongjiUI)后 Xcode 看似没更新 ——
File → Packages → Reset Package Caches,或直接关闭 Xcode 后rm -rf Jinitaimei.xcodeproj/project.xcworkspace/xcshareddata/swiftpm再xcodegen generate。 - 课程成绩类别显示旧值或括号编号 ——
进入
校园 → 课程成绩下拉刷新。成绩缓存会在同步成功后重写;类别解析会同时兼容网页红圈序号与后端queryCourseTag.id。 - 真机首次安装提示"不受信任的开发者" —— 见上面"真机数据线调试"第 5 步。
- 复旦 DanXi(Flutter):https://github.com/DanXi-Dev/DanXi
- 复旦 DanXi-swift(SwiftUI):https://github.com/DanXi-Dev/DanXi-swift
- 同济 wish_drom(.NET MAUI 原型, https://github.com/wyyyz1937365497/wish_drom)
本项目源代码均为重写而成,未直接复制上游代码。借鉴部分主要在:架构分层(DanXi-swift 的
Fudan Kit+FudanUI双 Package 拆分)、信息架构("校园服务首页 + 可点开列表"模式)、SwiftUI 视图组织与 WKWebView SSO 登录交互。
待选(计划与上游 DanXi 一致,使用 MIT 或 Apache 2.0)。在确定前所有代码暂以"All Rights Reserved" 默认许可。