一、人员管理
1、需求说明
人员管理业务流程如下:
- 登录系统: 首先,后台管理人员需要登录到帝可得后台管理系统中。
- 新增工作人员: 登录系统后,管理人员可以新增工作人员,包括姓名、联系方式等信息。
- 关联角色: 确定此员工是运维人员还是运营人员,这将影响他们的职责和权限。
- 关联区域: 确定员工负责的区域范围,确保工作人员能够高效地完成区域内的设备安装、维修、商品补货等工作。
对于人员和其他管理数据,下面是示意图:
- 关系字段:role_id、region_id
- 数据字典:status(1启用、0停用)
- 冗余字段:region_name、role_code、role_name
关于冗余字段的设计
- 优点:减少关联查询,现在只需要单表查询,提升了查询速度,简化查询逻辑。在读写分离架构时,冗余字段可以降低对主表的依赖,增加了读写操作的灵活性。
- 缺点:增加了数据维护的复杂性,占用了额外的存储空间,空间换时间。
2、生成基础代码
需求:使用若依代码生成器,生成人员列表前后端基础代码,并导入到项目中
(1)创建目录菜单
创建人员管理目录菜单
(2)添加数据字典
先创建员工状态
的字典类型
再创建员工状态
的字典数据
(3)配置代码生成信息
导入tb_emp、tb_role两张表
配置员工表(参考原型)
配置角色表(无原型无页面,只需要所生成的role.js和后端项目文件)
(4)下载代码并导入项目
选中二张表生成下载,解压ruoyi.zip
得到前后端代码和动态菜单sql
注意:角色动态菜单sql和视图组件不需要导入
前后端代码导入
对于无页面原型的角色表,需要导入所生成的role.js和后端项目文件(仅提供前后端api支持),是因为员工表的前端请求页面,在新增和修改的表单里需要查询角色名称列表下拉框。
3、人员列表改造
(1)基础页面
- 需求:参考页面原型,完成基础布局展示改造
- 代码实现
在emp/index.vue视图组件中修改
<!-- 搜索区域 --> <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px"> <el-form-item label="人员名称" prop="userName"> <el-input v-model="queryParams.userName" placeholder="请输入人员名称" clearable @keyup.enter="handleQuery" /> </el-form-item> <el-form-item> <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button> <el-button icon="Refresh" @click="resetQuery">重置</el-button> </el-form-item> </el-form> <!-- 人员列表 --> <el-table v-loading="loading" :data="empList" @selection-change="handleSelectionChange"> <el-table-column type="selection" width="55" align="center" /> <el-table-column label="序号" type="index" width="80" align="center" prop="id" /> <el-table-column label="人员名称" align="center" prop="userName" /> <el-table-column label="归属区域" align="center" prop="regionName" /> <el-table-column label="角色" align="center" prop="roleName" /> <el-table-column label="联系电话" align="center" prop="mobile" /> <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <template #default="scope"> <el-button link type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['manage:emp:edit']">修改</el-button> <el-button link type="primary" @click="handleDelete(scope.row)" v-hasPermi="['manage:emp:remove']">删除</el-button> </template> </el-table-column> </el-table> <!-- 添加或修改人员列表对话框 --> <el-dialog :title="title" v-model="open" width="500px" append-to-body> <el-form ref="empRef" :model="form" :rules="rules" label-width="80px"> <el-form-item label="员工名称" prop="userName"> <el-input v-model="form.userName" placeholder="请输入员工名称" /> </el-form-item> <el-form-item label="角色" prop="roleId"> <!-- <el-input v-model="form.roleId" placeholder="请输入角色id" /> --> <el-select v-model="form.roleId" placeholder="请选择角色"> <el-option v-for="item in roleList" :key="item.roleId" :label="item.roleName" :value="item.roleId" /> </el-select> </el-form-item> <el-form-item label="联系电话" prop="mobile"> <el-input v-model="form.mobile" placeholder="请输入联系电话" /> </el-form-item> <el-form-item label="创建时间" prop="createTime" v-if="form.id!=null"> {{form.createTime }} </el-form-item> <el-form-item label="负责区域" prop="regionId"> <!-- <el-input v-model="form.regionId" placeholder="请输入所属区域Id" /> --> <el-select v-model="form.regionId" placeholder="请选择所属区域"> <el-option v-for="item in regionList" :key="item.id" :label="item.regionName" :value="item.id" /> </el-select> </el-form-item> <el-form-item label="员工头像" prop="image"> <image-upload v-model="form.image" /> </el-form-item> <el-form-item label="是否启用" prop="status"> <el-radio-group v-model="form.status"> <el-radio v-for="dict in emp_status" :key="dict.value" :label="parseInt(dict.value)">{{ dict.label }}</el-radio> </el-radio-group> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="submitForm">确 定</el-button> <el-button @click="cancel">取 消</el-button> </div> </template> </el-dialog> <script> import { listRegion } from "@/api/manage/region"; import { listRole } from "@/api/manage/role"; import { loadAllParams } from "@/api/page"; // 查询角色列表 const roleList = ref([]); function getRoleList() { listRole(loadAllParams).then(response => { roleList.value = response.rows; }); } // 查询区域列表 const regionList = ref([]); function getRegionList() { listRegion(loadAllParams).then(response => { regionList.value = response.rows; }); } getRegionList(); getRoleList(); </script>
因为前端新增和修改的表单提交的是角色id和区域id,还需要后端通过这两个id查询数据库相关字段,在EmpServiceImpl中新增和修改时,补充区域名称和角色信息给员工表的冗余字段。
@Autowired private RegionMapper regionMapper; @Autowired private RoleMapper roleMapper; /** * 新增人员列表 * * @param emp 人员列表 * @return 结果 */ @Override public int insertEmp(Emp emp) { // 补充区域名称 emp.setRegionName(regionMapper.selectRegionById(emp.getRegionId()).getRegionName()); // 补充角色信息 Role role = roleMapper.selectRoleByRoleId(emp.getRoleId()); emp.setRoleName(role.getRoleName()); emp.setRoleCode(role.getRoleCode()); emp.setCreateTime(DateUtils.getNowDate()); return empMapper.insertEmp(emp); } /** * 修改人员列表 * * @param emp 人员列表 * @return 结果 */ @Override public int updateEmp(Emp emp) { // 补充区域名称 emp.setRegionName(regionMapper.selectRegionById(emp.getRegionId()).getRegionName()); // 补充角色信息 Role role = roleMapper.selectRoleByRoleId(emp.getRoleId()); emp.setRoleName(role.getRoleName()); emp.setRoleCode(role.getRoleCode()); emp.setUpdateTime(DateUtils.getNowDate()); return empMapper.updateEmp(emp); }
- 测试新增和修改
(2)同步存储
- 问题演示:
当我们在修改区域管理中修改某个区域的名称时,区域表可以单表更新。
但是员工表中的区域显示没有发生改变,并没有同步更新。
- 此问题的解决方案:
同步存储:在员工表中有区域名称的冗余字段,在更新区域表的同时,同步更新员工表中区域名称。
- 优点:由于是单表查询操作,查询列表效率最高。
- 缺点:需要在区域修改时修改员工表中的数据,有额外的开销,数据也可能不一致。
- 这种同步存储适合读多写少的业务场景,不适合修改频繁的场景。
- SQL
-- 根据区域id修改区域名称 update tb_emp set region_name='北京市奥体中心' where region_id=4;
- EmpMapper
/** * 根据区域id修改员工表的区域名称(同步更新) * @param regionName * @param regionId * @return */ @Update("update tb_emp set region_name = #{regionName} where region_id = #{regionId}") int updateEmpByRegionId(@Param("regionName") String regionName, @Param("regionId") Long regionId);
- RegionServiceImpl
@Autowired private EmpMapper empMapper; /** * 修改区域管理 * * @param region 区域管理 * @return 结果 */ @Transactional(rollbackFor = Exception.class) @Override public int updateRegion(Region region) { // 先更新区域信息 region.setUpdateTime(DateUtils.getNowDate()); int result = regionMapper.updateRegion(region); // 同步更新员工表区域名称 empMapper.updateByRegionId(region.getRegionName(),region.getId()); return result; }
- 保持同步更新测试
4、文件存储
(1)本地存储
问题分析说明:
在若依框架目前的实现中,默认是把图片存储到了服务器本地的目录,通过服务进行访问。这样做存储的是比较省事,但是缺点也有很多:
- 硬件与网络要求:服务器通常需要高性能的硬件和稳定的网络环境,以保证文件传输的效率和稳定性。这可能会增加硬件和网络资源的成本和维护难度。
- 管理难度:服务器目录需要管理员进行配置和管理,包括权限设置、备份策略等。如果管理不善或配置不当,可能会引发一些安全问题和性能问题。
- 性能瓶颈:如果服务器处理能力不足或网络带宽不够,可能会导致性能瓶颈,影响文件上传、下载和访问的速度。
- 单点故障风险:服务器故障可能导致所有存储在其上的文件无法访问,尽管可以通过备份和冗余措施来降低这种风险,但单点故障的风险仍然存在。
- 本地存储:无法直接访问、磁盘空间限制、磁盘损坏等。
为了解决上述问题,通常有两种解决方案:
- 自己搭建存储服务器,如:fastDFS 、MinIO
- 使用现成的云服务,如:阿里云,腾讯云,华为云
(2)阿里云OSS
- 介绍
阿里云对象存储OSS(Object Storage Service),是一款海量、安全、低成本、高可靠的云存储服务。使用OSS,您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。
在我们使用了阿里云OSS对象存储服务之后,我们的项目当中如果涉及到文件上传这样的业务,在前端进行文件上传并请求到服务端时,在服务器本地磁盘当中就不需要再来存储文件了。我们直接将接收到的文件上传到oss,由oss帮我们存储和管理,同时阿里云的oss存储服务还保障了我们所存储内容的安全可靠。
第三方服务使用的通用思路,我们做一个简单介绍之后,接下来我们就来介绍一下我们当前要使用的阿里云oss对象存储服务具体的使用步骤。
Bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间。
SDK:Software Development Kit 的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包)、代码示例等,都可以叫做SDK。
简单说,sdk中包含了我们使用第三方云服务时所需要的依赖,以及一些示例代码。我们可以参照sdk所提供的示例代码就可以完成入门程序。
阿里云OSS具体使用教程请参考:https://blog.csdn.net/weixin_52152676/article/details/142332699
(3)x-file-storage
- 介绍
官方地址:https://x-file-storage.xuyanwu.cn/#/
一行代码将文件存储到本地、FTP、SFTP、WebDAV、阿里云 OSS、华为云 OBS、七牛云 Kodo、腾讯云 COS、百度云 BOS、又拍云 USS、MinIO、 Amazon S3、GoogleCloud Storage、FastDFS、 Azure Blob Storage、Cloudflare R2、金山云 KS3、美团云 MSS、京东云 OSS、天翼云 OOS、移动 云EOS、沃云 OSS、 网易数帆 NOS、Ucloud US3、青云 QingStor、平安云 OBS、首云 OSS、IBM COS、其它兼容 S3 协议的存储平台。
- 集成
1)在dkd-common的pom.xml中引入依赖
<!-- x-file-storage 统一文件上传管理器 --> <dependency> <groupId>org.dromara.x-file-storage</groupId> <artifactId>x-file-storage-spring</artifactId> <version>2.2.1</version> </dependency> <!-- 阿里云OSS --> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.17.4</version> </dependency> <!-- Java 9及以上的版本还需引入以下三个依赖 --> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency> <!-- no more than 2.3.3--> <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>2.3.3</version> </dependency>
2)在dkd-admin的application.yml
配置文件中先添加以下基础配置,再添加对应平台的配置
# 文件上传 dromara: x-file-storage: # 文件存储配置 default-platform: aliyun-oss-1 # 默认使用的存储平台 thumbnail-suffix: ".min.jpg" # 缩略图后缀,例如【.min.jpg】【.png】 # 对应平台的配置写在这里,注意缩进要对齐 aliyun-oss: # 阿里云OSS配置 - platform: aliyun-oss-1 # 存储平台标识 enable-storage: true # 启用存储 access-key: ?? secret-key: ?? end-point: oss-cn-beijing.aliyuncs.com # 不需要带https:// bucket-name: oss-bucket-web domain: https://oss-bucket-web.oss-cn-beijing.aliyuncs.com/ # 访问域名,注意“/”结尾,例如:https://abc.oss-cn-shanghai.aliyuncs.com/ base-path: dkd/dkd-images/ # 基础路径
3)在dkd-admin的启动类上加上@EnableFileStorage
注解
@EnableFileStorage @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }) public class DkdApplication { public static void main(String[] args) { // System.setProperty("spring.devtools.restart.enabled", "false"); SpringApplication.run(DkdApplication.class, args); System.out.println("(♥◠‿◠)ノ゙ 帝可得启动成功 ლ(´ڡ`ლ)゙"); } }
4)修改若依默认上传图片代码
若依前端上传图片发送的请求路径为:http://localhost/dev-api/common/upload,路由转发到后端就是http://localhost:8080/common/upload
找到ruoyi-admin模块中的com.ruoyi.web.controller.common.CommonController类,修改单个文件上传的方法
@Autowired private FileStorageService fileStorageService;//注入实列 /** * 通用上传请求(单个) */ @PostMapping("/upload") public AjaxResult uploadFile(MultipartFile file) throws Exception { try { // 指定oss保存文件路径 dkd/dkd-images/2024/06/19/文件名 String relativePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) + "/"; // 上传图片,成功返回文件信息 FileInfo fileInfo = fileStorageService.of(file).setPath(relativePath).upload(); // 设置返回结果 AjaxResult ajax = AjaxResult.success(); ajax.put("url", fileInfo.getUrl()); ajax.put("fileName", fileInfo.getUrl()); // 这里的值要改为ur1,前端访问的地址,需要文件的地址,而不是文件名称 ajax.put("newFileName", FileUtils.getName(fileInfo.getUrl())); ajax.put("originalFilename", file.getOriginalFilename()); return ajax; } catch (Exception e) { return AjaxResult.error(e.getMessage()); } }
这里说明一下fileName,若依原本上传图片时会为fileName前面拼接http://localhost/dev-api/profile/upload/。
针对前端图片回显问题,我们之前在前端代码中增加了对http的判断,如果响应的fileName中包含http的话,就不做拼接,直接返回图片路径URL。
5)联调测试,重启后台程序,前端上传图片操作,F12抓包工具查看效果:
二、设备管理
1、需求说明
业务场景:管理员在系统录入设备信息后,员工将负责设备(智能售货机)的投放和商品补货工作。
设备管理主要涉及到三个功能模块,业务流程如下:
- 新增设备类型: 允许管理员定义新的售货机型号,包括其规格和容量。
- 新增设备: 在新的设备类型定义后,系统应允许添加新的售货机实例,并将它们分配到特定的点位。
- 新增货道: 对于每个新添加的设备,系统应支持定义新的货道,后期用于关联相应的商品SKU。
对于设备和其他管理数据,下面是示意图:
- 关系字段:vm_type_id、node_id、vm_id
- 数据字典:vm_status(0未投放、1运营、3撤机)
- 冗余字段:addr、business_type、region_id、partner_id(简化查询接口、提高查询效率)
2、生成基础代码
- 需求:使用若依代码生成器,生成设备类型、设备、货道前后端基础代码,并导入到项目中:
(1)创建目录菜单
创建设备管理目录菜单
(2)添加数据字典
先创建设备状态
的字典类型
再创建设备状态
的字典数据
(3)配置代码生成信息
导入三张表
- 配置设备类型表(参考原型)
- 配置设备表(参考原型)
- 配置货道表(无原型)
(4)下载代码并导入项目
选中三张表生成下载
注意:货道动态菜单sql和前端不需要导入
调整动态菜单的显示顺序(设备状态模块稍后完成)
前端代码导入(由于货道表没有页面原型,因此我们只需要导入它的后端代码,实现业务逻辑的查询即可)
后端代码导入
3、设备类型改造
(1)基础页面
- 需求:参考页面原型,完成基础布局展示改造
设备类型管理列表
新增和修改共用一个表单框
- 代码实现
在vmType/index.vue视图组件中修改
<!-- 查询条件 --> <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px"> <el-form-item label="型号名称" prop="name"> <el-input v-model="queryParams.name" placeholder="请输入型号名称" clearable @keyup.enter="handleQuery"/> </el-form-item> <el-form-item> <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button> <el-button icon="Refresh" @click="resetQuery">重置</el-button> </el-form-item> </el-form> <!-- 列表展示 --> <el-table v-loading="loading" :data="vmTypeList" @selection-change="handleSelectionChange"> <el-table-column type="selection" width="55" align="center" /> <el-table-column label="型号名称" align="center" prop="name" /> <el-table-column label="型号编码" align="center" prop="model" /> <el-table-column label="设备图片" align="center" prop="image" width="100"> <template #default="scope"> <image-preview :src="scope.row.image" :width="50" :height="50"/> </template> </el-table-column> <el-table-column label="货道行" align="center" prop="vmRow" /> <el-table-column label="货道列" align="center" prop="vmCol" /> <el-table-column label="设备容量" align="center" prop="channelMaxCapacity" /> <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <template #default="scope"> <el-button link type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['manage:vmType:edit']">修改</el-button> <el-button link type="primary" @click="handleDelete(scope.row)" v-hasPermi="['manage:vmType:remove']">删除</el-button> </template> </el-table-column> </el-table> <!-- 添加或修改设备类型管理对话框 --> <el-dialog :title="title" v-model="open" width="500px" append-to-body> <el-form ref="vmTypeRef" :model="form" :rules="rules" label-width="80px"> <el-form-item label="型号名称" prop="name"> <el-input v-model="form.name" placeholder="请输入型号名称" /> </el-form-item> <el-form-item label="型号编码" prop="model"> <el-input v-model="form.model" placeholder="请输入型号编码" /> </el-form-item> <el-form-item label="货道数" prop="vmRow"> <el-input-number v-model="form.vmRow" placeholder="请输入" :min="1" :max="10"/>行 <el-input-number v-model="form.vmCol" placeholder="请输入" :min="1" :max="10"/>列 </el-form-item> <el-form-item label="货道容量" prop="channelMaxCapacity"> <el-input-number v-model="form.channelMaxCapacity" placeholder="请输入" :min="1" :max="10"/>个 </el-form-item> <el-form-item label="设备图片" prop="image"> <image-upload v-model="form.image"/> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="submitForm">确 定</el-button> <el-button @click="cancel">取 消</el-button> </div> </template> </el-dialog>
- 测试和修改新增设备类型
- 设备类型管理列表
4、设备管理改造
(1)基础页面
- 需求:参考页面原型,完成基础布局展示改造
设备管理列表
新增设备
设备修改
货道设置
- 代码实现
将设备表中的测试数据更新,保证和点位表中的详细地址是一致的。
update tb_vending_machine set partner_id=2 where id=80; update tb_vending_machine set addr=(select address from tb_node where id = 1) where node_id=1; update tb_vending_machine set addr=(select address from tb_node where id = 2) where node_id=2;
- 优化查询方案
通过页面原型我们可以观察到,列表界面需要查询设备型号(设备类型表tb_vending_machine)、合作商(合作商表tb_partner),新增和修改界面需要回显点位名称(点位表tb_node)、所属区域(区域表tb_region)。如果要保证数据实时性,都需要进行多表查询。而在之前我们针对多表查询
这种业务使用的是MyBatis嵌套查询(将多表查询中的联合查询语句拆成单个表的查询)
、设置数据库冗余字段(空间换时间的单表查询,需要考虑数据同步存储)
两种方案进行查询优化,降低数据库的访问压力。
这里提供第三种简化多表查询的解决方案,就是前端模拟数据字典的套路。因为数据量不大,我们可以在前端预加载设备类型表、点位表、区域表、合作商表,将所有数据存储到各表的List。由于设备表中设有外键vm_type_id和node_id,还存在冗余字段region_id、partner_id,可以配合使用<div v-for>遍历前端预加载好的List 和 <span v-if> 判断各表List的id和外键id是否相同。
在数据列表中使用template模板插槽,从scope作用域获取行对象row的外键id;
在新增或修改的表单中通过form.属性名,获取外键id。
通过预加载这样的方式,我们就能将多表的关联查询,优化成单表查询,减少了笛卡尔积,提高了查询效率。
在vm/index.vue视图组件中修改
<!-- 查询条件 --> <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px"> <el-form-item label="设备编号" prop="innerCode"> <el-input v-model="queryParams.innerCode" placeholder="请输入设备编号" clearable @keyup.enter="handleQuery" /> </el-form-item> <el-form-item> <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button> <el-button icon="Refresh" @click="resetQuery">重置</el-button> </el-form-item> </el-form> <!-- 列表展示 --> <el-table v-loading="loading" :data="vmList" @selection-change="handleSelectionChange"> <el-table-column type="selection" width="55" align="center" /> <el-table-column label="设备编号" align="center" prop="innerCode" /> <el-table-column label="设备型号" align="center" prop="vmTypeId" > <template #default="scope"> <div v-for="item in vmTypeList" :key="item.id"> <span v-if="item.id == scope.row.vmTypeId">{{ item.name }}</span> </div> </template> </el-table-column> <el-table-column label="详细地址" align="center" prop="addr" /> <el-table-column label="合作商" align="center" prop="partnerId" > <template #default="scope"> <div v-for="item in partnerList" :key="item.id"> <span v-if="item.id == scope.row.partnerId">{{ item.partnerName }}</span> </div> </template> </el-table-column> <el-table-column label="设备状态" align="center" prop="vmStatus"> <template #default="scope"> <dict-tag :options="vm_status" :value="scope.row.vmStatus" /> </template> </el-table-column> <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <template #default="scope"> <el-button link type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['manage:vm:edit']">修改</el-button> </template> </el-table-column> </el-table> <!-- 添加或修改设备管理对话框 --> <el-dialog :title="title" v-model="open" width="500px" append-to-body> <el-form ref="vmRef" :model="form" :rules="rules" label-width="80px"> <el-form-item label="设备编号"> <span>{{ form.innerCode == null ? '系统自动生成' : form.innerCode }}</span> </el-form-item> <el-form-item label="供货时间" v-if="form.innerCode != null"> <span>{{ parseTime(form.lastSupplyTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span> </el-form-item> <el-form-item label="设备类型" v-if="form.innerCode != null"> <div v-for="item in vmTypeList" :key="item.id"> <span v-if="form.vmTypeId == item.id">{{ item.name }}</span> </div> </el-form-item> <el-form-item label="设备容量" v-if="form.innerCode != null"> <span>{{ form.channelMaxCapacity }}</span> </el-form-item> <el-form-item label="选择型号" prop="vmTypeId" v-if="form.innerCode == null"> <el-select v-model="form.vmTypeId" placeholder="请选择设备型号" style="width: 100%"> <el-option v-for="item in vmTypeList" :key="item.id" :label="item.name" :value="item.id" /> </el-select> </el-form-item> <el-form-item label="选择点位" prop="nodeId"> <el-select v-model="form.nodeId" placeholder="请选择点位" style="width: 100%"> <el-option v-for="item in nodeList" :key="item.id" :label="item.nodeName" :value="item.id" /> </el-select> </el-form-item> <el-form-item label="合作商" v-if="form.innerCode != null"> <div v-for="item in partnerList" :key="item.id"> <span v-if="form.partnerId == item.id">{{ item.partnerName }}</span> </div> </el-form-item> <el-form-item label="所属区域" v-if="form.innerCode != null"> <div v-for="item in regionList" :key="item.id"> <span v-if="form.regionId == item.id">{{ item.regionName }}</span> </div> </el-form-item> <el-form-item label="设备地址" v-if="form.innerCode != null"> <span>{{ form.addr }}</span> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="submitForm">确 定</el-button> <el-button @click="cancel">取 消</el-button> </div> </template> </el-dialog> <script setup name="Vm"> import { listVmType } from "@/api/manage/vmType"; import { listPartner } from "@/api/manage/partner"; import { loadAllParams } from '@/api/page'; import { listNode } from '@/api/manage/node'; import { listRegion } from "@/api/manage/region"; /* 查询设备类型列表 */ const vmTypeList = ref([]); function getVmTypeList() { listVmType(loadAllParams).then((response) => { vmTypeList.value = response.rows; }); } /* 查询合作商列表 */ const partnerList = ref([]); function getPartnerList() { listPartner(loadAllParams).then((response) => { partnerList.value = response.rows; }); } /* 查询点位列表 */ const nodeList = ref([]); function getNodeList() { listNode(loadAllParams).then((response) => { nodeList.value = response.rows; }); } /* 查询区域列表 */ const regionList = ref([]); function getRegionList() { listRegion(loadAllParams).then((response) => { regionList.value = response.rows; }); } getRegionList(); getPartnerList(); getNodeList(); getVmTypeList(); </script>
(2)新增设备
- 需求:新增设备时,补充设备表其他字段信息,还需要根据售货机类型创建所属货道
我们了解到在新增设备时,添加设备和货道表,还包含点位和设备类型的查询,共涉及到四张表的操作。
这个过程需要我们仔细处理每个字段,对于新增时没有的属性字段,需要补充进去,确保数据的一致性和完整性。
- VendingMachineServiceImpl
@Autowired private IVmTypeService vmTypeService; @Autowired private INodeService nodeService; @Autowired private IChannelService channelService; /** * 新增设备管理 * * @param vendingMachine 设备管理 * @return 结果 */ @Transactional @Override public int insertVendingMachine(VendingMachine vendingMachine) { // 新增设备 // 生成8位的唯一标识,补充货道编号 String innerCode = UUIDUtils.getUUID(); vendingMachine.setInnerCode(innerCode); // 查询售货机类型表,补充设备容量 VmType vmType = vmTypeService.selectVmTypeById(vendingMachine.getVmTypeId()); vendingMachine.setChannelMaxCapacity(vmType.getChannelMaxCapacity()); // 查询点位表,补充区域、点位、合作商等信息 Node node = nodeService.selectNodeById(vendingMachine.getNodeId()); // 将相同属性名称的属性值拷贝到目标对象中 copyProperties(源bean, 目的bean, 拷贝忽略字段) BeanUtil.copyProperties(node, vendingMachine, "id", "createTime", "updateTime"); // 拷贝商圈类型、区域id、合作商id vendingMachine.setAddr(node.getAddress()); // 设备详细地址 // 设备状态 vendingMachine.setVmStatus(DkdContants.VM_STATUS_NODEPLOY); // 0-未投放 vendingMachine.setCreateTime(DateUtils.getNowDate()); vendingMachine.setUpdateTime(DateUtils.getNowDate()); int result = vendingMachineMapper.insertVendingMachine(vendingMachine); // 新增货道 List<Channel> channelList = new ArrayList<>(); for (int i = 1; i <= vmType.getVmRow(); i++) { for (int j = 1; j <= vmType.getVmCol(); j++) { Channel channel = new Channel(); channel.setChannelCode(i + "-" + j); // 货道编号 channel.setVmId(vendingMachine.getId()); // 售货机id channel.setInnerCode(vendingMachine.getInnerCode()); // 售货机编号 channel.setMaxCapacity(vendingMachine.getChannelMaxCapacity()); // 货道最大容量 channel.setCreateTime(DateUtils.getNowDate()); channel.setUpdateTime(DateUtils.getNowDate()); channelList.add(channel); } } channelService.batchInsertChannels(channelList); // 批量保存 return result; }
- ChannelMapper接口和xml
/** * 批量新增售货机货道 * @param channelList * @return 结果 */ public int batchInsertChannel(List<Channel> channelList); <insert id="batchInsertChannel" parameterType="java.util.List"> INSERT INTO tb_channel ( channel_code, vm_id, inner_code, max_capacity, last_supply_time, create_time, update_time ) VALUES <foreach collection="list" item="channel" separator=","> ( #{channel.channelCode}, #{channel.vmId}, #{channel.innerCode}, #{channel.maxCapacity}, #{channel.lastSupplyTime}, #{channel.createTime}, #{channel.updateTime} ) </foreach> </insert>
- IChannelService和ChannelServiceImpl
/** * 批量新增售货机货道 * @param channelList * @return 结果 */ public int batchInsertChannel(List<Channel> channelList); /** * 批量新增售货机货道 * @param channelList * @return 结果 */ @Override public int batchInsertChannel(List<Channel> channelList) { return channelMapper.batchInsertChannel(channelList); }
- 测试新增设备功能
因为饮料机设备类型的货道数是5行6列,所以数据库中同步插入30条货道。
(3)修改设备
- 需求:修改设备时,根据点位同步更新冗余字段信息
根据前端提交的点位ID,后端需要查询点位表,来获取点位的详细信息,包括详细地址、商圈类型、区域ID和合作商ID,获取到点位信息后,我们需要更新设备表中的相关冗余字段。
- VendingMachineServiceImpl
/** * 修改设备管理 * * @param vendingMachine 设备管理 * @return 结果 */ @Override public int updateVendingMachine(VendingMachine vendingMachine) { // 查询点位表,补充:区域、点位、合作商等信息 Node node = nodeService.selectNodeById(vendingMachine.getNodeId()); BeanUtil.copyProperties(node, vendingMachine, "id", "createTime"); // 商圈类型、区域、合作商 vendingMachine.setAddr(node.getAddress()); // 设备地址 vendingMachine.setUpdateTime(DateUtils.getNowDate()); return vendingMachineMapper.updateVendingMachine(vendingMachine); }
(4)设备状态改造
- 页面原型
设备状态列表
查看设备详情
因为设备状态并没有单独创建一张表,只是作为设备表中的一个字段vm_status,所以没有让若依生成设备状态的前端代码。因此我们来学习一下在若依中如何自己定制化开发一个前端页面,并配置响应的菜单路径映射、vue路由等信息。
另外,对于查看设备详情界面展示的信息,都是需要智能售货机投入运营后产生的订单和工单统计的(后期工单管理写完后再进行完善,这里先搭一个基本框架),包括设备状态列表界面中的设备运行状态(running_status),是售货机自己通过物联网MQTT协议进行自我更新上传信息到数据库。
- 创建视图组件
创建vmStatus/index.vue视图组件
- 创建二级菜单
上级菜单:设备管理
菜单名称:设备状态
路由地址:显示在url上的名称(localhost/vm/vmStatus),需要和视图组件产生一个映射关系。
组件路径:设置前端组件的视图页面,也就是manage/vmStatus/index,默认映射views目录不用写,.vue后缀也不用填写。设置完后 路由地址 和 组件路径 就会产生映射关系,实现页面的跳转了。
权限字符:查询的是设备列表,参考查询设备管理列表的权限字符 manage:vm:list
- 改造视图组件
智能售货机运营后自我上传的设备运行状态running_status,存储到数据库是json格式,我们需要用status进行判断。
我们在前端列表展示这个字段需要使用JSON.parse()方法将字符串转为json对象,前端判断如下:
<el-table-column label="设备状态" align="center" prop="runningStatus"> <template #default="scope"> <span v-if="scope.row.runningStatus != null"> {{ JSON.parse(scope.row.runningStatus).status ? "正常" : "异常" }} </span> <span v-else>异常</span> </template> </el-table-column>
完整代码:
<template> <div class="app-container"> <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px"> <el-form-item label="设备编号" prop="innerCode"> <el-input v-model="queryParams.innerCode" placeholder="请输入设备编号" clearable @keyup.enter="handleQuery" /> </el-form-item> <el-form-item> <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button> <el-button icon="Refresh" @click="resetQuery">重置</el-button> </el-form-item> </el-form> <el-table v-loading="loading" :data="vmList" @selection-change="handleSelectionChange"> <el-table-column label="序号" type="index" width="55" align="center" /> <el-table-column label="设备编号" align="center" prop="innerCode" /> <el-table-column label="设备型号" align="center" prop="vmTypeId"> <template #default="scope"> <div v-for="item in vmTypeList" :key="item.id"> <span v-if="item.id == scope.row.vmTypeId">{{ item.name }}</span> </div> </template> </el-table-column> <el-table-column label="详细地址" align="center" prop="addr" show-overflow-tooltip="true"/> <el-table-column label="运营状态" align="center" prop="vmStatus"> <template #default="scope"> <dict-tag :options="vm_status" :value="scope.row.vmStatus"/> </template> </el-table-column> <el-table-column label="设备状态" align="center" prop="runningStatus"> <template #default="scope"> <!-- <span v-if="scope.row.runningStatus != null"> {{ JSON.parse(scope.row.runningStatus).status ? "正常" : "异常" }} </span> <span v-else>异常</span> --> {{ scope.row.runningStatus != null ? JSON.parse(scope.row.runningStatus).status ? "正常" : "异常" : "异常" }} </template> </el-table-column> <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <template #default="scope"> <el-button link type="primary" @click="getVmInfo(scope.row)" v-hasPermi="['manage:vm:query']">查看详情</el-button> </template> </el-table-column> </el-table> <pagination v-show="total>0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" /> <!-- 添加或修改设备管理对话框 --> <el-dialog :title="title" v-model="open" width="500px" append-to-body> <el-row> 销售量:{{ 100 }} 销售额:{{ 500 }} 补货次数:{{ 10 }}次 维修次数:{{ 3 }}次 </el-row> <br/> <el-row> 商品销量(月) </el-row> </el-dialog> </div> </template> <script setup name="Vm"> import { listVm, getVm, delVm, addVm, updateVm } from "@/api/manage/vm"; import { listVmType } from "@/api/manage/vmType"; import { listPartner } from "@/api/manage/partner"; import { listNode } from "@/api/manage/node"; import { listRegion } from '@/api/manage/region'; import { loadAllParams } from "@/api/page"; const { proxy } = getCurrentInstance(); const { vm_status } = proxy.useDict('vm_status'); const vmList = ref([]); const open = ref(false); const loading = ref(true); const showSearch = ref(true); const ids = ref([]); const single = ref(true); const multiple = ref(true); const total = ref(0); const title = ref(""); const data = reactive({ form: {}, queryParams: { pageNum: 1, pageSize: 10, innerCode: null, nodeId: null, businessType: null, regionId: null, partnerId: null, vmTypeId: null, vmStatus: null, runningStatus: null, policyId: null, }, rules: { nodeId: [ { required: true, message: "点位Id不能为空", trigger: "blur" } ], vmTypeId: [ { required: true, message: "设备型号不能为空", trigger: "blur" } ], } }); const { queryParams, form, rules } = toRefs(data); /** 查询设备管理列表 */ function getList() { loading.value = true; listVm(queryParams.value).then(response => { vmList.value = response.rows; total.value = response.total; loading.value = false; }); } // 表单重置 function reset() { form.value = { id: null, innerCode: null, channelMaxCapacity: null, nodeId: null, addr: null, lastSupplyTime: null, businessType: null, regionId: null, partnerId: null, vmTypeId: null, vmStatus: null, runningStatus: null, longitudes: null, latitude: null, clientId: null, policyId: null, createTime: null, updateTime: null }; proxy.resetForm("vmRef"); } /** 搜索按钮操作 */ function handleQuery() { queryParams.value.pageNum = 1; getList(); } /** 重置按钮操作 */ function resetQuery() { proxy.resetForm("queryRef"); handleQuery(); } // 多选框选中数据 function handleSelectionChange(selection) { ids.value = selection.map(item => item.id); single.value = selection.length != 1; multiple.value = !selection.length; } /** 查看设备详情 */ function getVmInfo(row) { reset(); const _id = row.id || ids.value getVm(_id).then(response => { form.value = response.data; open.value = true; title.value = "设备详情"; }); } /* 查询设备类型列表 */ const vmTypeList = ref([]); function getVmTypeList() { listVmType(loadAllParams).then(response => { vmTypeList.value = response.rows; }); } /* 查询合作商列表 */ const partnerList = ref([]); function getPartnerList() { listPartner(loadAllParams).then(response => { partnerList.value = response.rows; }); } /* 查询点位列表 */ const nodeList = ref([]); function getNodeList() { listNode(loadAllParams).then(response => { nodeList.value = response.rows; }); } /* 查询区域列表 */ const regionList = ref([]); function getRegionList() { listRegion(loadAllParams).then(response => { regionList.value = response.rows; }); } getVmTypeList(); getPartnerList(); getNodeList(); getRegionList(); getList(); </script>
- 测试页面效果
(5)点位查看详情
- 需求:点位管理页面中点击查看详情,需要显示当前点位下所有设备信息
- 实现思路:在点位管理的页面中创建查看详情按钮,引入设备的api和设备的数据字典,查询所有设备信息,请求参数中多携带一个nodeId,通过后端动态SQL进行查询。
在node/index.vue视图组件中修改
<el-button link type="primary" @click="getNodeInfo(scope.row)" v-hasPermi="['manage:vm:list']">查看详情</el-button> <!-- 点位详情对话框 --> <el-dialog title="点位详情" v-model="nodeInfoOpen" width="600px" append-to-body> <el-table :data="vmList"> <el-table-column label="序号" type="index" width="80" align="center" prop="id" /> <el-table-column label="设备编号" align="center" prop="innerCode" /> <el-table-column label="设备状态" align="center" prop="vmStatus"> <template #default="scope"> <dict-tag :options="vm_status" :value="scope.row.vmStatus" /> </template> </el-table-column> <el-table-column label="最后一次供货时间" align="center" prop="lastSupplyTime" width="180"> <template #default="scope"> <span>{{ parseTime(scope.row.lastSupplyTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span> </template> </el-table-column> </el-table> </el-dialog> <script setup name="Node"> import { listVm } from "@/api/manage/vm"; import { loadAllParams } from "@/api/page"; /* 引入设备状态数据字典 */ const { vm_status } = proxy.useDict('vm_status'); /* 查看点位详情 */ const nodeInfoOpen = ref(false); const vmList = ref([]); function getNodeInfo(row) { // 根据点位id,查询设备列表 loadAllParams.nodeId = row.id; // 在查询条件loadAllParams中增加一个nodeId参数 listVm(loadAllParams).then(response => { vmList.value = response.rows; nodeInfoOpen.value = true; }); } </script>
三、策略管理
1、需求说明
业务场景:管理员在系统中可以对每一台设备设置一个固定折扣,用于营销作用。
策略管理主要涉及到二个功能模块,业务流程如下:
- 新增策略:允许管理员定义新的策略,包括策略的具体内容和参数(如折扣率)。
- 策略分配:将策略分配给一个或多个售货机。
对于策略和其他管理数据,下面是示意图:
- 关系字段:policy_id
2、生成基础代码
- 需求:使用若依代码生成器,生成策略管理前后端基础代码,并导入到项目中
- 步骤
(1)创建目录菜单
创建策略管理目录菜单
(2)配置代码生成信息
导入并配置策略表(参考原型)
(3)下载代码并导入项目
选中策略表生成下载,解压ruoyi.zip
得到前后端代码和动态菜单sql,完成sql脚本执行和前后端代码导入。导入过程和之前同理。
3、策略管理改造
(1)基础页面
- 需求:参考页面原型,完成基础布局展示改造
- 代码实现
在policy/index.vue视图组件中修改
<!-- 列表展示 --> <el-table v-loading="loading" :data="policyList" @selection-change="handleSelectionChange"> <el-table-column type="selection" width="55" align="center" /> <el-table-column label="序号" type="index" width="50" align="center" prop="policyId" /> <el-table-column label="策略名称" align="center" prop="policyName" /> <el-table-column label="策略方案" align="center" prop="discount" /> <el-table-column label="创建时间" align="center" prop="createTime" width="180"> <template #default="scope"> <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span> </template> </el-table-column> <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <template #default="scope"> <el-button link type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['manage:policy:edit']">修改</el-button> <el-button link type="primary" @click="handleDelete(scope.row)" v-hasPermi="['manage:policy:remove']">删除</el-button> </template> </el-table-column> </el-table> <!-- 添加或修改策略管理对话框 --> <el-dialog :title="title" v-model="open" width="500px" append-to-body> <el-form ref="policyRef" :model="form" :rules="rules" label-width="80px"> <el-form-item label="策略名称" prop="policyName"> <el-input v-model="form.policyName" placeholder="请输入策略名称" /> </el-form-item> <el-form-item label="策略方案" prop="discount"> <el-input-number :min="0" :max="100" :step="5" :precision="0" controls-position="right" v-model="form.discount" placeholder="请输入策略方案" /> % </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="submitForm">确 定</el-button> <el-button @click="cancel">取 消</el-button> </div> </template> </el-dialog>
- 测试新增和修改策略
(2)查看详情
需求:点击查看详情,展示策略名称和该策略下的设备列表
- 代码实现
在policy/index.vue视图组件中修改
<el-button link type="primary" @click="getPolicyInfo(scope.row)" v-hasPermi="['manage:vm:list']">查看详情</el-button> <!-- 策略详情对话框 --> <el-dialog v-model="policyOpen" title="策略详情" width="500px"> <el-form-item label="策略名称" prop="policyName"> <el-input v-model="form.policyName" placeholder="请输入策略名称" disabled /> </el-form-item> <label>包含设备:</label> <el-table :data="vmList"> <el-table-column label="序号" type="index" width="80" align="center" prop="id" /> <el-table-column label="点位地址" align="left" prop="addr" show-overflow-tooltip /> <el-table-column label="设备编号" align="center" prop="innerCode" /> </el-table> </el-dialog> <script setup name="Policy"> import { listVm } from "@/api/manage/vm"; import { loadAllParams } from "@/api/page"; /* 查看策略详情 */ const policyOpen = ref(false); const vmList = ref([]); function getPolicyInfo(row) { // 1. 获取策略信息 form.value = row; // 2. 根据策略id,查询设备列表 loadAllParams.policyId = row.policyId; listVm(loadAllParams).then(response => { vmList.value = response.rows; policyOpen.value = true; }); } </script>
4、设备策略分配
- 需求:在设备管理页面中点击策略,对设备设置一个固定折扣,用于营销作用。
- 设备管理页面原型
- 代码实现
在vm/index.vue视图组件中修改
<el-button link type="primary" @click="handleUpdatePolicy(scope.row)" v-hasPermi="['manage:vm:edit']">策略</el-button> <!-- 策略管理对话框 --> <el-dialog title="策略管理" v-model="policyOpen" width="500px" append-to-body> <el-form ref="vmRef" :model="form" label-width="80px"> <el-form-item label="策略" prop="policyId"> <el-select v-model="form.policyId" placeholder="请选择策略"> <el-option v-for="item in policyList" :key="item.policyId" :label="item.policyName" :value="item.policyId"></el-option> </el-select> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="submitForm">确 定</el-button> <el-button @click="cancel">取 消</el-button> </div> </template> </el-dialog> <script setup name="Vm"> import { listPolicy } from '@/api/manage/policy'; // 取消按钮 function cancel() { open.value = false; policyOpen.value = false; // 关闭策略管理对话框 reset(); } /** 提交按钮 */ function submitForm() { proxy.$refs["vmRef"].validate(valid => { if (valid) { if (form.value.id != null) { updateVm(form.value).then(response => { proxy.$modal.msgSuccess("修改成功"); open.value = false; policyOpen.value = false; // 关闭策略管理对话框 getList(); }); } else { addVm(form.value).then(response => { proxy.$modal.msgSuccess("新增成功"); open.value = false; getList(); }); } } }); } /* 设备策略分配 */ const policyList = ref([]); const policyOpen = ref(false); function handleUpdatePolicy(row) { // 为表单提供设备id和策略id(减少一次后端查询开销) form.value.id = row.id; // 设备id作为更新条件 form.value.policyId = row.policyId; // 策略id作为提交参数 // 查询策略列表 listPolicy(loadAllParams).then((response) => { policyList.value = response.rows; policyOpen.value = true; }); } </script>
- 修改VendingMachineServiceImpl的更新设备方法,加入更新策略的特判。
/** * 修改设备管理 * * @param vendingMachine 设备管理 * @return 结果 */ @Override public int updateVendingMachine(VendingMachine vendingMachine) { // 仅当修改设备信息时做点位的更新,策略管理修改只提交设备id和策略id,不包含点位id,只需更新基本信息。 if (vendingMachine.getNodeId() != null) { // 查询点位表,补充:区域、点位、合作商等信息 Node node = nodeService.selectNodeById(vendingMachine.getNodeId()); BeanUtil.copyProperties(node, vendingMachine, "id", "createTime"); // 商圈类型、区域、合作商 vendingMachine.setAddr(node.getAddress()); // 设备地址 } vendingMachine.setUpdateTime(DateUtils.getNowDate()); return vendingMachineMapper.updateVendingMachine(vendingMachine); }