260603-后端框架全局开发规范与最佳实践.md 51 KB

JeecgBoot 后端框架全局开发规范与最佳实践

分析日期:2026-06-03 框架版本:JeecgBoot 3.9.2 分析范围:JeecgBoot 原框架(排除 jeecg-module-zjrs 业务子模块)


一、框架概述

1.1 项目结构

jeecg-boot/
├── jeecg-boot-base-core/          # 框架核心基础库
│   └── src/main/java/org/jeecg/
│       ├── common/                # 通用工具类、切面、异常、常量、VO
│       ├── config/                # 全局配置(Shiro/MyBatis/WebMVC/Swagger等)
│       └── modules/base/          # 基础公共服务(日志、消息)
├── jeecg-boot-module/
│   ├── jeecg-module-system/       # 系统管理模块(用户/角色/部门/字典/消息/定时任务等)
│   ├── jeecg-module-demo/         # 示例模块(单表CRUD/主子表/动态数据源/MCP等)
│   └── jeecg-module-zjrs/         # 湛江人社业务模块(本次分析排除)
└── jeecg-server-cloud/            # 微服务Cloud版本相关

1.2 技术栈

层级 技术
基础框架 Spring Boot 2.x + Spring MVC
ORM MyBatis-Plus 3.x
安全认证 Apache Shiro + JWT
缓存 Redis(Spring Cache + RedisTemplate)
数据库连接池 Druid
Excel AutoPoi(基于 Apache POI 封装)
模板引擎 Freemarker
定时任务 Quartz
API文档 Swagger3 / OpenAPI 3.0
对象存储 MinIO / 阿里云 OSS / 本地存储
消息推送 WebSocket + 邮件 + 短信 + 微信 + 钉钉 + 企业微信

二、框架分层架构与开发规约

2.1 标准分层

Controller 层      →  接收请求、参数校验、调用Service、返回Result
   │                   继承 JeecgController<T, S>
   ▼
Service 层         →  业务逻辑、事务管理、调用Mapper
   │                   接口 extends IService<T>,实现 extends ServiceImpl<M, T>
   ▼
Mapper 层          →  数据访问、自定义SQL
   │                   继承 BaseMapper<T>,XML 可选
   ▼
Entity 层          →  数据实体、数据库表映射
                       继承 JeecgEntity(推荐),加 @TableName、@Excel 等

2.2 各层最优写法

Entity 层(实体类)

@Data
@TableName("table_name")                     // 数据库表名
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)                     // 支持链式调用
public class XxxEntity extends JeecgEntity { // 继承基类,自动获得 id/createBy/createTime/updateBy/updateTime

    @Excel(name = "字段名", width = 15)       // 需要导出/导入的字段加 @Excel
    private String fieldName;

    @Excel(name = "字典字段", width = 15, dicCode = "dict_code")  // 字典翻译
    private String dictField;

    @Excel(name = "日期字段", width = 20, format = "yyyy-MM-dd HH:mm:ss")  // 日期格式
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date dateField;

    // 不需要导出/导入的字段不加 @Excel 注解
    private String internalField;
}

JeecgEntity 基类提供的能力

  • 自动填充公共字段(idcreateBycreateTimeupdateByupdateTime
  • 通过 MybatisInterceptor 拦截器自动注入 createBy/createTime/sysOrgCode(插入时)和 updateBy/updateTime(更新时)

Mapper 层

// 标准 Mapper(无自定义 SQL 场景)
public interface XxxMapper extends BaseMapper<XxxEntity> {
    // MyBatis-Plus 内置方法即可满足常规 CRUD
}
// 带自定义 SQL 的 Mapper(需要配合 XML)
public interface XxxMapper extends BaseMapper<XxxEntity> {

    @Select("SELECT * FROM table WHERE field = #{value}")  // 简单 SQL 可用注解
    List<XxxEntity> selectByField(String value);

    List<XxxEntity> selectByCondition(@Param("param") XxxEntity param);  // 复杂 SQL 用 XML
}

Service 层

// 接口
public interface IXxxService extends IService<XxxEntity> {
    // 自定义业务方法
    void customBusinessMethod(XxxEntity entity);
}

// 实现
@Service
public class XxxServiceImpl extends ServiceImpl<XxxMapper, XxxEntity>
        implements IXxxService {

    @Autowired
    private XxxMapper xxxMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)  // 涉及多表操作必须加事务
    public void customBusinessMethod(XxxEntity entity) {
        // 业务逻辑
    }
}

Controller 层

@Slf4j
@RestController
@RequestMapping("/模块/实体")
public class XxxController extends JeecgController<XxxEntity, IXxxService> {

    // ========== 分页列表查询 ==========
    @GetMapping(value = "/list")
    @PermissionData(pageComponent = "system/模块/index")  // 数据权限
    public Result<?> list(XxxEntity entity,
                          @RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
                          @RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
                          HttpServletRequest req) {
        QueryWrapper<XxxEntity> queryWrapper = QueryGenerator.initQueryWrapper(entity, req.getParameterMap());
        Page<XxxEntity> page = new Page<>(pageNo, pageSize);
        IPage<XxxEntity> pageList = service.page(page, queryWrapper);
        return Result.OK(pageList);
    }

