feat: 搭建后端多租户名片服务
- 初始化 Spring Boot 多模块工程与通用基础能力 - 增加租户、组织、用户、名片、文件、统计等业务模块 - 补充数据库迁移脚本与基础测试
This commit is contained in:
35
backend/easycard-module-user/pom.xml
Normal file
35
backend/easycard-module-user/pom.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.easycard</groupId>
|
||||
<artifactId>easycard-parent</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>easycard-module-user</artifactId>
|
||||
<name>easycard-module-user</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.easycard</groupId>
|
||||
<artifactId>easycard-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easycard</groupId>
|
||||
<artifactId>easycard-module-org</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.easycard.module.user.controller;
|
||||
|
||||
import com.easycard.common.api.ApiResponse;
|
||||
import com.easycard.common.auth.SecurityUtils;
|
||||
import com.easycard.common.web.ClientRequestUtils;
|
||||
import com.easycard.module.user.service.AuthService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
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;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth")
|
||||
public class AuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
|
||||
public AuthController(AuthService authService) {
|
||||
this.authService = authService;
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public ApiResponse<AuthService.LoginResult> login(@Valid @RequestBody LoginRequest request, HttpServletRequest servletRequest) {
|
||||
return ApiResponse.success(authService.login(
|
||||
request.username(),
|
||||
request.password(),
|
||||
ClientRequestUtils.getClientIp(servletRequest),
|
||||
servletRequest.getHeader("User-Agent")
|
||||
));
|
||||
}
|
||||
|
||||
@PostMapping("/logout")
|
||||
public ApiResponse<Void> logout() {
|
||||
return ApiResponse.success("退出成功", null);
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public ApiResponse<AuthService.CurrentUserView> me() {
|
||||
return ApiResponse.success(authService.getCurrentUser());
|
||||
}
|
||||
|
||||
@PostMapping("/change-password")
|
||||
public ApiResponse<Void> changePassword(@Valid @RequestBody ChangePasswordRequest request) {
|
||||
authService.changePassword(request.oldPassword(), request.newPassword());
|
||||
return ApiResponse.success("修改成功", null);
|
||||
}
|
||||
}
|
||||
|
||||
record LoginRequest(
|
||||
@NotBlank(message = "账号不能为空") String username,
|
||||
@NotBlank(message = "密码不能为空") String password
|
||||
) {
|
||||
}
|
||||
|
||||
record ChangePasswordRequest(
|
||||
@NotBlank(message = "原密码不能为空") String oldPassword,
|
||||
@NotBlank(message = "新密码不能为空") String newPassword
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.easycard.module.user.controller;
|
||||
|
||||
import com.easycard.common.api.ApiResponse;
|
||||
import com.easycard.module.user.service.TenantUserService;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/tenant/users")
|
||||
public class TenantUserController {
|
||||
|
||||
private final TenantUserService tenantUserService;
|
||||
|
||||
public TenantUserController(TenantUserService tenantUserService) {
|
||||
this.tenantUserService = tenantUserService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||
public ApiResponse<List<TenantUserService.TenantUserView>> listUsers(@RequestParam(required = false) String keyword) {
|
||||
return ApiResponse.success(tenantUserService.listUsers(keyword));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||
public ApiResponse<TenantUserService.TenantUserView> createUser(@Valid @RequestBody UpsertTenantUserRequest request) {
|
||||
return ApiResponse.success(tenantUserService.createUser(request.toServiceRequest()));
|
||||
}
|
||||
|
||||
@PutMapping("/{userId}")
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||
public ApiResponse<TenantUserService.TenantUserView> updateUser(@PathVariable Long userId, @Valid @RequestBody UpsertTenantUserRequest request) {
|
||||
return ApiResponse.success(tenantUserService.updateUser(userId, request.toServiceRequest()));
|
||||
}
|
||||
|
||||
@PatchMapping("/{userId}/status")
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||
public ApiResponse<TenantUserService.TenantUserView> updateUserStatus(@PathVariable Long userId, @Valid @RequestBody UpdateTenantUserStatusRequest request) {
|
||||
return ApiResponse.success(tenantUserService.updateUserStatus(userId, request.userStatus()));
|
||||
}
|
||||
|
||||
@PutMapping("/sort")
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||
public ApiResponse<Void> sortUsers(@Valid @RequestBody SortTenantUsersRequest request) {
|
||||
tenantUserService.sortUsers(request.userIds());
|
||||
return ApiResponse.success("排序已生效", null);
|
||||
}
|
||||
|
||||
@PostMapping("/{userId}/reset-password")
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','PLATFORM_SUPER_ADMIN')")
|
||||
public ApiResponse<Void> resetPassword(@PathVariable Long userId) {
|
||||
tenantUserService.resetPassword(userId);
|
||||
return ApiResponse.success("重置成功", null);
|
||||
}
|
||||
|
||||
@GetMapping("/{userId}")
|
||||
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN','TENANT_USER','PLATFORM_SUPER_ADMIN')")
|
||||
public ApiResponse<TenantUserService.TenantUserView> getUser(@PathVariable Long userId) {
|
||||
return ApiResponse.success(tenantUserService.getUser(userId));
|
||||
}
|
||||
}
|
||||
|
||||
record UpsertTenantUserRequest(
|
||||
@NotBlank(message = "账号不能为空") String username,
|
||||
@NotBlank(message = "姓名不能为空") String realName,
|
||||
String mobile,
|
||||
@Email(message = "邮箱格式错误") String email,
|
||||
Long deptId,
|
||||
String jobTitle,
|
||||
@NotBlank(message = "状态不能为空") String userStatus,
|
||||
@NotBlank(message = "角色不能为空") String roleCode
|
||||
) {
|
||||
TenantUserService.CreateOrUpdateUserRequest toServiceRequest() {
|
||||
return new TenantUserService.CreateOrUpdateUserRequest(
|
||||
username,
|
||||
realName,
|
||||
mobile,
|
||||
email,
|
||||
deptId,
|
||||
jobTitle,
|
||||
userStatus,
|
||||
roleCode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
record UpdateTenantUserStatusRequest(
|
||||
@NotBlank(message = "状态不能为空") String userStatus
|
||||
) {
|
||||
}
|
||||
|
||||
record SortTenantUsersRequest(
|
||||
@Size(min = 1, message = "排序成员不能为空") List<@NotNull(message = "成员ID不能为空") Long> userIds
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.easycard.module.user.dal.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("sys_login_log")
|
||||
public class SysLoginLogDO {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private Long userId;
|
||||
private String userType;
|
||||
private String loginType;
|
||||
private String loginStatus;
|
||||
private String clientIp;
|
||||
private String userAgent;
|
||||
private String failReason;
|
||||
private LocalDateTime loginAt;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.easycard.module.user.dal.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("sys_operation_log")
|
||||
public class SysOperationLogDO {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private Long userId;
|
||||
private String userName;
|
||||
private String moduleName;
|
||||
private String bizType;
|
||||
private Long bizId;
|
||||
private String operationType;
|
||||
private String requestMethod;
|
||||
private String requestUri;
|
||||
private String requestBody;
|
||||
private String responseCode;
|
||||
private String responseMessage;
|
||||
private String clientIp;
|
||||
private LocalDateTime operatedAt;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.easycard.module.user.dal.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("sys_role")
|
||||
public class SysRoleDO {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private String roleScope;
|
||||
private String roleCode;
|
||||
private String roleName;
|
||||
private String dataScope;
|
||||
private Integer isBuiltin;
|
||||
private String roleStatus;
|
||||
private String remark;
|
||||
private Long createdBy;
|
||||
private LocalDateTime createdTime;
|
||||
private Long updatedBy;
|
||||
private LocalDateTime updatedTime;
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.easycard.module.user.dal.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("sys_user")
|
||||
public class SysUserDO {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private String userType;
|
||||
private String username;
|
||||
private String passwordHash;
|
||||
private String realName;
|
||||
private String nickName;
|
||||
private String gender;
|
||||
private String mobile;
|
||||
private String email;
|
||||
private Long avatarAssetId;
|
||||
private Long deptId;
|
||||
private String jobTitle;
|
||||
private String userStatus;
|
||||
private Integer memberSort;
|
||||
private Integer mustUpdatePassword;
|
||||
private LocalDateTime lastLoginAt;
|
||||
private String lastLoginIp;
|
||||
private String remark;
|
||||
private Long createdBy;
|
||||
private LocalDateTime createdTime;
|
||||
private Long updatedBy;
|
||||
private LocalDateTime updatedTime;
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.easycard.module.user.dal.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("sys_user_role")
|
||||
public class SysUserRoleDO {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private Long userId;
|
||||
private Long roleId;
|
||||
private Long createdBy;
|
||||
private LocalDateTime createdTime;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.easycard.module.user.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.easycard.module.user.dal.entity.SysLoginLogDO;
|
||||
|
||||
public interface SysLoginLogMapper extends BaseMapper<SysLoginLogDO> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.easycard.module.user.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.easycard.module.user.dal.entity.SysOperationLogDO;
|
||||
|
||||
public interface SysOperationLogMapper extends BaseMapper<SysOperationLogDO> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.easycard.module.user.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.easycard.module.user.dal.entity.SysRoleDO;
|
||||
|
||||
public interface SysRoleMapper extends BaseMapper<SysRoleDO> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.easycard.module.user.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.easycard.module.user.dal.entity.SysUserDO;
|
||||
|
||||
public interface SysUserMapper extends BaseMapper<SysUserDO> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.easycard.module.user.dal.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.easycard.module.user.dal.entity.SysUserRoleDO;
|
||||
|
||||
public interface SysUserRoleMapper extends BaseMapper<SysUserRoleDO> {
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package com.easycard.module.user.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.easycard.common.auth.JwtTokenService;
|
||||
import com.easycard.common.auth.LoginUser;
|
||||
import com.easycard.common.auth.SecurityUtils;
|
||||
import com.easycard.common.exception.BusinessException;
|
||||
import com.easycard.module.org.dal.entity.OrgDepartmentDO;
|
||||
import com.easycard.module.org.dal.mapper.OrgDepartmentMapper;
|
||||
import com.easycard.module.user.dal.entity.SysRoleDO;
|
||||
import com.easycard.module.user.dal.entity.SysUserDO;
|
||||
import com.easycard.module.user.dal.entity.SysUserRoleDO;
|
||||
import com.easycard.module.user.dal.mapper.SysRoleMapper;
|
||||
import com.easycard.module.user.dal.mapper.SysUserMapper;
|
||||
import com.easycard.module.user.dal.mapper.SysUserRoleMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AuthService {
|
||||
|
||||
private final SysUserMapper sysUserMapper;
|
||||
private final SysRoleMapper sysRoleMapper;
|
||||
private final SysUserRoleMapper sysUserRoleMapper;
|
||||
private final OrgDepartmentMapper orgDepartmentMapper;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtTokenService jwtTokenService;
|
||||
private final UserAuditService userAuditService;
|
||||
|
||||
@Value("${easycard.security.jwt-expire-hours:24}")
|
||||
private long jwtExpireHours;
|
||||
|
||||
@Transactional
|
||||
public LoginResult login(String username, String password, String clientIp, String userAgent) {
|
||||
SysUserDO user = sysUserMapper.selectOne(Wrappers.<SysUserDO>lambdaQuery()
|
||||
.eq(SysUserDO::getUsername, username)
|
||||
.eq(SysUserDO::getDeleted, 0)
|
||||
.last("LIMIT 1"));
|
||||
if (user == null) {
|
||||
userAuditService.recordLogin(0L, null, "UNKNOWN", "FAIL", clientIp, userAgent, "账号不存在");
|
||||
throw new BusinessException("AUTH_FAILED", "账号或密码错误");
|
||||
}
|
||||
if (!"ENABLED".equals(user.getUserStatus())) {
|
||||
userAuditService.recordLogin(user.getTenantId(), user.getId(), user.getUserType(), "FAIL", clientIp, userAgent, "账号已停用");
|
||||
throw new BusinessException("AUTH_FAILED", "账号已停用");
|
||||
}
|
||||
if (!passwordEncoder.matches(password, user.getPasswordHash())) {
|
||||
userAuditService.recordLogin(user.getTenantId(), user.getId(), user.getUserType(), "FAIL", clientIp, userAgent, "密码错误");
|
||||
throw new BusinessException("AUTH_FAILED", "账号或密码错误");
|
||||
}
|
||||
|
||||
List<String> roleCodes = getRoleCodes(user.getId(), user.getTenantId());
|
||||
LoginUser loginUser = new LoginUser(
|
||||
user.getId(),
|
||||
user.getTenantId(),
|
||||
user.getUsername(),
|
||||
user.getRealName(),
|
||||
user.getUserType(),
|
||||
roleCodes
|
||||
);
|
||||
user.setLastLoginAt(LocalDateTime.now());
|
||||
user.setLastLoginIp(clientIp);
|
||||
user.setUpdatedBy(user.getId());
|
||||
sysUserMapper.updateById(user);
|
||||
userAuditService.recordLogin(user.getTenantId(), user.getId(), user.getUserType(), "SUCCESS", clientIp, userAgent, null);
|
||||
return new LoginResult(jwtTokenService.generateToken(loginUser, Duration.ofHours(jwtExpireHours)), buildCurrentUser(user, roleCodes));
|
||||
}
|
||||
|
||||
public CurrentUserView getCurrentUser() {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
SysUserDO user = getRequiredUser(loginUser.userId());
|
||||
return buildCurrentUser(user, loginUser.roleCodes());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void changePassword(String oldPassword, String newPassword) {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
SysUserDO user = getRequiredUser(loginUser.userId());
|
||||
if (!passwordEncoder.matches(oldPassword, user.getPasswordHash())) {
|
||||
throw new BusinessException("PASSWORD_INVALID", "原密码错误");
|
||||
}
|
||||
user.setPasswordHash(passwordEncoder.encode(newPassword));
|
||||
user.setMustUpdatePassword(0);
|
||||
user.setUpdatedBy(user.getId());
|
||||
sysUserMapper.updateById(user);
|
||||
userAuditService.recordOperation(user.getTenantId(), user.getId(), user.getRealName(), "AUTH", "PASSWORD", user.getId(), "CHANGE_PASSWORD");
|
||||
}
|
||||
|
||||
public SysUserDO getRequiredUser(Long userId) {
|
||||
SysUserDO user = sysUserMapper.selectById(userId);
|
||||
if (user == null || Integer.valueOf(1).equals(user.getDeleted())) {
|
||||
throw new BusinessException("USER_NOT_FOUND", "用户不存在");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
public List<String> getRoleCodes(Long userId, Long tenantId) {
|
||||
List<SysUserRoleDO> userRoles = sysUserRoleMapper.selectList(Wrappers.<SysUserRoleDO>lambdaQuery()
|
||||
.eq(SysUserRoleDO::getUserId, userId)
|
||||
.eq(SysUserRoleDO::getTenantId, tenantId));
|
||||
if (userRoles.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<Long> roleIds = userRoles.stream().map(SysUserRoleDO::getRoleId).toList();
|
||||
return sysRoleMapper.selectList(Wrappers.<SysRoleDO>lambdaQuery()
|
||||
.in(SysRoleDO::getId, roleIds)
|
||||
.eq(SysRoleDO::getDeleted, 0)
|
||||
.eq(SysRoleDO::getRoleStatus, "ENABLED"))
|
||||
.stream()
|
||||
.map(SysRoleDO::getRoleCode)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private CurrentUserView buildCurrentUser(SysUserDO user, List<String> roleCodes) {
|
||||
String tenantName = "平台";
|
||||
if (user.getTenantId() != null && user.getTenantId() > 0) {
|
||||
tenantName = resolveTenantName(user.getTenantId());
|
||||
}
|
||||
String roleName = roleCodes.isEmpty() ? "未分配角色" : roleCodes.get(0);
|
||||
String deptName = "";
|
||||
if (user.getDeptId() != null) {
|
||||
OrgDepartmentDO department = orgDepartmentMapper.selectById(user.getDeptId());
|
||||
deptName = department == null ? "" : department.getDeptName();
|
||||
}
|
||||
return new CurrentUserView(
|
||||
user.getId(),
|
||||
user.getTenantId(),
|
||||
user.getUsername(),
|
||||
user.getRealName(),
|
||||
user.getUserType(),
|
||||
tenantName,
|
||||
roleName,
|
||||
roleCodes,
|
||||
deptName,
|
||||
user.getJobTitle(),
|
||||
user.getMustUpdatePassword() != null && user.getMustUpdatePassword() == 1
|
||||
);
|
||||
}
|
||||
|
||||
private String resolveTenantName(Long tenantId) {
|
||||
List<String> tenantNames = jdbcTemplate.query(
|
||||
"""
|
||||
SELECT tenant_name
|
||||
FROM sys_tenant
|
||||
WHERE id = ?
|
||||
AND deleted = 0
|
||||
LIMIT 1
|
||||
""",
|
||||
(rs, rowNum) -> rs.getString("tenant_name"),
|
||||
tenantId
|
||||
);
|
||||
String tenantName = tenantNames.isEmpty() ? "" : tenantNames.getFirst();
|
||||
return StringUtils.hasText(tenantName) ? tenantName : "当前租户";
|
||||
}
|
||||
|
||||
public record LoginResult(String accessToken, CurrentUserView currentUser) {
|
||||
}
|
||||
|
||||
public record CurrentUserView(
|
||||
Long id,
|
||||
Long tenantId,
|
||||
String username,
|
||||
String realName,
|
||||
String userType,
|
||||
String tenantName,
|
||||
String roleName,
|
||||
List<String> roleCodes,
|
||||
String deptName,
|
||||
String jobTitle,
|
||||
boolean mustUpdatePassword
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
package com.easycard.module.user.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.easycard.common.auth.LoginUser;
|
||||
import com.easycard.common.auth.SecurityUtils;
|
||||
import com.easycard.common.exception.BusinessException;
|
||||
import com.easycard.module.org.dal.entity.OrgDepartmentDO;
|
||||
import com.easycard.module.org.dal.mapper.OrgDepartmentMapper;
|
||||
import com.easycard.module.user.dal.entity.SysRoleDO;
|
||||
import com.easycard.module.user.dal.entity.SysUserDO;
|
||||
import com.easycard.module.user.dal.entity.SysUserRoleDO;
|
||||
import com.easycard.module.user.dal.mapper.SysRoleMapper;
|
||||
import com.easycard.module.user.dal.mapper.SysUserMapper;
|
||||
import com.easycard.module.user.dal.mapper.SysUserRoleMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TenantUserService {
|
||||
|
||||
private final SysUserMapper sysUserMapper;
|
||||
private final SysRoleMapper sysRoleMapper;
|
||||
private final SysUserRoleMapper sysUserRoleMapper;
|
||||
private final OrgDepartmentMapper orgDepartmentMapper;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final UserAuditService userAuditService;
|
||||
|
||||
public List<TenantUserView> listUsers(String keyword) {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
Long tenantId = loginUser.tenantId();
|
||||
List<SysUserDO> users = sysUserMapper.selectList(Wrappers.<SysUserDO>lambdaQuery()
|
||||
.eq(SysUserDO::getTenantId, tenantId)
|
||||
.eq(SysUserDO::getDeleted, 0)
|
||||
.like(StringUtils.hasText(keyword), SysUserDO::getRealName, keyword)
|
||||
.orderByAsc(SysUserDO::getMemberSort)
|
||||
.orderByDesc(SysUserDO::getId));
|
||||
if (users.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Map<Long, OrgDepartmentDO> deptMap = orgDepartmentMapper.selectList(Wrappers.<OrgDepartmentDO>lambdaQuery()
|
||||
.eq(OrgDepartmentDO::getTenantId, tenantId)
|
||||
.eq(OrgDepartmentDO::getDeleted, 0))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(OrgDepartmentDO::getId, Function.identity()));
|
||||
Map<Long, String> roleCodeMap = loadPrimaryRoleMap(users, tenantId);
|
||||
return users.stream().map(user -> new TenantUserView(
|
||||
user.getId(),
|
||||
user.getUsername(),
|
||||
user.getRealName(),
|
||||
user.getMobile(),
|
||||
user.getEmail(),
|
||||
user.getJobTitle(),
|
||||
user.getUserStatus(),
|
||||
user.getMemberSort(),
|
||||
user.getDeptId(),
|
||||
deptMap.containsKey(user.getDeptId()) ? deptMap.get(user.getDeptId()).getDeptName() : "",
|
||||
roleCodeMap.getOrDefault(user.getId(), "")
|
||||
)).toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public TenantUserView createUser(CreateOrUpdateUserRequest request) {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
validateDepartment(loginUser.tenantId(), request.deptId());
|
||||
SysUserDO existedUser = sysUserMapper.selectOne(Wrappers.<SysUserDO>lambdaQuery()
|
||||
.eq(SysUserDO::getTenantId, loginUser.tenantId())
|
||||
.eq(SysUserDO::getUsername, request.username())
|
||||
.eq(SysUserDO::getDeleted, 0)
|
||||
.last("LIMIT 1"));
|
||||
if (existedUser != null) {
|
||||
throw new BusinessException("USERNAME_DUPLICATED", "登录账号已存在");
|
||||
}
|
||||
validateUserStatus(request.userStatus());
|
||||
SysRoleDO role = getRoleByCode(loginUser.tenantId(), request.roleCode());
|
||||
SysUserDO user = new SysUserDO();
|
||||
user.setTenantId(loginUser.tenantId());
|
||||
user.setUserType("TENANT");
|
||||
user.setUsername(request.username());
|
||||
user.setPasswordHash(passwordEncoder.encode("123456"));
|
||||
user.setRealName(request.realName());
|
||||
user.setNickName(request.realName());
|
||||
user.setGender("UNKNOWN");
|
||||
user.setMobile(request.mobile());
|
||||
user.setEmail(request.email());
|
||||
user.setDeptId(request.deptId());
|
||||
user.setJobTitle(request.jobTitle());
|
||||
user.setUserStatus(request.userStatus());
|
||||
user.setMemberSort(nextMemberSort(loginUser.tenantId()));
|
||||
user.setMustUpdatePassword(1);
|
||||
user.setCreatedBy(loginUser.userId());
|
||||
user.setUpdatedBy(loginUser.userId());
|
||||
user.setDeleted(0);
|
||||
sysUserMapper.insert(user);
|
||||
|
||||
SysUserRoleDO userRole = new SysUserRoleDO();
|
||||
userRole.setTenantId(loginUser.tenantId());
|
||||
userRole.setUserId(user.getId());
|
||||
userRole.setRoleId(role.getId());
|
||||
userRole.setCreatedBy(loginUser.userId());
|
||||
sysUserRoleMapper.insert(userRole);
|
||||
userAuditService.recordOperation(loginUser.tenantId(), loginUser.userId(), loginUser.realName(), "USER", "SYS_USER", user.getId(), "CREATE");
|
||||
return getUser(user.getId());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public TenantUserView updateUser(Long userId, CreateOrUpdateUserRequest request) {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
SysUserDO user = getTenantUser(userId, loginUser.tenantId());
|
||||
validateDepartment(loginUser.tenantId(), request.deptId());
|
||||
validateUserStatus(request.userStatus());
|
||||
SysRoleDO role = getRoleByCode(loginUser.tenantId(), request.roleCode());
|
||||
user.setRealName(request.realName());
|
||||
user.setNickName(request.realName());
|
||||
user.setMobile(request.mobile());
|
||||
user.setEmail(request.email());
|
||||
user.setDeptId(request.deptId());
|
||||
user.setJobTitle(request.jobTitle());
|
||||
user.setUserStatus(request.userStatus());
|
||||
user.setUpdatedBy(loginUser.userId());
|
||||
sysUserMapper.updateById(user);
|
||||
|
||||
sysUserRoleMapper.delete(Wrappers.<SysUserRoleDO>lambdaQuery()
|
||||
.eq(SysUserRoleDO::getTenantId, loginUser.tenantId())
|
||||
.eq(SysUserRoleDO::getUserId, userId));
|
||||
SysUserRoleDO userRole = new SysUserRoleDO();
|
||||
userRole.setTenantId(loginUser.tenantId());
|
||||
userRole.setUserId(user.getId());
|
||||
userRole.setRoleId(role.getId());
|
||||
userRole.setCreatedBy(loginUser.userId());
|
||||
sysUserRoleMapper.insert(userRole);
|
||||
userAuditService.recordOperation(loginUser.tenantId(), loginUser.userId(), loginUser.realName(), "USER", "SYS_USER", user.getId(), "UPDATE");
|
||||
return getUser(userId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public TenantUserView updateUserStatus(Long userId, String userStatus) {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
validateUserStatus(userStatus);
|
||||
SysUserDO user = getTenantUser(userId, loginUser.tenantId());
|
||||
if (userStatus.equals(user.getUserStatus())) {
|
||||
return getUser(userId);
|
||||
}
|
||||
user.setUserStatus(userStatus);
|
||||
user.setUpdatedBy(loginUser.userId());
|
||||
sysUserMapper.updateById(user);
|
||||
userAuditService.recordOperation(
|
||||
loginUser.tenantId(),
|
||||
loginUser.userId(),
|
||||
loginUser.realName(),
|
||||
"USER",
|
||||
"SYS_USER",
|
||||
user.getId(),
|
||||
"UPDATE_STATUS"
|
||||
);
|
||||
return getUser(userId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void sortUsers(List<Long> userIds) {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
if (userIds == null || userIds.isEmpty()) {
|
||||
throw new BusinessException("USER_SORT_INVALID", "排序数据不能为空");
|
||||
}
|
||||
Set<Long> uniqueUserIds = new HashSet<>(userIds);
|
||||
if (uniqueUserIds.size() != userIds.size()) {
|
||||
throw new BusinessException("USER_SORT_INVALID", "排序数据存在重复成员");
|
||||
}
|
||||
List<SysUserDO> tenantUsers = sysUserMapper.selectList(Wrappers.<SysUserDO>lambdaQuery()
|
||||
.eq(SysUserDO::getTenantId, loginUser.tenantId())
|
||||
.eq(SysUserDO::getDeleted, 0)
|
||||
.select(SysUserDO::getId));
|
||||
Set<Long> expectedUserIds = tenantUsers.stream().map(SysUserDO::getId).collect(Collectors.toSet());
|
||||
if (expectedUserIds.size() != uniqueUserIds.size() || !expectedUserIds.equals(uniqueUserIds)) {
|
||||
throw new BusinessException("USER_SORT_INVALID", "排序数据必须包含当前租户全部成员");
|
||||
}
|
||||
Map<Long, SysUserDO> userMap = sysUserMapper.selectBatchIds(userIds).stream()
|
||||
.filter(item -> loginUser.tenantId().equals(item.getTenantId()) && Integer.valueOf(0).equals(item.getDeleted()))
|
||||
.collect(Collectors.toMap(SysUserDO::getId, Function.identity()));
|
||||
for (int i = 0; i < userIds.size(); i++) {
|
||||
Long sortedUserId = userIds.get(i);
|
||||
SysUserDO user = userMap.get(sortedUserId);
|
||||
if (user == null) {
|
||||
throw new BusinessException("USER_SORT_INVALID", "成员不存在或不属于当前租户");
|
||||
}
|
||||
user.setMemberSort((i + 1) * 10);
|
||||
user.setUpdatedBy(loginUser.userId());
|
||||
sysUserMapper.updateById(user);
|
||||
}
|
||||
userAuditService.recordOperation(
|
||||
loginUser.tenantId(),
|
||||
loginUser.userId(),
|
||||
loginUser.realName(),
|
||||
"USER",
|
||||
"SYS_USER",
|
||||
0L,
|
||||
"SORT"
|
||||
);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void resetPassword(Long userId) {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
SysUserDO user = getTenantUser(userId, loginUser.tenantId());
|
||||
user.setPasswordHash(passwordEncoder.encode("123456"));
|
||||
user.setMustUpdatePassword(1);
|
||||
user.setUpdatedBy(loginUser.userId());
|
||||
sysUserMapper.updateById(user);
|
||||
userAuditService.recordOperation(loginUser.tenantId(), loginUser.userId(), loginUser.realName(), "USER", "SYS_USER", user.getId(), "RESET_PASSWORD");
|
||||
}
|
||||
|
||||
public TenantUserView getUser(Long userId) {
|
||||
LoginUser loginUser = SecurityUtils.getRequiredLoginUser();
|
||||
SysUserDO user = getTenantUser(userId, loginUser.tenantId());
|
||||
Map<Long, String> roleMap = loadPrimaryRoleMap(List.of(user), loginUser.tenantId());
|
||||
String deptName = "";
|
||||
if (user.getDeptId() != null) {
|
||||
OrgDepartmentDO department = orgDepartmentMapper.selectById(user.getDeptId());
|
||||
deptName = department == null ? "" : department.getDeptName();
|
||||
}
|
||||
return new TenantUserView(
|
||||
user.getId(),
|
||||
user.getUsername(),
|
||||
user.getRealName(),
|
||||
user.getMobile(),
|
||||
user.getEmail(),
|
||||
user.getJobTitle(),
|
||||
user.getUserStatus(),
|
||||
user.getMemberSort(),
|
||||
user.getDeptId(),
|
||||
deptName,
|
||||
roleMap.getOrDefault(user.getId(), "")
|
||||
);
|
||||
}
|
||||
|
||||
private Map<Long, String> loadPrimaryRoleMap(List<SysUserDO> users, Long tenantId) {
|
||||
List<Long> userIds = users.stream().map(SysUserDO::getId).toList();
|
||||
List<SysUserRoleDO> userRoles = sysUserRoleMapper.selectList(Wrappers.<SysUserRoleDO>lambdaQuery()
|
||||
.eq(SysUserRoleDO::getTenantId, tenantId)
|
||||
.in(SysUserRoleDO::getUserId, userIds));
|
||||
if (userRoles.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
List<Long> roleIds = userRoles.stream().map(SysUserRoleDO::getRoleId).distinct().toList();
|
||||
Map<Long, SysRoleDO> roleMap = sysRoleMapper.selectList(Wrappers.<SysRoleDO>lambdaQuery()
|
||||
.in(SysRoleDO::getId, roleIds)
|
||||
.eq(SysRoleDO::getDeleted, 0))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(SysRoleDO::getId, Function.identity()));
|
||||
return userRoles.stream()
|
||||
.sorted(Comparator.comparing(SysUserRoleDO::getRoleId))
|
||||
.collect(Collectors.toMap(
|
||||
SysUserRoleDO::getUserId,
|
||||
item -> roleMap.containsKey(item.getRoleId()) ? roleMap.get(item.getRoleId()).getRoleCode() : "",
|
||||
(left, right) -> left
|
||||
));
|
||||
}
|
||||
|
||||
private void validateDepartment(Long tenantId, Long deptId) {
|
||||
if (deptId == null) {
|
||||
return;
|
||||
}
|
||||
OrgDepartmentDO department = orgDepartmentMapper.selectById(deptId);
|
||||
if (department == null || Integer.valueOf(1).equals(department.getDeleted()) || !tenantId.equals(department.getTenantId())) {
|
||||
throw new BusinessException("DEPARTMENT_NOT_FOUND", "所属组织不存在");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateUserStatus(String userStatus) {
|
||||
if (!"ENABLED".equals(userStatus) && !"DISABLED".equals(userStatus)) {
|
||||
throw new BusinessException("USER_STATUS_INVALID", "成员状态仅支持启用或停用");
|
||||
}
|
||||
}
|
||||
|
||||
private Integer nextMemberSort(Long tenantId) {
|
||||
SysUserDO lastUser = sysUserMapper.selectOne(Wrappers.<SysUserDO>lambdaQuery()
|
||||
.eq(SysUserDO::getTenantId, tenantId)
|
||||
.eq(SysUserDO::getDeleted, 0)
|
||||
.orderByDesc(SysUserDO::getMemberSort)
|
||||
.orderByDesc(SysUserDO::getId)
|
||||
.last("LIMIT 1"));
|
||||
int baseSort = lastUser == null || lastUser.getMemberSort() == null ? 0 : lastUser.getMemberSort();
|
||||
return baseSort + 10;
|
||||
}
|
||||
|
||||
private SysRoleDO getRoleByCode(Long tenantId, String roleCode) {
|
||||
SysRoleDO role = sysRoleMapper.selectOne(Wrappers.<SysRoleDO>lambdaQuery()
|
||||
.eq(SysRoleDO::getTenantId, tenantId)
|
||||
.eq(SysRoleDO::getRoleCode, roleCode)
|
||||
.eq(SysRoleDO::getDeleted, 0)
|
||||
.last("LIMIT 1"));
|
||||
if (role == null) {
|
||||
throw new BusinessException("ROLE_NOT_FOUND", "角色不存在");
|
||||
}
|
||||
return role;
|
||||
}
|
||||
|
||||
private SysUserDO getTenantUser(Long userId, Long tenantId) {
|
||||
SysUserDO user = sysUserMapper.selectById(userId);
|
||||
if (user == null || Integer.valueOf(1).equals(user.getDeleted()) || !tenantId.equals(user.getTenantId())) {
|
||||
throw new BusinessException("USER_NOT_FOUND", "用户不存在");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
public record CreateOrUpdateUserRequest(
|
||||
String username,
|
||||
String realName,
|
||||
String mobile,
|
||||
String email,
|
||||
Long deptId,
|
||||
String jobTitle,
|
||||
String userStatus,
|
||||
String roleCode
|
||||
) {
|
||||
}
|
||||
|
||||
public record TenantUserView(
|
||||
Long id,
|
||||
String username,
|
||||
String realName,
|
||||
String mobile,
|
||||
String email,
|
||||
String jobTitle,
|
||||
String userStatus,
|
||||
Integer memberSort,
|
||||
Long deptId,
|
||||
String deptName,
|
||||
String roleCode
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.easycard.module.user.service;
|
||||
|
||||
import com.easycard.module.user.dal.entity.SysLoginLogDO;
|
||||
import com.easycard.module.user.dal.entity.SysOperationLogDO;
|
||||
import com.easycard.module.user.dal.mapper.SysLoginLogMapper;
|
||||
import com.easycard.module.user.dal.mapper.SysOperationLogMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
public class UserAuditService {
|
||||
|
||||
private final SysLoginLogMapper sysLoginLogMapper;
|
||||
private final SysOperationLogMapper sysOperationLogMapper;
|
||||
|
||||
public UserAuditService(SysLoginLogMapper sysLoginLogMapper, SysOperationLogMapper sysOperationLogMapper) {
|
||||
this.sysLoginLogMapper = sysLoginLogMapper;
|
||||
this.sysOperationLogMapper = sysOperationLogMapper;
|
||||
}
|
||||
|
||||
public void recordLogin(Long tenantId, Long userId, String userType, String loginStatus, String clientIp, String userAgent, String failReason) {
|
||||
SysLoginLogDO loginLog = new SysLoginLogDO();
|
||||
loginLog.setTenantId(tenantId == null ? 0L : tenantId);
|
||||
loginLog.setUserId(userId);
|
||||
loginLog.setUserType(userType == null ? "TENANT" : userType);
|
||||
loginLog.setLoginType("PASSWORD");
|
||||
loginLog.setLoginStatus(loginStatus);
|
||||
loginLog.setClientIp(clientIp);
|
||||
loginLog.setUserAgent(userAgent);
|
||||
loginLog.setFailReason(failReason);
|
||||
loginLog.setLoginAt(LocalDateTime.now());
|
||||
sysLoginLogMapper.insert(loginLog);
|
||||
}
|
||||
|
||||
public void recordOperation(Long tenantId, Long userId, String userName, String moduleName, String bizType, Long bizId, String operationType) {
|
||||
SysOperationLogDO operationLog = new SysOperationLogDO();
|
||||
operationLog.setTenantId(tenantId == null ? 0L : tenantId);
|
||||
operationLog.setUserId(userId);
|
||||
operationLog.setUserName(userName);
|
||||
operationLog.setModuleName(moduleName);
|
||||
operationLog.setBizType(bizType);
|
||||
operationLog.setBizId(bizId);
|
||||
operationLog.setOperationType(operationType);
|
||||
operationLog.setOperatedAt(LocalDateTime.now());
|
||||
sysOperationLogMapper.insert(operationLog);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user