diff --git a/.env.dev b/.env.dev index 3d6fc97..fbd090e 100644 --- a/.env.dev +++ b/.env.dev @@ -56,8 +56,8 @@ CONFIG_EMS_SIGN_SECRET=ZmVRRjkyeDZnWHFnUGxkRQ== CONFIG_EMS_SENDER_NO=1100241354944 -## 震元his api -CONFIG_ZYT_HIS_BASE_URL=http://115.239.161.48:8089 +## 海宁his api +CONFIG_HN_HIS_BASE_URL=http://122.225.16.74:8026 ## 银联支付请求基础路径 CONFIG_UNION_PAY_BASE_URL=https://test-api-open.chinaums.com/v1 diff --git a/hlw/consult-order/index.js b/hlw/consult-order/index.js index 1e41ba1..10ce89b 100644 --- a/hlw/consult-order/index.js +++ b/hlw/consult-order/index.js @@ -111,28 +111,16 @@ async function addConsultOrder(item) { params.corpId = corpId; params.orderSource = "ALIPAY_MINI"; // 支付宝小程序 params.doctorCode = params.doctorCode.trim(); + params.drugStoreId = 'HN000001'; + if (!params.drugStoreId) { + return { success: false, message: "药房信息缺失" }; + } return await addOnlineConsultOrder(params); } async function addOnlineConsultOrder(params) { try { params.registerId = common.generateUniqueStr('GH'); - const res1 = await getConfig(params.corpId); - if (!res1) { - return { success: false, message: "未查询到配置信息" }; - } - const { autoAcceptOrder, consultationDuration } = res1 || {}; - if (typeof autoAcceptOrder !== 'boolean' || !(Number.isInteger(consultationDuration) && consultationDuration > 0)) { - return { success: false, message: "咨询配置错误" }; - } - if (autoAcceptOrder) { - params.orderStatus = "processing"; - params.expireTime = dayjs().add(consultationDuration, "minute").valueOf(); - } - params.drugStoreId = 'HN000001'; - if (!params.drugStoreId) { - return { success: false, message: "药房信息缺失" }; - } const storeApi = require("../drug-store"); const { success, list, message } = await storeApi( { @@ -200,6 +188,26 @@ async function addOnlineConsultOrder(params) { return { success: false, message: data.message }; } } + const imAccountResult = await tencentIM({ + type: "importAccount", + account: params.orderId, + corpId: params.corpId, + }); + if (!imAccountResult.success) { + const rawError = + imAccountResult.errorInfo || + imAccountResult.error || + imAccountResult.message || + "未知错误"; + const errorInfo = + typeof rawError === "string" ? rawError : JSON.stringify(rawError); + return { + success: false, + message: `咨询订单 IM 账号初始化失败:${errorInfo}`, + errorCode: imAccountResult.errorCode, + errorInfo: imAccountResult.errorInfo, + }; + } params.medorg_order_no = common.generateUniqueStr('HLW', 12); // 加密敏感字段 const encryptedParams = encryptOrderFields(params); @@ -207,6 +215,14 @@ async function addOnlineConsultOrder(params) { .collection("consult-order") .insertOne(encryptedParams); if (insertedId) { + if (params.payStatus === 'success') { + await startConsultOrder(params); + } + const res1 = await getConfig(params.corpId); + const { autoAcceptOrder } = res1 || {}; + if (autoAcceptOrder) { + await acceptConsultOrder(params); + } return { success: true, message: "新增成功", @@ -647,6 +663,9 @@ async function acceptConsultOrder(item) { ) { return { success: false, message: "订单已过期,请刷新" }; } + if (item.orderStatus === 'processing') { + return { success: true, message: "订单已处理" }; + } if (item.orderStatus !== "pending") { return { success: false, message: "订单已失效,请刷新" }; } @@ -917,6 +936,8 @@ async function getConsultOrder(item) { patientId, idCard, orderStatus, + orderStatuses, + payStatus, mergeStatus, doctorCode, corpId, @@ -926,14 +947,56 @@ async function getConsultOrder(item) { orderSource, accountId, } = item; - const query = {}; + const query = { corpId }; + const allowedOrderStatuses = new Set([ + "pending", + "processing", + "completed", + "finished", + "cancelled", + ]); + const allowedPayStatuses = new Set([ + "pending", + "success", + "failed", + "expired", + "cancelled", + "refunded", + "refundFailed", + ]); if (orderId) query.orderId = orderId; if (hospitalId) query.hospitalId = hospitalId; if (patientId) query.patientId = patientId; if (idCard) query.idCard = idCard; - // if (corpId) query.corpId = corpId; if (doctorCode) query.doctorCode = doctorCode; - if (orderStatus) query.orderStatus = orderStatus; + if (orderStatus !== undefined && orderStatuses !== undefined) { + return { success: false, message: "订单状态参数不能重复" }; + } + if (orderStatus !== undefined) { + if ( + typeof orderStatus !== "string" || + !allowedOrderStatuses.has(orderStatus) + ) { + return { success: false, message: "订单状态参数错误" }; + } + query.orderStatus = orderStatus; + } + if (orderStatuses !== undefined) { + if ( + !Array.isArray(orderStatuses) || + orderStatuses.length === 0 || + orderStatuses.some((value) => !allowedOrderStatuses.has(value)) + ) { + return { success: false, message: "订单状态参数错误" }; + } + query.orderStatus = { $in: [...new Set(orderStatuses)] }; + } + if (payStatus !== undefined) { + if (typeof payStatus !== "string" || !allowedPayStatuses.has(payStatus)) { + return { success: false, message: "支付状态参数错误" }; + } + query.payStatus = payStatus; + } if (drugStoreId) query.drugStoreId = drugStoreId; if (orderSource) query.orderSource = orderSource; if (typeof accountId === 'string' && accountId.trim()) query.accountId = accountId.trim(); @@ -959,7 +1022,7 @@ async function getConsultOrder(item) { // 先查询所有通过审核的诊断记录的 orderId const passedDiagnosticOrderIds = await db .collection("diagnostic-record") - .distinct("orderId", { status: "PASS" }); + .distinct("orderId", { corpId, status: "PASS" }); // 根据 hasPassDiagnostic 条件调整查询 if (hasPassDiagnostic) { // 只查询有通过诊断的订单 @@ -1060,7 +1123,7 @@ async function getConsultOrder(item) { // 仍然需要根据诊断状态过滤 const passedDiagnosticOrderIds = await db .collection("diagnostic-record") - .distinct("orderId", { status: "PASS" }); + .distinct("orderId", { corpId, status: "PASS" }); if (hasPassDiagnostic) { // 只查询有通过诊断的订单 @@ -1114,7 +1177,7 @@ async function getConsultOrder(item) { // : query; // console.log("allQuery case2: ", allQuery); const allQuery = query; - + console.log("allQuery case3333: ", allQuery); res = await db .collection("consult-order") .find(allQuery) diff --git a/hlw/diagnostic-record/document.ts b/hlw/diagnostic-record/document.ts index 622a951..1297279 100644 --- a/hlw/diagnostic-record/document.ts +++ b/hlw/diagnostic-record/document.ts @@ -118,4 +118,6 @@ interface DiagnosticRecord { hisChargeStatus?: string // 配药订单处方的his支付状态 (在线购药的处方才有) needReUpload?: boolean // 门店处方 是否需要重新上传(可能存在的场景 调用his接口上传成功了 但是his未上传医保, 需要药师手动去his的上传处理页面上传) -} \ No newline at end of file + printCount?: number; // 门店自费处方打印次数 + lastPrintTime?: number; // 门店自费处方最近一次打印时间 +} diff --git a/hlw/diagnostic-record/format.js b/hlw/diagnostic-record/format.js index c6709dc..ec957c9 100644 --- a/hlw/diagnostic-record/format.js +++ b/hlw/diagnostic-record/format.js @@ -80,8 +80,12 @@ function decryptDiagnosticRecordList(list) { exports.formatUploadRxData = function (data) { return { + pay_type: data.feeType === 'YB' ? '1' : '0', patientid: data.patientId, orderno: data.medOrgOrderNo, + med_type: data.med_type, + dise_codg: data.dise_codg, + dise_name: data.dise_name, doctorcode: data.doctorCode, unitcode: data.unitCode, hospid: "108", @@ -92,9 +96,10 @@ exports.formatUploadRxData = function (data) { prsc_time: dayjs(data.prescriptionEndTime).format("YYYY-MM-DD HH:mm:ss"), details: data.drugs.map((i) => { const item = { - drug_id: i.insurance_code, + erp_id: i.erpId, + drug_id: i.insurance_code || i._id, drug_name: i.drugName, - drug_dosform: "", + drug_dosform: i.dosage_form, drug_spec: i.specification || "", drug_pric: "0", drug_cnt: i.quantity, @@ -125,79 +130,6 @@ exports.formatUploadRxData = function (data) { }; -exports.formatOnlineMedicinePurchaseData = function (data) { - const details = data.drugs.map(i => { - const item = { - drug_type: "xy", - drug_groupno: i.hisChargeInfo.drugGroupNo, - drug_group: "", - drug_id: i.hisChargeInfo.drugId, - drug_serial: i.hisChargeInfo.drugSerial, - drug_name: i.drugName, - drug_spec: i.specification || "", - drug_pric: i.hisChargeInfo.drugPrice, - drug_cnt: i.quantity, - drug_sumamt: i.hisChargeInfo.drugSumamt, //总金额 总金额=数量*单价 - supple_code: i.usageCode, - supple_name: i.usageName, - medc_days: i.days, - drug_times: 1, - drug_comm: "", - drug_ma: "", - remark: "", - limit_flag: "0", - usage: i.frequencyCode, - usage_name: i.frequencyName, - sin_doscnt: i.dosage, - sin_dosunt: i.dosage_unit_code, - unit: i.unit, - appr_flag: i.forceSelfPay === 'Y' ? '2':'1' - } - if (item.appr_flag === '2') { - //[his要求]当appr_flag=2时必填,如果这个药品维护了限制使用范围的,传1,否则就传2 - item.self_pay_rea = i.limitUsageScope === 'Y' ? '1' : '2' - } - return item - }) - return { - patientid: data.patientId, - registerid: "", - orderno: data.medOrgOrderNo, - doctorcode: data.doctorCode, - unitcode: data.unitCode, - visit_time: dayjs(data.prescriptionStartTime).format("YYYY-MM-DD HH:mm:ss"), - area_code: data.areaCode, - hospid: "108", - details, - icds: data.diagnosisList.map((i, idx) => ({ - icd_code: i.code, - icd_name: i.name, - sort: idx + 1 - })) - } -} - -exports.formatWinePurchaseData = function (data, order) { - const [wine] = data.wines; - return { - patientid: data.patientId, - registerid: order.medorg_order_no, - orderno: data.medOrgOrderNo, - doctorcode: data.doctorCode, - unitcode: data.unitCode, - visit_time: dayjs(data.prescriptionStartTime).format("YYYY-MM-DD HH:mm:ss"), - operator_id: data.drugStoreNo, - tc_code: wine.hisId, - drug_times: wine.quantity, - delivery: data.pickUpType === 'delivery' ? 1 :0,//是否配送 1配送 0自提 order.pickUpType === 'delivery' ? 1 :0 - hospid: "108", - icds: data.diagnosisList.map((i) => ({ - icd_code: i.code, - icd_name: i.name, - })), - }; -} - /** * 构建支持明文+密文混合搜索的查询条件 * 用于数据迁移期间,数据库中同时存在明文和密文数据的情况 @@ -301,7 +233,7 @@ exports.formatReviewData = function (data) { sin_doscnt: i.dosage, sin_dosunt: i.dosage_unit_code, unit: i.unit, - appr_flag: i.forceSelfPay === 'Y' ? '2':'1' + appr_flag: i.forceSelfPay === 'Y' ? '2' : '1' } if (item.appr_flag === '2') { //[his要求]当appr_flag=2时必填,如果这个药品维护了限制使用范围的,传1,否则就传2 diff --git a/hlw/diagnostic-record/index.js b/hlw/diagnostic-record/index.js index 2724714..7cd8914 100644 --- a/hlw/diagnostic-record/index.js +++ b/hlw/diagnostic-record/index.js @@ -1,5 +1,6 @@ let db = ""; const { ObjectId } = require("mongodb"); +const { getDatabase } = require("../../mongodb"); const dayjs = require("dayjs"); const { decryptOrderList } = require("../consult-order/format"); const { @@ -12,20 +13,13 @@ const { decryptDiagnosticRecordList, buildHybridSearchQuery, } = require("./format"); -const zytHis = require("../zyt-his"); +const hnHis = require("../hn-his"); const consultOrder = require("../consult-order"); const tencentIM = require("../tencent-im"); const beijingCa = require("../beijing-ca"); const BigNumber = require("bignumber.js"); const timeApi = require("../hlw-time-duration"); const types = require("./types"); -const { getConfig } = require("../utils/config"); - -const PrescriptionTypeMap = { - onlineMedicinePurchase: 'onlineMedicinePurchase', - onlineConsult: 'storeMedicinePurchase', - wineConsult: 'medicalWinePurchase' -} /** * 性能优化建议: @@ -83,7 +77,7 @@ const status = { invalid: "INVALID", //处方作废 (目前就订单退费会导致处方作废) }; -module.exports = async (item, mongodb) => { +module.exports = async (item, mongodb, user) => { db = mongodb; switch (item.type) { case "addConsultDiagnosis": @@ -93,7 +87,9 @@ module.exports = async (item, mongodb) => { case "getDiagnosisById": return await getDiagnosisById(item, db); case "getStoreDiagnosisRecord": - return await getStoreDiagnosisRecord(item, db); + return await getStoreDiagnosisRecord(item, db, user); + case "markStoreRxPrinted": + return await markStoreRxPrinted(item, db, user); case "getAuditDiagnosisList": return await getAuditDiagnosisList(item, db); case "getAuditedDiagnosisList": @@ -164,18 +160,20 @@ async function addConsultDiagnosis(item) { const timeData = { type: 'addConsultDiagnosis', startTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), orderId } try { const order = await db.collection("consult-order").findOne( - { orderId }, + { orderId, corpId }, { projection: { + corpId: 1, createTime: 1, orderStatus: 1, registerId: 1, medorg_order_no: 1, accountId: 1, prescriptionStartTime: 1, - consultType: 1, areaCode: 1, pickUpType: 1, + medInfo: 1, + feeType: 1, sex: 1, age: 1 }, @@ -192,15 +190,9 @@ async function addConsultDiagnosis(item) { return { success: false, message: "当前订单不在有效期内" }; } timeApi({ type: "addDoctorRxDuration", orderId }, db); - // 提交诊断前, 先判单CA 自动签名是否失效 - const res = await conditionalCallBeijingCa({ - type: "autoSignVerifyCA", - code: doctorCode, - }); - timeData.caSignVerifyTime = dayjs().format('YYYY-MM-DD HH:mm:ss'); - const doctorCAUserId = res.userId; - if (!res.success) { - return { success: false, message: "CA自动签名已失效, 请开启自动签" }; + const doctor = await db.collection("hlw-doctor").findOne({ corpId, doctorNo: doctorCode }); + if (!doctor) { + return { success: false, message: "医生不存在" }; } // 判断是否医生发的消息是否大于2条 const chatCount = await db.collection("im-chat-msg").countDocuments({ @@ -228,21 +220,7 @@ async function addConsultDiagnosis(item) { if (record && record.status === status.pass) { return { success: false, message: "该订单已审核通过,请勿重复添加" }; } - if (order.consultType === "onlineMedicinePurchase") { - const { success, message, drugs } = await getMedicinesWithHisChargeInfo(params.drugs, order.areaCode); - if (!success) { - return { success, message } - } - params.drugs = drugs; - } - if (order.consultType === "wineConsult") { - const { success, message, wines } = await getWinesWithHisChargeInfo(params.wines); - if (!success) { - return { success, message } - } - params.wines = wines; - params.pickUpType = order.pickUpType; - } + if (record && record.status === status.unpass) { return await rewriteConsultDiagnosis( params, @@ -258,30 +236,30 @@ async function addConsultDiagnosis(item) { success, message, data: pharmacist, - } = await getRecommendPharmacist({ type: order.consultType === 'wineConsult' ? 'chinese' : 'west' }); + } = await getRecommendPharmacist({ corpId }); timeData.pharmacistQueryTime = dayjs().format('YYYY-MM-DD HH:mm:ss'); if (!success) { return { success: false, message }; } - const common = require('../../common') const data = { ...params, createTime: Date.now(), status: status.init, pharmacistNo: pharmacist.pharmacistNo, pharmacist: pharmacist.pharmacistName, - doctorCAUserId, + doctorSignImg: doctor.signatureUrl, sex: order.sex, age: order.age, - prescriptionType: PrescriptionTypeMap[order.consultType], + // prescriptionType: PrescriptionTypeMap[order.consultType], accountId: order.accountId, medOrgOrderNo: order.registerId || order.medorg_order_no, areaCode: order.areaCode, + med_type: order.medInfo.med_type, + dise_codg: order.medInfo.dise_codg, + dise_name: order.medInfo.dise_name, + feeType: order.feeType, + corpId, }; - if (order.consultType === 'wineConsult') { - data.medOrgOrderNo = common.generateUniqueStr('R') - } - // 由后端统一设置处方时间: // - 开始时间从 consult-order 读取(接单时已由后端写入) // - 结束时间为首次提交医嘱的服务器时间 @@ -317,14 +295,7 @@ async function addConsultDiagnosis(item) { await db.collection('doctor-last-submit-time').insertOne({ doctorCode: data.doctorCode, submitTime: Date.now() }); } } - if (['onlineMedicinePurchase', 'medicalWinePurchase'].includes(data.prescriptionType)) { - const { medicinePurchaseRxDuration } = await getConfig(corpId); - if (Number.isInteger(medicinePurchaseRxDuration) && medicinePurchaseRxDuration >= 10) { - data.expireTime = dayjs().add(medicinePurchaseRxDuration, 'minute').valueOf(); - } else { - return { success: false, message: "配药处方持续时长配置不正确, 请检查配置(最小10分钟)" }; - } - } + // return { success: false, message: '不让提交', data } // 加密敏感字段 const encryptedData = encryptDiagnosticRecordFields(data); const { insertedId } = await db @@ -351,7 +322,6 @@ async function addConsultDiagnosis(item) { success: true, message: "新增成功", data: insertedId, - doctorCAUserId, }; } catch (err) { return { @@ -468,147 +438,94 @@ async function getOrderDiagnosis(item, db) { } } -async function getStoreDiagnosisRecord(item, db) { - if (typeof item.storeId !== "string" || item.storeId.trim() === "") { +async function resolveStoreIds(item, db, user) { + let authorizedStoreId = + user && typeof user.drugStoreId === "string" ? user.drugStoreId.trim() : ""; + if (!authorizedStoreId && user && ObjectId.isValid(user.userId)) { + const corpDb = await getDatabase("corp"); + const account = await corpDb.collection("corp-member").findOne( + { _id: new ObjectId(user.userId) }, + { projection: { drugStoreId: 1 } } + ); + authorizedStoreId = + account && typeof account.drugStoreId === "string" + ? account.drugStoreId.trim() + : ""; + } + if (!authorizedStoreId) { + return []; + } + const storeIds = [authorizedStoreId]; + const store = await db.collection("store-list").findOne( + { store_id: authorizedStoreId }, + { projection: { login_no: 1 } } + ); + if (store && typeof store.login_no === "string" && store.login_no.trim()) { + storeIds.push(store.login_no.trim()); + } + return [...new Set(storeIds)]; +} + +async function getStoreDiagnosisRecord(item, db, user) { + const storeIds = await resolveStoreIds(item, db, user); + if (storeIds.length === 0) { return { success: false, message: "参数错误" }; } + const query = { + drugStoreNo: { $in: [...new Set(storeIds)] }, + status: status.pass, + }; const page = item.page > 0 && Number.isInteger(item.page) ? item.page : 1; const pageSize = item.pageSize > 0 && Number.isInteger(item.pageSize) ? item.pageSize : 20; - const date = - item.date && dayjs(item.date).isValid() ? dayjs(item.date) : dayjs(); - const createTime = { - $gte: date.startOf("day").valueOf(), - $lte: date.endOf("day").valueOf(), - }; - const query = { - drugStoreNo: item.storeId, - orderSource: "MATEPAD", - status: status.pass, - createTime, - }; - if (typeof item.keyword == "string" && item.keyword.trim() !== "") { - // 使用混合搜索支持明文和密文数据 - const nameCondition = buildHybridSearchQuery("name", item.keyword, false); - const orConditions = [ - { doctorName: { $regex: item.keyword.trim(), $options: "i" } }, - ]; + const startDate = item.startDate && dayjs(item.startDate).isValid() ? dayjs(item.startDate) : null; + const endDate = item.endDate && dayjs(item.endDate).isValid() ? dayjs(item.startDate) : null; + if (startDate) { + query.createTime = { $gte: startDate.startOf("day").valueOf() } + } + if (endDate) { + query.createTime = query.createTime || {}; + query.createTime['$lte'] = endDate.endOf("day").valueOf(); + } - // 如果构建了name搜索条件,添加到$or中 - if (nameCondition && nameCondition.$or) { - orConditions.push(...nameCondition.$or); - } else if (nameCondition) { - orConditions.push(nameCondition); - } - - query.$or = orConditions; + const nameKeyword = + typeof item.name === "string" ? item.name.trim().toLocaleLowerCase() : ""; + if (typeof item.medOrgOrderNo === "string" && item.medOrgOrderNo.trim()) { + const keyword = item.medOrgOrderNo.trim().replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + query.medOrgOrderNo = { $regex: keyword, $options: "i" }; + } + if (["YB", "ZF"].includes(item.feeType)) { + query.feeType = item.feeType; } try { + if (nameKeyword) { + // 姓名是加密字段,密文无法直接进行模糊查询。 + // 先读取当前筛选范围内的姓名和 ID,解密后匹配,再用 ID 正常分页。 + const nameCandidates = await db + .collection("diagnostic-record") + .find(query, { projection: { name: 1 } }) + .toArray(); + const matchedIds = decryptDiagnosticRecordList(nameCandidates) + .filter( + (record) => + typeof record.name === "string" && + record.name.toLocaleLowerCase().includes(nameKeyword) + ) + .map((record) => record._id); + query._id = { $in: matchedIds }; + } const total = await db .collection("diagnostic-record") .countDocuments(query); const list = await db .collection("diagnostic-record") .find(query) - .sort({ auditTime: -1 }) + .sort({ createTime: -1, _id: -1 }) .skip((page - 1) * pageSize) .limit(pageSize) .toArray(); - // 查询今日的处方 对已上传但未获取 his处方上传状态的处方进行查询 - const start = dayjs().startOf("day").valueOf(); - const toSignList = list - .filter( - (i) => - i.patientId && - i.medOrgOrderNo && - i.status === status.pass && - i.uploadStatus === "uploaded" && - !i.hisUploadStatus && - i.createTime > start - ) - .map((i) => ({ - _id: i._id, - patientId: i.patientId, - medOrgOrderNo: i.medOrgOrderNo, - })); - if (toSignList.length > 0) { - const task = toSignList.map((data) => - zytHis({ - type: "getHisPrescriptionUploadStatus", - orderno: data.medOrgOrderNo, - patientId: data.patientId, - _id: data._id, - }) - ); - const res = await Promise.all(task); - const result = res.reduce( - (acc, item) => { - if (item.hisUploadStatus === "uploaded") { - acc.uploaded.push(item._id); - } else if (item.hisUploadStatus === "error") { - acc.uploadError.push(item._id); - } else if (item.hisUploadStatus === "notUploaded") { - acc.notUploaded.push(item._id); - } - if (item.hisUploadStatus && item.hisUploadStatus !== "notUploaded") { - acc.map[item.orderno] = item.hisUploadStatus; - } - return acc; - }, - { uploaded: [], uploadError: [], notUploaded: [], map: {} } - ); - if (result.uploaded.length > 0) { - await db - .collection("diagnostic-record") - .updateMany( - { _id: { $in: result.uploaded } }, - { $set: { hisUploadStatus: "uploaded" } } - ); - } - if (result.uploadError.length > 0) { - await db - .collection("diagnostic-record") - .updateMany( - { _id: { $in: result.uploadError } }, - { $set: { hisUploadStatus: "error" } } - ); - } - if (result.notUploaded.length > 0) { - await db - .collection("diagnostic-record") - .updateMany( - { _id: { $in: result.notUploaded }, needReUpload: { $exists: false } }, - { $set: { needReUpload: true } } - ); - } - list.forEach((i) => { - if (result.map[i.orderId]) { - i.hisUploadStatus = result.map[i.orderId]; - } - }); - } - // 解密敏感字段 + const decryptedList = decryptDiagnosticRecordList(list); - const wineList = decryptedList.filter(i => i.prescriptionType === 'medicalWinePurchase'); - if (wineList.length) { - const wines = await db.collection('chinese-medicine-order').find({ rpNo: { $in: wineList.map(i => i._id.toString()) } }).toArray(); - for (let item of wineList) { - if (item.pickUpType === 'selfTake') { - item.wineStatus = 'completed'; - continue; - } - const wine = wines.find(i => i.rpNo === item._id.toString()); - if (wine) { - item.wineStatus = wine.status; - continue; - } - if (item.expireTime > Date.now()) { - item.wineStatus = 'unpay'; - } else { - item.wineStatus = 'expired'; - } - } - } return { success: true, message: "查询成功", @@ -621,6 +538,37 @@ async function getStoreDiagnosisRecord(item, db) { } } +async function markStoreRxPrinted(item, db, user) { + const storeIds = await resolveStoreIds(item, db, user); + if ( + typeof item.id !== "string" || + !ObjectId.isValid(item.id.trim()) || + storeIds.length === 0 + ) { + return { success: false, message: "参数错误" }; + } + try { + const result = await db.collection("diagnostic-record").updateOne( + { + _id: new ObjectId(item.id.trim()), + drugStoreNo: { $in: [...new Set(storeIds)] }, + status: status.pass, + feeType: "ZF", + }, + { + $inc: { printCount: 1 }, + $set: { lastPrintTime: Date.now() }, + } + ); + if (result.matchedCount === 0) { + return { success: false, message: "处方不存在或不可打印" }; + } + return { success: true, message: "打印状态记录成功" }; + } catch (e) { + return { success: false, message: e.message || "记录打印状态失败" }; + } +} + async function getAuditDiagnosisList(item, db) { const page = item.page > 0 && Number.isInteger(item.page) ? item.page : 1; const pageSize = @@ -951,27 +899,25 @@ async function getAuditedDiagnosisList(item, db) { } async function auditDiagnosis(item, db) { - const { pharmacistNo, pharmacist } = item; - const timeData = { type: 'auditDiagnosis', startTime: dayjs().format('YYYY-MM-DD HH:mm:ss') }; - const res = await conditionalCallBeijingCa({ - type: "autoSignVerifyCA", - code: pharmacistNo, - }, db); - timeData.caSignVerifyTime = dayjs().format('YYYY-MM-DD HH:mm:ss'); - if (!res.success) { - return { success: false, message: "CA自动签名已失效, 请开启自动签" }; + // const { pharmacistNo, pharmacist, corpId } = item; + const pharmacistNo = item && typeof item.pharmacistNo === 'string' ? item.pharmacistNo : ''; + const corpId = item && typeof item.corpId === 'string' ? item.corpId : ''; + if (!corpId || !pharmacistNo) { + return { success: false, message: "参数错误" }; } - const pharmacistCAUserId = res.userId; if (![status.pass, status.unpass].includes(item.status)) { return { success: false, message: "状态错误" }; } - if (!pharmacistNo || !pharmacist) { - return { success: false, message: "审方药师信息不能为空" }; - } const ids = Array.isArray(item.ids) ? item.ids.filter(i => typeof i === 'string' && i.trim() !== '') : []; if (ids.length === 0) { return { success: false, message: "id错误" }; } + const yaoshi = await db.collection('hlw-doctor').findOne({ corpId, doctorNo: pharmacistNo }); + if (!yaoshi) { + return { success: false, message: "审方药师不存在" }; + } + const pharmacist = yaoshi.doctorName; + const pharmacistCAUserId = yaoshi.signatureUrl; const reasons = Array.isArray(item.reasons) ? item.reasons.filter((i) => typeof i === "string" && i.trim() !== "") : []; @@ -982,7 +928,6 @@ async function auditDiagnosis(item, db) { // 1. 根据待审核id 查询待处理的处方记录 const _rxList = await db.collection("diagnostic-record").find( { _id: { $in: ids.map((id) => new ObjectId(id)) }, status: status.init, pharmacistNo }, - // { projection: { orderId: 1, _id: 1, name: 1, sex: 1, age: 1, drugs: 1, prescriptionType: 1, diagnosisList: 1 } } ).toArray(); // 患者姓名 证件号 手机号解密 const rxList = _rxList.map(i => decryptDiagnosticRecordFields(i)); @@ -1101,21 +1046,10 @@ async function auditDiagnosis(item, db) { async function uploadRx2His(rx, order) { - const hisUploadFn = { - storeMedicinePurchase: 'hlwuploadprescription', - onlineMedicinePurchase: 'uploadMedicinePurchaseRx', - medicalWinePurchase: 'uploadWinePurchaseRx', - } - const formatFn = { - storeMedicinePurchase: formatUploadRxData, - onlineMedicinePurchase: formatOnlineMedicinePurchaseData, - medicalWinePurchase: formatWinePurchaseData, - } try { - const uploadData = formatFn[rx.prescriptionType](rx, order); - const type = hisUploadFn[rx.prescriptionType]; - const { success, message } = await zytHis({ - type, + const uploadData = formatUploadRxData(rx, order); + const { success, message } = await hnHis({ + type: 'hlwuploadprescription', data: uploadData }) return { success, message, id: rx._id } @@ -1867,7 +1801,7 @@ async function getAccountRxList(ctx) { } 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 query = { orderSource: "ALIPAY_MINI", accountId: ctx.accountId.trim(), status: status.pass }; + const query = { corpId: ctx.corpId, orderSource: "ALIPAY_MINI", accountId: ctx.accountId.trim(), status: status.pass }; const [list, total] = await Promise.all([ db.collection("diagnostic-record").find(query, { projection: types.output.accountRxList }).sort({ createTime: -1 }).skip((page - 1) * pageSize).limit(pageSize).toArray(), db.collection("diagnostic-record").countDocuments(query), @@ -1875,7 +1809,7 @@ async function getAccountRxList(ctx) { await getHisPrescriptionUploadStatus(list); // 获取处方单号 const rpNos = list.filter(i => i.prescriptionType === 'onlineMedicinePurchase').map(i => i._id.toString()); - const orderList = await db.collection("medicine-purchase-order").find({ rpNo: { $in: rpNos } }, { projection: types.output.medicinePurchaseOrder }).toArray(); + const orderList = await db.collection("medicine-purchase-order").find({ corpId: ctx.corpId, rpNo: { $in: rpNos } }, { projection: types.output.medicinePurchaseOrder }).toArray(); return { success: true, message: "查询成功", orderList, list: decryptDiagnosticRecordList(list), total, pages: Math.ceil(total / pageSize), now: Date.now() }; } catch (e) { return { success: false, message: `获取处方记录失败:${e?.message || '未知错误 '}` }; @@ -2337,4 +2271,4 @@ async function getPatientWineBuyCount(ctx, db) { } catch (e) { return { success: false, message: `查询患者购买酒类次数失败: ${e.message}` } } -} \ No newline at end of file +} diff --git a/hlw/download-xlsx/index.js b/hlw/download-xlsx/index.js index 0b50513..f817c94 100644 --- a/hlw/download-xlsx/index.js +++ b/hlw/download-xlsx/index.js @@ -1,6 +1,7 @@ const dayjs = require("dayjs"); const ExcelJS = require('exceljs'); const Sm4Util = require("../../utils/sm4-util"); +const { buildHybridSearchQuery } = require("../diagnostic-record/format"); module.exports = async (item, db, res) => { switch (item.downloadType) { @@ -87,6 +88,7 @@ async function downloadStream(db, res, options) { async function downloadRx(ctx, db, res) { const { startDate, endDate, startAuditDate, endAuditDate } = ctx; + const isStoreRxExport = ctx.prescriptionType === "storeMedicinePurchase"; const hasDates = startDate && dayjs(startDate).isValid() && endDate && dayjs(endDate).isValid(); const hasAuditDate = startAuditDate && dayjs(startAuditDate).isValid() && endAuditDate && dayjs(endAuditDate).isValid(); const query = {}; @@ -97,7 +99,8 @@ async function downloadRx(ctx, db, res) { query.auditTime = { $gte: dayjs(startAuditDate).startOf('day').valueOf(), $lte: dayjs(endAuditDate).endOf('day').valueOf() }; } if (typeof ctx.name === 'string' && ctx.name.trim() !== "") { - query.name = new RegExp(ctx.name); + const nameCondition = buildHybridSearchQuery("name", ctx.name, false); + if (nameCondition) Object.assign(query, nameCondition); } if (typeof ctx.doctorCode === 'string') { query.doctorCode = ctx.doctorCode.trim(); @@ -105,8 +108,47 @@ async function downloadRx(ctx, db, res) { if (typeof ctx.pharmacistNo === 'string' && ctx.pharmacistNo.trim() !== "") { query.pharmacistNo = ctx.pharmacistNo.trim(); } + const storeIds = [ctx.storeId, ctx.storeLoginNo] + .filter(value => typeof value === "string" && value.trim()) + .map(value => value.trim()); + if (isStoreRxExport && typeof ctx.storeId === "string" && ctx.storeId.trim()) { + const store = await db.collection("store-list").findOne( + { store_id: ctx.storeId.trim() }, + { projection: { login_no: 1 } } + ); + if (store && typeof store.login_no === "string" && store.login_no.trim()) { + storeIds.push(store.login_no.trim()); + } + } + if (storeIds.length) { + query.drugStoreNo = { $in: [...new Set(storeIds)] }; + } + if (isStoreRxExport) { + query.prescriptionType = "storeMedicinePurchase"; + } + if (typeof ctx.status === "string" && ctx.status.trim()) { + query.status = ctx.status.trim(); + } + if (typeof ctx.medOrgOrderNo === "string" && ctx.medOrgOrderNo.trim()) { + const keyword = ctx.medOrgOrderNo.trim().replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + query.medOrgOrderNo = { $regex: keyword, $options: "i" }; + } + if (["YB", "ZF"].includes(ctx.feeType)) { + query.feeType = ctx.feeType; + } - const columns = [ + const columns = isStoreRxExport ? [ + { header: '开具时间', key: 'createTime', width: 20 }, + { header: '费别', key: 'feeType', width: 10 }, + { header: '就诊人姓名', key: 'name', width: 18 }, + { header: '诊断', key: 'diagnosisList', width: 24 }, + { header: '开具药品', key: 'drugs', width: 36 }, + { header: '流转状态', key: 'transferStatus', width: 14 }, + { header: '处方编号', key: 'medOrgOrderNo', width: 24 }, + { header: '开方医生', key: 'doctorName', width: 14 }, + { header: '药品类型', key: 'drugType', width: 12 }, + { header: '打印状态', key: 'printStatus', width: 12 }, + ] : [ { header: '状态', key: 'status', width: 10 }, { header: '原因', key: 'reasons', width: 10 }, { header: '提交时间', key: 'createTime', width: 20 }, @@ -117,7 +159,7 @@ async function downloadRx(ctx, db, res) { { header: '诊断', key: 'diagnosisList', width: 10 }, { header: '药品', key: 'drugs', width: 10 }, ]; - let filename = '处方记录'; + let filename = isStoreRxExport ? '门店处方记录' : '处方记录'; if (hasDates) { filename += `_${dayjs(startDate).format('YYYY-MM-DD')}_${dayjs(endDate).format('YYYY-MM-DD')}`; } else if (hasAuditDate) { @@ -139,10 +181,21 @@ async function downloadRx(ctx, db, res) { collection: 'diagnostic-record', aggregateList: [ { $match: query }, - { $project: { _id: 0, status: 1, createTime: 1, reasons: 1, auditTime: 1, name: 1, doctorName: 1, pharmacist: 1, diagnosisList: 1, drugs: 1, orderId: 1, specification: 1 } }, + { $project: { _id: 0, status: 1, createTime: 1, reasons: 1, auditTime: 1, name: 1, sex: 1, age: 1, doctorName: 1, pharmacist: 1, diagnosisList: 1, drugs: 1, wines: 1, orderId: 1, medOrgOrderNo: 1, feeType: 1, hisUploadStatus: 1, prescriptionType: 1, printCount: 1 } }, { $sort: { createTime: -1 } }, ], - format: i => ({ + format: i => isStoreRxExport ? { + createTime: i.createTime ? dayjs(i.createTime).format("YYYY-MM-DD HH:mm:ss") : "", + feeType: i.feeType === "YB" ? "医保" : "自费", + name: [Sm4Util.decryptDataForSm3(i.name) || i.name, i.sex, i.age > 0 ? `${i.age}岁` : ""].filter(Boolean).join(" "), + diagnosisList: Array.isArray(i.diagnosisList) ? i.diagnosisList.map(d => d.name || '').join("、") : "", + drugs: Array.isArray(i.drugs) ? i.drugs.map(d => d.drugName || "").filter(Boolean).join("、") : "", + transferStatus: i.feeType !== "YB" ? "-" : i.hisUploadStatus === "uploaded" ? "流转完成" : i.hisUploadStatus === "error" ? "流转失败" : "流转中", + medOrgOrderNo: i.medOrgOrderNo || "", + doctorName: i.doctorName || "", + drugType: i.prescriptionType === "medicalWinePurchase" ? "中药" : "西药", + printStatus: Number(i.printCount) > 0 ? "已打印" : "未打印", + } : ({ status: rxStatusMap[i.status] || i.status, reasons: Array.isArray(i.reasons) ? i.reasons.join(",") : "", createTime: i.createTime ? dayjs(i.createTime).format("YYYY-MM-DD HH:mm:ss") : "", @@ -151,7 +204,7 @@ async function downloadRx(ctx, db, res) { pharmacist: i.pharmacist, name: Sm4Util.decryptDataForSm3(i.name)|| i.name, diagnosisList: Array.isArray(i.diagnosisList) ? i.diagnosisList.map(d => d.name || '').join(",") : "", - drugs: Array.isArray(i.drugs) ? i.drugs.map(d => `${d.drugName} ${i.specification || ''} (${d.dosage}${d.dosage_unit}, ${d.frequencyName}, ${d.days}天)`).join(",") : "", + drugs: Array.isArray(i.drugs) ? i.drugs.map(d => `${d.drugName} ${d.specification || ''} (${d.dosage}${d.dosage_unit}, ${d.frequencyName}, ${d.days}天)`).join(",") : "", }) } await downloadStream(db, res, options); diff --git a/hlw/drug-info/field-info.ts b/hlw/drug-info/field-info.ts index 00251d9..1253b0f 100644 --- a/hlw/drug-info/field-info.ts +++ b/hlw/drug-info/field-info.ts @@ -6,8 +6,7 @@ interface Drug { _id: ObjectId; // 数据库中的唯一标识符 pinyin_code: string; // 拼音码 name: string; // 药品名称 - product_id?: number; // 产品ID - product_id_str?: string; // 产品ID的字符串形式 + erpId: string; // ERP ID specification: string; // 药品规格 package_amount: number; // 包装数量 manufacturer: string; // 生产厂家 @@ -39,4 +38,4 @@ interface Drug { his_drug_code: string, // hisId generic_name: string;//通用名(商品名) manage_category: string;//管理类别 -} \ No newline at end of file +} diff --git a/hlw/drug-info/index.js b/hlw/drug-info/index.js index 8705881..3cca02b 100644 --- a/hlw/drug-info/index.js +++ b/hlw/drug-info/index.js @@ -3,6 +3,7 @@ const { ObjectId } = require("mongodb"); const { getDrugList } = require('./verify'); let db = ""; + module.exports = async (item, mongodb) => { db = mongodb; switch (item.type) { @@ -54,6 +55,7 @@ async function getDrugInfo(item) { { generic_name: new RegExp(keyword.trim(), "i") }, { barcode: keyword.trim() }, { pinyin_code: new RegExp(keyword.trim(), "i") }, + { erpId: keyword.trim() }, { product_id_str: keyword.trim() }, ]; fuzzyQuery = { $or: arr }; @@ -128,7 +130,7 @@ async function updateDrugInfo(item) { //新增药品信息 /** * pinyin_code:拼音操作码 - product_id:药店货品ID + erpId:ERP ID name:名称 specification:规格 manufacturer:厂家 @@ -177,8 +179,11 @@ async function getHlwDrugInfo(ctx) { if (typeof ctx.onSale === "boolean") { query.onSale = ctx.onSale } - if (typeof ctx.productId === 'string' && ctx.productId.trim()) { - query.product_id_str = ctx.productId.trim() + if (typeof ctx.erpId === 'string' && ctx.erpId.trim()) { + query.$or = [ + { erpId: ctx.erpId.trim() }, + { product_id_str: ctx.erpId.trim() } + ] } if (typeof ctx.barcode === 'string' && ctx.barcode.trim()) { query.barcode = ctx.barcode.trim() @@ -239,6 +244,20 @@ async function editHlwDrugInfo(ctx) { } try { const { onSale, ...data } = params; + const currentDrug = await db.collection("drug-info").findOne( + { _id: new ObjectId(id) }, + { projection: { erpId: 1, product_id: 1, product_id_str: 1 } } + ); + if (!currentDrug) { + return { success: false, message: "未找到对应药品" } + } + const legacyErpId = currentDrug.product_id_str || currentDrug.product_id; + const erpId = Object.prototype.hasOwnProperty.call(data, "erpId") + ? data.erpId + : (currentDrug.erpId || (legacyErpId || legacyErpId === 0 ? String(legacyErpId) : "")); + if (typeof erpId !== "string" || !erpId.trim()) { + return { success: false, message: "ERP ID不能为空" } + } let updateData = {}; if (Object.keys(data).length) { const peopleInfo = { @@ -366,7 +385,12 @@ async function getStoreSimilarDrugInfo(ctx) { })) similarDrugs = await db.collection("drug-info").find({ $or: similarQuery }).toArray(); } - return { success: true, message: "查询成功", drugs: medicines, similarDrugs } + return { + success: true, + message: "查询成功", + drugs: medicines, + similarDrugs: similarDrugs + } } catch (e) { return { success: false, message: e.message } } @@ -382,6 +406,7 @@ async function searchDrugs(ctx) { $or: [ { name: { $regex: keyword, $options: "i" } }, { pinyin_code: keyword }, + { erpId: keyword }, { product_id_str: keyword }, { barcode: keyword }, { insurance_code: keyword } @@ -392,4 +417,4 @@ async function searchDrugs(ctx) { } catch (e) { return { success: false, message: e.message } } -} \ No newline at end of file +} diff --git a/hlw/drug-info/verify.js b/hlw/drug-info/verify.js index 2033e0e..8e5c2d6 100644 --- a/hlw/drug-info/verify.js +++ b/hlw/drug-info/verify.js @@ -16,7 +16,8 @@ const drugFieldDict = [ { name: 'ERP编码', field: 'his_drug_code', verify: (val, label) => validator.verifyString(val, label) }, { name: '药品分类', field: 'category', verify: (val, label) => validator.verifyListItem(val, categoryList, false, label) }, { name: '药品拼音码', field: 'pinyin_code', verify: (val, label) => validator.verifyString(val, label) }, - { name: '药库货品ID', field: 'product_id', verify: (val, label) => validateNumber(val, label) }, + { name: 'ERP ID', field: 'erpId', required: true, libType: 'hlw', verify: (val, label) => validator.verifyString(val, label, 1) }, + { name: '药库货品ID', field: 'product_id', libType: 'online', verify: (val, label) => validateNumber(val, label) }, { name: '通用名', field: 'generic_name', verify: (val, label) => validator.verifyString(val, label) }, { name: '通用名拼音码', field: 'generic_pinyin', verify: (val, label) => validator.verifyString(val, label) }, // 仅在线药品库使用:管理分类(处方药 / OTC)与剂型 @@ -47,9 +48,10 @@ function validateNumber(val, label, len = 0, required = false) { return validator.verifyNumber(val, label, len) } -function getDrugList(arr, verifyEveryField = true, extraInfo = {}) { +function getDrugList(arr, verifyEveryField = true, extraInfo = {}, libType = 'hlw') { + const fieldDict = drugFieldDict.filter(item => !item.libType || item.libType === libType); return arr.map(drug => { - const res = drugFieldDict.reduce((obj, item) => { + const res = fieldDict.reduce((obj, item) => { const key = item.field; if (!verifyEveryField && !(key in drug)) { return obj @@ -64,7 +66,7 @@ function getDrugList(arr, verifyEveryField = true, extraInfo = {}) { } return obj }, { msgs: [] }) - if ('product_id' in res) { + if (libType === 'online' && 'product_id' in res) { res.product_id_str = res.product_id.toString(); } if ('frequency' in res) { @@ -74,12 +76,12 @@ function getDrugList(arr, verifyEveryField = true, extraInfo = {}) { }) } -function getDrugFromChineseKeyObj(data) { +function getDrugFromChineseKeyObj(data, libType = 'hlw') { const rowData = {}; Object.keys(data).forEach(key => { rowData[key.trim()] = data[key]; }) - return drugFieldDict.reduce((obj, item) => { + return drugFieldDict.filter(item => !item.libType || item.libType === libType).reduce((obj, item) => { obj[item.field] = rowData[item.name] ?? ''; return obj; }, {}); diff --git a/hlw/hlw-doctor/document.ts b/hlw/hlw-doctor/document.ts index 2eaddb6..e6cdcbd 100644 --- a/hlw/hlw-doctor/document.ts +++ b/hlw/hlw-doctor/document.ts @@ -17,6 +17,7 @@ interface HlwDoctor { job: Job; offlineTime?: number; // 上次下线时间 testDoctor: boolean; // 是否为测试医生 + signatureUrl?: string; // 医生签名图片地址 openWestPres: boolean; // 是否开启西药处方 openChinesePres: boolean; // 是否开启中药处方 } diff --git a/hlw/hlw-doctor/hlw-people-utils.js b/hlw/hlw-doctor/hlw-people-utils.js index 0043b53..fd2363c 100644 --- a/hlw/hlw-doctor/hlw-people-utils.js +++ b/hlw/hlw-doctor/hlw-people-utils.js @@ -69,6 +69,10 @@ const rules = [ // 是否开通视频问诊功能:允许 true / false,默认 false validate: val => validator.verifyListItem(val, [true, false], false, '视频问诊功能', false) }, + { + key: 'signatureUrl', + validate: val => validator.verifyString(val, '签名图片', 0, 500) + }, { key: 'openWestPres', validate: val => validator.verifyListItem(val, [true, false], false, '西医处方', false) diff --git a/hlw/hlw-doctor/index.js b/hlw/hlw-doctor/index.js index 802df39..1c26d54 100644 --- a/hlw/hlw-doctor/index.js +++ b/hlw/hlw-doctor/index.js @@ -598,10 +598,7 @@ async function setPharmacistOnlineStatus(ctx) { if (res.matchedCount === 0) { return { success: false, message: "药师不存在或不是药师" } } - if (res.modifiedCount === 1) { - return { success: true, message: "在线状态修改成功", data: ctx.onlineStatus } - } - return { success: false, message: "在线状态修改失败" } + return { success: true, message: "在线状态修改成功", data: ctx.onlineStatus } } catch (e) { return { success: false, diff --git a/hlw/hlw-patient/index.js b/hlw/hlw-patient/index.js index 9e1604c..c6ea1c9 100644 --- a/hlw/hlw-patient/index.js +++ b/hlw/hlw-patient/index.js @@ -1,6 +1,7 @@ const { ObjectId } = require('mongodb'); const validator = require('../../utils/validator'); const Sm4Util = require('../../utils/sm4-util'); +const hnHis = require('../hn-his'); const COLLECTION_NAME = 'hlw-patient'; @@ -55,6 +56,12 @@ function validatePatient(item) { return { success: true, + origin: { + name, + certNo, + mobile, + address, + }, data: { accountId: accountResult.accountId, anotherIdNo: Sm4Util.encryptDataForSm3(certNo), @@ -78,6 +85,16 @@ async function addHlwPatient(item, db) { if (exists > 0) { return { success: false, message: '当前账户下已存在相同证件号的就诊人' }; } + console.log(validation.data) + + const res = await hnHis({ + type: 'addHisCustomer', + name: validation.origin.name, + idCard: validation.origin.certNo, + mobile: validation.origin.mobile, + address: validation.origin.address, + }) + if (!res.success) return res; const result = await db.collection(COLLECTION_NAME).insertOne({ ...validation.data, diff --git a/hlw/hlw-patient/index.test.js b/hlw/hlw-patient/index.test.js deleted file mode 100644 index 3fedac5..0000000 --- a/hlw/hlw-patient/index.test.js +++ /dev/null @@ -1,185 +0,0 @@ -process.env.CONFIG_ZYB_YB_APPID = '1234567890123456'; -process.env.CONFIG_ZYB_YB_SECRET = 'hlw-patient-test-secret'; - -const { ObjectId } = require('mongodb'); -const patientApi = require('./index'); - -function matches(document, query) { - return Object.entries(query).every(([key, value]) => { - if (key === '_id') return document._id.toString() === value.toString(); - return document[key] === value; - }); -} - -function createDb() { - const documents = []; - return { - documents, - collection: jest.fn(() => ({ - insertOne: async document => { - const insertedId = new ObjectId(); - documents.push({ _id: insertedId, ...document }); - return { insertedId }; - }, - countDocuments: async query => documents.filter(document => matches(document, query)).length, - find: query => ({ - sort: () => ({ - toArray: async () => documents.filter(document => matches(document, query)), - }), - }), - findOne: async query => documents.find(document => matches(document, query)) || null, - updateOne: async (query, update) => { - const document = documents.find(item => matches(item, query)); - if (!document) return { matchedCount: 0, modifiedCount: 0 }; - Object.assign(document, update.$set); - return { matchedCount: 1, modifiedCount: 1 }; - }, - })), - }; -} - -describe('hlw-patient', () => { - test('新增、查询、更新并软删除就诊人', async () => { - const db = createDb(); - const patient = { - accountId: 'account-1', - name: '张三', - certNo: '330101199001011234', - mobile: '13800138000', - }; - - const added = await patientApi.main({ type: 'addHlwPatient', ...patient }, db); - expect(added.success).toBe(true); - expect(db.documents[0]).toMatchObject({ - accountId: 'account-1', - address: '', - disabled: false, - }); - expect(db.documents[0].anotherName).not.toBe(patient.name); - expect(db.documents[0].anotherIdNo).not.toBe(patient.certNo); - expect(db.documents[0].anotherMobile).not.toBe(patient.mobile); - expect(db.documents[0].createTime).toBeInstanceOf(Date); - - const queried = await patientApi.main({ - type: 'getHlwPatientList', - accountId: 'account-1', - }, db); - expect(queried.data).toEqual([expect.objectContaining(patient)]); - - const detail = await patientApi.main({ - type: 'getHlwPatient', - accountId: 'account-1', - id: added.id, - }, db); - expect(detail).toMatchObject({ - success: true, - data: patient, - }); - - const otherAccountDetail = await patientApi.main({ - type: 'getHlwPatient', - accountId: 'account-2', - id: added.id, - }, db); - expect(otherAccountDetail.success).toBe(false); - - const updated = await patientApi.main({ - type: 'updateHlwPatient', - ...patient, - id: added.id, - name: '李四', - mobile: '13900139000', - address: '杭州市', - }, db); - expect(updated.success).toBe(true); - - const updatedList = await patientApi.main({ - type: 'getHlwPatientList', - accountId: 'account-1', - }, db); - expect(updatedList.data[0]).toMatchObject({ - name: '李四', - mobile: '13900139000', - certNo: patient.certNo, - address: '杭州市', - }); - - const deleted = await patientApi.main({ - type: 'deleteHlwPatient', - accountId: 'account-1', - id: added.id, - }, db); - expect(deleted.success).toBe(true); - expect(db.documents[0].disabled).toBe(true); - - const emptyList = await patientApi.main({ - type: 'getHlwPatientList', - accountId: 'account-1', - }, db); - expect(emptyList.data).toEqual([]); - }); - - test('拒绝格式错误的手机号', async () => { - const db = createDb(); - const result = await patientApi.main({ - type: 'addHlwPatient', - accountId: 'account-1', - name: '张三', - certNo: '330101199001011234', - mobile: '12345', - }, db); - - expect(result.success).toBe(false); - expect(db.documents).toHaveLength(0); - }); - - test('同一账户下不能新增相同证件号的就诊人', async () => { - const db = createDb(); - const patient = { - type: 'addHlwPatient', - accountId: 'account-1', - name: '张三', - certNo: '330101199001011234', - mobile: '13800138000', - }; - - const first = await patientApi.main(patient, db); - const duplicate = await patientApi.main({ - ...patient, - name: '李四', - mobile: '13900139000', - }, db); - - expect(first.success).toBe(true); - expect(duplicate).toEqual({ - success: false, - message: '当前账户下已存在相同证件号的就诊人', - }); - expect(db.documents).toHaveLength(1); - - const otherAccount = await patientApi.main({ - ...patient, - accountId: 'account-2', - }, db); - expect(otherAccount.success).toBe(true); - expect(db.documents).toHaveLength(2); - }); - - test('自动新增接口对同一账号和证件号保持幂等', async () => { - const db = createDb(); - const request = { - type: 'autoAddHlwPatient', - accountId: 'account-1', - name: '张三', - certNo: '330101199001011234', - mobile: '13800138000', - }; - - const first = await patientApi.main(request, db); - const second = await patientApi.main(request, db); - - expect(first).toMatchObject({ success: true, data: true }); - expect(second).toMatchObject({ success: true, data: true }); - expect(db.documents).toHaveLength(1); - }); -}); diff --git a/hlw/hn-his/index.js b/hlw/hn-his/index.js new file mode 100644 index 0000000..95a1bf0 --- /dev/null +++ b/hlw/hn-his/index.js @@ -0,0 +1,942 @@ +const dayjs = require("dayjs"); +const { decode } = require('iconv-lite'); +const request = require("../../request"); + +// const baseUrl = "http://115.239.161.48:8089"; //his说 8089可能会关闭,用8083 +const baseUrl = process.env.CONFIG_HN_HIS_BASE_URL; +const url = `${baseUrl}/getYoucanData/hlw`; + +const AppWin = { + line: '线上挂号', + win: '线下挂号', +} + +module.exports = async (item) => { + switch (item.type) { + case "getHisCustomer": + return await getHisCustomer(item); + case "addHisCustomer": + return await addHisCustomer(item); + case "registration": + return await registration(item); + case "getPayStatus": + return await getPayStatus(item); + case "hlwRefund": + return await hlwRefund(item); + case "hlwuploadprescription": + return await hlwuploadprescription(item); + case "hlwPrescriptionOrderStatus": + return await hlwPrescriptionOrderStatus(item); + case "getHisPrescriptionUploadStatus": + return await getHisPrescriptionUploadStatus(item); + case "getHisStoreRegList": + return await getHisStoreRegList(item); + case "insurancePreSettle": + return await insurancePreSettle(item); + case "insuranceSettle": + return await insuranceSettle(item); + case "insuranceRefund": + return await insuranceRefund(item); + case "getRecent90daysDrugRecord": + return await getRecent90daysDrugRecord(item); + case "uploadMedicinePurchaseRx": + return await uploadMedicinePurchaseRx(item); + case "uploadWinePurchaseRx": + return await uploadWinePurchaseRx(item); + case "getHisDrugInfo": + return await getHisDrugInfo(item); + case "getHisMedicinePurchase": + return await getHisMedicinePurchase(item); + case "cancelMedicinePurchase": + return await cancelMedicinePurchase(item); + case "refundMedicinePurchase": + return await refundMedicinePurchase(item); + case "getHisInvoice": + return await getHisInvoice(item); + case "updateMedicinePurchaseDelivery": + return await updateMedicinePurchaseDelivery(item); + case 'syncHisChargeStatus': + return await syncHisChargeStatus(item); + case "hisTest": + return await hisTest(item); + case "getHisEmployeeInfo": + return await getHisEmployeeInfo(item); + case "syncHisRefundStatus": + return await syncHisRefundStatus(item); + case "getWineDrugStockInfo": + return await getWineDrugStockInfo(item); + case "ybReviewPrescription": + return await ybReviewPrescription(item); + } +}; + +async function getHisCustomer(ctx) { + const idCard = typeof ctx.idCard === 'string' ? ctx.idCard.trim() : ''; + if (!idCard) { + return { success: false, message: '证件号不能为空' } + } + try { + const params = { type: "getHisCustomer", idCard }; + const { data, status_code, message } = await request.main(url, params, "POST"); + if (status_code == 200 && !message) { + return { + success: true, + list: data?.patients, + patient: data?.patients?.[0], + tmbxx: Array.isArray(data?.tmbxx) ? data?.tmbxx : [], + message: `[his]查询档案:获取his档案成功`, + }; + } else { + return { + success: false, + message: message || "获取失败", + }; + } + } catch (err) { + return { + success: false, + message: err.message || "获取失败", + }; + } +} +/** + * + * @param {buildType} 建档类型 1.自费建档 2.电子凭证建档 3.线下医保建档 + * @returns + */ +async function addHisCustomer(item) { + // let { idCard, name, mobile, address, psnToken, buildType = '1', mdtrt_cert_type, mdtrt_cert_no, card_sn } = item; + try { + const name = typeof item.name === 'string' ? item.name.trim() : ''; + const idCard = typeof item.idCard === 'string' ? item.idCard.trim() : ''; + const mobile = typeof item.mobile === 'string' ? item.mobile.trim() : ''; + 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 card_sn = typeof item.card_sn === 'string' ? item.card_sn.trim() : ''; + if (!name || !idCard || !mobile) { + return { success: false, message: '姓名、证件号、手机号不能为空' } + } + const payload = { + type: 'addHisCustomer', + name, + idCard, + mobile, + address, + buildType: '1', // 1.自费建档 2.电子凭证建档 3.线下医保 + }; + // 电子凭证建档 医保插件授权建档 + if (psnToken) { + 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) { + payload.mdtrt_cert_type = '03'; + payload.mdtrt_cert_no = mdtrt_cert_no; + payload.card_sn = card_sn; //就诊凭证类型为“03”时填写社会保障卡内码 + } + + const { data, status_code, message } = await request.main( + url, + payload, + "POST" + ); + if (status_code == 200 && !message) { + return { + success: true, + list: data?.patients, + tmbxx: Array.isArray(data?.tmbxx) ? data?.tmbxx : [], + message: "获取成功", + }; + } else { + return { + success: false, + message: message || "获取失败", + }; + } + } catch (err) { + return { + success: false, + message: "获取失败", + }; + } +} + +// 6.外配处方回写 +/** + * + * @param {*} param0 + * @returns + */ +async function hlwuploadprescription(item) { + try { + const params = { type: "hlwuploadprescription", data:item.data }; + + const { code, status_code, message } = await request.main(url, params, "POST"); + + if (status_code == 200 && code == '1') { + return { + success: true, + message: "[his]处方上传成功", + }; + } else { + return { + success: false, + message: `[his]处方上传失败: ${message || '未知错误'}`, + }; + } + } catch (err) { + return { + success: false, + message: `[his]处方上传错误: ${err.message || '未知错误'}`, + }; + } +} + +// 预约挂号 +async function registration(item) { + const { patientId, doctorCode, appwin = 'line', unitCode, registerId, chargecode, reg_fee, shopid, shopname, op_id = '', channel } = item; + if (AppWin[appwin] == undefined) { + return { + success: false, + message: "appwin参数错误" + } + } + if (AppWin[appwin] == '线下挂号' && (typeof op_id !== 'string' || op_id.trim() === '')) { + return { + success: false, + message: "op_id参数错误" + } + } + try { + const url = `${baseUrl}/his/hlw/registration`; + // 根据dayjs 判断是上午 还是下午 + const ampm = dayjs().hour() < 12 ? "a" : "p"; + const requestday = dayjs().format("YYYY-MM-DD HH:mm:ss"); + // registerId 为 H开头 15位随机数 + const payload = { + patientId, + doctorCode, + unitCode, + requestday, + ampm, + registerId, + chargecode, + reg_fee, + shopid, + shopname, + chargetype: "普通", + appwin + }; + if (AppWin[appwin] == '线下挂号') { + payload.op_id = op_id; + } + if (channel === 1) { + payload.channel = 1 + } + const { data, status_code, message } = await request.main( + url, + payload, + "POST" + ); + if (data && data.register && data.register.medorg_order_no) { + return { + success: true, + data, + message: message || "挂号成功", + }; + } else { + return { + success: false, + message: message || "挂号失败", + }; + } + } catch (err) { + return { + success: false, + message: err.message || "挂号失败" + }; + } +} + +//订单支付状态查询 +async function getPayStatus(item) { + const { patientId, registerId, medorgOrderNo } = item; + try { + const url = `${baseUrl}/his/hlw/settlement`; + const { status, status_code, message, ...rest } = await request.main( + url, + { + patientid: patientId, + registerid: registerId, + medorg_order_no: medorgOrderNo, + }, + "POST" + ); + const settlement = + rest.data && rest.data.settlement ? rest.data.settlement : null; + //settlement.status 0 已收费 1 已退费 5 未收费 x 已作废 + if (status_code == 200) { + return { + success: true, + status: settlement ? settlement.status : '', + settlement, + message: "订单查询成功", + }; + } else { + return { + success: false, + message: message || "获取失败", + }; + } + } catch (err) { + return { + success: false, + message: "获取失败", + }; + } +} + +//医保患者退费 +async function hlwRefund(item) { + const { patientId, registerId, medorgOrderNo } = item; + try { + const url = `${baseUrl}/his/hlw/refund`; + const { data, status_code, message, ...rest } = await request.main( + url, + { + patientid: patientId, + registerid: registerId, + medorg_order_no: medorgOrderNo, + }, + "POST" + ); + if (status_code == 200 && !message) { + return { + success: true, + message: "退费成功", + }; + } else { + return { + success: false, + message: message || "退费失败", + }; + } + } catch (err) { + return { + success: false, + message: "退费失败", + }; + } +} + + + +// 外配处方状态查询 +async function hlwPrescriptionOrderStatus(item) { + const { patientId, orderno } = item; + try { + const url = `${baseUrl}/his/hlw/orderstatus`; + const { status_code, message, data } = await request.main( + url, + { + patientid: patientId, + orderno, + }, + "POST" + ); + if (status_code == 200 && data && data.orderstatus) { + return { + success: true, + uploaded: data.orderstatus.status == '1', + status: data.orderstatus.status, + message: "查询成功", + }; + } else { + return { + success: false, + message: message || "查询失败", + }; + } + } catch (err) { + return { + success: false, + message: "查询失败", + }; + } +} + +async function getHisPrescriptionUploadStatus(item) { + const { patientId, orderno, _id } = item; + try { + const url = `${baseUrl}/his/hlw/orderstatus`; + const { status_code, message, data } = await request.main( + url, + { + patientid: patientId, + orderno, + }, + "POST" + ); + if (status_code == 200 && data && data.orderstatus && data.orderstatus.status == 1) { + return { orderno, hisUploadStatus: 'uploaded', _id } + } + if (status_code == 200 && data && data.orderstatus && data.orderstatus.status == -1) { + return { orderno, hisUploadStatus: 'error', _id } + } + if (status_code == 200 && data && data.orderstatus && data.orderstatus.status == 0) { + return { orderno, hisUploadStatus: 'notUploaded', _id } + } + return { + success: false, + message: message || "查询失败", + + }; + } catch (err) { + return { + success: false, + message: "查询失败", + }; + } + +} + +async function getHisStoreRegList(item) { + const { storeId: shopid } = item; + if (typeof shopid !== 'string' || shopid.trim() === '') { + return { + success: false, + message: "店铺id无效", + }; + } + try { + const url = `${baseUrl}/his/hlw/registerquery`; + const { status_code, message, data } = await request.main( + url, + { + shopid, + requestday: dayjs().format('YYYY-MM-DD HH:mm:ss'), + status: "0", + starttime: dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss'), + endtime: dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'), + hospid: "108", + }, + "POST" + ); + if (status_code == 200 && Array.isArray(data)) { + return { + success: true, + list: data, + message: "查询成功", + }; + } else { + return { + success: false, + message: message || "查询失败", + }; + } + } catch (err) { + return { + success: false, + message: "查询失败", + }; + } +} + +async function insurancePreSettle(ctx) { + const { + medorg_order_no: orderno, + patientId: patientid, + mdtrtCertNo: mdtrt_cert_no, + mdtrtCertType: mdtrt_cert_type, + cardSn: card_sn, + operator_code, + operator_name, + shopid, + hospid = "108" + } = ctx; + try { + const url = `${baseUrl}/his/hlw/presettle`; + const payload = { orderno, patientid, mdtrt_cert_no, mdtrt_cert_type, card_sn, operator_code, operator_name, hospid, shopid }; + const { data, status_code, message } = await request.main(url, payload, "POST"); + + if (status_code == 200 && !message) { + return { success: true, data, message: '预结算成功' } + } + return { success: false, message: message || '预结算失败' } + } catch (e) { + return { success: false, message: e.message || '预结算失败' } + } +} + +async function insuranceSettle(ctx) { + const { + patientid, + orderno, + mdtrt_id, + mdtrt_cert_no, + outparm_1101, + outparm_2206A, + inparm_2207A, + outparm_2204, + totalamount, + selfpay, + operator_code, + operator_name + } = ctx; + const payload = { + patientid, + orderno, + settle_method: '7', + paymethod: '1', // 1: 现金 w: 支付宝 z: 微信 + paysn: '', + payrequestsn: '', + mdtrt_id, + mdtrt_cert_no, + outparm_1101, + outparm_2206A, + inparm_2207A, + outparm_2204, + totalamount, + selfpay, + operator_code, + operator_name + } + try { + const url = `${baseUrl}/his/hlw/ybsettle`; + // const url = `${baseUrl}/his/hlw/presettle`; + const { data, status_code, message } = await request.main(url, payload, "POST"); + if (status_code == 200 && !message) { + return { success: true, data, message: '结算成功' } + } + return { success: false, message: message || '结算失败' } + } catch (e) { + return { success: false, message: e.message || '结算失败' } + } +} + +async function insuranceRefund(ctx) { + const { type, ...payload } = ctx; + try { + const url = `${baseUrl}/his/hlw/ybrefund`; + // const url = `${baseUrl}/his/hlw/presettle`; + const { data, status_code, message } = await request.main(url, payload, "POST"); + if (status_code == 200 && !message) { + return { success: true, data, message: '退费成功' } + } + return { success: false, message: message || '退费失败' } + } catch (e) { + return { success: false, message: e.message || '退费失败' } + } +} + +async function getRecent90daysDrugRecord(ctx) { + const patientid = typeof ctx.patientId === 'string' ? ctx.patientId.trim() : ''; + if (patientid === '') { + return { success: false, message: '患者ID不能为空' } + } + try { + const url = `${baseUrl}/his/hlw/patientzj3100`; + const payload = { patientid, hospid: "108", doctorCode: '10021' }; + if (typeof ctx.doctorcode === 'string' && ctx.doctorcode.trim() !== '') { + payload.doctorcode = ctx.doctorcode.trim(); + } + const { data, status_code, message } = await request.main(url, payload, "POST"); + + + if (status_code == 200 && !message && data && data.outparm_zj3100) { + // 解析医保用药数据列表 + const decodedBuffer = Buffer.from(data.outparm_zj3100, 'base64'); + const gbkDecodedStr = decode(decodedBuffer, 'gbk'); + const res = JSON.parse(gbkDecodedStr); + const drugList = res && res.data && Array.isArray(res.data.lists) ? res.data.lists.map(i => ({ + hiListName: i.hiListName, + insuranceCode: i.hilistCode, + mdtrtId: i.mdtrtId, + quantity: i.cnt, + spec: i.spec && i.spec !== 'null' ? i.spec : '', + unit: i.unit && i.unit !== 'null' ? i.unit : '' + })) : []; + + // 解析医保记录 + const decodedBuffer1 = Buffer.from(data.outparm_zj3101, 'base64'); + const gbkDecodedStr1 = decode(decodedBuffer1, 'gbk'); + const res1 = JSON.parse(gbkDecodedStr1); + const recordList = res1 && res1.data && Array.isArray(res1.data.lists) ? res1.data.lists.map(i => ({ + mdtrtId: i.mdtrtId, + date: i.admDate, + medinsName: i.medinsName, + })) : []; + + return { success: true, drugList, message: '查询成功', recordList } + } + return { success: false, message: message || '查询失败' } + } catch (e) { + return { success: false, message: e.message || '查询失败' } + } +} + +async function getHisDrugInfo(item) { + try { + const ignoreAreaCode = typeof item.ignoreAreaCode === 'boolean' ? item.ignoreAreaCode : false; + if (!ignoreAreaCode && (typeof item.areaCode !== 'string' || item.areaCode.trim() === '')) { + return { success: false, message: '区域代码不能为空' } + } + const showAll = typeof item.showAll === 'boolean' ? item.showAll : false; + const gjbm = Array.isArray(item.insuranceCodes) ? item.insuranceCodes.filter(i => typeof i === 'string' && i.trim() !== '') : []; + console.log('gjbm: ', gjbm) + if (gjbm.length === 0 && !showAll) { + return { success: false, message: '医保编码不能为空' } + } + const url = `${baseUrl}/his/hlw/drug`; + const payload = { + hospid: '108', + drug_type: 'xy', + area_code: typeof item.areaCode === 'string' ? item.areaCode.trim() : '', + gjbm: gjbm.join(',') + } + const { code, status_code, message, data } = await request.main( + url, + payload, + "POST" + ); + if (status_code == 200 && code == '1' && !message) { + return { + success: true, + message: "[his]查询药品信息成功", + data: data && Array.isArray(data.drugs) ? data.drugs : [] + }; + } else { + return { + success: false, + message: `[his]查询药品信息传失败: ${message || '未知错误'}`, + }; + } + } catch (err) { + return { + success: false, + message: `[his]查询药品信息传错误: ${err.message || '未知'}`, + }; + } +} + +async function uploadMedicinePurchaseRx(item) { + try { + const { data } = item; + const url = `${baseUrl}/his/hlw/mzordersave`; + const { code, status_code, message } = await request.main( + url, + data, + "POST" + ); + if (status_code == 200 && code == '1' && !message) { + return { + success: true, + message: "[his]购药订单处上传成功", + }; + } else { + return { + success: false, + message: `[his]购药订单处方上传失败: ${message || '未知错误'}`, + }; + } + } catch (err) { + return { + success: false, + message: `[his]购药订单处方上传错误: ${err.message || '未知'}`, + }; + } +} + +async function uploadWinePurchaseRx(item) { + try { + const { data } = item; + const url = `${baseUrl}/his/hlw/tcsave`; + const { code, status_code, message } = await request.main( + url, + data, + "POST" + ); + console.log('data', code, status_code, message); + if (status_code == 200 && code == '1' && !message) { + return { + success: true, + message: "[his]浸酒处上传成功", + }; + } else { + return { + success: false, + message: `[his]浸酒处方上传失败: ${message || '未知错误'}`, + }; + } + } catch (err) { + return { + success: false, + message: `[his]浸酒处方上传错误: ${err.message || '未知'}`, + }; + } +} + +// hlwuploadprescription +async function ybReviewPrescription(item) { + try { + const url = `${baseUrl}/his/hlw/ybsh`; + const { code, data, status_code, message } = await request.main( + url, + item.data, + "POST" + ); + if (status_code == 200 && code == '1' && !message) { + const str = Buffer.from(data, 'base64'); + const dataStr = decode(str, 'gbk'); + const strData = JSON.parse(dataStr); + const result = strData && strData.output && Array.isArray(strData.output.result) ? strData.output.result.filter(Boolean) : []; + const advices = result.map(i => ({ + name: typeof i.rule_name === 'string' ? i.rule_name.trim() : '', + text: typeof i.vola_cont === 'string' ? i.vola_cont.trim() : '', + })).filter(i => i.name || i.text) + return { + success: true, + advices, + message: "[his]医保审核成功", + }; + } else { + return { + success: false, + message: `[his]医保审核失败: ${message || '未知错误'}`, + }; + } + } catch (err) { + return { + success: false, + message: `[his]医保审核错误: ${err.message || '未知错误'}`, + }; + } +} + + +async function getHisMedicinePurchase({ patientId, orderNo }) { + try { + const url = `${baseUrl}/his/hlw/orderquery`; + const { code, status_code, message, data } = await request.main(url, { patientid: patientId, orderno: orderNo }, "POST"); + const order = data && Array.isArray(data.orders) ? data.orders[0] : null; + if (status_code == 200 && code == '1' && !message) { + if (order) { + return { success: true, data: order, message: '查询成功' } + } + return { success: false, message: `[his]处方订单查询失败: 未找到订单` } + } + return { + success: false, + message: `[his]处方订单查询失败: ${message || '未知错误'}`, + }; + } catch (err) { + return { success: false, message: `[his]处方订单查询错误: ${err.message || '未知错误'}` }; + } +} + +async function cancelMedicinePurchase({ patientId: patientid, orderNo: orderno }) { + try { + const url = `${baseUrl}/his/hlw/ordercancel`; + const { code, status_code, message } = await request.main(url, { patientid, orderno }, "POST"); + if (status_code == 200 && code == '1' && !message) { + return { success: true, message: '[his]取消购药订单处方成功' } + } + return { success: false, message: `[his]取消购药订单处方失败: ${message || '未知错误'}` } + } catch (err) { + return { success: false, message: `[his]取消购药订单处方错误: ${err.message || '未知错误'}` }; + } +} + +async function refundMedicinePurchase({ patientId: patientid, orderNo: orderno }) { + try { + const url = `${baseUrl}/his/hlw/orderrefund`; + const { code, status_code, message } = await request.main(url, { patientid, orderno }, "POST"); + if (status_code == 200 && code == '1' && !message) { + return { success: true, message: '[his]退费购药订单处方成功' } + } + return { success: false, message: `[his]退费购药订单处方失败: ${message || '未知错误'}` } + } catch (err) { + return { success: false, message: `[his]退费购药订单处方错误: ${err.message || '未知错误'}` }; + } +} + +async function getHisInvoice(item) { + try { + const { patientId, orderNo } = item; + const url = `${baseUrl}/his/hlw/dzfp`; + let { code, status_code, message, data } = await request.main(url, { patientid: patientId, orderno: orderNo }, "POST"); + if (status_code == 200 && code == '1' && !message) { + const dzfp = data && Array.isArray(data.dzfp) ? data.dzfp[0] : null; + return { success: true, data: dzfp && dzfp.dzfp_url ? dzfp.dzfp_url : '', message: '发票查询成功' } + } + return { success: false, message: `[his]发票查询失败: ${message || '未知错误'}` } + } catch (err) { + return { success: false, message: `[his]发票查询错误: ${err.message || '未知错误'}` }; + } +} + +async function updateMedicinePurchaseDelivery(item) { + try { + const { + patientId: patientid, + orderNo: orderno, + backFlag: back_flag = 0, + shippingType: deliv_type, + shippingNo: deliv_no, + shippingFee: deliv_fee, + pickupCode: tracking_no, + payTime: pay_dt, + tradeNo: pay_orderno, + receiveName: shipp_name, + receivePhone: shipp_tel, + fullAddress: shipp_addr, + invoice + } = item; + const payload = { + patientid, + orderno, + back_flag, // 0 收取运费 | 1 退还运费 + deliv_type, + deliv_no, + deliv_fee, + pay_dt, + pay_orderno, + shipp_name, + shipp_tel, + shipp_addr, + tracking_no, + invoice + } + const url = `${baseUrl}/his/hlw/delivery`; + let { code, status_code, message, data } = await request.main(url, payload, "POST"); + if (status_code == 200 && code == '1' && !message) { + return { success: true, data, message: '[his]同步配药订单物流信息成功' } + } + return { success: false, message: `[his]同步配药订单物流信息失败: ${message || '未知错误'}` } + } catch (err) { + return { success: false, message: `[his]同步配药订单物流信息错误: ${err.message || '未知错误'}` }; + } +} + +async function syncHisChargeStatus(ctx) { + try { + const payload = { + patientid: ctx.patientId, + orderno: ctx.orderNo, + settle_method: 1, + paymethod: 'h', + paysn: ctx.tradeNo, + payrequestsn: ctx.outTradeNo, + totalamount: ctx.totalAmount, + selfpay: ctx.totalAmount, + operator_code: 'hlw', + operator_name: '互联网医院' + } + const url = `${baseUrl}/his/hlw/ybsettle`; + let { code, status_code, message, data } = await request.main(url, payload, "POST"); + if (status_code == 200 && code == '1' && !message) { + return { success: true, data, message: '[his]同步浸酒订单收费成功' } + } + return { success: false, message: `[his]同步浸酒订单收费失败: ${message || '未知错误'}` } + } catch (err) { + return { success: false, message: `[his]同步浸酒订单收费错误: ${err.message || '未知错误'}` }; + } +} + +async function syncHisRefundStatus(ctx) { + try { + const payload = { + patientid: ctx.patientId, + orderno: ctx.orderNo, + ptlsh: ctx.refundSerialNo + } + const url = `${baseUrl}/his/hlw/orderrefund`; + let { code, status_code, message, data } = await request.main(url, payload, "POST"); + if (status_code == 200 && code == '1' && !message) { + return { success: true, data, message: '[his]同步浸酒订单退费成功' } + } + return { success: false, message: `[his]同步浸酒订单退费失败: ${message || '未知错误'}` } + } catch (err) { + return { success: false, message: `[his]同步浸酒订单退费错误: ${err.message || '未知错误'}` }; + } +} + + +async function getHisEmployeeInfo(item) { + const { code } = item; + try { + const testUrl = "http://115.239.161.48:8089"; + const url = `${testUrl}/his/hlw/employeeinfo`; + const { data, status_code, message } = await request.main( + url, + { type: "01", code: code }, + "POST" + ); + if (status_code == 200 && !message) { + return { + success: true, + data, + message: "获取成功", + } + } + } catch (e) { + return { success: false, message: e?.message } + } +} + +async function getWineDrugStockInfo(item) { + try { + if (typeof item.code !== 'string' || item.code.trim() === '') { + return { success: false, message: '中药饮片代码不能为空' } + } + if (!(Number.isInteger(item.num) && item.num > 0)) { + return { success: false, message: '中药饮片数量必须是整数且大于0' } + } + const url = `${baseUrl}/his/hlw/drug`; + const payload = { + hospid: '108', + drug_type: 'zcy', + tc_code: item.code.trim(), + tc_number: item.num + } + const { code, status_code, message, data } = await request.main( + url, + payload, + "POST" + ); + if (status_code == 200 && code == '1') { + return { + success: true, + message: "[his]查询药品信息成功", + data: data + }; + } + return { + success: false, + message: `中药饮品查询失败: ${message || '未知错误'}`, + }; + + } catch (err) { + return { + success: false, + message: `中药饮品查询失败错误: ${err.message || '未知'}`, + }; + } +} + +async function hisTest() { + // const data = { + // patientId: '10428622', + // orderNo: 'R260530160013084765664', + // tradeNo: '3JHR2605301770353161405592', + // outTradeNo: '3JHR260530160352836640789', + // totalAmount: 78 + // } + // const data = { "patientid": "30015424", "registerid": "h202501070833145550001", "orderno": "h202501070833145550001", "doctorcode": "10010", "unitcode": "1005000", "visit_time": "2025-12-04 13:15", "area_code": "330602", "hospid": "108", "icds": [{ "icd_code ": "R51.x00", "icd_name": "头痛", "sort": 1 }], "details": [{ "drug_type": "xy", "drug_groupno": "7X", "drug_group": "", "drug_id": "00110", "drug_serial": "01", "drug_name": "富马酸替诺福韦二吡呋酯片", "drug_spec": "300mg×30片/盒", "drug_pric": "329.41", "drug_cnt": "1", "drug_sumamt": "329.41", "supple_code": "1", "supple_name": "口服", "medc_days": "3", "drug_times": "1", "drug_comm": "", "drug_ma": "", "remark": "", "limit_flag": "0", "usage": "11", "usage_name": "每天一次(qd)", "sin_doscnt": "2.00", "sin_dosunt": "片", "unit": "盒" }] } + // return await syncHisChargeStatus(data); +} \ No newline at end of file diff --git a/hlw/hn-his/notify.js b/hlw/hn-his/notify.js new file mode 100644 index 0000000..24bd6b9 --- /dev/null +++ b/hlw/hn-his/notify.js @@ -0,0 +1,220 @@ + +const { getDatabase } = require('../../mongodb') +const Sm4Util = require('../../utils/sm4-util') +const CodeMap = { + updateOnlineDrug: '01'// 同步his在线药品信息 +} + +let hlwDb = null; + +function isValidString(str) { + return typeof str === 'string' && str.trim() !== '' && !(/^null$/i.test(str.trim())) && str.length < 200; +} + +async function getHlwDb() { + try { + if (hlwDb) return hlwDb; + hlwDb = await getDatabase('Internet-hospital'); + if (!hlwDb) throw new Error('数据库连接失败'); + return hlwDb; + } catch (e) { + return Promise.reject(e) + } +} + +async function handleNotify(ctx) { + let data = null; + try { + const ciphertext = ctx && typeof ctx.data === 'string' ? ctx.data.trim() : ''; + const jsonStr = Sm4Util.decryptDataForSm3(ciphertext); + data = JSON.parse(jsonStr); + if (Object.prototype.toString.call(data) !== '[object Object]') { + return { success: false, message: '无效的通知类型' } + } + if (data.code === CodeMap.updateOnlineDrug) { + return await updateOnlineDrug(data) + } + return { success: false, message: '无效的通知类型' } + } catch (e) { + return { success: false, message: `参数错误: ${e.message}` } + } +} + +async function updateOnlineDrug(data) { + try { + const db = await getHlwDb(); + const corpId = typeof data.corpId === 'string' ? data.corpId.trim() : ''; + const erp_id = typeof data.erp_id === 'string' ? data.erp_id.trim() : ''; + const drugname = typeof data.drugname === 'string' ? data.drugname.trim() : ''; + const specification = typeof data.specification === 'string' ? data.specification.trim() : ''; + const pack_unit_name = typeof data.pack_unit_name === 'string' ? data.pack_unit_name.trim() : ''; + const manu_name = typeof data.manu_name === 'string' ? data.manu_name.trim() : ''; + const gjbm = typeof data.gjbm === 'string' ? data.gjbm.trim() : ''; + const py_code = typeof data.py_code === 'string' ? data.py_code.trim() : ''; + const default_dosage = Number(data.default_dosage); + const default_unit = typeof data.default_unit === 'string' ? data.default_unit.trim() : ''; + const default_drug_win = Number(data.default_drug_win); + const default_total = Number(data.default_total); + const default_usage_name = typeof data.default_usage_name === 'string' ? data.default_usage_name.trim() : ''; + const default_supply_name = typeof data.default_supply_name === 'string' ? data.default_supply_name.trim() : ''; + + const newData = {}; + const effectKeys = []; + if (!corpId) { + return { success: false, message: '参数错误' } + } + if (isValidString(erp_id) && Number(erp_id) > 0 && Number.isInteger(Number(erp_id))) { + newData.product_id = Number(erp_id); + newData.product_id_str = Number(erp_id).toString(); + } else { + return { success: false, message: 'erp id格式错误(正整数)' } + } + if (isValidString(drugname)) { + newData.name = drugname; + effectKeys.push('drugname') + } else { + return { success: false, message: '药瓶名称不能为空' } + } + if (isValidString(gjbm)) { + newData.insurance_code = gjbm; + effectKeys.push('gjbm') + } else { + return { success: false, message: '医保编码不能为空' } + } + if (isValidString(specification)) { + newData.specification = specification; + effectKeys.push('specification') + } + if (isValidString(manu_name)) { + newData.manufacturer = manu_name; + effectKeys.push('manu_name') + } + if (isValidString(py_code)) { + newData.pinyin_code = py_code; + effectKeys.push('py_code') + } + if (default_dosage > 0 && (default_dosage * 100) % 1 === 0) { + newData.dosage = default_dosage + effectKeys.push('default_dosage') + } + if (default_drug_win > 0 && Number.isInteger(default_drug_win)) { + newData.days = default_drug_win + effectKeys.push('default_drug_win') + } + if (default_total > 0 && Number.isInteger(default_total)) { + newData.recommended_quantity = default_total + effectKeys.push('default_total') + } + const configKeys = [ + `${corpId}-medicine-package-unit`,// 药品包装单位 + `${corpId}-online-medicine-dosage-unit`,//线上药品单次剂量单位 + `${corpId}-online-medicine-frequence`,//线上药品用药频率 + `${corpId}-online-medicine-administration`,//线上药品用药用法 + ] + const res = await db.collection('hlw-config').find({ corpId: { $in: configKeys } }, { projection: { corpId: 1, list: 1 } }).toArray() + if (isValidString(pack_unit_name)) { + const packUnitConfig = res.find(i => i.corpId === `${corpId}-medicine-package-unit`) + const list = packUnitConfig && Array.isArray(packUnitConfig.list) ? packUnitConfig.list : []; + const packUnit = list.find(i => i.name === pack_unit_name); + if (packUnit) { + newData.unit = packUnit.name + effectKeys.push('pack_unit_name') + } + } + if (isValidString(default_unit)) { + const dosageUnitConfig = res.find(i => i.corpId === `${corpId}-online-medicine-dosage-unit`) + const list = dosageUnitConfig && Array.isArray(dosageUnitConfig.list) ? dosageUnitConfig.list : []; + const dosageUnit = list.find(i => i.name === default_unit); + if (dosageUnit) { + newData.dosage_unit = dosageUnit.name; + effectKeys.push('default_unit') + } + } + + if (isValidString(default_usage_name)) { + const administrationConfig = res.find(i => i.corpId === `${corpId}-online-medicine-administration`) + const list = administrationConfig && Array.isArray(administrationConfig.list) ? administrationConfig.list : []; + const usage = list.find(i => i.name === default_usage_name); + if (usage) { + newData.administration_method = usage.name; + effectKeys.push('default_usage_name') + } + } + + if (isValidString(default_supply_name)) { + const supplyConfig = res.find(i => i.corpId === `${corpId}-online-medicine-frequence`) + const list = supplyConfig && Array.isArray(supplyConfig.list) ? supplyConfig.list : []; + const supply = list.find(i => i.name === default_supply_name); + if (supply) { + newData.frequency = supply.name; + newData.freq = supply.name; + effectKeys.push('default_supply_name') + } + } + // return { success: true, data, effectKeys } + const med = await db.collection('online-drug-info').findOne({ product_id: Number(erp_id) }); + if (med) { + newData.updateTime = Date.now(); + newData.updater = 'hisPush'; + await db.collection('online-drug-info').updateOne({ _id: med._id }, { $set: newData }) + return { success: true, message: '更新成功', valid_keys: effectKeys.join() } + } else { + newData.createTime = Date.now(); + newData.creator = 'hisPush'; + newData.onSale = false; + await db.collection('online-drug-info').insertOne(newData) + return { success: true, message: '新增成功', valid_keys: effectKeys.join() } + } + } catch (e) { + return { success: false, message: `更新失败: ${e.message}` } + } +} + +async function notifyTest() { + const str = getPayload() + const res = await handleNotify(str); + return res +} + +function getPayload() { + // const data = { + // code: '01', + // corpId: '4G7H8J2K9L0M1N3P5Q6R7S8T9U0V1', + // erp_id: '0001', + // drugname: '药品名称1', + // specification: '药品规格1', + // pack_unit_name: '瓶', + // manu_name: '厂商名称1', + // gjbm: '医保编码1', + // py_code: 'pinyin1', + // default_dosage: 2, + // default_unit: '颗', + // default_usage_name: '口服', + // default_drug_win: 8, + // default_supply_name: '每天二次', + // default_total: 4 + // }; + const data = { + "code": "01", + "corpId": "4G7H8J2K9L0M1N3P5Q6R7S8T9U0V1", + "erp_id": "52362", + "drugname": "(21金维他)多维元素片(21)", + "specification": "60粒×5瓶/条 ", + "pack_unit_name": "条", + "manu_name": "杭州民生健康药业有限公司", + "gjbm": "XA11AAD190A001020904494", + "py_code": "(21JWT)D", + "default_dosage": null, + "default_unit": null, + "default_usage_name": null, + "default_drug_win": null, + "default_supply_name": null, + "default_total": null + } + return { data: Sm4Util.encryptDataForSm3(JSON.stringify(data)) } +} + +module.exports = { + handleNotify, + notifyTest +} \ No newline at end of file diff --git a/hlw/hn-his/types.ts b/hlw/hn-his/types.ts new file mode 100644 index 0000000..aff31d7 --- /dev/null +++ b/hlw/hn-his/types.ts @@ -0,0 +1,54 @@ + +// interface UpdateOnlineMedicine { +// erp_id: string; +// drugname: string; // 药品名称 +// specification: string; // 规格 +// pack_unit_name: string;// 包装单位 +// manu_name: string; // 厂家 +// gjbm: string; // 医保码 +// py_code: string; // 拼音码 +// default_dosage: number; // 单次剂量 (最多2位小数) +// default_unit: string; // 单次剂量单位 +// default_usage_name: string; // 用法 +// default_drug_win: number;// 用药天数 +// default_supply_name: string; // 用药频率 +// default_total: number; // 推荐数量 +// } + +interface UpdateOnlineMedicineBody { + code: '01'; + corpId: string; + erp_id: string; + drugname: string; // 药品名称 + specification: string; // 规格 + pack_unit_name: string;// 包装单位 + manu_name: string; // 厂家 + gjbm: string; // 医保码 + py_code: string; // 拼音码 + default_dosage: number; // 单次剂量 (最多2位小数) + default_unit: string; // 单次剂量单位 + default_usage_name: string; // 用法 + default_drug_win: number;// 用药天数 + default_supply_name: string; // 用药频率 + default_total: number; // 推荐数量 +} + +const body: UpdateOnlineMedicineBody = { + code: '01', + corpId: '4G7H8J2K9L0M1N3P5Q6R7S8T9U0V1', + erp_id: '0001', + drugname: '123', + specification: '123', + pack_unit_name: '盒', + manu_name: '123', + gjbm: '123', + py_code: '1222', + default_dosage: 1, + default_unit: '片', + default_usage_name: '口服', + default_drug_win: 7, + default_supply_name: '每天一次', + default_total: 3 +} + + diff --git a/hlw/index.js b/hlw/index.js index 3bb8c3a..a3b0c61 100644 --- a/hlw/index.js +++ b/hlw/index.js @@ -1,4 +1,4 @@ -const zytHis = require("./zyt-his"); +const hnHis = require("./hn-his"); const drugInfo = require("./drug-info"); const patientDescription = require("./patient-description"); const tencentIM = require("./tencent-im"); @@ -27,8 +27,9 @@ const miniAppLog = require("./mini-app-log"); const chineseMedicine = require("./chinese-medicine"); const chineseMedicineOrder = require("./chinese-medicine-order"); const hlwPatient = require("./hlw-patient"); +const scannedCardArchive = require("./scanned-card-archive"); -module.exports = async (item, db, res) => { +module.exports = async (item, db, context) => { switch (item.type) { case "getHisCustomer": case "addHisCustomer": @@ -42,7 +43,7 @@ module.exports = async (item, db, res) => { case "getHisInvoice": case "getHisEmployeeInfo": case "getWineDrugStockInfo": - return await zytHis(item, db); + return await hnHis(item, db); case "getDrugInfo": case "deleteDrugInfo": case "updateDrugInfo": @@ -129,6 +130,7 @@ module.exports = async (item, db, res) => { case "getLatestSubmitTime": case "orderHasDiagnosis": case "getStoreDiagnosisRecord": + case "markStoreRxPrinted": case "getAllDiagnosisRecord": case "getDoctorRxStats": case "getPharmacistRxStats": @@ -145,7 +147,7 @@ module.exports = async (item, db, res) => { case "getDiagnosisById": case "getPatientWineBuyCount": case "getYbReviewResult": - return diagnosticRecord(item, db); + return diagnosticRecord(item, db, context); case "caAuth": case "getSealImage": case "getCaUserInfo": @@ -183,7 +185,7 @@ module.exports = async (item, db, res) => { case "getRegPayStatus": return await hlwReg(item, db); case "downloadXlsx": - return await downloadXlsx(item, db, res); + return await downloadXlsx(item, db, context); case "getDoctorTimeStats": return await hlwTimeDuration(item, db); case "getHlwOrderConfig": @@ -267,6 +269,8 @@ module.exports = async (item, db, res) => { case "updateHlwPatient": case "deleteHlwPatient": return await hlwPatient.main(item, db); + case "saveScannedCard": + return await scannedCardArchive(item, db); default: return { success: false, diff --git a/hlw/online-drug-info/batch-upload.js b/hlw/online-drug-info/batch-upload.js index d7f251d..92cdba9 100644 --- a/hlw/online-drug-info/batch-upload.js +++ b/hlw/online-drug-info/batch-upload.js @@ -26,7 +26,7 @@ async function getBatchInsertOnlineDrugFn() { rowData[key.trim()] = row[key]; } if (!rowData.his_drug_code || !String(rowData.his_drug_code).trim()) return; - const [drugData] = getDrugList([rowData], true, extraInfo); + const [drugData] = getDrugList([rowData], true, extraInfo, 'online'); const { msgs, ...drug } = drugData; if (msgs.length === 0) { batch.push(drug); diff --git a/hlw/online-drug-info/index.js b/hlw/online-drug-info/index.js index e937d63..c29e73b 100644 --- a/hlw/online-drug-info/index.js +++ b/hlw/online-drug-info/index.js @@ -181,7 +181,7 @@ async function addOnlineDrugInfo(ctx, db) { return { success: false, message: "ERP编码已存在" }; } } - const [item] = getDrugList([data], true, peopleInfo); + const [item] = getDrugList([data], true, peopleInfo, 'online'); const { msgs, ...drug } = item; if (msgs.length) { return { success: false, message: msgs.join(",") }; @@ -236,7 +236,7 @@ async function editOnlineDrugInfo(ctx, db) { typeof ctx.operatorId === "string" ? ctx.operatorId : "", updateTime: Date.now(), }; - const [item] = getDrugList([data], false, peopleInfo); + const [item] = getDrugList([data], false, peopleInfo, 'online'); const { msgs, ...drug } = item; if (msgs.length) { return { success: false, message: msgs.join(",") }; @@ -321,7 +321,7 @@ async function importOnlineDrugInfo(ctx, db) { typeof ctx.operatorId === "string" ? ctx.operatorId : "", createTime: Date.now(), }; - const datalist = getDrugList(drugs, true, peopleInfo); + const datalist = getDrugList(drugs, true, peopleInfo, 'online'); datalist.forEach((i) => { if (!i.his_drug_code || !String(i.his_drug_code).trim()) { i.msgs.push("ERP编码不能为空"); diff --git a/hlw/scanned-card-archive/index.js b/hlw/scanned-card-archive/index.js new file mode 100644 index 0000000..db69b0d --- /dev/null +++ b/hlw/scanned-card-archive/index.js @@ -0,0 +1,87 @@ +const Sm4Util = require("../../utils/sm4-util"); + +const CARD_STRING_FIELDS = [ + "CardIDCode", + "CardNum", + "CardType", + "Gender", + "IDNum", + "Name", + "PsamTN", + "Message", + "flag", + "ylbxtcqbm", +]; + +function normalizeCardData(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return null; + } + + const data = { + CanOpen: value.CanOpen === true, + Success: value.Success === true, + }; + + for (const field of CARD_STRING_FIELDS) { + if (value[field] === undefined || value[field] === null) continue; + if (typeof value[field] !== "string") return null; + const text = value[field].trim(); + if (text.length > 256) return null; + data[field] = text; + } + + if (!data.CanOpen || !data.Success) return null; + if (!data.IDNum && !data.CardNum && !data.CardIDCode) return null; + return data; +} + +function normalizeString(value, maxLength) { + if (typeof value !== "string") return ""; + const text = value.trim(); + return text.length <= maxLength ? text : ""; +} + +async function saveScannedCard(item, db) { + const cardData = normalizeCardData(item.cardData); + if (!cardData) { + return { success: false, message: "卡片数据无效" }; + } + + try { + const encryptedCardData = Sm4Util.encryptDataForSm3(JSON.stringify(cardData)); + const storeIds = Array.isArray(item.storeIds) + ? [...new Set(item.storeIds.map((value) => normalizeString(value, 100)).filter(Boolean))] + : []; + const record = { + encryptedCardData, + encryption: "SM4", + dataVersion: 1, + readerType: normalizeString(item.readerType, 50) || "ZJ_HZ_310000", + corpId: normalizeString(item.corpId, 100), + storeIds, + createTime: Date.now(), + }; + const result = await db.collection("scanned-card-archive").insertOne(record); + return { + success: true, + id: result.insertedId.toString(), + message: "卡片数据保存成功", + }; + } catch (error) { + console.error("保存扫码建档数据失败:", error.message); + return { success: false, message: "保存卡片数据失败" }; + } +} + +module.exports = async (item, db) => { + switch (item.type) { + case "saveScannedCard": + return saveScannedCard(item, db); + default: + return { success: false, message: "未找到接口" }; + } +}; + +module.exports.normalizeCardData = normalizeCardData; +module.exports.saveScannedCard = saveScannedCard; diff --git a/hlw/scanned-card-archive/index.test.js b/hlw/scanned-card-archive/index.test.js new file mode 100644 index 0000000..bf7ccfc --- /dev/null +++ b/hlw/scanned-card-archive/index.test.js @@ -0,0 +1,74 @@ +jest.mock("../../utils/sm4-util", () => ({ + encryptDataForSm3: jest.fn(() => "encrypted-card-data"), +})); + +const Sm4Util = require("../../utils/sm4-util"); +const scannedCardArchive = require("./index"); + +describe("scanned-card-archive", () => { + test("encrypts the full normalized card payload and only stores ciphertext", async () => { + const insertOne = jest.fn().mockResolvedValue({ + insertedId: { toString: () => "archive-id" }, + }); + const db = { + collection: jest.fn(() => ({ insertOne })), + }; + const cardData = { + CanOpen: true, + CardIDCode: "3306012345678901A", + CardNum: "DB5712345", + CardType: "3", + Gender: "", + IDNum: "330601199001011234", + Name: "张三", + PsamTN: "330412345678", + Success: true, + flag: "1", + ylbxtcqbm: "330600", + ignoredField: "must-not-be-saved", + }; + + const result = await scannedCardArchive({ + type: "saveScannedCard", + cardData, + readerType: "ZJ_HZ_310000", + corpId: "corp-1", + storeIds: ["store-1"], + }, db); + + expect(result).toEqual({ + success: true, + id: "archive-id", + message: "卡片数据保存成功", + }); + expect(Sm4Util.encryptDataForSm3).toHaveBeenCalledTimes(1); + const encryptedInput = JSON.parse(Sm4Util.encryptDataForSm3.mock.calls[0][0]); + expect(encryptedInput).toEqual(expect.objectContaining({ + IDNum: cardData.IDNum, + Name: cardData.Name, + Success: true, + })); + expect(encryptedInput).not.toHaveProperty("ignoredField"); + + const stored = insertOne.mock.calls[0][0]; + expect(stored.encryptedCardData).toBe("encrypted-card-data"); + expect(stored).not.toHaveProperty("IDNum"); + expect(stored).not.toHaveProperty("Name"); + expect(stored).not.toHaveProperty("cardData"); + }); + + test("rejects unsuccessful or malformed card payloads", async () => { + const db = { collection: jest.fn() }; + const result = await scannedCardArchive({ + type: "saveScannedCard", + cardData: { + CanOpen: false, + Success: false, + Message: "-2无卡", + }, + }, db); + + expect(result).toEqual({ success: false, message: "卡片数据无效" }); + expect(db.collection).not.toHaveBeenCalled(); + }); +}); diff --git a/hlw/tencent-im/index.js b/hlw/tencent-im/index.js index ec5c4d2..9c6b38e 100644 --- a/hlw/tencent-im/index.js +++ b/hlw/tencent-im/index.js @@ -14,6 +14,8 @@ module.exports = async (item, mongodb) => { return await setUserProfile(item); case "generateUserSig": return generateUserSig(item); + case "importAccount": + return await importAccount(item); case "deleteSession": return deleteSession(item); case "deleteFriend": @@ -39,28 +41,35 @@ async function sendSystemNotification(item) { const { formAccount, toAccount, msgBody, SyncOtherMachine, corpId } = item; console.log("发送系统通知请求体msgBody:", msgBody); console.log("item", item); - const { sdkAppId, identifier, secretKey } = await getConfig(corpId); - const { data: userSig } = await generateUserSig({ - userId: identifier, - corpId, - sdkAppId, - secretKey, - }); - // 构造请求 URL - // 构造请求体 - const data = { - SyncOtherMachine, // 若不希望将消息同步至 From_Account,则 SyncOtherMachine 填写2;若希望将消息同步至 From_Account,则 SyncOtherMachine 填写1。 - From_Account: formAccount, // 发送方 - To_Account: toAccount, // 接收方 - MsgRandom: Math.floor(Math.random() * 1000000), // 消息随机数(32位无符号整数),后台用于同一秒内的消息去重。请确保该字段填的是随机数 - MsgBody: msgBody, - SupportMessageExtension: 1, - }; - console.log("发送系统通知请求体:", data); - const url = `https://console.tim.qq.com/v4/openim/sendmsg?sdkappid=${sdkAppId}&identifier=${identifier}&usersig=${userSig}&random=${random}&contenttype=json`; try { + const { sdkAppId, identifier, secretKey } = (await getConfig(corpId)) || {}; + const sigResult = await generateUserSig({ + userId: identifier, + corpId, + sdkAppId, + secretKey, + }); + if (!sigResult.success) { + return sigResult; + } + // 构造请求 URL + // 构造请求体 + const data = { + SyncOtherMachine, // 若不希望将消息同步至 From_Account,则 SyncOtherMachine 填写2;若希望将消息同步至 From_Account,则 SyncOtherMachine 填写1。 + From_Account: formAccount, // 发送方 + To_Account: toAccount, // 接收方 + MsgRandom: Math.floor(Math.random() * 1000000), // 消息随机数(32位无符号整数),后台用于同一秒内的消息去重。请确保该字段填的是随机数 + MsgBody: msgBody, + SupportMessageExtension: 1, + }; + console.log("发送系统通知请求体:", data); + const url = `https://console.tim.qq.com/v4/openim/sendmsg?sdkappid=${sdkAppId}&identifier=${identifier}&usersig=${sigResult.data}&random=${random}&contenttype=json`; const response = await request.main(url, data, "POST"); - console.log("系统通知发送成功:", response.data); + if (!isTencentIMSuccess(response)) { + console.error("发送系统通知失败:", response); + return createTencentIMFailure("系统通知发送失败", response); + } + console.log("系统通知发送成功:", response); return { success: true, message: "系统通知发送成功", @@ -77,6 +86,24 @@ async function sendSystemNotification(item) { }; } } + +function isTencentIMSuccess(response) { + return Boolean( + response && + response.ActionStatus === "OK" && + Number(response.ErrorCode) === 0 + ); +} + +function createTencentIMFailure(message, response) { + return { + success: false, + message, + errorCode: response && response.ErrorCode, + errorInfo: response && response.ErrorInfo, + error: response, + }; +} /** * * 用户状态 Tag_Profile_Custom_Status @@ -330,67 +357,77 @@ async function deleteFriend(item) { } } -// 批量导入医生账号 -async function importDoctorAccount(event) { - console.log("用户资料设置求体:", item); - const { formAccount, profileItem, corpId } = item; - const { - sdkAppId, - identifier, - secretKey, - } = await getConfig(corpId); - if (!sdkAppId) { +// 将业务账号导入腾讯 IM。重复导入同一账号是幂等操作。 +async function importAccount(item) { + const { account, corpId } = item; + if (typeof account !== "string" || !account.trim()) { return { success: false, - message: "未找到企业配置", + message: "IM 账号不能为空", }; } - const { data: userSig } = await generateUserSig({ - userId: identifier, - corpId, - sdkAppId, - secretKey, - }); - // 构造请求 URL - // 构造请求体 - const data = { - AccountList: [], - }; - const url = `https://console.tim.qq.com/v4/im_open_login_svc/multiaccount_import?sdkappid=${sdkAppId}&identifier=${identifier}&usersig=${userSig}&random=${random}&contenttype=json`; + try { + const { sdkAppId, identifier, secretKey } = (await getConfig(corpId)) || {}; + const sigResult = await generateUserSig({ + userId: identifier, + corpId, + sdkAppId, + secretKey, + }); + if (!sigResult.success) { + return sigResult; + } + + const data = { + Identifier: account.trim(), + }; + const url = `https://console.tim.qq.com/v4/im_open_login_svc/account_import?sdkappid=${sdkAppId}&identifier=${identifier}&usersig=${sigResult.data}&random=${random}&contenttype=json`; const response = await request.main(url, data, "POST"); - console.log("用户资料设置", response.data); + if (!isTencentIMSuccess(response)) { + console.error("腾讯 IM 账号导入失败:", response); + return createTencentIMFailure("腾讯 IM 账号导入失败", response); + } + console.log("腾讯 IM 账号导入成功:", account); return { success: true, - message: "用户资料设置成功", + message: "腾讯 IM 账号导入成功", }; } catch (error) { return { success: false, - message: "用户资料设置失败", + message: "腾讯 IM 账号导入失败", error: error.response ? error.response.data : error.message, }; } } async function generateUserSig({ userId, corpId, sdkAppId, secretKey }) { - if (!sdkAppId || !secretKey) { - const res = await getConfig(corpId); - sdkAppId = res.sdkAppId; - secretKey = res.secretKey; - } - if (!sdkAppId) { + try { + if (!sdkAppId || !secretKey) { + const res = (await getConfig(corpId)) || {}; + sdkAppId = res.sdkAppId; + secretKey = res.secretKey; + } + if (!sdkAppId || !secretKey || !userId) { + return { + success: false, + message: "未找到完整的企业 IM 配置", + }; + } + var api = new TLSSigAPIv2.Api(sdkAppId, secretKey); + var sig = api.genSig(userId, 86400 * 180); + return { + success: true, + message: "生成成功", + data: sig, + sdkAppId, + }; + } catch (error) { return { success: false, - message: "未找到企业配置", + message: "生成 UserSig 失败", + error: error.message, }; } - var api = new TLSSigAPIv2.Api(sdkAppId, secretKey); - var sig = api.genSig(userId, 86400 * 180); - return { - success: true, - message: "生成成功", - data: sig, - sdkAppId, - }; } diff --git a/hlw/tencent-im/index.test.js b/hlw/tencent-im/index.test.js new file mode 100644 index 0000000..df71c3a --- /dev/null +++ b/hlw/tencent-im/index.test.js @@ -0,0 +1,178 @@ +jest.mock("../../request", () => ({ + main: jest.fn(), +})); +jest.mock("../utils/config", () => ({ + getConfig: jest.fn(), +})); +jest.mock("./TLSSigAPIv2", () => ({ + Api: jest.fn(), +})); + +const request = require("../../request"); +const { getConfig } = require("../utils/config"); +const TLSSigAPIv2 = require("./TLSSigAPIv2"); +const tencentIM = require("./index"); + +describe("tencent-im", () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(console, "log").mockImplementation(() => {}); + jest.spyOn(console, "error").mockImplementation(() => {}); + getConfig.mockResolvedValue({ + sdkAppId: 1600000000, + identifier: "administrator", + secretKey: "test-secret", + }); + TLSSigAPIv2.Api.mockImplementation(() => ({ + genSig: jest.fn(() => "test-user-sig"), + })); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + test("imports a new account", async () => { + request.main.mockResolvedValue({ + ActionStatus: "OK", + ErrorCode: 0, + ErrorInfo: "", + }); + + const result = await tencentIM({ + type: "importAccount", + account: "WZTEST001", + corpId: "corp-1", + }); + + expect(result).toEqual({ + success: true, + message: "腾讯 IM 账号导入成功", + }); + expect(request.main).toHaveBeenCalledWith( + expect.stringContaining("/v4/im_open_login_svc/account_import?"), + { Identifier: "WZTEST001" }, + "POST" + ); + }); + + test("treats repeated successful imports as success", async () => { + request.main.mockResolvedValue({ + ActionStatus: "OK", + ErrorCode: 0, + ErrorInfo: "", + }); + + const first = await tencentIM({ + type: "importAccount", + account: "WZTEST001", + corpId: "corp-1", + }); + const second = await tencentIM({ + type: "importAccount", + account: "WZTEST001", + corpId: "corp-1", + }); + + expect(first.success).toBe(true); + expect(second.success).toBe(true); + expect(request.main).toHaveBeenCalledTimes(2); + }); + + test("preserves Tencent error details when an import returns HTTP 200 with FAIL", async () => { + request.main.mockResolvedValue({ + ActionStatus: "FAIL", + ErrorCode: 70001, + ErrorInfo: "import failed", + }); + + const result = await tencentIM({ + type: "importAccount", + account: "WZTEST002", + corpId: "corp-1", + }); + + expect(result).toMatchObject({ + success: false, + message: "腾讯 IM 账号导入失败", + errorCode: 70001, + errorInfo: "import failed", + }); + }); + + test("does not report sendmsg ErrorCode 20003 as success", async () => { + request.main.mockResolvedValue({ + ActionStatus: "FAIL", + ErrorCode: 20003, + ErrorInfo: "Invalid sender or receiver identifier!", + }); + + const result = await tencentIM({ + type: "sendSystemNotification", + formAccount: "WZTEST003", + toAccount: "000001", + SyncOtherMachine: 2, + corpId: "corp-1", + msgBody: [], + }); + + expect(result).toMatchObject({ + success: false, + errorCode: 20003, + errorInfo: "Invalid sender or receiver identifier!", + }); + }); + + test("returns a diagnostic error when IM configuration is incomplete", async () => { + getConfig.mockResolvedValue({}); + + const result = await tencentIM({ + type: "importAccount", + account: "WZTEST004", + corpId: "corp-1", + }); + + expect(result).toMatchObject({ + success: false, + message: "未找到完整的企业 IM 配置", + }); + expect(request.main).not.toHaveBeenCalled(); + }); + + test("returns a diagnostic error when UserSig generation fails", async () => { + TLSSigAPIv2.Api.mockImplementation(() => ({ + genSig: jest.fn(() => { + throw new Error("signature failure"); + }), + })); + + const result = await tencentIM({ + type: "importAccount", + account: "WZTEST005", + corpId: "corp-1", + }); + + expect(result).toMatchObject({ + success: false, + message: "生成 UserSig 失败", + error: "signature failure", + }); + expect(request.main).not.toHaveBeenCalled(); + }); + + test("returns a diagnostic error on network failure", async () => { + request.main.mockRejectedValue(new Error("network unavailable")); + + const result = await tencentIM({ + type: "importAccount", + account: "WZTEST006", + corpId: "corp-1", + }); + + expect(result).toMatchObject({ + success: false, + message: "腾讯 IM 账号导入失败", + error: "network unavailable", + }); + }); +}); diff --git a/index.js b/index.js index 668c8db..428d475 100644 --- a/index.js +++ b/index.js @@ -49,6 +49,7 @@ const { jwtAuth, optionalJwtAuth } = require("./middleware/jwt-auth"); const { smartAuth } = require("./middleware/smart-auth"); const sms = require("./sms"); const path = require("path"); +const { ObjectId } = require("mongodb"); const { useUploadFileParse } = require("./upload-file-parse"); const app = express(); const PORT = process.env.CONFIG_NODE_PORT; @@ -215,13 +216,46 @@ app.get( app.get( "/zyt/2excel", - // jwtAuth, + jwtAuth, asyncHandler(async (req, res) => { try { const requestData = req.query; const db = await getDatabase("Internet-hospital"); + const downloadParams = { + ...req.query, + type: "downloadXlsx", + downloadType: req.query.type, + }; + if ( + downloadParams.downloadType === "downloadRx" && + downloadParams.prescriptionType === "storeMedicinePurchase" + ) { + let drugStoreId = + req.user && typeof req.user.drugStoreId === "string" + ? req.user.drugStoreId.trim() + : ""; + if (!drugStoreId && req.user && ObjectId.isValid(req.user.userId)) { + const corpDb = await getDatabase("corp"); + const account = await corpDb.collection("corp-member").findOne( + { _id: new ObjectId(req.user.userId) }, + { projection: { drugStoreId: 1 } } + ); + drugStoreId = + account && typeof account.drugStoreId === "string" + ? account.drugStoreId.trim() + : ""; + } + if (!drugStoreId) { + return safeSend(res, { + success: false, + message: "当前账号未关联门店", + }); + } + downloadParams.storeId = drugStoreId; + delete downloadParams.storeLoginNo; + } const message = await hlw( - { ...req.query, type: "downloadXlsx", downloadType: req.query.type }, + downloadParams, db, res );