fix: 续期开发

This commit is contained in:
huxuejian 2026-07-31 16:22:09 +08:00
parent e3e7951310
commit cf095dba0c
16 changed files with 2051 additions and 56 deletions

136
hlw/automation/config.js Normal file
View File

@ -0,0 +1,136 @@
const AUTOMATION_TYPES = {
ACCEPT: "AUTO_ACCEPT",
OPEN_RX: "AUTO_OPEN_RX",
PASS_RX: "AUTO_PASS_RX",
};
const DEFAULT_AUTOMATION_CONFIG = {
autoAcceptOrder: false,
autoOpenRx: false,
autoPassRx: false,
autoAcceptDelayMinSeconds: 2,
autoAcceptDelayMaxSeconds: 5,
autoOpenRxDelayMinSeconds: 20,
autoOpenRxDelayMaxSeconds: 40,
autoPassRxDelayMinSeconds: 10,
autoPassRxDelayMaxSeconds: 30,
};
const TYPE_CONFIG = {
[AUTOMATION_TYPES.ACCEPT]: {
enabledKey: "autoAcceptOrder",
minKey: "autoAcceptDelayMinSeconds",
maxKey: "autoAcceptDelayMaxSeconds",
},
[AUTOMATION_TYPES.OPEN_RX]: {
enabledKey: "autoOpenRx",
minKey: "autoOpenRxDelayMinSeconds",
maxKey: "autoOpenRxDelayMaxSeconds",
},
[AUTOMATION_TYPES.PASS_RX]: {
enabledKey: "autoPassRx",
minKey: "autoPassRxDelayMinSeconds",
maxKey: "autoPassRxDelayMaxSeconds",
},
};
const AUTOMATION_DELAY_KEYS = [
"autoAcceptDelayMinSeconds",
"autoAcceptDelayMaxSeconds",
"autoOpenRxDelayMinSeconds",
"autoOpenRxDelayMaxSeconds",
"autoPassRxDelayMinSeconds",
"autoPassRxDelayMaxSeconds",
];
const AUTOMATION_DELAY_PAIRS = [
["autoAcceptDelayMinSeconds", "autoAcceptDelayMaxSeconds"],
["autoOpenRxDelayMinSeconds", "autoOpenRxDelayMaxSeconds"],
["autoPassRxDelayMinSeconds", "autoPassRxDelayMaxSeconds"],
];
function normalizeNonNegativeInteger(value, fallback) {
return Number.isInteger(value) && value >= 0 ? value : fallback;
}
function normalizeAutomationConfig(config = {}) {
const result = {
...DEFAULT_AUTOMATION_CONFIG,
...config,
};
Object.values(TYPE_CONFIG).forEach(({ minKey, maxKey }) => {
const defaultMin = DEFAULT_AUTOMATION_CONFIG[minKey];
const defaultMax = DEFAULT_AUTOMATION_CONFIG[maxKey];
result[minKey] = normalizeNonNegativeInteger(config[minKey], defaultMin);
result[maxKey] = normalizeNonNegativeInteger(config[maxKey], defaultMax);
if (result[minKey] > result[maxKey]) {
result[minKey] = defaultMin;
result[maxKey] = defaultMax;
}
});
result.autoAcceptOrder = config.autoAcceptOrder === true;
result.autoOpenRx = config.autoOpenRx === true;
result.autoPassRx = config.autoPassRx === true;
return result;
}
function getTypeConfig(config, type) {
const normalized = normalizeAutomationConfig(config);
const mapping = TYPE_CONFIG[type];
if (!mapping) {
throw new Error(`未知自动化任务类型: ${type}`);
}
return {
enabled: normalized[mapping.enabledKey] === true,
minSeconds: normalized[mapping.minKey],
maxSeconds: normalized[mapping.maxKey],
};
}
function randomInteger(min, max, random = Math.random) {
if (min === max) return min;
return Math.floor(random() * (max - min + 1)) + min;
}
function validateAutomationDelayConfig(input = {}, current = {}) {
const data = {};
for (const key of AUTOMATION_DELAY_KEYS) {
if (key in input) {
if (!Number.isInteger(input[key]) || input[key] < 0 || input[key] > 3600) {
return {
success: false,
message: `${key}必须是0到3600之间的整数`,
};
}
data[key] = input[key];
}
}
const merged = {
...normalizeAutomationConfig(current),
...data,
};
for (const [minKey, maxKey] of AUTOMATION_DELAY_PAIRS) {
if (merged[minKey] > merged[maxKey]) {
return {
success: false,
message: `${minKey}不能大于${maxKey}`,
};
}
}
return { success: true, data };
}
module.exports = {
AUTOMATION_TYPES,
DEFAULT_AUTOMATION_CONFIG,
TYPE_CONFIG,
AUTOMATION_DELAY_KEYS,
AUTOMATION_DELAY_PAIRS,
normalizeAutomationConfig,
getTypeConfig,
randomInteger,
validateAutomationDelayConfig,
};

View File

@ -0,0 +1,75 @@
const {
AUTOMATION_TYPES,
getTypeConfig,
normalizeAutomationConfig,
randomInteger,
validateAutomationDelayConfig,
} = require("./config");
describe("automation config", () => {
test("provides backward-compatible defaults", () => {
expect(normalizeAutomationConfig({})).toMatchObject({
autoAcceptOrder: false,
autoOpenRx: false,
autoPassRx: false,
autoAcceptDelayMinSeconds: 2,
autoAcceptDelayMaxSeconds: 5,
autoOpenRxDelayMinSeconds: 20,
autoOpenRxDelayMaxSeconds: 40,
autoPassRxDelayMinSeconds: 10,
autoPassRxDelayMaxSeconds: 30,
});
});
test("normalizes an invalid stored interval back to defaults", () => {
const config = normalizeAutomationConfig({
autoOpenRx: true,
autoOpenRxDelayMinSeconds: 50,
autoOpenRxDelayMaxSeconds: 10,
});
expect(getTypeConfig(config, AUTOMATION_TYPES.OPEN_RX)).toEqual({
enabled: true,
minSeconds: 20,
maxSeconds: 40,
});
});
test("validates partial updates against the stored counterpart", () => {
expect(
validateAutomationDelayConfig(
{ autoPassRxDelayMinSeconds: 31 },
{ autoPassRxDelayMaxSeconds: 30 }
)
).toEqual({
success: false,
message: "autoPassRxDelayMinSeconds不能大于autoPassRxDelayMaxSeconds",
});
expect(
validateAutomationDelayConfig(
{ autoPassRxDelayMinSeconds: 12 },
{ autoPassRxDelayMaxSeconds: 30 }
)
).toEqual({
success: true,
data: { autoPassRxDelayMinSeconds: 12 },
});
});
test("rejects non-integer and out-of-range delays", () => {
expect(
validateAutomationDelayConfig({ autoAcceptDelayMinSeconds: 1.5 })
.success
).toBe(false);
expect(
validateAutomationDelayConfig({ autoAcceptDelayMinSeconds: 3601 })
.success
).toBe(false);
});
test("selects both ends of a configured random interval", () => {
expect(randomInteger(2, 5, () => 0)).toBe(2);
expect(randomInteger(2, 5, () => 0.999999)).toBe(5);
});
});

View File

@ -0,0 +1,34 @@
import { ObjectId } from "mongodb";
export type AutomationTaskType =
| "AUTO_ACCEPT"
| "AUTO_OPEN_RX"
| "AUTO_PASS_RX";
export type AutomationTaskStatus =
| "PENDING"
| "RUNNING"
| "SUCCEEDED"
| "CANCELLED"
| "FAILED";
export interface AutomationTask {
_id: ObjectId;
taskKey: string;
type: AutomationTaskType;
corpId: string;
orderId: string;
rxId?: string;
status: AutomationTaskStatus;
dueAt: number;
nextRunAt: number;
expiresAt: number;
attempts: number;
leaseUntil: number;
leaseOwner: string;
lastError: string;
result?: Record<string, unknown>;
createdAt: number;
updatedAt: number;
completedAt?: number;
}

958
hlw/automation/index.js Normal file
View File

