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