提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
项目介绍
基于上个章节的学习,这一节做一个基于物联网 概念下的智能售货机运营管理系统帝可得

一个区域存在多个点位 一个点位又可以存放多个售货机 一个售货机又具有多个货道,每个货道可以存放一类商品

本次课程通过若依完成帝可得系统后台,管理员角色主要功能



管理员通过此平台进行添加点位(包含所在区域 商圈类型 合作商 点位详情地址等),关联运营和运维人员
新建售货机,进行型号选择和点位选择,工单管理---创建运维工单(运维人员负责投放/维修)/运营工单(运营人员负责补货等操作会直接关联数据库)

基于若以框架,完成每个模块的基本代码,再根据页面原型进行业务改造,完成后台管理得核心功能。

数据库表结构设计

一、初始
1.初始AI
1.AICG

AI:人工智能,是指通过计算机系统模拟人类思维和行为得一种技术,通过机器学习、深度学习等算法,是计算机具备对数据分析、理解、推理和决策的能力。"把AI当作一个人"

2.提示工程
提示(Prompt):就是我们对大模型提出的问题

- 提示工程也被称为上下文提示
- 设计和优化输入文本来引导AI模型生成预期的输出



2.搭建项目

工程高亮且加粗表示项目搭建完毕,如果没有高亮且加粗,点击maven-parent模块进行clean再进行package打包。

更改配置文件数据库连接和redis配置,创建dkd数据库,导入sql脚本。运行DKDApplication启动项目

npm install
npm run dev

二、库表设计
设置点位表:一个区域下可以添加多个点位,一对多,所以需要添加区域外键 ,商圈类型可以通过若依的数据字典进行设置,一个合作商对应多个点位,同样是一对多关系,添加合作商外键。
若依提供的基类BaseEntity,通过代码生成器生成的业务实体类会默认继承这个类,并且拥有这些公共属性
高效生成 方便管理,规范 标准化


利用ai工具生成建表sql语句
xml
你是一位软件工程师,帮我生成MySQL的表结构
需求如下:
1,区域表,表名tb_region,字段有主键id、区域名称
2,合作商表,表名tb_partner,字段有主键id、合作商名称、联系人、联系电话、分成比例(int类型)、账号、密码
3,点位表,表名tb_node,字段有主键id、点位名称、详细地址、商圈类型(int类型)
其他要求:
1,每张表中都有创建时间(create_time)、修改时间(date_time)、创建人(create_by)、修改人(update_by)、备注(remark)这些字段
2,每张表的主键都是自增的
3,区域与点位是一对多的关系,合作商与点位是一对多的关系,请用字段表示出来,并建立外键约束
4,请为所有字段都添加上comment
5,帮我给生成的表中插入一些北京城市相关区域、点位、合作商的测试数据

三、点位管理代码开发
1.需求说明


2.生成基础代码



注意业务代码放在manager包下,注意更改包路径



3.区域管理改造
1.基础页面
前端代码增删,具体看官方笔记
2.区域列表
需求
在区域列表查询中,需要显示每个区域的点位数

实现思路
实现此功能也有多种方案:
(1)同步存储:在区域表中有点位数的字段,当点位发生变化时,同步区域表中的点位数。
- 优点:由于是单表查询操作,查询列表效率最高。
- 缺点:需要在点位增删改时修改区域表中的数据,有额外的开销,数据也可能不一致。
(2)关联查询:编写关联查询语句,在mapper 层封装。 - 优点:实时查询,数据100%正确,不需要单独维护。
- 缺点:SQL语句较复杂,如果数据量大,性能比较低。
区域和点位表,记录个数都不是很多,所以我们采用关联查询这种方案。

