面向北京信息科技大学(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/"));运行 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 返回的服务地址。
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 模型使用 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() 清除客户端状态,并删除安全存储中的副本。该方法只清理本地会话,不发送服务端注销请求。
核心客户端与教务模块都提供原始请求接口。教务模块的原始请求同样自动完成所需的业务认证:
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 响应,运行测试时使用测试夹具,不调用真实账号登录。
更完整的 SSO、CAS、教务系统接口路径、请求参数和响应结构见 docs/api.md。文档中的账号、票据和其他敏感值均使用占位符。