feat: 支持中转平台 direct 路由模式

- gateway 新增 local/direct/edge 多模式路由
- 邹平改为 direct 模式,由 gateway 主动访问前置机地址
- 更新 gateway/zpfb 环境变量示例和部署说明
This commit is contained in:
Jafeng 2026-06-22 15:49:26 +08:00
parent 11c141fc21
commit b8a6e9a8e1
8 changed files with 169 additions and 29 deletions

View File

@ -1,9 +1,12 @@
CONFIG_NODE_PORT=8082 CONFIG_NODE_PORT=8082
CONFIG_ADAPTER_ROLE=standalone CONFIG_ADAPTER_ROLE=gateway
CONFIG_MONGO_ENABLED=false CONFIG_MONGO_ENABLED=false
# 测试环境: 前端按 HIS 选项传固定 corpId网关按 corpId 路由到 gk 映射。 # 测试环境: 前端按 HIS 选项传固定 corpId网关按 corpId 路由到医院编码。
CONFIG_HOSPITAL_ADAPTER_CORP_MAP={"wwa54dfba0b5441ef1":"gk"} CONFIG_HOSPITAL_ADAPTER_CORP_MAP={"wwa54dfba0b5441ef1":"gk","zpfb-corp-id":"zpfb"}
CONFIG_HOSPITAL_ADAPTER_ROUTE_MODE_MAP={"gk":"local","zpfb":"direct"}
CONFIG_HOSPITAL_ADAPTER_DIRECT_URL_MAP={"zpfb":"http://120.224.145.13:8082"}
CONFIG_GK_JHIDS_PROXY_URL=https://crm.gykqyy.com/ykt/getYoucanData/gkJhids CONFIG_GK_JHIDS_PROXY_URL=https://crm.gykqyy.com/ykt/getYoucanData/gkJhids
CONFIG_RPC_TIMEOUT_MS=30000 CONFIG_RPC_TIMEOUT_MS=30000

View File

@ -1,8 +1,5 @@
CONFIG_NODE_PORT=8082 CONFIG_NODE_PORT=18082
CONFIG_ADAPTER_ROLE=standalone CONFIG_ADAPTER_ROLE=standalone
CONFIG_GATEWAY_URL=
CONFIG_EDGE_ID=zpfb-edge-1
CONFIG_EDGE_HOSPITAL_CODE=zpfb
CONFIG_HOSPITAL_ADAPTER_CORP_MAP={} CONFIG_HOSPITAL_ADAPTER_CORP_MAP={}
CONFIG_MONGO_ENABLED=false CONFIG_MONGO_ENABLED=false

15
.env.zpfb.example Normal file
View File

@ -0,0 +1,15 @@
CONFIG_NODE_PORT=18082
CONFIG_ADAPTER_ROLE=standalone
CONFIG_HOSPITAL_ADAPTER_CORP_MAP={}
CONFIG_MONGO_ENABLED=false
CONFIG_ZPFB_SOURCE_TYPE=view
CONFIG_ZPFB_DB_TYPE=oracle_sqlplus
CONFIG_ZPFB_DB_HOST=<db-host>
CONFIG_ZPFB_DB_PORT=1521
CONFIG_ZPFB_DB_NAME=<db-name>
CONFIG_ZPFB_DB_USER=<db-user>
CONFIG_ZPFB_DB_PASSWORD=
CONFIG_ZPFB_DB_ORACLE_CONNECT_MODE=service
CONFIG_ZPFB_DB_SQLPLUS_PATH=<sqlplus-path>
CONFIG_ZPFB_DB_SQLPLUS_ENCODING=gbk

View File

