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


本章为基于若依开发帝可得项目实战的最后一章,主要完成商品管理、订单管理(帝可得项目的核心模块)、帝可得运营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-option
          v-for="item in skuClassList"
          :key="item.classId"
          :label="item.className"
          :value="item.classId"
        />
      </el-select>
    </el-form-item>
    <el-form-item label="规格" prop="unit">
      <el-input v-model="form.unit" placeholder="请输入规格" />
    </el-form-item>
    <el-form-item label="商品图片" prop="skuImage">
      <image-upload v-model="form.skuImage" />
    </el-form-item>
  </el-form>
  <template #footer>
    <div class="dialog-footer">
      <el-button type="primary" @click="submitForm">确 定</el-button>
      <el-button @click="cancel">取 消</el-button>
    </div>
  </template>
</el-dialog>

<script setup name="Sku">
import { listSkuClass } from "@/api/manage/skuClass";
import { loadAllParams } from "@/api/page";
/** 修改按钮操作 */
function handleUpdate(row) {
  reset();
  const _skuId = row.skuId || ids.value
  getSku(_skuId).then(response => {
    form.value = response.data;
    form.value.price /= 100;	// 从数据库查询回显时,将价格单位从分转换为元
    open.value = true;
    title.value = "修改商品管理";
  });
}
/** 提交按钮 */
function submitForm() {
  proxy.$refs["skuRef"].validate(valid => {
    if (valid) {
      // 提交到数据库时,将价格单位从元转换回分
      form.value.price *= 100;
      if (form.value.skuId != null) {
        updateSku(form.value).then(response => {
          proxy.$modal.msgSuccess("修改成功");
          open.value = false;
          getList();
        });
      } else {
        addSku(form.value).then(response => {
          proxy.$modal.msgSuccess("新增成功");
          open.value = false;
          getList();
        });
      }
    }
  });
}
/* 查询商品类型列表 */
const skuClassList = ref([]);
function getSkuClassList() {
  listSkuClass(loadAllParams).then(response => {
    skuClassList.value = response.rows;
  });
}
getSkuClassList();
</script>
  • 测试商品列表

  • 测试新增和修改商品

注意数据库存储的商品价格单位是分,因为考虑到float和double计算会丢失精度,因此数据库存储是以分为单位,前端页面展示是以元为单位。

前端在提交到数据存储到数据库前,将价格单位从元转换回分。

从数据库查询回显时,将价格单位从分转换为元。

(2)商品删除

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

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

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

新创建出来的货道关联的商品id默认都为0(表示该货道未关联商品),但由于物理外键约束存在,将无法在货道表插入新sku_id。

因此我们需要编写一个逻辑外键约束,在删除商品(前端传入的是要删除的商品id集合)时,判断商品是否与货道关联,如果已tb_channel.sku_id=tb_sku.sku_id,则给出提示无法删除,否则可以执行删除操作。

SkuServiceImpl:

@Autowired
private IChannelService channelService;
/**
 * 批量删除商品管理
 * 
 * @param skuIds 需要删除的商品管理主键
 * @return 结果
 */
@Override
public int deleteSkuBySkuIds(Long[] skuIds)
{
    // 判断商品id集合是否有关联货道,如果有一个商品关联了货道,阻止删除并抛出异常
    int count = channelService.countChannelBySkuIds(skuIds);
    if (count > 0) throw new ServiceException("此商品被货道关联,无法删除");
    // 没有关联货道,执行删除
    return skuMapper.deleteSkuBySkuIds(skuIds);
}

IChannelService和ChannelServiceImpl:

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

/**
 * 根据商品id集合统计货道数量
 * @param skuIds
 * @return 统计结果
 */
@Override
public int countChannelBySkuIds(Long[] skuIds) {
    return channelMapper.countChannelBySkuIds(skuIds);
}

ChannelMapper接口和xml:

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

<select id="countChannelBySkuIds" resultType="java.lang.Integer">
    select count(1) from tb_channel where sku_id in
    <foreach item="id" collection="array" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>
  • 测试商品删除

5、商品批量导入

需求:点击导入数据弹出导入数据弹窗,上传合法Excel文件,实现商品的批量导入。

  • 页面原型

  • 接口文档

注意:请求头Headers里需要携带Authorization权限校验信息,才能进行文件上传。

支持Excel单文件上传,实现商品信息的批量导入。

  • 实现细节说明

对于前后端分离项目,都会存在一个跨域请求的问题。若依在 vite.config.js 中配置了代理转发,每个前端请求的前缀都需要有 /dev-api ,才能被代理到目标服务器的8080端口上,路由重写中将/dev-api替换为空字符串。

我们在实现前端发送请求时,不需要将开发环境前缀 /dev-api 硬编码拼接到请求地址中,可以使用 .env.development 中预定义好的 VITE_APP_BASE_API 变量作为baseUrl进行请求地址的拼接。

若依在 utils/request.js 请求工具类的api中,为每次请求的请求头headers里都携带了 Authorization(="Bearer " + token),我们的接口文档中也要求有这个权限校验信息。

因此我们可以借鉴若依的写法来构造我们的文件上传请求信息。

<script setup name="Sku">
import { getToken } from "@/utils/auth";

/* 上传地址 */
const uploadExcelUrl = ref(import.meta.env.VITE_APP_BASE_API + "/manage/sku/import"); // 上传excel文件地址
/* 上传请求头 */
const headers = ref({ Authorization: "Bearer " + getToken() });
</script>

(1)前端实现

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

<!--  导入按钮-->
<el-col :span="1.5">
    <el-button type="warning" plain icon="Upload" @click="handleExcelImport" v-hasPermi="['manage:sku:add']">导入</el-button>
</el-col>

<!-- 数据导入对话框 -->
<el-dialog title="数据导入" v-model="importOpen" width="400px" append-to-body>
  <el-upload ref="uploadRef" class="upload-demo"
    :action="uploadExcelUrl"
    :headers="headers"
    :on-success="handleUploadSuccess"
    :on-error="handleUploadError"
    :before-upload="handleBeforeUpload"         
    :limit="1"
    :auto-upload="false">
    <template #trigger>
      <el-button type="primary">上传文件</el-button>
    </template>
    <el-button class="ml-3" type="success" @click="submitUpload">
      上传
    </el-button>
    <template #tip>
      <div class="el-upload__tip">
        上传文件仅支持,xls/xlsx格式,文件大小不得超过1M
      </div>
    </template>
  </el-upload>
</el-dialog>

<script setup name="Sku">
import { getToken } from "@/utils/auth";
    
/* 打开数据导入对话框 */
const importOpen = ref(false);
function handleExcelImport() {
  importOpen.value = true;
}

/* 上传excel */
const uploadRef = ref({});
function submitUpload() {
  uploadRef.value.submit()
}

/* 上传地址 */
const uploadExcelUrl = ref(import.meta.env.VITE_APP_BASE_API + "/manage/sku/import"); // 上传excel文件地址
/* 上传请求头 */
const headers = ref({ Authorization: "Bearer " + getToken() });
    
const props = defineProps({
  modelValue: [String, Object, Array],
  // 大小限制(MB)
  fileSize: {
    type: Number,
    default: 1,
  },
  // 文件类型, 例如["xls", "xlsx"]
  fileType: {
    type: Array,
    default: () => ["xls", "xlsx"],
  },
});

// 上传前loading加载
function handleBeforeUpload(file) {
  let isExcel = false;
  if (props.fileType.length) {
    let fileExtension = "";
    if (file.name.lastIndexOf(".") > -1) {
      fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
    }
    isExcel = props.fileType.some(type => {
      if (file.type.indexOf(type) > -1) return true;
      if (fileExtension && fileExtension.indexOf(type) > -1) return true;
      return false;
    });
  } 
  if (!isExcel) {
    proxy.$modal.msgError(
      `文件格式不正确, 请上传${props.fileType.join("/")}格式文件!`
    );
    return false;
  }
  if (props.fileSize) {
    const isLt = file.size / 1024 / 1024 < props.fileSize;
    if (!isLt) {
      proxy.$modal.msgError(`上传excel大小不能超过 ${props.fileSize} MB!`);
      return false;
    }
  }
  proxy.$modal.loading("正在上传excel,请稍候...");
}

// 上传成功回调
function handleUploadSuccess(res, file) {
  if (res.code === 200) {
    proxy.$modal.msgSuccess("上传excel成功");
    excelOpen.value = false;
    getList();
  }else{
    proxy.$modal.msgError(res.msg);
  } 
  // 清空文件上传列表记录
  uploadRef.value.clearFiles();
  // 关闭正在上传的loading提示信息
  proxy.$modal.closeLoading();
}

// 上传失败
function handleUploadError() {
  proxy.$modal.msgError("上传excel失败");
  // 清空文件上传列表记录
  uploadRef.value.clearFiles();
  // 关闭正在上传的loading提示信息
  proxy.$modal.closeLoading();
}
</script>
  • 测试前端上传,上传合法的Excel文件,前端状态码200,后端响应状态码500(因为后端还没有编写)

并且成功在headers中携带了拼接好的token。

  • 测试上传不合法文件,提示上传失败,并限制不能上传多个文件。

对于不合法的文件直接在前端拦截,并在上传成功或失败后都清空文件上传列表。

(2)后端实现

  • SkuController
/**
 * 导入商品管理列表
 */
@PreAuthorize("@ss.hasPermi('manage:sku:add')")
@Log(title = "商品管理", businessType = BusinessType.IMPORT)
@PostMapping("/import")
public AjaxResult excelImport(MultipartFile file) throws Exception {
    ExcelUtil<Sku> util = new ExcelUtil<Sku>(Sku.class);
    List<Sku> skuList = util.importExcel(file.getInputStream());
    return toAjax(skuService.insertSkus(skuList));
}
  • SkuMapper和xml
/**
 * 批量新增商品管理
 * @param skuList
 * @return 结果
 */
public int insertSkus(List<Sku> skuList);

