【若依RuoYi-Vue | 项目实战】帝可得后台管理系统(二)

文章目录

  • 一、人员管理
    • 1、需求说明
    • 2、生成基础代码
      • (1)创建目录菜单
      • (2)添加数据字典
      • (3)配置代码生成信息
      • (4)下载代码并导入项目
    • 3、人员列表改造
      • (1)基础页面
      • (2)同步存储
    • 4、文件存储
      • (1)本地存储
      • (2)阿里云OSS
      • (3)x-file-storage
  • 二、设备管理
    • 1、需求说明
    • 2、生成基础代码
      • (1)创建目录菜单
      • (2)添加数据字典
      • (3)配置代码生成信息
      • (4)下载代码并导入项目
    • 3、设备类型改造
      • (1)基础页面
    • 4、设备管理改造
      • (1)基础页面
      • (2)新增设备
      • (3)修改设备
      • (4)设备状态改造
      • (5)点位查看详情
  • 三、策略管理
    • 1、需求说明
    • 2、生成基础代码
      • (1)创建目录菜单
      • (2)配置代码生成信息
      • (3)下载代码并导入项目
    • 3、策略管理改造
      • (1)基础页面
      • (2)查看详情
    • 4、设备策略分配


接着上回的【若依RuoYi-Vue | 项目实战】基于若依的帝可得后台管理系统(一),本次我们继续完成人员管理、设备管理、策略管理模块的开发。

页面原型参考:帝可得(智能货柜)页面原型

一、人员管理

1、需求说明

人员管理业务流程如下:

  1. 登录系统: 首先,后台管理人员需要登录到帝可得后台管理系统中。
  2. 新增工作人员: 登录系统后,管理人员可以新增工作人员,包括姓名、联系方式等信息。
  3. 关联角色: 确定此员工是运维人员还是运营人员,这将影响他们的职责和权限。
  4. 关联区域: 确定员工负责的区域范围,确保工作人员能够高效地完成区域内的设备安装、维修、商品补货等工作。

对于人员和其他管理数据,下面是示意图:

  • 关系字段: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和视图组件不需要导入

[外链图片转存中...(img-42tIO2Uq-1726906936486)]

前后端代码导入

[外链图片转存中...(img-FssZQIT2-1726906936486)]

对于无页面原型的角色表,需要导入所生成的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具体使用教程请参考:【服务集成】最新版 | 阿里云OSS对象存储服务使用教程(包含OSS工具类优化、自定义阿里云OSS服务starter)

(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>
  1. 在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-webdomain: https://oss-bucket-web.oss-cn-beijing.aliyuncs.com/ # 访问域名,注意“/”结尾,例如:https://abc.oss-cn-shanghai.aliyuncs.com/base-path: dkd/dkd-images/ # 基础路径
  1. 在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("(♥◠‿◠)ノ゙  帝可得启动成功   ლ(´ڡ`ლ)゙");}
}
  1. 修改若依默认上传图片代码

若依前端上传图片发送的请求路径为:http://localhost/dev-api/common/upload,路由转发到后端就是http://localhost:8080/common/upload

找到 ruoyi-admin 模块中的com.ruoyi.web.controller.common.CommonController类,修改单个文件上传的方法,将本地文件上传的代码改为由x-file-storage管理的阿里云OSS文件上传:

@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。

在这里插入图片描述

  1. 联调测试,重启后台程序,前端上传图片操作,F12抓包工具查看效果:

成功将项目中图片上传从本地存储改为了阿里云OSS文件存储。


二、设备管理

1、需求说明

业务场景:管理员在系统录入设备信息后,员工将负责设备(智能售货机)的投放和商品补货工作。

设备管理主要涉及到三个功能模块,业务流程如下:

  1. 新增设备类型: 允许管理员定义新的售货机型号,包括其规格和容量。
  2. 新增设备: 在新的设备类型定义后,系统应允许添加新的售货机实例,并将它们分配到特定的点位。
  3. 新增货道: 对于每个新添加的设备,系统应支持定义新的货道,后期用于关联相应的商品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"/>&nbsp;&nbsp;<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,可以配合使用

遍历前端预加载好的List 和 判断各表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、合作商idvendingMachine.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());  // 售货机idchannel.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上的名称(http://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-inputv-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><paginationv-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 }}&nbsp;&nbsp;销售额:{{ 500 }}&nbsp;&nbsp;补货次数:{{ 10 }}次&nbsp;&nbsp;维修次数:{{ 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.valuegetVm(_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、需求说明

业务场景:管理员在系统中可以对每一台设备设置一个固定折扣,用于营销作用。

