feat(V23): 知识库+评估趋势+安全认证+经营分析

V23 Flyway — 4张新表:
- clinical_knowledge_base: 临床知识库(指南/药物/诊断/操作)
- nursing_assessment_trend: 评估趋势数据(评分/风险等级)
- sys_api_auth: 接口安全认证(AppKey/Secret/限流/权限)
- business_analytics: 经营分析日报(收入/成本/利润/床位率)

后端Controller:
- KnowledgeBaseController: 知识库CRUD+全文搜索
- AssessmentTrendController: 评估趋势查询+记录
- ApiAuthController: 接口认证管理(新增/禁用)
- BusinessAnalyticsController: 经营分析+汇总统计

前端页面:
- knowledgebase: 临床知识库(分类搜索)
- assessmenttrend: 评估趋势图数据
- apiauth: 接口安全认证管理
- businessanalytics: 经营分析(收入/成本/利润/患者)

三甲能力清单: 108/150完成(72%), 40项基础(27%), 2项选配未实现(3D重建/消毒供应)
所有模块编译通过 (mvn clean compile -DskipTests)
This commit is contained in:
2026-06-06 17:30:28 +08:00
parent 7292b00186
commit d05ff14258
30 changed files with 634 additions and 7 deletions

View File

@@ -77,11 +77,11 @@
| 3 | **体温单** | 自动生成体温单(三测单) | 护理文书规范 | ⚠️ 基础 |
| 4 | **护理评估** | 入院评估/压疮评估/跌倒评估/营养评估 | 护理安全 | ✅ |
| 5 | **护理记录** | 一般/危重护理记录单 | 病历书写规范 | ✅ |
| 6 | **执行扫码** | 扫码执行医嘱(腕带/药品/标本) | 患者安全目标 | ❌ 缺失 |
| 7 | **交接班** | 护理交接班记录+重点患者提示 | 护理安全 | ❌ 缺失 |
| 6 | **执行扫码** | 扫码执行医嘱(腕带/药品/标本) | 患者安全目标 | ✅ 已完成(V21) |
| 7 | **交接班** | 护理交接班记录+重点患者提示 | 护理安全 | ✅ 已完成(V21) |
| 8 | **压疮预警** | Braden评分→自动预警→干预→跟踪 | 护理质量指标 | ⚠️ 基础 |
| 9 | **跌倒预警** | Morse评分→风险分级→防护措施 | 患者安全目标 | ⚠️ 基础 |
| 10 | **输液管理** | 输液巡视记录+速度监控 | 护理安全 | ❌ 缺失 |
| 10 | **输液管理** | 输液巡视记录+速度监控 | 护理安全 | ✅ 已完成(V21) |
---
@@ -106,7 +106,7 @@
| 8 | **抗菌药物分级** | 非限制/限制/特殊使用级管控 | 抗菌药物管理办法 | ✅ |
| 9 | **DDD监测** | 抗菌药物限定日剂量使用强度监测 | 抗菌药物管理办法 | ✅ |
| 10 | **处方点评工作台** | 自动筛查+人工点评+科室排名 | 处方点评规范 | ✅ |
| 11 | **药品库存联动** | 药品库存不足时提醒 | 基本功能规范 | ❌ 缺失 |
| 11 | **药品库存联动** | 药品库存不足时提醒 | 基本功能规范 | ✅ 已完成(V21) |
| 12 | **处方前置拦截** | 不合理处方必须拦截才能继续 | 处方审核率100% | ⚠️ 部分 |
---
@@ -156,7 +156,7 @@
| 5 | **室内质控** | 质控图+Westgard规则+失控处理 | 质量管理 | ✅ 已完成(V19) |
| 6 | **室间质评** | 参加省级/国家级室间质评 | 质量管理 | ✅ 已完成(V19) |
| 7 | **参考范围** | 按年龄/性别/种族设置参考范围 | 检验规范 | ⚠️ 基础 |
| 8 | **历史结果对比** | 同一患者历次结果趋势图 | 临床决策 | ❌ 缺失 |
| 8 | **历史结果对比** | 同一患者历次结果趋势图 | 临床决策 | ✅ 已完成(V22) |
| 9 | **检验报告打印** | 标准格式报告单打印 | 基本功能规范 | ✅ |
| 10 | **危急值统计** | 危急值检出率/处理及时率统计 | 评审指标 | ✅ |
@@ -179,7 +179,7 @@
| 4 | **图文报告** | 结构化报告+图像标注 | 检查规范 | ⚠️ 基础 |
| 5 | **报告审核** | 书写→审核→发布流程 | 检查规范 | ✅ |
| 6 | **紧急报告** | 急诊检查优先出报告 | 患者安全 | ✅ 已完成(V19) |
| 7 | **影像对比** | 历史影像对比查看 | 临床决策 | ❌ 缺失 |
| 7 | **影像对比** | 历史影像对比查看 | 临床决策 | ✅ 已完成(V22) |
| 8 | **3D重建** | 三维图像重建(选配) | 高级功能 | ❌ 缺失 |
| 9 | **DICOM打印** | 胶片打印 | 基本功能规范 | ⚠️ 基础 |
| 10 | **检查统计** | 检查量/阳性率/报告时效统计 | 评审指标 | ✅ 已完成(V19) |
@@ -278,7 +278,7 @@
| 7 | **评估提醒** | 按评估频率自动提醒 | 护理质量 | ✅ 已完成(V18) |
| 8 | **评估趋势** | 历次评估结果趋势图 | 临床决策 | ❌ 缺失 |
| 9 | **护理计划** | 基于评估结果自动生成护理计划 | 护理规范 | ✅ 已完成(V18) |
| 10 | **护理质量指标** | 护理敏感指标自动采集+上报 | 护理质量 | ❌ 缺失 |
| 10 | **护理质量指标** | 护理敏感指标自动采集+上报 | 护理质量 | ✅ 已完成(V22) |
---

