- widget_catalog:comp_type=1 内容在 front 工程(库里只登记键)、=2 内容存 content,全表无 url 字段 ⇒ 小组件链路彻底不再有「html 地址」;sch_id 只留字段与 visibleToSchool 过滤钩子(v1 全校一致)
- 新接口(真实路径按 /{platform} 展开为 /api/xxx):getWidgetData?code=¶ms=(统一取数,未知 code 回空数据+msg 不抛 500)、getWidgetContent?code=(仅 comp_type=2 按需取正文,不塞进 getUserSettings 免每次进桌面下发全部 HTML)、GetWidgetCatalogList、SaveWidgetCatalog(content 走 JSON body)
- 管理端鉴权照抄 AppSettingController.checkAdmin(超管 type==1 或 roleId∈{2,5,7},判定标准一字未改;该方法是 private 故复制逻辑),拒绝走 BizException(ErrorCode.FORBIDDEN),按平台信封为 code=3 非 500
- getUserSettings:widgetAppList 改读自建表并增 code/compType、删 url,id/appId 同值以复用既有 widgetAppIds 存取与面板排序链路;saveUserWidget/toggleWidgetShow 未动
- 数据源按拍板走 L2 可溯源 mock(yunhai 9 工程检索课表/听评课零命中),字段逐一等于 design/widget-snapshots-old-bsch 旧接口口径,接入点以 // TODO 接入 oapi: 标注;本期无出站,故未注入 YschOpenCall(零手写签名代码)
- DDL:仓库无 migration 机制(ddl-auto=update),脚本按既有约定落 resources/sql/widget_catalog.sql,dev 自动建表、测试/生产需手工执行(INSERT IGNORE 可重复跑)
This commit is contained in:
@@ -19,6 +19,7 @@ import com.yunhai.userhome.service.AppIconKeyService;
|
||||
import com.yunhai.userhome.service.ISchSettingsService;
|
||||
import com.yunhai.userhome.service.IUserCurrentRoleService;
|
||||
import com.yunhai.userhome.service.IUserSettingsService;
|
||||
import com.yunhai.userhome.service.IWidgetCatalogService;
|
||||
import com.yunhai.userhome.service.UsedAppService;
|
||||
import com.yunhai.userhome.utils.EnumUtil;
|
||||
import com.yunhai.userhome.utils.FileUtil;
|
||||
@@ -34,11 +35,6 @@ import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用户设置控制器(对齐旧 UserHome contorller.api.UserSettingsController)。
|
||||
*
|
||||
* <p>待确认接口(已写入待确认清单):
|
||||
* <ul>
|
||||
* <li>getUserSettings 中 bschBaseOpenApi.getPcGrzxPage —— 新框架 YschBaseOpenApi 是否有此方法待确认</li>
|
||||
* </ul>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(Config.API_PATH)
|
||||
@@ -68,6 +64,9 @@ public class UserSettingsController {
|
||||
@Autowired
|
||||
private AppIconKeyService appIconKeyService;
|
||||
|
||||
@Autowired
|
||||
private IWidgetCatalogService widgetCatalogService;
|
||||
|
||||
// ==================== 壁纸 ====================
|
||||
|
||||
/**
|
||||
@@ -288,9 +287,8 @@ public class UserSettingsController {
|
||||
Integer schId = currentRoleOpt.isPresent() ? currentRoleOpt.get().getSchId() : null;
|
||||
UserSettings model = new UserSettings();
|
||||
Optional<UserSettings> optional = userSettingsService.findUserSettings(authUserWithDept.getId());
|
||||
// TODO 待确认:bschBaseOpenApi.getPcGrzxPage —— 新框架 YschBaseOpenApi 是否有此方法待确认
|
||||
// List<BoaApp> appPageList = bschBaseOpenApi.getPcGrzxPage(authUserWithDept.getId(), schId);
|
||||
List<BoaApp> appPageList = Collections.emptyList();
|
||||
// 候选池真源 = 自建 widget_catalog(enabled=1,sch_id 按校过滤;NULL=全校)
|
||||
List<WidgetCatalog> catalogList = widgetCatalogService.findEnabledForSchool(schId);
|
||||
if (optional.isPresent()) model = optional.get();
|
||||
// 壁纸池一次取全:悬空自愈判定与兜底选择共用
|
||||
List<Wallpaper> wallList = userSettingsService.findWallList(authUserWithDept.getId());
|
||||
@@ -303,12 +301,14 @@ public class UserSettingsController {
|
||||
if (model.getWallpaperUrl() == null || model.getWallpaperUrl().isEmpty()) {
|
||||
model.setWallpaperUrl(resolveFallbackWallpaper(authUserWithDept, schId, wallList));
|
||||
}
|
||||
List<Map> mapList = appPageList.stream().map(boaApp -> {
|
||||
// id/appId 同值:对齐既有 widgetAppIds 的存取与面板排序链路;不再有 url
|
||||
List<Map> mapList = catalogList.stream().map(catalog -> {
|
||||
Map map = new HashMap<>();
|
||||
map.put("id", boaApp.getId());
|
||||
map.put("appId", boaApp.getId());
|
||||
map.put("name", boaApp.getName());
|
||||
map.put("url", boaApp.getWebAddr());
|
||||
map.put("id", catalog.getId());
|
||||
map.put("appId", catalog.getId());
|
||||
map.put("name", catalog.getName());
|
||||
map.put("code", catalog.getCode());
|
||||
map.put("compType", catalog.getCompType());
|
||||
return map;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.yunhai.userhome.controller.api;
|
||||
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import com.yunhai.userhome.config.Config;
|
||||
import com.yunhai.userhome.entity.UserCurrentRole;
|
||||
import com.yunhai.userhome.entity.WidgetCatalog;
|
||||
import com.yunhai.userhome.service.IUserCurrentRoleService;
|
||||
import com.yunhai.userhome.service.IWidgetCatalogService;
|
||||
import com.yunhai.userhome.service.WidgetDataService;
|
||||
import com.yunhai.userhome.utils.OtherUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 小组件控制器:候选池按需取内容 + 统一取数(登录侧),以及管理端候选池读写(管理员)。
|
||||
*
|
||||
* <p>真实路径按 {@code /{platform}} 展开为 {@code /api/getWidgetData} 等(见 Config.API_PATH)。</p>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(Config.API_PATH)
|
||||
public class WidgetController {
|
||||
|
||||
private static final int COMP_TYPE_FRAGMENT = 2;
|
||||
|
||||
@Autowired
|
||||
private IWidgetCatalogService widgetCatalogService;
|
||||
|
||||
@Autowired
|
||||
private WidgetDataService widgetDataService;
|
||||
|
||||
@Autowired
|
||||
private IUserCurrentRoleService userCurrentRoleService;
|
||||
|
||||
/** 统一取数入口:params 为 urlencoded 查询串;未配置的数据键回空数据,不抛异常 */
|
||||
@GetMapping(value = "getWidgetData", produces = "application/json")
|
||||
public Object getWidgetData(BoaUser authUserWithDept,
|
||||
@RequestParam(value = "code", required = false) String code,
|
||||
@RequestParam(value = "params", required = false) String params) {
|
||||
Object data = widgetDataService.getData(code, params, currentSchId(authUserWithDept));
|
||||
if (data != null) {
|
||||
return data;
|
||||
}
|
||||
Map<String, Object> empty = new HashMap<>();
|
||||
empty.put("msg", "该小工具暂无可用数据源:" + code);
|
||||
return empty;
|
||||
}
|
||||
|
||||
/** 仅 comp_type=2:按需取片段内容(不塞进 getUserSettings,避免每次进桌面下发全部 HTML) */
|
||||
@GetMapping(value = "getWidgetContent", produces = "application/json")
|
||||
public Map<String, Object> getWidgetContent(BoaUser authUserWithDept,
|
||||
@RequestParam(value = "code", required = false) String code) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("content", "");
|
||||
Optional<WidgetCatalog> optional = widgetCatalogService.findByCode(code);
|
||||
if (!optional.isPresent()) {
|
||||
result.put("msg", "小工具不存在:" + code);
|
||||
return result;
|
||||
}
|
||||
WidgetCatalog catalog = optional.get();
|
||||
if (!widgetCatalogService.visibleToSchool(catalog, currentSchId(authUserWithDept))) {
|
||||
result.put("msg", "该小工具未对本校下发");
|
||||
return result;
|
||||
}
|
||||
if (catalog.getCompType() == null || catalog.getCompType() != COMP_TYPE_FRAGMENT) {
|
||||
result.put("msg", "内置组件的内容在 front 工程里,无按需取内容这条路:" + code);
|
||||
return result;
|
||||
}
|
||||
result.put("code", catalog.getCode());
|
||||
result.put("name", catalog.getName());
|
||||
result.put("compType", catalog.getCompType());
|
||||
result.put("dataCode", catalog.getDataCode());
|
||||
result.put("content", catalog.getContent() == null ? "" : catalog.getContent());
|
||||
return result;
|
||||
}
|
||||
|
||||
@GetMapping(value = "GetWidgetCatalogList", produces = "application/json")
|
||||
public List<WidgetCatalog> getWidgetCatalogList(BoaUser authUserWithDept) {
|
||||
checkAdmin(authUserWithDept);
|
||||
return widgetCatalogService.findAllForAdmin();
|
||||
}
|
||||
|
||||
/** 管理端保存:content 走 JSON body(同 SaveTempFile 口径,规避 URL 长度限制) */
|
||||
@PostMapping(value = "SaveWidgetCatalog", produces = "application/json")
|
||||
public void saveWidgetCatalog(BoaUser authUserWithDept, @RequestBody WidgetCatalog widgetCatalog) {
|
||||
checkAdmin(authUserWithDept);
|
||||
widgetCatalogService.save(widgetCatalog);
|
||||
}
|
||||
|
||||
private Integer currentSchId(BoaUser authUserWithDept) {
|
||||
Optional<UserCurrentRole> currentRoleOpt = userCurrentRoleService.findById(authUserWithDept.getId());
|
||||
return currentRoleOpt.isPresent() ? currentRoleOpt.get().getSchId() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写操作权限校验:超管(type==1) 或 校管理员(roleId∈{2,5,7}),其余拒绝——判定标准与
|
||||
* AppSettingController.checkAdmin 一字不改(该方法是 private,不能直接调用)。
|
||||
* 注:平台 yh-api-std 的异常处理器无 @ResponseStatus,拒绝表现为 HTTP 200 + 信封 code=3(非 500)。
|
||||
*/
|
||||
private void checkAdmin(BoaUser authUserWithDept) {
|
||||
if (authUserWithDept.getType() == 1) {
|
||||
return;
|
||||
}
|
||||
Optional<UserCurrentRole> currentRoleOpt = userCurrentRoleService.findById(authUserWithDept.getId());
|
||||
if (!currentRoleOpt.isPresent() || !OtherUtil.isAdminById(currentRoleOpt.get().getRoleId())) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.yunhai.userhome.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.UniqueConstraint;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 小组件候选池(widget_catalog 表):内容自持,全表无 html 地址概念。
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "widget_catalog", uniqueConstraints = @UniqueConstraint(columnNames = "code"))
|
||||
public class WidgetCatalog {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
/** 内置组件 = 前端 widgetRegistry 的键;自定义片段 = 自定义标识 */
|
||||
@Column(name = "code", nullable = false, length = 32)
|
||||
private String code;
|
||||
|
||||
@Column(name = "name", nullable = false, length = 64)
|
||||
private String name;
|
||||
|
||||
/** 1 = front 工程内置组件(内容在代码库,本行不存 content);2 = 自定义 HTML 片段(内容在 content) */
|
||||
@Column(name = "comp_type", nullable = false)
|
||||
private Integer compType = 1;
|
||||
|
||||
/** 仅 comp_type=2:片段 HTML 源码 */
|
||||
@Column(name = "content", columnDefinition = "mediumtext")
|
||||
private String content;
|
||||
|
||||
/** 片段取数用的数据键,须在 WidgetDataService 白名单内 */
|
||||
@Column(name = "data_code", length = 32)
|
||||
private String dataCode;
|
||||
|
||||
/** 下发范围:NULL = 全校可见(本期只留字段与过滤钩子,v1 全校一致) */
|
||||
@Column(name = "sch_id")
|
||||
private Integer schId;
|
||||
|
||||
@Column(name = "sort", nullable = false)
|
||||
private Integer sort = 0;
|
||||
|
||||
/** 1 = 进候选池,0 = 下线 */
|
||||
@Column(name = "enabled", nullable = false)
|
||||
private Integer enabled = 1;
|
||||
|
||||
@Column(name = "update_time")
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.yunhai.userhome.repository;
|
||||
|
||||
import com.yunhai.userhome.entity.WidgetCatalog;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface WidgetCatalogRepository extends JpaRepository<WidgetCatalog, Integer>,
|
||||
JpaSpecificationExecutor<WidgetCatalog> {
|
||||
|
||||
Optional<WidgetCatalog> findByCode(String code);
|
||||
|
||||
List<WidgetCatalog> findByEnabledOrderBySortAscIdAsc(Integer enabled);
|
||||
|
||||
List<WidgetCatalog> findAllByOrderBySortAscIdAsc();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.yunhai.userhome.service;
|
||||
|
||||
import com.yunhai.userhome.entity.WidgetCatalog;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface IWidgetCatalogService {
|
||||
|
||||
/** 候选池(登录侧):enabled=1 且对本校可见,按 sort 升序 */
|
||||
List<WidgetCatalog> findEnabledForSchool(Integer schId);
|
||||
|
||||
Optional<WidgetCatalog> findByCode(String code);
|
||||
|
||||
/** 下发范围判定:sch_id 为 NULL = 全校可见 */
|
||||
boolean visibleToSchool(WidgetCatalog catalog, Integer schId);
|
||||
|
||||
/** 管理端列表:全量(含 content 与 update_time),鉴权在 Controller */
|
||||
List<WidgetCatalog> findAllForAdmin();
|
||||
|
||||
void save(WidgetCatalog model);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.yunhai.userhome.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 小组件统一取数(方案 §3.4 已拍板:先按 L2 可溯源 mock)。
|
||||
*
|
||||
* <p>字段名逐一等于 design/widget-snapshots-old-bsch/*.js 的旧接口口径,接入真实数据源前不外扩;
|
||||
* 未知 code 返回 null 由调用方兜空数据,不抛异常。</p>
|
||||
*/
|
||||
@Service
|
||||
public class WidgetDataService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WidgetDataService.class);
|
||||
|
||||
private static final String CODE_EVALUATE = "evaluate";
|
||||
|
||||
private static final String CODE_SYLLABUS = "syllabus";
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 按数据键取数;params 为 urlencoded 查询串(对齐旧 otherParams 口径)。
|
||||
*
|
||||
* @return 数据结构由数据键决定;null = 该键未接入
|
||||
*/
|
||||
public Object getData(String code, String params, Integer schId) {
|
||||
if (CODE_EVALUATE.equals(code)) return evaluate(params);
|
||||
if (CODE_SYLLABUS.equals(code)) return syllabus();
|
||||
if (CODE_WEEKPLAN.equals(code)) return weekplan(params, schId);
|
||||
log.warn("[widget] 未配置的数据键:{}", code);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 听评课:分页 {list, count, pageSize},入参 {page, count}(page 从 0 起)。
|
||||
* // TODO 接入 oapi: openCall.post(TARGET_USERBASE, "/oapi/...", null, body, Vo.class)
|
||||
*/
|
||||
private Map<String, Object> evaluate(String params) {
|
||||
int page = Math.max(0, intParam(params, "page", 0));
|
||||
int count = Math.min(50, Math.max(1, intParam(params, "count", EVALUATE_DEFAULT_COUNT)));
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
rows.add(row("id", 101, "title", "《圆的认识》课堂观察",
|
||||
"lectureTime", "2026-09-28 08:30:00", "lectureteaName", "王晓梅", "subName", "数学"));
|
||||
rows.add(row("id", 102, "title", "《Unit 5 Reading》公开课",
|
||||
"lectureTime", "2026-09-29 10:00:00", "lectureteaName", "李强", "subName", "英语"));
|
||||
rows.add(row("id", 103, "title", "《光的折射》实验课",
|
||||
"lectureTime", "2026-09-30 14:00:00", "lectureteaName", "陈雷", "subName", "物理"));
|
||||
rows.add(row("id", 104, "title", "《三角形内角和》研讨课",
|
||||
"lectureTime", "2026-10-09 09:00:00", "lectureteaName", "赵敏", "subName", "数学"));
|
||||
int from = Math.min(page * count, rows.size());
|
||||
int to = Math.min(from + count, rows.size());
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("list", rows.subList(from, to));
|
||||
result.put("count", rows.size());
|
||||
result.put("pageSize", count);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的课表:数组,一格一行(节次 CourseTimes × 星期 Weeks)。
|
||||
* // TODO 接入 oapi: openCall.getList(TARGET_USERBASE, "/oapi/...", null, Vo.class)
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 一周安排:{Data:[{date, list:[{contents, times}]}], OtherData:{week, schId}},入参 iweek(-1/0/1)。
|
||||
* // TODO 接入 oapi: openCall.get(TARGET_USERBASE, "/oapi/...", query, Vo.class)
|
||||
*/
|
||||
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"));
|
||||
}
|
||||
data.add(row("date", monday.plusDays(day).format(DATE_FORMAT), "list", items));
|
||||
}
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("Data", data);
|
||||
// 真实教学周次由数据源下发,mock 固定第 7 周
|
||||
result.put("OtherData", row("week", 7, "schId", schId));
|
||||
return result;
|
||||
}
|
||||
|
||||
private int intParam(String params, String key, int defaultValue) {
|
||||
if (params == null || params.isEmpty()) return defaultValue;
|
||||
for (String pair : params.split("&")) {
|
||||
int split = pair.indexOf('=');
|
||||
if (split <= 0 || !key.equals(pair.substring(0, split))) continue;
|
||||
try {
|
||||
return Integer.parseInt(URLDecoder.decode(pair.substring(split + 1), "UTF-8").trim());
|
||||
} catch (NumberFormatException | UnsupportedEncodingException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private Map<String, Object> row(Object... keyValues) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
for (int i = 0; i + 1 < keyValues.length; i += 2) {
|
||||
map.put(String.valueOf(keyValues[i]), keyValues[i + 1]);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.yunhai.userhome.service.impl;
|
||||
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.userhome.entity.WidgetCatalog;
|
||||
import com.yunhai.userhome.repository.WidgetCatalogRepository;
|
||||
import com.yunhai.userhome.service.IWidgetCatalogService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 小组件候选池服务(对齐 IUserSettingsService 分层:Controller 只做鉴权与出参组装,规则在此)。
|
||||
*/
|
||||
@Service
|
||||
public class WidgetCatalogServiceImpl implements IWidgetCatalogService {
|
||||
|
||||
private static final int COMP_TYPE_BUILTIN = 1;
|
||||
|
||||
private static final int COMP_TYPE_FRAGMENT = 2;
|
||||
|
||||
private static final int ENABLED = 1;
|
||||
|
||||
@Autowired
|
||||
private WidgetCatalogRepository widgetCatalogRepository;
|
||||
|
||||
@Override
|
||||
public List<WidgetCatalog> findEnabledForSchool(Integer schId) {
|
||||
return widgetCatalogRepository.findByEnabledOrderBySortAscIdAsc(ENABLED).stream()
|
||||
.filter(model -> visibleToSchool(model, schId))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<WidgetCatalog> findByCode(String code) {
|
||||
if (code == null || code.trim().isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return widgetCatalogRepository.findByCode(code);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visibleToSchool(WidgetCatalog catalog, Integer schId) {
|
||||
return catalog.getSchId() == null || catalog.getSchId().equals(schId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<WidgetCatalog> findAllForAdmin() {
|
||||
return widgetCatalogRepository.findAllByOrderBySortAscIdAsc();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(WidgetCatalog model) {
|
||||
if (model.getCode() == null || model.getCode().trim().isEmpty()) {
|
||||
throw new BizException(ErrorCode.CUSTOM, "缺少组件键 code");
|
||||
}
|
||||
if (model.getName() == null || model.getName().trim().isEmpty()) {
|
||||
throw new BizException(ErrorCode.CUSTOM, "缺少名称");
|
||||
}
|
||||
if (model.getCompType() == null
|
||||
|| (model.getCompType() != COMP_TYPE_BUILTIN && model.getCompType() != COMP_TYPE_FRAGMENT)) {
|
||||
throw new BizException(ErrorCode.CUSTOM, "类型只能是内置(1)或自定义片段(2)");
|
||||
}
|
||||
Optional<WidgetCatalog> exist = widgetCatalogRepository.findByCode(model.getCode());
|
||||
if (exist.isPresent() && !exist.get().getId().equals(model.getId())) {
|
||||
throw new BizException(ErrorCode.CUSTOM, "组件键已存在:" + model.getCode());
|
||||
}
|
||||
if (model.getSort() == null) model.setSort(0);
|
||||
if (model.getEnabled() == null) model.setEnabled(ENABLED);
|
||||
// 内置组件的内容在 front 工程里,库里只登记键
|
||||
if (model.getCompType() == COMP_TYPE_BUILTIN) model.setContent(null);
|
||||
model.setUpdateTime(new Date());
|
||||
widgetCatalogRepository.save(model);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
-- ============================================================
|
||||
-- 小组件候选池 widget_catalog(UserHome小组件内置化落地方案-20261009 §3.1)
|
||||
-- 候选池真源由旧 bsch 开放接口换成本地表;内容自持,全表没有 url / html 地址字段。
|
||||
-- comp_type=1 → 内容在 front 工程(内置 Vue 组件),本表只登记键;
|
||||
-- comp_type=2 → 内容在 content(管理端贴/传,srcdoc 渲染)。
|
||||
-- dev 由 JPA ddl-auto=update 依实体自动建表;测试/生产等关闭 ddl-auto 的环境手工执行本文件。
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `widget_catalog` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`code` VARCHAR(32) NOT NULL COMMENT '组件键:内置=前端注册表键,自定义=自定义标识',
|
||||
`name` VARCHAR(64) NOT NULL COMMENT '展示名(候选池文案 / 面板标题)',
|
||||
`comp_type` TINYINT NOT NULL DEFAULT 1 COMMENT '1=front 内置组件 2=自定义 HTML 片段',
|
||||
`content` MEDIUMTEXT NULL COMMENT 'comp_type=2 的 HTML 源码;内置类型恒空',
|
||||
`data_code` VARCHAR(32) NULL COMMENT '片段取数用的数据键(须在 WidgetDataService 白名单内)',
|
||||
`sch_id` INT NULL COMMENT '下发范围:NULL=全校可见(本期全校一致,字段与过滤钩子先就位)',
|
||||
`sort` INT NOT NULL DEFAULT 0 COMMENT '候选池排序',
|
||||
`enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '1=进候选池 0=下线',
|
||||
`update_time` DATETIME NULL COMMENT '内容最后修改时间(管理端展示)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_widget_catalog_code` (`code`)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COMMENT = '小组件候选池(内容自持,无外部 html 托管依赖)';
|
||||
|
||||
-- 初始化三个存量组件(判定规则 §9:全部内置,content 留空)
|
||||
-- code 上有唯一键,用 INSERT IGNORE 保证脚本可重复执行
|
||||
INSERT IGNORE INTO `widget_catalog` (`code`, `name`, `comp_type`, `content`, `data_code`, `sch_id`, `sort`, `enabled`, `update_time`)
|
||||
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());
|
||||
|
||||
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());
|
||||
|
||||
-- 回滚:
|
||||
-- DROP TABLE IF EXISTS `widget_catalog`;
|
||||
Reference in New Issue
Block a user