refactor: 重命名 app 模块为 client 与 open 模块

- 将 app 包下的用户、文件相关类迁移到 client 包
- 将认证相关类迁移到 open 包,并更新安全配置放行路径
- 修复 ApkMetaParser 合并元数据时图标未保留的问题
- 调整 FilePath 上传路径逻辑
This commit is contained in:
TongTongStudio
2026-08-16 04:17:46 +08:00
parent 4a38c1c1a4
commit 0591ff67d0
20 changed files with 160 additions and 159 deletions

View File

@@ -0,0 +1,57 @@
package com.youlai.boot.client.controller;
import com.youlai.boot.client.model.vo.ClientFileInfo;
import com.youlai.boot.client.service.ClientFileService;
import com.youlai.boot.common.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* 文件控制层
*
* @author Ray.Hao
* @since 2022/10/16
*/
@Tag(name = "10.文件接口")
@RestController
@RequestMapping("/api/v1/client/files")
@RequiredArgsConstructor
public class ClientFileController {
private final ClientFileService fileService;
@PostMapping
@Operation(summary = "文件上传")
public Result<ClientFileInfo> uploadFile(
@Parameter(
name = "file",
description = "表单文件对象",
required = true,
in = ParameterIn.DEFAULT,
schema = @Schema(name = "file", format = "binary")
)
@RequestPart(value = "file") MultipartFile file
) {
Assert.isTrue(!file.isEmpty(), "上传文件不能为空文件");
ClientFileInfo fileInfo = fileService.uploadFile(file);
return Result.success(fileInfo);
}
@DeleteMapping
@Operation(summary = "文件删除")
@SneakyThrows
public Result<?> deleteFile(
@Parameter(description = "文件路径") @RequestParam String filePath
) {
boolean result = fileService.deleteFile(filePath);
return Result.judge(result);
}
}

View File

@@ -0,0 +1,65 @@
package com.youlai.boot.client.model.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.youlai.boot.common.base.BaseEntity;
import lombok.Getter;
import lombok.Setter;
/**
* 移动端用户实体
*/
@TableName("app_user")
@Getter
@Setter
public class ClientUser extends BaseEntity {
/**
* 用户名
*/
private String username;
/**
* 昵称
*/
private String nickname;
/**
* 性别((1-男 2-女 0-保密)
*/
private Integer gender;
/**
* 密码
*/
private String password;
/**
* 用户头像
*/
private String avatar;
/**
* 绑定手机
*/
private String mobile;
/**
* 绑定微信
*/
private String wechatOpenid;
/**
* 状态((1-正常 0-禁用)
*/
private Integer status;
/**
* 用户邮箱
*/
private String email;
/**
* 是否删除(0-否 1-是)
*/
private Integer isDeleted;
}

View File

@@ -0,0 +1,58 @@
package com.youlai.boot.client.model.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import java.util.List;
/**
* 用户表单对象
*
* @author haoxr
* @since 2022/4/12 11:04
*/
@Schema(description = "用户表单对象")
@Data
public class ClientUserForm {
@Schema(description="用户ID")
private Long id;
@Schema(description="用户名")
@NotBlank(message = "用户名不能为空")
private String username;
@Schema(description="昵称")
@NotBlank(message = "昵称不能为空")
private String nickname;
@Schema(description="手机号码")
@Pattern(regexp = "^$|^1(3\\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\\d|9[0-35-9])\\d{8}$", message = "手机号码格式不正确")
private String mobile;
@Schema(description="性别")
private Integer gender;
@Schema(description="用户头像")
private String avatar;
@Schema(description="邮箱")
private String email;
@Schema(description="用户状态(1:正常;0:禁用)")
@Range(min = 0, max = 1, message = "用户状态不正确")
private Integer status;
@Schema(description="部门ID")
private Long deptId;
@Schema(description="角色ID集合")
@NotEmpty(message = "用户角色不能为空")
private List<Long> roleIds;
}

