文件存储

本地存储

在若依框架目前的实现中,是把图片存储到了服务器本地的目录

  1. 本地存储存在的问题
  • 性能瓶颈
  • 磁盘满了
  • 单点故障
  1. 自定义存储服务
  • FastDFS
  • Minlo
  1. 第三方储存服务
  • 我们要使用云服务方案替代本地储存方案

阿里云OSS

阿里云对象存储OSS (Object Storage Service),是一款海量、安全、低成本、高可靠的云存储服务。使用OSS,您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。

阿里云使用步骤

  1. SDK: Software Development Kit的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包)、代码示例等,都可以叫做SDK.
  2. BuCket: 存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间。
  3. 创建Bucket

x-file-storage

文件存储

XFile Storage官网: https://x-file-storage.xuyanwu.cn/

  1. 一行代码将文件存储到本地、阿里云OSS、华为云OBS、七牛云Kodo、腾讯云COS....其它兼容S3协议的存储平台
  1. 优势
  • 简化配置
  • 易于集成
  • 功能丰富
  • 开箱即用

集成到项目中,官网写的很详细,这里只做关键记录

  1. 引入依赖
        <!-- 文件上传  -->
        <dependency>
            <groupId>org.dromara.x-file-storage</groupId>
            <artifactId>x-file-storage-spring</artifactId>
            <version>2.3.0</version>
        </dependency>

        <!-- 阿里云OSS -->
        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
            <version>3.16.1</version>
        </dependency>

  1. 配置参数
# 文件存储配置
dromara:
  x-file-storage: #文件存储配置
    default-platform:  aliyun-oss-1 #默认使用的存储平台
    thumbnail-suffix: ".min.jpg" #缩略图后缀,例如【.min.jpg】【.png】
    #对应平台的配置写在这里,注意缩进要对齐
    aliyun-oss:
      - platform: aliyun-oss-1 # 存储平台标识,default-platform要和该配置保持一致
        enable-storage: true  # 启用存储
        access-key: 
        secret-key: 
        end-point: oss-cn-qingdao.aliyuncs.com # 从阿里云控制台获取 【对象存储/Bucket列表/dkd-project-app/概览/访问端口/外网访问】
        bucket-name: dkd-project-app
        domain: https://dkd-project-app.oss-cn-qingdao.aliyuncs.com/ # 访问域名,注意“/”结尾,例如:https://abc.oss-cn-shanghai.aliyuncs.com/  【自己拼就行】
        base-path: dkd-images/ # 基础路径

  1. 添加注解

  1. 后端代码改造
package com.dkd.web.controller.common;

/**
 * 通用请求处理
 * 
 * @author ruoyi
 */
@RestController
@RequestMapping("/common")
public class CommonController
{
    private static final Logger log = LoggerFactory.getLogger(CommonController.class);

    @Autowired
    private ServerConfig serverConfig;

    @Autowired
    private FileStorageService fileStorageService;

    private static final String FILE_DELIMETER = ",";


    /**
     * 通用上传请求(单个)
     */
    @PostMapping("/upload")
    public AjaxResult uploadFile(MultipartFile file) throws Exception
    {
        try
        {
            //  原始代码
            //  // 上传文件路径
            //  String filePath = RuoYiConfig.getUploadPath();
            //  // 上传并返回新文件名称
            //  String fileName = FileUploadUtils.upload(filePath, file);
            //  String url = serverConfig.getUrl() + fileName;
            //  AjaxResult ajax = AjaxResult.success();
            //  ajax.put("url", url);
            //  ajax.put("fileName", fileName);
            //  ajax.put("newFileName", FileUtils.getName(fileName));
            //  ajax.put("originalFilename", file.getOriginalFilename());
            //  return ajax;

            // 指定oss保存的文件路径 (例 dkd-images/2020/12/09/文件名)
            String objectName = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) +  "/";
            FileInfo fileInfo = fileStorageService.of(file)
                    .setPath(objectName) //保存到相对路径下,为了方便管理,不需要可以不写
                    .upload();
            AjaxResult ajax = AjaxResult.success();
            ajax.put("url", fileInfo.getUrl());
            ajax.put("fileName", fileInfo.getUrl());
            // 注意:这里的值需要改成URL, 因为前端的访问地址已经做了判断,是http开头就直接显示图片
            ajax.put("newFileName", fileInfo.getUrl());
            ajax.put("originalFilename", file.getOriginalFilename());
            return ajax;

        }
        catch (Exception e)
        {
            return AjaxResult.error(e.getMessage());
        }
    }

}
  1. 前端代码改造

  1. 重启服务,测试一下

设备管理

前置工作

需求说明

  1. 业务场景: 管理员在系统录入设备信息后,员工将负责设备(智能售货机)的投放和商品补货工作

  1. 设备管理主要涉及到三个功能模块,业务流程如下:

  • 新增设备类型: 允许管理员定义新的售货机型号,包括其规格和容量。
  • 新增设备: 在新的设备类型定义后,系统应允许添加新的售货机实例,并将它们分配到特定的点位。
  • 新增货道: 对于每个新添加的设备,系统应支持定义新的货道,后期用于关联相应的商品SKU。

库表设计: 对于设备管理数据模型,下面是示意图:

  • 关系字段:vm_type_id、node_id、vm_id
  • 数据字典:vm_status(0未投放、1运营、3撤机)
  • 冗余字段:addr、business_type、region_id、partner_id(简化查询接口、提高查询效率)

代码生成

使用若依代码生成器,生成设备类型、设备、货道前后端基础代码,并导入到项目中:

  1. 创建目录菜单

  1. 添加数据字典

  1. 配置代码生成信息

  1. 下载代码并导入项目

