feat(一周安排小组件): weekplan 对接 WorkWeekPlan GET /oapi/workplan/Get,替换原 L2 mock
build / build-push (push) Successful in 56s

- 分发 CODE_WEEKPLAN -> weekplan(caller, params),原样透传单个周对象 data(含 days[7])
- week 为相对本周偏移(0=本周/-1=上周/+1=下周),缺省 0 不向外传(签名 target 最小化);合法范围见 minWeek/maxWeek
- 缓存 key 改 schId|week偏移(全校共享、与身份无关,但不同周内容不同)
- 90s TTL + 15s 失败退避 + 上限 500;缺身份/不可达/验签失败/越界均降级空对象,不抛 500
- 配套 front WeekplanWidget 翻页改版
This commit is contained in:
chenxing
2026-10-15 16:30:13 +08:00
parent ad28149731
commit 1f31612609
@@ -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]}),由前端按班级维度渲染。</p>
*
* <p>{@code evaluate} / {@code weekplan} 仍为 L2 可溯源 mock(字段名逐一等于
* <p>{@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偏移} 双段键。</p>
*
* <p>{@code evaluate} 仍为 L2 可溯源 mock(字段名逐一等于
* {@code design/widget-snapshots-old-bsch/*.js} 的旧接口口径);未知 code 返回 null 由调用方兜空数据,不抛异常。</p>
*/
@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<String, CacheEntry> syllabusCache = new ConcurrentHashMap<>();
/** key = schId|weekOffset → 缓存条目(周安排为全校共享数据,与 userid/roleId 无关;但不同周内容不同) */
private final ConcurrentHashMap<String, CacheEntry> 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)。
*
* <p>{@code week} 为**相对本周的偏移**(0=本周、-1=上一周、+1=下一周),合法范围由响应
* {@code minWeek}/{@code maxWeek} 提示(前端据此驱动翻页边界);注意与数据字段 {@code data.week}
* (绝对周次)同名不同义。</p>
*
* <p>userid 仅用于 oapi 侧审计、不影响返回内容,故缓存按 {@code schId|week偏移}
* (全校共享、与身份无关,但不同周内容不同,不可共用单键)。</p>
*/
private Map<String, Object> 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<Map<String, Object>> data = new ArrayList<>();
for (int day = 0; day < 7; day++) {
List<Map<String, Object>> 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<String, String> 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<String, Object> 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<String, CacheEntry> entry : syllabusCache.entrySet()) {
private void cache(ConcurrentHashMap<String, CacheEntry> store, int max, String key, Object value,
long expireAt, long now) {
if (store.size() >= max) {
for (Map.Entry<String, CacheEntry> 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) {