Compare commits

...
2 Commits
3 changed files with 204 additions and 89 deletions
+82 -88
View File
@@ -1,10 +1,8 @@
# Hospital Front — 医院前置机程序 # Hospital Front — 医院前置机程序
> 基于 Kotlin + Solon 4.0.6 重构的医院前置机对接系统 > 基于 Java 17 + Solon 4.0.6 的医院前置机对接系统
> >
> 重构源:[hospitalmiddle](../hospitalmiddle)SpringBoot 2.7 + MyBatis-Plus + Oracle/SqlServer > 重构源:hospitalmiddleSpringBoot 2.7 + MyBatis-Plus + Oracle/SqlServer
>
> 完整方案:[E:\data\ob_data\myob\11 健康长庆\医院前置机重构\](../ob_data)
--- ---
@@ -12,34 +10,35 @@
体检预约系统通过本程序与多家医院 HIS 系统对接,实现: 体检预约系统通过本程序与多家医院 HIS 系统对接,实现:
- 体检预约的提交、取消 - 体检预约的提交、取消
- 体检项目查询、状态查询 - 体检项目 / 组合查询、体检状态查询
- 体检结果/结论/建议的获取 - 体检结果 / 结论 / 建议的获取
- 单位信息同步 - 单位信息同步
**支持 5 家医院**(拼音编码): **支持医院**(拼音编码,由 `hospital.active` 指定激活):
| 编码 | 医院名 | 数据库 |
|---|---|---| | 编码 | 医院名 | 数据库 | Adapter |
| `xinglongyuan` | 兴隆园医院 | Oracle | |---|---|---|---|
| `qingyang` | 庆阳医院 | Oracle | | `xinglongyuan` | 兴隆园医院 | Oracle | `XingLongYuanAdapter` |
| `ningxia` | 宁夏宝石花医院 | SqlServer | | `qingyang` | 庆阳医院 | Oracle | `QingyangAdapter` |
| `yanan` | 延安医院 | SqlServer | | `ningxia` | 宁夏宝石花医院 | SqlServer | `NingxiaAdapter` |
| `jinghe` | 泾河医院 | Oracle | | `yanan` | 延安医院 | SqlServer | `YananAdapter` |
| `jinghe` | 泾河医院 | Oracle | `JingheAdapter` |
医院通过 `HospitalAdapter` 接口 + `HospitalRouter` 路由(策略模式),方言基类:
- `BsmAdapterBase`Oracle 方言(bsm Mapper
- `NxAdapterBase`SqlServer 方言(nx Mapper
--- ---
## 2. 技术栈 ## 2. 技术栈
- **语言**Kotlin 2.0.21`jvmTarget=17` - **语言**Java 17
- **框架**Solon 4.0.6(国产轻量级 Java 框架) - **框架**Solon 4.0.6
- **JDK**17 LTS(推荐)或 21 LTS
- **ORM**MyBatis-Plus 3.5.9 - **ORM**MyBatis-Plus 3.5.9
- **数据库**Oracle / SqlServer(直连医院库,不可修改) - **数据库**Oracleojdbc11 + orai18nZHS16GBK 字符集支持)/ SqlServer(直连医院库,只读对接,不可修改表结构
- **日志**Logback 1.5.12 - **日志**Logback(异步 appenderinfo.log / error.log 分文件滚动)
- **HTTP/WS**OkHttp 4.12.0
- **构建**Maven 3.9+ - **构建**Maven 3.9+
> ✅ **依赖瘦身**:不接入 Knife4j / Druid / Lombok / Jasypt(详见 [01-技术栈选型.md](../ob_data/01-技术栈选型.md)
--- ---
## 3. 项目结构 ## 3. 项目结构
@@ -48,31 +47,22 @@
hospital-front/ hospital-front/
├── pom.xml ├── pom.xml
├── README.md ├── README.md
├── .gitignore ├── conf/ # 各医院外置配置(含生产凭据,不入 Git)
── src/ ── src/main/
├── main/ ├── java/com/hospital/front/
│ ├── kotlin/com/hospital/front/ │ ├── HospitalFrontApp.java # 启动类
│ │ │ ├── HospitalFrontApp.kt # @SolonMain 启动类 │ ├── adapter/ # 医院适配器(策略模式 + 方言基类)
│ │ │ ├── adapter/ # 医院适配器(策略模式 │ ├── api/ # HTTP 接口(order / admin
│ │ │ ├── api/ # HTTP 接口层(按业务域) │ ├── service/ # 业务服务层
│ │ │ ├── service/ # 业务服务层 │ ├── mapper/bsm|nx/ # 数据访问层(按方言分包)
│ │ │ ├── mapper/ # 数据访问层 │ ├── vo/ dto/ # 值对象 / 传输对象
│ │ │ ├── vo/ # 值对象 │ ├── infra/ # 基础设施(过滤器、异常处理、扫描器)
│ │── dto/ # 数据传输对象 ── task/ # 定时任务(Uptime-Kuma 上报)
│ │ │ ├── infra/ # 基础设施 └── resources/
── task/ # 定时任务 ── app.yml # 主配置(框架级 + 公共配置)
── resources/ ── logback.xml # 日志配置
│ │ ├── app.yml # Solon 主配置 ├── tls10.security # 延安专用 JDK 安全策略(放开 TLSv1/弱算法)
│ │ ── logback.xml # 日志配置 ── mapper/ # MyBatis XML
│ │ └── mapper/ # MyBatis XML
│ └── test/kotlin/ # 单元测试
└── pac/ # 部署包(运行时外置)
├── bin/ # 启动脚本
├── conf/ # 外置配置
├── backup/ # 历史版本
├── log/ # 日志
├── pid/ # PID 文件
└── tmp/ # 临时文件
``` ```
--- ---
@@ -89,57 +79,61 @@ mvn clean package -DskipTests
## 5. 启动(开发环境) ## 5. 启动(开发环境)
```bash ```bash
# 拷贝外置配置(首次启动 # 方式一:默认加载 classpath 的 app.ymlhospital.active 默认 jinghe
cp -r pac/conf-example pac/conf java -jar target/hospital-front-1.0.0.jar
# 编辑配置
vim pac/conf/app-jinghe.yml
# 启动(默认加载 app.yml + conf/app-{HOSPITAL}.yml
HOSPITAL=jinghe java -jar target/hospital-front-1.0.0.jar
``` ```
数据源等医院差异配置在 `app-{医院}.yml` 中,开发期可用 `--solon.config.add` 外挂。
--- ---
## 6. 部署 ## 6. 生产部署注意事项(重要)
详细部署、升级、回滚 SOP[05-部署运维.md](../ob_data/05-部署运维.md) ### 6.1 外挂医院配置
**Windows Server 简版** 生产环境配置文件**外置于 jar 同级或 `conf/` 目录**,通过 `--solon.config.add` 追加加载,
```batch 修改数据库地址 / 鉴权等无需重新打包:
D:
cd D:\hospital-front\bin ```bash
call start.bat start java -jar hospital-front.jar --solon.config.add=conf/app-yanan.yml --server.port=8003
``` ```
- 配置模板见仓库 `conf/` 目录(app-jinghe / app-ningxia / app-yanan
- `--server.port` 覆盖默认端口 8082
- 外挂文件中必须指定 `hospital.active``hm.type`bsm / nx / ya),并定义 `solon.dataSources.db1`
### 6.2 延安医院:TLS 1.0 特殊处理
延安 SqlServer 为老版本(仅支持 TLS 1.0 / 弱加密算法),JDK 17 默认禁用,
必须外挂 JDK 安全属性文件启动,否则连接报 `handshake failure` / `No appropriate protocol`
```bash
java -Dfile.encoding=UTF-8 -Djava.security.properties=tls10.security -jar hospital-front.jar --solon.config.add=conf/app-yanan.yml --server.port=8003
```
- `tls10.security` 已打包在 jar 内(`src/main/resources/`),上述命令按 classpath 加载,无需单独分发文件;也可用文件系统绝对路径覆盖
- `-Dfile.encoding=UTF-8`Windows 服务器默认 GBK,防止日志与接口中文乱码
- 其余医院**不要**带 `tls10.security`(会全局放开弱算法,降低安全基线)
### 6.3 安全配置(生产必读)
`app.yml` 中以下配置生产环境必须核对(可在外挂配置中覆盖):
| 配置 | 说明 |
|---|---|
| `order.basic` | `/order/**` 的 HTTP Basic 认证(`user:password`),生产必须配置 |
| `admin.token` | `/api/admin/**``X-Admin-Token`Agent 环回调用 |
| `security.ipWhitelistEnabled` / `ipWhitelist` | IP 白名单(支持精确 IP / 通配段 / CIDR),端口暴露公网时务必开启 |
| `security.trustProxy` | nginx 反代部署时开启,从 X-Forwarded-For 取真实客户端 IP |
### 6.4 日志
- 日志目录默认 `./log``-Dlog.path=xxx` 可覆盖)
- `info.log`INFO 及以上)/ `error.log`(仅 ERROR)分文件,50MB 按天滚动,保留 30 天
- pattern 不含 `%file:%line`AsyncAppender 下取不到调用者信息,会打成 `?:?`
--- ---
## 7. 文档 ## 7. 文档
完整设计文档位于:`E:\data\ob_data\myob\11 健康长庆\医院前置机重构\` 完整设计文档位于:`E:\data\ob_data\myob\11 健康长庆\医院前置机重构\`
| 文档 | 内容 |
|---|---|
| [00-总体方案.md](../ob_data/00-总体方案.md) | 整体方案 |
| [01-技术栈选型.md](../ob_data/01-技术栈选型.md) | Solon 选型、版本、注解对照 |
| [02-医院差异化矩阵.md](../ob_data/02-医院差异化矩阵.md) | 5 家医院差异、Adapter 拆分 |
| [03-架构设计.md](../ob_data/03-架构设计.md) | 系统架构、模块、时序 |
| [04-数据流与接口契约.md](../ob_data/04-数据流与接口契约.md) | 接口清单、入参出参 |
| [05-部署运维.md](../ob_data/05-部署运维.md) | 部署、升级、回滚 SOP |
| [06-变更记录.md](../ob_data/06-变更记录.md) | 所有变更登记 |
---
## 8. 任务进度
| Step | 内容 | 状态 |
|---|---|---|
| 1 | 工程骨架 | ✅ |
| 2 | 5 医院配置中心 | ⏳ |
| 3 | 业务接口 Kotlin 重写 | ⏳ |
| 4 | 5 套 Mapper 适配 | ⏳ |
| 5 | 主程序被控端 API | ⏳ |
| 6 | Agent 工程 | ⏳ |
| 7 | 升级流程 | ⏳ |
| 8 | 日志推送 | ⏳ |
| 9 | B 端管理后台 | ⏳ |
| 10 | 联调 + 文档 | ⏳ |
@@ -2,6 +2,8 @@ package com.hospital.front.api.admin;
import java.io.File; import java.io.File;
import java.lang.management.ManagementFactory; import java.lang.management.ManagementFactory;
import java.net.URI;
import java.util.Base64;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
@@ -280,7 +282,123 @@ public class AdminApi {
return Result.success(); return Result.success();
} }
// ================= 5. 接口清单(供 B 端接口测试工具加载 ================= // ================= 5. 接口代理(供 B 端接口测试工具直连调用 =================
/** httpProxy 请求体(Solon 自动绑定 JSON 请求体) */
public static class HttpProxyReq {
/** HTTP 方法:GET/POST/PUT/DELETE */
public String method = "GET";
/** 接口路径,如 /order/getItemList */
public String path;
/** query 参数(拼到 URL 上) */
public Map<String, String> query;
/** 请求体字符串(POST/PUT 时使用) */
public String body;
/** 额外请求头 */
public Map<String, String> headers;
}
/** 代理专用 HttpClient:环回调用本机接口,读超时 120s 适配大接口 */
private final java.net.http.HttpClient proxyHttp = java.net.http.HttpClient.newBuilder()
.connectTimeout(java.time.Duration.ofSeconds(5))
.build();
/**
* HTTP 代理:在 main 进程内环回调用本机接口,原样透传响应。
*
* B 端接口测试工具经此代理访问业务接口,免去 Agent WS 通道倒手
* (WS 通道有 2MB 消息上限与 60s 等待超时,大接口会超时/掉线)。
* 自动按 path 附加鉴权头:/order/** → Basicorder.basic 配置),
* /api/admin/** → X-Admin-Token。
*/
@Mapping(value = "/httpProxy", method = {MethodType.POST})
public Result<Map<String, Object>> httpProxy(Context ctx, HttpProxyReq req) {
checkToken(ctx);
// 参数校验与路径白名单:只允许代理本机 /order/** 与 /api/admin/**
if (req == null || req.path == null || req.path.isEmpty()) {
throw BusinessException.paramMissing("path 不能为空");
}
String path = req.path.trim();
if (!path.startsWith("/order/") && !path.startsWith("/api/admin/")) {
throw BusinessException.businessFailed("仅允许代理 /order/** 与 /api/admin/** 路径:" + path);
}
// 拼接环回 URLserver.port + path + query
String port = Solon.cfg().get("server.port", "8080");
StringBuilder url = new StringBuilder("http://127.0.0.1:").append(port).append(path);
if (req.query != null && !req.query.isEmpty()) {
StringBuilder qs = new StringBuilder();
for (Map.Entry<String, String> e : req.query.entrySet()) {
if (e.getKey() == null || e.getValue() == null) {
continue;
}
if (qs.length() > 0) {
qs.append('&');
}
qs.append(java.net.URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8))
.append('=')
.append(java.net.URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8));
}
if (qs.length() > 0) {
url.append('?').append(qs);
}
}
// 构建请求:按 path 附加鉴权头
java.net.http.HttpRequest.Builder rb = java.net.http.HttpRequest.newBuilder(URI_COMPAT.apply(url.toString()));
String m = req.method == null ? "GET" : req.method.toUpperCase();
java.net.http.HttpRequest.BodyPublisher pub = req.body == null || req.body.isEmpty()
? java.net.http.HttpRequest.BodyPublishers.noBody()
: java.net.http.HttpRequest.BodyPublishers.ofString(req.body, StandardCharsets.UTF_8);
switch (m) {
case "POST" -> rb.POST(pub);
case "PUT" -> rb.PUT(pub);
case "DELETE" -> rb.DELETE();
default -> rb.GET();
}
if (path.startsWith("/order/")) {
String basic = Solon.cfg().get("order.basic", "");
boolean authFlag = !"false".equals(Solon.cfg().get("order.authFlag", "true"));
if (authFlag && basic != null && !basic.isEmpty()) {
// 对齐 OrderApi checkBasic 的校验格式:Basic + Base64(user:password)
rb.header("Authorization", "Basic "
+ Base64.getEncoder().encodeToString(basic.getBytes(StandardCharsets.UTF_8)));
}
} else {
rb.header("X-Admin-Token", Solon.cfg().get("admin.token", ""));
}
if (req.headers != null) {
for (Map.Entry<String, String> e : req.headers.entrySet()) {
if (e.getKey() != null && e.getValue() != null) {
rb.header(e.getKey(), e.getValue());
}
}
}
rb.timeout(java.time.Duration.ofSeconds(120));
// 执行并透传
try {
java.net.http.HttpResponse<String> resp = proxyHttp.send(rb.build(),
java.net.http.HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
Map<String, Object> out = new LinkedHashMap<>();
out.put("status", resp.statusCode());
out.put("body", resp.body());
return Result.success(out);
} catch (java.io.IOException e) {
throw BusinessException.businessFailed("环回调用失败(本机 " + path + "):" + e.getMessage());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw BusinessException.businessFailed("环回调用被中断:" + path);
}
}
/** URI 构建容错(URL 已校验,异常时给出可读错误) */
private static final java.util.function.Function<String, URI> URI_COMPAT = s -> {
try {
return URI.create(s);
} catch (Exception e) {
throw BusinessException.paramMissing("非法 URL" + s);
}
};
// ================= 6. 接口清单(供 B 端接口测试工具加载) =================
/** /**
* 返回 /order 业务接口元数据清单。 * 返回 /order 业务接口元数据清单。
+3
View File
@@ -0,0 +1,3 @@
# 重新允许 TLSv1/TLSv1.1(仅本进程生效,为兼容老版 SQL Server 的登录加密)
jdk.tls.disabledAlgorithms=SSLv3, RC4, DES, MD5withRSA, \
DH keySize < 1024, EC keySize < 224, 3DES_EDE_CBC, anon, NULL