1. 修复界面bug

2. 新增 api 可配置匿名访问固定用户配置
3. 新增密码弱口令校验规则
4. e 办使用 loginName 确认唯一用户逻辑
This commit is contained in:
chenbowen
2025-10-24 17:02:10 +08:00
parent 27796ff67d
commit 6e4cc4d55e
56 changed files with 1268 additions and 246 deletions

View File

@@ -17,6 +17,9 @@ public class AdminUserRespDTO implements VO {
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小王")
private String nickname;
@Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long tenantId;
@Schema(description = "帐号状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer status; // 参见 CommonStatusEnum 枚举

View File

@@ -22,8 +22,9 @@ public enum DataScopeEnum implements ArrayValuable<Integer> {
DEPT_CUSTOM(2), // 指定部门数据权限
DEPT_ONLY(3), // 部门数据权限
DEPT_AND_CHILD(4), // 部门及以下数据权限
SELF(5), // 仅本人数据权限
SELF(5); // 仅本人数据权限
COMPANY_AND_DEPT(6); // 公司及所属部门数据权限
/**
* 范围

View File

@@ -5,6 +5,7 @@ import cn.hutool.core.util.ObjUtil;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.util.object.BeanUtils;
import com.zt.plat.framework.datapermission.core.util.DataPermissionUtils;
import com.zt.plat.framework.tenant.core.aop.TenantIgnore;
import com.zt.plat.module.system.api.user.dto.AdminUserRespDTO;
import com.zt.plat.module.system.api.user.dto.AdminUserSaveReqDTO;
import com.zt.plat.module.system.api.user.dto.AdminUserUpdatePasswordReqDTO;
@@ -70,6 +71,7 @@ public class AdminUserApiImpl implements AdminUserApi {
}
@Override
@TenantIgnore
public CommonResult<AdminUserRespDTO> getUser(Long id) {
AdminUserDO user = userService.getUser(id);
return success(BeanUtils.toBean(user, AdminUserRespDTO.class));

View File

@@ -1,6 +1,7 @@
package com.zt.plat.module.system.controller.admin.auth.vo;
import com.zt.plat.framework.common.validation.Password;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
@@ -26,6 +27,7 @@ public class AuthRegisterReqVO extends CaptchaVerificationReqVO {
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16")
@Length(min = 6, max = 20, message = "密码长度为 6-20")
@Password
private String password;
}

View File

@@ -1,6 +1,7 @@
package com.zt.plat.module.system.controller.admin.auth.vo;
import com.zt.plat.framework.common.validation.Mobile;
import com.zt.plat.framework.common.validation.Password;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
@@ -18,7 +19,8 @@ public class AuthResetPasswordReqVO {
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1234")
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16")
@Length(min = 6, max = 20, message = "密码长度为 6-20")
@Password
private String password;
@Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13312341234")

View File

@@ -1,7 +1,6 @@
package com.zt.plat.module.system.controller.admin.auth.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -13,9 +12,8 @@ import org.hibernate.validator.constraints.Length;
@AllArgsConstructor
public class AuthVerifyPasswordReqVO {
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao")
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
@Schema(description = "密码", example = "buzhidao")
@Length(max = 16, message = "密码长度不能超过 16 位")
private String password;
}

View File

@@ -136,6 +136,15 @@ public class DeptController {
return success(BeanUtils.toBean(list, DeptRespVO.class));
}
@GetMapping("/search")
@Operation(summary = "根据关键字搜索部门树", description = "返回匹配节点及其上级节点,便于前端展示搜索结果")
@Parameter(name = "name", description = "部门名称关键字", required = true, example = "研发")
@PreAuthorize("@ss.hasPermission('system:dept:query')")
public CommonResult<List<DeptRespVO>> searchDeptTree(@RequestParam("name") String name) {
List<DeptDO> list = deptService.searchDeptTree(name);
return success(BeanUtils.toBean(list, DeptRespVO.class));
}
@GetMapping("/get")
@Operation(summary = "获得部门信息")
@Parameter(name = "id", description = "编号", required = true, example = "1024")

View File

@@ -10,6 +10,9 @@ public class DeptListReqVO {
@Schema(description = "部门名称,模糊匹配", example = "芋道")
private String name;
@Schema(description = "部门编码,精确匹配", example = "ZT001")
private String code;
@Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1")
private Integer status;

View File

@@ -2,14 +2,14 @@ package com.zt.plat.module.system.controller.admin.tenant.vo.tenant;
import cn.hutool.core.util.ObjectUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.zt.plat.framework.common.validation.Password;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 租户创建/修改 Request VO")
@@ -57,7 +57,8 @@ public class TenantSaveReqVO {
private String username;
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@Length(min = 4, max = 16, message = "密码长度为 4-16")
@Length(min = 6, max = 20, message = "密码长度为 6-20")
@Password
private String password;
@AssertTrue(message = "用户账号、密码不能为空")

View File

@@ -11,7 +11,6 @@ import com.zt.plat.module.system.controller.admin.user.vo.user.*;
import com.zt.plat.module.system.convert.user.UserConvert;
import com.zt.plat.module.system.dal.dataobject.user.AdminUserDO;
import com.zt.plat.module.system.enums.common.SexEnum;
import com.zt.plat.module.system.service.dept.DeptService;
import com.zt.plat.module.system.service.user.AdminUserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -38,8 +37,6 @@ public class UserController {
@Resource
private AdminUserService userService;
@Resource
private DeptService deptService;
@PostMapping("/create")
@Operation(summary = "新增用户")

View File

@@ -1,11 +1,11 @@
package com.zt.plat.module.system.controller.admin.user.vo.profile;
import com.zt.plat.framework.common.validation.Password;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import jakarta.validation.constraints.NotEmpty;
@Schema(description = "管理后台 - 用户个人中心更新密码 Request VO")
@Data
public class UserProfileUpdatePasswordReqVO {
@@ -17,7 +17,8 @@ public class UserProfileUpdatePasswordReqVO {
@Schema(description = "新密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "654321")
@NotEmpty(message = "新密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16")
@Length(min = 6, max = 20, message = "密码长度为 6-20")
@Password
private String newPassword;
}

View File

@@ -34,9 +34,9 @@ public class UserRespVO{
@Schema(description = "部门ID列表", example = "我是一个部门Id列表")
private List<Long> deptIds;
// @Schema(description = "部门名称", example = "IT 部")
// @ExcelProperty("部门名称")
// private String deptName;
@Schema(description = "部门名称", example = "总部研发部、平台组")
private String deptNames;
@Schema(description = "岗位编号数组", example = "1")
private Set<Long> postIds;

View File

@@ -1,12 +1,13 @@
package com.zt.plat.module.system.controller.admin.user.vo.user;
import cn.hutool.core.util.ObjectUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.mzt.logapi.starter.annotation.DiffLogField;
import com.zt.plat.framework.common.validation.Mobile;
import com.zt.plat.framework.common.validation.Password;
import com.zt.plat.module.system.framework.operatelog.core.DeptParseFunction;
import com.zt.plat.module.system.framework.operatelog.core.PostParseFunction;
import com.zt.plat.module.system.framework.operatelog.core.SexParseFunction;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.mzt.logapi.starter.annotation.DiffLogField;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.Data;
@@ -76,7 +77,8 @@ public class UserSaveReqVO {
// ========== 仅【创建】时,需要传递的字段 ==========
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@Length(min = 4, max = 16, message = "密码长度为 4-16")
@Length(min = 6, max = 20, message = "密码长度为 6-20")
@Password
private String password;
@AssertTrue(message = "密码不能为空")

View File

@@ -0,0 +1,22 @@
package com.zt.plat.module.system.controller.admin.user.vo.user;
import com.zt.plat.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Schema(description = "管理后台 - 用户选择器分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class UserSelectorPageReqVO extends PageParam {
@Schema(description = "关键字(支持姓名/账号/手机号模糊匹配)", example = "张三")
private String keyword;
@Schema(description = "部门编号,同时筛选子部门", example = "1024")
private Long deptId;
@Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "0")
private Integer status;
}

View File

@@ -0,0 +1,27 @@
package com.zt.plat.module.system.controller.admin.user.vo.user;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 用户选择器 Response VO")
@Data
public class UserSelectorRespVO {
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王小明")
private String nickname;
@Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "wangxm")
private String username;
@Schema(description = "手机号码", example = "13800000000")
private String mobile;
@Schema(description = "帐号状态,参见 CommonStatusEnum 枚举类", example = "0")
private Integer status;
@Schema(description = "所属部门名称(多个部门以、分隔)", example = "总部研发部、平台组")
private String deptNames;
}

View File

@@ -1,11 +1,11 @@
package com.zt.plat.module.system.controller.admin.user.vo.user;
import com.zt.plat.framework.common.validation.Password;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
@Schema(description = "管理后台 - 用户更新密码 Request VO")
@Data
@@ -17,7 +17,8 @@ public class UserUpdatePasswordReqVO {
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16")
@Length(min = 6, max = 20, message = "密码长度为 6-20")
@Password
private String password;
}

View File

@@ -30,7 +30,11 @@ public interface UserConvert {
}
default UserRespVO convert(AdminUserDO user) {
return BeanUtils.toBean(user, UserRespVO.class);
UserRespVO vo = BeanUtils.toBean(user, UserRespVO.class);
if (user.getDeptIds() != null) {
vo.setDeptIds(CollectionUtils.convertList(user.getDeptIds(), Long::longValue));
}
return vo;
}
default List<UserSimpleRespVO> convertSimpleList(List<AdminUserDO> list) {

View File

@@ -60,6 +60,9 @@ public class AdminUserDO extends TenantBaseDO {
@TableField(exist = false, typeHandler = JacksonTypeHandler.class )
@NotEmpty
private Set<Long> deptIds;
@TableField(exist = false)
private String deptNames;
/**
* 公司 ID 列表
*/

View File

@@ -1,11 +1,12 @@
package com.zt.plat.module.system.dal.mysql.dept;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.zt.plat.framework.common.enums.CommonStatusEnum;
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptListReqVO;
import com.zt.plat.module.system.dal.dataobject.dept.DeptDO;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
@@ -20,6 +21,7 @@ public interface DeptMapper extends BaseMapperX<DeptDO> {
default List<DeptDO> selectList(DeptListReqVO reqVO) {
return selectList(new LambdaQueryWrapperX<DeptDO>()
.likeIfPresent(DeptDO::getName, reqVO.getName())
.eqIfPresent(DeptDO::getCode, reqVO.getCode())
.eqIfPresent(DeptDO::getStatus, reqVO.getStatus())
.eqIfPresent(DeptDO::getIsCompany, reqVO.getIsCompany())
);
@@ -127,4 +129,17 @@ public interface DeptMapper extends BaseMapperX<DeptDO> {
);
}
/**
* 根据部门名称模糊查询启用状态的部门列表
*
* @param name 部门名称关键字
* @return 部门列表
*/
default List<DeptDO> selectListByName(String name) {
return selectList(new LambdaQueryWrapperX<DeptDO>()
.likeIfPresent(DeptDO::getName, name)
.eq(DeptDO::getStatus, CommonStatusEnum.ENABLE.getStatus())
);
}
}

View File

@@ -1,6 +1,10 @@
package com.zt.plat.module.system.service.auth;
import cn.hutool.core.util.ObjectUtil;
import com.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.CaptchaService;
import com.google.common.annotations.VisibleForTesting;
import com.zt.plat.framework.common.enums.CommonStatusEnum;
import com.zt.plat.framework.common.enums.UserTypeEnum;
import com.zt.plat.framework.common.util.monitor.TracerUtils;
@@ -19,17 +23,13 @@ import com.zt.plat.module.system.enums.logger.LoginLogTypeEnum;
import com.zt.plat.module.system.enums.logger.LoginResultEnum;
import com.zt.plat.module.system.enums.oauth2.OAuth2ClientConstants;
import com.zt.plat.module.system.enums.sms.SmsSceneEnum;
import com.zt.plat.module.system.enums.user.UserSourceEnum;
import com.zt.plat.module.system.service.logger.LoginLogService;
import com.zt.plat.module.system.service.member.MemberService;
import com.zt.plat.module.system.service.oauth2.OAuth2TokenService;
import com.zt.plat.module.system.service.oauth2.EbanOAuth2Service;
import com.zt.plat.module.system.service.oauth2.OAuth2TokenService;
import com.zt.plat.module.system.service.social.SocialUserService;
import com.zt.plat.module.system.service.user.AdminUserService;
import com.zt.plat.module.system.enums.user.UserSourceEnum;
import com.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.CaptchaService;
import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Resource;
import jakarta.validation.Validator;
import lombok.Setter;
@@ -286,6 +286,16 @@ public class AdminAuthServiceImpl implements AdminAuthService {
if (user == null) {
throw exception(USER_NOT_EXISTS);
}
if (isInternalUser(user)) {
return;
}
if (StringUtils.isBlank(password)) {
throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
}
int length = password.length();
if (length < 4 || length > 16) {
throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
}
if (!userService.isPasswordMatch(password, user.getPassword())) {
throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
}

View File

@@ -128,6 +128,15 @@ public interface DeptService {
Set<CompanyDeptInfo> getCompanyDeptInfoListByUserId(Long userId);
/**
* 计算公司及其直属部门数据权限范围
* 在给定部门集合的场景下,按所属公司维度汇总公司本级及其直属部门(不含其他子公司)
*
* @param deptIds 部门编号集合
* @return 公司及其直属部门编号集合
*/
Set<Long> computeCompanyScopeDeptIds(Set<Long> deptIds);
/**
* 获取当前用户可访问的顶级部门列表
* 用于懒加载,返回当前用户所属部门的最顶层祖先部门
@@ -157,4 +166,12 @@ public interface DeptService {
* 按照新的编码规则初始化全部部门编码
*/
void initializeDeptCodes();
/**
* 根据关键字搜索部门树,包含匹配节点及其上级节点
*
* @param keyword 关键字
* @return 部门列表
*/
List<DeptDO> searchDeptTree(String keyword);
}

View File

@@ -400,19 +400,11 @@ public class DeptServiceImpl implements DeptService {
return Collections.emptyList();
}
Set<Long> companyIds = new HashSet<>();
Map<Long, DeptDO> deptCache = new HashMap<>();
for (Long deptId : deptIds) {
DeptDO dept = getDept(deptId);
while (dept != null) {
// 如果当前部门是公司,加入结果并结束本次递归
if (Boolean.TRUE.equals(dept.getIsCompany())) {
companyIds.add(dept.getId());
break;
}
// 到达根节点或无上级,结束递归
if (dept.getParentId() == null || DeptDO.PARENT_ID_ROOT.equals(dept.getParentId())) {
break;
}
dept = getDept(dept.getParentId());
Long companyId = resolveNearestCompanyId(deptId, deptCache);
if (companyId != null) {
companyIds.add(companyId);
}
}
return getDeptList(companyIds);
@@ -440,43 +432,15 @@ public class DeptServiceImpl implements DeptService {
// 如果指定了公司ID则进一步过滤属于该公司的部门
if (companyId != null) {
Map<Long, DeptDO> deptCache = new HashMap<>();
return deptList.stream()
.filter(dept -> isUnderCompany(dept, companyId))
.filter(dept -> Objects.equals(resolveNearestCompanyId(dept.getId(), deptCache), companyId))
.collect(Collectors.toList());
}
return deptList;
}
/**
* 判断部门是否属于指定公司
*
* @param dept 部门
* @param companyId 公司ID
* @return 是否属于指定公司
*/
private boolean isUnderCompany(DeptDO dept, Long companyId) {
if (dept == null || companyId == null) {
return false;
}
// 如果部门本身就是指定的公司
if (dept.getId().equals(companyId) && Boolean.TRUE.equals(dept.getIsCompany())) {
return true;
}
// 向上递归查找,看是否有祖先部门是指定的公司
DeptDO current = dept;
while (current != null && current.getParentId() != null && !DeptDO.PARENT_ID_ROOT.equals(current.getParentId())) {
current = getDept(current.getParentId());
if (current != null && current.getId().equals(companyId) && Boolean.TRUE.equals(current.getIsCompany())) {
return true;
}
}
return false;
}
/**
* 根据用户ID查询其归属公司及直属部门关系列表不递归下级公司
*/
@@ -493,18 +457,13 @@ public class DeptServiceImpl implements DeptService {
// 查询所有部门信息
Map<Long, DeptDO> deptMap = getDeptList(deptIds).stream()
.collect(Collectors.toMap(DeptDO::getId, d -> d));
Map<Long, DeptDO> deptCache = new HashMap<>(deptMap);
Set<CompanyDeptInfo> result = new HashSet<>();
for (Long deptId : deptIds) {
DeptDO dept = deptMap.get(deptId);
DeptDO dept = loadDept(deptId, deptCache);
if (dept == null) continue;
// 向上查找公司如果到达顶层parentId为PARENT_ID_ROOT还没找到公司则用顶层部门作为公司
DeptDO company = dept;
while (company != null && !Boolean.TRUE.equals(company.getIsCompany())) {
if (company.getParentId() == null || DeptDO.PARENT_ID_ROOT.equals(company.getParentId())) {
break;
}
company = getDept(company.getParentId());
}
Long companyId = resolveNearestCompanyId(deptId, deptCache);
DeptDO company = companyId != null ? loadDept(companyId, deptCache) : findTopLevelAncestor(dept);
if (company == null) continue;
CompanyDeptInfo info = new CompanyDeptInfo();
info.setCompanyId(company.getId());
@@ -518,6 +477,26 @@ public class DeptServiceImpl implements DeptService {
return result;
}
@Override
public Set<Long> computeCompanyScopeDeptIds(Set<Long> deptIds) {
if (CollUtil.isEmpty(deptIds)) {
return Collections.emptySet();
}
Map<Long, DeptDO> deptCache = new HashMap<>();
Map<Long, Set<Long>> companyDeptCache = new HashMap<>();
Set<Long> result = new HashSet<>();
for (Long deptId : deptIds) {
Long companyId = resolveNearestCompanyId(deptId, deptCache);
if (companyId == null) {
continue;
}
Set<Long> scopedDeptIds = companyDeptCache.computeIfAbsent(companyId,
id -> resolveCompanyDeptIds(id, deptCache));
result.addAll(scopedDeptIds);
}
return result;
}
@Override
public List<DeptDO> getTopLevelDeptList() {
// 获取当前用户所属的部门列表
@@ -614,4 +593,93 @@ public class DeptServiceImpl implements DeptService {
}
}
@Override
public List<DeptDO> searchDeptTree(String keyword) {
if (StrUtil.isBlank(keyword)) {
return Collections.emptyList();
}
List<DeptDO> matchedDepts = deptMapper.selectListByName(keyword.trim());
if (CollUtil.isEmpty(matchedDepts)) {
return Collections.emptyList();
}
Map<Long, DeptDO> cache = new HashMap<>();
LinkedHashMap<Long, DeptDO> resultMap = new LinkedHashMap<>();
for (DeptDO dept : matchedDepts) {
appendDeptWithAncestors(dept, resultMap, cache);
}
return new ArrayList<>(resultMap.values());
}
private void appendDeptWithAncestors(DeptDO dept, Map<Long, DeptDO> resultMap, Map<Long, DeptDO> cache) {
DeptDO current = dept;
while (current != null) {
if (!resultMap.containsKey(current.getId())) {
resultMap.put(current.getId(), current);
}
Long parentId = current.getParentId();
if (parentId == null || DeptDO.PARENT_ID_ROOT.equals(parentId)) {
break;
}
if (Objects.equals(parentId, current.getId())) {
break;
}
current = cache.computeIfAbsent(parentId, deptMapper::selectById);
}
}
private Set<Long> resolveCompanyDeptIds(Long companyId, Map<Long, DeptDO> deptCache) {
DeptDO company = loadDept(companyId, deptCache);
if (company == null) {
return Collections.emptySet();
}
Set<Long> deptIds = new HashSet<>();
deptIds.add(company.getId());
List<DeptDO> descendants = getChildDeptList(companyId);
if (CollUtil.isEmpty(descendants)) {
return deptIds;
}
descendants.forEach(dept -> deptCache.putIfAbsent(dept.getId(), dept));
for (DeptDO dept : descendants) {
if (Boolean.TRUE.equals(dept.getIsCompany())) {
continue;
}
Long anchorCompanyId = resolveNearestCompanyId(dept.getId(), deptCache);
if (Objects.equals(anchorCompanyId, companyId)) {
deptIds.add(dept.getId());
}
}
return deptIds;
}
private Long resolveNearestCompanyId(Long deptId, Map<Long, DeptDO> deptCache) {
DeptDO current = loadDept(deptId, deptCache);
while (current != null) {
if (Boolean.TRUE.equals(current.getIsCompany())) {
return current.getId();
}
Long parentId = current.getParentId();
if (parentId == null || DeptDO.PARENT_ID_ROOT.equals(parentId)) {
return null;
}
current = loadDept(parentId, deptCache);
}
return null;
}
private DeptDO loadDept(Long deptId, Map<Long, DeptDO> deptCache) {
if (deptId == null) {
return null;
}
DeptDO cached = deptCache.get(deptId);
if (cached != null) {
return cached;
}
DeptDO dept = getDept(deptId);
if (dept != null) {
deptCache.put(deptId, dept);
}
return dept;
}
}

View File

@@ -1,7 +1,8 @@
package com.zt.plat.module.system.service.oauth2;
import com.zt.plat.module.system.controller.admin.auth.vo.AuthOAuth2CallbackReqVO;
import com.zt.plat.module.system.controller.admin.auth.vo.AuthLoginRespVO;
import com.zt.plat.module.system.controller.admin.auth.vo.AuthOAuth2CallbackReqVO;
import lombok.Data;
/**
* E办OAuth2服务接口
@@ -30,6 +31,7 @@ public interface EbanOAuth2Service {
/**
* E办用户信息
*/
@Data
class EbanUserInfo {
private String username;
private String realName;
@@ -37,47 +39,12 @@ public interface EbanOAuth2Service {
private String mobile;
private String deptName;
private String uid;
private String displayName;
private String displayName;
private String loginName;
private String rawUserInfoJson;
private EbanOAuth2ServiceImpl.EbanTokenInfo tokenInfo; // 添加Token信息
// 构造函数
public EbanUserInfo() {}
public EbanUserInfo(String username, String realName, String email, String mobile, String deptName) {
this.username = username;
this.realName = realName;
this.email = email;
this.mobile = mobile;
this.deptName = deptName;
}
// getter和setter方法
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getRealName() { return realName; }
public void setRealName(String realName) { this.realName = realName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getMobile() { return mobile; }
public void setMobile(String mobile) { this.mobile = mobile; }
public String getDeptName() { return deptName; }
public void setDeptName(String deptName) { this.deptName = deptName; }
public String getUid() { return uid; }
public void setUid(String uid) { this.uid = uid; }
public String getDisplayName() { return displayName; }
public void setDisplayName(String displayName) { this.displayName = displayName; }
public String getRawUserInfoJson() { return rawUserInfoJson; }
public void setRawUserInfoJson(String rawUserInfoJson) { this.rawUserInfoJson = rawUserInfoJson; }
public EbanOAuth2ServiceImpl.EbanTokenInfo getTokenInfo() { return tokenInfo; }
public void setTokenInfo(EbanOAuth2ServiceImpl.EbanTokenInfo tokenInfo) { this.tokenInfo = tokenInfo; }
}
}

View File

@@ -14,7 +14,6 @@ import com.zt.plat.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import com.zt.plat.module.system.dal.dataobject.user.AdminUserDO;
import com.zt.plat.module.system.enums.logger.LoginLogTypeEnum;
import com.zt.plat.module.system.enums.logger.LoginResultEnum;
import com.zt.plat.module.system.enums.oauth2.OAuth2ClientConstants;
import com.zt.plat.module.system.service.logger.LoginLogService;
import com.zt.plat.module.system.service.user.AdminUserService;
import jakarta.annotation.Resource;
@@ -26,7 +25,6 @@ import java.util.HashMap;
import java.util.Map;
import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.zt.plat.framework.common.util.servlet.ServletUtils.getClientIP;
import static com.zt.plat.module.system.enums.ErrorCodeConstants.*;
/**
@@ -46,9 +44,6 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service {
@Resource
private LoginLogService loginLogService;
@Resource
private OAuth2TokenService oauth2TokenService;
@Resource
private EbanTokenService ebanTokenService;
@@ -77,16 +72,16 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service {
throw exception(AUTH_LOGIN_EBAN_TOKEN_INVALID);
}
String displayName = StrUtil.trim(StrUtil.blankToDefault(userInfo.getDisplayName(), userInfo.getUsername()));
if (StrUtil.isBlank(displayName)) {
log.error("E办OAuth2用户信息缺少displayName与loginName无法匹配账号: {}", JSONUtil.toJsonStr(userInfo));
String username = StrUtil.trim(StrUtil.blankToDefault(userInfo.getLoginName(), userInfo.getUsername()));
if (StrUtil.isBlank(username)) {
log.error("E办OAuth2用户信息缺少 username 与 loginName无法匹配账号: {}", JSONUtil.toJsonStr(userInfo));
throw exception(AUTH_LOGIN_EBAN_TOKEN_INVALID);
}
AdminUserDO user = userService.getUserByUsername(displayName);
AdminUserDO user = userService.getUserByUsername(username);
if (user == null) {
createLoginLog(null, displayName, LoginLogTypeEnum.LOGIN_SOCIAL, LoginResultEnum.BAD_CREDENTIALS);
log.warn("E办OAuth2用户displayName未在系统中找到对应账号: {}", displayName);
createLoginLog(null, username, LoginLogTypeEnum.LOGIN_SOCIAL, LoginResultEnum.BAD_CREDENTIALS);
log.warn("E办OAuth2用户displayName未在系统中找到对应账号: {}", username);
throw exception(AUTH_LOGIN_EBAN_USER_NOT_SYNC);
}
@@ -95,27 +90,24 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service {
throw exception(AUTH_LOGIN_USER_DISABLED);
}
try {
EbanTokenInfo tokenInfo = userInfo.getTokenInfo();
if (tokenInfo != null && StrUtil.isNotBlank(tokenInfo.getAccessToken())) {
String userInfoJson = StrUtil.blankToDefault(userInfo.getRawUserInfoJson(), buildBasicUserInfoJson(userInfo));
Long tenantId = user.getTenantId() != null ? user.getTenantId() : 0L;
ebanTokenService.createEbanToken(
user.getId(),
tenantId,
tokenInfo.getAccessToken(),
tokenInfo.getRefreshToken(),
tokenInfo.getExpiresIn(),
userInfo.getUid(),
userInfoJson
);
log.info("成功保存E办tokenuserId={}, uid={}, displayName={}", user.getId(), userInfo.getUid(), displayName);
}
} catch (Exception e) {
log.error("保存E办token失败userId={}, uid={}, displayName={}", user.getId(), userInfo.getUid(), displayName, e);
EbanTokenInfo tokenInfo = userInfo.getTokenInfo();
if (tokenInfo == null || StrUtil.isBlank(tokenInfo.getAccessToken())) {
log.error("E办OAuth2回调缺少有效的token信息uid={}, username={}", userInfo.getUid(), username);
throw exception(AUTH_LOGIN_EBAN_TOKEN_INVALID);
}
return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL);
Long tenantId = user.getTenantId() != null ? user.getTenantId() : 0L;
OAuth2AccessTokenDO ebanAccessTokenDO = ebanTokenService.createEbanToken(
user.getId(),
tenantId,
tokenInfo.getAccessToken(),
tokenInfo.getRefreshToken(),
tokenInfo.getExpiresIn(),
userInfo
);
log.info("成功保存E办tokenuserId={}, uid={}, username={}", user.getId(), userInfo.getUid(), username);
return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL, ebanAccessTokenDO);
} catch (ServiceException e) {
throw e;
@@ -139,18 +131,6 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service {
return userInfo;
}
private String buildBasicUserInfoJson(EbanUserInfo userInfo) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("uid", userInfo.getUid());
jsonObject.put("displayName", userInfo.getDisplayName());
jsonObject.put("loginName", userInfo.getUsername());
jsonObject.put("realName", userInfo.getRealName());
jsonObject.put("email", userInfo.getEmail());
jsonObject.put("mobile", userInfo.getMobile());
jsonObject.put("deptName", userInfo.getDeptName());
return jsonObject.toString();
}
/**
* E办Token信息
*/
@@ -340,18 +320,15 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service {
/**
* 登录成功后创建Token
*/
private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) {
private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType,
OAuth2AccessTokenDO accessTokenDO) {
// 插入登陆日志
createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS);
// 创建访问令牌
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(
userId,
com.zt.plat.framework.common.enums.UserTypeEnum.ADMIN.getValue(),
OAuth2ClientConstants.CLIENT_ID_DEFAULT,
null
);
if (accessTokenDO == null) {
throw exception(AUTH_LOGIN_EBAN_TOKEN_INVALID);
}
// 构建返回结果
return AuthConvert.INSTANCE.convert(accessTokenDO);
}

View File

@@ -17,12 +17,11 @@ public interface EbanTokenService {
* @param accessToken E办访问令牌
* @param refreshToken E办刷新令牌
* @param expiresIn 过期时间(秒)
* @param uid E办用户唯一标识
* @param userInfo E办用户信息JSON格式
* @param ebanUserInfo e办用户信息
* @return OAuth2AccessTokenDO
*/
OAuth2AccessTokenDO createEbanToken(Long userId, Long tenantId, String accessToken, String refreshToken,
Integer expiresIn, String uid, String userInfo);
Integer expiresIn, EbanOAuth2Service.EbanUserInfo ebanUserInfo);
/**
* 根据用户ID获取E办Token
@@ -54,7 +53,7 @@ public interface EbanTokenService {
* @return 是否有效
*/
boolean isEbanTokenValid(Long userId);
/**
* 根据access_token获取E办Token信息
*

View File

@@ -53,19 +53,26 @@ public class EbanTokenServiceImpl implements EbanTokenService {
@Override
public OAuth2AccessTokenDO createEbanToken(Long userId, Long tenantId, String accessToken, String refreshToken,
Integer expiresIn, String uid, String userInfo) {
Integer expiresIn, EbanOAuth2Service.EbanUserInfo userInfo) {
if (StrUtil.isBlank(accessToken)) {
throw ServiceExceptionUtil.exception(AUTH_LOGIN_EBAN_TOKEN_INVALID);
}
if (userInfo == null) {
throw ServiceExceptionUtil.exception(AUTH_LOGIN_EBAN_TOKEN_INVALID);
}
LocalDateTime expiresTime = calculateExpiresTime(expiresIn);
Map<String, String> userInfoMap = MapUtil.newHashMap(2, false);
if (StrUtil.isNotBlank(uid)) {
userInfoMap.put("uid", uid);
Map<String, String> userInfoMap = MapUtil.newHashMap(6, false);
if (StrUtil.isNotBlank(userInfo.getLoginName())) {
userInfoMap.put("username", userInfo.getLoginName());
}
if (StrUtil.isNotBlank(userInfo)) {
userInfoMap.put("rawUserInfo", userInfo);
if (StrUtil.isNotBlank(userInfo.getDisplayName())) {
userInfoMap.put("nickname", userInfo.getDisplayName());
}
if (StrUtil.isNotBlank(userInfo.getUid())) {
userInfoMap.put("uid", userInfo.getUid());
}
userInfoMap.put("tenantId", String.valueOf(tenantId != null ? tenantId : 0L));
OAuth2AccessTokenDO tokenDO = oauth2AccessTokenMapper.selectByUserIdAndClientId(userId, EBAN_CLIENT_ID);
if (tokenDO == null) {
@@ -87,7 +94,7 @@ public class EbanTokenServiceImpl implements EbanTokenService {
oauth2AccessTokenMapper.updateById(tokenDO);
}
log.info("保存E办Token成功userId={}, uid={}, expiresTime={}", userId, uid, expiresTime);
log.info("保存E办Token成功userId={}, expiresTime={}", userId, expiresTime);
return tokenDO;
}

View File

@@ -372,7 +372,7 @@ public class PermissionServiceImpl implements PermissionService {
CollUtil.addAll(result.getDeptIds(), userDeptIds.get());
continue;
}
// 情况四DEPT_DEPT_AND_CHILD
// 情况四DEPT_AND_CHILD
if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_AND_CHILD.getScope())) {
Set<Long> deptIds = Optional.ofNullable(userDeptIds.get()).orElseGet(Collections::emptySet);
for (Long userDeptId : deptIds) {
@@ -382,7 +382,15 @@ public class PermissionServiceImpl implements PermissionService {
CollUtil.addAll(result.getDeptIds(), deptIds);
continue;
}
// 情况五,SELF
// 情况五,COMPANY_AND_DEPT
if (Objects.equals(role.getDataScope(), DataScopeEnum.COMPANY_AND_DEPT.getScope())) {
// 公司及所属部门数据范围:由部门服务汇总最近所属公司及其直属部门
Set<Long> deptIds = deptService.computeCompanyScopeDeptIds(Optional.ofNullable(userDeptIds.get())
.orElse(Collections.emptySet()));
CollUtil.addAll(result.getDeptIds(), deptIds);
continue;
}
// 情况六SELF
if (Objects.equals(role.getDataScope(), DataScopeEnum.SELF.getScope())) {
result.setSelf(true);
continue;

View File

@@ -10,11 +10,11 @@ import com.zt.plat.module.system.dal.dataobject.dept.UserPostDO;
import com.zt.plat.module.system.dal.dataobject.user.AdminUserDO;
import com.zt.plat.module.system.dal.dataobject.userdept.UserDeptDO;
import com.zt.plat.module.system.dal.mysql.dept.UserPostMapper;
import com.zt.plat.module.system.enums.user.UserSourceEnum;
import com.zt.plat.module.system.service.dept.PostService;
import com.zt.plat.module.system.service.user.AdminUserService;
import com.zt.plat.module.system.service.userdept.UserDeptService;
import com.zt.plat.module.system.util.sync.SyncVerifyUtil;
import com.zt.plat.module.system.enums.user.UserSourceEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -55,7 +55,7 @@ public class UserSyncServiceImpl implements UserSyncService {
Long tenantId = Optional.ofNullable(loginUser).orElse(new LoginUser()).getTenantId();
TenantContextHolder.setTenantId(tenantId);
// 中铝 e 办不会设置密码,设置默认密码
saveReqVO.setPassword("ZLEB");
saveReqVO.setPassword("Zgty@9527");
// 设置为同步用户
saveReqVO.setUserSource(UserSourceEnum.SYNC.getSource());
Long userId = adminUserService.createUser(saveReqVO);

View File

@@ -328,7 +328,9 @@ public class AdminUserServiceImpl implements AdminUserService {
permissionService.getUserRoleIdListByRoleId(singleton(reqVO.getRoleId())) : null;
// 分页查询
return userMapper.selectPage(reqVO, getDeptCondition(reqVO.getDeptId()), userIds);
PageResult<AdminUserDO> pageResult = userMapper.selectPage(reqVO, getDeptCondition(reqVO.getDeptId()), userIds);
fillUserDeptInfo(pageResult.getList());
return pageResult;
}
@Override
@@ -338,7 +340,15 @@ public class AdminUserServiceImpl implements AdminUserService {
adminUserDO.setDeptIds(companyDeptInfoListByUserId.stream().map(CompanyDeptInfo::getDeptId).collect(Collectors.toSet()));
adminUserDO.setCompanyIds(companyDeptInfoListByUserId.stream().map(CompanyDeptInfo::getCompanyId).collect(Collectors.toSet()));
adminUserDO.setCompanyDeptInfos(companyDeptInfoListByUserId);
// 设置用户的部门名称集合
String deptNames = companyDeptInfoListByUserId.stream()
.map(CompanyDeptInfo::getDeptName)
.filter(StrUtil::isNotBlank)
.distinct()
.collect(Collectors.joining(""));
adminUserDO.setDeptNames(StrUtil.blankToDefault(deptNames, "-"));
if (CollUtil.isEmpty(adminUserDO.getDeptIds())) {
adminUserDO.setDeptIds(Collections.emptySet());
}
return adminUserDO;
}
@@ -416,6 +426,48 @@ public class AdminUserServiceImpl implements AdminUserService {
return deptIds;
}
private void fillUserDeptInfo(List<AdminUserDO> users) {
if (CollUtil.isEmpty(users)) {
return;
}
Map<Long, AdminUserDO> userMap = CollectionUtils.convertMap(users, AdminUserDO::getId);
userMap.values().forEach(user -> {
user.setDeptIds(Collections.emptySet());
user.setDeptNames("-");
});
List<UserDeptDO> relations = userDeptService.getValidUserDeptListByUserIds(userMap.keySet());
if (CollUtil.isEmpty(relations)) {
return;
}
Map<Long, LinkedHashSet<Long>> userDeptIdsMap = new HashMap<>();
relations.forEach(relation ->
userDeptIdsMap.computeIfAbsent(relation.getUserId(), key -> new LinkedHashSet<>())
.add(relation.getDeptId()));
Set<Long> allDeptIds = userDeptIdsMap.values().stream()
.flatMap(Collection::stream)
.collect(Collectors.toCollection(LinkedHashSet::new));
Map<Long, DeptDO> deptMap = allDeptIds.isEmpty() ? Collections.emptyMap() : deptService.getDeptMap(allDeptIds);
userDeptIdsMap.forEach((userId, deptIds) -> {
AdminUserDO user = userMap.get(userId);
if (user == null) {
return;
}
user.setDeptIds(deptIds);
String deptNames = deptIds.stream()
.map(deptMap::get)
.filter(Objects::nonNull)
.map(DeptDO::getName)
.filter(StrUtil::isNotBlank)
.distinct()
.collect(Collectors.joining(""));
user.setDeptNames(StrUtil.blankToDefault(deptNames, "-"));
});
}
private AdminUserDO validateUserForCreateOrUpdate(Long id, String username, String mobile, String email,
Set<Long> deptIds, Set<Long> postIds) {
// 关闭数据权限,避免因为没有数据权限,查询不到数据,进而导致唯一校验不正确

View File

@@ -16,6 +16,7 @@ import com.zt.plat.module.system.enums.sms.SmsSceneEnum;
import com.zt.plat.module.system.enums.social.SocialTypeEnum;
import com.zt.plat.module.system.service.logger.LoginLogService;
import com.zt.plat.module.system.service.member.MemberService;
import com.zt.plat.module.system.service.oauth2.EbanOAuth2Service;
import com.zt.plat.module.system.service.oauth2.OAuth2TokenService;
import com.zt.plat.module.system.service.social.SocialUserService;
import com.zt.plat.module.system.service.user.AdminUserService;
@@ -60,6 +61,8 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest {
private OAuth2TokenService oauth2TokenService;
@MockBean
private MemberService memberService;
@MockBean
private EbanOAuth2Service ebanOAuth2Service;
@MockBean
private Validator validator;

View File

@@ -457,7 +457,7 @@ public class SocialClientServiceImplTest extends BaseDbUnitTest {
reqVO.setName("");
reqVO.setSocialType(SocialTypeEnum.GITEE.getType());
reqVO.setUserType(UserTypeEnum.ADMIN.getValue());
reqVO.setClientId("yu");
reqVO.setClientId("z");
reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
// 调用

View File

@@ -12,7 +12,9 @@ create table IF NOT EXISTS system_user_dept (
);
CREATE TABLE IF NOT EXISTS "system_dept" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"code" varchar(64) NOT NULL DEFAULT '',
"name" varchar(30) NOT NULL DEFAULT '',
"short_name" varchar(30) DEFAULT '',
"parent_id" bigint NOT NULL DEFAULT '0',
"sort" int NOT NULL DEFAULT '0',
"leader_user_id" bigint DEFAULT NULL,