sql
-- 传统模式
-- 1.先聚合统计每个区域下的点位数
-- 确定查询表 tb_node
-- 确定分组字段 region_id
select region_id,count(*) as node_count from tb_node group by region_id;
-- 2.然后与区域表进行关联查询
select r.id,r.region_name,r.remark,ifnull(n.node_count,0) as node_count from tb_region r
left join (select region_id,count(*) as node_count from tb_node group by region_id) n on r.id=n.region_id;
-- AI辅助编程模式
-- 查询区域表所有的信息,需要显示每个区域的点位数
SELECT r.*, COUNT(n.id) AS node_count FROM tb_region r LEFT JOIN tb_node n ON r.id = n.region_id GROUP BY r.id;
RegionMapper.xml
xml
<select id="selectRegionVoList" resultType="com.dkd.manage.domain.vo.RegionVo">
select r.id,r.region_name,r.remark,ifnull(n.node_count,0) as node_count from tb_region r
left join (select region_id,count(*) as node_count from tb_node group by region_id) n on r.id=n.region_id
<where>
<if test="regionName != null and regionName != ''"> and r.region_name like concat('%', #{regionName}, '%')</if>
</where>
</select>

4 .合作商管理

合作商改造 列表查询
xml
<select id="selectPartnerVoList" resultType="com.dkd.manage.domain.vo.PartnerVo">
SELECT p.*, COUNT(n.id) AS node_count FROM tb_partner p
LEFT JOIN tb_node n ON p.id = n.partner_id
<where>
<if test="partnerName != null and partnerName != ''">and partner_name like concat('%', #{partnerName},'%')
</if>
</where>
GROUP BY p.id
</select>


mybatis封装结果,必须要给需要映射的属性添加getter和setter方法


为什么需要动态查询条件?



重置密码


5.点位管理

NodeMapper.xml
xml
<resultMap type="NodeVo" id="NodeVoResult">
<result property="id" column="id" />
<result property="nodeName" column="node_name" />
<result property="address" column="address" />
<result property="businessType" column="business_type" />
<result property="regionId" column="region_id" />
<result property="partnerId" column="partner_id" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
<result property="createBy" column="create_by" />
<result property="updateBy" column="update_by" />
<result property="remark" column="remark" />
<result property="vmCount" column="vm_count" />
<association property="region" javaType="Region" column="region_id" select="com.dkd.manage.mapper.RegionMapper.selectRegionById"/>
<association property="partner" javaType="Partner" column="partner_id" select="com.dkd.manage.mapper.PartnerMapper.selectPartnerById"/>
</resultMap>
<select id="selectNodeVoList" resultMap="NodeVoResult">
SELECT
n.id,
n.node_name,
n.address,
n.business_type,
n.region_id,
n.partner_id,
n.create_time,
n.update_time,
n.create_by,
n.update_by,
n.remark,
COUNT(v.id) AS vm_count
FROM
tb_node n
LEFT JOIN
tb_vending_machine v ON n.id = v.node_id
<where>
<if test="nodeName != null and nodeName != ''"> and n.node_name like concat('%', #{nodeName}, '%')</if>
<if test="regionId != null "> and n.region_id = #{regionId}</if>
<if test="partnerId != null "> and n.partner_id = #{partnerId}</if>
</where>
GROUP BY
n.id
</select>
降低多表查询的sql复杂度,避免笛卡尔积内存溢出风险,resultmap标签进行手动映射,association标签将合作商和区域关联到nodeVo中
resultMap 是 MyBatis 中最灵活的结果映射方式,用于解决「字段名与属性名不一致」「嵌套关联对象」等问题。
支持「嵌套关联对象」的封装(核心价值)
这是配置中最关键的作用!点位表(tb_node)只存储了 region_id(区域外键)和 partner_id(合作商外键),但业务需要获取「区域完整信息」(如区域名称、备注)和「合作商完整信息」(如合作商名称、联系人),而非仅外键 ID。
- resultMap 的 标签实现了「一对一嵌套查询 + 封装」:通过 column="region_id"提取点位表的外键,调用RegionMapper.selectRegionById 查询区域完整信息,自动封装到 NodeVo 的region 属性(类型为 Region 实体)。
- 同理,通过 column="partner_id" 查询合作商信息,封装到 NodeVo 的 partner 属性(类型为 Partner实体)。
这里 NodeVoResult 定义了 NodeVo 的映射规则:
xml
<resultMap type="NodeVo" id="NodeVoResult">
<!-- 1. 基础字段映射:SQL 字段 → NodeVo 简单属性 -->
<result property="id" column="id" />
<result property="nodeName" column="node_name" /> <!-- 下划线转小驼峰:node_name → nodeName -->
<result property="address" column="address" />
<result property="businessType" column="business_type" />
<result property="regionId" column="region_id" /> <!-- 关联区域的外键 -->
<result property="partnerId" column="partner_id" /> <!-- 关联合作商的外键 -->
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
<result property="createBy" column="create_by" />
<result property="updateBy" column="update_by" />
<result property="remark" column="remark" />
<result property="vmCount" column="vm_count" /> <!-- 统计的设备数别名 → vmCount -->
<!-- 2. 嵌套关联对象:通过外键查询区域完整信息(一对一) -->
<association
property="region" <!-- NodeVo 中的属性名:private Region region; -->
javaType="Region" <!-- 属性类型:Region 实体类(存储区域完整信息) -->
column="region_id" <!-- 关联条件:用点位表的 region_id 作为查询参数 -->
select="com.dkd.manage.mapper.RegionMapper.selectRegionById" <!-- 嵌套查询的 Mapper 方法:通过 region_id 查 Region -->
/>
<!-- 3. 嵌套关联对象:通过外键查询合作商完整信息(一对一) -->
<association
property="partner" <!-- NodeVo 中的属性名:private Partner partner; -->
javaType="Partner" <!-- 属性类型:Partner 实体类(存储合作商完整信息) -->
column="partner_id" <!-- 关联条件:用点位表的 partner_id 作为查询参数 -->
select="com.dkd.manage.mapper.PartnerMapper.selectPartnerById" <!-- 嵌套查询的 Mapper 方法:通过 partner_id 查 Partner -->
/>
</resultMap>
association 标签(一对一嵌套查询):点位表(tb_node)与区域表(tb_region)、合作商表(tb_partner)都是「一对一」关系(一个点位属于一个区域 / 一个合作商),所以用 association:
- 核心逻辑:先查询点位表,得到 region_id 和 partner_id 后,MyBatis 会自动调用 selectRegionById(region_id) 和selectPartnerById(partner_id) 查询区域和合作商的完整信息,再嵌套封装到 NodeVo 的 region 和 partner 属性中。
- 依赖:RegionMapper 必须有 selectRegionById(Integer regionId)方法,PartnerMapper 必须有 selectPartnerById(Integer partnerId) 方法,且能返回对应的Region/Partner 实体

更改service层 controller层

返回结果包括区域信息和合作商信息,只需要将前端展示的组件更改为要求的其他属性(名称)即可


6.数据完整性
当我们删除区域或合作商数据时,与之关联的点位数据该如何处理?

在默认情况下,由于我们在创建点位表时通过AI设置了外键约束,并配置了级联删除操作,所以删除区域或合作商会导致其关联的点位数据一并被删除。从技术角度来看,这是符合数据库的外键约束规则的。
但是,从业务角度来看,这种做法可能不太合适。想象一下,如果一个区域下有多个点位,一次误操作就可能导致所有的点位数据及其关联的设备信息被一并删除,这显然是我们不愿意看到的。
因此,我们需要对级联操作进行修改,将其改为限制删除。这样,当尝试删除一个区域或合作商时,如果它下面还有关联的点位数据,数据库将不会允许删除操作,并会给出错误提示。、
- CASCADE(级联操作):当父表中的某行记录被删除或更新时,与其关联的所有子表中的匹配行也会自动被删除或更新。这种方式适用于希望保持数据一致性的场景,即父记录不存在时,相关的子记录也应该被移除。
- SET NULL(设为空):若父表中的记录被删除或更新,子表中对应的外键字段会被设置为NULL。选择此选项的前提是子表的外键列允许为NULL值。这适用于那些子记录不再需要明确关联到任何父记录的情况。
- RESTRICT(限制):在尝试删除或更新父表中的记录之前,数据库首先检查是否有相关联的子记录存在。如果有,则拒绝执行删除或更新操作,以防止意外丢失数据或破坏数据关系的完整性。这是一种保守策略,确保数据间的引用完整性。
- NO ACTION(无操作):在标准SQL中,NO ACTION是一个关键字,它要求数据库在父表记录被删除或更新前,检查是否会影响子表中的相关记录。在MySQL中,NO ACTION的行为与RESTRICT相同,即如果子表中有匹配的行,则禁止执行父表的删除或更新操作。这意味着如果存在依赖关系,操作将被阻止,从而保护数据的参照完整性。
修改完毕后,如果你尝试进行删除操作,会发现数据库的完整性约束生效了,它会阻止删除操作并给出错误提示。但是,这个错误提示信息可能对于用户来说不够友好,可能会让用户感到困惑。

进行删除有关连点位信息的区域信息,会显示完整性约束违反异常


为了提升用户体验,我们可以使用Spring Boot框架的全局异常处理器来捕获这些错误信息,并返回更友好的提示信息给用户。这样,当用户遇到这种情况时,他们将收到一个清晰、易懂的提示,告知他们操作无法完成的原因

java
/**
* 数据完整性异常
*/
@ExceptionHandler(DataIntegrityViolationException.class)
public AjaxResult handelDataIntegrityViolationException(DataIntegrityViolationException e) {
if (e.getMessage().contains("foreign")) {
// 外键关联
return AjaxResult.error("无法删除,有其他数据引用");
}
return AjaxResult.error("您的操作违反了数据库中的完整性约束");
}

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

2、库表设计
对于人员和其他管理数据,下面是示意图:
- 关系字段:role_id、region_id
- 数据字典:status(1启用、0停用)
- 冗余字段:region_name、role_code、role_name------减少关联查询,简化查询逻辑,优化读写操作(降低对主表依赖),提高查询效率

角色信息明显是有业务关联的数据,相当于是控制权限的核心。字典里放都是一些静态枚举类型的数据
数据字典不是说只能关联两个字段,所谓的字典就是一些固定的值,只不过还要给他关联个标识而已。但是不是说100%两个字段,我一个id,一个名称,一个值不行吗?

3、生成基础代码

4、人员列表改造
同步存储策略 :实现区域和员工表数据一致性


empMapper更新员工表的区域信息
java
/**
* 修改人员列表状态
* @param regionName
* @param regionId
* @return 结果
*/
@Update("update tb_emp set region_name=#{regionName} where region_id=#{regionId}")
public int updateEmpStatus(@Param("regionName") String regionName,@Param("regionId") Long regionId);
RegionService中添加empMapper,在更新region信息成功时需要同步更新员工的信息,注意添加事务

测试:修改区域名称,人员信息同步更新


5、本地存储
阿里云OSS



通过 RAM 用户(阿里云的权限管理用户)的 AccessKey,配置系统环境变量,让本地程序(如 SDK、命令行工具)能安全访问 OSS 资源,避免直接使用主账号密钥。

官方入门案例
java
package com.dkd.common.test;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.common.auth.*;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import java.io.FileInputStream;
import java.io.InputStream;
public class Demo {
public static void main(String[] args) throws Exception {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-beijing.aliyuncs.com";
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
// 填写Bucket名称,例如examplebucket。
String bucketName = "dkd-itheima";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = "gao.png";
// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
String filePath= "E:\\temp\\upload\\gao.png";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
try {
InputStream inputStream = new FileInputStream(filePath);
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);
// 创建PutObject请求。
PutObjectResult result = ossClient.putObject(putObjectRequest);
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
}

x-file-storage
通过一行代码将文件进行存储,只需要在application.yml文件中配置好即可
官方地址:https://x-file-storage.xuyanwu.cn/#/





五、设备管理
1、需求说明


冗余字段,简化查询接口、提高查询效率

为什么有主键id,还要有设备编号字段?
如果id向外展示,则会公开设备数量,保密性安全性不够

2、基础代码生成

3、设备管理改造
新增设备
根据点位(node_id )和设备(vm_type_id )补充冗余字段,还需要根据售货机类型创建此售货机的所属货道
我们了解到在新增设备时,添加设备和货道表,还包含点位表和设备类型表的查询,共涉及到四张表的操作。
这个过程需要我们仔细处理每个字段,确保数据的一致性和完整性

涉及两张表新增操作,确保数据一致性和完整性,需要添加事务**@Transactional**
涉及循环,如果每次创建货道都保存到数据库,对数据库的性能影响过大。
方案优化:批量保存,循环遍历结束后,一次性添加到数据库中

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);
xml
<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>
修改设备
要求修改设备时 根据点位同步更新冗余字段

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


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);
}
设备状态改造
六、策略管理
1、需求说明
- 新增策略: 允许管理员定义新的策略,包括策略的具体内容和参数(如折扣率)
- 策略分配 : 将策略分配给一个或多个售货机。

2、库表设计

2、基础代码生成
3、设备策略分配
为表单赋值设备id和策略id
好处:在设备中进行关联策略时,只需要两个参数,设备id作为更新条件 策略id作为更新字段,直须提交两个参数,减少一次后端的查询开销


进行策略管理时,前端只提供设备id和策略id,不提供点位id,不执行设备对其冗余字段(点位)的更新

需要判断当前更新操作是进行策略分配(不传递nodeId),还是设备更新操作(传递nodeId),是否需要给设备表中补充点位的冗余字段

七、商品管理
1、需求说明
商品管理主要涉及到三个功能模块,业务流程如下:
- 新增商品类型: 定义商品的不同分类,如饮料、零食、日用品等。
- 新增商品: 添加新的商品信息,包括名称、规格、价格、类型等。
- 设备货道管理: 将商品与售货机的货道关联,管理每个货道的商品信息。
2、库表设计
- 关系字段:class_id、sku_id、vm_id

3、生成基础代码

如果添加商品类型重复,应该提示用户,故此我们需要在全局异常处理器中进行处理
Duplicate:当数据库有唯一约束,出现重名情况下,会出现此关键字。


4、商品管理改造
1、商品删除
逻辑校验!
保证销售一致性和数据一致性

SkuServiceImpl.java
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);
}
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>
在channel的mapper层和service层添加相关统计方法。
此时再进行删除则回显无法删除

