fix: 调整需求开发
This commit is contained in:
parent
1c149c9c50
commit
10b519082e
52
.env.dev
52
.env.dev
@ -22,58 +22,6 @@ CONFIG_ALIPAY_MINI_APPID = 2021004129669493
|
||||
CONFIG_ALIPAY_MINI_PUBLIC_KEY=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo7Ulswxr/3dJpX0RrPylV9rR3STdwGtcbV4ppNEiR0R37/9L/JpO/Y5gbEaaPn9S/xALzGKwI4N3/v1H2x566LJIQvzh5szCviy20NNGc+TbyxLJ9gLBxcsBDT8+o+a8CdaHbU/xvutZvLRa34D5pa6lcDCONC6q8flwWmXemUZ5widtW10F+4Niyufr+8hKkIpRfQMWYbsuA3EpXwjhztJglxPnjMN7r33vq9Pd5cU+KOOGuelXdB8aMwWcaXbffb7OKaTtZYnbUezJKbqZ+LtYgGF8+E5CKP7tzlEnZvQZwlPgS516EG3WRcCqjnEYO7RpG/eZ/0569d4TW8UPrwIDAQAB
|
||||
CONFIG_ALIPAY_MINI_PRIVATE_KEY=MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCirWNE4gjlUOV6+PNo6CPyLUnJOe/yw8ODZInBun6P2h3rs33WBLFBGdbV9Ct5pXXou48l2icKPsFAWVI5MmtZUIHGBkdvxzQeLNeKpKlJvpujWS/oLQq232MeM7YmWa4tjr40SHqlhVzxcgYST55jBXmVo9KJNrBj5i5zlR9N49x5EJZrUXsVUprsavk1z1JJyUKthw/FrycGsS47YQ5jHyQUFsqEviyOY5Bgz4qZ4xhHJ7p7zhwkiXHqscyeF0+Eu1xf4jS23jlbOiV+5yPo4eWcHElf0aD+k+ngPVpgVqNHMYBTSW1u8pFCpUVDv02QUNg1NRJpBcTAtnLY7yyhAgMBAAECggEAKl54ZV9tq7QnIKEoXGFhw1iSSCuj2BmumVEdI+zbu8G2W0YzdwNf2zHMp4rhq9wxWn1HKjeoRV3U3VqmNKlyFiAGvLmz72P1qgpJ1R1Pzhor1R6MgK8ChGCoIxyay2A+IjV0So+6CdQzQdtNw4UjHm01noi9c+lfhZVU8dub/J/62cGCfcBh/NX+juyI+5Vb9BLnG0CbFZ1SbssK8FN9w6MiKNq/fgIdMyvZXWt9Zoj21Bc9eyxd0EUPp800P6eeThzwNVyeSOETehIDSGwndrbxbLX1nCmievnEYZ/pT6T0J+UuPkdLjZq+VQQry0IxAlVvRim33JS4w99gBYIHcQKBgQDSQsEL5QtSLNg3xs5R3vMG+C8wJbm0eYiPlpSLcFgLxedoxRPdjzeT/Wfi245mA1L8NADRn37gfgPukc5TgWzTV340KzlScAv0Y2HHiQV9HII6b5I/f2S3AsM2IY4jDN+PryBDT5mTYUwW7LhtxC97LqCozPhY6aPozoD6VKBhFQKBgQDGEMAcrPSmXNjxhg/1lPJoe7fe4A3uyLPfHIVqZTFSxrfzwteL3bMuhsx8eSNnmT35trQfKXYT8Pz85IWGcoBPuhDkrjDQy+w5u4QeRcyJQRyUqtDxruIwNbvN/EL6FME24u6bIng6FjHW+0j2SFrFIWpUo1v3B5mSRGzvTWZIXQKBgAXI1lzIaV5SY4RtUMHJ22/Gpi2x7AD+Eva4Us/fQFzajfpA6cH/razBnPkNx0YhM0hC+f7KX4g3j04h7sY3WlIaJxuyULpm4L1ReqAtsdv2S9LlvOVIUPID/nDfzXRDZ2chj0aqyvPWCmzbCFvkuNWvu7XJJc/0ZZEDFWNNrrfdAoGAB/2dAosnw55zUisqcan0rapwHJnFdiC6wQGRkF1MxB4oRblSoN3s4K+niE4K058nFO/+DFPL47oROtKtYGM2MgE7tT4h0gqoGRwrlF0Uh2UNFiJeE6IOlgtU7FaFDTnRIbm+DpwxZYaHtDinFOiOYO2cMJX2m9y+oVwJWQ4yVjECgYEAg01aEYSOYQVeeub8g1e1wHKj3L4v/xECpZNzoQWF/WhZDHiKCqN2VD54XYl0dUHFLqBXn01tfg0gt4kO/roiWJdH273DBYu52ttwNdcRVlud6RlTBDO//M8dglrxOWz51HhSTepOXmJflF1dept9OPVddzQAJdFN65umF27DIVI=
|
||||
|
||||
## 蜂鸟即时配送请求域名
|
||||
CONFIG_FENGNIAO_API_DOMAIN=https://exam-anubis.ele.me
|
||||
## 蜂鸟即时配送分配的 appId 和 appSecret
|
||||
CONFIG_FENGNIAO_APPID=2711175259951256784
|
||||
CONFIG_FENGNIAO_SECRET=744c587a-61eb-4dc5-a226-e88b7f7b15d0
|
||||
## 蜂鸟即时配送 后台管理页面 -> 应用管理 -> 查看沙箱环境 -> 商户信息的商户ID 测试(15649332) | 正式(15549684)
|
||||
CONFIG_FENGNIAO_MERCHANT_ID=15649332
|
||||
## 蜂鸟即时配送 后台管理页面 -> 应用管理 -> 查看沙箱环境 -> 门店信息的门店编码 测试(testorder001) | 正式(ZY108)
|
||||
CONFIG_FENGNIAO_OUT_SHOP_CODE=testorder001
|
||||
## 蜂鸟即时配送 商家授权回调地址
|
||||
CONFIG_FENGNIAO_AUTH_URL=https://patient.youcan365.com/zyt/fengniao-auth
|
||||
|
||||
|
||||
## (邮乐)邮政快递 测试环境请求域名、分配的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=1100241354944
|
||||
|
||||
|
||||
## 海宁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
|
||||
## 银联支付分配的appId
|
||||
CONFIG_UNION_PAY_APPID=10037ca764636bbc01647d1ef4e10009
|
||||
## 银联支付分配的appKey
|
||||
CONFIG_UNION_PAY_APPKEY=389d830dcc0c46a58b0ea1962041e497
|
||||
## 银联支付分配的商户号
|
||||
CONFIG_UNION_PAY_MID=898201612345678
|
||||
## 银联支付分配的终端号
|
||||
CONFIG_UNION_PAY_TID=88880001
|
||||
## 银联支付回调地址
|
||||
CONFIG_UNION_PAY_NOTIFY=https://patient.youcan365.com/zyt/union-pay-notify
|
||||
## 银联支付金额
|
||||
CONFIG_DEV_UNION_PAY_AMOUNT=0.02
|
||||
|
||||
CONFIG_SKIP_SMS_VERIFY_CODE=YES
|
||||
|
||||
|
||||
|
||||
57
.env.pro
57
.env.pro
@ -1,5 +1,5 @@
|
||||
CONFIG_DB_USERNAME=zytAdmin
|
||||
CONFIG_DB_PASSWORD=Lie89763!Zhy
|
||||
CONFIG_DB_USERNAME=hnhlwYyAdmin
|
||||
CONFIG_DB_PASSWORD=Lie89763!HnhlwYy
|
||||
CONFIG_DB_HOST=192.168.0.4
|
||||
CONFIG_DB_PORT=27017
|
||||
CONFIG_NODE_ENV=production
|
||||
@ -13,57 +13,8 @@ CONFIG_ZYB_YB_APPID=7D8D8E29924048539165F5E2E08C4616
|
||||
CONFIG_ZYB_YB_SECRET=77F9610253B94263BCF8DBB61BF90D92
|
||||
CONFIG_SMS_GATEWAY_DISABLED=false
|
||||
|
||||
## 支付宝回调地址
|
||||
CONFIG_ALIPAY_PAY_NOTIFY=https://hlw.zytzymz.com/alipay-pay-notify
|
||||
## 支付宝小程序appId
|
||||
CONFIG_ALIPAY_MINI_APPID = 2021005110656858
|
||||
CONFIG_ALIPAY_MINI_PUBLIC_KEY=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo7Ulswxr/3dJpX0RrPylV9rR3STdwGtcbV4ppNEiR0R37/9L/JpO/Y5gbEaaPn9S/xALzGKwI4N3/v1H2x566LJIQvzh5szCviy20NNGc+TbyxLJ9gLBxcsBDT8+o+a8CdaHbU/xvutZvLRa34D5pa6lcDCONC6q8flwWmXemUZ5widtW10F+4Niyufr+8hKkIpRfQMWYbsuA3EpXwjhztJglxPnjMN7r33vq9Pd5cU+KOOGuelXdB8aMwWcaXbffb7OKaTtZYnbUezJKbqZ+LtYgGF8+E5CKP7tzlEnZvQZwlPgS516EG3WRcCqjnEYO7RpG/eZ/0569d4TW8UPrwIDAQAB
|
||||
CONFIG_ALIPAY_MINI_PRIVATE_KEY=MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCirWNE4gjlUOV6+PNo6CPyLUnJOe/yw8ODZInBun6P2h3rs33WBLFBGdbV9Ct5pXXou48l2icKPsFAWVI5MmtZUIHGBkdvxzQeLNeKpKlJvpujWS/oLQq232MeM7YmWa4tjr40SHqlhVzxcgYST55jBXmVo9KJNrBj5i5zlR9N49x5EJZrUXsVUprsavk1z1JJyUKthw/FrycGsS47YQ5jHyQUFsqEviyOY5Bgz4qZ4xhHJ7p7zhwkiXHqscyeF0+Eu1xf4jS23jlbOiV+5yPo4eWcHElf0aD+k+ngPVpgVqNHMYBTSW1u8pFCpUVDv02QUNg1NRJpBcTAtnLY7yyhAgMBAAECggEAKl54ZV9tq7QnIKEoXGFhw1iSSCuj2BmumVEdI+zbu8G2W0YzdwNf2zHMp4rhq9wxWn1HKjeoRV3U3VqmNKlyFiAGvLmz72P1qgpJ1R1Pzhor1R6MgK8ChGCoIxyay2A+IjV0So+6CdQzQdtNw4UjHm01noi9c+lfhZVU8dub/J/62cGCfcBh/NX+juyI+5Vb9BLnG0CbFZ1SbssK8FN9w6MiKNq/fgIdMyvZXWt9Zoj21Bc9eyxd0EUPp800P6eeThzwNVyeSOETehIDSGwndrbxbLX1nCmievnEYZ/pT6T0J+UuPkdLjZq+VQQry0IxAlVvRim33JS4w99gBYIHcQKBgQDSQsEL5QtSLNg3xs5R3vMG+C8wJbm0eYiPlpSLcFgLxedoxRPdjzeT/Wfi245mA1L8NADRn37gfgPukc5TgWzTV340KzlScAv0Y2HHiQV9HII6b5I/f2S3AsM2IY4jDN+PryBDT5mTYUwW7LhtxC97LqCozPhY6aPozoD6VKBhFQKBgQDGEMAcrPSmXNjxhg/1lPJoe7fe4A3uyLPfHIVqZTFSxrfzwteL3bMuhsx8eSNnmT35trQfKXYT8Pz85IWGcoBPuhDkrjDQy+w5u4QeRcyJQRyUqtDxruIwNbvN/EL6FME24u6bIng6FjHW+0j2SFrFIWpUo1v3B5mSRGzvTWZIXQKBgAXI1lzIaV5SY4RtUMHJ22/Gpi2x7AD+Eva4Us/fQFzajfpA6cH/razBnPkNx0YhM0hC+f7KX4g3j04h7sY3WlIaJxuyULpm4L1ReqAtsdv2S9LlvOVIUPID/nDfzXRDZ2chj0aqyvPWCmzbCFvkuNWvu7XJJc/0ZZEDFWNNrrfdAoGAB/2dAosnw55zUisqcan0rapwHJnFdiC6wQGRkF1MxB4oRblSoN3s4K+niE4K058nFO/+DFPL47oROtKtYGM2MgE7tT4h0gqoGRwrlF0Uh2UNFiJeE6IOlgtU7FaFDTnRIbm+DpwxZYaHtDinFOiOYO2cMJX2m9y+oVwJWQ4yVjECgYEAg01aEYSOYQVeeub8g1e1wHKj3L4v/xECpZNzoQWF/WhZDHiKCqN2VD54XYl0dUHFLqBXn01tfg0gt4kO/roiWJdH273DBYu52ttwNdcRVlud6RlTBDO//M8dglrxOWz51HhSTepOXmJflF1dept9OPVddzQAJdFN65umF27DIVI=
|
||||
|
||||
## 蜂鸟即时配送请求域名
|
||||
CONFIG_FENGNIAO_API_DOMAIN=https://open-anubis.ele.me
|
||||
## 蜂鸟即时配送分配的 appId 和 appSecret
|
||||
CONFIG_FENGNIAO_APPID=2711175259951256784
|
||||
CONFIG_FENGNIAO_SECRET=744c587a-61eb-4dc5-a226-e88b7f7b15d0
|
||||
## 蜂鸟即时配送 后台管理页面 -> 应用管理 -> 查看沙箱环境 -> 商户信息的商户ID (生产环境目前未提供)
|
||||
CONFIG_FENGNIAO_MERCHANT_ID=15549684
|
||||
## 蜂鸟即时配送 后台管理页面 -> 应用管理 -> 查看沙箱环境 -> 门店信息的门店编码 (生产环境目前未提供)
|
||||
CONFIG_FENGNIAO_OUT_SHOP_CODE=ZY108
|
||||
## 蜂鸟即时配送 商家授权回调地址
|
||||
CONFIG_FENGNIAO_AUTH_URL=https://hlw.zytzymz.com/fengniao-auth
|
||||
|
||||
## 邮政快递 测试环境请求域名、分配的orgId、AES加密秘钥以及IV
|
||||
CONFIG_EMS_API_DOMAIN=https://topup.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/open
|
||||
## 邮政 协议用户编码
|
||||
CONFIG_EMS_USER_CODE=NNLXIQRYV
|
||||
## 邮政 生产授权码
|
||||
CONFIG_EMS_AUTH_CODE=SKtohTy8lN43fJKH
|
||||
## 邮政 生产签名钥匙
|
||||
CONFIG_EMS_SIGN_SECRET=OGdLZ1ZIN0VJVTN6c1RSeA==
|
||||
## 邮政 协议客户号
|
||||
CONFIG_EMS_SENDER_NO=1100241354944
|
||||
|
||||
|
||||
## 震元his api
|
||||
CONFIG_ZYT_HIS_BASE_URL=http://115.239.161.48:8083
|
||||
|
||||
## 银联支付请求基础路径
|
||||
CONFIG_UNION_PAY_BASE_URL=https://api-mop.chinaums.com/v1
|
||||
## 银联支付分配的appId
|
||||
CONFIG_UNION_PAY_APPID=8a9481d09c055fef019d6755a0120ca8
|
||||
## 银联支付分配的appKey
|
||||
CONFIG_UNION_PAY_APPKEY=790709c7a7e444c8b7e24688eb24f54a
|
||||
## 银联支付分配的商户号
|
||||
CONFIG_UNION_PAY_MID=89833068011004Y
|
||||
## 银联支付分配的终端号
|
||||
CONFIG_UNION_PAY_TID=15K1FZZN
|
||||
## 银联支付回调地址
|
||||
CONFIG_UNION_PAY_NOTIFY=https://hlw.zytzymz.com/union-pay-notify
|
||||
|
||||
@ -7,6 +7,9 @@ const trigger = require("./trigger");
|
||||
const { ObjectId } = require("mongodb");
|
||||
const timeApi = require("../hlw-time-duration");
|
||||
const { encryptOrderFields, decryptOrderFields, decryptOrderList } = require("./format");
|
||||
const { getRequiredDrugAccessLevels, doctorMeetsDrugAccessLevels } = require("../drug-access-level");
|
||||
|
||||
const doctorOrPharmacistOfflineMessage = "当前医生或药师不在线,暂无法提交问诊单,请稍后再试";
|
||||
|
||||
const hisPayStatus = {
|
||||
0: "已支付",
|
||||
@ -155,7 +158,8 @@ async function addConsultOrder(item) {
|
||||
|
||||
async function addOnlineConsultOrder(params) {
|
||||
try {
|
||||
params.registerId = common.generateUniqueStr('GH');
|
||||
params.registerId = await generateRegisterId();
|
||||
const requiredDrugAccessLevels = await getRequiredDrugAccessLevels(db, params.drugs);
|
||||
const storeApi = require("../drug-store");
|
||||
const { success, list, message } = await storeApi(
|
||||
{
|
||||
@ -195,14 +199,19 @@ async function addOnlineConsultOrder(params) {
|
||||
deptName: 1,
|
||||
deptCode: 1,
|
||||
onlineStatus: 1,
|
||||
antibiotic_level: 1,
|
||||
tumor_use: 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!doctor) {
|
||||
return { success: false, message: "未查询到医生" };
|
||||
return { success: false, message: doctorOrPharmacistOfflineMessage };
|
||||
}
|
||||
if (doctor.onlineStatus !== "online") {
|
||||
return { success: false, message: "医生未在线, 请重新选择医生" };
|
||||
return { success: false, message: doctorOrPharmacistOfflineMessage };
|
||||
}
|
||||
if (!doctorMeetsDrugAccessLevels(doctor, requiredDrugAccessLevels)) {
|
||||
return { success: false, message: doctorOrPharmacistOfflineMessage };
|
||||
}
|
||||
params.doctorName = doctor.doctorName || "";
|
||||
params.doctorTitle = doctor.title || "";
|
||||
@ -212,7 +221,12 @@ async function addOnlineConsultOrder(params) {
|
||||
} else {
|
||||
// 随机选取一位医生 (单量少的权重大)
|
||||
const doctorApi = require("../hlw-doctor");
|
||||
const data = await doctorApi({ type: "getOptimalRecommendDoctor", doctorType: 'west' }, db);
|
||||
const data = await doctorApi({
|
||||
type: "getOptimalRecommendDoctor",
|
||||
doctorType: 'west',
|
||||
minimumAntibioticLevel: requiredDrugAccessLevels.antibioticLevel,
|
||||
minimumTumorUse: requiredDrugAccessLevels.tumorUse,
|
||||
}, db);
|
||||
if (data.success && data.data) {
|
||||
params.doctorName = data.data.doctorName || "";
|
||||
params.doctorCode = data.data.doctorNo;
|
||||
@ -220,12 +234,12 @@ async function addOnlineConsultOrder(params) {
|
||||
params.deptName = data.data.deptName || "";
|
||||
params.unitCode = data.data.deptCode || "";
|
||||
} else {
|
||||
return { success: false, message: data.message };
|
||||
return { success: false, message: doctorOrPharmacistOfflineMessage };
|
||||
}
|
||||
}
|
||||
const onlineYaoshiCount = await db.collection("hlw-doctor").countDocuments({ corpId: params.corpId, onlineStatus: "online", job: "pharmacist" });
|
||||
if (onlineYaoshiCount === 0) {
|
||||
return { success: false, message: "当前医生或药师不在线,暂无法提交问诊单,请稍后再试" };
|
||||
return { success: false, message: doctorOrPharmacistOfflineMessage };
|
||||
}
|
||||
const imAccountResult = await tencentIM({
|
||||
type: "importAccount",
|
||||
@ -2345,3 +2359,24 @@ async function autoHandleExpireOrder() {
|
||||
return { success: false, message: e.message || "操作失败" };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function generateRegisterId(db, type = 'consult-register-id') {
|
||||
const key = dayjs().format('YYYY-MM-DD');
|
||||
const day = dayjs().format('YYMMDD')
|
||||
const collection = db.collection('sequence-counter');
|
||||
|
||||
const result = await collection.findOneAndUpdate(
|
||||
{ key, type }, // 添加 type 字段区分不同场景
|
||||
{
|
||||
$inc: { sequence: 1 },
|
||||
$setOnInsert: { key, type }
|
||||
},
|
||||
{
|
||||
upsert: true,
|
||||
returnDocument: 'after'
|
||||
}
|
||||
);
|
||||
const sequence = result.sequence;
|
||||
return `${day}${String(sequence).padStart(4, '0')}`;
|
||||
}
|
||||
|
||||
@ -53,7 +53,7 @@ async function triggerDelayedTask({ triggerTaskId }) {
|
||||
let reason = `本次问诊已取消,取消原因:${
|
||||
orderStatus === "pending" ? "超时未接诊" : "超时未处理"
|
||||
}。${
|
||||
orderSource === "MATEPAD" ? "请联系线下退费。" : "费用将于48小时内退还您的支付账户。"
|
||||
orderSource === "MATEPAD" ? "请联系线下退费。" : ""
|
||||
}`;
|
||||
await consultOrder(
|
||||
{
|
||||
|
||||
66
hlw/drug-access-level.js
Normal file
66
hlw/drug-access-level.js
Normal file
@ -0,0 +1,66 @@
|
||||
const { ObjectId } = require("mongodb");
|
||||
|
||||
const ACCESS_LEVEL_MIN = 0;
|
||||
const ACCESS_LEVEL_MAX = 3;
|
||||
|
||||
function normalizeAccessLevel(value) {
|
||||
return Number.isInteger(value) && value >= ACCESS_LEVEL_MIN && value <= ACCESS_LEVEL_MAX
|
||||
? value
|
||||
: ACCESS_LEVEL_MIN;
|
||||
}
|
||||
|
||||
function getHighestDrugAccessLevels(drugs = []) {
|
||||
return drugs.reduce(
|
||||
(levels, drug) => ({
|
||||
antibioticLevel: Math.max(levels.antibioticLevel, normalizeAccessLevel(drug && drug.antibiotic_level)),
|
||||
tumorUse: Math.max(levels.tumorUse, normalizeAccessLevel(drug && drug.tumor_use)),
|
||||
}),
|
||||
{ antibioticLevel: ACCESS_LEVEL_MIN, tumorUse: ACCESS_LEVEL_MIN }
|
||||
);
|
||||
}
|
||||
|
||||
async function getRequiredDrugAccessLevels(db, drugs) {
|
||||
const drugIds = Array.isArray(drugs)
|
||||
? [...new Set(drugs
|
||||
.map(drug => drug && drug._id)
|
||||
.filter(id => ObjectId.isValid(id))
|
||||
.map(id => String(id)))]
|
||||
: [];
|
||||
|
||||
if (drugIds.length === 0) {
|
||||
return getHighestDrugAccessLevels([]);
|
||||
}
|
||||
|
||||
const drugList = await db.collection("drug-info").find(
|
||||
{ _id: { $in: drugIds.map(id => new ObjectId(id)) } },
|
||||
{ projection: { antibiotic_level: 1, tumor_use: 1 } }
|
||||
).toArray();
|
||||
|
||||
return getHighestDrugAccessLevels(drugList);
|
||||
}
|
||||
|
||||
function doctorMeetsDrugAccessLevels(doctor, requiredLevels) {
|
||||
return normalizeAccessLevel(doctor && doctor.antibiotic_level) >= normalizeAccessLevel(requiredLevels && requiredLevels.antibioticLevel)
|
||||
&& normalizeAccessLevel(doctor && doctor.tumor_use) >= normalizeAccessLevel(requiredLevels && requiredLevels.tumorUse);
|
||||
}
|
||||
|
||||
function getDoctorAccessLevelQuery(minimumAntibioticLevel, minimumTumorUse) {
|
||||
const query = {};
|
||||
const antibioticLevel = normalizeAccessLevel(minimumAntibioticLevel);
|
||||
const tumorUse = normalizeAccessLevel(minimumTumorUse);
|
||||
if (antibioticLevel > 0) {
|
||||
query.antibiotic_level = { $gte: antibioticLevel };
|
||||
}
|
||||
if (tumorUse > 0) {
|
||||
query.tumor_use = { $gte: tumorUse };
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
normalizeAccessLevel,
|
||||
getHighestDrugAccessLevels,
|
||||
getRequiredDrugAccessLevels,
|
||||
doctorMeetsDrugAccessLevels,
|
||||
getDoctorAccessLevelQuery,
|
||||
};
|
||||
56
hlw/drug-access-level.test.js
Normal file
56
hlw/drug-access-level.test.js
Normal file
@ -0,0 +1,56 @@
|
||||
const { ObjectId } = require("mongodb");
|
||||
const {
|
||||
normalizeAccessLevel,
|
||||
getHighestDrugAccessLevels,
|
||||
getRequiredDrugAccessLevels,
|
||||
doctorMeetsDrugAccessLevels,
|
||||
getDoctorAccessLevelQuery,
|
||||
} = require("./drug-access-level");
|
||||
|
||||
describe("drug access levels", () => {
|
||||
test("normalizes missing and invalid values to zero", () => {
|
||||
expect([undefined, null, "2", -1, 4].map(normalizeAccessLevel)).toEqual([0, 0, 0, 0, 0]);
|
||||
expect([0, 1, 2, 3].map(normalizeAccessLevel)).toEqual([0, 1, 2, 3]);
|
||||
});
|
||||
|
||||
test("calculates the independent highest levels", () => {
|
||||
expect(getHighestDrugAccessLevels([
|
||||
{ antibiotic_level: 3, tumor_use: 1 },
|
||||
{ antibiotic_level: 1, tumor_use: 2 },
|
||||
{ antibiotic_level: 99 },
|
||||
])).toEqual({ antibioticLevel: 3, tumorUse: 2 });
|
||||
expect(getHighestDrugAccessLevels([])).toEqual({ antibioticLevel: 0, tumorUse: 0 });
|
||||
});
|
||||
|
||||
test("queries valid drug ids and treats missing records as zero", async () => {
|
||||
const validId = new ObjectId();
|
||||
const toArray = jest.fn().mockResolvedValue([{ antibiotic_level: 2 }]);
|
||||
const find = jest.fn().mockReturnValue({ toArray });
|
||||
const db = { collection: jest.fn().mockReturnValue({ find }) };
|
||||
|
||||
await expect(getRequiredDrugAccessLevels(db, [
|
||||
{ _id: validId.toString() },
|
||||
{ _id: "invalid" },
|
||||
{ _id: validId.toString() },
|
||||
])).resolves.toEqual({ antibioticLevel: 2, tumorUse: 0 });
|
||||
expect(find.mock.calls[0][0]._id.$in).toHaveLength(1);
|
||||
expect(find.mock.calls[0][1]).toEqual({ projection: { antibiotic_level: 1, tumor_use: 1 } });
|
||||
});
|
||||
|
||||
test("requires doctors to meet both levels and treats missing fields as zero", () => {
|
||||
const required = { antibioticLevel: 2, tumorUse: 1 };
|
||||
expect(doctorMeetsDrugAccessLevels({ antibiotic_level: 2, tumor_use: 3 }, required)).toBe(true);
|
||||
expect(doctorMeetsDrugAccessLevels({ antibiotic_level: 1, tumor_use: 3 }, required)).toBe(false);
|
||||
expect(doctorMeetsDrugAccessLevels({}, { antibioticLevel: 0, tumorUse: 0 })).toBe(true);
|
||||
expect(doctorMeetsDrugAccessLevels({}, { antibioticLevel: 1, tumorUse: 0 })).toBe(false);
|
||||
});
|
||||
|
||||
test("adds minimum-level query conditions without changing legacy calls", () => {
|
||||
expect(getDoctorAccessLevelQuery()).toEqual({});
|
||||
expect(getDoctorAccessLevelQuery(0, 0)).toEqual({});
|
||||
expect(getDoctorAccessLevelQuery(2, 3)).toEqual({
|
||||
antibiotic_level: { $gte: 2 },
|
||||
tumor_use: { $gte: 3 },
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -239,6 +239,12 @@ async function addHlwDrugInfo(ctx) {
|
||||
if (diagnosisIds.present) {
|
||||
newDrug.applicableDiagnosisIds = diagnosisIds.value;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(newDrug, "antibiotic_level")) {
|
||||
newDrug.antibiotic_level = 0;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(newDrug, "tumor_use")) {
|
||||
newDrug.tumor_use = 0;
|
||||
}
|
||||
newDrug.createTime = Date.now();
|
||||
const res = await db.collection("drug-info").insertOne(newDrug);
|
||||
return { success: true, message: "新增药品成功", id: res.insertedId }
|
||||
|
||||
@ -20,6 +20,8 @@ const drugFieldDict = [
|
||||
{ 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) },
|
||||
{ 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) },
|
||||
// 仅在线药品库使用:管理分类(处方药 / OTC)与剂型
|
||||
{ name: '管理分类', field: 'manage_category', verify: (val, label) => validator.verifyString(val, label) },
|
||||
{ name: '剂型', field: 'dosage_form', verify: (val, label) => validator.verifyString(val, label) },
|
||||
|
||||
@ -52,6 +52,14 @@ const rules = [
|
||||
key: 'title',
|
||||
validate: val => validator.verifyString(val, '职称', 0, 30)
|
||||
},
|
||||
{
|
||||
key: 'antibiotic_level',
|
||||
validate: val => validator.verifyListItem(val, [0, 1, 2, 3], false, '抗生素级别', 0)
|
||||
},
|
||||
{
|
||||
key: 'tumor_use',
|
||||
validate: val => validator.verifyListItem(val, [0, 1, 2, 3], false, '肿瘤使用', 0)
|
||||
},
|
||||
{
|
||||
key: 'intro',
|
||||
validate: val => validator.verifyString(val, '个人介绍', 0, 300)
|
||||
|
||||
@ -2,6 +2,7 @@ const { ObjectId } = require("mongodb");
|
||||
const dayjs = require("dayjs");
|
||||
const peopleUtils = require("./hlw-people-utils");
|
||||
const { recordDoctorRecommendation, getDoctorRecommendationCount } = require("./doctor-count-in-one-minute");
|
||||
const { getDoctorAccessLevelQuery } = require("../drug-access-level");
|
||||
let db = "";
|
||||
module.exports = async (item, mongodb) => {
|
||||
db = mongodb;
|
||||
@ -133,7 +134,12 @@ async function getRecommendDoctor({ storeCode, presType }) {
|
||||
* @param {boolean} recordInOneMinute 是否记录一分钟内获取推荐医生的数量
|
||||
* @returns
|
||||
*/
|
||||
async function getOptimalRecommendDoctor({ recordInOneMinute = false, doctorType }) {
|
||||
async function getOptimalRecommendDoctor({
|
||||
recordInOneMinute = false,
|
||||
doctorType,
|
||||
minimumAntibioticLevel,
|
||||
minimumTumorUse,
|
||||
}) {
|
||||
try {
|
||||
const type = ['west', 'chinese'].includes(doctorType) ? doctorType : 'west';
|
||||
// 查询条件:在线且服务状态启用的医生
|
||||
@ -148,6 +154,7 @@ async function getOptimalRecommendDoctor({ recordInOneMinute = false, doctorType
|
||||
} else if (type === 'west') {
|
||||
query.openWestPres = true;
|
||||
}
|
||||
Object.assign(query, getDoctorAccessLevelQuery(minimumAntibioticLevel, minimumTumorUse));
|
||||
const projection = {
|
||||
doctorNo: 1,
|
||||
doctorName: 1,
|
||||
@ -157,6 +164,8 @@ async function getOptimalRecommendDoctor({ recordInOneMinute = false, doctorType
|
||||
deptCode: 1,
|
||||
onlineStatus: 1,
|
||||
serviceStatus: 1,
|
||||
antibiotic_level: 1,
|
||||
tumor_use: 1,
|
||||
}
|
||||
const doctorList = await db.collection("hlw-doctor").find(query, { projection }).toArray();
|
||||
if (doctorList.length === 0) {
|
||||
@ -657,6 +666,10 @@ async function addHlwPerson(ctx) {
|
||||
if (total > 0) {
|
||||
return { success: false, message: "院内工号已存在" }
|
||||
}
|
||||
if (data.job === 'doctor') {
|
||||
data.antibiotic_level = data.antibiotic_level ?? 0;
|
||||
data.tumor_use = data.tumor_use ?? 0;
|
||||
}
|
||||
const res = await db.collection("hlw-doctor").insertOne({
|
||||
...data,
|
||||
corpId,
|
||||
|
||||
@ -28,6 +28,7 @@ const chineseMedicine = require("./chinese-medicine");
|
||||
const chineseMedicineOrder = require("./chinese-medicine-order");
|
||||
const hlwPatient = require("./hlw-patient");
|
||||
const scannedCardArchive = require("./scanned-card-archive");
|
||||
const selfAuth = require("./self-auth");
|
||||
|
||||
module.exports = async (item, db, context) => {
|
||||
switch (item.type) {
|
||||
@ -273,6 +274,11 @@ module.exports = async (item, db, context) => {
|
||||
case "saveScannedCard":
|
||||
case "getScannedCard":
|
||||
return await scannedCardArchive(item, db);
|
||||
case "createSelfAuthRequest":
|
||||
case "getSelfAuthPatient":
|
||||
case "submitSelfAuth":
|
||||
case "getSelfAuthResult":
|
||||
return await selfAuth(item, db, context);
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
|
||||
@ -196,6 +196,12 @@ async function addOnlineDrugInfo(ctx, db) {
|
||||
if (diagnosisIds.present) {
|
||||
newDrug.applicableDiagnosisIds = diagnosisIds.value;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(newDrug, "antibiotic_level")) {
|
||||
newDrug.antibiotic_level = 0;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(newDrug, "tumor_use")) {
|
||||
newDrug.tumor_use = 0;
|
||||
}
|
||||
newDrug.createTime = Date.now();
|
||||
const res = await db
|
||||
.collection("online-drug-info")
|
||||
|
||||
332
hlw/self-auth/index.js
Normal file
332
hlw/self-auth/index.js
Normal file
@ -0,0 +1,332 @@
|
||||
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-self-auth";
|
||||
const PATIENT_COLLECTION_NAME = "hlw-patient";
|
||||
const AUTH_VALID_MS = 10 * 60 * 1000;
|
||||
const RECORD_RETENTION_MS = 24 * 60 * 60 * 1000;
|
||||
const indexPromises = new WeakMap();
|
||||
|
||||
async function removeLegacyAuthIdIndex(collection) {
|
||||
try {
|
||||
await collection.dropIndex("authId_1");
|
||||
} catch (error) {
|
||||
// 旧索引不存在时无需处理;其他数据库错误仍应暴露,避免留下隐蔽的唯一索引冲突。
|
||||
if (error && (error.code === 27 || error.codeName === "IndexNotFound")) return;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureIndexes(db) {
|
||||
if (!indexPromises.has(db)) {
|
||||
const collection = db.collection(COLLECTION_NAME);
|
||||
const promise = removeLegacyAuthIdIndex(collection)
|
||||
.then(() => Promise.all([
|
||||
collection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }),
|
||||
collection.createIndex({ ownerUserId: 1, status: 1 }),
|
||||
]))
|
||||
.catch((error) => {
|
||||
indexPromises.delete(db);
|
||||
throw error;
|
||||
});
|
||||
indexPromises.set(db, promise);
|
||||
}
|
||||
return indexPromises.get(db);
|
||||
}
|
||||
|
||||
function parseRequestId(value) {
|
||||
const id = typeof value === "string" ? value.trim() : "";
|
||||
if (!/^[a-f\d]{24}$/i.test(id) || !ObjectId.isValid(id)) {
|
||||
return { success: false, message: "授权链接无效" };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
id,
|
||||
objectId: new ObjectId(id),
|
||||
};
|
||||
}
|
||||
|
||||
function isExpired(requestTimestamp, now = Date.now()) {
|
||||
return now - requestTimestamp > AUTH_VALID_MS;
|
||||
}
|
||||
|
||||
function getCurrentUserId(context) {
|
||||
const internalUserId = context && typeof context.userId === "string"
|
||||
? context.userId.trim()
|
||||
: "";
|
||||
return ObjectId.isValid(internalUserId) ? internalUserId : "";
|
||||
}
|
||||
|
||||
function decryptPatient(patient) {
|
||||
return {
|
||||
id: patient._id.toString(),
|
||||
accountId: patient.accountId,
|
||||
name: Sm4Util.decryptDataForSm3(patient.anotherName),
|
||||
certNo: Sm4Util.decryptDataForSm3(patient.anotherIdNo),
|
||||
mobile: Sm4Util.decryptDataForSm3(patient.anotherMobile),
|
||||
address: typeof patient.address === "string" ? patient.address : "",
|
||||
};
|
||||
}
|
||||
|
||||
function maskCertNo(certNo) {
|
||||
if (typeof certNo !== "string" || certNo.length <= 7) return "--";
|
||||
return `${certNo.slice(0, 4)}${"*".repeat(certNo.length - 7)}${certNo.slice(-3)}`;
|
||||
}
|
||||
|
||||
function getPersonInfo(certNo, now = new Date()) {
|
||||
if (!validator.isChinaId(certNo)) return { age: "", sex: "" };
|
||||
const birthYear = Number(certNo.slice(6, 10));
|
||||
const birthMonth = Number(certNo.slice(10, 12));
|
||||
const birthDay = Number(certNo.slice(12, 14));
|
||||
const birthday = new Date(birthYear, birthMonth - 1, birthDay);
|
||||
if (
|
||||
birthday.getFullYear() !== birthYear
|
||||
|| birthday.getMonth() + 1 !== birthMonth
|
||||
|| birthday.getDate() !== birthDay
|
||||
|| birthday > now
|
||||
) {
|
||||
return { age: "", sex: "" };
|
||||
}
|
||||
let age = now.getFullYear() - birthYear;
|
||||
const birthdayPassed = now.getMonth() + 1 > birthMonth
|
||||
|| (now.getMonth() + 1 === birthMonth && now.getDate() >= birthDay);
|
||||
if (!birthdayPassed) age -= 1;
|
||||
return {
|
||||
age: Math.max(0, age),
|
||||
sex: Number(certNo.charAt(16)) % 2 === 0 ? "女" : "男",
|
||||
};
|
||||
}
|
||||
|
||||
async function findRequest(db, parsed) {
|
||||
return db.collection(COLLECTION_NAME).findOne({ _id: parsed.objectId });
|
||||
}
|
||||
|
||||
async function createSelfAuthRequest(item, db, context) {
|
||||
const patientId = item && typeof item.patientId === "string" ? item.patientId.trim() : "";
|
||||
if (!ObjectId.isValid(patientId)) {
|
||||
return { success: false, message: "就诊人ID格式不正确" };
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureIndexes(db);
|
||||
const ownerUserId = getCurrentUserId(context);
|
||||
const accountId = item && typeof item.accountId === "string" ? item.accountId.trim() : "";
|
||||
if (!ownerUserId || !accountId) return { success: false, message: "登录信息无效,请重新登录" };
|
||||
|
||||
const patient = await db.collection(PATIENT_COLLECTION_NAME).findOne({
|
||||
_id: new ObjectId(patientId),
|
||||
accountId,
|
||||
disabled: false,
|
||||
});
|
||||
if (!patient) return { success: false, message: "就诊人不存在或无权操作" };
|
||||
|
||||
const requestTimestamp = Date.now();
|
||||
const insertResult = await db.collection(COLLECTION_NAME).insertOne({
|
||||
patientId: new ObjectId(patientId),
|
||||
ownerAccountId: accountId,
|
||||
ownerUserId,
|
||||
requestTimestamp,
|
||||
status: "pending",
|
||||
attempts: 0,
|
||||
createdAt: new Date(requestTimestamp),
|
||||
updatedAt: new Date(requestTimestamp),
|
||||
expiresAt: new Date(requestTimestamp + RECORD_RETENTION_MS),
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "授权请求创建成功",
|
||||
data: { id: insertResult.insertedId.toString() },
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message || "授权请求创建失败" };
|
||||
}
|
||||
}
|
||||
|
||||
async function getSelfAuthPatient(item, db) {
|
||||
const parsed = parseRequestId(item && item.id);
|
||||
if (!parsed.success) return parsed;
|
||||
|
||||
try {
|
||||
await ensureIndexes(db);
|
||||
const request = await findRequest(db, parsed);
|
||||
if (!request) return { success: false, message: "授权链接不存在或已失效" };
|
||||
if (request.status !== "success" && isExpired(request.requestTimestamp)) {
|
||||
return { success: false, message: "授权链接已过期,请重新发起授权" };
|
||||
}
|
||||
|
||||
const patient = await db.collection(PATIENT_COLLECTION_NAME).findOne({
|
||||
_id: request.patientId,
|
||||
accountId: request.ownerAccountId,
|
||||
disabled: false,
|
||||
});
|
||||
if (!patient) return { success: false, message: "就诊人不存在" };
|
||||
|
||||
const data = decryptPatient(patient);
|
||||
const personInfo = getPersonInfo(data.certNo);
|
||||
return {
|
||||
success: true,
|
||||
message: "获取就诊人成功",
|
||||
data: {
|
||||
name: data.name,
|
||||
certNo: maskCertNo(data.certNo),
|
||||
age: personInfo.age,
|
||||
sex: personInfo.sex,
|
||||
status: request.status,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message || "获取授权信息失败" };
|
||||
}
|
||||
}
|
||||
|
||||
async function submitSelfAuth(item, db, context) {
|
||||
const parsed = parseRequestId(item && item.id);
|
||||
if (!parsed.success) return parsed;
|
||||
const psnToken = item && typeof item.psnToken === "string" ? item.psnToken.trim() : "";
|
||||
if (!psnToken || psnToken.length > 2048) {
|
||||
return { success: false, message: "医保授权信息无效" };
|
||||
}
|
||||
|
||||
const collection = db.collection(COLLECTION_NAME);
|
||||
try {
|
||||
await ensureIndexes(db);
|
||||
const request = await findRequest(db, parsed);
|
||||
if (!request) return { success: false, message: "授权链接不存在或已失效" };
|
||||
if (request.status === "success") {
|
||||
return { success: true, message: "已完成授权", data: { status: "success" } };
|
||||
}
|
||||
if (isExpired(request.requestTimestamp)) {
|
||||
return { success: false, message: "授权链接已过期,请重新发起授权" };
|
||||
}
|
||||
|
||||
const authorizerUserId = getCurrentUserId(context);
|
||||
if (!authorizerUserId) return { success: false, message: "登录信息无效,请重新登录" };
|
||||
|
||||
const claimed = await collection.findOneAndUpdate(
|
||||
{ _id: parsed.objectId, status: { $in: ["pending", "failed"] } },
|
||||
{
|
||||
$set: {
|
||||
status: "processing",
|
||||
authorizerUserId,
|
||||
errorMessage: "",
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
$inc: { attempts: 1 },
|
||||
},
|
||||
{ returnDocument: "after" }
|
||||
);
|
||||
if (!claimed) {
|
||||
const latest = await collection.findOne({ _id: parsed.objectId });
|
||||
if (latest && latest.status === "success") {
|
||||
return { success: true, message: "已完成授权", data: { status: "success" } };
|
||||
}
|
||||
return { success: false, message: "授权正在处理中,请勿重复提交", data: { status: "processing" } };
|
||||
}
|
||||
|
||||
const patientRecord = await db.collection(PATIENT_COLLECTION_NAME).findOne({
|
||||
_id: claimed.patientId,
|
||||
accountId: claimed.ownerAccountId,
|
||||
disabled: false,
|
||||
});
|
||||
if (!patientRecord) throw new Error("就诊人不存在");
|
||||
const patient = decryptPatient(patientRecord);
|
||||
|
||||
const hisResult = await hnHis({
|
||||
type: "addHisCustomer",
|
||||
idCard: patient.certNo,
|
||||
name: patient.name,
|
||||
mobile: patient.mobile,
|
||||
address: patient.address,
|
||||
psnToken,
|
||||
});
|
||||
const patients = Array.isArray(hisResult && hisResult.list) ? hisResult.list : [];
|
||||
const hisArchive = patients.find((entry) => entry && entry.socialno === patient.certNo);
|
||||
if (!hisResult || !hisResult.success || !hisArchive) {
|
||||
throw new Error((hisResult && hisResult.message) || "医保建档失败,未找到匹配档案");
|
||||
}
|
||||
if (hisArchive.isyb !== "1") throw new Error("医保建档失败");
|
||||
|
||||
const result = {
|
||||
...hisArchive,
|
||||
socialno: hisArchive.socialno || patient.certNo,
|
||||
tmbxx: Array.isArray(hisResult.tmbxx) ? hisResult.tmbxx : [],
|
||||
};
|
||||
const completedAt = new Date();
|
||||
await collection.updateOne(
|
||||
{ _id: parsed.objectId, status: "processing" },
|
||||
{
|
||||
$set: {
|
||||
status: "success",
|
||||
encryptedResult: Sm4Util.encryptDataForSm3(JSON.stringify(result)),
|
||||
encryption: "SM4",
|
||||
completedAt,
|
||||
updatedAt: completedAt,
|
||||
},
|
||||
$unset: { errorMessage: "" },
|
||||
}
|
||||
);
|
||||
return { success: true, message: "授权并建档成功", data: { status: "success" } };
|
||||
} catch (error) {
|
||||
await collection.updateOne(
|
||||
{ _id: parsed.objectId, status: "processing" },
|
||||
{ $set: { status: "failed", errorMessage: error.message || "医保建档失败", updatedAt: new Date() } }
|
||||
).catch(() => { });
|
||||
return { success: false, message: error.message || "医保建档失败", data: { status: "failed" } };
|
||||
}
|
||||
}
|
||||
|
||||
async function getSelfAuthResult(item, db, context) {
|
||||
const parsed = parseRequestId(item && item.id);
|
||||
console.log('parsed: ', parsed)
|
||||
if (!parsed.success) return parsed;
|
||||
try {
|
||||
await ensureIndexes(db);
|
||||
const ownerUserId = getCurrentUserId(context);
|
||||
if (!ownerUserId) return { success: false, message: "登录信息无效,请重新登录" };
|
||||
const request = await findRequest(db, parsed);
|
||||
console.log('request: ', request)
|
||||
if (!request || request.ownerUserId !== ownerUserId) {
|
||||
return { success: false, message: "授权记录不存在或无权访问" };
|
||||
}
|
||||
|
||||
if (request.status === "success") {
|
||||
if (!request.encryptedResult || request.encryption !== "SM4") {
|
||||
return { success: false, message: "授权结果无效" };
|
||||
}
|
||||
const hisArchive = JSON.parse(Sm4Util.decryptDataForSm3(request.encryptedResult));
|
||||
return { success: true, message: "授权成功", data: { status: "success", hisArchive } };
|
||||
}
|
||||
if (request.status !== "processing" && isExpired(request.requestTimestamp)) {
|
||||
return { success: true, message: "授权链接已过期", data: { status: "expired" } };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
message: request.errorMessage || "等待就诊人授权",
|
||||
data: { status: request.status, errorMessage: request.errorMessage || "" },
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message || "查询授权结果失败" };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = async (item, db, context) => {
|
||||
switch (item.type) {
|
||||
case "createSelfAuthRequest":
|
||||
return createSelfAuthRequest(item, db, context);
|
||||
case "getSelfAuthPatient":
|
||||
return getSelfAuthPatient(item, db);
|
||||
case "submitSelfAuth":
|
||||
return submitSelfAuth(item, db, context);
|
||||
case "getSelfAuthResult":
|
||||
return getSelfAuthResult(item, db, context);
|
||||
default:
|
||||
return { success: false, message: "未找到本人授权接口" };
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.parseRequestId = parseRequestId;
|
||||
module.exports.maskCertNo = maskCertNo;
|
||||
module.exports.getPersonInfo = getPersonInfo;
|
||||
module.exports.constants = { AUTH_VALID_MS, RECORD_RETENTION_MS, COLLECTION_NAME };
|
||||
282
hlw/self-auth/index.test.js
Normal file
282
hlw/self-auth/index.test.js
Normal file
@ -0,0 +1,282 @@
|
||||
jest.mock("../../utils/sm4-util", () => ({
|
||||
encryptDataForSm3: jest.fn((value) => Buffer.from(value).toString("base64")),
|
||||
decryptDataForSm3: jest.fn((value) => {
|
||||
const patientValues = {
|
||||
encryptedName: "张三",
|
||||
encryptedCertNo: "11010519491231002X",
|
||||
encryptedMobile: "13800138000",
|
||||
};
|
||||
return patientValues[value] || Buffer.from(value, "base64").toString();
|
||||
}),
|
||||
}));
|
||||
jest.mock("../hn-his", () => jest.fn());
|
||||
|
||||
const { ObjectId } = require("mongodb");
|
||||
const Sm4Util = require("../../utils/sm4-util");
|
||||
const hnHis = require("../hn-his");
|
||||
const selfAuth = require("./index");
|
||||
|
||||
const OWNER_USER_ID = "507f1f77bcf86cd799439011";
|
||||
const OTHER_USER_ID = "507f191e810c19729de860ea";
|
||||
const PATIENT_ID = "507f1f77bcf86cd799439012";
|
||||
const REQUEST_ID = "507f1f77bcf86cd799439013";
|
||||
const ACCOUNT_ID = "2088000000000000";
|
||||
|
||||
function sameValue(left, right) {
|
||||
if (left instanceof ObjectId || right instanceof ObjectId) {
|
||||
return String(left) === String(right);
|
||||
}
|
||||
return left === right;
|
||||
}
|
||||
|
||||
function matches(record, query) {
|
||||
return Object.entries(query).every(([key, expected]) => {
|
||||
if (expected && expected.$in) return expected.$in.includes(record[key]);
|
||||
return sameValue(record[key], expected);
|
||||
});
|
||||
}
|
||||
|
||||
function createDb({ requests = [], patient = null } = {}) {
|
||||
const state = { requests, patient };
|
||||
const authCollection = {
|
||||
dropIndex: jest.fn().mockRejectedValue(Object.assign(new Error("index not found"), {
|
||||
code: 27,
|
||||
codeName: "IndexNotFound",
|
||||
})),
|
||||
createIndex: jest.fn().mockResolvedValue("index"),
|
||||
insertOne: jest.fn(async (record) => {
|
||||
if (!record._id) record._id = new ObjectId();
|
||||
state.requests.push(record);
|
||||
return { insertedId: record._id };
|
||||
}),
|
||||
findOne: jest.fn(async (query) => state.requests.find((record) => matches(record, query)) || null),
|
||||
findOneAndUpdate: jest.fn(async (query, update) => {
|
||||
const record = state.requests.find((entry) => matches(entry, query));
|
||||
if (!record) return null;
|
||||
Object.assign(record, update.$set || {});
|
||||
if (update.$inc) {
|
||||
Object.entries(update.$inc).forEach(([key, value]) => {
|
||||
record[key] = (record[key] || 0) + value;
|
||||
});
|
||||
}
|
||||
return record;
|
||||
}),
|
||||
updateOne: jest.fn(async (query, update) => {
|
||||
const record = state.requests.find((entry) => matches(entry, query));
|
||||
if (!record) return { modifiedCount: 0 };
|
||||
Object.assign(record, update.$set || {});
|
||||
if (update.$unset) Object.keys(update.$unset).forEach((key) => delete record[key]);
|
||||
return { modifiedCount: 1 };
|
||||
}),
|
||||
};
|
||||
const patientCollection = {
|
||||
findOne: jest.fn(async (query) => {
|
||||
if (!state.patient || !matches(state.patient, query)) return null;
|
||||
return state.patient;
|
||||
}),
|
||||
};
|
||||
const db = {
|
||||
collection: jest.fn((name) => {
|
||||
if (name === "hlw-patient-self-auth") return authCollection;
|
||||
if (name === "hlw-patient") return patientCollection;
|
||||
throw new Error(`Unexpected collection: ${name}`);
|
||||
}),
|
||||
};
|
||||
return { db, state, authCollection };
|
||||
}
|
||||
|
||||
function makePatient() {
|
||||
return {
|
||||
_id: new ObjectId(PATIENT_ID),
|
||||
accountId: ACCOUNT_ID,
|
||||
anotherName: "encryptedName",
|
||||
anotherIdNo: "encryptedCertNo",
|
||||
anotherMobile: "encryptedMobile",
|
||||
address: "测试地址",
|
||||
disabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
function makeRequest(status = "pending", timestamp = Date.now()) {
|
||||
return {
|
||||
_id: new ObjectId(REQUEST_ID),
|
||||
patientId: new ObjectId(PATIENT_ID),
|
||||
ownerAccountId: ACCOUNT_ID,
|
||||
ownerUserId: OWNER_USER_ID,
|
||||
requestTimestamp: timestamp,
|
||||
status,
|
||||
attempts: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function requestId(request) {
|
||||
return request._id.toString();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test("rejects malformed authorization ids", async () => {
|
||||
const { db } = createDb();
|
||||
await expect(selfAuth({ type: "getSelfAuthPatient", id: "bad-id" }, db, {}))
|
||||
.resolves.toEqual({ success: false, message: "授权链接无效" });
|
||||
expect(db.collection).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("creates a pending request owned by the authenticated user", async () => {
|
||||
const { db, state } = createDb({ patient: makePatient() });
|
||||
const result = await selfAuth({
|
||||
type: "createSelfAuthRequest",
|
||||
patientId: PATIENT_ID,
|
||||
accountId: ACCOUNT_ID,
|
||||
}, db, { userId: OWNER_USER_ID });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data.id).toMatch(/^[a-f\d]{24}$/i);
|
||||
expect(result.data.id).toBe(state.requests[0]._id.toString());
|
||||
expect(state.requests[0]).toEqual(expect.objectContaining({
|
||||
ownerAccountId: ACCOUNT_ID,
|
||||
ownerUserId: OWNER_USER_ID,
|
||||
status: "pending",
|
||||
}));
|
||||
expect(state.requests[0]).not.toHaveProperty("authId");
|
||||
expect(state.requests[0]).not.toHaveProperty("psnToken");
|
||||
});
|
||||
|
||||
test("returns masked patient details and rejects an expired request", async () => {
|
||||
const active = makeRequest();
|
||||
const activeDb = createDb({ requests: [active], patient: makePatient() }).db;
|
||||
const activeResult = await selfAuth({ type: "getSelfAuthPatient", id: requestId(active) }, activeDb, {});
|
||||
expect(activeResult).toEqual(expect.objectContaining({
|
||||
success: true,
|
||||
data: expect.objectContaining({
|
||||
name: "张三",
|
||||
certNo: "1101***********02X",
|
||||
sex: "女",
|
||||
}),
|
||||
}));
|
||||
|
||||
const expired = makeRequest("pending", Date.now() - 11 * 60 * 1000);
|
||||
const expiredDb = createDb({ requests: [expired], patient: makePatient() }).db;
|
||||
const expiredResult = await selfAuth({ type: "getSelfAuthPatient", id: requestId(expired) }, expiredDb, {});
|
||||
expect(expiredResult).toEqual({ success: false, message: "授权链接已过期,请重新发起授权" });
|
||||
});
|
||||
|
||||
test("rejects missing requests and patients", async () => {
|
||||
const missingRequest = makeRequest();
|
||||
const emptyDb = createDb({ patient: makePatient() }).db;
|
||||
await expect(selfAuth({ type: "getSelfAuthPatient", id: requestId(missingRequest) }, emptyDb, {}))
|
||||
.resolves.toEqual({ success: false, message: "授权链接不存在或已失效" });
|
||||
|
||||
const request = makeRequest();
|
||||
const noPatientDb = createDb({ requests: [request] }).db;
|
||||
await expect(selfAuth({ type: "getSelfAuthPatient", id: requestId(request) }, noPatientDb, {}))
|
||||
.resolves.toEqual({ success: false, message: "就诊人不存在" });
|
||||
});
|
||||
|
||||
test("reports zero years old for a valid infant id", () => {
|
||||
const now = new Date();
|
||||
const date = [
|
||||
now.getFullYear(),
|
||||
String(now.getMonth() + 1).padStart(2, "0"),
|
||||
String(now.getDate()).padStart(2, "0"),
|
||||
].join("");
|
||||
const body = `110105${date}001`;
|
||||
const factors = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
||||
const parity = ["1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"];
|
||||
const sum = body.split("").reduce((total, digit, index) => total + Number(digit) * factors[index], 0);
|
||||
const certNo = `${body}${parity[sum % 11]}`;
|
||||
expect(selfAuth.getPersonInfo(certNo, now).age).toBe(0);
|
||||
});
|
||||
|
||||
test("stores only the encrypted matching HIS archive after successful submission", async () => {
|
||||
const request = makeRequest();
|
||||
const { db, state } = createDb({ requests: [request], patient: makePatient() });
|
||||
hnHis.mockResolvedValue({
|
||||
success: true,
|
||||
list: [{ socialno: "11010519491231002X", isyb: "1", patientId: "his-patient", blhno: "card-no" }],
|
||||
tmbxx: [{ code: "11", name: "普通门诊" }],
|
||||
});
|
||||
|
||||
const result = await selfAuth({ type: "submitSelfAuth", id: requestId(request), psnToken: "secret-token" }, db, { userId: OTHER_USER_ID });
|
||||
expect(result).toEqual({ success: true, message: "授权并建档成功", data: { status: "success" } });
|
||||
expect(state.requests[0].status).toBe("success");
|
||||
expect(state.requests[0].encryptedResult).toBeTruthy();
|
||||
expect(state.requests[0]).not.toHaveProperty("psnToken");
|
||||
expect(JSON.stringify(state.requests[0])).not.toContain("secret-token");
|
||||
expect(Sm4Util.encryptDataForSm3).toHaveBeenCalledWith(expect.stringContaining("his-patient"));
|
||||
});
|
||||
|
||||
test("allows a failed request to be retried", async () => {
|
||||
const request = makeRequest("failed");
|
||||
request.errorMessage = "previous failure";
|
||||
const { db, state } = createDb({ requests: [request], patient: makePatient() });
|
||||
hnHis.mockResolvedValue({
|
||||
success: true,
|
||||
list: [{ socialno: "11010519491231002X", isyb: "1" }],
|
||||
tmbxx: [],
|
||||
});
|
||||
const result = await selfAuth({ type: "submitSelfAuth", id: requestId(request), psnToken: "new-token" }, db, { userId: OTHER_USER_ID });
|
||||
expect(result.success).toBe(true);
|
||||
expect(state.requests[0].status).toBe("success");
|
||||
expect(state.requests[0].attempts).toBe(1);
|
||||
});
|
||||
|
||||
test("only one concurrent submission can call HIS", async () => {
|
||||
const request = makeRequest();
|
||||
const { db } = createDb({ requests: [request], patient: makePatient() });
|
||||
let releaseHis;
|
||||
hnHis.mockImplementation(() => new Promise((resolve) => {
|
||||
releaseHis = () => resolve({
|
||||
success: true,
|
||||
list: [{ socialno: "11010519491231002X", isyb: "1" }],
|
||||
tmbxx: [],
|
||||
});
|
||||
}));
|
||||
|
||||
const first = selfAuth({ type: "submitSelfAuth", id: requestId(request), psnToken: "token-1" }, db, { userId: OTHER_USER_ID });
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
const second = await selfAuth({ type: "submitSelfAuth", id: requestId(request), psnToken: "token-2" }, db, { userId: OTHER_USER_ID });
|
||||
expect(second).toEqual({
|
||||
success: false,
|
||||
message: "授权正在处理中,请勿重复提交",
|
||||
data: { status: "processing" },
|
||||
});
|
||||
expect(hnHis).toHaveBeenCalledTimes(1);
|
||||
releaseHis();
|
||||
await expect(first).resolves.toEqual({ success: true, message: "授权并建档成功", data: { status: "success" } });
|
||||
});
|
||||
|
||||
test.each([
|
||||
[{ success: true, list: [{ socialno: "330100000000000000", isyb: "1" }] }, "医保建档失败,未找到匹配档案"],
|
||||
[{ success: true, list: [{ socialno: "11010519491231002X", isyb: "0" }] }, "医保建档失败"],
|
||||
])("marks invalid HIS results as failed", async (hisResult, message) => {
|
||||
const request = makeRequest();
|
||||
const { db, state } = createDb({ requests: [request], patient: makePatient() });
|
||||
hnHis.mockResolvedValue(hisResult);
|
||||
const result = await selfAuth({ type: "submitSelfAuth", id: requestId(request), psnToken: "token" }, db, { userId: OTHER_USER_ID });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe(message);
|
||||
expect(state.requests[0].status).toBe("failed");
|
||||
});
|
||||
|
||||
test("is idempotent after success and restricts result access to the owner", async () => {
|
||||
const archive = { socialno: "11010519491231002X", isyb: "1" };
|
||||
const request = {
|
||||
...makeRequest("success"),
|
||||
encryptedResult: Buffer.from(JSON.stringify(archive)).toString("base64"),
|
||||
encryption: "SM4",
|
||||
};
|
||||
const { db } = createDb({ requests: [request], patient: makePatient() });
|
||||
|
||||
const repeated = await selfAuth({ type: "submitSelfAuth", id: requestId(request), psnToken: "token" }, db, { userId: OTHER_USER_ID });
|
||||
expect(repeated.success).toBe(true);
|
||||
expect(hnHis).not.toHaveBeenCalled();
|
||||
|
||||
const denied = await selfAuth({ type: "getSelfAuthResult", id: requestId(request) }, db, { userId: OTHER_USER_ID });
|
||||
expect(denied).toEqual({ success: false, message: "授权记录不存在或无权访问" });
|
||||
|
||||
const allowed = await selfAuth({ type: "getSelfAuthResult", id: requestId(request) }, db, { userId: OWNER_USER_ID });
|
||||
expect(allowed).toEqual({ success: true, message: "授权成功", data: { status: "success", hisArchive: archive } });
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user