@ -19,7 +19,7 @@ npm install
npm run dev npm run dev
``` ```
默认端口: `8082` 默认端口: `8082`,邹平子程序端口使用 `18082`
测试 gateway 本地启动: 测试 gateway 本地启动:
@ -27,13 +27,19 @@ npm run dev
npm run gateway npm run gateway
``` ```
邹平子程序本地启动。默认按 `standalone` 模式启动,内部监听 `18082`gateway 通过前置机/Nginx 暴露的地址访问,例如 `http://120.224.145.13:8082`:
```cmd
npm run zp
```
前置机轻量部署可以不安装 Mongo只安装 Node.js 和 PM2。Mongo 默认关闭,查询接口仍可直接访问医院视图。 前置机轻量部署可以不安装 Mongo只安装 Node.js 和 PM2。Mongo 默认关闭,查询接口仍可直接访问医院视图。
## 运行角色 ## 运行角色
- `standalone`: 单机调试/院内直接部署,标准资源接口和 YKT 兼容接口都由本机查询 HIS。 - `standalone`: 单机调试/院内直接部署,标准资源接口和 YKT 兼容接口都由本机查询 HIS。
- `gateway`: 中心网关,给 `ytk-customer-service` 调用,并通过 WebSocket 转发到院内 Agent。 - `gateway`: 中心网关,给 `ytk-customer-service` 调用,并按医院配置选择 `local``direct``edge` 路由
- `edge`: 院内 Agent主动连接中心网关只负责访问院内 HIS。 - `edge`: 院内 Agent主动连接中心网关只负责访问院内 HIS,适合医院不开放入站端口的场景
关键配置: 关键配置:
@ -43,6 +49,8 @@ CONFIG_GATEWAY_URL=http://中心网关地址
CONFIG_EDGE_ID=zpfb-edge-1 CONFIG_EDGE_ID=zpfb-edge-1
CONFIG_EDGE_HOSPITAL_CODE=zpfb CONFIG_EDGE_HOSPITAL_CODE=zpfb
CONFIG_HOSPITAL_ADAPTER_CORP_MAP={"corpId":"zpfb"} CONFIG_HOSPITAL_ADAPTER_CORP_MAP={"corpId":"zpfb"}
CONFIG_HOSPITAL_ADAPTER_ROUTE_MODE_MAP={"zpfb":"direct"}
CONFIG_HOSPITAL_ADAPTER_DIRECT_URL_MAP={"zpfb":"http://120.224.145.13:8082"}
``` ```
## Windows 前置机部署 ## Windows 前置机部署
@ -52,7 +60,7 @@ CONFIG_HOSPITAL_ADAPTER_CORP_MAP={"corpId":"zpfb"}
```cmd ```cmd
cd /d C:\code\yk\ykt\hospital-adapter-service cd /d C:\code\yk\ykt\hospital-adapter-service
npm install npm install
npm run build -- --env zpfb npm run build:zp
``` ```
构建输出: 构建输出:
@ -79,26 +87,30 @@ PM2 启动示例:
pm2 start bundle.js --name hospital-adapter-service pm2 start bundle.js --name hospital-adapter-service
``` ```
edge bundle 也是同一个构建命令,只是每家医院用自己的 env 文件。比如 `.env.zpfb`: 邹平子程序 bundle 也是同一个构建命令,只是每家医院用自己的 env 文件。比如 `.env.zpfb`:
```bash ```bash
CONFIG_ADAPTER_ROLE=edge CONFIG_NODE_PORT=18082
CONFIG_GATEWAY_URL=http://中心网关地址 CONFIG_ADAPTER_ROLE=standalone
CONFIG_EDGE_ID=zpfb-edge-1
CONFIG_EDGE_HOSPITAL_CODE=zpfb
CONFIG_MONGO_ENABLED=false CONFIG_MONGO_ENABLED=false
``` ```
然后重新构建: 然后重新构建:
```cmd ```cmd
npm run build -- --env zpfb npm run build:zp
``` ```
把生成的 `dist\bundle.zpfb.js` 复制到前置机后启动,可以复制时改名为 `bundle.js`: 把生成的 `dist\bundle.zpfb.js` 复制到前置机后启动,可以复制时改名为 `bundle.js`:
```cmd ```cmd
pm2 start bundle.js --name hospital-adapter-edge-zpfb pm2 start bundle.js --name hospital-adapter-zpfb
```
也可以不改名直接启动:
```cmd
pm2 start bundle.zpfb.js --name hospital-adapter-zpfb
``` ```
中心 gateway 用自己的 env例如 `.env.gateway`: 中心 gateway 用自己的 env例如 `.env.gateway`:
@ -113,10 +125,12 @@ npm run build:gateway
dist\bundle.gateway.js dist\bundle.gateway.js
``` ```
当前 `.env.gateway`standalone 模式,前端 HIS 选项会传固定广口 `corpId`,网关按 `corpId` 路由到本地 `gk` adapter`gk` adapter 再调用现有 dev-jcpt 服务获取原始数据,并由中继平台完成字段映射: 当前 `.env.gateway``gateway` 模式,前端 HIS 选项会传固定 `corpId`,网关按 `corpId` 路由医院编码,再按医院编码选择调用模式。广口走本地 `gk` adapter邹平走 `direct` 访问前置机公网地址:
```bash ```bash
CONFIG_HOSPITAL_ADAPTER_CORP_MAP={"wwa54dfba0b5441ef1":"gk"} CONFIG_HOSPITAL_ADAPTER_CORP_MAP={"wwa54dfba0b5441ef1":"gk","zpfb-corp-id":"zpfb"}
CONFIG_HOSPITAL_ADAPTER_ROUTE_MODE_MAP={"gk":"local","zpfb":"direct"}
CONFIG_HOSPITAL_ADAPTER_DIRECT_URL_MAP={"zpfb":"http://120.224.145.13:8082"}
CONFIG_GK_JHIDS_PROXY_URL=https://crm.gykqyy.com/ykt/getYoucanData/gkJhids CONFIG_GK_JHIDS_PROXY_URL=https://crm.gykqyy.com/ykt/getYoucanData/gkJhids
``` ```
@ -191,8 +205,10 @@ CONFIG_GK_JHIDS_TLS_REJECT_UNAUTHORIZED=true
## 查询接口 ## 查询接口
以下 curl 用于 `standalone` 或院内直连调试。邹平正式接中心网关时使用 `direct` 模式:邹平子程序内部监听 `18082`,前置机/Nginx 对外暴露 `http://120.224.145.13:8082`gateway 主动访问这个对外地址。
```bash ```bash
curl -X POST "http://127.0.0.1:8082/api/zpfb/patient/query" \ curl -X POST "http://127.0.0.1:18082/api/zpfb/patient/query" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"id_num":"110101199001011234","page":1,"pageSize":20}' -d '{"id_num":"110101199001011234","page":1,"pageSize":20}'
``` ```
@ -200,7 +216,7 @@ curl -X POST "http://127.0.0.1:8082/api/zpfb/patient/query" \
YKT HIS 兼容入口: YKT HIS 兼容入口:
```bash ```bash
curl -X POST "http://127.0.0.1:8082/api/ykt/customerHisSync" \ curl -X POST "http://127.0.0.1:18082/api/ykt/customerHisSync" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"hospitalCode":"zpfb","type":"getHisCustomerArchive","idCard":"身份证号"}' -d '{"hospitalCode":"zpfb","type":"getHisCustomerArchive","idCard":"身份证号"}'
``` ```
@ -254,6 +270,10 @@ CONFIG_HOSPITAL_ADAPTER_GATEWAY_URL=http://127.0.0.1:8082
邹平妇保示例: 邹平妇保示例:
```bash ```bash
CONFIG_NODE_PORT=18082
CONFIG_ADAPTER_ROLE=standalone
CONFIG_MONGO_ENABLED=false
CONFIG_ZPFB_SOURCE_TYPE=view CONFIG_ZPFB_SOURCE_TYPE=view
CONFIG_ZPFB_DB_TYPE=oracle_sqlplus CONFIG_ZPFB_DB_TYPE=oracle_sqlplus
CONFIG_ZPFB_DB_HOST=<db-host> CONFIG_ZPFB_DB_HOST=<db-host>
@ -266,14 +286,17 @@ CONFIG_ZPFB_DB_SQLPLUS_PATH=<sqlplus-path>
CONFIG_ZPFB_DB_SQLPLUS_ENCODING=gbk CONFIG_ZPFB_DB_SQLPLUS_ENCODING=gbk
``` ```
前置机 CMD 临时启动示例仅用于未打包时排查。正常单文件部署时,相关变量应写入本机 `.env.production` 后重新 `npm run build`。 前置机 CMD 临时启动示例仅用于未打包时排查。正常单文件部署时,相关变量应写入本机 `.env.zpfb` 后重新 `npm run build:zp`。
```cmd ```cmd
set CONFIG_NODE_PORT=8082 set CONFIG_NODE_PORT=18082
set CONFIG_ADAPTER_ROLE=standalone
set CONFIG_MONGO_ENABLED=false set CONFIG_MONGO_ENABLED=false
node index.js node index.js
``` ```
说明: 邹平正式接中心网关时使用 `direct`,由 gateway 主动访问 `http://120.224.145.13:8082`;前置机内部子程序仍监听 `18082`
邹平第一版默认只启用: 邹平第一版默认只启用:
- `patient` -> `v_youcan_patient` - `patient` -> `v_youcan_patient`
@ -284,7 +307,7 @@ node index.js
## 同步标准快照 ## 同步标准快照
```bash ```bash
curl -X POST "http://127.0.0.1:8082/api/his/sync" \ curl -X POST "http://127.0.0.1:18082/api/his/sync" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"hospitalCode":"zpfb","resource":"patient","maxPages":1,"pageSize":200}' -d '{"hospitalCode":"zpfb","resource":"patient","maxPages":1,"pageSize":200}'
``` ```