2、批量导入
接口文档

实体类当中存在Excel注解,与文件的列标题进行对应,实现一一对应


java
/**
* 导入商品管理列表
* @param file
* @param updateSupport
* @return
* @throws Exception
*/
@PreAuthorize("@ss.hasPermi('manage:sku:add')")
@Log(title = "商品管理", businessType = BusinessType.IMPORT)
@PostMapping("/import")
public AjaxResult excelImport(MultipartFile file, boolean updateSupport) throws Exception
{
//通过excel工具类解析上传的excel文件,获取sku商品集合
ExcelUtil<Sku> util = new ExcelUtil<Sku>(Sku.class);
List<Sku> skuList = util.importExcel(file.getInputStream());
return toAjax(skuService.insertSkus(skuList));
}
新增service mapper的批量新增方法,在controller层中直接调用
excel表格中没有创建时间 修改时间列 所以在编写批量操作sql语句时 不需要编写这些字段,这些字段是有默认值 的(当前时间)。还有是否打折也不需要添加,因为打折是根据商品所关联的货道 根据其售货机的策略模式进行判断的,很商品无关,该字段也不需要添加

xml
<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>

5、Easy Excel
官方地址:https://easyexcel.alibaba.com/
Easy Excel只能处理excel文件 而Apache POI可以处理各种类型的office文件