<insert id="insertSkus" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="skuId">
    insert into tb_sku (sku_name, sku_image, brand_Name, unit, price, class_id)
    values
    <foreach item="item" index="index" collection="list" separator=",">
        (#{item.skuName}, #{item.skuImage}, #{item.brandName}, #{item.unit}, #{item.price}, #{item.classId})
    </foreach>
</insert>
  • ISkuService和SkuServiceImpl
/**
 * 批量新增商品管理
 * @param skuList
 * @return 结果
 */
public int insertSkus(List<Sku> skuList);

/**
 * 批量新增商品管理
 * @param skuList
 * @return 结果
 */
@Override
public int insertSkus(List<Sku> skuList) {
    return skuMapper.insertSkus(skuList);
}

6、EasyExcel

(1)介绍

官方地址:https://easyexcel.alibaba.com/

Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便

(2)项目集成

若依插件集成地址:https://doc.ruoyi.vip/ruoyi-vue/document/cjjc.html#%E9%9B%86%E6%88%90easyexcel%E5%AE%9E%E7%8E%B0excel%E8%A1%A8%E6%A0%BC%E5%A2%9E%E5%BC%BA

  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-dialog
    width="940px"
    title="货道设置"
    v-model="visible"
    :close-on-click-modal="false"
    :close-on-press-escape="false"
    @open="handleGoodOpen"
    @close="handleGoodcClose"
  >
    <div class="vm-config-channel-dialog-wrapper">
      <div class="channel-basic">
        <span class="vm-row">货道行数:{{ vmType.vmRow }}</span>
        <span class="vm-col">货道列数:{{ vmType.vmCol }}</span>
        <span class="channel-max-capacity"
          >货道容量(个):{{ vmType.channelMaxCapacity }}</span
        >
      </div>
      <el-scrollbar ref="scroll" v-loading="listLoading" class="scrollbar">
        <el-row
          v-for="vmRowIndex in vmType.vmRow"
          :key="vmRowIndex"
          type="flex"
          :gutter="16"
          class="space"
        >
          <el-col
            v-for="vmColIndex in vmType.vmCol"
            :key="vmColIndex"
            :span="vmType.vmCol <= 5 ? 5 : 12"
          >
            <ChannelDialogItem
              :current-index="computedCurrentIndex(vmRowIndex, vmColIndex)"
              :channel="channels[computedCurrentIndex(vmRowIndex, vmColIndex)]"
              @openSetSkuDialog="openSetSkuDialog"
              @openRemoveSkuDialog="openRemoveSkuDialog"
            >
            </ChannelDialogItem>
          </el-col>
        </el-row>
      </el-scrollbar>
      <el-icon
        v-if="vmType.vmCol > 5"
        class="arrow arrow-left"
        :class="scrollStatus === 'LEFT' ? 'disabled' : ''"
        @click="handleClickPrevButton"
        ><ArrowLeft
      /></el-icon>
      <el-icon
        v-if="vmType.vmCol > 5"
        class="arrow arrow-right"
        :class="scrollStatus === 'RIGHT' ? 'disabled' : ''"
        @click="handleClickNextButton"
        ><ArrowRight
      /></el-icon>
    </div>
    <div class="dialog-footer">
      <el-button
        type="primary"
        class="el-button--primary1"
        @click="handleClick"
      >
        确认
      </el-button>
    </div>

    <!-- 商品选择 -->
    <el-dialog
      width="858px"
      title="选择商品"
      v-model="skuDialogVisible"
      :close-on-click-modal="false"
      :close-on-press-escape="false"
      append-to-body
      @open="handleListOpen"
      @close="handleListClose"
    >
      <div class="vm-select-sku-dialog-wrapper">
        <!-- 搜索区 -->
        <el-form
          ref="form"
          class="search"
          :model="listQuery"
          :label-width="formLabelWidth"
        >
          <el-form-item label="商品名称:">
            <el-row type="flex" justify="space-between">
              <el-col>
                <el-input
                  v-model="listQuery.skuName"
                  placeholder="请输入"
                  clearable
                  class="sku-name"
                  @input="resetPageIndex"
                />
              </el-col>
              <el-col>
                <el-button
                  type="primary"
                  class="el-button--primary1"
                  @click="handleListOpen"
                >
                  <el-icon><Search /></el-icon>&nbsp;&nbsp;查询
                </el-button>
              </el-col>
            </el-row>
          </el-form-item>
        </el-form>
        <el-scrollbar
          ref="scroll2"
          v-loading="listSkuLoading"
          class="scrollbar"
        >
          <el-row v-loading="listSkuLoading" :gutter="20">
            <el-col
              v-for="(item, index) in listSkuData.rows"
              :key="index"
              :span="5"
            >
              <div class="item">
                <!-- TODO: 只有一行的时候考虑 -->
                <div
                  class="sku"
                  :class="index < 5 ? 'space' : ''"
                  @click="handleCurrentChange(index)"
                >
                  <img
                    v-show="currentRow.skuId === item.skuId"
                    class="selected"
                    src="@/assets/vm/selected.png"
                  />
                  <img class="img" :src="item.skuImage" />
                  <div class="name" :title="item.skuName">
                    {{ item.skuName }}
                  </div>
                </div>
              </div>
            </el-col>
          </el-row>
        </el-scrollbar>
        <el-icon
          v-if="pageCount > 1"
          class="arrow arrow-left"
          :class="pageCount === 1 ? 'disabled' : ''"
          @click="handleClickPrev"
          ><ArrowLeft
        /></el-icon>
        <el-icon
          v-if="pageCount > 1"
          class="arrow arrow-right"
          :class="listQuery.pageIndex === pageCount ? 'disabled' : ''"
          @click="handleClickNext"
          ><ArrowRight
        /></el-icon>
      </div>
      <div class="dialog-footer">
        <el-button
          type="primary"
          class="el-button--primary1"
          @click="handleSelectClick"
        >
          确认
        </el-button>
      </div>
    </el-dialog>
    <!-- end -->
  </el-dialog>
  <!-- end -->
</template>
<script setup>
import { require } from '@/utils/validate';
const { proxy } = getCurrentInstance();
// 滚动插件
import { ElScrollbar } from 'element-plus';
// 接口
import {
  getGoodsList,
  getGoodsType,
  channelConfig,
} from '@/api/manage/channel';
import { listSku } from '@/api/manage/sku';
// 内部组件
import ChannelDialogItem from './ChannelDialogItem.vue';
import { watch } from 'vue';
// 获取父组件参数
const props = defineProps({
  //  弹层隐藏显示
  goodVisible: {
    type: Boolean,
    default: false,
  },
  //   触发的货道信息
  goodData: {
    type: Object,
    default: () => {},
  },
});
// 获取父组件的方法
const emit = defineEmits(['handleCloseGood']);
// ******定义变量******
const visible = ref(false); //货道弹层显示隐藏
const scrollStatus = ref('LEFT');
const listLoading = ref(false);
const vmType = ref({}); //获取货道基本信息
const channels = ref({}); //货道数据
const scroll = ref(null); //滚动条ref
// 监听货道弹层显示/隐藏
watch(
  () => props.goodVisible,
  (val) => {
    visible.value = val;
  }
);
// ******定义方法******
// 获取货道基本信息
const handleGoodOpen = () => {
  getVmType();
  channelList();
};
// 获取货道基本信息
const getVmType = async () => {
  const { data } = await getGoodsType(props.goodData.vmTypeId);
  vmType.value = data;
};
// 获取货道列表
const channelList = async () => {
  listLoading.value = true;
  const { data } = await getGoodsList(props.goodData.innerCode);
  channels.value = data;
  listLoading.value = false;
};
const computedCurrentIndex = (vmRowIndex, vmColIndex) => {
  return (vmRowIndex - 1) * vmType.value.vmCol + vmColIndex - 1;
};
// 关闭货道弹窗

const handleGoodcClose = () => {
    visible.value = false
    emit('handleCloseGood');
};
const handleClickPrevButton = () => {
  scroll.value.wrapRef.scrollLeft = 0;
  scrollStatus.value = 'LEFT';
};

const handleClickNextButton = () => {
  scroll.value.wrapRef.scrollLeft = scroll.value.wrapRef.scrollWidth;
  scrollStatus.value = 'RIGHT';
};
const currentIndex = ref(0);
const channelCode = ref('');
const skuDialogVisible = ref(false); //添加商品弹层
// 删除选中的商品
const openRemoveSkuDialog = (index, code) => {
  currentIndex.value = index;
  channelCode.value = code;
  channels.value[currentIndex.value].skuId = '0';
  channels.value[currentIndex.value].sku = undefined;
};
// 添加商品
const listQuery = ref({
  pageIndex: 1,
  pageSize: 10,
}); //搜索商品
const listSkuLoading = ref(false); //商品列表loading
const listSkuData = ref({}); //商品数据
const currentRow = ref({});
const pageCount = ref(0); //总页数
const channelModelView = ref({});
// 商品弹层列表
const handleListOpen = async () => {
  listSkuLoading.value = true;
  listQuery.value.skuName = listQuery.value.skuName || undefined;
  const data = await listSku(listQuery.value);
  listSkuData.value = data;
  pageCount.value = Math.ceil(data.total / 10);
  listSkuLoading.value = false;
};
// 打开商品选择弹层
const openSetSkuDialog = (index, code) => {
  currentIndex.value = index;
  channelCode.value = code;
  skuDialogVisible.value = true;
};
// 关闭商品详情
const handleListClose = () => {
  skuDialogVisible.value = false;
};
// 商品上一页
const handleClickPrev = () => {
  if (listQuery.value.pageIndex === 1) {
    return;
  }
  listQuery.value.pageIndex--;
  handleListOpen();
};
// 商品下一页
const handleClickNext = () => {
  if (listQuery.value.pageIndex === pageCount.value) {
    return;
  }
  listQuery.value.pageIndex++;
  handleListOpen();
};
// 搜索
const resetPageIndex = () => {
  listQuery.value.pageIndex = 1;
  handleListOpen();
};
// 商品选择
const handleCurrentChange = (i) => {
  // TODO:点击取消选中功能
  currentRow.value = listSkuData.value.rows[i];
};
// 确认商品选择
const handleSelectClick = (sku) => {
  handleListClose();
  channels.value[currentIndex.value].skuId = currentRow.value.skuId;
  channels.value[currentIndex.value].sku = {
    skuName: currentRow.value.skuName,
    skuImage: currentRow.value.skuImage,
  };
};
// 确认货道提交
const handleClick = async () => {
  channelModelView.value.innerCode = props.goodData.innerCode;
  channelModelView.value.channelList = channels.value.map((item) => {
    return {
      innerCode: props.goodData.innerCode,
      channelCode: item.channelCode,
      skuId: item.skuId,
    };
  });
  const res = await channelConfig(channelModelView.value);
  if (res.code === 200) {
    proxy.$modal.msgSuccess('操作成功');
    visible.value = false
    emit('handleCloseGood');
  }
};
</script>
// <style lang="scss" scoped src="../index.scss"></style>

views/manage/vm/components/ChannelDialogItem.vue

<template>
  <div v-if="channel" class="item">
    <div class="code">
      {{ channel.channelCode }}
    </div>
    <div class="sku">
      <img
        class="img"
        :src="channel.sku&&channel.sku.skuImage
            ? channel.sku.skuImage
            : require('@/assets/vm/default_sku.png')"
      />
      <div class="name" :title="channel.sku ? channel.sku.skuName : '暂无商品'">
        {{ channel.sku ? channel.sku.skuName : '暂无商品' }}
      </div>
    </div>
    <div>
      <el-button
        type="text"
        class="el-button--primary-text"
        @click="handleSetClick"
      >
        添加
      </el-button>
      <el-button
        type="text"
        class="el-button--danger-text"
        :disabled="!channel.sku ? true : false"
        @click="handleRemoveClick"
      >
        删除
      </el-button>
    </div>
  </div>
</template>
<script setup>
import { require } from '@/utils/validate';
const props = defineProps({
  currentIndex: {
    type: Number,
    default: 0,
  },
  channel: {
    type: Object,
    default: () => {},
  },
});
const emit = defineEmits(['openSetSkuDialog','openRemoveSkuDialog']);
// 添加商品
const handleSetClick = () => {
  emit('openSetSkuDialog', props.currentIndex, props.channel.channelCode);
};
// 删除产品
const handleRemoveClick = () => {
  emit('openRemoveSkuDialog', props.currentIndex, props.channel.channelCode);
};
</script>
<style scoped lang="scss">
@import '@/assets/styles/variables.module.scss';
.item {
  position: relative;
  width: 150px;
  height: 180px;
  background: $base-menu-light-background;
  box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.06);
  border-radius: 4px;
  text-align: center;

  .code {
    position: absolute;
    top: 10px;
    left: 0;
    width: 43px;
    height: 23px;
    line-height: 23px;
    background: #829bed;
    border-radius: 0px 10px 10px 0px;
    font-size: 12px;
    color: $base-menu-light-background;
  }

  .sku {
    height: 135px;
    padding-top: 16px;
    background-color: #f6f7fb;
    border-radius: 4px;

    .img {
      display: inline-block;
      width: 84px;
      height: 78px;
      margin-bottom: 10px;
      object-fit: contain;
    }

    .name {
      padding: 0 16px;
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
    }
  }
}
</style>

在设备管理列表views/manage/vm/index.vue页面的对应位置中,添加货道按钮、货道组件、引入css样式等代码:

<el-button link type="primary" @click="handleGoods(scope.row)" v-hasPermi="['manage:vm:edit']">货道</el-button>

<!-- 货道组件 -->
<ChannelDialog :goodVisible="goodVisible" :goodData="goodData" @handleCloseGood="handleCloseGood"></ChannelDialog>
<!-- end -->

