Skip to content

Repository files navigation

ProjektMing.Bistu

面向北京信息科技大学(BISTU)统一身份认证和业务系统的 .NET SDK。

SDK 使用一个顶层客户端维护共享身份、Cookie 和 HTTP 传输。业务系统由独立扩展包提供,通过 bistu.Jwxt 等属性按需创建,无需预先注册。各模块管理自己的认证协议;简单 CAS 系统可以复用核心的认证便捷能力。

当前功能

  • BISTU SSO 用户名/密码登录。
  • 共享 Cookie 会话和登录状态管理。
  • 免注册的按需模块,以及显式 CAS 请求上下文。
  • 原始 HTTP 请求接口,便于调用尚未封装的 API。
  • JWXT(教务系统)强类型接口:学期、校区、校历、成绩、课表、考试和空闲教室。
  • 支持通过扩展包接入其他 BISTU 业务系统。

项目结构

项目 说明
src/Bistu 核心客户端、SSO 登录、Cookie 会话、重定向和模块生命周期。
src/Bistu.Jwxt 独立的教务模块及强类型 API。
samples/Bistu.Console.cs 使用 SDK 登录并读取学期、成绩和课表的 file-based 控制台示例。
tests/Bistu.Tests 核心客户端和 JWXT 集成测试。
docs/api.md BISTU SSO、教务系统及其他业务系统的 API 记录。
docs/module-design.md 按需模块的契约、认证边界和验证要求。

核心包名为 ProjektMing.Bistu,教务系统扩展包名为 ProjektMing.Bistu.Jwxt。当前 API 版本为 1.0.0,以.Net 10为目标框架。

快速开始

引用 ProjektMing.Bistu.Jwxt 后,即可直接创建客户端使用教务模块:

using ProjektMing.Bistu;
using ProjektMing.Bistu.Jwxt;

using var bistu = new BistuClient();
await bistu.LoginWithPasswordAsync("学号", "密码");
var schedule = await bistu.Jwxt.GetScheduleAsync();

使用依赖注入时,只需注册核心客户端:

using Microsoft.Extensions.DependencyInjection;
using ProjektMing.Bistu;
using ProjektMing.Bistu.Jwxt;

var services = new ServiceCollection();

services.AddBistu();

using var provider = services.BuildServiceProvider();
using var scope = provider.CreateScope();

var bistu = scope.ServiceProvider.GetRequiredService<IBistuClient>();
var login = await bistu.LoginWithPasswordAsync("学号", "密码");

Console.WriteLine($"登录成功,凭证预计于 {login.ExpiresAt:u} 过期");

var terms = await bistu.Jwxt.GetTermsAsync();
var grades = await bistu.Jwxt.GetGradesAsync();
var schedule = await bistu.Jwxt.GetScheduleAsync();

SsoEndpoint 默认值为 https://sso.bistu.edu.cn/。需要覆盖时可通过选项配置:

services.AddBistu(options => options.SsoEndpoint = new Uri("https://sso.example.test/"));

控制台示例(file-based app)

运行 samples/Bistu.Console 前,可以通过环境变量提供账号;未提供密码时,示例会在终端中隐藏输入:

dotnet run --file samples/Bistu.Console.cs -e BISTU_USERNAME='<学号>' -e BISTU_PASSWORD='<密码>'

示例会登录 SSO,显示 selected=true 的学期并使用该学期查询成绩,然后选择第一个校区输出完整课表。若接口没有返回 selected=true,示例会停止后续查询。课表会分别展示已安排、合并、未安排和实践课程,并输出课程、时间、地点、周次、教师和教学班信息。账号和密码只用于当前进程,不会写入项目文件。

也可以直接编译或发布这个文件:

dotnet build samples/Bistu.Console.cs
dotnet publish samples/Bistu.Console.cs -c Release -r win-x64 --self-contained false

用户名和密码仅用于当前登录流程。密码在发送到 SSO 前会使用 SSO 返回的 SM2 公钥加密;登录成功后,客户端在当前作用域内保存 Cookie 和登录会话。

会话和生命周期

通过依赖注入注册时,BistuClient 与 IBistuClient 都是 Scoped 服务。一个作用域对应一个独立的:

  • CookieContainer;
  • HTTP 传输实例;
  • SSO 登录状态;
  • 按需创建的业务模块及其认证状态。

建议在需要共享登录态的操作范围内创建一个作用域,并在操作结束后释放作用域。客户端也可以直接创建:

using var bistu = new BistuClient();
await bistu.LoginWithPasswordAsync("学号", "密码");

独立使用时,可直接向构造函数传入选项:

using var bistu = new BistuClient(new BistuOptions
{
    SsoEndpoint = new Uri("https://sso.example.test/"),
});
await bistu.LoginWithPasswordAsync("学号", "密码");
var schedule = await bistu.Jwxt.GetScheduleAsync();

每个客户端按模块契约类型缓存实例;获取 bistu.Jwxt 不发送网络请求。并发首次访问只创建一个实例。模块由客户端拥有,调用方只释放顶层客户端。登录、清理和成功恢复会话会改变 SessionVersion;拥有独立令牌的模块应据此丢弃旧身份状态。应在当前请求结束后切换身份或释放客户端。

