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

7.7 KiB
Raw Permalink Blame History

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 位,下标 06 = 周一周日,空串=未排课

示例(节选):

{
  "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.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 班。