4

趁热打铁,整一个新功能出来

 2 years ago
source link: http://www.javaboy.org/2022/0426/tienchin-feature.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

[TOC]

上篇文章中我们已经实现了自定义菜单了,我们可以根据自己的实际需求去定制自己需要的菜单,做好了这一步,接下来我们就可以开发新功能了。

我们就先从最简单的渠道管理开始。

还是老规矩,一个特别基础的细节我就不啰嗦了,如果大家阅读吃力,也可以先看看 vhr(https://github.com/lenve/vhr) 再看这个就容易多了。

1. 分配权限

我们依葫芦画瓢,首先在 sys_menu 中为渠道相关的操作添加权限,新增如下两条记录:

2008 就是渠道管理菜单项的 id。渠道管理将来就对应了这四个操作。

2. 渠道管理表

渠道管理比较简单,一张表,也不需要引用其他表,如下:

20220423171829.png

这个表很简单,没啥好说的。

3. 服务端接口开发

3.1 现有功能分析

用了这个脚手架,我也就懒得另起炉灶了,我们现在要写接口,接口该怎么写?我们可以参考一个他自己写好的,例如用户管理接口。

用户管理接口位置在 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 这个模块中。

好了,看懂这个,我们就照猫画虎。

3.2 创建工程

首先,我们新建一个自己的功能模块,这是一个 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 等,这些我就不逐一说明了,大家可以在文末下载源码查看。

3.3 配置 MP

这个脚手架中虽然用了 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 就配置好了。

3.4 生成代码

接下来,我们在 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 操作权限就按一开始在数据库中配置的内容即可。

照猫画虎,很快就写出来这样一个接口。

4. 开发前端页面

接下来我们来整前端页面,前端页面我们在第二篇文章中提到过,该功能对应的页面是 src/views/tienchin/channel/index.vue,所以我们只需要修改该页面即可,这个修改,我们也找一个参照物,找一个也是表格的页面改一下就行了,例如 src/views/system/dict/index.vue,这是字典管理的页面,我们就照着这个来改就行了,前端的代码量太大了,我就不全部贴出来了,我挑几个关键的地方来说一下。

4.1 网络请求

前端是每一个 .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'
})
}

4.2 页面展示

页面展示有一个地方需要和大家聊一聊。

就是当用户登录成功之后,前端会调用服务端的接口查看当前用户信息,包括用户的权限信息,而且前端还封装了一个空闲显示或者隐藏的工具,位置在 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_datasys_dict_type,像下面这样(下图为 sys_dict_data 表,关于他这个里边的数据字典,后面有空了松哥可以再整一篇文章和大家分析具体用法):

20220423174744.png

需要用到哪条记录,就在 vue 文件定义的时候声明就行了,像下面这样:

20220423174854.png

这样,后期就可以直接引用这个变量了,如下:

<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 基本上都能看懂,我也就不啰嗦了。

最终弄出来的页面如下:

20220423175152.png

好啦,今天就先聊这么多,源码地址如下:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK