feat(小组件管理改版): 后端校级覆盖三态+目录权/校级权拆分+内容ETag,退役应用管理(AppCategory全家)——配套已上线front 5a19a4e;含getWidgetData传参改Caller{userId,roleId,schId}(数据层WidgetDataService随下一提交);DDL见sql/widget_catalog_v2.sql(生产需手工执行);msg by-code换源已随9370429上线不在本提交
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
package com.yunhai.userhome.config;
|
||||
|
||||
import com.yunhai.userhome.entity.WidgetCatalog;
|
||||
import com.yunhai.userhome.repository.WidgetCatalogRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 小组件内置目录行启动 seeding(幂等)。
|
||||
*
|
||||
* <p>背景:种子 INSERT 原先只在 {@code resources/sql/widget_catalog.sql} 里,代码无 seeding,
|
||||
* 新环境首次启动时候选池恒为空。此处补一条启动期兜底:库为空才写入 3 条内置目录行。</p>
|
||||
*
|
||||
* <p>三条种子与 {@code resources/sql/widget_catalog.sql} 用同一份常量(code / name / comp_type / scope_type / enabled),
|
||||
* 仅补「目录行」,不补内容(内置组件内容在前端工程);{@code WidgetDataService} 白名单不动。</p>
|
||||
*
|
||||
* @author chenx
|
||||
* @date 2026-10-11
|
||||
*/
|
||||
@Component
|
||||
public class WidgetCatalogSeeder implements CommandLineRunner {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WidgetCatalogSeeder.class);
|
||||
|
||||
/** {code, name, sort}:与 resources/sql/widget_catalog.sql 的 INSERT 一一对应 */
|
||||
private static final String[][] SEEDS = {
|
||||
{"evaluate", "听评课", "1"},
|
||||
{"syllabus", "课程表", "2"},
|
||||
{"weekplan", "一周安排", "3"},
|
||||
};
|
||||
|
||||
private static final int COMP_TYPE_BUILTIN = 1;
|
||||
|
||||
private static final int SCOPE_ALL = 0;
|
||||
|
||||
private static final int ENABLED = 1;
|
||||
|
||||
@Autowired
|
||||
private WidgetCatalogRepository widgetCatalogRepository;
|
||||
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
try {
|
||||
// 幂等:库非空即整体跳过,避免管理员下线过的内置组件在重启后被重新种回
|
||||
if (widgetCatalogRepository.count() > 0) {
|
||||
log.info("[widget] widget_catalog 非空,跳过内置目录 seeding");
|
||||
return;
|
||||
}
|
||||
for (String[] seed : SEEDS) {
|
||||
String code = seed[0];
|
||||
if (widgetCatalogRepository.findByCode(code).isPresent()) {
|
||||
continue;
|
||||
}
|
||||
WidgetCatalog catalog = new WidgetCatalog();
|
||||
catalog.setCode(code);
|
||||
catalog.setName(seed[1]);
|
||||
catalog.setCompType(COMP_TYPE_BUILTIN);
|
||||
catalog.setScopeType(SCOPE_ALL);
|
||||
catalog.setSort(Integer.valueOf(seed[2]));
|
||||
catalog.setEnabled(ENABLED);
|
||||
catalog.setUpdateTime(new Date());
|
||||
widgetCatalogRepository.save(catalog);
|
||||
log.info("[widget] seeding 内置目录行:{}", code);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// seeding 失败不阻断应用启动(候选池另有管理端入口补录)
|
||||
log.error("[widget] 内置目录 seeding 失败,跳过", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
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.AppCategory;
|
||||
import com.yunhai.userhome.entity.UserCurrentRole;
|
||||
import com.yunhai.userhome.service.IAppCategoryService;
|
||||
import com.yunhai.userhome.service.IUserCurrentRoleService;
|
||||
import com.yunhai.userhome.utils.FileUtil;
|
||||
import com.yunhai.userhome.utils.OtherUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
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 org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 应用设置控制器(自 systemsetting AppSettingController + SysController 迁移,1:1 对齐)。
|
||||
*
|
||||
* <p>与原版差异点:写操作(应用 CRUD / UpdateResDate / 模板上传保存)校验调用者
|
||||
* 为超管(type==1)或校管理员(roleId∈{2,5,7})——原版任何登录用户可写,因新入口仅管理员可见,收紧。
|
||||
* 查询(分页列表 / FindAppCategory / GetTempList / downloadTemp)登录即可。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(Config.API_PATH)
|
||||
public class AppSettingController {
|
||||
|
||||
@Value("${FILE_PATH:./upload/}")
|
||||
private String FILE_PATH;
|
||||
|
||||
@Value("${FILE_WEB_PATH:}")
|
||||
private String FILE_WEB_PATH;
|
||||
|
||||
@Autowired
|
||||
private IAppCategoryService appCategoryService;
|
||||
|
||||
@Autowired
|
||||
private IUserCurrentRoleService userCurrentRoleService;
|
||||
|
||||
// ==================== 应用类别 CRUD ====================
|
||||
|
||||
/** 新增/编辑应用(JSON body,1:1 对齐原 SaveAppCategory) */
|
||||
@PostMapping(value = "SaveAppCategory", produces = "application/json")
|
||||
public void saveAppCategory(BoaUser authUserWithDept, @RequestBody AppCategory appCategory) {
|
||||
checkAdmin(authUserWithDept);
|
||||
if (appCategory.getCDate() == null) appCategory.setCDate(new Date());
|
||||
if (appCategory.getResDate() == null) appCategory.setResDate(new Date());
|
||||
appCategory.setCId(authUserWithDept.getId());
|
||||
appCategoryService.save(appCategory);
|
||||
}
|
||||
|
||||
@PostMapping(value = "DelAppCategory", produces = "application/json")
|
||||
public void delAppCategory(BoaUser authUserWithDept, Integer id) {
|
||||
checkAdmin(authUserWithDept);
|
||||
appCategoryService.deleteById(id);
|
||||
}
|
||||
|
||||
@GetMapping(value = "GetAppCategoryList")
|
||||
public Map getAppCategoryList(BoaUser authUserWithDept, Integer page, Integer count,
|
||||
@RequestParam(value = "roleType") Integer roleType,
|
||||
@RequestParam(value = "name", required = false) String name) {
|
||||
Page<AppCategory> data = appCategoryService.findPageList(page, count, roleType, name);
|
||||
Map map = new HashMap();
|
||||
map.put("total", data.getTotalElements());
|
||||
map.put("list", data.getContent());
|
||||
return map;
|
||||
}
|
||||
|
||||
@GetMapping(value = "FindAppCategory")
|
||||
public Optional<AppCategory> findAppCategory(BoaUser authUserWithDept, Integer id) {
|
||||
return appCategoryService.findById(id);
|
||||
}
|
||||
|
||||
@PostMapping(value = "UpdateResDate", produces = "application/json")
|
||||
public void updateResDate(BoaUser authUserWithDept, Integer id) {
|
||||
checkAdmin(authUserWithDept);
|
||||
appCategoryService.updateResDate(id);
|
||||
}
|
||||
|
||||
// ==================== 模板管理(对齐原 SysController) ====================
|
||||
|
||||
/** 上传模板压缩包(zip/rar),解压覆盖到 tempUpload/<appModId>/,删除压缩包 */
|
||||
@PostMapping(value = "tempUpload", produces = "application/json")
|
||||
public void tempUpload(BoaUser authUserWithDept, @RequestParam("file") MultipartFile file, Integer appModId) {
|
||||
checkAdmin(authUserWithDept);
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BizException(ErrorCode.CUSTOM, "请选择上传文件");
|
||||
}
|
||||
String originalName = file.getOriginalFilename();
|
||||
String ext = originalName == null ? "" : originalName.substring(originalName.lastIndexOf(".")).toLowerCase(Locale.ROOT);
|
||||
if (!".zip".equals(ext) && !".rar".equals(ext)) {
|
||||
throw new BizException(ErrorCode.CUSTOM, "仅支持 zip/rar 格式");
|
||||
}
|
||||
// 不走图片 5MB 校验(模板包允许更大,依赖 multipart 配置),存到 tempUpload 子目录
|
||||
String uploadPath = FILE_PATH + "/tempUpload/";
|
||||
File fl = new File(uploadPath);
|
||||
if (!fl.exists()) fl.mkdirs();
|
||||
String fileName = FileUtil.uploadFile(file, uploadPath);
|
||||
String filePath = "/upload/tempUpload/" + fileName;
|
||||
|
||||
File unzipFiles = new File(FILE_PATH + "/tempUpload/" + appModId);
|
||||
if (!unzipFiles.exists()) unzipFiles.mkdirs();
|
||||
else FileUtil.delAllFile(unzipFiles.getPath());
|
||||
File zipFile = new File(filePath.replace("/upload", FILE_PATH));
|
||||
try {
|
||||
FileUtil.zipDecompress(zipFile, unzipFiles);
|
||||
} catch (Exception e) {
|
||||
throw new BizException(ErrorCode.CUSTOM, e.getMessage());
|
||||
} finally {
|
||||
zipFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/** 模板文件列表(name + 访问 path) */
|
||||
@GetMapping(value = "GetTempList")
|
||||
public List<Map> getTempList(BoaUser authUserWithDept, Integer appModId) {
|
||||
File sourceFile = new File(FILE_PATH + "/tempUpload/" + appModId);
|
||||
List<Map> result = new ArrayList<>();
|
||||
if (sourceFile.exists() && sourceFile.isDirectory() && sourceFile.listFiles() != null) {
|
||||
Arrays.stream(sourceFile.listFiles()).forEach(file -> {
|
||||
Map map = new HashMap();
|
||||
map.put("name", file.getName());
|
||||
map.put("path", FILE_WEB_PATH + "/tempUpload/" + appModId + "/" + file.getName());
|
||||
result.add(map);
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 保存模板文件内容(新增 fileName 路径穿越校验,原版没有;content 走 JSON body 避免 URL 长度限制) */
|
||||
@PostMapping(value = "SaveTempFile", produces = "application/json")
|
||||
public void saveTempFile(BoaUser authUserWithDept, @RequestBody Map<String, Object> body) {
|
||||
checkAdmin(authUserWithDept);
|
||||
Object appModIdObj = body.get("appModId");
|
||||
String fileName = body.get("fileName") == null ? null : body.get("fileName").toString();
|
||||
String content = body.get("content") == null ? "" : body.get("content").toString();
|
||||
if (appModIdObj == null) {
|
||||
throw new BizException(ErrorCode.CUSTOM, "缺少appModId");
|
||||
}
|
||||
Integer appModId;
|
||||
try {
|
||||
appModId = Integer.parseInt(appModIdObj.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
throw new BizException(ErrorCode.CUSTOM, "非法appModId");
|
||||
}
|
||||
if (fileName == null || fileName.contains("/") || fileName.contains("\\") || fileName.contains("..")) {
|
||||
throw new BizException(ErrorCode.CUSTOM, "非法文件名");
|
||||
}
|
||||
File dir = new File(FILE_PATH + "/tempUpload/" + appModId);
|
||||
if (!dir.exists()) dir.mkdirs();
|
||||
File file = new File(FILE_PATH + "/tempUpload/" + appModId + "/" + fileName);
|
||||
try {
|
||||
if (!file.exists()) {
|
||||
file.createNewFile();
|
||||
}
|
||||
FileOutputStream fos = new FileOutputStream(file);
|
||||
fos.write(content.getBytes());
|
||||
fos.flush();
|
||||
fos.close();
|
||||
} catch (IOException e) {
|
||||
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "保存失败");
|
||||
}
|
||||
}
|
||||
|
||||
/** 打包下载模板目录(zip 流式下载,临时 zip 关闭即删) */
|
||||
@GetMapping(value = "downloadTemp", produces = "application/json;charset=UTF-8")
|
||||
public ResponseEntity<InputStreamResource> downloadTemp(BoaUser authUserWithDept, Integer appModId, String name) {
|
||||
File zipFilePath = new File(FILE_PATH + "/tempUpload/" + appModId);
|
||||
if (!zipFilePath.exists() || !zipFilePath.isDirectory()) {
|
||||
throw new BizException(ErrorCode.CUSTOM, "模板不存在");
|
||||
}
|
||||
String zipName = UUID.randomUUID().toString().substring(0, 8) + ".zip";
|
||||
File zipFile = new File(FILE_PATH + "/tempUpload/" + zipName);
|
||||
try {
|
||||
FileUtil.zipCompress(zipFilePath, zipFile);
|
||||
} catch (Exception e) {
|
||||
throw new BizException(ErrorCode.CUSTOM, e.getMessage());
|
||||
}
|
||||
String downloadName = (name == null || name.trim().isEmpty()) ? "template" : name;
|
||||
try {
|
||||
InputStream inputStream = new FileInputStream(zipFile) {
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
super.close();
|
||||
zipFile.delete();
|
||||
}
|
||||
};
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
headers.add("Content-Disposition",
|
||||
"attachment; filename=" + new String((downloadName + ".zip").getBytes("gbk"), "iso8859-1"));
|
||||
headers.add("Pragma", "no-cache");
|
||||
headers.add("Expires", "0");
|
||||
return ResponseEntity.ok().headers(headers)
|
||||
.contentType(MediaType.parseMediaType("application/octet-stream"))
|
||||
.body(new InputStreamResource(inputStream));
|
||||
} catch (IOException e) {
|
||||
zipFile.delete();
|
||||
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "下载失败");
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
/**
|
||||
* 写操作权限校验:超管(type==1) 或 校管理员(roleId∈{2,5,7}),其余 403。
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,10 +285,11 @@ public class UserSettingsController {
|
||||
public UserSettings getUserSettings(BoaUser authUserWithDept) {
|
||||
Optional<UserCurrentRole> currentRoleOpt = userCurrentRoleService.findById(authUserWithDept.getId());
|
||||
Integer schId = currentRoleOpt.isPresent() ? currentRoleOpt.get().getSchId() : null;
|
||||
Integer roleId = currentRoleOpt.isPresent() ? currentRoleOpt.get().getRoleId() : null;
|
||||
UserSettings model = new UserSettings();
|
||||
Optional<UserSettings> optional = userSettingsService.findUserSettings(authUserWithDept.getId());
|
||||
// 候选池真源 = 自建 widget_catalog(enabled=1,sch_id 按校过滤;NULL=全校)
|
||||
List<WidgetCatalog> catalogList = widgetCatalogService.findEnabledForSchool(schId);
|
||||
// 候选池真源 = 自建 widget_catalog(生效启用 + 可见范围命中本校 + 可见角色命中当前角色;name 已按显示名三级链合成)
|
||||
List<WidgetCatalog> catalogList = widgetCatalogService.findEnabledForSchool(schId, roleId);
|
||||
if (optional.isPresent()) model = optional.get();
|
||||
// 壁纸池一次取全:悬空自愈判定与兜底选择共用
|
||||
List<Wallpaper> wallList = userSettingsService.findWallList(authUserWithDept.getId());
|
||||
|
||||
@@ -6,11 +6,13 @@ 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.entity.WidgetSchoolSetting;
|
||||
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.util.DigestUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -18,6 +20,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -26,6 +30,9 @@ import java.util.Optional;
|
||||
/**
|
||||
* 小组件控制器:候选池按需取内容 + 统一取数(登录侧),以及管理端候选池读写(管理员)。
|
||||
*
|
||||
* <p>写权限按「目录权 / 校级权」拆开:全局目录(新增 / 改内容 / 删除 / 三态可见范围 / 角色命名)仅超管;
|
||||
* 校级覆盖(启用 / 可见角色 / 排序 / 别名)超管或校管,且校管只能写本校。</p>
|
||||
*
|
||||
* <p>真实路径按 {@code /{platform}} 展开为 {@code /api/getWidgetData} 等(见 Config.API_PATH)。</p>
|
||||
*/
|
||||
@RestController
|
||||
@@ -48,7 +55,7 @@ public class WidgetController {
|
||||
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));
|
||||
Object data = widgetDataService.getData(code, params, resolveCaller(authUserWithDept));
|
||||
if (data != null) {
|
||||
return data;
|
||||
}
|
||||
@@ -59,7 +66,7 @@ public class WidgetController {
|
||||
|
||||
/** 仅 comp_type=2:按需取片段内容(不塞进 getUserSettings,避免每次进桌面下发全部 HTML) */
|
||||
@GetMapping(value = "getWidgetContent", produces = "application/json")
|
||||
public Map<String, Object> getWidgetContent(BoaUser authUserWithDept,
|
||||
public Map<String, Object> getWidgetContent(BoaUser authUserWithDept, HttpServletResponse response,
|
||||
@RequestParam(value = "code", required = false) String code) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("content", "");
|
||||
@@ -77,38 +84,85 @@ public class WidgetController {
|
||||
result.put("msg", "内置组件的内容在 front 工程里,无按需取内容这条路:" + code);
|
||||
return result;
|
||||
}
|
||||
String content = catalog.getContent() == null ? "" : catalog.getContent();
|
||||
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());
|
||||
result.put("content", content);
|
||||
// 内容 hash 派生 ETag:内容或更新时间变更即换标签,便于前端条件请求复用
|
||||
response.setHeader("ETag", "\"" + contentHash(content, catalog.getUpdateTime()) + "\"");
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 管理端列表(超管 / 校管):按视角返回——超管=全局原值,校管=本校生效值 + 原名 + 覆盖标记 */
|
||||
@GetMapping(value = "GetWidgetCatalogList", produces = "application/json")
|
||||
public List<WidgetCatalog> getWidgetCatalogList(BoaUser authUserWithDept) {
|
||||
checkAdmin(authUserWithDept);
|
||||
return widgetCatalogService.findAllForAdmin();
|
||||
checkSchAdmin(authUserWithDept);
|
||||
return widgetCatalogService.findEffectiveForAdmin(currentSchId(authUserWithDept), isSuperAdmin(authUserWithDept));
|
||||
}
|
||||
|
||||
/** 管理端保存:content 走 JSON body(同 SaveTempFile 口径,规避 URL 长度限制) */
|
||||
/** 管理端保存全局目录:content 走 JSON body(同 SaveTempFile 口径,规避 URL 长度限制) */
|
||||
@PostMapping(value = "SaveWidgetCatalog", produces = "application/json")
|
||||
public void saveWidgetCatalog(BoaUser authUserWithDept, @RequestBody WidgetCatalog widgetCatalog) {
|
||||
checkAdmin(authUserWithDept);
|
||||
checkSuperAdmin(authUserWithDept);
|
||||
widgetCatalogService.save(widgetCatalog);
|
||||
}
|
||||
|
||||
/** 删除全局目录行,连带删可见范围点名表与校级覆盖行 */
|
||||
@PostMapping(value = "DeleteWidgetCatalog", produces = "application/json")
|
||||
public void deleteWidgetCatalog(BoaUser authUserWithDept, @RequestParam(value = "id") Integer id) {
|
||||
checkSuperAdmin(authUserWithDept);
|
||||
widgetCatalogService.deleteById(id);
|
||||
}
|
||||
|
||||
/** 读取本校覆盖行(编辑回显):只认当前登录校 */
|
||||
@GetMapping(value = "GetWidgetSchoolSetting", produces = "application/json")
|
||||
public WidgetSchoolSetting getWidgetSchoolSetting(BoaUser authUserWithDept,
|
||||
@RequestParam(value = "widgetId") Integer widgetId) {
|
||||
checkSchAdmin(authUserWithDept);
|
||||
return widgetCatalogService.findSchoolSetting(widgetId, currentSchId(authUserWithDept)).orElse(null);
|
||||
}
|
||||
|
||||
/** 保存本校覆盖:schId 强制取当前登录校,不接受前端传入 */
|
||||
@PostMapping(value = "SaveWidgetSchoolSetting", produces = "application/json")
|
||||
public void saveWidgetSchoolSetting(BoaUser authUserWithDept, @RequestBody WidgetSchoolSetting setting) {
|
||||
checkSchAdmin(authUserWithDept);
|
||||
widgetCatalogService.saveSchoolSetting(setting, currentSchId(authUserWithDept));
|
||||
}
|
||||
|
||||
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,不能直接调用)。
|
||||
* 取数身份上下文(userid / roleId / schId 全部会话推导,前端不可传):
|
||||
* userid = 平台用户ID(BoaUser.id);roleId/schId = 当前激活身份(UserBase /oapi/identity/current)。
|
||||
*/
|
||||
private WidgetDataService.Caller resolveCaller(BoaUser authUserWithDept) {
|
||||
Optional<UserCurrentRole> currentRoleOpt = userCurrentRoleService.findById(authUserWithDept.getId());
|
||||
Integer roleId = currentRoleOpt.isPresent() ? currentRoleOpt.get().getRoleId() : null;
|
||||
Integer schId = currentRoleOpt.isPresent() ? currentRoleOpt.get().getSchId() : null;
|
||||
return new WidgetDataService.Caller(authUserWithDept.getId(), roleId, schId);
|
||||
}
|
||||
|
||||
private boolean isSuperAdmin(BoaUser authUserWithDept) {
|
||||
return authUserWithDept.getType() == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 目录权校验:仅超管(type==1)可写全局目录 / 删除 / 三态可见范围 / 角色命名 / 登记内置。
|
||||
* 注:平台 yh-api-std 的异常处理器无 @ResponseStatus,拒绝表现为 HTTP 200 + 信封 code=3(非 500)。
|
||||
*/
|
||||
private void checkAdmin(BoaUser authUserWithDept) {
|
||||
private void checkSuperAdmin(BoaUser authUserWithDept) {
|
||||
if (authUserWithDept.getType() != 1) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
/** 校级权校验:超管或校管理员(roleId∈{2,5,7}),其余拒绝 */
|
||||
private void checkSchAdmin(BoaUser authUserWithDept) {
|
||||
if (authUserWithDept.getType() == 1) {
|
||||
return;
|
||||
}
|
||||
@@ -117,4 +171,9 @@ public class WidgetController {
|
||||
throw new BizException(ErrorCode.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
private String contentHash(String content, java.util.Date updateTime) {
|
||||
long stamp = updateTime == null ? 0L : updateTime.getTime();
|
||||
return DigestUtils.md5DigestAsHex((stamp + "\n" + content).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
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.validation.constraints.NotNull;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 桌面应用类别(自 systemsetting.app_category 1:1 迁移)。
|
||||
*
|
||||
* <p>roleType:1 校管理员 | 2 教师 | 3 学生(见 {@code enums.RoleType});
|
||||
* resDate 为资源更新时间戳,前端上传模板后经 UpdateResDate 刷新缓存。</p>
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "app_category")
|
||||
public class AppCategory {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@NotNull(groups = Update.class)
|
||||
private Integer id;
|
||||
|
||||
private String name;
|
||||
|
||||
@Column(name = "api_url", length = 500)
|
||||
private String apiUrl;
|
||||
|
||||
@Column(name = "data_key")
|
||||
private String dataKey;
|
||||
|
||||
/** 接口参数 JSON 数组字符串([{k,v,type}]) */
|
||||
@Column(name = "params_key", columnDefinition = "TEXT")
|
||||
private String paramsKey;
|
||||
|
||||
@Column(name = "role_type")
|
||||
private Integer roleType;
|
||||
|
||||
@Column(name = "c_id")
|
||||
private Integer cId;
|
||||
|
||||
@Column(name = "c_date")
|
||||
private Date cDate = new Date();
|
||||
|
||||
/** 资源更新时间戳 */
|
||||
@Column(name = "res_date")
|
||||
private Date resDate = new Date();
|
||||
|
||||
public @interface Update {}
|
||||
}
|
||||
@@ -10,8 +10,10 @@ import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.Transient;
|
||||
import javax.persistence.UniqueConstraint;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 小组件候选池(widget_catalog 表):内容自持,全表无 html 地址概念。
|
||||
@@ -46,10 +48,22 @@ public class WidgetCatalog {
|
||||
@Column(name = "data_code", length = 32)
|
||||
private String dataCode;
|
||||
|
||||
/** 下发范围:NULL = 全校可见(本期只留字段与过滤钩子,v1 全校一致) */
|
||||
/** 下发范围:旧语义(NULL = 全校可见)已被 scopeType 取代,字段保留仅为兼容存量数据,读侧不再使用 */
|
||||
@Column(name = "sch_id")
|
||||
private Integer schId;
|
||||
|
||||
/** 下发范围三态:0 = 全部学校 1 = 白名单(仅点名的学校可见) 2 = 黑名单(点名的学校不可见) */
|
||||
@Column(name = "scope_type", nullable = false)
|
||||
private Integer scopeType = 0;
|
||||
|
||||
/** 可见角色:逗号分隔 RoleTypeEnum(1 教师 / 2 校领导 / 3 学生);NULL 或空 = 全角色 */
|
||||
@Column(name = "role_types", length = 16)
|
||||
private String roleTypes;
|
||||
|
||||
/** 角色专属命名(JSON 文本,如 {"2":"全校课表"},键 = RoleTypeEnum);读侧 Jackson 解析,失败按无角色命名降级 */
|
||||
@Column(name = "role_alias", length = 255)
|
||||
private String roleAlias;
|
||||
|
||||
@Column(name = "sort", nullable = false)
|
||||
private Integer sort = 0;
|
||||
|
||||
@@ -59,4 +73,20 @@ public class WidgetCatalog {
|
||||
|
||||
@Column(name = "update_time")
|
||||
private Date updateTime;
|
||||
|
||||
/** 管理端视图:scopeType=1/2 时点名的学校 id 列表(非持久化,随 SaveWidgetCatalog 一并提交) */
|
||||
@Transient
|
||||
private List<Integer> scopeSchIds;
|
||||
|
||||
/** 管理端视图(校管视角):本校别名(widget_school_setting.alias,非持久化) */
|
||||
@Transient
|
||||
private String alias;
|
||||
|
||||
/** 管理端视图(校管视角):超管下发的全局名称(非持久化,用于弹窗回显原名) */
|
||||
@Transient
|
||||
private String originalName;
|
||||
|
||||
/** 管理端视图(校管视角):本校是否已有覆盖行(非持久化) */
|
||||
@Transient
|
||||
private Boolean covered;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* 小组件可见范围点名表(widget_catalog_school):只存被点名的学校行。
|
||||
*
|
||||
* <p>配合 {@link WidgetCatalog#getScopeType()}:1(白名单)= 表中有记录的学校可见;
|
||||
* 2(黑名单)= 表中有记录的学校不可见。</p>
|
||||
*
|
||||
* @author chenx
|
||||
* @date 2026-10-11
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "widget_catalog_school",
|
||||
uniqueConstraints = @UniqueConstraint(name = "uk_widget_school", columnNames = {"widget_id", "sch_id"}))
|
||||
public class WidgetCatalogSchool {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
/** widget_catalog.id */
|
||||
@Column(name = "widget_id", nullable = false)
|
||||
private Integer widgetId;
|
||||
|
||||
/** 被点名的学校 id */
|
||||
@Column(name = "sch_id", nullable = false)
|
||||
private Integer schId;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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_school_setting):一校一行。
|
||||
*
|
||||
* <p>字段为 NULL 表示沿用全局值,生效值 = COALESCE(本行, widget_catalog 全局值);
|
||||
* 「一键清除覆盖」= 删除本行。</p>
|
||||
*
|
||||
* @author chenx
|
||||
* @date 2026-10-11
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "widget_school_setting",
|
||||
uniqueConstraints = @UniqueConstraint(name = "uk_widget_sch", columnNames = {"widget_id", "sch_id"}))
|
||||
public class WidgetSchoolSetting {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
@Column(name = "widget_id", nullable = false)
|
||||
private Integer widgetId;
|
||||
|
||||
@Column(name = "sch_id", nullable = false)
|
||||
private Integer schId;
|
||||
|
||||
/** NULL = 沿用全局 enabled */
|
||||
@Column(name = "enabled")
|
||||
private Integer enabled;
|
||||
|
||||
/** NULL = 沿用全局 role_types */
|
||||
@Column(name = "role_types", length = 16)
|
||||
private String roleTypes;
|
||||
|
||||
/** NULL = 沿用全局 sort */
|
||||
@Column(name = "sort_no")
|
||||
private Integer sortNo;
|
||||
|
||||
/** 本校别名,NULL = 沿用全局 name */
|
||||
@Column(name = "alias", length = 64)
|
||||
private String alias;
|
||||
|
||||
@Column(name = "update_time")
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.yunhai.userhome.repository;
|
||||
|
||||
import com.yunhai.userhome.entity.AppCategory;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AppCategoryRepository extends JpaRepository<AppCategory, Integer>, JpaSpecificationExecutor<AppCategory> {
|
||||
|
||||
List<AppCategory> findAllByRoleType(Integer roleType);
|
||||
}
|
||||
@@ -3,6 +3,8 @@ 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.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
@@ -17,4 +19,16 @@ public interface WidgetCatalogRepository extends JpaRepository<WidgetCatalog, In
|
||||
List<WidgetCatalog> findByEnabledOrderBySortAscIdAsc(Integer enabled);
|
||||
|
||||
List<WidgetCatalog> findAllByOrderBySortAscIdAsc();
|
||||
|
||||
/**
|
||||
* 某校「可见范围命中」的目录行(不含启用状态、角色过滤与校级覆盖,由服务层合成)。
|
||||
* 一处 SQL 决定 scope_type 语义:0 全放;1 仅点名表命中;2 仅点名表未命中。
|
||||
*/
|
||||
@Query("select w from WidgetCatalog w where coalesce(w.scopeType, 0) = 0 "
|
||||
+ "or (coalesce(w.scopeType, 0) = 1 and exists "
|
||||
+ "(select 1 from WidgetCatalogSchool s where s.widgetId = w.id and s.schId = :schId)) "
|
||||
+ "or (coalesce(w.scopeType, 0) = 2 and not exists "
|
||||
+ "(select 1 from WidgetCatalogSchool s where s.widgetId = w.id and s.schId = :schId)) "
|
||||
+ "order by w.sort asc, w.id asc")
|
||||
List<WidgetCatalog> findVisibleForSchool(@Param("schId") Integer schId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.yunhai.userhome.repository;
|
||||
|
||||
import com.yunhai.userhome.entity.WidgetCatalogSchool;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 小组件可见范围点名表访问。
|
||||
*
|
||||
* @author chenx
|
||||
* @date 2026-10-11
|
||||
*/
|
||||
@Repository
|
||||
public interface WidgetCatalogSchoolRepository extends JpaRepository<WidgetCatalogSchool, Integer> {
|
||||
|
||||
/** 命中判定:某组件是否点名了某校(scope_type 白/黑名单共用) */
|
||||
boolean existsByWidgetIdAndSchId(Integer widgetId, Integer schId);
|
||||
|
||||
List<WidgetCatalogSchool> findByWidgetId(Integer widgetId);
|
||||
|
||||
List<WidgetCatalogSchool> findByWidgetIdIn(Collection<Integer> widgetIds);
|
||||
|
||||
void deleteByWidgetId(Integer widgetId);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.yunhai.userhome.repository;
|
||||
|
||||
import com.yunhai.userhome.entity.WidgetSchoolSetting;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 小组件校级覆盖层访问。
|
||||
*
|
||||
* @author chenx
|
||||
* @date 2026-10-11
|
||||
*/
|
||||
@Repository
|
||||
public interface WidgetSchoolSettingRepository extends JpaRepository<WidgetSchoolSetting, Integer> {
|
||||
|
||||
Optional<WidgetSchoolSetting> findByWidgetIdAndSchId(Integer widgetId, Integer schId);
|
||||
|
||||
List<WidgetSchoolSetting> findBySchId(Integer schId);
|
||||
|
||||
List<WidgetSchoolSetting> findByWidgetId(Integer widgetId);
|
||||
|
||||
void deleteByWidgetId(Integer widgetId);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.yunhai.userhome.service;
|
||||
|
||||
import com.yunhai.userhome.entity.AppCategory;
|
||||
import org.springframework.data.domain.Page;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface IAppCategoryService {
|
||||
|
||||
void save(AppCategory appCategory);
|
||||
|
||||
void deleteById(Integer id);
|
||||
|
||||
Optional<AppCategory> findById(Integer id);
|
||||
|
||||
List<AppCategory> findByRoleType(Integer roleType);
|
||||
|
||||
Page<AppCategory> findPageList(Integer page, Integer count, Integer roleType, String name);
|
||||
|
||||
void updateResDate(Integer id);
|
||||
}
|
||||
@@ -1,22 +1,83 @@
|
||||
package com.yunhai.userhome.service;
|
||||
|
||||
import com.yunhai.userhome.entity.WidgetCatalog;
|
||||
import com.yunhai.userhome.entity.WidgetSchoolSetting;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 小组件候选池服务。
|
||||
*
|
||||
* @author chenx
|
||||
* @date 2026-10-11
|
||||
*/
|
||||
public interface IWidgetCatalogService {
|
||||
|
||||
/** 候选池(登录侧):enabled=1 且对本校可见,按 sort 升序 */
|
||||
List<WidgetCatalog> findEnabledForSchool(Integer schId);
|
||||
/**
|
||||
* 候选池(登录侧):可见范围命中 + 生效启用 + 角色命中,按生效 sort 升序。
|
||||
*
|
||||
* @param schId 当前登录校(用于 scope_type 点名判定与校级覆盖合成)
|
||||
* @param roleId 当前登录角色 RoleTypeEnum(1 教师 / 2 校领导 / 3 学生),用于 role_types 过滤
|
||||
* @return 生效值视图(name 已按「校级别名 > 角色命名 > 名称」合成)
|
||||
*/
|
||||
List<WidgetCatalog> findEnabledForSchool(Integer schId, Integer roleId);
|
||||
|
||||
Optional<WidgetCatalog> findByCode(String code);
|
||||
|
||||
/** 下发范围判定:sch_id 为 NULL = 全校可见 */
|
||||
/**
|
||||
* 可见范围判定:scope_type = 0 全放;1 仅点名表命中可见;2 仅点名表未命中可见。
|
||||
*
|
||||
* @param catalog 目录行
|
||||
* @param schId 当前登录校
|
||||
* @return 命中返回 true
|
||||
*/
|
||||
boolean visibleToSchool(WidgetCatalog catalog, Integer schId);
|
||||
|
||||
/** 管理端列表:全量(含 content 与 update_time),鉴权在 Controller */
|
||||
/** 管理端列表(全局原值,含 scopeSchIds 回显),鉴权在 Controller */
|
||||
List<WidgetCatalog> findAllForAdmin();
|
||||
|
||||
/**
|
||||
* 管理端按视角取列表:超管 = 全局原值;校管 = 本校生效值 + 别名 + 原名 + 覆盖标记。
|
||||
*
|
||||
* @param viewerSchId 访问者当前登录校
|
||||
* @param superAdmin 访问者是否超管(BoaUser.type == 1)
|
||||
* @return 视角化的目录列表
|
||||
*/
|
||||
List<WidgetCatalog> findEffectiveForAdmin(Integer viewerSchId, boolean superAdmin);
|
||||
|
||||
/**
|
||||
* 保存目录行(仅超管调用),并整体重写 scope_type 的点名表。
|
||||
*
|
||||
* @param model 目录行(scopeSchIds 为点名的学校列表)
|
||||
*/
|
||||
void save(WidgetCatalog model);
|
||||
|
||||
/**
|
||||
* 删除目录行,连带删点名表与校级覆盖行。
|
||||
*
|
||||
* @param id 目录 id
|
||||
*/
|
||||
void deleteById(Integer id);
|
||||
|
||||
Optional<WidgetSchoolSetting> findSchoolSetting(Integer widgetId, Integer schId);
|
||||
|
||||
/**
|
||||
* 保存校级覆盖(仅本校)。schId 一律取登录校,忽略入参里的 schId,防越权。
|
||||
* 四个可覆盖字段全空表示「清除覆盖」,直接删行。
|
||||
*
|
||||
* @param model 覆盖行(widgetId 必填)
|
||||
* @param currentSchId 当前登录校
|
||||
*/
|
||||
void saveSchoolSetting(WidgetSchoolSetting model, Integer currentSchId);
|
||||
|
||||
/**
|
||||
* 显示名三级链:校级别名 > 角色专属命名 > 组件名称。
|
||||
*
|
||||
* @param catalog 目录行
|
||||
* @param schId 当前登录校
|
||||
* @param roleId 当前登录角色
|
||||
* @return 最终显示名
|
||||
*/
|
||||
String displayName(WidgetCatalog catalog, Integer schId, Integer roleId);
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
package com.yunhai.userhome.service.impl;
|
||||
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.userhome.entity.AppCategory;
|
||||
import com.yunhai.userhome.repository.AppCategoryRepository;
|
||||
import com.yunhai.userhome.service.IAppCategoryService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.persistence.criteria.Predicate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 应用类别服务(自 systemsetting AppCategoryService 1:1 迁移,GetAppCategorySel/getTimestampByIds 无消费方不迁)。
|
||||
*/
|
||||
@Service
|
||||
public class AppCategoryServiceImpl implements IAppCategoryService {
|
||||
|
||||
@Autowired
|
||||
private AppCategoryRepository appCategoryRepository;
|
||||
|
||||
@Override
|
||||
public void save(AppCategory appCategory) {
|
||||
appCategoryRepository.save(appCategory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(Integer id) {
|
||||
if (!appCategoryRepository.existsById(id)) {
|
||||
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
appCategoryRepository.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AppCategory> findById(Integer id) {
|
||||
return appCategoryRepository.findById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AppCategory> findByRoleType(Integer roleType) {
|
||||
return appCategoryRepository.findAllByRoleType(roleType);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
@Override
|
||||
public Page<AppCategory> findPageList(Integer page, Integer count, Integer roleType, String name) {
|
||||
Sort sort = Sort.by(Sort.Direction.DESC, "id");
|
||||
Pageable pageable = PageRequest.of(page - 1, count, sort);
|
||||
Specification specification = (Specification) (root, criteriaQuery, criteriaBuilder) -> {
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
if (roleType != null) {
|
||||
predicates.add(criteriaBuilder.equal(root.get("roleType"), roleType));
|
||||
}
|
||||
if (name != null && !name.trim().isEmpty()) {
|
||||
predicates.add(criteriaBuilder.like(root.get("name"), "%" + name + "%"));
|
||||
}
|
||||
return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
|
||||
};
|
||||
return appCategoryRepository.findAll(specification, pageable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateResDate(Integer id) {
|
||||
Optional<AppCategory> optionalAppCategory = findById(id);
|
||||
if (!optionalAppCategory.isPresent()) {
|
||||
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
AppCategory model = optionalAppCategory.get();
|
||||
model.setResDate(new Date());
|
||||
appCategoryRepository.save(model);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,76 @@
|
||||
package com.yunhai.userhome.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.userhome.entity.WidgetCatalog;
|
||||
import com.yunhai.userhome.entity.WidgetCatalogSchool;
|
||||
import com.yunhai.userhome.entity.WidgetSchoolSetting;
|
||||
import com.yunhai.userhome.repository.WidgetCatalogRepository;
|
||||
import com.yunhai.userhome.repository.WidgetCatalogSchoolRepository;
|
||||
import com.yunhai.userhome.repository.WidgetSchoolSettingRepository;
|
||||
import com.yunhai.userhome.service.IWidgetCatalogService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 小组件候选池服务(对齐 IUserSettingsService 分层:Controller 只做鉴权与出参组装,规则在此)。
|
||||
*
|
||||
* <p>生效值合成口径:生效值 = COALESCE(校级覆盖行, 全局值);显示名 = 校级别名 > 角色专属命名 > 组件名称;
|
||||
* 可见性 = 生效启用 AND 可见范围命中 AND 可见角色命中。</p>
|
||||
*/
|
||||
@Service
|
||||
public class WidgetCatalogServiceImpl implements IWidgetCatalogService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WidgetCatalogServiceImpl.class);
|
||||
|
||||
private static final int COMP_TYPE_BUILTIN = 1;
|
||||
|
||||
private static final int COMP_TYPE_FRAGMENT = 2;
|
||||
|
||||
private static final int ENABLED = 1;
|
||||
|
||||
/** 下发范围:全部学校 */
|
||||
private static final int SCOPE_ALL = 0;
|
||||
|
||||
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
|
||||
|
||||
@Autowired
|
||||
private WidgetCatalogRepository widgetCatalogRepository;
|
||||
|
||||
@Autowired
|
||||
private WidgetCatalogSchoolRepository widgetCatalogSchoolRepository;
|
||||
|
||||
@Autowired
|
||||
private WidgetSchoolSettingRepository widgetSchoolSettingRepository;
|
||||
|
||||
@Override
|
||||
public List<WidgetCatalog> findEnabledForSchool(Integer schId) {
|
||||
return widgetCatalogRepository.findByEnabledOrderBySortAscIdAsc(ENABLED).stream()
|
||||
.filter(model -> visibleToSchool(model, schId))
|
||||
.collect(Collectors.toList());
|
||||
public List<WidgetCatalog> findEnabledForSchool(Integer schId, Integer roleId) {
|
||||
Map<Integer, WidgetSchoolSetting> overrideMap = loadOverrideMap(schId);
|
||||
List<WidgetCatalog> result = new ArrayList<>();
|
||||
for (WidgetCatalog catalog : widgetCatalogRepository.findVisibleForSchool(schId)) {
|
||||
WidgetSchoolSetting override = overrideMap.get(catalog.getId());
|
||||
if (effectiveEnabled(catalog, override) != ENABLED) continue;
|
||||
if (!roleHit(catalog, override, roleId)) continue;
|
||||
result.add(effectiveCopy(catalog, override, schId, roleId));
|
||||
}
|
||||
result.sort(Comparator.comparing(WidgetCatalog::getSort, Comparator.nullsFirst(Integer::compareTo))
|
||||
.thenComparing(WidgetCatalog::getId, Comparator.nullsFirst(Integer::compareTo)));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -45,15 +83,56 @@ public class WidgetCatalogServiceImpl implements IWidgetCatalogService {
|
||||
|
||||
@Override
|
||||
public boolean visibleToSchool(WidgetCatalog catalog, Integer schId) {
|
||||
return catalog.getSchId() == null || catalog.getSchId().equals(schId);
|
||||
int scopeType = catalog.getScopeType() == null ? SCOPE_ALL : catalog.getScopeType();
|
||||
if (scopeType == SCOPE_ALL) {
|
||||
return true;
|
||||
}
|
||||
boolean pointed = schId != null
|
||||
&& widgetCatalogSchoolRepository.existsByWidgetIdAndSchId(catalog.getId(), schId);
|
||||
// 1 = 白名单(点名才可见);2 = 黑名单(点名不可见)
|
||||
return scopeType == 1 ? pointed : !pointed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<WidgetCatalog> findAllForAdmin() {
|
||||
return widgetCatalogRepository.findAllByOrderBySortAscIdAsc();
|
||||
List<WidgetCatalog> list = widgetCatalogRepository.findAllByOrderBySortAscIdAsc();
|
||||
fillScopeSchIds(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<WidgetCatalog> findEffectiveForAdmin(Integer viewerSchId, boolean superAdmin) {
|
||||
if (superAdmin || viewerSchId == null) {
|
||||
return findAllForAdmin();
|
||||
}
|
||||
Map<Integer, WidgetSchoolSetting> overrideMap = loadOverrideMap(viewerSchId);
|
||||
List<WidgetCatalog> result = new ArrayList<>();
|
||||
for (WidgetCatalog catalog : widgetCatalogRepository.findVisibleForSchool(viewerSchId)) {
|
||||
WidgetSchoolSetting override = overrideMap.get(catalog.getId());
|
||||
WidgetCatalog item = new WidgetCatalog();
|
||||
item.setId(catalog.getId());
|
||||
item.setCode(catalog.getCode());
|
||||
item.setName(effectiveName(catalog, override, null));
|
||||
item.setOriginalName(catalog.getName());
|
||||
item.setAlias(override == null ? null : override.getAlias());
|
||||
item.setCovered(override != null);
|
||||
item.setCompType(catalog.getCompType());
|
||||
item.setContent(catalog.getContent());
|
||||
item.setDataCode(catalog.getDataCode());
|
||||
item.setScopeType(catalog.getScopeType());
|
||||
item.setRoleTypes(effectiveRoleTypes(catalog, override));
|
||||
item.setRoleAlias(catalog.getRoleAlias());
|
||||
item.setSort(effectiveSort(catalog, override));
|
||||
item.setEnabled(effectiveEnabled(catalog, override));
|
||||
item.setUpdateTime(resolveUpdateTime(catalog, override));
|
||||
result.add(item);
|
||||
}
|
||||
fillScopeSchIds(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void save(WidgetCatalog model) {
|
||||
if (model.getCode() == null || model.getCode().trim().isEmpty()) {
|
||||
throw new BizException(ErrorCode.CUSTOM, "缺少组件键 code");
|
||||
@@ -69,11 +148,238 @@ public class WidgetCatalogServiceImpl implements IWidgetCatalogService {
|
||||
if (exist.isPresent() && !exist.get().getId().equals(model.getId())) {
|
||||
throw new BizException(ErrorCode.CUSTOM, "组件键已存在:" + model.getCode());
|
||||
}
|
||||
int scopeType = model.getScopeType() == null ? SCOPE_ALL : model.getScopeType();
|
||||
if (scopeType != SCOPE_ALL && scopeType != 1 && scopeType != 2) {
|
||||
throw new BizException(ErrorCode.CUSTOM, "可见范围只能是 全部学校(0) / 指定可见(1) / 指定不可见(2)");
|
||||
}
|
||||
model.setScopeType(scopeType);
|
||||
if (scopeType != SCOPE_ALL && (model.getScopeSchIds() == null || model.getScopeSchIds().isEmpty())) {
|
||||
throw new BizException(ErrorCode.CUSTOM, "请选择学校后再保存可见范围");
|
||||
}
|
||||
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);
|
||||
rewritePointSchools(model);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void deleteById(Integer id) {
|
||||
if (id == null) {
|
||||
throw new BizException(ErrorCode.CUSTOM, "缺少组件 id");
|
||||
}
|
||||
widgetCatalogSchoolRepository.deleteByWidgetId(id);
|
||||
widgetSchoolSettingRepository.deleteByWidgetId(id);
|
||||
widgetCatalogRepository.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<WidgetSchoolSetting> findSchoolSetting(Integer widgetId, Integer schId) {
|
||||
if (widgetId == null || schId == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return widgetSchoolSettingRepository.findByWidgetIdAndSchId(widgetId, schId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void saveSchoolSetting(WidgetSchoolSetting model, Integer currentSchId) {
|
||||
if (model == null || model.getWidgetId() == null) {
|
||||
throw new BizException(ErrorCode.CUSTOM, "缺少组件 id");
|
||||
}
|
||||
if (currentSchId == null) {
|
||||
throw new BizException(ErrorCode.CUSTOM, "当前登录身份没有学校,无法保存本校设置");
|
||||
}
|
||||
// 防越权:schId 一律取当前登录校,不采信前端传参
|
||||
model.setSchId(currentSchId);
|
||||
if (!widgetCatalogRepository.findById(model.getWidgetId()).isPresent()) {
|
||||
throw new BizException(ErrorCode.CUSTOM, "组件不存在:" + model.getWidgetId());
|
||||
}
|
||||
if (!StringUtils.hasText(model.getRoleTypes())) model.setRoleTypes(null);
|
||||
if (!StringUtils.hasText(model.getAlias())) model.setAlias(null);
|
||||
WidgetSchoolSetting exist = widgetSchoolSettingRepository
|
||||
.findByWidgetIdAndSchId(model.getWidgetId(), currentSchId).orElse(null);
|
||||
// 四个可覆盖字段全空 = 清除覆盖(删行,恢复沿用全局)
|
||||
boolean clearing = model.getEnabled() == null && model.getRoleTypes() == null
|
||||
&& model.getSortNo() == null && model.getAlias() == null;
|
||||
if (clearing) {
|
||||
if (exist != null) widgetSchoolSettingRepository.delete(exist);
|
||||
return;
|
||||
}
|
||||
if (exist != null) model.setId(exist.getId());
|
||||
model.setUpdateTime(new Date());
|
||||
widgetSchoolSettingRepository.save(model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String displayName(WidgetCatalog catalog, Integer schId, Integer roleId) {
|
||||
WidgetSchoolSetting override = null;
|
||||
if (schId != null && catalog.getId() != null) {
|
||||
override = widgetSchoolSettingRepository
|
||||
.findByWidgetIdAndSchId(catalog.getId(), schId).orElse(null);
|
||||
}
|
||||
return effectiveName(catalog, override, roleId);
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
/** 显示名三级链:校级别名 > 角色专属命名 > 组件名称 */
|
||||
private String effectiveName(WidgetCatalog catalog, WidgetSchoolSetting override, Integer roleId) {
|
||||
if (override != null && StringUtils.hasText(override.getAlias())) {
|
||||
return override.getAlias();
|
||||
}
|
||||
String aliasByRole = roleAliasOf(catalog.getRoleAlias(), roleId);
|
||||
if (StringUtils.hasText(aliasByRole)) {
|
||||
return aliasByRole;
|
||||
}
|
||||
return catalog.getName();
|
||||
}
|
||||
|
||||
/** 角色专属命名读取:JSON 文本里取当前角色那一项;解析失败按「无角色命名」降级,不抛 500 */
|
||||
private String roleAliasOf(String roleAliasJson, Integer roleId) {
|
||||
if (!StringUtils.hasText(roleAliasJson) || roleId == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
JsonNode value = JSON_MAPPER.readTree(roleAliasJson).get(String.valueOf(roleId));
|
||||
return value == null || value.isNull() ? null : value.asText();
|
||||
} catch (Exception e) {
|
||||
log.warn("[widget] role_alias 解析失败,按无角色命名降级:{}", roleAliasJson);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean roleHit(WidgetCatalog catalog, WidgetSchoolSetting override, Integer roleId) {
|
||||
String roleTypes = effectiveRoleTypes(catalog, override);
|
||||
if (!StringUtils.hasText(roleTypes)) {
|
||||
return true;
|
||||
}
|
||||
if (roleId == null) {
|
||||
return false;
|
||||
}
|
||||
for (String token : roleTypes.split(",")) {
|
||||
if (roleId.equals(parseInt(token))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String effectiveRoleTypes(WidgetCatalog catalog, WidgetSchoolSetting override) {
|
||||
if (override != null && StringUtils.hasText(override.getRoleTypes())) {
|
||||
return override.getRoleTypes();
|
||||
}
|
||||
return catalog.getRoleTypes();
|
||||
}
|
||||
|
||||
private int effectiveEnabled(WidgetCatalog catalog, WidgetSchoolSetting override) {
|
||||
if (override != null && override.getEnabled() != null) {
|
||||
return override.getEnabled();
|
||||
}
|
||||
return catalog.getEnabled() == null ? ENABLED : catalog.getEnabled();
|
||||
}
|
||||
|
||||
private int effectiveSort(WidgetCatalog catalog, WidgetSchoolSetting override) {
|
||||
if (override != null && override.getSortNo() != null) {
|
||||
return override.getSortNo();
|
||||
}
|
||||
return catalog.getSort() == null ? 0 : catalog.getSort();
|
||||
}
|
||||
|
||||
private Date resolveUpdateTime(WidgetCatalog catalog, WidgetSchoolSetting override) {
|
||||
if (override != null && override.getUpdateTime() != null) {
|
||||
return override.getUpdateTime();
|
||||
}
|
||||
return catalog.getUpdateTime();
|
||||
}
|
||||
|
||||
/** 下发视图:复制出独立对象并写入生效值,避免改动 JPA 托管实体(脏检查) */
|
||||
private WidgetCatalog effectiveCopy(WidgetCatalog catalog, WidgetSchoolSetting override,
|
||||
Integer schId, Integer roleId) {
|
||||
WidgetCatalog item = new WidgetCatalog();
|
||||
item.setId(catalog.getId());
|
||||
item.setCode(catalog.getCode());
|
||||
item.setName(effectiveName(catalog, override, roleId));
|
||||
item.setCompType(catalog.getCompType());
|
||||
item.setContent(catalog.getContent());
|
||||
item.setDataCode(catalog.getDataCode());
|
||||
item.setScopeType(catalog.getScopeType());
|
||||
item.setRoleTypes(effectiveRoleTypes(catalog, override));
|
||||
item.setRoleAlias(catalog.getRoleAlias());
|
||||
item.setSort(effectiveSort(catalog, override));
|
||||
item.setEnabled(effectiveEnabled(catalog, override));
|
||||
item.setUpdateTime(resolveUpdateTime(catalog, override));
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<Integer, WidgetSchoolSetting> loadOverrideMap(Integer schId) {
|
||||
Map<Integer, WidgetSchoolSetting> map = new HashMap<>();
|
||||
if (schId == null) {
|
||||
return map;
|
||||
}
|
||||
for (WidgetSchoolSetting setting : widgetSchoolSettingRepository.findBySchId(schId)) {
|
||||
map.put(setting.getWidgetId(), setting);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private void fillScopeSchIds(Collection<WidgetCatalog> catalogs) {
|
||||
Map<Integer, List<Integer>> pointMap = loadPointSchoolMap(catalogs);
|
||||
for (WidgetCatalog catalog : catalogs) {
|
||||
List<Integer> ids = pointMap.get(catalog.getId());
|
||||
catalog.setScopeSchIds(ids == null ? new ArrayList<>() : ids);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Integer, List<Integer>> loadPointSchoolMap(Collection<WidgetCatalog> catalogs) {
|
||||
Map<Integer, List<Integer>> map = new HashMap<>();
|
||||
List<Integer> widgetIds = new ArrayList<>();
|
||||
for (WidgetCatalog catalog : catalogs) {
|
||||
if (catalog.getId() != null) widgetIds.add(catalog.getId());
|
||||
}
|
||||
if (widgetIds.isEmpty()) {
|
||||
return map;
|
||||
}
|
||||
for (WidgetCatalogSchool row : widgetCatalogSchoolRepository.findByWidgetIdIn(widgetIds)) {
|
||||
map.computeIfAbsent(row.getWidgetId(), key -> new ArrayList<>()).add(row.getSchId());
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/** 整体重写点名表:scope_type=0 或未选校时清空 */
|
||||
private void rewritePointSchools(WidgetCatalog model) {
|
||||
widgetCatalogSchoolRepository.deleteByWidgetId(model.getId());
|
||||
if (model.getScopeType() == null || model.getScopeType() == SCOPE_ALL) {
|
||||
return;
|
||||
}
|
||||
List<Integer> schIds = model.getScopeSchIds();
|
||||
if (schIds == null || schIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<WidgetCatalogSchool> rows = new ArrayList<>();
|
||||
for (Integer schId : schIds) {
|
||||
if (schId == null) continue;
|
||||
WidgetCatalogSchool row = new WidgetCatalogSchool();
|
||||
row.setWidgetId(model.getId());
|
||||
row.setSchId(schId);
|
||||
rows.add(row);
|
||||
}
|
||||
if (!rows.isEmpty()) {
|
||||
widgetCatalogSchoolRepository.saveAll(rows);
|
||||
}
|
||||
}
|
||||
|
||||
private Integer parseInt(String text) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Integer.valueOf(text.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
-- ============================================================
|
||||
-- 小组件管理改版 v2(doc/小组件管理改版-执行方案.md §3)
|
||||
-- dev 由 JPA ddl-auto=update 依实体自动加列/建表;本文件供关闭 ddl-auto 的测试/生产环境手工执行一次。
|
||||
-- 幂等性:CREATE TABLE 用 IF NOT EXISTS;ALTER ADD COLUMN 在 MySQL 8 无 IF NOT EXISTS,
|
||||
-- 重复执行会报 1060 Duplicate column name,属预期,按需手动跳过即可。
|
||||
-- ============================================================
|
||||
|
||||
-- ---------- 1. widget_catalog 加 3 列 ----------
|
||||
ALTER TABLE `widget_catalog`
|
||||
ADD COLUMN `scope_type` TINYINT NOT NULL DEFAULT 0
|
||||
COMMENT '下发范围:0=全部学校 1=白名单(仅点名的学校可见) 2=黑名单(点名的学校不可见)',
|
||||
ADD COLUMN `role_types` VARCHAR(16) NULL
|
||||
COMMENT '可见角色:逗号分隔 RoleTypeEnum(1教师/2校领导/3学生);NULL/空=全角色',
|
||||
ADD COLUMN `role_alias` VARCHAR(255) NULL
|
||||
COMMENT '角色专属命名(JSON 文本):{"2":"全校课表"},键=RoleTypeEnum;读侧按当前角色取值';
|
||||
|
||||
-- sch_id 旧语义(NULL=全校 / 单校等值)由 scope_type 取代,标记弃用:列保留以兼容存量数据,读侧不再使用。
|
||||
|
||||
-- ---------- 2. 新增点名表(配合 scope_type 白/黑名单) ----------
|
||||
CREATE TABLE IF NOT EXISTS `widget_catalog_school` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`widget_id` INT NOT NULL COMMENT 'widget_catalog.id',
|
||||
`sch_id` INT NOT NULL COMMENT '被点名的学校',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_widget_school` (`widget_id`, `sch_id`)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COMMENT = '小组件可见范围点名表(配合 scope_type 白/黑名单)';
|
||||
|
||||
-- ---------- 3. 新增校级覆盖层 ----------
|
||||
CREATE TABLE IF NOT EXISTS `widget_school_setting` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`widget_id` INT NOT NULL,
|
||||
`sch_id` INT NOT NULL,
|
||||
`enabled` TINYINT NULL COMMENT 'NULL=沿用全局',
|
||||
`role_types` VARCHAR(16) NULL COMMENT 'NULL=沿用全局',
|
||||
`sort_no` INT NULL COMMENT 'NULL=沿用全局',
|
||||
`alias` VARCHAR(64) NULL COMMENT '本校别名,NULL=沿用全局 name',
|
||||
`update_time` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_widget_sch` (`widget_id`, `sch_id`)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COMMENT = '小组件校级覆盖(一键清除=删行)';
|
||||
|
||||
-- 存量 3 条内置目录行的 scope_type / role_types / role_alias 保持默认(0 / NULL / NULL)即「全部学校 + 全角色」。
|
||||
|
||||
-- 回滚:
|
||||
-- ALTER TABLE `widget_catalog` DROP COLUMN `scope_type`, DROP COLUMN `role_types`, DROP COLUMN `role_alias`;
|
||||
-- DROP TABLE IF EXISTS `widget_catalog_school`;
|
||||
-- DROP TABLE IF EXISTS `widget_school_setting`;
|
||||
Reference in New Issue
Block a user