common-base 开发文档
1. 项目概述
common-base 是一个基于 Spring Boot 3.x 的公共基础模块,提供了一系列通用功能和工具类,用于支撑其他业务模块的开发。该模块包含了常量定义、异常体系、数据模型、工具类等核心组件,旨在提高开发效率和代码复用性。
2. 核心功能模块
2.1 常量定义
提供了项目中常用的常量定义,包括 HTTP 方法、操作类型、文件类型、IP 地址相关等常量。
主要常量类:
CommonConstants:通用常量(HTTP方法、操作类型、文件类型等)HeaderConstants:请求头常量RequestConstants:请求相关常量CaffeineTimeConstant:缓存时间常量
2.2 异常体系
定义了完整的异常体系,包括业务异常、参数异常、系统异常等,便于统一处理和管理系统中的异常情况。
主要异常类型:
BusinessException:业务异常ParameterInvalidException:参数无效异常NotFoundException:资源不存在异常DataConflictException:数据冲突异常InternalServerException:内部服务器异常RequestException:请求异常基类RequestSqlInjectException:SQL 注入异常RequestEncryptException:请求加密异常RemoteAccessException:远程访问异常ExcelException:Excel操作异常FileNotExistException:文件不存在异常
异常枚举:
ResponseCodeEnums:响应码枚举(200-成功,10001-参数错误,20001-用户错误等)RequestExceptionEnums:请求异常枚举(映射异常类到HTTP状态码和响应码)
2.3 数据模型
提供了通用的数据模型类,用于统一数据传输和响应格式。
主要模型类:
CommonResult:统一响应结果(包含status、code、msg、data、timestamp、traceId、reason)ErrorResult:错误结果Page:分页模型(支持当前页、每页大小、总记录数、数据列表)FlexBaseEntity:基础实体类(包含createTime、updateTime、createBy、updateBy等字段)ThreadShareDataDTO:线程共享数据BeyondSoftThirdAppKey:第三方应用密钥
2.4 工具类
提供了丰富的工具类,涵盖了 IP 处理、Excel 操作、Redis 操作、Spring 工具等多个方面。
主要工具类:
IpUtils:IP 地址处理工具(基于ip2region库)ExcelUtil:Excel 操作工具(基于FastExcel库)RedisUtil:Redis 操作工具(需要注入StringRedisTemplate)MongoUtil:MongoDB 操作工具SpringUtil:Spring 工具类(获取Bean、应用上下文等)TokenUtils:Token 处理工具SqlInjectUtils:SQL 注入检测工具ClassUtils:类操作工具MimeTypeUtils:MIME 类型工具PdfUtil:PDF 操作工具(基于PDFBox)
2.5 注解
提供了一系列注解,用于简化开发和提高代码可读性。
主要注解:
- 查询条件注解:
@Eq(等于)、@Gt(大于)、@Lt(小于)、@Gte(大于等于)、@Lte(小于等于)、@Ne(不等于)、@In(包含)、@Regex(正则匹配) - 操作符注解:
@AndOperator(与操作)、@OrOperator(或操作)、@NorOperator(非或操作) - 其他注解:
@ConditionsAnnotation(条件注解标记)、@No(否操作)
2.6 监听器
提供了实体监听器和 Excel 监听器,用于处理实体生命周期事件和 Excel 导入导出事件。
主要监听器:
BaseEntityListener:基础实体监听器(自动填充createBy、updateBy、createTime、updateTime字段)ExcelListener:Excel 监听器(用于Excel导入时的数据处理)
2.7 方言扩展
提供了数据库方言扩展,支持动态排序功能。
主要方言类:
DynamicOrderByDialect:动态排序方言(自动为查询添加order by id desc)
2.8 请求包装
提供了请求包装类,用于支持多次读取请求体。
主要包装类:
MultiReadHttpServletRequestWrapper:多读取 HTTP 请求包装器(缓存请求体,支持重复读取)
2.9 日志服务
提供了统一的日志服务,支持PostgreSQL自动分区管理。
主要服务类:
BeyondSoftLogService:日志服务接口BeyondSoftLogServiceImpl:日志服务实现(支持PostgreSQL自动分区、Caffeine缓存、异步批量写入)
3. 主要API和使用方法
3.1 统一响应结果
3.1.1 成功响应
// 基本成功响应
CommonResult result1 = CommonResult.success();
// 输出: {"status":200,"code":200,"msg":"操作成功","data":null,"timestamp":1634567890000,"traceId":null,"reason":null}
// 带数据的成功响应
User user = new User();
user.setId(1L);
user.setUsername("admin");
CommonResult result2 = CommonResult.success(user);
// 输出: {"status":200,"code":200,"msg":"操作成功","data":{"id":1,"username":"admin"},"timestamp":1634567890000,"traceId":"1234567890abcdef","reason":null}3.1.2 失败响应
// 基本失败响应
CommonResult result3 = CommonResult.failure(ResponseCodeEnums.PARAM_IS_INVALID);
// 输出: {"status":null,"code":10001,"msg":"参数校验不通过","data":null,"timestamp":1634567890000,"traceId":null,"reason":null}
// 带数据的失败响应
Map<String, String> errors = new HashMap<>();
errors.put("username", "用户名不能为空");
errors.put("password", "密码长度不能小于6位");
CommonResult result4 = CommonResult.failure(ResponseCodeEnums.PARAM_IS_INVALID, errors);
// 输出: {"status":null,"code":10001,"msg":"参数校验不通过","data":{"username":"用户名不能为空","password":"密码长度不能小于6位"},"timestamp":1634567890000,"traceId":null,"reason":null}3.1.3 在Controller中使用
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
// 业务逻辑
User user = userService.getUserById(id);
if (user == null) {
throw new BusinessException(ResponseCodeEnums.USER_NOT_EXIST);
}
return user;
}
@PostMapping
public User createUser(@RequestBody @Valid UserCreateRequest request) {
// 业务逻辑
User user = userService.createUser(request);
return user;
}
@GetMapping("/list")
public List<User> listUsers() {
// 业务逻辑
return userService.getAllUsers();
}
}
// 注意:实际返回会在web模块中经过统一处理,格式化为CommonResult3.2 异常体系
3.2.1 基本异常使用
// 抛出业务异常
throw new BusinessException("业务异常信息");
// 使用响应码枚举抛出异常
throw new BusinessException(ResponseCodeEnums.USER_NOT_EXIST);
// 带数据的异常
Map<String, Object> data = new HashMap<>();
data.put("userId", 1L);
throw new BusinessException(ResponseCodeEnums.USER_NOT_EXIST, data);
// 参数无效异常
throw new ParameterInvalidException("参数无效");
// 资源不存在异常
throw new NotFoundException("资源不存在");
// 数据冲突异常
throw new DataConflictException(ResponseCodeEnums.DATA_CONFLICT);
// 内部服务器异常
throw new InternalServerException();3.2.2 在Service中使用
@Service
public class UserServiceImpl implements UserService {
@Override
public User createUser(UserCreateRequest request) {
// 检查用户名是否已存在
User existingUser = userMapper.findByUsername(request.getUsername());
if (existingUser != null) {
throw new BusinessException(ResponseCodeEnums.USER_HAS_EXISTED);
}
// 检查参数有效性
if (request.getPassword().length() < 6) {
throw new ParameterInvalidException("密码长度不能小于6位");
}
// 创建用户
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
userMapper.insert(user);
return user;
}
@Override
public User getUserById(Long id) {
User user = userMapper.findById(id);
if (user == null) {
throw new NotFoundException(ResponseCodeEnums.USER_NOT_EXIST);
}
return user;
}
}3.3 工具类
3.3.1 IP地址处理工具
// 在Controller中获取请求IP地址
@GetMapping("/ip")
public CommonResult<String> getIp(HttpServletRequest request) {
Optional<String> ip = IpUtils.getIpAddress(request);
return CommonResult.success(ip.orElse(CommonConstants.NO_IP));
}
// 获取IP地址对应的位置信息
String ip = "192.168.1.1";
Optional<String> location = IpUtils.getLocationInfo(ip);
System.out.println("IP地址: " + ip + ",位置: " + location.orElse(CommonConstants.NO_LOCATION));
// 输出示例: IP地址: 192.168.1.1,位置: 中国|0|广东省|深圳市|电信3.3.2 Spring工具类
// 获取Spring Bean
UserService userService = SpringUtil.getBean(UserService.class);
// 通过name获取Bean
Object someBean = SpringUtil.getBean("beanName");
// 通过name和class获取Bean
UserService userService = SpringUtil.getBean("userService", UserService.class);
// 获取应用上下文
ApplicationContext applicationContext = SpringUtil.getApplicationContext();3.3.3 Redis工具类
注意:RedisUtil不是静态工具类,需要注入使用
@Service
public class UserService {
@Autowired
private RedisUtil redisUtil;
public void cacheUser(User user) {
// 设置字符串值
redisUtil.set("user:" + user.getId(), user.getUsername(), Duration.ofHours(1));
// 设置对象(自动序列化为JSON)
redisUtil.set("user:object:" + user.getId(), user, Duration.ofHours(1));
// 获取字符串值
String username = redisUtil.get("user:" + user.getId());
// 获取对象(自动反序列化)
User cachedUser = redisUtil.get("user:object:" + user.getId(), User.class);
// 删除键
redisUtil.delete("user:" + user.getId());
// 判断键是否存在
boolean exists = redisUtil.hasKey("user:" + user.getId());
// 设置过期时间
redisUtil.expire("user:" + user.getId(), 3600, TimeUnit.SECONDS);
}
}3.3.4 Excel工具类
// 导出Excel
@GetMapping("/export")
public void exportExcel(HttpServletResponse response) throws IOException {
List<User> userList = userService.getAllUsers();
ExcelUtil.exportExcel(userList, response, "用户列表", "用户信息", User.class);
}
// 导入Excel
@PostMapping("/import")
public CommonResult<Integer> importExcel(@RequestParam("file") MultipartFile file) throws IOException {
List<User> userList = ExcelUtil.importExcel(file, User.class);
int count = userService.batchInsert(userList);
return CommonResult.success(count);
}3.3.5 Sql注入检测工具
// 检测SQL注入
String sql = "SELECT * FROM user WHERE username = 'admin' OR '1'='1'";
boolean hasSqlInject = SqlInjectUtils.check(sql);
if (hasSqlInject) {
throw new RequestSqlInjectException("请求参数存在SQL注入攻击");
}
// 检测请求参数中的SQL注入
public void checkRequestParams(HttpServletRequest request) {
Enumeration<String> parameterNames = request.getParameterNames();
while (parameterNames.hasMoreElements()) {
String paramName = parameterNames.nextElement();
String paramValue = request.getParameter(paramName);
if (SqlInjectUtils.check(paramValue)) {
throw new RequestSqlInjectException("请求参数" + paramName + "存在SQL注入攻击");
}
}
}
// 删除转义字符
String safeText = SqlInjectUtils.removeEscapeCharacter("user' OR '1'='1");
// 结果: user OR 113.4 基础实体类
3.4.1 继承基础实体类
import com.beyondsoft.common.model.FlexBaseEntity;
import lombok.Data;
@Data
public class User extends FlexBaseEntity {
private Long id;
private String username;
private String password;
private String email;
private String phone;
private Integer status;
private Long departmentId;
}3.4.2 使用BaseEntityListener
import com.beyondsoft.common.listener.BaseEntityListener;
import com.mybatisflex.annotation.Table;
import lombok.Data;
@Data
@Table(value = "user")
@InsertListener(BaseEntityListener.class)
@UpdateListener(BaseEntityListener.class)
public class User extends FlexBaseEntity {
// 字段定义
}3.5 监听器
3.5.1 使用ExcelListener
import com.beyondsoft.common.listener.ExcelListener;
import cn.idev.excel.context.AnalysisContext;
public class UserExcelListener extends ExcelListener<User> {
private List<User> userList = new ArrayList<>();
@Override
public void invoke(User user, AnalysisContext context) {
// 处理每一行数据
userList.add(user);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 所有数据解析完成后处理
System.out.println("共解析了" + userList.size() + "条数据");
}
public List<User> getUserList() {
return userList;
}
}
// 使用监听器导入Excel
@PostMapping("/import-with-listener")
public CommonResult<Integer> importExcelWithListener(@RequestParam("file") MultipartFile file) throws IOException {
UserExcelListener listener = new UserExcelListener();
InputStream inputStream = new BufferedInputStream(file.getInputStream());
FastExcelFactory.read(inputStream, listener)
.head(User.class)
.doReadAll();
List<User> userList = listener.getUserList();
int count = userService.batchInsert(userList);
return CommonResult.success(count);
}3.6 请求包装器
3.6.1 在过滤器中使用MultiReadHttpServletRequestWrapper
@Component
public class RequestLogFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 包装请求,支持多次读取请求体
MultiReadHttpServletRequestWrapper wrappedRequest = new MultiReadHttpServletRequestWrapper(request);
// 读取请求体
String requestBody = wrappedRequest.getMultiBody();
// 记录请求日志
log.info("Request: {} {}", wrappedRequest.getMethod(), wrappedRequest.getRequestURI());
log.info("Request Body: {}", requestBody);
// 继续过滤器链,使用包装后的请求
filterChain.doFilter(wrappedRequest, response);
}
}3.7 常量使用
// HTTP方法常量
if (request.getMethod().equals(CommonConstants.GET)) {
// 处理GET请求
} else if (request.getMethod().equals(CommonConstants.POST)) {
// 处理POST请求
}
// 操作类型常量
log.info("用户执行了{}操作", CommonConstants.MODIFY_INSERT);
log.info("用户执行了{}操作", CommonConstants.MODIFY_UPDATE);
log.info("用户执行了{}操作", CommonConstants.MODIFY_DELETE);
// 文件类型常量
if (file.getContentType().equals(CommonConstants.EXCEL_CONTENT)) {
// 处理Excel文件
}
// 超级管理员常量
if (user.getUsername().equals(CommonConstants.SUPER_ADMIN)) {
// 超级管理员特殊处理
}
// 分隔符常量
String[] parts = "a,b,c".split(CommonConstants.SPLIT_COMMA);
String fileName = "file" + CommonConstants.SPLIT_LINE + "name";
String keyValue = "key" + CommonConstants.SPLIT_EQUAL + "value";3.8 分页模型
3.8.1 创建分页对象
// 创建分页对象
Page<User> page = new Page<>(1, 10); // 当前页码1,每页10条
// 设置总记录数
page.setTotal(100L);
// 设置数据
page.setData(userList);
// 获取分页信息
int currentPage = page.getPageNum(); // 当前页码
int pageSize = page.getPageSize(); // 每页大小
long total = page.getTotal(); // 总记录数
List<User> records = page.getData(); // 当前页数据
long skip = page.getSkip(); // 跳过的记录数(用于分页查询)
// 检查当前页是否有数据
boolean hasData = page.currentIsHaveData();3.8.2 在Controller中使用
@GetMapping("/list")
public CommonResult<Page<User>> listUsers(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String username) {
Page<User> page = new Page<>(current, size);
// 构建查询条件
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
if (StringUtils.isNotBlank(username)) {
queryWrapper.like(User::getUsername, username);
}
// 执行分页查询
long total = userMapper.selectCount(queryWrapper);
page.setTotal(total);
if (page.currentIsHaveData()) {
List<User> userList = userMapper.selectListByPage(page.getSkip(), page.getPageSize(), queryWrapper);
page.setData(userList);
} else {
page.setData(Collections.emptyList());
}
return CommonResult.success(page);
}3.9 日志服务
3.9.1 使用日志服务
@Service
public class AuditLogService {
@Autowired
private BeyondSoftLogService logService;
public void saveLoginLog(BeyondSoftLoginLog loginLog) {
// 保存登录日志(异步)
logService.save(loginLog, false);
}
public void saveAuditLog(BeyondSoftDataAuditLog auditLog) {
// 保存审计日志,需要默认对比
logService.save(auditLog, true);
}
public void batchSaveRequestLogs(List<BeyondSoftRequestLog> requestLogs) {
// 批量保存请求日志
logService.saveBatch(requestLogs, false);
}
public Page<BeyondSoftLoginLog> queryLoginLogs(Map<String, String> condition, Integer pageSize, Integer pageNum) {
// 查询登录日志
return logService.queryMongoLogs(BeyondSoftLoginLog.class, condition, pageSize, pageNum);
}
}3.9.2 日志服务特性
- 自动分区管理:基于PostgreSQL的按天自动分区
- Caffeine缓存:缓存分区状态,24小时过期
- 异步写入:使用@Async注解实现异步批量写入
- 多数据源支持:支持MyBatis-Flex多数据源切换
- 审计日志对比:自动对比新旧对象,生成变更记录
- 不可序列化字段处理:自动处理MultipartFile等不可序列化字段
4. 依赖说明
| 依赖项 | 版本 | 用途 | 备注 |
|---|---|---|---|
| fastexcel | - | Excel 操作 | 高性能Excel读写库 |
| commons-compress | - | 压缩处理 | 文件压缩解压 |
| lombok | - | 简化 Java 代码 | 自动生成getter/setter等 |
| ip2region | - | IP 地址解析 | IP地址定位库 |
| spring-boot-starter-data-redis | - | Redis 操作 | Redis客户端 |
| hutool-json | - | JSON 处理 | JSON序列化/反序列化 |
| hutool-db | - | 数据库操作 | 数据库工具 |
| spring-data-commons | - | Spring 数据处理 | Spring数据通用组件 |
| mybatis-flex-spring-boot3-starter | - | MyBatis Flex 支持 | ORM框架 |
| sa-token-spring-boot3-starter | - | 认证授权 | 权限认证框架 |
| spring-boot-starter-data-mongodb | - | MongoDB 支持 | MongoDB客户端 |
| spring-boot-starter-mail | - | 邮件发送 | 邮件服务 |
| spring-boot-starter-thymeleaf | - | 模板引擎 | 模板渲染 |
| spring-boot-starter-web | - | Web 支持 | Web MVC |
| commons-lang3 | - | 语言工具 | Apache Commons工具 |
| logback-core | - | 日志处理 | 日志框架核心 |
| logback-classic | - | 日志处理 | 日志框架实现 |
| caffeine | - | 缓存 | 本地缓存库 |
| spring-jdbc | - | JDBC 支持 | JDBC操作 |
| pdfbox | - | PDF 处理 | PDF文档操作 |
注意:实际版本由父pom统一管理,具体版本请查看pom.xml文件。
5. 配置说明
5.1 日志服务配置
# application.yml
beyond-soft:
log:
schema: audit # 日志表所在的schema,默认public
datasource-key: audit # MyBatis-Flex多数据源key,单数据源可不配置5.2 Redis配置
spring:
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 3000ms
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 05.3 IP地址数据库配置
确保 db/ip2region.xdb 文件存在于classpath下,用于IP地址定位。
6. 注意事项
RedisUtil使用:RedisUtil不是静态工具类,需要通过Spring注入使用。它依赖于StringRedisTemplate。
IP地址解析:使用 ip2region 进行 IP 地址解析,需确保 ip2region.xdb 文件存在于 classpath 的
db/目录下。日志服务初始化:BeyondSoftLogServiceImpl需要DataSource或JdbcTemplate进行初始化,支持单数据源和多数据源场景。
PostgreSQL分区:日志服务使用PostgreSQL的按天分区功能,会自动创建未来7天的分区。需要确保主表已存在。
Excel操作:ExcelUtil基于FastExcel库,支持.xlsx格式,性能优于Apache POI。
SQL注入检测:SqlInjectUtils使用正则表达式检测SQL注入,但可能存在误报或漏报,建议结合其他安全措施。
实体监听器:BaseEntityListener依赖于SaToken获取当前用户ID,如果未使用SaToken,会尝试从ThreadLocal获取。
异常处理:所有异常都继承自RequestException,可以通过RequestExceptionEnums映射到HTTP状态码和响应码。
工具类线程安全:大部分工具类都是线程安全的,但RedisUtil等需要确保正确配置连接池。
版本兼容性:该模块基于Spring Boot 3.x开发,与Spring Boot 2.x不兼容。
7. 常见问题
7.1 RedisUtil无法注入
问题:启动时报错,找不到RedisUtil的Bean。
解决方案:
- 确保配置了StringRedisTemplate的Bean
- 在配置类中创建RedisUtil的Bean:
@Configuration
public class RedisConfig {
@Bean
public RedisUtil redisUtil(StringRedisTemplate redisTemplate) {
return new RedisUtil(redisTemplate);
}
}7.2 IP地址解析失败
问题:IpUtils.getLocationInfo()返回空。
解决方案:
- 检查classpath下是否有
db/ip2region.xdb文件 - 确保文件路径正确:
src/main/resources/db/ip2region.xdb - 下载最新的ip2region数据库文件
7.3 日志服务分区创建失败
问题:日志保存时报分区不存在错误。
解决方案:
- 确保PostgreSQL版本支持分区功能(10.0+)
- 检查主表是否已创建
- 检查数据库用户是否有创建表的权限
- 查看日志服务的schema配置是否正确
7.4 BaseEntityListener无法获取用户ID
问题:保存实体时,createBy和updateBy字段为null。
解决方案:
- 确保已集成SaToken并正确配置
- 或者通过ThreadShareDataDTO设置用户信息
- 检查用户是否已登录
8. 版本历史
- v1.0.0 (2025-12-30): 初始版本
- 提供基础功能和工具类
- 统一的异常体系
- 完整的日志服务(PostgreSQL自动分区)
- 常用工具类(IP、Excel、Redis等)
9. 联系方式
如有问题或建议,请联系开发团队。
文档更新时间:2025-12-30
文档版本:1.0.0
模块版本:${revision} //