diff --git a/.env.dev b/.env.dev index 56f3373..98bdfc8 100644 --- a/.env.dev +++ b/.env.dev @@ -23,3 +23,6 @@ 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 +## 查询药品信息的时候 带上corpId (测试数据 药品信息存在多家机构, 但是线上各自独立部署, 所以测试环境带corpId区分下) +CONFIG_SEARCH_DRUG_WITH_CORPID=YES + diff --git a/.env.development b/.env.development deleted file mode 100644 index 4acc653..0000000 --- a/.env.development +++ /dev/null @@ -1,52 +0,0 @@ -CONFIG_DB_USERNAME=youcan -CONFIG_DB_PASSWORD=youcanTEST&2025 -CONFIG_DB_HOST=119.45.106.100 -CONFIG_DB_PORT=27017 -CONFIG_NODE_ENV=zytDev -CONFIG_CA_BASE_URL=https://newcoss-dev.isignet.cn:10201/coss/service/v1/ -CONFIG_CA_APPID=APP_7B3F36A14E99410A80B37AEF332E3247 -CONFIG_CA_SECRET=DLwiH46Esb8ccNTkuSSVAadNTWUfW0sc -CONFIG_JWT_SECRET_KEY=ZYTZYY2025 -CONFIG_JWT_EXPIRE_TIME=12h -CONFIG_NODE_PORT=8080 -CONFIG_ZYB_YB_APPID=7D8D8E29924048539165F5E2E08C4616 -CONFIG_ZYB_YB_SECRET=77F9610253B94263BCF8DBB61BF90D92 -CONFIG_SMS_GATEWAY_DISABLED=true - -## 支付宝回调地址 -CONFIG_ALIPAY_PAY_NOTIFY=https://patient.youcan365.com/zyt/alipay-pay-notify -## 支付宝小程序appId -CONFIG_ALIPAY_MINI_APPID = 2021005110656858 - -## 蜂鸟即时配送请求域名 -CONFIG_FENGNIAO_API_DOMAIN=https://exam-anubis.ele.me -## 蜂鸟即时配送分配的 appId 和 appSecret -CONFIG_FENGNIAO_APPID=2711175259951256784 -CONFIG_FENGNIAO_SECRET=744c587a-61eb-4dc5-a226-e88b7f7b15d0 -## 蜂鸟即时配送 后台管理页面 -> 应用管理 -> 查看沙箱环境 -> 商户信息的商户ID -CONFIG_FENGNIAO_MERCHANT_ID=15649332 -## 蜂鸟即时配送 后台管理页面 -> 应用管理 -> 查看沙箱环境 -> 门店信息的门店编码 -CONFIG_FENGNIAO_OUT_SHOP_CODE=testorder001 - -## 邮政快递 测试环境请求域名、分配的orgId、AES加密秘钥以及IV -CONFIG_EMS_API_DOMAIN=https://topup.beta.ule.com -## 邮政快递 测试环境分配的orgId、AES加密秘钥以及IV -CONFIG_EMS_ORGID=900004 -CONFIG_EMS_AES_Key=HOKAwvtyPe9Atve4 -CONFIG_EMS_AES_IV=HOKAwvtyPe9Atve4 -## 邮政快递 邮乐分享用户ID (接口文档 2.1订单数据回传接口的入参uleShareUserId字段值, 由对方技术人员提供) -CONFIG_EMS_SHARE_USER_ID=yzg10000050008 - -## 邮政 请求地址 -CONFIG_EMS_API_URL=https://api.ems.com.cn/amp-prod-api/f/amp/api/test -## 邮政 协议用户编码 -CONFIG_EMS_USER_CODE=NNLXIQRYV -## 邮政 测试授权码 -CONFIG_EMS_AUTH_CODE=U1pPOYv6VBULDah6 -## 邮政 测试签名钥匙 -CONFIG_EMS_SIGN_SECRET=ZmVRRjkyeDZnWHFnUGxkRQ== -## 邮政 协议客户号 -CONFIG_EMS_SENDER_NO=1100100971796 - -## 震元his api -CONFIG_ZYT_HIS_BASE_URL=http://115.239.161.48:8089 \ No newline at end of file diff --git a/.env.hn b/.env.hn index c1c5716..eb00a54 100644 --- a/.env.hn +++ b/.env.hn @@ -24,3 +24,17 @@ CONFIG_ALIPAY_MINI_PRIVATE_KEY=MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBA CONFIG_HN_HIS_BASE_URL=http://122.225.16.74:8026 +## im图片存储服务端的位置 +CONFIG_IM_IMAGE_ROOT=/root/service/hn/private-files/im-chat + +## 处方 PDF(Linux Chrome) +PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome +PRESCRIPTION_PDF_MAX_CONCURRENCY=2 +CONFIG_PRESCRIPTION_PDF_ROOT=/root/service/hn/private-files/prescriptions +CONFIG_PRESCRIPTION_PDF_BASE_URL=https://patient.youcan365.com/hn + + +## 查询药品信息的时候 带上corpId (测试数据 药品信息存在多家机构, 但是线上各自独立部署, 所以测试环境带corpId区分下) +CONFIG_SEARCH_DRUG_WITH_CORPID=YES + + diff --git a/hlw/automation/index.js b/hlw/automation/index.js index 09fa5ea..51ef6c4 100644 --- a/hlw/automation/index.js +++ b/hlw/automation/index.js @@ -20,6 +20,7 @@ const TASK_STATUS = { const ERROR_SOURCES = { SYSTEM: "SYSTEM", HIS: "HIS", + RATIONAL_DRUG: "RATIONAL_DRUG", }; const TASK_CANCEL_REASONS = { HIGH_RISK_DRUG: "HIGH_RISK_DRUG", @@ -44,7 +45,9 @@ class AutomationError extends Error { super(message); this.name = "AutomationError"; this.retryable = retryable; - this.source = source === ERROR_SOURCES.HIS ? ERROR_SOURCES.HIS : ERROR_SOURCES.SYSTEM; + this.source = Object.values(ERROR_SOURCES).includes(source) + ? source + : ERROR_SOURCES.SYSTEM; } } @@ -485,14 +488,16 @@ function cleanHisUploadFailureMessage(message) { function getFailureNotificationExt(error) { const errorSource = - error && error.source === ERROR_SOURCES.HIS - ? ERROR_SOURCES.HIS + error && Object.values(ERROR_SOURCES).includes(error.source) + ? error.source : ERROR_SOURCES.SYSTEM; const rawReason = error && typeof error.message === "string" ? error.message.trim() : ""; const reason = errorSource === ERROR_SOURCES.HIS ? `HIS上传错误:${cleanHisUploadFailureMessage(rawReason) || "未知错误"}` + : errorSource === ERROR_SOURCES.RATIONAL_DRUG + ? `合理用药错误:${rawReason || "未知错误"}` : `系统错误:${rawReason || "未知错误"}`; return { errorSource, reason }; } @@ -969,9 +974,9 @@ async function handleAutoOpenRx(db, task) { db ); if (!reviewResult || !reviewResult.success) { - throw new AutomationError( - reviewResult?.message || "合理用药审核未通过" - ); + throw new AutomationError(reviewResult?.message || "合理用药审核未通过", { + source: ERROR_SOURCES.RATIONAL_DRUG, + }); } const result = await diagnosticRecord( diff --git a/hlw/automation/index.test.js b/hlw/automation/index.test.js index a790db3..979a4d9 100644 --- a/hlw/automation/index.test.js +++ b/hlw/automation/index.test.js @@ -726,6 +726,21 @@ describe("automation task scheduling", () => { }), }) ); + expect(tencentIM).toHaveBeenCalledWith( + expect.objectContaining({ + msgBody: [ + expect.objectContaining({ + MsgContent: expect.objectContaining({ + Ext: JSON.stringify({ + errorSource: ERROR_SOURCES.RATIONAL_DRUG, + reason: "合理用药错误:该处方存在不合理用药", + }), + }), + }), + ], + }), + db + ); }); test("fails automatic audit immediately and sends the failure reason", async () => { diff --git a/hlw/drug-info/index.js b/hlw/drug-info/index.js index 8ae227f..30508c7 100644 --- a/hlw/drug-info/index.js +++ b/hlw/drug-info/index.js @@ -6,6 +6,8 @@ const { validateApplicableDiagnosisCodes } = require('./applicable-diagnosis'); const hnHis = require("../hn-his"); let db = ""; +const WithCorpId = process.env.CONFIG_SEARCH_DRUG_WITH_CORPID === 'YES'; + function getConfigOptionNames(list) { return [...new Set((Array.isArray(list) ? list : []) .map(item => typeof item === "string" ? item : item && item.name) @@ -67,6 +69,9 @@ async function getDrugInfo(item) { let { name, _id, ids, pinyin_code, keyword, insuranceCodes, barcode } = item; let fuzzyQuery = null; let query = { onSale: true }; + if (WithCorpId) { + query.corpId = item.corpId + } if (typeof name === "string") query.name = new RegExp(name.trim(), "i"); if (_id) query._id = new ObjectId(_id); if (Array.isArray(ids) && ids.length > 0) { @@ -211,6 +216,9 @@ async function getHlwDrugInfo(ctx) { const page = Number.isSafeInteger(requestedPage) && requestedPage > 0 ? requestedPage : 1; const pageSize = Number.isSafeInteger(requestedPageSize) && requestedPageSize > 0 ? requestedPageSize : 10; const query = {}; + if (WithCorpId) { + query.corpId = ctx.corpId + } // if (typeof ctx.corpId === "string" && ctx.corpId.trim()) { // query.corpId = ctx.corpId.trim(); // } @@ -285,6 +293,9 @@ async function addHlwDrugInfo(ctx) { newDrug.tumor_use = 0; } newDrug.createTime = Date.now(); + if (WithCorpId) { + newDrug.corpId = ctx.corpId + } const res = await db.collection("drug-info").insertOne(newDrug); if (res && res.insertedId) { updateHisDrugInfo({ id: res.insertedId.toString() }) diff --git a/hlw/drug-info/verify.js b/hlw/drug-info/verify.js index bec8530..7a13240 100644 --- a/hlw/drug-info/verify.js +++ b/hlw/drug-info/verify.js @@ -23,6 +23,7 @@ const drugFieldDict = [ { name: '抗生素级别', field: 'antibiotic_level', verify: (val, label) => validator.verifyListItem(val, [0, 1, 2, 3], false, label, 0) }, { name: '肿瘤使用', field: 'tumor_use', verify: (val, label) => validator.verifyListItem(val, [0, 1, 2, 3], false, label, 0) }, { name: '风险等级', field: 'riskLevel', verify: (val, label) => validator.verifyListItem(val, ['低风险', '高风险'], false, label) }, + { name: '默认医保自费', field: 'forceSelfPay', verify: (val, label) => validator.verifyListItem(val, ['Y', 'N'], false, label) }, // 仅在线药品库使用:管理分类(处方药 / OTC)与剂型 { name: '管理分类', field: 'manage_category', verify: (val, label) => validator.verifyString(val, label) }, { name: '剂型', field: 'dosage_form', verify: (val, label) => validator.verifyString(val, label) }, diff --git a/hlw/hlw-diagnosis/index.js b/hlw/hlw-diagnosis/index.js index ea3137b..a373196 100644 --- a/hlw/hlw-diagnosis/index.js +++ b/hlw/hlw-diagnosis/index.js @@ -53,22 +53,55 @@ async function getDiagnosisList(item) { if (category && typeof category === "string") { query.category = new RegExp(category.trim(), "i"); } - if (typeof keyword == "string" && keyword.trim()) { + const normalizedKeyword = typeof keyword === "string" ? keyword.trim() : ""; + if (normalizedKeyword) { query.$or = [ - { name: new RegExp(keyword.trim(), "i") }, - { code: new RegExp(keyword.trim(), "i") }, - { category: new RegExp(keyword.trim(), "i") }, - { pinyin: new RegExp(keyword.trim(), "i") }, + { name: new RegExp(normalizedKeyword, "i") }, + { code: new RegExp(normalizedKeyword, "i") }, + { category: new RegExp(normalizedKeyword, "i") }, + { pinyin: new RegExp(normalizedKeyword, "i") }, ]; } - const res = await db - .collection("hlw-diagnosis") - .find(query) - .sort({ _id: -1 }) - .skip((page - 1) * pageSize) - .limit(pageSize) - .toArray(); - const total = await db.collection("hlw-diagnosis").countDocuments(query); + const collection = db.collection("hlw-diagnosis"); + let res; + if (normalizedKeyword) { + // Keep fuzzy search behavior, but bring an exact match for any searchable + // field to the front before applying pagination. + const exactKeyword = `^${normalizedKeyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`; + res = await collection.aggregate([ + { $match: query }, + { + $addFields: { + exactKeywordMatch: { + $cond: [ + { + $or: [ + { $regexMatch: { input: { $ifNull: ["$name", ""] }, regex: exactKeyword, options: "i" } }, + { $regexMatch: { input: { $ifNull: ["$code", ""] }, regex: exactKeyword, options: "i" } }, + { $regexMatch: { input: { $ifNull: ["$category", ""] }, regex: exactKeyword, options: "i" } }, + { $regexMatch: { input: { $ifNull: ["$pinyin", ""] }, regex: exactKeyword, options: "i" } }, + ], + }, + 1, + 0, + ], + }, + }, + }, + { $sort: { exactKeywordMatch: -1, _id: -1 } }, + { $skip: (page - 1) * pageSize }, + { $limit: pageSize }, + { $project: { exactKeywordMatch: 0 } }, + ]).toArray(); + } else { + res = await collection + .find(query) + .sort({ _id: -1 }) + .skip((page - 1) * pageSize) + .limit(pageSize) + .toArray(); + } + const total = await collection.countDocuments(query); return { success: true, message: "查询成功", diff --git a/hlw/hlw-patient/index.js b/hlw/hlw-patient/index.js index c6ea1c9..3136e0d 100644 --- a/hlw/hlw-patient/index.js +++ b/hlw/hlw-patient/index.js @@ -40,7 +40,9 @@ function validatePatient(item) { const mobile = typeof item.mobile === 'string' ? item.mobile.trim() : ''; if (!name) return { success: false, message: '姓名不能为空' }; - if (!certNo) return { success: false, message: '证件号不能为空' }; + if (certNo && !validator.isChinaId(certNo)) { + return { success: false, message: '证件号格式不正确' }; + } if (!mobile) return { success: false, message: '手机号不能为空' }; if (!validator.isMobile(mobile)) { return { success: false, message: '手机号格式不正确' }; @@ -64,7 +66,7 @@ function validatePatient(item) { }, data: { accountId: accountResult.accountId, - anotherIdNo: Sm4Util.encryptDataForSm3(certNo), + anotherIdNo: certNo ? Sm4Util.encryptDataForSm3(certNo) : '', anotherName: Sm4Util.encryptDataForSm3(name), anotherMobile: Sm4Util.encryptDataForSm3(mobile), address, @@ -77,16 +79,17 @@ async function addHlwPatient(item, db) { const validation = validatePatient(item); if (!validation.success) return validation; - const exists = await db.collection(COLLECTION_NAME).countDocuments({ - accountId: validation.data.accountId, - anotherIdNo: validation.data.anotherIdNo, - disabled: false, - }); - if (exists > 0) { - return { success: false, message: '当前账户下已存在相同证件号的就诊人' }; + if (validation.origin.certNo) { + const exists = await db.collection(COLLECTION_NAME).countDocuments({ + accountId: validation.data.accountId, + anotherIdNo: validation.data.anotherIdNo, + disabled: false, + }); + if (exists > 0) { + return { success: false, message: '当前账户下已存在相同证件号的就诊人' }; + } } - console.log(validation.data) - + // if (validation.origin.certNo) { const res = await hnHis({ type: 'addHisCustomer', name: validation.origin.name, @@ -95,6 +98,7 @@ async function addHlwPatient(item, db) { address: validation.origin.address, }) if (!res.success) return res; + // } const result = await db.collection(COLLECTION_NAME).insertOne({ ...validation.data, @@ -117,14 +121,16 @@ async function autoAddHlwPatient(item, db) { const validation = validatePatient(item); if (!validation.success) return validation; - const exists = await db.collection(COLLECTION_NAME).countDocuments({ - accountId: validation.data.accountId, - anotherIdNo: validation.data.anotherIdNo, - disabled: false, - }); + if (validation.origin.certNo) { + const exists = await db.collection(COLLECTION_NAME).countDocuments({ + accountId: validation.data.accountId, + anotherIdNo: validation.data.anotherIdNo, + disabled: false, + }); - if (exists > 0) { - return { success: true, data: true, message: '就诊人已存在' }; + if (exists > 0) { + return { success: true, data: true, message: '就诊人已存在' }; + } } const result = await addHlwPatient(item, db); diff --git a/hlw/hn-his/index.js b/hlw/hn-his/index.js index 7e1a93c..cbd5f64 100644 --- a/hlw/hn-his/index.js +++ b/hlw/hn-his/index.js @@ -75,8 +75,9 @@ async function addHisCustomer(item) { const address = typeof item.address === 'string' ? item.address.trim() : ''; const psnToken = typeof item.psnToken === 'string' ? item.psnToken.trim() : ''; const mdtrt_cert_no = typeof item.mdtrt_cert_no === 'string' ? item.mdtrt_cert_no.trim() : ''; + const ecToken = typeof item.ecToken === 'string' ? item.ecToken.trim() : ''; const card_sn = typeof item.card_sn === 'string' ? item.card_sn.trim() : ''; - if (!name || !idCard || !mobile) { + if (!name || !mobile) { return { success: false, message: '姓名、证件号、手机号不能为空' } } const payload = { @@ -92,16 +93,15 @@ async function addHisCustomer(item) { payload.psnToken = psnToken; payload.buildType = "2"; // payload.mdtrt_cert_type = '01' - } - // 线下医保建档 - // mdtrt_cert_type 就诊凭证类型 01 | 03 - // mdtrt_cert_no 就诊凭证号码 mdtrt_cert_type为01 填写电子凭证令牌 | mdtrt_cert_type为03 填写社会保障卡卡号 - if (mdtrt_cert_no) { + } else if (mdtrt_cert_no) { payload.mdtrt_cert_type = '03'; payload.mdtrt_cert_no = mdtrt_cert_no; payload.card_sn = card_sn; //就诊凭证类型为“03”时填写社会保障卡内码 + } else if (ecToken) { + payload.buildType = '2'; + payload.mdtrt_cert_type = '01'; + payload.mdtrt_cert_no = ecToken; } - const { data, status_code, message } = await request.main( url, payload,