View File

@@ -0,0 +1,23 @@
package com.youlai.boot.client.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 文件信息对象
*
* @author Ray.Hao
* @since 1.0.0
*/
@Schema(description = "文件对象")
@Data
public class ClientFileInfo {
@Schema(description = "文件名称")
private String name;
@Schema(description = "文件URL")
private String url;
}

View File

@@ -0,0 +1,30 @@
package com.youlai.boot.client.service;
import com.youlai.boot.client.model.vo.ClientFileInfo;
import org.springframework.web.multipart.MultipartFile;
/**
* 对象存储服务接口层
*
* @author haoxr
* @since 2022/11/19
*/
public interface ClientFileService {
/**
* 上传文件
* @param file 表单文件对象
* @return 文件信息
*/
ClientFileInfo uploadFile(MultipartFile file);
/**
* 删除文件
*
* @param filePath 文件完整URL
* @return 删除结果
*/
boolean deleteFile(String filePath);
}

View File

@@ -0,0 +1,197 @@
package com.youlai.boot.client.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.boot.client.model.entity.ClientUser;
import com.youlai.boot.client.model.form.ClientUserForm;
import com.youlai.boot.common.model.Option;
import com.youlai.boot.framework.security.model.SecurityUser;
import com.youlai.boot.system.model.form.*;
import com.youlai.boot.system.model.query.UserQuery;
import com.youlai.boot.system.model.vo.CurrentUserVO;
import com.youlai.boot.system.model.vo.UserExportVO;
import com.youlai.boot.system.model.vo.UserPageVO;
import com.youlai.boot.system.model.vo.UserProfileVO;
import java.util.List;
/**
* 用户业务接口
*
* @author Ray.Hao
* @since 2022/1/14
*/
public interface ClientUserService extends IService<ClientUser> {
/**
* 用户分页列表
*
* @return {@link IPage<UserPageVO>} 用户分页列表
*/
IPage<UserPageVO> getUserPage(UserQuery queryParams);
/**
* 获取用户表单数据
*
* @param userId 用户ID
* @return {@link ClientUserForm} 用户表单数据
*/
ClientUserForm getUserFormData(Long userId);
/**
* 新增用户
*
* @param userForm 用户表单对象
* @return {@link Boolean} 是否新增成功
*/
boolean saveUser(ClientUserForm userForm);
/**
* 修改用户
*
* @param userId 用户ID
* @param userForm 用户表单对象
* @return {@link Boolean} 是否修改成功
*/
boolean updateUser(Long userId, ClientUserForm userForm);
/**
* 删除用户
*
* @param idsStr 用户ID多个以英文逗号(,)分割
* @return {@link Boolean} 是否删除成功
*/
boolean deleteUsers(String idsStr);
/**
* 根据用户名获取认证信息
*
* @param username 用户名
* @return {@link SecurityUser}
*/
SecurityUser getAuthInfoByUsername(String username);
default SecurityUser getAuthCredentialsByUsername(String username) {
return getAuthInfoByUsername(username);
}
/**
* 获取导出用户列表
*
* @param queryParams 查询参数
* @return {@link List<UserExportVO>} 导出用户列表
*/
List<UserExportVO> listExportUsers(UserQuery queryParams);
/**
* 获取登录用户信息
*
* @return {@link CurrentUserVO} 登录用户信息
*/
CurrentUserVO getCurrentUserInfo();
/**
* 获取个人中心用户信息
*
* @return {@link UserProfileVO} 个人中心用户信息
*/
UserProfileVO getUserProfile(Long userId);
/**
* 修改个人中心用户信息
*
* @param formData 表单数据
* @return {@link Boolean} 是否修改成功
*/
boolean updateUserProfile(UserProfileForm formData);
/**
* 修改指定用户密码
*
* @param userId 用户ID
* @param data 修改密码表单数据
* @return {@link Boolean} 是否修改成功
*/
boolean changeUserPassword(Long userId, PasswordUpdateForm data);
/**
* 重置指定用户密码
*
* @param userId 用户ID
* @param password 重置后的密码
* @return {@link Boolean} 是否重置成功
*/
boolean resetUserPassword(Long userId, String password);
/**
* 发送短信验证码(绑定或更换手机号)
*
* @param mobile 手机号
* @return {@link Boolean} 是否发送成功
*/
boolean sendMobileCode(String mobile);
/**
* 修改当前用户手机号
*
* @param data 表单数据
* @return {@link Boolean} 是否修改成功
*/
boolean bindOrChangeMobile(MobileUpdateForm data);
/**
* 发送邮箱验证码(绑定或更换邮箱)
*
* @param email 邮箱
*/
void sendEmailCode(String email);
/**
* 绑定或更换邮箱
*
* @param data 表单数据
* @return {@link Boolean} 是否绑定成功
*/
boolean bindOrChangeEmail(EmailUpdateForm data);
/**
* 解绑手机号
*
* @param data 表单数据
* @return {@link Boolean} 是否解绑成功
*/
boolean unbindMobile(PasswordVerifyForm data);
/**
* 解绑邮箱
*
* @param data 表单数据
* @return {@link Boolean} 是否解绑成功
*/
boolean unbindEmail(PasswordVerifyForm data);
/**
* 获取用户选项列表
*
* @return {@link List<Option<String>>} 用户选项列表
*/
// List<Option<String>> listUserOptions();
/**
* 根据手机号获取用户认证信息
*
* @param mobile 手机号
* @return {@link SecurityUser}
*/
SecurityUser getAuthInfoByMobile(String mobile);
default SecurityUser getAuthCredentialsByMobile(String mobile) {
return getAuthInfoByMobile(mobile);
}
}

