【若依RuoYi-Vue | 项目实战】帝可得后台管理系统(三)

文章目录

  • 一、商品管理
    • 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、需求说明

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

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

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

  • 关系字段: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="请输入商品价格" />&nbsp;</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

  1. dkd-common\pom.xml 模块添加整合依赖。
<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>4.0.3</version>
</dependency>
  1. 在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());}
}

  1. 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;// 其他略...
}
  1. 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));
}
  1. 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, "商品管理数据");
}
  1. 搜索可口可乐,测试导出功能

  1. 将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>&nbsp;&nbsp;查询</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中查看并根据情况选择执行或取消分配给自己的任务。

工单管理主要涉及到两个功能模块,业务流程如下:

帝可得工单分为两大类 :

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

工单有四种状态:

  1. 待处理
  2. 已接受(进行中)
  3. 已取消
  4. 已完成

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

  • 关系字段: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)

  • 实现思路

新增工单时序图

新增工单业务流程图

  1. 查询售货机是否存在
  2. 校验售货机状态与工单类型是否相符
  3. 检查设备是否有未完成的同类型工单
  4. 查询并校验员工是否存在
  5. 校验员工区域是否匹配
  6. TaskDTO->Task并补充属性,保存工单
  7. 判断是否为补货工单
  8. 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 相比有以下优势,友好界面,离线文档,接口排序,安全控制,在线调试,文档清晰,注解增强,容易上手。

  1. ruoyi-common\pom.xml模块添加整合依赖
<!-- knife4j -->
<dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>3.0.3</version>
</dependency>
  1. views/tool/swagger/index.vue修改跳转访问地址(修改为knife4j的默认访问地址)
const url = ref(import.meta.env.VITE_APP_BASE_API + "/doc.html")
  1. 登录系统,访问菜单系统工具/系统接口,出现如下图表示成功。

  1. 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解析问题,减少整体改动量。

  1. 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;
}
  1. 接口测试

测试查看工单补货详情接口,F12获取工单id

通过Application的Cookies中获取Admin-Token,填入请求头(必须有Authorization才能测试接口)

发送请求

  1. 设置文档信息

修改作者信息




三、运营管理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


帝可得项目的开发到这里就结束了,如果后期有修改会有补充~



本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/435147.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【ADC】使用仪表放大器驱动 SAR 型 ADC 时的输入输出范围

概述 本文学习于TI 高精度实验室课程&#xff0c;介绍使用仪表放大器时 SAR ADC 驱动放大器的注意事项。具体包括&#xff1a;介绍如何使用仪表放大器设计数据转换器驱动电路。 仪表放大器&#xff08;Instrumentation Amplifier&#xff0c;下文简称 INA&#xff09;可抑制输…

开关电源为什么要进行负载测试,负载测试都包含哪些项目?

开关电源在现代电子设备中占据着重要的地位&#xff0c;其性能的稳定性和可靠性直接影响着电子设备的正常运行。为了确保开关电源的质量&#xff0c;需要对其进行负载测试。负载测试可以模拟实际工作环境中的负载情况&#xff0c;检测开关电源在不同负载条件下的输出特性、稳定…

wireshark使用要点

目录 IP过滤 端口过滤 内容过滤 过滤udp 过滤tcp IP过滤 ip.src XXX.XXX.XXX.XXX 只显示消息源地址为XXX.XXX.XXX.XXX的信息 ip.dst XXX.XXX.XXX.XXX 只显示消息目的地址为XXX.XXX.XXX.XXX的信息 ip.addr XXX.XXX.XXX.XXX显示消息源地址为XXX.XXX.XXX.XXX&#xff0…

Python库matplotlib之四

Python库matplotlib之四 小部件(widget)RadioButtons构造器APIs应用实列 Slider构造器APIs应用实列 小部件(widget) 小部件(widget)可与任何GUI后端一起工作。所有这些小部件都要求预定义一个Axes实例&#xff0c;并将其作为第一个参数传递。 Matplotlib不会试图布局这些小部件…

DTH11温湿度传感器

DHT11 是一款温湿度复合传感器&#xff0c;常用于单片机系统中进行环境温湿度的测量。以下是对 DHT11 温湿度传感器的详细讲解&#xff1a; 一、传感器概述 DHT11 数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器。它应用专用的数字模块采集技术和温湿度传感…

【STM32】江科大STM32笔记汇总(已完结)

STM32江科大笔记汇总 STM32学习笔记课程简介(01)STM32简介(02)软件安装(03)新建工程(04)GPIO输出(05)LED闪烁& LED流水灯& 蜂鸣器(06)GPIO输入(07)按键控制LED 光敏传感器控制蜂鸣器(08)OLED调试工具(09)OLED显示屏(10)EXTI外部中断(11)对射式红外传感器计次 旋转编码器…

C++:模拟实现vector

目录 成员变量与迭代器 size capacity empty 迭代器有关函数 实现默认成员函数的前置准备 reserve ​编辑 ​编辑 push_back 构造函数 无参构造 迭代器区间构造 n个val来进行构造 析构函数 拷贝构造函数 赋值重载 增删查改 clear resize pop_back inser…

git add成功后忘记commit的文件丢了?

