diff --git a/src/main/java/com/yunhai/dormnotice/config/DictSeedRunner.java b/src/main/java/com/yunhai/dormnotice/config/DictSeedRunner.java deleted file mode 100644 index af7c32e..0000000 --- a/src/main/java/com/yunhai/dormnotice/config/DictSeedRunner.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.yunhai.dormnotice.config; - -import com.yunhai.dormnotice.entity.SysDict; -import com.yunhai.dormnotice.repository.SysDictRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -/** - * 字典自举种子:启动时若 sys_dict 缺 dept / noticeType 则补入默认口径(幂等,仅在为空时插)。 - *

解决「发布部门/通知类型下拉为空」——不依赖 DBA 手动跑 dormnotice_init.sql。 - * 生产可用 {@code dormnotice.seed-dict=false} 关闭。

- */ -@Component -public class DictSeedRunner implements ApplicationRunner { - - private static final Logger log = LoggerFactory.getLogger(DictSeedRunner.class); - - private static final String[][] DEPTS = { - {"D001", "校办"}, {"D002", "德育处"}, {"D003", "生活管理科"}, {"D004", "教务处"}, - {"D005", "后勤保障部"}, {"D006", "校医务室"}, {"D007", "武术训练部"}, {"D008", "安全管理办公室"}, - }; - private static final String[][] NOTICE_TYPES = { - {"T001", "作息调整"}, {"T002", "宿舍调整"}, {"T003", "卫生检查"}, {"T004", "违规通报"}, - {"T005", "停水停电"}, {"T006", "安全排查"}, {"T007", "留宿申请"}, {"T008", "家长探视"}, - {"T009", "夜间查寝"}, {"T010", "物品清退"}, {"T011", "疫情防控"}, {"T012", "假期安排"}, {"T013", "其他"}, - }; - - @Autowired - private SysDictRepository sysDictRepository; - - @Value("${dormnotice.seed-dict:true}") - private boolean enabled; - - @Override - @Transactional - public void run(ApplicationArguments args) { - if (!enabled) { - return; - } - seed("dept", DEPTS); - seed("noticeType", NOTICE_TYPES); - } - - private void seed(String type, String[][] items) { - List existing = sysDictRepository.findByDictTypeOrderBySortOrderAsc(type); - if (!existing.isEmpty()) { - log.info("[DictSeed] 字典 {} 已有 {} 条,跳过种子", type, existing.size()); - return; - } - int order = 1; - for (String[] it : items) { - SysDict d = new SysDict(); - d.setDictType(type); - d.setDictKey(it[0]); - d.setDictValue(it[1]); - d.setSortOrder(order++); - d.setEnableFlag(1); - sysDictRepository.save(d); - } - log.info("[DictSeed] 字典 {} 已种入 {} 条", type, items.length); - } -} diff --git a/src/main/java/com/yunhai/dormnotice/entity/SysDict.java b/src/main/java/com/yunhai/dormnotice/entity/SysDict.java index 620d93a..8e523bc 100644 --- a/src/main/java/com/yunhai/dormnotice/entity/SysDict.java +++ b/src/main/java/com/yunhai/dormnotice/entity/SysDict.java @@ -9,7 +9,7 @@ import javax.persistence.Entity; import javax.persistence.Table; /** - * 系统字典(发文部门等)。对应原 .NET Common_DeptDic 表。 + * 系统字典(发文部门 / 通知类型)。按学校隔离(sch_id),对应原 .NET Common_DeptDic 表。 */ @NoArgsConstructor @Data @@ -18,7 +18,7 @@ import javax.persistence.Table; @EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) public class SysDict extends BaseEntity { - /** 字典类型(如 "dept"=发文部门) */ + /** 字典类型("dept"=发文部门 / "noticeType"=通知类型) */ @Column(name = "dict_type", length = 50) private String dictType; @@ -37,4 +37,8 @@ public class SysDict extends BaseEntity { /** 是否启用:0 禁用 / 1 启用(原 Enable) */ @Column(name = "enable_flag") private Integer enableFlag = 1; + + /** 学校ID(多租户隔离;发布/查询按当前学校过滤) */ + @Column(name = "sch_id") + private Integer schId; } diff --git a/src/main/java/com/yunhai/dormnotice/repository/SysDictRepository.java b/src/main/java/com/yunhai/dormnotice/repository/SysDictRepository.java index dceff6b..2437ca2 100644 --- a/src/main/java/com/yunhai/dormnotice/repository/SysDictRepository.java +++ b/src/main/java/com/yunhai/dormnotice/repository/SysDictRepository.java @@ -12,4 +12,11 @@ public interface SysDictRepository extends JpaRepository { /** 按类型+key 取单条字典(发文部门名称解析用) */ Optional findByDictTypeAndDictKey(String dictType, String dictKey); + + /** 学校隔离:按 schId + 类型 查询(schId 传 null 命中 sch_id IS NULL) */ + List findBySchIdAndDictTypeOrderBySortOrderAsc(Integer schId, String dictType); + + List findBySchIdAndDictTypeAndEnableFlagOrderBySortOrderAsc(Integer schId, String dictType, Integer enableFlag); + + Optional findBySchIdAndDictTypeAndDictKey(Integer schId, String dictType, String dictKey); } diff --git a/src/main/java/com/yunhai/dormnotice/service/impl/DictService.java b/src/main/java/com/yunhai/dormnotice/service/impl/DictService.java index 71c4de9..eb1b5a8 100644 --- a/src/main/java/com/yunhai/dormnotice/service/impl/DictService.java +++ b/src/main/java/com/yunhai/dormnotice/service/impl/DictService.java @@ -3,6 +3,7 @@ package com.yunhai.dormnotice.service.impl; import com.yunhai.dormnotice.entity.SysDict; import com.yunhai.dormnotice.repository.SysDictRepository; import com.yunhai.dormnotice.service.IDictService; +import com.yunhai.dormnotice.utils.UserContext; import com.yunhai.platform.apistd.BizException; import com.yunhai.platform.utils.exception.ErrorCode; import org.springframework.beans.factory.annotation.Autowired; @@ -19,7 +20,8 @@ import java.util.stream.Collectors; /** * 系统字典服务(宿舍通知版)。type 维度:dept 发布部门 / noticeType 通知类型。 - * dict_key 存 CodeID(D001/T001…),dict_value 存展示名(Name)。 + *

按学校隔离(sys_dict.sch_id):读写均限当前 {@link UserContext#getSchId()}; + * 某校首次访问某类型且为空时,惰性种入默认口径(部门 8 / 类型 13),各校独立可增删改。

*/ @Service public class DictService implements IDictService { @@ -27,9 +29,24 @@ public class DictService implements IDictService { private static final String DEPT = "dept"; private static final String NOTICE_TYPE = "noticeType"; + /** 默认种子(首次访问按学校惰性写入) */ + private static final String[][] DEPT_SEED = { + {"D001", "校办"}, {"D002", "德育处"}, {"D003", "生活管理科"}, {"D004", "教务处"}, + {"D005", "后勤保障部"}, {"D006", "校医务室"}, {"D007", "武术训练部"}, {"D008", "安全管理办公室"}, + }; + private static final String[][] NOTICE_TYPE_SEED = { + {"T001", "作息调整"}, {"T002", "宿舍调整"}, {"T003", "卫生检查"}, {"T004", "违规通报"}, + {"T005", "停水停电"}, {"T006", "安全排查"}, {"T007", "留宿申请"}, {"T008", "家长探视"}, + {"T009", "夜间查寝"}, {"T010", "物品清退"}, {"T011", "疫情防控"}, {"T012", "假期安排"}, {"T013", "其他"}, + }; + @Autowired private SysDictRepository sysDictRepository; + private Integer currentSchId() { + return UserContext.getSchId(); + } + private String normalizeType(String type) { if (!StringUtils.hasText(type)) { return NOTICE_TYPE; @@ -41,12 +58,34 @@ public class DictService implements IDictService { return DEPT.equals(type) ? "D" : "T"; } + /** 首次访问某校某类型且为空时,种入默认口径(幂等,按 schId)。 */ + @Transactional + public void ensureSeeded(Integer schId, String type) { + if (!sysDictRepository.findBySchIdAndDictTypeOrderBySortOrderAsc(schId, type).isEmpty()) { + return; + } + String[][] seed = DEPT.equals(type) ? DEPT_SEED : NOTICE_TYPE_SEED; + int order = 1; + for (String[] it : seed) { + SysDict d = new SysDict(); + d.setDictType(type); + d.setDictKey(it[0]); + d.setDictValue(it[1]); + d.setSortOrder(order++); + d.setEnableFlag(1); + d.setSchId(schId); + sysDictRepository.save(d); + } + } + // ============================ List(下拉) ============================ @Override public List> list(String type) { String t = normalizeType(type); - List items = sysDictRepository.findByDictTypeAndEnableFlagOrderBySortOrderAsc(t, 1); + Integer schId = currentSchId(); + ensureSeeded(schId, t); + List items = sysDictRepository.findBySchIdAndDictTypeAndEnableFlagOrderBySortOrderAsc(schId, t, 1); List> out = new ArrayList<>(); for (SysDict d : items) { Map m = new LinkedHashMap<>(); @@ -62,7 +101,9 @@ public class DictService implements IDictService { @Override public List> manageList(String type) { String t = normalizeType(type); - return sysDictRepository.findByDictTypeOrderBySortOrderAsc(t).stream().map(d -> { + Integer schId = currentSchId(); + ensureSeeded(schId, t); + return sysDictRepository.findBySchIdAndDictTypeOrderBySortOrderAsc(schId, t).stream().map(d -> { Map m = new LinkedHashMap<>(); m.put("SerID", d.getId()); m.put("CodeID", d.getDictKey()); @@ -82,7 +123,9 @@ public class DictService implements IDictService { } String trimmed = name.trim(); String t = normalizeType(type); - List existing = sysDictRepository.findByDictTypeOrderBySortOrderAsc(t); + Integer schId = currentSchId(); + ensureSeeded(schId, t); + List existing = sysDictRepository.findBySchIdAndDictTypeOrderBySortOrderAsc(schId, t); boolean dup = existing.stream().anyMatch(d -> trimmed.equals(d.getDictValue())); if (dup) { throw new BizException(ErrorCode.FORBIDDEN, NOTICE_TYPE.equals(t) ? "该通知类型已存在" : "已存在该分类,请重新填写!"); @@ -93,6 +136,7 @@ public class DictService implements IDictService { dict.setDictValue(trimmed); dict.setSortOrder(maxSort + 1); dict.setEnableFlag(1); + dict.setSchId(schId); dict.setDictKey(codePrefix(t) + "000"); // 占位,save 后按 id 生成 CodeID SysDict saved = sysDictRepository.save(dict); saved.setDictKey(codePrefix(t) + String.format("%03d", saved.getId())); @@ -109,9 +153,10 @@ public class DictService implements IDictService { } SysDict dict = sysDictRepository.findById(serId) .orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "字典记录不存在")); + assertSameSchool(dict); if (StringUtils.hasText(name)) { String t = normalizeType(type); - boolean dup = sysDictRepository.findByDictTypeOrderBySortOrderAsc(t).stream() + boolean dup = sysDictRepository.findBySchIdAndDictTypeOrderBySortOrderAsc(currentSchId(), t).stream() .anyMatch(d -> !d.getId().equals(serId) && name.trim().equals(d.getDictValue())); if (dup) { throw new BizException(ErrorCode.FORBIDDEN, "已存在该分类,请重新填写!"); @@ -131,7 +176,7 @@ public class DictService implements IDictService { public void delete(Integer serId, String type) { SysDict dict = sysDictRepository.findById(serId) .orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "字典记录不存在")); - // 通知引用校验在 NoticeService 侧按 deptId/noticeType 命中判定;此处直接删字典项。 + assertSameSchool(dict); sysDictRepository.delete(dict); } @@ -149,8 +194,10 @@ public class DictService implements IDictService { for (int id : idArr) { final int order = index--; sysDictRepository.findById(id).ifPresent(d -> { - d.setSortOrder(order); - sysDictRepository.save(d); + if (sameSchool(d)) { + d.setSortOrder(order); + sysDictRepository.save(d); + } }); } } @@ -169,7 +216,27 @@ public class DictService implements IDictService { if (!StringUtils.hasText(deptId)) { return null; } - return sysDictRepository.findByDictTypeAndDictKey(DEPT, deptId.trim()) + Integer schId = currentSchId(); + String name = sysDictRepository.findBySchIdAndDictTypeAndDictKey(schId, DEPT, deptId.trim()) .map(SysDict::getDictValue).orElse(null); + if (name == null) { + name = sysDictRepository.findByDictTypeAndDictKey(DEPT, deptId.trim()) + .map(SysDict::getDictValue).orElse(null); + } + return name; + } + + // ============================ 租户守卫 ============================ + + private boolean sameSchool(SysDict d) { + Integer cur = currentSchId(); + // 无学校上下文(系统/脚本)放行;否则要求同校(历史 sch_id 为 null 的行也放行,避免锁死) + return cur == null || d.getSchId() == null || cur.equals(d.getSchId()); + } + + private void assertSameSchool(SysDict d) { + if (!sameSchool(d)) { + throw new BizException(ErrorCode.FORBIDDEN, "无权操作其他学校的字典"); + } } }