实战篇

1.项目介绍

帝可得是一个基于物联网概念下的智能售货机运营管理系统

1.1角色与功能

一个完整的售货机系统由五端五角色组成:

  1. 管理员:对基础数据(区域、点位、设备、货道、商品等)进行管理,创建工单指派运维或运营人员,查看订单,查看各种统计报表。
  2. 运维人员:投放设备、撤除设备、维修设备。
  3. 运营人员:补货。
  4. 合作商:仅提供点位,坐收渔翁之利。
  5. 消费者: 在小程序或屏幕端下单购买商品。

img

1.2 业务流程

整个工程中,课程里会对主要核心的业务进行实现,主要包含下面的业务流程: (1)平台管理员:主要作用有基础数据的管理和创建工单排除员工完成维修或补货。 (2)运营人员:主要作用是处理运营工单业务(补货等操作) (3)运维人员:主要作用是处理运维工单业务(设备维修等操作) (4)消费者:提供C端用户使用。消费者扫描售货机上的二维码可以打开此端。主要作用是完成在售货机的购物操作。

img

1.4.1 平台管理员

img

img

上图中的简要流程: ①:平台管理人员登录到系统管理后台系统 ②:创建区域数据 ③:创建区域下点位数据 ④:添加运维/运营人员 ⑤:创建售货机信息 ⑥:设置售货机点位信息 ⑦:创建运维投放工单,由运维人员开始投放设备(安装设备) ⑧:设置售卖的商品信息 ⑨:创建运营补货工单,由运营人员开始投放商品信息

1.4.2 运维人员

img

上图中的简要流程: ①:运维人员通过App登录运营系统 ②:在App对派送过来的工单进行处理 ③:接受工单后在指定的投放点安装售货机 ④:拒绝工单该运维人员的工单结束

1.4.3 运营人员

img

上图中的简要流程: ①:运营人员通过App登录运营系统 ②:在App对派送过来的工单进行处理 ③:接受工单后在指定的售货机的商品进行补货 ④:拒绝工单该运维人员的工单结束

1.4.4 消费者

上图中的简要流程:

img

上图中的简要流程: 方式一: ①:用户通过售货机二维码进行购买商品 ②:扫码后在手机端微信小程序选择商品 ③:支付成功后在售货机取货 方式二: ①:用户在售货机上选择商品 ②:在选择商品后扫码支付商品的二维码 ③:支付成功后在售货机取货

1.5 产品原型

帝可得项目点击链接立即查看 https://codesign.qq.com/s/426304924036117

1.6 库表设计

系统后台基础数据表关系说明:

img

一个区域可以有多个点位 一个点位可以有多个售货机 一个售货机有多个货道 多个货道可以放置同一样商品 一个商品类型下有多个商品 一个售货机类型下有多个售货机 一个合作商有多个点位 合作商和区域之间没有关系,因为合作商拥有的多个点位可以分布在不同的区域 每个区域下有多个运维和运营人员,他们来负责这个区域下的设备的运维和运营

2.项目搭建

2.1 搭建后端项目

2.1.1初始化项目

Git下载

通过idea克隆源码,仓库地址:https://gitee.com/ys-gitee/dkd-parent.git

Maven构建

使用idea打开项目后,等待环境检查(主要是Maven下载项目依赖)

2.1.2MySQL相关

导入sql

1、创建数据库create schema dkd; 2、执行sql脚本文件,完成导入

配置信息

dkd-admin模块下,编辑resources目录下的application-druid.yml,修改数据库连接

2.1.3 Redis相关

在redis解压目录下,执行redis-server.exe redis.windows.conf启动

dkd-admin模块下,resources目录下的application.yml,设置redis密码等相关信息

2.1.4 项目运行

dkd-admin模块下,运行com.ruoyi.DkdApplication.java

2.2 搭建前端项目

2.2.1 初始化项目

通过vscode克隆源码,仓库地址:https://gitee.com/ys-gitee/dkd-vue.git

2.2.2 安装依赖

2.2.3 项目运行