本文目标&#xff1a;开发人员&#xff0c;在了解git fsck命令用法的条件下&#xff0c;进行git add成功但由于误操作导致丢失的文件找回&#xff0c;达到找回丢失文件的程度。 文章目录 1 痛点2 解决方案3 总结/练习 1 痛点 开发过程中&#xff0c;分支太多&#xff08;基线分…

通信工程学习:什么是MIMO多输入多输出技术

MIMO:多输入多输出技术 MIMO(Multiple-Input Multiple-Output)多输入多输出技术是一种在无线通信中广泛应用的技术,它通过利用多个天线进行数据传输和接收,可以显著提高无线通信系统的性能和容量。以下是对MIMO技术的详细解释: 一、定义与原理 MIMO技术…

铺铜修改后自动重铺

很多初学者对于敷铜操作感到比较麻烦&#xff1a;为什么每次打过孔&#xff0c;修改走线后都需要手动右击-重新修改敷铜。如何提升layout的效率&#xff1f; 版本&#xff1a;Altium Designer 21.9.2 首先&#xff0c;点击面板右边的小齿轮&#xff0c;进入设置 接下来&#…

【国庆要来了】基于Leaflet的旅游路线WebGIS可视化实践

前言 转眼2024年的国庆节马上就要来临了&#xff0c;估计很多小伙伴都计划好了旅游路线。金秋十月&#xff0c;不管是选择出门去看看风景&#xff0c;还是选择在家里看人。从自己生活惯了的城市去别人生活惯了的城市&#xff0c;去感受城市烟火、去感受人文风景&#xff0c;为2…

SpringBoot整合JPA 基础使用

一、什么是JPA ‌‌1.JPA的定义和基本概念‌‌ ‌JPA&#xff08;Java Persistence API&#xff09;‌是Java中用于进行持久化操作的一种规范&#xff0c;它定义了一系列用于操作关系型数据库的API接口。通过这些接口&#xff0c;开发人员可以方便地进行数据库的增删改查等操…

DC00021基于springboot问卷调查管理系统web项目调查问卷管理系统MySQL(附源码)

1、项目功能演示 DC00021基于springboot问卷调查管理系统web项目调查问卷管理系统MySQL 2、项目功能描述 基于springboot问卷调查管理系统包括以下功能&#xff1a; 1、系统登录、系统注册 2、创建题目、题目信息查看 3、创建问卷、我的问卷信息查看 4、创建活动、我的活动信息…

看Threejs好玩示例,学习创新与技术(ThreePipe)

下面这个示例我觉得特别棒&#xff0c;我会推荐给我们的美工&#xff0c;以后产品的宣传图用它。比如下面这个图&#xff0c;不需要PS&#xff0c;仅需拖拽一个照片进去&#xff0c;它会自动铺到笔记本电脑上。完成后点击截图就可以得到高清图片&#xff0c;不需要摆拍和PS。大…

光伏设计难点在哪儿?如何解决?

一、光伏设计的主要难点 1.技术门槛高 光伏设计领域的一大难题在于技术使用的复杂性。用户往往需要下载并安装特定的软件和控件&#xff0c;这些工具操作复杂&#xff0c;增加了学习成本和使用难度。此外&#xff0c;现有的设计工具并非专为光伏设计而生&#xff0c;组件库不…

【华为】用策略路由解决双出口运营商问题

需求描述 不同网段访问互联网资源时&#xff0c;走不同的出口&#xff0c;即PC1走电信出口&#xff0c;PC2走移动出口。 客户在内网接口下应用策略路由后往往出现无法访问内网管理地址的现象&#xff0c;该举例给出解决办法。 拓扑图 基础配置 #sysname R1 # # interface G…

Android15音频进阶之新播放器HwAudioSource(八十六)

简介: CSDN博客专家、《Android系统多媒体进阶实战》一书作者 新书发布:《Android系统多媒体进阶实战》🚀 优质专栏: Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏: 多媒体系统工程师系列【原创干货持续更新中……】🚀 优质视频课程:AAOS车载系统+…

亚马逊IP关联揭秘:发生ip关联如何处理

在亚马逊这一全球领先的电商平台上&#xff0c;IP关联是一个不可忽视的问题&#xff0c;尤其是对于多账号运营的卖家而言。本文将深入解析亚马逊IP关联的含义、影响以及应对策略&#xff0c;帮助卖家更好地理解和应对这一问题。 什么是亚马逊IP关联&#xff1f; 亚马逊IP关联…

Redis篇(应用案例 - 优惠卷秒杀)

目录 一、全局唯一ID 1. 简介 2. Redis实现全局唯一Id 3. 测试类 3.1. 关于 countdownlatch 3.2. CountDownLatch 中有两个最重要的方法 二、添加优惠卷 三、实现秒杀下单 四、库存超卖问题分析 六、乐观锁解决超卖问题 七、优惠券秒杀-一人一单 八、集群环境下的并…

1比25万基础电子地图(港澳版)

我们为你分享过四川、云南、江西、贵州、重庆、青海、西藏、新疆、甘肃、黑龙江、吉林、湖北、内蒙古、广东、广西、浙江、河南、湖南、宁夏、山西、陕西、天津、山东、河北、江苏、福建、辽宁、北京、安徽、上海、海南和台湾的1比25万基础电子地图&#xff0c;现在再为你分享港…