feat: 优化需求开发

This commit is contained in:
huxuejian 2026-10-21 10:54:44 +08:00
parent 6185f89f95
commit a88b0edd77
10 changed files with 132 additions and 96 deletions

View File

@ -23,3 +23,6 @@ PUPPETEER_EXECUTABLE_PATH="C:\Program Files\Google\Chrome\Application\chrome.exe
PRESCRIPTION_PDF_MAX_CONCURRENCY=2
CONFIG_PRESCRIPTION_PDF_BASE_URL=https://patient.youcan365.com/hn
## 查询药品信息的时候 带上corpId (测试数据 药品信息存在多家机构, 但是线上各自独立部署, 所以测试环境带corpId区分下)
CONFIG_SEARCH_DRUG_WITH_CORPID=YES

View File

@ -1,52 +0,0 @@
CONFIG_DB_USERNAME=youcan
CONFIG_DB_PASSWORD=youcanTEST&2025
CONFIG_DB_HOST=119.45.106.100
CONFIG_DB_PORT=27017
CONFIG_NODE_ENV=zytDev
CONFIG_CA_BASE_URL=https://newcoss-dev.isignet.cn:10201/coss/service/v1/
CONFIG_CA_APPID=APP_7B3F36A14E99410A80B37AEF332E3247
CONFIG_CA_SECRET=DLwiH46Esb8ccNTkuSSVAadNTWUfW0sc
CONFIG_JWT_SECRET_KEY=ZYTZYY2025
CONFIG_JWT_EXPIRE_TIME=12h
CONFIG_NODE_PORT=8080
CONFIG_ZYB_YB_APPID=7D8D8E29924048539165F5E2E08C4616
CONFIG_ZYB_YB_SECRET=77F9610253B94263BCF8DBB61BF90D92
CONFIG_SMS_GATEWAY_DISABLED=true
## 支付宝回调地址
CONFIG_ALIPAY_PAY_NOTIFY=https://patient.youcan365.com/zyt/alipay-pay-notify
## 支付宝小程序appId
CONFIG_ALIPAY_MINI_APPID = 2021005110656858
## 蜂鸟即时配送请求域名
CONFIG_FENGNIAO_API_DOMAIN=https://exam-anubis.ele.me
## 蜂鸟即时配送分配的 appId 和 appSecret
CONFIG_FENGNIAO_APPID=2711175259951256784
CONFIG_FENGNIAO_SECRET=744c587a-61eb-4dc5-a226-e88b7f7b15d0
## 蜂鸟即时配送 后台管理页面 -> 应用管理 -> 查看沙箱环境 -> 商户信息的商户ID
CONFIG_FENGNIAO_MERCHANT_ID=15649332
## 蜂鸟即时配送 后台管理页面 -> 应用管理 -> 查看沙箱环境 -> 门店信息的门店编码
CONFIG_FENGNIAO_OUT_SHOP_CODE=testorder001
## 邮政快递 测试环境请求域名、分配的orgId、AES加密秘钥以及IV
CONFIG_EMS_API_DOMAIN=https://topup.beta.ule.com
## 邮政快递 测试环境分配的orgId、AES加密秘钥以及IV
CONFIG_EMS_ORGID=900004
CONFIG_EMS_AES_Key=HOKAwvtyPe9Atve4
CONFIG_EMS_AES_IV=HOKAwvtyPe9Atve4
## 邮政快递 邮乐分享用户ID (接口文档 2.1订单数据回传接口的入参uleShareUserId字段值, 由对方技术人员提供)
CONFIG_EMS_SHARE_USER_ID=yzg10000050008
## 邮政 请求地址
CONFIG_EMS_API_URL=https://api.ems.com.cn/amp-prod-api/f/amp/api/test
## 邮政 协议用户编码
CONFIG_EMS_USER_CODE=NNLXIQRYV
## 邮政 测试授权码
CONFIG_EMS_AUTH_CODE=U1pPOYv6VBULDah6
## 邮政 测试签名钥匙
CONFIG_EMS_SIGN_SECRET=ZmVRRjkyeDZnWHFnUGxkRQ==
## 邮政 协议客户号
CONFIG_EMS_SENDER_NO=1100100971796
## 震元his api
CONFIG_ZYT_HIS_BASE_URL=http://115.239.161.48:8089

14
.env.hn
View File