策略管理主要涉及到二个功能模块,业务流程如下:

  1. 新增策略:允许管理员定义新的策略,包括策略的具体内容和参数(如折扣率)。
  2. 策略分配:将策略分配给一个或多个售货机。

在这里插入图片描述

对于策略和其他管理数据,下面是示意图:

  • 关系字段: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="请输入策略方案" /> &nbsp;%</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);
}

到此,我们基于若依完成了帝可得的人员管理、设备管理、策略管理模块的功能开发与测试,并使用阿里云OSS服务配合x-file-storage改造了若依原有的文件上传功能。

下期我们将完成商品管理、工单管理、订单管理模块的开发,并使用运营管理App测试投放工单和补货工单,在设备屏幕端完成下单、支付出货流程。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1541994.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

最新免费域名申请

在互联网时代&#xff0c;每个码农都想拥有一个免费的域名&#xff0c;方便开发调试&#xff0c;也可用作自己网站等。如何申请一个免费的域名&#xff0c;时间上先错过了freenom&#xff0c;后面又错过nic.eu.org申请(现在申请时间长且很难通过)&#xff0c;直到最近又有免费的…

计算机网络(八) —— Udp协议

目录 一&#xff0c;再谈端口号 1.1 端口号 1.2 netsta命令 二&#xff0c;UDP协议 2.1 关于UDP 2.2 Udp协议格式 2.3 Udp协议特点 2.4 Udp的缓冲区 一&#xff0c;再谈端口号 http协议本质是“请求 - 响应”形式的协议&#xff0c;但是应用层需要先将数据交给传输层&…

pthread_cond_signal 和pthread_cond_wait

0、pthread_join()函数作用&#xff1a; pthread_join() 函数会一直阻塞调用它的线程&#xff0c;直至目标线程执行结束&#xff08;接收到目标线程的返回值&#xff09;&#xff0c;阻塞状态才会解除。如果 pthread_join() 函数成功等到了目标线程执行结束&#xff08;成功获取…

(Python) Structured Streaming读取Kafka源实时处理图像

Producer.py import cv2 from kafka import KafkaProducer import os import os.path as osp# Kafka 服务器地址 bootstrap_servers [xxx.xxx.xxx.xxx:9092] #terminal运行ifconfig可以查看localhost# 创建 Kafka 生产者 producer KafkaProducer(bootstrap_serversbootstrap…

什么是 GPT?通过图形化的方式来理解 Transformer 架构

Predict, sample, repeat 预测、取样、重复 GPT 是 Generative Pre-trained Transformer 的缩写。首个单词较为直接&#xff0c;它们是用来生成新文本的机器人。“Pre-trained” 指的是模型经历了从大量数据中学习的过程&#xff0c;这个词暗示了该模型还有进一步在特定任务中…

开关柜设备红外检测数据集

开关柜设备红外检测数据集 包含以下2个数据文件&#xff1a; /train&#xff1a;训练集 /valid&#xff1a;验证集 /test&#xff1a;测试集 README.txt&#xff1a;数据说明 【数据说明】检测目标以Pascal VOC格式进行标注&#xff0c;对每个图像进行以下预处理&#xff0c;统…

极度精简 Winows11 系统镜像!Tiny11 2311下载 - 支持苹果 M 芯片 Mac 安装 (ARM 精简版)!

最新推出的 Tiny11 是一款极端精简版 Windows 11 系统镜像&#xff0c;针对苹果 M 芯片 Mac 用户&#xff08;ARM 架构&#xff09;提供良好支持。Tiny11 内置了众多优化特性&#xff0c;如更小的安装体积和更快的启动速度&#xff0c;特别适合有特殊需求或老机型的用户。用户可…

华为HarmonyOS地图服务 7- 在地图上绘制标记

场景介绍 本章节将向您介绍如何在地图的指定位置添加标记以标识位置、商家、建筑等。 点标记用来在地图上标记任何位置,例如用户位置、车辆位置、店铺位置等一切带有位置属性的事物。Map Kit提供的点标记功能(又称 Marker)封装了大量的触发事件,例如点击事件、长按事件、…

基于YOLO算法的网球运动实时分析-击球速度测量-击球次数(附源码)

这个项目通过分析视频中的网球运动员来测量他们的速度、击球速度以及击球次数。该项目使用YOLO&#xff08;You Only Look Once&#xff09;算法来检测球员和网球&#xff0c;并利用卷积神经网络&#xff08;CNNs&#xff09;来提取球场的关键点。此实战项目非常适合提升您的机…

VsCode C语言 SDL包配置 2024.9

