refactor: RuntimeException 统一为 BusinessException 并规范注释

- 文件上传、代码生成、用户/菜单/角色校验等场景的 RuntimeException 统一替换为 BusinessException
- 阿里云 OSS 上传失败补记日志并保留原始异常 cause
- 修复 prod 微信小程序配置 app-secret 无法绑定 WxMaProperties.secret 字段的问题
- UserServiceImpl 展开通配符 import 并规范化 javadoc
This commit is contained in:
Ray.Hao
2026-08-22 12:00:25 +08:00
parent a33de52700
commit 7ecbc16b69
14 changed files with 159 additions and 89 deletions

View File

@@ -1,6 +1,7 @@
package com.youlai.boot.codegen.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.result.PageResult;
import com.youlai.boot.common.result.Result;
import com.youlai.boot.codegen.config.CodegenProperties;
@@ -106,7 +107,7 @@ public class CodegenController {
outputStream.flush();
} catch (IOException e) {
log.error("Error while writing the zip file1 to response", e);
throw new RuntimeException("Failed to write the zip file1 to response", e);
throw new BusinessException("代码生成压缩包响应写入失败", e);
}
}
}

View File

@@ -389,7 +389,7 @@ public class CodegenServiceImpl implements CodegenService {
} catch (IOException e) {
log.error("Error while generating zip for code download", e);
throw new RuntimeException("Failed to generate code zip file1", e);
throw new BusinessException("代码生成压缩包失败", e);
}
}

View File

@@ -6,30 +6,47 @@ import com.youlai.boot.common.enums.LogModuleEnum;
import java.time.LocalDateTime;
/**
* 操作日志通用载荷
* <p>由操作日志切面采集,经 {@link OperationLogPort} 交给各业务模块落库</p>
* 操作日志载荷
*
* @author Ray.Hao
* @since 0.0.1
* @since 4.7.0
*/
public record OperationLog(
/** 模块 */
LogModuleEnum module,
/** 操作类型 */
ActionTypeEnum actionType,
/** 操作标题 */
String title,
/** 自定义日志内容 */
String content,
/** 操作人 ID */
Long operatorId,
/** 操作人名称 */
String operatorName,
/** 请求路径 */
String requestUri,
/** 请求方式 */
String requestMethod,
/** IP 地址 */
String ip,
/** 省份 */
String province,
/** 城市 */
String city,
/** 设备 */
String device,
/** 操作系统 */
String os,
/** 浏览器 */
String browser,
/** 状态:0 失败 1 成功 */
Integer status,
/** 错误信息 */
String errorMsg,
/** 执行时间(毫秒) */
Integer executionTime,
/** 操作时间 */
LocalDateTime createTime
) {
}

View File

@@ -25,10 +25,9 @@ import java.util.concurrent.Executor;
/**
* 操作日志切面
* <p>@Log 标注的方法执行后异步采集操作日志,通过 {@link OperationLogPort} 落库</p>
*
* @author Ray.Hao
* @since 0.0.1
* @since 4.7.0
*/
@Aspect
@Component

View File