登录状态可通过 bistu.IsAuthenticated 查询。LoginWithPasswordAsync 返回 LoginResult,其中包含:

  • ExpiresAt:登录会话的过期时间;
  • Service:SSO 返回的服务地址。

JWXT 强类型 API

GetTermWeeksAsync(termCode) 返回校历中的教学周、开始和结束日期,供应用自动定位学期日期:

var weeks = await bistu.Jwxt.GetTermWeeksAsync("2026-2027-1");
var currentWeek = weeks.FirstOrDefault(week =>
    DateOnly.FromDateTime(DateTime.Today) >= week.StartDate &&
    DateOnly.FromDateTime(DateTime.Today) <= week.EndDate);

通过 bistu.Jwxt 获取当前会话的 IJwxtClient:

// 获取学期列表;IsCurrent 为 true 表示当前学期,null 表示教务系统未标注。
var terms = await bistu.Jwxt.GetTermsAsync();

// 查询指定学期的校区。
var campuses = await bistu.Jwxt.GetCampusesAsync("2026-2027-1");

// 查询当前学期成绩,或指定学期成绩。
var currentGrades = await bistu.Jwxt.GetGradesAsync();
var termGrades = await bistu.Jwxt.GetGradesAsync("2026-2027-1");

// 自动选择当前学期和第一个有课校区。
var currentSchedule = await bistu.Jwxt.GetScheduleAsync();

// 指定学期、校区和教学周查询课表。
var weekSchedule = await bistu.Jwxt.GetScheduleAsync(
    "2026-2027-1",
    "10",
    week: 1);

// 获取完整的课表模型。
var schedule = await bistu.Jwxt.GetScheduleAsync(
    "2026-2027-1",
    "10");
var mondayCourses = schedule.GetCourses(DayOfWeek.Monday);

// 考试安排:已排定与待排定记录分别保留,包含考场、座位和说明。
var exams = await bistu.Jwxt.GetExamsAsync("2026-2027-1");

// 查询某天第 3–4 节、至少 20 座的空闲教室;返回一页及总数。
var rooms = await bistu.Jwxt.GetEmptyClassroomsAsync(
    new JwxtClassroomQuery("10", new DateOnly(2026, 9, 10), 3, 4, MinimumSeats: 20)
    {
        CampusName = "沙河校区",
    });

主要返回模型如下:

  • JwxtTerm:学期代码、名称和是否为当前学期;
  • JwxtTermWeek:校历教学周、起止日期、学期代码、当前周标记和显示名称;
  • JwxtCampus:校区代码和名称;
  • JwxtGrade:课程代码、课程名称、成绩、学分和通过状态;
  • JwxtSchedule:已安排、合并、未安排和实践课程列表,并提供按星期筛选;
  • JwxtScheduleCourse:使用 DayOfWeek、TimeOnly 和已解码周次的课程信息,并提供 WeeksText 紧凑周次文本。
  • JwxtExams / JwxtExam:已排定及待排定考试,保留日期、起止时间文本、考场、座位、状态和资格/排考说明。
  • JwxtClassroomQuery / JwxtClassroomPage:按校区、日期、连续节次和座位数查询空教室,支持页码及每页 1–100 条记录。查询结果不是教室预约。

考试使用已记录的 queryMyExamArrangeMent.do 接口;空教室先访问教室借用模块入口,再请求 cxkxjs.do。无效 JSON、缺少预期分组/行数组和认证拒绝均保留为错误,不转换成「没有考试/教室」。

JwxtScheduleCourse.WeekPattern 保留教务系统返回的周次位图字符串。字符串第 n 个字符对应第 n 周,1 表示上课,0 表示不上课;解析时应保留前导零并按字符串处理,避免转换为整数。Weeks 是从 1 开始的只读周次列表,WeeksText 按升序去重并把连续周次合并为 1-3,5-6,8-10 这样的闭区间文本。没有 IsCurrent == true 的学期时,无参成绩和课表查询会抛出 JwxtApiException,请改用显式学期代码查询。

JSON 字段命名

内部 JSON 模型使用 docs/api.md 中记录的字段名。只有 CLR 属性名与 API 字段存在语义差异时才声明 JsonPropertyName;单纯的 PascalCase/camelCase 差异由 source-generated camelCase 命名策略处理。JwxtSchedule 和 JwxtScheduleCourse 是面向 .NET 的友好模型,原始字段映射在 XML 文档的 <remarks> 和 <value> 中列出。

持久登录会话

ExportSession() 导出未过期会话,TryRestoreSession(json) 在另一个客户端中恢复。导出内容包含 Cookie 和登录有效期,调用方应存入系统安全存储(例如 MAUI SecureStorage),避免日志和普通偏好设置。一次性的 COOKIE_INFO 不导出。

var session = bistu.ExportSession(); // 未登录或已过期时为 null。
// 将 session 写入系统安全存储;下次启动从相同位置读取。
var restored = session is not null && bistu.TryRestoreSession(session);