// ********************货道********************
// 货道组件
import ChannelDialog from './components/ChannelDialog.vue';
const goodVisible = ref(false); //货道弹层显示隐藏
const goodData = ref({}); //货道信息用来拿取 vmTypeId和innerCode
// 打开货道弹层
const handleGoods = (row) => {
  goodVisible.value = true;
  goodData.value = row;
};
// 关闭货道弹层
const handleCloseGood = () => {
  goodVisible.value = false;
};
// ********************货道end********************


<style lang="scss" scoped src="./index.scss"></style>

(2)查询货道列表

需求:根据售货机编号查询货道列表。

  • 接口文档

可以看到后端响应的数据中,包含有该设备上所有的货道的信息和每个货道上关联的商品信息,类型是object[],每个object都包含单个货道基本信息和该货道上关联的单个商品。因此我们可以返回一个List集合。

  • 实现思路:

创建ChannelVo,将Sku类型的属性封装在Vo中,在xml中手动映射resultMap并使用Mybatis嵌套查询。

  • ChannelVo
@Data
public class ChannelVo extends Channel {
    // 商品
    private Sku sku;
}
  • ChannelMapper和xml
/**
* 根据售货机编号查询货道列表
*
* @param innerCode
* @return ChannelVo集合
*/
List<ChannelVo> selectChannelVoListByInnerCode(String innerCode);

<!-- 将嵌套查询的结果封装给ChannelVo的Sku sku属性上 -->
<resultMap type="ChannelVo" id="ChannelVoResult">
    <result property="id"    column="id"    />
    <result property="channelCode"    column="channel_code"    />
    <result property="skuId"    column="sku_id"    />
    <result property="vmId"    column="vm_id"    />
    <result property="innerCode"    column="inner_code"    />
    <result property="maxCapacity"    column="max_capacity"    />
    <result property="currentCapacity"    column="current_capacity"    />
    <result property="lastSupplyTime"    column="last_supply_time"    />
    <result property="createTime"    column="create_time"    />
    <result property="updateTime"    column="update_time"    />
    <!-- 1对1嵌套查询(1个货道关联1个商品)根据sku_id查询该货道上关联的Sku -->
    <association property="sku" javaType="Sku" column="sku_id" select="com.dkd.manage.mapper.SkuMapper.selectSkuBySkuId" />
</resultMap>

<sql id="selectChannelVo">
    select id, channel_code, sku_id, vm_id, inner_code, max_capacity, current_capacity, last_supply_time, create_time, update_time from tb_channel
</sql>

<!-- 将自动映射封装resultType改为手动映射封装resultMap -->
<select id="selectChannelVoListByInnerCode" resultMap="ChannelVoResult">
    <include refid="selectChannelVo"/>
    where inner_code = #{innerCode}
</select>
  • IChannelService接口和实现
/**
 * 根据售货机编号查询货道列表
 *
 * @param innerCode
 * @return ChannelVo集合
 */
List<ChannelVo> selectChannelVoListByInnerCode(String innerCode);

/**
 * 根据售货机编号查询货道列表
 *
 * @param innerCode
 * @return ChannelVo集合
 */
@Override
public List<ChannelVo> selectChannelVoListByInnerCode(String innerCode) {
    return channelMapper.selectChannelVoListByInnerCode(innerCode);
}
  • ChannelController
/**
 * 根据售货机编号查询货道列表
 */
@PreAuthorize("@ss.hasPermi('manage:channel:list')")
@GetMapping("/list/{innerCode}")
public AjaxResult lisetByInnerCode(@PathVariable("innerCode") String innerCode) {
    List<ChannelVo> voList = channelService.selectChannelVoListByInnerCode(innerCode);
    return success(voList);
}

(3)货道关联商品

  • 接口文档

请求体中包括object[]类型的channelList,说明是需要批量修改货道关联信息。

  • 前端返回的json示例

最外层包含innerCode和channelList,channelList又包含innerCode、channelCode、skuId三个属性。即根据设备编号innerCode和货道编号channelCode定位到货道id,再根据skuId去更新货道表中该货道上的sku_id。

{
	"innerCode": "aim5xu4I",
	"channelList": [{
			"innerCode": "aim5xu4I",
			"channelCode": "1-1",
			"skuId": 5
		},
		{
			"innerCode": "aim5xu4I",
			"channelCode": "1-2",
			"skuId": 1
		},
		{
			"innerCode": "aim5xu4I",
			"channelCode": "2-1",
			"skuId": 2
		},
		{
			"innerCode": "aim5xu4I",
			"channelCode": "2-2",
			"skuId": 4
		}
	]
}

而我们后端并没有能直接接收这样格式的实体类,因此需要封装数据传输对象(DTO)来接收前端给我们传输的json数据,包括ChannelConfigDTO 和 ChannelSkuDTO。

  • 实现思路:

创建ChannelConfigDTO 和 ChannelSkuDTO,在Service层将数据传输对象DTO转换为持久化对象PO,Mapper层需要根据售货机编号inner_code和货道编号channel_code查询货道信息,批量修改货道的sku_id。

  • ChannelSkuDTO
// 单个货道对应的sku信息
@Data
public class ChannelSkuDTO {
    // 售货机编号
    private String innerCode;
    // 货道编号
    private String channelCode;
    // 关联商品id
    private Long skuId;
}
  • ChannelConfigDTO
// 售货机货道配置
@Data
public class ChannelConfigDTO {
    // 售货机编号
    private String innerCode;
    // 货道DTO集合
    private List<ChannelSkuDTO> channelList;
}
  • ChannelMapper和xml
/**
 * 批量修改货道
 * @param channelList
 * @return
 */
int batchUpdateChannels(List<Channel> channelList);

<!-- 批量更新货道信息 -->
<update id="batchUpdateChannels" parameterType="java.util.List">
    <foreach collection="list" item="channel" index="index" open="" close="" separator="; ">
        UPDATE tb_channel
        <set>
            <if test="channel.channelCode != null and channel.channelCode != ''">channel_code = #{channel.channelCode},</if>
            <if test="channel.skuId != null">sku_id = #{channel.skuId},</if>
            <if test="channel.vmId != null">vm_id = #{channel.vmId},</if>
            <if test="channel.innerCode != null and channel.innerCode != ''">inner_code = #{channel.innerCode},</if>
            <if test="channel.maxCapacity != null">max_capacity = #{channel.maxCapacity},</if>
            <if test="channel.currentCapacity != null">current_capacity = #{channel.currentCapacity},</if>
            <if test="channel.lastSupplyTime != null">last_supply_time = #{channel.lastSupplyTime},</if>
            <if test="channel.createTime != null">create_time = #{channel.createTime},</if>
            <if test="channel.updateTime != null">update_time = #{channel.updateTime},</if>
        </set>
        WHERE id = #{channel.id}
    </foreach>
</update>

注意:这种批量更新的方式取决于数据库的支持情况,不是所有数据库都支持在单个请求中发送多条独立的SQL语句。如果目标数据库不支持这种方式,可能需要采用其他方法如存储过程或批处理更新。

  • application-druid.yml:允许mybatis框架在单个请求中发送多个sql语句
# 一次请求中可以包含多条SQL语句(支持多个分号;)
&allowMultiQueries=true

# 数据源配置
spring:
    datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        druid:
            # 主库数据源
            master:
                url: jdbc:mysql://localhost:3306/dkd?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true
                username: root
                password: root
  • IChannelService和实现类
/**
 * 货道关联商品
 * @param channelConfigDTO
 * @return 结果
 */
int setChannels(ChannelConfigDTO channelConfigDTO);

/**
 * 货道关联商品
 * @param channelConfigDTO
 * @return 结果
 */
@Override
public int setChannels(ChannelConfigDTO channelConfigDTO) {
    // 将DTO转为PO对象
    List<Channel> channelList = channelConfigDTO.getChannelList().stream().map(dto -> {
        // 根据售货机编号和货道编号查询货道信息
        Channel channel = channelMapper.getChannelInfo(dto.getInnerCode(), dto.getChannelCode());
        // 如果该货道存在
        if (channel != null) {
            // 关联最新商品id
            channel.setSkuId(dto.getSkuId());
            // 更新货道修改时间
            channel.setUpdateTime(DateUtils.getNowDate());
        }
        return channel; // 将转换后的PO对象返回
    }).collect(Collectors.toList());
    // 批量修改货道
    return channelMapper.batchUpdateChannels(channelList);
}
  • ChannelController
@PreAuthorize("@ss.hasPermi('manage:channel:edit')")
@Log(title = "售货机货道", businessType = BusinessType.UPDATE)
@PutMapping("/config")
public AjaxResult setChannels(@RequestBody ChannelConfigDTO channelConfigDTO) {
    return toAjax(channelService.setChannels(channelConfigDTO));
}
  • 货道关联商品功能测试

添加货道信息与商品关联成功,测试删除功能。

点击确认后向后端发送修改请求,该货道关联删除成功。




二、工单管理

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

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

1、需求说明

业务场景:管理员在后台创建工单后,工作人员可在运营管理App中查看并根据情况选择执行或取消分配给自己的任务。

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

帝可得工单分为两大类 :

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

工单有四种状态:

  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-dialog
    width="630px"
    title="工单详情"
    :close-on-click-modal="false"
    :close-on-press-escape="false"
    v-model="visible"
    @close="cancel"
  >
    <div class="task-status">
      <img
        v-if="taskDada.taskStatus"
        class="icon"
        :src="require('@/assets/task/icon_' + taskDada.taskStatus + '.png')"
      />
      <span class="status">
        <label v-if="taskDada.taskStatus === 1">代办</label>
        <label v-else-if="taskDada.taskStatus === 2">进行</label>
        <label v-else-if="taskDada.taskStatus === 3">取消</label>
        <label v-else>完成</label>
      </span>
      <img
        v-if="taskDada.taskStatus"
        class="pic"
        :src="require('@/assets/task/pic_' + taskDada.taskStatus + '.png')"
      />
    </div>
    <el-form label-width="120">
      <el-row>
        <el-col :span="12">
          <el-form-item label="设备编号:">
            {{ taskDada.innerCode }}
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="创建日期:">
            {{ taskDada.createTime }}
          </el-form-item>
        </el-col>
        <el-col v-if="taskDada.taskStatus === 3" :span="12">
          <el-form-item label="取消日期:">
            {{ taskDada.updateTime ? taskDada.updateTime : '--' }}
          </el-form-item>
        </el-col>
        <el-col v-if="taskDada.taskStatus === 4" :span="12">
          <el-form-item label="完成日期:">
            {{ taskDada.updateTime ? taskDada.updateTime : '--' }}
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="运营人员:">
            {{ taskDada.userName }}
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="工单类型:">
            <span v-if="taskDada.productTypeId === 1">投放工单</span>
            <span v-else-if="taskDada.productTypeId === 2">补货工单</span>
            <span v-else-if="taskDada.productTypeId === 3">维修工单</span>
            <span v-else>撤机工单</span>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="补货数量:" prop="details">
            <el-button type="text" @click="channelDetails">
              <el-icon><List /></el-icon>补货清单
            </el-button>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="工单方式:">
            {{ taskDada.createType === 0 ? '自动' : '手动' }}
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item
            :label="taskDada.taskStatus === 3 ? '取消原因:' : '备注:'"
          >
            <div class="desc">
              {{ taskDada.desc }}
            </div>
          </el-form-item>
        </el-col>
        <el-col v-if="taskDada.productTypeId === 1" :span="12">
          <el-form-item label="定位:">
            <div class="addr">
              <el-icon><Location /></el-icon><span>{{ taskDada.addr }}</span>
            </div>
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
    <div v-if="taskDada.taskStatus !== 4" class="dialog-footer">
      <el-button
        v-if="taskDada.taskStatus === 1 || taskDada.taskStatus === 2"
        @click="handleCancelTask"
      >
        取消工单
      </el-button>
      <el-button
        type="primary"
        v-else-if="taskDada.taskStatus === 3"
        @click="handleCreateTask"
      >
        重新创建
      </el-button>
    </div>
    <!-- 货道列表弹层 -->
    <BusinessReplenishmentListDialog
      :listVisible="listVisible"
      :detailData="detailData"
      @handleClose="channelCloseDetails"
    ></BusinessReplenishmentListDialog>
    <!-- end -->
  </el-dialog>