写这篇文章的起因是&#xff0c;最近我需要使用 SDL 包&#xff0c;我懒得下载V-studio ,所以直接在VsCode 里配置C环境。我搞了好几个小时&#xff0c;啥都弄好了&#xff0c;但是一直被下面几个问题缠绕导致demo启动不了&#xff0c;现在我记录一下这奇葩的解决过程。所有路径…

Qt Debugging帮助文档

Qt中给断点添加条件&#xff1a; 示例1&#xff1a; 当i10时&#xff0c;程序中断 但不知道为什么&#xff0c;46行的条件没有生效&#xff0c;47行的条件生效了 给断点添加忽略次数&#xff1a; 在程序停止之前忽略该断点200次。 Breakpoints (Debugging with GDB)

Apache Doris 实践

Apache Doris 实践 官方使用指南&#xff1a;https://doris.incubator.apache.org/zh-CN/docs/install/source-install/compilation-with-docker/ 手动安装 下载二进制安装包https://apache-doris-releases.oss-accelerate.aliyuncs.com/apache-doris-2.1.5-bin-x64.tar.gz …

华润电力最新校招社招润择认知能力测评:逻辑推理数字计算语言理解高分攻略

​ 尊敬的求职者们&#xff0c; 在您准备加入华润电力这个大家庭之前&#xff0c;了解其招聘测评的详细流程和要求是至关重要的。以下是我们为您整理的测评系统核心内容&#xff0c;希望对您的求职之旅有所帮助。 测评系统概览 华润电力的招聘测评系统旨在全面评估求职者的认…

机器学习04-逻辑回归(python)-02原理与损失函数

1. 逻辑回归概念 逻辑回归&#xff08;Logistic Regression&#xff09; 是一种 分类模型&#xff0c;主要用于解决 二分类问题&#xff08;即分成两类&#xff0c;如是否通过、是否患病等&#xff09;。逻辑回归的目标是根据输入的特征预测一个 概率&#xff0c;这个概率值介于…

计算机毕业设计hadoop+spark+hive新能源汽车销售数据分析系统 二手车销量分析 新能源汽车推荐系统 可视化大屏 汽车爬虫 机器学习

《HadoopSparkHive新能源汽车销售数据分析系统》开题报告 一、选题背景与意义 1.1 选题背景 随着全球对环境保护意识的增强和能源结构的转型&#xff0c;新能源汽车市场迅速崛起。新能源汽车的销售数据不仅反映了市场趋势和消费者偏好&#xff0c;还为企业决策、政府监管和政…

微服务——网关登录校验(一)

1.网关登录校验 微服务中的网关登录校验是微服务架构中常见的一种安全机制&#xff0c;用于在请求到达微服务之前&#xff0c;对用户的身份进行验证&#xff0c;确保只有合法的用户才能访问相应的服务。 在微服务架构中&#xff0c;每个微服务都是独立部署的&#xff0c;它们之…

(C++17) optional 的 3 种用法

文章目录 *️⃣前言*️⃣3 种主流用法1️⃣函数返回值2️⃣函数参数3️⃣类成员 ⭐END&#x1f31f;跋&#x1f31f;交流方式 *️⃣前言 在 C17 中标准化了 std::optional。该类型可以容纳一种类型&#xff0c;且判断是否有无。 若使用的标准在低于 C17 则可以使用 Abseil 的…

浅谈递推法

递推法 递推法是一种数学方法&#xff0c;用于通过利用已知的初始条件和递推关系来计算要求中的每一项。以数列来举例&#xff0c;在递推法中&#xff0c;它的思想很简单&#xff1a;我们首先知道数列的第一项&#xff08;初始条件&#xff09;&#xff0c;然后通过一个规律&a…

GEE 数据集:人类造成的热带潮湿森林退化程度的估计

目录 简介 摘要 代码 结论 数据和代码 引用 网址推荐 0代码在线构建地图应用 机器学习 人类造成的热带潮湿森林退化程度超出了先前的估计 简介 选择性采伐、火灾和边缘效应造成的热带森林退化是碳和生物多样性损失的主要驱动因素1,2,3,其年增长率可与森林砍伐相媲美…

Golang | Leetcode Golang题解之第424题替换后的最长重复字符

题目&#xff1a; 题解&#xff1a; func characterReplacement(s string, k int) int {cnt : [26]int{}maxCnt, left : 0, 0for right, ch : range s {cnt[ch-A]maxCnt max(maxCnt, cnt[ch-A])if right-left1-maxCnt > k {cnt[s[left]-A]--left}}return len(s) - left }f…