重邮课程表导入日历

虽然现在重邮课表查看可以通过掌上重邮 app 和 We 重邮微信小程序，但是这两个应用都不能有课程的时候提前通知。因此不如通过这两个应用的接口提供课表数据，然后转化为通用的 iCalendar格式导入日历。就可以体验到系统级的日历通知了。

此前已经有一个 Python 版本的 CQUPT-ics 是重邮的学长写的，但是目前这两个数据源的接口都已经发生改变，已经无法正常使用。

因此我就打算写一个新的项目来实现上述目标。按照先前学长项目的思路来一个重写 RIIR (Rewrite It In Rust)

新屎山地址：https://github.com/jizizr/cqupt-ics-rs 项目源码在这里

使用地址：https://cqupt.op.wiki/ 要用这个项目点这里

基本实现了先前的 Python 版本的功能，此外，也实现了调休的自动课程调节。避免了放假时候还收到课程的通知，和忘记今天调休要上课（重邮官方课表是不会根据放假去改的，需要自己手动查看调休安排照着去看）

当然，纯靠凭拼接订阅链接还是不够友好，当然要来一个前端的使用界面（AI 搓的），应该可以友好地使用本项目了。学校晚上会关闭课表查询，因此 大概 0-7 点查不了课表，属于正常现象

PS: 一键导入的实现只实现了 Android 和 apple 系列系统。其他系统自行在支持 ics 订阅的日历里复制链接添加

本来是打算使用 webcal:// 跳转日历添加，理论上 webcal:// 协议头跳转基本系统都实现了，但是在 apple (macos ios) 之外其他系统由于实际提供日历的软件不同，具体有着极大的差异，而且基本会出现奇奇怪怪的问题。因此只提供给了 apple 使用，然后 Android 使用 intent:// 去跳转系统日历。因此只实现了 apple 系列系统和 Android。

如果有更好的实现欢迎提 PR，有 bug 欢迎提 issue。

最后来看一下导入的效果图

在国庆节（休）去除了所有的课，在国庆节（班）的有对应调休的课。

手机上的效果

要增加新的 provider 源只要实现一个 trait 就行（非常简单）

Copy
#[async_trait]
pub trait Provider: Send + Sync {
/// Token type for this provider
type Token: Send + Sync + Serialize + DeserializeOwned;
type ContextType: Send + Sync;
/// Provider 的名字
fn name(&self) -> &str;

/// Provider 的描述
fn description(&self) -> &str;

/// Get timezone for this provider
///
/// Returns the timezone used by this provider for time calculations.
/// This is used to ensure consistent timezone handling across all
/// provider operations.
/// provider 的时区
fn timezone(&self) -> FixedOffset;

/// Authenticate and get token
/// 获取 token 的方法
async fn authenticate<'a, 'b>(
&'a self,
context: ParamContext<'b, Self::ContextType>,
request: &CourseRequest,
) -> Result<Self::Token>;

/// Validate existing token
/// 验证 token 是否有效
async fn validate_token(&self, token: &Self::Token) -> Result<bool>;

/// Refresh token
/// 刷新 token 的方法（如果 provider 不存在则直接返回 Err 即可）
async fn refresh_token(&self, token: &Self::Token) -> Result<Self::Token>;

/// Get courses using token
/// request.semester should be Some before calling this method
/// If use crate::providers::Wrapper, it will ensure semester is Some
/// 获取课程数据的方法
async fn get_courses<'a, 'b>(
&'a self,
context: ParamContext<'b, Self::ContextType>,
request: &mut CourseRequest,
token: &Self::Token,
) -> Result<CourseResponse>;

/// Get semester start date
/// This is called if request.semester is None before get_courses
/// If you use crate::providers::Wrapper, it will call this method automatically if request.semester is None
/// You can use the context to store intermediate data if needed
/// 获取学期开始日期的方法
async fn get_semester_start<'a, 'b>(
&'a self,
context: ParamContext<'b, Self::ContextType>,
request: &mut CourseRequest,
token: &Self::Token,
) -> Result<chrono::DateTime<FixedOffset>>;

/// Token TTL
/// 返回 token 的有效期，用于控制缓存
fn token_ttl(&self) -> Duration {
Duration::from_secs(3600 * 24) // 24 hours default
}
}