打开浏览器,输入:([http://localhost:80) 默认账户/密码 admin/admin123)若能正确展示登录页面,并能成功登录,菜单及页面展示正常,表明环境搭建成功

3. 点位管理

3.1 需求说明

业务场景: 假设我们的公司现在有一个宏伟的计划——在北京发展业务。首先,我们需要确定几个有潜力的区域,这些区域可能是人流量大、消费能力高的商业区或居民区。然后,我们要与这些区域内的潜在合作商进行洽谈,比如商场、写字楼、学校等地方的管理者或所有者。

一旦我们与合作商达成协议,确定了合作的细节和点位,我们就可以安排工作人员去投放智能售货机了。这些点位将成为我们智能售货机的“家”,为消费者提供便捷的购买服务。

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

  1. 登录系统:后台管理人员登录后台系统
  2. 新增区域: 后台管理人员可以添加区域范围,区域范围与运维/运维人员挂钩,区域下可关联点位。
  3. 新增合作商: 管理人员可以添加合作商,合作商与点位进行关联。
  4. 新增区域点位: 后台管理人员可以在特定区域内新增点位,这些点位是放置智能售货机的具体位置。

img

3.2库表设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
CREATE TABLE `tb_region` (
`id` INT AUTO_INCREMENT COMMENT '主键id' PRIMARY KEY,
`region_name` VARCHAR(255) NOT NULL COMMENT '区域名称',
`create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`create_by` VARCHAR(64) COMMENT '创建人',
`update_by` VARCHAR(64) COMMENT '修改人',
`remark` TEXT COMMENT '备注'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='区域表';

-- 插入测试数据
INSERT INTO `tb_region` (`region_name`,`remark`) VALUES ('北京市朝阳区','北京市朝阳区'), ('北京市海淀区','北京市海淀区'), ('北京市东城区','北京市东城区');

CREATE TABLE `tb_partner` (
`id` INT AUTO_INCREMENT COMMENT '主键id' PRIMARY KEY,
`partner_name` VARCHAR(255) NOT NULL COMMENT '合作商名称',
`contact_person` VARCHAR(64) COMMENT '联系人',
`contact_phone` VARCHAR(15) COMMENT '联系电话',
`profit_ratio` INT COMMENT '分成比例',
`account` VARCHAR(64) COMMENT '账号',
`password` VARCHAR(64) COMMENT '密码',
`create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`create_by` VARCHAR(64) COMMENT '创建人',
`update_by` VARCHAR(64) COMMENT '修改人',
`remark` TEXT COMMENT '备注'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合作商表';

-- 插入测试数据
INSERT INTO `tb_partner` (`partner_name`, `contact_person`, `contact_phone`, `profit_ratio`, `account`, `password`) VALUES
('合作商A', '张三', '13800138000', 30, 'a001', 'pwdA'),
('合作商B', '李四', '13912345678', 25, 'b002', 'pwdB');

CREATE TABLE `tb_node` (
`id` INT AUTO_INCREMENT COMMENT '主键id' PRIMARY KEY,
`node_name` VARCHAR(255) NOT NULL COMMENT '点位名称',
`address` VARCHAR(255) NOT NULL COMMENT '详细地址',
`business_type` INT COMMENT '商圈类型',
`region_id` INT COMMENT '区域ID',
`partner_id` INT COMMENT '合作商ID',
`create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`create_by` VARCHAR(64) COMMENT '创建人',
`update_by` VARCHAR(64) COMMENT '修改人',
`remark` TEXT COMMENT '备注',
FOREIGN KEY (`region_id`) REFERENCES `tb_region`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (`partner_id`) REFERENCES `tb_partner`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='点位表';


-- 插入测试数据
-- 假设区域ID为1对应'北京市朝阳区',合作商ID为1对应'合作商A'
INSERT INTO `tb_node` (`node_name`, `address`, `business_type`, `region_id`, `partner_id`) VALUES
('三里屯点位', '北京市朝阳区三里屯路', 1, 1, 1),
('五道口点位', '北京市海淀区五道口', 2, 2, 2);

对于点位管理数据模型,下面是示意图:

  • 关系字段:region_id、partner_id
  • 数据字典:business_type

img

img

3.3 生成基础代码

这里的若依生成代码在基础篇已经讲的很详细了,这里这贴出部分重要的配置图

配置合作商表(参考原型)

img

img

img

配置区域表(参考原型)

img

img

img

配置点位表(参考原型)

img

img

img

3.4 区域管理改造

3.4.1 基础页面

需求

参考页面原型,完成基础布局展示改造

img

代码实现

将vue代码中的主键id改为序号, 并加上type=“index” width=“50” ,去掉修改和删除前面的图标

在region/index.vue视图组件中修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 区域列表 -->
<el-table v-loading="loading" :data="regionList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" width="50" align="center" prop="id" />
<el-table-column label="区域名称" align="center" prop="regionName" />
<el-table-column label="备注说明" align="center" prop="remark" />
<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:region:edit']">修改</el-button>

<el-button link type="primary" @click="handleDelete(scope.row)" v-hasPermi="['manage:region:remove']">删除</el-button>

</template>

</el-table-column>

</el-table>

3.4.2 区域列表

需求

在区域列表查询中,需要显示每个区域的点位数

img

实现思路

实现此功能也有多种方案: **(1)同步存储:**在区域表中有点位数的字段,当点位发生变化时,同步区域表中的点位数。

  • 优点:由于是单表查询操作,查询列表效率最高。
  • 缺点:需要在点位增删改时修改区域表中的数据,有额外的开销,数据也可能不一致。

**(2)关联查询:**编写关联查询语句,在mapper 层封装。

  • 优点:实时查询,数据100%正确,不需要单独维护。
  • 缺点:SQL语句较复杂,如果数据量大,性能比较低。

区域和点位表,记录个数都不是很多,所以我们采用关联查询这种方案。

img

SQL

SQL查询:先聚合统计每个区域的点位数,然后与区域表进行关联查询

1
2
3
4
5
6
7
8
9
10
11
12
-- 传统模式
-- 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;

RegionVo

根据接口文档和sql创建RegionVo

img

RegionMapper

1
2
3
4
5
6
/**
* 查询区域管理列表
* @param region
* @return RegionVo集合
*/
public List<RegionVo> selectRegionVoList(Region region);

RegionMapper.xml

1
2
3
4
5
6
7
8
9
<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>

mybatis-config.xml

img

IRegionService

1
2
3
4
5
6
/**
* 查询区域管理列表
* @param region
* @return RegionVo集合
*/
public List<RegionVo> selectRegionVoList(Region region);

RegionServiceImpl

1
2
3
4
5
6
7
8
9
/**
* 查询区域管理列表
* @param region
* @return RegionVo集合
*/
@Override
public List<RegionVo> selectRegionVoList(Region region) {
return regionMapper.selectRegionVoList(region);
}

RegionController

1
2
3
4
5
6
7
8
9
10
11
/**
* 查询区域管理列表
*/
@PreAuthorize("@ss.hasPermi('manage:region:list')")
@GetMapping("/list")
public TableDataInfo list(Region region)
{
startPage();
List<RegionVo> voList = regionService.selectRegionVoList(region);
return getDataTable(voList);
}

region/index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 区域列表 -->
<el-table v-loading="loading" :data="regionList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" width="50" align="center" prop="id" />
<el-table-column label="区域名称" align="center" prop="regionName" />
<el-table-column label="点位数" align="center" prop="nodeCount" />
<el-table-column label="备注说明" align="center" prop="remark" />
<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:region:edit']">修改</el-button>

<el-button link type="primary" @click="handleDelete(scope.row)" v-hasPermi="['manage:region:remove']">删除</el-button>

</template>

</el-table-column>

</el-table>

img

img

3.5合作商管理改造

3.5.1 基础页面

需求

参考页面原型,完成基础布局展示改造

img

代码实现

在partner/index.vue视图组件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<!-- 搜索区域 -->
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="90px">
<el-form-item label="合作商名称" prop="name">
<el-input
v-model="queryParams.partnerName" 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="partnerList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" width="50" align="center" prop="id" />
<el-table-column label="合作商名称" align="center" prop="partnerName" />
<el-table-column label="账号" align="center" prop="account" />
<el-table-column label="分成比例" align="center" prop="profitRatio" >
<template #default="scope">{{scope.row.profitRatio}}%</template>

</el-table-column>

<el-table-column label="联系人" align="center" prop="contactPerson" />
<el-table-column label="联系电话" align="center" prop="contactPhone" />
<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:partner:edit']">修改</el-button>

<el-button link type="primary" @click="handleDelete(scope.row)" v-hasPermi="['manage:partner:remove']">删除</el-button>

</template>

</el-table-column>

</el-table>

<!-- 添加或修改合作商管理对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="partnerRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="合作商名称" prop="partnerName">
<el-input v-model="form.partnerName" placeholder="请输入合作商名称" />
</el-form-item>

<el-form-item label="联系人" prop="contactPerson">
<el-input v-model="form.contactPerson" placeholder="请输入联系人" />
</el-form-item>

<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="form.contactPhone" placeholder="请输入联系电话" />
</el-form-item>

<el-form-item label="创建时间" prop="contactPhone" v-if="form.id!=null">
{{form.createTime}}
</el-form-item>

<el-form-item label="分成比例" prop="profitRatio">
<el-input v-model="form.profitRatio" placeholder="请输入分成比例" />
</el-form-item>

<el-form-item label="账号" prop="account" v-if="form.id==null">
<el-input v-model="form.account" placeholder="请输入账号" />
</el-form-item>

<el-form-item label="密码" prop="password" v-if="form.id==null">
<el-input v-model="form.password" type="password" 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>

</div>

</template>

在PartnerServiceImpl的新增方法中修改

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 新增合作商
*
* @param partner 合作商
* @return 结果
*/
@Override
public int insertPartner(Partner partner) {
// 使用SecurityUtil工具类,对密码加密
partner.setPassword(SecurityUtils.encryptPassword(partner.getPassword()));
partner.setCreateTime(DateUtils.getNowDate());
return partnerMapper.insertPartner(partner);
}

3.5.2 查看详情

在partner/index.vue视图组件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<el-button link type="primary" @click="getPartnerInfo(scope.row)" v-hasPermi="['manage:partner:query']">查看详情</el-button>


<!-- 查看合作商详情 -->
<el-dialog title="合作商详情" v-model="partnerInfoOpen" width="500px" append-to-body>
<el-row>
<el-col :span="12">合作商名称:{{ form.partnerName }}</el-col>

<el-col :span="12">联系人:{{ form.contactPerson }}</el-col>

</el-row>

<el-row>
<el-col :span="12">联系电话:{{ form.contactPhone }}</el-col>

<el-col :span="12">分成比例:{{ form.profitRatio }}%</el-col>

</el-row>

</el-dialog>

<script>
/* 查看合作商详情 */
const partnerInfoOpen = ref(false);
function getPartnerInfo(row) {
reset();
const _id = row.id;
getPartner(_id).then(response => {
form.value = response.data;
partnerInfoOpen.value = true;
});
}
</script>
<!-- 查看合作商详情对话框 -->
<el-dialog title="合作商详情" v-model="partnerInfoOpen" width="500px" append-to-body>
<!-- 使用el-descriptions组件以卡片形式展示信息,更加整洁 -->
<el-descriptions :column="2" border>
<el-descriptions-item label="合作商名称">{{ form.partnerName }}</el-descriptions-item>

<el-descriptions-item label="联系人">{{ form.contactPerson }}</el-descriptions-item>

<el-descriptions-item label="联系电话">{{ form.contactPhone }}</el-descriptions-item>

<el-descriptions-item label="分成比例">{{ form.profitRatio }}%</el-descriptions-item>

</el-descriptions>

</el-dialog>

3.5.3 合作商列表

实现思路

与区域列表实现方式相同。

img

SQL

SQL查询:先聚合统计每个合作商的点位数,然后与合作商表进行关联查询

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 传统模式
-- 1.先聚合统计每个合作商的点位数
-- 确定查询的表 tb_node
-- 确定分组字段 partner_id
SELECT partner_id, COUNT(1) AS node_count from tb_node group by partner_id;
-- 2.然后与合作商表进行关联查询
select tp.*,ifnull(tn.node_count,0) from tb_partner tp
left join (SELECT partner_id, COUNT(1) AS node_count from tb_node group by partner_id) tn on tn.partner_id = tp.id;
-- AI辅助编程模式
SELECT p.*, COUNT(n.id) AS node_count FROM tb_partner p
LEFT JOIN tb_node n ON p.id = n.partner_id
GROUP BY
p.id;

PartnerVo

根据接口文档和sql创建PartnerVo

img

PartnerMapper

1
2
3
4
5
6
/**
* 查询合作商管理列表
* @param partner
* @return partnerVo集合
*/
public List<PartnerVo> selectPartnerVoList(Partner partner);

PartnerMapper.xml

1
2
3
4
5
6
7
8
9
10
11
<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>

IPartnerService

1
2
3
4
5
6
/**
* 查询合作商管理列表
* @param partner
* @return partnerVo集合
*/
public List<PartnerVo> selectPartnerVoList(Partner partner);

PartnerServiceImpl

1
2
3
4
5
6
7
8
9
/**
* 查询合作商管理列表
* @param partner
* @return partnerVo集合
*/
@Override
public List<PartnerVo> selectPartnerVoList(Partner partner) {
return partnerMapper.selectPartnerVoList(partner);
}

PartnerController

1
2
3
4
5
6
7
8
9
10
/**
* 查询合作商管理列表
*/
@PreAuthorize("@ss.hasPermi('manage:partner:list')")
@GetMapping("/list")
public TableDataInfo list(Partner partner) {
startPage();
List<PartnerVo> voList = partnerService.selectPartnerVoList(partner);
return getDataTable(voList);
}

region/index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!-- 合作商列表 -->
<el-table v-loading="loading" :data="partnerList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" width="50" align="center" prop="id" />
<el-table-column label="合作商名称" align="center" prop="partnerName" />
<el-table-column label="点位数" align="center" prop="nodeCount" />
<el-table-column label="账号" align="center" prop="account" />
<el-table-column label="分成比例" align="center" prop="profitRatio" >
<template #default="scope">{{ scope.row.profitRatio }}%</template>

</el-table-column>

<el-table-column label="联系人" align="center" prop="contactPerson" />
<el-table-column label="联系电话" align="center" prop="contactPhone" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" @click="getPartnerInfo(scope.row)" v-hasPermi="['manage:partner:query']">查看详情</el-button>

<el-button link type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['manage:partner:edit']">修改</el-button>

<el-button link type="primary" @click="handleDelete(scope.row)" v-hasPermi="['manage:partner:remove']">删除</el-button>

</template>

</el-table-column>

</el-table>

3.5.4 重置密码

后端部分

在PartnerController中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 重置合作商密码
*/
@PreAuthorize("@ss.hasPermi('manage:partner:edit')")
@Log(title = "重置合作商密码", businessType = BusinessType.UPDATE)
@PutMapping("/resetPwd/{id}")
public AjaxResult resetpwd(@PathVariable Long id) {//1. 接收参数
//2. 创建合作商对象
Partner partner = new Partner();
partner.setId(id);// 设置id
partner.setPassword(SecurityUtils.encryptPassword("123456"));// 设置加密后的初始密码
//3. 调用service更新密码
return toAjax(partnerService.updatePartner(partner));
}

前端部分

在manage/partner.js请求api中

1
2
3
4
5
6
7
// 重置合作商密码
export function resetPartnerPwd(id){
return request({
url: '/manage/partner/resetPwd/' + id,
method: 'put'
})
}

在partner/index.vue视图组件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="300px">
<template #default="scope">
<el-button link type="primary" @click="resetPwd(scope.row)" v-hasPermi="['manage:partner:edit']">重置密码</el-button>

</template>

</el-table-column>

<script>
import { listPartner, getPartner, delPartner, addPartner, updatePartner,resetPartnerPwd } from "@/api/manage/partner";
/* 重置合作商密码 */
function resetPwd(row) {
proxy.$modal.confirm('你确定要重置该合作商密码吗?').then(function () {
return resetPartnerPwd(row.id);
}).then(() => {
proxy.$modal.msgSuccess("重置成功");
}).catch(() => { });
}
</script>

img

img

img

img

3.6 点位管理改造

3.6.1 基础页面

需求

参考页面原型,完成基础布局展示改造

img

代码实现

在node/index.vue视图组件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
<!-- 搜索区域 -->
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="点位名称" prop="nodeName">
<el-input v-model="queryParams.nodeName" placeholder="请输入点位名称" clearable @keyup.enter="handleQuery" />
</el-form-item>

<el-form-item label="区域搜索" prop="regionId">
<!-- <el-input v-model="queryParams.regionId" placeholder="请输入区域ID" clearable @keyup.enter="handleQuery" /> -->
<el-select v-model="queryParams.regionId" placeholder="请选择区域" clearable>
<el-option v-for="item in regionList" :key="item.id" :label="item.regionName" :value="item.id"></el-option>

</el-select>

</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="nodeList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" width="50" align="center" prop="id" />
<el-table-column label="点位名称" align="center" prop="nodeName" />
<el-table-column label="区域ID" align="center" prop="regionId" />
<el-table-column label="商圈类型" align="center" prop="businessType">
<template #default="scope">
<dict-tag :options="business_type" :value="scope.row.businessType" />
</template>

</el-table-column>

<el-table-column label="合作商ID" align="center" prop="partnerId" />
<el-table-column label="详细地址" align="center" prop="address" show-overflow-tooltip/>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['manage:node:edit']">修改</el-button>

<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['manage:node:remove']">删除</el-button>

</template>

</el-table-column>

</el-table>

<!-- 添加或修改点位管理对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="nodeRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="点位名称" prop="nodeName">
<el-input v-model="form.nodeName" placeholder="请输入点位名称" />
</el-form-item>

<el-form-item label="所在区域" prop="regionId">
<!-- <el-input v-model="form.regionId" placeholder="请输入区域ID" /> -->
<el-select v-model="form.regionId" placeholder="请选择区域" clearable>
<el-option v-for="item in regionList" :key="item.id" :label="item.regionName" :value="item.id"></el-option>

</el-select>

</el-form-item>

<el-form-item label="商圈类型" prop="businessType">
<el-select v-model="form.businessType" placeholder="请选择商圈类型">
<el-option v-for="dict in business_type" :key="dict.value" :label="dict.label"
:value="parseInt(dict.value)"></el-option>

</el-select>

</el-form-item>

<el-form-item label="归属合作商" prop="partnerId">
<!-- <el-input v-model="form.partnerId" placeholder="请输入合作商ID" /> -->
<el-select v-model="form.partnerId" placeholder="请选择合作商" clearable>
<el-option v-for="item in partnerList" :key="item.id" :label="item.partnerName" :value="item.id"></el-option>

</el-select>

</el-form-item>

<el-form-item label="详细地址" prop="address">
<el-input type="textarea" v-model="form.address" 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>

<script setup name="Node">
import { listPartner } from "@/api/manage/partner";
import { listRegion } from "@/api/manage/region";
import { loadAllParams } from "@/api/page";

/* 查询所有的条件对象 */
/* const loadAllParams = reactive({
pageNum: 1,
pageSize: 10000,
}); */

/* 查询合作商列表 */
const partnerList = ref([]);
function getPartnerList() {
listPartner(loadAllParams).then(response => {
partnerList.value = response.rows;
});
}
getPartnerList();
/* 查询区域列表 */
const regionList = ref([]);
function getRegionList() {
listRegion(loadAllParams).then(response => {
regionList.value = response.rows;
});
}
getRegionList();

</script>

在api/page.js中,抽取一个查询所有的条件

1
2
3
4
export const loadAllParams = reactive({
pageNum: 1,
pageSize: 10000,
});

3.6.2 点位列表

需求

在区域详情中,需要显示每个点位的设备数

img

在点位列表查询中,会关联显示区域、商圈等信息

img

实现思路

关联查询:对于设备数量的统计,我们需要执行关联查询,在mapper 层封装。 关联实体:对于区域和合作商的数据,我们会采用Mybatis提供的嵌套查询功能。

MyBatis 嵌套查询就是将原来多表查询中的联合查询语句拆成单个表的查询,再使用mybatis的语法嵌套在一 起,通过定义resultMapsql语句中的associationcollection元素来实现嵌套查询。

img

导入资料中dkd.sql业务表到开发数据库中

img

SQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
-- AI辅助编程模式
-- 你是一个软件开发工程师,现在要根据数据库的sql脚本,查询并显示点位表所有的字段信息,同时显示每个点位的设备数量,sql脚本如下:
create table tb_node
(
id int auto_increment comment '主键id'
primary key,
node_name varchar(255) not null comment '点位名称',
address varchar(255) not null comment '详细地址',
business_type int null comment '商圈类型',
region_id int null comment '区域ID',
partner_id int null comment '合作商ID',
create_time timestamp default CURRENT_TIMESTAMP null comment '创建时间',
update_time timestamp default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '修改时间',
create_by varchar(64) null comment '创建人',
update_by varchar(64) null comment '修改人',
remark text null comment '备注',
constraint tb_node_ibfk_1
foreign key (region_id) references tb_region (id)
on update cascade on delete cascade,
constraint tb_node_ibfk_2
foreign key (partner_id) references tb_partner (id)
on update cascade on delete cascade
)
comment '点位表';

create table tb_vending_machine
(
id bigint auto_increment comment '主键'
primary key,
inner_code varchar(15) default '000' null comment '设备编号',
channel_max_capacity int null comment '设备容量',
node_id int not null comment '点位Id',
addr varchar(100) null comment '详细地址',
last_supply_time datetime default '2000-01-01 00:00:00' not null comment '上次补货时间',
business_type int not null comment '商圈类型',
region_id int not null comment '区域Id',
partner_id int not null comment '合作商Id',
vm_type_id int default 0 not null comment '设备型号',
vm_status int default 0 not null comment '设备状态,0:未投放;1-运营;3-撤机',
running_status varchar(100) null comment '运行状态',
longitudes double default 0 null comment '经度',
latitude double default 0 null comment '维度',
client_id varchar(50) null comment '客户端连接Id,做emq认证用',
policy_id bigint null comment '策略id',
create_time timestamp default CURRENT_TIMESTAMP not null comment '创建时间',
update_time timestamp default CURRENT_TIMESTAMP null comment '修改时间',
constraint vendingmachine_VmId_uindex
unique (inner_code),
constraint tb_vending_machine_ibfk_1
foreign key (vm_type_id) references tb_vm_type (id),
constraint tb_vending_machine_ibfk_2
foreign key (node_id) references tb_node (id),
constraint tb_vending_machine_ibfk_3
foreign key (policy_id) references tb_policy (policy_id)
)
comment '设备表';
-- 查询并显示点位表所有的字段信息,同时显示每个点位的设备数量
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
GROUP BY
n.id;

-- 根据区域id查询区域信息
select * from tb_region where id=1;
-- 根据合作商id查询合作商信息
select * from tb_partner where id=1;

NodeVo

1
2
3
4
5
6
7
8
9
10
11
12
@Data
public class NodeVo extends Node {

// 设备数量
private int vmCount;

// 区域
private Region region;

// 合作商
private Partner partner;
}

NodeMapper

1
2
3
4
5
6
/**
* 查询点位管理列表
* @param node
* @return NodeVo集合
*/
public List<NodeVo> selectNodeVoList(Node node);

NodeMapper.xml

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

NodeService

1
2
3
4
5
6
/**
* 查询点位管理列表
* @param node
* @return NodeVo集合
*/
public List<NodeVo> selectNodeVoList(Node node);

NodeServiceImpl

1
2
3
4
5
6
7
8
9
10
/**
* 查询点位管理列表
*
* @param node
* @return NodeVo集合
*/
@Override
public List<NodeVo> selectNodeVoList(Node node) {
return nodeMapper.selectNodeVoList(node);
}

NodeController

1
2
3
4
5
6
7
8
9
10
11
/**
* 查询点位管理列表
*/
@PreAuthorize("@ss.hasPermi('manage:node:list')")
@GetMapping("/list")
public TableDataInfo list(Node node)
{
startPage();
List<NodeVo> voList = nodeService.selectNodeVoList(node);
return getDataTable(voList);
}

node/index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!-- 点位列表 -->
<el-table v-loading="loading" :data="nodeList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" width="50" align="center" prop="id" />
<el-table-column label="点位名称" align="center" prop="nodeName" />
<el-table-column label="所在区域" align="center" prop="region.regionName" />
<el-table-column label="商圈类型" align="center" prop="businessType">
<template #default="scope">
<dict-tag :options="business_type" :value="scope.row.businessType" />
</template>

</el-table-column>

<el-table-column label="合作商" align="center" prop="partner.partnerName" />
<el-table-column label="详细地址" align="center" prop="address" show-overflow-tooltip="true"/>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['manage:node:edit']">修改</el-button>

<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['manage:node:remove']">删除</el-button>

</template>

</el-table-column>

</el-table>

img

3.7 区域查看详情

在region/index.vue视图组件中修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<el-button link type="primary" @click="getRegionInfo(scope.row)" v-hasPermi="['manage:node:list']">查看详情</el-button>

<!-- 查看详情对话框 -->
<el-dialog title="区域详情" v-model="regionInfoOpen" width="500px" append-to-body>
<el-form-item label="区域名称" prop="regionName">
<el-input v-model="form.regionName" disabled />
</el-form-item>

<label>包含点位:</label>

<el-table :data="nodeList">
<el-table-column label="序号" type="index" width="50" align="center" />
<el-table-column label="点位名称" align="center" prop="nodeName" />
<el-table-column label="设备数量" align="center" prop="vmCount" />
</el-table>

</el-dialog>

<script>
import { listNode } from "@/api/manage/node";
import { loadAllParams } from "@/api/page";

/* 查看详情按钮操作 */
const nodeList = ref([]);
const regionInfoOpen = ref(false);
function getRegionInfo(row) {
// 查询区域信息
reset();
const _id = row.id
getRegion(_id).then(response => {
form.value = response.data;
});
// 查询点位列表
loadAllParams.regionId = row.id;
listNode(loadAllParams).then(response => {
nodeList.value = response.rows;
});
regionInfoOpen.value = true;
</script>

img

img

3.8数据完整性

现在我们要思考一个问题,当我们删除区域或合作商数据时,与之关联的点位数据该如何处理?

img

在默认情况下,由于我们在创建点位表时设置了外键约束,并配置了级联删除操作,所以删除区域或合作商会导致其关联的点位数据一并被删除。从技术角度来看,这是符合数据库的外键约束规则的。 但是,从业务角度来看,这种做法可能不太合适。想象一下,如果一个区域下有多个点位,一次误操作就可能导致所有的点位数据及其关联的设备信息被一并删除,这显然是我们不愿意看到的。 因此,我们需要对级联操作进行修改,将其改为限制删除。这样,当尝试删除一个区域或合作商时,如果它下面还有关联的点位数据,数据库将不会允许删除操作,并会给出错误提示。

img

**CASCADE(级联操作):**当父表中的某行记录被删除或更新时,与其关联的所有子表中的匹配行也会自动被删除或更新。这种方式适用于希望保持数据一致性的场景,即父记录不存在时,相关的子记录也应该被移除。 **SET NULL(设为空):**若父表中的记录被删除或更新,子表中对应的外键字段会被设置为NULL。选择此选项的前提是子表的外键列允许为NULL值。这适用于那些子记录不再需要明确关联到任何父记录的情况。 **RESTRICT(限制):**在尝试删除或更新父表中的记录之前,数据库首先检查是否有相关联的子记录存在。如果有,则拒绝执行删除或更新操作,以防止意外丢失数据或破坏数据关系的完整性。这是一种保守策略,确保数据间的引用完整性。 **NO ACTION(无操作):**在标准SQL中,NO ACTION是一个关键字,它要求数据库在父表记录被删除或更新前,检查是否会影响子表中的相关记录。在MySQL中,NO ACTION的行为与RESTRICT相同,即如果子表中有匹配的行,则禁止执行父表的删除或更新操作。这意味着如果存在依赖关系,操作将被阻止,从而保护数据的参照完整性。

修改完毕后,如果你尝试进行删除操作,会发现数据库的完整性约束生效了,它会阻止删除操作并给出错误提示。但是,这个错误提示信息可能对于用户来说不够友好,可能会让用户感到困惑。

img

SQLIntegrityConstraintViolationException是Java中的一个异常类,这个类通常用于表示SQL数据库操作中的完整性约束违反异常 例如:外键约束、唯一约束等。当数据库操作违反了这些约束时,就会抛出这个异常。 这个错误是由于外键约束导致的。它表明在删除或更新父表的行时,存在外键约束,子表中的相关行会受到影响。 是因为在删除tb_region表中的行时,tb_node表中的region_id外键约束会阻止操作。 如果你在使用Spring框架进行数据库操作,可能会先遇到DataIntegrityViolationException,它是对SQLIntegrityConstraintViolationException的一个更高层次的抽象,旨在提供一种更加面向应用的错误表示。 而SQLIntegrityConstraintViolationException是更底层的异常,直接来源于数据库驱动,包含更多底层数据库相关的细节。 在实际开发中,推荐捕获并处理DataIntegrityViolationException,因为它更符合Spring应用的异常处理模式,同时也可以通过其内部的cause(原因)属性来获取具体的SQLIntegrityConstraintViolationException,进而获取详细的错误信息。

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

修改全局异常处理器,添加以下内容

img

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 数据完整性异常
*/
@ExceptionHandler(DataIntegrityViolationException.class)
public AjaxResult handelDataIntegrityViolationException(DataIntegrityViolationException e) {

if (e.getMessage().contains("foreign")) {

return AjaxResult.error("无法删除,有其他数据引用");
}
return AjaxResult.error("您的操作违反了数据库中的完整性约束");
}

4. 人员管理

4.1 需求说明

人员管理业务流程如下:

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

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

  • 关系字段:role_id、region_id
  • 数据字典:status(1启用、0停用)
  • 冗余字段:region_name、role_code、role_name

img

4.2生成基础代码

操作同上,这里只贴出重要的配置图

配置员工表(参考原型)

img

img

img

配置角色表(无原型)

img

img

img

4.3 人员列表改造

4.3.1基础页面

需求

参考页面原型,完成基础布局展示改造

img

代码实现

在emp/index.vue视图组件中修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<!-- 搜索区域 -->
<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>

在EmpServiceImpl中新增和修改时,补充区域名称和角色信息

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

img

img

img

4.3.2 同步存储

实现思路

实现此功能方案: **同步存储:**在员工表中有区域名称的冗余字段,在更新区域表的同时,同步更新员工表中区域名称。

  • 优点:由于是单表查询操作,查询列表效率最高。
  • 缺点:需要在区域修改时修改员工表中的数据,有额外的开销,数据也可能不一致。

img

sql

1
2
-- 根据区域id修改区域名称
update tb_emp set region_name='北京市奥体中心' where region_id=5

EmpMapper

1
2
3
4
5
6
7
8
/**
* 根据区域id修改区域名称
* @param regionName
* @param regionId
* @return 结果
*/
@Update("update tb_emp set region_name=#{regionName} where region_id=#{regionId}")
int updateByRegionId(@Param("regionName") String regionName, @Param("regionId") Long regionId);

RegionServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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.4 文件存储

4.4.1 本地存储(不考虑)

4.4.2 阿里云OSS(SSM+Springboot基础篇讲过了这里不再提及)

4.4.3x-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中引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 文件上传-->
<dependency>
<groupId>org.dromara.x-file-storage</groupId>

<artifactId>x-file-storage-spring</artifactId>

<version>2.1.0</version>

</dependency>

<!-- 阿里云oss-->
<dependency>
<groupId>com.aliyun.oss</groupId>

<artifactId>aliyun-sdk-oss</artifactId>

<version>3.16.1</version>

</dependency>

2)在dkd-admin的application.yml 配置文件中先添加以下基础配置,再添加对应平台的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 文件上传
dromara:
x-file-storage: #文件存储配置
default-platform: aliyun-oss-1 #默认使用的存储平台
thumbnail-suffix: ".min.jpg" #缩略图后缀,例如【.min.jpg】【.png】
#对应平台的配置写在这里,注意缩进要对齐
aliyun-oss:
- platform: aliyun-oss-1 # 存储平台标识
enable-storage: true # 启用存储
access-key: ??
secret-key: ??
end-point: ??
bucket-name: ??
domain: ?? # 访问域名,注意“/”结尾,例如:https://abc.oss-cn-shanghai.aliyuncs.com/
base-path: dkd-images/ # 基础路径

3)在dkd-admin的启动类上加上@EnableFileStorage注解

1
2
3
4
5
6
7
8
9
10
11
@EnableFileStorage
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class DkdApplication
{
public static void main(String[] args)
{
// System.setProperty("spring.devtools.restart.enabled", "false");
SpringApplication.run(DkdApplication.class, args);
System.out.println("(♥◠‿◠)ノ゙ 帝可得启动成功 ლ(´ڡ`ლ)゙");
}
}

4)修改若依默认上传图片代码 找到ruoyi-admin模块中的com.ruoyi.web.controller.common.CommonController类,修改单个文件上传的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Autowired
private FileStorageService fileStorageService;//注入实列

