新增权限:实现管理员账户绑定流程
This commit is contained in:
1 parent
d8a963f611
commit
d00d039ef5
27 files changed
+1946
-26
No files matched your search
+120
@@ -0,0 +1,120 @@
|
||||
package com.ruoyi.web.controller.train;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.train.domain.dto.UserBindingDTO;
|
||||
import com.ruoyi.train.domain.vo.BindableUserVO;
|
||||
import com.ruoyi.web.service.train.TrainAccountBindingAdminService;
|
||||
|
||||
/**
|
||||
* 管理员端业务身份系统账号关联 Controller。
|
||||
*
|
||||
* <p>为管理员提供受控的系统账号与教师/学员业务档案关联与解绑功能,
|
||||
* 满足 Wave 2.1 / Wave 3 真实三角色生命周期管理流。</p>
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping
|
||||
public class TrainAccountBindingAdminController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private TrainAccountBindingAdminService bindingAdminService;
|
||||
|
||||
// ==========================================
|
||||
// 教师业务档案账号绑定
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* 查询可关联教师身份的系统用户列表
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:teacher:bindUser')")
|
||||
@GetMapping({"/train/teacher/bindable-users", "/train/teachers/bindable-users"})
|
||||
public AjaxResult getTeacherBindableUsers(@RequestParam(value = "keyword", required = false) String keyword)
|
||||
{
|
||||
List<BindableUserVO> users = bindingAdminService.selectBindableUsers("teacher", keyword);
|
||||
return success(users);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为教师关联系统用户
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:teacher:bindUser')")
|
||||
@Log(title = "教师账号关联", businessType = BusinessType.UPDATE)
|
||||
@RequestMapping(value = {"/train/teacher/{tId}/user-binding", "/train/teachers/{tId}/user-binding"},
|
||||
method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
public AjaxResult bindTeacherUser(@PathVariable("tId") String tId,
|
||||
@Validated @RequestBody UserBindingDTO dto)
|
||||
{
|
||||
bindingAdminService.bindTeacherAccount(tId, dto.getUserId());
|
||||
return success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 为教师解除系统用户关联
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:teacher:unbindUser')")
|
||||
@Log(title = "教师账号解绑", businessType = BusinessType.UPDATE)
|
||||
@DeleteMapping({"/train/teacher/{tId}/user-binding", "/train/teachers/{tId}/user-binding"})
|
||||
public AjaxResult unbindTeacherUser(@PathVariable("tId") String tId)
|
||||
{
|
||||
bindingAdminService.unbindTeacherAccount(tId);
|
||||
return success();
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 学员业务档案账号绑定
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* 查询可关联学员身份的系统用户列表
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:student:bindUser')")
|
||||
@GetMapping({"/train/student/bindable-users", "/train/students/bindable-users"})
|
||||
public AjaxResult getStudentBindableUsers(@RequestParam(value = "keyword", required = false) String keyword)
|
||||
{
|
||||
List<BindableUserVO> users = bindingAdminService.selectBindableUsers("student", keyword);
|
||||
return success(users);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为学员关联系统用户
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:student:bindUser')")
|
||||
@Log(title = "学员账号关联", businessType = BusinessType.UPDATE)
|
||||
@RequestMapping(value = {"/train/student/{sId}/user-binding", "/train/students/{sId}/user-binding"},
|
||||
method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
public AjaxResult bindStudentUser(@PathVariable("sId") String sId,
|
||||
@Validated @RequestBody UserBindingDTO dto)
|
||||
{
|
||||
bindingAdminService.bindStudentAccount(sId, dto.getUserId());
|
||||
return success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 为学员解除系统用户关联
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:student:unbindUser')")
|
||||
@Log(title = "学员账号解绑", businessType = BusinessType.UPDATE)
|
||||
@DeleteMapping({"/train/student/{sId}/user-binding", "/train/students/{sId}/user-binding"})
|
||||
public AjaxResult unbindStudentUser(@PathVariable("sId") String sId)
|
||||
{
|
||||
bindingAdminService.unbindStudentAccount(sId);
|
||||
return success();
|
||||
}
|
||||
}
|
||||
+31
-14
@@ -2,16 +2,6 @@ package com.ruoyi.web.controller.train;
|
||||
|
||||
import java.util.List;
|
||||
import javax.validation.Valid;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.train.domain.dto.StudentCreateDTO;
|
||||
import com.ruoyi.train.domain.query.StudentQuery;
|
||||
import com.ruoyi.train.domain.vo.StudentDetailVO;
|
||||
import com.ruoyi.train.domain.vo.StudentItemVO;
|
||||
import com.ruoyi.train.service.ITrainStudentService;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -19,17 +9,34 @@ 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;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.constant.HttpStatus;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.train.domain.dto.StudentCreateDTO;
|
||||
import com.ruoyi.train.domain.query.StudentQuery;
|
||||
import com.ruoyi.train.domain.vo.StudentAdminVO;
|
||||
import com.ruoyi.train.domain.vo.StudentDetailVO;
|
||||
import com.ruoyi.train.domain.vo.StudentItemVO;
|
||||
import com.ruoyi.train.service.ITrainStudentService;
|
||||
import com.ruoyi.web.service.train.TrainAccountBindingAdminService;
|
||||
|
||||
/** ISSUE-01-02 学员主档 P0 API。 */
|
||||
@RestController
|
||||
@RequestMapping("/train/student")
|
||||
@RequestMapping({"/train/student", "/train/students"})
|
||||
public class TrainStudentController extends BaseController
|
||||
{
|
||||
private final ITrainStudentService trainStudentService;
|
||||
private final TrainAccountBindingAdminService bindingAdminService;
|
||||
|
||||
public TrainStudentController(ITrainStudentService trainStudentService)
|
||||
public TrainStudentController(ITrainStudentService trainStudentService,
|
||||
TrainAccountBindingAdminService bindingAdminService)
|
||||
{
|
||||
this.trainStudentService = trainStudentService;
|
||||
this.bindingAdminService = bindingAdminService;
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('train:student:list')")
|
||||
@@ -38,7 +45,13 @@ public class TrainStudentController extends BaseController
|
||||
{
|
||||
startPage();
|
||||
List<StudentItemVO> list = trainStudentService.selectStudentList(query);
|
||||
return getDataTable(list);
|
||||
List<StudentAdminVO> adminList = bindingAdminService.toStudentAdminList(list);
|
||||
TableDataInfo rspData = new TableDataInfo();
|
||||
rspData.setCode(HttpStatus.SUCCESS);
|
||||
rspData.setMsg("查询成功");
|
||||
rspData.setRows(adminList);
|
||||
rspData.setTotal(new PageInfo(list).getTotal());
|
||||
return rspData;
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('train:student:query')")
|
||||
@@ -46,7 +59,11 @@ public class TrainStudentController extends BaseController
|
||||
public AjaxResult getInfo(@PathVariable("sId") String sId)
|
||||
{
|
||||
StudentDetailVO detail = trainStudentService.selectStudentDetail(sId);
|
||||
return success(detail);
|
||||
if (detail == null)
|
||||
{
|
||||
return success((Object) null);
|
||||
}
|
||||
return success(bindingAdminService.toStudentAdminVO(detail));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('train:student:add')")
|
||||
|
||||
+16
-3
@@ -11,7 +11,9 @@ import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.constant.HttpStatus;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.PageDomain;
|
||||
@@ -21,7 +23,9 @@ import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.train.domain.TrainTeacher;
|
||||
import com.ruoyi.train.domain.vo.TeacherAdminVO;
|
||||
import com.ruoyi.train.service.ITrainTeacherService;
|
||||
import com.ruoyi.web.service.train.TrainAccountBindingAdminService;
|
||||
|
||||
/**
|
||||
* 教师信息Controller
|
||||
@@ -30,7 +34,7 @@ import com.ruoyi.train.service.ITrainTeacherService;
|
||||
* @date 2026-09-14
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/train/teacher")
|
||||
@RequestMapping({"/train/teacher", "/train/teachers"})
|
||||
public class TrainTeacherController extends BaseController
|
||||
{
|
||||
private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
|
||||
@@ -38,6 +42,9 @@ public class TrainTeacherController extends BaseController
|
||||
@Autowired
|
||||
private ITrainTeacherService trainTeacherService;
|
||||
|
||||
@Autowired
|
||||
private TrainAccountBindingAdminService bindingAdminService;
|
||||
|
||||
/**
|
||||
* 查询教师信息列表 (GET /train/teacher/list)
|
||||
*/
|
||||
@@ -53,7 +60,13 @@ public class TrainTeacherController extends BaseController
|
||||
}
|
||||
startPage();
|
||||
List<TrainTeacher> list = trainTeacherService.selectTrainTeacherList(trainTeacher);
|
||||
return getDataTable(list);
|
||||
List<TeacherAdminVO> adminList = bindingAdminService.toTeacherAdminList(list);
|
||||
TableDataInfo rspData = new TableDataInfo();
|
||||
rspData.setCode(HttpStatus.SUCCESS);
|
||||
rspData.setMsg("查询成功");
|
||||
rspData.setRows(adminList);
|
||||
rspData.setTotal(new PageInfo(list).getTotal());
|
||||
return rspData;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,7 +81,7 @@ public class TrainTeacherController extends BaseController
|
||||
{
|
||||
return AjaxResult.error(40401, "教师不存在");
|
||||
}
|
||||
return success(teacher);
|
||||
return success(bindingAdminService.toTeacherAdminVO(teacher));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
@@ -79,6 +79,7 @@ public class TrainTeacherMeController extends BaseController
|
||||
{
|
||||
throw new ServiceException("未找到对应的教师档案数据", HttpStatus.NOT_FOUND);
|
||||
}
|
||||
teacher.setUserId(null);
|
||||
return success(teacher);
|
||||
}
|
||||
|
||||
|
||||
+460
@@ -0,0 +1,460 @@
|
||||
package com.ruoyi.web.service.train;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.common.core.domain.entity.SysRole;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.bean.BeanUtils;
|
||||
import com.ruoyi.system.service.ISysRoleService;
|
||||
import com.ruoyi.system.service.ISysUserService;
|
||||
import com.ruoyi.train.domain.TrainStudent;
|
||||
import com.ruoyi.train.domain.TrainTeacher;
|
||||
import com.ruoyi.train.domain.vo.BindableUserVO;
|
||||
import com.ruoyi.train.domain.vo.StudentAdminVO;
|
||||
import com.ruoyi.train.domain.vo.StudentDetailVO;
|
||||
import com.ruoyi.train.domain.vo.StudentItemVO;
|
||||
import com.ruoyi.train.domain.vo.TeacherAdminVO;
|
||||
import com.ruoyi.train.mapper.TrainStudentMapper;
|
||||
import com.ruoyi.train.mapper.TrainTeacherMapper;
|
||||
import com.ruoyi.train.service.ITrainingIdentityService;
|
||||
|
||||
/**
|
||||
* 管理员端业务身份账号绑定编排服务。
|
||||
*
|
||||
* <p>提供受控候选系统用户检索、角色校验、防静默覆盖保护,
|
||||
* 并复用底层 {@link ITrainingIdentityService} 执行跨表互斥与并发锁绑定。</p>
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Service
|
||||
public class TrainAccountBindingAdminService
|
||||
{
|
||||
private final ITrainingIdentityService identityService;
|
||||
private final ISysUserService userService;
|
||||
private final ISysRoleService roleService;
|
||||
private final TrainTeacherMapper teacherMapper;
|
||||
private final TrainStudentMapper studentMapper;
|
||||
|
||||
public TrainAccountBindingAdminService(ITrainingIdentityService identityService,
|
||||
ISysUserService userService,
|
||||
ISysRoleService roleService,
|
||||
TrainTeacherMapper teacherMapper,
|
||||
TrainStudentMapper studentMapper)
|
||||
{
|
||||
this.identityService = identityService;
|
||||
this.userService = userService;
|
||||
this.roleService = roleService;
|
||||
this.teacherMapper = teacherMapper;
|
||||
this.studentMapper = studentMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询候选可绑定系统用户列表 (最小受控字段)
|
||||
*
|
||||
* @param targetType 目标身份类型 ("TEACHER" / "STUDENT")
|
||||
* @param keyword 模糊搜索关键字 (匹配 userName 或 nickName)
|
||||
* @return 候选用户视图列表
|
||||
*/
|
||||
public List<BindableUserVO> selectBindableUsers(String targetType, String keyword)
|
||||
{
|
||||
String expectedRoleKey = "TEACHER".equalsIgnoreCase(targetType) ? "train_teacher" : "train_student";
|
||||
SysUser queryUser = new SysUser();
|
||||
queryUser.setDelFlag("0");
|
||||
if (StringUtils.isNotEmpty(keyword))
|
||||
{
|
||||
queryUser.setUserName(keyword.trim());
|
||||
}
|
||||
List<SysUser> userList = userService.selectUserList(queryUser);
|
||||
List<BindableUserVO> result = new ArrayList<>();
|
||||
|
||||
for (SysUser user : userList)
|
||||
{
|
||||
// 如果搜索关键字不匹配用户名也不匹配用户昵称,则跳过
|
||||
if (StringUtils.isNotEmpty(keyword))
|
||||
{
|
||||
String kw = keyword.trim().toLowerCase();
|
||||
boolean matchName = user.getUserName() != null && user.getUserName().toLowerCase().contains(kw);
|
||||
boolean matchNick = user.getNickName() != null && user.getNickName().toLowerCase().contains(kw);
|
||||
if (!matchName && !matchNick)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
BindableUserVO vo = new BindableUserVO();
|
||||
vo.setUserId(user.getUserId());
|
||||
vo.setUserName(user.getUserName());
|
||||
vo.setNickName(user.getNickName());
|
||||
vo.setStatus(user.getStatus());
|
||||
|
||||
List<SysRole> roles = roleService.selectRolesByUserId(user.getUserId());
|
||||
List<String> roleKeys = roles != null
|
||||
? roles.stream().map(SysRole::getRoleKey).collect(Collectors.toList())
|
||||
: new ArrayList<>();
|
||||
vo.setRoleKeys(roleKeys);
|
||||
|
||||
boolean hasTargetRole = roleKeys.contains(expectedRoleKey);
|
||||
vo.setHasRole(hasTargetRole);
|
||||
|
||||
// 检查当前用户是否已被绑定
|
||||
TrainTeacher boundTeacher = teacherMapper.selectTrainTeacherByUserId(user.getUserId());
|
||||
TrainStudent boundStudent = studentMapper.selectTrainStudentByUserId(user.getUserId());
|
||||
|
||||
boolean canBind = true;
|
||||
String reason = "";
|
||||
|
||||
if (!"0".equals(user.getStatus()))
|
||||
{
|
||||
canBind = false;
|
||||
reason = "账号已被停用";
|
||||
}
|
||||
else if (!hasTargetRole)
|
||||
{
|
||||
canBind = false;
|
||||
reason = "TEACHER".equalsIgnoreCase(targetType)
|
||||
? "未分配教师角色(train_teacher)"
|
||||
: "未分配学员角色(train_student)";
|
||||
}
|
||||
else if (boundTeacher != null)
|
||||
{
|
||||
vo.setBoundType("TEACHER");
|
||||
vo.setBoundBusinessId(boundTeacher.gettId());
|
||||
canBind = false;
|
||||
reason = "已被教师(" + boundTeacher.gettName() + " / " + boundTeacher.gettId() + ")绑定";
|
||||
}
|
||||
else if (boundStudent != null)
|
||||
{
|
||||
vo.setBoundType("STUDENT");
|
||||
vo.setBoundBusinessId(boundStudent.getsId());
|
||||
canBind = false;
|
||||
reason = "已被学员(" + boundStudent.getsName() + " / " + boundStudent.getsId() + ")绑定";
|
||||
}
|
||||
|
||||
vo.setCanBind(canBind);
|
||||
vo.setReason(reason);
|
||||
result.add(vo);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员为教师绑定系统用户 (防静默覆盖、严格校验角色与账号状态)
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void bindTeacherAccount(String tId, Long userId)
|
||||
{
|
||||
if (userId == null)
|
||||
{
|
||||
throw new ServiceException("绑定的系统用户ID不能为空");
|
||||
}
|
||||
TrainTeacher teacher = teacherMapper.selectTrainTeacherByTId(tId);
|
||||
if (teacher == null)
|
||||
{
|
||||
throw new ServiceException("教师业务档案不存在");
|
||||
}
|
||||
if (teacher.getUserId() != null)
|
||||
{
|
||||
if (teacher.getUserId().equals(userId))
|
||||
{
|
||||
return; // 幂等支持
|
||||
}
|
||||
throw new ServiceException("该教师档案已关联系统账号(userId=" + teacher.getUserId() + "),请先解除关联后再绑定新账号");
|
||||
}
|
||||
|
||||
// 严格校验目标用户有效性与 train_teacher 角色
|
||||
validateUserRole(userId, "train_teacher", "教师");
|
||||
|
||||
// 复用底层原子互斥绑定
|
||||
identityService.bindTeacherUser(tId, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员为教师解绑系统用户
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void unbindTeacherAccount(String tId)
|
||||
{
|
||||
TrainTeacher teacher = teacherMapper.selectTrainTeacherByTId(tId);
|
||||
if (teacher == null)
|
||||
{
|
||||
throw new ServiceException("教师业务档案不存在");
|
||||
}
|
||||
if (teacher.getUserId() == null)
|
||||
{
|
||||
return; // 幂等支持
|
||||
}
|
||||
identityService.unbindTeacherUser(tId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员为学员绑定系统用户 (防静默覆盖、严格校验角色与账号状态)
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void bindStudentAccount(String sId, Long userId)
|
||||
{
|
||||
if (userId == null)
|
||||
{
|
||||
throw new ServiceException("绑定的系统用户ID不能为空");
|
||||
}
|
||||
TrainStudent student = studentMapper.selectTrainStudentBySId(sId);
|
||||
if (student == null)
|
||||
{
|
||||
throw new ServiceException("学员业务档案不存在");
|
||||
}
|
||||
if (student.getUserId() != null)
|
||||
{
|
||||
if (student.getUserId().equals(userId))
|
||||
{
|
||||
return; // 幂等支持
|
||||
}
|
||||
throw new ServiceException("该学员档案已关联系统账号(userId=" + student.getUserId() + "),请先解除关联后再绑定新账号");
|
||||
}
|
||||
|
||||
// 严格校验目标用户有效性与 train_student 角色
|
||||
validateUserRole(userId, "train_student", "学员");
|
||||
|
||||
// 复用底层原子互斥绑定
|
||||
identityService.bindStudentUser(sId, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员为学员解绑系统用户
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void unbindStudentAccount(String sId)
|
||||
{
|
||||
TrainStudent student = studentMapper.selectTrainStudentBySId(sId);
|
||||
if (student == null)
|
||||
{
|
||||
throw new ServiceException("学员业务档案不存在");
|
||||
}
|
||||
if (student.getUserId() == null)
|
||||
{
|
||||
return; // 幂等支持
|
||||
}
|
||||
identityService.unbindStudentUser(sId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量为教师列表装配账号绑定管理视图 (单次批量查询 + 内存映射,杜绝 N+1)
|
||||
*
|
||||
* @param list 原始教师列表
|
||||
* @return 管理端教师视图列表
|
||||
*/
|
||||
public List<TeacherAdminVO> toTeacherAdminList(List<TrainTeacher> list)
|
||||
{
|
||||
if (list == null || list.isEmpty())
|
||||
{
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 1. 批量收集非空 userId
|
||||
Set<Long> userIds = list.stream()
|
||||
.map(TrainTeacher::getUserId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// 2. 单次批量查询系统用户并构建映射 Map
|
||||
Map<Long, SysUser> userMap;
|
||||
if (!userIds.isEmpty())
|
||||
{
|
||||
List<SysUser> users = userService.selectUserByUserIds(userIds);
|
||||
userMap = users.stream().collect(Collectors.toMap(SysUser::getUserId, u -> u, (k1, k2) -> k1));
|
||||
}
|
||||
else
|
||||
{
|
||||
userMap = Collections.emptyMap();
|
||||
}
|
||||
|
||||
// 3. 内存 O(1) 组装
|
||||
List<TeacherAdminVO> result = new ArrayList<>(list.size());
|
||||
for (TrainTeacher teacher : list)
|
||||
{
|
||||
TeacherAdminVO vo = new TeacherAdminVO();
|
||||
BeanUtils.copyBeanProp(vo, teacher);
|
||||
if (teacher.getUserId() != null)
|
||||
{
|
||||
vo.setAccountBindingStatus(1);
|
||||
SysUser user = userMap.get(teacher.getUserId());
|
||||
if (user != null)
|
||||
{
|
||||
vo.setUserName(user.getUserName());
|
||||
vo.setNickName(user.getNickName());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
vo.setAccountBindingStatus(0);
|
||||
}
|
||||
result.add(vo);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为单个教师对象装配账号绑定管理视图
|
||||
*
|
||||
* @param teacher 原始教师对象
|
||||
* @return 管理端教师视图对象
|
||||
*/
|
||||
public TeacherAdminVO toTeacherAdminVO(TrainTeacher teacher)
|
||||
{
|
||||
if (teacher == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
TeacherAdminVO vo = new TeacherAdminVO();
|
||||
BeanUtils.copyBeanProp(vo, teacher);
|
||||
if (teacher.getUserId() != null)
|
||||
{
|
||||
vo.setAccountBindingStatus(1);
|
||||
SysUser user = userService.selectUserById(teacher.getUserId());
|
||||
if (user != null)
|
||||
{
|
||||
vo.setUserName(user.getUserName());
|
||||
vo.setNickName(user.getNickName());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
vo.setAccountBindingStatus(0);
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量为学员列表装配账号绑定管理视图 (单次批量查询 + 内存映射,杜绝 N+1)
|
||||
*
|
||||
* @param list 原始学员列表
|
||||
* @return 管理端学员视图列表
|
||||
*/
|
||||
public List<StudentAdminVO> toStudentAdminList(List<StudentItemVO> list)
|
||||
{
|
||||
if (list == null || list.isEmpty())
|
||||
{
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 1. 批量收集 sId
|
||||
Set<String> sIds = list.stream()
|
||||
.map(StudentItemVO::getsId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// 2. 单次批量查询学员的关联 userId 映射
|
||||
Map<String, Long> studentUserMap;
|
||||
if (!sIds.isEmpty())
|
||||
{
|
||||
List<TrainStudent> students = studentMapper.selectTrainStudentsBySIds(sIds);
|
||||
studentUserMap = students.stream()
|
||||
.filter(s -> s.getUserId() != null)
|
||||
.collect(Collectors.toMap(TrainStudent::getsId, TrainStudent::getUserId, (k1, k2) -> k1));
|
||||
}
|
||||
else
|
||||
{
|
||||
studentUserMap = Collections.emptyMap();
|
||||
}
|
||||
|
||||
// 3. 批量查询关联的系统用户
|
||||
Set<Long> userIds = new HashSet<>(studentUserMap.values());
|
||||
Map<Long, SysUser> userMap;
|
||||
if (!userIds.isEmpty())
|
||||
{
|
||||
List<SysUser> users = userService.selectUserByUserIds(userIds);
|
||||
userMap = users.stream().collect(Collectors.toMap(SysUser::getUserId, u -> u, (k1, k2) -> k1));
|
||||
}
|
||||
else
|
||||
{
|
||||
userMap = Collections.emptyMap();
|
||||
}
|
||||
|
||||
// 4. 内存 O(1) 组装
|
||||
List<StudentAdminVO> result = new ArrayList<>(list.size());
|
||||
for (StudentItemVO item : list)
|
||||
{
|
||||
StudentAdminVO vo = new StudentAdminVO();
|
||||
BeanUtils.copyBeanProp(vo, item);
|
||||
Long uid = studentUserMap.get(item.getsId());
|
||||
if (uid != null)
|
||||
{
|
||||
vo.setUserId(uid);
|
||||
vo.setAccountBindingStatus(1);
|
||||
SysUser user = userMap.get(uid);
|
||||
if (user != null)
|
||||
{
|
||||
vo.setUserName(user.getUserName());
|
||||
vo.setNickName(user.getNickName());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
vo.setAccountBindingStatus(0);
|
||||
}
|
||||
result.add(vo);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为单个学员详情装配账号绑定管理视图
|
||||
*
|
||||
* @param detail 学员详情对象
|
||||
* @return 管理端学员视图对象
|
||||
*/
|
||||
public StudentAdminVO toStudentAdminVO(StudentDetailVO detail)
|
||||
{
|
||||
if (detail == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
StudentAdminVO vo = new StudentAdminVO();
|
||||
BeanUtils.copyBeanProp(vo, detail);
|
||||
TrainStudent student = studentMapper.selectTrainStudentBySId(detail.getsId());
|
||||
if (student != null && student.getUserId() != null)
|
||||
{
|
||||
vo.setUserId(student.getUserId());
|
||||
vo.setAccountBindingStatus(1);
|
||||
SysUser user = userService.selectUserById(student.getUserId());
|
||||
if (user != null)
|
||||
{
|
||||
vo.setUserName(user.getUserName());
|
||||
vo.setNickName(user.getNickName());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
vo.setAccountBindingStatus(0);
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
private void validateUserRole(Long userId, String requiredRoleKey, String roleLabel)
|
||||
{
|
||||
SysUser user = userService.selectUserById(userId);
|
||||
if (user == null || "2".equals(user.getDelFlag()))
|
||||
{
|
||||
throw new ServiceException("指定的系统用户不存在或已删除");
|
||||
}
|
||||
if (!"0".equals(user.getStatus()))
|
||||
{
|
||||
throw new ServiceException("指定的系统用户已被停用,禁止关联业务身份");
|
||||
}
|
||||
List<SysRole> roles = roleService.selectRolesByUserId(userId);
|
||||
boolean hasRole = roles != null && roles.stream().anyMatch(r -> requiredRoleKey.equals(r.getRoleKey()));
|
||||
if (!hasRole)
|
||||
{
|
||||
throw new ServiceException("所选系统账号(" + user.getUserName() + ")未分配" + roleLabel + "角色(" + requiredRoleKey + "),请先在用户管理中分配对应角色");
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -52,6 +52,10 @@ public class RbacPermissionMigrationContractTest
|
||||
"train:board:export",
|
||||
"train:me:teacher",
|
||||
"train:me:student",
|
||||
"train:teacher:binduser",
|
||||
"train:teacher:unbinduser",
|
||||
"train:student:binduser",
|
||||
"train:student:unbinduser",
|
||||
"insert into public.sys_role_menu");
|
||||
|
||||
// 严禁物理删除系统用户与无谓 DROP
|
||||
@@ -60,6 +64,31 @@ public class RbacPermissionMigrationContractTest
|
||||
assertFalse("must not execute truncate", sql.contains("truncate table "));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void accountBindingControllersAlignPermissions()
|
||||
{
|
||||
// 校验 TrainAccountBindingAdminController 上的教师与学员绑定解绑权限注解
|
||||
boolean teacherBindFound = false;
|
||||
boolean teacherUnbindFound = false;
|
||||
boolean studentBindFound = false;
|
||||
boolean studentUnbindFound = false;
|
||||
for (Method method : TrainAccountBindingAdminController.class.getDeclaredMethods())
|
||||
{
|
||||
if (method.isAnnotationPresent(PreAuthorize.class))
|
||||
{
|
||||
String perm = method.getAnnotation(PreAuthorize.class).value();
|
||||
if (perm.contains("train:teacher:bindUser")) teacherBindFound = true;
|
||||
if (perm.contains("train:teacher:unbindUser")) teacherUnbindFound = true;
|
||||
if (perm.contains("train:student:bindUser")) studentBindFound = true;
|
||||
if (perm.contains("train:student:unbindUser")) studentUnbindFound = true;
|
||||
}
|
||||
}
|
||||
assertTrue("TrainAccountBindingAdminController 必须配置 train:teacher:bindUser", teacherBindFound);
|
||||
assertTrue("TrainAccountBindingAdminController 必须配置 train:teacher:unbindUser", teacherUnbindFound);
|
||||
assertTrue("TrainAccountBindingAdminController 必须配置 train:student:bindUser", studentBindFound);
|
||||
assertTrue("TrainAccountBindingAdminController 必须配置 train:student:unbindUser", studentUnbindFound);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void electronicBoardControllerAlignsToTrainNamespace()
|
||||
{
|
||||
|
||||
+29
@@ -314,4 +314,33 @@ public class StudentSelfServiceApiTest
|
||||
assertEquals("当前登录账号未绑定任何学员档案,禁止访问学员个人培训中心", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStudentProfileZeroLeakageOfAdminBindingFields() throws Exception
|
||||
{
|
||||
AjaxResult result = controller.getStudentProfile();
|
||||
assertEquals(200, result.get("code"));
|
||||
Object data = result.get("data");
|
||||
assertTrue(data instanceof StudentDetailVO);
|
||||
|
||||
Class<?> clazz = data.getClass();
|
||||
while (clazz != null && clazz != Object.class)
|
||||
{
|
||||
for (java.lang.reflect.Field field : clazz.getDeclaredFields())
|
||||
{
|
||||
assertFalse("学员自服务视图绝不可暴露 userId", "userId".equals(field.getName()));
|
||||
assertFalse("学员自服务视图绝不可暴露 userName", "userName".equals(field.getName()));
|
||||
assertFalse("学员自服务视图绝不可暴露 nickName", "nickName".equals(field.getName()));
|
||||
assertFalse("学员自服务视图绝不可暴露 accountBindingStatus", "accountBindingStatus".equals(field.getName()));
|
||||
}
|
||||
clazz = clazz.getSuperclass();
|
||||
}
|
||||
|
||||
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||
String json = mapper.writeValueAsString(data);
|
||||
assertFalse("JSON不得包含 userId", json.contains("\"userId\""));
|
||||
assertFalse("JSON不得包含 userName", json.contains("\"userName\""));
|
||||
assertFalse("JSON不得包含 nickName", json.contains("\"nickName\""));
|
||||
assertFalse("JSON不得包含 accountBindingStatus", json.contains("\"accountBindingStatus\""));
|
||||
}
|
||||
}
|
||||
+26
@@ -277,4 +277,30 @@ public class TeacherSelfServiceApiTest
|
||||
assertEquals(1, list.size());
|
||||
assertEquals("S001", list.get(0).getStudentId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTeacherProfileZeroLeakageOfAdminBindingFields() throws Exception
|
||||
{
|
||||
AjaxResult result = controller.getTeacherProfile();
|
||||
assertEquals(200, result.get("code"));
|
||||
TrainTeacher teacher = (TrainTeacher) result.get("data");
|
||||
// 验证返回前 userId 已置 null
|
||||
assertEquals(null, teacher.getUserId());
|
||||
|
||||
// 验证反射中 TrainTeacher 绝不存在管理端字段
|
||||
for (java.lang.reflect.Field field : TrainTeacher.class.getDeclaredFields())
|
||||
{
|
||||
assertFalse("TrainTeacher 绝不可包含 userName", "userName".equals(field.getName()));
|
||||
assertFalse("TrainTeacher 绝不可包含 nickName", "nickName".equals(field.getName()));
|
||||
assertFalse("TrainTeacher 绝不可包含 accountBindingStatus", "accountBindingStatus".equals(field.getName()));
|
||||
}
|
||||
|
||||
// 验证 JSON 序列化零泄露
|
||||
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||
String json = mapper.writeValueAsString(teacher);
|
||||
assertFalse("教师自服务响应 JSON 绝不可包含 userId", json.contains("\"userId\""));
|
||||
assertFalse("教师自服务响应 JSON 绝不可包含 userName", json.contains("\"userName\""));
|
||||
assertFalse("教师自服务响应 JSON 绝不可包含 nickName", json.contains("\"nickName\""));
|
||||
assertFalse("教师自服务响应 JSON 绝不可包含 accountBindingStatus", json.contains("\"accountBindingStatus\""));
|
||||
}
|
||||
}
|
||||
+459
@@ -0,0 +1,459 @@
|
||||
package com.ruoyi.web.controller.train;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.domain.entity.SysRole;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.system.service.ISysRoleService;
|
||||
import com.ruoyi.system.service.ISysUserService;
|
||||
import com.ruoyi.train.domain.TrainStudent;
|
||||
import com.ruoyi.train.domain.TrainTeacher;
|
||||
import com.ruoyi.train.domain.dto.UserBindingDTO;
|
||||
import com.ruoyi.train.domain.vo.BindableUserVO;
|
||||
import com.ruoyi.train.domain.vo.StudentAdminVO;
|
||||
import com.ruoyi.train.domain.vo.StudentDetailVO;
|
||||
import com.ruoyi.train.domain.vo.StudentItemVO;
|
||||
import com.ruoyi.train.domain.vo.TeacherAdminVO;
|
||||
import com.ruoyi.train.mapper.TrainStudentMapper;
|
||||
import com.ruoyi.train.mapper.TrainTeacherMapper;
|
||||
import com.ruoyi.train.service.ITrainingIdentityService;
|
||||
import com.ruoyi.web.service.train.TrainAccountBindingAdminService;
|
||||
|
||||
/**
|
||||
* 管理员端账号绑定业务编排与 Controller 接口测试。
|
||||
*
|
||||
* <p>测试用例覆盖:
|
||||
* 1. 候选用户查询与状态校验(包含已停用、缺失角色、已被其他教师或学员绑定的拒绝理由)
|
||||
* 2. 教师绑定:正常绑定、角色不匹配拒绝、已被绑定防静默覆盖拒绝、同账号幂等
|
||||
* 3. 教师解绑:正常解绑、未绑定幂等解绑
|
||||
* 4. 学员绑定:正常绑定、角色不匹配拒绝、已被绑定防静默覆盖拒绝、同账号幂等
|
||||
* 5. 学员解绑:正常解绑、未绑定幂等解绑
|
||||
* 6. 列表信息装配 (enrich) 校验
|
||||
* </p>
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class TrainAccountBindingAdminTest
|
||||
{
|
||||
private TrainAccountBindingAdminService service;
|
||||
private TrainAccountBindingAdminController controller;
|
||||
|
||||
private Map<Long, SysUser> userStore = new HashMap<>();
|
||||
private Map<Long, List<SysRole>> roleStore = new HashMap<>();
|
||||
private Map<String, TrainTeacher> teacherStore = new HashMap<>();
|
||||
private Map<String, TrainStudent> studentStore = new HashMap<>();
|
||||
|
||||
private List<String> identityServiceCallLog = new ArrayList<>();
|
||||
|
||||
@Before
|
||||
public void setUp()
|
||||
{
|
||||
userStore.clear();
|
||||
roleStore.clear();
|
||||
teacherStore.clear();
|
||||
studentStore.clear();
|
||||
identityServiceCallLog.clear();
|
||||
|
||||
// 构造用户数据
|
||||
// 1. teacherUser (具备 train_teacher 角色, 正常)
|
||||
SysUser u1 = new SysUser();
|
||||
u1.setUserId(101L);
|
||||
u1.setUserName("teacher_zhang");
|
||||
u1.setNickName("张老师");
|
||||
u1.setStatus("0");
|
||||
u1.setDelFlag("0");
|
||||
userStore.put(101L, u1);
|
||||
|
||||
SysRole rTeacher = new SysRole();
|
||||
rTeacher.setRoleId(4L);
|
||||
rTeacher.setRoleKey("train_teacher");
|
||||
roleStore.put(101L, Collections.singletonList(rTeacher));
|
||||
|
||||
// 2. studentUser (具备 train_student 角色, 正常)
|
||||
SysUser u2 = new SysUser();
|
||||
u2.setUserId(102L);
|
||||
u2.setUserName("student_li");
|
||||
u2.setNickName("李学员");
|
||||
u2.setStatus("0");
|
||||
u2.setDelFlag("0");
|
||||
userStore.put(102L, u2);
|
||||
|
||||
SysRole rStudent = new SysRole();
|
||||
rStudent.setRoleId(5L);
|
||||
rStudent.setRoleKey("train_student");
|
||||
roleStore.put(102L, Collections.singletonList(rStudent));
|
||||
|
||||
// 3. disabledUser (具备角色但已停用)
|
||||
SysUser u3 = new SysUser();
|
||||
u3.setUserId(103L);
|
||||
u3.setUserName("disabled_user");
|
||||
u3.setNickName("已停用用户");
|
||||
u3.setStatus("1");
|
||||
u3.setDelFlag("0");
|
||||
userStore.put(103L, u3);
|
||||
roleStore.put(103L, Collections.singletonList(rTeacher));
|
||||
|
||||
// 4. normalUser (无培训角色)
|
||||
SysUser u4 = new SysUser();
|
||||
u4.setUserId(104L);
|
||||
u4.setUserName("common_user");
|
||||
u4.setNickName("普通用户");
|
||||
u4.setStatus("0");
|
||||
u4.setDelFlag("0");
|
||||
userStore.put(104L, u4);
|
||||
SysRole rCommon = new SysRole();
|
||||
rCommon.setRoleId(2L);
|
||||
rCommon.setRoleKey("common");
|
||||
roleStore.put(104L, Collections.singletonList(rCommon));
|
||||
|
||||
// 教师业务档案
|
||||
TrainTeacher t1 = new TrainTeacher();
|
||||
t1.settId("T1001");
|
||||
t1.settName("张老师档案");
|
||||
t1.setStatus(1);
|
||||
t1.setDelFlag("0");
|
||||
teacherStore.put("T1001", t1);
|
||||
|
||||
// 学员业务档案
|
||||
TrainStudent s1 = new TrainStudent();
|
||||
s1.setsId("S2001");
|
||||
s1.setsName("李学员档案");
|
||||
s1.setDelFlag("0");
|
||||
studentStore.put("S2001", s1);
|
||||
|
||||
// Dynamic Proxy for ISysUserService
|
||||
ISysUserService userService = (ISysUserService) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{ISysUserService.class},
|
||||
(proxy, method, args) -> {
|
||||
if ("selectUserById".equals(method.getName())) {
|
||||
return userStore.get((Long) args[0]);
|
||||
} else if ("selectUserList".equals(method.getName())) {
|
||||
return new ArrayList<>(userStore.values());
|
||||
} else if ("selectUserByUserIds".equals(method.getName())) {
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.Collection<Long> uids = (java.util.Collection<Long>) args[0];
|
||||
List<SysUser> res = new ArrayList<>();
|
||||
for (Long id : uids) {
|
||||
SysUser u = userStore.get(id);
|
||||
if (u != null) res.add(u);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Dynamic Proxy for ISysRoleService
|
||||
ISysRoleService roleService = (ISysRoleService) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{ISysRoleService.class},
|
||||
(proxy, method, args) -> {
|
||||
if ("selectRolesByUserId".equals(method.getName())) {
|
||||
return roleStore.getOrDefault((Long) args[0], Collections.emptyList());
|
||||
}
|
||||
return Collections.emptyList();
|
||||
});
|
||||
|
||||
// Dynamic Proxy for TrainTeacherMapper
|
||||
TrainTeacherMapper teacherMapper = (TrainTeacherMapper) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{TrainTeacherMapper.class},
|
||||
(proxy, method, args) -> {
|
||||
if ("selectTrainTeacherByTId".equals(method.getName())) {
|
||||
return teacherStore.get((String) args[0]);
|
||||
} else if ("selectTrainTeacherByUserId".equals(method.getName())) {
|
||||
Long uid = (Long) args[0];
|
||||
return teacherStore.values().stream()
|
||||
.filter(t -> uid.equals(t.getUserId()))
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Dynamic Proxy for TrainStudentMapper
|
||||
TrainStudentMapper studentMapper = (TrainStudentMapper) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{TrainStudentMapper.class},
|
||||
(proxy, method, args) -> {
|
||||
if ("selectTrainStudentBySId".equals(method.getName())) {
|
||||
return studentStore.get((String) args[0]);
|
||||
} else if ("selectTrainStudentByUserId".equals(method.getName())) {
|
||||
Long uid = (Long) args[0];
|
||||
return studentStore.values().stream()
|
||||
.filter(s -> uid.equals(s.getUserId()))
|
||||
.findFirst().orElse(null);
|
||||
} else if ("selectTrainStudentsBySIds".equals(method.getName())) {
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.Collection<String> sIds = (java.util.Collection<String>) args[0];
|
||||
List<TrainStudent> res = new ArrayList<>();
|
||||
for (String sid : sIds) {
|
||||
TrainStudent s = studentStore.get(sid);
|
||||
if (s != null) res.add(s);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Dynamic Proxy for ITrainingIdentityService
|
||||
ITrainingIdentityService identityService = (ITrainingIdentityService) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{ITrainingIdentityService.class},
|
||||
(proxy, method, args) -> {
|
||||
identityServiceCallLog.add(method.getName() + ":" + args[0] + ":" + (args.length > 1 ? args[1] : ""));
|
||||
if ("bindTeacherUser".equals(method.getName())) {
|
||||
TrainTeacher teacher = teacherStore.get((String) args[0]);
|
||||
if (teacher != null) teacher.setUserId((Long) args[1]);
|
||||
} else if ("unbindTeacherUser".equals(method.getName())) {
|
||||
TrainTeacher teacher = teacherStore.get((String) args[0]);
|
||||
if (teacher != null) teacher.setUserId(null);
|
||||
} else if ("bindStudentUser".equals(method.getName())) {
|
||||
TrainStudent student = studentStore.get((String) args[0]);
|
||||
if (student != null) student.setUserId((Long) args[1]);
|
||||
} else if ("unbindStudentUser".equals(method.getName())) {
|
||||
TrainStudent student = studentStore.get((String) args[0]);
|
||||
if (student != null) student.setUserId(null);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
service = new TrainAccountBindingAdminService(identityService, userService, roleService, teacherMapper, studentMapper);
|
||||
controller = new TrainAccountBindingAdminController();
|
||||
try {
|
||||
java.lang.reflect.Field field = TrainAccountBindingAdminController.class.getDeclaredField("bindingAdminService");
|
||||
field.setAccessible(true);
|
||||
field.set(controller, service);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectBindableUsersEvaluatesRolesAndStatus()
|
||||
{
|
||||
List<BindableUserVO> teacherCandidates = service.selectBindableUsers("TEACHER", null);
|
||||
assertNotNull(teacherCandidates);
|
||||
assertFalse(teacherCandidates.isEmpty());
|
||||
|
||||
// teacher_zhang (101) 应该可以绑定
|
||||
BindableUserVO u101 = teacherCandidates.stream().filter(u -> u.getUserId().equals(101L)).findFirst().orElse(null);
|
||||
assertNotNull(u101);
|
||||
assertTrue("具备 train_teacher 角色的账号应当可绑定", u101.getCanBind());
|
||||
assertTrue(u101.getHasRole());
|
||||
|
||||
// student_li (102) 不可作为教师绑定
|
||||
BindableUserVO u102 = teacherCandidates.stream().filter(u -> u.getUserId().equals(102L)).findFirst().orElse(null);
|
||||
assertNotNull(u102);
|
||||
assertFalse("学员角色不可绑定教师", u102.getCanBind());
|
||||
assertTrue(u102.getReason().contains("未分配教师角色"));
|
||||
|
||||
// disabled_user (103) 停用状态不可绑定
|
||||
BindableUserVO u103 = teacherCandidates.stream().filter(u -> u.getUserId().equals(103L)).findFirst().orElse(null);
|
||||
assertNotNull(u103);
|
||||
assertFalse("已停用账号不可绑定", u103.getCanBind());
|
||||
assertTrue(u103.getReason().contains("停用"));
|
||||
|
||||
// common_user (104) 无培训角色不可绑定
|
||||
BindableUserVO u104 = teacherCandidates.stream().filter(u -> u.getUserId().equals(104L)).findFirst().orElse(null);
|
||||
assertNotNull(u104);
|
||||
assertFalse("普通用户无角色不可绑定", u104.getCanBind());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBindTeacherSuccessAndAntiSilentOverwrite()
|
||||
{
|
||||
// 1. 正常绑定
|
||||
service.bindTeacherAccount("T1001", 101L);
|
||||
assertEquals(Long.valueOf(101L), teacherStore.get("T1001").getUserId());
|
||||
assertTrue(identityServiceCallLog.contains("bindTeacherUser:T1001:101"));
|
||||
|
||||
// 2. 幂等重复绑定相同账号:直接成功且不抛异常
|
||||
service.bindTeacherAccount("T1001", 101L);
|
||||
|
||||
// 3. 静默覆盖保护:已有绑定时尝试绑定另一个账号应抛出 ServiceException
|
||||
try {
|
||||
service.bindTeacherAccount("T1001", 102L);
|
||||
fail("已有绑定时应拒绝直接覆盖");
|
||||
} catch (ServiceException se) {
|
||||
assertTrue(se.getMessage().contains("请先解除关联后再绑定新账号"));
|
||||
}
|
||||
|
||||
// 4. 解绑后可重新绑定
|
||||
service.unbindTeacherAccount("T1001");
|
||||
assertEquals(null, teacherStore.get("T1001").getUserId());
|
||||
|
||||
// 5. 再次解绑:幂等不报错
|
||||
service.unbindTeacherAccount("T1001");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBindTeacherRejectsMismatchedRole()
|
||||
{
|
||||
try {
|
||||
service.bindTeacherAccount("T1001", 102L); // 102 是学员
|
||||
fail("应当拒绝绑定未分配教师角色的系统账号");
|
||||
} catch (ServiceException se) {
|
||||
assertTrue(se.getMessage().contains("未分配教师角色"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBindStudentSuccessAndAntiSilentOverwrite()
|
||||
{
|
||||
// 1. 正常绑定
|
||||
service.bindStudentAccount("S2001", 102L);
|
||||
assertEquals(Long.valueOf(102L), studentStore.get("S2001").getUserId());
|
||||
assertTrue(identityServiceCallLog.contains("bindStudentUser:S2001:102"));
|
||||
|
||||
// 2. 幂等重复绑定相同账号
|
||||
service.bindStudentAccount("S2001", 102L);
|
||||
|
||||
// 3. 静默覆盖保护
|
||||
try {
|
||||
service.bindStudentAccount("S2001", 101L);
|
||||
fail("已有绑定时应拒绝直接覆盖");
|
||||
} catch (ServiceException se) {
|
||||
assertTrue(se.getMessage().contains("请先解除关联后再绑定新账号"));
|
||||
}
|
||||
|
||||
// 4. 解绑
|
||||
service.unbindStudentAccount("S2001");
|
||||
assertEquals(null, studentStore.get("S2001").getUserId());
|
||||
|
||||
// 5. 幂等解绑
|
||||
service.unbindStudentAccount("S2001");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBindStudentRejectsMismatchedRole()
|
||||
{
|
||||
try {
|
||||
service.bindStudentAccount("S2001", 101L); // 101 是教师
|
||||
fail("应当拒绝绑定未分配学员角色的系统账号");
|
||||
} catch (ServiceException se) {
|
||||
assertTrue(se.getMessage().contains("未分配学员角色"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControllerEndpointsDelegateToService()
|
||||
{
|
||||
// 测试 Controller 教师绑定调用
|
||||
UserBindingDTO dto = new UserBindingDTO();
|
||||
dto.setUserId(101L);
|
||||
AjaxResult res = controller.bindTeacherUser("T1001", dto);
|
||||
assertEquals(200, res.get("code"));
|
||||
assertEquals(Long.valueOf(101L), teacherStore.get("T1001").getUserId());
|
||||
|
||||
// 测试 Controller 教师候选人查询
|
||||
AjaxResult candidateRes = controller.getTeacherBindableUsers(null);
|
||||
assertEquals(200, candidateRes.get("code"));
|
||||
|
||||
// 测试 Controller 教师解绑调用
|
||||
AjaxResult unbindRes = controller.unbindTeacherUser("T1001");
|
||||
assertEquals(200, unbindRes.get("code"));
|
||||
assertEquals(null, teacherStore.get("T1001").getUserId());
|
||||
|
||||
// 测试 Controller 学员绑定调用
|
||||
UserBindingDTO sDto = new UserBindingDTO();
|
||||
sDto.setUserId(102L);
|
||||
AjaxResult sRes = controller.bindStudentUser("S2001", sDto);
|
||||
assertEquals(200, sRes.get("code"));
|
||||
assertEquals(Long.valueOf(102L), studentStore.get("S2001").getUserId());
|
||||
|
||||
// 测试 Controller 学员解绑调用
|
||||
AjaxResult sUnbindRes = controller.unbindStudentUser("S2001");
|
||||
assertEquals(200, sUnbindRes.get("code"));
|
||||
assertEquals(null, studentStore.get("S2001").getUserId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTeacherAndStudentAdminVOAssemblyAndZeroLeakage()
|
||||
{
|
||||
// 1. 教师列表批量装配与单体验证
|
||||
TrainTeacher t1 = new TrainTeacher();
|
||||
t1.settId("T1001");
|
||||
t1.setUserId(101L);
|
||||
t1.settName("张老师");
|
||||
|
||||
TrainTeacher tUnbound = new TrainTeacher();
|
||||
tUnbound.settId("T1002");
|
||||
tUnbound.settName("未绑定教师");
|
||||
|
||||
List<TrainTeacher> teacherList = new ArrayList<>();
|
||||
teacherList.add(t1);
|
||||
teacherList.add(tUnbound);
|
||||
|
||||
List<TeacherAdminVO> teacherAdminList = service.toTeacherAdminList(teacherList);
|
||||
assertEquals(2, teacherAdminList.size());
|
||||
|
||||
TeacherAdminVO vo1 = teacherAdminList.get(0);
|
||||
assertEquals(Integer.valueOf(1), vo1.getAccountBindingStatus());
|
||||
assertEquals("teacher_zhang", vo1.getUserName());
|
||||
assertEquals("张老师", vo1.getNickName());
|
||||
|
||||
TeacherAdminVO voUnbound = teacherAdminList.get(1);
|
||||
assertEquals(Integer.valueOf(0), voUnbound.getAccountBindingStatus());
|
||||
assertEquals(null, voUnbound.getUserName());
|
||||
|
||||
TeacherAdminVO singleTeacherVO = service.toTeacherAdminVO(t1);
|
||||
assertEquals(Integer.valueOf(1), singleTeacherVO.getAccountBindingStatus());
|
||||
assertEquals("teacher_zhang", singleTeacherVO.getUserName());
|
||||
|
||||
// 2. 学员列表批量装配与单体验证
|
||||
studentStore.get("S2001").setUserId(102L);
|
||||
|
||||
TrainStudent sUnboundStudent = new TrainStudent();
|
||||
sUnboundStudent.setsId("S2002");
|
||||
sUnboundStudent.setsName("未绑定学员");
|
||||
studentStore.put("S2002", sUnboundStudent);
|
||||
|
||||
StudentItemVO sItem1 = new StudentItemVO();
|
||||
sItem1.setsId("S2001");
|
||||
sItem1.setsName("李学员");
|
||||
|
||||
StudentItemVO sItemUnbound = new StudentItemVO();
|
||||
sItemUnbound.setsId("S2002");
|
||||
sItemUnbound.setsName("未绑定学员");
|
||||
|
||||
List<StudentItemVO> studentItemList = new ArrayList<>();
|
||||
studentItemList.add(sItem1);
|
||||
studentItemList.add(sItemUnbound);
|
||||
|
||||
List<StudentAdminVO> studentAdminList = service.toStudentAdminList(studentItemList);
|
||||
assertEquals(2, studentAdminList.size());
|
||||
|
||||
StudentAdminVO sAdmin1 = studentAdminList.get(0);
|
||||
assertEquals(Long.valueOf(102L), sAdmin1.getUserId());
|
||||
assertEquals(Integer.valueOf(1), sAdmin1.getAccountBindingStatus());
|
||||
assertEquals("student_li", sAdmin1.getUserName());
|
||||
assertEquals("李学员", sAdmin1.getNickName());
|
||||
|
||||
StudentAdminVO sAdminUnbound = studentAdminList.get(1);
|
||||
assertEquals(null, sAdminUnbound.getUserId());
|
||||
assertEquals(Integer.valueOf(0), sAdminUnbound.getAccountBindingStatus());
|
||||
assertEquals(null, sAdminUnbound.getUserName());
|
||||
|
||||
StudentDetailVO sDetail = new StudentDetailVO();
|
||||
sDetail.setsId("S2001");
|
||||
sDetail.setsName("李学员");
|
||||
StudentAdminVO singleStudentVO = service.toStudentAdminVO(sDetail);
|
||||
assertEquals(Long.valueOf(102L), singleStudentVO.getUserId());
|
||||
assertEquals(Integer.valueOf(1), singleStudentVO.getAccountBindingStatus());
|
||||
assertEquals("student_li", singleStudentVO.getUserName());
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,14 @@ public interface SysUserMapper
|
||||
*/
|
||||
public SysUser selectUserById(Long userId);
|
||||
|
||||
/**
|
||||
* 根据用户ID列表批量查询用户
|
||||
*
|
||||
* @param userIds 用户ID集合
|
||||
* @return 用户列表
|
||||
*/
|
||||
public List<SysUser> selectUserByUserIds(@Param("userIds") java.util.Collection<Long> userIds);
|
||||
|
||||
/**
|
||||
* 新增用户信息
|
||||
*
|
||||
|
||||
@@ -51,6 +51,14 @@ public interface ISysUserService
|
||||
*/
|
||||
public SysUser selectUserById(Long userId);
|
||||
|
||||
/**
|
||||
* 根据用户ID列表批量查询用户
|
||||
*
|
||||
* @param userIds 用户ID集合
|
||||
* @return 用户列表
|
||||
*/
|
||||
public List<SysUser> selectUserByUserIds(java.util.Collection<Long> userIds);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询用户所属角色组
|
||||
*
|
||||
|
||||
+16
@@ -129,6 +129,22 @@ public class SysUserServiceImpl implements ISysUserService
|
||||
return userMapper.selectUserById(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID列表批量查询用户
|
||||
*
|
||||
* @param userIds 用户ID集合
|
||||
* @return 用户列表
|
||||
*/
|
||||
@Override
|
||||
public List<SysUser> selectUserByUserIds(java.util.Collection<Long> userIds)
|
||||
{
|
||||
if (userIds == null || userIds.isEmpty())
|
||||
{
|
||||
return java.util.Collections.emptyList();
|
||||
}
|
||||
return userMapper.selectUserByUserIds(userIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户所属角色组
|
||||
*
|
||||
|
||||
@@ -130,6 +130,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<include refid="selectUserVo"/>
|
||||
where u.user_id = #{userId}
|
||||
</select>
|
||||
|
||||
<select id="selectUserByUserIds" resultMap="SysUserResult">
|
||||
select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.phonenumber, u.status, u.del_flag
|
||||
from public.sys_user u
|
||||
where u.del_flag = '0' and u.user_id in
|
||||
<foreach collection="userIds" item="userId" open="(" separator="," close=")">
|
||||
#{userId}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
<select id="checkUserNameUnique" parameterType="String" resultMap="SysUserResult">
|
||||
select user_id, user_name from public.sys_user where user_name = #{userName} and del_flag = '0' limit 1
|
||||
|
||||
@@ -34,6 +34,7 @@ public class TrainTeacher extends BaseEntity
|
||||
private Date tBirth;
|
||||
|
||||
/** sys_user登录账号逻辑关联ID */
|
||||
@com.fasterxml.jackson.annotation.JsonInclude(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL)
|
||||
private Long userId;
|
||||
|
||||
/** 所属单位/部门名称 (由旧 t_department 迁移) */
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.ruoyi.train.domain.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 业务实体关联系统账号请求 DTO。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class UserBindingDTO implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@NotNull(message = "系统用户ID不能为空")
|
||||
private Long userId;
|
||||
|
||||
public Long getUserId()
|
||||
{
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId)
|
||||
{
|
||||
this.userId = userId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.ruoyi.train.domain.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 候选系统账号视图对象 (最小受控字段,严禁泄露密码/Salt/安全信息)。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class BindableUserVO implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 系统用户ID */
|
||||
private Long userId;
|
||||
|
||||
/** 登录账号 */
|
||||
private String userName;
|
||||
|
||||
/** 用户昵称 */
|
||||
private String nickName;
|
||||
|
||||
/** 账号状态 (0:正常, 1:停用) */
|
||||
private String status;
|
||||
|
||||
/** 拥有角色键列表 */
|
||||
private List<String> roleKeys;
|
||||
|
||||
/** 是否具有目标业务角色 */
|
||||
private boolean hasRole;
|
||||
|
||||
/** 当前已绑定身份类型 (TEACHER / STUDENT / null) */
|
||||
private String boundType;
|
||||
|
||||
/** 当前已绑定的业务ID */
|
||||
private String boundBusinessId;
|
||||
|
||||
/** 是否可被当前业务实体绑定 */
|
||||
private boolean canBind;
|
||||
|
||||
/** 状态与不可绑定原因说明 */
|
||||
private String reason;
|
||||
|
||||
public Long getUserId()
|
||||
{
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId)
|
||||
{
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getUserName()
|
||||
{
|
||||
return userName;
|
||||
}
|
||||
|
||||
public void setUserName(String userName)
|
||||
{
|
||||
this.userName = userName;
|
||||
}
|
||||
|
||||
public String getNickName()
|
||||
{
|
||||
return nickName;
|
||||
}
|
||||
|
||||
public void setNickName(String nickName)
|
||||
{
|
||||
this.nickName = nickName;
|
||||
}
|
||||
|
||||
public String getStatus()
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status)
|
||||
{
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public List<String> getRoleKeys()
|
||||
{
|
||||
return roleKeys;
|
||||
}
|
||||
|
||||
public void setRoleKeys(List<String> roleKeys)
|
||||
{
|
||||
this.roleKeys = roleKeys;
|
||||
}
|
||||
|
||||
public boolean isHasRole()
|
||||
{
|
||||
return hasRole;
|
||||
}
|
||||
|
||||
public boolean getHasRole()
|
||||
{
|
||||
return hasRole;
|
||||
}
|
||||
|
||||
public void setHasRole(boolean hasRole)
|
||||
{
|
||||
this.hasRole = hasRole;
|
||||
}
|
||||
|
||||
public String getBoundType()
|
||||
{
|
||||
return boundType;
|
||||
}
|
||||
|
||||
public void setBoundType(String boundType)
|
||||
{
|
||||
this.boundType = boundType;
|
||||
}
|
||||
|
||||
public String getBoundBusinessId()
|
||||
{
|
||||
return boundBusinessId;
|
||||
}
|
||||
|
||||
public void setBoundBusinessId(String boundBusinessId)
|
||||
{
|
||||
this.boundBusinessId = boundBusinessId;
|
||||
}
|
||||
|
||||
public boolean isCanBind()
|
||||
{
|
||||
return canBind;
|
||||
}
|
||||
|
||||
public boolean getCanBind()
|
||||
{
|
||||
return canBind;
|
||||
}
|
||||
|
||||
public void setCanBind(boolean canBind)
|
||||
{
|
||||
this.canBind = canBind;
|
||||
}
|
||||
|
||||
public String getReason()
|
||||
{
|
||||
return reason;
|
||||
}
|
||||
|
||||
public void setReason(String reason)
|
||||
{
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.ruoyi.train.domain.vo;
|
||||
|
||||
/**
|
||||
* 管理端学员信息视图对象 (包含系统账号绑定扩展信息,严禁暴露至自服务接口)。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class StudentAdminVO extends StudentDetailVO
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 绑定的系统用户ID */
|
||||
private Long userId;
|
||||
|
||||
/** 绑定的系统登录账号 */
|
||||
private String userName;
|
||||
|
||||
/** 绑定的用户昵称 */
|
||||
private String nickName;
|
||||
|
||||
/** 账号关联状态 (0: 未关联, 1: 已关联) */
|
||||
private Integer accountBindingStatus;
|
||||
|
||||
public Long getUserId()
|
||||
{
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId)
|
||||
{
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getUserName()
|
||||
{
|
||||
return userName;
|
||||
}
|
||||
|
||||
public void setUserName(String userName)
|
||||
{
|
||||
this.userName = userName;
|
||||
}
|
||||
|
||||
public String getNickName()
|
||||
{
|
||||
return nickName;
|
||||
}
|
||||
|
||||
public void setNickName(String nickName)
|
||||
{
|
||||
this.nickName = nickName;
|
||||
}
|
||||
|
||||
public Integer getAccountBindingStatus()
|
||||
{
|
||||
return accountBindingStatus;
|
||||
}
|
||||
|
||||
public void setAccountBindingStatus(Integer accountBindingStatus)
|
||||
{
|
||||
this.accountBindingStatus = accountBindingStatus;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.ruoyi.train.domain.vo;
|
||||
|
||||
import com.ruoyi.train.domain.TrainTeacher;
|
||||
|
||||
/**
|
||||
* 管理端教师信息视图对象 (包含账号绑定扩展信息,严禁暴露至学员/教师自服务接口)。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class TeacherAdminVO extends TrainTeacher
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 绑定的系统登录账号 (仅管理端展示) */
|
||||
private String userName;
|
||||
|
||||
/** 绑定的用户昵称 (仅管理端展示) */
|
||||
private String nickName;
|
||||
|
||||
/** 账号关联状态 (0: 未关联, 1: 已关联) */
|
||||
private Integer accountBindingStatus;
|
||||
|
||||
public String getUserName()
|
||||
{
|
||||
return userName;
|
||||
}
|
||||
|
||||
public void setUserName(String userName)
|
||||
{
|
||||
this.userName = userName;
|
||||
}
|
||||
|
||||
public String getNickName()
|
||||
{
|
||||
return nickName;
|
||||
}
|
||||
|
||||
public void setNickName(String nickName)
|
||||
{
|
||||
this.nickName = nickName;
|
||||
}
|
||||
|
||||
public Integer getAccountBindingStatus()
|
||||
{
|
||||
return accountBindingStatus;
|
||||
}
|
||||
|
||||
public void setAccountBindingStatus(Integer accountBindingStatus)
|
||||
{
|
||||
this.accountBindingStatus = accountBindingStatus;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ public interface TrainStudentMapper
|
||||
{
|
||||
TrainStudent selectTrainStudentBySId(String sId);
|
||||
|
||||
List<TrainStudent> selectTrainStudentsBySIds(@org.apache.ibatis.annotations.Param("sIds") java.util.Collection<String> sIds);
|
||||
|
||||
List<TrainStudent> selectTrainStudentList(StudentQuery query);
|
||||
|
||||
/** 使用冻结的 id_card_hash 做精确防重;存在时返回 1,不存在返回 null。 */
|
||||
|
||||
@@ -61,6 +61,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where s_id = #{sId} and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="selectTrainStudentsBySIds" resultMap="TrainStudentResult">
|
||||
<include refid="selectTrainStudentDomain" />
|
||||
where del_flag = '0' and s_id in
|
||||
<foreach collection="sIds" item="id" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
<select id="selectOneByIdCardHash" parameterType="String" resultType="java.lang.Integer">
|
||||
select 1 from train_student where id_card_hash = #{idCardHash} limit 1
|
||||
</select>
|
||||
|
||||
+18
@@ -941,6 +941,24 @@ public class EnrollmentAppServiceImplTest
|
||||
return studentStore.get(sId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TrainStudent> selectTrainStudentsBySIds(java.util.Collection<String> sIds)
|
||||
{
|
||||
List<TrainStudent> list = new ArrayList<>();
|
||||
if (sIds != null)
|
||||
{
|
||||
for (String sId : sIds)
|
||||
{
|
||||
TrainStudent s = studentStore.get(sId);
|
||||
if (s != null)
|
||||
{
|
||||
list.add(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TrainStudent> selectTrainStudentList(StudentQuery query)
|
||||
{
|
||||
|
||||
+18
@@ -258,6 +258,24 @@ public class TrainStudentServiceImplTest
|
||||
return store.get(sId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TrainStudent> selectTrainStudentsBySIds(java.util.Collection<String> sIds)
|
||||
{
|
||||
List<TrainStudent> list = new ArrayList<>();
|
||||
if (sIds != null)
|
||||
{
|
||||
for (String sId : sIds)
|
||||
{
|
||||
TrainStudent s = store.get(sId);
|
||||
if (s != null)
|
||||
{
|
||||
list.add(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TrainStudent> selectTrainStudentList(StudentQuery query)
|
||||
{
|
||||
|
||||
@@ -40,13 +40,13 @@ BEGIN
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
-- 0.3 检查 menu_id 2201 ~ 2240 的碰撞:若ID已存在,必须属于预期本系统权限,严禁覆盖外来资源
|
||||
-- 0.3 检查 menu_id 2201 ~ 2244 的碰撞:若ID已存在,必须属于预期本系统权限,严禁覆盖外来资源
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM public.sys_menu
|
||||
WHERE menu_id BETWEEN 2201 AND 2240
|
||||
WHERE menu_id BETWEEN 2201 AND 2244
|
||||
AND (perms IS NULL OR perms NOT LIKE 'train:%')
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Collision detected: sys_menu.menu_id in 2201..2240 is occupied by non-train permissions';
|
||||
RAISE EXCEPTION 'Collision detected: sys_menu.menu_id in 2201..2244 is occupied by non-train permissions';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
@@ -104,6 +104,14 @@ INSERT INTO public.sys_menu (menu_id, menu_name, parent_id, order_num, path, com
|
||||
SELECT 2204, '教师修改', 2155, 4, '#', '', '', 1, 0, 'F', '0', '0', 'train:teacher:edit', '#', 'admin', CURRENT_TIMESTAMP, '教师修改'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2204 OR perms = 'train:teacher:edit');
|
||||
|
||||
INSERT INTO public.sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2241, '教师账号关联', 2155, 5, '#', '', '', 1, 0, 'F', '0', '0', 'train:teacher:bindUser', '#', 'admin', CURRENT_TIMESTAMP, '教师系统账号关联'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2241 OR perms = 'train:teacher:bindUser');
|
||||
|
||||
INSERT INTO public.sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2242, '教师账号解绑', 2155, 6, '#', '', '', 1, 0, 'F', '0', '0', 'train:teacher:unbindUser', '#', 'admin', CURRENT_TIMESTAMP, '教师系统账号解绑'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2242 OR perms = 'train:teacher:unbindUser');
|
||||
|
||||
-- 3.2 学员管理 (parent: 2156)
|
||||
INSERT INTO public.sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2205, '学员查询', 2156, 1, '#', '', '', 1, 0, 'F', '0', '0', 'train:student:list', '#', 'admin', CURRENT_TIMESTAMP, '学员列表查询'
|
||||
@@ -117,6 +125,14 @@ INSERT INTO public.sys_menu (menu_id, menu_name, parent_id, order_num, path, com
|
||||
SELECT 2207, '学员新增', 2156, 3, '#', '', '', 1, 0, 'F', '0', '0', 'train:student:add', '#', 'admin', CURRENT_TIMESTAMP, '学员新增'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2207 OR perms = 'train:student:add');
|
||||
|
||||
INSERT INTO public.sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2243, '学员账号关联', 2156, 4, '#', '', '', 1, 0, 'F', '0', '0', 'train:student:bindUser', '#', 'admin', CURRENT_TIMESTAMP, '学员系统账号关联'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2243 OR perms = 'train:student:bindUser');
|
||||
|
||||
INSERT INTO public.sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2244, '学员账号解绑', 2156, 5, '#', '', '', 1, 0, 'F', '0', '0', 'train:student:unbindUser', '#', 'admin', CURRENT_TIMESTAMP, '学员系统账号解绑'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2244 OR perms = 'train:student:unbindUser');
|
||||
|
||||
-- 3.3 教室管理 (parent: 2157)
|
||||
INSERT INTO public.sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2208, '教室查询', 2157, 1, '#', '', '', 1, 0, 'F', '0', '0', 'train:classroom:list', '#', 'admin', CURRENT_TIMESTAMP, '教室列表查询'
|
||||
@@ -266,7 +282,7 @@ WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2240 OR perms =
|
||||
INSERT INTO public.sys_role_menu (role_id, menu_id)
|
||||
SELECT 3, m.menu_id
|
||||
FROM public.sys_menu m
|
||||
WHERE (m.menu_id BETWEEN 2154 AND 2183 OR m.menu_id BETWEEN 2201 AND 2238)
|
||||
WHERE (m.menu_id BETWEEN 2154 AND 2183 OR m.menu_id BETWEEN 2201 AND 2238 OR m.menu_id BETWEEN 2241 AND 2244)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM public.sys_role_menu rm WHERE rm.role_id = 3 AND rm.menu_id = m.menu_id
|
||||
);
|
||||
@@ -302,6 +318,14 @@ BEGIN
|
||||
RAISE EXCEPTION 'Student self-service permission missing';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE perms = 'train:teacher:bindUser') THEN
|
||||
RAISE EXCEPTION 'Teacher binding permission missing';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE perms = 'train:student:bindUser') THEN
|
||||
RAISE EXCEPTION 'Student binding permission missing';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM public.sys_menu WHERE perms LIKE 'system:board:%') THEN
|
||||
RAISE EXCEPTION 'Legacy system:board:* permission namespace still exists';
|
||||
END IF;
|
||||
|
||||
@@ -25,3 +25,29 @@ export function addStudent(data) {
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 查询可关联学员的系统用户候选列表
|
||||
export function getStudentBindableUsers(keyword) {
|
||||
return request({
|
||||
url: '/train/student/bindable-users',
|
||||
method: 'get',
|
||||
params: { keyword }
|
||||
})
|
||||
}
|
||||
|
||||
// 为学员关联系统用户账号
|
||||
export function bindStudentUser(sId, userId) {
|
||||
return request({
|
||||
url: '/train/student/' + sId + '/user-binding',
|
||||
method: 'put',
|
||||
data: { userId }
|
||||
})
|
||||
}
|
||||
|
||||
// 为学员解除系统用户账号关联
|
||||
export function unbindStudentUser(sId) {
|
||||
return request({
|
||||
url: '/train/student/' + sId + '/user-binding',
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@@ -34,3 +34,29 @@ export function updateTeacher(data) {
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 查询可关联教师的系统用户候选列表
|
||||
export function getTeacherBindableUsers(keyword) {
|
||||
return request({
|
||||
url: '/train/teacher/bindable-users',
|
||||
method: 'get',
|
||||
params: { keyword }
|
||||
})
|
||||
}
|
||||
|
||||
// 为教师关联系统用户账号
|
||||
export function bindTeacherUser(tId, userId) {
|
||||
return request({
|
||||
url: '/train/teacher/' + tId + '/user-binding',
|
||||
method: 'put',
|
||||
data: { userId }
|
||||
})
|
||||
}
|
||||
|
||||
// 为教师解除系统用户账号关联
|
||||
export function unbindTeacherUser(tId) {
|
||||
return request({
|
||||
url: '/train/teacher/' + tId + '/user-binding',
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@@ -82,7 +82,19 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="90">
|
||||
<el-table-column label="关联系统账号" align="center" min-width="150">
|
||||
<template slot-scope="scope">
|
||||
<div v-if="scope.row.accountBindingStatus === 1 || scope.row.userName">
|
||||
<el-tag type="success" size="mini">已关联</el-tag>
|
||||
<div style="font-size: 12px; color: #606266; margin-top: 2px;">
|
||||
{{ scope.row.userName }}
|
||||
<span v-if="scope.row.nickName">({{ scope.row.nickName }})</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-tag v-else type="info" size="mini">未关联</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-hasPermi="['train:student:query']"
|
||||
@@ -91,6 +103,23 @@
|
||||
icon="el-icon-view"
|
||||
@click="handleDetail(scope.row)"
|
||||
>详情</el-button>
|
||||
<el-button
|
||||
v-if="!scope.row.userName && !scope.row.userId"
|
||||
v-hasPermi="['train:student:bindUser']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-link"
|
||||
@click="handleOpenBinding(scope.row)"
|
||||
>关联账号</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
v-hasPermi="['train:student:unbindUser']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-circle-close"
|
||||
style="color: #F56C6C;"
|
||||
@click="handleUnbind(scope.row)"
|
||||
>解绑账号</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -155,9 +184,13 @@
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog title="学员主档详情" :visible.sync="detailOpen" width="560px" append-to-body>
|
||||
<el-form :model="detail" label-width="90px" class="student-detail">
|
||||
<el-form :model="detail" label-width="110px" class="student-detail">
|
||||
<el-form-item label="学员姓名"><span>{{ detail.sName || '-' }}</span></el-form-item>
|
||||
<el-form-item label="系统编号"><span>{{ detail.sId || '-' }}</span></el-form-item>
|
||||
<el-form-item label="关联系统账号">
|
||||
<span v-if="detail.userName">{{ detail.userName }} <span v-if="detail.nickName">({{ detail.nickName }})</span></span>
|
||||
<el-tag v-else type="info" size="mini">未关联</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属单位"><span>{{ detail.orgName || '-' }}</span></el-form-item>
|
||||
<el-form-item label="联系电话"><span>{{ detail.sPhone || '-' }}</span></el-form-item>
|
||||
<el-form-item label="邮箱"><span>{{ detail.sEmail || '-' }}</span></el-form-item>
|
||||
@@ -166,11 +199,71 @@
|
||||
<el-form-item label="备注"><span>{{ detail.remark || '-' }}</span></el-form-item>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 关联系统账号对话框 -->
|
||||
<el-dialog title="关联系统账号 (学员业务档案)" :visible.sync="bindingOpen" width="700px" append-to-body>
|
||||
<el-alert
|
||||
title="关联说明"
|
||||
type="info"
|
||||
description="将学员业务档案与系统用户账号关联后,学员可凭该系统账号登录并使用“学员中心”。系统仅允许选择正常启用且已分配学员角色(train_student)的账号。"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-bottom: 15px;"
|
||||
/>
|
||||
<div style="margin-bottom: 15px; font-weight: bold; color: #303133;">
|
||||
当前学员:{{ currentStudent.sName }} (学员编号: {{ currentStudent.sId }})
|
||||
</div>
|
||||
<el-form size="small" :inline="true" @submit.native.prevent>
|
||||
<el-form-item label="查找用户">
|
||||
<el-input
|
||||
v-model="bindingKeyword"
|
||||
placeholder="请输入用户名或姓名搜索"
|
||||
clearable
|
||||
style="width: 240px;"
|
||||
@keyup.enter.native="loadBindableUsers"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="loadBindableUsers">搜索</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="bindingLoading" :data="bindableUsers" max-height="300" highlight-current-row @current-change="handleSelectedUserChange">
|
||||
<el-table-column width="55" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-radio v-model="selectedUserId" :label="scope.row.userId" :disabled="!scope.row.canBind">{{ '' }}</el-radio>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="系统账号" prop="userName" min-width="110" />
|
||||
<el-table-column label="姓名/昵称" prop="nickName" min-width="110" />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.status === '0' ? 'success' : 'danger'" size="mini">
|
||||
{{ scope.row.status === '0' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分配角色" min-width="120" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span>{{ (scope.row.roleKeys || []).join(', ') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="可否关联" min-width="150">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.canBind" type="success" size="mini">可选</el-tag>
|
||||
<span v-else style="color: #F56C6C; font-size: 12px;">{{ scope.row.reason }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" :disabled="!selectedUserId" :loading="bindingSubmitting" @click="submitBinding">确 定 关 联</el-button>
|
||||
<el-button @click="bindingOpen = false">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addStudent, getStudent, listStudent } from '@/api/train/student'
|
||||
import { addStudent, getStudent, listStudent, getStudentBindableUsers, bindStudentUser, unbindStudentUser } from '@/api/train/student'
|
||||
|
||||
export default {
|
||||
name: 'Student',
|
||||
@@ -195,6 +288,14 @@ export default {
|
||||
createOpen: false,
|
||||
detailOpen: false,
|
||||
detail: {},
|
||||
// 账号关联弹窗
|
||||
bindingOpen: false,
|
||||
bindingLoading: false,
|
||||
bindingSubmitting: false,
|
||||
bindingKeyword: '',
|
||||
bindableUsers: [],
|
||||
currentStudent: {},
|
||||
selectedUserId: null,
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
@@ -281,6 +382,60 @@ export default {
|
||||
this.detailOpen = true
|
||||
})
|
||||
},
|
||||
/** 打开关联系统账号对话框 */
|
||||
handleOpenBinding(row) {
|
||||
this.currentStudent = row
|
||||
this.selectedUserId = null
|
||||
this.bindingKeyword = ''
|
||||
this.bindableUsers = []
|
||||
this.bindingOpen = true
|
||||
this.loadBindableUsers()
|
||||
},
|
||||
/** 加载候选用户列表 */
|
||||
loadBindableUsers() {
|
||||
this.bindingLoading = true
|
||||
getStudentBindableUsers(this.bindingKeyword).then(response => {
|
||||
this.bindableUsers = response.data || []
|
||||
this.bindingLoading = false
|
||||
}).catch(() => {
|
||||
this.bindingLoading = false
|
||||
})
|
||||
},
|
||||
/** 单击行选中候选账号 */
|
||||
handleSelectedUserChange(row) {
|
||||
if (row && row.canBind) {
|
||||
this.selectedUserId = row.userId
|
||||
}
|
||||
},
|
||||
/** 提交账号关联 */
|
||||
submitBinding() {
|
||||
if (!this.selectedUserId) {
|
||||
this.$modal.msgWarning('请选择要关联的系统账号')
|
||||
return
|
||||
}
|
||||
const selectedUser = this.bindableUsers.find(u => u.userId === this.selectedUserId)
|
||||
const userName = selectedUser ? selectedUser.userName : this.selectedUserId
|
||||
this.$modal.confirm('确定要将学员档案【' + this.currentStudent.sName + '】关联到账号【' + userName + '】吗?').then(() => {
|
||||
this.bindingSubmitting = true
|
||||
return bindStudentUser(this.currentStudent.sId, this.selectedUserId)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('关联成功')
|
||||
this.bindingOpen = false
|
||||
this.bindingSubmitting = false
|
||||
this.getList()
|
||||
}).catch(() => {
|
||||
this.bindingSubmitting = false
|
||||
})
|
||||
},
|
||||
/** 解除账号关联 */
|
||||
handleUnbind(row) {
|
||||
this.$modal.confirm('确定要解除学员【' + row.sName + '】与系统账号【' + (row.userName || row.userId) + '】的关联吗?').then(() => {
|
||||
return unbindStudentUser(row.sId)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('解除关联成功')
|
||||
this.getList()
|
||||
})
|
||||
},
|
||||
sourceTypeLabel(sourceType) {
|
||||
const labels = {
|
||||
1: '管理员录入',
|
||||
|
||||
@@ -90,8 +90,20 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="关联系统账号" align="center" min-width="150">
|
||||
<template slot-scope="scope">
|
||||
<div v-if="scope.row.accountBindingStatus === 1 || scope.row.userName">
|
||||
<el-tag type="success" size="mini">已关联</el-tag>
|
||||
<div style="font-size: 12px; color: #606266; margin-top: 2px;">
|
||||
{{ scope.row.userName }}
|
||||
<span v-if="scope.row.nickName">({{ scope.row.nickName }})</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-tag v-else type="info" size="mini">未关联</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="100">
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-hasPermi="['train:teacher:edit']"
|
||||
@@ -100,6 +112,23 @@
|
||||
icon="el-icon-edit"
|
||||
@click="handleUpdate(scope.row)"
|
||||
>修改</el-button>
|
||||
<el-button
|
||||
v-if="!scope.row.userName && !scope.row.userId"
|
||||
v-hasPermi="['train:teacher:bindUser']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-link"
|
||||
@click="handleOpenBinding(scope.row)"
|
||||
>关联账号</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
v-hasPermi="['train:teacher:unbindUser']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-circle-close"
|
||||
style="color: #F56C6C;"
|
||||
@click="handleUnbind(scope.row)"
|
||||
>解绑账号</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -160,11 +189,71 @@
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 关联系统账号对话框 -->
|
||||
<el-dialog title="关联系统账号 (教师业务档案)" :visible.sync="bindingOpen" width="700px" append-to-body>
|
||||
<el-alert
|
||||
title="关联说明"
|
||||
type="info"
|
||||
description="将教师业务档案与系统用户账号关联后,教师可凭该系统账号登录并使用“教师工作台”。系统仅允许选择正常启用且已分配教师角色(train_teacher)的账号。"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-bottom: 15px;"
|
||||
/>
|
||||
<div style="margin-bottom: 15px; font-weight: bold; color: #303133;">
|
||||
当前教师:{{ currentTeacher.tName }} (工号: {{ currentTeacher.tId }})
|
||||
</div>
|
||||
<el-form size="small" :inline="true" @submit.native.prevent>
|
||||
<el-form-item label="查找用户">
|
||||
<el-input
|
||||
v-model="bindingKeyword"
|
||||
placeholder="请输入用户名或姓名搜索"
|
||||
clearable
|
||||
style="width: 240px;"
|
||||
@keyup.enter.native="loadBindableUsers"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="loadBindableUsers">搜索</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="bindingLoading" :data="bindableUsers" max-height="300" highlight-current-row @current-change="handleSelectedUserChange">
|
||||
<el-table-column width="55" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-radio v-model="selectedUserId" :label="scope.row.userId" :disabled="!scope.row.canBind">{{ '' }}</el-radio>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="系统账号" prop="userName" min-width="110" />
|
||||
<el-table-column label="姓名/昵称" prop="nickName" min-width="110" />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.status === '0' ? 'success' : 'danger'" size="mini">
|
||||
{{ scope.row.status === '0' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分配角色" min-width="120" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span>{{ (scope.row.roleKeys || []).join(', ') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="可否关联" min-width="150">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.canBind" type="success" size="mini">可选</el-tag>
|
||||
<span v-else style="color: #F56C6C; font-size: 12px;">{{ scope.row.reason }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" :disabled="!selectedUserId" :loading="bindingSubmitting" @click="submitBinding">确 定 关 联</el-button>
|
||||
<el-button @click="bindingOpen = false">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listTeacher, getTeacher, addTeacher, updateTeacher } from '@/api/train/teacher'
|
||||
import { listTeacher, getTeacher, addTeacher, updateTeacher, getTeacherBindableUsers, bindTeacherUser, unbindTeacherUser } from '@/api/train/teacher'
|
||||
|
||||
export default {
|
||||
name: 'Teacher',
|
||||
@@ -186,6 +275,14 @@ export default {
|
||||
title: '',
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
// 账号关联弹窗
|
||||
bindingOpen: false,
|
||||
bindingLoading: false,
|
||||
bindingSubmitting: false,
|
||||
bindingKeyword: '',
|
||||
bindableUsers: [],
|
||||
currentTeacher: {},
|
||||
selectedUserId: null,
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
@@ -297,6 +394,60 @@ export default {
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
/** 打开关联系统账号对话框 */
|
||||
handleOpenBinding(row) {
|
||||
this.currentTeacher = row
|
||||
this.selectedUserId = null
|
||||
this.bindingKeyword = ''
|
||||
this.bindableUsers = []
|
||||
this.bindingOpen = true
|
||||
this.loadBindableUsers()
|
||||
},
|
||||
/** 加载候选用户列表 */
|
||||
loadBindableUsers() {
|
||||
this.bindingLoading = true
|
||||
getTeacherBindableUsers(this.bindingKeyword).then(response => {
|
||||
this.bindableUsers = response.data || []
|
||||
this.bindingLoading = false
|
||||
}).catch(() => {
|
||||
this.bindingLoading = false
|
||||
})
|
||||
},
|
||||
/** 单击行选中候选账号 */
|
||||
handleSelectedUserChange(row) {
|
||||
if (row && row.canBind) {
|
||||
this.selectedUserId = row.userId
|
||||
}
|
||||
},
|
||||
/** 提交账号关联 */
|
||||
submitBinding() {
|
||||
if (!this.selectedUserId) {
|
||||
this.$modal.msgWarning('请选择要关联的系统账号')
|
||||
return
|
||||
}
|
||||
const selectedUser = this.bindableUsers.find(u => u.userId === this.selectedUserId)
|
||||
const userName = selectedUser ? selectedUser.userName : this.selectedUserId
|
||||
this.$modal.confirm('确定要将教师档案【' + this.currentTeacher.tName + '】关联到账号【' + userName + '】吗?').then(() => {
|
||||
this.bindingSubmitting = true
|
||||
return bindTeacherUser(this.currentTeacher.tId, this.selectedUserId)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('关联成功')
|
||||
this.bindingOpen = false
|
||||
this.bindingSubmitting = false
|
||||
this.getList()
|
||||
}).catch(() => {
|
||||
this.bindingSubmitting = false
|
||||
})
|
||||
},
|
||||
/** 解除账号关联 */
|
||||
handleUnbind(row) {
|
||||
this.$modal.confirm('确定要解除教师【' + row.tName + '】与系统账号【' + (row.userName || row.userId) + '】的关联吗?').then(() => {
|
||||
return unbindTeacherUser(row.tId)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('解除关联成功')
|
||||
this.getList()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user