View File

@@ -0,0 +1,56 @@
package com.healthlink.his.web.clinical.controller;
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.clinical.domain.ClinicalKnowledgeBase;
import com.healthlink.his.clinical.service.IClinicalKnowledgeBaseService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/knowledge-base")
@Slf4j
@AllArgsConstructor
public class KnowledgeBaseController {
private final IClinicalKnowledgeBaseService kbService;
@GetMapping("/page")
public R<?> getPage(
@RequestParam(value = "category", required = false) String category,
@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<ClinicalKnowledgeBase> w = new LambdaQueryWrapper<>();
w.eq(StringUtils.hasText(category), ClinicalKnowledgeBase::getCategory, category)
.and(StringUtils.hasText(keyword), q -> q.like(ClinicalKnowledgeBase::getTitle, keyword)
.or().like(ClinicalKnowledgeBase::getKeywords, keyword))
.orderByDesc(ClinicalKnowledgeBase::getCreateTime);
return R.ok(kbService.page(new Page<>(pageNo, pageSize), w));
}
@GetMapping("/search")
public R<?> search(@RequestParam String keyword) {
LambdaQueryWrapper<ClinicalKnowledgeBase> w = new LambdaQueryWrapper<>();
w.eq(ClinicalKnowledgeBase::getStatus, "ACTIVE")
.and(q -> q.like(ClinicalKnowledgeBase::getTitle, keyword)
.or().like(ClinicalKnowledgeBase::getKeywords, keyword)
.or().like(ClinicalKnowledgeBase::getContent, keyword));
return R.ok(kbService.list(w));
}
@PostMapping("/add")
@Transactional(rollbackFor = Exception.class)
public R<?> add(@RequestBody ClinicalKnowledgeBase kb) {
kb.setStatus("ACTIVE");
kb.setCreateTime(new Date());
kbService.save(kb);
return R.ok(kb);
}
}

View File

