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_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_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
|
## 海宁his api
|
||||||
CONFIG_HN_HIS_BASE_URL=http://122.225.16.74:8026
|
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_USERNAME=hnhlwYyAdmin
|
||||||
CONFIG_DB_PASSWORD=Lie89763!Zhy
|
CONFIG_DB_PASSWORD=Lie89763!HnhlwYy
|
||||||
CONFIG_DB_HOST=192.168.0.4
|
CONFIG_DB_HOST=192.168.0.4
|
||||||
CONFIG_DB_PORT=27017
|
CONFIG_DB_PORT=27017
|
||||||
CONFIG_NODE_ENV=production
|
CONFIG_NODE_ENV=production
|
||||||
@ -13,57 +13,8 @@ CONFIG_ZYB_YB_APPID=7D8D8E29924048539165F5E2E08C4616
|
|||||||
CONFIG_ZYB_YB_SECRET=77F9610253B94263BCF8DBB61BF90D92
|
CONFIG_ZYB_YB_SECRET=77F9610253B94263BCF8DBB61BF90D92
|
||||||
CONFIG_SMS_GATEWAY_DISABLED=false
|
CONFIG_SMS_GATEWAY_DISABLED=false
|
||||||
|
|
||||||
## 支付宝回调地址
|
|
||||||
CONFIG_ALIPAY_PAY_NOTIFY=https://hlw.zytzymz.com/alipay-pay-notify
|
|
||||||
## 支付宝小程序appId
|
## 支付宝小程序appId
|
||||||
CONFIG_ALIPAY_MINI_APPID = 2021005110656858
|
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 { ObjectId } = require("mongodb");
|
||||||
const timeApi = require("../hlw-time-duration");
|
const timeApi = require("../hlw-time-duration");
|
||||||
const { encryptOrderFields, decryptOrderFields, decryptOrderList } = require("./format");
|
const { encryptOrderFields, decryptOrderFields, decryptOrderList } = require("./format");
|
||||||
|
const { getRequiredDrugAccessLevels, doctorMeetsDrugAccessLevels } = require("../drug-access-level");
|
||||||
|
|
||||||
|
const doctorOrPharmacistOfflineMessage = "当前医生或药师不在线,暂无法提交问诊单,请稍后再试";
|
||||||
|
|
||||||
const hisPayStatus = {
|
const hisPayStatus = {
|
||||||
0: "已支付",
|
0: "已支付",
|
||||||
@ -155,7 +158,8 @@ async function addConsultOrder(item) {
|
|||||||
|
|
||||||
async function addOnlineConsultOrder(params) {
|
async function addOnlineConsultOrder(params) {
|
||||||
try {
|
try {
|
||||||
params.registerId = common.generateUniqueStr('GH');
|
params.registerId = await generateRegisterId();
|
||||||
|
const requiredDrugAccessLevels = await getRequiredDrugAccessLevels(db, params.drugs);
|
||||||
const storeApi = require("../drug-store");
|
const storeApi = require("../drug-store");
|
||||||
const { success, list, message } = await storeApi(
|
const { success, list, message } = await storeApi(
|
||||||
{
|
{
|
||||||
@ -195,14 +199,19 @@ async function addOnlineConsultOrder(params) {
|
|||||||
deptName: 1,
|
deptName: 1,
|
||||||
deptCode: 1,
|
deptCode: 1,
|
||||||
onlineStatus: 1,
|
onlineStatus: 1,
|
||||||
|
antibiotic_level: 1,
|
||||||
|
tumor_use: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (!doctor) {
|
if (!doctor) {
|
||||||
return { success: false, message: "未查询到医生" };
|
return { success: false, message: doctorOrPharmacistOfflineMessage };
|
||||||
}
|
}
|
||||||
if (doctor.onlineStatus !== "online") {
|
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.doctorName = doctor.doctorName || "";
|
||||||
params.doctorTitle = doctor.title || "";
|
params.doctorTitle = doctor.title || "";
|
||||||
@ -212,7 +221,12 @@ async function addOnlineConsultOrder(params) {
|
|||||||
} else {
|
} else {
|
||||||
// 随机选取一位医生 (单量少的权重大)
|
// 随机选取一位医生 (单量少的权重大)
|
||||||
const doctorApi = require("../hlw-doctor");
|
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) {
|
if (data.success && data.data) {
|
||||||
params.doctorName = data.data.doctorName || "";
|
params.doctorName = data.data.doctorName || "";
|
||||||
params.doctorCode = data.data.doctorNo;
|
params.doctorCode = data.data.doctorNo;
|
||||||
@ -220,12 +234,12 @@ async function addOnlineConsultOrder(params) {
|
|||||||
params.deptName = data.data.deptName || "";
|
params.deptName = data.data.deptName || "";
|
||||||
params.unitCode = data.data.deptCode || "";
|
params.unitCode = data.data.deptCode || "";
|
||||||
} else {
|
} 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" });
|
const onlineYaoshiCount = await db.collection("hlw-doctor").countDocuments({ corpId: params.corpId, onlineStatus: "online", job: "pharmacist" });
|
||||||
if (onlineYaoshiCount === 0) {
|
if (onlineYaoshiCount === 0) {
|
||||||
return { success: false, message: "当前医生或药师不在线,暂无法提交问诊单,请稍后再试" };
|
return { success: false, message: doctorOrPharmacistOfflineMessage };
|
||||||
}
|
}
|
||||||
const imAccountResult = await tencentIM({
|
const imAccountResult = await tencentIM({
|
||||||
type: "importAccount",
|
type: "importAccount",
|
||||||
@ -2345,3 +2359,24 @@ async function autoHandleExpireOrder() {
|
|||||||
return { success: false, message: e.message || "操作失败" };
|
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 = `本次问诊已取消,取消原因:${
|
let reason = `本次问诊已取消,取消原因:${
|
||||||
orderStatus === "pending" ? "超时未接诊" : "超时未处理"
|
orderStatus === "pending" ? "超时未接诊" : "超时未处理"
|
||||||
}。${
|
}。${
|
||||||
orderSource === "MATEPAD" ? "请联系线下退费。" : "费用将于48小时内退还您的支付账户。"
|
orderSource === "MATEPAD" ? "请联系线下退费。" : ""
|
||||||
}`;
|
}`;
|
||||||
await consultOrder(
|
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) {
|
if (diagnosisIds.present) {
|
||||||
newDrug.applicableDiagnosisIds = diagnosisIds.value;
|
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();
|
newDrug.createTime = Date.now();
|
||||||
const res = await db.collection("drug-info").insertOne(newDrug);
|
const res = await db.collection("drug-info").insertOne(newDrug);
|
||||||
return { success: true, message: "新增药品成功", id: res.insertedId }
|
return { success: true, message: "新增药品成功", id: res.insertedId }
|
||||||
@ -454,4 +460,4 @@ async function getDrugMatchedDisease(ctx) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { success: false, message: e.message }
|
return { success: false, message: e.message }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,8 @@ const drugFieldDict = [
|
|||||||
{ name: '药库货品ID', field: 'product_id', libType: 'online', verify: (val, label) => validateNumber(val, label) },
|
{ 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_name', verify: (val, label) => validator.verifyString(val, label) },
|
||||||
{ name: '通用名拼音码', field: 'generic_pinyin', 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)与剂型
|
// 仅在线药品库使用:管理分类(处方药 / OTC)与剂型
|
||||||
{ name: '管理分类', field: 'manage_category', verify: (val, label) => validator.verifyString(val, label) },
|
{ name: '管理分类', field: 'manage_category', verify: (val, label) => validator.verifyString(val, label) },
|
||||||
{ name: '剂型', field: 'dosage_form', 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',
|
key: 'title',
|
||||||
validate: val => validator.verifyString(val, '职称', 0, 30)
|
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',
|
key: 'intro',
|
||||||
validate: val => validator.verifyString(val, '个人介绍', 0, 300)
|
validate: val => validator.verifyString(val, '个人介绍', 0, 300)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ const { ObjectId } = require("mongodb");
|
|||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const peopleUtils = require("./hlw-people-utils");
|
const peopleUtils = require("./hlw-people-utils");
|
||||||
const { recordDoctorRecommendation, getDoctorRecommendationCount } = require("./doctor-count-in-one-minute");
|
const { recordDoctorRecommendation, getDoctorRecommendationCount } = require("./doctor-count-in-one-minute");
|
||||||
|
const { getDoctorAccessLevelQuery } = require("../drug-access-level");
|
||||||
let db = "";
|
let db = "";
|
||||||
module.exports = async (item, mongodb) => {
|
module.exports = async (item, mongodb) => {
|
||||||
db = mongodb;
|
db = mongodb;
|
||||||
@ -133,7 +134,12 @@ async function getRecommendDoctor({ storeCode, presType }) {
|
|||||||
* @param {boolean} recordInOneMinute 是否记录一分钟内获取推荐医生的数量
|
* @param {boolean} recordInOneMinute 是否记录一分钟内获取推荐医生的数量
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
async function getOptimalRecommendDoctor({ recordInOneMinute = false, doctorType }) {
|
async function getOptimalRecommendDoctor({
|
||||||
|
recordInOneMinute = false,
|
||||||
|
doctorType,
|
||||||
|
minimumAntibioticLevel,
|
||||||
|
minimumTumorUse,
|
||||||
|
}) {
|
||||||
try {
|
try {
|
||||||
const type = ['west', 'chinese'].includes(doctorType) ? doctorType : 'west';
|
const type = ['west', 'chinese'].includes(doctorType) ? doctorType : 'west';
|
||||||
// 查询条件:在线且服务状态启用的医生
|
// 查询条件:在线且服务状态启用的医生
|
||||||
@ -148,6 +154,7 @@ async function getOptimalRecommendDoctor({ recordInOneMinute = false, doctorType
|
|||||||
} else if (type === 'west') {
|
} else if (type === 'west') {
|
||||||
query.openWestPres = true;
|
query.openWestPres = true;
|
||||||
}
|
}
|
||||||
|
Object.assign(query, getDoctorAccessLevelQuery(minimumAntibioticLevel, minimumTumorUse));
|
||||||
const projection = {
|
const projection = {
|
||||||
doctorNo: 1,
|
doctorNo: 1,
|
||||||
doctorName: 1,
|
doctorName: 1,
|
||||||
@ -157,6 +164,8 @@ async function getOptimalRecommendDoctor({ recordInOneMinute = false, doctorType
|
|||||||
deptCode: 1,
|
deptCode: 1,
|
||||||
onlineStatus: 1,
|
onlineStatus: 1,
|
||||||
serviceStatus: 1,
|
serviceStatus: 1,
|
||||||
|
antibiotic_level: 1,
|
||||||
|
tumor_use: 1,
|
||||||
}
|
}
|
||||||
const doctorList = await db.collection("hlw-doctor").find(query, { projection }).toArray();
|
const doctorList = await db.collection("hlw-doctor").find(query, { projection }).toArray();
|
||||||
if (doctorList.length === 0) {
|
if (doctorList.length === 0) {
|
||||||
@ -657,6 +666,10 @@ async function addHlwPerson(ctx) {
|
|||||||
if (total > 0) {
|
if (total > 0) {
|
||||||
return { success: false, message: "院内工号已存在" }
|
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({
|
const res = await db.collection("hlw-doctor").insertOne({
|
||||||
...data,
|
...data,
|
||||||
corpId,
|
corpId,
|
||||||
@ -812,4 +825,4 @@ async function getDoctorByNo(ctx) {
|
|||||||
message: err.message || "查询失败",
|
message: err.message || "查询失败",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,7 @@ const chineseMedicine = require("./chinese-medicine");
|
|||||||
const chineseMedicineOrder = require("./chinese-medicine-order");
|
const chineseMedicineOrder = require("./chinese-medicine-order");
|
||||||
const hlwPatient = require("./hlw-patient");
|
const hlwPatient = require("./hlw-patient");
|
||||||
const scannedCardArchive = require("./scanned-card-archive");
|
const scannedCardArchive = require("./scanned-card-archive");
|
||||||
|
const selfAuth = require("./self-auth");
|
||||||
|
|
||||||
module.exports = async (item, db, context) => {
|
module.exports = async (item, db, context) => {
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
@ -273,6 +274,11 @@ module.exports = async (item, db, context) => {
|
|||||||
case "saveScannedCard":
|
case "saveScannedCard":
|
||||||
case "getScannedCard":
|
case "getScannedCard":
|
||||||
return await scannedCardArchive(item, db);
|
return await scannedCardArchive(item, db);
|
||||||
|
case "createSelfAuthRequest":
|
||||||
|
case "getSelfAuthPatient":
|
||||||
|
case "submitSelfAuth":
|
||||||
|
case "getSelfAuthResult":
|
||||||
|
return await selfAuth(item, db, context);
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@ -196,6 +196,12 @@ async function addOnlineDrugInfo(ctx, db) {
|
|||||||
if (diagnosisIds.present) {
|
if (diagnosisIds.present) {
|
||||||
newDrug.applicableDiagnosisIds = diagnosisIds.value;
|
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();
|
newDrug.createTime = Date.now();
|
||||||
const res = await db
|
const res = await db
|
||||||
.collection("online-drug-info")
|
.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