feat(课表小组件): syllabus对接AcaSyllabu GET /oapi/timetable/Get——YschOpenCall出站零手写签名,真实ctx=/aca-syllabu(20261013生产401定位,手册/acasyllabu有误);90s TTL缓存+15s失败退避+上限500,不可达降级空数组不500;身份三参全会话推导前端不可传;widget_catalog种子名改「课程表」
build / build-push (push) Successful in 2m7s
build / build-push (push) Successful in 2m7s
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
package com.yunhai.userhome.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.yunhai.platform.openapi.core.YschOpenCall;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
@@ -11,15 +14,21 @@ import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalAdjusters;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 小组件统一取数(方案 §3.4 已拍板:先按 L2 可溯源 mock)。
|
||||
* 小组件统一取数。
|
||||
*
|
||||
* <p>字段名逐一等于 design/widget-snapshots-old-bsch/*.js 的旧接口口径,接入真实数据源前不外扩;
|
||||
* 未知 code 返回 null 由调用方兜空数据,不抛异常。</p>
|
||||
* <p>{@code syllabus}(我的课表)已对接 AcaSyllabu {@code GET /oapi/timetable/Get}(出站走 {@link YschOpenCall},
|
||||
* 带 yh-app-auth 签名;目标 {@code acasyllabu} 配于 {@code yh.call.targets}),**原样透传** {@code data[]}
|
||||
* 班级数组(含 {@code timetable.slots[].subjects[7]}),由前端按班级维度渲染。</p>
|
||||
*
|
||||
* <p>{@code evaluate} / {@code weekplan} 仍为 L2 可溯源 mock(字段名逐一等于
|
||||
* {@code design/widget-snapshots-old-bsch/*.js} 的旧接口口径);未知 code 返回 null 由调用方兜空数据,不抛异常。</p>
|
||||
*/
|
||||
@Service
|
||||
public class WidgetDataService {
|
||||
@@ -37,15 +46,36 @@ public class WidgetDataService {
|
||||
/** 听评课旧接口 pageSize 默认值(evaluate_index.js 传 count=5) */
|
||||
private static final int EVALUATE_DEFAULT_COUNT = 5;
|
||||
|
||||
/** 课表 oapi 目标名(见 yh.call.targets.acasyllabu) */
|
||||
private static final String TARGET_ACASYLLABU = "acasyllabu";
|
||||
|
||||
/** 课表 oapi 路径(签名按目标 ctx 剥离,故此处不含 /acasyllabu) */
|
||||
private static final String PATH_TIMETABLE = "/oapi/timetable/Get";
|
||||
|
||||
/** 课表缓存时长:课表低频变更,避免每次进桌面都打出站 */
|
||||
private static final long SYLLABUS_TTL_MS = 90_000L;
|
||||
|
||||
/** 课表取数失败后的静默期(短 TTL 空结果,避免下游停摆时持续打出站) */
|
||||
private static final long SYLLABUS_FAIL_BACKOFF_MS = 15_000L;
|
||||
|
||||
/** 课表缓存条目上限,超出时清理过期项 */
|
||||
private static final int SYLLABUS_CACHE_MAX = 500;
|
||||
|
||||
@Autowired
|
||||
private YschOpenCall openCall;
|
||||
|
||||
/** key = userId|roleId|schId → 缓存条目 */
|
||||
private final ConcurrentHashMap<String, CacheEntry> syllabusCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 按数据键取数;params 为 urlencoded 查询串(对齐旧 otherParams 口径)。
|
||||
*
|
||||
* @return 数据结构由数据键决定;null = 该键未接入
|
||||
*/
|
||||
public Object getData(String code, String params, Integer schId) {
|
||||
public Object getData(String code, String params, Caller caller) {
|
||||
if (CODE_EVALUATE.equals(code)) return evaluate(params);
|
||||
if (CODE_SYLLABUS.equals(code)) return syllabus();
|
||||
if (CODE_WEEKPLAN.equals(code)) return weekplan(params, schId);
|
||||
if (CODE_SYLLABUS.equals(code)) return syllabus(caller);
|
||||
if (CODE_WEEKPLAN.equals(code)) return weekplan(params, caller == null ? null : caller.getSchId());
|
||||
log.warn("[widget] 未配置的数据键:{}", code);
|
||||
return null;
|
||||
}
|
||||
@@ -76,29 +106,41 @@ public class WidgetDataService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的课表:数组,一格一行(节次 CourseTimes × 星期 Weeks)。
|
||||
* // TODO 接入 oapi: openCall.getList(TARGET_USERBASE, "/oapi/...", null, Vo.class)
|
||||
* 我的课表:AcaSyllabu {@code GET /oapi/timetable/Get?userid=&roleId=&schId=}(SDK 出站,签名自动)。
|
||||
* 原样返回 {@code data[]} 班级数组;无可见范围返回 {@code []};不可达 / 验签失败降级空数组(不抛 500)。
|
||||
*
|
||||
* <p>身份三参均来自服务端会话推导({@link Caller}),不接受前端传入;roleId<=0 时不透传
|
||||
* (oapi 侧默认按普通教师处理)。</p>
|
||||
*/
|
||||
private List<Map<String, Object>> syllabus() {
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
addCourse(rows, 1, "第1节", "数学", 1, "周一", "高一(3)班");
|
||||
addCourse(rows, 1, "第1节", "数学", 2, "周二", "高一(4)班");
|
||||
addCourse(rows, 1, "第1节", "数学", 4, "周四", "高一(3)班");
|
||||
addCourse(rows, 2, "第2节", "英语", 2, "周二", "高一(5)班");
|
||||
addCourse(rows, 2, "第2节", "英语", 3, "周三", "高一(4)班");
|
||||
addCourse(rows, 2, "第2节", "英语", 5, "周五", "高一(5)班");
|
||||
addCourse(rows, 3, "第3节", "物理", 1, "周一", "高一(6)班");
|
||||
addCourse(rows, 3, "第3节", "物理", 3, "周三", "高一(6)班");
|
||||
addCourse(rows, 3, "第3节", "物理", 4, "周四", "高一(4)班");
|
||||
addCourse(rows, 4, "第4节", "体育", 3, "周三", "高一(3)班");
|
||||
addCourse(rows, 4, "第4节", "体育", 5, "周五", "高一(6)班");
|
||||
return rows;
|
||||
}
|
||||
|
||||
private void addCourse(List<Map<String, Object>> rows, int courseTimes, String courseName, String subName,
|
||||
int weeks, String weekName, String className) {
|
||||
rows.add(row("CourseName", courseName, "CourseTimes", courseTimes, "Weeks", weeks,
|
||||
"weekName", weekName, "SubName", subName, "ClassName", className));
|
||||
private Object syllabus(Caller caller) {
|
||||
if (caller == null || caller.getUserId() == null || caller.getSchId() == null) {
|
||||
// 缺身份(超管无当前身份等)→ 空数据,不打出站
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String key = caller.getUserId() + "|" + caller.getRoleId() + "|" + caller.getSchId();
|
||||
long now = System.currentTimeMillis();
|
||||
CacheEntry hit = syllabusCache.get(key);
|
||||
if (hit != null && now < hit.expireAt) {
|
||||
return hit.data;
|
||||
}
|
||||
try {
|
||||
Map<String, String> query = new LinkedHashMap<>();
|
||||
query.put("userid", String.valueOf(caller.getUserId()));
|
||||
if (caller.getRoleId() != null && caller.getRoleId() > 0) {
|
||||
query.put("roleId", String.valueOf(caller.getRoleId()));
|
||||
}
|
||||
query.put("schId", String.valueOf(caller.getSchId()));
|
||||
JsonNode data = openCall.get(TARGET_ACASYLLABU, PATH_TIMETABLE, query, JsonNode.class);
|
||||
Object value = (data != null && data.isArray()) ? data : Collections.emptyList();
|
||||
cache(key, value, now + SYLLABUS_TTL_MS, now);
|
||||
return value;
|
||||
} catch (Exception e) {
|
||||
log.warn("[widget] 课表 oapi 不可达(降级空)user={} role={} sch={}:{}",
|
||||
caller.getUserId(), caller.getRoleId(), caller.getSchId(), e.toString());
|
||||
// 失败退避:短 TTL 空结果,避免下游停摆时每次进桌面都吃一次超时
|
||||
cache(key, Collections.emptyList(), now + SYLLABUS_FAIL_BACKOFF_MS, now);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,6 +170,18 @@ public class WidgetDataService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 写缓存并做上限清理(只清过期项,避免无界增长) */
|
||||
private void cache(String key, Object value, long expireAt, long now) {
|
||||
if (syllabusCache.size() >= SYLLABUS_CACHE_MAX) {
|
||||
for (Map.Entry<String, CacheEntry> entry : syllabusCache.entrySet()) {
|
||||
if (now >= entry.getValue().expireAt) {
|
||||
syllabusCache.remove(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
syllabusCache.put(key, new CacheEntry(value, expireAt));
|
||||
}
|
||||
|
||||
private int intParam(String params, String key, int defaultValue) {
|
||||
if (params == null || params.isEmpty()) return defaultValue;
|
||||
for (String pair : params.split("&")) {
|
||||
@@ -149,4 +203,42 @@ public class WidgetDataService {
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/** 取数调用方上下文:三参均会话推导,不接受前端传入 */
|
||||
public static final class Caller {
|
||||
|
||||
private final Integer userId;
|
||||
private final Integer roleId;
|
||||
private final Integer schId;
|
||||
|
||||
public Caller(Integer userId, Integer roleId, Integer schId) {
|
||||
this.userId = userId;
|
||||
this.roleId = roleId;
|
||||
this.schId = schId;
|
||||
}
|
||||
|
||||
public Integer getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public Integer getRoleId() {
|
||||
return roleId;
|
||||
}
|
||||
|
||||
public Integer getSchId() {
|
||||
return schId;
|
||||
}
|
||||
}
|
||||
|
||||
/** 课表缓存条目 */
|
||||
private static final class CacheEntry {
|
||||
|
||||
private final Object data;
|
||||
private final long expireAt;
|
||||
|
||||
private CacheEntry(Object data, long expireAt) {
|
||||
this.data = data;
|
||||
this.expireAt = expireAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,16 @@ yh:
|
||||
# 身份密钥唯一来源 = yh.app-auth(本文件 app-auth 段),open.* 副本已删
|
||||
base-host: http://base.yunhaixingtong.com
|
||||
base-path: /userbase
|
||||
call:
|
||||
# YschCall 直调目标目录(寻址优先级高于 yh.open.services 与 PlatformService 枚举兜底)。
|
||||
# acasyllabu:课程表 oapi(小组件 syllabus 取数)。base-path 即目标 ctx,
|
||||
# 客户端签名按该 ctx 剥离(待签 target = /oapi/timetable/Get?...,不含 /aca-syllabu)。
|
||||
# ⚠️ 真实 ctx = /aca-syllabu(带横线,20261013 生产实测 401 定位);手册写的 /acasyllabu 有误。
|
||||
# 本地联调:AcaSyllabu 在 8109 且无 ctx ⇒ 覆盖 base-host: http://localhost:8109、base-path 留空。
|
||||
targets:
|
||||
acasyllabu:
|
||||
base-host: http://base.yunhaixingtong.com
|
||||
base-path: /aca-syllabu
|
||||
# env-config 前端运行时配置(免登录 GET /api/env-config?target=front,数据源见
|
||||
# config/PropertyEnvConfigSource:聚合 starter 自动装配,有 bean 即注册端点)。
|
||||
# 本地无需配值(前端 dev 走 .env.dev);线上值经环境变量 YH_ENV_CONFIG_SOURCE_FRONT
|
||||
|
||||
@@ -29,7 +29,7 @@ INSERT IGNORE INTO `widget_catalog` (`code`, `name`, `comp_type`, `content`, `da
|
||||
VALUES ('evaluate', '听评课', 1, NULL, NULL, NULL, 1, 1, NOW());
|
||||
|
||||
INSERT IGNORE INTO `widget_catalog` (`code`, `name`, `comp_type`, `content`, `data_code`, `sch_id`, `sort`, `enabled`, `update_time`)
|
||||
VALUES ('syllabus', '我的课表', 1, NULL, NULL, NULL, 2, 1, NOW());
|
||||
VALUES ('syllabus', '课程表', 1, NULL, NULL, NULL, 2, 1, NOW());
|
||||
|
||||
INSERT IGNORE INTO `widget_catalog` (`code`, `name`, `comp_type`, `content`, `data_code`, `sch_id`, `sort`, `enabled`, `update_time`)
|
||||
VALUES ('weekplan', '一周安排', 1, NULL, NULL, NULL, 3, 1, NOW());
|
||||
|
||||
Reference in New Issue
Block a user