</template>

<script setup name="Task">
import { watch } from 'vue';
import { require } from '@/utils/validate';
import { ElMessageBox } from 'element-plus';
import { cancelTaskType } from '@/api/manage/taskType';
// 组件
import BusinessReplenishmentListDialog from './business-replenishment-list-dialog.vue';
// 从父组件获取数据
const props = defineProps({
  // 工单详情
  taskDada: {
    type: Object,
    default: () => {},
  },
  // 获取货道列表
  detailData:{
    type: Object,
    default: () => [],
  },
  // 详情弹层显示隐藏
  detailVisible: {
    type: Boolean,
    default: false,
  },
  // 工单id
  taskId: {
    type: Number,
    default: '',
  },
});
// 定义变量
const emit = defineEmits(['handleClose', 'handleAdd', 'getList']);
const visible = ref(false);
const listVisible = ref(false); //货道弹层
watch(
  () => props.detailVisible,
  (val) => {
    if (val) {
      visible.value = val;
    }
  }
);
// 取消工单
const handleCancelTask = () => {
  ElMessageBox.confirm('取消工单后,将不能恢复,是否确认取消?', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning',
  })
    .then(() => {
      const obj = {
        taskId: props.taskId,
        desc: '后台工作人员取消',
      };
      cancelTaskType(obj).then((res) => {
        if (res.code === 200) {
          emit('getList');
          cancel();
        }
      });
    })
    .catch(() => {});
};
// 关闭 弹层
const cancel = () => {
  visible.value = false;
  emit('handleClose');
};
// 重新创建
const handleCreateTask = () => {
  cancel(); //关闭详情窗口
  emit('handleAdd', 'anew'); //打开新增窗口
};
// 打开货道列表弹层
const channelDetails = () => {
  listVisible.value = true;
};
// 关闭货道列表 弹层
const channelCloseDetails = () => {
  listVisible.value = false;
};
</script>

views\manage\task\components\business-replenishment-dialog.vue

<template>
  <el-dialog
    width="630px"
    title="补货详情"
    :close-on-click-modal="false"
    :close-on-press-escape="false"
    v-model="visible"
    @close="cancel"
    @open="open"
  >
    <el-scrollbar class="scrollbar" style="height: 330px">
      <el-table
        style="width: 568px; margin: 0 auto"
        :data="channelList"
        :header-cell-style="{
          'line-height': '1.15',
          padding: '10px 0 9px',
          background: '#F3F6FB',
          'font-weight': '500',
          'text-align': 'left',
          color: '#666666',
        }"
        :cell-style="{
          height: '44px',
          padding: '2px 0',
          'text-align': 'left',
          color: '#666666',
        }"
      >
        <el-table-column label="货道编号">
          <template #default="scope">
            {{ scope.row.channelCode }}
          </template>
        </el-table-column>

        <el-table-column label="商品名称">
          <template #default="scope">
            {{ scope.row.skuId && scope.row.sku.skuId? scope.row.sku.skuName : '-' }}
          </template>
        </el-table-column>

        <el-table-column label="当前数量">
          <template #default="scope">
            {{ scope.row.skuId && scope.row.sku.skuId? scope.row.currentCapacity : '-' }}
          </template>
        </el-table-column>

        <el-table-column label="还可添加">
          <template #default="scope">
            {{ scope.row.skuId && scope.row.sku.skuId? getAvailableCapacity(scope.row) : '-' }}
          </template>
        </el-table-column>

        <el-table-column label="补满数量" width="200">
          <template #default="scope">
            <el-input-number
              v-if="scope.row.skuId && scope.row.sku.skuId"
              v-model="scope.row.expectCapacity"
              controls-position="right"
              :min="0"
              :max="getAvailableCapacity(scope.row)"
              label="补满数量"
              style="width: 100%"
              placeholder="请输入"
            />
            <span v-else>货道暂无商品</span>
          </template>
        </el-table-column>
      </el-table>
    </el-scrollbar>
    <div class="dialog-footer">
      <el-button @click="cancel">取消</el-button>
      <el-button type="primary" @click="ensureDialog">确认</el-button>
    </div>
  </el-dialog>
</template>

<script setup name="Task">
import { watch } from 'vue';
import { require } from '@/utils/validate';
import { ElMessageBox } from 'element-plus';
import { cancelTaskType } from '@/api/manage/taskType';
// 接口
// 获取货道接口
import { getGoodsList } from '@/api/manage/channel';
// 从父组件获取数据
const props = defineProps({
  // 详情弹层显示隐藏
  channelVisible: {
    type: Boolean,
    default: false,
  },
  // 设备编号
  innerCode: {
    type: String,
    default: '',
  },
});
// 定义变量
const emit = defineEmits(['handleClose', 'getDetailList']);
const visible = ref(false);
const channelList = ref([]); //货道列表
const detailList = ref([]); //补货列表
watch(
  () => props.channelVisible,
  (val) => {
    if (val) {
      visible.value = val;
    }
  }
);
// 弹层打开
const open = () => {
  getChannelList();
};
// 还可添加
const getAvailableCapacity = (channel) => {
  let availableCapacity = channel.maxCapacity - channel.currentCapacity;
  return availableCapacity > 0 ? availableCapacity : 0;
};
// 获取货道列表
const getChannelList = () => {
  getGoodsList(props.innerCode).then((response) => {
    channelList.value = response.data;
    channelList.value.map((channel) => {
      channel.expectCapacity =
        channel.sku !== null
          ? channel.maxCapacity - channel.currentCapacity
          : 0;
    });
  });
};
// 确定货道清单
const ensureDialog = () => {
  cancel();
  channelList.value.forEach((ele) => {
    
    if (ele.sku&&ele.sku.skuId&&ele.expectCapacity>0) {
      detailList.value.push({
        channelCode: ele.channelCode,
        expectCapacity: ele.expectCapacity,
        skuId: ele.skuId,
        skuName: ele.sku ? ele.sku.skuName : '',
        skuImage: ele.sku ? ele.sku.skuImage : '',
      });
    }
  });
  emit('getDetailList', detailList.value);
};
// 关闭 弹层
const cancel = () => {
  visible.value = false;
  detailList.value=[]
  emit('handleClose');
};
</script>

views\manage\task\components\business-replenishment-list-dialog.vue

<template>
  <el-dialog
    width="630px"
    title="补货详情"
    :close-on-click-modal="false"
    :close-on-press-escape="false"
    v-model="visible"
    append-to-body
    @close="cancel"
    @open="open"
  >
    <el-scrollbar
      class="scrollbar"
      style="height: 330px;"
    >
      <el-table
        style="width: 552px;margin: 0 auto;"
        :data="detailData"
        :header-cell-style="{'line-height': '1.15', 'padding': '10px 0 9px', 'background': '#F3F6FB', 'font-weight': '500', 'text-align': 'left', 'color': '#666666'}"
        :cell-style="{'height': '44px', 'padding': '2px 0', 'text-align': 'left', 'color': '#666666'}"
      >
        <el-table-column label="货道编号">
          <template #default="scope">
            {{ scope.row.channelCode }}
          </template>
        </el-table-column>

        <el-table-column label="商品">
          <template #default="scope">
            {{ scope.row.skuName?scope.row.skuName:'--' }}
          </template>
        </el-table-column>

        <el-table-column label="补货数量">
          <template #default="scope">
            {{ scope.row.expectCapacity }}
          </template>
        </el-table-column>
      </el-table>
    </el-scrollbar>
  </el-dialog>
</template>

<script setup name="Task">
import { watch } from 'vue';
import { getTaskDetails } from '@/api/manage/task';
// 接口
// 获取货道接口
import { getGoodsList } from '@/api/manage/channel';
// 从父组件获取数据
const props = defineProps({
  // 详情弹层显示隐藏
  listVisible: {
    type: Boolean,
    default: false,
  },
  // 获取货道列表
  detailData:{
    type: Object,
    default: () => [],
  },
});
// 定义变量
const emit = defineEmits(['handleClose']);
const visible = ref(false);
watch(
  () => props.listVisible,
  (val) => {
    if (val) {
      visible.value = val;
    }
  }
);
// 关闭 弹层
const cancel = () => {
  visible.value = false;
  emit('handleClose');
};
</script>

views\manage\task\components\operation-detail-dialog.vue

<template>
  <el-dialog
    width="630px"
    title="工单详情"
    :close-on-click-modal="false"
    :close-on-press-escape="false"
    v-model="visible"
    @close="cancel"
    @open="open"
  >
    <div class="task-status">
      <img
        v-if="taskDada.taskStatus"
        class="icon"
        :src="require('@/assets/task/icon_' + taskDada.taskStatus + '.png')"
      />
      <span class="status">
        <label v-if="taskDada.taskStatus === 1">代办</label>
        <label v-else-if="taskDada.taskStatus === 2">进行</label>
        <label v-else-if="taskDada.taskStatus === 3">取消</label>
        <label v-else>完成</label>
      </span>
      <img
        v-if="taskDada.taskStatus"
        class="pic"
        :src="require('@/assets/task/pic_' + taskDada.taskStatus + '.png')"
      />
    </div>
    <el-form label-width="120">
      <el-row>
        <el-col :span="12">
          <el-form-item label="设备编号:">
            {{ taskDada.innerCode }}
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="创建日期:">
            {{ taskDada.createTime }}
          </el-form-item>
        </el-col>
        <el-col v-if="taskDada.taskStatus === 3" :span="12">
          <el-form-item label="取消日期:">
            {{ taskDada.updateTime ? taskDada.updateTime : '--' }}
          </el-form-item>
        </el-col>
        <el-col v-if="taskDada.taskStatus === 4" :span="12">
          <el-form-item label="完成日期:">
            {{ taskDada.updateTime ? taskDada.updateTime : '--' }}
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="运营人员:">
            {{ taskDada.userName }}
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="工单类型:">
            <span v-if="taskDada.productTypeId === 1">投放工单</span>
            <span v-else-if="taskDada.productTypeId === 2">补货工单</span>
            <span v-else-if="taskDada.productTypeId === 3">维修工单</span>
            <span v-else>撤机工单</span>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="工单方式:">
            {{ taskDada.createType === 0 ? '自动' : '手动' }}
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item
            :label="taskDada.taskStatus === 3 ? '取消原因:' : '备注:'"
          >
            <div class="desc">
              {{ taskDada.desc }}
            </div>
          </el-form-item>
        </el-col>
        <el-col v-if="taskDada.productTypeId === 1" :span="12">
          <el-form-item label="定位:">
            <div class="addr">
              <el-icon><Location /></el-icon
              ><span>{{
                taskDada.addr
              }}</span>
            </div>
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
    <div v-if="taskDada.taskStatus !== 4" class="dialog-footer">
      <el-button
        v-if="taskDada.taskStatus === 1 || taskDada.taskStatus === 2"
        @click="handleCancelTask"
      >
        取消工单
      </el-button>
      <el-button
        type="primary"
        v-else-if="taskDada.taskStatus === 3"
        @click="handleCreateTask"
      >
        重新创建
      </el-button>
    </div>
  </el-dialog>
</template>