@ -0,0 +1,958 @@
const { ObjectId } = require("mongodb");
const { randomUUID } = require("crypto");
const {
AUTOMATION_TYPES,
getTypeConfig,
normalizeAutomationConfig,
randomInteger,
} = require("./config");
const { decryptOrderFields } = require("../consult-order/format");
const COLLECTION = "hlw-automation-task";
const TASK_STATUS = {
PENDING: "PENDING",
RUNNING: "RUNNING",
SUCCEEDED: "SUCCEEDED",
CANCELLED: "CANCELLED",
FAILED: "FAILED",
};
const LEASE_MS = 2 * 60 * 1000;
const POLL_INTERVAL_MS = 1000;
const RECONCILE_INTERVAL_MS = 30 * 1000;
const MAX_TASKS_PER_TICK = 10;
const MAX_RETRY_DELAY_MS = 10 * 60 * 1000;
const WORKER_ID = `${process.pid}:${randomUUID()}`;
let workerDb = null;
let pollTimer = null;
let reconcileTimer = null;
let pollIsRunning = false;
let reconcileIsRunning = false;
class AutomationError extends Error {
constructor(message, { retryable = false } = {}) {
super(message);
this.name = "AutomationError";
this.retryable = retryable;
}
}
function getTaskKey(type, orderId, rxId) {
if (type === AUTOMATION_TYPES.PASS_RX) {
return `${type}:${rxId}`;
}
return `${type}:${orderId}`;
}
async function getConfig(db, corpId) {
const config = await db.collection("hlw-config").findOne({ corpId });
return normalizeAutomationConfig(config || {});
}
function getBusinessExpiry(order, fallbackMs = 30 * 60 * 1000) {
if (order && Number.isFinite(order.expireTime) && order.expireTime > Date.now()) {
return order.expireTime;
}
return Date.now() + fallbackMs;
}
function calculateDueAt(type, config, baseTime = Date.now(), random = Math.random) {
const { minSeconds, maxSeconds } = getTypeConfig(config, type);
const seconds = randomInteger(minSeconds, maxSeconds, random);
let dueAt = baseTime + seconds * 1000;
if (type === AUTOMATION_TYPES.OPEN_RX) {
const firstSubmitRxIntervel =
Number.isInteger(config.firstSubmitRxIntervel) && config.firstSubmitRxIntervel > 0
? config.firstSubmitRxIntervel
: 0;
dueAt = Math.max(dueAt, baseTime + firstSubmitRxIntervel * 1000);
}
if (dueAt < Date.now()) {
dueAt = Date.now() + randomInteger(0, 2, random) * 1000;
}
return dueAt;
}
async function enqueueTask(
db,
{ type, corpId, orderId, rxId = "", baseTime = Date.now(), expiresAt }
) {
if (!corpId || !orderId || !Object.values(AUTOMATION_TYPES).includes(type)) {
return { success: false, message: "自动化任务参数错误" };
}
if (type === AUTOMATION_TYPES.PASS_RX && !rxId) {
return { success: false, message: "自动审方任务缺少处方ID" };
}
const config = await getConfig(db, corpId);
const stageConfig = getTypeConfig(config, type);
if (!stageConfig.enabled) {
return { success: false, skipped: true, message: "自动化配置未开启" };
}
const now = Date.now();
const taskKey = getTaskKey(type, orderId, rxId);
const dueAt = calculateDueAt(type, config, baseTime);
const task = {
taskKey,
type,
corpId,
orderId,
rxId,
status: TASK_STATUS.PENDING,
dueAt,
nextRunAt: dueAt,
expiresAt: Number.isFinite(expiresAt) ? expiresAt : now + 30 * 60 * 1000,
attempts: 0,
leaseUntil: 0,
leaseOwner: "",
lastError: "",
createdAt: now,
updatedAt: now,
};
const reactivated = await db.collection(COLLECTION).updateOne(
{ taskKey, status: TASK_STATUS.CANCELLED },
{
$set: {
...task,
createdAt: now,
},
$unset: {
completedAt: "",
result: "",
},
}
);
if (reactivated.modifiedCount === 1) {
return {
success: true,
created: true,
reactivated: true,
taskKey,
dueAt,
};
}
let result;
try {
result = await db.collection(COLLECTION).updateOne(
{ taskKey },
{ $setOnInsert: task },
{ upsert: true }
);
} catch (error) {
if (error && error.code === 11000) {
return { success: true, created: false, taskKey, dueAt };
}
throw error;
}
return {
success: true,
created: result.upsertedCount === 1,
taskKey,
dueAt,
};
}
async function scheduleAutoAccept({ db, orderId, corpId, baseTime, expiresAt }) {
const eligible = await db.collection("consult-order").findOne(
{ orderId, corpId, orderSource: "ALIPAY_MINI" },
{ projection: { _id: 1 } }
);
if (!eligible) {
return { success: false, skipped: true, message: "非支付宝小程序订单" };
}
return enqueueTask(db, {
type: AUTOMATION_TYPES.ACCEPT,
orderId,
corpId,
baseTime,
expiresAt,
});
}
async function scheduleAutoOpenRx({ db, orderId, corpId, baseTime, expiresAt }) {
const eligible = await db.collection("consult-order").findOne(
{ orderId, corpId, orderSource: "ALIPAY_MINI" },
{ projection: { _id: 1 } }
);
if (!eligible) {
return { success: false, skipped: true, message: "非支付宝小程序订单" };
}
return enqueueTask(db, {
type: AUTOMATION_TYPES.OPEN_RX,
orderId,
corpId,
baseTime,
expiresAt,
});
}
async function scheduleAutoPassRx({
db,
orderId,
rxId,
corpId,
baseTime,
expiresAt,
}) {
const eligible = await db.collection("consult-order").findOne(
{ orderId, corpId, orderSource: "ALIPAY_MINI" },
{ projection: { _id: 1 } }
);
if (!eligible) {
return { success: false, skipped: true, message: "非支付宝小程序订单" };
}
return enqueueTask(db, {
type: AUTOMATION_TYPES.PASS_RX,
orderId,
rxId: rxId && rxId.toString(),
corpId,
baseTime,
expiresAt,
});
}
async function ensureIndexes(db) {
const collection = db.collection(COLLECTION);
await collection.createIndex({ taskKey: 1 }, { unique: true });
await collection.createIndex({ status: 1, nextRunAt: 1, leaseUntil: 1 });
await collection.createIndex({ orderId: 1, type: 1 });
await collection.createIndex({ completedAt: 1 });
}
async function claimNextTask(db, now = Date.now()) {
const result = await db.collection(COLLECTION).findOneAndUpdate(
{
$or: [
{
status: TASK_STATUS.PENDING,
nextRunAt: { $lte: now },
},
{
status: TASK_STATUS.RUNNING,
leaseUntil: { $lte: now },
},
],
},
{
$set: {
status: TASK_STATUS.RUNNING,
leaseUntil: now + LEASE_MS,
leaseOwner: WORKER_ID,
updatedAt: now,
},
$inc: { attempts: 1 },
},
{
sort: { nextRunAt: 1, createdAt: 1 },
returnDocument: "after",
}
);
return result && result.value ? result.value : result;
}
async function markTask(db, task, status, extra = {}) {
const now = Date.now();
const terminal = [
TASK_STATUS.SUCCEEDED,
TASK_STATUS.CANCELLED,
TASK_STATUS.FAILED,
].includes(status);
await db.collection(COLLECTION).updateOne(
{
_id: task._id,
status: TASK_STATUS.RUNNING,
leaseOwner: WORKER_ID,
},
{
$set: {
status,
leaseUntil: 0,
leaseOwner: "",
updatedAt: now,
...(terminal ? { completedAt: now } : {}),
...extra,
},
}
);
}
async function renewLease(db, task) {
await db.collection(COLLECTION).updateOne(
{
_id: task._id,
status: TASK_STATUS.RUNNING,
leaseOwner: WORKER_ID,
},
{
$set: {
leaseUntil: Date.now() + LEASE_MS,
updatedAt: Date.now(),
},
}
);
}
function getRetryDelay(attempts) {
const exponent = Math.max(0, Math.min(attempts - 1, 10));
const base = Math.min(5000 * 2 ** exponent, MAX_RETRY_DELAY_MS);
return Math.min(base + Math.floor(Math.random() * 3000), MAX_RETRY_DELAY_MS);
}
async function retryTask(db, task, error) {
const now = Date.now();
if (!Number.isFinite(task.expiresAt) || task.expiresAt <= now) {
await markTask(db, task, TASK_STATUS.FAILED, {
lastError: `业务已过期: ${error.message}`,
});
return;
}
const nextRunAt = Math.min(now + getRetryDelay(task.attempts || 1), task.expiresAt);
await markTask(db, task, TASK_STATUS.PENDING, {
nextRunAt,
lastError: error.message,
});
}
async function assertTaskEnabled(db, task) {
const config = await getConfig(db, task.corpId);
const { enabled } = getTypeConfig(config, task.type);
if (!enabled) {
await markTask(db, task, TASK_STATUS.CANCELLED, {
lastError: "执行前检查发现自动化配置已关闭",
});
return null;
}
return config;
}
async function getAlipayOrder(db, orderId) {
const raw = await db.collection("consult-order").findOne({ orderId });
if (!raw) {
throw new AutomationError("咨询订单不存在");
}
if (raw.orderSource !== "ALIPAY_MINI") {
throw new AutomationError("仅支持支付宝小程序咨询订单");
}
return decryptOrderFields(raw);
}
async function handleAutoAccept(db, task) {
const order = await getAlipayOrder(db, task.orderId);
if (order.orderStatus === "processing") {
await scheduleAutoOpenRx({
db,
orderId: order.orderId,
corpId: order.corpId,
baseTime: order.prescriptionStartTime || Date.now(),
expiresAt: getBusinessExpiry(order),
});
return { orderId: order.orderId };
}
if (order.orderStatus !== "pending") {
throw new AutomationError(`订单状态不支持自动接诊: ${order.orderStatus}`);
}
if (Number.isFinite(order.expireTime) && order.expireTime <= Date.now()) {
throw new AutomationError("咨询订单已过期");
}
const consultOrder = require("../consult-order");
const result = await consultOrder(
{
type: "acceptConsultOrder",
orderId: order.orderId,
corpId: order.corpId,
doctorCode: order.doctorCode,
operationSource: "AUTO",
},
db
);
if (!result || !result.success) {
throw new AutomationError(result?.message || "自动接诊失败", { retryable: true });
}
return { orderId: order.orderId };
}
async function resolveDiagnosisList(db, order) {
const result = [];
const seen = new Set();
const add = (code, name) => {
const normalizedCode = typeof code === "string" ? code.trim() : "";
const normalizedName = typeof name === "string" ? name.trim() : "";
if (!normalizedCode || !normalizedName) return;
const key = `${normalizedCode}:${normalizedName}`;
if (!seen.has(key)) {
result.push({ code: normalizedCode, name: normalizedName, desc: "" });
seen.add(key);
}
};
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())
: [];
if (diseaseNames.length) {
const diagnosisRecords = await db
.collection("hlw-diagnosis")
.find({ name: { $in: diseaseNames } }, { projection: { code: 1, name: 1 } })
.toArray();
for (const name of diseaseNames) {
const match = diagnosisRecords.find((item) => item.name === name);
if (!match || !match.code) {
throw new AutomationError(`诊断“${name}”无法精确匹配诊断编码`);
}
add(match.code, match.name);
}
}
// if (!result.length) {
// throw new AutomationError("订单缺少可用于开方的诊断编码");
// }
return result;
}
async function getMedicineConfig(db, corpId) {
const records = await db
.collection("hlw-config")
.find(
{ group: `${corpId}-medicine-related` },
{ projection: { key: 1, list: 1 } }
)
.toArray();
return records.reduce((map, item) => {
if (item.key && Array.isArray(item.list)) {
map[item.key] = item.list;
}
return map;
}, {});
}
function matchConfig(list, code, name) {
const items = Array.isArray(list) ? list : [];
return (
items.find((item) => code !== undefined && code !== "" && item.code == code) ||
items.find((item) => name && item.name === name)
);
}
async function resolvePrescriptionDrugs(db, order) {
const orderDrugs = Array.isArray(order.drugs) ? order.drugs : [];
if (!orderDrugs.length) {
throw new AutomationError("订单缺少药品,无法自动开方");
}
const ids = orderDrugs
.map((item) => (ObjectId.isValid(item._id) ? new ObjectId(item._id) : null))
.filter(Boolean);
if (ids.length !== orderDrugs.length) {
throw new AutomationError("订单药品ID不完整");
}
const collectionName =
order.consultType === "onlineMedicinePurchase" ? "online-drug-info" : "drug-info";
const masterDrugs = await db
.collection(collectionName)
.find({ _id: { $in: ids }, onSale: true })
.toArray();
if (masterDrugs.length !== orderDrugs.length) {
throw new AutomationError("订单中存在已下架或不存在的药品");
}
const medicineConfig = await getMedicineConfig(db, order.corpId);
const prefix =
order.consultType === "onlineMedicinePurchase" ? "online-medicine" : "store-medicine";
const dosageUnitList = medicineConfig[`${prefix}-dosage-unit`] || [];
const frequencyList = medicineConfig[`${prefix}-frequence`] || [];
const usageList = medicineConfig[`${prefix}-administration`] || [];
const unitList = medicineConfig["medicine-package-unit"] || [];
return orderDrugs.map((requested) => {
const master = masterDrugs.find(
(item) => item._id.toString() === requested._id.toString()
);
const usage = matchConfig(
usageList,
requested.usageCode,
requested.usageName || master.administration_method
);
const frequency = matchConfig(
frequencyList,
requested.frequencyCode,
requested.frequencyName || master.freq
);
const dosageUnit = matchConfig(
dosageUnitList,
requested.dosage_unit_code,
requested.dosage_unit || master.dosage_unit
);
const unit = matchConfig(unitList, requested.unit || master.unit, requested.unit || master.unit);
const dosage = Number(requested.dosage);
const quantity = Number(requested.quantity);
const days = Number(master.days);
if (!usage || !frequency || !dosageUnit || !unit) {
throw new AutomationError(`药品“${master.name}”的用法用量配置无法匹配`);
}
if (!(dosage > 0) || !(quantity > 0) || !Number.isInteger(quantity)) {
throw new AutomationError(`药品“${master.name}”的剂量或数量不正确`);
}
if (!(days > 0) || !Number.isInteger(days)) {
throw new AutomationError(`药品“${master.name}”未维护有效的用药天数`);
}
if (!master.erpId || !master.insurance_code) {
throw new AutomationError(`药品“${master.name}”缺少HIS所需编码`);
}
return {
_id: master._id.toString(),
erpId: master.erpId,
dosage_form: master.dosage_form || "",
days,
dosage,
dosage_unit: dosageUnit.name,
dosage_unit_code: dosageUnit.code,
drugName: master.name,
specification: master.specification || "",
frequencyCode: frequency.code,
frequencyName: frequency.name,
insurance_code: master.insurance_code,
product_id: master.product_id,
quantity,
unit: unit.code,
usageCode: usage.code,
usageName: usage.name,
package_amount: master.package_amount,
recommended_quantity: master.recommended_quantity,
forceSelfPay: master.forceSelfPay === "Y" ? "Y" : "N",
limitUsageScope:
typeof master.usage_restriction_desc === "string" &&
master.usage_restriction_desc.trim()
? "Y"
: "N",
};
});
}
async function buildAutoPrescriptionParams(db, order) {
const doctor = await db.collection("hlw-doctor").findOne({
corpId: order.corpId,
doctorNo: order.doctorCode,
job: "doctor",
});
if (!doctor) {
throw new AutomationError("开方医生不存在");
}
if (doctor.onlineStatus !== "online") {
throw new AutomationError("开方医生暂不在线", { retryable: true });
}
const [diagnosisList, drugs] = await Promise.all([
resolveDiagnosisList(db, order),
resolvePrescriptionDrugs(db, order),
]);
const diseaseText = Array.isArray(order.diseases) ? order.diseases.join("") : "";
const complaint = [diseaseText, order.description]
.filter((item) => typeof item === "string" && item.trim())
.join(" ")
.slice(0, 500);
if (!complaint) {
throw new AutomationError("订单缺少主诉和病情描述");
}
const config = await getConfig(db, order.corpId);
const prescriptionType =
order.consultType === "onlineMedicinePurchase"
? "onlineMedicinePurchase"
: "storeMedicinePurchase";
const medicinePurchaseRxDuration =
Number.isInteger(config.medicinePurchaseRxDuration) &&
config.medicinePurchaseRxDuration > 0
? config.medicinePurchaseRxDuration
: 30;
return {
complaint,
presentIllness:
typeof order.pastHistoryStr === "string" ? order.pastHistoryStr : "",
dispose: "",
doctorCAUserId: doctor.signatureUrl || "",
patientId: order.patientId,
name: order.name,
orderId: order.orderId,
doctorCode: order.doctorCode,
doctorName: order.doctorName,
deptName: order.deptName,
unitCode: order.unitCode,
drugStoreNo: order.drugStoreNo,
orderSource: order.orderSource,
idCard: order.idCard,
blhno: order.blhno,
medOrgOrderNo: order.medorg_order_no,
address: order.address,
mobile: order.mobile,
prescriptionType,
pickUpType: order.pickUpType,
expireTime:
prescriptionType === "onlineMedicinePurchase"
? Date.now() + medicinePurchaseRxDuration * 60 * 1000
: order.expireTime,
diagnosisList,
drugs,
};
}
async function handleAutoOpenRx(db, task) {
const order = await getAlipayOrder(db, task.orderId);
const existing = await db.collection("diagnostic-record").findOne(
{
orderId: order.orderId,
status: { $in: ["INIT", "PASS"] },
},
{ projection: { _id: 1, status: 1, createTime: 1, expireTime: 1 } }
);
if (existing) {
await scheduleAutoPassRx({
db,
orderId: order.orderId,
rxId: existing._id,
corpId: order.corpId,
baseTime: existing.createTime || Date.now(),
expiresAt: existing.expireTime || getBusinessExpiry(order),
});
return { orderId: order.orderId, rxId: existing._id.toString() };
}
if (order.orderStatus !== "processing") {
throw new AutomationError(`订单状态不支持自动开方: ${order.orderStatus}`);
}
if (Number.isFinite(order.expireTime) && order.expireTime <= Date.now()) {
throw new AutomationError("咨询订单已过期");
}
const params = await buildAutoPrescriptionParams(db, order);
const diagnosticRecord = require("../diagnostic-record");
const result = await diagnosticRecord(
{
type: "addConsultDiagnosis",
corpId: order.corpId,
params,
automated: true,
operationSource: "AUTO",
},
db
);
if (!result || !result.success) {
throw new AutomationError(result?.message || "自动开方失败", { retryable: true });
}
const rx = await db.collection("diagnostic-record").findOne(
{ orderId: order.orderId, status: { $in: ["INIT", "PASS"] } },
{ projection: { _id: 1, createTime: 1, expireTime: 1 } }
);
if (!rx) {
throw new AutomationError("自动开方成功但未查询到处方", { retryable: true });
}
await scheduleAutoPassRx({
db,
orderId: order.orderId,
rxId: rx._id,
corpId: order.corpId,
baseTime: rx.createTime || Date.now(),
expiresAt: rx.expireTime || getBusinessExpiry(order),
});
return { orderId: order.orderId, rxId: rx._id.toString() };
}
async function handleAutoPassRx(db, task) {
if (!ObjectId.isValid(task.rxId)) {
throw new AutomationError("处方ID格式错误");
}
const rx = await db.collection("diagnostic-record").findOne({
_id: new ObjectId(task.rxId),
orderId: task.orderId,
});
if (!rx) {
throw new AutomationError("处方不存在");
}
if (rx.status === "PASS") {
return { orderId: task.orderId, rxId: task.rxId };
}
if (rx.status !== "INIT") {
throw new AutomationError(`处方状态不支持自动审方: ${rx.status}`);
}
if (!rx.pharmacistNo) {
throw new AutomationError("处方未分配审方药师", { retryable: true });
}
const pharmacist = await db.collection("hlw-doctor").findOne({
corpId: task.corpId,
doctorNo: rx.pharmacistNo,
job: "pharmacist",
});
if (!pharmacist) {
throw new AutomationError("审方药师不存在");
}
if (pharmacist.onlineStatus !== "online") {
throw new AutomationError("审方药师暂不在线", { retryable: true });
}
const diagnosticRecord = require("../diagnostic-record");
const result = await diagnosticRecord(
{
type: "auditDiagnosis",
ids: [task.rxId],
status: "PASS",
pharmacistNo: rx.pharmacistNo,
corpId: task.corpId,
operationSource: "AUTO",
},
db
);
if (!result || !result.success) {
const detail =
Array.isArray(result?.failList) && result.failList[0]
? result.failList[0].message
: result?.message;
throw new AutomationError(detail || "自动审方失败", { retryable: true });
}
return { orderId: task.orderId, rxId: task.rxId };
}
async function executeTask(db, task) {
const config = await assertTaskEnabled(db, task);
if (!config) return;
let result;
if (task.type === AUTOMATION_TYPES.ACCEPT) {
result = await handleAutoAccept(db, task);
} else if (task.type === AUTOMATION_TYPES.OPEN_RX) {
result = await handleAutoOpenRx(db, task);
} else if (task.type === AUTOMATION_TYPES.PASS_RX) {
result = await handleAutoPassRx(db, task);
} else {
throw new AutomationError(`未知自动化任务类型: ${task.type}`);
}
await markTask(db, task, TASK_STATUS.SUCCEEDED, {
lastError: "",
result: result || {},
});
}
async function processClaimedTask(db, task) {
const heartbeat = setInterval(
() =>
renewLease(db, task).catch((error) => {
console.error("[自动化任务] 续租失败:", error.message);
}),
Math.floor(LEASE_MS / 3)
);
if (typeof heartbeat.unref === "function") heartbeat.unref();
try {
await executeTask(db, task);
} catch (error) {
const automationError =
error instanceof AutomationError
? error
: new AutomationError(error.message || "自动化任务执行失败", {
retryable: true,
});
if (automationError.retryable) {
await retryTask(db, task, automationError);
} else {
await markTask(db, task, TASK_STATUS.FAILED, {
lastError: automationError.message,
});
}
} finally {
clearInterval(heartbeat);
}
}
async function runDueTasks(db = workerDb) {
if (!db || pollIsRunning) return;
pollIsRunning = true;
try {
const tasks = [];
for (let i = 0; i < MAX_TASKS_PER_TICK; i += 1) {
const task = await claimNextTask(db);
if (!task) break;
tasks.push(task);
}
await Promise.all(tasks.map((task) => processClaimedTask(db, task)));
} catch (error) {
console.error("[自动化任务] 执行器异常:", error.message);
} finally {
pollIsRunning = false;
}
}
async function reconcileAutomationTasks(db = workerDb) {
if (!db || reconcileIsRunning) return;
reconcileIsRunning = true;
try {
const now = Date.now();
const todayStart = new Date();
todayStart.setHours(0, 0, 0, 0);
const orders = await db
.collection("consult-order")
.find(
{
orderSource: "ALIPAY_MINI",
orderStatus: { $in: ["pending", "processing"] },
$or: [
{ expireTime: { $gt: now } },
{
expireTime: { $exists: false },
createTime: { $gte: todayStart.getTime() },
},
],
},
{
projection: {
orderId: 1,
corpId: 1,
orderStatus: 1,
createTime: 1,
prescriptionStartTime: 1,
expireTime: 1,
},
}
)
.toArray();
const corpIds = [...new Set(orders.map((item) => item.corpId).filter(Boolean))];
const configs = await db
.collection("hlw-config")
.find({ corpId: { $in: corpIds } })
.toArray();
const configMap = new Map(
configs.map((item) => [item.corpId, normalizeAutomationConfig(item)])
);
for (const order of orders) {
const config = configMap.get(order.corpId) || normalizeAutomationConfig({});
if (order.orderStatus === "pending" && config.autoAcceptOrder) {
await scheduleAutoAccept({
db,
orderId: order.orderId,
corpId: order.corpId,
baseTime: order.createTime,
expiresAt: order.expireTime,
});
}
if (order.orderStatus === "processing" && config.autoOpenRx) {
const record = await db.collection("diagnostic-record").findOne(
{ orderId: order.orderId, status: { $in: ["INIT", "PASS"] } },
{ projection: { _id: 1 } }
);
if (!record) {
await scheduleAutoOpenRx({
db,
orderId: order.orderId,
corpId: order.corpId,
baseTime: order.prescriptionStartTime || order.createTime,
expiresAt: order.expireTime,
});
}
}
}
const pendingRxList = await db
.collection("diagnostic-record")
.find(
{
status: "INIT",
orderSource: "ALIPAY_MINI",
$or: [
{ expireTime: { $gt: now } },
{
expireTime: { $exists: false },
createTime: { $gte: todayStart.getTime() },
},
],
},
{
projection: {
_id: 1,
orderId: 1,
corpId: 1,
createTime: 1,
expireTime: 1,
},
}
)
.toArray();
for (const rx of pendingRxList) {
const config =
configMap.get(rx.corpId) ||
(await getConfig(db, rx.corpId));
if (config.autoPassRx) {
const order = orders.find((item) => item.orderId === rx.orderId);
await scheduleAutoPassRx({
db,
orderId: rx.orderId,
rxId: rx._id,
corpId: rx.corpId,
baseTime: rx.createTime,
expiresAt: rx.expireTime || getBusinessExpiry(order),
});
}
}
} catch (error) {
console.error("[自动化任务] 核对任务失败:", error.message);
} finally {
reconcileIsRunning = false;
}
}
async function start(db) {
if (pollTimer || reconcileTimer) return;
workerDb = db;
await ensureIndexes(db);
await reconcileAutomationTasks(db);
await runDueTasks(db);
pollTimer = setInterval(() => runDueTasks(db), POLL_INTERVAL_MS);
reconcileTimer = setInterval(
() => reconcileAutomationTasks(db),
RECONCILE_INTERVAL_MS
);
if (typeof pollTimer.unref === "function") pollTimer.unref();
if (typeof reconcileTimer.unref === "function") reconcileTimer.unref();
console.log("[自动化任务] 执行器已启动");
}
function stop() {
if (pollTimer) clearInterval(pollTimer);
if (reconcileTimer) clearInterval(reconcileTimer);
pollTimer = null;
reconcileTimer = null;
workerDb = null;
pollIsRunning = false;
reconcileIsRunning = false;
}
module.exports = {
COLLECTION,
TASK_STATUS,
AUTOMATION_TYPES,
AutomationError,
calculateDueAt,
enqueueTask,
scheduleAutoAccept,
scheduleAutoOpenRx,
scheduleAutoPassRx,
buildAutoPrescriptionParams,
claimNextTask,
processClaimedTask,
reconcileAutomationTasks,
runDueTasks,
start,
stop,
};

