# 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` 下追加一行: ```yaml yh: app-auth: trusted-apps: "100021": "d3ee380e..." # 既有 "100xxx": "新调用方密钥" # 新增 ``` > 生产环境接了中央密钥链(Redis 注册表)后可改走 `trust-registry`,届时以平台注册为准,无需手工维护白名单。 ## 3. 响应 外层为平台统一包装: ```json { "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~6 = 周一~周日,空串=未排课 | 示例(节选): ```json { "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(可直接运行) ```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 依赖) ```java 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 生成) ```bash 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.yml `trusted-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 班。