diff --git a/src/main/java/com/yunhai/userhome/controller/api/UserSettingsController.java b/src/main/java/com/yunhai/userhome/controller/api/UserSettingsController.java index 57db3bb..cccab2c 100644 --- a/src/main/java/com/yunhai/userhome/controller/api/UserSettingsController.java +++ b/src/main/java/com/yunhai/userhome/controller/api/UserSettingsController.java @@ -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)。 - * - *

待确认接口(已写入待确认清单): - *

*/ @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 optional = userSettingsService.findUserSettings(authUserWithDept.getId()); - // TODO 待确认:bschBaseOpenApi.getPcGrzxPage —— 新框架 YschBaseOpenApi 是否有此方法待确认 - // List appPageList = bschBaseOpenApi.getPcGrzxPage(authUserWithDept.getId(), schId); - List appPageList = Collections.emptyList(); + // 候选池真源 = 自建 widget_catalog(enabled=1,sch_id 按校过滤;NULL=全校) + List catalogList = widgetCatalogService.findEnabledForSchool(schId); if (optional.isPresent()) model = optional.get(); // 壁纸池一次取全:悬空自愈判定与兜底选择共用 List 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 mapList = appPageList.stream().map(boaApp -> { + // id/appId 同值:对齐既有 widgetAppIds 的存取与面板排序链路;不再有 url + List 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()); diff --git a/src/main/java/com/yunhai/userhome/controller/api/WidgetController.java b/src/main/java/com/yunhai/userhome/controller/api/WidgetController.java new file mode 100644 index 0000000..a7154b0 --- /dev/null +++ b/src/main/java/com/yunhai/userhome/controller/api/WidgetController.java @@ -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; + +/** + * 小组件控制器:候选池按需取内容 + 统一取数(登录侧),以及管理端候选池读写(管理员)。 + * + *

真实路径按 {@code /{platform}} 展开为 {@code /api/getWidgetData} 等(见 Config.API_PATH)。

+ */ +@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 empty = new HashMap<>(); + empty.put("msg", "该小工具暂无可用数据源:" + code); + return empty; + } + + /** 仅 comp_type=2:按需取片段内容(不塞进 getUserSettings,避免每次进桌面下发全部 HTML) */ + @GetMapping(value = "getWidgetContent", produces = "application/json") + public Map getWidgetContent(BoaUser authUserWithDept, + @RequestParam(value = "code", required = false) String code) { + Map result = new HashMap<>(); + result.put("content", ""); + Optional 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 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 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 currentRoleOpt = userCurrentRoleService.findById(authUserWithDept.getId()); + if (!currentRoleOpt.isPresent() || !OtherUtil.isAdminById(currentRoleOpt.get().getRoleId())) { + throw new BizException(ErrorCode.FORBIDDEN); + } + } +} diff --git a/src/main/java/com/yunhai/userhome/entity/WidgetCatalog.java b/src/main/java/com/yunhai/userhome/entity/WidgetCatalog.java new file mode 100644 index 0000000..1425e83 --- /dev/null +++ b/src/main/java/com/yunhai/userhome/entity/WidgetCatalog.java @@ -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; +} diff --git a/src/main/java/com/yunhai/userhome/repository/WidgetCatalogRepository.java b/src/main/java/com/yunhai/userhome/repository/WidgetCatalogRepository.java new file mode 100644 index 0000000..26165c2 --- /dev/null +++ b/src/main/java/com/yunhai/userhome/repository/WidgetCatalogRepository.java @@ -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, + JpaSpecificationExecutor { + + Optional findByCode(String code); + + List findByEnabledOrderBySortAscIdAsc(Integer enabled); + + List findAllByOrderBySortAscIdAsc(); +} diff --git a/src/main/java/com/yunhai/userhome/service/IWidgetCatalogService.java b/src/main/java/com/yunhai/userhome/service/IWidgetCatalogService.java new file mode 100644 index 0000000..5fa8902 --- /dev/null +++ b/src/main/java/com/yunhai/userhome/service/IWidgetCatalogService.java @@ -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 findEnabledForSchool(Integer schId); + + Optional findByCode(String code); + + /** 下发范围判定:sch_id 为 NULL = 全校可见 */ + boolean visibleToSchool(WidgetCatalog catalog, Integer schId); + + /** 管理端列表:全量(含 content 与 update_time),鉴权在 Controller */ + List findAllForAdmin(); + + void save(WidgetCatalog model); +} diff --git a/src/main/java/com/yunhai/userhome/service/WidgetDataService.java b/src/main/java/com/yunhai/userhome/service/WidgetDataService.java new file mode 100644 index 0000000..63a7d12 --- /dev/null +++ b/src/main/java/com/yunhai/userhome/service/WidgetDataService.java @@ -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)。 + * + *

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

+ */ +@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 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> 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 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> 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)); + } + + /** + * 一周安排:{Data:[{date, list:[{contents, times}]}], OtherData:{week, schId}},入参 iweek(-1/0/1)。 + * // TODO 接入 oapi: openCall.get(TARGET_USERBASE, "/oapi/...", query, Vo.class) + */ + 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")); + } + data.add(row("date", monday.plusDays(day).format(DATE_FORMAT), "list", items)); + } + Map 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 row(Object... keyValues) { + Map map = new LinkedHashMap<>(); + for (int i = 0; i + 1 < keyValues.length; i += 2) { + map.put(String.valueOf(keyValues[i]), keyValues[i + 1]); + } + return map; + } +} diff --git a/src/main/java/com/yunhai/userhome/service/impl/WidgetCatalogServiceImpl.java b/src/main/java/com/yunhai/userhome/service/impl/WidgetCatalogServiceImpl.java new file mode 100644 index 0000000..04fb931 --- /dev/null +++ b/src/main/java/com/yunhai/userhome/service/impl/WidgetCatalogServiceImpl.java @@ -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 findEnabledForSchool(Integer schId) { + return widgetCatalogRepository.findByEnabledOrderBySortAscIdAsc(ENABLED).stream() + .filter(model -> visibleToSchool(model, schId)) + .collect(Collectors.toList()); + } + + @Override + public Optional 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 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 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); + } +} diff --git a/src/main/resources/sql/widget_catalog.sql b/src/main/resources/sql/widget_catalog.sql new file mode 100644 index 0000000..34bd4a7 --- /dev/null +++ b/src/main/resources/sql/widget_catalog.sql @@ -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`;