feat: enforce organization mode boundaries

main
chenyuan 1 month ago
parent e4c5093466
commit 8e4e5b6ac7

@ -12,6 +12,7 @@ import com.sztzjy.linkCommerce.entity.dto.SchoolMajorDto;
import com.sztzjy.linkCommerce.entity.importDto.SchoolAdminStudentImportDTO;
import com.sztzjy.linkCommerce.mapper.*;
import com.sztzjy.linkCommerce.service.SchoolAdminService;
import com.sztzjy.linkCommerce.service.SchoolProductConfigService;
import com.sztzjy.linkCommerce.service.SchoolService;
import com.sztzjy.linkCommerce.service.UserInfoService;
import com.sztzjy.linkCommerce.util.ResultEntity;
@ -56,6 +57,8 @@ public class SchoolAdminController {
private UserinfoMapper userinfoMapper;
@Autowired
private TaskAllocationMapper taskAllocationMapper;
@Autowired
private SchoolProductConfigService schoolProductConfigService;
@GetMapping("/school")
@ApiOperation("学校管理员-当前学校详情")
@ -81,6 +84,7 @@ public class SchoolAdminController {
@RequestParam(required = false) String schoolFacultyName,
HttpServletRequest request) {
String schoolId = currentSchoolId(request);
requireAdministrativeOrganization(schoolId);
PageInfo<SchoolFacultyDto> pageInfo = schoolService.seleteSchoolFaculty(index, size, schoolId, schoolFacultyName);
return new ResultEntity<>(HttpStatus.OK, "查询成功", pageInfo);
}
@ -89,6 +93,7 @@ public class SchoolAdminController {
@ApiOperation("学校管理员-新增院系")
public ResultEntity addFaculty(@RequestBody SchoolFaculty faculty, HttpServletRequest request) {
String schoolId = currentSchoolId(request);
requireAdministrativeOrganization(schoolId);
if (StringUtils.isBlank(faculty.getSchoolFacultyName())) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "院系名称不能为空");
}
@ -105,6 +110,7 @@ public class SchoolAdminController {
@RequestBody SchoolFaculty faculty,
HttpServletRequest request) {
String schoolId = currentSchoolId(request);
requireAdministrativeOrganization(schoolId);
SchoolFaculty existing = requireFaculty(id, schoolId);
faculty.setSchoolFacultyId(existing.getSchoolFacultyId());
faculty.setSchoolId(schoolId);
@ -116,6 +122,7 @@ public class SchoolAdminController {
@ApiOperation("学校管理员-删除院系")
public ResultEntity deleteFaculty(@PathVariable String id, HttpServletRequest request) {
String schoolId = currentSchoolId(request);
requireAdministrativeOrganization(schoolId);
requireFaculty(id, schoolId);
if (hasMajorInFaculty(id) || hasUserByFaculty(id)) {
return new ResultEntity<>(HttpStatus.ACCEPTED, "院系下存在专业或用户,不能删除");
@ -132,6 +139,7 @@ public class SchoolAdminController {
@RequestParam(required = false) String schoolFacultyId,
HttpServletRequest request) {
String schoolId = currentSchoolId(request);
requireAdministrativeOrganization(schoolId);
if (StringUtils.isNotBlank(schoolFacultyId)) {
requireFaculty(schoolFacultyId, schoolId);
}
@ -143,6 +151,7 @@ public class SchoolAdminController {
@ApiOperation("学校管理员-新增专业")
public ResultEntity addMajor(@RequestBody SchoolMajor major, HttpServletRequest request) {
String schoolId = currentSchoolId(request);
requireAdministrativeOrganization(schoolId);
if (StringUtils.isBlank(major.getSchoolMajorName())) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "专业名称不能为空");
}
@ -159,6 +168,7 @@ public class SchoolAdminController {
@RequestBody SchoolMajor major,
HttpServletRequest request) {
String schoolId = currentSchoolId(request);
requireAdministrativeOrganization(schoolId);
requireMajor(id, schoolId);
if (StringUtils.isNotBlank(major.getSchoolFacultyId())) {
requireFaculty(major.getSchoolFacultyId(), schoolId);
@ -172,6 +182,7 @@ public class SchoolAdminController {
@ApiOperation("学校管理员-删除专业")
public ResultEntity deleteMajor(@PathVariable String id, HttpServletRequest request) {
String schoolId = currentSchoolId(request);
requireAdministrativeOrganization(schoolId);
requireMajor(id, schoolId);
if (hasClassInMajor(id) || hasUserByMajor(id)) {
return new ResultEntity<>(HttpStatus.ACCEPTED, "专业下存在班级或用户,不能删除");
@ -189,6 +200,7 @@ public class SchoolAdminController {
@RequestParam(required = false) String schoolMajorId,
HttpServletRequest request) {
String schoolId = currentSchoolId(request);
requireAdministrativeOrganization(schoolId);
if (StringUtils.isNotBlank(schoolMajorId)) {
requireMajor(schoolMajorId, schoolId);
}
@ -200,6 +212,7 @@ public class SchoolAdminController {
@ApiOperation("学校管理员-新增班级")
public ResultEntity addClass(@RequestBody SchoolClass schoolClass, HttpServletRequest request) {
String schoolId = currentSchoolId(request);
requireAdministrativeOrganization(schoolId);
if (StringUtils.isBlank(schoolClass.getClassName())) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "班级名称不能为空");
}
@ -217,6 +230,7 @@ public class SchoolAdminController {
@RequestBody SchoolClass schoolClass,
HttpServletRequest request) {
String schoolId = currentSchoolId(request);
requireAdministrativeOrganization(schoolId);
requireClass(id, schoolId);
if (StringUtils.isNotBlank(schoolClass.getSchoolMajorId())) {
requireMajor(schoolClass.getSchoolMajorId(), schoolId);
@ -231,6 +245,7 @@ public class SchoolAdminController {
@ApiOperation("学校管理员-删除班级")
public ResultEntity deleteClass(@PathVariable String id, HttpServletRequest request) {
String schoolId = currentSchoolId(request);
requireAdministrativeOrganization(schoolId);
requireClass(id, schoolId);
if (hasStudentInClass(id) || hasTaskInClass(id, schoolId)) {
return new ResultEntity<>(HttpStatus.ACCEPTED, "班级下存在学生或任务,不能删除");
@ -302,6 +317,7 @@ public class SchoolAdminController {
@RequestParam(required = false) String schoolClassId,
HttpServletRequest request) {
String schoolId = currentSchoolId(request);
requireAdministrativeOrganization(schoolId);
if (StringUtils.isNotBlank(schoolClassId)) {
requireClass(schoolClassId, schoolId);
}
@ -313,6 +329,7 @@ public class SchoolAdminController {
@ApiOperation("学校管理员-新增学生")
public ResultEntity addStudent(@RequestBody Userinfo student, HttpServletRequest request) {
String schoolId = currentSchoolId(request);
requireAdministrativeOrganization(schoolId);
if (StringUtils.isBlank(student.getUsername()) || StringUtils.isBlank(student.getName())) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "学生姓名和学号不能为空");
}
@ -337,6 +354,7 @@ public class SchoolAdminController {
@Transactional(rollbackFor = Exception.class)
public ResultEntity importStudents(@RequestParam("file") MultipartFile file, HttpServletRequest request) {
String schoolId = currentSchoolId(request);
requireAdministrativeOrganization(schoolId);
ResultEntity fileError = validateImportFile(file);
if (fileError != null) {
return fileError;
@ -377,6 +395,7 @@ public class SchoolAdminController {
@RequestBody Userinfo student,
HttpServletRequest request) {
String schoolId = currentSchoolId(request);
requireAdministrativeOrganization(schoolId);
requireUser(id, schoolId, 4);
if (StringUtils.isNotBlank(student.getSchoolClassId())) {
requireClass(student.getSchoolClassId(), schoolId);
@ -391,6 +410,7 @@ public class SchoolAdminController {
@ApiOperation("学校管理员-删除学生")
public ResultEntity deleteStudent(@PathVariable String id, HttpServletRequest request) {
String schoolId = currentSchoolId(request);
requireAdministrativeOrganization(schoolId);
requireUser(id, schoolId, 4);
int count = userinfoMapper.deleteByPrimaryKey(id);
return writeResult(count, "删除成功", "删除失败");
@ -401,6 +421,10 @@ public class SchoolAdminController {
return schoolAdminService.requireSchoolAdmin(user);
}
private void requireAdministrativeOrganization(String schoolId) {
schoolProductConfigService.requireAdminClassEnabled(schoolId);
}
private ResultEntity writeResult(int count, String success, String fail) {
if (count > 0) {
return new ResultEntity<>(HttpStatus.OK, success);

@ -1115,9 +1115,20 @@ public class UserController {
for (TeacherRosterStudentImportDTO row : rows) {
String name = row == null ? "" : StringUtils.trimToEmpty(row.getName());
String username = row == null ? "" : StringUtils.trimToEmpty(row.getUsername());
if (StringUtils.isBlank(name) || StringUtils.isBlank(username) || userInfoService.existsByUserName(username)) {
if (StringUtils.isBlank(name) || StringUtils.isBlank(username)) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "学生姓名或学号无效");
}
Userinfo existingStudent = userinfoMapper.selectBySchoolIdAndUsername(operator.getSchoolId(), username);
if (existingStudent != null) {
if (!Integer.valueOf(4).equals(existingStudent.getRole()) || !StringUtils.equals(existingStudent.getName(), name)) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "学号与已有学生信息冲突");
}
upsertActiveTeachingClassMember(teachingClassId, existingStudent, null, "TEACHER_ROSTER_IMPORT");
continue;
}
if (userInfoService.existsByUserName(username)) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "学号已被其他学校使用");
}
Userinfo student = new Userinfo();
student.setUserId(UUID.randomUUID().toString());
student.setName(name);
@ -1545,7 +1556,6 @@ public class UserController {
|| StringUtils.isBlank(student.getUserId())) {
return;
}
teachingClassStudentMapper.exitActiveByStudentUserIdExceptClass(student.getUserId(), teachingClassId);
TeachingClassStudent existing = teachingClassStudentMapper
.selectByStudentUserIdAndTeachingClassId(student.getUserId(), teachingClassId);
if (existing != null) {

@ -24,6 +24,8 @@ public interface UserinfoMapper {
Userinfo selectByPrimaryKey(String userId);
Userinfo selectBySchoolIdAndUsername(@Param("schoolId") String schoolId, @Param("username") String username);
int updateByExampleSelective(@Param("record") Userinfo record, @Param("example") UserinfoExample example);
int updateByExample(@Param("record") Userinfo record, @Param("example") UserinfoExample example);
@ -35,4 +37,4 @@ public interface UserinfoMapper {
@Select("select DISTINCT school_id from userinfo ")
List<String> selectAllSchoolIds();
}
}

@ -1,8 +1,12 @@
package com.sztzjy.linkCommerce.service.impl;
import com.sztzjy.linkCommerce.config.exception.handler.ServiceException;
import com.sztzjy.linkCommerce.entity.SchoolClassExample;
import com.sztzjy.linkCommerce.entity.SchoolProductConfig;
import com.sztzjy.linkCommerce.entity.UserinfoExample;
import com.sztzjy.linkCommerce.mapper.SchoolClassMapper;
import com.sztzjy.linkCommerce.mapper.SchoolProductConfigMapper;
import com.sztzjy.linkCommerce.mapper.UserinfoMapper;
import com.sztzjy.linkCommerce.service.SchoolProductConfigService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
@ -21,6 +25,12 @@ public class SchoolProductConfigServiceImpl implements SchoolProductConfigServic
@Autowired
public SchoolProductConfigMapper schoolProductConfigMapper;
@Autowired
public SchoolClassMapper schoolClassMapper;
@Autowired
public UserinfoMapper userinfoMapper;
@Override
public void createDefaultConfig(String schoolId) {
if (StringUtils.isBlank(schoolId)) {
@ -45,7 +55,11 @@ public class SchoolProductConfigServiceImpl implements SchoolProductConfigServic
if (!standard && !teacherManaged) {
throw new ServiceException(HttpStatus.BAD_REQUEST, "学校组织模式与名单维护方不匹配");
}
getRequiredConfig(config.getSchoolId());
SchoolProductConfig current = getRequiredConfig(config.getSchoolId());
if (!StringUtils.equals(current.getOrganizationMode(), config.getOrganizationMode())
&& hasAdministrativeOrganizationData(config.getSchoolId())) {
throw new ServiceException(HttpStatus.BAD_REQUEST, "学校已有组织数据,不能直接切换组织模式");
}
config.setUpdateTime(new Date());
schoolProductConfigMapper.updateByPrimaryKey(config);
return getRequiredConfig(config.getSchoolId());
@ -79,4 +93,19 @@ public class SchoolProductConfigServiceImpl implements SchoolProductConfigServic
throw new ServiceException(HttpStatus.BAD_REQUEST, "当前学校未启用行政班");
}
}
private boolean hasAdministrativeOrganizationData(String schoolId) {
SchoolClassExample classExample = new SchoolClassExample();
classExample.createCriteria()
.andSchoolIdEqualTo(schoolId)
.andClassTypeEqualTo("ADMIN");
if (schoolClassMapper.countByExample(classExample) > 0) {
return true;
}
UserinfoExample userExample = new UserinfoExample();
userExample.createCriteria()
.andSchoolIdEqualTo(schoolId)
.andSchoolClassIdIsNotNull();
return userinfoMapper.countByExample(userExample) > 0;
}
}

@ -107,6 +107,12 @@
from userinfo
where user_id = #{userId,jdbcType=VARCHAR}
</select>
<select id="selectBySchoolIdAndUsername" resultMap="BaseResultMap">
select <include refid="Base_Column_List" /> from userinfo
where school_id = #{schoolId,jdbcType=VARCHAR}
and username = #{username,jdbcType=VARCHAR}
limit 1
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.String">
delete from userinfo
where user_id = #{userId,jdbcType=VARCHAR}

@ -1,11 +1,15 @@
package com.sztzjy.linkCommerce.controller.schooladmin;
import com.sztzjy.linkCommerce.service.SchoolProductConfigService;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
class SchoolAdminControllerTest {
@ -18,4 +22,15 @@ class SchoolAdminControllerTest {
assertTrue(exists, "School admin student import must use /api/school-admin/students/import");
}
@Test
void administrativeOrganizationGuardDelegatesToSchoolConfiguration() {
SchoolAdminController controller = new SchoolAdminController();
SchoolProductConfigService configService = mock(SchoolProductConfigService.class);
ReflectionTestUtils.setField(controller, "schoolProductConfigService", configService);
ReflectionTestUtils.invokeMethod(controller, "requireAdministrativeOrganization", "school-zj");
verify(configService).requireAdminClassEnabled("school-zj");
}
}

@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -68,5 +69,33 @@ class TeacherRosterImportTest {
verify(controller.teachingClassStudentMapper).insertSelective(membership.capture());
assertEquals("teach-1", membership.getValue().getTeachingClassId());
assertEquals(null, membership.getValue().getAdminClassId());
verify(controller.teachingClassStudentMapper, never())
.exitActiveByStudentUserIdExceptClass(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString());
}
@Test
void teacherRosterImportReusesExistingStudentInTheSameSchool() {
UserController controller = new UserController();
controller.userinfoMapper = mock(UserinfoMapper.class);
controller.schoolClassMapper = mock(SchoolClassMapper.class);
controller.teachingClassStudentMapper = mock(TeachingClassStudentMapper.class);
controller.schoolProductConfigService = mock(SchoolProductConfigService.class);
Userinfo teacher = new Userinfo();
teacher.setUserId("teacher-1"); teacher.setRole(3); teacher.setSchoolId("school-zj");
Userinfo student = new Userinfo();
student.setUserId("student-1"); student.setRole(4); student.setSchoolId("school-zj"); student.setUsername("20260001"); student.setName("张三");
SchoolClass teachingClass = new SchoolClass();
teachingClass.setSchoolClassId("teach-2"); teachingClass.setSchoolId("school-zj"); teachingClass.setClassType("TEACHING"); teachingClass.setCreatedBy("teacher-1");
when(controller.userinfoMapper.selectByPrimaryKey("teacher-1")).thenReturn(teacher);
when(controller.schoolClassMapper.selectByPrimaryKey("teach-2")).thenReturn(teachingClass);
when(controller.schoolProductConfigService.isTeacherRosterManaged("school-zj")).thenReturn(true);
when(controller.userinfoMapper.selectBySchoolIdAndUsername("school-zj", "20260001")).thenReturn(student);
TeacherRosterStudentImportDTO row = new TeacherRosterStudentImportDTO(); row.setName("张三"); row.setUsername("20260001");
ResultEntity result = controller.importTeacherRosterRows(Collections.singletonList(row), "teach-2", "teacher-1");
assertEquals(HttpStatus.OK, result.getStatusCode());
verify(controller.userinfoMapper, never()).insertSelective(any(Userinfo.class));
verify(controller.teachingClassStudentMapper).insertSelective(any(TeachingClassStudent.class));
}
}

@ -697,7 +697,7 @@ class UserControllerTeacherAdminTest {
}
@Test
void importTeachingClassStudentRowsTransfersStudentFromOtherTeachingClass() {
void importTeachingClassStudentRowsKeepsStudentInOtherTeachingClasses() {
UserController controller = controllerWithCommonMocks();
SchoolClass teachingClass = classCreatedBy("teacher-1");
teachingClass.setSchoolClassId("teaching-1");
@ -712,7 +712,8 @@ class UserControllerTeacherAdminTest {
ResultEntity result = controller.importTeachingClassStudentRows(Collections.singletonList(row), "teaching-1", "teacher-1");
assertEquals(HttpStatus.OK, result.getStatusCode());
verify(controller.teachingClassStudentMapper).exitActiveByStudentUserIdExceptClass("student-1", "teaching-1");
verify(controller.teachingClassStudentMapper, never())
.exitActiveByStudentUserIdExceptClass(any(), any());
verify(controller.teachingClassStudentMapper).insertSelective(any(TeachingClassStudent.class));
}
@ -789,7 +790,7 @@ class UserControllerTeacherAdminTest {
}
@Test
void createTeachingClassByImportRowsTransfersStudentFromOtherTeachingClass() {
void createTeachingClassByImportRowsKeepsStudentInOtherTeachingClasses() {
UserController controller = controllerWithCommonMocks();
Userinfo student = student("student-1", "student-a", "2024001", "admin-1");
when(controller.schoolClassMapper.selectByExample(any())).thenReturn(Collections.emptyList());
@ -806,7 +807,8 @@ class UserControllerTeacherAdminTest {
assertEquals(HttpStatus.OK, result.getStatusCode());
verify(controller.schoolClassMapper).insert(any(SchoolClass.class));
verify(controller.teachingClassStudentMapper).exitActiveByStudentUserIdExceptClass(argThat(id -> "student-1".equals(id)), any());
verify(controller.teachingClassStudentMapper, never())
.exitActiveByStudentUserIdExceptClass(any(), any());
verify(controller.teachingClassStudentMapper).insertSelective(any(TeachingClassStudent.class));
}

@ -2,7 +2,9 @@ package com.sztzjy.linkCommerce.service.impl;
import com.sztzjy.linkCommerce.config.exception.handler.ServiceException;
import com.sztzjy.linkCommerce.entity.SchoolProductConfig;
import com.sztzjy.linkCommerce.mapper.SchoolClassMapper;
import com.sztzjy.linkCommerce.mapper.SchoolProductConfigMapper;
import com.sztzjy.linkCommerce.mapper.UserinfoMapper;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
@ -67,6 +69,20 @@ class SchoolProductConfigServiceImplTest {
&& "SCHOOL_ADMIN".equals(config.getStudentRosterOwner())));
}
@Test
void updateRejectsOrganizationModeChangeWhenAdministrativeDataExists() {
SchoolProductConfigServiceImpl service = new SchoolProductConfigServiceImpl();
service.schoolProductConfigMapper = mock(SchoolProductConfigMapper.class);
service.schoolClassMapper = mock(SchoolClassMapper.class);
service.userinfoMapper = mock(UserinfoMapper.class);
SchoolProductConfig current = config("school-standard", "ADMIN_CLASS_ENABLED", "SCHOOL_ADMIN");
when(service.schoolProductConfigMapper.selectByPrimaryKey("school-standard")).thenReturn(current);
when(service.schoolClassMapper.countByExample(org.mockito.ArgumentMatchers.any())).thenReturn(1L);
assertThrows(ServiceException.class, () -> service.updateConfig(
config("school-standard", "TEACHING_CLASS_ONLY", "TEACHER")));
}
private SchoolProductConfig config(String schoolId, String organizationMode, String rosterOwner) {
SchoolProductConfig config = new SchoolProductConfig();
config.setSchoolId(schoolId);

Loading…
Cancel
Save