嘿,朋友!如果你正在寻找一个成熟的企业级 Java 项目框架,或者想学习如何从零搭建一个多模块的 Spring Boot 项目,那你来对地方了。这篇文档会带你一步步了解这个框架的每个角落,看完之后,你完全可以照着搭建出一个一模一样的框架来。
这个框架采用了 Spring Boot 3.2 + Vue 2.x 的前后端分离架构,后端是标准的多模块 Maven 项目,前端是基于 Element UI 的管理系统。整体设计遵循"高内聚、低耦合"的原则,各模块职责清晰,扩展性强。
一、技术栈总览
在开始之前,先让你对整个技术栈有个全局认识。这套框架选用的都是目前主流且稳定的技术:
后端核心技术:
Java 17 + Spring Boot 3.2.11
PostgreSQL 数据库 + Druid 连接池
MyBatis-Plus 3.5.12(ORM 框架)
Redis(缓存 + 会话管理)
Sa-Token 1.38.0(认证授权框架)
Undertow(替代 Tomcat 的高性能 Web 服务器)
工具库:
Hutool 5.8.34(Java 工具库,超级好用)
Retrofit2 + OkHttp(HTTP 客户端)
EasyExcel 3.3.4(Excel 导入导出)
ZXing 3.5.3(二维码生成)
外部服务集成:
微信公众号(weixin-java-mp 4.6.8.B)
阿里云短信
邮件服务(Jakarta Mail)
钉钉机器人通知
前端技术:
Vue 2.6.10 + Vue Router + Vuex
Element UI 2.15.14
Axios(HTTP 请求)
ECharts(图表)
二、项目模块结构
这个框架采用 Maven 多模块设计,一共有 8 个模块,每个模块都有明确的职责。先看看整体结构:
ip-monitor/ # 项目根目录
├── pom.xml # 父 POM,统一管理依赖版本
├── monitor-standalone/ # 启动模块,Spring Boot 入口
├── monitor-api/ # REST API 控制器层
├── monitor-service/ # 业务逻辑层
├── monitor-common/ # 公共模块(实体、工具、常量)
├── monitor-spring/ # Spring 配置模块
├── monitor-external/ # 外部服务集成模块
│ ├── external-sms/ # 短信服务
│ ├── external-mail/ # 邮件服务
│ ├── external-wx/ # 微信服务
│ └── external-dingding/ # 钉钉通知
├── monitor-open/ # 开放 API 模块
└── monitor-ui/ # Vue 前端项目模块依赖关系是这样的:
monitor-standalone (启动模块)
↓ 依赖
monitor-api (API 层)
↓ 依赖
monitor-service (业务层)
↓ 依赖
├── monitor-open (开放 API)
├── monitor-spring (Spring 配置)
└── monitor-external (外部服务)
↓ 依赖
monitor-common (公共模块)这种分层设计的好处是:上层模块可以调用下层模块,但下层不能调用上层,保证了代码的单向依赖,避免循环依赖的问题。
三、各模块详解
3.1 monitor-common(公共模块)
这是整个框架的基石,所有其他模块都会依赖它。它包含了项目中最基础、最通用的代码。
模块结构:
monitor-common/
└── src/main/java/com/mank/ipms/common/
├── api/ # API 响应封装
│ ├── ResponseData.java # 统一响应格式
│ ├── ResultCode.java # 响应码接口
│ └── IpmsResultCode.java # 具体响应码枚举
├── constant/ # 常量定义
│ ├── IpmsConstant.java # 系统常量
│ └── ...Enum.java # 各种枚举类
├── core/
│ ├── event/ # 事件机制
│ ├── exception/ # 自定义异常
│ └── model/
│ ├── dto/ # 数据传输对象
│ ├── entity/ # 数据库实体类
│ └── vo/ # 视图对象
├── excel/ # Excel 导入导出工具
├── mapper/ # MyBatis Mapper 接口
├── secure/ # 安全相关工具
└── util/ # 工具类核心组件说明:
1. 统一响应格式(ResponseData)
所有 API 接口都返回统一的格式,前端处理起来很方便:
@Data
@NoArgsConstructor
public class ResponseData {
private boolean success; // 是否成功
private int code; // 响应码
private String msg; // 响应消息
private Object data; // 响应数据
// 成功响应
public static ResponseData data(Object data) {
return data(data, IpmsResultCode.SUCCESS.msg);
}
// 失败响应
public static ResponseData fail(ResultCode resultCode, String msg) {
return new ResponseData(resultCode, msg);
}
}2. 实体基类(BaseEntity / BaseIdEntity)
所有数据库实体都继承这两个基类,自动包含创建时间、更新时间、逻辑删除等公共字段:
// 没有 ID 的基类
@Data
public class BaseEntity implements Serializable {
@TableField(fill = FieldFill.INSERT)
private String createBy; // 创建者
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime; // 创建时间
@TableField(fill = FieldFill.UPDATE)
private String updateBy; // 更新者
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime; // 更新时间
@TableLogic(value = "0", delval = "1")
@TableField(fill = FieldFill.INSERT)
private Integer deleted; // 逻辑删除
@Version
private Long version; // 乐观锁版本号
}
// 有 ID 的基类
@Data
public class BaseIdEntity extends BaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private String id; // 主键,使用雪花算法生成
}3. 事件机制(EventManager)
框架内置了一套简单的事件驱动机制,用于解耦业务逻辑:
// 定义事件类型
public enum MonitorEvents {
SEND_MSG_APPLY_INFO, // 法律状态变化发送消息
SEND_MSG_COST_INFO, // 待缴费发送消息
}
// 触发事件
EventManager.getInstance().triggerEvent(MonitorEvents.SEND_MSG_APPLY_INFO, params);
// 监听事件(实现 EventListener 接口)
@Component
public class MyEventListener implements EventListener {
@Override
public MonitorEvents[] listenEvents() {
return new MonitorEvents[]{MonitorEvents.SEND_MSG_APPLY_INFO};
}
@Override
public void onEvent(MonitorEvents event, Object params) {
// 处理事件
}
}3.2 monitor-spring(Spring 配置模块)
这个模块集中管理所有 Spring 相关的配置,包括 MyBatis-Plus、Redis、线程池、Web MVC 等。把配置独立成模块的好处是:配置代码集中管理,其他模块只需要依赖这个模块就能获得所有配置能力。
模块结构:
monitor-spring/
└── src/main/java/com/mank/ipms/spring/
├── config/
│ ├── ThreadPoolConfiguration.java # 线程池配置
│ ├── SchedulingConfig.java # 定时任务配置
│ └── TaskManagerConfiguration.java # 任务管理配置
├── mp/config/
│ └── MybatisPlusConfiguration.java # MyBatis-Plus 配置
├── redis/
│ ├── cache/IpmsRedis.java # Redis 操作工具类
│ ├── config/RedisTemplateConfiguration.java
│ └── ratelimiter/ # 限流器
├── web/
│ ├── IpmsWebMvcConfigurer.java # Web MVC 配置
│ ├── JacksonConfig.java # JSON 序列化配置
│ └── DeviceInfoInterceptor.java # 设备信息拦截器
└── log/
└── RestExceptionTranslator.java # 全局异常处理核心配置说明:
1. 线程池配置(ThreadPoolConfiguration)
配置了一个通用的线程池,用于异步任务处理:
@Configuration
@EnableAsync
public class ThreadPoolConfiguration {
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
int corePoolSize = Runtime.getRuntime().availableProcessors();
executor.setCorePoolSize(corePoolSize * 2 + 1); // 核心线程数
executor.setMaxPoolSize(corePoolSize * 2 * 2 + 1); // 最大线程数
executor.setKeepAliveSeconds(10); // 空闲线程存活时间
executor.setQueueCapacity(500); // 等待队列大小
executor.setThreadNamePrefix("custom-thread-"); // 线程名前缀
// 拒绝策略:交由调用者线程执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(10);
executor.initialize();
return executor;
}
}2. 定时任务配置(SchedulingConfig)
支持通过配置开关控制定时任务是否启用:
@Configuration
@EnableScheduling
@ConditionalOnProperty(name="spring.scheduling.enabled", havingValue = "true", matchIfMissing = true)
public class SchedulingConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
int corePoolSize = Runtime.getRuntime().availableProcessors();
scheduler.setPoolSize(corePoolSize);
scheduler.setThreadNamePrefix("scheduled-task-");
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(30);
scheduler.initialize();
taskRegistrar.setTaskScheduler(scheduler);
}
}3. MyBatis-Plus 配置(MybatisPlusConfiguration)
这是数据库操作的核心配置,包含分页插件、自动填充、动态表名等功能:
@Configuration
@MapperScan("com.mank.ipms.**.mapper.**")
public class MybatisPlusConfiguration {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 动态表名插件(支持分表)
DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor =
new DynamicTableNameInnerInterceptor((sql, tableName) -> {
if (IpmsConstant.TABLE_NAMES.contains(tableName)) {
return tableName + "_" + LocalDateTimeUtil.format(LocalDateTimeUtil.now(), "yyyyMM");
}
return tableName;
});
interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL));
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
@Bean
public GlobalConfig globalConfig() {
GlobalConfig globalConfig = new GlobalConfig();
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
dbConfig.setTableUnderline(true); // 驼峰转下划线
dbConfig.setIdType(IdType.ASSIGN_UUID); // ID 生成策略
dbConfig.setLogicDeleteField("deleted"); // 逻辑删除字段
globalConfig.setDbConfig(dbConfig);
globalConfig.setMetaObjectHandler(new IpmsMetaObjectHandler()); // 自动填充
return globalConfig;
}
// 自动填充处理器
public static class IpmsMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTimeUtil.now());
this.strictInsertFill(metaObject, "deleted", Integer.class, 0);
String userId = ContextHandler.getUserId();
this.strictInsertFill(metaObject, "createBy", String.class, userId);
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTimeUtil.now());
String userId = ContextHandler.getUserId();
this.strictUpdateFill(metaObject, "updateBy", String.class, userId);
}
}
}4. Redis 配置与工具类
Redis 配置使用 JSON 序列化,方便调试和查看数据:
@Configuration
public class RedisTemplateConfiguration {
@Bean
public RedisSerializer<Object> redisSerializer() {
return new GenericJackson2JsonRedisSerializer("@class");
}
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory,
RedisSerializer<Object> redisSerializer) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
RedisKeySerializer keySerializer = new RedisKeySerializer();
redisTemplate.setKeySerializer(keySerializer);
redisTemplate.setHashKeySerializer(keySerializer);
redisTemplate.setValueSerializer(redisSerializer);
redisTemplate.setHashValueSerializer(redisSerializer);
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
@Bean
public IpmsRedis ipmsRedis(RedisTemplate<String, Object> redisTemplate,
StringRedisTemplate stringRedisTemplate) {
return new IpmsRedis(redisTemplate, stringRedisTemplate);
}
}封装的 Redis 工具类,使用起来非常方便:
public class IpmsRedis {
// 获取值
public static <T> T get(String key) {
return (T) valueOps.get(key);
}
// 设置值(带过期时间)
public static void setEx(String key, Object value, Duration timeout) {
valueOps.set(key, value, timeout);
}
// 删除
public static Boolean del(String key) {
return redisTemplate.delete(key);
}
// Set 集合操作
public static void addSet(String key, Object value) {
setOps.add(key, value);
}
public static Boolean setIsMember(String key, Object value) {
return setOps.isMember(key, value);
}
}5. Web MVC 配置与认证拦截
配置了 Sa-Token 认证拦截器,实现登录校验:
@Configuration
public class IpmsWebMvcConfigurer implements WebMvcConfigurer {
// Sa-Token 整合 JWT (Simple 简单模式)
@Bean
public StpLogic getStpLogicJwt() {
return new StpLogicJwtForSimple();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 1. 设备信息采集拦截器
registry.addInterceptor(new DeviceInfoInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/error", "/favicon.ico", "/static/**");
// 2. Sa-Token 登录校验拦截器
registry.addInterceptor(new SaInterceptor(handle -> {
StpUtil.checkLogin();
// 将用户 ID 放到线程变量中
ContextHandler.setUserId(StpUtil.getLoginId(StpUtil.getLoginIdAsString()));
}))
.addPathPatterns("/**")
.excludePathPatterns("/openapi/**"); // 开放 API 不需要登录
}
}6. 全局异常处理(RestExceptionTranslator)
统一处理各种异常,返回友好的错误信息:
@Slf4j
@RestControllerAdvice
public class RestExceptionTranslator {
// 业务异常
@ExceptionHandler(ServiceException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseData handleError(ServiceException e) {
log.error("业务异常", e);
return ResponseData.fail(e.getResultCode(), e.getMessage());
}
// 参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseData handleValidationExceptions(MethodArgumentNotValidException ex) {
Set<String> errors = new LinkedHashSet<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.add(error.getDefaultMessage()));
return ResponseData.fail(IpmsResultCode.PARAM_VALID_ERROR, String.join(",", errors));
}
// Sa-Token 认证异常
@ExceptionHandler(SaTokenException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ResponseData handleError(SaTokenException e) {
log.error("请求未授权", e);
return ResponseData.fail(IpmsResultCode.UN_AUTHORIZED, e.getMessage());
}
// 其他异常
@ExceptionHandler(Throwable.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseData handleError(Throwable e) {
log.error("服务器异常", e);
return ResponseData.fail(IpmsResultCode.INTERNAL_SERVER_ERROR, e.getMessage());
}
}3.3 monitor-external(外部服务集成模块)
这个模块专门用来集成各种外部服务,每个服务都是一个独立的子模块,方便按需引入。
模块结构:
monitor-external/
├── pom.xml # 父 POM
├── external-sms/ # 阿里云短信服务
├── external-mail/ # 邮件服务
├── external-wx/ # 微信公众号服务
└── external-dingding/ # 钉钉机器人通知1. 邮件服务(external-mail)
封装了邮件发送和验证码功能:
@Component
public class EmailService {
private static final String EMAIL_LIMIT_KEY_PREFIX = "email:limit:";
// 发送验证码
public boolean sendEmailCode(String email) {
// 检查 60 秒内是否已发送
String limitKey = EMAIL_LIMIT_KEY_PREFIX + email;
String limitValue = IpmsRedis.get(limitKey);
if (ObjectUtil.isNotEmpty(limitValue)) {
throw new ServiceException("60秒内只能发送一次验证码,请稍后再试");
}
String code = RandomUtil.randomNumbers(6);
boolean flag = MailUtils.sendHtmlEmail(email, "验证码",
"您的验证码:<h1>" + code + "</h1>验证码10分钟后过期。");
// 设置 60 秒发送限制
IpmsRedis.setEx(limitKey, "1", Duration.ofSeconds(60));
// 验证码 10 分钟有效
IpmsRedis.setEx("email:code:" + email, code, Duration.ofMinutes(10));
return flag;
}
// 校验验证码
public boolean validateEmailCode(String email, String code) {
String cache = IpmsRedis.get("email:code:" + email);
if (ObjectUtil.isNotEmpty(cache) && cache.equals(code)) {
IpmsRedis.del("email:code:" + email);
return true;
}
return false;
}
}2. 微信公众号服务(external-wx)
封装了微信登录、模板消息推送等功能:
@Slf4j
@Component
public class WechatMpService {
@Resource
WxMpService wxMpService;
@Resource
WeChatProperties weChatConfig;
@Resource
TaskExecutor taskExecutor;
// 检查用户是否关注服务号
public boolean checkUserSubscribed(String openId) {
if (StrUtil.isBlank(openId)) {
return false;
}
try {
var wxUser = wxMpService.getUserService().userInfo(openId);
return wxUser != null && wxUser.getSubscribe();
} catch (WxErrorException e) {
log.error("检查用户关注状态失败:{}", e.getMessage());
return false;
}
}
// 发送登录成功模板消息
public String sendLoginSuccessMsg(String openId, String userName,
UserLoginType4WechatEnum loginType) {
String templateId = weChatConfig.getMsgTemplateId().getLoginSuccess();
WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
.toUser(openId)
.templateId(templateId)
.build();
String loginTime = LocalDateTime.now().format(
DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"));
templateMessage.addData(new WxMpTemplateData("thing1", userName));
templateMessage.addData(new WxMpTemplateData("time3", loginTime));
templateMessage.addData(new WxMpTemplateData("character_string4", ContextHandler.getIP()));
templateMessage.addData(new WxMpTemplateData("const2", loginType.getText()));
return wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
}
// 异步发送(使用线程池)
public void sendLoginSuccessMsg4Task(String openId, String userName,
UserLoginType4WechatEnum loginType) {
taskExecutor.execute(() -> sendLoginSuccessMsg(openId, userName, loginType));
}
}3.4 monitor-service(业务逻辑层)
这是核心业务逻辑所在的模块,包含所有的 Service 类和定时任务。
模块结构:
monitor-service/
└── src/main/java/com/mank/ipms/service/
├── uc/ # 用户中心
│ ├── UserService.java # 用户服务
│ └── WechatService.java # 微信登录服务
├── membership/ # 会员管理
│ ├── MembershipLevelService.java
│ └── MembershipOrderService.java
├── mail/ # 邮件相关
│ └── PatentWeeklyReportService.java
├── sms/ # 短信服务
│ └── SmsService.java
├── sys/ # 系统服务
│ └── SysMessageService.java
├── task/ # 定时任务
│ ├── UserJob.java
│ └── ZlJob.java
└── zl/ # 专利相关(业务模块)
├── event/ # 事件监听
├── proxy/ # 代理 IP 管理
└── search/ # 搜索服务核心服务示例 - 用户服务(UserService):
@Service
@Slf4j
public class UserService extends ServiceImpl<UserMapper, UserEntity> {
@Resource
private MembershipLevelService membershipLevelService;
@Resource
private SmsService smsService;
@Resource
private EmailService emailService;
@Resource
private WechatMpService wechatMpService;
@Resource
TaskExecutor taskExecutor;
// 用户登录
public SaTokenInfo token(LoginDTO loginDTO) {
// 1. 查询用户
UserEntity userEntity = getOne(Wrappers.lambdaQuery(UserEntity.class)
.and(waper -> waper
.eq(UserEntity::getEmail, loginDTO.getLoginName())
.or().eq(UserEntity::getPhoneNumber, loginDTO.getLoginName())
.or().eq(UserEntity::getLoginName, loginDTO.getLoginName()))
.eq(UserEntity::getUserStatus, UserEntity.UserStatusEnum.ENABLE.name())
.last("limit 1"));
Assert.notNull(userEntity, () -> new ServiceException("当前登录账号不存在"));
// 2. 解密并验证密码
String decryptPw = PasswordUtil.decryptPw(loginDTO.getPassword());
if (!BCrypt.checkpw(decryptPw, userEntity.getPassword())) {
throw new ServiceException("密码错误");
}
// 3. Sa-Token 登录
StpUtil.login(userEntity.getId());
// 4. 异步发送微信登录通知
wechatMpService.sendLoginSuccessMsg4Task(
userEntity.getOpenId(),
userEntity.getUserName(),
UserLoginType4WechatEnum.PASSWORD
);
return StpUtil.getTokenInfo();
}
}定时任务示例(UserJob):
@Slf4j
@Component
public class UserJob {
@Resource
private UserService userService;
@Resource
private PatentWeeklyReportService patentWeeklyReportService;
@Resource
private WechatMpService wechatMpService;
// 每天 00:01 执行,检查会员到期
@Scheduled(cron = "0 1 0 * * ?")
public void checkMembershipExpire() {
userService.checkMembershipExpire();
}
// 每周一上午 10:00 执行,发送周报邮件
@Scheduled(cron = "0 0 10 ? * MON")
public void sendPatentWeeklyReport() {
// 遍历所有有邮箱的用户
// 检查用户会员等级是否有发送邮件的权限
// 生成周报并发送
}
// 每天 9:00 和 15:00 执行,发送微信消息通知
@Scheduled(cron = "0 0 9,15 * * ?")
public void sendSysMessageWechatNotice() {
// 查询三天内未读消息
// 按用户分组
// 发送微信模板消息
}
}3.5 monitor-api(REST API 控制器层)
这个模块包含所有的 REST API 接口,是前端调用的入口。
模块结构:
monitor-api/
└── src/main/java/com/mank/ipms/api/
├── common/ # 通用接口
│ ├── SmsController.java # 短信接口
│ └── UtilController.java # 工具接口
├── login/ # 登录相关
│ ├── LoginController.java
│ └── WeChatController.java
├── uc/ # 用户中心
│ ├── UserController.java
│ └── SubAccountController.java
├── membership/ # 会员管理
│ └── MembershipLevelController.java
├── sys/ # 系统管理
│ └── SysMessageController.java
├── wx/ # 微信支付
│ └── WechatPayController.java
└── zl/ # 专利相关
├── ZlIpInfoController.java
├── ZlQueryController.java
└── ...控制器示例(LoginController):
@Slf4j
@RestController
@RequestMapping("/api/login")
public class LoginController {
@Resource
private UserService userService;
// 登录获取 Token
@SaIgnore // 忽略登录校验
@PostMapping("/token")
public SaTokenInfo token(@RequestBody @Validated(LoginDTO.Token.class) LoginDTO loginDTO) {
return userService.token(loginDTO);
}
// 退出登录
@SaIgnore
@PostMapping("/logout")
public ResponseData logout() {
StpUtil.logout();
return ResponseData.data(Boolean.TRUE);
}
// 获取当前用户信息
@PostMapping("/currentUser")
public ResponseData currentUser() {
return ResponseData.data(userService.currentUser());
}
// 邮箱注册
@SaIgnore
@PostMapping("/registerByEmail")
public ResponseData registerByEmail(
@RequestBody @Validated(RegisterByEmailDTO.Create.class) RegisterByEmailDTO dto) {
return ResponseData.data(userService.registerByEmail(dto));
}
}3.6 monitor-open(开放 API 模块)
这个模块提供对外开放的 API 接口,使用签名验证机制保证安全性。
签名验证拦截器(OpenInterceptor):
@Slf4j
public class OpenInterceptor implements HandlerInterceptor {
public static final String APP_ID = "appId";
public static final String SIGN = "sign";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object object) throws Exception {
String appId = request.getHeader(APP_ID);
String sign = request.getHeader(SIGN);
Assert.isTrue(StrUtil.isAllNotBlank(appId, sign),
() -> new ServiceException("请求头信息缺失"));
// 获取请求参数
String param = getRequestBody(request);
if (StringUtils.isBlank(param)) {
param = getUrlPram(request);
}
// 查询应用信息
OpenClientEntity openClientEntity = openClientMapper.selectOne(
new QueryWrapper<OpenClientEntity>()
.eq("app_id", appId)
.last("limit 1"));
Assert.notNull(openClientEntity, () -> new ServiceException("appId不存在"));
// 验签:MD5(参数 + appId + appSecret)
String combination = param.concat(appId).concat(openClientEntity.getAppSecret());
String combinationMd5 = SecureUtil.md5(combination);
Assert.isTrue(StringUtils.equals(sign, combinationMd5),
() -> new ServiceException("验签失败"));
return true;
}
}开放 API 配置:
@Configuration
public class OpenWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new OpenInterceptor())
.addPathPatterns("/openapi/**"); // 只拦截开放 API
}
}3.7 monitor-standalone(启动模块)
这是 Spring Boot 应用的启动入口,包含主类和配置文件。
启动类:
@Slf4j
@SpringBootApplication
public class IpmsApplication {
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication(IpmsApplication.class);
springApplication.run(args);
log.info("启动成功");
}
}配置文件结构:
monitor-standalone/src/main/resources/
├── application.yml # 主配置文件
├── application-prod.yml # 生产环境配置
└── logback.xml # 日志配置主配置文件(application.yml):
spring:
profiles:
active: prod # 激活生产环境配置
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: org.postgresql.Driver
druid:
validation-query: SELECT 1
servlet:
multipart:
max-file-size: 100MB
max-request-size: 300MB
# Thymeleaf 邮件模板配置
thymeleaf:
prefix: classpath:/templates/
suffix: .html
mode: HTML
encoding: UTF-8
cache: true生产环境配置(application-prod.yml):
server:
port: 18080
spring:
file:
path: ./file/ # 文件存储路径
datasource:
url: jdbc:postgresql://localhost:5432/your_db
username: your_username
password: your_password
data:
redis:
host: localhost
port: 6379
password: your_redis_password
database: 0
timeout: 3000ms
jedis:
pool:
enabled: true
max-active: 8
max-idle: 8
min-idle: 0
scheduling:
enabled: true # 是否启用定时任务
# 短信配置
ipms:
sms:
enable: false
templateId: ""
regionId: cn-hangzhou
accessKey: ""
secretKey: ""
signName: ""
# 邮件配置
mail:
host: smtp.126.com
port: 994
sslEnable: true
from: your_email@126.com
user: your_email@126.com
pass: your_auth_code
signature: 系统签名
# 钉钉机器人配置
dingding:
webhook: https://oapi.dingtalk.com/robot/send?access_token=xxx
secret: SECxxx
# 微信公众号配置
wx:
mp:
appId: your_app_id
secret: your_app_secret
redirectUrl: https://your-domain.com/callback
msgTemplateId:
signature: 系统签名
loginSuccess: template_id_1
zlNotice: template_id_2
pay:
enabled: false
mch-id: your_mch_id
mch-serial-no: your_serial_no
private-key-path: cert/apiclient_key.pem
api-v3-key: your_api_v3_key
notify-url: https://your-domain.com/api/wx/pay/notify/pay日志配置(logback.xml):
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="5 seconds" debug="false">
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d | %5.5level [%20.20thread] %50.50logger{50} : %.-1024msg%n</pattern>
</encoder>
</appender>
<!-- 按天滚动的文件日志 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logs.dir:-logs}/system.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${logs.dir:-logs}/system.%d{yyyy-MM-dd}.%i.log.zip</fileNamePattern>
<maxFileSize>20MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
</rollingPolicy>
<encoder>
<pattern>%d | %5.5level [%20.20thread] %50.50logger{50} : %msg%n</pattern>
</encoder>
</appender>
<!-- 异步日志 -->
<appender name="ASYNC-INFO" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>512</queueSize>
<discardingThreshold>0</discardingThreshold>
<includeCallerData>true</includeCallerData>
<appender-ref ref="FILE"/>
</appender>
<!-- 特定模块独立日志文件 -->
<logger name="com.mank.ipms.service.zl.search" level="INFO" additivity="false">
<appender-ref ref="ZL-SEARCH-FILE"/>
<appender-ref ref="STDOUT"/>
</logger>
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ASYNC-INFO"/>
</root>
</configuration>3.8 monitor-ui(Vue 前端项目)
前端采用 Vue 2.x + Element UI 的技术栈,是一个标准的后台管理系统模板。
项目结构:
monitor-ui/
├── package.json # 依赖配置
├── vue.config.js # Vue CLI 配置
├── src/
│ ├── main.js # 入口文件
│ ├── App.vue # 根组件
│ ├── permission.js # 路由权限控制
│ ├── settings.js # 系统设置
│ ├── api/ # API 接口定义
│ ├── assets/ # 静态资源
│ ├── components/ # 公共组件
│ ├── directive/ # 自定义指令
│ ├── filters/ # 过滤器
│ ├── icons/ # SVG 图标
│ ├── layout/ # 后台布局组件
│ ├── layout4customer/ # 客户端布局组件
│ ├── router/ # 路由配置
│ ├── store/ # Vuex 状态管理
│ ├── styles/ # 全局样式
│ ├── utils/ # 工具函数
│ └── views/ # 页面视图路由配置(router/index.js):
import Vue from 'vue'
import Router from 'vue-router'
import Layout from '@/layout'
import Layout4customer from '@/layout4customer'
Vue.use(Router)
// 静态路由(不需要权限)
export const constantRoutes = [
{
path: '/',
redirect: '/customer/static/index',
hidden: true
},
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/register',
component: () => import('@/views/login/register'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/error-page/404'),
hidden: true
}
]
// 动态路由(需要权限)
export const asyncRoutes = () => [
{
path: '/index',
component: Layout,
name: 'Index',
redirect: { name: 'IndexPage' },
meta: { title: '首页' },
children: [
{
path: '',
component: () => import('@/views/system/Index'),
name: 'IndexPage',
meta: { title: '首页', icon: 'dashboard' }
}
]
},
{
path: '/system',
component: Layout,
name: 'System',
meta: { title: '系统管理', icon: 'el-icon-setting' },
children: [
{
path: 'user',
component: () => import('@/views/system/user'),
name: 'UserManagement',
meta: { title: '用户管理', icon: 'el-icon-user' }
}
// ... 其他子路由
]
}
]
const router = new Router({
mode: 'history',
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
export default routerAxios 请求封装(utils/request.js):
import axios from 'axios'
import { Message, MessageBox } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
const service = axios.create({
baseURL: '',
timeout: 1000 * 60 * 20 // 20 分钟超时
})
// 请求拦截器
service.interceptors.request.use(
config => {
if (store.getters.token) {
config.headers['ipms-token'] = getToken()
}
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
const res = response.data
if (res.code === 401) {
// 未登录,触发登录弹框
showGlobalLoginModal()
return Promise.reject(new Error('未登录'))
} else if (res.code === 40001) {
// 会员权益不足
MessageBox.alert('您的会员权益等级不够', '提示', {
confirmButtonText: '我知道了',
type: 'warning'
})
return Promise.reject(new Error(res.msg))
} else if (res.code !== 200) {
Message({
message: res.msg || '服务器异常',
type: 'error',
duration: 5000
})
return Promise.reject(new Error(res.msg))
}
return res
},
error => {
Message({
message: error.response?.data?.msg || '服务器异常',
type: 'error',
duration: 5000
})
return Promise.reject(error)
}
)
export default serviceVue CLI 配置(vue.config.js):
module.exports = {
publicPath: '/',
outputDir: 'dist',
assetsDir: 'static',
lintOnSave: false,
productionSourceMap: false,
devServer: {
port: 18090,
open: true,
proxy: {
'/api': {
target: 'http://127.0.0.1:18080',
changeOrigin: true,
pathRewrite: {
'^/api': '/api'
}
}
}
},
configureWebpack: {
resolve: {
alias: {
'@': resolve('src')
}
}
}
}四、父 POM 配置详解
父 POM 是整个项目的核心配置文件,统一管理所有模块的依赖版本和构建配置。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mank.ipms</groupId>
<artifactId>ip-monitor</artifactId>
<version>2025.1.0</version>
<packaging>pom</packaging>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 版本号统一管理 -->
<ipms.version>2025.1.0</ipms.version>
<spring.boot.version>3.2.11</spring.boot.version>
<retrofit2.version>2.11.0</retrofit2.version>
<okhttp.version>4.12.0</okhttp.version>
<hutool.all.version>5.8.34</hutool.all.version>
<postgresql.version>42.7.4</postgresql.version>
<druid.starter.version>1.2.23</druid.starter.version>
<mybatis-plus.start.version>3.5.12</mybatis-plus.start.version>
<sa.token.version>1.38.0</sa.token.version>
<weixin.java.version>4.6.8.B</weixin.java.version>
<easyexcel.version>3.3.4</easyexcel.version>
</properties>
<!-- 子模块 -->
<modules>
<module>monitor-api</module>
<module>monitor-open</module>
<module>monitor-common</module>
<module>monitor-service</module>
<module>monitor-spring</module>
<module>monitor-standalone</module>
<module>monitor-external</module>
</modules>
<!-- 依赖版本管理 -->
<dependencyManagement>
<dependencies>
<!-- 内部模块 -->
<dependency>
<groupId>com.mank.ipms</groupId>
<artifactId>monitor-api</artifactId>
<version>${ipms.version}</version>
</dependency>
<dependency>
<groupId>com.mank.ipms</groupId>
<artifactId>monitor-common</artifactId>
<version>${ipms.version}</version>
</dependency>
<!-- ... 其他内部模块 ... -->
<!-- 数据库相关 -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.starter.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.start.version}</version>
</dependency>
<!-- 认证授权 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${sa.token.version}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<version>${sa.token.version}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>${sa.token.version}</version>
</dependency>
<!-- 工具库 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.all.version}</version>
</dependency>
<!-- Spring Boot 依赖管理 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 所有模块共享的依赖 -->
<dependencies>
<!-- Web 启动器(排除 Tomcat,使用 Undertow) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<!-- Maven 仓库配置(使用阿里云镜像) -->
<repositories>
<repository>
<id>aliyun-repos</id>
<url>https://maven.aliyun.com/repository/public/</url>
</repository>
</repositories>
<!-- 构建配置 -->
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>五、数据库设计
这个框架使用 PostgreSQL 数据库,充分利用了 PostgreSQL 的 JSONB 类型来存储灵活的扩展数据。
5.1 基础表结构设计原则
所有表都遵循以下设计原则:
主键使用 VARCHAR(20),存储雪花算法生成的 ID
包含 create_by、create_time、update_by、update_time 审计字段
使用 deleted 字段实现逻辑删除(0=未删除,1=已删除)
使用 version 字段实现乐观锁
扩展信息使用 JSONB 类型存储
5.2 完整建表 SQL
-------------------------------------用户相关表-------------------------------------
-- 用户表
CREATE TABLE IF NOT EXISTS uc_user
(
id VARCHAR(20) PRIMARY KEY,
login_name VARCHAR(50) NOT NULL,
password VARCHAR(100) NOT NULL,
user_name VARCHAR(50),
phone_number VARCHAR(20),
email VARCHAR(50),
open_id VARCHAR(100),
union_id VARCHAR(100),
avatar_url VARCHAR(500),
nick_name VARCHAR(100),
user_status VARCHAR(20),
user_type VARCHAR(20) DEFAULT 'CUSTOM',
level_code VARCHAR(20) DEFAULT 'FREE',
extra_info jsonb,
membership_expire_time TIMESTAMP(6),
parent_user_id VARCHAR(64),
wx_push_enabled BOOLEAN DEFAULT true,
create_by VARCHAR(32),
create_time TIMESTAMP(6),
update_by VARCHAR(32),
update_time TIMESTAMP(6),
deleted INT4,
version INT8 DEFAULT 0
);
COMMENT ON TABLE uc_user IS '用户表';
COMMENT ON COLUMN uc_user.id IS '主键ID';
COMMENT ON COLUMN uc_user.login_name IS '登录名';
COMMENT ON COLUMN uc_user.password IS '密码(BCrypt加密)';
COMMENT ON COLUMN uc_user.user_name IS '用户姓名';
COMMENT ON COLUMN uc_user.phone_number IS '手机号';
COMMENT ON COLUMN uc_user.email IS '邮箱';
COMMENT ON COLUMN uc_user.open_id IS '微信openId';
COMMENT ON COLUMN uc_user.union_id IS '微信unionId';
COMMENT ON COLUMN uc_user.avatar_url IS '微信头像URL';
COMMENT ON COLUMN uc_user.nick_name IS '微信昵称';
COMMENT ON COLUMN uc_user.user_status IS '用户状态(ENABLE:启用,DISABLE:禁用)';
COMMENT ON COLUMN uc_user.user_type IS '用户类型(SYSTEM:系统用户,CUSTOM:客户)';
COMMENT ON COLUMN uc_user.level_code IS '会员等级(FREE:免费,VIP:会员,PREMIUM:高级会员)';
COMMENT ON COLUMN uc_user.extra_info IS '扩展信息(JSONB格式)';
COMMENT ON COLUMN uc_user.membership_expire_time IS '会员到期时间';
COMMENT ON COLUMN uc_user.parent_user_id IS '父账户ID(子账户功能)';
COMMENT ON COLUMN uc_user.wx_push_enabled IS '微信推送开关';
COMMENT ON COLUMN uc_user.create_by IS '创建人';
COMMENT ON COLUMN uc_user.create_time IS '创建时间';
COMMENT ON COLUMN uc_user.update_by IS '更新人';
COMMENT ON COLUMN uc_user.update_time IS '更新时间';
COMMENT ON COLUMN uc_user.deleted IS '逻辑删除(1:已删除,0:未删除)';
COMMENT ON COLUMN uc_user.version IS '乐观锁';
-- 初始化管理员账号(密码:000000)
INSERT INTO "uc_user" ("id", "login_name", "password", "user_name", "phone_number", "email",
"user_status", "user_type", "create_by", "create_time", "deleted")
VALUES ('1', 'admin', '$2a$10$2kZ9rJC1RjvH91PEOiuF8Oghfz2Y2GaoqsXfVuFNqKRxrjOT5TV5m',
'管理员', '', NULL, 'ENABLE', 'SYSTEM', '1', NOW(), 0);
-- 开放API客户端表
CREATE TABLE IF NOT EXISTS open_client
(
id VARCHAR(20) PRIMARY KEY,
app_name VARCHAR(100) NOT NULL,
app_id VARCHAR(100) NOT NULL,
app_secret VARCHAR(100) NOT NULL,
create_by VARCHAR(32),
create_time TIMESTAMP(6),
update_by VARCHAR(32),
update_time TIMESTAMP(6),
deleted INT4,
version INT8 DEFAULT 0
);
COMMENT ON TABLE open_client IS 'OpenAPI客户端表';
COMMENT ON COLUMN open_client.id IS '主键ID';
COMMENT ON COLUMN open_client.app_name IS '应用名称';
COMMENT ON COLUMN open_client.app_id IS '应用ID';
COMMENT ON COLUMN open_client.app_secret IS '应用密钥';
-- 会员等级配置表
CREATE TABLE IF NOT EXISTS membership_level
(
id VARCHAR(20) PRIMARY KEY,
level_code VARCHAR(20) NOT NULL,
level_name VARCHAR(50) NOT NULL,
level_sub_name VARCHAR(50),
extra_info jsonb,
sort_order INT4 DEFAULT 99,
status VARCHAR(20) NOT NULL DEFAULT 'ENABLE',
recommended boolean default false,
description VARCHAR(200),
create_by VARCHAR(32),
create_time TIMESTAMP(6),
update_by VARCHAR(32),
update_time TIMESTAMP(6),
deleted INT4,
version INT8 DEFAULT 0
);
COMMENT ON TABLE membership_level IS '会员等级配置表';
COMMENT ON COLUMN membership_level.level_code IS '等级代码:FREE, VIP1, VIP2, VIP3';
COMMENT ON COLUMN membership_level.level_name IS '等级名称';
COMMENT ON COLUMN membership_level.level_sub_name IS '副名称';
COMMENT ON COLUMN membership_level.extra_info IS '扩展信息(包含:monitorNum监听数量、cycle周期、cycleType周期类型、price价格等)';
COMMENT ON COLUMN membership_level.sort_order IS '排序(小的在前面)';
COMMENT ON COLUMN membership_level.status IS '状态(ENABLE-启用,DISABLE-禁用)';
COMMENT ON COLUMN membership_level.recommended IS '是否推荐';
-- 初始化会员等级数据
INSERT INTO "membership_level" ("id", "level_code", "level_name", "level_sub_name", "extra_info",
"sort_order", "status", "create_by", "create_time", "deleted")
VALUES
('1', 'FREE', '普通会员', '免费体验',
'{"cycle":-1,"cycleType":"NONE","price":"0","monitorNum":10,"export":false,"wxSend":false,"mailSend":false}',
1, 'ENABLE', '1', NOW(), 0),
('2', 'VIP1', 'VIP', '基础套餐',
'{"cycle":1,"cycleType":"MONTH","price":99,"monitorNum":800,"export":true,"wxSend":true,"mailSend":false}',
2, 'ENABLE', '1', NOW(), 0),
('3', 'VIP2', '高级VIP', '专业套餐',
'{"cycle":1,"cycleType":"MONTH","price":"199","monitorNum":2000,"export":true,"wxSend":true,"mailSend":true}',
3, 'ENABLE', '1', NOW(), 0),
('4', 'VIP3', '至尊VIP', '企业套餐',
'{"cycle":1,"cycleType":"MONTH","price":"399","monitorNum":5000,"export":true,"wxSend":true,"mailSend":true}',
4, 'ENABLE', '1', NOW(), 0);
-- 系统消息表
CREATE TABLE IF NOT EXISTS sys_message
(
id VARCHAR(20) PRIMARY KEY,
message_type varchar(50) NOT NULL DEFAULT 'SYS',
message_title varchar(50) NOT NULL,
description text,
send_user_id VARCHAR(20) NOT NULL,
resource_id VARCHAR(20),
read_status VARCHAR(10) NOT NULL DEFAULT 'UNREAD',
create_by VARCHAR(32),
create_time TIMESTAMP(6),
update_by VARCHAR(32),
update_time TIMESTAMP(6),
deleted INT4,
version INT8 DEFAULT 0
);
COMMENT ON TABLE sys_message IS '系统消息表';
COMMENT ON COLUMN sys_message.message_type IS '消息类型(SYS:系统;ZL_LEGAL_STATUS:专利法律状态变更;ZL_COST:专利费用)';
COMMENT ON COLUMN sys_message.message_title IS '消息标题';
COMMENT ON COLUMN sys_message.send_user_id IS '接收用户ID';
COMMENT ON COLUMN sys_message.resource_id IS '关联资源ID';
COMMENT ON COLUMN sys_message.read_status IS '阅读状态(UNREAD:未读;READ:已读)';
六、认证授权机制
这个框架使用 Sa-Token 作为认证授权框架,结合 JWT 实现无状态认证,并使用 Redis 存储会话信息。
6.1 Sa-Token 配置
在 application-prod.yml 中配置 Sa-Token:
sa-token:
# 是否允许同一账号并发登录(false=单点登录)
is-concurrent: false
# Token 名称
token-name: ipms-token
# Token 有效期(秒),8小时
timeout: 28800
# JWT 密钥
jwt-secret-key: your-secret-key-here6.2 密码加密机制
密码采用双重加密:
前端使用对称加密(AES)传输密码
后端使用 BCrypt 加盐加密存储
// 前端传来的密码解密
String decryptPw = PasswordUtil.decryptPw(loginDTO.getPassword());
// BCrypt 加密存储
String encryptedPw = BCrypt.hashpw(decryptPw, BCrypt.gensalt());
// BCrypt 验证密码
if (!BCrypt.checkpw(decryptPw, userEntity.getPassword())) {
throw new ServiceException("密码错误");
}6.3 登录流程
// 1. 验证用户名密码
UserEntity user = userService.getByLoginName(loginName);
if (!BCrypt.checkpw(password, user.getPassword())) {
throw new ServiceException("密码错误");
}
// 2. Sa-Token 登录
StpUtil.login(user.getId());
// 3. 获取 Token 信息返回给前端
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();6.4 权限校验
// 在拦截器中校验登录状态
StpUtil.checkLogin();
// 获取当前登录用户 ID
String userId = StpUtil.getLoginIdAsString();
// 将用户 ID 存入线程变量,方便后续使用
ContextHandler.setUserId(userId);七、前后端交互规范
7.1 请求头规范
ipms-token: {token} # 认证 Token
Content-Type: application/json7.2 响应格式规范
{
"success": true,
"code": 200,
"msg": "操作成功",
"data": { ... }
}7.3 常用响应码
八、快速搭建指南
如果你想从零搭建这个框架,按照以下步骤来:
8.1 环境准备
JDK 17+
Maven 3.8+
PostgreSQL 14+
Redis 6+
Node.js 16+(前端)
8.2 后端搭建步骤
Step 1:创建 Maven 多模块项目
# 创建父项目
mvn archetype:generate -DgroupId=com.yourcompany -DartifactId=your-project -DarchetypeArtifactId=maven-archetype-quickstart
# 修改 pom.xml,设置 packaging 为 pom
# 添加子模块Step 2:创建子模块
按照以下顺序创建模块:
your-common(公共模块)
your-spring(Spring 配置模块)
your-external(外部服务模块)
your-service(业务逻辑模块)
your-api(API 控制器模块)
your-open(开放 API 模块)
your-standalone(启动模块)
Step 3:配置依赖关系
在各模块的 pom.xml 中配置依赖,遵循单向依赖原则。
Step 4:复制核心配置类
从本文档中复制以下核心配置类:
ThreadPoolConfiguration
SchedulingConfig
MybatisPlusConfiguration
RedisTemplateConfiguration
IpmsWebMvcConfigurer
RestExceptionTranslator
Step 5:创建数据库表
执行本文档中的 SQL 脚本创建数据库表。
Step 6:配置 application.yml
根据你的环境配置数据库、Redis、邮件等信息。
8.3 前端搭建步骤
Step 1:使用 Vue CLI 创建项目
vue create your-uiStep 2:安装依赖
npm install element-ui axios vuex vue-router js-cookie --saveStep 3:配置路由和状态管理
参考本文档中的路由配置和 Vuex 配置。
Step 4:封装 Axios 请求
复制本文档中的 request.js 文件。
8.4 启动项目
# 后端
cd your-standalone
mvn spring-boot:run
# 前端
cd your-ui
npm run dev九、框架特色功能
9.1 动态表名(分表支持)
MyBatis-Plus 配置了动态表名插件,可以根据时间自动切换表名,实现按月分表:
DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor =
new DynamicTableNameInnerInterceptor((sql, tableName) -> {
if (IpmsConstant.TABLE_NAMES.contains(tableName)) {
// 自动添加年月后缀,如:zl_query_record_202501
return tableName + "_" + LocalDateTimeUtil.format(LocalDateTimeUtil.now(), "yyyyMM");
}
return tableName;
});9.2 乐观锁
所有实体类都包含 version 字段,MyBatis-Plus 自动处理乐观锁:
// 实体类
@Version
private Long version;
// 更新时自动检查版本号
userService.updateById(user); // 自动添加 WHERE version = ?9.3 逻辑删除
所有删除操作都是逻辑删除,不会真正删除数据:
// 实体类
@TableLogic(value = "0", delval = "1")
private Integer deleted;
// 删除时自动变成 UPDATE ... SET deleted = 1
userService.removeById(id);
// 查询时自动过滤已删除数据
userService.list(); // 自动添加 WHERE deleted = 09.4 自动填充
创建时间、更新时间、创建人、更新人自动填充:
public class IpmsMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTimeUtil.now());
this.strictInsertFill(metaObject, "createBy", String.class, ContextHandler.getUserId());
this.strictInsertFill(metaObject, "deleted", Integer.class, 0);
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTimeUtil.now());
this.strictUpdateFill(metaObject, "updateBy", String.class, ContextHandler.getUserId());
}
}9.5 JSONB 类型支持
PostgreSQL 的 JSONB 类型可以直接映射到 Java 对象:
// 实体类
@TableField("extra_info")
private JSONObject extraInfo;
// 使用
user.setExtraInfo(JSONUtil.parseObj("{\"key\": \"value\"}"));9.6 限流器
基于 Redis 实现的接口限流:
@RateLimiter(value = "api:sms:send", max = 5, ttl = 60, timeUnit = TimeUnit.SECONDS)
@PostMapping("/send")
public ResponseData sendSms(@RequestParam String phone) {
// 每分钟最多调用 5 次
}9.7 事件驱动
内置的事件机制,用于解耦业务逻辑:
// 触发事件
EventManager.getInstance().triggerEvent(MonitorEvents.SEND_MSG_APPLY_INFO, params);
// 监听事件
@Component
public class MyListener implements EventListener {
@Override
public MonitorEvents[] listenEvents() {
return new MonitorEvents[]{MonitorEvents.SEND_MSG_APPLY_INFO};
}
@Override
public void onEvent(MonitorEvents event, Object params) {
// 处理事件
}
}十、最佳实践建议
10.1 代码规范
Controller 层:只做参数校验和调用 Service,不写业务逻辑
Service 层:核心业务逻辑,事务控制
Mapper 层:数据库操作,复杂 SQL 写在 XML 中
10.2 异常处理
业务异常使用
ServiceException会员权限异常使用
MembershipException所有异常都会被全局异常处理器捕获
10.3 日志规范
使用
@Slf4j注解关键操作记录 INFO 日志
异常记录 ERROR 日志
调试信息使用 DEBUG 日志
10.4 安全建议
密码使用 BCrypt 加密
敏感配置使用环境变量
开放 API 使用签名验证
接口限流防止恶意请求
十一、总结
这个框架是一个功能完整、架构清晰的企业级 Java 项目模板。它采用了当前主流的技术栈,遵循了良好的设计原则,具有以下特点:
模块化设计:各模块职责清晰,易于维护和扩展
配置集中管理:Spring 配置独立成模块,方便复用
外部服务解耦:短信、邮件、微信等服务独立封装
安全机制完善:Sa-Token + JWT + BCrypt 多重保障
数据库设计规范:统一的表结构设计,支持逻辑删除和乐观锁
前后端分离:Vue + Element UI 的现代化前端
拿着这份文档,你完全可以从零搭建出一个相同的框架。如果有任何问题,欢迎交流讨论!
文档版本:v1.0 最后更新:2026年1月
十二、如何修改框架名称和包名
如果你想把这个框架改成自己的项目名称和包名,比如把 com.mank.ipms 改成 com.yourcompany.yourproject,需要修改以下几个地方。别担心,跟着步骤来,十分钟就能搞定。
12.1 需要修改的内容清单
12.2 后端修改步骤
Step 1:修改父 pom.xml
<!-- 修改前 -->
<groupId>com.mank.ipms</groupId>
<artifactId>ip-monitor</artifactId>
<version>2025.1.0</version>
<!-- 修改后 -->
<groupId>com.yourcompany.yourproject</groupId>
<artifactId>your-project</artifactId>
<version>1.0.0</version>同时修改 <properties> 中的版本变量:
<!-- 修改前 -->
<ipms.version>2025.1.0</ipms.version>
<!-- 修改后 -->
<yourproject.version>1.0.0</yourproject.version>修改 <modules> 中的模块名:
<!-- 修改前 -->
<modules>
<module>monitor-api</module>
<module>monitor-common</module>
...
</modules>
<!-- 修改后 -->
<modules>
<module>yourproject-api</module>
<module>yourproject-common</module>
...
</modules>修改 <dependencyManagement> 中的内部模块依赖:
<!-- 修改前 -->
<dependency>
<groupId>com.mank.ipms</groupId>
<artifactId>monitor-api</artifactId>
<version>${ipms.version}</version>
</dependency>
<!-- 修改后 -->
<dependency>
<groupId>com.yourcompany.yourproject</groupId>
<artifactId>yourproject-api</artifactId>
<version>${yourproject.version}</version>
</dependency>Step 2:重命名模块目录
# 在项目根目录执行
mv monitor-api yourproject-api
mv monitor-common yourproject-common
mv monitor-service yourproject-service
mv monitor-spring yourproject-spring
mv monitor-external yourproject-external
mv monitor-open yourproject-open
mv monitor-standalone yourproject-standalone
mv monitor-ui yourproject-uiStep 3:修改各子模块的 pom.xml
每个子模块的 pom.xml 都需要修改 parent 和 artifactId:
<!-- 修改前 -->
<parent>
<groupId>com.mank.ipms</groupId>
<artifactId>ip-monitor</artifactId>
<version>2025.1.0</version>
</parent>
<artifactId>monitor-api</artifactId>
<!-- 修改后 -->
<parent>
<groupId>com.yourcompany.yourproject</groupId>
<artifactId>your-project</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>yourproject-api</artifactId>Step 4:重命名 Java 包目录
这是最关键的一步,需要把所有 Java 文件的包路径改掉:
# 以 yourproject-common 模块为例
cd yourproject-common/src/main/java
# 创建新的包目录
mkdir -p com/yourcompany/yourproject
# 移动文件(把 common 目录移到新位置)
mv com/mank/ipms/common com/yourcompany/yourproject/
# 删除旧的空目录
rm -rf com/mank对每个模块都执行类似操作。
Step 5:批量替换包名
使用 IDE 的全局替换功能(推荐),或者用命令行:
# macOS/Linux
find . -name "*.java" -exec sed -i '' 's/com\.mank\.ipms/com.yourcompany.yourproject/g' {} +
find . -name "*.xml" -exec sed -i '' 's/com\.mank\.ipms/com.yourcompany.yourproject/g' {} +
find . -name "*.yml" -exec sed -i '' 's/com\.mank\.ipms/com.yourcompany.yourproject/g' {} +Step 6:修改类名前缀(可选)
如果你想把类名前缀也改掉,比如 IpmsApplication 改成 YourProjectApplication:
# 重命名文件
mv IpmsApplication.java YourProjectApplication.java
mv IpmsConstant.java YourProjectConstant.java
mv IpmsRedis.java YourProjectRedis.java
mv IpmsWebMvcConfigurer.java YourProjectWebMvcConfigurer.java
mv IpmsResultCode.java YourProjectResultCode.java
# ... 其他以 Ipms 开头的类
# 然后全局替换类名引用
find . -name "*.java" -exec sed -i '' 's/Ipms/YourProject/g' {} +Step 7:修改配置文件中的引用
检查并修改以下配置文件:
application.yml/application-prod.ymllogback.xmlMyBatis Mapper XML 文件中的 namespace
<!-- Mapper XML 修改前 -->
<mapper namespace="com.mank.ipms.common.mapper.UserMapper">
<!-- Mapper XML 修改后 -->
<mapper namespace="com.yourcompany.yourproject.common.mapper.UserMapper">Step 8:修改 MyBatis-Plus 配置中的扫描路径
// 修改前
@MapperScan("com.mank.ipms.**.mapper.**")
// 修改后
@MapperScan("com.yourcompany.yourproject.**.mapper.**")12.3 前端修改步骤
Step 1:修改 package.json
{
"name": "your-project-ui",
"version": "1.0.0",
"description": "Your Project Management System"
}Step 2:修改 vue.config.js 中的项目名
configureWebpack: {
name: '你的项目名称',
// ...
}Step 3:修改页面标题和 Logo
修改
public/index.html中的<title>替换
src/assets/中的 Logo 图片修改
src/settings.js中的系统名称
12.4 数据库修改
如果你想修改数据库名和表前缀:
-- 创建新数据库
CREATE DATABASE your_project_db;
-- 如果要修改表前缀,比如把 uc_user 改成 yp_user
-- 需要同时修改:
-- 1. SQL 建表语句中的表名
-- 2. Java 实体类的 @TableName 注解
-- 3. Mapper XML 中的表名引用12.5 使用 IDE 批量重构(推荐)
如果你用的是 IntelliJ IDEA,可以用它的重构功能,会更安全:
重命名包:右键包名 → Refactor → Rename,IDEA 会自动更新所有引用
重命名类:右键类名 → Refactor → Rename
全局替换:Ctrl+Shift+R(Mac: Cmd+Shift+R),勾选 "Regex" 可以用正则表达式
12.6 修改后的验证清单
改完之后,按这个清单检查一遍:
所有 pom.xml 的 groupId、artifactId 已修改
所有 Java 文件的 package 声明已修改
所有 import 语句已更新
MyBatis Mapper XML 的 namespace 已修改
@MapperScan 扫描路径已修改
application.yml 中的配置已检查
logback.xml 中的 logger name 已修改
前端 package.json 已修改
项目能正常编译:
mvn clean compile项目能正常启动
前端能正常访问
12.7 常见问题
Q: 改完后编译报错找不到类?
A: 检查 import 语句是否都改过来了,IDE 的全局替换可能漏掉了一些。
Q: 启动报错 Mapper 扫描不到?
A: 检查 @MapperScan 注解的路径,以及 Mapper XML 文件的 namespace。
Q: 前端请求 404?
A: 检查 vue.config.js 中的代理配置,以及后端 Controller 的 RequestMapping 路径。
好了,按照这个步骤来,你就能把框架完全改成自己的项目了。虽然步骤看起来多,但其实就是"改名字"这一件事,耐心点就行。祝你顺利!🎉
评论区