    // ========== 新增 ==========
    @PostMapping(value = "/add")
    @AutoLog(value = "新增XXX")
    public Result<?> add(@RequestBody XxxEntity entity) {
        service.save(entity);
        return Result.OK("添加成功!");
    }

    // ========== 修改 ==========
    @PutMapping(value = "/edit")
    @AutoLog(value = "编辑XXX", operateType = CommonConstant.OPERATE_TYPE_3)
    public Result<?> edit(@RequestBody XxxEntity entity) {
        service.updateById(entity);
        return Result.OK("更新成功!");
    }

    // ========== 删除 ==========
    @DeleteMapping(value = "/delete")
    @AutoLog(value = "删除XXX")
    public Result<?> delete(@RequestParam(name="id", required=true) String id) {
        service.removeById(id);
        return Result.OK("删除成功!");
    }

    // ========== 批量删除 ==========
    @DeleteMapping(value = "/deleteBatch")
    public Result<?> deleteBatch(@RequestParam(name="ids", required=true) String ids) {
        service.removeByIds(Arrays.asList(ids.split(",")));
        return Result.OK("批量删除成功!");
    }

    // ========== 通过ID查询 ==========
    @GetMapping(value = "/queryById")
    public Result<?> queryById(@RequestParam(name="id", required=true) String id) {
        XxxEntity obj = service.getById(id);
        return Result.OK(obj);
    }

    // ========== Excel导出 ==========
    @RequestMapping(value = "/exportXls")
    public ModelAndView exportXls(HttpServletRequest request, XxxEntity entity) {
        return super.exportXls(request, entity, XxxEntity.class, "文件名");
    }

    // ========== Excel导入 ==========
    @RequestMapping(value = "/importExcel", method = RequestMethod.POST)
    public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
        return super.importExcel(request, response, XxxEntity.class);
    }
}

三、QueryGenerator —— 查询条件自动生成引擎

3.1 核心用法

QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(entity, request.getParameterMap());

这是整个 CRUD 体系的核心引擎,实现了前端零配置即可完成复杂查询条件自动构建

3.2 自动推断查询规则

QueryGenerator 通过分析请求参数的值特征,自动推断查询规则:

值特征 推断规则 示例
>= 值 大于等于(GE) >= 100
<= 值 小于等于(LE) <= 100
> 值 大于(GT) > 100
< 值 小于(LT) < 100
*值* 全模糊 LIKE *张*%张%
*值 左模糊 LIKE *张%张
值* 右模糊 LIKE 张*张%
值1,值2 IN 查询 1,2,3IN (1,2,3)
!值 不等于(NE) !admin
普通值 等于(EQ) admin

3.3 区间查询

通过 字段_begin字段_end 参数实现:

?createTime_begin=2026-01-01&createTime_end=2026-12-31

3.4 高级查询

通过 superQueryParams 参数传入 JSON 格式的复杂查询条件,支持 AND/OR 组合。

3.5 排序支持

优先级从高到低:

  1. sortInfoString — 多字段排序(最高优先级)
  2. defSortString — 默认排序
  3. column + order — 单字段排序

3.6 数据权限注入

QueryGenerator 在构建查询条件时,会自动从 Request 的 Attribute 中读取由 PermissionDataAspect 注入的数据权限规则,拼接到 SQL 条件中,实现行级数据过滤。


四、Excel 导入导出体系

4.1 @Excel 注解速查

属性 说明 示例
name 列标题(必填) @Excel(name = "姓名")
width 列宽 @Excel(width = 15)
format 日期格式 @Excel(format = "yyyy-MM-dd HH:mm:ss")
dicCode 字典编码(自动翻译) @Excel(dicCode = "sex")
dictTable 字典表名(表字典) @Excel(dictTable = "sys_depart")
dicText 字典文本字段 @Excel(dicText = "depart_name")
dicCode 字典编码字段 @Excel(dicCode = "id")
type 字段类型 1=文本(默认), 2=图片, 4=数值

4.2 导出(最优写法)

单表导出(直接调用父类方法):

@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, XxxEntity entity) {
    return super.exportXls(request, entity, XxxEntity.class, "文件名");
}

父类 JeecgController.exportXls 执行流程

Step 1: QueryGenerator.initQueryWrapper() 组装查询条件
Step 2: selections 参数过滤选中数据
Step 3: service.list(queryWrapper) 获取导出数据
Step 4: 构建 ModelAndView(JeecgEntityExcelView)
   ├── FILE_NAME → 文件名
   ├── CLASS → 实体类(含 @Excel 注解定义列结构)
   ├── PARAMS → ExportParams(标题、Sheet名、格式)
   ├── DATA_LIST → 导出数据列表
   └── EXPORT_FIELDS → 可选,指定导出列

