帝可得管理系统发

项目介绍

售货机简介

帝可得是一个基于物联网概念下的智能售货机运营管理系统

物联网

物联网(IoT:Internet of Things)简单来说,就是让各种物品通过互联网连接起来,实现信息的交换和通信。

这个概念听起来可能有点抽象,但我们可以把它想象成一个超级大的社交网络。不过,这个网络里的成员不是人类,而是各种物品。比如,你的冰箱、洗衣机、甚至是你的汽车,它们都可以通过互联网互相交流信息,就像是它们自己在聊天一样。

物联网的神奇之处在于,它能够让这些物品变得更加“聪明”。它们能够感知周围的环境,并且能够根据情况自动做出反应。举个例子,如果你的家里安装了智能家居系统,当你下班回家的路上,家里的空调就能提前开启,自动调整到你喜欢的温度,让你一回到家就能感受到舒适的环境。

总的来说将现实物体赋予感知、通信和智能化的能力,为人们提供更加智能化和便利的生活和工作环境

应用场景:智能家居、共享充电中、智能售货机

售货机

近年来,无接触式智能售货机逐渐成为年轻消费者的首选购物方式。这些设备不仅在公园、地铁站和商场等公共场所广泛分布,为顾客提供了极大的购物便利,而且由于其运营成本低廉和较高的利润空间,被众多电商企业视为新兴的蓝海市场。

与传统售货机相比,智能售货机的优势在于其自我管理能力,表现如下:

  • 物联网技术:像是售货机的顺风耳和千里眼。不管售货机在哪,管理人员都能通过电脑或手机知道它的状态,比如哪些商品快卖完了,哪些需要补货。

  • 智能分析与推荐:智能售货机就像有一个隐形的聪明脑袋,它能分析顾客的喜好,推荐那些可能让你心动的商品。就像有一个贴心的小助手,总能帮你找到最想要的东西。

  • 人员设备绑定管理:每个售货机都有自己的专属“保镖”,一旦售货机出现问题,这些“保镖”就能立刻出现,迅速解决问题,让售货机始终保持最佳状态。

  • 移动支付支持:现在谁还带现金出门呢?智能售货机支持各种移动支付方式,一扫即付,既方便又安全,让购物体验更加流畅。

  • 线上线下融合(OMO):智能售货机还能把线上和线下完美结合起来,顾客可以在线上浏览商品,然后直接在售货机上购买,或者在售货机上看到喜欢的商品后,直接在线上下单。

智能售货机,不仅仅是一个自动售货的机器,它更像是一个智能的、能够自我管理和自我优化的系统。它让运营更高效,让购物体验更个性化,同时也为零售行业带来了创新和发展。

售货机术语

区域管理: 为了更高效地进行经营管理,公司将运营范围划分为若干个逻辑区域。这些区域的划分基于业务需求,可能与地理上的行政区域有所区别,以确保更合理的资源分配和更高效的运营管理。

点位选择: 点位指的是智能售货机的具体放置位置。选择点位时,我们会考虑人流量、目标顾客群体、可见度以及便利性等因素,以最大化售货机的使用效率和顾客的购买体验。 售货机功能: 智能售货机就像是一个自动的小店,里面摆满了各种商品。顾客想要什么,直接在机器上选,然后机器就会把商品送到他们手中,就像是一个自动化的仓库。  货道设计:售货机里面的货道,你可以想象成超市里的那种货架。每一层都有好几个位置可以放商品,这样就能放很多种不同的商品,而且每一层都能放很多,这样顾客的选择就会更多。

角色与功能

一个完整的售货机系统由五端五角色组成:

  1. 管理员:对基础数据(区域、点位、设备、货道、商品等)进行管理,创建工单指派运维或运营人员,查看订单,查看各种统计报表。

  2. 运维人员:投放设备、撤除设备、维修设备。

  3. 运营人员:补货。

  4. 合作商:仅提供点位,坐收渔翁之利。

  5. 消费者: 在小程序或屏幕端下单购买商品。

    业务流程

    整个工程中,课程里会对主要核心的业务进行实现,主要包含下面的业务流程:

    (1)平台管理员:主要作用有基础数据的管理和创建工单排除员工完成维修或补货。

    (2)运营人员:主要作用是处理运营工单业务(补货等操作)

    (3)运维人员:主要作用是处理运维工单业务(设备维修等操作)

    (4)消费者:供C端用户使用。消费者扫描售货机上的二维码可以打开此端。主要作用是完成在售货机的购物操作。

平台管理员

上图中的简要流程:

①:平台管理人员登录到系统管理后台系统

②:创建区域数据

③:创建区域下点位数据

④:添加运维/运营人员

⑤:创建售货机信息

⑥:设置售货机点位信息

⑦:创建运维投放工单,由运维人员开始投放设备(安装设备)

⑧:设置售卖的商品信息

⑨:创建运营补货工单,由运营人员开始投放商品信息

运维人员

上图中的简要流程:

①:运维人员通过App登录运营系统

②:在App对派送过来的工单进行处理

③:接受工单后在指定的投放点安装售货机

④:拒绝工单该运维人员的工单结束

运营人员

上图中的简要流程:

①:运营人员通过App登录运营系统

②:在App对派送过来的工单进行处理

③:接受工单后在指定的售货机的商品进行补货

④:拒绝工单该运维人员的工单结束

消费者

上图中的简要流程:

方式一:

①:用户通过售货机二维码进行购买商品

②:扫码后在手机端微信小程序选择商品

③:支付成功后在售货机取货

方式二:

①:用户在售货机上选择商品

②:在选择商品后扫码支付商品的二维码

③:支付成功后在售货机取货

产品原型

帝可得项目点击链接立即查看 腾讯 CoDesign - 腾讯 CoDesign

库表设计

系统后台基础数据表关系说明:

一个区域可以有多个点位

一个点位可以有多个售货机

一个售货机有多个货道

多个货道可以放置同一样商品

一个商品类型下有多个商品

一个售货机类型下有多个售货机

一个合作商有多个点位

合作商和区域之间没有关系,因为合作商拥有的多个点位可以分布在不同的区域

每个区域下有多个运维和运营人员,他们来负责这个区域下的设备的运维和运营

初始AI

AIGC

AI:即人工智能,是计算机科学体系下的一个学科,是指通过计算机系统模拟人类智力的一种技术。

简单地说,AI是一种模拟人类智能的技术,它通过机器学习、深度学习等算法,使计算机具备对数据分析、理解、推理和决策的能力。

我们可以把“人工智能”想象成一个聪明的机器人小伙伴,它不仅会自动学习,还能像人的大脑一样思考和决策。在常生活中,它可以帮助我们解决诸多问题,例如在出门前预测当天的天气状况、通过分析交通路况推荐汽车的最佳行驶路线等。

AIGC(AI Generated Content):AIGC是AI领域的一个应用分支,专注于利用AI技术自动生成内容,包括文本,代码,图片,音频,视频。

 AI大模型:通常指的是具有大量参数的深度学习模型,经过大量数据训练,具备复杂计算能力的人工智能系统,它们能够执行多种高级任务,其中包括内容生成。 常见的通用大模型产品:

国家对话产品大模型链接
美国OpenAI ChatGPTGPT-3.5、GPT-4https://chat.openai.com/
美国Microsoft CopilotGPT-4 和未知搜索 Microsoft Copilot: 你的日常 AI 助手
美国Google BardGeminihttps://bard.google.com/
中国百度文心一言文心4.0文心一言
中国讯飞星火星火3.5讯飞星火大模型-AI大语言模型-星火大模型-科大讯飞
中国智谱清言GLM-4智谱清言
中国月之暗面 Kimi ChatMoonshotKimi.ai - 帮你看更大的世界
中国MiniMax星野abab6星野_沉浸式智能体社区_星野官网_星野app
中国通义干问Qwen-Max通义官网

提示工程

什么是Prompt?

提示(Prompt)是我们对大模型提出的问题。

举一个最简单的例子,很多同学在第一次使用AI时,都会问AI"你是谁","你是谁"这个问题便是prompt。

为什么学?

在与AI交流时,我们常常发现直接提问可能得不到满意的答案。

但如果我们换一种方式提问,或者提供一些额外的上下文信息,AI的表现就会大大提升。

同一个问题使用不同的prompt可能会获得不同的答案,怎么调教出一个聪明的AI助手呢?

这时候,提示工程就派上用场了。通过精心设计的提示,我们可以引导AI模型,让它的输出更加准确、相关和有用。

什么是提示工程?