View File

@@ -0,0 +1,90 @@
package com.youlai.boot.client.service.impl;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import com.youlai.boot.client.model.vo.ClientFileInfo;
import com.youlai.boot.client.service.ClientFileService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.InputStream;
import java.time.LocalDateTime;
/**
* 本地存储服务类
*
* @author Theo
* @since 2024-12-09 17:11
*/
@Data
@Slf4j
@Component
//@ConditionalOnProperty(value = "oss.type", havingValue = "local")
//@ConfigurationProperties(prefix = "oss.local")
@RequiredArgsConstructor
public class ClientClientFileService implements ClientFileService {
@Value("${file-storage.local.path}")
private String storagePath;
/**
* 上传文件方法
*
* @param file 表单文件对象
* @return 文件信息
*/
@Override
public ClientFileInfo uploadFile(MultipartFile file) {
// 获取文件名
String originalFilename = file.getOriginalFilename();
// 获取文件后缀
String suffix = FileUtil.getSuffix(originalFilename);
// 生成uuid
String fileName = IdUtil.simpleUUID()+ "." + suffix;;
// 生成文件名(日期文件夹)
String folder = DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN);
String filePrefix = storagePath.endsWith(File.separator) ? storagePath : storagePath + File.separator;
// try-with-resource 语法糖自动释放流
try (InputStream inputStream = file.getInputStream()) {
// 上传文件
FileUtil.writeFromStream(inputStream, filePrefix + folder + File.separator + fileName);
} catch (Exception e) {
log.error("文件上传失败", e);
throw new RuntimeException("文件上传失败");
}
// 获取文件访问路径,因为这里是本地存储,所以直接返回文件的相对路径,需要前端自行处理访问前缀
String fileUrl = File.separator + folder + File.separator + fileName;
ClientFileInfo fileInfo = new ClientFileInfo();
fileInfo.setName(originalFilename);
fileInfo.setUrl(fileUrl);
return fileInfo;
}
/**
* 删除文件
* @param filePath 文件完整URL
* @return 是否删除成功
*/
@Override
public boolean deleteFile(String filePath) {
//判断文件是否为空
if (filePath == null || filePath.isEmpty()) {
return false;
}
// 判断filepath是否为文件夹
if (FileUtil.isDirectory(storagePath + filePath)) {
// 禁止删除文件夹
return false;
}
// 删除文件
return FileUtil.del(storagePath + filePath);
}
}