View File

@ -0,0 +1,451 @@
const { ObjectId } = require("mongodb");
const {
AUTOMATION_TYPES,
COLLECTION,
TASK_STATUS,
buildAutoPrescriptionParams,
calculateDueAt,
claimNextTask,
enqueueTask,
processClaimedTask,
reconcileAutomationTasks,
} = require("./index");
function createCollectionDb(collections) {
return {
collection: jest.fn((name) => {
if (!collections[name]) {
throw new Error(`unexpected collection: ${name}`);
}
return collections[name];
}),
};
}
describe("automation task scheduling", () => {
afterEach(() => {
jest.restoreAllMocks();
});
test("uses the configured delay and respects firstSubmitRxIntervel", () => {
jest.spyOn(Date, "now").mockReturnValue(1_000_000);
const config = {
autoOpenRx: true,
autoOpenRxDelayMinSeconds: 20,
autoOpenRxDelayMaxSeconds: 40,
firstSubmitRxIntervel: 60,
};
expect(
calculateDueAt(AUTOMATION_TYPES.OPEN_RX, config, 1_000_000, () => 0)
).toBe(1_060_000);
expect(
calculateDueAt(
AUTOMATION_TYPES.OPEN_RX,
{ ...config, firstSubmitRxIntervel: 10 },
1_000_000,
() => 0.999999
)
).toBe(1_040_000);
});
test("creates a task idempotently through a unique task key", async () => {
const updateOne = jest
.fn()
.mockResolvedValueOnce({ modifiedCount: 0 })
.mockResolvedValueOnce({ upsertedCount: 1 });
const db = createCollectionDb({
"hlw-config": {
findOne: jest.fn().mockResolvedValue({
autoAcceptOrder: true,
autoAcceptDelayMinSeconds: 2,
autoAcceptDelayMaxSeconds: 5,
}),
},
[COLLECTION]: { updateOne },
});
const result = await enqueueTask(db, {
type: AUTOMATION_TYPES.ACCEPT,
corpId: "corp-1",
orderId: "order-1",
baseTime: Date.now(),
expiresAt: Date.now() + 60_000,
});
expect(result).toMatchObject({
success: true,
created: true,
taskKey: "AUTO_ACCEPT:order-1",
});
expect(updateOne).toHaveBeenLastCalledWith(
{ taskKey: "AUTO_ACCEPT:order-1" },
expect.objectContaining({
$setOnInsert: expect.objectContaining({
status: TASK_STATUS.PENDING,
attempts: 0,
}),
}),
{ upsert: true }
);
});
test("reactivates a task cancelled by a previously disabled switch", async () => {
const updateOne = jest.fn().mockResolvedValue({ modifiedCount: 1 });
const db = createCollectionDb({
"hlw-config": {
findOne: jest.fn().mockResolvedValue({ autoAcceptOrder: true }),
},
[COLLECTION]: { updateOne },
});
const result = await enqueueTask(db, {
type: AUTOMATION_TYPES.ACCEPT,
corpId: "corp-1",
orderId: "order-1",
});
expect(result).toMatchObject({
success: true,
created: true,
reactivated: true,
});
expect(updateOne).toHaveBeenCalledTimes(1);
expect(updateOne).toHaveBeenCalledWith(
{
taskKey: "AUTO_ACCEPT:order-1",
status: TASK_STATUS.CANCELLED,
},
expect.objectContaining({
$set: expect.objectContaining({ status: TASK_STATUS.PENDING }),
$unset: { completedAt: "", result: "" },
})
);
});
test("treats a concurrent unique-key insert as idempotent success", async () => {
const duplicateError = Object.assign(new Error("duplicate key"), {
code: 11000,
});
const updateOne = jest
.fn()
.mockResolvedValueOnce({ modifiedCount: 0 })
.mockRejectedValueOnce(duplicateError);
const db = createCollectionDb({
"hlw-config": {
findOne: jest.fn().mockResolvedValue({ autoPassRx: true }),
},
[COLLECTION]: { updateOne },
});
await expect(
enqueueTask(db, {
type: AUTOMATION_TYPES.PASS_RX,
corpId: "corp-1",
orderId: "order-1",
rxId: new ObjectId().toString(),
})
).resolves.toMatchObject({
success: true,
created: false,
});
});
test("claims due and expired-lease tasks with one atomic update", async () => {
const claimed = {
_id: new ObjectId(),
taskKey: "AUTO_ACCEPT:order-1",
status: TASK_STATUS.RUNNING,
};
const findOneAndUpdate = jest.fn().mockResolvedValue(claimed);
const db = createCollectionDb({
[COLLECTION]: { findOneAndUpdate },
});
await expect(claimNextTask(db, 10_000)).resolves.toBe(claimed);
expect(findOneAndUpdate).toHaveBeenCalledWith(
{
$or: [
{
status: TASK_STATUS.PENDING,
nextRunAt: { $lte: 10_000 },
},
{
status: TASK_STATUS.RUNNING,
leaseUntil: { $lte: 10_000 },
},
],
},
expect.objectContaining({
$set: expect.objectContaining({
status: TASK_STATUS.RUNNING,
leaseOwner: expect.any(String),
}),
$inc: { attempts: 1 },
}),
{
sort: { nextRunAt: 1, createdAt: 1 },
returnDocument: "after",
}
);
});
test("cancels a claimed task when its switch has been turned off", async () => {
const updateOne = jest.fn().mockResolvedValue({ modifiedCount: 1 });
const db = createCollectionDb({
"hlw-config": {
findOne: jest.fn().mockResolvedValue({ autoAcceptOrder: false }),
},
[COLLECTION]: { updateOne },
});
const task = {
_id: new ObjectId(),
type: AUTOMATION_TYPES.ACCEPT,
corpId: "corp-1",
orderId: "order-1",
attempts: 1,
expiresAt: Date.now() + 60_000,
};
await processClaimedTask(db, task);
expect(updateOne).toHaveBeenCalledWith(
expect.objectContaining({
_id: task._id,
status: TASK_STATUS.RUNNING,
leaseOwner: expect.any(String),
}),
expect.objectContaining({
$set: expect.objectContaining({
status: TASK_STATUS.CANCELLED,
completedAt: expect.any(Number),
}),
})
);
});
test("reconciliation recreates a missing task after service restart", async () => {
jest.spyOn(Date, "now").mockReturnValue(2_000_000);
const taskUpdateOne = jest
.fn()
.mockResolvedValueOnce({ modifiedCount: 0 })
.mockResolvedValueOnce({ upsertedCount: 1 });
const order = {
orderId: "order-recovery",
corpId: "corp-1",
orderSource: "ALIPAY_MINI",
orderStatus: "pending",
createTime: 1_990_000,
expireTime: 2_060_000,
};
const db = createCollectionDb({
"consult-order": {
find: jest.fn().mockReturnValue({
toArray: jest.fn().mockResolvedValue([order]),
}),
findOne: jest.fn().mockResolvedValue({ _id: new ObjectId() }),
},
"hlw-config": {
find: jest.fn().mockReturnValue({
toArray: jest
.fn()
.mockResolvedValue([{ corpId: "corp-1", autoAcceptOrder: true }]),
}),
findOne: jest
.fn()
.mockResolvedValue({ corpId: "corp-1", autoAcceptOrder: true }),
},
"diagnostic-record": {
find: jest.fn().mockReturnValue({
toArray: jest.fn().mockResolvedValue([]),
}),
},
[COLLECTION]: { updateOne: taskUpdateOne },
});
await reconcileAutomationTasks(db);
expect(taskUpdateOne).toHaveBeenLastCalledWith(
{ taskKey: "AUTO_ACCEPT:order-recovery" },
expect.objectContaining({
$setOnInsert: expect.objectContaining({
orderId: "order-recovery",
status: TASK_STATUS.PENDING,
}),
}),
{ upsert: true }
);
});
test("retries automatic audit while the assigned pharmacist is offline", async () => {
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-1",
status: "INIT",
pharmacistNo: "P001",
}),
},
"hlw-doctor": {
findOne: jest.fn().mockResolvedValue({
doctorNo: "P001",
onlineStatus: "offline",
}),
},
[COLLECTION]: { updateOne: taskUpdateOne },
});
const task = {
_id: new ObjectId(),
type: AUTOMATION_TYPES.PASS_RX,
corpId: "corp-1",
orderId: "order-1",
rxId: rxId.toString(),
attempts: 1,
expiresAt: Date.now() + 60_000,
};
await processClaimedTask(db, task);
expect(taskUpdateOne).toHaveBeenCalledWith(
expect.objectContaining({
_id: task._id,
status: TASK_STATUS.RUNNING,
}),
expect.objectContaining({
$set: expect.objectContaining({
status: TASK_STATUS.PENDING,
nextRunAt: expect.any(Number),
lastError: "审方药师暂不在线",
}),
})
);
});
});
describe("automatic prescription assembly", () => {
test("reloads master data and produces the existing prescription contract", async () => {
const drugId = new ObjectId();
const findConfig = jest.fn((query) => {
if (query.group) {
return {
toArray: jest.fn().mockResolvedValue([
{
key: "store-medicine-dosage-unit",
list: [{ code: "MG", name: "mg" }],
},
{
key: "store-medicine-frequence",
list: [{ code: "BID", name: "每日两次" }],
},
{
key: "store-medicine-administration",
list: [{ code: "PO", name: "口服" }],
},
{
key: "medicine-package-unit",
list: [{ code: "盒", name: "盒" }],
},
]),
};
}
throw new Error("unexpected config find");
});
const db = createCollectionDb({
"hlw-doctor": {
findOne: jest.fn().mockResolvedValue({
doctorNo: "D001",
signatureUrl: "doctor-sign",
onlineStatus: "online",
}),
},
"hlw-diagnosis": {
find: jest.fn().mockReturnValue({
toArray: jest
.fn()
.mockResolvedValue([{ code: "J00", name: "普通感冒" }]),
}),
},
"drug-info": {
find: jest.fn().mockReturnValue({
toArray: jest.fn().mockResolvedValue([
{
_id: drugId,
onSale: true,
name: "测试药品",
erpId: "ERP-1",
insurance_code: "MED-1",
product_id: "P-1",
dosage_form: "片剂",
days: 3,
dosage_unit: "mg",
administration_method: "口服",
freq: "每日两次",
unit: "盒",
specification: "10mg*10片",
package_amount: 10,
recommended_quantity: 1,
},
]),
}),
},
"hlw-config": {
find: findConfig,
findOne: jest
.fn()
.mockResolvedValue({ medicinePurchaseRxDuration: 30 }),
},
});
const order = {
corpId: "corp-1",
orderId: "order-1",
orderSource: "ALIPAY_MINI",
consultType: "storeMedicinePurchase",
doctorCode: "D001",
doctorName: "医生甲",
patientId: "P001",
name: "患者甲",
diseases: ["普通感冒"],
description: "鼻塞流涕",
pastHistoryStr: "两天前起病",
drugs: [
{
_id: drugId.toString(),
dosage: 10,
dosage_unit: "mg",
quantity: 1,
usageName: "口服",
frequencyName: "每日两次",
unit: "盒",
},
],
expireTime: Date.now() + 60_000,
};
const params = await buildAutoPrescriptionParams(db, order);
expect(params).toMatchObject({
complaint: "普通感冒 鼻塞流涕",
presentIllness: "两天前起病",
doctorCAUserId: "doctor-sign",
prescriptionType: "storeMedicinePurchase",
diagnosisList: [{ code: "J00", name: "普通感冒", desc: "" }],
drugs: [
expect.objectContaining({
_id: drugId.toString(),
erpId: "ERP-1",
dosage_unit_code: "MG",
frequencyCode: "BID",
usageCode: "PO",
quantity: 1,
days: 3,
}),
],
});
});
});