@ -24,3 +24,17 @@ CONFIG_ALIPAY_MINI_PRIVATE_KEY=MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBA
CONFIG_HN_HIS_BASE_URL=http://122.225.16.74:8026
## im图片存储服务端的位置
CONFIG_IM_IMAGE_ROOT=/root/service/hn/private-files/im-chat
## 处方 PDF(Linux Chrome)
PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome
PRESCRIPTION_PDF_MAX_CONCURRENCY=2
CONFIG_PRESCRIPTION_PDF_ROOT=/root/service/hn/private-files/prescriptions
CONFIG_PRESCRIPTION_PDF_BASE_URL=https://patient.youcan365.com/hn
## 查询药品信息的时候 带上corpId (测试数据 药品信息存在多家机构, 但是线上各自独立部署, 所以测试环境带corpId区分下)
CONFIG_SEARCH_DRUG_WITH_CORPID=YES

View File

@ -20,6 +20,7 @@ const TASK_STATUS = {
const ERROR_SOURCES = {
SYSTEM: "SYSTEM",
HIS: "HIS",
RATIONAL_DRUG: "RATIONAL_DRUG",
};
const TASK_CANCEL_REASONS = {
HIGH_RISK_DRUG: "HIGH_RISK_DRUG",
@ -44,7 +45,9 @@ class AutomationError extends Error {
super(message);
this.name = "AutomationError";
this.retryable = retryable;
this.source = source === ERROR_SOURCES.HIS ? ERROR_SOURCES.HIS : ERROR_SOURCES.SYSTEM;
this.source = Object.values(ERROR_SOURCES).includes(source)
? source
: ERROR_SOURCES.SYSTEM;
}
}
@ -485,14 +488,16 @@ function cleanHisUploadFailureMessage(message) {
function getFailureNotificationExt(error) {
const errorSource =
error && error.source === ERROR_SOURCES.HIS
? ERROR_SOURCES.HIS
error && Object.values(ERROR_SOURCES).includes(error.source)
? error.source
: ERROR_SOURCES.SYSTEM;
const rawReason =
error && typeof error.message === "string" ? error.message.trim() : "";
const reason =
errorSource === ERROR_SOURCES.HIS
? `HIS上传错误:${cleanHisUploadFailureMessage(rawReason) || "未知错误"}`
: errorSource === ERROR_SOURCES.RATIONAL_DRUG
? `合理用药错误:${rawReason || "未知错误"}`
: `系统错误:${rawReason || "未知错误"}`;
return { errorSource, reason };
}
@ -969,9 +974,9 @@ async function handleAutoOpenRx(db, task) {
db
);
if (!reviewResult || !reviewResult.success) {
throw new AutomationError(
reviewResult?.message || "合理用药审核未通过"
);
throw new AutomationError(reviewResult?.message || "合理用药审核未通过", {
source: ERROR_SOURCES.RATIONAL_DRUG,
});
}
const result = await diagnosticRecord(

View File

@ -726,6 +726,21 @@ describe("automation task scheduling", () => {
}),
})
);
expect(tencentIM).toHaveBeenCalledWith(
expect.objectContaining({
msgBody: [
expect.objectContaining({
MsgContent: expect.objectContaining({
Ext: JSON.stringify({
errorSource: ERROR_SOURCES.RATIONAL_DRUG,
reason: "合理用药错误:该处方存在不合理用药",
}),
}),
}),
],
}),
db
);
});
test("fails automatic audit immediately and sends the failure reason", async () => {

View File

@ -6,6 +6,8 @@ const { validateApplicableDiagnosisCodes } = require('./applicable-diagnosis');
const hnHis = require("../hn-his");
let db = "";
const WithCorpId = process.env.CONFIG_SEARCH_DRUG_WITH_CORPID === 'YES';
function getConfigOptionNames(list) {
return [...new Set((Array.isArray(list) ? list : [])
.map(item => typeof item === "string" ? item : item && item.name)
@ -67,6 +69,9 @@ async function getDrugInfo(item) {
let { name, _id, ids, pinyin_code, keyword, insuranceCodes, barcode } = item;
let fuzzyQuery = null;
let query = { onSale: true };
if (WithCorpId) {
query.corpId = item.corpId
}
if (typeof name === "string") query.name = new RegExp(name.trim(), "i");
if (_id) query._id = new ObjectId(_id);
if (Array.isArray(ids) && ids.length > 0) {
@ -211,6 +216,9 @@ async function getHlwDrugInfo(ctx) {
const page = Number.isSafeInteger(requestedPage) && requestedPage > 0 ? requestedPage : 1;
const pageSize = Number.isSafeInteger(requestedPageSize) && requestedPageSize > 0 ? requestedPageSize : 10;
const query = {};
if (WithCorpId) {
query.corpId = ctx.corpId
}
// if (typeof ctx.corpId === "string" && ctx.corpId.trim()) {
// query.corpId = ctx.corpId.trim();
// }
@ -285,6 +293,9 @@ async function addHlwDrugInfo(ctx) {
newDrug.tumor_use = 0;
}
newDrug.createTime = Date.now();
if (WithCorpId) {
newDrug.corpId = ctx.corpId
}
const res = await db.collection("drug-info").insertOne(newDrug);
if (res && res.insertedId) {
updateHisDrugInfo({ id: res.insertedId.toString() })

View File

@ -23,6 +23,7 @@ const drugFieldDict = [
{ name: '抗生素级别', field: 'antibiotic_level', verify: (val, label) => validator.verifyListItem(val, [0, 1, 2, 3], false, label, 0) },
{ name: '肿瘤使用', field: 'tumor_use', verify: (val, label) => validator.verifyListItem(val, [0, 1, 2, 3], false, label, 0) },
{ name: '风险等级', field: 'riskLevel', verify: (val, label) => validator.verifyListItem(val, ['低风险', '高风险'], false, label) },
{ name: '默认医保自费', field: 'forceSelfPay', verify: (val, label) => validator.verifyListItem(val, ['Y', 'N'], false, label) },
// 仅在线药品库使用:管理分类(处方药 / OTC)与剂型
{ name: '管理分类', field: 'manage_category', verify: (val, label) => validator.verifyString(val, label) },
{ name: '剂型', field: 'dosage_form', verify: (val, label) => validator.verifyString(val, label) },

View File

@ -53,22 +53,55 @@ async function getDiagnosisList(item) {
if (category && typeof category === "string") {
query.category = new RegExp(category.trim(), "i");
}
if (typeof keyword == "string" && keyword.trim()) {
const normalizedKeyword = typeof keyword === "string" ? keyword.trim() : "";
if (normalizedKeyword) {
query.$or = [
{ name: new RegExp(keyword.trim(), "i") },
{ code: new RegExp(keyword.trim(), "i") },
{ category: new RegExp(keyword.trim(), "i") },
{ pinyin: new RegExp(keyword.trim(), "i") },
{ name: new RegExp(normalizedKeyword, "i") },
{ code: new RegExp(normalizedKeyword, "i") },
{ category: new RegExp(normalizedKeyword, "i") },
{ pinyin: new RegExp(normalizedKeyword, "i") },
];
}
const res = await db
.collection("hlw-diagnosis")
.find(query)
.sort({ _id: -1 })
.skip((page - 1) * pageSize)
.limit(pageSize)
.toArray();
const total = await db.collection("hlw-diagnosis").countDocuments(query);
const collection = db.collection("hlw-diagnosis");
let res;
if (normalizedKeyword) {
// Keep fuzzy search behavior, but bring an exact match for any searchable
// field to the front before applying pagination.
const exactKeyword = `^${normalizedKeyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`;
res = await collection.aggregate([
{ $match: query },
{
$addFields: {
exactKeywordMatch: {
$cond: [
{
$or: [
{ $regexMatch: { input: { $ifNull: ["$name", ""] }, regex: exactKeyword, options: "i" } },
{ $regexMatch: { input: { $ifNull: ["$code", ""] }, regex: exactKeyword, options: "i" } },
{ $regexMatch: { input: { $ifNull: ["$category", ""] }, regex: exactKeyword, options: "i" } },
{ $regexMatch: { input: { $ifNull: ["$pinyin", ""] }, regex: exactKeyword, options: "i" } },
],
},
1,
0,
],
},
},
},
{ $sort: { exactKeywordMatch: -1, _id: -1 } },
{ $skip: (page - 1) * pageSize },
{ $limit: pageSize },
{ $project: { exactKeywordMatch: 0 } },
]).toArray();
} else {
res = await collection
.find(query)
.sort({ _id: -1 })
.skip((page - 1) * pageSize)
.limit(pageSize)
.toArray();
}
const total = await collection.countDocuments(query);
return {
success: true,
message: "查询成功",

View File

@ -40,7 +40,9 @@ function validatePatient(item) {
const mobile = typeof item.mobile === 'string' ? item.mobile.trim() : '';
if (!name) return { success: false, message: '姓名不能为空' };
if (!certNo) return { success: false, message: '证件号不能为空' };
if (certNo && !validator.isChinaId(certNo)) {
return { success: false, message: '证件号格式不正确' };
}
if (!mobile) return { success: false, message: '手机号不能为空' };
if (!validator.isMobile(mobile)) {
return { success: false, message: '手机号格式不正确' };
@ -64,7 +66,7 @@ function validatePatient(item) {
},
data: {
accountId: accountResult.accountId,
anotherIdNo: Sm4Util.encryptDataForSm3(certNo),
anotherIdNo: certNo ? Sm4Util.encryptDataForSm3(certNo) : '',
anotherName: Sm4Util.encryptDataForSm3(name),
anotherMobile: Sm4Util.encryptDataForSm3(mobile),
address,
@ -77,16 +79,17 @@ async function addHlwPatient(item, db) {
const validation = validatePatient(item);
if (!validation.success) return validation;
const exists = await db.collection(COLLECTION_NAME).countDocuments({
accountId: validation.data.accountId,
anotherIdNo: validation.data.anotherIdNo,
disabled: false,
});
if (exists > 0) {
return { success: false, message: '当前账户下已存在相同证件号的就诊人' };
if (validation.origin.certNo) {
const exists = await db.collection(COLLECTION_NAME).countDocuments({
accountId: validation.data.accountId,
anotherIdNo: validation.data.anotherIdNo,
disabled: false,
});
if (exists > 0) {
return { success: false, message: '当前账户下已存在相同证件号的就诊人' };
}
}
console.log(validation.data)
// if (validation.origin.certNo) {
const res = await hnHis({
type: 'addHisCustomer',
name: validation.origin.name,
@ -95,6 +98,7 @@ async function addHlwPatient(item, db) {
address: validation.origin.address,
})
if (!res.success) return res;
// }
const result = await db.collection(COLLECTION_NAME).insertOne({
...validation.data,
@ -117,14 +121,16 @@ async function autoAddHlwPatient(item, db) {
const validation = validatePatient(item);
if (!validation.success) return validation;
const exists = await db.collection(COLLECTION_NAME).countDocuments({
accountId: validation.data.accountId,
anotherIdNo: validation.data.anotherIdNo,
disabled: false,
});
if (validation.origin.certNo) {
const exists = await db.collection(COLLECTION_NAME).countDocuments({
accountId: validation.data.accountId,
anotherIdNo: validation.data.anotherIdNo,
disabled: false,
});
if (exists > 0) {
return { success: true, data: true, message: '就诊人已存在' };
if (exists > 0) {
return { success: true, data: true, message: '就诊人已存在' };
}
}
const result = await addHlwPatient(item, db);

View File

@ -75,8 +75,9 @@ async function addHisCustomer(item) {
const address = typeof item.address === 'string' ? item.address.trim() : '';
const psnToken = typeof item.psnToken === 'string' ? item.psnToken.trim() : '';
const mdtrt_cert_no = typeof item.mdtrt_cert_no === 'string' ? item.mdtrt_cert_no.trim() : '';
const ecToken = typeof item.ecToken === 'string' ? item.ecToken.trim() : '';
const card_sn = typeof item.card_sn === 'string' ? item.card_sn.trim() : '';
if (!name || !idCard || !mobile) {
if (!name || !mobile) {
return { success: false, message: '姓名、证件号、手机号不能为空' }
}
const payload = {
@ -92,16 +93,15 @@ async function addHisCustomer(item) {
payload.psnToken = psnToken;
payload.buildType = "2";
// payload.mdtrt_cert_type = '01'
}
// 线下医保建档
// mdtrt_cert_type 就诊凭证类型 01 | 03
// mdtrt_cert_no 就诊凭证号码 mdtrt_cert_type为01 填写电子凭证令牌 | mdtrt_cert_type为03 填写社会保障卡卡号
if (mdtrt_cert_no) {
} else if (mdtrt_cert_no) {
payload.mdtrt_cert_type = '03';
payload.mdtrt_cert_no = mdtrt_cert_no;
payload.card_sn = card_sn; //就诊凭证类型为“03”时填写社会保障卡内码
} else if (ecToken) {
payload.buildType = '2';
payload.mdtrt_cert_type = '01';
payload.mdtrt_cert_no = ecToken;
}
const { data, status_code, message } = await request.main(
url,
payload,