Skip to content

Mike-Zhuang/Jinitaimei

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

41 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Jinitaimei · 同济大学 iPhone App

一个面向同济大学学生的开源 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 实现了三层兜底续期:

  1. L1 静默续期:业务 API 收到 401/403 时,由 AuthRecoveryManager 通过隐藏在根视图上的 0×0 WKWebView 重新访问 1.tongji.edu.cn/workbench。只要 IAM SSO cookie 仍有效,浏览器会自动跳回 workbench 并写入新的 sessiondata,全过程对用户无感。
  2. L2 密码自动回填(严格 opt-in):用户在 设置 → 自动登录 中开启后,账号密码会以 kSecAttrAccessControl(biometryCurrentSet) 写入 Keychain,密钥锁进 Secure Enclave。L1 失败时,框架会触发一次 Face ID / Touch ID 校验,然后用 JS 注入填充 IAM 登录表单提交。若学校 SSO 当时要求图形验证码 / 短信码 / 二次验证,会自动检测并降级到 L3。
  3. L3 交互登录:上述全部失败 → CampusModel.authStaterequiresInteractiveLogin,日程页 / 卓越星页顶部出现非阻塞 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 保护的自动登录策略。

日程页的校历接口只用于缓存"本学期第几周对应哪一段日期":calendarIdbeginDayendDayweekNum 等。当前显示第几周由本机系统日期和缓存的 beginDay 即时计算,同一学期内不会每次进入日程页都请求校历接口。学期选择只展示从学生入学学年第一学期开始,到当前学期加下一学期为止;入学年份优先取一系统个人资料中的年级,缺失时用学号前两位兜底推断。

课表导出到系统日历时,导出 Sheet 内完成课程选择、考试开关、提醒和目标日历选择。若系统只授予写入权限、不能列出全部日历,则默认写入系统默认日历。

考试安排与课程成绩

考试安排与课程成绩拆成两个独立校园服务入口:

  • 考试安排:同步当前学期考试与考察条目。本地缓存成功后,具有明确日期和起止时间的考试会同步显示在日程页对应周的"本周考试"区域,也可从考试安排页写入系统日历。考察、论文、汇报等无明确时间条目只在 App 内展示,不导出。
  • 课程成绩:展示总平均绩点、实修学分、不及格统计、分学期课程列表和课程详情。列表保持克制,不直接复刻网页大表格。

一系统课程类别存在两套编号:网页底部红圈序号与后端 queryCourseTag.id。App 会动态拉取 queryCourseTag,同时按两种编号映射到 shortName。例如:

原始字段 App 展示
科学探索与生命关怀[2] 科学探索与生命关怀 · 精品类(核心)
人文经典与审美素养[125] 人文经典与审美素养 · 大学美育
人文经典与审美素养 人文经典与审美素养

courseNature 中的 SJ 等值属于一系统内部代码,不作为用户可见课程类别展示。

校园卡与教务附件

  • 校园卡续期优先走同济校园卡登录中转页;必要时使用 all4u 入口获取可用凭证。余额接口失效时会清理校园卡专属旧凭证并重新续期,不影响一系统登录态。
  • 校园卡详情页展示余额、更新时间、最近 7 天每日消费曲线和精简后的消费记录。卡号、账户号、机位号以及重复泛化文案不在列表中堆叠展示。
  • 教务通知附件只在用户点击时按需下载。下载请求复用一系统 Cookie 与 X-Token,凭证失效时自动续期并重试一次;下载后的文件仅写入临时目录,用于系统预览或分享。

图书馆座位

图书馆座位功能只读展示,不提交预约、取消预约或签到。首版重点展示四个图书馆:

  • 四平路校区图书馆
  • 德文图书馆
  • 东区图书馆
  • 嘉定校区图书馆

当前抓包未发现真实门禁入馆人数接口,因此页面明确使用“座位占用口径”:已占用座位 / 总座位可用座位 / 总座位。若后续拿到真实入馆人数接口,再在同一页面升级为双口径展示。

