Compare commits
15
Commits
15ab717317
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97be68db61 | ||
|
|
8fff1db294 | ||
|
|
04e4d66a4c | ||
|
|
b871409361 | ||
|
|
a3566c1f0c | ||
|
|
ae2b585df9 | ||
|
|
b7283db0bd | ||
|
|
aeed30f014 | ||
|
|
51b050bd4c | ||
|
|
754448e370 | ||
|
|
8b32335eee | ||
|
|
b01d5d1d7a | ||
|
|
f333c87aa5 | ||
|
|
695a7f84f3 | ||
|
|
d659c4c030 |
+2
-1
@@ -2,6 +2,7 @@
|
|||||||
target/
|
target/
|
||||||
*.class
|
*.class
|
||||||
*.jar
|
*.jar
|
||||||
|
dist/
|
||||||
|
|
||||||
# 运行时目录(本地联调用,不入 Git)
|
# 运行时目录(本地联调用,不入 Git)
|
||||||
bin/
|
bin/
|
||||||
@@ -54,4 +55,4 @@ mvnw.cmd
|
|||||||
|
|
||||||
# JDK
|
# JDK
|
||||||
*.jdk
|
*.jdk
|
||||||
*.jre
|
*.jredist/
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
# Hospital Front — 医院前置机程序
|
# Hospital Front — 医院前置机程序
|
||||||
|
|
||||||
> 基于 Kotlin + Solon 4.0.6 重构的医院前置机对接系统
|
> 基于 Java 17 + Solon 4.0.6 的医院前置机对接系统
|
||||||
>
|
>
|
||||||
> 重构源:[hospitalmiddle](../hospitalmiddle)(SpringBoot 2.7 + MyBatis-Plus + Oracle/SqlServer)
|
> 重构源:hospitalmiddle(SpringBoot 2.7 + MyBatis-Plus + Oracle/SqlServer)
|
||||||
>
|
|
||||||
> 完整方案:[E:\data\ob_data\myob\11 健康长庆\医院前置机重构\](../ob_data)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -12,34 +10,35 @@
|
|||||||
|
|
||||||
体检预约系统通过本程序与多家医院 HIS 系统对接,实现:
|
体检预约系统通过本程序与多家医院 HIS 系统对接,实现:
|
||||||
- 体检预约的提交、取消
|
- 体检预约的提交、取消
|
||||||
- 体检项目查询、状态查询
|
- 体检项目 / 组合查询、体检状态查询
|
||||||
- 体检结果 / 结论 / 建议的获取
|
- 体检结果 / 结论 / 建议的获取
|
||||||
- 单位信息同步
|
- 单位信息同步
|
||||||
|
|
||||||
**支持 5 家医院**(拼音编码):
|
**支持医院**(拼音编码,由 `hospital.active` 指定激活):
|
||||||
| 编码 | 医院名 | 数据库 |
|
|
||||||
|---|---|---|
|
| 编码 | 医院名 | 数据库 | Adapter |
|
||||||
| `xinglongyuan` | 兴隆园医院 | Oracle |
|
|---|---|---|---|
|
||||||
| `qingyang` | 庆阳医院 | Oracle |
|
| `xinglongyuan` | 兴隆园医院 | Oracle | `XingLongYuanAdapter` |
|
||||||
| `ningxia` | 宁夏宝石花医院 | SqlServer |
|
| `qingyang` | 庆阳医院 | Oracle | `QingyangAdapter` |
|
||||||
| `yanan` | 延安医院 | SqlServer |
|
| `ningxia` | 宁夏宝石花医院 | SqlServer | `NingxiaAdapter` |
|
||||||
| `jinghe` | 泾河医院 | Oracle |
|
| `yanan` | 延安医院 | SqlServer | `YananAdapter` |
|
||||||
|
| `jinghe` | 泾河医院 | Oracle | `JingheAdapter` |
|
||||||
|
|
||||||
|
医院通过 `HospitalAdapter` 接口 + `HospitalRouter` 路由(策略模式),方言基类:
|
||||||
|
- `BsmAdapterBase`:Oracle 方言(bsm Mapper)
|
||||||
|
- `NxAdapterBase`:SqlServer 方言(nx Mapper)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 技术栈
|
## 2. 技术栈
|
||||||
|
|
||||||
- **语言**:Kotlin 2.0.21(`jvmTarget=17`)
|
- **语言**:Java 17
|
||||||
- **框架**:Solon 4.0.6(国产轻量级 Java 框架)
|
- **框架**:Solon 4.0.6
|
||||||
- **JDK**:17 LTS(推荐)或 21 LTS
|
|
||||||
- **ORM**:MyBatis-Plus 3.5.9
|
- **ORM**:MyBatis-Plus 3.5.9
|
||||||
- **数据库**:Oracle / SqlServer(直连医院库,不可修改)
|
- **数据库**:Oracle(ojdbc11 + orai18n,ZHS16GBK 字符集支持)/ SqlServer(直连医院库,只读对接,不可修改表结构)
|
||||||
- **日志**:Logback 1.5.12
|
- **日志**:Logback(异步 appender,info.log / error.log 分文件滚动)
|
||||||
- **HTTP/WS**:OkHttp 4.12.0
|
|
||||||
- **构建**:Maven 3.9+
|
- **构建**:Maven 3.9+
|
||||||
|
|
||||||
> ✅ **依赖瘦身**:不接入 Knife4j / Druid / Lombok / Jasypt(详见 [01-技术栈选型.md](../ob_data/01-技术栈选型.md))
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 项目结构
|
## 3. 项目结构
|
||||||
@@ -48,31 +47,22 @@
|
|||||||
hospital-front/
|
hospital-front/
|
||||||
├── pom.xml
|
├── pom.xml
|
||||||
├── README.md
|
├── README.md
|
||||||
├── .gitignore
|
├── conf/ # 各医院外置配置(含生产凭据,不入 Git)
|
||||||
├── src/
|
└── src/main/
|
||||||
│ ├── main/
|
├── java/com/hospital/front/
|
||||||
│ │ ├── kotlin/com/hospital/front/
|
│ ├── HospitalFrontApp.java # 启动类
|
||||||
│ │ │ ├── HospitalFrontApp.kt # @SolonMain 启动类
|
│ ├── adapter/ # 医院适配器(策略模式 + 方言基类)
|
||||||
│ │ │ ├── adapter/ # 医院适配器(策略模式)
|
│ ├── api/ # HTTP 接口(order / admin)
|
||||||
│ │ │ ├── api/ # HTTP 接口层(按业务域)
|
│ ├── service/ # 业务服务层
|
||||||
│ │ │ ├── service/ # 业务服务层
|
│ ├── mapper/bsm|nx/ # 数据访问层(按方言分包)
|
||||||
│ │ │ ├── mapper/ # 数据访问层
|
│ ├── vo/ dto/ # 值对象 / 传输对象
|
||||||
│ │ │ ├── vo/ # 值对象
|
│ ├── infra/ # 基础设施(过滤器、异常处理、扫描器)
|
||||||
│ │ │ ├── dto/ # 数据传输对象
|
│ └── task/ # 定时任务(Uptime-Kuma 上报)
|
||||||
│ │ │ ├── infra/ # 基础设施
|
└── resources/
|
||||||
│ │ │ └── task/ # 定时任务
|
├── app.yml # 主配置(框架级 + 公共配置)
|
||||||
│ │ └── resources/
|
├── logback.xml # 日志配置
|
||||||
│ │ ├── app.yml # Solon 主配置
|
├── tls10.security # 延安专用 JDK 安全策略(放开 TLSv1/弱算法)
|
||||||
│ │ ├── logback.xml # 日志配置
|
└── mapper/ # MyBatis XML
|
||||||
│ │ └── mapper/ # MyBatis XML
|
|
||||||
│ └── test/kotlin/ # 单元测试
|
|
||||||
└── pac/ # 部署包(运行时外置)
|
|
||||||
├── bin/ # 启动脚本
|
|
||||||
├── conf/ # 外置配置
|
|
||||||
├── backup/ # 历史版本
|
|
||||||
├── log/ # 日志
|
|
||||||
├── pid/ # PID 文件
|
|
||||||
└── tmp/ # 临时文件
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -89,57 +79,61 @@ mvn clean package -DskipTests
|
|||||||
## 5. 启动(开发环境)
|
## 5. 启动(开发环境)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 拷贝外置配置(首次启动)
|
# 方式一:默认加载 classpath 的 app.yml(hospital.active 默认 jinghe)
|
||||||
cp -r pac/conf-example pac/conf
|
java -jar target/hospital-front-1.0.0.jar
|
||||||
# 编辑配置
|
|
||||||
vim pac/conf/app-jinghe.yml
|
|
||||||
|
|
||||||
# 启动(默认加载 app.yml + conf/app-{HOSPITAL}.yml)
|
|
||||||
HOSPITAL=jinghe java -jar target/hospital-front-1.0.0.jar
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
数据源等医院差异配置在 `app-{医院}.yml` 中,开发期可用 `--solon.config.add` 外挂。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. 部署
|
## 6. 生产部署注意事项(重要)
|
||||||
|
|
||||||
详细部署、升级、回滚 SOP:[05-部署运维.md](../ob_data/05-部署运维.md)
|
### 6.1 外挂医院配置
|
||||||
|
|
||||||
**Windows Server 简版**:
|
生产环境配置文件**外置于 jar 同级或 `conf/` 目录**,通过 `--solon.config.add` 追加加载,
|
||||||
```batch
|
修改数据库地址 / 鉴权等无需重新打包:
|
||||||
D:
|
|
||||||
cd D:\hospital-front\bin
|
```bash
|
||||||
call start.bat start
|
java -jar hospital-front.jar --solon.config.add=conf/app-yanan.yml --server.port=8003
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- 配置模板见仓库 `conf/` 目录(app-jinghe / app-ningxia / app-yanan)
|
||||||
|
- `--server.port` 覆盖默认端口 8082
|
||||||
|
- 外挂文件中必须指定 `hospital.active` 与 `hm.type`(bsm / nx / ya),并定义 `solon.dataSources.db1`
|
||||||
|
|
||||||
|
### 6.2 延安医院:TLS 1.0 特殊处理
|
||||||
|
|
||||||
|
延安 SqlServer 为老版本(仅支持 TLS 1.0 / 弱加密算法),JDK 17 默认禁用,
|
||||||
|
必须外挂 JDK 安全属性文件启动,否则连接报 `handshake failure` / `No appropriate protocol`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -Dfile.encoding=UTF-8 -Djava.security.properties=tls10.security -jar hospital-front.jar --solon.config.add=conf/app-yanan.yml --server.port=8003
|
||||||
|
```
|
||||||
|
|
||||||
|
- `tls10.security` 已打包在 jar 内(`src/main/resources/`),上述命令按 classpath 加载,无需单独分发文件;也可用文件系统绝对路径覆盖
|
||||||
|
- `-Dfile.encoding=UTF-8`:Windows 服务器默认 GBK,防止日志与接口中文乱码
|
||||||
|
- 其余医院**不要**带 `tls10.security`(会全局放开弱算法,降低安全基线)
|
||||||
|
|
||||||
|
### 6.3 安全配置(生产必读)
|
||||||
|
|
||||||
|
`app.yml` 中以下配置生产环境必须核对(可在外挂配置中覆盖):
|
||||||
|
|
||||||
|
| 配置 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `order.basic` | `/order/**` 的 HTTP Basic 认证(`user:password`),生产必须配置 |
|
||||||
|
| `admin.token` | `/api/admin/**` 的 `X-Admin-Token`,Agent 环回调用 |
|
||||||
|
| `security.ipWhitelistEnabled` / `ipWhitelist` | IP 白名单(支持精确 IP / 通配段 / CIDR),端口暴露公网时务必开启 |
|
||||||
|
| `security.trustProxy` | nginx 反代部署时开启,从 X-Forwarded-For 取真实客户端 IP |
|
||||||
|
|
||||||
|
### 6.4 日志
|
||||||
|
|
||||||
|
- 日志目录默认 `./log`(`-Dlog.path=xxx` 可覆盖)
|
||||||
|
- `info.log`(INFO 及以上)/ `error.log`(仅 ERROR)分文件,50MB 按天滚动,保留 30 天
|
||||||
|
- pattern 不含 `%file:%line`(AsyncAppender 下取不到调用者信息,会打成 `?:?`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. 文档
|
## 7. 文档
|
||||||
|
|
||||||
完整设计文档位于:`E:\data\ob_data\myob\11 健康长庆\医院前置机重构\`
|
完整设计文档位于:`E:\data\ob_data\myob\11 健康长庆\医院前置机重构\`
|
||||||
|
|
||||||
| 文档 | 内容 |
|
|
||||||
|---|---|
|
|
||||||
| [00-总体方案.md](../ob_data/00-总体方案.md) | 整体方案 |
|
|
||||||
| [01-技术栈选型.md](../ob_data/01-技术栈选型.md) | Solon 选型、版本、注解对照 |
|
|
||||||
| [02-医院差异化矩阵.md](../ob_data/02-医院差异化矩阵.md) | 5 家医院差异、Adapter 拆分 |
|
|
||||||
| [03-架构设计.md](../ob_data/03-架构设计.md) | 系统架构、模块、时序 |
|
|
||||||
| [04-数据流与接口契约.md](../ob_data/04-数据流与接口契约.md) | 接口清单、入参出参 |
|
|
||||||
| [05-部署运维.md](../ob_data/05-部署运维.md) | 部署、升级、回滚 SOP |
|
|
||||||
| [06-变更记录.md](../ob_data/06-变更记录.md) | 所有变更登记 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 任务进度
|
|
||||||
|
|
||||||
| Step | 内容 | 状态 |
|
|
||||||
|---|---|---|
|
|
||||||
| 1 | 工程骨架 | ✅ |
|
|
||||||
| 2 | 5 医院配置中心 | ⏳ |
|
|
||||||
| 3 | 业务接口 Kotlin 重写 | ⏳ |
|
|
||||||
| 4 | 5 套 Mapper 适配 | ⏳ |
|
|
||||||
| 5 | 主程序被控端 API | ⏳ |
|
|
||||||
| 6 | Agent 工程 | ⏳ |
|
|
||||||
| 7 | 升级流程 | ⏳ |
|
|
||||||
| 8 | 日志推送 | ⏳ |
|
|
||||||
| 9 | B 端管理后台 | ⏳ |
|
|
||||||
| 10 | 联调 + 文档 | ⏳ |
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# 外置配置示例(部署时拷贝到 conf\)
|
|
||||||
|
|
||||||
> **使用流程**:
|
|
||||||
> 1. 首次部署:把整个 `conf-example\` 目录拷贝为 `conf\`
|
|
||||||
> ```
|
|
||||||
> xcopy /E /I conf-example conf
|
|
||||||
> ```
|
|
||||||
> 2. 编辑 `conf\app-{编码}.yml`,把 `CHANGE_ME` 替换为真实密码(或通过环境变量注入)
|
|
||||||
> 3. 根据实际部署环境修改 `hospital.id`、数据库 URL 等
|
|
||||||
>
|
|
||||||
> **本目录不入 Git**(已在 `.gitignore` 中声明 `pac/conf/*.yml`)。
|
|
||||||
|
|
||||||
## 5 家医院配置清单
|
|
||||||
|
|
||||||
| 医院 | 文件 | 数据库 | 默认端口 | `hm.type` |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| 兴隆园医院 | `app-xinglongyuan.yml` | Oracle | 8082 | `bsm` |
|
|
||||||
| 庆阳医院 | `app-qingyang.yml` | Oracle | 8082 | `bsm` |
|
|
||||||
| 宁夏宝石花医院 | `app-ningxia.yml` | SqlServer | 8082 | `nx` |
|
|
||||||
| 延安医院 | `app-yanan.yml` | SqlServer | 8082 | `ya` |
|
|
||||||
| 泾河医院 | `app-jinghe.yml` | Oracle | 8082 | `bsm` |
|
|
||||||
|
|
||||||
## 切换医院
|
|
||||||
|
|
||||||
```batch
|
|
||||||
# 启动泾河(默认)
|
|
||||||
bin\start.bat start
|
|
||||||
|
|
||||||
# 启动其他医院
|
|
||||||
set HOSPITAL=xinglongyuan
|
|
||||||
bin\start.bat start
|
|
||||||
```
|
|
||||||
|
|
||||||
## 环境变量占位
|
|
||||||
|
|
||||||
配置文件中使用 `${ENV_VAR:DEFAULT}` 占位语法,例如:
|
|
||||||
```yaml
|
|
||||||
password: ${DB_PWD_JINGHE:CHANGE_ME}
|
|
||||||
```
|
|
||||||
|
|
||||||
启动前设置环境变量(推荐,避免明文写密码):
|
|
||||||
```batch
|
|
||||||
set DB_PWD_JINGHE=ActualPassword
|
|
||||||
bin\start.bat start
|
|
||||||
```
|
|
||||||
|
|
||||||
> **安全提示**:
|
|
||||||
> 1. 部署后 `chmod 600` 整个 `conf\` 目录(Linux)或 `Set-Acl`(Windows PowerShell)
|
|
||||||
> 2. **不要**把包含真实密码的 yml 提交到 Git
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# ====================================
|
|
||||||
# Agent 配置(部署时拷贝到 D:\hospital-front\conf\agent.properties)
|
|
||||||
# Agent 启动命令:javaw -cp bin\hospital-front.jar com.hospital.agent.AgentMainKt
|
|
||||||
# 环境变量可覆盖:AGENT_ 前缀 + 大写 + 下划线(如 AGENT_SERVER_URL)
|
|
||||||
# ====================================
|
|
||||||
|
|
||||||
# B 端管理服务器 WebSocket 地址
|
|
||||||
server.url=ws://127.0.0.1:9091/ws
|
|
||||||
|
|
||||||
# 设备编号(每台前置机唯一,建议按医院+序号命名)
|
|
||||||
device.id=jinghe-01
|
|
||||||
|
|
||||||
# 设备鉴权 Token(与 B 端注册信息一致)
|
|
||||||
device.token=
|
|
||||||
|
|
||||||
# 主程序健康检查地址
|
|
||||||
main.health.url=http://127.0.0.1:8082/api/admin/health
|
|
||||||
|
|
||||||
# 主程序管理接口 Token(与主程序 app.yml 的 admin.token 一致)
|
|
||||||
main.admin.token=
|
|
||||||
|
|
||||||
# 主程序启动命令(宕机拉起用;Windows 用 .bat,Linux 用 .sh)
|
|
||||||
main.start.cmd=bin/start.bat
|
|
||||||
|
|
||||||
# 心跳间隔(秒)
|
|
||||||
heartbeat.sec=30
|
|
||||||
|
|
||||||
# 是否守护主程序(宕机自动拉起)
|
|
||||||
guard.enable=true
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# 宁夏宝石花医院 → 本地 SqlServer 测试库(mac204,nx 方言)
|
|
||||||
hospital:
|
|
||||||
active: ningxia
|
|
||||||
hospital.id: 402885e0788113ad01788124b3430007
|
|
||||||
hm:
|
|
||||||
type: nx
|
|
||||||
ver: V2
|
|
||||||
allsecondFlag: true
|
|
||||||
occFlag: false
|
|
||||||
solon.dataSources:
|
|
||||||
db1!:
|
|
||||||
class: "com.zaxxer.hikari.HikariDataSource"
|
|
||||||
jdbcUrl: jdbc:sqlserver://192.168.50.204:1433;DatabaseName=HospitalTest;trustServerCertificate=true;encrypt=false
|
|
||||||
username: sa
|
|
||||||
password: ${DB_PWD:jyf@2024}
|
|
||||||
driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
|
|
||||||
minimumIdle: 2
|
|
||||||
maximumPoolSize: 10
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# ====================================
|
|
||||||
# 庆阳医院(先与 jinghe 一致,正式环境再调整) 配置
|
|
||||||
# 文件路径:D:\hospital-front\conf\app-qingyang.yml
|
|
||||||
# 启动:set HOSPITAL=qingyang && bin\start.bat start
|
|
||||||
# ====================================
|
|
||||||
|
|
||||||
hospital:
|
|
||||||
active: qingyang
|
|
||||||
|
|
||||||
# 医院业务 ID(HIS 系统分配,正式环境核对)
|
|
||||||
hospital.id: CHANGE_ME_HOSPITAL_ID
|
|
||||||
|
|
||||||
# 业务开关
|
|
||||||
hm:
|
|
||||||
type: bsm
|
|
||||||
ver: V2
|
|
||||||
allsecondFlag: false
|
|
||||||
occFlag: false
|
|
||||||
|
|
||||||
# 数据源(HikariCP,Solon 默认连接池;db1! 中 ! 表示默认数据源)
|
|
||||||
solon.dataSources:
|
|
||||||
db1!:
|
|
||||||
class: "com.zaxxer.hikari.HikariDataSource"
|
|
||||||
jdbcUrl: jdbc:oracle:thin:@CHANGE_ME:1521/ORCL
|
|
||||||
username: CHANGE_ME
|
|
||||||
password: ${DB_PWD:CHANGE_ME}
|
|
||||||
minimumIdle: 2
|
|
||||||
maximumPoolSize: 10
|
|
||||||
connectionTimeout: 30000
|
|
||||||
|
|
||||||
# 驱动说明:Oracle 由 jdbcUrl 自动识别;SqlServer 需加 driverClassName
|
|
||||||
# Oracle 驱动按 jdbcUrl 自动识别
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# ====================================
|
|
||||||
# 兴隆园医院 配置
|
|
||||||
# 文件路径:D:\hospital-front\conf\app-xinglongyuan.yml
|
|
||||||
# 启动:set HOSPITAL=xinglongyuan && bin\start.bat start
|
|
||||||
# ====================================
|
|
||||||
|
|
||||||
hospital:
|
|
||||||
active: xinglongyuan
|
|
||||||
|
|
||||||
# 医院业务 ID(HIS 系统分配,正式环境核对)
|
|
||||||
hospital.id: CHANGE_ME_HOSPITAL_ID
|
|
||||||
|
|
||||||
# 业务开关
|
|
||||||
hm:
|
|
||||||
type: bsm
|
|
||||||
ver: V2
|
|
||||||
allsecondFlag: false
|
|
||||||
occFlag: false
|
|
||||||
|
|
||||||
# 数据源(HikariCP,Solon 默认连接池;db1! 中 ! 表示默认数据源)
|
|
||||||
solon.dataSources:
|
|
||||||
db1!:
|
|
||||||
class: "com.zaxxer.hikari.HikariDataSource"
|
|
||||||
jdbcUrl: jdbc:oracle:thin:@172.17.10.8:1521/ORCL
|
|
||||||
username: CHANGE_ME
|
|
||||||
password: ${DB_PWD:CHANGE_ME}
|
|
||||||
minimumIdle: 2
|
|
||||||
maximumPoolSize: 10
|
|
||||||
connectionTimeout: 30000
|
|
||||||
|
|
||||||
# 驱动说明:Oracle 由 jdbcUrl 自动识别;SqlServer 需加 driverClassName
|
|
||||||
# Oracle 驱动按 jdbcUrl 自动识别
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# 延安医院 → 本地 SqlServer 测试库(mac204,ya 方言)
|
|
||||||
hospital:
|
|
||||||
active: yanan
|
|
||||||
hospital.id: 402885e0788113ad01788124b3430007
|
|
||||||
hm:
|
|
||||||
type: ya
|
|
||||||
ver: V2
|
|
||||||
allsecondFlag: false
|
|
||||||
occFlag: false
|
|
||||||
solon.dataSources:
|
|
||||||
db1!:
|
|
||||||
class: "com.zaxxer.hikari.HikariDataSource"
|
|
||||||
jdbcUrl: jdbc:sqlserver://192.168.50.204:1433;DatabaseName=HospitalTest;trustServerCertificate=true;encrypt=false
|
|
||||||
username: sa
|
|
||||||
password: ${DB_PWD:jyf@2024}
|
|
||||||
driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
|
|
||||||
minimumIdle: 2
|
|
||||||
maximumPoolSize: 10
|
|
||||||
@@ -29,19 +29,21 @@
|
|||||||
<java.version>17</java.version>
|
<java.version>17</java.version>
|
||||||
<maven.compiler.source>17</maven.compiler.source>
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
<maven.compiler.target>17</maven.compiler.target>
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
<kotlin.version>2.0.21</kotlin.version>
|
|
||||||
<kotlin.jvmTarget>17</kotlin.jvmTarget>
|
|
||||||
|
|
||||||
<!-- 数据库驱动(solon-parent 不管理) -->
|
<!-- 数据库驱动(solon-parent 不管理) -->
|
||||||
<ojdbc.version>23.3.0.23.09</ojdbc.version>
|
<ojdbc.version>23.3.0.23.09</ojdbc.version>
|
||||||
<mssql-jdbc.version>12.6.1.jre11</mssql-jdbc.version>
|
<mssql-jdbc.version>12.6.1.jre11</mssql-jdbc.version>
|
||||||
<sqlite-jdbc.version>3.46.1.0</sqlite-jdbc.version>
|
|
||||||
|
|
||||||
<!-- 鉴权 -->
|
|
||||||
<sa-token.version>1.39.0</sa-token.version>
|
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<!-- ========== Lombok(简化 POJO:getter/setter/toString 等) ========== -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.34</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- ========== Solon 核心 ========== -->
|
<!-- ========== Solon 核心 ========== -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.noear</groupId>
|
<groupId>org.noear</groupId>
|
||||||
@@ -77,6 +79,14 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.noear</groupId>
|
<groupId>org.noear</groupId>
|
||||||
<artifactId>mybatis-plus-solon-plugin</artifactId>
|
<artifactId>mybatis-plus-solon-plugin</artifactId>
|
||||||
|
<!-- 排除插件传递的 core 3.5.5:显式声明的 mybatis-plus 3.5.9 已内含 core 类,
|
||||||
|
双版本共存既造成 shade 同类冲突(随机取一边混搭),也白白多出 ~4MB 体积 -->
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-core</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.baomidou</groupId>
|
<groupId>com.baomidou</groupId>
|
||||||
@@ -97,6 +107,12 @@
|
|||||||
<artifactId>ojdbc11</artifactId>
|
<artifactId>ojdbc11</artifactId>
|
||||||
<version>${ojdbc.version}</version>
|
<version>${ojdbc.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Oracle NLS 字符集支持:ZHS16GBK 等中文/多字节字符集转换表,缺失报 ORA-17056 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.oracle.database.nls</groupId>
|
||||||
|
<artifactId>orai18n</artifactId>
|
||||||
|
<version>${ojdbc.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- SqlServer -->
|
<!-- SqlServer -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -105,43 +121,12 @@
|
|||||||
<version>${mssql-jdbc.version}</version>
|
<version>${mssql-jdbc.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- SQLite(B 端 Agent 用) -->
|
<!-- 已移除依赖说明(精简 jar 体积):
|
||||||
<dependency>
|
- sqlite-jdbc:原为 B 端 Agent 用,Agent 已拆分为独立 Python 项目
|
||||||
<groupId>org.xerial</groupId>
|
- h2:开发 Mock 数据源,需要时本地临时加回
|
||||||
<artifactId>sqlite-jdbc</artifactId>
|
- sa-token-solon-plugin:鉴权已改为 OrderApi 手写 HTTP Basic
|
||||||
<version>${sqlite-jdbc.version}</version>
|
- okhttp:UptimeKuma 心跳已改用 JDK 内置 java.net.http.HttpClient
|
||||||
</dependency>
|
-->
|
||||||
|
|
||||||
<!-- H2(开发 Mock 数据源) -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.h2database</groupId>
|
|
||||||
<artifactId>h2</artifactId>
|
|
||||||
<version>2.3.232</version>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- ========== HTTP / WebSocket(Agent 用) ========== -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.squareup.okhttp3</groupId>
|
|
||||||
<artifactId>okhttp</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- ========== 鉴权(可选,按需启用) ========== -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>cn.dev33</groupId>
|
|
||||||
<artifactId>sa-token-solon-plugin</artifactId>
|
|
||||||
<version>${sa-token.version}</version>
|
|
||||||
<optional>true</optional>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- ========== Kotlin 标准库 ========== -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jetbrains.kotlin</groupId>
|
|
||||||
<artifactId>kotlin-stdlib</artifactId>
|
|
||||||
<version>${kotlin.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- ========== JSON 解析(Solon 生态标准库) ========== -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.noear</groupId>
|
<groupId>org.noear</groupId>
|
||||||
<artifactId>snack3</artifactId>
|
<artifactId>snack3</artifactId>
|
||||||
@@ -153,55 +138,12 @@
|
|||||||
<artifactId>solon-serialization-snack3</artifactId>
|
<artifactId>solon-serialization-snack3</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- ========== 测试 ========== -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jetbrains.kotlin</groupId>
|
|
||||||
<artifactId>kotlin-test-junit5</artifactId>
|
|
||||||
<version>${kotlin.version}</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<finalName>hospital-front-${project.version}</finalName>
|
<finalName>hospital-front</finalName>
|
||||||
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
|
|
||||||
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
|
|
||||||
|
|
||||||
<plugins>
|
<plugins>
|
||||||
<!-- Kotlin 编译 -->
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.jetbrains.kotlin</groupId>
|
|
||||||
<artifactId>kotlin-maven-plugin</artifactId>
|
|
||||||
<version>${kotlin.version}</version>
|
|
||||||
<configuration>
|
|
||||||
<compilerPlugins>
|
|
||||||
<plugin>spring</plugin>
|
|
||||||
</compilerPlugins>
|
|
||||||
<jvmTarget>${kotlin.jvmTarget}</jvmTarget>
|
|
||||||
</configuration>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<id>compile</id>
|
|
||||||
<goals>
|
|
||||||
<goal>compile</goal>
|
|
||||||
</goals>
|
|
||||||
</execution>
|
|
||||||
<execution>
|
|
||||||
<id>test-compile</id>
|
|
||||||
<goals>
|
|
||||||
<goal>test-compile</goal>
|
|
||||||
</goals>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jetbrains.kotlin</groupId>
|
|
||||||
<artifactId>kotlin-maven-allopen</artifactId>
|
|
||||||
<version>${kotlin.version}</version>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</plugin>
|
|
||||||
|
|
||||||
<!-- 打 fat jar(包含所有依赖) -->
|
<!-- 打 fat jar(包含所有依赖) -->
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
@@ -217,7 +159,7 @@
|
|||||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||||
<transformers>
|
<transformers>
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
<mainClass>com.hospital.front.HospitalFrontAppKt</mainClass>
|
<mainClass>com.hospital.front.HospitalFrontApp</mainClass>
|
||||||
</transformer>
|
</transformer>
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
||||||
</transformers>
|
</transformers>
|
||||||
@@ -228,10 +170,19 @@
|
|||||||
<exclude>META-INF/*.SF</exclude>
|
<exclude>META-INF/*.SF</exclude>
|
||||||
<exclude>META-INF/*.DSA</exclude>
|
<exclude>META-INF/*.DSA</exclude>
|
||||||
<exclude>META-INF/*.RSA</exclude>
|
<exclude>META-INF/*.RSA</exclude>
|
||||||
|
<!-- 依赖自带的模块描述文件与 fat jar 不兼容,剔除以消除 shade 警告 -->
|
||||||
|
<exclude>module-info.class</exclude>
|
||||||
|
<exclude>META-INF/versions/*/module-info.class</exclude>
|
||||||
|
<!-- 以下重叠资源均为 IDE 提示/native-image 元数据/许可证文本,
|
||||||
|
运行时不读取,剔除以消除 shade 的 overlapping 警告 -->
|
||||||
|
<exclude>META-INF/solon/solon-configuration-metadata.json</exclude>
|
||||||
|
<exclude>META-INF/native-image/**</exclude>
|
||||||
|
<exclude>META-INF/LICENSE</exclude>
|
||||||
|
<exclude>META-INF/MANIFEST.MF</exclude>
|
||||||
</excludes>
|
</excludes>
|
||||||
</filter>
|
</filter>
|
||||||
</filters>
|
</filters>
|
||||||
<finalName>hospital-front-${project.version}</finalName>
|
<finalName>hospital-front</finalName>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.hospital.front;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
import org.noear.solon.Solon;
|
||||||
|
import org.noear.solon.annotation.SolonMain;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 医院前置机主程序启动类。
|
||||||
|
*
|
||||||
|
* 使用 Solon 3.x 框架,对外暴露体检预约业务接口(/order 路径)
|
||||||
|
* 与被控端接口(/api/admin 路径)。
|
||||||
|
*
|
||||||
|
* 启动命令示例:
|
||||||
|
* java -Xms256m -Xmx512m -Dsolon.env=jinghe -Dsolon.config.load=app.yml,conf/app-jinghe.yml -jar hospital-front-1.0.0.jar
|
||||||
|
*/
|
||||||
|
@SolonMain
|
||||||
|
public class HospitalFrontApp {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(HospitalFrontApp.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主入口。
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) {
|
||||||
|
Solon.start(HospitalFrontApp.class, args);
|
||||||
|
long pid = ProcessHandle.current().pid();
|
||||||
|
// 走 logback 落盘(服务方式运行时控制台不可见,System.out 进不了日志文件)
|
||||||
|
log.info("启动完成,PID={},激活医院={},端口={}",
|
||||||
|
pid,
|
||||||
|
Solon.cfg().get("hospital.active", "?"),
|
||||||
|
Solon.cfg().get("server.port", "?"));
|
||||||
|
// 写 PID 文件(供 start.bat stop / Agent 升级流程强杀使用)
|
||||||
|
try {
|
||||||
|
File dir = new File("pid");
|
||||||
|
if (!dir.exists()) {
|
||||||
|
dir.mkdirs();
|
||||||
|
}
|
||||||
|
Files.write(Paths.get(dir.getPath(), "hospital-front.pid"), String.valueOf(pid).getBytes());
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
// 注册停机钩子:清理 PID + 记录优雅退出日志
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||||
|
try {
|
||||||
|
new File("pid/hospital-front.pid").delete();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
log.info("优雅停机中...");
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
package com.hospital.front.adapter;
|
||||||
|
|
||||||
|
import com.hospital.front.vo.ConclusionVo;
|
||||||
|
import com.hospital.front.vo.ExamListVo;
|
||||||
|
import com.hospital.front.vo.MedicalMiddleResultVo;
|
||||||
|
import com.hospital.front.vo.PeStatusVo;
|
||||||
|
import com.hospital.front.vo.SuggestVo;
|
||||||
|
|
||||||
|
import com.hospital.front.dto.ProcResult;
|
||||||
|
import com.hospital.front.dto.Result;
|
||||||
|
import com.hospital.front.mapper.bsm.BsmOrderMapper;
|
||||||
|
import com.hospital.front.vo.UnitVo;
|
||||||
|
import org.noear.solon.annotation.Inject;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BSM 方言适配器基类(Oracle)。
|
||||||
|
*
|
||||||
|
* 适用医院:xinglongyuan(兴隆园)/ jinghe(泾河)/ qingyang(庆阳)。
|
||||||
|
* 由具体医院子类提供 code / displayName,其余行为完全一致。
|
||||||
|
*/
|
||||||
|
public abstract class BsmAdapterBase implements HospitalAdapter {
|
||||||
|
|
||||||
|
/** 数据库 Mapper(Solon 按类型注入) */
|
||||||
|
@Inject
|
||||||
|
protected BsmOrderMapper mapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String hmType() {
|
||||||
|
return "bsm";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<Boolean> healthCheck() {
|
||||||
|
try {
|
||||||
|
return Result.success(mapper.ping() == 1);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Result.error("数据库连接失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProcResult saveOrder(String peInfoStr, String peItemStr) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("peInfoStr", peInfoStr);
|
||||||
|
map.put("peItemStr", peItemStr);
|
||||||
|
mapper.saveOrder(map);
|
||||||
|
return new ProcResult(
|
||||||
|
str(map.get("resultCode")),
|
||||||
|
str(map.get("errorMsg")),
|
||||||
|
str(map.get("peId"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProcResult cancelOrder(String peId, String name, String idcard) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("peId", peId);
|
||||||
|
map.put("name", name);
|
||||||
|
map.put("idcard", idcard);
|
||||||
|
mapper.cancelOrder(map);
|
||||||
|
return new ProcResult(
|
||||||
|
str(map.get("resultCode")),
|
||||||
|
str(map.get("errorMsg")),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProcResult addUnit(UnitVo unit, boolean useV2) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("unitCode", unit.getUnitCode());
|
||||||
|
map.put("unitName", unit.getUnitName());
|
||||||
|
map.put("parentUnitCode", unit.getParentUnitCode());
|
||||||
|
map.put("address", unit.getAddress());
|
||||||
|
map.put("connecter1", unit.getConnecter1());
|
||||||
|
map.put("phone1", unit.getPhone1());
|
||||||
|
map.put("connecter2", unit.getConnecter2());
|
||||||
|
map.put("phone2", unit.getPhone2());
|
||||||
|
if (useV2) {
|
||||||
|
mapper.addUnitV2(map);
|
||||||
|
} else {
|
||||||
|
mapper.addUnit(map);
|
||||||
|
}
|
||||||
|
return new ProcResult(
|
||||||
|
str(map.get("resultCode")),
|
||||||
|
str(map.get("errorMsg")),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PeStatusVo> getPhyexamStatus(String peIds) {
|
||||||
|
return mapper.getPhyexamStatus(peIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getItemList() {
|
||||||
|
return mapper.getItemList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getAllItemList() {
|
||||||
|
return mapper.getAllItemList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getPeItemList() {
|
||||||
|
return mapper.getPeItemList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getItemInfo(String peItemCode) {
|
||||||
|
return mapper.getItemInfo(peItemCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getEmpItem(String peId) {
|
||||||
|
return mapper.getEmpItem(peId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getResult(String peId) {
|
||||||
|
return mapper.getResult(peId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ConclusionVo> getConclusionAll(String peId) {
|
||||||
|
return mapper.getConclusionAll(peId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SuggestVo> getSuggestAll(String peId) {
|
||||||
|
return mapper.getSuggestAll(peId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PeStatusVo getPeStatusByIdnoAndYear(String idNo, String year) {
|
||||||
|
return mapper.getPeStatusByIdnoAndYear(idNo, year);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MedicalMiddleResultVo> getResultByIdcardAndYear(String idNo, String year) {
|
||||||
|
return mapper.getResultByIdcardAndYear(idNo, year);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MedicalMiddleResultVo> getResultByIdcardAndYearAll(String idNo, String year) {
|
||||||
|
return mapper.getResultByIdcardAndYearAll(idNo, year);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MedicalMiddleResultVo> getResultByPeId(String peId) {
|
||||||
|
return mapper.getResultByPeId(peId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MedicalMiddleResultVo> getItemByPeId(String peId) {
|
||||||
|
return mapper.getItemByPeId(peId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MedicalMiddleResultVo> getItemByPeIdAll(String peId) {
|
||||||
|
return mapper.getItemByPeIdAll(peId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getPeIdAndDateByIdcard(String idNo) {
|
||||||
|
return mapper.getPeIdAndDateByIdcard(idNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getPeIdAndDateByIdcardList(String idNo) {
|
||||||
|
return mapper.getPeIdAndDateByIdcardList(idNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ExamListVo> getExamListByDate(String auditDate) {
|
||||||
|
return mapper.getExamListByDate(auditDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 等价于 Kotlin 的 map[key]?.toString():值为 null 时返回 null,否则转字符串 */
|
||||||
|
private static String str(Object value) {
|
||||||
|
return value == null ? null : String.valueOf(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package com.hospital.front.adapter;
|
||||||
|
|
||||||
|
import com.hospital.front.vo.ConclusionVo;
|
||||||
|
import com.hospital.front.vo.ExamListVo;
|
||||||
|
import com.hospital.front.vo.MedicalMiddleResultVo;
|
||||||
|
import com.hospital.front.vo.PeStatusVo;
|
||||||
|
import com.hospital.front.vo.SuggestVo;
|
||||||
|
|
||||||
|
import com.hospital.front.dto.ProcResult;
|
||||||
|
import com.hospital.front.dto.Result;
|
||||||
|
import com.hospital.front.vo.UnitVo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 医院适配器接口(各医院方言实现:bsm/nx,延安复用 nx)。
|
||||||
|
*
|
||||||
|
* getConclusion/getSuggest 与其 All 版 SQL 完全重复,已合并为 All 版
|
||||||
|
* (对外接口路径 /getConclusion、/getSuggest 保留,内部走 All 版)。
|
||||||
|
*/
|
||||||
|
public interface HospitalAdapter {
|
||||||
|
|
||||||
|
/** 医院编码(唯一,如 jinghe) */
|
||||||
|
String code();
|
||||||
|
|
||||||
|
/** 医院显示名 */
|
||||||
|
String displayName();
|
||||||
|
|
||||||
|
/** 医院方言类型(bsm/nx/ya) */
|
||||||
|
String hmType();
|
||||||
|
|
||||||
|
/** 数据库健康检查 */
|
||||||
|
Result<Boolean> healthCheck();
|
||||||
|
|
||||||
|
/** 提交预约(存储过程) */
|
||||||
|
ProcResult saveOrder(String peInfoStr, String peItemStr);
|
||||||
|
|
||||||
|
/** 取消预约(存储过程) */
|
||||||
|
ProcResult cancelOrder(String peId, String name, String idcard);
|
||||||
|
|
||||||
|
/** 添加/修改单位(存储过程,V1/V2 参数不同) */
|
||||||
|
ProcResult addUnit(UnitVo unit, boolean useV2);
|
||||||
|
|
||||||
|
/** 体检状态查询(多个 peId 逗号分隔) */
|
||||||
|
List<PeStatusVo> getPhyexamStatus(String peIds);
|
||||||
|
|
||||||
|
/** 体检项目查询 */
|
||||||
|
List<Map<String, Object>> getItemList();
|
||||||
|
|
||||||
|
/** 体检指标项及体检组合数据 */
|
||||||
|
List<Map<String, Object>> getAllItemList();
|
||||||
|
|
||||||
|
/** 体检指标项数据 */
|
||||||
|
List<Map<String, Object>> getPeItemList();
|
||||||
|
|
||||||
|
/** 按体检指标项查所属组合 */
|
||||||
|
List<Map<String, Object>> getItemInfo(String peItemCode);
|
||||||
|
|
||||||
|
/** 员工体检项目 */
|
||||||
|
List<Map<String, Object>> getEmpItem(String peId);
|
||||||
|
|
||||||
|
/** 按体检编号查体检结果 */
|
||||||
|
List<Map<String, Object>> getResult(String peId);
|
||||||
|
|
||||||
|
/** 体检结论(列表) */
|
||||||
|
List<ConclusionVo> getConclusionAll(String peId);
|
||||||
|
|
||||||
|
/** 体检建议(全量,内部组装用) */
|
||||||
|
List<SuggestVo> getSuggestAll(String peId);
|
||||||
|
|
||||||
|
/** 按身份证号 + 年份查询体检状态 */
|
||||||
|
PeStatusVo getPeStatusByIdnoAndYear(String idNo, String year);
|
||||||
|
|
||||||
|
/** 按身份证号 + 年份查询体检结果(默认 / 全量) */
|
||||||
|
List<MedicalMiddleResultVo> getResultByIdcardAndYear(String idNo, String year);
|
||||||
|
|
||||||
|
List<MedicalMiddleResultVo> getResultByIdcardAndYearAll(String idNo, String year);
|
||||||
|
|
||||||
|
/** 按体检编号查询体检结果 */
|
||||||
|
List<MedicalMiddleResultVo> getResultByPeId(String peId);
|
||||||
|
|
||||||
|
/** 按体检编号查询体检项目子表(默认 / 全量) */
|
||||||
|
List<MedicalMiddleResultVo> getItemByPeId(String peId);
|
||||||
|
|
||||||
|
List<MedicalMiddleResultVo> getItemByPeIdAll(String peId);
|
||||||
|
|
||||||
|
/** 按身份证号查询当年 peId 与预约日期(单条 / 列表) */
|
||||||
|
Map<String, Object> getPeIdAndDateByIdcard(String idNo);
|
||||||
|
|
||||||
|
List<Map<String, Object>> getPeIdAndDateByIdcardList(String idNo);
|
||||||
|
|
||||||
|
/** 按审核日期查询当天体检人员列表(去重;日期空则当天) */
|
||||||
|
List<ExamListVo> getExamListByDate(String auditDate);
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.hospital.front.adapter;
|
||||||
|
|
||||||
|
import org.noear.solon.Solon;
|
||||||
|
import org.noear.solon.annotation.Component;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 医院适配器路由。
|
||||||
|
*
|
||||||
|
* 通过 Solon.context().getBeanOfType 在首次使用时动态获取所有 HospitalAdapter 实现,
|
||||||
|
* 避免依赖注入时序问题。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class HospitalRouter {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(HospitalRouter.class);
|
||||||
|
|
||||||
|
/** 启动激活的医院编码(默认 jinghe) */
|
||||||
|
private final String activeCode = Solon.cfg().get("hospital.active", "jinghe");
|
||||||
|
|
||||||
|
/** 适配器映射(懒加载,等价于 Kotlin 的 by lazy) */
|
||||||
|
private volatile Map<String, HospitalAdapter> map;
|
||||||
|
|
||||||
|
public HospitalRouter() {
|
||||||
|
// 走 logback 落盘(服务方式运行时 System.out 进不了日志文件)
|
||||||
|
log.info("启动激活医院:{}", activeCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态拉取所有 HospitalAdapter 实现(每次调用都重新拉取,避免 bean 注入时序问题)。
|
||||||
|
*/
|
||||||
|
private List<HospitalAdapter> listAdapters() {
|
||||||
|
return Solon.context().getBeansOfType(HospitalAdapter.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取适配器映射(首次调用时构建) */
|
||||||
|
private Map<String, HospitalAdapter> getMap() {
|
||||||
|
if (map == null) {
|
||||||
|
synchronized (this) {
|
||||||
|
if (map == null) {
|
||||||
|
map = new HashMap<>();
|
||||||
|
for (HospitalAdapter adapter : listAdapters()) {
|
||||||
|
map.put(adapter.code(), adapter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取指定医院编码的适配器 */
|
||||||
|
public HospitalAdapter get(String code) {
|
||||||
|
HospitalAdapter adapter = getMap().get(code);
|
||||||
|
if (adapter == null) {
|
||||||
|
throw new IllegalStateException("未注册医院适配器:" + code + "(已注册:" + getMap().keySet() + ")");
|
||||||
|
}
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取启动激活的医院适配器 */
|
||||||
|
public HospitalAdapter active() {
|
||||||
|
return get(activeCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前激活医院编码 */
|
||||||
|
public String activeCode() {
|
||||||
|
return activeCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 所有已注册的医院编码 */
|
||||||
|
public Set<String> registeredCodes() {
|
||||||
|
return getMap().keySet();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.hospital.front.adapter;
|
||||||
|
|
||||||
|
import org.noear.solon.annotation.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 泾河医院适配器(Oracle,bsm 方言,默认激活医院)。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class JingheAdapter extends BsmAdapterBase {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String code() {
|
||||||
|
return "jinghe";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String displayName() {
|
||||||
|
return "泾河医院";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.hospital.front.adapter;
|
||||||
|
|
||||||
|
import org.noear.solon.annotation.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 宁夏宝石花医院适配器(SqlServer,nx 方言)。
|
||||||
|
*
|
||||||
|
* 业务开关(在 OrderService 中按配置生效):
|
||||||
|
* - hm.allsecondFlag=true:强制二级单位编码/名称截断
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class NingxiaAdapter extends NxAdapterBase {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String code() {
|
||||||
|
return "ningxia";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String displayName() {
|
||||||
|
return "宁夏宝石花医院";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package com.hospital.front.adapter;
|
||||||
|
|
||||||
|
import com.hospital.front.vo.ConclusionVo;
|
||||||
|
import com.hospital.front.vo.ExamListVo;
|
||||||
|
import com.hospital.front.vo.MedicalMiddleResultVo;
|
||||||
|
import com.hospital.front.vo.PeStatusVo;
|
||||||
|
import com.hospital.front.vo.SuggestVo;
|
||||||
|
|
||||||
|
import com.hospital.front.dto.ProcResult;
|
||||||
|
import com.hospital.front.dto.Result;
|
||||||
|
import com.hospital.front.mapper.nx.NxOrderMapper;
|
||||||
|
import com.hospital.front.vo.UnitVo;
|
||||||
|
import org.noear.solon.annotation.Inject;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NX 方言适配器基类(SqlServer)。
|
||||||
|
*
|
||||||
|
* 适用医院:ningxia(宁夏宝石花)/ yanan(延安,库结构已与宁夏完全一致)。
|
||||||
|
*/
|
||||||
|
public abstract class NxAdapterBase implements HospitalAdapter {
|
||||||
|
|
||||||
|
/** 数据库 Mapper(Solon 按类型注入) */
|
||||||
|
@Inject
|
||||||
|
protected NxOrderMapper mapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String hmType() {
|
||||||
|
return "nx";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<Boolean> healthCheck() {
|
||||||
|
try {
|
||||||
|
return Result.success(mapper.ping() == 1);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Result.error("数据库连接失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProcResult saveOrder(String peInfoStr, String peItemStr) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("peInfoStr", peInfoStr);
|
||||||
|
map.put("peItemStr", peItemStr);
|
||||||
|
mapper.saveOrder(map);
|
||||||
|
return new ProcResult(
|
||||||
|
str(map.get("resultCode")),
|
||||||
|
str(map.get("errorMsg")),
|
||||||
|
str(map.get("peId"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProcResult cancelOrder(String peId, String name, String idcard) {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("peId", peId);
|
||||||
|
map.put("name", name);
|
||||||
|
map.put("idcard", idcard);
|
||||||
|
mapper.cancelOrder(map);
|
||||||
|
return new ProcResult(
|
||||||
|
str(map.get("resultCode")),
|
||||||
|
str(map.get("errorMsg")),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProcResult addUnit(UnitVo unit, boolean useV2) {
|
||||||
|
// 宁夏使用 addUnitV2(原 OrderServiceImpl 中 nx 分支固定走 V2)
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put("unitCode", unit.getUnitCode());
|
||||||
|
map.put("unitName", unit.getUnitName());
|
||||||
|
map.put("parentUnitCode", unit.getParentUnitCode());
|
||||||
|
map.put("address", unit.getAddress());
|
||||||
|
map.put("connecter1", unit.getConnecter1());
|
||||||
|
map.put("phone1", unit.getPhone1());
|
||||||
|
map.put("connecter2", unit.getConnecter2());
|
||||||
|
map.put("phone2", unit.getPhone2());
|
||||||
|
mapper.addUnitV2(map);
|
||||||
|
return new ProcResult(
|
||||||
|
str(map.get("resultCode")),
|
||||||
|
str(map.get("errorMsg")),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PeStatusVo> getPhyexamStatus(String peIds) {
|
||||||
|
return mapper.getPhyexamStatus(peIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getItemList() {
|
||||||
|
return mapper.getItemList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getAllItemList() {
|
||||||
|
return mapper.getAllItemList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getPeItemList() {
|
||||||
|
return mapper.getPeItemList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getItemInfo(String peItemCode) {
|
||||||
|
return mapper.getItemInfo(peItemCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getEmpItem(String peId) {
|
||||||
|
return mapper.getEmpItem(peId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getResult(String peId) {
|
||||||
|
return mapper.getResult(peId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ConclusionVo> getConclusionAll(String peId) {
|
||||||
|
return mapper.getConclusionAll(peId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SuggestVo> getSuggestAll(String peId) {
|
||||||
|
return mapper.getSuggestAll(peId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PeStatusVo getPeStatusByIdnoAndYear(String idNo, String year) {
|
||||||
|
return mapper.getPeStatusByIdnoAndYear(idNo, year);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MedicalMiddleResultVo> getResultByIdcardAndYear(String idNo, String year) {
|
||||||
|
return mapper.getResultByIdcardAndYear(idNo, year);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MedicalMiddleResultVo> getResultByIdcardAndYearAll(String idNo, String year) {
|
||||||
|
return mapper.getResultByIdcardAndYearAll(idNo, year);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MedicalMiddleResultVo> getResultByPeId(String peId) {
|
||||||
|
return mapper.getResultByPeId(peId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MedicalMiddleResultVo> getItemByPeId(String peId) {
|
||||||
|
return mapper.getItemByPeId(peId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MedicalMiddleResultVo> getItemByPeIdAll(String peId) {
|
||||||
|
return mapper.getItemByPeIdAll(peId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getPeIdAndDateByIdcard(String idNo) {
|
||||||
|
return mapper.getPeIdAndDateByIdcard(idNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getPeIdAndDateByIdcardList(String idNo) {
|
||||||
|
return mapper.getPeIdAndDateByIdcardList(idNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ExamListVo> getExamListByDate(String auditDate) {
|
||||||
|
return mapper.getExamListByDate(auditDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 等价于 Kotlin 的 map[key]?.toString():值为 null 时返回 null,否则转字符串 */
|
||||||
|
private static String str(Object value) {
|
||||||
|
return value == null ? null : String.valueOf(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.hospital.front.adapter;
|
||||||
|
|
||||||
|
import org.noear.solon.annotation.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 庆阳医院适配器(Oracle,bsm 方言)。
|
||||||
|
*
|
||||||
|
* 数据库连接暂与泾河一致,正式环境就绪后调整 pac/conf/app-qingyang.yml。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class QingyangAdapter extends BsmAdapterBase {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String code() {
|
||||||
|
return "qingyang";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String displayName() {
|
||||||
|
return "庆阳医院";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.hospital.front.adapter;
|
||||||
|
|
||||||
|
import org.noear.solon.annotation.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兴隆园医院适配器(Oracle,bsm 方言)。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class XingLongYuanAdapter extends BsmAdapterBase {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String code() {
|
||||||
|
return "xinglongyuan";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String displayName() {
|
||||||
|
return "兴隆园医院";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.hospital.front.adapter;
|
||||||
|
|
||||||
|
import org.noear.solon.annotation.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 延安医院适配器(SqlServer,nx 方言)。
|
||||||
|
*
|
||||||
|
* 延安数据库结构已与宁夏完全一致(V_PHYEXAM_* 视图 + P_ADD_* 存储过程),
|
||||||
|
* 仅连接配置不同,直接复用 NxAdapterBase。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class YananAdapter extends NxAdapterBase {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String code() {
|
||||||
|
return "yanan";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String displayName() {
|
||||||
|
return "延安医院";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,416 @@
|
|||||||
|
package com.hospital.front.api.admin;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.noear.solon.Solon;
|
||||||
|
import org.noear.solon.annotation.Controller;
|
||||||
|
import org.noear.solon.annotation.Inject;
|
||||||
|
import org.noear.solon.annotation.Mapping;
|
||||||
|
import org.noear.solon.annotation.Param;
|
||||||
|
import org.noear.solon.core.handle.Context;
|
||||||
|
import org.noear.solon.core.handle.MethodType;
|
||||||
|
|
||||||
|
import com.hospital.front.adapter.HospitalRouter;
|
||||||
|
import com.hospital.front.dto.Result;
|
||||||
|
import com.hospital.front.infra.DbHealthMonitor;
|
||||||
|
import com.hospital.front.infra.exception.BusinessException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主程序被控端 API(/api/admin)。
|
||||||
|
*
|
||||||
|
* 供同机 Agent(环回地址)调用,实现远程运维:
|
||||||
|
* - 健康检查 / 版本查询 / JVM 指标
|
||||||
|
* - 配置查看(脱敏)
|
||||||
|
* - 日志文件列表 / 尾部查看 / 全量下载
|
||||||
|
* - 优雅重启
|
||||||
|
*
|
||||||
|
* 鉴权:请求头 X-Admin-Token 与配置 admin.token 比对(未配置时放行,仅本机使用)。
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
@Mapping("/api/admin")
|
||||||
|
public class AdminApi {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private HospitalRouter router;
|
||||||
|
|
||||||
|
/** DB 健康缓存监测(health 接口读缓存秒回,避免 DB 慢时阻塞被 Agent 误判宕机) */
|
||||||
|
@Inject
|
||||||
|
private DbHealthMonitor dbHealthMonitor;
|
||||||
|
|
||||||
|
/** 日志目录(与 logback.xml 的 log.path 保持一致) */
|
||||||
|
private File getLogDir() {
|
||||||
|
return new File(Solon.cfg().get("log.path", "./log"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 鉴权 =================
|
||||||
|
|
||||||
|
private void checkToken(Context ctx) {
|
||||||
|
String expected = Solon.cfg().get("admin.token", "");
|
||||||
|
if (expected != null && !expected.trim().isEmpty() && !expected.equals(ctx.header("X-Admin-Token"))) {
|
||||||
|
throw BusinessException.of("管理接口鉴权失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 1. 健康与指标 =================
|
||||||
|
|
||||||
|
/** 健康检查(进程健康即时返回;DB 状态读后台探测缓存,不阻塞) */
|
||||||
|
@Mapping("/health")
|
||||||
|
public Result<Map<String, Object>> health(Context ctx) {
|
||||||
|
checkToken(ctx);
|
||||||
|
Runtime mem = Runtime.getRuntime();
|
||||||
|
Result<Boolean> db = dbHealthMonitor.last();
|
||||||
|
Map<String, Object> out = new LinkedHashMap<>();
|
||||||
|
// db=null 表示后台首轮探测未完成(启动/唤醒初期),进程本身健康,报 STARTING 而非 DEGRADED
|
||||||
|
out.put("status", db == null ? "STARTING" : (db.isSuccess() ? "UP" : "DEGRADED"));
|
||||||
|
out.put("hospital", router.activeCode());
|
||||||
|
out.put("hospitalName", router.active().displayName());
|
||||||
|
out.put("registeredHospitals", router.registeredCodes());
|
||||||
|
Map<String, Object> database = new HashMap<>();
|
||||||
|
database.put("ok", db != null && db.isSuccess());
|
||||||
|
database.put("msg", db != null ? db.getMsg() : "DB 探测进行中");
|
||||||
|
out.put("database", database);
|
||||||
|
Map<String, Object> jvm = new HashMap<>();
|
||||||
|
jvm.put("totalMemoryMb", mem.totalMemory() / 1048576);
|
||||||
|
jvm.put("freeMemoryMb", mem.freeMemory() / 1048576);
|
||||||
|
jvm.put("maxMemoryMb", mem.maxMemory() / 1048576);
|
||||||
|
jvm.put("availableProcessors", mem.availableProcessors());
|
||||||
|
out.put("jvm", jvm);
|
||||||
|
out.put("uptimeSec", ManagementFactory.getRuntimeMXBean().getUptime() / 1000);
|
||||||
|
out.put("pid", ProcessHandle.current().pid());
|
||||||
|
return Result.success(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 版本信息(读 MANIFEST) */
|
||||||
|
@Mapping("/version")
|
||||||
|
public Result<Map<String, String>> version(Context ctx) {
|
||||||
|
checkToken(ctx);
|
||||||
|
Package pkg = this.getClass().getPackage();
|
||||||
|
Map<String, String> out = new LinkedHashMap<>();
|
||||||
|
out.put("app", "hospital-front");
|
||||||
|
out.put("version", pkg != null && pkg.getImplementationVersion() != null ? pkg.getImplementationVersion() : "1.0.0-dev");
|
||||||
|
out.put("hospital", router.activeCode());
|
||||||
|
out.put("javaVersion", System.getProperty("java.version"));
|
||||||
|
out.put("startTime", String.valueOf(ManagementFactory.getRuntimeMXBean().getStartTime()));
|
||||||
|
return Result.success(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 2. 配置管理 =================
|
||||||
|
|
||||||
|
/** 查看当前生效配置(密码脱敏) */
|
||||||
|
@Mapping("/config")
|
||||||
|
public Result<Map<String, Object>> config(Context ctx) {
|
||||||
|
checkToken(ctx);
|
||||||
|
List<String> keys = Arrays.asList(
|
||||||
|
"hospital.active", "hospital.id",
|
||||||
|
"hm.type", "hm.ver", "hm.allsecondFlag", "hm.occFlag",
|
||||||
|
"server.port", "admin.token", "log.path");
|
||||||
|
Map<String, Object> conf = new LinkedHashMap<>();
|
||||||
|
for (String key : keys) {
|
||||||
|
conf.put(key, Solon.cfg().get(key, ""));
|
||||||
|
}
|
||||||
|
// 数据源脱敏(db1! 的 ! 在 cfg 键中已剥离,两种写法都尝试)
|
||||||
|
Map<String, Object> ds = new LinkedHashMap<>();
|
||||||
|
ds.put("db1.jdbcUrl", orDefault(
|
||||||
|
Solon.cfg().get("solon.dataSources.db1.jdbcUrl"),
|
||||||
|
orDefault(Solon.cfg().get("solon.dataSources.db1!.jdbcUrl"), "")));
|
||||||
|
ds.put("db1.username", orDefault(
|
||||||
|
Solon.cfg().get("solon.dataSources.db1.username"),
|
||||||
|
orDefault(Solon.cfg().get("solon.dataSources.db1!.username"), "")));
|
||||||
|
ds.put("db1.password", "******");
|
||||||
|
Map<String, Object> out = new HashMap<>();
|
||||||
|
out.put("config", conf);
|
||||||
|
out.put("dataSource", ds);
|
||||||
|
return Result.success(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kotlin elvis 的等价实现 */
|
||||||
|
private static String orDefault(String value, String fallback) {
|
||||||
|
return value != null ? value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出 pac/conf 目录下的医院配置文件内容(供 B 端查看/修改)。
|
||||||
|
*/
|
||||||
|
@Mapping("/config/file")
|
||||||
|
public Result<String> configFile(Context ctx, @Param("hospital") String hospital) {
|
||||||
|
checkToken(ctx);
|
||||||
|
File file = new File("conf/app-" + hospital + ".yml");
|
||||||
|
if (!file.exists()) {
|
||||||
|
throw BusinessException.paramMissing("配置文件 conf/app-" + hospital + ".yml 不存在");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Result.success(new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写回医院配置文件(B 端修改后下发)。重启后生效。
|
||||||
|
*/
|
||||||
|
@Mapping(value = "/config/file", method = {MethodType.POST})
|
||||||
|
public Result<?> saveConfigFile(Context ctx, @Param("hospital") String hospital, @Param(name = "content") String content) {
|
||||||
|
checkToken(ctx);
|
||||||
|
if (!Pattern.matches("[a-z]+", hospital)) {
|
||||||
|
throw BusinessException.businessFailed("非法医院编码:" + hospital);
|
||||||
|
}
|
||||||
|
File file = new File("conf/app-" + hospital + ".yml");
|
||||||
|
if (file.getParentFile() != null) {
|
||||||
|
file.getParentFile().mkdirs();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 3. 日志管理 =================
|
||||||
|
|
||||||
|
/** 日志文件列表 */
|
||||||
|
@Mapping("/log/files")
|
||||||
|
public Result<List<Map<String, Object>>> logFiles(Context ctx) {
|
||||||
|
checkToken(ctx);
|
||||||
|
File dir = getLogDir();
|
||||||
|
if (!dir.exists()) {
|
||||||
|
return Result.success(new ArrayList<>());
|
||||||
|
}
|
||||||
|
File[] files = dir.listFiles(File::isFile);
|
||||||
|
if (files == null) {
|
||||||
|
return Result.success(new ArrayList<>());
|
||||||
|
}
|
||||||
|
Arrays.sort(files, (a, b) -> Long.compare(b.lastModified(), a.lastModified()));
|
||||||
|
List<Map<String, Object>> out = new ArrayList<>();
|
||||||
|
for (File f : files) {
|
||||||
|
if (out.size() >= 50) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Map<String, Object> item = new HashMap<>();
|
||||||
|
item.put("name", f.getName());
|
||||||
|
item.put("sizeBytes", f.length());
|
||||||
|
item.put("lastModified", f.lastModified());
|
||||||
|
out.add(item);
|
||||||
|
}
|
||||||
|
return Result.success(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 日志尾部 N 行(默认 200);容错解码(历史日志可能混有 GBK 内容) */
|
||||||
|
@Mapping("/log/tail")
|
||||||
|
public Result<String> logTail(Context ctx, @Param("file") String file, @Param("lines") Integer lines) {
|
||||||
|
checkToken(ctx);
|
||||||
|
File target = safeLogFile(file);
|
||||||
|
int n = Math.max(1, Math.min(lines != null ? lines : 200, 2000));
|
||||||
|
java.nio.charset.CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
|
||||||
|
.onMalformedInput(java.nio.charset.CodingErrorAction.REPLACE)
|
||||||
|
.onUnmappableCharacter(java.nio.charset.CodingErrorAction.REPLACE);
|
||||||
|
try {
|
||||||
|
String text = decoder.decode(ByteBuffer.wrap(Files.readAllBytes(target.toPath()))).toString();
|
||||||
|
String[] all = text.split("\n", -1);
|
||||||
|
int from = Math.max(0, all.length - n);
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = from; i < all.length; i++) {
|
||||||
|
if (i > from) {
|
||||||
|
sb.append('\n');
|
||||||
|
}
|
||||||
|
sb.append(all[i]);
|
||||||
|
}
|
||||||
|
return Result.success(sb.toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 日志全量下载 */
|
||||||
|
@Mapping("/log/download")
|
||||||
|
public void logDownload(Context ctx, @Param("file") String file) {
|
||||||
|
checkToken(ctx);
|
||||||
|
File target = safeLogFile(file);
|
||||||
|
ctx.contentType("application/octet-stream");
|
||||||
|
ctx.headerSet("Content-Disposition", "attachment; filename=\"" + target.getName() + "\"");
|
||||||
|
try {
|
||||||
|
Files.copy(target.toPath(), ctx.outputStream());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 限制只能读日志目录内的文件(防路径穿越) */
|
||||||
|
private File safeLogFile(String name) {
|
||||||
|
if (name.contains("..") || name.contains("/") || name.contains("\\")) {
|
||||||
|
throw BusinessException.businessFailed("非法文件名");
|
||||||
|
}
|
||||||
|
File f = new File(getLogDir(), name);
|
||||||
|
if (!f.exists()) {
|
||||||
|
throw BusinessException.paramMissing("日志文件不存在:" + name);
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 4. 进程管理 =================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优雅重启:记录标记后延迟退出,Agent 检测到进程退出后拉起新进程。
|
||||||
|
*/
|
||||||
|
@Mapping(value = "/restart", method = {MethodType.POST})
|
||||||
|
public Result<?> restart(Context ctx, @Param("delayMs") Long delayMs) {
|
||||||
|
checkToken(ctx);
|
||||||
|
long delay = Math.max(0, Math.min(delayMs != null ? delayMs : 1000L, 10000L));
|
||||||
|
Thread t = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
Thread.sleep(delay);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
Solon.stopBlock(); // 触发 Solon 停机事件(优雅退出)
|
||||||
|
});
|
||||||
|
t.setDaemon(true);
|
||||||
|
t.start();
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 5. 接口代理(供 B 端接口测试工具直连调用) =================
|
||||||
|
|
||||||
|
/** httpProxy 请求体(Solon 自动绑定 JSON 请求体) */
|
||||||
|
public static class HttpProxyReq {
|
||||||
|
/** HTTP 方法:GET/POST/PUT/DELETE */
|
||||||
|
public String method = "GET";
|
||||||
|
/** 接口路径,如 /order/getItemList */
|
||||||
|
public String path;
|
||||||
|
/** query 参数(拼到 URL 上) */
|
||||||
|
public Map<String, String> query;
|
||||||
|
/** 请求体字符串(POST/PUT 时使用) */
|
||||||
|
public String body;
|
||||||
|
/** 额外请求头 */
|
||||||
|
public Map<String, String> headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 代理专用 HttpClient:环回调用本机接口,读超时 120s 适配大接口 */
|
||||||
|
private final java.net.http.HttpClient proxyHttp = java.net.http.HttpClient.newBuilder()
|
||||||
|
.connectTimeout(java.time.Duration.ofSeconds(5))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP 代理:在 main 进程内环回调用本机接口,原样透传响应。
|
||||||
|
*
|
||||||
|
* B 端接口测试工具经此代理访问业务接口,免去 Agent WS 通道倒手
|
||||||
|
* (WS 通道有 2MB 消息上限与 60s 等待超时,大接口会超时/掉线)。
|
||||||
|
* 自动按 path 附加鉴权头:/order/** → Basic(order.basic 配置),
|
||||||
|
* /api/admin/** → X-Admin-Token。
|
||||||
|
*/
|
||||||
|
@Mapping(value = "/httpProxy", method = {MethodType.POST})
|
||||||
|
public Result<Map<String, Object>> httpProxy(Context ctx, HttpProxyReq req) {
|
||||||
|
checkToken(ctx);
|
||||||
|
// 参数校验与路径白名单:只允许代理本机 /order/** 与 /api/admin/**
|
||||||
|
if (req == null || req.path == null || req.path.isEmpty()) {
|
||||||
|
throw BusinessException.paramMissing("path 不能为空");
|
||||||
|
}
|
||||||
|
String path = req.path.trim();
|
||||||
|
if (!path.startsWith("/order/") && !path.startsWith("/api/admin/")) {
|
||||||
|
throw BusinessException.businessFailed("仅允许代理 /order/** 与 /api/admin/** 路径:" + path);
|
||||||
|
}
|
||||||
|
// 拼接环回 URL:server.port + path + query
|
||||||
|
String port = Solon.cfg().get("server.port", "8080");
|
||||||
|
StringBuilder url = new StringBuilder("http://127.0.0.1:").append(port).append(path);
|
||||||
|
if (req.query != null && !req.query.isEmpty()) {
|
||||||
|
StringBuilder qs = new StringBuilder();
|
||||||
|
for (Map.Entry<String, String> e : req.query.entrySet()) {
|
||||||
|
if (e.getKey() == null || e.getValue() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (qs.length() > 0) {
|
||||||
|
qs.append('&');
|
||||||
|
}
|
||||||
|
qs.append(java.net.URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8))
|
||||||
|
.append('=')
|
||||||
|
.append(java.net.URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
if (qs.length() > 0) {
|
||||||
|
url.append('?').append(qs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 构建请求:按 path 附加鉴权头
|
||||||
|
java.net.http.HttpRequest.Builder rb = java.net.http.HttpRequest.newBuilder(URI_COMPAT.apply(url.toString()));
|
||||||
|
String m = req.method == null ? "GET" : req.method.toUpperCase();
|
||||||
|
java.net.http.HttpRequest.BodyPublisher pub = req.body == null || req.body.isEmpty()
|
||||||
|
? java.net.http.HttpRequest.BodyPublishers.noBody()
|
||||||
|
: java.net.http.HttpRequest.BodyPublishers.ofString(req.body, StandardCharsets.UTF_8);
|
||||||
|
switch (m) {
|
||||||
|
case "POST" -> rb.POST(pub);
|
||||||
|
case "PUT" -> rb.PUT(pub);
|
||||||
|
case "DELETE" -> rb.DELETE();
|
||||||
|
default -> rb.GET();
|
||||||
|
}
|
||||||
|
if (path.startsWith("/order/")) {
|
||||||
|
String basic = Solon.cfg().get("order.basic", "");
|
||||||
|
boolean authFlag = !"false".equals(Solon.cfg().get("order.authFlag", "true"));
|
||||||
|
if (authFlag && basic != null && !basic.isEmpty()) {
|
||||||
|
// 对齐 OrderApi checkBasic 的校验格式:Basic + Base64(user:password)
|
||||||
|
rb.header("Authorization", "Basic "
|
||||||
|
+ Base64.getEncoder().encodeToString(basic.getBytes(StandardCharsets.UTF_8)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rb.header("X-Admin-Token", Solon.cfg().get("admin.token", ""));
|
||||||
|
}
|
||||||
|
if (req.headers != null) {
|
||||||
|
for (Map.Entry<String, String> e : req.headers.entrySet()) {
|
||||||
|
if (e.getKey() != null && e.getValue() != null) {
|
||||||
|
rb.header(e.getKey(), e.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rb.timeout(java.time.Duration.ofSeconds(120));
|
||||||
|
// 执行并透传
|
||||||
|
try {
|
||||||
|
java.net.http.HttpResponse<String> resp = proxyHttp.send(rb.build(),
|
||||||
|
java.net.http.HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||||
|
Map<String, Object> out = new LinkedHashMap<>();
|
||||||
|
out.put("status", resp.statusCode());
|
||||||
|
out.put("body", resp.body());
|
||||||
|
return Result.success(out);
|
||||||
|
} catch (java.io.IOException e) {
|
||||||
|
throw BusinessException.businessFailed("环回调用失败(本机 " + path + "):" + e.getMessage());
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw BusinessException.businessFailed("环回调用被中断:" + path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** URI 构建容错(URL 已校验,异常时给出可读错误) */
|
||||||
|
private static final java.util.function.Function<String, URI> URI_COMPAT = s -> {
|
||||||
|
try {
|
||||||
|
return URI.create(s);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw BusinessException.paramMissing("非法 URL:" + s);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ================= 6. 接口清单(供 B 端接口测试工具加载) =================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 /order 业务接口元数据清单。
|
||||||
|
*
|
||||||
|
* 反射扫描 OrderApi 的 @ApiMeta 注解自动生成,无需手写维护。
|
||||||
|
* B 端因前置机白名单无法直连,通过 Agent 通道拉取本接口清单,
|
||||||
|
* 再经 Agent HTTP_PROXY 代理调用具体接口。
|
||||||
|
* 走 X-Admin-Token 鉴权(与其它管理接口一致)。
|
||||||
|
*/
|
||||||
|
@Mapping("/apis")
|
||||||
|
public Result<List<Map<String, Object>>> apis(Context ctx) {
|
||||||
|
checkToken(ctx);
|
||||||
|
return Result.success(com.hospital.front.infra.ApiScanner.scan(com.hospital.front.api.order.OrderApi.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
package com.hospital.front.api.order;
|
||||||
|
|
||||||
|
import com.hospital.front.vo.MedicalMiddleResultVo;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.noear.solon.annotation.Body;
|
||||||
|
import org.noear.solon.annotation.Controller;
|
||||||
|
import org.noear.solon.annotation.Inject;
|
||||||
|
import org.noear.solon.annotation.Mapping;
|
||||||
|
import org.noear.solon.annotation.Param;
|
||||||
|
import org.noear.solon.annotation.Path;
|
||||||
|
import org.noear.solon.core.handle.Context;
|
||||||
|
import org.noear.solon.core.handle.MethodType;
|
||||||
|
import org.noear.solon.Solon;
|
||||||
|
|
||||||
|
import com.hospital.front.dto.Result;
|
||||||
|
import com.hospital.front.infra.annotation.ApiMeta;
|
||||||
|
import com.hospital.front.infra.annotation.ApiParam;
|
||||||
|
import com.hospital.front.infra.exception.BusinessException;
|
||||||
|
import com.hospital.front.service.OrderService;
|
||||||
|
import com.hospital.front.vo.ExamListVo;
|
||||||
|
import com.hospital.front.vo.PeStatusPhyVo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 体检预约业务接口,路径前缀 /order。
|
||||||
|
*
|
||||||
|
* 路径与出参结构保持与原 hospitalmiddle 一致(调用方契约,不可修改)。
|
||||||
|
* 根路径前缀为 /order,各方法 Mapping 为相对路径。
|
||||||
|
*
|
||||||
|
* 鉴权:对齐原程序 Sa-Token 的 SaBasicUtil.check(),对 /order/** 做 HTTP Basic 认证。
|
||||||
|
* 配置 order.basic(格式 "user:password",未配置时放行——与原程序 authFlag=true 且
|
||||||
|
* basic 为空时行为一致,仅建议生产环境必配)。
|
||||||
|
*
|
||||||
|
* 每个业务方法标注 @ApiMeta,供 /api/admin/apis 反射扫描自动生成接口清单(B 端接口测试工具用)。
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
@Mapping("/order")
|
||||||
|
public class OrderApi {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private OrderService service;
|
||||||
|
|
||||||
|
// ================= 0. 鉴权 =================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全接口 Basic 认证(等价原程序 SaServletFilter 对 /order/** 的 SaBasicUtil.check())。
|
||||||
|
* order.authFlag 总开关(默认 true);order.basic("user:password")为账号。
|
||||||
|
* 校验失败返回 401 并带 WWW-Authenticate 头(浏览器弹登录框、B 端感知未授权)。
|
||||||
|
*/
|
||||||
|
private void checkBasic(Context ctx) {
|
||||||
|
// authFlag=false 时整体关闭鉴权(对齐原程序 sa-token.authFlag 开关语义)
|
||||||
|
if (!Solon.cfg().getBool("order.authFlag", true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String expected = Solon.cfg().get("order.basic", "");
|
||||||
|
if (expected == null || expected.trim().isEmpty()) {
|
||||||
|
return; // 未配置账号放行(兼容与调试;生产环境必须配置)
|
||||||
|
}
|
||||||
|
|
||||||
|
String auth = ctx.header("Authorization");
|
||||||
|
if (auth != null && auth.startsWith("Basic ")) {
|
||||||
|
try {
|
||||||
|
String decoded = new String(Base64.getDecoder().decode(auth.substring(6).trim()),
|
||||||
|
java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
if (constantTimeEquals(expected, decoded)) {
|
||||||
|
return; // 认证通过
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// base64 非法 → 按认证失败处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 认证失败:401 + 质询头(对齐 Sa-Token SaBasicUtil.check 的响应行为)
|
||||||
|
ctx.status(401);
|
||||||
|
ctx.headerSet("WWW-Authenticate", "Basic realm=\"hospital-front\"");
|
||||||
|
throw BusinessException.of("Basic 认证失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 恒定时间字符串比较(防时序攻击) */
|
||||||
|
private static boolean constantTimeEquals(String a, String b) {
|
||||||
|
return java.security.MessageDigest.isEqual(
|
||||||
|
a.getBytes(java.nio.charset.StandardCharsets.UTF_8),
|
||||||
|
b.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 1. 预约提交 / 取消 =================
|
||||||
|
|
||||||
|
/** 4. 提交预约保存 */
|
||||||
|
@ApiMeta(desc = "提交预约保存", params = {
|
||||||
|
@ApiParam(name = "body", desc = "预约 JSON 体")
|
||||||
|
})
|
||||||
|
@Mapping(value = "/saveOrder", method = {MethodType.POST})
|
||||||
|
public Result<String> saveOrder(Context ctx, @Body com.hospital.front.vo.OrderV2Vo body) {
|
||||||
|
checkBasic(ctx);
|
||||||
|
// 强类型 DTO 直收(Solon 对 DTO 反序列化无问题),避免 Map 中转
|
||||||
|
return Result.success(service.saveOrder(body));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 5. 取消体检预约 */
|
||||||
|
@ApiMeta(desc = "取消体检预约", params = {
|
||||||
|
@ApiParam(name = "peId", desc = "体检编号"),
|
||||||
|
@ApiParam(name = "name", desc = "姓名"),
|
||||||
|
@ApiParam(name = "idcard", desc = "身份证号")
|
||||||
|
})
|
||||||
|
@Mapping(value = "/cancelOrder/{peId}", method = {MethodType.POST})
|
||||||
|
public Result<?> cancelOrder(
|
||||||
|
Context ctx,
|
||||||
|
@Path("peId") String peId,
|
||||||
|
@Param("name") String name,
|
||||||
|
@Param("idcard") String idcard) {
|
||||||
|
checkBasic(ctx);
|
||||||
|
service.cancelOrder(peId, name, idcard);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 2. 体检状态 =================
|
||||||
|
|
||||||
|
/** 3. 体检状态查询(多个 peId 逗号分隔) */
|
||||||
|
@ApiMeta(desc = "体检状态查询(多个 peId 逗号分隔)", params = {
|
||||||
|
@ApiParam(name = "peIds", desc = "体检编号逗号分隔")
|
||||||
|
})
|
||||||
|
@Mapping("/getPeStatus/{peIds}")
|
||||||
|
public Result<List<PeStatusPhyVo>> getPeStatus(Context ctx, @Path("peIds") String peIds) {
|
||||||
|
checkBasic(ctx);
|
||||||
|
return Result.success(service.getPhyexamStatus(peIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 13. 根据身份证号及年份获取当前体检状态及体检项状态 */
|
||||||
|
@ApiMeta(desc = "体检状态及体检项状态", params = {
|
||||||
|
@ApiParam(name = "idNo", desc = "身份证号"),
|
||||||
|
@ApiParam(name = "year", desc = "年份")
|
||||||
|
})
|
||||||
|
@Mapping("/getPeStatusAndItemList/{idNo}/{year}")
|
||||||
|
public Result<Map<String, Object>> getPeStatusAndItemList(Context ctx, @Path("idNo") String idNo, @Path("year") String year) {
|
||||||
|
checkBasic(ctx);
|
||||||
|
return Result.success(service.getPeStatusByIdnoAndYear(idNo, year));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 3. 体检项目 =================
|
||||||
|
|
||||||
|
/** 1. 查询员工体检项目 */
|
||||||
|
@ApiMeta(desc = "员工体检项目", params = {
|
||||||
|
@ApiParam(name = "peId", desc = "体检编号")
|
||||||
|
})
|
||||||
|
@Mapping("/getEmpItem/{peId}")
|
||||||
|
public Result<List<Map<String, Object>>> getEmpItem(Context ctx, @Path("peId") String peId) {
|
||||||
|
checkBasic(ctx);
|
||||||
|
return Result.success(service.getEmpItem(peId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 2. 体检项目查询 */
|
||||||
|
@ApiMeta(desc = "体检项目查询")
|
||||||
|
@Mapping("/getItemList")
|
||||||
|
public Result<List<Map<String, Object>>> getItemList(Context ctx) {
|
||||||
|
checkBasic(ctx);
|
||||||
|
return Result.success(service.getItemList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 6. 体检指标项数据获取 */
|
||||||
|
@ApiMeta(desc = "体检指标项数据获取")
|
||||||
|
@Mapping("/getPeItemList")
|
||||||
|
public Result<List<Map<String, Object>>> getPeItemList(Context ctx) {
|
||||||
|
checkBasic(ctx);
|
||||||
|
return Result.success(service.getPeItemList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 6.1 体检指标项及体检组合数据获取 */
|
||||||
|
@ApiMeta(desc = "体检指标项及体检组合数据获取")
|
||||||
|
@Mapping("/getAllItemList")
|
||||||
|
public Result<List<Map<String, Object>>> getAllItemList(Context ctx) {
|
||||||
|
checkBasic(ctx);
|
||||||
|
return Result.success(service.getAllItemList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 7. 根据体检指标项查询所属体检组合项 */
|
||||||
|
@ApiMeta(desc = "按体检指标项查所属组合", params = {
|
||||||
|
@ApiParam(name = "peItemCode", desc = "体检指标项编码")
|
||||||
|
})
|
||||||
|
@Mapping("/getItemInfo/{peItemCode}")
|
||||||
|
public Result<List<Map<String, Object>>> getItemInfo(Context ctx, @Path("peItemCode") String peItemCode) {
|
||||||
|
checkBasic(ctx);
|
||||||
|
return Result.success(service.getItemInfo(peItemCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 4. 体检结果 / 结论 / 建议 =================
|
||||||
|
|
||||||
|
/** 8. 根据体检编号获取员工体检结果 */
|
||||||
|
@ApiMeta(desc = "按体检编号查员工体检结果", params = {
|
||||||
|
@ApiParam(name = "peId", desc = "体检编号")
|
||||||
|
})
|
||||||
|
@Mapping("/getResult/{peId}")
|
||||||
|
public Result<List<Map<String, Object>>> getResult(Context ctx, @Path("peId") String peId) {
|
||||||
|
checkBasic(ctx);
|
||||||
|
return Result.success(service.getResult(peId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9/10/11/12(体检结论/建议 汇总与列表)不再对外暴露:结论/建议仅作为
|
||||||
|
// 体检报告的组成部分在应用层组装,相关查询(getConclusionAll/getSuggestAll)为内部使用
|
||||||
|
|
||||||
|
// ================= 5. 按身份证 / 日期查询 =================
|
||||||
|
|
||||||
|
// 14(按身份证及年份查结果)/ 15.3(按审查日期查报告)/ 16(按身份证或编号+年份查报告)
|
||||||
|
// 不再对外暴露:相关查询为内部使用(如 getPeStatusByIdnoAndYear 的宁夏兜底)
|
||||||
|
// 14.1 全量版保留对外
|
||||||
|
|
||||||
|
/** 14.1 根据身份证号及年份获取体检结果(全量) */
|
||||||
|
@ApiMeta(desc = "按身份证及年份查全量结果", params = {
|
||||||
|
@ApiParam(name = "idNo", desc = "身份证号"),
|
||||||
|
@ApiParam(name = "year", desc = "年份")
|
||||||
|
})
|
||||||
|
@Mapping("/getResultByIdcardAndYearAll/{idNo}/{year}")
|
||||||
|
public Result<List<MedicalMiddleResultVo>> getResultByIdcardAndYearAll(Context ctx, @Path("idNo") String idNo, @Path("year") String year) {
|
||||||
|
checkBasic(ctx);
|
||||||
|
return Result.success(service.getResultByIdcardAndYearAll(idNo, year));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 15.1 根据体检编号获取体检结果 */
|
||||||
|
@ApiMeta(desc = "按体检编号查体检结果", params = {
|
||||||
|
@ApiParam(name = "peId", desc = "体检编号")
|
||||||
|
})
|
||||||
|
@Mapping("/getResultByPeId/{peId}")
|
||||||
|
public Result<List<MedicalMiddleResultVo>> getResultByPeId(Context ctx, @Path("peId") String peId) {
|
||||||
|
checkBasic(ctx);
|
||||||
|
return Result.success(service.getResultByPeId(peId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 15.2 根据体检编号获取体检状态(简) */
|
||||||
|
@ApiMeta(desc = "按体检编号查体检状态(简)", params = {
|
||||||
|
@ApiParam(name = "peId", desc = "体检编号")
|
||||||
|
})
|
||||||
|
@Mapping("/getResultStatus/{peId}")
|
||||||
|
public Result<List<MedicalMiddleResultVo>> getResultStatus(Context ctx, @Path("peId") String peId) {
|
||||||
|
checkBasic(ctx);
|
||||||
|
return Result.success(service.getResultStatus(peId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 17. 根据身份证号查询当前年的 peId 和预约日期 */
|
||||||
|
@ApiMeta(desc = "按身份证查当年 peId 和预约日期", params = {
|
||||||
|
@ApiParam(name = "idNo", desc = "身份证号")
|
||||||
|
})
|
||||||
|
@Mapping("/getPeIdAndDateByIdcard/{idNo}")
|
||||||
|
public Result<Map<String, Object>> getPeIdAndDateByIdcard(Context ctx, @Path("idNo") String idNo) {
|
||||||
|
checkBasic(ctx);
|
||||||
|
return Result.success(service.getPeIdAndDateByIdcard(idNo));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 17.1 同上(数组) */
|
||||||
|
@ApiMeta(desc = "按身份证查 peId 和预约日期(数组)", params = {
|
||||||
|
@ApiParam(name = "idNo", desc = "身份证号")
|
||||||
|
})
|
||||||
|
@Mapping("/getPeIdAndDateByIdcardList/{idNo}")
|
||||||
|
public Result<List<Map<String, Object>>> getPeIdAndDateByIdcardList(Context ctx, @Path("idNo") String idNo) {
|
||||||
|
checkBasic(ctx);
|
||||||
|
return Result.success(service.getPeIdAndDateByIdcardList(idNo));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 6. 单位 / 人员列表 =================
|
||||||
|
|
||||||
|
/** 18. 添加修改单位信息 */
|
||||||
|
@ApiMeta(desc = "添加修改单位信息", params = {
|
||||||
|
@ApiParam(name = "body", desc = "单位 JSON 数组")
|
||||||
|
})
|
||||||
|
@Mapping(value = "/addUnit", method = {MethodType.POST})
|
||||||
|
public Result<?> addUnit(Context ctx, @Body String body) {
|
||||||
|
checkBasic(ctx);
|
||||||
|
// Solon 对 List 泛型 body 支持不佳,用 JsonUtils 统一解析(空串返回空列表)
|
||||||
|
String errors = service.addUnits(com.hospital.front.infra.util.JsonUtils.parseList(body));
|
||||||
|
return errors != null ? Result.error(errors) : Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 19. 根据日期查询当天体检人员列表(去重),日期为空则查当天 */
|
||||||
|
@ApiMeta(desc = "按日期查当天体检人员列表(去重)", params = {
|
||||||
|
@ApiParam(name = "auditDate", desc = "审查日期(空则查当天)")
|
||||||
|
})
|
||||||
|
@Mapping("/getExamListByDate")
|
||||||
|
public Result<List<ExamListVo>> getExamListByDate(Context ctx, @Param("auditDate") String auditDate) {
|
||||||
|
checkBasic(ctx);
|
||||||
|
return Result.success(service.getExamListByDate(auditDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kotlin isNullOrBlank 的等价实现 */
|
||||||
|
private static boolean isNullOrBlank(String s) {
|
||||||
|
return s == null || s.trim().isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.hospital.front.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 医院存储过程执行结果。
|
||||||
|
*
|
||||||
|
* @author hospital-front重构
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ProcResult {
|
||||||
|
|
||||||
|
/** 医院侧返回码("1" = 成功) */
|
||||||
|
private String resultCode;
|
||||||
|
|
||||||
|
/** 错误信息(失败时) */
|
||||||
|
private String errorMsg;
|
||||||
|
|
||||||
|
/** 预约成功后医院分配的体检编号(仅 saveOrder 有值) */
|
||||||
|
private String peId;
|
||||||
|
|
||||||
|
/** 医院侧是否执行成功 */
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return "1".equals(resultCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.hospital.front.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一 API 响应包装。
|
||||||
|
*
|
||||||
|
* 结构与原 hospitalmiddle 的 Result 保持一致(对外 JSON 契约,不可修改):
|
||||||
|
* {"success": true/false, "msg": "成功", "data": ...}
|
||||||
|
*
|
||||||
|
* @param <T> 数据类型
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class Result<T> {
|
||||||
|
|
||||||
|
/** 是否成功 */
|
||||||
|
private boolean success = false;
|
||||||
|
|
||||||
|
/** 提示信息 */
|
||||||
|
private String msg;
|
||||||
|
|
||||||
|
/** 业务数据 */
|
||||||
|
private T data;
|
||||||
|
|
||||||
|
/** 成功响应(无数据),msg 固定"成功"(对齐原项目) */
|
||||||
|
public static Result success() {
|
||||||
|
Result result = new Result();
|
||||||
|
result.setSuccess(true);
|
||||||
|
result.setMsg("成功");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 成功响应(带数据) */
|
||||||
|
public static <T> Result success(T t) {
|
||||||
|
Result result = new Result();
|
||||||
|
result.setSuccess(true);
|
||||||
|
result.setMsg("成功");
|
||||||
|
result.setData(t);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 错误响应 */
|
||||||
|
public static Result error(String msg) {
|
||||||
|
Result result = new Result();
|
||||||
|
result.setSuccess(false);
|
||||||
|
result.setMsg(msg);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.hospital.front.infra;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.noear.solon.annotation.Component;
|
||||||
|
import org.noear.solon.core.handle.Context;
|
||||||
|
import org.noear.solon.core.handle.Filter;
|
||||||
|
import org.noear.solon.core.handle.FilterChain;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局接口访问日志过滤器。
|
||||||
|
*
|
||||||
|
* 统一打印每个请求的调用日志:HTTP 方法、路径、来源 IP、请求参数、耗时、响应状态码。
|
||||||
|
* 覆盖所有 Controller(/order、/api/admin),新增接口自动生效,无需逐个手写日志。
|
||||||
|
*
|
||||||
|
* 参数覆盖说明:
|
||||||
|
* - @Param(query/form)与 @Path(路径变量)统一通过 ctx.paramMap() 取;@Path 的值同时也在 ctx.path() 里可见。
|
||||||
|
* - @Body 参数(saveOrder/addUnit)不在此读取,避免消费 body 流破坏 @Body 反序列化;
|
||||||
|
* 由 service 层日志覆盖(saveOrder 打印 peInfoStr/peItemStr,addUnit 打印 units)。
|
||||||
|
*
|
||||||
|
* 顺序:index = -1 保证本过滤器在 GlobalExceptionHandler / IpWhitelistFilter 之前执行(最外层),
|
||||||
|
* 从而能记录到异常被处理后写入 ctx 的最终状态码(401/500 等)。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component(index = -1)
|
||||||
|
public class ApiAccessLogFilter implements Filter {
|
||||||
|
|
||||||
|
/** 无需打印访问日志的路径(高频健康探测等,避免刷屏) */
|
||||||
|
private static final Set<String> SKIP_LOG_PATHS = Set.of("/api/admin/health");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
try {
|
||||||
|
chain.doFilter(ctx);
|
||||||
|
} finally {
|
||||||
|
long cost = System.currentTimeMillis() - start;
|
||||||
|
if (!SKIP_LOG_PATHS.contains(ctx.path())) {
|
||||||
|
log.info("[接口调用] {} {} ip={} 参数=[{}] cost={}ms status={}",
|
||||||
|
ctx.method(), ctx.path(), ctx.remoteIp(), paramsOf(ctx), cost, ctx.status());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 汇总请求参数(query/form/path,@Body 除外),形如 key1=v1, key2=v2 */
|
||||||
|
private String paramsOf(Context ctx) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (var kv : ctx.paramMap()) {
|
||||||
|
if (sb.length() > 0) {
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
sb.append(kv.getKey()).append('=').append(kv.getFirstValue());
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package com.hospital.front.infra;
|
||||||
|
|
||||||
|
import com.hospital.front.infra.annotation.ApiMeta;
|
||||||
|
import com.hospital.front.infra.annotation.ApiParam;
|
||||||
|
import org.noear.solon.annotation.Body;
|
||||||
|
import org.noear.solon.annotation.Mapping;
|
||||||
|
import org.noear.solon.annotation.Param;
|
||||||
|
import org.noear.solon.annotation.Path;
|
||||||
|
import org.noear.solon.core.handle.Context;
|
||||||
|
import org.noear.solon.core.handle.MethodType;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Parameter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接口元数据扫描器。
|
||||||
|
*
|
||||||
|
* 反射扫描指定 Controller 类的所有 @ApiMeta 标注方法,自动提取:
|
||||||
|
* - method:从 @Mapping.method()(空数组视为 GET)
|
||||||
|
* - path:类 @Mapping 前缀 + 方法 @Mapping.value()
|
||||||
|
* - params:从方法签名 @Path/@Param/@Body 提取 name/in/required/type,@ApiMeta.params 补充 desc
|
||||||
|
*
|
||||||
|
* 替代手写接口清单,新增/调整接口时只需维护 @ApiMeta 注解即可自动同步。
|
||||||
|
*/
|
||||||
|
public final class ApiScanner {
|
||||||
|
|
||||||
|
private ApiScanner() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描指定 Controller 类,返回带 @ApiMeta 的接口元数据清单。
|
||||||
|
*
|
||||||
|
* @param controllerClass Controller 类(如 OrderApi.class)
|
||||||
|
* @return 接口元数据列表(不可变)
|
||||||
|
*/
|
||||||
|
public static List<Map<String, Object>> scan(Class<?> controllerClass) {
|
||||||
|
// 类级路径前缀(如 /order)
|
||||||
|
String prefix = "";
|
||||||
|
Mapping classMapping = controllerClass.getAnnotation(Mapping.class);
|
||||||
|
if (classMapping != null && !classMapping.value().isEmpty()) {
|
||||||
|
prefix = classMapping.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, Object>> list = new ArrayList<>();
|
||||||
|
for (Method method : controllerClass.getDeclaredMethods()) {
|
||||||
|
ApiMeta meta = method.getAnnotation(ApiMeta.class);
|
||||||
|
Mapping methodMapping = method.getAnnotation(Mapping.class);
|
||||||
|
if (meta == null || methodMapping == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拼接完整路径
|
||||||
|
String path = prefix + (methodMapping.value() == null ? "" : methodMapping.value());
|
||||||
|
|
||||||
|
// HTTP 方法:Solon @Mapping 不显式指定时 method() 返回 ALL(支持所有方法),
|
||||||
|
// 接口测试场景下默认按 GET 处理(OrderApi 的 GET 接口均未显式指定 method)
|
||||||
|
String httpMethod = "GET";
|
||||||
|
MethodType[] mtypes = methodMapping.method();
|
||||||
|
if (mtypes != null && mtypes.length > 0) {
|
||||||
|
String name = mtypes[0].name();
|
||||||
|
if (!"ALL".equals(name) && !"UNKNOWN".equals(name)) {
|
||||||
|
httpMethod = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ApiMeta.params 转 name→desc 映射,供参数补充描述
|
||||||
|
Map<String, String> descMap = new LinkedHashMap<>();
|
||||||
|
for (ApiParam ap : meta.params()) {
|
||||||
|
descMap.put(ap.name(), ap.desc());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扫描方法参数(跳过 Context)
|
||||||
|
List<Map<String, Object>> params = new ArrayList<>();
|
||||||
|
for (Parameter p : method.getParameters()) {
|
||||||
|
if (p.getType() == Context.class) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Map<String, Object> param = extractParam(p, descMap);
|
||||||
|
if (param != null) {
|
||||||
|
params.add(param);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> item = new LinkedHashMap<>();
|
||||||
|
item.put("method", httpMethod);
|
||||||
|
item.put("path", path);
|
||||||
|
item.put("desc", meta.desc());
|
||||||
|
item.put("params", params);
|
||||||
|
list.add(item);
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从单个方法参数提取元数据 */
|
||||||
|
private static Map<String, Object> extractParam(Parameter p, Map<String, String> descMap) {
|
||||||
|
Map<String, Object> param = new LinkedHashMap<>();
|
||||||
|
Path pathAnno = p.getAnnotation(Path.class);
|
||||||
|
Param paramAnno = p.getAnnotation(Param.class);
|
||||||
|
Body bodyAnno = p.getAnnotation(Body.class);
|
||||||
|
|
||||||
|
if (pathAnno != null) {
|
||||||
|
String name = pathAnno.value().isEmpty() ? p.getName() : pathAnno.value();
|
||||||
|
param.put("name", name);
|
||||||
|
param.put("in", "path");
|
||||||
|
param.put("type", "string");
|
||||||
|
param.put("required", true);
|
||||||
|
} else if (bodyAnno != null) {
|
||||||
|
param.put("name", "body");
|
||||||
|
param.put("in", "body");
|
||||||
|
param.put("type", "json");
|
||||||
|
param.put("required", true);
|
||||||
|
} else if (paramAnno != null) {
|
||||||
|
String name = paramAnno.value().isEmpty() ? p.getName() : paramAnno.value();
|
||||||
|
param.put("name", name);
|
||||||
|
param.put("in", "query");
|
||||||
|
param.put("type", "string");
|
||||||
|
param.put("required", paramAnno.required());
|
||||||
|
} else {
|
||||||
|
// 无 Solon 参数注解,跳过(如注入的 bean 等)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
param.put("desc", descMap.getOrDefault((String) param.get("name"), ""));
|
||||||
|
return param;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.hospital.front.infra;
|
||||||
|
|
||||||
|
import org.noear.solon.annotation.Component;
|
||||||
|
import org.noear.solon.annotation.Inject;
|
||||||
|
import org.noear.solon.scheduling.annotation.Scheduled;
|
||||||
|
|
||||||
|
import com.hospital.front.adapter.HospitalRouter;
|
||||||
|
import com.hospital.front.dto.Result;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库健康后台监测(缓存式)。
|
||||||
|
*
|
||||||
|
* 后台周期探测 DB(默认 30 秒),/api/admin/health 直接读缓存秒回——
|
||||||
|
* 避免 DB 建连慢(如机器休眠唤醒后网络未就绪,实测可达 4 分钟)时
|
||||||
|
* 健康接口长时间阻塞,导致 Agent 看门狗 30s 超时误判主程序宕机而反复重启。
|
||||||
|
*
|
||||||
|
* 启动初期缓存为空视为 STARTING(探测线程尚未跑完第一轮),不算故障。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class DbHealthMonitor {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private HospitalRouter router;
|
||||||
|
|
||||||
|
/** 最近一次探测结果(null = 尚未完成首轮探测) */
|
||||||
|
private final AtomicReference<Result<Boolean>> last = new AtomicReference<>();
|
||||||
|
|
||||||
|
/** 周期探测(首轮延迟由调度器决定,失败不影响后续轮次) */
|
||||||
|
@Scheduled(cron = "*/30 * * * * *")
|
||||||
|
public void probe() {
|
||||||
|
try {
|
||||||
|
last.set(router.active().healthCheck());
|
||||||
|
} catch (Exception e) {
|
||||||
|
// healthCheck 内部已捕获,此处兜底防止调度线程中断
|
||||||
|
last.set(Result.error("探测异常:" + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 最近一次探测结果;未完成首轮探测时返回 null(调用方按 STARTING 处理) */
|
||||||
|
public Result<Boolean> last() {
|
||||||
|
return last.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package com.hospital.front.infra;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.noear.solon.Solon;
|
||||||
|
import org.noear.solon.annotation.Component;
|
||||||
|
import org.noear.solon.core.handle.Context;
|
||||||
|
import org.noear.solon.core.handle.Filter;
|
||||||
|
import org.noear.solon.core.handle.FilterChain;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IP 白名单过滤器(应对前置机端口被公网扫描器探测)。
|
||||||
|
*
|
||||||
|
* 配置(app.yml,支持医院配置覆盖):
|
||||||
|
* security:
|
||||||
|
* ipWhitelistEnabled: false # 白名单开关(默认关,行为不变)
|
||||||
|
* ipWhitelist: "10.10.10.228,10.0.0.0/24" # IP 池,逗号分隔
|
||||||
|
*
|
||||||
|
* 规则:
|
||||||
|
* - 开关关闭时全放行(默认)
|
||||||
|
* - 127.0.0.1 / ::1(Agent 环回访问 /api/admin)永远放行,无需配置
|
||||||
|
* - 直连部署:取连接对端 IP(remoteIp)判定
|
||||||
|
* - nginx 反代部署(security.trustProxy=true):连接对端必须是环回或 nginx 所在 IP,
|
||||||
|
* 然后取 X-Forwarded-For 的最右侧 IP 判定(最右侧由可信 nginx 追加,无法伪造;
|
||||||
|
* 不能取最左侧——客户端可自带 XFF 冒充白名单)
|
||||||
|
* - 支持三种写法:精确 IP(10.0.0.5)、通配段(112.113.*.*,每段可单独 *)、
|
||||||
|
* CIDR 网段(10.0.0.0/24)
|
||||||
|
* - 非白名单一律 403 打回,并记 WARN 一行(含来源 IP 与路径,便于追踪扫描源)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class IpWhitelistFilter implements Filter {
|
||||||
|
|
||||||
|
/** 环回地址(永远放行) */
|
||||||
|
private static final Set<String> LOOPBACK = new HashSet<>(Arrays.asList("127.0.0.1", "0:0:0:0:0:0:0:1", "::1"));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
|
||||||
|
if (!enabled()) {
|
||||||
|
chain.doFilter(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String peer = ctx.remoteIp() != null ? ctx.remoteIp() : "";
|
||||||
|
String ip = peer;
|
||||||
|
|
||||||
|
if (trustProxy()) {
|
||||||
|
// 反代模式:对端必须是环回(同机 nginx)或白名单中的代理 IP,
|
||||||
|
// 否则视为伪造直连,直接打回
|
||||||
|
if (!LOOPBACK.contains(peer)) {
|
||||||
|
log.warn("[IP白名单] 代理模式下非可信代理直连:ip={} {} {}", peer, ctx.method(), ctx.path());
|
||||||
|
reject(ctx, peer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// X-Forwarded-For 最右侧为本机 nginx 追加的真实客户端 IP(不可伪造)
|
||||||
|
String xff = ctx.header("X-Forwarded-For");
|
||||||
|
if (xff != null && !xff.trim().isEmpty()) {
|
||||||
|
String[] hops = xff.split(",");
|
||||||
|
ip = hops[hops.length - 1].trim();
|
||||||
|
}
|
||||||
|
// 无 XFF 的环回请求 = Agent 本机访问,保持环回 IP(放行)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAllowed(ip)) {
|
||||||
|
chain.doFilter(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(ctx, ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 非白名单:403 打回(一行日志,避免扫描器刷爆日志) */
|
||||||
|
private void reject(Context ctx, String ip) {
|
||||||
|
log.warn("[IP白名单] 拒绝访问:ip={} {} {}", ip, ctx.method(), ctx.path());
|
||||||
|
ctx.status(403);
|
||||||
|
ctx.contentType("application/json;charset=UTF-8");
|
||||||
|
ctx.output("{\"success\":false,\"msg\":\"禁止访问\",\"data\":null}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 白名单开关(惰性读取,支持热更新) */
|
||||||
|
private boolean enabled() {
|
||||||
|
return "true".equals(Solon.cfg().get("security.ipWhitelistEnabled", "false"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 代理模式开关:nginx 反代部署时置 true,从 X-Forwarded-For 取真实客户端 IP */
|
||||||
|
private boolean trustProxy() {
|
||||||
|
return "true".equals(Solon.cfg().get("security.trustProxy", "false"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** IP 是否放行:环回 / 精确匹配 / 通配段匹配 / CIDR 网段匹配 */
|
||||||
|
private boolean isAllowed(String ip) {
|
||||||
|
if (LOOPBACK.contains(ip)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (String entry : pool()) {
|
||||||
|
if (entry.contains("/")) {
|
||||||
|
if (cidrMatch(ip, entry)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (entry.contains("*")) {
|
||||||
|
if (wildcardMatch(ip, entry)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (entry.equals(ip)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通配段匹配(仅 IPv4):entry 每段可为 * 或具体数字,如 112.113.*.*。
|
||||||
|
*/
|
||||||
|
private static boolean wildcardMatch(String ip, String entry) {
|
||||||
|
String[] ipParts = ip.split("\\.");
|
||||||
|
String[] entryParts = entry.split("\\.");
|
||||||
|
if (ipParts.length != 4 || entryParts.length != 4) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
if (!"*".equals(entryParts[i]) && !entryParts[i].equals(ipParts[i])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 白名单 IP 池(逗号分隔,惰性读取) */
|
||||||
|
private Set<String> pool() {
|
||||||
|
String raw = Solon.cfg().get("security.ipWhitelist", "");
|
||||||
|
Set<String> set = new HashSet<>();
|
||||||
|
if (raw != null && !raw.trim().isEmpty()) {
|
||||||
|
for (String s : raw.split(",")) {
|
||||||
|
if (!s.trim().isEmpty()) {
|
||||||
|
set.add(s.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** IPv4 CIDR 网段匹配(如 10.0.0.0/24) */
|
||||||
|
private static boolean cidrMatch(String ip, String cidr) {
|
||||||
|
try {
|
||||||
|
String[] parts = cidr.split("/");
|
||||||
|
byte[] net = java.net.InetAddress.getByName(parts[0]).getAddress();
|
||||||
|
int prefix = Integer.parseInt(parts[1]);
|
||||||
|
byte[] addr = java.net.InetAddress.getByName(ip).getAddress();
|
||||||
|
if (net.length != addr.length || net.length != 4) {
|
||||||
|
return false; // 仅支持 IPv4 网段
|
||||||
|
}
|
||||||
|
int fullBytes = prefix / 8;
|
||||||
|
int restBits = prefix % 8;
|
||||||
|
for (int i = 0; i < fullBytes; i++) {
|
||||||
|
if (net[i] != addr[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (restBits > 0 && fullBytes < 4) {
|
||||||
|
int mask = 0xFF << (8 - restBits);
|
||||||
|
if ((net[fullBytes] & mask) != (addr[fullBytes] & mask)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false; // 网段配置非法视为不匹配
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.hospital.front.infra.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接口元数据注解(标注在 Controller 方法上)。
|
||||||
|
*
|
||||||
|
* 用于 /api/admin/apis 反射扫描自动生成接口清单,供 B 端接口测试工具加载。
|
||||||
|
* 标注后无需手写清单,新增/调整接口时自动同步。
|
||||||
|
*
|
||||||
|
* 参数的 name/in/required/type 由方法签名(@Path/@Param/@Body)反射自动提取,
|
||||||
|
* 此处仅补充方法描述与参数描述(按 name 匹配)。
|
||||||
|
*/
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface ApiMeta {
|
||||||
|
/** 接口描述 */
|
||||||
|
String desc();
|
||||||
|
|
||||||
|
/** 参数描述(按 name 匹配方法参数补充 desc) */
|
||||||
|
ApiParam[] params() default {};
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.hospital.front.infra.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接口参数描述注解(仅作为 {@link ApiMeta#params()} 数组成员,不单独标注)。
|
||||||
|
*
|
||||||
|
* 扫描时按 name 匹配方法参数,补充其 desc。
|
||||||
|
*/
|
||||||
|
@Target(ElementType.ANNOTATION_TYPE)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface ApiParam {
|
||||||
|
/** 参数名(须与方法签名 @Path/@Param/@Body 的参数名一致) */
|
||||||
|
String name();
|
||||||
|
|
||||||
|
/** 参数描述 */
|
||||||
|
String desc() default "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.hospital.front.infra.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务异常。
|
||||||
|
*
|
||||||
|
* 对外契约与原 hospitalmiddle 一致:异常统一以 {"success":false,"msg":...} 返回,
|
||||||
|
* 不携带错误码(code 体系已随 Result 对齐移除,分类信息仅用于日志排查)。
|
||||||
|
*
|
||||||
|
* @author hospital-front重构
|
||||||
|
*/
|
||||||
|
public class BusinessException extends RuntimeException {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造业务异常。
|
||||||
|
*
|
||||||
|
* @param message 错误描述
|
||||||
|
*/
|
||||||
|
public BusinessException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 静态工厂方法 */
|
||||||
|
public static BusinessException of(String message) {
|
||||||
|
return new BusinessException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 参数缺失异常 */
|
||||||
|
public static BusinessException paramMissing(String field) {
|
||||||
|
return new BusinessException("参数缺失:" + field);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 业务校验失败异常 */
|
||||||
|
public static BusinessException businessFailed(String msg) {
|
||||||
|
return new BusinessException(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 存储过程调用失败异常 */
|
||||||
|
public static BusinessException procFailed(String msg) {
|
||||||
|
return new BusinessException(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.hospital.front.infra.exception;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import com.hospital.front.dto.Result;
|
||||||
|
import org.noear.solon.annotation.Component;
|
||||||
|
import org.noear.solon.core.handle.Context;
|
||||||
|
import org.noear.solon.core.handle.Filter;
|
||||||
|
import org.noear.solon.core.handle.FilterChain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局异常处理(Solon Filter)。
|
||||||
|
*
|
||||||
|
* 将未捕获的异常统一包装为 Result 格式返回。
|
||||||
|
* 业务异常请直接抛出 BusinessException。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class GlobalExceptionHandler implements Filter {
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilter(Context ctx, FilterChain chain) {
|
||||||
|
try {
|
||||||
|
chain.doFilter(ctx);
|
||||||
|
} catch (org.noear.solon.core.exception.StatusException e) {
|
||||||
|
int code = e.getCode();
|
||||||
|
if (code >= 500) {
|
||||||
|
// 5xx 服务端状态异常:真实故障,ERROR 记堆栈
|
||||||
|
log.error("服务端状态异常:{} {}(HTTP {})", ctx.method(), ctx.path(), code, e);
|
||||||
|
} else if (code == 400) {
|
||||||
|
// 400 请求解析失败(多为 multipart/Content-Type 错误):真实调用失败,WARN 并带 Content-Type 便于定位
|
||||||
|
log.warn("请求解析失败(HTTP 400):{} {},Content-Type={}", ctx.method(), ctx.path(), ctx.header("Content-Type"));
|
||||||
|
} else if (code == 404) {
|
||||||
|
// 404 路由未匹配:多为扫描器探测,DEBUG 静默(INFO 级别下不再输出)
|
||||||
|
log.debug("路由未匹配:{} {}(HTTP 404)", ctx.method(), ctx.path());
|
||||||
|
} else {
|
||||||
|
// 其它 4xx(401/403/405/415 等)
|
||||||
|
log.info("客户端状态异常:{} {}(HTTP {})", ctx.method(), ctx.path(), code);
|
||||||
|
}
|
||||||
|
renderStatus(ctx, code);
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
String msg = e.getMessage() != null ? e.getMessage() : "业务异常";
|
||||||
|
log.warn("业务异常:{}", e.getMessage());
|
||||||
|
renderResult(ctx, Result.error(msg));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
String msg = e.getMessage() != null ? e.getMessage() : "参数错误";
|
||||||
|
log.warn("参数异常:{}", e.getMessage());
|
||||||
|
renderResult(ctx, Result.error(msg));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("系统异常", e);
|
||||||
|
String msg = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
|
||||||
|
renderResult(ctx, Result.error("系统异常:" + msg));
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// 兜底:捕获 Error 及其它 Throwable(如 OOM),保证响应不悬挂
|
||||||
|
log.error("严重异常(Throwable)", t);
|
||||||
|
renderResult(ctx, Result.error("严重异常:" + t.getClass().getSimpleName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输出路由状态异常(404/405 等)响应。
|
||||||
|
*/
|
||||||
|
private void renderStatus(Context ctx, int code) {
|
||||||
|
ctx.status(code);
|
||||||
|
ctx.contentType("application/json;charset=UTF-8");
|
||||||
|
ctx.output("{\"success\":false,\"msg\":\"Not Found\",\"data\":null}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输出 Result JSON 响应(与原 hospitalmiddle 契约一致:{"success":false,"msg":...})。
|
||||||
|
*/
|
||||||
|
private void renderResult(Context ctx, Result<?> result) {
|
||||||
|
// 保留上游预设的 4xx/5xx 状态码(如 checkBasic 认证失败已设 401),否则默认 200。
|
||||||
|
// 若强行刷成 200,Basic 认证失败将不再返回 401,浏览器不会弹出账号密码框。
|
||||||
|
if (ctx.status() < 400) {
|
||||||
|
ctx.status(200);
|
||||||
|
}
|
||||||
|
ctx.contentType("application/json;charset=UTF-8");
|
||||||
|
ctx.output("{\"success\":" + result.isSuccess()
|
||||||
|
+ ",\"msg\":" + toJsonValue(result.getMsg())
|
||||||
|
+ ",\"data\":null}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 极简 JSON 值序列化(字符串加引号,其它直接 toString)。
|
||||||
|
*/
|
||||||
|
private String toJsonValue(Object v) {
|
||||||
|
if (v == null) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
if (v instanceof Number || v instanceof Boolean) {
|
||||||
|
return v.toString();
|
||||||
|
}
|
||||||
|
return "\"" + v.toString().replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n") + "\"";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.hospital.front.infra.util;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日期工具类(统一格式)。
|
||||||
|
*
|
||||||
|
* 业务系统约定:
|
||||||
|
* - 日期:yyyy-MM-dd
|
||||||
|
* - 时间:yyyy-MM-dd HH:mm:ss
|
||||||
|
* - 时区:GMT+8
|
||||||
|
*
|
||||||
|
* @author hospital-front重构
|
||||||
|
*/
|
||||||
|
public final class DateUtils {
|
||||||
|
|
||||||
|
/** 日期格式 */
|
||||||
|
public static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
|
||||||
|
/** 日期时间格式 */
|
||||||
|
public static final DateTimeFormatter DATETIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
/** 工具类禁止实例化 */
|
||||||
|
private DateUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前日期字符串(yyyy-MM-dd) */
|
||||||
|
public static String today() {
|
||||||
|
return LocalDate.now().format(DATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前时间字符串(yyyy-MM-dd HH:mm:ss) */
|
||||||
|
public static String now() {
|
||||||
|
return LocalDateTime.now().format(DATETIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析日期字符串 */
|
||||||
|
public static LocalDate parseDate(String text) {
|
||||||
|
return LocalDate.parse(text, DATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析日期时间字符串 */
|
||||||
|
public static LocalDateTime parseDateTime(String text) {
|
||||||
|
return LocalDateTime.parse(text, DATETIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 格式化为日期字符串 */
|
||||||
|
public static String format(LocalDate date) {
|
||||||
|
return date.format(DATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 格式化为日期时间字符串 */
|
||||||
|
public static String format(LocalDateTime dateTime) {
|
||||||
|
return dateTime.format(DATETIME);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.hospital.front.infra.util;
|
||||||
|
|
||||||
|
import org.noear.snack.ONode;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 解析工具(snack3 封装)。
|
||||||
|
*
|
||||||
|
* Solon 对 Map/List 泛型 body 反序列化支持不佳(接口层只能拿 String body),
|
||||||
|
* 此处集中封装手动解析,接口层一行调用;单 DTO 结构应优先用 @Body 强类型直收。
|
||||||
|
*/
|
||||||
|
public final class JsonUtils {
|
||||||
|
|
||||||
|
private JsonUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** JSON 对象字符串 → Map(解析失败抛 IllegalArgumentException) */
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static Map<String, Object> parseMap(String json) {
|
||||||
|
try {
|
||||||
|
return (Map<String, Object>) ONode.load(json).toObject(Map.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalArgumentException("JSON 对象格式错误:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** JSON 数组字符串 → List<Map>(空串返回空列表) */
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static List<Map<String, Object>> parseList(String json) {
|
||||||
|
if (json == null || json.trim().isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return (List<Map<String, Object>>) ONode.load(json).toObject(List.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalArgumentException("JSON 数组格式错误:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.hospital.front.mapper.bsm;
|
||||||
|
|
||||||
|
import com.hospital.front.vo.ConclusionVo;
|
||||||
|
import com.hospital.front.vo.ExamListVo;
|
||||||
|
import com.hospital.front.vo.MedicalMiddleResultVo;
|
||||||
|
import com.hospital.front.vo.PeStatusVo;
|
||||||
|
import com.hospital.front.vo.SuggestVo;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BSM 医院组 Mapper(Oracle 方言)。
|
||||||
|
*
|
||||||
|
* 适用医院:xinglongyuan(兴隆园)/ jinghe(泾河)/ qingyang(庆阳)
|
||||||
|
* XML 位置:classpath:mapper/bsm/OrderMapper.xml
|
||||||
|
*
|
||||||
|
* 存储过程参数与医院 HIS 系统约定,不可修改。
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface BsmOrderMapper {
|
||||||
|
|
||||||
|
/** 健康探测(SELECT 1) */
|
||||||
|
int ping();
|
||||||
|
|
||||||
|
/** 预约提交(存储过程 P_ADD_PE_REGIST,出参 resultCode/errorMsg/peId) */
|
||||||
|
void saveOrder(Map<String, Object> map);
|
||||||
|
|
||||||
|
/** 取消预约(存储过程 P_DEL_PE_REGIST) */
|
||||||
|
void cancelOrder(Map<String, Object> map);
|
||||||
|
|
||||||
|
/** 添加/修改单位 V1(存储过程 P_ADD_UNIT) */
|
||||||
|
void addUnit(Map<String, Object> map);
|
||||||
|
|
||||||
|
/** 添加/修改单位 V2(存储过程 P_ADD_UNIT,8 入参版) */
|
||||||
|
void addUnitV2(Map<String, Object> map);
|
||||||
|
|
||||||
|
/** 按体检预约 ID(逗号分隔)查询体检状态 */
|
||||||
|
List<PeStatusVo> getPhyexamStatus(@Param("peIds") String peIds);
|
||||||
|
|
||||||
|
/** 医院所有体检项目(组合项) */
|
||||||
|
List<Map<String, Object>> getItemList();
|
||||||
|
|
||||||
|
/** 医院所有体检项目及指标项 */
|
||||||
|
List<Map<String, Object>> getAllItemList();
|
||||||
|
|
||||||
|
/** 医院所有体检指标项 */
|
||||||
|
List<Map<String, Object>> getPeItemList();
|
||||||
|
|
||||||
|
/** 员工体检项目 */
|
||||||
|
List<Map<String, Object>> getEmpItem(@Param("peId") String peId);
|
||||||
|
|
||||||
|
/** 按指标项 CODE 查询体检组合 */
|
||||||
|
List<Map<String, Object>> getItemInfo(@Param("peItemCode") String peItemCode);
|
||||||
|
|
||||||
|
/** 员工体检结果 */
|
||||||
|
List<Map<String, Object>> getResult(@Param("peId") String peId);
|
||||||
|
|
||||||
|
/** 体检结论(全量) */
|
||||||
|
List<ConclusionVo> getConclusionAll(@Param("peId") String peId);
|
||||||
|
|
||||||
|
/** 体检建议(全量) */
|
||||||
|
List<SuggestVo> getSuggestAll(@Param("peId") String peId);
|
||||||
|
|
||||||
|
|
||||||
|
/** 按身份证号及年份查询体检状态 */
|
||||||
|
PeStatusVo getPeStatusByIdnoAndYear(@Param("idNo") String idNo, @Param("year") String year);
|
||||||
|
|
||||||
|
/** 按身份证号及年份查询体检结果 */
|
||||||
|
List<MedicalMiddleResultVo> getResultByIdcardAndYear(@Param("idNo") String idNo, @Param("year") String year);
|
||||||
|
|
||||||
|
/** 按身份证号及年份查询体检结果(全量) */
|
||||||
|
List<MedicalMiddleResultVo> getResultByIdcardAndYearAll(@Param("idNo") String idNo, @Param("year") String year);
|
||||||
|
|
||||||
|
/** 按体检编号查询体检结果 */
|
||||||
|
List<MedicalMiddleResultVo> getResultByPeId(@Param("peId") String peId);
|
||||||
|
|
||||||
|
|
||||||
|
/** 按体检编号查询体检项目子表 */
|
||||||
|
List<MedicalMiddleResultVo> getItemByPeId(@Param("peId") String peId);
|
||||||
|
|
||||||
|
/** 按体检编号查询体检项目子表(全量) */
|
||||||
|
List<MedicalMiddleResultVo> getItemByPeIdAll(@Param("peId") String peId);
|
||||||
|
|
||||||
|
|
||||||
|
/** 按身份证号查询当年 peId 与预约日期 */
|
||||||
|
Map<String, Object> getPeIdAndDateByIdcard(@Param("idNo") String idNo);
|
||||||
|
|
||||||
|
/** 按身份证号查询当年 peId 与预约日期(数组) */
|
||||||
|
List<Map<String, Object>> getPeIdAndDateByIdcardList(@Param("idNo") String idNo);
|
||||||
|
|
||||||
|
/** 按审核日期查询当天体检人员列表(去重) */
|
||||||
|
List<ExamListVo> getExamListByDate(@Param("auditDate") String auditDate);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.hospital.front.mapper.nx;
|
||||||
|
|
||||||
|
import com.hospital.front.vo.ConclusionVo;
|
||||||
|
import com.hospital.front.vo.ExamListVo;
|
||||||
|
import com.hospital.front.vo.MedicalMiddleResultVo;
|
||||||
|
import com.hospital.front.vo.PeStatusVo;
|
||||||
|
import com.hospital.front.vo.SuggestVo;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 宁夏医院 Mapper(SqlServer 方言)。
|
||||||
|
* getConclusion/getSuggest 与 All 版 SQL 完全重复,已合并为 All 版。
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface NxOrderMapper {
|
||||||
|
|
||||||
|
/** 健康探测 */
|
||||||
|
int ping();
|
||||||
|
|
||||||
|
/** 提交预约(存储过程) */
|
||||||
|
void saveOrder(Map<String, Object> map);
|
||||||
|
|
||||||
|
/** 取消预约(存储过程) */
|
||||||
|
void cancelOrder(Map<String, Object> map);
|
||||||
|
|
||||||
|
/** 添加/修改单位 V2(存储过程) */
|
||||||
|
void addUnitV2(Map<String, Object> map);
|
||||||
|
|
||||||
|
/** 体检状态查询(多个 peId 逗号分隔) */
|
||||||
|
List<PeStatusVo> getPhyexamStatus(@Param("peIds") String peIds);
|
||||||
|
|
||||||
|
/** 体检项目查询 */
|
||||||
|
List<Map<String, Object>> getItemList();
|
||||||
|
|
||||||
|
/** 体检指标项及体检组合数据 */
|
||||||
|
List<Map<String, Object>> getAllItemList();
|
||||||
|
|
||||||
|
/** 体检指标项数据 */
|
||||||
|
List<Map<String, Object>> getPeItemList();
|
||||||
|
|
||||||
|
/** 按体检指标项查所属组合 */
|
||||||
|
List<Map<String, Object>> getItemInfo(@Param("peItemCode") String peItemCode);
|
||||||
|
|
||||||
|
/** 员工体检项目 */
|
||||||
|
List<Map<String, Object>> getEmpItem(@Param("peId") String peId);
|
||||||
|
|
||||||
|
/** 按体检编号查体检结果 */
|
||||||
|
List<Map<String, Object>> getResult(@Param("peId") String peId);
|
||||||
|
|
||||||
|
/** 体检结论(列表) */
|
||||||
|
List<ConclusionVo> getConclusionAll(@Param("peId") String peId);
|
||||||
|
|
||||||
|
/** 体检建议(全量) */
|
||||||
|
List<SuggestVo> getSuggestAll(@Param("peId") String peId);
|
||||||
|
|
||||||
|
/** 按身份证号及年份查询体检状态 */
|
||||||
|
PeStatusVo getPeStatusByIdnoAndYear(@Param("idNo") String idNo, @Param("year") String year);
|
||||||
|
|
||||||
|
/** 按身份证号及年份查询体检结果 */
|
||||||
|
List<MedicalMiddleResultVo> getResultByIdcardAndYear(@Param("idNo") String idNo, @Param("year") String year);
|
||||||
|
|
||||||
|
/** 按身份证号及年份查询体检结果(全量) */
|
||||||
|
List<MedicalMiddleResultVo> getResultByIdcardAndYearAll(@Param("idNo") String idNo, @Param("year") String year);
|
||||||
|
|
||||||
|
/** 按体检编号查询体检结果 */
|
||||||
|
List<MedicalMiddleResultVo> getResultByPeId(@Param("peId") String peId);
|
||||||
|
|
||||||
|
|
||||||
|
/** 按体检编号查询体检项目子表 */
|
||||||
|
List<MedicalMiddleResultVo> getItemByPeId(@Param("peId") String peId);
|
||||||
|
|
||||||
|
/** 按体检编号查询体检项目子表(全量) */
|
||||||
|
List<MedicalMiddleResultVo> getItemByPeIdAll(@Param("peId") String peId);
|
||||||
|
|
||||||
|
|
||||||
|
/** 按身份证号查询当年 peId 与预约日期 */
|
||||||
|
Map<String, Object> getPeIdAndDateByIdcard(@Param("idNo") String idNo);
|
||||||
|
|
||||||
|
/** 按身份证号查询当年 peId 与预约日期(数组) */
|
||||||
|
List<Map<String, Object>> getPeIdAndDateByIdcardList(@Param("idNo") String idNo);
|
||||||
|
|
||||||
|
/** 按审核日期查询当天体检人员列表(去重) */
|
||||||
|
List<ExamListVo> getExamListByDate(@Param("auditDate") String auditDate);
|
||||||
|
}
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
package com.hospital.front.service;
|
||||||
|
|
||||||
|
import com.hospital.front.vo.MedicalMiddleResultVo;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.noear.solon.Solon;
|
||||||
|
import org.noear.solon.annotation.Component;
|
||||||
|
import org.noear.solon.annotation.Inject;
|
||||||
|
|
||||||
|
import com.hospital.front.adapter.HospitalAdapter;
|
||||||
|
import com.hospital.front.adapter.HospitalRouter;
|
||||||
|
import com.hospital.front.infra.exception.BusinessException;
|
||||||
|
import com.hospital.front.infra.util.DateUtils;
|
||||||
|
import com.hospital.front.vo.ExamListVo;
|
||||||
|
import com.hospital.front.vo.OrderItemVo;
|
||||||
|
import com.hospital.front.vo.OrderV2Vo;
|
||||||
|
import com.hospital.front.vo.PeStatusPhyVo;
|
||||||
|
import com.hospital.front.vo.UnitVo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 体检预约业务服务。
|
||||||
|
*
|
||||||
|
* 通过 HospitalRouter 委托给当前激活医院的适配器,
|
||||||
|
* 并承载跨医院共有的组装/汇总逻辑(替代原 OrderServiceImpl 的重复代码)。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class OrderService {
|
||||||
|
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private HospitalRouter router;
|
||||||
|
|
||||||
|
// ================= 配置开关(惰性读取,支持热更新) =================
|
||||||
|
|
||||||
|
/** VO 版本(V1 / V2),默认 V2 */
|
||||||
|
private String getHmVer() {
|
||||||
|
return Solon.cfg().get("hm.ver", "V2");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 宁夏:强制发送二级单位编码和名称 */
|
||||||
|
private boolean isAllsecondFlag() {
|
||||||
|
return "true".equals(Solon.cfg().get("hm.allsecondFlag", "false"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 健康体检不发送职业相关信息 */
|
||||||
|
private boolean isOccFlag() {
|
||||||
|
return "true".equals(Solon.cfg().get("hm.occFlag", "false"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前激活医院的适配器 */
|
||||||
|
private HospitalAdapter adapter() {
|
||||||
|
return router.active();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 体检状态字典 =================
|
||||||
|
|
||||||
|
/** 体检状态名 → 字典数字 */
|
||||||
|
private static final Map<String, String> PE_STATUS_DICT;
|
||||||
|
|
||||||
|
static {
|
||||||
|
Map<String, String> m = new HashMap<>();
|
||||||
|
m.put("预约", "1");
|
||||||
|
m.put("报到", "2");
|
||||||
|
m.put("检中", "3");
|
||||||
|
m.put("初审", "4");
|
||||||
|
m.put("终审", "5");
|
||||||
|
PE_STATUS_DICT = m;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 写操作 =================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交预约:入参 Map(V1/V2 VO 结构)→ 拼接管道符字符串 → 调医院存储过程。
|
||||||
|
*
|
||||||
|
* @return 成功时返回 peId
|
||||||
|
*/
|
||||||
|
public String saveOrder(com.hospital.front.vo.OrderV2Vo vo) {
|
||||||
|
if (isAllsecondFlag()) {
|
||||||
|
vo = vo.applyAllsecondFlag();
|
||||||
|
}
|
||||||
|
if (isOccFlag()) {
|
||||||
|
vo = vo.applyOccFlag();
|
||||||
|
}
|
||||||
|
|
||||||
|
String peInfoStr = vo.toPeInfoStr();
|
||||||
|
String peItemStr = OrderItemVo.toPeItemStr(vo.getItemList()); // companion 无 @JvmStatic,需经 Companion 调用
|
||||||
|
log.info("提交预约:peInfoStr={}, peItemStr={}", peInfoStr, peItemStr);
|
||||||
|
|
||||||
|
com.hospital.front.dto.ProcResult result = adapter().saveOrder(peInfoStr, peItemStr);
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
if (result.getPeId() == null) {
|
||||||
|
throw BusinessException.procFailed("医院未返回体检编号");
|
||||||
|
}
|
||||||
|
log.info("提交预约成功:peId={}", result.getPeId());
|
||||||
|
return result.getPeId();
|
||||||
|
} else {
|
||||||
|
log.warn("提交预约失败:errorMsg={}", result.getErrorMsg());
|
||||||
|
throwHospitalFailed(result.getErrorMsg());
|
||||||
|
// 不可达,仅为满足 Java 返回类型要求
|
||||||
|
throw new IllegalStateException("unreachable");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消预约 */
|
||||||
|
public void cancelOrder(String peId, String name, String idcard) {
|
||||||
|
log.info("取消预约:peId={}, name={}", peId, name);
|
||||||
|
com.hospital.front.dto.ProcResult result = adapter().cancelOrder(peId, name, idcard);
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
throwHospitalFailed(result.getErrorMsg());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量添加/修改单位;返回失败信息(全部成功返回 null) */
|
||||||
|
public String addUnits(List<Map<String, Object>> units) {
|
||||||
|
log.info("添加单位:units={}", units);
|
||||||
|
StringBuilder errors = new StringBuilder();
|
||||||
|
boolean useV2 = "V2".equals(getHmVer());
|
||||||
|
for (int index = 0; index < units.size(); index++) {
|
||||||
|
Map<String, Object> raw = units.get(index);
|
||||||
|
try {
|
||||||
|
UnitVo unit = convertUnit(raw);
|
||||||
|
com.hospital.front.dto.ProcResult result = adapter().addUnit(unit, useV2);
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
errors.append(result.getErrorMsg() != null ? result.getErrorMsg() : "第" + (index + 1) + "条单位同步失败;");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("addUnit 第{}条处理异常:{}", index + 1, e.getMessage());
|
||||||
|
errors.append("第").append(index + 1).append("条异常:").append(e.getMessage()).append(";");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.length() == 0 ? null : errors.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 查询操作(直通适配器) =================
|
||||||
|
|
||||||
|
public List<Map<String, Object>> getEmpItem(String peId) {
|
||||||
|
return adapter().getEmpItem(peId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> getItemList() {
|
||||||
|
return adapter().getItemList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> getAllItemList() {
|
||||||
|
return adapter().getAllItemList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> getPeItemList() {
|
||||||
|
return adapter().getPeItemList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> getItemInfo(String peItemCode) {
|
||||||
|
return adapter().getItemInfo(peItemCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> getResult(String peId) {
|
||||||
|
return adapter().getResult(peId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按身份证及年份查体检结果(内部使用:宁夏 getPeStatusByIdnoAndYear 兜底) */
|
||||||
|
public List<MedicalMiddleResultVo> getResultByIdcardAndYear(String idNo, String year) {
|
||||||
|
return adapter().getResultByIdcardAndYear(idNo, year);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MedicalMiddleResultVo> getResultByIdcardAndYearAll(String idNo, String year) {
|
||||||
|
return enrichResultAll(adapter().getResultByIdcardAndYearAll(idNo, year));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MedicalMiddleResultVo> getResultByPeId(String peId) {
|
||||||
|
return enrichResultAll(adapter().getResultByPeId(peId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 15.2 体检状态(简):返回原始基础字段,不做报告组装(对齐原 hospitalmiddle 的 getResultStatus)。
|
||||||
|
* 与 getResultByPeId 的区别在于不调用 enrichResultAll,故不含 conclusion/suggest/hospitalId/resultVoList。
|
||||||
|
*/
|
||||||
|
public List<MedicalMiddleResultVo> getResultStatus(String peId) {
|
||||||
|
return adapter().getResultByPeId(peId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getPeIdAndDateByIdcard(String idNo) {
|
||||||
|
return adapter().getPeIdAndDateByIdcard(idNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> getPeIdAndDateByIdcardList(String idNo) {
|
||||||
|
return adapter().getPeIdAndDateByIdcardList(idNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 体检状态查询:按医院返回的 PE_STATUS 名转字典数字。
|
||||||
|
*/
|
||||||
|
public List<PeStatusPhyVo> getPhyexamStatus(String peIds) {
|
||||||
|
List<PeStatusPhyVo> out = new ArrayList<>();
|
||||||
|
for (com.hospital.front.vo.PeStatusVo row : adapter().getPhyexamStatus(peIds)) {
|
||||||
|
out.add(new PeStatusPhyVo(
|
||||||
|
row.getPeId() != null ? row.getPeId() : "",
|
||||||
|
PE_STATUS_DICT.getOrDefault(
|
||||||
|
row.getPeStatus() != null ? row.getPeStatus() : "", "")));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按日期查询当天体检人员列表(日期空则当天)。
|
||||||
|
*/
|
||||||
|
public List<ExamListVo> getExamListByDate(String auditDate) {
|
||||||
|
return adapter().getExamListByDate(auditDate != null ? auditDate : DateUtils.today());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按身份证号 + 年份查询体检状态及项目(含宁夏体检报告兜底)。
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getPeStatusByIdnoAndYear(String idNo, String year) {
|
||||||
|
com.hospital.front.vo.PeStatusVo vo = adapter().getPeStatusByIdnoAndYear(idNo, year);
|
||||||
|
if (vo != null && vo.getPeId() != null) {
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("PE_ID", vo.getPeId());
|
||||||
|
result.put("NAME", vo.getName());
|
||||||
|
result.put("ID_NO", vo.getIdNo());
|
||||||
|
result.put("UNIT_NAME", vo.getUnitName());
|
||||||
|
result.put("DEPARTMENT", vo.getDepartment());
|
||||||
|
result.put("PE_PRE_DATE", vo.getPePreDate());
|
||||||
|
result.put("PE_QUEUE_DATE", vo.getPeQueueDate());
|
||||||
|
result.put("PE_STATUS", vo.getPeStatus());
|
||||||
|
result.put("peItemList", adapter().getEmpItem(vo.getPeId()));
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
// 宁夏:从体检报告继续查
|
||||||
|
if ("nx".equals(adapter().hmType())) {
|
||||||
|
List<MedicalMiddleResultVo> rows = adapter().getResultByIdcardAndYear(idNo, year);
|
||||||
|
if (!rows.isEmpty()) {
|
||||||
|
MedicalMiddleResultVo row = rows.get(0);
|
||||||
|
String auditDate = row.getAuditDate();
|
||||||
|
Map<String, Object> out = new LinkedHashMap<>();
|
||||||
|
out.put("PE_ID", row.getPeId());
|
||||||
|
out.put("PE_PRE_DATE",
|
||||||
|
auditDate != null && auditDate.length() >= 10 ? auditDate.substring(0, 10) : auditDate);
|
||||||
|
out.put("peItemList", adapter().getEmpItem(row.getPeId()));
|
||||||
|
return out;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 私有工具 =================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 报告组装(对齐原 hospitalmiddle 的 dealMiddleChildDataAll):
|
||||||
|
* 为每条结果补 hospitalId、item 子表(全量)、体检结论、体检建议。
|
||||||
|
*/
|
||||||
|
private List<MedicalMiddleResultVo> enrichResultAll(List<MedicalMiddleResultVo> rows) {
|
||||||
|
String hospitalId = Solon.cfg().get("hospital.id", "");
|
||||||
|
for (MedicalMiddleResultVo row : rows) {
|
||||||
|
row.setHospitalId(hospitalId);
|
||||||
|
// item 子表(全量版)
|
||||||
|
row.setResultVoList(adapter().getItemByPeIdAll(row.getPeId()));
|
||||||
|
// 体检结论:多条按 "序号.科室:结论文本;<br/>" 拼接
|
||||||
|
StringBuilder conclusion = new StringBuilder();
|
||||||
|
int cIndex = 1;
|
||||||
|
for (com.hospital.front.vo.ConclusionVo c : adapter().getConclusionAll(row.getPeId())) {
|
||||||
|
String deptName = c.getPeDeptName() != null ? c.getPeDeptName() : "";
|
||||||
|
String text = c.getConclusionText() != null ? c.getConclusionText() : "";
|
||||||
|
conclusion.append(cIndex).append(".").append(deptName).append(":").append(text).append(";<br/>");
|
||||||
|
cIndex++;
|
||||||
|
}
|
||||||
|
row.setConclusion(conclusion.toString());
|
||||||
|
// 体检建议:宁夏取首条 guideContent;其余按 "序号.建议项:建议内容;<br/>" 拼接(跳过建议内容为空的行)
|
||||||
|
StringBuilder suggest = new StringBuilder();
|
||||||
|
List<com.hospital.front.vo.SuggestVo> suggestRows = adapter().getSuggestAll(row.getPeId());
|
||||||
|
if ("nx".equals(adapter().hmType())) {
|
||||||
|
if (!suggestRows.isEmpty() && suggestRows.get(0).getGuideContent() != null) {
|
||||||
|
suggest.append(suggestRows.get(0).getGuideContent());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int sIndex = 1;
|
||||||
|
for (com.hospital.front.vo.SuggestVo s : suggestRows) {
|
||||||
|
if (s.getGuideContent() != null && !s.getGuideContent().isEmpty()) {
|
||||||
|
String rec = s.getRecContent() != null ? s.getRecContent() : "";
|
||||||
|
suggest.append(sIndex).append(".").append(rec).append(":")
|
||||||
|
.append(s.getGuideContent()).append(";<br/>");
|
||||||
|
sIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row.setSuggest(suggest.toString());
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 医院侧业务失败:抛出业务异常 */
|
||||||
|
private void throwHospitalFailed(String errorMsg) {
|
||||||
|
if (errorMsg != null && !errorMsg.isEmpty()) {
|
||||||
|
throw BusinessException.of(errorMsg);
|
||||||
|
} else {
|
||||||
|
throw BusinessException.procFailed("医院系统返回业务失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取字符串值(null 安全) */
|
||||||
|
private static String str(Map<String, Object> map, String key) {
|
||||||
|
Object v = map.get(key);
|
||||||
|
return v != null ? String.valueOf(v) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Map → UnitVo */
|
||||||
|
private UnitVo convertUnit(Map<String, Object> raw) {
|
||||||
|
return new UnitVo(
|
||||||
|
str(raw, "unitCode"),
|
||||||
|
str(raw, "unitName"),
|
||||||
|
str(raw, "parentUnitCode"),
|
||||||
|
str(raw, "address"),
|
||||||
|
str(raw, "connecter1"),
|
||||||
|
str(raw, "phone1"),
|
||||||
|
str(raw, "connecter2"),
|
||||||
|
str(raw, "phone2"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-11
@@ -1,10 +1,12 @@
|
|||||||
package com.hospital.front.task
|
package com.hospital.front.task;
|
||||||
|
|
||||||
import com.hospital.front.adapter.HospitalRouter
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.noear.solon.annotation.Component
|
|
||||||
import org.noear.solon.annotation.Inject
|
import org.noear.solon.annotation.Component;
|
||||||
import org.noear.solon.scheduling.annotation.Scheduled
|
import org.noear.solon.annotation.Inject;
|
||||||
import org.slf4j.LoggerFactory
|
import org.noear.solon.scheduling.annotation.Scheduled;
|
||||||
|
|
||||||
|
import com.hospital.front.adapter.HospitalRouter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 心跳任务(占位)。
|
* 心跳任务(占位)。
|
||||||
@@ -14,20 +16,20 @@ import org.slf4j.LoggerFactory
|
|||||||
*
|
*
|
||||||
* @author hospital-front重构
|
* @author hospital-front重构
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
class HeartbeatTask {
|
public class HeartbeatTask {
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(HeartbeatTask::class.java)
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private lateinit var router: HospitalRouter
|
private HospitalRouter router;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 每 60 秒一次心跳(默认 cron:0/60)。
|
* 每 60 秒一次心跳(默认 cron:0/60)。
|
||||||
* Step 6 将改为上报 B 端的 WS 消息。
|
* Step 6 将改为上报 B 端的 WS 消息。
|
||||||
*/
|
*/
|
||||||
@Scheduled(cron = "0 * * * * *")
|
@Scheduled(cron = "0 * * * * *")
|
||||||
fun heartbeat() {
|
public void heartbeat() {
|
||||||
log.debug("[Heartbeat] hospital={}, uptime ok", router.activeCode())
|
log.debug("[Heartbeat] hospital={}, uptime ok", router.activeCode());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.hospital.front.task;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
import org.noear.solon.Solon;
|
||||||
|
import org.noear.solon.annotation.Component;
|
||||||
|
import org.noear.solon.annotation.Inject;
|
||||||
|
import org.noear.solon.scheduling.annotation.Scheduled;
|
||||||
|
|
||||||
|
import com.hospital.front.adapter.HospitalRouter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uptime-Kuma 监控心跳推送。
|
||||||
|
*
|
||||||
|
* 定时检查数据库连接是否正常,正常则向 Kuma 服务器 push 心跳。
|
||||||
|
* 配置(app.yml 或医院配置中):
|
||||||
|
* kuma.enable=true
|
||||||
|
* kuma.cron=0 *\/3 * * * *
|
||||||
|
* kuma.url=https://monitor.rk-health.com:9200/api/push/uzBrX74NTC?status=up&msg=OK
|
||||||
|
*
|
||||||
|
* 沿用原 hospitalmiddle 的 UptimeKumaTask 逻辑,适配 Solon。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class UptimeKumaTask {
|
||||||
|
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private HospitalRouter router;
|
||||||
|
|
||||||
|
/** JDK 内置 HttpClient(替代 OkHttp,免额外依赖):连接 5s、读取 10s */
|
||||||
|
private final HttpClient http = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(5))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时 push 心跳(cron 由配置 kuma.cron 控制,默认每 3 分钟)。
|
||||||
|
* 仅当 kuma.enable=true 且 kuma.url 非空时执行。
|
||||||
|
*/
|
||||||
|
@Scheduled(cron = "${kuma.cron:0 */3 * * * *}")
|
||||||
|
public void kumaPush() {
|
||||||
|
boolean enable = "true".equals(Solon.cfg().get("kuma.enable", "false"));
|
||||||
|
String kumaUrl = Solon.cfg().get("kuma.url", "");
|
||||||
|
if (!enable || kumaUrl == null || kumaUrl.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 检查数据库连接
|
||||||
|
com.hospital.front.dto.Result<Boolean> health = router.active().healthCheck();
|
||||||
|
if (health.isSuccess()) {
|
||||||
|
try {
|
||||||
|
HttpResponse<String> resp = http.send(
|
||||||
|
HttpRequest.newBuilder(URI.create(kumaUrl))
|
||||||
|
.GET()
|
||||||
|
.timeout(Duration.ofSeconds(10))
|
||||||
|
.build(),
|
||||||
|
HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (resp.statusCode() >= 200 && resp.statusCode() < 300) {
|
||||||
|
log.info("[Kuma] 心跳推送成功:{}", kumaUrl);
|
||||||
|
} else {
|
||||||
|
log.warn("[Kuma] 心跳推送失败:HTTP {}", resp.statusCode());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[Kuma] 心跳推送异常:{}", e.getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("[Kuma] 数据库异常,跳过心跳:{}", health.getMsg());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.hospital.front.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 体检结论明细 VO(getConclusionAll 强类型映射,内部使用)。
|
||||||
|
*
|
||||||
|
* 一个 peId 按科室返回多条结论(汇总拼接用),故为列表元素。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ConclusionVo {
|
||||||
|
/** 体检编号 */
|
||||||
|
private String peId;
|
||||||
|
/** 就诊编号 */
|
||||||
|
private String peVisitId;
|
||||||
|
/** 科室编码 */
|
||||||
|
private String peDeptCode;
|
||||||
|
/** 科室名称 */
|
||||||
|
private String peDeptName;
|
||||||
|
/** 结论文本(已去换行) */
|
||||||
|
private String conclusionText;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.hospital.front.vo;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当天体检人员列表 VO(getExamListByDate 对外强类型出参)。
|
||||||
|
*
|
||||||
|
* 三院 SQL 出参统一(列别名即驼峰字段),日期已在 SQL 层转为字符串。
|
||||||
|
* 字段名与 snack3 默认序列化输出 key 一致,可直接返回 HTTP 调用方。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ExamListVo {
|
||||||
|
/** 体检编号 */
|
||||||
|
private String peId;
|
||||||
|
/** 身份证号 */
|
||||||
|
private String idNo;
|
||||||
|
/** 姓名 */
|
||||||
|
private String name;
|
||||||
|
/** 审查日期 yyyy-MM-dd */
|
||||||
|
private String auditDate;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.hospital.front.vo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 体检结果传输 VO(Mapper XML resultType)。
|
||||||
|
*
|
||||||
|
* 字段名与原 hospitalmiddle 的 MedicalMiddleResultVo 保持一致(XML 映射契约)。
|
||||||
|
* 数据库日期列已由 SQL 转为字符串(to_char / CONVERT)。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class MedicalMiddleResultVo {
|
||||||
|
private String idNo;
|
||||||
|
private String peResult;
|
||||||
|
private String hospitalId;
|
||||||
|
private String unitCode;
|
||||||
|
private String peQueueDate;
|
||||||
|
private String printContext;
|
||||||
|
private String peItemName;
|
||||||
|
private String peId;
|
||||||
|
private String name;
|
||||||
|
private String itemAssemName;
|
||||||
|
private String peItemCode;
|
||||||
|
private String auditDate;
|
||||||
|
private String itemAssemCode;
|
||||||
|
private String unitName;
|
||||||
|
private String peVisitId;
|
||||||
|
private String unit;
|
||||||
|
private String conclusion;
|
||||||
|
private String suggest;
|
||||||
|
/** 子表:体检项目结果列表(汇总接口组装用) */
|
||||||
|
private List<MedicalMiddleResultVo> resultVoList;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.hospital.front.vo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预约订单体检项目条目。
|
||||||
|
*
|
||||||
|
* JSON 字段名与原 hospitalmiddle 的 OrderItemVo 保持一致(调用方契约,不可修改)。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class OrderItemVo {
|
||||||
|
private String itemNo;
|
||||||
|
private String itemName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拼接子表字符串:多项之间用 & 分隔,项内 编号|名称 分隔。
|
||||||
|
*/
|
||||||
|
public static String toPeItemStr(List<OrderItemVo> items) {
|
||||||
|
if (items == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < items.size(); i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
sb.append('&');
|
||||||
|
}
|
||||||
|
OrderItemVo it = items.get(i);
|
||||||
|
sb.append(it.itemNo == null ? "" : it.itemNo)
|
||||||
|
.append('|')
|
||||||
|
.append(it.itemName == null ? "" : it.itemName);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package com.hospital.front.vo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 版预约订单 VO(hm.ver=V2 时使用)。
|
||||||
|
*
|
||||||
|
* 与 V1 的差异:工种拆为 编码+其它;危害因素拆为 编码+其它+接害时间。
|
||||||
|
* 字段顺序即存储过程 P_ADD_PE_REGIST 的 peInfoStr 竖线分隔顺序,不可调整。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class OrderV2Vo {
|
||||||
|
private String orderDate; // 1 预约日期 yyyy-MM-dd
|
||||||
|
private String secondDepartId; // 2 所属单位编码
|
||||||
|
private String secondDepartName; // 3 所属单位名称
|
||||||
|
private String thirdDepartName; // 4 所属部门
|
||||||
|
private String name; // 5 姓名
|
||||||
|
private String sexName; // 6 性别(男/女)
|
||||||
|
private String birthDay; // 7 出生日期 yyyy-MM-dd
|
||||||
|
private Integer age; // 8 年龄
|
||||||
|
private String idCard; // 9 身份证号
|
||||||
|
private String marrigeStatusName; // 10 婚姻
|
||||||
|
private String country; // 11 国家
|
||||||
|
private String nationName; // 12 民族
|
||||||
|
private String birthplace; // 13 出生地
|
||||||
|
private String position; // 14 身份
|
||||||
|
private String profession; // 15 职业
|
||||||
|
private String feeType; // 16 费别
|
||||||
|
private String liveSpace; // 17 家庭住址
|
||||||
|
private String zipCode; // 18 邮编
|
||||||
|
private String mobile; // 19 移动电话
|
||||||
|
private String email; // 20 邮箱
|
||||||
|
private String empSysno; // 21 员工编号
|
||||||
|
private String medicalType; // 22 体检类型(职业/健康)
|
||||||
|
private String medicalClass; // 23 体检类别(岗前/在岗期间)
|
||||||
|
private String workShape; // 24 用工形式(合同/劳务)
|
||||||
|
private String workDate; // 25 入职时间 yyyy-MM-dd
|
||||||
|
private String jobLevel; // 26 职务级别
|
||||||
|
private String workPlace; // 27 工作地
|
||||||
|
private String baseSite; // 28 基层站
|
||||||
|
private String workTypeCode; // 29 职业工种编码(V2)
|
||||||
|
private String workTypeOther; // 29b 其它工种(V2)
|
||||||
|
private String education; // 30 学历
|
||||||
|
private String title; // 31 职称
|
||||||
|
private String oldDepartName; // 32 原单位
|
||||||
|
private String harmReasonCode; // 33 危害因素编码(V2)
|
||||||
|
private String harmReasonOther; // 33b 其它危害因素(V2)
|
||||||
|
private String harmDate; // 33c 接害时间 yyyy-MM-dd(V2)
|
||||||
|
private String defendCancer; // 34 防癌项
|
||||||
|
private List<OrderItemVo> itemList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按存储过程约定顺序拼接主表字符串(竖线分隔,空值为空串)。
|
||||||
|
*/
|
||||||
|
public String toPeInfoStr() {
|
||||||
|
Object[] fields = {
|
||||||
|
orderDate, secondDepartId, secondDepartName, thirdDepartName, name,
|
||||||
|
sexName, birthDay, age, idCard, marrigeStatusName,
|
||||||
|
country, nationName, birthplace, position, profession,
|
||||||
|
feeType, liveSpace, zipCode, mobile, email,
|
||||||
|
empSysno, medicalType, medicalClass, workShape, workDate,
|
||||||
|
jobLevel, workPlace, baseSite, workTypeCode, workTypeOther,
|
||||||
|
education, title, oldDepartName, harmReasonCode, harmReasonOther,
|
||||||
|
harmDate, defendCancer,
|
||||||
|
};
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < fields.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
sb.append('|');
|
||||||
|
}
|
||||||
|
sb.append(fields[i] == null ? "" : String.valueOf(fields[i]));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* allsecondFlag 特殊处理(宁夏):二级单位编码 9 位截前 6 位,名称截"-"之前。
|
||||||
|
*/
|
||||||
|
public OrderV2Vo applyAllsecondFlag() {
|
||||||
|
if (secondDepartId != null && secondDepartId.length() == 9) {
|
||||||
|
secondDepartId = secondDepartId.substring(0, 6);
|
||||||
|
}
|
||||||
|
if (secondDepartName != null && secondDepartName.contains("-")) {
|
||||||
|
secondDepartName = secondDepartName.substring(0, secondDepartName.indexOf('-'));
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* occFlag 特殊处理:健康体检不发送职业相关信息(危害因素、工种、接害时间)。
|
||||||
|
*/
|
||||||
|
public OrderV2Vo applyOccFlag() {
|
||||||
|
if (!"职业".equals(medicalType)) {
|
||||||
|
harmDate = null;
|
||||||
|
harmReasonCode = null;
|
||||||
|
harmReasonOther = null;
|
||||||
|
workTypeCode = null;
|
||||||
|
workTypeOther = null;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.hospital.front.vo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V1 版预约订单 VO(hm.ver=V1 时使用)。
|
||||||
|
*
|
||||||
|
* 字段名与字段顺序均与原 hospitalmiddle 的 OrderVo(Java)保持一致,
|
||||||
|
* 字段顺序即存储过程 P_ADD_PE_REGIST 的 peInfoStr 竖线分隔顺序,不可调整。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class OrderVo {
|
||||||
|
private String orderDate; // 1 预约日期 yyyy-MM-dd
|
||||||
|
private String secondDepartId; // 2 所属单位编码
|
||||||
|
private String secondDepartName; // 3 所属单位名称
|
||||||
|
private String thirdDepartName; // 4 所属部门
|
||||||
|
private String name; // 5 姓名
|
||||||
|
private String sexName; // 6 性别(男/女)
|
||||||
|
private String birthDay; // 7 出生日期 yyyy-MM-dd
|
||||||
|
private Integer age; // 8 年龄
|
||||||
|
private String idCard; // 9 身份证号
|
||||||
|
private String marrigeStatusName; // 10 婚姻
|
||||||
|
private String country; // 11 国家
|
||||||
|
private String nationName; // 12 民族
|
||||||
|
private String birthplace; // 13 出生地
|
||||||
|
private String position; // 14 身份
|
||||||
|
private String profession; // 15 职业
|
||||||
|
private String feeType; // 16 费别
|
||||||
|
private String liveSpace; // 17 家庭住址
|
||||||
|
private String zipCode; // 18 邮编
|
||||||
|
private String mobile; // 19 移动电话
|
||||||
|
private String email; // 20 邮箱
|
||||||
|
private String empSysno; // 21 员工编号
|
||||||
|
private String medicalType; // 22 体检类型(职业/健康)
|
||||||
|
private String medicalClass; // 23 体检类别(岗前/在岗期间)
|
||||||
|
private String workShape; // 24 用工形式(合同/劳务)
|
||||||
|
private String workDate; // 25 入职时间 yyyy-MM-dd
|
||||||
|
private String jobLevel; // 26 职务级别
|
||||||
|
private String workPlace; // 27 工作地
|
||||||
|
private String baseSite; // 28 基层站
|
||||||
|
private String workType; // 29 职业工种(V1 单字段)
|
||||||
|
private String education; // 30 学历
|
||||||
|
private String title; // 31 职称
|
||||||
|
private String oldDepartName; // 32 原单位
|
||||||
|
private String harmReason; // 33 危害因素(V1 单字段)
|
||||||
|
private String defendCancer; // 34 防癌项
|
||||||
|
private List<OrderItemVo> itemList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按存储过程约定顺序拼接主表字符串(竖线分隔,空值为空串)。
|
||||||
|
*/
|
||||||
|
public String toPeInfoStr() {
|
||||||
|
Object[] fields = {
|
||||||
|
orderDate, secondDepartId, secondDepartName, thirdDepartName, name,
|
||||||
|
sexName, birthDay, age, idCard, marrigeStatusName,
|
||||||
|
country, nationName, birthplace, position, profession,
|
||||||
|
feeType, liveSpace, zipCode, mobile, email,
|
||||||
|
empSysno, medicalType, medicalClass, workShape, workDate,
|
||||||
|
jobLevel, workPlace, baseSite, workType, education,
|
||||||
|
title, oldDepartName, harmReason, defendCancer,
|
||||||
|
};
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < fields.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
sb.append('|');
|
||||||
|
}
|
||||||
|
sb.append(fields[i] == null ? "" : String.valueOf(fields[i]));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.hospital.front.vo;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 体检状态简版 VO(getPhyexamStatus 对外强类型出参)。
|
||||||
|
*
|
||||||
|
* 字段名与 snack3 默认序列化输出 key 一致(peId / peStatus),
|
||||||
|
* 可直接返回给 HTTP 调用方,无需手工 Map 包装。
|
||||||
|
* peStatus 已由 OrderService 加工为字典数字(与原 Map 契约一致)。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PeStatusPhyVo {
|
||||||
|
/** 体检编号(null 时输出空字符串,保持与原 Map 契约一致) */
|
||||||
|
private String peId;
|
||||||
|
/** 体检状态字典数字(原始状态名查不到时输出空字符串) */
|
||||||
|
private String peStatus;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.hospital.front.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 体检状态 VO(getPhyexamStatus / getPeStatusByIdnoAndYear 强类型映射,内部中间态)。
|
||||||
|
*
|
||||||
|
* 对外出参由 OrderService 加工(key 小写 peId/peStatus + 状态字典转换),
|
||||||
|
* 本 VO 仅作 adapter → service 的内部传输,不影响 HTTP 契约。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class PeStatusVo {
|
||||||
|
/** 体检编号 */
|
||||||
|
private String peId;
|
||||||
|
/** 姓名 */
|
||||||
|
private String name;
|
||||||
|
/** 身份证号 */
|
||||||
|
private String idNo;
|
||||||
|
/** 单位名称 */
|
||||||
|
private String unitName;
|
||||||
|
/** 部门 */
|
||||||
|
private String department;
|
||||||
|
/** 预约日期 */
|
||||||
|
private String pePreDate;
|
||||||
|
/** 排队日期 */
|
||||||
|
private String peQueueDate;
|
||||||
|
/** 状态名(报到/检中/初审/终审/预约/转向) */
|
||||||
|
private String peStatus;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.hospital.front.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 体检建议 VO(getSuggestAll 强类型映射,内部使用)。
|
||||||
|
*
|
||||||
|
* 按 peId 查询仅一条建议记录,adapter 层取首条返回单对象。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class SuggestVo {
|
||||||
|
/** 体检编号 */
|
||||||
|
private String peId;
|
||||||
|
/** 建议项(科室/类别描述) */
|
||||||
|
private String recContent;
|
||||||
|
/** 建议内容(已去换行) */
|
||||||
|
private String guideContent;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.hospital.front.vo;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单位信息 VO(addUnit 接口入参,字段名与原 UnitVo 一致)。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class UnitVo {
|
||||||
|
private String unitCode; // 单位编码
|
||||||
|
private String unitName; // 单位名称
|
||||||
|
private String parentUnitCode; // 上级体检单位编号(V2)
|
||||||
|
private String address; // 单位地址(V2)
|
||||||
|
private String connecter1; // 联系人1(V2)
|
||||||
|
private String phone1; // 电话1(V2)
|
||||||
|
private String connecter2; // 联系人2(V2)
|
||||||
|
private String phone2; // 电话2(V2)
|
||||||
|
}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package com.hospital.agent
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
import java.util.Properties
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Agent 配置。
|
|
||||||
*
|
|
||||||
* 从 conf/agent.properties 读取(key=value),支持环境变量覆盖(AGENT_ 前缀大写)。
|
|
||||||
* 不依赖 Solon 容器,Agent 以纯 main 方式运行。
|
|
||||||
*
|
|
||||||
* 配置项:
|
|
||||||
* - server.url B 端 WebSocket 地址(ws://host:9091/ws)
|
|
||||||
* - device.id 设备编号(医院唯一,如 jinghe-01)
|
|
||||||
* - device.token 设备鉴权 Token
|
|
||||||
* - main.health.url 主程序健康检查地址(http://127.0.0.1:8082/api/admin/health)
|
|
||||||
* - main.admin.token 主程序管理接口 Token(与主程序 admin.token 一致)
|
|
||||||
* - main.start.cmd 主程序启动命令(Windows: bin\start.bat;Linux: bin/start.sh)
|
|
||||||
* - heartbeat.sec 心跳间隔(默认 30 秒)
|
|
||||||
* - guard.enable 是否守护主程序(默认 true)
|
|
||||||
*/
|
|
||||||
class AgentConfig private constructor(
|
|
||||||
val serverUrl: String,
|
|
||||||
val deviceId: String,
|
|
||||||
val deviceToken: String,
|
|
||||||
val mainHealthUrl: String,
|
|
||||||
val mainAdminToken: String,
|
|
||||||
val mainStartCmd: String,
|
|
||||||
val heartbeatSec: Long,
|
|
||||||
val guardEnabled: Boolean,
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
/** 从 conf/agent.properties 加载,环境变量优先 */
|
|
||||||
fun load(confPath: String = "conf/agent.properties"): AgentConfig {
|
|
||||||
val props = Properties()
|
|
||||||
val file = File(confPath)
|
|
||||||
if (file.exists()) file.inputStream().use { props.load(it) }
|
|
||||||
|
|
||||||
fun get(key: String, def: String): String {
|
|
||||||
val envKey = "AGENT_" + key.replace(".", "_").uppercase()
|
|
||||||
return System.getenv(envKey) ?: props.getProperty(key) ?: def
|
|
||||||
}
|
|
||||||
|
|
||||||
return AgentConfig(
|
|
||||||
serverUrl = get("server.url", "ws://127.0.0.1:9091/ws"),
|
|
||||||
deviceId = get("device.id", "unknown-device"),
|
|
||||||
deviceToken = get("device.token", ""),
|
|
||||||
mainHealthUrl = get("main.health.url", "http://127.0.0.1:8082/api/admin/health"),
|
|
||||||
mainAdminToken = get("main.admin.token", ""),
|
|
||||||
mainStartCmd = get("main.start.cmd", "bin/start.bat"),
|
|
||||||
heartbeatSec = get("heartbeat.sec", "30").toLong(),
|
|
||||||
guardEnabled = get("guard.enable", "true") == "true",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
package com.hospital.agent
|
|
||||||
|
|
||||||
import com.hospital.agent.cmd.CommandDispatcher
|
|
||||||
import com.hospital.agent.cmd.UpgradeCommand
|
|
||||||
import com.hospital.agent.ipc.MainAppClient
|
|
||||||
import com.hospital.agent.process.ProcessGuard
|
|
||||||
import com.hospital.agent.ws.AgentClient
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Agent 启动入口(独立进程,不启动 Solon 容器)。
|
|
||||||
*
|
|
||||||
* 启动方式:
|
|
||||||
* java -cp hospital-front-1.0.0.jar com.hospital.agent.AgentMainKt
|
|
||||||
*
|
|
||||||
* 职责:
|
|
||||||
* 1. WebSocket 主动连接 B 端管理服务器(注册/心跳/命令)
|
|
||||||
* 2. 守护主程序(健康探测 + 宕机拉起)
|
|
||||||
* 3. 写 PID 文件(pid/hospital-agent.pid)
|
|
||||||
*
|
|
||||||
* 由 WinSW Windows 服务(HospitalAgent)守护本进程。
|
|
||||||
*/
|
|
||||||
object AgentMain {
|
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(AgentMain::class.java)
|
|
||||||
|
|
||||||
fun run() {
|
|
||||||
val config = AgentConfig.load()
|
|
||||||
log.info("Agent 启动:deviceId={}, server={}", config.deviceId, config.serverUrl)
|
|
||||||
println("[Agent] deviceId=${config.deviceId}, server=${config.serverUrl}")
|
|
||||||
|
|
||||||
// 写 PID 文件
|
|
||||||
writePidFile()
|
|
||||||
|
|
||||||
// 组装组件(client 与 dispatcher 互相引用,通过 lateinit 解耦)
|
|
||||||
val mainAppClient = MainAppClient(config)
|
|
||||||
val dispatcher = CommandDispatcher(config, mainAppClient)
|
|
||||||
val client = AgentClient(config, dispatcher)
|
|
||||||
dispatcher.client = client
|
|
||||||
val guard = ProcessGuard(config, mainAppClient, client)
|
|
||||||
|
|
||||||
// 优雅停机钩子
|
|
||||||
Runtime.getRuntime().addShutdownHook(Thread {
|
|
||||||
log.info("Agent 停机中...")
|
|
||||||
dispatcher.shutdownLogStream()
|
|
||||||
guard.stop()
|
|
||||||
client.stop()
|
|
||||||
})
|
|
||||||
|
|
||||||
client.start()
|
|
||||||
guard.start()
|
|
||||||
|
|
||||||
// 主线程保活
|
|
||||||
Thread.currentThread().join()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun writePidFile() {
|
|
||||||
try {
|
|
||||||
val dir = File("pid")
|
|
||||||
if (!dir.exists()) dir.mkdirs()
|
|
||||||
File(dir, "hospital-agent.pid").writeText(ProcessHandle.current().pid().toString())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.warn("PID 文件写入失败:{}", e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 主入口(顶层函数,编译为 AgentMainKt 的静态 main)。
|
|
||||||
*/
|
|
||||||
fun main() {
|
|
||||||
AgentMain.run()
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
package com.hospital.agent.cmd
|
|
||||||
|
|
||||||
import com.hospital.agent.AgentConfig
|
|
||||||
import com.hospital.agent.ipc.MainAppClient
|
|
||||||
import com.hospital.agent.ws.AgentClient
|
|
||||||
import org.noear.snack.ONode
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 下行命令分发器。
|
|
||||||
*
|
|
||||||
* B 端通过 WebSocket 下发 JSON 命令:{"type":"CMD_TYPE","payload":{...}}
|
|
||||||
*
|
|
||||||
* 已支持命令:
|
|
||||||
* - PING 探活(立即回 PONG)
|
|
||||||
* - GET_HEALTH 查询主程序健康
|
|
||||||
* - GET_VERSION 查询主程序版本
|
|
||||||
* - GET_LOG_FILES 日志文件列表
|
|
||||||
* - LOG_TAIL 日志尾部(payload: file, lines)
|
|
||||||
* - RESTART 重启主程序(优雅停机后由 ProcessGuard 拉起)
|
|
||||||
* - GET_CONFIG 读取医院配置文件(payload: hospital)
|
|
||||||
* - SAVE_CONFIG 写回医院配置文件(payload: hospital, content)
|
|
||||||
* - UPGRADE 升级主程序(Step 7 实现,当前返回未实现)
|
|
||||||
*
|
|
||||||
* 所有命令执行结果通过 AgentClient 上行(CMD_RESULT)。
|
|
||||||
*/
|
|
||||||
class CommandDispatcher(
|
|
||||||
val config: AgentConfig,
|
|
||||||
val mainAppClient: MainAppClient,
|
|
||||||
) {
|
|
||||||
private val log = LoggerFactory.getLogger(CommandDispatcher::class.java)
|
|
||||||
|
|
||||||
/** 由 AgentMain 注入(避免构造环) */
|
|
||||||
lateinit var client: AgentClient
|
|
||||||
|
|
||||||
/** 升级命令(懒加载,依赖 client 注入完成后可用) */
|
|
||||||
private val upgradeCommand: UpgradeCommand by lazy {
|
|
||||||
UpgradeCommand(config, mainAppClient, client)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 日志实时流管理(懒加载) */
|
|
||||||
private val logStream: LogStreamManager by lazy {
|
|
||||||
LogStreamManager(config, mainAppClient, client)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 日志实时流(供 AgentMain 停机时统一释放) */
|
|
||||||
fun shutdownLogStream() = logStream.stopAll()
|
|
||||||
|
|
||||||
/** 分发并执行命令 */
|
|
||||||
fun dispatch(type: String, payloadJson: String?) {
|
|
||||||
log.info("收到命令:{}", type)
|
|
||||||
val payload = payloadJson?.let { runCatching { ONode.load(it) }.getOrNull() }
|
|
||||||
|
|
||||||
val result: Map<String, Any?> = when (type) {
|
|
||||||
"PING" -> mapOf("pong" to true, "ts" to System.currentTimeMillis() / 1000)
|
|
||||||
|
|
||||||
"GET_HEALTH" -> mainAppClient.health()
|
|
||||||
|
|
||||||
"GET_VERSION" -> mainAppClient.version()
|
|
||||||
|
|
||||||
"GET_LOG_FILES" -> mapOf("ok" to true, "data" to mainAppClient.logFiles())
|
|
||||||
|
|
||||||
"LOG_TAIL" -> {
|
|
||||||
val file = payload?.get("file")?.getString() ?: "info.log"
|
|
||||||
val lines = payload?.get("lines")?.getInt() ?: 200
|
|
||||||
mapOf("ok" to true, "data" to mainAppClient.logTail(file, lines))
|
|
||||||
}
|
|
||||||
|
|
||||||
"LOG_TAIL_CTRL" -> {
|
|
||||||
val action = payload?.get("action")?.getString() ?: "start"
|
|
||||||
val file = payload?.get("file")?.getString() ?: "info.log"
|
|
||||||
logStream.control(action, file)
|
|
||||||
}
|
|
||||||
|
|
||||||
"RESTART" -> {
|
|
||||||
val ok = mainAppClient.restart()
|
|
||||||
mapOf("ok" to ok, "msg" to if (ok) "已触发优雅重启,等待 ProcessGuard 拉起" else "重启请求失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
"GET_CONFIG" -> {
|
|
||||||
val hospital = payload?.get("hospital")?.getString() ?: ""
|
|
||||||
val content = mainAppClient.configFile(hospital)
|
|
||||||
mapOf("ok" to (content != null), "data" to content)
|
|
||||||
}
|
|
||||||
|
|
||||||
"SAVE_CONFIG" -> {
|
|
||||||
val hospital = payload?.get("hospital")?.getString() ?: ""
|
|
||||||
val content = payload?.get("content")?.getString() ?: ""
|
|
||||||
mapOf("ok" to mainAppClient.saveConfigFile(hospital, content))
|
|
||||||
}
|
|
||||||
|
|
||||||
"UPGRADE" -> {
|
|
||||||
// 异步执行(升级耗时长,避免阻塞 WS 消息循环)
|
|
||||||
val version = payload?.get("version")?.getString() ?: "unknown"
|
|
||||||
val downloadUrl = payload?.get("downloadUrl")?.getString()
|
|
||||||
val sha256 = payload?.get("sha256")?.getString()
|
|
||||||
if (downloadUrl.isNullOrBlank()) {
|
|
||||||
mapOf("ok" to false, "msg" to "缺少 downloadUrl")
|
|
||||||
} else {
|
|
||||||
Thread {
|
|
||||||
val result = upgradeCommand.upgrade(version, downloadUrl, sha256)
|
|
||||||
client.send("UPGRADE_RESULT", result)
|
|
||||||
}.apply { isDaemon = true }.start()
|
|
||||||
mapOf("ok" to true, "msg" to "升级已开始(异步执行,结果将通过 UPGRADE_RESULT 上报)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> mapOf("ok" to false, "msg" to "未知命令:$type")
|
|
||||||
}
|
|
||||||
|
|
||||||
client.send("CMD_RESULT", mapOf("cmd" to type, "result" to result))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
package com.hospital.agent.cmd
|
|
||||||
|
|
||||||
import com.hospital.agent.AgentConfig
|
|
||||||
import com.hospital.agent.ipc.MainAppClient
|
|
||||||
import com.hospital.agent.ws.AgentClient
|
|
||||||
import org.noear.snack.ONode
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import java.util.concurrent.ScheduledExecutorService
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 日志实时推送管理器。
|
|
||||||
*
|
|
||||||
* B 端通过 LOG_TAIL_CTRL 命令开启/停止某设备某日志文件的实时流:
|
|
||||||
* - 开启后每 2 秒从主程序 /api/admin/log/tail 拉取增量行
|
|
||||||
* - 新行打包为 LOG_BATCH 上报 B 端(B 端转发给浏览器展示)
|
|
||||||
* - 每个文件独立游标(已发送行数),支持多文件并行
|
|
||||||
*
|
|
||||||
* 增量算法:tail(max(offset+FETCH, 400)) 行,取 offset 之后的部分;
|
|
||||||
* 日志滚动(文件变小/重置)时自动重置游标。
|
|
||||||
*/
|
|
||||||
class LogStreamManager(
|
|
||||||
private val config: AgentConfig,
|
|
||||||
private val mainAppClient: MainAppClient,
|
|
||||||
private val client: AgentClient,
|
|
||||||
) {
|
|
||||||
private val log = LoggerFactory.getLogger(LogStreamManager::class.java)
|
|
||||||
|
|
||||||
/** 单流状态:文件名 → 已发送行数 */
|
|
||||||
private val cursors = ConcurrentHashMap<String, Int>()
|
|
||||||
|
|
||||||
/** 运行中的流:文件名 → 是否激活 */
|
|
||||||
private val active = ConcurrentHashMap<String, Boolean>()
|
|
||||||
|
|
||||||
private val scheduler: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/** 轮询间隔(秒) */
|
|
||||||
private const val POLL_SEC = 2L
|
|
||||||
|
|
||||||
/** 单次拉取上限(行) */
|
|
||||||
private const val FETCH_MAX = 400
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 控制命令入口。
|
|
||||||
*
|
|
||||||
* @param action start / stop
|
|
||||||
* @param file 日志文件名(info.log / error.log)
|
|
||||||
*/
|
|
||||||
fun control(action: String, file: String): Map<String, Any?> {
|
|
||||||
return when (action) {
|
|
||||||
"start" -> {
|
|
||||||
if (active.putIfAbsent(file, true) != null) {
|
|
||||||
return mapOf("ok" to true, "msg" to "流已存在:$file")
|
|
||||||
}
|
|
||||||
cursors.remove(file)
|
|
||||||
scheduler.scheduleWithFixedDelay({ poll(file) }, 0, POLL_SEC, TimeUnit.SECONDS)
|
|
||||||
log.info("日志流开启:{}", file)
|
|
||||||
mapOf("ok" to true, "msg" to "日志流已开启:$file")
|
|
||||||
}
|
|
||||||
"stop" -> {
|
|
||||||
active.remove(file)
|
|
||||||
cursors.remove(file)
|
|
||||||
log.info("日志流停止:{}", file)
|
|
||||||
mapOf("ok" to true, "msg" to "日志流已停止:$file")
|
|
||||||
}
|
|
||||||
else -> mapOf("ok" to false, "msg" to "未知 action:$action(支持 start/stop)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 停止全部流(Agent 停机时调用) */
|
|
||||||
fun stopAll() {
|
|
||||||
active.clear()
|
|
||||||
cursors.clear()
|
|
||||||
scheduler.shutdownNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 单轮增量拉取:取尾部行,比对游标,新行打包上报。
|
|
||||||
*/
|
|
||||||
private fun poll(file: String) {
|
|
||||||
if (active[file] != true) return
|
|
||||||
try {
|
|
||||||
val offset = cursors[file] ?: 0
|
|
||||||
// 多拉一段,保证能覆盖到 offset 之前的已有内容
|
|
||||||
val fetch = (offset + 100).coerceAtLeast(FETCH_MAX)
|
|
||||||
val raw = mainAppClient.logTail(file, fetch)
|
|
||||||
log.debug("日志流轮询:file={}, offset={}, fetch={}, rawLen={}", file, offset, fetch, raw?.length ?: -1)
|
|
||||||
if (raw == null) return
|
|
||||||
val node = ONode.load(raw)
|
|
||||||
if (node.get("code").getInt() != 0) return
|
|
||||||
val content = node.get("data").getString() ?: return
|
|
||||||
if (content.isEmpty()) return
|
|
||||||
val lines = content.split('\n')
|
|
||||||
|
|
||||||
// 日志滚动检测:行数比游标还少很多 → 文件已重置
|
|
||||||
if (lines.size < offset) {
|
|
||||||
cursors[file] = 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (lines.size <= offset) return // 无新行
|
|
||||||
|
|
||||||
val fresh = lines.drop(offset).filter { it.isNotBlank() }
|
|
||||||
cursors[file] = lines.size
|
|
||||||
if (fresh.isEmpty()) return
|
|
||||||
|
|
||||||
client.send("LOG_BATCH", mapOf(
|
|
||||||
"file" to file,
|
|
||||||
"lines" to fresh,
|
|
||||||
"ts" to System.currentTimeMillis() / 1000,
|
|
||||||
))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.warn("日志流轮询失败({}):{}", file, e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
package com.hospital.agent.cmd
|
|
||||||
|
|
||||||
import com.hospital.agent.AgentConfig
|
|
||||||
import com.hospital.agent.ipc.MainAppClient
|
|
||||||
import com.hospital.agent.ws.AgentClient
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.io.File
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 主程序升级命令。
|
|
||||||
*
|
|
||||||
* 流程(见 05-部署运维.md SOP):
|
|
||||||
* 1. 从 B 端下载新 jar 到 tmp(SHA-256 校验,防传输损坏)
|
|
||||||
* 2. 备份当前 bin/hospital-front.jar → backup/hospital-front-{时间戳}.jar(保留最近 5 个)
|
|
||||||
* 3. 优雅停止主程序(调 /api/admin/restart;超时强杀)
|
|
||||||
* 4. 替换 jar
|
|
||||||
* 5. 调启动命令拉起主程序
|
|
||||||
* 6. 轮询健康检查(最长 90 秒)
|
|
||||||
* 7. 成功 → 上报;失败 → 用备份 jar 自动回滚并重启 → 上报
|
|
||||||
*
|
|
||||||
* 期间整个流程串行(upgradeLock 防并发升级)。
|
|
||||||
*/
|
|
||||||
class UpgradeCommand(
|
|
||||||
private val config: AgentConfig,
|
|
||||||
private val mainAppClient: MainAppClient,
|
|
||||||
private val client: AgentClient,
|
|
||||||
) {
|
|
||||||
private val log = LoggerFactory.getLogger(UpgradeCommand::class.java)
|
|
||||||
|
|
||||||
private val http = OkHttpClient.Builder()
|
|
||||||
.connectTimeout(10, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(120, TimeUnit.SECONDS) // 大文件下载
|
|
||||||
.build()
|
|
||||||
|
|
||||||
/** 升级互斥锁(同一时刻只允许一个升级在跑) */
|
|
||||||
private val upgradeLock = Any()
|
|
||||||
|
|
||||||
/** 备份保留数量 */
|
|
||||||
private val keepBackups = 5
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行升级。
|
|
||||||
*
|
|
||||||
* @param version 新版本号(仅用于日志与上报)
|
|
||||||
* @param downloadUrl B 端提供的 jar 下载地址(A 主动出站拉取)
|
|
||||||
* @param sha256 可选校验值
|
|
||||||
*/
|
|
||||||
fun upgrade(version: String, downloadUrl: String, sha256: String?): Map<String, Any?> = synchronized(upgradeLock) {
|
|
||||||
log.info("开始升级到版本 {}:{}", version, downloadUrl)
|
|
||||||
client.send("UPGRADE_PROGRESS", mapOf("version" to version, "stage" to "download"))
|
|
||||||
|
|
||||||
// ===== 1. 下载 =====
|
|
||||||
val tmpJar = File("tmp/upgrade-$version.jar")
|
|
||||||
tmpJar.parentFile?.mkdirs()
|
|
||||||
try {
|
|
||||||
http.newCall(Request.Builder().url(downloadUrl).get().build()).execute().use { resp ->
|
|
||||||
if (!resp.isSuccessful) return fail(version, "下载失败:HTTP ${resp.code}")
|
|
||||||
resp.body?.byteStream()?.use { input ->
|
|
||||||
tmpJar.outputStream().use { output -> input.copyTo(output) }
|
|
||||||
} ?: return fail(version, "下载失败:响应体为空")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return fail(version, "下载异常:${e.message}")
|
|
||||||
}
|
|
||||||
log.info("下载完成:{}({} 字节)", tmpJar.name, tmpJar.length())
|
|
||||||
|
|
||||||
// ===== 2. 校验 =====
|
|
||||||
if (sha256 != null) {
|
|
||||||
val actual = sha256File(tmpJar)
|
|
||||||
if (!actual.equals(sha256, ignoreCase = true)) {
|
|
||||||
tmpJar.delete()
|
|
||||||
return fail(version, "SHA-256 校验失败:expected=$sha256, actual=$actual")
|
|
||||||
}
|
|
||||||
log.info("SHA-256 校验通过")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 3. 备份 =====
|
|
||||||
val jarFile = File("bin/hospital-front.jar")
|
|
||||||
val backupFile = File("backup/hospital-front-${System.currentTimeMillis()}.jar")
|
|
||||||
backupFile.parentFile?.mkdirs()
|
|
||||||
if (jarFile.exists()) {
|
|
||||||
jarFile.copyTo(backupFile, overwrite = true)
|
|
||||||
log.info("已备份当前版本 → {}", backupFile.name)
|
|
||||||
cleanOldBackups()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 4. 停止主程序 =====
|
|
||||||
client.send("UPGRADE_PROGRESS", mapOf("version" to version, "stage" to "stop"))
|
|
||||||
stopMainApp()
|
|
||||||
log.info("主程序已停止")
|
|
||||||
|
|
||||||
// ===== 5. 替换 =====
|
|
||||||
// 注意:Agent 自身以 -cp bin/hospital-agent.jar(主 jar 的副本)运行,
|
|
||||||
// 因此可以安全替换 bin/hospital-front.jar;替换后同步刷新 Agent 副本
|
|
||||||
// (Agent 副本在下次重启时生效,不影响本次升级)。
|
|
||||||
try {
|
|
||||||
copyWithRetry(tmpJar, jarFile)
|
|
||||||
tmpJar.delete()
|
|
||||||
copyWithRetry(jarFile, File("bin/hospital-agent.jar"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// 替换失败(极少见):直接回滚
|
|
||||||
rollback(backupFile)
|
|
||||||
return fail(version, "替换 jar 失败,已回滚:${e.message}")
|
|
||||||
}
|
|
||||||
log.info("jar 已替换(含 Agent 副本)")
|
|
||||||
|
|
||||||
// ===== 6. 拉起 + 健康检查 =====
|
|
||||||
client.send("UPGRADE_PROGRESS", mapOf("version" to version, "stage" to "restart"))
|
|
||||||
startMainApp()
|
|
||||||
|
|
||||||
client.send("UPGRADE_PROGRESS", mapOf("version" to version, "stage" to "healthcheck"))
|
|
||||||
if (waitHealthy(90)) {
|
|
||||||
log.info("升级成功:{} ", version)
|
|
||||||
return mapOf(
|
|
||||||
"ok" to true,
|
|
||||||
"version" to version,
|
|
||||||
"msg" to "升级成功",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 7. 健康检查失败 → 回滚 =====
|
|
||||||
log.error("新版本健康检查失败,自动回滚到 {}", backupFile.name)
|
|
||||||
client.send("UPGRADE_PROGRESS", mapOf("version" to version, "stage" to "rollback"))
|
|
||||||
stopMainApp()
|
|
||||||
rollback(backupFile)
|
|
||||||
startMainApp()
|
|
||||||
val recovered = waitHealthy(90)
|
|
||||||
return if (recovered) {
|
|
||||||
fail(version, "新版本启动失败,已自动回滚到 ${backupFile.name} 并恢复服务")
|
|
||||||
} else {
|
|
||||||
client.send("ALERT", mapOf(
|
|
||||||
"level" to "CRITICAL",
|
|
||||||
"msg" to "升级失败且回滚后仍不健康,请人工介入!版本=$version",
|
|
||||||
))
|
|
||||||
fail(version, "升级失败且回滚后仍不健康,请人工介入")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= 内部步骤 =================
|
|
||||||
|
|
||||||
/** 停止主程序:优先优雅(admin API),超时后按 PID 强杀;确保进程完全退出(jar 锁释放) */
|
|
||||||
private fun stopMainApp() {
|
|
||||||
val pid = readPidFile()
|
|
||||||
if (mainAppClient.restart(delayMs = 100)) {
|
|
||||||
// 等待端口关闭(最长 30 秒)
|
|
||||||
val deadline = System.currentTimeMillis() + 30_000
|
|
||||||
while (System.currentTimeMillis() < deadline) {
|
|
||||||
if (mainAppClient.health()["ok"] != true) break
|
|
||||||
Thread.sleep(1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 兜底:按 PID 文件强杀
|
|
||||||
killByPidFile("pid/hospital-front.pid")
|
|
||||||
// 等待进程真正退出(Windows 下进程退出后才释放 jar 文件锁)
|
|
||||||
if (pid != null) waitProcessExit(pid, 15)
|
|
||||||
Thread.sleep(1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 读 PID 文件 */
|
|
||||||
private fun readPidFile(): String? = try {
|
|
||||||
val f = File("pid/hospital-front.pid")
|
|
||||||
if (f.exists()) f.readText().trim().ifEmpty { null } else null
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 轮询等待进程退出(tasklist 查不到即退出) */
|
|
||||||
private fun waitProcessExit(pid: String, timeoutSec: Long) {
|
|
||||||
val os = System.getProperty("os.name").lowercase()
|
|
||||||
val deadline = System.currentTimeMillis() + timeoutSec * 1000
|
|
||||||
while (System.currentTimeMillis() < deadline) {
|
|
||||||
val alive = try {
|
|
||||||
if (os.contains("win")) {
|
|
||||||
ProcessBuilder("tasklist", "/FI", "PID eq $pid")
|
|
||||||
.start().inputStream.use { it.readBytes().toString(Charsets.UTF_8).contains(pid) }
|
|
||||||
} else {
|
|
||||||
ProcessBuilder("kill", "-0", pid).start().waitFor() == 0
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
if (!alive) return
|
|
||||||
Thread.sleep(500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 拉起主程序 */
|
|
||||||
private fun startMainApp() {
|
|
||||||
try {
|
|
||||||
val cmd = config.mainStartCmd.replace("/", "\\") // cmd/c 要求反斜杠路径
|
|
||||||
val pb = if (cmd.endsWith(".bat", ignoreCase = true)) {
|
|
||||||
ProcessBuilder("cmd", "/c", cmd, "start")
|
|
||||||
} else {
|
|
||||||
ProcessBuilder("sh", cmd, "start")
|
|
||||||
}
|
|
||||||
pb.directory(File(".").absoluteFile)
|
|
||||||
pb.redirectErrorStream(true)
|
|
||||||
val proc = pb.start()
|
|
||||||
log.info("主程序拉起命令已执行,pid={}", proc.pid())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.error("主程序拉起失败:{}", e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 轮询等待主程序健康 */
|
|
||||||
private fun waitHealthy(timeoutSec: Long): Boolean {
|
|
||||||
val deadline = System.currentTimeMillis() + timeoutSec * 1000
|
|
||||||
while (System.currentTimeMillis() < deadline) {
|
|
||||||
if (mainAppClient.health()["ok"] == true) return true
|
|
||||||
Thread.sleep(3000)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 回滚:备份 jar 覆盖回 bin(含 Agent 副本) */
|
|
||||||
private fun rollback(backupFile: File) {
|
|
||||||
try {
|
|
||||||
if (backupFile.exists()) {
|
|
||||||
val jarFile = File("bin/hospital-front.jar")
|
|
||||||
copyWithRetry(backupFile, jarFile)
|
|
||||||
copyWithRetry(jarFile, File("bin/hospital-agent.jar"))
|
|
||||||
log.info("已回滚到 {}", backupFile.name)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.error("回滚失败:{}", e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 带重试的文件覆盖(Windows 下进程退出有延迟,锁释放需等待) */
|
|
||||||
private fun copyWithRetry(src: File, dst: File, retries: Int = 10) {
|
|
||||||
var lastError: Exception? = null
|
|
||||||
repeat(retries) {
|
|
||||||
try {
|
|
||||||
if (dst.exists()) dst.delete()
|
|
||||||
if (!dst.exists()) {
|
|
||||||
src.copyTo(dst)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
lastError = e
|
|
||||||
}
|
|
||||||
Thread.sleep(1000)
|
|
||||||
}
|
|
||||||
throw lastError ?: IllegalStateException("文件替换失败(重试 $retries 次):${dst.name}")
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 清理旧备份(保留最近 N 个) */
|
|
||||||
private fun cleanOldBackups() {
|
|
||||||
val dir = File("backup")
|
|
||||||
val backups = dir.listFiles { f -> f.name.startsWith("hospital-front-") }
|
|
||||||
?.sortedByDescending { it.name } ?: return
|
|
||||||
backups.drop(keepBackups).forEach { it.delete() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 按 PID 文件强杀进程 */
|
|
||||||
private fun killByPidFile(pidFile: String) {
|
|
||||||
try {
|
|
||||||
val f = File(pidFile)
|
|
||||||
if (!f.exists()) return
|
|
||||||
val pid = f.readText().trim()
|
|
||||||
if (pid.isEmpty()) return
|
|
||||||
val os = System.getProperty("os.name").lowercase()
|
|
||||||
if (os.contains("win")) {
|
|
||||||
ProcessBuilder("taskkill", "/PID", pid, "/T", "/F").start().waitFor()
|
|
||||||
} else {
|
|
||||||
ProcessBuilder("kill", "-9", pid).start().waitFor()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.warn("PID 强杀失败:{}", e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 文件 SHA-256 */
|
|
||||||
private fun sha256File(file: File): String {
|
|
||||||
val digest = MessageDigest.getInstance("SHA-256")
|
|
||||||
file.inputStream().use { input ->
|
|
||||||
val buf = ByteArray(8192)
|
|
||||||
while (true) {
|
|
||||||
val n = input.read(buf)
|
|
||||||
if (n < 0) break
|
|
||||||
digest.update(buf, 0, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return digest.digest().joinToString("") { "%02x".format(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 统一失败上报 */
|
|
||||||
private fun fail(version: String, msg: String): Map<String, Any?> {
|
|
||||||
log.error("升级失败:{}", msg)
|
|
||||||
return mapOf("ok" to false, "version" to version, "msg" to msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
package com.hospital.agent.ipc
|
|
||||||
|
|
||||||
import com.hospital.agent.AgentConfig
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import org.noear.snack.ONode
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 主程序 IPC 客户端(环回 HTTP)。
|
|
||||||
*
|
|
||||||
* Agent 通过本机 HTTP 调用主程序被控端 API(/api/admin)。
|
|
||||||
*/
|
|
||||||
class MainAppClient(private val config: AgentConfig) {
|
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(MainAppClient::class.java)
|
|
||||||
|
|
||||||
private val http = OkHttpClient.Builder()
|
|
||||||
.connectTimeout(5, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
/** 主程序健康检查:返回解析后的 JSON 字符串;不可达返回 {"ok":false,"error":"..."} */
|
|
||||||
fun health(): Map<String, Any?> {
|
|
||||||
val body = getJson(config.mainHealthUrl) ?: return mapOf("ok" to false, "error" to "主程序不可达")
|
|
||||||
return try {
|
|
||||||
val node = ONode.load(body)
|
|
||||||
mapOf(
|
|
||||||
"ok" to true,
|
|
||||||
"code" to node.get("code").getInt(),
|
|
||||||
"data" to node.get("data")?.toJson(),
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
mapOf("ok" to false, "error" to "健康响应解析失败:${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 主程序版本 */
|
|
||||||
fun version(): Map<String, Any?> {
|
|
||||||
val body = getJson(url("/api/admin/version")) ?: return mapOf("ok" to false)
|
|
||||||
return try {
|
|
||||||
val node = ONode.load(body)
|
|
||||||
mapOf("ok" to true, "data" to node.get("data")?.toJson())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
mapOf("ok" to false, "error" to e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 日志尾部 */
|
|
||||||
fun logTail(file: String, lines: Int): String? =
|
|
||||||
getJson(url("/api/admin/log/tail?file=$file&lines=$lines"))
|
|
||||||
|
|
||||||
/** 日志文件列表 */
|
|
||||||
fun logFiles(): String? = getJson(url("/api/admin/log/files"))
|
|
||||||
|
|
||||||
/** 触发主程序优雅重启 */
|
|
||||||
fun restart(delayMs: Long = 1000): Boolean =
|
|
||||||
post(url("/api/admin/restart?delayMs=$delayMs"), "") != null
|
|
||||||
|
|
||||||
/** 读取医院配置文件 */
|
|
||||||
fun configFile(hospital: String): String? =
|
|
||||||
getJson(url("/api/admin/config/file?hospital=$hospital"))
|
|
||||||
|
|
||||||
/** 写回医院配置文件 */
|
|
||||||
fun saveConfigFile(hospital: String, content: String): Boolean {
|
|
||||||
val body = post(url("/api/admin/config/file"), "hospital=$hospital&content=$content")
|
|
||||||
return body != null
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= 基础 HTTP =================
|
|
||||||
|
|
||||||
private fun url(path: String): String {
|
|
||||||
val base = config.mainHealthUrl.substringBefore("/api/")
|
|
||||||
return base + path
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getJson(url: String): String? = try {
|
|
||||||
http.newCall(
|
|
||||||
Request.Builder().url(url)
|
|
||||||
.header("X-Admin-Token", config.mainAdminToken)
|
|
||||||
.get().build()
|
|
||||||
).execute().use { resp -> if (resp.isSuccessful) resp.body?.string() else null }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.debug("GET {} 失败:{}", url, e.message)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun post(url: String, formBody: String): String? = try {
|
|
||||||
http.newCall(
|
|
||||||
Request.Builder().url(url)
|
|
||||||
.header("X-Admin-Token", config.mainAdminToken)
|
|
||||||
.post(formBody.toRequestBody(null))
|
|
||||||
.build()
|
|
||||||
).execute().use { resp -> if (resp.isSuccessful) resp.body?.string() else null }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.debug("POST {} 失败:{}", url, e.message)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package com.hospital.agent.process
|
|
||||||
|
|
||||||
import com.hospital.agent.AgentConfig
|
|
||||||
import com.hospital.agent.ipc.MainAppClient
|
|
||||||
import com.hospital.agent.ws.AgentClient
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.io.File
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 主程序进程守护(看门狗)。
|
|
||||||
*
|
|
||||||
* 周期探测主程序健康:
|
|
||||||
* - 连续 3 次失败 → 认为主程序宕机 → 强杀残留 → 调启动命令拉起
|
|
||||||
* - 拉起后等待健康恢复;连续 3 次拉起失败 → 上报告警(ALERT)
|
|
||||||
*/
|
|
||||||
class ProcessGuard(
|
|
||||||
private val config: AgentConfig,
|
|
||||||
private val mainAppClient: MainAppClient,
|
|
||||||
private val client: AgentClient,
|
|
||||||
) {
|
|
||||||
private val log = LoggerFactory.getLogger(ProcessGuard::class.java)
|
|
||||||
|
|
||||||
private val failCount = AtomicInteger(0)
|
|
||||||
private val restartCount = AtomicInteger(0)
|
|
||||||
|
|
||||||
private val scheduler = Executors.newSingleThreadScheduledExecutor()
|
|
||||||
|
|
||||||
/** 启动守护(默认 15 秒探测一次) */
|
|
||||||
fun start() {
|
|
||||||
if (!config.guardEnabled) {
|
|
||||||
log.info("进程守护未启用(guard.enable=false)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
scheduler.scheduleWithFixedDelay({ check() }, 30, 15, TimeUnit.SECONDS)
|
|
||||||
log.info("进程守护已启动:每 15 秒探测 {}", config.mainHealthUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
|
||||||
scheduler.shutdownNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun check() {
|
|
||||||
val health = mainAppClient.health()
|
|
||||||
val ok = health["ok"] == true
|
|
||||||
if (ok) {
|
|
||||||
failCount.set(0)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val fails = failCount.incrementAndGet()
|
|
||||||
log.warn("主程序健康探测失败({}/3):{}", fails, health["error"])
|
|
||||||
if (fails < 3) return
|
|
||||||
|
|
||||||
// 连续 3 次失败 → 拉起
|
|
||||||
failCount.set(0)
|
|
||||||
val attempts = restartCount.incrementAndGet()
|
|
||||||
if (attempts > 3) {
|
|
||||||
client.send("ALERT", mapOf(
|
|
||||||
"level" to "CRITICAL",
|
|
||||||
"msg" to "主程序连续 $attempts 次拉起失败,请人工介入",
|
|
||||||
))
|
|
||||||
// 重置计数,之后继续尝试(降频可后续优化)
|
|
||||||
restartCount.set(3)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.error("主程序宕机,执行第 {} 次拉起:{}", attempts, config.mainStartCmd)
|
|
||||||
client.send("ALERT", mapOf(
|
|
||||||
"level" to "WARN",
|
|
||||||
"msg" to "主程序宕机,Agent 正在执行第 $attempts 次拉起",
|
|
||||||
))
|
|
||||||
run {
|
|
||||||
try {
|
|
||||||
val cmd = config.mainStartCmd.replace("/", "\\") // cmd/c 要求反斜杠路径
|
|
||||||
val isBat = cmd.endsWith(".bat", ignoreCase = true)
|
|
||||||
val pb = if (isBat) {
|
|
||||||
ProcessBuilder("cmd", "/c", cmd, "start")
|
|
||||||
} else {
|
|
||||||
ProcessBuilder("sh", cmd, "start")
|
|
||||||
}
|
|
||||||
pb.directory(File(".").absoluteFile)
|
|
||||||
pb.redirectErrorStream(true)
|
|
||||||
val proc = pb.start()
|
|
||||||
log.info("启动命令已执行,pid={}", proc.pid())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.error("启动命令执行失败:{}", e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
package com.hospital.agent.ws
|
|
||||||
|
|
||||||
import com.hospital.agent.AgentConfig
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.WebSocket
|
|
||||||
import okhttp3.WebSocketListener
|
|
||||||
import org.noear.snack.ONode
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import java.util.concurrent.ScheduledExecutorService
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Agent WebSocket 客户端。
|
|
||||||
*
|
|
||||||
* - 主动出站连接 B 端(穿透白名单,无需 A 端开放入站端口)
|
|
||||||
* - 断线指数退避重连(1s → 2s → 4s → ... 最大 60s)
|
|
||||||
* - 周期心跳:拉取主程序 /api/admin/health 并转发给 B 端
|
|
||||||
* - 收到下行命令交给 CommandDispatcher 处理
|
|
||||||
*
|
|
||||||
* 上行消息格式:{"type":"HEARTBEAT","deviceId":"...","ts":...,"payload":{...}}
|
|
||||||
*/
|
|
||||||
class AgentClient(
|
|
||||||
private val config: AgentConfig,
|
|
||||||
private val dispatcher: com.hospital.agent.cmd.CommandDispatcher,
|
|
||||||
) {
|
|
||||||
private val log = LoggerFactory.getLogger(AgentClient::class.java)
|
|
||||||
|
|
||||||
private val client = OkHttpClient.Builder()
|
|
||||||
.pingInterval(25, TimeUnit.SECONDS) // 协议层心跳
|
|
||||||
.connectTimeout(10, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(0, TimeUnit.MILLISECONDS) // WebSocket 不设读超时
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private var webSocket: WebSocket? = null
|
|
||||||
private val connected = AtomicBoolean(false)
|
|
||||||
private val reconnectAttempts = AtomicInteger(0)
|
|
||||||
private val scheduler: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
|
|
||||||
|
|
||||||
/** 启动:连接 + 心跳调度 */
|
|
||||||
fun start() {
|
|
||||||
connect()
|
|
||||||
scheduler.scheduleWithFixedDelay(
|
|
||||||
{ heartbeat() },
|
|
||||||
config.heartbeatSec, config.heartbeatSec, TimeUnit.SECONDS,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 停止并释放资源 */
|
|
||||||
fun stop() {
|
|
||||||
scheduler.shutdownNow()
|
|
||||||
webSocket?.close(1000, "agent stopping")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= 连接管理 =================
|
|
||||||
|
|
||||||
private fun connect() {
|
|
||||||
if (connected.get()) return
|
|
||||||
log.info("连接管理服务器:{}", config.serverUrl)
|
|
||||||
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(config.serverUrl)
|
|
||||||
.header("X-Device-Id", config.deviceId)
|
|
||||||
.header("X-Device-Token", config.deviceToken)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
webSocket = client.newWebSocket(request, object : WebSocketListener() {
|
|
||||||
override fun onOpen(ws: WebSocket, response: Response) {
|
|
||||||
connected.set(true)
|
|
||||||
reconnectAttempts.set(0)
|
|
||||||
log.info("已连接管理服务器")
|
|
||||||
send("REGISTER", mapOf(
|
|
||||||
"deviceId" to config.deviceId,
|
|
||||||
"version" to "1.0.0",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMessage(ws: WebSocket, text: String) {
|
|
||||||
try {
|
|
||||||
val node = ONode.load(text)
|
|
||||||
val type = node.get("type").getString()
|
|
||||||
val payload = node.get("payload")?.toJson()
|
|
||||||
dispatcher.dispatch(type, payload)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.warn("命令处理异常:{}", e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClosed(ws: WebSocket, code: Int, reason: String) {
|
|
||||||
connected.set(false)
|
|
||||||
log.info("连接关闭:code={}, reason={}", code, reason)
|
|
||||||
scheduleReconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) {
|
|
||||||
connected.set(false)
|
|
||||||
log.warn("连接失败:{},将重连", t.message)
|
|
||||||
scheduleReconnect()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 指数退避重连:1s 起,翻倍至 60s 封顶 */
|
|
||||||
private fun scheduleReconnect() {
|
|
||||||
val attempt = reconnectAttempts.incrementAndGet()
|
|
||||||
val delay = (1000L shl (attempt - 1).coerceAtMost(6)).coerceAtMost(60_000L)
|
|
||||||
log.info("{}ms 后进行第 {} 次重连", delay, attempt)
|
|
||||||
scheduler.schedule({ connect() }, delay, TimeUnit.MILLISECONDS)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= 上行消息 =================
|
|
||||||
|
|
||||||
/** 心跳:拉主程序健康数据转发 */
|
|
||||||
private fun heartbeat() {
|
|
||||||
val mainHealth = dispatcher.mainAppClient.health()
|
|
||||||
send("HEARTBEAT", mapOf(
|
|
||||||
"deviceId" to config.deviceId,
|
|
||||||
"mainApp" to mainHealth,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 发送上行消息(未连接时丢弃并记日志) */
|
|
||||||
fun send(type: String, payload: Any?) {
|
|
||||||
val ws = webSocket ?: run {
|
|
||||||
log.debug("未连接,丢弃上行消息:{}", type)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val json = ONode.load(
|
|
||||||
linkedMapOf(
|
|
||||||
"type" to type,
|
|
||||||
"deviceId" to config.deviceId,
|
|
||||||
"ts" to System.currentTimeMillis() / 1000,
|
|
||||||
"payload" to payload,
|
|
||||||
)
|
|
||||||
).toJson()
|
|
||||||
ws.send(json)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 当前是否已连接(供健康状态展示) */
|
|
||||||
fun isConnected(): Boolean = connected.get()
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package com.hospital.front
|
|
||||||
|
|
||||||
import org.noear.solon.Solon
|
|
||||||
import org.noear.solon.annotation.SolonMain
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 医院前置机主程序启动类。
|
|
||||||
*
|
|
||||||
* 使用 Solon 3.x 框架,对外暴露体检预约业务接口(/order 路径)
|
|
||||||
* 与被控端接口(/api/admin 路径)。
|
|
||||||
*
|
|
||||||
* 启动命令示例:
|
|
||||||
* java -Xms256m -Xmx512m -Dsolon.env=jinghe -Dsolon.config.load=app.yml,conf/app-jinghe.yml -jar hospital-front-1.0.0.jar
|
|
||||||
*/
|
|
||||||
@SolonMain
|
|
||||||
class HospitalFrontApp
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 主入口(顶层函数,编译为 HospitalFrontAppKt 的静态 main)。
|
|
||||||
*/
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
Solon.start(HospitalFrontApp::class.java, args)
|
|
||||||
val pid = ProcessHandle.current().pid()
|
|
||||||
println("[HospitalFront] 启动完成,PID=$pid")
|
|
||||||
// 写 PID 文件(供 start.bat stop / Agent 升级流程强杀使用)
|
|
||||||
try {
|
|
||||||
val dir = java.io.File("pid")
|
|
||||||
if (!dir.exists()) dir.mkdirs()
|
|
||||||
java.io.File(dir, "hospital-front.pid").writeText(pid.toString())
|
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
|
||||||
// 注册停机钩子:清理 PID + 记录优雅退出日志
|
|
||||||
Runtime.getRuntime().addShutdownHook(Thread {
|
|
||||||
try {
|
|
||||||
java.io.File("pid/hospital-front.pid").delete()
|
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
|
||||||
println("[HospitalFront] 优雅停机中...")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
package com.hospital.front.adapter
|
|
||||||
|
|
||||||
import com.hospital.front.dto.ProcResult
|
|
||||||
import com.hospital.front.dto.Result
|
|
||||||
import com.hospital.front.mapper.bsm.BsmOrderMapper
|
|
||||||
import com.hospital.front.vo.UnitVo
|
|
||||||
import org.noear.solon.annotation.Inject
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BSM 方言适配器基类(Oracle)。
|
|
||||||
*
|
|
||||||
* 适用医院:xinglongyuan(兴隆园)/ jinghe(泾河)/ qingyang(庆阳)。
|
|
||||||
* 由具体医院子类提供 code / displayName,其余行为完全一致。
|
|
||||||
*/
|
|
||||||
abstract class BsmAdapterBase : HospitalAdapter {
|
|
||||||
|
|
||||||
/** 数据库 Mapper(Solon 按类型注入) */
|
|
||||||
@Inject
|
|
||||||
protected lateinit var mapper: BsmOrderMapper
|
|
||||||
|
|
||||||
override fun hmType(): String = "bsm"
|
|
||||||
|
|
||||||
override fun healthCheck(): Result<Boolean> = try {
|
|
||||||
Result.success(mapper.ping() == 1)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Result.error(Result.CODE_DB_UNREACHABLE, "数据库连接失败:${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun saveOrder(peInfoStr: String, peItemStr: String): ProcResult {
|
|
||||||
val map = mutableMapOf<String, Any?>("peInfoStr" to peInfoStr, "peItemStr" to peItemStr)
|
|
||||||
mapper.saveOrder(map)
|
|
||||||
return ProcResult(
|
|
||||||
resultCode = map["resultCode"]?.toString(),
|
|
||||||
errorMsg = map["errorMsg"]?.toString(),
|
|
||||||
peId = map["peId"]?.toString(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancelOrder(peId: String, name: String, idcard: String): ProcResult {
|
|
||||||
val map = mutableMapOf<String, Any?>("peId" to peId, "name" to name, "idcard" to idcard)
|
|
||||||
mapper.cancelOrder(map)
|
|
||||||
return ProcResult(
|
|
||||||
resultCode = map["resultCode"]?.toString(),
|
|
||||||
errorMsg = map["errorMsg"]?.toString(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addUnit(unit: UnitVo, useV2: Boolean): ProcResult {
|
|
||||||
val map = mutableMapOf<String, Any?>(
|
|
||||||
"unitCode" to unit.unitCode,
|
|
||||||
"unitName" to unit.unitName,
|
|
||||||
"parentUnitCode" to unit.parentUnitCode,
|
|
||||||
"address" to unit.address,
|
|
||||||
"connecter1" to unit.connecter1,
|
|
||||||
"phone1" to unit.phone1,
|
|
||||||
"connecter2" to unit.connecter2,
|
|
||||||
"phone2" to unit.phone2,
|
|
||||||
)
|
|
||||||
if (useV2) mapper.addUnitV2(map) else mapper.addUnit(map)
|
|
||||||
return ProcResult(
|
|
||||||
resultCode = map["resultCode"]?.toString(),
|
|
||||||
errorMsg = map["errorMsg"]?.toString(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPhyexamStatus(peIds: String) = mapper.getPhyexamStatus(peIds)
|
|
||||||
override fun getItemList() = mapper.getItemList()
|
|
||||||
override fun getAllItemList() = mapper.getAllItemList()
|
|
||||||
override fun getPeItemList() = mapper.getPeItemList()
|
|
||||||
override fun getItemInfo(peItemCode: String) = mapper.getItemInfo(peItemCode)
|
|
||||||
override fun getEmpItem(peId: String) = mapper.getEmpItem(peId)
|
|
||||||
override fun getResult(peId: String) = mapper.getResult(peId)
|
|
||||||
override fun getConclusion(peId: String) = mapper.getConclusion(peId)
|
|
||||||
override fun getConclusionAll(peId: String) = mapper.getConclusionAll(peId)
|
|
||||||
override fun getSuggest(peId: String) = mapper.getSuggest(peId)
|
|
||||||
override fun getSuggestAll(peId: String) = mapper.getSuggestAll(peId)
|
|
||||||
override fun getPeStatusByIdnoAndYear(idNo: String, year: String) = mapper.getPeStatusByIdnoAndYear(idNo, year)
|
|
||||||
override fun getResultByIdcardAndYear(idNo: String, year: String) = mapper.getResultByIdcardAndYear(idNo, year)
|
|
||||||
override fun getResultByIdcardAndYearAll(idNo: String, year: String) = mapper.getResultByIdcardAndYearAll(idNo, year)
|
|
||||||
override fun getResultByPeId(peId: String) = mapper.getResultByPeId(peId)
|
|
||||||
override fun getDisResultByAuditDate(auditDate: String) = mapper.getDisResultByAuditDate(auditDate)
|
|
||||||
override fun getItemByPeId(peId: String) = mapper.getItemByPeId(peId)
|
|
||||||
override fun getItemByPeIdAll(peId: String) = mapper.getItemByPeIdAll(peId)
|
|
||||||
override fun getResultByIdNoOrPeId(idNo: String?, peId: String?, auditYear: String) =
|
|
||||||
mapper.getResultByIdNoOrPeId(idNo, peId, auditYear)
|
|
||||||
override fun getPeIdAndDateByIdcard(idNo: String) = mapper.getPeIdAndDateByIdcard(idNo)
|
|
||||||
override fun getPeIdAndDateByIdcardList(idNo: String) = mapper.getPeIdAndDateByIdcardList(idNo)
|
|
||||||
override fun getExamListByDate(auditDate: String?) = mapper.getExamListByDate(auditDate)
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package com.hospital.front.adapter
|
|
||||||
|
|
||||||
import com.hospital.front.dto.ProcResult
|
|
||||||
import com.hospital.front.dto.Result
|
|
||||||
import com.hospital.front.vo.UnitVo
|
|
||||||
import org.noear.solon.annotation.Component
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 开发环境适配器(hospital.active=dev 时激活)。
|
|
||||||
*
|
|
||||||
* 不连接任何数据库:查询一律返回空列表,存储过程一律返回失败。
|
|
||||||
* 用于本机无医院数据库时的启动与联调。
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
class DevAdapter : HospitalAdapter {
|
|
||||||
|
|
||||||
override fun code(): String = "dev"
|
|
||||||
|
|
||||||
override fun displayName(): String = "开发环境(Mock)"
|
|
||||||
|
|
||||||
override fun hmType(): String = "dev"
|
|
||||||
|
|
||||||
override fun healthCheck(): Result<Boolean> = Result.success(true)
|
|
||||||
|
|
||||||
override fun saveOrder(peInfoStr: String, peItemStr: String): ProcResult =
|
|
||||||
ProcResult(resultCode = "0", errorMsg = "开发环境不支持预约提交")
|
|
||||||
|
|
||||||
override fun cancelOrder(peId: String, name: String, idcard: String): ProcResult =
|
|
||||||
ProcResult(resultCode = "0", errorMsg = "开发环境不支持取消预约")
|
|
||||||
|
|
||||||
override fun addUnit(unit: UnitVo, useV2: Boolean): ProcResult =
|
|
||||||
ProcResult(resultCode = "0", errorMsg = "开发环境不支持单位同步")
|
|
||||||
|
|
||||||
override fun getPhyexamStatus(peIds: String): List<Map<String, Any?>> = emptyList()
|
|
||||||
override fun getItemList(): List<Map<String, Any?>> = emptyList()
|
|
||||||
override fun getAllItemList(): List<Map<String, Any?>> = emptyList()
|
|
||||||
override fun getPeItemList(): List<Map<String, Any?>> = emptyList()
|
|
||||||
override fun getItemInfo(peItemCode: String): List<Map<String, Any?>> = emptyList()
|
|
||||||
override fun getEmpItem(peId: String): List<Map<String, Any?>> = emptyList()
|
|
||||||
override fun getResult(peId: String): List<Map<String, Any?>> = emptyList()
|
|
||||||
override fun getConclusion(peId: String): List<Map<String, Any?>> = emptyList()
|
|
||||||
override fun getConclusionAll(peId: String): List<Map<String, Any?>> = emptyList()
|
|
||||||
override fun getSuggest(peId: String): List<Map<String, Any?>> = emptyList()
|
|
||||||
override fun getSuggestAll(peId: String): List<Map<String, Any?>> = emptyList()
|
|
||||||
override fun getPeStatusByIdnoAndYear(idNo: String, year: String): Map<String, Any?>? = null
|
|
||||||
override fun getResultByIdcardAndYear(idNo: String, year: String): List<Map<String, Any?>> = emptyList()
|
|
||||||
override fun getResultByIdcardAndYearAll(idNo: String, year: String): List<Map<String, Any?>> = emptyList()
|
|
||||||
override fun getResultByPeId(peId: String): List<Map<String, Any?>> = emptyList()
|
|
||||||
override fun getDisResultByAuditDate(auditDate: String): List<Map<String, Any?>> = emptyList()
|
|
||||||
override fun getItemByPeId(peId: String): List<Map<String, Any?>> = emptyList()
|
|
||||||
override fun getItemByPeIdAll(peId: String): List<Map<String, Any?>> = emptyList()
|
|
||||||
override fun getResultByIdNoOrPeId(idNo: String?, peId: String?, auditYear: String): List<Map<String, Any?>> = emptyList()
|
|
||||||
override fun getPeIdAndDateByIdcard(idNo: String): Map<String, Any?>? = null
|
|
||||||
override fun getPeIdAndDateByIdcardList(idNo: String): List<Map<String, Any?>> = emptyList()
|
|
||||||
override fun getExamListByDate(auditDate: String?): List<Map<String, Any?>> = emptyList()
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
package com.hospital.front.adapter
|
|
||||||
|
|
||||||
import com.hospital.front.dto.ProcResult
|
|
||||||
import com.hospital.front.dto.Result
|
|
||||||
import com.hospital.front.vo.OrderItemVo
|
|
||||||
import com.hospital.front.vo.OrderV2Vo
|
|
||||||
import com.hospital.front.vo.UnitVo
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 医院适配器接口(策略模式)。
|
|
||||||
*
|
|
||||||
* 5 家医院各自实现本接口,通过 HospitalRouter 按医院编码路由。
|
|
||||||
* 所有数据库差异(Oracle/SqlServer 方言、存储过程名)均封装在实现内;
|
|
||||||
* 存储过程的入参出参契约由医院 HIS 系统约定,不可修改。
|
|
||||||
*
|
|
||||||
* 查询类方法统一返回 Map 列表(列名与医院视图/存储过程保持一致)。
|
|
||||||
*/
|
|
||||||
interface HospitalAdapter {
|
|
||||||
|
|
||||||
/** 医院编码(拼音):xinglongyuan / qingyang / ningxia / yanan / jinghe / dev */
|
|
||||||
fun code(): String
|
|
||||||
|
|
||||||
/** 医院中文名(日志与展示用) */
|
|
||||||
fun displayName(): String
|
|
||||||
|
|
||||||
/** Mapper 方言类型:bsm(Oracle)/ nx / ya(SqlServer) */
|
|
||||||
fun hmType(): String
|
|
||||||
|
|
||||||
/** 轻量健康检查(SELECT 1 探测) */
|
|
||||||
fun healthCheck(): Result<Boolean>
|
|
||||||
|
|
||||||
// ================= 存储过程(写) =================
|
|
||||||
|
|
||||||
/** 提交预约(P_ADD_PE_REGIST) */
|
|
||||||
fun saveOrder(peInfoStr: String, peItemStr: String): ProcResult
|
|
||||||
|
|
||||||
/** 取消预约(P_DEL_PE_REGIST) */
|
|
||||||
fun cancelOrder(peId: String, name: String, idcard: String): ProcResult
|
|
||||||
|
|
||||||
/** 添加/修改单位(P_ADD_UNIT) */
|
|
||||||
fun addUnit(unit: UnitVo, useV2: Boolean): ProcResult
|
|
||||||
|
|
||||||
// ================= 查询(视图) =================
|
|
||||||
|
|
||||||
/** 按体检预约 ID(逗号分隔多个)查询体检状态 */
|
|
||||||
fun getPhyexamStatus(peIds: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 医院所有体检项目(组合项) */
|
|
||||||
fun getItemList(): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 医院所有体检项目及指标项 */
|
|
||||||
fun getAllItemList(): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 医院所有体检指标项 */
|
|
||||||
fun getPeItemList(): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按指标项 CODE 查询所属体检组合 */
|
|
||||||
fun getItemInfo(peItemCode: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 员工体检项目 */
|
|
||||||
fun getEmpItem(peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 员工体检结果 */
|
|
||||||
fun getResult(peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 体检结论(列表 / 全量) */
|
|
||||||
fun getConclusion(peId: String): List<Map<String, Any?>>
|
|
||||||
fun getConclusionAll(peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 体检建议(列表 / 全量) */
|
|
||||||
fun getSuggest(peId: String): List<Map<String, Any?>>
|
|
||||||
fun getSuggestAll(peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按身份证号 + 年份查询体检状态 */
|
|
||||||
fun getPeStatusByIdnoAndYear(idNo: String, year: String): Map<String, Any?>?
|
|
||||||
|
|
||||||
/** 按身份证号 + 年份查询体检结果(默认 / 全量) */
|
|
||||||
fun getResultByIdcardAndYear(idNo: String, year: String): List<Map<String, Any?>>
|
|
||||||
fun getResultByIdcardAndYearAll(idNo: String, year: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按体检编号查询体检结果 */
|
|
||||||
fun getResultByPeId(peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按审核日期查询体检报告(去重主表) */
|
|
||||||
fun getDisResultByAuditDate(auditDate: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按体检编号查询体检项目子表(默认 / 全量) */
|
|
||||||
fun getItemByPeId(peId: String): List<Map<String, Any?>>
|
|
||||||
fun getItemByPeIdAll(peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按身份证号或体检编号 + 审核年份查询体检结果 */
|
|
||||||
fun getResultByIdNoOrPeId(idNo: String?, peId: String?, auditYear: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按身份证号查询当年 peId 与预约日期(单条 / 列表) */
|
|
||||||
fun getPeIdAndDateByIdcard(idNo: String): Map<String, Any?>?
|
|
||||||
fun getPeIdAndDateByIdcardList(idNo: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按审核日期查询当天体检人员列表(去重;日期空则当天) */
|
|
||||||
fun getExamListByDate(auditDate: String?): List<Map<String, Any?>>
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package com.hospital.front.adapter
|
|
||||||
|
|
||||||
import org.noear.solon.Solon
|
|
||||||
import org.noear.solon.annotation.Component
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 医院适配器路由。
|
|
||||||
*
|
|
||||||
* 通过 Solon.context().getBeanOfType 在首次使用时动态获取所有 HospitalAdapter 实现,
|
|
||||||
* 避免依赖注入时序问题。
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
class HospitalRouter {
|
|
||||||
|
|
||||||
private val activeCode: String = Solon.cfg().get("hospital.active", "jinghe")!!
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 动态拉取所有 HospitalAdapter 实现(每次调用都重新拉取,避免 bean 注入时序问题)。
|
|
||||||
*/
|
|
||||||
private fun listAdapters(): List<HospitalAdapter> =
|
|
||||||
Solon.context().getBeansOfType(HospitalAdapter::class.java)
|
|
||||||
|
|
||||||
private val map: Map<String, HospitalAdapter> by lazy {
|
|
||||||
listAdapters().associateBy { it.code() }
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
println("[HospitalRouter] 启动激活医院:$activeCode")
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取指定医院编码的适配器 */
|
|
||||||
fun get(code: String): HospitalAdapter =
|
|
||||||
map[code] ?: throw IllegalStateException("未注册医院适配器:$code(已注册:${map.keys})")
|
|
||||||
|
|
||||||
/** 获取启动激活的医院适配器 */
|
|
||||||
fun active(): HospitalAdapter = get(activeCode)
|
|
||||||
|
|
||||||
/** 当前激活医院编码 */
|
|
||||||
fun activeCode(): String = activeCode
|
|
||||||
|
|
||||||
/** 所有已注册的医院编码 */
|
|
||||||
fun registeredCodes(): Set<String> = map.keys
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.hospital.front.adapter
|
|
||||||
|
|
||||||
import org.noear.solon.annotation.Component
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 泾河医院适配器(Oracle,bsm 方言,默认激活医院)。
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
class JingheAdapter : BsmAdapterBase() {
|
|
||||||
|
|
||||||
override fun code(): String = "jinghe"
|
|
||||||
|
|
||||||
override fun displayName(): String = "泾河医院"
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.hospital.front.adapter
|
|
||||||
|
|
||||||
import org.noear.solon.annotation.Component
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 宁夏宝石花医院适配器(SqlServer,nx 方言)。
|
|
||||||
*
|
|
||||||
* 业务开关(在 OrderService 中按配置生效):
|
|
||||||
* - hm.allsecondFlag=true:强制二级单位编码/名称截断
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
class NingxiaAdapter : NxAdapterBase() {
|
|
||||||
|
|
||||||
override fun code(): String = "ningxia"
|
|
||||||
|
|
||||||
override fun displayName(): String = "宁夏宝石花医院"
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
package com.hospital.front.adapter
|
|
||||||
|
|
||||||
import com.hospital.front.dto.ProcResult
|
|
||||||
import com.hospital.front.dto.Result
|
|
||||||
import com.hospital.front.mapper.nx.NxOrderMapper
|
|
||||||
import com.hospital.front.vo.UnitVo
|
|
||||||
import org.noear.solon.annotation.Inject
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NX 方言适配器基类(SqlServer)。
|
|
||||||
*
|
|
||||||
* 适用医院:ningxia(宁夏宝石花)。
|
|
||||||
*/
|
|
||||||
abstract class NxAdapterBase : HospitalAdapter {
|
|
||||||
|
|
||||||
/** 数据库 Mapper(Solon 按类型注入) */
|
|
||||||
@Inject
|
|
||||||
protected lateinit var mapper: NxOrderMapper
|
|
||||||
|
|
||||||
override fun hmType(): String = "nx"
|
|
||||||
|
|
||||||
override fun healthCheck(): Result<Boolean> = try {
|
|
||||||
Result.success(mapper.ping() == 1)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Result.error(Result.CODE_DB_UNREACHABLE, "数据库连接失败:${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun saveOrder(peInfoStr: String, peItemStr: String): ProcResult {
|
|
||||||
val map = mutableMapOf<String, Any?>("peInfoStr" to peInfoStr, "peItemStr" to peItemStr)
|
|
||||||
mapper.saveOrder(map)
|
|
||||||
return ProcResult(
|
|
||||||
resultCode = map["resultCode"]?.toString(),
|
|
||||||
errorMsg = map["errorMsg"]?.toString(),
|
|
||||||
peId = map["peId"]?.toString(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancelOrder(peId: String, name: String, idcard: String): ProcResult {
|
|
||||||
val map = mutableMapOf<String, Any?>("peId" to peId, "name" to name, "idcard" to idcard)
|
|
||||||
mapper.cancelOrder(map)
|
|
||||||
return ProcResult(
|
|
||||||
resultCode = map["resultCode"]?.toString(),
|
|
||||||
errorMsg = map["errorMsg"]?.toString(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addUnit(unit: UnitVo, useV2: Boolean): ProcResult {
|
|
||||||
// 宁夏使用 addUnitV2(原 OrderServiceImpl 中 nx 分支固定走 V2)
|
|
||||||
val map = mutableMapOf<String, Any?>(
|
|
||||||
"unitCode" to unit.unitCode,
|
|
||||||
"unitName" to unit.unitName,
|
|
||||||
"parentUnitCode" to unit.parentUnitCode,
|
|
||||||
"address" to unit.address,
|
|
||||||
"connecter1" to unit.connecter1,
|
|
||||||
"phone1" to unit.phone1,
|
|
||||||
"connecter2" to unit.connecter2,
|
|
||||||
"phone2" to unit.phone2,
|
|
||||||
)
|
|
||||||
mapper.addUnitV2(map)
|
|
||||||
return ProcResult(
|
|
||||||
resultCode = map["resultCode"]?.toString(),
|
|
||||||
errorMsg = map["errorMsg"]?.toString(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPhyexamStatus(peIds: String) = mapper.getPhyexamStatus(peIds)
|
|
||||||
override fun getItemList() = mapper.getItemList()
|
|
||||||
override fun getAllItemList() = mapper.getAllItemList()
|
|
||||||
override fun getPeItemList() = mapper.getPeItemList()
|
|
||||||
override fun getItemInfo(peItemCode: String) = mapper.getItemInfo(peItemCode)
|
|
||||||
override fun getEmpItem(peId: String) = mapper.getEmpItem(peId)
|
|
||||||
override fun getResult(peId: String) = mapper.getResult(peId)
|
|
||||||
override fun getConclusion(peId: String) = mapper.getConclusion(peId)
|
|
||||||
override fun getConclusionAll(peId: String) = mapper.getConclusionAll(peId)
|
|
||||||
override fun getSuggest(peId: String) = mapper.getSuggest(peId)
|
|
||||||
override fun getSuggestAll(peId: String) = mapper.getSuggestAll(peId)
|
|
||||||
override fun getPeStatusByIdnoAndYear(idNo: String, year: String) = mapper.getPeStatusByIdnoAndYear(idNo, year)
|
|
||||||
override fun getResultByIdcardAndYear(idNo: String, year: String) = mapper.getResultByIdcardAndYear(idNo, year)
|
|
||||||
override fun getResultByIdcardAndYearAll(idNo: String, year: String) = mapper.getResultByIdcardAndYearAll(idNo, year)
|
|
||||||
override fun getResultByPeId(peId: String) = mapper.getResultByPeId(peId)
|
|
||||||
override fun getDisResultByAuditDate(auditDate: String) = mapper.getDisResultByAuditDate(auditDate)
|
|
||||||
override fun getItemByPeId(peId: String) = mapper.getItemByPeId(peId)
|
|
||||||
override fun getItemByPeIdAll(peId: String) = mapper.getItemByPeIdAll(peId)
|
|
||||||
override fun getResultByIdNoOrPeId(idNo: String?, peId: String?, auditYear: String) =
|
|
||||||
mapper.getResultByIdNoOrPeId(idNo, peId, auditYear)
|
|
||||||
override fun getPeIdAndDateByIdcard(idNo: String) = mapper.getPeIdAndDateByIdcard(idNo)
|
|
||||||
override fun getPeIdAndDateByIdcardList(idNo: String) = mapper.getPeIdAndDateByIdcardList(idNo)
|
|
||||||
override fun getExamListByDate(auditDate: String?) = mapper.getExamListByDate(auditDate)
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.hospital.front.adapter
|
|
||||||
|
|
||||||
import org.noear.solon.annotation.Component
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 庆阳医院适配器(Oracle,bsm 方言)。
|
|
||||||
*
|
|
||||||
* 数据库连接暂与泾河一致,正式环境就绪后调整 pac/conf/app-qingyang.yml。
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
class QingyangAdapter : BsmAdapterBase() {
|
|
||||||
|
|
||||||
override fun code(): String = "qingyang"
|
|
||||||
|
|
||||||
override fun displayName(): String = "庆阳医院"
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.hospital.front.adapter
|
|
||||||
|
|
||||||
import org.noear.solon.annotation.Component
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 兴隆园医院适配器(Oracle,bsm 方言)。
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
class XingLongYuanAdapter : BsmAdapterBase() {
|
|
||||||
|
|
||||||
override fun code(): String = "xinglongyuan"
|
|
||||||
|
|
||||||
override fun displayName(): String = "兴隆园医院"
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
package com.hospital.front.adapter
|
|
||||||
|
|
||||||
import com.hospital.front.dto.ProcResult
|
|
||||||
import com.hospital.front.dto.Result
|
|
||||||
import com.hospital.front.mapper.ya.YaOrderMapper
|
|
||||||
import com.hospital.front.vo.UnitVo
|
|
||||||
import org.noear.solon.annotation.Inject
|
|
||||||
|
|
||||||
/**
|
|
||||||
* YA 方言适配器基类(SqlServer)。
|
|
||||||
*
|
|
||||||
* 适用医院:yanan(延安)。
|
|
||||||
*/
|
|
||||||
abstract class YaAdapterBase : HospitalAdapter {
|
|
||||||
|
|
||||||
/** 数据库 Mapper(Solon 按类型注入) */
|
|
||||||
@Inject
|
|
||||||
protected lateinit var mapper: YaOrderMapper
|
|
||||||
|
|
||||||
override fun hmType(): String = "ya"
|
|
||||||
|
|
||||||
override fun healthCheck(): Result<Boolean> = try {
|
|
||||||
Result.success(mapper.ping() == 1)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Result.error(Result.CODE_DB_UNREACHABLE, "数据库连接失败:${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun saveOrder(peInfoStr: String, peItemStr: String): ProcResult {
|
|
||||||
val map = mutableMapOf<String, Any?>("peInfoStr" to peInfoStr, "peItemStr" to peItemStr)
|
|
||||||
mapper.saveOrder(map)
|
|
||||||
return ProcResult(
|
|
||||||
resultCode = map["resultCode"]?.toString(),
|
|
||||||
errorMsg = map["errorMsg"]?.toString(),
|
|
||||||
peId = map["peId"]?.toString(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancelOrder(peId: String, name: String, idcard: String): ProcResult {
|
|
||||||
val map = mutableMapOf<String, Any?>("peId" to peId, "name" to name, "idcard" to idcard)
|
|
||||||
mapper.cancelOrder(map)
|
|
||||||
return ProcResult(
|
|
||||||
resultCode = map["resultCode"]?.toString(),
|
|
||||||
errorMsg = map["errorMsg"]?.toString(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addUnit(unit: UnitVo, useV2: Boolean): ProcResult {
|
|
||||||
val map = mutableMapOf<String, Any?>(
|
|
||||||
"unitCode" to unit.unitCode,
|
|
||||||
"unitName" to unit.unitName,
|
|
||||||
"parentUnitCode" to unit.parentUnitCode,
|
|
||||||
"address" to unit.address,
|
|
||||||
"connecter1" to unit.connecter1,
|
|
||||||
"phone1" to unit.phone1,
|
|
||||||
"connecter2" to unit.connecter2,
|
|
||||||
"phone2" to unit.phone2,
|
|
||||||
)
|
|
||||||
if (useV2) mapper.addUnitV2(map) else mapper.addUnit(map)
|
|
||||||
return ProcResult(
|
|
||||||
resultCode = map["resultCode"]?.toString(),
|
|
||||||
errorMsg = map["errorMsg"]?.toString(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPhyexamStatus(peIds: String) = mapper.getPhyexamStatus(peIds)
|
|
||||||
override fun getItemList() = mapper.getItemList()
|
|
||||||
override fun getAllItemList() = mapper.getAllItemList()
|
|
||||||
override fun getPeItemList() = mapper.getPeItemList()
|
|
||||||
override fun getItemInfo(peItemCode: String) = mapper.getItemInfo(peItemCode)
|
|
||||||
override fun getEmpItem(peId: String) = mapper.getEmpItem(peId)
|
|
||||||
override fun getResult(peId: String) = mapper.getResult(peId)
|
|
||||||
override fun getConclusion(peId: String) = mapper.getConclusion(peId)
|
|
||||||
override fun getConclusionAll(peId: String) = mapper.getConclusionAll(peId)
|
|
||||||
override fun getSuggest(peId: String) = mapper.getSuggest(peId)
|
|
||||||
override fun getSuggestAll(peId: String) = mapper.getSuggestAll(peId)
|
|
||||||
override fun getPeStatusByIdnoAndYear(idNo: String, year: String) = mapper.getPeStatusByIdnoAndYear(idNo, year)
|
|
||||||
override fun getResultByIdcardAndYear(idNo: String, year: String) = mapper.getResultByIdcardAndYear(idNo, year)
|
|
||||||
override fun getResultByIdcardAndYearAll(idNo: String, year: String) = mapper.getResultByIdcardAndYearAll(idNo, year)
|
|
||||||
override fun getResultByPeId(peId: String) = mapper.getResultByPeId(peId)
|
|
||||||
override fun getDisResultByAuditDate(auditDate: String) = mapper.getDisResultByAuditDate(auditDate)
|
|
||||||
override fun getItemByPeId(peId: String) = mapper.getItemByPeId(peId)
|
|
||||||
override fun getItemByPeIdAll(peId: String) = mapper.getItemByPeIdAll(peId)
|
|
||||||
override fun getResultByIdNoOrPeId(idNo: String?, peId: String?, auditYear: String) =
|
|
||||||
mapper.getResultByIdNoOrPeId(idNo, peId, auditYear)
|
|
||||||
override fun getPeIdAndDateByIdcard(idNo: String) = mapper.getPeIdAndDateByIdcard(idNo)
|
|
||||||
override fun getPeIdAndDateByIdcardList(idNo: String) = mapper.getPeIdAndDateByIdcardList(idNo)
|
|
||||||
override fun getExamListByDate(auditDate: String?) = mapper.getExamListByDate(auditDate)
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.hospital.front.adapter
|
|
||||||
|
|
||||||
import org.noear.solon.annotation.Component
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 延安医院适配器(SqlServer,ya 方言)。
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
class YananAdapter : YaAdapterBase() {
|
|
||||||
|
|
||||||
override fun code(): String = "yanan"
|
|
||||||
|
|
||||||
override fun displayName(): String = "延安医院"
|
|
||||||
}
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
package com.hospital.front.api.admin
|
|
||||||
|
|
||||||
import com.hospital.front.adapter.HospitalRouter
|
|
||||||
import com.hospital.front.dto.Result
|
|
||||||
import com.hospital.front.infra.exception.BusinessException
|
|
||||||
import org.noear.solon.Solon
|
|
||||||
import org.noear.solon.annotation.Controller
|
|
||||||
import org.noear.solon.annotation.Inject
|
|
||||||
import org.noear.solon.annotation.Mapping
|
|
||||||
import org.noear.solon.annotation.Param
|
|
||||||
import org.noear.solon.core.handle.Context
|
|
||||||
import java.io.File
|
|
||||||
import java.lang.management.ManagementFactory
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Paths
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 主程序被控端 API(/api/admin)。
|
|
||||||
*
|
|
||||||
* 供同机 Agent(环回地址)调用,实现远程运维:
|
|
||||||
* - 健康检查 / 版本查询 / JVM 指标
|
|
||||||
* - 配置查看(脱敏)
|
|
||||||
* - 日志文件列表 / 尾部查看 / 全量下载
|
|
||||||
* - 优雅重启
|
|
||||||
*
|
|
||||||
* 鉴权:请求头 X-Admin-Token 与配置 admin.token 比对(未配置时放行,仅本机使用)。
|
|
||||||
*/
|
|
||||||
@Controller
|
|
||||||
@Mapping("/api/admin")
|
|
||||||
class AdminApi {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
private lateinit var router: HospitalRouter
|
|
||||||
|
|
||||||
/** 日志目录(与 logback.xml 的 log.path 保持一致) */
|
|
||||||
private val logDir: File
|
|
||||||
get() = File(Solon.cfg().get("log.path", "./log")!!)
|
|
||||||
|
|
||||||
// ================= 鉴权 =================
|
|
||||||
|
|
||||||
private fun checkToken(ctx: Context) {
|
|
||||||
val expected = Solon.cfg().get("admin.token", "")
|
|
||||||
if (expected.isNotBlank() && ctx.header("X-Admin-Token") != expected) {
|
|
||||||
throw BusinessException.of(Result.CODE_AUTH_FAILED, "管理接口鉴权失败")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= 1. 健康与指标 =================
|
|
||||||
|
|
||||||
/** 健康检查(含数据库探测) */
|
|
||||||
@Mapping("/health")
|
|
||||||
fun health(ctx: Context): Result<Map<String, Any?>> {
|
|
||||||
checkToken(ctx)
|
|
||||||
val mem = Runtime.getRuntime()
|
|
||||||
val db = router.active().healthCheck()
|
|
||||||
return Result.success(
|
|
||||||
mapOf(
|
|
||||||
"status" to if (db.code == 0) "UP" else "DEGRADED",
|
|
||||||
"hospital" to router.activeCode(),
|
|
||||||
"hospitalName" to router.active().displayName(),
|
|
||||||
"registeredHospitals" to router.registeredCodes(),
|
|
||||||
"database" to mapOf(
|
|
||||||
"ok" to (db.code == 0),
|
|
||||||
"msg" to db.msg,
|
|
||||||
),
|
|
||||||
"jvm" to mapOf(
|
|
||||||
"totalMemoryMb" to mem.totalMemory() / 1048576,
|
|
||||||
"freeMemoryMb" to mem.freeMemory() / 1048576,
|
|
||||||
"maxMemoryMb" to mem.maxMemory() / 1048576,
|
|
||||||
"availableProcessors" to mem.availableProcessors(),
|
|
||||||
),
|
|
||||||
"uptimeSec" to ManagementFactory.getRuntimeMXBean().uptime / 1000,
|
|
||||||
"pid" to ProcessHandle.current().pid(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 版本信息(读 MANIFEST) */
|
|
||||||
@Mapping("/version")
|
|
||||||
fun version(ctx: Context): Result<Map<String, String>> {
|
|
||||||
checkToken(ctx)
|
|
||||||
val pkg = this::class.java.`package`
|
|
||||||
return Result.success(
|
|
||||||
mapOf(
|
|
||||||
"app" to "hospital-front",
|
|
||||||
"version" to (pkg?.implementationVersion ?: "1.0.0-dev"),
|
|
||||||
"hospital" to router.activeCode(),
|
|
||||||
"javaVersion" to System.getProperty("java.version"),
|
|
||||||
"startTime" to ManagementFactory.getRuntimeMXBean().startTime.toString(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= 2. 配置管理 =================
|
|
||||||
|
|
||||||
/** 查看当前生效配置(密码脱敏) */
|
|
||||||
@Mapping("/config")
|
|
||||||
fun config(ctx: Context): Result<Map<String, Any?>> {
|
|
||||||
checkToken(ctx)
|
|
||||||
val keys = listOf(
|
|
||||||
"hospital.active", "hospital.id",
|
|
||||||
"hm.type", "hm.ver", "hm.allsecondFlag", "hm.occFlag",
|
|
||||||
"server.port", "admin.token", "log.path",
|
|
||||||
)
|
|
||||||
val conf = keys.associateWith { Solon.cfg().get(it, "") }
|
|
||||||
// 数据源脱敏(db1! 的 ! 在 cfg 键中已剥离,两种写法都尝试)
|
|
||||||
val ds = mapOf(
|
|
||||||
"db1.jdbcUrl" to (Solon.cfg().get("solon.dataSources.db1.jdbcUrl")
|
|
||||||
?: Solon.cfg().get("solon.dataSources.db1!.jdbcUrl") ?: ""),
|
|
||||||
"db1.username" to (Solon.cfg().get("solon.dataSources.db1.username")
|
|
||||||
?: Solon.cfg().get("solon.dataSources.db1!.username") ?: ""),
|
|
||||||
"db1.password" to "******",
|
|
||||||
)
|
|
||||||
return Result.success(mapOf("config" to conf, "dataSource" to ds))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 列出 pac/conf 目录下的医院配置文件内容(供 B 端查看/修改)。
|
|
||||||
*/
|
|
||||||
@Mapping("/config/file")
|
|
||||||
fun configFile(ctx: Context, @Param("hospital") hospital: String): Result<String> {
|
|
||||||
checkToken(ctx)
|
|
||||||
val file = File("conf/app-$hospital.yml")
|
|
||||||
if (!file.exists()) {
|
|
||||||
throw BusinessException.paramMissing("配置文件 conf/app-$hospital.yml 不存在")
|
|
||||||
}
|
|
||||||
return Result.success(String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 写回医院配置文件(B 端修改后下发)。重启后生效。
|
|
||||||
*/
|
|
||||||
@Mapping(value = "/config/file", method = [org.noear.solon.core.handle.MethodType.POST])
|
|
||||||
fun saveConfigFile(ctx: Context, @Param("hospital") hospital: String, @Param(name = "content") content: String): Result<Nothing?> {
|
|
||||||
checkToken(ctx)
|
|
||||||
if (!hospital.matches(Regex("[a-z]+"))) {
|
|
||||||
throw BusinessException.businessFailed("非法医院编码:$hospital")
|
|
||||||
}
|
|
||||||
val file = File("conf/app-$hospital.yml")
|
|
||||||
file.parentFile?.mkdirs()
|
|
||||||
Files.write(file.toPath(), content.toByteArray(StandardCharsets.UTF_8))
|
|
||||||
return Result.success()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= 3. 日志管理 =================
|
|
||||||
|
|
||||||
/** 日志文件列表 */
|
|
||||||
@Mapping("/log/files")
|
|
||||||
fun logFiles(ctx: Context): Result<List<Map<String, Any?>>> {
|
|
||||||
checkToken(ctx)
|
|
||||||
val dir = logDir
|
|
||||||
if (!dir.exists()) return Result.success(emptyList())
|
|
||||||
return Result.success(
|
|
||||||
dir.listFiles { f -> f.isFile }
|
|
||||||
?.sortedByDescending { it.lastModified() }
|
|
||||||
?.take(50)
|
|
||||||
?.map { mapOf("name" to it.name, "sizeBytes" to it.length(), "lastModified" to it.lastModified()) }
|
|
||||||
?: emptyList()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 日志尾部 N 行(默认 200);容错解码(历史日志可能混有 GBK 内容) */
|
|
||||||
@Mapping("/log/tail")
|
|
||||||
fun logTail(ctx: Context, @Param("file") file: String, @Param("lines") lines: Int?): Result<String> {
|
|
||||||
checkToken(ctx)
|
|
||||||
val target = safeLogFile(file)
|
|
||||||
val n = (lines ?: 200).coerceIn(1, 2000)
|
|
||||||
val decoder = StandardCharsets.UTF_8.newDecoder()
|
|
||||||
.onMalformedInput(java.nio.charset.CodingErrorAction.REPLACE)
|
|
||||||
.onUnmappableCharacter(java.nio.charset.CodingErrorAction.REPLACE)
|
|
||||||
val text = decoder.decode(java.nio.ByteBuffer.wrap(Files.readAllBytes(target.toPath()))).toString()
|
|
||||||
val all = text.split('\n')
|
|
||||||
return Result.success(all.takeLast(n).joinToString("\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 日志全量下载 */
|
|
||||||
@Mapping("/log/download")
|
|
||||||
fun logDownload(ctx: Context, @Param("file") file: String) {
|
|
||||||
checkToken(ctx)
|
|
||||||
val target = safeLogFile(file)
|
|
||||||
ctx.contentType("application/octet-stream")
|
|
||||||
ctx.headerSet("Content-Disposition", "attachment; filename=\"${target.name}\"")
|
|
||||||
Files.copy(target.toPath(), ctx.outputStream())
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 限制只能读日志目录内的文件(防路径穿越) */
|
|
||||||
private fun safeLogFile(name: String): File {
|
|
||||||
if (name.contains("..") || name.contains("/") || name.contains("\\")) {
|
|
||||||
throw BusinessException.businessFailed("非法文件名")
|
|
||||||
}
|
|
||||||
val f = File(logDir, name)
|
|
||||||
if (!f.exists()) throw BusinessException.paramMissing("日志文件不存在:$name")
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= 4. 进程管理 =================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 优雅重启:记录标记后延迟退出,Agent 检测到进程退出后拉起新进程。
|
|
||||||
*/
|
|
||||||
@Mapping(value = "/restart", method = [org.noear.solon.core.handle.MethodType.POST])
|
|
||||||
fun restart(ctx: Context, @Param("delayMs") delayMs: Long?): Result<Nothing?> {
|
|
||||||
checkToken(ctx)
|
|
||||||
val delay = (delayMs ?: 1000L).coerceIn(0, 10000)
|
|
||||||
Thread {
|
|
||||||
Thread.sleep(delay)
|
|
||||||
Solon.stopBlock() // 触发 Solon 停机事件(优雅退出)
|
|
||||||
}.apply { isDaemon = true }.start()
|
|
||||||
return Result.success()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
package com.hospital.front.api.order
|
|
||||||
|
|
||||||
import com.hospital.front.dto.Result
|
|
||||||
import com.hospital.front.infra.exception.BusinessException
|
|
||||||
import com.hospital.front.service.OrderService
|
|
||||||
import org.noear.solon.annotation.Body
|
|
||||||
import org.noear.solon.annotation.Controller
|
|
||||||
import org.noear.solon.annotation.Inject
|
|
||||||
import org.noear.solon.annotation.Mapping
|
|
||||||
import org.noear.solon.annotation.Param
|
|
||||||
import org.noear.solon.core.handle.MethodType
|
|
||||||
import org.noear.solon.annotation.Path
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 体检预约业务接口,路径前缀 /order。
|
|
||||||
*
|
|
||||||
* 路径与出参结构保持与原 hospitalmiddle 一致(调用方契约,不可修改)。
|
|
||||||
* 根路径前缀为 /order,各方法 Mapping 为相对路径。
|
|
||||||
*/
|
|
||||||
@Controller
|
|
||||||
@Mapping("/order")
|
|
||||||
class OrderApi {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
private lateinit var service: OrderService
|
|
||||||
|
|
||||||
// ================= 1. 预约提交 / 取消 =================
|
|
||||||
|
|
||||||
/** 4. 提交预约保存 */
|
|
||||||
@Mapping(value = "/saveOrder", method = [MethodType.POST])
|
|
||||||
fun saveOrder(@Body body: String): Result<String> {
|
|
||||||
// Solon 对 Map 泛型 body 反序列化支持不佳,用 snack3 手动解析
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val map: Map<String, Any?> = org.noear.snack.ONode.load(body).toObject(Map::class.java) as Map<String, Any?>
|
|
||||||
return Result.success(service.saveOrder(map))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 5. 取消体检预约 */
|
|
||||||
@Mapping(value = "/cancelOrder/{peId}", method = [MethodType.POST])
|
|
||||||
fun cancelOrder(
|
|
||||||
@Path("peId") peId: String,
|
|
||||||
@Param("name") name: String,
|
|
||||||
@Param("idcard") idcard: String,
|
|
||||||
): Result<Nothing?> {
|
|
||||||
service.cancelOrder(peId, name, idcard)
|
|
||||||
return Result.success()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= 2. 体检状态 =================
|
|
||||||
|
|
||||||
/** 3. 体检状态查询(多个 peId 逗号分隔) */
|
|
||||||
@Mapping("/getPeStatus/{peIds}")
|
|
||||||
fun getPeStatus(@Path("peIds") peIds: String): Result<List<Map<String, String>>> =
|
|
||||||
Result.success(service.getPhyexamStatus(peIds))
|
|
||||||
|
|
||||||
/** 13. 根据身份证号及年份获取当前体检状态及体检项状态 */
|
|
||||||
@Mapping("/getPeStatusAndItemList/{idNo}/{year}")
|
|
||||||
fun getPeStatusAndItemList(@Path("idNo") idNo: String, @Path("year") year: String): Result<Map<String, Any?>?> =
|
|
||||||
Result.success(service.getPeStatusByIdnoAndYear(idNo, year))
|
|
||||||
|
|
||||||
// ================= 3. 体检项目 =================
|
|
||||||
|
|
||||||
/** 1. 查询员工体检项目 */
|
|
||||||
@Mapping("/getEmpItem/{peId}")
|
|
||||||
fun getEmpItem(@Path("peId") peId: String): Result<List<Map<String, Any?>>> =
|
|
||||||
Result.success(service.getEmpItem(peId))
|
|
||||||
|
|
||||||
/** 2. 体检项目查询 */
|
|
||||||
@Mapping("/getItemList")
|
|
||||||
fun getItemList(): Result<List<Map<String, Any?>>> =
|
|
||||||
Result.success(service.getItemList())
|
|
||||||
|
|
||||||
/** 6. 体检指标项数据获取 */
|
|
||||||
@Mapping("/getPeItemList")
|
|
||||||
fun getPeItemList(): Result<List<Map<String, Any?>>> =
|
|
||||||
Result.success(service.getPeItemList())
|
|
||||||
|
|
||||||
/** 6.1 体检指标项及体检组合数据获取 */
|
|
||||||
@Mapping("/getAllItemList")
|
|
||||||
fun getAllItemList(): Result<List<Map<String, Any?>>> =
|
|
||||||
Result.success(service.getAllItemList())
|
|
||||||
|
|
||||||
/** 7. 根据体检指标项查询所属体检组合项 */
|
|
||||||
@Mapping("/getItemInfo/{peItemCode}")
|
|
||||||
fun getItemInfo(@Path("peItemCode") peItemCode: String): Result<List<Map<String, Any?>>> =
|
|
||||||
Result.success(service.getItemInfo(peItemCode))
|
|
||||||
|
|
||||||
// ================= 4. 体检结果 / 结论 / 建议 =================
|
|
||||||
|
|
||||||
/** 8. 根据体检编号获取员工体检结果 */
|
|
||||||
@Mapping("/getResult/{peId}")
|
|
||||||
fun getResult(@Path("peId") peId: String): Result<List<Map<String, Any?>>> =
|
|
||||||
Result.success(service.getResult(peId))
|
|
||||||
|
|
||||||
/** 9. 体检结论(汇总) */
|
|
||||||
@Mapping("/getTotalConclusion/{peId}")
|
|
||||||
fun getTotalConclusion(@Path("peId") peId: String): Result<Map<String, String>> =
|
|
||||||
Result.success(service.getTotalConclusion(peId))
|
|
||||||
|
|
||||||
/** 10. 体检建议(汇总) */
|
|
||||||
@Mapping("/getTotalSuggest/{peId}")
|
|
||||||
fun getTotalSuggest(@Path("peId") peId: String): Result<Map<String, String>> =
|
|
||||||
Result.success(service.getTotalSuggest(peId))
|
|
||||||
|
|
||||||
/** 11. 体检结论(列表) */
|
|
||||||
@Mapping("/getConclusion/{peId}")
|
|
||||||
fun getConclusion(@Path("peId") peId: String): Result<List<Map<String, Any?>>> =
|
|
||||||
Result.success(service.getConclusion(peId))
|
|
||||||
|
|
||||||
/** 12. 体检建议(列表) */
|
|
||||||
@Mapping("/getSuggest/{peId}")
|
|
||||||
fun getSuggest(@Path("peId") peId: String): Result<List<Map<String, Any?>>> =
|
|
||||||
Result.success(service.getSuggest(peId))
|
|
||||||
|
|
||||||
// ================= 5. 按身份证 / 日期查询 =================
|
|
||||||
|
|
||||||
/** 14. 根据身份证号及年份获取体检结果 */
|
|
||||||
@Mapping("/getResultByIdcardAndYear/{idNo}/{year}")
|
|
||||||
fun getResultByIdcardAndYear(@Path("idNo") idNo: String, @Path("year") year: String): Result<List<Map<String, Any?>>> =
|
|
||||||
Result.success(service.getResultByIdcardAndYear(idNo, year))
|
|
||||||
|
|
||||||
/** 14.1 根据身份证号及年份获取体检结果(全量) */
|
|
||||||
@Mapping("/getResultByIdcardAndYearAll/{idNo}/{year}")
|
|
||||||
fun getResultByIdcardAndYearAll(@Path("idNo") idNo: String, @Path("year") year: String): Result<List<Map<String, Any?>>> =
|
|
||||||
Result.success(service.getResultByIdcardAndYearAll(idNo, year))
|
|
||||||
|
|
||||||
/** 15.1 根据体检编号获取体检结果 */
|
|
||||||
@Mapping("/getResultByPeId/{peId}")
|
|
||||||
fun getResultByPeId(@Path("peId") peId: String): Result<List<Map<String, Any?>>> =
|
|
||||||
Result.success(service.getResultByPeId(peId))
|
|
||||||
|
|
||||||
/** 15.2 根据体检编号获取体检状态(简) */
|
|
||||||
@Mapping("/getResultStatus/{peId}")
|
|
||||||
fun getResultStatus(@Path("peId") peId: String): Result<List<Map<String, Any?>>> =
|
|
||||||
Result.success(service.getResultByPeId(peId))
|
|
||||||
|
|
||||||
/** 15.3 根据 audit_date 查询当天的体检报告 */
|
|
||||||
@Mapping("/getResultByAuditDate/{auditDate}")
|
|
||||||
fun getResultByAuditDate(@Path("auditDate") auditDate: String): Result<List<Map<String, Any?>>> =
|
|
||||||
Result.success(service.getResultByAuditDate(auditDate))
|
|
||||||
|
|
||||||
/** 16. 根据 id_code 或 pe_id 和审查年份查询体检报告 */
|
|
||||||
@Mapping(value = "/getResultByIdNoOrPeId", method = [MethodType.POST])
|
|
||||||
fun getResultByIdNoOrPeId(
|
|
||||||
@Param(value = "idNo", required = false) idNo: String?,
|
|
||||||
@Param(value = "peId", required = false) peId: String?,
|
|
||||||
@Param("auditYear") auditYear: String,
|
|
||||||
): Result<List<Map<String, Any?>>> {
|
|
||||||
if (idNo.isNullOrBlank() && peId.isNullOrBlank()) {
|
|
||||||
throw BusinessException.paramMissing("身份证号或体检编号")
|
|
||||||
}
|
|
||||||
return Result.success(service.getResultByIdNoOrPeId(idNo, peId, auditYear))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 17. 根据身份证号查询当前年的 peId 和预约日期 */
|
|
||||||
@Mapping("/getPeIdAndDateByIdcard/{idNo}")
|
|
||||||
fun getPeIdAndDateByIdcard(@Path("idNo") idNo: String): Result<Map<String, Any?>?> =
|
|
||||||
Result.success(service.getPeIdAndDateByIdcard(idNo))
|
|
||||||
|
|
||||||
/** 17.1 同上(数组) */
|
|
||||||
@Mapping("/getPeIdAndDateByIdcardList/{idNo}")
|
|
||||||
fun getPeIdAndDateByIdcardList(@Path("idNo") idNo: String): Result<List<Map<String, Any?>>> =
|
|
||||||
Result.success(service.getPeIdAndDateByIdcardList(idNo))
|
|
||||||
|
|
||||||
// ================= 6. 单位 / 人员列表 =================
|
|
||||||
|
|
||||||
/** 18. 添加修改单位信息 */
|
|
||||||
@Mapping(value = "/addUnit", method = [MethodType.POST])
|
|
||||||
fun addUnit(@Body body: String): Result<Nothing?> {
|
|
||||||
// Solon 对 List 泛型 body 反序列化支持不佳,此处用 snack3 手动解析
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val units: List<Map<String, Any?>> =
|
|
||||||
if (body.isBlank()) emptyList()
|
|
||||||
else org.noear.snack.ONode.load(body).toObject(List::class.java) as List<Map<String, Any?>>
|
|
||||||
val errors = service.addUnits(units)
|
|
||||||
return if (errors != null) Result.error(errors) else Result.success()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 19. 根据日期查询当天体检人员列表(去重),日期为空则查当天 */
|
|
||||||
@Mapping("/getExamListByDate")
|
|
||||||
fun getExamListByDate(@Param("auditDate") auditDate: String?): Result<List<Map<String, Any?>>> =
|
|
||||||
Result.success(service.getExamListByDate(auditDate))
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.hospital.front.dto
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 医院存储过程执行结果。
|
|
||||||
*
|
|
||||||
* @property resultCode 医院侧返回码("1" = 成功)
|
|
||||||
* @property errorMsg 错误信息(失败时)
|
|
||||||
* @property peId 预约成功后医院分配的体检编号(仅 saveOrder 有值)
|
|
||||||
*/
|
|
||||||
data class ProcResult(
|
|
||||||
val resultCode: String? = null,
|
|
||||||
val errorMsg: String? = null,
|
|
||||||
val peId: String? = null,
|
|
||||||
) {
|
|
||||||
/** 医院侧是否执行成功 */
|
|
||||||
val isSuccess: Boolean get() = resultCode == "1"
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package com.hospital.front.dto
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 统一 API 响应包装。
|
|
||||||
*
|
|
||||||
* 业务接口均使用此结构返回,便于前端统一处理。
|
|
||||||
*
|
|
||||||
* @param T 数据类型
|
|
||||||
* @property code 业务码(0=成功)
|
|
||||||
* @property msg 提示信息
|
|
||||||
* @property data 业务数据
|
|
||||||
*/
|
|
||||||
data class Result<T>(
|
|
||||||
val code: Int,
|
|
||||||
val msg: String,
|
|
||||||
val data: T? = null,
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
/** 业务成功码 */
|
|
||||||
const val SUCCESS_CODE = 0
|
|
||||||
|
|
||||||
/** 业务失败默认码 */
|
|
||||||
const val ERROR_CODE = 5001
|
|
||||||
|
|
||||||
/** 参数缺失 */
|
|
||||||
const val CODE_PARAM_MISSING = 1001
|
|
||||||
|
|
||||||
/** 参数格式错误 */
|
|
||||||
const val CODE_PARAM_FORMAT = 1002
|
|
||||||
|
|
||||||
/** 业务校验失败 */
|
|
||||||
const val CODE_BUSINESS = 1003
|
|
||||||
|
|
||||||
/** 存储过程调用失败 */
|
|
||||||
const val CODE_PROC_FAILED = 2001
|
|
||||||
|
|
||||||
/** 数据库连接失败 */
|
|
||||||
const val CODE_DB_UNREACHABLE = 2002
|
|
||||||
|
|
||||||
/** 医院系统返回业务失败 */
|
|
||||||
const val CODE_HOSPITAL_FAILED = 2003
|
|
||||||
|
|
||||||
/** 鉴权失败 */
|
|
||||||
const val CODE_AUTH_FAILED = 3001
|
|
||||||
|
|
||||||
/** Token 过期 */
|
|
||||||
const val CODE_TOKEN_EXPIRED = 3002
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 成功响应(无数据)。
|
|
||||||
*/
|
|
||||||
fun <T> success(): Result<T?> = Result(SUCCESS_CODE, "success", null)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 成功响应。
|
|
||||||
*/
|
|
||||||
fun <T> success(data: T): Result<T> = Result(SUCCESS_CODE, "success", data)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 错误响应(默认错误码)。
|
|
||||||
*/
|
|
||||||
fun <T> error(msg: String): Result<T> = Result(ERROR_CODE, msg, null)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 错误响应(指定错误码)。
|
|
||||||
*/
|
|
||||||
fun <T> error(code: Int, msg: String): Result<T> = Result(code, msg, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package com.hospital.front.infra.config
|
|
||||||
|
|
||||||
import org.noear.solon.annotation.BindProps
|
|
||||||
import org.noear.solon.annotation.Configuration
|
|
||||||
import org.noear.solon.annotation.Inject
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用配置(Solon @ConfigurationProperties)。
|
|
||||||
*
|
|
||||||
* 加载 `app.yml` 与 `pac/conf/app-{编码}.yml` 中的 hospital.* 配置。
|
|
||||||
*
|
|
||||||
* @author hospital-front重构
|
|
||||||
*/
|
|
||||||
@BindProps(prefix = "hospital")
|
|
||||||
@Configuration
|
|
||||||
open class AppConfig {
|
|
||||||
|
|
||||||
/** 启动激活哪家医院 */
|
|
||||||
var active: String = "jinghe"
|
|
||||||
|
|
||||||
/** 注册的医院编码清单(启动自检) */
|
|
||||||
var adapters: List<String> = emptyList()
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package com.hospital.front.infra.exception
|
|
||||||
|
|
||||||
import com.hospital.front.dto.Result
|
|
||||||
import org.noear.solon.annotation.Component
|
|
||||||
import org.noear.solon.core.handle.Context
|
|
||||||
import org.noear.solon.core.handle.Filter
|
|
||||||
import org.noear.solon.core.handle.FilterChain
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局异常处理(Solon Filter)。
|
|
||||||
*
|
|
||||||
* 将未捕获的异常统一包装为 Result 格式返回。
|
|
||||||
* 业务异常请直接抛出 BusinessException。
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
class GlobalExceptionHandler : Filter {
|
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(GlobalExceptionHandler::class.java)
|
|
||||||
|
|
||||||
override fun doFilter(ctx: Context, chain: FilterChain) {
|
|
||||||
try {
|
|
||||||
chain.doFilter(ctx)
|
|
||||||
} catch (e: BusinessException) {
|
|
||||||
log.warn("业务异常:code={}, msg={}", e.code, e.message)
|
|
||||||
renderResult(ctx, Result.error<Any>(e.code, e.message ?: "业务异常"))
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
log.warn("参数异常:{}", e.message)
|
|
||||||
renderResult(ctx, Result.error<Any>(Result.CODE_PARAM_FORMAT, e.message ?: "参数错误"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.error("系统异常", e)
|
|
||||||
renderResult(ctx, Result.error<Any>(Result.ERROR_CODE, "系统异常:" + (e.message ?: e.javaClass.simpleName)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun renderResult(ctx: Context, result: Result<*>) {
|
|
||||||
ctx.status(200)
|
|
||||||
ctx.contentType("application/json;charset=UTF-8")
|
|
||||||
// 简易 JSON 序列化(避免引入 fastjson 依赖)
|
|
||||||
val dataJson = result.data?.let { toJsonValue(it) } ?: "null"
|
|
||||||
ctx.output("""{"code":${result.code},"msg":${toJsonValue(result.msg)},"data":$dataJson}""")
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 极简 JSON 值序列化(字符串加引号,其它直接 toString) */
|
|
||||||
private fun toJsonValue(v: Any?): String = when (v) {
|
|
||||||
null -> "null"
|
|
||||||
is Number, is Boolean -> v.toString()
|
|
||||||
else -> "\"" + v.toString().replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n") + "\""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 业务异常。
|
|
||||||
*
|
|
||||||
* 用于业务层显式抛出指定错误码的异常,会被 GlobalExceptionHandler 捕获并包装为 Result。
|
|
||||||
*
|
|
||||||
* @param code 业务错误码(参见 Result 中的常量)
|
|
||||||
* @param message 错误描述
|
|
||||||
*/
|
|
||||||
class BusinessException(
|
|
||||||
val code: Int,
|
|
||||||
override val message: String?,
|
|
||||||
) : RuntimeException(message) {
|
|
||||||
companion object {
|
|
||||||
fun of(code: Int, message: String?): BusinessException = BusinessException(code, message)
|
|
||||||
fun paramMissing(field: String): BusinessException =
|
|
||||||
BusinessException(Result.CODE_PARAM_MISSING, "参数缺失:$field")
|
|
||||||
fun businessFailed(msg: String): BusinessException =
|
|
||||||
BusinessException(Result.CODE_BUSINESS, msg)
|
|
||||||
fun procFailed(msg: String): BusinessException =
|
|
||||||
BusinessException(Result.CODE_PROC_FAILED, msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package com.hospital.front.infra.util
|
|
||||||
|
|
||||||
import java.time.LocalDate
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 日期工具类(统一格式)。
|
|
||||||
*
|
|
||||||
* 业务系统约定:
|
|
||||||
- 日期:yyyy-MM-dd
|
|
||||||
- 时间:yyyy-MM-dd HH:mm:ss
|
|
||||||
- 时区:GMT+8
|
|
||||||
*
|
|
||||||
* @author hospital-front重构
|
|
||||||
*/
|
|
||||||
object DateUtils {
|
|
||||||
|
|
||||||
/** 日期格式 */
|
|
||||||
val DATE: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
|
||||||
|
|
||||||
/** 日期时间格式 */
|
|
||||||
val DATETIME: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
|
|
||||||
|
|
||||||
/** 当前日期字符串(yyyy-MM-dd) */
|
|
||||||
fun today(): String = LocalDate.now().format(DATE)
|
|
||||||
|
|
||||||
/** 当前时间字符串(yyyy-MM-dd HH:mm:ss) */
|
|
||||||
fun now(): String = LocalDateTime.now().format(DATETIME)
|
|
||||||
|
|
||||||
/** 解析日期字符串 */
|
|
||||||
fun parseDate(text: String): LocalDate = LocalDate.parse(text, DATE)
|
|
||||||
|
|
||||||
/** 解析日期时间字符串 */
|
|
||||||
fun parseDateTime(text: String): LocalDateTime = LocalDateTime.parse(text, DATETIME)
|
|
||||||
|
|
||||||
/** 格式化为日期字符串 */
|
|
||||||
fun format(date: LocalDate): String = date.format(DATE)
|
|
||||||
|
|
||||||
/** 格式化为日期时间字符串 */
|
|
||||||
fun format(dateTime: LocalDateTime): String = dateTime.format(DATETIME)
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
package com.hospital.front.mapper.bsm
|
|
||||||
|
|
||||||
import org.apache.ibatis.annotations.Mapper
|
|
||||||
import org.apache.ibatis.annotations.Param
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BSM 医院组 Mapper(Oracle 方言)。
|
|
||||||
*
|
|
||||||
* 适用医院:xinglongyuan(兴隆园)/ jinghe(泾河)/ qingyang(庆阳)
|
|
||||||
* XML 位置:classpath:mapper/bsm/OrderMapper.xml
|
|
||||||
*
|
|
||||||
* 存储过程参数与医院 HIS 系统约定,不可修改。
|
|
||||||
*/
|
|
||||||
@Mapper
|
|
||||||
interface BsmOrderMapper {
|
|
||||||
|
|
||||||
/** 健康探测(SELECT 1) */
|
|
||||||
fun ping(): Int
|
|
||||||
|
|
||||||
/** 预约提交(存储过程 P_ADD_PE_REGIST,出参 resultCode/errorMsg/peId) */
|
|
||||||
fun saveOrder(map: MutableMap<String, Any?>)
|
|
||||||
|
|
||||||
/** 取消预约(存储过程 P_DEL_PE_REGIST) */
|
|
||||||
fun cancelOrder(map: MutableMap<String, Any?>)
|
|
||||||
|
|
||||||
/** 添加/修改单位 V1(存储过程 P_ADD_UNIT) */
|
|
||||||
fun addUnit(map: MutableMap<String, Any?>)
|
|
||||||
|
|
||||||
/** 添加/修改单位 V2(存储过程 P_ADD_UNIT,8 入参版) */
|
|
||||||
fun addUnitV2(map: MutableMap<String, Any?>)
|
|
||||||
|
|
||||||
/** 按体检预约 ID(逗号分隔)查询体检状态 */
|
|
||||||
fun getPhyexamStatus(@Param("peIds") peIds: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 医院所有体检项目(组合项) */
|
|
||||||
fun getItemList(): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 医院所有体检项目及指标项 */
|
|
||||||
fun getAllItemList(): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 医院所有体检指标项 */
|
|
||||||
fun getPeItemList(): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 员工体检项目 */
|
|
||||||
fun getEmpItem(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按指标项 CODE 查询体检组合 */
|
|
||||||
fun getItemInfo(@Param("peItemCode") peItemCode: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 员工体检结果 */
|
|
||||||
fun getResult(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 体检结论(列表) */
|
|
||||||
fun getConclusion(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 体检结论(全量) */
|
|
||||||
fun getConclusionAll(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 体检建议(列表) */
|
|
||||||
fun getSuggest(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 体检建议(全量) */
|
|
||||||
fun getSuggestAll(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按身份证号及年份查询体检状态 */
|
|
||||||
fun getPeStatusByIdnoAndYear(@Param("idNo") idNo: String, @Param("year") year: String): Map<String, Any?>?
|
|
||||||
|
|
||||||
/** 按身份证号及年份查询体检结果 */
|
|
||||||
fun getResultByIdcardAndYear(@Param("idNo") idNo: String, @Param("year") year: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按身份证号及年份查询体检结果(全量) */
|
|
||||||
fun getResultByIdcardAndYearAll(@Param("idNo") idNo: String, @Param("year") year: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按体检编号查询体检结果 */
|
|
||||||
fun getResultByPeId(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按审核日期查询体检报告(去重) */
|
|
||||||
fun getDisResultByAuditDate(@Param("auditDate") auditDate: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按体检编号查询体检项目子表 */
|
|
||||||
fun getItemByPeId(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按体检编号查询体检项目子表(全量) */
|
|
||||||
fun getItemByPeIdAll(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按身份证号或体检编号 + 审核年份查询体检结果 */
|
|
||||||
fun getResultByIdNoOrPeId(
|
|
||||||
@Param("idNo") idNo: String?,
|
|
||||||
@Param("peId") peId: String?,
|
|
||||||
@Param("auditYear") auditYear: String,
|
|
||||||
): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按身份证号查询当年 peId 与预约日期 */
|
|
||||||
fun getPeIdAndDateByIdcard(@Param("idNo") idNo: String): Map<String, Any?>?
|
|
||||||
|
|
||||||
/** 按身份证号查询当年 peId 与预约日期(数组) */
|
|
||||||
fun getPeIdAndDateByIdcardList(@Param("idNo") idNo: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按审核日期查询当天体检人员列表(去重) */
|
|
||||||
fun getExamListByDate(@Param("auditDate") auditDate: String?): List<Map<String, Any?>>
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
package com.hospital.front.mapper.nx
|
|
||||||
|
|
||||||
import org.apache.ibatis.annotations.Mapper
|
|
||||||
import org.apache.ibatis.annotations.Param
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BSM 医院组 Mapper(Oracle 方言)。
|
|
||||||
*
|
|
||||||
* 适用医院:ningxia(宁夏宝石花,SqlServer)
|
|
||||||
* XML 位置:classpath:mapper/nx/OrderMapper.xml
|
|
||||||
*
|
|
||||||
* 存储过程参数与医院 HIS 系统约定,不可修改。
|
|
||||||
*/
|
|
||||||
@Mapper
|
|
||||||
interface NxOrderMapper {
|
|
||||||
|
|
||||||
/** 健康探测(SELECT 1) */
|
|
||||||
fun ping(): Int
|
|
||||||
|
|
||||||
/** 预约提交(存储过程 P_ADD_PE_REGIST,出参 resultCode/errorMsg/peId) */
|
|
||||||
fun saveOrder(map: MutableMap<String, Any?>)
|
|
||||||
|
|
||||||
/** 取消预约(存储过程 P_DEL_PE_REGIST) */
|
|
||||||
fun cancelOrder(map: MutableMap<String, Any?>)
|
|
||||||
|
|
||||||
/** 添加/修改单位 V1(存储过程 P_ADD_UNIT) */
|
|
||||||
fun addUnit(map: MutableMap<String, Any?>)
|
|
||||||
|
|
||||||
/** 添加/修改单位 V2(存储过程 P_ADD_UNIT,8 入参版) */
|
|
||||||
fun addUnitV2(map: MutableMap<String, Any?>)
|
|
||||||
|
|
||||||
/** 按体检预约 ID(逗号分隔)查询体检状态 */
|
|
||||||
fun getPhyexamStatus(@Param("peIds") peIds: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 医院所有体检项目(组合项) */
|
|
||||||
fun getItemList(): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 医院所有体检项目及指标项 */
|
|
||||||
fun getAllItemList(): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 医院所有体检指标项 */
|
|
||||||
fun getPeItemList(): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 员工体检项目 */
|
|
||||||
fun getEmpItem(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按指标项 CODE 查询体检组合 */
|
|
||||||
fun getItemInfo(@Param("peItemCode") peItemCode: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 员工体检结果 */
|
|
||||||
fun getResult(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 体检结论(列表) */
|
|
||||||
fun getConclusion(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 体检结论(全量) */
|
|
||||||
fun getConclusionAll(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 体检建议(列表) */
|
|
||||||
fun getSuggest(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 体检建议(全量) */
|
|
||||||
fun getSuggestAll(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按身份证号及年份查询体检状态 */
|
|
||||||
fun getPeStatusByIdnoAndYear(@Param("idNo") idNo: String, @Param("year") year: String): Map<String, Any?>?
|
|
||||||
|
|
||||||
/** 按身份证号及年份查询体检结果 */
|
|
||||||
fun getResultByIdcardAndYear(@Param("idNo") idNo: String, @Param("year") year: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按身份证号及年份查询体检结果(全量) */
|
|
||||||
fun getResultByIdcardAndYearAll(@Param("idNo") idNo: String, @Param("year") year: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按体检编号查询体检结果 */
|
|
||||||
fun getResultByPeId(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按审核日期查询体检报告(去重) */
|
|
||||||
fun getDisResultByAuditDate(@Param("auditDate") auditDate: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按体检编号查询体检项目子表 */
|
|
||||||
fun getItemByPeId(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按体检编号查询体检项目子表(全量) */
|
|
||||||
fun getItemByPeIdAll(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按身份证号或体检编号 + 审核年份查询体检结果 */
|
|
||||||
fun getResultByIdNoOrPeId(
|
|
||||||
@Param("idNo") idNo: String?,
|
|
||||||
@Param("peId") peId: String?,
|
|
||||||
@Param("auditYear") auditYear: String,
|
|
||||||
): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按身份证号查询当年 peId 与预约日期 */
|
|
||||||
fun getPeIdAndDateByIdcard(@Param("idNo") idNo: String): Map<String, Any?>?
|
|
||||||
|
|
||||||
/** 按身份证号查询当年 peId 与预约日期(数组) */
|
|
||||||
fun getPeIdAndDateByIdcardList(@Param("idNo") idNo: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按审核日期查询当天体检人员列表(去重) */
|
|
||||||
fun getExamListByDate(@Param("auditDate") auditDate: String?): List<Map<String, Any?>>
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
package com.hospital.front.mapper.ya
|
|
||||||
|
|
||||||
import org.apache.ibatis.annotations.Mapper
|
|
||||||
import org.apache.ibatis.annotations.Param
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BSM 医院组 Mapper(Oracle 方言)。
|
|
||||||
*
|
|
||||||
* 适用医院:yanan(延安,SqlServer)
|
|
||||||
* XML 位置:classpath:mapper/ya/OrderMapper.xml
|
|
||||||
*
|
|
||||||
* 存储过程参数与医院 HIS 系统约定,不可修改。
|
|
||||||
*/
|
|
||||||
@Mapper
|
|
||||||
interface YaOrderMapper {
|
|
||||||
|
|
||||||
/** 健康探测(SELECT 1) */
|
|
||||||
fun ping(): Int
|
|
||||||
|
|
||||||
/** 预约提交(存储过程 P_ADD_PE_REGIST,出参 resultCode/errorMsg/peId) */
|
|
||||||
fun saveOrder(map: MutableMap<String, Any?>)
|
|
||||||
|
|
||||||
/** 取消预约(存储过程 P_DEL_PE_REGIST) */
|
|
||||||
fun cancelOrder(map: MutableMap<String, Any?>)
|
|
||||||
|
|
||||||
/** 添加/修改单位 V1(存储过程 P_ADD_UNIT) */
|
|
||||||
fun addUnit(map: MutableMap<String, Any?>)
|
|
||||||
|
|
||||||
/** 添加/修改单位 V2(存储过程 P_ADD_UNIT,8 入参版) */
|
|
||||||
fun addUnitV2(map: MutableMap<String, Any?>)
|
|
||||||
|
|
||||||
/** 按体检预约 ID(逗号分隔)查询体检状态 */
|
|
||||||
fun getPhyexamStatus(@Param("peIds") peIds: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 医院所有体检项目(组合项) */
|
|
||||||
fun getItemList(): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 医院所有体检项目及指标项 */
|
|
||||||
fun getAllItemList(): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 医院所有体检指标项 */
|
|
||||||
fun getPeItemList(): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 员工体检项目 */
|
|
||||||
fun getEmpItem(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按指标项 CODE 查询体检组合 */
|
|
||||||
fun getItemInfo(@Param("peItemCode") peItemCode: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 员工体检结果 */
|
|
||||||
fun getResult(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 体检结论(列表) */
|
|
||||||
fun getConclusion(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 体检结论(全量) */
|
|
||||||
fun getConclusionAll(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 体检建议(列表) */
|
|
||||||
fun getSuggest(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 体检建议(全量) */
|
|
||||||
fun getSuggestAll(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按身份证号及年份查询体检状态 */
|
|
||||||
fun getPeStatusByIdnoAndYear(@Param("idNo") idNo: String, @Param("year") year: String): Map<String, Any?>?
|
|
||||||
|
|
||||||
/** 按身份证号及年份查询体检结果 */
|
|
||||||
fun getResultByIdcardAndYear(@Param("idNo") idNo: String, @Param("year") year: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按身份证号及年份查询体检结果(全量) */
|
|
||||||
fun getResultByIdcardAndYearAll(@Param("idNo") idNo: String, @Param("year") year: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按体检编号查询体检结果 */
|
|
||||||
fun getResultByPeId(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按审核日期查询体检报告(去重) */
|
|
||||||
fun getDisResultByAuditDate(@Param("auditDate") auditDate: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按体检编号查询体检项目子表 */
|
|
||||||
fun getItemByPeId(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按体检编号查询体检项目子表(全量) */
|
|
||||||
fun getItemByPeIdAll(@Param("peId") peId: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按身份证号或体检编号 + 审核年份查询体检结果 */
|
|
||||||
fun getResultByIdNoOrPeId(
|
|
||||||
@Param("idNo") idNo: String?,
|
|
||||||
@Param("peId") peId: String?,
|
|
||||||
@Param("auditYear") auditYear: String,
|
|
||||||
): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按身份证号查询当年 peId 与预约日期 */
|
|
||||||
fun getPeIdAndDateByIdcard(@Param("idNo") idNo: String): Map<String, Any?>?
|
|
||||||
|
|
||||||
/** 按身份证号查询当年 peId 与预约日期(数组) */
|
|
||||||
fun getPeIdAndDateByIdcardList(@Param("idNo") idNo: String): List<Map<String, Any?>>
|
|
||||||
|
|
||||||
/** 按审核日期查询当天体检人员列表(去重) */
|
|
||||||
fun getExamListByDate(@Param("auditDate") auditDate: String?): List<Map<String, Any?>>
|
|
||||||
}
|
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
package com.hospital.front.service
|
|
||||||
|
|
||||||
import com.hospital.front.adapter.HospitalAdapter
|
|
||||||
import com.hospital.front.adapter.HospitalRouter
|
|
||||||
import com.hospital.front.infra.exception.BusinessException
|
|
||||||
import com.hospital.front.infra.util.DateUtils
|
|
||||||
import com.hospital.front.vo.OrderItemVo
|
|
||||||
import com.hospital.front.vo.OrderV2Vo
|
|
||||||
import org.noear.solon.Solon
|
|
||||||
import org.noear.solon.annotation.Component
|
|
||||||
import org.noear.solon.annotation.Inject
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 体检预约业务服务。
|
|
||||||
*
|
|
||||||
* 通过 HospitalRouter 委托给当前激活医院的适配器,
|
|
||||||
* 并承载跨医院共有的组装/汇总逻辑(替代原 OrderServiceImpl 的重复代码)。
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
class OrderService {
|
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(OrderService::class.java)
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
private lateinit var router: HospitalRouter
|
|
||||||
|
|
||||||
// ================= 配置开关(惰性读取,支持热更新) =================
|
|
||||||
|
|
||||||
/** VO 版本(V1 / V2),默认 V2 */
|
|
||||||
private val hmVer: String get() = Solon.cfg().get("hm.ver", "V2")!!
|
|
||||||
|
|
||||||
/** 宁夏:强制发送二级单位编码和名称 */
|
|
||||||
private val allsecondFlag: Boolean get() = Solon.cfg().get("hm.allsecondFlag", "false") == "true"
|
|
||||||
|
|
||||||
/** 健康体检不发送职业相关信息 */
|
|
||||||
private val occFlag: Boolean get() = Solon.cfg().get("hm.occFlag", "false") == "true"
|
|
||||||
|
|
||||||
/** 当前激活医院的适配器 */
|
|
||||||
private fun adapter(): HospitalAdapter = router.active()
|
|
||||||
|
|
||||||
// ================= 体检状态字典 =================
|
|
||||||
|
|
||||||
/** 体检状态名 → 字典数字 */
|
|
||||||
private val peStatusDict = mapOf("预约" to "1", "报到" to "2", "检中" to "3", "初审" to "4", "终审" to "5")
|
|
||||||
|
|
||||||
// ================= 写操作 =================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提交预约:入参 Map(V1/V2 VO 结构)→ 拼接管道符字符串 → 调医院存储过程。
|
|
||||||
*
|
|
||||||
* @return 成功时返回 peId
|
|
||||||
*/
|
|
||||||
fun saveOrder(body: Map<String, Any?>): String {
|
|
||||||
var vo = convertOrderV2(body)
|
|
||||||
if (allsecondFlag) vo = vo.applyAllsecondFlag()
|
|
||||||
if (occFlag) vo = vo.applyOccFlag()
|
|
||||||
|
|
||||||
val peInfoStr = vo.toPeInfoStr()
|
|
||||||
val peItemStr = OrderItemVo.toPeItemStr(vo.itemList)
|
|
||||||
log.info("提交预约:peInfoStr={}, peItemStr={}", peInfoStr, peItemStr)
|
|
||||||
|
|
||||||
val result = adapter().saveOrder(peInfoStr, peItemStr)
|
|
||||||
return if (result.isSuccess) {
|
|
||||||
result.peId ?: throw BusinessException.procFailed("医院未返回体检编号")
|
|
||||||
} else {
|
|
||||||
throwHospitalFailed(result.errorMsg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 取消预约 */
|
|
||||||
fun cancelOrder(peId: String, name: String, idcard: String) {
|
|
||||||
log.info("取消预约:peId={}, name={}", peId, name)
|
|
||||||
val result = adapter().cancelOrder(peId, name, idcard)
|
|
||||||
if (!result.isSuccess) throwHospitalFailed(result.errorMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 批量添加/修改单位;返回失败信息(全部成功返回 null) */
|
|
||||||
fun addUnits(units: List<Map<String, Any?>>): String? {
|
|
||||||
val errors = StringBuilder()
|
|
||||||
val useV2 = hmVer == "V2"
|
|
||||||
for ((index, raw) in units.withIndex()) {
|
|
||||||
try {
|
|
||||||
val unit = convertUnit(raw)
|
|
||||||
val result = adapter().addUnit(unit, useV2)
|
|
||||||
if (!result.isSuccess) errors.append(result.errorMsg ?: "第${index + 1}条单位同步失败;")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.warn("addUnit 第{}条处理异常:{}", index + 1, e.message)
|
|
||||||
errors.append("第${index + 1}条异常:${e.message};")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errors.toString().ifEmpty { null }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= 查询操作(直通适配器) =================
|
|
||||||
|
|
||||||
fun getEmpItem(peId: String) = adapter().getEmpItem(peId)
|
|
||||||
fun getItemList() = adapter().getItemList()
|
|
||||||
fun getAllItemList() = adapter().getAllItemList()
|
|
||||||
fun getPeItemList() = adapter().getPeItemList()
|
|
||||||
fun getItemInfo(peItemCode: String) = adapter().getItemInfo(peItemCode)
|
|
||||||
fun getResult(peId: String) = adapter().getResult(peId)
|
|
||||||
fun getConclusion(peId: String) = adapter().getConclusion(peId)
|
|
||||||
fun getSuggest(peId: String) = adapter().getSuggest(peId)
|
|
||||||
fun getResultByIdcardAndYear(idNo: String, year: String) = adapter().getResultByIdcardAndYear(idNo, year)
|
|
||||||
fun getResultByIdcardAndYearAll(idNo: String, year: String) = adapter().getResultByIdcardAndYearAll(idNo, year)
|
|
||||||
fun getResultByPeId(peId: String) = adapter().getResultByPeId(peId)
|
|
||||||
|
|
||||||
/** 按审核日期查询体检报告(去重主表) */
|
|
||||||
fun getResultByAuditDate(auditDate: String) = adapter().getDisResultByAuditDate(auditDate)
|
|
||||||
fun getResultByIdNoOrPeId(idNo: String?, peId: String?, auditYear: String) =
|
|
||||||
adapter().getResultByIdNoOrPeId(idNo, peId, auditYear)
|
|
||||||
fun getPeIdAndDateByIdcard(idNo: String) = adapter().getPeIdAndDateByIdcard(idNo)
|
|
||||||
fun getPeIdAndDateByIdcardList(idNo: String) = adapter().getPeIdAndDateByIdcardList(idNo)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 体检状态查询:按医院返回的 PE_STATUS 名转字典数字。
|
|
||||||
*/
|
|
||||||
fun getPhyexamStatus(peIds: String): List<Map<String, String>> =
|
|
||||||
adapter().getPhyexamStatus(peIds).map { row ->
|
|
||||||
mapOf(
|
|
||||||
"peId" to (row["PE_ID"]?.toString() ?: ""),
|
|
||||||
"peStatus" to (peStatusDict[row["PE_STATUS"]?.toString() ?: ""] ?: ""),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 按日期查询当天体检人员列表(日期空则当天)。
|
|
||||||
*/
|
|
||||||
fun getExamListByDate(auditDate: String?) =
|
|
||||||
adapter().getExamListByDate(auditDate ?: DateUtils.today())
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 体检结论(汇总为单字符串)。
|
|
||||||
*/
|
|
||||||
fun getTotalConclusion(peId: String): Map<String, String> =
|
|
||||||
mapOf("totalConclusion" to buildSummary(adapter().getConclusion(peId)))
|
|
||||||
|
|
||||||
/** 体检结论(汇总,全量来源) */
|
|
||||||
fun getTotalConclusionAll(peId: String): Map<String, String> =
|
|
||||||
mapOf("totalConclusion" to buildSummary(adapter().getConclusionAll(peId)))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 体检建议(汇总)。宁夏(nx)仅取首条 GUIDE_CONTENT。
|
|
||||||
*/
|
|
||||||
fun getTotalSuggest(peId: String): Map<String, String> =
|
|
||||||
mapOf("totalSuggest" to buildSuggest(adapter().getSuggest(peId), adapter().hmType() == "nx"))
|
|
||||||
|
|
||||||
/** 体检建议(汇总,全量来源) */
|
|
||||||
fun getTotalSuggestAll(peId: String): Map<String, String> =
|
|
||||||
mapOf("totalSuggest" to buildSuggest(adapter().getSuggestAll(peId), adapter().hmType() == "nx"))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 按身份证号 + 年份查询体检状态及项目(含宁夏体检报告兜底)。
|
|
||||||
*/
|
|
||||||
fun getPeStatusByIdnoAndYear(idNo: String, year: String): Map<String, Any?>? {
|
|
||||||
val map = adapter().getPeStatusByIdnoAndYear(idNo, year)
|
|
||||||
return if (map != null && map["PE_ID"] != null) {
|
|
||||||
val result = map.toMutableMap()
|
|
||||||
result["peItemList"] = adapter().getEmpItem(map["PE_ID"].toString())
|
|
||||||
result
|
|
||||||
} else {
|
|
||||||
// 宁夏:从体检报告继续查
|
|
||||||
if (adapter().hmType() == "nx") {
|
|
||||||
val rows = adapter().getResultByIdcardAndYear(idNo, year)
|
|
||||||
if (rows.isNotEmpty()) {
|
|
||||||
val row = rows.first()
|
|
||||||
val auditDate = row["AUDIT_DATE"]?.toString()
|
|
||||||
buildMap {
|
|
||||||
put("PE_ID", row["PE_ID"])
|
|
||||||
put("PE_PRE_DATE", auditDate?.takeIf { it.length >= 10 }?.substring(0, 10) ?: auditDate)
|
|
||||||
put("peItemList", adapter().getEmpItem(row["PE_ID"].toString()))
|
|
||||||
}
|
|
||||||
} else null
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= 私有工具 =================
|
|
||||||
|
|
||||||
/** 医院侧业务失败:抛出 2003 业务异常 */
|
|
||||||
private fun throwHospitalFailed(errorMsg: String?): Nothing =
|
|
||||||
if (!errorMsg.isNullOrEmpty()) throw BusinessException.of(2003, errorMsg)
|
|
||||||
else throw BusinessException.procFailed("医院系统返回业务失败")
|
|
||||||
|
|
||||||
/** 结论汇总:1.科室:结论;<br/> */
|
|
||||||
private fun buildSummary(rows: List<Map<String, Any?>>): String =
|
|
||||||
rows.mapIndexed { i, row ->
|
|
||||||
val dept = row["PE_DEPT_NAME"]?.toString()?.takeIf { it.isNotEmpty() }?.let { "$it:" } ?: ""
|
|
||||||
"${i + 1}.$dept${row["CONCLUSION_TEXT"] ?: ""};<br/>"
|
|
||||||
}.joinToString("")
|
|
||||||
|
|
||||||
/** 建议汇总:宁夏仅取首条 GUIDE_CONTENT;其余 1.建议项:内容;<br/> */
|
|
||||||
private fun buildSuggest(rows: List<Map<String, Any?>>, nxStyle: Boolean): String =
|
|
||||||
if (nxStyle) {
|
|
||||||
rows.firstOrNull()?.get("GUIDE_CONTENT")?.toString() ?: ""
|
|
||||||
} else {
|
|
||||||
rows.filter { !it["GUIDE_CONTENT"].toString().isNullOrEmpty() }
|
|
||||||
.mapIndexed { i, row -> "${i + 1}.${row["REC_CONTENT"] ?: ""}:${row["GUIDE_CONTENT"] ?: ""};<br/>" }
|
|
||||||
.joinToString("")
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Map → OrderV2Vo(数值/空值容错) */
|
|
||||||
private fun convertOrderV2(body: Map<String, Any?>): OrderV2Vo = OrderV2Vo(
|
|
||||||
orderDate = body["orderDate"]?.toString(),
|
|
||||||
secondDepartId = body["secondDepartId"]?.toString(),
|
|
||||||
secondDepartName = body["secondDepartName"]?.toString(),
|
|
||||||
thirdDepartName = body["thirdDepartName"]?.toString(),
|
|
||||||
name = body["name"]?.toString(),
|
|
||||||
sexName = body["sexName"]?.toString(),
|
|
||||||
birthDay = body["birthDay"]?.toString(),
|
|
||||||
age = (body["age"] as? Number)?.toInt(),
|
|
||||||
idCard = body["idCard"]?.toString(),
|
|
||||||
marrigeStatusName = body["marrigeStatusName"]?.toString(),
|
|
||||||
country = body["country"]?.toString(),
|
|
||||||
nationName = body["nationName"]?.toString(),
|
|
||||||
birthplace = body["birthplace"]?.toString(),
|
|
||||||
position = body["position"]?.toString(),
|
|
||||||
profession = body["profession"]?.toString(),
|
|
||||||
feeType = body["feeType"]?.toString(),
|
|
||||||
liveSpace = body["liveSpace"]?.toString(),
|
|
||||||
zipCode = body["zipCode"]?.toString(),
|
|
||||||
mobile = body["mobile"]?.toString(),
|
|
||||||
email = body["email"]?.toString(),
|
|
||||||
empSysno = body["empSysno"]?.toString(),
|
|
||||||
medicalType = body["medicalType"]?.toString(),
|
|
||||||
medicalClass = body["medicalClass"]?.toString(),
|
|
||||||
workShape = body["workShape"]?.toString(),
|
|
||||||
workDate = body["workDate"]?.toString(),
|
|
||||||
jobLevel = body["jobLevel"]?.toString(),
|
|
||||||
workPlace = body["workPlace"]?.toString(),
|
|
||||||
baseSite = body["baseSite"]?.toString(),
|
|
||||||
workTypeCode = body["workTypeCode"]?.toString(),
|
|
||||||
workTypeOther = body["workTypeOther"]?.toString(),
|
|
||||||
education = body["education"]?.toString(),
|
|
||||||
title = body["title"]?.toString(),
|
|
||||||
oldDepartName = body["oldDepartName"]?.toString(),
|
|
||||||
harmReasonCode = body["harmReasonCode"]?.toString(),
|
|
||||||
harmReasonOther = body["harmReasonOther"]?.toString(),
|
|
||||||
harmDate = body["harmDate"]?.toString(),
|
|
||||||
defendCancer = body["defendCancer"]?.toString(),
|
|
||||||
itemList = (body["itemList"] as? List<*>)?.mapNotNull { item ->
|
|
||||||
(item as? Map<*, *>)?.let {
|
|
||||||
OrderItemVo(it["itemNo"]?.toString(), it["itemName"]?.toString())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Map → UnitVo */
|
|
||||||
private fun convertUnit(raw: Map<String, Any?>) = com.hospital.front.vo.UnitVo(
|
|
||||||
unitCode = raw["unitCode"]?.toString(),
|
|
||||||
unitName = raw["unitName"]?.toString(),
|
|
||||||
parentUnitCode = raw["parentUnitCode"]?.toString(),
|
|
||||||
address = raw["address"]?.toString(),
|
|
||||||
connecter1 = raw["connecter1"]?.toString(),
|
|
||||||
phone1 = raw["phone1"]?.toString(),
|
|
||||||
connecter2 = raw["connecter2"]?.toString(),
|
|
||||||
phone2 = raw["phone2"]?.toString(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package com.hospital.front.vo
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 体检结果传输 VO(Mapper XML resultType)。
|
|
||||||
*
|
|
||||||
* 字段名与原 hospitalmiddle 的 MedicalMiddleResultVo 保持一致(XML 映射契约)。
|
|
||||||
* 数据库日期列已由 SQL 转为字符串(to_char / CONVERT)。
|
|
||||||
*/
|
|
||||||
data class MedicalMiddleResultVo(
|
|
||||||
var idNo: String? = null,
|
|
||||||
var peResult: String? = null,
|
|
||||||
var hospitalId: String? = null,
|
|
||||||
var unitCode: String? = null,
|
|
||||||
var peQueueDate: String? = null,
|
|
||||||
var printContext: String? = null,
|
|
||||||
var peItemName: String? = null,
|
|
||||||
var peId: String? = null,
|
|
||||||
var name: String? = null,
|
|
||||||
var itemAssemName: String? = null,
|
|
||||||
var peItemCode: String? = null,
|
|
||||||
var auditDate: String? = null,
|
|
||||||
var itemAssemCode: String? = null,
|
|
||||||
var unitName: String? = null,
|
|
||||||
var peVisitId: String? = null,
|
|
||||||
var unit: String? = null,
|
|
||||||
var conclusion: String? = null,
|
|
||||||
var suggest: String? = null,
|
|
||||||
/** 子表:体检项目结果列表(汇总接口组装用) */
|
|
||||||
var resultVoList: List<MedicalMiddleResultVo>? = null,
|
|
||||||
)
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
package com.hospital.front.vo
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 预约订单体检项目条目。
|
|
||||||
*
|
|
||||||
* JSON 字段名与原 hospitalmiddle 的 OrderItemVo 保持一致(调用方契约,不可修改)。
|
|
||||||
*/
|
|
||||||
data class OrderItemVo(
|
|
||||||
val itemNo: String? = null,
|
|
||||||
val itemName: String? = null,
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* 拼接子表字符串:多项之间用 & 分隔,项内 编号|名称 分隔。
|
|
||||||
*/
|
|
||||||
fun toPeItemStr(items: List<OrderItemVo>?): String =
|
|
||||||
items?.joinToString("&") { "${it.itemNo ?: ""}|${it.itemName ?: ""}" } ?: ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* V1 版预约订单 VO(hm.ver=V1 时使用)。
|
|
||||||
*
|
|
||||||
* 字段名与字段顺序均与原 hospitalmiddle 的 OrderVo(Java)保持一致,
|
|
||||||
* 字段顺序即存储过程 P_ADD_PE_REGIST 的 peInfoStr 竖线分隔顺序,不可调整。
|
|
||||||
*/
|
|
||||||
data class OrderVo(
|
|
||||||
val orderDate: String? = null, // 1 预约日期 yyyy-MM-dd
|
|
||||||
val secondDepartId: String? = null, // 2 所属单位编码
|
|
||||||
val secondDepartName: String? = null, // 3 所属单位名称
|
|
||||||
val thirdDepartName: String? = null, // 4 所属部门
|
|
||||||
val name: String? = null, // 5 姓名
|
|
||||||
val sexName: String? = null, // 6 性别(男/女)
|
|
||||||
val birthDay: String? = null, // 7 出生日期 yyyy-MM-dd
|
|
||||||
val age: Int? = null, // 8 年龄
|
|
||||||
val idCard: String? = null, // 9 身份证号
|
|
||||||
val marrigeStatusName: String? = null, // 10 婚姻
|
|
||||||
val country: String? = null, // 11 国家
|
|
||||||
val nationName: String? = null, // 12 民族
|
|
||||||
val birthplace: String? = null, // 13 出生地
|
|
||||||
val position: String? = null, // 14 身份
|
|
||||||
val profession: String? = null, // 15 职业
|
|
||||||
val feeType: String? = null, // 16 费别
|
|
||||||
val liveSpace: String? = null, // 17 家庭住址
|
|
||||||
val zipCode: String? = null, // 18 邮编
|
|
||||||
val mobile: String? = null, // 19 移动电话
|
|
||||||
val email: String? = null, // 20 邮箱
|
|
||||||
val empSysno: String? = null, // 21 员工编号
|
|
||||||
val medicalType: String? = null, // 22 体检类型(职业/健康)
|
|
||||||
val medicalClass: String? = null, // 23 体检类别(岗前/在岗期间)
|
|
||||||
val workShape: String? = null, // 24 用工形式(合同/劳务)
|
|
||||||
val workDate: String? = null, // 25 入职时间 yyyy-MM-dd
|
|
||||||
val jobLevel: String? = null, // 26 职务级别
|
|
||||||
val workPlace: String? = null, // 27 工作地
|
|
||||||
val baseSite: String? = null, // 28 基层站
|
|
||||||
val workType: String? = null, // 29 职业工种(V1 单字段)
|
|
||||||
val education: String? = null, // 30 学历
|
|
||||||
val title: String? = null, // 31 职称
|
|
||||||
val oldDepartName: String? = null, // 32 原单位
|
|
||||||
val harmReason: String? = null, // 33 危害因素(V1 单字段)
|
|
||||||
val defendCancer: String? = null, // 34 防癌项
|
|
||||||
val itemList: List<OrderItemVo>? = null,
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* 按存储过程约定顺序拼接主表字符串(竖线分隔,空值为空串)。
|
|
||||||
*/
|
|
||||||
fun toPeInfoStr(): String = listOf(
|
|
||||||
orderDate, secondDepartId, secondDepartName, thirdDepartName, name,
|
|
||||||
sexName, birthDay, age, idCard, marrigeStatusName,
|
|
||||||
country, nationName, birthplace, position, profession,
|
|
||||||
feeType, liveSpace, zipCode, mobile, email,
|
|
||||||
empSysno, medicalType, medicalClass, workShape, workDate,
|
|
||||||
jobLevel, workPlace, baseSite, workType, education,
|
|
||||||
title, oldDepartName, harmReason, defendCancer,
|
|
||||||
).joinToString("|") { it?.toString() ?: "" }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* V2 版预约订单 VO(hm.ver=V2 时使用)。
|
|
||||||
*
|
|
||||||
* 与 V1 的差异:工种拆为 编码+其它;危害因素拆为 编码+其它+接害时间。
|
|
||||||
*/
|
|
||||||
data class OrderV2Vo(
|
|
||||||
val orderDate: String? = null, // 1 预约日期 yyyy-MM-dd
|
|
||||||
val secondDepartId: String? = null, // 2 所属单位编码
|
|
||||||
val secondDepartName: String? = null, // 3 所属单位名称
|
|
||||||
val thirdDepartName: String? = null, // 4 所属部门
|
|
||||||
val name: String? = null, // 5 姓名
|
|
||||||
val sexName: String? = null, // 6 性别(男/女)
|
|
||||||
val birthDay: String? = null, // 7 出生日期 yyyy-MM-dd
|
|
||||||
val age: Int? = null, // 8 年龄
|
|
||||||
val idCard: String? = null, // 9 身份证号
|
|
||||||
val marrigeStatusName: String? = null, // 10 婚姻
|
|
||||||
val country: String? = null, // 11 国家
|
|
||||||
val nationName: String? = null, // 12 民族
|
|
||||||
val birthplace: String? = null, // 13 出生地
|
|
||||||
val position: String? = null, // 14 身份
|
|
||||||
val profession: String? = null, // 15 职业
|
|
||||||
val feeType: String? = null, // 16 费别
|
|
||||||
val liveSpace: String? = null, // 17 家庭住址
|
|
||||||
val zipCode: String? = null, // 18 邮编
|
|
||||||
val mobile: String? = null, // 19 移动电话
|
|
||||||
val email: String? = null, // 20 邮箱
|
|
||||||
val empSysno: String? = null, // 21 员工编号
|
|
||||||
val medicalType: String? = null, // 22 体检类型(职业/健康)
|
|
||||||
val medicalClass: String? = null, // 23 体检类别(岗前/在岗期间)
|
|
||||||
val workShape: String? = null, // 24 用工形式(合同/劳务)
|
|
||||||
val workDate: String? = null, // 25 入职时间 yyyy-MM-dd
|
|
||||||
val jobLevel: String? = null, // 26 职务级别
|
|
||||||
val workPlace: String? = null, // 27 工作地
|
|
||||||
val baseSite: String? = null, // 28 基层站
|
|
||||||
val workTypeCode: String? = null, // 29 职业工种编码(V2)
|
|
||||||
val workTypeOther: String? = null, // 29b 其它工种(V2)
|
|
||||||
val education: String? = null, // 30 学历
|
|
||||||
val title: String? = null, // 31 职称
|
|
||||||
val oldDepartName: String? = null, // 32 原单位
|
|
||||||
val harmReasonCode: String? = null, // 33 危害因素编码(V2)
|
|
||||||
val harmReasonOther: String? = null, // 33b 其它危害因素(V2)
|
|
||||||
val harmDate: String? = null, // 33c 接害时间 yyyy-MM-dd(V2)
|
|
||||||
val defendCancer: String? = null, // 34 防癌项
|
|
||||||
val itemList: List<OrderItemVo>? = null,
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* 按存储过程约定顺序拼接主表字符串(竖线分隔,空值为空串)。
|
|
||||||
*/
|
|
||||||
fun toPeInfoStr(): String = listOf(
|
|
||||||
orderDate, secondDepartId, secondDepartName, thirdDepartName, name,
|
|
||||||
sexName, birthDay, age, idCard, marrigeStatusName,
|
|
||||||
country, nationName, birthplace, position, profession,
|
|
||||||
feeType, liveSpace, zipCode, mobile, email,
|
|
||||||
empSysno, medicalType, medicalClass, workShape, workDate,
|
|
||||||
jobLevel, workPlace, baseSite, workTypeCode, workTypeOther,
|
|
||||||
education, title, oldDepartName, harmReasonCode, harmReasonOther,
|
|
||||||
harmDate, defendCancer,
|
|
||||||
).joinToString("|") { it?.toString() ?: "" }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* allsecondFlag 特殊处理(宁夏):二级单位编码 9 位截前 6 位,名称截"-"之前。
|
|
||||||
*/
|
|
||||||
fun applyAllsecondFlag(): OrderV2Vo {
|
|
||||||
val newId = secondDepartId?.takeIf { it.length == 9 }?.substring(0, 6) ?: secondDepartId
|
|
||||||
val newName = secondDepartName?.takeIf { it.contains("-") }?.substringBefore("-") ?: secondDepartName
|
|
||||||
return copy(secondDepartId = newId, secondDepartName = newName)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* occFlag 特殊处理:健康体检不发送职业相关信息(危害因素、工种、接害时间)。
|
|
||||||
*/
|
|
||||||
fun applyOccFlag(): OrderV2Vo =
|
|
||||||
if (medicalType == "职业") this
|
|
||||||
else copy(
|
|
||||||
harmDate = null, harmReasonCode = null, harmReasonOther = null,
|
|
||||||
workTypeCode = null, workTypeOther = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 单位信息 VO(addUnit 接口入参,字段名与原 UnitVo 一致)。
|
|
||||||
*/
|
|
||||||
data class UnitVo(
|
|
||||||
val unitCode: String? = null, // 单位编码
|
|
||||||
val unitName: String? = null, // 单位名称
|
|
||||||
val parentUnitCode: String? = null, // 上级体检单位编号(V2)
|
|
||||||
val address: String? = null, // 单位地址(V2)
|
|
||||||
val connecter1: String? = null, // 联系人1(V2)
|
|
||||||
val phone1: String? = null, // 电话1(V2)
|
|
||||||
val connecter2: String? = null, // 联系人2(V2)
|
|
||||||
val phone2: String? = null, // 电话2(V2)
|
|
||||||
)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# ====================================
|
|
||||||
# 开发环境配置(H2 内存数据库 Mock)
|
|
||||||
# 启动:java -Dsolon.env=dev -Dsolon.config.load=app.yml,app-dev.yml -jar hospital-front-1.0.0.jar
|
|
||||||
# ====================================
|
|
||||||
# 本机无法直连医院数据库时使用;H2 以 Oracle 兼容模式运行
|
|
||||||
|
|
||||||
hospital:
|
|
||||||
active: dev # 启动激活"开发医院"
|
|
||||||
|
|
||||||
hm:
|
|
||||||
type: bsm
|
|
||||||
ver: V2
|
|
||||||
|
|
||||||
solon.dataSources:
|
|
||||||
db1!:
|
|
||||||
class: "org.h2.jdbcx.JdbcDataSource"
|
|
||||||
url: "jdbc:h2:mem:dev;MODE=Oracle;DB_CLOSE_DELAY=-1"
|
|
||||||
username: sa
|
|
||||||
password: ""
|
|
||||||
@@ -12,7 +12,7 @@ solon.dataSources:
|
|||||||
class: "com.zaxxer.hikari.HikariDataSource"
|
class: "com.zaxxer.hikari.HikariDataSource"
|
||||||
jdbcUrl: jdbc:oracle:thin:@192.168.50.204:1521/FREEPDB1
|
jdbcUrl: jdbc:oracle:thin:@192.168.50.204:1521/FREEPDB1
|
||||||
username: SYSTEM
|
username: SYSTEM
|
||||||
password: ${DB_PWD:jyf@2024}
|
password: Msor6085008
|
||||||
minimumIdle: 2
|
minimumIdle: 2
|
||||||
maximumPoolSize: 10
|
maximumPoolSize: 10
|
||||||
connectionTimeout: 30000
|
connectionTimeout: 30000
|
||||||
+48
-11
@@ -11,16 +11,18 @@ solon:
|
|||||||
app:
|
app:
|
||||||
name: "hospital-front"
|
name: "hospital-front"
|
||||||
group: "hospital"
|
group: "hospital"
|
||||||
|
# JSON 序列化对齐原 hospitalmiddle(Jackson 默认保留 null 字段 + 日期格式)
|
||||||
|
serialization:
|
||||||
|
json:
|
||||||
|
nullAsWriteable: true
|
||||||
|
# 对齐原 spring.jackson.date-format / time-zone:Date/Timestamp 序列化为字符串而非毫秒时间戳
|
||||||
|
dateAsFormat: "yyyy-MM-dd HH:mm:ss"
|
||||||
|
dateAsTimeZone: "GMT+8"
|
||||||
|
|
||||||
# 应用配置
|
# 应用配置
|
||||||
|
# 医院注册清单由代码决定(各 Adapter 的 @Component),此处仅指定激活哪家
|
||||||
hospital:
|
hospital:
|
||||||
active: "jinghe" # 启动激活哪家医院
|
active: "jinghe" # 启动激活哪家医院
|
||||||
adapters: # 注册的医院清单(启动自检)
|
|
||||||
- "xinglongyuan"
|
|
||||||
- "qingyang"
|
|
||||||
- "ningxia"
|
|
||||||
- "yanan"
|
|
||||||
- "jinghe"
|
|
||||||
|
|
||||||
# ====================================
|
# ====================================
|
||||||
# 业务开关
|
# 业务开关
|
||||||
@@ -31,6 +33,34 @@ hm:
|
|||||||
allsecondFlag: false # 宁夏医院强制发送二级单位编码和名称
|
allsecondFlag: false # 宁夏医院强制发送二级单位编码和名称
|
||||||
occFlag: false # 健康体检不发送职业相关信息
|
occFlag: false # 健康体检不发送职业相关信息
|
||||||
|
|
||||||
|
# ====================================
|
||||||
|
# 接口鉴权
|
||||||
|
# ====================================
|
||||||
|
# /order/** 的 HTTP Basic 认证(对齐原 hospitalmiddle Sa-Token basic),
|
||||||
|
# 格式 "user:password",未配置时放行;生产环境必须配置
|
||||||
|
# order.basic: "hmapiuser:xxxx"
|
||||||
|
order:
|
||||||
|
# 鉴权总开关(对齐原程序 sa-token.authFlag),false 时 /order/** 全放行
|
||||||
|
authFlag: true
|
||||||
|
basic: ""
|
||||||
|
# /api/admin/** 的管理 Token(Agent 环回调用携带 X-Admin-Token 头),未配置时放行
|
||||||
|
admin:
|
||||||
|
token: ""
|
||||||
|
|
||||||
|
# IP 白名单(应对端口暴露公网被扫描器探测;环回地址 Agent 永远放行)
|
||||||
|
# 开启后非白名单 IP 一律 403。
|
||||||
|
# ipWhitelist 三种写法可混用,逗号分隔:
|
||||||
|
# 精确 IP:10.0.0.5
|
||||||
|
# 通配段:112.113.*.*(每段可单独 *)
|
||||||
|
# CIDR 网段:10.0.0.0/24
|
||||||
|
security:
|
||||||
|
ipWhitelistEnabled: false
|
||||||
|
ipWhitelist: ""
|
||||||
|
# ipWhitelist: "112.113.*.*,10.20.30.40,10.0.0.0/24"
|
||||||
|
# nginx 反代部署时开启:从 X-Forwarded-For 最右侧取真实客户端 IP 判定
|
||||||
|
# (对端必须是本机环回的 nginx,防止客户端伪造 XFF 绕过白名单)
|
||||||
|
# trustProxy: false
|
||||||
|
|
||||||
# ====================================
|
# ====================================
|
||||||
# MyBatis-Plus 公共配置(db1 为唯一数据源,随医院配置注入)
|
# MyBatis-Plus 公共配置(db1 为唯一数据源,随医院配置注入)
|
||||||
# mappers 条目两种形式(Solon mybatis 约定):
|
# mappers 条目两种形式(Solon mybatis 约定):
|
||||||
@@ -43,10 +73,8 @@ mybatis.db1:
|
|||||||
mappers:
|
mappers:
|
||||||
- "com.hospital.front.mapper.bsm"
|
- "com.hospital.front.mapper.bsm"
|
||||||
- "com.hospital.front.mapper.nx"
|
- "com.hospital.front.mapper.nx"
|
||||||
- "com.hospital.front.mapper.ya"
|
- "com/hospital/front/mapper/bsm/BsmOrderMapper.xml"
|
||||||
- "com/hospital/front/mapper/bsm/OrderMapper.xml"
|
- "com/hospital/front/mapper/nx/NxOrderMapper.xml"
|
||||||
- "com/hospital/front/mapper/nx/OrderMapper.xml"
|
|
||||||
- "com/hospital/front/mapper/ya/OrderMapper.xml"
|
|
||||||
configuration:
|
configuration:
|
||||||
mapUnderscoreToCamelCase: true
|
mapUnderscoreToCamelCase: true
|
||||||
logImpl: "org.apache.ibatis.logging.slf4j.Slf4jImpl"
|
logImpl: "org.apache.ibatis.logging.slf4j.Slf4jImpl"
|
||||||
@@ -60,4 +88,13 @@ logging:
|
|||||||
level:
|
level:
|
||||||
root: INFO
|
root: INFO
|
||||||
com.hospital.front: INFO
|
com.hospital.front: INFO
|
||||||
org.noear: WARN
|
org.noear: INFO
|
||||||
|
|
||||||
|
# ====================================
|
||||||
|
# Uptime-Kuma 监控(沿用原 hospitalmiddle 逻辑)
|
||||||
|
# kuma.code 在各医院配置中覆盖
|
||||||
|
# ====================================
|
||||||
|
kuma:
|
||||||
|
enable: false
|
||||||
|
cron: "0 */3 * * * *"
|
||||||
|
url: ""
|
||||||
@@ -7,12 +7,14 @@
|
|||||||
SELECT 1 FROM DUAL
|
SELECT 1 FROM DUAL
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- ==================== 存储过程:预约/取消/单位 ==================== -->
|
||||||
|
|
||||||
<parameterMap id="pm" type="java.util.Map">
|
<parameterMap id="pm" type="java.util.Map">
|
||||||
<parameter property="peInfoStr" jdbcType="VARCHAR" mode="IN"></parameter>
|
<parameter property="peInfoStr" jdbcType="VARCHAR" mode="IN"/>
|
||||||
<parameter property="peItemStr" jdbcType="VARCHAR" mode="IN"></parameter>
|
<parameter property="peItemStr" jdbcType="VARCHAR" mode="IN"/>
|
||||||
<parameter property="resultCode" jdbcType="VARCHAR" mode="OUT"></parameter>
|
<parameter property="resultCode" jdbcType="VARCHAR" mode="OUT"/>
|
||||||
<parameter property="errorMsg" jdbcType="VARCHAR" mode="OUT"></parameter>
|
<parameter property="errorMsg" jdbcType="VARCHAR" mode="OUT"/>
|
||||||
<parameter property="peId" jdbcType="VARCHAR" mode="OUT"></parameter>
|
<parameter property="peId" jdbcType="VARCHAR" mode="OUT"/>
|
||||||
</parameterMap>
|
</parameterMap>
|
||||||
<insert id="saveOrder" parameterMap="pm" statementType="CALLABLE">
|
<insert id="saveOrder" parameterMap="pm" statementType="CALLABLE">
|
||||||
{call P_ADD_PE_REGIST(
|
{call P_ADD_PE_REGIST(
|
||||||
@@ -25,11 +27,11 @@
|
|||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<parameterMap id="cancenlPm" type="java.util.Map">
|
<parameterMap id="cancenlPm" type="java.util.Map">
|
||||||
<parameter property="peId" jdbcType="VARCHAR" mode="IN"></parameter>
|
<parameter property="peId" jdbcType="VARCHAR" mode="IN"/>
|
||||||
<parameter property="name" jdbcType="VARCHAR" mode="IN"></parameter>
|
<parameter property="name" jdbcType="VARCHAR" mode="IN"/>
|
||||||
<parameter property="idcard" jdbcType="VARCHAR" mode="IN"></parameter>
|
<parameter property="idcard" jdbcType="VARCHAR" mode="IN"/>
|
||||||
<parameter property="resultCode" jdbcType="VARCHAR" mode="OUT"></parameter>
|
<parameter property="resultCode" jdbcType="VARCHAR" mode="OUT"/>
|
||||||
<parameter property="errorMsg" jdbcType="VARCHAR" mode="OUT"></parameter>
|
<parameter property="errorMsg" jdbcType="VARCHAR" mode="OUT"/>
|
||||||
</parameterMap>
|
</parameterMap>
|
||||||
<insert id="cancelOrder" parameterMap="cancenlPm" statementType="CALLABLE">
|
<insert id="cancelOrder" parameterMap="cancenlPm" statementType="CALLABLE">
|
||||||
{call P_DEL_PE_REGIST(
|
{call P_DEL_PE_REGIST(
|
||||||
@@ -42,13 +44,11 @@
|
|||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<parameterMap id="unitPm" type="java.util.Map">
|
<parameterMap id="unitPm" type="java.util.Map">
|
||||||
<parameter property="unitCode" jdbcType="VARCHAR" mode="IN"></parameter>
|
<parameter property="unitCode" jdbcType="VARCHAR" mode="IN"/>
|
||||||
<parameter property="unitName" jdbcType="VARCHAR" mode="IN"></parameter>
|
<parameter property="unitName" jdbcType="VARCHAR" mode="IN"/>
|
||||||
<parameter property="resultCode" jdbcType="VARCHAR" mode="OUT"></parameter>
|
<parameter property="resultCode" jdbcType="VARCHAR" mode="OUT"/>
|
||||||
<parameter property="errorMsg" jdbcType="VARCHAR" mode="OUT"></parameter>
|
<parameter property="errorMsg" jdbcType="VARCHAR" mode="OUT"/>
|
||||||
</parameterMap>
|
</parameterMap>
|
||||||
|
|
||||||
|
|
||||||
<insert id="addUnit" parameterMap="unitPm" statementType="CALLABLE">
|
<insert id="addUnit" parameterMap="unitPm" statementType="CALLABLE">
|
||||||
{call P_ADD_UNIT(
|
{call P_ADD_UNIT(
|
||||||
#{unitCode,mode=IN,jdbcType=VARCHAR},
|
#{unitCode,mode=IN,jdbcType=VARCHAR},
|
||||||
@@ -59,16 +59,16 @@
|
|||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<parameterMap id="unitPmV2" type="java.util.Map">
|
<parameterMap id="unitPmV2" type="java.util.Map">
|
||||||
<parameter property="unitCode" jdbcType="VARCHAR" mode="IN"></parameter>
|
<parameter property="unitCode" jdbcType="VARCHAR" mode="IN"/>
|
||||||
<parameter property="unitName" jdbcType="VARCHAR" mode="IN"></parameter>
|
<parameter property="unitName" jdbcType="VARCHAR" mode="IN"/>
|
||||||
<parameter property="parentUnitCode" jdbcType="VARCHAR" mode="IN"></parameter>
|
<parameter property="parentUnitCode" jdbcType="VARCHAR" mode="IN"/>
|
||||||
<parameter property="address" jdbcType="VARCHAR" mode="IN"></parameter>
|
<parameter property="address" jdbcType="VARCHAR" mode="IN"/>
|
||||||
<parameter property="connecter1" jdbcType="VARCHAR" mode="IN"></parameter>
|
<parameter property="connecter1" jdbcType="VARCHAR" mode="IN"/>
|
||||||
<parameter property="phone1" jdbcType="VARCHAR" mode="IN"></parameter>
|
<parameter property="phone1" jdbcType="VARCHAR" mode="IN"/>
|
||||||
<parameter property="connecter2" jdbcType="VARCHAR" mode="IN"></parameter>
|
<parameter property="connecter2" jdbcType="VARCHAR" mode="IN"/>
|
||||||
<parameter property="phone2" jdbcType="VARCHAR" mode="IN"></parameter>
|
<parameter property="phone2" jdbcType="VARCHAR" mode="IN"/>
|
||||||
<parameter property="resultCode" jdbcType="VARCHAR" mode="OUT"></parameter>
|
<parameter property="resultCode" jdbcType="VARCHAR" mode="OUT"/>
|
||||||
<parameter property="errorMsg" jdbcType="VARCHAR" mode="OUT"></parameter>
|
<parameter property="errorMsg" jdbcType="VARCHAR" mode="OUT"/>
|
||||||
</parameterMap>
|
</parameterMap>
|
||||||
<insert id="addUnitV2" parameterMap="unitPmV2" statementType="CALLABLE">
|
<insert id="addUnitV2" parameterMap="unitPmV2" statementType="CALLABLE">
|
||||||
{call P_ADD_UNIT(
|
{call P_ADD_UNIT(
|
||||||
@@ -85,24 +85,35 @@
|
|||||||
)}
|
)}
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<select id="getPhyexamStatus" resultType="java.util.Map">
|
<!-- ==================== 体检状态 ==================== -->
|
||||||
|
|
||||||
|
<select id="getPhyexamStatus" resultType="com.hospital.front.vo.PeStatusVo">
|
||||||
select PE_ID, NAME, ID_NO, UNIT_NAME, DEPARTMENT, PE_PRE_DATE, PE_QUEUE_DATE, PE_STATUS
|
select PE_ID, NAME, ID_NO, UNIT_NAME, DEPARTMENT, PE_PRE_DATE, PE_QUEUE_DATE, PE_STATUS
|
||||||
from V_PHYEXAM_PE_STATUS
|
from V_PHYEXAM_PE_STATUS
|
||||||
where instr(',' || #{peIds} || ',', ',' || pe_id || ',') > 0
|
where instr(',' || #{peIds} || ',', ',' || pe_id || ',') > 0
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getItemList" resultType="java.util.Map">
|
<select id="getPeStatusByIdnoAndYear" resultType="com.hospital.front.vo.PeStatusVo">
|
||||||
select distinct item_assem_code, item_assem_name
|
select * from V_PHYEXAM_PE_STATUS
|
||||||
from V_PHYEXAM_ITEM_ASSEMBLED
|
where ID_NO = #{idNo}
|
||||||
|
and to_char(PE_PRE_DATE, 'yyyy') = #{year}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- ==================== 体检项目 ==================== -->
|
||||||
|
|
||||||
|
<select id="getItemList" resultType="java.util.Map">
|
||||||
|
SELECT DISTINCT ITEM_ASSEM_CODE, ITEM_ASSEM_NAME
|
||||||
|
FROM V_PHYEXAM_ITEM_ASSEMBLED
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="getAllItemList" resultType="java.util.Map">
|
<select id="getAllItemList" resultType="java.util.Map">
|
||||||
select ITEM_ASSEM_CODE, ITEM_ASSEM_NAME, PE_ITEM_CODE, PE_ITEM_NAME
|
SELECT ITEM_ASSEM_CODE, ITEM_ASSEM_NAME, PE_ITEM_CODE, PE_ITEM_NAME
|
||||||
from V_PHYEXAM_ITEM_ASSEMBLED
|
FROM V_PHYEXAM_ITEM_ASSEMBLED
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getPeItemList" resultType="java.util.Map">
|
<select id="getPeItemList" resultType="java.util.Map">
|
||||||
select *
|
SELECT PE_ITEM_CODE, PE_ITEM_NAME
|
||||||
from V_PHYEXAM_ITEM
|
FROM V_PHYEXAM_ITEM
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getEmpItem" resultType="java.util.Map">
|
<select id="getEmpItem" resultType="java.util.Map">
|
||||||
@@ -112,13 +123,15 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getItemInfo" resultType="java.util.Map">
|
<select id="getItemInfo" resultType="java.util.Map">
|
||||||
select ITEM_ASSEM_CODE, ITEM_ASSEM_NAME, PE_ITEM_CODE, PE_ITEM_NAME
|
SELECT ITEM_ASSEM_CODE, ITEM_ASSEM_NAME, PE_ITEM_CODE, PE_ITEM_NAME
|
||||||
from V_PHYEXAM_ITEM_ASSEMBLED
|
FROM V_PHYEXAM_ITEM_ASSEMBLED
|
||||||
where PE_ITEM_CODE = #{peItemCode}
|
WHERE PE_ITEM_CODE = #{peItemCode}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- ==================== 体检结果 ==================== -->
|
||||||
|
|
||||||
<select id="getResult" resultType="java.util.Map">
|
<select id="getResult" resultType="java.util.Map">
|
||||||
select PE_ID,
|
SELECT PE_ID,
|
||||||
PE_VISIT_ID,
|
PE_VISIT_ID,
|
||||||
ID_NO,
|
ID_NO,
|
||||||
NAME,
|
NAME,
|
||||||
@@ -135,263 +148,192 @@
|
|||||||
PE_RESULT,
|
PE_RESULT,
|
||||||
UNIT,
|
UNIT,
|
||||||
PRINT_CONTEXT
|
PRINT_CONTEXT
|
||||||
from V_PHYEXAM_RESULT
|
FROM (
|
||||||
where PE_ID = #{peId}
|
SELECT b.pe_id, b.pe_visit_id, a.id_no, a.name, b.unit_code,
|
||||||
|
(SELECT t.unit_name FROM pe_unit_dict t WHERE t.unit_code = b.unit_code) unit_name,
|
||||||
|
b.pe_queue_date, b.audit_date,
|
||||||
|
c.pe_dept_code, c.pe_dept_name, c.item_assem_code, c.item_assem_name,
|
||||||
|
c.pe_item_code, c.pe_item_name, c.pe_result, c.unit, c.print_context
|
||||||
|
FROM pe_master_index a, pe_visit b, pe_result_dict c
|
||||||
|
WHERE a.pe_id = b.pe_id
|
||||||
|
AND b.pe_id = c.pe_id
|
||||||
|
AND b.pe_visit_id = c.pe_visit_id
|
||||||
|
)
|
||||||
|
WHERE PE_ID = #{peId}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getConclusion" resultType="java.util.Map">
|
<!-- ==================== 体检结论 / 建议 ==================== -->
|
||||||
select PE_ID,
|
|
||||||
|
|
||||||
|
<select id="getConclusionAll" resultType="com.hospital.front.vo.ConclusionVo">
|
||||||
|
SELECT PE_ID,
|
||||||
PE_VISIT_ID,
|
PE_VISIT_ID,
|
||||||
PE_DEPT_CODE,
|
PE_DEPT_CODE,
|
||||||
PE_DEPT_NAME,
|
PE_DEPT_NAME,
|
||||||
REPLACE(REPLACE(REPLACE(DBMS_LOB.SUBSTR(CONCLUSION_TEXT, 4000, 1), chr(13) || chr(10), ''), chr(13), ''), chr(10), '') CONCLUSION_TEXT
|
REPLACE(REPLACE(REPLACE(DBMS_LOB.SUBSTR(CONCLUSION_TEXT, 4000, 1), chr(13) || chr(10), ''), chr(13), ''), chr(10), '') CONCLUSION_TEXT
|
||||||
from V_PHYEXAM_DEPT_RESULT
|
FROM (
|
||||||
where PE_ID = #{peId}
|
SELECT a.pe_id, a.pe_visit_id, a.pe_dept_code, a.pe_dept_name, a.conclusion_text
|
||||||
and CONCLUSION_TEXT is not null
|
FROM pe_dept_result_dict a, pe_visit b
|
||||||
|
WHERE a.pe_id = b.pe_id
|
||||||
|
AND a.pe_visit_id = b.pe_visit_id
|
||||||
|
)
|
||||||
|
WHERE PE_ID = #{peId}
|
||||||
|
AND CONCLUSION_TEXT IS NOT NULL
|
||||||
</select>
|
</select>
|
||||||
<select id="getConclusionAll" resultType="java.util.Map">
|
|
||||||
select PE_ID,
|
|
||||||
PE_VISIT_ID,
|
|
||||||
PE_DEPT_CODE,
|
|
||||||
PE_DEPT_NAME,
|
|
||||||
REPLACE(REPLACE(REPLACE(DBMS_LOB.SUBSTR(CONCLUSION_TEXT, 4000, 1), chr(13) || chr(10), ''), chr(13), ''), chr(10), '') CONCLUSION_TEXT
|
|
||||||
from (select a.pe_id,
|
|
||||||
a.pe_visit_id,
|
|
||||||
a.pe_dept_code,
|
|
||||||
a.pe_dept_name,
|
|
||||||
a.conclusion_text
|
|
||||||
from pe_dept_result_dict a,pe_visit b
|
|
||||||
where a.pe_id = b.pe_id
|
|
||||||
and a.pe_visit_id = b.pe_visit_id) a
|
|
||||||
where PE_ID = #{peId}
|
|
||||||
and CONCLUSION_TEXT is not null
|
|
||||||
</select>
|
|
||||||
<!-- getTotalConclusion/getTotalSuggest 已删除:原实现依赖 wm_concat(Oracle 18c 起移除,
|
|
||||||
且接口未引用);汇总逻辑由 OrderService 基于本文件列表查询在应用层完成 -->
|
|
||||||
|
|
||||||
<select id="getSuggest" resultType="java.util.Map">
|
<select id="getSuggestAll" resultType="com.hospital.front.vo.SuggestVo">
|
||||||
select PE_ID, PE_VISIT_ID, REC_CONTENT, REPLACE(REPLACE(REPLACE(DBMS_LOB.SUBSTR(GUIDE_CONTENT, 4000, 1), chr(13) || chr(10), ''), chr(13), ''), chr(10), '') GUIDE_CONTENT
|
SELECT PE_ID, PE_VISIT_ID, REC_CONTENT,
|
||||||
from V_PHYEXAM_DIAGNOSIS
|
REPLACE(REPLACE(REPLACE(DBMS_LOB.SUBSTR(GUIDE_CONTENT, 4000, 1), chr(13) || chr(10), ''), chr(13), ''), chr(10), '') GUIDE_CONTENT
|
||||||
where PE_ID = #{peId}
|
FROM (
|
||||||
and GUIDE_CONTENT is not null
|
SELECT a.pe_id, a.pe_visit_id, a.rec_content, a.guide_content
|
||||||
</select>
|
FROM pe_diagnosis_record a, pe_visit b
|
||||||
<select id="getSuggestAll" resultType="java.util.HashMap">
|
WHERE a.pe_id = b.pe_id
|
||||||
select PE_ID, PE_VISIT_ID, REC_CONTENT, REPLACE(REPLACE(REPLACE(DBMS_LOB.SUBSTR(GUIDE_CONTENT, 4000, 1), chr(13) || chr(10), ''), chr(13), ''), chr(10), '') GUIDE_CONTENT
|
AND a.pe_visit_id = b.pe_visit_id
|
||||||
from (select a.pe_id,
|
)
|
||||||
a.pe_visit_id,
|
WHERE PE_ID = #{peId}
|
||||||
a.rec_content,
|
AND GUIDE_CONTENT IS NOT NULL
|
||||||
a.guide_content
|
|
||||||
from pe_diagnosis_record a,pe_visit b
|
|
||||||
where a.pe_id = b.pe_id
|
|
||||||
and a.pe_visit_id = b.pe_visit_id) a
|
|
||||||
where PE_ID = #{peId}
|
|
||||||
and GUIDE_CONTENT is not null
|
|
||||||
</select>
|
|
||||||
<select id="getPeStatusByIdnoAndYear" resultType="java.util.HashMap">
|
|
||||||
select *
|
|
||||||
from V_PHYEXAM_PE_STATUS
|
|
||||||
where ID_NO = #{idNo}
|
|
||||||
and PE_PRE_DATE >= TO_DATE(#{year} || '-01-01', 'YYYY-MM-DD')
|
|
||||||
and PE_PRE_DATE < ADD_MONTHS(TO_DATE(#{year} || '-01-01', 'YYYY-MM-DD'), 12)
|
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- ==================== 按身份证 / 日期 / 编号查询结果 ==================== -->
|
||||||
|
|
||||||
<select id="getResultByIdcardAndYear" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
<select id="getResultByIdcardAndYear" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
||||||
select DISTINCT
|
SELECT DISTINCT
|
||||||
ID_NO,
|
ID_NO, NAME, PE_ID, PE_VISIT_ID, UNIT_CODE, UNIT_NAME,
|
||||||
NAME,
|
|
||||||
PE_ID,
|
|
||||||
PE_VISIT_ID,
|
|
||||||
UNIT_CODE,
|
|
||||||
UNIT_NAME,
|
|
||||||
to_char(PE_QUEUE_DATE, 'yyyy-mm-dd HH24:mi:ss') PE_QUEUE_DATE,
|
to_char(PE_QUEUE_DATE, 'yyyy-mm-dd HH24:mi:ss') PE_QUEUE_DATE,
|
||||||
to_char(AUDIT_DATE, 'yyyy-mm-dd HH24:mi:ss') AUDIT_DATE
|
to_char(AUDIT_DATE, 'yyyy-mm-dd HH24:mi:ss') AUDIT_DATE
|
||||||
from V_PHYEXAM_RESULT
|
FROM (
|
||||||
where ID_NO = #{idNo}
|
SELECT b.pe_id, b.pe_visit_id, a.id_no, a.name, b.unit_code,
|
||||||
and PE_QUEUE_DATE >= TO_DATE(#{year} || '-01-01', 'YYYY-MM-DD')
|
(SELECT t.unit_name FROM pe_unit_dict t WHERE t.unit_code = b.unit_code) unit_name,
|
||||||
and PE_QUEUE_DATE < ADD_MONTHS(TO_DATE(#{year} || '-01-01', 'YYYY-MM-DD'), 12)
|
b.pe_queue_date, b.audit_date,
|
||||||
|
c.pe_dept_code, c.pe_dept_name, c.item_assem_code, c.item_assem_name,
|
||||||
|
c.pe_item_code, c.pe_item_name, c.pe_result, c.unit, c.print_context
|
||||||
|
FROM pe_master_index a, pe_visit b, pe_result_dict c
|
||||||
|
WHERE a.pe_id = b.pe_id
|
||||||
|
AND b.pe_id = c.pe_id
|
||||||
|
AND b.pe_visit_id = c.pe_visit_id
|
||||||
|
)
|
||||||
|
WHERE ID_NO = #{idNo}
|
||||||
|
AND PE_QUEUE_DATE >= TO_DATE(#{year} || '-01-01', 'YYYY-MM-DD')
|
||||||
|
AND PE_QUEUE_DATE < ADD_MONTHS(TO_DATE(#{year} || '-01-01', 'YYYY-MM-DD'), 12)
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getResultByIdcardAndYearAll" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
<select id="getResultByIdcardAndYearAll" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
ID_NO,
|
ID_NO, NAME, PE_ID, PE_VISIT_ID, UNIT_CODE, UNIT_NAME,
|
||||||
NAME,
|
|
||||||
PE_ID,
|
|
||||||
PE_VISIT_ID,
|
|
||||||
UNIT_CODE,
|
|
||||||
UNIT_NAME,
|
|
||||||
to_char(PE_QUEUE_DATE, 'yyyy-mm-dd HH24:mi:ss') PE_QUEUE_DATE,
|
to_char(PE_QUEUE_DATE, 'yyyy-mm-dd HH24:mi:ss') PE_QUEUE_DATE,
|
||||||
to_char(AUDIT_DATE, 'yyyy-mm-dd HH24:mi:ss') AUDIT_DATE
|
to_char(AUDIT_DATE, 'yyyy-mm-dd HH24:mi:ss') AUDIT_DATE
|
||||||
FROM
|
FROM (
|
||||||
(
|
SELECT b.pe_id, b.pe_visit_id, a.id_no, a.NAME, b.unit_code, t.unit_name,
|
||||||
SELECT
|
b.pe_queue_date, b.audit_date,
|
||||||
b.pe_id,
|
c.pe_dept_code, c.pe_dept_name, c.item_assem_code, c.item_assem_name,
|
||||||
b.pe_visit_id,
|
c.pe_item_code, c.pe_item_name, c.pe_result, c.unit, c.print_context
|
||||||
a.id_no,
|
FROM pe_master_index a
|
||||||
a.NAME,
|
|
||||||
b.unit_code,
|
|
||||||
t.unit_name,
|
|
||||||
b.pe_queue_date,
|
|
||||||
b.audit_date,
|
|
||||||
c.pe_dept_code,
|
|
||||||
c.pe_dept_name,
|
|
||||||
c.item_assem_code,
|
|
||||||
c.item_assem_name,
|
|
||||||
c.pe_item_code,
|
|
||||||
c.pe_item_name,
|
|
||||||
c.pe_result,
|
|
||||||
c.unit,
|
|
||||||
c.print_context
|
|
||||||
FROM
|
|
||||||
pe_master_index a
|
|
||||||
JOIN pe_visit b ON b.pe_id = a.pe_id
|
JOIN pe_visit b ON b.pe_id = a.pe_id
|
||||||
JOIN pe_result_dict c ON c.pe_id = b.pe_id AND c.pe_visit_id = b.pe_visit_id
|
JOIN pe_result_dict c ON c.pe_id = b.pe_id AND c.pe_visit_id = b.pe_visit_id
|
||||||
LEFT JOIN pe_unit_dict t ON t.unit_code = b.unit_code
|
LEFT JOIN pe_unit_dict t ON t.unit_code = b.unit_code
|
||||||
WHERE
|
WHERE b.audit_date >= TO_DATE(#{year} || '-01-01', 'YYYY-MM-DD')
|
||||||
b.audit_date >= TO_DATE(#{year} || '-01-01', 'YYYY-MM-DD')
|
AND b.audit_date < ADD_MONTHS(TO_DATE(#{year} || '-01-01', 'YYYY-MM-DD'), 12)
|
||||||
and b.audit_date < ADD_MONTHS(TO_DATE(#{year} || '-01-01', 'YYYY-MM-DD'), 12)
|
AND a.id_no = #{idNo}
|
||||||
and a.id_no = #{idNo}
|
)
|
||||||
) a
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="getResultByAuditDate" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
|
||||||
select *
|
|
||||||
from V_PHYEXAM_RESULT
|
|
||||||
where to_char(AUDIT_DATE,'yyyy-mm-dd')=#{auditDate}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="getDisResultByAuditDate" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
|
||||||
SELECT DISTINCT
|
|
||||||
ID_NO,
|
|
||||||
NAME,
|
|
||||||
PE_ID,
|
|
||||||
PE_VISIT_ID,
|
|
||||||
UNIT_CODE,
|
|
||||||
UNIT_NAME,
|
|
||||||
to_char( PE_QUEUE_DATE, 'yyyy-mm-dd HH24:mi:ss' ) PE_QUEUE_DATE,
|
|
||||||
to_char( AUDIT_DATE, 'yyyy-mm-dd HH24:mi:ss' ) AUDIT_DATE
|
|
||||||
FROM
|
|
||||||
V_PHYEXAM_RESULT
|
|
||||||
WHERE
|
|
||||||
AUDIT_DATE >= TO_DATE(#{auditDate}, 'YYYY-MM-DD')
|
|
||||||
and AUDIT_DATE < TO_DATE(#{auditDate}, 'YYYY-MM-DD') + 1
|
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getItemByPeId" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
<select id="getItemByPeId" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
||||||
SELECT
|
SELECT ITEM_ASSEM_CODE, ITEM_ASSEM_NAME, PE_ITEM_CODE, PE_ITEM_NAME, PE_RESULT, UNIT, PRINT_CONTEXT
|
||||||
ITEM_ASSEM_CODE,
|
FROM (
|
||||||
ITEM_ASSEM_NAME,
|
SELECT b.pe_id, b.pe_visit_id, a.id_no, a.NAME, b.unit_code, t.unit_name,
|
||||||
PE_ITEM_CODE,
|
b.pe_queue_date, b.audit_date,
|
||||||
PE_ITEM_NAME,
|
c.pe_dept_code, c.pe_dept_name, c.item_assem_code, c.item_assem_name,
|
||||||
PE_RESULT,
|
c.pe_item_code, c.pe_item_name, c.pe_result, c.unit, c.print_context
|
||||||
UNIT,
|
FROM pe_master_index a
|
||||||
PRINT_CONTEXT
|
|
||||||
FROM
|
|
||||||
V_PHYEXAM_RESULT
|
|
||||||
WHERE
|
|
||||||
PE_ID = #{peId}
|
|
||||||
</select>
|
|
||||||
<select id="getItemByPeIdAll" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
|
||||||
SELECT
|
|
||||||
ITEM_ASSEM_CODE,
|
|
||||||
ITEM_ASSEM_NAME,
|
|
||||||
PE_ITEM_CODE,
|
|
||||||
PE_ITEM_NAME,
|
|
||||||
PE_RESULT,
|
|
||||||
UNIT,
|
|
||||||
PRINT_CONTEXT
|
|
||||||
FROM
|
|
||||||
(
|
|
||||||
SELECT
|
|
||||||
b.pe_id,
|
|
||||||
b.pe_visit_id,
|
|
||||||
a.id_no,
|
|
||||||
a.NAME,
|
|
||||||
b.unit_code,
|
|
||||||
t.unit_name,
|
|
||||||
b.pe_queue_date,
|
|
||||||
b.audit_date,
|
|
||||||
c.pe_dept_code,
|
|
||||||
c.pe_dept_name,
|
|
||||||
c.item_assem_code,
|
|
||||||
c.item_assem_name,
|
|
||||||
c.pe_item_code,
|
|
||||||
c.pe_item_name,
|
|
||||||
c.pe_result,
|
|
||||||
c.unit,
|
|
||||||
c.print_context
|
|
||||||
FROM
|
|
||||||
pe_master_index a
|
|
||||||
JOIN pe_visit b ON b.pe_id = a.pe_id
|
JOIN pe_visit b ON b.pe_id = a.pe_id
|
||||||
JOIN pe_result_dict c ON c.pe_id = b.pe_id AND c.pe_visit_id = b.pe_visit_id
|
JOIN pe_result_dict c ON c.pe_id = b.pe_id AND c.pe_visit_id = b.pe_visit_id
|
||||||
LEFT JOIN pe_unit_dict t ON t.unit_code = b.unit_code
|
LEFT JOIN pe_unit_dict t ON t.unit_code = b.unit_code
|
||||||
WHERE
|
WHERE b.pe_id = #{peId}
|
||||||
b.pe_id = #{peId}
|
)
|
||||||
) a
|
|
||||||
|
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getResultByIdNoOrPeId" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
<select id="getItemByPeIdAll" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
||||||
select DISTINCT
|
SELECT ITEM_ASSEM_CODE, ITEM_ASSEM_NAME, PE_ITEM_CODE, PE_ITEM_NAME, PE_RESULT, UNIT, PRINT_CONTEXT
|
||||||
ID_NO,
|
FROM (
|
||||||
NAME,
|
SELECT b.pe_id, b.pe_visit_id, a.id_no, a.NAME, b.unit_code, t.unit_name,
|
||||||
PE_ID,
|
b.pe_queue_date, b.audit_date,
|
||||||
PE_VISIT_ID,
|
c.pe_dept_code, c.pe_dept_name, c.item_assem_code, c.item_assem_name,
|
||||||
UNIT_CODE,
|
c.pe_item_code, c.pe_item_name, c.pe_result, c.unit, c.print_context
|
||||||
UNIT_NAME,
|
FROM pe_master_index a
|
||||||
|
JOIN pe_visit b ON b.pe_id = a.pe_id
|
||||||
|
JOIN pe_result_dict c ON c.pe_id = b.pe_id AND c.pe_visit_id = b.pe_visit_id
|
||||||
|
LEFT JOIN pe_unit_dict t ON t.unit_code = b.unit_code
|
||||||
|
WHERE b.pe_id = #{peId}
|
||||||
|
)
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="getResultByPeId" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
||||||
|
SELECT DISTINCT
|
||||||
|
ID_NO, NAME, PE_ID, PE_VISIT_ID, UNIT_CODE, UNIT_NAME,
|
||||||
to_char(PE_QUEUE_DATE, 'yyyy-mm-dd HH24:mi:ss') PE_QUEUE_DATE,
|
to_char(PE_QUEUE_DATE, 'yyyy-mm-dd HH24:mi:ss') PE_QUEUE_DATE,
|
||||||
to_char(AUDIT_DATE, 'yyyy-mm-dd HH24:mi:ss') AUDIT_DATE
|
to_char(AUDIT_DATE, 'yyyy-mm-dd HH24:mi:ss') AUDIT_DATE
|
||||||
from V_PHYEXAM_RESULT
|
FROM (
|
||||||
<where>
|
SELECT b.pe_id, b.pe_visit_id, a.id_no, a.name, b.unit_code,
|
||||||
1=1
|
(SELECT t.unit_name FROM pe_unit_dict t WHERE t.unit_code = b.unit_code) unit_name,
|
||||||
<if test="idNo != null and idNo != '' ">
|
b.pe_queue_date, b.audit_date,
|
||||||
and ID_NO = #{idNo}
|
c.pe_dept_code, c.pe_dept_name, c.item_assem_code, c.item_assem_name,
|
||||||
</if>
|
c.pe_item_code, c.pe_item_name, c.pe_result, c.unit, c.print_context
|
||||||
<if test="peId != null and peId != '' ">
|
FROM pe_master_index a, pe_visit b, pe_result_dict c
|
||||||
and PE_ID = #{peId}
|
WHERE a.pe_id = b.pe_id
|
||||||
</if>
|
AND b.pe_id = c.pe_id
|
||||||
|
AND b.pe_visit_id = c.pe_visit_id
|
||||||
and AUDIT_DATE >= TO_DATE(#{auditYear} || '-01-01', 'YYYY-MM-DD')
|
)
|
||||||
and AUDIT_DATE < ADD_MONTHS(TO_DATE(#{auditYear} || '-01-01', 'YYYY-MM-DD'), 12)
|
WHERE PE_ID = #{peId}
|
||||||
</where>
|
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- ==================== 按身份证查 peId/预约日期 / 人员列表 ==================== -->
|
||||||
|
|
||||||
<select id="getPeIdAndDateByIdcard" resultType="java.util.Map">
|
<select id="getPeIdAndDateByIdcard" resultType="java.util.Map">
|
||||||
select PE_ID,to_char( PE_PRE_DATE, 'yyyy-mm-dd' ) PE_PRE_DATE from V_PHYEXAM_PE_STATUS
|
SELECT PE_ID, to_char(PE_PRE_DATE, 'yyyy-mm-dd') PE_PRE_DATE
|
||||||
|
FROM (
|
||||||
|
SELECT b.pe_id, b.name, b.id_no,
|
||||||
|
(SELECT unit_name FROM pe_unit_dict t WHERE t.unit_code = a.unit_code) unit_name,
|
||||||
|
a.department, a.pe_pre_date, a.pe_queue_date,
|
||||||
|
DECODE(a.queue_flag, '1',
|
||||||
|
DECODE(result_status, '7', '初审', '9', '终审', '1', '检中', '报到'),
|
||||||
|
DECODE(pe_transfer_hospital, '', '预约', '转向' || pe_transfer_hospital)) pe_status
|
||||||
|
FROM pe_visit a, pe_master_index b
|
||||||
|
WHERE a.pe_id = b.pe_id
|
||||||
|
)
|
||||||
<where>
|
<where>
|
||||||
<if test="idNo != null and idNo != ''">
|
<if test="idNo != null and idNo != ''">
|
||||||
and ID_NO = #{idNo}
|
AND ID_NO = #{idNo}
|
||||||
</if>
|
</if>
|
||||||
and PE_PRE_DATE >= TRUNC(SYSDATE, 'YYYY')
|
AND PE_PRE_DATE >= TRUNC(SYSDATE, 'YYYY')
|
||||||
and PE_PRE_DATE < ADD_MONTHS(TRUNC(SYSDATE, 'YYYY'), 12)
|
AND PE_PRE_DATE < ADD_MONTHS(TRUNC(SYSDATE, 'YYYY'), 12)
|
||||||
</where>
|
</where>
|
||||||
order by PE_PRE_DATE desc
|
ORDER BY PE_PRE_DATE DESC
|
||||||
|
|
||||||
</select>
|
|
||||||
<select id="getResultByPeId" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
|
||||||
select DISTINCT
|
|
||||||
ID_NO,
|
|
||||||
NAME,
|
|
||||||
PE_ID,
|
|
||||||
PE_VISIT_ID,
|
|
||||||
UNIT_CODE,
|
|
||||||
UNIT_NAME,
|
|
||||||
to_char( PE_QUEUE_DATE, 'yyyy-mm-dd HH24:mi:ss' ) PE_QUEUE_DATE,
|
|
||||||
to_char( AUDIT_DATE, 'yyyy-mm-dd HH24:mi:ss' ) AUDIT_DATE
|
|
||||||
from V_PHYEXAM_RESULT
|
|
||||||
where PE_ID = #{peId}
|
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getPeIdAndDateByIdcardList" resultType="java.util.Map">
|
<select id="getPeIdAndDateByIdcardList" resultType="java.util.Map">
|
||||||
select PE_ID,to_char( PE_PRE_DATE, 'yyyy-mm-dd' ) PE_PRE_DATE from V_PHYEXAM_PE_STATUS
|
SELECT PE_ID, to_char(PE_PRE_DATE, 'yyyy-mm-dd') PE_PRE_DATE
|
||||||
|
FROM (
|
||||||
|
SELECT b.pe_id, b.name, b.id_no,
|
||||||
|
(SELECT unit_name FROM pe_unit_dict t WHERE t.unit_code = a.unit_code) unit_name,
|
||||||
|
a.department, a.pe_pre_date, a.pe_queue_date,
|
||||||
|
DECODE(a.queue_flag, '1',
|
||||||
|
DECODE(result_status, '7', '初审', '9', '终审', '1', '检中', '报到'),
|
||||||
|
DECODE(pe_transfer_hospital, '', '预约', '转向' || pe_transfer_hospital)) pe_status
|
||||||
|
FROM pe_visit a, pe_master_index b
|
||||||
|
WHERE a.pe_id = b.pe_id
|
||||||
|
)
|
||||||
<where>
|
<where>
|
||||||
<if test="idNo != null and idNo != ''">
|
<if test="idNo != null and idNo != ''">
|
||||||
and ID_NO = #{idNo}
|
AND ID_NO = #{idNo}
|
||||||
</if>
|
</if>
|
||||||
and PE_PRE_DATE >= TRUNC(SYSDATE, 'YYYY')
|
AND PE_PRE_DATE >= TRUNC(SYSDATE, 'YYYY')
|
||||||
and PE_PRE_DATE < ADD_MONTHS(TRUNC(SYSDATE, 'YYYY'), 12)
|
AND PE_PRE_DATE < ADD_MONTHS(TRUNC(SYSDATE, 'YYYY'), 12)
|
||||||
</where>
|
</where>
|
||||||
order by PE_PRE_DATE desc
|
ORDER BY PE_PRE_DATE DESC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- 根据audit_date查询当天体检人员列表(去重) -->
|
<!-- 根据audit_date查询当天体检人员列表(去重) -->
|
||||||
<select id="getExamListByDate" resultType="java.util.Map">
|
<select id="getExamListByDate" resultType="com.hospital.front.vo.ExamListVo">
|
||||||
SELECT DISTINCT PE_ID AS "peId",
|
SELECT DISTINCT PE_ID AS "peId",
|
||||||
ID_NO AS "idNo",
|
ID_NO AS "idNo",
|
||||||
NAME AS "name",
|
NAME AS "name",
|
||||||
@@ -410,4 +352,5 @@
|
|||||||
</choose>
|
</choose>
|
||||||
</where>
|
</where>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
)}
|
)}
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<select id="getPhyexamStatus" resultType="java.util.Map">
|
<select id="getPhyexamStatus" resultType="com.hospital.front.vo.PeStatusVo">
|
||||||
SELECT
|
SELECT
|
||||||
PE_ID,
|
PE_ID,
|
||||||
NAME,
|
NAME,
|
||||||
@@ -123,6 +123,9 @@
|
|||||||
DEPARTMENT,
|
DEPARTMENT,
|
||||||
ITEM_ASSEM_CODE,
|
ITEM_ASSEM_CODE,
|
||||||
ITEM_ASSEM_NAME,
|
ITEM_ASSEM_NAME,
|
||||||
|
PE_DEPT_CODE,
|
||||||
|
PE_DEPT_NAME,
|
||||||
|
CONVERT ( VARCHAR ( 100 ), REGISTER_DATE, 20 ) REGISTER_DATE,
|
||||||
(case when Finished_sign = '1' then N'完成' when Finished_sign != '1' then N'未完成' end) FINISHED_SIGN,
|
(case when Finished_sign = '1' then N'完成' when Finished_sign != '1' then N'未完成' end) FINISHED_SIGN,
|
||||||
CONVERT ( VARCHAR ( 100 ), PE_DATE, 20 ) PE_DATE
|
CONVERT ( VARCHAR ( 100 ), PE_DATE, 20 ) PE_DATE
|
||||||
from V_PHYEXAM_PE_PERSONAL_ITEM
|
from V_PHYEXAM_PE_PERSONAL_ITEM
|
||||||
@@ -144,35 +147,25 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<select id="getConclusion" resultType="java.util.Map">
|
<select id="getConclusionAll" resultType="com.hospital.front.vo.ConclusionVo">
|
||||||
select PE_ID,
|
select PE_ID,
|
||||||
'' PE_VISIT_ID,
|
'' PE_VISIT_ID,
|
||||||
PE_DEPT_CODE,
|
PE_DEPT_CODE,
|
||||||
PE_DEPT_NAME,
|
PE_DEPT_NAME,
|
||||||
REPLACE(REPLACE(REPLACE(REPLACE(CONCLUSION_TEXT,CHAR(10),''), ' ', ''),char(9),''),char(13),'') CONCLUSION_TEXT
|
REPLACE(REPLACE(REPLACE(REPLACE(cast(CONCLUSION_TEXT as varchar(8000)),CHAR(10),''), ' ', ''),char(9),''),char(13),'') CONCLUSION_TEXT
|
||||||
from V_PHYEXAM_DEPT_RESULT
|
|
||||||
where PE_ID = #{peId}
|
|
||||||
and CONCLUSION_TEXT is not null
|
|
||||||
</select>
|
|
||||||
<select id="getConclusionAll" resultType="java.util.Map">
|
|
||||||
select PE_ID,
|
|
||||||
'' PE_VISIT_ID,
|
|
||||||
PE_DEPT_CODE,
|
|
||||||
PE_DEPT_NAME,
|
|
||||||
REPLACE(REPLACE(REPLACE(REPLACE(CONCLUSION_TEXT,CHAR(10),''), ' ', ''),char(9),''),char(13),'') CONCLUSION_TEXT
|
|
||||||
from V_PHYEXAM_DEPT_RESULT
|
from V_PHYEXAM_DEPT_RESULT
|
||||||
where PE_ID = #{peId}
|
where PE_ID = #{peId}
|
||||||
and CONCLUSION_TEXT is not null
|
and CONCLUSION_TEXT is not null
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getSuggest" resultType="java.util.Map">
|
<select id="getSuggestAll" resultType="com.hospital.front.vo.SuggestVo">
|
||||||
SELECT top 1
|
SELECT top 1
|
||||||
PE_ID,
|
PE_ID,
|
||||||
REPLACE(
|
REPLACE(
|
||||||
REPLACE(
|
REPLACE(
|
||||||
REPLACE(
|
REPLACE(
|
||||||
REPLACE(
|
REPLACE(
|
||||||
REPLACE(REPLACE(GUIDE_CONTENT, CHAR(10), '<br/>'), ' ', ''),
|
REPLACE(REPLACE(CAST (GUIDE_CONTENT AS VARCHAR (8000)), CHAR(10), '<br/>'), ' ', ''),
|
||||||
CHAR(9),''
|
CHAR(9),''
|
||||||
), CHAR(13), ''
|
), CHAR(13), ''
|
||||||
), '<br/><br/>','<br/>'
|
), '<br/><br/>','<br/>'
|
||||||
@@ -183,25 +176,7 @@
|
|||||||
where PE_ID = #{peId}
|
where PE_ID = #{peId}
|
||||||
AND REC_CONTENT IS NOT NULL
|
AND REC_CONTENT IS NOT NULL
|
||||||
</select>
|
</select>
|
||||||
<select id="getSuggestAll" resultType="java.util.Map">
|
<select id="getPeStatusByIdnoAndYear" resultType="com.hospital.front.vo.PeStatusVo">
|
||||||
SELECT top 1
|
|
||||||
PE_ID,
|
|
||||||
REPLACE(
|
|
||||||
REPLACE(
|
|
||||||
REPLACE(
|
|
||||||
REPLACE(
|
|
||||||
REPLACE(REPLACE(GUIDE_CONTENT, CHAR(10), '<br/>'), ' ', ''),
|
|
||||||
CHAR(9),''
|
|
||||||
), CHAR(13), ''
|
|
||||||
), '<br/><br/>','<br/>'
|
|
||||||
), '<br/>-谢谢合作祝您健康!<br/>',''
|
|
||||||
) GUIDE_CONTENT
|
|
||||||
FROM
|
|
||||||
V_PHYEXAM_DIAGNOSIS
|
|
||||||
where PE_ID = #{peId}
|
|
||||||
AND REC_CONTENT IS NOT NULL
|
|
||||||
</select>
|
|
||||||
<select id="getPeStatusByIdnoAndYear" resultType="java.util.Map">
|
|
||||||
SELECT
|
SELECT
|
||||||
PE_ID,
|
PE_ID,
|
||||||
NAME,
|
NAME,
|
||||||
@@ -314,23 +289,6 @@
|
|||||||
group by id_no,NAME,PE_ID,UNIT_NAME
|
group by id_no,NAME,PE_ID,UNIT_NAME
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getDisResultByAuditDate" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
|
||||||
select
|
|
||||||
ID_NO,
|
|
||||||
NAME,
|
|
||||||
PE_ID,
|
|
||||||
'' PE_VISIT_ID,
|
|
||||||
'' UNIT_CODE,
|
|
||||||
UNIT_NAME,
|
|
||||||
CONVERT(varchar(100),max(PE_QUEUE_DATE),20) PE_QUEUE_DATE,
|
|
||||||
CONVERT(varchar(100),max(AUDIT_DATE),20) AUDIT_DATE
|
|
||||||
from V_PHYEXAM_RESULT
|
|
||||||
where AUDIT_DATE >= CONVERT(DATETIME, #{auditDate})
|
|
||||||
AND AUDIT_DATE < DATEADD(DAY, 1, CONVERT(DATETIME, #{auditDate}))
|
|
||||||
group by id_no,NAME,PE_ID,UNIT_NAME
|
|
||||||
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="getItemByPeId" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
<select id="getItemByPeId" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
||||||
SELECT
|
SELECT
|
||||||
ITEM_ASSEM_CODE,
|
ITEM_ASSEM_CODE,
|
||||||
@@ -362,31 +320,6 @@
|
|||||||
and PE_RESULT is not null
|
and PE_RESULT is not null
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getResultByIdNoOrPeId" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
|
||||||
select
|
|
||||||
ID_NO,
|
|
||||||
NAME,
|
|
||||||
PE_ID,
|
|
||||||
'' PE_VISIT_ID,
|
|
||||||
'' UNIT_CODE,
|
|
||||||
UNIT_NAME,
|
|
||||||
CONVERT(varchar(100),max(PE_QUEUE_DATE),20) PE_QUEUE_DATE,
|
|
||||||
CONVERT(varchar(100),max(AUDIT_DATE),20) AUDIT_DATE
|
|
||||||
from V_PHYEXAM_RESULT
|
|
||||||
<where>
|
|
||||||
1=1
|
|
||||||
<if test="idNo != null and idNo != '' ">
|
|
||||||
and ID_NO = #{idNo}
|
|
||||||
</if>
|
|
||||||
<if test="peId != null and peId != '' ">
|
|
||||||
and PE_ID = #{peId}
|
|
||||||
</if>
|
|
||||||
|
|
||||||
and AUDIT_DATE >= CONVERT(DATETIME, #{auditYear} + '-01-01')
|
|
||||||
and AUDIT_DATE < DATEADD(YEAR, 1, CONVERT(DATETIME, #{auditYear} + '-01-01'))
|
|
||||||
</where>
|
|
||||||
group by id_no,NAME,PE_ID,UNIT_NAME
|
|
||||||
</select>
|
|
||||||
<select id="getResultByPeId" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
<select id="getResultByPeId" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
||||||
select
|
select
|
||||||
ID_NO,
|
ID_NO,
|
||||||
@@ -403,7 +336,7 @@
|
|||||||
group by id_no,NAME,PE_ID,UNIT_NAME
|
group by id_no,NAME,PE_ID,UNIT_NAME
|
||||||
</select>
|
</select>
|
||||||
<!-- 根据audit_date查询当天体检人员列表(去重) -->
|
<!-- 根据audit_date查询当天体检人员列表(去重) -->
|
||||||
<select id="getExamListByDate" resultType="java.util.Map">
|
<select id="getExamListByDate" resultType="com.hospital.front.vo.ExamListVo">
|
||||||
SELECT DISTINCT PE_ID AS "peId",
|
SELECT DISTINCT PE_ID AS "peId",
|
||||||
ID_NO AS "idNo",
|
ID_NO AS "idNo",
|
||||||
NAME AS "name",
|
NAME AS "name",
|
||||||
|
|||||||
@@ -1,414 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
||||||
<mapper namespace="com.hospital.front.mapper.ya.YaOrderMapper">
|
|
||||||
|
|
||||||
<!-- 健康探测 -->
|
|
||||||
<select id="ping" resultType="int">
|
|
||||||
SELECT 1
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<parameterMap id="pm" type="java.util.Map">
|
|
||||||
<parameter property="peInfoStr" jdbcType="VARCHAR" mode="IN"></parameter>
|
|
||||||
<parameter property="peItemStr" jdbcType="VARCHAR" mode="IN"></parameter>
|
|
||||||
<parameter property="resultCode" jdbcType="VARCHAR" mode="OUT"></parameter>
|
|
||||||
<parameter property="errorMsg" jdbcType="VARCHAR" mode="OUT"></parameter>
|
|
||||||
<parameter property="peId" jdbcType="VARCHAR" mode="OUT"></parameter>
|
|
||||||
</parameterMap>
|
|
||||||
<insert id="saveOrder" parameterMap="pm" statementType="CALLABLE">
|
|
||||||
{call P_OrderRegister_YARMYY(
|
|
||||||
#{peInfoStr,mode=IN,jdbcType=VARCHAR},
|
|
||||||
#{peItemStr,mode=IN,jdbcType=VARCHAR},
|
|
||||||
#{resultCode,mode=OUT,jdbcType=VARCHAR},
|
|
||||||
#{errorMsg,mode=OUT,jdbcType=VARCHAR},
|
|
||||||
#{peId,mode=OUT,jdbcType=VARCHAR}
|
|
||||||
)}
|
|
||||||
</insert>
|
|
||||||
|
|
||||||
<parameterMap id="cancenlPm" type="java.util.Map">
|
|
||||||
<parameter property="peId" jdbcType="VARCHAR" mode="IN"></parameter>
|
|
||||||
<parameter property="name" jdbcType="VARCHAR" mode="IN"></parameter>
|
|
||||||
<parameter property="idcard" jdbcType="VARCHAR" mode="IN"></parameter>
|
|
||||||
<parameter property="resultCode" jdbcType="VARCHAR" mode="OUT"></parameter>
|
|
||||||
<parameter property="errorMsg" jdbcType="VARCHAR" mode="OUT"></parameter>
|
|
||||||
</parameterMap>
|
|
||||||
<insert id="cancelOrder" parameterMap="cancenlPm" statementType="CALLABLE">
|
|
||||||
{call P_CANCELORDER(
|
|
||||||
#{peId,mode=IN,jdbcType=VARCHAR},
|
|
||||||
#{name,mode=IN,jdbcType=VARCHAR},
|
|
||||||
#{idcard,mode=IN,jdbcType=VARCHAR},
|
|
||||||
#{resultCode,mode=OUT,jdbcType=VARCHAR},
|
|
||||||
#{errorMsg,mode=OUT,jdbcType=VARCHAR}
|
|
||||||
)}
|
|
||||||
</insert>
|
|
||||||
|
|
||||||
<parameterMap id="unitPm" type="java.util.Map">
|
|
||||||
<parameter property="unitCode" jdbcType="VARCHAR" mode="IN"></parameter>
|
|
||||||
<parameter property="unitName" jdbcType="VARCHAR" mode="IN"></parameter>
|
|
||||||
<parameter property="resultCode" jdbcType="VARCHAR" mode="OUT"></parameter>
|
|
||||||
<parameter property="errorMsg" jdbcType="VARCHAR" mode="OUT"></parameter>
|
|
||||||
</parameterMap>
|
|
||||||
|
|
||||||
<insert id="addUnit" parameterMap="unitPm" statementType="CALLABLE">
|
|
||||||
{call P_ORGINFOUPDATE(
|
|
||||||
#{unitCode,mode=IN,jdbcType=VARCHAR},
|
|
||||||
#{unitName,mode=IN,jdbcType=VARCHAR},
|
|
||||||
#{resultCode,mode=OUT,jdbcType=VARCHAR},
|
|
||||||
#{errorMsg,mode=OUT,jdbcType=VARCHAR}
|
|
||||||
)}
|
|
||||||
</insert>
|
|
||||||
|
|
||||||
<select id="getPhyexamStatus" resultType="java.util.Map">
|
|
||||||
SELECT
|
|
||||||
t.tjh PE_ID,
|
|
||||||
t.xm NAME,
|
|
||||||
t.sfzh ID_NO,
|
|
||||||
t.dwmc UNIT_NAME,
|
|
||||||
t.dwbm DEPARTMENT,
|
|
||||||
(CONVERT(varchar(100), t.yyrq, 20)) PE_PRE_DATE,
|
|
||||||
(
|
|
||||||
CASE t.status_sum
|
|
||||||
WHEN 4 THEN N'终审'
|
|
||||||
WHEN 3 THEN N'初审'
|
|
||||||
WHEN 2 THEN N'检中'
|
|
||||||
WHEN 1 THEN N'报道'
|
|
||||||
WHEN 0 THEN N'预约'
|
|
||||||
ELSE N'预约'
|
|
||||||
END
|
|
||||||
) PE_STATUS
|
|
||||||
FROM
|
|
||||||
( SELECT tjh, xm, sfzh, dwmc, dwbm, yyrq,
|
|
||||||
( ISNULL(bdzt,0) + ISNULL(jczt,0) + ISNULL(cszt,0) + ISNULL(zszt,0) ) status_sum
|
|
||||||
FROM V_YGTJZT ) t
|
|
||||||
WHERE
|
|
||||||
CHARINDEX( ',' + t.tjh + ',', ',' + #{peIds} + ',' ) >0
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="getItemList" resultType="java.util.Map">
|
|
||||||
SELECT DISTINCT
|
|
||||||
tjzhxbm item_assem_code,
|
|
||||||
tjzhxmc item_assem_name
|
|
||||||
FROM
|
|
||||||
V_TJXMZH
|
|
||||||
</select>
|
|
||||||
<select id="getAllItemList" resultType="java.util.Map">
|
|
||||||
SELECT DISTINCT
|
|
||||||
tjzhxbm ITEM_ASSEM_CODE,
|
|
||||||
tjzhxmc ITEM_ASSEM_NAME,
|
|
||||||
tjzbxbm PE_ITEM_CODE,
|
|
||||||
tjzbxmc PE_ITEM_NAME
|
|
||||||
FROM
|
|
||||||
V_TJXMZH
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="getPeItemList" resultType="java.util.Map">
|
|
||||||
SELECT DISTINCT
|
|
||||||
tjzbxbm PE_ITEM_CODE,
|
|
||||||
tjzbxmc PE_ITEM_NAME
|
|
||||||
FROM
|
|
||||||
V_TJXMZH
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="getEmpItem" resultType="java.util.Map">
|
|
||||||
SELECT
|
|
||||||
TJH PE_ID,
|
|
||||||
XM NAME,
|
|
||||||
DWMC UNIT_NAME,
|
|
||||||
DWBM DEPARTMENT,
|
|
||||||
TJZHXBM ITEM_ASSEM_CODE,
|
|
||||||
TJZHXMC ITEM_ASSEM_NAME,
|
|
||||||
(case when WCZT = '1' then N'完成' when WCZT != '1' then N'未完成' end) FINISHED_SIGN,
|
|
||||||
CONVERT ( VARCHAR ( 100 ), JCWCSJ, 20 ) PE_DATE
|
|
||||||
from V_YGYYTJZHX
|
|
||||||
where TJH = #{peId}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="getItemInfo" resultType="java.util.Map">
|
|
||||||
SELECT
|
|
||||||
tjzhxbm ITEM_ASSEM_CODE,
|
|
||||||
tjzhxmc ITEM_ASSEM_NAME,
|
|
||||||
TJZBXBM PE_ITEM_CODE,
|
|
||||||
TJZBXMC PE_ITEM_NAME
|
|
||||||
FROM
|
|
||||||
V_TJXMZH
|
|
||||||
where tjzhxbm = #{peItemCode}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="getResult" resultType="java.util.Map">
|
|
||||||
SELECT
|
|
||||||
TJH PE_ID ,
|
|
||||||
XM NAME,
|
|
||||||
SFZH ID_NO,
|
|
||||||
DWMC UNIT_NAME,
|
|
||||||
DWBM PE_DEPT_NAME,
|
|
||||||
TJZHXBM ITEM_ASSEM_CODE,
|
|
||||||
TJZHXMC ITEM_ASSEM_NAME,
|
|
||||||
TJZBXBM PE_ITEM_CODE,
|
|
||||||
TJZBXMC PE_ITEM_NAME,
|
|
||||||
TJZBZ PE_RESULT,
|
|
||||||
TJZBDW UNIT,
|
|
||||||
CONVERT(varchar(100),shsj,20) PE_QUEUE_DATE,
|
|
||||||
CONVERT(varchar(100),shsj,20) AUDIT_DATE,
|
|
||||||
CKFW PRINT_CONTEXT
|
|
||||||
FROM
|
|
||||||
V_TJBGJG
|
|
||||||
WHERE
|
|
||||||
TJH = #{peId}
|
|
||||||
and TJZBXBM is not null
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="getConclusion" resultType="java.util.Map">
|
|
||||||
select TJH PE_ID,'' PE_VISIT_ID,
|
|
||||||
REPLACE(REPLACE(REPLACE(REPLACE(TJJL,CHAR(10),''), ' ', ''),char(9),''),char(13),'') CONCLUSION_TEXT
|
|
||||||
from V_TJJY
|
|
||||||
where TJH = #{peId}
|
|
||||||
and TJJL is not null
|
|
||||||
</select>
|
|
||||||
<select id="getConclusionAll" resultType="java.util.Map">
|
|
||||||
select TJH PE_ID,'' PE_VISIT_ID,
|
|
||||||
REPLACE(REPLACE(REPLACE(REPLACE(TJJL,CHAR(10),''), ' ', ''),char(9),''),char(13),'') CONCLUSION_TEXT
|
|
||||||
from V_TJJY
|
|
||||||
where TJH = #{peId}
|
|
||||||
and TJJL is not null
|
|
||||||
</select>
|
|
||||||
<!--<select id="getTotalConclusion" resultType="java.util.HashMap">
|
|
||||||
select wm_concat(PE_DEPT_NAME || ':' || REPLACE(REPLACE(REPLACE(to_lob(CONCLUSION_TEXT), chr(13) || chr(10), ''), chr(13), ''), chr(10), '') || '###') CONCLUSION_TEXT
|
|
||||||
from V_PHYEXAM_DEPT_RESULT
|
|
||||||
where PE_ID = #{peId}
|
|
||||||
and CONCLUSION_TEXT is not null
|
|
||||||
</select>-->
|
|
||||||
|
|
||||||
<select id="getSuggest" resultType="java.util.Map">
|
|
||||||
select TJH PE_ID,'' PE_VISIT_ID,
|
|
||||||
REPLACE(REPLACE(REPLACE(REPLACE(TJJL,CHAR(10),''), ' ', ''),char(9),''),char(13),'') REC_CONTENT,
|
|
||||||
REPLACE(REPLACE(REPLACE(REPLACE(TJJY,CHAR(10),''), ' ', ''),char(9),''),char(13),'') GUIDE_CONTENT
|
|
||||||
from V_TJJY
|
|
||||||
where TJH = #{peId}
|
|
||||||
and TJJL is not null
|
|
||||||
</select>
|
|
||||||
<select id="getSuggestAll" resultType="java.util.Map">
|
|
||||||
select TJH PE_ID,'' PE_VISIT_ID,
|
|
||||||
REPLACE(REPLACE(REPLACE(REPLACE(TJJL,CHAR(10),''), ' ', ''),char(9),''),char(13),'') REC_CONTENT,
|
|
||||||
REPLACE(REPLACE(REPLACE(REPLACE(TJJY,CHAR(10),''), ' ', ''),char(9),''),char(13),'') GUIDE_CONTENT
|
|
||||||
from V_TJJY
|
|
||||||
where TJH = #{peId}
|
|
||||||
and TJJL is not null
|
|
||||||
</select>
|
|
||||||
<!--<select id="getTotalSuggest" resultType="java.util.HashMap">
|
|
||||||
select wm_concat(REC_CONTENT || ':' || REPLACE(REPLACE(REPLACE(GUIDE_CONTENT, chr(13) || chr(10), ''), chr(13), ''), chr(10), '') || '###') GUIDE_CONTENT
|
|
||||||
from V_PHYEXAM_DIAGNOSIS
|
|
||||||
where PE_ID = #{peId}
|
|
||||||
and GUIDE_CONTENT is not null
|
|
||||||
</select>-->
|
|
||||||
<select id="getPeStatusByIdnoAndYear" resultType="java.util.Map">
|
|
||||||
SELECT
|
|
||||||
t.tjh PE_ID,
|
|
||||||
t.xm NAME,
|
|
||||||
t.sfzh ID_NO,
|
|
||||||
t.dwmc UNIT_NAME,
|
|
||||||
t.dwbm DEPARTMENT,
|
|
||||||
(CONVERT(varchar(100), t.yyrq, 20)) PE_PRE_DATE,
|
|
||||||
(
|
|
||||||
CASE t.status_sum
|
|
||||||
WHEN 4 THEN N'终审'
|
|
||||||
WHEN 3 THEN N'初审'
|
|
||||||
WHEN 2 THEN N'检中'
|
|
||||||
WHEN 1 THEN N'报道'
|
|
||||||
WHEN 0 THEN N'预约'
|
|
||||||
ELSE N'预约'
|
|
||||||
END
|
|
||||||
) PE_STATUS
|
|
||||||
FROM
|
|
||||||
( SELECT tjh, xm, sfzh, dwmc, dwbm, yyrq,
|
|
||||||
( ISNULL(bdzt,0) + ISNULL(jczt,0) + ISNULL(cszt,0) + ISNULL(zszt,0) ) status_sum
|
|
||||||
FROM V_YGTJZT ) t
|
|
||||||
WHERE t.SFZH = #{idNo}
|
|
||||||
AND t.yyrq >= CONVERT(DATETIME, #{year} + '-01-01')
|
|
||||||
AND t.yyrq < DATEADD(YEAR, 1, CONVERT(DATETIME, #{year} + '-01-01'))
|
|
||||||
</select>
|
|
||||||
<select id="getResultByIdcardAndYear" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
|
||||||
SELECT
|
|
||||||
SFZH ID_NO,
|
|
||||||
XM NAME,
|
|
||||||
TJH PE_ID,
|
|
||||||
'' PE_VISIT_ID,
|
|
||||||
'' UNIT_CODE,
|
|
||||||
DWMC UNIT_NAME,
|
|
||||||
CONVERT(varchar(100),max(shsj),20) PE_QUEUE_DATE,
|
|
||||||
CONVERT(varchar(100),max(shsj),20) AUDIT_DATE
|
|
||||||
FROM
|
|
||||||
V_TJBGJG
|
|
||||||
WHERE
|
|
||||||
SFZH = #{ idNo }
|
|
||||||
AND SHSJ >= CONVERT(DATETIME, #{year} + '-01-01')
|
|
||||||
AND SHSJ < DATEADD(YEAR, 1, CONVERT(DATETIME, #{year} + '-01-01'))
|
|
||||||
group by SFZH,XM,TJH,DWMC
|
|
||||||
</select>
|
|
||||||
<select id="getResultByIdcardAndYearAll" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
|
||||||
SELECT
|
|
||||||
SFZH ID_NO,
|
|
||||||
XM NAME,
|
|
||||||
TJH PE_ID,
|
|
||||||
'' PE_VISIT_ID,
|
|
||||||
'' UNIT_CODE,
|
|
||||||
DWMC UNIT_NAME,
|
|
||||||
CONVERT(varchar(100),max(shsj),20) PE_QUEUE_DATE,
|
|
||||||
CONVERT(varchar(100),max(shsj),20) AUDIT_DATE
|
|
||||||
FROM
|
|
||||||
V_TJBGJG
|
|
||||||
WHERE
|
|
||||||
SFZH = #{ idNo }
|
|
||||||
AND SHSJ >= CONVERT(DATETIME, #{year} + '-01-01')
|
|
||||||
AND SHSJ < DATEADD(YEAR, 1, CONVERT(DATETIME, #{year} + '-01-01'))
|
|
||||||
group by SFZH,XM,TJH,DWMC
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<!--<select id="getResultByAuditDate" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
|
||||||
select *
|
|
||||||
from V_PHYEXAM_RESULT
|
|
||||||
where to_char(AUDIT_DATE,'yyyy-mm-dd')=#{auditDate}
|
|
||||||
</select>-->
|
|
||||||
|
|
||||||
<select id="getDisResultByAuditDate" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
|
||||||
SELECT
|
|
||||||
SFZH ID_NO,
|
|
||||||
XM NAME,
|
|
||||||
TJH PE_ID,
|
|
||||||
'' PE_VISIT_ID,
|
|
||||||
'' UNIT_CODE,
|
|
||||||
DWMC UNIT_NAME,
|
|
||||||
CONVERT(varchar(100),max(shsj),20) PE_QUEUE_DATE,
|
|
||||||
CONVERT(varchar(100),max(shsj),20) AUDIT_DATE
|
|
||||||
FROM
|
|
||||||
V_TJBGJG
|
|
||||||
WHERE
|
|
||||||
shsj >= CONVERT(DATETIME, #{auditDate})
|
|
||||||
AND shsj < DATEADD(DAY, 1, CONVERT(DATETIME, #{auditDate}))
|
|
||||||
GROUP BY SFZH,XM,TJH,DWMC
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="getItemByPeId" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
|
||||||
SELECT DISTINCT
|
|
||||||
TJZHXBM ITEM_ASSEM_CODE,
|
|
||||||
TJZHXMC ITEM_ASSEM_NAME,
|
|
||||||
TJZBXBM PE_ITEM_CODE,
|
|
||||||
TJZBXMC PE_ITEM_NAME,
|
|
||||||
TJZBZ PE_RESULT,
|
|
||||||
TJZBDW UNIT,
|
|
||||||
CKFW PRINT_CONTEXT
|
|
||||||
FROM
|
|
||||||
V_TJBGJG
|
|
||||||
WHERE
|
|
||||||
TJH = #{peId}
|
|
||||||
and TJZBXBM is not null
|
|
||||||
</select>
|
|
||||||
<select id="getItemByPeIdAll" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
|
||||||
SELECT DISTINCT
|
|
||||||
TJZHXBM ITEM_ASSEM_CODE,
|
|
||||||
TJZHXMC ITEM_ASSEM_NAME,
|
|
||||||
TJZBXBM PE_ITEM_CODE,
|
|
||||||
TJZBXMC PE_ITEM_NAME,
|
|
||||||
TJZBZ PE_RESULT,
|
|
||||||
TJZBDW UNIT,
|
|
||||||
CKFW PRINT_CONTEXT
|
|
||||||
FROM
|
|
||||||
V_TJBGJG
|
|
||||||
WHERE
|
|
||||||
TJH = #{peId}
|
|
||||||
and TJZBXBM is not null
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="getResultByIdNoOrPeId" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
|
||||||
SELECT
|
|
||||||
SFZH ID_NO,
|
|
||||||
XM NAME,
|
|
||||||
TJH PE_ID,
|
|
||||||
'' PE_VISIT_ID,
|
|
||||||
'' UNIT_CODE,
|
|
||||||
DWMC UNIT_NAME,
|
|
||||||
CONVERT(varchar(100),max(shsj),20) PE_QUEUE_DATE,
|
|
||||||
CONVERT(varchar(100),max(shsj),20) AUDIT_DATE
|
|
||||||
FROM
|
|
||||||
V_TJBGJG
|
|
||||||
<where>
|
|
||||||
1=1
|
|
||||||
<if test="idNo != null and idNo != '' ">
|
|
||||||
and SFZH = #{idNo}
|
|
||||||
</if>
|
|
||||||
<if test="peId != null and peId != '' ">
|
|
||||||
and TJH = #{peId}
|
|
||||||
</if>
|
|
||||||
|
|
||||||
and shsj >= CONVERT(DATETIME, #{auditYear} + '-01-01')
|
|
||||||
and shsj < DATEADD(YEAR, 1, CONVERT(DATETIME, #{auditYear} + '-01-01'))
|
|
||||||
</where>
|
|
||||||
GROUP BY SFZH,XM,TJH,DWMC
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="getPeIdAndDateByIdcard" resultType="java.util.Map">
|
|
||||||
SELECT
|
|
||||||
tjh pe_id,
|
|
||||||
CONVERT ( VARCHAR ( 100 ), JCWCSJ, 23 ) PE_PRE_DATE
|
|
||||||
FROM
|
|
||||||
V_YGYYTJZHX
|
|
||||||
<where>
|
|
||||||
<if test="idNo != null and idNo != '' ">
|
|
||||||
and SFZH = #{idNo}
|
|
||||||
</if>
|
|
||||||
and JCWCSJ >= CONVERT(DATETIME, CONVERT(VARCHAR(4), GETDATE(), 23) + '-01-01')
|
|
||||||
and JCWCSJ < DATEADD(YEAR, 1, CONVERT(DATETIME, CONVERT(VARCHAR(4), GETDATE(), 23) + '-01-01'))
|
|
||||||
</where>
|
|
||||||
order by JCWCSJ desc
|
|
||||||
|
|
||||||
</select>
|
|
||||||
<select id="getResultByPeId" resultType="com.hospital.front.vo.MedicalMiddleResultVo">
|
|
||||||
SELECT
|
|
||||||
SFZH ID_NO,
|
|
||||||
XM NAME,
|
|
||||||
TJH PE_ID,
|
|
||||||
'' PE_VISIT_ID,
|
|
||||||
'' UNIT_CODE,
|
|
||||||
DWMC UNIT_NAME,
|
|
||||||
CONVERT(varchar(100),max(shsj),20) PE_QUEUE_DATE,
|
|
||||||
CONVERT(varchar(100),max(shsj),20) AUDIT_DATE
|
|
||||||
FROM
|
|
||||||
V_TJBGJG
|
|
||||||
WHERE
|
|
||||||
TJH = #{peId}
|
|
||||||
GROUP BY SFZH,XM,TJH,DWMC
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="getPeIdAndDateByIdcardList" resultType="java.util.Map">
|
|
||||||
SELECT
|
|
||||||
distinct
|
|
||||||
tjh PE_ID,
|
|
||||||
CONVERT ( VARCHAR ( 100 ), JCWCSJ, 23 ) PE_PRE_DATE
|
|
||||||
FROM
|
|
||||||
V_YGYYTJZHX
|
|
||||||
<where>
|
|
||||||
<if test="idNo != null and idNo != '' ">
|
|
||||||
and SFZH = #{idNo}
|
|
||||||
</if>
|
|
||||||
and JCWCSJ >= CONVERT(DATETIME, CONVERT(VARCHAR(4), GETDATE(), 23) + '-01-01')
|
|
||||||
and JCWCSJ < DATEADD(YEAR, 1, CONVERT(DATETIME, CONVERT(VARCHAR(4), GETDATE(), 23) + '-01-01'))
|
|
||||||
</where>
|
|
||||||
|
|
||||||
</select>
|
|
||||||
<!-- 根据audit_date查询当天体检人员列表(去重) -->
|
|
||||||
<select id="getExamListByDate" resultType="java.util.Map">
|
|
||||||
SELECT DISTINCT TJH AS "peId",
|
|
||||||
SFZH AS "idNo",
|
|
||||||
XM AS "name",
|
|
||||||
CONVERT(VARCHAR(10), shsj, 23) AS "auditDate"
|
|
||||||
FROM V_TJBGJG
|
|
||||||
<where>
|
|
||||||
<choose>
|
|
||||||
<when test="auditDate != null and auditDate != ''">
|
|
||||||
CONVERT(VARCHAR(10), shsj, 23) = #{auditDate}
|
|
||||||
</when>
|
|
||||||
<otherwise>
|
|
||||||
CONVERT(VARCHAR(10), shsj, 23) = CONVERT(VARCHAR(10), GETDATE(), 23)
|
|
||||||
</otherwise>
|
|
||||||
</choose>
|
|
||||||
</where>
|
|
||||||
</select>
|
|
||||||
</mapper>
|
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<totalSizeCap>5GB</totalSizeCap>
|
<totalSizeCap>5GB</totalSizeCap>
|
||||||
</rollingPolicy>
|
</rollingPolicy>
|
||||||
<encoder>
|
<encoder>
|
||||||
<pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern>
|
<pattern>%date [%thread] %-5level [%logger{50}] - %msg%n</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<totalSizeCap>2GB</totalSizeCap>
|
<totalSizeCap>2GB</totalSizeCap>
|
||||||
</rollingPolicy>
|
</rollingPolicy>
|
||||||
<encoder>
|
<encoder>
|
||||||
<pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern>
|
<pattern>%date [%thread] %-5level [%logger{50}] - %msg%n</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
<level>ERROR</level>
|
<level>ERROR</level>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# 重新允许 TLSv1/TLSv1.1(仅本进程生效,为兼容老版 SQL Server 的登录加密)
|
||||||
|
jdk.tls.disabledAlgorithms=SSLv3, RC4, DES, MD5withRSA, \
|
||||||
|
DH keySize < 1024, EC keySize < 224, 3DES_EDE_CBC, anon, NULL
|
||||||
Reference in New Issue
Block a user