View File

@ -8,7 +8,8 @@ const { handleQuery, queryStandardResource } = require("./src/core/query-control
const { handleSync } = require("./src/sync/sync-controller"); const { handleSync } = require("./src/sync/sync-controller");
const { connectMongo, ensureIndexes, isMongoAvailable, getMongoDisabledReason } = require("./src/mongo"); const { connectMongo, ensureIndexes, isMongoAvailable, getMongoDisabledReason } = require("./src/mongo");
const { handleYktCustomerHisSync } = require("./src/ykt/customer-his-sync"); const { handleYktCustomerHisSync } = require("./src/ykt/customer-his-sync");
const { attachGateway, queryEdgeResource, queryEdgeCustomerHisSync, listEdges } = require("./src/gateway/edge-registry"); const { attachGateway, listEdges } = require("./src/gateway/edge-registry");
const { queryGatewayResource, queryGatewayCustomerHisSync } = require("./src/gateway/router");
const { startEdgeClient } = require("./src/edge/client"); const { startEdgeClient } = require("./src/edge/client");
loadEnv(); loadEnv();
@ -41,11 +42,11 @@ app.get("/healthz", (req, res) => {
app.post("/api/ykt/customerHisSync", async (req, res) => { app.post("/api/ykt/customerHisSync", async (req, res) => {
const queryResource = const queryResource =
config.role === "gateway" config.role === "gateway"
? queryEdgeResource ? queryGatewayResource
: ({ hospitalCode, resource, query }) => queryStandardResource({ hospitalCode, resource, query, saveSnapshot: false }).then((result) => ({ status: "success", message: "查询成功", ...result })); : ({ hospitalCode, resource, query }) => queryStandardResource({ hospitalCode, resource, query, saveSnapshot: false }).then((result) => ({ status: "success", message: "查询成功", ...result }));
return handleYktCustomerHisSync(req, res, { return handleYktCustomerHisSync(req, res, {
queryResource, queryResource,
forwardCustomHisSync: config.role === "gateway" ? queryEdgeCustomerHisSync : undefined, forwardCustomHisSync: config.role === "gateway" ? queryGatewayCustomerHisSync : undefined,
}); });
}); });

View File

@ -6,12 +6,15 @@
"scripts": { "scripts": {
"dev": "node index.js", "dev": "node index.js",
"gateway": "cross-env NODE_ENV=gateway node index.js", "gateway": "cross-env NODE_ENV=gateway node index.js",
"zp": "cross-env NODE_ENV=zpfb node index.js",
"pro": "cross-env NODE_ENV=production node index.js", "pro": "cross-env NODE_ENV=production node index.js",
"build": "node esbuild.config.js", "build": "node esbuild.config.js",
"build:gateway": "node esbuild.config.js --env gateway --out dist/bundle.gateway.js", "build:gateway": "node esbuild.config.js --env gateway --out dist/bundle.gateway.js",
"build:zp": "node esbuild.config.js --env zpfb --out dist/bundle.zpfb.js",
"build:pro": "cross-env NODE_ENV=production node esbuild.config.js", "build:pro": "cross-env NODE_ENV=production node esbuild.config.js",
"start": "node dist/bundle.js", "start": "node dist/bundle.js",
"start:gateway": "node dist/bundle.gateway.js" "start:gateway": "node dist/bundle.gateway.js",
"start:zp": "node dist/bundle.zpfb.js"
}, },
"dependencies": { "dependencies": {
"axios": "^1.7.7", "axios": "^1.7.7",

View File

@ -42,6 +42,8 @@ function getConfig() {
edgeReconnectMs: parsePositiveInteger(process.env.CONFIG_EDGE_RECONNECT_MS, 5000), edgeReconnectMs: parsePositiveInteger(process.env.CONFIG_EDGE_RECONNECT_MS, 5000),
rpcTimeoutMs: parsePositiveInteger(process.env.CONFIG_RPC_TIMEOUT_MS, 30000), rpcTimeoutMs: parsePositiveInteger(process.env.CONFIG_RPC_TIMEOUT_MS, 30000),
corpMap: parseMap(process.env.CONFIG_HOSPITAL_ADAPTER_CORP_MAP || process.env.CONFIG_CORP_HOSPITAL_MAP || ""), corpMap: parseMap(process.env.CONFIG_HOSPITAL_ADAPTER_CORP_MAP || process.env.CONFIG_CORP_HOSPITAL_MAP || ""),
routeModeMap: parseMap(process.env.CONFIG_HOSPITAL_ADAPTER_ROUTE_MODE_MAP || ""),
directUrlMap: parseMap(process.env.CONFIG_HOSPITAL_ADAPTER_DIRECT_URL_MAP || ""),
mongoEnabled: parseBoolean(process.env.CONFIG_MONGO_ENABLED, false), mongoEnabled: parseBoolean(process.env.CONFIG_MONGO_ENABLED, false),
mongoOptional: parseBoolean(process.env.CONFIG_MONGO_OPTIONAL, true), mongoOptional: parseBoolean(process.env.CONFIG_MONGO_OPTIONAL, true),
mongoUri: process.env.CONFIG_MONGO_URI || "", mongoUri: process.env.CONFIG_MONGO_URI || "",

96
src/gateway/router.js Normal file
View File

@ -0,0 +1,96 @@
const axios = require("axios");
const { getConfig } = require("../core/config");
const { queryStandardResource } = require("../core/query-controller");
const { getAdapter } = require("../adapters");
const { queryEdgeResource, queryEdgeCustomerHisSync } = require("./edge-registry");
async function queryGatewayResource({ hospitalCode, resource, query }) {
const mode = getRouteMode(hospitalCode);
if (mode === "local") {
const result = await queryStandardResource({ hospitalCode, resource, query, saveSnapshot: false });
return { status: "success", message: "查询成功", ...result };
}
if (mode === "direct") return queryDirectResource({ hospitalCode, resource, query });
return queryEdgeResource({ hospitalCode, resource, query });
}
async function queryGatewayCustomerHisSync({ hospitalCode, event }) {
const mode = getRouteMode(hospitalCode);
if (mode === "local") {
const adapter = await getAdapter(hospitalCode);
if (adapter && typeof adapter.customerHisSync === "function") return adapter.customerHisSync(event);
throwGatewayError("本地医院适配器未提供 HIS 兼容接口", "LOCAL_ADAPTER_UNSUPPORTED");
}
if (mode === "direct") return queryDirectCustomerHisSync({ hospitalCode, event });
return queryEdgeCustomerHisSync({ hospitalCode, event });
}
async function queryDirectResource({ hospitalCode, resource, query }) {
const baseUrl = getDirectBaseUrl(hospitalCode);
const url = `${baseUrl}/api/${encodeURIComponent(hospitalCode)}/${encodeURIComponent(resource)}/query`;
const response = await requestDirect(url, query || {});
return normalizeStandardResponse(response);
}
async function queryDirectCustomerHisSync({ hospitalCode, event }) {
const baseUrl = getDirectBaseUrl(hospitalCode);
const url = `${baseUrl}/api/ykt/customerHisSync`;
const response = await requestDirect(url, { ...(event || {}), hospitalCode });
if (response && response.success === false) return { success: false, message: response.message || "医院子程序查询失败", list: [], err: response.err || response.code };
if (response && response.success === true) return response;
if (response && response.status === "fail") return { success: false, message: response.message || "医院子程序查询失败", list: [], err: response.code };
return response;
}
async function requestDirect(url, payload) {
try {
const res = await axios.post(url, payload, {
timeout: getConfig().timeoutMs,
headers: { "Content-Type": "application/json" },
});
return res.data;
} catch (err) {
const message = (err.response && err.response.data && err.response.data.message) || err.message || "医院子程序调用失败";
throwGatewayError(message, (err.response && err.response.data && err.response.data.code) || err.code || "DIRECT_REQUEST_FAILED");
}
}
function normalizeStandardResponse(response) {
if (!response) return { status: "fail", message: "医院子程序无响应", code: "DIRECT_EMPTY_RESPONSE", data: [] };
if (response.status === "success" || response.status === "fail") return response;
if (response.success === false) return { status: "fail", message: response.message || "医院子程序查询失败", code: response.err || response.code || "DIRECT_QUERY_FAILED", data: [] };
return {
status: "success",
message: response.message || "查询成功",
data: Array.isArray(response.data) ? response.data : Array.isArray(response.list) ? response.list : [],
page: response.page || 1,
pageSize: response.pageSize || 20,
total: response.total || 0,
};
}
function getRouteMode(hospitalCode) {
const config = getConfig();
const raw = config.routeModeMap[hospitalCode] || config.routeModeMap["*"] || "edge";
const mode = String(raw || "").trim().toLowerCase();
return ["local", "direct", "edge"].includes(mode) ? mode : "edge";
}
function getDirectBaseUrl(hospitalCode) {
const config = getConfig();
const url = config.directUrlMap[hospitalCode] || config.directUrlMap["*"];
if (!url) throwGatewayError(`未配置医院 direct 地址: ${hospitalCode}`, "DIRECT_URL_MISSING");
return String(url).replace(/\/+$/, "");
}
function throwGatewayError(message, code) {
const err = new Error(message);
err.code = code;
throw err;
}
module.exports = {
queryGatewayResource,
queryGatewayCustomerHisSync,
getRouteMode,
};