提示工程(Prompt Engineering)也被称为上下文提示,它涉及到设计和优化输入文本,也就是Prompt,来引导AI模型生成预期的输出。

简单来说,就像是给AI出一个好问题,让它给我们一个满意的答案。

常见的编程相关的Prompt        

表结构

你是一个软件工程师,帮我生成MySQL的表结构
需求如下:
    1,课程管理表,表名tb_course,字段有主键id、课程编码、课程学科、课程名称、课程价格、适用人群、课程介绍
其他要求:
    1,每个表中都有创建时间(create_time)、修改时间(date_time)、创建人(create_by)、修改人(update_by)、备注(remark)这些字段
    2,每个表的主键都是自增的
    3,课程价格是整型、课程编码是字符串
    4,请为每个字段都添加上comment
    5,帮我给生成的表中插入一些IT课程示例数据
        课程学科:Java、人工智能、大数据
        适用人群:小白学员、中级程序员 

生成数据库说明文档

你是一个软件工程师,现在要根据数据库的sql脚本,编写数据库说明文档,sql脚本如下:
CREATE TABLE `tb_course` (
    `id` INT AUTO_INCREMENT COMMENT '主键ID',
    `course_code` VARCHAR(255) NOT NULL COMMENT '课程编码',
    `course_subject` VARCHAR(100) NOT NULL COMMENT '课程学科',
    `course_name` VARCHAR(255) NOT NULL COMMENT '课程名称',
    `course_price` INT COMMENT '课程价格',
    `target_audience` VARCHAR(100) COMMENT '适用人群',
    `course_introduction` TEXT COMMENT '课程介绍',
    `create_time` DATETIME COMMENT '创建时间',
    `update_time` DATETIME COMMENT '修改时间',
    `create_by` VARCHAR(64) COMMENT '创建人',
    `update_by` VARCHAR(64) COMMENT '修改人',
    `remark` VARCHAR(255) COMMENT '备注',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程管理表';

输出要求是:
    1,每个表以及每个表的字段都要详细说明,包括,字段名称、类型、作用
    2,使用markdown的输出格式,字段的描述需要使用表格展示
    3,如果表之间有关系,需要描述清楚表之间的关系

生成代码

代码生成算是比较常规的方案,用的也比较多,分为了几种情况

  • 给出表生成代码(项目中常见)

    • 给出表结构的ddl,可以输出这个表的增删改查的所有代码

    • 给出表结构的dll,可以输出增删改查的接口文档

  • 补全代码

    • 例1-给出实体类,帮助编写getter、setter、toString、构造方法等等

    • 例2-给出一个controller,帮助编写swagger注解等

  • 提取结构(无含金量,费时间的编程)

    • 例1-根据接口文档提取dto类或者vo类

你是一个软件工程师,为了方便理解代码执行流程,需要给出代码执行的流程图,代码如下:
    // 创建工单
    @Transactional
    @Override
    public int insertTaskDto(TaskDto taskDto) {
    //1. 查询售货机是否存在
    VendingMachine vm = vendingMachineService.selectVendingMachineByInnerCode(taskDto.getInnerCode());
    if (vm == null) {
        throw new ServiceException("设备不存在");
    }
    //2. 校验售货机状态与工单类型是否相符
    checkCreateTask(vm.getVmStatus(), taskDto.getProductTypeId());
    //3. 校验这台设备是否有未完成的同类型工单,如果存在则不能创建
    hasTask(taskDto.getInnerCode(), taskDto.getProductTypeId());
    //4. 校验员工是否存在
    Emp emp = empService.selectEmpById(taskDto.getUserId());
    if (emp == null) {
        throw new ServiceException("员工不存在");
    }
    // 5. 校验非同区域下的工作人员不能接受工单
    if (emp.getRegionId() != vm.getRegionId()) {
        throw new ServiceException("非同区域下的工作人员不能接受工单");
    }
    //6. 保存工单信息
    Task task = new Task();
    BeanUtil.copyProperties(taskDto, task);// 属性赋值
    task.setCreateTime(DateUtils.getNowDate());// 创建时间
    task.setTaskCode(generateTaskCode());// 工单编号 202405150001
    task.setTaskStatus(DkdContants.TASK_STATUS_CREATE);// 创建工单
    task.setAddr(vm.getAddr());
    task.setRegionId(vm.getRegionId());
    task.setUserName(emp.getUserName());
    int taskResult = taskMapper.insertTask(task);
    //7. 如果是补货工单,向工单明细表插入记录
    if (task.getProductTypeId() == DkdContants.TASK_TYPE_SUPPLY) {
        if (CollUtil.isEmpty(taskDto.getDetails())) {
            throw new ServiceException("补货工单明细不能为空");
        }
        List<TaskDetails> taskDetailsList = taskDto.getDetails().stream().map(details -> {
            TaskDetails taskDetails = BeanUtil.copyProperties(details, TaskDetails.class);
            taskDetails.setTaskId(task.getTaskId());
            return taskDetails;
        }).collect(Collectors.toList());
        taskDetailsService.insertBatch(taskDetailsList);
    }

    return taskResult;
}

输出:请使用mermaid语言来描述这个方法的流程

项目搭建

搭建后端项目

初始化项目

Git下载

通过idea克隆源码,仓库地址:dkd-parent: 帝可得后台管理系统

Maven构建

使用idea打开项目后,等待环境检查(主要是Maven下载项目依赖)

         

MySQL相关

导入sql

1、创建数据库create schema dkd;

2、执行下图的sql脚本文件,完成导入

配置信息

dkd-admin模块下,编辑resources目录下的application-druid.yml,修改数据库连接

Redis相关

启动

在redis解压目录下,编辑redis.windows.conf配置文件,设置redis密码

设置Redis密码是为了增强数据安全性、防止未授权访问和保护关键信息,从而确保应用程序的稳定性和合规性。

在redis解压目录下,执行redis-server.exe redis.windows.conf启动  

配置信息

dkd-admin模块下,resources目录下的application.yml,设置redis密码等相关信息

项目运行

dkd-admin模块下,运行com.ruoyi.DkdApplication.java,出现如下图表示启动成功

后端运行成功可以通过([http://localhost:8080 )访问,但是不会出现静态页面,可以继续参考下面步骤部署前端,然后通过前端地址来访问。

搭建前端项目

初始化项目

通过vscode克隆源码,仓库地址:dkd-vue: 帝可得前端

 # 安装依赖
npm install

项目运行

# 启动服务
npm run dev

打开浏览器,输入:([http://localhost:80) 默认账户/密码 admin/admin123)若能正确展示登录页面,并能成功登录,菜单及页面展示正常,表明环境搭建成功  

点位管理

需求说明

业务场景: 假设我们的公司现在有一个宏伟的计划——在北京发展业务。首先,我们需要确定几个有潜力的区域,这些区域可能是人流量大、消费能力高的商业区或居民区。然后,我们要与这些区域内的潜在合作商进行洽谈,比如商场、写字楼、学校等地方的管理者或所有者。

一旦我们与合作商达成协议,确定了合作的细节和点位,我们就可以安排工作人员去投放智能售货机了。这些点位将成为我们智能售货机的“家”,为消费者提供便捷的购买服务。

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

  1. 登录系统:后台管理人员登录后台系统

  2. 新增区域: 后台管理人员可以添加区域范围,区域范围与运维/运维人员挂钩,区域下可关联点位。

  3. 新增合作商: 管理人员可以添加合作商,合作商与点位进行关联。

  4. 新增区域点位: 后台管理人员可以在特定区域内新增点位,这些点位是放置智能售货机的具体位置。库表设计

你是一位软件工程师,帮我生成MySQL的表结构
需求如下:
1,区域表,表名tb_region,字段有主键id、区域名称
2,合作商表,表名tb_partner,字段有主键id、合作商名称、联系人、联系电话、分成比例(int类型)、账号、密码
3,点位表,表名tb_node,字段有主键id、点位名称、详细地址、商圈类型(int类型)
其他要求:
1,每张表中都有创建时间(create_time)、修改时间(date_time)、创建人(create_by)、修改人(update_by)、备注(remark)这些字段
2,每张表的主键都是自增的
3,区域与点位是一对多的关系,合作商与点位是一对多的关系,请用字段表示出来,并建立外键约束
4,请为所有字段都添加上comment
5,帮我给生成的表中插入一些北京城市相关区域、点位、合作商的测试数据 

CREATE TABLE `tb_region` (
  `id` INT AUTO_INCREMENT COMMENT '主键id' PRIMARY KEY,
  `region_name` VARCHAR(255) NOT NULL COMMENT '区域名称',
  `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `create_by` VARCHAR(64) COMMENT '创建人',
  `update_by` VARCHAR(64) COMMENT '修改人',
  `remark` TEXT COMMENT '备注'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='区域表';

-- 插入测试数据
INSERT INTO `tb_region` (`region_name`,`remark`) VALUES ('北京市朝阳区','北京市朝阳区'), ('北京市海淀区','北京市海淀区'), ('北京市东城区','北京市东城区');

CREATE TABLE `tb_partner` (
  `id` INT AUTO_INCREMENT COMMENT '主键id' PRIMARY KEY,
  `partner_name` VARCHAR(255) NOT NULL COMMENT '合作商名称',
  `contact_person` VARCHAR(64) COMMENT '联系人',
  `contact_phone` VARCHAR(15) COMMENT '联系电话',
  `profit_ratio` INT COMMENT '分成比例',
  `account` VARCHAR(64) COMMENT '账号',
  `password` VARCHAR(64) COMMENT '密码',
  `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `create_by` VARCHAR(64) COMMENT '创建人',
  `update_by` VARCHAR(64) COMMENT '修改人',
  `remark` TEXT COMMENT '备注'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合作商表';

-- 插入测试数据
INSERT INTO `tb_partner` (`partner_name`, `contact_person`, `contact_phone`, `profit_ratio`, `account`, `password`) VALUES
('合作商A', '张三', '13800138000', 30, 'a001', 'pwdA'),
('合作商B', '李四', '13912345678', 25, 'b002', 'pwdB');

CREATE TABLE `tb_node` (
  `id` INT AUTO_INCREMENT COMMENT '主键id' PRIMARY KEY,
  `node_name` VARCHAR(255) NOT NULL COMMENT '点位名称',
  `address` VARCHAR(255) NOT NULL COMMENT '详细地址',
  `business_type` INT COMMENT '商圈类型',
  `region_id` INT COMMENT '区域ID',
  `partner_id` INT COMMENT '合作商ID',
  `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `create_by` VARCHAR(64) COMMENT '创建人',
  `update_by` VARCHAR(64) COMMENT '修改人',
  `remark` TEXT COMMENT '备注',
  FOREIGN KEY (`region_id`) REFERENCES `tb_region`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
  FOREIGN KEY (`partner_id`) REFERENCES `tb_partner`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='点位表';


-- 插入测试数据
-- 假设区域ID为1对应'北京市朝阳区',合作商ID为1对应'合作商A'
INSERT INTO `tb_node` (`node_name`, `address`, `business_type`, `region_id`, `partner_id`) VALUES
('三里屯点位', '北京市朝阳区三里屯路', 1, 1, 1),
('五道口点位', '北京市海淀区五道口', 2, 2, 2);

对于点位管理数据模型,下面是示意图:

        

生成基础代码

需求

使用若依代码生成器,生成区域管理、合作商管理、点位管理前后端基础代码,并导入到项目中:

步骤

①创建目录菜单

创建点位管理目录菜单

②添加数据字典

先创建商圈的字典类型

 再创建商圈的字典数据

③配置代码生成信息

导入三张表配置合作商表(参考原型)配置区域表(参考原型)配置点位表(参考原型)注意:这里查询我们把合作商也选上了,方便后续用到,前端用不到把这个选项删除就行了

④下载代码并导入项目

选中三张表生成下载解压ruoyi.zip得到前后端代码和动态菜单sql

代码导入

调整二级菜单显示顺序

区域管理改造

基础页面

需求

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

代码实现

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

区域列表

需求

在区域列表查询中,需要显示每个区域的点位数

实现思路

实现此功能也有多种方案:

(1)同步存储:在区域表中有点位数的字段(node_count),当点位发生变化时,同步区域表中的点位数。
      优点:由于是单表查询操作,查询列表效率最高。
      缺点:需要在点位增删改时修改区域表中的数据,有额外的开销,数据也可能不一致。
(2)关联查询:**编写关联查询语句,在mapper 层封装。
     优点:实时查询,数据100%正确,不需要单独维护。
    缺点:SQL语句较复杂,如果数据量大,性能比较低。

 区域和点位表,记录个数都不是很多,所以我们采用关联查询这种方案。

SQL

SQL查询:先聚合统计每个区域的点位数,然后与区域表进行关联查询

RegionVo

根据接口文档和sql创建RegionVoRegionMapper

/**
 * 查询区域管理列表
 * @param region
 * @return RegionVo集合
 */
public List<RegionVo> selectRegionVoList(Region region);

RegionMapper.xml 

<select id="selectRegionVoList" resultType="com.dkd.manage.domain.vo.RegionVo">
select r.id,r.region_name,r.remark,ifnull(n.node_count,0) as node_count from tb_region r
    left join (select region_id,count(*) as node_count from tb_node group by region_id) n on r.id=n.region_id
    <where>
       <if test="regionName != null  and regionName != ''"> and r.region_name like concat('%', #{regionName}, '%')</if>
    </where>
</select> 

mybatis-config.xml IRegionService

/**
 * 查询区域管理列表
 * @param region
 * @return RegionVo集合
 */
public List<RegionVo> selectRegionVoList(Region region);

 RegionServiceImpl

/**
 * 查询区域管理列表
 * @param region
 * @return RegionVo集合
 */
@Override
public List<RegionVo> selectRegionVoList(Region region) {
    return regionMapper.selectRegionVoList(region);
}

RegionController

/**
 * 查询区域管理列表
 */
@PreAuthorize("@ss.hasPermi('manage:region:list')")
@GetMapping("/list")
public TableDataInfo list(Region region)
{
    startPage();
    List<RegionVo> voList = regionService.selectRegionVoList(region);
    return getDataTable(voList);

region/index.vue

<!-- 区域列表 -->
<el-table v-loading="loading" :data="regionList" @selection-change="handleSelectionChange">
  <el-table-column type="selection" width="55" align="center" />
  <el-table-column label="序号" type="index" width="50" align="center" prop="id" />
  <el-table-column label="区域名称" align="center" prop="regionName" />
  <el-table-column label="点位数" align="center" prop="nodeCount" />
  <el-table-column label="备注说明" align="center" prop="remark" />
  <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:region:edit']">修改</el-button>
      <el-button link type="primary"  @click="handleDelete(scope.row)" v-hasPermi="['manage:region:remove']">删除</el-button>
    </template>
  </el-table-column>
</el-table> 

合作商管理改造

基础页面

需求

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

代码实现

在partner/index.vue视图组件中

<!-- 搜索区域 -->
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="90px">
    <el-form-item label="合作商名称" prop="name">
        <el-input
                  v-model="queryParams.partnerName" 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="partnerList" @selection-change="handleSelectionChange">
  <el-table-column type="selection" width="55" align="center" />
  <el-table-column label="序号" type="index" width="50" align="center" prop="id" />
  <el-table-column label="合作商名称" align="center" prop="partnerName" />
  <el-table-column label="账号" align="center" prop="account" />
  <el-table-column label="分成比例" align="center" prop="profitRatio" >
    <template #default="scope">{{scope.row.profitRatio}}%</template>
  </el-table-column>
  <el-table-column label="联系人" align="center" prop="contactPerson" />
  <el-table-column label="联系电话" align="center" prop="contactPhone" />
  <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:partner:edit']">修改</el-button>
      <el-button link type="primary"  @click="handleDelete(scope.row)" v-hasPermi="['manage:partner:remove']">删除</el-button>
    </template>
  </el-table-column>
</el-table>

<!-- 添加或修改合作商管理对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
  <el-form ref="partnerRef" :model="form" :rules="rules" label-width="100px">
    <el-form-item label="合作商名称" prop="partnerName">
      <el-input v-model="form.partnerName" placeholder="请输入合作商名称" />
    </el-form-item>
    <el-form-item label="联系人" prop="contactPerson">
      <el-input v-model="form.contactPerson" placeholder="请输入联系人" />
    </el-form-item>
    <el-form-item label="联系电话" prop="contactPhone">
      <el-input v-model="form.contactPhone" placeholder="请输入联系电话" />
    </el-form-item>
    <el-form-item label="创建时间" prop="createTime" v-if="form.id!=null">
      {{form.createTime}}
    </el-form-item>
    <el-form-item label="分成比例" prop="profitRatio">
      <el-input v-model="form.profitRatio" placeholder="请输入分成比例" />
    </el-form-item>
    <el-form-item label="账号" prop="account" v-if="form.id==null">
      <el-input v-model="form.account" placeholder="请输入账号" />
    </el-form-item>
    <el-form-item label="密码" prop="password" v-if="form.id==null">
      <el-input v-model="form.password" type="password" 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>
</div>
</template>

在PartnerServiceImpl的新增方法中修改

/**
 * 新增合作商
 *
 * @param partner 合作商
 * @return 结果
 */
@Override
public int insertPartner(Partner partner) {
    // 使用SecurityUtil工具类,对密码加密
    partner.setPassword(SecurityUtils.encryptPassword(partner.getPassword()));
    partner.setCreateTime(DateUtils.getNowDate());
    return partnerMapper.insertPartner(partner);

查看详情

在partner/index.vue视图组件中

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


<!-- 查看合作商详情 -->
<el-dialog title="合作商详情" v-model="partnerInfoOpen" width="500px" append-to-body>
    <el-row>
        <el-col :span="12">合作商名称:{{ form.partnerName }}</el-col>
        <el-col :span="12">联系人:{{ form.contactPerson }}</el-col>
    </el-row>
    <el-row>
        <el-col :span="12">联系电话:{{ form.contactPhone }}</el-col>
        <el-col :span="12">分成比例:{{ form.profitRatio }}%</el-col>
    </el-row>
</el-dialog>

<script>
    /* 查看合作商详情 */
    const partnerInfoOpen = ref(false);
    function getPartnerInfo(row) {
        reset();
        const _id = row.id;
        getPartner(_id).then(response => {
            form.value = response.data;
            partnerInfoOpen.value = true;
        });
    }
</script>

<!-- 查看合作商详情对话框 -->
<el-dialog title="合作商详情" v-model="partnerInfoOpen" width="500px" append-to-body>
    <!-- 使用el-descriptions组件以卡片形式展示信息,更加整洁 -->
    <el-descriptions :column="2" border>
        <el-descriptions-item label="合作商名称">{{ form.partnerName }}</el-descriptions-item>
        <el-descriptions-item label="联系人">{{ form.contactPerson }}</el-descriptions-item>
        <el-descriptions-item label="联系电话">{{ form.contactPhone }}</el-descriptions-item>
        <el-descriptions-item label="分成比例">{{ form.profitRatio }}%</el-descriptions-item>
    </el-descriptions>
</el-dialog> 

合作商列表

实现思路

与区域列表实现方式相同。

SQL

SQL查询:先聚合统计每个合作商的点位数,然后与合作商表进行关联查询

-- 传统模式
-- 1.先聚合统计每个合作商的点位数
-- 确定查询的表 tb_node
-- 确定分组字段 partner_id
SELECT partner_id, COUNT(1) AS node_count from tb_node group by partner_id;
-- 2.然后与合作商表进行关联查询
select tp.*,ifnull(tn.node_count,0) from tb_partner tp
left join (SELECT partner_id, COUNT(1) AS node_count from tb_node group by partner_id) tn on tn.partner_id = tp.id;

-- AI辅助编程模式
-- 你是一个软件开发工程师,现在要根据数据库的sql脚本,查询并显示合作商表所有的字段信息,同时显示每个合作商的点位数,sql脚本如下
create table tb_node
(
    id            int auto_increment comment '主键id'
        primary key,
    node_name     varchar(255)                        not null comment '点位名称',
    address       varchar(255)                        not null comment '详细地址',
    business_type int                                 null comment '商圈类型',
    region_id     int                                 null comment '区域ID',
    partner_id    int                                 null comment '合作商ID',
    create_time   timestamp default CURRENT_TIMESTAMP null comment '创建时间',
    update_time   timestamp default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '修改时间',
    create_by     varchar(64)                         null comment '创建人',
    update_by     varchar(64)                         null comment '修改人',
    remark        text                                null comment '备注',
    constraint tb_node_ibfk_1
        foreign key (region_id) references tb_region (id)
            on update cascade on delete cascade,
    constraint tb_node_ibfk_2
        foreign key (partner_id) references tb_partner (id)
            on update cascade on delete cascade
)
    comment '点位表';
    
create table tb_partner
(
    id             int auto_increment comment '主键id'
        primary key,
    partner_name   varchar(255)                        not null comment '合作商名称',
    contact_person varchar(64)                         null comment '联系人',
    contact_phone  varchar(15)                         null comment '联系电话',
    profit_ratio   int                                 null comment '分成比例',
    account        varchar(64)                         null comment '账号',
    password       varchar(64)                         null comment '密码',
    create_time    timestamp default CURRENT_TIMESTAMP null comment '创建时间',
    update_time    timestamp default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '修改时间',
    create_by      varchar(64)                         null comment '创建人',
    update_by      varchar(64)                         null comment '修改人',
    remark         text                                null comment '备注'
)
    comment '合作商表'; 

PartnerVo

根据接口文档和sql创建PartnerVo

 PartnerMapper

/**
 * 查询合作商管理列表
 * @param partner
 * @return partnerVo集合
 */
public List<PartnerVo> selectPartnerVoList(Partner partner);

PartnerMapper.xml

<select id="selectPartnerVoList" resultType="com.dkd.manage.domain.vo.PartnerVo">
    SELECT p.*, COUNT(n.id) AS node_count FROM tb_partner p
    LEFT JOIN tb_node n ON p.id = n.partner_id
    <where>
        <if test="partnerName != null  and partnerName != ''">and partner_name like concat('%', #{partnerName},'%')
        </if>
    </where>
    GROUP BY p.id
</select> 

IPartnerService

/**
 * 查询合作商管理列表
 * @param partner
 * @return partnerVo集合
 */
public List<PartnerVo> selectPartnerVoList(Partner partner); 

PartnerServiceImpl 

/**
 * 查询合作商管理列表
 * @param partner
 * @return partnerVo集合
 */
@Override
public List<PartnerVo> selectPartnerVoList(Partner partner) {
    return partnerMapper.selectPartnerVoList(partner);
}

PartnerController

/**
 * 查询合作商管理列表
 */
@PreAuthorize("@ss.hasPermi('manage:partner:list')")
@GetMapping("/list")
public TableDataInfo list(Partner partner) {
    startPage();
    List<PartnerVo> voList = partnerService.selectPartnerVoList(partner);
    return getDataTable(voList);
}

region/index.vue 

<!-- 合作商列表 -->
<el-table v-loading="loading" :data="partnerList" @selection-change="handleSelectionChange">
    <el-table-column type="selection" width="55" align="center" />
    <el-table-column label="序号" type="index" width="50" align="center" prop="id" />
    <el-table-column label="合作商名称" align="center" prop="partnerName" />
    <el-table-column label="点位数" align="center" prop="nodeCount" />
    <el-table-column label="账号" align="center" prop="account" />
    <el-table-column label="分成比例" align="center" prop="profitRatio" >
        <template #default="scope">{{ scope.row.profitRatio }}%</template>
    </el-table-column>
    <el-table-column label="联系人" align="center" prop="contactPerson" />
    <el-table-column label="联系电话" align="center" prop="contactPhone" />
    <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
        <template #default="scope">
            <el-button link type="primary"  @click="getParnterInfo(scope.row)" v-hasPermi="['manage:partner:query']">查看详情</el-button>
            <el-button link type="primary"  @click="handleUpdate(scope.row)" v-hasPermi="['manage:partner:edit']">修改</el-button>
            <el-button link type="primary"  @click="handleDelete(scope.row)" v-hasPermi="['manage:partner:remove']">删除</el-button>
        </template>
    </el-table-column>
</el-table>

重置密码

后端部分

在PartnerController中

/**
 * 重置合作商密码
 */
@PreAuthorize("@ss.hasPermi('manage:partner:edit')")
@Log(title = "重置合作商密码", businessType = BusinessType.UPDATE)
@PutMapping("/resetPwd/{id}")
public AjaxResult resetpwd(@PathVariable Long id) {//1. 接收参数
    //2. 创建合作商对象
    Partner partner = new Partner();
    partner.setId(id);// 设置id
    partner.setPassword(SecurityUtils.encryptPassword("123456"));// 设置加密后的初始密码
    //3. 调用service更新密码
    return toAjax(partnerService.updatePartner(partner));
}

前端部分

在manage/partner.js请求api中

// 重置合作商密码
export function resetPartnerPwd(id){
  return request({
    url: '/manage/partner/resetPwd/' + id,
    method: 'put'
  })

前端部分

在manage/partner.js请求api中

在partner/index.vue视图组件中 

点位管理改造

基础页面

需求

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

代码实现

在node/index.vue视图组件中

在api/page.js中,抽取一个查询所有的条件 

export const loadAllParams = reactive({
  pageNum: 1,
  pageSize: 10000,
});

<!-- 搜索区域 -->
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
    <el-form-item label="点位名称" prop="nodeName">
        <el-input v-model="queryParams.nodeName" placeholder="请输入点位名称" clearable @keyup.enter="handleQuery" />
    </el-form-item>
    <el-form-item label="区域搜索" prop="regionId">
        <!-- <el-input v-model="queryParams.regionId" placeholder="请输入区域ID" clearable @keyup.enter="handleQuery" /> -->
        <el-select v-model="queryParams.regionId" placeholder="请选择区域" clearable>
            <el-option v-for="item in regionList" :key="item.id" :label="item.regionName" :value="item.id"></el-option>
        </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-table v-loading="loading" :data="nodeList" @selection-change="handleSelectionChange">
    <el-table-column type="selection" width="55" align="center" />
    <el-table-column label="序号" type="index" width="50" align="center" prop="id" />
    <el-table-column label="点位名称" align="center" prop="nodeName" />
    <el-table-column label="区域ID" align="center" prop="regionId" />
    <el-table-column label="商圈类型" align="center" prop="businessType">
        <template #default="scope">
<dict-tag :options="business_type" :value="scope.row.businessType" />
        </template>
    </el-table-column>
    <el-table-column label="合作商ID" align="center" prop="partnerId" />
    <el-table-column label="详细地址" align="center" prop="address" show-overflow-tooltip/>
    <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
        <template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['manage:node:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['manage:node:remove']">删除</el-button>
        </template>
    </el-table-column>
</el-table>

<!-- 添加或修改点位管理对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
  <el-form ref="nodeRef" :model="form" :rules="rules" label-width="100px">
    <el-form-item label="点位名称" prop="nodeName">
      <el-input v-model="form.nodeName" placeholder="请输入点位名称" />
    </el-form-item>
    <el-form-item label="所在区域" prop="regionId">
      <!-- <el-input v-model="form.regionId" placeholder="请输入区域ID" /> -->
        <el-select v-model="form.regionId" placeholder="请选择区域" clearable>
        <el-option v-for="item in regionList" :key="item.id" :label="item.regionName" :value="item.id"></el-option>
      </el-select>
    </el-form-item>
    <el-form-item label="商圈类型" prop="businessType">
      <el-select v-model="form.businessType" placeholder="请选择商圈类型">
        <el-option v-for="dict in business_type" :key="dict.value" :label="dict.label"
          :value="parseInt(dict.value)"></el-option>
      </el-select>
    </el-form-item>
    <el-form-item label="归属合作商" prop="partnerId">
      <!-- <el-input v-model="form.partnerId" placeholder="请输入合作商ID" /> -->
        <el-select v-model="form.partnerId" placeholder="请选择合作商" clearable>
        <el-option v-for="item in partnerList" :key="item.id" :label="item.partnerName" :value="item.id"></el-option>
      </el-select>
    </el-form-item>
    <el-form-item label="详细地址" prop="address">
      <el-input type="textarea" v-model="form.address" 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>

<script setup name="Node">
    import { listPartner } from "@/api/manage/partner";
    import { listRegion } from "@/api/manage/region";
    import { loadAllParams } from "@/api/page";

    /* 查询所有的条件对象 */
    /* const loadAllParams = reactive({
      pageNum: 1,
      pageSize: 10000,
    }); */

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

</script>

在api/page.js中,抽取一个查询所有的条件

export const loadAllParams = reactive({
  pageNum: 1,
  pageSize: 10000,
});

点位列表

需求

在区域详情中,需要显示每个点位的设备数

 在点位列表查询中,会关联显示区域、商圈等信息

 

实现思路

关联查询:对于设备数量的统计,我们需要执行关联查询,在mapper 层封装。

关联实体:对于区域和合作商的数据,我们会采用Mybatis提供的嵌套查询功能。ORM映射

MyBatis 嵌套查询就是将原来多表查询中的联合查询语句拆成单个表的查询,再使用mybatis的语法嵌套在一 起,通过定义resultMapsql语句中的association(一对一/多对一)collection(一对多)元素来实现嵌套查询。导入资料中dkd.sql业务表到开发数据库中

SQL

-- AI辅助编程模式
-- 你是一个软件开发工程师,现在要根据数据库的sql脚本,查询并显示点位表所有的字段信息,同时显示每个点位的设备数量,sql脚本如下:
create table tb_node
(
    id            int auto_increment comment '主键id'
        primary key,
    node_name     varchar(255)                        not null comment '点位名称',
    address       varchar(255)                        not null comment '详细地址',
    business_type int                                 null comment '商圈类型',
    region_id     int                                 null comment '区域ID',
    partner_id    int                                 null comment '合作商ID',
    create_time   timestamp default CURRENT_TIMESTAMP null comment '创建时间',
    update_time   timestamp default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '修改时间',
    create_by     varchar(64)                         null comment '创建人',
    update_by     varchar(64)                         null comment '修改人',
    remark        text                                null comment '备注',
    constraint tb_node_ibfk_1
        foreign key (region_id) references tb_region (id)
            on update cascade on delete cascade,
    constraint tb_node_ibfk_2
        foreign key (partner_id) references tb_partner (id)
            on update cascade on delete cascade
)
    comment '点位表';
    
create table tb_vending_machine
(
    id                   bigint auto_increment comment '主键'
        primary key,
    inner_code           varchar(15) default '000'                 null comment '设备编号',
    channel_max_capacity int                                       null comment '设备容量',
    node_id              int                                       not null comment '点位Id',
    addr                 varchar(100)                              null comment '详细地址',
    last_supply_time     datetime    default '2000-01-01 00:00:00' not null comment '上次补货时间',
    business_type        int                                       not null comment '商圈类型',
    region_id            int                                       not null comment '区域Id',
    partner_id           int                                       not null comment '合作商Id',
    vm_type_id           int         default 0                     not null comment '设备型号',
    vm_status            int         default 0                     not null comment '设备状态,0:未投放;1-运营;3-撤机',
    running_status       varchar(100)                              null comment '运行状态',
    longitudes           double      default 0                     null comment '经度',
    latitude             double      default 0                     null comment '维度',
    client_id            varchar(50)                               null comment '客户端连接Id,做emq认证用',
    policy_id            bigint                                    null comment '策略id',
    create_time          timestamp   default CURRENT_TIMESTAMP     not null comment '创建时间',
    update_time          timestamp   default CURRENT_TIMESTAMP     null comment '修改时间',
    constraint vendingmachine_VmId_uindex
        unique (inner_code),
    constraint tb_vending_machine_ibfk_1
        foreign key (vm_type_id) references tb_vm_type (id),
    constraint tb_vending_machine_ibfk_2
        foreign key (node_id) references tb_node (id),
    constraint tb_vending_machine_ibfk_3
        foreign key (policy_id) references tb_policy (policy_id)
)
    comment '设备表'; 

-- 查询并显示点位表所有的字段信息,同时显示每个点位的设备数量
SELECT
    n.id,
    n.node_name,
    n.address,
    n.business_type,
    n.region_id,
    n.partner_id,
    n.create_time,
    n.update_time,
    n.create_by,
    n.update_by,
    n.remark,
    COUNT(v.id) AS vm_count
FROM
    tb_node n
LEFT JOIN
    tb_vending_machine v ON n.id = v.node_id
GROUP BY
    n.id;

-- 根据区域id查询区域信息
select * from tb_region where id=1;
-- 根据合作商id查询合作商信息
select * from tb_partner where id=1;

NodeVo

@Data
public class NodeVo extends Node {

    // 设备数量
    private int vmCount;

    // 区域
    private Region region;

    // 合作商
    private Partner partner;
}

NodeMapper 

/**
 * 查询点位管理列表
 * @param node
 * @return NodeVo集合
 */
public List<NodeVo> selectNodeVoList(Node node); 

NodeMapper.xml 

<resultMap type="NodeVo" id="NodeVoResult">
    <result property="id"    column="id"    />
    <result property="nodeName"    column="node_name"    />
    <result property="address"    column="address"    />
    <result property="businessType"    column="business_type"    />
    <result property="regionId"    column="region_id"    />
    <result property="partnerId"    column="partner_id"    />
    <result property="createTime"    column="create_time"    />
    <result property="updateTime"    column="update_time"    />
    <result property="createBy"    column="create_by"    />
    <result property="updateBy"    column="update_by"    />
    <result property="remark"    column="remark"    />
    <result property="vmCount"    column="vm_count"    />
   <association property="region" javaType="Region" column="region_id" select="com.dkd.manage.mapper.RegionMapper.selectRegionById"/>
   <association property="partner" javaType="Partner" column="partner_id" select="com.dkd.manage.mapper.PartnerMapper.selectPartnerById"/>
</resultMap>

<select id="selectNodeVoList" resultMap="NodeVoResult">
    SELECT
    n.id,
    n.node_name,
    n.address,
    n.business_type,
    n.region_id,
    n.partner_id,
    n.create_time,
    n.update_time,
    n.create_by,
    n.update_by,
    n.remark,
    COUNT(v.id) AS vm_count
    FROM
    tb_node n
    LEFT JOIN
    tb_vending_machine v ON n.id = v.node_id
    <where>
        <if test="nodeName != null  and nodeName != ''"> and n.node_name like concat('%', #{nodeName}, '%')</if>
        <if test="regionId != null "> and n.region_id = #{regionId}</if>
        <if test="partnerId != null "> and n.partner_id = #{partnerId}</if>
    </where>
    GROUP BY
    n.id
</select>

NodeService 

/**
 * 查询点位管理列表
 * @param node
 * @return NodeVo集合
 */
public List<NodeVo> selectNodeVoList(Node node); 

NodeServiceImpl 

/**
 * 查询点位管理列表
 *
 * @param node
 * @return NodeVo集合
 */
@Override
public List<NodeVo> selectNodeVoList(Node node) {
    return nodeMapper.selectNodeVoList(node);

NodeController

/**
 * 查询点位管理列表
 */
@PreAuthorize("@ss.hasPermi('manage:node:list')")
@GetMapping("/list")
public TableDataInfo list(Node node)
{
    startPage();
    List<NodeVo> voList = nodeService.selectNodeVoList(node);
    return getDataTable(voList);

区域查看详情

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

 

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

<!-- 查看详情对话框 -->
<el-dialog title="区域详情" v-model="regionInfoOpen" width="500px" append-to-body>
    <el-form-item label="区域名称" prop="regionName">
        <el-input v-model="form.regionName" disabled />
    </el-form-item>
    <label>包含点位:</label>
    <el-table :data="nodeList">
        <el-table-column label="序号" type="index" width="50" align="center" />
        <el-table-column label="点位名称" align="center" prop="nodeName" />
        <el-table-column label="设备数量" align="center" prop="vmCount" />
    </el-table>
</el-dialog>

<script>
    import { listNode } from "@/api/manage/node";
    import { loadAllParams } from "@/api/page";
    
    /* 查看详情按钮操作 */
    const nodeList = ref([]);
    const regionInfoOpen = ref(false);
    function getRegionInfo(row) {
        // 查询区域信息
        reset();
        const _id = row.id
        getRegion(_id).then(response => {
            form.value = response.data;
        });
        // 查询点位列表
        loadAllParams.regionId = row.id;
        listNode(loadAllParams).then(response => {
            nodeList.value = response.rows;
        });
        regionInfoOpen.value = true;
</script>

数据完整性

现在我们要思考一个问题,当我们删除区域或合作商数据时,与之关联的点位数据该如何处理?

在默认情况下,由于我们在创建点位表时通过AI设置了外键约束,并配置了级联删除操作,所以删除区域或合作商会导致其关联的点位数据一并被删除。从技术角度来看,这是符合数据库的外键约束规则的。

但是,从业务角度来看,这种做法可能不太合适。想象一下,如果一个区域下有多个点位,一次误操作就可能导致所有的点位数据及其关联的设备信息被一并删除,这显然是我们不愿意看到的。

因此,我们需要对级联操作进行修改,将其改为限制删除。这样,当尝试删除一个区域或合作商时,如果它下面还有关联的点位数据,数据库将不会允许删除操作,并会给出错误提示。

**CASCADE(级联操作):**当父表中的某行记录被删除或更新时,与其关联的所有子表中的匹配行也会自动被删除或更新。这种方式适用于希望保持数据一致性的场景,即父记录不存在时,相关的子记录也应该被移除。

**SET NULL(设为空):**若父表中的记录被删除或更新,子表中对应的外键字段会被设置为NULL。选择此选项的前提是子表的外键列允许为NULL值。这适用于那些子记录不再需要明确关联到任何父记录的情况。

**RESTRICT(限制):**在尝试删除或更新父表中的记录之前,数据库首先检查是否有相关联的子记录存在。如果有,则拒绝执行删除或更新操作,以防止意外丢失数据或破坏数据关系的完整性。这是一种保守策略,确保数据间的引用完整性。

**NO ACTION(无操作):**在标准SQL中,NO ACTION是一个关键字,它要求数据库在父表记录被删除或更新前,检查是否会影响子表中的相关记录。在MySQL中,NO ACTION的行为与RESTRICT相同,即如果子表中有匹配的行,则禁止执行父表的删除或更新操作。这意味着如果存在依赖关系,操作将被阻止,从而保护数据的参照完整性。

修改完毕后,如果你尝试进行删除操作,会发现数据库的完整性约束生效了,它会阻止删除操作并给出错误提示。但是,这个错误提示信息可能对于用户来说不够友好,可能会让用户感到困惑。

SQLIntegrityConstraintViolationException是Java中的一个异常类,这个类通常用于表示SQL数据库操作中的完整性约束违反异常

例如:外键约束、唯一约束等。当数据库操作违反了这些约束时,就会抛出这个异常。

这个错误是由于外键约束导致的。它表明在删除或更新父表的行时,存在外键约束,子表中的相关行会受到影响。

是因为在删除tb_region表中的行时,tb_node表中的region_id外键约束会阻止操作。

如果你在使用Spring框架进行数据库操作,可能会先遇到DataIntegrityViolationException,它是对SQLIntegrityConstraintViolationException的一个更高层次的抽象,旨在提供一种更加面向应用的错误表示。

而SQLIntegrityConstraintViolationException是更底层的异常,直接来源于数据库驱动,包含更多底层数据库相关的细节。

在实际开发中,推荐捕获并处理DataIntegrityViolationException,因为它更符合Spring应用的异常处理模式,同时也可以通过其内部的cause(原因)属性来获取具体的SQLIntegrityConstraintViolationException,进而获取详细的错误信息。

为了提升用户体验,我们可以使用Spring Boot框架的全局异常处理器来捕获这些错误信息,并返回更友好的提示信息给用户。这样,当用户遇到这种情况时,他们将收到一个清晰、易懂的提示,告知他们操作无法完成的原因。

修改全局异常处理器,添加以下内容

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

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

        return AjaxResult.error("无法删除,有其他数据引用");
    }
    return AjaxResult.error("您的操作违反了数据库中的完整性约束");
}

人员管理

需求说明

人员管理业务流程如下:

1. **登录系统:** 首先,后台管理人员需要登录到帝可得后台管理系统中。
2. **新增工作人员:** 登录系统后,管理人员可以新增工作人员,包括姓名、联系方式等信息。
3. **关联角色:** 确定此员工是运维人员还是运营人员,这将影响他们的职责和权限。
4. **关联区域:** 确定员工负责的区域范围,确保工作人员能够高效地完成区域内的设备安装、维修、商品补货等工作。

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

- 关系字段:role_id、region_id
- 数据字典(静态数据):status(1启用、0停用)--->数据字典只能设计两个
- 冗余字段:region_name、role_code、role_name

 一个角色下可以拥有多个员工,一个员工属于某一个角色

可通过外键字段/角色编码查看角色信息

 一个区域下可以关联多名员工,员工负责某一区域

生成基础代码

需求

使用若依代码生成器,生成人员列表前后端基础代码,并导入到项目中:

步骤

①创建目录菜单

创建人员管理目录菜单

#### ②添加数据字典

先创建`员工状态`的字典类型

再创建员工状态的字典数据  

③配置代码生成信息

导入二张表

 

#### ④下载代码并导入项目

选中二张表生成下载

解压ruoyi.zip得到前后端代码和动态菜单sql

注意:角色动态菜单sql和视图组件不需要导入

代码导入  

人员列表改造 

### 基础页面

#### 需求

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

代码实现

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

<!-- 搜索区域 -->
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
    <el-form-item label="人员名称" prop="userName">
        <el-input v-model="queryParams.userName" 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="empList" @selection-change="handleSelectionChange">
    <el-table-column type="selection" width="55" align="center" />
    <el-table-column label="序号" type="index" width="80" align="center" prop="id" />
    <el-table-column label="人员名称" align="center" prop="userName" />
    <el-table-column label="归属区域" align="center" prop="regionName" />
    <el-table-column label="角色" align="center" prop="roleName" />
    <el-table-column label="联系电话" align="center" prop="mobile" />
    <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:emp:edit']">修改</el-button>
			<el-button link type="primary"  @click="handleDelete(scope.row)" v-hasPermi="['manage:emp:remove']">删除</el-button>
        </template>
    </el-table-column>
</el-table>

<!-- 添加或修改人员列表对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
  <el-form ref="empRef" :model="form" :rules="rules" label-width="80px">
    <el-form-item label="员工名称" prop="userName">
      <el-input v-model="form.userName" placeholder="请输入员工名称" />
    </el-form-item>
    <el-form-item label="角色" prop="roleId">
      <!-- <el-input v-model="form.roleId" placeholder="请输入角色id" /> -->
      <el-select v-model="form.roleId" placeholder="请选择角色">
        <el-option v-for="item in roleList" :key="item.roleId" :label="item.roleName" :value="item.roleId" />
      </el-select>
    </el-form-item>
    <el-form-item label="联系电话" prop="mobile">
      <el-input v-model="form.mobile" placeholder="请输入联系电话" />
    </el-form-item>
    <el-form-item label="创建时间" prop="createTime" v-if="form.id!=null">
      {{form.createTime }}
    </el-form-item>
    <el-form-item label="负责区域" prop="regionId">
      <!-- <el-input v-model="form.regionId" placeholder="请输入所属区域Id" /> -->
      <el-select v-model="form.regionId" placeholder="请选择所属区域">
        <el-option v-for="item in regionList" :key="item.id" :label="item.regionName" :value="item.id" />
      </el-select>
    </el-form-item>
    <el-form-item label="员工头像" prop="image">
      <image-upload v-model="form.image" />
    </el-form-item>
    <el-form-item label="是否启用" prop="status">
      <el-radio-group v-model="form.status">
        <el-radio v-for="dict in emp_status" :key="dict.value"
          :label="parseInt(dict.value)">{{ dict.label }}</el-radio>
      </el-radio-group>
    </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>
    import { listRegion } from "@/api/manage/region";
    import { listRole } from "@/api/manage/role";
    import { loadAllParams } from "@/api/page";

    // 查询角色列表
    const roleList = ref([]);
    function getRoleList() {
        listRole(loadAllParams).then(response => {
            roleList.value = response.rows;
        });
    }
    // 查询区域列表
    const regionList = ref([]);
    function getRegionList() {
        listRegion(loadAllParams).then(response => {
            regionList.value = response.rows;
        });
    }
    getRegionList();
    getRoleList();
</script>

在EmpServiceImpl中新增和修改时,补充区域名称和角色信息

@Autowired
private RegionMapper regionMapper;

@Autowired
private RoleMapper roleMapper;

/**
 * 新增人员列表
 *
 * @param emp 人员列表
 * @return 结果
 */
@Override
public int insertEmp(Emp emp)
{
    // 补充区域名称
    emp.setRegionName(regionMapper.selectRegionById(emp.getRegionId()).getRegionName());
    // 补充角色信息
    Role role = roleMapper.selectRoleByRoleId(emp.getRoleId());
    emp.setRoleName(role.getRoleName());
    emp.setRoleCode(role.getRoleCode());
    emp.setCreateTime(DateUtils.getNowDate());
    return empMapper.insertEmp(emp);
}

/**
 * 修改人员列表
 *
 * @param emp 人员列表
 * @return 结果
 */
@Override
public int updateEmp(Emp emp)
{
    // 补充区域名称
    emp.setRegionName(regionMapper.selectRegionById(emp.getRegionId()).getRegionName());
    // 补充角色信息
    Role role = roleMapper.selectRoleByRoleId(emp.getRoleId());
    emp.setRoleName(role.getRoleName());
    emp.setRoleCode(role.getRoleCode());
    emp.setUpdateTime(DateUtils.getNowDate());
    return empMapper.updateEmp(emp);
}

同步存储

实现思路

实现此功能方案:

**同步存储:**在员工表中有区域名称的冗余字段,在更新区域表的同时,同步更新员工表中区域名称。

- 优点:由于是单表查询操作,查询列表效率最高。
- 缺点:需要在区域修改时修改员工表中的数据,有额外的开销,数据也可能不一致。

sql

-- 根据区域id修改区域名称
update tb_emp set region_name='北京市奥体中心' where region_id=5 

EmpMapper

 /**
 * 根据区域id修改区域名称
 * @param regionName
 * @param regionId
 * @return 结果
 */
@Update("update tb_emp set region_name=#{regionName} where region_id=#{regionId}")
int updateByRegionId(@Param("regionName") String regionName, @Param("regionId") Long regionId);

RegionServiceImpl

@Autowired
private EmpMapper empMapper;

/**
 * 修改区域管理
 *
 * @param region 区域管理
 * @return 结果
 */
@Transactional(rollbackFor = Exception.class)
@Override
public int updateRegion(Region region)
{
    // 先更新区域信息
    region.setUpdateTime(DateUtils.getNowDate());
    int result = regionMapper.updateRegion(region);

    // 同步更新员工表区域名称
    empMapper.updateByRegionId(region.getRegionName(),region.getId());
    return result;
}

文件存储

### 本地存储

问题分析说明:
 在若依框架目前的实现中,是把图片存储到了服务器本地的目录,通过服务进行访问,这样做存储的是比较省事,但是缺点也有很多:

- 硬件与网络要求:服务器通常需要高性能的硬件和稳定的网络环境,以保证文件传输的效率和稳定性。这可能会增加硬件和网络资源的成本和维护难度。
- 管理难度:服务器目录需要管理员进行配置和管理,包括权限设置、备份策略等。如果管理不善或配置不当,可能会引发一些安全问题和性能问题。
- 性能瓶颈:如果服务器处理能力不足或网络带宽不够,可能会导致性能瓶颈,影响文件上传、下载和访问的速度。
- 单点故障风险:服务器故障可能导致所有存储在其上的文件无法访问,尽管可以通过备份和冗余措施来降低这种风险,但单点故障的风险仍然存在。

为了解决上述问题呢,通常有两种解决方案:

- 自己搭建存储服务器,如:fastDFS 、MinIO
- 使用现成的云服务,如:阿里云,腾讯云,华为云

阿里云OSS

介绍

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

在我们使用了阿里云OSS对象存储服务之后,我们的项目当中如果涉及到文件上传这样的业务,在前端进行文件上传并请求到服务端时,在服务器本地磁盘当中就不需要再来存储文件了。我们直接将接收到的文件上传到oss,由 oss帮我们存储和管理,同时阿里云的oss存储服务还保障了我们所存储内容的安全可靠。 

 第三方服务使用的通用思路,我们做一个简单介绍之后,接下来我们就来介绍一下我们当前要使用的阿里云oss对象存储服务具体的使用步骤。

账号准备

下面我们根据之前介绍的使用步骤,完成准备工作:

  1. 注册阿里云账户(注册完成后需要实名认证

    阿里云登录 - 欢迎登录阿里云,安全稳定的云计算服务平台

1. 

2. 注册完账号之后,就可以登录阿里云

开通OSS云服务

1). 通过控制台找到对象存储OSS服务

 

如果是第一次访问,还需要开通对象存储服务OSS 

2). 开通OSS服务之后,就可以进入到阿里云对象存储的控制台

3). 点击左侧的 "Bucket列表",创建一个Bucket  

 

#### 配置AK & SK

1). 创建AccessKey

点击 "AccessKey管理",进入到管理页面。

 

2). 配置AK & SK

管理员身份打开CMD命令行,执行如下命令,配置系统的环境变量。

xxxxxxxxxx set OSS_ACCESS_KEY_ID=LTAI5tXXXXXXXXXXXXXXXXXXXXM8TPset OSS_ACCESS_KEY_SECRET=UzMcJXXXXXXXXXXXXXXXXXXXXdabTNafi 

注意:将上述的ACCESS_KEY_ID  与 ACCESS_KEY_SECRET 的值一定一定一定一定一定一定要替换成自己的 。

执行如下命令,让更改生效。

setx OSS_ACCESS_KEY_ID "%OSS_ACCESS_KEY_ID%"
setx OSS_ACCESS_KEY_SECRET "%OSS_ACCESS_KEY_SECRET%"

执行如下命令,验证环境变量是否生效。  

echo %OSS_ACCESS_KEY_ID%
echo %OSS_ACCESS_KEY_SECRET%

入门

阿里云oss 对象存储服务的准备工作我们已经完成了,接下来我们就来完成第二步操作:参照官方所提供的sdk示例来编写入门程序。

首先我们需要来打开阿里云OSS的官方文档,在官方文档中找到 SDK 的示例代码:

如果是在实际开发当中,我们是需要从前往后仔细的去阅读这一份文档的,但是由于现在是教学,我们就只挑重点的去看。有兴趣的同学大家下来也可以自己去看一下这份官方文档。 参照官方提供的SDK,改造一下,即可实现文件上传功能:

package com.dkd.common.test;

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.common.auth.*;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import java.io.FileInputStream;
import java.io.InputStream;

public class Demo {

    public static void main(String[] args) throws Exception {
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "https://oss-cn-beijing.aliyuncs.com";
        // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
        EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "dkd-itheima";
        // 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
        String objectName = "gao.png";
        // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
        // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
        String filePath= "E:\\temp\\upload\\gao.png";

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);

        try {
            InputStream inputStream = new FileInputStream(filePath);
            // 创建PutObjectRequest对象。
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);
            // 创建PutObject请求。
            PutObjectResult result = ossClient.putObject(putObjectRequest);
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }
}

> 在以上代码中,需要替换的内容为:
>
> - endpoint:阿里云OSS中的bucket对应的域名
>
> - bucketName:Bucket名称
> - objectName:对象名称,在Bucket中存储的对象的名称
> - filePath:文件路径

运行以上程序后,会把本地的文件上传到阿里云OSS服务器上。

官方地址:X File Storage

一行代码将文件存储到本地、FTP、SFTP、WebDAV、阿里云 OSS、华为云 OBS、七牛云 Kodo、腾讯云 COS、百度云 BOS、又拍云 USS、MinIO、 Amazon S3、GoogleCloud Storage、FastDFS、 Azure Blob Storage、Cloudflare R2、金山云 KS3、美团云 MSS、京东云 OSS、天翼云 OOS、移动 云EOS、沃云 OSS、 网易数帆 NOS、Ucloud US3、青云 QingStor、平安云 OBS、首云 OSS、IBM COS、其它兼容 S3 协议的存储平台。

#### 集成

1)在dkd-common的pom.xml中引入依赖

 <!-- 文件上传-->
<dependency>
    <groupId>org.dromara.x-file-storage</groupId>
    <artifactId>x-file-storage-spring</artifactId>
    <version>2.1.0</version>
</dependency>
<!-- 阿里云oss-->
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.16.1</version>
</dependency>

2)在dkd-admin的application.yml 配置文件中先添加以下基础配置,再添加对应平台的配置  

# 文件上传
dromara:
  x-file-storage: #文件存储配置
    default-platform: aliyun-oss-1 #默认使用的存储平台
    thumbnail-suffix: ".min.jpg" #缩略图后缀,例如【.min.jpg】【.png】
    #对应平台的配置写在这里,注意缩进要对齐
    aliyun-oss:
      - platform: aliyun-oss-1 # 存储平台标识
        enable-storage: true  # 启用存储
        access-key: ??
        secret-key: ??
        end-point: ??
        bucket-name: ??
        domain: ?? # 访问域名,注意“/”结尾,例如:https://abc.oss-cn-shanghai.aliyuncs.com/ 

3)在dkd-admin的启动类上加上@EnableFileStorage注解  

@EnableFileStorage
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class DkdApplication
{
    public static void main(String[] args)
    {
        // System.setProperty("spring.devtools.restart.enabled", "false");
        SpringApplication.run(DkdApplication.class, args);
        System.out.println("(♥◠‿◠)ノ゙  帝可得启动成功   ლ(´ڡ`ლ)゙");
    }

4)修改若依默认上传图片代码

找到ruoyi-admin模块中的com.ruoyi.web.controller.common.**CommonController类**,修改单个文件上传的方法

@Autowired
private FileStorageService fileStorageService;//注入实列

/**
  * 通用上传请求(单个)
*/
@PostMapping("/upload")
public AjaxResult uploadFile(MultipartFile file) throws Exception {

    try {
        // 指定oss保存文件路径
        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,前端访问的地址,需要文件的地址 而不是文件名称
        ajax.put("newFileName", fileInfo.getUrl());
        ajax.put("originalFilename", file.getOriginalFilename());
        return ajax;
    } catch (Exception e) {
        return AjaxResult.error(e.getMessage());
    }
}

5)联调测试,重启后台程序,前端上传图片操作,F12抓包工具查看效果:  

设备管理

需求说明

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

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

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

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

生成基础代码

需求

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

步骤

①创建目录菜单

创建设备管理目录菜单

#### ②添加数据字典

先创建`设备状态`的字典类型

 再创建设备状态的字典数据

#### ③配置代码生成信息

导入三张表

 

#### ④下载代码并导入项目

选中三张表生成下载

 

解压`ruoyi.zip`得到前后端代码和动态菜单sql

> 注意:货道动态菜单sql和前端不需要导入代码导入 调整二级菜单显示顺序(设备状态稍后完成

设备类型改造

基础页面

需求

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

#### 代码实现

在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>

设备管理改造

基础页面

需求

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

#### 代码实现

刷新设备表数据

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; 

在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>

### 新增设备

#### 需求

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

我们了解到在新增设备时,添加设备和货道表,还包含点位和设备类型的查询,共涉及到四张表的操作。

这个过程需要我们仔细处理每个字段,确保数据的一致性和完整性

VendingMachineServiceImpl
@Autowired
private INodeService nodeService;
@Autowired
private IVmTypeService vmTypeService;
@Autowired
private IChannelService channelService;

/**
 * 新增设备管理
 *
 * @param vendingMachine 设备管理
 * @return 结果
 */
@Transactional
@Override
public int insertVendingMachine(VendingMachine vendingMachine) {
    //1. 新增设备
    //1-1 生成8位编号,补充货道编号
    String innerCode = UUIDUtils.getUUID();
    vendingMachine.setInnerCode(innerCode); // 售货机编号
    //1-2 查询售货机类型表,补充设备容量
    VmType vmType = vmTypeService.selectVmTypeById(vendingMachine.getVmTypeId());
    vendingMachine.setChannelMaxCapacity(vmType.getChannelMaxCapacity());
    //1-3 查询点位表,补充 区域、点位、合作商等信息
    Node node = nodeService.selectNodeById(vendingMachine.getNodeId());
    BeanUtil.copyProperties(node, vendingMachine, "id");
    vendingMachine.setAddr(node.getAddress());
    //1-4 设备状态
    vendingMachine.setVmStatus(DkdContants.VM_STATUS_NODEPLOY);// 0-未投放(数据库有默认值,这个不写也不影响)
    vendingMachine.setCreateTime(DateUtils.getNowDate());// 创建时间
    vendingMachine.setUpdateTime(DateUtils.getNowDate());// 更新时间
    //1-5 保存
    int result = vendingMachineMapper.insertVendingMachine(vendingMachine);
    //2. 新增货道
    //2-1 声明货道集合
    List<Channel> channelList = new ArrayList<>();
    //2-2 双层for循环
    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);
        }
    }
    //2-4 批量新增
    channelService.batchInsertChannel(channelList);
    return result;
}

ChannelMapper接口和xml

/**
 * 批量新增售货机货道
 * @param channelList
 * @return 结果
 */
public int batchInsertChannel(List<Channel> channelList); 

<insert id="batchInsertChannel" parameterType="java.util.List">
    INSERT INTO tb_channel (
    channel_code, vm_id, inner_code, max_capacity, current_capacity, last_supply_time, create_time, update_time
    ) VALUES
    <foreach collection="list" item="channel" separator=",">
        (
        #{channel.channelCode},
        #{channel.vmId},
        #{channel.innerCode},
        #{channel.maxCapacity},
        #{channel.currentCapacity},
        #{channel.lastSupplyTime},
        #{channel.createTime},
        #{channel.updateTime}
        )
    </foreach>
</insert>

IChannelService

/**
 * 批量新增售货机货道
 * @param channelList
 * @return 结果
 */
public int batchInsertChannel(List<Channel> channelList); 

ChannelServiceImpl 

/**
 * 批量新增售货机货道
 * @param channelList
 * @return 结果
 */
@Override
public int batchInsertChannel(List<Channel> channelList) {
    return channelMapper.batchInsertChannel(channelList);

修改设备

需求

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

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

VendingMachineServiceImpl

/**
 * 修改设备管理
 *
 * @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);

设备状态改造

创建视图组件

创建vmStatus/index.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="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>

  • 19
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值