若依集成easyexcel

先添加依赖后在工具类中添加读写方法
在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());
}
}

更改Sku实体类
java
/**
* 商品管理对象 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;
更改controller层的方法位easyexcel的操作


6、根据售货机编号查询货道列表
对智能售货机内部的货道进行商品摆放的管理




根据接口文档返回数据结果 需要在定义实体类ChannelVo继承Channer,添加Sku属性
java
@Data
public class ChannelVo extends Channel {
//商品对象
private Sku sku;
}

xml
<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>
java
/**
* 根据售货机编号查询货道列表
*
* @param innerCode
* @return ChannelVo集合
*/
@Override
public List<ChannelVo> selectChannelVoListByInnerCode(String innerCode) {
return channelMapper.selectChannelVoListByInnerCode(innerCode);
}
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);
}

7、货道关联商品




思路:controller->service->mapper;代码开发从后到前

设计两个dto对象,用于接收前端传递的参数

查询货道信息以及批量修改货道
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>
批量修改时,一次请求需要包含多条sql语句,需要在配置文件中添加此参数表示一次请求可以包含多条sql语句

service层将dto转为po查询货道对象,将商品id批量修改到货道对象中
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);
}
八、工单管理
1、需求说明



2、库表设计

3、生成基础代码

添加菜单后访问报sql语法错误