/**
* 通用上传请求(单个)
*/
@PostMapping("/upload")
public AjaxResult uploadFile(MultipartFile file) throws Exception {

try {
// 指定oss保存文件路径
String objectName = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) + "/";
// 上传图片,成功返回文件信息
FileInfo fileInfo = fileStorageService.of(file).setPath(objectName).upload();
// 设置返回结果
AjaxResult ajax = AjaxResult.success();
ajax.put("url", fileInfo.getUrl());
ajax.put("fileName", fileInfo.getUrl()); //注意:这里的值要改为url,前端访问的地址,需要文件的地址 而不是文件名称
ajax.put("newFileName", fileInfo.getUrl());
ajax.put("originalFilename", file.getOriginalFilename());
return ajax;
} catch (Exception e) {
return AjaxResult.error(e.getMessage());
}
}

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

img

5.设备管理

5.1需求说明

img

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

  1. 新增设备类型: 允许管理员定义新的售货机型号,包括其规格和容量。
  2. 新增设备: 在新的设备类型定义后,系统应允许添加新的售货机实例,并将它们分配到特定的点位。
  3. 新增货道: 对于每个新添加的设备,系统应支持定义新的货道,后期用于关联相应的商品SKU。

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

  • 关系字段:vm_type_id、node_id、vm_id
  • 数据字典:vm_status(0未投放、1运营、3撤机)
  • 冗余字段:addr、business_type、region_id、partner_id(简化查询接口、提高查询效率)

