目录
[6. 设备管理](#6. 设备管理)
[6.1 需求说明](#6.1 需求说明)
[6.2 生成基础代码](#6.2 生成基础代码)
[6.2.1 需求](#6.2.1 需求)
[6.2.2 步骤](#6.2.2 步骤)
[6.3 设备类型改造](#6.3 设备类型改造)
[6.3.1 基础页面](#6.3.1 基础页面)
[6.4 设备管理改造](#6.4 设备管理改造)
[6.4.1 基础页面](#6.4.1 基础页面)
[6.4.2 新增设备](#6.4.2 新增设备)
[6.4.3 修改设备](#6.4.3 修改设备)
[6.5 设备状态改造](#6.5 设备状态改造)
[6.5.1 创建视图组件](#6.5.1 创建视图组件)
[6.5.2 创建二级菜单](#6.5.2 创建二级菜单)
[6.5.3 改造视图组件](#6.5.3 改造视图组件)
[6.6 点位查看详情](#6.6 点位查看详情)
[7.1 需求说明](#7.1 需求说明)
[7.2 生成基础代码](#7.2 生成基础代码)
[7.2.1 需求](#7.2.1 需求)
[7.2.2 步骤](#7.2.2 步骤)
[7.3 策略管理改造](#7.3 策略管理改造)
[7.3.1 基础页面](#7.3.1 基础页面)
[7.3.2 查看详情](#7.3.2 查看详情)
[7.3 设备策略分配](#7.3 设备策略分配)
[7.3.1 需求](#7.3.1 需求)
[7.3.2 代码实现](#7.3.2 代码实现)
[8.1 需求说明](#8.1 需求说明)
[8.2 生成基础代码](#8.2 生成基础代码)
[8.2.1 需求](#8.2.1 需求)
[8.2.2 步骤](#8.2.2 步骤)
[8.3 商品类型改造](#8.3 商品类型改造)
[8.3.1 基础页面](#8.3.1 基础页面)
[8.3.2 代码实现](#8.3.2 代码实现)
[8.4 商品管理改造](#8.4 商品管理改造)
[8.4.1 基础页面](#8.4.1 基础页面)
[8.4.2 商品删除](#8.4.2 商品删除)
[8.5 批量导入](#8.5 批量导入)
[8.5.1 需求](#8.5.1 需求)
[8.5.2 前端](#8.5.2 前端)
[8.5.3 后端](#8.5.3 后端)
[8.6 EasyExcel](#8.6 EasyExcel)
[8.6.1 介绍](#8.6.1 介绍)
[8.6.2 项目集成](#8.6.2 项目集成)
[8.7 货道关联商品](#8.7 货道关联商品)
[8.7.1 需求](#8.7.1 需求)
[8.7.2 货道对话框](#8.7.2 货道对话框)
[8.7.3 查询货道列表](#8.7.3 查询货道列表)
[8.7.4 货道关联商品](#8.7.4 货道关联商品)
[9.1 需求说明](#9.1 需求说明)
[9.2 基础代码生成](#9.2 基础代码生成)
[9.2.1 需求](#9.2.1 需求)
[9.2.2 步骤](#9.2.2 步骤)
[9.2.3 修复bug](#9.2.3 修复bug)
[9.3 查询工单列表](#9.3 查询工单列表)
[9.3.1 需求](#9.3.1 需求)
[9.3.2 TaskVo](#9.3.2 TaskVo)
[9.3.3 TaskMapper](#9.3.3 TaskMapper)
[9.3.4 ITaskService](#9.3.4 ITaskService)
[9.3.5 TaskController](#9.3.5 TaskController)
[9.4 获取运营人员列表](#9.4 获取运营人员列表)
[9.4.1 需求](#9.4.1 需求)
[9.4.2 VendingMachineMapper](#9.4.2 VendingMachineMapper)
[9.4.3 IVendingMachineService](#9.4.3 IVendingMachineService)
[9.4.4 DkdContants](#9.4.4 DkdContants)
[9.4.5 EmpController](#9.4.5 EmpController)
[9.5 获取运维人员列表](#9.5 获取运维人员列表)
[9.5.1 需求](#9.5.1 需求)
[9.5.2 EmpController](#9.5.2 EmpController)
[10.1 Android模拟器](#10.1 Android模拟器)
[10.2 Java后端](#10.2 Java后端)
[10.3 功能测试](#10.3 功能测试)
[10.3.1 投放工单](#10.3.1 投放工单)
[10.3.2 补货工单](#10.3.2 补货工单)
[10.4 源码介绍](#10.4 源码介绍)
[11.1 设备屏幕](#11.1 设备屏幕)
[11.2 Java后端](#11.2 Java后端)
[11.3 功能测试](#11.3 功能测试)
[11.4 支付出货流程](#11.4 支付出货流程)
[11.5 源码介绍](#11.5 源码介绍)
[12.1 业务说明](#12.1 业务说明)
前言
在当今数字化浪潮汹涌的时代,人工智能(AI)与高效开发框架的融合已成为推动软件开发领域不断前进的强大动力。若依框架,作为 Java 语言生态下备受瞩目的后台管理系统快速开发框架,以其模块化设计、前后端分离架构、强大的代码生成器以及全面的安全机制等显著优势,在众多项目中发挥着关键作用。而 AI 技术的蓬勃发展,为解决复杂业务问题、提升系统智能化水平提供了无限可能。本博客聚焦于 AI 与若依框架的实战结合,上篇已初步探索了项目的基础搭建与部分功能实现,实战篇(下)将进一步深入挖掘,带你领略如何利用二者合力,打造出功能更为强大、智能的应用系统,在实际项目开发中披荆斩棘,实现效率与创新的双赢。
6. 设备管理
6.1 需求说明

点位管理主要涉及到三个功能模块,业务流程如下:
-
新增设备类型: 允许管理员定义新的售货机型号,包括其规格和容量。
-
新增设备: 在新的设备类型定义后,系统应允许添加新的售货机实例,并将它们分配到特定的点位。
-
新增货道: 对于每个新添加的设备,系统应支持定义新的货道,后期用于关联相应的商品SKU。
对于设备和其他管理数据,下面是示意图:
-
关系字段:vm_type_id、node_id、vm_id
-
数据字典:vm_status(0未投放、1运营、3撤机)
-
冗余字段:addr、business_type、region_id、partner_id(简化查询接口、提高查询效率)

6.2 生成基础代码
6.2.1 需求
使用若依代码生成器,生成设备类型、设备、货道前后端基础代码,并导入到项目中:

6.2.2 步骤
①创建目录菜单
创建设备管理目录菜单

②添加数据字典
先创建设备状态
的字典类型


再创建设备状态
的字典数据


③配置代码生成信息
导入三张表











④下载代码并导入项目
选中三张表生成下载

解压ruoyi.zip
得到前后端代码和动态菜单sql
注意:货道动态菜单sql和前端不需要导入

调整二级菜单显示顺序(设备状态稍后完成)

6.3 设备类型改造
6.3.1 基础页面
需求
参考页面原型,完成基础布局展示改造

代码实现
在vmType/index.vue视图组件中修改
html
<!-- 查询条件 -->
<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>
6.4 设备管理改造
6.4.1 基础页面
需求
参考页面原型,完成基础布局展示改造

代码实现
刷新设备表数据
sql
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;
html
<!-- 查询条件 -->
<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-input v-model="form.vmTypeId" placeholder="请输入设备型号" /> -->
<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-input v-model="form.nodeId" placeholder="请输入点位Id" /> -->
<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>
6.4.2 新增设备
需求
新增设备时,补充设备表其他字段信息,还需要根据售货机类型创建所属货道


我们了解到在新增设备时,添加设备和货道表,还包含点位和设备类型的查询,共涉及到四张表的操作。 这个过程需要我们仔细处理每个字段,确保数据的一致性和完整性
VendingMachineServiceImpl
java
@Autowired
private INodeService nodeService;
@Autowired
private IVmTypeService vmTypeService;
@Autowired
private IChannelService channelService;
/**
* 新增设备管理
*
* @param vendingMachine 设备管理
* @return 结果
*/
@Transactional
@Override
public int insertVendingMachine(VendingMachine vendingMachine) {
//1. 新增设备
//1-1 生成8位编号,补充货道编号
String innerCode = UUIDUtils.getUUID();
vendingMachine.setInnerCode(innerCode); // 售货机编号
//1-2 查询售货机类型表,补充设备容量
VmType vmType = vmTypeService.selectVmTypeById(vendingMachine.getVmTypeId());
vendingMachine.setChannelMaxCapacity(vmType.getChannelMaxCapacity());
//1-3 查询点位表,补充 区域、点位、合作商等信息
Node node = nodeService.selectNodeById(vendingMachine.getNodeId());
BeanUtil.copyProperties(node, vendingMachine, "id");
vendingMachine.setAddr(node.getAddress());
//1-4 设备状态
vendingMachine.setVmStatus(DkdContants.VM_STATUS_NODEPLOY);// 0-未投放(数据库有默认值,这个不写也不影响)
vendingMachine.setCreateTime(DateUtils.getNowDate());// 创建时间
vendingMachine.setUpdateTime(DateUtils.getNowDate());// 更新时间
//1-5 保存
int result = vendingMachineMapper.insertVendingMachine(vendingMachine);
//2. 新增货道
//2-1 声明货道集合
List<Channel> channelList = new ArrayList<>();
//2-2 双层for循环
for (int i = 1; i <= vmType.getVmRow(); i++) { // 外层行
for (int j = 1; j <= vmType.getVmCol(); j++) {// 内层列
//2-3 封装channel
Channel channel = new Channel();
channel.setChannelCode(i + "-" + j);// 货道编号
channel.setVmId(vendingMachine.getId());// 售货机id
channel.setInnerCode(vendingMachine.getInnerCode());// 售货机编号
channel.setMaxCapacity(vmType.getChannelMaxCapacity());// 货道最大容量
channel.setCreateTime(DateUtils.getNowDate());// 创建时间
channel.setUpdateTime(DateUtils.getNowDate());// 更新时间
channelList.add(channel);
}
}
//2-4 批量新增
channelService.batchInsertChannel(channelList);
return result;
}
ChannelMapper接口和xml
java
/**
* 批量新增售货机货道
* @param channelList
* @return 结果
*/
public int batchInsertChannel(List<Channel> channelList);
sql
<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
java
/**
* 批量新增售货机货道
* @param channelList
* @return 结果
*/
public int batchInsertChannel(List<Channel> channelList);
ChannelServiceImpl
java
/**
* 批量新增售货机货道
* @param channelList
* @return 结果
*/
@Override
public int batchInsertChannel(List<Channel> channelList) {
return channelMapper.batchInsertChannel(channelList);
}
6.4.3 修改设备
需求
修改设备时,根据点位同步更新冗余字段信息

根据前端提交的点位ID,后端需要查询点位表,来获取点位的详细信息,包括详细地址、商圈类型、区域ID和合作商ID,获取到点位信息后,我们需要更新设备表中的相关冗余字段。

VendingMachineServiceImpl
java
/**
* 修改设备管理
*
* @param vendingMachine 设备管理
* @return 结果
*/
@Override
public int updateVendingMachine(VendingMachine vendingMachine) {
//查询点位表,补充 区域、点位、合作商等信息
Node node = nodeService.selectNodeById(vendingMachine.getNodeId());
BeanUtil.copyProperties(node, vendingMachine, "id");// 商圈类型、区域、合作商
vendingMachine.setAddr(node.getAddress());// 设备地址
vendingMachine.setUpdateTime(DateUtils.getNowDate());// 更新时间
return vendingMachineMapper.updateVendingMachine(vendingMachine);
}
6.5 设备状态改造
6.5.1 创建视图组件
创建vmStatus/index.vue视图组件

6.5.2 创建二级菜单


6.5.3 改造视图组件
html
<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="left" 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="vmStatus">
<template #default="scope">
{{ scope.row.runningStatus!=null? JSON.parse(scope.row.runningStatus).status==true?'正常':'异常' :'异常'}}
</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-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{loadAllParams} from "@/api/page";
import{listNode} from "@/api/manage/node";
import{listRegion} from "@/api/manage/region";
import { ref } from "vue";
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 cancel() {
open.value = false;
reset();
}
// 表单重置
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 handleAdd() {
reset();
open.value = true;
title.value = "添加设备管理";
}
/** 修改按钮操作 */
function getVmInfo(row) {
reset();
const _id = row.id || ids.value
getVm(_id).then(response => {
form.value = response.data;
open.value = true;
title.value = "设备详情";
});
}
/** 提交按钮 */
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;
getList();
});
} else {
addVm(form.value).then(response => {
proxy.$modal.msgSuccess("新增成功");
open.value = false;
getList();
});
}
}
});
}
/** 删除按钮操作 */
function handleDelete(row) {
const _ids = row.id || ids.value;
proxy.$modal.confirm('是否确认删除设备管理编号为"' + _ids + '"的数据项?').then(function() {
return delVm(_ids);
}).then(() => {
getList();
proxy.$modal.msgSuccess("删除成功");
}).catch(() => {});
}
/** 导出按钮操作 */
function handleExport() {
proxy.download('manage/vm/export', {
...queryParams.value
}, `vm_${new Date().getTime()}.xlsx`)
}
/* 查询设备类型列表 */
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();
getNodeList();
getPartnerList();
getVmTypeList();
getList();
</script>
6.6 点位查看详情
在node/index.vue视图组件中修改
html
<el-button link type="primary" @click="getNodeInfo(scope.row)" v-hasPermi="['manage:vm:list']">查看详情</el-button>
<!-- 点位详情对话框 -->
<el-dialog title="点位详情" v-model="nodeOpen" 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";
/* 引入设备状态数据字典 */
const { vm_status } = proxy.useDict('vm_status');
/* 查看详情 */
const nodeOpen = ref(false);
const vmList = ref([]);
function getNodeInfo(row) {
// 根据点位id,查询设备列表
loadAllParams.nodeId = row.id;
listVm(loadAllParams).then(response => {
vmList.value = response.rows;
nodeOpen.value = true;
});
}
</script>
7.策略管理
7.1 需求说明
策略管理主要涉及到二个功能模块,业务流程如下:
-
新增策略: 允许管理员定义新的策略,包括策略的具体内容和参数(如折扣率)
-
策略分配: 将策略分配给一个或多个售货机。
对于策略和其他管理数据,下面是示意图:
- 关系字段:policy_id

7.2 生成基础代码
7.2.1 需求
使用若依代码生成器,生成策略管理前后端基础代码,并导入到项目中:

7.2.2 步骤
①创建目录菜单
创建策略管理目录菜单

②配置代码生成信息
导入策略表

配置策略表(参考原型)


③下载代码并导入项目
选中策略表生成下载

解压ruoyi.zip
得到前后端代码和动态菜单sql

7.3 策略管理改造
7.3.1 基础页面
需求
参考页面原型,完成基础布局展示改造

java
<!-- 列表展示 -->
<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="1" :max="100" :precision="0" 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>
7.3.2 查看详情
需求
点击查看详情,展示策略名称和该策略下的设备列表

代码实现
在policy/index.vue视图组件中修改
html
<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>
7.3 设备策略分配
7.3.1 需求
在设备管理页面中点击策略,对设备设置一个固定折扣,用于营销作用

7.3.2 代码实现
在vm/index.vue视图组件中修改
html
<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;
getList();
});
} else {
addVm(form.value).then(response => {
proxy.$modal.msgSuccess("新增成功");
open.value = false;
getList();
});
}
}
});
}
/* 设备策略分配 */
const policyList = ref([]);
const policyOpen = ref(false);
function handleUpdatePolicy(row) {
//1. 为表单赋值设备id和策略id
form.value.id = row.id;
form.value.policyId = row.policyId;
//2. 查询策略列表
listPolicy(loadAllParams).then((response) => {
policyList.value = response.rows;
policyOpen.value = true;
});
}
</script>
在VendingMachineServiceImpl中修改
java
/**
* 修改设备管理
*
* @param vendingMachine 设备管理
* @return 结果
*/
@Override
public int updateVendingMachine(VendingMachine vendingMachine)
{
if (vendingMachine.getNodeId()!=null) {
// 查询点位表,补充:区域、点位、合作商等信息
Node node = nodeService.selectNodeById(vendingMachine.getNodeId());
BeanUtil.copyProperties(node,vendingMachine,"id");// 商圈类型、区域、合作商
vendingMachine.setAddr(node.getAddress());// 设备地址
}
vendingMachine.setUpdateTime(DateUtils.getNowDate());// 更新时间
return vendingMachineMapper.updateVendingMachine(vendingMachine);
}
8.商品管理
8.1 需求说明
点位管理主要涉及到三个功能模块,业务流程如下:
-
新增商品类型: 定义商品的不同分类,如饮料、零食、日用品等。
-
新增商品: 添加新的商品信息,包括名称、规格、价格、类型等。
-
设备货道管理: 将商品与售货机的货道关联,管理每个货道的商品信息。
对于商品和其他管理数据,下面是示意图:
- 关系字段:class_id、sku_id、vm_id

8.2 生成基础代码
8.2.1 需求
使用若依代码生成器,生成商品类型、商品管理前后端基础代码,并导入到项目中:

8.2.2 步骤
①创建目录菜单
创建商品管理目录菜单

②配置代码生成信息
导入二张表

配置商品类型表(参考原型)






③下载代码并导入项目
选中商品表和商品类型表生成下载

解压ruoyi.zip
得到前后端代码和动态菜单sql

代码导入

8.3 商品类型改造
8.3.1 基础页面
需求
参考页面原型,完成基础布局展示改造

8.3.2 代码实现
在skuClass/index.vue视图组件中修改
html
<!-- 列表展示 -->
<el-table v-loading="loading" :data="skuClassList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" width="50" align="center" prop="classId" />
<el-table-column label="商品类型" align="center" prop="className" />
<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:skuClass:edit']">修改</el-button>
<el-button link type="primary" @click="handleDelete(scope.row)" v-hasPermi="['manage:skuClass:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
修改全局异常处理器,添加以下内容

java
/**
* 数据完整性异常
*/
@ExceptionHandler(DataIntegrityViolationException.class)
public AjaxResult handelDataIntegrityViolationException(DataIntegrityViolationException e) {
if (e.getMessage().contains("foreign")) {
return AjaxResult.error("无法删除,有其他数据引用");
}
if(e.getMessage().contains("Duplicate")){
return AjaxResult.error("无法保存,名称已存在");
}
return AjaxResult.error("数据完整性异常,请联系管理员");
}
8.4 商品管理改造
8.4.1 基础页面
需求
参考页面原型,完成基础布局展示改造

代码实现
在sku/index.vue视图组件中修改
html
<!-- 查询条件 -->
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="商品名称" prop="skuName">
<el-input v-model="queryParams.skuName" 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="skuList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" width="50" align="center" prop="skuId" />
<el-table-column label="商品名称" align="center" prop="skuName" />
<el-table-column label="商品图片" align="center" prop="skuImage" width="100">
<template #default="scope">
<image-preview :src="scope.row.skuImage" :width="50" :height="50" />
</template>
</el-table-column>
<el-table-column label="品牌" align="center" prop="brandName" />
<el-table-column label="规格" align="center" prop="unit" />
<el-table-column label="商品价格" align="center" prop="price" >
<template #default="scope">
<el-tag>{{ scope.row.price / 100 }}元</el-tag>
</template>
</el-table-column>
<el-table-column label="商品类型" align="center" prop="classId">
<template #default="scope">
<div v-for="item in skuClassList" :key="item.classI">
<span v-if="item.classId == scope.row.classId">{{ item.className }}</span>
</div>
</template>
</el-table-column>
<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:sku:edit']">修改</el-button>
<el-button link type="primary" @click="handleDelete(scope.row)"
v-hasPermi="['manage:sku:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 添加或修改商品管理对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="skuRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="商品名称" prop="skuName">
<el-input v-model="form.skuName" placeholder="请输入商品名称" />
</el-form-item>
<el-form-item label="品牌" prop="brandName">
<el-input v-model="form.brandName" placeholder="请输入品牌" />
</el-form-item>
<el-form-item label="商品价格" prop="price">
<el-input-number :min="0.01" :max="999.99" :precision="2" :step="0.5" v-model="form.price" placeholder="请输入" />元
</el-form-item>
<el-form-item label="商品类型" prop="classId">
<!-- <el-input v-model="form.classId" placeholder="请输入商品类型Id" /> -->
<el-select v-model="form.classId" placeholder="请选择商品类型">
<el-option
v-for="item in skuClassList"
:key="item.classId"
:label="item.className"
:value="item.classId"
/>
</el-select>
</el-form-item>
<el-form-item label="规格" prop="unit">
<el-input v-model="form.unit" placeholder="请输入规格" />
</el-form-item>
<el-form-item label="商品图片" prop="skuImage">
<image-upload v-model="form.skuImage" />
</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="Sku">
import { listSkuClass } from "@/api/manage/skuClass";
import { loadAllParams } from "@/api/page";
/** 修改按钮操作 */
function handleUpdate(row) {
reset();
const _skuId = row.skuId || ids.value
getSku(_skuId).then(response => {
form.value = response.data;
form.value.price/=100;
open.value = true;
title.value = "修改商品管理";
});
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["skuRef"].validate(valid => {
if (valid) {
// 将价格单位从元转换回分
form.value.price*=100;
if (form.value.skuId != null) {
updateSku(form.value).then(response => {
proxy.$modal.msgSuccess("修改成功");
open.value = false;
getList();
});
} else {
addSku(form.value).then(response => {
proxy.$modal.msgSuccess("新增成功");
open.value = false;
getList();
});
}
}
});
}
/* 查询商品类型列表 */
const skuClassList = ref([]);
function getSkuClassList() {
listSkuClass(loadAllParams).then(response => {
skuClassList.value = response.rows;
});
}
getSkuClassList();
</script>
8.4.2 商品删除
需求
在删除商品时,需要判断此商品是否被售货机的货道关联,如果关联则无法删除
-
物理外键约束:通过在子表中添加一个外键列和约束,该列与父表的主键列相关联,由数据库维护数据的一致性和完整性
-
逻辑外键约束:在不使用数据库外键约束的情况下,通常在应用程序中通过代码来检查和维护数据的一致性和完整性
使用逻辑外键约束的原因:我们在新增售货机货道记录时暂不指定商品,货道表中的SKU_ID有默认值0,而这个值在商品表中并不存在,那么物理外键约束会阻止货道表的插入,因为0并不指向任何有效的商品记录

SkuServiceImpl
java
@Autowired
private IChannelService channelService;
/**
* 批量删除商品管理
*
* @param skuIds 需要删除的商品管理主键
* @return 结果
*/
@Override
public int deleteSkuBySkuIds(Long[] skuIds)
{ //1. 判断商品的id集合是否有关联货道
int count = channelService.countChannelBySkuIds(skuIds);
if(count>0){
throw new ServiceException("此商品被货道关联,无法删除");
}
//2. 没有关联货道才能删除
return skuMapper.deleteSkuBySkuIds(skuIds);
}
IChannelService
java
/**
* 根据商品id集合统计货道数量
* @param skuIds
* @return 统计结果
*/
int countChannelBySkuIds(Long[] skuIds);
ChannelServiceImpl
java
/**
* 根据商品id集合统计货道数量
* @param skuIds
* @return 统计结果
*/
@Override
public int countChannelBySkuIds(Long[] skuIds) {
return channelMapper.countChannelBySkuIds(skuIds);
}
ChannelMapper接口和xml
XML
/**
* 根据商品id集合统计货道数量
* @param skuIds
* @return 统计结果
*/
int countChannelBySkuIds(Long[] skuIds);
XML
<select id="countChannelBySkuIds" resultType="java.lang.Integer">
select count(1) from tb_channel where sku_id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</select>
8.5 批量导入
8.5.1 需求
点击导入数据弹出导入数据弹窗,实现商品的批量导入

8.5.2 前端
在sku/index.vue视图组件中修改
html
<!-- 导入按钮-->
<el-col :span="1.5">
<el-button type="warning" plain icon="Upload" @click="handleExcelImport" v-hasPermi="['manage:sku:add']">导入</el-button>
</el-col>
<!-- 数据导入对话框 -->
<el-dialog title="数据导入" v-model="excelOpen" width="400px" append-to-body>
<el-upload ref="uploadRef" class="upload-demo"
:action="uploadExcelUrl"
:headers="headers"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:before-upload="handleBeforeUpload"
:limit="1"
:auto-upload="false">
<template #trigger>
<el-button type="primary">上传文件</el-button>
</template>
<el-button class="ml-3" type="success" @click="submitUpload">
上传
</el-button>
<template #tip>
<div class="el-upload__tip">
上传文件仅支持,xls/xlsx格式,文件大小不得超过1M
</div>
</template>
</el-upload>
</el-dialog>
<script setup name="Sku">
import { getToken } from "@/utils/auth";
/* 打开数据导入对话框 */
const excelOpen = ref(false);
function handleExcelImport() {
excelOpen.value = true;
}
/* 上传地址 */
const uploadExcelUrl = ref(import.meta.env.VITE_APP_BASE_API + "/manage/sku/import"); // 上传excel文件地址
/* 上传请求头 */
const headers = ref({ Authorization: "Bearer " + getToken() });
/* 上传excel */
const uploadRef = ref({});
function submitUpload() {
uploadRef.value.submit()
}
const props = defineProps({
modelValue: [String, Object, Array],
// 大小限制(MB)
fileSize: {
type: Number,
default: 1,
},
// 文件类型, 例如["xls", "xlsx"]
fileType: {
type: Array,
default: () => ["xls", "xlsx"],
},
});
// 上传前loading加载
function handleBeforeUpload(file) {
let isExcel = false;
if (props.fileType.length) {
let fileExtension = "";
if (file.name.lastIndexOf(".") > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
}
isExcel = props.fileType.some(type => {
if (file.type.indexOf(type) > -1) return true;
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
return false;
});
}
if (!isExcel) {
proxy.$modal.msgError(
`文件格式不正确, 请上传${props.fileType.join("/")}格式文件!`
);
return false;
}
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
if (!isLt) {
proxy.$modal.msgError(`上传excel大小不能超过 ${props.fileSize} MB!`);
return false;
}
}
proxy.$modal.loading("正在上传excel,请稍候...");
}
// 上传失败
function handleUploadError() {
proxy.$modal.msgError("上传excel失败");
uploadRef.value.clearFiles();
proxy.$modal.closeLoading();
}
// 上传成功回调
function handleUploadSuccess(res, file) {
if (res.code === 200) {
proxy.$modal.msgSuccess("上传excel成功");
excelOpen.value = false;
getList();
}else{
proxy.$modal.msgError("res.msg);
}
uploadRef.value.clearFiles();
proxy.$modal.closeLoading();
}
</script>
8.5.3 后端
SkuController
java
/**
* 导入商品管理列表
*/
@PreAuthorize("@ss.hasPermi('manage:sku:add')")
@Log(title = "商品管理", businessType = BusinessType.IMPORT)
@PostMapping("/import")
public AjaxResult excelImport(MultipartFile file) throws Exception {
ExcelUtil<Sku> util = new ExcelUtil<Sku>(Sku.class);
List<Sku> skuList = util.importExcel(file.getInputStream());
return toAjax(skuService.insertSkus(skuList));
}
SKuMapper
java
/**
* 批量新增商品管理
* @param skuList
* @return 结果
*/
public int insertSkus(List<Sku> skuList);
java
<insert id="insertSkus" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="skuId">
insert into tb_sku (sku_name, sku_image, brand_Name, unit, price, class_id)
values
<foreach item="item" index="index" collection="list" separator=",">
(#{item.skuName}, #{item.skuImage}, #{item.brandName}, #{item.unit}, #{item.price}, #{item.classId})
</foreach>
</insert>
ISkuService
java
/**
* 批量新增商品管理
* @param skuList
* @return 结果
*/
public int insertSkus(List<Sku> skuList);
java
/**
* 批量新增商品管理
* @param skuList
* @return 结果
*/
@Override
public int insertSkus(List<Sku> skuList) {
return skuMapper.insertSkus(skuList);
}
8.6 EasyExcel
8.6.1 介绍

官方地址:EasyExcel官方文档 - 基于Java的Excel处理工具 | Easy Excel 官网 Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。 easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便
8.6.2 项目集成
文档地址 1、dkd-common\pom.xml
模块添加整合依赖
XML
<!-- excel处理工具-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>4.0.1</version>
</dependency>
2、在dkd-common\
模块的ExcelUtil.java
新增easyexcel
导出导入方法
java
/**
* 对excel表单默认第一个索引名转换成list(EasyExcel)
*
* @param is 输入流
* @return 转换后集合
*/
public List<T> importEasyExcel(InputStream is) throws Exception
{
return EasyExcel.read(is).head(clazz).sheet().doReadSync();
}
/**
* 对list数据源将其里面的数据导入到excel表单(EasyExcel)
*
* @param list 导出数据集合
* @param sheetName 工作表的名称
* @return 结果
*/
public void exportEasyExcel(HttpServletResponse response, List<T> list, String sheetName)
{
try
{
EasyExcel.write(response.getOutputStream(), clazz).sheet(sheetName).doWrite(list);
}
catch (IOException e)
{
log.error("导出EasyExcel异常{}", e.getMessage());
}
}
3、Sku.java 修改为@ExcelProperty
注解
java
package com.dkd.manage.domain;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.HeadFontStyle;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import com.dkd.common.annotation.Excel;
import com.dkd.common.core.domain.BaseEntity;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
/**
* 商品管理对象 tb_sku
*
* @author itheima
* @date 2024-07-15
*/
@ExcelIgnoreUnannotated// 注解表示在导出Excel时,忽略没有被任何注解标记的字段
@ColumnWidth(16)// 注解用于设置列的宽度
@HeadRowHeight(14)// 注解用于设置表头行的高度
@HeadFontStyle(fontHeightInPoints = 11)// 注解用于设置表头的字体样式
public class Sku extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 主键 */
private Long skuId;
/** 商品名称 */
@Excel(name = "商品名称")
@ExcelProperty("商品名称")
private String skuName;
/** 商品图片 */
@Excel(name = "商品图片")
@ExcelProperty("商品图片")
private String skuImage;
/** 品牌 */
@Excel(name = "品牌")
@ExcelProperty("品牌")
private String brandName;
/** 规格(净含量) */
@Excel(name = "规格(净含量)")
@ExcelProperty("规格(净含量)")
private String unit;
/** 商品价格 */
@Excel(name = "商品价格")
@ExcelProperty("商品价格")
private Long price;
/** 商品类型Id */
@Excel(name = "商品类型Id")
@ExcelProperty("商品类型Id")
private Long classId;
/** 是否打折促销 */
private Integer isDiscount;
// 其他略...
}
4、SkuController.java 改为importEasyExcel
java
/**
* 导入商品管理列表
*/
@PreAuthorize("@ss.hasPermi('manage:sku:add')")
@Log(title = "商品管理", businessType = BusinessType.IMPORT)
@PostMapping("/import")
public AjaxResult excelImport(MultipartFile file) throws Exception {
ExcelUtil<Sku> util = new ExcelUtil<Sku>(Sku.class);
List<Sku> skuList = util.importEasyExcel(file.getInputStream());
return toAjax(skuService.insertSkus(skuList));
}
5、SkuController.java 改为exportEasyExcel
java
/**
* 导出商品管理列表
*/
@PreAuthorize("@ss.hasPermi('manage:sku:export')")
@Log(title = "商品管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, Sku sku) {
List<Sku> list = skuService.selectSkuList(sku);
ExcelUtil<Sku> util = new ExcelUtil<Sku>(Sku.class);
util.exportEasyExcel(response, list, "商品管理数据");
}
8.7 货道关联商品
8.7.1 需求
对智能售货机内部的货道进行商品摆放的管理


8.7.2 货道对话框
① 从资料中复制货道api请求js文件到api/manage目录下

② 从资料中复制货道的视图组件到views/manage/vm目录下

③ 修改设备index.vue
html
<el-button link type="primary" @click="handleGoods(scope.row)" v-hasPermi="['manage:vm:edit']">货道</el-button>
<!-- 货道组件 -->
<ChannelDialog :goodVisible="goodVisible" :goodData="goodData" @handleCloseGood="handleCloseGood"></ChannelDialog>
<!-- end -->
<script setup name="Vm">
// ********************货道********************
// 货道组件
import ChannelDialog from './components/ChannelDialog.vue';
const goodVisible = ref(false); //货道弹层显示隐藏
const goodData = ref({}); //货道信息用来拿取 vmTypeId和innerCode
// 打开货道弹层
const handleGoods = (row) => {
goodVisible.value = true;
goodData.value = row;
};
// 关闭货道弹层
const handleCloseGood = () => {
goodVisible.value = false;
};
// ********************货道end********************
</script>
<style lang="scss" scoped src="./index.scss"></style>
8.7.3 查询货道列表
ChannelVo
java
@Data
public class ChannelVo extends Channel {
// 商品
private Sku sku;
}
ChannelMapper和xml
java
/**
* 根据售货机编号查询货道列表
*
* @param innerCode
* @return ChannelVo集合
*/
List<ChannelVo> selectChannelVoListByInnerCode(String innerCode);
html
<resultMap type="ChannelVo" id="ChannelVoResult">
<result property="id" column="id" />
<result property="channelCode" column="channel_code" />
<result property="skuId" column="sku_id" />
<result property="vmId" column="vm_id" />
<result property="innerCode" column="inner_code" />
<result property="maxCapacity" column="max_capacity" />
<result property="currentCapacity" column="current_capacity" />
<result property="lastSupplyTime" column="last_supply_time" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
<association property="sku" javaType="Sku" column="sku_id" select="com.dkd.manage.mapper.SkuMapper.selectSkuBySkuId"/>
</resultMap>
<select id="selectChannelVoListByInnerCode" resultMap="ChannelVoResult">
<include refid="selectChannelVo"/>
where inner_code = #{innerCode}
</select>
IChannelService接口和实现
java
/**
* 根据售货机编号查询货道列表
*
* @param innerCode
* @return ChannelVo集合
*/
List<ChannelVo> selectChannelVoListByInnerCode(String innerCode);
html
/**
* 根据售货机编号查询货道列表
*
* @param innerCode
* @return ChannelVo集合
*/
@Override
public List<ChannelVo> selectChannelVoListByInnerCode(String innerCode) {
return channelMapper.selectChannelVoListByInnerCode(innerCode);
}
ChannelController
java
/**
* 根据售货机编号查询货道列表
*/
@PreAuthorize("@ss.hasPermi('manage:channel:list')")
@GetMapping("/list/{innerCode}")
public AjaxResult lisetByInnerCode(@PathVariable("innerCode") String innerCode) {
List<ChannelVo> voList = channelService.selectChannelVoListByInnerCode(innerCode);
return success(voList);
}
8.7.4 货道关联商品
ChannelSkuDto
java
// 某个货道对应的sku信息
@Data
public class ChannelSkuDto {
private String innerCode;
private String channelCode;
private Long skuId;
}
ChannelConfigDto
java
// 售货机货道配置
@Data
public class ChannelConfigDto {
// 售货机编号
private String innerCode;
// 货道配置
private List<ChannelSkuDto> channelList;
}
ChannelMapper和xml
java
/**
* 根据售货机编号和货道编号查询货道信息
* @param innerCode
* @param channelCode
* @return 售货机货道
*/
@Select("select * from tb_channel where inner_code =#{innerCode} and channel_code=#{channelCode}")
Channel getChannelInfo(@Param("innerCode") String innerCode, @Param("channelCode") String channelCode);
/**
* 批量修改货道
* @param list
* @return 结果
*/
int batchUpdateChannel(List<Channel> list);
XML
<update id="batchUpdateChannel" parameterType="java.util.List">
<foreach item="channel" collection="list" separator=";">
UPDATE tb_channel
<set>
<if test="channel.channelCode != null and channel.channelCode != ''">channel_code = #{channel.channelCode},</if>
<if test="channel.skuId != null">sku_id = #{channel.skuId},</if>
<if test="channel.vmId != null">vm_id = #{channel.vmId},</if>
<if test="channel.innerCode != null and channel.innerCode != ''">inner_code = #{channel.innerCode},</if>
<if test="channel.maxCapacity != null">max_capacity = #{channel.maxCapacity},</if>
<if test="channel.currentCapacity != null">current_capacity = #{channel.currentCapacity},</if>
<if test="channel.lastSupplyTime != null">last_supply_time = #{channel.lastSupplyTime},</if>
<if test="channel.createTime != null">create_time = #{channel.createTime},</if>
<if test="channel.updateTime != null">update_time = #{channel.updateTime},</if>
</set>
WHERE id = #{channel.id}
</foreach>
</update>
application-druid.yml
允许mybatis框架在单个请求中发送多个sql语句

IChannelService接口和实现
java
/**
* 货道关联商品
* @param channelConfigDto
* @return 结果
*/
int setChannel(ChannelConfigDto channelConfigDto);
java
/**
* 货道关联商品
* @param channelConfigDto
* @return 结果
*/
@Override
public int setChannel(ChannelConfigDto channelConfigDto) {
//1. dto转po
List<Channel> list = channelConfigDto.getChannelList().stream().map(c -> {
// 根据售货机编号和货道编号查询货道
Channel channel = channelMapper.getChannelInfo(c.getInnerCode(), c.getChannelCode());
if (channel != null) {
// 货道更新skuId
channel.setSkuId(c.getSkuId());
// 货道更新时间
channel.setUpdateTime(DateUtils.getNowDate());
}
return channel;
}).collect(Collectors.toList());
//2. 批量修改货道
return channelMapper.batchUpdateChannel(list);
}
ChannelController
java
/**
* 货道关联商品
*/
@PreAuthorize("@ss.hasPermi('manage:channel:edit')")
@Log(title = "售货机货道", businessType = BusinessType.UPDATE)
@PutMapping("/config")
public AjaxResult setChannel(@RequestBody ChannelConfigDto channelConfigDto) {
return toAjax(channelService.setChannel(channelConfigDto));
}
9.工单管理
9.1 需求说明
工单是一种专业名词,是指用于记录、处理、跟踪
一项工作的完成情况。
-
管理人员登录后台系统选择创建工单,在工单类型里选择合适的工单类型,在设备编号里输入正确的设备编号。
-
工作人员在运营管理App可以看到分配给自己的工单,根据实际情况选择接收工单并完成,或者拒绝/取消工单。
立可得工单分为两大类 :
-
运营工单:运营人员来维护售货机
商品
,即补货工单。 -
运维工单:运维人员来维护售货机
设备
,即投放工单、撤机工单、维修工单。
工单有四种状态: 1 待处理 2 进行中 3 已取消 4 已完成

对于工单和其他管理数据,下面是示意图:
-
关系字段:task_id、 product_type_id、inner_code、user_id、assignor_id、region_id
-
数据字典:task_status(1待办、2进行、3取消、4完成)
-
数据字典:create_type(0自动、1手动)
-
PS:运营的工单包含补货信息,运维工单没有,所以运营工单需要单独创建补货工单

创建所有工单,都会在工单表和工单明细表插入记录吗?
创建运维类工单只会在工单表插入数据。
创建运营类工单(补货工单)会在工单表和工单明细表插入数据。
task_code和task_id有什么区别?
task_code是工单编号,具有业务规则 ,格式为年月日+当日序号。
task_id 为工单表数据唯一标识。
工单表的user_id和assignor_id分别是做什么的?
user_id是工单执行人的id(运维或运营)
assignor_id是工单指派人的id(创建工单的人)
9.2 基础代码生成
9.2.1 需求
使用若依代码生成器,生成工管理前后端基础代码,并导入到项目中:

9.2.2 步骤
①创建目录菜单
创建点位管理目录菜单


②添加数据字典
先创建工单状态
的字典类型


再创建工单状态
的字典数据


先创建工单创建类型
的字典类型


再创建工单创建类型
的字典数据


③配置代码生成信息
导入四张表

配置工单表(运维、运营)







创建自动补货任务表(工单原型)


④下载代码并导入项目
选中四张表生成下载

解压ruoyi.zip
得到前后端代码和动态菜单sql
注意:工单管理只需要后端代码,前端使用资料中的

代码导入

⑤配置工单前端代码
1)从资料中复制工单api请求js文件到api/manage目录下

4)从资料中复制货道的视图组件到views/manage目录下

3)创建运营工单二级菜单

4)创建运维工单二级菜单

9.2.3 修复bug
在工单表中,有一个desc
备注字段,这个desc
是一个数据库关键字,所以我们在执行查询时,报了语法错误所以要给所有的desc
增加反引号表示一个普通的sql字段
XML
<sql id="selectTaskVo">
select task_id, task_code, task_status, create_type, inner_code, user_id, user_name, region_id, `desc`, product_type_id, assignor_id, addr, create_time, update_time from tb_task
</sql>
9.3 查询工单列表
9.3.1 需求
运营和运营工单共享一套后端接口,通过特定的查询条件区分工单类型,并在返回结果中包含工单类型的详细信息


9.3.2 TaskVo
java
@Data
public class TaskVo extends Task {
// 工单类型
private TaskType taskType;
}
9.3.3 TaskMapper
java
/**
* 查询运维工单列表
*
* @param task 运维工单
* @return TaskVo集合
*/
List<TaskVo> selectTaskVoList(Task task);
XML
<resultMap type="taskVo" id="TaskVoResult">
<result property="taskId" column="task_id"/>
<result property="taskCode" column="task_code"/>
<result property="taskStatus" column="task_status"/>
<result property="createType" column="create_type"/>
<result property="innerCode" column="inner_code"/>
<result property="userId" column="user_id"/>
<result property="userName" column="user_name"/>
<result property="regionId" column="region_id"/>
<result property="desc" column="desc"/>
<result property="productTypeId" column="product_type_id"/>
<result property="assignorId" column="assignor_id"/>
<result property="addr" column="addr"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
<association property="taskType" javaType="TaskType" column="product_type_id"
select="com.dkd.manage.mapper.TaskTypeMapper.selectTaskTypeByTypeId"/>
</resultMap>
<select id="selectTaskVoList" resultMap="TaskVoResult">
<include refid="selectTaskVo"/>
<where>
<if test="taskCode != null and taskCode != ''">and task_code = #{taskCode}</if>
<if test="taskStatus != null ">and task_status = #{taskStatus}</if>
<if test="createType != null ">and create_type = #{createType}</if>
<if test="innerCode != null and innerCode != ''">and inner_code = #{innerCode}</if>
<if test="userId != null ">and user_id = #{userId}</if>
<if test="userName != null and userName != ''">and user_name like concat('%', #{userName}, '%')</if>
<if test="regionId != null ">and region_id = #{regionId}</if>
<if test="desc != null and desc != ''">and `desc` = #{desc}</if>
<if test="productTypeId != null ">and product_type_id = #{productTypeId}</if>
<if test="assignorId != null ">and assignor_id = #{assignorId}</if>
<if test="addr != null and addr != ''">and addr = #{addr}</if>
<if test="params.isRepair != null and params.isRepair=='true'">
and product_type_id in (1,3,4)
</if>
<if test="params.isRepair != null and params.isRepair=='false'">
and product_type_id =2
</if>
order by create_time desc
</where>
</select>
9.3.4 ITaskService
java
/**
* 查询运维工单列表
* @param task
* @return TaskVo集合
*/
List<TaskVo> selectTaskVoList(Task task);
java
/**
* 查询运维工单列表
* @param task
* @return TaskVo集合
*/
@Override
public List<TaskVo> selectTaskVoList(Task task) {
return taskMapper.selectTaskVoList(task);
}
9.3.5 TaskController
java
/**
* 查询工单列表
*/
@PreAuthorize("@ss.hasPermi('manage:task:list')")
@GetMapping("/list")
public TableDataInfo list(Task task)
{
startPage();
List<TaskVo> voList = taskService.selectTaskVoList(task);
return getDataTable(voList);
}
9.4 获取运营人员列表
9.4.1 需求
根据售货机编号获取负责当前区域下的运营人员列表

9.4.2 VendingMachineMapper
java
/**
* 根据设备编号查询设备信息
*
* @param innerCode
* @return VendingMachine
*/
@Select("select * from tb_vending_machine where inner_code=#{innerCode}")
VendingMachine selectVendingMachineByInnerCode(String innerCode);
9.4.3 IVendingMachineService
java
/**
* 根据设备编号查询设备信息
*
* @param innerCode
* @return VendingMachine
*/
VendingMachine selectVendingMachineByInnerCode(String innerCode);
java
/**
* 根据设备编号查询设备信息
*
* @param innerCode
* @return VendingMachine
*/
@Override
public VendingMachine selectVendingMachineByInnerCode(String innerCode) {
return vendingMachineMapper.selectVendingMachineByInnerCode(innerCode);
}
9.4.4 DkdContants
java
/**
* 员工启用
*/
public static final Long EMP_STATUS_NORMAL = 1L;
/**
* 员工禁用
*/
public static final Long EMP_STATUS_DISABLE = 0L;
9.4.5 EmpController
java
@Autowired
private IVendingMachineService vendingMachineService;
/**
* 根据售货机获取运营人员列表
*/
@PreAuthorize("@ss.hasPermi('manage:emp:list')")
@GetMapping("/businessList/{innerCode}")
public AjaxResult businessList(@PathVariable("innerCode") String innerCode) {
// 1.查询售货机信息
VendingMachine vm = vendingMachineService.selectVendingMachineByInnerCode(innerCode);
if (vm == null) {
return error();
}
// 2.根据区域id、角色编号、员工状态查询运营人员列表
Emp empParam = new Emp();
empParam.setRegionId(vm.getRegionId());// 设备所属区域
empParam.setStatus(DkdContants.EMP_STATUS_NORMAL);// 员工启用
empParam.setRoleCode(DkdContants.ROLE_CODE_BUSINESS);// 角色编码:运营员
return success(empService.selectEmpList(empParam));
}
9.5 获取运维人员列表
9.5.1 需求
根据售货机编号获取负责当前区域下的运维人员列表

9.5.2 EmpController
java
/**
* 根据售货机获取运维人员列表
*/
@PreAuthorize("@ss.hasPermi('manage:emp:list')")
@GetMapping("/operationList/{innerCode}")
public AjaxResult getOperationList(@PathVariable("innerCode") String innerCode) {
// 1.查询售货机信息
VendingMachine vm = vendingMachineService.selectVendingMachineByInnerCode(innerCode);
if (vm == null) {
return error("售货机不存在");
}
// 2.根据区域id、角色编号、状态查询运维人员列表
Emp empParam = new Emp();
empParam.setRegionId(vm.getRegionId());// 设备所属区域
empParam.setStatus(DkdContants.EMP_STATUS_NORMAL);// 员工启用
empParam.setRoleCode(DkdContants.ROLE_CODE_OPERATOR);// 角色编码:维修员
return success(empService.selectEmpList(empParam));
}
10.运营管理App
10.1 Android模拟器
本项目的App客户端部分已经由前端团队进行开发完成,并且以apk的方式提供出来,供我们测试使用,如果要运行apk,需要先安装安卓的模拟器。 可以选择国内的安卓模拟器产品,比如:网易mumu、雷电、夜神等。课程中使用网易mumu模拟器,官网地址:http://mumu.172.com/。 资料中提供了mumu安装包,大家安装到非中文路径即可。



10.2 Java后端
本项目运营管理App的java后端已开发完成,在资料中已提供源码,导入idea中即可

本项目连接的也是dkd数据库,如果密码不是root可以进行修改

10.3 功能测试
10.3.1 投放工单
帝可得管理端,创建新设备

帝可得管理端,创建投放工单

运营管理App端登录负责此工单员工,即可查看待办工单,可以选择拒绝、接受

如果点击接受,帝可得管理端工单状态改为进行

在进行工单界面,可以点击查看详情,选择取消、完成

10.3.2 补货工单
帝可得管理端,货道关联商品(大家练习时,可以全部关联)

帝可得管理端,创建补货工单


运营管理App端登录负责此工单员工,即可查看待办工单,可以选择拒绝、接受

如果点击接受,帝可得管理端工单状态改为进行

在进行工单界面,可以点击查看详情,选择取消、完成

如果点击完成工单,帝可得管理端工单状态改为完成


10.4 源码介绍
运营管理App的java后端技术栈:SpringBoot+MybatisPlus+阿里云短信
11.设备屏幕端
商品列表--选择支付方式--显示支付二维码--用户扫码完成支付

11.1 设备屏幕
本项目的设备屏幕客户端部分已经由前端团队进行开发完成,在资料中已提供源码,双击打开index.html
即可

11.2 Java后端
本项目设备屏幕端的java后端已开发完成,在资料中已提供源码,导入idea中即可

11.3 功能测试
在设备屏幕端加上innerCode=设备编号
,即可显示当前设备货道信息

11.4 支付出货流程
我们能够从屏幕上看到支付二维码,其实是经历了"长途跋涉" ,屏幕端实际上是一个H5页面,向后端发起支付请求,订单服务首先会创建订单,然后调用第三方支付来获得用于生成支付二维码的链接。 然后订单微服务将二维码链接返回给屏幕端,屏幕端生成二维码图片展示。

用户看到二维码后,拿出手机扫码支付,此时第三方支付平台确认用户支付成功后会回调订单服务。订单服务收到回调信息后修改订单状态,并通知设备发货

11.5 源码介绍
设备屏幕端的java后端技术栈:SpringBoot+MybatisPlus

12.订单管理(作业)
12.1 业务说明
用户在设备屏幕端下单后,系统将自动创建订单数据。后台管理人员可以查看这些订单信息,进行后续的管理操作。


我将跟大家分享一下实现订单管理功能的基本思路:
首先,根据原型和数据库表结构来生成订单管理的基础代码。
在生成代码的过程中,考虑是否需要引入数据字典来优化数据管理。
接下来,完成基础页面的改造,以适应订单管理的特定需求。
前端页面包含了日期选择范围的查询功能,这部分知识我们尚未讲解。
不过没关系,小伙们可以参考代码生成页面的日期搜索框,以及后端代码来实现此功能。
结尾
通过这一系列关于 AI + 若依框架的实战探索,我们深入体验到了两者融合所释放出的巨大能量。从项目的一步步搭建,到各个功能模块借助 AI 技术实现智能化升级,每一个环节都凝聚着创新与实践的成果。在这个过程中,我们不仅掌握了若依框架高效开发的精髓,更学会了如何巧妙运用 AI 工具,为项目注入智能的灵魂。展望未来,随着技术的不断进步,AI 与若依框架的结合必将拥有更广阔的应用前景。无论是在优化现有系统的用户体验,还是拓展全新的业务场景方面,都大有可为。希望读者们能从这篇博客中汲取灵感,在自己的项目实践中充分发挥 AI + 若依框架的优势,创造出更多优秀的软件作品 。