From 0c369655b77e4b1b8d1d6eb956719cb70c948faf Mon Sep 17 00:00:00 2001 From: huxuejian Date: Thu, 22 Oct 2026 15:30:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E9=9C=80=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.dev | 2 + .env.hn | 2 + hlw/automation/index.js | 3 +- hlw/automation/index.test.js | 2 + hlw/consult-order/index.js | 9 ++-- hlw/diagnostic-record/format.js | 8 +-- hlw/diagnostic-record/index.js | 2 + hlw/hlw-patient/index.js | 87 ++++++++++++++++++++++++++++----- hlw/hn-his/index.js | 9 +++- utils/validator.js | 26 +++++++++- 10 files changed, 126 insertions(+), 24 deletions(-) diff --git a/.env.dev b/.env.dev index 98bdfc8..c4e8890 100644 --- a/.env.dev +++ b/.env.dev @@ -26,3 +26,5 @@ CONFIG_PRESCRIPTION_PDF_BASE_URL=https://patient.youcan365.com/hn ## 查询药品信息的时候 带上corpId (测试数据 药品信息存在多家机构, 但是线上各自独立部署, 所以测试环境带corpId区分下) CONFIG_SEARCH_DRUG_WITH_CORPID=YES +## 生成的咨询订单挂号id编号前缀, 也用于后续生成处方的编号 (his正式环境和测试环境同一个 生成序号规则相同, 产生同编号的话有问题, 所以加个前缀区分下) +CONFIG_CONSULT_REGISTER_ID_PREFIX=Test \ No newline at end of file diff --git a/.env.hn b/.env.hn index eb00a54..4b17531 100644 --- a/.env.hn +++ b/.env.hn @@ -37,4 +37,6 @@ CONFIG_PRESCRIPTION_PDF_BASE_URL=https://patient.youcan365.com/hn ## 查询药品信息的时候 带上corpId (测试数据 药品信息存在多家机构, 但是线上各自独立部署, 所以测试环境带corpId区分下) CONFIG_SEARCH_DRUG_WITH_CORPID=YES +## 生成的咨询订单挂号id编号前缀, 也用于后续生成处方的编号 (his正式环境和测试环境同一个 生成序号规则相同, 产生同编号的话有问题, 所以加个前缀区分下) +CONFIG_CONSULT_REGISTER_ID_PREFIX=Test diff --git a/hlw/automation/index.js b/hlw/automation/index.js index 51ef6c4..2613ed1 100644 --- a/hlw/automation/index.js +++ b/hlw/automation/index.js @@ -498,7 +498,7 @@ function getFailureNotificationExt(error) { ? `HIS上传错误:${cleanHisUploadFailureMessage(rawReason) || "未知错误"}` : errorSource === ERROR_SOURCES.RATIONAL_DRUG ? `合理用药错误:${rawReason || "未知错误"}` - : `系统错误:${rawReason || "未知错误"}`; + : `系统错误:${rawReason || "未知错误"}`; return { errorSource, reason }; } @@ -845,6 +845,7 @@ async function resolvePrescriptionDrugs(db, order) { dosage, dosage_unit: dosageUnit.name, dosage_unit_code: dosageUnit.code, + dose_time_name: requested.dose_time_name || "", drugName: master.name, specification: master.specification || "", frequencyCode: frequency.code, diff --git a/hlw/automation/index.test.js b/hlw/automation/index.test.js index 979a4d9..5c37daa 100644 --- a/hlw/automation/index.test.js +++ b/hlw/automation/index.test.js @@ -1027,6 +1027,7 @@ describe("automatic prescription assembly", () => { dosage_form: "片剂", days: 3, dosage_unit: "mg", + dose_time_name: "餐后", administration_method: "口服", freq: "每日两次", unit: "盒", @@ -1083,6 +1084,7 @@ describe("automatic prescription assembly", () => { _id: drugId.toString(), erpId: "ERP-1", dosage_unit_code: "MG", + dose_time_name: "餐后", frequencyCode: "BID", usageCode: "PO", quantity: 1, diff --git a/hlw/consult-order/index.js b/hlw/consult-order/index.js index 9c4ba82..47ca381 100644 --- a/hlw/consult-order/index.js +++ b/hlw/consult-order/index.js @@ -10,6 +10,7 @@ const { encryptOrderFields, decryptOrderFields, decryptOrderList } = require("./ const { getRequiredDrugAccessLevels, doctorMeetsDrugAccessLevels } = require("../drug-access-level"); const doctorOrPharmacistOfflineMessage = "当前医生或药师不在线,暂无法提交问诊单,请稍后再试"; +const registerIdPrefix = process.env.CONFIG_CONSULT_REGISTER_ID_PREFIX || ''; const hisPayStatus = { 0: "已支付", @@ -158,7 +159,6 @@ async function addConsultOrder(item) { async function addOnlineConsultOrder(params) { try { - params.registerId = await generateRegisterId(db); const requiredDrugAccessLevels = await getRequiredDrugAccessLevels(db, params.drugs); const storeApi = require("../drug-store"); const { success, list, message } = await storeApi( @@ -187,6 +187,7 @@ async function addOnlineConsultOrder(params) { params.drugStoreName = store.name; params.drugStoreNo = store.login_no; const doctorCode = params.doctorCode.trim(); + params.registerId = await generateRegisterId(db); // 指定医生 if (doctorCode) { const doctor = await db.collection("hlw-doctor").findOne( @@ -261,7 +262,9 @@ async function addOnlineConsultOrder(params) { errorInfo: imAccountResult.errorInfo, }; } - params.medorg_order_no = common.generateUniqueStr('HLW', 12); + const registerId = await generateRegisterId(db); + params.registerId = registerId + params.medorg_order_no = registerId; // 加密敏感字段 const encryptedParams = encryptOrderFields(params); const { insertedId } = await db @@ -2378,5 +2381,5 @@ async function generateRegisterId(db, type = 'consult-register-id') { } ); const sequence = result.sequence; - return `${day}${String(sequence).padStart(4, '0')}`; + return `${registerIdPrefix}${day}${String(sequence).padStart(4, '0')}`; } diff --git a/hlw/diagnostic-record/format.js b/hlw/diagnostic-record/format.js index 458b0bf..7a0fe15 100644 --- a/hlw/diagnostic-record/format.js +++ b/hlw/diagnostic-record/format.js @@ -130,10 +130,10 @@ exports.formatUploadRxData = function (data, opt) { appr_flag: i.forceSelfPay === 'Y' ? '2' : '1', dose_time_name: i.dose_time_name, } - if (item.appr_flag === '2') { - //[his要求]当appr_flag=2时必填,如果这个药品维护了限制使用范围的,传1,否则就传2 - item.self_pay_rea = i.limitUsageScope === 'Y' ? '1' : '2' - } + // if (item.appr_flag === '2') { + // //[his要求]当appr_flag=2时必填,如果这个药品维护了限制使用范围的,传1,否则就传2 + // item.self_pay_rea = i.limitUsageScope === 'Y' ? '1' : '2' + // } return item }), icds: data.diagnosisList.map((i) => ({ diff --git a/hlw/diagnostic-record/index.js b/hlw/diagnostic-record/index.js index 114107b..22dbc6d 100644 --- a/hlw/diagnostic-record/index.js +++ b/hlw/diagnostic-record/index.js @@ -2234,6 +2234,7 @@ async function getYbReviewResult(ctx, db) { name: 1, dosage_form: 1, specification: 1, + forceSelfPay: 1 }, } ) @@ -2264,6 +2265,7 @@ async function getYbReviewResult(ctx, db) { dosage: drug.dosage, dosage_unit: drug.dosage_unit, dose_time_name: drug.dose_time_name, + forceSelfPay: dbDrug.forceSelfPay }; }); diff --git a/hlw/hlw-patient/index.js b/hlw/hlw-patient/index.js index 3136e0d..98bcf98 100644 --- a/hlw/hlw-patient/index.js +++ b/hlw/hlw-patient/index.js @@ -31,6 +31,18 @@ function validateAccountId(accountId) { return { success: true, accountId: accountId.trim() }; } +function isValidBirthday(birthday) { + if (!/^\d{4}-\d{2}-\d{2}$/.test(birthday)) return false; + const [year, month, day] = birthday.split('-').map(Number); + const date = new Date(Date.UTC(year, month - 1, day)); + return ( + date.getUTCFullYear() === year + && date.getUTCMonth() + 1 === month + && date.getUTCDate() === day + && date.getTime() <= Date.now() + ); +} + function validatePatient(item) { const accountResult = validateAccountId(item && item.accountId); if (!accountResult.success) return accountResult; @@ -38,10 +50,27 @@ function validatePatient(item) { const name = typeof item.name === 'string' ? item.name.trim() : ''; const certNo = typeof item.certNo === 'string' ? item.certNo.trim() : ''; const mobile = typeof item.mobile === 'string' ? item.mobile.trim() : ''; + let birthday = typeof item.birthday === 'string' ? item.birthday.trim() : ''; + let sex = typeof item.sex === 'string' ? item.sex.trim() : ''; if (!name) return { success: false, message: '姓名不能为空' }; - if (certNo && !validator.isChinaId(certNo)) { - return { success: false, message: '证件号格式不正确' }; + let certInfo = null; + if (certNo) { + certInfo = validator.getChinaIdInfo(certNo); + if (!certInfo) return { success: false, message: '证件号格式不正确' }; + birthday = birthday || certInfo.birthday; + sex = sex || certInfo.sex; + } + if (!birthday) return { success: false, message: '出生日期不能为空' }; + if (!isValidBirthday(birthday)) { + return { success: false, message: '出生日期格式不正确' }; + } + if (!sex) return { success: false, message: '性别不能为空' }; + if (!['男', '女'].includes(sex)) { + return { success: false, message: '性别格式不正确' }; + } + if (certInfo && (birthday !== certInfo.birthday || sex !== certInfo.sex)) { + return { success: false, message: '出生日期或性别与证件号信息不一致' }; } if (!mobile) return { success: false, message: '手机号不能为空' }; if (!validator.isMobile(mobile)) { @@ -62,6 +91,8 @@ function validatePatient(item) { name, certNo, mobile, + birthday, + sex, address, }, data: { @@ -69,6 +100,8 @@ function validatePatient(item) { anotherIdNo: certNo ? Sm4Util.encryptDataForSm3(certNo) : '', anotherName: Sm4Util.encryptDataForSm3(name), anotherMobile: Sm4Util.encryptDataForSm3(mobile), + birthday, + sex, address, }, }; @@ -90,14 +123,16 @@ async function addHlwPatient(item, db) { } } // if (validation.origin.certNo) { - 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 res = await hnHis({ + // type: 'addHisCustomer', + // name: validation.origin.name, + // idCard: validation.origin.certNo, + // mobile: validation.origin.mobile, + // address: validation.origin.address, + // birthday: validation.origin.birthday, + // sex: validation.origin.sex, + // }) + // if (!res.success) return res; // } const result = await db.collection(COLLECTION_NAME).insertOne({ @@ -150,7 +185,7 @@ async function getHlwPatientList(item, db) { .find({ accountId: validation.accountId, disabled: false }) .toArray(); - const data = patients.map(toPatientData); + const data = patients.map((patient) => toPatientData(patient, true)); return { success: true, message: '查询就诊人列表成功', data }; } catch (error) { @@ -181,13 +216,39 @@ async function getHlwPatient(item, db) { } } -function toPatientData(patient) { +function calculateAge(birthday) { + if (!isValidBirthday(birthday)) return ''; + + const [birthYear, birthMonth, birthDay] = birthday.split('-').map(Number); + const today = new Date(); + let age = today.getFullYear() - birthYear; + const hasHadBirthdayThisYear = ( + today.getMonth() + 1 > birthMonth + || (today.getMonth() + 1 === birthMonth && today.getDate() >= birthDay) + ); + + if (!hasHadBirthdayThisYear) age -= 1; + return age > 0 ? age : 1; +} + +function toPatientData(patient, normalizeDemographics = false) { + const certNo = Sm4Util.decryptDataForSm3(patient.anotherIdNo); + const certInfo = normalizeDemographics && certNo + ? validator.getChinaIdInfo(certNo) + : null; + // 证件号信息仅用于本次响应,不回写数据库中的历史就诊人数据。 + const birthday = certInfo ? certInfo.birthday : (patient.birthday || ''); + const sex = certInfo ? certInfo.sex : (patient.sex || ''); + return { id: patient._id.toString(), accountId: patient.accountId, name: Sm4Util.decryptDataForSm3(patient.anotherName), - certNo: Sm4Util.decryptDataForSm3(patient.anotherIdNo), + certNo, mobile: Sm4Util.decryptDataForSm3(patient.anotherMobile), + birthday, + sex, + ...(normalizeDemographics ? { age: calculateAge(birthday) } : {}), address: patient.address || '', createTime: patient.createTime, }; diff --git a/hlw/hn-his/index.js b/hlw/hn-his/index.js index cbd5f64..55d8822 100644 --- a/hlw/hn-his/index.js +++ b/hlw/hn-his/index.js @@ -77,8 +77,11 @@ async function addHisCustomer(item) { const mdtrt_cert_no = typeof item.mdtrt_cert_no === 'string' ? item.mdtrt_cert_no.trim() : ''; const ecToken = typeof item.ecToken === 'string' ? item.ecToken.trim() : ''; const card_sn = typeof item.card_sn === 'string' ? item.card_sn.trim() : ''; + const birthday = typeof item.birthday === 'string' ? item.birthday.trim() : ''; + const sex = typeof item.sex === 'string' ? item.sex.trim() : ''; + if (!name || !mobile) { - return { success: false, message: '姓名、证件号、手机号不能为空' } + return { success: false, message: '姓名、手机号不能为空' } } const payload = { type: 'addHisCustomer', @@ -88,6 +91,10 @@ async function addHisCustomer(item) { address, buildType: '1', // 1.自费建档 2.电子凭证建档 3.线下医保 }; + if (!idCard) { + payload.brdy = birthday; + payload.gend = sex === '男' ? 1 : 2 + } // 电子凭证建档 医保插件授权建档 if (psnToken) { payload.psnToken = psnToken; diff --git a/utils/validator.js b/utils/validator.js index ab64478..c164488 100644 --- a/utils/validator.js +++ b/utils/validator.js @@ -173,10 +173,32 @@ function isChinaId(s) { return true } +function getChinaIdInfo(s) { + if (typeof s !== 'string' || !isChinaId(s)) return null; + + const sId = idTransfer(s); + const birthday = `${sId.substring(6, 10)}-${sId.substring(10, 12)}-${sId.substring(12, 14)}`; + const [year, month, day] = birthday.split('-').map(Number); + const date = new Date(Date.UTC(year, month - 1, day)); + if ( + date.getUTCFullYear() !== year + || date.getUTCMonth() + 1 !== month + || date.getUTCDate() !== day + ) { + return null; + } + + return { + birthday, + sex: Number(sId.charAt(16)) % 2 === 0 ? '女' : '男', + }; +} + module.exports = { verifyString, verifyNumber, verifyListItem, isMobile, - isChinaId -} \ No newline at end of file + isChinaId, + getChinaIdInfo, +}