img

5.2 生成基础代码(略)

5.3设备类型改造

5.3.1基础页面

需求

参考页面原型,完成基础布局展示改造

img

代码实现

在vmType/index.vue视图组件中修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<!-- 查询条件 -->
<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>

5.4设备管理改造

5.4.1基础页面

需求

参考页面原型,完成基础布局展示改造

img

代码实现

刷新设备表数据

1
2
3
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;

在vm/index.vue视图组件中修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
<!-- 查询条件 -->
<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>

5.4.2 新增设备

需求

新增设备时,补充设备表其他字段信息,还需要根据售货机类型创建所属货道

img

img

我们了解到在新增设备时,添加设备和货道表,还包含点位和设备类型的查询,共涉及到四张表的操作。 这个过程需要我们仔细处理每个字段,确保数据的一致性和完整性

img

VendingMachineServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 批量新增售货机货道
* @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

1
2
3
4
5
6
/**
* 批量新增售货机货道
* @param channelList
* @return 结果
*/
public int batchInsertChannel(List<Channel> channelList);

ChannelServiceImpl

1
2
3
4
5
6
7
8
9
/**
* 批量新增售货机货道
* @param channelList
* @return 结果
*/
@Override
public int batchInsertChannel(List<Channel> channelList) {
return channelMapper.batchInsertChannel(channelList);
}