设备类型改造

参考页面原型,完成基础布局展示改造

  1. 在vmType/index.vue视图组件中修改
<!-- 查询条件 -->
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
    <el-form-item label="型号名称" prop="name">
        <el-input  v-model="queryParams.name" 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="vmTypeList" @selection-change="handleSelectionChange">
    <el-table-column type="selection" width="55" align="center" />
    <el-table-column label="型号名称" align="center" prop="name" />
    <el-table-column label="型号编码" align="center" prop="model" />
    <el-table-column label="设备图片" align="center" prop="image" width="100">
        <template #default="scope">
            <image-preview :src="scope.row.image" :width="50" :height="50"/>
        </template>
    </el-table-column>

    <el-table-column label="货道行" align="center" prop="vmRow" />
    <el-table-column label="货道列" align="center" prop="vmCol" />
    <el-table-column label="设备容量" align="center" prop="channelMaxCapacity" />
    <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:vmType:edit']">修改</el-button>

        <el-button link type="primary" @click="handleDelete(scope.row)" v-hasPermi="['manage:vmType:remove']">删除</el-button>

        </template>

    </el-table-column>

</el-table>

<!-- 添加或修改设备类型管理对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
    <el-form ref="vmTypeRef" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="型号名称" prop="name">
            <el-input v-model="form.name" placeholder="请输入型号名称" />
        </el-form-item>

        <el-form-item label="型号编码" prop="model">
            <el-input v-model="form.model" placeholder="请输入型号编码" />
        </el-form-item>

        <el-form-item label="货道数" prop="vmRow">
            <el-input-number v-model="form.vmRow" placeholder="请输入"  :min="1" :max="10"/>行&nbsp;&nbsp;
            <el-input-number v-model="form.vmCol" placeholder="请输入"  :min="1" :max="10"/>列
        </el-form-item>

        <el-form-item label="货道容量" prop="channelMaxCapacity">
            <el-input-number v-model="form.channelMaxCapacity" placeholder="请输入"  :min="1" :max="10"/>个
        </el-form-item>

        <el-form-item label="设备图片" prop="image">
            <image-upload v-model="form.image"/>
        </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>

设备管理改造

参考页面原型,完成基础布局展示改造

  1. 刷新设备表数据
update tb_vending_machine set partner_id=2 where id=80;
update tb_vending_machine set addr=(select address from tb_node where id = 1) where node_id=1;
update tb_vending_machine set addr=(select address from tb_node where id = 2) where node_id=2;

  1. 在vm/index.vue视图组件中修改
<!-- 查询条件 -->
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
    <el-form-item label="设备编号" prop="innerCode">
        <el-input v-model="queryParams.innerCode" 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="vmList" @selection-change="handleSelectionChange">
  <el-table-column type="selection" width="55" align="center" />
  <el-table-column label="设备编号" align="center" prop="innerCode" />
  <el-table-column label="设备型号" align="center" prop="vmTypeId" >
    <template #default="scope">
      <div v-for="item in vmTypeList" :key="item.id">
        <span v-if="item.id==scope.row.vmTypeId">{{ item.name }}</span>

      </div>

    </template>

  </el-table-column>

  <el-table-column label="详细地址" align="center" prop="addr" />
  <el-table-column label="合作商" align="center" prop="partnerId" >
    <template #default="scope">
      <div v-for="item in partnerList" :key="item.id">
        <span v-if="item.id==scope.row.partnerId">{{ item.partnerName }}</span>

      </div>

    </template>

  </el-table-column>

  <el-table-column label="设备状态" align="center" prop="vmStatus">
    <template #default="scope">
      <dict-tag :options="vm_status" :value="scope.row.vmStatus" />
    </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:vm:edit']">修改</el-button>

    </template>

  </el-table-column>

</el-table>

