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
|
|
|
|
});
|