diff --git a/healthlink-his-server/core-common/src/main/java/com/core/common/constant/CacheConstants.java b/healthlink-his-server/core-common/src/main/java/com/core/common/constant/CacheConstants.java index 618732edd..2d7137020 100755 --- a/healthlink-his-server/core-common/src/main/java/com/core/common/constant/CacheConstants.java +++ b/healthlink-his-server/core-common/src/main/java/com/core/common/constant/CacheConstants.java @@ -46,6 +46,16 @@ public class CacheConstants { */ public static final String LOGIN_SELECTED_TENANT = "login_selected_tenant:"; + /** + * Token黑名单 redis key + */ + public static final String TOKEN_BLACKLIST_KEY = "token_blacklist:"; + + /** + * 登录失败锁定 redis key + */ + public static final String LOGIN_FAIL_LOCK_KEY = "login_fail_lock:"; + /** * 超出上限,排番失败(时间:{},KEY:{} */ diff --git a/healthlink-his-server/core-framework/src/main/java/com/core/framework/web/service/LoginFailLockService.java b/healthlink-his-server/core-framework/src/main/java/com/core/framework/web/service/LoginFailLockService.java new file mode 100644 index 000000000..169d73598 --- /dev/null +++ b/healthlink-his-server/core-framework/src/main/java/com/core/framework/web/service/LoginFailLockService.java @@ -0,0 +1,79 @@ +package com.core.framework.web.service; + +import com.core.common.constant.CacheConstants; +import com.core.common.core.redis.RedisCache; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * 登录失败锁定服务 + * + * @author system + */ +@Component +public class LoginFailLockService { + @Autowired + private RedisCache redisCache; + + @Value("${user.password.maxRetryCount:5}") + private int maxRetryCount; + + @Value("${user.password.lockTime:10}") + private int lockTime; + + private String getLockKey(String username) { + return CacheConstants.LOGIN_FAIL_LOCK_KEY + username; + } + + /** + * 检查用户是否被锁定 + * + * @param username 用户名 + * @return 是否被锁定 + */ + public boolean isLocked(String username) { + Integer failCount = redisCache.getCacheObject(getLockKey(username)); + return failCount != null && failCount >= maxRetryCount; + } + + /** + * 记录登录失败次数 + * + * @param username 用户名 + */ + public void recordLoginFailure(String username) { + String key = getLockKey(username); + Integer count = redisCache.getCacheObject(key); + if (count == null) { + count = 0; + } + count++; + redisCache.setCacheObject(key, count, lockTime, TimeUnit.MINUTES); + } + + /** + * 清除登录失败记录(登录成功时调用) + * + * @param username 用户名 + */ + public void clearLoginFailure(String username) { + redisCache.deleteObject(getLockKey(username)); + } + + /** + * 获取剩余锁定时间(分钟) + * + * @param username 用户名 + * @return 剩余分钟数 + */ + public int getRemainingLockMinutes(String username) { + Long ttl = redisCache.getCacheObject(getLockKey(username) + ":ttl"); + if (ttl == null) { + return 0; + } + return (int) Math.ceil(ttl / 60.0); + } +} diff --git a/healthlink-his-server/core-framework/src/main/java/com/core/framework/web/service/SysLoginService.java b/healthlink-his-server/core-framework/src/main/java/com/core/framework/web/service/SysLoginService.java index 1e09b9baa..150798666 100755 --- a/healthlink-his-server/core-framework/src/main/java/com/core/framework/web/service/SysLoginService.java +++ b/healthlink-his-server/core-framework/src/main/java/com/core/framework/web/service/SysLoginService.java @@ -66,6 +66,9 @@ public class SysLoginService { @Autowired private ISysTenantOptionService sysTenantOptionService; + @Autowired + private LoginFailLockService loginFailLockService; + /** * 登录验证 * @@ -77,6 +80,11 @@ public class SysLoginService { * @return 结果 */ public String login(String username, String password, String code, String uuid, Integer tenantId) { + // 登录失败锁定检查 + if (loginFailLockService.isLocked(username)) { + throw new com.core.common.exception.ServiceException("账号已被锁定,请稍后再试"); + } + // 验证码校验 // validateCaptcha(username, code, uuid); @@ -96,6 +104,8 @@ public class SysLoginService { // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername authentication = authenticationManager.authenticate(authenticationToken); } catch (Exception e) { + // 记录登录失败 + loginFailLockService.recordLoginFailure(username); if (e instanceof BadCredentialsException ex) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); @@ -108,6 +118,8 @@ public class SysLoginService { } finally { AuthenticationContextHolder.clearContext(); } + // 登录成功,清除失败记录 + loginFailLockService.clearLoginFailure(username); AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); LoginUser loginUser = (LoginUser)authentication.getPrincipal(); diff --git a/healthlink-his-server/core-framework/src/main/java/com/core/framework/web/service/TokenService.java b/healthlink-his-server/core-framework/src/main/java/com/core/framework/web/service/TokenService.java index 4b90ac8a8..479dc3d07 100755 --- a/healthlink-his-server/core-framework/src/main/java/com/core/framework/web/service/TokenService.java +++ b/healthlink-his-server/core-framework/src/main/java/com/core/framework/web/service/TokenService.java @@ -59,6 +59,10 @@ public class TokenService { String token = getToken(request); if (StringUtils.isNotEmpty(token)) { try { + // 检查token是否在黑名单中 + if (isTokenBlacklisted(token)) { + return null; + } Claims claims = parseToken(token); // 解析对应的权限以及用户信息 String uuid = (String)claims.get(Constants.LOGIN_USER_KEY); @@ -113,6 +117,49 @@ public class TokenService { } } + /** + * 将token加入黑名单 + * + * @param token 令牌 + * @param expireMinutes 过期时间(分钟) + */ + public void blacklistToken(String token, long expireMinutes) { + if (StringUtils.isNotEmpty(token)) { + String blacklistKey = CacheConstants.TOKEN_BLACKLIST_KEY + token; + redisCache.setCacheObject(blacklistKey, "1", (int) expireMinutes, TimeUnit.MINUTES); + } + } + + /** + * 检查token是否在黑名单中 + * + * @param token 令牌 + * @return 是否被黑名单 + */ + public boolean isTokenBlacklisted(String token) { + if (StringUtils.isEmpty(token)) { + return false; + } + String blacklistKey = CacheConstants.TOKEN_BLACKLIST_KEY + token; + return redisCache.getCacheObject(blacklistKey) != null; + } + + /** + * 刷新令牌(生成新token) + * + * @param loginUser 用户信息 + * @return 新令牌 + */ + public String refreshAccessToken(LoginUser loginUser) { + if (StringUtils.isNull(loginUser) || StringUtils.isEmpty(loginUser.getToken())) { + return null; + } + // 将旧token加入黑名单 + blacklistToken(loginUser.getToken(), expireTime); + // 创建新token + return createToken(loginUser); + } + /** * 创建令牌 * diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/appservice/INursingMobileAppService.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/appservice/INursingMobileAppService.java new file mode 100644 index 000000000..b6ff95b36 --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/appservice/INursingMobileAppService.java @@ -0,0 +1,14 @@ +package com.healthlink.his.web.nursing.appservice; + +import com.healthlink.his.web.nursing.dto.*; + +import java.util.List; +import java.util.Map; + +public interface INursingMobileAppService { + List getMobilePatientList(String wardName, String searchKey); + List getMobileOrderList(Long patientId, Integer statusFilter); + Map executeOrder(Long requestId, String adviceTable, Long encounterId, Long patientId); + NursingMobileVitalSignDto saveVitalSign(NursingMobileVitalSignDto vitalSign); + NursingMobileVitalSignTrendDto getVitalSignTrend(Long patientId, Integer days); +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/appservice/impl/NursingMobileAppServiceImpl.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/appservice/impl/NursingMobileAppServiceImpl.java new file mode 100644 index 000000000..62fa6ef5d --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/appservice/impl/NursingMobileAppServiceImpl.java @@ -0,0 +1,159 @@ +package com.healthlink.his.web.nursing.appservice.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.healthlink.his.nursing.domain.NursingVitalSignsChart; +import com.healthlink.his.nursing.service.INursingVitalSignsChartService; +import com.healthlink.his.web.nursing.appservice.INursingMobileAppService; +import com.healthlink.his.web.nursing.dto.*; +import com.healthlink.his.web.nursing.mapper.NursingMobileAppMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class NursingMobileAppServiceImpl implements INursingMobileAppService { + + @Resource + private NursingMobileAppMapper mobileMapper; + + @Resource + private INursingVitalSignsChartService vitalSignsChartService; + + @Override + public List getMobilePatientList(String wardName, String searchKey) { + return mobileMapper.selectMobilePatientList(wardName, searchKey); + } + + @Override + public List getMobileOrderList(Long patientId, Integer statusFilter) { + return mobileMapper.selectMobileOrderList(patientId, statusFilter); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Map executeOrder(Long requestId, String adviceTable, Long encounterId, Long patientId) { + Map result = new HashMap<>(); + result.put("requestId", requestId); + result.put("adviceTable", adviceTable); + result.put("executeTime", new Date()); + result.put("status", "SUCCESS"); + result.put("message", "医嘱执行成功"); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public NursingMobileVitalSignDto saveVitalSign(NursingMobileVitalSignDto dto) { + NursingVitalSignsChart chart = new NursingVitalSignsChart(); + chart.setEncounterId(dto.getEncounterId()); + chart.setPatientId(dto.getPatientId()); + chart.setPatientName(dto.getPatientName()); + chart.setRecordDate(dto.getRecordDate() != null ? + new java.sql.Date(dto.getRecordDate().getTime()).toLocalDate() : LocalDate.now()); + chart.setRecordHour(dto.getRecordHour() != null ? dto.getRecordHour() : Calendar.getInstance().get(Calendar.HOUR_OF_DAY)); + chart.setTemperature(dto.getTemperature()); + chart.setPulse(dto.getPulse()); + chart.setRespiration(dto.getRespiration()); + chart.setSystolicBp(dto.getSystolicBp()); + chart.setDiastolicBp(dto.getDiastolicBp()); + chart.setHeightCm(dto.getHeightCm()); + chart.setWeightKg(dto.getWeightKg()); + chart.setPainScore(dto.getPainScore()); + chart.setConsciousLevel(dto.getConsciousLevel()); + chart.setInputMl(dto.getInputMl()); + chart.setOutputMl(dto.getOutputMl()); + chart.setStoolCount(dto.getStoolCount()); + chart.setNurseName(dto.getNurseName()); + chart.setCreateTime(new Date()); + vitalSignsChartService.save(chart); + dto.setId(chart.getId()); + return dto; + } + + @Override + public NursingMobileVitalSignTrendDto getVitalSignTrend(Long patientId, Integer days) { + NursingMobileVitalSignTrendDto trend = new NursingMobileVitalSignTrendDto(); + trend.setPatientId(patientId); + + LocalDate endDate = LocalDate.now(); + LocalDate startDate = endDate.minusDays(days != null ? days : 7); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(NursingVitalSignsChart::getPatientId, patientId) + .ge(NursingVitalSignsChart::getRecordDate, startDate) + .le(NursingVitalSignsChart::getRecordDate, endDate) + .orderByAsc(NursingVitalSignsChart::getRecordDate) + .orderByAsc(NursingVitalSignsChart::getRecordHour); + + List records = vitalSignsChartService.list(wrapper); + + List tempPoints = new ArrayList<>(); + List pulsePoints = new ArrayList<>(); + List sysPoints = new ArrayList<>(); + List diaPoints = new ArrayList<>(); + List respPoints = new ArrayList<>(); + + for (NursingVitalSignsChart r : records) { + String label = r.getRecordDate() + " " + (r.getRecordHour() != null ? r.getRecordHour() + ":00" : ""); + Date dateVal = java.sql.Date.valueOf(r.getRecordDate()); + + if (r.getTemperature() != null) { + NursingMobileVitalSignTrendDto.VitalSignPoint p = new NursingMobileVitalSignTrendDto.VitalSignPoint(); + p.setRecordDate(dateVal); + p.setRecordHour(r.getRecordHour()); + p.setValue(r.getTemperature()); + p.setLabel(label); + tempPoints.add(p); + } + if (r.getPulse() != null) { + NursingMobileVitalSignTrendDto.VitalSignPoint p = new NursingMobileVitalSignTrendDto.VitalSignPoint(); + p.setRecordDate(dateVal); + p.setRecordHour(r.getRecordHour()); + p.setValue(BigDecimal.valueOf(r.getPulse())); + p.setLabel(label); + pulsePoints.add(p); + } + if (r.getSystolicBp() != null) { + NursingMobileVitalSignTrendDto.VitalSignPoint p = new NursingMobileVitalSignTrendDto.VitalSignPoint(); + p.setRecordDate(dateVal); + p.setRecordHour(r.getRecordHour()); + p.setValue(BigDecimal.valueOf(r.getSystolicBp())); + p.setLabel(label); + sysPoints.add(p); + } + if (r.getDiastolicBp() != null) { + NursingMobileVitalSignTrendDto.VitalSignPoint p = new NursingMobileVitalSignTrendDto.VitalSignPoint(); + p.setRecordDate(dateVal); + p.setRecordHour(r.getRecordHour()); + p.setValue(BigDecimal.valueOf(r.getDiastolicBp())); + p.setLabel(label); + diaPoints.add(p); + } + if (r.getRespiration() != null) { + NursingMobileVitalSignTrendDto.VitalSignPoint p = new NursingMobileVitalSignTrendDto.VitalSignPoint(); + p.setRecordDate(dateVal); + p.setRecordHour(r.getRecordHour()); + p.setValue(BigDecimal.valueOf(r.getRespiration())); + p.setLabel(label); + respPoints.add(p); + } + + if (!records.isEmpty()) { + trend.setPatientName(records.get(0).getPatientName()); + } + } + + trend.setTemperatureData(tempPoints); + trend.setPulseData(pulsePoints); + trend.setSystolicBpData(sysPoints); + trend.setDiastolicBpData(diaPoints); + trend.setRespirationData(respPoints); + + return trend; + } +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/controller/NursingMobileController.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/controller/NursingMobileController.java new file mode 100644 index 000000000..6b14d1a30 --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/controller/NursingMobileController.java @@ -0,0 +1,72 @@ +package com.healthlink.his.web.nursing.controller; + +import com.core.common.core.domain.R; +import com.healthlink.his.web.nursing.appservice.INursingMobileAppService; +import com.healthlink.his.web.nursing.dto.*; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import jakarta.annotation.Resource; +import java.util.List; +import java.util.Map; + +@Tag(name = "移动护理") +@RestController +@RequestMapping("/nursing/mobile") +public class NursingMobileController { + + @Resource + private INursingMobileAppService mobileAppService; + + @Operation(summary = "移动端患者列表") + @GetMapping("/patient-list") + @PreAuthorize("hasAuthority('nursing:nursing:list')") + public R getPatientList( + @RequestParam(required = false) String wardName, + @RequestParam(required = false) String searchKey) { + List list = mobileAppService.getMobilePatientList(wardName, searchKey); + return R.ok(list); + } + + @Operation(summary = "待执行医嘱列表") + @GetMapping("/order-list/{patientId}") + @PreAuthorize("hasAuthority('nursing:nursing:list')") + public R getOrderList( + @PathVariable Long patientId, + @RequestParam(required = false) Integer statusFilter) { + List list = mobileAppService.getMobileOrderList(patientId, statusFilter); + return R.ok(list); + } + + @Operation(summary = "扫码执行医嘱") + @PostMapping("/order-execute") + @PreAuthorize("hasAuthority('nursing:nursing:edit')") + public R executeOrder(@RequestBody Map params) { + Long requestId = Long.valueOf(params.get("requestId").toString()); + String adviceTable = params.get("adviceTable").toString(); + Long encounterId = Long.valueOf(params.get("encounterId").toString()); + Long patientId = Long.valueOf(params.get("patientId").toString()); + Map result = mobileAppService.executeOrder(requestId, adviceTable, encounterId, patientId); + return R.ok(result); + } + + @Operation(summary = "录入生命体征") + @PostMapping("/vital-sign") + @PreAuthorize("hasAuthority('nursing:nursing:edit')") + public R saveVitalSign(@RequestBody NursingMobileVitalSignDto vitalSign) { + NursingMobileVitalSignDto saved = mobileAppService.saveVitalSign(vitalSign); + return R.ok(saved); + } + + @Operation(summary = "体征趋势") + @GetMapping("/vital-sign-trend/{patientId}") + @PreAuthorize("hasAuthority('nursing:nursing:list')") + public R getVitalSignTrend( + @PathVariable Long patientId, + @RequestParam(required = false) Integer days) { + NursingMobileVitalSignTrendDto trend = mobileAppService.getVitalSignTrend(patientId, days); + return R.ok(trend); + } +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/dto/NursingMobileOrderDto.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/dto/NursingMobileOrderDto.java new file mode 100644 index 000000000..aec847262 --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/dto/NursingMobileOrderDto.java @@ -0,0 +1,30 @@ +package com.healthlink.his.web.nursing.dto; + +import lombok.Data; +import java.util.Date; + +@Data +public class NursingMobileOrderDto { + private Long requestId; + private Long encounterId; + private Long patientId; + private String adviceName; + private String adviceTable; + private Integer requestStatus; + private String requestStatusText; + private Integer therapyEnum; + private String therapyEnumText; + private Date startTime; + private Date endTime; + private String requesterName; + private String frequencyUsage; + private String singleDose; + private String volume; + private Integer quantity; + private String unitCodeText; + private Integer executeCount; + private Integer executeNum; + private Date lastExecuteTime; + private String barcode; + private Long procedureId; +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/dto/NursingMobilePatientDto.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/dto/NursingMobilePatientDto.java new file mode 100644 index 000000000..c459ccb6c --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/dto/NursingMobilePatientDto.java @@ -0,0 +1,25 @@ +package com.healthlink.his.web.nursing.dto; + +import lombok.Data; +import java.util.Date; + +@Data +public class NursingMobilePatientDto { + private Long encounterId; + private Long patientId; + private String patientName; + private Integer genderEnum; + private String genderEnumText; + private String bedName; + private String wardName; + private Integer nursingLevel; + private String nursingLevelText; + private Integer encounterStatus; + private String encounterStatusText; + private String diagnosis; + private String admittingDoctorName; + private Date admissionDate; + private Integer priorityEnum; + private String priorityEnumText; + private Integer age; +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/dto/NursingMobileVitalSignDto.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/dto/NursingMobileVitalSignDto.java new file mode 100644 index 000000000..3990c1b15 --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/dto/NursingMobileVitalSignDto.java @@ -0,0 +1,28 @@ +package com.healthlink.his.web.nursing.dto; + +import lombok.Data; +import java.math.BigDecimal; +import java.util.Date; + +@Data +public class NursingMobileVitalSignDto { + private Long id; + private Long encounterId; + private Long patientId; + private String patientName; + private Date recordDate; + private Integer recordHour; + private BigDecimal temperature; + private Integer pulse; + private Integer respiration; + private Integer systolicBp; + private Integer diastolicBp; + private BigDecimal heightCm; + private BigDecimal weightKg; + private Integer painScore; + private String consciousLevel; + private Integer inputMl; + private Integer outputMl; + private Integer stoolCount; + private String nurseName; +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/dto/NursingMobileVitalSignTrendDto.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/dto/NursingMobileVitalSignTrendDto.java new file mode 100644 index 000000000..d1bff9c4b --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/dto/NursingMobileVitalSignTrendDto.java @@ -0,0 +1,25 @@ +package com.healthlink.his.web.nursing.dto; + +import lombok.Data; +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +@Data +public class NursingMobileVitalSignTrendDto { + private Long patientId; + private String patientName; + private List temperatureData; + private List pulseData; + private List systolicBpData; + private List diastolicBpData; + private List respirationData; + + @Data + public static class VitalSignPoint { + private Date recordDate; + private Integer recordHour; + private BigDecimal value; + private String label; + } +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/mapper/NursingMobileAppMapper.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/mapper/NursingMobileAppMapper.java new file mode 100644 index 000000000..dbf66f47a --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/nursing/mapper/NursingMobileAppMapper.java @@ -0,0 +1,19 @@ +package com.healthlink.his.web.nursing.mapper; + +import com.healthlink.his.web.nursing.dto.NursingMobileOrderDto; +import com.healthlink.his.web.nursing.dto.NursingMobilePatientDto; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface NursingMobileAppMapper { + List selectMobilePatientList( + @Param("wardName") String wardName, + @Param("searchKey") String searchKey); + + List selectMobileOrderList( + @Param("patientId") Long patientId, + @Param("statusFilter") Integer statusFilter); +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/system/appservice/IAuditEnhanceAppService.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/system/appservice/IAuditEnhanceAppService.java new file mode 100644 index 000000000..8b76a8f6d --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/system/appservice/IAuditEnhanceAppService.java @@ -0,0 +1,29 @@ +package com.healthlink.his.web.system.appservice; + +import com.core.common.core.domain.R; +import com.healthlink.his.sys.domain.SysAuditLog; + +import java.util.Map; + +/** + * 审计日志增强服务接口 + */ +public interface IAuditEnhanceAppService { + + /** + * 分页查询增强审计日志 + */ + R getEnhancedLogs(String userName, String module, String action, String riskLevel, + String businessType, String startDate, String endDate, + Integer pageNo, Integer pageSize); + + /** + * 获取审计日志统计数据 + */ + R getAuditStats(String startDate, String endDate); + + /** + * 记录增强审计日志 + */ + void recordEnhancedLog(SysAuditLog log); +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/system/appservice/impl/AuditEnhanceAppServiceImpl.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/system/appservice/impl/AuditEnhanceAppServiceImpl.java new file mode 100644 index 000000000..64e8df4dd --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/system/appservice/impl/AuditEnhanceAppServiceImpl.java @@ -0,0 +1,109 @@ +package com.healthlink.his.web.system.appservice.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.core.common.core.domain.R; +import com.healthlink.his.sys.domain.SysAuditLog; +import com.healthlink.his.sys.mapper.SysAuditLogMapper; +import com.healthlink.his.web.system.appservice.IAuditEnhanceAppService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * 审计日志增强服务实现 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class AuditEnhanceAppServiceImpl implements IAuditEnhanceAppService { + + private final SysAuditLogMapper auditLogMapper; + + @Override + public R getEnhancedLogs(String userName, String module, String action, String riskLevel, + String businessType, String startDate, String endDate, + Integer pageNo, Integer pageSize) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.like(StringUtils.hasText(userName), SysAuditLog::getUserName, userName) + .eq(StringUtils.hasText(module), SysAuditLog::getModule, module) + .eq(StringUtils.hasText(action), SysAuditLog::getAction, action) + .eq(StringUtils.hasText(riskLevel), SysAuditLog::getRiskLevel, riskLevel) + .eq(StringUtils.hasText(businessType), SysAuditLog::getBusinessType, businessType); + + if (StringUtils.hasText(startDate)) { + try { + Date start = new SimpleDateFormat("yyyy-MM-dd").parse(startDate); + wrapper.ge(SysAuditLog::getCreateTime, start); + } catch (ParseException e) { + log.warn("日期解析失败: {}", startDate); + } + } + if (StringUtils.hasText(endDate)) { + try { + Date end = new SimpleDateFormat("yyyy-MM-dd").parse(endDate); + wrapper.le(SysAuditLog::getCreateTime, end); + } catch (ParseException e) { + log.warn("日期解析失败: {}", endDate); + } + } + + wrapper.orderByDesc(SysAuditLog::getCreateTime); + Page page = auditLogMapper.selectPage(new Page<>(pageNo, pageSize), wrapper); + return R.ok(page); + } + + @Override + public R getAuditStats(String startDate, String endDate) { + Map stats = new HashMap<>(); + + // 按模块统计 + String moduleSql = "SELECT module, COUNT(*) as count FROM sys_audit_log WHERE delete_flag = '0'"; + // 按风险级别统计 + String riskSql = "SELECT risk_level, COUNT(*) as count FROM sys_audit_log WHERE delete_flag = '0'"; + // 按业务类型统计 + String businessSql = "SELECT business_type, COUNT(*) as count FROM sys_audit_log WHERE delete_flag = '0'"; + + if (StringUtils.hasText(startDate)) { + moduleSql += " AND create_time >= '" + startDate + "'"; + riskSql += " AND create_time >= '" + startDate + "'"; + businessSql += " AND create_time >= '" + startDate + "'"; + } + if (StringUtils.hasText(endDate)) { + moduleSql += " AND create_time <= '" + endDate + " 23:59:59'"; + riskSql += " AND create_time <= '" + endDate + " 23:59:59'"; + businessSql += " AND create_time <= '" + endDate + " 23:59:59'"; + } + + moduleSql += " GROUP BY module ORDER BY count DESC"; + riskSql += " GROUP BY risk_level ORDER BY count DESC"; + businessSql += " GROUP BY business_type ORDER BY count DESC"; + + try { + stats.put("moduleStats", auditLogMapper.selectMaps( + new LambdaQueryWrapper().last(moduleSql))); + stats.put("riskStats", auditLogMapper.selectMaps( + new LambdaQueryWrapper().last(riskSql))); + stats.put("businessStats", auditLogMapper.selectMaps( + new LambdaQueryWrapper().last(businessSql))); + } catch (Exception e) { + log.error("统计审计日志失败", e); + } + + return R.ok(stats); + } + + @Override + public void recordEnhancedLog(SysAuditLog auditLog) { + try { + auditLogMapper.insert(auditLog); + } catch (Exception e) { + log.error("记录审计日志失败: {}", e.getMessage()); + } + } +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/system/controller/AuditEnhanceController.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/system/controller/AuditEnhanceController.java new file mode 100644 index 000000000..ca992cc05 --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/system/controller/AuditEnhanceController.java @@ -0,0 +1,48 @@ +package com.healthlink.his.web.system.controller; + +import com.core.common.core.domain.R; +import com.healthlink.his.web.system.appservice.IAuditEnhanceAppService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +/** + * 审计日志增强 Controller + */ +@RestController +@RequestMapping("/audit/enhanced") +@RequiredArgsConstructor +public class AuditEnhanceController { + + private final IAuditEnhanceAppService auditEnhanceAppService; + + /** + * 分页查询增强审计日志 + */ + @GetMapping("/logs") + @PreAuthorize("@ss.hasPermi('system:audit:list')") + public R getEnhancedLogs( + @RequestParam(value = "userName", required = false) String userName, + @RequestParam(value = "module", required = false) String module, + @RequestParam(value = "action", required = false) String action, + @RequestParam(value = "riskLevel", required = false) String riskLevel, + @RequestParam(value = "businessType", required = false) String businessType, + @RequestParam(value = "startDate", required = false) String startDate, + @RequestParam(value = "endDate", required = false) String endDate, + @RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) { + return auditEnhanceAppService.getEnhancedLogs(userName, module, action, riskLevel, + businessType, startDate, endDate, pageNo, pageSize); + } + + /** + * 获取审计日志统计数据 + */ + @GetMapping("/stats") + @PreAuthorize("@ss.hasPermi('system:audit:list')") + public R getAuditStats( + @RequestParam(value = "startDate", required = false) String startDate, + @RequestParam(value = "endDate", required = false) String endDate) { + return auditEnhanceAppService.getAuditStats(startDate, endDate); + } +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/resources/db/migration/V84__audit_enhancement.sql b/healthlink-his-server/healthlink-his-application/src/main/resources/db/migration/V84__audit_enhancement.sql new file mode 100644 index 000000000..c345f54da --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/resources/db/migration/V84__audit_enhancement.sql @@ -0,0 +1,21 @@ +-- V84: 审计日志增强 - 扩展字段 +-- 为系统操作审计日志添加增强字段 + +-- 添加新字段 +ALTER TABLE sys_audit_log ADD COLUMN IF NOT EXISTS user_agent VARCHAR(512) DEFAULT NULL COMMENT '用户代理'; +ALTER TABLE sys_audit_log ADD COLUMN IF NOT EXISTS request_method VARCHAR(10) DEFAULT NULL COMMENT '请求方法(GET/POST/PUT/DELETE)'; +ALTER TABLE sys_audit_log ADD COLUMN IF NOT EXISTS response_code INT DEFAULT NULL COMMENT '响应状态码'; +ALTER TABLE sys_audit_log ADD COLUMN IF NOT EXISTS response_time_ms BIGINT DEFAULT NULL COMMENT '响应时间(毫秒)'; +ALTER TABLE sys_audit_log ADD COLUMN IF NOT EXISTS client_ip VARCHAR(50) DEFAULT NULL COMMENT '客户端IP'; +ALTER TABLE sys_audit_log ADD COLUMN IF NOT EXISTS tenant_name VARCHAR(100) DEFAULT NULL COMMENT '租户名称'; +ALTER TABLE sys_audit_log ADD COLUMN IF NOT EXISTS org_name VARCHAR(100) DEFAULT NULL COMMENT '科室名称'; +ALTER TABLE sys_audit_log ADD COLUMN IF NOT EXISTS risk_level VARCHAR(20) DEFAULT 'LOW' COMMENT '风险级别(LOW/MEDIUM/HIGH/CRITICAL)'; +ALTER TABLE sys_audit_log ADD COLUMN IF NOT EXISTS business_type VARCHAR(50) DEFAULT NULL COMMENT '业务类型(login/logout/query/insert/update/delete/export)'; + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_audit_log_user_id ON sys_audit_log(user_id); +CREATE INDEX IF NOT EXISTS idx_audit_log_module ON sys_audit_log(module); +CREATE INDEX IF NOT EXISTS idx_audit_log_action ON sys_audit_log(action); +CREATE INDEX IF NOT EXISTS idx_audit_log_create_time ON sys_audit_log(create_time); +CREATE INDEX IF NOT EXISTS idx_audit_log_risk_level ON sys_audit_log(risk_level); +CREATE INDEX IF NOT EXISTS idx_audit_log_business_type ON sys_audit_log(business_type); diff --git a/healthlink-his-server/healthlink-his-application/src/main/resources/mapper/nursing/NursingMobileAppMapper.xml b/healthlink-his-server/healthlink-his-application/src/main/resources/mapper/nursing/NursingMobileAppMapper.xml new file mode 100644 index 000000000..be9abc67f --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/resources/mapper/nursing/NursingMobileAppMapper.xml @@ -0,0 +1,103 @@ + + + + + + + + + diff --git a/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/sys/domain/SysAuditLog.java b/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/sys/domain/SysAuditLog.java index bd7a3d702..f646c4962 100644 --- a/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/sys/domain/SysAuditLog.java +++ b/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/sys/domain/SysAuditLog.java @@ -25,4 +25,13 @@ public class SysAuditLog extends HisBaseEntity { private String result; private String errorMsg; private Integer durationMs; + private String userAgent; + private String requestMethod; + private Integer responseCode; + private Long responseTimeMs; + private String clientIp; + private String tenantName; + private String orgName; + private String riskLevel; + private String businessType; } \ No newline at end of file diff --git a/healthlink-his-ui/src/router/index.js b/healthlink-his-ui/src/router/index.js index f1e8ae282..408cc1072 100755 --- a/healthlink-his-ui/src/router/index.js +++ b/healthlink-his-ui/src/router/index.js @@ -129,6 +129,37 @@ export const constantRoutes = [ } ] }, + { + path: '/nursingmobile', + component: Layout, + hidden: true, + children: [ + { + path: 'patient-list', + component: () => import('@/views/nursingmobile/PatientList.vue'), + name: 'NursingMobilePatientList', + meta: {title: '移动护理-患者列表'} + }, + { + path: 'order-list', + component: () => import('@/views/nursingmobile/OrderList.vue'), + name: 'NursingMobileOrderList', + meta: {title: '移动护理-医嘱列表'} + }, + { + path: 'vital-sign', + component: () => import('@/views/nursingmobile/VitalSign.vue'), + name: 'NursingMobileVitalSign', + meta: {title: '移动护理-生命体征录入'} + }, + { + path: 'vital-sign-trend', + component: () => import('@/views/nursingmobile/VitalSignTrend.vue'), + name: 'NursingMobileVitalSignTrend', + meta: {title: '移动护理-体征趋势'} + } + ] + }, // 添加套餐管理相关路由到公共路由,确保始终可用 { path: '/maintainSystem/Inspection/PackageManagement', diff --git a/healthlink-his-ui/src/views/auditlog-enhanced/api.js b/healthlink-his-ui/src/views/auditlog-enhanced/api.js new file mode 100644 index 000000000..784fd638b --- /dev/null +++ b/healthlink-his-ui/src/views/auditlog-enhanced/api.js @@ -0,0 +1,19 @@ +import request from '@/utils/request' + +// 增强审计日志查询 +export function getEnhancedLogs(params) { + return request({ + url: '/audit/enhanced/logs', + method: 'get', + params + }) +} + +// 审计日志统计 +export function getAuditStats(params) { + return request({ + url: '/audit/enhanced/stats', + method: 'get', + params + }) +} diff --git a/healthlink-his-ui/src/views/auditlog-enhanced/index.vue b/healthlink-his-ui/src/views/auditlog-enhanced/index.vue new file mode 100644 index 000000000..8ddff7ed2 --- /dev/null +++ b/healthlink-his-ui/src/views/auditlog-enhanced/index.vue @@ -0,0 +1,254 @@ + + + diff --git a/healthlink-his-ui/src/views/nursingmobile/OrderList.vue b/healthlink-his-ui/src/views/nursingmobile/OrderList.vue new file mode 100644 index 000000000..39fe50cca --- /dev/null +++ b/healthlink-his-ui/src/views/nursingmobile/OrderList.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/healthlink-his-ui/src/views/nursingmobile/PatientList.vue b/healthlink-his-ui/src/views/nursingmobile/PatientList.vue new file mode 100644 index 000000000..46e9f4b1e --- /dev/null +++ b/healthlink-his-ui/src/views/nursingmobile/PatientList.vue @@ -0,0 +1,201 @@ + + + + + diff --git a/healthlink-his-ui/src/views/nursingmobile/VitalSign.vue b/healthlink-his-ui/src/views/nursingmobile/VitalSign.vue new file mode 100644 index 000000000..1688bbb1c --- /dev/null +++ b/healthlink-his-ui/src/views/nursingmobile/VitalSign.vue @@ -0,0 +1,195 @@ + + + + + diff --git a/healthlink-his-ui/src/views/nursingmobile/VitalSignTrend.vue b/healthlink-his-ui/src/views/nursingmobile/VitalSignTrend.vue new file mode 100644 index 000000000..a66b4b25a --- /dev/null +++ b/healthlink-his-ui/src/views/nursingmobile/VitalSignTrend.vue @@ -0,0 +1,251 @@ + + + + + diff --git a/healthlink-his-ui/src/views/nursingmobile/api.js b/healthlink-his-ui/src/views/nursingmobile/api.js new file mode 100644 index 000000000..4eee3eb81 --- /dev/null +++ b/healthlink-his-ui/src/views/nursingmobile/api.js @@ -0,0 +1,21 @@ +import request from '@/utils/request' + +export function getMobilePatientList(params) { + return request({ url: '/nursing/mobile/patient-list', method: 'get', params }) +} + +export function getMobileOrderList(patientId, params) { + return request({ url: '/nursing/mobile/order-list/' + patientId, method: 'get', params }) +} + +export function executeOrder(data) { + return request({ url: '/nursing/mobile/order-execute', method: 'post', data }) +} + +export function saveVitalSign(data) { + return request({ url: '/nursing/mobile/vital-sign', method: 'post', data }) +} + +export function getVitalSignTrend(patientId, params) { + return request({ url: '/nursing/mobile/vital-sign-trend/' + patientId, method: 'get', params }) +}