导出变体

  • 分 Sheet 导出:super.exportXlsSheet(request, entity, Class, "文件名", exportFields, sheetSize)
  • 大数据量导出:super.exportXlsForBigData(request, entity, Class, "文件名", pageSize)

主子表导出(手动实现):

@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, XxxEntity entity) {
    QueryWrapper<XxxEntity> qw = QueryGenerator.initQueryWrapper(entity, request.getParameterMap());
    List<XxxVo> pageList = new ArrayList<>();
    List<XxxEntity> mainList = service.list(qw);
    for (XxxEntity main : mainList) {
        XxxVo vo = new XxxVo();
        BeanUtils.copyProperties(main, vo);
        vo.setSubList(subService.selectByMainId(main.getId()));  // 查询子表
        pageList.add(vo);
    }
    ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
    mv.addObject(NormalExcelConstants.FILE_NAME, "文件名");
    mv.addObject(NormalExcelConstants.CLASS, XxxVo.class);       // 使用 VO(含 @ExcelCollection)
    mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("标题", "导出人:xxx", "Sheet名"));
    mv.addObject(NormalExcelConstants.DATA_LIST, pageList);
    return mv;
}

4.3 导入(最优写法)

单表导入(直接调用父类方法):

@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
    return super.importExcel(request, response, XxxEntity.class);
}

带校验和错误汇总的导入

@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
    MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
    Map<String, MultipartFile> fileMap = multipartRequest.getFileMap();
    for (Map.Entry<String, MultipartFile> entity : fileMap.entrySet()) {
        MultipartFile file = entity.getValue();
        ImportParams params = new ImportParams();
        params.setTitleRows(2);
        params.setHeadRows(1);
        params.setNeedSave(true);
        try {
            List<XxxEntity> list = ExcelImportUtil.importExcel(file.getInputStream(), XxxEntity.class, params);
            List<String> errorMessage = new ArrayList<>();
            int successLines = 0, errorLines = 0;
            for (int i = 0; i < list.size(); i++) {
                try {
                    service.save(list.get(i));
                    successLines++;
                } catch (Exception e) {
                    errorLines++;
                    errorMessage.add("第 " + (i + 1) + " 行:" + e.getMessage());
                }
            }
            return ImportExcelUtil.imporReturnRes(errorLines, successLines, errorMessage);
        } catch (Exception e) {
            return Result.error("文件导入失败:" + e.getMessage());
        }
    }
    return Result.error("文件导入失败!");
}

4.4 下载导入模板 = 复用导出接口

JeecgBoot 没有独立的"下载导入模板"接口。当前端请求 exportXls 接口但不带查询条件时,查询结果为空列表,AutoPoi 仍生成包含标题行和表头行的 Excel 文件——这就是"导入模板"。

┌──────────────────────────────────────────────────┐
│ 第1行: 大标题(ExportParams.title)                │  ← titleRows=2
├──────────────────────────────────────────────────┤
│ 第2行: 副标题(ExportParams.secondTitle)          │
├──────┬──────┬──────┬──────┬──────────────────────┤
│ 列1  │ 列2  │ 列3  │ 列4  │ ...                  │  ← headRows=1
├──────┼──────┼──────┼──────┼──────────────────────┤
│ (空) │ (空) │ (空) │ (空) │ ...                  │  ← 数据行为空
└──────┴──────┴──────┴──────┴──────────────────────┘

五、主子表(一对多)CRUD 模式

5.1 架构

  • 主表实体子表实体 独立定义,子表通过 外键字段 关联主表
  • VO/Page类 将主表字段与子表列表组合,用于前后端数据传输(含 @ExcelCollection 注解)
  • Service层 负责事务管理,统一处理主表和子表的增删改
  • Mapper层 提供通过外键查询/删除子表数据的专用方法

5.2 子表实体(外键是关键)

@Data
@TableName("sub_table")
public class SubEntity implements Serializable {
    @TableId(type = IdType.ASSIGN_ID)
    private String id;
    private String name;
    /** 外键 — 关联主表ID */
    private String mainId;  // 命名规范: {主表实体名小写}Id
}

