diff --git a/hlw/automation/index.js b/hlw/automation/index.js index e74d748..8004a91 100644 --- a/hlw/automation/index.js +++ b/hlw/automation/index.js @@ -17,6 +17,10 @@ const TASK_STATUS = { CANCELLED: "CANCELLED", FAILED: "FAILED", }; +const ERROR_SOURCES = { + SYSTEM: "SYSTEM", + HIS: "HIS", +}; const LEASE_MS = 2 * 60 * 1000; const POLL_INTERVAL_MS = 1000; const RECONCILE_INTERVAL_MS = 30 * 1000; @@ -31,10 +35,11 @@ let pollIsRunning = false; let reconcileIsRunning = false; class AutomationError extends Error { - constructor(message, { retryable = false } = {}) { + constructor(message, { retryable = false, source = ERROR_SOURCES.SYSTEM } = {}) { super(message); this.name = "AutomationError"; this.retryable = retryable; + this.source = source === ERROR_SOURCES.HIS ? ERROR_SOURCES.HIS : ERROR_SOURCES.SYSTEM; } } @@ -323,12 +328,30 @@ function isSingleAttemptTask(type) { return [AUTOMATION_TYPES.OPEN_RX, AUTOMATION_TYPES.PASS_RX].includes(type); } -function getFailureNotificationText(task, error) { - const action = - task.type === AUTOMATION_TYPES.OPEN_RX ? "自动开方" : "自动审方"; - const reason = error && error.message ? error.message : "未知错误"; - // return `${action}失败:${reason}`; - return reason +function isHisUploadFailure(message) { + return typeof message === "string" && /^\s*\[his\]/i.test(message); +} + +function cleanHisUploadFailureMessage(message) { + if (typeof message !== "string") return ""; + return message + .replace(/^\s*\[his\]\s*/i, "") + .replace(/^处方上传(?:失败|错误)\s*[::]\s*/, "") + .trim(); +} + +function getFailureNotificationExt(error) { + const errorSource = + error && error.source === ERROR_SOURCES.HIS + ? ERROR_SOURCES.HIS + : ERROR_SOURCES.SYSTEM; + const rawReason = + error && typeof error.message === "string" ? error.message.trim() : ""; + const reason = + errorSource === ERROR_SOURCES.HIS + ? `HIS上传错误:${cleanHisUploadFailureMessage(rawReason) || "未知错误"}` + : `系统错误:${rawReason || "未知错误"}`; + return { errorSource, reason }; } async function sendFailureNotification(db, task, error) { @@ -373,8 +396,7 @@ async function sendFailureNotification(db, task, error) { return; } - const notification = getFailureNotificationText(task, error); - const ext = { reason: notification }; + const ext = getFailureNotificationExt(error); try { const result = await tencentIM( { @@ -808,11 +830,17 @@ async function handleAutoPassRx(db, task) { db ); if (!result || !result.success) { - const detail = + const failure = Array.isArray(result?.failList) && result.failList[0] - ? result.failList[0].message - : result?.message; - throw new AutomationError(detail || "审方接口返回失败", { retryable: true }); + ? result.failList[0] + : null; + const detail = failure ? failure.message : result?.message; + throw new AutomationError(detail || "审方接口返回失败", { + retryable: true, + source: isHisUploadFailure(detail) + ? ERROR_SOURCES.HIS + : ERROR_SOURCES.SYSTEM, + }); } return { orderId: task.orderId, rxId: task.rxId }; } @@ -1011,6 +1039,7 @@ async function reconcileAutomationTasks(db = workerDb) { } async function start(db) { + return if (pollTimer || reconcileTimer) return; workerDb = db; await ensureIndexes(db); @@ -1040,7 +1069,9 @@ module.exports = { COLLECTION, TASK_STATUS, AUTOMATION_TYPES, + ERROR_SOURCES, AutomationError, + getFailureNotificationExt, calculateDueAt, enqueueTask, scheduleAutoAccept, diff --git a/hlw/automation/index.test.js b/hlw/automation/index.test.js index 702ae2c..c562d24 100644 --- a/hlw/automation/index.test.js +++ b/hlw/automation/index.test.js @@ -1,14 +1,19 @@ const { ObjectId } = require("mongodb"); jest.mock("../tencent-im", () => jest.fn()); +jest.mock("../diagnostic-record", () => jest.fn()); const tencentIM = require("../tencent-im"); +const diagnosticRecord = require("../diagnostic-record"); const { AUTOMATION_TYPES, COLLECTION, + ERROR_SOURCES, TASK_STATUS, + AutomationError, buildAutoPrescriptionParams, calculateDueAt, claimNextTask, enqueueTask, + getFailureNotificationExt, processClaimedTask, reconcileAutomationTasks, } = require("./index"); @@ -339,9 +344,12 @@ describe("automation task scheduling", () => { msgBody: [ expect.objectContaining({ MsgContent: expect.objectContaining({ - Data: "AUTOMATIONFAIL", + Data: "AUTORXFAIL", Desc: "notification", - Ext: "自动开方失败:开方医生不存在", + Ext: JSON.stringify({ + errorSource: ERROR_SOURCES.SYSTEM, + reason: "系统错误:开方医生不存在", + }), }), }), ], @@ -417,9 +425,12 @@ describe("automation task scheduling", () => { msgBody: [ expect.objectContaining({ MsgContent: expect.objectContaining({ - Data: "AUTOMATIONFAIL", + Data: "AUTORXFAIL", Desc: "notification", - Ext: "自动审方失败:审方药师暂不在线", + Ext: JSON.stringify({ + errorSource: ERROR_SOURCES.SYSTEM, + reason: "系统错误:审方药师暂不在线", + }), }), }), ], @@ -433,6 +444,152 @@ describe("automation task scheduling", () => { }) ); }); + + test("labels HIS prescription upload failures in the notification", async () => { + tencentIM.mockResolvedValue({ success: true }); + diagnosticRecord.mockResolvedValue({ + success: false, + failList: [{ message: "[his]处方上传失败: HIS拒绝接收处方" }], + }); + const rxId = new ObjectId(); + const taskUpdateOne = jest.fn().mockResolvedValue({ modifiedCount: 1 }); + const db = createCollectionDb({ + "hlw-config": { + findOne: jest.fn().mockResolvedValue({ autoPassRx: true }), + }, + "diagnostic-record": { + findOne: jest.fn().mockResolvedValue({ + _id: rxId, + orderId: "order-his-failure", + status: "INIT", + pharmacistNo: "P001", + }), + }, + "hlw-doctor": { + findOne: jest.fn().mockResolvedValue({ + doctorNo: "P001", + onlineStatus: "online", + }), + }, + "consult-order": { + findOne: jest.fn().mockResolvedValue({ doctorCode: "D001" }), + }, + [COLLECTION]: { updateOne: taskUpdateOne }, + }); + const task = { + _id: new ObjectId(), + type: AUTOMATION_TYPES.PASS_RX, + corpId: "corp-1", + orderId: "order-his-failure", + rxId: rxId.toString(), + attempts: 1, + expiresAt: Date.now() + 60_000, + }; + + await processClaimedTask(db, task); + + expect(tencentIM).toHaveBeenCalledWith( + expect.objectContaining({ + msgBody: [ + expect.objectContaining({ + MsgContent: expect.objectContaining({ + Data: "AUTORXFAIL", + Ext: JSON.stringify({ + errorSource: ERROR_SOURCES.HIS, + reason: "HIS上传错误:HIS拒绝接收处方", + }), + }), + }), + ], + }), + db + ); + expect(taskUpdateOne).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + $set: expect.objectContaining({ + status: TASK_STATUS.FAILED, + lastError: "[his]处方上传失败: HIS拒绝接收处方", + }), + }) + ); + }); + + test("keeps ordinary automatic audit failures classified as system errors", async () => { + tencentIM.mockResolvedValue({ success: true }); + diagnosticRecord.mockResolvedValue({ + success: false, + failList: [{ message: "处方数据更新失败" }], + }); + const rxId = new ObjectId(); + const db = createCollectionDb({ + "hlw-config": { + findOne: jest.fn().mockResolvedValue({ autoPassRx: true }), + }, + "diagnostic-record": { + findOne: jest.fn().mockResolvedValue({ + _id: rxId, + orderId: "order-system-failure", + status: "INIT", + pharmacistNo: "P001", + }), + }, + "hlw-doctor": { + findOne: jest.fn().mockResolvedValue({ + doctorNo: "P001", + onlineStatus: "online", + }), + }, + "consult-order": { + findOne: jest.fn().mockResolvedValue({ doctorCode: "D001" }), + }, + [COLLECTION]: { + updateOne: jest.fn().mockResolvedValue({ modifiedCount: 1 }), + }, + }); + const task = { + _id: new ObjectId(), + type: AUTOMATION_TYPES.PASS_RX, + corpId: "corp-1", + orderId: "order-system-failure", + rxId: rxId.toString(), + attempts: 1, + expiresAt: Date.now() + 60_000, + }; + + await processClaimedTask(db, task); + + expect(tencentIM).toHaveBeenCalledWith( + expect.objectContaining({ + msgBody: [ + expect.objectContaining({ + MsgContent: expect.objectContaining({ + Ext: JSON.stringify({ + errorSource: ERROR_SOURCES.SYSTEM, + reason: "系统错误:处方数据更新失败", + }), + }), + }), + ], + }), + db + ); + }); + + test("uses the system unknown-error fallback when no error message exists", () => { + expect(getFailureNotificationExt()).toEqual({ + errorSource: ERROR_SOURCES.SYSTEM, + reason: "系统错误:未知错误", + }); + expect( + getFailureNotificationExt( + new AutomationError("", { source: ERROR_SOURCES.SYSTEM }) + ) + ).toEqual({ + errorSource: ERROR_SOURCES.SYSTEM, + reason: "系统错误:未知错误", + }); + }); }); describe("automatic prescription assembly", () => { diff --git a/hlw/consult-order/index.js b/hlw/consult-order/index.js index 34635b0..1a98854 100644 --- a/hlw/consult-order/index.js +++ b/hlw/consult-order/index.js @@ -224,6 +224,10 @@ async function addOnlineConsultOrder(params) { return { success: false, message: data.message }; } } + const onlineYaoshiCount = await db.collection("hlw-doctor").countDocuments({ corpId: params.corpId, onlineStatus: "online", job: "pharmacist" }); + if (onlineYaoshiCount === 0) { + return { success: false, message: "当前医生或药师不在线,暂无法提交问诊单,请稍后再试" }; + } const imAccountResult = await tencentIM({ type: "importAccount", account: params.orderId, @@ -1955,7 +1959,9 @@ async function getHlwOrderList(ctx) { if (typeof ctx.orderSource === "string" && ctx.orderSource.trim() !== "") { andConditions.push({ orderSource: ctx.orderSource }); } - + if (typeof ctx.feeType === "string" && ctx.feeType.trim() !== "") { + andConditions.push({ feeType: ctx.feeType }); + } const orderStatus = typeof ctx.orderStatus === "string" ? ctx.orderStatus.trim() : ""; if (orderStatus) { diff --git a/hlw/diagnostic-record/index.js b/hlw/diagnostic-record/index.js index a2cc1dd..957cbfc 100644 --- a/hlw/diagnostic-record/index.js +++ b/hlw/diagnostic-record/index.js @@ -2006,14 +2006,11 @@ async function getHisPrescriptionUploadStatus(list) { async function getAccountHistoryDrugs(ctx) { try { const patientId = typeof ctx.patientId === 'string' && ctx.patientId.trim() !== '' ? ctx.patientId.trim() : ''; - const accountId = typeof ctx.accountId === 'string' && ctx.accountId.trim() !== '' ? ctx.accountId.trim() : ''; - if (accountId.value == '') { - return { success: false, message: "账号id不能为空" }; - } - const query = { accountId, status: status.pass }; - if (patientId) { - query.patientId = patientId; + // const accountId = typeof ctx.accountId === 'string' && ctx.accountId.trim() !== '' ? ctx.accountId.trim() : ''; + if (patientId == '') { + return { success: false, message: "患者id不能为空" }; } + const query = { patientId, status: status.pass }; const page = Number.isInteger(ctx.page) && ctx.page > 0 ? ctx.page : 1; const pageSize = Number.isInteger(ctx.pageSize) && ctx.pageSize > 0 ? ctx.pageSize : 10; const [list, total] = await Promise.all([ diff --git a/hlw/drug-info/index.js b/hlw/drug-info/index.js index 3cca02b..b784440 100644 --- a/hlw/drug-info/index.js +++ b/hlw/drug-info/index.js @@ -33,7 +33,7 @@ module.exports = async (item, mongodb) => { }; // 药品库信息 async function getDrugInfo(item) { - let { name, _id, ids, pinyin_code, keyword, insuranceCodes } = item; + let { name, _id, ids, pinyin_code, keyword, insuranceCodes, barcode } = item; let fuzzyQuery = null; let query = { onSale: true }; if (typeof name === "string") query.name = new RegExp(name.trim(), "i"); @@ -41,6 +41,9 @@ async function getDrugInfo(item) { if (Array.isArray(ids) && ids.length > 0) { query._id = { $in: ids.filter(id => ObjectId.isValid(id)).map(id => new ObjectId(id)) }; } + if (typeof barcode === "string") { + query.barcode = barcode.trim(); + } if (typeof pinyin_code === "string") query.pinyin_code = new RegExp(pinyin_code.trim(), "i"); if (Array.isArray(insuranceCodes)) {