hn-hlw-service/index.js
2026-07-27 11:28:33 +08:00

674 lines
19 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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