<script setup name="Task">
import { watch } from 'vue';
import { require } from '@/utils/validate';
import { ElMessageBox } from 'element-plus';
import { cancelTaskType } from '@/api/manage/taskType';
// 从父组件获取数据
const props = defineProps({
    // 工单详情
    taskDada:{
        type: Object,
        default:()=>{}
    },
  // 详情弹层显示隐藏
  detailVisible: {
    type: Boolean,
    default: false,
  },
  // 工单id
  taskId: {
    type: String,
    default: '',
  },
});
// 定义变量
const emit = defineEmits(['handleClose','handleAdd','getList']);
const visible = ref(false);
watch(
  () => props.detailVisible,
  (val) => {
    if (val) {
      visible.value = val;
    }
  }
);
// 弹层打开
const open = () => {
  // 工单详情
//   taskInfo();
  // // TODO:工单状态和工单类型可以直接从工单详情中获得
  // 工单状态列表
  //   getAllTaskStatus()
  // // 工单类型列表
  // getTaskTypeList()
};
// 取消工单
const handleCancelTask = () => {
  ElMessageBox.confirm('取消工单后,将不能恢复,是否确认取消?', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning',
  })
    .then(() => {
      const obj = {
        taskId: props.taskId,
        desc: '后台工作人员取消',
      };
      cancelTaskType(obj).then((res) => {
        if (res.code === 200) {
          emit('getList')
          cancel();
        }
      });
    })
    .catch(() => {});
};
// 关闭 弹层
const cancel = () => {
  visible.value = false;
  emit('handleClose');
};
// 重新创建
const handleCreateTask = ()=>{
    cancel()//关闭详情窗口
    emit('handleAdd','anew')//打开新增窗口
}
</script>

views\manage\task\components\task-config.vue

<template>
  <el-dialog
    width="630px"
    title="工单配置"
    :close-on-click-modal="false"
    :close-on-press-escape="false"
    v-model="visible"
    append-to-body
    @close="cancel"
    @open="open"
  >
    <el-form
      ref="taskRef"
      :inline="true"
      :model="form"
      :rules="rules"
      label-width="120"
    >
      <el-form-item label="补货警戒线:" prop="alertValue">
        <el-input-number
          v-model="form.alertValue"
          controls-position="right"
          :min="1"
          :max="100"
          placeholder="请输入"
        />
      </el-form-item>
    </el-form>
    <div class="dialog-footer">
      <el-button @click="cancel"> 取消 </el-button>
      <el-button type="primary" @click="submitForm"> 确认 </el-button>
    </div>
  </el-dialog>
</template>
<script setup name="Task">
import { watch } from 'vue';
const { proxy } = getCurrentInstance();
// 接口
import { getJob, setJob } from '@/api/manage/task';
// 从父组件获取数据
const props = defineProps({
  // 弹层显示隐藏
  taskConfigVisible: {
    type: Boolean,
    default: false,
  },
});
// 定义变量
const emit = defineEmits(['handleClose']);
const visible = ref(false);
const data = reactive({
  form: {},
  rules: {
    alertValue: [{ required: true, message: '请输入', trigger: 'blur' }],
  },
});
const { form, rules } = toRefs(data);
watch(
  () => props.taskConfigVisible,
  (val) => {
    if (val) {
      visible.value = val;
    }
  }
);
// 打开弹层
const open = () => {
  getJobData()
};
// 获取获取补货预警值
const getJobData = () => {
  getJob(1).then((response) => {
    const res = response.data;
    form.value = {
      id: res.id,
      alertValue: res.alertValue,
    };
  });
};
// 提交表单
const submitForm = () => {
  proxy.$refs['taskRef'].validate((valid) => {
    setJob(form.value).then((res) => {
      if (res.code === 200) {
        proxy.$modal.msgSuccess('配置成功');
        cancel();
        getJobData()
      }
    });
  });
};
// 关闭弹层
const cancel = () => {
  visible.value = false;
  emit('handleClose');
};
</script>

views\manage\task\business.vue

<template>
  <div class="app-container">
    <el-form
      :model="queryParams"
      ref="queryRef"
      :inline="true"
      v-show="showSearch"
      label-width="68px"
    >
      <el-form-item label="工单编号" prop="taskCode">
        <el-input
          v-model="queryParams.taskCode"
          placeholder="请输入工单编号"
          clearable
          @keyup.enter="handleQuery"
        />
      </el-form-item>
      <el-form-item label="工单状态" prop="taskStatus">
        <el-select
          v-model="queryParams.taskStatus"
          placeholder="请选择工单状态"
          clearable
        >
          <el-option
            v-for="dict in task_status"
            :key="dict.value"
            :label="dict.label"
            :value="dict.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="Search" @click="handleQuery"
          >搜索</el-button
        >
        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>

    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button
          type="primary"
          plain
          icon="Plus"
          @click="handleAdd"
          v-hasPermi="['manage:task:add']"
          >新增</el-button
        >
        <el-button type="primary" plain @click="openTaskConfig"
          >工单配置</el-button
        >
      </el-col>
      <right-toolbar
        v-model:showSearch="showSearch"
        @queryTable="getList"
      ></right-toolbar>
    </el-row>

    <el-table
      v-loading="loading"
      :data="taskList"
      @selection-change="handleSelectionChange"
    >
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column
        label="序号"
        type="index"
        width="50"
        align="center"
        prop="taskId"
      />
      <el-table-column label="工单编号" align="center" prop="taskCode" />
      <el-table-column label="设备编号" align="center" prop="innerCode" />
      <el-table-column
        label="工单类型"
        align="center"
        prop="taskType.typeName"
      />
      <el-table-column label="工单方式" align="center" prop="createType">
        <template #default="scope">
          <dict-tag :options="task_create_type" :value="scope.row.createType" />
        </template>
      </el-table-column>
      <el-table-column label="工单状态" align="center" prop="taskStatus">
        <template #default="scope">
          <dict-tag :options="task_status" :value="scope.row.taskStatus" />
        </template>
      </el-table-column>
      <el-table-column label="运营人员" align="center" prop="userName" />
      <el-table-column
        label="创建时间"
        align="center"
        prop="createTime"
        width="180"
      >
        <template #default="scope">
          <span>{{
            parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}')
          }}</span>
        </template>
      </el-table-column>
      <el-table-column
        label="操作"
        align="center"
        class-name="small-padding fixed-width"
      >
        <template #default="scope">
          <el-button
            link
            type="primary"
            @click="openTaskDetailDialog(scope.row)"
            v-hasPermi="['manage:task:edit']"
            >查看详情</el-button
          >
        </template>
      </el-table-column>
    </el-table>

    <pagination
      v-show="total > 0"
      :total="total"
      v-model:page="queryParams.pageNum"
      v-model:limit="queryParams.pageSize"
      @pagination="getList"
    />

    <!-- 添加工单对话框 -->
    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
      <el-form ref="taskRef" :model="form" :rules="rules" label-width="100px">
        <el-form-item label="设备编号" prop="innerCode">
          <el-input
            v-model="form.innerCode"
            placeholder="请输入设备编号"
            @blur="handleCode"
          />
        </el-form-item>
        <el-form-item label="工单类型" prop="productTypeId">
          <el-select
            v-model="form.productTypeId"
            placeholder="请选择工单类型"
            clearable
          >
            <el-option
              v-for="dict in taskTypeList"
              :key="dict.typeId"
              :label="dict.typeName"
              :value="dict.typeId"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="补货数量:" prop="details">
          <el-button type="text" @click="channelDetails">
            <el-icon> <List /> </el-icon>补货清单
          </el-button>
        </el-form-item>
        <el-form-item label="运营人员:" prop="userId">
          <el-select
            v-model="form.userId"
            placeholder="请选择"
            :filterable="true"
          >
            <el-option
              v-for="(item, index) in userList"
              :key="index"
              :label="item.userName"
              :value="item.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="备注" prop="desc">
          <el-input
            type="textarea"
            v-model="form.desc"
            placeholder="请输入备注"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确 定</el-button>
          <el-button @click="cancel">取 消</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- 查看详情组件 -->
    <DetailDialog
      :detailVisible="detailVisible"
      :taskId="taskId"
      :taskDada="form"
      :detailData="detailData"
      @getList="getList"
      @handleClose="handleClose"
      @handleAdd="handleAdd"
    ></DetailDialog>
    <!-- end -->
    <!-- 补货详情 -->
    <ReplenishmentDialog
      :channelVisible="channelVisible"
      :innerCode="form.innerCode"
      @getDetailList="getDetailList"
      @handleClose="channelDetailsClose"
    ></ReplenishmentDialog>
    <!-- end -->
    <!-- 工单配置 -->
    <TaskConfig
      :taskConfigVisible="taskConfigVisible"
      @handleClose="handleConfigClose"
    ></TaskConfig>
    <!-- end -->
  </div>
</template>

<script setup name="Task">
import {
  listTask,
  getTask,
  delTask,
  addTask,
  updateTask,
  getBusinessList,
  getTaskDetails,
} from '@/api/manage/task';
import { listTaskType } from '@/api/manage/taskType';
import { loadAllParams } from '@/api/page';
// 组件
import DetailDialog from './components/business-detail-dialog.vue'; //详情组件
import ReplenishmentDialog from './components/business-replenishment-dialog.vue'; //补货组件
import TaskConfig from './components/task-config.vue';
const { proxy } = getCurrentInstance();
const { task_status, task_create_type } = proxy.useDict(
  'task_status',
  'task_create_type'
);

const taskList = ref([]);
const open = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref('');
const detailVisible = ref(false); //查看详情弹层显示/隐藏
const taskId = ref(null); //工单id
const taskDada = ref({}); //工单详情
const userList = ref([]); //运维人员
const channelVisible = ref(false); //补货弹层
const detailData = ref([]); //货道列表
const taskConfigVisible = ref(false); //工单配置弹层
const data = reactive({
  form: {},
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    taskCode: null,
    taskStatus: null,
    createType: null,
    innerCode: null,
    userName: null,
    regionId: null,
    desc: null,
    productTypeId: null,
    userId: null,
    addr: null,
    params: { isRepair: false },
  },
  rules: {
    innerCode: [
      { required: true, message: '设备编号不能为空', trigger: 'blur' },
    ],
    productTypeId: [
      { required: true, message: '设备类型不能为空', trigger: 'blur' },
    ],
    // details: [{ required: true, message: '补货数量不能为空', trigger: 'blur' }],

    userId: [{ required: true, message: '人员不能为空', trigger: 'blur' }],
    desc: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
  },
});

const { queryParams, form, rules } = toRefs(data);

/** 查询运营工单列表 */
function getList() {
  loading.value = true;
  listTask(queryParams.value).then((response) => {
    taskList.value = response.rows;
    total.value = response.total;
    loading.value = false;
  });
}

// 取消按钮
function cancel() {
  open.value = false;
  reset();
}

// 表单重置
function reset() {
  form.value = {
    taskId: null,
    taskCode: null,
    taskStatus: null,
    createType: null,
    innerCode: null,
    userId: null,
    userName: null,
    regionId: null,
    desc: null,
    productTypeId: null,
    addr: null,
    createTime: null,
    updateTime: null,
    details: [],
  };
  proxy.resetForm('taskRef');
}

/** 搜索按钮操作 */
function handleQuery() {
  queryParams.value.pageNum = 1;
  getList();
}

/** 重置按钮操作 */
function resetQuery() {
  proxy.resetForm('queryRef');
  handleQuery();
}

// 多选框选中数据
function handleSelectionChange(selection) {
  ids.value = selection.map((item) => item.taskId);
  single.value = selection.length != 1;
  multiple.value = !selection.length;
}

/** 新增按钮操作 */
function handleAdd(val) {
  if (val === 'anew') {
    taskInfo();
    getUserList();
  } else {
    taskId.val = '';
  }
  reset();
  open.value = true;
  title.value = '添加运营工单';
}

/** 提交按钮 */
function submitForm() {
  proxy.$refs['taskRef'].validate((valid) => {
    if (valid) {
      const data = form.value;
      form.value = {
        innerCode: data.innerCode,
        userId: data.userId,
        productTypeId: data.productTypeId,
        desc: data.desc,
        createType: 1,
        details: data.details,
      };
      addTask(form.value).then((response) => {
        proxy.$modal.msgSuccess('新增成功');
        open.value = false;
        getList();
      });
    }
  });
}

