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"); 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"); 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 验证 ==================== // 连接数据库 connectToMongoDB().catch((err) => { logger.error("数据库连接失败,服务器将无法正常工作:", err); // 不退出进程,让错误处理中间件处理后续请求 }); 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", // jwtAuth, asyncHandler(async (req, res) => { try { const requestData = req.query; const db = await getDatabase("Internet-hospital"); const message = await hlw( { ...req.query, type: "downloadXlsx", downloadType: req.query.type }, db, res ); if (message) { safeSend(res, message); } } catch (error) { sendError(res, "数据库连接失败", error.message, 500); } }) ); // 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/todo", jwtAuth, asyncHandler(async (req, res) => { const requestData = req.body; const db = await getDatabase("admin"); let item = await todo.main(requestData, db); safeSend(res, item); }) ); app.post( "/getYoucanData/groupmsg", jwtAuth, asyncHandler(async (req, res) => { const requestData = req.body; const db = await getDatabase("admin"); let item = await groupmsg.main(requestData, db); safeSend(res, item); }) ); 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/sessionArchive", jwtAuth, asyncHandler(async (req, res) => { const requestData = req.body; const db = await getDatabase("corp"); const item = await sessionArchive.main(requestData, db); safeSend(res, item); }) ); app.post( "/getYoucanData/weCom", jwtAuth, asyncHandler(async (req, res) => { const requestData = req.body; const item = await weCom.main(requestData); safeSend(res, item); }) ); app.post( "/getYoucanData/customerHisSync", jwtAuth, asyncHandler(async (req, res) => { const requestData = req.body; const item = await customerHisSync.main(requestData); 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( "/union-pay-notify", smartAuth, // 使用智能鉴权,支持白名单 type 跳过鉴权 asyncHandler(async (req, res) => { await trade.handleUnionPayNotify(req, res); }) ); app.post( "/fengniao-auth", smartAuth, // 使用智能鉴权,支持白名单 type 跳过鉴权 asyncHandler(async (req, res) => { await fengniao.handleFengNiaoAuth(req, res); }) ); app.post( "/fengniao-notify", smartAuth, // 使用智能鉴权,支持白名单 type 跳过鉴权 asyncHandler(async (req, res) => { await fengniao.notify(req, res); }) ); app.post( "/zyt-his-notify", smartAuth, // 使用智能鉴权,支持白名单 type 跳过鉴权 asyncHandler(async (req, res) => { const notifyHandler = require('./hlw/zyt-his/notify'); const requestData = req.body; const result = await notifyHandler.handleNotify(requestData); safeSend(res, result) }) ); app.post( "/IMCallBack", asyncHandler(async (req, res) => { const requestData = req.body; const db = await getDatabase("Internet-hospital"); await hlw( { type: "addChatMsg", params: requestData, }, db ); sendSuccess(res, null, "请求成功"); }) ); /** * mPaaS 音视频通话:云端自动录制回调 * - 控制台配置:通话应用 -> 功能配置 -> 配置云端自动录制及录制回调地址 * - 回调方式:POST(application/json) * - 业务侧响应:{ bizRequestId, code: 0 } */ app.post( "/mrtc/record/callback", asyncHandler(async (req, res) => { const payload = req.body && typeof req.body === "object" ? req.body : {}; const { bizRequestId, bizName, appId, workspaceId, roomId, recordId, eventCode, recordResult, } = payload; // 按文档要求回包,避免云端重试;落库失败也不影响回包 res.json({ bizRequestId: bizRequestId || "", code: 0 }); try { const db = await getDatabase("Internet-hospital"); const now = Date.now(); let orderId = ""; let doctorCode = ""; if (typeof roomId === "string" && roomId) { const order = await db.collection("consult-order").findOne( { mrtcRoomId: roomId }, { projection: { orderId: 1, doctorCode: 1 } } ); if (order && order.orderId) orderId = order.orderId; if (order && order.doctorCode) doctorCode = order.doctorCode; } const normalizedEventCode = typeof eventCode === "number" ? eventCode : typeof eventCode === "string" && eventCode !== "" ? Number(eventCode) : undefined; const rr = recordResult && typeof recordResult === "object" ? recordResult : null; const status = rr && rr.status !== undefined && rr.status !== null && rr.status !== "" ? Number(rr.status) : undefined; const $set = { updatedAt: now, source: "AUTO_RECORD_CALLBACK", }; if (orderId) $set.orderId = orderId; if (doctorCode) $set.doctorCode = doctorCode; if (typeof roomId === "string" && roomId) $set.roomId = roomId; if (typeof recordId === "string" && recordId) $set.recordId = recordId; if (typeof bizRequestId === "string" && bizRequestId) $set.bizRequestId = bizRequestId; if (typeof bizName === "string" && bizName) $set.bizName = bizName; if (typeof appId === "string" && appId) $set.appId = appId; if (typeof workspaceId === "string" && workspaceId) $set.workspaceId = workspaceId; if (normalizedEventCode !== undefined && Number.isFinite(normalizedEventCode)) $set.eventCode = normalizedEventCode; // eventCode=99 时传 recordResult:status/fileType/filePath/recordStartTime/mediaType if (normalizedEventCode === 99 && rr) { if (status !== undefined && Number.isFinite(status)) $set.status = status; if (rr.fileType !== undefined && rr.fileType !== null && rr.fileType !== "") $set.fileType = Number(rr.fileType); if (typeof rr.filePath === "string") $set.filePath = rr.filePath; if (rr.recordStartTime !== undefined && rr.recordStartTime !== null && rr.recordStartTime !== "") $set.startAt = Number(rr.recordStartTime); if (rr.mediaType !== undefined && rr.mediaType !== null && rr.mediaType !== "") $set.mediaType = Number(rr.mediaType); } // eventCode=11:录制结束(结束时间以回调到达时间为准) if (normalizedEventCode === 11) { $set.endAt = now; } // 以 recordId 为主键幂等(回调不会携带 recordThirdId) const filter = recordId ? { recordId } : { bizRequestId: bizRequestId || `AUTO_${now}` }; await db.collection("mrtc-video-record").updateOne( filter, { // 注意:MongoDB 不允许在同一次 update 中同时用 $setOnInsert 与 $push 修改同一路径(会报 conflict) // events 字段由 $push 在首次写入时自动创建为数组 $setOnInsert: { createdAt: now }, $set, $push: { events: { event: "AUTO_RECORD_CALLBACK", at: now, eventCode: normalizedEventCode, status, payload, }, }, }, { upsert: true } ); logger.info("[MRTC][AUTO_RECORD] callback stored", { bizRequestId, roomId, recordId, eventCode: normalizedEventCode, orderId, status, }); } catch (e) { logger.error("[MRTC][AUTO_RECORD] callback store failed", { message: e && e.message ? e.message : String(e), }); } }) ); 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(); consultTrigger({ type: "recoverDelayedTasks", });