深圳幻海软件技术有限公司 欢迎您!

趁热打铁,整一个新功能出来,你学会了吗?

2023-02-28

在​​上篇文章​​中我们已经实现了自定义菜单了,我们可以根据自己的实际需求去定制自己需要的菜单,做好了这一步,接下来我们就可以开发新功能了。我们就先从最简单的渠道管理开始。还是老规矩,一个特别基础的细节我就不啰嗦了,如果大家阅读吃力,也可以先看看vhr(https://github.com/lenv

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

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

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

1. 分配权限

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

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

2. 渠道管理表

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

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

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);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

大家看,首先通过权限注解确保用户具备相应的权限。这个权限注解对应的方法是 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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

创建完成后,我们手动修改一下 tienchin-channel 的 pom.xml 文件,照着脚手架 system 模块的改即可:

<description>
    渠道管理模块
</description>
<dependencies>
    <!-- 通用工具-->
    <dependency>
        <groupId>org.javaboy</groupId>
        <artifactId>tienchin-common</artifactId>
    </dependency>
</dependencies>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

渠道管理模块 org.javaboy tienchin-common

接下来,在 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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

另外依赖我还有一些细微的调整,例如为父模块添加了 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>
  • 1.
  • 2.
  • 3.
  • 4.

然后,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();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

小伙伴们看一下,在配置的过程中,将原本的 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();
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

自动生成的源码自带 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)));
    }

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.

都是常规操作,没啥特别值得说的地方。

@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'
  })
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.

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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.

每个按钮上都有一个 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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

options 其实就引用了数据字典中的值。

关于这个页面其他的内容就都是常规操作了,会 vhr 基本上都能看懂,我也就不啰嗦了。

最终弄出来的页面如下:

5. 小结

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

​​https://github.com/lenve/tienchin​​