Compare commits
7 Commits
b886d6d836
...
6185f89f95
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6185f89f95 | ||
|
|
552dc546be | ||
|
|
f1c1812516 | ||
|
|
ec816dfdd6 | ||
|
|
0165c2c064 | ||
|
|
c8f4c72205 | ||
|
|
8956fb8c8a |
5
.env.dev
5
.env.dev
@ -18,3 +18,8 @@ CONFIG_ALIPAY_MINI_PRIVATE_KEY=MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBA
|
||||
## 海宁his api
|
||||
CONFIG_HN_HIS_BASE_URL=http://122.225.16.74:8026
|
||||
|
||||
##
|
||||
PUPPETEER_EXECUTABLE_PATH="C:\Program Files\Google\Chrome\Application\chrome.exe"
|
||||
PRESCRIPTION_PDF_MAX_CONCURRENCY=2
|
||||
CONFIG_PRESCRIPTION_PDF_BASE_URL=https://patient.youcan365.com/hn
|
||||
|
||||
|
||||
8
.env.pro
8
.env.pro
@ -19,4 +19,10 @@ CONFIG_ALIPAY_MINI_PRIVATE_KEY=MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBA
|
||||
CONFIG_HN_HIS_BASE_URL=http://122.225.16.74:8026
|
||||
|
||||
## im图片存储服务端的位置
|
||||
CONFIG_IM_IMAGE_ROOT=/opt/apps/private-files/im-chat
|
||||
CONFIG_IM_IMAGE_ROOT=/opt/apps/private-files/im-chat
|
||||
|
||||
## 处方 PDF(Linux Chrome)
|
||||
PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome
|
||||
PRESCRIPTION_PDF_MAX_CONCURRENCY=2
|
||||
CONFIG_PRESCRIPTION_PDF_ROOT=/opt/apps/private-files/prescriptions
|
||||
CONFIG_PRESCRIPTION_PDF_BASE_URL=https://hlwyy.hnkhhospital.com
|
||||
|
||||
20
README.md
20
README.md
@ -45,3 +45,23 @@ GET /im-chat/image/:msgId
|
||||
|
||||
当前接口按需求暂未启用 JWT,但仅允许通过 `MsgId` 查询数据库中归档成功的受控路径。
|
||||
生产使用前建议补充问诊订单权限验证。
|
||||
## 处方 PDF
|
||||
|
||||
处方笺下载接口为 `GET /hlw/prescriptions/:id/pdf`;默认输出 A5,传 `paperSize=A4` 可输出 A4。获取 PDF Base64 内容可调用 `GET /hlw/prescriptions/:id/base64`,响应中的 `data` 字段为 Base64 字符串,也支持 `paperSize=A4`。
|
||||
|
||||
服务运行环境需要预装 Chromium/Chrome,并配置其可执行文件路径:
|
||||
|
||||
```env
|
||||
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||
PRESCRIPTION_PDF_MAX_CONCURRENCY=2
|
||||
# 推荐挂载到持久化卷;未设置时默认 ../private-files/prescriptions
|
||||
CONFIG_PRESCRIPTION_PDF_ROOT=/data/hn-private-files/prescriptions
|
||||
```
|
||||
|
||||
`PRESCRIPTION_PDF_MAX_CONCURRENCY` 默认为 2,用于限制 Chromium 同时渲染的页面数。首次访问会生成并原子写入持久化目录,之后同一处方和纸张规格直接读取已有文件。该链接当前不需要认证。医生和药师签名支持 `data:image/*;base64,...`、项目上传目录的 `/upload-files/*.png|jpg|jpeg|webp` 路径,或 HTTP(S) 图片 URL。按本地文件、网络下载、姓名文本的顺序回退;网络请求最长 5 秒、最大 1 MB。
|
||||
|
||||
PDF 响应使用 `Cache-Control: no-cache`,浏览器每次访问都会向服务端校验文件;已保存的 PDF 仍直接从磁盘读取,不会重复渲染。
|
||||
|
||||
PDF 按处方 ObjectId 的 UTC 创建日期分目录保存,例如 `/opt/apps/private-files/prescriptions/2026/09/15/{处方ID}-A5-v5.pdf`,避免大量文件堆积在同一个目录。
|
||||
|
||||
Linux 服务端需安装中文字体,否则 Chrome 无法渲染处方中的中文。TencentOS Server 4 可执行 `dnf install -y google-noto-sans-cjk-sc-fonts fontconfig && fc-cache -f`,模板会优先使用 `Noto Sans CJK SC`。
|
||||
|
||||
@ -708,8 +708,8 @@ async function resolveDiagnosisList(db, order) {
|
||||
}
|
||||
};
|
||||
|
||||
const medInfo = order.medInfo || {};
|
||||
add(medInfo.dise_codg, medInfo.dise_name);
|
||||
// const medInfo = order.medInfo || {};
|
||||
// add(medInfo.dise_codg, medInfo.dise_name);
|
||||
|
||||
const diseaseNames = Array.isArray(order.diseases)
|
||||
? order.diseases.filter((item) => typeof item === "string" && item.trim())
|
||||
|
||||
@ -18,6 +18,7 @@ enum DiagnosticStatus {
|
||||
EXPIRED = 'EXPIRED', // 订单过期
|
||||
PASS_INVALID = 'PASS_INVALID', // 通过的处方作废
|
||||
UNPASS_INVALID = 'UNPASS_INVALID', // 未通过的处方作废
|
||||
CANCELLED = 'CANCELLED', // 门店主动撤销
|
||||
INVALID = 'INVALID', // 处方作废
|
||||
}
|
||||
|
||||
@ -122,4 +123,5 @@ interface DiagnosticRecord {
|
||||
needReUpload?: boolean // 门店处方 是否需要重新上传(可能存在的场景 调用his接口上传成功了 但是his未上传医保, 需要药师手动去his的上传处理页面上传)
|
||||
printCount?: number; // 门店自费处方打印次数
|
||||
lastPrintTime?: number; // 门店自费处方最近一次打印时间
|
||||
cancelTime?: number; // 门店主动撤销时间
|
||||
}
|
||||
|
||||
@ -6,6 +6,18 @@ const Sm4Util = require("../../utils/sm4-util");
|
||||
*/
|
||||
const ENCRYPTED_FIELDS = ["name", "mobile", "idCard"];
|
||||
|
||||
function getPrescriptionPdfUrl(data) {
|
||||
const id = data && (data._id || data.id);
|
||||
if (id === undefined || id === null || String(id).trim() === "") {
|
||||
return "";
|
||||
}
|
||||
const baseUrl = String(process.env.CONFIG_PRESCRIPTION_PDF_BASE_URL || "")
|
||||
.trim()
|
||||
.replace(/\/+$/, "");
|
||||
if (!baseUrl) return "";
|
||||
return `${baseUrl}/hlw/prescriptions/${encodeURIComponent(String(id))}/pdf`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密诊断记录敏感字段
|
||||
* @param {Object} data - 诊断记录数据
|
||||
@ -78,8 +90,9 @@ function decryptDiagnosticRecordList(list) {
|
||||
return list.map((item) => decryptDiagnosticRecordFields(item));
|
||||
}
|
||||
|
||||
exports.formatUploadRxData = function (data) {
|
||||
return {
|
||||
exports.formatUploadRxData = function (data, opt) {
|
||||
const result = {
|
||||
// pdfUrl: getPrescriptionPdfUrl(data),
|
||||
pay_type: data.feeType === 'YB' ? '1' : '0',
|
||||
patientid: data.patientId,
|
||||
orderno: data.medOrgOrderNo,
|
||||
@ -128,6 +141,10 @@ exports.formatUploadRxData = function (data) {
|
||||
icd_name: i.name,
|
||||
})),
|
||||
};
|
||||
if (opt && opt.pdf) {
|
||||
result.pdf = opt.pdf
|
||||
}
|
||||
return result
|
||||
};
|
||||
|
||||
|
||||
@ -266,3 +283,4 @@ exports.decryptDiagnosticRecordList = decryptDiagnosticRecordList;
|
||||
exports.buildHybridSearchQuery = buildHybridSearchQuery;
|
||||
exports.buildMultiFieldHybridSearch = buildMultiFieldHybridSearch;
|
||||
exports.ENCRYPTED_FIELDS = ENCRYPTED_FIELDS;
|
||||
exports.getPrescriptionPdfUrl = getPrescriptionPdfUrl;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
let db = "";
|
||||
const { ObjectId } = require("mongodb");
|
||||
const fs = require("fs/promises");
|
||||
const { getDatabase } = require("../../mongodb");
|
||||
const dayjs = require("dayjs");
|
||||
const { decryptOrderList } = require("../consult-order/format");
|
||||
@ -20,6 +21,7 @@ const beijingCa = require("../beijing-ca");
|
||||
const BigNumber = require("bignumber.js");
|
||||
// const timeApi = require("../hlw-time-duration");
|
||||
const types = require("./types");
|
||||
const { getOrCreatePrescriptionPdf } = require("../prescription-pdf");
|
||||
|
||||
/**
|
||||
* 性能优化建议:
|
||||
@ -75,6 +77,7 @@ const status = {
|
||||
passInvalid: "PASS_INVALID", //通过的处方作废 (目前就订单退费会导致处方作废)
|
||||
unpassInvalid: "UNPASS_INVALID", //未通过的处方作废 (目前就订单退费会导致处方作废)
|
||||
invalid: "INVALID", //处方作废 (目前就订单退费会导致处方作废)
|
||||
cancelled: "CANCELLED", //门店主动撤销
|
||||
};
|
||||
|
||||
module.exports = async (item, mongodb, user) => {
|
||||
@ -90,6 +93,8 @@ module.exports = async (item, mongodb, user) => {
|
||||
return await getStoreDiagnosisRecord(item, db, user);
|
||||
case "markStoreRxPrinted":
|
||||
return await markStoreRxPrinted(item, db, user);
|
||||
case "cancelStoreDiagnosisRecord":
|
||||
return await cancelStoreDiagnosisRecord(item, db, user);
|
||||
case "getAuditDiagnosisList":
|
||||
return await getAuditDiagnosisList(item, db);
|
||||
case "getAuditedDiagnosisList":
|
||||
@ -100,8 +105,6 @@ module.exports = async (item, mongodb, user) => {
|
||||
return await getPatientPassDiagnosisRecord(item, db);
|
||||
case "auditDiagnosis":
|
||||
return await auditDiagnosis(item, db);
|
||||
case "uploadDiagnosis":
|
||||
return await uploadDiagnosis(item, db);
|
||||
case "orderHasDiagnosis":
|
||||
return await orderHasDiagnosis(item, db);
|
||||
case "getLatestSubmitTime":
|
||||
@ -140,11 +143,28 @@ module.exports = async (item, mongodb, user) => {
|
||||
return await getPatientWineBuyCount(item, db);
|
||||
case "getYbReviewResult":
|
||||
return await getYbReviewResult(item, db);
|
||||
case "getpdfbase64":
|
||||
return {
|
||||
success: true,
|
||||
message: "查询成功",
|
||||
data: await getpdfbase64(item, db),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let scheduleTaskIsRunning = false;
|
||||
|
||||
async function getpdfbase64(ctx = {}, db) {
|
||||
const prescriptionId = String(ctx.id || ctx._id || "").trim();
|
||||
if (!ObjectId.isValid(prescriptionId)) {
|
||||
throw new Error("处方 id 格式错误");
|
||||
}
|
||||
|
||||
const paperSize = ctx.paperSize === "A4" ? "A4" : "A5";
|
||||
const { pdfPath } = await getOrCreatePrescriptionPdf(db, prescriptionId, { paperSize });
|
||||
return (await fs.readFile(pdfPath)).toString("base64");
|
||||
}
|
||||
|
||||
async function scheduleAutoPassAfterPrescription({
|
||||
rxId,
|
||||
orderId,
|
||||
@ -554,7 +574,7 @@ async function getStoreDiagnosisRecord(item, db, user) {
|
||||
}
|
||||
const query = {
|
||||
drugStoreNo: { $in: [...new Set(storeIds)] },
|
||||
status: status.pass,
|
||||
status: { $in: [status.pass, status.cancelled] },
|
||||
};
|
||||
const page = item.page > 0 && Number.isInteger(item.page) ? item.page : 1;
|
||||
const pageSize =
|
||||
@ -619,6 +639,56 @@ async function getStoreDiagnosisRecord(item, db, user) {
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelStoreDiagnosisRecord(item, db, user) {
|
||||
const storeIds = await resolveStoreIds(item, db, user);
|
||||
if (
|
||||
typeof item.id !== "string" ||
|
||||
!ObjectId.isValid(item.id.trim()) ||
|
||||
storeIds.length === 0
|
||||
) {
|
||||
return { success: false, message: "参数错误" };
|
||||
}
|
||||
|
||||
try {
|
||||
const record = await db.collection("diagnostic-record").findOne({
|
||||
_id: new ObjectId(item.id.trim()),
|
||||
drugStoreNo: { $in: [...new Set(storeIds)] },
|
||||
status: status.pass,
|
||||
});
|
||||
if (!record) {
|
||||
return { success: false, message: "处方不存在或当前状态不可撤销" };
|
||||
}
|
||||
|
||||
// 医保处方必须先由 HIS 撤销成功,才允许修改本地状态。
|
||||
if (record.feeType === "YB") {
|
||||
const hisResult = await hnHis({
|
||||
type: "cancelInsuranceRx",
|
||||
patientId: record.patientId,
|
||||
orderno: record.medOrgOrderNo,
|
||||
});
|
||||
if (!hisResult || !hisResult.success) {
|
||||
return { success: false, message: (hisResult && hisResult.message) || "医保处方撤销失败" };
|
||||
}
|
||||
}
|
||||
|
||||
const cancelTime = Date.now();
|
||||
const result = await db.collection("diagnostic-record").updateOne(
|
||||
{
|
||||
_id: record._id,
|
||||
drugStoreNo: { $in: [...new Set(storeIds)] },
|
||||
status: status.pass,
|
||||
},
|
||||
{ $set: { status: status.cancelled, cancelTime } }
|
||||
);
|
||||
if (result.modifiedCount !== 1) {
|
||||
return { success: false, message: "处方状态已变更,请刷新后重试" };
|
||||
}
|
||||
return { success: true, message: "处方撤销成功", data: { cancelTime } };
|
||||
} catch (e) {
|
||||
return { success: false, message: e.message || "处方撤销失败" };
|
||||
}
|
||||
}
|
||||
|
||||
async function markStoreRxPrinted(item, db, user) {
|
||||
const storeIds = await resolveStoreIds(item, db, user);
|
||||
if (
|
||||
@ -634,7 +704,7 @@ async function markStoreRxPrinted(item, db, user) {
|
||||
_id: new ObjectId(item.id.trim()),
|
||||
drugStoreNo: { $in: [...new Set(storeIds)] },
|
||||
status: status.pass,
|
||||
feeType: "ZF",
|
||||
// feeType: "ZF",
|
||||
},
|
||||
{
|
||||
$inc: { printCount: 1 },
|
||||
@ -1051,6 +1121,15 @@ async function auditDiagnosis(item, db) {
|
||||
if (passList.length) {
|
||||
let successRxList = [];
|
||||
const auditTime = Date.now();
|
||||
// uploadRx2His 内部会按处方 id 从 MongoDB 重新读取记录并生成 PDF,
|
||||
// 因此不能只在内存 rx 对象中补签名,必须先写入数据库。
|
||||
await db.collection("diagnostic-record").updateMany(
|
||||
{
|
||||
_id: { $in: passList.map(i => i.rx._id) },
|
||||
status: status.init,
|
||||
},
|
||||
{ $set: { pharmacistSignImg: pharmacistCAUserId } }
|
||||
);
|
||||
const uploadResult = await Promise.all(passList.map(i => uploadRx2His({ ...i.rx, auditTime }, i.order)));
|
||||
uploadResult.forEach(i => {
|
||||
if (i.success) {
|
||||
@ -1143,7 +1222,9 @@ async function auditDiagnosis(item, db) {
|
||||
|
||||
async function uploadRx2His(rx, order) {
|
||||
try {
|
||||
const uploadData = formatUploadRxData(rx, order);
|
||||
// 获取处方笺pdf
|
||||
const base64 = await getpdfbase64({ id: rx._id.toString() }, db);
|
||||
const uploadData = await formatUploadRxData(rx, { pdf: base64 });
|
||||
const { success, message } = await hnHis({
|
||||
type: 'hlwuploadprescription',
|
||||
data: uploadData
|
||||
@ -1281,52 +1362,6 @@ function getOriData(data) {
|
||||
return `医生${doctorName}向患者${patientName}开具的诊断是${diagnosis},开具的药物是${drugs}。`;
|
||||
}
|
||||
|
||||
async function uploadDiagnosis(ids) {
|
||||
if (!Array.isArray(ids) || ids.length == 0)
|
||||
return {
|
||||
success: false,
|
||||
message: "参数错误",
|
||||
};
|
||||
try {
|
||||
const res = await db
|
||||
.collection("diagnostic-record")
|
||||
.find({ _id: { $in: ids }, status: status.pass })
|
||||
.limit(ids.length)
|
||||
.toArray();
|
||||
const list = res.map((item) => ({
|
||||
payload: formatUploadRxData(item),
|
||||
_id: item._id,
|
||||
}));
|
||||
const res1 = await Promise.all(
|
||||
list.map((item) =>
|
||||
zytHis({
|
||||
type: "hlwuploadprescription",
|
||||
payload: item.payload,
|
||||
_id: item._id,
|
||||
})
|
||||
)
|
||||
);
|
||||
const successIds = res1.filter((i) => i.success).map((i) => i._id);
|
||||
await db
|
||||
.collection("diagnostic-record")
|
||||
.updateMany(
|
||||
{ _id: { $in: successIds } },
|
||||
{ $set: { uploadStatus: "uploaded" } }
|
||||
);
|
||||
const successCount = successIds.length;
|
||||
return {
|
||||
success: true,
|
||||
message: `成功上传${successCount}条数据`,
|
||||
};
|
||||
} catch (err) {
|
||||
console.log(err, err.message);
|
||||
return {
|
||||
success: false,
|
||||
message: err.message || "上传失败",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* orderStatus completed(已完成) processing(处理中) pending(待处理) cancelled(已取消) finished(已结束)
|
||||
@ -1435,30 +1470,16 @@ async function orderHasDiagnosis(item) {
|
||||
|
||||
async function reUploadDiagnosticRecord({ _id }) {
|
||||
try {
|
||||
return
|
||||
const data = await db
|
||||
.collection("diagnostic-record")
|
||||
.findOne({ _id: new ObjectId(_id) });
|
||||
console.log(data)
|
||||
if (!data) return { success: false, message: "处方不存在" };
|
||||
if (data.prescriptionType !== "storeMedicinePurchase") {
|
||||
return { success: false, message: "暂只支持门店处方重新上传" };
|
||||
}
|
||||
if (data && data.uploadStatus === "uploaded")
|
||||
return { success: false, message: "处方已上传" };
|
||||
const { uploaded } = await zytHis({
|
||||
type: "hlwPrescriptionOrderStatus",
|
||||
orderno: data.orderId,
|
||||
patientId: data.patientId,
|
||||
});
|
||||
if (uploaded) {
|
||||
await db
|
||||
.collection("diagnostic-record")
|
||||
.updateOne(
|
||||
{ _id: new ObjectId(_id) },
|
||||
{ $set: { uploadStatus: "uploaded", hisUploadStatus: "uploaded" } }
|
||||
);
|
||||
return { success: true, message: "处方上传成功" };
|
||||
}
|
||||
const payload = formatUploadRxData(data);
|
||||
const base64 = await getpdfbase64({ id: data._id.toString() }, db);
|
||||
const payload = formatUploadRxData(data, { pdf: base64 });
|
||||
console.log(payload)
|
||||
return { success: false, message: '打住' }
|
||||
const { success, message } = await zytHis({
|
||||
type: "hlwuploadprescription",
|
||||
payload,
|
||||
|
||||
@ -3,6 +3,7 @@ const { ObjectId } = require("mongodb");
|
||||
|
||||
const { getDrugList } = require('./verify');
|
||||
const { validateApplicableDiagnosisCodes } = require('./applicable-diagnosis');
|
||||
const hnHis = require("../hn-his");
|
||||
let db = "";
|
||||
|
||||
function getConfigOptionNames(list) {
|
||||
@ -57,6 +58,8 @@ module.exports = async (item, mongodb) => {
|
||||
return await searchDrugs(item);
|
||||
case "getDrugMatchedDisease":
|
||||
return await getDrugMatchedDisease(item);
|
||||
case "updateHisDrugInfo":
|
||||
return await updateHisDrugInfo(item)
|
||||
}
|
||||
};
|
||||
// 药品库信息
|
||||
@ -258,6 +261,8 @@ async function addHlwDrugInfo(ctx) {
|
||||
}
|
||||
const diagnosisCodes = await validateApplicableDiagnosisCodes(params, db);
|
||||
const { onSale, applicableDiagnosisCodes, ...data } = params;
|
||||
// ERP ID is allocated by the service so clients cannot supply or predict it.
|
||||
data.erpId = await generateDrugErpId(db);
|
||||
let newDrug = {};
|
||||
if (Object.keys(data).length) {
|
||||
const [item] = getDrugList([data], true, peopleInfo, 'hlw', optionLists);
|
||||
@ -281,6 +286,9 @@ async function addHlwDrugInfo(ctx) {
|
||||
}
|
||||
newDrug.createTime = Date.now();
|
||||
const res = await db.collection("drug-info").insertOne(newDrug);
|
||||
if (res && res.insertedId) {
|
||||
updateHisDrugInfo({ id: res.insertedId.toString() })
|
||||
}
|
||||
return { success: true, message: "新增药品成功", id: res.insertedId }
|
||||
} catch (e) {
|
||||
return { success: false, message: e.message || '新增药品失败' }
|
||||
@ -296,6 +304,9 @@ async function editHlwDrugInfo(ctx) {
|
||||
const optionLists = await getHlwDrugOptionLists(ctx.corpId);
|
||||
const diagnosisCodes = await validateApplicableDiagnosisCodes(params, db);
|
||||
const { onSale, applicableDiagnosisCodes, ...data } = params;
|
||||
// ERP ID is immutable after creation. Ignore it even when sent by an older
|
||||
// client or a manually constructed request.
|
||||
delete data.erpId;
|
||||
const currentDrug = await db.collection("drug-info").findOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{ projection: { erpId: 1, product_id: 1, product_id_str: 1 } }
|
||||
@ -303,13 +314,6 @@ async function editHlwDrugInfo(ctx) {
|
||||
if (!currentDrug) {
|
||||
return { success: false, message: "未找到对应药品" }
|
||||
}
|
||||
const legacyErpId = currentDrug.product_id_str || currentDrug.product_id;
|
||||
const erpId = Object.prototype.hasOwnProperty.call(data, "erpId")
|
||||
? data.erpId
|
||||
: (currentDrug.erpId || (legacyErpId || legacyErpId === 0 ? String(legacyErpId) : ""));
|
||||
if (typeof erpId !== "string" || !erpId.trim()) {
|
||||
return { success: false, message: "ERP ID不能为空" }
|
||||
}
|
||||
let updateData = {};
|
||||
if (Object.keys(data).length) {
|
||||
const peopleInfo = {
|
||||
@ -336,6 +340,7 @@ async function editHlwDrugInfo(ctx) {
|
||||
if (res.matchedCount === 0) {
|
||||
return { success: false, message: "未找到对应药品" }
|
||||
}
|
||||
updateHisDrugInfo({ id })
|
||||
return { success: true, message: "更新成功" }
|
||||
}
|
||||
return { success: true, message: "更新成功" }
|
||||
@ -346,6 +351,23 @@ async function editHlwDrugInfo(ctx) {
|
||||
|
||||
}
|
||||
|
||||
async function generateDrugErpId(db) {
|
||||
const key = 'drug-erp-id';
|
||||
const type = 'drug-erp-id';
|
||||
const result = await db.collection('sequence-counter').findOneAndUpdate(
|
||||
{ key, type },
|
||||
{
|
||||
$inc: { sequence: 1 },
|
||||
$setOnInsert: { key, type }
|
||||
},
|
||||
{
|
||||
upsert: true,
|
||||
returnDocument: 'after'
|
||||
}
|
||||
);
|
||||
return String(result.sequence).padStart(6, '0');
|
||||
}
|
||||
|
||||
async function setHlwDrugOnSale(ctx) {
|
||||
const { ids, onSale, updaterId, updater } = ctx;
|
||||
const _ids = Array.isArray(ids) ? ids.filter(i => typeof i === 'string' && i.trim()).map(i => new ObjectId(i.trim())) : [ids];
|
||||
@ -500,3 +522,42 @@ async function getDrugMatchedDisease(ctx) {
|
||||
return { success: false, message: e.message }
|
||||
}
|
||||
}
|
||||
|
||||
async function updateHisDrugInfo(ctx) {
|
||||
try {
|
||||
const id = typeof ctx.id === 'string' ? ctx.id.trim() : '';
|
||||
if (id === '' || !ObjectId.isValid(id)) {
|
||||
return { success: false, message: '参数错误' }
|
||||
}
|
||||
const drug = await db.collection('drug-info').findOne({ _id: new ObjectId(id) });
|
||||
if (!drug) {
|
||||
return { success: false, message: '药品不存在' }
|
||||
}
|
||||
const data = {
|
||||
code: '01',
|
||||
erp_id: drug.erpId,
|
||||
drugname: drug.name,
|
||||
gjbm: drug.insurance_code,
|
||||
specification: drug.specification,
|
||||
pack_unit_name: drug.unit,
|
||||
manu_name: drug.manufacturer,
|
||||
py_code: drug.pinyin_code,
|
||||
default_dosage: drug.dosage,
|
||||
default_unit: drug.dosage_unit,
|
||||
default_drug_win: drug.days,
|
||||
default_total: drug.recommended_quantity,
|
||||
default_usage_name: drug.administration_method,
|
||||
default_supply_name: drug.frequency || drug.freq,
|
||||
drug_dosform: drug.dosage_form,
|
||||
zl: drug.package_amount,
|
||||
kfxs: drug.package_amount,
|
||||
kfdw: drug.unit,
|
||||
jlxs: drug.dosage,
|
||||
jldw: drug.dosage_unit
|
||||
}
|
||||
return await hnHis({ type: 'updateHisDrug', data: [data] })
|
||||
// return { success: true, data }
|
||||
} catch (e) {
|
||||
return { success: false, message: e?.message }
|
||||
}
|
||||
}
|
||||
|
||||
@ -558,6 +558,9 @@ async function getAllDoctorNos(ctx) {
|
||||
try {
|
||||
const jobs = Array.isArray(ctx.jobs) ? ctx.jobs : ['doctor'];
|
||||
const query = ctx.showAll === true ? {} : { job: { $in: jobs } };
|
||||
if (typeof ctx.corpId === 'string' && ctx.corpId.trim()) {
|
||||
query.corpId = ctx.corpId.trim();
|
||||
}
|
||||
const res = await db
|
||||
.collection("hlw-doctor")
|
||||
.find(query, { projection: { doctorNo: 1, doctorName: 1, job: 1 } })
|
||||
|
||||
163
hlw/hlw-schedule/index.js
Normal file
163
hlw/hlw-schedule/index.js
Normal file
@ -0,0 +1,163 @@
|
||||
const dayjs = require('dayjs');
|
||||
|
||||
module.exports = async (item, db) => {
|
||||
switch (item.type) {
|
||||
case 'getHlwSchedule':
|
||||
return getHlwSchedule(item, db);
|
||||
case 'saveHlwSchedule':
|
||||
return saveHlwSchedule(item, db);
|
||||
case 'setTodayScheduledPeopleOnline':
|
||||
return setTodayScheduledPeopleOnline(db);
|
||||
}
|
||||
};
|
||||
|
||||
function getDate(value) {
|
||||
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return '';
|
||||
const parsed = dayjs(value);
|
||||
return parsed.isValid() && parsed.format('YYYY-MM-DD') === value ? value : '';
|
||||
}
|
||||
|
||||
function getMonth(value) {
|
||||
if (typeof value !== 'string' || !/^\d{4}-\d{2}$/.test(value)) return '';
|
||||
const parsed = dayjs(`${value}-01`);
|
||||
return parsed.isValid() && parsed.format('YYYY-MM') === value ? value : '';
|
||||
}
|
||||
|
||||
async function getHlwSchedule({ corpId, month }, db) {
|
||||
const normalizedCorpId = typeof corpId === 'string' ? corpId.trim() : '';
|
||||
const normalizedMonth = getMonth(month);
|
||||
if (!normalizedCorpId || !normalizedMonth) return { success: false, message: '机构或月份参数错误' };
|
||||
|
||||
try {
|
||||
const startDate = `${normalizedMonth}-01`;
|
||||
const endDate = dayjs(startDate).add(1, 'month').format('YYYY-MM-DD');
|
||||
const data = await db.collection('hlw-schedule')
|
||||
.find({ corpId: normalizedCorpId, date: { $gte: startDate, $lt: endDate } }, { projection: { _id: 0 } })
|
||||
.sort({ date: 1 })
|
||||
.toArray();
|
||||
return { success: true, message: '查询成功', data };
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message || '查询排班失败' };
|
||||
}
|
||||
}
|
||||
|
||||
async function saveHlwSchedule({ corpId, date, doctorNos = [], pharmacistNos = [], operator, operatorId }, db) {
|
||||
const normalizedCorpId = typeof corpId === 'string' ? corpId.trim() : '';
|
||||
const normalizedDate = getDate(date);
|
||||
if (!normalizedCorpId || !normalizedDate) return { success: false, message: '机构或日期参数错误' };
|
||||
if (dayjs(normalizedDate).isBefore(dayjs(), 'day')) return { success: false, message: '过去日期的排班不可编辑' };
|
||||
|
||||
const normalizeNos = values => [...new Set((Array.isArray(values) ? values : [])
|
||||
.filter(value => typeof value === 'string' && value.trim())
|
||||
.map(value => value.trim()))];
|
||||
const normalizedDoctorNos = normalizeNos(doctorNos);
|
||||
const normalizedPharmacistNos = normalizeNos(pharmacistNos);
|
||||
|
||||
try {
|
||||
const personnel = await db.collection('hlw-doctor').find(
|
||||
{
|
||||
corpId: normalizedCorpId,
|
||||
doctorNo: { $in: [...normalizedDoctorNos, ...normalizedPharmacistNos] },
|
||||
job: { $in: ['doctor', 'pharmacist'] }
|
||||
},
|
||||
{ projection: { doctorNo: 1, doctorName: 1, job: 1 } }
|
||||
).toArray();
|
||||
const peopleByNo = new Map(personnel.map(person => [person.doctorNo, person]));
|
||||
const toPeople = (nos, job) => {
|
||||
const people = nos.map(no => peopleByNo.get(no));
|
||||
return people.length === nos.length && people.every(person => person && person.job === job)
|
||||
? people.map(({ doctorNo, doctorName }) => ({ doctorNo, doctorName }))
|
||||
: null;
|
||||
};
|
||||
const doctors = toPeople(normalizedDoctorNos, 'doctor');
|
||||
const pharmacists = toPeople(normalizedPharmacistNos, 'pharmacist');
|
||||
if (!doctors || !pharmacists) return { success: false, message: '排班人员不存在或岗位不匹配' };
|
||||
|
||||
await db.collection('hlw-schedule').updateOne(
|
||||
{ corpId: normalizedCorpId, date: normalizedDate },
|
||||
{
|
||||
$set: {
|
||||
doctors,
|
||||
pharmacists,
|
||||
updater: typeof operator === 'string' ? operator : '',
|
||||
updaterId: typeof operatorId === 'string' ? operatorId : '',
|
||||
updateTime: Date.now()
|
||||
},
|
||||
$setOnInsert: { corpId: normalizedCorpId, date: normalizedDate, createTime: Date.now() }
|
||||
},
|
||||
{ upsert: true }
|
||||
);
|
||||
if (normalizedDate === dayjs().format('YYYY-MM-DD')) {
|
||||
const onlineResult = await setTodayScheduledPeopleOnline(db);
|
||||
if (!onlineResult.success) {
|
||||
return { success: false, message: `排班已保存,但设置当天排班人员在线失败:${onlineResult.message}` };
|
||||
}
|
||||
}
|
||||
return { success: true, message: '排班保存成功' };
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message || '保存排班失败' };
|
||||
}
|
||||
}
|
||||
|
||||
async function setTodayScheduledPeopleOnline(db) {
|
||||
const date = dayjs().format('YYYY-MM-DD');
|
||||
try {
|
||||
const schedules = await db.collection('hlw-schedule')
|
||||
.find({ date }, { projection: { corpId: 1, doctors: 1, pharmacists: 1 } })
|
||||
.toArray();
|
||||
const peopleByCorp = new Map();
|
||||
const addPeople = (corpId, people, job) => {
|
||||
if (typeof corpId !== 'string' || !corpId.trim()) return;
|
||||
if (!peopleByCorp.has(corpId)) {
|
||||
peopleByCorp.set(corpId, { doctors: new Set(), pharmacists: new Set() });
|
||||
}
|
||||
const target = job === 'doctor'
|
||||
? peopleByCorp.get(corpId).doctors
|
||||
: peopleByCorp.get(corpId).pharmacists;
|
||||
(Array.isArray(people) ? people : []).forEach((person) => {
|
||||
if (typeof person?.doctorNo === 'string' && person.doctorNo.trim()) {
|
||||
target.add(person.doctorNo.trim());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
schedules.forEach(({ corpId, doctors, pharmacists }) => {
|
||||
addPeople(corpId, doctors, 'doctor');
|
||||
addPeople(corpId, pharmacists, 'pharmacist');
|
||||
});
|
||||
|
||||
const updateTime = Date.now();
|
||||
const operations = [];
|
||||
for (const [corpId, { doctors, pharmacists }] of peopleByCorp) {
|
||||
if (doctors.size > 0) {
|
||||
operations.push({
|
||||
updateMany: {
|
||||
filter: { corpId, doctorNo: { $in: [...doctors] }, job: 'doctor' },
|
||||
update: { $set: { onlineStatus: 'online', offlineTime: null, updateTime } }
|
||||
}
|
||||
});
|
||||
}
|
||||
if (pharmacists.size > 0) {
|
||||
operations.push({
|
||||
updateMany: {
|
||||
filter: { corpId, doctorNo: { $in: [...pharmacists] }, job: 'pharmacist' },
|
||||
update: { $set: { onlineStatus: 'online', offlineTime: null, updateTime } }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (operations.length === 0) {
|
||||
return { success: true, message: '当天无排班人员', data: { date, matchedCount: 0, modifiedCount: 0 } };
|
||||
}
|
||||
|
||||
const result = await db.collection('hlw-doctor').bulkWrite(operations);
|
||||
return {
|
||||
success: true,
|
||||
message: '当天排班人员已上线',
|
||||
data: { date, matchedCount: result.matchedCount, modifiedCount: result.modifiedCount }
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message || '设置当天排班人员在线失败' };
|
||||
}
|
||||
}
|
||||
@ -13,60 +13,22 @@ const AppWin = {
|
||||
|
||||
module.exports = async (item) => {
|
||||
switch (item.type) {
|
||||
case "updateHisDrug":
|
||||
return await updateHisDrug(item);
|
||||
case "getHisCustomer":
|
||||
return await getHisCustomer(item);
|
||||
case "addHisCustomer":
|
||||
return await addHisCustomer(item);
|
||||
case "registration":
|
||||
return await registration(item);
|
||||
case "getPayStatus":
|
||||
return await getPayStatus(item);
|
||||
case "hlwRefund":
|
||||
return await hlwRefund(item);
|
||||
case "hlwuploadprescription":
|
||||
return await hlwuploadprescription(item);
|
||||
case "hlwPrescriptionOrderStatus":
|
||||
return await hlwPrescriptionOrderStatus(item);
|
||||
case "getHisPrescriptionUploadStatus":
|
||||
return await getHisPrescriptionUploadStatus(item);
|
||||
case "getHisStoreRegList":
|
||||
return await getHisStoreRegList(item);
|
||||
case "insurancePreSettle":
|
||||
return await insurancePreSettle(item);
|
||||
case "insuranceSettle":
|
||||
return await insuranceSettle(item);
|
||||
case "insuranceRefund":
|
||||
return await insuranceRefund(item);
|
||||
case "getRecent90daysDrugRecord":
|
||||
return await getRecent90daysDrugRecord(item);
|
||||
case "uploadMedicinePurchaseRx":
|
||||
return await uploadMedicinePurchaseRx(item);
|
||||
case "uploadWinePurchaseRx":
|
||||
return await uploadWinePurchaseRx(item);
|
||||
case "getHisDrugInfo":
|
||||
return await getHisDrugInfo(item);
|
||||
case "getHisMedicinePurchase":
|
||||
return await getHisMedicinePurchase(item);
|
||||
case "cancelMedicinePurchase":
|
||||
return await cancelMedicinePurchase(item);
|
||||
case "refundMedicinePurchase":
|
||||
return await refundMedicinePurchase(item);
|
||||
case "getHisInvoice":
|
||||
return await getHisInvoice(item);
|
||||
case "updateMedicinePurchaseDelivery":
|
||||
return await updateMedicinePurchaseDelivery(item);
|
||||
case 'syncHisChargeStatus':
|
||||
return await syncHisChargeStatus(item);
|
||||
case "hisTest":
|
||||
return await hisTest(item);
|
||||
case "getHisEmployeeInfo":
|
||||
return await getHisEmployeeInfo(item);
|
||||
case "syncHisRefundStatus":
|
||||
return await syncHisRefundStatus(item);
|
||||
case "getWineDrugStockInfo":
|
||||
return await getWineDrugStockInfo(item);
|
||||
case "ybReviewPrescription":
|
||||
return await ybReviewPrescription(item);
|
||||
case "cancelInsuranceRx":
|
||||
return await cancelInsuranceRx(item);
|
||||
}
|
||||
};
|
||||
|
||||
@ -197,490 +159,6 @@ async function hlwuploadprescription(item) {
|
||||
}
|
||||
}
|
||||
|
||||
// 预约挂号
|
||||
async function registration(item) {
|
||||
const { patientId, doctorCode, appwin = 'line', unitCode, registerId, chargecode, reg_fee, shopid, shopname, op_id = '', channel } = item;
|
||||
if (AppWin[appwin] == undefined) {
|
||||
return {
|
||||
success: false,
|
||||
message: "appwin参数错误"
|
||||
}
|
||||
}
|
||||
if (AppWin[appwin] == '线下挂号' && (typeof op_id !== 'string' || op_id.trim() === '')) {
|
||||
return {
|
||||
success: false,
|
||||
message: "op_id参数错误"
|
||||
}
|
||||
}
|
||||
try {
|
||||
const url = `${baseUrl}/his/hlw/registration`;
|
||||
// 根据dayjs 判断是上午 还是下午
|
||||
const ampm = dayjs().hour() < 12 ? "a" : "p";
|
||||
const requestday = dayjs().format("YYYY-MM-DD HH:mm:ss");
|
||||
// registerId 为 H开头 15位随机数
|
||||
const payload = {
|
||||
patientId,
|
||||
doctorCode,
|
||||
unitCode,
|
||||
requestday,
|
||||
ampm,
|
||||
registerId,
|
||||
chargecode,
|
||||
reg_fee,
|
||||
shopid,
|
||||
shopname,
|
||||
chargetype: "普通",
|
||||
appwin
|
||||
};
|
||||
if (AppWin[appwin] == '线下挂号') {
|
||||
payload.op_id = op_id;
|
||||
}
|
||||
if (channel === 1) {
|
||||
payload.channel = 1
|
||||
}
|
||||
const { data, status_code, message } = await request.main(
|
||||
url,
|
||||
payload,
|
||||
"POST"
|
||||
);
|
||||
if (data && data.register && data.register.medorg_order_no) {
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
message: message || "挂号成功",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: message || "挂号失败",
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
message: err.message || "挂号失败"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//订单支付状态查询
|
||||
async function getPayStatus(item) {
|
||||
const { patientId, registerId, medorgOrderNo } = item;
|
||||
try {
|
||||
const url = `${baseUrl}/his/hlw/settlement`;
|
||||
const { status, status_code, message, ...rest } = await request.main(
|
||||
url,
|
||||
{
|
||||
patientid: patientId,
|
||||
registerid: registerId,
|
||||
medorg_order_no: medorgOrderNo,
|
||||
},
|
||||
"POST"
|
||||
);
|
||||
const settlement =
|
||||
rest.data && rest.data.settlement ? rest.data.settlement : null;
|
||||
//settlement.status 0 已收费 1 已退费 5 未收费 x 已作废
|
||||
if (status_code == 200) {
|
||||
return {
|
||||
success: true,
|
||||
status: settlement ? settlement.status : '',
|
||||
settlement,
|
||||
message: "订单查询成功",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: message || "获取失败",
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
message: "获取失败",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//医保患者退费
|
||||
async function hlwRefund(item) {
|
||||
const { patientId, registerId, medorgOrderNo } = item;
|
||||
try {
|
||||
const url = `${baseUrl}/his/hlw/refund`;
|
||||
const { data, status_code, message, ...rest } = await request.main(
|
||||
url,
|
||||
{
|
||||
patientid: patientId,
|
||||
registerid: registerId,
|
||||
medorg_order_no: medorgOrderNo,
|
||||
},
|
||||
"POST"
|
||||
);
|
||||
if (status_code == 200 && !message) {
|
||||
return {
|
||||
success: true,
|
||||
message: "退费成功",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: message || "退费失败",
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
message: "退费失败",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 外配处方状态查询
|
||||
async function hlwPrescriptionOrderStatus(item) {
|
||||
const { patientId, orderno } = item;
|
||||
try {
|
||||
const url = `${baseUrl}/his/hlw/orderstatus`;
|
||||
const { status_code, message, data } = await request.main(
|
||||
url,
|
||||
{
|
||||
patientid: patientId,
|
||||
orderno,
|
||||
},
|
||||
"POST"
|
||||
);
|
||||
if (status_code == 200 && data && data.orderstatus) {
|
||||
return {
|
||||
success: true,
|
||||
uploaded: data.orderstatus.status == '1',
|
||||
status: data.orderstatus.status,
|
||||
message: "查询成功",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: message || "查询失败",
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
message: "查询失败",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function getHisPrescriptionUploadStatus(item) {
|
||||
const { patientId, orderno, _id } = item;
|
||||
try {
|
||||
const url = `${baseUrl}/his/hlw/orderstatus`;
|
||||
const { status_code, message, data } = await request.main(
|
||||
url,
|
||||
{
|
||||
patientid: patientId,
|
||||
orderno,
|
||||
},
|
||||
"POST"
|
||||
);
|
||||
if (status_code == 200 && data && data.orderstatus && data.orderstatus.status == 1) {
|
||||
return { orderno, hisUploadStatus: 'uploaded', _id }
|
||||
}
|
||||
if (status_code == 200 && data && data.orderstatus && data.orderstatus.status == -1) {
|
||||
return { orderno, hisUploadStatus: 'error', _id }
|
||||
}
|
||||
if (status_code == 200 && data && data.orderstatus && data.orderstatus.status == 0) {
|
||||
return { orderno, hisUploadStatus: 'notUploaded', _id }
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message: message || "查询失败",
|
||||
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
message: "查询失败",
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function getHisStoreRegList(item) {
|
||||
const { storeId: shopid } = item;
|
||||
if (typeof shopid !== 'string' || shopid.trim() === '') {
|
||||
return {
|
||||
success: false,
|
||||
message: "店铺id无效",
|
||||
};
|
||||
}
|
||||
try {
|
||||
const url = `${baseUrl}/his/hlw/registerquery`;
|
||||
const { status_code, message, data } = await request.main(
|
||||
url,
|
||||
{
|
||||
shopid,
|
||||
requestday: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
status: "0",
|
||||
starttime: dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
||||
endtime: dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
||||
hospid: "108",
|
||||
},
|
||||
"POST"
|
||||
);
|
||||
if (status_code == 200 && Array.isArray(data)) {
|
||||
return {
|
||||
success: true,
|
||||
list: data,
|
||||
message: "查询成功",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: message || "查询失败",
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
message: "查询失败",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function insurancePreSettle(ctx) {
|
||||
const {
|
||||
medorg_order_no: orderno,
|
||||
patientId: patientid,
|
||||
mdtrtCertNo: mdtrt_cert_no,
|
||||
mdtrtCertType: mdtrt_cert_type,
|
||||
cardSn: card_sn,
|
||||
operator_code,
|
||||
operator_name,
|
||||
shopid,
|
||||
hospid = "108"
|
||||
} = ctx;
|
||||
try {
|
||||
const url = `${baseUrl}/his/hlw/presettle`;
|
||||
const payload = { orderno, patientid, mdtrt_cert_no, mdtrt_cert_type, card_sn, operator_code, operator_name, hospid, shopid };
|
||||
const { data, status_code, message } = await request.main(url, payload, "POST");
|
||||
|
||||
if (status_code == 200 && !message) {
|
||||
return { success: true, data, message: '预结算成功' }
|
||||
}
|
||||
return { success: false, message: message || '预结算失败' }
|
||||
} catch (e) {
|
||||
return { success: false, message: e.message || '预结算失败' }
|
||||
}
|
||||
}
|
||||
|
||||
async function insuranceSettle(ctx) {
|
||||
const {
|
||||
patientid,
|
||||
orderno,
|
||||
mdtrt_id,
|
||||
mdtrt_cert_no,
|
||||
outparm_1101,
|
||||
outparm_2206A,
|
||||
inparm_2207A,
|
||||
outparm_2204,
|
||||
totalamount,
|
||||
selfpay,
|
||||
operator_code,
|
||||
operator_name
|
||||
} = ctx;
|
||||
const payload = {
|
||||
patientid,
|
||||
orderno,
|
||||
settle_method: '7',
|
||||
paymethod: '1', // 1: 现金 w: 支付宝 z: 微信
|
||||
paysn: '',
|
||||
payrequestsn: '',
|
||||
mdtrt_id,
|
||||
mdtrt_cert_no,
|
||||
outparm_1101,
|
||||
outparm_2206A,
|
||||
inparm_2207A,
|
||||
outparm_2204,
|
||||
totalamount,
|
||||
selfpay,
|
||||
operator_code,
|
||||
operator_name
|
||||
}
|
||||
try {
|
||||
const url = `${baseUrl}/his/hlw/ybsettle`;
|
||||
// const url = `${baseUrl}/his/hlw/presettle`;
|
||||
const { data, status_code, message } = await request.main(url, payload, "POST");
|
||||
if (status_code == 200 && !message) {
|
||||
return { success: true, data, message: '结算成功' }
|
||||
}
|
||||
return { success: false, message: message || '结算失败' }
|
||||
} catch (e) {
|
||||
return { success: false, message: e.message || '结算失败' }
|
||||
}
|
||||
}
|
||||
|
||||
async function insuranceRefund(ctx) {
|
||||
const { type, ...payload } = ctx;
|
||||
try {
|
||||
const url = `${baseUrl}/his/hlw/ybrefund`;
|
||||
// const url = `${baseUrl}/his/hlw/presettle`;
|
||||
const { data, status_code, message } = await request.main(url, payload, "POST");
|
||||
if (status_code == 200 && !message) {
|
||||
return { success: true, data, message: '退费成功' }
|
||||
}
|
||||
return { success: false, message: message || '退费失败' }
|
||||
} catch (e) {
|
||||
return { success: false, message: e.message || '退费失败' }
|
||||
}
|
||||
}
|
||||
|
||||
async function getRecent90daysDrugRecord(ctx) {
|
||||
const patientid = typeof ctx.patientId === 'string' ? ctx.patientId.trim() : '';
|
||||
if (patientid === '') {
|
||||
return { success: false, message: '患者ID不能为空' }
|
||||
}
|
||||
try {
|
||||
const url = `${baseUrl}/his/hlw/patientzj3100`;
|
||||
const payload = { patientid, hospid: "108", doctorCode: '10021' };
|
||||
if (typeof ctx.doctorcode === 'string' && ctx.doctorcode.trim() !== '') {
|
||||
payload.doctorcode = ctx.doctorcode.trim();
|
||||
}
|
||||
const { data, status_code, message } = await request.main(url, payload, "POST");
|
||||
|
||||
|
||||
if (status_code == 200 && !message && data && data.outparm_zj3100) {
|
||||
// 解析医保用药数据列表
|
||||
const decodedBuffer = Buffer.from(data.outparm_zj3100, 'base64');
|
||||
const gbkDecodedStr = decode(decodedBuffer, 'gbk');
|
||||
const res = JSON.parse(gbkDecodedStr);
|
||||
const drugList = res && res.data && Array.isArray(res.data.lists) ? res.data.lists.map(i => ({
|
||||
hiListName: i.hiListName,
|
||||
insuranceCode: i.hilistCode,
|
||||
mdtrtId: i.mdtrtId,
|
||||
quantity: i.cnt,
|
||||
spec: i.spec && i.spec !== 'null' ? i.spec : '',
|
||||
unit: i.unit && i.unit !== 'null' ? i.unit : ''
|
||||
})) : [];
|
||||
|
||||
// 解析医保记录
|
||||
const decodedBuffer1 = Buffer.from(data.outparm_zj3101, 'base64');
|
||||
const gbkDecodedStr1 = decode(decodedBuffer1, 'gbk');
|
||||
const res1 = JSON.parse(gbkDecodedStr1);
|
||||
const recordList = res1 && res1.data && Array.isArray(res1.data.lists) ? res1.data.lists.map(i => ({
|
||||
mdtrtId: i.mdtrtId,
|
||||
date: i.admDate,
|
||||
medinsName: i.medinsName,
|
||||
})) : [];
|
||||
|
||||
return { success: true, drugList, message: '查询成功', recordList }
|
||||
}
|
||||
return { success: false, message: message || '查询失败' }
|
||||
} catch (e) {
|
||||
return { success: false, message: e.message || '查询失败' }
|
||||
}
|
||||
}
|
||||
|
||||
async function getHisDrugInfo(item) {
|
||||
try {
|
||||
const ignoreAreaCode = typeof item.ignoreAreaCode === 'boolean' ? item.ignoreAreaCode : false;
|
||||
if (!ignoreAreaCode && (typeof item.areaCode !== 'string' || item.areaCode.trim() === '')) {
|
||||
return { success: false, message: '区域代码不能为空' }
|
||||
}
|
||||
const showAll = typeof item.showAll === 'boolean' ? item.showAll : false;
|
||||
const gjbm = Array.isArray(item.insuranceCodes) ? item.insuranceCodes.filter(i => typeof i === 'string' && i.trim() !== '') : [];
|
||||
console.log('gjbm: ', gjbm)
|
||||
if (gjbm.length === 0 && !showAll) {
|
||||
return { success: false, message: '医保编码不能为空' }
|
||||
}
|
||||
const url = `${baseUrl}/his/hlw/drug`;
|
||||
const payload = {
|
||||
hospid: '108',
|
||||
drug_type: 'xy',
|
||||
area_code: typeof item.areaCode === 'string' ? item.areaCode.trim() : '',
|
||||
gjbm: gjbm.join(',')
|
||||
}
|
||||
const { code, status_code, message, data } = await request.main(
|
||||
url,
|
||||
payload,
|
||||
"POST"
|
||||
);
|
||||
if (status_code == 200 && code == '1' && !message) {
|
||||
return {
|
||||
success: true,
|
||||
message: "[his]查询药品信息成功",
|
||||
data: data && Array.isArray(data.drugs) ? data.drugs : []
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: `[his]查询药品信息传失败: ${message || '未知错误'}`,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
message: `[his]查询药品信息传错误: ${err.message || '未知'}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadMedicinePurchaseRx(item) {
|
||||
try {
|
||||
const { data } = item;
|
||||
const url = `${baseUrl}/his/hlw/mzordersave`;
|
||||
const { code, status_code, message } = await request.main(
|
||||
url,
|
||||
data,
|
||||
"POST"
|
||||
);
|
||||
if (status_code == 200 && code == '1' && !message) {
|
||||
return {
|
||||
success: true,
|
||||
message: "[his]购药订单处上传成功",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: `[his]购药订单处方上传失败: ${message || '未知错误'}`,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
message: `[his]购药订单处方上传错误: ${err.message || '未知'}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadWinePurchaseRx(item) {
|
||||
try {
|
||||
const { data } = item;
|
||||
const url = `${baseUrl}/his/hlw/tcsave`;
|
||||
const { code, status_code, message } = await request.main(
|
||||
url,
|
||||
data,
|
||||
"POST"
|
||||
);
|
||||
console.log('data', code, status_code, message);
|
||||
if (status_code == 200 && code == '1' && !message) {
|
||||
return {
|
||||
success: true,
|
||||
message: "[his]浸酒处上传成功",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: `[his]浸酒处方上传失败: ${message || '未知错误'}`,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
message: `[his]浸酒处方上传错误: ${err.message || '未知'}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function ybReviewPrescription(item) {
|
||||
try {
|
||||
const params = { type: "rationalDrugReview", data: item.data };
|
||||
@ -704,155 +182,6 @@ async function ybReviewPrescription(item) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function getHisMedicinePurchase({ patientId, orderNo }) {
|
||||
try {
|
||||
const url = `${baseUrl}/his/hlw/orderquery`;
|
||||
const { code, status_code, message, data } = await request.main(url, { patientid: patientId, orderno: orderNo }, "POST");
|
||||
const order = data && Array.isArray(data.orders) ? data.orders[0] : null;
|
||||
if (status_code == 200 && code == '1' && !message) {
|
||||
if (order) {
|
||||
return { success: true, data: order, message: '查询成功' }
|
||||
}
|
||||
return { success: false, message: `[his]处方订单查询失败: 未找到订单` }
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message: `[his]处方订单查询失败: ${message || '未知错误'}`,
|
||||
};
|
||||
} catch (err) {
|
||||
return { success: false, message: `[his]处方订单查询错误: ${err.message || '未知错误'}` };
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelMedicinePurchase({ patientId: patientid, orderNo: orderno }) {
|
||||
try {
|
||||
const url = `${baseUrl}/his/hlw/ordercancel`;
|
||||
const { code, status_code, message } = await request.main(url, { patientid, orderno }, "POST");
|
||||
if (status_code == 200 && code == '1' && !message) {
|
||||
return { success: true, message: '[his]取消购药订单处方成功' }
|
||||
}
|
||||
return { success: false, message: `[his]取消购药订单处方失败: ${message || '未知错误'}` }
|
||||
} catch (err) {
|
||||
return { success: false, message: `[his]取消购药订单处方错误: ${err.message || '未知错误'}` };
|
||||
}
|
||||
}
|
||||
|
||||
async function refundMedicinePurchase({ patientId: patientid, orderNo: orderno }) {
|
||||
try {
|
||||
const url = `${baseUrl}/his/hlw/orderrefund`;
|
||||
const { code, status_code, message } = await request.main(url, { patientid, orderno }, "POST");
|
||||
if (status_code == 200 && code == '1' && !message) {
|
||||
return { success: true, message: '[his]退费购药订单处方成功' }
|
||||
}
|
||||
return { success: false, message: `[his]退费购药订单处方失败: ${message || '未知错误'}` }
|
||||
} catch (err) {
|
||||
return { success: false, message: `[his]退费购药订单处方错误: ${err.message || '未知错误'}` };
|
||||
}
|
||||
}
|
||||
|
||||
async function getHisInvoice(item) {
|
||||
try {
|
||||
const { patientId, orderNo } = item;
|
||||
const url = `${baseUrl}/his/hlw/dzfp`;
|
||||
let { code, status_code, message, data } = await request.main(url, { patientid: patientId, orderno: orderNo }, "POST");
|
||||
if (status_code == 200 && code == '1' && !message) {
|
||||
const dzfp = data && Array.isArray(data.dzfp) ? data.dzfp[0] : null;
|
||||
return { success: true, data: dzfp && dzfp.dzfp_url ? dzfp.dzfp_url : '', message: '发票查询成功' }
|
||||
}
|
||||
return { success: false, message: `[his]发票查询失败: ${message || '未知错误'}` }
|
||||
} catch (err) {
|
||||
return { success: false, message: `[his]发票查询错误: ${err.message || '未知错误'}` };
|
||||
}
|
||||
}
|
||||
|
||||
async function updateMedicinePurchaseDelivery(item) {
|
||||
try {
|
||||
const {
|
||||
patientId: patientid,
|
||||
orderNo: orderno,
|
||||
backFlag: back_flag = 0,
|
||||
shippingType: deliv_type,
|
||||
shippingNo: deliv_no,
|
||||
shippingFee: deliv_fee,
|
||||
pickupCode: tracking_no,
|
||||
payTime: pay_dt,
|
||||
tradeNo: pay_orderno,
|
||||
receiveName: shipp_name,
|
||||
receivePhone: shipp_tel,
|
||||
fullAddress: shipp_addr,
|
||||
invoice
|
||||
} = item;
|
||||
const payload = {
|
||||
patientid,
|
||||
orderno,
|
||||
back_flag, // 0 收取运费 | 1 退还运费
|
||||
deliv_type,
|
||||
deliv_no,
|
||||
deliv_fee,
|
||||
pay_dt,
|
||||
pay_orderno,
|
||||
shipp_name,
|
||||
shipp_tel,
|
||||
shipp_addr,
|
||||
tracking_no,
|
||||
invoice
|
||||
}
|
||||
const url = `${baseUrl}/his/hlw/delivery`;
|
||||
let { code, status_code, message, data } = await request.main(url, payload, "POST");
|
||||
if (status_code == 200 && code == '1' && !message) {
|
||||
return { success: true, data, message: '[his]同步配药订单物流信息成功' }
|
||||
}
|
||||
return { success: false, message: `[his]同步配药订单物流信息失败: ${message || '未知错误'}` }
|
||||
} catch (err) {
|
||||
return { success: false, message: `[his]同步配药订单物流信息错误: ${err.message || '未知错误'}` };
|
||||
}
|
||||
}
|
||||
|
||||
async function syncHisChargeStatus(ctx) {
|
||||
try {
|
||||
const payload = {
|
||||
patientid: ctx.patientId,
|
||||
orderno: ctx.orderNo,
|
||||
settle_method: 1,
|
||||
paymethod: 'h',
|
||||
paysn: ctx.tradeNo,
|
||||
payrequestsn: ctx.outTradeNo,
|
||||
totalamount: ctx.totalAmount,
|
||||
selfpay: ctx.totalAmount,
|
||||
operator_code: 'hlw',
|
||||
operator_name: '互联网医院'
|
||||
}
|
||||
const url = `${baseUrl}/his/hlw/ybsettle`;
|
||||
let { code, status_code, message, data } = await request.main(url, payload, "POST");
|
||||
if (status_code == 200 && code == '1' && !message) {
|
||||
return { success: true, data, message: '[his]同步浸酒订单收费成功' }
|
||||
}
|
||||
return { success: false, message: `[his]同步浸酒订单收费失败: ${message || '未知错误'}` }
|
||||
} catch (err) {
|
||||
return { success: false, message: `[his]同步浸酒订单收费错误: ${err.message || '未知错误'}` };
|
||||
}
|
||||
}
|
||||
|
||||
async function syncHisRefundStatus(ctx) {
|
||||
try {
|
||||
const payload = {
|
||||
patientid: ctx.patientId,
|
||||
orderno: ctx.orderNo,
|
||||
ptlsh: ctx.refundSerialNo
|
||||
}
|
||||
const url = `${baseUrl}/his/hlw/orderrefund`;
|
||||
let { code, status_code, message, data } = await request.main(url, payload, "POST");
|
||||
if (status_code == 200 && code == '1' && !message) {
|
||||
return { success: true, data, message: '[his]同步浸酒订单退费成功' }
|
||||
}
|
||||
return { success: false, message: `[his]同步浸酒订单退费失败: ${message || '未知错误'}` }
|
||||
} catch (err) {
|
||||
return { success: false, message: `[his]同步浸酒订单退费错误: ${err.message || '未知错误'}` };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function getHisEmployeeInfo(item) {
|
||||
const { code } = item;
|
||||
try {
|
||||
@ -875,46 +204,6 @@ async function getHisEmployeeInfo(item) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getWineDrugStockInfo(item) {
|
||||
try {
|
||||
if (typeof item.code !== 'string' || item.code.trim() === '') {
|
||||
return { success: false, message: '中药饮片代码不能为空' }
|
||||
}
|
||||
if (!(Number.isInteger(item.num) && item.num > 0)) {
|
||||
return { success: false, message: '中药饮片数量必须是整数且大于0' }
|
||||
}
|
||||
const url = `${baseUrl}/his/hlw/drug`;
|
||||
const payload = {
|
||||
hospid: '108',
|
||||
drug_type: 'zcy',
|
||||
tc_code: item.code.trim(),
|
||||
tc_number: item.num
|
||||
}
|
||||
const { code, status_code, message, data } = await request.main(
|
||||
url,
|
||||
payload,
|
||||
"POST"
|
||||
);
|
||||
if (status_code == 200 && code == '1') {
|
||||
return {
|
||||
success: true,
|
||||
message: "[his]查询药品信息成功",
|
||||
data: data
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message: `中药饮品查询失败: ${message || '未知错误'}`,
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
message: `中药饮品查询失败错误: ${err.message || '未知'}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function hisTest() {
|
||||
// const data = {
|
||||
// patientId: '10428622',
|
||||
@ -926,3 +215,55 @@ async function hisTest() {
|
||||
// const data = { "patientid": "30015424", "registerid": "h202501070833145550001", "orderno": "h202501070833145550001", "doctorcode": "10010", "unitcode": "1005000", "visit_time": "2025-12-04 13:15", "area_code": "330602", "hospid": "108", "icds": [{ "icd_code ": "R51.x00", "icd_name": "头痛", "sort": 1 }], "details": [{ "drug_type": "xy", "drug_groupno": "7X", "drug_group": "", "drug_id": "00110", "drug_serial": "01", "drug_name": "富马酸替诺福韦二吡呋酯片", "drug_spec": "300mg×30片/盒", "drug_pric": "329.41", "drug_cnt": "1", "drug_sumamt": "329.41", "supple_code": "1", "supple_name": "口服", "medc_days": "3", "drug_times": "1", "drug_comm": "", "drug_ma": "", "remark": "", "limit_flag": "0", "usage": "11", "usage_name": "每天一次(qd)", "sin_doscnt": "2.00", "sin_dosunt": "片", "unit": "盒" }] }
|
||||
// return await syncHisChargeStatus(data);
|
||||
}
|
||||
|
||||
|
||||
async function updateHisDrug({ data: drugArr }) {
|
||||
try {
|
||||
const params = { type: "updateOnlineDrug", data: drugArr };
|
||||
const { data, status_code, message } = await request.main(url, params, "POST");
|
||||
if (status_code == 200 && !message) {
|
||||
return {
|
||||
success: true,
|
||||
message: `同步his药品成功`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: message || "同步his药品失败",
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
message: err.message || "同步his药品错误",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelInsuranceRx(item) {
|
||||
const patientId = typeof item.patientId === "string" ? item.patientId.trim() : "";
|
||||
const orderno = typeof item.orderno === "string" ? item.orderno.trim() : "";
|
||||
if (!patientId || !orderno) {
|
||||
return { success: false, message: "医保处方缺少患者编号或处方编号" };
|
||||
}
|
||||
try {
|
||||
const params = { type: "hlwPrescriptionOrderCancel", patientId, orderno };
|
||||
const { code, status_code, message } = await request.main(url, params, "POST");
|
||||
if (status_code == 200 && code == "1") {
|
||||
return {
|
||||
success: true,
|
||||
message: `处方撤销成功`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: message || "处方撤销失败",
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
message: err.message || "处方撤销错误",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +29,7 @@ const chineseMedicineOrder = require("./chinese-medicine-order");
|
||||
const hlwPatient = require("./hlw-patient");
|
||||
const scannedCardArchive = require("./scanned-card-archive");
|
||||
const selfAuth = require("./self-auth");
|
||||
const hlwSchedule = require("./hlw-schedule");
|
||||
|
||||
module.exports = async (item, db, context) => {
|
||||
switch (item.type) {
|
||||
@ -57,6 +58,7 @@ module.exports = async (item, db, context) => {
|
||||
case "getStoreSimilarDrugInfo":
|
||||
case "searchDrugs":
|
||||
case "getDrugMatchedDisease":
|
||||
case "updateHisDrugInfo":
|
||||
return await drugInfo(item, db);
|
||||
case "addPatientDescription":
|
||||
case "deletePatientDescription":
|
||||
@ -87,6 +89,10 @@ module.exports = async (item, db, context) => {
|
||||
case "autoSetPeopleOffline":
|
||||
case "getDoctorByNo":
|
||||
return await hlwDoctor(item, db);
|
||||
case "getHlwSchedule":
|
||||
case "saveHlwSchedule":
|
||||
case "setTodayScheduledPeopleOnline":
|
||||
return await hlwSchedule(item, db);
|
||||
case "getDiagnosisList":
|
||||
case "deleteHlwDiagnosis":
|
||||
case "updateHlwDiagnosis":
|
||||
@ -125,7 +131,6 @@ module.exports = async (item, db, context) => {
|
||||
case "getAuditDiagnosisList":
|
||||
case "getAuditedDiagnosisList":
|
||||
case "auditDiagnosis":
|
||||
case "uploadDiagnosis":
|
||||
case "reUploadDiagnosticRecord":
|
||||
case "getPatientDiagnosisRecord":
|
||||
case "getPatientPassDiagnosisRecord":
|
||||
@ -133,6 +138,7 @@ module.exports = async (item, db, context) => {
|
||||
case "orderHasDiagnosis":
|
||||
case "getStoreDiagnosisRecord":
|
||||
case "markStoreRxPrinted":
|
||||
case "cancelStoreDiagnosisRecord":
|
||||
case "getAllDiagnosisRecord":
|
||||
case "getDoctorRxStats":
|
||||
case "getPharmacistRxStats":
|
||||
|
||||
501
hlw/prescription-pdf/index.js
Normal file
501
hlw/prescription-pdf/index.js
Normal file
@ -0,0 +1,501 @@
|
||||
const fs = require("fs");
|
||||
const fsp = require("fs/promises");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
const { ObjectId } = require("mongodb");
|
||||
const dayjs = require("dayjs");
|
||||
const axios = require("axios");
|
||||
const puppeteer = require("puppeteer-core");
|
||||
const { decryptDiagnosticRecordFields } = require("../diagnostic-record/format");
|
||||
|
||||
let browserPromise;
|
||||
let activeJobs = 0;
|
||||
const waiters = [];
|
||||
const cachedPdfJobs = new Map();
|
||||
const PDF_TEMPLATE_VERSION = "v11";
|
||||
const MAX_SIGNATURE_IMAGE_BYTES = 1024 * 1024;
|
||||
const SIGNATURE_DOWNLOAD_TIMEOUT_MS = 5000;
|
||||
const MAX_PRESCRIPTION_PDF_BYTES = 100 * 1024;
|
||||
const RASTER_PAGE_SIZES = {
|
||||
// A5 (148mm) 减去左右 8mm 页边距后的实际打印宽度为 132mm。
|
||||
A5: { viewport: { width: 500, height: 733, deviceScaleFactor: 1 }, contentWidth: "132mm", contentHeight: "194mm" },
|
||||
A4: { viewport: { width: 733, height: 1062, deviceScaleFactor: 1 }, contentWidth: "194mm", contentHeight: "281mm" },
|
||||
};
|
||||
const PRESCRIPTION_SEAL_IMAGE_URL = process.env.CONFIG_PRESCRIPTION_SEAL_IMAGE_URL
|
||||
|| "https://hlwyy.hnkhhospital.com/upload-files/other/stamp.png";
|
||||
|
||||
const signatureMimeTypes = {
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".webp": "image/webp",
|
||||
};
|
||||
|
||||
function resolveDefaultPdfRoot(
|
||||
moduleDirectory = __dirname,
|
||||
isBuilt = process.env.IS_BUILDED === "YES"
|
||||
) {
|
||||
const serviceRoot = isBuilt
|
||||
? moduleDirectory
|
||||
: path.resolve(moduleDirectory, "../..");
|
||||
return path.resolve(serviceRoot, "../private-files/prescriptions");
|
||||
}
|
||||
|
||||
function getPdfRoot() {
|
||||
return path.resolve(
|
||||
process.env.CONFIG_PRESCRIPTION_PDF_ROOT || resolveDefaultPdfRoot()
|
||||
);
|
||||
}
|
||||
|
||||
async function initializeStorage() {
|
||||
const pdfRoot = getPdfRoot();
|
||||
await fsp.mkdir(pdfRoot, { recursive: true });
|
||||
await fsp.access(pdfRoot, fs.constants.R_OK | fs.constants.W_OK);
|
||||
return pdfRoot;
|
||||
}
|
||||
|
||||
function maxConcurrency() {
|
||||
const value = Number.parseInt(process.env.PRESCRIPTION_PDF_MAX_CONCURRENCY, 10);
|
||||
return Number.isInteger(value) && value > 0 ? value : 2;
|
||||
}
|
||||
|
||||
async function acquireSlot() {
|
||||
if (activeJobs < maxConcurrency()) {
|
||||
activeJobs += 1;
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => waiters.push(resolve));
|
||||
activeJobs += 1;
|
||||
}
|
||||
|
||||
function releaseSlot() {
|
||||
activeJobs -= 1;
|
||||
const next = waiters.shift();
|
||||
if (next) next();
|
||||
}
|
||||
|
||||
function getExecutablePath() {
|
||||
const platformCandidates = process.platform === "linux"
|
||||
? [
|
||||
"/usr/bin/google-chrome",
|
||||
"/usr/bin/google-chrome-stable",
|
||||
"/usr/bin/chromium",
|
||||
"/usr/bin/chromium-browser",
|
||||
]
|
||||
: [
|
||||
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
||||
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
||||
];
|
||||
const candidates = [process.env.PUPPETEER_EXECUTABLE_PATH, ...platformCandidates]
|
||||
.filter(Boolean);
|
||||
const executablePath = candidates.find((candidate) => fs.existsSync(candidate));
|
||||
if (!executablePath) {
|
||||
throw new Error(
|
||||
`未找到 Chrome/Chromium,请配置 PUPPETEER_EXECUTABLE_PATH。已检查:${candidates.join(", ")}`
|
||||
);
|
||||
}
|
||||
return executablePath;
|
||||
}
|
||||
|
||||
async function getBrowser() {
|
||||
if (!browserPromise) {
|
||||
browserPromise = puppeteer.launch({
|
||||
executablePath: getExecutablePath(),
|
||||
headless: true,
|
||||
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-gpu"],
|
||||
});
|
||||
const browser = await browserPromise;
|
||||
browser.on("disconnected", () => {
|
||||
browserPromise = undefined;
|
||||
});
|
||||
}
|
||||
return browserPromise;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/\"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function safeDataImage(value) {
|
||||
if (typeof value !== "string" || value.length > 1024 * 1024) return "";
|
||||
return /^data:image\/(png|jpeg|webp);base64,[a-z0-9+/=\s]+$/i.test(value)
|
||||
? value
|
||||
: "";
|
||||
}
|
||||
|
||||
function getUploadRoot() {
|
||||
// 源码运行时:<repo>/hn-hlw-service/hlw/prescription-pdf -> <repo>/upload-files。
|
||||
// esbuild 后 __dirname 为 /opt/apps(bundle_pro.js 所在目录),上传目录则为 /opt/apps/upload-files。
|
||||
return process.env.IS_BUILDED === "YES"
|
||||
? path.resolve(__dirname, "../upload-files")
|
||||
: path.resolve(__dirname, "../../../upload-files");
|
||||
}
|
||||
|
||||
function getImageMimeType(buffer, contentType = "") {
|
||||
const normalizedContentType = String(contentType).split(";", 1)[0].trim().toLowerCase();
|
||||
if (["image/png", "image/jpeg", "image/webp"].includes(normalizedContentType)) {
|
||||
return normalizedContentType;
|
||||
}
|
||||
if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex"))) return "image/png";
|
||||
if (buffer.length >= 3 && buffer.subarray(0, 3).equals(Buffer.from([0xff, 0xd8, 0xff]))) return "image/jpeg";
|
||||
if (buffer.length >= 12 && buffer.subarray(0, 4).toString() === "RIFF" && buffer.subarray(8, 12).toString() === "WEBP") return "image/webp";
|
||||
return "";
|
||||
}
|
||||
|
||||
function getLocalSignaturePath(value) {
|
||||
if (typeof value !== "string") return "";
|
||||
let pathname = value;
|
||||
if (/^https?:\/\//i.test(value)) {
|
||||
try {
|
||||
pathname = new URL(value).pathname;
|
||||
} catch (_error) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
if (!pathname.startsWith("/upload-files/")) return "";
|
||||
|
||||
const uploadRoot = getUploadRoot();
|
||||
let relativePath;
|
||||
try {
|
||||
relativePath = decodeURIComponent(pathname.slice("/upload-files/".length));
|
||||
} catch (_error) {
|
||||
return "";
|
||||
}
|
||||
const filePath = path.resolve(uploadRoot, relativePath);
|
||||
return filePath.startsWith(`${uploadRoot}${path.sep}`) ? filePath : "";
|
||||
}
|
||||
|
||||
async function loadLocalSignatureDataImage(value) {
|
||||
const filePath = getLocalSignaturePath(value);
|
||||
if (!filePath) return "";
|
||||
const mimeType = signatureMimeTypes[path.extname(filePath).toLowerCase()];
|
||||
if (!mimeType) return "";
|
||||
try {
|
||||
const file = await fsp.readFile(filePath);
|
||||
if (file.length === 0 || file.length > MAX_SIGNATURE_IMAGE_BYTES) return "";
|
||||
return `data:${mimeType};base64,${file.toString("base64")}`;
|
||||
} catch (_error) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadSignatureDataImage(value) {
|
||||
if (typeof value !== "string" || !/^https?:\/\//i.test(value)) return "";
|
||||
try {
|
||||
const url = new URL(value);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") return "";
|
||||
const response = await axios.get(url.toString(), {
|
||||
responseType: "arraybuffer",
|
||||
timeout: SIGNATURE_DOWNLOAD_TIMEOUT_MS,
|
||||
maxContentLength: MAX_SIGNATURE_IMAGE_BYTES,
|
||||
maxBodyLength: MAX_SIGNATURE_IMAGE_BYTES,
|
||||
maxRedirects: 2,
|
||||
validateStatus: (status) => status >= 200 && status < 300,
|
||||
});
|
||||
const image = Buffer.from(response.data);
|
||||
if (image.length === 0 || image.length > MAX_SIGNATURE_IMAGE_BYTES) return "";
|
||||
const mimeType = getImageMimeType(image, response.headers["content-type"]);
|
||||
return mimeType ? `data:${mimeType};base64,${image.toString("base64")}` : "";
|
||||
} catch (_error) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSignatureDataImage(value) {
|
||||
const embeddedImage = safeDataImage(value);
|
||||
if (embeddedImage) return embeddedImage;
|
||||
const localImage = await loadLocalSignatureDataImage(value);
|
||||
if (localImage) return localImage;
|
||||
return downloadSignatureDataImage(value);
|
||||
}
|
||||
|
||||
function getAge(record) {
|
||||
if (Number.isFinite(Number(record.age)) && Number(record.age) >= 0) {
|
||||
return `${Number(record.age)}岁`;
|
||||
}
|
||||
const idCard = String(record.idCard || "");
|
||||
if (!/^\d{17}[\dXx]$/.test(idCard)) return "";
|
||||
const birth = dayjs(`${idCard.slice(6, 10)}-${idCard.slice(10, 12)}-${idCard.slice(12, 14)}`);
|
||||
const reference = dayjs(record.prescriptionEndTime || record.createTime);
|
||||
return birth.isValid() && reference.isValid() ? `${reference.diff(birth, "year")}岁` : "";
|
||||
}
|
||||
|
||||
function getSex(record) {
|
||||
if (record.sex === "男" || record.sex === "女") return record.sex;
|
||||
const idCard = String(record.idCard || "");
|
||||
return /^\d{17}[\dXx]$/.test(idCard) ? (Number(idCard[16]) % 2 ? "男" : "女") : "";
|
||||
}
|
||||
|
||||
function medicineUsage(drug) {
|
||||
return [
|
||||
drug.usageName,
|
||||
drug.frequencyName,
|
||||
drug.dosage ? `每次${drug.dosage}${drug.dosage_unit || ""}` : "",
|
||||
].filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
function signature(value, fallback) {
|
||||
const image = safeDataImage(value);
|
||||
return image
|
||||
? `<img class="signature-image" src="${image}" alt="签名" />`
|
||||
: `<span>${escapeHtml(fallback)}</span>`;
|
||||
}
|
||||
|
||||
async function addDrugStoreName(db, record) {
|
||||
if (typeof record.drugStoreName === "string" || typeof record.orderId !== "string" || !record.orderId.trim()) {
|
||||
return record;
|
||||
}
|
||||
try {
|
||||
const order = await db.collection("consult-order").findOne(
|
||||
{ orderId: record.orderId },
|
||||
{ projection: { drugStoreName: 1 } }
|
||||
);
|
||||
return { ...record, drugStoreName: order?.drugStoreName || "" };
|
||||
} catch (_error) {
|
||||
// 门店名称仅用于处方提示语;查询异常不能阻止 PDF 的生成。
|
||||
return record;
|
||||
}
|
||||
}
|
||||
|
||||
function buildPrescriptionHtml(record, paperSize = "A5", signatureImages = {}) {
|
||||
const size = paperSize === "A4" ? "A4" : "A5";
|
||||
const diagnosis = Array.isArray(record.diagnosisList) ? record.diagnosisList : [];
|
||||
const drugs = Array.isArray(record.drugs) ? record.drugs : [];
|
||||
const prescriptionTime = dayjs(record.prescriptionEndTime || record.createTime);
|
||||
const diagnosisText = diagnosis.map((item) => item?.name).filter(Boolean).join("+");
|
||||
const drugHtml = drugs.length
|
||||
? drugs.map((drug, index) => `
|
||||
<section class="medicine-item">
|
||||
<div class="medicine-name">${index + 1}. ${escapeHtml(drug.drugName)} x ${escapeHtml(drug.quantity)} ${escapeHtml(drug.unit)}</div>
|
||||
<div class="medicine-detail">规格:${escapeHtml(drug.specification)}</div>
|
||||
<div class="medicine-detail">用法:${escapeHtml(medicineUsage(drug))}</div>
|
||||
</section>`).join("")
|
||||
: "<section class=\"medicine-item\">暂无药品</section>";
|
||||
|
||||
return `<!doctype html>
|
||||
<html lang="zh-CN"><head><meta charset="utf-8" />
|
||||
<style>
|
||||
@page { size: ${size} portrait; margin: 8mm; }
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; color: #111; background: #fff; font-family: "Noto Sans CJK SC", "Noto Sans SC", SimSun, "Songti SC", serif; font-size: 14px; }
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.prescription { width: calc(100% - 1px); min-height: ${size === "A5" ? "194mm" : "281mm"}; display: flex; flex-direction: column; line-height: 1.28; overflow: hidden; }
|
||||
.header { position: relative; border-bottom: 1px solid #111; padding: 3px 0 4px; }
|
||||
.title { font-weight: 700; font-size: 17px; text-align: center; line-height: 1.4; }
|
||||
.rx-type { position: absolute; top: 0; right: 1px; width: 40px; border: 1px solid #333; text-align: center; color: #666; font-size: 12px; line-height: 1.3; }
|
||||
.rx-number { margin-top: 5px; }
|
||||
.patient-info { padding-top: 5px; }
|
||||
.info-row { display: flex; padding: 2px; }
|
||||
.info-cell { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
||||
.info-half { width: 50%; overflow-wrap: anywhere; }
|
||||
.diagnosis { margin-top: 5px; }
|
||||
.diagnosis-head { display: flex; justify-content: space-between; gap: 8px; }
|
||||
.diagnosis-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
||||
.date { flex: none; white-space: nowrap; }
|
||||
.medicine { margin-top: 5px; }
|
||||
.rp-line { position: relative; display: flex; align-items: center; min-height: 30px; margin-bottom: 6px; }
|
||||
.rp-line::after { content: ""; flex: 1; margin-left: 8px; border-top: 1px dashed #6b7280; }
|
||||
.rp { font-size: 20px; font-weight: 700; }
|
||||
.seal-image { position: absolute; z-index: 1; top: -4px; right: 0; width: 120px; height: 120px; object-fit: contain; }
|
||||
.medicine-item { break-inside: avoid; margin: 0 0 6px; }
|
||||
.medicine-name { font-weight: 700; }
|
||||
.medicine-detail { color: #666; margin-top: 1px; }
|
||||
.footer { margin-top: auto; border-top: 1px dashed #999; padding-top: 5px; break-inside: avoid; }
|
||||
.signature-row, .action-row { display: flex; padding-top: 4px; }
|
||||
.signature-cell, .action-cell { width: 33.333%; min-height: 24px; }
|
||||
.signature-image { display: inline-block; width: 76px; height: 30px; object-fit: contain; vertical-align: middle; }
|
||||
.notice { margin-top: 0; font-size: 12px; line-height: 1.25; white-space: nowrap; }
|
||||
.notice-first { margin-top: 7px; }
|
||||
@media print { .medicine-item, .footer { page-break-inside: avoid; } }
|
||||
</style></head><body>
|
||||
<main class="prescription">
|
||||
<header class="header">
|
||||
<div class="rx-type">普通<br>处方</div>
|
||||
<div class="title">海宁康华医院互联网医院<br>处方笺</div>
|
||||
<div class="rx-number">NO:${escapeHtml(record.medOrgOrderNo || record._id)}</div>
|
||||
</header>
|
||||
<section class="patient-info">
|
||||
<div class="info-row"><div class="info-cell">姓名:${escapeHtml(record.name)}</div><div class="info-cell">性别:${escapeHtml(getSex(record))}</div><div class="info-cell">年龄:${escapeHtml(getAge(record))}</div><div class="info-cell">费别:${record.feeType === "YB" ? "医保" : "自费"}</div></div>
|
||||
<div class="info-row"><div class="info-half">门诊病历号:${escapeHtml(record.blhno)}</div><div class="info-half">科别:${escapeHtml(record.deptName)}</div></div>
|
||||
<div class="info-row"><div class="info-cell">地址:${escapeHtml(record.address)}</div></div>
|
||||
<div class="info-row"><div class="info-cell">电话号:${escapeHtml(record.mobile)}</div></div>
|
||||
</section>
|
||||
<section class="diagnosis"><div class="diagnosis-head"><div class="diagnosis-text">诊断:${escapeHtml(diagnosisText)}</div><div class="date">开方日期:${prescriptionTime.isValid() ? prescriptionTime.format("YYYY-MM-DD HH:mm") : ""}</div></div></section>
|
||||
<section class="medicine"><div class="rp-line"><div class="rp">Rp</div>${signatureImages.seal ? `<img class="seal-image" src="${signatureImages.seal}" alt="处方印章" />` : ""}</div>${drugHtml}</section>
|
||||
<footer class="footer"><div class="signature-row"><div class="signature-cell">医生:${signature(signatureImages.doctor || record.doctorSignImg, record.doctorName)}</div><div class="signature-cell">审方药师:${signature(signatureImages.pharmacist || record.pharmacistSignImg, record.pharmacist)}</div><div class="signature-cell"></div></div><div class="action-row"><div class="action-cell">审核:</div><div class="action-cell">调配:</div><div class="action-cell">核对/发药:</div></div><div class="notice notice-first">1.根据《处方管理办法》规定,本处方开具三天有效;</div><div class="notice">2.本处方仅供患者${escapeHtml(record.drugStoreName)}使用,本处方金额以实际药店成交金额为准;</div><div class="notice">3.请仔细阅读药品使用说明,服药谨遵医嘱,如有不适请立即停药并前往医院就诊。</div></footer>
|
||||
</main>
|
||||
</body></html>`;
|
||||
}
|
||||
|
||||
function buildRasterPdfHtml(image, paperSize) {
|
||||
const size = RASTER_PAGE_SIZES[paperSize];
|
||||
return `<!doctype html><html><head><meta charset="utf-8" /><style>
|
||||
@page { size: ${paperSize} portrait; margin: 8mm; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
img { display: block; width: ${size.contentWidth}; height: ${size.contentHeight}; }
|
||||
</style></head><body><img src="${image}" alt="处方笺" /></body></html>`;
|
||||
}
|
||||
|
||||
async function generateCompactPrescriptionPdf(html, paperSize) {
|
||||
// Chrome 会为中文文本嵌入字体子集;即使没有图片,单页也约 280KB。
|
||||
// 将实际打印区域以 96dpi 截图并转为 JPEG,可保留处方可读性且避免字体嵌入。
|
||||
const browser = await getBrowser();
|
||||
const size = RASTER_PAGE_SIZES[paperSize];
|
||||
const rasterPage = await browser.newPage();
|
||||
try {
|
||||
await rasterPage.setViewport(size.viewport);
|
||||
await rasterPage.setContent(html, { waitUntil: "load" });
|
||||
await rasterPage.evaluate(() => document.fonts && document.fonts.ready);
|
||||
const screenshot = await rasterPage.screenshot({
|
||||
type: "jpeg",
|
||||
quality: 72,
|
||||
clip: { x: 0, y: 0, width: size.viewport.width, height: size.viewport.height },
|
||||
});
|
||||
const compactPage = await browser.newPage();
|
||||
try {
|
||||
await compactPage.setContent(
|
||||
buildRasterPdfHtml(`data:image/jpeg;base64,${Buffer.from(screenshot).toString("base64")}`, paperSize),
|
||||
{ waitUntil: "load" }
|
||||
);
|
||||
const pdf = Buffer.from(await compactPage.pdf({
|
||||
format: paperSize,
|
||||
preferCSSPageSize: true,
|
||||
printBackground: true,
|
||||
margin: { top: 0, right: 0, bottom: 0, left: 0 },
|
||||
}));
|
||||
if (pdf.length > MAX_PRESCRIPTION_PDF_BYTES) {
|
||||
throw new Error(`处方 PDF 压缩后仍超过 ${MAX_PRESCRIPTION_PDF_BYTES} 字节`);
|
||||
}
|
||||
return pdf;
|
||||
} finally {
|
||||
await compactPage.close().catch(() => {});
|
||||
}
|
||||
} finally {
|
||||
await rasterPage.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
async function generatePrescriptionPdf(record, options = {}) {
|
||||
await acquireSlot();
|
||||
try {
|
||||
const [doctor, pharmacist, seal] = await Promise.all([
|
||||
loadSignatureDataImage(record.doctorSignImg),
|
||||
loadSignatureDataImage(record.pharmacistSignImg),
|
||||
loadSignatureDataImage(PRESCRIPTION_SEAL_IMAGE_URL),
|
||||
]);
|
||||
const html = buildPrescriptionHtml(record, options.paperSize, { doctor, pharmacist, seal });
|
||||
const paperSize = options.paperSize === "A4" ? "A4" : "A5";
|
||||
return generateCompactPrescriptionPdf(html, paperSize);
|
||||
} finally {
|
||||
releaseSlot();
|
||||
}
|
||||
}
|
||||
|
||||
async function createPrescriptionPdf(db, id, options) {
|
||||
if (typeof id !== "string" || !ObjectId.isValid(id)) {
|
||||
const error = new Error("处方 id 格式错误");
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
const record = await db.collection("diagnostic-record").findOne({ _id: new ObjectId(id) });
|
||||
if (!record) {
|
||||
const error = new Error("处方不存在");
|
||||
error.statusCode = 404;
|
||||
throw error;
|
||||
}
|
||||
return generatePrescriptionPdf(
|
||||
await addDrugStoreName(db, decryptDiagnosticRecordFields(record)),
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
function getPdfPath(id, paperSize) {
|
||||
const size = paperSize === "A4" ? "A4" : "A5";
|
||||
// MongoDB ObjectId 的前四字节是创建时间戳;用 UTC 日期分目录,避免因服务器时区不同写入不同路径。
|
||||
const createdAt = new ObjectId(id).getTimestamp();
|
||||
const year = String(createdAt.getUTCFullYear());
|
||||
const month = String(createdAt.getUTCMonth() + 1).padStart(2, "0");
|
||||
const day = String(createdAt.getUTCDate()).padStart(2, "0");
|
||||
return path.join(
|
||||
getPdfRoot(),
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
`${id}-${size}-${PDF_TEMPLATE_VERSION}.pdf`
|
||||
);
|
||||
}
|
||||
|
||||
async function getOrCreatePrescriptionPdf(db, id, options = {}) {
|
||||
if (typeof id !== "string" || !ObjectId.isValid(id)) {
|
||||
const error = new Error("处方 id 格式错误");
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
const paperSize = options.paperSize === "A4" ? "A4" : "A5";
|
||||
const pdfPath = getPdfPath(id, paperSize);
|
||||
try {
|
||||
await fsp.access(pdfPath, fs.constants.R_OK);
|
||||
return { pdfPath, cached: true };
|
||||
} catch (_error) {
|
||||
// 文件尚未生成,继续生成;不要将 access 错误泄漏给客户端。
|
||||
}
|
||||
|
||||
const key = `${id}:${paperSize}`;
|
||||
if (!cachedPdfJobs.has(key)) {
|
||||
cachedPdfJobs.set(key, (async () => {
|
||||
await initializeStorage();
|
||||
await fsp.mkdir(path.dirname(pdfPath), { recursive: true });
|
||||
try {
|
||||
await fsp.access(pdfPath, fs.constants.R_OK);
|
||||
return;
|
||||
} catch (_error) {
|
||||
// 仍不存在,当前请求负责生成。
|
||||
}
|
||||
const record = await db.collection("diagnostic-record").findOne({ _id: new ObjectId(id) });
|
||||
if (!record) {
|
||||
const error = new Error("处方不存在");
|
||||
error.statusCode = 404;
|
||||
throw error;
|
||||
}
|
||||
const pdf = await generatePrescriptionPdf(
|
||||
await addDrugStoreName(db, decryptDiagnosticRecordFields(record)),
|
||||
{ paperSize }
|
||||
);
|
||||
const temporaryPath = `${pdfPath}.${crypto.randomUUID()}.tmp`;
|
||||
try {
|
||||
await fsp.writeFile(temporaryPath, pdf, { flag: "wx" });
|
||||
await fsp.rename(temporaryPath, pdfPath);
|
||||
} catch (error) {
|
||||
await fsp.unlink(temporaryPath).catch(() => {});
|
||||
throw error;
|
||||
}
|
||||
})().finally(() => cachedPdfJobs.delete(key)));
|
||||
}
|
||||
await cachedPdfJobs.get(key);
|
||||
return { pdfPath, cached: false };
|
||||
}
|
||||
|
||||
async function closeBrowser() {
|
||||
if (!browserPromise) return;
|
||||
const browser = await browserPromise;
|
||||
browserPromise = undefined;
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildPrescriptionHtml,
|
||||
createPrescriptionPdf,
|
||||
generatePrescriptionPdf,
|
||||
getOrCreatePrescriptionPdf,
|
||||
initializeStorage,
|
||||
loadSignatureDataImage,
|
||||
loadLocalSignatureDataImage,
|
||||
downloadSignatureDataImage,
|
||||
getPdfPath,
|
||||
closeBrowser,
|
||||
};
|
||||
243
index.js
243
index.js
@ -42,6 +42,7 @@ const customerHisSync = require("./customerHisSync");
|
||||
const hlw = require("./hlw");
|
||||
const automation = require("./hlw/automation");
|
||||
const imImageArchive = require("./hlw/im-image-archive");
|
||||
const { getOrCreatePrescriptionPdf, initializeStorage: initializePrescriptionPdfStorage } = require("./hlw/prescription-pdf");
|
||||
const trigger = require("./trigger");
|
||||
const consultTrigger = require("./hlw/consult-order/trigger");
|
||||
const callBack = require("./callBack");
|
||||
@ -51,6 +52,7 @@ const { jwtAuth, optionalJwtAuth } = require("./middleware/jwt-auth");
|
||||
const { smartAuth } = require("./middleware/smart-auth");
|
||||
const sms = require("./sms");
|
||||
const path = require("path");
|
||||
const fs = require("fs/promises");
|
||||
const { ObjectId } = require("mongodb");
|
||||
const { useUploadFileParse } = require("./upload-file-parse");
|
||||
const app = express();
|
||||
@ -207,6 +209,11 @@ const imageStorageReadyPromise = imImageArchive.initializeStorage().catch((err)
|
||||
return null;
|
||||
});
|
||||
|
||||
const prescriptionPdfStorageReadyPromise = initializePrescriptionPdfStorage().catch((err) => {
|
||||
logger.error("处方 PDF 目录初始化失败:", err);
|
||||
return null;
|
||||
});
|
||||
|
||||
app.get(
|
||||
"/zyt/stats",
|
||||
asyncHandler(async (req, res) => {
|
||||
@ -277,6 +284,35 @@ app.get(
|
||||
})
|
||||
);
|
||||
|
||||
// 服务端渲染处方笺。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()));
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// 返回处方笺 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"), "查询成功");
|
||||
})
|
||||
);
|
||||
|
||||
// POST请求处理器
|
||||
app.post(
|
||||
"/getYoucanData/member",
|
||||
@ -293,28 +329,6 @@ app.post(
|
||||
})
|
||||
);
|
||||
|
||||
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 跳过鉴权
|
||||
@ -361,37 +375,6 @@ app.post(
|
||||
})
|
||||
);
|
||||
|
||||
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) => {
|
||||
@ -433,42 +416,6 @@ app.post(
|
||||
})
|
||||
);
|
||||
|
||||
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",
|
||||
@ -505,124 +452,6 @@ app.get(
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
|
||||
1229
package-lock.json
generated
1229
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -47,6 +47,7 @@
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-schedule": "^2.1.1",
|
||||
"puppeteer-core": "^24.0.0",
|
||||
"querystring": "^0.2.1",
|
||||
"sm-crypto": "^0.3.13",
|
||||
"web-streams-polyfill": "^4.1.0",
|
||||
|
||||
@ -28,11 +28,15 @@ function get(url) {
|
||||
}
|
||||
|
||||
function post(url, data, config) {
|
||||
const hasPdf = data && data.data && typeof data.data.pdf == 'string' ? data.data.pdf.length > 50 : false;
|
||||
const info = { url, method: "POST", data, start: dayjs().format('YYYY-MM-DD HH:mm:ss:SSS') };
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const response = await axios.post(url, data, config);
|
||||
resolve(response.data);
|
||||
if (hasPdf) {
|
||||
info.data.data.pdf = 'pdfbase64'
|
||||
}
|
||||
writeLog(info, response);
|
||||
} catch (error) {
|
||||
info.errMsg = error?.message || error;
|
||||
|
||||
17
trigger.js
17
trigger.js
@ -4,9 +4,24 @@ const dayjs = require("dayjs");
|
||||
|
||||
exports.initSchedule = () => {
|
||||
// triggerEveryMinute()
|
||||
triggerAtNight()
|
||||
triggerAtNight();
|
||||
triggerDailySchedulePeopleOnline();
|
||||
};
|
||||
|
||||
function triggerDailySchedulePeopleOnline() {
|
||||
// node-schedule 格式:秒 分 时 日 月 周;每天 00:30 将当天排班的医生、药师上线。
|
||||
schedule.scheduleJob("0 30 0 * * *", async () => {
|
||||
try {
|
||||
const result = await api.getInternetHospitalApi({ type: "setTodayScheduledPeopleOnline" });
|
||||
if (!result?.success) {
|
||||
console.log(`[${dayjs().format('YYYY-MM-DD HH:mm:ss:SSS')}] 排班人员自动上线失败: ${result?.message || '未知错误'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`[${dayjs().format('YYYY-MM-DD HH:mm:ss:SSS')}] 排班人员自动上线失败: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function triggerEveryMinute() {
|
||||
schedule.scheduleJob("0 * * * * *", async () => {
|
||||
runHighFrequencyTask()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user