5.4.3修改设备

需求

修改设备时,根据点位同步更新冗余字段信息

img

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

img

VendingMachineServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 修改设备管理
*
* @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);
}

img

img

img

5.5 设备状态改造

5.5.1 创建视图组件

创建vmStatus/index.vue视图组件

img

5.5.2 创建二级菜单

img

img

5.5.3改造视图组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
<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>

img

img

5.6点位查看详情

在node/index.vue视图组件中修改

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

img

6.策略管理

6.1需求说明

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

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

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

  • 关系字段:policy_id

img

6.2生成基础代码(略)

6.3策略管理改造

6.3.1基础页面

需求

参考页面原型,完成基础布局展示改造

img

代码实现

在policy/index.vue视图组件中修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<!-- 列表展示 -->
<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>

6.3.2 查看详情

需求

点击查看详情,展示策略名称和该策略下的设备列表

img

代码实现

在policy/index.vue视图组件中修改

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

img

img

6.4设备策略分配

6.4.1需求

在设备管理页面中点击策略,对设备设置一个固定折扣,用于营销作用

img

6.4.2代码实现

在vm/index.vue视图组件中修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<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中修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 修改设备管理
*
* @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);
}

img

img

7.商品管理

7.1 需求说明

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

  1. 新增商品类型: 定义商品的不同分类,如饮料、零食、日用品等。
  2. 新增商品: 添加新的商品信息,包括名称、规格、价格、类型等。
  3. 设备货道管理: 将商品与售货机的货道关联,管理每个货道的商品信息。

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

  • 关系字段:class_id、sku_id、vm_id

img

7.2 生成基础代码(略)

7.3商品类型改造

7.3.1 基础页面

需求

参考页面原型,完成基础布局展示改造

img

7.3.2 代码实现

在skuClass/index.vue视图组件中修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 列表展示 -->
<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>

修改全局异常处理器,添加以下内容

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 数据完整性异常
*/
@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("数据完整性异常,请联系管理员");
}

7.4 商品管理改造

7.4.1 基础页面

需求

参考页面原型,完成基础布局展示改造

img

7.4.2代码实现

在sku/index.vue视图组件中修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
<!-- 查询条件 -->
<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>

7.4.2 商品删除

需求

在删除商品时,需要判断此商品是否被售货机的货道关联,如果关联则无法删除

  • 物理外键约束:通过在子表中添加一个外键列和约束,该列与父表的主键列相关联,由数据库维护数据的一致性和完整性
  • 逻辑外键约束:在不使用数据库外键约束的情况下,通常在应用程序中通过代码来检查和维护数据的一致性和完整性

使用逻辑外键约束的原因:我们在新增售货机货道记录时暂不指定商品,货道表中的SKU_ID有默认值0,而这个值在商品表中并不存在,那么物理外键约束会阻止货道表的插入,因为0并不指向任何有效的商品记录

img

img

SkuServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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

1
2
3
4
5
6
/**
* 根据商品id集合统计货道数量
* @param skuIds
* @return 统计结果
*/
int countChannelBySkuIds(Long[] skuIds);

ChannelServiceImpl

1
2
3
4
5
6
7
8
9
/**
* 根据商品id集合统计货道数量
* @param skuIds
* @return 统计结果
*/
@Override
public int countChannelBySkuIds(Long[] skuIds) {
return channelMapper.countChannelBySkuIds(skuIds);
}

ChannelMapper接口和xml

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 根据商品id集合统计货道数量
* @param skuIds
* @return 统计结果
*/
int countChannelBySkuIds(Long[] skuIds);
<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>

7.5批量导入

7.5.1 需求

点击导入数据弹出导入数据弹窗,实现商品的批量导入

img

7.5.2 前端

在sku/index.vue视图组件中修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
<!--  导入按钮-->
<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>

7.5.3 后端

SkuController

1
2
3
4
5
6
7
8
9
10
11
/**
* 导入商品管理列表
*/
@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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 批量新增商品管理
* @param skuList
* @return 结果
*/
public int insertSkus(List<Sku> skuList);
<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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 批量新增商品管理
* @param skuList
* @return 结果
*/
public int insertSkus(List<Sku> skuList);
/**
* 批量新增商品管理
* @param skuList
* @return 结果
*/
@Override
public int insertSkus(List<Sku> skuList) {
return skuMapper.insertSkus(skuList);
}

7.6 EasyExcel

官方地址:https://easyexcel.alibaba.com/ 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模式,在上层做了模型转换的封装,让使用者更加简单方便

7.6.1 项目集成

文档地址 集成easyexcel实现excel表格增强

1、dkd-common\pom.xml模块添加整合依赖

1
2
3
4
5
6
7
8
9
<!--  excel处理工具-->
<dependency>
<groupId>com.alibaba</groupId>

<artifactId>easyexcel</artifactId>

<version>4.0.1</version>

</dependency>

2、在dkd-common\模块的ExcelUtil.java新增easyexcel导出导入方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 对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注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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

1
2
3
4
5
6
7
8
9
10
11
/**
* 导入商品管理列表
*/
@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

1
2
3
4
5
6
7
8
9
10
11
/**
* 导出商品管理列表
*/
@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, "商品管理数据");
}

7.7 货道关联商品

7.7.1 需求

对智能售货机内部的货道进行商品摆放的管理

img

img

此功能涉及四个后端接口

  • 查询设备类型(已完成)
  • 查询货道列表(待完成)
  • 查询商品列表(已完成)
  • 货道关联商品(待完成)

7.7.2 货道对话框

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

img

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

img

③ 修改设备index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<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>

7.7.3 查询货道列表

ChannelVo

1
2
3
4
5
6
@Data
public class ChannelVo extends Channel {

// 商品
private Sku sku;
}

ChannelMapper和xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 根据售货机编号查询货道列表
*
* @param innerCode
* @return ChannelVo集合
*/
List<ChannelVo> selectChannelVoListByInnerCode(String innerCode);
<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接口和实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 根据售货机编号查询货道列表
*
* @param innerCode
* @return ChannelVo集合
*/
List<ChannelVo> selectChannelVoListByInnerCode(String innerCode);
/**
* 根据售货机编号查询货道列表
*
* @param innerCode
* @return ChannelVo集合
*/
@Override
public List<ChannelVo> selectChannelVoListByInnerCode(String innerCode) {
return channelMapper.selectChannelVoListByInnerCode(innerCode);
}

ChannelController

1
2
3
4
5
6
7
8
9
/**
* 根据售货机编号查询货道列表
*/
@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.7.4 货道关联商品

ChannelSkuDto

1
2
3
4
5
6
7
8
// 某个货道对应的sku信息
@Data
public class ChannelSkuDto {

private String innerCode;
private String channelCode;
private Long skuId;
}

ChannelConfigDto

1
2
3
4
5
6
7
8
9
// 售货机货道配置
@Data
public class ChannelConfigDto {

// 售货机编号
private String innerCode;
// 货道配置
private List<ChannelSkuDto> channelList;
}

ChannelMapper和xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 根据售货机编号和货道编号查询货道信息
* @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);
<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语句

img

IChannelService接口和实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 货道关联商品
* @param channelConfigDto
* @return 结果
*/
int setChannel(ChannelConfigDto channelConfigDto);
/**
* 货道关联商品
* @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

1
2
3
4
5
6
7
8
9
/**
* 货道关联商品
*/
@PreAuthorize("@ss.hasPermi('manage:channel:edit')")
@Log(title = "售货机货道", businessType = BusinessType.UPDATE)
@PutMapping("/config")
public AjaxResult setChannel(@RequestBody ChannelConfigDto channelConfigDto) {
return toAjax(channelService.setChannel(channelConfigDto));
}

8.工单管理

8.1 需求说明

工单是一种专业名词,是指用于记录、处理、跟踪一项工作的完成情况。

  • 管理人员登录后台系统选择创建工单,在工单类型里选择合适的工单类型,在设备编号里输入正确的设备编号。
  • 工作人员在运营管理App可以看到分配给自己的工单,根据实际情况选择接收工单并完成,或者拒绝/取消工单。

立可得工单分为两大类 :

  • 运营工单:运营人员来维护售货机商品,即补货工单。
  • 运维工单:运维人员来维护售货机设备,即投放工单、撤机工单、维修工单。

