From d659c4c03094b5fbf4866c4d9271a3c881907fb6 Mon Sep 17 00:00:00 2001 From: lianlonggang Date: Thu, 27 Aug 2026 15:22:01 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20Kotlin=E2=86=92Java=20=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=20+=20Agent=20=E6=8B=86=E5=88=86=E4=B8=BA=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=20Python=20=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 业务代码全部迁移至 src/main/java(Lombok 替代 Kotlin 数据类),pom 移除 Kotlin 依赖 - Agent(WS客户端/看门狗/日志流/升级)拆出至 hospital-front-agent 独立 Python 仓库,本仓库删除 agent 包 - pac/ 部署目录移除:app-jinghe.yml 迁入 resources,部署脚本与配置由 agent 仓库及部署文档承接 - BsmOrderMapper.xml SQL 调整、app.yml 更新 Co-Authored-By: Claude --- pac/conf-example/README.md | 49 --- pac/conf-example/agent.properties | 29 -- pac/conf-example/app-ningxia.yml | 18 - pac/conf-example/app-qingyang.yml | 32 -- pac/conf-example/app-xinglongyuan.yml | 32 -- pac/conf-example/app-yanan.yml | 18 - pom.xml | 62 +-- .../com/hospital/front/HospitalFrontApp.java | 64 +++ .../front/adapter/BsmAdapterBase.java | 203 +++++++++ .../hospital/front/adapter/DevAdapter.java | 165 +++++++ .../front/adapter/HospitalAdapter.java | 106 +++++ .../front/adapter/HospitalRouter.java | 75 ++++ .../hospital/front/adapter/JingheAdapter.java | 20 + .../front/adapter/NingxiaAdapter.java | 23 + .../hospital/front/adapter/NxAdapterBase.java | 199 +++++++++ .../front/adapter/QingyangAdapter.java | 22 + .../front/adapter/XingLongYuanAdapter.java | 20 + .../hospital/front/adapter/YaAdapterBase.java | 202 +++++++++ .../hospital/front/adapter/YananAdapter.java | 20 + .../hospital/front/api/admin/AdminApi.java | 276 ++++++++++++ .../hospital/front/api/order/OrderApi.java | 216 ++++++++++ .../com/hospital/front/dto/ProcResult.java | 71 +++ .../java/com/hospital/front/dto/Result.java | 119 +++++ .../front/infra/config/AppConfig.java | 41 ++ .../infra/exception/BusinessException.java | 58 +++ .../exception/GlobalExceptionHandler.java | 70 +++ .../hospital/front/infra/util/DateUtils.java | 58 +++ .../front/mapper/bsm/BsmOrderMapper.java | 104 +++++ .../front/mapper/nx/NxOrderMapper.java | 104 +++++ .../front/mapper/ya/YaOrderMapper.java | 104 +++++ .../hospital/front/service/OrderService.java | 406 ++++++++++++++++++ .../hospital/front/task/HeartbeatTask.java} | 26 +- .../hospital/front/task/UptimeKumaTask.java | 70 +++ .../front/vo/MedicalMiddleResultVo.java | 35 ++ .../com/hospital/front/vo/OrderItemVo.java | 57 +++ .../java/com/hospital/front/vo/OrderV2Vo.java | 230 ++++++++++ .../java/com/hospital/front/vo/OrderVo.java | 139 ++++++ .../java/com/hospital/front/vo/UnitVo.java | 50 +++ .../kotlin/com/hospital/agent/AgentConfig.kt | 56 --- .../kotlin/com/hospital/agent/AgentMain.kt | 74 ---- .../hospital/agent/cmd/CommandDispatcher.kt | 113 ----- .../hospital/agent/cmd/LogStreamManager.kt | 119 ----- .../com/hospital/agent/cmd/UpgradeCommand.kt | 296 ------------- .../com/hospital/agent/ipc/MainAppClient.kt | 101 ----- .../hospital/agent/process/ProcessGuard.kt | 93 ---- .../com/hospital/agent/ws/AgentClient.kt | 145 ------- .../com/hospital/front/HospitalFrontApp.kt | 40 -- .../hospital/front/adapter/BsmAdapterBase.kt | 89 ---- .../com/hospital/front/adapter/DevAdapter.kt | 56 --- .../hospital/front/adapter/HospitalAdapter.kt | 100 ----- .../hospital/front/adapter/HospitalRouter.kt | 43 -- .../hospital/front/adapter/JingheAdapter.kt | 14 - .../hospital/front/adapter/NingxiaAdapter.kt | 17 - .../hospital/front/adapter/NxAdapterBase.kt | 89 ---- .../hospital/front/adapter/QingyangAdapter.kt | 16 - .../front/adapter/XingLongYuanAdapter.kt | 14 - .../hospital/front/adapter/YaAdapterBase.kt | 88 ---- .../hospital/front/adapter/YananAdapter.kt | 14 - .../com/hospital/front/api/admin/AdminApi.kt | 212 --------- .../com/hospital/front/api/order/OrderApi.kt | 183 -------- .../com/hospital/front/dto/ProcResult.kt | 17 - .../kotlin/com/hospital/front/dto/Result.kt | 69 --- .../hospital/front/infra/config/AppConfig.kt | 23 - .../infra/exception/GlobalExceptionHandler.kt | 73 ---- .../hospital/front/infra/util/DateUtils.kt | 42 -- .../front/mapper/bsm/BsmOrderMapper.kt | 101 ----- .../hospital/front/mapper/nx/NxOrderMapper.kt | 101 ----- .../hospital/front/mapper/ya/YaOrderMapper.kt | 101 ----- .../hospital/front/service/OrderService.kt | 260 ----------- .../front/vo/MedicalMiddleResultVo.kt | 30 -- .../kotlin/com/hospital/front/vo/OrderVo.kt | 169 -------- .../main/resources}/app-jinghe.yml | 2 +- src/main/resources/app.yml | 17 +- .../front/mapper/bsm/BsmOrderMapper.xml | 112 +++-- 74 files changed, 3432 insertions(+), 3250 deletions(-) delete mode 100644 pac/conf-example/README.md delete mode 100644 pac/conf-example/agent.properties delete mode 100644 pac/conf-example/app-ningxia.yml delete mode 100644 pac/conf-example/app-qingyang.yml delete mode 100644 pac/conf-example/app-xinglongyuan.yml delete mode 100644 pac/conf-example/app-yanan.yml create mode 100644 src/main/java/com/hospital/front/HospitalFrontApp.java create mode 100644 src/main/java/com/hospital/front/adapter/BsmAdapterBase.java create mode 100644 src/main/java/com/hospital/front/adapter/DevAdapter.java create mode 100644 src/main/java/com/hospital/front/adapter/HospitalAdapter.java create mode 100644 src/main/java/com/hospital/front/adapter/HospitalRouter.java create mode 100644 src/main/java/com/hospital/front/adapter/JingheAdapter.java create mode 100644 src/main/java/com/hospital/front/adapter/NingxiaAdapter.java create mode 100644 src/main/java/com/hospital/front/adapter/NxAdapterBase.java create mode 100644 src/main/java/com/hospital/front/adapter/QingyangAdapter.java create mode 100644 src/main/java/com/hospital/front/adapter/XingLongYuanAdapter.java create mode 100644 src/main/java/com/hospital/front/adapter/YaAdapterBase.java create mode 100644 src/main/java/com/hospital/front/adapter/YananAdapter.java create mode 100644 src/main/java/com/hospital/front/api/admin/AdminApi.java create mode 100644 src/main/java/com/hospital/front/api/order/OrderApi.java create mode 100644 src/main/java/com/hospital/front/dto/ProcResult.java create mode 100644 src/main/java/com/hospital/front/dto/Result.java create mode 100644 src/main/java/com/hospital/front/infra/config/AppConfig.java create mode 100644 src/main/java/com/hospital/front/infra/exception/BusinessException.java create mode 100644 src/main/java/com/hospital/front/infra/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/hospital/front/infra/util/DateUtils.java create mode 100644 src/main/java/com/hospital/front/mapper/bsm/BsmOrderMapper.java create mode 100644 src/main/java/com/hospital/front/mapper/nx/NxOrderMapper.java create mode 100644 src/main/java/com/hospital/front/mapper/ya/YaOrderMapper.java create mode 100644 src/main/java/com/hospital/front/service/OrderService.java rename src/main/{kotlin/com/hospital/front/task/HeartbeatTask.kt => java/com/hospital/front/task/HeartbeatTask.java} (52%) create mode 100644 src/main/java/com/hospital/front/task/UptimeKumaTask.java create mode 100644 src/main/java/com/hospital/front/vo/MedicalMiddleResultVo.java create mode 100644 src/main/java/com/hospital/front/vo/OrderItemVo.java create mode 100644 src/main/java/com/hospital/front/vo/OrderV2Vo.java create mode 100644 src/main/java/com/hospital/front/vo/OrderVo.java create mode 100644 src/main/java/com/hospital/front/vo/UnitVo.java delete mode 100644 src/main/kotlin/com/hospital/agent/AgentConfig.kt delete mode 100644 src/main/kotlin/com/hospital/agent/AgentMain.kt delete mode 100644 src/main/kotlin/com/hospital/agent/cmd/CommandDispatcher.kt delete mode 100644 src/main/kotlin/com/hospital/agent/cmd/LogStreamManager.kt delete mode 100644 src/main/kotlin/com/hospital/agent/cmd/UpgradeCommand.kt delete mode 100644 src/main/kotlin/com/hospital/agent/ipc/MainAppClient.kt delete mode 100644 src/main/kotlin/com/hospital/agent/process/ProcessGuard.kt delete mode 100644 src/main/kotlin/com/hospital/agent/ws/AgentClient.kt delete mode 100644 src/main/kotlin/com/hospital/front/HospitalFrontApp.kt delete mode 100644 src/main/kotlin/com/hospital/front/adapter/BsmAdapterBase.kt delete mode 100644 src/main/kotlin/com/hospital/front/adapter/DevAdapter.kt delete mode 100644 src/main/kotlin/com/hospital/front/adapter/HospitalAdapter.kt delete mode 100644 src/main/kotlin/com/hospital/front/adapter/HospitalRouter.kt delete mode 100644 src/main/kotlin/com/hospital/front/adapter/JingheAdapter.kt delete mode 100644 src/main/kotlin/com/hospital/front/adapter/NingxiaAdapter.kt delete mode 100644 src/main/kotlin/com/hospital/front/adapter/NxAdapterBase.kt delete mode 100644 src/main/kotlin/com/hospital/front/adapter/QingyangAdapter.kt delete mode 100644 src/main/kotlin/com/hospital/front/adapter/XingLongYuanAdapter.kt delete mode 100644 src/main/kotlin/com/hospital/front/adapter/YaAdapterBase.kt delete mode 100644 src/main/kotlin/com/hospital/front/adapter/YananAdapter.kt delete mode 100644 src/main/kotlin/com/hospital/front/api/admin/AdminApi.kt delete mode 100644 src/main/kotlin/com/hospital/front/api/order/OrderApi.kt delete mode 100644 src/main/kotlin/com/hospital/front/dto/ProcResult.kt delete mode 100644 src/main/kotlin/com/hospital/front/dto/Result.kt delete mode 100644 src/main/kotlin/com/hospital/front/infra/config/AppConfig.kt delete mode 100644 src/main/kotlin/com/hospital/front/infra/exception/GlobalExceptionHandler.kt delete mode 100644 src/main/kotlin/com/hospital/front/infra/util/DateUtils.kt delete mode 100644 src/main/kotlin/com/hospital/front/mapper/bsm/BsmOrderMapper.kt delete mode 100644 src/main/kotlin/com/hospital/front/mapper/nx/NxOrderMapper.kt delete mode 100644 src/main/kotlin/com/hospital/front/mapper/ya/YaOrderMapper.kt delete mode 100644 src/main/kotlin/com/hospital/front/service/OrderService.kt delete mode 100644 src/main/kotlin/com/hospital/front/vo/MedicalMiddleResultVo.kt delete mode 100644 src/main/kotlin/com/hospital/front/vo/OrderVo.kt rename {pac/conf-example => src/main/resources}/app-jinghe.yml (92%) diff --git a/pac/conf-example/README.md b/pac/conf-example/README.md deleted file mode 100644 index 169324e..0000000 --- a/pac/conf-example/README.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/pac/conf-example/agent.properties b/pac/conf-example/agent.properties deleted file mode 100644 index 6051a84..0000000 --- a/pac/conf-example/agent.properties +++ /dev/null @@ -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 \ No newline at end of file diff --git a/pac/conf-example/app-ningxia.yml b/pac/conf-example/app-ningxia.yml deleted file mode 100644 index 731e331..0000000 --- a/pac/conf-example/app-ningxia.yml +++ /dev/null @@ -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 diff --git a/pac/conf-example/app-qingyang.yml b/pac/conf-example/app-qingyang.yml deleted file mode 100644 index 17b61bb..0000000 --- a/pac/conf-example/app-qingyang.yml +++ /dev/null @@ -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 自动识别 diff --git a/pac/conf-example/app-xinglongyuan.yml b/pac/conf-example/app-xinglongyuan.yml deleted file mode 100644 index 5335525..0000000 --- a/pac/conf-example/app-xinglongyuan.yml +++ /dev/null @@ -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 自动识别 diff --git a/pac/conf-example/app-yanan.yml b/pac/conf-example/app-yanan.yml deleted file mode 100644 index 26b350e..0000000 --- a/pac/conf-example/app-yanan.yml +++ /dev/null @@ -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 diff --git a/pom.xml b/pom.xml index 6b1c899..d8a5f48 100644 --- a/pom.xml +++ b/pom.xml @@ -29,8 +29,6 @@ 17 17 17 - 2.0.21 - 17 23.3.0.23.09 @@ -42,6 +40,14 @@ + + + org.projectlombok + lombok + 1.18.34 + provided + + org.noear @@ -134,13 +140,6 @@ true - - - org.jetbrains.kotlin - kotlin-stdlib - ${kotlin.version} - - org.noear @@ -153,55 +152,12 @@ solon-serialization-snack3 - - - org.jetbrains.kotlin - kotlin-test-junit5 - ${kotlin.version} - test - hospital-front-${project.version} - ${project.basedir}/src/main/kotlin - ${project.basedir}/src/test/kotlin - - - org.jetbrains.kotlin - kotlin-maven-plugin - ${kotlin.version} - - - spring - - ${kotlin.jvmTarget} - - - - compile - - compile - - - - test-compile - - test-compile - - - - - - org.jetbrains.kotlin - kotlin-maven-allopen - ${kotlin.version} - - - - org.apache.maven.plugins @@ -217,7 +173,7 @@ false - com.hospital.front.HospitalFrontAppKt + com.hospital.front.HospitalFrontApp diff --git a/src/main/java/com/hospital/front/HospitalFrontApp.java b/src/main/java/com/hospital/front/HospitalFrontApp.java new file mode 100644 index 0000000..059cb8f --- /dev/null +++ b/src/main/java/com/hospital/front/HospitalFrontApp.java @@ -0,0 +1,64 @@ +package com.hospital.front; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +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 +public class HospitalFrontApp { + + /** + * 主入口。 + */ + public static void main(String[] args) { + // TODO 临时诊断代码:打印完整异常因果链,定位注入失败根因后移除 + try { + Solon.start(HospitalFrontApp.class, args); + } catch (Throwable e) { + Throwable t = e; + int depth = 0; + while (t != null && depth < 10) { + System.err.println("=== 因果链[" + depth + "]: " + t.getClass().getName() + ": " + t.getMessage()); + StackTraceElement[] stackTrace = t.getStackTrace(); + for (int i = 0; i < Math.min(10, stackTrace.length); i++) { + System.err.println(" at " + stackTrace[i]); + } + t = t.getCause(); + depth++; + } + System.exit(1); + } + long pid = ProcessHandle.current().pid(); + System.out.println("[HospitalFront] 启动完成,PID=" + pid); + // 写 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) { + } + System.out.println("[HospitalFront] 优雅停机中..."); + })); + } +} diff --git a/src/main/java/com/hospital/front/adapter/BsmAdapterBase.java b/src/main/java/com/hospital/front/adapter/BsmAdapterBase.java new file mode 100644 index 0000000..9280503 --- /dev/null +++ b/src/main/java/com/hospital/front/adapter/BsmAdapterBase.java @@ -0,0 +1,203 @@ +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; + +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 healthCheck() { + try { + return Result.success(mapper.ping() == 1); + } catch (Exception e) { + return Result.error(Result.CODE_DB_UNREACHABLE, "数据库连接失败:" + e.getMessage()); + } + } + + @Override + public ProcResult saveOrder(String peInfoStr, String peItemStr) { + Map 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 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 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> getPhyexamStatus(String peIds) { + return mapper.getPhyexamStatus(peIds); + } + + @Override + public List> getItemList() { + return mapper.getItemList(); + } + + @Override + public List> getAllItemList() { + return mapper.getAllItemList(); + } + + @Override + public List> getPeItemList() { + return mapper.getPeItemList(); + } + + @Override + public List> getItemInfo(String peItemCode) { + return mapper.getItemInfo(peItemCode); + } + + @Override + public List> getEmpItem(String peId) { + return mapper.getEmpItem(peId); + } + + @Override + public List> getResult(String peId) { + return mapper.getResult(peId); + } + + @Override + public List> getConclusion(String peId) { + return mapper.getConclusion(peId); + } + + @Override + public List> getConclusionAll(String peId) { + return mapper.getConclusionAll(peId); + } + + @Override + public List> getSuggest(String peId) { + return mapper.getSuggest(peId); + } + + @Override + public List> getSuggestAll(String peId) { + return mapper.getSuggestAll(peId); + } + + @Override + public Map getPeStatusByIdnoAndYear(String idNo, String year) { + return mapper.getPeStatusByIdnoAndYear(idNo, year); + } + + @Override + public List> getResultByIdcardAndYear(String idNo, String year) { + return mapper.getResultByIdcardAndYear(idNo, year); + } + + @Override + public List> getResultByIdcardAndYearAll(String idNo, String year) { + return mapper.getResultByIdcardAndYearAll(idNo, year); + } + + @Override + public List> getResultByPeId(String peId) { + return mapper.getResultByPeId(peId); + } + + @Override + public List> getDisResultByAuditDate(String auditDate) { + return mapper.getDisResultByAuditDate(auditDate); + } + + @Override + public List> getItemByPeId(String peId) { + return mapper.getItemByPeId(peId); + } + + @Override + public List> getItemByPeIdAll(String peId) { + return mapper.getItemByPeIdAll(peId); + } + + @Override + public List> getResultByIdNoOrPeId(String idNo, String peId, String auditYear) { + return mapper.getResultByIdNoOrPeId(idNo, peId, auditYear); + } + + @Override + public Map getPeIdAndDateByIdcard(String idNo) { + return mapper.getPeIdAndDateByIdcard(idNo); + } + + @Override + public List> getPeIdAndDateByIdcardList(String idNo) { + return mapper.getPeIdAndDateByIdcardList(idNo); + } + + @Override + public List> 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); + } +} diff --git a/src/main/java/com/hospital/front/adapter/DevAdapter.java b/src/main/java/com/hospital/front/adapter/DevAdapter.java new file mode 100644 index 0000000..cef8f69 --- /dev/null +++ b/src/main/java/com/hospital/front/adapter/DevAdapter.java @@ -0,0 +1,165 @@ +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; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * 开发环境适配器(hospital.active=dev 时激活)。 + * + * 不连接任何数据库:查询一律返回空列表,存储过程一律返回失败。 + * 用于本机无医院数据库时的启动与联调。 + */ +@Component +public class DevAdapter implements HospitalAdapter { + + @Override + public String code() { + return "dev"; + } + + @Override + public String displayName() { + return "开发环境(Mock)"; + } + + @Override + public String hmType() { + return "dev"; + } + + @Override + public Result healthCheck() { + return Result.success(true); + } + + @Override + public ProcResult saveOrder(String peInfoStr, String peItemStr) { + return new ProcResult("0", "开发环境不支持预约提交", null); + } + + @Override + public ProcResult cancelOrder(String peId, String name, String idcard) { + return new ProcResult("0", "开发环境不支持取消预约", null); + } + + @Override + public ProcResult addUnit(UnitVo unit, boolean useV2) { + return new ProcResult("0", "开发环境不支持单位同步", null); + } + + @Override + public List> getPhyexamStatus(String peIds) { + return Collections.emptyList(); + } + + @Override + public List> getItemList() { + return Collections.emptyList(); + } + + @Override + public List> getAllItemList() { + return Collections.emptyList(); + } + + @Override + public List> getPeItemList() { + return Collections.emptyList(); + } + + @Override + public List> getItemInfo(String peItemCode) { + return Collections.emptyList(); + } + + @Override + public List> getEmpItem(String peId) { + return Collections.emptyList(); + } + + @Override + public List> getResult(String peId) { + return Collections.emptyList(); + } + + @Override + public List> getConclusion(String peId) { + return Collections.emptyList(); + } + + @Override + public List> getConclusionAll(String peId) { + return Collections.emptyList(); + } + + @Override + public List> getSuggest(String peId) { + return Collections.emptyList(); + } + + @Override + public List> getSuggestAll(String peId) { + return Collections.emptyList(); + } + + @Override + public Map getPeStatusByIdnoAndYear(String idNo, String year) { + return null; + } + + @Override + public List> getResultByIdcardAndYear(String idNo, String year) { + return Collections.emptyList(); + } + + @Override + public List> getResultByIdcardAndYearAll(String idNo, String year) { + return Collections.emptyList(); + } + + @Override + public List> getResultByPeId(String peId) { + return Collections.emptyList(); + } + + @Override + public List> getDisResultByAuditDate(String auditDate) { + return Collections.emptyList(); + } + + @Override + public List> getItemByPeId(String peId) { + return Collections.emptyList(); + } + + @Override + public List> getItemByPeIdAll(String peId) { + return Collections.emptyList(); + } + + @Override + public List> getResultByIdNoOrPeId(String idNo, String peId, String auditYear) { + return Collections.emptyList(); + } + + @Override + public Map getPeIdAndDateByIdcard(String idNo) { + return null; + } + + @Override + public List> getPeIdAndDateByIdcardList(String idNo) { + return Collections.emptyList(); + } + + @Override + public List> getExamListByDate(String auditDate) { + return Collections.emptyList(); + } +} diff --git a/src/main/java/com/hospital/front/adapter/HospitalAdapter.java b/src/main/java/com/hospital/front/adapter/HospitalAdapter.java new file mode 100644 index 0000000..7f50282 --- /dev/null +++ b/src/main/java/com/hospital/front/adapter/HospitalAdapter.java @@ -0,0 +1,106 @@ +package com.hospital.front.adapter; + +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; + +/** + * 医院适配器接口(策略模式)。 + * + * 5 家医院各自实现本接口,通过 HospitalRouter 按医院编码路由。 + * 所有数据库差异(Oracle/SqlServer 方言、存储过程名)均封装在实现内; + * 存储过程的入参出参契约由医院 HIS 系统约定,不可修改。 + * + * 查询类方法统一返回 Map 列表(列名与医院视图/存储过程保持一致)。 + */ +public interface HospitalAdapter { + + /** 医院编码(拼音):xinglongyuan / qingyang / ningxia / yanan / jinghe / dev */ + String code(); + + /** 医院中文名(日志与展示用) */ + String displayName(); + + /** Mapper 方言类型:bsm(Oracle)/ nx / ya(SqlServer) */ + String hmType(); + + /** 轻量健康检查(SELECT 1 探测) */ + Result healthCheck(); + + // ================= 存储过程(写) ================= + + /** 提交预约(P_ADD_PE_REGIST) */ + ProcResult saveOrder(String peInfoStr, String peItemStr); + + /** 取消预约(P_DEL_PE_REGIST) */ + ProcResult cancelOrder(String peId, String name, String idcard); + + /** 添加/修改单位(P_ADD_UNIT) */ + ProcResult addUnit(UnitVo unit, boolean useV2); + + // ================= 查询(视图) ================= + + /** 按体检预约 ID(逗号分隔多个)查询体检状态 */ + List> getPhyexamStatus(String peIds); + + /** 医院所有体检项目(组合项) */ + List> getItemList(); + + /** 医院所有体检项目及指标项 */ + List> getAllItemList(); + + /** 医院所有体检指标项 */ + List> getPeItemList(); + + /** 按指标项 CODE 查询所属体检组合 */ + List> getItemInfo(String peItemCode); + + /** 员工体检项目 */ + List> getEmpItem(String peId); + + /** 员工体检结果 */ + List> getResult(String peId); + + /** 体检结论(列表 / 全量) */ + List> getConclusion(String peId); + + List> getConclusionAll(String peId); + + /** 体检建议(列表 / 全量) */ + List> getSuggest(String peId); + + List> getSuggestAll(String peId); + + /** 按身份证号 + 年份查询体检状态 */ + Map getPeStatusByIdnoAndYear(String idNo, String year); + + /** 按身份证号 + 年份查询体检结果(默认 / 全量) */ + List> getResultByIdcardAndYear(String idNo, String year); + + List> getResultByIdcardAndYearAll(String idNo, String year); + + /** 按体检编号查询体检结果 */ + List> getResultByPeId(String peId); + + /** 按审核日期查询体检报告(去重主表) */ + List> getDisResultByAuditDate(String auditDate); + + /** 按体检编号查询体检项目子表(默认 / 全量) */ + List> getItemByPeId(String peId); + + List> getItemByPeIdAll(String peId); + + /** 按身份证号或体检编号 + 审核年份查询体检结果 */ + List> getResultByIdNoOrPeId(String idNo, String peId, String auditYear); + + /** 按身份证号查询当年 peId 与预约日期(单条 / 列表) */ + Map getPeIdAndDateByIdcard(String idNo); + + List> getPeIdAndDateByIdcardList(String idNo); + + /** 按审核日期查询当天体检人员列表(去重;日期空则当天) */ + List> getExamListByDate(String auditDate); +} diff --git a/src/main/java/com/hospital/front/adapter/HospitalRouter.java b/src/main/java/com/hospital/front/adapter/HospitalRouter.java new file mode 100644 index 0000000..3128632 --- /dev/null +++ b/src/main/java/com/hospital/front/adapter/HospitalRouter.java @@ -0,0 +1,75 @@ +package com.hospital.front.adapter; + +import org.noear.solon.Solon; +import org.noear.solon.annotation.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 医院适配器路由。 + * + * 通过 Solon.context().getBeanOfType 在首次使用时动态获取所有 HospitalAdapter 实现, + * 避免依赖注入时序问题。 + */ +@Component +public class HospitalRouter { + + /** 启动激活的医院编码(默认 jinghe) */ + private final String activeCode = Solon.cfg().get("hospital.active", "jinghe"); + + /** 适配器映射(懒加载,等价于 Kotlin 的 by lazy) */ + private volatile Map map; + + public HospitalRouter() { + System.out.println("[HospitalRouter] 启动激活医院:" + activeCode); + } + + /** + * 动态拉取所有 HospitalAdapter 实现(每次调用都重新拉取,避免 bean 注入时序问题)。 + */ + private List listAdapters() { + return Solon.context().getBeansOfType(HospitalAdapter.class); + } + + /** 获取适配器映射(首次调用时构建) */ + private Map 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 registeredCodes() { + return getMap().keySet(); + } +} diff --git a/src/main/java/com/hospital/front/adapter/JingheAdapter.java b/src/main/java/com/hospital/front/adapter/JingheAdapter.java new file mode 100644 index 0000000..a963598 --- /dev/null +++ b/src/main/java/com/hospital/front/adapter/JingheAdapter.java @@ -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 "泾河医院"; + } +} diff --git a/src/main/java/com/hospital/front/adapter/NingxiaAdapter.java b/src/main/java/com/hospital/front/adapter/NingxiaAdapter.java new file mode 100644 index 0000000..d597df2 --- /dev/null +++ b/src/main/java/com/hospital/front/adapter/NingxiaAdapter.java @@ -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 "宁夏宝石花医院"; + } +} diff --git a/src/main/java/com/hospital/front/adapter/NxAdapterBase.java b/src/main/java/com/hospital/front/adapter/NxAdapterBase.java new file mode 100644 index 0000000..2b5774c --- /dev/null +++ b/src/main/java/com/hospital/front/adapter/NxAdapterBase.java @@ -0,0 +1,199 @@ +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; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * NX 方言适配器基类(SqlServer)。 + * + * 适用医院:ningxia(宁夏宝石花)。 + */ +public abstract class NxAdapterBase implements HospitalAdapter { + + /** 数据库 Mapper(Solon 按类型注入) */ + @Inject + protected NxOrderMapper mapper; + + @Override + public String hmType() { + return "nx"; + } + + @Override + public Result healthCheck() { + try { + return Result.success(mapper.ping() == 1); + } catch (Exception e) { + return Result.error(Result.CODE_DB_UNREACHABLE, "数据库连接失败:" + e.getMessage()); + } + } + + @Override + public ProcResult saveOrder(String peInfoStr, String peItemStr) { + Map 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 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 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> getPhyexamStatus(String peIds) { + return mapper.getPhyexamStatus(peIds); + } + + @Override + public List> getItemList() { + return mapper.getItemList(); + } + + @Override + public List> getAllItemList() { + return mapper.getAllItemList(); + } + + @Override + public List> getPeItemList() { + return mapper.getPeItemList(); + } + + @Override + public List> getItemInfo(String peItemCode) { + return mapper.getItemInfo(peItemCode); + } + + @Override + public List> getEmpItem(String peId) { + return mapper.getEmpItem(peId); + } + + @Override + public List> getResult(String peId) { + return mapper.getResult(peId); + } + + @Override + public List> getConclusion(String peId) { + return mapper.getConclusion(peId); + } + + @Override + public List> getConclusionAll(String peId) { + return mapper.getConclusionAll(peId); + } + + @Override + public List> getSuggest(String peId) { + return mapper.getSuggest(peId); + } + + @Override + public List> getSuggestAll(String peId) { + return mapper.getSuggestAll(peId); + } + + @Override + public Map getPeStatusByIdnoAndYear(String idNo, String year) { + return mapper.getPeStatusByIdnoAndYear(idNo, year); + } + + @Override + public List> getResultByIdcardAndYear(String idNo, String year) { + return mapper.getResultByIdcardAndYear(idNo, year); + } + + @Override + public List> getResultByIdcardAndYearAll(String idNo, String year) { + return mapper.getResultByIdcardAndYearAll(idNo, year); + } + + @Override + public List> getResultByPeId(String peId) { + return mapper.getResultByPeId(peId); + } + + @Override + public List> getDisResultByAuditDate(String auditDate) { + return mapper.getDisResultByAuditDate(auditDate); + } + + @Override + public List> getItemByPeId(String peId) { + return mapper.getItemByPeId(peId); + } + + @Override + public List> getItemByPeIdAll(String peId) { + return mapper.getItemByPeIdAll(peId); + } + + @Override + public List> getResultByIdNoOrPeId(String idNo, String peId, String auditYear) { + return mapper.getResultByIdNoOrPeId(idNo, peId, auditYear); + } + + @Override + public Map getPeIdAndDateByIdcard(String idNo) { + return mapper.getPeIdAndDateByIdcard(idNo); + } + + @Override + public List> getPeIdAndDateByIdcardList(String idNo) { + return mapper.getPeIdAndDateByIdcardList(idNo); + } + + @Override + public List> 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); + } +} diff --git a/src/main/java/com/hospital/front/adapter/QingyangAdapter.java b/src/main/java/com/hospital/front/adapter/QingyangAdapter.java new file mode 100644 index 0000000..374eb10 --- /dev/null +++ b/src/main/java/com/hospital/front/adapter/QingyangAdapter.java @@ -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 "庆阳医院"; + } +} diff --git a/src/main/java/com/hospital/front/adapter/XingLongYuanAdapter.java b/src/main/java/com/hospital/front/adapter/XingLongYuanAdapter.java new file mode 100644 index 0000000..7fd6cc6 --- /dev/null +++ b/src/main/java/com/hospital/front/adapter/XingLongYuanAdapter.java @@ -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 "兴隆园医院"; + } +} diff --git a/src/main/java/com/hospital/front/adapter/YaAdapterBase.java b/src/main/java/com/hospital/front/adapter/YaAdapterBase.java new file mode 100644 index 0000000..a9f3484 --- /dev/null +++ b/src/main/java/com/hospital/front/adapter/YaAdapterBase.java @@ -0,0 +1,202 @@ +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; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * YA 方言适配器基类(SqlServer)。 + * + * 适用医院:yanan(延安)。 + */ +public abstract class YaAdapterBase implements HospitalAdapter { + + /** 数据库 Mapper(Solon 按类型注入) */ + @Inject + protected YaOrderMapper mapper; + + @Override + public String hmType() { + return "ya"; + } + + @Override + public Result healthCheck() { + try { + return Result.success(mapper.ping() == 1); + } catch (Exception e) { + return Result.error(Result.CODE_DB_UNREACHABLE, "数据库连接失败:" + e.getMessage()); + } + } + + @Override + public ProcResult saveOrder(String peInfoStr, String peItemStr) { + Map 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 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 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> getPhyexamStatus(String peIds) { + return mapper.getPhyexamStatus(peIds); + } + + @Override + public List> getItemList() { + return mapper.getItemList(); + } + + @Override + public List> getAllItemList() { + return mapper.getAllItemList(); + } + + @Override + public List> getPeItemList() { + return mapper.getPeItemList(); + } + + @Override + public List> getItemInfo(String peItemCode) { + return mapper.getItemInfo(peItemCode); + } + + @Override + public List> getEmpItem(String peId) { + return mapper.getEmpItem(peId); + } + + @Override + public List> getResult(String peId) { + return mapper.getResult(peId); + } + + @Override + public List> getConclusion(String peId) { + return mapper.getConclusion(peId); + } + + @Override + public List> getConclusionAll(String peId) { + return mapper.getConclusionAll(peId); + } + + @Override + public List> getSuggest(String peId) { + return mapper.getSuggest(peId); + } + + @Override + public List> getSuggestAll(String peId) { + return mapper.getSuggestAll(peId); + } + + @Override + public Map getPeStatusByIdnoAndYear(String idNo, String year) { + return mapper.getPeStatusByIdnoAndYear(idNo, year); + } + + @Override + public List> getResultByIdcardAndYear(String idNo, String year) { + return mapper.getResultByIdcardAndYear(idNo, year); + } + + @Override + public List> getResultByIdcardAndYearAll(String idNo, String year) { + return mapper.getResultByIdcardAndYearAll(idNo, year); + } + + @Override + public List> getResultByPeId(String peId) { + return mapper.getResultByPeId(peId); + } + + @Override + public List> getDisResultByAuditDate(String auditDate) { + return mapper.getDisResultByAuditDate(auditDate); + } + + @Override + public List> getItemByPeId(String peId) { + return mapper.getItemByPeId(peId); + } + + @Override + public List> getItemByPeIdAll(String peId) { + return mapper.getItemByPeIdAll(peId); + } + + @Override + public List> getResultByIdNoOrPeId(String idNo, String peId, String auditYear) { + return mapper.getResultByIdNoOrPeId(idNo, peId, auditYear); + } + + @Override + public Map getPeIdAndDateByIdcard(String idNo) { + return mapper.getPeIdAndDateByIdcard(idNo); + } + + @Override + public List> getPeIdAndDateByIdcardList(String idNo) { + return mapper.getPeIdAndDateByIdcardList(idNo); + } + + @Override + public List> 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); + } +} diff --git a/src/main/java/com/hospital/front/adapter/YananAdapter.java b/src/main/java/com/hospital/front/adapter/YananAdapter.java new file mode 100644 index 0000000..dac78c1 --- /dev/null +++ b/src/main/java/com/hospital/front/adapter/YananAdapter.java @@ -0,0 +1,20 @@ +package com.hospital.front.adapter; + +import org.noear.solon.annotation.Component; + +/** + * 延安医院适配器(SqlServer,ya 方言)。 + */ +@Component +public class YananAdapter extends YaAdapterBase { + + @Override + public String code() { + return "yanan"; + } + + @Override + public String displayName() { + return "延安医院"; + } +} diff --git a/src/main/java/com/hospital/front/api/admin/AdminApi.java b/src/main/java/com/hospital/front/api/admin/AdminApi.java new file mode 100644 index 0000000..59fd32e --- /dev/null +++ b/src/main/java/com/hospital/front/api/admin/AdminApi.java @@ -0,0 +1,276 @@ +package com.hospital.front.api.admin; + +import java.io.File; +import java.lang.management.ManagementFactory; +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.exception.BusinessException; + +/** + * 主程序被控端 API(/api/admin)。 + * + * 供同机 Agent(环回地址)调用,实现远程运维: + * - 健康检查 / 版本查询 / JVM 指标 + * - 配置查看(脱敏) + * - 日志文件列表 / 尾部查看 / 全量下载 + * - 优雅重启 + * + * 鉴权:请求头 X-Admin-Token 与配置 admin.token 比对(未配置时放行,仅本机使用)。 + */ +@Controller +@Mapping("/api/admin") +public class AdminApi { + + @Inject + private HospitalRouter router; + + /** 日志目录(与 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(Result.CODE_AUTH_FAILED, "管理接口鉴权失败"); + } + } + + // ================= 1. 健康与指标 ================= + + /** 健康检查(含数据库探测) */ + @Mapping("/health") + public Result> health(Context ctx) { + checkToken(ctx); + Runtime mem = Runtime.getRuntime(); + Result db = router.active().healthCheck(); + Map out = new LinkedHashMap<>(); + out.put("status", db.getCode() == 0 ? "UP" : "DEGRADED"); + out.put("hospital", router.activeCode()); + out.put("hospitalName", router.active().displayName()); + out.put("registeredHospitals", router.registeredCodes()); + Map database = new HashMap<>(); + database.put("ok", db.getCode() == 0); + database.put("msg", db.getMsg()); + out.put("database", database); + Map 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> version(Context ctx) { + checkToken(ctx); + Package pkg = this.getClass().getPackage(); + Map 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> config(Context ctx) { + checkToken(ctx); + List keys = Arrays.asList( + "hospital.active", "hospital.id", + "hm.type", "hm.ver", "hm.allsecondFlag", "hm.occFlag", + "server.port", "admin.token", "log.path"); + Map conf = new LinkedHashMap<>(); + for (String key : keys) { + conf.put(key, Solon.cfg().get(key, "")); + } + // 数据源脱敏(db1! 的 ! 在 cfg 键中已剥离,两种写法都尝试) + Map 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 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 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>> 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> out = new ArrayList<>(); + for (File f : files) { + if (out.size() >= 50) { + break; + } + Map 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 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(); + } +} diff --git a/src/main/java/com/hospital/front/api/order/OrderApi.java b/src/main/java/com/hospital/front/api/order/OrderApi.java new file mode 100644 index 0000000..c314695 --- /dev/null +++ b/src/main/java/com/hospital/front/api/order/OrderApi.java @@ -0,0 +1,216 @@ +package com.hospital.front.api.order; + +import java.util.ArrayList; +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.MethodType; +import org.noear.snack.ONode; + +import com.hospital.front.dto.Result; +import com.hospital.front.infra.exception.BusinessException; +import com.hospital.front.service.OrderService; + +/** + * 体检预约业务接口,路径前缀 /order。 + * + * 路径与出参结构保持与原 hospitalmiddle 一致(调用方契约,不可修改)。 + * 根路径前缀为 /order,各方法 Mapping 为相对路径。 + */ +@Controller +@Mapping("/order") +public class OrderApi { + + @Inject + private OrderService service; + + // ================= 1. 预约提交 / 取消 ================= + + /** 4. 提交预约保存 */ + @Mapping(value = "/saveOrder", method = {MethodType.POST}) + public Result saveOrder(@Body String body) { + // Solon 对 Map 泛型 body 反序列化支持不佳,用 snack3 手动解析 + @SuppressWarnings("unchecked") + Map map = (Map) ONode.load(body).toObject(Map.class); + return Result.success(service.saveOrder(map)); + } + + /** 5. 取消体检预约 */ + @Mapping(value = "/cancelOrder/{peId}", method = {MethodType.POST}) + public Result cancelOrder( + @Path("peId") String peId, + @Param("name") String name, + @Param("idcard") String idcard) { + service.cancelOrder(peId, name, idcard); + return Result.success(); + } + + // ================= 2. 体检状态 ================= + + /** 3. 体检状态查询(多个 peId 逗号分隔) */ + @Mapping("/getPeStatus/{peIds}") + public Result>> getPeStatus(@Path("peIds") String peIds) { + return Result.success(service.getPhyexamStatus(peIds)); + } + + /** 13. 根据身份证号及年份获取当前体检状态及体检项状态 */ + @Mapping("/getPeStatusAndItemList/{idNo}/{year}") + public Result> getPeStatusAndItemList(@Path("idNo") String idNo, @Path("year") String year) { + return Result.success(service.getPeStatusByIdnoAndYear(idNo, year)); + } + + // ================= 3. 体检项目 ================= + + /** 1. 查询员工体检项目 */ + @Mapping("/getEmpItem/{peId}") + public Result>> getEmpItem(@Path("peId") String peId) { + return Result.success(service.getEmpItem(peId)); + } + + /** 2. 体检项目查询 */ + @Mapping("/getItemList") + public Result>> getItemList() { + return Result.success(service.getItemList()); + } + + /** 6. 体检指标项数据获取 */ + @Mapping("/getPeItemList") + public Result>> getPeItemList() { + return Result.success(service.getPeItemList()); + } + + /** 6.1 体检指标项及体检组合数据获取 */ + @Mapping("/getAllItemList") + public Result>> getAllItemList() { + return Result.success(service.getAllItemList()); + } + + /** 7. 根据体检指标项查询所属体检组合项 */ + @Mapping("/getItemInfo/{peItemCode}") + public Result>> getItemInfo(@Path("peItemCode") String peItemCode) { + return Result.success(service.getItemInfo(peItemCode)); + } + + // ================= 4. 体检结果 / 结论 / 建议 ================= + + /** 8. 根据体检编号获取员工体检结果 */ + @Mapping("/getResult/{peId}") + public Result>> getResult(@Path("peId") String peId) { + return Result.success(service.getResult(peId)); + } + + /** 9. 体检结论(汇总) */ + @Mapping("/getTotalConclusion/{peId}") + public Result> getTotalConclusion(@Path("peId") String peId) { + return Result.success(service.getTotalConclusion(peId)); + } + + /** 10. 体检建议(汇总) */ + @Mapping("/getTotalSuggest/{peId}") + public Result> getTotalSuggest(@Path("peId") String peId) { + return Result.success(service.getTotalSuggest(peId)); + } + + /** 11. 体检结论(列表) */ + @Mapping("/getConclusion/{peId}") + public Result>> getConclusion(@Path("peId") String peId) { + return Result.success(service.getConclusion(peId)); + } + + /** 12. 体检建议(列表) */ + @Mapping("/getSuggest/{peId}") + public Result>> getSuggest(@Path("peId") String peId) { + return Result.success(service.getSuggest(peId)); + } + + // ================= 5. 按身份证 / 日期查询 ================= + + /** 14. 根据身份证号及年份获取体检结果 */ + @Mapping("/getResultByIdcardAndYear/{idNo}/{year}") + public Result>> getResultByIdcardAndYear(@Path("idNo") String idNo, @Path("year") String year) { + return Result.success(service.getResultByIdcardAndYear(idNo, year)); + } + + /** 14.1 根据身份证号及年份获取体检结果(全量) */ + @Mapping("/getResultByIdcardAndYearAll/{idNo}/{year}") + public Result>> getResultByIdcardAndYearAll(@Path("idNo") String idNo, @Path("year") String year) { + return Result.success(service.getResultByIdcardAndYearAll(idNo, year)); + } + + /** 15.1 根据体检编号获取体检结果 */ + @Mapping("/getResultByPeId/{peId}") + public Result>> getResultByPeId(@Path("peId") String peId) { + return Result.success(service.getResultByPeId(peId)); + } + + /** 15.2 根据体检编号获取体检状态(简) */ + @Mapping("/getResultStatus/{peId}") + public Result>> getResultStatus(@Path("peId") String peId) { + return Result.success(service.getResultByPeId(peId)); + } + + /** 15.3 根据 audit_date 查询当天的体检报告 */ + @Mapping("/getResultByAuditDate/{auditDate}") + public Result>> getResultByAuditDate(@Path("auditDate") String auditDate) { + return Result.success(service.getResultByAuditDate(auditDate)); + } + + /** 16. 根据 id_code 或 pe_id 和审查年份查询体检报告 */ + @Mapping(value = "/getResultByIdNoOrPeId", method = {MethodType.POST}) + public Result>> getResultByIdNoOrPeId( + @Param(value = "idNo", required = false) String idNo, + @Param(value = "peId", required = false) String peId, + @Param("auditYear") String auditYear) { + if (isNullOrBlank(idNo) && isNullOrBlank(peId)) { + throw BusinessException.paramMissing("身份证号或体检编号"); + } + return Result.success(service.getResultByIdNoOrPeId(idNo, peId, auditYear)); + } + + /** 17. 根据身份证号查询当前年的 peId 和预约日期 */ + @Mapping("/getPeIdAndDateByIdcard/{idNo}") + public Result> getPeIdAndDateByIdcard(@Path("idNo") String idNo) { + return Result.success(service.getPeIdAndDateByIdcard(idNo)); + } + + /** 17.1 同上(数组) */ + @Mapping("/getPeIdAndDateByIdcardList/{idNo}") + public Result>> getPeIdAndDateByIdcardList(@Path("idNo") String idNo) { + return Result.success(service.getPeIdAndDateByIdcardList(idNo)); + } + + // ================= 6. 单位 / 人员列表 ================= + + /** 18. 添加修改单位信息 */ + @Mapping(value = "/addUnit", method = {MethodType.POST}) + public Result addUnit(@Body String body) { + // Solon 对 List 泛型 body 反序列化支持不佳,此处用 snack3 手动解析 + List> units; + if (body == null || body.trim().isEmpty()) { + units = new ArrayList<>(); + } else { + @SuppressWarnings("unchecked") + List> parsed = (List>) ONode.load(body).toObject(List.class); + units = parsed; + } + String errors = service.addUnits(units); + return errors != null ? Result.error(errors) : Result.success(); + } + + /** 19. 根据日期查询当天体检人员列表(去重),日期为空则查当天 */ + @Mapping("/getExamListByDate") + public Result>> getExamListByDate(@Param("auditDate") String auditDate) { + return Result.success(service.getExamListByDate(auditDate)); + } + + /** Kotlin isNullOrBlank 的等价实现 */ + private static boolean isNullOrBlank(String s) { + return s == null || s.trim().isEmpty(); + } +} diff --git a/src/main/java/com/hospital/front/dto/ProcResult.java b/src/main/java/com/hospital/front/dto/ProcResult.java new file mode 100644 index 0000000..c0bfc2c --- /dev/null +++ b/src/main/java/com/hospital/front/dto/ProcResult.java @@ -0,0 +1,71 @@ +package com.hospital.front.dto; + +/** + * 医院存储过程执行结果。 + * + * @author hospital-front重构 + */ +public class ProcResult { + + /** 医院侧返回码("1" = 成功) */ + private String resultCode; + + /** 错误信息(失败时) */ + private String errorMsg; + + /** 预约成功后医院分配的体检编号(仅 saveOrder 有值) */ + private String peId; + + /** + * 无参构造器(各字段默认为 null,与 Kotlin 默认参数语义一致)。 + */ + public ProcResult() { + } + + /** + * 全参构造器。 + * + * @param resultCode 医院侧返回码("1" = 成功) + * @param errorMsg 错误信息(失败时) + * @param peId 预约成功后医院分配的体检编号 + */ + public ProcResult(String resultCode, String errorMsg, String peId) { + this.resultCode = resultCode; + this.errorMsg = errorMsg; + this.peId = peId; + } + + /** 医院侧是否执行成功 */ + public boolean isSuccess() { + return "1".equals(resultCode); + } + + public String getResultCode() { + return resultCode; + } + + public void setResultCode(String resultCode) { + this.resultCode = resultCode; + } + + public String getErrorMsg() { + return errorMsg; + } + + public void setErrorMsg(String errorMsg) { + this.errorMsg = errorMsg; + } + + public String getPeId() { + return peId; + } + + public void setPeId(String peId) { + this.peId = peId; + } + + @Override + public String toString() { + return "ProcResult(resultCode=" + resultCode + ", errorMsg=" + errorMsg + ", peId=" + peId + ")"; + } +} diff --git a/src/main/java/com/hospital/front/dto/Result.java b/src/main/java/com/hospital/front/dto/Result.java new file mode 100644 index 0000000..39b732b --- /dev/null +++ b/src/main/java/com/hospital/front/dto/Result.java @@ -0,0 +1,119 @@ +package com.hospital.front.dto; + +/** + * 统一 API 响应包装。 + * + * 业务接口均使用此结构返回,便于前端统一处理。 + * + * @param 数据类型 + * @author hospital-front重构 + */ +public class Result { + + /** 业务码(0=成功) */ + private final int code; + + /** 提示信息 */ + private final String msg; + + /** 业务数据 */ + private final T data; + + /** + * 全参构造器。 + * + * @param code 业务码(0=成功) + * @param msg 提示信息 + * @param data 业务数据(可为 null) + */ + public Result(int code, String msg, T data) { + this.code = code; + this.msg = msg; + this.data = data; + } + + /** + * 两参构造器(data 默认为 null,与 Kotlin 默认参数语义一致)。 + * + * @param code 业务码 + * @param msg 提示信息 + */ + public Result(int code, String msg) { + this(code, msg, null); + } + + /** 业务成功码 */ + public static final int SUCCESS_CODE = 0; + + /** 业务失败默认码 */ + public static final int ERROR_CODE = 5001; + + /** 参数缺失 */ + public static final int CODE_PARAM_MISSING = 1001; + + /** 参数格式错误 */ + public static final int CODE_PARAM_FORMAT = 1002; + + /** 业务校验失败 */ + public static final int CODE_BUSINESS = 1003; + + /** 存储过程调用失败 */ + public static final int CODE_PROC_FAILED = 2001; + + /** 数据库连接失败 */ + public static final int CODE_DB_UNREACHABLE = 2002; + + /** 医院系统返回业务失败 */ + public static final int CODE_HOSPITAL_FAILED = 2003; + + /** 鉴权失败 */ + public static final int CODE_AUTH_FAILED = 3001; + + /** Token 过期 */ + public static final int CODE_TOKEN_EXPIRED = 3002; + + /** + * 成功响应(无数据)。 + */ + public static Result success() { + return new Result<>(SUCCESS_CODE, "success", null); + } + + /** + * 成功响应。 + */ + public static Result success(T data) { + return new Result<>(SUCCESS_CODE, "success", data); + } + + /** + * 错误响应(默认错误码)。 + */ + public static Result error(String msg) { + return new Result<>(ERROR_CODE, msg, null); + } + + /** + * 错误响应(指定错误码)。 + */ + public static Result error(int code, String msg) { + return new Result<>(code, msg, null); + } + + public int getCode() { + return code; + } + + public String getMsg() { + return msg; + } + + public T getData() { + return data; + } + + @Override + public String toString() { + return "Result(code=" + code + ", msg=" + msg + ", data=" + data + ")"; + } +} diff --git a/src/main/java/com/hospital/front/infra/config/AppConfig.java b/src/main/java/com/hospital/front/infra/config/AppConfig.java new file mode 100644 index 0000000..f370f1d --- /dev/null +++ b/src/main/java/com/hospital/front/infra/config/AppConfig.java @@ -0,0 +1,41 @@ +package com.hospital.front.infra.config; + +import java.util.ArrayList; +import java.util.List; + +import org.noear.solon.annotation.BindProps; +import org.noear.solon.annotation.Configuration; + +/** + * 应用配置(Solon @ConfigurationProperties)。 + * + * 加载 `app.yml` 与 `pac/conf/app-{编码}.yml` 中的 hospital.* 配置。 + * + * @author hospital-front重构 + */ +@BindProps(prefix = "hospital") +@Configuration +public class AppConfig { + + /** 启动激活哪家医院 */ + private String active = "jinghe"; + + /** 注册的医院编码清单(启动自检) */ + private List adapters = new ArrayList<>(); + + public String getActive() { + return active; + } + + public void setActive(String active) { + this.active = active; + } + + public List getAdapters() { + return adapters; + } + + public void setAdapters(List adapters) { + this.adapters = adapters; + } +} diff --git a/src/main/java/com/hospital/front/infra/exception/BusinessException.java b/src/main/java/com/hospital/front/infra/exception/BusinessException.java new file mode 100644 index 0000000..3175fbe --- /dev/null +++ b/src/main/java/com/hospital/front/infra/exception/BusinessException.java @@ -0,0 +1,58 @@ +package com.hospital.front.infra.exception; + +import com.hospital.front.dto.Result; + +/** + * 业务异常。 + * + * 用于业务层显式抛出指定错误码的异常,会被 GlobalExceptionHandler 捕获并包装为 Result。 + * + * @author hospital-front重构 + */ +public class BusinessException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** 业务错误码(参见 Result 中的常量) */ + private final int code; + + /** + * 构造业务异常。 + * + * @param code 业务错误码 + * @param message 错误描述 + */ + public BusinessException(int code, String message) { + super(message); + this.code = code; + } + + /** + * 静态工厂方法。 + * + * @param code 业务错误码 + * @param message 错误描述 + */ + public static BusinessException of(int code, String message) { + return new BusinessException(code, message); + } + + /** 参数缺失异常 */ + public static BusinessException paramMissing(String field) { + return new BusinessException(Result.CODE_PARAM_MISSING, "参数缺失:" + field); + } + + /** 业务校验失败异常 */ + public static BusinessException businessFailed(String msg) { + return new BusinessException(Result.CODE_BUSINESS, msg); + } + + /** 存储过程调用失败异常 */ + public static BusinessException procFailed(String msg) { + return new BusinessException(Result.CODE_PROC_FAILED, msg); + } + + public int getCode() { + return code; + } +} diff --git a/src/main/java/com/hospital/front/infra/exception/GlobalExceptionHandler.java b/src/main/java/com/hospital/front/infra/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..e98f59a --- /dev/null +++ b/src/main/java/com/hospital/front/infra/exception/GlobalExceptionHandler.java @@ -0,0 +1,70 @@ +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 (BusinessException e) { + String msg = e.getMessage() != null ? e.getMessage() : "业务异常"; + log.warn("业务异常:code={}, msg={}", e.getCode(), e.getMessage()); + renderResult(ctx, Result.error(e.getCode(), msg)); + } catch (IllegalArgumentException e) { + String msg = e.getMessage() != null ? e.getMessage() : "参数错误"; + log.warn("参数异常:{}", e.getMessage()); + renderResult(ctx, Result.error(Result.CODE_PARAM_FORMAT, msg)); + } catch (Exception e) { + log.error("系统异常", e); + String msg = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName(); + renderResult(ctx, Result.error(Result.ERROR_CODE, "系统异常:" + msg)); + } catch (Throwable t) { + // 兜底:捕获 Error 及其它 Throwable(如 OOM),保证响应不悬挂 + log.error("严重异常(Throwable)", t); + renderResult(ctx, Result.error(Result.ERROR_CODE, "严重异常:" + t.getClass().getSimpleName())); + } + } + + /** + * 输出 Result JSON 响应。 + */ + private void renderResult(Context ctx, Result result) { + ctx.status(200); + ctx.contentType("application/json;charset=UTF-8"); + // 简易 JSON 序列化(避免引入 fastjson 依赖) + String dataJson = result.getData() != null ? toJsonValue(result.getData()) : "null"; + ctx.output("{\"code\":" + result.getCode() + + ",\"msg\":" + toJsonValue(result.getMsg()) + + ",\"data\":" + dataJson + "}"); + } + + /** + * 极简 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") + "\""; + } +} diff --git a/src/main/java/com/hospital/front/infra/util/DateUtils.java b/src/main/java/com/hospital/front/infra/util/DateUtils.java new file mode 100644 index 0000000..d5402b4 --- /dev/null +++ b/src/main/java/com/hospital/front/infra/util/DateUtils.java @@ -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); + } +} diff --git a/src/main/java/com/hospital/front/mapper/bsm/BsmOrderMapper.java b/src/main/java/com/hospital/front/mapper/bsm/BsmOrderMapper.java new file mode 100644 index 0000000..10a7fea --- /dev/null +++ b/src/main/java/com/hospital/front/mapper/bsm/BsmOrderMapper.java @@ -0,0 +1,104 @@ +package com.hospital.front.mapper.bsm; + +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 map); + + /** 取消预约(存储过程 P_DEL_PE_REGIST) */ + void cancelOrder(Map map); + + /** 添加/修改单位 V1(存储过程 P_ADD_UNIT) */ + void addUnit(Map map); + + /** 添加/修改单位 V2(存储过程 P_ADD_UNIT,8 入参版) */ + void addUnitV2(Map map); + + /** 按体检预约 ID(逗号分隔)查询体检状态 */ + List> getPhyexamStatus(@Param("peIds") String peIds); + + /** 医院所有体检项目(组合项) */ + List> getItemList(); + + /** 医院所有体检项目及指标项 */ + List> getAllItemList(); + + /** 医院所有体检指标项 */ + List> getPeItemList(); + + /** 员工体检项目 */ + List> getEmpItem(@Param("peId") String peId); + + /** 按指标项 CODE 查询体检组合 */ + List> getItemInfo(@Param("peItemCode") String peItemCode); + + /** 员工体检结果 */ + List> getResult(@Param("peId") String peId); + + /** 体检结论(列表) */ + List> getConclusion(@Param("peId") String peId); + + /** 体检结论(全量) */ + List> getConclusionAll(@Param("peId") String peId); + + /** 体检建议(列表) */ + List> getSuggest(@Param("peId") String peId); + + /** 体检建议(全量) */ + List> getSuggestAll(@Param("peId") String peId); + + /** 按身份证号及年份查询体检状态 */ + Map getPeStatusByIdnoAndYear(@Param("idNo") String idNo, @Param("year") String year); + + /** 按身份证号及年份查询体检结果 */ + List> getResultByIdcardAndYear(@Param("idNo") String idNo, @Param("year") String year); + + /** 按身份证号及年份查询体检结果(全量) */ + List> getResultByIdcardAndYearAll(@Param("idNo") String idNo, @Param("year") String year); + + /** 按体检编号查询体检结果 */ + List> getResultByPeId(@Param("peId") String peId); + + /** 按审核日期查询体检报告(去重) */ + List> getDisResultByAuditDate(@Param("auditDate") String auditDate); + + /** 按体检编号查询体检项目子表 */ + List> getItemByPeId(@Param("peId") String peId); + + /** 按体检编号查询体检项目子表(全量) */ + List> getItemByPeIdAll(@Param("peId") String peId); + + /** 按身份证号或体检编号 + 审核年份查询体检结果 */ + List> getResultByIdNoOrPeId( + @Param("idNo") String idNo, + @Param("peId") String peId, + @Param("auditYear") String auditYear + ); + + /** 按身份证号查询当年 peId 与预约日期 */ + Map getPeIdAndDateByIdcard(@Param("idNo") String idNo); + + /** 按身份证号查询当年 peId 与预约日期(数组) */ + List> getPeIdAndDateByIdcardList(@Param("idNo") String idNo); + + /** 按审核日期查询当天体检人员列表(去重) */ + List> getExamListByDate(@Param("auditDate") String auditDate); +} diff --git a/src/main/java/com/hospital/front/mapper/nx/NxOrderMapper.java b/src/main/java/com/hospital/front/mapper/nx/NxOrderMapper.java new file mode 100644 index 0000000..ceeef7c --- /dev/null +++ b/src/main/java/com/hospital/front/mapper/nx/NxOrderMapper.java @@ -0,0 +1,104 @@ +package com.hospital.front.mapper.nx; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * BSM 医院组 Mapper(Oracle 方言)。 + * + * 适用医院:ningxia(宁夏宝石花,SqlServer) + * XML 位置:classpath:mapper/nx/OrderMapper.xml + * + * 存储过程参数与医院 HIS 系统约定,不可修改。 + */ +@Mapper +public interface NxOrderMapper { + + /** 健康探测(SELECT 1) */ + int ping(); + + /** 预约提交(存储过程 P_ADD_PE_REGIST,出参 resultCode/errorMsg/peId) */ + void saveOrder(Map map); + + /** 取消预约(存储过程 P_DEL_PE_REGIST) */ + void cancelOrder(Map map); + + /** 添加/修改单位 V1(存储过程 P_ADD_UNIT) */ + void addUnit(Map map); + + /** 添加/修改单位 V2(存储过程 P_ADD_UNIT,8 入参版) */ + void addUnitV2(Map map); + + /** 按体检预约 ID(逗号分隔)查询体检状态 */ + List> getPhyexamStatus(@Param("peIds") String peIds); + + /** 医院所有体检项目(组合项) */ + List> getItemList(); + + /** 医院所有体检项目及指标项 */ + List> getAllItemList(); + + /** 医院所有体检指标项 */ + List> getPeItemList(); + + /** 员工体检项目 */ + List> getEmpItem(@Param("peId") String peId); + + /** 按指标项 CODE 查询体检组合 */ + List> getItemInfo(@Param("peItemCode") String peItemCode); + + /** 员工体检结果 */ + List> getResult(@Param("peId") String peId); + + /** 体检结论(列表) */ + List> getConclusion(@Param("peId") String peId); + + /** 体检结论(全量) */ + List> getConclusionAll(@Param("peId") String peId); + + /** 体检建议(列表) */ + List> getSuggest(@Param("peId") String peId); + + /** 体检建议(全量) */ + List> getSuggestAll(@Param("peId") String peId); + + /** 按身份证号及年份查询体检状态 */ + Map getPeStatusByIdnoAndYear(@Param("idNo") String idNo, @Param("year") String year); + + /** 按身份证号及年份查询体检结果 */ + List> getResultByIdcardAndYear(@Param("idNo") String idNo, @Param("year") String year); + + /** 按身份证号及年份查询体检结果(全量) */ + List> getResultByIdcardAndYearAll(@Param("idNo") String idNo, @Param("year") String year); + + /** 按体检编号查询体检结果 */ + List> getResultByPeId(@Param("peId") String peId); + + /** 按审核日期查询体检报告(去重) */ + List> getDisResultByAuditDate(@Param("auditDate") String auditDate); + + /** 按体检编号查询体检项目子表 */ + List> getItemByPeId(@Param("peId") String peId); + + /** 按体检编号查询体检项目子表(全量) */ + List> getItemByPeIdAll(@Param("peId") String peId); + + /** 按身份证号或体检编号 + 审核年份查询体检结果 */ + List> getResultByIdNoOrPeId( + @Param("idNo") String idNo, + @Param("peId") String peId, + @Param("auditYear") String auditYear + ); + + /** 按身份证号查询当年 peId 与预约日期 */ + Map getPeIdAndDateByIdcard(@Param("idNo") String idNo); + + /** 按身份证号查询当年 peId 与预约日期(数组) */ + List> getPeIdAndDateByIdcardList(@Param("idNo") String idNo); + + /** 按审核日期查询当天体检人员列表(去重) */ + List> getExamListByDate(@Param("auditDate") String auditDate); +} diff --git a/src/main/java/com/hospital/front/mapper/ya/YaOrderMapper.java b/src/main/java/com/hospital/front/mapper/ya/YaOrderMapper.java new file mode 100644 index 0000000..5ea9977 --- /dev/null +++ b/src/main/java/com/hospital/front/mapper/ya/YaOrderMapper.java @@ -0,0 +1,104 @@ +package com.hospital.front.mapper.ya; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * BSM 医院组 Mapper(Oracle 方言)。 + * + * 适用医院:yanan(延安,SqlServer) + * XML 位置:classpath:mapper/ya/OrderMapper.xml + * + * 存储过程参数与医院 HIS 系统约定,不可修改。 + */ +@Mapper +public interface YaOrderMapper { + + /** 健康探测(SELECT 1) */ + int ping(); + + /** 预约提交(存储过程 P_ADD_PE_REGIST,出参 resultCode/errorMsg/peId) */ + void saveOrder(Map map); + + /** 取消预约(存储过程 P_DEL_PE_REGIST) */ + void cancelOrder(Map map); + + /** 添加/修改单位 V1(存储过程 P_ADD_UNIT) */ + void addUnit(Map map); + + /** 添加/修改单位 V2(存储过程 P_ADD_UNIT,8 入参版) */ + void addUnitV2(Map map); + + /** 按体检预约 ID(逗号分隔)查询体检状态 */ + List> getPhyexamStatus(@Param("peIds") String peIds); + + /** 医院所有体检项目(组合项) */ + List> getItemList(); + + /** 医院所有体检项目及指标项 */ + List> getAllItemList(); + + /** 医院所有体检指标项 */ + List> getPeItemList(); + + /** 员工体检项目 */ + List> getEmpItem(@Param("peId") String peId); + + /** 按指标项 CODE 查询体检组合 */ + List> getItemInfo(@Param("peItemCode") String peItemCode); + + /** 员工体检结果 */ + List> getResult(@Param("peId") String peId); + + /** 体检结论(列表) */ + List> getConclusion(@Param("peId") String peId); + + /** 体检结论(全量) */ + List> getConclusionAll(@Param("peId") String peId); + + /** 体检建议(列表) */ + List> getSuggest(@Param("peId") String peId); + + /** 体检建议(全量) */ + List> getSuggestAll(@Param("peId") String peId); + + /** 按身份证号及年份查询体检状态 */ + Map getPeStatusByIdnoAndYear(@Param("idNo") String idNo, @Param("year") String year); + + /** 按身份证号及年份查询体检结果 */ + List> getResultByIdcardAndYear(@Param("idNo") String idNo, @Param("year") String year); + + /** 按身份证号及年份查询体检结果(全量) */ + List> getResultByIdcardAndYearAll(@Param("idNo") String idNo, @Param("year") String year); + + /** 按体检编号查询体检结果 */ + List> getResultByPeId(@Param("peId") String peId); + + /** 按审核日期查询体检报告(去重) */ + List> getDisResultByAuditDate(@Param("auditDate") String auditDate); + + /** 按体检编号查询体检项目子表 */ + List> getItemByPeId(@Param("peId") String peId); + + /** 按体检编号查询体检项目子表(全量) */ + List> getItemByPeIdAll(@Param("peId") String peId); + + /** 按身份证号或体检编号 + 审核年份查询体检结果 */ + List> getResultByIdNoOrPeId( + @Param("idNo") String idNo, + @Param("peId") String peId, + @Param("auditYear") String auditYear + ); + + /** 按身份证号查询当年 peId 与预约日期 */ + Map getPeIdAndDateByIdcard(@Param("idNo") String idNo); + + /** 按身份证号查询当年 peId 与预约日期(数组) */ + List> getPeIdAndDateByIdcardList(@Param("idNo") String idNo); + + /** 按审核日期查询当天体检人员列表(去重) */ + List> getExamListByDate(@Param("auditDate") String auditDate); +} diff --git a/src/main/java/com/hospital/front/service/OrderService.java b/src/main/java/com/hospital/front/service/OrderService.java new file mode 100644 index 0000000..3efefcf --- /dev/null +++ b/src/main/java/com/hospital/front/service/OrderService.java @@ -0,0 +1,406 @@ +package com.hospital.front.service; + +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.OrderItemVo; +import com.hospital.front.vo.OrderV2Vo; +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 PE_STATUS_DICT; + + static { + Map 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(Map body) { + OrderV2Vo vo = convertOrderV2(body); + 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("医院未返回体检编号"); + } + return result.getPeId(); + } else { + 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> units) { + StringBuilder errors = new StringBuilder(); + boolean useV2 = "V2".equals(getHmVer()); + for (int index = 0; index < units.size(); index++) { + Map 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> getEmpItem(String peId) { + return adapter().getEmpItem(peId); + } + + public List> getItemList() { + return adapter().getItemList(); + } + + public List> getAllItemList() { + return adapter().getAllItemList(); + } + + public List> getPeItemList() { + return adapter().getPeItemList(); + } + + public List> getItemInfo(String peItemCode) { + return adapter().getItemInfo(peItemCode); + } + + public List> getResult(String peId) { + return adapter().getResult(peId); + } + + public List> getConclusion(String peId) { + return adapter().getConclusion(peId); + } + + public List> getSuggest(String peId) { + return adapter().getSuggest(peId); + } + + public List> getResultByIdcardAndYear(String idNo, String year) { + return adapter().getResultByIdcardAndYear(idNo, year); + } + + public List> getResultByIdcardAndYearAll(String idNo, String year) { + return adapter().getResultByIdcardAndYearAll(idNo, year); + } + + public List> getResultByPeId(String peId) { + return adapter().getResultByPeId(peId); + } + + /** 按审核日期查询体检报告(去重主表) */ + public List> getResultByAuditDate(String auditDate) { + return adapter().getDisResultByAuditDate(auditDate); + } + + public List> getResultByIdNoOrPeId(String idNo, String peId, String auditYear) { + return adapter().getResultByIdNoOrPeId(idNo, peId, auditYear); + } + + public Map getPeIdAndDateByIdcard(String idNo) { + return adapter().getPeIdAndDateByIdcard(idNo); + } + + public List> getPeIdAndDateByIdcardList(String idNo) { + return adapter().getPeIdAndDateByIdcardList(idNo); + } + + /** + * 体检状态查询:按医院返回的 PE_STATUS 名转字典数字。 + */ + public List> getPhyexamStatus(String peIds) { + List> out = new ArrayList<>(); + for (Map row : adapter().getPhyexamStatus(peIds)) { + Map item = new HashMap<>(); + item.put("peId", row.get("PE_ID") != null ? String.valueOf(row.get("PE_ID")) : ""); + String statusName = row.get("PE_STATUS") != null ? String.valueOf(row.get("PE_STATUS")) : ""; + item.put("peStatus", PE_STATUS_DICT.getOrDefault(statusName, "")); + out.add(item); + } + return out; + } + + /** + * 按日期查询当天体检人员列表(日期空则当天)。 + */ + public List> getExamListByDate(String auditDate) { + return adapter().getExamListByDate(auditDate != null ? auditDate : DateUtils.today()); + } + + /** + * 体检结论(汇总为单字符串)。 + */ + public Map getTotalConclusion(String peId) { + Map map = new HashMap<>(); + map.put("totalConclusion", buildSummary(adapter().getConclusion(peId))); + return map; + } + + /** 体检结论(汇总,全量来源) */ + public Map getTotalConclusionAll(String peId) { + Map map = new HashMap<>(); + map.put("totalConclusion", buildSummary(adapter().getConclusionAll(peId))); + return map; + } + + /** + * 体检建议(汇总)。宁夏(nx)仅取首条 GUIDE_CONTENT。 + */ + public Map getTotalSuggest(String peId) { + Map map = new HashMap<>(); + map.put("totalSuggest", buildSuggest(adapter().getSuggest(peId), "nx".equals(adapter().hmType()))); + return map; + } + + /** 体检建议(汇总,全量来源) */ + public Map getTotalSuggestAll(String peId) { + Map map = new HashMap<>(); + map.put("totalSuggest", buildSuggest(adapter().getSuggestAll(peId), "nx".equals(adapter().hmType()))); + return map; + } + + /** + * 按身份证号 + 年份查询体检状态及项目(含宁夏体检报告兜底)。 + */ + public Map getPeStatusByIdnoAndYear(String idNo, String year) { + Map map = adapter().getPeStatusByIdnoAndYear(idNo, year); + if (map != null && map.get("PE_ID") != null) { + Map result = new LinkedHashMap<>(map); + result.put("peItemList", adapter().getEmpItem(String.valueOf(map.get("PE_ID")))); + return result; + } else { + // 宁夏:从体检报告继续查 + if ("nx".equals(adapter().hmType())) { + List> rows = adapter().getResultByIdcardAndYear(idNo, year); + if (!rows.isEmpty()) { + Map row = rows.get(0); + String auditDate = row.get("AUDIT_DATE") != null ? String.valueOf(row.get("AUDIT_DATE")) : null; + Map out = new LinkedHashMap<>(); + out.put("PE_ID", row.get("PE_ID")); + out.put("PE_PRE_DATE", + auditDate != null && auditDate.length() >= 10 ? auditDate.substring(0, 10) : auditDate); + out.put("peItemList", adapter().getEmpItem(String.valueOf(row.get("PE_ID")))); + return out; + } else { + return null; + } + } else { + return null; + } + } + } + + // ================= 私有工具 ================= + + /** 医院侧业务失败:抛出 2003 业务异常 */ + private void throwHospitalFailed(String errorMsg) { + if (errorMsg != null && !errorMsg.isEmpty()) { + throw BusinessException.of(2003, errorMsg); + } else { + throw BusinessException.procFailed("医院系统返回业务失败"); + } + } + + /** 结论汇总:1.科室:结论;
*/ + private String buildSummary(List> rows) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < rows.size(); i++) { + Map row = rows.get(i); + String deptName = row.get("PE_DEPT_NAME") != null ? String.valueOf(row.get("PE_DEPT_NAME")) : ""; + String dept = !deptName.isEmpty() ? deptName + ":" : ""; + Object conclusion = row.get("CONCLUSION_TEXT"); + sb.append(i + 1).append(".").append(dept).append(conclusion != null ? conclusion : "").append(";
"); + } + return sb.toString(); + } + + /** 建议汇总:宁夏仅取首条 GUIDE_CONTENT;其余 1.建议项:内容;
*/ + private String buildSuggest(List> rows, boolean nxStyle) { + if (nxStyle) { + if (rows.isEmpty() || rows.get(0).get("GUIDE_CONTENT") == null) { + return ""; + } + return String.valueOf(rows.get(0).get("GUIDE_CONTENT")); + } else { + StringBuilder sb = new StringBuilder(); + int i = 1; + for (Map row : rows) { + String guide = row.get("GUIDE_CONTENT") != null ? String.valueOf(row.get("GUIDE_CONTENT")) : ""; + if (guide.isEmpty()) { + continue; + } + Object rec = row.get("REC_CONTENT"); + sb.append(i).append(".").append(rec != null ? rec : "").append(":").append(guide).append(";
"); + i++; + } + return sb.toString(); + } + } + + /** Map → OrderV2Vo(数值/空值容错) */ + private OrderV2Vo convertOrderV2(Map body) { + List itemList = null; + Object rawList = body.get("itemList"); + if (rawList instanceof List) { + itemList = new ArrayList<>(); + for (Object item : (List) rawList) { + if (item instanceof Map) { + Map m = (Map) item; + itemList.add(new OrderItemVo( + m.get("itemNo") != null ? String.valueOf(m.get("itemNo")) : null, + m.get("itemName") != null ? String.valueOf(m.get("itemName")) : null)); + } + } + } + return new OrderV2Vo( + str(body, "orderDate"), + str(body, "secondDepartId"), + str(body, "secondDepartName"), + str(body, "thirdDepartName"), + str(body, "name"), + str(body, "sexName"), + str(body, "birthDay"), + body.get("age") instanceof Number ? ((Number) body.get("age")).intValue() : null, + str(body, "idCard"), + str(body, "marrigeStatusName"), + str(body, "country"), + str(body, "nationName"), + str(body, "birthplace"), + str(body, "position"), + str(body, "profession"), + str(body, "feeType"), + str(body, "liveSpace"), + str(body, "zipCode"), + str(body, "mobile"), + str(body, "email"), + str(body, "empSysno"), + str(body, "medicalType"), + str(body, "medicalClass"), + str(body, "workShape"), + str(body, "workDate"), + str(body, "jobLevel"), + str(body, "workPlace"), + str(body, "baseSite"), + str(body, "workTypeCode"), + str(body, "workTypeOther"), + str(body, "education"), + str(body, "title"), + str(body, "oldDepartName"), + str(body, "harmReasonCode"), + str(body, "harmReasonOther"), + str(body, "harmDate"), + str(body, "defendCancer"), + itemList); + } + + /** 取字符串值(null 安全) */ + private static String str(Map map, String key) { + Object v = map.get(key); + return v != null ? String.valueOf(v) : null; + } + + /** Map → UnitVo */ + private UnitVo convertUnit(Map 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")); + } +} diff --git a/src/main/kotlin/com/hospital/front/task/HeartbeatTask.kt b/src/main/java/com/hospital/front/task/HeartbeatTask.java similarity index 52% rename from src/main/kotlin/com/hospital/front/task/HeartbeatTask.kt rename to src/main/java/com/hospital/front/task/HeartbeatTask.java index 39af386..8bef88a 100644 --- a/src/main/kotlin/com/hospital/front/task/HeartbeatTask.kt +++ b/src/main/java/com/hospital/front/task/HeartbeatTask.java @@ -1,10 +1,12 @@ -package com.hospital.front.task +package com.hospital.front.task; -import com.hospital.front.adapter.HospitalRouter -import org.noear.solon.annotation.Component -import org.noear.solon.annotation.Inject -import org.noear.solon.scheduling.annotation.Scheduled -import org.slf4j.LoggerFactory +import lombok.extern.slf4j.Slf4j; + +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; /** * 心跳任务(占位)。 @@ -14,20 +16,20 @@ import org.slf4j.LoggerFactory * * @author hospital-front重构 */ +@Slf4j @Component -class HeartbeatTask { +public class HeartbeatTask { - private val log = LoggerFactory.getLogger(HeartbeatTask::class.java) @Inject - private lateinit var router: HospitalRouter + private HospitalRouter router; /** * 每 60 秒一次心跳(默认 cron:0/60)。 * Step 6 将改为上报 B 端的 WS 消息。 */ @Scheduled(cron = "0 * * * * *") - fun heartbeat() { - log.debug("[Heartbeat] hospital={}, uptime ok", router.activeCode()) + public void heartbeat() { + log.debug("[Heartbeat] hospital={}, uptime ok", router.activeCode()); } -} \ No newline at end of file +} diff --git a/src/main/java/com/hospital/front/task/UptimeKumaTask.java b/src/main/java/com/hospital/front/task/UptimeKumaTask.java new file mode 100644 index 0000000..fcd12fb --- /dev/null +++ b/src/main/java/com/hospital/front/task/UptimeKumaTask.java @@ -0,0 +1,70 @@ +package com.hospital.front.task; + +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +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; + + private final OkHttpClient http = new OkHttpClient.Builder() + .connectTimeout(5, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .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 health = router.active().healthCheck(); + if (health.getCode() == 0) { + try { + Response resp = http.newCall(new Request.Builder().url(kumaUrl).get().build()).execute(); + if (resp.isSuccessful()) { + log.info("[Kuma] 心跳推送成功:{}", kumaUrl); + } else { + log.warn("[Kuma] 心跳推送失败:HTTP {}", resp.code()); + } + resp.close(); + } catch (Exception e) { + log.warn("[Kuma] 心跳推送异常:{}", e.getMessage()); + } + } else { + log.warn("[Kuma] 数据库异常,跳过心跳:{}", health.getMsg()); + } + } +} diff --git a/src/main/java/com/hospital/front/vo/MedicalMiddleResultVo.java b/src/main/java/com/hospital/front/vo/MedicalMiddleResultVo.java new file mode 100644 index 0000000..45b1a61 --- /dev/null +++ b/src/main/java/com/hospital/front/vo/MedicalMiddleResultVo.java @@ -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 resultVoList; +} diff --git a/src/main/java/com/hospital/front/vo/OrderItemVo.java b/src/main/java/com/hospital/front/vo/OrderItemVo.java new file mode 100644 index 0000000..a556a19 --- /dev/null +++ b/src/main/java/com/hospital/front/vo/OrderItemVo.java @@ -0,0 +1,57 @@ +package com.hospital.front.vo; + +import java.util.List; + +/** + * 预约订单体检项目条目。 + * + * JSON 字段名与原 hospitalmiddle 的 OrderItemVo 保持一致(调用方契约,不可修改)。 + */ +public class OrderItemVo { + private String itemNo; + private String itemName; + + public OrderItemVo() { + } + + public OrderItemVo(String itemNo, String itemName) { + this.itemNo = itemNo; + this.itemName = itemName; + } + + public String getItemNo() { + return itemNo; + } + + public void setItemNo(String itemNo) { + this.itemNo = itemNo; + } + + public String getItemName() { + return itemName; + } + + public void setItemName(String itemName) { + this.itemName = itemName; + } + + /** + * 拼接子表字符串:多项之间用 & 分隔,项内 编号|名称 分隔。 + */ + public static String toPeItemStr(List 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(); + } +} diff --git a/src/main/java/com/hospital/front/vo/OrderV2Vo.java b/src/main/java/com/hospital/front/vo/OrderV2Vo.java new file mode 100644 index 0000000..98785b2 --- /dev/null +++ b/src/main/java/com/hospital/front/vo/OrderV2Vo.java @@ -0,0 +1,230 @@ +package com.hospital.front.vo; + +/** + * V2 版预约订单 VO(hm.ver=V2 时使用)。 + * + * 与 V1 的差异:工种拆为 编码+其它;危害因素拆为 编码+其它+接害时间。 + */ +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 java.util.List itemList; + + /** + * 全参构造器(保持与 Kotlin data class 相同的字段顺序,供 OrderService 组装用)。 + */ + public OrderV2Vo(String orderDate, String secondDepartId, String secondDepartName, String thirdDepartName, + String name, String sexName, String birthDay, Integer age, String idCard, + String marrigeStatusName, String country, String nationName, String birthplace, + String position, String profession, String feeType, String liveSpace, String zipCode, + String mobile, String email, String empSysno, String medicalType, String medicalClass, + String workShape, String workDate, String jobLevel, String workPlace, String baseSite, + String workTypeCode, String workTypeOther, String education, String title, + String oldDepartName, String harmReasonCode, String harmReasonOther, String harmDate, + String defendCancer, java.util.List itemList) { + this.orderDate = orderDate; + this.secondDepartId = secondDepartId; + this.secondDepartName = secondDepartName; + this.thirdDepartName = thirdDepartName; + this.name = name; + this.sexName = sexName; + this.birthDay = birthDay; + this.age = age; + this.idCard = idCard; + this.marrigeStatusName = marrigeStatusName; + this.country = country; + this.nationName = nationName; + this.birthplace = birthplace; + this.position = position; + this.profession = profession; + this.feeType = feeType; + this.liveSpace = liveSpace; + this.zipCode = zipCode; + this.mobile = mobile; + this.email = email; + this.empSysno = empSysno; + this.medicalType = medicalType; + this.medicalClass = medicalClass; + this.workShape = workShape; + this.workDate = workDate; + this.jobLevel = jobLevel; + this.workPlace = workPlace; + this.baseSite = baseSite; + this.workTypeCode = workTypeCode; + this.workTypeOther = workTypeOther; + this.education = education; + this.title = title; + this.oldDepartName = oldDepartName; + this.harmReasonCode = harmReasonCode; + this.harmReasonOther = harmReasonOther; + this.harmDate = harmDate; + this.defendCancer = defendCancer; + this.itemList = itemList; + } + + public OrderV2Vo() { + } + + public String getOrderDate() { return orderDate; } + public void setOrderDate(String v) { this.orderDate = v; } + public String getSecondDepartId() { return secondDepartId; } + public void setSecondDepartId(String v) { this.secondDepartId = v; } + public String getSecondDepartName() { return secondDepartName; } + public void setSecondDepartName(String v) { this.secondDepartName = v; } + public String getThirdDepartName() { return thirdDepartName; } + public void setThirdDepartName(String v) { this.thirdDepartName = v; } + public String getName() { return name; } + public void setName(String v) { this.name = v; } + public String getSexName() { return sexName; } + public void setSexName(String v) { this.sexName = v; } + public String getBirthDay() { return birthDay; } + public void setBirthDay(String v) { this.birthDay = v; } + public Integer getAge() { return age; } + public void setAge(Integer v) { this.age = v; } + public String getIdCard() { return idCard; } + public void setIdCard(String v) { this.idCard = v; } + public String getMarrigeStatusName() { return marrigeStatusName; } + public void setMarrigeStatusName(String v) { this.marrigeStatusName = v; } + public String getCountry() { return country; } + public void setCountry(String v) { this.country = v; } + public String getNationName() { return nationName; } + public void setNationName(String v) { this.nationName = v; } + public String getBirthplace() { return birthplace; } + public void setBirthplace(String v) { this.birthplace = v; } + public String getPosition() { return position; } + public void setPosition(String v) { this.position = v; } + public String getProfession() { return profession; } + public void setProfession(String v) { this.profession = v; } + public String getFeeType() { return feeType; } + public void setFeeType(String v) { this.feeType = v; } + public String getLiveSpace() { return liveSpace; } + public void setLiveSpace(String v) { this.liveSpace = v; } + public String getZipCode() { return zipCode; } + public void setZipCode(String v) { this.zipCode = v; } + public String getMobile() { return mobile; } + public void setMobile(String v) { this.mobile = v; } + public String getEmail() { return email; } + public void setEmail(String v) { this.email = v; } + public String getEmpSysno() { return empSysno; } + public void setEmpSysno(String v) { this.empSysno = v; } + public String getMedicalType() { return medicalType; } + public void setMedicalType(String v) { this.medicalType = v; } + public String getMedicalClass() { return medicalClass; } + public void setMedicalClass(String v) { this.medicalClass = v; } + public String getWorkShape() { return workShape; } + public void setWorkShape(String v) { this.workShape = v; } + public String getWorkDate() { return workDate; } + public void setWorkDate(String v) { this.workDate = v; } + public String getJobLevel() { return jobLevel; } + public void setJobLevel(String v) { this.jobLevel = v; } + public String getWorkPlace() { return workPlace; } + public void setWorkPlace(String v) { this.workPlace = v; } + public String getBaseSite() { return baseSite; } + public void setBaseSite(String v) { this.baseSite = v; } + public String getWorkTypeCode() { return workTypeCode; } + public void setWorkTypeCode(String v) { this.workTypeCode = v; } + public String getWorkTypeOther() { return workTypeOther; } + public void setWorkTypeOther(String v) { this.workTypeOther = v; } + public String getEducation() { return education; } + public void setEducation(String v) { this.education = v; } + public String getTitle() { return title; } + public void setTitle(String v) { this.title = v; } + public String getOldDepartName() { return oldDepartName; } + public void setOldDepartName(String v) { this.oldDepartName = v; } + public String getHarmReasonCode() { return harmReasonCode; } + public void setHarmReasonCode(String v) { this.harmReasonCode = v; } + public String getHarmReasonOther() { return harmReasonOther; } + public void setHarmReasonOther(String v) { this.harmReasonOther = v; } + public String getHarmDate() { return harmDate; } + public void setHarmDate(String v) { this.harmDate = v; } + public String getDefendCancer() { return defendCancer; } + public void setDefendCancer(String v) { this.defendCancer = v; } + public java.util.List getItemList() { return itemList; } + public void setItemList(java.util.List v) { this.itemList = v; } + + /** + * 按存储过程约定顺序拼接主表字符串(竖线分隔,空值为空串)。 + */ + 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; + } +} diff --git a/src/main/java/com/hospital/front/vo/OrderVo.java b/src/main/java/com/hospital/front/vo/OrderVo.java new file mode 100644 index 0000000..01bdf50 --- /dev/null +++ b/src/main/java/com/hospital/front/vo/OrderVo.java @@ -0,0 +1,139 @@ +package com.hospital.front.vo; + +/** + * V1 版预约订单 VO(hm.ver=V1 时使用)。 + * + * 字段名与字段顺序均与原 hospitalmiddle 的 OrderVo(Java)保持一致, + * 字段顺序即存储过程 P_ADD_PE_REGIST 的 peInfoStr 竖线分隔顺序,不可调整。 + */ +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 java.util.List itemList; + + public String getOrderDate() { return orderDate; } + public void setOrderDate(String v) { this.orderDate = v; } + public String getSecondDepartId() { return secondDepartId; } + public void setSecondDepartId(String v) { this.secondDepartId = v; } + public String getSecondDepartName() { return secondDepartName; } + public void setSecondDepartName(String v) { this.secondDepartName = v; } + public String getThirdDepartName() { return thirdDepartName; } + public void setThirdDepartName(String v) { this.thirdDepartName = v; } + public String getName() { return name; } + public void setName(String v) { this.name = v; } + public String getSexName() { return sexName; } + public void setSexName(String v) { this.sexName = v; } + public String getBirthDay() { return birthDay; } + public void setBirthDay(String v) { this.birthDay = v; } + public Integer getAge() { return age; } + public void setAge(Integer v) { this.age = v; } + public String getIdCard() { return idCard; } + public void setIdCard(String v) { this.idCard = v; } + public String getMarrigeStatusName() { return marrigeStatusName; } + public void setMarrigeStatusName(String v) { this.marrigeStatusName = v; } + public String getCountry() { return country; } + public void setCountry(String v) { this.country = v; } + public String getNationName() { return nationName; } + public void setNationName(String v) { this.nationName = v; } + public String getBirthplace() { return birthplace; } + public void setBirthplace(String v) { this.birthplace = v; } + public String getPosition() { return position; } + public void setPosition(String v) { this.position = v; } + public String getProfession() { return profession; } + public void setProfession(String v) { this.profession = v; } + public String getFeeType() { return feeType; } + public void setFeeType(String v) { this.feeType = v; } + public String getLiveSpace() { return liveSpace; } + public void setLiveSpace(String v) { this.liveSpace = v; } + public String getZipCode() { return zipCode; } + public void setZipCode(String v) { this.zipCode = v; } + public String getMobile() { return mobile; } + public void setMobile(String v) { this.mobile = v; } + public String getEmail() { return email; } + public void setEmail(String v) { this.email = v; } + public String getEmpSysno() { return empSysno; } + public void setEmpSysno(String v) { this.empSysno = v; } + public String getMedicalType() { return medicalType; } + public void setMedicalType(String v) { this.medicalType = v; } + public String getMedicalClass() { return medicalClass; } + public void setMedicalClass(String v) { this.medicalClass = v; } + public String getWorkShape() { return workShape; } + public void setWorkShape(String v) { this.workShape = v; } + public String getWorkDate() { return workDate; } + public void setWorkDate(String v) { this.workDate = v; } + public String getJobLevel() { return jobLevel; } + public void setJobLevel(String v) { this.jobLevel = v; } + public String getWorkPlace() { return workPlace; } + public void setWorkPlace(String v) { this.workPlace = v; } + public String getBaseSite() { return baseSite; } + public void setBaseSite(String v) { this.baseSite = v; } + public String getWorkType() { return workType; } + public void setWorkType(String v) { this.workType = v; } + public String getEducation() { return education; } + public void setEducation(String v) { this.education = v; } + public String getTitle() { return title; } + public void setTitle(String v) { this.title = v; } + public String getOldDepartName() { return oldDepartName; } + public void setOldDepartName(String v) { this.oldDepartName = v; } + public String getHarmReason() { return harmReason; } + public void setHarmReason(String v) { this.harmReason = v; } + public String getDefendCancer() { return defendCancer; } + public void setDefendCancer(String v) { this.defendCancer = v; } + public java.util.List getItemList() { return itemList; } + public void setItemList(java.util.List v) { this.itemList = v; } + + /** + * 按存储过程约定顺序拼接主表字符串(竖线分隔,空值为空串)。 + */ + 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(); + } +} diff --git a/src/main/java/com/hospital/front/vo/UnitVo.java b/src/main/java/com/hospital/front/vo/UnitVo.java new file mode 100644 index 0000000..efceddc --- /dev/null +++ b/src/main/java/com/hospital/front/vo/UnitVo.java @@ -0,0 +1,50 @@ +package com.hospital.front.vo; + +/** + * 单位信息 VO(addUnit 接口入参,字段名与原 UnitVo 一致)。 + */ +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) + + /** + * 全参构造器(保持与 Kotlin data class 相同的字段顺序,供 OrderService 组装用)。 + */ + public UnitVo(String unitCode, String unitName, String parentUnitCode, String address, + String connecter1, String phone1, String connecter2, String phone2) { + this.unitCode = unitCode; + this.unitName = unitName; + this.parentUnitCode = parentUnitCode; + this.address = address; + this.connecter1 = connecter1; + this.phone1 = phone1; + this.connecter2 = connecter2; + this.phone2 = phone2; + } + + public UnitVo() { + } + + public String getUnitCode() { return unitCode; } + public void setUnitCode(String v) { this.unitCode = v; } + public String getUnitName() { return unitName; } + public void setUnitName(String v) { this.unitName = v; } + public String getParentUnitCode() { return parentUnitCode; } + public void setParentUnitCode(String v) { this.parentUnitCode = v; } + public String getAddress() { return address; } + public void setAddress(String v) { this.address = v; } + public String getConnecter1() { return connecter1; } + public void setConnecter1(String v) { this.connecter1 = v; } + public String getPhone1() { return phone1; } + public void setPhone1(String v) { this.phone1 = v; } + public String getConnecter2() { return connecter2; } + public void setConnecter2(String v) { this.connecter2 = v; } + public String getPhone2() { return phone2; } + public void setPhone2(String v) { this.phone2 = v; } +} diff --git a/src/main/kotlin/com/hospital/agent/AgentConfig.kt b/src/main/kotlin/com/hospital/agent/AgentConfig.kt deleted file mode 100644 index 0c1bc19..0000000 --- a/src/main/kotlin/com/hospital/agent/AgentConfig.kt +++ /dev/null @@ -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", - ) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/agent/AgentMain.kt b/src/main/kotlin/com/hospital/agent/AgentMain.kt deleted file mode 100644 index 52695e3..0000000 --- a/src/main/kotlin/com/hospital/agent/AgentMain.kt +++ /dev/null @@ -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() -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/agent/cmd/CommandDispatcher.kt b/src/main/kotlin/com/hospital/agent/cmd/CommandDispatcher.kt deleted file mode 100644 index 4f9ddc6..0000000 --- a/src/main/kotlin/com/hospital/agent/cmd/CommandDispatcher.kt +++ /dev/null @@ -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 = 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)) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/agent/cmd/LogStreamManager.kt b/src/main/kotlin/com/hospital/agent/cmd/LogStreamManager.kt deleted file mode 100644 index 9ccf03b..0000000 --- a/src/main/kotlin/com/hospital/agent/cmd/LogStreamManager.kt +++ /dev/null @@ -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() - - /** 运行中的流:文件名 → 是否激活 */ - private val active = ConcurrentHashMap() - - 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 { - 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) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/agent/cmd/UpgradeCommand.kt b/src/main/kotlin/com/hospital/agent/cmd/UpgradeCommand.kt deleted file mode 100644 index 6c34ca3..0000000 --- a/src/main/kotlin/com/hospital/agent/cmd/UpgradeCommand.kt +++ /dev/null @@ -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 = 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 { - log.error("升级失败:{}", msg) - return mapOf("ok" to false, "version" to version, "msg" to msg) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/agent/ipc/MainAppClient.kt b/src/main/kotlin/com/hospital/agent/ipc/MainAppClient.kt deleted file mode 100644 index 524de84..0000000 --- a/src/main/kotlin/com/hospital/agent/ipc/MainAppClient.kt +++ /dev/null @@ -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 { - 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 { - 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 - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/agent/process/ProcessGuard.kt b/src/main/kotlin/com/hospital/agent/process/ProcessGuard.kt deleted file mode 100644 index 047f757..0000000 --- a/src/main/kotlin/com/hospital/agent/process/ProcessGuard.kt +++ /dev/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) - } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/agent/ws/AgentClient.kt b/src/main/kotlin/com/hospital/agent/ws/AgentClient.kt deleted file mode 100644 index 1ce47ed..0000000 --- a/src/main/kotlin/com/hospital/agent/ws/AgentClient.kt +++ /dev/null @@ -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() -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/HospitalFrontApp.kt b/src/main/kotlin/com/hospital/front/HospitalFrontApp.kt deleted file mode 100644 index 31774c7..0000000 --- a/src/main/kotlin/com/hospital/front/HospitalFrontApp.kt +++ /dev/null @@ -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) { - 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] 优雅停机中...") - }) -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/adapter/BsmAdapterBase.kt b/src/main/kotlin/com/hospital/front/adapter/BsmAdapterBase.kt deleted file mode 100644 index 55481a1..0000000 --- a/src/main/kotlin/com/hospital/front/adapter/BsmAdapterBase.kt +++ /dev/null @@ -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 = 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("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("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( - "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) -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/adapter/DevAdapter.kt b/src/main/kotlin/com/hospital/front/adapter/DevAdapter.kt deleted file mode 100644 index f835b60..0000000 --- a/src/main/kotlin/com/hospital/front/adapter/DevAdapter.kt +++ /dev/null @@ -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 = 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> = emptyList() - override fun getItemList(): List> = emptyList() - override fun getAllItemList(): List> = emptyList() - override fun getPeItemList(): List> = emptyList() - override fun getItemInfo(peItemCode: String): List> = emptyList() - override fun getEmpItem(peId: String): List> = emptyList() - override fun getResult(peId: String): List> = emptyList() - override fun getConclusion(peId: String): List> = emptyList() - override fun getConclusionAll(peId: String): List> = emptyList() - override fun getSuggest(peId: String): List> = emptyList() - override fun getSuggestAll(peId: String): List> = emptyList() - override fun getPeStatusByIdnoAndYear(idNo: String, year: String): Map? = null - override fun getResultByIdcardAndYear(idNo: String, year: String): List> = emptyList() - override fun getResultByIdcardAndYearAll(idNo: String, year: String): List> = emptyList() - override fun getResultByPeId(peId: String): List> = emptyList() - override fun getDisResultByAuditDate(auditDate: String): List> = emptyList() - override fun getItemByPeId(peId: String): List> = emptyList() - override fun getItemByPeIdAll(peId: String): List> = emptyList() - override fun getResultByIdNoOrPeId(idNo: String?, peId: String?, auditYear: String): List> = emptyList() - override fun getPeIdAndDateByIdcard(idNo: String): Map? = null - override fun getPeIdAndDateByIdcardList(idNo: String): List> = emptyList() - override fun getExamListByDate(auditDate: String?): List> = emptyList() -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/adapter/HospitalAdapter.kt b/src/main/kotlin/com/hospital/front/adapter/HospitalAdapter.kt deleted file mode 100644 index aa3e68f..0000000 --- a/src/main/kotlin/com/hospital/front/adapter/HospitalAdapter.kt +++ /dev/null @@ -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 - - // ================= 存储过程(写) ================= - - /** 提交预约(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> - - /** 医院所有体检项目(组合项) */ - fun getItemList(): List> - - /** 医院所有体检项目及指标项 */ - fun getAllItemList(): List> - - /** 医院所有体检指标项 */ - fun getPeItemList(): List> - - /** 按指标项 CODE 查询所属体检组合 */ - fun getItemInfo(peItemCode: String): List> - - /** 员工体检项目 */ - fun getEmpItem(peId: String): List> - - /** 员工体检结果 */ - fun getResult(peId: String): List> - - /** 体检结论(列表 / 全量) */ - fun getConclusion(peId: String): List> - fun getConclusionAll(peId: String): List> - - /** 体检建议(列表 / 全量) */ - fun getSuggest(peId: String): List> - fun getSuggestAll(peId: String): List> - - /** 按身份证号 + 年份查询体检状态 */ - fun getPeStatusByIdnoAndYear(idNo: String, year: String): Map? - - /** 按身份证号 + 年份查询体检结果(默认 / 全量) */ - fun getResultByIdcardAndYear(idNo: String, year: String): List> - fun getResultByIdcardAndYearAll(idNo: String, year: String): List> - - /** 按体检编号查询体检结果 */ - fun getResultByPeId(peId: String): List> - - /** 按审核日期查询体检报告(去重主表) */ - fun getDisResultByAuditDate(auditDate: String): List> - - /** 按体检编号查询体检项目子表(默认 / 全量) */ - fun getItemByPeId(peId: String): List> - fun getItemByPeIdAll(peId: String): List> - - /** 按身份证号或体检编号 + 审核年份查询体检结果 */ - fun getResultByIdNoOrPeId(idNo: String?, peId: String?, auditYear: String): List> - - /** 按身份证号查询当年 peId 与预约日期(单条 / 列表) */ - fun getPeIdAndDateByIdcard(idNo: String): Map? - fun getPeIdAndDateByIdcardList(idNo: String): List> - - /** 按审核日期查询当天体检人员列表(去重;日期空则当天) */ - fun getExamListByDate(auditDate: String?): List> -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/adapter/HospitalRouter.kt b/src/main/kotlin/com/hospital/front/adapter/HospitalRouter.kt deleted file mode 100644 index 0df89aa..0000000 --- a/src/main/kotlin/com/hospital/front/adapter/HospitalRouter.kt +++ /dev/null @@ -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 = - Solon.context().getBeansOfType(HospitalAdapter::class.java) - - private val map: Map 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 = map.keys -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/adapter/JingheAdapter.kt b/src/main/kotlin/com/hospital/front/adapter/JingheAdapter.kt deleted file mode 100644 index e124535..0000000 --- a/src/main/kotlin/com/hospital/front/adapter/JingheAdapter.kt +++ /dev/null @@ -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 = "泾河医院" -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/adapter/NingxiaAdapter.kt b/src/main/kotlin/com/hospital/front/adapter/NingxiaAdapter.kt deleted file mode 100644 index 74bc575..0000000 --- a/src/main/kotlin/com/hospital/front/adapter/NingxiaAdapter.kt +++ /dev/null @@ -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 = "宁夏宝石花医院" -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/adapter/NxAdapterBase.kt b/src/main/kotlin/com/hospital/front/adapter/NxAdapterBase.kt deleted file mode 100644 index 5663709..0000000 --- a/src/main/kotlin/com/hospital/front/adapter/NxAdapterBase.kt +++ /dev/null @@ -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 = 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("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("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( - "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) -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/adapter/QingyangAdapter.kt b/src/main/kotlin/com/hospital/front/adapter/QingyangAdapter.kt deleted file mode 100644 index 0adc0f8..0000000 --- a/src/main/kotlin/com/hospital/front/adapter/QingyangAdapter.kt +++ /dev/null @@ -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 = "庆阳医院" -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/adapter/XingLongYuanAdapter.kt b/src/main/kotlin/com/hospital/front/adapter/XingLongYuanAdapter.kt deleted file mode 100644 index 8cfe5af..0000000 --- a/src/main/kotlin/com/hospital/front/adapter/XingLongYuanAdapter.kt +++ /dev/null @@ -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 = "兴隆园医院" -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/adapter/YaAdapterBase.kt b/src/main/kotlin/com/hospital/front/adapter/YaAdapterBase.kt deleted file mode 100644 index 3e3cc50..0000000 --- a/src/main/kotlin/com/hospital/front/adapter/YaAdapterBase.kt +++ /dev/null @@ -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 = 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("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("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( - "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) -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/adapter/YananAdapter.kt b/src/main/kotlin/com/hospital/front/adapter/YananAdapter.kt deleted file mode 100644 index 0a9e13d..0000000 --- a/src/main/kotlin/com/hospital/front/adapter/YananAdapter.kt +++ /dev/null @@ -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 = "延安医院" -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/api/admin/AdminApi.kt b/src/main/kotlin/com/hospital/front/api/admin/AdminApi.kt deleted file mode 100644 index 563c061..0000000 --- a/src/main/kotlin/com/hospital/front/api/admin/AdminApi.kt +++ /dev/null @@ -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> { - 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> { - 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> { - 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 { - 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 { - 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>> { - 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 { - 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 { - checkToken(ctx) - val delay = (delayMs ?: 1000L).coerceIn(0, 10000) - Thread { - Thread.sleep(delay) - Solon.stopBlock() // 触发 Solon 停机事件(优雅退出) - }.apply { isDaemon = true }.start() - return Result.success() - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/api/order/OrderApi.kt b/src/main/kotlin/com/hospital/front/api/order/OrderApi.kt deleted file mode 100644 index 12addd2..0000000 --- a/src/main/kotlin/com/hospital/front/api/order/OrderApi.kt +++ /dev/null @@ -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 { - // Solon 对 Map 泛型 body 反序列化支持不佳,用 snack3 手动解析 - @Suppress("UNCHECKED_CAST") - val map: Map = org.noear.snack.ONode.load(body).toObject(Map::class.java) as Map - 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 { - service.cancelOrder(peId, name, idcard) - return Result.success() - } - - // ================= 2. 体检状态 ================= - - /** 3. 体检状态查询(多个 peId 逗号分隔) */ - @Mapping("/getPeStatus/{peIds}") - fun getPeStatus(@Path("peIds") peIds: String): Result>> = - Result.success(service.getPhyexamStatus(peIds)) - - /** 13. 根据身份证号及年份获取当前体检状态及体检项状态 */ - @Mapping("/getPeStatusAndItemList/{idNo}/{year}") - fun getPeStatusAndItemList(@Path("idNo") idNo: String, @Path("year") year: String): Result?> = - Result.success(service.getPeStatusByIdnoAndYear(idNo, year)) - - // ================= 3. 体检项目 ================= - - /** 1. 查询员工体检项目 */ - @Mapping("/getEmpItem/{peId}") - fun getEmpItem(@Path("peId") peId: String): Result>> = - Result.success(service.getEmpItem(peId)) - - /** 2. 体检项目查询 */ - @Mapping("/getItemList") - fun getItemList(): Result>> = - Result.success(service.getItemList()) - - /** 6. 体检指标项数据获取 */ - @Mapping("/getPeItemList") - fun getPeItemList(): Result>> = - Result.success(service.getPeItemList()) - - /** 6.1 体检指标项及体检组合数据获取 */ - @Mapping("/getAllItemList") - fun getAllItemList(): Result>> = - Result.success(service.getAllItemList()) - - /** 7. 根据体检指标项查询所属体检组合项 */ - @Mapping("/getItemInfo/{peItemCode}") - fun getItemInfo(@Path("peItemCode") peItemCode: String): Result>> = - Result.success(service.getItemInfo(peItemCode)) - - // ================= 4. 体检结果 / 结论 / 建议 ================= - - /** 8. 根据体检编号获取员工体检结果 */ - @Mapping("/getResult/{peId}") - fun getResult(@Path("peId") peId: String): Result>> = - Result.success(service.getResult(peId)) - - /** 9. 体检结论(汇总) */ - @Mapping("/getTotalConclusion/{peId}") - fun getTotalConclusion(@Path("peId") peId: String): Result> = - Result.success(service.getTotalConclusion(peId)) - - /** 10. 体检建议(汇总) */ - @Mapping("/getTotalSuggest/{peId}") - fun getTotalSuggest(@Path("peId") peId: String): Result> = - Result.success(service.getTotalSuggest(peId)) - - /** 11. 体检结论(列表) */ - @Mapping("/getConclusion/{peId}") - fun getConclusion(@Path("peId") peId: String): Result>> = - Result.success(service.getConclusion(peId)) - - /** 12. 体检建议(列表) */ - @Mapping("/getSuggest/{peId}") - fun getSuggest(@Path("peId") peId: String): Result>> = - Result.success(service.getSuggest(peId)) - - // ================= 5. 按身份证 / 日期查询 ================= - - /** 14. 根据身份证号及年份获取体检结果 */ - @Mapping("/getResultByIdcardAndYear/{idNo}/{year}") - fun getResultByIdcardAndYear(@Path("idNo") idNo: String, @Path("year") year: String): Result>> = - Result.success(service.getResultByIdcardAndYear(idNo, year)) - - /** 14.1 根据身份证号及年份获取体检结果(全量) */ - @Mapping("/getResultByIdcardAndYearAll/{idNo}/{year}") - fun getResultByIdcardAndYearAll(@Path("idNo") idNo: String, @Path("year") year: String): Result>> = - Result.success(service.getResultByIdcardAndYearAll(idNo, year)) - - /** 15.1 根据体检编号获取体检结果 */ - @Mapping("/getResultByPeId/{peId}") - fun getResultByPeId(@Path("peId") peId: String): Result>> = - Result.success(service.getResultByPeId(peId)) - - /** 15.2 根据体检编号获取体检状态(简) */ - @Mapping("/getResultStatus/{peId}") - fun getResultStatus(@Path("peId") peId: String): Result>> = - Result.success(service.getResultByPeId(peId)) - - /** 15.3 根据 audit_date 查询当天的体检报告 */ - @Mapping("/getResultByAuditDate/{auditDate}") - fun getResultByAuditDate(@Path("auditDate") auditDate: String): Result>> = - 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>> { - 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?> = - Result.success(service.getPeIdAndDateByIdcard(idNo)) - - /** 17.1 同上(数组) */ - @Mapping("/getPeIdAndDateByIdcardList/{idNo}") - fun getPeIdAndDateByIdcardList(@Path("idNo") idNo: String): Result>> = - Result.success(service.getPeIdAndDateByIdcardList(idNo)) - - // ================= 6. 单位 / 人员列表 ================= - - /** 18. 添加修改单位信息 */ - @Mapping(value = "/addUnit", method = [MethodType.POST]) - fun addUnit(@Body body: String): Result { - // Solon 对 List 泛型 body 反序列化支持不佳,此处用 snack3 手动解析 - @Suppress("UNCHECKED_CAST") - val units: List> = - if (body.isBlank()) emptyList() - else org.noear.snack.ONode.load(body).toObject(List::class.java) as List> - 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>> = - Result.success(service.getExamListByDate(auditDate)) -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/dto/ProcResult.kt b/src/main/kotlin/com/hospital/front/dto/ProcResult.kt deleted file mode 100644 index 4ed17e7..0000000 --- a/src/main/kotlin/com/hospital/front/dto/ProcResult.kt +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/dto/Result.kt b/src/main/kotlin/com/hospital/front/dto/Result.kt deleted file mode 100644 index d746b36..0000000 --- a/src/main/kotlin/com/hospital/front/dto/Result.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.hospital.front.dto - -/** - * 统一 API 响应包装。 - * - * 业务接口均使用此结构返回,便于前端统一处理。 - * - * @param T 数据类型 - * @property code 业务码(0=成功) - * @property msg 提示信息 - * @property data 业务数据 - */ -data class Result( - 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 success(): Result = Result(SUCCESS_CODE, "success", null) - - /** - * 成功响应。 - */ - fun success(data: T): Result = Result(SUCCESS_CODE, "success", data) - - /** - * 错误响应(默认错误码)。 - */ - fun error(msg: String): Result = Result(ERROR_CODE, msg, null) - - /** - * 错误响应(指定错误码)。 - */ - fun error(code: Int, msg: String): Result = Result(code, msg, null) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/infra/config/AppConfig.kt b/src/main/kotlin/com/hospital/front/infra/config/AppConfig.kt deleted file mode 100644 index 85b7ab5..0000000 --- a/src/main/kotlin/com/hospital/front/infra/config/AppConfig.kt +++ /dev/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 = emptyList() -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/infra/exception/GlobalExceptionHandler.kt b/src/main/kotlin/com/hospital/front/infra/exception/GlobalExceptionHandler.kt deleted file mode 100644 index 5fcdd0e..0000000 --- a/src/main/kotlin/com/hospital/front/infra/exception/GlobalExceptionHandler.kt +++ /dev/null @@ -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(e.code, e.message ?: "业务异常")) - } catch (e: IllegalArgumentException) { - log.warn("参数异常:{}", e.message) - renderResult(ctx, Result.error(Result.CODE_PARAM_FORMAT, e.message ?: "参数错误")) - } catch (e: Exception) { - log.error("系统异常", e) - renderResult(ctx, Result.error(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) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/infra/util/DateUtils.kt b/src/main/kotlin/com/hospital/front/infra/util/DateUtils.kt deleted file mode 100644 index 8676c84..0000000 --- a/src/main/kotlin/com/hospital/front/infra/util/DateUtils.kt +++ /dev/null @@ -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) -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/mapper/bsm/BsmOrderMapper.kt b/src/main/kotlin/com/hospital/front/mapper/bsm/BsmOrderMapper.kt deleted file mode 100644 index 2807598..0000000 --- a/src/main/kotlin/com/hospital/front/mapper/bsm/BsmOrderMapper.kt +++ /dev/null @@ -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) - - /** 取消预约(存储过程 P_DEL_PE_REGIST) */ - fun cancelOrder(map: MutableMap) - - /** 添加/修改单位 V1(存储过程 P_ADD_UNIT) */ - fun addUnit(map: MutableMap) - - /** 添加/修改单位 V2(存储过程 P_ADD_UNIT,8 入参版) */ - fun addUnitV2(map: MutableMap) - - /** 按体检预约 ID(逗号分隔)查询体检状态 */ - fun getPhyexamStatus(@Param("peIds") peIds: String): List> - - /** 医院所有体检项目(组合项) */ - fun getItemList(): List> - - /** 医院所有体检项目及指标项 */ - fun getAllItemList(): List> - - /** 医院所有体检指标项 */ - fun getPeItemList(): List> - - /** 员工体检项目 */ - fun getEmpItem(@Param("peId") peId: String): List> - - /** 按指标项 CODE 查询体检组合 */ - fun getItemInfo(@Param("peItemCode") peItemCode: String): List> - - /** 员工体检结果 */ - fun getResult(@Param("peId") peId: String): List> - - /** 体检结论(列表) */ - fun getConclusion(@Param("peId") peId: String): List> - - /** 体检结论(全量) */ - fun getConclusionAll(@Param("peId") peId: String): List> - - /** 体检建议(列表) */ - fun getSuggest(@Param("peId") peId: String): List> - - /** 体检建议(全量) */ - fun getSuggestAll(@Param("peId") peId: String): List> - - /** 按身份证号及年份查询体检状态 */ - fun getPeStatusByIdnoAndYear(@Param("idNo") idNo: String, @Param("year") year: String): Map? - - /** 按身份证号及年份查询体检结果 */ - fun getResultByIdcardAndYear(@Param("idNo") idNo: String, @Param("year") year: String): List> - - /** 按身份证号及年份查询体检结果(全量) */ - fun getResultByIdcardAndYearAll(@Param("idNo") idNo: String, @Param("year") year: String): List> - - /** 按体检编号查询体检结果 */ - fun getResultByPeId(@Param("peId") peId: String): List> - - /** 按审核日期查询体检报告(去重) */ - fun getDisResultByAuditDate(@Param("auditDate") auditDate: String): List> - - /** 按体检编号查询体检项目子表 */ - fun getItemByPeId(@Param("peId") peId: String): List> - - /** 按体检编号查询体检项目子表(全量) */ - fun getItemByPeIdAll(@Param("peId") peId: String): List> - - /** 按身份证号或体检编号 + 审核年份查询体检结果 */ - fun getResultByIdNoOrPeId( - @Param("idNo") idNo: String?, - @Param("peId") peId: String?, - @Param("auditYear") auditYear: String, - ): List> - - /** 按身份证号查询当年 peId 与预约日期 */ - fun getPeIdAndDateByIdcard(@Param("idNo") idNo: String): Map? - - /** 按身份证号查询当年 peId 与预约日期(数组) */ - fun getPeIdAndDateByIdcardList(@Param("idNo") idNo: String): List> - - /** 按审核日期查询当天体检人员列表(去重) */ - fun getExamListByDate(@Param("auditDate") auditDate: String?): List> -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/mapper/nx/NxOrderMapper.kt b/src/main/kotlin/com/hospital/front/mapper/nx/NxOrderMapper.kt deleted file mode 100644 index 0adfff4..0000000 --- a/src/main/kotlin/com/hospital/front/mapper/nx/NxOrderMapper.kt +++ /dev/null @@ -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) - - /** 取消预约(存储过程 P_DEL_PE_REGIST) */ - fun cancelOrder(map: MutableMap) - - /** 添加/修改单位 V1(存储过程 P_ADD_UNIT) */ - fun addUnit(map: MutableMap) - - /** 添加/修改单位 V2(存储过程 P_ADD_UNIT,8 入参版) */ - fun addUnitV2(map: MutableMap) - - /** 按体检预约 ID(逗号分隔)查询体检状态 */ - fun getPhyexamStatus(@Param("peIds") peIds: String): List> - - /** 医院所有体检项目(组合项) */ - fun getItemList(): List> - - /** 医院所有体检项目及指标项 */ - fun getAllItemList(): List> - - /** 医院所有体检指标项 */ - fun getPeItemList(): List> - - /** 员工体检项目 */ - fun getEmpItem(@Param("peId") peId: String): List> - - /** 按指标项 CODE 查询体检组合 */ - fun getItemInfo(@Param("peItemCode") peItemCode: String): List> - - /** 员工体检结果 */ - fun getResult(@Param("peId") peId: String): List> - - /** 体检结论(列表) */ - fun getConclusion(@Param("peId") peId: String): List> - - /** 体检结论(全量) */ - fun getConclusionAll(@Param("peId") peId: String): List> - - /** 体检建议(列表) */ - fun getSuggest(@Param("peId") peId: String): List> - - /** 体检建议(全量) */ - fun getSuggestAll(@Param("peId") peId: String): List> - - /** 按身份证号及年份查询体检状态 */ - fun getPeStatusByIdnoAndYear(@Param("idNo") idNo: String, @Param("year") year: String): Map? - - /** 按身份证号及年份查询体检结果 */ - fun getResultByIdcardAndYear(@Param("idNo") idNo: String, @Param("year") year: String): List> - - /** 按身份证号及年份查询体检结果(全量) */ - fun getResultByIdcardAndYearAll(@Param("idNo") idNo: String, @Param("year") year: String): List> - - /** 按体检编号查询体检结果 */ - fun getResultByPeId(@Param("peId") peId: String): List> - - /** 按审核日期查询体检报告(去重) */ - fun getDisResultByAuditDate(@Param("auditDate") auditDate: String): List> - - /** 按体检编号查询体检项目子表 */ - fun getItemByPeId(@Param("peId") peId: String): List> - - /** 按体检编号查询体检项目子表(全量) */ - fun getItemByPeIdAll(@Param("peId") peId: String): List> - - /** 按身份证号或体检编号 + 审核年份查询体检结果 */ - fun getResultByIdNoOrPeId( - @Param("idNo") idNo: String?, - @Param("peId") peId: String?, - @Param("auditYear") auditYear: String, - ): List> - - /** 按身份证号查询当年 peId 与预约日期 */ - fun getPeIdAndDateByIdcard(@Param("idNo") idNo: String): Map? - - /** 按身份证号查询当年 peId 与预约日期(数组) */ - fun getPeIdAndDateByIdcardList(@Param("idNo") idNo: String): List> - - /** 按审核日期查询当天体检人员列表(去重) */ - fun getExamListByDate(@Param("auditDate") auditDate: String?): List> -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/mapper/ya/YaOrderMapper.kt b/src/main/kotlin/com/hospital/front/mapper/ya/YaOrderMapper.kt deleted file mode 100644 index b323745..0000000 --- a/src/main/kotlin/com/hospital/front/mapper/ya/YaOrderMapper.kt +++ /dev/null @@ -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) - - /** 取消预约(存储过程 P_DEL_PE_REGIST) */ - fun cancelOrder(map: MutableMap) - - /** 添加/修改单位 V1(存储过程 P_ADD_UNIT) */ - fun addUnit(map: MutableMap) - - /** 添加/修改单位 V2(存储过程 P_ADD_UNIT,8 入参版) */ - fun addUnitV2(map: MutableMap) - - /** 按体检预约 ID(逗号分隔)查询体检状态 */ - fun getPhyexamStatus(@Param("peIds") peIds: String): List> - - /** 医院所有体检项目(组合项) */ - fun getItemList(): List> - - /** 医院所有体检项目及指标项 */ - fun getAllItemList(): List> - - /** 医院所有体检指标项 */ - fun getPeItemList(): List> - - /** 员工体检项目 */ - fun getEmpItem(@Param("peId") peId: String): List> - - /** 按指标项 CODE 查询体检组合 */ - fun getItemInfo(@Param("peItemCode") peItemCode: String): List> - - /** 员工体检结果 */ - fun getResult(@Param("peId") peId: String): List> - - /** 体检结论(列表) */ - fun getConclusion(@Param("peId") peId: String): List> - - /** 体检结论(全量) */ - fun getConclusionAll(@Param("peId") peId: String): List> - - /** 体检建议(列表) */ - fun getSuggest(@Param("peId") peId: String): List> - - /** 体检建议(全量) */ - fun getSuggestAll(@Param("peId") peId: String): List> - - /** 按身份证号及年份查询体检状态 */ - fun getPeStatusByIdnoAndYear(@Param("idNo") idNo: String, @Param("year") year: String): Map? - - /** 按身份证号及年份查询体检结果 */ - fun getResultByIdcardAndYear(@Param("idNo") idNo: String, @Param("year") year: String): List> - - /** 按身份证号及年份查询体检结果(全量) */ - fun getResultByIdcardAndYearAll(@Param("idNo") idNo: String, @Param("year") year: String): List> - - /** 按体检编号查询体检结果 */ - fun getResultByPeId(@Param("peId") peId: String): List> - - /** 按审核日期查询体检报告(去重) */ - fun getDisResultByAuditDate(@Param("auditDate") auditDate: String): List> - - /** 按体检编号查询体检项目子表 */ - fun getItemByPeId(@Param("peId") peId: String): List> - - /** 按体检编号查询体检项目子表(全量) */ - fun getItemByPeIdAll(@Param("peId") peId: String): List> - - /** 按身份证号或体检编号 + 审核年份查询体检结果 */ - fun getResultByIdNoOrPeId( - @Param("idNo") idNo: String?, - @Param("peId") peId: String?, - @Param("auditYear") auditYear: String, - ): List> - - /** 按身份证号查询当年 peId 与预约日期 */ - fun getPeIdAndDateByIdcard(@Param("idNo") idNo: String): Map? - - /** 按身份证号查询当年 peId 与预约日期(数组) */ - fun getPeIdAndDateByIdcardList(@Param("idNo") idNo: String): List> - - /** 按审核日期查询当天体检人员列表(去重) */ - fun getExamListByDate(@Param("auditDate") auditDate: String?): List> -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/service/OrderService.kt b/src/main/kotlin/com/hospital/front/service/OrderService.kt deleted file mode 100644 index 16aa515..0000000 --- a/src/main/kotlin/com/hospital/front/service/OrderService.kt +++ /dev/null @@ -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 { - 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>): 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> = - 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 = - mapOf("totalConclusion" to buildSummary(adapter().getConclusion(peId))) - - /** 体检结论(汇总,全量来源) */ - fun getTotalConclusionAll(peId: String): Map = - mapOf("totalConclusion" to buildSummary(adapter().getConclusionAll(peId))) - - /** - * 体检建议(汇总)。宁夏(nx)仅取首条 GUIDE_CONTENT。 - */ - fun getTotalSuggest(peId: String): Map = - mapOf("totalSuggest" to buildSuggest(adapter().getSuggest(peId), adapter().hmType() == "nx")) - - /** 体检建议(汇总,全量来源) */ - fun getTotalSuggestAll(peId: String): Map = - mapOf("totalSuggest" to buildSuggest(adapter().getSuggestAll(peId), adapter().hmType() == "nx")) - - /** - * 按身份证号 + 年份查询体检状态及项目(含宁夏体检报告兜底)。 - */ - fun getPeStatusByIdnoAndYear(idNo: String, year: String): Map? { - 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.科室:结论;
*/ - private fun buildSummary(rows: List>): String = - rows.mapIndexed { i, row -> - val dept = row["PE_DEPT_NAME"]?.toString()?.takeIf { it.isNotEmpty() }?.let { "$it:" } ?: "" - "${i + 1}.$dept${row["CONCLUSION_TEXT"] ?: ""};
" - }.joinToString("") - - /** 建议汇总:宁夏仅取首条 GUIDE_CONTENT;其余 1.建议项:内容;
*/ - private fun buildSuggest(rows: List>, 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"] ?: ""};
" } - .joinToString("") - } - - /** Map → OrderV2Vo(数值/空值容错) */ - private fun convertOrderV2(body: Map): 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) = 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(), - ) -} \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/vo/MedicalMiddleResultVo.kt b/src/main/kotlin/com/hospital/front/vo/MedicalMiddleResultVo.kt deleted file mode 100644 index aba537c..0000000 --- a/src/main/kotlin/com/hospital/front/vo/MedicalMiddleResultVo.kt +++ /dev/null @@ -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? = null, -) \ No newline at end of file diff --git a/src/main/kotlin/com/hospital/front/vo/OrderVo.kt b/src/main/kotlin/com/hospital/front/vo/OrderVo.kt deleted file mode 100644 index 370dc39..0000000 --- a/src/main/kotlin/com/hospital/front/vo/OrderVo.kt +++ /dev/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?): 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? = 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? = 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) -) \ No newline at end of file diff --git a/pac/conf-example/app-jinghe.yml b/src/main/resources/app-jinghe.yml similarity index 92% rename from pac/conf-example/app-jinghe.yml rename to src/main/resources/app-jinghe.yml index 1aee28e..edde9c1 100644 --- a/pac/conf-example/app-jinghe.yml +++ b/src/main/resources/app-jinghe.yml @@ -12,7 +12,7 @@ solon.dataSources: class: "com.zaxxer.hikari.HikariDataSource" jdbcUrl: jdbc:oracle:thin:@192.168.50.204:1521/FREEPDB1 username: SYSTEM - password: ${DB_PWD:jyf@2024} + password: Msor6085008 minimumIdle: 2 maximumPoolSize: 10 connectionTimeout: 30000 diff --git a/src/main/resources/app.yml b/src/main/resources/app.yml index 0aa9d61..8b1e84c 100644 --- a/src/main/resources/app.yml +++ b/src/main/resources/app.yml @@ -44,9 +44,9 @@ mybatis.db1: - "com.hospital.front.mapper.bsm" - "com.hospital.front.mapper.nx" - "com.hospital.front.mapper.ya" - - "com/hospital/front/mapper/bsm/OrderMapper.xml" - - "com/hospital/front/mapper/nx/OrderMapper.xml" - - "com/hospital/front/mapper/ya/OrderMapper.xml" + - "com/hospital/front/mapper/bsm/BsmOrderMapper.xml" + - "com/hospital/front/mapper/nx/NxOrderMapper.xml" + - "com/hospital/front/mapper/ya/YaOrderMapper.xml" configuration: mapUnderscoreToCamelCase: true logImpl: "org.apache.ibatis.logging.slf4j.Slf4jImpl" @@ -60,4 +60,13 @@ logging: level: root: INFO com.hospital.front: INFO - org.noear: WARN \ No newline at end of file + org.noear: WARN + +# ==================================== +# Uptime-Kuma 监控(沿用原 hospitalmiddle 逻辑) +# kuma.code 在各医院配置中覆盖 +# ==================================== +kuma: + enable: false + cron: "0 */3 * * * *" + url: "" \ No newline at end of file diff --git a/src/main/resources/com/hospital/front/mapper/bsm/BsmOrderMapper.xml b/src/main/resources/com/hospital/front/mapper/bsm/BsmOrderMapper.xml index 5044365..ae65ecc 100644 --- a/src/main/resources/com/hospital/front/mapper/bsm/BsmOrderMapper.xml +++ b/src/main/resources/com/hospital/front/mapper/bsm/BsmOrderMapper.xml @@ -276,55 +276,79 @@