diff --git a/src/main/java/com/yunhai/userhome/service/WidgetDataService.java b/src/main/java/com/yunhai/userhome/service/WidgetDataService.java index 4dab6d8..9c51b63 100644 --- a/src/main/java/com/yunhai/userhome/service/WidgetDataService.java +++ b/src/main/java/com/yunhai/userhome/service/WidgetDataService.java @@ -9,10 +9,6 @@ import org.springframework.stereotype.Service; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; -import java.time.DayOfWeek; -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; @@ -27,7 +23,12 @@ import java.util.concurrent.ConcurrentHashMap; * 带 yh-app-auth 签名;目标 {@code acasyllabu} 配于 {@code yh.call.targets}),**原样透传** {@code data[]} * 班级数组(含 {@code timetable.slots[].subjects[7]}),由前端按班级维度渲染。

* - *

{@code evaluate} / {@code weekplan} 仍为 L2 可溯源 mock(字段名逐一等于 + *

{@code weekplan}(一周安排)已对接 WorkWeekPlan {@code GET /oapi/workplan/Get?userid=&schId=&week=}(出站走 + * {@link YschOpenCall})。全校共享数据、不区分角色,**原样透传**单个周对象 {@code data}(含 {@code days[7]}); + * {@code week} 为相对本周偏移(0=本周、-1=上一周、+1=下一周),合法范围见响应 {@code minWeek}/{@code maxWeek}; + * 返回内容与身份无关但随周偏移变化,缓存按 {@code schId|week偏移} 双段键。

+ * + *

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

*/ @Service @@ -41,8 +42,6 @@ public class WidgetDataService { private static final String CODE_WEEKPLAN = "weekplan"; - private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - /** 听评课旧接口 pageSize 默认值(evaluate_index.js 传 count=5) */ private static final int EVALUATE_DEFAULT_COUNT = 5; @@ -61,12 +60,30 @@ public class WidgetDataService { /** 课表缓存条目上限,超出时清理过期项 */ private static final int SYLLABUS_CACHE_MAX = 500; + /** 一周安排 oapi 目标名(见 yh.call.targets.workplan) */ + private static final String TARGET_WORKPLAN = "workplan"; + + /** 一周安排 oapi 路径(签名按目标 ctx 剥离,故此处不含 /work-week-plan) */ + private static final String PATH_WORKPLAN = "/oapi/workplan/Get"; + + /** 一周安排缓存时长:全校共享、低频变更 */ + private static final long WEEKPLAN_TTL_MS = 90_000L; + + /** 一周安排取数失败后的静默期 */ + private static final long WEEKPLAN_FAIL_BACKOFF_MS = 15_000L; + + /** 一周安排缓存条目上限,超出时清理过期项 */ + private static final int WEEKPLAN_CACHE_MAX = 500; + @Autowired private YschOpenCall openCall; /** key = userId|roleId|schId → 缓存条目 */ private final ConcurrentHashMap syllabusCache = new ConcurrentHashMap<>(); + /** key = schId|weekOffset → 缓存条目(周安排为全校共享数据,与 userid/roleId 无关;但不同周内容不同) */ + private final ConcurrentHashMap weekplanCache = new ConcurrentHashMap<>(); + /** * 按数据键取数;params 为 urlencoded 查询串(对齐旧 otherParams 口径)。 * @@ -75,7 +92,7 @@ public class WidgetDataService { public Object getData(String code, String params, Caller caller) { if (CODE_EVALUATE.equals(code)) return evaluate(params); if (CODE_SYLLABUS.equals(code)) return syllabus(caller); - if (CODE_WEEKPLAN.equals(code)) return weekplan(params, caller == null ? null : caller.getSchId()); + if (CODE_WEEKPLAN.equals(code)) return weekplan(caller, params); log.warn("[widget] 未配置的数据键:{}", code); return null; } @@ -132,54 +149,74 @@ public class WidgetDataService { 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); + cache(syllabusCache, SYLLABUS_CACHE_MAX, 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); + cache(syllabusCache, SYLLABUS_CACHE_MAX, key, Collections.emptyList(), now + SYLLABUS_FAIL_BACKOFF_MS, now); return Collections.emptyList(); } } /** - * 一周安排:{Data:[{date, list:[{contents, times}]}], OtherData:{week, schId}},入参 iweek(-1/0/1)。 - * // TODO 接入 oapi: openCall.get(TARGET_USERBASE, "/oapi/...", query, Vo.class) + * 一周安排:WorkWeekPlan {@code GET /oapi/workplan/Get?userid=&schId=&week=}(SDK 出站,签名自动)。 + * 全校共享数据(不区分角色),原样透传单个周对象 {@code data}(含 {@code days[7]}); + * 缺身份 / 不可达 / 验签失败 / week 越界(oapi {@code code:2},SDK 抛异常)均降级空对象(不抛 500)。 + * + *

{@code week} 为**相对本周的偏移**(0=本周、-1=上一周、+1=下一周),合法范围由响应 + * {@code minWeek}/{@code maxWeek} 提示(前端据此驱动翻页边界);注意与数据字段 {@code data.week} + * (绝对周次)同名不同义。

+ * + *

userid 仅用于 oapi 侧审计、不影响返回内容,故缓存按 {@code schId|week偏移} + * (全校共享、与身份无关,但不同周内容不同,不可共用单键)。

*/ - private Map weekplan(String params, Integer schId) { - int iweek = Math.max(-1, Math.min(1, intParam(params, "iweek", 0))); - LocalDate monday = LocalDate.now().with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)) - .plusWeeks(iweek); - List> data = new ArrayList<>(); - for (int day = 0; day < 7; day++) { - List> items = new ArrayList<>(); - if (day == 0) { - items.add(row("contents", "行政会(三楼会议室)", "times", "09:00-10:00")); - } else if (day == 2) { - items.add(row("contents", "数学教研组备课磨课", "times", "14:00-15:30")); - } else if (day == 4) { - items.add(row("contents", "家长开放日准备会", "times", "16:00-17:00")); + private Object weekplan(Caller caller, String params) { + if (caller == null || caller.getUserId() == null || caller.getSchId() == null) { + // 缺身份(超管无当前身份等)→ 空数据,不打出站 + return Collections.emptyMap(); + } + // week = 相对本周偏移(0=本周 / -1=上一周 / +1=下一周),复用 evaluate 同款解析(支持负数) + int weekOffset = intParam(params, "week", 0); + String key = caller.getSchId() + "|" + weekOffset; + long now = System.currentTimeMillis(); + CacheEntry hit = weekplanCache.get(key); + if (hit != null && now < hit.expireAt) { + return hit.data; + } + try { + Map query = new LinkedHashMap<>(); + query.put("userid", String.valueOf(caller.getUserId())); + query.put("schId", String.valueOf(caller.getSchId())); + if (weekOffset != 0) { + // 0 为缺省值(本周):不传可让本周请求与签名 target 保持最小化 + query.put("week", String.valueOf(weekOffset)); } - data.add(row("date", monday.plusDays(day).format(DATE_FORMAT), "list", items)); + JsonNode data = openCall.get(TARGET_WORKPLAN, PATH_WORKPLAN, query, JsonNode.class); + Object value = (data != null && data.isObject()) ? data : Collections.emptyMap(); + cache(weekplanCache, WEEKPLAN_CACHE_MAX, key, value, now + WEEKPLAN_TTL_MS, now); + return value; + } catch (Exception e) { + log.warn("[widget] 一周安排 oapi 不可达(降级空)sch={} week={}:{}", + caller.getSchId(), weekOffset, e.toString()); + // 失败退避:短 TTL 空结果,避免下游停摆时每次翻页都吃一次超时 + cache(weekplanCache, WEEKPLAN_CACHE_MAX, key, Collections.emptyMap(), now + WEEKPLAN_FAIL_BACKOFF_MS, now); + return Collections.emptyMap(); } - Map result = new LinkedHashMap<>(); - result.put("Data", data); - // 真实教学周次由数据源下发,mock 固定第 7 周 - result.put("OtherData", row("week", 7, "schId", schId)); - 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()) { + private void cache(ConcurrentHashMap store, int max, String key, Object value, + long expireAt, long now) { + if (store.size() >= max) { + for (Map.Entry entry : store.entrySet()) { if (now >= entry.getValue().expireAt) { - syllabusCache.remove(entry.getKey(), entry.getValue()); + store.remove(entry.getKey(), entry.getValue()); } } } - syllabusCache.put(key, new CacheEntry(value, expireAt)); + store.put(key, new CacheEntry(value, expireAt)); } private int intParam(String params, String key, int defaultValue) {