新增教师名册只读接口

教师名册来自开放平台全量同步,只用于检索与选人(教师不建登录账号,ADR-0001),
因此不提供认领类接口,也没有可写字段。

- GET /api/gxmu/teacher/page:学院/教工号/姓名/状态筛选,默认只返回启用
- POST /api/gxmu/teacher/export:导出用列表,带操作日志
- 权限拆成 gxmu:teacher:list 与 gxmu:teacher:export
- 手机号按 spec §5.2 落盘但不进任何列表接口,返回前一律清空(hideSensitive)
- 菜单与按钮权限见 sql/20260915_add_teacher_roster_menu.sql,可重复执行
This commit is contained in:
2026-09-15 17:21:32 +08:00
parent 582d7dfbcb
commit 853f3c3f5b
3 changed files with 252 additions and 0 deletions
@@ -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;
/**
* 教师名册控制器(只读)。
*
* <p>教师名册来自开放平台全量同步,只用于检索与选人,教师本身不建登录账号(见 ADR-0001)。
* 因此这里没有认领类接口,也没有可写字段。
*
* <p>手机号按 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<PageResult<GxmuTeacher>> page(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "limit", defaultValue = "20") Integer limit,
@RequestParam(value = "collegeId", required = false) Integer collegeId,
@RequestParam(value = "jgh", required = false) String jgh,
@RequestParam(value = "xm", required = false) String xm,
@RequestParam(value = "status", required = false) Integer status) {
LambdaQueryWrapper<GxmuTeacher> wrapper = buildWrapper(collegeId, jgh, xm, status);
Page<GxmuTeacher> pageParam = new Page<>(page, limit);
IPage<GxmuTeacher> result = teacherMapper.selectPage(pageParam, wrapper);
List<GxmuTeacher> records = result.getRecords();
hideSensitive(records);
enrich(records);
return success(new PageResult<>(records, result.getTotal()));
}
@ApiOperation("导出教师名册(不含手机号)")
@PreAuthorize("hasAuthority('gxmu:teacher:export')")
@OperationLog
@PostMapping("/export")
public ApiResult<List<GxmuTeacher>> export(
@RequestParam(value = "collegeId", required = false) Integer collegeId,
@RequestParam(value = "status", required = false) Integer status) {
List<GxmuTeacher> records = teacherMapper.selectList(buildWrapper(collegeId, null, null, status));
hideSensitive(records);
enrich(records);
return success(records);
}
private LambdaQueryWrapper<GxmuTeacher> buildWrapper(Integer collegeId, String jgh, String xm, Integer status) {
LambdaQueryWrapper<GxmuTeacher> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(GxmuTeacher::getTenantId, getTenantId());
wrapper.eq(collegeId != null, GxmuTeacher::getCollegeId, collegeId);
wrapper.eq(StrUtil.isNotBlank(jgh), GxmuTeacher::getJgh, StrUtil.trim(jgh));
wrapper.like(StrUtil.isNotBlank(xm), GxmuTeacher::getXm, StrUtil.trim(xm));
// 默认只看启用(停用=上游已消失,见 ADR-0004,需要时显式查 status=0
wrapper.eq(status != null, GxmuTeacher::getStatus, status);
wrapper.eq(status == null, GxmuTeacher::getStatus, 1);
wrapper.orderByAsc(GxmuTeacher::getCollegeId).orderByAsc(GxmuTeacher::getJgh);
return wrapper;
}
/**
* 手机号绝不随响应返回:置空而不是脱敏,教师名册页也不需要这个字段(spec §5.2)。
*
* <p>包级可见是为了让单元测试能直接钉住这条规则。
*/
static void hideSensitive(List<GxmuTeacher> records) {
for (GxmuTeacher teacher : records) {
teacher.setSjh(null);
}
}
private void enrich(List<GxmuTeacher> records) {
if (records.isEmpty()) {
return;
}
Map<Integer, String> orgNames = syncSupport.orgNameById();
for (GxmuTeacher teacher : records) {
if (teacher.getCollegeId() != null) {
teacher.setCollegeName(orgNames.get(teacher.getCollegeId()));
}
}
}
}
@@ -0,0 +1,84 @@
package com.gxwebsoft.gxmu.openplat.controller;
import com.gxwebsoft.gxmu.openplat.entity.GxmuTeacher;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* 教师名册的敏感字段守卫测试(spec §5.2)。
*
* <p>教师手机号是落盘但「不进任何列表接口」的字段。名册页是本项目里第一个把
* {@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<GxmuTeacher> records = new ArrayList<>(Arrays.asList(
sample("10001", "13800000000"),
sample("10002", "13900000000")));
GxmuTeacherController.hideSensitive(records);
for (GxmuTeacher teacher : records) {
assertNull(teacher.getSjh(), "手机号必须清空后再返回");
}
}
/**
* 清空的是手机号这一列,不是整行——列表页要靠其余字段渲染。
*/
@Test
void otherFieldsAreUntouched() {
List<GxmuTeacher> records = new ArrayList<>(Arrays.asList(sample("10001", "13800000000")));
GxmuTeacherController.hideSensitive(records);
GxmuTeacher teacher = records.get(0);
assertEquals("10001", teacher.getJgh());
assertEquals("张三", teacher.getXm());
assertEquals("", teacher.getSex());
assertEquals(Integer.valueOf(60), teacher.getCollegeId());
assertEquals("01031801", teacher.getKsjybh());
assertEquals(Integer.valueOf(1), teacher.getStatus());
}
@Test
void emptyListIsFine() {
GxmuTeacherController.hideSensitive(new ArrayList<>());
}
@Test
void nullPhoneStaysNull() {
List<GxmuTeacher> records = new ArrayList<>(Arrays.asList(sample("10001", null)));
GxmuTeacherController.hideSensitive(records);
assertNull(records.get(0).getSjh());
}
}