/** 删除按钮操作 */
function handleDelete(row) {
  const _taskIds = row.taskId || ids.value;
  proxy.$modal
    .confirm('是否确认删除运营工单编号为"' + _taskIds + '"的数据项?')
    .then(function () {
      return delTask(_taskIds);
    })
    .then(() => {
      getList();
      proxy.$modal.msgSuccess('删除成功');
    })
    .catch(() => {});
}

/** 导出按钮操作 */
function handleExport() {
  proxy.download(
    'manage/task/export',
    {
      ...queryParams.value,
    },
    `task_${new Date().getTime()}.xlsx`
  );
}

// 查询工单类型列表
const taskTypeList = ref([]);
function getTaskTypeList() {
  // 默认时获取所有得工单类型,需要用type区别开,1:运维工单类型,2:运营工单类型
  const page = {
    ...loadAllParams,
    type: 2,
  };
  listTaskType(page).then((response) => {
    taskTypeList.value = response.rows;
  });
}
// 填写设备编号后
const handleCode = () => {
  if (form.value.innerCode) {
    getUserList();
  }
};
// 获取运营人员列表
const getUserList = () => {
  getBusinessList(form.value.innerCode).then((response) => {
    userList.value = response.data;
  });
};
// 获取工单详情
const taskInfo = () => {
  let dataArr = [];
  let obj = {};
  getTask(taskId.value).then((response) => {
    form.value = response.data;
  });
  // 获取货道列表
  getTaskDetails(taskId.value).then((res) => {
    detailData.value = res.data;
    detailData.value.map((taskDetail) => {
      obj = {
        channelCode: taskDetail.channelCode,
        expectCapacity: taskDetail.expectCapacity,
        skuId: taskDetail.skuId,
        skuName: taskDetail.skuName,
        skuImage: taskDetail.skuImage,
      };
      dataArr.push(obj);
    });
    form.value.details = dataArr;
  });
};
// 查看详情
const openTaskDetailDialog = (row) => {
  taskId.value = row.taskId;
  taskInfo();
  detailVisible.value = true;
};
// 关闭详情弹层
const handleClose = () => {
  detailVisible.value = false;
};
// 补货清单
const channelDetails = () => {
  proxy.$refs['taskRef'].validateField('innerCode', (error) => {
    if (!error) {
      return;
    }
    channelVisible.value = true;
  });
};
// 关闭补货清单
const channelDetailsClose = () => {
  channelVisible.value = false;
};
// 获取货道清单数据
const getDetailList = (val) => {
  form.value.details = val;
};
// 打开工单配置弹层
const openTaskConfig = () => {
  taskConfigVisible.value = true;
};
// 关闭工单配置弹层
const handleConfigClose = () => {
  taskConfigVisible.value = false;
};
getTaskTypeList();

getList();
</script>
<style lang="scss" scoped src="./index.scss"></style>

views\manage\task\operation.vue

<template>
  <div class="app-container">
    <el-form
      :model="queryParams"
      ref="queryRef"
      :inline="true"
      v-show="showSearch"
      label-width="68px"
    >
      <el-form-item label="工单编号" prop="taskCode">
        <el-input
          v-model="queryParams.taskCode"
          placeholder="请输入工单编号"
          clearable
          @keyup.enter="handleQuery"
        />
      </el-form-item>
      <el-form-item label="工单状态" prop="taskStatus">
        <el-select
          v-model="queryParams.taskStatus"
          placeholder="请选择工单状态"
          clearable
        >
          <el-option
            v-for="dict in task_status"
            :key="dict.value"
            :label="dict.label"
            :value="dict.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="工单类型" prop="productTypeId">
        <el-select
          v-model="queryParams.productTypeId"
          placeholder="请选择工单类型"
          clearable
        >
          <el-option
            v-for="dict in taskTypeList"
            :key="dict.typeId"
            :label="dict.typeName"
            :value="dict.typeId"
          />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="Search" @click="handleQuery"
          >搜索</el-button
        >
        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>

    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button
          type="primary"
          plain
          icon="Plus"
          @click="handleAdd"
          v-hasPermi="['manage:task:add']"
          >新增</el-button
        >
      </el-col>
      <right-toolbar
        v-model:showSearch="showSearch"
        @queryTable="getList"
      ></right-toolbar>
    </el-row>

    <el-table
      v-loading="loading"
      :data="taskList"
      @selection-change="handleSelectionChange"
    >
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column
        label="序号"
        type="index"
        width="50"
        align="center"
        prop="taskId"
      />
      <el-table-column label="工单编号" align="center" prop="taskCode" />
      <el-table-column label="设备编号" align="center" prop="innerCode" />
      <el-table-column
        label="工单类型"
        align="center"
        prop="taskType.typeName"
      />
      <el-table-column label="工单方式" align="center" prop="createType">
        <template #default="scope">
          <dict-tag :options="task_create_type" :value="scope.row.createType" />
        </template>
      </el-table-column>
      <el-table-column label="工单状态" align="center" prop="taskStatus">
        <template #default="scope">
          <dict-tag :options="task_status" :value="scope.row.taskStatus" />
        </template>
      </el-table-column>
      <el-table-column label="运维人员" align="center" prop="userName" />
      <el-table-column
        label="创建时间"
        align="center"
        prop="createTime"
        width="180"
      >
        <template #default="scope">
          <span>{{
            parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}')
          }}</span>
        </template>
      </el-table-column>
      <el-table-column
        label="操作"
        align="center"
        class-name="small-padding fixed-width"
      >
        <template #default="scope">
          <el-button
            link
            type="primary"
            @click="openTaskDetailDialog(scope.row)"
            v-hasPermi="['manage:task:edit']"
            >查看详情</el-button
          >
        </template>
      </el-table-column>
    </el-table>

    <pagination
      v-show="total > 0"
      :total="total"
      v-model:page="queryParams.pageNum"
      v-model:limit="queryParams.pageSize"
      @pagination="getList"
    />

    <!-- 添加工单对话框 -->
    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
      <el-form ref="taskRef" :model="form" :rules="rules" label-width="100px">
        <el-form-item label="设备编号" prop="innerCode">
          <el-input
            v-model="form.innerCode"
            placeholder="请输入设备编号"
            @blur="handleCode"
          />
        </el-form-item>
        <el-form-item label="工单类型" prop="productTypeId">
          <el-select
            v-model="form.productTypeId"
            placeholder="请选择工单类型"
            clearable
          >
            <el-option
              v-for="dict in taskTypeList"
              :key="dict.typeId"
              :label="dict.typeName"
              :value="dict.typeId"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="运维人员:" prop="userId">
          <el-select
            v-model="form.userId"
            placeholder="请选择"
            :filterable="true"
          >
            <el-option
              v-for="(item, index) in userList"
              :key="index"
              :label="item.userName"
              :value="item.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="备注" prop="desc">
          <el-input
            type="textarea"
            v-model="form.desc"
            placeholder="请输入备注"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确 定</el-button>
          <el-button @click="cancel">取 消</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- 查看详情组件 -->
    <DetailDialog
      :detailVisible="detailVisible"
      :taskId="taskId"
      :taskDada="form"
      @handleClose="handleClose"
      @handleAdd="handleAdd"
      @getList="getList"
    ></DetailDialog>
    <!-- end -->
  </div>
</template>

<script setup name="Task">
import {
  listTask,
  getTask,
  delTask,
  addTask,
  updateTask,
  getOperationList,
} from '@/api/manage/task';
import { listTaskType } from '@/api/manage/taskType';
import { loadAllParams } from '@/api/page';
// 组件
import DetailDialog from './components/operation-detail-dialog.vue'; //详情组件
const { proxy } = getCurrentInstance();
const { task_status, task_create_type } = proxy.useDict(
  'task_status',
  'task_create_type'
);

const taskList = ref([]);
const open = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref('');
const detailVisible = ref(false); //查看详情弹层显示/隐藏
const taskId = ref(''); //工单id
const taskDada = ref({}); //工单详情
const userList = ref([]); //运维人员
const data = reactive({
  form: {},
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    taskCode: null,
    taskStatus: null,
    createType: null,
    innerCode: null,
    userId: null,
    userName: null,
    regionId: null,
    desc: null,
    productTypeId: null,
    userId: null,
    addr: null,
    params: { isRepair: true },
  },
  rules: {
    innerCode: [
      { required: true, message: '设备编号不能为空', trigger: 'blur' },
    ],
    productTypeId: [
      { required: true, message: '设备类型不能为空', trigger: 'blur' },
    ],
    userId: [
      { required: true, message: '人员不能为空', trigger: 'blur' },
    ],
    desc: [
      { required: true, message: '备注不能为空', trigger: 'blur' },
    ]
  },
});

const { queryParams, form, rules } = toRefs(data);

/** 查询运维工单列表 */
function getList() {
  loading.value = true;
  listTask(queryParams.value).then((response) => {
    taskList.value = response.rows;
    total.value = response.total;
    loading.value = false;
  });
}

// 取消按钮
function cancel() {
  open.value = false;
  reset();
}

// 表单重置
function reset() {
  form.value = {
    taskId: null,
    taskCode: null,
    taskStatus: null,
    createType: null,
    innerCode: null,
    userId: null,
    userName: null,
    regionId: null,
    desc: null,
    productTypeId: null,
    userId: null,
    addr: null,
    createTime: null,
    updateTime: null,
  };
  proxy.resetForm('taskRef');
}

/** 搜索按钮操作 */
function handleQuery() {
  queryParams.value.pageNum = 1;
  getList();
}

/** 重置按钮操作 */
function resetQuery() {
  proxy.resetForm('queryRef');
  handleQuery();
}

// 多选框选中数据
function handleSelectionChange(selection) {
  ids.value = selection.map((item) => item.taskId);
  single.value = selection.length != 1;
  multiple.value = !selection.length;
}

/** 新增按钮操作 */
function handleAdd(val) {
  if (val === 'anew') {
    taskInfo();
    getUserList();
  } else {
    taskId.val = '';
  }
  reset();
  open.value = true;
  title.value = '添加运维工单';
}

/** 提交按钮 */
function submitForm() {
  proxy.$refs['taskRef'].validate((valid) => {
    if (valid) {
      form.value={
        ...form.value,
        createType:1
      }
      addTask(form.value).then((response) => {
        proxy.$modal.msgSuccess('新增成功');
        open.value = false;
        getList();
      });
    }
  });
}

/** 删除按钮操作 */
function handleDelete(row) {
  const _taskIds = row.taskId || ids.value;
  proxy.$modal
    .confirm('是否确认删除运维工单编号为"' + _taskIds + '"的数据项?')
    .then(function () {
      return delTask(_taskIds);
    })
    .then(() => {
      getList();
      proxy.$modal.msgSuccess('删除成功');
    })
    .catch(() => {});
}

/** 导出按钮操作 */
function handleExport() {
  proxy.download(
    'manage/task/export',
    {
      ...queryParams.value,
    },
    `task_${new Date().getTime()}.xlsx`
  );
}

// 查询工单类型列表
const taskTypeList = ref([]);
function getTaskTypeList() {
  // 默认时获取所有得工单类型,需要用type区别开,1:运维工单类型,2:运营工单类型
  const page = {
    ...loadAllParams,
    type: 1,
  };
  listTaskType(page).then((response) => {
    taskTypeList.value = response.rows;
  });
}
// 填写设备编号后
const handleCode = () => {
  if (form.value.innerCode) {
    getUserList();
  }
};
// 获取运维人员列表
const getUserList = () => {
  getOperationList(form.value.innerCode).then((response) => {
    userList.value = response.data;
  });
};
// 获取工单详情
const taskInfo = () => {
  getTask(taskId.value).then((response) => {
    form.value = response.data;
  });
};
// 查看详情
const openTaskDetailDialog = (row) => {
  taskId.value = row.taskId;
  taskInfo();
  detailVisible.value = true;
};
// 关闭详情弹层
const handleClose = () => {
  detailVisible.value = false;
};
getTaskTypeList();

