文章目录
- 一、商品管理
- 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-optionv-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.valuegetSku(_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-dialogwidth="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-rowv-for="vmRowIndex in vmType.vmRow":key="vmRowIndex"type="flex":gutter="16"class="space"><el-colv-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-iconv-if="vmType.vmCol > 5"class="arrow arrow-left":class="scrollStatus === 'LEFT' ? 'disabled' : ''"@click="handleClickPrevButton"><ArrowLeft/></el-icon><el-iconv-if="vmType.vmCol > 5"class="arrow arrow-right":class="scrollStatus === 'RIGHT' ? 'disabled' : ''"@click="handleClickNextButton"><ArrowRight/></el-icon></div><div class="dialog-footer"><el-buttontype="primary"class="el-button--primary1"@click="handleClick">确认</el-button></div><!-- 商品选择 --><el-dialogwidth="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-formref="form"class="search":model="listQuery":label-width="formLabelWidth"><el-form-item label="商品名称:"><el-row type="flex" justify="space-between"><el-col><el-inputv-model="listQuery.skuName"placeholder="请输入"clearableclass="sku-name"@input="resetPageIndex"/></el-col><el-col><el-buttontype="primary"class="el-button--primary1"@click="handleListOpen"><el-icon><Search /></el-icon> 查询</el-button></el-col></el-row></el-form-item></el-form><el-scrollbarref="scroll2"v-loading="listSkuLoading"class="scrollbar"><el-row v-loading="listSkuLoading" :gutter="20"><el-colv-for="(item, index) in listSkuData.rows":key="index":span="5"><div class="item"><!-- TODO: 只有一行的时候考虑 --><divclass="sku":class="index < 5 ? 'space' : ''"@click="handleCurrentChange(index)"><imgv-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-iconv-if="pageCount > 1"class="arrow arrow-left":class="pageCount === 1 ? 'disabled' : ''"@click="handleClickPrev"><ArrowLeft/></el-icon><el-iconv-if="pageCount > 1"class="arrow arrow-right":class="listQuery.pageIndex === pageCount ? 'disabled' : ''"@click="handleClickNext"><ArrowRight/></el-icon></div><div class="dialog-footer"><el-buttontype="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 = falseemit('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 = falseemit('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"><imgclass="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-buttontype="text"class="el-button--primary-text"@click="handleSetClick">添加</el-button><el-buttontype="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;// 关联商品idprivate 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.DruidDataSourcedriverClassName: com.mysql.cj.jdbc.Driverdruid:# 主库数据源master:url: jdbc:mysql://localhost:3306/dkd?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=trueusername: rootpassword: 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) {// 关联最新商品idchannel.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-dialogwidth="630px"title="工单详情":close-on-click-modal="false":close-on-press-escape="false"v-model="visible"@close="cancel"><div class="task-status"><imgv-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><imgv-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-buttonv-if="taskDada.taskStatus === 1 || taskDada.taskStatus === 2"@click="handleCancelTask">取消工单</el-button><el-buttontype="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,},// 工单idtaskId: {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-dialogwidth="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-tablestyle="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-numberv-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-dialogwidth="630px"title="补货详情":close-on-click-modal="false":close-on-press-escape="false"v-model="visible"append-to-body@close="cancel"@open="open"><el-scrollbarclass="scrollbar"style="height: 330px;"><el-tablestyle="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-dialogwidth="630px"title="工单详情":close-on-click-modal="false":close-on-press-escape="false"v-model="visible"@close="cancel"@open="open"><div class="task-status"><imgv-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><imgv-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-buttonv-if="taskDada.taskStatus === 1 || taskDada.taskStatus === 2"@click="handleCancelTask">取消工单</el-button><el-buttontype="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,},// 工单idtaskId: {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-dialogwidth="630px"title="工单配置":close-on-click-modal="false":close-on-press-escape="false"v-model="visible"append-to-body@close="cancel"@open="open"><el-formref="taskRef":inline="true":model="form":rules="rules"label-width="120"><el-form-item label="补货警戒线:" prop="alertValue"><el-input-numberv-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-inputv-model="queryParams.taskCode"placeholder="请输入工单编号"clearable@keyup.enter="handleQuery"/></el-form-item><el-form-item label="工单状态" prop="taskStatus"><el-selectv-model="queryParams.taskStatus"placeholder="请选择工单状态"clearable><el-optionv-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-buttontype="primary"plainicon="Plus"@click="handleAdd"v-hasPermi="['manage:task:add']">新增</el-button><el-button type="primary" plain @click="openTaskConfig">工单配置</el-button></el-col><right-toolbarv-model:showSearch="showSearch"@queryTable="getList"></right-toolbar></el-row><el-tablev-loading="loading":data="taskList"@selection-change="handleSelectionChange"><el-table-column type="selection" width="55" align="center" /><el-table-columnlabel="序号"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-columnlabel="工单类型"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-columnlabel="创建时间"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-columnlabel="操作"align="center"class-name="small-padding fixed-width"><template #default="scope"><el-buttonlinktype="primary"@click="openTaskDetailDialog(scope.row)"v-hasPermi="['manage:task:edit']">查看详情</el-button></template></el-table-column></el-table><paginationv-show="total > 0":total="total"v-model:page="queryParams.pageNum"v-model:limit="queryParams.pageSize"@pagination="getList"/><!-- 添加工单对话框 --><el-dialog :title="title" v-model="open" width="500px" append-to-body><el-form ref="taskRef" :model="form" :rules="rules" label-width="100px"><el-form-item label="设备编号" prop="innerCode"><el-inputv-model="form.innerCode"placeholder="请输入设备编号"@blur="handleCode"/></el-form-item><el-form-item label="工单类型" prop="productTypeId"><el-selectv-model="form.productTypeId"placeholder="请选择工单类型"clearable><el-optionv-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-selectv-model="form.userId"placeholder="请选择":filterable="true"><el-optionv-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-inputtype="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-inputv-model="queryParams.taskCode"placeholder="请输入工单编号"clearable@keyup.enter="handleQuery"/></el-form-item><el-form-item label="工单状态" prop="taskStatus"><el-selectv-model="queryParams.taskStatus"placeholder="请选择工单状态"clearable><el-optionv-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-selectv-model="queryParams.productTypeId"placeholder="请选择工单类型"clearable><el-optionv-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-buttontype="primary"plainicon="Plus"@click="handleAdd"v-hasPermi="['manage:task:add']">新增</el-button></el-col><right-toolbarv-model:showSearch="showSearch"@queryTable="getList"></right-toolbar></el-row><el-tablev-loading="loading":data="taskList"@selection-change="handleSelectionChange"><el-table-column type="selection" width="55" align="center" /><el-table-columnlabel="序号"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-columnlabel="工单类型"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-columnlabel="创建时间"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-columnlabel="操作"align="center"class-name="small-padding fixed-width"><template #default="scope"><el-buttonlinktype="primary"@click="openTaskDetailDialog(scope.row)"v-hasPermi="['manage:task:edit']">查看详情</el-button></template></el-table-column></el-table><paginationv-show="total > 0":total="total"v-model:page="queryParams.pageNum"v-model:limit="queryParams.pageSize"@pagination="getList"/><!-- 添加工单对话框 --><el-dialog :title="title" v-model="open" width="500px" append-to-body><el-form ref="taskRef" :model="form" :rules="rules" label-width="100px"><el-form-item label="设备编号" prop="innerCode"><el-inputv-model="form.innerCode"placeholder="请输入设备编号"@blur="handleCode"/></el-form-item><el-form-item label="工单类型" prop="productTypeId"><el-selectv-model="form.productTypeId"placeholder="请选择工单类型"clearable><el-optionv-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-selectv-model="form.userId"placeholder="请选择":filterable="true"><el-optionv-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-inputtype="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()); // 设备所属区域idemp.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()); // 设备所属区域idemp.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; // 商品Idprivate String skuName; // 商品名称private String skuImage; // 商品图片
}
TaskDTO
/*** 工单基本信息DTO*/
@Data
public class TaskDTO {private Long createType; // 创建类型private String innerCode; // 关联设备编号private Long userId; // 任务执行人Idprivate Long assignorId; // 用户创建人idprivate 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()); // 所属区域idtask.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
{@Autowiredprivate 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)@PostMappingpublic 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)@PutMappingpublic 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
帝可得项目的开发到这里就结束了,如果后期有修改会有补充~