48 Commits
Author SHA1 Message Date
lianlonggang 177e0e4d97 feat(watch): listCustom 接口与导出增加 eventId、locateTime 字段
1. AbnormalEvent 新增 eventId、locateTime 字段
2. listCustom/largeScreenListCustom/listCustomForMap 三个接口返回新字段
3. 导出 Excel 通过 @Excel 注解自动支持新字段(顺序 12、13)
4. locateTime 时间格式化为 yyyy-MM-dd HH:mm:ss
5. 修正 createMonitorData 中遗漏的 eventId 与 locateTime 字段赋值
6. DDL 表名修正为 QH_WAT_MONITOR_DATA(项目实际表名)
2026-09-03 13:43:26 +08:00
lianlonggang f74a2322e4 chore(db): 将已执行完成的 SQL 脚本从 updatesql 迁移至 done 目录
将历史已执行完毕的 SQL 更新脚本统一归档至 db/done/ 目录,
便于区分待执行脚本与已完成脚本。
2026-09-03 10:43:23 +08:00
lianlonggang 822cdbf539 feat(watch): notice 入参新增 eventId、locateTime;新增 updateMonitorGps 接口
1. WatchNotifyVo、WatchMonitorData 新增 eventId 与 locateTime 字段
2. 新增 POST /watchApi/updateMonitorGps 接口
3. watchNo + eventId 联合定位告警数据,任一缺失则忽略
4. 已存在 lon/lat 数据则忽略本次更新,避免覆盖
5. GPS84 → GCJ02 坐标转换 + 高德地址反查(参考 createMonitorData 逻辑)
6. 仅更新 GPS 相关字段,不推送大屏与短信
2026-09-03 10:40:14 +08:00
lianlonggang d75f21c17e feat(db): watch_monitor_data 表新增 EVENT_ID 与 LOCATE_TIME 字段
为支持手表告警事件与定时GPS位置上报的关联,新增两个字段:
- EVENT_ID(事件ID,用于告警事件与GPS位置更新关联)
- LOCATE_TIME(定位时间,手表定时上报的GPS定位时间)

同时新增 IDX_EVENT_ID 单列索引与 IDX_WATCH_NO_EVENT_ID 联合索引,
优化 watchNo + eventId 联合定位查询性能。

