fix: 调整需求开发

This commit is contained in:
huxuejian 2026-08-12 15:20:58 +08:00
parent 1c149c9c50
commit 10b519082e
14 changed files with 826 additions and 115 deletions

View File

@ -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

View File

@ -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
## 邮政快递 测试环境请求域名分配的orgIdAES加密秘钥以及IV
CONFIG_EMS_API_DOMAIN=https://topup.ule.com
## 邮政快递 测试环境分配的orgIdAES加密秘钥以及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

View File

@ -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')}`;
}

View File

@ -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
View 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,
};

View 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 },
});
});
});

View File

@ -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 }

View File

@ -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) },

View File

@ -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)

View File

@ -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,

View File

@ -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,

View File

@ -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
View 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
View 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 } });
});