From b8a6e9a8e11183170d2d98d19c992529c4a147e0 Mon Sep 17 00:00:00 2001 From: Jafeng <2998840497@qq.com> Date: Mon, 22 Jun 2026 15:49:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E4=B8=AD=E8=BD=AC?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=20direct=20=E8=B7=AF=E7=94=B1=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - gateway 新增 local/direct/edge 多模式路由 - 邹平改为 direct 模式,由 gateway 主动访问前置机地址 - 更新 gateway/zpfb 环境变量示例和部署说明 --- .env.gateway.example | 9 ++-- .env.production.example | 5 +-- .env.zpfb.example | 15 +++++++ README.md | 59 +++++++++++++++++-------- index.js | 7 +-- package.json | 5 ++- src/core/config.js | 2 + src/gateway/router.js | 96 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 169 insertions(+), 29 deletions(-) create mode 100644 .env.zpfb.example create mode 100644 src/gateway/router.js diff --git a/.env.gateway.example b/.env.gateway.example index 49f5a87..8f5eb88 100644 --- a/.env.gateway.example +++ b/.env.gateway.example @@ -1,9 +1,12 @@ CONFIG_NODE_PORT=8082 -CONFIG_ADAPTER_ROLE=standalone +CONFIG_ADAPTER_ROLE=gateway CONFIG_MONGO_ENABLED=false -# 测试环境: 前端按 HIS 选项传固定 corpId,网关按 corpId 路由到 gk 映射。 -CONFIG_HOSPITAL_ADAPTER_CORP_MAP={"wwa54dfba0b5441ef1":"gk"} +# 测试环境: 前端按 HIS 选项传固定 corpId,网关按 corpId 路由到医院编码。 +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_RPC_TIMEOUT_MS=30000 diff --git a/.env.production.example b/.env.production.example index 05b0c3f..d9d47ca 100644 --- a/.env.production.example +++ b/.env.production.example @@ -1,8 +1,5 @@ -CONFIG_NODE_PORT=8082 +CONFIG_NODE_PORT=18082 CONFIG_ADAPTER_ROLE=standalone -CONFIG_GATEWAY_URL= -CONFIG_EDGE_ID=zpfb-edge-1 -CONFIG_EDGE_HOSPITAL_CODE=zpfb CONFIG_HOSPITAL_ADAPTER_CORP_MAP={} CONFIG_MONGO_ENABLED=false diff --git a/.env.zpfb.example b/.env.zpfb.example new file mode 100644 index 0000000..02c4c1e --- /dev/null +++ b/.env.zpfb.example @@ -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= +CONFIG_ZPFB_DB_PORT=1521 +CONFIG_ZPFB_DB_NAME= +CONFIG_ZPFB_DB_USER= +CONFIG_ZPFB_DB_PASSWORD= +CONFIG_ZPFB_DB_ORACLE_CONNECT_MODE=service +CONFIG_ZPFB_DB_SQLPLUS_PATH= +CONFIG_ZPFB_DB_SQLPLUS_ENCODING=gbk diff --git a/README.md b/README.md index 6d8b957..51a48b5 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ npm install npm run dev ``` -默认端口: `8082` +默认端口: `8082`,邹平子程序端口使用 `18082`。 测试 gateway 本地启动: @@ -27,13 +27,19 @@ npm run dev npm run gateway ``` +邹平子程序本地启动。默认按 `standalone` 模式启动,内部监听 `18082`;gateway 通过前置机/Nginx 暴露的地址访问,例如 `http://120.224.145.13:8082`: + +```cmd +npm run zp +``` + 前置机轻量部署可以不安装 Mongo,只安装 Node.js 和 PM2。Mongo 默认关闭,查询接口仍可直接访问医院视图。 ## 运行角色 - `standalone`: 单机调试/院内直接部署,标准资源接口和 YKT 兼容接口都由本机查询 HIS。 -- `gateway`: 中心网关,给 `ytk-customer-service` 调用,并通过 WebSocket 转发到院内 Agent。 -- `edge`: 院内 Agent,主动连接中心网关,只负责访问院内 HIS。 +- `gateway`: 中心网关,给 `ytk-customer-service` 调用,并按医院配置选择 `local`、`direct` 或 `edge` 路由。 +- `edge`: 院内 Agent,主动连接中心网关,只负责访问院内 HIS,适合医院不开放入站端口的场景。 关键配置: @@ -43,6 +49,8 @@ CONFIG_GATEWAY_URL=http://中心网关地址 CONFIG_EDGE_ID=zpfb-edge-1 CONFIG_EDGE_HOSPITAL_CODE=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 前置机部署 @@ -52,7 +60,7 @@ CONFIG_HOSPITAL_ADAPTER_CORP_MAP={"corpId":"zpfb"} ```cmd cd /d C:\code\yk\ykt\hospital-adapter-service npm install -npm run build -- --env zpfb +npm run build:zp ``` 构建输出: @@ -79,26 +87,30 @@ PM2 启动示例: pm2 start bundle.js --name hospital-adapter-service ``` -edge bundle 也是同一个构建命令,只是每家医院用自己的 env 文件。比如 `.env.zpfb`: +邹平子程序 bundle 也是同一个构建命令,只是每家医院用自己的 env 文件。比如 `.env.zpfb`: ```bash -CONFIG_ADAPTER_ROLE=edge -CONFIG_GATEWAY_URL=http://中心网关地址 -CONFIG_EDGE_ID=zpfb-edge-1 -CONFIG_EDGE_HOSPITAL_CODE=zpfb +CONFIG_NODE_PORT=18082 +CONFIG_ADAPTER_ROLE=standalone CONFIG_MONGO_ENABLED=false ``` 然后重新构建: ```cmd -npm run build -- --env zpfb +npm run build:zp ``` 把生成的 `dist\bundle.zpfb.js` 复制到前置机后启动,可以复制时改名为 `bundle.js`: ```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`: @@ -113,10 +125,12 @@ npm run build:gateway 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 -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 ``` @@ -191,8 +205,10 @@ CONFIG_GK_JHIDS_TLS_REJECT_UNAUTHORIZED=true ## 查询接口 +以下 curl 用于 `standalone` 或院内直连调试。邹平正式接中心网关时使用 `direct` 模式:邹平子程序内部监听 `18082`,前置机/Nginx 对外暴露 `http://120.224.145.13:8082`,gateway 主动访问这个对外地址。 + ```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" \ -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 兼容入口: ```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" \ -d '{"hospitalCode":"zpfb","type":"getHisCustomerArchive","idCard":"身份证号"}' ``` @@ -254,6 +270,10 @@ CONFIG_HOSPITAL_ADAPTER_GATEWAY_URL=http://127.0.0.1:8082 邹平妇保示例: ```bash +CONFIG_NODE_PORT=18082 +CONFIG_ADAPTER_ROLE=standalone +CONFIG_MONGO_ENABLED=false + CONFIG_ZPFB_SOURCE_TYPE=view CONFIG_ZPFB_DB_TYPE=oracle_sqlplus CONFIG_ZPFB_DB_HOST= @@ -266,14 +286,17 @@ CONFIG_ZPFB_DB_SQLPLUS_PATH= CONFIG_ZPFB_DB_SQLPLUS_ENCODING=gbk ``` -前置机 CMD 临时启动示例仅用于未打包时排查。正常单文件部署时,相关变量应写入本机 `.env.production` 后重新 `npm run build`。 +前置机 CMD 临时启动示例仅用于未打包时排查。正常单文件部署时,相关变量应写入本机 `.env.zpfb` 后重新 `npm run build:zp`。 ```cmd -set CONFIG_NODE_PORT=8082 +set CONFIG_NODE_PORT=18082 +set CONFIG_ADAPTER_ROLE=standalone set CONFIG_MONGO_ENABLED=false node index.js ``` +说明: 邹平正式接中心网关时使用 `direct`,由 gateway 主动访问 `http://120.224.145.13:8082`;前置机内部子程序仍监听 `18082`。 + 邹平第一版默认只启用: - `patient` -> `v_youcan_patient` @@ -284,7 +307,7 @@ node index.js ## 同步标准快照 ```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" \ -d '{"hospitalCode":"zpfb","resource":"patient","maxPages":1,"pageSize":200}' ``` diff --git a/index.js b/index.js index c7b3a5b..324d7ac 100644 --- a/index.js +++ b/index.js @@ -8,7 +8,8 @@ const { handleQuery, queryStandardResource } = require("./src/core/query-control const { handleSync } = require("./src/sync/sync-controller"); const { connectMongo, ensureIndexes, isMongoAvailable, getMongoDisabledReason } = require("./src/mongo"); 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"); loadEnv(); @@ -41,11 +42,11 @@ app.get("/healthz", (req, res) => { app.post("/api/ykt/customerHisSync", async (req, res) => { const queryResource = config.role === "gateway" - ? queryEdgeResource + ? queryGatewayResource : ({ hospitalCode, resource, query }) => queryStandardResource({ hospitalCode, resource, query, saveSnapshot: false }).then((result) => ({ status: "success", message: "查询成功", ...result })); return handleYktCustomerHisSync(req, res, { queryResource, - forwardCustomHisSync: config.role === "gateway" ? queryEdgeCustomerHisSync : undefined, + forwardCustomHisSync: config.role === "gateway" ? queryGatewayCustomerHisSync : undefined, }); }); diff --git a/package.json b/package.json index a98aecc..165e4cb 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,15 @@ "scripts": { "dev": "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", "build": "node esbuild.config.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", "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": { "axios": "^1.7.7", diff --git a/src/core/config.js b/src/core/config.js index 90883df..48d1eac 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -42,6 +42,8 @@ function getConfig() { edgeReconnectMs: parsePositiveInteger(process.env.CONFIG_EDGE_RECONNECT_MS, 5000), rpcTimeoutMs: parsePositiveInteger(process.env.CONFIG_RPC_TIMEOUT_MS, 30000), 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), mongoOptional: parseBoolean(process.env.CONFIG_MONGO_OPTIONAL, true), mongoUri: process.env.CONFIG_MONGO_URI || "", diff --git a/src/gateway/router.js b/src/gateway/router.js new file mode 100644 index 0000000..e40cf3e --- /dev/null +++ b/src/gateway/router.js @@ -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, +};