侧边栏壁纸
  • 累计撰写 8 篇文章
  • 累计创建 0 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

从零搭建企业级 Spring Boot 多模块项目框架

大锤灬
2026-01-13 / 0 评论 / 0 点赞 / 7 阅读 / 0 字

嘿,朋友!如果你正在寻找一个成熟的企业级 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 router

Axios 请求封装(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 service

Vue 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-here

6.2 密码加密机制

密码采用双重加密:

  1. 前端使用对称加密(AES)传输密码

  2. 后端使用 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/json

7.2 响应格式规范

{
  "success": true,
  "code": 200,
  "msg": "操作成功",
  "data": { ... }
}

7.3 常用响应码

响应码

说明

200

成功

400

请求参数错误

401

未登录或 Token 过期

40001

会员权益不足

500

服务器内部错误

八、快速搭建指南

如果你想从零搭建这个框架,按照以下步骤来:

8.1 环境准备

  1. JDK 17+

  2. Maven 3.8+

  3. PostgreSQL 14+

  4. Redis 6+

  5. 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:创建子模块

按照以下顺序创建模块:

  1. your-common(公共模块)

  2. your-spring(Spring 配置模块)

  3. your-external(外部服务模块)

  4. your-service(业务逻辑模块)

  5. your-api(API 控制器模块)

  6. your-open(开放 API 模块)

  7. 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-ui

Step 2:安装依赖

npm install element-ui axios vuex vue-router js-cookie --save

Step 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 = 0

9.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 代码规范

  1. Controller 层:只做参数校验和调用 Service,不写业务逻辑

  2. Service 层:核心业务逻辑,事务控制

  3. Mapper 层:数据库操作,复杂 SQL 写在 XML 中

10.2 异常处理

  1. 业务异常使用 ServiceException

  2. 会员权限异常使用 MembershipException

  3. 所有异常都会被全局异常处理器捕获

10.3 日志规范

  1. 使用 @Slf4j 注解

  2. 关键操作记录 INFO 日志

  3. 异常记录 ERROR 日志

  4. 调试信息使用 DEBUG 日志

10.4 安全建议

  1. 密码使用 BCrypt 加密

  2. 敏感配置使用环境变量

  3. 开放 API 使用签名验证

  4. 接口限流防止恶意请求

十一、总结

这个框架是一个功能完整、架构清晰的企业级 Java 项目模板。它采用了当前主流的技术栈,遵循了良好的设计原则,具有以下特点:

  1. 模块化设计:各模块职责清晰,易于维护和扩展

  2. 配置集中管理:Spring 配置独立成模块,方便复用

  3. 外部服务解耦:短信、邮件、微信等服务独立封装

  4. 安全机制完善:Sa-Token + JWT + BCrypt 多重保障

  5. 数据库设计规范:统一的表结构设计,支持逻辑删除和乐观锁

  6. 前后端分离:Vue + Element UI 的现代化前端

拿着这份文档,你完全可以从零搭建出一个相同的框架。如果有任何问题,欢迎交流讨论!


文档版本:v1.0 最后更新:2026年1月

十二、如何修改框架名称和包名

如果你想把这个框架改成自己的项目名称和包名,比如把 com.mank.ipms 改成 com.yourcompany.yourproject,需要修改以下几个地方。别担心,跟着步骤来,十分钟就能搞定。

12.1 需要修改的内容清单

修改项

原值

示例新值

groupId

com.mank.ipms

com.yourcompany.yourproject

artifactId

ip-monitor

your-project

模块名前缀

monitor-

yourproject-

包名

com.mank.ipms

com.yourcompany.yourproject

类名前缀

Ipms

YourProject

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-ui

Step 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:修改配置文件中的引用

检查并修改以下配置文件:

  1. application.yml / application-prod.yml

  2. logback.xml

  3. MyBatis 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,可以用它的重构功能,会更安全:

  1. 重命名包:右键包名 → Refactor → Rename,IDEA 会自动更新所有引用

  2. 重命名类:右键类名 → Refactor → Rename

  3. 全局替换: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 路径。


好了,按照这个步骤来,你就能把框架完全改成自己的项目了。虽然步骤看起来多,但其实就是"改名字"这一件事,耐心点就行。祝你顺利!🎉

0

评论区