接入医科大开放平台数据同步
新增 com.gxwebsoft.gxmu.openplat:开放平台客户端(oauth 鉴权、token 缓存与失效 重取、分页拉取)、部门/教师/班级/学生四段同步、异步编排与手动同步接口、学生认领、 学生名册查询与导出、每天 03:30 的定时任务、同步记录。 关键设计: - 名册与登录账号分离,学生通过「认领」(手机号自动 / 学号姓名自助)关联 sys_user - 组织数据以 sys_organization 为唯一载体,gxmu_college 废弃 - 平台无可用增量字段,只做全量拉取 + 幂等 upsert - 上游消失只停用不删除,且只作用于同步来源的记录(避免误停用人工维护的研究生班) - 平台权威字段按白名单覆盖,人工字段不触碰 - 码表可配置并保留原始码,码值实测存在两套编码混用,不做硬编码 - 手动同步为异步:提交返回批次号,前端轮询 /status 看分段进度 性能: - 班级/部门由逐条 update 改为多行 upsert,班级 1584 条 41s -> 0.19s - 码表解析加进程内缓存,学生同步由 >17 分钟不完成 -> 170 秒完成 98187 行 迁移脚本需手动执行(可重复执行): sql/20260912_create_openplat_sync_tables.sql sql/20260912_add_openplat_menu.sql 离线单测 43 个;另有联网集成测试 OpenPlatformClientIT(需 -Dopenplat.it=true)。
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
package com.gxwebsoft.gxmu.openplat.controller;
|
||||
|
||||
import cn.hutool.core.util.DesensitizedUtil;
|
||||
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.common.system.entity.User;
|
||||
import com.gxwebsoft.gxmu.entity.ClassInfo;
|
||||
import com.gxwebsoft.gxmu.openplat.OpenplatSyncConstants;
|
||||
import com.gxwebsoft.gxmu.openplat.SyncSupport;
|
||||
import com.gxwebsoft.gxmu.openplat.claim.ClaimResult;
|
||||
import com.gxwebsoft.gxmu.openplat.claim.StudentClaimService;
|
||||
import com.gxwebsoft.gxmu.openplat.entity.GxmuStudent;
|
||||
import com.gxwebsoft.gxmu.openplat.mapper.GxmuStudentMapper;
|
||||
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.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
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 javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 学生名册与认领控制器。
|
||||
*
|
||||
* <p>响应中的手机号与身份证一律脱敏,原始值不出本控制器。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-09-12
|
||||
*/
|
||||
@Api(tags = "学生名册")
|
||||
@RestController
|
||||
@RequestMapping("/api/gxmu/student")
|
||||
public class GxmuStudentController extends BaseController {
|
||||
|
||||
@Resource
|
||||
private GxmuStudentMapper studentMapper;
|
||||
|
||||
@Resource
|
||||
private StudentClaimService studentClaimService;
|
||||
|
||||
@Resource
|
||||
private SyncSupport syncSupport;
|
||||
|
||||
@PreAuthorize("hasAuthority('gxmu:student:list')")
|
||||
@ApiOperation("学生名册分页查询")
|
||||
@GetMapping("/page")
|
||||
public ApiResult<PageResult<GxmuStudent>> page(
|
||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||
@RequestParam(value = "limit", defaultValue = "20") Integer limit,
|
||||
@RequestParam(value = "collegeId", required = false) Integer collegeId,
|
||||
@RequestParam(value = "classId", required = false) Integer classId,
|
||||
@RequestParam(value = "xh", required = false) String xh,
|
||||
@RequestParam(value = "xm", required = false) String xm,
|
||||
@RequestParam(value = "pyccmc", required = false) String pyccmc,
|
||||
@RequestParam(value = "status", required = false) Integer status,
|
||||
@RequestParam(value = "claimed", required = false) Boolean claimed) {
|
||||
LambdaQueryWrapper<GxmuStudent> wrapper = buildWrapper(collegeId, classId, xh, xm, pyccmc, status, claimed);
|
||||
Page<GxmuStudent> pageParam = new Page<>(page, limit);
|
||||
IPage<GxmuStudent> result = studentMapper.selectPage(pageParam, wrapper);
|
||||
List<GxmuStudent> records = result.getRecords();
|
||||
desensitize(records);
|
||||
enrich(records);
|
||||
return success(new PageResult<>(records, result.getTotal()));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('gxmu:student:list')")
|
||||
@ApiOperation("学生名册详情")
|
||||
@GetMapping("/{id}")
|
||||
public ApiResult<GxmuStudent> get(@PathVariable("id") Integer id) {
|
||||
GxmuStudent student = studentMapper.selectById(id);
|
||||
if (student == null) {
|
||||
return fail("记录不存在", null);
|
||||
}
|
||||
desensitize(java.util.Collections.singletonList(student));
|
||||
enrich(java.util.Collections.singletonList(student));
|
||||
return success(student);
|
||||
}
|
||||
|
||||
@ApiOperation("导出学生名册(脱敏)")
|
||||
@PreAuthorize("hasAuthority('gxmu:student:export')")
|
||||
@OperationLog
|
||||
@PostMapping("/export")
|
||||
public ApiResult<List<GxmuStudent>> export(
|
||||
@RequestParam(value = "collegeId", required = false) Integer collegeId,
|
||||
@RequestParam(value = "classId", required = false) Integer classId,
|
||||
@RequestParam(value = "status", required = false) Integer status) {
|
||||
LambdaQueryWrapper<GxmuStudent> wrapper = buildWrapper(collegeId, classId, null, null, null, status, null);
|
||||
List<GxmuStudent> records = studentMapper.selectList(wrapper);
|
||||
desensitize(records);
|
||||
enrich(records);
|
||||
return success(records);
|
||||
}
|
||||
|
||||
@ApiOperation("自助认领(学号+姓名)")
|
||||
@PostMapping("/claim")
|
||||
public ApiResult<ClaimResult> claim(@RequestBody ClaimParam param) {
|
||||
User user = getLoginUser();
|
||||
ClaimResult result = studentClaimService.claimByXhAndName(user,
|
||||
param == null ? null : param.getXh(), param == null ? null : param.getXm());
|
||||
return result.isSuccess() ? success(result.getMessage(), result) : fail(result.getMessage(), result);
|
||||
}
|
||||
|
||||
@OperationLog
|
||||
@PreAuthorize("hasAuthority('gxmu:student:list')")
|
||||
@ApiOperation("手动认领/改绑")
|
||||
@PutMapping("/{id}/claim")
|
||||
public ApiResult<?> bind(@PathVariable("id") Integer id, @RequestBody ClaimParam param) {
|
||||
boolean ok = studentClaimService.bind(id, param == null ? null : param.getUserId(),
|
||||
param == null ? null : param.getPhone(), getTenantId());
|
||||
return ok ? success("认领成功") : fail("认领失败:未找到匹配的账号");
|
||||
}
|
||||
|
||||
@OperationLog
|
||||
@PreAuthorize("hasAuthority('gxmu:student:list')")
|
||||
@ApiOperation("解除认领")
|
||||
@PutMapping("/{id}/unclaim")
|
||||
public ApiResult<?> unbind(@PathVariable("id") Integer id) {
|
||||
return studentClaimService.unbind(id, getTenantId()) ? success("已解除认领") : fail("解除失败");
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<GxmuStudent> buildWrapper(Integer collegeId, Integer classId, String xh,
|
||||
String xm, String pyccmc, Integer status, Boolean claimed) {
|
||||
LambdaQueryWrapper<GxmuStudent> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(GxmuStudent::getTenantId, getTenantId());
|
||||
wrapper.eq(collegeId != null, GxmuStudent::getCollegeId, collegeId);
|
||||
wrapper.eq(classId != null, GxmuStudent::getClassId, classId);
|
||||
wrapper.eq(StrUtil.isNotBlank(xh), GxmuStudent::getXh, StrUtil.trim(xh));
|
||||
wrapper.like(StrUtil.isNotBlank(xm), GxmuStudent::getXm, StrUtil.trim(xm));
|
||||
wrapper.eq(StrUtil.isNotBlank(pyccmc), GxmuStudent::getPyccmc, pyccmc);
|
||||
// 默认只看在校生
|
||||
wrapper.eq(status != null, GxmuStudent::getStatus, status);
|
||||
wrapper.eq(status == null, GxmuStudent::getStatus, 1);
|
||||
if (claimed != null) {
|
||||
if (claimed) {
|
||||
wrapper.isNotNull(GxmuStudent::getUserId);
|
||||
} else {
|
||||
wrapper.isNull(GxmuStudent::getUserId);
|
||||
}
|
||||
}
|
||||
wrapper.orderByAsc(GxmuStudent::getCollegeId).orderByAsc(GxmuStudent::getXh);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏:手机号与身份证明文绝不离开后端。
|
||||
*/
|
||||
private void desensitize(List<GxmuStudent> records) {
|
||||
for (GxmuStudent student : records) {
|
||||
if (StrUtil.isNotBlank(student.getSjh())) {
|
||||
student.setSjh(DesensitizedUtil.mobilePhone(student.getSjh()));
|
||||
}
|
||||
if (StrUtil.isNotBlank(student.getSfzjh()) && student.getSfzjh().length() >= 8) {
|
||||
student.setSfzjh(DesensitizedUtil.idCardNum(student.getSfzjh(), 4, 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void enrich(List<GxmuStudent> records) {
|
||||
if (records.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<Integer, String> orgNames = syncSupport.orgNameById();
|
||||
Map<Integer, String> classNames = syncSupport.classNameById();
|
||||
for (GxmuStudent student : records) {
|
||||
if (student.getCollegeId() != null) {
|
||||
student.setCollegeName(orgNames.get(student.getCollegeId()));
|
||||
}
|
||||
if (student.getClassId() != null) {
|
||||
student.setClassName(classNames.get(student.getClassId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 认领参数
|
||||
*/
|
||||
@io.swagger.annotations.ApiModel("学生认领参数")
|
||||
public static class ClaimParam {
|
||||
@io.swagger.annotations.ApiModelProperty("学号")
|
||||
private String xh;
|
||||
@io.swagger.annotations.ApiModelProperty("姓名")
|
||||
private String xm;
|
||||
@io.swagger.annotations.ApiModelProperty("目标用户ID(手动认领时)")
|
||||
private Integer userId;
|
||||
@io.swagger.annotations.ApiModelProperty("目标用户手机号(手动认领时, 可不传则用名册手机号)")
|
||||
private String phone;
|
||||
|
||||
public String getXh() {
|
||||
return xh;
|
||||
}
|
||||
|
||||
public void setXh(String xh) {
|
||||
this.xh = xh;
|
||||
}
|
||||
|
||||
public String getXm() {
|
||||
return xm;
|
||||
}
|
||||
|
||||
public void setXm(String xm) {
|
||||
this.xm = xm;
|
||||
}
|
||||
|
||||
public Integer getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Integer userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public void setPhone(String phone) {
|
||||
this.phone = phone;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user