恢复只接受相同 SSO 端点的未过期会话,首次访问业务系统仍执行 CAS 认证。网络故障不代表登录失效;BistuAuthenticationException 或服务端拒绝认证时,应用应提示重新登录。退出时调用 ClearSession() 清除客户端状态,并删除安全存储中的副本。该方法只清理本地会话,不发送服务端注销请求。

原始 HTTP 请求

核心客户端与教务模块都提供原始请求接口。教务模块的原始请求同样自动完成所需的业务认证:

using var response = await bistu.Jwxt.GetAsync(
    "jwapp/sys/homeapp/api/home/kb/xnxq.do");

response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();

通过 bistu.Jwxt 发出的相对地址以 https://jwxt.bistu.edu.cn/ 为基地址,绝对地址也必须属于该来源。通过 bistu.GetAsync(...) 发出的相对地址以 SSO 地址为基地址;绝对地址按自身地址发送,且不按 URL 自动选择业务模块。

也可以使用完整的 HttpRequestMessage:

using var request = new HttpRequestMessage(
    HttpMethod.Get,
    "https://jwxt.bistu.edu.cn/jwapp/sys/homeapp/api/home/kb/xnxq.do");

using var response = await bistu.Jwxt.SendAsync(request);

教务模块在首次请求前完成 CAS 桥接,后续请求复用业务认证状态。获取模块属性不会改变通用 HTTP 请求的行为。调用教务接口前仍需先完成 SSO 登录。

业务系统认证

LoginWithPasswordAsync 负责建立 BISTU SSO 登录态。每个业务模块负责自己的会话桥接,CAS 模块可使用 BistuCasService 和 SendToServiceAsync:

var service = new BistuCasService(
    new Uri("https://example.bistu.edu.cn/"),
    new Uri("https://example.bistu.edu.cn/cas/callback"));
using var request = new HttpRequestMessage(HttpMethod.Get, "api/data");
using var response = await bistu.SendToServiceAsync(service, request);

服务的认证状态按来源和回调地址共享,相同主机上的不同回调各自维护状态;失败或取消后允许重新认证。需要直接访问某个认证入口时,可以调用:

await bistu.AuthenticateServiceAsync(
    new Uri("https://jwxt.bistu.edu.cn/jwapp/sys/yjsrzfwapp/bistuLogin/casLogin.do"));

通用 HTTP 请求如果重定向到 BISTU SSO,会抛出 ServiceAuthenticationRequiredException,提示使用相应模块或显式认证上下文。异常中的 RequestUri 和 ServiceUri 提供原始请求和所需服务地址。

添加业务系统扩展

扩展包定义自己的契约、实现和默认工厂,通过扩展属性接入。下面的 IExampleClient 与 ExampleClient 由该扩展包定义,无需继承公共 endpoint 接口:

public static class ExampleExtensions
{
    private static readonly BistuModule<IExampleClient> Definition =
        new(static client => new ExampleClient(client));

    extension(IBistuClient client)
    {
        public IExampleClient Example => client.GetOrCreateModule(Definition);
    }
}

工厂只构造本地对象;网络认证由业务方法按需执行。模块可使用通用传输、显式认证入口或 CAS 便捷能力,自行管理协议、令牌、请求头和模型。模块创建失败后可重试,循环模块依赖会报告错误。

需要替换默认实现或提供额外依赖时,配置 BistuOptions.ConfigureModule<TModule>。同一契约最后一次配置生效,创建客户端之后的配置变更不影响已有客户端。例如,以下类型由扩展包或应用提供:

services.AddBistu((provider, options) =>
    options.ConfigureModule<IExampleClient>(client =>
        new ExampleClient(client, provider.GetRequiredService<ExampleDependency>())));

返回的模块由客户端拥有。模块可以借用作用域依赖,需要清理自身资源时实现幂等的 IDisposable;借用依赖由原所有者释放。业务代码若希望直接注入教务契约,可增加作用域映射:

services.AddScoped<IJwxtClient>(provider =>
    provider.GetRequiredService<IBistuClient>().Jwxt);

异常处理

  • BistuAuthenticationException:SSO 登录失败、登录响应无效或认证过程异常。
  • ServiceAuthenticationRequiredException:通用请求需要显式的业务认证上下文。
  • JwxtApiException:JWXT 返回业务错误、空响应或无效数据。
  • HttpRequestException:HTTP 请求失败或重定向次数超过上限。

业务代码可以结合 IsAuthenticated、HTTP 状态码和上述异常类型记录登录状态及接口错误。

构建和测试

在仓库根目录执行:

dotnet build ProjektMing.Bistu.slnx
dotnet test --solution ProjektMing.Bistu.slnx

测试项目使用本地 HTTP 处理器模拟 SSO 和 JWXT 响应,运行测试时使用测试夹具,不调用真实账号登录。

API 参考

更完整的 SSO、CAS、教务系统接口路径、请求参数和响应结构见 docs/api.md。文档中的账号、票据和其他敏感值均使用占位符。

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages