其他接口
ci-userbase-service / build-and-push (push) Successful in 4m7s

This commit is contained in:
chenxing
2026-08-18 21:51:18 +08:00
parent 52b29c5082
commit 44f966c33b
11 changed files with 582 additions and 0 deletions
@@ -0,0 +1,62 @@
package com.yunhai.userbase.controller.api;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.userbase.config.Config;
import com.yunhai.userbase.model.identity.IdentityCurrentVo;
import com.yunhai.userbase.model.identity.IdentityOptionVo;
import com.yunhai.userbase.model.identity.IdentitySwitchRequest;
import com.yunhai.userbase.service.impl.IdentityService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
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 java.util.List;
/**
* 用户身份(平台身份角色 × 学校)管理接口。<br><br>
* 走 CAS 会话路线({@code /{platform}/identity/}),当前用户从 CAS 会话注入的
* {@link BoaUser#getId()} 取,不接受外部传入 userId(防越权)。<br>
* 三个端点对应「切换身份」业务:<br>
* ① {@code GET current} 当前激活身份(无切换记录则兜底主身份);<br>
* ② {@code GET available} 全部可选身份清单;<br>
* ③ {@code POST switch} 切换到指定 (roleId, schoolId),服务端校验属主后写入当前激活状态。
*
* @author chenx
* @date 2026/8/18
*/
@RestController
@RequestMapping(Config.API_PATH + "/identity/")
public class IdentityController {
@Autowired
private IdentityService identityService;
/**
* 当前激活身份。
*/
@GetMapping("current")
public IdentityCurrentVo current(BoaUser authUserWithDept) {
return identityService.getCurrent(authUserWithDept.getId());
}
/**
* 全部可选身份(正常态),供「切换身份」页渲染。
*/
@GetMapping("available")
public List<IdentityOptionVo> available(BoaUser authUserWithDept) {
return identityService.listAvailable(authUserWithDept.getId());
}
/**
* 切换身份:校验属主后在 sys_user_current_role 写当前激活状态。
*/
@PostMapping("switch")
public IdentityCurrentVo switchIdentity(BoaUser authUserWithDept,
@RequestBody @Validated IdentitySwitchRequest request) {
return identityService.switchIdentity(authUserWithDept.getId(), request);
}
}
@@ -0,0 +1,86 @@
package com.yunhai.userbase.entity;
import com.yunhai.userbase.common.converter.StatusEnumConverter;
import com.yunhai.userbase.enums.StatusEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
/**
* @author chenx
* @date 2026/8/18
* @description 用户-SysRoleEnum角色-学校 三元关联(平台身份角色的权威源)。
* <p>区别于 {@link SysUserAppRoleRel}(业务权限角色):本表表达"用户在哪个学校下激活哪个平台身份角色"
* (如"刘文娟在 A 学校是老师"),角色取值对应 {@link com.yunhai.userbase.enums.SysRoleEnum} 的 key。</p>
* <p>不继承 BaseEntity:高频读写关系表,自定义字段更轻量,与 {@link SysUserGroupRel} 风格一致。</p>
*/
@Data
@NoArgsConstructor
@Entity
@Table(
name = "sys_user_role_rel",
uniqueConstraints = @UniqueConstraint(columnNames = {"userId", "roleId", "schoolId"}),
indexes = {
@Index(columnList = "userId"),
@Index(columnList = "schoolId"),
@Index(columnList = "roleId")
}
)
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class SysUserRoleRel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@NotNull(groups = Update.class)
private Integer id;
/**
* 用户ID
*/
@Column(nullable = false)
private Integer userId;
/**
* 角色ID(SysRoleEnum.key,1-7)
*/
@Column(nullable = false)
private Integer roleId;
/**
* 学校组ID(=type=SCHOOL_GROUP 的 sys_group.id);区域/学段/根直属用户填 0
*/
@Column(nullable = false)
private Integer schoolId;
/**
* 是否主身份标记:每 userId 最多 1 条 isMain=1(应用层校验,不靠 uk)
*/
@Column(nullable = false)
private Integer isMain = 0;
/**
* 排序号:越小越靠前
*/
@Column(name = "sort_no", nullable = false)
private Integer sortNo = 0;
/**
* 状态:0=禁用 1=正常(StatusEnum)
*/
@Convert(converter = StatusEnumConverter.class)
@Column(nullable = false)
private StatusEnum status = StatusEnum.NORMAL;
@Column(name = "create_time", nullable = false, updatable = false)
private LocalDateTime createTime;
@Column(name = "update_time", nullable = false)
private LocalDateTime updateTime;
public @interface Update {
}
}
@@ -0,0 +1,32 @@
package com.yunhai.userbase.model.identity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 当前激活身份(来自 {@code sys_user_current_role};无切换记录时兜底主身份)。<br>
*
* @author chenx
* @date 2026/8/18
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IdentityCurrentVo {
/** 当前激活角色ID(SysRoleEnum.key) */
private Integer roleId;
/** 当前激活角色名 */
private String roleName;
/** 当前激活学校ID(=学校组 sys_group.id;无学校时为 0) */
private Integer schoolId;
/** 当前激活学校名 */
private String schoolName;
/** 是否兜底(无主动切换记录时为 true,提示前端可引导用户显式切换) */
private Boolean fallback;
}
@@ -0,0 +1,33 @@
package com.yunhai.userbase.model.identity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用户可选身份(一条 = 某学校下的某平台角色)。<br>
* 数据源:{@code sys_user_role_rel}(平台身份角色权威源)。<br>
*
* @author chenx
* @date 2026/8/18
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IdentityOptionVo {
/** 角色ID(SysRoleEnum.key,1-7) */
private Integer roleId;
/** 角色名(老师/校领导/学生…,查枚举快取) */
private String roleName;
/** 学校ID(=学校组 sys_group.id;区域/学段/直属无学校时为 0) */
private Integer schoolId;
/** 学校名(冗余,免 join sys_group) */
private String schoolName;
/** 是否主身份(isMain=1) */
private Boolean isMain;
}
@@ -0,0 +1,25 @@
package com.yunhai.userbase.model.identity;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 切换身份入参:指定目标学校与该学校下的角色。<br>
* 服务端校验 (userId, roleId, schoolId) 在 {@code sys_user_role_rel} 中存在且正常态,<br>
* 不存在则拒绝(防止越权切换到非授权身份)。<br>
*
* @author chenx
* @date 2026/8/18
*/
@Data
public class IdentitySwitchRequest {
/** 目标角色ID(SysRoleEnum.key) */
@NotNull(message = "角色ID不能为空")
private Integer roleId;
/** 目标学校ID(=学校组 sys_group.id;无学校维度传 0) */
@NotNull(message = "学校ID不能为空")
private Integer schoolId;
}
@@ -0,0 +1,24 @@
package com.yunhai.userbase.model.sysuser;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* @description 开放接口:移动用户到指定组请求(语义同后台「调整组」——覆盖式把用户组归属替换为目标组,目标设为主组)。
*/
@Data
public class SysUserMoveRequest {
/** 待移动用户ID。 */
@NotNull(message = "用户ID不能为空")
private Integer userId;
/** 目标组ID(移动后的主归属组,必须存在)。 */
@NotNull(message = "目标组ID不能为空")
private Integer groupId;
/** 学校组ID:限定移动范围,仅允许将用户在同一学校(组)范围内移动。 */
@NotNull(message = "学校ID不能为空")
private Integer schoolId;
}
@@ -0,0 +1,17 @@
package com.yunhai.userbase.repository;
import com.yunhai.userbase.entity.SysUserCurrentRole;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import java.util.Optional;
/**
* @author chenx
* @date 2026/8/18
* @description 用户当前激活身份仓储(一对一:user_id 唯一)
*/
public interface SysUserCurrentRoleRepository extends JpaRepository<SysUserCurrentRole, Integer>, JpaSpecificationExecutor<SysUserCurrentRole> {
Optional<SysUserCurrentRole> findByUserId(Integer userId);
}
@@ -0,0 +1,39 @@
package com.yunhai.userbase.repository;
import com.yunhai.userbase.entity.SysUserRoleRel;
import com.yunhai.userbase.enums.StatusEnum;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;
/**
* @author chenx
* @date 2026/8/18
* @description 用户-SysRoleEnum角色-学校 关联仓储(平台身份角色权威源)
*/
public interface SysUserRoleRelRepository extends JpaRepository<SysUserRoleRel, Integer>, JpaSpecificationExecutor<SysUserRoleRel> {
/** 用户全部可选身份(含禁用态由调用方过滤) */
List<SysUserRoleRel> findByUserId(Integer userId);
/** 用户某状态下的全部可选身份 */
List<SysUserRoleRel> findByUserIdAndStatus(Integer userId, StatusEnum status);
/** 用户主身份(isMain=1),最多 1 条 */
Optional<SysUserRoleRel> findByUserIdAndIsMain(Integer userId, Integer isMain);
/** 用户在某学校下、某状态下的全部角色关联 */
List<SysUserRoleRel> findByUserIdAndSchoolIdAndStatus(Integer userId, Integer schoolId, StatusEnum status);
/** 用户所有正常态的 roleId 去重列表(用于 BoaUser.roleIds) */
@Query("SELECT DISTINCT r.roleId FROM SysUserRoleRel r WHERE r.userId = :userId AND r.status = :status")
List<Integer> findDistinctRoleIdsByUserIdAndStatus(@Param("userId") Integer userId, @Param("status") StatusEnum status);
/** 用户所有正常态的 schoolId 去重列表(用于 BoaUser.schools,schoolId>0 的过滤在调用方做) */
@Query("SELECT DISTINCT r.schoolId FROM SysUserRoleRel r WHERE r.userId = :userId AND r.status = :status")
List<Integer> findDistinctSchoolIdsByUserIdAndStatus(@Param("userId") Integer userId, @Param("status") StatusEnum status);
}
@@ -0,0 +1,146 @@
package com.yunhai.userbase.service.impl;
import com.yunhai.platform.utils.exception.DException;
import com.yunhai.userbase.entity.SysGroup;
import com.yunhai.userbase.entity.SysUserCurrentRole;
import com.yunhai.userbase.entity.SysUserRoleRel;
import com.yunhai.userbase.enums.IEnum;
import com.yunhai.userbase.enums.StatusEnum;
import com.yunhai.userbase.enums.SysRoleEnum;
import com.yunhai.userbase.model.identity.IdentityCurrentVo;
import com.yunhai.userbase.model.identity.IdentityOptionVo;
import com.yunhai.userbase.model.identity.IdentitySwitchRequest;
import com.yunhai.userbase.repository.SysGroupRepository;
import com.yunhai.userbase.repository.SysUserCurrentRoleRepository;
import com.yunhai.userbase.repository.SysUserRoleRelRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* 用户身份服务:管理「平台身份角色 × 学校」维度的当前激活状态与可选清单。<br><br>
* 身份 = (userId, roleId, schoolId) 三元组,权威源为 {@code sys_user_role_rel};<br>
* 当前激活状态存 {@code sys_user_current_role}(1:1,每用户一条)。<br>
* 本服务不处理业务权限角色(sys_app_role),那套与学校维度无关。
*
* @author chenx
* @date 2026/8/18
*/
@Service
public class IdentityService {
@Autowired
private SysUserRoleRelRepository sysUserRoleRelRepository;
@Autowired
private SysUserCurrentRoleRepository sysUserCurrentRoleRepository;
@Autowired
private SysGroupRepository sysGroupRepository;
/**
* 用户全部可选身份(正常态),用于「切换身份」页渲染。
*/
public List<IdentityOptionVo> listAvailable(Integer userId) {
List<SysUserRoleRel> rels = sysUserRoleRelRepository
.findByUserIdAndStatus(userId, StatusEnum.NORMAL);
List<IdentityOptionVo> result = new ArrayList<>(rels.size());
for (SysUserRoleRel rel : rels) {
IdentityOptionVo vo = new IdentityOptionVo();
vo.setRoleId(rel.getRoleId());
vo.setRoleName(roleName(rel.getRoleId()));
vo.setSchoolId(rel.getSchoolId());
vo.setSchoolName(schoolName(rel.getSchoolId()));
vo.setIsMain(rel.getIsMain() != null && rel.getIsMain() == 1);
result.add(vo);
}
return result;
}
/**
* 当前激活身份:优先读 sys_user_current_role;无切换记录时兜底主身份(isMain=1)。
*/
public IdentityCurrentVo getCurrent(Integer userId) {
Optional<SysUserCurrentRole> currentOpt = sysUserCurrentRoleRepository.findByUserId(userId);
IdentityCurrentVo vo = new IdentityCurrentVo();
if (currentOpt.isPresent()) {
SysUserCurrentRole current = currentOpt.get();
vo.setRoleId(current.getRoleId());
vo.setRoleName(roleName(current.getRoleId()));
vo.setSchoolId(current.getSchoolId());
vo.setSchoolName(current.getSchoolName());
vo.setFallback(false);
return vo;
}
// 兜底:主身份整条记录(isMain=1)
SysUserRoleRel mainRel = sysUserRoleRelRepository.findByUserIdAndIsMain(userId, 1).orElse(null);
if (mainRel != null) {
vo.setRoleId(mainRel.getRoleId());
vo.setRoleName(roleName(mainRel.getRoleId()));
vo.setSchoolId(mainRel.getSchoolId());
vo.setSchoolName(schoolName(mainRel.getSchoolId()));
vo.setFallback(true);
return vo;
}
// 连主身份都没有(测试期初数据尚未建立):返回空对象,前端引导创建身份
vo.setFallback(true);
return vo;
}
/**
* 切换身份:校验 (userId, roleId, schoolId) 在 sys_user_role_rel 正常态中存在,
* 再 UPSERT sys_user_current_role(按 userId 唯一)。
*/
public IdentityCurrentVo switchIdentity(Integer userId, IdentitySwitchRequest request) {
Integer roleId = request.getRoleId();
Integer schoolId = request.getSchoolId();
// 校验目标身份确属该用户且正常态,防越权切换
SysUserRoleRel rel = sysUserRoleRelRepository
.findByUserIdAndSchoolIdAndStatus(userId, schoolId, StatusEnum.NORMAL)
.stream()
.filter(r -> Objects.equals(r.getRoleId(), roleId))
.findFirst()
.orElse(null);
if (rel == null) {
throw new DException("目标身份不存在或非正常态,无法切换");
}
SysUserCurrentRole current = sysUserCurrentRoleRepository.findByUserId(userId).orElse(null);
if (current == null) {
current = new SysUserCurrentRole();
current.setUserId(userId);
}
current.setRoleId(roleId);
current.setSchoolId(schoolId);
current.setSchoolName(schoolName(schoolId));
current.setSwitchTime(LocalDateTime.now());
sysUserCurrentRoleRepository.save(current);
IdentityCurrentVo vo = new IdentityCurrentVo();
vo.setRoleId(roleId);
vo.setRoleName(roleName(roleId));
vo.setSchoolId(schoolId);
vo.setSchoolName(current.getSchoolName());
vo.setFallback(false);
return vo;
}
private String roleName(Integer roleId) {
if (roleId == null) {
return null;
}
SysRoleEnum e = IEnum.getByKey(SysRoleEnum.class, roleId);
return e != null ? e.getValue() : null;
}
private String schoolName(Integer schoolId) {
if (schoolId == null || schoolId <= 0) {
return null;
}
SysGroup g = sysGroupRepository.findById(schoolId).orElse(null);
return g != null ? g.getName() : null;
}
}