View File

@ -84,6 +84,7 @@ interface Order {
chargeFee: number;// 费用金额 chargeFee: number;// 费用金额
updateTime: number;// 更新时间 updateTime: number;// 更新时间
expireTime: number;// 过期时间 expireTime: number;// 过期时间
acceptSource?: 'AUTO' | 'MANUAL';// 接诊操作来源
signature: string;// 签名图片路径 (ipad发起的咨询需要用户手动签名) signature: string;// 签名图片路径 (ipad发起的咨询需要用户手动签名)
wines?: Wine[];// 浸酒订单 wines?: Wine[];// 浸酒订单
} }

View File

@ -97,6 +97,42 @@ module.exports = async (item, mongodb) => {
}; };
let scheduleTaskIsRunning = false; let scheduleTaskIsRunning = false;
async function scheduleAutoAcceptAfterCreate({ orderId, corpId, createTime, expireTime }) {
try {
const automation = require("../automation");
await automation.scheduleAutoAccept({
db,
orderId,
corpId,
baseTime: createTime,
expiresAt: expireTime,
});
} catch (error) {
console.error("[自动化任务] 创建自动接诊任务失败:", error.message);
}
}
async function scheduleAutoOpenAfterAccept({
orderId,
corpId,
prescriptionStartTime,
expireTime,
}) {
try {
const automation = require("../automation");
await automation.scheduleAutoOpenRx({
db,
orderId,
corpId,
baseTime: prescriptionStartTime || Date.now(),
expiresAt: expireTime,
});
} catch (error) {
console.error("[自动化任务] 创建自动开方任务失败:", error.message);
}
}
// 咨询订单库 数据库是 consult-order // 咨询订单库 数据库是 consult-order
async function addConsultOrder(item) { async function addConsultOrder(item) {
let { params, corpId } = item; let { params, corpId } = item;
@ -215,14 +251,16 @@ async function addOnlineConsultOrder(params) {
.collection("consult-order") .collection("consult-order")
.insertOne(encryptedParams); .insertOne(encryptedParams);
if (insertedId) { if (insertedId) {
let startResult = null;
if (params.payStatus === 'success') { if (params.payStatus === 'success') {
await startConsultOrder(params); startResult = await startConsultOrder(params);
}
const res1 = await getConfig(params.corpId);
const { autoAcceptOrder } = res1 || {};
if (autoAcceptOrder) {
await acceptConsultOrder(params);
} }
await scheduleAutoAcceptAfterCreate({
orderId: params.orderId,
corpId: params.corpId,
createTime: params.createTime,
expireTime: startResult && startResult.expireTime,
});
return { return {
success: true, success: true,
message: "新增成功", message: "新增成功",
@ -641,9 +679,10 @@ async function getDrugStoreOrderList(ctx) {
async function acceptConsultOrder(item) { async function acceptConsultOrder(item) {
const { orderId, corpId, doctorCode } = item; const { orderId, corpId, doctorCode } = item;
const operationSource = item.operationSource === "AUTO" ? "AUTO" : "MANUAL";
try { try {
const item = await db.collection("consult-order").findOne( const item = await db.collection("consult-order").findOne(
{ orderId }, { orderId, corpId },
{ {
projection: { projection: {
expireTime: 1, expireTime: 1,
@ -651,6 +690,7 @@ async function acceptConsultOrder(item) {
doctorCode: 1, doctorCode: 1,
orderStatus: 1, orderStatus: 1,
doctorName: 1, doctorName: 1,
prescriptionStartTime: 1,
}, },
} }
); );
@ -664,6 +704,12 @@ async function acceptConsultOrder(item) {
return { success: false, message: "订单已过期,请刷新" }; return { success: false, message: "订单已过期,请刷新" };
} }
if (item.orderStatus === 'processing') { if (item.orderStatus === 'processing') {
await scheduleAutoOpenAfterAccept({
orderId,
corpId,
prescriptionStartTime: item.prescriptionStartTime,
expireTime: item.expireTime,
});
return { success: true, message: "订单已处理" }; return { success: true, message: "订单已处理" };
} }
if (item.orderStatus !== "pending") { if (item.orderStatus !== "pending") {
@ -671,13 +717,41 @@ async function acceptConsultOrder(item) {
} }
const { consultationDuration } = await getConfig(corpId); const { consultationDuration } = await getConfig(corpId);
const expireTime = dayjs().add(consultationDuration, "minute").valueOf(); const expireTime = dayjs().add(consultationDuration, "minute").valueOf();
await updateConsultOrderStatus({ const serverPrescriptionStartTime = Date.now();
orderId, const acceptResult = await db.collection("consult-order").updateOne(
orderStatus: "processing", { orderId, corpId, orderStatus: "pending" },
expireTime, {
doctorCode: item.doctorCode, $set: {
corpId, orderStatus: "processing",
}); expireTime,
prescriptionStartTime: serverPrescriptionStartTime,
acceptSource: operationSource,
updateTime: serverPrescriptionStartTime,
},
}
);
if (acceptResult.modifiedCount !== 1) {
const current = await db.collection("consult-order").findOne(
{ orderId, corpId },
{
projection: {
orderStatus: 1,
prescriptionStartTime: 1,
expireTime: 1,
},
}
);
if (current && current.orderStatus === "processing") {
await scheduleAutoOpenAfterAccept({
orderId,
corpId,
prescriptionStartTime: current.prescriptionStartTime,
expireTime: current.expireTime,
});
return { success: true, message: "订单已处理" };
}
return { success: false, message: "订单已失效,请刷新" };
}
timeApi( timeApi(
{ {
type: "addDoctorOrderDuration", type: "addDoctorOrderDuration",
@ -688,26 +762,6 @@ async function acceptConsultOrder(item) {
}, },
db db
); );
// 后端自动生成并保存处方开始时间(毫秒),由服务器记录接单时间点
let serverPrescriptionStartTime = null;
try {
serverPrescriptionStartTime = Date.now();
await db.collection("consult-order").updateOne(
{ orderId },
{
$set: {
prescriptionStartTime: serverPrescriptionStartTime,
updateTime: Date.now(),
},
}
);
} catch (e) {
console.error(
"保存 prescriptionStartTime 到 consult-order 失败",
e && e.message
);
// 保持 serverPrescriptionStartTime 的值(可能为 null 如果 Date.now() 未赋值),以便返回给调用者以作判断
}
// 接受问诊 发送系统消息 // 接受问诊 发送系统消息
await tencentIM({ await tencentIM({
@ -740,6 +794,12 @@ async function acceptConsultOrder(item) {
endTime: expireTime, endTime: expireTime,
corpId, corpId,
}); });
await scheduleAutoOpenAfterAccept({
orderId,
corpId,
prescriptionStartTime: serverPrescriptionStartTime,
expireTime,
});
return { return {
success: true, success: true,
message: "接诊成功", message: "接诊成功",

View File

@ -107,6 +107,8 @@ interface DiagnosticRecord {
prescriptionStartTime: number; // 处方开始时间 prescriptionStartTime: number; // 处方开始时间
prescriptionEndTime: number; // 处方结束时间 prescriptionEndTime: number; // 处方结束时间
auditTime?: number; // 审核时间 auditTime?: number; // 审核时间
prescriptionSource?: 'AUTO' | 'MANUAL'; // 开方操作来源
auditSource?: 'AUTO' | 'MANUAL'; // 审方操作来源
reasons?: string[]; // 拒绝原因列表 reasons?: string[]; // 拒绝原因列表
expireTime?: number; // 过期时间 expireTime?: number; // 过期时间

View File

@ -145,8 +145,32 @@ module.exports = async (item, mongodb, user) => {
let scheduleTaskIsRunning = false; let scheduleTaskIsRunning = false;
async function scheduleAutoPassAfterPrescription({
rxId,
orderId,
corpId,
createTime,
expireTime,
}) {
try {
const automation = require("../automation");
await automation.scheduleAutoPassRx({
db,
rxId,
orderId,
corpId,
baseTime: createTime || Date.now(),
expiresAt: expireTime,
});
} catch (error) {
console.error("[自动化任务] 创建自动审方任务失败:", error.message);
}
}
async function addConsultDiagnosis(item) { async function addConsultDiagnosis(item) {
let { params = {} } = item; let { params = {} } = item;
const automated = item.automated === true;
const operationSource = item.operationSource === "AUTO" ? "AUTO" : "MANUAL";
const corpId = typeof item.corpId === "string" ? item.corpId.trim() : ''; const corpId = typeof item.corpId === "string" ? item.corpId.trim() : '';
const { orderId, doctorCode } = params; const { orderId, doctorCode } = params;
if ( if (
@ -170,6 +194,7 @@ async function addConsultDiagnosis(item) {
medorg_order_no: 1, medorg_order_no: 1,
accountId: 1, accountId: 1,
prescriptionStartTime: 1, prescriptionStartTime: 1,
expireTime: 1,
areaCode: 1, areaCode: 1,
pickUpType: 1, pickUpType: 1,
medInfo: 1, medInfo: 1,
@ -186,7 +211,11 @@ async function addConsultDiagnosis(item) {
if (order.orderStatus !== "processing") { if (order.orderStatus !== "processing") {
return { success: false, message: "当前订单状态不支持开方" }; return { success: false, message: "当前订单状态不支持开方" };
} }
if (!(order.createTime > dayjs().startOf("day").valueOf())) { const orderExpired =
Number.isFinite(order.expireTime)
? order.expireTime <= Date.now()
: !(order.createTime > dayjs().startOf("day").valueOf());
if (orderExpired) {
return { success: false, message: "当前订单不在有效期内" }; return { success: false, message: "当前订单不在有效期内" };
} }
timeApi({ type: "addDoctorRxDuration", orderId }, db); timeApi({ type: "addDoctorRxDuration", orderId }, db);
@ -195,12 +224,14 @@ async function addConsultDiagnosis(item) {
return { success: false, message: "医生不存在" }; return { success: false, message: "医生不存在" };
} }
// 判断是否医生发的消息是否大于2条 // 判断是否医生发的消息是否大于2条
const chatCount = await db.collection("im-chat-msg").countDocuments({ const chatCount = automated
From_Account: doctorCode, ? 3
To_Account: orderId, : await db.collection("im-chat-msg").countDocuments({
}); From_Account: doctorCode,
To_Account: orderId,
});
timeData.chatCountQueryTime = dayjs().format('YYYY-MM-DD HH:mm:ss'); timeData.chatCountQueryTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
if (chatCount < 3) { if (!automated && chatCount < 3) {
return { return {
success: false, success: false,
message: "请先向患者发送2条提问消息后再开方", message: "请先向患者发送2条提问消息后再开方",
@ -210,9 +241,31 @@ async function addConsultDiagnosis(item) {
.collection("diagnostic-record") .collection("diagnostic-record")
.findOne( .findOne(
{ orderId, doctorCode }, { orderId, doctorCode },
{ projection: { _id: 1, status: 1, pharmacistNo: 1 } } {
projection: {
_id: 1,
status: 1,
pharmacistNo: 1,
createTime: 1,
expireTime: 1,
},
}
); );
timeData.recordQueryTime = dayjs().format('YYYY-MM-DD HH:mm:ss'); timeData.recordQueryTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
if (automated && record && [status.init, status.pass].includes(record.status)) {
await scheduleAutoPassAfterPrescription({
rxId: record._id,
orderId,
corpId,
createTime: record.createTime,
expireTime: record.expireTime || order.expireTime,
});
return {
success: true,
message: "处方已存在",
data: record._id,
};
}
if (record && record.status === status.init) { if (record && record.status === status.init) {
return { success: false, message: "该订单已存在诊断信息,请勿重复添加" }; return { success: false, message: "该订单已存在诊断信息,请勿重复添加" };
} }
@ -222,14 +275,25 @@ async function addConsultDiagnosis(item) {
} }
if (record && record.status === status.unpass) { if (record && record.status === status.unpass) {
return await rewriteConsultDiagnosis( const rewriteResult = await rewriteConsultDiagnosis(
params, params,
record._id, record._id,
corpId, corpId,
record.pharmacistNo, record.pharmacistNo,
timeData, timeData,
order order,
operationSource
); );
if (rewriteResult && rewriteResult.success) {
await scheduleAutoPassAfterPrescription({
rxId: record._id,
orderId,
corpId,
createTime: Date.now(),
expireTime: order.expireTime,
});
}
return rewriteResult;
} }
// 获取推荐药师 随机获取药师 // 获取推荐药师 随机获取药师
const { const {
@ -259,6 +323,7 @@ async function addConsultDiagnosis(item) {
dise_name: order.medInfo.dise_name, dise_name: order.medInfo.dise_name,
feeType: order.feeType, feeType: order.feeType,
corpId, corpId,
prescriptionSource: operationSource,
}; };
// 由后端统一设置处方时间: // 由后端统一设置处方时间:
// - 开始时间从 consult-order 读取(接单时已由后端写入) // - 开始时间从 consult-order 读取(接单时已由后端写入)
@ -314,6 +379,13 @@ async function addConsultDiagnosis(item) {
orderStatus: "completed", orderStatus: "completed",
msgType: "MEDICALADVICE", msgType: "MEDICALADVICE",
}, timeData); }, timeData);
await scheduleAutoPassAfterPrescription({
rxId: insertedId,
orderId,
corpId,
createTime: data.createTime,
expireTime: data.expireTime || order.expireTime,
});
timeData.duration = dayjs().diff(timeData.startTime, 's'); timeData.duration = dayjs().diff(timeData.startTime, 's');
if (timeData.duration > 3) { if (timeData.duration > 3) {
db.collection("perf-observation").insertOne(timeData); db.collection("perf-observation").insertOne(timeData);
@ -331,7 +403,15 @@ async function addConsultDiagnosis(item) {
} }
} }
// 未通过的处方 重新提交 原药师在线的话 // 未通过的处方 重新提交 原药师在线的话
async function rewriteConsultDiagnosis(item, _id, corpId, pharmacistNo, timeData, order) { async function rewriteConsultDiagnosis(
item,
_id,
corpId,
pharmacistNo,
timeData,
order,
operationSource = "MANUAL"
) {
const { const {
complaint, complaint,
presentIllness, presentIllness,
@ -352,6 +432,7 @@ async function rewriteConsultDiagnosis(item, _id, corpId, pharmacistNo, timeData
wines: item.wines, wines: item.wines,
updateTime: Date.now(), updateTime: Date.now(),
status: status.init, status: status.init,
prescriptionSource: operationSource,
}; };
const doctorType = order.consultType === 'wineConsult' ? 'chinese' : 'west' const doctorType = order.consultType === 'wineConsult' ? 'chinese' : 'west'
const { const {
@ -918,6 +999,7 @@ async function auditDiagnosis(item, db) {
} }
const pharmacist = yaoshi.doctorName; const pharmacist = yaoshi.doctorName;
const pharmacistCAUserId = yaoshi.signatureUrl; const pharmacistCAUserId = yaoshi.signatureUrl;
const auditSource = item.operationSource === "AUTO" ? "AUTO" : "MANUAL";
const reasons = Array.isArray(item.reasons) const reasons = Array.isArray(item.reasons)
? item.reasons.filter((i) => typeof i === "string" && i.trim() !== "") ? item.reasons.filter((i) => typeof i === "string" && i.trim() !== "")
: []; : [];
@ -979,13 +1061,17 @@ async function auditDiagnosis(item, db) {
}) })
const { modifiedCount } = await db.collection("diagnostic-record").updateMany( const { modifiedCount } = await db.collection("diagnostic-record").updateMany(
{ _id: { $in: successRxList.map(i => i.rx._id) } }, {
_id: { $in: successRxList.map(i => i.rx._id) },
status: status.init,
},
{ {
$set: { $set: {
uploadStatus: "uploaded", uploadStatus: "uploaded",
status: status.pass, status: status.pass,
auditTime: Date.now(), auditTime: Date.now(),
pharmacistSignImg: pharmacistCAUserId pharmacistSignImg: pharmacistCAUserId,
auditSource
} }
} }
) )
@ -1003,7 +1089,8 @@ async function auditDiagnosis(item, db) {
$set: { $set: {
status: item.status, status: item.status,
auditTime: Date.now(), auditTime: Date.now(),
reasons reasons,
auditSource
} }
} }
) )
@ -1029,7 +1116,14 @@ async function auditDiagnosis(item, db) {
if (expireList.length) { if (expireList.length) {
const { modifiedCount } = await db.collection("diagnostic-record").updateMany( const { modifiedCount } = await db.collection("diagnostic-record").updateMany(
{ _id: { $in: expireList.map(i => i.rx._id) }, status: status.init }, { _id: { $in: expireList.map(i => i.rx._id) }, status: status.init },
{ $set: { status: status.expired }, updateTime: Date.now() } {
$set: {
status: status.expired,
auditTime: Date.now(),
auditSource,
updateTime: Date.now(),
},
}
) )
statsCount.expireCount += modifiedCount; statsCount.expireCount += modifiedCount;
} }

View File

@ -14,7 +14,8 @@ exports.output = {
"diagnosisList.name": 1, "diagnosisList.name": 1,
"drugs.drugName": 1, "drugs.drugName": 1,
expireTime: 1, expireTime: 1,
status: 1 status: 1,
feeType: 1
}, },
medicinePurchaseOrder: { medicinePurchaseOrder: {
_id: 1, _id: 1,

View File

@ -1,4 +1,9 @@
const validator = require('../../utils/validator.js') const validator = require('../../utils/validator.js')
const {
AUTOMATION_DELAY_KEYS,
DEFAULT_AUTOMATION_CONFIG,
validateAutomationDelayConfig,
} = require("../automation/config");
let db = ""; let db = "";
module.exports = async (item, mongodb) => { module.exports = async (item, mongodb) => {
@ -34,13 +39,26 @@ async function getHlwOrderConfig({ corpId }) {
fengNiaoExpressDiscountFee: 1, fengNiaoExpressDiscountFee: 1,
fengNiaoExpressFee: 1, fengNiaoExpressFee: 1,
firstSubmitRxIntervel: 1, firstSubmitRxIntervel: 1,
submitRxInterval: 1 submitRxInterval: 1,
autoAcceptOrder: 1,
autoOpenRx: 1,
autoPassRx: 1,
autoAcceptDelayMinSeconds: 1,
autoAcceptDelayMaxSeconds: 1,
autoOpenRxDelayMinSeconds: 1,
autoOpenRxDelayMaxSeconds: 1,
autoPassRxDelayMinSeconds: 1,
autoPassRxDelayMaxSeconds: 1
} }
}); });
if (!res) { if (!res) {
return { success: false, message: "未找到配置" }; return { success: false, message: "未找到配置" };
} }
return { success: true, data: res, message: "获取配置成功" }; return {
success: true,
data: { ...DEFAULT_AUTOMATION_CONFIG, ...res },
message: "获取配置成功",
};
} catch (e) { } catch (e) {
return { success: false, message: e.message } return { success: false, message: e.message }
} }
@ -48,7 +66,20 @@ async function getHlwOrderConfig({ corpId }) {
async function updateHlwOrderConfig(ctx) { async function updateHlwOrderConfig(ctx) {
try { try {
const { payDuration, acceptDuration, consultationDuration, medicinePurchaseRxDuration, emsExpressDiscountFee, emsExpressFee, fengNiaoExpressDiscountFee, fengNiaoExpressFee } = ctx; const {
payDuration,
acceptDuration,
consultationDuration,
medicinePurchaseRxDuration,
emsExpressDiscountFee,
emsExpressFee,
fengNiaoExpressDiscountFee,
fengNiaoExpressFee,
presOpen,
autoAcceptOrder,
autoOpenRx,
autoPassRx,
} = ctx;
const data = {} const data = {}
if (typeof payDuration == 'number') { if (typeof payDuration == 'number') {
const [valid, message] = validator.verifyNumber(payDuration, "支付时长", 0, 5, 60, true, true); const [valid, message] = validator.verifyNumber(payDuration, "支付时长", 0, 5, 60, true, true);
@ -81,6 +112,31 @@ async function updateHlwOrderConfig(ctx) {
if (typeof presOpen == 'boolean') { if (typeof presOpen == 'boolean') {
data.presOpen = presOpen; data.presOpen = presOpen;
} }
if (typeof autoAcceptOrder === 'boolean') {
data.autoAcceptOrder = autoAcceptOrder;
}
if (typeof autoOpenRx === 'boolean') {
data.autoOpenRx = autoOpenRx;
}
if (typeof autoPassRx === 'boolean') {
data.autoPassRx = autoPassRx;
}
if (AUTOMATION_DELAY_KEYS.some((key) => key in ctx)) {
const current = await db.collection("hlw-config").findOne(
{ corpId: ctx.corpId },
{
projection: Object.fromEntries(
AUTOMATION_DELAY_KEYS.map((key) => [key, 1])
),
}
);
const validation = validateAutomationDelayConfig(ctx, current || {});
if (!validation.success) {
return validation;
}
Object.assign(data, validation.data);
}
if (typeof emsExpressDiscountFee == 'number' || typeof emsExpressFee == 'number') { if (typeof emsExpressDiscountFee == 'number' || typeof emsExpressFee == 'number') {
const [valid, message] = validator.verifyNumber(emsExpressDiscountFee, "邮政快递优惠费用", 2, 0, 9999, true, true); const [valid, message] = validator.verifyNumber(emsExpressDiscountFee, "邮政快递优惠费用", 2, 0, 9999, true, true);
@ -187,4 +243,4 @@ async function getHlwConfigByCorpIds(ctx) {
} catch (e) { } catch (e) {
return { success: false, message: e.message } return { success: false, message: e.message }
} }
} }

View File

@ -270,6 +270,7 @@ module.exports = async (item, db, context) => {
case "deleteHlwPatient": case "deleteHlwPatient":
return await hlwPatient.main(item, db); return await hlwPatient.main(item, db);
case "saveScannedCard": case "saveScannedCard":
case "getScannedCard":
return await scannedCardArchive(item, db); return await scannedCardArchive(item, db);
default: default:
return { return {

View File

@ -1,3 +1,4 @@
const { ObjectId } = require("mongodb");
const Sm4Util = require("../../utils/sm4-util"); const Sm4Util = require("../../utils/sm4-util");
const CARD_STRING_FIELDS = [ const CARD_STRING_FIELDS = [
@ -74,10 +75,58 @@ async function saveScannedCard(item, db) {
} }
} }
async function getScannedCard(item, db) {
const id = normalizeString(item.id, 24);
if (!ObjectId.isValid(id)) {
return { success: false, message: "二维码无效" };
}
try {
const record = await db.collection("scanned-card-archive").findOne({
_id: new ObjectId(id),
});
if (!record) {
return { success: false, message: "未找到本次扫码信息" };
}
const storeId = normalizeString(item.storeId, 100);
const corpId = normalizeString(item.corpId, 100);
if (record.corpId && record.corpId !== corpId) {
return { success: false, message: "无权读取本次扫码信息" };
}
if (
Array.isArray(record.storeIds) &&
record.storeIds.length > 0 &&
!record.storeIds.includes(storeId)
) {
return { success: false, message: "无权读取本次扫码信息" };
}
if (!record.encryptedCardData || record.encryption !== "SM4") {
return { success: false, message: "扫码信息无效" };
}
const decrypted = Sm4Util.decryptDataForSm3(record.encryptedCardData);
const cardData = normalizeCardData(JSON.parse(decrypted));
if (!cardData) {
return { success: false, message: "扫码信息无效" };
}
return {
success: true,
data: cardData,
message: "获取扫码信息成功",
};
} catch (error) {
console.error("获取扫码建档数据失败:", error.message);
return { success: false, message: "获取扫码信息失败" };
}
}
module.exports = async (item, db) => { module.exports = async (item, db) => {
switch (item.type) { switch (item.type) {
case "saveScannedCard": case "saveScannedCard":
return saveScannedCard(item, db); return saveScannedCard(item, db);
case "getScannedCard":
return getScannedCard(item, db);
default: default:
return { success: false, message: "未找到接口" }; return { success: false, message: "未找到接口" };
} }
@ -85,3 +134,4 @@ module.exports = async (item, db) => {
module.exports.normalizeCardData = normalizeCardData; module.exports.normalizeCardData = normalizeCardData;
module.exports.saveScannedCard = saveScannedCard; module.exports.saveScannedCard = saveScannedCard;
module.exports.getScannedCard = getScannedCard;

View File

@ -1,5 +1,13 @@
jest.mock("../../utils/sm4-util", () => ({ jest.mock("../../utils/sm4-util", () => ({
encryptDataForSm3: jest.fn(() => "encrypted-card-data"), encryptDataForSm3: jest.fn(() => "encrypted-card-data"),
decryptDataForSm3: jest.fn(() => JSON.stringify({
CanOpen: true,
Success: true,
IDNum: "330601199001011234",
Name: "张三",
CardNum: "DB5712345",
CardIDCode: "3306012345678901A",
})),
})); }));
const Sm4Util = require("../../utils/sm4-util"); const Sm4Util = require("../../utils/sm4-util");
@ -71,4 +79,57 @@ describe("scanned-card-archive", () => {
expect(result).toEqual({ success: false, message: "卡片数据无效" }); expect(result).toEqual({ success: false, message: "卡片数据无效" });
expect(db.collection).not.toHaveBeenCalled(); expect(db.collection).not.toHaveBeenCalled();
}); });
test("decrypts a scanned card that belongs to the current store", async () => {
const findOne = jest.fn().mockResolvedValue({
encryptedCardData: "encrypted-card-data",
encryption: "SM4",
corpId: "corp-1",
storeIds: ["store-1"],
});
const db = {
collection: jest.fn(() => ({ findOne })),
};
const result = await scannedCardArchive({
type: "getScannedCard",
id: "507f1f77bcf86cd799439011",
corpId: "corp-1",
storeId: "store-1",
}, db);
expect(result.success).toBe(true);
expect(result.data).toEqual(expect.objectContaining({
IDNum: "330601199001011234",
Name: "张三",
CardNum: "DB5712345",
CardIDCode: "3306012345678901A",
}));
expect(Sm4Util.decryptDataForSm3).toHaveBeenCalledWith("encrypted-card-data");
});
test("rejects invalid ids and records from another store", async () => {
const db = {
collection: jest.fn(() => ({
findOne: jest.fn().mockResolvedValue({
encryptedCardData: "encrypted-card-data",
encryption: "SM4",
corpId: "corp-1",
storeIds: ["store-2"],
}),
})),
};
await expect(scannedCardArchive({
type: "getScannedCard",
id: "invalid",
}, db)).resolves.toEqual({ success: false, message: "二维码无效" });
await expect(scannedCardArchive({
type: "getScannedCard",
id: "507f1f77bcf86cd799439011",
corpId: "corp-1",
storeId: "store-1",
}, db)).resolves.toEqual({ success: false, message: "无权读取本次扫码信息" });
});
}); });

View File

@ -40,6 +40,7 @@ const system = require("./system");
const sessionArchive = require("./sessionArchive"); const sessionArchive = require("./sessionArchive");
const customerHisSync = require("./customerHisSync"); const customerHisSync = require("./customerHisSync");
const hlw = require("./hlw"); const hlw = require("./hlw");
const automation = require("./hlw/automation");
const trigger = require("./trigger"); const trigger = require("./trigger");
const consultTrigger = require("./hlw/consult-order/trigger"); const consultTrigger = require("./hlw/consult-order/trigger");
const callBack = require("./callBack"); const callBack = require("./callBack");
@ -193,9 +194,10 @@ app.post(
// ==================== 以下接口需要 JWT 验证 ==================== // ==================== 以下接口需要 JWT 验证 ====================
// 连接数据库 // 连接数据库
connectToMongoDB().catch((err) => { const databaseReadyPromise = connectToMongoDB().catch((err) => {
logger.error("数据库连接失败,服务器将无法正常工作:", err); logger.error("数据库连接失败,服务器将无法正常工作:", err);
// 不退出进程,让错误处理中间件处理后续请求 // 不退出进程,让错误处理中间件处理后续请求
return null;
}); });
app.get( app.get(
@ -702,6 +704,18 @@ console.log(process.env.CONFIG_NODE_ENV);
// 定时任务 // 定时任务
trigger.initSchedule(); trigger.initSchedule();
consultTrigger({ databaseReadyPromise.then(async (connected) => {
type: "recoverDelayedTasks", if (!connected) return;
try {
const internetHospitalDb = await getDatabase("Internet-hospital");
await consultTrigger(
{
type: "recoverDelayedTasks",
},
internetHospitalDb
);
await automation.start(internetHospitalDb);
} catch (error) {
logger.error("恢复问诊延迟任务或启动自动化任务失败:", error);
}
}); });

View File

@ -17,6 +17,7 @@
"test-startup": "node test-startup.js", "test-startup": "node test-startup.js",
"test-concurrent": "node test-concurrent.js", "test-concurrent": "node test-concurrent.js",
"test-production": "node test-production-environment.js", "test-production": "node test-production-environment.js",
"test:automation": "jest hlw/automation --runInBand",
"verify-fix": "npm run check-env && npm run test-db && npm run test-production" "verify-fix": "npm run check-env && npm run test-db && npm run test-production"
}, },
"author": "", "author": "",