文档以及资料:04-企业级智能物联网项目(中州养老) - 飞书云文档
项目中的护理模块相关内容已经在若依框架资料中实现
入住办理 在开发之前呢,重新搞定一套新的环境,详细请参考:初始代码-环境准备
需求分析 入住办理列表页 原型文件如下:
搜索:老人姓名为模糊搜索,老人身份证号为精准搜索;
列表数据:列表中所展示的数据是入住成功且未退住的老人信息
发起入住办理:点击【发起入住申请】进入到【入住申请详情页】;
查看:点击【查看】进入到【入住详情页】,数据回显,不可编辑,不显示文本框,只显示已填写/已选择的内容;
入住办理详情页 通过原型打开入住办理详情页,详细如下:
上述需求中,共包含了四部分:
基本信息:入住老人的基本信息,包含了姓名、年龄、身份证号、性别,住址、照片等信息
家属信息:入住老人的家属列表,可以有多个
入住配置:入住老人在养老院选择的费用信息,包含了护理等级,入住床位、入住期限等信息
签约办理:入住老人签订的合同信息
表结构设计 后端开发流程 在开发接口之前呢,需要先熟悉一下后端开发的流程,如下:
需求分析(基于原型和PRD)
开发计划(工期评估)
表结构设计(基于原型和PRD)
接口设计(基于原型和PRD)
功能实现(基于接口设计+原型+PRD)
前后端联调
测试提bug
前后端优化,再联调
测试回归bug
功能验收
如何设计表 ER图 E-R图也称实体-联系图(Entity Relationship Diagram),提供了表示实体 类型、属性 和联系 的方法,用来描述现实世界的概念模型。
一个栗子:
1 2 3 4 5 6 7 8 9 10 11 12 13 假设咱们要设计一个简单的图书馆数据库系统。咱们可以使用E-R图来描述这个系统的数据模型 咱们可能会有以下实体(Entities): - 书籍(Book)- 作者(Author)- 图书馆会员(Library Member)然后,咱们可以描述它们之间的关系(Relationships): - 一本书可以由一个或多个作者编写,这是一个"一对多"的关系。- 图书馆成员可以借阅多本书,而每本书也可以被多个图书馆成员借阅,这是一个"多对多"的关系。最后,咱们可以添加一些属性(Attributes): - 书籍实体可能包括书名、ISBN号、出版日期等属性。- 作者实体可能包括姓名、国籍、出生日期等属性。- 图书馆成员实体可能包括会员号、姓名、联系信息等属性。通过E-R图,咱们可以清晰地展示这些实体之间的关系和它们的属性,从而更好地理解图书馆数据库系统的数据模型。
上述例子的E-R图:
详细设计
命名规则参考阿里开发手册(见名知意)
数据类型
基本准则:
可靠性:考虑字段存储的内容长度,尽可能的合适
存储空间:在考虑可靠性的前提下,再考虑存储空间
常见数据类型 MySQL常见数据类型
主键(必须存在)
冗余字段
创建时间
修改时间
创建人
修改人
备注
要结合业务的情况,再去设计冗余字段(为了提升性能,减少多表查询)
建表工具
创建表的工具,可以选择使DataGrip或者IDEA中集成的DataGrip工具,协助开发人员来设计表结构
可以借助AI大模型问答,写对应的提示词来创建表结构:
1 2 3 4 5 6 你是一个软件工程师,帮我生成MySQL的表结构,需求如下: 1,入住表(check_in),包含的字段有:主键(ID),老人id(elder_id)、身份证号、入住开始时间、入住结束时间、护理等级、入住床位、状态(0:已入住、1:已退住) 2,表中其他必要字段:排序编号、创建时间(create_time)、修改时间(update_time)、创建人(create_by)、修改人(update_by)、备注(remark)这些字段 3,表的主键都是自增的 4,请为每个字段都添加上comment 5,帮我给生成的表中插入一些示例数据
入住相关表结构设计 表结构的E-R图 通过需求分析,咱们可以得到以下信息
老人的基本信息 里面的所有字段,都可以存储到老人表中(elder表)
老人关联的家属 是一个列表
老人选择的入住配置 字段有很多,可以创建入住表来进行存储
签约办理 相关的内容可以存储到合同表中
整体的E-R图结构如下:
如果这么设计的话,其中的check_in表的字段就非常的多,将会带来以下问题
性能下降 ,字段越多,数据库在执行查询时需要处理的数据量就越大,插入和更新尤为明显
维护困难 :字段过多会使得表结构复杂,导致难以理解和维护,同时修改表结构也会变得更加复杂和耗时
存储空间增加 :每个额外的字段,即使大多数情况下为空或数据很少,也会占用额外存储空间,并可能导致数据库备份和恢复操作耗时增加
可扩展性差 :当表结构变得过于复杂时,添加新功能或进行其他类型的扩展可能会变得更加困难
为了缓解这些问题,可以考虑以下策略:
归档旧数据:将不经常访问的旧数据归档到单独的表中或数据库中
优化数据类型:确保使用适当的数据类型来存储数据,以减少存储空间的浪费并提高性能
垂直分割 :将表垂直分割成多个较小的表,每个表包含相关的字段集
什么是垂直分割 垂直分割表是数据库管理中常用的一种优化技术,它通过将一个大表中的列分割成多个小表,每个小表包含原始表的一部分列,以达到优化数据存储和访问效率的目的。以下是垂直分割表的优缺点总结:
优点
提高查询性能:通过减少表的宽度,可以加快查询速度,特别是在**涉及大量列的表 **中。当查询只需要访问部分列时,数据库系统只需扫描包含这些列的小表,减少了不必要的数据扫描量
减少I/O操作:在读取数据时,数据库系统可以只读取需要的列,而无需扫描整个表,从而减少了I/O操作的次数和数据传输量
缺点
增加复杂性:垂直分割表会增加数据库结构的复杂性 ,需要更多的管理和维护工作。例如,需要修改应用程序中的查询和更新操作,以适应新的表结构
事务处理复杂:垂直分割表后,事务的处理可能会变得更加复杂。因为事务可能需要跨越多个子表,这增加了事务管理的难度和开销
表连接操作增加:由于数据被分散到多个子表中,因此在查询时可能需要更多的表连接操作 ,这可能会增加CPU的开销和查询的响应时间
经过垂直分割之后,其中的入住表可以分割为两个表
其中列表查询所展示的字段可以认为是经常访问的字段,详情中查看的字段认为是访问较少的字段,由此可以得出:
经常访问的字段
访问较少的字段
ID主键
ID主键
老人姓名
入住ID
老人id(elder_id)
护理等级ID
身份证号
护理等级名称
入住开始时间
入住床位
入住结束时间
费用开始时间
护理等级名称
费用结束时间
入住床位
押金(元)
状态(0:已入住、1:已退住)
护理费用(元/月)
床位费用(元/月)
医保支付(元/月)
政府补贴(元/月)
其他费用(元/月)
其中在经常访问的字段中,有一些冗余 字段:
老人姓名(elder表中存在)
身份证号(elder表中存在)
护理等级名称(check_in_config表中存在)
入住床位(check_in_config表中存在)
在设计数据库表时,引入冗余字段 是一种常见的优化手段,主要目的:
冗余字段减少查询时的表连接操作,降低查询复杂性和执行时间
冗余字段简化查询逻辑 ,使查询语句更简洁易懂,易于维护和减少出错概率
最终表结构
上图中的四张表的关系都是一对一的关系
废弃了老人家属表,由于老人家属信息主要用于回显展示,可以以json格式存储到入住表中的remark字段中
根据之前的分析结合需求文档,可以使用AI协助创建表结构,共为三张表:
1 2 3 4 5 6 7 8 9 你是一个软件工程师,帮我生成MySQL的表结构,需求如下: 1,入住表(check_in),包含的字段有:主键(ID),老人姓名(elder_name)、老人id(elder_id)、身份证号(id_card_no)、入住开始时间、入住结束时间、护理等级名称(nursing_level_name)、入住床位、状态(0:已入住、1:已退住) 2,入住配置表(check_in_config),包含的字段有:主键(ID),入住表ID、护理等级ID(nursing_level_id)、护理等级名称(nursing_level_name)、费用开始时间、费用结束时间、押金(元)、护理费用(元/月)、床位费用(元/月)、医保支付(元/月)、政府补贴(元/月)、其他费用(元/月) 其他要求: 1,表中其他必要字段:排序编号、创建时间(create_time)、修改时间(update_time)、创建人(create_by)、修改人(update_by)、备注(remark)这些字段 2,入住表和入住配置表是一对一的关系,不需要设置外键约束 3,表的主键都是自增的,类型为bigint 4,请为每个字段都添加上comment 5,帮我给生成的表中插入一些示例数据
最终生成的SQL语句为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 CREATE TABLE `check_in` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID' , `elder_name` VARCHAR (50 ) NOT NULL COMMENT '老人姓名' , `elder_id` BIGINT NOT NULL COMMENT '老人ID' , `id_card_no` CHAR (18 ) NOT NULL COMMENT '身份证号' , `start_date` DATE NOT NULL COMMENT '入住开始时间' , `end_date` DATE COMMENT '入住结束时间' , `nursing_level_name` VARCHAR (50 ) NOT NULL COMMENT '护理等级名称' , `bed_number` VARCHAR (50 ) NOT NULL COMMENT '入住床位' , `status` TINYINT NOT NULL DEFAULT '0' COMMENT '状态 (0: 已入住, 1: 已退住)' , `sort_order` INT NOT NULL DEFAULT '0' COMMENT '排序编号' , `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' , `update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间' , `create_by` VARCHAR (50 ) COMMENT '创建人' , `update_by` VARCHAR (50 ) COMMENT '修改人' , `remark` VARCHAR (255 ) COMMENT '备注' , PRIMARY KEY (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_unicode_ci; CREATE TABLE `check_in_config` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID' , `check_in_id` BIGINT NOT NULL COMMENT '入住表ID' , `nursing_level_id` BIGINT NOT NULL COMMENT '护理等级ID' , `nursing_level_name` VARCHAR (50 ) NOT NULL COMMENT '护理等级名称' , `fee_start_date` DATE NOT NULL COMMENT '费用开始时间' , `fee_end_date` DATE COMMENT '费用结束时间' , `deposit` DECIMAL (10 , 2 ) NOT NULL COMMENT '押金(元)' , `nursing_fee` DECIMAL (10 , 2 ) NOT NULL COMMENT '护理费用(元/月)' , `bed_fee` DECIMAL (10 , 2 ) NOT NULL COMMENT '床位费用(元/月)' , `insurance_payment` DECIMAL (10 , 2 ) NOT NULL COMMENT '医保支付(元/月)' , `government_subsidy` DECIMAL (10 , 2 ) NOT NULL COMMENT '政府补贴(元/月)' , `other_fees` DECIMAL (10 , 2 ) NOT NULL COMMENT '其他费用(元/月)' , `sort_order` INT NOT NULL DEFAULT '0' COMMENT '排序编号' , `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' , `update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间' , `create_by` VARCHAR (50 ) COMMENT '创建人' , `update_by` VARCHAR (50 ) COMMENT '修改人' , `remark` VARCHAR (255 ) COMMENT '备注' , PRIMARY KEY (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_unicode_ci;
老人表已经存在了,接下来使用AI生成合同表 ,提示词如下:
1 2 3 4 5 6 你是一个软件工程师,帮我生成MySQL的表结构,需求如下: 1,合同表(contract),包含的字段有:主键(ID),老人id(elder_id)、合同名称、合同编号、协议地址、丙方手机号、丙方姓名、老人姓名、开始时间、结束时间、状态(status 0:未生效,1:已生效,2:已过期, 3:已失效)、签约日期、解除提交人、解除日期、解除协议地址 2,表中其他必要字段:排序编号、创建时间(create_time)、修改时间(update_time)、创建人(create_by)、修改人(update_by)、备注(remark)这些字段,这些字段默认为null 3,表的主键都是自增的,类型为bigint 4,请为每个字段都添加上comment 5,帮我给生成的表中插入一些示例数据
最终生成的SQL语句为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 CREATE TABLE `contract` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID' , `elder_id` INT NOT NULL COMMENT '老人ID' , `contract_name` VARCHAR (100 ) NOT NULL COMMENT '合同名称' , `contract_number` VARCHAR (50 ) NOT NULL COMMENT '合同编号' , `agreement_path` VARCHAR (255 ) NOT NULL COMMENT '协议地址(文件路径或URL)' , `third_party_phone` VARCHAR (20 ) NOT NULL COMMENT '丙方手机号' , `third_party_name` VARCHAR (50 ) NOT NULL COMMENT '丙方姓名' , `elder_name` VARCHAR (50 ) NOT NULL COMMENT '老人姓名' , `start_date` DATE NOT NULL COMMENT '开始时间' , `end_date` DATE NOT NULL COMMENT '结束时间' , `status` TINYINT NOT NULL DEFAULT '0' COMMENT '状态 (0: 未生效, 1: 已生效, 2: 已过期, 3: 已失效)' , `sign_date` DATE NOT NULL COMMENT '签约日期' , `termination_submitter` VARCHAR (50 ) COMMENT '解除提交人' , `termination_date` DATE COMMENT '解除日期' , `termination_agreement_path` VARCHAR (255 ) COMMENT '解除协议地址(文件路径或URL)' , `sort_order` INT NOT NULL DEFAULT '0' COMMENT '排序编号' , `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' , `update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间' , `create_by` VARCHAR (50 ) COMMENT '创建人' , `update_by` VARCHAR (50 ) COMMENT '修改人' , `remark` VARCHAR (255 ) COMMENT '备注' , PRIMARY KEY (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_unicode_ci;
其他修改:由于AI生成的字段都要求是必填的,这样在后期操作数据的时候有很多的限制,咱们可以通过数据库连接工具调整一下
重点调整的字段:sort_order、create_time、update_time、create_by、update_by、remark
接口设计 接口四要素 搞明白需求之后,下面就可以来设计接口了,一个接口包含了四个基本要素,分别是:请求路径、请求方式、接口入参、接口出参
请求路径 命名:以模块名称进行区分(英文)
请求方式(需要符合restful风格)
查询 GET
新增 POST
修改 PUT
删除 DELETE
接口入参
路径中的参数
问号传参—->后端形参接收
path传参—->后端PathVariable注解接收
请求体中的参数
接口出参
统一格式 {code:200,msg:"成功",data:{}}
数据封装,一般为VO
接口测试 测试工具有很多,以下几个是比较常见的接口测试工具
Postman
ApiPost
Apifox
Swagger 在线接口文档
Knife4j 对swagger的增强,可生成离线接口文档
使用postman或者apifox工具测试接口,需要知道明确的接口信息
入住相关的接口 接口文档:入退管理-接口文档
入住办理这个模块中共6个接口,分别是:
分页查询入住列表
请求方式:GET
参数:分页条件查询(elderName、idNumber、pageNum、pageSize)
响应结果:参考若依框架定义的分页对象:{total:120,rows:[],code:200,msg:成功或失败}
查询所有护理等级信息
选择护理等级之后,可以自动填充护理费用
根据床位状态查询所有楼层数据
请求方式:GET
参数:status(比较通用,只查询未入住的床位)
响应结果:参考若依框架定义的普通返回对象:{msg:成功或失败,code:200,data:[]}
查询房间数据(楼层、房间、价格)
申请入住
查询入住详情
接口开发 通过若依的代码生成,根据之前创建的四张表(elder、contract、check_in、check_in_config)生成代码,生成前先确保生成配置是否正确
生成完毕后,不需要调整前端代码 ,只要将生成的后端代码移动到当前项目中,以下是提供的测试数据:
1 2 3 4 INSERT INTO `check_in`(`end_date`,`id_card_no`,`nursing_level_name`,`create_time`,`remark`,`elder_id`,`create_by`,`update_time`,`elder_name`,`id`,`bed_number`,`sort_order`,`start_date`,`status`) VALUES ('2024-09-30' ,'132123196712131234' ,'测试护理等级' ,'2024-08-27 16:43:20.0' ,'[{"kinship":"1","name":"13211223322","phone":"13211223322"}]' ,325 ,'1' ,'2024-08-27 08:43:19.0' ,'张三' ,1 ,'104-1' ,0 ,'2024-08-27' ,0 );INSERT INTO `check_in`(`end_date`,`id_card_no`,`nursing_level_name`,`create_time`,`remark`,`elder_id`,`create_by`,`update_time`,`elder_name`,`id`,`bed_number`,`sort_order`,`start_date`,`status`) VALUES ('2024-09-30' ,'132123196712131239' ,'1号护理计划' ,'2024-08-27 16:50:09.0' ,'[{"kinship":"0","name":"李天","phone":"13222334439"}]' ,326 ,'1' ,'2024-08-27 08:50:08.0' ,'李天龙' ,2 ,'104-2' ,0 ,'2024-08-27' ,0 );INSERT INTO `check_in`(`end_date`,`id_card_no`,`nursing_level_name`,`create_time`,`remark`,`elder_id`,`create_by`,`update_time`,`elder_name`,`id`,`bed_number`,`sort_order`,`start_date`,`status`) VALUES ('2024-10-31' ,'132123195612132345' ,'2号护理等级' ,'2024-09-12 18:51:36.0' ,'[{"kinship":"1","name":"小李","phone":"13212349900"}]' ,327 ,'1' ,'2024-09-12 18:51:36.0' ,'老李' ,3 ,'101-2' ,0 ,'2024-09-12' ,0 );INSERT INTO `check_in`(`end_date`,`id_card_no`,`nursing_level_name`,`create_time`,`remark`,`elder_id`,`create_by`,`update_time`,`elder_name`,`id`,`bed_number`,`sort_order`,`start_date`,`status`) VALUES ('2024-10-31' ,'410725196904056698' ,'2号护理等级' ,'2024-09-12 19:10:23.0' ,'[{"kinship":"0","name":"老王","phone":"15100000002"}]' ,328 ,'1' ,'2024-09-12 19:10:23.0' ,'老李头儿' ,4 ,'101-1' ,0 ,'2024-09-12' ,0 );
插入数据后,重启项目并访问入住办理页面
查询所有护理等级列表 控制层 基于接口文档的定义,在NursingLevelController定义新的方法,如下:
1 2 3 4 5 6 @ApiOperation(value = "获取所有护理等级") @GetMapping("/all") public R<List<NursingLevel>> listAll () { List<NursingLevel> list = nursingLevelService.listAll(); return R.ok(list); }
Service层 1 2 3 4 5 List<NursingLevel> listAll () ;
Service层实现类 1 2 3 4 5 6 7 8 9 10 11 12 @Override public List<NursingLevel> listAll () { LambdaQueryWrapper<NursingLevel> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(NursingLevel::getStatus, 1 ); List<NursingLevel> list = list(queryWrapper); return list; }
根据床位状态查询所有楼层数据 思路说明 最终的效果图如下:
结合接口文档的数据结构 ,这个接口需要查询三张表的数据,分别是楼层表、房间表、床位表 ,它们的关系如下:
控制层 在FloorController中定义新的方法,如下:
1 2 3 4 5 6 @GetMapping("/getRoomAndBedByBedStatus/{status}") @ApiOperation("按照状态查询楼层房间床位-树形结构") public R<List<TreeVo>> getRoomAndBedByBedStatus (@ApiParam(value = "床位状态(未入住0, 已入住1)", required = true) @PathVariable("status") Integer status) { List<TreeVo> list = floorService.getRoomAndBedByBedStatus(status); return R.ok(list); }
TreeVo(需要结合着接口文档的数据结构来编写vo类)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package com.zzyl.nursing.vo;import io.swagger.annotations.ApiModel;import io.swagger.annotations.ApiModelProperty;import lombok.Data;import java.util.List;@Data @ApiModel("树形结构VO") public class TreeVo { @ApiModelProperty(value = "菜单ID") private String value; @ApiModelProperty(value = "菜单名称") private String label; @ApiModelProperty(value = "子菜单") private List<TreeVo> children; }
业务层 在IFloorService中定义新的方法:
1 2 3 4 5 6 List<TreeVo> getRoomAndBedByBedStatus (Integer status) ;
实现方法:
1 2 3 4 5 6 7 8 9 10 @Override public List<TreeVo> getRoomAndBedByBedStatus (Integer status) { return floorMapper.getRoomAndBedByBedStatus(status); }
持久层(重点) 在FloorMapper中定义查询的方法
1 List<TreeVo> getRoomAndBedByBedStatus (Integer status) ;
xml映射文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <resultMap id ="TreeVoResultMap" type ="com.zzyl.nursing.vo.TreeVo" > <id column ="fid" property ="value" > </id > <result column ="name" property ="label" > </result > <collection property ="children" ofType ="com.zzyl.nursing.vo.TreeVo" > <id column ="rid" property ="value" > </id > <result column ="code" property ="label" > </result > <collection property ="children" ofType ="com.zzyl.nursing.vo.TreeVo" > <id column ="bid" property ="value" > </id > <result column ="bed_number" property ="label" > </result > </collection > </collection > </resultMap > <select id ="getRoomAndBedByBedStatus" resultMap ="TreeVoResultMap" > select f.id fid, f.name, r.id rid, r.code, b.id bid, b.bed_number from floor f left join room r on f.id = r.floor_id left join bed b on r.id = b.room_id where b.bed_status = #{status} order by f.id, r.id, b.id </select >
根据房间id查询房间数据(楼层、房间、价格) 思路说明 效果图:
当选择了某一个床位之后,需要根据床位所在的房间,查询所匹配的价格,结合接口文档的返回结果来分析,这里面涉及到了3张表:
控制层 在RoomController中定义新的方法,如下:
1 2 3 4 5 6 @GetMapping("/one/{id}") @ApiOperation("按照房间id查询楼层、房间、价格") public R<RoomVo> getRoomById (@ApiParam(value = "房间ID", required = true) @PathVariable("id") Long id) { RoomVo roomVo = roomService.getRoomById(id); return R.ok(roomVo); }
RoomVo在代码中已提供,符合接口返回的要求
业务层 在IRoomService中定义新的方法:
1 2 3 4 5 6 RoomVo getRoomById (Long id) ;
实现方法:
1 2 3 4 5 6 7 8 9 10 @Override public RoomVo getRoomById (Long id) { return roomMapper.getRoomById(id); }
持久层(重点) 在RoomMapper中定义新的方法,如下:
1 RoomVo getRoomById (Long id) ;
xml映射文件:
1 2 3 4 5 6 <select id ="getRoomById" resultType ="com.zzyl.nursing.vo.RoomVo" > select f.name floorName, f.id floorId, r.id roomId, r.code, rt.price from floor f left join room r on f.id = r.floor_id left join room_type rt on rt.name = r.type_name where r.id = #{id} </select >
申请入住 控制层 在CheckInController中定义申请入住的方法
1 2 3 4 5 @PostMapping("/apply") public AjaxResult apply (@RequestBody CheckInApplyDto dto) { checkInService.apply(dto); return AjaxResult.success(); }
基于接口的入参分析,需要定义多个dto来接收参数。可以自己根据接口文档定义类来接收数据,也可以使用AI大模型,基于json格式的请求数据生成java类来接收数据
基本的结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class CheckInApplyDto { private CheckInElderDto checkInElderDto; private List<ElderFamilyDto> elderFamilyDtoList; private CheckInConfigDto checkInConfigDto; private CheckInContractDto checkInContractDto; }
注意:属性名称要与json结构中保持一致
业务层(重点) 在ICheckInService中定义新的方法,如下:
1 2 3 4 5 void apply (CheckInApplyDto checkInApplyDto) ;
思路说明
申请入住的流程较长,涉及到多张表的操作,流程如下:
接口添加缓存 下图分析了各个接口的情况,重点标明了哪些接口推荐使用缓存
接口名称
是否需要使用缓存
理由
分页查询入住列表
否
不涉及多表,量也不大
查询所有护理等级
是
组件加载需要查询,有效率要求,并且是检索表中所有数据
根据床位状态查询树形数据
否
经常性的有写操作 (比如:老人入住后需要更新床位状态)
查询房间价格等数据
是
可以添加,涉及到多表查询
申请入住
否
新增,不是查询
查询入住详情
否
并非高频访问
护理等级添加缓存 在护理等级中添加缓存基本思路如下:
查询所有护理等级的时候,先到缓存查,缓存有则返回,缓存没有则查数据库,同时放到缓存中
增删改操作之后,需要删除缓存
需要修改NursingLevelServiceImpl类中的方法,详细代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 package com.zzyl.nursing.service.impl;import java.util.List;import com.zzyl.common.utils.DateUtils;import com.zzyl.nursing.vo.NursingLevelVo;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Service;import com.zzyl.nursing.mapper.NursingLevelMapper;import com.zzyl.nursing.domain.NursingLevel;import com.zzyl.nursing.service.INursingLevelService;@Service public class NursingLevelServiceImpl implements INursingLevelService { @Autowired private NursingLevelMapper nursingLevelMapper; @Autowired private RedisTemplate<Object, Object> redisTemplate; private static final String CACHE_KEY_PREFIX = "nursingLevel:all" ; @Override public NursingLevel selectNursingLevelById (Long id) { return nursingLevelMapper.selectNursingLevelById(id); } @Override public List<NursingLevelVo> selectNursingLevelList (NursingLevel nursingLevel) { return nursingLevelMapper.selectNursingLevelList(nursingLevel); } @Override public int insertNursingLevel (NursingLevel nursingLevel) { nursingLevel.setCreateTime(DateUtils.getNowDate()); int flag = nursingLevelMapper.insertNursingLevel(nursingLevel); deleteCache(); return flag; } private void deleteCache () { redisTemplate.delete(CACHE_KEY_PREFIX); } @Override public int updateNursingLevel (NursingLevel nursingLevel) { nursingLevel.setUpdateTime(DateUtils.getNowDate()); int flag = nursingLevelMapper.updateNursingLevel(nursingLevel); deleteCache(); return flag; } @Override public int deleteNursingLevelByIds (Long[] ids) { int flag = nursingLevelMapper.deleteNursingLevelByIds(ids); deleteCache(); return flag; } @Override public int deleteNursingLevelById (Long id) { int flag = nursingLevelMapper.deleteNursingLevelById(id); deleteCache(); return flag; } @Override public List<NursingLevel> listAll () { List<NursingLevel> list = (List<NursingLevel>) redisTemplate.opsForValue().get(CACHE_KEY_PREFIX); if (list != null && list.size() > 0 ){ return list; } list = nursingLevelMapper.listAll(); redisTemplate.opsForValue().set(CACHE_KEY_PREFIX, list); return list; } }
智能评估 需求分析 健康评估 是指老人办理入住前需上传体检报告,由AI自动进行分析,并对报告进行总结,同时给出合理的建议;
下图是健康评估列表页,展示了经过健康评估的老人列表
当点击了上传体检报告 按钮之后会弹窗,效果如下,需要输入信息,需要提前准备好老人的体检报告(PDF格式 )
点击确定 按钮之后,会使用AI对老人的健康报告进行评估
下图是健康评估的详情页面,是使用AI分析后的结果页,给了很多的数据,可以让护理员或销售人员来查看老人的健康状况,进一步更好的服务老人或者给老人推荐一些护理服务
表结构说明 基于需求原型,设计出的健康评估表(health_assessment)如下:
特别字段说明:
abnormal_analysis(异常分析) 这个字段是字符串类型,会把异常数据转换为json进行存储
建表语句:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 CREATE TABLE "health_assessment" ( "id" bigint NOT NULL AUTO_INCREMENT COMMENT '主键' , "elder_name" varchar (255 ) DEFAULT NULL COMMENT '老人姓名' , "id_card" varchar (255 ) DEFAULT NULL COMMENT '身份证号' , "birth_date" datetime DEFAULT NULL COMMENT '出生日期' , "age" int DEFAULT NULL COMMENT '年龄' , "gender" int DEFAULT NULL COMMENT '性别(0:男,1:女)' , "health_score" varchar (255 ) DEFAULT NULL COMMENT '健康评分' , "risk_level" varchar (255 ) DEFAULT NULL COMMENT '危险等级(健康, 提示, 风险, 危险, 严重危险)' , "suggestion_for_admission" int DEFAULT NULL COMMENT '是否建议入住(0:建议,1:不建议)' , "nursing_level_name" varchar (255 ) DEFAULT NULL COMMENT '推荐护理等级' , "admission_status" int DEFAULT NULL COMMENT '入住情况(0:已入住,1:未入住)' , "total_check_date" varchar (64 ) DEFAULT NULL COMMENT '总检日期' , "physical_exam_institution" varchar (255 ) DEFAULT NULL COMMENT '体检机构' , "physical_report_url" varchar (255 ) DEFAULT NULL COMMENT '体检报告URL链接' , "assessment_time" datetime DEFAULT NULL COMMENT '评估时间' , "report_summary" text COMMENT '报告总结' , "disease_risk" text COMMENT '疾病风险' , "abnormal_analysis" text COMMENT '异常分析' , "system_score" varchar (255 ) DEFAULT NULL COMMENT '健康系统分值' , "create_by" varchar (255 ) DEFAULT NULL COMMENT '创建者' , "create_time" datetime DEFAULT NULL COMMENT '创建时间' , "update_by" varchar (255 ) DEFAULT NULL COMMENT '更新者' , "update_time" datetime DEFAULT NULL COMMENT '更新时间' , "remark" text COMMENT '备注' , PRIMARY KEY ("id") ) ENGINE= InnoDB AUTO_INCREMENT= 7 DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT= '健康评估表' ;
接口说明 基于需求说明,在健康评估这个模块中,共有4个接口需要开发,分别是
前端代码已经全部提供(zznursing-vue3 ),所以要结合接口文档开发后端代码
关于健康评估的接口文档,参考链接:入退管理-接口文档
实现方案 整体实现流程如下:
读取PDF文件内容 Apache PDFBox库是一个用于处理PDF文档的开源Java工具。该项目允许创建新的PDF文档,编辑现有的文档,以及从文档中提取内容
导入对应的依赖,在zzyl-common模块中导入以下依赖:
1 2 3 4 5 <dependency > <groupId > org.apache.pdfbox</groupId > <artifactId > pdfbox</artifactId > <version > 2.0.24</version > </dependency >
创建工具类,通过pdf文件来读取内容,工具类路径:zzyl-common模块下的com.zzyl.common.utils.PDFUtil
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package com.zzyl.common.utils;import org.apache.pdfbox.pdmodel.PDDocument;import org.apache.pdfbox.text.PDFTextStripper;import java.io.File;import java.io.IOException;import java.io.InputStream;public class PDFUtil { public static String pdfToString (InputStream inputStream) { PDDocument document = null ; try { document = PDDocument.load(inputStream); PDFTextStripper pdfStripper = new PDFTextStripper (); String text = pdfStripper.getText(document); return text; } catch (IOException e) { e.printStackTrace(); } finally { if (document != null ) { try { document.close(); inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } return null ; } }
百度千帆大模型 注册地址:千帆大模型平台
应用创建地址:百度智能云控制台
使用流程参考:飞书云文档
大模型API说明 当前需求中,prompt是比较多的,也就是说需要更多的token,对话Chat V2支持的模型列表如下:
千帆ModelBuilder文档首页-百度智能云
经过综合评估,本次采用的是ERNIE-4.0-8K
官方地址:ERNIE-4.0-8K - ModelBuilder
ERNIE 4.0 是百度自研的旗舰级超大规模⼤语⾔模型,相较ERNIE 3.5实现了模型能力全面升级,广泛适用于各领域复杂任务场景;支持自动对接百度搜索插件,保障问答信息时效,支持5K tokens输入+2K tokens输出
集成大模型
在zzyl-common模块下新增以下依赖:
1 2 3 4 5 <dependency > <groupId > com.openai</groupId > <artifactId > openai-java</artifactId > <version > 0.22.0</version > # 按需更新版本号 </dependency >
由于openai用到了kotlin,而这个依赖自带的kotlin版本和项目中的kotlin版本冲突了,所以,还需要排除一些依赖,然后重新引入一个统一的版本,才能正常使用
在父工程pom.xml文件中统一管理依赖的版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > com.zzyl</groupId > <artifactId > zzyl</artifactId > <version > 3.8.9</version > <name > zzyl</name > <url > http://www.ruoyi.vip</url > <description > 中州养老</description > <properties > <zzyl.version > 3.8.9</zzyl.version > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <project.reporting.outputEncoding > UTF-8</project.reporting.outputEncoding > <java.version > 11</java.version > <maven-jar-plugin.version > 3.1.1</maven-jar-plugin.version > <spring-boot.version > 2.5.15</spring-boot.version > <druid.version > 1.2.23</druid.version > <bitwalker.version > 1.21</bitwalker.version > <swagger.version > 3.0.0</swagger.version > <kaptcha.version > 2.3.3</kaptcha.version > <pagehelper.boot.version > 1.4.7</pagehelper.boot.version > <fastjson.version > 2.0.53</fastjson.version > <oshi.version > 6.8.1</oshi.version > <commons.io.version > 2.19.0</commons.io.version > <poi.version > 4.1.2</poi.version > <velocity.version > 2.3</velocity.version > <jwt.version > 0.9.1</jwt.version > <tomcat.version > 9.0.105</tomcat.version > <logback.version > 1.2.13</logback.version > <spring-security.version > 5.7.12</spring-security.version > <spring-framework.version > 5.3.39</spring-framework.version > <lombok.version > 1.18.22</lombok.version > <mybatis-plus-spring-boot.version > 3.5.2</mybatis-plus-spring-boot.version > <aliyun.sdk.oss > 3.17.4</aliyun.sdk.oss > <hutool-all.version > 5.8.10</hutool-all.version > <openai-java.version > 2.8.1</openai-java.version > <kotlin.version > 1.9.23</kotlin.version > <jackson.version > 2.16.1</jackson.version > <okhttp.version > 4.12.0</okhttp.version > </properties > <dependencyManagement > <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-framework-bom</artifactId > <version > ${spring-framework.version}</version > <type > pom</type > <scope > import</scope > </dependency > <dependency > <groupId > org.springframework.security</groupId > <artifactId > spring-security-bom</artifactId > <version > ${spring-security.version}</version > <type > pom</type > <scope > import</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-dependencies</artifactId > <version > ${spring-boot.version}</version > <type > pom</type > <scope > import</scope > </dependency > <dependency > <groupId > ch.qos.logback</groupId > <artifactId > logback-core</artifactId > <version > ${logback.version}</version > </dependency > <dependency > <groupId > ch.qos.logback</groupId > <artifactId > logback-classic</artifactId > <version > ${logback.version}</version > </dependency > <dependency > <groupId > org.apache.tomcat.embed</groupId > <artifactId > tomcat-embed-core</artifactId > <version > ${tomcat.version}</version > </dependency > <dependency > <groupId > org.apache.tomcat.embed</groupId > <artifactId > tomcat-embed-el</artifactId > <version > ${tomcat.version}</version > </dependency > <dependency > <groupId > org.apache.tomcat.embed</groupId > <artifactId > tomcat-embed-websocket</artifactId > <version > ${tomcat.version}</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > ${druid.version}</version > </dependency > <dependency > <groupId > eu.bitwalker</groupId > <artifactId > UserAgentUtils</artifactId > <version > ${bitwalker.version}</version > </dependency > <dependency > <groupId > com.github.pagehelper</groupId > <artifactId > pagehelper-spring-boot-starter</artifactId > <version > ${pagehelper.boot.version}</version > </dependency > <dependency > <groupId > com.github.oshi</groupId > <artifactId > oshi-core</artifactId > <version > ${oshi.version}</version > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-boot-starter</artifactId > <version > ${swagger.version}</version > <exclusions > <exclusion > <groupId > io.swagger</groupId > <artifactId > swagger-models</artifactId > </exclusion > </exclusions > </dependency > <dependency > <groupId > commons-io</groupId > <artifactId > commons-io</artifactId > <version > ${commons.io.version}</version > </dependency > <dependency > <groupId > org.apache.poi</groupId > <artifactId > poi-ooxml</artifactId > <version > ${poi.version}</version > </dependency > <dependency > <groupId > org.apache.velocity</groupId > <artifactId > velocity-engine-core</artifactId > <version > ${velocity.version}</version > </dependency > <dependency > <groupId > com.alibaba.fastjson2</groupId > <artifactId > fastjson2</artifactId > <version > ${fastjson.version}</version > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > <version > ${jwt.version}</version > </dependency > <dependency > <groupId > pro.fessional</groupId > <artifactId > kaptcha</artifactId > <version > ${kaptcha.version}</version > </dependency > <dependency > <groupId > com.zzyl</groupId > <artifactId > zzyl-quartz</artifactId > <version > ${zzyl.version}</version > </dependency > <dependency > <groupId > com.zzyl</groupId > <artifactId > zzyl-generator</artifactId > <version > ${zzyl.version}</version > </dependency > <dependency > <groupId > com.zzyl</groupId > <artifactId > zzyl-framework</artifactId > <version > ${zzyl.version}</version > </dependency > <dependency > <groupId > com.zzyl</groupId > <artifactId > zzyl-system</artifactId > <version > ${zzyl.version}</version > </dependency > <dependency > <groupId > com.zzyl</groupId > <artifactId > zzyl-common</artifactId > <version > ${zzyl.version}</version > </dependency > <dependency > <groupId > com.zzyl</groupId > <artifactId > zzyl-nursing-platform</artifactId > <version > ${zzyl.version}</version > </dependency > <dependency > <groupId > com.zzyl</groupId > <artifactId > zzyl-oss</artifactId > <version > ${zzyl.version}</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > ${lombok.version}</version > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > ${mybatis-plus-spring-boot.version}</version > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-annotation</artifactId > <version > ${mybatis-plus-spring-boot.version}</version > </dependency > <dependency > <groupId > com.aliyun.oss</groupId > <artifactId > aliyun-sdk-oss</artifactId > <version > ${aliyun.sdk.oss}</version > </dependency > <dependency > <groupId > cn.hutool</groupId > <artifactId > hutool-all</artifactId > <version > ${hutool-all.version}</version > </dependency > <dependency > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-bom</artifactId > <version > ${kotlin.version}</version > <scope > import</scope > <type > pom</type > </dependency > <dependency > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-stdlib</artifactId > <version > ${kotlin.version}</version > </dependency > <dependency > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-reflect</artifactId > <version > ${kotlin.version}</version > </dependency > <dependency > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-stdlib-jdk8</artifactId > <version > ${kotlin.version}</version > </dependency > <dependency > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-stdlib-jdk7</artifactId > <version > ${kotlin.version}</version > </dependency > <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-databind</artifactId > <version > ${jackson.version}</version > </dependency > <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-core</artifactId > <version > ${jackson.version}</version > </dependency > <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-annotations</artifactId > <version > ${jackson.version}</version > </dependency > <dependency > <groupId > com.fasterxml.jackson.datatype</groupId > <artifactId > jackson-datatype-jdk8</artifactId > <version > ${jackson.version}</version > </dependency > <dependency > <groupId > com.fasterxml.jackson.datatype</groupId > <artifactId > jackson-datatype-jsr310</artifactId > <version > ${jackson.version}</version > </dependency > <dependency > <groupId > com.fasterxml.jackson.module</groupId > <artifactId > jackson-module-kotlin</artifactId > <version > ${jackson.version}</version > </dependency > <dependency > <groupId > com.openai</groupId > <artifactId > openai-java</artifactId > <version > ${openai-java.version}</version > </dependency > <dependency > <groupId > com.squareup.okhttp3</groupId > <artifactId > okhttp</artifactId > <version > ${okhttp.version}</version > </dependency > </dependencies > </dependencyManagement > <modules > <module > zzyl-admin</module > <module > zzyl-framework</module > <module > zzyl-system</module > <module > zzyl-quartz</module > <module > zzyl-generator</module > <module > zzyl-common</module > <module > zzyl-nursing-platform</module > <module > zzyl-oss</module > </modules > <packaging > pom</packaging > <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <version > 3.1</version > <configuration > <source > ${java.version}</source > <target > ${java.version}</target > <encoding > ${project.build.sourceEncoding}</encoding > </configuration > </plugin > </plugins > </build > <repositories > <repository > <id > public</id > <name > aliyun nexus</name > <url > https://maven.aliyun.com/repository/public</url > <releases > <enabled > true</enabled > </releases > </repository > </repositories > <pluginRepositories > <pluginRepository > <id > public</id > <name > aliyun nexus</name > <url > https://maven.aliyun.com/repository/public</url > <releases > <enabled > true</enabled > </releases > <snapshots > <enabled > false</enabled > </snapshots > </pluginRepository > </pluginRepositories > </project >
接下来,在common模块的pom.xml文件中引入openai-java的依赖,并排除有版本冲突的依赖然后重新引入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <parent > <artifactId > zzyl</artifactId > <groupId > com.zzyl</groupId > <version > 3.8.9</version > </parent > <modelVersion > 4.0.0</modelVersion > <artifactId > zzyl-common</artifactId > <description > common通用工具 </description > <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context-support</artifactId > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency > <dependency > <groupId > com.github.pagehelper</groupId > <artifactId > pagehelper-spring-boot-starter</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-validation</artifactId > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-lang3</artifactId > </dependency > <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-databind</artifactId > </dependency > <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-core</artifactId > </dependency > <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-annotations</artifactId > </dependency > <dependency > <groupId > com.fasterxml.jackson.datatype</groupId > <artifactId > jackson-datatype-jdk8</artifactId > </dependency > <dependency > <groupId > com.fasterxml.jackson.datatype</groupId > <artifactId > jackson-datatype-jsr310</artifactId > </dependency > <dependency > <groupId > com.fasterxml.jackson.module</groupId > <artifactId > jackson-module-kotlin</artifactId > <exclusions > <exclusion > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-reflect</artifactId > </exclusion > <exclusion > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-stdlib</artifactId > </exclusion > <exclusion > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-stdlib-jdk7</artifactId > </exclusion > </exclusions > </dependency > <dependency > <groupId > com.alibaba.fastjson2</groupId > <artifactId > fastjson2</artifactId > </dependency > <dependency > <groupId > commons-io</groupId > <artifactId > commons-io</artifactId > </dependency > <dependency > <groupId > org.apache.poi</groupId > <artifactId > poi-ooxml</artifactId > </dependency > <dependency > <groupId > org.yaml</groupId > <artifactId > snakeyaml</artifactId > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > </dependency > <dependency > <groupId > javax.xml.bind</groupId > <artifactId > jaxb-api</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-pool2</artifactId > </dependency > <dependency > <groupId > eu.bitwalker</groupId > <artifactId > UserAgentUtils</artifactId > </dependency > <dependency > <groupId > javax.servlet</groupId > <artifactId > javax.servlet-api</artifactId > </dependency > <dependency > <groupId > com.github.xiaoymin</groupId > <artifactId > knife4j-spring-boot-starter</artifactId > <version > 3.0.3</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <exclusions > <exclusion > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-stdlib</artifactId > </exclusion > <exclusion > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-reflect</artifactId > </exclusion > <exclusion > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-stdlib-jdk7</artifactId > </exclusion > <exclusion > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-stdlib-jdk8</artifactId > </exclusion > </exclusions > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-annotation</artifactId > </dependency > <dependency > <groupId > cn.hutool</groupId > <artifactId > hutool-all</artifactId > </dependency > <dependency > <groupId > org.apache.pdfbox</groupId > <artifactId > pdfbox</artifactId > <version > 2.0.24</version > </dependency > <dependency > <groupId > com.squareup.okhttp3</groupId > <artifactId > okhttp</artifactId > </dependency > <dependency > <groupId > com.openai</groupId > <artifactId > openai-java</artifactId > <exclusions > <exclusion > <groupId > com.squareup.okhttp3</groupId > <artifactId > okhttp</artifactId > </exclusion > <exclusion > <groupId > com.squareup.okio</groupId > <artifactId > okio</artifactId > </exclusion > <exclusion > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-stdlib</artifactId > </exclusion > <exclusion > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-stdlib-jdk7</artifactId > </exclusion > <exclusion > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-stdlib-jdk8</artifactId > </exclusion > <exclusion > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-reflect</artifactId > </exclusion > </exclusions > </dependency > <dependency > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-stdlib-jdk7</artifactId > </dependency > <dependency > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-stdlib</artifactId > </dependency > <dependency > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-reflect</artifactId > </dependency > <dependency > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-stdlib-jdk8</artifactId > <exclusions > <exclusion > <groupId > org.jetbrains.kotlin</groupId > <artifactId > kotlin-stdlib-jdk7</artifactId > </exclusion > </exclusions > </dependency > </dependencies > </project >
参考官方示例(https://cloud.baidu.com/doc/qianfan-docs/s/nm9l6oc8e)编写一个main方法来测试即可,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.zzyl;import com.openai.client.OpenAIClient;import com.openai.client.okhttp.OpenAIOkHttpClient;import com.openai.models.ResponseFormatJsonObject;import com.openai.models.chat.completions.ChatCompletion;import com.openai.models.chat.completions.ChatCompletionCreateParams;public class QianfanAIModelTest { public static void main (String[] args) { OpenAIClient client = OpenAIOkHttpClient.builder() .apiKey("bce-v3/ALTAK-6H6k7kuLyTmZPBmOJ2ajC/2520d4ad36847715153ca819932f1d854bb9e71f" ) .baseUrl("https://qianfan.baidubce.com/v2/" ) .build(); ChatCompletionCreateParams params = ChatCompletionCreateParams.builder() .addUserMessage("你好" ) .model("deepseek-r1" ) .responseFormat(ChatCompletionCreateParams.ResponseFormat.ofJsonObject(ResponseFormatJsonObject.builder().build())) .build(); ChatCompletion chatCompletion = client.chat().completions().create(params); System.out.println(chatCompletion.choices().get(0 ).message().content().orElseGet(() -> "" )); } }
接口开发 基础代码准备 打开后台管理系统,咱们继续使用代码生成功能,来完成基础代码的准备,导入最新的表结构(健康评估表)
修改代码生成
生成包路径:com.zzyl.nursing
生成的模块名:nursing
生成的业务名:healthAssessment
上级菜单:不选择(由于目前已经提供了所有的前端和菜单的表结构,无需自己创建选择菜单)
配置完成后,下载代码到本地
特别注意 :只需要拷贝后端代码到idea中即可,不需要执行SQL ,不需要拷贝前端代码
当代码拷贝之后,就已经完成了两个接口的开发,分别是列表分页查询 和查看详情 。不过关于上传体检报告和智能评测还需要咱们自行实现。
将后端代码都拷贝到项目中后,可以执行下面这条SQL语句插入一条数据:
1 INSERT INTO `health_assessment`(`system_score`,`nursing_level_name`,`gender`,`total_check_date`,`create_time`,`disease_risk`,`birth_date`,`health_score`,`id_card`,`abnormal_analysis`,`physical_exam_institution`,`create_by`,`risk_level`,`assessment_time`,`suggestion_for_admission`,`admission_status`,`physical_report_url`,`elder_name`,`id`,`age`,`report_summary`) VALUES ('{"breathingSystem":100,"digestiveSystem":85,"endocrineSystem":90,"immuneSystem":95,"circulatorySystem":80,"urinarySystem":90,"motionSystem":100,"senseSystem":90}' ,'一级护理等级' ,0 ,'2023-10-10' ,'2024-10-10T01:07:42' ,'{"healthy":60,"caution":30,"risk":10,"danger":0,"severeDanger":0}' ,'1960-01-26T00:00' ,'80.5' ,'210102196001267626' ,'[{"conclusion":"心率略快","examinationItem":"心率","result":"92 次/分","referenceValue":"< 140","unit":"次/分","interpret":"心率稍高于理想范围,可能与情绪、活动或轻度心脏负担有关。","advice":"建议心电图检查及定期监测心率,保持情绪稳定,适量运动。"},{"conclusion":"轻度脂肪肝可能","examinationItem":"肝","result":"形态大小正常,实质回声略粗糙","referenceValue":"-","unit":"-","interpret":"肝脏实质回声略粗糙,提示可能有轻度脂肪肝,与饮食习惯、生活方式或遗传因素有关。","advice":"建议调整饮食结构,减少高脂食物摄入,增加运动,定期检查肝脏情况。"},{"conclusion":"慢性胆囊炎可能","examinationItem":"胆","result":"胆囊壁毛糙","referenceValue":"-","unit":"-","interpret":"胆囊壁毛糙,可能表示有慢性胆囊炎,可能与胆囊结石、感染或长期饮食不规律有关。","advice":"建议进一步检查胆囊情况,保持规律饮食,避免高脂食物。"},{"conclusion":"脾轻度增大","examinationItem":"脾","result":"轻度增大","referenceValue":"-","unit":"-","interpret":"脾脏轻度增大,可能与感染、血液系统疾病或自身免疫性疾病有关。","advice":"建议进一步检查脾脏,以确定增大的原因,并采取相应的治疗措施。"},{"conclusion":"右肾小囊肿可能","examinationItem":"肾","result":"右肾下极见一大小约 5mm 的无回声区","referenceValue":"-","unit":"mm","interpret":"右肾小囊肿,一般为良性病变,可能与肾小管憩室增多有关。","advice":"建议定期监测囊肿大小,若无症状且囊肿不增大,可暂不处理。"},{"conclusion":"前列腺形态略增大","examinationItem":"前列腺","result":"形态略增大","referenceValue":"-","unit":"-","interpret":"前列腺形态略增大,可能与前列腺增生有关,是老年男性常见病变。","advice":"建议进行PSA检查以排除前列腺肿瘤,并定期检查前列腺情况。"}]' ,'中州体检' ,'1' ,'caution' ,'2024-10-10T01:07:42' ,0 ,1 ,'https://java110-ai.oss-cn-beijing.aliyuncs.com/2024/10/af59dc76-0c01-4043-9706-b9ed1ec08984.pdf' ,'刘爱国' ,9 ,64 ,'体检报告中心率、肝脏、胆囊、脾脏、肾脏、前列腺共6项指标提示异常,综合这些临床指标和数据分析:循环系统、消化系统存在隐患,其中循环系统有“中危”风险;消化系统部位有“低危”风险。' );
这样就能看到列表分页查询 和查看详情 的内容了
设计Prompt 回顾一下Prompt的构成部分:
角色 :给 AI 定义一个最匹配任务的角色,比如:「你是一位软件工程师」「你是一位小学老师」
指示 :对任务进行描述
上下文 :给出与任务相关的其它背景信息(尤其在多轮交互中)
例子 :必要时给出举例,[实践证明例子对输出正确性有帮助]
输入 :任务的输入信息;在提示词中明确的标识出输入
输出 :输出的格式描述,以便后继模块自动解析模型的输出结果,比如(JSON、Java)
清楚了Prompt的构成部分之后,再来分析一下之前的需求,重点来看一下哪些字段需要AI来提供,如下图:
设计之后的Prompt提示词,这里涉及了大量的专业名词和健康指标,实际开发中需要找产品经理协助
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 请以一个专业医生的视角来分析这份体检报告,报告中包含了一些异常数据,我需要您对这些数据进行解读,并给出相应的健康建议。 体检内容如下: 内容略.... 要求: 1. 提取体检报告中的“总检日期”; 2. 通过临床医学、疾病风险评估模型和数据智能分析,给该用户的风险等级和健康指数给出结果。风险等级分为:健康、提示、风险、危险、严重危险。健康指数范围为0至100分; 3. 根据用户身体各项指标数据,详细说明该用户各项风险等级的占比是多少,最多保留两位小数。结论格式:该用户健康占比20.00%,提示占比20.00%,风险占比20%,危险占比20%,严重危险占比20%; 4. 对于体检报告中的异常数据,请列出(异常数据的结论、体检项目名称、检查结果、参考值、单位、异常解读、建议)这7字段。解读异常数据,解决这些数据可能代表的健康问题或风险。分析可能的原因,包括但不限于生活习惯、饮食习惯、遗传因素等。基于这些异常数据和可能的原因,请给出具体的健康建议,包括饮食调整、运动建议、生活方式改变以及是否需要进一步检查或治疗等。 结论格式:异常数据的结论:肥胖,体检项目名称:体重指数BMI,检查结果:29.2,参考值>24,单位:-。异常解读:体重超标包括超重与肥胖。体重指数(BMI)=体重(kg)/身⾼(m)的平⽅,BMI≥24为超重,BMI≥28为肥胖;男性腰围≥90cm和⼥性腰围≥85cm为腹型肥胖。体重超标是⼀种由多因素(如遗传、进⻝油脂较多、运动少、疾病等)引起的慢性代谢性疾病,尤其是肥胖,已经被世界卫⽣组织列为导致疾病负担的⼗⼤危险因素之⼀。AI建议:采取综合措施预防和控制体重,积极改变⽣活⽅式,宜低脂、低糖、⾼纤维素膳⻝,多⻝果蔬及菌藻类⻝物,增加有氧运动。若有相关疾病(如⾎脂异常、⾼⾎压、糖尿病等)应积极治疗。 5. 根据这个体检报告的内容,分别给人体的8大系统打分,每项满分为100分,8大系统分别为:呼吸系统、消化系统、内分泌系统、免疫系统、循环系统、泌尿系统、运动系统、感官系统 6. 给体检报告做一个总结,总结格式:体检报告中尿蛋⽩、癌胚抗原、⾎沉、空腹⾎糖、总胆固醇、⽢油三酯、低密度脂蛋⽩胆固醇、⾎清载脂蛋⽩B、动脉硬化指数、⽩细胞、平均红细胞体积、平均⾎红蛋⽩共12项指标提示异常,尿液常规共1项指标处于临界值,⾎脂、⾎液常规、尿液常规、糖类抗原、⾎清酶类等共43项指标提示正常,综合这些临床指标和数据分析:肾脏、肝胆、⼼脑⾎管存在隐患,其中⼼脑⾎管有“⾼危”⻛险;肾脏部位有“中危”⻛险;肝胆部位有“低危”⻛险。 输出要求: 最后,将以上结果输出为纯JSON格式,不要包含其他的文字说明,也不要出现Markdown语法相关的文字,所有的返回结果都是json,详细格式如下: { "totalCheckDate": "YYYY-MM-DD", "healthAssessment": { "riskLevel": "healthy/caution/risk/danger/severeDanger", "healthIndex": XX.XX }, "riskDistribution": { "healthy": XX.XX, "caution": XX.XX, "risk": XX.XX, "danger": XX.XX, "severeDanger": XX.XX }, "abnormalData": [ { "conclusion": "异常数据的结论", "examinationItem": "体检项目名称", "result": "检查结果", "referenceValue": "参考值", "unit": "单位", "interpret":"对于异常的结论进一步详细的说明", "advice":"针对于这一项的异常,给出一些健康的建议" } ], "systemScore": { "breathingSystem": XX, "digestiveSystem": XX, "endocrineSystem": XX, "immuneSystem": XX, "circulatorySystem": XX, "urinarySystem": XX, "motionSystem": XX, "senseSystem": XX }, "summarize": "体检报告的总结" }
上传体检报告 需求回顾 如下图,当点击了上传体检报告之后,弹窗需要输入信息,当选择了文件之后,会自动触发接口调用
参数有两个:身份证号和体检报告文件
思路回顾 如下图:
功能实现 文件需要上传到OSS中,所以需要在zzyl-nursing-platform模块中新增zzyl-oss的依赖
1 2 3 4 <dependency > <groupId > com.zzyl</groupId > <artifactId > zzyl-oss</artifactId > </dependency >
在HealthAssessmentController中定义新的方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Autowired private AliyunOSSOperator aliyunOSSOperator;@Autowired private RedisTemplate<String, String> redisTemplate;@ApiOperation("上传体检报告") @PostMapping("/upload") public AjaxResult uploadFile (MultipartFile file, String idCardNo) throws Exception{ try { String url = aliyunOSSOperator.upload(file.getBytes(), file.getOriginalFilename()); AjaxResult ajax = AjaxResult.success(); ajax.put("url" , url); ajax.put("fileName" , url); ajax.put("originalFilename" , file.getOriginalFilename()); String content = PDFUtil.pdfToString(file.getInputStream()); redisTemplate.opsForHash().put("healthReport" , idCardNo, content); return ajax; } catch (Exception e) { return AjaxResult.error(e.getMessage()); } }
智能评测 抽取大模型调用工具 提前将调用大模型的代码抽取成一个工具方法
一些可变的参数,在application.yml文件中定义,在application.yml文件中定义内容,如下:
1 2 3 4 5 6 baidu: qianfan: apiKey: xxxxxxxxx baseUrl: https://qianfan.baidubce.com/v2/ model: ernie-4.0-8k
定义properties类来方便读取配置,在zzyl-common模块下新增一个类com.zzyl.common.ai.BaiduAIProperties
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.zzyl.common.ai;import lombok.Data;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Configuration;@Data @Configuration @ConfigurationProperties(prefix = "baidu.qianfan") public class BaiduAIProperties { private String apiKey; private String baseUrl; private String model; }
参考之前集成千帆大模型的方式,在zzyl-common模块下新增一个类com.zzyl.common.ai.AIModelInvoker
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package com.zzyl.common.ai;import com.openai.client.OpenAIClient;import com.openai.client.okhttp.OpenAIOkHttpClient;import com.openai.models.ResponseFormatJsonObject;import com.openai.models.chat.completions.ChatCompletion;import com.openai.models.chat.completions.ChatCompletionCreateParams;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;@Component @Slf4j public class AIModelInvoker { @Autowired private BaiduAIProperties baiduAIProperties; public String qianfanInvoker (String prompt) { OpenAIClient client = OpenAIOkHttpClient.builder() .apiKey(baiduAIProperties.getApiKey()) .baseUrl(baiduAIProperties.getBaseUrl()) .build(); ChatCompletionCreateParams params = ChatCompletionCreateParams.builder() .addUserMessage(prompt) .model(baiduAIProperties.getModel()) .responseFormat(ChatCompletionCreateParams.ResponseFormat.ofJsonObject(ResponseFormatJsonObject.builder().build())) .build(); ChatCompletion chatCompletion = client.chat().completions().create(params); return chatCompletion.choices().get(0 ).message().content().orElseGet(() -> "" ); } }
修改控制层 找到HealthAssessmentController中的add方法的返回值类型为Long类型,用来接收Service层方法返回的主键id
1 2 3 4 5 6 7 8 9 10 11 12 @ApiOperation("新增健康评估") @PreAuthorize("@ss.hasPermi('nursing:healthAssessment:add')") @Log(title = "健康评估", businessType = BusinessType.INSERT) @PostMapping public AjaxResult add (@RequestBody @ApiParam("新增的健康评估对象") HealthAssessment healthAssessment) { Long id = healthAssessmentService.insertHealthAssessment(healthAssessment); return success(id); }
修改业务层
修改IHealthAssessmentService中的insertHealthAssessment方法,把返回值类型替换为Long
1 2 3 4 5 6 7 public Long insertHealthAssessment (HealthAssessment healthAssessment) ;
实现类中的返回值类型也要改为Long,并且按照思路来编写业务代码
最终代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 @Autowired private RedisTemplate<String, String> redisTemplate;@Autowired private AIModelInvoker aiModelInvoker;@Override public Long insertHealthAssessment (HealthAssessment healthAssessment) { String prompt = getPrompt(healthAssessment.getIdCard()); String qianfanResult = aiModelInvoker.qianfanInvoker(prompt); HealthReportVo healthReportVo = JSON.parseObject(qianfanResult, HealthReportVo.class); return saveHealthAssessment(healthReportVo, healthAssessment); } private Long saveHealthAssessment (HealthReportVo healthReportVo, HealthAssessment healthAssessment) { String idCard = healthAssessment.getIdCard(); healthAssessment.setBirthDate(IDCardUtils.getBirthDateByIdCard(idCard)); healthAssessment.setAge(IDCardUtils.getAgeByIdCard(idCard)); healthAssessment.setGender(IDCardUtils.getGenderFromIdCard(idCard)); double healthScore = healthReportVo.getHealthAssessment().getHealthIndex(); healthAssessment.setHealthScore(String.valueOf(healthScore)); healthAssessment.setRiskLevel(healthReportVo.getHealthAssessment().getRiskLevel()); healthAssessment.setSuggestionForAdmission(healthScore >= 60 ? 0 : 1 ); String nursingLevelName = getLevelNameByHealthScore(healthScore); healthAssessment.setNursingLevelName(nursingLevelName); healthAssessment.setAdmissionStatus(1 ); healthAssessment.setTotalCheckDate(healthReportVo.getTotalCheckDate()); healthAssessment.setAssessmentTime(LocalDateTime.now()); healthAssessment.setReportSummary(healthReportVo.getSummarize()); healthAssessment.setDiseaseRisk(JSON.toJSONString(healthReportVo.getRiskDistribution())); healthAssessment.setAbnormalAnalysis(JSON.toJSONString(healthReportVo.getAbnormalData())); healthAssessment.setSystemScore(JSON.toJSONString(healthReportVo.getSystemScore())); healthAssessmentMapper.insert(healthAssessment); return healthAssessment.getId(); } private String getLevelNameByHealthScore (double healthScore) { if (healthScore > 100 || healthScore < 0 ) { throw new BaseException ("健康评分值不合法" ); } if (healthScore >= 90 ) { return "四级护理等级" ; } else if (healthScore >= 80 ) { return "三级护理等级" ; } else if (healthScore >= 70 ) { return "二级护理等级" ; } else if (healthScore >= 60 ) { return "一级护理等级" ; } else { return "特级护理等级" ; } } private String getPrompt (String idCard) { String content = (String) redisTemplate.opsForHash().get("healthReport" , idCard); if (StringUtils.isEmpty(content)) { throw new BaseException ("文件提取内容失败,请重新上传提交报告" ); } String prompt = "请以一个专业医生的视角来分析这份体检报告,报告中包含了一些异常数据,我需要您对这些数据进行解读,并给出相应的健康建议。\n" + "体检内容如下:\n" + content + " \n" + "\n" + "要求:\n" + "1. 提取体检报告中的“总检日期”;\n" + "2. 通过临床医学、疾病风险评估模型和数据智能分析,给该用户的风险等级和健康指数给出结果。风险等级分为:健康、提示、风险、危险、严重危险。健康指数范围为0至100分;\n" + "3. 根据用户身体各项指标数据,详细说明该用户各项风险等级的占比是多少,最多保留两位小数。结论格式:该用户健康占比20.00%,提示占比20.00%,风险占比20%,危险占比20%,严重危险占比20%;\n" + "4. 对于体检报告中的异常数据,请列出(异常数据的结论、体检项目名称、检查结果、参考值、单位、异常解读、建议)这7字段。解读异常数据,解决这些数据可能代表的健康问题或风险。分析可能的原因,包括但不限于生活习惯、饮食习惯、遗传因素等。基于这些异常数据和可能的原因,请给出具体的健康建议,包括饮食调整、运动建议、生活方式改变以及是否需要进一步检查或治疗等。\n" + "结论格式:异常数据的结论:肥胖,体检项目名称:体重指数BMI,检查结果:29.2,参考值>24,单位:-。异常解读:体重超标包括超重与肥胖。体重指数(BMI)=体重(kg)/身⾼(m)的平⽅,BMI≥24为超重,BMI≥28为肥胖;男性腰围≥90cm和⼥性腰围≥85cm为腹型肥胖。体重超标是⼀种由多因素(如遗传、进⻝油脂较多、运动少、疾病等)引起的慢性代谢性疾病,尤其是肥胖,已经被世界卫⽣组织列为导致疾病负担的⼗⼤危险因素之⼀。AI建议:采取综合措施预防和控制体重,积极改变⽣活⽅式,宜低脂、低糖、⾼纤维素膳⻝,多⻝果蔬及菌藻类⻝物,增加有氧运动。若有相关疾病(如⾎脂异常、⾼⾎压、糖尿病等)应积极治疗。\n" + "5. 根据这个体检报告的内容,分别给人体的8大系统打分,每项满分为100分,8大系统分别为:呼吸系统、消化系统、内分泌系统、免疫系统、循环系统、泌尿系统、运动系统、感官系统\n" + "6. 给体检报告做一个总结,总结格式:体检报告中尿蛋⽩、癌胚抗原、⾎沉、空腹⾎糖、总胆固醇、⽢油三酯、低密度脂蛋⽩胆固醇、⾎清载脂蛋⽩B、动脉硬化指数、⽩细胞、平均红细胞体积、平均⾎红蛋⽩共12项指标提示异常,尿液常规共1项指标处于临界值,⾎脂、⾎液常规、尿液常规、糖类抗原、⾎清酶类等共43项指标提示正常,综合这些临床指标和数据分析:肾脏、肝胆、⼼脑⾎管存在隐患,其中⼼脑⾎管有“⾼危”⻛险;肾脏部位有“中危”⻛险;肝胆部位有“低危”⻛险。\n" + "\n" + "输出要求:\n" + "最后,将以上结果输出为纯JSON格式,不要包含其他的文字说明,也不要出现Markdown语法相关的文字,所有的返回结果都是json,详细格式如下:\n" + "\n" + "{\n" + " \"totalCheckDate\": \"YYYY-MM-DD\",\n" + " \"healthAssessment\": {\n" + " \"riskLevel\": \"healthy/caution/risk/danger/severeDanger\",\n" + " \"healthIndex\": XX.XX\n" + " },\n" + " \"riskDistribution\": {\n" + " \"healthy\": XX.XX,\n" + " \"caution\": XX.XX,\n" + " \"risk\": XX.XX,\n" + " \"danger\": XX.XX,\n" + " \"severeDanger\": XX.XX\n" + " },\n" + " \"abnormalData\": [\n" + " {\n" + " \"conclusion\": \"异常数据的结论\",\n" + " \"examinationItem\": \"体检项目名称\",\n" + " \"result\": \"检查结果\",\n" + " \"referenceValue\": \"参考值\",\n" + " \"unit\": \"单位\",\n" + " \"interpret\":\"对于异常的结论进一步详细的说明\",\n" + " \"advice\":\"针对于这一项的异常,给出一些健康的建议\"\n" + " }\n" + " ],\n" + " \"systemScore\": {\n" + " \"breathingSystem\": XX,\n" + " \"digestiveSystem\": XX,\n" + " \"endocrineSystem\": XX,\n" + " \"immuneSystem\": XX,\n" + " \"circulatorySystem\": XX,\n" + " \"urinarySystem\": XX,\n" + " \"motionSystem\": XX,\n" + " \"senseSystem\": XX\n" + " },\n" + " \"summarize\": \"体检报告的总结\"\n" + "}" ; return prompt; }
相关的Vo类可以在资料中获取,或者自行根据结果进行编辑
智能检测 产品数据准备 根据IOT产品设备管理的教程,创建两款产品,用于后续的使用
1号产品 :烟雾报警器
属性名称
属性描述
数据类型
访问权限
取值范围
CurrentHumidity
湿度
int
读写
0~100
IndoorTemperature
温度
int
读写
-40~80
SmokeSensorState
烟雾监测状态
bool(布尔)
读写
布尔值:0:正常1:检测到烟雾
二号产品 :睡眠监测带
属性名称
属性描述
数据类型
访问权限
取值范围
BedTime
离床时间
dateTime(日期时间)
读写
300
BedExitCount
离床次数
int
读写
0~10
SleepPhaseState
睡眠状态
int
读写
0~2
HeartRate
心率
int
读写
1~200
RespiratoryRate
呼吸率
int
读写
1~150
设备管理 熟悉了IOT平台的基本概念以及基本操作后,可以来分析后台系统的功能需求。在后台管理系统中,需要自己维护设备,不需要创建产品,因为产品直接在物联网平台创建添加即可
需要单独维护设备的原因是,设备需要跟养老院的老人 或者位置 进行绑定 ,才能做到精准的监控
比如:
烟雾报警器需要绑定到某个房间
智能手表需要绑定某个老人
需求分析 设备管理列表页 找到智能监测->设备管理
搜索:可以根据设备名称模糊查询,根据产品和设备类型精确查询
所属产品=【Iot平台-产品管理-产品名称】
设备类型=随身设备、固定设备;
同步数据:点击【同步数据】后,获取华为云产品数据
新增设备:
随身设备:表示绑定在老人身上的设备,如手表;
固定设备:表示绑定在固定位置的设备,如睡眠监测带、门磁、摄像头;
接入位置为随身设备=入退管理->入住管理->入住办理->老人姓名->当前已入住的老人;
接入位置为固定设备=楼层、房间、床位 = 在住管理->床位管理->床位房型;
删除:将该条设备从列表中移除。老人与设备自动解绑
编辑:先查看该条设备,然后再修改设备数据
查看:跳转到设备详情页
开门
设备详情 当在列表点击查看 按钮之后,可以查看设备详情页,如下效果
物模型数据-运行状态 点击【物模型数据】,查询物模型运行状态,列表中的数据为上行指令数据;
当点击了【查看数据】,出【查看数据】弹窗;
表结构说明 因为需要在本地维护设备数据,所以需要创建设备表;设备上报的数据也要存储,还需要创建设备数据表,如下:
详细表字段说明:
sql表结构:导入本章资料中的sql脚本,里面包含了智能监测模块的所有需要的表
接口分析 接口列表 依据刚才的需求分析,在养老系统中需要维护设备数据,咱们需要开发以下接口
从物联网平台同步产品列表
查询所有产品列表
注册设备
分页查询设备列表
查询设备详细数据
查看设备上报的数据
修改设备备注名称
删除设备
分页查询设备服务调用数据(暂不开发,等接收到数据之后再完成)
接口文档 接口文档参考:智能监测-接口文档
接口文档中的设备管理 部分
IOT接口对接 刚才分析了功能中涉及到的接口,其中关于设备的维护(新增、删除、修改、查询),咱们都需要在IOT平台中去操作,同时也需要在本地保存一份,那为什么要保存两份呢?
IOT平台中只是维护了基础的设备信息 ,并没有跟业务数据进行绑定,比如,设备属于哪个位置,绑定了哪个老人
只有设备绑定了业务数据,等后面采集数据之后,才能有针对性的进行排查问题。
所以,在接口开发过程中,需要调用远程的接口维护设备,同时也需要在本地进行数据操作。关于远程接口,IOT平台给提供了丰富的API,利于开发人员操作
API列表 先查阅IOT官方提供API列表:API列表
目前业务中所需要的接口文档如下:
环境集成 IOT平台已经提供了完整的SDK ,可以把它们集成到项目中进行接口的调用,集成方式:官方说明
在zzyl-framework模块导入依赖:
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > com.huaweicloud.sdk</groupId > <artifactId > huaweicloud-sdk-core</artifactId > <version > 3.1.76</version > </dependency > <dependency > <groupId > com.huaweicloud.sdk</groupId > <artifactId > huaweicloud-sdk-iotda</artifactId > <version > 3.1.76</version > </dependency >
在zzyl-admin模块中的application-dev.yml文件中添加关于IOT的配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 huaweicloud: ak: UTVLYVJKFVGYVEFFWG sk: WkEWqfwZoFlLwbR5Kq5NmWTLmj71WhRXe regionId: cn-east-3 endpoint: 38e7abf.st1.iotda-app.cn-east-3.myhuaweicloud.com projectId: 57ee9b4c827a44cb94319a077f0fe7cb host: 38e7abedbf.st1.iotda-app.cn-east-3.myhuaweicloud.com accessKey: S25ZeTC5 accessCode: a4fKpE5zbk0nbGNJU0d1bKkJNRZxQzlp queueName: DefaultQueue
AK/SK认证:通过AK(Access Key ID)/SK(Secret Access Key)加密调用请求
ak和sk是华为云身份凭证,需要新增一个访问密钥,操作如下:
endpoint 请在控制台的”总览”界面的”接入信息”中查看“应用侧”的https接入地址。
accessKey和accessCode从预置接入凭证 获取
在zzyl-framework中新增HuaWeiIotConfigProperties 来读取配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 package com.zzyl.framework.config.properties;import lombok.Data;import lombok.NoArgsConstructor;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Configuration;import java.util.Map;@Data @NoArgsConstructor @Configuration @ConfigurationProperties(prefix = "huaweicloud") public class HuaWeiIotConfigProperties { private String ak; private String sk; private String regionId; private String endpoint; private String projectId; private String host; private int port = 5671 ; private String accessKey; private String accessCode; private int connectionCount = 4 ; private String queueName; private String smartDoorServiceId; private String doorOpenPropertyName; private String doorOpenCommandName; private String passwordSetCommandName; private boolean useSsl = true ; private String vhost = "default" ; private String saslMechanisms = "PLAIN" ; private boolean isAutoAcknowledge = true ; private long reconnectDelay = 3000L ; private long maxReconnectDelay = 30 * 1000L ; private long maxReconnectAttempts = -1 ; private long idleTimeout = 30 * 1000L ; private int queuePrefetch = 1000 ; private Map<String, String> extendedOptions; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package com.zzyl.framework.config;import com.huaweicloud.sdk.core.auth.BasicCredentials;import com.huaweicloud.sdk.core.auth.ICredential;import com.huaweicloud.sdk.core.region.Region;import com.huaweicloud.sdk.iotda.v5.IoTDAClient;import com.zzyl.framework.config.properties.HuaWeiIotConfigProperties;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configuration public class IotClientConfig { @Autowired private HuaWeiIotConfigProperties huaWeiIotConfigProperties; @Bean public IoTDAClient huaWeiIotInstance () { ICredential auth = new BasicCredentials () .withAk(huaWeiIotConfigProperties.getAk()) .withSk(huaWeiIotConfigProperties.getSk()) .withDerivedPredicate(BasicCredentials.DEFAULT_DERIVED_PREDICATE) .withProjectId(huaWeiIotConfigProperties.getProjectId()); return IoTDAClient.newBuilder() .withCredential(auth) .withRegion(new Region (huaWeiIotConfigProperties.getRegionId(), huaWeiIotConfigProperties.getEndpoint())) .build(); } }
测试,在zzyl-admin模块下创建单元测试,查询产品列表,接口说明
详细代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package com.zzyl.test;import com.huaweicloud.sdk.iotda.v5.IoTDAClient;import com.huaweicloud.sdk.iotda.v5.model.ListProductsRequest;import com.huaweicloud.sdk.iotda.v5.model.ListProductsResponse;import com.huaweicloud.sdk.iotda.v5.model.Page;import com.huaweicloud.sdk.iotda.v5.model.ProductSummary;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTest public class IoTDeviceTest { @Autowired private IoTDAClient client; @Test public void selectProduceList () throws Exception { ListProductsRequest listProductsRequest = new ListProductsRequest (); listProductsRequest.setLimit(50 ); ListProductsResponse response = client.listProducts(listProductsRequest); List<ProductSummary> products = response.getProducts(); System.out.println(products); } }
功能实现 基础代码准备 按照之前的思路,使用代码生成的功能来生成代码
包名:com.zzyl.nursing
模块名:nursing
只拷贝后端代码到idea中
同时删除DeviceController中除了list方法之外的其他方法,如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @RestController @RequestMapping("/nursing/device") @Api(tags = "智能设备的接口") public class DeviceController extends BaseController { @Autowired private IDeviceService deviceService; @PreAuthorize("@ss.hasPermi('elder:device:list')") @GetMapping("/list") @ApiOperation("查询设备列表") public TableDataInfo list (Device device) { startPage(); List<Device> list = deviceService.selectDeviceList(device); return getDataTable(list); } }
从物联网平台同步产品列表
思路分析 :
前四个接口,分别是:从物联网平台同步产品列表、查询所有产品列表、注册设备、分页查询设备列表,这四个接口都与产品有关系
物联网中的产品是在物理网平台进行维护的,设备是在养老项目后台管理并同步到物联网平台的
其中注册设备、分页查询设备列表都需要用到产品数据
由于以上两条原因,需要先让物联网平台的产品数据同步到后台,然后再被注册设备、分页查询设备列表这两个接口所引用
以下就是物联网产品与养老后台同步的思路
接口定义:
在DeviceController定义同步的方法,详细如下:
1 2 3 4 5 6 @PostMapping("/syncProductList" ) @ApiOperation(value = "从物联网平台同步产品列表" ) public AjaxResult syncProductList() { deviceService.syncProductList(); return success(); }
业务层:
在IDeviceService类中定义同步的方法,如下:
1 2 3 4 void syncProductList () ;
实现类的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Autowired private RedisTemplate<String, String> redisTemplate;@Autowired private IoTDAClient client;@Override public void syncProductList () { ListProductsRequest listProductsRequest = new ListProductsRequest (); listProductsRequest.setLimit(50 ); ListProductsResponse response = client.listProducts(listProductsRequest); if (response.getHttpStatusCode() != 200 ) { throw new BaseException ("物联网接口 - 查询产品,同步失败" ); } redisTemplate.opsForValue().set(CacheConstants.IOT_ALL_PRODUCT_LIST, JSONUtil.toJsonStr(response.getProducts())); }
其中的缓存常量,可以自己定义
public static final String IOT_ALL_PRODUCT_LIST = “iot:all_product_list”;
查询所有产品列表
接口定义:
在DeviceController中新增方法,如下:
1 2 3 4 5 6 @ApiOperation("查询所有产品列表") @GetMapping("/allProduct") public R<List<ProductVo>> allProduct () { List<ProductVo> list = deviceService.allProduct(); return R.ok(list); }
业务层:
在DeviceService中新增方法如下:
1 2 3 4 5 6 List<ProductVo> allProduct () ;
ProductVo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.zzyl.nursing.vo;import io.swagger.annotations.ApiModel;import io.swagger.annotations.ApiModelProperty;import lombok.Data;@Data @ApiModel("产品信息响应模型") public class ProductVo { @ApiModelProperty("产品的ProductKey,物联网平台产品唯一标识") private String productId; @ApiModelProperty("产品名称") private String name; }
实现方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override public List<ProductVo> allProduct () { String jsonStr = redisTemplate.opsForValue().get(CacheConstants.IOT_ALL_PRODUCT_LIST); if (StringUtils.isEmpty(jsonStr)){ return Collections.emptyList(); } return JSONUtil.toList(jsonStr, ProductVo.class); }
查询已经入住的老人列表 当新增设备的时候,如果选择的的随身设备 ,则会弹窗选择已经入住的老人,如下图
该接口已经在之前的代码生成中完成了
注册设备接口
思路分析:
整体思路如下:
接口定义:
在DeviceController中新增方法,如下:
1 2 3 4 5 6 @PostMapping("/register") @ApiOperation(value = "注册设备") public AjaxResult registerDevice (@RequestBody DeviceDto deviceDto) { deviceService.registerDevice(deviceDto); return success(); }
基于接口文档,定义DeviceDto,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package com.zzyl.nursing.dto;import io.swagger.annotations.ApiModel;import io.swagger.annotations.ApiModelProperty;import lombok.Data;@Data @ApiModel(value = "设备注册参数") public class DeviceDto { private Long id; private String remark; @ApiModelProperty(value = "设备标识码", required = true) private String nodeId; @ApiModelProperty(value = "设备id") public String iotId; @ApiModelProperty(value = "产品的id") public String productKey; @ApiModelProperty(value = "产品名称") private String productName; @ApiModelProperty(value = "位置名称回显字段") private String deviceDescription; @ApiModelProperty(value = "位置类型 0 老人 1位置") Integer locationType; @ApiModelProperty(value = "绑定位置") Long bindingLocation; @ApiModelProperty(value = "设备名称") String deviceName; @ApiModelProperty(value = "物理位置类型 -1 老人 0楼层 1房间 2床位") Integer physicalLocationType; }
业务层:
在IDeviceService接口中新增方法,如下:
1 2 3 4 5 void registerDevice (DeviceDto deviceDto) ;
实现方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 @Override public void registerDevice (DeviceDto dto) { long count = count(Wrappers.<Device>lambdaQuery().eq(Device::getDeviceName, dto.getDeviceName())); if (count > 0 ) { throw new BaseException ("设备名称已存在,请重新输入" ); } count = count(Wrappers.<Device>lambdaQuery().eq(Device::getNodeId, dto.getNodeId())); if (count > 0 ) { throw new BaseException ("设备标识码已存在,请重新输入" ); } count = count(Wrappers.<Device>lambdaQuery() .eq(Device::getProductKey, dto.getProductKey()) .eq(Device::getBindingLocation, dto.getBindingLocation()) .eq(Device::getLocationType, dto.getLocationType()) .eq(dto.getPhysicalLocationType() != null , Device::getPhysicalLocationType, dto.getPhysicalLocationType())); if (count > 0 ) { throw new BaseException ("该老人/位置已绑定该产品,请重新选择" ); } AddDeviceRequest request = new AddDeviceRequest (); AddDevice body = new AddDevice (); body.withProductId(dto.getProductKey()); body.withDeviceName(dto.getDeviceName()); body.withNodeId(dto.getNodeId()); AuthInfo authInfo = new AuthInfo (); String secret = UUID.randomUUID().toString().replaceAll("-" , "" ); authInfo.withSecret(secret); body.setAuthInfo(authInfo); request.setBody(body); AddDeviceResponse response; try { response = client.addDevice(request); } catch (Exception e) { e.printStackTrace(); throw new BaseException ("物联网接口 - 注册设备,调用失败" ); } Device device = BeanUtil.toBean(dto, Device.class); device.setSecret(secret); device.setIotId(response.getDeviceId()); save(device); }
主要的业务逻辑分为三部分:第一部分用于校验属性重复,第二部分用于注册设备,最后一部分保存设备信息
分页查询设备列表 该接口在生成的代码中已经实现
其中的搜索条件,需要在后台添加数据字典:
最终效果:
查询设备详细数据
思路分析
这里的查询是有两部分数据的,第一部分是数据库中存储的设备数据,第二部分是物联网中的设备数据,查询流程如下:
接口定义
在DeviceController中定义方法,如下:
1 2 3 4 5 6 7 8 @GetMapping("/{iotId}") @ApiOperation("获取设备详细信息") public AjaxResult getInfo (@PathVariable("iotId") String iotId) { return success(deviceService.queryDeviceDetail(iotId)); }
业务层
在DeviceService中新增方法,如下:
1 2 3 4 5 6 DeviceDetailVo queryDeviceDetail (String iotId) ;
DeviceDetailVo :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 package com.zzyl.nursing.vo;import com.fasterxml.jackson.annotation.JsonFormat;import com.zzyl.common.annotation.Excel;import io.swagger.annotations.ApiModel;import io.swagger.annotations.ApiModelProperty;import lombok.Data;import java.time.LocalDateTime;@Data @ApiModel("设备详情响应模型") public class DeviceDetailVo { @ApiModelProperty(value = "设备id") private Long id; @ApiModelProperty(value = "物联网设备id") private String iotId; @ApiModelProperty(value = "设备名称") private String deviceName; @ApiModelProperty(value = "设备标识码") private String nodeId; @ApiModelProperty(value = "设备秘钥") private String secret; @ApiModelProperty(value = "产品id") public String productKey; @ApiModelProperty(value = "产品名称") public String productName; @ApiModelProperty(value = "位置类型 0 随身设备 1固定设备") private Integer locationType; @ApiModelProperty(value = "绑定位置,如果是随身设备为老人id,如果是固定设备为位置的最后一级id") private Long bindingLocation; @ApiModelProperty(value = "接入位置") private String remark; @ApiModelProperty(value = "设备状态,ONLINE:设备在线,OFFLINE:设备离线,ABNORMAL:设备异常,INACTIVE:设备未激活,FROZEN:设备冻结") private String deviceStatus; @ApiModelProperty(value = "激活时间,格式:yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime activeTime; @ApiModelProperty(value = "创建时间,格式:yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime createTime; @ApiModelProperty(value = "创建人id") private Long createBy; @ApiModelProperty(value = "创建人名称") private String creator; @ApiModelProperty(value = "位置备注") private String deviceDescription; }
实现方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Override public DeviceDetailVo queryDeviceDetail (String iotId) { Device device = getOne(Wrappers.<Device>lambdaQuery().eq(Device::getIotId, iotId)); if (ObjectUtil.isEmpty(device)) { return null ; } ShowDeviceRequest request = new ShowDeviceRequest (); request.setDeviceId(iotId); ShowDeviceResponse response; try { response = client.showDevice(request); } catch (Exception e) { throw new BaseException ("物联网接口 - 查询设备详情,调用失败" ); } DeviceDetailVo deviceVo = BeanUtil.toBean(device, DeviceDetailVo.class); deviceVo.setDeviceStatus(response.getStatus()); String activeTimeStr = response.getActiveTime(); if (StringUtils.isNotEmpty(activeTimeStr)) { LocalDateTime activeTime = LocalDateTimeUtil.parse(activeTimeStr, DatePattern.UTC_MS_PATTERN); deviceVo.setActiveTime(DateTimeZoneConverter.utcToShanghai(activeTime)); } return deviceVo; }
从本章资料可以找到日期转换工具类DateTimeZoneConverter
查看设备上报的数据(设备影子)
接口定义:
在DeviceController中新增方法,如下:
接收的参数就是接口的请求参数
1 2 3 4 5 6 7 8 9 @GetMapping("/queryServiceProperties/{iotId}") @ApiOperation("查询设备上报数据") public AjaxResult queryServiceProperties (@PathVariable("iotId") String iotId) { AjaxResult ajaxResult = deviceService.queryServiceProperties(iotId); return ajaxResult; }
业务层:
在IDeviceService中定义新的方法
1 2 3 4 5 6 AjaxResult queryServiceProperties (String iotId) ;
实现方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Override public AjaxResult queryServiceProperties (String iotId) { ShowDeviceShadowRequest request = new ShowDeviceShadowRequest (); request.setDeviceId(iotId); ShowDeviceShadowResponse response = client.showDeviceShadow(request); if (response.getHttpStatusCode() != 200 ) { throw new BaseException ("物联网接口 - 查询设备影子,调用失败" ); } List<DeviceShadowData> shadow = response.getShadow(); if (CollUtil.isEmpty(shadow)) { List<Object> emptyList = Collections.emptyList(); return AjaxResult.success(emptyList); } DeviceShadowProperties reported = shadow.get(0 ).getReported(); JSONObject jsonObject = JSONUtil.parseObj(reported.getProperties()); List<Map<String,Object>> list = new ArrayList <>(); String eventTimeStr = reported.getEventTime(); LocalDateTime eventTimeLocalDateTime = LocalDateTimeUtil.parse(eventTimeStr, "yyyyMMdd'T'HHmmss'Z'" ); LocalDateTime eventTime = DateTimeZoneConverter.utcToShanghai(eventTimeLocalDateTime); jsonObject.forEach((k,v) -> { Map<String,Object> map = new HashMap <>(); map.put("functionId" , k); map.put("value" , v); map.put("eventTime" , eventTime); list.add(map); }); return AjaxResult.success(list); }
这里利用的是Map构造返回值,而不是使用实体类(实体类也是可以的)
接收设备端数据 前置内容:智能检测-数据处理展示
整体的接收数据思路如下:
表结构
基础代码生成 使用代码生成的功能,来生成代码
包名:com.zzyl.nursing
模块名:nursing
除了主键之外的Long类型,改为Integer
数据上报时间字段改为LocalDateTime类型
生成业务名称:data
生成功能名称:设备数据
只拷贝后端代码到idea中
修改DeviceData表,添加三个注解,方便使用构建者设计模式 来构建对象,如下图:
功能实现
思路分析
接收到的数据格式如下,是一个json字符串,可以定义一个类来转换接收数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 { "resource" : "device.property" , "event" : "report" , "event_time" : "20250706T023712Z" , "event_time_ms" : "2025-07-06T02:37:12.573Z" , "request_id" : "c13e6473-2709-4f34-8017-3538d9c57ccd" , "notify_data" : { "header" : { "app_id" : "a7bb0103499749d984777860b53eadf6" , "device_id" : "6864b04ed582f2001837367e_watch01" , "node_id" : "watch01" , "product_id" : "6864b04ed582f2001837367e" , "gateway_id" : "6864b04ed582f2001837367e_watch01" } , "body" : { "services" : [ { "service_id" : "watch_services" , "properties" : { "BodyTemp" : 36.8 , "HeartRate" : 5.9712353 , "xueyang" : 84.60445 , "BatteryPercentage" : 24.040668 } , "event_time" : "20250706T023712Z" } ] } } }
重点关注:device_id、properties、event_time
上述json的数据结构中,已经标明颜色的是重点要解析的,由于嵌套层级较多,需要定义多个类来进行接收,对应的类可以在本章资料中找到,如下图
在设备数据表中,一次上报需要保存一个设备的多个物模型数据
比如,烟雾报警有三个物模型,那么当接收到一个数据上报的时候,就要保存三条数据到设备数据中
修改AmqpClient类,解析json数据并调用设备数据的业务层,批量添加数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 package com.zzyl.nursing.task;import cn.hutool.core.text.CharSequenceUtil;import cn.hutool.core.util.ObjectUtil;import cn.hutool.json.JSONObject;import cn.hutool.json.JSONUtil;import com.zzyl.framework.config.properties.HuaWeiIotConfigProperties;import com.zzyl.nursing.job.vo.IotMsgNotifyData;import com.zzyl.nursing.service.IDeviceDataService;import lombok.extern.slf4j.Slf4j;import org.apache.qpid.jms.*;import org.apache.qpid.jms.message.JmsInboundMessageDispatch;import org.apache.qpid.jms.transports.TransportOptions;import org.apache.qpid.jms.transports.TransportSupport;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.ApplicationArguments;import org.springframework.boot.ApplicationRunner;import org.springframework.stereotype.Component;import javax.annotation.Resource;import javax.jms.*;import java.net.InetAddress;import java.net.URI;import java.net.UnknownHostException;import java.text.MessageFormat;import java.util.HashMap;import java.util.Map;import java.util.UUID;import java.util.concurrent.ExecutorService;import java.util.stream.Collectors;@Slf4j @Component public class AmqpClient implements ApplicationRunner { @Resource private HuaWeiIotConfigProperties huaWeiIotConfigProperties; @Resource private ExecutorService executorService; private static String clientId; static { try { clientId = InetAddress.getLocalHost().getHostAddress(); } catch (UnknownHostException e) { e.printStackTrace(); } } @Override public void run (ApplicationArguments args) throws Exception { start(); } public void start () throws Exception { for (int i = 0 ; i < huaWeiIotConfigProperties.getConnectionCount(); i++) { Connection connection = getConnection(); ((JmsConnection) connection).addConnectionListener(myJmsConnectionListener); Session session = connection.createSession(false , Session.AUTO_ACKNOWLEDGE); connection.start(); MessageConsumer consumer = newConsumer(session, connection, huaWeiIotConfigProperties.getQueueName()); consumer.setMessageListener(messageListener); } log.info("amqp is started successfully, and will exit after server shutdown " ); } private Connection getConnection () throws Exception { String connectionUrl = generateConnectUrl(); JmsConnectionFactory cf = new JmsConnectionFactory (connectionUrl); TransportOptions to = new TransportOptions (); to.setTrustAll(true ); cf.setSslContext(TransportSupport.createJdkSslContext(to)); String userName = "accessKey=" + huaWeiIotConfigProperties.getAccessKey(); cf.setExtension(JmsConnectionExtensions.USERNAME_OVERRIDE.toString(), (connection, uri) -> { String newUserName = userName; if (connection instanceof JmsConnection) { newUserName = ((JmsConnection) connection).getUsername(); } return newUserName + "|timestamp=" + System.currentTimeMillis(); }); return cf.createConnection(userName, huaWeiIotConfigProperties.getAccessCode()); } public String generateConnectUrl () { String uri = MessageFormat.format("{0}://{1}:{2}" , (huaWeiIotConfigProperties.isUseSsl() ? "amqps" : "amqp" ), huaWeiIotConfigProperties.getHost(), String.valueOf(huaWeiIotConfigProperties.getPort())); Map<String, String> uriOptions = new HashMap <>(); uriOptions.put("amqp.vhost" , huaWeiIotConfigProperties.getVhost()); uriOptions.put("amqp.idleTimeout" , String.valueOf(huaWeiIotConfigProperties.getIdleTimeout())); uriOptions.put("amqp.saslMechanisms" , huaWeiIotConfigProperties.getSaslMechanisms()); Map<String, String> jmsOptions = new HashMap <>(); jmsOptions.put("jms.prefetchPolicy.queuePrefetch" , String.valueOf(huaWeiIotConfigProperties.getQueuePrefetch())); if (CharSequenceUtil.isNotBlank(clientId)) { jmsOptions.put("jms.clientID" , clientId); } else { jmsOptions.put("jms.clientID" , UUID.randomUUID().toString()); } jmsOptions.put("failover.reconnectDelay" , String.valueOf(huaWeiIotConfigProperties.getReconnectDelay())); jmsOptions.put("failover.maxReconnectDelay" , String.valueOf(huaWeiIotConfigProperties.getMaxReconnectDelay())); if (huaWeiIotConfigProperties.getMaxReconnectAttempts() > 0 ) { jmsOptions.put("failover.maxReconnectAttempts" , String.valueOf(huaWeiIotConfigProperties.getMaxReconnectAttempts())); } if (huaWeiIotConfigProperties.getExtendedOptions() != null ) { for (Map.Entry<String, String> option : huaWeiIotConfigProperties.getExtendedOptions().entrySet()) { if (option.getKey().startsWith("amqp." ) || option.getKey().startsWith("transport." )) { uriOptions.put(option.getKey(), option.getValue()); } else { jmsOptions.put(option.getKey(), option.getValue()); } } } StringBuilder stringBuilder = new StringBuilder (); stringBuilder.append(uriOptions.entrySet().stream() .map(option -> MessageFormat.format("{0}={1}" , option.getKey(), option.getValue())) .collect(Collectors.joining("&" , "failover:(" + uri + "?" , ")" ))); stringBuilder.append(jmsOptions.entrySet().stream() .map(option -> MessageFormat.format("{0}={1}" , option.getKey(), option.getValue())) .collect(Collectors.joining("&" , "?" , "" ))); return stringBuilder.toString(); } public MessageConsumer newConsumer (Session session, Connection connection, String queueName) throws Exception { if (connection == null || !(connection instanceof JmsConnection) || ((JmsConnection) connection).isClosed()) { throw new Exception ("create consumer failed,the connection is disconnected." ); } return session.createConsumer(new JmsQueue (queueName)); } private final MessageListener messageListener = message -> { try { executorService.submit(() -> processMessage(message)); } catch (Exception e) { log.error("submit task occurs exception " , e); } }; @Autowired private IDeviceDataService deviceDataService; private void processMessage (Message message) { String contentStr; try { contentStr = message.getBody(String.class); String topic = message.getStringProperty("topic" ); String messageId = message.getStringProperty("messageId" ); log.info("receive message,\n topic = {},\n messageId = {},\n content = {}" , topic, messageId, contentStr); } catch (JMSException e) { throw new RuntimeException ("服务器错误" ); } JSONObject jsonMsg = JSONUtil.parseObj(contentStr); JSONObject jsonNotifyData = jsonMsg.getJSONObject("notify_data" ); if (ObjectUtil.isEmpty(jsonNotifyData)) { return ; } IotMsgNotifyData iotMsgNotifyData = JSONUtil.toBean(jsonNotifyData, IotMsgNotifyData.class); if (ObjectUtil.isEmpty(iotMsgNotifyData.getBody()) || ObjectUtil.isEmpty(iotMsgNotifyData.getBody().getServices())) { return ; } deviceDataService.batchInsertDeviceData(iotMsgNotifyData); } private final JmsConnectionListener myJmsConnectionListener = new JmsConnectionListener () { @Override public void onConnectionEstablished (URI remoteURI) { log.info("onConnectionEstablished, remoteUri:{}" , remoteURI); } @Override public void onConnectionFailure (Throwable error) { log.error("onConnectionFailure, {}" , error.getMessage()); } @Override public void onConnectionInterrupted (URI remoteURI) { log.info("onConnectionInterrupted, remoteUri:{}" , remoteURI); } @Override public void onConnectionRestored (URI remoteURI) { log.info("onConnectionRestored, remoteUri:{}" , remoteURI); } @Override public void onInboundMessage (JmsInboundMessageDispatch envelope) { } @Override public void onSessionClosed (Session session, Throwable cause) { } @Override public void onConsumerClosed (MessageConsumer consumer, Throwable cause) { } @Override public void onProducerClosed (MessageProducer producer, Throwable cause) { } }; }
流程
在设备数据的业务层定义批量添加的方法
在IDeviceDataService中定义新的方法:
1 2 3 4 5 void batchInsertDeviceData (IotMsgNotifyData iotMsgNotifyData) ;
实现方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @Autowired private DeviceMapper deviceMapper;@Override public void batchInsertDeviceData (IotMsgNotifyData iotMsgNotifyData) { String iotId = iotMsgNotifyData.getHeader().getDeviceId(); Device device = deviceMapper.selectOne(Wrappers.<Device>lambdaQuery().eq(Device::getIotId, iotId)); if (ObjectUtil.isEmpty(device)) { log.error("设备不存在" ); return ; } iotMsgNotifyData.getBody().getServices().forEach(s -> { Map<String, Object> properties = s.getProperties(); if (CollUtil.isEmpty(properties)) { return ; } String eventTimeStr = s.getEventTime(); LocalDateTime localDateTime = LocalDateTimeUtil.parse(eventTimeStr, "yyyyMMdd'T'HHmmss'Z'" ); LocalDateTime eventTime = DateTimeZoneConverter.utcToShanghai(localDateTime); List<DeviceData> list = new ArrayList <>(); properties.forEach((k,v) -> { DeviceData deviceData = BeanUtil.toBean(device, DeviceData.class); deviceData.setId(null ); deviceData.setAlarmTime(eventTime); deviceData.setFunctionId(k); deviceData.setDataValue(v + "" ); list.add(deviceData); }); saveBatch(list); }); }
AmqpClient 是通过消息队列接收华为IoT数据的后台服务,它运行在独立的线程中 ,不是Web请求线程。当这个线程中执行数据库操作时,会触发 MyMetaObjectHandler 的自动填充功能,但由于没有HTTP请求上下文,request 对象无法正常使用。需要修改一下MyMetaObjectHandler中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 public boolean isExclude() { // 当request无法正常获取到请求路径时,会抛出异常。如果抛出异常,就直接设置为true即可 try { String requestURI = request.getRequestURI(); if (requestURI.startsWith("/member")) { return true; } } catch (Exception e) { return true; } return false; }
bug修复 当设备增多后,数据库连接池可能会很快耗尽,可以适当增加连接数数量和等待时间
可能的错误提示:
1 wait millis 60003, active 20, maxActive 20, creating 0
数据库连接池耗尽 ,活跃连接数已达上限(maxActive=20),无可用连接且等待超时(60秒),导致新请求无法获取连接
修改application.yml文件关于数据库的配置,如下标颜色内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver druid: master: url: jdbc:mysql://192.168.100.168:3306/zzyl?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: heima123 slave: enabled: false url: username: password: initialSize: 5 minIdle: 10 maxActive: 60 maxWait: 120000 connectTimeout: 30000 socketTimeout: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 maxEvictableIdleTimeMillis: 900000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: true testOnReturn: false webStatFilter: enabled: true statViewServlet: enabled: true allow: url-pattern: /druid/* login-username: ruoyi login-password: 123456 filter: stat: enabled: true log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true
修改了maxActive、maxWait、testOnBorrow
查询设备的物模型数据 需求分析 最终的效果:
其中的数据值,是从IOT平台实时获取到的数据值
当点击了某个功能(物模型)的查看数据 按钮,则会显示这个功能的历史数据,可以按照时间范围进行检索,如下图:
时间范围=1小时、24小时、7天
接口定义 参考接口文档:智能监测-接口文档
功能实现 接口定义 在DeviceDataController ,改造查询数据的方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @PreAuthorize("@ss.hasPermi('elder:data:list')") @GetMapping("/list") @ApiOperation("查询设备数据列表") public TableDataInfo list (DeviceDataPageReqDto deviceDataPageReqDto) { return deviceDataService.selectDeviceDataList(deviceDataPageReqDto); } @ApiOperation("导出设备数据列表") @PreAuthorize("@ss.hasPermi('nursing:deviceData:export')") @Log(title = "设备数据", businessType = BusinessType.EXPORT) @PostMapping("/export") public void export (HttpServletResponse response, DeviceData deviceData) { List<DeviceData> list = deviceDataService.selectDeviceDataList(deviceData); ExcelUtil<DeviceData> util = new ExcelUtil <DeviceData>(DeviceData.class); util.exportExcel(response, list, "设备数据数据" ); }
DeviceDataPageReqDto
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package com.zzyl.nursing.dto;import io.swagger.annotations.ApiModel;import io.swagger.annotations.ApiModelProperty;import lombok.Data;import org.springframework.format.annotation.DateTimeFormat;import java.time.LocalDateTime;@Data @ApiModel("设备数据分页查询请求模型") public class DeviceDataPageReqDto { @ApiModelProperty(value = "设备名称", required = false) private String deviceName; @ApiModelProperty(value = "功能ID", required = false) private String functionId; @ApiModelProperty(value = "开始时间", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime startTime; @ApiModelProperty(value = "结束时间", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime endTime; @ApiModelProperty(value = "页码", required = true, example = "1") private Integer pageNum; @ApiModelProperty(value = "页面大小", required = true, example = "10") private Integer pageSize; }
业务层 在IDeviceDataService接口,改造分页查询方法,代码如下:
1 2 3 4 5 6 7 public TableDataInfo selectDeviceDataList (DeviceDataPageReqDto deviceDataPageReqDto) ;
实现方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @Override public TableDataInfo selectDeviceDataList (DeviceDataPageReqDto dto) { LambdaQueryWrapper<DeviceData> lambdaQueryWrapper = new LambdaQueryWrapper <>(); Page<DeviceData> page = new Page (dto.getPageNum(), dto.getPageSize()); if (StringUtils.isNotEmpty(dto.getDeviceName())) { lambdaQueryWrapper.eq(DeviceData::getDeviceName, dto.getDeviceName()); } if (StringUtils.isNotEmpty(dto.getFunctionId())) { lambdaQueryWrapper.eq(DeviceData::getFunctionId, dto.getFunctionId()); } if (ObjectUtils.isNotEmpty(dto.getStartTime()) && ObjectUtils.isNotEmpty(dto.getEndTime())) { lambdaQueryWrapper.between(DeviceData::getAlarmTime, dto.getStartTime(), dto.getEndTime()); } page = page(page, lambdaQueryWrapper); return getTableDataInfo(page); } @NotNull private static TableDataInfo getTableDataInfo (Page<DeviceData> page) { TableDataInfo tableData = new TableDataInfo (); tableData.setCode(HttpStatus.SUCCESS); tableData.setMsg("查询成功" ); tableData.setRows(page.getRecords()); tableData.setTotal(page.getTotal()); return tableData; }
智能床位 参考接口文档:智能监测-接口文档
数据准备:
获取所有楼层(智能楼层) 思路分析 目的是为了展示绑定了智能设备的楼层和楼层下的房间、床位信息以及智能设备最近一次上报的数据
比如下图中,只有3楼、4楼、5楼、1楼绑定了设备,那就只展示这些楼层
表关系
目前设备的绑定都是房间 或者是床位
比如:烟雾报警绑定的位置是房间,睡眠监测带绑定的位置是床位
在设备(device)表中同时有三个字段确定绑定的位置
location_type 位置类型 0:随身设备 1:固定设备
physical_location_type 物理位置类型 0楼层 1房间 2床位
binding_location 具体位置对应的id值(如果是房间,则是房间id,如果是床位,则对应床位id)
5张表关联查询(floor、room、bed、device【房间|床位】),对应的sql
1 2 3 4 5 6 7 8 select f.id, f.name, f.codefrom floor f left join room r on f.id = r.floor_id left join bed b on r.id = b.room_id left join device rd on rd.binding_location = r.id and rd.location_type = 1 and rd.physical_location_type = 1 left join device bd on bd.binding_location = b.id and bd.location_type = 1 and bd.physical_location_type = 2 where (rd.id is not null or bd.id is not null )group by f.id;
上述sql中的group by f.id 是为了去除重复数据
控制层 在FloorController中新增查询智能楼层的方法,如下:
1 2 3 4 5 @GetMapping("/getAllFloorsWithDevice") @ApiOperation("查询所有楼层(智能设备)") public R<List<FloorVo>> getAllFloorsWithDevice () { return R.ok(floorService.getAllFloorsWithDevice()); }
业务层 在FloorService中新增查询智能楼层的方法,如下:
1 2 3 4 5 List<FloorVo> getAllFloorsWithDevice () ;
对应的实现方法:
1 2 3 4 5 6 7 8 @Override public List<FloorVo> getAllFloorsWithDevice () { return floorMapper.getAllFloorsWithDevice(); }
持久层 在FloorMapper中新增方法,无参
1 List<FloorVo> getAllFloorsWithDevice () ;
对应的映射文件:
1 2 3 4 5 6 7 8 9 10 <select id="getAllFloorsWithDevice" resultType="com.zzyl.nursing.vo.FloorVo" > select f.id, f.name, f.code from floor f left join room r on f.id = r.floor_id left join bed b on r.id = b.room_id left join device rd on rd.binding_location = r.id and rd.location_type = 1 and rd.physical_location_type = 1 left join device bd on bd.binding_location = b.id and bd.location_type = 1 and bd.physical_location_type = 2 where (rd.id is not null or bd.id is not null ) group by f.id </select>
获取房间中的智能设备及数据 思路分析
根据楼层ID 查询房间或者是床位的设备数据(最新的一条数据)
在这个接口中返回的数据较多:
房间数据以及房间绑定的设备、设备上报的数据
床位数据以及床位入住的老人姓名、绑定的设备、设备上报的数据
实现方案有两种
方案一:通过楼层关联房间、床位、老人、设备表2次、设备数据表2次共7张表查询对应的数据(设备数据表数据较多,效率不高),不推荐这种方案
方案二:使用缓存,因为只需要查询最近一次 的设备数据,可以把最近一次采集的数据存储到redis中,然后让房间或者床位进行匹配
按照这种实现思路,需要改造上报数据的接收逻辑,将设备上报的数据保存到Redis中存储,将来需要展示设备最近一次上报的数据时只需从Redis中获取即可
改造上报数据接收逻辑 找到DeviceDataServiceImpl类中的batchInsertDeviceData方法,把每次上报的数据存储到Redis中,由于Redis保存数据的时候key相同的情况下可以覆盖旧数据,所以在Redis中存储的永远都是最新的数据
详细代码,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 @Autowired private RedisTemplate<String, String> redisTemplate;@Override public void batchInsertDeviceData (IotMsgNotifyData data) { Device device = deviceMapper.selectOne(Wrappers.<Device>lambdaQuery().eq(Device::getIotId, data.getHeader().getDeviceId())); if (ObjectUtil.isEmpty(device)){ log.info("设备不存在,设备id:{}" ,data.getHeader().getDeviceId()); return ; } List<IotMsgService> services = data.getBody().getServices(); if (CollUtil.isEmpty(services)){ log.info("设备数据为空" ); return ; } services.forEach(s->{ String eventTimeStr = s.getEventTime(); LocalDateTime localDateTime = LocalDateTimeUtil.parse(eventTimeStr, "yyyyMMdd'T'HHmmss'Z'" ); LocalDateTime eventTime = DateTimeZoneConverter.utcToShanghai(localDateTime); List<DeviceData> list = new ArrayList <>(); s.getProperties().forEach((k,v)->{ DeviceData deviceData = BeanUtil.toBean(device, DeviceData.class); deviceData.setId(null ); deviceData.setCreateTime(null ); deviceData.setFunctionId(k); deviceData.setDataValue(v+"" ); deviceData.setAlarmTime(eventTime); list.add(deviceData); }); saveBatch(list); redisTemplate.opsForHash().put(CacheConstants.IOT_DEVICE_LAST_DATA, device.getIotId(), JSONUtil.toJsonStr(list)); }); }
需要在CacheConstants类中定义新的常量
1 public static final String `*`IOT_DEVICE_LAST_DATA `*`= "iot:device_last_data";
查询基础数据 按照前面分析的思路实现,结合接口需要的响应结果,需要准备基础数据并返回,基础数据就包括了房间、床位、老人、设备(房间或床位),其中的设备数据 从Redis中获取
查询基础的sql :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 select r.* , b.id bid, b.bed_number, b.sort, b.bed_status, b.room_id, b.create_by, b.update_by, b.remark, b.create_time, b.update_time, e.name ename, e.id eid, d.id as r_did, d.iot_id, d.product_key as product_key, d.device_name, d.product_name, dd.id as b_did, dd.iot_id b_iot_id, dd.product_key as b_product_key, dd.device_name as b_device_name, dd.product_name as b_product_name from room r left join bed b on r.id = b.room_id left join elder e on b.id = e.bed_id left join device d on d.binding_location = r.id and d.location_type = 1 and d.physical_location_type = 1 left join device dd on dd.binding_location = b.id and dd.location_type = 1 and dd.physical_location_type = 2 where r.floor_id = 1 and (d.id is not null or dd.id is not null )
根据接口文档中返回数据的样式修改Vo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package com.zzyl.nursing.vo;import com.zzyl.nursing.domain.DeviceData;import io.swagger.annotations.ApiModel;import io.swagger.annotations.ApiModelProperty;import lombok.Data;import java.util.List;@ApiModel("设备信息响应模型") @Data public class DeviceInfo { @ApiModelProperty(value = "主键") private Long id; @ApiModelProperty(value = "物联网设备ID") private String iotId; @ApiModelProperty(value = "设备名称") private String deviceName; @ApiModelProperty(value = "产品key") public String productKey; @ApiModelProperty(value = "产品名称") public String productName; @ApiModelProperty(value = "设备数据") private List<DeviceData> deviceDataVos; }
其中的设备数据deviceDataVos字段,是不需要从数据库中查询数据的,后面会到redis中查询设备数据
在RoomVo和BedVo中两个类中分别添加一个deviceVos设备集合属性
1 2 3 4 private List<DeviceInfo> deviceVos;
控制层 根据接口中的路径分析,这个接口需要定义在RoomController中,代码如下:
1 2 3 4 5 @GetMapping("/getRoomsWithDeviceByFloorId/{floorId}") @ApiOperation("获取所有房间(智能床位)") public R<List<RoomVo>> getRoomsWithDeviceByFloorId (@PathVariable(name = "floorId") Long floorId) { return R.ok(roomService.getRoomsWithDeviceByFloorId(floorId)); }
持久层 这里需要使用MyBatis的多表查询,根据刚才的sql,其中的房间、床位、老人、房间设备、床位设备数据都要返回
在RoomMapper中新增方法
1 List<RoomVo> getRoomsWithDeviceByFloorId (Long floorId) ;
业务层 1 2 3 4 5 6 List<RoomVo> getRoomsWithDeviceByFloorId (Long floorId) ;
对应的实现方法,需要到redis中根据iotId匹配最新上报的设备数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Autowired private RedisTemplate <String ,String > redisTemplate;@Override public List <RoomVo > getRoomsWithDeviceByFloorId (Long floorId ) { List <RoomVo > roomVos = roomMapper.getRoomsWithDeviceByFloorId (floorId); roomVos.forEach (roomVo -> { List <DeviceInfo > deviceVos = roomVo.getDeviceVos (); deviceVos.forEach (deviceInfo -> { String jsonStr = (String ) redisTemplate.opsForHash ().get (CacheConstants .IOT_DEVICE_LAST_DATA , deviceInfo.getIotId ()); if (StringUtils .isEmpty (jsonStr)) { return ; } deviceInfo.setDeviceDataVos (JSON Util.toList (jsonStr, DeviceData .class )); }); roomVo.getBedVoList ().forEach (bedVo -> { bedVo.getDeviceVos ().forEach (deviceInfo -> { String jsonStr = (String ) redisTemplate.opsForHash ().get (CacheConstants .IOT_DEVICE_LAST_DATA , deviceInfo.getIotId ()); if (StringUtils .isEmpty (jsonStr)) { return ; } deviceInfo.setDeviceDataVos (JSON Util.toList (jsonStr, DeviceData .class )); }); }); }); return roomVos; }
报警管理和通知 报警规则 想要获取报警数据,必须先根据不同的设备,不同的物模型来定义报警规则
新增报警规则 先分析需求,打开原型图
数据来源:
逻辑规则:
1)若多条报警规则是包含/互斥关系时,只要符合报警规则时,就会产生一条报警数据;
例如:规则1: 手表电量 >=10 ,规则2:手表电量< 50, 此时是包含关系,当手机电量=40时,符合两条报警规则,则产生两条报警数据;
2)报警数据见下方示例,1分钟(数据聚合周期)检查一次智能手表(所属产品)中的全部设备(关联设备)的血氧(功能名称),
监控值(统计字段)是否 < 90(运算符+阈值),当持续3个周期(持续周期)都满足这个规则时,触发报警;
通知方式 :
1)报警生效时间:报警规则的生效时间,报警规则只在生效时间内才会检查监控数据是否需要报警;
2)报警沉默周期:指报警发生后如果未恢复正常,重复发送报警通知的时间间隔;
报警方式:
1)当触发报警规则时,则发送消息通知,
2)通知对象:设备数据类型=老人异常数据时,通知老人对应的护理员;设备数据类型=设备异常数据时,通知后勤部维修工;
报警规则其他需求 下面展示的就是刚刚创建的报警规则查询列表
其中在操作中可以处理报警规则(删除,编辑,启用禁用)
表结构
报警规则建表语句:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 CREATE TABLE "alert_rule" ( "id" bigint NOT NULL AUTO_INCREMENT, "product_key" varchar (50 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '所属产品的key' , "product_name" varchar (500 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '产品名称' , "module_id" varchar (50 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '模块的key' , "module_name" varchar (500 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '模块名称' , "function_name" varchar (255 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '功能名称' , "function_id" varchar (255 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '功能标识' , "iot_id" varchar (255 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '物联网设备id' , "device_name" varchar (255 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '设备名称' , "alert_data_type" int DEFAULT NULL COMMENT '报警数据类型,0:老人异常数据,1:设备异常数据' , "alert_rule_name" varchar (255 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '告警规则名称' , "operator" varchar (255 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '运算符' , "value" float DEFAULT NULL COMMENT '阈值' , "duration" int DEFAULT NULL COMMENT '持续周期' , "alert_effective_period" varchar (255 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '报警生效时段' , "alert_silent_period" int DEFAULT NULL COMMENT '报警沉默周期' , "status" int DEFAULT NULL COMMENT '0 禁用 1启用' , "create_time" datetime NOT NULL COMMENT '创建时间' , "update_time" datetime DEFAULT NULL COMMENT '更新时间' , "create_by" bigint DEFAULT NULL COMMENT '创建人id' , "update_by" bigint DEFAULT NULL COMMENT '更新人id' , "remark" varchar (255 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '备注' , PRIMARY KEY ("id") USING BTREE ) ENGINE= InnoDB AUTO_INCREMENT= 53 DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT= DYNAMIC ;
需求加强说明 基于前面的报警规则,一旦有不符合预期的数据就会触发报警,产生报警数据,通知相关的负责人解决
案例一 详细报警规则,如下图:
监测的产品为睡眠监测带 ,物模型为心率, 过滤的是该产品下的所有设备
报警类型为老人异常数据(设备报警通知老人绑定的护理员和超级管理员 )
持续周期:
持续1个周期(1周期=1分钟):表示触发报警之后,马上会保存报警数据
持续 3个周期(1周期=1分钟):表示触发报警之后,连续三次都是异常数据才会保存报警数据**
依此类推
阈值为65,运算符为**<:表示采集的 心率数据如果小于65**就触发报警
沉默周期为5分钟,已经保存报警数据之后,如果后面有连续报警,5分钟之后再触发报警规则
报警生效时段为00:00:00~23:59:59:表示任意时段都会采集数据
案例二 报警规则如下图:
监测的产品为烟雾报警器 ,物模型为温度, 过滤的是该产品下的全部设备
报警类型为设备异常数据 (设备报警通知行政和超级管理员 )
持续周期为持续1个周期(1后期=1分钟):表示触发报警之后,马上会保存报警数据
阈值为55 ,运算符为**>=:表示采集的室内 温度数据大于等于55**就触发报警
沉默周期为5分钟,已经保存报警数据之后,如果后面有连续报警,5分钟之后再触发报警规则
报警生效时段为00:00:00~23:59:59:表示任意时段都会采集数据
报警规则基础代码准备
包名:com.zzyl.nursing
模块名:nursing
生成业务名称:alertRule
生成功能名称:报警规则
只需要拷贝后端代码
由于报警规则页面就是单表的增删改查,在若依代码生成功能生成的代码中所有功能都已经实现了,无需编写代码,但是要充分理解业务
查询产品详情接口 需求说明 当新增报警规则的时候,需要选择产品中具体的功能,所以在选择产品之后需要查询到该产品下的服务列表及属性列表
接口定义 接口文档参考:智能监测-接口文档
华为云接口:https://support.huaweicloud.com/api-iothub/iot_06_v5_0052.html
在DeviceController定义新的方法,如下:
1 2 3 4 5 @GetMapping("/queryProduct/{productKey}") @ApiOperation(value = "查询产品详情") public AjaxResult queryProduct (@PathVariable String productKey) { return deviceService.queryProduct(productKey); }
业务层 在IDeviceService中定义新的方法,如下:
1 2 3 4 5 6 AjaxResult queryProduct (String productKey) ;
实现方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Override public AjaxResult queryProduct (String productKey) { if (StringUtils.isEmpty(productKey)) { throw new BaseException ("请输入正确的参数" ); } ShowProductRequest showProductRequest = new ShowProductRequest (); showProductRequest.setProductId(productKey); ShowProductResponse response; try { response = client.showProduct(showProductRequest); } catch (Exception e) { throw new BaseException ("查询产品详情失败" ); } List<ServiceCapability> serviceCapabilities = response.getServiceCapabilities(); if (CollUtil.isEmpty(serviceCapabilities)) { return AjaxResult.success(Collections.emptyList()); } return AjaxResult.success(serviceCapabilities); }
报警数据过滤及处理 执行流程
准备工作 负责老人功能介绍
在服务管理下提供了一个负责老人 功能,在这里可以给已经入住的老人分配对应的护理员:
用户表sys_user:存储养老院的员工信息,包含了院长、销售、财务、护理员等各种角色的员工信息
老人表elder:养老院中入住的老人数据
护理员老人关联表nursing_elder:养老院的老人所负责的护理员列表
批量保存报警数据 一旦有了报警数据之后,可能需要通知多个人,所以要为每一个要通知的人保存一条报警数据
告警数据表结构:alert_data(已提供)
报警规则与报警数据为一对多的关系
重要字段提醒
报警数据基础代码准备
包名:com.zzyl.nursing
模块名:nursing
生成业务名称:alertData
处理时间字段为:LocalDateTime
生成功能名称:报警数据
只需要拷贝后端代码
查询报警通知人 在流程结束之后,如果出现了报警数据,基于不同的报警设备的类型,通知的人的也是不同的
老人异常数据,通知负责老人的护理员及超级管理员
如果是随身设备,可以通过设备找到老人ID,然后通过老人id查询nursing_elder表找到护理员的ID
如果是床位上的固定设备,则需要通过设备表中存储的床位ID查询老人表找到老人ID,,然后通过老人id查询nursing_elder表找到护理员的ID
设备异常数据,通知行政人员(维修工)及超级管理员
如果是楼层或者房间中的固定设备,不需要通知护理员,需要根据维修工 角色名称找到对应的维修工的ID
查询老人异常数据要通知的护理员 表关系
在DeviceMapper中新增两个方法,都是通过iotId查询对应的护理员
1 2 3 4 5 6 7 8 9 10 11 12 13 List<Long> selectNursingIdsByIotIdWithElder (@Param("iotId") String iotId) ; List<Long> selectNursingIdsByIotIdWithBed (@Param("iotId") String iotId) ;
对应的xml映射文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <select id ="selectNursingIdsByIotIdWithElder" resultType ="java.lang.Long" > select ne.nursing_id from device d left join nursing_elder ne on ne.elder_id = d.binding_location where d.location_type = 0 and d.iot_id = #{iotId} </select > <select id ="selectNursingIdsByIotIdWithBed" resultType ="java.lang.Long" > select ne.nursing_id from device d left join elder e on e.bed_id = d.binding_location left join nursing_elder ne on ne.elder_id = e.id where d.location_type = 1 and d.physical_location_type = 2 and d.iot_id = #{iotId} </select >
通过角色名称查询用户id 表关系
需要在SysUserRoleMapper(zzyl-system模块下)中新增方法,通过角色名称查询用户id
1 2 @Select("select sur.user_id from sys_user_role sur left join sys_role sr on sur.role_id = sr.role_id where sr.role_name = #{roleName}") List<Long> selectUserIdByRoleName (String roleName) ;
定时任务 在zzyl-nursing-platform中新增定时任务类,启动数据过滤
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.zzyl.nursing.task;import com.zzyl.nursing.service.IAlertRuleService;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component;@Component @Slf4j public class AlertTask { @Autowired private IAlertRuleService alertRuleService; public void deviceDataAlertFilter () { alertRuleService.alertFilter(); } }
核心过滤逻辑 在AlertRuleService中新增方法,过滤数据,如下:
实现方法(在编写代码的过程中,需要打开流程图,按图编写代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 @Autowired private SysUserRoleMapper userRoleMapper;@Autowired private DeviceMapper deviceMapper;@Autowired private IAlertDataService alertDataService;@Value("${alert.deviceMaintainerRole}") private String deviceMaintainerRole;@Value("${alert.managerRole}") private String managerRole;@Autowired private RedisTemplate<String, String> redisTemplate;@Override public void alertFilter () { long count = count(Wrappers.<AlertRule>lambdaQuery().eq(AlertRule::getStatus, 1 )); if (count <= 0 ) { return ; } List<Object> values = redisTemplate.opsForHash().values(CacheConstants.IOT_DEVICE_LAST_DATA); if (CollUtil.isEmpty(values)) { return ; } List<DeviceData> deviceDatas = new ArrayList <>(); values.forEach(v -> deviceDatas.addAll(JSONUtil.toList(v.toString(), DeviceData.class))); deviceDatas.forEach(d -> alertFilter(d)); } private void alertFilter (DeviceData deviceData) { LocalDateTime alarmTime = deviceData.getAlarmTime(); long between = LocalDateTimeUtil.between(alarmTime, LocalDateTime.now(), ChronoUnit.SECONDS); if (between > 60 ) { return ; } List<AlertRule> allRules = list(Wrappers.<AlertRule>lambdaQuery() .eq(AlertRule::getProductKey, deviceData.getProductKey()) .eq(AlertRule::getIotId, "-1" ) .eq(AlertRule::getFunctionId, deviceData.getFunctionId()) .eq(AlertRule::getStatus, 1 )); List<AlertRule> iotIdRules = list(Wrappers.<AlertRule>lambdaQuery() .eq(AlertRule::getProductKey, deviceData.getProductKey()) .eq(AlertRule::getIotId, deviceData.getIotId()) .eq(AlertRule::getFunctionId, deviceData.getFunctionId()) .eq(AlertRule::getStatus, 1 )); Collection<AlertRule> allArertRules = CollUtil.addAll(allRules, iotIdRules); if (CollUtil.isEmpty(allArertRules)) { return ; } allArertRules.forEach(alertRule -> deviceDataAlarmHandler(alertRule, deviceData)); } private void deviceDataAlarmHandler (AlertRule rule, DeviceData deviceData) { String[] split = rule.getAlertEffectivePeriod().split("~" ); LocalTime startTime = LocalTime.parse(split[0 ]); LocalTime endTime = LocalTime.parse(split[1 ]); LocalTime time = LocalDateTimeUtil.of(deviceData.getAlarmTime()).toLocalTime(); if (time.isBefore(startTime) || time.isAfter(endTime)) { return ; } String iotId = deviceData.getIotId(); String aggCountKey = CacheConstants.ALERT_TRIGGER_COUNT_PREFIX + iotId + ":" + deviceData.getFunctionId() + ":" + rule.getId(); int compare = NumberUtil.compare(Double.valueOf(deviceData.getDataValue()), rule.getValue()); if ((rule.getOperator().equals(">=" ) && compare >= 0 ) || (rule.getOperator().equals("<" ) && compare < 0 )) { log.info("当前上报的数据符合规则异常" ); } else { redisTemplate.delete(aggCountKey); return ; } String silentKey = CacheConstants.ALERT_SILENT_PREFIX + iotId + ":" + deviceData.getFunctionId() + ":" + rule.getId(); String silentData = redisTemplate.opsForValue().get(silentKey); if (StringUtils.isNotEmpty(silentData)) { return ; } String aggData = redisTemplate.opsForValue().get(aggCountKey); int count = StringUtils.isEmpty(aggData) ? 1 : Integer.parseInt(aggData) + 1 ; if (ObjectUtil.notEqual(count, rule.getDuration())) { redisTemplate.opsForValue().set(aggCountKey, count + "" ); return ; } redisTemplate.delete(aggCountKey); redisTemplate.opsForValue().set(silentKey, "1" , rule.getAlertSilentPeriod(), TimeUnit.MINUTES); List<Long> userIds = new ArrayList <>(); if (rule.getAlertDataType().equals(0 )) { if (deviceData.getLocationType().equals(0 )) { userIds = deviceMapper.selectNursingIdsByIotIdWithElder(iotId); } else if (deviceData.getLocationType().equals(1 ) && deviceData.getPhysicalLocationType().equals(2 )) { userIds = deviceMapper.selectNursingIdsByIotIdWithBed(iotId); } } else { userIds = userRoleMapper.selectUserIdByRoleName(deviceMaintainerRole); } List<Long> managerIds = userRoleMapper.selectUserIdByRoleName(managerRole); Collection<Long> allUserIds = CollUtil.addAll(userIds, managerIds); allUserIds = CollUtil.distinct(allUserIds); insertAlertData(allUserIds, rule, deviceData); } private void insertAlertData (Collection<Long> allUserIds, AlertRule rule, DeviceData deviceData) { AlertData alertData = BeanUtil.toBean(deviceData, AlertData.class); alertData.setAlertRuleId(rule.getId()); String alertReason = CharSequenceUtil.format("{}{}{},持续{}个周期就报警" , rule.getFunctionName(), rule.getOperator(), rule.getValue(), rule.getDuration()); alertData.setAlertReason(alertReason); alertData.setStatus(0 ); alertData.setType(rule.getAlertDataType()); List<AlertData> list = allUserIds.stream().map(userId -> { AlertData dbAlertData = BeanUtil.toBean(alertData, AlertData.class); dbAlertData.setUserId(userId); dbAlertData.setId(null ); return dbAlertData; }).collect(Collectors.toList()); alertDataService.saveBatch(list); }
逻辑相对复杂,一定要对照流程图阅读代码理解
上述代码中多次用到了redis,并且定义了常量来表示redis的key,需要在CacheConstants新增常量,如下:
1 2 3 4 5 6 7 8 public static final String ALERT_TRIGGER_COUNT_PREFIX = "iot:alert_trigger_count:" ;public static final String ALERT_SILENT_PREFIX = "iot:alert_silent:" ;
上述代码中,对于超级管理员和维修工是通过配置的方式读取的名字,需要在application-dev.yml文件中添加数据
1 2 3 alert: deviceMaintainerRole: 维修工 managerRole: 超级管理员
报警通知提醒 需求说明 当设备上报的数据触发了报警规则之后,需要及时提醒相关人员来处理,为了更快的让相应的负责人收到消息,一旦监测到异常数据,可以给相关负责人发送通知,例如:
对于当前项目来说,可以在后台管理系统中进行通知,效果如下:
要想实现这个功能,需要使用一个新的技术:WebSocket
功能实现 实现思路
环境集成
导入依赖
在zzyl-nursing-platform中如导入以下依赖
1 2 3 4 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
定义WebSocket服务和注册WebSocket
配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.zzyl.nursing.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.socket.server.standard.ServerEndpointExporter;@Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter () { return new ServerEndpointExporter (); } }
WebSocketServer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 package com.zzyl.nursing.config;import cn.hutool.core.collection.CollUtil;import cn.hutool.core.util.ObjectUtil;import cn.hutool.json.JSONUtil;import com.zzyl.common.exception.base.BaseException;import com.zzyl.nursing.vo.AlertNotifyVo;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;import org.springframework.util.CollectionUtils;import org.springframework.web.socket.config.annotation.EnableWebSocket;import javax.websocket.*;import javax.websocket.server.PathParam;import javax.websocket.server.ServerEndpoint;import java.io.IOException;import java.util.Collection;import java.util.HashMap;import java.util.List;import java.util.Map;@Slf4j @Component @EnableWebSocket @ServerEndpoint("/ws/{sid}") public class WebSocketServer { private static Map<String, Session> sessionMap = new HashMap <>(); @OnOpen public void onOpen (Session session, @PathParam("sid") String sid) { log.info("有客户端连接到了服务器 , {}" , sid); sessionMap.put(sid, session); } @OnMessage public void onMessage (Session session, String message, @PathParam("sid") String sid) { log.info("接收到了客户端 {} 发来的消息 : {}" , sid, message); } @OnClose public void onClose (Session session, @PathParam("sid") String sid) { System.out.println("连接断开:" + sid); sessionMap.remove(sid); } @OnError public void onError (Session session, @PathParam("sid") String sid, Throwable throwable) { System.out.println("出现错误:" + sid); throwable.printStackTrace(); } public void sendMessageToAll (String message) throws IOException { Collection<Session> sessions = sessionMap.values(); if (!CollectionUtils.isEmpty(sessions)) { for (Session session : sessions) { session.getBasicRemote().sendText(message); } } } public void sendMessageToConsumer (AlertNotifyVo alertNotifyVo, Collection<Long> userIds) { if (CollUtil.isEmpty(userIds)) { return ; } if (ObjectUtil.isEmpty(sessionMap)) { return ; } userIds.forEach(userId -> { Session session = sessionMap.get(String.valueOf(userId)); if (ObjectUtil.isEmpty(session)) { return ; } try { session.getBasicRemote().sendText(JSONUtil.toJsonStr(alertNotifyVo)); } catch (IOException e) { throw new BaseException ("websocket推送消息失败" ); } }); } }
消息通知Vo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 package com.zzyl.nursing.vo;import lombok.AllArgsConstructor;import lombok.Builder;import lombok.Data;import lombok.NoArgsConstructor;@Data @Builder @NoArgsConstructor @AllArgsConstructor public class AlertNotifyVo { private Long id; private String accessLocation; private Integer locationType; private Integer physicalLocationType; private String deviceDescription; private String productName; private String functionName; private String dataValue; private Integer alertDataType; private Integer voiceNotifyStatus; private Integer notifyType; private Boolean isAllConsumer; }
报警数据推送到前端 修改AlertRuleServiceImpl类,添加推送报警数据逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 private void deviceDataAlarmHandler (DeviceData deviceData, AlertRule rule) { String[] split = rule.getAlertEffectivePeriod().split("~" ); LocalTime startTime = LocalTime.parse(split[0 ]); LocalTime endTime = LocalTime.parse(split[1 ]); LocalTime localTime = deviceData.getAlarmTime().toLocalTime(); if (localTime.isBefore(startTime) || localTime.isAfter(endTime)) { return ; } String aggCountKey = CacheConstants.IOT_COUNT_ALERT+deviceData.getIotId()+":" +deviceData.getFunctionId()+":" +rule.getId(); Double dataValue = Double.valueOf(deviceData.getDataValue()); Double value = rule.getValue(); int compare = NumberUtil.compare(dataValue, value); if ((rule.getOperator().equals(">=" ) && compare >= 0 ) || (rule.getOperator().equals("<" ) && compare < 0 )){ log.info("当前数据符合报警规则" ); } else { log.info("正常上报的数据" ); redisTemplate.delete(aggCountKey); return ; } String silentKey = CacheConstants.IOT_SILENT_ALERT+deviceData.getIotId()+":" +deviceData.getFunctionId()+":" +rule.getId(); String silentData = redisTemplate.opsForValue().get(silentKey); if (StringUtils.isNotEmpty(silentData)){ return ; } String aggCountData = redisTemplate.opsForValue().get(aggCountKey); Integer count = StringUtils.isEmpty(aggCountData)? 1 : (Integer.parseInt(aggCountData) + 1 ); if (ObjectUtil.notEqual(count,rule.getDuration())){ redisTemplate.opsForValue().set(aggCountKey,count+"" ); return ; } redisTemplate.opsForValue().set(silentKey,"1" ,rule.getAlertSilentPeriod(), TimeUnit.MINUTES); redisTemplate.delete(aggCountKey); List<Long> userIds = new ArrayList <>(); if (rule.getAlertDataType().equals(0 )){ if (deviceData.getLocationType().equals(0 )){ userIds = deviceMapper.selectNursingIdsByIotIdWithElder(deviceData.getIotId()); }else if (deviceData.getLocationType().equals(1 ) && deviceData.getPhysicalLocationType().equals(2 )){ userIds = deviceMapper.selectNursingIdsByIotIdWithBed(deviceData.getIotId()); } }else { userIds = sysUserRoleMapper.selectByRoleName("维修工" ); } List<Long> managerIds = sysUserRoleMapper.selectByRoleName("超级管理员" ); Collection<Long> allUserIds = CollUtil.addAll(userIds, managerIds); allUserIds = CollUtil.distinct(allUserIds); List<AlertData> alertDataList = insertAlertData(allUserIds, deviceData, rule); webSocketNotity(alertDataList.get(0 ), rule, allUserIds); } @Autowired private WebSocketServer webSocketServer;private void webSocketNotity (AlertData alertData, AlertRule rule, Collection<Long> allUserIds) { AlertNotifyVo alertNotifyVo = BeanUtil.toBean(alertData, AlertNotifyVo.class); alertNotifyVo.setAccessLocation(alertData.getRemark()); alertNotifyVo.setFunctionName(alertRule.getFunctionName()); alertNotifyVo.setAlertDataType(alertRule.getAlertDataType()); alertNotifyVo.setNotifyType(1 ); webSocketServer.sendMessageToConsumer(alertNotifyVo, ids); } @Autowired private IAlertDataService alertDataService;private List<AlertData> insertAlertData (Collection<Long> allUserIds, DeviceData deviceData, AlertRule rule) { AlertData alertData = BeanUtil.toBean(deviceData, AlertData.class); alertData.setAlertRuleId(rule.getId()); String reason = CharSequenceUtil.format("{}{}{},持续了{}周期,就报警" ,rule.getFunctionId(), rule.getOperator(),rule.getValue(),rule.getDuration()); alertData.setAlertReason(reason); alertData.setStatus(0 ); alertData.setType(rule.getAlertDataType()); List<AlertData> list = allUserIds.stream().map(userId -> { AlertData dBalertData = BeanUtil.toBean(alertData, AlertData.class); dBalertData.setUserId(userId); dBalertData.setId(null ); return dBalertData; }).collect(Collectors.toList()); alertDataService.saveBatch(list); return list; }
前端环境配置 用资料中提供的index.vue替换src/layout目录下的index.vue(主要是集成WebSocket)
修改前端的ws的连接地址:
注意:是ws 不是wss
由于前端要发请求到后端建立连接,所以还需要在SpringSecurity中放行ws 开头的请求
放行代码
中州养老项目总结 若依框架的使用 若依框架的使用,重点关注三个功能:代码生成、表单构建、定时任务
代码生成功能 :若依提供的代码生成功能可以基于数据库表结构快速生成前后端基础增删改查代码和系统菜单
表单构建功能 :若依提供的表单构建功能可以通过图形化界面拖拉拽的形式快速生成复杂的页面表单
定时任务功能 :若依提供的定时任务功能可以在系统页面管理系统中的定时任务以及查看定时任务的执行情况
入退管理模块 入退管理模块是项目中一个比较重要的模块,涉及到健康评估和入住办理两个功能:
健康评估 :老人在入住养老院之前,需要提供一份最近完成的PDF版本的体检报告,在系统中上传体检报告后(从PDF中提取体检信息文本),AI大模型会自动分析体检报告并给出分析结果
入住办理 :当老人确定要入住养老院后,可以在系统上发起入住办理,在入住办理页面填写老人的基本信息、家属信息、入住配置信息和签约办理信息 ,就可以让老人入住到养老院中
在住管理模块 在住管理模块包括三个功能:房型设置、床位预览、智能床位
房型设置 :在房型设置页面可以维护养老院中的房型,包括房间图片、房型类型、床位费用、房型介绍 等
床位预览 :在床位预览页面可以维护养老院中的楼层、楼层下的房间、房间中的床位
智能床位 :智能床位功能展示的是养老院中有智能床位的楼层、楼层下有智能设备的房间和床位信息以及智能设备最近一次上报的数据
服务管理模块 服务管理模块包括四个功能:
护理项目 :在护理项目页面可以维护养老院中能够提供的护理项目 ,例如:洗头、洗脚、助餐等
护理计划 :在护理计划页面可以维护养老院中的护理计划 ,一个护理计划中可以关联多个护理项目
护理等级 :在护理等级页面可以维护养老院中的护理等级 ,一个护理等级唯一对应一个护理计划
负责老人 :在负责老人页面可以展示出有老人入住的楼层,以及楼层下的房间和床位信息,可以单独维护某一个床位的护理人员,也可以批量维护一个房间中所有床位上的护理人员
智能监测模块 智能监测模块包括三个功能:设备管理、报警规则、报警数据
设备管理 :在设备管理页面可以维护华为云IOT平台的设备,并将设备和系统中的位置(老人、床位、房间)进行绑定
报警规则 :在报警规则页面可以维护系统中的报警规则,方便通过报警规则过滤异常数据
报警数据 :在报警数据页面可以看到符合报警规则的数据,并能对报警数据进行处理
微信小程序模块 微信小程序模块包括:微信小程序登录、绑定家人、参观预约
微信小程序登录 :在微信小程序端获取用户授权后,调用微信开放平台提供的登录接口进行登录
绑定家人 :在微信小程序端绑定正在养老院入住的家人信息 ,方便发起探访预约申请和查看家人的身体健康数据等
参观预约 :可以在微信小程序端发起参观预约申请 ,并在预约的时间到养老院进行参观