getList();
</script>
<style lang="scss" scoped src="./index.scss"></style>

views\manage\task\index.scss

@import '@/assets/styles/variables.module.scss';
:deep(.task-status) {
    display: flex;
    align-items: center;
    height: 54px;
    margin-bottom: 25px;
    background-color: rgba(236, 236, 236, 0.39);
  
    .icon {
      margin-left: 22px;
    }
  
    .status {
      flex: 1;
      margin-left: 16px;
      color: rgba(0, 0, 0, 0.85);
    }
  
    .pic {
      margin-right: 76px;
      margin-bottom: 7px;
    }
  }
  .addr{
      display: flex;
      .el-icon{
          margin: 10px 5px 0 0;
      }
  }
  .desc, .addr {
    margin-top: 10px;
    line-height: 20px;
  
    .svg-icon {
      margin-right: 4px;
      color: $--color-primary;
    }
  }

(6)手动创建二级菜单

手动创建运营工单二级菜单

手动创建运维工单二级菜单

注意:在使用若依生成的动态SQL导入后,会有对菜单中的按钮权限进行导入。而自己手动创建的也需要手动为该菜单所用到的每个按钮其他Controller的请求分配相应的权限字符。如果不分配,其他用户登录后仅有菜单访问的权限,没有操作的权限。

运维工单二级菜单同理分配按钮权限。

注意:添加完菜单或按钮后需要管理员重新为角色分配菜单权限!

3、查询工单列表

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

  • 运营工单页面原型

  • 运维工单页面原型

  • 接口文档

  • 实现思路

  • 代码实现

TaskVo

@Data
public class TaskVo extends Task {

    // 工单类型
    private TaskType taskType;
}

TaskMapper

/**
* 查询运维工单列表
*
* @param task 运维工单
* @return TaskVo集合
*/
List<TaskVo> selectTaskVoList(Task task);

<!-- 手动映射Mapper -->
<resultMap type="TaskVo" id="TaskVoResult">
    <result property="taskId"    column="task_id"    />
    <result property="taskCode"    column="task_code"    />
    <result property="taskStatus"    column="task_status"    />
    <result property="createType"    column="create_type"    />
    <result property="innerCode"    column="inner_code"    />
    <result property="userId"    column="user_id"    />
    <result property="userName"    column="user_name"    />
    <result property="regionId"    column="region_id"    />
    <result property="desc"    column="desc"    />
    <result property="productTypeId"    column="product_type_id"    />
    <result property="assignorId"    column="assignor_id"    />
    <result property="addr"    column="addr"    />
    <result property="createTime"    column="create_time"    />
    <result property="updateTime"    column="update_time"    />
    <!-- 工单里查工单类型,工单:工单类型=1:n,column为查询条件字段 -->
    <association property="taskType" javaType="TaskType" column="product_type_id" select="com.dkd.manage.mapper.TaskTypeMapper.selectTaskTypeByTypeId" />
</resultMap>

<select id="selectTaskVoList" parameterType="Task" resultMap="TaskVoResult">
    <include refid="selectTaskVo"/>
    <where>
        <if test="taskCode != null  and taskCode != ''"> and task_code = #{taskCode}</if>
        <if test="taskStatus != null "> and task_status = #{taskStatus}</if>
        <if test="createType != null "> and create_type = #{createType}</if>
        <if test="innerCode != null  and innerCode != ''"> and inner_code = #{innerCode}</if>
        <if test="userId != null "> and user_id = #{userId}</if>
        <if test="userName != null  and userName != ''"> and user_name like concat('%', #{userName}, '%')</if>
        <if test="regionId != null "> and region_id = #{regionId}</if>
        <if test="desc != null  and desc != ''"> and `desc` = #{desc}</if>
        <if test="productTypeId != null "> and product_type_id = #{productTypeId}</if>
        <if test="assignorId != null "> and assignor_id = #{assignorId}</if>
        <if test="addr != null  and addr != ''"> and addr = #{addr}</if>
        <if test="params.isRepair != null and params.isRepair == 'true'">
            and product_type_id in (1,3,4)
        </if>
        <if test="params.isRepair != null and params.isRepair == 'false'">
            and product_type_id = 2
        </if>
    </where>
    order by create_time desc
</select>

ITaskService和实现类

/**
 * 查询运维工单列表
 * @param task
 * @return TaskVo集合
 */
List<TaskVo> selectTaskVoList(Task task);

/**
 * 查询运维工单列表
 * @param task
 * @return TaskVo集合
 */
@Override
public List<TaskVo> selectTaskVoList(Task task) {
    return taskMapper.selectTaskVoList(task);
}

TaskController

/**
 * 查询工单列表
 */
@PreAuthorize("@ss.hasPermi('manage:task:list')")
@GetMapping("/list")
public TableDataInfo list(Task task)
{
    startPage();
    List<TaskVo> voList = taskService.selectTaskVoList(task);
    return getDataTable(voList);
}

4、获取运营人员列表

  • 需求:根据售货机编号获取负责当前区域下的运营人员列表。
  • 页面原型(当设备编号输入完,输入框失去聚焦点后,会向后端发送请求查询运营人员列表)

  • 接口文档

接口文档中要求我们通过前端传入的设备编号innerCode,来查询该设备所属区域下的员工集合。

先分析一下员工到设备之间的关系:

一个设备投放在一个区域下的某点位,有多个员工负责在这个区域下工作。

看似我们需要查询四张表,才能根据售货机编号获取该区域下运营人员的列表。其实只需要两张表,下面是设备表和员工表的数据库字段设计:

设备表中有设备编号innerCode和所属区域id,而员工表中也有所属区域id,这样通过两张表就可以查询出来,需要注意的是我们要查询的是运营人员,因此需要role_code=1002的所有运营员,并且员工上班状态status=1为启用,所以这三个查询条件都要满足。

  • 实现思路

在设备的Mapper和Service编写根据innerCode查询设备信息的方法,之后在Emp的Controller中注入设备的Service对象,获取该设备所属区域id,将查询条件封装给参数,去查询该区域下启用的运营人员列表。

VendingMachineMapper

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

IVendingMachineService和实现类

/**
 * 根据设备编号查询设备信息
 *
 * @param innerCode
 * @return VendingMachine
 */
VendingMachine selectVendingMachineByInnerCode(String innerCode);

/**
 * 根据设备编号查询设备信息
 *
 * @param innerCode
 * @return VendingMachine
 */
@Override
public VendingMachine selectVendingMachineByInnerCode(String innerCode) {
    return vendingMachineMapper.selectVendingMachineByInnerCode(innerCode);
}

DkdContants(帝可得常量类)

/**
 * 员工启用
 */
public static final Long EMP_STATUS_NORMAL = 1L;
/**
 * 员工禁用
 */
public static final Long EMP_STATUS_DISABLE = 0L;
/**
 * 角色编码:运营员
 */
public static final String ROLE_CODE_BUSINESS = "1002";

/**
 * 角色编码:维修员
 */
public static final String ROLE_CODE_OPERATOR = "1003";

EmpController

@Autowired
private IVendingMachineService vendingMachineService;

/**
 * 根据售货机获取运营人员列表
 */
@PreAuthorize("@ss.hasPermi('manage:emp:list')")
@GetMapping("/businessList/{innerCode}")
public AjaxResult businessList(@PathVariable String innerCode) {
    // 根据innerCode查询售货机信息
    VendingMachine vm = vendingMachineService.selectVendingMachineByInnerCode(innerCode);
    if (vm == null) return error("售货机不存在");
    // 根据区域id、角色编号、员工状态查询运营人员列表
    Emp emp = new Emp();    // 封装查询条件对象
    emp.setRegionId(vm.getRegionId());  // 设备所属区域id
    emp.setRoleCode(DkdContants.ROLE_CODE_BUSINESS);    // 角色编码:运营员(1002)
    emp.setStatus(DkdContants.EMP_STATUS_NORMAL);   // 员工状态:启用(1)
    return success(empService.selectEmpList(emp));
}

5、获取运维人员列表

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

  • 接口文档

实现方式和思路与之前的获取运营人员列表同理,查两张表。

  • 代码实现

EmpController

/**
 * 根据售货机编码获取运维人员列表
 */
@PreAuthorize("@ss.hasPermi('manage:emp:list')")
@GetMapping("/operationList/{innerCode}")
public AjaxResult operationList(@PathVariable String innerCode) {
    // 根据innerCode查询售货机信息
    VendingMachine vm = vendingMachineService.selectVendingMachineByInnerCode(innerCode);
    if (vm == null) return error("售货机不存在");
    // 根据区域id、角色编号、员工状态查询运维人员列表
    Emp emp = new Emp();    // 封装查询条件对象
    emp.setRegionId(vm.getRegionId());  // 设备所属区域id
    emp.setRoleCode(DkdContants.ROLE_CODE_OPERATOR);    // 角色编码:维修员(1003)
    emp.setStatus(DkdContants.EMP_STATUS_NORMAL);   // 员工状态:启用(1)
    return success(empService.selectEmpList(emp));
}

6、新增工单

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

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

运营工单和运维工单共用一个后端新增工单接口,提高代码复用性。

  • 页面原型

  • 接口文档(需要创建DTO)

  • 实现思路

新增工单时序图

新增工单业务流程图

  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; // 商品Id
    private String skuName; // 商品名称
    private String skuImage;    // 商品图片
}

TaskDTO

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

TaskDetailsMapper和xml

/**
 * 批量新增工单详情
 * @param taskDetailsList
 * @return 结果
 */
int batchInsertTaskDetails(List<TaskDetails> taskDetailsList);

