diff --git a/src/main/java/com/yunhai/userhome/service/WidgetDataService.java b/src/main/java/com/yunhai/userhome/service/WidgetDataService.java index 63a7d12..4dab6d8 100644 --- a/src/main/java/com/yunhai/userhome/service/WidgetDataService.java +++ b/src/main/java/com/yunhai/userhome/service/WidgetDataService.java @@ -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)。 + * 小组件统一取数。 * - *

字段名逐一等于 design/widget-snapshots-old-bsch/*.js 的旧接口口径,接入真实数据源前不外扩; - * 未知 code 返回 null 由调用方兜空数据,不抛异常。

+ *

{@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]}),由前端按班级维度渲染。

+ * + *

{@code evaluate} / {@code weekplan} 仍为 L2 可溯源 mock(字段名逐一等于 + * {@code design/widget-snapshots-old-bsch/*.js} 的旧接口口径);未知 code 返回 null 由调用方兜空数据,不抛异常。

*/ @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 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)。 + * + *

身份三参均来自服务端会话推导({@link Caller}),不接受前端传入;roleId<=0 时不透传 + * (oapi 侧默认按普通教师处理)。

*/ - private List> syllabus() { - List> 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> 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 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 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; + } + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ec4cb0d..1f1633b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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 diff --git a/src/main/resources/sql/widget_catalog.sql b/src/main/resources/sql/widget_catalog.sql index 34bd4a7..f76a1f4 100644 --- a/src/main/resources/sql/widget_catalog.sql +++ b/src/main/resources/sql/widget_catalog.sql @@ -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());