座位详情按区域懒加载学校返回的坐标与状态,并绘制为平面图,不展示几千条座位列表。筛选支持学校标签(靠窗、带电源、安静等)和本地 单人单座 标签。本地规则来自人工确认:

  • 四平路二楼:113-128191-214
  • 四平路 6 / 8 / 10 楼南北:001-007032-040073-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.mddocs/FEATURE-BEHAVIOR.md

暂未实现 / 不计划在 v0.1 实现

已删除 / 不创建(参考 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

在 Xcode 中调试

环境要求

  • macOS 14 Sonoma 或更新
  • Xcode 15.0+(含 iOS 17.0 SDK)
  • 必装 XcodeGenbrew 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

在 iPhone 模拟器中运行

  1. 打开 Jinitaimei.xcodeproj
  2. 顶部 scheme 选 Jinitaimei
  3. 顶部 device 选择任意 iPhone 模拟器(例如 iPhone 15 / iPhone 15 Pro,iOS 17.x)。
  4. ⌘R 运行。首次启动不会自动弹出登录页;进入 设置 → 校园账户 后,用学号 + 统一身份密码登录即可。登录完成后 App 会询问是否保存账号密码用于自动续期登录(Face ID / Touch ID 保护,默认拒绝;也可随时在 设置 → 自动登录 中关闭)。
  5. 模拟器内 WebView 调试:在 macOS Safari 菜单中开启 开发 → 模拟器 → <你的 WebView>,可像调试网页一样调试 SSO 流程。

用数据线在真机 iPhone 上调试

  1. 用数据线把 iPhone 接到 Mac,iPhone 端点击 信任此电脑
  2. iPhone:设置 → 隐私与安全性 → 开发者模式 打开(仅 iOS 16+ 需要),重启 iPhone。
  3. Xcode 顶部 device 列表里选你的 iPhone(不要选模拟器)。
  4. 选中项目 → Signing & Capabilities 标签:
    • Team 改为你自己的 Apple ID(在 Xcode → Settings → Accounts 里先登录账号)。
    • Bundle Identifier 改成全局唯一值,例如 com.<yourname>.jinitaimei(默认 com.jinitaimei.app 可能被占用)。
  5. ⌘R 安装并运行。首次安装后 iPhone 会弹"不受信任的开发者",去 设置 → 通用 → VPN与设备管理 → 开发者 App → 你的 Apple ID → 信任 一次即可。
  6. 免费 Apple ID 的真机证书 7 天后失效;到期后重新 ⌘R 一次即可重签。

真机 WebView 调试

  1. iPhone:设置 → Safari → 高级 → Web 检查器 打开。
  2. iPhone 接到 Mac,启动 App 并停留在登录页。
  3. Mac Safari:开发 → <你的 iPhone> → <WebView> 即可。

命令行构建(CI 友好)

# 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 会被系统暂停,导致抽取失败。
  • 修改 project.yml 后 Xcode 编译不到新文件 —— 重新跑 xcodegen generate
  • 修改了 SPM Package(TongjiKit / TongjiUI)后 Xcode 看似没更新 —— File → Packages → Reset Package Caches,或直接关闭 Xcode 后 rm -rf Jinitaimei.xcodeproj/project.xcworkspace/xcshareddata/swiftpmxcodegen generate
  • 课程成绩类别显示旧值或括号编号 —— 进入 校园 → 课程成绩 下拉刷新。成绩缓存会在同步成功后重写;类别解析会同时兼容网页红圈序号与后端 queryCourseTag.id
  • 真机首次安装提示"不受信任的开发者" —— 见上面"真机数据线调试"第 5 步。

上游参考链接

本项目源代码均为重写而成,未直接复制上游代码。借鉴部分主要在:架构分层(DanXi-swift 的 Fudan Kit + FudanUI 双 Package 拆分)、信息架构("校园服务首页 + 可点开列表"模式)、SwiftUI 视图组织与 WKWebView SSO 登录交互。

License

待选(计划与上游 DanXi 一致,使用 MIT 或 Apache 2.0)。在确定前所有代码暂以"All Rights Reserved" 默认许可。

About

No description, website, or topics provided.

Resources

License

Stars

2 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages