# T-FAST **Repository Path**: Thmspring/t-fast ## Basic Information - **Project Name**: T-FAST - **Description**: T-FAST是一款基于SpringBoot+MyBatis-Plus的快速开发脚手架,拥有完整的权限管理功能,可对接Vue前端,开箱即用。 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2022-11-01 - **Last Updated**: 2023-03-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # T-FAST # 简介 T-FAST是一款基于SpringBoot+MyBatis-Plus的快速开发脚手架,拥有完整的权限管理功能,可对接Vue前端,开箱即用。 # 前端项目 该项目为前后端分离项目的后端部分,前端项目T-FAST-WEB地址:https://gitee.com/Thmspring/t-fast-web # 在线演示地址 - 地址:http://114.116.12.151 - 账号:develop - 密码:1qaz!QAZ # 技术选型 | 技术 | 版本 | 说明 | |------------------------|----------|-------------| | SpringBoot | 2.7.0 | 容器+MVC框架 | | SpringSecurity | 5.7.1 | 认证和授权框架 | | MyBatis | 3.5.4 | ORM框架 | | MyBatis-Plus | 3.5.2 | MyBatis增强工具 | | MyBatis-Plus Generator | 3.5.2 | 数据层代码生成器 | | Swagger-UI | 3.0.0 | 文档生产工具 | | MySql | 8.0.31 | 关系数据库 | | Redis | 6.2.7 | 分布式缓存 | | Docker | 20.10.17 | 应用容器引擎 | | Druid | 1.1.10 | 数据库连接池 | | Hutool | 5.8.8 | Java工具类库 | | JWT | 0.9.0 | JWT登录支持 | | Lombok | 1.18.24 | 简化对象封装工具 | | minio | 8.4.2 | oss对象存储 | | JDK | 17.0.4.1 | JDK | # 表结构 ![](./doc/img/01.png) - 化繁为简,仅保留了权限管理功能相关的表,方便自由定制; - 数据库源文件地址: https://gitee.com/Thmspring/t-fast/blob/master/doc/sql/t-fast.sql # 环境搭建 简化依赖服务,只需安装最常用的MySql和Redis服务即可 # 开发规约 ## 项目包结构 ```text src ├── core -- 系统核心 | ├── annotation -- 相关注解 | ├── aspect -- 相关切面 | ├── config -- 相关配置 | ├── constant -- 常量信息 | ├── entity -- 公共实体 | ├── enums -- 枚举信息 | ├── exception -- 全局异常处理相关类 | ├── generator -- MyBatis-Plus代码生成器 | ├── oss -- 对象存储 | ├── service -- 通用业务类 | ├── security -- SpringSecurity认证授权相关代码 | ├── task -- 定时任务 | | ├── base -- 抽象定时任务父类以及注册器 | | └── job -- 具体定时任务 | └── utils -- 通用工具类 ├── modules -- 存放业务代码的基础包 | └── sys -- 系统管理模块业务代码 | ├── controller -- 该模块相关接口 | ├── mapper -- 该模块相关Mapper接口 | ├── model -- 该模块相关实体类 | | ├── dto -- 该模块相关请求实体 | | └── vo -- 该模块相关响应实体 | └── service -- 该模块相关业务处理类 ``` ## 资源文件说明 ```text resources ├── log -- Logback日志配置文件 ├── mapper -- MyBatis中mapper.xml存放位置 ├── application.yml -- SpringBoot通用配置文件 ├── application-dev.yml -- SpringBoot开发环境配置文件 ├── application-prod.yml -- SpringBoot生产环境配置文件 └── generator.properties -- MyBatis-Plus代码生成器配置 ``` ## 接口定义规则 | 描述 | method | path | |------------------------|---------|-------------| | 分页查询数据 | GET | /{控制器路由名称}/page | | 获取数据列表 | GET | /{控制器路由名称}/list | | 获取指定记录详情 | GET | /{控制器路由名称}/{id} | | 新增数据 | POST | /{控制器路由名称}/insert | | 修改数据 | PUT | /{控制器路由名称}/update | | 删除数据 | DELETE | /{控制器路由名称}/delete/{id} | - 具体参数及返回结果定义可以运行代码查看Swagger-UI的Api文档: - http://localhost:11428/swagger-ui/index.html ![](./doc/img/02.png) - http://localhost:11428/doc.html ![](./doc/img/09.png) ## 项目运行 直接运行启动类`TFastApplication`的`main`函数 ## 业务代码开发流程 ### 创建业务表 >示例:创建好sys模块的所有表,需要注意的是一定要写好表字段的注释,这样实体类和接口文档中就会自动生成字段说明了。 ![](./doc/img/03.png) ### 使用代码生成器 > 运行MyBatisPlusGenerator类的main方法来生成代码,可直接生成controller、service、mapper、model、mapper.xml的代码,无需手动创建。 - 代码生成器支持两种模式,一种生成单表的代码,比如只生成`sys_dict`表代码可以先输入模块名称`sys`,后输入`sys_dict` ![](./doc/img/04.png) - 生成代码结构预览 - ![](./doc/img/05.png) - 直接生成整个模块代码,比如生成`sys`模块代码,先输入模块名称`sys`,再输入表通配`sys_*` ![](./doc/img/06.png) ## 编写业务代码 ### 参数定义与校验 - 请求参数 > 请求参数统一定义在该模块下的`model/dto`中 ![](./doc/img/07.png) - 新增参数格式:xxxInsertDto ```java @Data @EqualsAndHashCode(callSuper = false) public class MenuInsertDto { @ApiModelProperty(value = "父级ID 0-表示没有父级 ") private Long parentId; @ApiModelProperty(value = "菜单标题", required = true) @NotEmpty(message = "菜单标题不能为空") @Size(max = 12, message = "菜单标题不能超过12位") private String title; @ApiModelProperty(value = "菜单排序") private Integer sort; @ApiModelProperty(value = "前端路径", required = true) @NotEmpty(message = "菜单标题不能为空") @Size(max = 36, message = "菜单标题不能超过36位") private String path; @ApiModelProperty(value = "前端图标") private String icon; @ApiModelProperty(value = "前端隐藏 0-隐藏 1-显示") private Integer hidden; } ``` - 修改参数格式:xxxUpdateDto ```java @Data @EqualsAndHashCode(callSuper = false) public class MenuUpdateDto { @ApiModelProperty(value = "菜单ID", required = true) @NotNull(message = "菜单ID不能为空") private Long id; @ApiModelProperty(value = "父级ID 0-表示没有父级 ") private Long parentId; @ApiModelProperty(value = "菜单标题") @Size(max = 12, message = "菜单标题不能超过12位") private String title; @ApiModelProperty(value = "菜单层级") private Integer level; @ApiModelProperty(value = "菜单排序") private Integer sort; @ApiModelProperty(value = "前端路径") @Size(max = 36, message = "前端路径不能超过36位") private String path; @ApiModelProperty(value = "前端图标") private String icon; @ApiModelProperty(value = "前端隐藏 0-隐藏 1-显示") private Integer hidden; @ApiModelProperty(value = "删除标识 0-删除 1-未删除") private Integer delFlag; } ``` - 参数校验 > 接口中添加@Validated注解开启参数校验功能即可。 ```java @RestController @Api(tags = "系统:菜单管理") @RequestMapping("/sysMenu") public class SysMenuController { @Resource private SysMenuService sysMenuService; @ApiOperation("新增菜单信息") @PostMapping("/insert") public ViewResult insert(@Validated @RequestBody MenuInsertDto dto){ return ViewResult.success(sysMenuService.insert(dto)); } @ApiOperation("修改菜单信息") @PutMapping("/update") public ViewResult update(@Validated @RequestBody MenuUpdateDto dto) { boolean success = sysMenuService.update(dto); if (success) { return ViewResult.success(); } return ViewResult.error(); } } ``` ### 单表操作 > 由于MyBatis-Plus提供的增强功能相当强大,单表查询几乎不用手写SQL,直接使用ServiceImpl和BaseMapper中提供的方法即可。 比如我们的菜单管理业务实现类UmsMenuServiceImpl中的方法都直接使用了这些方法。 ```java @Service public class SysDictServiceImpl extends ServiceImpl implements SysDictService { @Resource private SysDictItemService sysDictItemService; @Override public Page page(String keyword, Integer pageNum, Integer pageSize) { Page page = new Page<>(pageNum, pageSize); LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); if (StrUtil.isNotEmpty(keyword)) { wrapper.like(SysDict::getType, keyword) .or() .like(SysDict::getName, keyword); } wrapper.ne(SysDict::getDelFlag, DeleteFlagEnum.DELETED.getCode()); return this.page(page, wrapper); } @Override @Transactional public Long insert(DictInsertDto dto) { SysDict sysDict = this.getOne(Wrappers .lambdaQuery() .eq(SysDict::getType, dto.getType())); if (Objects.nonNull(sysDict)){ Asserts.fail("字典已经存在"); } sysDict = BeanUtil.toBean(dto,SysDict.class); SysUserDetails currentUser = SecurityUtil.getCurrentUser(); sysDict.setCreateUser(currentUser.getUserId()); sysDict.setCreateTime(currentUser.getDate()); sysDict.setUpdateUser(currentUser.getUserId()); sysDict.setUpdateTime(currentUser.getDate()); this.save(sysDict); return sysDict.getId(); } @Override @Transactional public Boolean update(DictUpdateDto dto) { if (Objects.isNull(this.getById(dto.getId()))){ Asserts.fail("字典信息不存在"); } SysDict sysDict = this.getOne(Wrappers .lambdaQuery() .eq(SysDict::getType, dto.getType()) .ne(SysDict::getId, dto.getId())); if (Objects.nonNull(sysDict)){ Asserts.fail(dto.getType() + "字典已经存在"); } sysDict = BeanUtil.toBean(dto, SysDict.class); //修改字典类型,同时修改字典项信息 if (StrUtil.isNotEmpty(sysDict.getType())){ sysDictItemService.update(Wrappers .lambdaUpdate() .set(SysDictItem::getType,sysDict.getType()) .eq(SysDictItem::getDictId,sysDict.getId())); } SysUserDetails currentUser = SecurityUtil.getCurrentUser(); sysDict.setUpdateUser(currentUser.getUserId()); sysDict.setUpdateTime(currentUser.getDate()); //删除字典相关缓存 sysDictItemService.delDictCache(sysDict.getType()); return this.updateById(sysDict); } @Override @Transactional public Boolean delete(Long dictId) { SysDict sysDict = this.getById(dictId); if (Objects.isNull(sysDict)){ Asserts.fail("字典信息不存在"); } int count = sysDictItemService.count(Wrappers .lambdaQuery() .eq(SysDictItem::getDictId, dictId) .eq(SysDictItem::getDelFlag,DeleteFlagEnum.UNDELETE.getCode())); if (count != 0){ Asserts.fail("存在字典项信息,不能删除字典信息"); } SysUserDetails currentUser = SecurityUtil.getCurrentUser(); sysDict.setUpdateUser(currentUser.getUserId()); sysDict.setUpdateTime(currentUser.getDate()); sysDict.setDelFlag(DeleteFlagEnum.DELETED.getCode()); //删除与菜单相关缓存 sysDictItemService.delDictCache(sysDict.getType()); return this.updateById(sysDict); } } ``` ### 分页查询 > 对于分页查询MyBatis-Plus原生支持,不需要再整合其他插件,直接构造Page对象,然后调用ServiceImpl中的page方法即可。 - 单表分页查询 ```java @Service public class SysDictServiceImpl extends ServiceImpl implements SysDictService { @Resource private SysDictItemService sysDictItemService; @Override public Page page(String keyword, Integer pageNum, Integer pageSize) { Page page = new Page<>(pageNum, pageSize); LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); if (StrUtil.isNotEmpty(keyword)) { wrapper.like(SysDict::getType, keyword) .or() .like(SysDict::getName, keyword); } wrapper.ne(SysDict::getDelFlag, DeleteFlagEnum.DELETED.getCode()); return this.page(page, wrapper); } } ``` - 多表分页查询 - service ```java @Service public class SysLogServiceImpl extends ServiceImpl implements SysLogService { @Resource private SysUserService sysUserService; @Override public Page page(String keyword, Integer type, Integer pageNum, Integer pageSize) { Page page = new Page<>(pageNum,pageSize); List logVos = this.baseMapper.page(keyword, type, page); page.setRecords(logVos); return page; } } ``` - mapper ```java public interface SysLogMapper extends BaseMapper { /** * 根据条件分页获取日志列表 * @param keyword 关键字:日志标题 * @param type 日志类型 * @param iPage 分页参数 * @return */ List page(@Param("keyword") String keyword, @Param("type") Integer type, IPage iPage); } ``` - xml ```xml ``` ### 多表查询 > 对于多表查询,我们需要手写mapper.xml中的SQL实现,由于之前我们已经生成了mapper.xml文件,所以我们直接在Mapper接口中定义好方法,然后在mapper.xml写好SQL实现即可。 - 比如说我们需要写一个根据用户ID获取其分配的权限的方法,首先我们在`SysPermissionMapper`接口中添加好`getPermissionByUserId`方法 ```java public interface SysPermissionMapper extends BaseMapper { /** * 通过用户ID获取权限集合 * @param userId 用户ID * @return 权限集合 */ List getPermissionByUserId(Long userId); } ``` - 然后在`SysPermissionMapper.xml`添加该方法的对应SQL实现即可。 ```xml ``` ### 定时任务 > 项目已经提供定时任务模板,也可以自行实现 - 创建定时任务如`SimpleTask`并继承`AbstractTask` ```java @Slf4j @Component public class SimpleTask extends AbstractTask { @Override @PostConstruct public void init(AbstractTask task) { super.init(this); } @Override @Scheduled(cron = "0/5 * * * * ?") public void execute() { try { log.info("简单的定时任务!!!!"); status = 1; } catch (Exception e){ status = 2; log.error("任务运行异常,e={}",e.getMessage()); } } @Override public String getTaskName() { return SimpleTask.class.getSimpleName(); } @Override public String getDesc() { return "示例定时任务"; } @Override public Integer getStatus() { return status; } } ``` # 接口权限 > 项目采用动态接口权限,无需编码只需要添加接口资源配置即可达到权限控制目地,灵活多变! ## 配置方法 - 直接操作数据表:sys_resource - 通过前端资源管理进行添加 ![](./doc/img/10.png) ## 配置示例 > 项目接口采用restful风格,有一定要求具体参考以下示例 - GET请求配置 - 示例接口:/sysOrganization/page ![](./doc/img/11.png) - 示例接口:/sysOrganization/{orgId} > 请求接口带ID或者其他参数配置时只需要写前缀即可,无须写上{XXX} ![](./doc/img/12.png) - POST请求配置 - 示例接口:/sysOrganization/insert ![](./doc/img/13.png) - PUT请求配置 - 示例接口:/sysOrganization/update ![](./doc/img/14.png) - DELETE请求配置 - 示例接口:/sysOrganization/delete/{orgId} ![](./doc/img/15.png) # 项目部署 > 项目采用docker容器化部署,非docker环境请求自行部署 - 部署环境准备 项目依赖环境`docker-compose.yml`已经提供 路径:https://gitee.com/Thmspring/t-fast/blob/master/doc/docker/environment/docker-compose.yml ```shell docker-compose -f docker-compose.yml up -d ``` - jar部署 - 项目采用`DockerFile`部署已经编写完整的部署脚本,结果如下: ![](./doc/img/08.png) - apps:存在配置文件和运行jar - app.env:配置jar名称(影响apps目录下jar包名称)、jvm参数、引用配置文件(影响apps目录下配置文件) - docker-compose.yml:配置启动容器名称、Dockerfile、映射容器卷等 - Dockerfile:定义容器工作目录、启动运行脚本 - entrypoint.sh:容器启动后运行脚本 - global.env:全局配置(如果没有app.env则此配置文件生效) - 部署步骤 1. 进入服务器创建服务目录如:deploy ```shell mkdir deploy ``` 2. 进入目录创建`apps`文件夹并上传相关文件 ```shell cd deploy # 上传:app.env、docker-compose.yml、Dockerfile、entrypoint.sh、global.env mkdir apps cd apps # 上传:application-prod.yml、t-fast.jar ``` 3. 启动容器 ```shell cd deploy # 启动 docker-compose -f docker-compose.yml up -d # 停止 docker-compose -f docker-compose.yml down # 查看日志 docker logs -f t-fast # 查看映射日志 cd logs tail -f t-fast.log ```