19 Commits
Author SHA1 Message Date
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
44 changed files with 686 additions and 55 deletions
@@ -1,5 +1,6 @@
package com.renkang.consultation.dto;
import cn.hutool.http.HtmlUtil;
import com.renkang.consultation.entity.ConPage;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -11,6 +12,10 @@ public class AccountDTO extends ConPage {
@Schema(title = "专家姓名")
private String doctorName;
public void setDoctorName(String doctorName) {
this.doctorName = doctorName != null ? HtmlUtil.unescape(doctorName) : null;
}
@Schema(title = "医院选择")
private String hospitalName;
@@ -1,5 +1,6 @@
package com.renkang.consultation.dto;
import cn.hutool.http.HtmlUtil;
import com.renkang.consultation.entity.ConPage;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -10,6 +11,10 @@ public class ConDoctorDTO extends ConPage {
@Schema(title = "专家姓名")
private String doctorName;
public void setDoctorName(String doctorName) {
this.doctorName = doctorName != null ? HtmlUtil.unescape(doctorName) : null;
}
@Schema(title = "专家编号")
private String doctorNo;
@@ -1,5 +1,6 @@
package com.renkang.consultation.dto;
import cn.hutool.http.HtmlUtil;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.renkang.consultation.entity.ConPage;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -17,6 +18,9 @@ public class ConEvaluateDTO extends ConPage {
@Schema(title = "咨询人")
private String userName;
public void setUserName(String userName) {
this.userName = userName != null ? HtmlUtil.unescape(userName) : null;
}
@Schema(title = "二级单位id")
private String sysOrgSecondId;
@@ -27,6 +31,10 @@ public class ConEvaluateDTO extends ConPage {
@Schema(title = "咨询专家")
private String expertName;
public void setExpertName(String expertName) {
this.expertName = expertName != null ? HtmlUtil.unescape(expertName) : null;
}
@Schema(title = "专家医院")
private String expertHospital;
@@ -1,5 +1,6 @@
package com.renkang.consultation.dto;
import cn.hutool.http.HtmlUtil;
import com.renkang.consultation.entity.ConPage;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -10,6 +11,10 @@ public class ConHealthInfoAnswerDTO extends ConPage {
@Schema(title = "员工姓名")
private String fromName;
public void setFromName(String fromName) {
this.fromName = fromName != null ? HtmlUtil.unescape(fromName) : null;
}
@Schema(title = "手机号")
private String phone;
@@ -1,5 +1,6 @@
package com.renkang.consultation.dto;
import cn.hutool.http.HtmlUtil;
import com.renkang.consultation.entity.ConPage;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -11,6 +12,10 @@ public class ConHelperDTO extends ConPage {
@Schema(title = "全科医生姓名")
private String userName;
public void setUserName(String userName) {
this.userName = userName != null ? HtmlUtil.unescape(userName) : null;
}
@Schema(title = "状态")
private Integer status;
@@ -1,5 +1,6 @@
package com.renkang.consultation.dto;
import cn.hutool.http.HtmlUtil;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.renkang.consultation.entity.ConPage;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -17,6 +18,10 @@ public class ConSessionDTO extends ConPage {
@Schema(title = "员工姓名")
private String fromName;
public void setFromName(String fromName) {
this.fromName = fromName != null ? HtmlUtil.unescape(fromName) : null;
}
@Schema(title = "员工id")
private String fromAccount;
@@ -26,6 +31,10 @@ public class ConSessionDTO extends ConPage {
@Schema(title = "专家姓名")
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超时自动结束)")
private String contentStatus;
@@ -480,4 +480,10 @@ public class ConSessionApiController {
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();
String endVideoSession();
}
@@ -2114,4 +2114,8 @@ public class ConSessionApiServiceImpl implements ConSessionApiService {
return conSessionMapper.selectCount(wrapper).intValue();
}
public String endVideoSession() {
return conSessionMapper.endVideoSession();
}
}
@@ -294,4 +294,13 @@ public interface ConSessionMapper extends BaseMapper<ConSession> , MPJBaseMapper
@Param("doctorjob")String doctorjob,
@Param("contentType")String contentType,
@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();
}
@@ -3,7 +3,6 @@ package com.renkang.consultation.service.impl;
import cn.hutool.core.util.ObjectUtil;
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.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -249,9 +248,6 @@ public class ConDoctorServiceImpl extends ServiceImpl<ConDoctorMapper, ConDoctor
if (StringUtils.hasLength(dto.getHospitalName())) {
conDoctor.setResourceId(dto.getHospitalName());
}
if (StrUtil.isNotEmpty(dto.getDoctorName())){
conDoctor.setDoctorName(HtmlUtil.unescape(dto.getDoctorName()));
}
IPage<ConDoctor> conDoctorIPage = conDoctorMapper.queryPageList(page, conDoctor);
List<ConDoctor> conDoctorList = conDoctorIPage.getRecords();
if (CollectionUtils.isEmpty(conDoctorList)) {
@@ -7,6 +7,7 @@ import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.config.shiro.ClientSignThreadLocal;
import org.parboiled.common.StringUtils;
@@ -53,4 +54,12 @@ public class SyncScheduleJob {
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;
import cn.hutool.http.HtmlUtil;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -7,6 +8,10 @@ import lombok.Data;
public class CallLogDTO {
@Schema(description = "姓名")
private String realName;
public void setRealName(String realName) {
this.realName = realName != null ? HtmlUtil.unescape(realName) : null;
}
@Schema(description = "呼叫中心")
private String centerId;
@Schema(description = "坐席工号")
@@ -1,5 +1,6 @@
package com.renkang.emergency.entity.dto;
import cn.hutool.http.HtmlUtil;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -21,6 +22,10 @@ public class PhoneListDTO {
private String phoneType;
@Schema(description = "姓名")
private String realName;
public void setRealName(String realName) {
this.realName = realName != null ? HtmlUtil.unescape(realName) : null;
}
/**
* 0:全部 1:女 2:男
*/
@@ -1,5 +1,6 @@
package com.renkang.emergency.entity.dto;
import cn.hutool.http.HtmlUtil;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -9,6 +10,10 @@ public class TblQimoNoticeDTO {
private String seatId;
@Schema(title = "姓名")
private String realName;
public void setRealName(String realName) {
this.realName = realName != null ? HtmlUtil.unescape(realName) : null;
}
@Schema(title = "所属应急中心")
private String centerId;
@Schema(title = "orgCode")
@@ -1,6 +1,7 @@
package com.renkang.emergency.controller;
import cn.hutool.http.HtmlUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.renkang.emergency.entity.EmergencyProfessionUser;
import com.renkang.emergency.service.EmergencyProfessionUserService;
@@ -40,6 +41,7 @@ public class EmergencyProfessionUserController {
@RequestParam(name = "type", defaultValue = "5") Integer type,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize) {
name = HtmlUtil.unescape(name);
IPage<EmergencyProfessionUser> pageList = emergencyProfessionUserService.customPage(name, centerId, type, pageNo, pageSize);
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.ObjectUtil;
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.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -58,6 +59,9 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
@Override
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 initiatorUserName = order.getInitiatorUserName();
Order orderCopy = new Order();
@@ -22,6 +22,10 @@ public class WatchH5IndexItemNewVo {
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
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时此字段返回长睡时长
*/
@@ -1,5 +1,6 @@
package com.renkang.watch.bean.request;
import cn.hutool.http.HtmlUtil;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
@@ -19,6 +20,9 @@ public class AbnormalEventFilter {
private String departOrgCode;
// 用户名称
private String userName;
public void setUserName(String userName) {
this.userName = userName != null ? HtmlUtil.unescape(userName) : null;
}
// 工具编码
private String watchNo;
//员工编号
@@ -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;
import cn.hutool.http.HtmlUtil;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -18,6 +19,10 @@ public class CalorieDataByUserDTO {
private Integer pageSize = 10;
@Schema(title = "用户姓名")
private String realName;
public void setRealName(String realName) {
this.realName = realName != null ? HtmlUtil.unescape(realName) : null;
}
@Schema(title = "工号")
private String workNo;
@Schema(title = "用户id组")
@@ -1,22 +1,71 @@
package com.renkang.watch.vo.UserData.res;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.renkang.watch.WatchH5IndexItemNewVo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/** 用户统计数据
/**
* 用户统计数据
*
* @author stan
* @since 2024-11-04 16:33
*/
@Data
public class UserStat {
/**
* 数据类型:0=心率 1=血氧 2=压力 3=体温 4=睡眠
*/
private String type;
private BigDecimal latest;
private BigDecimal max;
private BigDecimal min;
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;
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));
}
}
@@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
import java.util.List;
import java.util.Map;
@RestController
@@ -41,6 +42,16 @@ public class WatchDataApiController {
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 java.util.Date;
import java.util.List;
import java.util.Map;
public interface WatchDataApiService {
@@ -43,6 +44,14 @@ public interface WatchDataApiService {
UserStat userStatResult(String type);
/**
* 查询当前登录用户所有健康指标统计数据(按指定日期过滤)
*
* @param date 查询日期,为 null 时返回最新一条数据
* @return 各指标统计结果列表
*/
List<UserStat> userStatResultAll(Date date);
Boolean userHaveWatch(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.QueryWrapper;
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.service.WatchDataApiService;
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.heartRate.WatchDataHeartRateRes;
import com.renkang.watch.vo.UserData.res.sleep.WatchDataSleepRes;
import com.renkang.watch.vo.WatchH5IndexItemVo;
import org.apache.commons.collections4.CollectionUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.DateUtils;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.global.GlobalUtils;
import org.springframework.beans.BeanUtils;
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 javax.annotation.PostConstruct;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Service
public class WatchDataApiServiceImpl implements WatchDataApiService {
@Autowired
private WatchDataWorkoutMapper watchDataWorkoutMapper;
@Autowired
private WatchStatUserInfoDayHeartRateMapper watchStatUserInfoDayHeartRateMapper;
@Autowired
private WatchDataHeartRateMapper watchDataHeartRateMapper;
@@ -60,9 +65,6 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
@Autowired
private WatchStatUserInfoDaySpo2ServiceImpl watchStatUserInfoDaySpo2ServiceImpl;
@Autowired
private WatchStatUserInfoDaySleepMapper watchStatUserInfoDaySleepMapper;
@Autowired
private WatchDataSleepNewMinuteMapper watchDataSleepNewMinuteMapper;
@@ -96,7 +98,6 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
@Autowired
private IWatchDataTemperatureService watchDataTemperatureService;
@Autowired
private IWatchDataStressService watchDataStressService;
@Autowired
@@ -106,12 +107,47 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
@Autowired
private WatchUserDataMapper watchUserDataMapper;
@Autowired
private WatchStatUserInfoDayHeartRateMapper watchStatUserInfoDayHeartRateMapper;
@Autowired
private WatchStatUserInfoDaySleepMapper watchStatUserInfoDaySleepMapper;
@Autowired
private WatchStatUserInfoDaySpo2Mapper watchStatUserInfoDaySpo2Mapper;
@Autowired
private WatchStatUserInfoDayStressMapper watchStatUserInfoDayStressMapper;
@Autowired
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
public Result<Map<String,Object>> selectSportCostListApi() {
LoginUser loginUser = GlobalUtils.getLoginUser();
@@ -834,20 +870,171 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
String userId = GlobalUtils.getLoginUser().getId();
switch (type) {
case "0":
return handleHeartRate(userId);
return handleHeartRate(userId,null);
case "1":
return handleBloodOxygen(userId);
return handleBloodOxygen(userId,null);
case "2":
return handleStress(userId);
return handleStress(userId,null);
case "3":
return handleBodyTemperature(userId);
return handleBodyTemperature(userId,null);
case "4":
return handleSleep(userId);
return handleSleep(userId,null);
default:
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
public Boolean userHaveWatch(String 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)
.select(WatchStatUserInfoDaySleep::getId,
WatchStatUserInfoDaySleep::getDeepSleepTimes,
@@ -893,13 +1080,15 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
WatchStatUserInfoDaySleep::getDeepLightTimes,
WatchStatUserInfoDaySleep::getDataDate)
.eq(WatchStatUserInfoDaySleep::getUserId, userId)
.orderByDesc(WatchStatUserInfoDaySleep::getDataDate)
// 传了日期则精确匹配,否则取最新一条
.eq(date != null, WatchStatUserInfoDaySleep::getDataDate, date)
.orderByDesc(date == null, WatchStatUserInfoDaySleep::getDataDate)
.last("limit 1");
WatchStatUserInfoDaySleep data = watchStatUserInfoDaySleepMapper.selectOne(queryWrapper);
if (data == null) {
return null;
return new UserStat();
}
UserStat stat = new UserStat();
@@ -915,15 +1104,15 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
return stat;
}
private UserStat handleBodyTemperature(String userId) {
private UserStat handleBodyTemperature(String userId, Date date) {
int type = 5;
int scale = 1;
// 最新数据
UserStat userStat = userWatchDataLatest(userId, type, scale);
if (userStat == null) {
return null;
return new UserStat();
}
LambdaQueryWrapper<WatchStatUserInfoDayTemp> queryWrapper = getQueryWrapper(userId);
LambdaQueryWrapper<WatchStatUserInfoDayTemp> queryWrapper = getQueryWrapper(userId, date);
WatchStatUserInfoDayTemp data = watchStatUserInfoDayTempMapper.selectOne(queryWrapper);
if (data == null) {
return userStat;
@@ -933,15 +1122,15 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
return userStat;
}
private UserStat handleStress(String userId) {
private UserStat handleStress(String userId, Date date) {
int type = 3;
int scale = 0;
// 最新数据
UserStat userStat = userWatchDataLatest(userId, type, scale);
if (userStat == null) {
return null;
return new UserStat();
}
LambdaQueryWrapper<WatchStatUserInfoDayStress> queryWrapper = getQueryWrapper(userId);
LambdaQueryWrapper<WatchStatUserInfoDayStress> queryWrapper = getQueryWrapper(userId, date);
WatchStatUserInfoDayStress data = watchStatUserInfoDayStressMapper.selectOne(queryWrapper);
if (data == null) {
return userStat;
@@ -951,15 +1140,15 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
return userStat;
}
private UserStat handleBloodOxygen(String userId) {
private UserStat handleBloodOxygen(String userId, Date date) {
int type = 2;
int scale = 0;
// 最新数据
UserStat userStat = userWatchDataLatest(userId, type, scale);
if (userStat == null) {
return null;
return new UserStat();
}
LambdaQueryWrapper<WatchStatUserInfoDaySpo2> queryWrapper = getQueryWrapper(userId);
LambdaQueryWrapper<WatchStatUserInfoDaySpo2> queryWrapper = getQueryWrapper(userId, date);
WatchStatUserInfoDaySpo2 data = watchStatUserInfoDaySpo2Mapper.selectOne(queryWrapper);
if (data == null) {
return userStat;
@@ -969,15 +1158,15 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
return userStat;
}
private UserStat handleHeartRate(String userId) {
private UserStat handleHeartRate(String userId, Date date) {
int type = 1;
int scale = 0;
// 最新数据
UserStat userStat = userWatchDataLatest(userId, type, scale);
if (userStat == null) {
return null;
return new UserStat();
}
LambdaQueryWrapper<WatchStatUserInfoDayHeartRate> queryWrapper = getQueryWrapper(userId);
LambdaQueryWrapper<WatchStatUserInfoDayHeartRate> queryWrapper = getQueryWrapper(userId, date);
WatchStatUserInfoDayHeartRate data = watchStatUserInfoDayHeartRateMapper.selectOne(queryWrapper);
if (data == null) {
return userStat;
@@ -1029,10 +1218,12 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
return stat;
}
private <T> LambdaQueryWrapper<T> getQueryWrapper(String userId) {
private <T> LambdaQueryWrapper<T> getQueryWrapper(String userId, Date date) {
QueryWrapper<T> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", userId)
.orderByDesc("data_date")
// 传了日期则精确匹配,否则取最新一条
.eq(date != null, "data_date", date)
.orderByDesc(date == null, "data_date")
.last("limit 1");
return queryWrapper.lambda();
}
@@ -1051,4 +1242,17 @@ public class WatchDataApiServiceImpl implements WatchDataApiService {
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;
}
}
@@ -11,10 +11,7 @@ import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;
@@ -327,4 +324,5 @@ public class WatchController {
// }
return result;
}
}
@@ -185,6 +185,12 @@ public class WatchDeviceController extends JeecgController<WatchDevice, IWatchDe
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,16 @@ public class WatchDeviceController extends JeecgController<WatchDevice, IWatchDe
@Operation(summary = "解绑用户", description = "解绑用户")
@PostMapping(value = "/unbundleUser")
@RequiresPermissions("watch:watch_device:unbindUser")
public Result unbundleUser(@RequestBody() BindUserDTO dto) throws ParseException {
public Result unbundleUser(@RequestBody() BindUserDTO dto) {
return watchDeviceService.unbundleUser(dto);
}
@Operation(summary = "移动端解绑设备(手表)", description = "移动端解绑设备(手表)")
@GetMapping("/app/unbundleDevice")
public Result unbundleDevice(@RequestParam(name = "watchNo") String watchNo) {
return watchDeviceService.unbundleDevice(watchNo);
}
/**
* 设置上传频次
*
@@ -43,12 +43,16 @@ public interface IWatchDeviceService extends IService<WatchDevice> {
*/
Result bindUser(BindUserDTO dto) throws ParseException;
Result bindDevice(String watchNo) throws ParseException;
/**
* 绑定用户
* @param dto
* @return
*/
Result unbundleUser(BindUserDTO dto) throws ParseException;
Result unbundleUser(BindUserDTO dto);
Result unbundleDevice(String watchNo);
/**
* 工具开关
@@ -3,6 +3,7 @@ package com.renkang.watch.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -87,6 +88,7 @@ public class WatchBindHisServiceImpl extends ServiceImpl<WatchBindHisMapper, Wat
@Override
public IPage<WatchBindHisVo> queryPageList(IPage<WatchBindHisVo> page, WatchBindHisDTO dto) {
if (StringUtils.hasLength(dto.getBindRealName())) {
dto.setBindRealName(HtmlUtil.unescape(dto.getBindRealName()));
List<String> userIdsByDepartAndUsername = watchCloudManager.getUserIdsByDepartAndUsername("", dto.getBindRealName(), "", "");
if (CollectionUtil.isEmpty(userIdsByDepartAndUsername)) {
return new Page<>();
@@ -372,6 +374,7 @@ public class WatchBindHisServiceImpl extends ServiceImpl<WatchBindHisMapper, Wat
String orgCode = StrUtil.isNotBlank(dto.getThirdDepart())?dto.getThirdDepart() : dto.getSecondDepart();
List<String> userIds = new ArrayList<>();
if (StrUtil.isNotBlank(dto.getBindRealName())) {
dto.setBindRealName(HtmlUtil.unescape(dto.getBindRealName()));
userIds = watchCloudManager.getUserIdsByDepartAndUsername("", dto.getBindRealName(), "", "");
if (CollectionUtil.isEmpty(userIds)) {
return new Page<>();
@@ -7,6 +7,7 @@ import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
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.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -104,6 +105,9 @@ public class WatchDataServiceImpl extends ServiceImpl<WatchDataMapper, WatchData
}
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);
if (CollectionUtil.isNotEmpty(pageList.getRecords())) {
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.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -167,6 +168,9 @@ public class WatchDeviceServiceImpl extends ServiceImpl<WatchDeviceMapper, Watch
@Override
public IPage<WatchDeviceVo> queryPageList(IPage<WatchDeviceVo> page, WatchDeviceDTO dto) {
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());
if (CollectionUtil.isEmpty(userIds)) {
return new Page<>();
@@ -291,10 +295,20 @@ public class WatchDeviceServiceImpl extends ServiceImpl<WatchDeviceMapper, Watch
}
WatchDevice watchDevice = getDeviceByWatchNo(dto.getWatchNo());
if (watchDevice == null) {
return Result.error("该工具不存在");
return Result.error("监测工具不存在");
}
// 新增手表只能绑定给本单位的人
LoginUser userById = sysBaseAPI.getUserById(dto.getBindUserId());
if (userById.getOrgCode() == null || dto.getOrgCode() == null ||
userById.getOrgCode().length() < 6 || dto.getOrgCode().length() < 6 ||
!userById.getOrgCode().substring(0, 6).equals(dto.getOrgCode().substring(0, 6))) {
return Result.error("手表只能绑定给本单位人员");
}
if (isDeviceAlreadyBound(watchDevice, dto)) {
return Result.error("该设备已绑定该员工");
return Result.error("该设备已绑定");
}
// 查询员工是否有绑定
List<WatchDevice> deviceByUserIdList = getDeviceByUserId(dto.getBindUserId());
@@ -322,7 +336,6 @@ public class WatchDeviceServiceImpl extends ServiceImpl<WatchDeviceMapper, Watch
// 初始化开关数据
unbindUserAndInitDeviceSwitch(dto);
}
LoginUser userById = sysBaseAPI.getUserById(dto.getBindUserId());
// 此处修改 只有是职工的是才赋值单位信息
if (dto.getIsEmployee()) {
SysDepart inDepart = sysCache.getDepartByOrgCode(userById.getOrgCode());
@@ -350,6 +363,17 @@ public class WatchDeviceServiceImpl extends ServiceImpl<WatchDeviceMapper, Watch
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 +409,7 @@ public class WatchDeviceServiceImpl extends ServiceImpl<WatchDeviceMapper, Watch
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Result unbundleUser(BindUserDTO dto) throws ParseException {
public Result unbundleUser(BindUserDTO dto){
WatchDevice watchDevice = getDeviceByWatchNo(dto.getWatchNo());
if (watchDevice == null) {
return Result.error("该设备不存在");
@@ -409,6 +433,15 @@ public class WatchDeviceServiceImpl extends ServiceImpl<WatchDeviceMapper, Watch
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);
}
/**
* 根据手表编号获取手表信息
@@ -721,4 +721,9 @@ public interface CommonConstant {
* 多选
*/
String PD = "PD";
/**
* url加解密密钥(AES
*/
String URL_SECRET_KEY = "a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5";
}
@@ -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();
}
}
}
@@ -82,18 +82,36 @@ public class BaseEmployeeInfo {
@Schema(title = "二级部门Code(单位)")
private String secondDeptCode;
@Schema(title = "二级部门id(单位)")
private String secondDeptId;
@Schema(title = "二级部门名称(单位)")
private String secondDeptName;
@Schema(title = "三级部门Code(部门)")
private String thirdDeptCode;
@Schema(title = "三级部门id(部门)")
private String thirdDeptId;
@Schema(title = "三级部门名称(部门)")
private String thirdDeptName;
@Schema(title = "当前部门Code(用户所属部门)")
private String thisDeptCode;
@Schema(title = "当前部门id(用户所属部门)")
private String thisDeptId;
@Schema(title = "当前部门名称(用户所属部门)")
private String thisDeptName;
@Schema(title = "角色编码")
private String roleCodes;
@Schema(title = "管理部门(code")
private String departCodes;
@Schema(title = "管理部门(id")
private String departIds;
}
@@ -3,16 +3,20 @@ package org.jeecg.modules.api.controller;
import org.jeecg.base.UserBase;
import org.jeecg.bean.response.BaseEmployeeInfo;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.modules.system.bean.response.ScalePersonalUserInfo;
import org.jeecg.modules.system.service.ISysUserService;
import org.jeecg.modules.system.service.ScalePersonalService;
import org.jeecg.util.AesEncryptUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author stan
@@ -67,5 +71,14 @@ public class SysApiController {
return Result.ok(sysUserService.getUserInfo());
}
/**
* 字符串加密
*/
@GetMapping("encryptInfo")
public Result<Map<String, String>> encryptInfo(@RequestParam String str) throws Exception {
Map<String, String> map = new HashMap<>();
map.put("encryptData", AesEncryptUtils.encrypt(str, CommonConstant.URL_SECRET_KEY));
return Result.ok(map);
}
}
@@ -2,6 +2,7 @@ package org.jeecg.modules.system.controller;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.common.collect.Maps;
@@ -124,6 +125,7 @@ public class HealthUserEmployeeExController extends JeecgController<HealthUserEm
HttpServletRequest req) {
Page<UserEmployee> page = new Page<>(pageNo, pageSize);
userFilter.setRealname(HtmlUtil.unescape(userFilter.getRealname()));
IPage<UserEmployee> pageList = healthUserEmployeeExService.selectHealthUserEmployeeExList(page, userFilter, idStatus);
return Result.OK(pageList);
}
@@ -2,6 +2,7 @@ package org.jeecg.modules.system.controller;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
@@ -460,6 +461,9 @@ public class SysUserController {
@RequestMapping(value = "/listAll", method = RequestMethod.GET)
public Result<IPage<SysUser>> queryAllPageList(SysUser user, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, HttpServletRequest req) {
if(StrUtil.isNotBlank(user.getRealname())){
user.setRealname(HtmlUtil.unescape(user.getRealname()));
}
QueryWrapper<SysUser> queryWrapper = QueryGenerator.initQueryWrapper(user, req.getParameterMap());
return sysUserService.queryPageList(req, queryWrapper, pageSize, pageNo);
}
@@ -1,6 +1,8 @@
package org.jeecg.modules.system.controller;
import cn.hutool.core.collection.CollectionUtil;
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.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -77,6 +79,9 @@ public class SysUserDepartChangeApplyController extends JeecgController<SysUserD
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize) {
Page<ChangeDepartVo> page = new Page<>(pageNo, pageSize);
if(StrUtil.isNotBlank(userName)){
userName = HtmlUtil.unescape(userName);
}
IPage<ChangeDepartVo> pageList = sysUserDepartChangeApplyService.pageCustom(page, userName, status,workNo, userId);
return Result.OK(pageList);
}
@@ -1,5 +1,6 @@
package org.jeecg.modules.system.entity;
import cn.hutool.http.HtmlUtil;
import lombok.Data;
/**
@@ -11,6 +12,10 @@ import lombok.Data;
public class SelectWrapperDTO {
private String orgCode;
private String realName;
public void setRealName(String realName) {
this.realName = realName != null ? HtmlUtil.unescape(realName) : null;
}
private String workNo;
private String sex;
private String code;
@@ -192,6 +192,10 @@ public class HealthUserEmployeeExServiceImpl extends ServiceImpl<HealthUserEmplo
}
List<String> orgCodes = GlobalUtils.getManagedCodes(filter.getOrgCode());
filter.setOrgCodes(orgCodes);
// 名字中包含·中文间隔号前端可能传入 HTML 实体 &middot;需转义回原字符
if (StrUtil.isNotBlank(filter.getRealname())) {
filter.setRealname(filter.getRealname().replaceAll("&middot;", "·"));
}
IPage<UserEmployee> userEmployeeIPage = healthUserEmployeeExMapper.queryUserEmployeeList(page, filter);
if (CollectionUtils.isEmpty(userEmployeeIPage.getRecords())) {
return userEmployeeIPage;
@@ -8,6 +8,7 @@ import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdcardUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -262,6 +263,9 @@ public class SysBaseApiImpl implements ISysBaseAPI {
@Override
public List<String> getUserIdsByDepartAndUsername(String departCode, String username, String idCard, String workNo) {
if(StrUtil.isNotBlank(username)){
username = HtmlUtil.unescape(username);
}
if (StrUtil.isAllBlank(departCode, username, idCard, workNo)) {
return Collections.emptyList();
}
@@ -6,6 +6,7 @@ import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.*;
import cn.hutool.extra.pinyin.PinyinUtil;
import cn.hutool.http.HtmlUtil;
import com.alibaba.csp.sentinel.util.StringUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
@@ -2875,6 +2876,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
queryWrapper.eq(SysUser::getStatus, 1);
queryWrapper.eq(SysUser::getPersonType, 1);
if (StrUtil.isNotEmpty(name)) {
name = HtmlUtil.unescape(name);
queryWrapper.like(SysUser::getRealname, name);
}
if (StrUtil.isNotEmpty(orgCode)) {
@@ -3906,6 +3908,16 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
if (null == loginUser) {
ExceptionAssertsUtil.fail("用户不存在!");
}
List<String> managedCodes = GlobalUtils.getManagedCodes(null);
Set<String> departIds = new HashSet<>();
if (CollUtil.isNotEmpty(managedCodes)) {
managedCodes.forEach(s -> {
String departIdByOrgCode = sysCache.getDepartIdByOrgCode(s);
if (StringUtils.isNotBlank(departIdByOrgCode) && !"null".equals(departIdByOrgCode)) {
departIds.add(departIdByOrgCode);
}
});
}
MPJLambdaWrapper<SysUser> queryWrapper = new MPJLambdaWrapper<SysUser>()
.selectAs(SysUser::getId, BaseEmployeeInfo::getUserId)
.selectAs(SysUser::getUsername, BaseEmployeeInfo::getUserName)
@@ -3931,13 +3943,19 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
baseEmployeeInfo.setAge(ObjectUtils.isNotEmpty(loginUser.getAge()) && loginUser.getAge() > 0 ? loginUser.getAge() : null);
SysDepart secondDepart = sysCache.getDepartByOrgCode(GlobalUtils.getSecondDepartOrgCode(loginUser.getOrgCode()));
baseEmployeeInfo.setSecondDeptCode(secondDepart.getOrgCode());
baseEmployeeInfo.setSecondDeptId(secondDepart.getId());
baseEmployeeInfo.setSecondDeptName(secondDepart.getDepartName());
SysDepart thirdDepart = sysCache.getDepartByOrgCode(GlobalUtils.getThirdDepartOrgCode(loginUser.getOrgCode()));
baseEmployeeInfo.setThirdDeptCode(thirdDepart.getOrgCode());
baseEmployeeInfo.setThirdDeptId(thirdDepart.getId());
baseEmployeeInfo.setThirdDeptName(thirdDepart.getDepartName());
baseEmployeeInfo.setThisDeptCode(loginUser.getOrgCode());
SysDepart departByOrgCode = sysCache.getDepartByOrgCode(loginUser.getOrgCode());
baseEmployeeInfo.setThisDeptName(departByOrgCode.getDepartName());
baseEmployeeInfo.setThisDeptId(departByOrgCode.getId());
baseEmployeeInfo.setRoleCodes(loginUser.getRoleCodes());
baseEmployeeInfo.setDepartCodes(String.join(",", managedCodes));
baseEmployeeInfo.setDepartIds(String.join(",", departIds));
return baseEmployeeInfo;
}
}
+2 -2
View File
@@ -50,7 +50,7 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<!--<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.4.0</version>
@@ -87,7 +87,7 @@
</container>
</configuration>
</plugin>
</plugin>-->
</plugins>
</build>
</profile>
+2 -2
View File
@@ -685,10 +685,10 @@
</nonFilteredFileExtensions>
</configuration>
</plugin>
<plugin>
<!--<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
</plugin>
</plugin>-->
</plugins>
<resources>
<resource>