@@ -0,0 +1,40 @@
package com.healthlink.his.web.nursing.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.core.common.core.domain.R;
import com.healthlink.his.nursing.domain.NursingAssessmentTrend;
import com.healthlink.his.nursing.service.INursingAssessmentTrendService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/assessment-trend")
@Slf4j
@AllArgsConstructor
public class AssessmentTrendController {
private final INursingAssessmentTrendService trendService;
@GetMapping("/trend")
public R<?> getTrend(
@RequestParam Long encounterId,
@RequestParam(required = false) String assessmentType) {
LambdaQueryWrapper<NursingAssessmentTrend> w = new LambdaQueryWrapper<>();
w.eq(NursingAssessmentTrend::getEncounterId, encounterId)
.eq(assessmentType != null, NursingAssessmentTrend::getAssessmentType, assessmentType)
.orderByAsc(NursingAssessmentTrend::getAssessTime);
return R.ok(trendService.list(w));
}
@PostMapping("/add")
@Transactional(rollbackFor = Exception.class)
public R<?> addTrend(@RequestBody NursingAssessmentTrend trend) {
trend.setCreateTime(new Date());
trendService.save(trend);
return R.ok(trend);
}
}

View File

@@ -0,0 +1,62 @@
package com.healthlink.his.web.quality.controller;
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.quality.domain.BusinessAnalytics;
import com.healthlink.his.quality.service.IBusinessAnalyticsService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.*;
@RestController
@RequestMapping("/business-analytics")
@Slf4j
@AllArgsConstructor
public class BusinessAnalyticsController {
private final IBusinessAnalyticsService analyticsService;
@GetMapping("/page")
public R<?> getPage(
@RequestParam(value = "departmentName", required = false) String deptName,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<BusinessAnalytics> w = new LambdaQueryWrapper<>();
w.like(StringUtils.hasText(deptName), BusinessAnalytics::getDepartmentName, deptName)
.orderByDesc(BusinessAnalytics::getStatDate);
return R.ok(analyticsService.page(new Page<>(pageNo, pageSize), w));
}
@PostMapping("/add")
@Transactional(rollbackFor = Exception.class)
public R<?> add(@RequestBody BusinessAnalytics analytics) {
analytics.setCreateTime(new Date());
analyticsService.save(analytics);
return R.ok(analytics);
}
@GetMapping("/summary")
public R<?> getSummary() {
Map<String, Object> summary = new HashMap<>();
List<BusinessAnalytics> list = analyticsService.list();
BigDecimal totalRevenue = BigDecimal.ZERO, totalCost = BigDecimal.ZERO;
int totalPatients = 0;
for (BusinessAnalytics ba : list) {
totalRevenue = totalRevenue.add(ba.getRevenue() != null ? ba.getRevenue() : BigDecimal.ZERO);
totalCost = totalCost.add(ba.getCost() != null ? ba.getCost() : BigDecimal.ZERO);
totalPatients += ba.getPatientCount() != null ? ba.getPatientCount() : 0;
}
summary.put("totalRecords", list.size());
summary.put("totalRevenue", totalRevenue);
summary.put("totalCost", totalCost);
summary.put("totalProfit", totalRevenue.subtract(totalCost));
summary.put("totalPatients", totalPatients);
return R.ok(summary);
}
}

View File

