Skip to content

Activity

ChangYo edited this page Oct 26, 2026 · 1 revision

第二课堂

项 值
平台 activity.sjtu.edu.cn
认证 站点会话令牌(Authorization: Bearer)
数据方向 只读为主;报名是写操作
难度 中
最后验证 2026-10-26
证据等级 B(私有接口 + 关键端点实测,无官方文档)

第二课堂(校园活动平台)是校内活动的发布、检索和报名站点。它没有公开开发者文档,接口来自前端打包结果和社区实现;本页只覆盖本人账号可读的部分。

相关:jAccount 与校园会话、安全、隐私与合规。

要点

  • 可获取:个人资料、活动类型、活动列表、活动详情、报名表结构。
  • 前置:活动平台自己的会话令牌。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]


1. 平台架构

部分 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]


2. 认证

站点令牌

活动平台前端用 IMPSA-Token 作为站点令牌的存储键(前端打包产物里可检索到),把它作为 Bearer 令牌放进 Authorization 头即可访问业务 API。[代码]、[实测 2026-10-26]

JAAuthCookie 不是接口凭证

单独复用 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]


3. 端点表

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]


4. 响应结构与分页

列表:

{"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]


5. 数据质量

  • 详情不扁平:活动本体在 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。[代码]

6. 常见问题

  1. 只带 Cookie 拿到登录页 HTML:jAccount Cookie 不是活动平台接口凭证,改用站点令牌。[实测 2026-10-25]
  2. 未登录时 HTTP 还是 200:curl 和健康检查看到 200 不代表有权限;按 body 的 code、message_key 判断,不要按 HTTP 状态或空数组判断。[实测 2026-10-26]
  3. 活动列表为空:先探活 /profile,再确认 activity_type_id 是否正确。
  4. id 对不上:网页地址里是 base64,接口里是数字 id,两者需要转换。
  5. 报名失败了不知道原因:message 是服务端原文,可能要求补表单字段;不要把失败当成功重试。
  6. 同一活动重复报名:详情里的 in_signed_up 为真时社区实现直接跳过。
  7. 频率:未观察到明确限流;活动列表按小时级刷新足够,不要轮询。
  8. 详情里取不到 name / method:它们在 data.base_info 和 data.sign_up_info 里。[实测 2026-10-26]
  9. method 是 7:不在社区枚举里,按未知处理,别映射成「无需报名」。[实测 2026-10-26]
  10. 列表拿不到报名状态:in_signed_up 只在详情,列表项没有这个字段。[实测 2026-10-26]
  11. 热门列表 id 是 undefined:热门条目的主键叫 activity_id。[实测 2026-10-26]

7. 最小示例

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、日志和分享配置。


8. 不要做

  • 不要自动报名、自动抢名额或批量提交报名表;这些都会改变个人记录和名额。
  • 不要为他人代填报名表,也不要收集报名表里的身份证、手机等字段。
  • 不要把网页内部接口当稳定契约对外提供公共服务。
  • 不要绕过登录、伪造令牌或压测活动列表。

9. 证据与来源

  • 站点令牌可读、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 个样本上的分布。

Clone this wiki locally