5.3 VO 类(聚合主子表 + @ExcelCollection

@Data
public class MainEntityPage {
    private String id;
    @Excel(name = "主表字段", width = 15)
    private String field;
    
    @ExcelCollection(name = "子表")               // AutoPoi 注解,Excel 导入导出时识别
    private List<SubEntity> subEntityList;
}

5.4 子表 Mapper(增加外键查询/删除方法)

public interface SubEntityMapper extends BaseMapper<SubEntity> {
    /** 通过主表外键批量删除 */
    @Delete("DELETE FROM sub_table WHERE main_id = #{mainId}")
    boolean deleteByMainId(String mainId);

    /** 通过主表外键查询 */
    @Select("SELECT * FROM sub_table WHERE main_id = #{mainId}")
    List<SubEntity> selectByMainId(String mainId);
}

5.5 Service 层(事务管理)

// 新增:先插主表,再循环插子表,设置外键
@Override
@Transactional(rollbackFor = Exception.class)
public void saveMain(MainEntity main, List<SubEntity> subList) {
    mainMapper.insert(main);
    if (subList != null) {
        for (SubEntity sub : subList) {
            sub.setMainId(main.getId());  // 设置外键
            subMapper.insert(sub);
        }
    }
}

// 修改策略一(先删后插)
@Override
@Transactional(rollbackFor = Exception.class)
public void updateMain(MainEntity main, List<SubEntity> subList) {
    mainMapper.updateById(main);
    subMapper.deleteByMainId(main.getId());     // 删除旧子表数据
    if (subList != null) {
        for (SubEntity sub : subList) {
            sub.setMainId(main.getId());
            subMapper.insert(sub);              // 重新插入
        }
    }
}

// 修改策略二(差异对比——推荐):前端传完整子表列表,后端对比新增/更新/删除
@Override
@Transactional(rollbackFor = Exception.class)
public void updateCopyMain(MainEntity main, List<SubEntity> subList) {
    mainMapper.updateById(main);
    for (SubEntity sub : subList) {
        SubEntity dbItem = subMapper.selectById(sub.getId());
        if (dbItem == null) {
            sub.setMainId(main.getId());
            subMapper.insert(sub);              // 数据库不存在 → 新增
        } else {
            subMapper.updateById(sub);          // 数据库存在 → 更新
        }
    }
    // 取差集:数据库有但前端没传的 → 删除
    List<SubEntity> dbList = subMapper.selectByMainId(main.getId());
    List<String> submittedIds = subList.stream().map(SubEntity::getId).collect(Collectors.toList());
    dbList.stream()
        .filter(item -> !submittedIds.contains(item.getId()))
        .forEach(item -> subMapper.deleteById(item.getId()));
}

// 删除:先删子表,再删主表
@Override
@Transactional(rollbackFor = Exception.class)
public void delMain(String id) {
    subMapper.deleteByMainId(id);
    mainMapper.deleteById(id);
}

5.6 Controller 层

// 新增(接收 VO,拆分为主表实体和子表列表)
@PostMapping(value = "/add")
public Result<?> add(@RequestBody MainEntityPage vo) {
    MainEntity main = new MainEntity();
    BeanUtils.copyProperties(vo, main);
    service.saveMain(main, vo.getSubEntityList());
    return Result.ok("添加成功!");
}

// 查询子表(每个子表一个独立接口)
@GetMapping(value = "/querySubListByMainId")
public Result<?> querySubListByMainId(@RequestParam(name="id", required=true) String id) {
    List<SubEntity> list = subService.selectByMainId(id);
    return Result.ok(list);
}

5.7 主子表 vs 单表差异对比

对比维度 单表 CRUD 主子表 CRUD
Controller入参 @RequestBody Entity @RequestBody EntityPage(VO对象)
新增方法 service.save(entity) service.saveMain(main, subList)
修改方法 service.updateById(entity) service.updateMain()service.updateCopyMain()
删除方法 service.removeById(id) service.delMain(id)
查询子表 每个子表一个独立查询接口
Service事务 框架默认 显式 @Transactional(rollbackFor = Exception.class)
导入 headRows 1 2(主子表表头占2行)

5.8 代码生成器模板风格

JeecgBoot 提供 6 种主子表生成模板风格(位于 jeecg-system-biz/src/main/resources/jeecg/code-template-online/):

风格 路径 说明
default default/onetomany/ 默认经典风格
tab tab/onetomany/ Tab页签风格
jvxe jvxe/onetomany/ JVxe行编辑风格
inner-table inner-table/onetomany/ 内嵌表格风格
erp erp/onetomany/ ERP风格

六、全局异常处理体系

6.1 异常类继承体系

RuntimeException
├── JeecgBootException (核心业务异常, errCode=500)
│   └── JeecgBootAssertException (断言异常, 由 AssertUtils 使用)
├── JeecgBootBizTipException (业务提醒异常, errCode=500, 不记系统日志)
├── JeecgBoot401Exception (认证/鉴权异常)
└── JeecgSqlInjectionException (SQL注入防护异常)

6.2 JeecgBootException vs JeecgBootBizTipException

维度 JeecgBootException JeecgBootBizTipException
语义 系统级错误、不可恢复的业务错误 业务操作提醒、可预期的用户提示
记录系统日志 是(写入 sys_log 表) 否(仅 log.error 打印)
适用场景 数据不存在、管理员保护、验证码失效 密码错误、权限不足提醒、操作冲突
典型示例 "admin用户,不允许删除!""token非法" "旧密码输入错误""您已是该租户成员"

6.3 异常抛出方式

方式一:直接抛出 JeecgBootException(最常用,用于系统级错误)

throw new JeecgBootException("操作数据不存在!");
throw new JeecgBootException("admin用户,不允许删除!");
throw new JeecgBootException("短信接口请求太多,请稍后再试!", CommonConstant.PHONE_SMS_FAIL_CODE);
throw new JeecgBootException("获取钉钉用户信息失败", cause);

方式二:抛出 JeecgBootBizTipException(业务提醒,不记系统日志)

throw new JeecgBootBizTipException("旧密码输入错误!");
throw new JeecgBootBizTipException("您已是该租户成员");

方式三:使用 AssertUtils 断言(声明式校验)

AssertUtils.assertNotEmpty("姓名不能为空", name);
AssertUtils.assertNotEmpty("用户ID不能为空", userId);
AssertUtils.assertTrue("操作数据不存在", data != null);
AssertUtils.assertEmpty("该记录已存在", existingRecord);
AssertUtils.assertEquals("验证码不匹配", expectedCode, actualCode);
AssertUtils.assertGt("数量必须大于0", quantity, 0);
AssertUtils.assertIn("非法状态", status, "ACTIVE", "PENDING");

方式四:直接返回 Result.error()(仅 Controller 层使用)

if (obj == null) {
    return Result.error("未找到对应数据");
}

6.4 全局异常处理器 (JeecgBootExceptionHandler)

使用 @RestControllerAdvice 统一拦截所有 Controller 层异常,转换为 Result<?> 返回:

异常类型 返回code 记录系统日志
JeecgBootException 自定义errCode(默认500)
JeecgBootBizTipException 自定义errCode(默认500)
JeecgBoot401Exception 401
JeecgSqlInjectionException 500 视情况
MethodArgumentNotValidException 500
DuplicateKeyException 500
DataIntegrityViolationException 500
UnauthorizedException 510
MaxUploadSizeExceededException 500
Exception(兜底) 500

6.5 异常选择原则

  • 如果异常需要被运维关注排查(数据不一致、关键操作失败)→ JeecgBootException
  • 如果异常只是给用户的友好提示(输入校验、操作冲突)→ JeecgBootBizTipException
  • 字段非空/格式校验AssertUtilsJeecgBootBizTipException
  • 不允许直接抛出 RuntimeException,必须使用框架异常类

七、数据校验体系

7.1 推荐方案:分层组合校验

最优实践是三种方式混合使用

  • 字段级校验(非空、长度、格式)→ JSR303 注解 + @Validated
  • 业务规则校验(唯一性、关联性、状态约束)→ AssertUtils 或直接抛异常
  • 安全校验 → 框架内置的 SqlInjectionUtil + SsrfFileTypeFilter

7.2 JSR303 声明式校验

// 实体类
@Data
@TableName("xxx")
public class XxxEntity extends JeecgEntity {
    @NotBlank(message = "姓名不能为空")
    @Size(max = 50, message = "姓名长度不能超过50")
    @Excel(name = "姓名", width = 15)
    private String name;

    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    @Excel(name = "手机号", width = 15)
    private String phone;
}

// Controller
@PostMapping(value = "/add")
public Result<?> add(@Validated @RequestBody XxxEntity entity) {
    service.save(entity);
    return Result.OK("添加成功!");
}

7.3 AssertUtils 业务规则校验

@Override
public void saveEntity(XxxEntity entity) {
    AssertUtils.assertNotEmpty("姓名不能为空", entity.getName());
    // 业务规则校验
    long count = this.count(new LambdaQueryWrapper<XxxEntity>()
            .eq(XxxEntity::getCode, entity.getCode()));
    AssertUtils.assertTrue("编码已存在", count == 0);
    this.save(entity);
}

7.4 SQL注入防护

框架内置了三层防护:

工具类 功能
SqlInjectionUtil SQL关键词/正则检测、表名/字段名/排序字段合法性校验
AbstractQueryBlackListHandler 查询表/字段黑名单(如 sys_user.password 禁止查询)
SsrfFileTypeFilter 上传文件类型白名单 + 文件头黑名单检测

八、全局工具类体系

所有工具类位于 jeecg-boot-base-core/src/main/java/org/jeecg/common/util/

8.1 常用工具类速查

工具类 功能 常用方法
oConvertUtils 通用转换工具 getString()getInt()isEmpty()randomGen()copyNonNullFields()listIsNotEmpty()
SpringContextUtils Spring上下文 getApplicationContext()getBean()getHttpServletRequest()
TokenUtils Token工具 getTokenByRequest()verifyToken()
PasswordUtil 密码加密 encrypt(username, password, salt)
SqlInjectionUtil SQL注入防护 filterContent()checkSqlInject()
QueryGenerator 查询条件生成 initQueryWrapper(entity, paramMap)
ImportExcelUtil 导入结果封装 imporReturnRes(errorLines, successLines, errorMessages)
IPUtils IP获取 getIpAddr()
DateUtils 日期处理 parseDate()formatDate()
FillRuleUtil 编码规则生成 自动生成订单号、部门编码等
YouBianCodeUtil 流水号生成 getNextYouBianCode()
UUIDGenerator UUID生成 generate()
RestUtil HTTP调用 get()post()put()delete()
CommonUtils 文件上传路由 upload(file, bizPath, uploadType)

8.2 加密/安全工具

工具类 位置 功能
AesEncryptUtil util/encryption/ AES加解密(CBC+PKCS5Padding)
SecurityTools util/security/ RSA/AES加解密、签名验证
Md5Util util/ MD5加密
SsrfFileTypeFilter util/filter/ 文件上传安全校验

8.3 存储工具

工具类 功能
MinioUtil MinIO 对象存储(上传/下载/删除/预签名URL)
OssBootUtil 阿里云 OSS(上传/下载/删除/预签名URL)
CommonUtils 上传路由(根据配置自动选择 local/minio/alioss)

8.4 动态数据源工具

工具类 功能
DynamicDBUtil 动态数据源下通过 Spring JDBC Template 直接操作数据库
DataSourceCachePool 动态数据源缓存池
FreemarkerParseFactory Freemarker模板解析工厂(用于代码生成和SQL模板渲染)

九、AOP 切面体系

所有切面位于 jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/

9.1 四大核心切面

切面类 拦截注解 功能
AutoLogAspect @AutoLog 自动记录操作日志到 sys_log 表(含方法名、参数、耗时、IP)
DictAspect @Dict / @AutoDict 在 Controller 返回前自动将字典code翻译为文本(如 sex: 1sex_dictText: "男"
PermissionDataAspect @PermissionData 查询数据权限规则并注入 Request Attribute,供 MyBatis 拦截器组装 SQL 条件
DynamicTableAspect @DynamicTable 运行时动态切换表名

9.2 @Dict 字典翻译注解

最佳实践:在实体类字段上标注 @Dict,Controller 方法上标注 @AutoDict,框架会自动在下发 JSON 时追加 _dictText 后缀的翻译字段。

// 实体类
@Dict(dicCode = "sex")
@Excel(name = "性别", width = 15, dicCode = "sex")
private Integer sex;

@Dict(dictTable = "sys_depart", dicText = "depart_name", dicCode = "id")
@Excel(name = "部门", dictTable = "sys_depart", dicText = "depart_name", dicCode = "id")
private String departId;

// Controller
@AutoDict  // 方法级注解,启用自动字典翻译
@GetMapping(value = "/list")
public Result<?> list(...) { ... }

返回的 JSON 中会自动追加:

{
  "sex": 1,
  "sex_dictText": "男",
  "departId": "xxx",
  "departId_dictText": "技术部"
}

9.3 @PermissionData 数据权限

@PermissionData(pageComponent = "system/user/index")
@GetMapping(value = "/list")
public Result<?> list(...) { ... }

执行流程

1. @PermissionData 注解 → PermissionDataAspect 切面拦截
2. 查询当前用户在此菜单组件的 SysPermissionDataRule
3. 将规则写入 Request Attribute
4. QueryGenerator 从 Request 读取规则 → 拼接到 SQL WHERE 条件

十、缓存体系

10.1 缓存架构

组件 说明
Redis 底层缓存存储
RedisUtil Redis 操作工具类(来自 jeecg-boot-common 依赖)
Spring Cache(@Cacheable/@CacheEvict 声明式缓存
CacheConstant 缓存Key常量(jeecg-boot-common 依赖)

10.2 缓存使用最佳实践

字典缓存(框架已内置):

// 字典查询自动缓存
@Cacheable(value = CacheConstant.SYS_DICT_TABLE_CACHE)
public List<DictModel> queryDictItemsByCode(String code) { ... }

// 字典变更时清除缓存
@CacheEvict(value = {CacheConstant.SYS_DICT_CACHE, CacheConstant.SYS_ENABLE_DICT_CACHE}, allEntries = true)
public Result<?> delete(@RequestParam String id) { ... }

用户缓存(框架已内置):

// 用户变更时清除用户缓存
@CacheEvict(value = {CacheConstant.SYS_USERS_CACHE}, allEntries = true)
public void updateUser(...) { ... }

自定义业务缓存示例

@Cacheable(value = "myBusinessCache", key = "#id")
public XxxEntity getFromDb(String id) {
    return mapper.selectById(id);
}

@CacheEvict(value = "myBusinessCache", key = "#entity.id")
public void updateEntity(XxxEntity entity) {
    mapper.updateById(entity);
}

10.3 DictAspect 的 Redis 缓存

DictAspect 在字典翻译时直接使用 RedisTemplate

  • 普通字典 Key:sys:cache:dict::{dictCode}:{value}
  • 表字典 Key:sys:cache:dictTable::SimpleKey [{table},{text},{code},{value}]

十一、安全认证与权限体系

11.1 Shiro + JWT 架构

请求到达 → JwtFilter(校验Token)→ ShiroRealm(认证+授权)→ Controller

核心文件清单:

文件 功能
ShiroConfig.java Shiro核心配置,注册过滤器链、SecurityManager、Redis缓存
ShiroRealm.java 认证授权Realm(JWT解析、角色/权限查询)
JwtFilter.java JWT过滤器(Token校验、跨域处理、租户上下文注入)
JwtToken.java JWT Token封装
@IgnoreAuth 免认证注解(标注在方法上跳过Token校验)

11.2 权限注解使用

// 功能权限
@RequiresPermissions("system:user:add")
@PostMapping("/add")
public Result<?> add(@RequestBody SysUser user) { ... }

// 角色权限
@RequiresRoles("admin")
@GetMapping("/adminOnly")
public Result<?> adminOnly() { ... }

// 数据权限
@PermissionData(pageComponent = "system/user/index")
@GetMapping("/list")
public Result<?> list(...) { ... }

11.3 免认证配置

// 方式一:@IgnoreAuth 注解(方法级)
@IgnoreAuth
@GetMapping("/publicApi")
public Result<?> publicApi() { ... }

// 方式二:jeecg.shiro.excludeUrls 配置(路径级)
// 在 application.yml 中配置
jeecg:
  shiro:
    excludeUrls: /sys/common/static/**, /sys/common/upload, ...

十二、消息推送体系

12.1 消息发送架构

业务代码调用 PushMsgUtil.sendMessage(templateCode, params)
    ↓
Freemarker 渲染模板内容 → 写入 sys_sms 表
    ↓
Quartz 定时任务 SendMsgJob 扫描未发送消息
    ↓
根据消息类型分发到不同实现:
  ├── EmailSendMsgHandle    → 邮件
  ├── SmsSendMsgHandle      → 短信(支持阿里云/腾讯云)
  ├── WxSendMsgHandle       → 微信公众号
  ├── DdSendMsgHandle       → 钉钉
  ├── QywxSendMsgHandle     → 企业微信
  └── SystemSendMsgHandle   → 站内消息(WebSocket推送)

12.2 消息模板使用

@Autowired
private PushMsgUtil pushMsgUtil;

// 发送消息(模板编码 + 参数)
pushMsgUtil.sendMessage("C2010006", JSON.toJSONString(params));

12.3 系统通告

通过 SysAnnouncementController 管理通告的发布、撤回,SysAnnouncementSend 跟踪用户阅读状态。


十三、定时任务体系

13.1 Quartz 定时任务

// 1. 创建 Job 类,实现 org.quartz.Job 接口
public class MyJob implements Job {
    @Override
    public void execute(JobExecutionContext context) {
        // 定时任务逻辑
    }
}

// 2. 通过系统管理界面或 API 配置
// 实体: QuartzJob(jobClassName + cronExpression + parameter + status)
// 管理界面: /sys/quartzJob

13.2 示例 Job

  • SampleJob — 无参示例
  • SampleParamJob — 带参示例
  • AsyncJob — 异步执行示例
  • SendMsgJob — 消息发送定时扫描

十四、文件上传体系

14.1 上传架构

前端 POST /sys/common/upload (file + biz)
    ↓
CommonController.upload()
    ↓
SsrfFileTypeFilter.checkUploadFileType()  // 安全校验
    ↓
uploadType == "local" ?
  YES → uploadLocal() → /opt/upFiles
  NO  → CommonUtils.upload() → MinioUtil / OssBootUtil
    ↓
返回 Result(success=true, message=相对路径)

14.2 配置

jeecg:
  uploadType: local          # local / minio / alioss
  path:
    upload: /opt/upFiles     # 本地上传根目录
    webapp: /opt/webapp      # webapp路径
spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB

14.3 安全校验

检测项 方式
文件后缀白名单 jpg, jpeg, png, gif, bmp, pdf, doc, docx, xls, xlsx, zip, rar 等30+
文件头黑名单 jsp, php, class, sql(检测文件魔数)
路径遍历防护 过滤 .. 字符、限制深度5层
下载类型校验 checkDownloadFileType()

14.4 静态文件访问

  • 接口:GET /sys/common/static/{相对路径}
  • 认证:Shiro 配置为 anon(匿名访问)
  • 实现:流式读取本地文件输出

十五、代码生成器与在线表单

15.1 代码生成器模板

模板位于 jeecg-system-biz/src/main/resources/jeecg/code-template-online/

类型 风格 模板路径 支持前端
单表 经典 default/one/ vue2, vue3, vue3Native, uniapp
一对多 经典 default/onetomany/ vue2
一对多 JVxe jvxe/onetomany/ vue3, vue3Native
一对多 ERP erp/onetomany/ vue3, vue3Native
一对多 内嵌子表 inner-table/onetomany/ vue3
一对多 Tab tab/onetomany/ vue3
树形 默认 default/tree/ vue3, vue3Native

每个模板生成的文件:

  • 后端:Controller、Entity、Mapper、MapperXML、Service接口、Service实现
  • 前端:列表页、表单组件、弹窗组件、API接口定义、列/字段配置
  • SQL:菜单插入脚本

15.2 在线表单(Online Form)

通过 /online/cgform/** 接口提供零代码开发能力,核心路由:

URL 功能
/online/cgform/api/getData/{code} 获取表单数据
/online/cgform/api/getTreeData/{code} 获取树形数据
/online/cgform/api/addAll 新增配置
/online/cgform/api/editAll 编辑配置
/online/cgform/head/enhanceJs/** JS增强
/online/cgform/head/enhanceJava/** Java增强回调
/online/cgreport/** Online报表
/online/graphreport/** Online图表
/bigscreen/** 大屏配置
/drag/** 仪表盘配置

十六、开发一个新模块的最小步骤

基于以上分析,在 JeecgBoot 框架中开发一个带完整 CRUD + 导入导出 的新业务模块,所需步骤:

16.1 单表模块(4个步骤)

  1. 创建实体类:继承 JeecgEntity,加 @TableName@Excel@Dict 注解
  2. 创建 Mapper:继承 BaseMapper<T>
  3. 创建 Service:接口继承 IService<T>,实现继承 ServiceImpl<Mapper, T>
  4. 创建 Controller:继承 JeecgController<T, IService<T>>,实现6个CRUD方法 + 2个导入导出方法

无需额外开发的

  • 查询条件自动构建 → QueryGenerator 已实现
  • 公共字段自动填充 → MybatisInterceptor 已实现
  • 字典值自动翻译 → DictAspect + @Dict 已实现
  • 操作日志自动记录 → AutoLogAspect + @AutoLog 已实现
  • Excel 导出渲染 → JeecgEntityExcelView 已实现
  • Excel 导入解析 → ExcelImportUtil 已实现
  • 导入模板下载 → 复用 exportXls 接口

16.2 主子表模块(10个步骤)

  1. 创建主表实体:继承 JeecgEntity,加 @TableName@TableId
  2. 创建子表实体:同上,必须包含外键字段
  3. 创建VO类:包含主表字段 + @ExcelCollection 标注的子表列表字段
  4. 创建主表Mapper:继承 BaseMapper<T>
  5. 创建子表Mapper:继承 BaseMapper<T>,增加 deleteByMainId + selectByMainId
  6. 创建子表Service:接口 IService<T> + 实现 ServiceImpl<M, T>
  7. 创建主表Service接口:增加 saveMainupdateMaindelMaindelBatchMain
  8. 创建主表Service实现:注入所有子表Mapper,实现事务方法
  9. 创建Controller:继承 JeecgController<T, S>,实现CRUD + 子表查询 + 导入导出
  10. 前端页面配置:添加菜单SQL、配置路由、开发页面组件

十七、关键源码索引

17.1 框架核心库 (jeecg-boot-base-core)

组件 文件
Controller基类 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/controller/JeecgController.java
Entity基类 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/entity/JeecgEntity.java
Service基类接口 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/service/JeecgService.java
查询生成器 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/query/QueryGenerator.java
统一返回结果 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/api/vo/Result.java
全局异常处理器 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootExceptionHandler.java
核心业务异常 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootException.java
业务提醒异常 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootBizTipException.java
断言工具类 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/AssertUtils.java
导入工具类 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/ImportExcelUtil.java
通用工具类 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/oConvertUtils.java
SQL注入防护 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/SqlInjectionUtil.java
文件安全过滤器 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/filter/SsrfFileTypeFilter.java
全局常量 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/CommonConstant.java

17.2 切面与注解 (jeecg-boot-base-core)

组件 文件
日志切面 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/AutoLogAspect.java
字典翻译切面 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/DictAspect.java
数据权限切面 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/PermissionDataAspect.java
@AutoLog注解 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/AutoLog.java
@Dict注解 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/Dict.java
@AutoDict注解 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/AutoDict.java
@PermissionData注解 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/PermissionData.java
@IgnoreAuth注解 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/IgnoreAuth.java

17.3 全局配置 (jeecg-boot-base-core)

组件 文件
Shiro配置 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java
ShiroRealm jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroRealm.java
JWT过滤器 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/filters/JwtFilter.java
MVC配置 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/WebMvcConfiguration.java
MyBatis拦截器 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/mybatis/MybatisInterceptor.java
核心配置加载 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/JeecgBaseConfig.java
AutoPoi配置 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/AutoPoiConfig.java
Swagger3配置 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/Swagger3Config.java
多租户配置 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/mybatis/MybatisPlusSaasConfig.java

17.4 系统模块示例 (jeecg-module-system)

组件 文件
用户管理Controller jeecg-boot/jeecg-boot-module/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysUserController.java
字典管理Controller jeecg-boot/jeecg-boot-module/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDictController.java
角色管理Controller jeecg-boot/jeecg-boot-module/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysRoleController.java
通用上传Controller jeecg-boot/jeecg-boot-module/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/CommonController.java
定时任务Controller jeecg-boot/jeecg-boot-module/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/quartz/controller/QuartzJobController.java

17.5 Demo模块示例 (jeecg-module-demo)

组件 文件
单表CRUD Controller jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/controller/JeecgDemoController.java
主子表Controller jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/controller/JeecgOrderMainController.java
主子表Service实现 jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/service/impl/JeecgOrderMainServiceImpl.java
主子表VO jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/vo/JeecgOrderMainPage.java

十八、涉及的文件清单

文件 操作 原因
.docs/260603-JeecgBoot后端框架全局开发规范与最佳实践.md 新增 重新分析框架,撰写全面开发规范文档