hn-hlw-service/index.js

674 lines
19 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");
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 时传 recordResultstatus/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",
});