View File

@@ -0,0 +1,680 @@
package com.youlai.boot.client.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.boot.open.converter.ClientUserConverter;
import com.youlai.boot.open.mapper.ClientUserMapper;
import com.youlai.boot.client.model.entity.ClientUser;
import com.youlai.boot.client.model.form.ClientUserForm;
import com.youlai.boot.client.service.ClientUserService;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SystemConstants;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.model.Option;
import com.youlai.boot.framework.security.model.RoleDataScope;
import com.youlai.boot.framework.security.model.SecurityUser;
import com.youlai.boot.framework.security.token.TokenManager;
import com.youlai.boot.framework.security.util.SecurityUtils;
import com.youlai.boot.support.mail.EmailService;
import com.youlai.boot.support.sms.SmsService;
import com.youlai.boot.support.sms.SmsTypeEnum;
import com.youlai.boot.system.enums.DictCodeEnum;
import com.youlai.boot.system.model.entity.DictItem;
import com.youlai.boot.system.model.entity.Role;
import com.youlai.boot.system.model.form.*;
import com.youlai.boot.system.model.query.UserQuery;
import com.youlai.boot.system.model.vo.CurrentUserVO;
import com.youlai.boot.system.model.vo.UserExportVO;
import com.youlai.boot.system.model.vo.UserPageVO;
import com.youlai.boot.system.model.vo.UserProfileVO;
import com.youlai.boot.system.service.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* app用户业务实现类
*
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class ClientUserServiceImpl extends ServiceImpl<ClientUserMapper, ClientUser> implements ClientUserService {
private final PasswordEncoder passwordEncoder;
private final UserRoleService userRoleService;
private final DeptService deptService;
private final RoleService roleService;
private final RoleMenuService roleMenuService;
private final SmsService smsService;
private final EmailService mailService;
private final StringRedisTemplate redisTemplate;
private final TokenManager tokenManager;
private final DictItemService dictItemService;
private final ClientUserConverter userConverter;
/**
* 获取用户分页列表
*
* @param queryParams 查询参数
* @return {@link IPage<UserPageVO>} 用户分页列表
*/
@Override
public IPage<UserPageVO> getUserPage(UserQuery queryParams) {
// 参数构建
int pageNum = queryParams.getPageNum();
int pageSize = queryParams.getPageSize();
Page<UserPageVO> page = new Page<>(pageNum, pageSize);
boolean isRoot = SecurityUtils.isRoot();
queryParams.setIsRoot(isRoot);
// 查询数据
return this.baseMapper.getUserPage(page, queryParams);
}
/**
* 获取用户表单数据
*
* @param userId 用户ID
* @return {@link ClientUserForm} 用户表单数据
*/
@Override
public ClientUserForm getUserFormData(Long userId) {
return this.baseMapper.getUserFormData(userId);
}
/**
* 新增用户
*
* @param userForm 用户表单对象
* @return true|false
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean saveUser(ClientUserForm userForm) {
String username = userForm.getUsername();
// 实体转换 form->entity
ClientUser entity = userConverter.toEntity(userForm);
// 检查用户名是否已存在
long count = this.count(new LambdaQueryWrapper<ClientUser>()
.eq(ClientUser::getUsername, username));
Assert.isTrue(count == 0, "用户名已存在");
// 设置默认加密密码
String defaultEncryptPwd = passwordEncoder.encode(SystemConstants.DEFAULT_PASSWORD);
entity.setPassword(defaultEncryptPwd);
// entity.setCreateBy(SecurityUtils.getUserId());
// 新增用户
boolean result = this.save(entity);
if (result) {
// 保存用户角色
userRoleService.saveUserRoles(entity.getId(), userForm.getRoleIds());
}
return result;
}
/**
* 更新用户
*
* @param userId 用户ID
* @param userForm 用户表单对象
* @return true|false
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateUser(Long userId, ClientUserForm userForm) {
String username = userForm.getUsername();
// 获取原用户信息
ClientUser oldUser = this.getById(userId);
Assert.notNull(oldUser, "用户不存在");
// 检查用户名是否已存在(排除当前用户)
long count = this.count(new LambdaQueryWrapper<ClientUser>()
.eq(ClientUser::getUsername, username)
.ne(ClientUser::getId, userId)
);
Assert.isTrue(count == 0, "用户名已存在");
// form -> entity
ClientUser entity = userConverter.toEntity(userForm);
// entity.setUpdateBy(SecurityUtils.getUserId());
// 修改用户
boolean result = this.updateById(entity);
if (result) {
// 保存用户角色
userRoleService.saveUserRoles(entity.getId(), userForm.getRoleIds());
}
return result;
}
/**
* 删除用户
*
* @param idsStr 用户ID多个以英文逗号(,)分割
* @return true|false
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteUsers(String idsStr) {
Assert.isTrue(StrUtil.isNotBlank(idsStr), "删除的用户数据为空");
// 逻辑删除
List<Long> ids = Arrays.stream(idsStr.split(","))
.map(Long::parseLong)
.collect(Collectors.toList());
boolean result = this.removeByIds(ids);
return result;
}
/**
* 根据用户名获取认证凭证信息
*
* @param username 用户名
* @return 用户认证凭证信息 {@link SecurityUser}
*/
@Override
public SecurityUser getAuthInfoByUsername(String username) {
SecurityUser userAuthInfo = this.baseMapper.getAuthInfoByUsername(username);
if (userAuthInfo != null) {
Set<String> roles = userAuthInfo.getRoles();
// 获取数据权限列表(用于并集策略)
List<RoleDataScope> dataScopes = roleService.getRoleDataScopes(roles);
userAuthInfo.setDataScopes(dataScopes);
}
return userAuthInfo;
}
/**
* 根据手机号获取用户认证信息
*
* @param mobile 手机号
* @return 用户认证信息
*/
@Override
public SecurityUser getAuthInfoByMobile(String mobile) {
if (StrUtil.isBlank(mobile)) {
return null;
}
SecurityUser userAuthInfo = this.baseMapper.getAuthInfoByMobile(mobile);
if (userAuthInfo != null) {
Set<String> roles = userAuthInfo.getRoles();
// 获取数据权限列表(用于并集策略)
List<RoleDataScope> dataScopes = roleService.getRoleDataScopes(roles);
userAuthInfo.setDataScopes(dataScopes);
}
return userAuthInfo;
}
/**
* 获取导出用户列表
*
* @param queryParams 查询参数
* @return {@link List<UserExportVO>} 导出用户列表
*/
@Override
public List<UserExportVO> listExportUsers(UserQuery queryParams) {
boolean isRoot = SecurityUtils.isRoot();
queryParams.setIsRoot(isRoot);
List<UserExportVO> exportUsers = this.baseMapper.listExportUsers(queryParams);
if (CollectionUtil.isNotEmpty(exportUsers)) {
//获取性别的字典项
Map<String, String> genderMap = dictItemService.list(
new LambdaQueryWrapper<DictItem>().eq(DictItem::getDictCode,
DictCodeEnum.GENDER.getValue())
).stream()
.collect(Collectors.toMap(DictItem::getValue, DictItem::getLabel)
);
exportUsers.forEach(item -> {
String gender = item.getGender();
if (StrUtil.isBlank(gender)) {
return;
}
// 判断map是否为空
if (genderMap.isEmpty()) {
return;
}
item.setGender(genderMap.get(gender));
});
}
return exportUsers;
}
/**
* 获取登录用户信息
*
* @return {@link CurrentUserVO} 用户信息
*/
@Override
public CurrentUserVO getCurrentUserInfo() {
String username = SecurityUtils.getUsername();
// 获取登录用户基础信息
ClientUser user = this.getOne(new LambdaQueryWrapper<ClientUser>()
.eq(ClientUser::getUsername, username)
.select(
ClientUser::getId,
ClientUser::getUsername,
ClientUser::getNickname,
ClientUser::getAvatar,
ClientUser::getGender
// AppUser::getDeptId
)
);
// entity->Vo
CurrentUserVO userInfoVo = userConverter.toCurrentUserVo(user);
// 性别
userInfoVo.setGender(user.getGender());
// 部门名称
// if (user.getDeptId() != null) {
// Dept dept = deptService.getById(user.getDeptId());
// if (dept != null) {
// userInfoVo.setDeptName(dept.getName());
// }
// }
// 用户角色集合
Set<String> roles = SecurityUtils.getRoles();
userInfoVo.setRoles(roles);
// 用户角色名称集合
if (CollectionUtil.isNotEmpty(roles)) {
Set<String> roleNames = roleService.list(new LambdaQueryWrapper<Role>()
.in(Role::getCode, roles)
.select(Role::getName)
).stream()
.map(Role::getName)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toCollection(LinkedHashSet::new));
userInfoVo.setRoleNames(roleNames);
}
// 用户权限集合
if (CollectionUtil.isNotEmpty(roles)) {
Set<String> perms = roleMenuService.getRolePermsByRoleCodes(roles);
userInfoVo.setPerms(perms);
}
return userInfoVo;
}
/**
* 获取个人中心用户信息
*
* @param userId 用户ID
* @return {@link UserProfileVO} 个人中心用户信息
*/
@Override
public UserProfileVO getUserProfile(Long userId) {
return this.baseMapper.getUserProfile(userId);
}
/**
* 修改个人中心用户信息
*
* @param formData 表单数据
* @return true|false
*/
@Override
public boolean updateUserProfile(UserProfileForm formData) {
Long userId = SecurityUtils.getUserId();
if (formData.getNickname() == null && formData.getAvatar() == null && formData.getGender() == null) {
throw new BusinessException("请修改至少一个字段");
}
return this.update(new LambdaUpdateWrapper<ClientUser>()
.eq(ClientUser::getId, userId)
.set(formData.getNickname() != null, ClientUser::getNickname, formData.getNickname())
.set(formData.getAvatar() != null, ClientUser::getAvatar, formData.getAvatar())
.set(formData.getGender() != null, ClientUser::getGender, formData.getGender())
);
}
/**
* 修改指定用户密码
*
* @param userId 用户ID
* @param data 密码修改表单数据
* @return true|false
*/
@Override
public boolean changeUserPassword(Long userId, PasswordUpdateForm data) {
ClientUser user = this.getById(userId);
if (user == null) {
throw new BusinessException("用户不存在");
}
String oldPassword = data.getOldPassword();
// 校验原密码
if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
throw new BusinessException("原密码错误");
}
// 新旧密码不能相同
if (passwordEncoder.matches(data.getNewPassword(), user.getPassword())) {
throw new BusinessException("新密码不能与原密码相同");
}
// 判断新密码和确认密码是否一致
if (!Objects.equals(data.getNewPassword(), data.getConfirmPassword())) {
throw new BusinessException("新密码和确认密码不一致");
}
String newPassword = data.getNewPassword();
boolean result = this.update(new LambdaUpdateWrapper<ClientUser>()
.eq(ClientUser::getId, userId)
.set(ClientUser::getPassword, passwordEncoder.encode(newPassword))
);
if (result) {
// 密码变更后,使当前用户的所有会话失效,强制重新登录
tokenManager.invalidateUserSessions(userId);
}
return result;
}
/**
* 重置指定用户密码
*
* @param userId 用户ID
* @param password 密码重置表单数据
* @return true|false
*/
@Override
public boolean resetUserPassword(Long userId, String password) {
boolean result = this.update(new LambdaUpdateWrapper<ClientUser>()
.eq(ClientUser::getId, userId)
.set(ClientUser::getPassword, passwordEncoder.encode(password))
);
if (result) {
// 管理员重置用户密码后,使该用户的所有会话失效
tokenManager.invalidateUserSessions(userId);
}
return result;
}
/**
* 发送短信验证码(绑定或更换手机号)
*
* @param mobile 手机号
* @return true|false
*/
@Override
public boolean sendMobileCode(String mobile) {
Long currentUserId = SecurityUtils.getUserId();
long mobileCount = this.count(new LambdaQueryWrapper<ClientUser>()
.eq(ClientUser::getMobile, mobile)
.ne(ClientUser::getId, currentUserId)
);
if (mobileCount > 0) {
throw new BusinessException("手机号已被其他账号绑定");
}
// String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000));
// TODO 为了方便测试,验证码固定为 123456实际开发中在配置了厂商短信服务后可以使用上面的随机验证码
String code = "123456";
Map<String, String> templateParams = new HashMap<>();
templateParams.put("code", code);
boolean result = smsService.send(mobile, SmsTypeEnum.CHANGE_MOBILE, templateParams);
if (result) {
// 缓存验证码5分钟有效用于更换手机号校验
String redisCacheKey = StrUtil.format(RedisConstants.Captcha.MOBILE_CODE, mobile);
redisTemplate.opsForValue().set(redisCacheKey, code, 5, TimeUnit.MINUTES);
}
return result;
}
/**
* 绑定或更换手机号
*
* @param form 表单数据
* @return true|false
*/
@Override
public boolean bindOrChangeMobile(MobileUpdateForm form) {
Long currentUserId = SecurityUtils.getUserId();
ClientUser currentUser = this.getById(currentUserId);
if (currentUser == null) {
throw new BusinessException("用户不存在");
}
if (!passwordEncoder.matches(form.getPassword(), currentUser.getPassword())) {
throw new BusinessException("当前密码错误");
}
// 校验验证码
String inputVerifyCode = form.getCode();
String mobile = form.getMobile();
String cacheKey = StrUtil.format(RedisConstants.Captcha.MOBILE_CODE, mobile);
String cachedVerifyCode = redisTemplate.opsForValue().get(cacheKey);
if (StrUtil.isBlank(cachedVerifyCode)) {
throw new BusinessException("验证码已过期");
}
if (!inputVerifyCode.equals(cachedVerifyCode)) {
throw new BusinessException("验证码错误");
}
long mobileCount = this.count(new LambdaQueryWrapper<ClientUser>()
.eq(ClientUser::getMobile, mobile)
.ne(ClientUser::getId, currentUserId)
);
if (mobileCount > 0) {
throw new BusinessException("手机号已被其他账号绑定");
}
redisTemplate.delete(cacheKey);
// 更新手机号码
return this.update(
new LambdaUpdateWrapper<ClientUser>()
.eq(ClientUser::getId, currentUserId)
.set(ClientUser::getMobile, mobile)
);
}
/**
* 发送邮箱验证码(绑定或更换邮箱)
*
* @param email 邮箱
*/
@Override
public void sendEmailCode(String email) {
Long currentUserId = SecurityUtils.getUserId();
long emailCount = this.count(new LambdaQueryWrapper<ClientUser>()
.eq(ClientUser::getEmail, email)
.ne(ClientUser::getId, currentUserId)
);
if (emailCount > 0) {
throw new BusinessException("邮箱已被其他账号绑定");
}
// String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000));
// TODO 为了方便测试,验证码固定为 123456实际开发中在配置了邮箱服务后可以使用上面的随机验证码
String code = "123456";
mailService.send(email, "邮箱验证码", "您的验证码为:" + code + "请在5分钟内使用");
// 缓存验证码5分钟有效用于更换邮箱校验
String redisCacheKey = StrUtil.format(RedisConstants.Captcha.EMAIL_CODE, email);
redisTemplate.opsForValue().set(redisCacheKey, code, 5, TimeUnit.MINUTES);
}
/**
* 修改当前用户邮箱
*
* @param form 表单数据
* @return true|false
*/
@Override
public boolean bindOrChangeEmail(EmailUpdateForm form) {
Long currentUserId = SecurityUtils.getUserId();
ClientUser currentUser = this.getById(currentUserId);
if (currentUser == null) {
throw new BusinessException("用户不存在");
}
if (!passwordEncoder.matches(form.getPassword(), currentUser.getPassword())) {
throw new BusinessException("当前密码错误");
}
// 获取前端输入的验证码
String inputVerifyCode = form.getCode();
// 获取缓存的验证码
String email = form.getEmail();
String redisCacheKey = StrUtil.format(RedisConstants.Captcha.EMAIL_CODE, email);
String cachedVerifyCode = redisTemplate.opsForValue().get(redisCacheKey);
if (StrUtil.isBlank(cachedVerifyCode)) {
throw new BusinessException("验证码已过期");
}
if (!inputVerifyCode.equals(cachedVerifyCode)) {
throw new BusinessException("验证码错误");
}
long emailCount = this.count(new LambdaQueryWrapper<ClientUser>()
.eq(ClientUser::getEmail, email)
.ne(ClientUser::getId, currentUserId)
);
if (emailCount > 0) {
throw new BusinessException("邮箱已被其他账号绑定");
}
redisTemplate.delete(redisCacheKey);
// 更新邮箱地址
return this.update(
new LambdaUpdateWrapper<ClientUser>()
.eq(ClientUser::getId, currentUserId)
.set(ClientUser::getEmail, email)
);
}
/**
* 解绑手机号
*
* @param form 表单数据
* @return true|false
*/
@Override
public boolean unbindMobile(PasswordVerifyForm form) {
Long currentUserId = SecurityUtils.getUserId();
ClientUser currentUser = this.getById(currentUserId);
if (currentUser == null) {
throw new BusinessException("用户不存在");
}
if (StrUtil.isBlank(currentUser.getMobile())) {
throw new BusinessException("当前账号未绑定手机号");
}
if (!passwordEncoder.matches(form.getPassword(), currentUser.getPassword())) {
throw new BusinessException("当前密码错误");
}
return this.update(new LambdaUpdateWrapper<ClientUser>()
.eq(ClientUser::getId, currentUserId)
.set(ClientUser::getMobile, null)
);
}
/**
* 解绑邮箱
*
* @param form 表单数据
* @return true|false
*/
@Override
public boolean unbindEmail(PasswordVerifyForm form) {
Long currentUserId = SecurityUtils.getUserId();
ClientUser currentUser = this.getById(currentUserId);
if (currentUser == null) {
throw new BusinessException("用户不存在");
}
if (StrUtil.isBlank(currentUser.getEmail())) {
throw new BusinessException("当前账号未绑定邮箱");
}
if (!passwordEncoder.matches(form.getPassword(), currentUser.getPassword())) {
throw new BusinessException("当前密码错误");
}
return this.update(new LambdaUpdateWrapper<ClientUser>()
.eq(ClientUser::getId, currentUserId)
.set(ClientUser::getEmail, null)
);
}
/**
* 获取用户选项列表
*
* @return {@link List<Option<String>>} 用户选项列表
*/
// @Override
// public List<Option<String>> listUserOptions() {
// List<AppUser> list = this.list(new LambdaQueryWrapper<AppUser>()
// .eq(AppUser::getStatus, 1)
// );
// return userConverter.toOptions(list);
// }
}