工单有四种状态: 1 待处理 2 进行中 3 已取消 4 已完成

img

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

  • 关系字段:task_id、 product_type_id、inner_code、user_id、assignor_id、region_id
  • 数据字典:task_status(1待办、2进行、3取消、4完成)
  • 数据字典:create_type(0自动、1手动)
  • PS:运营的工单包含补货信息,运维工单没有,所以运营工单需要单独创建补货工单

img

创建所有工单,都会在工单表和工单明细表插入记录吗? 创建运维类工单只会在工单表插入数据。 创建运营类工单(补货工单)会在工单表和工单明细表插入数据。 task_code和task_id有什么区别? task_code是工单编号,具有业务规则 ,格式为年月日+当日序号。 task_id 为工单表数据唯一标识。 工单表的user_id和assignor_id分别是做什么的? user_id是工单执行人的id(运维或运营) assignor_id是工单指派人的id(创建工单的人)

8.2基础代码生成(略)

8.3查询工单列表

8.3.1需求

运营和运营工单共享一套后端接口,通过特定的查询条件区分工单类型,并在返回结果中包含工单类型的详细信息

img

img

8.3.2 TaskVo

1
2
3
4
5
6
@Data
public class TaskVo extends Task {

// 工单类型
private TaskType taskType;
}

8.3.3 TaskMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* 查询运维工单列表
*
* @param task 运维工单
* @return TaskVo集合
*/
List<TaskVo> selectTaskVoList(Task task);
<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>

8.3.4 ITaskService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 查询运维工单列表
* @param task
* @return TaskVo集合
*/
List<TaskVo> selectTaskVoList(Task task);
/**
* 查询运维工单列表
* @param task
* @return TaskVo集合
*/
@Override
public List<TaskVo> selectTaskVoList(Task task) {
return taskMapper.selectTaskVoList(task);
}

8.3.5 TaskController

1
2
3
4
5
6
7
8
9
10
11
/**
* 查询工单列表
*/
@PreAuthorize("@ss.hasPermi('manage:task:list')")
@GetMapping("/list")
public TableDataInfo list(Task task)
{
startPage();
List<TaskVo> voList = taskService.selectTaskVoList(task);
return getDataTable(voList);
}

8.4获取运营人员列表

8.4.1需求

根据售货机编号获取负责当前区域下的运营人员列表

img

8.4.2 VendingMachineMapper

1
2
3
4
5
6
7
8
/**
* 根据设备编号查询设备信息
*
* @param innerCode
* @return VendingMachine
*/
@Select("select * from tb_vending_machine where inner_code=#{innerCode}")
VendingMachine selectVendingMachineByInnerCode(String innerCode);

8.4.3 IVendingMachineService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 根据设备编号查询设备信息
*
* @param innerCode
* @return VendingMachine
*/
VendingMachine selectVendingMachineByInnerCode(String innerCode);
/**
* 根据设备编号查询设备信息
*
* @param innerCode
* @return VendingMachine
*/
@Override
public VendingMachine selectVendingMachineByInnerCode(String innerCode) {
return vendingMachineMapper.selectVendingMachineByInnerCode(innerCode);
}

8.4.4 DkdContants

1
2
3
4
5
6
7
8
9
/**
* 员工启用
*/
public static final Long EMP_STATUS_NORMAL = 1L;

/**
* 员工禁用
*/
public static final Long EMP_STATUS_DISABLE = 0L;

8.4.5 EmpController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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));
}

8.5 获取运维人员列表

8.5.1 需求

根据售货机编号获取负责当前区域下的运维人员列表

img

8.5.2 EmpController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 根据售货机获取运维人员列表
*/
@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));
}

8.6 新增工单

8.6.1 需求

本系统中有两类工单需要创建,分别是:

  • 运维工单:运维工单主要是对售货机的操作,又可以细分为投放工单、撤机工单、维修工单
  • 运营工单:运营工单主要是对货物的操作,只有一种就是补货工单

img

8.6.2 思路

新增工单时序图

img

新增工单业务流程图

img

8.6.3 TaskDetailsDto

1
2
3
4
5
6
7
8
9
@Data
public class TaskDetailsDto {

private String channelCode;// 货道编号
private Long expectCapacity;// 期望补货数量
private Long skuId;// 商品Id
private String skuName;// 商品名称
private String skuImage;// 商品图片
}

8.6.4 TaskDto

1
2
3
4
5
6
7
8
9
10
11
@Data
public class TaskDto {

private Long createType;// 创建类型
private String innerCode;// 关联设备编号
private Long userId;// 任务执行人Id
private Long assignorId;// 用户创建人id
private Long productTypeId;// 工单类型
private String desc;// 描述信息
private List<TaskDetailsDto> details;// 工单详情(只有补货工单才涉及)
}

8.6.5 TaskDetailsMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 批量新增工单详情
* @param taskDetailsList
* @return 结果
*/
int batchInsertTaskDetails(List<TaskDetails> taskDetailsList);
<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>

8.6.6 ITaskDetailsService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 批量新增工单详情
* @param taskDetailsList
* @return 结果
*/
int batchInsertTaskDetails(List<TaskDetails> taskDetailsList);
/**
* 批量新增工单详情
* @param taskDetailsList
* @return 结果
*/
@Override
public int batchInsertTaskDetails(List<TaskDetails> taskDetailsList) {
return taskDetailsMapper.batchInsertTaskDetails(taskDetailsList);
}

8.6.7 ITaskService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/**
* 新增运营、运维工单
*
* @param taskDto
* @return 结果
*/
int insertTaskDto(TaskDto taskDto);
@Autowired
private IVendingMachineService vendingMachineService;

@Autowired
private IEmpService empService;

@Autowired
private ITaskDetailsService taskDetailsService;

@Autowired
private RedisTemplate redisTemplate;

/**
* 新增运营、运维工单
*
* @param taskDto
* @return 结果
*/
@Transactional
@Override
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;
}

/**
* 生成并获取当天任务代码的唯一标识。
* 该方法首先尝试从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');
}

/**
* 检查设备是否已有未完成的同类型工单。
* 本方法用于在创建新工单前,验证指定设备是否已经有处于进行中的同类型工单。
* 如果存在未完成的同类型工单,则抛出服务异常,阻止新工单的创建。
*
* @param innerCode 设备的内部编码,用于唯一标识设备。
* @param productTypeId 任务的类型,决定任务的性质(投放、维修、补货、撤机)。
*/
private void hasTask(String innerCode, Long productTypeId) {
// 创建Task对象,并设置设备编号和工单类型ID,以及任务状态为进行中
Task taskParam = new Task();
taskParam.setInnerCode(innerCode);
taskParam.setProductTypeId(productTypeId);
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("该设备状态不是运行中,无法进行撤机");
}
}

8.6.8 TaskController

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 新增工单
*/
@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.7 取消工单

8.7.1 需求

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

img

8.7.2 TaskController

1
2
3
4
5
6
7
8
9
/**
* 取消工单
*/
@PreAuthorize("@ss.hasPermi('manage:task:edit')")
@Log(title = "工单", businessType = BusinessType.UPDATE)
@PutMapping("/cancel")
public AjaxResult cancelTask(@RequestBody Task task) {
return toAjax(taskService.cancelTask(task));
}

8.7.3 ITaskService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 取消工单
* @param task
* @return 结果
*/
int cancelTask(Task task);
/**
* 取消工单
* @param task
* @return 结果
*/
@Override
public int cancelTask(Task task) {
//1. 判断工单状态是否可以取消
// 先根据工单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参数
}

8.8 查询补货详情

8.8.1 需求

运营工单页面可以查看补货详情

img

img

8.8.2 TaskDetailsController

1
2
3
4
5
6
7
8
9
10
/**
* 查看工单补货详情
*/
@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));
}

8.9 Knife4j

如果不习惯使用swagger可以使用前端UI的增强解决方案knife4j,对比swagger相比有以下优势,友好界面,离线文档,接口排序,安全控制,在线调试,文档清晰,注解增强,容易上手。 1、ruoyi-common\pom.xml模块添加整合依赖

1
2
3
4
5
6
7
8
9
<!-- knife4j -->
<dependency>
<groupId>com.github.xiaoymin</groupId>

<artifactId>knife4j-spring-boot-starter</artifactId>

<version>3.0.3</version>

</dependency>

2、views/tool/swagger/index.vue修改跳转访问地址(已完成)

1
const url = ref(import.meta.env.VITE_APP_BASE_API + "/doc.html")

3、登录系统,访问菜单系统工具/系统接口,出现如下图表示成功。

img

4、TaskDetailsController添加swagger注解

  • @Api: 用于类级别,描述API的标签和描述。
  • @ApiOperation: 用于方法级别,描述一个HTTP操作。
  • @ApiParam: 用于参数级别,描述请求参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 工单详情Controller
*
* @author itheima
* @date 2024-07-22
*/
@RestController
@RequestMapping("/manage/taskDetails")
@Api(tags = "工单详情")
public class TaskDetailsController extends BaseController {

/**
* 查看工单补货详情
*/
@ApiOperation("根据工单id查看工单补货详情")
@PreAuthorize("@ss.hasPermi('manage:taskDetails:query')")
@GetMapping(value = "/byTaskId/{taskId}")
public R<List<TaskDetails>> byTaskId(
@ApiParam(value = "工单ID", required = true) @PathVariable Long taskId) {
TaskDetails taskDetailsParam = new TaskDetails();
taskDetailsParam.setTaskId(taskId);
return R.ok(taskDetailsService.selectTaskDetailsList(taskDetailsParam));
}
}

注意:若依框架的AjaxResult由于继承自HashMap导致与Swagger和knife4j不兼容的问题,选择替换返回值类型为R以解决Swagger解析问题,减少整体改动量。

