7.7 KiB
7.7 KiB
oapi 课表查询接口 · 调用手册
服务:AcaSyllabu(课程表) 接口:
GET /oapi/timetable/Get更新:2026-10-13
按「用户 + 角色 + 学校」查询其可见范围内所有班级的当前学年学期课程表。身份由调用方显式传入(不依赖浏览器登录态),适用于平台内其他应用(首页卡片、移动端、大屏等)服务端直调。
| 调用方身份 | 可见班级 | 可见科目 |
|---|---|---|
| 管理员(roleId=7)/ 校领导(roleId=2) | 全部班级 | 全部科目 |
| 班主任 | 所带班级(正/副班主任) | 全部科目 |
| 任教老师(roleId=1) | 任教班级 | 仅任教科目(其余科目格清空返回) |
| 学生(roleId=3) | 本人所在班级 | 全部科目 |
roleId不传时**默认按普通教师(1)**处理。- 一个用户兼任多重身份(如既是班主任又任教其他班)时,取并集:班主任班给全部科目,任教班只给任教科目。
- 无任何可见范围(如普通教师没有任教记录)时返回空数组
data: [],不报错。
1. 请求
GET {base}/oapi/timetable/Get?userid={userId}&roleId={roleId}&schId={schId}
- 本地联调:
http://localhost:8109(无 context-path) - 生产:
http://{host}/acasyllabu(context-path/acasyllabu,以网关实际地址为准)
| 参数 | 必填 | 说明 |
|---|---|---|
userid |
是 | 平台用户ID。教师=教师ID;学生=学生树里的学生节点ID |
roleId |
否 | 角色:1 教师(默认)、2 校领导、3 学生、7 管理员 |
schId |
是 | 学校ID(数据严格限定在该校范围内) |
2. 签名鉴权(必须)
接口在 /oapi/* 强制验签下:浏览器/匿名直访一律 401,只有注册应用带上签名三件套才能通过。
2.1 请求头
| Header | 说明 |
|---|---|
X-App-Key |
调用方应用ID(在服务方 trusted-apps 白名单登记的 appKey) |
X-Timestamp |
当前毫秒时间戳。有效期默认 5 分钟,过期拒绝,请用当前时间 |
X-Signature |
签名,算法见下 |
2.2 签名算法
待签串 = 请求方法 + "\n" + target + "\n" + X-Timestamp
target = 请求路径 + "?" + 查询参数(按参数名字典序排列后的 k=v&k=v)
签名 = Base64( HmacSHA256( 待签串, appSecret ) )
要点:
- 方法用大写
GET;分隔符是换行\n,共两处; - query 参数按参数名排序后再拼(如
roleId在schId在userid前),排序后原样拼接、不做 URL 编码; - 密钥即服务方
trusted-apps里登记的该 appKey 对应 secret(找 AcaSyllabu 管理员索取)。
2.3 接入新调用方
服务方在 application.yml(生产为 Nacos acasyllabu.yaml)的 yh.app-auth.trusted-apps 下追加一行:
yh:
app-auth:
trusted-apps:
"100021": "d3ee380e..." # 既有
"100xxx": "新调用方密钥" # 新增
生产环境接了中央密钥链(Redis 注册表)后可改走
trust-registry,届时以平台注册为准,无需手工维护白名单。
3. 响应
外层为平台统一包装:
{ "code": 0, "msg": "", "data": [ …班级数组… ] }
data 数组每项:
| 字段 | 说明 |
|---|---|
classId / className / gradeName |
班级ID / 班级名 / 年级名 |
headTeacher |
该用户在本班是否班主任 |
viewAllSubjects |
是否全部科目可见(班主任/管理员/校领导/学生=true,仅任教=false) |
subjects |
任教科目列表(仅 viewAllSubjects=false 时非空) |
timetable |
该班课表,结构同 TimetableVo(见下) |
timetable:
| 字段 | 说明 |
|---|---|
year / term |
课表所属学年学期(term:1 上学期 2 下学期) |
photoFileId / photoName / photoSize |
照片课表附件(未上传为 null) |
slots[] |
节次数组;slotNo 从 1 起,start/end 为 HH:mm |
slots[].subjects |
固定 7 位,下标 0 |
示例(节选):
{
"code": 0, "msg": "",
"data": [
{
"classId": 371, "className": "2班", "gradeName": "高一",
"headTeacher": false, "viewAllSubjects": false,
"subjects": ["语文"],
"timetable": {
"classId": 371, "year": 2026, "term": 1,
"className": "2班", "gradeName": "高一",
"photoFileId": null, "photoName": null, "photoSize": null,
"slots": [
{ "slotNo": 1, "start": "08:20", "end": "09:00",
"subjects": ["语文", "", "", "", "", "", ""] }
],
"viewAllSubjects": false, "mySubjects": ["语文"]
}
}
]
}
4. 错误情况
| 现象 | 原因 |
|---|---|
HTTP 401 missing app signature |
未带签名三件套 |
HTTP 401 invalid signature or expired |
密钥错误、待签串拼错或时间戳超过 5 分钟 |
HTTP 401 unknown appKey |
appKey 未在服务方 trusted-apps 登记 |
HTTP 200 {"code":6,"msg":"内部服务错误"} |
缺 userid/schId 等必填参数 |
HTTP 200 code=0, data=[] |
该身份在该校无可见班级(如无任教记录的教师) |
5. 调用示例
5.1 Node.js(可直接运行)
const crypto = require('crypto');
const BASE = 'http://localhost:8109';
const APP_KEY = '100xxx';
const APP_SECRET = '你的密钥';
function get(pathAndQuery) {
const q = pathAndQuery.indexOf('?');
const path = q < 0 ? pathAndQuery : pathAndQuery.substring(0, q);
const pairs = q < 0 ? [] : pathAndQuery.substring(q + 1).split('&').sort();
const target = path + (pairs.length ? '?' + pairs.join('&') : '');
const ts = String(Date.now());
const sign = crypto.createHmac('sha256', APP_SECRET)
.update(`GET\n${target}\n${ts}`)
.digest('base64');
return fetch(BASE + pathAndQuery, {
headers: { 'X-App-Key': APP_KEY, 'X-Timestamp': ts, 'X-Signature': sign },
}).then(r => r.json());
}
// 管理员查全校
get('/oapi/timetable/Get?userid=1&roleId=7&schId=3').then(console.log);
5.2 Java(平台内服务,有 yh-utils-core 依赖)
String pathAndQuery = "/oapi/timetable/Get?userid=1&roleId=7&schId=3";
// query 按参数名排序后拼回
String target = "/oapi/timetable/Get?roleId=7&schId=3&userid=1";
String ts = String.valueOf(System.currentTimeMillis());
String sign = SignUtil.signRequest("GET", target, ts, appSecret); // 平台工具,同款算法
HttpHeaders headers = new HttpHeaders();
headers.set("X-App-Key", appKey);
headers.set("X-Timestamp", ts);
headers.set("X-Signature", sign);
restTemplate.exchange(baseUrl + pathAndQuery, HttpMethod.GET, new HttpEntity<>(headers), String.class);
平台 Feign 客户端加
SignFeignInterceptor可自动签名,无需手工拼。
5.3 curl(Git Bash,签名用 openssl 生成)
SECRET="你的密钥"
TARGET="/oapi/timetable/Get?roleId=7&schId=3&userid=1" # 参数已按名排序
TS=$(date +%s000)
SIGN=$(printf "GET\n%b\n%b" "$TARGET" "$TS" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64)
curl -s "http://localhost:8109$TARGET" \
-H "X-App-Key: 100xxx" -H "X-Timestamp: $TS" -H "X-Signature: $SIGN"
注意 curl 的 URL 必须与参与签名的 target 完全一致(含参数顺序)。
6. 本地联调
- 服务方后端:8109 端口(devtools 热重启);本地验签白名单只收了本应用
100021(application.ymltrusted-apps)。 - 现成冒烟脚本:
E:\YHXT\java\gui-test-screenshots\oapi-sign-smoke.js(用法node oapi-sign-smoke.js "/oapi/timetable/Get?userid=1&roleId=7&schId=3",密钥内置 100021)。 - 真实数据参考(schId=3):管理员 userid=1 返回全部班级;教师 10008 = 班主任(1班 全部科目) + 任教(2班 仅语文);学生 10066 = 仅 1 班。