@@ -0,0 +1,56 @@
package com.healthlink.his.web.system.controller;
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.SysApiAuth;
import com.healthlink.his.sys.service.ISysApiAuthService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/api-auth")
@Slf4j
@AllArgsConstructor
public class ApiAuthController {
private final ISysApiAuthService authService;
@GetMapping("/page")
public R<?> getPage(
@RequestParam(value = "appName", required = false) String appName,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<SysApiAuth> w = new LambdaQueryWrapper<>();
w.like(StringUtils.hasText(appName), SysApiAuth::getAppName, appName)
.orderByDesc(SysApiAuth::getCreateTime);
return R.ok(authService.page(new Page<>(pageNo, pageSize), w));
}
@PostMapping("/add")
@Transactional(rollbackFor = Exception.class)
public R<?> add(@RequestBody SysApiAuth auth) {
auth.setStatus(0);
auth.setCreateTime(new Date());
auth.setAppKey("HL-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase());
auth.setAppSecret(UUID.randomUUID().toString().replace("-", ""));
authService.save(auth);
return R.ok(auth);
}
@PostMapping("/disable")
@Transactional(rollbackFor = Exception.class)
public R<?> disable(@RequestParam Long id) {
SysApiAuth auth = authService.getById(id);
if (auth == null) return R.fail("认证不存在");
auth.setStatus(1);
auth.setUpdateTime(new Date());
authService.updateById(auth);
return R.ok();
}
}

View File

@@ -0,0 +1,83 @@
-- V23: 知识库+评估趋势+安全认证+经营分析
-- 1. 临床知识库
CREATE TABLE IF NOT EXISTS clinical_knowledge_base (
id BIGSERIAL PRIMARY KEY,
category VARCHAR(50) NOT NULL,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
keywords VARCHAR(500),
source VARCHAR(200),
version VARCHAR(20),
status VARCHAR(20) DEFAULT 'ACTIVE',
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE clinical_knowledge_base IS '临床知识库';
COMMENT ON COLUMN clinical_knowledge_base.category IS '类别(guideline指南/drug药物/diagnosis诊断/procedure操作)';
CREATE INDEX idx_ckb_category ON clinical_knowledge_base(category);
-- 2. 评估趋势数据
CREATE TABLE IF NOT EXISTS nursing_assessment_trend (
id BIGSERIAL PRIMARY KEY,
encounter_id BIGINT NOT NULL,
patient_id BIGINT NOT NULL,
assessment_type VARCHAR(50) NOT NULL,
score DECIMAL(8,2),
risk_level VARCHAR(20),
assess_time TIMESTAMP NOT NULL,
assessor_name VARCHAR(50),
detail_json TEXT,
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE nursing_assessment_trend IS '护理评估趋势数据';
CREATE INDEX idx_nat_encounter ON nursing_assessment_trend(encounter_id);
CREATE INDEX idx_nat_type ON nursing_assessment_trend(assessment_type);
-- 3. 接口安全认证
CREATE TABLE IF NOT EXISTS sys_api_auth (
id BIGSERIAL PRIMARY KEY,
app_name VARCHAR(100) NOT NULL,
app_key VARCHAR(100) NOT NULL,
app_secret VARCHAR(200) NOT NULL,
permissions TEXT,
rate_limit INT DEFAULT 1000,
expire_time TIMESTAMP,
status INT NOT NULL DEFAULT 0,
last_access_time TIMESTAMP,
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE sys_api_auth IS '接口安全认证';
COMMENT ON COLUMN sys_api_auth.status IS '状态(0启用 1禁用)';
CREATE UNIQUE INDEX idx_saa_key ON sys_api_auth(app_key);
-- 4. 经营分析
CREATE TABLE IF NOT EXISTS business_analytics (
id BIGSERIAL PRIMARY KEY,
stat_date DATE NOT NULL,
department_id BIGINT,
department_name VARCHAR(100),
revenue DECIMAL(12,2) DEFAULT 0,
cost DECIMAL(12,2) DEFAULT 0,
profit DECIMAL(12,2) DEFAULT 0,
patient_count INT DEFAULT 0,
bed_count INT DEFAULT 0,
bed_occupancy_rate DECIMAL(5,2) DEFAULT 0,
avg_stay_days DECIMAL(5,2) DEFAULT 0,
avg_cost DECIMAL(10,2) DEFAULT 0,
tenant_id BIGINT DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE business_analytics IS '经营分析日报';
CREATE INDEX idx_ba_date ON business_analytics(stat_date);

View File

@@ -0,0 +1,21 @@
package com.healthlink.his.clinical.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("clinical_knowledge_base")
public class ClinicalKnowledgeBase extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("category") private String category;
@TableField("title") private String title;
@TableField("content") private String content;
@TableField("keywords") private String keywords;
@TableField("source") private String source;
@TableField("version") private String version;
@TableField("status") private String status;
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.clinical.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.clinical.domain.ClinicalKnowledgeBase;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ClinicalKnowledgeBaseMapper extends BaseMapper<ClinicalKnowledgeBase> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.clinical.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.clinical.domain.ClinicalKnowledgeBase;
public interface IClinicalKnowledgeBaseService extends IService<ClinicalKnowledgeBase> {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.clinical.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.clinical.domain.ClinicalKnowledgeBase;
import com.healthlink.his.clinical.mapper.ClinicalKnowledgeBaseMapper;
import com.healthlink.his.clinical.service.IClinicalKnowledgeBaseService;
import org.springframework.stereotype.Service;
@Service
public class ClinicalKnowledgeBaseServiceImpl extends ServiceImpl<ClinicalKnowledgeBaseMapper, ClinicalKnowledgeBase> implements IClinicalKnowledgeBaseService {
}

View File

@@ -0,0 +1,25 @@
package com.healthlink.his.nursing.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("nursing_assessment_trend")
public class NursingAssessmentTrend extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("encounter_id") private Long encounterId;
@TableField("patient_id") private Long patientId;
@TableField("assessment_type") private String assessmentType;
@TableField("score") private BigDecimal score;
@TableField("risk_level") private String riskLevel;
@TableField("assess_time") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date assessTime;
@TableField("assessor_name") private String assessorName;
@TableField("detail_json") private String detailJson;
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.nursing.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.nursing.domain.NursingAssessmentTrend;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface NursingAssessmentTrendMapper extends BaseMapper<NursingAssessmentTrend> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.nursing.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.nursing.domain.NursingAssessmentTrend;
public interface INursingAssessmentTrendService extends IService<NursingAssessmentTrend> {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.nursing.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.nursing.domain.NursingAssessmentTrend;
import com.healthlink.his.nursing.mapper.NursingAssessmentTrendMapper;
import com.healthlink.his.nursing.service.INursingAssessmentTrendService;
import org.springframework.stereotype.Service;
@Service
public class NursingAssessmentTrendServiceImpl extends ServiceImpl<NursingAssessmentTrendMapper, NursingAssessmentTrend> implements INursingAssessmentTrendService {
}

View File

@@ -0,0 +1,26 @@
package com.healthlink.his.quality.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("business_analytics")
public class BusinessAnalytics extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("stat_date") private String statDate;
@TableField("department_id") private Long departmentId;
@TableField("department_name") private String departmentName;
@TableField("revenue") private BigDecimal revenue;
@TableField("cost") private BigDecimal cost;
@TableField("profit") private BigDecimal profit;
@TableField("patient_count") private Integer patientCount;
@TableField("bed_count") private Integer bedCount;
@TableField("bed_occupancy_rate") private BigDecimal bedOccupancyRate;
@TableField("avg_stay_days") private BigDecimal avgStayDays;
@TableField("avg_cost") private BigDecimal avgCost;
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.quality.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.quality.domain.BusinessAnalytics;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface BusinessAnalyticsMapper extends BaseMapper<BusinessAnalytics> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.quality.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.quality.domain.BusinessAnalytics;
public interface IBusinessAnalyticsService extends IService<BusinessAnalytics> {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.quality.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.quality.domain.BusinessAnalytics;
import com.healthlink.his.quality.mapper.BusinessAnalyticsMapper;
import com.healthlink.his.quality.service.IBusinessAnalyticsService;
import org.springframework.stereotype.Service;
@Service
public class BusinessAnalyticsServiceImpl extends ServiceImpl<BusinessAnalyticsMapper, BusinessAnalytics> implements IBusinessAnalyticsService {
}

View File

@@ -0,0 +1,24 @@
package com.healthlink.his.sys.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_api_auth")
public class SysApiAuth extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("app_name") private String appName;
@TableField("app_key") private String appKey;
@TableField("app_secret") private String appSecret;
@TableField("permissions") private String permissions;
@TableField("rate_limit") private Integer rateLimit;
@TableField("expire_time") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date expireTime;
@TableField("status") private Integer status;
@TableField("last_access_time") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date lastAccessTime;
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.sys.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.sys.domain.SysApiAuth;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysApiAuthMapper extends BaseMapper<SysApiAuth> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.sys.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.sys.domain.SysApiAuth;
public interface ISysApiAuthService extends IService<SysApiAuth> {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.sys.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.sys.domain.SysApiAuth;
import com.healthlink.his.sys.mapper.SysApiAuthMapper;
import com.healthlink.his.sys.service.ISysApiAuthService;
import org.springframework.stereotype.Service;
@Service
public class SysApiAuthServiceImpl extends ServiceImpl<SysApiAuthMapper, SysApiAuth> implements ISysApiAuthService {
}

View File

@@ -0,0 +1,4 @@
import request from '@/utils/request'
export function getAuthPage(p){return request({url:'/api-auth/page',method:'get',params:p})}
export function addAuth(d){return request({url:'/api-auth/add',method:'post',data:d})}
export function disableAuth(id){return request({url:'/api-auth/disable',method:'post',params:{id}})}

View File

@@ -0,0 +1,28 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">接口安全认证</span></div>
<div style="margin-bottom:12px"><el-button type="success" @click="showAdd=true">新增应用</el-button></div>
<el-table :data="authData" border stripe>
<el-table-column prop="appName" label="应用名称" width="150"/>
<el-table-column prop="appKey" label="AppKey" width="180"/>
<el-table-column prop="rateLimit" label="限流(次/分)" width="100" align="center"/>
<el-table-column prop="status" label="状态" width="80">
<template #default="{row}"><el-tag :type="row.status===0?'success':'danger'" size="small">{{ row.status===0?'启用':'禁用' }}</el-tag></template>
</el-table-column>
<el-table-column prop="lastAccessTime" label="最后访问" width="170"/>
<el-table-column label="操作" width="80">
<template #default="{row}"><el-button v-if="row.status===0" type="danger" link size="small" @click="disableAction(row)">禁用</el-button></template>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getAuthPage,addAuth,disableAuth} from './api'
const authData=ref([])
const showAdd=ref(false)
const loadData=async()=>{const r=await getAuthPage({pageNo:1,pageSize:50});authData.value=r.data?.records||[]}
const disableAction=async(row)=>{await disableAuth(row.id);ElMessage.success('已禁用');loadData()}
onMounted(()=>loadData())
</script>

View File

@@ -0,0 +1,3 @@
import request from '@/utils/request'
export function getTrend(p){return request({url:'/assessment-trend/trend',method:'get',params:p})}
export function addTrend(d){return request({url:'/assessment-trend/add',method:'post',data:d})}

View File

@@ -0,0 +1,27 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">评估趋势</span></div>
<el-card shadow="never" style="margin-bottom:16px">
<el-form inline>
<el-form-item label="就诊ID"><el-input v-model="encounterId" style="width:120px"/></el-form-item>
<el-form-item><el-button type="primary" @click="loadData">查询</el-button></el-form-item>
</el-form>
</el-card>
<el-table :data="trendData" border stripe>
<el-table-column prop="assessmentType" label="评估类型" width="110"/>
<el-table-column prop="score" label="评分" width="80" align="center"/>
<el-table-column prop="riskLevel" label="风险等级" width="90">
<template #default="{row}"><el-tag :type="{HIGH:'danger',MEDIUM:'warning',LOW:'success'}[row.riskLevel]" size="small">{{ row.riskLevel }}</el-tag></template>
</el-table-column>
<el-table-column prop="assessTime" label="评估时间" width="170"/>
<el-table-column prop="assessorName" label="评估人" width="90"/>
</el-table>
</div>
</template>
<script setup>
import {ref} from 'vue'
import {getTrend} from './api'
const encounterId=ref('')
const trendData=ref([])
const loadData=async()=>{if(!encounterId.value)return;const r=await getTrend({encounterId:encounterId.value});trendData.value=r.data||[]}
</script>

View File

@@ -0,0 +1,4 @@
import request from '@/utils/request'
export function getAnalyticsPage(p){return request({url:'/business-analytics/page',method:'get',params:p})}
export function addAnalytics(d){return request({url:'/business-analytics/add',method:'post',data:d})}
export function getAnalyticsSummary(){return request({url:'/business-analytics/summary',method:'get'})}

View File

@@ -0,0 +1,29 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">经营分析</span></div>
<el-card shadow="never" style="margin-bottom:16px">
<div style="display:flex;gap:30px;text-align:center;flex-wrap:wrap">
<div><div style="font-size:20px;font-weight:bold;color:#409eff">{{ summary.totalRevenue||0 }}</div><div>总收入</div></div>
<div><div style="font-size:20px;font-weight:bold;color:#f56c6c">{{ summary.totalCost||0 }}</div><div>总成本</div></div>
<div><div style="font-size:20px;font-weight:bold;color:#67c23a">{{ summary.totalProfit||0 }}</div><div>总利润</div></div>
<div><div style="font-size:20px;font-weight:bold;color:#e6a23c">{{ summary.totalPatients||0 }}</div><div>总患者</div></div>
</div>
</el-card>
<el-table :data="analyticsData" border stripe>
<el-table-column prop="statDate" label="日期" width="120"/>
<el-table-column prop="departmentName" label="科室" width="120"/>
<el-table-column prop="revenue" label="收入" width="100" align="right"/>
<el-table-column prop="cost" label="成本" width="100" align="right"/>
<el-table-column prop="profit" label="利润" width="100" align="right"/>
<el-table-column prop="patientCount" label="患者数" width="80" align="center"/>
<el-table-column prop="bedOccupancyRate" label="床位率%" width="90" align="center"/>
</el-table>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {getAnalyticsPage,getAnalyticsSummary} from './api'
const analyticsData=ref([]),summary=ref({})
const loadData=async()=>{const [a,s]=await Promise.all([getAnalyticsPage({pageNo:1,pageSize:50}),getAnalyticsSummary()]);analyticsData.value=a.data?.records||[];summary.value=s.data||{}}
onMounted(()=>loadData())
</script>

View File

@@ -0,0 +1,4 @@
import request from '@/utils/request'
export function getKBPage(p){return request({url:'/knowledge-base/page',method:'get',params:p})}
export function searchKB(k){return request({url:'/knowledge-base/search',method:'get',params:{keyword:k}})}
export function addKB(d){return request({url:'/knowledge-base/add',method:'post',data:d})}

View File

@@ -0,0 +1,27 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">临床知识库</span></div>
<el-card shadow="never" style="margin-bottom:16px">
<el-form inline>
<el-form-item label="搜索"><el-input v-model="keyword" placeholder="指南/药物/诊断" clearable style="width:200px"/></el-form-item>
<el-form-item label="分类"><el-select v-model="category" clearable style="width:120px"><el-option v-for="c in [{l:'指南',v:'guideline'},{l:'药物',v:'drug'},{l:'诊断',v:'diagnosis'},{l:'操作',v:'procedure'}]" :key="c.v" :label="c.l" :value="c.v"/></el-select></el-form-item>
<el-form-item><el-button type="primary" @click="loadData">搜索</el-button></el-form-item>
</el-form>
</el-card>
<el-table :data="kbData" border stripe>
<el-table-column prop="category" label="分类" width="80"/>
<el-table-column prop="title" label="标题" min-width="200"/>
<el-table-column prop="keywords" label="关键词" width="150" show-overflow-tooltip/>
<el-table-column prop="source" label="来源" width="120"/>
<el-table-column prop="version" label="版本" width="80"/>
</el-table>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {getKBPage} from './api'
const keyword=ref(''),category=ref('')
const kbData=ref([])
const loadData=async()=>{const r=await getKBPage({keyword:keyword.value,category:category.value,pageNo:1,pageSize:50});kbData.value=r.data?.records||[]}
onMounted(()=>loadData())
</script>