Files
aca-syllabu-service/oapi课表调用手册.md
T
2026-10-14 16:15:16 +08:00

202 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 班。