是因为我们表中存在desc备注字段和mysql降序关键字冲突了,我们需要在此字段加反引号进行标识

注意:idea在复制文件后,target目录下的TaskMapper(编译后的运行环境)会出现问题(删除或不更新),需要我们把复制的文件重新复制到target目录下
4、查询工单列表




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>
编写service层方法(调用mapper)--省略

java
/**
* 查询工单列表
*/
@PreAuthorize("@ss.hasPermi('manage:task:list')")
@GetMapping("/list")
public TableDataInfo list(Task task)
{
startPage();
List<TaskVo> voList = taskService.selectTaskVoList(task);
return getDataTable(voList);
}
5、获取运营人员列表
根据设备列表即可查询运营人员数据




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));
}
}
6、获取运维人员列表
根据前端传递的设备编号查询设备信息,再根据设备的区域id以及员工的状态、类型查询该区域下的员工表


7、新增工单
运维工单和运营工单新增共享一套后端接口
请求参数json格式

返回数据AjaxResult




批量新增工单详情 TaskDetails
xml
<insert id="batchInsertTaskDetails">
INSERT INTO tb_task_details(task_id, channel_code, expect_capacity, sku_id, sku_name, sku_image)
VALUES
<foreach item="item" collection="list" separator=",">
(#{item.taskId}, #{item.channelCode}, #{item.expectCapacity}, #{item.skuId}, #{item.skuName}, #{item.skuImage})
</foreach>
</insert>
java
/**
* 批量新增工单详情
* @param taskDetailsList
* @return 结果
*/
int batchInsertTaskDetails(List<TaskDetails> taskDetailsList);
java
/**
* 批量新增工单详情
* @param taskDetailsList
* @return 结果
*/
@Override
public int batchInsertTaskDetails(List<TaskDetails> taskDetailsList) {
return taskDetailsMapper.batchInsertTaskDetails(taskDetailsList);
}
新增运营 运维工单

设置工单编号,生成并获取当天任务代码的唯一标识,格式为"日期XXXX"。
- 该方法首先尝试从Redis中获取当天的任务代码计数,如果不存在,则初始化为1并返回"日期0001"格式的字符串。
- 如果存在,则对计数加1并返回更新后的任务代码。
java
/**
* 生成并获取当天任务代码的唯一标识。
* 该方法首先尝试从Redis中获取当天的任务代码计数,如果不存在,则初始化为1并返回"日期0001"格式的字符串。
* 如果存在,则对计数加1并返回更新后的任务代码。
*
* @return 返回当天任务代码的唯一标识,格式为"日期XXXX",其中XXXX是四位数字的计数。
*/
public String generateTaskCode() {
// 获取当前日期并格式化为"yyyyMMdd"
String dateStr = DateUtils.getDate().replaceAll("-", "");
// 根据日期生成redis的键
String key = "dkd.task.code." + dateStr;
// 判断key是否存在
if (!redisTemplate.hasKey(key)) {
// 如果key不存在,设置初始值为1,并指定过期时间为1天
redisTemplate.opsForValue().set(key, 1, Duration.ofDays(1));
// 返回工单编号(日期+0001)
return dateStr + "0001";
}
// 如果key存在,计数器+1(0002),确保字符串长度为4位
return dateStr+StrUtil.padPre(redisTemplate.opsForValue().increment(key).toString(),4,'0');
}
因为涉及到工单表和工单详情表的新增操作,需要添加事务,加入事务注解
java
@Override
@Transactional
public int insertTaskDto(TaskDto taskDto) {
//1.查询售货机是否存在
VendingMachine vm = vendingMachineService.selectVendingMachineByInnerCode(taskDto.getInnerCode());
if (vm == null) {
throw new ServiceException("设备不存在");
}
//2.检查售货及状态与工单类型是否相符
checkCreateTask(vm.getVmStatus(), taskDto.getProductTypeId());
//3. 检查设备是否有未完成的同类型工单
hasTask(taskDto);
//4. 查询并校验员工是否存在
Emp emp = empService.selectEmpById(taskDto.getUserId());
if (emp == null) {
throw new ServiceException("员工不存在");
}
//5. 校验员工区域是否匹配
if (!emp.getRegionId().equals(vm.getRegionId())) {
throw new ServiceException("员工区域与设备区域不一致,无法处理此工单");
}
//6. 将dto转为po并补充属性,保存工单
Task task = BeanUtil.copyProperties(taskDto, Task.class);// 属性复制
task.setTaskStatus(DkdContants.TASK_STATUS_CREATE);// 创建工单
task.setUserName(emp.getUserName());// 执行人名称
task.setRegionId(vm.getRegionId());// 所属区域id
task.setAddr(vm.getAddr());// 地址
task.setCreateTime(DateUtils.getNowDate());// 创建时间
task.setTaskCode(generateTaskCode());// 工单编号
int taskResult = taskMapper.insertTask(task);
//7. 判断是否为补货工单
if (taskDto.getProductTypeId().equals(DkdContants.TASK_TYPE_SUPPLY)) {
// 8.保存工单详情
List<TaskDetailsDto> details = taskDto.getDetails();
if (CollUtil.isEmpty(details)) {
throw new ServiceException("补货工单详情不能为空");
}
// 将dto转为po补充属性
List<TaskDetails> taskDetailsList = details.stream().map(dto -> {
TaskDetails taskDetails = BeanUtil.copyProperties(dto, TaskDetails.class);
taskDetails.setTaskId(task.getTaskId());
return taskDetails;
}).collect(Collectors.toList());
// 批量新增
taskDetailsService.batchInsertTaskDetails(taskDetailsList);
}
return taskResult;
}
java
/**
* 检查设备是否已有未完成的同类型工单。
* 本方法用于在创建新工单前,验证指定设备是否已经有处于进行中的同类型工单。
* 如果存在未完成的同类型工单,则抛出服务异常,阻止新工单的创建。
*
*/
private void hasTask(TaskDto taskDto) {// 创建Task对象,并设置设备编号和工单类型ID,以及任务状态为进行中
Task taskParam = new Task();
//设备的内部编码,用于唯一标识设备。
taskParam.setInnerCode(taskDto.getInnerCode());
//任务的类型,决定任务的性质(投放、维修、补货、撤机)。
taskParam.setProductTypeId(taskDto.getProductTypeId());
taskParam.setTaskStatus(DkdContants.TASK_STATUS_PROGRESS);
// 查询数据库中符合指定条件的工单列表
List<Task> taskList = taskMapper.selectTaskList(taskParam);
// 如果存在未完成的同类型工单,则抛出服务异常
if (CollUtil.isNotEmpty(taskList)) {
throw new ServiceException("该设备有未完成的同类型工单,不能重复创建");
}
}
/**
* 根据设备的状态和任务类型,验证是否可以创建相应的任务。
* 如果条件不满足,抛出服务异常。
*
* @param vmStatus 设备的状态,表示设备是否在运行。
* @param productTypeId 任务的类型,决定任务的性质(投放、维修、补货、撤机)。
*/
private void checkCreateTask(Long vmStatus, Long productTypeId) {
// 如果是投放工单,且设备状态为运行中,则抛出异常,因为设备已在运营中无法进行投放
if (productTypeId == DkdContants.TASK_TYPE_DEPLOY && vmStatus == DkdContants.VM_STATUS_RUNNING) {
throw new ServiceException("该设备状态为运行中,无法进行投放");
}
// 如果是维修工单,且设备状态不是运行中,则抛出异常,因为设备不在运营中无法进行维修
if (productTypeId == DkdContants.TASK_TYPE_REPAIR && vmStatus != DkdContants.VM_STATUS_RUNNING) {
throw new ServiceException("该设备状态不是运行中,无法进行维修");
}
// 如果是补货工单,且设备状态不是运行中,则抛出异常,因为设备不在运营状态无法进行补货
if (productTypeId == DkdContants.TASK_TYPE_SUPPLY && vmStatus != DkdContants.VM_STATUS_RUNNING) {
throw new ServiceException("该设备状态不是运行中,无法进行补货");
}
// 如果是撤机工单,且设备状态不是运行中,则抛出异常,因为设备不在运营状态无法进行撤机
if (productTypeId == DkdContants.TASK_TYPE_REVOKE && vmStatus != DkdContants.VM_STATUS_RUNNING) {
throw new ServiceException("该设备状态不是运行中,无法进行撤机");
}
}
更改TaskController

java
/**
* 新增工单
*/
@PreAuthorize("@ss.hasPermi('manage:task:add')")
@Log(title = "工单", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody TaskDto taskDto)
{
// 设置指派人(登录用户)id
taskDto.setAssignorId(getUserId());
return toAjax(taskService.insertTaskDto(taskDto));
}
添加运维工单,如果是投放工单,要求设备不能在运行状态。如果是撤机 维修 补货工单要求设备必须在运行状态
添加运营工单,如果是补货工单,还需要提供工单详情



8、查询工单详情
运维工单和运营工单查看详情依旧共用同一套接口,不同的是运营工单查看详情中可以查看捕获工单详情


取消工单
对于未完成的工单,管理员可以进行取消操作


controller,拿到前端传递参数封装给task实体类
java
/**
* 取消工单
*/
@PreAuthorize("@ss.hasPermi('manage:task:edit')")
@Log(title = "工单", businessType = BusinessType.UPDATE)
@PutMapping("/cancel")
public AjaxResult cancelTask(@RequestBody Task task) {
return toAjax(taskService.cancelTask(task));
}
service
java
/**
* 取消工单
* @param task
* @return 结果
*/
@Override
public int cancelTask(Task task) {
//1. 判断工单状态是否可以取消
// 先根据工单id查询数据库,task只携带id和取消原因
Task taskDb = taskMapper.selectTaskByTaskId(task.getTaskId());
// 判断工单状态是否为已取消,如果是,则抛出异常
if (taskDb.getTaskStatus().equals(DkdContants.TASK_STATUS_CANCEL)) {
throw new ServiceException("该工单已取消了,不能再次取消");
}
// 判断工单状态是否为已完成,如果是,则抛出异常
if (taskDb.getTaskStatus().equals(DkdContants.TASK_STATUS_FINISH)) {
throw new ServiceException("该工单已完成了,不能取消");
}
//2. 设置更新字段
task.setTaskStatus(DkdContants.TASK_STATUS_CANCEL);// 工单状态:取消
task.setUpdateTime(DateUtils.getNowDate());// 更新时间
//3. 更新工单
return taskMapper.updateTask(task);// 注意别传错了,这里是前端task参数
}
查看工单详情
运营工单页面可以查看补货详情
根据id为条件查询工单详情,返回结果



根据工单id查询当前工单的补货详情列表,将taskid直接封装到taskDetails中,调用service原有的方法即可
java
/**
* 查看工单补货详情
*/
@PreAuthorize("@ss.hasPermi('manage:taskDetails:list')")
@GetMapping(value = "/byTaskId/{taskId}")
public AjaxResult byTaskId(@PathVariable("taskId") Long taskId) {
TaskDetails taskDetailsParam = new TaskDetails();
taskDetailsParam.setTaskId(taskId);
return success(taskDetailsService.selectTaskDetailsList(taskDetailsParam));
}