5、TaskDetails实体类添加swagger注解

  • @ApiModelProperty注解来描述每个字段的意义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 工单详情对象 tb_task_details
*
* @author itheima
* @date 2024-07-22
*/
@ApiModel(description = "任务详情对象")
public class TaskDetails extends BaseEntity
{
private static final long serialVersionUID = 1L;

/** $column.columnComment */
@ApiModelProperty(value = "任务详情ID", hidden = true)
private Long detailsId;

/** 工单Id */
@ApiModelProperty(value = "工单Id")
@Excel(name = "工单Id")
private Long taskId;

/** 货道编号 */
@ApiModelProperty(value = "货道编号")
@Excel(name = "货道编号")
private String channelCode;

/** 补货期望容量 */
@ApiModelProperty(value = "补货期望容量")
@Excel(name = "补货期望容量")
private Long expectCapacity;

/** 商品Id */
@ApiModelProperty(value = "商品Id")
@Excel(name = "商品Id")
private Long skuId;

/** $column.columnComment */
@ApiModelProperty(value = "商品名称")
@Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
private String skuName;

/** $column.columnComment */
@ApiModelProperty(value = "商品图片")
@Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
private String skuImage;
}

6、接口测试

img

7、设置文档信息

img

9.运营管理App

9.1 Android模拟器

本项目的App客户端部分已经由前端团队进行开发完成,并且以apk的方式提供出来,供我们测试使用,如果要运行apk,需要先安装安卓的模拟器。

img

9.2 Java后端

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

img

9.3 功能测试

9.3.1 投放工单

帝可得管理端,创建新设备

img

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

img

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

img

img

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

img

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

img

img

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

img

帝可得管理端设备状态改为运营,表示设备投放成功

img

9.3.2 补货工单

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

img

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

img

img

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

img

img

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

img

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

img

img

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

img

数据库货道表的库存已同步更新

img

9.4 源码介绍

技术栈:SpringBoot+MybatisPlus+阿里云短信

img

10.设备屏幕端

商品列表–选择支付方式–显示支付二维码–用户扫码完成支付

img

10.1设备屏幕

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

10.2 Java后端

img

10.3功能测试

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

img

帝可得管理端,设备策略分配,设置折扣信息

img

再次访问设备屏幕端,价格就是折扣的了

img

10.4支付出货流程

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

img

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

img

10.5源码介绍

设备屏幕端的java后端技术栈:SpringBoot+MybatisPlus

img

11.订单管理

11.1 业务说明

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

img

对于订单数据模型,下面是示意图:

img

11.2代码实现

OrderController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
package com.dkd.manage.controller;

import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.dkd.common.annotation.Log;
import com.dkd.common.core.controller.BaseController;
import com.dkd.common.core.domain.AjaxResult;
import com.dkd.common.enums.BusinessType;
import com.dkd.manage.domain.Order;
import com.dkd.manage.service.IOrderService;
import com.dkd.common.utils.poi.ExcelUtil;
import com.dkd.common.core.page.TableDataInfo;

