diff --git a/sql/20260915_add_teacher_roster_menu.sql b/sql/20260915_add_teacher_roster_menu.sql new file mode 100644 index 0000000..7cc928f --- /dev/null +++ b/sql/20260915_add_teacher_roster_menu.sql @@ -0,0 +1,50 @@ +-- ============================================================================ +-- 教师名册:后台菜单与按钮权限 +-- 参见 .scratch/openplat-sync/issues/19-admin-teacher-roster.md +-- 可重复执行 +-- +-- 说明:后台菜单是动态从 sys_menu 读取的(GET /auth/user -> formatMenus -> addRoute)。 +-- menu_type=1 的记录不生成路由,只作按钮权限。 +-- path 与 component 填相同值,组件按 import.meta.glob('/src/views/**/index.vue') 解析。 +-- +-- 教师名册只是查看(分页 + 导出),没有任何写操作,因此按钮只有查询与导出两个。 +-- ============================================================================ + +-- 菜单:教师名册 +INSERT INTO `sys_menu` + (`menu_id`, `parent_id`, `title`, `path`, `component`, `menu_type`, `sort_number`, + `authority`, `hide`, `app_id`, `deleted`, `merchant_code`, `tenant_id`) +SELECT 14405, 8574, '教师名册', '/gxmu/teacherRoster', '/gxmu/teacherRoster', 0, 106, + '', 0, 0, 0, '', 10049 +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` m WHERE m.`menu_id` = 14405); + +-- 按钮:名册查询 +INSERT INTO `sys_menu` + (`menu_id`, `parent_id`, `title`, `path`, `component`, `menu_type`, `sort_number`, + `authority`, `hide`, `app_id`, `deleted`, `merchant_code`, `tenant_id`) +SELECT 14406, 14405, '名册查询', '', '', 1, 1, + 'gxmu:teacher:list', 0, 0, 0, '', 10049 +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` m WHERE m.`menu_id` = 14406); + +-- 按钮:名册导出(泄露风险最高,单独授权) +INSERT INTO `sys_menu` + (`menu_id`, `parent_id`, `title`, `path`, `component`, `menu_type`, `sort_number`, + `authority`, `hide`, `app_id`, `deleted`, `merchant_code`, `tenant_id`) +SELECT 14407, 14405, '名册导出', '', '', 1, 2, + 'gxmu:teacher:export', 0, 0, 0, '', 10049 +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` m WHERE m.`menu_id` = 14407); + +-- --------------------------------------------------------------------------- +-- 给超级管理员授权(其它角色请按需在后台「角色管理」里勾选) +-- --------------------------------------------------------------------------- +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`, `tenant_id`) +SELECT r.`role_id`, m.`menu_id`, 10049 +FROM `sys_role` r +JOIN `sys_menu` m ON m.`menu_id` IN (14405, 14406, 14407) +LEFT JOIN `sys_role_menu` rm ON rm.`role_id` = r.`role_id` AND rm.`menu_id` = m.`menu_id` +WHERE r.`role_code` = 'superAdmin' + AND r.`tenant_id` = 10049 + AND rm.`id` IS NULL; diff --git a/src/main/java/com/gxwebsoft/gxmu/openplat/controller/GxmuTeacherController.java b/src/main/java/com/gxwebsoft/gxmu/openplat/controller/GxmuTeacherController.java new file mode 100644 index 0000000..b8e0092 --- /dev/null +++ b/src/main/java/com/gxwebsoft/gxmu/openplat/controller/GxmuTeacherController.java @@ -0,0 +1,118 @@ +package com.gxwebsoft.gxmu.openplat.controller; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.gxwebsoft.common.core.annotation.OperationLog; +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.BaseController; +import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.gxmu.openplat.SyncSupport; +import com.gxwebsoft.gxmu.openplat.entity.GxmuTeacher; +import com.gxwebsoft.gxmu.openplat.mapper.GxmuTeacherMapper; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +/** + * 教师名册控制器(只读)。 + * + *
教师名册来自开放平台全量同步,只用于检索与选人,教师本身不建登录账号(见 ADR-0001)。 + * 因此这里没有认领类接口,也没有可写字段。 + * + *
手机号按 spec §5.2「落盘但不进任何列表接口」处理:{@link #hideSensitive} 在返回前一律清空,
+ * 明文不出本控制器。
+ *
+ * @author Codex
+ * @since 2026-09-15
+ */
+@Api(tags = "教师名册")
+@RestController
+@RequestMapping("/api/gxmu/teacher")
+public class GxmuTeacherController extends BaseController {
+
+ @Resource
+ private GxmuTeacherMapper teacherMapper;
+
+ @Resource
+ private SyncSupport syncSupport;
+
+ @PreAuthorize("hasAuthority('gxmu:teacher:list')")
+ @ApiOperation("教师名册分页查询")
+ @GetMapping("/page")
+ public ApiResult 包级可见是为了让单元测试能直接钉住这条规则。
+ */
+ static void hideSensitive(List 教师手机号是落盘但「不进任何列表接口」的字段。名册页是本项目里第一个把
+ * {@link GxmuTeacher} 直接序列化出去的地方,一旦漏掉清空,1.7 万条手机号就会随
+ * 分页接口泄露——这类事故从代码评审里看不出来,所以直接钉在测试里。
+ *
+ * @author Codex
+ * @since 2026-09-15
+ */
+class GxmuTeacherControllerTest {
+
+ private static GxmuTeacher sample(String jgh, String sjh) {
+ GxmuTeacher teacher = new GxmuTeacher();
+ teacher.setJgh(jgh);
+ teacher.setXm("张三");
+ teacher.setXbm("1");
+ teacher.setSex("男");
+ teacher.setDwh("010318");
+ teacher.setCollegeId(60);
+ teacher.setKsjybh("01031801");
+ teacher.setSjh(sjh);
+ teacher.setStatus(1);
+ return teacher;
+ }
+
+ @Test
+ void phoneNumberNeverSurvives() {
+ List> export(
+ @RequestParam(value = "collegeId", required = false) Integer collegeId,
+ @RequestParam(value = "status", required = false) Integer status) {
+ List