Repository navigation
Activity
| 项 | 值 |
|---|---|
| 平台 | activity.sjtu.edu.cn |
| 认证 | 站点会话令牌(Authorization: Bearer) |
| 数据方向 | 只读为主;报名是写操作 |
| 难度 | 中 |
| 最后验证 | 2026-10-26 |
| 证据等级 | B(私有接口 + 关键端点实测,无官方文档) |
第二课堂(校园活动平台)是校内活动的发布、检索和报名站点。它没有公开开发者文档,接口来自前端打包结果和社区实现;本页只覆盖本人账号可读的部分。
- 可获取:个人资料、活动类型、活动列表、活动详情、报名表结构。
- 前置:活动平台自己的会话令牌。jAccount 的
JAAuthCookie不能直接当接口凭证。[实测 2026-10-25] - 注意:报名、取消报名都会改个人记录,本页不提供自动化方案;活动详情页 URL 里的 id 是 base64,不是数字。
- 判登录态看响应体:未登录时 HTTP 仍是 200,
code为 401。[实测 2026-10-26] - 详情是嵌套结构:活动字段在
data.base_info,报名配置在data.sign_up_info,不是扁平对象。[实测 2026-10-26]
| 对象 | 内容 | 备注 |
|---|---|---|
| 个人资料 |
id、name、grade、major_name、major_id、train_level/trainLevel、top_organize_id/topOrganizeId、top_organize_name、privileges、roleName、userType、locale
|
命名风格混杂:top_organize_* 与 train_level 有两种写法,major_name、roleName、userType 只有一种 |
| 活动类型 | 65 条,含 id、label、path、calendar_name、banner_pc、banner_mobile、visible_orgs、all_user_types、show_calendar
|
比「类型字典」复杂得多 |
| 轮播 |
banner/list 返回条目数组 |
实测 activity_type_id=1 时是空数组 |
| 活动列表 |
id、name、sponsor、presenter、public、method、person_num、signed_up_num、activity_time、registration_time、address、img
|
总数在外层 total
|
| 热门列表 |
activity_id(不是 id)、index、activity_type_id,其余同上 |
主键名与活动列表不同 |
| 活动详情 |
base_info(含 contact_person_*、custom_properties、content_design、grading、status、published、duration)+ sign_up_info
|
两层结构,见第 4 节 |
| 报名配置 |
method、person_num、need_captcha、cancel_sign_up、auto_selected、not_enroll_sign_in、more_sign_in、sign_up_url、form_design[]
|
form_design 可以是空数组 |
activity_time 和 registration_time 是两元素数组,元素是 "YYYY-MM-DD HH:mm:ss" 字符串;registration_time 实测出现过 [null, null] 和 [] 两种空形态。[实测 2026-10-26]
| 部分 | Base URL | 鉴权 | 用途 |
|---|---|---|---|
| 网页前端 | https://activity.sjtu.edu.cn/ |
jAccount SSO | 登录、浏览、报名 |
| 业务 API | https://activity.sjtu.edu.cn/api/v1 |
站点令牌 | 本页主体 |
| 登录换令牌 | https://activity.sjtu.edu.cn/api/v1/login/token |
一次性 code
|
社区脚本使用的授权码换令牌 |
| 活动详情页 | /activity/detail/{base64} |
网页会话 | 只适合人工打开 |
社区实现用 jAccount OAuth 授权码换取站点令牌:client_id 为 NMCTdJI6Tluw2SSTe6tW,redirect_uri 为 https://activity.sjtu.edu.cn/auth,scope 为 profile,再用 GET /api/v1/login/token?code=... 换回 data 字段里的令牌。[代码]
个人自用更简单:让用户在受控窗口完成一次网页登录,直接读本人已登录会话里的令牌。本轮就是这条路径。[实测 2026-10-25]
活动平台前端用 IMPSA-Token 作为站点令牌的存储键(前端打包产物里可检索到),把它作为 Bearer 令牌放进 Authorization 头即可访问业务 API。[代码]、[实测 2026-10-26]
单独复用 jAccount 的登录 Cookie 会被重定向回 jAccount 登录页,拿不到 JSON。它的作用只是维持统一登录会话。[实测 2026-10-25]
GET https://activity.sjtu.edu.cn/api/v1/profile
Authorization: Bearer <token>未带令牌请求时,接口仍返回 HTTP 200,响应体是:
{"code":401,"data":"","message":"未登录","message_key":"auth.unauthenticated"}所以登录态只能看 body 的 code,不能看 HTTP 状态;错误时 data 是空字符串而不是 null。未登录访问网页路由(如 /activity/detail/...)也只返回 200 的 SPA 外壳,不会 302 跳转。[实测 2026-10-26,未带令牌]
GET /api/v1/login/token 不带 code 时返回 500 和 HTML 错误页,说明它不是可探测的公共端点;授权码换令牌的流程本轮没有复现。[实测 2026-10-26]
Base:https://activity.sjtu.edu.cn/api/v1
| 接口 | 关键参数 | 返回 | 状态 |
|---|---|---|---|
GET /profile |
- | 本人资料、组织、专业、年级、权限列表 | 已实测 |
GET /system/activity_type |
isAll=true |
65 条活动类型 | 已实测 |
GET /banner/list |
activity_type_id |
轮播条目;实测为空数组 | 已实测 |
GET /activity/list/home |
page、per_page、activity_type_id、time_sort、can_apply_with_quota
|
活动列表 + 外层 total
|
已实测 |
GET /hot/list |
activity_type_id、fill=1
|
热门活动,键为 activity_id
|
已实测 |
GET /activity/{id} |
路径参数 | 详情,base_info + sign_up_info
|
已实测 |
POST /signUp |
body:id、college、form_value
|
code、data、message、message_key
|
已实测(只测了错误路径) |
网页自己请求列表时用 per_page=96&time_sort=desc&can_apply_with_quota=false。这个参数的作用已确认:置 true 时只返回当前可报名且名额未满的活动,activity_type_id=1 下从 2266 条收缩到 34 条。[实测 2026-10-26]
列表:
{"code": 200, "data": [ ... ], "message": "", "message_key": "", "total": 42}- 总数是
data的兄弟字段total,不是分页对象;page、per_page照旧生效。[实测 2026-10-26] - 列表项没有
in_signed_up、sign_up_info,报名状态只在详情里。[实测 2026-10-26]
详情:
{
"code": 200,
"data": {
"base_info": { "id": 19022, "name": "...", "sponsor": "...", "activity_time": ["...", "..."] },
"sign_up_info": { "method": 7, "person_num": 30, "need_captcha": false, "form_design": [] },
"in_signed_up": false,
"in_calendar": false,
"in_collection": false,
"has_subscribed": false,
"show_subscribe": true,
"signed_up_num": 0,
"display_url": null
}
}报名方式 method:社区实现只列了 1–6(线上审核、线下、先到先得、无需报名、随机录取、跳转其他报名),实测出现了 7,语义未知。[实测 2026-10-26]
写操作的失败也用同一层封装,但 HTTP 仍是 200、code 是业务码:空 body 提交报名返回 {"code":404,"data":false,"message":"报名的活动不存在","message_key":"signup.activity_not_found"}。未登录时 code 为 401、message_key 为 auth.unauthenticated。[实测 2026-10-26]
-
详情不扁平:活动本体在
base_info,报名配置在sign_up_info;把顶层当活动对象会拿不到name、method。[实测 2026-10-26] -
字段双份:
top_organize_id与topOrganizeId、train_level与trainLevel同时存在,任选其一但不要假设只有一种。[实测 2026-10-26] -
method有未记录取值:200 个活动样本的分布是 1×25、2×13、3×56、4×2、6×4、7×100,没有出现 5。7 占一半,但类型字典、前端产物和两个社区实现里都没有它的定义,只能按「未知报名方式」展示。[实测 2026-10-26] -
空时间形态不一致:
registration_time可能是[null, null]或[],sign_up_info.registration_time又是空数组,解析要都能吃下。[实测 2026-10-26] -
form_design可能为空:method: 7的样本form_design: [],报名信息在sign_up_url、need_captcha等字段上;method: 3的样本有 2 个必填字段。空数组不等于无需报名。[实测 2026-10-26] -
表单字段结构:实测样本里出现
ElInput(手机号,必填)和CheckboxGroup(活动日期,带 2 个选项、必填);Selector一类带dict选项表,完整 tag 列表来自社区实现。[实测 2026-10-26]、[代码] -
详情含联系人信息:
contact_person_name、contact_person_mobile、contact_person_email是主办方联系人,不是本人数据;不采集、不展示、不写日志。[实测 2026-10-26] -
报名人数:
signed_up_num/person_num只表示名额占用;线上审核与先到先得的判定都在服务端。 -
详情页 id:网页地址
/activity/detail/{base64}是活动 id 补空格后的 base64,接口里用数字 id。[代码]
-
只带 Cookie 拿到登录页 HTML:jAccount Cookie 不是活动平台接口凭证,改用站点令牌。
[实测 2026-10-25] -
未登录时 HTTP 还是 200:
curl和健康检查看到 200 不代表有权限;按 body 的code、message_key判断,不要按 HTTP 状态或空数组判断。[实测 2026-10-26] -
活动列表为空:先探活
/profile,再确认activity_type_id是否正确。 - id 对不上:网页地址里是 base64,接口里是数字 id,两者需要转换。
-
报名失败了不知道原因:
message是服务端原文,可能要求补表单字段;不要把失败当成功重试。 -
同一活动重复报名:详情里的
in_signed_up为真时社区实现直接跳过。 - 频率:未观察到明确限流;活动列表按小时级刷新足够,不要轮询。
-
详情里取不到
name/method:它们在data.base_info和data.sign_up_info里。[实测 2026-10-26] -
method是 7:不在社区枚举里,按未知处理,别映射成「无需报名」。[实测 2026-10-26] -
列表拿不到报名状态:
in_signed_up只在详情,列表项没有这个字段。[实测 2026-10-26] -
热门列表
id是 undefined:热门条目的主键叫activity_id。[实测 2026-10-26]
const BASE = 'https://activity.sjtu.edu.cn/api/v1';
async function listActivities(token: string, page = 1) {
const query = new URLSearchParams({
page: String(page),
per_page: '20',
activity_type_id: '1',
time_sort: 'desc',
});
const response = await fetch(`${BASE}/activity/list/home?${query}`, {
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
redirect: 'manual',
});
if (response.status >= 300 && response.status < 400) {
throw new Error('站点令牌已失效,请重新登录第二课堂');
}
const payload = await response.json().catch(() => null);
if (!payload) throw new Error('接口返回非 JSON,可能被重定向到登录页');
if (payload.code === 401) throw new Error('站点令牌已失效,请重新登录第二课堂');
if (!Array.isArray(payload.data)) throw new Error('活动列表结构已变化');
return payload.data.map((item: any) => ({
id: item.id,
name: item.name,
sponsor: item.sponsor,
method: item.method,
start: item.activity_time?.[0] ?? null,
end: item.activity_time?.[1] ?? null,
address: item.address,
}));
}令牌只放在主进程或本地密钥存储里,不要进 renderer、日志和分享配置。
- 不要自动报名、自动抢名额或批量提交报名表;这些都会改变个人记录和名额。
- 不要为他人代填报名表,也不要收集报名表里的身份证、手机等字段。
- 不要把网页内部接口当稳定契约对外提供公共服务。
- 不要绕过登录、伪造令牌或压测活动列表。
- 站点令牌可读、
JAAuthCookie不能直接当凭证、/profile与活动列表返回 200:[实测 2026-10-25,本人账号,只读] - 端点、参数、
method枚举、表单tag、详情页 base64、OAuth 换令牌:[代码]sjtu-mcp-contrib/sjtu_activity.py - 前端产物含
IMPSA-Token存储键,未登录访问网页路由返回 200 SPA 外壳、无跳转:[实测 2026-10-26] - 带令牌实测:
/profile、/system/activity_type(65 条)、/banner/list(空)、/hot/list(9 条)、/activity/list/home(total在外层)、/activity/19022(base_info+sign_up_info)均 200:[实测 2026-10-26,本人账号,只读];记录未入库。 - 未带令牌时已试端点返回 HTTP 200 +
code: 401、login/token报 500、网页可正常打开:[实测 2026-10-26,未登录,只读探测] - 服务入口与报名说明:
[官方]第二课堂平台、校内活动通知
- 2026-10-25:首版。记录站点令牌链路、只读端点与报名表结构,未覆盖报名写操作。
- 2026-10-26:按匿名探测修正登录态判断方式(HTTP 200 +
code: 401),补message_key字段与login/token的实际行为。 - 2026-10-26:带令牌复测六个端点,证据等级 C → B;更正详情嵌套结构、
total字段、method: 7、热门列表activity_id。 - 2026-10-26:补
IMPSA-Token来源、网页路由不跳转、method在 200 个样本上的分布。
本 Wiki 记录的是开发实践,不是学校官方承诺。每个结论都标注证据与日期;校园平台会调整字段、权限和登录方式,动手前请按页面里的「最后验证」复核。发现过期内容请提 Issue 或直接改。