数据库:118-DM 达梦数据库,SQL 全大写规范。
2026-09-03 10:40:02 +08:00
lianlonggangandClaude Opus 4.7 ffee8611fe fix(watch): 恢复设备导入校验和手表绑定单位限制,新增未分配设备数统计
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 11:05:33 +08:00
lianlonggang 0e8f5e7b14 feat(watch): 导入设备绑定时,手表所属单位以员工信息为准 2026-05-06 14:47:33 +08:00
lianlonggang 115407026f fix(device): 解决设备导入和绑定功能的部门权限问题
- 移除设备导入时的部门必填验证,允许为空时默认归入新疆油田公司(A01)
- 移除设备绑定时的跨单位限制检查
- 在设备绑定时根据员工信息更新手表的单位信息
- 修复部门统计数据查询中的层级展示逻辑
- 优化部门统计服务中的注释文档和代码结构
- 添加设备表更新操作中单位编码和部门ID的条件设置
2026-04-30 14:13:26 +08:00
lianlonggang 9f545952e4 fix(upload): 修复文件上传和权限配置问题
- 修正 MyUploadUtil 中 tar/apk 文件上传调用参数,添加必需的布尔参数
- 注释掉 Shiro 配置中 /sys/upload/down/** 和 /sys/upload/show/** 的匿名访问权限
- 解决文件上传功能与安全配置的兼容性问题
2026-04-30 08:49:19 +08:00
lianlonggang 1988e031e5 feat(file-upload): 集成新疆OSS文件上传功能并优化设备删除逻辑
- 实现新疆油田平台OSS文件上传工具类XjOssUtil,支持OAuth2鉴权和文件操作
- 在MyUploadUtil中集成OSS上传逻辑,通过oss_flag配置动态选择上传方式
- 为.tar和.apk文件强制使用MyUploadUtil上传,其他文件根据oss_flag配置决定上传方式
- 在手表设备删除接口中增加绑定用户检查,已绑定用户的手表不允许删除
- 添加GlobalManager依赖注入以支持动态配置读取
- 实现文件上传流程的日志记录和错误处理机制
2026-04-21 17:22:09 +08:00
lianlonggang 44b0f6ca2b feat(care-user): 添加年度参数并导入日期工具类
- 导入 cn.hutool.core.date.DateUtil 工具类
- 新增 year 字段用于存储当前年份
- 使用 DateUtil.thisYear() 设置默认值为当前年份
2026-04-14 10:34:10 +08:00
lianlonggang ae866f0791 fix(controller): 禁止移动端解绑手表设备并优化异步处理
- 移动端解绑设备接口返回错误提示,需联系健康管理员解绑
- 设备绑定流程中的警告开关设置改为异步执行
- 防止外部系统异常影响主业务流程
- 解绑操作中移除对数据中心的通知调用
2026-04-10 10:28:06 +08:00
wanghaoandClaude Sonnet 4.6 4610ac4737 fix(健康监测): 禁用移动端手表解绑功能,改为提示联系管理员操作
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 17:19:35 +08:00
wanghaoandClaude Opus 4.6 6d6920a2b3 fix(健康监测): 修复手表绑定单位校验使用前端传入orgCode导致的校验失效问题
将绑定校验的orgCode来源从dto改为watchDevice实体,
避免前端未传orgCode时校验必定失败,同时消除伪造绕过风险

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 14:41:42 +08:00
hejiayang c7041fa69e fix: 解决用户搜索中HTML转义字符显示问题(漏) 2026-03-31 14:12:18 +08:00
wanghao 370f5e0089 Merge remote-tracking branch 'origin/develop' into develop 2026-03-30 18:29:06 +08:00
hejiayang a47f27967e fix: 防止HTML注入(用户姓名中的 · ),对用户输入姓名进行转义处理
- 在AccountDTO中对doctorName字段进行HTML转义处理
- 在ConDoctorDTO中对doctorName字段进行HTML转义处理
- 在ConEvaluateDTO中对userName和expertName字段进行HTML转义处理
- 在ConHealthInfoAnswerDTO中对fromName字段进行HTML转义处理
- 在ConHelperDTO中对userName字段进行HTML转义处理
- 在ConSessionDTO中对fromName和doctorName字段进行HTML转义处理
- 在CallLogDTO中对realName字段进行HTML转义处理
- 在PhoneListDTO中对realName字段进行HTML转义处理
- 在TblQimoNoticeDTO中对realName字段进行HTML转义处理
- 在EmergencyProfessionUserController中对name参数进行HTML转义处理
- 在OrderServiceImpl中对salvageUserName字段进行HTML转义处理
- 在AbnormalEventFilter中对userName字段进行HTML转义处理
- 在CalorieDataByUserDTO中对realName字段进行HTML转义处理
- 在WatchBindHisServiceImpl中对bindRealName字段进行HTML转义处理
- 在WatchDataServiceImpl中对username字段进行HTML转义处理
- 在WatchDeviceServiceImpl中对bindRealName字段进行HTML转义处理
- 在HealthUserEmployeeExController中对realname字段进行HTML转义处理
- 在SysUserController中对realname字段进行HTML转义处理
- 在SysUserDepartChangeApplyController中对userName参数进行HTML转义处理
- 在SelectWrapperDTO中对realName字段进行HTML转义处理
- 在SysBaseApiImpl中对username参数进行HTML转义处理
- 在SysUserServiceImpl中对name参数进行HTML转义处理
2026-03-30 17:54:36 +08:00
wanghaoandClaude Sonnet 4.6 6c08f5f141 fix(用户管理): 修复不传 realname 时查询用户列表抛 NPE 的问题
对 realname 参数增加非空判断,避免 null 值时调用 replaceAll 抛出 NullPointerException

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 17:22:05 +08:00
lianlonggang 690f688c6a 屏蔽pom文件不需要的配置 2026-03-30 16:07:27 +08:00
wanghaoandClaude Sonnet 4.6 63cc815022 fix(用户管理): 修复用户查询时名字中间隔号被 HTML 实体转义的问题
将查询参数中的 &middot; 转义回 · 后再执行查询

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 14:54:56 +08:00
wanghaoandClaude Sonnet 4.6 8f00d07377 feat(健康咨询): 新增音视频咨询超时关闭功能及定时任务
- 新增 endVideoSession 接口,供手动触发和定时任务调用
- Mapper 层添加 UPDATE SQL:预约日期已过的视频咨询,有通话记录改为待评价(4),无记录改为取消(6)
- SyncScheduleJob 新增 XXL-Job 任务 endVideoSession,每日自动处理过期视频咨询

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 14:54:33 +08:00
wanghaoandClaude Sonnet 4.6 2e90ec71cc fix(手表设备): 手表绑定时校验只能绑定本单位人员
新增单位编码前6位匹配校验,禁止跨单位绑定手表;
同时将"该设备已绑定该员工"改为"该设备已被绑定"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 15:48:55 +08:00
wanghao a43806c4e9 fix(手表设备): 优化错误提示信息,将"该工具不存在"改为"该监测工具不存在" 2026-03-18 14:47:39 +08:00
wanghao 237df3ef38 修改移动端手表绑定接口 2026-03-12 17:28:52 +08:00
wanghao bd9fece99c 新增移动端手表绑定解绑接口 2026-03-12 16:16:58 +08:00
pengjie 47cebd3392 【用户信息】增加返回字段,加密接口 2026-03-06 17:01:49 +08:00
wanghao 61e049cc47 手表数据修改 2026-03-05 17:05:31 +08:00
wanghao ad03177e50 手表数据修改 2026-03-05 17:00:18 +08:00
wanghao 3db3301fe3 手表数据修改 2026-03-05 16:10:31 +08:00
wanghao fa9a186a5e 手表数据修改 2026-03-05 15:53:40 +08:00
wanghao 3cea926a1e 手表接口修改 2026-03-04 17:23:32 +08:00
wanghao fe82afd934 手表接口修改 2026-03-04 15:30:07 +08:00
wanghao 96242b5c00 手表数据返回处理 2026-03-03 17:22:20 +08:00
wanghao 291f6a4eaa 移动端首页手表数据修改 2026-03-03 10:13:15 +08:00
wanghao 98dc17552d 异常事件导出增加回传时间 2026-02-27 11:14:42 +08:00
lianlonggang e9784f7957 资源情况监测方法 2026-02-12 11:29:36 +08:00
wanghao 175238e291 调整移动端消息通知倒序 2026-02-11 16:42:30 +08:00
wanghao 3c822d581c 调整消息通知sql 2026-02-11 16:40:47 +08:00
wanghao 77d72d1d78 移动端通知接口优化 2026-02-11 15:33:14 +08:00
pengjie 9dbdd911d9 appbanner图 2026-02-11 11:03:33 +08:00
pengjie 0022497f64 appbanner图 2026-02-11 10:50:05 +08:00
pengjie 9714e97a71 appbanner图 2026-02-11 10:45:31 +08:00
wanghao 65f5755422 移动端首页通知 2026-02-11 10:07:27 +08:00
feng 677b4d5080 fix 2026-02-11 09:55:11 +08:00
wanghao 49f316dd82 移动端首页获取图片接口排除 2026-02-10 17:58:05 +08:00
pengjie b6aecf4e64 用户信息查询 2026-02-10 15:45:36 +08:00
pengjie cd7f5661de 【积分银行】积分 2026-02-09 17:03:19 +08:00
pengjie 6e005f2f4e 【积分银行】积分 2026-02-09 16:30:50 +08:00
wanghao 03f8be4c98 获取指定图片url 2026-02-09 16:23:33 +08:00
125 changed files with 3190 additions and 395 deletions
@@ -31,5 +31,5 @@ CREATE TABLE `sys_message_notice_scope` (
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `idx_notice_id` (`notice_id`), KEY `idx_notice_id` (`notice_id`),
KEY `idx_type` (`type`), KEY `idx_type` (`type`),
KEY `idx_scope_id` (`scope_id`) KEY `idx_scope_notice` (`scope_id`, `notice_id`) COMMENT '联表查询复合索引支持WHERE scope_id + JOIN notice_id'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息通知范围表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息通知范围表';
@@ -0,0 +1,10 @@
-- QH_WAT_MONITOR_DATA 新增 EVENT_ID事件ID LOCATE_TIME定位时间字段
-- 数据库118-DM 达梦数据库SQL 全大写规范
ALTER TABLE "QH_WAT_MONITOR_DATA" ADD "EVENT_ID" VARCHAR(64) DEFAULT NULL;
ALTER TABLE "QH_WAT_MONITOR_DATA" ADD "LOCATE_TIME" DATETIME DEFAULT NULL;
COMMENT ON COLUMN "QH_WAT_MONITOR_DATA"."EVENT_ID" IS '事件ID';
COMMENT ON COLUMN "QH_WAT_MONITOR_DATA"."LOCATE_TIME" IS '定位时间';
CREATE INDEX "IDX_EVENT_ID" ON "QH_WAT_MONITOR_DATA"("EVENT_ID");
CREATE INDEX "IDX_WATCH_NO_EVENT_ID" ON "QH_WAT_MONITOR_DATA"("WATCH_NO", "EVENT_ID");
@@ -99,7 +99,7 @@ public enum AnYanApiEnum {
/** /**
* 18.查询关爱联络员信息及急救人员查询接口 * 18.查询关爱联络员信息及急救人员查询接口
*/ */
QUERY_EMPLOYEE_CARE_PERSON("/aygc-znhpt-sys/0/api/empCareEmp/findPageByIdNo", "", HttpMethod.POST), QUERY_EMPLOYEE_CARE_PERSON("/aygc-znhpt-sys/0/api/empCareEmp/findPage", "", HttpMethod.POST),
/** /**
* 19.批量更新员工关键信息接口 * 19.批量更新员工关键信息接口
@@ -1,5 +1,6 @@
package com.renkang.anyan.model.dto; package com.renkang.anyan.model.dto;
import cn.hutool.core.date.DateUtil;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -34,4 +35,6 @@ public class CareUserDetailsParam {
@Schema(title = "pageType") @Schema(title = "pageType")
private String pageType = "plus"; private String pageType = "plus";
private int year = DateUtil.thisYear();
} }
@@ -15,7 +15,7 @@ public enum PointsSourceEnum {
TYPE_2_VIOLATION(2, "violation", "违规扣除"), TYPE_2_VIOLATION(2, "violation", "违规扣除"),
TYPE_2_EXPIRED(2, "expired", "过期清除"); TYPE_2_EXPIRED(2, "expired", "过期清除");
/** /**
*类型1-获得积分,2-扣减积分 * 类型1-获得积分,2-扣减积分
*/ */
public final Integer changeType; public final Integer changeType;
/** /**
@@ -23,7 +23,7 @@ public enum PointsSourceEnum {
*/ */
public final String source; public final String source;
/** /**
*来源描述 * 来源描述
*/ */
public final String desc; public final String desc;
@@ -6,6 +6,9 @@ import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.jeecg.base.PageInfo; import org.jeecg.base.PageInfo;
import java.util.Date;
import java.util.List;
/** /**
* @author pj * @author pj
* @version 1.0 * @version 1.0
@@ -16,7 +19,27 @@ import org.jeecg.base.PageInfo;
@Data @Data
public class UserPointsRankingDTO extends PageInfo<PointsRule> { public class UserPointsRankingDTO extends PageInfo<PointsRule> {
@Schema(title = "榜单类型,0榜,1周榜,2月榜") @Schema(title = "榜单类型,0榜,1周榜,2月榜3季榜,4年榜,5选时排名,10总榜")
private String rankType; private String rankType;
@Schema(title = "开始时间")
private Date startTime;
@Schema(title = "截止时间")
private Date endTime;
@Schema(title = "所属单位")
private String orgCode;
@Schema(title = "所属部门")
private String deptCode;
@Schema(title = "姓名")
private String realName;
@Schema(title = "员工编号")
private String workNo;
private List<String> userIds;
} }
@@ -81,4 +81,16 @@ public class PointsRule implements Serializable {
@Schema(title = "最后修改时间") @Schema(title = "最后修改时间")
private Date lastModifyDate; private Date lastModifyDate;
@Schema(title = "周期内打卡次数上限")
private Integer checkInUpper;
@Schema(title = "年额外积分上限")
private BigDecimal yearExtraPointUpper;
@Schema(title = "是否支持打卡叠加得分,0不支持,1支持")
private Integer supportCum;
@Schema(title = "是否需要填写补充信息,0不需要,1需要")
private Integer needRemark;
} }
@@ -9,6 +9,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable; import java.io.Serializable;
@@ -40,9 +41,11 @@ public class UserPoints implements Serializable {
@Schema(title = "用户部门") @Schema(title = "用户部门")
private String orgCode; private String orgCode;
@Excel(name = "当前可用积分", width = 15, orderNum = "7")
@Schema(title = "当前可用积分") @Schema(title = "当前可用积分")
private BigDecimal points; private BigDecimal points;
@Excel(name = "累计获得积分", width = 15, orderNum = "8")
@Schema(title = "历史获得的积分总和") @Schema(title = "历史获得的积分总和")
private BigDecimal addPointsSum; private BigDecimal addPointsSum;
@@ -55,6 +58,7 @@ public class UserPoints implements Serializable {
/** /**
* 积分兑换记录总和,只记录积分兑换 * 积分兑换记录总和,只记录积分兑换
*/ */
@Excel(name = "累计兑换积分", width = 15, orderNum = "9")
@Schema(title = "历史兑换积分总和") @Schema(title = "历史兑换积分总和")
private BigDecimal exchangePointsSum; private BigDecimal exchangePointsSum;
@@ -71,6 +75,7 @@ public class UserPoints implements Serializable {
@Schema(title = "最新一天消耗积分的总和") @Schema(title = "最新一天消耗积分的总和")
private BigDecimal latestDeductDaySum; private BigDecimal latestDeductDaySum;
@Excel(name = "最近一次打卡时间", width = 25, format = "yyyy-MM-dd HH:mm:ss", orderNum = "10")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(title = "最新获得积分的日期") @Schema(title = "最新获得积分的日期")
@@ -2,6 +2,7 @@ package com.renkang.bank.model.vo;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import org.jeecgframework.poi.excel.annotation.Excel;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -14,27 +15,44 @@ import java.math.BigDecimal;
@Data @Data
public class UserPointRankingVO { public class UserPointRankingVO {
@Excel(name = "排名", width = 15, orderNum = "8")
@Schema(title = "排名") @Schema(title = "排名")
private Integer ranking; private Integer ranking;
@Schema(title = "用户id") @Schema(title = "用户id")
private String userId; private String userId;
@Excel(name = "姓名", width = 15, orderNum = "3")
@Schema(title = "姓名") @Schema(title = "姓名")
private String realName; private String realName;
@Excel(name = "员工编号", width = 15, orderNum = "4")
@Schema(title = "员工编号")
private String workNo;
@Excel(name = "年龄", width = 15, orderNum = "6")
@Schema(title = "年龄")
private Integer age;
@Schema(title = "头像") @Schema(title = "头像")
private String avatar; private String avatar;
@Excel(name = "所属单位", width = 15, orderNum = "1")
@Schema(title = "单位名称") @Schema(title = "单位名称")
private String orgName; private String orgName;
@Excel(name = "所属部门", width = 15, orderNum = "2")
@Schema(title = "部门名称") @Schema(title = "部门名称")
private String deptName; private String deptName;
@Schema(title = "性别") @Schema(title = "性别")
private Integer sex; private Integer sex;
@Excel(name = "性别", width = 15, orderNum = "5")
@Schema(title = "性别")
private String sexName;
@Excel(name = "日累计积分", width = 15, orderNum = "7")
@Schema(title = "积分") @Schema(title = "积分")
private BigDecimal pointSum; private BigDecimal pointSum;
} }
@@ -0,0 +1,48 @@
package com.renkang.bank.model.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.util.Date;
/**
* 用户积分详情VO
*
* @author feng
* @date 2026-02-05
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(title = "用户积分详情VO", description = "用户积分详情VO")
public class UserPointsDeductDetailsVO {
@Schema(title = "id")
private Long id;
@Schema(title = "用户ID")
private String userId;
@Excel(name = "积分变化", width = 15, orderNum = "2")
@Schema(title = "本次积分变化")
private BigDecimal points;
@Excel(name = "支出信息", width = 25, orderNum = "1")
@Schema(title = "补充信息")
private String remark;
@Schema(title = "关联业务ID(规则ID,礼品兑换订单Id等)")
private Long businessId;
@Excel(name = "支出时间", width = 25, format = "yyyy-MM-dd HH:mm:ss", orderNum = "3")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(title = "创建时间")
private Date createTime;
}
@@ -0,0 +1,56 @@
package com.renkang.bank.model.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.util.Date;
/**
* 用户积分详情VO
*
* @author feng
* @date 2026-02-05
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(title = "用户积分详情VO", description = "用户积分详情VO")
public class UserPointsDetailsVO {
@Schema(title = "id")
private Long id;
@Schema(title = "用户ID")
private String userId;
@Excel(name = "积分变化", width = 15, orderNum = "4")
@Schema(title = "本次积分变化")
private BigDecimal points;
@Excel(name = "补充信息", width = 15, orderNum = "2")
@Schema(title = "补充信息")
private String remark;
@Schema(title = "关联业务ID(规则ID,礼品兑换订单Id等)")
private Long businessId;
@Excel(name = "健康行为", width = 25, orderNum = "1")
@Schema(title = "关联业务详情(可选择性的冗余业务详情:待定)")
private String businessInfo;
@Excel(name = "打卡时间", width = 25, format = "yyyy-MM-dd HH:mm:ss", orderNum = "5")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(title = "创建时间")
private Date createTime;
@Excel(name = "行为周期", width = 15, replace = {"次_1", "日_2", "周_3", "月_4", "季度_5", "年_6"}, orderNum = "3")
@Schema(title = "问答频次(1-单次,2-日/次,3-周/次,4-月/次,5-季度/次,6-年/次)")
private Integer frequency;
}
@@ -4,6 +4,7 @@ import com.renkang.bank.model.po.UserPoints;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.jeecgframework.poi.excel.annotation.Excel;
/** /**
* @author pj * @author pj
@@ -15,21 +16,27 @@ import lombok.EqualsAndHashCode;
@Data @Data
public class UserPointsListVO extends UserPoints { public class UserPointsListVO extends UserPoints {
@Excel(name = "姓名", width = 15, orderNum = "3")
@Schema(title = "姓名") @Schema(title = "姓名")
private String realName; private String realName;
@Excel(name = "员工编号", width = 15, orderNum = "4")
@Schema(title = "员工编号") @Schema(title = "员工编号")
private String workNo; private String workNo;
@Excel(name = "所属单位", width = 15, orderNum = "1")
@Schema(title = "所属单位名称") @Schema(title = "所属单位名称")
private String orgName; private String orgName;
@Excel(name = "所属部门", width = 15, orderNum = "2")
@Schema(title = "所属部门名称") @Schema(title = "所属部门名称")
private String deptName; private String deptName;
@Excel(name = "性别", width = 15, orderNum = "5")
@Schema(title = "性别") @Schema(title = "性别")
private String sexName; private String sexName;
@Excel(name = "年龄", width = 15, orderNum = "6")
@Schema(title = "年龄") @Schema(title = "年龄")
private Integer age; private Integer age;
} }
@@ -4,7 +4,6 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.renkang.bank.model.dto.UpdUserPointsDTO; import com.renkang.bank.model.dto.UpdUserPointsDTO;
import com.renkang.bank.model.dto.UserPointsPageDTO; import com.renkang.bank.model.dto.UserPointsPageDTO;
import com.renkang.bank.model.dto.UserPointsRankingDTO; import com.renkang.bank.model.dto.UserPointsRankingDTO;
import com.renkang.bank.model.po.UserPointsDetails;
import com.renkang.bank.model.vo.*; import com.renkang.bank.model.vo.*;
import com.renkang.bank.service.IUserPointsService; import com.renkang.bank.service.IUserPointsService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@@ -30,9 +29,9 @@ public class UserPointsController {
private IUserPointsService iUserPointsService; private IUserPointsService iUserPointsService;
@Operation(summary = "测试积分变更", description = "测试积分变更") @Operation(summary = "测试积分变更", description = "测试积分变更")
@PostMapping(value = "/updateUserPoints") @PostMapping(value = "/testUpdateUserPoints")
public Result<Boolean> updateUserPoints(@RequestBody UpdUserPointsDTO dto) { public Result<Boolean> testUpdateUserPoints(@RequestBody UpdUserPointsDTO dto) {
return Result.OK(iUserPointsService.updateUserPoints(dto)); return Result.OK(iUserPointsService.testUpdateUserPoints(dto));
} }
@Operation(summary = "用户积分信息-分页列表", description = "用户积分信息-分页列表") @Operation(summary = "用户积分信息-分页列表", description = "用户积分信息-分页列表")
@@ -41,12 +40,26 @@ public class UserPointsController {
return Result.OK(iUserPointsService.pageList(dto)); return Result.OK(iUserPointsService.pageList(dto));
} }
@Operation(summary = "用户积分信息-分页列表(导出)", description = "用户积分信息-分页列表(导出)")
@PostMapping(value = "/exportUserPoints")
public Result<String> exportUserPoints(@RequestBody UserPointsPageDTO dto) {
iUserPointsService.exportUserPoints(dto);
return Result.OK("导出成功,请在导出记录查看");
}
@Operation(summary = "用户积分信息-积分明细", description = "用户积分信息-积分明细") @Operation(summary = "用户积分信息-积分明细", description = "用户积分信息-积分明细")
@PostMapping(value = "/detailPageList") @PostMapping(value = "/detailPageList")
public Result<IPage<UserPointsDetails>> detailPageList(@RequestBody UserPointsPageDTO dto) { public Result<IPage<UserPointsDetailsVO>> detailPageList(@RequestBody UserPointsPageDTO dto) {
return Result.OK(iUserPointsService.detailPageList(dto)); return Result.OK(iUserPointsService.detailPageList(dto));
} }
@Operation(summary = "用户积分信息-积分明细(导出)", description = "用户积分信息-积分明细(导出)")
@PostMapping(value = "/exportPointsDetail")
public Result<String> exportPointsDetail(@RequestBody UserPointsPageDTO dto) {
iUserPointsService.exportPointsDetail(dto);
return Result.OK("导出成功,请在导出记录查看");
}
@Operation(summary = "用户积分信息-扣除积分", description = "用户积分信息-扣除积分") @Operation(summary = "用户积分信息-扣除积分", description = "用户积分信息-扣除积分")
@PostMapping(value = "/deductPoint") @PostMapping(value = "/deductPoint")
public Result<Boolean> deductPoint(@RequestBody UpdUserPointsDTO dto) { public Result<Boolean> deductPoint(@RequestBody UpdUserPointsDTO dto) {
@@ -59,6 +72,26 @@ public class UserPointsController {
return Result.OK(iUserPointsService.clockInMonitor(dto)); return Result.OK(iUserPointsService.clockInMonitor(dto));
} }
@Operation(summary = "用户积分信息-员工打卡监测(导出)", description = "用户积分信息-员工打卡监测(导出)")
@PostMapping(value = "/exportClockInMonitor")
public Result<String> exportClockInMonitor(@RequestBody UserPointsPageDTO dto) {
iUserPointsService.exportClockInMonitor(dto);
return Result.OK("导出成功,请在导出记录查看");
}
@Operation(summary = "用户积分信息-员工积分排名", description = "用户积分信息-员工积分排名")
@PostMapping(value = "/userRanking")
public Result<IPage<UserPointRankingVO>> userRanking(@RequestBody UserPointsRankingDTO dto) {
return Result.OK(iUserPointsService.userRanking(dto));
}
@Operation(summary = "用户积分信息-员工积分排名(导出)", description = "用户积分信息-员工积分排名(导出)")
@PostMapping(value = "/exportUserRanking")
public Result<String> exportUserRanking(@RequestBody UserPointsRankingDTO dto) {
iUserPointsService.exportUserRanking(dto);
return Result.OK("导出成功,请在导出记录查看");
}
@Operation(summary = "用户积分信息-app我的积分", description = "用户积分信息-app我的积分") @Operation(summary = "用户积分信息-app我的积分", description = "用户积分信息-app我的积分")
@GetMapping(value = "/app/myPoints") @GetMapping(value = "/app/myPoints")
public Result<UserPointsInfoVO> myPoints() { public Result<UserPointsInfoVO> myPoints() {
@@ -2,6 +2,7 @@ package com.renkang.bank.mapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.github.yulichang.base.MPJBaseMapper; import com.github.yulichang.base.MPJBaseMapper;
import com.renkang.bank.model.dto.UserPointsRankingDTO;
import com.renkang.bank.model.po.UserPointsDetails; import com.renkang.bank.model.po.UserPointsDetails;
import com.renkang.bank.model.vo.UserPointRankingVO; import com.renkang.bank.model.vo.UserPointRankingVO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
@@ -11,6 +12,8 @@ import java.util.Date;
@Mapper @Mapper
public interface UserPointsDetailsMapper extends MPJBaseMapper<UserPointsDetails> { public interface UserPointsDetailsMapper extends MPJBaseMapper<UserPointsDetails> {
IPage<UserPointRankingVO> userRanking(IPage<UserPointRankingVO> page, Date startTime, Date endTime, UserPointsRankingDTO dto);
IPage<UserPointRankingVO> pointRanking(IPage<UserPointRankingVO> page, Date startTime, Date endTime, String secondOrgCode); IPage<UserPointRankingVO> pointRanking(IPage<UserPointRankingVO> page, Date startTime, Date endTime, String secondOrgCode);
} }
@@ -3,6 +3,53 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.renkang.bank.mapper.UserPointsDetailsMapper"> <mapper namespace="com.renkang.bank.mapper.UserPointsDetailsMapper">
<select id="userRanking" resultType="com.renkang.bank.model.vo.UserPointRankingVO">
<!--定义公共表表达式用于计算每个用户的总积分-->
WITH UserPointsSummary AS (
SELECT
a.user_id,
SUM(CASE WHEN a.change_type = 1 THEN a.points ELSE 0 END) -
SUM(CASE WHEN a.change_type = 2 THEN a.points ELSE 0 END) AS pointSum
FROM
bank_user_points_details a
INNER JOIN bank_user_points b ON a.user_id = b.user_id
<if test="null != dto.orgCode and '' != dto.orgCode">
AND b.org_code like concat(#{dto.orgCode},'%')
</if>
<if test="null != dto.deptCode and '' != dto.deptCode">
AND b.org_code like concat(#{dto.deptCode},'%')
</if>
GROUP BY
a.user_id
),
<!--计算所有用户的排名-->
RankedUsers AS (
SELECT
user_id,
pointSum,
ROW_NUMBER() OVER (ORDER BY pointSum DESC, user_id ASC) AS ranking
FROM
UserPointsSummary
)
<!--主查询:根据是否传入用户 ID 返回结果-->
SELECT
ranking,
user_id,
pointSum
FROM
RankedUsers
<where>
<if test="dto.userIds != null and dto.userIds.size() > 0">
AND user_id in
<foreach collection="dto.userIds" item="userId" open="(" separator="," close=")">
#{userId}
</foreach>
</if>
</where>
ORDER BY
pointSum DESC, user_id ASC
</select>
<select id="pointRanking" resultType="com.renkang.bank.model.vo.UserPointRankingVO"> <select id="pointRanking" resultType="com.renkang.bank.model.vo.UserPointRankingVO">
WITH UserPointsSummary AS ( WITH UserPointsSummary AS (
SELECT SELECT
@@ -0,0 +1,90 @@
package com.renkang.bank.redis.config;
import com.renkang.bank.redis.listener.BankAsyncListener;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.system.vo.SysUserModel;
import org.jeecg.enums.ServerEnum;
import org.jeecg.redis.RedisStreamKeyEnum;
import org.jeecg.util.IPUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.hash.Jackson2HashMapper;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
/**
* @author Shunzhi Jiang
* @since 2023/6/13
*/
@Configuration
@Slf4j
public class BankRedisStreamConfig {
private final Consumer consumer = Consumer.from(ServerEnum.BANK.getCustomGroup(), IPUtil.getCurrentServiceIp());
private final ReadOffset readOffset = ReadOffset.lastConsumed();
private BankAsyncListener bankAsyncListener;
@Autowired
public void setDepartAsyncListener(BankAsyncListener bankAsyncListener) {
this.bankAsyncListener = bankAsyncListener;
}
@Bean
public StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, SysUserModel>> sysUserModelInterveneStreamOptions(
@Qualifier("asyncTaskExecutor") TaskExecutor taskExecutor,
StringRedisSerializer stringSerializer,
GenericJackson2JsonRedisSerializer jsonSerializer
) {
return StreamMessageListenerContainer.StreamMessageListenerContainerOptions
.builder()
// 一次最多获取多少条消息
.batchSize(10)
// 运行 Stream 的 poll task
.executor(taskExecutor)
// 可以理解为 Stream Key 的序列化方式
.keySerializer(stringSerializer)
// 可以理解为 Stream 后方的字段的 key 的序列化方式
.hashKeySerializer(stringSerializer)
// 可以理解为 Stream 后方的字段的 value 的序列化方式
.hashValueSerializer(jsonSerializer)
// ObjectRecord 时,将 对象的 filed 和 value 转换成一个 Map 比如:将Book对象转换成map
.objectMapper(new Jackson2HashMapper(true))
// 将发送到Stream中的Record转换成ObjectRecord,转换成具体的类型是这个地方指定的类型
.targetType(SysUserModel.class)
.build();
}
@Bean(initMethod = "start", destroyMethod = "stop")
public StreamMessageListenerContainer<String, ObjectRecord<String, SysUserModel>> sysUserModelInterveneStreamMessageListenerContainer(
RedisConnectionFactory redisConnectionFactory,
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, SysUserModel>> options
) {
StreamMessageListenerContainer<String, ObjectRecord<String, SysUserModel>> container =
StreamMessageListenerContainer.create(redisConnectionFactory, options);
// 修改用户
container.receive(
consumer,
createStreamOffset(RedisStreamKeyEnum.CHANGE_USER_DEPART),
bankAsyncListener
);
log.info("redisStream配置完成");
return container;
}
private StreamOffset<String> createStreamOffset(RedisStreamKeyEnum streamKeyEnum) {
return StreamOffset.create(streamKeyEnum.getKey(), readOffset);
}
}
@@ -0,0 +1,63 @@
package com.renkang.bank.redis.listener;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.renkang.bank.mapper.UserPointsMapper;
import com.renkang.bank.model.po.UserPoints;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.system.vo.SysUserModel;
import org.jeecg.enums.ServerEnum;
import org.jeecg.redis.RedisStreamKeyEnum;
import org.jeecg.redis.RedisStreamUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.stream.StreamListener;
import org.springframework.stereotype.Component;
/**
* @author Shunzhi Jiang
* @since 2023/6/13
*/
@Component
@Slf4j
public class BankAsyncListener implements StreamListener<String, ObjectRecord<String, SysUserModel>> {
@Autowired
private RedisStreamUtil redisStreamUtil;
@Autowired
private UserPointsMapper userPointsMapper;
@Override
public void onMessage(ObjectRecord<String, SysUserModel> message) {
String streamKey = message.getStream();
SysUserModel model = message.getValue();
log.info("收到Redis Stream Key{}", streamKey);
RecordId recordId = message.getId();
log.info("ID{}, DATA: {}", recordId, JSON.toJSONString(model));
try {
if (RedisStreamKeyEnum.CHANGE_USER_DEPART.getKey().equals(streamKey)) {
if (StrUtil.isBlank(model.getId()) || StrUtil.isBlank(model.getOrgCode())) {
return;
}
// 修改用户积分主表单位信息
updateUserPointsInfo(model);
}
ack(message);
log.info("Redis Stream消费成功:Key: {}, ID: {}", streamKey, recordId);
} catch (Exception e) {
log.error("Redis Stream消费异常:Key: {}, ID: {}", streamKey, recordId, e);
}
}
private void updateUserPointsInfo(SysUserModel model) {
userPointsMapper.update(new LambdaUpdateWrapper<UserPoints>()
.eq(UserPoints::getUserId, model.getId())
.set(UserPoints::getOrgCode, model.getOrgCode())
);
}
private void ack(ObjectRecord<String, SysUserModel> message) {
redisStreamUtil.ack(ServerEnum.BANK.getCustomGroup(), message);
}
}
@@ -4,23 +4,32 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.renkang.bank.model.dto.UpdUserPointsDTO; import com.renkang.bank.model.dto.UpdUserPointsDTO;
import com.renkang.bank.model.dto.UserPointsPageDTO; import com.renkang.bank.model.dto.UserPointsPageDTO;
import com.renkang.bank.model.dto.UserPointsRankingDTO; import com.renkang.bank.model.dto.UserPointsRankingDTO;
import com.renkang.bank.model.po.UserPointsDetails;
import com.renkang.bank.model.vo.*; import com.renkang.bank.model.vo.*;
import java.util.List; import java.util.List;
public interface IUserPointsService { public interface IUserPointsService {
boolean updateUserPoints(UpdUserPointsDTO dto); boolean testUpdateUserPoints(UpdUserPointsDTO dto);
IPage<UserPointsListVO> pageList(UserPointsPageDTO dto); IPage<UserPointsListVO> pageList(UserPointsPageDTO dto);
IPage<UserPointsDetails> detailPageList(UserPointsPageDTO dto); void exportUserPoints(UserPointsPageDTO dto);
IPage<UserPointsDetailsVO> detailPageList(UserPointsPageDTO dto);
void exportPointsDetail(UserPointsPageDTO dto);
boolean deductPoint(UpdUserPointsDTO dto); boolean deductPoint(UpdUserPointsDTO dto);
IPage<UserPointsMonitorVO> clockInMonitor(UserPointsPageDTO dto); IPage<UserPointsMonitorVO> clockInMonitor(UserPointsPageDTO dto);
void exportClockInMonitor(UserPointsPageDTO dto);
IPage<UserPointRankingVO> userRanking(UserPointsRankingDTO dto);
void exportUserRanking(UserPointsRankingDTO dto);
UserPointsInfoVO myPoints(); UserPointsInfoVO myPoints();
List<UserPointsTaskVO> pointsTaskList(); List<UserPointsTaskVO> pointsTaskList();
@@ -1,10 +1,10 @@
package com.renkang.bank.service.impl; package com.renkang.bank.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjUtil; import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -27,13 +27,18 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.exception.ExceptionAssertsUtil; import org.jeecg.common.exception.ExceptionAssertsUtil;
import org.jeecg.common.export.GenericExportHandler;
import org.jeecg.common.system.api.ISysBaseAPI; import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.vo.LoginUser; import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.enums.ExportExcelEnum;
import org.jeecg.global.GlobalUtils; import org.jeecg.global.GlobalUtils;
import org.jeecg.modules.manager.ISysCache; import org.jeecg.modules.manager.ISysCache;
import org.jeecg.modules.system.entity.SysDepart; import org.jeecg.modules.system.entity.SysDepart;
import org.jeecg.modules.system.entity.SysUser;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -67,7 +72,7 @@ public class UserPointsServiceImpl extends ServiceImpl<UserPointsMapper, UserPoi
// 动态排序 // 动态排序
if (StringUtils.isNotBlank(dto.getColumn())) { if (StringUtils.isNotBlank(dto.getColumn())) {
// 驼峰转下划线 // 驼峰转下划线
String column = StrUtil.toUnderlineCase(dto.getColumn()); String column = CharSequenceUtil.toUnderlineCase(dto.getColumn());
if ("asc".equalsIgnoreCase(dto.getOrder())) { if ("asc".equalsIgnoreCase(dto.getOrder())) {
queryWrapper.orderByAsc(column); queryWrapper.orderByAsc(column);
} else { } else {
@@ -95,11 +100,11 @@ public class UserPointsServiceImpl extends ServiceImpl<UserPointsMapper, UserPoi
vo.setSexName(ObjectUtil.isNotNull(loginUser.getSex()) && loginUser.getSex().equals(1) ? "" : ""); vo.setSexName(ObjectUtil.isNotNull(loginUser.getSex()) && loginUser.getSex().equals(1) ? "" : "");
vo.setAge(ObjectUtil.isNotNull(loginUser.getAge()) && loginUser.getAge() > 0 ? loginUser.getAge() : null); vo.setAge(ObjectUtil.isNotNull(loginUser.getAge()) && loginUser.getAge() > 0 ? loginUser.getAge() : null);
SysDepart secondDepart = sysCache.getDepartByOrgCode(GlobalUtils.getSecondDepartOrgCode(vo.getOrgCode())); SysDepart secondDepart = sysCache.getDepartByOrgCode(GlobalUtils.getSecondDepartOrgCode(vo.getOrgCode()));
if (ObjUtil.isNotEmpty(secondDepart)) { if (ObjectUtil.isNotEmpty(secondDepart)) {
vo.setDeptName(secondDepart.getDepartName()); vo.setDeptName(secondDepart.getDepartName());
} }
SysDepart thirdDepart = sysCache.getDepartByOrgCode(GlobalUtils.getThirdDepartOrgCode(vo.getOrgCode())); SysDepart thirdDepart = sysCache.getDepartByOrgCode(GlobalUtils.getThirdDepartOrgCode(vo.getOrgCode()));
if (ObjUtil.isNotEmpty(thirdDepart)) { if (ObjectUtil.isNotEmpty(thirdDepart)) {
vo.setOrgName(thirdDepart.getDepartName()); vo.setOrgName(thirdDepart.getDepartName());
} }
} }
@@ -108,28 +113,106 @@ public class UserPointsServiceImpl extends ServiceImpl<UserPointsMapper, UserPoi
return voPage; return voPage;
} }
@Resource
private AsyncTaskExecutor asyncTaskExecutor;
/**
* @description: 导出数据(设备数据)
* @author PengJ
* @date 2025/11/18 14:57
*/
@Override @Override
public IPage<UserPointsDetails> detailPageList(UserPointsPageDTO dto) { public void exportUserPoints(UserPointsPageDTO dto) {
GenericExportHandler<UserPointsListVO, UserPointsPageDTO> handler =
new GenericExportHandler<>(
UserPointsListVO.class,
ExportExcelEnum.BANK_USER_POINTS,
asyncTaskExecutor,
log,
filter -> {
filter.setPageNo(1);
filter.setPageSize(-1);
List<UserPointsListVO> records = pageList(filter).getRecords();
return GenericExportHandler.handleData(records, UserPointsListVO.class);
}
);
handler.exportAsync(dto);
}
@Override
public IPage<UserPointsDetailsVO> detailPageList(UserPointsPageDTO dto) {
if (StringUtils.isBlank(dto.getUserId())) { if (StringUtils.isBlank(dto.getUserId())) {
ExceptionAssertsUtil.fail("用户id不能为空"); ExceptionAssertsUtil.fail("用户id不能为空");
} }
if (ObjectUtil.isNull(dto.getChangeType())) { if (ObjectUtil.isNull(dto.getChangeType())) {
ExceptionAssertsUtil.fail("收支类型不能为空"); ExceptionAssertsUtil.fail("收支类型不能为空");
} }
Page<UserPointsDetails> page = new Page<>(dto.getPageNo(), dto.getPageSize()); Page<UserPointsDetailsVO> page = new Page<>(dto.getPageNo(), dto.getPageSize());
LambdaQueryWrapper<UserPointsDetails> queryWrapper = new LambdaQueryWrapper<>(); MPJLambdaWrapper<UserPointsDetails> queryWrapper = new MPJLambdaWrapper<>();
queryWrapper.select(UserPointsDetails::getId, UserPointsDetails::getBusinessInfo, queryWrapper.select(UserPointsDetails::getId, UserPointsDetails::getBusinessInfo,
UserPointsDetails::getPoints, UserPointsDetails::getCreateTime, UserPointsDetails::getChangeType); UserPointsDetails::getPoints, UserPointsDetails::getCreateTime, UserPointsDetails::getChangeType,
UserPointsDetails::getRemark, UserPointsDetails::getBusinessId, UserPointsDetails::getUserId);
queryWrapper.select(PointsRule::getFrequency);
queryWrapper.leftJoin(PointsRule.class, PointsRule::getId, UserPointsDetails::getBusinessId);
queryWrapper.eq(UserPointsDetails::getUserId, dto.getUserId()); queryWrapper.eq(UserPointsDetails::getUserId, dto.getUserId());
queryWrapper.eq(UserPointsDetails::getChangeType, dto.getChangeType()); queryWrapper.eq(UserPointsDetails::getChangeType, dto.getChangeType());
queryWrapper.orderByDesc(UserPointsDetails::getCreateTime); queryWrapper.orderByDesc(UserPointsDetails::getCreateTime);
return userPointsDetailsMapper.selectPage(page, queryWrapper); Page<UserPointsDetailsVO> voPage = userPointsDetailsMapper.selectJoinPage(page, UserPointsDetailsVO.class, queryWrapper);
if (dto.getChangeType().equals(HealthBankConstants.CHANGE_TYPE_DEDUCT)) {
voPage.getRecords().forEach(vo -> {
vo.setPoints(vo.getPoints().multiply(new BigDecimal(-1)));
});
}
return voPage;
}
@Override
public void exportPointsDetail(UserPointsPageDTO dto) {
if (StringUtils.isBlank(dto.getUserId())) {
ExceptionAssertsUtil.fail("用户id不能为空");
}
if (ObjectUtil.isNull(dto.getChangeType())) {
ExceptionAssertsUtil.fail("收支类型不能为空");
}
if (dto.getChangeType().equals(HealthBankConstants.CHANGE_TYPE_ADD)) {
GenericExportHandler<UserPointsDetailsVO, UserPointsPageDTO> handler =
new GenericExportHandler<>(
UserPointsDetailsVO.class,
ExportExcelEnum.BANK_POINTS_DETAIL,
asyncTaskExecutor,
log,
filter -> {
filter.setPageNo(1);
filter.setPageSize(-1);
List<UserPointsDetailsVO> records = detailPageList(filter).getRecords();
return GenericExportHandler.handleData(records, UserPointsDetailsVO.class);
}
);
handler.exportAsync(dto);
} else if (dto.getChangeType().equals(HealthBankConstants.CHANGE_TYPE_DEDUCT)) {
GenericExportHandler<UserPointsDeductDetailsVO, UserPointsPageDTO> handler =
new GenericExportHandler<>(
UserPointsDeductDetailsVO.class,
ExportExcelEnum.BANK_POINTS_DETAIL,
asyncTaskExecutor,
log,
filter -> {
filter.setPageNo(1);
filter.setPageSize(-1);
List<UserPointsDetailsVO> records = detailPageList(filter).getRecords();
List<UserPointsDeductDetailsVO> voList = BeanUtil.copyToList(records, UserPointsDeductDetailsVO.class);
return GenericExportHandler.handleData(voList, UserPointsDeductDetailsVO.class);
}
);
handler.exportAsync(dto);
}
} }
@Override @Override
public boolean deductPoint(UpdUserPointsDTO dto) { public boolean deductPoint(UpdUserPointsDTO dto) {
dto.setSourceType(PointsSourceEnum.TYPE_2_VIOLATION.getSource()); dto.setSourceType(PointsSourceEnum.TYPE_2_VIOLATION.getSource());
return updateUserPoints(dto); LoginUser loginUser = GlobalUtils.getLoginUser();
return updateUserPoints(dto, loginUser);
} }
@Override @Override
@@ -138,14 +221,12 @@ public class UserPointsServiceImpl extends ServiceImpl<UserPointsMapper, UserPoi
MPJLambdaWrapper<UserPointsDetails> queryWrapper = new MPJLambdaWrapper<>(); MPJLambdaWrapper<UserPointsDetails> queryWrapper = new MPJLambdaWrapper<>();
queryWrapper.selectAll(UserPointsDetails.class); queryWrapper.selectAll(UserPointsDetails.class);
queryWrapper.select(PointsRule::getFrequency); queryWrapper.select(PointsRule::getFrequency);
queryWrapper.eq(UserPointsDetails::getChangeType, 1); queryWrapper.eq(UserPointsDetails::getSource, PointsSourceEnum.TYPE_1_SURVEY.getSource());
if (StringUtils.isNotBlank(dto.getRealName()) || StringUtils.isNotBlank(dto.getWorkNo())) { if (StringUtils.isNotBlank(dto.getRealName()) || StringUtils.isNotBlank(dto.getWorkNo())) {
//根据姓名和工号查询用户 //根据姓名和工号查询用户
List<String> userIdList = iSysBaseAPI.getUserIdsByDepartAndUsername(null, dto.getRealName(), null, dto.getWorkNo()); List<String> userIdList = iSysBaseAPI.getUserIdsByDepartAndUsername(null, dto.getRealName(), null, dto.getWorkNo());
queryWrapper.in(!userIdList.isEmpty(), UserPoints::getUserId, userIdList); queryWrapper.in(!userIdList.isEmpty(), UserPoints::getUserId, userIdList);
} }
// 动态排序
queryWrapper.orderByDesc(UserPointsDetails::getCreateTime); queryWrapper.orderByDesc(UserPointsDetails::getCreateTime);
if (StringUtils.isNotBlank(dto.getOrgCode())) { if (StringUtils.isNotBlank(dto.getOrgCode())) {
queryWrapper.likeRight(UserPoints::getOrgCode, dto.getOrgCode()); queryWrapper.likeRight(UserPoints::getOrgCode, dto.getOrgCode());
@@ -154,6 +235,7 @@ public class UserPointsServiceImpl extends ServiceImpl<UserPointsMapper, UserPoi
queryWrapper.likeRight(UserPoints::getOrgCode, dto.getDeptCode()); queryWrapper.likeRight(UserPoints::getOrgCode, dto.getDeptCode());
} }
queryWrapper.leftJoin(PointsRule.class, PointsRule::getId, UserPointsDetails::getBusinessId); queryWrapper.leftJoin(PointsRule.class, PointsRule::getId, UserPointsDetails::getBusinessId);
queryWrapper.orderByDesc(UserPointsDetails::getCreateTime);
Page<UserPointsMonitorVO> voPage = userPointsDetailsMapper.selectJoinPage(page, UserPointsMonitorVO.class, queryWrapper); Page<UserPointsMonitorVO> voPage = userPointsDetailsMapper.selectJoinPage(page, UserPointsMonitorVO.class, queryWrapper);
List<String> userIds = voPage.getRecords().stream().map(UserPointsMonitorVO::getUserId).collect(Collectors.toList()); List<String> userIds = voPage.getRecords().stream().map(UserPointsMonitorVO::getUserId).collect(Collectors.toList());
if (!userIds.isEmpty()) { if (!userIds.isEmpty()) {
@@ -166,11 +248,11 @@ public class UserPointsServiceImpl extends ServiceImpl<UserPointsMapper, UserPoi
vo.setSexName(ObjectUtil.isNotNull(loginUser.getSex()) && loginUser.getSex().equals(1) ? "" : ""); vo.setSexName(ObjectUtil.isNotNull(loginUser.getSex()) && loginUser.getSex().equals(1) ? "" : "");
vo.setAge(ObjectUtil.isNotNull(loginUser.getAge()) && loginUser.getAge() > 0 ? loginUser.getAge() : null); vo.setAge(ObjectUtil.isNotNull(loginUser.getAge()) && loginUser.getAge() > 0 ? loginUser.getAge() : null);
SysDepart secondDepart = sysCache.getDepartByOrgCode(GlobalUtils.getSecondDepartOrgCode(loginUser.getOrgCode())); SysDepart secondDepart = sysCache.getDepartByOrgCode(GlobalUtils.getSecondDepartOrgCode(loginUser.getOrgCode()));
if (ObjUtil.isNotEmpty(secondDepart)) { if (ObjectUtil.isNotEmpty(secondDepart)) {
vo.setDeptName(secondDepart.getDepartName()); vo.setDeptName(secondDepart.getDepartName());
} }
SysDepart thirdDepart = sysCache.getDepartByOrgCode(GlobalUtils.getThirdDepartOrgCode(loginUser.getOrgCode())); SysDepart thirdDepart = sysCache.getDepartByOrgCode(GlobalUtils.getThirdDepartOrgCode(loginUser.getOrgCode()));
if (ObjUtil.isNotEmpty(thirdDepart)) { if (ObjectUtil.isNotEmpty(thirdDepart)) {
vo.setOrgName(thirdDepart.getDepartName()); vo.setOrgName(thirdDepart.getDepartName());
} }
} }
@@ -179,15 +261,139 @@ public class UserPointsServiceImpl extends ServiceImpl<UserPointsMapper, UserPoi
return voPage; return voPage;
} }
@Override
public void exportClockInMonitor(UserPointsPageDTO dto) {
GenericExportHandler<UserPointsMonitorVO, UserPointsPageDTO> handler =
new GenericExportHandler<>(
UserPointsMonitorVO.class,
ExportExcelEnum.BANK_CLOCK_IN_RECORD,
asyncTaskExecutor,
log,
filter -> {
filter.setPageNo(1);
filter.setPageSize(-1);
List<UserPointsMonitorVO> records = clockInMonitor(filter).getRecords();
return GenericExportHandler.handleData(records, UserPointsMonitorVO.class);
}
);
handler.exportAsync(dto);
}
@Override
public IPage<UserPointRankingVO> userRanking(UserPointsRankingDTO dto) {
Page<UserPointRankingVO> page = new Page<>(dto.getPageNo(), dto.getPageSize());
Date startTime = null;
Date endTime = null;
Date now = new Date();
if (StringUtils.isNotBlank(dto.getRankType())) {
if ("0".equals(dto.getRankType())) {
// 获取当天的开始时间
startTime = DateUtil.beginOfDay(now);
// 获取当天的结束时间
endTime = DateUtil.endOfDay(now);
} else if ("1".equals(dto.getRankType())) {
// 获取本周的开始时间(周一)
startTime = DateUtil.beginOfWeek(now, true);
// 获取本周的结束时间(周日)
endTime = DateUtil.endOfWeek(now, true);
} else if ("2".equals(dto.getRankType())) {
// 获取本月的开始时间
startTime = DateUtil.beginOfMonth(now);
// 获取本月的结束时间
endTime = DateUtil.endOfMonth(now);
} else if ("3".equals(dto.getRankType())) {
// 获取本季度的开始时间
startTime = DateUtil.beginOfQuarter(now);
// 获取本季度的结束时间
endTime = DateUtil.endOfQuarter(now);
} else if ("4".equals(dto.getRankType())) {
// 获取本年度的开始时间
startTime = DateUtil.beginOfYear(now);
// 获取本年度的结束时间
endTime = DateUtil.endOfYear(now);
} else if ("5".equals(dto.getRankType())) {
// 获取指定时间的开始时间
startTime = DateUtil.beginOfDay(dto.getStartTime());
// 获取指定时间的结束时间
endTime = DateUtil.endOfDay(dto.getEndTime());
}
}
if (StringUtils.isNotBlank(dto.getRealName()) || StringUtils.isNotBlank(dto.getWorkNo())) {
List<String> sysUserIds = iSysBaseAPI.getUserIdsByDepartAndUsername(
StringUtils.isNotBlank(dto.getDeptCode()) ? dto.getDeptCode() : dto.getOrgCode(),
dto.getRealName(),
null,
dto.getWorkNo()
);
if (!sysUserIds.isEmpty()) {
dto.setUserIds(sysUserIds);
}
}
IPage<UserPointRankingVO> voiPage = userPointsDetailsMapper.userRanking(page, startTime, endTime, dto);
List<String> userIds = voiPage.getRecords().stream().map(UserPointRankingVO::getUserId).collect(Collectors.toList());
List<LoginUser> loginUsers = iSysBaseAPI.listUserByIdsNew(userIds);
voiPage.getRecords().forEach(vo -> {
loginUsers.forEach(loginUser -> {
if (vo.getUserId().equals(loginUser.getId())) {
vo.setRealName(loginUser.getRealname());
vo.setAvatar(loginUser.getAvatar());
vo.setSex(loginUser.getSex());
vo.setSexName(ObjectUtil.isNotNull(loginUser.getSex()) && loginUser.getSex().equals(1) ? "" : "");
vo.setWorkNo(loginUser.getWorkNo());
vo.setAge(ObjectUtil.isNotNull(loginUser.getAge()) && loginUser.getAge() > 0 ? loginUser.getAge() : null);
SysDepart secondDepart = sysCache.getDepartByOrgCode(GlobalUtils.getSecondDepartOrgCode(loginUser.getOrgCode()));
if (ObjectUtil.isNotEmpty(secondDepart)) {
vo.setDeptName(secondDepart.getDepartName());
}
SysDepart thirdDepart = sysCache.getDepartByOrgCode(GlobalUtils.getThirdDepartOrgCode(loginUser.getOrgCode()));
if (ObjectUtil.isNotEmpty(thirdDepart)) {
vo.setOrgName(thirdDepart.getDepartName());
}
}
});
});
return voiPage;
}
@Override
public void exportUserRanking(UserPointsRankingDTO dto) {
GenericExportHandler<UserPointRankingVO, UserPointsRankingDTO> handler =
new GenericExportHandler<>(
UserPointRankingVO.class,
ExportExcelEnum.BANK_USER_RANKING,
asyncTaskExecutor,
log,
filter -> {
filter.setPageNo(1);
filter.setPageSize(-1);
List<UserPointRankingVO> records = userRanking(filter).getRecords();
return GenericExportHandler.handleData(records, UserPointRankingVO.class);
}
);
handler.exportAsync(dto);
}
@Override
public boolean testUpdateUserPoints(UpdUserPointsDTO dto) {
List<String> sysUserIds = iSysBaseAPI.findSysUserIds(new SysUser());
List<LoginUser> loginUsers = iSysBaseAPI.listUserByIdsNew(sysUserIds);
loginUsers.forEach(user -> {
try {
updateUserPoints(dto, user);
} catch (Exception e) {
log.error("用户:{}({})更新积分异常,入参:{}", user.getRealname(), user.getUsername(), dto, e);
}
});
return true;
}
/** /**
* 更新用户积分 * 更新用户积分
* *
* @param dto * @param dto
* @return * @return
*/ */
@Override public boolean updateUserPoints(UpdUserPointsDTO dto, LoginUser loginUser) {
public boolean updateUserPoints(UpdUserPointsDTO dto) {
LoginUser loginUser = GlobalUtils.getLoginUser();
log.info("------用户:{}({})开始更新积分,入参:{}------", loginUser.getRealname(), loginUser.getUsername(), dto); log.info("------用户:{}({})开始更新积分,入参:{}------", loginUser.getRealname(), loginUser.getUsername(), dto);
PointsSourceEnum sourceEnum = PointsSourceEnum.getBySource(dto.getSourceType()); PointsSourceEnum sourceEnum = PointsSourceEnum.getBySource(dto.getSourceType());
if (sourceEnum == null) { if (sourceEnum == null) {
@@ -203,7 +409,7 @@ public class UserPointsServiceImpl extends ServiceImpl<UserPointsMapper, UserPoi
//违规扣除 //违规扣除
userId = dto.getUserId(); userId = dto.getUserId();
} else { } else {
userId = GlobalUtils.getLoginUser().getId(); userId = loginUser.getId();
} }
UserPoints userPoints = baseMapper.selectOne(new LambdaQueryWrapper<UserPoints>() UserPoints userPoints = baseMapper.selectOne(new LambdaQueryWrapper<UserPoints>()
.eq(UserPoints::getUserId, userId) .eq(UserPoints::getUserId, userId)
@@ -253,10 +459,10 @@ public class UserPointsServiceImpl extends ServiceImpl<UserPointsMapper, UserPoi
List<UserPointsDetails> details = userPointsDetailsMapper.selectList(new LambdaQueryWrapper<UserPointsDetails>() List<UserPointsDetails> details = userPointsDetailsMapper.selectList(new LambdaQueryWrapper<UserPointsDetails>()
.eq(UserPointsDetails::getUserId, userId) .eq(UserPointsDetails::getUserId, userId)
.eq(UserPointsDetails::getChangeType, HealthBankConstants.CHANGE_TYPE_ADD) .eq(UserPointsDetails::getChangeType, HealthBankConstants.CHANGE_TYPE_ADD)
.apply("DATE_FORMAT(create_time, '%Y-%m-%d') = '" + DateUtil.today() + "'") .apply("DATE_FORMAT(create_time, '%Y-%m-%d') = {0}", DateUtil.today())
); );
double sum = details.stream().map(UserPointsDetails::getPoints).mapToDouble(BigDecimal::doubleValue).sum(); double sum = details.stream().map(UserPointsDetails::getPoints).mapToDouble(BigDecimal::doubleValue).sum();
userPoints.setLatestAddDaySum(new BigDecimal(sum).add(pointsRule.getPoints())); userPoints.setLatestAddDaySum(BigDecimal.valueOf(sum).add(pointsRule.getPoints()));
} }
break; break;
case TYPE_2_USE: case TYPE_2_USE:
@@ -269,7 +475,8 @@ public class UserPointsServiceImpl extends ServiceImpl<UserPointsMapper, UserPoi
ExceptionAssertsUtil.fail("用户积分不存在"); ExceptionAssertsUtil.fail("用户积分不存在");
} }
userPoints.setDeductPointsSum(userPoints.getDeductPointsSum().add(dto.getPoints())); userPoints.setDeductPointsSum(userPoints.getDeductPointsSum().add(dto.getPoints()));
userPoints.setPoints(userPoints.getPoints().subtract(dto.getPoints())); BigDecimal subtract = userPoints.getPoints().subtract(dto.getPoints());
userPoints.setPoints(subtract.compareTo(BigDecimal.valueOf(0)) < 0 ? BigDecimal.valueOf(0) : subtract);
userPointsDetails.setPoints(dto.getPoints()); userPointsDetails.setPoints(dto.getPoints());
userPointsDetails.setRemark(dto.getRemark()); userPointsDetails.setRemark(dto.getRemark());
userPointsDetails.setSource(sourceEnum.getSource()); userPointsDetails.setSource(sourceEnum.getSource());
@@ -316,7 +523,7 @@ public class UserPointsServiceImpl extends ServiceImpl<UserPointsMapper, UserPoi
wrapper.between(UserPointsDetails::getCreateTime, startOfDay, endOfDay); wrapper.between(UserPointsDetails::getCreateTime, startOfDay, endOfDay);
c = userPointsDetailsMapper.selectCount(wrapper); c = userPointsDetailsMapper.selectCount(wrapper);
if (c > 0) { if (c > 0) {
ExceptionAssertsUtil.fail("日已获得该积分"); ExceptionAssertsUtil.fail("日已获得该积分");
} }
break; break;
// 周/次:检查用户本周是否已经获得过该积分 // 周/次:检查用户本周是否已经获得过该积分
@@ -356,7 +563,7 @@ public class UserPointsServiceImpl extends ServiceImpl<UserPointsMapper, UserPoi
wrapper.between(UserPointsDetails::getCreateTime, startOfYear, endOfYear); wrapper.between(UserPointsDetails::getCreateTime, startOfYear, endOfYear);
c = userPointsDetailsMapper.selectCount(wrapper); c = userPointsDetailsMapper.selectCount(wrapper);
if (c > 0) { if (c > 0) {
ExceptionAssertsUtil.fail("本年已获得该积分"); ExceptionAssertsUtil.fail("本年已获得该积分");
} }
break; break;
default: default:
@@ -365,6 +572,11 @@ public class UserPointsServiceImpl extends ServiceImpl<UserPointsMapper, UserPoi
} }
} }
/**
* app我的积分
*
* @return 积分信息
*/
@Override @Override
public UserPointsInfoVO myPoints() { public UserPointsInfoVO myPoints() {
String userId = GlobalUtils.getLoginUser().getId(); String userId = GlobalUtils.getLoginUser().getId();
@@ -380,18 +592,36 @@ public class UserPointsServiceImpl extends ServiceImpl<UserPointsMapper, UserPoi
return vo; return vo;
} }
/**
* app积分任务
*
* @return 列表
*/
@Override @Override
public List<UserPointsTaskVO> pointsTaskList() { public List<UserPointsTaskVO> pointsTaskList() {
String userId = GlobalUtils.getLoginUser().getId(); String userId = GlobalUtils.getLoginUser().getId();
return pointsRuleMapper.pointsTaskList(userId); return pointsRuleMapper.pointsTaskList(userId);
} }
/**
* app积分打卡
*
* @param dto 参数
* @return 结果
*/
@Override @Override
public boolean pointCheckIn(UpdUserPointsDTO dto) { public boolean pointCheckIn(UpdUserPointsDTO dto) {
dto.setSourceType(PointsSourceEnum.TYPE_1_SURVEY.getSource()); dto.setSourceType(PointsSourceEnum.TYPE_1_SURVEY.getSource());
return updateUserPoints(dto); LoginUser loginUser = GlobalUtils.getLoginUser();
return updateUserPoints(dto, loginUser);
} }
/**
* app积分明细
*
* @param dto 参数
* @return 结果
*/
@Override @Override
public IPage<UserPointRecordVO> pointRecord(UserPointsPageDTO dto) { public IPage<UserPointRecordVO> pointRecord(UserPointsPageDTO dto) {
if (ObjectUtil.isNull(dto.getChangeType())) { if (ObjectUtil.isNull(dto.getChangeType())) {
@@ -406,6 +636,12 @@ public class UserPointsServiceImpl extends ServiceImpl<UserPointsMapper, UserPoi
return userPointsDetailsMapper.selectJoinPage(page, UserPointRecordVO.class, wrapper); return userPointsDetailsMapper.selectJoinPage(page, UserPointRecordVO.class, wrapper);
} }
/**
* app积分榜单
*
* @param dto 参数
* @return 结果
*/
@Override @Override
public IPage<UserPointRankingVO> pointRanking(UserPointsRankingDTO dto) { public IPage<UserPointRankingVO> pointRanking(UserPointsRankingDTO dto) {
String orgCode = GlobalUtils.getLoginUser().getOrgCode(); String orgCode = GlobalUtils.getLoginUser().getOrgCode();
@@ -437,11 +673,11 @@ public class UserPointsServiceImpl extends ServiceImpl<UserPointsMapper, UserPoi
vo.setAvatar(loginUser.getAvatar()); vo.setAvatar(loginUser.getAvatar());
vo.setSex(loginUser.getSex()); vo.setSex(loginUser.getSex());
SysDepart secondDepart = sysCache.getDepartByOrgCode(GlobalUtils.getSecondDepartOrgCode(loginUser.getOrgCode())); SysDepart secondDepart = sysCache.getDepartByOrgCode(GlobalUtils.getSecondDepartOrgCode(loginUser.getOrgCode()));
if (ObjUtil.isNotEmpty(secondDepart)) { if (ObjectUtil.isNotEmpty(secondDepart)) {
vo.setDeptName(secondDepart.getDepartName()); vo.setDeptName(secondDepart.getDepartName());
} }
SysDepart thirdDepart = sysCache.getDepartByOrgCode(GlobalUtils.getThirdDepartOrgCode(loginUser.getOrgCode())); SysDepart thirdDepart = sysCache.getDepartByOrgCode(GlobalUtils.getThirdDepartOrgCode(loginUser.getOrgCode()));
if (ObjUtil.isNotEmpty(thirdDepart)) { if (ObjectUtil.isNotEmpty(thirdDepart)) {
vo.setOrgName(thirdDepart.getDepartName()); vo.setOrgName(thirdDepart.getDepartName());
} }
} }
@@ -1,5 +1,6 @@
package com.renkang.consultation.dto; package com.renkang.consultation.dto;
import cn.hutool.http.HtmlUtil;
import com.renkang.consultation.entity.ConPage; import com.renkang.consultation.entity.ConPage;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -11,6 +12,10 @@ public class AccountDTO extends ConPage {
@Schema(title = "专家姓名") @Schema(title = "专家姓名")
private String doctorName; private String doctorName;
public void setDoctorName(String doctorName) {
this.doctorName = doctorName != null ? HtmlUtil.unescape(doctorName) : null;
}
@Schema(title = "医院选择") @Schema(title = "医院选择")
private String hospitalName; private String hospitalName;
@@ -1,5 +1,6 @@
package com.renkang.consultation.dto; package com.renkang.consultation.dto;
import cn.hutool.http.HtmlUtil;
import com.renkang.consultation.entity.ConPage; import com.renkang.consultation.entity.ConPage;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -10,6 +11,10 @@ public class ConDoctorDTO extends ConPage {
@Schema(title = "专家姓名") @Schema(title = "专家姓名")
private String doctorName; private String doctorName;
public void setDoctorName(String doctorName) {
this.doctorName = doctorName != null ? HtmlUtil.unescape(doctorName) : null;
}
@Schema(title = "专家编号") @Schema(title = "专家编号")
private String doctorNo; private String doctorNo;
@@ -1,5 +1,6 @@
package com.renkang.consultation.dto; package com.renkang.consultation.dto;
import cn.hutool.http.HtmlUtil;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.renkang.consultation.entity.ConPage; import com.renkang.consultation.entity.ConPage;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@@ -17,6 +18,9 @@ public class ConEvaluateDTO extends ConPage {
@Schema(title = "咨询人") @Schema(title = "咨询人")
private String userName; private String userName;
public void setUserName(String userName) {
this.userName = userName != null ? HtmlUtil.unescape(userName) : null;
}
@Schema(title = "二级单位id") @Schema(title = "二级单位id")
private String sysOrgSecondId; private String sysOrgSecondId;
@@ -27,6 +31,10 @@ public class ConEvaluateDTO extends ConPage {
@Schema(title = "咨询专家") @Schema(title = "咨询专家")
private String expertName; private String expertName;
public void setExpertName(String expertName) {
this.expertName = expertName != null ? HtmlUtil.unescape(expertName) : null;
}
@Schema(title = "专家医院") @Schema(title = "专家医院")
private String expertHospital; private String expertHospital;
@@ -1,5 +1,6 @@
package com.renkang.consultation.dto; package com.renkang.consultation.dto;
import cn.hutool.http.HtmlUtil;
import com.renkang.consultation.entity.ConPage; import com.renkang.consultation.entity.ConPage;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -10,6 +11,10 @@ public class ConHealthInfoAnswerDTO extends ConPage {
@Schema(title = "员工姓名") @Schema(title = "员工姓名")
private String fromName; private String fromName;
public void setFromName(String fromName) {
this.fromName = fromName != null ? HtmlUtil.unescape(fromName) : null;
}
@Schema(title = "手机号") @Schema(title = "手机号")
private String phone; private String phone;
@@ -1,5 +1,6 @@
package com.renkang.consultation.dto; package com.renkang.consultation.dto;
import cn.hutool.http.HtmlUtil;
import com.renkang.consultation.entity.ConPage; import com.renkang.consultation.entity.ConPage;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -11,6 +12,10 @@ public class ConHelperDTO extends ConPage {
@Schema(title = "全科医生姓名") @Schema(title = "全科医生姓名")
private String userName; private String userName;
public void setUserName(String userName) {
this.userName = userName != null ? HtmlUtil.unescape(userName) : null;
}
@Schema(title = "状态") @Schema(title = "状态")
private Integer status; private Integer status;
@@ -1,5 +1,6 @@
package com.renkang.consultation.dto; package com.renkang.consultation.dto;
import cn.hutool.http.HtmlUtil;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.renkang.consultation.entity.ConPage; import com.renkang.consultation.entity.ConPage;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@@ -17,6 +18,10 @@ public class ConSessionDTO extends ConPage {
@Schema(title = "员工姓名") @Schema(title = "员工姓名")
private String fromName; private String fromName;
public void setFromName(String fromName) {
this.fromName = fromName != null ? HtmlUtil.unescape(fromName) : null;
}
@Schema(title = "员工id") @Schema(title = "员工id")
private String fromAccount; private String fromAccount;
@@ -26,6 +31,10 @@ public class ConSessionDTO extends ConPage {
@Schema(title = "专家姓名") @Schema(title = "专家姓名")
private String doctorName; private String doctorName;
public void setDoctorName(String doctorName) {
this.doctorName = doctorName != null ? HtmlUtil.unescape(doctorName) : null;
}
@Schema(title = "咨询状态(0待确认 1接收预约 2拒绝预约 3已过期 4已取消 5咨询中 6专家咨询结束 7员工咨询结束 8超时自动结束)") @Schema(title = "咨询状态(0待确认 1接收预约 2拒绝预约 3已过期 4已取消 5咨询中 6专家咨询结束 7员工咨询结束 8超时自动结束)")
private String contentStatus; private String contentStatus;
@@ -480,4 +480,10 @@ public class ConSessionApiController {
return conSessionApiService.insertUserAndHelperSessionNew(); return conSessionApiService.insertUserAndHelperSessionNew();
} }
@Operation(summary = "超时关闭视频咨询", description = "超时关闭视频咨询")
@GetMapping("/endVideoSession")
public Result<String> endVideoSession() {
return Result.ok(conSessionApiService.endVideoSession());
}
} }
@@ -115,4 +115,6 @@ public interface ConSessionApiService {
UserSessionCount countForUser(); UserSessionCount countForUser();
String endVideoSession();
} }
@@ -2114,4 +2114,8 @@ public class ConSessionApiServiceImpl implements ConSessionApiService {
return conSessionMapper.selectCount(wrapper).intValue(); return conSessionMapper.selectCount(wrapper).intValue();
} }
public String endVideoSession() {
return conSessionMapper.endVideoSession();
}
} }
@@ -484,4 +484,9 @@ public class ConSessionController extends JeecgController<ConSession, IConSessio
return conSessionService.exportXlsConSession(request, dto); return conSessionService.exportXlsConSession(request, dto);
} }
@GetMapping("/getPicUrl")
public Result<String> getPicUrl(){
return Result.ok(conSessionService.getPicUrl());
}
} }
@@ -294,4 +294,13 @@ public interface ConSessionMapper extends BaseMapper<ConSession> , MPJBaseMapper
@Param("doctorjob")String doctorjob, @Param("doctorjob")String doctorjob,
@Param("contentType")String contentType, @Param("contentType")String contentType,
@Param("toAccount")String toAccount); @Param("toAccount")String toAccount);
/**
* 音视频咨询过期处理:预约日期已过的视频咨询,有通话记录则改为待评价,无通话记录则改为取消
* content_status: 4=待评价,6=取消
*/
@Update("UPDATE con_session SET content_status = CASE WHEN session_time IS NOT NULL THEN 4 ELSE 6 END " +
"WHERE content_type = 2 AND CURDATE() > session_date " +
"AND content_status NOT IN (4, 5, 6, 7, 8) AND del_flag = 0 AND status = 1")
String endVideoSession();
} }
@@ -85,5 +85,7 @@ public interface IConSessionService extends IService<ConSession> {
Result<List<com.renkang.im.entity.ImMsgRecordAll>> selectImMessageList(String groupId); Result<List<com.renkang.im.entity.ImMsgRecordAll>> selectImMessageList(String groupId);
Result<?> exportXlsConSession(HttpServletRequest request, ConSessionDTO dto); Result<?> exportXlsConSession(HttpServletRequest request, ConSessionDTO dto);
String getPicUrl();
} }
@@ -3,7 +3,6 @@ package com.renkang.consultation.service.impl;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -249,9 +248,6 @@ public class ConDoctorServiceImpl extends ServiceImpl<ConDoctorMapper, ConDoctor
if (StringUtils.hasLength(dto.getHospitalName())) { if (StringUtils.hasLength(dto.getHospitalName())) {
conDoctor.setResourceId(dto.getHospitalName()); conDoctor.setResourceId(dto.getHospitalName());
} }
if (StrUtil.isNotEmpty(dto.getDoctorName())){
conDoctor.setDoctorName(HtmlUtil.unescape(dto.getDoctorName()));
}
IPage<ConDoctor> conDoctorIPage = conDoctorMapper.queryPageList(page, conDoctor); IPage<ConDoctor> conDoctorIPage = conDoctorMapper.queryPageList(page, conDoctor);
List<ConDoctor> conDoctorList = conDoctorIPage.getRecords(); List<ConDoctor> conDoctorList = conDoctorIPage.getRecords();
if (CollectionUtils.isEmpty(conDoctorList)) { if (CollectionUtils.isEmpty(conDoctorList)) {
@@ -52,6 +52,7 @@ import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
import org.jeecgframework.poi.exception.excel.enums.ExcelExportEnum; import org.jeecgframework.poi.exception.excel.enums.ExcelExportEnum;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -1264,5 +1265,11 @@ public class ConSessionServiceImpl extends ServiceImpl<ConSessionMapper, ConSess
return sendTime; return sendTime;
} }
@Value("${banner.picUrl:}")
private String bannerPicUrl;
public String getPicUrl(){
// 读取配置文件的url
return bannerPicUrl;
}
} }
@@ -7,6 +7,7 @@ import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.context.XxlJobHelper; import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob; import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.constant.CommonConstant;
import org.jeecg.config.shiro.ClientSignThreadLocal; import org.jeecg.config.shiro.ClientSignThreadLocal;
import org.parboiled.common.StringUtils; import org.parboiled.common.StringUtils;
@@ -36,10 +37,10 @@ public class SyncScheduleJob {
String expireHour = "168"; String expireHour = "168";
String param = XxlJobHelper.getJobParam(); String param = XxlJobHelper.getJobParam();
if(StringUtils.isNotEmpty(param)){ if (StringUtils.isNotEmpty(param)) {
JSONObject jsonObject = JSONObject.parseObject(param); JSONObject jsonObject = JSONObject.parseObject(param);
String expireHourRequest = jsonObject.getString("expireHour"); String expireHourRequest = jsonObject.getString("expireHour");
if(StringUtils.isNotEmpty(expireHourRequest)){ if (StringUtils.isNotEmpty(expireHourRequest)) {
expireHour = jsonObject.getString("expireHour"); expireHour = jsonObject.getString("expireHour");
} }
} }
@@ -53,4 +54,12 @@ public class SyncScheduleJob {
return ReturnT.SUCCESS; return ReturnT.SUCCESS;
} }
@XxlJob(value = "endVideoSession")
public Result<String> endVideoSession() {
// 20260327 新增 音视频咨询过期处理 规则为:超过预约时间,比如预约的是 12月9日和专家视频沟通,但超过时间一天,即为过期,需要关闭咨询
conSessionApiController.endVideoSession();
return Result.ok("success");
}
} }
@@ -1,5 +1,6 @@
package com.renkang.emergency.entity.dto; package com.renkang.emergency.entity.dto;
import cn.hutool.http.HtmlUtil;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -7,6 +8,10 @@ import lombok.Data;
public class CallLogDTO { public class CallLogDTO {
@Schema(description = "姓名") @Schema(description = "姓名")
private String realName; private String realName;
public void setRealName(String realName) {
this.realName = realName != null ? HtmlUtil.unescape(realName) : null;
}
@Schema(description = "呼叫中心") @Schema(description = "呼叫中心")
private String centerId; private String centerId;
@Schema(description = "坐席工号") @Schema(description = "坐席工号")
@@ -1,5 +1,6 @@
package com.renkang.emergency.entity.dto; package com.renkang.emergency.entity.dto;
import cn.hutool.http.HtmlUtil;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -21,6 +22,10 @@ public class PhoneListDTO {
private String phoneType; private String phoneType;
@Schema(description = "姓名") @Schema(description = "姓名")
private String realName; private String realName;
public void setRealName(String realName) {
this.realName = realName != null ? HtmlUtil.unescape(realName) : null;
}
/** /**
* 0:全部 1:女 2:男 * 0:全部 1:女 2:男
*/ */
@@ -1,5 +1,6 @@
package com.renkang.emergency.entity.dto; package com.renkang.emergency.entity.dto;
import cn.hutool.http.HtmlUtil;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -9,6 +10,10 @@ public class TblQimoNoticeDTO {
private String seatId; private String seatId;
@Schema(title = "姓名") @Schema(title = "姓名")
private String realName; private String realName;
public void setRealName(String realName) {
this.realName = realName != null ? HtmlUtil.unescape(realName) : null;
}
@Schema(title = "所属应急中心") @Schema(title = "所属应急中心")
private String centerId; private String centerId;
@Schema(title = "orgCode") @Schema(title = "orgCode")
@@ -1,6 +1,7 @@
package com.renkang.emergency.controller; package com.renkang.emergency.controller;
import cn.hutool.http.HtmlUtil;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.renkang.emergency.entity.EmergencyProfessionUser; import com.renkang.emergency.entity.EmergencyProfessionUser;
import com.renkang.emergency.service.EmergencyProfessionUserService; import com.renkang.emergency.service.EmergencyProfessionUserService;
@@ -40,6 +41,7 @@ public class EmergencyProfessionUserController {
@RequestParam(name = "type", defaultValue = "5") Integer type, @RequestParam(name = "type", defaultValue = "5") Integer type,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize) { @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize) {
name = HtmlUtil.unescape(name);
IPage<EmergencyProfessionUser> pageList = emergencyProfessionUserService.customPage(name, centerId, type, pageNo, pageSize); IPage<EmergencyProfessionUser> pageList = emergencyProfessionUserService.customPage(name, centerId, type, pageNo, pageSize);
return Result.OK(pageList); return Result.OK(pageList);
} }
@@ -6,6 +6,7 @@ import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -58,6 +59,9 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
@Override @Override
public IPage<Order> pageCustom(Page<Order> page, Order order, HttpServletRequest req) { public IPage<Order> pageCustom(Page<Order> page, Order order, HttpServletRequest req) {
if(StrUtil.isNotBlank(order.getSalvageUserName())){
order.setSalvageUserName(HtmlUtil.unescape(order.getSalvageUserName()));
}
String salvageUserName = order.getSalvageUserName(); String salvageUserName = order.getSalvageUserName();
String initiatorUserName = order.getInitiatorUserName(); String initiatorUserName = order.getInitiatorUserName();
Order orderCopy = new Order(); Order orderCopy = new Order();
@@ -22,6 +22,10 @@ public class WatchH5IndexItemNewVo {
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd") @DateTimeFormat(pattern = "yyyy-MM-dd")
private Date dataDate; private Date dataDate;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date dataDateNew;
/** /**
* 注意 当wdType=sleep时此字段返回长睡时长 * 注意 当wdType=sleep时此字段返回长睡时长
*/ */
@@ -1,5 +1,6 @@
package com.renkang.watch.bean.request; package com.renkang.watch.bean.request;
import cn.hutool.http.HtmlUtil;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data; import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
@@ -19,6 +20,9 @@ public class AbnormalEventFilter {
private String departOrgCode; private String departOrgCode;
// 用户名称 // 用户名称
private String userName; private String userName;
public void setUserName(String userName) {
this.userName = userName != null ? HtmlUtil.unescape(userName) : null;
}
// 工具编码 // 工具编码
private String watchNo; private String watchNo;
//员工编号 //员工编号
@@ -35,6 +35,7 @@ public class AbnormalEvent extends BaseUser {
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "报警时间", format = "yyyy-MM-dd HH:mm:ss", width = 15, orderNum = "10") @Excel(name = "报警时间", format = "yyyy-MM-dd HH:mm:ss", width = 15, orderNum = "10")
private Date warnTime; private Date warnTime;
@Excel(name = "回传时间", format = "yyyy-MM-dd HH:mm:ss", width = 15, orderNum = "11")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(title = "创建日期") @Schema(title = "创建日期")
@@ -51,4 +52,18 @@ public class AbnormalEvent extends BaseUser {
private Date bindDate; private Date bindDate;
@Schema(title = "重点指标用户(0:非重点指标用户 1:大病异常 2:慢病异常 3:指标异常)") @Schema(title = "重点指标用户(0:非重点指标用户 1:大病异常 2:慢病异常 3:指标异常)")
private List<WatchKeyUser> keyUserList; private List<WatchKeyUser> keyUserList;
/**
* 事件ID(用于告警事件与GPS位置更新关联的唯一标识)
*/
@Excel(name = "事件ID", width = 32, orderNum = "12")
@Schema(title = "事件ID")
private String eventId;
/**
* 定位时间(手表定时上报的GPS定位时间)
*/
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "定位时间", format = "yyyy-MM-dd HH:mm:ss", width = 20, orderNum = "13")
@Schema(title = "定位时间")
private Date locateTime;
} }
@@ -0,0 +1,19 @@
package com.renkang.watch.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
* @author TianZi
* @date 2023/11/7 14:13
*/
@Data
public class BindDeviceDTO {
@Schema(title = "手表编号")
private String watchNo;
}
@@ -1,5 +1,6 @@
package com.renkang.watch.dto; package com.renkang.watch.dto;
import cn.hutool.http.HtmlUtil;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -18,6 +19,10 @@ public class CalorieDataByUserDTO {
private Integer pageSize = 10; private Integer pageSize = 10;
@Schema(title = "用户姓名") @Schema(title = "用户姓名")
private String realName; private String realName;
public void setRealName(String realName) {
this.realName = realName != null ? HtmlUtil.unescape(realName) : null;
}
@Schema(title = "工号") @Schema(title = "工号")
private String workNo; private String workNo;
@Schema(title = "用户id组") @Schema(title = "用户id组")
@@ -151,6 +151,20 @@ public class WatchMonitorData implements Serializable {
@Excel(name = "部门编码", width = 15) @Excel(name = "部门编码", width = 15)
@Schema(title = "部门编码") @Schema(title = "部门编码")
private java.lang.String orgCode; private java.lang.String orgCode;
/**
* 事件ID(用于告警事件与GPS位置更新关联的唯一标识)
*/
@Excel(name = "事件ID", width = 32)
@Schema(title = "事件ID")
private java.lang.String eventId;
/**
* 定位时间(手表上报的GPS定位时间,定时上报场景使用)
*/
@Excel(name = "定位时间", width = 20, format = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(title = "定位时间")
private java.util.Date locateTime;
@Schema(title = "员工编号") @Schema(title = "员工编号")
@TableField(exist = false) @TableField(exist = false)
@@ -1,22 +1,71 @@
package com.renkang.watch.vo.UserData.res; package com.renkang.watch.vo.UserData.res;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.renkang.watch.WatchH5IndexItemNewVo;
import lombok.Data; import lombok.Data;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
/** 用户统计数据 /**
* 用户统计数据
*
* @author stan * @author stan
* @since 2024-11-04 16:33 * @since 2024-11-04 16:33
*/ */
@Data @Data
public class UserStat { public class UserStat {
/**
* 数据类型:0=心率 1=血氧 2=压力 3=体温 4=睡眠
*/
private String type;
private BigDecimal latest; private BigDecimal latest;
private BigDecimal max; private BigDecimal max;
private BigDecimal min; private BigDecimal min;
private BigDecimal avg; private BigDecimal avg;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private Date latestTime; private Date latestTime;
public UserStat() {
}
public UserStat(WatchH5IndexItemNewVo watchH5IndexItemNewVo) {
switch (watchH5IndexItemNewVo.getWdType()) {
case "sleep":
this.type = "4";
break;
case "heart_rate":
this.type = "0";
break;
case "stress":
this.type = "2";
break;
case "spo2":
this.type = "1";
break;
case "temperature":
this.type = "3";
break;
default:
break;
}
this.latest = parseSafe(watchH5IndexItemNewVo.getDataValue());
this.max = parseSafe(watchH5IndexItemNewVo.getDataMax());
this.min = parseSafe(watchH5IndexItemNewVo.getDataMin());
this.avg = parseSafe(watchH5IndexItemNewVo.getDataAvg());
this.latestTime = watchH5IndexItemNewVo.getDataDateNew();
}
/**
* 安全解析字符串为 BigDecimal,入参为 null 或空串时返回 null
*
* @param value 待解析的字符串数值
* @return 解析结果,无效输入返回 null
*/
private static BigDecimal parseSafe(String value) {
if (value == null || value.isEmpty()) {
return null;
}
return BigDecimal.valueOf(Double.parseDouble(value));
}
} }
@@ -45,4 +45,15 @@ public class WatchNotifyVo implements Serializable {
private Date warnTime; private Date warnTime;
private String gpsErrorMsg; private String gpsErrorMsg;
/**
* 事件ID(用于告警事件与GPS位置更新关联的唯一标识)
*/
private String eventId;
/**
* 定位时间(手表上报的GPS定位时间,定时上报场景使用)
*/
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private Date locateTime;
} }
@@ -18,9 +18,14 @@ public class DeptStatData {
/** /**
* 已分配设备数 * 已分配设备数
*/ */
@Excel(name = "分配设备数", width = 15,orderNum = "4") @Excel(name = "分配设备数", width = 15,orderNum = "4")
private Integer assignedDeviceNums; private Integer assignedDeviceNums;
/**
* 未分配设备数(入库设备数 - 已分配设备数,仅一、二级部门有值)
*/
@Excel(name = "未分配设备数", width = 15,orderNum = "5")
private Integer undistributedDeviceNums;
@Excel(name = "入库设备数", width = 15,orderNum = "3") @Excel(name = "入库设备数", width = 15,orderNum = "3")
private Integer totalAssignedDeviceNums; private Integer totalAssignedDeviceNums;
@@ -28,25 +33,22 @@ public class DeptStatData {
/** /**
* 佩戴人员数 * 佩戴人员数
*/ */
@Excel(name = "佩戴人员数", width = 15,orderNum = "5") @Excel(name = "佩戴人员数", width = 15,orderNum = "6")
private Integer assignedUserNums; private Integer assignedUserNums;
/** /**
* 未佩戴人员数 * 未佩戴人员数
*/ */
@Excel(name = "未佩戴人员数", width = 15,orderNum = "6") @Excel(name = "未佩戴人员数", width = 15,orderNum = "7")
private Integer undistributedUserNums; private Integer undistributedUserNums;
@Excel(name = "心率异常人数", width = 15,orderNum = "7") @Excel(name = "心率异常人数", width = 15,orderNum = "8")
private Integer heartRateNums; private Integer heartRateNums;
@Excel(name = "血氧异常人数", width = 15,orderNum = "8") @Excel(name = "血氧异常人数", width = 15,orderNum = "9")
private Integer spo2Nums; private Integer spo2Nums;
@Excel(name = "压力异常人数", width = 15,orderNum = "9") @Excel(name = "压力异常人数", width = 15,orderNum = "10")
private Integer stressNums; private Integer stressNums;
@Excel(name = "体温异常人数", width = 15,orderNum = "10") @Excel(name = "体温异常人数", width = 15,orderNum = "11")
private Integer tempNums; private Integer tempNums;
} }
@@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Map; import java.util.Map;
@RestController @RestController
@@ -41,6 +42,16 @@ public class WatchDataApiController {
return Result.ok(watchDataApiService.userStatResult(type)); return Result.ok(watchDataApiService.userStatResult(type));
} }
/**
* 用户手表数据(批量返回)
*
* @return data
*/
@GetMapping("userWatchDataAll")
public Result<List<UserStat>> userStatResultAll(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date date) {
return Result.ok(watchDataApiService.userStatResultAll(date));
}
/** /**
* 用户此刻是否拥有手表 * 用户此刻是否拥有手表
* *
@@ -11,6 +11,7 @@ import com.renkang.watch.vo.UserData.res.sleep.WatchDataSleepRes;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Map; import java.util.Map;
public interface WatchDataApiService { public interface WatchDataApiService {
@@ -43,6 +44,14 @@ public interface WatchDataApiService {
UserStat userStatResult(String type); UserStat userStatResult(String type);
/**
* 查询当前登录用户所有健康指标统计数据(按指定日期过滤)
*
* @param date 查询日期,为 null 时返回最新一条数据
* @return 各指标统计结果列表
*/
List<UserStat> userStatResultAll(Date date);
Boolean userHaveWatch(String userId); Boolean userHaveWatch(String userId);
UserWatchInfo findWatchInfo(String userId); UserWatchInfo findWatchInfo(String userId);
@@ -7,6 +7,8 @@ import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import com.renkang.watch.WatchH5IndexItemNewVo;
import com.renkang.watch.api.bean.response.UserWatchInfo; import com.renkang.watch.api.bean.response.UserWatchInfo;
import com.renkang.watch.api.service.WatchDataApiService; import com.renkang.watch.api.service.WatchDataApiService;
import com.renkang.watch.dto.WatchDataWorkoutDTO; import com.renkang.watch.dto.WatchDataWorkoutDTO;
@@ -24,31 +26,34 @@ import com.renkang.watch.vo.UserData.res.bodyTemperature.BodyTempStat;
import com.renkang.watch.vo.UserData.res.bodyTemperature.BodyTempStatRes; import com.renkang.watch.vo.UserData.res.bodyTemperature.BodyTempStatRes;
import com.renkang.watch.vo.UserData.res.heartRate.WatchDataHeartRateRes; import com.renkang.watch.vo.UserData.res.heartRate.WatchDataHeartRateRes;
import com.renkang.watch.vo.UserData.res.sleep.WatchDataSleepRes; import com.renkang.watch.vo.UserData.res.sleep.WatchDataSleepRes;
import com.renkang.watch.vo.WatchH5IndexItemVo;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.vo.LoginUser; import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.DateUtils; import org.jeecg.common.util.DateUtils;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.global.GlobalUtils; import org.jeecg.global.GlobalUtils;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Service @Service
public class WatchDataApiServiceImpl implements WatchDataApiService { public class WatchDataApiServiceImpl implements WatchDataApiService {
@Autowired @Autowired
private WatchDataWorkoutMapper watchDataWorkoutMapper; private WatchDataWorkoutMapper watchDataWorkoutMapper;
@Autowired
private WatchStatUserInfoDayHeartRateMapper watchStatUserInfoDayHeartRateMapper;
@Autowired @Autowired
private WatchDataHeartRateMapper watchDataHeartRateMapper; private WatchDataHeartRateMapper watchDataHeartRateMapper;
@@ -60,9 +65,6 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
@Autowired @Autowired
private WatchStatUserInfoDaySpo2ServiceImpl watchStatUserInfoDaySpo2ServiceImpl; private WatchStatUserInfoDaySpo2ServiceImpl watchStatUserInfoDaySpo2ServiceImpl;
@Autowired
private WatchStatUserInfoDaySleepMapper watchStatUserInfoDaySleepMapper;
@Autowired @Autowired
private WatchDataSleepNewMinuteMapper watchDataSleepNewMinuteMapper; private WatchDataSleepNewMinuteMapper watchDataSleepNewMinuteMapper;
@@ -96,7 +98,6 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
@Autowired @Autowired
private IWatchDataTemperatureService watchDataTemperatureService; private IWatchDataTemperatureService watchDataTemperatureService;
@Autowired @Autowired
private IWatchDataStressService watchDataStressService; private IWatchDataStressService watchDataStressService;
@Autowired @Autowired
@@ -106,12 +107,47 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
@Autowired @Autowired
private WatchUserDataMapper watchUserDataMapper; private WatchUserDataMapper watchUserDataMapper;
@Autowired @Autowired
private WatchStatUserInfoDayHeartRateMapper watchStatUserInfoDayHeartRateMapper;
@Autowired
private WatchStatUserInfoDaySleepMapper watchStatUserInfoDaySleepMapper;
@Autowired
private WatchStatUserInfoDaySpo2Mapper watchStatUserInfoDaySpo2Mapper; private WatchStatUserInfoDaySpo2Mapper watchStatUserInfoDaySpo2Mapper;
@Autowired @Autowired
private WatchStatUserInfoDayStressMapper watchStatUserInfoDayStressMapper; private WatchStatUserInfoDayStressMapper watchStatUserInfoDayStressMapper;
@Autowired @Autowired
private WatchStatUserInfoDayTempMapper watchStatUserInfoDayTempMapper; private WatchStatUserInfoDayTempMapper watchStatUserInfoDayTempMapper;
@Autowired
private RedisUtil redisUtil;
/** 用户统计全量数据 Redis 缓存 key 前缀 */
private static final String USER_STAT_ALL_CACHE_KEY = "watch:userStatAll:";
private AsyncTaskExecutor asyncTaskExecutor;
public AsyncTaskExecutor getAsyncTaskExecutor() {
if (asyncTaskExecutor == null) {
initAsyncExecutor();
}
return asyncTaskExecutor;
}
@PostConstruct
public void init() {
initAsyncExecutor();
}
private void initAsyncExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(150);
taskExecutor.setMaxPoolSize(500);
taskExecutor.setQueueCapacity(500);
taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
taskExecutor.setThreadNamePrefix("watch-latest-pool1-");
taskExecutor.initialize();
asyncTaskExecutor = taskExecutor;
}
@Override @Override
public Result<Map<String,Object>> selectSportCostListApi() { public Result<Map<String,Object>> selectSportCostListApi() {
LoginUser loginUser = GlobalUtils.getLoginUser(); LoginUser loginUser = GlobalUtils.getLoginUser();
@@ -834,20 +870,171 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
String userId = GlobalUtils.getLoginUser().getId(); String userId = GlobalUtils.getLoginUser().getId();
switch (type) { switch (type) {
case "0": case "0":
return handleHeartRate(userId); return handleHeartRate(userId,null);
case "1": case "1":
return handleBloodOxygen(userId); return handleBloodOxygen(userId,null);
case "2": case "2":
return handleStress(userId); return handleStress(userId,null);
case "3": case "3":
return handleBodyTemperature(userId); return handleBodyTemperature(userId,null);
case "4": case "4":
return handleSleep(userId); return handleSleep(userId,null);
default: default:
return null; return null;
} }
} }
@Override
public List<UserStat> userStatResultAll(Date date) {
// 在主线程提前获取 userId,避免子线程无法访问 ThreadLocal 中的登录信息
String userId = GlobalUtils.getLoginUser().getId();
// 有时间参数时缓存 key 携带日期,避免与无参缓存冲突
String cacheKey = USER_STAT_ALL_CACHE_KEY + userId + (date != null ? "_" + date.getTime() : "");
// 命中 Redis 缓存直接返回
Object cached = redisUtil.get(cacheKey);
if (cached != null) {
return (List<UserStat>) cached;
}
// 构建「数据类型 → 异步查询 Future」映射,消除 switch-case 冗余
Map<String, CompletableFuture<WatchH5IndexItemNewVo>> futureMap = new HashMap<>();
futureMap.put("sleep", CompletableFuture.supplyAsync(() -> latestSleepData(userId), getAsyncTaskExecutor()));
futureMap.put("heart_rate", CompletableFuture.supplyAsync(() -> latestHeartRateData(userId), getAsyncTaskExecutor()));
futureMap.put("spo2", CompletableFuture.supplyAsync(() -> latestSpo2Data(userId), getAsyncTaskExecutor()));
futureMap.put("stress", CompletableFuture.supplyAsync(() -> latestStressData(userId), getAsyncTaskExecutor()));
futureMap.put("temperature", CompletableFuture.supplyAsync(() -> latestTempData(userId), getAsyncTaskExecutor()));
// 等待全部异步查询完成
CompletableFuture.allOf(futureMap.values().toArray(new CompletableFuture[0])).join();
// 转为顺序流遍历,避免并行流写入 ArrayList 的线程安全问题
List<WatchH5IndexItemNewVo> list = new ArrayList<>();
emptyItems().sequential().forEach(itemVo -> {
CompletableFuture<WatchH5IndexItemNewVo> future = futureMap.get(itemVo.getWdType());
// steps 等暂无对应查询的类型直接跳过
if (future == null) {
return;
}
WatchH5IndexItemNewVo data = future.join();
if (data == null) {
data = new WatchH5IndexItemNewVo();
}
// 将模板字段(wdType、wdTypeName、阈值)合并到结果对象
data.fieldCopy(itemVo);
// 睡眠数据特殊处理:dataValue 为空时补 "0"
if ("sleep".equals(itemVo.getWdType()) && StrUtil.isEmpty(data.getDataValue())) {
data.setDataValue("0");
}
list.add(data);
});
List<UserStat> result = list.stream().map(UserStat::new).collect(Collectors.toList());
// 写入 Redis 缓存,TTL=3分钟(180秒)
redisUtil.set(cacheKey, result, 180);
return result;
}
private Stream<WatchH5IndexItemVo> emptyItems() {
//睡眠
WatchH5IndexItemVo sleepVo = new WatchH5IndexItemVo();
sleepVo.setWdType("sleep");
//步数
WatchH5IndexItemVo stepsVo = new WatchH5IndexItemVo();
stepsVo.setWdType("steps");
//心率
WatchH5IndexItemVo hearRateVo = new WatchH5IndexItemVo();
hearRateVo.setWdType("heart_rate");
//压力
WatchH5IndexItemVo stressVo = new WatchH5IndexItemVo();
stressVo.setWdType("stress");
//血氧饱和度
WatchH5IndexItemVo spo2Vo = new WatchH5IndexItemVo();
spo2Vo.setWdType("spo2");
//体温
WatchH5IndexItemVo tempVo = new WatchH5IndexItemVo();
tempVo.setWdType("temperature");
List<WatchH5IndexItemVo> list = Arrays.asList(sleepVo, stepsVo, hearRateVo, stressVo, spo2Vo, tempVo);
return list.parallelStream()
.peek(itemVo -> {
itemVo.setWarnMin("0.0");
itemVo.setWarnMax("0.0");
});
}
private WatchH5IndexItemNewVo latestSleepData(String userId) {
MPJLambdaWrapper<WatchStatUserInfoDaySleep> wrapper = new MPJLambdaWrapper<>();
//因为睡眠详情页取的是长睡时长作为当天的睡眠时间 , 所以这里保持一致并没有取当天的睡眠总时长
wrapper.selectAs(WatchStatUserInfoDaySleep::getLongDurationTotal,WatchH5IndexItemNewVo::getDataValue);
wrapper.selectAs(WatchStatUserInfoDaySleep::getDeepSleepTimes,WatchH5IndexItemNewVo::getDataMax);
wrapper.selectAs(WatchStatUserInfoDaySleep::getLightSleepTimes,WatchH5IndexItemNewVo::getDataMin);
wrapper.selectAs(WatchStatUserInfoDaySleep::getSleepAvg,WatchH5IndexItemNewVo::getDataAvg);
wrapper.selectAs(WatchStatUserInfoDaySleep::getLastUploadTime,WatchH5IndexItemNewVo::getDataDateNew);
wrapper.eq(WatchStatUserInfoDaySleep::getUserId, userId);
wrapper.orderByDesc(WatchStatUserInfoDaySleep::getDataDate);
wrapper.last("limit 1");
return watchStatUserInfoDaySleepMapper.selectJoinOne(WatchH5IndexItemNewVo.class,wrapper);
}
private WatchH5IndexItemNewVo latestHeartRateData(String userId) {
MPJLambdaWrapper<WatchStatUserInfoDayHeartRate> wrapper = new MPJLambdaWrapper<>();
// 必须是watch_stat_user_info_day_heart_rate表中new_data字段启用后才能这样查
wrapper.selectAs(WatchStatUserInfoDayHeartRate::getNewValue,WatchH5IndexItemNewVo::getDataValue);
wrapper.selectAs(WatchStatUserInfoDayHeartRate::getMaxValue,WatchH5IndexItemNewVo::getDataMax);
wrapper.selectAs(WatchStatUserInfoDayHeartRate::getMinValue,WatchH5IndexItemNewVo::getDataMin);
wrapper.selectAs(WatchStatUserInfoDayHeartRate::getAvgValue,WatchH5IndexItemNewVo::getDataAvg);
wrapper.selectAs(WatchStatUserInfoDayHeartRate::getLastUploadTime,WatchH5IndexItemNewVo::getDataDateNew);
wrapper.eq(WatchStatUserInfoDayHeartRate::getUserId, userId);
wrapper.orderByDesc(WatchStatUserInfoDayHeartRate::getDataDate);
wrapper.last("limit 1");
return watchStatUserInfoDayHeartRateMapper.selectJoinOne(WatchH5IndexItemNewVo.class,wrapper);
}
private WatchH5IndexItemNewVo latestSpo2Data(String userId) {
MPJLambdaWrapper<WatchStatUserInfoDaySpo2> wrapper = new MPJLambdaWrapper<>();
// 必须是watch_stat_user_info_day_spo2表中new_data字段启用后才能这样查
wrapper.selectAs(WatchStatUserInfoDaySpo2::getNewValue,WatchH5IndexItemNewVo::getDataValue);
wrapper.selectAs(WatchStatUserInfoDaySpo2::getMaxValue,WatchH5IndexItemNewVo::getDataMax);
wrapper.selectAs(WatchStatUserInfoDaySpo2::getMinValue,WatchH5IndexItemNewVo::getDataMin);
wrapper.selectAs(WatchStatUserInfoDaySpo2::getAvgValue,WatchH5IndexItemNewVo::getDataAvg);
wrapper.selectAs(WatchStatUserInfoDaySpo2::getLastUploadTime,WatchH5IndexItemNewVo::getDataDateNew);
wrapper.eq(WatchStatUserInfoDaySpo2::getUserId, userId);
wrapper.orderByDesc(WatchStatUserInfoDaySpo2::getDataDate);
wrapper.last("limit 1");
return watchStatUserInfoDaySpo2Mapper.selectJoinOne(WatchH5IndexItemNewVo.class,wrapper);
}
private WatchH5IndexItemNewVo latestStressData(String userId) {
MPJLambdaWrapper<WatchStatUserInfoDayStress> wrapper = new MPJLambdaWrapper<>();
// 必须是watch_stat_user_info_day_stress表中new_data字段启用后才能这样查
wrapper.selectAs(WatchStatUserInfoDayStress::getNewValue,WatchH5IndexItemNewVo::getDataValue);
wrapper.selectAs(WatchStatUserInfoDayStress::getMaxValue,WatchH5IndexItemNewVo::getDataMax);
wrapper.selectAs(WatchStatUserInfoDayStress::getMinValue,WatchH5IndexItemNewVo::getDataMin);
wrapper.selectAs(WatchStatUserInfoDayStress::getAvgValue,WatchH5IndexItemNewVo::getDataAvg);
wrapper.selectAs(WatchStatUserInfoDayStress::getLastUploadTime,WatchH5IndexItemNewVo::getDataDateNew);
wrapper.eq(WatchStatUserInfoDayStress::getUserId, userId);
wrapper.orderByDesc(WatchStatUserInfoDayStress::getDataDate);
wrapper.last("limit 1");
return watchStatUserInfoDayStressMapper.selectJoinOne(WatchH5IndexItemNewVo.class,wrapper);
}
private WatchH5IndexItemNewVo latestTempData(String userId) {
MPJLambdaWrapper<WatchStatUserInfoDayTemp> wrapper = new MPJLambdaWrapper<>();
// 必须是watch_stat_user_info_day_temp表中new_data字段启用后才能这样查
wrapper.selectAs(WatchStatUserInfoDayTemp::getNewValue,WatchH5IndexItemNewVo::getDataValue);
wrapper.selectAs(WatchStatUserInfoDayTemp::getMaxValue,WatchH5IndexItemNewVo::getDataMax);
wrapper.selectAs(WatchStatUserInfoDayTemp::getMinValue,WatchH5IndexItemNewVo::getDataMin);
wrapper.selectAs(WatchStatUserInfoDayTemp::getAvgValue,WatchH5IndexItemNewVo::getDataAvg);
wrapper.selectAs(WatchStatUserInfoDayTemp::getLastUploadTime,WatchH5IndexItemNewVo::getDataDateNew);
wrapper.eq(WatchStatUserInfoDayTemp::getUserId, userId);
wrapper.orderByDesc(WatchStatUserInfoDayTemp::getDataDate);
wrapper.last("limit 1");
return watchStatUserInfoDayTempMapper.selectJoinOne(WatchH5IndexItemNewVo.class,wrapper);
}
@Override @Override
public Boolean userHaveWatch(String userId) { public Boolean userHaveWatch(String userId) {
if (StrUtil.isBlank(userId)) { if (StrUtil.isBlank(userId)) {
@@ -885,7 +1072,7 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
} }
private UserStat handleSleep(String userId) { private UserStat handleSleep(String userId, Date date) {
LambdaQueryWrapper<WatchStatUserInfoDaySleep> queryWrapper = Wrappers.lambdaQuery(WatchStatUserInfoDaySleep.class) LambdaQueryWrapper<WatchStatUserInfoDaySleep> queryWrapper = Wrappers.lambdaQuery(WatchStatUserInfoDaySleep.class)
.select(WatchStatUserInfoDaySleep::getId, .select(WatchStatUserInfoDaySleep::getId,
WatchStatUserInfoDaySleep::getDeepSleepTimes, WatchStatUserInfoDaySleep::getDeepSleepTimes,
@@ -893,13 +1080,15 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
WatchStatUserInfoDaySleep::getDeepLightTimes, WatchStatUserInfoDaySleep::getDeepLightTimes,
WatchStatUserInfoDaySleep::getDataDate) WatchStatUserInfoDaySleep::getDataDate)
.eq(WatchStatUserInfoDaySleep::getUserId, userId) .eq(WatchStatUserInfoDaySleep::getUserId, userId)
.orderByDesc(WatchStatUserInfoDaySleep::getDataDate) // 传了日期则精确匹配,否则取最新一条
.eq(date != null, WatchStatUserInfoDaySleep::getDataDate, date)
.orderByDesc(date == null, WatchStatUserInfoDaySleep::getDataDate)
.last("limit 1"); .last("limit 1");
WatchStatUserInfoDaySleep data = watchStatUserInfoDaySleepMapper.selectOne(queryWrapper); WatchStatUserInfoDaySleep data = watchStatUserInfoDaySleepMapper.selectOne(queryWrapper);
if (data == null) { if (data == null) {
return null; return new UserStat();
} }
UserStat stat = new UserStat(); UserStat stat = new UserStat();
@@ -915,15 +1104,15 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
return stat; return stat;
} }
private UserStat handleBodyTemperature(String userId) { private UserStat handleBodyTemperature(String userId, Date date) {
int type = 5; int type = 5;
int scale = 1; int scale = 1;
// 最新数据 // 最新数据
UserStat userStat = userWatchDataLatest(userId, type, scale); UserStat userStat = userWatchDataLatest(userId, type, scale);
if (userStat == null) { if (userStat == null) {
return null; return new UserStat();
} }
LambdaQueryWrapper<WatchStatUserInfoDayTemp> queryWrapper = getQueryWrapper(userId); LambdaQueryWrapper<WatchStatUserInfoDayTemp> queryWrapper = getQueryWrapper(userId, date);
WatchStatUserInfoDayTemp data = watchStatUserInfoDayTempMapper.selectOne(queryWrapper); WatchStatUserInfoDayTemp data = watchStatUserInfoDayTempMapper.selectOne(queryWrapper);
if (data == null) { if (data == null) {
return userStat; return userStat;
@@ -933,15 +1122,15 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
return userStat; return userStat;
} }
private UserStat handleStress(String userId) { private UserStat handleStress(String userId, Date date) {
int type = 3; int type = 3;
int scale = 0; int scale = 0;
// 最新数据 // 最新数据
UserStat userStat = userWatchDataLatest(userId, type, scale); UserStat userStat = userWatchDataLatest(userId, type, scale);
if (userStat == null) { if (userStat == null) {
return null; return new UserStat();
} }
LambdaQueryWrapper<WatchStatUserInfoDayStress> queryWrapper = getQueryWrapper(userId); LambdaQueryWrapper<WatchStatUserInfoDayStress> queryWrapper = getQueryWrapper(userId, date);
WatchStatUserInfoDayStress data = watchStatUserInfoDayStressMapper.selectOne(queryWrapper); WatchStatUserInfoDayStress data = watchStatUserInfoDayStressMapper.selectOne(queryWrapper);
if (data == null) { if (data == null) {
return userStat; return userStat;
@@ -951,15 +1140,15 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
return userStat; return userStat;
} }
private UserStat handleBloodOxygen(String userId) { private UserStat handleBloodOxygen(String userId, Date date) {
int type = 2; int type = 2;
int scale = 0; int scale = 0;
// 最新数据 // 最新数据
UserStat userStat = userWatchDataLatest(userId, type, scale); UserStat userStat = userWatchDataLatest(userId, type, scale);
if (userStat == null) { if (userStat == null) {
return null; return new UserStat();
} }
LambdaQueryWrapper<WatchStatUserInfoDaySpo2> queryWrapper = getQueryWrapper(userId); LambdaQueryWrapper<WatchStatUserInfoDaySpo2> queryWrapper = getQueryWrapper(userId, date);
WatchStatUserInfoDaySpo2 data = watchStatUserInfoDaySpo2Mapper.selectOne(queryWrapper); WatchStatUserInfoDaySpo2 data = watchStatUserInfoDaySpo2Mapper.selectOne(queryWrapper);
if (data == null) { if (data == null) {
return userStat; return userStat;
@@ -969,15 +1158,15 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
return userStat; return userStat;
} }
private UserStat handleHeartRate(String userId) { private UserStat handleHeartRate(String userId, Date date) {
int type = 1; int type = 1;
int scale = 0; int scale = 0;
// 最新数据 // 最新数据
UserStat userStat = userWatchDataLatest(userId, type, scale); UserStat userStat = userWatchDataLatest(userId, type, scale);
if (userStat == null) { if (userStat == null) {
return null; return new UserStat();
} }
LambdaQueryWrapper<WatchStatUserInfoDayHeartRate> queryWrapper = getQueryWrapper(userId); LambdaQueryWrapper<WatchStatUserInfoDayHeartRate> queryWrapper = getQueryWrapper(userId, date);
WatchStatUserInfoDayHeartRate data = watchStatUserInfoDayHeartRateMapper.selectOne(queryWrapper); WatchStatUserInfoDayHeartRate data = watchStatUserInfoDayHeartRateMapper.selectOne(queryWrapper);
if (data == null) { if (data == null) {
return userStat; return userStat;
@@ -1029,10 +1218,12 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
return stat; return stat;
} }
private <T> LambdaQueryWrapper<T> getQueryWrapper(String userId) { private <T> LambdaQueryWrapper<T> getQueryWrapper(String userId, Date date) {
QueryWrapper<T> queryWrapper = new QueryWrapper<>(); QueryWrapper<T> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", userId) queryWrapper.eq("user_id", userId)
.orderByDesc("data_date") // 传了日期则精确匹配,否则取最新一条
.eq(date != null, "data_date", date)
.orderByDesc(date == null, "data_date")
.last("limit 1"); .last("limit 1");
return queryWrapper.lambda(); return queryWrapper.lambda();
} }
@@ -1051,4 +1242,17 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
return BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_UP); return BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_UP);
} }
/**
* 设置统计数据类型标识,stat 为 null 时直接返回 null
*
* @param stat UserStat 对象
* @param type 类型编码:0=心率 1=血氧 2=压力 3=体温 4=睡眠
*/
private UserStat setType(UserStat stat, String type) {
if (stat != null) {
stat.setType(type);
}
return stat;
}
} }
@@ -336,8 +336,9 @@ public class AsyncImportService {
updatedWatchDevice.setBindDate(watchDevice.getBindDate()==null?new Date():watchDevice.getBindDate()); updatedWatchDevice.setBindDate(watchDevice.getBindDate()==null?new Date():watchDevice.getBindDate());
updatedWatchDevice.setUpdateDate(new Date()); updatedWatchDevice.setUpdateDate(new Date());
updatedWatchDevice.setUpdateBy(sysUser.getId()); updatedWatchDevice.setUpdateBy(sysUser.getId());
updatedWatchDevice.setDeptId(device.getDeptId()); // 这里修改成以用户的部门为准,更新手表的所属部门
updatedWatchDevice.setOrgCode(device.getOrgCode()); updatedWatchDevice.setDeptId(sysCache.getDepartIdByOrgCode(user.getOrgCode()));
updatedWatchDevice.setOrgCode(user.getOrgCode());
updateList.add(updatedWatchDevice); updateList.add(updatedWatchDevice);
} }
info1.setHandleMsg(index + "/" + count); info1.setHandleMsg(index + "/" + count);
@@ -38,6 +38,21 @@ public class WatchApiController {
return watchMonitorDataService.notice(json); return watchMonitorDataService.notice(json);
} }
/**
* 更新告警事件GPS位置信息接口
* 根据 watchNo + eventId 联合定位已有告警数据;
* 仅更新GPS相关字段,不推送大屏与短信;
* 内部会调用高德接口进行GPS84→GCJ02坐标转换与地址反查。
*
* @param json 入参(watchNo、eventId、lon、lat、address、locateTime、gpsErrorMsg
* @return 处理结果
*/
@PostMapping("/updateMonitorGps")
@Operation(summary = "更新告警事件GPS位置信息")
public AjaxJson updateMonitorGps(@RequestBody JSONObject json) {
return watchMonitorDataService.updateMonitorGps(json);
}
@GetMapping("/setData") @GetMapping("/setData")
public AjaxJson setData() { public AjaxJson setData() {
AjaxJson ajaxJson = new AjaxJson(); AjaxJson ajaxJson = new AjaxJson();
@@ -11,10 +11,7 @@ import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -327,4 +324,5 @@ public class WatchController {
// } // }
return result; return result;
} }
} }
@@ -185,6 +185,12 @@ public class WatchDeviceController extends JeecgController<WatchDevice, IWatchDe
return watchDeviceService.bindUser(dto); return watchDeviceService.bindUser(dto);
} }
@Operation(summary = "移动端绑定设备(手表)", description = "移动端绑定设备(手表)")
@GetMapping("/app/bindDevice")
public Result bindDevice(@RequestParam(name = "watchNo") String watchNo) throws ParseException {
return watchDeviceService.bindDevice(watchNo);
}
/** /**
* 解绑用户 * 解绑用户
* *
@@ -194,10 +200,18 @@ public class WatchDeviceController extends JeecgController<WatchDevice, IWatchDe
@Operation(summary = "解绑用户", description = "解绑用户") @Operation(summary = "解绑用户", description = "解绑用户")
@PostMapping(value = "/unbundleUser") @PostMapping(value = "/unbundleUser")
@RequiresPermissions("watch:watch_device:unbindUser") @RequiresPermissions("watch:watch_device:unbindUser")
public Result unbundleUser(@RequestBody() BindUserDTO dto) throws ParseException { public Result unbundleUser(@RequestBody() BindUserDTO dto) {
return watchDeviceService.unbundleUser(dto); return watchDeviceService.unbundleUser(dto);
} }
@Operation(summary = "移动端解绑设备(手表)", description = "移动端解绑设备(手表)")
@GetMapping("/app/unbundleDevice")
public Result unbundleDevice(@RequestParam(name = "watchNo") String watchNo) {
//移动端暂不允许解绑手表
return Result.error("请联系健康管理员解绑");
// return watchDeviceService.unbundleDevice(watchNo);
}
/** /**
* 设置上传频次 * 设置上传频次
* *
@@ -343,6 +357,10 @@ public class WatchDeviceController extends JeecgController<WatchDevice, IWatchDe
@DeleteMapping(value = "/delete") @DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name = "id", required = true) String id) { public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
WatchDevice watchDevice = watchDeviceService.getById(id); WatchDevice watchDevice = watchDeviceService.getById(id);
// 已绑定用户的手表不允许删除
if (StringUtils.hasLength(watchDevice.getBindUserId())) {
return Result.error("该手表已绑定用户,请先解绑后再删除!");
}
watchDeviceService.removeById(id); watchDeviceService.removeById(id);
// 删除开关 // 删除开关
LambdaQueryWrapper<WatchDeviceSwitch> queryWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<WatchDeviceSwitch> queryWrapper = new LambdaQueryWrapper<>();
@@ -422,6 +440,8 @@ public class WatchDeviceController extends JeecgController<WatchDevice, IWatchDe
} }
if (!StringUtils.hasLength(deptId) || "undefined".equals(deptId)) { if (!StringUtils.hasLength(deptId) || "undefined".equals(deptId)) {
throw new JeecgBootException("部门不能为空"); throw new JeecgBootException("部门不能为空");
//可为空,空则算入新疆油田公司
// deptId = sysCache.getDepartIdByOrgCode("A01");
} }
String sheetName = "设备导入"; String sheetName = "设备导入";
String fileName = sheetName + "_" + DateUtils.getDate("yyyyMMddHHmmss"); String fileName = sheetName + "_" + DateUtils.getDate("yyyyMMddHHmmss");
@@ -43,12 +43,16 @@ public interface IWatchDeviceService extends IService<WatchDevice> {
*/ */
Result bindUser(BindUserDTO dto) throws ParseException; Result bindUser(BindUserDTO dto) throws ParseException;
Result bindDevice(String watchNo) throws ParseException;
/** /**
* 绑定用户 * 绑定用户
* @param dto * @param dto
* @return * @return
*/ */
Result unbundleUser(BindUserDTO dto) throws ParseException; Result unbundleUser(BindUserDTO dto);
Result unbundleDevice(String watchNo);
/** /**
* 工具开关 * 工具开关
@@ -38,6 +38,17 @@ public interface IWatchMonitorDataService extends IService<WatchMonitorData> {
*/ */
AjaxJson notice(JSONObject jsonObject); AjaxJson notice(JSONObject jsonObject);
/**
* 更新告警事件GPS位置信息
* 根据 watchNo + eventId 联合定位已有告警数据;
* 缺失任一定位字段则忽略;已存在GPS信息则忽略;
* 仅更新数据,不推送大屏与短信。
*
* @param jsonObject 入参(含 watchNo、eventId、lon、lat、address、locateTime、gpsErrorMsg 等)
* @return 处理结果
*/
AjaxJson updateMonitorGps(JSONObject jsonObject);
/** /**
* 处理历史数据 orgCode为空的问题 * 处理历史数据 orgCode为空的问题
* @return * @return
@@ -595,11 +595,32 @@ public class DeptStatisticDataServiceImpl extends ServiceImpl<DeptStatisticDataM
filter.setOrgCodeLeaf("A99A99A99"); filter.setOrgCodeLeaf("A99A99A99");
} }
/**
* 查询部门统计数据(分页)
*
* @param filter 查询过滤条件,包含部门编码、时间范围、分页参数等
* @return 分页的部门统计数据列表
*/
@Override @Override
public IPage<DeptStatData> getDeptData(DepartStatsFilter filter) { public IPage<DeptStatData> getDeptData(DepartStatsFilter filter) {
return getDeptData(filter, true).pagedData(); return getDeptData(filter, true).pagedData();
} }
/**
* 查询部门统计数据的核心实现
* <p>
* 执行流程:
* 1. 权限校验,补全过滤条件
* 2. 查询各部门设备总数 Map 和绑定/佩戴数 Map
* 3. 根据过滤条件确定最终需要展示的部门列表
* 4. 查询各部门异常预警数量
* 5. 按需分页,将部门列表转换为统计 DTO
* </p>
*
* @param filter 查询过滤条件
* @param paged 是否需要分页,true=分页查询,false=全量查询(用于导出等场景)
* @return 包含统计数据和中间结果的传输对象
*/
private DepartStatTransfer getDeptData(DepartStatsFilter filter, boolean paged) { private DepartStatTransfer getDeptData(DepartStatsFilter filter, boolean paged) {
authCheck(filter); authCheck(filter);
DepartStatTransfer transfer = new DepartStatTransfer() DepartStatTransfer transfer = new DepartStatTransfer()
@@ -639,12 +660,29 @@ public class DeptStatisticDataServiceImpl extends ServiceImpl<DeptStatisticDataM
return transfer; return transfer;
} }
/**
* 将单个部门实体转换为统计 DTO
* <p>
* orgCode 编码规则:3位一层,如 A01=一级,A01A01=二级,A01A01A01=三级
* 区分三种视图:
* - 一级部门(orgCode 长度为 3):展示自身信息,叶子名称置为 "-",取自身设备总数
* - 二级部门(orgCode 长度为 6):同一级,展示自身信息,叶子名称置为 "-",取自身设备总数
* - 三级部门(orgCode 长度为 9):展示所属上级名称 + 自身名称,绑定/佩戴数取自身数据
* 当 allSecond=true(全量视图)时,绑定/佩戴数始终取各部门自身数据
* </p>
*
* @param depart 部门实体
* @param transfer 包含所有统计中间数据的传输对象
* @return 填充完毕的部门统计 DTO
*/
private DeptStatData transferData(SysDepart depart, DepartStatTransfer transfer) { private DeptStatData transferData(SysDepart depart, DepartStatTransfer transfer) {
DeptStatData deptStatData = new DeptStatData(); DeptStatData deptStatData = new DeptStatData();
String orgCode = null != depart ? depart.getOrgCode() : null; String orgCode = null != depart ? depart.getOrgCode() : null;
boolean allSecond = StrUtil.isEmpty(transfer.filter().getOrgCode()); boolean allSecond = StrUtil.isEmpty(transfer.filter().getOrgCode());
boolean isFirst = StrUtil.isNotEmpty(orgCode) && orgCode.length() == 3;
boolean isSecond = StrUtil.isNotEmpty(orgCode) && orgCode.length() == 6; boolean isSecond = StrUtil.isNotEmpty(orgCode) && orgCode.length() == 6;
if (isSecond) { if (isFirst || isSecond) {
// 一级/二级部门行:展示自身信息,叶子名称占位
deptStatData.setOrgCode(orgCode); deptStatData.setOrgCode(orgCode);
deptStatData.setOrgCodeLeaf(orgCode); deptStatData.setOrgCodeLeaf(orgCode);
deptStatData.setOrgCodeName(depart.getDepartName()); deptStatData.setOrgCodeName(depart.getDepartName());
@@ -655,6 +693,7 @@ public class DeptStatisticDataServiceImpl extends ServiceImpl<DeptStatisticDataM
.getDeviceCount() .getDeviceCount()
); );
} else { } else {
// 三级部门行:展示所属上级名称 + 自身名称
deptStatData.setOrgCode(Optional.ofNullable(transfer.secondDepart()).map(SysDepart::getOrgCode).orElse("")); deptStatData.setOrgCode(Optional.ofNullable(transfer.secondDepart()).map(SysDepart::getOrgCode).orElse(""));
deptStatData.setOrgCodeLeaf(orgCode); deptStatData.setOrgCodeLeaf(orgCode);
deptStatData.setOrgCodeName(Optional.ofNullable(transfer.secondDepart()).map(SysDepart::getDepartName).orElse("")); deptStatData.setOrgCodeName(Optional.ofNullable(transfer.secondDepart()).map(SysDepart::getDepartName).orElse(""));
@@ -662,15 +701,21 @@ public class DeptStatisticDataServiceImpl extends ServiceImpl<DeptStatisticDataM
} }
fillWatchNums( fillWatchNums(
deptStatData, deptStatData,
(!allSecond && isSecond) (!allSecond && (isFirst || isSecond))
? ?
transfer.secondDepartBindAndWearNum() transfer.secondDepartBindAndWearNum()
: :
transfer.departBindAndWearNumMap().getOrDefault(orgCode, new DepartDeviceNum()) transfer.departBindAndWearNumMap().getOrDefault(orgCode, new DepartDeviceNum())
); );
// 未分配设备数 = 入库设备数 - 已分配设备数,仅一、二级部门有入库数据
if (deptStatData.getTotalAssignedDeviceNums() != null) {
deptStatData.setUndistributedDeviceNums(
deptStatData.getTotalAssignedDeviceNums() - deptStatData.getAssignedDeviceNums()
);
}
fillWarningNums( fillWarningNums(
deptStatData, deptStatData,
(!allSecond && isSecond) (!allSecond && (isFirst || isSecond))
? ?
transfer.secondDepartAbnormalCount() transfer.secondDepartAbnormalCount()
: :
@@ -679,12 +724,30 @@ public class DeptStatisticDataServiceImpl extends ServiceImpl<DeptStatisticDataM
return deptStatData; return deptStatData;
} }
/**
* 填充手表绑定与佩戴相关数量
* <p>
* assignedDeviceNums = 已绑定设备数
* assignedUserNums = 已佩戴人数
* undistributedUserNums = 已绑定但未佩戴数(bindCount - wearCount
* </p>
*/
private void fillWatchNums(DeptStatData deptStatData, DepartDeviceNum departDeviceNum) { private void fillWatchNums(DeptStatData deptStatData, DepartDeviceNum departDeviceNum) {
deptStatData.setAssignedDeviceNums(Optional.ofNullable(departDeviceNum.getBindCount()).orElse(0)); deptStatData.setAssignedDeviceNums(Optional.ofNullable(departDeviceNum.getBindCount()).orElse(0));
deptStatData.setAssignedUserNums(Optional.ofNullable(departDeviceNum.getWearCount()).orElse(0)); deptStatData.setAssignedUserNums(Optional.ofNullable(departDeviceNum.getWearCount()).orElse(0));
deptStatData.setUndistributedUserNums(deptStatData.getAssignedDeviceNums() - deptStatData.getAssignedUserNums()); deptStatData.setUndistributedUserNums(deptStatData.getAssignedDeviceNums() - deptStatData.getAssignedUserNums());
} }
/**
* 填充各健康指标的预警数量
* <p>
* 从 abnormalCountMap 中按事件类型取值:
* stress → 压力预警数
* heart_rate → 心率预警数
* spo2 → 血氧预警数
* temperature_anomalies → 体温预警数
* </p>
*/
private void fillWarningNums(DeptStatData deptStatData, Map<String, AbnormalCount> abnormalCountMap) { private void fillWarningNums(DeptStatData deptStatData, Map<String, AbnormalCount> abnormalCountMap) {
deptStatData.setStressNums(Optional.ofNullable(abnormalCountMap.get("stress")).map(AbnormalCount::getWarningCount).orElse(0)); deptStatData.setStressNums(Optional.ofNullable(abnormalCountMap.get("stress")).map(AbnormalCount::getWarningCount).orElse(0));
deptStatData.setHeartRateNums(Optional.ofNullable(abnormalCountMap.get("heart_rate")).map(AbnormalCount::getWarningCount).orElse(0)); deptStatData.setHeartRateNums(Optional.ofNullable(abnormalCountMap.get("heart_rate")).map(AbnormalCount::getWarningCount).orElse(0));
@@ -722,6 +785,10 @@ public class DeptStatisticDataServiceImpl extends ServiceImpl<DeptStatisticDataM
return sysCache.getDepartByOrgCode(filter.getOrgCodeLeaf()); return sysCache.getDepartByOrgCode(filter.getOrgCodeLeaf());
} }
/**
* 查询各部门的设备总数 Map(orgCode → 设备数)
* 当过滤条件指定了三级部门(orgCodeLeaf 不为空)时,设备总数无需展示,直接返回空 Map
*/
private Map<String, DepartDeviceNum> getDepartDeviceNumMap(DepartStatsFilter filter) { private Map<String, DepartDeviceNum> getDepartDeviceNumMap(DepartStatsFilter filter) {
if (StrUtil.isNotEmpty(filter.getOrgCodeLeaf())) { if (StrUtil.isNotEmpty(filter.getOrgCodeLeaf())) {
return Collections.emptyMap(); return Collections.emptyMap();
@@ -732,6 +799,9 @@ public class DeptStatisticDataServiceImpl extends ServiceImpl<DeptStatisticDataM
.collect(Collectors.toMap(DepartDeviceNum::getOrgCode, Function.identity())); .collect(Collectors.toMap(DepartDeviceNum::getOrgCode, Function.identity()));
} }
/**
* 查询各部门的设备绑定数与佩戴数 Map(orgCode → 绑定/佩戴数)
*/
private Map<String, DepartDeviceNum> getDepartBindAndWearNumMap(DepartStatsFilter filter) { private Map<String, DepartDeviceNum> getDepartBindAndWearNumMap(DepartStatsFilter filter) {
return watchStatisticsMapper.deviceBindAndWearNums(filter) return watchStatisticsMapper.deviceBindAndWearNums(filter)
.stream() .stream()
@@ -739,6 +809,10 @@ public class DeptStatisticDataServiceImpl extends ServiceImpl<DeptStatisticDataM
.collect(Collectors.toMap(DepartDeviceNum::getOrgCode, Function.identity())); .collect(Collectors.toMap(DepartDeviceNum::getOrgCode, Function.identity()));
} }
/**
* 查询各部门各事件类型的异常预警数量,结构为双层 Map:
* orgCode → (eventType → AbnormalCount)
*/
private Map<String, Map<String, AbnormalCount>> getDepartAbnormalCountMap(DepartStatsFilter filter) { private Map<String, Map<String, AbnormalCount>> getDepartAbnormalCountMap(DepartStatsFilter filter) {
return watchStatisticsMapper.abnormalWarningCount(filter) return watchStatisticsMapper.abnormalWarningCount(filter)
.stream() .stream()
@@ -746,6 +820,12 @@ public class DeptStatisticDataServiceImpl extends ServiceImpl<DeptStatisticDataM
.collect(Collectors.groupingBy(AbnormalCount::getOrgCode, Collectors.toMap(AbnormalCount::getEventType, Function.identity()))); .collect(Collectors.groupingBy(AbnormalCount::getOrgCode, Collectors.toMap(AbnormalCount::getEventType, Function.identity())));
} }
/**
* 汇总二级部门下所有三级部门的绑定/佩戴数,用于二级部门行的数据展示
* <p>
* 仅在"指定了二级部门但未指定三级部门"时有效,其他情况返回 null
* </p>
*/
private DepartDeviceNum getSecondDepartBindAndWearNum(DepartStatsFilter filter, private DepartDeviceNum getSecondDepartBindAndWearNum(DepartStatsFilter filter,
Map<String, DepartDeviceNum> departBindAndWearNumMap) { Map<String, DepartDeviceNum> departBindAndWearNumMap) {
if (StrUtil.isNotEmpty(filter.getOrgCodeLeaf()) || StrUtil.isEmpty(filter.getOrgCode())) { if (StrUtil.isNotEmpty(filter.getOrgCodeLeaf()) || StrUtil.isEmpty(filter.getOrgCode())) {
@@ -764,6 +844,13 @@ public class DeptStatisticDataServiceImpl extends ServiceImpl<DeptStatisticDataM
return n1; return n1;
} }
/**
* 汇总二级部门下所有三级部门的异常预警数,用于二级部门行的数据展示
* <p>
* 仅在"指定了二级部门但未指定三级部门"时有效,其他情况返回 null
* 合并时按 eventType 累加 warningCount
* </p>
*/
private Map<String, AbnormalCount> getSecondDepartAbnormalCount(DepartStatsFilter filter, private Map<String, AbnormalCount> getSecondDepartAbnormalCount(DepartStatsFilter filter,
Map<String, Map<String, AbnormalCount>> departAbnormalCountMap) { Map<String, Map<String, AbnormalCount>> departAbnormalCountMap) {
if (StrUtil.isNotEmpty(filter.getOrgCodeLeaf()) || StrUtil.isEmpty(filter.getOrgCode())) { if (StrUtil.isNotEmpty(filter.getOrgCodeLeaf()) || StrUtil.isEmpty(filter.getOrgCode())) {
@@ -778,6 +865,14 @@ public class DeptStatisticDataServiceImpl extends ServiceImpl<DeptStatisticDataM
return n1; return n1;
} }
/**
* 根据过滤条件确定最终需要展示的部门列表,分三种情况:
* <ul>
* <li>指定三级部门(orgCodeLeaf 不为空):仅返回该三级部门(无绑定数据则返回空)</li>
* <li>指定二级部门(orgCode 不为空):返回该部门本身 + 其下有绑定数据的子部门,按层级(orgCode 长度)升序后再按排序字段排序</li>
* <li>全量(orgCode 为空):返回所有有设备数据的部门,同样按层级升序后再按排序字段排序,确保一级部门排在最前</li>
* </ul>
*/
private List<SysDepart> getFinalDeparts(DepartStatsFilter filter, private List<SysDepart> getFinalDeparts(DepartStatsFilter filter,
Map<String, DepartDeviceNum> departDeviceNumMap, Map<String, DepartDeviceNum> departDeviceNumMap,
Map<String, DepartDeviceNum> departBindAndWearNumMap) { Map<String, DepartDeviceNum> departBindAndWearNumMap) {
@@ -804,7 +899,10 @@ public class DeptStatisticDataServiceImpl extends ServiceImpl<DeptStatisticDataM
return departDeviceNumMap.keySet() return departDeviceNumMap.keySet()
.stream() .stream()
.map(sysBaseAPI::getDepartIdsByOrgCodeNew) .map(sysBaseAPI::getDepartIdsByOrgCodeNew)
.sorted(this::departComparator) .sorted(
Comparator.<SysDepart>comparingInt(sysDepart -> sysDepart.getOrgCode().length())
.thenComparing(this::departComparator)
)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
} }
@@ -825,6 +923,9 @@ public class DeptStatisticDataServiceImpl extends ServiceImpl<DeptStatisticDataM
return order1.compareTo(order2); return order1.compareTo(order2);
} }
/**
* 对内存中的部门列表执行分页截取
*/
private List<SysDepart> getPagedDeparts(List<SysDepart> sysDeparts, DepartStatsFilter filter) { private List<SysDepart> getPagedDeparts(List<SysDepart> sysDeparts, DepartStatsFilter filter) {
return getSubList(sysDeparts, filter.getPageNo(), filter.getPageSize()); return getSubList(sysDeparts, filter.getPageNo(), filter.getPageSize());
} }
@@ -3,6 +3,7 @@ package com.renkang.watch.service.impl;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -87,6 +88,7 @@ public class WatchBindHisServiceImpl extends ServiceImpl<WatchBindHisMapper, Wat
@Override @Override
public IPage<WatchBindHisVo> queryPageList(IPage<WatchBindHisVo> page, WatchBindHisDTO dto) { public IPage<WatchBindHisVo> queryPageList(IPage<WatchBindHisVo> page, WatchBindHisDTO dto) {
if (StringUtils.hasLength(dto.getBindRealName())) { if (StringUtils.hasLength(dto.getBindRealName())) {
dto.setBindRealName(HtmlUtil.unescape(dto.getBindRealName()));
List<String> userIdsByDepartAndUsername = watchCloudManager.getUserIdsByDepartAndUsername("", dto.getBindRealName(), "", ""); List<String> userIdsByDepartAndUsername = watchCloudManager.getUserIdsByDepartAndUsername("", dto.getBindRealName(), "", "");
if (CollectionUtil.isEmpty(userIdsByDepartAndUsername)) { if (CollectionUtil.isEmpty(userIdsByDepartAndUsername)) {
return new Page<>(); return new Page<>();
@@ -372,6 +374,7 @@ public class WatchBindHisServiceImpl extends ServiceImpl<WatchBindHisMapper, Wat
String orgCode = StrUtil.isNotBlank(dto.getThirdDepart())?dto.getThirdDepart() : dto.getSecondDepart(); String orgCode = StrUtil.isNotBlank(dto.getThirdDepart())?dto.getThirdDepart() : dto.getSecondDepart();
List<String> userIds = new ArrayList<>(); List<String> userIds = new ArrayList<>();
if (StrUtil.isNotBlank(dto.getBindRealName())) { if (StrUtil.isNotBlank(dto.getBindRealName())) {
dto.setBindRealName(HtmlUtil.unescape(dto.getBindRealName()));
userIds = watchCloudManager.getUserIdsByDepartAndUsername("", dto.getBindRealName(), "", ""); userIds = watchCloudManager.getUserIdsByDepartAndUsername("", dto.getBindRealName(), "", "");
if (CollectionUtil.isEmpty(userIds)) { if (CollectionUtil.isEmpty(userIds)) {
return new Page<>(); return new Page<>();
@@ -7,6 +7,7 @@ import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -104,6 +105,9 @@ public class WatchDataServiceImpl extends ServiceImpl<WatchDataMapper, WatchData
} }
String sql = QueryGenerator.installAuthJdbc(WatchData.class); String sql = QueryGenerator.installAuthJdbc(WatchData.class);
if(StrUtil.isNotBlank(watchData.getUsername())){
watchData.setUsername(HtmlUtil.unescape(watchData.getUsername()));
}
IPage<WatchData> pageList = watchDataMapper.pageCustom(page, watchData, sql); IPage<WatchData> pageList = watchDataMapper.pageCustom(page, watchData, sql);
if (CollectionUtil.isNotEmpty(pageList.getRecords())) { if (CollectionUtil.isNotEmpty(pageList.getRecords())) {
List<LoginUserNew> loginUserNews = null; List<LoginUserNew> loginUserNews = null;
@@ -7,6 +7,7 @@ import cn.hutool.core.date.DateUnit;
import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -167,6 +168,9 @@ public class WatchDeviceServiceImpl extends ServiceImpl<WatchDeviceMapper, Watch
@Override @Override
public IPage<WatchDeviceVo> queryPageList(IPage<WatchDeviceVo> page, WatchDeviceDTO dto) { public IPage<WatchDeviceVo> queryPageList(IPage<WatchDeviceVo> page, WatchDeviceDTO dto) {
if (StrUtil.isNotBlank(dto.getBindRealName()) || StrUtil.isNotBlank(dto.getWorkNo())) { if (StrUtil.isNotBlank(dto.getBindRealName()) || StrUtil.isNotBlank(dto.getWorkNo())) {
if(StrUtil.isNotBlank(dto.getBindRealName())){
dto.setBindRealName(HtmlUtil.unescape(dto.getBindRealName()));
}
List<String> userIds = watchCloudManager.getUserIdsByDepartAndUsername("", dto.getBindRealName(), "", dto.getWorkNo()); List<String> userIds = watchCloudManager.getUserIdsByDepartAndUsername("", dto.getBindRealName(), "", dto.getWorkNo());
if (CollectionUtil.isEmpty(userIds)) { if (CollectionUtil.isEmpty(userIds)) {
return new Page<>(); return new Page<>();
@@ -291,10 +295,20 @@ public class WatchDeviceServiceImpl extends ServiceImpl<WatchDeviceMapper, Watch
} }
WatchDevice watchDevice = getDeviceByWatchNo(dto.getWatchNo()); WatchDevice watchDevice = getDeviceByWatchNo(dto.getWatchNo());
if (watchDevice == null) { if (watchDevice == null) {
return Result.error("该工具不存在"); return Result.error("监测工具不存在");
} }
// 新增手表只能绑定给本单位的人
LoginUser userById = sysBaseAPI.getUserById(dto.getBindUserId());
if (userById.getOrgCode() == null || watchDevice.getOrgCode() == null ||
userById.getOrgCode().length() < 6 || watchDevice.getOrgCode().length() < 6 ||
!userById.getOrgCode().substring(0, 6).equals(watchDevice.getOrgCode().substring(0, 6))) {
return Result.error("手表只能绑定给本单位人员");
}
if (isDeviceAlreadyBound(watchDevice, dto)) { if (isDeviceAlreadyBound(watchDevice, dto)) {
return Result.error("该设备已绑定该员工"); return Result.error("该设备已绑定");
} }
// 查询员工是否有绑定 // 查询员工是否有绑定
List<WatchDevice> deviceByUserIdList = getDeviceByUserId(dto.getBindUserId()); List<WatchDevice> deviceByUserIdList = getDeviceByUserId(dto.getBindUserId());
@@ -317,14 +331,20 @@ public class WatchDeviceServiceImpl extends ServiceImpl<WatchDeviceMapper, Watch
} }
// 更新工具信息 // 更新工具信息
watchDevice.setBindDate(dto.getBindDate()); watchDevice.setBindDate(dto.getBindDate());
// 根据员工更新手表单位信息(如果单位编码大于6位取6位)
String orgCode = userById.getOrgCode();
if(StrUtil.isNotBlank(orgCode) && orgCode.length() > 6){
orgCode = orgCode.substring(0, 6);
}
watchDevice.setOrgCode(orgCode);
watchDevice.setDeptId(sysCache.getDepartIdByOrgCode(orgCode));
bindUserToDevice(watchDevice, dto); bindUserToDevice(watchDevice, dto);
if (StrUtil.isNotBlank(dto.getBindUserId())) { if (StrUtil.isNotBlank(dto.getBindUserId())) {
// 初始化开关数据 // 初始化开关数据
unbindUserAndInitDeviceSwitch(dto); unbindUserAndInitDeviceSwitch(dto);
} }
LoginUser userById = sysBaseAPI.getUserById(dto.getBindUserId());
// 此处修改 只有是职工的是才赋值单位信息 // 此处修改 只有是职工的是才赋值单位信息
if (dto.getIsEmployee()){ if (dto.getIsEmployee()) {
SysDepart inDepart = sysCache.getDepartByOrgCode(userById.getOrgCode()); SysDepart inDepart = sysCache.getDepartByOrgCode(userById.getOrgCode());
dto.setOrgCode(inDepart.getOrgCode()); dto.setOrgCode(inDepart.getOrgCode());
SysDepart thirdDepart = sysCache.getDepartByOrgCode(GlobalUtils.getThirdDepartOrgCode(userById.getOrgCode())); SysDepart thirdDepart = sysCache.getDepartByOrgCode(GlobalUtils.getThirdDepartOrgCode(userById.getOrgCode()));
@@ -350,6 +370,17 @@ public class WatchDeviceServiceImpl extends ServiceImpl<WatchDeviceMapper, Watch
return Result.ok("操作成功"); return Result.ok("操作成功");
} }
@Override
@Transactional(rollbackFor = Exception.class)
public Result bindDevice(String watchNo) throws ParseException {
BindUserDTO dto = new BindUserDTO();
dto.setBindUserId(GlobalUtils.getLoginUser().getId());
dto.setWatchNo(watchNo);
dto.setBindDate(new Date());
dto.setChangeUser(false);
return bindUser(dto);
}
/** /**
* 修改用户拉取数据 * 修改用户拉取数据
* *
@@ -385,7 +416,7 @@ public class WatchDeviceServiceImpl extends ServiceImpl<WatchDeviceMapper, Watch
*/ */
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public Result unbundleUser(BindUserDTO dto) throws ParseException { public Result unbundleUser(BindUserDTO dto){
WatchDevice watchDevice = getDeviceByWatchNo(dto.getWatchNo()); WatchDevice watchDevice = getDeviceByWatchNo(dto.getWatchNo());
if (watchDevice == null) { if (watchDevice == null) {
return Result.error("该设备不存在"); return Result.error("该设备不存在");
@@ -409,6 +440,17 @@ public class WatchDeviceServiceImpl extends ServiceImpl<WatchDeviceMapper, Watch
return Result.ok("操作成功"); return Result.ok("操作成功");
} }
@Override
@Transactional(rollbackFor = Exception.class)
public Result unbundleDevice(String watchNo){
// BindUserDTO dto = new BindUserDTO();
// dto.setBindUserId(GlobalUtils.getLoginUser().getId());
// dto.setWatchNo(watchNo);
// dto.setBindDate(new Date());
// return unbundleUser(dto);
// 2026.04.09 不能从移动端解绑了
return Result.error("请联系管理员解绑");
}
/** /**
* 根据手表编号获取手表信息 * 根据手表编号获取手表信息
@@ -456,6 +498,13 @@ public class WatchDeviceServiceImpl extends ServiceImpl<WatchDeviceMapper, Watch
updateWrapper.eq("id", watchDevice.getId()); updateWrapper.eq("id", watchDevice.getId());
updateWrapper.set("bind_user_id", dto.getBindUserId()); updateWrapper.set("bind_user_id", dto.getBindUserId());
updateWrapper.set("bind_date", watchDevice.getBindDate()); updateWrapper.set("bind_date", watchDevice.getBindDate());
if(StrUtil.isNotBlank(watchDevice.getOrgCode())){
updateWrapper.set("org_code", watchDevice.getOrgCode());
}
if (StrUtil.isNotBlank(watchDevice.getDeptId())){
updateWrapper.set("dept_id", watchDevice.getDeptId());
}
this.update(updateWrapper); this.update(updateWrapper);
} }
@@ -472,6 +521,8 @@ public class WatchDeviceServiceImpl extends ServiceImpl<WatchDeviceMapper, Watch
switchUpdateWrapper.eq(WatchDeviceSwitch::getWatchNo, dto.getWatchNo()); switchUpdateWrapper.eq(WatchDeviceSwitch::getWatchNo, dto.getWatchNo());
watchDeviceSwitchService.update(switchUpdateWrapper); watchDeviceSwitchService.update(switchUpdateWrapper);
String[] wdTypes = {"heart_rate", "spo2", "stress", "temperature"}; String[] wdTypes = {"heart_rate", "spo2", "stress", "temperature"};
// 异步调用外部系统防止外部系统异常导致功能异常解绑这里非必要
asyncTaskExecutor.execute(() -> {
for (int i = 0; i < wdTypes.length; i++) { for (int i = 0; i < wdTypes.length; i++) {
WarnSwitch warnSwitch = new WarnSwitch(); WarnSwitch warnSwitch = new WarnSwitch();
warnSwitch.setWatchNo(dto.getWatchNo()); warnSwitch.setWatchNo(dto.getWatchNo());
@@ -481,6 +532,7 @@ public class WatchDeviceServiceImpl extends ServiceImpl<WatchDeviceMapper, Watch
} }
//通知更新 //通知更新
dataCenterExecutor.cmdUpdateSwitch(dto.getWatchNo()); dataCenterExecutor.cmdUpdateSwitch(dto.getWatchNo());
});
} }
/** /**
@@ -265,6 +265,102 @@ public class WatchMonitorDataServiceImpl extends ServiceImpl<WatchMonitorDataMap
return ajaxJson; return ajaxJson;
} }
/**
* 更新告警事件GPS位置信息
* 根据 watchNo + eventId 联合定位已有告警数据
* watchNo eventId 任一缺失则忽略
* 已存在 GPS 信息则忽略本次更新
* 参考 createMonitorData 进行 GPS84 GCJ02 坐标转换 + 高德地址反查
* 仅更新数据不推送大屏与短信
*
* @param json 入参 watchNoeventIdlonlataddresslocateTimegpsErrorMsg
* @return 处理结果
*/
@Override
public AjaxJson updateMonitorGps(JSONObject json) {
GlobalUtils.setHttpFeignToken();
AjaxJson ajaxJson = new AjaxJson();
logger.info("接收更新告警事件GPS信息=={}", json.toJSONString());
WatchNotifyVo data = json.toJavaObject(WatchNotifyVo.class);
// watchNo eventId 任一缺失则忽略
if (StrUtil.isBlank(data.getWatchNo()) || StrUtil.isBlank(data.getEventId())) {
logger.warn("更新GPS信息缺失关键定位字段,忽略本次请求 watchNo={}, eventId={}", data.getWatchNo(), data.getEventId());
ajaxJson.setSuccess(true);
return ajaxJson;
}
// watchNo + eventId 联合定位已有告警数据
LambdaQueryWrapper<WatchMonitorData> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(WatchMonitorData::getWatchNo, data.getWatchNo())
.eq(WatchMonitorData::getEventId, data.getEventId());
List<WatchMonitorData> existList = this.list(queryWrapper);
if (CollUtil.isEmpty(existList)) {
logger.warn("更新GPS信息失败,未匹配到告警记录 watchNo={}, eventId={}", data.getWatchNo(), data.getEventId());
ajaxJson.setSuccess(true);
return ajaxJson;
}
// 已存在GPS信息则忽略本次更新避免覆盖已有有效坐标
boolean hasGps = existList.stream().anyMatch(m -> m.getLon() != null && m.getLat() != null);
if (hasGps) {
logger.info("告警记录已有GPS信息,忽略本次更新 watchNo={}, eventId={}", data.getWatchNo(), data.getEventId());
ajaxJson.setSuccess(true);
return ajaxJson;
}
// 计算高德坐标与高德地址参考 createMonitorData 逻辑
WatchMonitorData updateEntity = new WatchMonitorData();
updateEntity.setLon(data.getLon());
updateEntity.setLat(data.getLat());
updateEntity.setAddress(data.getAddress());
updateEntity.setGpsErrorMsg(data.getGpsErrorMsg());
updateEntity.setLocateTime(data.getLocateTime());
if (Stream.of(data.getLon(), data.getLat()).allMatch(value -> ObjUtil.isNotEmpty(value) && value != 0)) {
Gps gps = GPS84ToGCJ02(data.getLon(), data.getLat());
if (null != gps) {
// 写入高德坐标
updateEntity.setLonGd(BigDecimal.valueOf(gps.getLongitude()));
updateEntity.setLatGd(BigDecimal.valueOf(gps.getLatitude()));
// 调用高德接口获取地址信息
AmapResultVo amapResultVo = amapService.getFormattedAddress(gps.getLongitude(), gps.getLatitude(), 5000);
if (amapResultVo.isSuccess()) {
updateEntity.setAddressGd(amapResultVo.getAddress());
updateEntity.setAddress(amapResultVo.getAddress());
} else {
String originError = StrUtil.isBlank(updateEntity.getGpsErrorMsg()) ? "" : updateEntity.getGpsErrorMsg() + "";
updateEntity.setGpsErrorMsg(originError + amapResultVo.getErrorMsg());
}
}
}
// 仅更新 GPS 相关字段不推送大屏不发短信
boolean updated = this.lambdaUpdate()
.eq(WatchMonitorData::getWatchNo, data.getWatchNo())
.eq(WatchMonitorData::getEventId, data.getEventId())
.set(WatchMonitorData::getLon, updateEntity.getLon())
.set(WatchMonitorData::getLat, updateEntity.getLat())
.set(WatchMonitorData::getAddress, updateEntity.getAddress())
.set(WatchMonitorData::getLonGd, updateEntity.getLonGd())
.set(WatchMonitorData::getLatGd, updateEntity.getLatGd())
.set(WatchMonitorData::getAddressGd, updateEntity.getAddressGd())
.set(WatchMonitorData::getGpsErrorMsg, updateEntity.getGpsErrorMsg())
.set(WatchMonitorData::getLocateTime, updateEntity.getLocateTime())
.update();
if (!updated) {
logger.warn("更新GPS信息失败 watchNo={}, eventId={}", data.getWatchNo(), data.getEventId());
} else {
logger.info("更新GPS信息成功 watchNo={}, eventId={}, 共更新{}条", data.getWatchNo(), data.getEventId(), existList.size());
}
ajaxJson.setSuccess(true);
return ajaxJson;
}
/** /**
* 构建异常数据 * 构建异常数据
* *
@@ -272,13 +368,16 @@ public class WatchMonitorDataServiceImpl extends ServiceImpl<WatchMonitorDataMap
* @return 异常数据 * @return 异常数据
*/ */
private WatchMonitorData createMonitorData(WatchNotifyVo data) { private WatchMonitorData createMonitorData(WatchNotifyVo data) {
log.info("开始构建异常数据 => {}", JSONObject.toJSONString(data)); log.info("开始构建异常数据 => {}", JSONObject.toJSONString(data));
WatchMonitorData monitorDataEntity = new WatchMonitorData(); WatchMonitorData monitorDataEntity = new WatchMonitorData();
monitorDataEntity.setAddress(data.getAddress()); monitorDataEntity.setAddress(data.getAddress());
monitorDataEntity.setWatchNo(data.getWatchNo()); monitorDataEntity.setWatchNo(data.getWatchNo());
monitorDataEntity.setLon(data.getLon()); monitorDataEntity.setLon(data.getLon());
monitorDataEntity.setLat(data.getLat()); monitorDataEntity.setLat(data.getLat());
monitorDataEntity.setLocateTime(data.getLocateTime());
monitorDataEntity.setEventType(data.getEventType()); monitorDataEntity.setEventType(data.getEventType());
monitorDataEntity.setEventId(data.getEventId());
monitorDataEntity.setDataValue(data.getDataValue()); monitorDataEntity.setDataValue(data.getDataValue());
monitorDataEntity.setCreateDate(data.getCreateTime()); monitorDataEntity.setCreateDate(data.getCreateTime());
monitorDataEntity.setWarnTime(data.getWarnTime()); monitorDataEntity.setWarnTime(data.getWarnTime());
@@ -721,4 +721,9 @@ public interface CommonConstant {
* 多选 * 多选
*/ */
String PD = "PD"; String PD = "PD";
/**
* url加解密密钥AES
*/
String URL_SECRET_KEY = "a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5";
} }
@@ -22,6 +22,7 @@ import org.apache.http.impl.client.HttpClients;
import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.ExceptionAssertsUtil; import org.jeecg.common.exception.ExceptionAssertsUtil;
import org.jeecg.common.util.filter.StrAttackFilter; import org.jeecg.common.util.filter.StrAttackFilter;
import org.jeecg.util.GlobalManager;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
@@ -51,7 +52,9 @@ public class MyUploadUtil {
private static String muFileListUploadPath; private static String muFileListUploadPath;
private static String muDownPath; private static String muDownPath;
private static String fileTypeWhiteList; private static String fileTypeWhiteList;
// private static final int timeout = 10000; /** 全局配置管理器,用于读取 Redis 中的动态配置 */
private static GlobalManager globalManager;
// private static final int timeout = 10000;
//有些大文件可能需要比较长的时间这里改成5分钟 //有些大文件可能需要比较长的时间这里改成5分钟
private static final int socketTimeout = 10*60*1000; private static final int socketTimeout = 10*60*1000;
private static final int connectTimeout = 60*1000; private static final int connectTimeout = 60*1000;
@@ -108,6 +111,10 @@ public class MyUploadUtil {
MyUploadUtil.fileTypeWhiteList = fileTypeWhiteList; MyUploadUtil.fileTypeWhiteList = fileTypeWhiteList;
} }
public static void setGlobalManager(GlobalManager globalManager) {
MyUploadUtil.globalManager = globalManager;
}
/** /**
* 上传文件 * 上传文件
* *
@@ -116,43 +123,52 @@ public class MyUploadUtil {
*/ */
public static String upload(MultipartFile file, String bizPath) throws Exception { public static String upload(MultipartFile file, String bizPath) throws Exception {
String fileUrl = "";
String fileName = file.getOriginalFilename();// 获取文件名 String fileName = file.getOriginalFilename();// 获取文件名
checkFileLegitimacy(fileName); checkFileLegitimacy(fileName);
//???先屏蔽了
// if (StringUtils.hasText(file.getOriginalFilename())) {
// String originalFilename = file.getOriginalFilename();
// String ext = originalFilename.substring(originalFilename.lastIndexOf("."));
// fileName = fileName + ext;
// }
bizPath = StrAttackFilter.filter(bizPath); bizPath = StrAttackFilter.filter(bizPath);
// .tar 文件固定走 MyUpload
if (fileName != null && (fileName.endsWith(".tar") || fileName.endsWith(".apk"))) {
log.info("上传文件为tar/apk文件,使用MyUploadUtil上传");
try { try {
fileUrl = upload(file.getInputStream(), bizPath, fileName); return upload(file.getInputStream(), bizPath, fileName,true);
} catch (Exception e) { } catch (Exception e) {
fileUrl = null; log.error("upload error {}", e.getMessage(), e);
log.error("upload error", e); return null;
}
}
// 其他文件oss_flag=1 XjOssUtil否则走 MyUpload
String ossFlag = globalManager != null ? globalManager.getConfigValueByKey("oss_flag") : "";
if ("1".equals(ossFlag)) {
log.info("开启新疆OSS上传");
return XjOssUtil.upload(file, bizPath);
}
log.info("使用MyUploadUtil上传");
try {
return upload(file.getInputStream(), bizPath, fileName);
} catch (Exception e) {
log.error("upload error {}", e.getMessage(), e);
return null;
} }
return fileUrl;
} }
/** // /**
* 上传文件 带缩略图 // * 上传文件 带缩略图
* // *
* @param file // * @param file
* @return // * @return
*/ // */
public static String uploadImg(MultipartFile file, String bizPath, Boolean needThumbnailFlag) throws Exception { // public static String uploadImg(MultipartFile file, String bizPath, Boolean needThumbnailFlag) throws Exception {
String fileUrl = ""; // String fileUrl = "";
String fileName = file.getOriginalFilename();// 获取文件名 // String fileName = file.getOriginalFilename();// 获取文件名
checkFileLegitimacy(fileName); // checkFileLegitimacy(fileName);
bizPath = StrAttackFilter.filter(bizPath); // bizPath = StrAttackFilter.filter(bizPath);
try { // try {
fileUrl = uploadImg(file.getInputStream(), bizPath, fileName, needThumbnailFlag); // fileUrl = uploadImg(file.getInputStream(), bizPath, fileName, needThumbnailFlag);
} catch (Exception e) { // } catch (Exception e) {
log.error("upload error", e); // log.error("upload error", e);
} // }
return fileUrl; // return fileUrl;
} // }
public static String upload(InputStream stream, String bizPath, String fileName, boolean check) throws Exception { public static String upload(InputStream stream, String bizPath, String fileName, boolean check) throws Exception {
if (check) { if (check) {
@@ -220,145 +236,152 @@ public class MyUploadUtil {
* @return * @return
*/ */
public static String upload(InputStream stream, String bizPath, String fileName) throws Exception { public static String upload(InputStream stream, String bizPath, String fileName) throws Exception {
// 其他文件oss_flag=1 XjOssUtil否则走 MyUpload
String ossFlag = globalManager != null ? globalManager.getConfigValueByKey("oss_flag") : "";
if ("1".equals(ossFlag)) {
log.info("开启新疆OSS上传");
return XjOssUtil.upload(stream, bizPath,fileName);
}
log.info("使用MyUploadUtil上传");
return upload(stream, bizPath, fileName, true); return upload(stream, bizPath, fileName, true);
} }
/** // /**
* 上传文件到myupload // * 上传文件到myupload
* // *
* @param stream // * @param stream
* @return // * @return
*/ // */
public static String uploadImg(InputStream stream, String bizPath, String fileName, Boolean needThumbnailFlag) throws Exception { // public static String uploadImg(InputStream stream, String bizPath, String fileName, Boolean needThumbnailFlag) throws Exception {
//避免重复文件覆盖这里处理一下 // //避免重复文件覆盖这里处理一下
if (fileName.indexOf(".") > -1) { // if (fileName.indexOf(".") > -1) {
//String fileNameLeft = fileName.substring(0, fileName.lastIndexOf(".")); // //String fileNameLeft = fileName.substring(0, fileName.lastIndexOf("."));
String fileNameLeft = UUID.randomUUID().toString(); // String fileNameLeft = UUID.randomUUID().toString();
fileName = fileNameLeft + "_" + System.currentTimeMillis() + fileName.substring(fileName.lastIndexOf(".")); // fileName = fileNameLeft + "_" + System.currentTimeMillis() + fileName.substring(fileName.lastIndexOf("."));
} else { // } else {
fileName += "_" + System.currentTimeMillis(); // fileName += "_" + System.currentTimeMillis();
} // }
String nowday = new SimpleDateFormat("yyyyMMdd").format(new Date()); // String nowday = new SimpleDateFormat("yyyyMMdd").format(new Date());
String path = bizPath + File.separator + nowday; // String path = bizPath + File.separator + nowday;
String url = muUrl + "/" + muFileUploadPath; // String url = muUrl + "/" + muFileUploadPath;
String fileUrl = ""; // String fileUrl = "";
//创建post方法连接实例在post方法中传入待连接地址 // //创建post方法连接实例在post方法中传入待连接地址
CloseableHttpClient httpClient = HttpClients.createDefault(); // CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse httpResponse = null; // CloseableHttpResponse httpResponse = null;
//
// byte[] bytes = IoUtil.readBytes(stream);
//
// try {
// URI uri = new URIBuilder(url)
// .addParameter("fileName", fileName)
// .addParameter("path", path)
// .build();
// HttpPost httppost = new HttpPost(uri);
// RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout).build();
// httppost.setConfig(requestConfig);
// httppost.addHeader("token", muToken);
// HttpEntity reqEntity = MultipartEntityBuilder.create()
// //设置浏览器兼容模式
// .setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
// //设置请求的编码格式
// .setCharset(StandardCharsets.UTF_8)
// .addBinaryBody("file", stream, ContentType.APPLICATION_OCTET_STREAM, fileName)
// .build();
// httppost.setEntity(reqEntity);
// httppost.setEntity(reqEntity);
//
// httpResponse = httpClient.execute(httppost);
//
//
// int backCode = httpResponse.getStatusLine().getStatusCode();
// if (backCode != HttpStatus.SC_OK) {
// log.error("上传文件失败" + backCode);
// } else {
// fileUrl = path + File.separator + fileName;
// fileUrl = relaceSlash(fileUrl);
// }
// } catch (IOException e) {
// log.error("文件服务器连接失败", e);
// } finally {
// //释放资源
// try {
// httpClient.close();
// if (httpResponse != null) {
// httpResponse.close();
// }
// } catch (IOException e) {
//// e.printStackTrace();
// }
// }
//
// if (needThumbnailFlag) {
// uploadThumbnail(url, bytes, fileName, path);
// }
//
// return fileUrl;
// }
//
// /**
// * 上传缩略图到myupload
// *
// * @return
// */
// private static void uploadThumbnail(String url, byte[] bytes, String fileName, String path) {
// //创建post方法连接实例在post方法中传入待连接地址
// CloseableHttpClient httpClient = HttpClients.createDefault();
// CloseableHttpResponse httpResponse = null;
// try {
// String thumbnailName = CommonConstant.THUMBNAIL_PREFIX + fileName;
//
// BufferedImage bufferedImage = Thumbnails.of(new ByteArrayInputStream(bytes))
// .scale(CommonConstant.THUMBNAIL_SCALE)
// .asBufferedImage();
//
// ByteArrayOutputStream os = new ByteArrayOutputStream();
// ImageIO.write(bufferedImage, "png", os);
// InputStream is = new ByteArrayInputStream(os.toByteArray());
// URI uri = new URIBuilder(url)
// .addParameter("fileName", thumbnailName)
// .addParameter("path", path)
// .build();
// HttpPost httppost = new HttpPost(uri);
// RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout).build();
// httppost.setConfig(requestConfig);
// httppost.addHeader("token", muToken);
// HttpEntity reqEntity = MultipartEntityBuilder.create()
// //设置浏览器兼容模式
// .setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
// //设置请求的编码格式
// .setCharset(Consts.UTF_8)
//
// .addBinaryBody("file", is, ContentType.APPLICATION_OCTET_STREAM, thumbnailName)
// .build();
// httppost.setEntity(reqEntity);
//
// httpResponse = httpClient.execute(httppost);
//
// int backCode = httpResponse.getStatusLine().getStatusCode();
// if (backCode != HttpStatus.SC_OK) {
// log.error("上传缩略图失败" + backCode);
// }
// } catch (IOException e) {
// log.error("文件服务器连接失败", e);
// } catch (URISyntaxException e) {
// throw new RuntimeException(e);
// } finally {
// //释放资源
// try {
// httpClient.close();
// if (httpResponse != null) {
// httpResponse.close();
// }
// } catch (IOException e) {
// }
// }
// }
byte[] bytes = IoUtil.readBytes(stream); // public String getMuFileExistPath() {
// return muFileExistPath;
try { // }
URI uri = new URIBuilder(url)
.addParameter("fileName", fileName)
.addParameter("path", path)
.build();
HttpPost httppost = new HttpPost(uri);
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout).build();
httppost.setConfig(requestConfig);
httppost.addHeader("token", muToken);
HttpEntity reqEntity = MultipartEntityBuilder.create()
//设置浏览器兼容模式
.setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
//设置请求的编码格式
.setCharset(StandardCharsets.UTF_8)
.addBinaryBody("file", stream, ContentType.APPLICATION_OCTET_STREAM, fileName)
.build();
httppost.setEntity(reqEntity);
httppost.setEntity(reqEntity);
httpResponse = httpClient.execute(httppost);
int backCode = httpResponse.getStatusLine().getStatusCode();
if (backCode != HttpStatus.SC_OK) {
log.error("上传文件失败" + backCode);
} else {
fileUrl = path + File.separator + fileName;
fileUrl = relaceSlash(fileUrl);
}
} catch (IOException e) {
log.error("文件服务器连接失败", e);
} finally {
//释放资源
try {
httpClient.close();
if (httpResponse != null) {
httpResponse.close();
}
} catch (IOException e) {
// e.printStackTrace();
}
}
if (needThumbnailFlag) {
uploadThumbnail(url, bytes, fileName, path);
}
return fileUrl;
}
/**
* 上传缩略图到myupload
*
* @return
*/
private static void uploadThumbnail(String url, byte[] bytes, String fileName, String path) {
//创建post方法连接实例在post方法中传入待连接地址
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse httpResponse = null;
try {
String thumbnailName = CommonConstant.THUMBNAIL_PREFIX + fileName;
BufferedImage bufferedImage = Thumbnails.of(new ByteArrayInputStream(bytes))
.scale(CommonConstant.THUMBNAIL_SCALE)
.asBufferedImage();
ByteArrayOutputStream os = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, "png", os);
InputStream is = new ByteArrayInputStream(os.toByteArray());
URI uri = new URIBuilder(url)
.addParameter("fileName", thumbnailName)
.addParameter("path", path)
.build();
HttpPost httppost = new HttpPost(uri);
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout).build();
httppost.setConfig(requestConfig);
httppost.addHeader("token", muToken);
HttpEntity reqEntity = MultipartEntityBuilder.create()
//设置浏览器兼容模式
.setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
//设置请求的编码格式
.setCharset(Consts.UTF_8)
.addBinaryBody("file", is, ContentType.APPLICATION_OCTET_STREAM, thumbnailName)
.build();
httppost.setEntity(reqEntity);
httpResponse = httpClient.execute(httppost);
int backCode = httpResponse.getStatusLine().getStatusCode();
if (backCode != HttpStatus.SC_OK) {
log.error("上传缩略图失败" + backCode);
}
} catch (IOException e) {
log.error("文件服务器连接失败", e);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
} finally {
//释放资源
try {
httpClient.close();
if (httpResponse != null) {
httpResponse.close();
}
} catch (IOException e) {
}
}
}
public String getMuFileExistPath() {
return muFileExistPath;
}
public static void setMuFileExistPath(String muFileExistPath) { public static void setMuFileExistPath(String muFileExistPath) {
MyUploadUtil.muFileExistPath = muFileExistPath; MyUploadUtil.muFileExistPath = muFileExistPath;
@@ -0,0 +1,542 @@
package org.jeecg.common.util;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.Header;
import org.apache.http.HttpStatus;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.jeecg.common.exception.ExceptionAssertsUtil;
import org.jeecg.common.util.filter.StrAttackFilter;
import org.jeecg.util.RedisClientUtil;
import org.jeecg.util.RedisKeyPrefix;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
/**
* 新疆油田平台IOSPOSS 文件工具类
* <p>
* 功能
* 1. OAuth2 client_credentials 鉴权token 缓存到 Redis401 时自动重新鉴权
* 2. 文件上传支持 InputStream MultipartFile
* 3. 文件下载通过 fileKey 获取文件流处理 302 重定向
* </p>
* <p>
* 配置项Nacos 中配置前缀 xj.oss
* - gateway-url 网关地址 http://api.iosp.ydpt.tech
* - client-id OAuth2 客户端ID
* - client-secret OAuth2 客户端密钥
* - organization-id 组织ID上传/下载路径参数
* - config-code 配置Code上传/下载路径参数
* - base-url-prefix 文件基础路径前缀可选用于截取 Location 中的相对路径
* </p>
*/
@Slf4j
public class XjOssUtil {
// ===================== 静态配置字段 XjOssConfig 注入 =====================
/** 网关地址,如 http://api.iosp.ydpt.tech */
private static String gatewayUrl;
/** OAuth2 客户端ID */
private static String clientId;
/** OAuth2 客户端密钥 */
private static String clientSecret;
/** 组织ID */
private static String organizationId;
/** 配置Code */
private static String configCode;
/** 文件基础路径前缀,如 http://11.71.10.44:8060/wbwj/scyx/scfz/aygc/jkgl/0 */
private static String baseUrlPrefix;
/** 允许上传的文件类型白名单,逗号分隔,如 jpg,png,pdf */
private static String fileTypeWhiteList;
/** 授权上传目录,如 scyx/scfz/aygc/jkgl/ */
private static String directory;
// ===================== Redis 工具 XjOssConfig 注入 =====================
private static RedisClientUtil redisClientUtil;
// ===================== 常量 =====================
/** Redis key 前缀(不含过期时间,仅用于 delete/get 操作) */
private static final RedisKeyPrefix REDIS_PREFIX_NO_TTL = new RedisKeyPrefix("xj:oss");
/** Redis 中存储 token 的 key */
private static final String TOKEN_KEY = "access_token";
/** token 过期时间预留缓冲(秒),提前刷新避免临界过期 */
private static final int TOKEN_EXPIRE_BUFFER = 60;
/** HTTP 连接超时(毫秒) */
private static final int CONNECT_TIMEOUT = 30_000;
/** HTTP Socket 超时(毫秒),大文件上传需要较长时间 */
private static final int SOCKET_TIMEOUT = 10 * 60_000;
private static final String DIR = "/jkgl/";
// ===================== Setter XjOssConfig 注入 =====================
public static void setGatewayUrl(String gatewayUrl) {
XjOssUtil.gatewayUrl = gatewayUrl;
}
public static void setClientId(String clientId) {
XjOssUtil.clientId = clientId;
}
public static void setClientSecret(String clientSecret) {
XjOssUtil.clientSecret = clientSecret;
}
public static void setOrganizationId(String organizationId) {
XjOssUtil.organizationId = organizationId;
}
public static void setConfigCode(String configCode) {
XjOssUtil.configCode = configCode;
}
public static void setBaseUrlPrefix(String baseUrlPrefix) {
XjOssUtil.baseUrlPrefix = baseUrlPrefix;
}
public static void setFileTypeWhiteList(String fileTypeWhiteList) {
XjOssUtil.fileTypeWhiteList = fileTypeWhiteList;
}
public static void setDirectory(String directory) {
XjOssUtil.directory = directory;
}
public static void setRedisClientUtil(RedisClientUtil redisClientUtil) {
XjOssUtil.redisClientUtil = redisClientUtil;
}
// ===================== 鉴权 =====================
/**
* IOSP 网关发起 OAuth2 鉴权获取 access_token 并存入 Redis
* <p>
* 接口POST {gatewayUrl}/oauth/oauth/token
* 参数grant_type=client_credentials, client_id, client_secret
* </p>
*
* @return 获取到的 access_token失败返回 null
*/
public static String authenticate() {
String url = gatewayUrl + "/oauth/oauth/token";
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
try {
HttpPost post = new HttpPost(url);
post.setConfig(buildRequestConfig());
// 使用 multipart/form-data 提交鉴权参数
post.setEntity(MultipartEntityBuilder.create()
.setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
.setCharset(StandardCharsets.UTF_8)
.addTextBody("grant_type", "client_credentials")
.addTextBody("client_id", clientId)
.addTextBody("client_secret", clientSecret)
.build());
response = httpClient.execute(post);
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
log.error("[XjOssUtil] 鉴权失败,HTTP状态码: {}", statusCode);
return null;
}
String body = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
JSONObject json = JSON.parseObject(body);
String token = json.getString("access_token");
// expires_in 单位为秒减去缓冲时间后存入 Redis
int expiresIn = json.getIntValue("expires_in");
int ttl = Math.max(expiresIn - TOKEN_EXPIRE_BUFFER, 30);
// 存入 RedisTTL = expires_in - 缓冲动态创建带 TTL RedisKeyPrefix
RedisKeyPrefix prefixWithTtl = new RedisKeyPrefix(ttl, "xj:oss");
redisClientUtil.set(prefixWithTtl, TOKEN_KEY, token);
log.info("[XjOssUtil] 鉴权成功,token 有效期 {}s(含缓冲)", ttl);
return token;
} catch (Exception e) {
log.error("[XjOssUtil] 鉴权异常", e);
return null;
} finally {
closeQuietly(httpClient, response);
}
}
/**
* 获取有效的 access_token
* <p>
* 优先从 Redis 读取缓存不存在时自动重新鉴权
* </p>
*
* @return 有效的 access_token失败返回 null
*/
public static String getToken() {
String token = redisClientUtil.get(REDIS_PREFIX_NO_TTL, TOKEN_KEY, String.class);
if (token == null) {
log.info("[XjOssUtil] Redis 中 token 不存在或已过期,重新鉴权");
token = authenticate();
}
return token;
}
// ===================== 文件上传 =====================
/**
* 上传文件MultipartFile 方式
*
* @param file 前端上传的文件
* @param bizPath 业务子路径 importtemp拼接在授权目录之后
* @return 上传成功后的文件 URL失败返回 null
*/
public static String upload(MultipartFile file, String bizPath) {
try {
return upload(file.getInputStream(), bizPath, file.getOriginalFilename());
} catch (Exception e) {
log.error("[XjOssUtil] MultipartFile 上传失败", e);
return null;
}
}
/**
* 上传文件InputStream 方式
*
* @param stream 文件输入流
* @param bizPath 业务子路径 importtemp拼接在授权目录之后
* @param fileName 文件名 test.png
* @return 上传成功后的文件 URL失败返回 null
*/
public static String upload(InputStream stream, String bizPath, String fileName) {
checkFileLegitimacy(fileName);
// 过滤上传文件夹名特殊字符防止路径攻击
bizPath = StrAttackFilter.filter(bizPath);
// 构造上传 URL/hfle/v2/{organizationId}/files/{configCode}/multipart
String url = gatewayUrl + "/hfle/v2/" + organizationId + "/files/" + configCode + "/multipart";
// 避免重复文件覆盖追加时间戳
if (fileName.contains(".")) {
String fileNameLeft = fileName.substring(0, fileName.lastIndexOf("."));
fileName = fileNameLeft + "_" + System.currentTimeMillis() + fileName.substring(fileName.lastIndexOf("."));
} else {
fileName += "_" + System.currentTimeMillis();
}
String nowday = new SimpleDateFormat("yyyyMMdd").format(new Date());
// 最终 fileName = bizPath/yyyyMMdd/originalName_timestamp.ext
final String finalFileName = DIR + bizPath + "/" + nowday + "/" + fileName;
return doUpload(stream, url, finalFileName);
}
/**
* 执行实际上传请求401 时刷新 token 重试一次
* <p>fileName 为已处理好的完整路径不再做二次处理</p>
*/
private static String doUpload(InputStream stream, String url, String fileName) {
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
try {
URI uri = new URIBuilder(url)
.addParameter("directory", directory)
.addParameter("fileName", fileName)
.addParameter("docType", "0")
.build();
HttpPost post = new HttpPost(uri);
post.setConfig(buildRequestConfig());
post.addHeader("Authorization", "Bearer " + getToken());
post.addHeader("accept", "application/json;charset=utf-8");
post.setEntity(MultipartEntityBuilder.create()
.setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
.setCharset(StandardCharsets.UTF_8)
.addBinaryBody("file", stream, ContentType.APPLICATION_OCTET_STREAM, fileName)
.build());
response = httpClient.execute(post);
int statusCode = response.getStatusLine().getStatusCode();
// 401 表示 token 过期清除缓存后重试一次fileName 已处理不会重复拼接
if (statusCode == HttpStatus.SC_UNAUTHORIZED) {
log.warn("[XjOssUtil] 上传时 token 过期,重新鉴权后重试");
closeQuietly(httpClient, response);
redisClientUtil.delete(REDIS_PREFIX_NO_TTL, TOKEN_KEY);
return doUpload(stream, url, fileName);
}
if (statusCode != HttpStatus.SC_OK) {
log.error("[XjOssUtil] 文件上传失败,HTTP状态码: {}", statusCode);
return null;
}
String fileUrl = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
// 接口可能返回 HTTP 200 body 为业务错误 JSON需单独判断
JSONObject resultJson = null;
try {
resultJson = JSON.parseObject(fileUrl);
} catch (Exception ignored) {
}
if (resultJson != null && Boolean.TRUE.equals(resultJson.getBoolean("failed"))) {
log.error("[XjOssUtil] 文件上传业务失败: {}", fileUrl);
return null;
}
String fileKey = extractFileKey(fileUrl);
log.info("[XjOssUtil] 文件上传成功,fileKey: {}", fileKey);
return fileKey;
} catch (Exception e) {
log.error("[XjOssUtil] 文件上传异常", e);
return null;
} finally {
closeQuietly(httpClient, response);
}
}
// ===================== 文件下载 =====================
/**
* 下载文件返回文件输入流
* <p>
* 入参为短 fileKey 808fec572eda4428a84c8b4c4f07f511@aa.png
* 内部自动拼接完整路径{directory}0/{fileKey}
* </p>
*
* @param fileKey 短文件 key 808fec572eda4428a84c8b4c4f07f511@aa.png
* @return 文件输入流调用方负责关闭失败返回 null
*/
public static InputStream down(String fileKey) {
String locationUrl = getLocationUrl(buildFullFileKey(fileKey));
if (locationUrl == null) {
return null;
}
CloseableHttpClient downloadClient = HttpClients.createDefault();
try {
HttpGet downloadGet = new HttpGet(locationUrl);
downloadGet.setConfig(buildRequestConfig());
CloseableHttpResponse downloadResponse = downloadClient.execute(downloadGet);
int downloadStatus = downloadResponse.getStatusLine().getStatusCode();
if (downloadStatus != HttpStatus.SC_OK) {
log.error("[XjOssUtil] 获取文件流失败,HTTP状态码: {}", downloadStatus);
closeQuietly(downloadClient, downloadResponse);
return null;
}
// 返回包装流关闭时自动释放 HTTP 资源
return new HttpEntityInputStream(downloadResponse.getEntity().getContent(), downloadClient, downloadResponse);
} catch (Exception e) {
log.error("[XjOssUtil] 文件下载异常", e);
closeQuietly(downloadClient, null);
return null;
}
}
/**
* 获取文件预览地址Location 重定向 URL
* <p>
* 入参为短 fileKey 808fec572eda4428a84c8b4c4f07f511@aa.png
* 内部自动拼接完整路径{directory}0/{fileKey}
* 返回的 URL 可直接用于浏览器预览或前端展示
* </p>
*
* @param fileKey 短文件 key 808fec572eda4428a84c8b4c4f07f511@aa.png
* @return 预览 URL失败返回 null
*/
public static String show(String fileKey) {
return getLocationUrl(buildFullFileKey(fileKey));
}
/**
* 将短 fileKey 拼接为下载接口所需的完整路径
* <p>规则{directory}0/{fileKey}示例scyx/scfz/aygc/jkgl/0/808fec...@aa.png</p>
*/
private static String buildFullFileKey(String fileKey) {
String dir = directory.endsWith("/") ? directory : directory + "/";
return dir + "0/" + fileKey;
}
/**
* 调用 download-by-key 接口获取 302 重定向的 Location URL
* <p>401 时自动刷新 token 重试一次</p>
*
* @param fullFileKey 完整 fileKey scyx/scfz/aygc/jkgl/0/808fec...@aa.png
* @return Location URL失败返回 null
*/
private static String getLocationUrl(String fullFileKey) {
String url = gatewayUrl + "/hfle/v2/" + organizationId + "/files/" + configCode + "/download-by-key";
CloseableHttpClient noRedirectClient = org.apache.http.impl.client.HttpClientBuilder.create()
.disableRedirectHandling()
.build();
CloseableHttpResponse response = null;
try {
URI uri = new URIBuilder(url)
.addParameter("fileKey", fullFileKey)
.build();
HttpGet get = new HttpGet(uri);
get.setConfig(buildRequestConfig());
String token = getToken();
get.addHeader("Authorization", "Bearer " + token);
// 打印等效 curl 命令便于调试
log.info("[XjOssUtil] curl --request GET \\\n --url '{}' \\\n --header 'Authorization: Bearer {}'",
uri.toString(), token);
response = noRedirectClient.execute(get);
int statusCode = response.getStatusLine().getStatusCode();
// 401 表示 token 过期重新鉴权后重试一次
if (statusCode == HttpStatus.SC_UNAUTHORIZED) {
log.warn("[XjOssUtil] 获取 Location 时 token 过期,重新鉴权后重试");
closeQuietly(noRedirectClient, response);
redisClientUtil.delete(REDIS_PREFIX_NO_TTL, TOKEN_KEY);
return getLocationUrl(fullFileKey);
}
if (statusCode != HttpStatus.SC_MOVED_TEMPORARILY && statusCode != HttpStatus.SC_SEE_OTHER) {
log.error("[XjOssUtil] download-by-key 返回非预期状态码: {}", statusCode);
return null;
}
Header locationHeader = response.getFirstHeader("Location");
if (locationHeader == null) {
log.error("[XjOssUtil] download-by-key 未返回 Location 响应头");
return null;
}
String locationUrl = locationHeader.getValue();
log.info("[XjOssUtil] 获取到 Location: {}", locationUrl);
return locationUrl;
} catch (Exception e) {
log.error("[XjOssUtil] 获取 Location 异常", e);
return null;
} finally {
closeQuietly(noRedirectClient, response);
}
}
// ===================== 私有工具方法 =====================
/**
* 文件合法性校验文件类型白名单
* <p>
* 白名单为空时跳过校验文件名无扩展名或扩展名不在白名单中时抛出异常
* </p>
*
* @param fileName 文件名
*/
private static void checkFileLegitimacy(String fileName) {
if (StrUtil.isNotEmpty(fileTypeWhiteList)) {
String extName = FileNameUtil.extName(fileName);
if (StrUtil.isBlank(extName)) {
ExceptionAssertsUtil.fail("文件上传失败:不能识别的文件类型!");
}
List<String> fileTypeList = Arrays.asList(fileTypeWhiteList.split(","));
// 统一转小写比对忽略大小写差异
if (!fileTypeList.contains(extName.toLowerCase())) {
ExceptionAssertsUtil.fail("文件上传失败:不支持的文件类型!");
}
}
}
/**
* 从上传接口返回的完整 URL 中截取 fileKey
* <p>
* 返回示例https://wbwj.http://11.71.10.44:8060/scyx/.../0/808fec572eda4428a84c8b4c4f07f511@aa.png
* 截取结果808fec572eda4428a84c8b4c4f07f511@aa.png
* 规则 @ 符号前最后一个 / 之后的所有内容
* </p>
*
* @param fileUrl 接口返回的完整 URL
* @return fileKey解析失败时返回原始 URL
*/
private static String extractFileKey(String fileUrl) {
int atIndex = fileUrl.indexOf("@");
if (atIndex < 0) {
return fileUrl;
}
int slashIndex = fileUrl.lastIndexOf("/", atIndex);
if (slashIndex < 0) {
return fileUrl;
}
return fileUrl.substring(slashIndex + 1);
}
/**
* 构建统一的 HTTP 请求配置
*/
private static RequestConfig buildRequestConfig() {
return RequestConfig.custom()
.setConnectTimeout(CONNECT_TIMEOUT)
.setSocketTimeout(SOCKET_TIMEOUT)
.build();
}
/**
* 静默关闭 HTTP 资源忽略异常
*/
private static void closeQuietly(CloseableHttpClient client, CloseableHttpResponse response) {
try {
if (response != null) {
response.close();
}
} catch (IOException ignored) {
}
try {
if (client != null) {
client.close();
}
} catch (IOException ignored) {
}
}
// ===================== 内部类包装 InputStream关闭时释放 HTTP 资源 =====================
/**
* 包装 HTTP 响应的 InputStream关闭时自动释放 HTTP 连接资源
*/
private static class HttpEntityInputStream extends InputStream {
private final InputStream delegate;
private final CloseableHttpClient httpClient;
private final CloseableHttpResponse httpResponse;
HttpEntityInputStream(InputStream delegate, CloseableHttpClient httpClient, CloseableHttpResponse httpResponse) {
this.delegate = delegate;
this.httpClient = httpClient;
this.httpResponse = httpResponse;
}
@Override
public int read() throws IOException {
return delegate.read();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return delegate.read(b, off, len);
}
@Override
public void close() throws IOException {
try {
delegate.close();
} finally {
closeQuietly(httpClient, httpResponse);
}
}
}
}
@@ -2,6 +2,8 @@ package org.jeecg.config.oss;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.MyUploadUtil; import org.jeecg.common.util.MyUploadUtil;
import org.jeecg.util.GlobalManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -29,6 +31,9 @@ public class MyUploadConfig {
@Value(value = "${jeecg.myupload.mu_file_type_white_list}") @Value(value = "${jeecg.myupload.mu_file_type_white_list}")
private String fileTypeWhiteList; private String fileTypeWhiteList;
@Autowired
private GlobalManager globalManager;
@Bean @Bean
public void initMu() { public void initMu() {
MyUploadUtil.setMuUrl(muUrl); MyUploadUtil.setMuUrl(muUrl);
@@ -39,6 +44,7 @@ public class MyUploadConfig {
MyUploadUtil.setMuFileListUploadPath(muFileListUploadPath); MyUploadUtil.setMuFileListUploadPath(muFileListUploadPath);
MyUploadUtil.setMuDownPath(muDownPath); MyUploadUtil.setMuDownPath(muDownPath);
MyUploadUtil.setFileTypeWhiteList(fileTypeWhiteList); MyUploadUtil.setFileTypeWhiteList(fileTypeWhiteList);
MyUploadUtil.setGlobalManager(globalManager);
} }
} }
@@ -0,0 +1,84 @@
package org.jeecg.config.oss;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.XjOssUtil;
import org.jeecg.util.RedisClientUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 新疆油田平台IOSPOSS 配置类
* <p>
* Nacos 读取配置并注入到 XjOssUtil 静态字段中
* 对应 Nacos 配置前缀xj.oss
* </p>
*/
@Slf4j
@Configuration
public class XjOssConfig {
/** 网关地址,如 http://api.iosp.ydpt.tech */
@Value(value = "${variable.anyan.host}")
private String gatewayUrl;
/** OAuth2 客户端ID */
@Value(value = "${variable.anyan.client-id}")
private String clientId;
/** OAuth2 客户端密钥 */
@Value(value = "${variable.anyan.client-secret}")
private String clientSecret;
/** 组织ID(上传/下载路径参数) */
@Value(value = "${variable.anyan.organization-id}")
private String organizationId;
/** 配置Code(上传/下载路径参数) */
@Value(value = "${variable.anyan.config-code}")
private String configCode;
/**
* 文件基础路径前缀可选
* 示例http://11.71.10.44:8060/wbwj/scyx/scfz/aygc/jkgl/0
* 用于从完整 URL 中截取相对 fileKey
*/
@Value(value = "${variable.anyan.base-url-prefix:}")
private String baseUrlPrefix;
/**
* 允许上传的文件类型白名单逗号分隔可选
* 示例jpg,jpeg,png,pdf,doc,docx
* 为空时不做类型限制
*/
@Value(value = "${variable.anyan.file-type-white-list:}")
private String fileTypeWhiteList;
/**
* 授权上传目录需与 IOSP 平台授权的目录一致
* 示例scyx/scfz/aygc/jkgl/
*/
@Value(value = "${variable.anyan.directory}")
private String directory;
@Autowired
private RedisClientUtil redisClientUtil;
/**
* 初始化 XjOssUtil 静态配置
*/
@Bean
public void initXjOss() {
XjOssUtil.setGatewayUrl(gatewayUrl);
XjOssUtil.setClientId(clientId);
XjOssUtil.setClientSecret(clientSecret);
XjOssUtil.setOrganizationId(organizationId);
XjOssUtil.setConfigCode(configCode);
XjOssUtil.setBaseUrlPrefix(baseUrlPrefix);
XjOssUtil.setFileTypeWhiteList(fileTypeWhiteList);
XjOssUtil.setDirectory(directory);
XjOssUtil.setRedisClientUtil(redisClientUtil);
log.info("[XjOssConfig] IOSP OSS 配置初始化完成,网关地址: {}", gatewayUrl);
}
}
@@ -85,6 +85,7 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/api/qimo/callback", "anon"); // 云坐席推送不鉴权 filterChainDefinitionMap.put("/api/qimo/callback", "anon"); // 云坐席推送不鉴权
filterChainDefinitionMap.put("/sys/cas/client/validateLogin", "anon"); //cas验证登录 filterChainDefinitionMap.put("/sys/cas/client/validateLogin", "anon"); //cas验证登录
filterChainDefinitionMap.put("/sys/randomImage/**", "anon"); //登录验证码接口排除 filterChainDefinitionMap.put("/sys/randomImage/**", "anon"); //登录验证码接口排除
filterChainDefinitionMap.put("/consultation/conSession/getPicUrl", "anon"); // 移动端首页获取图片接口排除
filterChainDefinitionMap.put("/sys/checkCaptcha", "anon"); //登录验证码接口排除 filterChainDefinitionMap.put("/sys/checkCaptcha", "anon"); //登录验证码接口排除
filterChainDefinitionMap.put("/sys/login", "anon"); //登录接口排除 filterChainDefinitionMap.put("/sys/login", "anon"); //登录接口排除
filterChainDefinitionMap.put("/sys/loginAnYan", "anon"); //登录接口排除(三方授权) filterChainDefinitionMap.put("/sys/loginAnYan", "anon"); //登录接口排除(三方授权)
@@ -184,6 +185,8 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/version/getSysVersionDetailByPackageName", "anon"); filterChainDefinitionMap.put("/version/getSysVersionDetailByPackageName", "anon");
filterChainDefinitionMap.put("/sys/api/notice/selectIosUpdate", "anon"); filterChainDefinitionMap.put("/sys/api/notice/selectIosUpdate", "anon");
// filterChainDefinitionMap.put("/sys/upload/down/**", "anon");
// filterChainDefinitionMap.put("/sys/upload/show/**", "anon");
// 添加自己的过滤器并且取名为jwt // 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<String, Filter>(1); Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
@@ -10,9 +10,21 @@ import lombok.Getter;
@Getter @Getter
public enum ExportExcelEnum { public enum ExportExcelEnum {
/** /**
* 积分规则 * 员工积分信息
*/ */
BANK_POINTS_RULE("bcsStationExportCode", "站点数据", "/bcs/station", null), BANK_USER_POINTS("bankUserPointsExportCode", "员工积分信息", "/bank/userPoints", null),
/**
* 员工积分明细
*/
BANK_POINTS_DETAIL("bankPointsDetailExportCode", "员工积分明细", "/bank/pointsDetail", null),
/**
* 员工打卡监测
*/
BANK_CLOCK_IN_RECORD("bankClockInRecordExportCode", "员工打卡监测", "/bank/clockInRecord", null),
/**
* 员工积分排名
*/
BANK_USER_RANKING("bankUserRankingExportCode", "员工积分排名", "/bank/userRanking", null),
; ;
/** /**
@@ -49,7 +49,12 @@ public enum ServerEnum {
/** /**
* 医疗点模块 * 医疗点模块
*/ */
WATCH("health-watch", "watchCenter"); WATCH("health-watch", "watchCenter"),
/**
* 健康银行模块
*/
BANK("health-bank", "bank"),
;
/** /**
* 服务名 * 服务名
@@ -0,0 +1,110 @@
package org.jeecg.util;
import org.jeecg.common.constant.CommonConstant;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
/**
* AES/CBC/PKCS5Padding 对称加密工具类
* 加密结果经 Base64 URL Safe无填充编码可安全用于 URL 传参
*/
public class AesEncryptUtils {
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding";
/**
* CBC 模式 IV 固定 16 字节
*/
private static final int IV_LENGTH = 16;
/**
* 加密
* 随机生成 IV [IV(16字节) + 密文] 拼接后进行 Base64 URL Safe无填充编码
*
* @param plainText 明文字符串
* @param secretKey 密钥任意长度字符串内部经 SHA-256 截取为 128
* @return URL 安全的密文字符串不含 +/= 等特殊字符
* @throws Exception 加密异常
*/
public static String encrypt(String plainText, String secretKey) throws Exception {
byte[] keyBytes = deriveKey(secretKey);
byte[] ivBytes = generateIv();
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE,
new SecretKeySpec(keyBytes, ALGORITHM),
new IvParameterSpec(ivBytes));
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
// IV 前缀拼接到密文解密时从头部分离
byte[] combined = new byte[IV_LENGTH + encrypted.length];
System.arraycopy(ivBytes, 0, combined, 0, IV_LENGTH);
System.arraycopy(encrypted, 0, combined, IV_LENGTH, encrypted.length);
return Base64.getUrlEncoder().withoutPadding().encodeToString(combined);
}
/**
* 解密
* 从密文头部分离 IV再执行 AES/CBC 解密
*
* @param cipherText URL 安全的密文字符串 encrypt 方法生成
* @param secretKey 密钥须与加密时保持一致
* @return 解密后的明文字符串
* @throws Exception 解密异常密钥错误或数据被篡改时抛出
*/
public static String decrypt(String cipherText, String secretKey) throws Exception {
byte[] combined = Base64.getUrlDecoder().decode(cipherText);
byte[] ivBytes = Arrays.copyOfRange(combined, 0, IV_LENGTH);
byte[] encrypted = Arrays.copyOfRange(combined, IV_LENGTH, combined.length);
byte[] keyBytes = deriveKey(secretKey);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(keyBytes, ALGORITHM),
new IvParameterSpec(ivBytes));
return new String(cipher.doFinal(encrypted), StandardCharsets.UTF_8);
}
/**
* 通过 SHA-256 对密钥字符串进行哈希取前 16 字节作为 AES 128 位密钥
* 支持任意长度的密钥输入避免 AES 对密钥长度的严格限制
*
* @param secretKey 原始密钥字符串
* @return 16 字节的 AES 密钥
*/
private static byte[] deriveKey(String secretKey) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(secretKey.getBytes(StandardCharsets.UTF_8));
// 取前 16 字节 = 128 AES 密钥
return Arrays.copyOf(hash, 16);
}
/**
* 使用 SecureRandom 随机生成 16 字节 IV
* 每次加密使用不同 IV防止相同明文产生相同密文
*
* @return 16 字节随机 IV
*/
private static byte[] generateIv() {
byte[] iv = new byte[IV_LENGTH];
new SecureRandom().nextBytes(iv);
return iv;
}
public static void main(String[] args) {
try {
String plainText = "https://hello world";
String secretKey = CommonConstant.URL_SECRET_KEY;
// String cipherText = AesEncryptUtils.encrypt(plainText, secretKey);
// System.out.println(cipherText);
String decryptedText = AesEncryptUtils.decrypt("6oFxHVMYNHXpz8VtsREcNlokVqOdvaZNSGBJ5U3_7mY", secretKey);
System.out.println(decryptedText);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@@ -0,0 +1,227 @@
package org.jeecg.util;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
public class ContainerMetricsUtil {
// 缓存上一次 CPU 使用数据用于计算使用率
private static final Map<String, Long> LAST_CPU_USAGE = new ConcurrentHashMap<>();
private static final Map<String, Long> LAST_CPU_TIME = new ConcurrentHashMap<>();
// 判断是否为 cgroup v2
private static boolean isCgroupV2() {
return Files.exists(Paths.get("/sys/fs/cgroup/cgroup.controllers"));
}
// 通用读取第一行
private static String readFirstLine(String path) throws IOException {
java.nio.file.Path p = Paths.get(path);
if (!Files.exists(p)) {
throw new IOException("文件不存在: " + path);
}
List<String> lines = Files.readAllLines(p);
if (lines.isEmpty()) {
throw new IOException("文件为空: " + path);
}
return lines.get(0).trim();
}
// ========================
// 内存指标
// ========================
public static long getMemoryLimitBytes() {
try {
if (isCgroupV2()) {
String max = readFirstLine("/sys/fs/cgroup/memory.max");
if ("max".equals(max)) return -1; // 无限制
return Long.parseLong(max);
} else {
long limit = Long.parseLong(readFirstLine("/sys/fs/cgroup/memory/memory.limit_in_bytes"));
// 过滤无限制的极大值 9223372036854771712
return (limit > (1L << 40)) ? -1 : limit;
}
} catch (Exception e) {
log.debug("无法读取内存限制", e);
return -1;
}
}
public static long getMemoryUsageBytes() {
try {
if (isCgroupV2()) {
return Long.parseLong(readFirstLine("/sys/fs/cgroup/memory.current"));
} else {
return Long.parseLong(readFirstLine("/sys/fs/cgroup/memory/memory.usage_in_bytes"));
}
} catch (Exception e) {
log.debug("无法读取内存使用量", e);
return -1;
}
}
// ========================
// CPU 指标
// ========================
/**
* 获取 CPU 限制核数-1 表示无限制
*/
public static double getCpuLimitCores() {
try {
if (isCgroupV2()) {
String cpuMax = readFirstLine("/sys/fs/cgroup/cpu.max");
String[] parts = cpuMax.split(" ");
if ("max".equals(parts[0])) return -1;
long quota = Long.parseLong(parts[0]);
long period = Long.parseLong(parts[1]);
return (double) quota / period;
} else {
long quota = Long.parseLong(readFirstLine("/sys/fs/cgroup/cpu/cpu.cfs_quota_us"));
if (quota == -1) return -1;
long period = Long.parseLong(readFirstLine("/sys/fs/cgroup/cpu/cpu.cfs_period_us"));
return (double) quota / period;
}
} catch (Exception e) {
log.debug("无法读取 CPU 限制", e);
return -1;
}
}
/**
* 获取 CPU 使用率百分比单核基准
* 首次调用返回 0后续调用返回基于时间差的使用率
*/
public static double getCpuUsagePercent() {
try {
long currentUsage;
if (isCgroupV2()) {
// 读取 /sys/fs/cgroup/cpu.stat
String content = readFirstLine("/sys/fs/cgroup/cpu.stat");
String[] lines = content.split("\n");
long usageUsec = -1;
for (String line : lines) {
if (line.startsWith("usage_usec")) {
String[] parts = line.trim().split("\\s+");
if (parts.length >= 2) {
usageUsec = Long.parseLong(parts[1]);
break;
}
}
}
if (usageUsec == -1) {
log.warn("cgroup v2: 未找到 usage_usec,无法计算 CPU 使用率");
return -1;
}
currentUsage = usageUsec * 1000L; // 转为纳秒
} else {
// cgroup v1
currentUsage = Long.parseLong(readFirstLine("/sys/fs/cgroup/cpu/cpuacct.usage"));
}
long currentTime = System.nanoTime();
String key = isCgroupV2() ? "v2" : "v1";
Long lastUsage = LAST_CPU_USAGE.get(key);
Long lastTime = LAST_CPU_TIME.get(key);
if (lastUsage == null || lastTime == null) {
LAST_CPU_USAGE.put(key, currentUsage);
LAST_CPU_TIME.put(key, currentTime);
return 0.0;
}
long usageDelta = currentUsage - lastUsage;
long timeDeltaNs = currentTime - lastTime;
if (timeDeltaNs <= 0) {
return 0.0;
}
double cpuPercent = (usageDelta * 100.0) / timeDeltaNs;
// 归一化可选
double limit = getCpuLimitCores();
if (limit > 0 && limit < 1.0) {
cpuPercent = Math.min(cpuPercent / limit, 100.0);
} else {
cpuPercent = Math.min(cpuPercent, 100.0);
}
LAST_CPU_USAGE.put(key, currentUsage);
LAST_CPU_TIME.put(key, currentTime);
return Math.max(0.0, cpuPercent);
} catch (Exception e) {
log.debug("计算 CPU 使用率失败", e);
return -1;
}
}
// ========================
// 综合方法返回所有指标
// ========================
public static Map<String, Object> getContainerMetrics() {
Map<String, Object> metrics = new HashMap<>();
// 内存
long memLimit = getMemoryLimitBytes();
long memUsage = getMemoryUsageBytes();
metrics.put("memoryLimitBytes", memLimit > 0 ? memLimit : null);
metrics.put("memoryUsedBytes", memUsage > 0 ? memUsage : null);
if (memLimit > 0 && memUsage > 0) {
double memPercent = Math.round((memUsage * 100.0 / memLimit) * 100) / 100.0;
metrics.put("memoryPercent", memPercent);
}
// CPU
double cpuLimit = getCpuLimitCores();
double cpuUsage = getCpuUsagePercent();
metrics.put("cpuLimitCores", cpuLimit > 0 ? cpuLimit : null);
metrics.put("cpuUsagePercent", cpuUsage >= 0 ? cpuUsage : null);
// JVM 堆内存
long heapUsed = java.lang.management.ManagementFactory.getMemoryMXBean()
.getHeapMemoryUsage().getUsed();
metrics.put("jvmHeapUsedBytes", heapUsed);
return metrics;
}
// 辅助格式化字节仅用于日志不放入返回值
private static String formatBytes(long bytes) {
if (bytes <= 0) return "0 B";
if (bytes < 1024 * 1024) {
return String.format("%.2f MB", bytes / (1024.0 * 1024));
} else if (bytes < 1024 * 1024 * 1024) {
return String.format("%.2f MB", bytes / (1024.0 * 1024));
} else {
return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024));
}
}
// 便捷方法记录日志 + 返回数据
public static Map<String, Object> logAndCollectMetrics() {
Map<String, Object> m = getContainerMetrics();
log.info(
"容器资源 | 内存: {} / {} ({}%) | CPU: {}% (限制: {}核) | JVM堆: {}",
m.get("memoryUsedBytes") != null ? formatBytes((Long) m.get("memoryUsedBytes")) : "N/A",
m.get("memoryLimitBytes") != null ? formatBytes((Long) m.get("memoryLimitBytes")) : "N/A",
m.get("memoryPercent"),
m.get("cpuUsagePercent"),
m.get("cpuLimitCores"),
formatBytes((Long) m.get("jvmHeapUsedBytes"))
);
return m;
}
}

Some files were not shown because too many files have changed in this diff Show More