@@ -2,12 +2,10 @@ package com.youlai.boot.framework.web.log;
/**
* 操作日志落库端口
* <p>由各业务模块实现,framework 层通过此接口写入操作日志,不依赖具体落库实现。
* 如 system 模块的 {@code OperationLogAdapter} 落库 sys_log 表。</p>
* <p>由 {@link com.youlai.boot.system.adapter.log.OperationLogAdapter} 实现</p>
*
* @see com.youlai.boot.system.adapter.log.OperationLogAdapter
* @author Ray.Hao
* @since 0.0.1
* @since 4.7.0
*/
public interface OperationLogPort {

View File

@@ -8,12 +8,14 @@ import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.ObjectMetadata;
import com.aliyun.oss.model.PutObjectRequest;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.support.file.model.FileInfo;
import com.youlai.boot.support.file.service.FileService;
import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@@ -29,6 +31,7 @@ import java.time.LocalDateTime;
* 上传文件按日期分目录存储,URL 格式:{@code https://bucket.endpoint/yyyyMMdd/uuid.suffix}。
*/
@Component
@Slf4j
@ConditionalOnProperty(value = "file-storage.type", havingValue = "aliyun")
@ConfigurationProperties(prefix = "file-storage.aliyun")
@RequiredArgsConstructor
@@ -67,7 +70,8 @@ public class AliyunFileServiceImpl implements FileService {
PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, fileName, inputStream, metadata);
aliyunOssClient.putObject(putObjectRequest);
} catch (Exception e) {
throw new RuntimeException("文件上传失败");
log.error("文件上传失败", e);
throw new BusinessException("文件上传失败", e);
}
String fileUrl = "https://" + bucket + "." + endpoint + "/" + fileName;
FileInfo fileInfo = new FileInfo();

View File

@@ -4,6 +4,7 @@ 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.common.exception.BusinessException;
import com.youlai.boot.support.file.model.FileInfo;
import com.youlai.boot.support.file.service.FileService;
import lombok.Data;
@@ -52,7 +53,7 @@ public class LocalFileServiceImpl implements FileService {
FileUtil.writeFromStream(inputStream, filePrefix + folder + File.separator + fileName);
} catch (Exception e) {
log.error("文件上传失败", e);
throw new RuntimeException("文件上传失败");
throw new BusinessException("文件上传失败");
}
String fileUrl = File.separator + folder + File.separator + fileName;
FileInfo fileInfo = new FileInfo();

View File

@@ -3,18 +3,16 @@ package com.youlai.boot.system.adapter.log;
import com.youlai.boot.framework.web.log.OperationLog;
import com.youlai.boot.framework.web.log.OperationLogPort;
import com.youlai.boot.system.converter.LogConverter;
import com.youlai.boot.system.model.entity.SysLog;
import com.youlai.boot.system.service.LogService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 操作日志适配器
* <p>实现 framework 层的 {@link OperationLogPort},将切面采集的操作日志落库 sys_log 表</p>
* <p>实现 {@link OperationLogPort},落库 sys_log</p>
*
* @author Ray.Hao
* @since 0.0.1
* @see OperationLogPort
* @since 4.7.0
*/
@Component
@RequiredArgsConstructor
@@ -25,9 +23,6 @@ public class OperationLogAdapter implements OperationLogPort {
/**
* 记录操作日志
* <p>通过 {@link LogConverter} 将操作日志载荷转换为 {@link SysLog} 后落库</p>
*
* @param operationLog 操作日志载荷
*/
@Override
public void record(OperationLog operationLog) {

View File

@@ -7,6 +7,7 @@ import com.youlai.boot.common.annotation.RepeatSubmit;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.enums.StatusEnum;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.framework.security.util.SecurityUtils;
import com.youlai.boot.common.model.Option;
import com.youlai.boot.common.result.ExcelResult;
@@ -57,7 +58,7 @@ import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* 用户控制层
* 用户管理控制器.
*
* @author Ray.Hao
* @since 4.3.1
@@ -175,7 +176,7 @@ public class UserController {
ExcelWriter excelWriter = EasyExcel.write(outputStream).withTemplate(inputStream).build()) {
excelWriter.finish();
} catch (IOException e) {
throw new RuntimeException("用户导入模板下载失败", e);
throw new BusinessException("用户导入模板下载失败", e);
}
}

View File

@@ -5,13 +5,19 @@ import com.youlai.boot.system.model.entity.SysLog;
import org.mapstruct.Mapper;
/**
* 系统日志对象转换器
* 操作日志转换器
*
* @author Ray.Hao
* @since 0.0.1
* @since 4.7.0
*/
@Mapper(componentModel = "spring")
public interface LogConverter {
/**
* 操作日志载荷转为日志实体
*
* @param operationLog 操作日志载荷
* @return 日志实体
*/
SysLog toEntity(OperationLog operationLog);
}

View File

@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.boot.codegen.model.entity.GenTable;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.framework.security.util.SecurityUtils;
import com.youlai.boot.system.converter.MenuConverter;
import com.youlai.boot.system.mapper.MenuMapper;
@@ -303,7 +304,7 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
menuForm.setComponent(null);
}
if (Objects.equals(menuForm.getParentId(), menuForm.getId())) {
throw new RuntimeException("父级菜单不能为当前菜单");
throw new BusinessException("父级菜单不能为当前菜单");
}
Menu entity = menuConverter.toEntity(menuForm);
String treePath = generateMenuTreePath(menuForm.getParentId());

View File

@@ -278,7 +278,7 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
public void assignMenusToRole(Long roleId, List<Long> menuIds) {
Role role = this.getById(roleId);
if (role == null) {
throw new RuntimeException("角色不存在");
throw new BusinessException("角色不存在");
}
// 删除角色菜单
roleMenuService.remove(

View File

@@ -22,7 +22,12 @@ import com.youlai.boot.framework.security.util.SecurityUtils;
import com.youlai.boot.system.converter.UserConverter;
import com.youlai.boot.system.enums.DictCodeEnum;
import com.youlai.boot.system.mapper.UserMapper;
import com.youlai.boot.system.model.form.*;
import com.youlai.boot.system.model.form.EmailUpdateForm;
import com.youlai.boot.system.model.form.MobileUpdateForm;
import com.youlai.boot.system.model.form.PasswordUpdateForm;
import com.youlai.boot.system.model.form.PasswordVerifyForm;
import com.youlai.boot.system.model.form.UserForm;
import com.youlai.boot.system.model.form.UserProfileForm;
import com.youlai.boot.system.model.vo.CurrentUserVO;
import com.youlai.boot.system.model.vo.UserExportVO;
import com.youlai.boot.system.model.entity.Dept;
@@ -40,15 +45,21 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 用户业务实现类
* 用户业务实现类.
*
* @author Ray.Hao
* @since 2022/1/14
* @since 0.0.1
*/
@Service
@RequiredArgsConstructor
@@ -78,10 +89,10 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
/**
* 获取用户分页列表
* 获取用户分页列表.
*
* @param queryParams 查询参数
* @return {@link IPage<UserPageVO>} 用户分页列表
* @param queryParams 分页查询条件(含关键字、状态、部门过滤)
* @return 用户分页数据(含角色、部门名称)
*/
@Override
public IPage<UserPageVO> getUserPage(UserQuery queryParams) {
@@ -99,10 +110,10 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
}
/**
* 获取用户表单数据
* 获取用户表单回显数据.
*
* @param userId 用户ID
* @return {@link UserForm} 用户表单数据
* @param userId 用户 ID
* @return 用户表单(含已分配角色 ID 列表),用于编辑页回显
*/
@Override
public UserForm getUserFormData(Long userId) {
@@ -110,10 +121,12 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
}
/**
* 新增用户
* 新增用户.
* <p>校验用户名唯一性,设置默认加密密码,保存用户角色关联(事务一致)。
*
* @param userForm 用户表单对象
* @return true|false
* @param userForm 用户表单(username 必填且唯一,roleIds 为角色 ID 列表)
* @return 新增成功返回 true
* @throws BusinessException 如果用户名已存在
*/
@Override
@Transactional(rollbackFor = Exception.class)
@@ -144,11 +157,13 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
}
/**
* 更新用户
* 更新用户.
* <p>校验用户名唯一性(排除自身),同步更新角色关联(事务一致)。
*
* @param userId 用户ID
* @param userForm 用户表单对象
* @return true|false
* @param userId 用户 ID
* @param userForm 用户表单(username 必填且唯一,roleIds 为角色 ID 列表)
* @return 更新成功返回 true
* @throws BusinessException 如果用户不存在或用户名已被占用
*/
@Override
@Transactional(rollbackFor = Exception.class)
@@ -181,10 +196,11 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
}
/**
* 删除用户
* 批量删除用户.
*
* @param idsStr 用户ID,多个以英文逗号(,)分割
* @return true|false
* @param idsStr 用户 ID,多个以英文逗号分割
* @return 删除成功返回 true
* @throws BusinessException 如果 idsStr 为空
*/
@Override
@Transactional(rollbackFor = Exception.class)
@@ -200,10 +216,11 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
}
/**
* 根据用户名获取认证凭证信息
* 根据用户名获取认证凭证信息.
* <p>含角色集合和数据权限范围。
*
* @param username 用户名
* @return 用户认证凭证信息 {@link SecurityUser}
* @param username 用户名(唯一)
* @return 认证凭证,用户不存在时返回 null
*/
@Override
public SecurityUser getAuthInfoByUsername(String username) {
@@ -217,7 +234,11 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
}
/**
* 根据手机号获取用户认证信息
* 根据手机号获取认证凭证信息.
* <p>含角色集合和数据权限范围。
*
* @param mobile 手机号
* @return 认证凭证,手机号为空或用户不存在时返回 null
*/
@Override
public SecurityUser getAuthInfoByMobile(String mobile) {
@@ -234,10 +255,11 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
}
/**
* 获取导出用户列表
* 获取导出用户列表.
* <p>含性别字典翻译,用于 Excel 导出。
*
* @param queryParams 查询参数
* @return {@link List<UserExportVO>} 导出用户列表
* @param queryParams 查询条件(含关键字、状态、部门过滤)
* @return 导出用户列表(gender 字段已翻译为中文)
*/
@Override
public List<UserExportVO> listExportUsers(UserQuery queryParams) {
@@ -273,9 +295,10 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
}
/**
* 获取登录用户信息
* 获取当前登录用户信息.
* <p>含基础信息、部门名称、角色集合、权限集合(缓存未命中时触发全量刷新)。
*
* @return {@link CurrentUserVO} 用户信息
* @return 当前用户信息(含角色、权限)
*/
@Override
public CurrentUserVO getCurrentUserInfo() {
@@ -338,10 +361,10 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
}
/**
* 获取个人中心用户信息
* 获取个人中心用户信息.
*
* @param userId 用户ID
* @return {@link UserProfileVO} 个人中心用户信息
* @param userId 用户 ID
* @return 个人中心信息(含角色、部门名称)
*/
@Override
public UserProfileVO getUserProfile(Long userId) {
@@ -349,10 +372,12 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
}
/**
* 修改个人中心用户信息
* 修改个人中心用户信息.
* <p>仅支持修改昵称、头像、性别,至少修改一个字段。
*
* @param formData 表单数据
* @return true|false
* @param formData 个人中心表单(nickname/avatar/gender 至少一个非空)
* @return 修改成功返回 true
* @throws BusinessException 如果所有字段均为空
*/
@Override
public boolean updateUserProfile(UserProfileForm formData) {
@@ -371,11 +396,13 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
}
/**
* 修改指定用户密码
* 修改当前用户密码.
* <p>校验原密码,确认新旧不同,密码变更后使所有会话失效。
*
* @param userId 用户ID
* @param data 密码修改表单数据
* @return true|false
* @param userId 用户 ID
* @param data 密码表单(含原密码、新密码、确认密码)
* @return 修改成功返回 true
* @throws BusinessException 如果用户不存在、原密码错误、新旧密码相同或确认密码不一致
*/
@Override
public boolean changeUserPassword(Long userId, PasswordUpdateForm data) {
@@ -415,11 +442,12 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
}
/**
* 重置指定用户密码
* 重置用户密码.
* <p>管理员操作,重置后使该用户所有会话失效。
*
* @param userId 用户ID
* @param password 密码重置表单数据
* @return true|false
* @param userId 用户 ID
* @param password 新密码(明文,内部加密存储)
* @return 重置成功返回 true
*/
@Override
public boolean resetUserPassword(Long userId, String password) {
@@ -435,10 +463,12 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
}
/**
* 发送短信验证码(绑定或更换手机号)
* 发送短信验证码(绑定或更换手机号).
* <p>校验手机号未被其他账号绑定,验证码缓存 5 分钟。
*
* @param mobile 手机号
* @return true|false
* @return 发送成功返回 true,失败返回 false 并记录日志
* @throws BusinessException 如果手机号已被其他账号绑定
*/
@Override
public boolean sendMobileCode(String mobile) {
@@ -474,10 +504,12 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
}
/**
* 绑定或更换手机号
* 绑定或更换手机号.
* <p>校验密码、验证码、手机号唯一性,成功后删除验证码缓存。
*
* @param form 表单数据
* @return true|false
* @param form 手机号绑定表单(含密码、手机号、验证码)
* @return 绑定成功返回 true
* @throws BusinessException 如果用户不存在、密码错误、验证码过期/错误或手机号已被绑定
*/
@Override
public boolean bindOrChangeMobile(MobileUpdateForm form) {
@@ -527,9 +559,11 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
}
/**
* 发送邮箱验证码(绑定或更换邮箱)
* 发送邮箱验证码(绑定或更换邮箱).
* <p>校验邮箱未被其他账号绑定,验证码缓存 5 分钟。
*
* @param email 邮箱
* @param email 邮箱地址
* @throws BusinessException 如果邮箱已被其他账号绑定
*/
@Override
public void sendEmailCode(String email) {
@@ -554,10 +588,12 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
}
/**
* 修改当前用户邮箱
* 绑定或更换邮箱.
* <p>校验密码、验证码、邮箱唯一性,成功后删除验证码缓存。
*
* @param form 表单数据
* @return true|false
* @param form 邮箱绑定表单(含密码、邮箱、验证码)
* @return 绑定成功返回 true
* @throws BusinessException 如果用户不存在、密码错误、验证码过期/错误或邮箱已被绑定
*/
@Override
public boolean bindOrChangeEmail(EmailUpdateForm form) {
@@ -608,10 +644,12 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
}
/**
* 解绑手机号
* 解绑手机号.
* <p>校验密码后将手机号置空。
*
* @param form 表单数据
* @return true|false
* @param form 密码验证表单(含密码)
* @return 解绑成功返回 true
* @throws BusinessException 如果用户不存在、未绑定手机号或密码错误
*/
@Override
public boolean unbindMobile(PasswordVerifyForm form) {
@@ -638,10 +676,12 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
}
/**
* 解绑邮箱
* 解绑邮箱.
* <p>校验密码后将邮箱置空。
*
* @param form 表单数据
* @return true|false
* @param form 密码验证表单(含密码)
* @return 解绑成功返回 true
* @throws BusinessException 如果用户不存在、未绑定邮箱或密码错误
*/
@Override
public boolean unbindEmail(PasswordVerifyForm form) {
@@ -667,6 +707,13 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
);
}
/**
* 修改用户状态.
*
* @param userId 用户 ID
* @param status 状态(1:启用 0:禁用)
* @return 修改成功返回 true
*/
@Override
public boolean updateUserStatus(Long userId, Integer status) {
return this.update(new LambdaUpdateWrapper<SysUser>()
@@ -676,9 +723,9 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
}
/**
* 获取用户选项列表
* 获取用户下拉选项列表.
*
* @return {@link List<Option<String>>} 用户选项列表
* @return 启用状态的用户选项(value 为用户 ID,label 为昵称)
*/
@Override
public List<Option<String>> listUserOptions() {

View File

@@ -229,5 +229,5 @@ captcha:
# 微信小程配置
wx:
miniapp:
app-id: Your_AppId
app-secret: Your_AppSecret
appid: Your_AppId
secret: Your_AppSecret