Compare commits

..
29 Commits
Author SHA1 Message Date
wanghu e063e67822 修改教师
build / build-push (push) Successful in 2m17s
2026-10-28 11:22:50 +08:00
wanghu e0c9e14ab9 更新
build / build-push (push) Successful in 1m20s
2026-10-28 08:57:33 +08:00
wanghu 3fa52f21af 学年 学校 获取 年级
build / build-push (push) Successful in 1m12s
2026-10-21 14:43:27 +08:00
ops a0c6a27f55 fix(ci): 构建容器名唯一化 + npm 走国内镜像源(避免并发互踩 exit 137) 2026-10-21 06:24:29 +00:00
wanghu cee411472a 同步班级学生
build / build-push (push) Successful in 1m4s
2026-10-21 08:47:21 +08:00
wanghu a669bb67ec 提交
build / build-push (push) Successful in 1m19s
2026-10-20 19:55:50 +08:00
wanghu 064c855a55 git错误
build / build-push (push) Successful in 1m12s
2026-10-20 14:29:10 +08:00
wanghu d633609306 依赖
build / build-push (push) Failing after 2m2s
2026-10-20 14:17:10 +08:00
wanghu 1c19e3d2ef 自定义组开放接口
build / build-push (push) Successful in 1m7s
2026-09-29 15:45:23 +08:00
wanghu aecab2fd91 自定义组开放接口
build / build-push (push) Failing after 48s
2026-09-29 11:03:02 +08:00
wanghu 9f3b49c30a 任教
build / build-push (push) Successful in 1m4s
2026-09-20 20:13:54 +08:00
wanghu 3e18856c0c 学科排序
build / build-push (push) Successful in 1m3s
2026-09-20 19:28:23 +08:00
wanghu 43484a8936 Merge remote-tracking branch 'origin/main'
build / build-push (push) Successful in 2m12s
2026-09-20 19:12:33 +08:00
wanghu 8a61c77b12 添加接口 2026-09-20 19:12:13 +08:00
wanghu 0b1d8a3ecf 修改学科 2026-09-20 19:11:42 +08:00
chenxing 700d6526b3 refactor(config): yh.open 四域键退役(user-center-url/group-center-url 为 SDK 重构后废弃键,枚举兜底零配置)
build / build-push (push) Successful in 1m2s
2026-09-19 18:59:59 +08:00
wanghu dde2a147b0 log.zip
build / build-push (push) Successful in 3m14s
2026-09-17 17:12:40 +08:00
wanghu 0462c772e4 log.zip
build / build-push (push) Failing after 12s
2026-09-17 15:57:09 +08:00
wanghu 086e0bbf39 学生教师树
build / build-push (push) Failing after 17s
2026-09-17 14:59:15 +08:00
wanghu b9782faeae 学生教师树
build / build-push (push) Successful in 1m15s
2026-09-11 12:21:27 +08:00
wanghu 2d4f3db219 学生教师树
build / build-push (push) Successful in 1m1s
2026-09-11 11:27:34 +08:00
wanghu 2cf8334d74 schId 修改
build / build-push (push) Successful in 1m1s
2026-09-11 09:54:54 +08:00
wanghu d9604b3798 getstutree
build / build-push (push) Successful in 2m11s
2026-09-09 10:00:51 +08:00
wanghu 1b45924003 返回值[]{}
build / build-push (push) Successful in 2m4s
2026-09-08 17:30:55 +08:00
wanghu f73d9da730 schId 修改
build / build-push (push) Successful in 1m0s
2026-09-08 16:23:20 +08:00
wanghu eb734c5fea 返回空数组
build / build-push (push) Successful in 1m0s
2026-09-08 16:15:26 +08:00
wanghu 076524218f 111
build / build-push (push) Successful in 1m49s
2026-09-08 14:56:26 +08:00
wanghu 8a54bd7dc5 教师树+教师
build / build-push (push) Failing after 1m29s
2026-09-08 14:51:45 +08:00
wanghu d1a748c0ff tea null
build / build-push (push) Successful in 1m5s
2026-09-08 12:04:00 +08:00
64 changed files with 3192 additions and 77 deletions
+25 -10
View File
@@ -24,8 +24,17 @@ jobs:
image: docker:24
options: --network infra-infra-net
steps:
- name: 安装 git / curl(alpine)
run: apk add --no-cache git curl
# dl-cdn 在 CI 容器内被出口防火墙拒(apk 报 Permission denied,索引拉不下来即 curl no such package),
# 切国内镜像源安装;aliyun 不通再退 ustc,最后兜底官方源
- name: 安装 git / curl(alpine,国内镜像源)
run: |
set -e
for m in mirrors.aliyun.com mirrors.ustc.edu.cn dl-cdn.alpinelinux.org; do
sed -i "s#https://[^/]*#https://$m#" /etc/apk/repositories
echo ">> trying mirror: $m"
if apk add --no-cache git curl; then break; fi
done
git --version && curl --version | head -1
# 内网 Gitea 无法访问 GitHub 拉 actions/checkout,手动 clone 本仓库
- name: 拉取源码(内网无 GitHub,手动 clone 本仓库)
@@ -55,18 +64,24 @@ jobs:
- name: 构建 jar(maven 容器,复用宿主 m2 缓存)
run: |
set -e
docker rm -f mavenbuild 2>/dev/null || true
docker run -d --name mavenbuild \
# 构建容器名必须全局唯一:runner 挂的是宿主 docker.sock,容器名在整个 daemon 上共享
# 命名空间。固定名会在多仓/多条 workflow 并发时互相 docker rm -f 掉对方正在构建的
# 容器,表现为无编译报错的 exit 137(SIGKILL,2026-10-21 实测复现)。
# GITHUB_RUN_ID 实例级唯一,再拼时间戳兜底。
BUILD_CONTAINER="mavenbuild-${GITHUB_RUN_ID:-0}-$(date +%s)"
echo "BUILD_CONTAINER=${BUILD_CONTAINER}"
docker rm -f "$BUILD_CONTAINER" 2>/dev/null || true
docker run -d --name "$BUILD_CONTAINER" \
--network infra-infra-net \
-v /opt/docker/projman-m2:/root/.m2 \
-w /build maven:3.6.3-jdk-8 sleep 3600
docker cp "${PWD}/." mavenbuild:/build/
docker exec mavenbuild sh -c "printf '%s' '<settings><profiles><profile><id>nexus-repo</id><repositories><repository><id>nexus-internal</id><url>http://nexus:8081/repository/maven-public/</url><releases><enabled>true</enabled></releases><snapshots><enabled>true</enabled><updatePolicy>always</updatePolicy></snapshots></repository></repositories></profile></profiles><activeProfiles><activeProfile>nexus-repo</activeProfile></activeProfiles></settings>' > /root/.m2/settings.xml"
docker exec mavenbuild sh -c 'cd /build && mvn -B -DskipTests clean package'
docker exec mavenbuild sh -c 'cd /build/target && cp *.jar app.jar'
docker cp "${PWD}/." "$BUILD_CONTAINER":/build/
docker exec "$BUILD_CONTAINER" sh -c "printf '%s' '<settings><profiles><profile><id>nexus-repo</id><repositories><repository><id>nexus-internal</id><url>http://nexus:8081/repository/maven-public/</url><releases><enabled>true</enabled></releases><snapshots><enabled>true</enabled><updatePolicy>always</updatePolicy></snapshots></repository></repositories></profile></profiles><activeProfiles><activeProfile>nexus-repo</activeProfile></activeProfiles></settings>' > /root/.m2/settings.xml"
docker exec "$BUILD_CONTAINER" sh -c 'cd /build && mvn -B -DskipTests clean package'
docker exec "$BUILD_CONTAINER" sh -c 'cd /build/target && cp *.jar app.jar'
mkdir -p "${PWD}/target"
docker cp mavenbuild:/build/target/app.jar "${PWD}/target/app.jar"
docker rm -f mavenbuild
docker cp "$BUILD_CONTAINER":/build/target/app.jar "${PWD}/target/app.jar"
docker rm -f "$BUILD_CONTAINER"
- name: 构建并推送镜像
run: |
@@ -0,0 +1,49 @@
## 根因
`TeainfoController.convertToTeaInfoVO`(TeainfoController.java:84-86)检查 `teacher.getGroupList()`,但它是一个 `@Transient` 字段,而 `getSchTeacherList` 查询链路(SchTeacherCacheService.java:36-44)只查 `sch_teacher` 本表、从不填充它。因此**每个教师都在第 86 行 `return null`**,`stream().map()` 不过滤 null,接口最终返回 `200 OK + [null, null, ...]`。`GetTeainfo` 和 `GetArrTeainfo` 共用同一转换方法,有一模一样的 bug。
## 修复方案
### 改动 1:TeainfoController.java(修复 null 元素)
1. **注入 `SchTeacherGroupRepository`**(新增 `@Autowired` 字段 + import:`SchTeacherGroupRepository`、`java.util.Map`、`java.util.Collections`、`java.util.Objects`)。
2. **新增私有方法 `fillGroupList(List<SchTeacher> teachers, Integer schId)`**:批量查询组关系并回填到每个教师的 `@Transient groupList` 字段,复用项目既有模式(SchTeacherService.java:96-98 的 `findByTeaIdIn` + `groupingBy`):
```java
private void fillGroupList(List<SchTeacher> teachers, Integer schId) {
if (teachers == null || teachers.isEmpty()) return;
List<Integer> teaIds = teachers.stream()
.map(SchTeacher::getTeaId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (teaIds.isEmpty()) return;
List<SchTeacherGroup> relations = (schId != null)
? schTeacherGroupRepository.findByTeaIdInAndSchId(teaIds, schId)
: schTeacherGroupRepository.findByTeaIdIn(teaIds);
Map<Integer, List<SchTeacherGroup>> groupMap = relations.stream()
.filter(g -> g.getTeaId() != null)
.collect(Collectors.groupingBy(SchTeacherGroup::getTeaId));
teachers.forEach(t -> t.setGroupList(groupMap.getOrDefault(t.getTeaId(), Collections.emptyList())));
}
```
3. **三个接口调用处**:
- `getSchTeainfo`(:143-147):查询后调用 `fillGroupList(teachers, resolvedSchId)`(按学校过滤组关系,保证 GroupID/SchSerID 取的是本校分组),流上加 `.filter(Objects::nonNull)` 兜底。
- `getArrTeainfo`(:107-110):调用 `fillGroupList(teachers, null)`(无学校上下文,按教师ID查全部分组),流上加 `.filter(Objects::nonNull)`。
- `getTeainfo`(:52-55):`Optional.map` 内对单个教师调用 `fillGroupList(Collections.singletonList(teacher), null)`(Java 8,不用 `List.of`)。
### 改动 2:TeaInfoVO.java(锁定 JSON 键名)
每个字段加 `@JsonProperty("原始字段名")`(如 `@JsonProperty("SchSerID") private String SchSerID;`),与同包 SchInfoVO 的写法一致,确保输出键名为 `SchSerID`/`TeaIDCard`/`GroupID` 等原始大写形式,而不是 Jackson 默认推导的 `schSerID`/`teaIDCard`。新增 import `com.fasterxml.jackson.annotation.JsonProperty`。
## 保持现状(不在本次范围)
- `TeaNo` 映射的是 `sort` 排序号、其余 12 个字段(SchName、Photo 等)从不赋值恒为 null —— 属于字段映射契约问题,另行确认。
- `findTeaIdsBySchId` 不过滤 `transfer_status`(已转出教师也会出现在列表中)—— 与其他接口口径不一致,保持现状。
- 文件中历史遗留的乱码注释不动;新增代码 UTF-8 + CRLF 与文件一致。
## 验证
- `mvn -q compile` 编译通过。
- 如有条件,本地起服务后请求 `/oapi/OATea/GetSchTeainfo?schId=xxx`,确认返回教师数组(元素为对象而非 null,且键名为原始大写形式)。
+15
View File
@@ -204,6 +204,21 @@
<artifactId>yh-api-std</artifactId>
</dependency>
<!-- 前端运行时配置端点 GET /api/env-config(免登录,starter 自带 SSO 旁路过滤器):
数据源默认 FileEnvConfigSource 读 /app/config/env-config-{target}.json(projman 逐文件 bind mount)。
注意本端不在聚合包上,聚合里的 env-config 不会传递进来,必须显式引入 -->
<dependency>
<groupId>com.yunhai.platform</groupId>
<artifactId>yh-env-config-spring-boot-starter</artifactId>
</dependency>
<!-- 外观偏好同源读端点 GET /api/appearance(三态 proxy/local/off,默认 proxy 经 YschOpenCall
代理 UserHome /oapi/appearance)。平台件刻意不进聚合包(防本地同名控制器双注册),必须显式引入。
生效条件=@ConditionalOnBean(YschOpenCall),本端 yh.open.base-host 已配即满足 -->
<dependency>
<groupId>com.yunhai.platform</groupId>
<artifactId>yh-appearance-spring-boot-starter</artifactId>
</dependency>
<!-- CAS 客户端(浏览器 SSO 重定向 / ticket 校验 / getRemoteUser 包装)。
版本 3.5.1 已在本地 .m2 仓库。 -->
<dependency>
@@ -15,7 +15,11 @@ import java.util.Optional;
@Repository
public interface SchStudentRepository extends JpaRepository<SchStudent, Integer>, JpaSpecificationExecutor<SchStudent> {
Optional<SchStudent> findByStuId(Integer stuId);
/**
* stu_id 是用户中心用户ID(非本表主键),学生在多所学校各有一条就学记录,
* 同一 stu_id 可能存在多行,必须返回 List;返回 Optional 会在多行时抛 NonUniqueResultException
*/
List<SchStudent> findByStuId(Integer stuId);
List<SchStudent> findByStuIdIn(List<Integer> stuIds);
@@ -20,7 +20,11 @@ public interface SchTeacherRepository extends JpaRepository<SchTeacher, Integer>
List<SchTeacher> findByTeaIdIn(List<Integer> teaIds);
Optional<SchTeacher> findByTeaId(Integer teaId);
/**
* tea_id 是用户中心用户ID(非本表主键),教师在多所学校各有一条记录,
* 同一 tea_id 可能存在多行,必须返回 List;返回 Optional 会在多行时抛 NonUniqueResultException
*/
List<SchTeacher> findByTeaId(Integer teaId);
@Query(value = "SELECT DISTINCT t.* FROM sch_teacher t " +
@@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.*;
public class LogController extends BaseController {
@Autowired
private ILogService logService; // 使用接口1
private ILogService logService; // 使用接口
@GetMapping("systemSearch")
public PageVO<OperationLogRecord> systemSearch(@RequestParam(required = false) String keyword,
@@ -3,7 +3,7 @@ import com.yunhai.bschclassmanage.utils.UserContext;
import com.yunhai.bschclassmanage.config.Config;
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
import com.yunhai.platform.openapi.base.YschBaseOpenApi;
import com.yunhai.platform.utils.model.BoaGroup;
import com.yunhai.platform.utils.model.BoaModule;
import com.yunhai.platform.utils.model.BoaUser;
@@ -7,7 +7,6 @@ import com.yunhai.bschclassmanage.model.dto.SendUserGroupDTO;
import com.yunhai.bschclassmanage.model.vo.SelectIntVO;
import com.yunhai.bschclassmanage.service.ISendUserBaseService;
import com.yunhai.bschclassmanage.utils.UserContext;
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@@ -1,9 +1,8 @@
package com.yunhai.bschclassmanage.controller.api;
import com.yunhai.bschclassmanage.utils.UserContext;
import com.yunhai.bschclassmanage.config.Config;
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
import com.yunhai.platform.openapi.base.YschBaseOpenApi;
import com.yunhai.platform.utils.model.BoaGroup;
import com.yunhai.platform.utils.model.BoaModule;
import com.yunhai.platform.utils.model.BoaUser;
@@ -20,14 +20,11 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import java.util.stream.Collectors;
/** Compatibility endpoints used by BestSchool.IBaseClass.BLL.DbClass consumers. */
@Tag(name = "DbClass", description = "School class data APIs")
@RestController
@RequestMapping(Config.OAPI + "/DbClass/")
@@ -43,7 +40,7 @@ public class DbClassController {
public ResponseEntity<List<Map<String, Object>>> getSchteaList(
@Parameter(description = "School ID", required = true, example = "3")
@RequestParam("Group") Integer group) {
if (group == null || group <= 0) return ResponseEntity.badRequest().build();
if (group == null || group <= 0) return ResponseEntity.ok(new ArrayList<>());
SchInfoVO school = schInfoService.GetSchFirst(group);
String schSerId = school == null
@@ -69,6 +66,7 @@ public class DbClassController {
row.put("SchSerID", schSerId);
row.put("TeaTel", teacher.getTel());
row.put("TeaName", teacher.getName());
row.put("TeaGender", teacher.getGender());
row.put("UserTeaSerID", teacher.getTeaId());
row.put("TeaIDCard", teacher.getIdCard());
row.put("TeaNo", teacher.getSort());
@@ -4,12 +4,15 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import com.yunhai.bschclassmanage.config.Config;
import com.yunhai.bschclassmanage.model.oa.TeaTreeVO;
import com.yunhai.bschclassmanage.model.vo.oa.SchInfoVO;
import com.yunhai.bschclassmanage.service.IComSchTreeService;
import com.yunhai.bschclassmanage.service.ISchInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.List;
@Tag(name = "SchGroup", description = "SchGroup related APIs")
@@ -18,7 +21,51 @@ import java.util.List;
@CrossOrigin(originPatterns = {"*"}, allowCredentials = "true")
public class SchGroupController {
@Autowired
private IComSchTreeService comSchTreeService;
@Autowired
private ISchInfoService schInfoService;
@GetMapping("GetTeaTree")
@Operation(summary = "获取学校教师组树(toUser 默认 true 带教师,false 仅返回组骨架;SchSerID 与 schId 二选一)")
public ResponseEntity<List<TeaTreeVO>> getTeaTree(@RequestParam(value = "SchSerID", required = false) String SchSerID,
@RequestParam(value = "schId", required = false) Integer schId,
@RequestParam(value = "toUser", required = false, defaultValue = "true") boolean toUser) {
Integer resolvedSchId = resolveSchId(schId, SchSerID);
if (resolvedSchId == null) {
return ResponseEntity.ok(Collections.emptyList());
}
return ResponseEntity.ok(comSchTreeService.getSchTeaTree(resolvedSchId, toUser));
}
@GetMapping("GetStuTree")
@Operation(summary = "获取学校学生树:学校→学段→年级→班级→学生(toUser 默认 true 带学生,false 仅返回组织骨架;SchSerID 与 schId 二选一)")
public ResponseEntity<List<TeaTreeVO>> getStuTree(@RequestParam(value = "SchSerID", required = false) String SchSerID,
@RequestParam(value = "schId", required = false) Integer schId,
@RequestParam(value = "toUser", required = false, defaultValue = "true") boolean toUser) {
Integer resolvedSchId = resolveSchId(schId, SchSerID);
if (resolvedSchId == null) {
return ResponseEntity.ok(Collections.emptyList());
}
return ResponseEntity.ok(comSchTreeService.getSchStuTree(resolvedSchId, toUser));
}
/**
* 解析学校ID:支持直接传 schId,或传 SchSerID 反查 schId,二者至少提供一个。
*
* @param schId 学校内部ID(优先使用)
* @param schSerID 学校外部序列号
* @return 学校内部ID,参数非法或查不到时返回 null
*/
private Integer resolveSchId(Integer schId, String schSerID) {
if (schId != null) {
return schId;
}
if (schSerID == null || schSerID.trim().isEmpty()) {
return null;
}
SchInfoVO schInfoVO = schInfoService.GetSchFirst(schSerID);
return schInfoVO == null ? null : schInfoVO.getUserSchSerId();
}
}
@@ -28,7 +28,7 @@ public class SchSubController {
if (SchID == null || SchID.trim().isEmpty()) return ResponseEntity.ok(Collections.emptyList());
Optional<SchInfo> school = schInfoRepository.findBySchSerId(SchID);
if (!school.isPresent()) return ResponseEntity.ok(Collections.emptyList());
return ResponseEntity.ok(subjectService.getSchSubList(school.get().getSchId(), schooling).stream().map(s -> subject(s, school.get().getSchSerId())).collect(Collectors.toList()));
return ResponseEntity.ok(subjectService.getSchSubList(school.get().getSchId(), schooling).stream().filter(this::enabled).map(s -> subject(s, school.get().getSchSerId())).collect(Collectors.toList()));
}
@GetMapping("GetSchIdsSublist")
@@ -37,7 +37,7 @@ public class SchSubController {
if (SchIds == null || SchIds.trim().isEmpty()) return ResponseEntity.ok(Collections.emptyList());
List<String> ids = OaCompatSupport.strings(SchIds);
List<Map<String,Object>> out = new ArrayList<>();
for (String id : ids) schInfoRepository.findBySchSerId(id).ifPresent(s -> subjectService.getSchSubList(s.getSchId(), null).forEach(sub -> out.add(subject(sub, s.getSchSerId()))));
for (String id : ids) schInfoRepository.findBySchSerId(id).ifPresent(s -> subjectService.getSchSubList(s.getSchId(), null).stream().filter(this::enabled).forEach(sub -> out.add(subject(sub, s.getSchSerId()))));
return ResponseEntity.ok(out);
}
@@ -47,7 +47,10 @@ public class SchSubController {
if (UserTeaSerID == null) return ResponseEntity.ok(Collections.emptyList());
try {
Integer teaId = Integer.valueOf(UserTeaSerID);
return ResponseEntity.ok(teachRepository.findAll().stream().filter(t -> Objects.equals(t.getTeaId(), teaId)).map(SchClassTeach::getSubId).filter(Objects::nonNull).distinct().collect(Collectors.toList()));
List<SchClassTeach> teaches = teachRepository.findAll().stream().filter(t -> Objects.equals(t.getTeaId(), teaId)).collect(Collectors.toList());
Set<String> enabledCodes = new HashSet<>();
teaches.stream().map(SchClassTeach::getSchId).filter(Objects::nonNull).distinct().forEach(schId -> enabledCodes.addAll(enabledSubjectCodes(schId)));
return ResponseEntity.ok(teaches.stream().map(SchClassTeach::getSubId).filter(c -> c != null && enabledCodes.contains(c)).distinct().collect(Collectors.toList()));
} catch (NumberFormatException e) { return ResponseEntity.ok(Collections.emptyList()); }
}
@@ -57,9 +60,14 @@ public class SchSubController {
if (SchID == null) return ResponseEntity.ok(Collections.emptyList());
Optional<SchInfo> school = schInfoRepository.findBySchSerId(SchID);
if (!school.isPresent()) return ResponseEntity.ok(Collections.emptyList());
return ResponseEntity.ok(teachRepository.findAll().stream().filter(t -> Objects.equals(t.getSchId(), school.get().getSchId())).map(SchClassTeach::getSubId).filter(Objects::nonNull).map(s -> s.toLowerCase(Locale.ROOT)).distinct().collect(Collectors.toList()));
Set<String> enabledCodes = enabledSubjectCodes(school.get().getSchId()).stream().map(c -> c.toLowerCase(Locale.ROOT)).collect(Collectors.toSet());
return ResponseEntity.ok(teachRepository.findAll().stream().filter(t -> Objects.equals(t.getSchId(), school.get().getSchId())).map(SchClassTeach::getSubId).filter(Objects::nonNull).map(s -> s.toLowerCase(Locale.ROOT)).filter(enabledCodes::contains).distinct().collect(Collectors.toList()));
}
private boolean enabled(SchDicSubject s) { return Boolean.TRUE.equals(s.getEnable()); }
private Set<String> enabledSubjectCodes(Integer schId) { return subjectService.toEnableIdAndNameMap(schId, null).keySet(); }
private Map<String,Object> subject(SchDicSubject s, String schSerId) {
Map<String,Object> m = new LinkedHashMap<>(); m.put("SchSerID", schSerId); m.put("SubID", s.getCode() == null ? String.valueOf(s.getId()) : s.getCode()); m.put("SubName", s.getName()); m.put("No", s.getSort()); return m;
}
@@ -20,6 +20,8 @@ public class StuInfoController {
@Autowired private SchStudentRepository studentRepository;
@Autowired private SchInfoRepository schoolRepository;
@Autowired private SchStudentHistoryRepository historyRepository;
@Autowired private SchClassRepository classRepository;
@Autowired private BaseSchsystemGradeRepository baseSchsystemGradeRepository;
@GetMapping("GetAllStulist")
@Operation(summary = "GetAllStulist")
@@ -58,6 +60,39 @@ public class StuInfoController {
Set<Integer> ids = new HashSet<>(OaCompatSupport.ints(stuids));
return ResponseEntity.ok(students(studentRepository.findAll().stream().filter(s -> ids.contains(s.getStuId())).collect(Collectors.toList()), null));
}
/** 按学校+年级直查学生(CampusNotice 推送范围圈人:免逐班聚合) */
@GetMapping("GetGradeStulist")
@Operation(summary = "GetGradeStulist", description = "按学校+年级查询在校学生(不含已毕业班级)")
public ResponseEntity<List<Map<String,Object>>> GetGradeStulist(@RequestParam(required=false) String SchSerID, @RequestParam(required=false) Integer GradeId) {
Integer schId = SchSerID == null ? null : schoolRepository.findBySchSerId(SchSerID).map(SchInfo::getSchId).orElse(null);
if (schId == null) return ResponseEntity.ok(Collections.emptyList());
Set<Integer> classIds = activeClassIds(schId, null, GradeId == null ? null : Collections.singleton(GradeId));
if (classIds.isEmpty()) return ResponseEntity.ok(Collections.emptyList());
return ResponseEntity.ok(students(studentRepository.findAll().stream().filter(s -> classIds.contains(s.getClassId())).collect(Collectors.toList()), SchSerID));
}
/** 按学校+学段(学制)直查学生(CampusNotice「学部」级范围一键展开) */
@GetMapping("GetSubStulist")
@Operation(summary = "GetSubStulist", description = "按学校+学段查询在校学生(不含已毕业班级)")
public ResponseEntity<List<Map<String,Object>>> GetSubStulist(@RequestParam(required=false) String SchSerID, @RequestParam(required=false) Integer SchSystemId) {
Integer schId = SchSerID == null ? null : schoolRepository.findBySchSerId(SchSerID).map(SchInfo::getSchId).orElse(null);
if (schId == null || SchSystemId == null) return ResponseEntity.ok(Collections.emptyList());
Set<Integer> gradeIds = baseSchsystemGradeRepository.findBySchSystemId(SchSystemId).stream()
.map(BaseSchsystemGrade::getGradeId).filter(Objects::nonNull).collect(Collectors.toSet());
Set<Integer> classIds = activeClassIds(schId, gradeIds, null);
if (classIds.isEmpty()) return ResponseEntity.ok(Collections.emptyList());
return ResponseEntity.ok(students(studentRepository.findAll().stream().filter(s -> classIds.contains(s.getClassId())).collect(Collectors.toList()), SchSerID));
}
/** 学校在读班级 class_id 集合:gradeIds 非空按年级集合过滤,单个 gradeId 传 singleton */
private Set<Integer> activeClassIds(Integer schId, Set<Integer> gradeIds, Set<Integer> singleGrade) {
return classRepository.findAll().stream()
.filter(c -> Objects.equals(c.getSchId(), schId) && !Boolean.TRUE.equals(c.getGraduate()))
.filter(c -> (gradeIds == null || gradeIds.contains(c.getGradeId())) && (singleGrade == null || singleGrade.contains(c.getGradeId())))
.map(SchClass::getClassId).filter(Objects::nonNull).collect(Collectors.toSet());
}
@GetMapping("GetEleStuIdslist")
@Operation(summary = "GetEleStuIdslist")
public ResponseEntity<List<Map<String,Object>>> GetEleStuIdslist(@RequestParam(required=false) String stuids) { return GetStuIdslist(stuids); }
@@ -6,6 +6,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import com.yunhai.bschclassmanage.Repository.*;
import com.yunhai.bschclassmanage.config.Config;
import com.yunhai.bschclassmanage.entity.*;
import com.yunhai.bschclassmanage.enums.TransferStatus;
import com.yunhai.bschclassmanage.service.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -19,12 +21,16 @@ import java.util.stream.Collectors;
public class TeaChesController {
@Autowired private SchClassTeachRepository teachingRepository;
@Autowired private SchInfoRepository schoolRepository;
@Autowired private SchTeacherGroupRepository teacherGroupRepository;
@Autowired private ISchClassService classService;
@Autowired private ISchDicSubjectService subjectService;
@GetMapping("GetSchTeachlist")
@Operation(summary = "GetSchTeach")
public ResponseEntity<List<Map<String,Object>>> GetSchTeach(@RequestParam(required=false) String SchID, @RequestParam(required=false) Integer UserTeaSerID) {
Integer schId = SchID == null ? null : schoolRepository.findBySchSerId(SchID).map(SchInfo::getSchId).orElse(null);
return ResponseEntity.ok(teachingRepository.findAll().stream().filter(t -> (schId == null || Objects.equals(t.getSchId(), schId)) && (UserTeaSerID == null || Objects.equals(t.getTeaId(), UserTeaSerID))).map(t -> OaCompatSupport.teaching(t, schoolSer(t.getSchId()))).collect(Collectors.toList()));
List<SchClassTeach> list = teachingRepository.findAll().stream().filter(t -> (schId == null || Objects.equals(t.getSchId(), schId)) && (UserTeaSerID == null || Objects.equals(t.getTeaId(), UserTeaSerID))).collect(Collectors.toList());
return ResponseEntity.ok(normalFiltered(list).stream().map(t -> OaCompatSupport.teaching(t, schoolSer(t.getSchId()))).collect(Collectors.toList()));
}
@GetMapping("GetEleSchTeachlist")
@Operation(summary = "GetEleSchTeachlist")
@@ -39,7 +45,8 @@ public class TeaChesController {
@Operation(summary = "GetClassTeach")
public ResponseEntity<List<Map<String,Object>>> GetClassTeach(@RequestParam(required=false) String ClassID) {
Integer classId = intValue(ClassID);
return ResponseEntity.ok(teachingRepository.findAll().stream().filter(t -> classId != null && Objects.equals(t.getClassId(), classId)).map(t -> OaCompatSupport.teaching(t, schoolSer(t.getSchId()))).collect(Collectors.toList()));
List<SchClassTeach> list = teachingRepository.findAll().stream().filter(t -> classId != null && Objects.equals(t.getClassId(), classId)).collect(Collectors.toList());
return ResponseEntity.ok(normalFiltered(list).stream().map(t -> OaCompatSupport.teaching(t, schoolSer(t.getSchId()))).collect(Collectors.toList()));
}
@GetMapping("GetEleSchClaTeachlist")
@Operation(summary = "GetEleSchClaTeachlist")
@@ -53,7 +60,22 @@ public class TeaChesController {
private ResponseEntity<List<Map<String,Object>>> bySchools(String value) {
Set<Integer> ids = new HashSet<>(); for (String ser : OaCompatSupport.strings(value)) schoolRepository.findBySchSerId(ser).ifPresent(s -> ids.add(s.getSchId()));
return ResponseEntity.ok(teachingRepository.findAll().stream().filter(t -> ids.contains(t.getSchId())).map(t -> OaCompatSupport.teaching(t, schoolSer(t.getSchId()))).collect(Collectors.toList()));
List<SchClassTeach> list = teachingRepository.findAll().stream().filter(t -> ids.contains(t.getSchId())).collect(Collectors.toList());
return ResponseEntity.ok(normalFiltered(list).stream().map(t -> OaCompatSupport.teaching(t, schoolSer(t.getSchId()))).collect(Collectors.toList()));
}
/** 只保留正常教师(sch_teacher_group 中 transfer_status=转入)、正常班级(未毕业)、启用学科(enable=true)的任教记录,按学校分别过滤 */
private List<SchClassTeach> normalFiltered(List<SchClassTeach> teaches) {
List<SchClassTeach> out = new ArrayList<>();
Map<Integer, List<SchClassTeach>> bySch = new HashMap<>();
for (SchClassTeach t : teaches) if (t.getSchId() != null) bySch.computeIfAbsent(t.getSchId(), k -> new ArrayList<>()).add(t);
for (Map.Entry<Integer, List<SchClassTeach>> e : bySch.entrySet()) {
Integer schId = e.getKey();
Set<Integer> teaIds = teacherGroupRepository.findBySchIdAndTransferStatus(schId, TransferStatus.IN.getId()).stream().map(SchTeacherGroup::getTeaId).collect(Collectors.toSet());
Set<Integer> classIds = classService.getSchAllClassCacheList(schId).stream().filter(c -> !Boolean.TRUE.equals(c.getGraduate()) && c.getClassId() != null).map(SchClass::getClassId).collect(Collectors.toSet());
Set<String> subCodes = subjectService.toEnableIdAndNameMap(schId, null).keySet();
for (SchClassTeach t : e.getValue()) if (teaIds.contains(t.getTeaId()) && classIds.contains(t.getClassId()) && t.getSubId() != null && subCodes.contains(t.getSubId())) out.add(t);
}
return out;
}
private Integer intValue(String s) { try { return s == null ? null : Integer.valueOf(s); } catch (Exception e) { return null; } }
private String schoolSer(Integer id) { return id == null ? null : schoolRepository.findBySchId(id).map(SchInfo::getSchSerId).orElse(null); }
@@ -3,6 +3,7 @@ package com.yunhai.bschclassmanage.controller.oapi.oa;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import com.yunhai.bschclassmanage.Repository.SchTeacherGroupRepository;
import com.yunhai.bschclassmanage.Repository.SchTeacherRepository;
import com.yunhai.bschclassmanage.config.Config;
import com.yunhai.bschclassmanage.entity.SchTeacher;
@@ -21,7 +22,10 @@ import org.springframework.web.bind.annotation.*;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -37,6 +41,9 @@ public class TeainfoController {
@Autowired
private SchTeacherRepository schTeacherRepository;
@Autowired
private SchTeacherGroupRepository schTeacherGroupRepository;
@Autowired
private ISchInfoService schInfoService;
@@ -45,14 +52,19 @@ public class TeainfoController {
public ResponseEntity<TeaInfoVO> getTeainfo(@RequestParam Integer uid) {
// 1. 鍙傛暟鏍¢獙
if (uid == null) {
return ResponseEntity.badRequest().build();
return ResponseEntity.ok(new TeaInfoVO());
}
// 2. 鐩存帴浣跨敤 uid 鏌ヨ
return schTeacherRepository.findByTeaId(uid) // 鍋囪 findByTeaId 杩斿洖 Optional<SchTeacher>
.map(this::convertToTeaInfoVO) // 灏?SchTeacher 杞负 TeaInfoVO
return schTeacherRepository.findByTeaId(uid).stream()
.findFirst()
.map(teacher -> {
fillGroupList(Collections.singletonList(teacher), null);
return teacher;
})
.map(this::convertToTeaInfoVO)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
.orElse(ResponseEntity.ok(new TeaInfoVO()));
}
private TeaInfoVO convertToTeaInfoVO(SchTeacher teacher) {
@@ -94,22 +106,48 @@ public class TeainfoController {
return vo;
}
/**
* 批量回填教师的分组列表(groupList 为 @Transient 字段,查询后需要单独填充)。
* schId 不为空时只查该校的组关系,保证取到的 GroupID/SchSerID 属于当前学校。
*/
private void fillGroupList(List<SchTeacher> teachers, Integer schId) {
if (teachers == null || teachers.isEmpty()) {
return;
}
List<Integer> teaIds = teachers.stream()
.map(SchTeacher::getTeaId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (teaIds.isEmpty()) {
return;
}
List<SchTeacherGroup> relations = (schId != null)
? schTeacherGroupRepository.findByTeaIdInAndSchId(teaIds, schId)
: schTeacherGroupRepository.findByTeaIdIn(teaIds);
Map<Integer, List<SchTeacherGroup>> groupMap = relations.stream()
.filter(g -> g.getTeaId() != null)
.collect(Collectors.groupingBy(SchTeacherGroup::getTeaId));
teachers.forEach(t -> t.setGroupList(groupMap.getOrDefault(t.getTeaId(), Collections.emptyList())));
}
@PostMapping("GetArrTeainfo")
@Operation(summary = "根据教师id数组 获取教师列表")
public ResponseEntity<List<TeaInfoVO>> getArrTeainfo(@RequestBody String userIds) {
// 1. 鍙傛暟鏍¢獙
if (userIds == null) {
return ResponseEntity.badRequest().build();
return ResponseEntity.ok(new ArrayList<>());
}
List<Integer> uids= Arrays.asList(OtherUtil.convertToArray(userIds));
List<SchTeacher> teachers = schTeacherRepository.findByTeaIdIn(uids);
fillGroupList(teachers, null);
List<TeaInfoVO> voList = teachers.stream()
.map(this::convertToTeaInfoVO)
.filter(Objects::nonNull)
.collect(Collectors.toList());
return voList.isEmpty() ? ResponseEntity.noContent().build() : ResponseEntity.ok(voList);
return ResponseEntity.ok(voList);
}
/**
@@ -137,16 +175,18 @@ public class TeainfoController {
// 1. 参数校验:SchSerID 与 schId 至少传一个
Integer resolvedSchId = resolveSchId(schId, SchSerID);
if (resolvedSchId == null) {
return ResponseEntity.badRequest().build();
return ResponseEntity.ok(new ArrayList<>());
}
List<SchTeacher> teachers = schTeacherService.getSchTeacherList(resolvedSchId);
fillGroupList(teachers, resolvedSchId);
List<TeaInfoVO> voList = teachers.stream()
.map(this::convertToTeaInfoVO)
.filter(Objects::nonNull)
.collect(Collectors.toList());
return voList.isEmpty() ? ResponseEntity.noContent().build() : ResponseEntity.ok(voList);
return ResponseEntity.ok(voList);
}
@GetMapping("GetSchTeaList")
@@ -52,12 +52,12 @@ public class UserInfoController {
public ResponseEntity<List<UserInfoVO>> getUserList(@RequestParam String SchSerID) {
// 1. 鍙傛暟鏍¢獙
if (SchSerID == null || SchSerID.trim().isEmpty()) {
return ResponseEntity.badRequest().build();
return ResponseEntity.ok(new ArrayList<>());
}
SchInfoVO schInfoVO = schInfoService.GetSchFirst(SchSerID);
if (schInfoVO == null) {
return ResponseEntity.badRequest().build();
return ResponseEntity.ok(new ArrayList<>());
}
Integer schId = schInfoVO.getUserSchSerId();
@@ -81,7 +81,7 @@ public class UserInfoController {
}
// 4. 杩斿洖缁撴灉
return voList.isEmpty() ? ResponseEntity.noContent().build() : ResponseEntity.ok(voList);
return ResponseEntity.ok(voList);
}
// ===== 杞崲鏁欏笀 =====
@@ -157,10 +157,11 @@ public class UserInfoController {
@Operation(summary = "getUserIdList")
public ResponseEntity<List<UserInfoVO>> getUserIdList(@RequestParam Integer userid) {
if (userid == null) {
return ResponseEntity.badRequest().build();
return ResponseEntity.ok(new ArrayList<>());
}
Optional<SchTeacher> teacherOpt = schTeacherRepository.findByTeaId(userid);
// 同一 teaId/stuId 可能在多所学校各有记录,取第一条即可(不能按唯一查询)
Optional<SchTeacher> teacherOpt = schTeacherRepository.findByTeaId(userid).stream().findFirst();
if (teacherOpt.isPresent()) {
UserInfoVO vo = convertToUserInfoVO(teacherOpt.get());
if (vo != null) {
@@ -168,15 +169,14 @@ public class UserInfoController {
}
}
Optional<SchStudent> studentOpt = schStudentRepository.findByStuId(userid);
Optional<SchStudent> studentOpt = schStudentRepository.findByStuId(userid).stream().findFirst();
if (studentOpt.isPresent()) {
UserInfoVO vo = convertToUserInfoVO(studentOpt.get());
if (vo != null) {
return ResponseEntity.ok(Collections.singletonList(vo));
}
}
return ResponseEntity.notFound().build();
return ResponseEntity.ok(new ArrayList<>());
}
@@ -0,0 +1,182 @@
package com.yunhai.bschclassmanage.controller.oapi.sch;
import com.yunhai.bschclassmanage.Repository.SchClassRepository;
import com.yunhai.bschclassmanage.Repository.SchInfoRepository;
import com.yunhai.bschclassmanage.config.Config;
import com.yunhai.bschclassmanage.entity.SchClass;
import com.yunhai.bschclassmanage.model.vo.sch.OapiClassVO;
import com.yunhai.bschclassmanage.model.vo.sch.OapiGradeNameVO;
import com.yunhai.bschclassmanage.service.IBaseSchsystemGradeService;
import com.yunhai.bschclassmanage.service.IBaseSchsystemService;
import com.yunhai.bschclassmanage.service.ISchBinSchsystemService;
import com.yunhai.bschclassmanage.service.ISchClassService;
import com.yunhai.bschclassmanage.service.ISchTeacherService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.stream.Collectors;
@Tag(name = "OapiSchClass", description = "开放接口-班级(含班主任)")
@RestController
@RequestMapping(Config.OAPI + "/sch/")
@CrossOrigin(originPatterns = {"*"}, allowCredentials = "true")
public class OapiSchClassController {
@Autowired
private SchClassRepository schClassRepository;
@Autowired
private SchInfoRepository schInfoRepository;
@Autowired
private ISchClassService schClassService;
@Autowired
private ISchTeacherService schTeacherService;
@Autowired
private IBaseSchsystemGradeService baseSchsystemGradeService;
@Autowired
private ISchBinSchsystemService schBinSchsystemService;
@Autowired
private IBaseSchsystemService baseSchsystemService;
@GetMapping("getClassInfo")
@Operation(summary = "获取班级信息", description = "按班级ID(班级组ID)查询班级信息,含班主任/副班主任ID与姓名;可传 schId 精确到学校")
public ResponseEntity<OapiClassVO> getClassInfo(@RequestParam(value = "classId", required = false) Integer classId,
@RequestParam(value = "schId", required = false) Integer schId) {
if (classId == null) {
return ResponseEntity.ok(null);
}
Optional<SchClass> first = schId != null
? schClassRepository.findByClassIdAndSchId(classId, schId)
: schClassRepository.findAll().stream()
.filter(c -> classId.equals(c.getClassId()))
.findFirst();
return ResponseEntity.ok(first.map(c -> toVOList(Collections.singletonList(c)).get(0)).orElse(null));
}
@GetMapping("getGradeClassList")
@Operation(summary = "获取年级班级列表", description = "按年级编码查询班级列表;建议同时传 schId(走学校缓存),不传则跨校查询")
public ResponseEntity<List<OapiClassVO>> getGradeClassList(@RequestParam(value = "gradeId", required = false) Integer gradeId,
@RequestParam(value = "schId", required = false) Integer schId) {
if (gradeId == null) {
return ResponseEntity.ok(Collections.emptyList());
}
List<SchClass> classes = schId != null
? schClassService.getSchClassCacheFilter(schId, gradeId, null)
: schClassRepository.findAll().stream()
.filter(c -> gradeId.equals(c.getGradeId()) && !Boolean.TRUE.equals(c.getGraduate()))
.collect(Collectors.toList());
return ResponseEntity.ok(toVOList(classes));
}
@GetMapping("getSchClassList")
@Operation(summary = "获取全校班级列表", description = "按学校ID查询全部班级(含班主任);默认不含毕业班,includeGraduate=true 时含毕业班")
public ResponseEntity<List<OapiClassVO>> getSchClassList(@RequestParam(value = "schId", required = false) Integer schId,
@RequestParam(value = "includeGraduate", required = false, defaultValue = "false") boolean includeGraduate) {
if (schId == null) {
return ResponseEntity.ok(Collections.emptyList());
}
List<SchClass> classes = includeGraduate
? schClassService.getSchAllClassCacheList(schId)
: schClassService.getSchClassCacheList(schId);
return ResponseEntity.ok(toVOList(classes));
}
@GetMapping("getGradeName")
@Operation(summary = "获取年级名称", description = "按学校ID+入学年份+学段(1小学/2初中/3高中)查询学校年级字典名称(gradeNickname);"
+ "年份按当前学年与年级序换算(与班级导入同口径),无匹配学制/年级返回 null,由调用方自行降级")
public ResponseEntity<OapiGradeNameVO> getGradeName(@RequestParam("schId") Integer schId,
@RequestParam("year") Integer year,
@RequestParam(value = "stage", required = false) Integer stage) {
if (schId == null || year == null) {
return ResponseEntity.ok(null);
}
// 学段 → 学制:按学校各学制记录的学段名(小学/初中/高中)匹配;不传 stage 时不限学段
String stageName = stage == null ? null
: stage == 1 ? "小学" : stage == 2 ? "初中" : stage == 3 ? "高中" : null;
List<Integer> targetSystems = new ArrayList<>();
if (stageName == null) {
targetSystems.addAll(schBinSchsystemService.getSchBinSchsystemIds(schId));
} else {
for (Integer schsystemId : schBinSchsystemService.getSchBinSchsystemIds(schId)) {
try {
String name = baseSchsystemService.getAcademicStageName(schsystemId);
if (name != null && name.contains(stageName)) {
targetSystems.add(schsystemId);
}
} catch (Exception e) {
// 单个学制学段名缺失(学制未找到等)跳过,不影响其余学制
}
}
}
for (Integer schsystemId : targetSystems) {
// 年级→入学年份(当前学年-年级偏移,与班级导入 getNowYearGradeMap 同口径);再查字典别称
Map<Integer, Integer> yearMap = baseSchsystemGradeService.getNowYearGradeMap(schId, schsystemId);
if (yearMap == null || yearMap.isEmpty()) {
continue;
}
Map<Integer, String> nameMap = baseSchsystemGradeService.getGradeNameMap(schId, schsystemId, null);
for (Map.Entry<Integer, Integer> e : yearMap.entrySet()) {
if (year.equals(e.getValue()) && nameMap.get(e.getKey()) != null) {
OapiGradeNameVO vo = new OapiGradeNameVO();
vo.setGradeId(e.getKey());
vo.setGradeName(nameMap.get(e.getKey()));
vo.setYear(year);
return ResponseEntity.ok(vo);
}
}
}
return ResponseEntity.ok(null);
}
/** 批量转 VO:按学校分组后统一回填学校名/年级名/班主任姓名 */
private List<OapiClassVO> toVOList(List<SchClass> classes) {
if (classes == null || classes.isEmpty()) {
return Collections.emptyList();
}
Map<Integer, List<SchClass>> bySchool = classes.stream()
.filter(c -> c.getSchId() != null)
.collect(Collectors.groupingBy(SchClass::getSchId, LinkedHashMap::new, Collectors.toList()));
List<OapiClassVO> result = new ArrayList<>();
for (Map.Entry<Integer, List<SchClass>> entry : bySchool.entrySet()) {
Integer schId = entry.getKey();
String schName = schInfoRepository.findBySchId(schId).map(s -> s.getName()).orElse(null);
Map<Integer, String> gradeNameMap = baseSchsystemGradeService.getGradeNameMap(schId, null, null);
Map<Integer, String> teacherNameMap = schTeacherService.toIdAndNameMap(schId);
for (SchClass clazz : entry.getValue()) {
result.add(toVO(clazz, schName, gradeNameMap, teacherNameMap));
}
}
// 无学校ID的脏数据兜底转换
classes.stream()
.filter(c -> c.getSchId() == null)
.forEach(c -> result.add(toVO(c, null, Collections.emptyMap(), Collections.emptyMap())));
return result;
}
private OapiClassVO toVO(SchClass clazz, String schName,
Map<Integer, String> gradeNameMap, Map<Integer, String> teacherNameMap) {
OapiClassVO vo = new OapiClassVO();
vo.setId(clazz.getId());
vo.setClassId(clazz.getClassId());
vo.setCode(clazz.getCode());
vo.setName(clazz.getName());
vo.setCname(clazz.getCname());
vo.setGradeId(clazz.getGradeId());
vo.setGradeName(clazz.getGradeId() == null ? null : gradeNameMap.get(clazz.getGradeId()));
vo.setSchId(clazz.getSchId());
vo.setSchName(schName);
vo.setFirstTeacherId(clazz.getFirstTeacherId());
vo.setFirstTeacherName(clazz.getFirstTeacherId() == null ? null : teacherNameMap.get(clazz.getFirstTeacherId()));
vo.setSecondTeacherId(clazz.getSecondTeacherId());
vo.setSecondTeacherName(clazz.getSecondTeacherId() == null ? null : teacherNameMap.get(clazz.getSecondTeacherId()));
vo.setCampusId(clazz.getCampusId());
vo.setEnrollYear(clazz.getEnrollYear());
vo.setGraduate(clazz.getGraduate());
vo.setSort(clazz.getSort());
return vo;
}
}
@@ -0,0 +1,309 @@
package com.yunhai.bschclassmanage.controller.oapi.sch;
import com.yunhai.bschclassmanage.Repository.SchTeacherCustomGroupInGradeRepository;
import com.yunhai.bschclassmanage.Repository.SchTeacherCustomGroupInSubjectRepository;
import com.yunhai.bschclassmanage.Repository.SchTeacherCustomGroupInTeacherRepository;
import com.yunhai.bschclassmanage.Repository.SchTeacherCustomGroupRepository;
import com.yunhai.bschclassmanage.config.Config;
import com.yunhai.bschclassmanage.entity.SchTeacher;
import com.yunhai.bschclassmanage.entity.SchTeacherCustomGroup;
import com.yunhai.bschclassmanage.entity.SchTeacherCustomGroupInGrade;
import com.yunhai.bschclassmanage.entity.SchTeacherCustomGroupInSubject;
import com.yunhai.bschclassmanage.entity.SchTeacherCustomGroupInTeacher;
import com.yunhai.bschclassmanage.enums.CustomGroupType;
import com.yunhai.bschclassmanage.model.oa.TeaTreeVO;
import com.yunhai.bschclassmanage.model.vo.SelectIntVO;
import com.yunhai.bschclassmanage.model.vo.SelectStrVO;
import com.yunhai.bschclassmanage.model.vo.sch.OapiGroupVO;
import com.yunhai.bschclassmanage.model.vo.sch.TeaCustomGroupTreeVO;
import com.yunhai.bschclassmanage.service.IBaseSchsystemGradeService;
import com.yunhai.bschclassmanage.service.IComSchTreeService;
import com.yunhai.bschclassmanage.service.ISchDicSubjectService;
import com.yunhai.bschclassmanage.service.ISchTeacherService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.stream.Collectors;
@Tag(name = "OapiSchGroup", description = "开放接口-学校组(教师组树/学生组树/年级组教研组等自定义组树)")
@RestController
@RequestMapping(Config.OAPI + "/sch/")
@CrossOrigin(originPatterns = {"*"}, allowCredentials = "true")
public class OapiSchGroupController {
@Autowired
private IComSchTreeService comSchTreeService;
@Autowired
private ISchTeacherService schTeacherService;
@Autowired
private IBaseSchsystemGradeService baseSchsystemGradeService;
@Autowired
private ISchDicSubjectService schDicSubjectService;
@Autowired
private SchTeacherCustomGroupRepository customGroupRepository;
@Autowired
private SchTeacherCustomGroupInGradeRepository groupInGradeRepository;
@Autowired
private SchTeacherCustomGroupInSubjectRepository groupInSubjectRepository;
@Autowired
private SchTeacherCustomGroupInTeacherRepository groupInTeacherRepository;
@GetMapping("getTeaGroupTree")
@Operation(summary = "获取教师组树+教师", description = "学校→教师组(含子组)→教师;toUser=false 时仅返回组骨架。与 oa/SchGroup/GetTeaTree 同源同结构")
public ResponseEntity<List<TeaTreeVO>> getTeaGroupTree(@RequestParam(value = "schId", required = false) Integer schId,
@RequestParam(value = "toUser", required = false, defaultValue = "true") boolean toUser) {
if (schId == null) {
return ResponseEntity.ok(Collections.emptyList());
}
return ResponseEntity.ok(comSchTreeService.getSchTeaTree(schId, toUser));
}
@GetMapping("getStuGroupTree")
@Operation(summary = "获取学生组树+学生", description = "学校→学段→年级→班级→学生;toUser=false 时仅返回组织骨架。与 oa/SchGroup/GetStuTree 同源同结构")
public ResponseEntity<List<TeaTreeVO>> getStuGroupTree(@RequestParam(value = "schId", required = false) Integer schId,
@RequestParam(value = "toUser", required = false, defaultValue = "true") boolean toUser) {
if (schId == null) {
return ResponseEntity.ok(Collections.emptyList());
}
return ResponseEntity.ok(comSchTreeService.getSchStuTree(schId, toUser));
}
@GetMapping("getTeaCustomGroupTree")
@Operation(summary = "获取教师自定义组树+教师(年级组/教研组/备课组)",
description = "不传 groupType 时返回三层:类型根节点(id=-1年级组/-2教研组/-3备课组)→ 组节点 → 教师叶子;"
+ "传 groupType(1年级组 2教研组 3备课组,教研组即教师学科组)时直接返回该类型组节点列表(两层)。"
+ "组节点携带组长、关联年级、关联学科;教师叶子携带 isLeader")
public ResponseEntity<List<TeaCustomGroupTreeVO>> getTeaCustomGroupTree(@RequestParam(value = "schId") Integer schId,
@RequestParam(value = "groupType", required = false) Integer groupType) {
if (schId == null) {
return ResponseEntity.ok(Collections.emptyList());
}
List<SchTeacherCustomGroup> groups = (groupType != null)
? customGroupRepository.findBySchIdAndGroupType(schId, groupType)
: customGroupRepository.findBySchId(schId);
if (groups == null || groups.isEmpty()) {
return ResponseEntity.ok(Collections.emptyList());
}
groups = groups.stream()
.sorted(Comparator.comparing(SchTeacherCustomGroup::getSort, Comparator.nullsLast(Integer::compareTo)))
.collect(Collectors.toList());
return ResponseEntity.ok(buildTree(schId, groups, groupType != null));
}
@GetMapping("getTeaCustomGroupList")
@Operation(summary = "获取教师分组列表(组/组员/组长合一)",
description = "按学校查教师分组列表:组id、组名称、学科集合、年级集合、组员集合、组长集合;"
+ "groupType 可选(1年级组 2教研组 3备课组,不传返回全部类型)。"
+ "组长取关系表 group_type=1 的全部教师(取不到时兜底组记录 leader_id 字段),组员取 group_type=2 且排除组长")
public ResponseEntity<List<OapiGroupVO>> getTeaCustomGroupList(@RequestParam(value = "schId") Integer schId,
@RequestParam(value = "groupType", required = false) Integer groupType) {
if (schId == null) {
return ResponseEntity.ok(Collections.emptyList());
}
List<SchTeacherCustomGroup> groups = (groupType != null)
? customGroupRepository.findBySchIdAndGroupType(schId, groupType)
: customGroupRepository.findBySchId(schId);
if (groups == null || groups.isEmpty()) {
return ResponseEntity.ok(Collections.emptyList());
}
groups = groups.stream()
.sorted(Comparator.comparing(SchTeacherCustomGroup::getSort, Comparator.nullsLast(Integer::compareTo)))
.collect(Collectors.toList());
List<Integer> groupIds = groups.stream().map(SchTeacherCustomGroup::getId).collect(Collectors.toList());
Map<Integer, List<SchTeacherCustomGroupInGrade>> groupIdToGrades = groupInGradeRepository.findByCgGroupIdIn(groupIds).stream()
.collect(Collectors.groupingBy(SchTeacherCustomGroupInGrade::getCgGroupId));
Map<Integer, List<SchTeacherCustomGroupInSubject>> groupIdToSubjects = groupInSubjectRepository.findByCgGroupIdIn(groupIds).stream()
.collect(Collectors.groupingBy(SchTeacherCustomGroupInSubject::getCgGroupId));
// relationType:1组长 2组员
Map<Integer, List<SchTeacherCustomGroupInTeacher>> groupIdToTeachers = groupInTeacherRepository.findByCgGroupIdIn(groupIds).stream()
.filter(r -> r.getTeaId() != null)
.collect(Collectors.groupingBy(SchTeacherCustomGroupInTeacher::getCgGroupId));
Map<Integer, SchTeacher> teacherMap = schTeacherService.getSchTeacherCacheList(schId).stream()
.filter(t -> t.getTeaId() != null)
.collect(Collectors.toMap(SchTeacher::getTeaId, t -> t, (a, b) -> a));
Map<Integer, String> gradeNameMap = baseSchsystemGradeService.getGradeNameMap(schId, null, null);
Map<String, String> subjectNameMap = schDicSubjectService.toIdAndNameMap(schId, null);
List<OapiGroupVO> result = new ArrayList<>(groups.size());
for (SchTeacherCustomGroup group : groups) {
List<SchTeacherCustomGroupInTeacher> relations = groupIdToTeachers.getOrDefault(group.getId(), Collections.emptyList());
List<Integer> leaderIds = resolveLeaders(group, relations);
OapiGroupVO vo = new OapiGroupVO();
vo.setGroupId(group.getId());
vo.setGroupName(group.getGroupName());
vo.setGroupType(group.getGroupType());
vo.setGroupTypeName(group.getGroupType() == null ? null : CustomGroupType.nameById(group.getGroupType()));
vo.setSubjects(groupIdToSubjects.getOrDefault(group.getId(), Collections.emptyList()).stream()
.map(SchTeacherCustomGroupInSubject::getSubId)
.filter(Objects::nonNull)
.distinct()
.map(subId -> new SelectStrVO(subId, subjectNameMap.get(subId)))
.collect(Collectors.toList()));
vo.setGrades(groupIdToGrades.getOrDefault(group.getId(), Collections.emptyList()).stream()
.map(SchTeacherCustomGroupInGrade::getGradeId)
.filter(Objects::nonNull)
.distinct()
.map(gradeId -> new SelectIntVO(gradeId, gradeNameMap.get(gradeId)))
.collect(Collectors.toList()));
vo.setLeaders(toIdNameList(leaderIds, teacherMap));
vo.setMembers(toIdNameList(relations.stream()
.filter(r -> Integer.valueOf(2).equals(r.getGroupType()))
.map(SchTeacherCustomGroupInTeacher::getTeaId)
.filter(id -> !leaderIds.contains(id))
.distinct()
.collect(Collectors.toList()), teacherMap));
result.add(vo);
}
return ResponseEntity.ok(result);
}
/** 组长ID集合:关系表 group_type=1 的全部教师,取不到时兜底组记录 leader_id 字段(与 getSchCustomGroup 语义一致) */
private List<Integer> resolveLeaders(SchTeacherCustomGroup group, List<SchTeacherCustomGroupInTeacher> relations) {
List<Integer> leaderIds = relations.stream()
.filter(r -> Integer.valueOf(1).equals(r.getGroupType()))
.map(SchTeacherCustomGroupInTeacher::getTeaId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (leaderIds.isEmpty() && group.getLeaderId() != null) {
leaderIds = Collections.singletonList(group.getLeaderId());
}
return leaderIds;
}
/** 教师ID集合 → [{id,姓名}],按入参顺序;姓名取不到时与 getSchCustomGroup 一致回退"未知教师" */
private List<SelectIntVO> toIdNameList(List<Integer> teaIds, Map<Integer, SchTeacher> teacherMap) {
return teaIds.stream()
.map(teaId -> new SelectIntVO(teaId,
Optional.ofNullable(teacherMap.get(teaId)).map(SchTeacher::getName).orElse("未知教师")))
.collect(Collectors.toList());
}
/** 组装类型节点→组节点→教师叶子树(flatTypeRoot=true 时不建类型根,组节点直接作为顶层) */
private List<TeaCustomGroupTreeVO> buildTree(Integer schId, List<SchTeacherCustomGroup> groups, boolean flatTypeRoot) {
List<Integer> groupIds = groups.stream().map(SchTeacherCustomGroup::getId).collect(Collectors.toList());
Map<Integer, List<Integer>> groupIdToGradeIds = groupInGradeRepository.findByCgGroupIdIn(groupIds).stream()
.collect(Collectors.groupingBy(SchTeacherCustomGroupInGrade::getCgGroupId,
Collectors.mapping(SchTeacherCustomGroupInGrade::getGradeId, Collectors.toList())));
Map<Integer, List<String>> groupIdToSubIds = groupInSubjectRepository.findByCgGroupIdIn(groupIds).stream()
.collect(Collectors.groupingBy(SchTeacherCustomGroupInSubject::getCgGroupId,
Collectors.mapping(SchTeacherCustomGroupInSubject::getSubId, Collectors.toList())));
// relationType:1组长 2组员
Map<Integer, List<SchTeacherCustomGroupInTeacher>> groupIdToTeachers = groupInTeacherRepository.findByCgGroupIdIn(groupIds).stream()
.filter(r -> r.getTeaId() != null)
.collect(Collectors.groupingBy(SchTeacherCustomGroupInTeacher::getCgGroupId));
Map<Integer, SchTeacher> teacherMap = schTeacherService.getSchTeacherCacheList(schId).stream()
.filter(t -> t.getTeaId() != null)
.collect(Collectors.toMap(SchTeacher::getTeaId, t -> t, (a, b) -> a));
Map<Integer, String> gradeNameMap = baseSchsystemGradeService.getGradeNameMap(schId, null, null);
Map<String, String> subjectNameMap = schDicSubjectService.toIdAndNameMap(schId, null);
Map<Integer, List<TeaCustomGroupTreeVO>> groupNodesByType = new LinkedHashMap<>();
for (SchTeacherCustomGroup group : groups) {
TeaCustomGroupTreeVO groupNode = buildGroupNode(group, flatTypeRoot,
groupIdToGradeIds, groupIdToSubIds, groupIdToTeachers, teacherMap, gradeNameMap, subjectNameMap);
groupNodesByType.computeIfAbsent(group.getGroupType(), k -> new ArrayList<>()).add(groupNode);
}
if (flatTypeRoot) {
return groupNodesByType.values().stream().flatMap(List::stream).collect(Collectors.toList());
}
// 类型根节点(id = -groupType),只输出实际有组的类型,按 1年级组 2教研组 3备课组 顺序
List<TeaCustomGroupTreeVO> roots = new ArrayList<>();
for (CustomGroupType type : CustomGroupType.list()) {
List<TeaCustomGroupTreeVO> children = groupNodesByType.get(type.getId());
if (children == null || children.isEmpty()) {
continue;
}
TeaCustomGroupTreeVO typeNode = new TeaCustomGroupTreeVO();
typeNode.setId(-type.getId());
typeNode.setName(type.getName());
typeNode.setPid(0);
typeNode.setNo(type.getId());
typeNode.setType(1);
typeNode.setIsParent(true);
typeNode.setGroupType(type.getId());
typeNode.setGroupTypeName(type.getName());
typeNode.setChildren(children);
roots.add(typeNode);
}
return roots;
}
private TeaCustomGroupTreeVO buildGroupNode(SchTeacherCustomGroup group, boolean flatTypeRoot,
Map<Integer, List<Integer>> groupIdToGradeIds,
Map<Integer, List<String>> groupIdToSubIds,
Map<Integer, List<SchTeacherCustomGroupInTeacher>> groupIdToTeachers,
Map<Integer, SchTeacher> teacherMap,
Map<Integer, String> gradeNameMap,
Map<String, String> subjectNameMap) {
TeaCustomGroupTreeVO node = new TeaCustomGroupTreeVO();
node.setId(group.getId());
node.setName(group.getGroupName());
node.setPid(flatTypeRoot ? 0 : (group.getGroupType() == null ? null : -group.getGroupType()));
node.setNo(group.getSort());
node.setType(1);
node.setIsParent(true);
node.setGroupType(group.getGroupType());
node.setGroupTypeName(group.getGroupType() == null ? null : CustomGroupType.nameById(group.getGroupType()));
node.setLeaderId(group.getLeaderId());
node.setLeaderName(group.getLeaderId() == null ? null
: Optional.ofNullable(teacherMap.get(group.getLeaderId())).map(SchTeacher::getName).orElse(null));
List<Integer> gradeIds = groupIdToGradeIds.getOrDefault(group.getId(), Collections.emptyList());
node.setGradeIds(gradeIds);
node.setGradeNames(gradeIds.stream()
.map(gradeNameMap::get)
.filter(Objects::nonNull)
.collect(Collectors.toList()));
List<String> subIds = groupIdToSubIds.getOrDefault(group.getId(), Collections.emptyList());
node.setSubjectIds(subIds);
node.setSubjectNames(subIds.stream()
.map(subjectNameMap::get)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList()));
// 教师叶子:组长(relationType=1)在前,组员在后;同组内按 teaId 去重
List<SchTeacherCustomGroupInTeacher> relations = groupIdToTeachers.getOrDefault(group.getId(), Collections.emptyList());
List<TeaCustomGroupTreeVO> teacherNodes = new ArrayList<>();
Set<Integer> seenTeaIds = new HashSet<>();
relations.stream().filter(r -> Integer.valueOf(1).equals(r.getGroupType()))
.forEach(r -> addTeacherNode(teacherNodes, seenTeaIds, r, group.getId(), teacherMap));
relations.stream().filter(r -> !Integer.valueOf(1).equals(r.getGroupType()))
.forEach(r -> addTeacherNode(teacherNodes, seenTeaIds, r, group.getId(), teacherMap));
node.setChildren(teacherNodes);
return node;
}
private void addTeacherNode(List<TeaCustomGroupTreeVO> teacherNodes, Set<Integer> seenTeaIds,
SchTeacherCustomGroupInTeacher relation, Integer groupId,
Map<Integer, SchTeacher> teacherMap) {
if (!seenTeaIds.add(relation.getTeaId())) {
return;
}
SchTeacher teacher = teacherMap.get(relation.getTeaId());
if (teacher == null) {
return;
}
TeaCustomGroupTreeVO leaf = new TeaCustomGroupTreeVO();
leaf.setId(relation.getTeaId());
leaf.setName(teacher.getName());
leaf.setPid(groupId);
leaf.setNo(0);
leaf.setType(2);
leaf.setIsParent(false);
leaf.setIsLeader(Integer.valueOf(1).equals(relation.getGroupType()));
teacherNodes.add(leaf);
}
}
@@ -0,0 +1,127 @@
package com.yunhai.bschclassmanage.controller.oapi.sch;
import com.yunhai.bschclassmanage.Repository.BaseSchsystemRepository;
import com.yunhai.bschclassmanage.Repository.SchInfoRepository;
import com.yunhai.bschclassmanage.config.Config;
import com.yunhai.bschclassmanage.entity.BaseSchsystem;
import com.yunhai.bschclassmanage.entity.BaseSchsystemGrade;
import com.yunhai.bschclassmanage.entity.SchInfo;
import com.yunhai.bschclassmanage.enums.AcademicStage;
import com.yunhai.bschclassmanage.model.vo.SelectIntVO;
import com.yunhai.bschclassmanage.model.vo.sch.GradeItemVO;
import com.yunhai.bschclassmanage.model.vo.sch.SchInfoDetailVO;
import com.yunhai.bschclassmanage.model.vo.sch.SchSystemDetailVO;
import com.yunhai.bschclassmanage.service.IBaseSchsystemGradeService;
import com.yunhai.bschclassmanage.service.ISchBinSchsystemService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.stream.Collectors;
@Tag(name = "OapiSchInfo", description = "开放接口-学校信息(含学段/学制)")
@RestController
@RequestMapping(Config.OAPI + "/sch/")
@CrossOrigin(originPatterns = {"*"}, allowCredentials = "true")
public class OapiSchInfoController {
@Autowired
private SchInfoRepository schInfoRepository;
@Autowired
private BaseSchsystemRepository baseSchsystemRepository;
@Autowired
private ISchBinSchsystemService schBinSchsystemService;
@Autowired
private IBaseSchsystemGradeService baseSchsystemGradeService;
@GetMapping("getSchInfo")
@Operation(summary = "获取学校信息", description = "按 schId(用户中心学校组ID)查询学校信息,含学段/学制信息")
public ResponseEntity<SchInfoDetailVO> getSchInfo(@RequestParam(value = "schId", required = false) Integer schId) {
if (schId == null) {
return ResponseEntity.ok(null);
}
return ResponseEntity.ok(schInfoRepository.findBySchId(schId).map(this::toDetail).orElse(null));
}
@GetMapping("getSchInfoList")
@Operation(summary = "获取学校列表", description = "按 schIds(逗号分隔)查询学校列表,schIds 为空时返回全部学校,含学段/学制信息")
public ResponseEntity<List<SchInfoDetailVO>> getSchInfoList(@RequestParam(value = "schIds", required = false) String schIds) {
List<SchInfo> schools;
Set<Integer> idSet = parseInts(schIds);
if (idSet.isEmpty()) {
schools = schInfoRepository.findAll();
} else {
schools = schInfoRepository.findAll().stream()
.filter(s -> idSet.contains(s.getSchId()))
.collect(Collectors.toList());
}
return ResponseEntity.ok(schools.stream().map(this::toDetail).collect(Collectors.toList()));
}
/** 学校实体 → 详情 VO(拼装学段/学制信息) */
private SchInfoDetailVO toDetail(SchInfo schInfo) {
SchInfoDetailVO vo = new SchInfoDetailVO();
vo.setSchId(schInfo.getSchId());
vo.setSchName(schInfo.getName());
vo.setSchSerId(schInfo.getSchSerId());
vo.setSort(schInfo.getSort());
List<SelectIntVO> systemList = schBinSchsystemService.selectSchBinSchsystem(schInfo.getSchId());
if (systemList == null || systemList.isEmpty()) {
vo.setSchSystemList(Collections.emptyList());
return vo;
}
List<Integer> systemIds = systemList.stream().map(SelectIntVO::getId).collect(Collectors.toList());
Map<Integer, BaseSchsystem> systemMap = baseSchsystemRepository.findAllById(systemIds).stream()
.collect(Collectors.toMap(BaseSchsystem::getId, s -> s, (a, b) -> a));
vo.setSchSystemList(systemIds.stream()
.map(systemMap::get)
.filter(Objects::nonNull)
.map(system -> toSystemDetail(schInfo.getSchId(), system))
.collect(Collectors.toList()));
return vo;
}
/** 学制实体 → 学制 VO(含学段名称与该学制年级) */
private SchSystemDetailVO toSystemDetail(Integer schId, BaseSchsystem system) {
SchSystemDetailVO vo = new SchSystemDetailVO();
vo.setId(system.getId());
vo.setName(system.getName());
vo.setAcademicStage(system.getAcademicStage());
vo.setAcademicStageName(system.getAcademicStage() == null ? null : AcademicStage.nameById(system.getAcademicStage()));
vo.setStartGradeCode(system.getStartGradeCode());
vo.setEndGradeCode(system.getEndGradeCode());
vo.setGraduationLeng(system.getGraduationLeng());
vo.setType(system.getType());
List<BaseSchsystemGrade> gradeList = baseSchsystemGradeService.getSchGradeCacheList(schId, system.getId(), null);
vo.setGradeList(gradeList == null ? Collections.emptyList() : gradeList.stream()
.map(g -> {
GradeItemVO item = new GradeItemVO();
item.setGradeId(g.getGradeId());
item.setGradeNickname(g.getGradeNickname());
return item;
})
.collect(Collectors.toList()));
return vo;
}
private Set<Integer> parseInts(String value) {
Set<Integer> result = new LinkedHashSet<>();
if (value == null || value.trim().isEmpty()) {
return result;
}
for (String token : value.split(",")) {
try {
result.add(Integer.valueOf(token.trim()));
} catch (NumberFormatException ignored) {
}
}
return result;
}
}
@@ -0,0 +1,111 @@
package com.yunhai.bschclassmanage.controller.oapi.sch;
import com.yunhai.bschclassmanage.Repository.SchClassRepository;
import com.yunhai.bschclassmanage.Repository.SchInfoRepository;
import com.yunhai.bschclassmanage.Repository.SchStudentRepository;
import com.yunhai.bschclassmanage.config.Config;
import com.yunhai.bschclassmanage.entity.SchClass;
import com.yunhai.bschclassmanage.entity.SchStudent;
import com.yunhai.bschclassmanage.model.vo.sch.OapiStudentVO;
import com.yunhai.bschclassmanage.service.IBaseSchsystemGradeService;
import com.yunhai.bschclassmanage.service.ISchClassService;
import com.yunhai.bschclassmanage.service.ISchStudentService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.stream.Collectors;
@Tag(name = "OapiSchStudent", description = "开放接口-学生")
@RestController
@RequestMapping(Config.OAPI + "/sch/")
@CrossOrigin(originPatterns = {"*"}, allowCredentials = "true")
public class OapiSchStudentController {
@Autowired
private SchStudentRepository schStudentRepository;
@Autowired
private SchClassRepository schClassRepository;
@Autowired
private SchInfoRepository schInfoRepository;
@Autowired
private ISchStudentService schStudentService;
@Autowired
private ISchClassService schClassService;
@Autowired
private IBaseSchsystemGradeService baseSchsystemGradeService;
@GetMapping("getStudentInfo")
@Operation(summary = "获取学生信息", description = "按学生ID(用户中心用户ID)查询学生信息,含班级/年级/学校名称")
public ResponseEntity<OapiStudentVO> getStudentInfo(@RequestParam(value = "stuId", required = false) Integer stuId) {
if (stuId == null) {
return ResponseEntity.ok(null);
}
// 同一 stu_id 可能在多所学校各有记录,取第一条即可(不能按唯一查询)
return ResponseEntity.ok(schStudentRepository.findByStuId(stuId).stream()
.findFirst()
.map(stu -> toVOList(stu.getSchId(), Collections.singletonList(stu)).get(0))
.orElse(null));
}
@GetMapping("getClassStudentList")
@Operation(summary = "获取班级学生列表", description = "按学校ID+班级ID(班级组ID)查询班级学生列表(在读正常)")
public ResponseEntity<List<OapiStudentVO>> getClassStudentList(@RequestParam(value = "schId", required = false) Integer schId,
@RequestParam(value = "classId", required = false) Integer classId) {
if (schId == null || classId == null) {
return ResponseEntity.ok(Collections.emptyList());
}
List<SchStudent> students = schStudentService.getSchGradeClassStudentCacheList(schId, null, null, classId);
return ResponseEntity.ok(toVOList(schId, students));
}
@GetMapping("getSchStudentList")
@Operation(summary = "获取全校学生列表", description = "按学校ID查询全校学生列表(在读正常)")
public ResponseEntity<List<OapiStudentVO>> getSchStudentList(@RequestParam(value = "schId", required = false) Integer schId) {
if (schId == null) {
return ResponseEntity.ok(Collections.emptyList());
}
List<SchStudent> students = schStudentService.getSchGradeClassStudentCacheList(schId, null, null, null);
return ResponseEntity.ok(toVOList(schId, students));
}
/** 批量转 VO:统一回填学校/年级/班级名称 */
private List<OapiStudentVO> toVOList(Integer schId, List<SchStudent> students) {
if (students == null || students.isEmpty()) {
return Collections.emptyList();
}
String schName = schId == null ? null
: schInfoRepository.findBySchId(schId).map(s -> s.getName()).orElse(null);
Map<Integer, String> gradeNameMap = schId == null ? Collections.emptyMap()
: baseSchsystemGradeService.getGradeNameMap(schId, null, null);
Map<Integer, SchClass> classMap = schId == null ? Collections.emptyMap()
: schClassService.getSchAllClassCacheList(schId).stream()
.filter(c -> c.getClassId() != null)
.collect(Collectors.toMap(SchClass::getClassId, c -> c, (a, b) -> a));
return students.stream().map(stu -> {
OapiStudentVO vo = new OapiStudentVO();
vo.setId(stu.getId());
vo.setStuId(stu.getStuId());
vo.setAccount(stu.getAccount());
vo.setName(stu.getName());
vo.setGender(stu.getGender());
vo.setStuClassNo(stu.getStuClassNo());
vo.setClassId(stu.getClassId());
vo.setStatus(stu.getStatus());
vo.setSchId(schId);
vo.setSchName(schName);
SchClass clazz = stu.getClassId() == null ? null : classMap.get(stu.getClassId());
if (clazz != null) {
vo.setClassName(clazz.getCname() == null || clazz.getCname().trim().isEmpty()
? clazz.getName() : clazz.getCname());
vo.setGradeId(clazz.getGradeId());
}
vo.setGradeName(vo.getGradeId() == null ? null : gradeNameMap.get(vo.getGradeId()));
return vo;
}).collect(Collectors.toList());
}
}
@@ -0,0 +1,68 @@
package com.yunhai.bschclassmanage.controller.oapi.sch;
import com.yunhai.bschclassmanage.config.Config;
import com.yunhai.bschclassmanage.entity.SchDicSubject;
import com.yunhai.bschclassmanage.model.vo.sch.OapiSubjectVO;
import com.yunhai.bschclassmanage.service.ISchDicSubjectService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Tag(name = "OapiSchSubject", description = "开放接口-学科")
@RestController
@RequestMapping(Config.OAPI + "/sch/")
@CrossOrigin(originPatterns = {"*"}, allowCredentials = "true")
public class OapiSchSubjectController {
@Autowired
private ISchDicSubjectService schDicSubjectService;
@GetMapping("getSubjectInfo")
@Operation(summary = "获取学科信息", description = "按学校ID+学科ID(sch_dic_subject.id 主键,兼容传学科编码 code)查询单个学科")
public ResponseEntity<OapiSubjectVO> getSubjectInfo(@RequestParam(value = "schId", required = false) Integer schId,
@RequestParam(value = "subjectId", required = false) String subjectId) {
if (schId == null || subjectId == null || subjectId.trim().isEmpty()) {
return ResponseEntity.ok(null);
}
String code = subjectId.trim();
return schDicSubjectService.getSchSubList(schId, null).stream()
.filter(s -> code.equals(String.valueOf(s.getId())) || code.equals(s.getCode()))
.findFirst()
.map(this::toVO)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.ok(null));
}
@GetMapping("getSchSubjectList")
@Operation(summary = "获取学校学科列表", description = "按学校ID查询学科列表,可传 schSystemId 按学制过滤")
public ResponseEntity<List<OapiSubjectVO>> getSchSubjectList(@RequestParam(value = "schId", required = false) Integer schId,
@RequestParam(value = "schSystemId", required = false) Integer schSystemId) {
if (schId == null) {
return ResponseEntity.ok(Collections.emptyList());
}
return ResponseEntity.ok(schDicSubjectService.getSchSubList(schId, schSystemId).stream()
.filter(Objects::nonNull)
.map(this::toVO)
.collect(Collectors.toList()));
}
private OapiSubjectVO toVO(SchDicSubject subject) {
OapiSubjectVO vo = new OapiSubjectVO();
vo.setId(subject.getId());
vo.setCode(subject.getCode());
vo.setName(subject.getName());
vo.setSchSystemId(subject.getSchSystemId());
vo.setEnable(subject.getEnable());
vo.setSource(subject.getSource());
vo.setSort(subject.getSort());
vo.setSchId(subject.getSchId());
return vo;
}
}
@@ -0,0 +1,116 @@
package com.yunhai.bschclassmanage.controller.oapi.sch;
import com.yunhai.bschclassmanage.Repository.SchClassTeachRepository;
import com.yunhai.bschclassmanage.config.Config;
import com.yunhai.bschclassmanage.entity.SchClass;
import com.yunhai.bschclassmanage.entity.SchClassTeach;
import com.yunhai.bschclassmanage.model.vo.sch.OapiTeachVO;
import com.yunhai.bschclassmanage.service.IBaseSchsystemGradeService;
import com.yunhai.bschclassmanage.service.ISchClassService;
import com.yunhai.bschclassmanage.service.ISchClassTeachService;
import com.yunhai.bschclassmanage.service.ISchDicSubjectService;
import com.yunhai.bschclassmanage.service.ISchTeacherService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.stream.Collectors;
@Tag(name = "OapiSchTeach", description = "开放接口-班级任教")
@RestController
@RequestMapping(Config.OAPI + "/sch/")
@CrossOrigin(originPatterns = {"*"}, allowCredentials = "true")
public class OapiSchTeachController {
@Autowired
private SchClassTeachRepository schClassTeachRepository;
@Autowired
private ISchClassTeachService schClassTeachService;
@Autowired
private ISchClassService schClassService;
@Autowired
private ISchTeacherService schTeacherService;
@Autowired
private ISchDicSubjectService schDicSubjectService;
@Autowired
private IBaseSchsystemGradeService baseSchsystemGradeService;
@GetMapping("getClassTeachList")
@Operation(summary = "获取班级任教列表", description = "按学校ID+班级ID(班级组ID)查询班级任教的教师与学科")
public ResponseEntity<List<OapiTeachVO>> getClassTeachList(@RequestParam(value = "schId", required = false) Integer schId,
@RequestParam(value = "classId", required = false) Integer classId) {
if (schId == null || classId == null) {
return ResponseEntity.ok(Collections.emptyList());
}
return ResponseEntity.ok(toVOList(schId, schClassTeachService.getSchClassTeachCacheList(schId, null, classId)));
}
@GetMapping("getTeacherTeachList")
@Operation(summary = "获取教师任教列表", description = "按教师ID(teaId)查询任教记录;可传 schId 限定学校(走缓存),不传则跨校查询")
public ResponseEntity<List<OapiTeachVO>> getTeacherTeachList(@RequestParam(value = "teaId", required = false) Integer teaId,
@RequestParam(value = "schId", required = false) Integer schId) {
if (teaId == null) {
return ResponseEntity.ok(Collections.emptyList());
}
if (schId != null) {
List<SchClassTeach> teaches = schClassTeachService.getSchClassTeachCacheList(schId, null, null).stream()
.filter(t -> teaId.equals(t.getTeaId()))
.collect(Collectors.toList());
return ResponseEntity.ok(toVOList(schId, teaches));
}
Map<Integer, List<SchClassTeach>> bySchool = schClassTeachRepository.findAll().stream()
.filter(t -> teaId.equals(t.getTeaId()) && t.getSchId() != null)
.collect(Collectors.groupingBy(SchClassTeach::getSchId, LinkedHashMap::new, Collectors.toList()));
List<OapiTeachVO> result = new ArrayList<>();
bySchool.forEach((schoolId, teaches) -> result.addAll(toVOList(schoolId, teaches)));
return ResponseEntity.ok(result);
}
@GetMapping("getSchTeachList")
@Operation(summary = "获取全校任教列表", description = "按学校ID查询全校任教记录(未毕业班级)")
public ResponseEntity<List<OapiTeachVO>> getSchTeachList(@RequestParam(value = "schId", required = false) Integer schId) {
if (schId == null) {
return ResponseEntity.ok(Collections.emptyList());
}
return ResponseEntity.ok(toVOList(schId, schClassTeachService.getSchClassTeachCacheList(schId, null, null)));
}
/** 批量转 VO:按学校统一回填教师姓名/学科名称/班级名称/年级名称 */
private List<OapiTeachVO> toVOList(Integer schId, List<SchClassTeach> teaches) {
if (teaches == null || teaches.isEmpty()) {
return Collections.emptyList();
}
Map<Integer, String> teacherNameMap = schTeacherService.toIdAndNameMap(schId);
Map<String, String> subjectNameMap = schDicSubjectService.toIdAndNameMap(schId, null);
Map<Integer, String> gradeNameMap = baseSchsystemGradeService.getGradeNameMap(schId, null, null);
Map<Integer, SchClass> classMap = schClassService.getSchAllClassCacheList(schId).stream()
.filter(c -> c.getClassId() != null)
.collect(Collectors.toMap(SchClass::getClassId, c -> c, (a, b) -> a));
return teaches.stream().map(t -> {
OapiTeachVO vo = new OapiTeachVO();
vo.setId(t.getId());
vo.setSchId(t.getSchId());
vo.setGradeId(t.getGradeId());
vo.setGradeName(t.getGradeId() == null ? null : gradeNameMap.get(t.getGradeId()));
vo.setClassId(t.getClassId());
vo.setTeaId(t.getTeaId());
vo.setTeaName(t.getTeaId() == null ? null : teacherNameMap.get(t.getTeaId()));
vo.setSubId(t.getSubId());
vo.setSubName(t.getSubId() == null ? null : subjectNameMap.get(t.getSubId()));
SchClass clazz = t.getClassId() == null ? null : classMap.get(t.getClassId());
if (clazz != null) {
vo.setClassName(clazz.getCname() == null || clazz.getCname().trim().isEmpty()
? clazz.getName() : clazz.getCname());
if (vo.getGradeId() == null) {
vo.setGradeId(clazz.getGradeId());
vo.setGradeName(clazz.getGradeId() == null ? null : gradeNameMap.get(clazz.getGradeId()));
}
}
return vo;
}).collect(Collectors.toList());
}
}
@@ -0,0 +1,30 @@
package com.yunhai.bschclassmanage.controller.oapi.sch;
import com.yunhai.bschclassmanage.config.Config;
import com.yunhai.bschclassmanage.model.dto.TeacherBaseInfoDTO;
import com.yunhai.bschclassmanage.service.impl.teacher.SchTeacherBaseInfoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@Tag(name = "OapiSchTeacher", description = "开放接口-教师基础信息")
@RestController
@RequestMapping(Config.OAPI + "/sch/")
public class OapiSchTeacherController {
@Autowired private SchTeacherBaseInfoService service;
@GetMapping("getTeacherBaseInfo")
@Operation(summary = "获取教师基础信息", description = "姓名、性别、身份证、手机;teaId 为平台用户ID")
public ResponseEntity<TeacherBaseInfoDTO> get(@RequestParam Integer schId, @RequestParam Integer teaId) {
return ResponseEntity.ok(service.get(schId, teaId));
}
@PostMapping("editTeacherBaseInfo")
@Operation(summary = "编辑教师基础信息并同步基础平台", description = "提交四项资料;手机、身份证可清空,性别0女1男")
public ResponseEntity<TeacherBaseInfoDTO> edit(@Valid @RequestBody TeacherBaseInfoDTO dto) {
return ResponseEntity.ok(service.edit(dto));
}
}
@@ -0,0 +1,57 @@
package com.yunhai.bschclassmanage.controller.oapi.sch;
import com.yunhai.bschclassmanage.config.Config;
import com.yunhai.bschclassmanage.entity.BaseYearTerm;
import com.yunhai.bschclassmanage.model.tool.YearTerm;
import com.yunhai.bschclassmanage.model.vo.BaseYearTermVO;
import com.yunhai.bschclassmanage.model.vo.sch.NowYearTermVO;
import com.yunhai.bschclassmanage.service.IBaseYearTermService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "OapiYearTerm", description = "开放接口-学年学期")
@RestController
@RequestMapping(Config.OAPI + "/sch/")
@CrossOrigin(originPatterns = {"*"}, allowCredentials = "true")
public class OapiYearTermController {
@Autowired
private IBaseYearTermService baseYearTermService;
@GetMapping("getNowYearTerm")
@Operation(summary = "获取当前学年学期", description = "返回当前学年(nowYear=true)、当前学期 term(1第一学期 2第二学期)及当前学期起止日期")
public ResponseEntity<NowYearTermVO> getNowYearTerm() {
BaseYearTerm nowTerm = baseYearTermService.getNowYearTerm();
YearTerm current = baseYearTermService.getCurrentYearTerm();
return ResponseEntity.ok(toVO(nowTerm, current));
}
@GetMapping("getYearTermList")
@Operation(summary = "获取学年学期列表", description = "返回全部学年学期列表(按学年倒序),当前学年带 nowYear=true 标记")
public ResponseEntity<List<BaseYearTermVO>> getYearTermList() {
return ResponseEntity.ok(baseYearTermService.getAllYearTermVO());
}
private NowYearTermVO toVO(BaseYearTerm entity, YearTerm current) {
NowYearTermVO vo = new NowYearTermVO();
vo.setId(entity.getId());
vo.setYear(current != null && current.getYear() != null ? current.getYear() : entity.getYear());
vo.setTerm(current == null ? null : current.getTerm());
vo.setYearStartDate(entity.getYearStartDate());
vo.setYearEndDate(entity.getYearEndDate());
vo.setFirstTermStartDate(entity.getFirstTermStartDate());
vo.setFirstTermEndDate(entity.getFirstTermEndDate());
vo.setSecondTermStartDate(entity.getSecondTermStartDate());
vo.setSecondTermEndDate(entity.getSecondTermEndDate());
vo.setUpgradeDate(entity.getUpgradeDate());
vo.setTermStartDate(Integer.valueOf(2).equals(vo.getTerm()) ? entity.getSecondTermStartDate() : entity.getFirstTermStartDate());
vo.setTermEndDate(Integer.valueOf(2).equals(vo.getTerm()) ? entity.getSecondTermEndDate() : entity.getFirstTermEndDate());
vo.setNowYear(true);
return vo;
}
}
@@ -0,0 +1,52 @@
package com.yunhai.bschclassmanage.controller.oapi.sync;
import com.yunhai.bschclassmanage.annotation.SyncOpLog;
import com.yunhai.bschclassmanage.config.Config;
import com.yunhai.bschclassmanage.enums.SyncDataType;
import com.yunhai.bschclassmanage.model.tool.SyncClassDTO;
import com.yunhai.bschclassmanage.model.tool.SyncClassStudentDTO;
import com.yunhai.bschclassmanage.model.vo.sync.SyncClassResultVO;
import com.yunhai.bschclassmanage.model.vo.sync.SyncStudentResultVO;
import com.yunhai.bschclassmanage.service.ISyncClassService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
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.RestController;
/**
* 分班结果推送开放接口(/oapi/class/,调用方:分班管理 AcaClassAssign)。
* <p>与既有 /oapi/group、/oapi/user 同步接口不同:本控制器用强类型 DTO 入参 + 结果 VO 出参
* (推送方需要拿到 本地班号→平台classId 映射与逐生成败结果,void 返回无法承载),
* 风格对齐 /oapi/sch 只读接口。</p>
*/
@Slf4j
@Tag(name = "OapiSyncClass", description = "开放接口-分班结果推送(班级/班级学生)")
@RestController
@RequestMapping(Config.OAPI + "/class/")
@CrossOrigin(originPatterns = {"*"}, allowCredentials = "true")
public class SyncClassController {
@Autowired
private ISyncClassService syncClassService;
// 返回 ResponseEntity 绕过平台 ApiResponseAdvice 包装(对齐 /oapi/sch 只读接口:成功裸 VO,业务异常走 ValidationExceptionHandler 输出 {code,msg})
@SyncOpLog(SyncDataType.SYNC_ADD_CLASS)
@PostMapping("AddClass")
@Operation(summary = "添加班级(同步组)", description = "学生组→[学段组]→年级组(入学年份)→班级组(年份+两位班号)+sch_class;按 code 幂等,返回平台 classId 映射")
public ResponseEntity<SyncClassResultVO> AddClass(@RequestBody SyncClassDTO dto) {
return ResponseEntity.ok(syncClassService.addClass(dto));
}
@SyncOpLog(SyncDataType.SYNC_ADD_STU)
@PostMapping("AddClassStudent")
@Operation(summary = "添加班级学生(同步学生)", description = "用户中心建号+sch_student 落库;按 account/班内姓名幂等;默认聚合模式逐生返回成败,rollbackOnError=true 整批回滚")
public ResponseEntity<SyncStudentResultVO> AddClassStudent(@RequestBody SyncClassStudentDTO dto) {
return ResponseEntity.ok(syncClassService.addClassStudent(dto));
}
}
@@ -23,6 +23,9 @@ public enum SyncDataType
SYNC_MOVE_USER("Sync-MoveUser","用户移组"),
SYNC_USER("Sync-SyncUser","全量用户"),
SYNC_ADD_CLASS("Sync-AddClass","分班推送添加班级"),
SYNC_ADD_STU("Sync-AddClassStudent","分班推送添加班级学生"),
//推送
SEND_ADD_GROUP("Send-AddGroup","添加组"),
SEND_EDIT_GROUP("Send-EditGroup","编辑组"),
@@ -0,0 +1,22 @@
package com.yunhai.bschclassmanage.model.dto;
import lombok.Data;
import javax.validation.constraints.*;
/** 教师基础资料;teaId 为基础平台用户 ID,而非本地教师表主键。 */
@Data
public class TeacherBaseInfoDTO {
@NotNull @Min(1)
private Integer schId;
@NotNull @Min(1)
private Integer teaId;
@NotBlank @Size(max = 255)
private String name;
/** 0=女,1=男;null=未登记 */
@Min(0) @Max(1)
private Integer gender;
@Pattern(regexp = "^$|^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]$", message = "身份证号码格式不正确")
private String idcard;
@Size(max = 255)
private String phone;
}
@@ -1,30 +1,54 @@
package com.yunhai.bschclassmanage.model.oa;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class TeaInfoVO {
@JsonProperty("SchSerID")
private String SchSerID;
@JsonProperty("Teapolitical")
private String Teapolitical;
@JsonProperty("TeaWorkStart")
private String TeaWorkStart;
@JsonProperty("Account")
private String Account;
@JsonProperty("SchSerIDs")
private String SchSerIDs;
@JsonProperty("role")
private Integer role;
@JsonProperty("JiaoYuJu")
private String JiaoYuJu;
@JsonProperty("TeaSex")
private String TeaSex;
@JsonProperty("IsLeadingMiddle")
private Integer IsLeadingMiddle;
@JsonProperty("TeaOriginPlace")
private String TeaOriginPlace;
@JsonProperty("Photo")
private String Photo;
@JsonProperty("TeaIDCard")
private String TeaIDCard;
@JsonProperty("Name")
private String Name;
@JsonProperty("GroupID")
private Integer GroupID;
@JsonProperty("SchName")
private String SchName;
@JsonProperty("TeaTeachStart")
private String TeaTeachStart;
@JsonProperty("TeaSerID")
private String TeaSerID;
@JsonProperty("UserID")
private Integer UserID;
@JsonProperty("TeaNation")
private String TeaNation;
@JsonProperty("Tel")
private String Tel;
@JsonProperty("TeaWorkType")
private String TeaWorkType;
@JsonProperty("TeaNo")
private Integer TeaNo;
@JsonProperty("TeaBirth")
private String TeaBirth;
}
@@ -0,0 +1,25 @@
package com.yunhai.bschclassmanage.model.oa;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* 教师组树节点(SchGroup/GetTeaTree 输出)
* type=1 组节点(含学校根节点),type=2 教师叶子节点
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class TeaTreeVO {
private Integer id;
private String name;
private Integer pid;
private Integer no;
private Integer type;
@JsonProperty("isParent")
private Boolean isParent;
private String photo = "";
private List<TeaTreeVO> children;
}
@@ -0,0 +1,42 @@
package com.yunhai.bschclassmanage.model.tool;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.List;
/**
* 分班结果推送-添加班级入参 DTO(POST /oapi/class/AddClass)。
* 调用方为分班管理(AcaClassAssign),同步组:学生组→[学段组]→年级组→班级组 + sch_class。
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class SyncClassDTO {
/** 学校id(=学校组id) */
private Integer schId;
/** 学段:AcademicStage 口径 1小学/2初中/3高中(分班方负责换算,非分班侧 2/3/4) */
private Integer stage;
/** 入学年份(=分班项目 year,如 2026)。显式传入,禁止用 NowYearTerm 推导(分班多在 6-8 月,会差一学年) */
private Integer enrollYear;
/** 班级列表 */
private List<ClassItem> classes;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class ClassItem {
/** 班号 1..N(=aca_assign_class.sort_order,决定 code 尾号与 sch_class.sort) */
private Integer sort;
/** 分班侧班名(可空;非空时写 cname 别名,name 统一按规则生成 sort+"班")。
* 分班侧口径(2026-10-21):仅语义班名(阳光班/苹果班)才传,序号生成的班名(1班/2班)不传,cname 留空 */
private String name;
/** 班主任(平台教师 UserTeaSerID,可空)→ first_teacher_id */
private Integer teacherId;
}
}
@@ -0,0 +1,47 @@
package com.yunhai.bschclassmanage.model.tool;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.List;
/**
* 分班结果推送-添加班级学生入参 DTO(POST /oapi/class/AddClassStudent)。
* 用户中心建号 + sch_student 落库;默认聚合模式(逐生返回成败),rollbackOnError=true 走整批回滚。
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class SyncClassStudentDTO {
/** 学校id */
private Integer schId;
/** true=整批 all-or-nothing(任一失败回滚本批已建账号并整批报错);缺省 false=聚合模式 */
private Boolean rollbackOnError;
/** 学生列表(可跨班) */
private List<StudentItem> students;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class StudentItem {
/** 目标班级(班级组id,=AddClass 返回的 classId) */
private Integer classId;
/** 姓名(必填) */
private String name;
/** 性别:本接口口径 1男/0女(分班侧 1男/2女/0未知,由分班方换算后传入) */
private Integer gender;
/** 登录账号(可空;空则由用户中心默认逻辑处理) */
private String account;
/** 班内学号(同时决定 sort) */
private String stuClassNo;
/** 初始密码(可空=不带,走用户中心默认密码策略) */
private String pwd;
}
}
@@ -0,0 +1,18 @@
package com.yunhai.bschclassmanage.model.vo.sch;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
/**
* 学制年级项(oapi/sch 学校信息内的 gradeList 元素)
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class GradeItemVO {
/** 年级编码(BaseSchsystemGrade.gradeId,与 sch_class.grade_id 同源) */
private Integer gradeId;
/** 年级别称 */
private String gradeNickname;
}
@@ -0,0 +1,56 @@
package com.yunhai.bschclassmanage.model.vo.sch;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.util.Date;
/**
* 当前学年学期(oapi/sch/getNowYearTerm 输出)
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class NowYearTermVO {
/** 学年(如 2026) */
private Integer year;
/** 当前学期(1第一学期 2第二学期),按当前日期落在学期区间判定 */
private Integer term;
/** 学年学期记录ID(base_year_term.id) */
private Integer id;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date yearStartDate;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date yearEndDate;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date firstTermStartDate;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date firstTermEndDate;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date secondTermStartDate;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date secondTermEndDate;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date upgradeDate;
/** 当前学期开始日期(按 term 取一/二学期开始日期) */
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date termStartDate;
/** 当前学期结束日期(按 term 取一/二学期结束日期) */
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date termEndDate;
/** 是否当前学年学期,恒为 true */
private Boolean nowYear;
}
@@ -0,0 +1,63 @@
package com.yunhai.bschclassmanage.model.vo.sch;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
/**
* 班级信息(oapi/sch 班级接口输出),含班主任
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class OapiClassVO {
/** 班级记录ID(sch_class.id 主键) */
private Integer id;
/** 班级ID(班级组ID group_class_id,学生/任教的 classId 同源) */
private Integer classId;
/** 班级编码 */
private String code;
/** 班级名称 */
private String name;
/** 班级别称 */
private String cname;
/** 年级编码(base_sch_system_grade.grade_id) */
private Integer gradeId;
/** 年级名称 */
private String gradeName;
/** 学校ID */
private Integer schId;
/** 学校名称 */
private String schName;
/** 班主任ID(教师 teaId) */
private Integer firstTeacherId;
/** 班主任姓名 */
private String firstTeacherName;
/** 副班主任ID(教师 teaId) */
private Integer secondTeacherId;
/** 副班主任姓名 */
private String secondTeacherName;
/** 校区ID */
private Integer campusId;
/** 注册年份 */
private Integer enrollYear;
/** 是否毕业 */
private Boolean graduate;
/** 序号 */
private Integer sort;
}
@@ -0,0 +1,20 @@
package com.yunhai.bschclassmanage.model.vo.sch;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 年级名称 VO(开放接口 getGradeName 返回):学校年级字典名称 + 对应年级编码与入学年份。
*/
@Data
public class OapiGradeNameVO {
@Schema(description = "年级编码(K12 年级ID)")
private Integer gradeId;
@Schema(description = "年级名称(学校年级字典别称,如 小一/一年级/初一)")
private String gradeName;
@Schema(description = "入学年份")
private Integer year;
}
@@ -0,0 +1,40 @@
package com.yunhai.bschclassmanage.model.vo.sch;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.yunhai.bschclassmanage.model.vo.SelectIntVO;
import com.yunhai.bschclassmanage.model.vo.SelectStrVO;
import lombok.Data;
import java.util.List;
/**
* 教师分组信息(oapi/sch/getTeaCustomGroupList 输出,对应教师分组管理页面的组/组员/组长三栏)
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class OapiGroupVO {
/** 组ID(sch_teacher_custom_group.id) */
private Integer groupId;
/** 组名称 */
private String groupName;
/** 组类型(1年级组 2教研组 3备课组) */
private Integer groupType;
/** 组类型名称 */
private String groupTypeName;
/** 学科集合 [{id:学科编码, name:学科名称}] */
private List<SelectStrVO> subjects;
/** 年级集合 [{id:年级编码, name:年级名称}] */
private List<SelectIntVO> grades;
/** 组员集合 [{id:教师id, name:教师名称}](不含组长) */
private List<SelectIntVO> members;
/** 组长集合 [{id:教师id, name:教师名称}] */
private List<SelectIntVO> leaders;
}
@@ -0,0 +1,51 @@
package com.yunhai.bschclassmanage.model.vo.sch;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
/**
* 学生信息(oapi/sch 学生接口输出)
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class OapiStudentVO {
/** 学生记录ID(sch_student.id 主键) */
private Integer id;
/** 学生ID(用户中心用户ID stu_id) */
private Integer stuId;
/** 学生账户 */
private String account;
/** 学生姓名 */
private String name;
/** 性别(0女 1男) */
private Integer gender;
/** 班级学号 */
private String stuClassNo;
/** 班级ID(班级组ID,与班级接口的 classId 同源) */
private Integer classId;
/** 班级名称 */
private String className;
/** 年级编码 */
private Integer gradeId;
/** 年级名称 */
private String gradeName;
/** 学校ID */
private Integer schId;
/** 学校名称 */
private String schName;
/** 状态(0正常 1禁用) */
private Integer status;
}
@@ -0,0 +1,36 @@
package com.yunhai.bschclassmanage.model.vo.sch;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
/**
* 学科信息(oapi/sch 学科接口输出)
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class OapiSubjectVO {
/** 学科ID(sch_dic_subject.id 主键) */
private Integer id;
/** 学科编码(任教/自定义组关联的 subId 即该编码) */
private String code;
/** 学科名称 */
private String name;
/** 学制ID */
private Integer schSystemId;
/** 是否启用 */
private Boolean enable;
/** 数据来源(0系统 1学校) */
private Integer source;
/** 序号 */
private Integer sort;
/** 学校ID */
private Integer schId;
}
@@ -0,0 +1,42 @@
package com.yunhai.bschclassmanage.model.vo.sch;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
/**
* 任教信息(oapi/sch 任教接口输出)
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class OapiTeachVO {
/** 任教记录ID(sch_class_teach.id 主键) */
private Integer id;
/** 学校ID */
private Integer schId;
/** 班级ID(班级组ID) */
private Integer classId;
/** 班级名称 */
private String className;
/** 年级编码 */
private Integer gradeId;
/** 年级名称 */
private String gradeName;
/** 教师ID(teaId) */
private Integer teaId;
/** 教师姓名 */
private String teaName;
/** 学科ID(sch_dic_subject.code) */
private String subId;
/** 学科名称 */
private String subName;
}
@@ -0,0 +1,29 @@
package com.yunhai.bschclassmanage.model.vo.sch;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.util.List;
/**
* 学校信息(oapi/sch/getSchInfo、getSchInfoList 输出),含学段/学制信息
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class SchInfoDetailVO {
/** 学校ID(用户中心学校组ID,sch_info.sch_id) */
private Integer schId;
/** 学校名称 */
private String schName;
/** 学校序列号(sch_info.sch_serid) */
private String schSerId;
/** 排序号 */
private Integer sort;
/** 学段/学制信息(学校绑定的学制列表,每个学制含其年级) */
private List<SchSystemDetailVO> schSystemList;
}
@@ -0,0 +1,41 @@
package com.yunhai.bschclassmanage.model.vo.sch;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.util.List;
/**
* 学制信息(oapi/sch 学校信息内的 schSystemList 元素,含学段编码与该学制下的年级)
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class SchSystemDetailVO {
/** 学制ID(base_sch_system.id) */
private Integer id;
/** 学制名称 */
private String name;
/** 学段编码(0学前 1小学 2初中 3高中 4高等) */
private Integer academicStage;
/** 学段名称 */
private String academicStageName;
/** 开始年级编码 */
private Integer startGradeCode;
/** 结束年级编码 */
private Integer endGradeCode;
/** 毕业年长度 */
private Integer graduationLeng;
/** 教育类型(1基础教育 2职业教育) */
private Integer type;
/** 该学制下的年级列表 */
private List<GradeItemVO> gradeList;
}
@@ -0,0 +1,64 @@
package com.yunhai.bschclassmanage.model.vo.sch;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* 教师自定义组树节点(oapi/sch/getTeaCustomGroupTree 输出)。
* 节点契约与 SchGroup/GetTeaTree 的 TeaTreeVO 一致:
* type=1 组节点(isParent=true),type=2 教师叶子(isParent=false、children 省略)。
* 组节点额外携带组类型/组长/年级/学科信息,教师叶子额外携带 isLeader。
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class TeaCustomGroupTreeVO {
/** 组节点:组ID(sch_teacher_custom_group.id);类型节点:-1/-2/-3;教师叶子:teaId */
private Integer id;
private String name;
/** 父节点ID(类型节点 pid=0) */
private Integer pid;
/** 排序号 */
private Integer no;
/** 1组节点 2教师叶子 */
private Integer type;
@JsonProperty("isParent")
private Boolean isParent;
/** 组节点:组类型(1年级组 2教研组 3备课组) */
private Integer groupType;
/** 组节点:组类型名称 */
private String groupTypeName;
/** 组节点:组长ID(teaId) */
private Integer leaderId;
/** 组节点:组长姓名 */
private String leaderName;
/** 组节点:关联年级编码列表 */
private List<Integer> gradeIds;
/** 组节点:关联年级名称列表 */
private List<String> gradeNames;
/** 组节点:关联学科编码列表 */
private List<String> subjectIds;
/** 组节点:关联学科名称列表 */
private List<String> subjectNames;
/** 教师叶子:是否组长 */
private Boolean isLeader;
private List<TeaCustomGroupTreeVO> children;
}
@@ -0,0 +1,44 @@
package com.yunhai.bschclassmanage.model.vo.sync;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.util.List;
/**
* 分班结果推送-添加班级出参(POST /oapi/class/AddClass)。
* 返回本地班号 → 平台 classId 映射,供 AddClassStudent 分组使用。
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class SyncClassResultVO {
/** 起始年级编码(base_sch_system_grade.grade_id,本学段最小编码) */
private Integer gradeId;
/** 年级组id(平台组) */
private Integer gradeGroupId;
/** 班级结果(按入参顺序) */
private List<ClassItem> classes;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class ClassItem {
/** 班号(=入参 sort) */
private Integer sort;
/** 平台班级id(班级组id) */
private Integer classId;
/** 班级编码(enrollYear + 两位班号) */
private String code;
/** 班级名称("N班") */
private String name;
/** true=本次新建;false=按 code 查重命中已有班级组(幂等重推,未覆盖既有数据) */
private Boolean created;
}
}
@@ -0,0 +1,38 @@
package com.yunhai.bschclassmanage.model.vo.sync;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.util.List;
/**
* 分班结果推送-添加班级学生出参(POST /oapi/class/AddClassStudent)。
* 逐生返回成败,不因个别失败整体报错(聚合模式)。
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class SyncStudentResultVO {
/** 逐生结果(与入参顺序一致) */
private List<StudentItem> results;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class StudentItem {
/** 姓名 */
private String name;
/** 登录账号(=入参 account,可能为空) */
private String account;
/** 是否成功(含幂等命中"已存在") */
private Boolean ok;
/** 平台学生id(用户中心用户id,成功时返回) */
private Integer stuId;
/** 失败原因 / 幂等命中说明(成功时也可能非空,如"已存在") */
private String reason;
}
}
@@ -1,6 +1,7 @@
package com.yunhai.bschclassmanage.service;
import com.yunhai.bschclassmanage.entity.BaseDicGrade;
import com.yunhai.bschclassmanage.model.oa.TeaTreeVO;
import com.yunhai.bschclassmanage.model.vo.SchSystemTreeVO;
import com.yunhai.bschclassmanage.model.vo.SelectIntVO;
import com.yunhai.bschclassmanage.model.vo.SelectStrVO;
@@ -28,6 +29,10 @@ public interface IComSchTreeService {
BoaGroup getTeacherTree(Integer schId);
List<TeaTreeVO> getSchTeaTree(Integer schId, boolean toUser);
List<TeaTreeVO> getSchStuTree(Integer schId, boolean toUser);
List<SelectIntVO> getSchoolList();
BoaGroup getTeaTree(Integer schId, Boolean withSub);
@@ -22,4 +22,9 @@ public interface ISchDicSubjectService {
List<SelectStrVO> select(Integer schId,Integer schsystemId);
Map<String, String> toIdAndNameMap(Integer schId, Integer schsystemId);
/**
* 获取学校学科 code -> name 映射(仅启用的学科)
*/
Map<String, String> toEnableIdAndNameMap(Integer schId, Integer schsystemId);
}
@@ -0,0 +1,26 @@
package com.yunhai.bschclassmanage.service;
import com.yunhai.bschclassmanage.model.tool.SyncClassDTO;
import com.yunhai.bschclassmanage.model.tool.SyncClassStudentDTO;
import com.yunhai.bschclassmanage.model.vo.sync.SyncClassResultVO;
import com.yunhai.bschclassmanage.model.vo.sync.SyncStudentResultVO;
/**
* 分班结果推送开放接口(/oapi/class/)。
* 调用方为分班管理(AcaClassAssign):提交分班结果后把班级与学生落到班级管理。
*/
public interface ISyncClassService {
/**
* 添加班级(同步组):学生组→[学段组]→年级组(name=入学年份)→班级组(code=年份+两位班号) + sch_class。
* 幂等:按组 name/code 查重复用,不覆盖既有班级数据;新建组失败时补偿删除本次新建的组。
*/
SyncClassResultVO addClass(SyncClassDTO dto);
/**
* 添加班级学生:用户中心建号 + sch_student 落库。
* 默认聚合模式(逐生返回成败,个别失败不中断);rollbackOnError=true 走整批回滚。
* 幂等:按 account(全库)/ 班内姓名查重跳过。
*/
SyncStudentResultVO addClassStudent(SyncClassStudentDTO dto);
}
@@ -329,7 +329,7 @@ public class SchDicSubjectService implements ISchDicSubjectService {
public List<SelectStrVO> select(Integer schId, Integer schsystemId) {
List<SchDicSubject> list = getSchDicSubjectCacheList(schId, schsystemId);
// 按 code 去重,保留顺序(若需要保持原顺序,可用 LinkedHashSet 包装)
list = list.stream()
list = list.stream().filter(s -> Boolean.TRUE.equals(s.getEnable()))
.collect(Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(SchDicSubject::getCode))),
ArrayList::new
@@ -342,4 +342,22 @@ public class SchDicSubjectService implements ISchDicSubjectService {
List<SchDicSubject> list = getSchDicSubjectCacheList(schId,schsystemId);
return ModelMapperUtil.toMap(list, SchDicSubject::getCode, SchDicSubject::getName);
}
public Map<String, String> toEnableIdAndNameMap(Integer schId, Integer schsystemId)
{
List<SchDicSubject> list = getSchDicSubjectCacheList(schId, schsystemId).stream()
.filter(s -> Boolean.TRUE.equals(s.getEnable()))
.collect(Collectors.toList());
// 按序号升序排序(null 排最后),与学科管理列表口径一致
list.sort(Comparator.comparing(SchDicSubject::getSort,
Comparator.nullsLast(Comparator.naturalOrder())));
// LinkedHashMap 保持排序后的遍历顺序;重复 code 保留第一个
Map<String, String> map = new LinkedHashMap<>();
for (SchDicSubject subject : list) {
if (subject.getCode() != null) {
map.putIfAbsent(subject.getCode(), subject.getName());
}
}
return map;
}
}
@@ -17,7 +17,6 @@ import com.yunhai.bschclassmanage.utils.LogContextHolder;
import com.yunhai.bschclassmanage.utils.OtherUtil;
import com.yunhai.bschclassmanage.utils.RedisUtil;
import com.yunhai.bschclassmanage.utils.UserContext;
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
import com.yunhai.platform.utils.exception.DException;
import com.yunhai.platform.utils.model.BoaGroup;
import org.apache.commons.lang3.StringUtils;
@@ -108,7 +107,7 @@ public class SchInfoService implements ISchInfoService {
public SchInfoVO GetSchFirst(String SchID) {
return schInfoRepository.findBySchSerId(SchID)
.map(schInfo -> SchInfoVO.builder()
.userSchSerId(schInfo.getId())
.userSchSerId(schInfo.getSchId())
.schSerId(schInfo.getSchSerId())
.schName(schInfo.getName())
.build())
@@ -118,7 +117,7 @@ public class SchInfoService implements ISchInfoService {
public SchInfoVO GetSchFirst(Integer schId) {
return schInfoRepository.findBySchId(schId)
.map(schInfo -> SchInfoVO.builder()
.userSchSerId(schInfo.getId())
.userSchSerId(schInfo.getSchId())
.schSerId(schInfo.getSchSerId())
.schName(schInfo.getName())
.build())
@@ -300,13 +300,15 @@ public class StudentClassHistoryService implements IStudentClassHistoryService {
*/
public Map<String, Object> getHistoryClassTeachByStuId(Integer schId, Integer stuId) {
// 1. 解析学生所属班级 & 姓名
// stu_id 是用户中心用户ID,学生在多所学校各有就学记录(同一 stu_id 多行),需按当前学校过滤后取一条
Integer classId = null;
String studentName = null;
Optional<SchStudent> live = schStudentRepository.findByStuId(stuId);
if (live.isPresent() && live.get().getClassId() != null
&& Objects.equals(live.get().getSchId(), schId)) {
classId = live.get().getClassId();
studentName = live.get().getName();
SchStudent liveMatch = schStudentRepository.findByStuId(stuId).stream()
.filter(s -> s.getClassId() != null && Objects.equals(s.getSchId(), schId))
.findFirst().orElse(null);
if (liveMatch != null) {
classId = liveMatch.getClassId();
studentName = liveMatch.getName();
} else {
List<SchStudentHistory> hist = schStudentHistoryRepository.findBySchIdAndStuId(schId, stuId);
if (hist.isEmpty()) {
@@ -9,7 +9,6 @@ import com.yunhai.bschclassmanage.service.*;
import com.yunhai.bschclassmanage.utils.LogContextHolder;
import com.yunhai.bschclassmanage.utils.OtherUtil;
import com.yunhai.bschclassmanage.utils.UserContext;
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
import com.yunhai.platform.utils.exception.DException;
import com.yunhai.platform.utils.model.BoaGroup;
import lombok.extern.slf4j.Slf4j;
@@ -8,12 +8,13 @@ import com.yunhai.bschclassmanage.entity.*;
import com.yunhai.bschclassmanage.enums.AcademicStage;
import com.yunhai.bschclassmanage.enums.NodeType;
import com.yunhai.bschclassmanage.enums.SchType;
import com.yunhai.bschclassmanage.model.oa.TeaTreeVO;
import com.yunhai.bschclassmanage.model.vo.SchSystemTreeVO;
import com.yunhai.bschclassmanage.model.vo.SelectIntVO;
import com.yunhai.bschclassmanage.service.*;
import com.yunhai.bschclassmanage.utils.RedisConstants;
import com.yunhai.bschclassmanage.utils.RedisUtil;
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
import com.yunhai.platform.openapi.base.YschBaseOpenApi;
import com.yunhai.platform.utils.exception.DException;
import com.yunhai.platform.utils.model.BoaGroup;
import org.apache.commons.collections4.CollectionUtils;
@@ -57,6 +58,10 @@ public class ComSchTreeService implements IComSchTreeService {
@Lazy
private ISchTeacherService schTeacherService;
@Autowired
@Lazy
private ISchStudentService schStudentService;
@Autowired
private SchTeacherGroupRepository schTeacherGroupRepository;
@@ -433,6 +438,239 @@ public class ComSchTreeService implements IComSchTreeService {
return teaTree;
}
/** 教师叶子的"未分组"节点 ID(与 getTeacherTree 的默认组保持一致) */
private static final int UNGROUPED_ID = -1;
/**
* 教师组树 + 教师(SchGroup/GetTeaTree 输出)。
* 结构:学校根(type=1) → 教师组(type=1) → 子组递归 + 教师叶子(type=2)。
* 节点字段:pid=父节点id(根为0)、no=组取 BoaGroup.sort / 教师固定 0、
* isParent=组true/教师false、photo 固定空串(本库无头像数据)。
* toUser=false 时只返回组织骨架(不查询、不挂载教师叶子)。
* 返回单元素列表(学校根节点)。
*/
public List<TeaTreeVO> getSchTeaTree(Integer schId, boolean toUser) {
// 1. 学校根节点(远程组树的根即学校节点,取不到时用学校名兜底)
List<BoaGroup> schTree = getSchTree(schId);
BoaGroup schRoot = (schTree == null || schTree.isEmpty()) ? null : schTree.get(0);
TeaTreeVO rootVo = new TeaTreeVO();
if (schRoot != null && schRoot.getId() != null) {
rootVo.setId(schRoot.getId());
rootVo.setName(schRoot.getName());
rootVo.setNo(schRoot.getSort());
} else {
rootVo.setId(schId);
rootVo.setName(getSchoolName(schId));
rootVo.setNo(0);
}
rootVo.setPid(0);
rootVo.setType(1);
rootVo.setIsParent(true);
rootVo.setChildren(new ArrayList<>());
// 2. 教师组骨架(type=7 子树)转 VO,同时建立 groupId -> 节点 映射供挂教师用
Map<Integer, TeaTreeVO> groupVoMap = new HashMap<>();
BoaGroup teaTree = getTeaTree(schId, true);
TeaTreeVO teaRootVo;
if (teaTree == null || teaTree.getId() == null) {
teaRootVo = newGroupNode(0, "教师组", rootVo.getId(), 1);
} else {
teaRootVo = convertGroupTree(teaTree, rootVo.getId(), groupVoMap);
}
groupVoMap.put(teaRootVo.getId(), teaRootVo);
rootVo.getChildren().add(teaRootVo);
// 3. 挂教师:多组各挂一份;无组或组不在树中的落到"未分组"(toUser=false 时不带用户,跳过查询)
if (toUser) {
List<SchTeacher> teacherList = schTeacherService.getSchTeacherList(schId);
if (CollectionUtils.isNotEmpty(teacherList)) {
List<Integer> teaIds = teacherList.stream()
.map(SchTeacher::getTeaId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
Map<Integer, List<Integer>> teaIdToGroupIds = schTeacherGroupRepository.findByTeaIdInAndSchId(teaIds, schId)
.stream()
.collect(Collectors.groupingBy(SchTeacherGroup::getTeaId,
Collectors.mapping(SchTeacherGroup::getGroupId, Collectors.toList())));
for (SchTeacher teacher : teacherList) {
if (teacher.getTeaId() == null) {
continue;
}
List<Integer> groupIds = teaIdToGroupIds.getOrDefault(teacher.getTeaId(), Collections.emptyList());
if (groupIds.isEmpty()) {
TeaTreeVO ungrouped = getOrCreateUngrouped(teaRootVo, groupVoMap);
ungrouped.getChildren().add(newTeacherNode(teacher, ungrouped.getId()));
} else {
for (Integer groupId : groupIds) {
TeaTreeVO groupVo = groupVoMap.get(groupId);
if (groupVo == null) {
groupVo = getOrCreateUngrouped(teaRootVo, groupVoMap);
}
// 同一节点下按 teaId 去重(防脏数据中同一组出现多条关联)
boolean exists = groupVo.getChildren().stream()
.anyMatch(c -> Integer.valueOf(2).equals(c.getType()) && c.getId().equals(teacher.getTeaId()));
if (!exists) {
groupVo.getChildren().add(newTeacherNode(teacher, groupVo.getId()));
}
}
}
}
}
}
// 4. 每个节点 children 按 no 升序(nullsLast、稳定排序):教师 no=0 排在子组之前,
// 同 no 的教师保持 getSchTeacherList 的 sort 顺序
sortChildrenByNo(rootVo);
return Collections.singletonList(rootVo);
}
/** 组节点 VO(type=1,isParent=true,children 初始化为空列表) */
private TeaTreeVO newGroupNode(Integer id, String name, Integer pid, Integer no) {
TeaTreeVO vo = new TeaTreeVO();
vo.setId(id);
vo.setName(name);
vo.setPid(pid);
vo.setNo(no);
vo.setType(1);
vo.setIsParent(true);
vo.setChildren(new ArrayList<>());
return vo;
}
/** 教师叶子 VO(type=2,isParent=false,no 固定 0,children=null → JSON 省略) */
private TeaTreeVO newTeacherNode(SchTeacher teacher, Integer pid) {
TeaTreeVO vo = new TeaTreeVO();
vo.setId(teacher.getTeaId());
vo.setName(teacher.getName());
vo.setPid(pid);
vo.setNo(0);
vo.setType(2);
vo.setIsParent(false);
return vo;
}
/** 递归把 BoaGroup 骨架转成 TeaTreeVO(type=1),pid 记录父节点 id,并收集 groupId -> 节点 */
private TeaTreeVO convertGroupTree(BoaGroup node, Integer pid, Map<Integer, TeaTreeVO> groupVoMap) {
TeaTreeVO vo = newGroupNode(node.getId(), node.getName(), pid, node.getSort());
groupVoMap.put(node.getId(), vo);
if (node.getChildren() != null) {
for (BoaGroup child : node.getChildren()) {
vo.getChildren().add(convertGroupTree(child, node.getId(), groupVoMap));
}
}
return vo;
}
/** 获取或惰性创建"未分组"节点(挂在教师组根下) */
private TeaTreeVO getOrCreateUngrouped(TeaTreeVO teaRootVo, Map<Integer, TeaTreeVO> groupVoMap) {
TeaTreeVO ungrouped = groupVoMap.get(UNGROUPED_ID);
if (ungrouped == null) {
ungrouped = newGroupNode(UNGROUPED_ID, "未分组", teaRootVo.getId(), null);
teaRootVo.getChildren().add(ungrouped);
groupVoMap.put(UNGROUPED_ID, ungrouped);
}
return ungrouped;
}
/** 递归按 no 升序排序每个节点的 children(nullsLast、稳定排序) */
private void sortChildrenByNo(TeaTreeVO node) {
if (node == null || node.getChildren() == null) {
return;
}
node.getChildren().sort(Comparator.comparing(TeaTreeVO::getNo, Comparator.nullsLast(Integer::compareTo)));
for (TeaTreeVO child : node.getChildren()) {
sortChildrenByNo(child);
}
}
/**
* 学生树(SchGroup/GetStuTree 输出):学校 → 学段 → 年级 → 班级 → 学生。
* 层级数据链与 getGradeClassTree 一致(学段=base_sch_system、年级=base_sch_system_grade(no=主键 id)、
* 班级=sch_class(id=group_class_id,名称 cname 优先,no=sort,不含毕业班),
* 学生仅取在读正常状态(复用按班缓存查询,已过滤休学/转出/禁用并按 sort 排序)。
* 节点契约与 getSchTeaTree 相同:组织节点 type=1、isParent=true;学生叶子 type=2、isParent=false、children=null。
* toUser=false 时只返回组织骨架(不查询、不挂载学生叶子)。
* 返回单元素列表(学校根节点)。
*/
public List<TeaTreeVO> getSchStuTree(Integer schId, boolean toUser) {
TeaTreeVO rootVo = new TeaTreeVO();
rootVo.setId(schId);
rootVo.setName(getSchoolName(schId));
rootVo.setPid(0);
rootVo.setNo(0);
rootVo.setType(1);
rootVo.setIsParent(true);
rootVo.setChildren(new ArrayList<>());
List<SelectIntVO> systemList = schBinSchsystemService.selectSchBinSchsystem(schId);
if (CollectionUtils.isEmpty(systemList)) {
return Collections.singletonList(rootVo);
}
for (SelectIntVO system : systemList) {
TeaTreeVO systemVo = newGroupNode(system.getId(), system.getName(), schId, null);
rootVo.getChildren().add(systemVo);
List<BaseSchsystemGrade> gradeList = baseSchsystemGradeService.getSchGradeCacheList(schId, system.getId(), null);
if (CollectionUtils.isEmpty(gradeList)) {
continue;
}
List<Integer> gradeIds = gradeList.stream()
.map(BaseSchsystemGrade::getGradeId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
Map<Integer, List<SchClass>> classGroupMap = schClassService.getSchClassCacheFilter(schId, gradeIds, null)
.stream()
.collect(Collectors.groupingBy(SchClass::getGradeId));
for (BaseSchsystemGrade grade : gradeList) {
if (grade.getGradeId() == null) {
continue;
}
TeaTreeVO gradeVo = newGroupNode(grade.getGradeId(), grade.getGradeNickname(), system.getId(), grade.getId());
systemVo.getChildren().add(gradeVo);
for (SchClass clazz : classGroupMap.getOrDefault(grade.getGradeId(), Collections.emptyList())) {
if (clazz.getClassId() == null) {
continue;
}
TeaTreeVO classVo = newGroupNode(clazz.getClassId(),
StringUtils.isBlank(clazz.getCname()) ? clazz.getName() : clazz.getCname(),
grade.getGradeId(), clazz.getSort());
gradeVo.getChildren().add(classVo);
// toUser=false 时不带用户,跳过按班查询
if (toUser) {
List<SchStudent> students = schStudentService.getSchGradeClassStudentCacheList(schId, null, null, clazz.getClassId());
for (SchStudent student : students) {
if (student.getStuId() == null) {
continue;
}
classVo.getChildren().add(newStudentNode(student, clazz.getClassId()));
}
}
}
}
}
sortChildrenByNo(rootVo);
return Collections.singletonList(rootVo);
}
/** 学生叶子 VO(type=2,isParent=false,no 固定 0,children=null → JSON 省略) */
private TeaTreeVO newStudentNode(SchStudent student, Integer pid) {
TeaTreeVO vo = new TeaTreeVO();
vo.setId(student.getStuId());
vo.setName(student.getName());
vo.setPid(pid);
vo.setNo(0);
vo.setType(2);
vo.setIsParent(false);
return vo;
}
/**
* 递归确保所有节点的 children 不为 null,若为 null 则设置为空列表 []
*/
@@ -12,10 +12,10 @@ import com.yunhai.bschclassmanage.model.dto.SchTeacherDTO;
import com.yunhai.bschclassmanage.model.dto.SendMoveGroupUserDTO;
import com.yunhai.bschclassmanage.service.IComSchTreeService;
import com.yunhai.bschclassmanage.service.ISendUserBaseService;
import com.yunhai.platform.openapi.client.OpenApiGroupClient;
import com.yunhai.platform.openapi.client.OpenApiUserClient;
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
import com.yunhai.platform.openapi.model.*;
import com.yunhai.platform.openapi.base.OpenApiGroupClient;
import com.yunhai.platform.openapi.base.OpenApiUserClient;
import com.yunhai.platform.openapi.base.YschBaseOpenApi;
import com.yunhai.platform.openapi.base.model.*;
import com.yunhai.platform.utils.exception.DException;
import com.yunhai.platform.utils.model.BoaGroup;
import com.yunhai.platform.utils.model.BoaUser;
@@ -103,7 +103,7 @@ public class SendUserBaseService implements ISendUserBaseService {
sysUser.setIdcard(teacher.getIdCard());
sysUser.setPhone(teacher.getTel());
sysUser.setGroupId(groupId);
sysUser.setRoles(Arrays.asList(UserRole.TEACHER_USER.getValue()));
sysUser.setRoleIds(Arrays.asList(UserRole.TEACHER_USER.getValue()));
if (StringUtils.isNotBlank(pwd)) {
sysUser.setPassword(pwd);
sysUser.setConfirmPassword(pwd);
@@ -119,7 +119,7 @@ public class SendUserBaseService implements ISendUserBaseService {
sysUser.setGender(student.getGender());
sysUser.setIdcard(student.getIdCard());
sysUser.setGroupId(student.getGroupId());
sysUser.setRoles(Arrays.asList(UserRole.STU_USER.getValue()));
sysUser.setRoleIds(Arrays.asList(UserRole.STU_USER.getValue()));
if (StringUtils.isNotBlank(pwd)) {
sysUser.setPassword(pwd);
sysUser.setConfirmPassword(pwd);
@@ -139,7 +139,7 @@ public class SendUserBaseService implements ISendUserBaseService {
user.setIdcard(teacher.getIdCard());
user.setPhone(teacher.getTel());
user.setGroupId(groupId);
user.setRoles(Arrays.asList(UserRole.TEACHER_USER.getValue()));
user.setRoleIds(Arrays.asList(UserRole.TEACHER_USER.getValue()));
if (StringUtils.isNotBlank(pwd)) {
user.setPassword(pwd);
}
@@ -157,7 +157,7 @@ public class SendUserBaseService implements ISendUserBaseService {
user.setGender(student.getGender());
user.setIdcard(student.getIdCard());
user.setGroupId(student.getGroupId());
user.setRoles(Arrays.asList(UserRole.STU_USER.getValue()));
user.setRoleIds(Arrays.asList(UserRole.STU_USER.getValue()));
if (StringUtils.isNotBlank(pwd)) {
user.setPassword(pwd);
}
@@ -298,7 +298,7 @@ public class SendUserBaseService implements ISendUserBaseService {
sysUser.setGender(stu.getGender());
sysUser.setIdcard(stu.getIdCard());
sysUser.setGroupId(stu.getGroupId());
sysUser.setRoles(Arrays.asList(UserRole.STU_USER.getValue()));
sysUser.setRoleIds(Arrays.asList(UserRole.STU_USER.getValue()));
String pwd = stu.getPwd();
if (StringUtils.isNotBlank(pwd)) {
sysUser.setPassword(pwd);
@@ -324,7 +324,7 @@ public class SendUserBaseService implements ISendUserBaseService {
sysUser.setIdcard(teacher.getIdCard());
sysUser.setPhone(teacher.getTel());
sysUser.setGroupId(teacher.getGroupId());
sysUser.setRoles(Arrays.asList(UserRole.TEACHER_USER.getValue()));
sysUser.setRoleIds(Arrays.asList(UserRole.TEACHER_USER.getValue()));
String pwd = teacher.getPwd();
if (StringUtils.isNotBlank(pwd)) {
sysUser.setPassword(pwd);
@@ -13,7 +13,6 @@ import com.yunhai.bschclassmanage.model.vo.SelectIntVO;
import com.yunhai.bschclassmanage.service.*;
import com.yunhai.bschclassmanage.utils.RedisConstants;
import com.yunhai.bschclassmanage.utils.RedisUtil;
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
import com.yunhai.platform.utils.exception.DException;
import com.yunhai.platform.utils.model.BoaGroup;
import org.apache.commons.collections4.CollectionUtils;
@@ -21,7 +21,6 @@ import com.yunhai.bschclassmanage.utils.EasyPoiUtils;
import com.yunhai.bschclassmanage.utils.UserContext;
import com.yunhai.bschclassmanage.utils.ValidateUtil;
import com.yunhai.bschclassmanage.utils.ValidationUtil;
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.platform.utils.exception.DException;
import com.yunhai.platform.utils.exception.ErrorCode;
@@ -0,0 +1,437 @@
package com.yunhai.bschclassmanage.service.impl.sync;
import com.yunhai.bschclassmanage.Repository.SchClassRepository;
import com.yunhai.bschclassmanage.Repository.SchStudentRepository;
import com.yunhai.bschclassmanage.entity.BaseSchsystemGrade;
import com.yunhai.bschclassmanage.entity.SchClass;
import com.yunhai.bschclassmanage.entity.SchStudent;
import com.yunhai.bschclassmanage.enums.AcademicStage;
import com.yunhai.bschclassmanage.enums.NodeType;
import com.yunhai.bschclassmanage.enums.StuAbnormalStatus;
import com.yunhai.bschclassmanage.enums.TransferStatus;
import com.yunhai.bschclassmanage.enums.UserStatus;
import com.yunhai.bschclassmanage.model.tool.SyncClassDTO;
import com.yunhai.bschclassmanage.model.tool.SyncClassStudentDTO;
import com.yunhai.bschclassmanage.model.vo.SelectIntVO;
import com.yunhai.bschclassmanage.model.vo.sync.SyncClassResultVO;
import com.yunhai.bschclassmanage.model.vo.sync.SyncStudentResultVO;
import com.yunhai.bschclassmanage.service.IBaseSchsystemGradeService;
import com.yunhai.bschclassmanage.service.IBaseSchsystemService;
import com.yunhai.bschclassmanage.service.IComSchTreeService;
import com.yunhai.bschclassmanage.service.ISchBinSchsystemService;
import com.yunhai.bschclassmanage.service.ISendUserBaseService;
import com.yunhai.bschclassmanage.service.ISyncClassService;
import com.yunhai.bschclassmanage.service.impl.classes.SchClassCacheService;
import com.yunhai.bschclassmanage.service.impl.student.SchStudentCacheService;
import com.yunhai.platform.utils.exception.DException;
import com.yunhai.platform.utils.model.BoaGroup;
import com.yunhai.platform.utils.model.BoaUser;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 分班结果推送开放接口实现。
* <p>组创建模板:SchClassInfoService.addBaseSchClass;差异①:入学年份取入参 enrollYear(不用 NowYearTerm 推导,
* 分班多发生在 6-8 月旧学年末,照抄模板会把年级组建到旧年份);差异②:班号以入参 sort 为准(分班结果已定班号,
* 不按 findMaxCodeNumber 自动续号)。</p>
* <p>回滚策略:本地事务 + 外部系统补偿删除(rollbackCreatedGroups/rollbackSyncedStudents)+ 幂等重推自愈;
* createdGroupIds 只记本次新建的组,查重复用的既有组绝不进回滚清单。</p>
*/
@Slf4j
@Service
public class SyncClassService implements ISyncClassService {
@Autowired
private SchClassRepository schClassRepository;
@Autowired
private SchStudentRepository schStudentRepository;
@Autowired
private IComSchTreeService comSchTreeService;
@Autowired
private ISchBinSchsystemService schBinSchsystemService;
@Autowired
private IBaseSchsystemService baseSchsystemService;
@Autowired
private IBaseSchsystemGradeService baseSchsystemGradeService;
@Autowired
private ISendUserBaseService sendUserBaseService;
@Autowired
private SchClassCacheService classCacheService;
@Autowired
private SchStudentCacheService studentCacheService;
// ==================== 添加班级(同步组) ====================
@Override
@Transactional(rollbackFor = Exception.class)
public SyncClassResultVO addClass(SyncClassDTO dto) {
if (dto == null || dto.getSchId() == null || dto.getSchId() <= 0) {
throw new DException("schId不能为空");
}
String stageName = AcademicStage.nameById(dto.getStage() == null ? -1 : dto.getStage());
if (stageName == null) {
throw new DException("stage参数非法(AcademicStage口径:1小学/2初中/3高中)");
}
if (dto.getEnrollYear() == null || dto.getEnrollYear() <= 0) {
throw new DException("enrollYear不能为空(入学年份需显式传入)");
}
if (dto.getClasses() == null || dto.getClasses().isEmpty()) {
throw new DException("classes不能为空");
}
Set<Integer> sorts = new HashSet<>();
for (SyncClassDTO.ClassItem item : dto.getClasses()) {
if (item.getSort() == null || item.getSort() <= 0 || item.getSort() > 99) {
throw new DException("班级班号非法(须为1-99):" + item.getSort());
}
if (!sorts.add(item.getSort())) {
throw new DException("班级班号重复:" + item.getSort());
}
}
Integer schId = dto.getSchId();
Integer enrollYear = dto.getEnrollYear();
// 1. 学生组(组树在用户中心,不在本地库)
BoaGroup stuGroup = comSchTreeService.getStuTree(schId, false);
if (stuGroup == null) {
throw new DException("学生组不存在!");
}
// 2. 学段定位:逐学制比对学段名(起始年级编码由各校 base_sch_system_grade 数据决定,不硬编码)
List<Integer> schsystemIds = schBinSchsystemService.getSchBinSchsystemIds(schId);
Integer schsystemId = null;
for (Integer id : schsystemIds) {
if (stageName.equals(baseSchsystemService.getAcademicStageName(id))) {
schsystemId = id;
break;
}
}
if (schsystemId == null) {
throw new DException("学校未配置该学段:" + stageName);
}
List<BaseSchsystemGrade> gradeList = baseSchsystemGradeService.getSchGradeCacheList(schId, schsystemId, null);
if (gradeList == null || gradeList.isEmpty()) {
throw new DException("学校学制未配置年级,无法定位起始年级");
}
gradeList.sort(Comparator.comparing(BaseSchsystemGrade::getGradeId));
Integer gradeId = gradeList.get(0).getGradeId();
List<Integer> createdGroupIds = new ArrayList<>();
try {
Integer parentId = stuGroup.getId();
// 3. 多学段学校:查/建学段组
if (schsystemIds.size() > 1) {
List<BoaGroup> stageGroups = comSchTreeService.getAcademicStageGroupList(schId);
Optional<BoaGroup> hit = stageGroups == null ? Optional.empty()
: stageGroups.stream().filter(g -> stageName.equals(g.getName())).findFirst();
if (hit.isPresent()) {
parentId = hit.get().getId();
} else {
BoaGroup sectionGroup = sendUserBaseService.addGroup(stageName, parentId, schsystemId, NodeType.SECTION, schId);
createdGroupIds.add(sectionGroup.getId());
parentId = sectionGroup.getId();
}
}
// 4. 查/建年级组(⚠️ 多学段学校各学段都有同年份同名年级组,必须 .get(schsystemId) 过滤本学段)
Integer gradeGroupId = null;
List<SelectIntVO> gradeGroups = comSchTreeService.getSchGroupTypeList(schId, NodeType.GRADE.getId()).get(schsystemId);
if (gradeGroups != null) {
Optional<SelectIntVO> hit = gradeGroups.stream()
.filter(g -> enrollYear.toString().equals(g.getName())).findFirst();
if (hit.isPresent()) {
gradeGroupId = hit.get().getId();
}
}
if (gradeGroupId == null) {
BoaGroup gradeGroup = sendUserBaseService.addGroup(enrollYear.toString(), parentId, enrollYear, NodeType.GRADE, schId);
createdGroupIds.add(gradeGroup.getId());
gradeGroupId = gradeGroup.getId();
}
// 5. 逐班查/建班级组(code=入学年份+两位班号,入参 sort 为准)+ sch_class 幂等落库
List<SelectIntVO> classGroups = comSchTreeService.getSchGroupTypeList(schId, NodeType.CLASS.getId()).get(schsystemId);
Map<String, Integer> classGroupByName = classGroups == null ? Collections.emptyMap()
: classGroups.stream().collect(Collectors.toMap(SelectIntVO::getName, SelectIntVO::getId, (a, b) -> a));
Date now = new Date();
List<SchClass> saveList = new ArrayList<>();
List<SyncClassResultVO.ClassItem> outClasses = new ArrayList<>();
for (SyncClassDTO.ClassItem item : dto.getClasses()) {
int sort = item.getSort();
String code = enrollYear + String.format("%02d", sort);
Integer classGroupId = classGroupByName.get(code);
boolean created = false;
if (classGroupId == null) {
BoaGroup classGroup = sendUserBaseService.addGroup(code, gradeGroupId, sort, NodeType.CLASS, schId);
createdGroupIds.add(classGroup.getId());
classGroupId = classGroup.getId();
created = true;
}
// sch_class 按 classId(班级组id)幂等:已有行不覆盖(要修改走班级管理编辑功能);缺行补建
SchClass existRow = schClassRepository.findByClassIdAndSchId(classGroupId, schId).orElse(null);
String outName = sort + "班";
if (existRow == null) {
SchClass row = new SchClass();
row.setSchId(schId);
row.setGradeId(gradeId);
row.setSort(sort);
row.setCode(code);
row.setName(outName);
row.setCname(item.getName());
row.setGroupId(gradeGroupId);
row.setClassId(classGroupId);
row.setGraduate(false);
row.setEnrollYear(enrollYear);
if (item.getTeacherId() != null) {
row.setFirstTeacherId(item.getTeacherId());
}
row.setCreateDate(now);
row.setUpdateDate(now);
saveList.add(row);
} else {
outName = existRow.getName() != null ? existRow.getName() : outName;
}
SyncClassResultVO.ClassItem out = new SyncClassResultVO.ClassItem();
out.setSort(sort);
out.setClassId(classGroupId);
out.setCode(code);
out.setName(outName);
out.setCreated(created);
outClasses.add(out);
}
if (!saveList.isEmpty()) {
schClassRepository.saveAll(saveList);
}
classCacheService.clearClassCache(schId);
SyncClassResultVO vo = new SyncClassResultVO();
vo.setGradeId(gradeId);
vo.setGradeGroupId(gradeGroupId);
vo.setClasses(outClasses);
log.info("[addClass] schId={}, stage={}, enrollYear={}, 新建组={}, 班级 {} 个(新建 {})",
schId, stageName, enrollYear, createdGroupIds.size(), outClasses.size(),
outClasses.stream().filter(c -> Boolean.TRUE.equals(c.getCreated())).count());
return vo;
} catch (Exception e) {
// 组的删除是对用户中心的远程调用,不归本地事务管,须显式补偿;清单只含本次新建的组
log.error("[addClass] 添加班级失败,回滚本次新建组 {} 个:{}", createdGroupIds.size(), createdGroupIds, e);
sendUserBaseService.rollbackCreatedGroups(createdGroupIds, schId);
if (e instanceof DException) {
throw (DException) e;
}
throw new DException("添加班级失败:" + e.getMessage());
}
}
// ==================== 添加班级学生(同步学生) ====================
@Override
@Transactional(rollbackFor = Exception.class)
public SyncStudentResultVO addClassStudent(SyncClassStudentDTO dto) {
if (dto == null || dto.getSchId() == null || dto.getSchId() <= 0) {
throw new DException("schId不能为空");
}
if (dto.getStudents() == null || dto.getStudents().isEmpty()) {
throw new DException("students不能为空");
}
Integer schId = dto.getSchId();
boolean allOrNothing = Boolean.TRUE.equals(dto.getRollbackOnError());
// 整批预检:classId 必须属于本校(此时无任何副作用,直接报错)
Set<Integer> classIdSet = new LinkedHashSet<>();
for (SyncClassStudentDTO.StudentItem stu : dto.getStudents()) {
if (stu.getClassId() != null) {
classIdSet.add(stu.getClassId());
}
}
if (classIdSet.isEmpty()) {
throw new DException("students[].classId不能为空");
}
List<SchClass> belongClasses = schClassRepository.findBySchIdAndClassIds(schId, new ArrayList<>(classIdSet));
Map<Integer, SchClass> classMap = belongClasses.stream()
.collect(Collectors.toMap(SchClass::getClassId, c -> c, (a, b) -> a));
for (Integer classId : classIdSet) {
if (!classMap.containsKey(classId)) {
throw new DException("班级不存在或不属于本校:classId=" + classId);
}
}
Date now = new Date();
List<SchStudent> saveList = new ArrayList<>();
// 已建平台账号(按下标与 groupIds 对应,供整批回滚)
List<BoaUser> syncedList = new ArrayList<>();
List<Integer> syncedGroupIds = new ArrayList<>();
// 本批内 (classId, name) / account 查重(库内查重看不到本批未落库的行)
Set<String> batchNameKeys = new HashSet<>();
Set<String> batchAccounts = new HashSet<>();
List<SyncStudentResultVO.StudentItem> results = new ArrayList<>();
for (SyncClassStudentDTO.StudentItem stu : dto.getStudents()) {
SyncStudentResultVO.StudentItem r = new SyncStudentResultVO.StudentItem();
r.setName(stu.getName());
r.setAccount(stu.getAccount());
String fail = null;
if (StringUtils.isBlank(stu.getName())) {
fail = "姓名不能为空";
} else if (stu.getClassId() == null) {
fail = "classId不能为空";
}
if (fail == null && StringUtils.isNotBlank(stu.getAccount())) {
String account = stu.getAccount().trim();
r.setAccount(account);
// 幂等查重①:account 全库查询(非本校范围)
Optional<SchStudent> exist = schStudentRepository.findByAccount(account);
if (exist.isPresent()) {
SchStudent s = exist.get();
if (!schId.equals(s.getSchId())) {
fail = "账号已被其他学校占用";
} else if (Objects.equals(stu.getClassId(), s.getClassId())) {
r.setOk(true);
r.setStuId(s.getStuId());
r.setReason("已存在");
results.add(r);
continue;
} else {
fail = "账号已存在于其他班级";
}
} else if (!batchAccounts.add(account)) {
fail = "本批账号重复";
}
}
if (fail == null) {
// 幂等查重②:班内姓名唯一(含本批)
String nameKey = stu.getClassId() + "_" + stu.getName();
if (schStudentRepository.existsBySchIdAndClassIdAndName(schId, stu.getClassId(), stu.getName())) {
r.setOk(true);
r.setReason("班内同名已存在");
results.add(r);
continue;
}
if (!batchNameKeys.add(nameKey)) {
fail = "本批班级内姓名重复";
}
}
if (fail != null) {
r.setOk(false);
r.setReason(fail);
results.add(r);
if (allOrNothing) {
rollbackAllOrNothing(syncedList, syncedGroupIds, schId, "整批失败(all-or-nothing):" + stu.getName() + " " + fail);
}
continue;
}
// 新增:先收集不入库,尾部统一 saveAll
SchStudent entity = new SchStudent();
entity.setSchId(schId);
entity.setName(stu.getName());
entity.setGender(stu.getGender());
entity.setAccount(r.getAccount());
entity.setStuClassNo(stu.getStuClassNo());
entity.setGroupId(stu.getClassId());
entity.setClassId(stu.getClassId());
entity.setTransferStatus(TransferStatus.IN.getId());
entity.setAbnormalStatus(StuAbnormalStatus.NORMAL.getId());
entity.setStatus(UserStatus.NORMAL.getId());
entity.setSort(sortOf(stu.getStuClassNo()));
entity.setCreateDate(now);
entity.setUpdateDate(now);
try {
BoaUser boaUser = sendUserBaseService.addStudent(entity, stu.getPwd());
entity.setStuId(boaUser.getId());
saveList.add(entity);
syncedList.add(boaUser);
syncedGroupIds.add(stu.getClassId());
r.setOk(true);
r.setStuId(boaUser.getId());
} catch (Exception e) {
log.error("[addClassStudent] 建号失败 schId={}, name={}, account={}", schId, stu.getName(), r.getAccount(), e);
r.setOk(false);
String msg = e.getMessage();
r.setReason(msg == null || msg.isEmpty() ? e.getClass().getSimpleName() : msg);
results.add(r);
if (allOrNothing) {
rollbackAllOrNothing(syncedList, syncedGroupIds, schId, "整批失败(all-or-nothing):" + stu.getName() + " " + r.getReason());
}
continue;
}
results.add(r);
}
try {
if (!saveList.isEmpty()) {
schStudentRepository.saveAll(saveList);
}
} catch (Exception e) {
// 落库失败属批次级失败:all-or-nothing 删已建账号;聚合模式保留账号(重推按 account 幂等补齐,避免删号/重建抖动)
log.error("[addClassStudent] 学生落库失败 schId={}, 已建账号 {} 个", schId, syncedList.size(), e);
if (allOrNothing) {
rollbackAllOrNothing(syncedList, syncedGroupIds, schId, "整批失败(all-or-nothing):学生落库失败 " + e.getMessage());
}
throw new DException("学生落库失败(已建账号保留,可重推补齐):" + e.getMessage());
}
studentCacheService.clearClassStudentCaches(schId, classIdSet);
long okCount = results.stream().filter(x -> Boolean.TRUE.equals(x.getOk())).count();
log.info("[addClassStudent] schId={}, 学生 {} 人:成功 {} 失败 {}(allOrNothing={})",
schId, results.size(), okCount, results.size() - okCount, allOrNothing);
SyncStudentResultVO vo = new SyncStudentResultVO();
vo.setResults(results);
return vo;
}
/** all-or-nothing 失败出口:删本批已建平台账号后整批报错(本地事务由 @Transactional 回滚) */
private void rollbackAllOrNothing(List<BoaUser> syncedList, List<Integer> syncedGroupIds,
Integer schId, String message) {
sendUserBaseService.rollbackSyncedStudents(syncedList, syncedGroupIds, schId);
throw new DException(message);
}
/** 学号数字部分 → 排序(与导入模板口径一致) */
private Integer sortOf(String stuClassNo) {
if (StringUtils.isBlank(stuClassNo)) {
return null;
}
try {
String digits = stuClassNo.replaceAll("\\D+", "");
if (StringUtils.isNotBlank(digits)) {
return Integer.parseInt(digits);
}
} catch (NumberFormatException ignored) {
// ignore
}
return null;
}
}
@@ -0,0 +1,93 @@
package com.yunhai.bschclassmanage.service.impl.teacher;
import com.yunhai.bschclassmanage.Repository.SchTeacherGroupRepository;
import com.yunhai.bschclassmanage.Repository.SchTeacherRepository;
import com.yunhai.bschclassmanage.entity.SchTeacher;
import com.yunhai.bschclassmanage.entity.SchTeacherGroup;
import com.yunhai.bschclassmanage.model.dto.TeacherBaseInfoDTO;
import com.yunhai.bschclassmanage.annotation.SyncOpLog;
import com.yunhai.bschclassmanage.enums.SyncDataType;
import com.yunhai.bschclassmanage.service.ISendUserBaseService;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.platform.utils.exception.DException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.stream.Collectors;
/** 开放接口只修改四项资料,经既有教师同步通道(SendUserBaseService)推送用户中心。 */
@Service
public class SchTeacherBaseInfoService {
@Autowired private SchTeacherRepository teacherRepository;
@Autowired private SchTeacherGroupRepository groupRepository;
@Autowired private ISendUserBaseService sendUserBaseService;
@Autowired private SchTeacherCacheService cacheService;
public TeacherBaseInfoDTO get(Integer schId, Integer teaId) {
requireMembership(schId, teaId);
return toDto(schId, teachers(teaId).get(0));
}
@Transactional(rollbackFor = Exception.class)
@SyncOpLog(SyncDataType.SEND_EDIT_TEACHER)
public TeacherBaseInfoDTO edit(TeacherBaseInfoDTO dto) {
List<SchTeacherGroup> memberships = requireMembership(dto.getSchId(), dto.getTeaId());
List<SchTeacher> teachers = teachers(dto.getTeaId());
String name = dto.getName() == null ? "" : dto.getName().trim();
if (name.isEmpty()) throw new DException("姓名不能为空");
// 同名规则与管理端一致,排除教师自己的所有本地记录。
List<Integer> ids = groupRepository.findTeaIdsBySchId(dto.getSchId());
if (teacherRepository.findByTeaIdIn(ids).stream()
.anyMatch(t -> !dto.getTeaId().equals(t.getTeaId()) && name.equals(t.getName()))) {
throw new DException("该校下已存在同名教师:" + name);
}
for (SchTeacher teacher : teachers) {
teacher.setName(name);
teacher.setGender(dto.getGender());
teacher.setIdCard(trim(dto.getIdcard()));
teacher.setTel(trim(dto.getPhone()));
teacher.setUpdateDate(new Date());
teacher.setUpdateUserId(dto.getTeaId());
}
// 先同步;失败抛异常并回滚本地事务。teaId 可能对应多行,统一修改。
// 复用管理端同款教师同步(用户中心 /oapi/user/edit/{id},身份追加语义、不改密码);
// groupId 仅用于解析学校范围,不会移动教师分组。
BoaUser synced = sendUserBaseService.editTeacher(teachers.get(0), null, memberships.get(0).getGroupId());
if (synced == null || !dto.getTeaId().equals(synced.getId())) {
throw new DException("用户中心未返回有效教师信息,同步失败");
}
teacherRepository.saveAll(teachers);
Set<Integer> schools = groupRepository.findByTeaIdIn(Collections.singletonList(dto.getTeaId())).stream()
.map(SchTeacherGroup::getSchId).collect(Collectors.toSet());
schools.add(dto.getSchId());
cacheService.clearTeacherCaches(schools);
return toDto(dto.getSchId(), teachers.get(0));
}
private List<SchTeacherGroup> requireMembership(Integer schId, Integer teaId) {
if (schId == null || schId <= 0 || teaId == null || teaId <= 0) {
throw new DException("学校和教师平台用户ID不能为空");
}
List<SchTeacherGroup> memberships = groupRepository.findByTeaIdAndSchId(teaId, schId).stream()
.filter(g -> Integer.valueOf(0).equals(g.getTransferStatus())).collect(Collectors.toList());
if (memberships.isEmpty()) throw new DException("教师不属于指定学校或已转出");
return memberships;
}
private List<SchTeacher> teachers(Integer teaId) {
List<SchTeacher> teachers = teacherRepository.findByTeaId(teaId);
if (teachers.isEmpty()) throw new DException("教师不存在");
return teachers;
}
private TeacherBaseInfoDTO toDto(Integer schId, SchTeacher teacher) {
TeacherBaseInfoDTO dto = new TeacherBaseInfoDTO();
dto.setSchId(schId); dto.setTeaId(teacher.getTeaId()); dto.setName(teacher.getName());
dto.setGender(teacher.getGender()); dto.setIdcard(teacher.getIdCard()); dto.setPhone(teacher.getTel());
return dto;
}
private String trim(String value) { return value == null || value.trim().isEmpty() ? null : value.trim(); }
}
@@ -191,7 +191,7 @@ public class SchTeacherRelationService {
}
// 异步记录操作日志
schTeacherRepository.findByTeaId(teaId).ifPresent(teacher ->
schTeacherRepository.findByTeaId(teaId).stream().findFirst().ifPresent(teacher ->
logService.setTransferInLogs(user, Collections.singletonList(teacher), targetGroupId));
// 清除所有涉及学校的缓存
@@ -133,8 +133,8 @@ public class SchClassTeachQueryService {
return buildEmptyResult();
}
// 2. 获取学科数据
Map<String, String> subjectNameMap = schDicSubjectService.toIdAndNameMap(schId, schsystemId);
// 2. 获取学科数据(仅启用的学科)
Map<String, String> subjectNameMap = schDicSubjectService.toEnableIdAndNameMap(schId, schsystemId);
if (subjectNameMap == null || subjectNameMap.isEmpty()) {
return buildEmptyResult();
}
+10 -4
View File
@@ -74,7 +74,7 @@ spring:
yh:
sso:
server-url-prefix: http://sso.yunhaixingtong.com
client-host-url: http://192.168.10.166:8086
client-host-url: http://localhost:8080
allow-headers: all
allow-origins: all
url-pattern: /api/*
@@ -84,11 +84,17 @@ yh:
# 本应用自身身份(客户端签名用)
app-key: 100002
app-secret: 2314a3b158b24a838b2b9b69fd3db118
# 调用方信任名单:分班管理(AcaClassAssign,/oapi/class/AddClass|AddClassStudent)。
# 注意:app-auth 见 X-App-Key 即校验,未登记的 appKey 一律 401(实测 2026-10-21)。
# 生产发布需在 Nacos 同步登记该 appKey/secret,并对 /oapi/class/* 启用 enforce-patterns。
trusted-apps:
"100026": "73c08fceae3b48788358195760bf5977"
server:
# 分班推送写接口强制验签;只读 /oapi/sch/* 维持匿名(既有调用方不受影响)
enforce-patterns: [/oapi/class/*]
open:
enabled: true
# 四域服务目录零配置(userbase/userclass/usermsg/flowcenter 枚举兜底 = base-host + 默认 ctx)
base-host: http://base.yunhaixingtong.com
user-center-url: http://base.yunhaixingtong.com/userbase
group-center-url: http://base.yunhaixingtong.com/userbase
FILE_PATH: D://工作区/JAVA/bsch/
+1 -1
View File
@@ -16,7 +16,7 @@
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/app/logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/app/logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
<fileNamePattern>/app/logs/app.%d{yyyy-MM-dd}.log.zip</fileNamePattern>
<maxHistory>30</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
@@ -0,0 +1,106 @@
package com.yunhai.bschclassmanage;
import com.yunhai.bschclassmanage.service.IStudentClassHistoryService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* 本地复现 api/history/getHistoryClassTeachByStuId?stuId=10068 的内部服务错误。
* 直接调用 service 层(绕过 SSO 过滤器),打印真实异常堆栈。
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class ReproduceHistoryTeachByStuIdTest {
@Autowired
private IStudentClassHistoryService studentClassHistoryService;
@Test
void reproduce() {
try {
// 模拟真实 HTTP 场景:UserContextInterceptor.resolveUser 恒返回 null,schId 为 null
Object result = studentClassHistoryService.getHistoryClassTeachByStuId(null, 10068);
System.out.println("=== SUCCESS (schId=null) ===");
System.out.println(result);
} catch (Throwable e) {
System.out.println("=== EXCEPTION (schId=null): " + e.getClass().getName() + " ===");
System.out.println("message: " + e.getMessage());
e.printStackTrace();
}
}
@Autowired
private com.yunhai.bschclassmanage.utils.RedisUtil redisUtil;
@Autowired
private com.yunhai.bschclassmanage.Repository.SchStudentRepository schStudentRepository;
/**
* 回归用例:同一 stu_id 在 sch_student 存在多行(学生在多所学校各有一条就学记录)。
* 修复前 findByStuId 返回 Optional,会抛 NonUniqueResultException(线上表现为 code 6 内部服务错误)。
* 修复后应取当前学校那一行正常返回。@Transactional 保证测试数据回滚。
*/
@org.springframework.transaction.annotation.Transactional
@Test
void duplicateStuIdShouldNotCrash() {
// 本地库里 stu_id=10068 已有一行(sch_id=3, class_id=298),再造一行别校的记录
com.yunhai.bschclassmanage.entity.SchStudent other = new com.yunhai.bschclassmanage.entity.SchStudent();
other.setStuId(10068);
other.setSchId(999);
other.setClassId(888);
other.setName("重复记录-别校");
schStudentRepository.save(other);
schStudentRepository.flush();
// 当前学校 schId=3 的记录应被正确选中,不再抛 NonUniqueResultException
Object result = studentClassHistoryService.getHistoryClassTeachByStuId(3, 10068);
System.out.println("=== DUPLICATE stu_id RESULT ===");
System.out.println(result);
if (result == null) {
throw new AssertionError("expected non-null result");
}
// 换成重复行所在学校 schId=999:实时表命中但班级不存在 → 应返回业务异常而不是内部错误
try {
studentClassHistoryService.getHistoryClassTeachByStuId(999, 10068);
System.out.println("=== schId=999 unexpectedly succeeded ===");
} catch (com.yunhai.platform.utils.exception.DException e) {
System.out.println("=== schId=999 DException (expected): " + e.getMessage());
}
}
@Test
void poisonCacheRead() {
for (String key : new String[]{
"bsch.classmanage.system_yearterm_poison",
"bsch.classmanage.system_yearterm_poison2"}) {
try {
Object raw = redisUtil.get(key, Object.class);
System.out.println("=== RAW GET [" + key + "]: " + (raw == null ? "null" : raw.getClass().getName()));
} catch (Throwable e) {
System.out.println("=== RAW GET EXCEPTION [" + key + "]: " + e.getClass().getName() + " ===");
System.out.println("message: " + e.getMessage());
}
try {
Object list = redisUtil.getList(key, com.yunhai.bschclassmanage.entity.BaseYearTerm.class);
System.out.println("=== GET LIST [" + key + "]: " + list);
} catch (Throwable e) {
System.out.println("=== GET LIST EXCEPTION [" + key + "]: " + e.getClass().getName() + " ===");
System.out.println("message: " + e.getMessage());
}
}
}
@Test
void reproduceWithSchId() {
try {
Object result = studentClassHistoryService.getHistoryClassTeachByStuId(3, 10068);
System.out.println("=== SUCCESS (schId=3) ===");
System.out.println(result);
} catch (Throwable e) {
System.out.println("=== EXCEPTION (schId=3): " + e.getClass().getName() + " ===");
System.out.println("message: " + e.getMessage());
e.printStackTrace();
}
}
}
@@ -0,0 +1,114 @@
package com.yunhai.bschclassmanage.controller.oapi.sch;
import com.yunhai.bschclassmanage.model.vo.sch.OapiGradeNameVO;
import com.yunhai.bschclassmanage.service.IBaseSchsystemGradeService;
import com.yunhai.bschclassmanage.service.IBaseSchsystemService;
import com.yunhai.bschclassmanage.service.ISchBinSchsystemService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
/** getGradeName 开放接口单测:学段→学制匹配、学年反查年级字典名、无匹配返回 null */
class OapiGetGradeNameTest {
private OapiSchClassController controller;
private ISchBinSchsystemService schBinSchsystemService;
private IBaseSchsystemService baseSchsystemService;
private IBaseSchsystemGradeService baseSchsystemGradeService;
@BeforeEach
void setUp() {
controller = new OapiSchClassController();
schBinSchsystemService = Mockito.mock(ISchBinSchsystemService.class);
baseSchsystemService = Mockito.mock(IBaseSchsystemService.class);
baseSchsystemGradeService = Mockito.mock(IBaseSchsystemGradeService.class);
ReflectionTestUtils.setField(controller, "schClassRepository",
Mockito.mock(com.yunhai.bschclassmanage.Repository.SchClassRepository.class));
ReflectionTestUtils.setField(controller, "schInfoRepository",
Mockito.mock(com.yunhai.bschclassmanage.Repository.SchInfoRepository.class));
ReflectionTestUtils.setField(controller, "schClassService",
Mockito.mock(com.yunhai.bschclassmanage.service.ISchClassService.class));
ReflectionTestUtils.setField(controller, "schTeacherService",
Mockito.mock(com.yunhai.bschclassmanage.service.ISchTeacherService.class));
ReflectionTestUtils.setField(controller, "schBinSchsystemService", schBinSchsystemService);
ReflectionTestUtils.setField(controller, "baseSchsystemService", baseSchsystemService);
ReflectionTestUtils.setField(controller, "baseSchsystemGradeService", baseSchsystemGradeService);
// 云海小学:双学段(小学+初中),各自年级字典与入学年份映射
when(schBinSchsystemService.getSchBinSchsystemIds(27)).thenReturn(Arrays.asList(5, 9));
when(baseSchsystemService.getAcademicStageName(5)).thenReturn("小学");
when(baseSchsystemService.getAcademicStageName(9)).thenReturn("初中");
Map<Integer, Integer> primaryYears = new LinkedHashMap<>();
primaryYears.put(1, 2026);
primaryYears.put(2, 2025);
primaryYears.put(3, 2024);
when(baseSchsystemGradeService.getNowYearGradeMap(27, 5)).thenReturn(primaryYears);
Map<Integer, String> primaryNames = new HashMap<>();
primaryNames.put(1, "小一");
primaryNames.put(2, "小二");
primaryNames.put(3, "小三");
when(baseSchsystemGradeService.getGradeNameMap(27, 5, null)).thenReturn(primaryNames);
Map<Integer, Integer> juniorYears = new LinkedHashMap<>();
juniorYears.put(7, 2026);
juniorYears.put(8, 2025);
when(baseSchsystemGradeService.getNowYearGradeMap(27, 9)).thenReturn(juniorYears);
Map<Integer, String> juniorNames = new HashMap<>();
juniorNames.put(7, "初一");
juniorNames.put(8, "初二");
when(baseSchsystemGradeService.getGradeNameMap(27, 9, null)).thenReturn(juniorNames);
}
@Test
void resolvesGradeNameByStageAndYear() {
// 小学 2026 级入学 → 小一(学校字典真实名称)
OapiGradeNameVO vo = controller.getGradeName(27, 2026, 1).getBody();
assertEquals(1, vo.getGradeId());
assertEquals("小一", vo.getGradeName());
assertEquals(2026, vo.getYear());
// 初中 2026 → 初一(同校多学段按 stage 区分)
assertEquals("初一", controller.getGradeName(27, 2026, 2).getBody().getGradeName());
// 低年级已升学:2024 入学的小学年级 → 小三
assertEquals("小三", controller.getGradeName(27, 2024, 1).getBody().getGradeName());
}
@Test
void returnsNullWhenNoMatch() {
// 年份无对应年级(超出字典范围)
assertNull(controller.getGradeName(27, 2020, 1).getBody());
// 学段在学校未开设(高中不存在)
assertNull(controller.getGradeName(27, 2026, 3).getBody());
// 参数缺失
assertNull(controller.getGradeName(null, 2026, 1).getBody());
assertNull(controller.getGradeName(27, null, 1).getBody());
}
@Test
void skipsBrokenSchsystemAndContinues() {
// 单个学制学段名缺失(学制未找到等)跳过,不影响其余学制匹配
when(baseSchsystemService.getAcademicStageName(5)).thenThrow(new RuntimeException("学制未找到!"));
assertEquals("初一", controller.getGradeName(27, 2026, 2).getBody().getGradeName());
assertNull(controller.getGradeName(27, 2026, 1).getBody());
}
@Test
void emptyYearMapReturnsNull() {
when(baseSchsystemGradeService.getNowYearGradeMap(eq(27), anyInt())).thenReturn(new HashMap<>());
assertNull(controller.getGradeName(27, 2026, 1).getBody());
}
}
@@ -0,0 +1,77 @@
package com.yunhai.bschclassmanage.service.impl.teacher;
import com.yunhai.bschclassmanage.Repository.*;
import com.yunhai.bschclassmanage.entity.*;
import com.yunhai.bschclassmanage.model.dto.TeacherBaseInfoDTO;
import com.yunhai.bschclassmanage.service.ISendUserBaseService;
import com.yunhai.platform.utils.exception.DException;
import com.yunhai.platform.utils.model.BoaUser;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;
@ExtendWith(MockitoExtension.class)
class SchTeacherBaseInfoServiceTest {
@InjectMocks private SchTeacherBaseInfoService service;
@Mock private SchTeacherRepository teacherRepository;
@Mock private SchTeacherGroupRepository groupRepository;
@Mock private ISendUserBaseService sendUserBaseService;
@Mock private SchTeacherCacheService cacheService;
private SchTeacherGroup group(int school) {
SchTeacherGroup g = new SchTeacherGroup();
g.setSchId(school); g.setTeaId(101); g.setGroupId(31); g.setTransferStatus(0);
return g;
}
private TeacherBaseInfoDTO request() {
TeacherBaseInfoDTO d = new TeacherBaseInfoDTO();
d.setSchId(3); d.setTeaId(101); d.setName("新姓名"); d.setGender(1); d.setPhone("");
return d;
}
private List<SchTeacher> givenTeacher() {
when(groupRepository.findByTeaIdAndSchId(101, 3)).thenReturn(Collections.singletonList(group(3)));
SchTeacher a = new SchTeacher(); a.setId(11); a.setTeaId(101); a.setAccount("teacher"); a.setStatus(1);
SchTeacher b = new SchTeacher(); b.setId(12); b.setTeaId(101); b.setAccount("teacher"); b.setStatus(1);
List<SchTeacher> rows = Arrays.asList(a,b);
when(teacherRepository.findByTeaId(101)).thenReturn(rows);
return rows;
}
@Test void updatesAllTeacherRowsAndInvalidatesAllSchoolsAfterSync() {
List<SchTeacher> rows = givenTeacher();
when(sendUserBaseService.editTeacher(any(SchTeacher.class), isNull(), eq(31)))
.thenReturn(new BoaUser(101, "teacher", "新姓名"));
when(groupRepository.findByTeaIdIn(Collections.singletonList(101))).thenReturn(Arrays.asList(group(3),group(4)));
assertEquals("新姓名", service.edit(request()).getName());
for (SchTeacher t : rows) {
assertEquals("新姓名", t.getName()); assertEquals(1,t.getGender());
assertNull(t.getTel()); assertEquals("teacher",t.getAccount()); assertEquals(1,t.getStatus());
}
InOrder order = inOrder(sendUserBaseService,teacherRepository,cacheService);
order.verify(sendUserBaseService).editTeacher(any(SchTeacher.class), isNull(), eq(31));
order.verify(teacherRepository).saveAll(rows);
order.verify(cacheService).clearTeacherCaches(new HashSet<>(Arrays.asList(3,4)));
}
@Test void rejectsWrongSchoolAndTransferredOutTeacher() {
SchTeacherGroup out = group(3); out.setTransferStatus(1);
when(groupRepository.findByTeaIdAndSchId(101,3)).thenReturn(Collections.singletonList(out));
assertThrows(DException.class, () -> service.edit(request()));
verifyNoInteractions(sendUserBaseService,teacherRepository,cacheService);
}
@Test void syncFailureDoesNotSaveOrClearCache() {
List<SchTeacher> rows = givenTeacher();
when(sendUserBaseService.editTeacher(any(SchTeacher.class), isNull(), eq(31)))
.thenThrow(new DException("平台失败"));
assertThrows(DException.class, () -> service.edit(request()));
verify(teacherRepository,never()).saveAll(any()); verifyNoInteractions(cacheService);
// 同步前已就地改写的实体随事务回滚,不落库
assertEquals("新姓名", rows.get(0).getName());
}
}