fix: 问题修复

This commit is contained in:
huxuejian 2026-08-05 18:39:56 +08:00
parent 42d6a6d71b
commit b81a6a85ca
5 changed files with 220 additions and 26 deletions

View File

@ -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,

View File

@ -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", () => {

View File

@ -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) {

View File

@ -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([

View File

@ -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)) {