hn-hlw-service/index.js

587 lines
17 KiB
JavaScript
Raw Normal View History

2026-07-27 11:28:33 +08:00
const envFile = `.env.${process.env.NODE_ENV || "development"}`;
require("dotenv").config({ path: envFile });
const express = require("express");
const cors = require("cors");
const timeout = require("connect-timeout");
const mongodb = require("./mongodb");
const upload = require("./upload");
const bodyParser = require("body-parser");
const corsUtils = require("./utils/cors");
const logger = require("./utils/logger");
const trade = require("./hlw/trade");
const fengniao = require("./hlw/fengniao");
const {
safeSend,
sendSuccess,
sendError,
asyncHandler,
} = require("./utils/response");
const {
serverConfig,
optimizeServer,
setupProcessHandlers,
} = require("./utils/server-config");
const {
monitoringMiddleware,
errorMonitoringMiddleware,
monitor,
} = require("./utils/performance-monitor");
const { checkDatabaseHealth, safeGetDatabase } = require("./utils/db-health");
const member = require("./member");
const todo = require("./toDoEvents");
const groupmsg = require("./groupmsg");
const corp = require("./corp");
const knowledgeBase = require("./knowledgeBase");
const survery = require("./survery");
const weCom = require("./weCom");
const system = require("./system");
const sessionArchive = require("./sessionArchive");
const customerHisSync = require("./customerHisSync");
const hlw = require("./hlw");
2026-07-31 16:22:09 +08:00
const automation = require("./hlw/automation");
2026-08-25 17:03:56 +08:00
const imImageArchive = require("./hlw/im-image-archive");
2026-09-15 13:44:07 +08:00
const { getOrCreatePrescriptionPdf, initializeStorage: initializePrescriptionPdfStorage } = require("./hlw/prescription-pdf");
2026-07-27 11:28:33 +08:00
const trigger = require("./trigger");
const consultTrigger = require("./hlw/consult-order/trigger");
const callBack = require("./callBack");
const alipayApi = require("./alipayApi");
const auth = require("./auth");
const { jwtAuth, optionalJwtAuth } = require("./middleware/jwt-auth");
const { smartAuth } = require("./middleware/smart-auth");
const sms = require("./sms");
const path = require("path");
2026-09-24 18:22:05 +08:00
const fs = require("fs/promises");
2026-07-30 16:49:41 +08:00
const { ObjectId } = require("mongodb");
2026-07-27 11:28:33 +08:00
const { useUploadFileParse } = require("./upload-file-parse");
const app = express();
const PORT = process.env.CONFIG_NODE_PORT;
const { getDatabase, connectToMongoDB } = require("./mongodb");
// 禁用 X-Powered-By 响应头,防止版本信息泄露
app.disable("x-powered-by");
// 设置进程异常处理
setupProcessHandlers();
// 设置CORS中间件,允许所有来源的请求
app.use(cors(corsUtils.cors));
// 安全响应头中间件,防止版本信息泄露
app.use((req, res, next) => {
// 移除可能泄露版本信息的响应头
res.removeHeader("X-Powered-By");
res.removeHeader("Server");
// 添加安全响应头
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("X-Frame-Options", "DENY");
res.setHeader("X-XSS-Protection", "1; mode=block");
next();
});
// 添加性能监控中间件
app.use(monitoringMiddleware);
// 启动定期性能报告
monitor.startPeriodicReport(300000); // 每5分钟报告一次
app.use(timeout(serverConfig.timeout)); // 使用配置的超时时间
// 添加超时处理中间件 - 必须在所有路由之前
app.use((req, res, next) => {
// 如果请求没有超时,继续处理
if (!req.timedout) {
next();
} else {
// 如果已经超时,直接返回超时错误
sendError(res, "请求超时", "Request timeout", 408);
}
});
// 设置请求头中间件,允许自定义请求头(如果需要的话)
app.use((req, res, next) => {
res.header(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, X-Custom-Header"
);
next();
});
// 解析JSON格式的请求体 - 使用配置的大小限制
app.use(bodyParser.json({ limit: serverConfig.bodyLimit }));
// 解析urlencoded格式的请求体
app.use(
bodyParser.urlencoded({ extended: true, limit: serverConfig.bodyLimit })
);
app.use(bodyParser.text({ type: "*/xml", limit: serverConfig.bodyLimit }));
// GET请求处理器
app.get("/", (req, res) => {
sendSuccess(res, null, "GET request received");
});
// ==================== 认证相关接口(无需 JWT 验证) ====================
// 用户登录接口
app.post(
"/auth/login",
asyncHandler(async (req, res) => {
const requestData = { ...req.body, ip: req.ip };
const db = await getDatabase("corp");
const result = await auth.main({ ...requestData, type: "login" }, db);
safeSend(res, result);
})
);
// 小程序授权登录接口
app.post(
"/auth/miniapp/login",
asyncHandler(async (req, res) => {
const requestData = { ...req.body, ip: req.ip };
const db = await getDatabase("corp");
const result = await auth.main(
{ ...requestData, type: "miniProgramLogin" },
db
);
safeSend(res, result);
})
);
// Token 刷新接口
app.post(
"/auth/refresh",
asyncHandler(async (req, res) => {
const requestData = req.body;
const result = await auth.main({ ...requestData, type: "refreshToken" });
safeSend(res, result);
})
);
app.post(
"/getCloudServiceData",
asyncHandler(async (req, res) => {
try {
const db = await getDatabase("Internet-hospital");
const item = await hlw({ type: "getLastestPurchaseOrder", ciphertext: req.body.data }, db);
res.send(item);
} catch (error) {
res.send({ success: false, message: `获取最新购药订单错误: ${error.message}` });
}
})
);
// 获取当前用户信息(需要 JWT 验证)
app.get(
"/auth/current",
jwtAuth,
asyncHandler(async (req, res) => {
const db = await getDatabase("corp");
const result = await auth.main({ type: "getCurrentUser" }, db, req.user);
safeSend(res, result);
})
);
// 用户退出登录(需要 JWT 验证)
app.post(
"/auth/logout",
jwtAuth,
asyncHandler(async (req, res) => {
const db = await getDatabase("corp");
const result = await auth.main({ type: "logout" }, db, req.user);
safeSend(res, result);
})
);
// ==================== 以下接口需要 JWT 验证 ====================
// 连接数据库
2026-07-31 16:22:09 +08:00
const databaseReadyPromise = connectToMongoDB().catch((err) => {
2026-07-27 11:28:33 +08:00
logger.error("数据库连接失败,服务器将无法正常工作:", err);
// 不退出进程,让错误处理中间件处理后续请求
2026-07-31 16:22:09 +08:00
return null;
2026-07-27 11:28:33 +08:00
});
2026-08-25 18:43:04 +08:00
// 启动时立即创建并校验 IM 图片归档目录,避免等到首张图片下载时才暴露路径或权限问题。
const imageStorageReadyPromise = imImageArchive.initializeStorage().catch((err) => {
logger.error("腾讯 IM 图片归档目录初始化失败:", err);
return null;
});
2026-09-15 13:44:07 +08:00
const prescriptionPdfStorageReadyPromise = initializePrescriptionPdfStorage().catch((err) => {
logger.error("处方 PDF 目录初始化失败:", err);
return null;
});
2026-07-27 11:28:33 +08:00
app.get(
"/zyt/stats",
asyncHandler(async (req, res) => {
try {
const db = await getDatabase("Internet-hospital");
const item = await hlw(
{ ...req.query, type: "hlwStats", statsType: req.query.type },
db
);
safeSend(res, item);
} catch (error) {
sendError(res, "数据库连接失败", error.message, 500);
}
})
);
app.get(
"/zyt/2excel",
2026-07-30 16:49:41 +08:00
jwtAuth,
2026-07-27 11:28:33 +08:00
asyncHandler(async (req, res) => {
try {
const requestData = req.query;
const db = await getDatabase("Internet-hospital");
2026-07-30 16:49:41 +08:00
const downloadParams = {
...req.query,
type: "downloadXlsx",
downloadType: req.query.type,
};
if (
downloadParams.downloadType === "downloadRx" &&
downloadParams.prescriptionType === "storeMedicinePurchase"
) {
let drugStoreId =
req.user && typeof req.user.drugStoreId === "string"
? req.user.drugStoreId.trim()
: "";
if (!drugStoreId && req.user && ObjectId.isValid(req.user.userId)) {
const corpDb = await getDatabase("corp");
const account = await corpDb.collection("corp-member").findOne(
{ _id: new ObjectId(req.user.userId) },
{ projection: { drugStoreId: 1 } }
);
drugStoreId =
account && typeof account.drugStoreId === "string"
? account.drugStoreId.trim()
: "";
}
if (!drugStoreId) {
return safeSend(res, {
success: false,
message: "当前账号未关联门店",
});
}
downloadParams.storeId = drugStoreId;
delete downloadParams.storeLoginNo;
}
2026-07-27 11:28:33 +08:00
const message = await hlw(
2026-07-30 16:49:41 +08:00
downloadParams,
2026-07-27 11:28:33 +08:00
db,
res
);
if (message) {
safeSend(res, message);
}
} catch (error) {
sendError(res, "数据库连接失败", error.message, 500);
}
})
);
2026-09-15 13:44:07 +08:00
// 服务端渲染处方笺。Chromium 路径由 PUPPETEER_EXECUTABLE_PATH 配置,避免应用镜像携带浏览器。
app.get(
"/hlw/prescriptions/:id/pdf",
asyncHandler(async (req, res) => {
const db = await getDatabase("Internet-hospital");
const paperSize = req.query.paperSize === "A4" ? "A4" : "A5";
const { pdfPath } = await getOrCreatePrescriptionPdf(db, req.params.id, { paperSize });
res.set({
"Content-Type": "application/pdf",
"Content-Disposition": `inline; filename=\"prescription-${req.params.id}.pdf\"`,
"Cache-Control": "no-cache",
});
await new Promise((resolve, reject) => {
res.sendFile(pdfPath, (error) => (error ? reject(error) : resolve()));
});
})
);
2026-09-24 18:22:05 +08:00
// 返回处方笺 PDF 的 Base64 内容,供客户端上传或嵌入预览。
app.get(
"/hlw/prescriptions/:id/base64",
asyncHandler(async (req, res) => {
const db = await getDatabase("Internet-hospital");
const paperSize = req.query.paperSize === "A4" ? "A4" : "A5";
const { pdfPath } = await getOrCreatePrescriptionPdf(db, req.params.id, { paperSize });
sendSuccess(res, (await fs.readFile(pdfPath)).toString("base64"), "查询成功");
})
);
2026-07-27 11:28:33 +08:00
// POST请求处理器
app.post(
"/getYoucanData/member",
jwtAuth,
asyncHandler(async (req, res) => {
const requestData = req.body;
try {
const db = await getDatabase("admin");
let item = await member.main(requestData, db);
safeSend(res, item);
} catch (error) {
sendError(res, "数据库连接失败", error.message, 500);
}
})
);
app.post(
"/getYoucanData/corp",
smartAuth, // 使用智能鉴权,支持白名单 type 跳过鉴权
asyncHandler(async (req, res) => {
const requestData = req.body;
const db = await getDatabase("corp");
const item = await corp.main(requestData, db, req); // 传递 user 信息
logger.info(`接口请求:${JSON.stringify(requestData)}`);
logger.info(`接口输出:${JSON.stringify(item)}`);
safeSend(res, item);
})
);
app.post(
"/getYoucanData/knowledgeBase",
smartAuth, // 使用智能鉴权,支持白名单 type 跳过鉴权(如文章相关接口)
asyncHandler(async (req, res) => {
const requestData = req.body;
const db = await getDatabase("corp");
const item = await knowledgeBase.main(requestData, db, req.user); // 传递 user 信息以便需要时使用
safeSend(res, item);
})
);
app.post(
"/getYoucanData/survery",
jwtAuth,
asyncHandler(async (req, res) => {
const requestData = req.body;
const db = await getDatabase("corp");
const item = await survery.main(requestData, db);
safeSend(res, item);
})
);
app.post(
"/getYoucanData/system",
smartAuth, // 使用智能鉴权,支持白名单 type 跳过鉴权
asyncHandler(async (req, res) => {
const requestData = req.body;
const db = await getDatabase("corp");
const item = await system.main(requestData, db, req.user); // 传递 user 信息
safeSend(res, item);
})
);
app.post(
"/getYoucanData/sms",
asyncHandler(async (req, res) => {
const requestData = req.body;
const item = await sms.main(requestData);
safeSend(res, item);
})
);
// 添加静态HTML文件访问路径
app.use("/", express.static(path.join(__dirname, "./static")));
app.post(
"/getAlipayData",
smartAuth, // 使用智能鉴权,支持白名单 type 跳过鉴权
asyncHandler(async (req, res) => {
const requestData = req.body;
const item = await alipayApi.main(requestData);
safeSend(res, item);
})
);
app.post(
"/getYoucanData/hlw",
smartAuth, // 使用智能鉴权,支持白名单 type 跳过鉴权
asyncHandler(async (req, res) => {
const requestData = req.body;
const db = await getDatabase("Internet-hospital");
const item = await hlw(requestData, db, req.user); // 传递 user 信息
safeSend(res, item);
})
);
app.post(
"/alipay-pay-notify",
smartAuth, // 使用智能鉴权,支持白名单 type 跳过鉴权
asyncHandler(async (req, res) => {
await trade.handlePayNotify(req, res);
})
);
app.post(
"/IMCallBack",
asyncHandler(async (req, res) => {
const requestData = req.body;
const db = await getDatabase("Internet-hospital");
2026-08-25 17:03:56 +08:00
const storedMessage = imImageArchive.prepareMessageForArchive(requestData);
const result = await hlw(
2026-07-27 11:28:33 +08:00
{
type: "addChatMsg",
2026-08-25 17:03:56 +08:00
params: storedMessage,
2026-07-27 11:28:33 +08:00
},
db
);
2026-08-25 17:03:56 +08:00
if (
result &&
result.success &&
result.res &&
result.res.insertedId &&
storedMessage.imageArchive
) {
imImageArchive.enqueueArchive(db, result.res.insertedId);
}
2026-07-27 11:28:33 +08:00
sendSuccess(res, null, "请求成功");
})
);
2026-08-25 17:03:56 +08:00
// 暂不校验 JWT;接口只接受 MsgId,并从数据库中解析受控的私有文件路径。
app.get(
"/im-chat/image/:msgId",
asyncHandler(async (req, res) => {
const db = await getDatabase("Internet-hospital");
await imImageArchive.sendArchivedImage(req, res, db);
})
);
2026-07-27 11:28:33 +08:00
app.get(
"/callback/data",
asyncHandler(async (req, res) => {
const requestData = req.query;
console.log(requestData);
const item = await callBack.httpGet(requestData);
safeSend(res, item, false);
})
);
app.get(
"/callback/command",
asyncHandler(async (req, res) => {
const requestData = req.query;
const item = await callBack.httpGet(requestData);
safeSend(res, item, false);
})
);
app.post(
"/callback/command",
asyncHandler(async (req, res) => {
const requestData = req.body;
const item = await callBack.httpPost(requestData);
safeSend(res, "success", false);
})
);
app.use("/uploads", express.static(path.join(__dirname, "../uploads")));
app.use("/", express.static(path.join(__dirname, "./static")));
// 性能监控API端点
app.get(
"/monitor/stats",
asyncHandler(async (req, res) => {
const stats = monitor.getStats();
sendSuccess(res, stats, "性能统计获取成功");
})
);
app.post(
"/monitor/reset",
asyncHandler(async (req, res) => {
monitor.reset();
sendSuccess(res, null, "性能统计已重置");
})
);
// 数据库健康检查端点
app.get(
"/monitor/db-health",
asyncHandler(async (req, res) => {
const isHealthy = await checkDatabaseHealth();
if (isHealthy) {
sendSuccess(res, { status: "healthy" }, "数据库连接正常");
} else {
sendError(res, "数据库连接异常", null, 503);
}
})
);
// 文件上传接口
upload.uploadFile(app);
// 错误监控中间件
app.use(errorMonitoringMiddleware);
// 全局错误处理中间件
app.use((err, req, res, next) => {
logger.error("全局错误:", err);
// 记录错误到监控系统
monitor.recordError(err, req);
if (!res.headersSent) {
sendError(
res,
`服务器内部错误: ${err && err.message ? err.message : ''}`,
err && err.message ? err.message : "Internal Server Error",
500
);
}
});
app.post(
"/getYoucanData/logout",
asyncHandler(async (req, res) => {
const requestData = req.body;
requestData.type = 'loginout';
const db = await getDatabase("corp");
const item = await corp.main(requestData, db, req); // 传递 user 信息
safeSend(res, item);
})
);
useUploadFileParse(app);
// 启动服务器
const server = app.listen(PORT, "0.0.0.0", () => {
logger.info(`Server is running on http://localhost:${PORT}`);
});
// 应用服务器优化设置
optimizeServer(server);
console.log(process.env.CONFIG_NODE_ENV);
// 定时任务
trigger.initSchedule();
2026-07-31 16:22:09 +08:00
databaseReadyPromise.then(async (connected) => {
if (!connected) return;
try {
2026-08-25 18:43:04 +08:00
const imageStorageRoot = await imageStorageReadyPromise;
if (!imageStorageRoot) {
throw new Error("腾讯 IM 图片归档目录不可用");
}
2026-07-31 16:22:09 +08:00
const internetHospitalDb = await getDatabase("Internet-hospital");
await consultTrigger(
{
type: "recoverDelayedTasks",
},
internetHospitalDb
);
await automation.start(internetHospitalDb);
2026-08-25 17:03:56 +08:00
const resumedImageArchives = await imImageArchive.resumePendingArchives(
internetHospitalDb
);
if (resumedImageArchives) {
logger.info(`已恢复 ${resumedImageArchives} 个腾讯 IM 图片归档任务`);
}
2026-07-31 16:22:09 +08:00
} catch (error) {
logger.error("恢复问诊延迟任务或启动自动化任务失败:", error);
}
2026-07-27 11:28:33 +08:00
});