文章目录
- 一、商品管理
- 1、需求说明
- 2、生成基础代码
- (1)创建目录菜单
- (2)配置代码生成信息
- (3)下载代码并导入项目
- 3、商品类型改造
- (1)基础页面
- 4、商品管理改造
- (1)基础页面
- (2)商品删除
- 5、商品批量导入
- (1)前端实现
- (2)后端实现
- 6、EasyExcel
- (1)介绍
- (2)项目集成
- 7、货道关联商品
- (1)货道对话框
- (2)查询货道列表
- (3)货道关联商品
- 二、工单管理
- 1、需求说明
- 2、生成基础代码
- (1)创建目录菜单
- (2)添加数据字典
- (3)配置代码生成信息
- (4)下载代码并导入项目
- (5)配置工单前端代码
- (6)手动创建二级菜单
- 3、查询工单列表
- 4、获取运营人员列表
- 5、获取运维人员列表
- 6、新增工单
- 7、取消工单
- 8、查看补货详情
- 9、Knife4j
- 三、运营管理App
- 1、Android模拟器
- 2、Java后端
- 3、功能测试
- (1)运维工单
- (2)补货工单
- 四、设备屏幕端
- 1、设备屏幕
- 2、Java后端
- 3、功能测试
- 4、支付出货流程
本章为基于若依开发帝可得项目实战的最后一章,主要完成商品管理、订单管理(帝可得项目的核心模块)、帝可得运营APP、设备屏幕端的开发与测试。
一、商品管理
业务场景:智能售货机的货道管理、商品类型以及具体商品信息的管理。
1、需求说明
商品管理主要涉及到三个功能模块,业务流程如下:
- 新增商品类型:定义商品的不同分类,如饮料、零食、日用品等。
- 新增商品:添加新的商品信息,包括名称、规格、价格、类型等。
- 设备货道管理:将商品与售货机的货道关联,管理每个货道的商品信息。
对于设备和其他管理数据,下面是示意图:
- 关系字段:vm_type_id、node_id、vm_id
- 数据字典:vm_status(0未投放、1运营、3撤机)
- 冗余字段:addr、business_type、region_id、partner_id(简化查询接口、提高查询效率)
2、生成基础代码
需求:使用若依代码生成器,生成商品类型、商品管理前后端基础代码,并导入到项目中。
(1)创建目录菜单
创建商品管理目录菜单
(2)配置代码生成信息
在代码生成中导入商品表tb_sku、商品类型表tb_sku_class
配置商品类型表(参考原型)
配置商品表(参考原型)
(3)下载代码并导入项目
选中商品表和商品类型表生成下载,解压ruoyi.zip得到前后端代码和动态菜单sql,将代码导入到项目中。
3、商品类型改造
(1)基础页面
- 需求:参考页面原型,完成基础布局展示改造。
由于数据库字段没有创建日期字段,因此页面不做展示。
- 代码实现
在skuClass/index.vue视图组件中修改
<!-- 列表展示 -->
<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>
由于我们在数据库中为商品类型设置了唯一约束,在添加商品类型时,为防止管理员重复添加相同的数据,需要在全局异常处理器中给出补充提示。
/**
* 数据完整性异常
*/
@ExceptionHandler(DataIntegrityViolationException.class)
public AjaxResult handleDataIntegrityViolationException(DataIntegrityViolationException e) {
log.error(e.getMessage(), e);
if (e.getMessage().contains("foreign")) {
return AjaxResult.error("外键约束异常,无法删除,有其他数据引用");
}else if (e.getMessage().contains("Duplicate")) {
return AjaxResult.error("保存失败,数据重复已存在,请保证数据唯一性");
}
return AjaxResult.error("数据完整性异常,您的操作违反了数据库中的完整性约束");
}
4、商品管理改造
(1)基础页面
- 需求:参考页面原型,完成基础布局展示改造。
- 代码实现
在sku/index.vue视图组件中修改
<!-- 查询条件 -->
<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.classId">
<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-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>
- 测试商品列表
- 测试新增和修改商品
注意数据库存储的商品价格单位是分,因为考虑到float和double计算会丢失精度,因此数据库存储是以分为单位,前端页面展示是以元为单位。
前端在提交到数据存储到数据库前,将价格单位从元转换回分。
从数据库查询回显时,将价格单位从分转换为元。
(2)商品删除
需求:在删除商品时,需要判断此商品是否被售货机的货道关联,如果关联则无法删除。
- 物理外键约束:通过在子表中添加一个外键列和约束,该列与父表的主键列相关联,由数据库维护数据的一致性和完整性
- 逻辑外键约束:在不使用数据库外键约束的情况下,通常在应用程序中通过代码来检查和维护数据的一致性和完整性
使用逻辑外键约束的原因:我们在新增售货机货道记录时暂不指定商品,货道表中的SKU_ID有默认值0,而这个值在商品表中并不存在,那么物理外键约束会阻止货道表的插入,因为0并不指向任何有效的商品记录。
新创建出来的货道关联的商品id默认都为0(表示该货道未关联商品),但由于物理外键约束存在,将无法在货道表插入新sku_id。
因此我们需要编写一个逻辑外键约束,在删除商品(前端传入的是要删除的商品id集合)时,判断商品是否与货道关联,如果已tb_channel.sku_id=tb_sku.sku_id,则给出提示无法删除,否则可以执行删除操作。
SkuServiceImpl:
@Autowired
private IChannelService channelService;
/**
* 批量删除商品管理
*
* @param skuIds 需要删除的商品管理主键
* @return 结果
*/
@Override
public int deleteSkuBySkuIds(Long[] skuIds)
{
// 判断商品id集合是否有关联货道,如果有一个商品关联了货道,阻止删除并抛出异常
int count = channelService.countChannelBySkuIds(skuIds);
if (count > 0) throw new ServiceException("此商品被货道关联,无法删除");
// 没有关联货道,执行删除
return skuMapper.deleteSkuBySkuIds(skuIds);
}
IChannelService和ChannelServiceImpl:
/**
* 根据商品id集合统计货道数量
* @param skuIds
* @return 统计结果
*/
int countChannelBySkuIds(Long[] skuIds);
/**
* 根据商品id集合统计货道数量
* @param skuIds
* @return 统计结果
*/
@Override
public int countChannelBySkuIds(Long[] skuIds) {
return channelMapper.countChannelBySkuIds(skuIds);
}
ChannelMapper接口和xml:
/**
* 根据商品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>
- 测试商品删除
5、商品批量导入
需求:点击导入数据弹出导入数据弹窗,上传合法Excel文件,实现商品的批量导入。
- 页面原型
- 接口文档
注意:请求头Headers里需要携带Authorization权限校验信息,才能进行文件上传。
支持Excel单文件上传,实现商品信息的批量导入。
- 实现细节说明
对于前后端分离项目,都会存在一个跨域请求的问题。若依在 vite.config.js 中配置了代理转发,每个前端请求的前缀都需要有 /dev-api ,才能被代理到目标服务器的8080端口上,路由重写中将/dev-api替换为空字符串。
我们在实现前端发送请求时,不需要将开发环境前缀 /dev-api 硬编码拼接到请求地址中,可以使用 .env.development 中预定义好的 VITE_APP_BASE_API 变量作为baseUrl进行请求地址的拼接。
若依在 utils/request.js 请求工具类的api中,为每次请求的请求头headers里都携带了 Authorization(="Bearer " + token),我们的接口文档中也要求有这个权限校验信息。
因此我们可以借鉴若依的写法来构造我们的文件上传请求信息。
<script setup name="Sku">
import { getToken } from "@/utils/auth";
/* 上传地址 */
const uploadExcelUrl = ref(import.meta.env.VITE_APP_BASE_API + "/manage/sku/import"); // 上传excel文件地址
/* 上传请求头 */
const headers = ref({ Authorization: "Bearer " + getToken() });
</script>
(1)前端实现
在sku/index.vue视图组件中修改
<!-- 导入按钮-->
<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="importOpen" 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 importOpen = ref(false);
function handleExcelImport() {
importOpen.value = true;
}
/* 上传excel */
const uploadRef = ref({});
function submitUpload() {
uploadRef.value.submit()
}
/* 上传地址 */
const uploadExcelUrl = ref(import.meta.env.VITE_APP_BASE_API + "/manage/sku/import"); // 上传excel文件地址
/* 上传请求头 */
const headers = ref({ Authorization: "Bearer " + getToken() });
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 handleUploadSuccess(res, file) {
if (res.code === 200) {
proxy.$modal.msgSuccess("上传excel成功");
excelOpen.value = false;
getList();
}else{
proxy.$modal.msgError(res.msg);
}
// 清空文件上传列表记录
uploadRef.value.clearFiles();
// 关闭正在上传的loading提示信息
proxy.$modal.closeLoading();
}
// 上传失败
function handleUploadError() {
proxy.$modal.msgError("上传excel失败");
// 清空文件上传列表记录
uploadRef.value.clearFiles();
// 关闭正在上传的loading提示信息
proxy.$modal.closeLoading();
}
</script>
- 测试前端上传,上传合法的Excel文件,前端状态码200,后端响应状态码500(因为后端还没有编写)
并且成功在headers中携带了拼接好的token。
- 测试上传不合法文件,提示上传失败,并限制不能上传多个文件。
对于不合法的文件直接在前端拦截,并在上传成功或失败后都清空文件上传列表。
(2)后端实现
- SkuController
/**
* 导入商品管理列表
*/
@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和xml
/**
* 批量新增商品管理
* @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和SkuServiceImpl
/**
* 批量新增商品管理
* @param skuList
* @return 结果
*/
public int insertSkus(List<Sku> skuList);
/**
* 批量新增商品管理
* @param skuList
* @return 结果
*/
@Override
public int insertSkus(List<Sku> skuList) {
return skuMapper.insertSkus(skuList);
}
6、EasyExcel
(1)介绍
官方地址: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模式,在上层做了模型转换的封装,让使用者更加简单方便
(2)项目集成
若依插件集成地址:https://doc.ruoyi.vip/ruoyi-vue/document/cjjc.html#%E9%9B%86%E6%88%90easyexcel%E5%AE%9E%E7%8E%B0excel%E8%A1%A8%E6%A0%BC%E5%A2%9E%E5%BC%BA
dkd-common\pom.xml
模块添加整合依赖。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>4.0.3</version>
</dependency>
- 在dkd-common模块的ExcelUtil.java新增easyexcel导出导入方法。
/**
* 对excel表单默认第一个索引名转换成list(EasyExcel)
*
* @param is 输入流
* @return 转换后集合
*/
public List<T> importEasyExcel(InputStream is) throws Exception
{
return EasyExcel.read(is).head(clazz).sheet().doReadSync();
}
/**
* 对list数据源将其里面的数据导入到excel表单(EasyExcel)
*
* @param list 导出数据集合
* @param sheetName 工作表的名称
* @return 结果
*/
public void exportEasyExcel(HttpServletResponse response, List<T> list, String sheetName)
{
try
{
EasyExcel.write(response.getOutputStream(), clazz).sheet(sheetName).doWrite(list);
}
catch (IOException e)
{
log.error("导出EasyExcel异常{}", e.getMessage());
}
}
- Sku.java修改为
@ExcelProperty
注解
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 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_sku
*
* @author Aizen
* @date 2024-09-21
*/
@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;
// 其他略...
}
- SkuController.java改为importEasyExcel
/**
* 导入商品管理列表
*/
@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));
}
- SkuController.java改为
exportEasyExcel
/**
* 导出商品管理列表
*/
@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, "商品管理数据");
}
- 搜索可口可乐,测试导出功能
- 将xlsx文件中的可口可乐改为可口可乐plus,测试导入功能。
7、货道关联商品
需求:管理员对智能售货机内部的货道进行商品摆放的管理。
- 页面原型
此功能涉及四个后端接口
- 查询设备类型(已完成)
- 查询货道列表(待完成)
- 查询商品列表(已完成)
- 货道关联商品(待完成)
(1)货道对话框
此部分涉及到前端CSS样式美化和组件的编写,只靠若依生成无法完成,需要自己编写前端组件。
api/manage/channel.js
import request from '@/utils/request';
// 查询货道列表
export function getGoodsList(innerCode) {
return request({
url: '/manage/channel/list/' + innerCode,
method: 'get',
});
}
// 查询设备类型
export function getGoodsType(typeId) {
return request({
url: '/manage/vmType/' + typeId,
method: 'get',
});
}
// 提交获取的货道
export function channelConfig(data) {
return request({
url: '/manage/channel/config',
method: 'put',
data: data,
});
}
views/manage/vm/components/ChannelDialog.vue
<template>
<!-- 货道弹层 -->
<el-dialog
width="940px"
title="货道设置"
v-model="visible"
:close-on-click-modal="false"
:close-on-press-escape="false"
@open="handleGoodOpen"
@close="handleGoodcClose"
>
<div class="vm-config-channel-dialog-wrapper">
<div class="channel-basic">
<span class="vm-row">货道行数:{{ vmType.vmRow }}</span>
<span class="vm-col">货道列数:{{ vmType.vmCol }}</span>
<span class="channel-max-capacity"
>货道容量(个):{{ vmType.channelMaxCapacity }}</span
>
</div>
<el-scrollbar ref="scroll" v-loading="listLoading" class="scrollbar">
<el-row
v-for="vmRowIndex in vmType.vmRow"
:key="vmRowIndex"
type="flex"
:gutter="16"
class="space"
>
<el-col
v-for="vmColIndex in vmType.vmCol"
:key="vmColIndex"
:span="vmType.vmCol <= 5 ? 5 : 12"
>
<ChannelDialogItem
:current-index="computedCurrentIndex(vmRowIndex, vmColIndex)"
:channel="channels[computedCurrentIndex(vmRowIndex, vmColIndex)]"
@openSetSkuDialog="openSetSkuDialog"
@openRemoveSkuDialog="openRemoveSkuDialog"
>
</ChannelDialogItem>
</el-col>
</el-row>
</el-scrollbar>
<el-icon
v-if="vmType.vmCol > 5"
class="arrow arrow-left"
:class="scrollStatus === 'LEFT' ? 'disabled' : ''"
@click="handleClickPrevButton"
><ArrowLeft
/></el-icon>
<el-icon
v-if="vmType.vmCol > 5"
class="arrow arrow-right"
:class="scrollStatus === 'RIGHT' ? 'disabled' : ''"
@click="handleClickNextButton"
><ArrowRight
/></el-icon>
</div>
<div class="dialog-footer">
<el-button
type="primary"
class="el-button--primary1"
@click="handleClick"
>
确认
</el-button>
</div>
<!-- 商品选择 -->
<el-dialog
width="858px"
title="选择商品"
v-model="skuDialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
append-to-body
@open="handleListOpen"
@close="handleListClose"
>
<div class="vm-select-sku-dialog-wrapper">
<!-- 搜索区 -->
<el-form
ref="form"
class="search"
:model="listQuery"
:label-width="formLabelWidth"
>
<el-form-item label="商品名称:">
<el-row type="flex" justify="space-between">
<el-col>
<el-input
v-model="listQuery.skuName"
placeholder="请输入"
clearable
class="sku-name"
@input="resetPageIndex"
/>
</el-col>
<el-col>
<el-button
type="primary"
class="el-button--primary1"
@click="handleListOpen"
>
<el-icon><Search /></el-icon> 查询
</el-button>
</el-col>
</el-row>
</el-form-item>
</el-form>
<el-scrollbar
ref="scroll2"
v-loading="listSkuLoading"
class="scrollbar"
>
<el-row v-loading="listSkuLoading" :gutter="20">
<el-col
v-for="(item, index) in listSkuData.rows"
:key="index"
:span="5"
>
<div class="item">
<!-- TODO: 只有一行的时候考虑 -->
<div
class="sku"
:class="index < 5 ? 'space' : ''"
@click="handleCurrentChange(index)"
>
<img
v-show="currentRow.skuId === item.skuId"
class="selected"
src="@/assets/vm/selected.png"
/>
<img class="img" :src="item.skuImage" />
<div class="name" :title="item.skuName">
{{ item.skuName }}
</div>
</div>
</div>
</el-col>
</el-row>
</el-scrollbar>
<el-icon
v-if="pageCount > 1"
class="arrow arrow-left"
:class="pageCount === 1 ? 'disabled' : ''"
@click="handleClickPrev"
><ArrowLeft
/></el-icon>
<el-icon
v-if="pageCount > 1"
class="arrow arrow-right"
:class="listQuery.pageIndex === pageCount ? 'disabled' : ''"
@click="handleClickNext"
><ArrowRight
/></el-icon>
</div>
<div class="dialog-footer">
<el-button
type="primary"
class="el-button--primary1"
@click="handleSelectClick"
>
确认
</el-button>
</div>
</el-dialog>
<!-- end -->
</el-dialog>
<!-- end -->
</template>
<script setup>
import { require } from '@/utils/validate';
const { proxy } = getCurrentInstance();
// 滚动插件
import { ElScrollbar } from 'element-plus';
// 接口
import {
getGoodsList,
getGoodsType,
channelConfig,
} from '@/api/manage/channel';
import { listSku } from '@/api/manage/sku';
// 内部组件
import ChannelDialogItem from './ChannelDialogItem.vue';
import { watch } from 'vue';
// 获取父组件参数
const props = defineProps({
// 弹层隐藏显示
goodVisible: {
type: Boolean,
default: false,
},
// 触发的货道信息
goodData: {
type: Object,
default: () => {},
},
});
// 获取父组件的方法
const emit = defineEmits(['handleCloseGood']);
// ******定义变量******
const visible = ref(false); //货道弹层显示隐藏
const scrollStatus = ref('LEFT');
const listLoading = ref(false);
const vmType = ref({}); //获取货道基本信息
const channels = ref({}); //货道数据
const scroll = ref(null); //滚动条ref
// 监听货道弹层显示/隐藏
watch(
() => props.goodVisible,
(val) => {
visible.value = val;
}
);
// ******定义方法******
// 获取货道基本信息
const handleGoodOpen = () => {
getVmType();
channelList();
};
// 获取货道基本信息
const getVmType = async () => {
const { data } = await getGoodsType(props.goodData.vmTypeId);
vmType.value = data;
};
// 获取货道列表
const channelList = async () => {
listLoading.value = true;
const { data } = await getGoodsList(props.goodData.innerCode);
channels.value = data;
listLoading.value = false;
};
const computedCurrentIndex = (vmRowIndex, vmColIndex) => {
return (vmRowIndex - 1) * vmType.value.vmCol + vmColIndex - 1;
};
// 关闭货道弹窗
const handleGoodcClose = () => {
visible.value = false
emit('handleCloseGood');
};
const handleClickPrevButton = () => {
scroll.value.wrapRef.scrollLeft = 0;
scrollStatus.value = 'LEFT';
};
const handleClickNextButton = () => {
scroll.value.wrapRef.scrollLeft = scroll.value.wrapRef.scrollWidth;
scrollStatus.value = 'RIGHT';
};
const currentIndex = ref(0);
const channelCode = ref('');
const skuDialogVisible = ref(false); //添加商品弹层
// 删除选中的商品
const openRemoveSkuDialog = (index, code) => {
currentIndex.value = index;
channelCode.value = code;
channels.value[currentIndex.value].skuId = '0';
channels.value[currentIndex.value].sku = undefined;
};
// 添加商品
const listQuery = ref({
pageIndex: 1,
pageSize: 10,
}); //搜索商品
const listSkuLoading = ref(false); //商品列表loading
const listSkuData = ref({}); //商品数据
const currentRow = ref({});
const pageCount = ref(0); //总页数
const channelModelView = ref({});
// 商品弹层列表
const handleListOpen = async () => {
listSkuLoading.value = true;
listQuery.value.skuName = listQuery.value.skuName || undefined;
const data = await listSku(listQuery.value);
listSkuData.value = data;
pageCount.value = Math.ceil(data.total / 10);
listSkuLoading.value = false;
};
// 打开商品选择弹层
const openSetSkuDialog = (index, code) => {
currentIndex.value = index;
channelCode.value = code;
skuDialogVisible.value = true;
};
// 关闭商品详情
const handleListClose = () => {
skuDialogVisible.value = false;
};
// 商品上一页
const handleClickPrev = () => {
if (listQuery.value.pageIndex === 1) {
return;
}
listQuery.value.pageIndex--;
handleListOpen();
};
// 商品下一页
const handleClickNext = () => {
if (listQuery.value.pageIndex === pageCount.value) {
return;
}
listQuery.value.pageIndex++;
handleListOpen();
};
// 搜索
const resetPageIndex = () => {
listQuery.value.pageIndex = 1;
handleListOpen();
};
// 商品选择
const handleCurrentChange = (i) => {
// TODO:点击取消选中功能
currentRow.value = listSkuData.value.rows[i];
};
// 确认商品选择
const handleSelectClick = (sku) => {
handleListClose();
channels.value[currentIndex.value].skuId = currentRow.value.skuId;
channels.value[currentIndex.value].sku = {
skuName: currentRow.value.skuName,
skuImage: currentRow.value.skuImage,
};
};
// 确认货道提交
const handleClick = async () => {
channelModelView.value.innerCode = props.goodData.innerCode;
channelModelView.value.channelList = channels.value.map((item) => {
return {
innerCode: props.goodData.innerCode,
channelCode: item.channelCode,
skuId: item.skuId,
};
});
const res = await channelConfig(channelModelView.value);
if (res.code === 200) {
proxy.$modal.msgSuccess('操作成功');
visible.value = false
emit('handleCloseGood');
}
};
</script>
// <style lang="scss" scoped src="../index.scss"></style>
views/manage/vm/components/ChannelDialogItem.vue
<template>
<div v-if="channel" class="item">
<div class="code">
{{ channel.channelCode }}
</div>
<div class="sku">
<img
class="img"
:src="channel.sku&&channel.sku.skuImage
? channel.sku.skuImage
: require('@/assets/vm/default_sku.png')"
/>
<div class="name" :title="channel.sku ? channel.sku.skuName : '暂无商品'">
{{ channel.sku ? channel.sku.skuName : '暂无商品' }}
</div>
</div>
<div>
<el-button
type="text"
class="el-button--primary-text"
@click="handleSetClick"
>
添加
</el-button>
<el-button
type="text"
class="el-button--danger-text"
:disabled="!channel.sku ? true : false"
@click="handleRemoveClick"
>
删除
</el-button>
</div>
</div>
</template>
<script setup>
import { require } from '@/utils/validate';
const props = defineProps({
currentIndex: {
type: Number,
default: 0,
},
channel: {
type: Object,
default: () => {},
},
});
const emit = defineEmits(['openSetSkuDialog','openRemoveSkuDialog']);
// 添加商品
const handleSetClick = () => {
emit('openSetSkuDialog', props.currentIndex, props.channel.channelCode);
};
// 删除产品
const handleRemoveClick = () => {
emit('openRemoveSkuDialog', props.currentIndex, props.channel.channelCode);
};
</script>
<style scoped lang="scss">
@import '@/assets/styles/variables.module.scss';
.item {
position: relative;
width: 150px;
height: 180px;
background: $base-menu-light-background;
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.06);
border-radius: 4px;
text-align: center;
.code {
position: absolute;
top: 10px;
left: 0;
width: 43px;
height: 23px;
line-height: 23px;
background: #829bed;
border-radius: 0px 10px 10px 0px;
font-size: 12px;
color: $base-menu-light-background;
}
.sku {
height: 135px;
padding-top: 16px;
background-color: #f6f7fb;
border-radius: 4px;
.img {
display: inline-block;
width: 84px;
height: 78px;
margin-bottom: 10px;
object-fit: contain;
}
.name {
padding: 0 16px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
</style>
在设备管理列表views/manage/vm/index.vue页面的对应位置中,添加货道按钮、货道组件、引入css样式等代码:
<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 -->
// ********************货道********************
// 货道组件
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********************
<style lang="scss" scoped src="./index.scss"></style>
(2)查询货道列表
需求:根据售货机编号查询货道列表。
- 接口文档
可以看到后端响应的数据中,包含有该设备上所有的货道的信息和每个货道上关联的商品信息,类型是object[],每个object都包含单个货道基本信息和该货道上关联的单个商品。因此我们可以返回一个List集合。
- 实现思路:
创建ChannelVo,将Sku类型的属性封装在Vo中,在xml中手动映射resultMap并使用Mybatis嵌套查询。
- ChannelVo
@Data
public class ChannelVo extends Channel {
// 商品
private Sku sku;
}
- ChannelMapper和xml
/**
* 根据售货机编号查询货道列表
*
* @param innerCode
* @return ChannelVo集合
*/
List<ChannelVo> selectChannelVoListByInnerCode(String innerCode);
<!-- 将嵌套查询的结果封装给ChannelVo的Sku sku属性上 -->
<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" />
<!-- 1对1嵌套查询(1个货道关联1个商品)根据sku_id查询该货道上关联的Sku -->
<association property="sku" javaType="Sku" column="sku_id" select="com.dkd.manage.mapper.SkuMapper.selectSkuBySkuId" />
</resultMap>
<sql id="selectChannelVo">
select id, channel_code, sku_id, vm_id, inner_code, max_capacity, current_capacity, last_supply_time, create_time, update_time from tb_channel
</sql>
<!-- 将自动映射封装resultType改为手动映射封装resultMap -->
<select id="selectChannelVoListByInnerCode" resultMap="ChannelVoResult">
<include refid="selectChannelVo"/>
where inner_code = #{innerCode}
</select>
- IChannelService接口和实现
/**
* 根据售货机编号查询货道列表
*
* @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
/**
* 根据售货机编号查询货道列表
*/
@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);
}
(3)货道关联商品
- 接口文档
请求体中包括object[]类型的channelList,说明是需要批量修改货道关联信息。
- 前端返回的json示例
最外层包含innerCode和channelList,channelList又包含innerCode、channelCode、skuId三个属性。即根据设备编号innerCode和货道编号channelCode定位到货道id,再根据skuId去更新货道表中该货道上的sku_id。
{
"innerCode": "aim5xu4I",
"channelList": [{
"innerCode": "aim5xu4I",
"channelCode": "1-1",
"skuId": 5
},
{
"innerCode": "aim5xu4I",
"channelCode": "1-2",
"skuId": 1
},
{
"innerCode": "aim5xu4I",
"channelCode": "2-1",
"skuId": 2
},
{
"innerCode": "aim5xu4I",
"channelCode": "2-2",
"skuId": 4
}
]
}
而我们后端并没有能直接接收这样格式的实体类,因此需要封装数据传输对象(DTO)来接收前端给我们传输的json数据,包括ChannelConfigDTO 和 ChannelSkuDTO。
- 实现思路:
创建ChannelConfigDTO 和 ChannelSkuDTO,在Service层将数据传输对象DTO转换为持久化对象PO,Mapper层需要根据售货机编号inner_code和货道编号channel_code查询货道信息,批量修改货道的sku_id。
- ChannelSkuDTO
// 单个货道对应的sku信息
@Data
public class ChannelSkuDTO {
// 售货机编号
private String innerCode;
// 货道编号
private String channelCode;
// 关联商品id
private Long skuId;
}
- ChannelConfigDTO
// 售货机货道配置
@Data
public class ChannelConfigDTO {
// 售货机编号
private String innerCode;
// 货道DTO集合
private List<ChannelSkuDTO> channelList;
}
- ChannelMapper和xml
/**
* 批量修改货道
* @param channelList
* @return
*/
int batchUpdateChannels(List<Channel> channelList);
<!-- 批量更新货道信息 -->
<update id="batchUpdateChannels" parameterType="java.util.List">
<foreach collection="list" item="channel" index="index" open="" close="" separator="; ">
UPDATE tb_channel
<set>
<if test="channel.channelCode != null and channel.channelCode != ''">channel_code = #{channel.channelCode},</if>
<if test="channel.skuId != null">sku_id = #{channel.skuId},</if>
<if test="channel.vmId != null">vm_id = #{channel.vmId},</if>
<if test="channel.innerCode != null and channel.innerCode != ''">inner_code = #{channel.innerCode},</if>
<if test="channel.maxCapacity != null">max_capacity = #{channel.maxCapacity},</if>
<if test="channel.currentCapacity != null">current_capacity = #{channel.currentCapacity},</if>
<if test="channel.lastSupplyTime != null">last_supply_time = #{channel.lastSupplyTime},</if>
<if test="channel.createTime != null">create_time = #{channel.createTime},</if>
<if test="channel.updateTime != null">update_time = #{channel.updateTime},</if>
</set>
WHERE id = #{channel.id}
</foreach>
</update>
注意:这种批量更新的方式取决于数据库的支持情况,不是所有数据库都支持在单个请求中发送多条独立的SQL语句。如果目标数据库不支持这种方式,可能需要采用其他方法如存储过程或批处理更新。
- application-druid.yml:允许mybatis框架在单个请求中发送多个sql语句
# 一次请求中可以包含多条SQL语句(支持多个分号;)
&allowMultiQueries=true
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
# 主库数据源
master:
url: jdbc:mysql://localhost:3306/dkd?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true
username: root
password: root
- IChannelService和实现类
/**
* 货道关联商品
* @param channelConfigDTO
* @return 结果
*/
int setChannels(ChannelConfigDTO channelConfigDTO);
/**
* 货道关联商品
* @param channelConfigDTO
* @return 结果
*/
@Override
public int setChannels(ChannelConfigDTO channelConfigDTO) {
// 将DTO转为PO对象
List<Channel> channelList = channelConfigDTO.getChannelList().stream().map(dto -> {
// 根据售货机编号和货道编号查询货道信息
Channel channel = channelMapper.getChannelInfo(dto.getInnerCode(), dto.getChannelCode());
// 如果该货道存在
if (channel != null) {
// 关联最新商品id
channel.setSkuId(dto.getSkuId());
// 更新货道修改时间
channel.setUpdateTime(DateUtils.getNowDate());
}
return channel; // 将转换后的PO对象返回
}).collect(Collectors.toList());
// 批量修改货道
return channelMapper.batchUpdateChannels(channelList);
}
- ChannelController
@PreAuthorize("@ss.hasPermi('manage:channel:edit')")
@Log(title = "售货机货道", businessType = BusinessType.UPDATE)
@PutMapping("/config")
public AjaxResult setChannels(@RequestBody ChannelConfigDTO channelConfigDTO) {
return toAjax(channelService.setChannels(channelConfigDTO));
}
- 货道关联商品功能测试
添加货道信息与商品关联成功,测试删除功能。
点击确认后向后端发送修改请求,该货道关联删除成功。
二、工单管理
工单是一种专业名词,是指用于记录、处理、跟踪
一项工作的完成情况。
- 管理人员登录后台系统选择创建工单,在工单类型里选择合适的工单类型,在设备编号里输入正确的设备编号。
- 工作人员在运营管理App可以看到分配给自己的工单,根据实际情况选择接收工单并完成,或者拒绝/取消工单。
1、需求说明
业务场景:管理员在后台创建工单后,工作人员可在运营管理App中查看并根据情况选择执行或取消分配给自己的任务。
工单管理主要涉及到两个功能模块,业务流程如下:
帝可得工单分为两大类 :
- 运营工单:运营人员来维护售货机
商品
,即补货
工单。 - 运维工单:运维人员来维护售货机
设备
,即投放
工单、撤机
工单、维修
工单。
工单有四种状态:
- 待处理
- 已接受(进行中)
- 已取消
- 已完成
对于工单和其他管理数据,下面是示意图:
- 关系字段:task_id、 product_type_id、inner_code、user_id、assignor_id、region_id
- 数据字典:task_status(1待办、2进行、3取消、4完成)
- 数据字典:create_type(0自动、1手动)
运营的工单包含补货信息,运维工单没有,所以运营工单需要单独创建补货工单详情。
创建所有工单,都会在工单表和工单明细表插入记录吗?
- 创建运维类工单只会在工单表插入数据。
- 创建运营类工单(补货工单)会在工单表和工单明细表插入数据。
task_code和task_id有什么区别?
- task_code是工单编号,具有业务规则 ,格式为年月日+当日序号。
- task_id 为工单表数据唯一标识。
工单表中的工单创建类型什么是自动工单,什么是手动工单?
- 工单方式:0表示自动创建,1表示手动创建。
- 自动创建:当设备满足某些条件后,由系统自动触发创建的工单,例如,北京奥体中心的售货机设备,某货道最多放10件商品,现在只剩4件,到达了货道库存警戒线。系统会自动创建一个补货工单,并分配运营人员前去补货。
- 手动创建:管理员在帝可得管理界面主动检查设备库存,手动创建补货工单,并分配运营人员前去补货。例如,当前点位设备货道中最多放10件商品,现在还有8件,并没有达到货道库存警戒线,但由于此处要举报重大事件,需要保持设备的货道商品充足,达到满状态,此时需要联系管理员手动创建工单。
工单表的user_id和assignor_id分别是做什么的?
- user_id是工单执行人的id(运维或运营)
- assignor_id是工单指派人的id(创建工单的人)
2、生成基础代码
- 需求:使用若依代码生成器,生成工单管理前后端基础代码,并导入到项目中。
- 步骤
(1)创建目录菜单
创建工单管理目录菜单
(2)添加数据字典
先创建工单状态
的字典类型
再创建工单状态
的字典数据
先创建工单创建类型
的字典类型
再创建工单创建类型
的字典数据
(3)配置代码生成信息
导入四张表:工单表tb_task
、补货工单详情表tb_task_details
、工单类型表tb_task_type
、自动补货任务表tb_job
配置工单表(运维、运营)
工单管理的二级菜单由我们手动来创建
配置工单详情表(工单原型)
配置工单类型表(工单原型)
创建自动补货任务表(工单原型)
(4)下载代码并导入项目
选中四张表生成下载,解压ruoyi.zip得到前后端代码和动态菜单sql。
注意:工单管理只需要后端代码,不使用若依生成的前端。因为二级菜单中前端页面涉及到运营工单和运维工单,若依无法直接按要求生成,需要我们自己手动编写此页面组件。
后端代码导入
(5)配置工单前端代码
编写前端代码:
api/manage/task.js
import request from '@/utils/request'
// 查询运维工单列表
export function listTask(query) {
return request({
url: '/manage/task/list',
method: 'get',
params: query
})
}
// 查询运维工单详细
export function getTask(taskId) {
return request({
url: '/manage/task/' + taskId,
method: 'get'
})
}
// 新增运维工单
export function addTask(data) {
return request({
url: '/manage/task',
method: 'post',
data: data
})
}
// 修改运维工单
export function updateTask(data) {
return request({
url: '/manage/task',
method: 'put',
data: data
})
}
// 删除运维工单
export function delTask(taskId) {
return request({
url: '/manage/task/' + taskId,
method: 'delete'
})
}
//根据售货机获取维修人员列表
export function getOperationList(innerCode) {
return request({
url: '/manage/emp/operationList/' + innerCode,
method: 'get'
})
}
//根据售货机获取运营人员列表
export function getBusinessList(innerCode) {
return request({
url: '/manage/emp/businessList/' + innerCode,
method: 'get'
})
}
// 查看工单补货详情
export function getTaskDetails(taskId) {
return request({
url: '/manage/taskDetails/byTaskId/' + taskId,
method: 'get'
})
}
// 获取补货预警值
export function getJob(id) {
return request({
url: '/manage/job/' + id,
method: 'get'
})
}
// 设置补货阈值
export function setJob(data) {
return request({
url: '/manage/job',
method: 'put',
data:data
})
}
api/manage/taskType.js
import request from '@/utils/request'
// 查询工单类型列表
export function listTaskType(query) {
return request({
url: '/manage/taskType/list',
method: 'get',
params: query
})
}
// 查询工单类型详细
export function getTaskType(typeId) {
return request({
url: '/manage/taskType/' + typeId,
method: 'get'
})
}
// 新增工单类型
export function addTaskType(data) {
return request({
url: '/manage/taskType',
method: 'post',
data: data
})
}
// 修改工单类型
export function updateTaskType(data) {
return request({
url: '/manage/taskType',
method: 'put',
data: data
})
}
// 删除工单类型
export function delTaskType(typeId) {
return request({
url: '/manage/taskType/' + typeId,
method: 'delete'
})
}
// 取消工单
export function cancelTaskType(data) {
return request({
url: '/manage/task/cancel',
method: 'put',
data: data
})
}
views\manage\task\components\business-detail-dialog.vue
<template>
<el-dialog
width="630px"
title="工单详情"
:close-on-click-modal="false"
:close-on-press-escape="false"
v-model="visible"
@close="cancel"
>
<div class="task-status">
<img
v-if="taskDada.taskStatus"
class="icon"
:src="require('@/assets/task/icon_' + taskDada.taskStatus + '.png')"
/>
<span class="status">
<label v-if="taskDada.taskStatus === 1">代办</label>
<label v-else-if="taskDada.taskStatus === 2">进行</label>
<label v-else-if="taskDada.taskStatus === 3">取消</label>
<label v-else>完成</label>
</span>
<img
v-if="taskDada.taskStatus"
class="pic"
:src="require('@/assets/task/pic_' + taskDada.taskStatus + '.png')"
/>
</div>
<el-form label-width="120">
<el-row>
<el-col :span="12">
<el-form-item label="设备编号:">
{{ taskDada.innerCode }}
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="创建日期:">
{{ taskDada.createTime }}
</el-form-item>
</el-col>
<el-col v-if="taskDada.taskStatus === 3" :span="12">
<el-form-item label="取消日期:">
{{ taskDada.updateTime ? taskDada.updateTime : '--' }}
</el-form-item>
</el-col>
<el-col v-if="taskDada.taskStatus === 4" :span="12">
<el-form-item label="完成日期:">
{{ taskDada.updateTime ? taskDada.updateTime : '--' }}
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="运营人员:">
{{ taskDada.userName }}
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工单类型:">
<span v-if="taskDada.productTypeId === 1">投放工单</span>
<span v-else-if="taskDada.productTypeId === 2">补货工单</span>
<span v-else-if="taskDada.productTypeId === 3">维修工单</span>
<span v-else>撤机工单</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="补货数量:" prop="details">
<el-button type="text" @click="channelDetails">
<el-icon><List /></el-icon>补货清单
</el-button>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工单方式:">
{{ taskDada.createType === 0 ? '自动' : '手动' }}
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
:label="taskDada.taskStatus === 3 ? '取消原因:' : '备注:'"
>
<div class="desc">
{{ taskDada.desc }}
</div>
</el-form-item>
</el-col>
<el-col v-if="taskDada.productTypeId === 1" :span="12">
<el-form-item label="定位:">
<div class="addr">
<el-icon><Location /></el-icon><span>{{ taskDada.addr }}</span>
</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div v-if="taskDada.taskStatus !== 4" class="dialog-footer">
<el-button
v-if="taskDada.taskStatus === 1 || taskDada.taskStatus === 2"
@click="handleCancelTask"
>
取消工单
</el-button>
<el-button
type="primary"
v-else-if="taskDada.taskStatus === 3"
@click="handleCreateTask"
>
重新创建
</el-button>
</div>
<!-- 货道列表弹层 -->
<BusinessReplenishmentListDialog
:listVisible="listVisible"
:detailData="detailData"
@handleClose="channelCloseDetails"
></BusinessReplenishmentListDialog>
<!-- end -->
</el-dialog>
</template>
<script setup name="Task">
import { watch } from 'vue';
import { require } from '@/utils/validate';
import { ElMessageBox } from 'element-plus';
import { cancelTaskType } from '@/api/manage/taskType';
// 组件
import BusinessReplenishmentListDialog from './business-replenishment-list-dialog.vue';
// 从父组件获取数据
const props = defineProps({
// 工单详情
taskDada: {
type: Object,
default: () => {},
},
// 获取货道列表
detailData:{
type: Object,
default: () => [],
},
// 详情弹层显示隐藏
detailVisible: {
type: Boolean,
default: false,
},
// 工单id
taskId: {
type: Number,
default: '',
},
});
// 定义变量
const emit = defineEmits(['handleClose', 'handleAdd', 'getList']);
const visible = ref(false);
const listVisible = ref(false); //货道弹层
watch(
() => props.detailVisible,
(val) => {
if (val) {
visible.value = val;
}
}
);
// 取消工单
const handleCancelTask = () => {
ElMessageBox.confirm('取消工单后,将不能恢复,是否确认取消?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
const obj = {
taskId: props.taskId,
desc: '后台工作人员取消',
};
cancelTaskType(obj).then((res) => {
if (res.code === 200) {
emit('getList');
cancel();
}
});
})
.catch(() => {});
};
// 关闭 弹层
const cancel = () => {
visible.value = false;
emit('handleClose');
};
// 重新创建
const handleCreateTask = () => {
cancel(); //关闭详情窗口
emit('handleAdd', 'anew'); //打开新增窗口
};
// 打开货道列表弹层
const channelDetails = () => {
listVisible.value = true;
};
// 关闭货道列表 弹层
const channelCloseDetails = () => {
listVisible.value = false;
};
</script>
views\manage\task\components\business-replenishment-dialog.vue
<template>
<el-dialog
width="630px"
title="补货详情"
:close-on-click-modal="false"
:close-on-press-escape="false"
v-model="visible"
@close="cancel"
@open="open"
>
<el-scrollbar class="scrollbar" style="height: 330px">
<el-table
style="width: 568px; margin: 0 auto"
:data="channelList"
:header-cell-style="{
'line-height': '1.15',
padding: '10px 0 9px',
background: '#F3F6FB',
'font-weight': '500',
'text-align': 'left',
color: '#666666',
}"
:cell-style="{
height: '44px',
padding: '2px 0',
'text-align': 'left',
color: '#666666',
}"
>
<el-table-column label="货道编号">
<template #default="scope">
{{ scope.row.channelCode }}
</template>
</el-table-column>
<el-table-column label="商品名称">
<template #default="scope">
{{ scope.row.skuId && scope.row.sku.skuId? scope.row.sku.skuName : '-' }}
</template>
</el-table-column>
<el-table-column label="当前数量">
<template #default="scope">
{{ scope.row.skuId && scope.row.sku.skuId? scope.row.currentCapacity : '-' }}
</template>
</el-table-column>
<el-table-column label="还可添加">
<template #default="scope">
{{ scope.row.skuId && scope.row.sku.skuId? getAvailableCapacity(scope.row) : '-' }}
</template>
</el-table-column>
<el-table-column label="补满数量" width="200">
<template #default="scope">
<el-input-number
v-if="scope.row.skuId && scope.row.sku.skuId"
v-model="scope.row.expectCapacity"
controls-position="right"
:min="0"
:max="getAvailableCapacity(scope.row)"
label="补满数量"
style="width: 100%"
placeholder="请输入"
/>
<span v-else>货道暂无商品</span>
</template>
</el-table-column>
</el-table>
</el-scrollbar>
<div class="dialog-footer">
<el-button @click="cancel">取消</el-button>
<el-button type="primary" @click="ensureDialog">确认</el-button>
</div>
</el-dialog>
</template>
<script setup name="Task">
import { watch } from 'vue';
import { require } from '@/utils/validate';
import { ElMessageBox } from 'element-plus';
import { cancelTaskType } from '@/api/manage/taskType';
// 接口
// 获取货道接口
import { getGoodsList } from '@/api/manage/channel';
// 从父组件获取数据
const props = defineProps({
// 详情弹层显示隐藏
channelVisible: {
type: Boolean,
default: false,
},
// 设备编号
innerCode: {
type: String,
default: '',
},
});
// 定义变量
const emit = defineEmits(['handleClose', 'getDetailList']);
const visible = ref(false);
const channelList = ref([]); //货道列表
const detailList = ref([]); //补货列表
watch(
() => props.channelVisible,
(val) => {
if (val) {
visible.value = val;
}
}
);
// 弹层打开
const open = () => {
getChannelList();
};
// 还可添加
const getAvailableCapacity = (channel) => {
let availableCapacity = channel.maxCapacity - channel.currentCapacity;
return availableCapacity > 0 ? availableCapacity : 0;
};
// 获取货道列表
const getChannelList = () => {
getGoodsList(props.innerCode).then((response) => {
channelList.value = response.data;
channelList.value.map((channel) => {
channel.expectCapacity =
channel.sku !== null
? channel.maxCapacity - channel.currentCapacity
: 0;
});
});
};
// 确定货道清单
const ensureDialog = () => {
cancel();
channelList.value.forEach((ele) => {
if (ele.sku&&ele.sku.skuId&&ele.expectCapacity>0) {
detailList.value.push({
channelCode: ele.channelCode,
expectCapacity: ele.expectCapacity,
skuId: ele.skuId,
skuName: ele.sku ? ele.sku.skuName : '',
skuImage: ele.sku ? ele.sku.skuImage : '',
});
}
});
emit('getDetailList', detailList.value);
};
// 关闭 弹层
const cancel = () => {
visible.value = false;
detailList.value=[]
emit('handleClose');
};
</script>
views\manage\task\components\business-replenishment-list-dialog.vue
<template>
<el-dialog
width="630px"
title="补货详情"
:close-on-click-modal="false"
:close-on-press-escape="false"
v-model="visible"
append-to-body
@close="cancel"
@open="open"
>
<el-scrollbar
class="scrollbar"
style="height: 330px;"
>
<el-table
style="width: 552px;margin: 0 auto;"
:data="detailData"
:header-cell-style="{'line-height': '1.15', 'padding': '10px 0 9px', 'background': '#F3F6FB', 'font-weight': '500', 'text-align': 'left', 'color': '#666666'}"
:cell-style="{'height': '44px', 'padding': '2px 0', 'text-align': 'left', 'color': '#666666'}"
>
<el-table-column label="货道编号">
<template #default="scope">
{{ scope.row.channelCode }}
</template>
</el-table-column>
<el-table-column label="商品">
<template #default="scope">
{{ scope.row.skuName?scope.row.skuName:'--' }}
</template>
</el-table-column>
<el-table-column label="补货数量">
<template #default="scope">
{{ scope.row.expectCapacity }}
</template>
</el-table-column>
</el-table>
</el-scrollbar>
</el-dialog>
</template>
<script setup name="Task">
import { watch } from 'vue';
import { getTaskDetails } from '@/api/manage/task';
// 接口
// 获取货道接口
import { getGoodsList } from '@/api/manage/channel';
// 从父组件获取数据
const props = defineProps({
// 详情弹层显示隐藏
listVisible: {
type: Boolean,
default: false,
},
// 获取货道列表
detailData:{
type: Object,
default: () => [],
},
});
// 定义变量
const emit = defineEmits(['handleClose']);
const visible = ref(false);
watch(
() => props.listVisible,
(val) => {
if (val) {
visible.value = val;
}
}
);
// 关闭 弹层
const cancel = () => {
visible.value = false;
emit('handleClose');
};
</script>
views\manage\task\components\operation-detail-dialog.vue
<template>
<el-dialog
width="630px"
title="工单详情"
:close-on-click-modal="false"
:close-on-press-escape="false"
v-model="visible"
@close="cancel"
@open="open"
>
<div class="task-status">
<img
v-if="taskDada.taskStatus"
class="icon"
:src="require('@/assets/task/icon_' + taskDada.taskStatus + '.png')"
/>
<span class="status">
<label v-if="taskDada.taskStatus === 1">代办</label>
<label v-else-if="taskDada.taskStatus === 2">进行</label>
<label v-else-if="taskDada.taskStatus === 3">取消</label>
<label v-else>完成</label>
</span>
<img
v-if="taskDada.taskStatus"
class="pic"
:src="require('@/assets/task/pic_' + taskDada.taskStatus + '.png')"
/>
</div>
<el-form label-width="120">
<el-row>
<el-col :span="12">
<el-form-item label="设备编号:">
{{ taskDada.innerCode }}
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="创建日期:">
{{ taskDada.createTime }}
</el-form-item>
</el-col>
<el-col v-if="taskDada.taskStatus === 3" :span="12">
<el-form-item label="取消日期:">
{{ taskDada.updateTime ? taskDada.updateTime : '--' }}
</el-form-item>
</el-col>
<el-col v-if="taskDada.taskStatus === 4" :span="12">
<el-form-item label="完成日期:">
{{ taskDada.updateTime ? taskDada.updateTime : '--' }}
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="运营人员:">
{{ taskDada.userName }}
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工单类型:">
<span v-if="taskDada.productTypeId === 1">投放工单</span>
<span v-else-if="taskDada.productTypeId === 2">补货工单</span>
<span v-else-if="taskDada.productTypeId === 3">维修工单</span>
<span v-else>撤机工单</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工单方式:">
{{ taskDada.createType === 0 ? '自动' : '手动' }}
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
:label="taskDada.taskStatus === 3 ? '取消原因:' : '备注:'"
>
<div class="desc">
{{ taskDada.desc }}
</div>
</el-form-item>
</el-col>
<el-col v-if="taskDada.productTypeId === 1" :span="12">
<el-form-item label="定位:">
<div class="addr">
<el-icon><Location /></el-icon
><span>{{
taskDada.addr
}}</span>
</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div v-if="taskDada.taskStatus !== 4" class="dialog-footer">
<el-button
v-if="taskDada.taskStatus === 1 || taskDada.taskStatus === 2"
@click="handleCancelTask"
>
取消工单
</el-button>
<el-button
type="primary"
v-else-if="taskDada.taskStatus === 3"
@click="handleCreateTask"
>
重新创建
</el-button>
</div>
</el-dialog>
</template>
<script setup name="Task">
import { watch } from 'vue';
import { require } from '@/utils/validate';
import { ElMessageBox } from 'element-plus';
import { cancelTaskType } from '@/api/manage/taskType';
// 从父组件获取数据
const props = defineProps({
// 工单详情
taskDada:{
type: Object,
default:()=>{}
},
// 详情弹层显示隐藏
detailVisible: {
type: Boolean,
default: false,
},
// 工单id
taskId: {
type: String,
default: '',
},
});
// 定义变量
const emit = defineEmits(['handleClose','handleAdd','getList']);
const visible = ref(false);
watch(
() => props.detailVisible,
(val) => {
if (val) {
visible.value = val;
}
}
);
// 弹层打开
const open = () => {
// 工单详情
// taskInfo();
// // TODO:工单状态和工单类型可以直接从工单详情中获得
// 工单状态列表
// getAllTaskStatus()
// // 工单类型列表
// getTaskTypeList()
};
// 取消工单
const handleCancelTask = () => {
ElMessageBox.confirm('取消工单后,将不能恢复,是否确认取消?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
const obj = {
taskId: props.taskId,
desc: '后台工作人员取消',
};
cancelTaskType(obj).then((res) => {
if (res.code === 200) {
emit('getList')
cancel();
}
});
})
.catch(() => {});
};
// 关闭 弹层
const cancel = () => {
visible.value = false;
emit('handleClose');
};
// 重新创建
const handleCreateTask = ()=>{
cancel()//关闭详情窗口
emit('handleAdd','anew')//打开新增窗口
}
</script>
views\manage\task\components\task-config.vue
<template>
<el-dialog
width="630px"
title="工单配置"
:close-on-click-modal="false"
:close-on-press-escape="false"
v-model="visible"
append-to-body
@close="cancel"
@open="open"
>
<el-form
ref="taskRef"
:inline="true"
:model="form"
:rules="rules"
label-width="120"
>
<el-form-item label="补货警戒线:" prop="alertValue">
<el-input-number
v-model="form.alertValue"
controls-position="right"
:min="1"
:max="100"
placeholder="请输入"
/>
</el-form-item>
</el-form>
<div class="dialog-footer">
<el-button @click="cancel"> 取消 </el-button>
<el-button type="primary" @click="submitForm"> 确认 </el-button>
</div>
</el-dialog>
</template>
<script setup name="Task">
import { watch } from 'vue';
const { proxy } = getCurrentInstance();
// 接口
import { getJob, setJob } from '@/api/manage/task';
// 从父组件获取数据
const props = defineProps({
// 弹层显示隐藏
taskConfigVisible: {
type: Boolean,
default: false,
},
});
// 定义变量
const emit = defineEmits(['handleClose']);
const visible = ref(false);
const data = reactive({
form: {},
rules: {
alertValue: [{ required: true, message: '请输入', trigger: 'blur' }],
},
});
const { form, rules } = toRefs(data);
watch(
() => props.taskConfigVisible,
(val) => {
if (val) {
visible.value = val;
}
}
);
// 打开弹层
const open = () => {
getJobData()
};
// 获取获取补货预警值
const getJobData = () => {
getJob(1).then((response) => {
const res = response.data;
form.value = {
id: res.id,
alertValue: res.alertValue,
};
});
};
// 提交表单
const submitForm = () => {
proxy.$refs['taskRef'].validate((valid) => {
setJob(form.value).then((res) => {
if (res.code === 200) {
proxy.$modal.msgSuccess('配置成功');
cancel();
getJobData()
}
});
});
};
// 关闭弹层
const cancel = () => {
visible.value = false;
emit('handleClose');
};
</script>
views\manage\task\business.vue
<template>
<div class="app-container">
<el-form
:model="queryParams"
ref="queryRef"
:inline="true"
v-show="showSearch"
label-width="68px"
>
<el-form-item label="工单编号" prop="taskCode">
<el-input
v-model="queryParams.taskCode"
placeholder="请输入工单编号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="工单状态" prop="taskStatus">
<el-select
v-model="queryParams.taskStatus"
placeholder="请选择工单状态"
clearable
>
<el-option
v-for="dict in task_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</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-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
v-hasPermi="['manage:task:add']"
>新增</el-button
>
<el-button type="primary" plain @click="openTaskConfig"
>工单配置</el-button
>
</el-col>
<right-toolbar
v-model:showSearch="showSearch"
@queryTable="getList"
></right-toolbar>
</el-row>
<el-table
v-loading="loading"
:data="taskList"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column
label="序号"
type="index"
width="50"
align="center"
prop="taskId"
/>
<el-table-column label="工单编号" align="center" prop="taskCode" />
<el-table-column label="设备编号" align="center" prop="innerCode" />
<el-table-column
label="工单类型"
align="center"
prop="taskType.typeName"
/>
<el-table-column label="工单方式" align="center" prop="createType">
<template #default="scope">
<dict-tag :options="task_create_type" :value="scope.row.createType" />
</template>
</el-table-column>
<el-table-column label="工单状态" align="center" prop="taskStatus">
<template #default="scope">
<dict-tag :options="task_status" :value="scope.row.taskStatus" />
</template>
</el-table-column>
<el-table-column label="运营人员" align="center" prop="userName" />
<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="openTaskDetailDialog(scope.row)"
v-hasPermi="['manage:task:edit']"
>查看详情</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-form ref="taskRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="设备编号" prop="innerCode">
<el-input
v-model="form.innerCode"
placeholder="请输入设备编号"
@blur="handleCode"
/>
</el-form-item>
<el-form-item label="工单类型" prop="productTypeId">
<el-select
v-model="form.productTypeId"
placeholder="请选择工单类型"
clearable
>
<el-option
v-for="dict in taskTypeList"
:key="dict.typeId"
:label="dict.typeName"
:value="dict.typeId"
/>
</el-select>
</el-form-item>
<el-form-item label="补货数量:" prop="details">
<el-button type="text" @click="channelDetails">
<el-icon> <List /> </el-icon>补货清单
</el-button>
</el-form-item>
<el-form-item label="运营人员:" prop="userId">
<el-select
v-model="form.userId"
placeholder="请选择"
:filterable="true"
>
<el-option
v-for="(item, index) in userList"
:key="index"
:label="item.userName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="desc">
<el-input
type="textarea"
v-model="form.desc"
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>
<!-- 查看详情组件 -->
<DetailDialog
:detailVisible="detailVisible"
:taskId="taskId"
:taskDada="form"
:detailData="detailData"
@getList="getList"
@handleClose="handleClose"
@handleAdd="handleAdd"
></DetailDialog>
<!-- end -->
<!-- 补货详情 -->
<ReplenishmentDialog
:channelVisible="channelVisible"
:innerCode="form.innerCode"
@getDetailList="getDetailList"
@handleClose="channelDetailsClose"
></ReplenishmentDialog>
<!-- end -->
<!-- 工单配置 -->
<TaskConfig
:taskConfigVisible="taskConfigVisible"
@handleClose="handleConfigClose"
></TaskConfig>
<!-- end -->
</div>
</template>
<script setup name="Task">
import {
listTask,
getTask,
delTask,
addTask,
updateTask,
getBusinessList,
getTaskDetails,
} from '@/api/manage/task';
import { listTaskType } from '@/api/manage/taskType';
import { loadAllParams } from '@/api/page';
// 组件
import DetailDialog from './components/business-detail-dialog.vue'; //详情组件
import ReplenishmentDialog from './components/business-replenishment-dialog.vue'; //补货组件
import TaskConfig from './components/task-config.vue';
const { proxy } = getCurrentInstance();
const { task_status, task_create_type } = proxy.useDict(
'task_status',
'task_create_type'
);
const taskList = 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 detailVisible = ref(false); //查看详情弹层显示/隐藏
const taskId = ref(null); //工单id
const taskDada = ref({}); //工单详情
const userList = ref([]); //运维人员
const channelVisible = ref(false); //补货弹层
const detailData = ref([]); //货道列表
const taskConfigVisible = ref(false); //工单配置弹层
const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
taskCode: null,
taskStatus: null,
createType: null,
innerCode: null,
userName: null,
regionId: null,
desc: null,
productTypeId: null,
userId: null,
addr: null,
params: { isRepair: false },
},
rules: {
innerCode: [
{ required: true, message: '设备编号不能为空', trigger: 'blur' },
],
productTypeId: [
{ required: true, message: '设备类型不能为空', trigger: 'blur' },
],
// details: [{ required: true, message: '补货数量不能为空', trigger: 'blur' }],
userId: [{ required: true, message: '人员不能为空', trigger: 'blur' }],
desc: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
},
});
const { queryParams, form, rules } = toRefs(data);
/** 查询运营工单列表 */
function getList() {
loading.value = true;
listTask(queryParams.value).then((response) => {
taskList.value = response.rows;
total.value = response.total;
loading.value = false;
});
}
// 取消按钮
function cancel() {
open.value = false;
reset();
}
// 表单重置
function reset() {
form.value = {
taskId: null,
taskCode: null,
taskStatus: null,
createType: null,
innerCode: null,
userId: null,
userName: null,
regionId: null,
desc: null,
productTypeId: null,
addr: null,
createTime: null,
updateTime: null,
details: [],
};
proxy.resetForm('taskRef');
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
function resetQuery() {
proxy.resetForm('queryRef');
handleQuery();
}
// 多选框选中数据
function handleSelectionChange(selection) {
ids.value = selection.map((item) => item.taskId);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 新增按钮操作 */
function handleAdd(val) {
if (val === 'anew') {
taskInfo();
getUserList();
} else {
taskId.val = '';
}
reset();
open.value = true;
title.value = '添加运营工单';
}
/** 提交按钮 */
function submitForm() {
proxy.$refs['taskRef'].validate((valid) => {
if (valid) {
const data = form.value;
form.value = {
innerCode: data.innerCode,
userId: data.userId,
productTypeId: data.productTypeId,
desc: data.desc,
createType: 1,
details: data.details,
};
addTask(form.value).then((response) => {
proxy.$modal.msgSuccess('新增成功');
open.value = false;
getList();
});
}
});
}
/** 删除按钮操作 */
function handleDelete(row) {
const _taskIds = row.taskId || ids.value;
proxy.$modal
.confirm('是否确认删除运营工单编号为"' + _taskIds + '"的数据项?')
.then(function () {
return delTask(_taskIds);
})
.then(() => {
getList();
proxy.$modal.msgSuccess('删除成功');
})
.catch(() => {});
}
/** 导出按钮操作 */
function handleExport() {
proxy.download(
'manage/task/export',
{
...queryParams.value,
},
`task_${new Date().getTime()}.xlsx`
);
}
// 查询工单类型列表
const taskTypeList = ref([]);
function getTaskTypeList() {
// 默认时获取所有得工单类型,需要用type区别开,1:运维工单类型,2:运营工单类型
const page = {
...loadAllParams,
type: 2,
};
listTaskType(page).then((response) => {
taskTypeList.value = response.rows;
});
}
// 填写设备编号后
const handleCode = () => {
if (form.value.innerCode) {
getUserList();
}
};
// 获取运营人员列表
const getUserList = () => {
getBusinessList(form.value.innerCode).then((response) => {
userList.value = response.data;
});
};
// 获取工单详情
const taskInfo = () => {
let dataArr = [];
let obj = {};
getTask(taskId.value).then((response) => {
form.value = response.data;
});
// 获取货道列表
getTaskDetails(taskId.value).then((res) => {
detailData.value = res.data;
detailData.value.map((taskDetail) => {
obj = {
channelCode: taskDetail.channelCode,
expectCapacity: taskDetail.expectCapacity,
skuId: taskDetail.skuId,
skuName: taskDetail.skuName,
skuImage: taskDetail.skuImage,
};
dataArr.push(obj);
});
form.value.details = dataArr;
});
};
// 查看详情
const openTaskDetailDialog = (row) => {
taskId.value = row.taskId;
taskInfo();
detailVisible.value = true;
};
// 关闭详情弹层
const handleClose = () => {
detailVisible.value = false;
};
// 补货清单
const channelDetails = () => {
proxy.$refs['taskRef'].validateField('innerCode', (error) => {
if (!error) {
return;
}
channelVisible.value = true;
});
};
// 关闭补货清单
const channelDetailsClose = () => {
channelVisible.value = false;
};
// 获取货道清单数据
const getDetailList = (val) => {
form.value.details = val;
};
// 打开工单配置弹层
const openTaskConfig = () => {
taskConfigVisible.value = true;
};
// 关闭工单配置弹层
const handleConfigClose = () => {
taskConfigVisible.value = false;
};
getTaskTypeList();
getList();
</script>
<style lang="scss" scoped src="./index.scss"></style>
views\manage\task\operation.vue
<template>
<div class="app-container">
<el-form
:model="queryParams"
ref="queryRef"
:inline="true"
v-show="showSearch"
label-width="68px"
>
<el-form-item label="工单编号" prop="taskCode">
<el-input
v-model="queryParams.taskCode"
placeholder="请输入工单编号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="工单状态" prop="taskStatus">
<el-select
v-model="queryParams.taskStatus"
placeholder="请选择工单状态"
clearable
>
<el-option
v-for="dict in task_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="工单类型" prop="productTypeId">
<el-select
v-model="queryParams.productTypeId"
placeholder="请选择工单类型"
clearable
>
<el-option
v-for="dict in taskTypeList"
:key="dict.typeId"
:label="dict.typeName"
:value="dict.typeId"
/>
</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-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
v-hasPermi="['manage:task:add']"
>新增</el-button
>
</el-col>
<right-toolbar
v-model:showSearch="showSearch"
@queryTable="getList"
></right-toolbar>
</el-row>
<el-table
v-loading="loading"
:data="taskList"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column
label="序号"
type="index"
width="50"
align="center"
prop="taskId"
/>
<el-table-column label="工单编号" align="center" prop="taskCode" />
<el-table-column label="设备编号" align="center" prop="innerCode" />
<el-table-column
label="工单类型"
align="center"
prop="taskType.typeName"
/>
<el-table-column label="工单方式" align="center" prop="createType">
<template #default="scope">
<dict-tag :options="task_create_type" :value="scope.row.createType" />
</template>
</el-table-column>
<el-table-column label="工单状态" align="center" prop="taskStatus">
<template #default="scope">
<dict-tag :options="task_status" :value="scope.row.taskStatus" />
</template>
</el-table-column>
<el-table-column label="运维人员" align="center" prop="userName" />
<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="openTaskDetailDialog(scope.row)"
v-hasPermi="['manage:task:edit']"
>查看详情</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-form ref="taskRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="设备编号" prop="innerCode">
<el-input
v-model="form.innerCode"
placeholder="请输入设备编号"
@blur="handleCode"
/>
</el-form-item>
<el-form-item label="工单类型" prop="productTypeId">
<el-select
v-model="form.productTypeId"
placeholder="请选择工单类型"
clearable
>
<el-option
v-for="dict in taskTypeList"
:key="dict.typeId"
:label="dict.typeName"
:value="dict.typeId"
/>
</el-select>
</el-form-item>
<el-form-item label="运维人员:" prop="userId">
<el-select
v-model="form.userId"
placeholder="请选择"
:filterable="true"
>
<el-option
v-for="(item, index) in userList"
:key="index"
:label="item.userName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="desc">
<el-input
type="textarea"
v-model="form.desc"
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>
<!-- 查看详情组件 -->
<DetailDialog
:detailVisible="detailVisible"
:taskId="taskId"
:taskDada="form"
@handleClose="handleClose"
@handleAdd="handleAdd"
@getList="getList"
></DetailDialog>
<!-- end -->
</div>
</template>
<script setup name="Task">
import {
listTask,
getTask,
delTask,
addTask,
updateTask,
getOperationList,
} from '@/api/manage/task';
import { listTaskType } from '@/api/manage/taskType';
import { loadAllParams } from '@/api/page';
// 组件
import DetailDialog from './components/operation-detail-dialog.vue'; //详情组件
const { proxy } = getCurrentInstance();
const { task_status, task_create_type } = proxy.useDict(
'task_status',
'task_create_type'
);
const taskList = 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 detailVisible = ref(false); //查看详情弹层显示/隐藏
const taskId = ref(''); //工单id
const taskDada = ref({}); //工单详情
const userList = ref([]); //运维人员
const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
taskCode: null,
taskStatus: null,
createType: null,
innerCode: null,
userId: null,
userName: null,
regionId: null,
desc: null,
productTypeId: null,
userId: null,
addr: null,
params: { isRepair: true },
},
rules: {
innerCode: [
{ required: true, message: '设备编号不能为空', trigger: 'blur' },
],
productTypeId: [
{ required: true, message: '设备类型不能为空', trigger: 'blur' },
],
userId: [
{ required: true, message: '人员不能为空', trigger: 'blur' },
],
desc: [
{ required: true, message: '备注不能为空', trigger: 'blur' },
]
},
});
const { queryParams, form, rules } = toRefs(data);
/** 查询运维工单列表 */
function getList() {
loading.value = true;
listTask(queryParams.value).then((response) => {
taskList.value = response.rows;
total.value = response.total;
loading.value = false;
});
}
// 取消按钮
function cancel() {
open.value = false;
reset();
}
// 表单重置
function reset() {
form.value = {
taskId: null,
taskCode: null,
taskStatus: null,
createType: null,
innerCode: null,
userId: null,
userName: null,
regionId: null,
desc: null,
productTypeId: null,
userId: null,
addr: null,
createTime: null,
updateTime: null,
};
proxy.resetForm('taskRef');
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
function resetQuery() {
proxy.resetForm('queryRef');
handleQuery();
}
// 多选框选中数据
function handleSelectionChange(selection) {
ids.value = selection.map((item) => item.taskId);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 新增按钮操作 */
function handleAdd(val) {
if (val === 'anew') {
taskInfo();
getUserList();
} else {
taskId.val = '';
}
reset();
open.value = true;
title.value = '添加运维工单';
}
/** 提交按钮 */
function submitForm() {
proxy.$refs['taskRef'].validate((valid) => {
if (valid) {
form.value={
...form.value,
createType:1
}
addTask(form.value).then((response) => {
proxy.$modal.msgSuccess('新增成功');
open.value = false;
getList();
});
}
});
}
/** 删除按钮操作 */
function handleDelete(row) {
const _taskIds = row.taskId || ids.value;
proxy.$modal
.confirm('是否确认删除运维工单编号为"' + _taskIds + '"的数据项?')
.then(function () {
return delTask(_taskIds);
})
.then(() => {
getList();
proxy.$modal.msgSuccess('删除成功');
})
.catch(() => {});
}
/** 导出按钮操作 */
function handleExport() {
proxy.download(
'manage/task/export',
{
...queryParams.value,
},
`task_${new Date().getTime()}.xlsx`
);
}
// 查询工单类型列表
const taskTypeList = ref([]);
function getTaskTypeList() {
// 默认时获取所有得工单类型,需要用type区别开,1:运维工单类型,2:运营工单类型
const page = {
...loadAllParams,
type: 1,
};
listTaskType(page).then((response) => {
taskTypeList.value = response.rows;
});
}
// 填写设备编号后
const handleCode = () => {
if (form.value.innerCode) {
getUserList();
}
};
// 获取运维人员列表
const getUserList = () => {
getOperationList(form.value.innerCode).then((response) => {
userList.value = response.data;
});
};
// 获取工单详情
const taskInfo = () => {
getTask(taskId.value).then((response) => {
form.value = response.data;
});
};
// 查看详情
const openTaskDetailDialog = (row) => {
taskId.value = row.taskId;
taskInfo();
detailVisible.value = true;
};
// 关闭详情弹层
const handleClose = () => {
detailVisible.value = false;
};
getTaskTypeList();
getList();
</script>
<style lang="scss" scoped src="./index.scss"></style>
views\manage\task\index.scss
@import '@/assets/styles/variables.module.scss';
:deep(.task-status) {
display: flex;
align-items: center;
height: 54px;
margin-bottom: 25px;
background-color: rgba(236, 236, 236, 0.39);
.icon {
margin-left: 22px;
}
.status {
flex: 1;
margin-left: 16px;
color: rgba(0, 0, 0, 0.85);
}
.pic {
margin-right: 76px;
margin-bottom: 7px;
}
}
.addr{
display: flex;
.el-icon{
margin: 10px 5px 0 0;
}
}
.desc, .addr {
margin-top: 10px;
line-height: 20px;
.svg-icon {
margin-right: 4px;
color: $--color-primary;
}
}
(6)手动创建二级菜单
手动创建运营工单二级菜单
手动创建运维工单二级菜单
注意:在使用若依生成的动态SQL导入后,会有对菜单中的按钮权限进行导入。而自己手动创建的也需要手动为该菜单所用到的每个按钮
或其他Controller的请求
分配相应的权限字符。如果不分配,其他用户登录后仅有菜单访问的权限,没有操作的权限。
运维工单二级菜单同理分配按钮权限。
注意:添加完菜单或按钮后需要管理员重新为角色分配菜单权限!
3、查询工单列表
需求:运营和运营工单共享一套后端接口,通过特定的查询条件区分工单类型,并在返回结果中包含工单类型的详细信息。
- 运营工单页面原型
- 运维工单页面原型
- 接口文档
- 实现思路
- 代码实现
TaskVo
@Data
public class TaskVo extends Task {
// 工单类型
private TaskType taskType;
}
TaskMapper
/**
* 查询运维工单列表
*
* @param task 运维工单
* @return TaskVo集合
*/
List<TaskVo> selectTaskVoList(Task task);
<!-- 手动映射Mapper -->
<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" />
<!-- 工单里查工单类型,工单:工单类型=1:n,column为查询条件字段 -->
<association property="taskType" javaType="TaskType" column="product_type_id" select="com.dkd.manage.mapper.TaskTypeMapper.selectTaskTypeByTypeId" />
</resultMap>
<select id="selectTaskVoList" parameterType="Task" 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>
</where>
order by create_time desc
</select>
ITaskService和实现类
/**
* 查询运维工单列表
* @param task
* @return TaskVo集合
*/
List<TaskVo> selectTaskVoList(Task task);
/**
* 查询运维工单列表
* @param task
* @return TaskVo集合
*/
@Override
public List<TaskVo> selectTaskVoList(Task task) {
return taskMapper.selectTaskVoList(task);
}
TaskController
/**
* 查询工单列表
*/
@PreAuthorize("@ss.hasPermi('manage:task:list')")
@GetMapping("/list")
public TableDataInfo list(Task task)
{
startPage();
List<TaskVo> voList = taskService.selectTaskVoList(task);
return getDataTable(voList);
}
4、获取运营人员列表
- 需求:根据售货机编号获取负责当前区域下的运营人员列表。
- 页面原型(当设备编号输入完,输入框失去聚焦点后,会向后端发送请求查询运营人员列表)
- 接口文档
接口文档中要求我们通过前端传入的设备编号innerCode,来查询该设备所属区域下的员工集合。
先分析一下员工到设备之间的关系:
一个设备投放在一个区域下的某点位,有多个员工负责在这个区域下工作。
看似我们需要查询四张表,才能根据售货机编号获取该区域下运营人员的列表。其实只需要两张表,下面是设备表和员工表的数据库字段设计:
设备表中有设备编号innerCode和所属区域id,而员工表中也有所属区域id,这样通过两张表就可以查询出来,需要注意的是我们要查询的是运营人员,因此需要role_code=1002的所有运营员,并且员工上班状态status=1为启用,所以这三个查询条件都要满足。
- 实现思路
在设备的Mapper和Service编写根据innerCode查询设备信息的方法,之后在Emp的Controller中注入设备的Service对象,获取该设备所属区域id,将查询条件封装给参数,去查询该区域下启用的运营人员列表。
VendingMachineMapper
/**
* 根据设备编号查询设备信息
*
* @param innerCode
* @return VendingMachine
*/
@Select("select * from tb_vending_machine where inner_code = #{innerCode}")
VendingMachine selectVendingMachineByInnerCode(String innerCode);
IVendingMachineService和实现类
/**
* 根据设备编号查询设备信息
*
* @param innerCode
* @return VendingMachine
*/
VendingMachine selectVendingMachineByInnerCode(String innerCode);
/**
* 根据设备编号查询设备信息
*
* @param innerCode
* @return VendingMachine
*/
@Override
public VendingMachine selectVendingMachineByInnerCode(String innerCode) {
return vendingMachineMapper.selectVendingMachineByInnerCode(innerCode);
}
DkdContants(帝可得常量类)
/**
* 员工启用
*/
public static final Long EMP_STATUS_NORMAL = 1L;
/**
* 员工禁用
*/
public static final Long EMP_STATUS_DISABLE = 0L;
/**
* 角色编码:运营员
*/
public static final String ROLE_CODE_BUSINESS = "1002";
/**
* 角色编码:维修员
*/
public static final String ROLE_CODE_OPERATOR = "1003";
EmpController
@Autowired
private IVendingMachineService vendingMachineService;
/**
* 根据售货机获取运营人员列表
*/
@PreAuthorize("@ss.hasPermi('manage:emp:list')")
@GetMapping("/businessList/{innerCode}")
public AjaxResult businessList(@PathVariable String innerCode) {
// 根据innerCode查询售货机信息
VendingMachine vm = vendingMachineService.selectVendingMachineByInnerCode(innerCode);
if (vm == null) return error("售货机不存在");
// 根据区域id、角色编号、员工状态查询运营人员列表
Emp emp = new Emp(); // 封装查询条件对象
emp.setRegionId(vm.getRegionId()); // 设备所属区域id
emp.setRoleCode(DkdContants.ROLE_CODE_BUSINESS); // 角色编码:运营员(1002)
emp.setStatus(DkdContants.EMP_STATUS_NORMAL); // 员工状态:启用(1)
return success(empService.selectEmpList(emp));
}
5、获取运维人员列表
- 需求:根据售货机编号获取负责当前区域下的运维人员列表。
- 接口文档
实现方式和思路与之前的获取运营人员列表同理,查两张表。
- 代码实现
EmpController
/**
* 根据售货机编码获取运维人员列表
*/
@PreAuthorize("@ss.hasPermi('manage:emp:list')")
@GetMapping("/operationList/{innerCode}")
public AjaxResult operationList(@PathVariable String innerCode) {
// 根据innerCode查询售货机信息
VendingMachine vm = vendingMachineService.selectVendingMachineByInnerCode(innerCode);
if (vm == null) return error("售货机不存在");
// 根据区域id、角色编号、员工状态查询运维人员列表
Emp emp = new Emp(); // 封装查询条件对象
emp.setRegionId(vm.getRegionId()); // 设备所属区域id
emp.setRoleCode(DkdContants.ROLE_CODE_OPERATOR); // 角色编码:维修员(1003)
emp.setStatus(DkdContants.EMP_STATUS_NORMAL); // 员工状态:启用(1)
return success(empService.selectEmpList(emp));
}
6、新增工单
本系统中有两类工单需要创建,分别是:
- 运维工单:运维工单主要是对售货机的操作,又可以细分为
投放
工单、撤机
工单、维修
工单 - 运营工单:运营工单主要是对货物的操作,只有一种就是
补货
工单
运营工单和运维工单共用一个后端新增工单接口,提高代码复用性。
- 页面原型
- 接口文档(需要创建DTO)
- 实现思路
新增工单时序图
新增工单业务流程图
- 查询售货机是否存在
- 校验售货机状态与工单类型是否相符
- 检查设备是否有未完成的同类型工单
- 查询并校验员工是否存在
- 校验员工区域是否匹配
- TaskDTO->Task并补充属性,保存工单
- 判断是否为补货工单
- TaskDetailsDTO->TaskDetails并补充属性,批量保存
- 代码实现
TaskDetailsDTO
/**
* 补货工单详情DTO
*/
@Data
public class TaskDetailsDTO {
private String channelCode; // 货道编号
private Long expectCapacity; // 期望补货数量
private Long skuId; // 商品Id
private String skuName; // 商品名称
private String skuImage; // 商品图片
}
TaskDTO
/**
* 工单基本信息DTO
*/
@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; // 工单详情(只有补货工单才涉及)
}
TaskDetailsMapper和xml
/**
* 批量新增工单详情
* @param taskDetailsList
* @return 结果
*/
int batchInsertTaskDetails(List<TaskDetails> taskDetailsList);
<!-- 批量新增工单详情 -->
<insert id="batchInsertTaskDetails" parameterType="java.util.List">
insert into tb_task_details (task_id, channel_code, expect_capacity, sku_id, sku_name, sku_image)
values
<foreach collection="list" item="item" index="index" separator=", ">
(#{item.taskId}, #{item.channelCode}, #{item.expectCapacity}, #{item.skuId}, #{item.skuName}, #{item.skuImage})
</foreach>
</insert>
ITaskDetailsService和实现类
/**
* 批量新增工单详情
* @param taskDetailsList
* @return 结果
*/
int batchInsertTaskDetails(List<TaskDetails> taskDetailsList);
/**
* 批量新增工单详情
* @param taskDetailsList
* @return 结果
*/
@Override
public int batchInsertTaskDetails(List<TaskDetails> taskDetailsList) {
return taskDetailsMapper.batchInsertTaskDetails(taskDetailsList);
}
ITaskService和实现类
/**
* 新增运营或运维工单
* @param taskDTO
* @return
*/
int insertTaskDTO(TaskDTO taskDTO);
@Autowired
private IVendingMachineService vendingMachineService;
@Autowired
private IEmpService empService;
@Autowired
private RedisTemplate redisTemplate; // 注入redis的模板操作对象
@Autowired
private ITaskDetailsService taskDetailsService;
/**
* 新增运营或运维工单
* 事务管理:工单表、工单详情表
* @param taskDTO
* @return
*/
@Transactional
@Override
public int insertTaskDTO(TaskDTO taskDTO) {
// 查询售货机是否存在
VendingMachine vm = vendingMachineService.selectVendingMachineByInnerCode(taskDTO.getInnerCode());
if (vm == null) throw new ServiceException("设备不存在");
// 校验售货机状态和工单类型是否相符
checkCreateTask(vm.getVmStatus(), taskDTO.getProductTypeId());
// 检查设备是否有未完成的同类型工单
hasTask(taskDTO);
// 查询并校验员工是否存在(保证安全性)
Emp emp = empService.selectEmpById(taskDTO.getUserId());
if (emp == null) throw new ServiceException("所指派员工不存在");
// 校验员工区域是否匹配
if (!emp.getRegionId().equals(vm.getRegionId())) throw new ServiceException("员工所在区域与设备区域不一致,无法处理此工单");
// 将DTO转为PO并补充属性,保存工单
Task task = BeanUtil.copyProperties(taskDTO, Task.class); // 将DTO中的6个公共字段拷贝到PO中
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 result = taskMapper.insertTask(task);
// 判断是否为补货工单,如果是则批量新增工单详情
if (DkdContants.TASK_TYPE_SUPPLY.equals(task.getProductTypeId())) {
// 保存工单详情
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 result;
}
/**
* 生成并获取当天工单编号(唯一表示)
* 生成格式:当天日期 + redis自增序列(补齐4位)
* 如:202409240001 ~ 202409249999
* 该方法首先尝试从Redis中获取当天的任务代码计数,如果不存在,则初始化为1并返回"日期0001"格式的字符串。
* 如果存在,则对计数加1并返回更新后的任务代码。
* @return 工单编号
*/
private 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存在,redis计数器+1(0002),确保字符串长度为4位
return dateStr + StrUtil.padPre(redisTemplate.opsForValue().increment(key).toString(), 4, '0');
}
/**
* 检查该设备是否有未完成的同类型工单
* @param taskDTO
*/
private void hasTask(TaskDTO taskDTO) {
// 创建Task查询条件对象,并设置设备编号和工单类型,以及工单状态为进行中
Task task = new Task();
task.setInnerCode(taskDTO.getInnerCode());
task.setProductTypeId(taskDTO.getProductTypeId());
task.setTaskStatus(DkdContants.TASK_STATUS_PROGRESS); // 工单状态为进行中
// 调用taskMapper查询数据库查看是否有符合条件的工单列表
List<Task> taskList = taskMapper.selectTaskList(task);
// 如果存在未完成的同类型工单,抛出异常
if (CollUtil.isNotEmpty(taskList)) throw new ServiceException("该设备已有未完成的工单,不能重复创建");
// 如果存在已创建,待处理的同类型工单,抛出异常
task.setTaskStatus(DkdContants.TASK_STATUS_CREATE); // 工单状态为创建(待处理)
taskList = taskMapper.selectTaskList(task);
if (CollUtil.isNotEmpty(taskList)) throw new ServiceException("该设备已有待处理的工单,不能重复创建");
}
/**
* 校验售货机状态和工单类型是否相符
* @param vmStatus 设备状态
* @param productTypeId 工单类型id
*/
private void checkCreateTask(Long vmStatus, Long productTypeId) {
// 如果是投放工单,设备在运行中,无法投放,抛出异常
if (Objects.equals(productTypeId, DkdContants.TASK_TYPE_DEPLOY) && Objects.equals(vmStatus, DkdContants.VM_STATUS_RUNNING)) {
throw new ServiceException("该设备状态为运行中,无法进行投放");
}
// 如果是维修工单,设备不在运行中,抛出异常(未投放和撤机)
if (Objects.equals(productTypeId, DkdContants.TASK_TYPE_REPAIR) && !Objects.equals(vmStatus, DkdContants.VM_STATUS_RUNNING)) {
throw new ServiceException("该设备状态不在运行中,无法进行维修");
}
// 如果是补货工单,设备不在运行中,抛出异常(未投放和撤机)
if (Objects.equals(productTypeId, DkdContants.TASK_TYPE_SUPPLY) && !Objects.equals(vmStatus, DkdContants.VM_STATUS_RUNNING)) {
throw new ServiceException("该设备状态不在运行中,无法进行补货");
}
// 如果是撤机工单,设备不在运行中,无法撤机,抛出异常
if (Objects.equals(productTypeId, DkdContants.TASK_TYPE_REVOKE) && !Objects.equals(vmStatus, DkdContants.VM_STATUS_RUNNING)) {
throw new ServiceException("该设备状态不在运行中,无法进行撤机");
}
}
- 测试新增工单
- 测试再次添加同类型工单
7、取消工单
- 需求:对于未完成的工单,管理员可以进行取消操作。
运维工单和运营工单共享同一套取消工单后端接口。
- 接口文档
- 实现思路
- 代码实现
TaskController
/**
* 取消工单
*/
@PreAuthorize("@ss.hasPermi('manage:task:edit')")
@Log(title = "工单", businessType = BusinessType.UPDATE)
@PutMapping("/cancel")
public AjaxResult cancelTask(@RequestBody Task task) {
return toAjax(taskService.cancelTask(task));
}
ITaskService
/**
* 取消工单
* @param task
* @return 结果
*/
int cancelTask(Task task);
/**
* 取消工单
* @param task
* @return 结果
*/
@Override
public int cancelTask(Task task) {
// 判断工单状态是否可以取消
Task taskDb = taskMapper.selectTaskByTaskId(task.getTaskId());
if (DkdContants.TASK_STATUS_CANCEL.equals(taskDb.getTaskStatus())) {
throw new ServiceException("该工单已取消,不能再次取消");
}
// 判断工单状态是否为已完成,如果是,则抛出异常
if (DkdContants.TASK_STATUS_FINISH.equals(taskDb.getTaskStatus())) {
throw new ServiceException("该工单已完成,不能取消");
}
// 设置更新字段,注意更新使用的是前端的task作为参数
task.setTaskStatus(DkdContants.TASK_STATUS_CANCEL); // 工单状态:取消
task.setUpdateTime(DateUtils.getNowDate()); // 更新时间
return taskMapper.updateTask(task); // 更新工单
}
- 测试取消工单功能
8、查看补货详情
- 需求:运营工单页面可以查看补货详情。
- 页面原型
- 接口文档
- 实现思路
- 代码实现
TaskDetailsController
/**
* 查看工单补货详情
*/
@PreAuthorize("@ss.hasPermi('manage:taskDetails:list')")
@GetMapping("/byTaskId/{taskId}")
public AjaxResult byTaskId(@PathVariable Long taskId) {
TaskDetails taskDetails = new TaskDetails();
taskDetails.setTaskId(taskId);
return success(taskDetailsService.selectTaskDetailsList(taskDetails));
}
9、Knife4j
如果不习惯使用 swagger
可以使用 前端UI
的增强解决方案 knife4j
,对比 swagger
相比有以下优势,友好界面,离线文档,接口排序,安全控制,在线调试,文档清晰,注解增强,容易上手。
ruoyi-common\pom.xml
模块添加整合依赖
<!-- knife4j -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
views/tool/swagger/index.vue
修改跳转访问地址(修改为knife4j的默认访问地址)
const url = ref(import.meta.env.VITE_APP_BASE_API + "/doc.html")
- 登录系统,访问菜单系统工具/系统接口,出现如下图表示成功。
- TaskDetailsController添加swagger注解
@Api
: 用于类级别,描述API的标签和描述。@ApiOperation
: 用于方法级别,描述一个HTTP操作。@ApiParam
: 用于参数级别,描述请求参数。
/**
* 工单详情Controller
*
* @author Aizen
* @date 2024-09-23
*/
@Api(value="工单详情管理接口", tags={"工单详情"})
@RestController
@RequestMapping("/manage/taskDetails")
public class TaskDetailsController extends BaseController
{
@Autowired
private ITaskDetailsService taskDetailsService;
@ApiOperation(value = "获取工单详情列表", notes = "查询所有工单详情记录")
@ApiImplicitParams({
@ApiImplicitParam(name = "taskDetails", value = "工单详情对象", required = true, dataType = "TaskDetails", paramType = "query")
})
@PreAuthorize("@ss.hasPermi('manage:taskDetails:list')")
@GetMapping("/list")
public TableDataInfo list(TaskDetails taskDetails) {
startPage();
List<TaskDetails> list = taskDetailsService.selectTaskDetailsList(taskDetails);
return getDataTable(list);
}
@ApiOperation(value = "导出工单详情列表", notes = "导出工单详情记录到Excel文件")
@ApiImplicitParams({
@ApiImplicitParam(name = "taskDetails", value = "工单详情对象", required = true, dataType = "TaskDetails", paramType = "form")
})
@PreAuthorize("@ss.hasPermi('manage:taskDetails:export')")
@Log(title = "工单详情", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, @ApiParam(value = "工单详情对象", required = true) TaskDetails taskDetails) {
List<TaskDetails> list = taskDetailsService.selectTaskDetailsList(taskDetails);
ExcelUtil<TaskDetails> util = new ExcelUtil<TaskDetails>(TaskDetails.class);
util.exportExcel(response, list, "工单详情数据");
}
@ApiOperation(value = "获取工单详情详细信息", notes = "根据ID获取工单详情")
@ApiImplicitParam(name = "detailsId", value = "工单详情ID", required = true, dataType = "Long", paramType = "path")
@PreAuthorize("@ss.hasPermi('manage:taskDetails:query')")
@GetMapping(value = "/{detailsId}")
public R<TaskDetails> getInfo(@PathVariable("detailsId") Long detailsId) {
return R.ok(taskDetailsService.selectTaskDetailsByDetailsId(detailsId));
}
@ApiOperation(value = "新增工单详情", notes = "创建新的工单详情记录")
@ApiImplicitParams({
@ApiImplicitParam(name = "taskDetails", value = "工单详情对象", required = true, dataType = "TaskDetails", paramType = "body")
})
@PreAuthorize("@ss.hasPermi('manage:taskDetails:add')")
@Log(title = "工单详情", businessType = BusinessType.INSERT)
@PostMapping
public R add(@RequestBody TaskDetails taskDetails) {
return R.toAjax(taskDetailsService.insertTaskDetails(taskDetails));
}
@ApiOperation(value = "修改工单详情", notes = "更新现有的工单详情记录")
@ApiImplicitParams({
@ApiImplicitParam(name = "taskDetails", value = "工单详情对象", required = true, dataType = "TaskDetails", paramType = "body")
})
@PreAuthorize("@ss.hasPermi('manage:taskDetails:edit')")
@Log(title = "工单详情", businessType = BusinessType.UPDATE)
@PutMapping
public R edit(@RequestBody TaskDetails taskDetails) {
return R.toAjax(taskDetailsService.updateTaskDetails(taskDetails));
}
@ApiOperation(value = "删除工单详情", notes = "根据ID批量删除工单详情记录")
@ApiImplicitParams({
@ApiImplicitParam(name = "detailsIds", value = "工单详情ID数组", required = true, dataType = "Long[]", paramType = "path")
})
@PreAuthorize("@ss.hasPermi('manage:taskDetails:remove')")
@Log(title = "工单详情", businessType = BusinessType.DELETE)
@DeleteMapping("/{detailsIds}")
public R remove(@PathVariable Long[] detailsIds) {
return R.toAjax(taskDetailsService.deleteTaskDetailsByDetailsIds(detailsIds));
}
@ApiOperation(value = "查看工单补货详情", notes = "根据工单ID获取工单详情列表")
@ApiImplicitParam(name = "taskId", value = "工单ID", required = true, dataType = "Long", paramType = "path")
@PreAuthorize("@ss.hasPermi('manage:taskDetails:list')")
@GetMapping("/byTaskId/{taskId}")
public R<List<TaskDetails>> byTaskId(@PathVariable Long taskId) {
TaskDetails taskDetails = new TaskDetails();
taskDetails.setTaskId(taskId);
return R.ok(taskDetailsService.selectTaskDetailsList(taskDetails));
}
}
注意:若依框架的AjaxResult由于继承自HashMap导致与Swagger和knife4j不兼容的问题,选择替换返回值类型为R以解决Swagger解析问题,减少整体改动量。
- TaskDetails实体类添加swagger注解
@ApiModelProperty
注解来描述每个字段的意义
/**
* 工单详情对象 tb_task_details
*
* @author Aizen
* @date 2024-09-23
*/
@ApiModel(value = "TaskDetails", description = "工单详情")
public class TaskDetails extends BaseEntity {
private static final long serialVersionUID = 1L;
/** $column.columnComment */
@ApiModelProperty(value = "工单详情ID")
private Long detailsId;
/** 工单Id */
@Excel(name = "工单Id")
@ApiModelProperty("工单Id")
private Long taskId;
/** 货道编号 */
@Excel(name = "货道编号")
@ApiModelProperty("货道编号")
private String channelCode;
/** 补货期望容量 */
@Excel(name = "补货期望容量")
@ApiModelProperty("补货期望容量")
private Long expectCapacity;
/** 商品Id */
@Excel(name = "商品Id")
@ApiModelProperty("商品Id")
private Long skuId;
/** $column.columnComment */
@Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
@ApiModelProperty("商品名称")
private String skuName;
/** $column.columnComment */
@Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
@ApiModelProperty("商品图片")
private String skuImage;
}
- 接口测试
测试查看工单补货详情
接口,F12获取工单id
通过Application的Cookies中获取Admin-Token,填入请求头(必须有Authorization才能测试接口)
发送请求
- 设置文档信息
修改作者信息
三、运营管理App
1、Android模拟器
本项目的App客户端部分已经由前端团队进行开发完成,并且以apk的方式提供出来,供我们测试使用,如果要运行apk,需要先安装安卓的模拟器。
可以选择国内的安卓模拟器产品,比如:网易mumu、雷电、夜神等。课程中使用网易mumu模拟器,官网地址:https://mumu.163.com/mnqsjshell/。安装到非中文路径即可。
需要让模拟器中的App能够连接我们自己本地代码,需要修改下URL地址:
注意:10.0.2.2在mumu模拟器中默认找的是本机的地址,也可以填本机的IP,但不能是localhost或127.0.0.1,9007是帝可得app后端项目的端口号。
2、Java后端
运营管理App的java后端技术栈:SpringBoot+MybatisPlus+阿里云短信
本项目运营管理App的java后端已开发完成,导入idea中即可
本项目连接的也是dkd数据库,如果密码不是root可以进行修改
启动并测试app后端,输入帝可得员工手机号,验证码暂时默认12345,点击登录。
登录后可访问app即部署成功。
3、功能测试
(1)运维工单
帝可得管理端,创建新设备
设备h8zdv0pY创建成功。
帝可得管理端,复制设备编号,创建投放工单,指定运维人员。
投放工单创建成功,状态为待办(工单已创建,等待工作人员接单)。
该区域下负责此工单员工登录运营管理App端,即可查看待办工单,可以选择 拒绝 或 接受。
如果点击接受,帝可得管理端工单状态改为进行,app端将从待办工单转移到进行工单。
在进行工单界面,可以点击查看详情,选择取消、完成
如果点击完成工单,帝可得管理端工单状态改为完成,app端可在全部工单里查看已完成或已取消的工单。
帝可得管理端设备状态改为运营,表示设备投放成功。
为运营中的设备创建运维工单
工作人员点击拒绝,需填写拒绝原因并提交。
工单被拒绝接单,帝可得管理端工单状态改为取消。
(2)补货工单
帝可得管理端,为货道关联商品
帝可得管理端,创建补货工单
填写补货详情列表中的补货数量。
投放工单创建成功,状态为待办(工单已创建,等待工作人员接单)。
该区域下负责此工单的员工登录运营管理App端,即可查看待办工单,可以选择 拒绝 或 接受。
点击工单查看详情,显示补货详情等信息。
如果点击接受,帝可得管理端工单状态改为进行
在进行工单界面,可以点击查看详情,选择取消、完成
如果点击完成工单,帝可得管理端工单状态改为完成
数据库货道表的库存已同步更新
四、设备屏幕端
商品列表–选择支付方式–显示支付二维码–用户扫码完成支付
设备屏幕端的java后端技术栈:SpringBoot+MybatisPlus
1、设备屏幕
本项目的设备屏幕客户端部分已经由前端团队进行开发完成,双击打开index.html
即可
2、Java后端
本项目设备屏幕端的java后端已开发完成,导入idea中打开
配置MySQL和Redis的连接信息,与之前同理。
3、功能测试
在设备屏幕端加上innerCode=设备编号
,即可显示当前设备货道信息。
帝可得管理端,设备策略分配,设置折扣信息
再次访问设备屏幕端,价格就是折扣的了
4、支付出货流程
我们能够从屏幕上看到支付二维码,其实是经历了支付流程,屏幕端实际上是一个H5页面,向后端发起支付请求,订单服务首先会创建订单,然后调用第三方支付来获得用于生成支付二维码的链接。
然后订单微服务将二维码链接返回给屏幕端,屏幕端生成二维码图片展示。
用户看到二维码后,拿出手机扫码支付,此时第三方支付平台确认用户支付成功后会回调订单服务。订单服务收到回调信息后修改订单状态,并通知设备发货(系统通知设备进行发货,使用到物联网通信技术MQTT,想智能售货机发送指令,设备会从相应的货道中掉出商品,完成发货,并自动更新库存信息-1)。MQTT的国内技术实现:emqx。
由于第三方支付平台没有针对于个人开放,所以并没有实现具体的支付代码。
这里推荐一款简化支付流程开发的统一管理框架elegent-pay:https://gitee.com/myelegent/elegent-pay
帝可得项目的开发到这里就结束了,如果后期有修改会有补充~