<!-- 批量新增工单详情 -->
<insert id="batchInsertTaskDetails" parameterType="java.util.List">
    insert into tb_task_details (task_id, channel_code, expect_capacity, sku_id, sku_name, sku_image)
    values
    <foreach collection="list" item="item" index="index" separator=", ">
        (#{item.taskId}, #{item.channelCode}, #{item.expectCapacity}, #{item.skuId}, #{item.skuName}, #{item.skuImage})
    </foreach>
</insert>

ITaskDetailsService和实现类

/**
 * 批量新增工单详情
 * @param taskDetailsList
 * @return 结果
 */
int batchInsertTaskDetails(List<TaskDetails> taskDetailsList);

/**
 * 批量新增工单详情
 * @param taskDetailsList
 * @return 结果
 */
@Override
public int batchInsertTaskDetails(List<TaskDetails> taskDetailsList) {
    return taskDetailsMapper.batchInsertTaskDetails(taskDetailsList);
}

ITaskService和实现类

/**
 * 新增运营或运维工单
 * @param taskDTO
 * @return
 */
int insertTaskDTO(TaskDTO taskDTO);

@Autowired
private IVendingMachineService vendingMachineService;
@Autowired
private IEmpService empService;
@Autowired
private RedisTemplate redisTemplate;    // 注入redis的模板操作对象
@Autowired
private ITaskDetailsService taskDetailsService;

/**
 * 新增运营或运维工单
 * 事务管理:工单表、工单详情表
 * @param taskDTO
 * @return
 */
@Transactional
@Override
public int insertTaskDTO(TaskDTO taskDTO) {
    // 查询售货机是否存在
    VendingMachine vm = vendingMachineService.selectVendingMachineByInnerCode(taskDTO.getInnerCode());
    if (vm == null) throw new ServiceException("设备不存在");
    // 校验售货机状态和工单类型是否相符
    checkCreateTask(vm.getVmStatus(), taskDTO.getProductTypeId());
    // 检查设备是否有未完成的同类型工单
    hasTask(taskDTO);
    // 查询并校验员工是否存在(保证安全性)
    Emp emp = empService.selectEmpById(taskDTO.getUserId());
    if (emp == null) throw new ServiceException("所指派员工不存在");
    // 校验员工区域是否匹配
    if (!emp.getRegionId().equals(vm.getRegionId())) throw new ServiceException("员工所在区域与设备区域不一致,无法处理此工单");
    // 将DTO转为PO并补充属性,保存工单
    Task task = BeanUtil.copyProperties(taskDTO, Task.class);   // 将DTO中的6个公共字段拷贝到PO中
    task.setTaskStatus(DkdContants.TASK_STATUS_CREATE); // 工单状态:已创建,待指派
    task.setUserName(emp.getUserName());    // 执行人名称
    task.setRegionId(vm.getRegionId()); // 所属区域id
    task.setAddr(vm.getAddr()); // 设备详细地址
    task.setCreateTime(DateUtils.getNowDate()); // 创建时间
    task.setTaskCode(generateTaskCode());   // 工单编号
    int result = taskMapper.insertTask(task);
    // 判断是否为补货工单,如果是则批量新增工单详情
    if (DkdContants.TASK_TYPE_SUPPLY.equals(task.getProductTypeId())) {
        // 保存工单详情
        List<TaskDetailsDTO> details = taskDTO.getDetails();
        if (CollUtil.isEmpty(details)) throw new ServiceException("补货工单详情不能为空");
        // 将DTO转为PO对象,并补充属性
        List<TaskDetails> taskDetailsList = details.stream().map(dto -> {
            TaskDetails taskDetails = BeanUtil.copyProperties(dto, TaskDetails.class);
            taskDetails.setTaskId(task.getTaskId());
            return taskDetails;
        }).collect(Collectors.toList());
        // 批量新增
        taskDetailsService.batchInsertTaskDetails(taskDetailsList);
    }
    return result;
}

/**
 * 生成并获取当天工单编号(唯一表示)
 * 生成格式:当天日期 + redis自增序列(补齐4位)
 * 如:202409240001 ~ 202409249999
 * 该方法首先尝试从Redis中获取当天的任务代码计数,如果不存在,则初始化为1并返回"日期0001"格式的字符串。
 * 如果存在,则对计数加1并返回更新后的任务代码。
 * @return 工单编号
 */
private String generateTaskCode() {
    // 获取当前日期并格式化为"yyyyMMdd"
    String dateStr = DateUtils.getDate().replaceAll("-", "");
    // 根据日期生成redis自增器的键
    String key = "dkd.task.code." + dateStr;
    // 判断key是否存在
    if (!redisTemplate.hasKey(key)) {
        // 如果key不存在,设置初始值为1,并指定过期时间为1天,第二天自动销毁
        redisTemplate.opsForValue().set(key, 1, Duration.ofDays(1));
        // 返回日期编号(日期+0001)
        return dateStr + "0001";
    }
    // 如果key存在,redis计数器+1(0002),确保字符串长度为4位
    return dateStr + StrUtil.padPre(redisTemplate.opsForValue().increment(key).toString(), 4, '0');
}


/**
 * 检查该设备是否有未完成的同类型工单
 * @param taskDTO
 */
private void hasTask(TaskDTO taskDTO) {
    // 创建Task查询条件对象,并设置设备编号和工单类型,以及工单状态为进行中
    Task task = new Task();
    task.setInnerCode(taskDTO.getInnerCode());
    task.setProductTypeId(taskDTO.getProductTypeId());
    task.setTaskStatus(DkdContants.TASK_STATUS_PROGRESS);   // 工单状态为进行中
    // 调用taskMapper查询数据库查看是否有符合条件的工单列表
    List<Task> taskList = taskMapper.selectTaskList(task);
    // 如果存在未完成的同类型工单,抛出异常
    if (CollUtil.isNotEmpty(taskList)) throw new ServiceException("该设备已有未完成的工单,不能重复创建");
    // 如果存在已创建,待处理的同类型工单,抛出异常
    task.setTaskStatus(DkdContants.TASK_STATUS_CREATE); // 工单状态为创建(待处理)
    taskList = taskMapper.selectTaskList(task);
    if (CollUtil.isNotEmpty(taskList)) throw new ServiceException("该设备已有待处理的工单,不能重复创建");
}

/**
 * 校验售货机状态和工单类型是否相符
 * @param vmStatus 设备状态
 * @param productTypeId 工单类型id
 */
private void checkCreateTask(Long vmStatus, Long productTypeId) {
    // 如果是投放工单,设备在运行中,无法投放,抛出异常
    if (Objects.equals(productTypeId, DkdContants.TASK_TYPE_DEPLOY) && Objects.equals(vmStatus, DkdContants.VM_STATUS_RUNNING)) {
        throw new ServiceException("该设备状态为运行中,无法进行投放");
    }
    // 如果是维修工单,设备不在运行中,抛出异常(未投放和撤机)
    if (Objects.equals(productTypeId, DkdContants.TASK_TYPE_REPAIR) && !Objects.equals(vmStatus, DkdContants.VM_STATUS_RUNNING)) {
        throw new ServiceException("该设备状态不在运行中,无法进行维修");
    }
    // 如果是补货工单,设备不在运行中,抛出异常(未投放和撤机)
    if (Objects.equals(productTypeId, DkdContants.TASK_TYPE_SUPPLY) && !Objects.equals(vmStatus, DkdContants.VM_STATUS_RUNNING)) {
        throw new ServiceException("该设备状态不在运行中,无法进行补货");
    }
    // 如果是撤机工单,设备不在运行中,无法撤机,抛出异常
    if (Objects.equals(productTypeId, DkdContants.TASK_TYPE_REVOKE) && !Objects.equals(vmStatus, DkdContants.VM_STATUS_RUNNING)) {
        throw new ServiceException("该设备状态不在运行中,无法进行撤机");
    }
}
  • 测试新增工单

  • 测试再次添加同类型工单

7、取消工单

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

运维工单和运营工单共享同一套取消工单后端接口。

  • 接口文档

  • 实现思路

  • 代码实现

TaskController

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

ITaskService

/**
 * 取消工单
 * @param task
 * @return 结果
 */
int cancelTask(Task task);

/**
 * 取消工单
 * @param task
 * @return 结果
 */
@Override
public int cancelTask(Task task) {
    // 判断工单状态是否可以取消
    Task taskDb = taskMapper.selectTaskByTaskId(task.getTaskId());
    if (DkdContants.TASK_STATUS_CANCEL.equals(taskDb.getTaskStatus())) {
        throw new ServiceException("该工单已取消,不能再次取消");
    }
    // 判断工单状态是否为已完成,如果是,则抛出异常
    if (DkdContants.TASK_STATUS_FINISH.equals(taskDb.getTaskStatus())) {
        throw new ServiceException("该工单已完成,不能取消");
    }
    // 设置更新字段,注意更新使用的是前端的task作为参数
    task.setTaskStatus(DkdContants.TASK_STATUS_CANCEL); // 工单状态:取消
    task.setUpdateTime(DateUtils.getNowDate()); // 更新时间
    return taskMapper.updateTask(task); // 更新工单
}
  • 测试取消工单功能

8、查看补货详情

  • 需求:运营工单页面可以查看补货详情。

  • 页面原型

  • 接口文档

  • 实现思路

  • 代码实现

TaskDetailsController

/**
 * 查看工单补货详情
 */
@PreAuthorize("@ss.hasPermi('manage:taskDetails:list')")
@GetMapping("/byTaskId/{taskId}")
public AjaxResult byTaskId(@PathVariable Long taskId) {
    TaskDetails taskDetails = new TaskDetails();
    taskDetails.setTaskId(taskId);
    return success(taskDetailsService.selectTaskDetailsList(taskDetails));
}

9、Knife4j

如果不习惯使用 swagger 可以使用 前端UI 的增强解决方案 knife4j,对比 swagger 相比有以下优势,友好界面,离线文档,接口排序,安全控制,在线调试,文档清晰,注解增强,容易上手。

  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
{
    @Autowired
    private ITaskDetailsService taskDetailsService;

    @ApiOperation(value = "获取工单详情列表", notes = "查询所有工单详情记录")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "taskDetails", value = "工单详情对象", required = true, dataType = "TaskDetails", paramType = "query")
    })
    @PreAuthorize("@ss.hasPermi('manage:taskDetails:list')")
    @GetMapping("/list")
    public TableDataInfo list(TaskDetails taskDetails) {
        startPage();
        List<TaskDetails> list = taskDetailsService.selectTaskDetailsList(taskDetails);
        return getDataTable(list);
    }

    @ApiOperation(value = "导出工单详情列表", notes = "导出工单详情记录到Excel文件")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "taskDetails", value = "工单详情对象", required = true, dataType = "TaskDetails", paramType = "form")
    })
    @PreAuthorize("@ss.hasPermi('manage:taskDetails:export')")
    @Log(title = "工单详情", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response, @ApiParam(value = "工单详情对象", required = true) TaskDetails taskDetails) {
        List<TaskDetails> list = taskDetailsService.selectTaskDetailsList(taskDetails);
        ExcelUtil<TaskDetails> util = new ExcelUtil<TaskDetails>(TaskDetails.class);
        util.exportExcel(response, list, "工单详情数据");
    }

    @ApiOperation(value = "获取工单详情详细信息", notes = "根据ID获取工单详情")
    @ApiImplicitParam(name = "detailsId", value = "工单详情ID", required = true, dataType = "Long", paramType = "path")
    @PreAuthorize("@ss.hasPermi('manage:taskDetails:query')")
    @GetMapping(value = "/{detailsId}")
    public R<TaskDetails> getInfo(@PathVariable("detailsId") Long detailsId) {
        return R.ok(taskDetailsService.selectTaskDetailsByDetailsId(detailsId));
    }

    @ApiOperation(value = "新增工单详情", notes = "创建新的工单详情记录")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "taskDetails", value = "工单详情对象", required = true, dataType = "TaskDetails", paramType = "body")
    })
    @PreAuthorize("@ss.hasPermi('manage:taskDetails:add')")
    @Log(title = "工单详情", businessType = BusinessType.INSERT)
    @PostMapping
    public R add(@RequestBody TaskDetails taskDetails) {
        return R.toAjax(taskDetailsService.insertTaskDetails(taskDetails));
    }

    @ApiOperation(value = "修改工单详情", notes = "更新现有的工单详情记录")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "taskDetails", value = "工单详情对象", required = true, dataType = "TaskDetails", paramType = "body")
    })
    @PreAuthorize("@ss.hasPermi('manage:taskDetails:edit')")
    @Log(title = "工单详情", businessType = BusinessType.UPDATE)
    @PutMapping
    public R edit(@RequestBody TaskDetails taskDetails) {
        return R.toAjax(taskDetailsService.updateTaskDetails(taskDetails));
    }

    @ApiOperation(value = "删除工单详情", notes = "根据ID批量删除工单详情记录")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "detailsIds", value = "工单详情ID数组", required = true, dataType = "Long[]", paramType = "path")
    })
    @PreAuthorize("@ss.hasPermi('manage:taskDetails:remove')")
    @Log(title = "工单详情", businessType = BusinessType.DELETE)
    @DeleteMapping("/{detailsIds}")
    public R remove(@PathVariable Long[] detailsIds) {
        return R.toAjax(taskDetailsService.deleteTaskDetailsByDetailsIds(detailsIds));
    }

    @ApiOperation(value = "查看工单补货详情", notes = "根据工单ID获取工单详情列表")
    @ApiImplicitParam(name = "taskId", value = "工单ID", required = true, dataType = "Long", paramType = "path")
    @PreAuthorize("@ss.hasPermi('manage:taskDetails:list')")
    @GetMapping("/byTaskId/{taskId}")
    public R<List<TaskDetails>> byTaskId(@PathVariable Long taskId) {
        TaskDetails taskDetails = new TaskDetails();
        taskDetails.setTaskId(taskId);
        return R.ok(taskDetailsService.selectTaskDetailsList(taskDetails));
    }
}

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

  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


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



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值