在上篇文章中我们已经实现了自定义菜单了,我们可以根据自己的实际需求去定制自己需要的菜单,做好了这一步,接下来我们就可以开发新功能了。
我们就先从最简单的渠道管理开始。
❝还是老规矩,一个特别基础的细节我就不啰嗦了,如果大家阅读吃力,也可以先看看 vhr(https://github.com/lenve/vhr) 再看这个就容易多了。
我们依葫芦画瓢,首先在 sys_menu 中为渠道相关的操作添加权限,新增如下两条记录:
2008 就是渠道管理菜单项的 id。渠道管理将来就对应了这四个操作。
渠道管理比较简单,一张表,也不需要引用其他表,如下:
这个表很简单,没啥好说的。
用了这个脚手架,我也就懒得另起炉灶了,我们现在要写接口,接口该怎么写?我们可以参考一个他自己写好的,例如用户管理接口。
用户管理接口位置在 org.javaboy.web.controller.system.SysUserController#list
方法中:
@PreAuthorize("@ss.hasPermi('system:user:list')")
@GetMapping("/list")
public TableDataInfo list(SysUser user) {
startPage();
List<SysUser> list = userService.selectUserList(user);
return getDataTable(list);
}
大家看,首先通过权限注解确保用户具备相应的权限。这个权限注解对应的方法是 org.javaboy.framework.web.service.PermissionService#hasPermi
方法,具体的逻辑也并不难,当用户登录成功后,会查询出来当前用户的所有权限,并放到 LoginUser 对象中(这个在本系列的第一篇文章中已经讲过了),然后将之存入到 Redis 中,现在这里就是从 Redis 中取回 LoginUser 对象,然后拿出来用户的权限字符串,跟这里需要的权限字符串做比对。
由于这个脚手架自定义了一个 BaseController,里边封装了很多常用的操作,所有的业务 Controller 都是继承自这个 BaseController,所以这里的 startPage 方法其实就是 BaseController 中的方法,这个方法会自动开启分页功能,会从当前请求中提取出分页参数,然后进行查询。如果前端没有传递分页参数,那么默认查询第一页,查询 10 条数据。
接下来就是一个常规的查询操作,没啥好说的。
最后的 getDataTable 方法则是将数据包装成一个分页的 JSON 对象。
还有一点要捋清楚,就是这个脚手架是一个多模块项目,所有的借口定义统一在 admin 中,不同的功能对应不同的模块,例如用户管理相关的功能都在 system 这个模块中。
好了,看懂这个,我们就照猫画虎。
首先,我们新建一个自己的功能模块,这是一个 maven 项目,叫做 tienchin-channel。
这里我想用 MyBatis-Plus 来做,因此我先修改父工程的 dependencyManagement,将 mp 的版本号统一管理起来,同时也将新建模块加进去,方便后期引用的时候进行版本号统一管理:
<properties>
<mybatis-plus-boot-starter.version>3.5.1</mybatis-plus-boot-starter.version>
<mybatis-plus-generator.version>3.5.2</mybatis-plus-generator.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus-generator.version}</version>
</dependency>
<dependency>
<groupId>org.javaboy</groupId>
<artifactId>tienchin-channel</artifactId>
<version>${tienchin.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
创建完成后,我们手动修改一下 tienchin-channel 的 pom.xml 文件,照着脚手架 system 模块的改即可:
<description>
渠道管理模块
</description>
<dependencies>
<!-- 通用工具-->
<dependency>
<groupId>org.javaboy</groupId>
<artifactId>tienchin-common</artifactId>
</dependency>
</dependencies>
接下来,在 admin 模块中,依赖当前新建的 tienchin-channel 模块和 mp 的代码自动生成依赖,如下:
<dependency>
<groupId>org.javaboy</groupId>
<artifactId>tienchin-channel</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<scope>test</scope>
</dependency>
另外依赖我还有一些细微的调整,例如为父模块添加了 Spring Boot 作为其 parent 等,这些我就不逐一说明了,大家可以在文末下载源码查看。
这个脚手架中虽然用了 MyBatis 的 starter,但是实际上还是自己手动配置的 MyBatis,所以当我们使用 MP 的时候,并不能像在 Spring Boot 中使用 MP 那样,加个依赖就行了,我们还需要手动改一下配置。
首先我们将 mp 的依赖放到 common 模块中,毕竟将来无论是 framework 还是我们新建的 tienchin-channel 都依赖 common 模块,如下:
tienchin-common/pom.xml:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
然后,MyBatis 的配置是在 framework 模块中,具体代码在 tienchin-framework/src/main/java/org/javaboy/framework/config/MyBatisConfig.java
位置,我们直接在此进行修改即可。
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
String mapperLocations = env.getProperty("mybatis.mapperLocations");
String configLocation = env.getProperty("mybatis.configLocation");
typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
VFS.addImplClass(SpringBootVFS.class);
final MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
return sessionFactory.getObject();
}
小伙伴们看一下,在配置的过程中,将原本的 SqlSessionFactoryBean 改为 MybatisSqlSessionFactoryBean,其他都不变即可。
如此,我们的 MP 就配置好了。
接下来,我们在 admin 模块的单元测试中,通过如下代码来生成一下 channel 对应的实体类啥的,如果大家对这个自动生成代码的不熟悉的话,可以看看这篇文章:自动生成实体类,哪个最佳?:
public class Generator {
@Test
public void channelGenerator() {
FastAutoGenerator.create("jdbc:mysql:///tienchin?serverTimezone=Asia/Shanghai&useSSL=false", "root", "123")
.globalConfig(builder -> {
builder.author("javaboy") // 设置作者
.disableOpenDir()
.fileOverride() // 覆盖已生成文件
.outputDir("/Users/sang/workspace/workspace02/tienchin/tienchin-channel/src/main/java"); // 指定输出目录
})
.packageConfig(builder -> {
builder.parent("org.javaboy") // 设置父包名
.moduleName("channel") // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.xml, "/Users/sang/workspace/workspace02/tienchin/tienchin-channel/src/main/resources/mapper/channel")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.addInclude("tienchin_channel") // 设置需要生成的表名
.addTablePrefix("tienchin_");
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
}
}
自动生成的源码自带 Controller,我们将其删除,重新在 admin 模块中创建对应的 ChannelController 即可。
对照现有的任意一个 Controller,我们写出来自己的 Controller,如下:
@RestController
@RequestMapping("/tienchin/channel")
public class ChannelController extends BaseController {
@Autowired
IChannelService channelService;
@PreAuthorize("@ss.hasPermi('tienchin:channel:query')")
@GetMapping("/list")
public TableDataInfo getChannelList() {
startPage();
List<Channel> list = channelService.list();
return getDataTable(list);
}
@PreAuthorize("@ss.hasPermi('tienchin:channel:add')")
@Log(title = "渠道管理" , businessType = BusinessType.INSERT)
@PostMapping("/")
public AjaxResult add(@Validated @RequestBody Channel channel) {
channel.setCreateBy(getUsername());
return toAjax(channelService.saveChannel(channel));
}
@PreAuthorize("@ss.hasPermi('tienchin:channel:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable Long id) {
return AjaxResult.success(channelService.getById(id));
}
@PreAuthorize("@ss.hasPermi('tienchin:channel:edit')")
@Log(title = "渠道管理" , businessType = BusinessType.UPDATE)
@PutMapping("/")
public AjaxResult edit(@Validated @RequestBody Channel channel) {
channel.setUpdateBy(getUsername());
channel.setUpdateTime(LocalDateTime.now());
return toAjax(channelService.saveOrUpdate(channel));
}
@PreAuthorize("@ss.hasPermi('tienchin:channel:remove')")
@Log(title = "渠道管理" , businessType = BusinessType.DELETE)
@DeleteMapping("/{channelIds}")
public AjaxResult remove(@PathVariable Long[] channelIds) {
return toAjax(channelService.removeBatchByIds(Arrays.asList(channelIds)));
}
}
都是常规操作,没啥特别值得说的地方。
@Log 是脚手架中定义的日志记录注解,加一个这个注解,会自动将当前的操作记录到 sys_oper_log 表中,像下面这样:
@PreAuthorize 操作权限就按一开始在数据库中配置的内容即可。
照猫画虎,很快就写出来这样一个接口。
接下来我们来整前端页面,前端页面我们在第二篇文章中提到过,该功能对应的页面是 src/views/tienchin/channel/index.vue
,所以我们只需要修改该页面即可,这个修改,我们也找一个参照物,找一个也是表格的页面改一下就行了,例如 src/views/system/dict/index.vue
,这是字典管理的页面,我们就照着这个来改就行了,前端的代码量太大了,我就不全部贴出来了,我挑几个关键的地方来说一下。
前端是每一个 .vue
文件都将自己所需的网络请求封装在一个 js 文件中,然后将来在 .vue
文件中直接引用。
例如关于数据字典的所有请求封装在 src/api/system/dict/type.js
文件中,我照猫画虎写了关于 channel 的所有网络请求:
src/api/channel/index.js
import request from '@/utils/request'
// 查询所有的渠道信息
export function listChannel(query) {
return request({
url: '/tienchin/channel/list',
method: 'get',
params: query
})
}
// 根据 id 查询某一个渠道的信息
export function getChannel(channelId) {
return request({
url: '/tienchin/channel/' + channelId,
method: 'get'
})
}
// 添加渠道
export function addChannel(data) {
return request({
url: '/tienchin/channel/',
method: 'post',
data: data
})
}
// 更新渠道信息
export function updateChannel(data) {
return request({
url: '/tienchin/channel/',
method: 'put',
data: data
})
}
// 根据 id 删除渠道
export function delChannel(channelIds) {
return request({
url: '/tienchin/channel/' + channelIds,
method: 'delete'
})
}
页面展示有一个地方需要和大家聊一聊。
就是当用户登录成功之后,前端会调用服务端的接口查看当前用户信息,包括用户的权限信息,而且前端还封装了一个空闲显示或者隐藏的工具,位置在 src/directive/permission/hasPermi.js
,这个工具最终被做成了一个自定义指令,这样,我们在展示每一个按钮的时候,可以加上这个指令,将来就会自动根据用户是否具备相应的权限来展示相应的按钮,例如下面这几个按钮:
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['tienchin:channel:add']"
>新增
</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['tienchin:channel:edit']"
>修改
</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['tienchin:channel:remove']"
>删除
</el-button>
</el-col>
每个按钮上都有一个 v-hasPermi 标签来表述这个按钮将来显示的条件。
另外,前端也使用到了数据字典,也就是一些常见的字段取值我们将之固定下来了,在前端直接引用即可。数据字典本身对应的表是 sys_dict_data
和 sys_dict_type
,像下面这样(下图为 sys_dict_data
表,关于他这个里边的数据字典,后面有空了松哥可以再整一篇文章和大家分析具体用法):
需要用到哪条记录,就在 vue 文件定义的时候声明就行了,像下面这样:
这样,后期就可以直接引用这个变量了,如下:
<el-table-column label="渠道状态" align="center" prop="status">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status"/>
</template>
</el-table-column>
options 其实就引用了数据字典中的值。
关于这个页面其他的内容就都是常规操作了,会 vhr 基本上都能看懂,我也就不啰嗦了。
最终弄出来的页面如下:
好啦,今天就先聊这么多,源码地址如下: