本章为基于若依开发帝可得项目实战的最后一章,主要完成商品管理、订单管理(帝可得项目的核心模块)、帝可得运营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)项目集成
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>集合。
- 实现思路:
创建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