<!-- 添加或修改设备管理对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
  <el-form ref="vmRef" :model="form" :rules="rules" label-width="80px">
    <el-form-item label="设备编号">
      <span>{{ form.innerCode == null ? '系统自动生成' : form.innerCode }}</span>

    </el-form-item>

    <el-form-item label="供货时间" v-if="form.innerCode != null">
      <span>{{ parseTime(form.lastSupplyTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>

    </el-form-item>

    <el-form-item label="设备类型" v-if="form.innerCode != null">
      <div v-for="item in vmTypeList" :key="item.id">
        <span v-if="form.vmTypeId == item.id">{{ item.name }}</span>

      </div>

    </el-form-item>

    <el-form-item label="设备容量" v-if="form.innerCode != null">
      <span>{{ form.channelMaxCapacity }}</span>

    </el-form-item>

    <el-form-item label="选择型号" prop="vmTypeId" v-if="form.innerCode == null">
      <!-- <el-input v-model="form.vmTypeId" placeholder="请输入设备型号" /> -->
      <el-select v-model="form.vmTypeId" placeholder="请选择设备型号" style="width: 100%">
        <el-option v-for="item in vmTypeList" :key="item.id" :label="item.name" :value="item.id" />
      </el-select>

    </el-form-item>

    <el-form-item label="选择点位" prop="nodeId">
      <!-- <el-input v-model="form.nodeId" placeholder="请输入点位Id" /> -->
      <el-select v-model="form.nodeId" placeholder="请选择点位" style="width: 100%">
        <el-option v-for="item in nodeList" :key="item.id" :label="item.nodeName" :value="item.id" />
      </el-select>

    </el-form-item>

    <el-form-item label="合作商" v-if="form.innerCode != null">
      <div v-for="item in partnerList" :key="item.id">
        <span v-if="form.partnerId == item.id">{{ item.partnerName }}</span>

      </div>

    </el-form-item>

    <el-form-item label="所属区域" v-if="form.innerCode != null">
      <div v-for="item in regionList" :key="item.id">
        <span v-if="form.regionId == item.id">{{ item.regionName }}</span>

      </div>

    </el-form-item>

    <el-form-item label="设备地址" v-if="form.innerCode != null">
        <span>{{ form.addr }}</span>

    </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="Vm">
import { listVmType } from "@/api/manage/vmType";
import { listPartner } from "@/api/manage/partner";
import { loadAllParams } from '@/api/page';
import { listNode } from '@/api/manage/node';
import { listRegion } from "@/api/manage/region";
    
/* 查询设备类型列表 */
const vmTypeList = ref([]);
function getVmTypeList() {
  listVmType(loadAllParams).then((response) => {
    vmTypeList.value = response.rows;
  });
}
/* 查询合作商列表 */
const partnerList = ref([]);
function getPartnerList() {
  listPartner(loadAllParams).then((response) => {
    partnerList.value = response.rows;
  });
}
/* 查询点位列表 */
const nodeList = ref([]);
function getNodeList() {
  listNode(loadAllParams).then((response) => {
    nodeList.value = response.rows;
  });
}
/* 查询区域列表 */
const regionList = ref([]);
function getRegionList() {
  listRegion(loadAllParams).then((response) => {
    regionList.value = response.rows;
  });
}
getRegionList();
getPartnerList();
getNodeList();
getVmTypeList();
</script>

新增设备

新增设备时,补充设备表其他字段信息,还需要根据售货机类型创建所属货道

我们了解到在新增设备时,添加设备和货道表,还包含点位和设备类型的查询,共涉及到四张表的操作。 这个过程需要我们仔细处理每个字段,确保数据的一致性和完整性

package com.dkd.manage.service.impl;

/**
 * 设备管理Service业务层处理
 * 
 * @author itheima
 * @date 2026-02-21
 */
@Service
public class VendingMachineServiceImpl implements IVendingMachineService 
{
    @Autowired
    private VendingMachineMapper vendingMachineMapper;

    @Autowired
    private VmTypeServiceImpl vmTypeService;

    @Autowired
    private NodeServiceImpl nodeService;

    @Autowired
    private ChannelServiceImpl channelService;

    /**
     * 新增设备管理
     * 
     * @param vendingMachine 设备管理
     * @return 结果
     */
    @Override
    public int insertVendingMachine(VendingMachine vendingMachine)
    {
        // 1.新增设备
        // 设置售货机编号
        String innerCode = UUIDUtils.getUUID();
        vendingMachine.setInnerCode(innerCode);

        // 查询售货机类型表,补充设备容量
        VmType vmType = vmTypeService.selectVmTypeById(vendingMachine.getVmTypeId());
        vendingMachine.setChannelMaxCapacity(vmType.getChannelMaxCapacity());

        // 查询点位表,补充 区域、点位、合作商等信息
        Node node = nodeService.selectNodeById(vendingMachine.getNodeId());
        BeanUtil.copyProperties(node, vendingMachine, "id");
        vendingMachine.setAddr(node.getAddress());

        // 设备状态
        vendingMachine.setVmStatus(DkdContants.VM_STATUS_NODEPLOY);// 0-未投放(数据库有默认值,这个不写也不影响)
        vendingMachine.setCreateTime(DateUtils.getNowDate());// 创建时间
        vendingMachine.setUpdateTime(DateUtils.getNowDate());// 更新时间
        int res = vendingMachineMapper.insertVendingMachine(vendingMachine);

        // 2.新增货道
        List<Channel> channelList = new ArrayList<>();
        for (int i = 1; i <= vmType.getVmRow(); i++) { // 外层行
            for (int j = 1; j <= vmType.getVmCol(); j++) {// 内层列
                //2-3 封装channel
                Channel channel = new Channel();
                channel.setChannelCode(i + "-" + j);// 货道编号
                channel.setVmId(vendingMachine.getId());// 售货机id
                channel.setInnerCode(vendingMachine.getInnerCode());// 售货机编号
                channel.setMaxCapacity(vmType.getChannelMaxCapacity());// 货道最大容量
                channel.setCreateTime(DateUtils.getNowDate());// 创建时间
                channel.setUpdateTime(DateUtils.getNowDate());// 更新时间
                channelList.add(channel);
            }
        }
        channelService.batchInsertChannel(channelList);
        return res;
    }

}
package com.dkd.manage.service;

import java.util.List;
import com.dkd.manage.domain.Channel;

/**
 * 售货机货道Service接口
 * 
 * @author itheima
 * @date 2026-02-21
 */
public interface IChannelService 
{
    /**
     * 批量新增售货机货道
     *
     * @param channelList 售货机货道集合
     * @return 结果
     */
    int batchInsertChannel(List<Channel> channelList);
}
package com.dkd.manage.service.impl;

/**
 * 售货机货道Service业务层处理
 * 
 * @author itheima
 * @date 2026-02-21
 */
@Service
public class ChannelServiceImpl implements IChannelService 
{
    @Autowired
    private ChannelMapper channelMapper;

    /**
     * 批量新增售货机货道
     *
     * @param channelList 售货机货道集合
     * @return 结果
     */
    @Override
    public int batchInsertChannel(List<Channel> channelList) {
        return channelMapper.batchInsertChannel(channelList);
    }
}
package com.dkd.manage.mapper;

import java.util.List;
import com.dkd.manage.domain.Channel;

/**
 * 售货机货道Mapper接口
 * 
 * @author itheima
 * @date 2026-02-21
 */
public interface ChannelMapper 
{

    /**
     * 批量新增售货机货道
     *
     * @param channelList 售货机货道集合
     * @return 结果
     */
    int batchInsertChannel(List<Channel> channelList);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dkd.manage.mapper.ChannelMapper">
    <insert id="batchInsertChannel" parameterType="java.util.List">
        INSERT INTO tb_channel (
        channel_code, vm_id, inner_code, max_capacity, last_supply_time, create_time, update_time
        ) VALUES
        <foreach collection="list" item="channel" separator=",">
            (
            #{channel.channelCode},
            #{channel.vmId},
            #{channel.innerCode},
            #{channel.maxCapacity},
            #{channel.lastSupplyTime},
            #{channel.createTime},
            #{channel.updateTime}
            )
        </foreach>

    </insert>

</mapper>

测试一下: 新增设备后,设备对应的货道信息也创建了

修改设备

  1. 修改设备时,根据点位同步更新冗余字段信息

  1. 根据前端提交的点位ID,后端需要查询点位表,来获取点位的详细信息,包括详细地址、商圈类型、区域ID和合作商ID,获取到点位信息后,我们需要更新设备表中的相关冗余字段。

package com.dkd.manage.service.impl;

/**
 * 设备管理Service业务层处理
 * 
 * @author itheima
 * @date 2026-02-21
 */
@Service
public class VendingMachineServiceImpl implements IVendingMachineService 
{
    @Autowired
    private VendingMachineMapper vendingMachineMapper;

    @Autowired
    private VmTypeServiceImpl vmTypeService;

    @Autowired
    private NodeServiceImpl nodeService;

    @Autowired
    private ChannelServiceImpl channelService;

   
    /**
     * 修改设备管理
     * 
     * @param vendingMachine 设备管理
     * @return 结果
     */
    @Override
    public int updateVendingMachine(VendingMachine vendingMachine)
    {
        //查询点位表,补充 区域、点位、合作商等信息
        Node node = nodeService.selectNodeById(vendingMachine.getNodeId());
        BeanUtil.copyProperties(node, vendingMachine, "id");// 商圈类型、区域、合作商
        vendingMachine.setAddr(node.getAddress());// 设备地址
        vendingMachine.setUpdateTime(DateUtils.getNowDate());// 更新时间
        return vendingMachineMapper.updateVendingMachine(vendingMachine);
    }

}

设备状态改造

为设备状态管理功能创建前端页面,并在若依框架中定义相应的路由和菜单项, 然后基于原型完成视图组件基础布局展示改造

  1. 创建vmStatus/index.vue视图组件, 复用vm/index.vue页面

  1. 创建二级菜单

  1. 改造视图组件
<template>
  <div class="app-container">
    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
      <el-form-item label="设备编号" prop="innerCode">
        <el-input
          v-model="queryParams.innerCode"
          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="vmList" @selection-change="handleSelectionChange">
      <el-table-column label="序号" type="index" width="55" align="center" />
      <el-table-column label="设备编号" align="center" prop="innerCode" />
      <el-table-column label="设备型号" align="center" prop="vmTypeId" >
        <template #default="scope">
          <div v-for="item in vmTypeList" :key="item.id">
            <span v-if="item.id==scope.row.vmTypeId">{{ item.name }}</span>

          </div>

        </template>

      </el-table-column>

      <el-table-column label="详细地址" align="left" prop="addr" show-overflow-tooltip="true"/>
      <el-table-column label="运营状态" align="center" prop="vmStatus">
        <template #default="scope">
          <dict-tag :options="vm_status" :value="scope.row.vmStatus"/>
        </template>

      </el-table-column>

      <el-table-column label="设备状态" align="center" prop="vmStatus">
        <template #default="scope">
         {{ scope.row.runningStatus!=null? JSON.parse(scope.row.runningStatus).status==true?'正常':'异常' :'异常'}}
        </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="getVmInfo(scope.row)" v-hasPermi="['manage:vm:query']">查看详情</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-dialog>

  </div>

</template>

<script setup name="Vm">
import { listVm, getVm, delVm, addVm, updateVm } from "@/api/manage/vm";
import{listVmType} from "@/api/manage/vmType";
import{listPartner} from "@/api/manage/partner";
import{loadAllParams} from "@/api/page";
import{listNode} from "@/api/manage/node";
import{listRegion} from "@/api/manage/region";
import { ref } from "vue";

const { proxy } = getCurrentInstance();
const { vm_status } = proxy.useDict('vm_status');

const vmList = 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 data = reactive({
  form: {},
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    innerCode: null,
    nodeId: null,
    businessType: null,
    regionId: null,
    partnerId: null,
    vmTypeId: null,
    vmStatus: null,
    runningStatus: null,
    policyId: null,
  },
  rules: {
    nodeId: [
      { required: true, message: "点位Id不能为空", trigger: "blur" }
    ],
    vmTypeId: [
      { required: true, message: "设备型号不能为空", trigger: "blur" }
    ],
  }
});

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

/** 查询设备管理列表 */
function getList() {
  loading.value = true;
  listVm(queryParams.value).then(response => {
    vmList.value = response.rows;
    total.value = response.total;
    loading.value = false;
  });
}

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

// 表单重置
function reset() {
  form.value = {
    id: null,
    innerCode: null,
    channelMaxCapacity: null,
    nodeId: null,
    addr: null,
    lastSupplyTime: null,
    businessType: null,
    regionId: null,
    partnerId: null,
    vmTypeId: null,
    vmStatus: null,
    runningStatus: null,
    longitudes: null,
    latitude: null,
    clientId: null,
    policyId: null,
    createTime: null,
    updateTime: null
  };
  proxy.resetForm("vmRef");
}

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

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

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

/** 新增按钮操作 */
function handleAdd() {
  reset();
  open.value = true;
  title.value = "添加设备管理";
}

/** 修改按钮操作 */
function getVmInfo(row) {
  reset();
  const _id = row.id || ids.value
  getVm(_id).then(response => {
    form.value = response.data;
    open.value = true;
    title.value = "设备详情";
  });
}

/** 提交按钮 */
function submitForm() {
  proxy.$refs["vmRef"].validate(valid => {
    if (valid) {
      if (form.value.id != null) {
        updateVm(form.value).then(response => {
          proxy.$modal.msgSuccess("修改成功");
          open.value = false;
          getList();
        });
      } else {
        addVm(form.value).then(response => {
          proxy.$modal.msgSuccess("新增成功");
          open.value = false;
          getList();
        });
      }
    }
  });
}

/** 删除按钮操作 */
function handleDelete(row) {
  const _ids = row.id || ids.value;
  proxy.$modal.confirm('是否确认删除设备管理编号为"' + _ids + '"的数据项?').then(function() {
    return delVm(_ids);
  }).then(() => {
    getList();
    proxy.$modal.msgSuccess("删除成功");
  }).catch(() => {});
}

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

/* 查询设备类型列表 */
const vmTypeList=ref([]);
function getVmTypeList() {
  listVmType(loadAllParams).then(response => {
    vmTypeList.value = response.rows;
  });
}

/* 查询合作商列表 */
const partnerList=ref([]);
function getPartnerList() {
  listPartner(loadAllParams).then(response => {
    partnerList.value = response.rows;
  });
}

/* 查询点位列表 */
const nodeList=ref([]);
function getNodeList() {
  listNode(loadAllParams).then(response => {
    nodeList.value = response.rows;
  });
}

/* 查询区域列表 */
const regionList=ref([]);
function getRegionList() {
  listRegion(loadAllParams).then(response => {
    regionList.value = response.rows;
  });
}

getRegionList();
getNodeList();
getPartnerList();
getVmTypeList();
getList();
</script>

点位查看详情

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

<el-button link type="primary" @click="getNodeInfo(scope.row)" v-hasPermi="['manage:vm:list']">查看详情</el-button>


<!-- 点位详情对话框 -->
<el-dialog title="点位详情" v-model="nodeOpen" width="600px" append-to-body>
  <el-table :data="vmList">
    <el-table-column label="序号" type="index" width="80" align="center" prop="id" />
    <el-table-column label="设备编号" align="center" prop="innerCode" />
    <el-table-column label="设备状态" align="center" prop="vmStatus">
      <template #default="scope">
        <dict-tag :options="vm_status" :value="scope.row.vmStatus" />
      </template>

    </el-table-column>

    <el-table-column label="最后一次供货时间" align="center" prop="lastSupplyTime" width="180">
      <template #default="scope">
        <span>{{ parseTime(scope.row.lastSupplyTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>

      </template>

    </el-table-column>

  </el-table>

</el-dialog>



<script setup name="Node">
import { listVm } from "@/api/manage/vm";
/* 引入设备状态数据字典 */
const { vm_status } = proxy.useDict('vm_status');
    
/* 查看详情 */
const nodeOpen = ref(false);
const vmList = ref([]);
function getNodeInfo(row) {
  // 根据点位id,查询设备列表
  loadAllParams.nodeId = row.id;
  listVm(loadAllParams).then(response => {
    vmList.value = response.rows;
    nodeOpen.value = true;
  });
}
</script>

策略管理

需求说明

业务场景: 管理员在系统中可以对每一台设备设置一个固定折扣,用于营销作用

  1. 策略管理主要涉及到二个功能模块,业务流程如下:

  1. 对于策略管理数据模型,下面是示意图:

关系字段: policy_id

生成代码

使用若依代码生成器,生成策略管理前后端基础代码,并导入到项目中:

  1. 创建目录菜单

  1. 配置代码生成信息

  1. 下载代码并导入项目

策略管理改造

查看详情: 点击查看详情,展示策略名称和该策略下的设备列表

  1. 在policy/index.vue视图组件中修改
<el-button link type="primary"  @click="getPolicyInfo(scope.row)" v-hasPermi="['manage:vm:list']">查看详情</el-button>

<!-- 策略详情对话框 -->
<el-dialog v-model="policyOpen" title="策略详情" width="500px">
  <el-form-item label="策略名称" prop="policyName">
    <el-input v-model="form.policyName" placeholder="请输入策略名称" disabled />
  </el-form-item>

  <label>包含设备:</label>

  <el-table :data="vmList">
    <el-table-column label="序号" type="index" width="80" align="center" prop="id" />
    <el-table-column label="点位地址" align="left" prop="addr" show-overflow-tooltip />
    <el-table-column label="设备编号" align="center" prop="innerCode" />
  </el-table>

</el-dialog>

<script setup name="Policy">
import { listVm } from "@/api/manage/vm";
import { loadAllParams } from "@/api/page";
    
/* 查看策略详情 */
const policyOpen = ref(false);
const vmList = ref([]);
function getPolicyInfo(row) {
  //1. 获取策略信息
  form.value = row;
  //2. 根据策略id,查询设备列表
  loadAllParams.policyId = row.policyId;
  listVm(loadAllParams).then(response => {
    vmList.value = response.rows;
    policyOpen.value = true;
  });
}
</script>

设备策略分配

在设备管理页面中点击策略,对设备设置一个固定折扣,用于营销作用

  1. 在vm/index.vue视图组件中修改
<el-button link type="primary" @click="handleUpdatePolicy(scope.row)" v-hasPermi="['manage:vm:edit']">策略</el-button>

<!-- 策略管理对话框 -->
<el-dialog title="策略管理" v-model="policyOpen" width="500px" append-to-body>
  <el-form ref="vmRef" :model="form" label-width="80px">
    <el-form-item label="策略" prop="policyId">
      <el-select v-model="form.policyId" placeholder="请选择策略">
        <el-option v-for="item in policyList" :key="item.policyId" :label="item.policyName"
          :value="item.policyId"></el-option>

      </el-select>

    </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="Vm">
import { listPolicy } from '@/api/manage/policy';
    
// 取消按钮
function cancel() {
  open.value = false;
  policyOpen.value=false; // 关闭策略对话框
  reset();
}
    
/** 提交按钮 */
function submitForm() {
  proxy.$refs["vmRef"].validate(valid => {
    if (valid) {
      if (form.value.id != null) {
        updateVm(form.value).then(response => {
          proxy.$modal.msgSuccess("修改成功");
          open.value = false;
           
          getList();
        });
      } else {
        addVm(form.value).then(response => {
          proxy.$modal.msgSuccess("新增成功");
          open.value = false;
          getList();
        });
      }
    }
  });
}
    
/* 设备策略分配 */
const policyList = ref([]);
const policyOpen = ref(false);
function handleUpdatePolicy(row) {
  //1. 为表单赋值设备id和策略id
  form.value.id = row.id;
  form.value.policyId = row.policyId;
  //2. 查询策略列表
  listPolicy(loadAllParams).then((response) => {
    policyList.value = response.rows;
    policyOpen.value = true;
  });
}
</script>
  1. 在VendingMachineServiceImpl中修改
/**
 * 修改设备管理
 *
 * @param vendingMachine 设备管理
 * @return 结果
 */
@Override
public int updateVendingMachine(VendingMachine vendingMachine)
{
    if (vendingMachine.getNodeId()!=null) {
        // 查询点位表,补充:区域、点位、合作商等信息
        Node node = nodeService.selectNodeById(vendingMachine.getNodeId());
        BeanUtil.copyProperties(node,vendingMachine,"id");// 商圈类型、区域、合作商
        vendingMachine.setAddr(node.getAddress());// 设备地址
    }
    vendingMachine.setUpdateTime(DateUtils.getNowDate());// 更新时间
    return vendingMachineMapper.updateVendingMachine(vendingMachine);
}

商品管理

前置工作

业务场景: 智能售货机的货道管理、商品类型以及具体商品信息的管理

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

  1. 对于商品管理数据模型,下面是示意图:

关系字段: class_id、sku_id、vm_id

生成代码

使用若依代码生成器,生成商品管理前后端基础代码,并导入到项目中:

  1. 创建目录菜单

  1. 配置代码生成信息

  1. 下载代码并导入项目

商品类型改造

参考页面原型,完成基础布局展示改造

  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>
  1. 修改全局异常处理器,添加以下内容

表中对class_name字段进行了唯一性约束,保证该值得唯一性

但这个提示不精确,要进行修改

修改全局异常处理器

/**
 * 数据完整性异常
 */
@ExceptionHandler(DataIntegrityViolationException.class)
public AjaxResult handelDataIntegrityViolationException(DataIntegrityViolationException e) {

    if (e.getMessage().contains("foreign")) {

        return AjaxResult.error("无法删除,有其他数据引用");
    }
    if(e.getMessage().contains("Duplicate")){
        return AjaxResult.error("无法保存,名称已存在");
    }
    return AjaxResult.error("数据完整性异常,请联系管理员");
}

商品管理改造

参考页面原型,完成基础布局展示改造

  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.classI">
        <span v-if="item.classId == scope.row.classId">{{ item.className }}</span>

      </div>

    </template>

  </el-table-column>

  <el-table-column label="创建时间" align="center" prop="createTime" width="180">
    <template #default="scope">
      <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>

    </template>

  </el-table-column>

  <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
    <template #default="scope">
      <el-button link type="primary" @click="handleUpdate(scope.row)"
        v-hasPermi="['manage:sku:edit']">修改</el-button>

      <el-button link type="primary" @click="handleDelete(scope.row)"
        v-hasPermi="['manage:sku:remove']">删除</el-button>

    </template>

  </el-table-column>

</el-table>

<!-- 添加或修改商品管理对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
  <el-form ref="skuRef" :model="form" :rules="rules" label-width="80px">
    <el-form-item label="商品名称" prop="skuName">
      <el-input v-model="form.skuName" placeholder="请输入商品名称" />
    </el-form-item>

    <el-form-item label="品牌" prop="brandName">
      <el-input v-model="form.brandName" placeholder="请输入品牌" />
    </el-form-item>

    <el-form-item label="商品价格" prop="price">
      <el-input-number :min="0.01" :max="999.99" :precision="2" :step="0.5"   v-model="form.price" placeholder="请输入" />元
    </el-form-item>

    <el-form-item label="商品类型" prop="classId">
      <!-- <el-input v-model="form.classId" placeholder="请输入商品类型Id" /> -->
      <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>

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

  1. 物理外键约束和逻辑外键约束
  • 物理外键约束:通过在子表中添加一个外键列和约束,该列与父表的主键列相关联,由数据库维护数据的一致性和完整性
  • 逻辑外键约束:在不使用数据库外键约束的情况下,通常在应用程序中通过代码来检查和维护数据的一致性和完整性
  • 使用逻辑外键约束的原因:我们在新增售货机货道记录时暂不指定商品,货道表中的SKU_ID有默认值0,而这个值在商品表中并不存在,那么物理外键约束会阻止货道表的插入,因为0并不指向任何有效的商品记录

  1. 代码修改
package com.dkd.manage.service.impl;

/**
 * 商品管理Service业务层处理
 * 
 * @author itheima
 * @date 2026-02-22
 */
@Service
public class SkuServiceImpl implements ISkuService 
{
    @Autowired
    private SkuMapper skuMapper;

    @Autowired
    private IChannelService channelService;


    /**
     * 批量删除商品管理
     * 
     * @param skuIds 需要删除的商品管理主键
     * @return 结果
     */
    @Override
    public int deleteSkuBySkuIds(Long[] skuIds)
    {
        //1. 判断商品的id集合是否有关联货道
        int count = channelService.countChannelBySkuIds(skuIds);
        if(count>0){
            throw new ServiceException("此商品被货道关联,无法删除");
        }
        //2. 没有关联货道才能删除
        return skuMapper.deleteSkuBySkuIds(skuIds);
    }

}
package com.dkd.manage.service;

import java.util.List;
import com.dkd.manage.domain.Channel;

/**
 * 售货机货道Service接口
 * 
 * @author itheima
 * @date 2026-02-21
 */
public interface IChannelService 
{
    /**
     * 根据商品id集合统计货道数量
     *
     * @param skuIds skuId集合
     * @return 售货机货道集合
     */
    int countChannelBySkuIds(Long[] skuIds);
}
package com.dkd.manage.service.impl;

/**
 * 售货机货道Service业务层处理
 * 
 * @author itheima
 * @date 2026-02-21
 */
@Service
public class ChannelServiceImpl implements IChannelService 
{
    @Autowired
    private ChannelMapper channelMapper;

    /**
     * 根据商品id集合统计货道数量
     *
     * @param skuIds skuId集合
     * @return 售货机货道集合
     */
    @Override
    public int countChannelBySkuIds(Long[] skuIds) {
        return channelMapper.countChannelBySkuIds(skuIds);
    }
}
package com.dkd.manage.mapper;

import java.util.List;
import com.dkd.manage.domain.Channel;

/**
 * 售货机货道Mapper接口
 * 
 * @author itheima
 * @date 2026-02-21
 */
public interface ChannelMapper 
{
    /**
     * 根据商品id集合统计货道数量
     *
     * @param skuIds skuId集合
     * @return 售货机货道集合
     */
    int countChannelBySkuIds(Long[] skuIds);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dkd.manage.mapper.ChannelMapper">
    
    <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>

</mapper>

批量导入

点击导入数据弹出导入数据弹窗,实现商品的批量导入

前端

  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="excelOpen" 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 excelOpen = ref(false);
function handleExcelImport() {
  excelOpen.value = true;
}

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

/* 上传excel */
const uploadRef = ref({});
function submitUpload() {
  uploadRef.value.submit()
}
    
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 handleUploadError() {
  proxy.$modal.msgError("上传excel失败");
  uploadRef.value.clearFiles();
  proxy.$modal.closeLoading();
}

// 上传成功回调
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();
  proxy.$modal.closeLoading();
}    
</script>

后端

package com.dkd.manage.controller;

/**
 * 商品管理Controller
 * 
 * @author itheima
 * @date 2026-02-22
 */
@RestController
@RequestMapping("/manage/sku")
public class SkuController extends BaseController
{
    @Autowired
    private ISkuService skuService;

    /**
     * 导入商品管理列表
     */
    @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));
    }
}
package com.dkd.manage.service;

import java.util.List;
import com.dkd.manage.domain.Sku;

/**
 * 商品管理Service接口
 * 
 * @author itheima
 * @date 2026-02-22
 */
public interface ISkuService 
{

    /**
     * 批量新增商品管理
     * @param skuList
     * @return 结果
     */
    int insertSkus(List<Sku> skuList);
}
package com.dkd.manage.service.impl;

/**
 * 商品管理Service业务层处理
 * 
 * @author itheima
 * @date 2026-02-22
 */
@Service
public class SkuServiceImpl implements ISkuService 
{
    @Autowired
    private SkuMapper skuMapper;

    @Autowired
    private IChannelService channelService;


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

import java.util.List;
import com.dkd.manage.domain.Sku;

/**
 * 商品管理Mapper接口
 * 
 * @author itheima
 * @date 2026-02-22
 */
public interface SkuMapper 
{
    
    /**
     * 批量新增商品管理
     *
     * @param skuList 商品管理集合
     * @return 结果
     */
    int insertSkus(List<Sku> skuList);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dkd.manage.mapper.SkuMapper">

    <insert id="insertSkus">
        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>

</mapper>

先导出文件,再修改商品名称(有唯一性约束),再测试导入

EasyExcel

  1. 官方地址:https://easyexcel.alibaba.com/
  2. 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模式,在上层做了模型转换的封装,让使用者更加简单方便
  3. 阿里巴巴开源的框架,它以使用简单、功能强大和节省内存而著称,特别适合于需要进行大量数据导入和导出的场景
  4. 若依集成easyexcel实现excel表格增强,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
  5. easyexcel仅能处理excel文件,Apache poi可以处理多种文件
  6. dkd-common\pom.xml模块添加整合依赖
<!--  excel处理工具-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>4.0.1</version>
</dependency>
  1. dkd-common\模块的ExcelUtil.java新增easyexcel导出导入方法
package com.dkd.common.utils.poi;

/**
 * Excel相关处理
 * 
 * @author ruoyi
 */
public class ExcelUtil<T>
{
    
    /**
     * 对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注解
package com.dkd.manage.domain;

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 com.dkd.common.annotation.Excel;
import com.dkd.common.core.domain.BaseEntity;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

/**
 * 商品管理对象 tb_sku
 *
 * @author itheima
 * @date 2024-07-15
 */
@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. 从资料中复制货道api请求js文件到api/manage目录下

  1. 从资料中复制货道的视图组件到views/manage/vm目录下

  1. 修改设备index.vue
<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 -->

<script setup name="Vm">
    // ********************货道********************
    // 货道组件
    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********************
</script>

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

查询货道列表

根据售货机编号查询货道列表

package com.dkd.manage.domain.vo;

import com.dkd.manage.domain.Channel;
import com.dkd.manage.domain.Sku;
import lombok.Data;

@Data
public class ChannelVo extends Channel {
    // 商品
    private Sku sku;
}
package com.dkd.manage.controller;

/**
 * 售货机货道Controller
 * 
 * @author itheima
 * @date 2026-02-21
 */
@RestController
@RequestMapping("/manage/channel")
public class ChannelController extends BaseController
{
    @Autowired
    private IChannelService channelService;

    /**
     * 根据售货机编号查询货道列表
     */
    @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);
    }
}
package com.dkd.manage.service;

import java.util.List;
import com.dkd.manage.domain.Channel;
import com.dkd.manage.domain.vo.ChannelVo;

/**
 * 售货机货道Service接口
 * 
 * @author itheima
 * @date 2026-02-21
 */
public interface IChannelService 
{

    /**
     * 根据售货机编号查询货道列表
     *
     * @param innerCode
     * @return ChannelVo集合
     */
    List<ChannelVo> selectChannelVoListByInnerCode(String innerCode);
}
package com.dkd.manage.service.impl;


/**
 * 售货机货道Service业务层处理
 * 
 * @author itheima
 * @date 2026-02-21
 */
@Service
public class ChannelServiceImpl implements IChannelService 
{
    @Autowired
    private ChannelMapper channelMapper;


    /**
     * 根据售货机编号查询货道列表
     *
     * @param innerCode
     * @return ChannelVo集合
     */
    @Override
    public List<ChannelVo> selectChannelVoListByInnerCode(String innerCode) {
        return channelMapper.selectChannelVoListByInnerCode(innerCode);
    }
}
package com.dkd.manage.mapper;

import java.util.List;
import com.dkd.manage.domain.Channel;
import com.dkd.manage.domain.vo.ChannelVo;

/**
 * 售货机货道Mapper接口
 * 
 * @author itheima
 * @date 2026-02-21
 */
public interface ChannelMapper 
{

    /**
     * 根据售货机编号查询货道列表
     *
     * @param innerCode
     * @return ChannelVo集合
     */
    List<ChannelVo> selectChannelVoListByInnerCode(String innerCode);
}
<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"    />
    <association property="sku" javaType="Sku" column="sku_id" select="com.dkd.manage.mapper.SkuMapper.selectSkuBySkuId"/>
</resultMap>

<select id="selectChannelVoListByInnerCode" resultMap="ChannelVoResult">
    <include refid="selectChannelVo"/>
    where inner_code = #{innerCode}
</select>

货道关联商品

完成货道配置,实现货道关联商品

package com.dkd.manage.domain.dto;

import lombok.Data;

// 某个货道对应的sku信息
@Data
public class ChannelSkuDto {
    private String innerCode;
    private String channelCode;
    private Long skuId;
}
package com.dkd.manage.domain.dto;

import lombok.Data;

import java.util.List;

// 售货机货道配置
@Data
public class ChannelConfigDto {
    // 售货机编号
    private String innerCode;
    // 货道配置
    private List<ChannelSkuDto> channelList;
}
package com.dkd.manage.mapper;

/**
 * 售货机货道Mapper接口
 * 
 * @author itheima
 * @date 2026-02-21
 */
public interface ChannelMapper 
{
    /**
     * 根据售货机编号和货道编号查询货道信息
     * @param innerCode
     * @param channelCode
     * @return 售货机货道
     */
    @Select("select * from tb_channel where inner_code =#{innerCode} and channel_code=#{channelCode}")
    Channel getChannelInfo(@Param("innerCode") String innerCode, @Param("channelCode") String channelCode);

    /**
     * 批量修改货道
     * @param list
     * @return 结果
     */
    int batchUpdateChannel(List<Channel> list);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dkd.manage.mapper.ChannelMapper">

    <update id="batchUpdateChannel" parameterType="java.util.List">
        <foreach item="channel" collection="list" 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>
</mapper>
/**
 * 货道关联商品
 * @param channelConfigDto
 * @return 结果
 */
int setChannel(ChannelConfigDto channelConfigDto);
/**
 * 货道关联商品
 * @param channelConfigDto
 * @return 结果
 */
@Override
public int setChannel(ChannelConfigDto channelConfigDto) {
    //1. dto转po
    List<Channel> list = channelConfigDto.getChannelList().stream().map(c -> {
        // 根据售货机编号和货道编号查询货道
        Channel channel = channelMapper.getChannelInfo(c.getInnerCode(), c.getChannelCode());
        if (channel != null) {
            // 货道更新skuId
            channel.setSkuId(c.getSkuId());
            // 货道更新时间
            channel.setUpdateTime(DateUtils.getNowDate());
        }
        return channel;
    }).collect(Collectors.toList());
    //2. 批量修改货道
    return channelMapper.batchUpdateChannel(list);
}
/**
 * 货道关联商品
 */
@PreAuthorize("@ss.hasPermi('manage:channel:edit')")
@Log(title = "售货机货道", businessType = BusinessType.UPDATE)
@PutMapping("/config")
public AjaxResult setChannel(@RequestBody ChannelConfigDto channelConfigDto) {
    return toAjax(channelService.setChannel(channelConfigDto));
}

修改application-druid.yml

  1. 允许mybatis框架在单个请求中发送多个sql语句

  1. 说明
  • 单个请求包含单个语句: insert into value () ();
  • 单个请求包含多个语句: update xxx; update xxx;

测试一下

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