/**
* 订单管理Controller
*
* @author itheima
* @date 2024-07-29
*/
@RestController
@RequestMapping("/manage/order")
public class OrderController extends BaseController
{
@Autowired
private IOrderService orderService;

/**
* 查询订单管理列表
*/
@PreAuthorize("@ss.hasPermi('manage:order:list')")
@GetMapping("/list")
public TableDataInfo list(Order order)
{
startPage();
List<Order> list = orderService.selectOrderList(order);
return getDataTable(list);
}

/**
* 导出订单管理列表
*/
@PreAuthorize("@ss.hasPermi('manage:order:export')")
@Log(title = "订单管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, Order order)
{
List<Order> list = orderService.selectOrderList(order);
ExcelUtil<Order> util = new ExcelUtil<Order>(Order.class);
util.exportExcel(response, list, "订单管理数据");
}

/**
* 获取订单管理详细信息
*/
@PreAuthorize("@ss.hasPermi('manage:order:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id)
{
return success(orderService.selectOrderById(id));
}

/**
* 新增订单管理
*/
@PreAuthorize("@ss.hasPermi('manage:order:add')")
@Log(title = "订单管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody Order order)
{
return toAjax(orderService.insertOrder(order));
}

/**
* 修改订单管理
*/
@PreAuthorize("@ss.hasPermi('manage:order:edit')")
@Log(title = "订单管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody Order order)
{
return toAjax(orderService.updateOrder(order));
}

/**
* 删除订单管理
*/
@PreAuthorize("@ss.hasPermi('manage:order:remove')")
@Log(title = "订单管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids)
{
return toAjax(orderService.deleteOrderByIds(ids));
}
}

Order实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
package com.dkd.manage.domain;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.dkd.common.annotation.Excel;
import com.dkd.common.core.domain.BaseEntity;

/**
* 订单管理对象 tb_order
*
* @author itheima
* @date 2024-07-29
*/
public class Order extends BaseEntity
{
private static final long serialVersionUID = 1L;

/** 主键 */
private Long id;

/** 订单编号 */
@Excel(name = "订单编号")
private String orderNo;

/** 第三方平台单号 */
private String thirdNo;

/** 机器编号 */
private String innerCode;

/** 货道编号 */
private String channelCode;

/** skuId */
private Long skuId;

/** 商品名称 */
@Excel(name = "商品名称")
private String skuName;

/** 商品类别Id */
private Long classId;

/** 订单状态:0-待支付;1-支付完成;2-出货成功;3-出货失败;4-已取消 */
@Excel(name = "订单状态:0-待支付;1-支付完成;2-出货成功;3-出货失败;4-已取消")
private Long status;

/** 支付金额 */
@Excel(name = "支付金额")
private Long amount;

/** 商品金额 */
private Long price;

/** 支付类型,1支付宝 2微信 */
private String payType;

/** 支付状态,0-未支付;1-支付完成;2-退款中;3-退款完成 */
private Long payStatus;

/** 合作商账单金额 */
private Long bill;

/** 点位地址 */
private String addr;

/** 所属区域Id */
private Long regionId;

/** 区域名称 */
private String regionName;

/** 所属商圈 */
private Long businessType;

/** 合作商Id */
private Long partnerId;

/** 跨站身份验证 */
private String openId;

/** 点位Id */
private Long nodeId;

/** 点位名称 */
private String nodeName;

/** 取消原因 */
private String cancelDesc;

public void setId(Long id)
{
this.id = id;
}

public Long getId()
{
return id;
}
public void setOrderNo(String orderNo)
{
this.orderNo = orderNo;
}

public String getOrderNo()
{
return orderNo;
}
public void setThirdNo(String thirdNo)
{
this.thirdNo = thirdNo;
}

public String getThirdNo()
{
return thirdNo;
}
public void setInnerCode(String innerCode)
{
this.innerCode = innerCode;
}

public String getInnerCode()
{
return innerCode;
}
public void setChannelCode(String channelCode)
{
this.channelCode = channelCode;
}

public String getChannelCode()
{
return channelCode;
}
public void setSkuId(Long skuId)
{
this.skuId = skuId;
}

public Long getSkuId()
{
return skuId;
}
public void setSkuName(String skuName)
{
this.skuName = skuName;
}

public String getSkuName()
{
return skuName;
}
public void setClassId(Long classId)
{
this.classId = classId;
}

public Long getClassId()
{
return classId;
}
public void setStatus(Long status)
{
this.status = status;
}

public Long getStatus()
{
return status;
}
public void setAmount(Long amount)
{
this.amount = amount;
}

public Long getAmount()
{
return amount;
}
public void setPrice(Long price)
{
this.price = price;
}

public Long getPrice()
{
return price;
}
public void setPayType(String payType)
{
this.payType = payType;
}

public String getPayType()
{
return payType;
}
public void setPayStatus(Long payStatus)
{
this.payStatus = payStatus;
}

public Long getPayStatus()
{
return payStatus;
}
public void setBill(Long bill)
{
this.bill = bill;
}

public Long getBill()
{
return bill;
}
public void setAddr(String addr)
{
this.addr = addr;
}

public String getAddr()
{
return addr;
}
public void setRegionId(Long regionId)
{
this.regionId = regionId;
}

public Long getRegionId()
{
return regionId;
}
public void setRegionName(String regionName)
{
this.regionName = regionName;
}

public String getRegionName()
{
return regionName;
}
public void setBusinessType(Long businessType)
{
this.businessType = businessType;
}

public Long getBusinessType()
{
return businessType;
}
public void setPartnerId(Long partnerId)
{
this.partnerId = partnerId;
}

public Long getPartnerId()
{
return partnerId;
}
public void setOpenId(String openId)
{
this.openId = openId;
}

public String getOpenId()
{
return openId;
}
public void setNodeId(Long nodeId)
{
this.nodeId = nodeId;
}

public Long getNodeId()
{
return nodeId;
}
public void setNodeName(String nodeName)
{
this.nodeName = nodeName;
}

public String getNodeName()
{
return nodeName;
}
public void setCancelDesc(String cancelDesc)
{
this.cancelDesc = cancelDesc;
}

public String getCancelDesc()
{
return cancelDesc;
}

@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("id", getId())
.append("orderNo", getOrderNo())
.append("thirdNo", getThirdNo())
.append("innerCode", getInnerCode())
.append("channelCode", getChannelCode())
.append("skuId", getSkuId())
.append("skuName", getSkuName())
.append("classId", getClassId())
.append("status", getStatus())
.append("amount", getAmount())
.append("price", getPrice())
.append("payType", getPayType())
.append("payStatus", getPayStatus())
.append("bill", getBill())
.append("addr", getAddr())
.append("regionId", getRegionId())
.append("regionName", getRegionName())
.append("businessType", getBusinessType())
.append("partnerId", getPartnerId())
.append("openId", getOpenId())
.append("nodeId", getNodeId())
.append("nodeName", getNodeName())
.append("cancelDesc", getCancelDesc())
.append("createTime", getCreateTime())
.append("updateTime", getUpdateTime())
.toString();
}
}

IOrderService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.dkd.manage.service;

import java.util.List;
import com.dkd.manage.domain.Order;

/**
* 订单管理Service接口
*
* @author itheima
* @date 2024-07-29
*/
public interface IOrderService
{
/**
* 查询订单管理
*
* @param id 订单管理主键
* @return 订单管理
*/
public Order selectOrderById(Long id);

/**
* 查询订单管理列表
*
* @param order 订单管理
* @return 订单管理集合
*/
public List<Order> selectOrderList(Order order);

/**
* 新增订单管理
*
* @param order 订单管理
* @return 结果
*/
public int insertOrder(Order order);

/**
* 修改订单管理
*
* @param order 订单管理
* @return 结果
*/
public int updateOrder(Order order);

/**
* 批量删除订单管理
*
* @param ids 需要删除的订单管理主键集合
* @return 结果
*/
public int deleteOrderByIds(Long[] ids);

/**
* 删除订单管理信息
*
* @param id 订单管理主键
* @return 结果
*/
public int deleteOrderById(Long id);
}

1OrderServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package com.dkd.manage.service.impl;

import java.util.List;
import com.dkd.common.utils.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.dkd.manage.mapper.OrderMapper;
import com.dkd.manage.domain.Order;
import com.dkd.manage.service.IOrderService;

/**
* 订单管理Service业务层处理
*
* @author itheima
* @date 2024-07-29
*/
@Service
public class OrderServiceImpl implements IOrderService
{
@Autowired
private OrderMapper orderMapper;

/**
* 查询订单管理
*
* @param id 订单管理主键
* @return 订单管理
*/
@Override
public Order selectOrderById(Long id)
{
return orderMapper.selectOrderById(id);
}

/**
* 查询订单管理列表
*
* @param order 订单管理
* @return 订单管理
*/
@Override
public List<Order> selectOrderList(Order order)
{
return orderMapper.selectOrderList(order);
}

/**
* 新增订单管理
*
* @param order 订单管理
* @return 结果
*/
@Override
public int insertOrder(Order order)
{
order.setCreateTime(DateUtils.getNowDate());
return orderMapper.insertOrder(order);
}

/**
* 修改订单管理
*
* @param order 订单管理
* @return 结果
*/
@Override
public int updateOrder(Order order)
{
order.setUpdateTime(DateUtils.getNowDate());
return orderMapper.updateOrder(order);
}

/**
* 批量删除订单管理
*
* @param ids 需要删除的订单管理主键
* @return 结果
*/
@Override
public int deleteOrderByIds(Long[] ids)
{
return orderMapper.deleteOrderByIds(ids);
}

/**
* 删除订单管理信息
*
* @param id 订单管理主键
* @return 结果
*/
@Override
public int deleteOrderById(Long id)
{
return orderMapper.deleteOrderById(id);
}
}

mapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.dkd.manage.mapper;

import java.util.List;
import com.dkd.manage.domain.Order;

/**
* 订单管理Mapper接口
*
* @author itheima
* @date 2024-07-29
*/
public interface OrderMapper
{
/**
* 查询订单管理
*
* @param id 订单管理主键
* @return 订单管理
*/
public Order selectOrderById(Long id);

/**
* 查询订单管理列表
*
* @param order 订单管理
* @return 订单管理集合
*/
public List<Order> selectOrderList(Order order);

/**
* 新增订单管理
*
* @param order 订单管理
* @return 结果
*/
public int insertOrder(Order order);

/**
* 修改订单管理
*
* @param order 订单管理
* @return 结果
*/
public int updateOrder(Order order);

/**
* 删除订单管理
*
* @param id 订单管理主键
* @return 结果
*/
public int deleteOrderById(Long id);

/**
* 批量删除订单管理
*
* @param ids 需要删除的数据主键集合
* @return 结果
*/
public int deleteOrderByIds(Long[] ids);
}

mapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
<resultMap type="Order" id="OrderResult">
<result property="id" column="id" />
<result property="orderNo" column="order_no" />
<result property="thirdNo" column="third_no" />
<result property="innerCode" column="inner_code" />
<result property="channelCode" column="channel_code" />
<result property="skuId" column="sku_id" />
<result property="skuName" column="sku_name" />
<result property="classId" column="class_id" />
<result property="status" column="status" />
<result property="amount" column="amount" />
<result property="price" column="price" />
<result property="payType" column="pay_type" />
<result property="payStatus" column="pay_status" />
<result property="bill" column="bill" />
<result property="addr" column="addr" />
<result property="regionId" column="region_id" />
<result property="regionName" column="region_name" />
<result property="businessType" column="business_type" />
<result property="partnerId" column="partner_id" />
<result property="openId" column="open_id" />
<result property="nodeId" column="node_id" />
<result property="nodeName" column="node_name" />
<result property="cancelDesc" column="cancel_desc" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
</resultMap>

<sql id="selectOrderVo">
select id, order_no, third_no, inner_code, channel_code, sku_id, sku_name, class_id, status, amount, price, pay_type, pay_status, bill, addr, region_id, region_name, business_type, partner_id, open_id, node_id, node_name, cancel_desc, create_time, update_time from tb_order
</sql>

<select id="selectOrderList" parameterType="Order" resultMap="OrderResult">
<include refid="selectOrderVo"/>
<where>
<if test="orderNo != null and orderNo != ''"> and order_no = #{orderNo}</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
AND date_format(create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
</if>
<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
AND date_format(create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
</if>
</where>
</select>

<select id="selectOrderById" parameterType="Long" resultMap="OrderResult">
<include refid="selectOrderVo"/>
where id = #{id}
</select>

<insert id="insertOrder" parameterType="Order">
insert into tb_order
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if>
<if test="orderNo != null">order_no,</if>
<if test="thirdNo != null">third_no,</if>
<if test="innerCode != null">inner_code,</if>
<if test="channelCode != null">channel_code,</if>
<if test="skuId != null">sku_id,</if>
<if test="skuName != null">sku_name,</if>
<if test="classId != null">class_id,</if>
<if test="status != null">status,</if>
<if test="amount != null">amount,</if>
<if test="price != null">price,</if>
<if test="payType != null">pay_type,</if>
<if test="payStatus != null">pay_status,</if>
<if test="bill != null">bill,</if>
<if test="addr != null">addr,</if>
<if test="regionId != null">region_id,</if>
<if test="regionName != null">region_name,</if>
<if test="businessType != null">business_type,</if>
<if test="partnerId != null">partner_id,</if>
<if test="openId != null">open_id,</if>
<if test="nodeId != null">node_id,</if>
<if test="nodeName != null">node_name,</if>
<if test="cancelDesc != null">cancel_desc,</if>
<if test="createTime != null">create_time,</if>
<if test="updateTime != null">update_time,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if>
<if test="orderNo != null">#{orderNo},</if>
<if test="thirdNo != null">#{thirdNo},</if>
<if test="innerCode != null">#{innerCode},</if>
<if test="channelCode != null">#{channelCode},</if>
<if test="skuId != null">#{skuId},</if>
<if test="skuName != null">#{skuName},</if>
<if test="classId != null">#{classId},</if>
<if test="status != null">#{status},</if>
<if test="amount != null">#{amount},</if>
<if test="price != null">#{price},</if>
<if test="payType != null">#{payType},</if>
<if test="payStatus != null">#{payStatus},</if>
<if test="bill != null">#{bill},</if>
<if test="addr != null">#{addr},</if>
<if test="regionId != null">#{regionId},</if>
<if test="regionName != null">#{regionName},</if>
<if test="businessType != null">#{businessType},</if>
<if test="partnerId != null">#{partnerId},</if>
<if test="openId != null">#{openId},</if>
<if test="nodeId != null">#{nodeId},</if>
<if test="nodeName != null">#{nodeName},</if>
<if test="cancelDesc != null">#{cancelDesc},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateTime != null">#{updateTime},</if>
</trim>
</insert>

<update id="updateOrder" parameterType="Order">
update tb_order
<trim prefix="SET" suffixOverrides=",">
<if test="orderNo != null">order_no = #{orderNo},</if>
<if test="thirdNo != null">third_no = #{thirdNo},</if>
<if test="innerCode != null">inner_code = #{innerCode},</if>
<if test="channelCode != null">channel_code = #{channelCode},</if>
<if test="skuId != null">sku_id = #{skuId},</if>
<if test="skuName != null">sku_name = #{skuName},</if>
<if test="classId != null">class_id = #{classId},</if>
<if test="status != null">status = #{status},</if>
<if test="amount != null">amount = #{amount},</if>
<if test="price != null">price = #{price},</if>
<if test="payType != null">pay_type = #{payType},</if>
<if test="payStatus != null">pay_status = #{payStatus},</if>
<if test="bill != null">bill = #{bill},</if>
<if test="addr != null">addr = #{addr},</if>
<if test="regionId != null">region_id = #{regionId},</if>
<if test="regionName != null">region_name = #{regionName},</if>
<if test="businessType != null">business_type = #{businessType},</if>
<if test="partnerId != null">partner_id = #{partnerId},</if>
<if test="openId != null">open_id = #{openId},</if>
<if test="nodeId != null">node_id = #{nodeId},</if>
<if test="nodeName != null">node_name = #{nodeName},</if>
<if test="cancelDesc != null">cancel_desc = #{cancelDesc},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
</trim>
where id = #{id}
</update>

<delete id="deleteOrderById" parameterType="Long">
delete from tb_order where id = #{id}
</delete>

<delete id="deleteOrderByIds" parameterType="String">
delete from tb_order where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>

实战篇完结