diff --git a/src/main/java/com/ynxbd/common/action/ApiAction.java b/src/main/java/com/ynxbd/common/action/ApiAction.java index 5eb2e33..8a19740 100644 --- a/src/main/java/com/ynxbd/common/action/ApiAction.java +++ b/src/main/java/com/ynxbd/common/action/ApiAction.java @@ -5,9 +5,11 @@ import com.ynxbd.common.bean.Patient; import com.ynxbd.common.bean.User; import com.ynxbd.common.bean.sms.SmsTempEnum; import com.ynxbd.common.bean.sms.SmsTemplate; +import com.ynxbd.common.config.interceptor.AesDecode; import com.ynxbd.common.helper.common.*; import com.ynxbd.common.result.Result; import com.ynxbd.common.result.ResultEnum; +import com.ynxbd.common.result.ServiceException; import com.ynxbd.wx.config.WeChatConfig; import com.ynxbd.wx.utils.DesEncryptHelper; import com.ynxbd.wx.wxfactory.AesWxHelper; @@ -15,6 +17,7 @@ import com.ynxbd.wx.wxfactory.WxCacheHelper; import com.ynxbd.wx.wxfactory.WxFactory; import com.ynxbd.wx.wxfactory.base.auth.models.AccessToken; import com.ynxbd.wx.wxfactory.base.auth.models.QRCodeInfo; +import com.ynxbd.wx.wxfactory.base.auth.models.WxVerifyId; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; import org.apache.struts2.convention.annotation.Action; @@ -106,6 +109,49 @@ public class ApiAction extends BaseAction { return Result.success(data); } + /** + * 获取用户人脸核身会话唯一标识 + */ + @Action("getVerifyId") + public Result getVerifyId(@AesDecode String openid, String certName, String certNo) { + try { + log.info("[获取用户人脸核身会话唯一标识]openid={}, certName={}, certNo={}", openid, certName, certNo); + if (certName == null) { + return Result.error(ResultEnum.PARAM_IS_DEFECT); + } + if (openid == null || certNo == null) { + return Result.error(ResultEnum.PARAM_IS_INVALID); + } + + WxVerifyId info = WxFactory.Base.OAuth().getVerifyId(openid, certName, certNo); + return Result.success(info); + } catch (Exception e) { + return Result.error(e); + } + } + + /** + * 查询用户人脸核身真实验证结果 + */ + @Action("queryVerifyInfo") + public Result queryVerifyInfo(@AesDecode String openid, String outSeqNo, String verifyId, String certHash) { + try { + log.info("[获取用户人脸核身会话唯一标识]openid={}, outSeqNo={}, verifyId={}, certHash={}", openid, outSeqNo, verifyId, certHash); + if (outSeqNo == null || verifyId == null || certHash == null) { + return Result.error(ResultEnum.PARAM_IS_INVALID); + } + + if (openid == null) { + return Result.error(ResultEnum.PARAM_IS_INVALID); + } + + WxVerifyId info = WxFactory.Base.OAuth().queryVerifyInfo(openid, outSeqNo, verifyId, certHash); + return Result.success(info); + } catch (Exception e) { + return Result.error(e); + } + } + /** * 获取系统公钥 * @@ -116,12 +162,8 @@ public class ApiAction extends BaseAction { return Result.success(Base64Helper.decode(data)); } - /** - // * 获取系统公钥 - // * - // * @return 公钥 - // */ -// @Action("pdf") + +// @Action("pdf") // 获取系统公钥 // public Result pdf(String data) { // try { // System.out.println(data); diff --git a/src/main/java/com/ynxbd/common/action/HealthCardAction.java b/src/main/java/com/ynxbd/common/action/HealthCardAction.java index b2f6749..412643e 100644 --- a/src/main/java/com/ynxbd/common/action/HealthCardAction.java +++ b/src/main/java/com/ynxbd/common/action/HealthCardAction.java @@ -1,12 +1,26 @@ package com.ynxbd.common.action; +import com.alibaba.fastjson.JSONObject; import com.ynxbd.common.action.base.BaseAction; +import com.ynxbd.common.result.Result; +import com.ynxbd.common.service.HCodeService; +import com.ynxbd.wx.config.WeChatConfig; import lombok.extern.slf4j.Slf4j; +import org.apache.struts2.convention.annotation.Action; import org.apache.struts2.convention.annotation.Namespace; @Slf4j @Namespace("/hc") public class HealthCardAction extends BaseAction { + /** + * [电子健康卡]绑卡验证授权 + */ + @Action("registerHealthCardPreAuth") + public Result registerHealthCardPreAuth(Boolean isMiniApp, String wechatCode, String healthCode, String openId) { + log.info("[电子健康卡]绑卡验证授权 isMiniApp={}, wechatCode={}, healthCode={}, openId={}", isMiniApp, wechatCode, healthCode, openId); + JSONObject result = HCodeService.registerHealthCardPreAuth(isMiniApp, false, wechatCode, healthCode, openId); + return Result.success(result); + } } diff --git a/src/main/java/com/ynxbd/common/action/HealthCodeAction.java b/src/main/java/com/ynxbd/common/action/HealthCodeAction.java index 323a571..bd0203e 100644 --- a/src/main/java/com/ynxbd/common/action/HealthCodeAction.java +++ b/src/main/java/com/ynxbd/common/action/HealthCodeAction.java @@ -8,15 +8,17 @@ import com.ynxbd.common.bean.User; import com.ynxbd.common.bean.enums.HCardTypeEnum; import com.ynxbd.common.bean.enums.HealthCardEnum; import com.ynxbd.common.bean.enums.HealthCardRespCodeEnum; +import com.ynxbd.common.config.HealthCardConfig; import com.ynxbd.common.config.interceptor.AesDecode; import com.ynxbd.common.dao.PatientDao; import com.ynxbd.common.dao.his.HisPatientDao; -import com.ynxbd.common.helper.common.*; +import com.ynxbd.common.helper.common.DateHelper; +import com.ynxbd.common.helper.common.IDNumberHelper; +import com.ynxbd.common.helper.common.ValidHelper; import com.ynxbd.common.result.Result; import com.ynxbd.common.result.ResultEnum; import com.ynxbd.common.service.HCodeService; import com.ynxbd.common.service.PatientService; -import com.ynxbd.wx.config.WeChatConfig; import com.ynxbd.wx.wxfactory.AesWxHelper; import com.ynxbd.wx.wxfactory.WxCacheHelper; import lombok.extern.slf4j.Slf4j; @@ -96,7 +98,7 @@ public class HealthCodeAction extends BaseAction { */ @Action("getAppToken") public Result getAppToken(Boolean isMiniApp) { - String appToken = HCodeService.getAppToken(isMiniApp, true); + String appToken = HealthCardConfig.getAppToken(isMiniApp, true); return appToken == null ? Result.error() : Result.success(); } @@ -127,7 +129,7 @@ public class HealthCodeAction extends BaseAction { * @return result */ @Action("getDynamicQRCode") - public Result getDynamicQRCode(Boolean isMiniApp, String idCardNo, String healthCardId) { + public Result getDynamicQRCode(Boolean isMiniApp, @AesDecode String idCardNo, String healthCardId) { log.info("[电子健康卡]获取二维码 healthCardId={}, idCardNo={}", healthCardId, idCardNo); if (healthCardId == null || idCardNo == null) { return Result.error(ResultEnum.PARAM_IS_DEFECT); // 参数缺失 @@ -141,7 +143,7 @@ public class HealthCodeAction extends BaseAction { * 获取卡包订单ID */ @Action("getCardOrderId") - public Result getCardOrderId(Boolean isMiniApp, String idCardNo, String healthCardId) { + public Result getCardOrderId(Boolean isMiniApp, @AesDecode String idCardNo, String healthCardId) { log.info("[电子健康卡]获取卡包订单ID healthCardId={}, idCardNo={}", healthCardId, idCardNo); if (healthCardId == null || idCardNo == null) { return Result.error(ResultEnum.PARAM_IS_DEFECT); // 参数缺失 @@ -238,7 +240,7 @@ public class HealthCodeAction extends BaseAction { } String uuid = null; - if (HCodeService.isEnableHCode() && isHealthCard && healthCardId == null) { // 没有禁用电子健康卡 + if (HealthCardConfig.isEnable() && isHealthCard && healthCardId == null) { // 没有禁用电子健康卡 log.info("[电子健康卡]绑定 wechatCode={}", wechatCode); if (wechatCode == null) { return Result.error(ResultEnum.PARAM_IS_DEFECT); @@ -468,14 +470,14 @@ public class HealthCodeAction extends BaseAction { } - /** - * [电子健康卡]绑卡验证授权 - */ - @Action("registerHealthCardPreAuth") - public Result registerHealthCardPreAuth(Boolean isMiniApp, String wechatCode, String healthCode, String openId) { - log.info("[电子健康卡]绑卡验证授权 isMiniApp={}, wechatCode={}, healthCode={}, openId={}", isMiniApp, wechatCode, healthCode, openId); - JSONObject result = HCodeService.registerHealthCardPreAuth(isMiniApp, WeChatConfig.getDomain(true), wechatCode, healthCode, openId); - return Result.success(result); - } +// /** +// * [电子健康卡]绑卡验证授权 +// */ +// @Action("registerHealthCardPreAuth") +// public Result registerHealthCardPreAuth(Boolean isMiniApp, String wechatCode, String healthCode, String openId) { +// log.info("[电子健康卡]绑卡验证授权 isMiniApp={}, wechatCode={}, healthCode={}, openId={}", isMiniApp, wechatCode, healthCode, openId); +// JSONObject result = HCodeService.registerHealthCardPreAuth(isMiniApp, WeChatConfig.getDomain(true), wechatCode, healthCode, openId); +// return Result.success(result); +// } } diff --git a/src/main/java/com/ynxbd/common/action/PatientAction.java b/src/main/java/com/ynxbd/common/action/PatientAction.java index 4e13078..bac315c 100644 --- a/src/main/java/com/ynxbd/common/action/PatientAction.java +++ b/src/main/java/com/ynxbd/common/action/PatientAction.java @@ -256,6 +256,10 @@ public class PatientAction extends BaseAction { } if (HCardTypeEnum.NO_CARD.equals(cardTypeEnum)) { // 儿童 + if (WeChatConfig.IS_ENABLE_GMC) { + return Result.error("[医共体]不支持无证绑定!"); + } + fTel = getString("fTel"); fName = getString("fName"); fIDCardNo = getString("fIDCardNo"); @@ -311,12 +315,10 @@ public class PatientAction extends BaseAction { return new PatientService().bind(request, false, true, bindInfo); } - /** - * 患者解绑 - */ + @Action("unBind") - public Result unBind(String openid, String patientId) { - log.info("[患者解绑]解绑 openid={}, patientId={}", openid, patientId); + public Result unBind(@AesDecode String openid, @AesDecode String patientId) { + log.info("[患者解绑] openid={}, patientId={}", openid, patientId); if (openid == null || patientId == null) { return Result.error(ResultEnum.PARAM_IS_DEFECT); } @@ -400,7 +402,7 @@ public class PatientAction extends BaseAction { // http://127.0.0.1:8080/wx/patient/queryGMCId?hisPatientId=5C881337931CDBBC8D38FACAE2D302D8 @Action("queryGMCId") public Result queryGMCId(@AesDecode String hisPatientId) { - String gmcPatientId = new HisPatientDao().getGMCPatientId(hisPatientId); + String gmcPatientId = new HisPatientDao().getGMCEmpiId(hisPatientId); return Result.success(gmcPatientId); } } diff --git a/src/main/java/com/ynxbd/common/action/SmsAction.java b/src/main/java/com/ynxbd/common/action/SmsAction.java index 6713073..5fb86c8 100644 --- a/src/main/java/com/ynxbd/common/action/SmsAction.java +++ b/src/main/java/com/ynxbd/common/action/SmsAction.java @@ -158,10 +158,10 @@ public class SmsAction extends BaseAction { return Result.error(ResultEnum.SMS_SEND_REPEAT, sms, ResultEnum.SMS_SEND_REPEAT.makeMessage(message)); } - boolean isFlag; + boolean isFlag = false; switch (callNo) { case "na_admin": // 核酸结果上传逾期 - isFlag = SmsHelper.send(SmsTempEnum.SMS_260585044, tel, smsObj); +// isFlag = SmsHelper.send(SmsTempEnum.SMS_260585044, tel, smsObj); break; default: @@ -259,12 +259,12 @@ public class SmsAction extends BaseAction { * 备注:2025-05-07 只有放射科会发短信 * * @param tel 电话号码 - * @param content 内容 + * @param content 内容[网址] * @return 返回发送结果 */ @Action("danMiSms") public Result danMiSms(String tel, String content) { - log.info("[天助平台短信]url-{}, content={}", tel, content); + log.info("[天助平台短信]tel={}, content={}", tel, content); if (tel == null || content == null) { return Result.error(ResultEnum.PARAM_IS_INVALID); } diff --git a/src/main/java/com/ynxbd/common/action/test/TestAction.java b/src/main/java/com/ynxbd/common/action/test/TestAction.java index 90b5949..127ac8f 100644 --- a/src/main/java/com/ynxbd/common/action/test/TestAction.java +++ b/src/main/java/com/ynxbd/common/action/test/TestAction.java @@ -44,6 +44,13 @@ public class TestAction extends BaseAction { return Result.success(); } + // 医保退费 + @Action("refund_med_test") + public Result refund_med_test() throws ServiceException { +// WxMedOrder order = WxMedHelper.refundCash("WX_M02f373d25b94475b60aa9860977c", ("R" + "ORD530100202605181442001740995"), "ORD530100202605181442001740995", new BigDecimal("24.71"), "已有缴费记录,不能再次缴费"); + return Result.success(); + } + // @Action("a") // public String a() { diff --git a/src/main/java/com/ynxbd/common/bean/Patient.java b/src/main/java/com/ynxbd/common/bean/Patient.java index a2247dc..de46c8a 100644 --- a/src/main/java/com/ynxbd/common/bean/Patient.java +++ b/src/main/java/com/ynxbd/common/bean/Patient.java @@ -31,6 +31,12 @@ public class Patient implements Serializable { // 患者id private String patientId; private String enPatientId; + // 医共体使用兼容 + private String hisPatientId; + private String enHisPatientId; + // 医共体Id + private String empiId; + private String enEmpiId; private String name; private String sex; @@ -95,8 +101,6 @@ public class Patient implements Serializable { // 医共体主体认证id private String gmcOpenId; private String enGmcOpenId; - // 记录非医共体绑定的HIS患者id - private String hisPatientId; // 医共体患者唯一id private String gmcUniqueId; // 是否为医共体绑定[1:是] @@ -132,6 +136,8 @@ public class Patient implements Serializable { boolean noneCardNo = ObjectUtils.isEmpty(this.idCardNo); // 无证件号 // 患者ID this.enPatientId = AesWxHelper.encode(this.patientId, true); + this.enHisPatientId = AesWxHelper.encode(this.hisPatientId, true); + this.enEmpiId = AesWxHelper.encode(this.empiId, true); // 电话 this.showTel = ParamHelper.hideTel(this.tel); diff --git a/src/main/java/com/ynxbd/common/config/HealthCardConfig.java b/src/main/java/com/ynxbd/common/config/HealthCardConfig.java new file mode 100644 index 0000000..4748911 --- /dev/null +++ b/src/main/java/com/ynxbd/common/config/HealthCardConfig.java @@ -0,0 +1,188 @@ +package com.ynxbd.common.config; + +import com.alibaba.fastjson.JSONObject; +import com.tencent.healthcard.impl.HealthCardServerImpl; +import com.tencent.healthcard.model.CommonIn; +import com.ynxbd.common.helper.ProperHelper; +import com.ynxbd.common.helper.common.ErrorHelper; +import com.ynxbd.wx.config.WeChatConfig; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.ehcache.Cache; + +import java.util.UUID; + +// 电子健康卡配置 + +@Slf4j +public class HealthCardConfig { + public static final String MINI_CACHE_KEY = "mini_app_token_cache"; + public static final String APP_CACHE_KEY = "app_token_cache"; + + public static synchronized void initCache() { + if (CACHE == null) { // 创建一个缓存实例(7000s最大存活时间) + CACHE = EhCacheConfig.createCache(String.class, String.class, "health_card_cache", 1L, 3L, 10L, false, 7000L, null); + } + } + + private static final boolean IS_ENABLE; // 是否启用电子健康卡(true:启用, false:禁用) + public static final boolean IS_UPLOAD_DATA; // 是否允许上传数据(true:启用, false:禁用) + + // 健康码 + public static final String H_APP_ID; + public static final String H_APP_SECRET; + public static final String H_HOSPITAL_ID; // 医院ID(每家医院不一样) + public static final String H_MINI_HOSPITAL_ID; // 小程序-医院ID(每家医院不一样) + public static final String H_MINI_APP_ID; // 小程序-appid + // 万达 + public static final String CARD_URL; + public static final String CARD_APP_ID; + public static final String CARD_PUBLIC_KEY; + public static final String CARD_PRIVATE_KEY; + + // 缓存 + private static Cache CACHE; + + // 离线WeChartCode 批量领取健康卡用 + public static final String WE_CHART_CODE = "73EFA6796D3869FF82FAE7E81E9814B7"; + + // 公众号不用传,小程序内嵌必传,具体传参请联系平台对接人员分配 + public static final int domainChannel = 0; + + static { + ProperHelper config = new ProperHelper().read("hcode.properties"); + IS_ENABLE = config.getBoolean("is_enable", false); + IS_UPLOAD_DATA = config.getBoolean("h.is_upload_data", true); + // 不用对config设置开关,不开启时也需要用到参数 + H_APP_ID = config.getString("h.app_id"); + H_APP_SECRET = config.getString("h.app_secret"); + CARD_APP_ID = config.getString("h.card_app_id"); + CARD_PUBLIC_KEY = config.getString("h.card_public_key"); + CARD_PRIVATE_KEY = config.getString("h.card_private_key"); + CARD_URL = config.getString("h.card_url"); + + H_HOSPITAL_ID = config.getString("h.hospital_id"); + if (ObjectUtils.isEmpty(H_HOSPITAL_ID)) { + log.info("[电子健康卡]医院id缺失"); + } + H_MINI_APP_ID = config.getString("h.mini_app_id"); + H_MINI_HOSPITAL_ID = config.getString("h.mini_hospital_id"); + + initCache(); + } + + + // 判断是否开启电子健康卡 + public static boolean isEnable() { + if (!IS_ENABLE) { + log.info("[配置][hcode.properties]未开启电子健康卡IS_ENABLE"); + } + return IS_ENABLE; + } + + + public static HealthCardServerImpl createHealthCardService() { + return new HealthCardServerImpl(HealthCardConfig.H_APP_SECRET); + } + + public static CommonIn createCommonIn(Boolean isMiniApp) { + return createCommonIn(isMiniApp, getAppToken(isMiniApp, true)); + } + + public static CommonIn createCommonIn(Boolean isMiniApp, String appToken) { + if (appToken == null) { // 此处不判断空字符串 + log.info("[电子健康卡]appToken为空"); + return null; + } + if (isMiniApp == null) { + isMiniApp = false; + } + int channelNum = (isMiniApp ? 1 : 0); // 填入代码,0为微信服务号渠道,1为微信小程序渠道,3为刷脸终端,4为扫码终端 + String hospId = (isMiniApp ? H_MINI_HOSPITAL_ID : H_HOSPITAL_ID); + String requestId = UUID.randomUUID().toString().replaceAll("-", "").toUpperCase(); + String appId = (isMiniApp ? H_MINI_APP_ID : WeChatConfig.APP_ID); + return new CommonIn(appToken, requestId, hospId, channelNum, appId, null); + } + + /** + * 获取token + * + * @param isEnTokenSwitch [true:必须开启功能才能获取token] + * @return token + */ + public static String getAppToken(Boolean isMiniApp, boolean isEnTokenSwitch) { + if (isEnTokenSwitch && !isEnable()) { + return null; + } + + if (isMiniApp == null) { + isMiniApp = false; + } + + String appToken = null; + if (CACHE == null) { + initCache(); + } + if (CACHE != null) { + appToken = CACHE.get(isMiniApp ? MINI_CACHE_KEY : APP_CACHE_KEY); + if (appToken != null) { + return appToken; + } + } + + try { + CommonIn commonIn = createCommonIn(isMiniApp, ""); + if (commonIn == null) { + return null; + } + JSONObject resultJson = new HealthCardServerImpl(H_APP_SECRET, 5, 10).getAppToken(commonIn, H_APP_ID); + HCardResult result = new HCardResult(resultJson); + if (!result.isOk) { + log.info("[电子健康卡]获取appToken失败: {}", resultJson); + return null; + } + JSONObject respJson = result.getRsp(); + appToken = respJson.getString("appToken"); + + if (CACHE != null && !ObjectUtils.isEmpty(appToken)) { + CACHE.put("appToken", appToken); + } + } catch (Exception e) { + ErrorHelper.println(e); + } + + if (ObjectUtils.isEmpty(appToken)) { + log.info("[电子健康卡]获取appToken为空"); + } + return appToken; + } + + + @Setter + @Getter + @ToString + @NoArgsConstructor + public static class HCardResult { // 数据解析 + private JSONObject commonOut; + private JSONObject rsp; + private String resultCode; + public boolean isOk; + + public HCardResult(JSONObject resultJsonObj) { + if (resultJsonObj == null) { + return; + } + this.rsp = resultJsonObj.getJSONObject("rsp"); + this.commonOut = resultJsonObj.getJSONObject("commonOut"); + if (this.commonOut == null) { + return; + } + this.resultCode = commonOut.getString("resultCode"); + this.isOk = "0".equals(this.resultCode); + } + } +} diff --git a/src/main/java/com/ynxbd/common/dao/PatientDao.java b/src/main/java/com/ynxbd/common/dao/PatientDao.java index d436041..46b88b3 100644 --- a/src/main/java/com/ynxbd/common/dao/PatientDao.java +++ b/src/main/java/com/ynxbd/common/dao/PatientDao.java @@ -199,13 +199,13 @@ public class PatientDao { if (id == null) { return false; } - String sql = "update patientBase set deletedState=0, healthCardId=?, name=?, nation=?, tel=?, address=?, uuid=?, areaCode=?, areaAddress=?, patientId=?, hisPatientId=?, gmcBindState=?, " + + String sql = "update patientBase set deletedState=0, healthCardId=?, name=?, nation=?, tel=?, address=?, uuid=?, areaCode=?, areaAddress=?, patientId=?, empiId=?, gmcBindState=?, " + " uniqueId=if(uniqueId is null or uniqueId = '', ?, uniqueId), " + " gmcUniqueId=if(gmcUniqueId is null or gmcUniqueId = '', ?, gmcUniqueId) " + " where id=? and openid=? and idCardNo=?"; bindInfo.setGmcBindState(WeChatConfig.IS_ENABLE_GMC ? 1 : 0); - bindInfo.setHisPatientId(WeChatConfig.IS_ENABLE_GMC ? null : bindInfo.getPatientId()); + bindInfo.setEmpiId(WeChatConfig.IS_ENABLE_GMC ? bindInfo.getEmpiId() : null); return DataBase.update(sql, ps -> { ps.setString(1, bindInfo.getHealthCardId()); @@ -217,7 +217,7 @@ public class PatientDao { ps.setString(7, bindInfo.getAreaCode()); ps.setString(8, bindInfo.getAreaAddress()); ps.setString(9, bindInfo.getPatientId()); - ps.setString(10, bindInfo.getHisPatientId()); + ps.setString(10, bindInfo.getEmpiId()); ps.setInt(11, bindInfo.getGmcBindState()); ps.setString(12, CodeHelper.get32UUID()); ps.setString(13, bindInfo.getGmcUniqueId()); @@ -247,7 +247,7 @@ public class PatientDao { */ public boolean insert(boolean isMyself, Patient bindData) { bindData.setIsMyself(isMyself); - String sql = "insert into patientBase(bindDate, openid, patientId, hisTransNo, name, sex, idCardNo, tel, birthday, nation, healthCardId, age, uuid, fatherName, fatherTel, fatherIDCardNo, motherName, motherTel, motherIDCardNo, address, areaCode, areaAddress, isMyself, cardType, unionId, uniqueId, hisPatientId, gmcUniqueId, gmcBindState) " + + String sql = "insert into patientBase(bindDate, openid, patientId, hisTransNo, name, sex, idCardNo, tel, birthday, nation, healthCardId, age, uuid, fatherName, fatherTel, fatherIDCardNo, motherName, motherTel, motherIDCardNo, address, areaCode, areaAddress, isMyself, cardType, unionId, uniqueId, empiId, gmcUniqueId, gmcBindState) " + " values(now(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; return DataBase.insert(sql, ps -> { @@ -263,7 +263,7 @@ public class PatientDao { * @return bool */ public int insertBatch(List dataList) { - String sql = "insert into patientBase(bindDate, openid, patientId, hisTransNo, name, sex, idCardNo, tel, birthday, nation, healthCardId, age, uuid, fatherName, fatherTel, fatherIDCardNo, motherName, motherTel, motherIDCardNo, address, areaCode, areaAddress, isMyself, cardType, unionId, uniqueId, hisPatientId, gmcUniqueId, gmcBindState %s) " + + String sql = "insert into patientBase(bindDate, openid, patientId, hisTransNo, name, sex, idCardNo, tel, birthday, nation, healthCardId, age, uuid, fatherName, fatherTel, fatherIDCardNo, motherName, motherTel, motherIDCardNo, address, areaCode, areaAddress, isMyself, cardType, unionId, uniqueId, empiId, gmcUniqueId, gmcBindState %s) " + " values(now(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? %s)"; List hasIdList = new ArrayList<>(); @@ -296,7 +296,7 @@ public class PatientDao { // 设置插入的占位符 private void setInsertPs(PreparedStatement ps, Patient item) throws SQLException { item.setGmcBindState(WeChatConfig.IS_ENABLE_GMC ? 1 : item.getGmcBindState() == null ? 0 : item.getGmcBindState()); - item.setHisPatientId(WeChatConfig.IS_ENABLE_GMC ? null : item.getPatientId()); + item.setEmpiId(WeChatConfig.IS_ENABLE_GMC ? null : item.getEmpiId()); HCardTypeEnum cardTypeEnum = item.getCardTypeEnum(); @@ -327,7 +327,7 @@ public class PatientDao { ps.setString(24, item.getUnionId()); ps.setString(25, CodeHelper.get32UUID()); // 医共体 - ps.setString(26, item.getHisPatientId()); + ps.setString(26, item.getEmpiId()); ps.setString(27, item.getGmcUniqueId()); ps.setObject(28, item.getGmcBindState(), Types.INTEGER); } @@ -453,6 +453,7 @@ public class PatientDao { }); } + /** * 查询不是医共体的患者 * @@ -463,11 +464,11 @@ public class PatientDao { log.warn("[医共体]功能未开启-禁止查询不是医共体的用户信息"); return new ArrayList<>(); } - String sql = "select * from patientBase where (hisPatientId is null or hisPatientId = '')"; + String sql = "select * from patientBase where (empiId is null or empiId = '')"; return DataBase.select(sql, Patient.class, null); } - public boolean removePatient(Integer id, String remark){ + public boolean removePatient(Integer id, String remark) { String sql = "update patientBase set deletedState=1, remark=? where id=?"; return DataBase.update(sql, ps -> { ps.setString(1, remark); @@ -475,32 +476,31 @@ public class PatientDao { }) > 0; } - public boolean updateGmcInfo(boolean hasCard, Integer id, String gmcPatientId, String hisPatientId) { + public boolean updateGmcInfo(boolean hasCard, Integer id, String empiId) { if (!hasCard) { // 无证绑定->标记为已删除 return removePatient(id, "无证绑定"); } - if (ObjectUtils.isEmpty(gmcPatientId)) { + if (ObjectUtils.isEmpty(empiId)) { return false; } int gmcBindState = WeChatConfig.IS_ENABLE_GMC && WeChatConfig.IS_GMC_SERVER ? 1 : 0; // 医共体主服务器直接标识为1,其他医院的服务器同步时为0,后续使用时同步 - String sql = "update patientBase set gmcBindState=?, patientId=?, hisPatientId=? where id=?"; + String sql = "update patientBase set gmcBindState=?, empiId=? where id=?"; return DataBase.update(sql, ps -> { ps.setInt(1, gmcBindState); - ps.setString(2, gmcPatientId); - ps.setString(3, hisPatientId); + ps.setString(2, empiId); // 条件 - ps.setLong(4, id); + ps.setLong(3, id); }) > 0; } - public boolean updateGmcBindState(String gmcUniqueId, Integer id, String gmcPatientId) { - String sql = "update patientBase set gmcBindState= 1, gmcUniqueId=? where id=? and patientId=?"; + public boolean updateGmcBindState(String gmcUniqueId, Integer id, String empiId) { + String sql = "update patientBase set gmcBindState= 1, gmcUniqueId=? where id=? and empiId=?"; return DataBase.update(sql, ps -> { ps.setString(1, gmcUniqueId); // 条件 ps.setLong(2, id); - ps.setString(3, gmcPatientId); + ps.setString(3, empiId); }) > 0; } } diff --git a/src/main/java/com/ynxbd/common/dao/his/HisPatientDao.java b/src/main/java/com/ynxbd/common/dao/his/HisPatientDao.java index 4bf0080..242647d 100644 --- a/src/main/java/com/ynxbd/common/dao/his/HisPatientDao.java +++ b/src/main/java/com/ynxbd/common/dao/his/HisPatientDao.java @@ -81,6 +81,7 @@ public class HisPatientDao { if (patient == null) { throw new ServiceException(JsonResult.getMessage()); } + String patientId = patient.getPatientId(); if (patientId == null) { throw new ServiceException(JsonResult.getMessage()); @@ -164,22 +165,22 @@ public class HisPatientDao { * * @return 患者 */ - public String getGMCPatientId(String hisPatientId) { - if (ObjectUtils.isEmpty(hisPatientId)) { + public String getGMCEmpiId(String patientId) { + if (ObjectUtils.isEmpty(patientId)) { return null; } Map params = new HashMap<>(); - params.put("PatientID", hisPatientId); + params.put("PatientID", patientId); JsonResult jsonResult = HisHelper.getJsonResult(HisEnum.AP_Query_GMC_Patient, params); if (!jsonResult.success()) { return null; } - String gmcId = jsonResult.getDataMapString("Empi_Id"); - if (ObjectUtils.isEmpty(gmcId)) { + String empiId = jsonResult.getDataMapString("Empi_Id"); + if (ObjectUtils.isEmpty(empiId)) { return null; } - return gmcId; + return empiId; } /** diff --git a/src/main/java/com/ynxbd/common/helper/common/CodeHelper.java b/src/main/java/com/ynxbd/common/helper/common/CodeHelper.java index 4181840..60efbcb 100644 --- a/src/main/java/com/ynxbd/common/helper/common/CodeHelper.java +++ b/src/main/java/com/ynxbd/common/helper/common/CodeHelper.java @@ -2,7 +2,10 @@ package com.ynxbd.common.helper.common; import com.ynxbd.common.bean.enums.MerchantEnum; import com.ynxbd.common.bean.pay.Order; +import org.apache.commons.lang3.ObjectUtils; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.*; @@ -58,6 +61,7 @@ public class CodeHelper { public static void main(String[] args) { System.out.println(UUID.randomUUID()); } + /** * 生成数字验证码 * @@ -267,4 +271,27 @@ public class CodeHelper { return result; } + + /** + * SHA256 哈希 → 小写十六进制 + */ + public static String sha256ToHexLowerCase(String input) { + try { + if (ObjectUtils.isEmpty(input)) { + return null; + } + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + + // 转十六进制小写 + StringBuilder hexSb = new StringBuilder(); + for (byte b : hashBytes) { + // 0xFF 保证无符号转换,%02x 保证两位小写 + hexSb.append(String.format("%02x", b & 0xFF)); + } + return hexSb.toString(); + } catch (Exception e) { + return null; + } + } } diff --git a/src/main/java/com/ynxbd/common/service/GMCService.java b/src/main/java/com/ynxbd/common/service/GMCService.java index d08f599..d0ffddd 100644 --- a/src/main/java/com/ynxbd/common/service/GMCService.java +++ b/src/main/java/com/ynxbd/common/service/GMCService.java @@ -6,6 +6,7 @@ import com.ynxbd.common.bean.Patient; import com.ynxbd.common.bean.enums.HCardTypeEnum; import com.ynxbd.common.dao.GMCUserDao; import com.ynxbd.common.dao.PatientDao; +import com.ynxbd.common.dao.his.HisPatientDao; import com.ynxbd.common.helper.common.JsonHelper; import com.ynxbd.common.helper.http.OkHttpHelper; import com.ynxbd.common.result.JsonResult; @@ -57,7 +58,6 @@ public class GMCService { List removeIds = new ArrayList<>(); // 需删除用户ids List dbPatients = new PatientDao().selectListByToken(wxOpenId, unionId); - for (Patient item : gmcPatients) { item.setId(null); item.setOpenid(wxOpenId); @@ -66,15 +66,17 @@ public class GMCService { } Patient findDBItem = dbPatients.isEmpty() ? null : dbPatients.stream().filter(o -> ( - !ObjectUtils.isEmpty(o.getPatientId()) && o.getPatientId().equals(item.getPatientId()) + !ObjectUtils.isEmpty(o.getEmpiId()) && o.getEmpiId().equals(item.getEmpiId()) && !ObjectUtils.isEmpty(o.getIdCardNo()) && o.getIdCardNo().equals(item.getIdCardNo()) )).findFirst().orElse(null); if (findDBItem == null) { // 需新增 + item.setPatientId(null); // 清空本院患者id addList.add(item); } else { // 比对数据 if (!findDBItem.equalsPatient(item)) { // 数据不同->需修改本地数据 item.setId(findDBItem.getId()); + item.setPatientId(findDBItem.getPatientId()); removeIds.add(findDBItem.getId()); addList.add(item); } @@ -83,7 +85,7 @@ public class GMCService { for (Patient item : dbPatients) { Patient findItem = gmcPatients.stream().filter(o -> ( - !ObjectUtils.isEmpty(o.getPatientId()) && o.getPatientId().equals(item.getPatientId())) + !ObjectUtils.isEmpty(o.getEmpiId()) && o.getEmpiId().equals(item.getEmpiId())) && !ObjectUtils.isEmpty(o.getIdCardNo()) && o.getIdCardNo().equals(item.getIdCardNo()) ).findFirst().orElse(null); if (findItem == null) { // 本地数据多余->发送给主体医院=>进行绑定 @@ -97,17 +99,17 @@ public class GMCService { log.error("[医共体]同步用户数据,返回数据为空 id:{}", item.getId()); continue; } - String gmcPatientId = patient.getPatientId(); + String empiId = patient.getEmpiId(); String gmcUniqueId = patient.getGmcUniqueId(); - if (ObjectUtils.isEmpty(gmcUniqueId) || ObjectUtils.isEmpty(gmcPatientId)) { + if (ObjectUtils.isEmpty(gmcUniqueId) || ObjectUtils.isEmpty(empiId)) { log.error("[医共体]同步用户数据,返回唯一键为空 id:{}", item.getId()); continue; } - if (!gmcPatientId.equals(item.getPatientId())) { + if (!empiId.equals(item.getEmpiId())) { removeIds.add(item.getId()); continue; } - if (new PatientDao().updateGmcBindState(gmcUniqueId, item.getId(), item.getPatientId())) { + if (new PatientDao().updateGmcBindState(gmcUniqueId, item.getId(), item.getEmpiId())) { log.error("[医共体]同步用户数据,修改医共体绑定状态失败 id:{}", item.getId()); item.setGmcUniqueId(gmcUniqueId); gmcPatients.add(item); @@ -120,19 +122,50 @@ public class GMCService { // log.info("[医共体-数据同步 本地{}条 移除[{}], add:{}", dbPatients.size(), removeIds, JsonHelper.toJsonString(addList)); - int delRows = 0, addRows = 0; + int delRows = 0; if (!removeIds.isEmpty()) { delRows = new PatientDao().delByIds(wxOpenId, removeIds); } - if (!addList.isEmpty()) { - addRows = new PatientDao().insertBatch(addList); - } + + int addRows = addItems(addList); + long endTime = System.currentTimeMillis(); String takeTime = (endTime - begTime) + "ms"; // 耗时 log.info("[医共体-数据同步][{}]][耗时:{}]-共[{}]条数据, 删除:[{}]条, 同步:[{}]条", wxOpenId, takeTime, gmcPatients.size(), delRows, addRows); return gmcPatients; } + public int addItems(List patients) { + HisPatientDao hisPatientDao = new HisPatientDao(); + int addRows = 0; + List addList = new ArrayList<>(); + if (!patients.isEmpty()) { + for (Patient item : patients) { + try { + if (ObjectUtils.isEmpty(item.getEmpiId())) { + log.info("[医共体-数据同步]本院绑定失败"); + continue; + } + Patient bindInfo = hisPatientDao.bind(false, item); + if (bindInfo == null) { + log.info("[医共体-数据同步]本院绑定失败"); + continue; + } + String patientId = bindInfo.getPatientId(); // 本院患者id + if (ObjectUtils.isEmpty(patientId)) { + continue; + } + item.setPatientId(patientId); + addList.add(item); + } catch (Exception e) { + log.error(e.getMessage()); + } + } + addRows = new PatientDao().insertBatch(addList); + } + return addRows; + } + /** * 医共体绑定 * @@ -189,13 +222,20 @@ public class GMCService { String gmcUniqueId = resp.getString("gmcUniqueId"); String enPatientId = resp.getString("enPatientId"); + String enEmpiId = resp.getString("enEmpiId"); String enOpenId = resp.getString("enOpenId"); - if (ObjectUtils.isEmpty(gmcUniqueId) || ObjectUtils.isEmpty(enPatientId) || ObjectUtils.isEmpty(enOpenId)) { + if (ObjectUtils.isEmpty(gmcUniqueId) || ObjectUtils.isEmpty(enPatientId) || ObjectUtils.isEmpty(enOpenId) || ObjectUtils.isEmpty(enEmpiId)) { return null; } String patientId = AesWxHelper.decode(enPatientId); if (ObjectUtils.isEmpty(patientId)) { - log.error("[医共体]主体绑定,返回patientId解密失败"); + log.error("[医共体]主体绑定-数据enPatientId解密失败"); + return null; + } + + String empiId = AesWxHelper.decode(enEmpiId); + if (ObjectUtils.isEmpty(enEmpiId)) { + log.error("[医共体]主体绑定-数据enEmpiId解密失败"); return null; } @@ -204,6 +244,8 @@ public class GMCService { patient.setEnPatientId(enPatientId); patient.setEnOpenId(enOpenId); patient.setGmcUniqueId(gmcUniqueId); + patient.setEnEmpiId(enEmpiId); + patient.setEmpiId(empiId); return patient; } @@ -211,7 +253,7 @@ public class GMCService { /** * 医共体解绑 * - * @param request request + * @param request request */ public boolean unBindGmcServer(HttpServletRequest request, String openId, String patientId) throws ServiceException { log.info("[医共体解绑-转发]openId={}, patientId={}", openId, patientId); diff --git a/src/main/java/com/ynxbd/common/service/GMCUserService.java b/src/main/java/com/ynxbd/common/service/GMCUserService.java index 1f85e96..ebdd20a 100644 --- a/src/main/java/com/ynxbd/common/service/GMCUserService.java +++ b/src/main/java/com/ynxbd/common/service/GMCUserService.java @@ -1,10 +1,8 @@ package com.ynxbd.common.service; import com.ynxbd.common.bean.GMCUser; -import com.ynxbd.common.bean.User; import com.ynxbd.common.dao.GMCUserDao; import com.ynxbd.common.helper.common.ErrorHelper; -import com.ynxbd.wx.wxfactory.WxCacheHelper; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; @@ -37,5 +35,4 @@ public class GMCUserService { } } - } diff --git a/src/main/java/com/ynxbd/common/service/HCodeService.java b/src/main/java/com/ynxbd/common/service/HCodeService.java index b30fede..6586d34 100644 --- a/src/main/java/com/ynxbd/common/service/HCodeService.java +++ b/src/main/java/com/ynxbd/common/service/HCodeService.java @@ -11,6 +11,7 @@ import com.ynxbd.common.bean.enums.HCardTypeEnum; import com.ynxbd.common.bean.enums.HealthCardEnum; import com.ynxbd.common.bean.enums.HealthCardSceneEnum; import com.ynxbd.common.config.EhCacheConfig; +import com.ynxbd.common.config.HealthCardConfig; import com.ynxbd.common.helper.ProperHelper; import com.ynxbd.common.helper.common.ErrorHelper; import com.ynxbd.common.helper.common.JsonHelper; @@ -32,171 +33,6 @@ import java.util.UUID; @Slf4j public class HCodeService { - @Setter - @Getter - @ToString - @NoArgsConstructor - static class HealthCardResult { // 数据解析 - private JSONObject commonOut; - private JSONObject rsp; - private String resultCode; - private boolean isOk; - - public HealthCardResult(JSONObject resultJsonObj) { - if (resultJsonObj == null) { - return; - } - this.rsp = resultJsonObj.getJSONObject("rsp"); - this.commonOut = resultJsonObj.getJSONObject("commonOut"); - if (this.commonOut == null) { - return; - } - this.resultCode = commonOut.getString("resultCode"); - this.isOk = "0".equals(this.resultCode); - } - } - - - public static final String MINI_CACHE_KEY = "mini_app_token_cache"; - public static final String APP_CACHE_KEY = "app_token_cache"; - - public static synchronized void initCache() { - if (CACHE == null) { // 创建一个缓存实例(7000s最大存活时间) - CACHE = EhCacheConfig.createCache(String.class, String.class, "health_card_cache", 1L, 3L, 10L, false, 7000L, null); - } - } - - private static final boolean IS_ENABLE; // 是否启用电子健康卡(true:启用, false:禁用) - public static final boolean IS_UPLOAD_DATA; // 是否允许上传数据(true:启用, false:禁用) - - // 健康码 - private static final String H_APP_ID; - private static final String H_APP_SECRET; - private static final String H_HOSPITAL_ID; // 医院ID(每家医院不一样) - private static final String H_MINI_HOSPITAL_ID; // 小程序-医院ID(每家医院不一样) - private static final String H_MINI_APP_ID; // 小程序-appid - // 万达 - private static final String CARD_URL; - private static final String CARD_APP_ID; - private static final String CARD_PUBLIC_KEY; - private static final String CARD_PRIVATE_KEY; - - // 缓存 - private static Cache CACHE; - - // 离线WeChartCode 批量领取健康卡用 - private static final String WE_CHART_CODE = "73EFA6796D3869FF82FAE7E81E9814B7"; - - // 公众号不用传,小程序内嵌必传,具体传参请联系平台对接人员分配 - private static final int domainChannel = 0; - - static { - ProperHelper config = new ProperHelper().read("hcode.properties"); - IS_ENABLE = config.getBoolean("is_enable", false); - IS_UPLOAD_DATA = config.getBoolean("h.is_upload_data", true); - // 不用对config设置开关,不开启时也需要用到参数 - H_APP_ID = config.getString("h.app_id"); - H_APP_SECRET = config.getString("h.app_secret"); - CARD_APP_ID = config.getString("h.card_app_id"); - CARD_PUBLIC_KEY = config.getString("h.card_public_key"); - CARD_PRIVATE_KEY = config.getString("h.card_private_key"); - CARD_URL = config.getString("h.card_url"); - - H_HOSPITAL_ID = config.getString("h.hospital_id"); - if (ObjectUtils.isEmpty(H_HOSPITAL_ID)) { - log.info("[电子健康卡]医院id缺失"); - } - H_MINI_APP_ID = config.getString("h.mini_app_id"); - H_MINI_HOSPITAL_ID = config.getString("h.mini_hospital_id"); - - initCache(); - } - - private static HealthCardServerImpl createHealthCardService() { - return new HealthCardServerImpl(H_APP_SECRET); - } - - private static CommonIn createCommonIn(Boolean isMiniApp) { - return createCommonIn(isMiniApp, getAppToken(isMiniApp, true)); - } - - private static CommonIn createCommonIn(Boolean isMiniApp, String appToken) { - if (appToken == null) { // 此处不判断空字符串 - log.info("[电子健康卡]appToken为空"); - return null; - } - if (isMiniApp == null) { - isMiniApp = false; - } - int channelNum = (isMiniApp ? 1 : 0); // 填入代码,0为微信服务号渠道,1为微信小程序渠道,3为刷脸终端,4为扫码终端 - String hospId = (isMiniApp ? H_MINI_HOSPITAL_ID : H_HOSPITAL_ID); - String requestId = UUID.randomUUID().toString().replaceAll("-", "").toUpperCase(); - String appId = (isMiniApp ? H_MINI_APP_ID : WeChatConfig.APP_ID); - return new CommonIn(appToken, requestId, hospId, channelNum, appId, null); - } - - // 判断是否开启电子健康卡 - public static boolean isEnableHCode() { - if (!IS_ENABLE) { - log.info("[配置][hcode.properties]未开启电子健康卡IS_ENABLE"); - } - return IS_ENABLE; - } - - - /** - * 获取token - * - * @param isEnTokenSwitch [true:必须开启功能才能获取token] - * @return token - */ - public static String getAppToken(Boolean isMiniApp, boolean isEnTokenSwitch) { - if (isEnTokenSwitch && !isEnableHCode()) { - return null; - } - - if (isMiniApp == null) { - isMiniApp = false; - } - - String appToken = null; - if (CACHE == null) { - initCache(); - } - if (CACHE != null) { - appToken = CACHE.get(isMiniApp ? MINI_CACHE_KEY : APP_CACHE_KEY); - if (appToken != null) { - return appToken; - } - } - - try { - CommonIn commonIn = createCommonIn(isMiniApp, ""); - if (commonIn == null) { - return null; - } - JSONObject resultJson = new HealthCardServerImpl(H_APP_SECRET, 5, 10).getAppToken(commonIn, H_APP_ID); - HealthCardResult result = new HealthCardResult(resultJson); - if (!result.isOk) { - log.info("[电子健康卡]获取appToken失败: {}", resultJson); - return null; - } - JSONObject respJson = result.getRsp(); - appToken = respJson.getString("appToken"); - - if (CACHE != null && !ObjectUtils.isEmpty(appToken)) { - CACHE.put("appToken", appToken); - } - } catch (Exception e) { - ErrorHelper.println(e); - } - - if (ObjectUtils.isEmpty(appToken)) { - log.info("[电子健康卡]获取appToken为空"); - } - return appToken; - } - /** * 注册健康卡 @@ -216,7 +52,7 @@ public class HCodeService { String birthday, HCardTypeEnum cardTypeEnum, String address, String areaAddress, String sex, String nation, String name, String idCardNo, String phone1) { try { - CommonIn commonIn = createCommonIn(isMiniApp); + CommonIn commonIn = HealthCardConfig.createCommonIn(isMiniApp); if (commonIn == null) { return null; } @@ -241,8 +77,8 @@ public class HCodeService { req.setWechatCode(wechatCode); req.setPatid(patientId); - JSONObject resultJson = createHealthCardService().registerHealthCard(commonIn, req); - HealthCardResult result = new HealthCardResult(resultJson); + JSONObject resultJson = HealthCardConfig.createHealthCardService().registerHealthCard(commonIn, req); + HealthCardConfig.HCardResult result = new HealthCardConfig.HCardResult(resultJson); if (!result.isOk) { log.info("[电子健康卡]注册失败: {}", resultJson); return result.getCommonOut(); @@ -262,12 +98,12 @@ public class HCodeService { */ public static Patient getHealthCardByHealthCode(Boolean isMiniApp, String healthCode) { try { - CommonIn commonIn = createCommonIn(isMiniApp); + CommonIn commonIn = HealthCardConfig.createCommonIn(isMiniApp); if (commonIn == null) { return null; } - JSONObject resultJson = createHealthCardService().getHealthCardByHealthCode(commonIn, healthCode); - HealthCardResult result = new HealthCardResult(resultJson); + JSONObject resultJson = HealthCardConfig.createHealthCardService().getHealthCardByHealthCode(commonIn, healthCode); + HealthCardConfig.HCardResult result = new HealthCardConfig.HCardResult(resultJson); JSONObject respJson = result.getRsp(); if (!result.isOk || respJson == null) { log.info("[电子健康卡]健康卡授权码获取健康卡信息-失败: {}", resultJson); @@ -312,13 +148,13 @@ public class HCodeService { if (ObjectUtils.isEmpty(qrCode)) { return null; } - CommonIn commonIn = createCommonIn(isMiniApp); + CommonIn commonIn = HealthCardConfig.createCommonIn(isMiniApp); if (commonIn == null) { return null; } - JSONObject resultJson = createHealthCardService().getHealthCardByQRCode(commonIn, qrCode); - HealthCardResult result = new HealthCardResult(resultJson); + JSONObject resultJson = HealthCardConfig.createHealthCardService().getHealthCardByQRCode(commonIn, qrCode); + HealthCardConfig.HCardResult result = new HealthCardConfig.HCardResult(resultJson); if (!result.isOk) { log.info("[电子健康卡]二维码获取健康卡失败: {}", resultJson); return null; @@ -363,14 +199,14 @@ public class HCodeService { if (ObjectUtils.isEmpty(idCardNo) || ObjectUtils.isEmpty(name)) { return null; } - CommonIn commonIn = createCommonIn(isMiniApp); + CommonIn commonIn = HealthCardConfig.createCommonIn(isMiniApp); if (commonIn == null) { return null; } String okURL = WeChatConfig.getWebReqURL() + "/health-card-bind.html"; - JSONObject resultJson = createHealthCardService().registerUniformVerifyOrder(commonIn, + JSONObject resultJson = HealthCardConfig.createHealthCardService().registerUniformVerifyOrder(commonIn, idCardNo, "01", name, @@ -384,7 +220,7 @@ public class HCodeService { null, null, 0); - HealthCardResult result = new HealthCardResult(resultJson); + HealthCardConfig.HCardResult result = new HealthCardConfig.HCardResult(resultJson); // JSONObject resultJson = new HealthCardServerImpl(H_APP_SECRET).registerUniformVerifyOrder(commonIn, idCardNo, "01", name, wechatCode); if (!result.isOk) { log.info("[电子健康卡]实人认证生成orderId接口失败: {}", resultJson); @@ -411,13 +247,13 @@ public class HCodeService { if (ObjectUtils.isEmpty(verifyOrderId) || ObjectUtils.isEmpty(registerOrderId)) { return false; } - CommonIn commonIn = createCommonIn(isMiniApp); + CommonIn commonIn = HealthCardConfig.createCommonIn(isMiniApp); if (commonIn == null) { return false; } - JSONObject resultJson = createHealthCardService().checkUniformVerifyResult(commonIn, verifyOrderId, registerOrderId); - HealthCardResult result = new HealthCardResult(resultJson); + JSONObject resultJson = HealthCardConfig.createHealthCardService().checkUniformVerifyResult(commonIn, verifyOrderId, registerOrderId); + HealthCardConfig.HCardResult result = new HealthCardConfig.HCardResult(resultJson); if (!result.isOk) { log.info("[电子健康卡]实人认证结果查询失败: {}", resultJson); return false; @@ -441,13 +277,13 @@ public class HCodeService { */ public static JSONObject getCardOrderId(Boolean isMiniApp, String qrCodeText) { try { - CommonIn commonIn = createCommonIn(isMiniApp); + CommonIn commonIn = HealthCardConfig.createCommonIn(isMiniApp); if (commonIn == null) { return null; } //调用接口 - JSONObject resultJson = createHealthCardService().getOrderIdByOutAppId(commonIn, WeChatConfig.APP_ID, qrCodeText); - HealthCardResult result = new HealthCardResult(resultJson); + JSONObject resultJson = HealthCardConfig.createHealthCardService().getOrderIdByOutAppId(commonIn, WeChatConfig.APP_ID, qrCodeText); + HealthCardConfig.HCardResult result = new HealthCardConfig.HCardResult(resultJson); if (!result.isOk) { log.info("[电子健康卡]获取卡包订单ID 失败: {}", resultJson); return null; @@ -474,13 +310,13 @@ public class HCodeService { } try { - CommonIn commonIn = createCommonIn(isMiniApp); + CommonIn commonIn = HealthCardConfig.createCommonIn(isMiniApp); if (commonIn == null) { return null; } - JSONObject resultJson = createHealthCardService().getDynamicQRCode(commonIn, healthCardId, "01", idCardNo, codeType); - HealthCardResult result = new HealthCardResult(resultJson); + JSONObject resultJson = HealthCardConfig.createHealthCardService().getDynamicQRCode(commonIn, healthCardId, "01", idCardNo, codeType); + HealthCardConfig.HCardResult result = new HealthCardConfig.HCardResult(resultJson); if (!result.isOk) { log.info("[电子健康卡]获取健康卡二维码失败: {}", resultJson); return null; @@ -505,12 +341,12 @@ public class HCodeService { return null; } try { - CommonIn commonIn = createCommonIn(isMiniApp); + CommonIn commonIn = HealthCardConfig.createCommonIn(isMiniApp); if (commonIn == null) { return null; } - JSONObject resultJson = createHealthCardService().bindCardRelation(commonIn, patientId, qrCodeText); - HealthCardResult result = new HealthCardResult(resultJson); + JSONObject resultJson = HealthCardConfig.createHealthCardService().bindCardRelation(commonIn, patientId, qrCodeText); + HealthCardConfig.HCardResult result = new HealthCardConfig.HCardResult(resultJson); if (!result.isOk) { log.info("[电子健康卡]绑定健康卡和医院关系-失败: {}", resultJson); return null; @@ -564,12 +400,12 @@ public class HCodeService { Patient patient = new Patient(); try { - if (!HCodeService.isEnableHCode()) { // 判断是否禁用电子健康卡 + if (!HealthCardConfig.isEnable()) { // 判断是否禁用电子健康卡 return null; } - CommonIn commonIn = new CommonIn(getAppToken(false, false), UUID.randomUUID().toString().replaceAll("-", ""), H_HOSPITAL_ID, 0, null, null); - JSONObject resultJson = createHealthCardService().ocrInfo(commonIn, imageContent); - HealthCardResult result = new HealthCardResult(resultJson); + CommonIn commonIn = new CommonIn(HealthCardConfig.getAppToken(false, false), UUID.randomUUID().toString().replaceAll("-", ""), HealthCardConfig.H_HOSPITAL_ID, 0, null, null); + JSONObject resultJson = HealthCardConfig.createHealthCardService().ocrInfo(commonIn, imageContent); + HealthCardConfig.HCardResult result = new HealthCardConfig.HCardResult(resultJson); if (!result.isOk) { log.info("[电子健康卡]身份证识别失败: {}", resultJson); return null; @@ -607,7 +443,7 @@ public class HCodeService { */ public static JSONArray batchUpdate(List lstPatient) { //构造公共输入参数commonIn - CommonIn commonIn = createCommonIn(false); + CommonIn commonIn = HealthCardConfig.createCommonIn(false); if (commonIn == null) { return null; } @@ -623,15 +459,15 @@ public class HCodeService { healthCardInfo.setNation(patient.getNation()); healthCardInfo.setName(patient.getName()); healthCardInfo.setPhone1(patient.getTel()); - healthCardInfo.setWechatCode(WE_CHART_CODE); + healthCardInfo.setWechatCode(HealthCardConfig.WE_CHART_CODE); healthCardInfo.setPatid(patient.getPatientId()); healthCardInfo.setOpenId(patient.getOpenid()); healthCardInfo.setWechatUrl("http://www.ynxbdkj.cn"); lstHealthCard.add(healthCardInfo); } - JSONObject resultJson = createHealthCardService().registerBatchHealthCard(commonIn, lstHealthCard); - HealthCardResult result = new HealthCardResult(resultJson); + JSONObject resultJson = HealthCardConfig.createHealthCardService().registerBatchHealthCard(commonIn, lstHealthCard); + HealthCardConfig.HCardResult result = new HealthCardConfig.HCardResult(resultJson); JSONObject rspJson = result.getRsp(); if (!result.isOk || rspJson == null) { log.info("[电子健康卡]批量领取-失败: {}", resultJson); @@ -655,7 +491,7 @@ public class HCodeService { * @return JSONObject */ public static JSONObject reportHISData(String qrCodeText, String deptName, String scene, String cardType, String cardCostType) { - if (!HCodeService.IS_UPLOAD_DATA) { // 禁止数据上传 + if (!HealthCardConfig.IS_UPLOAD_DATA) { // 禁止数据上传 return null; } @@ -665,7 +501,7 @@ public class HCodeService { } try { - CommonIn commonIn = createCommonIn(false); + CommonIn commonIn = HealthCardConfig.createCommonIn(false); if (commonIn == null) { return null; } @@ -675,7 +511,7 @@ public class HCodeService { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); req.setTime(format.format(new Date())); - req.setHospitalCode(H_HOSPITAL_ID); + req.setHospitalCode(HealthCardConfig.H_HOSPITAL_ID); // 010101 挂号 req.setScene(scene); req.setDepartment(deptName); // 科室代码 @@ -686,8 +522,8 @@ public class HCodeService { // 自费:0100,医保:0200,公费:0300,其他:0000 req.setCardCostTypes(cardCostType); //调用接口 - JSONObject resultJson = createHealthCardService().reportHISData(commonIn, req); - HealthCardResult result = new HealthCardResult(resultJson); + JSONObject resultJson = HealthCardConfig.createHealthCardService().reportHISData(commonIn, req); + HealthCardConfig.HCardResult result = new HealthCardConfig.HCardResult(resultJson); if (!result.isOk) { log.info("[电子健康卡]用卡数据监测接口 {}", resultJson); return null; @@ -707,12 +543,12 @@ public class HCodeService { */ public Patient getRegInfoByCode(Boolean isMiniApp, String code) { try { - CommonIn commonIn = createCommonIn(isMiniApp); + CommonIn commonIn = HealthCardConfig.createCommonIn(isMiniApp); if (commonIn == null) { return null; } - JSONObject resultJson = createHealthCardService().getRegInfoByCode(commonIn, code); - HealthCardResult result = new HealthCardResult(resultJson); + JSONObject resultJson = HealthCardConfig.createHealthCardService().getRegInfoByCode(commonIn, code); + HealthCardConfig.HCardResult result = new HealthCardConfig.HCardResult(resultJson); JSONObject respJsonObj = result.getRsp(); if (!result.isOk || respJsonObj == null) { log.info("[电子健康卡]获取建档信息-失败: {}", resultJson); @@ -750,19 +586,22 @@ public class HCodeService { * 绑卡验证授权 * * @param wechatCode 微信身份码 - * @param openId openId */ - public static JSONObject registerHealthCardPreAuth(Boolean isMiniApp, String domain, String wechatCode, String healthCode, String openId) { + public static JSONObject registerHealthCardPreAuth(Boolean isMiniApp, Boolean isHCBindUI, String wechatCode, String healthCode, String openid) { try { - CommonIn commonIn = createCommonIn(isMiniApp); + CommonIn commonIn = HealthCardConfig.createCommonIn(isMiniApp); if (commonIn == null) { return null; } + String domain = WeChatConfig.getDomain(false); + String webURL = WeChatConfig.getWebReqURL(); + System.out.println(webURL); + // 成功回调页 - String successRedirectUrl = (isMiniApp ? "mini:/path/to/isvpage?healthCode=" : (domain + "/path/to/isvpage?healthCode=")) + wechatCode; + String successRedirectUrl = (isMiniApp ? "mini:/path/to/isvpage?healthCode=" : (webURL + "/hc-ok.html")) + healthCode; // 失败回调页 - String failRedirectUrl = (isMiniApp ? "/path/to/isvpage?regInfoCode=" : (domain + "/path/to/isvpage?regInfoCode=")) + wechatCode; + String failRedirectUrl = (isMiniApp ? "/path/to/isvpage?regInfoCode=" : (webURL + "/hc-fail.html")) + healthCode; /* * 用户手动填写信息建卡可选两种方式(二选一): * 一、服务商自行提供建卡页(可以是H5或小程序),需增加authCode占位符,示例如下: @@ -775,13 +614,17 @@ public class HCodeService { * 小程序内嵌,仍以 mini 协议开头,且需要UrlEncode编码。 */ - String userFormPageUrl = String.format("%s/h5/tencent/open/card/regist?hospitalId=%s&redirect_uri=%s&fail_redirect_uri=%s&authCode=%s", - (isMiniApp ? domain : " https://h5-health.tengmed.com"), - H_HOSPITAL_ID, + String hcBindUIUrl = String.format("%s/h5/tencent/open/card/regist?hospitalId=%s&redirect_uri=%s&fail_redirect_uri=%s&authCode=%s", + (isMiniApp ? domain : "https://h5-health.tengmed.com"), + HealthCardConfig.H_HOSPITAL_ID, URLHelper.encodeURL(successRedirectUrl), URLHelper.encodeURL(failRedirectUrl), wechatCode); + String userFormPageUrl = !isHCBindUI + ? (WeChatConfig.getWebReqURL() + "?authCode=" + wechatCode) + : hcBindUIUrl; + // 小程序内嵌必传(固定为小程序路径,不需要加“mini:”前缀)示例: /path/to/facePage String faceUrl = isMiniApp ? "/path/to/facePage" : null; // 放弃验证回调页 @@ -789,7 +632,7 @@ public class HCodeService { int patientType = 0; // 0-新患者 1-老患者(针对就诊卡升级健康卡);不传默认为0 - JSONObject resultJson = createHealthCardService().registerHealthCardPreAuth(commonIn, + JSONObject resultJson = HealthCardConfig.createHealthCardService().registerHealthCardPreAuth(commonIn, wechatCode, patientType, successRedirectUrl, @@ -797,9 +640,9 @@ public class HCodeService { userFormPageUrl, faceUrl, verifyFailRedirectUrl, - domainChannel); + HealthCardConfig.domainChannel); log.info("[电子健康卡]绑卡验证授权 resp={}", JsonHelper.toJsonString(resultJson)); - HealthCardResult result = new HealthCardResult(resultJson); + HealthCardConfig.HCardResult result = new HealthCardConfig.HCardResult(resultJson); if (!result.isOk) { log.info("[电子健康卡]绑卡验证授权-失败: {}", resultJson); return result.getCommonOut(); @@ -822,7 +665,7 @@ public class HCodeService { String birthday, HCardTypeEnum cardTypeEnum, String address, String areaAddress, String sex, String nation, String name, String idCardNo, String phone1) { try { - CommonIn commonIn = createCommonIn(isMiniApp); + CommonIn commonIn = HealthCardConfig.createCommonIn(isMiniApp); if (commonIn == null) { return null; } @@ -849,15 +692,15 @@ public class HCodeService { req.setWechatCode(wechatCode); req.setPatid(patientId); - JSONObject resultJson = createHealthCardService().registerHealthCardPreFill(commonIn, + JSONObject resultJson = HealthCardConfig.createHealthCardService().registerHealthCardPreFill(commonIn, req, authCode, successRedirectUrl, failRedirectUrl, faceUrl, verifyFailRedirectUrl, - domainChannel); - HealthCardResult result = new HealthCardResult(resultJson); + HealthCardConfig.domainChannel); + HealthCardConfig.HCardResult result = new HealthCardConfig.HCardResult(resultJson); if (!result.isOk) { log.info("[电子健康卡]提交建卡相关信息-失败: {}", resultJson); return result.getCommonOut(); diff --git a/src/main/java/com/ynxbd/common/service/HealthCardService.java b/src/main/java/com/ynxbd/common/service/HealthCardService.java new file mode 100644 index 0000000..fb7cdff --- /dev/null +++ b/src/main/java/com/ynxbd/common/service/HealthCardService.java @@ -0,0 +1,78 @@ +package com.ynxbd.common.service; + +import com.alibaba.fastjson.JSONObject; +import com.tencent.healthcard.model.CommonIn; +import com.ynxbd.common.config.HealthCardConfig; +import com.ynxbd.common.helper.common.ErrorHelper; +import com.ynxbd.common.helper.common.JsonHelper; +import com.ynxbd.common.helper.common.URLHelper; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class HealthCardService { + + /** + * 绑卡验证授权 + * + * @param wechatCode 微信身份码 + * @param openId openId + */ + public static JSONObject registerHealthCardPreAuth(Boolean isMiniApp, String domain, String wechatCode, String healthCode, String openId) { + try { + CommonIn commonIn = HealthCardConfig.createCommonIn(isMiniApp); + if (commonIn == null) { + return null; + } + + // 成功回调页 + String successRedirectUrl = (isMiniApp ? "mini:/path/to/isvpage?healthCode=" : (domain + "/path/to/isvpage?healthCode=")) + wechatCode; + // 失败回调页 + String failRedirectUrl = (isMiniApp ? "/path/to/isvpage?regInfoCode=" : (domain + "/path/to/isvpage?regInfoCode=")) + wechatCode; + /* + * 用户手动填写信息建卡可选两种方式(二选一): + * 一、服务商自行提供建卡页(可以是H5或小程序),需增加authCode占位符,示例如下: + * H5: https://xxx.xxx.xx/path/to/isvpage?authCode=${authCode} |小程序: mini:/path/to/isvpage?authCode=${authCode} + * 二、使用开放平台的H5绑卡组件页,需增加authCode占位符,示例如下: + * https://h5-health.tengmed.com/h5/tencent/open/card/regist?hospitalId=${hospitalId}&redirect_uri=${redirect_uri}&fail_redirect_uri=${fail_redirect_uri}&authCode=${authCode}小程序需修改该域名地址为已配置的业务域名 + * ${hospitalId},必传,为开放平台分配的医院ID(hospitalId); + * ${redirect_uri},必传,为建卡成功后的回跳服务商页面,必须要对该URL进行UrlEncode编码。 + * ${fail_redirect_uri},必传,为建卡失败后的回跳服务商页面,必须要对该URL进行UrlEncode编码。 + * 小程序内嵌,仍以 mini 协议开头,且需要UrlEncode编码。 + */ + + String userFormPageUrl = String.format("%s/h5/tencent/open/card/regist?hospitalId=%s&redirect_uri=%s&fail_redirect_uri=%s&authCode=%s", + (isMiniApp ? domain : " https://h5-health.tengmed.com"), + HealthCardConfig.H_HOSPITAL_ID, + URLHelper.encodeURL(successRedirectUrl), + URLHelper.encodeURL(failRedirectUrl), + wechatCode); + + // 小程序内嵌必传(固定为小程序路径,不需要加“mini:”前缀)示例: /path/to/facePage + String faceUrl = isMiniApp ? "/path/to/facePage" : null; + // 放弃验证回调页 + String verifyFailRedirectUrl = isMiniApp ? "mini:/path/to/isvpage" : (domain + "/path/to/isvpage"); + + int patientType = 0; // 0-新患者 1-老患者(针对就诊卡升级健康卡);不传默认为0 + + JSONObject resultJson = HealthCardConfig.createHealthCardService().registerHealthCardPreAuth(commonIn, + wechatCode, + patientType, + successRedirectUrl, + failRedirectUrl, + userFormPageUrl, + faceUrl, + verifyFailRedirectUrl, + HealthCardConfig.domainChannel); + log.info("[电子健康卡]绑卡验证授权 resp={}", JsonHelper.toJsonString(resultJson)); + HealthCardConfig.HCardResult result = new HealthCardConfig.HCardResult(resultJson); + if (!result.isOk) { + log.info("[电子健康卡]绑卡验证授权-失败: {}", resultJson); + return result.getCommonOut(); + } + return result.getRsp(); + } catch (Exception e) { + ErrorHelper.println(e); + } + return null; + } +} diff --git a/src/main/java/com/ynxbd/common/service/HealthUploadService.java b/src/main/java/com/ynxbd/common/service/HealthUploadService.java index 959fefa..76d2036 100644 --- a/src/main/java/com/ynxbd/common/service/HealthUploadService.java +++ b/src/main/java/com/ynxbd/common/service/HealthUploadService.java @@ -3,6 +3,7 @@ package com.ynxbd.common.service; import com.alibaba.fastjson.JSONObject; import com.ynxbd.common.bean.Patient; import com.ynxbd.common.bean.enums.HealthCardEnum; +import com.ynxbd.common.config.HealthCardConfig; import com.ynxbd.common.dao.PatientDao; import lombok.extern.slf4j.Slf4j; @@ -22,11 +23,11 @@ public class HealthUploadService { */ public void regPayReportHISData(String openid, String patientId, String deptName, String regDate) { try { - if (!HCodeService.IS_UPLOAD_DATA) { // 禁止数据上传 + if (!HealthCardConfig.IS_UPLOAD_DATA) { // 禁止数据上传 return; } - if (!HCodeService.isEnableHCode()) { // 判断是否禁用电子健康卡 + if (!HealthCardConfig.isEnable()) { // 判断是否禁用电子健康卡 return; } @@ -72,10 +73,10 @@ public class HealthUploadService { */ public void rxReportHISData(String openid, String patientId) { try { - if (!HCodeService.IS_UPLOAD_DATA) { // 禁止数据上传 + if (!HealthCardConfig.IS_UPLOAD_DATA) { // 禁止数据上传 return; } - if (!HCodeService.isEnableHCode()) { // 判断是否禁用电子健康卡 + if (!HealthCardConfig.isEnable()) { // 判断是否禁用电子健康卡 return; } diff --git a/src/main/java/com/ynxbd/common/service/PatientService.java b/src/main/java/com/ynxbd/common/service/PatientService.java index 0f324d1..33332b4 100644 --- a/src/main/java/com/ynxbd/common/service/PatientService.java +++ b/src/main/java/com/ynxbd/common/service/PatientService.java @@ -81,14 +81,30 @@ public class PatientService { GMCUserService gmcUserService = new GMCUserService(); GMCUser gmcUser = gmcUserService.queryInfoByOpenId(openId); if (gmcUser == null) { - log.warn("[医供体]未找到关联关系 openId={}", openId); + log.warn("[医共体]未找到关联关系 openId={}", openId); return new ArrayList<>(); } return new GMCService().syncPatientData(request, openId, gmcUser.getGmcOpenId(), gmcUser.getGmcUnionId()); } - List patients = new PatientDao().selectListByToken(openId, unionId); - if (isEnPid) { - patients = enPatientList(patients); + List dbPatients = new PatientDao().selectListByToken(openId, unionId); + + List patients = new ArrayList<>(); + for (Patient item : dbPatients) { + String patientId = item.getPatientId(); + item.setHisPatientId(patientId); // 本院患者id + + if (WeChatConfig.IS_ENABLE_GMC) { + String empiId = item.getEmpiId(); + if (ObjectUtils.isEmpty(empiId)) { // 医共体开启时,如果没有id就不返回该信息 + continue; + } + item.setPatientId(empiId); + } + + if (isEnPid) { + item.filterData(false); + } + patients.add(item); } return patients; } @@ -180,33 +196,36 @@ public class PatientService { return Result.error(ResultEnum.INTERFACE_HIS_INVOKE_ERROR); // HIS接口调用失败 } + String empiId = hisPatient.getEmpiId(); // 医共体ID String patientId = hisPatient.getPatientId(); String hisTransNo = hisPatient.getHisTransNo(); - // String gmcHospId = null; // 医共体下的医院id + if (WeChatConfig.IS_ENABLE_GMC && ObjectUtils.isEmpty(empiId)) { + return Result.error("医共体绑定失败,HIS未返回医共体ID"); + } + + bindInfo.setEmpiId(empiId); bindInfo.setPatientId(patientId); bindInfo.setHisTransNo(hisTransNo); - // bindInfo.setGmcHospId(gmcHospId); - try { List dbPatients = patientDao.selectByOpenIdAndCardNo(openid, idCardNo); if (dbPatients.isEmpty()) { // 数据库没有-->添加 - log.info("[用户身份绑定]添加 name={}, patientId={}", name, patientId); + log.info("[用户身份绑定]添加 name={}, patientId={}, empiId={}", name, patientId, empiId); bindInfo.setGmcBindState(1); if (bindInfo.getEnUnionId() != null) { bindInfo.setUnionId(AesWxHelper.decode(bindInfo.getEnUnionId())); } if (!patientDao.insert(isMyself, bindInfo)) { - log.info("[用户身份绑定]添加患者失败 name={}, patientId={}", name, patientId); + log.info("[用户身份绑定]添加患者失败 name={}, patientId={}, empiId={}", name, patientId, empiId); return Result.error(ResultEnum.PATIENT_ADD_ERROR); } } else { - log.info("[患者]更新 isMyself={}, patientId={}, name={}, healthCardId={}", isMyself, patientId, name, healthCardId); + log.info("[患者]更新 isMyself={}, patientId={}, name={}, empiId={}, healthCardId={}", isMyself, patientId, name, empiId, healthCardId); if (isMyself) { if (!patientDao.updateMyself(bindInfo)) { - log.info("[患者]自身信息更新失败 patientId={}", patientId); + log.info("[患者]自身信息更新失败 patientId={}, empiId={}", patientId, empiId); return Result.error(ResultEnum.PATIENT_UPDATE_ERROR); } } else { @@ -227,6 +246,9 @@ public class PatientService { map.put("gmcUniqueId", bindInfo.getGmcUniqueId()); map.put("enOpenId", AesWxHelper.encode(openid)); map.put("enPatientId", AesWxHelper.encode(patientId)); + if (!ObjectUtils.isEmpty(empiId)) { + map.put("enEmpiId", AesWxHelper.encode(empiId)); + } return Result.success(map); } @@ -450,28 +472,25 @@ public class PatientService { PatientDao patientDao = new PatientDao(); HisPatientDao hisPatientDao = new HisPatientDao(); for (Patient patient : patients) { - if (HCardTypeEnum.NO_CARD.WX_CODE.equals(patient.getCardType()) || ObjectUtils.isEmpty(patient.getIdCardNo())) { // 无证绑定 - continue; - } - String gmcPatientId = null; + String empiId = null; // 医共体患者id Integer id = patient.getId(); - String hisPatientId = patient.getPatientId(); // 旧id + String patientId = patient.getPatientId(); // 本院患者id // 有证 boolean hasCard = !HCardTypeEnum.NO_CARD.WX_CODE.equals(patient.getCardType()) && !ObjectUtils.isEmpty(patient.getIdCardNo()); if (hasCard) { - gmcPatientId = hisPatientDao.getGMCPatientId(hisPatientId); // 医共体患者id - if (ObjectUtils.isEmpty(gmcPatientId)) { + empiId = hisPatientDao.getGMCEmpiId(patientId); // 医共体患者id + if (ObjectUtils.isEmpty(empiId)) { patientDao.removePatient(id, "医共体"); - log.warn("[医共体id]替换失败 hisPatientId={}", hisPatientId); + log.warn("[医共体id]查询失败 patientId={}", patientId); continue; } } - boolean isUpdate = patientDao.updateGmcInfo(hasCard, id, gmcPatientId, hisPatientId); + boolean isUpdate = patientDao.updateGmcInfo(hasCard, id, empiId); if (!isUpdate) { errNum = errNum + 1; - log.warn("[医共体]患者id替换更新数据失败 id={}, hisPatientId={}", id, hisPatientId); + log.warn("[医共体]患者id替换更新数据失败 id={}, patientId={}", id, patientId); } } log.warn("[医共体]患者id替换 共:[{}]条, 失败:[{}]条", patients.size(), errNum); @@ -539,7 +558,6 @@ public class PatientService { } - // /** // * [维护]替换患者id为医共体id // * diff --git a/src/main/java/com/ynxbd/wx/wxfactory/AesWxHelper.java b/src/main/java/com/ynxbd/wx/wxfactory/AesWxHelper.java index 8536626..fb2c7fb 100644 --- a/src/main/java/com/ynxbd/wx/wxfactory/AesWxHelper.java +++ b/src/main/java/com/ynxbd/wx/wxfactory/AesWxHelper.java @@ -26,8 +26,13 @@ public class AesWxHelper extends AesHelper { return encryptHex(data.toString(), KEY, IV); } + /** * 加密 + * + * @param data 数据 + * @param isDataNotNull 是否允许为空 + * @return string */ public static String encode(String data, boolean isDataNotNull) { if (isDataNotNull && ObjectUtils.isEmpty(data)) { @@ -57,7 +62,7 @@ public class AesWxHelper extends AesHelper { } public static void main(String[] args) { - System.out.println(encode("30020502")); + System.out.println(encode("162423")); // System.out.println(decode("E6835E243069406F53EC8464898B37C0")); } } diff --git a/src/main/java/com/ynxbd/wx/wxfactory/WxFactory.java b/src/main/java/com/ynxbd/wx/wxfactory/WxFactory.java index d07b7bf..66a4906 100644 --- a/src/main/java/com/ynxbd/wx/wxfactory/WxFactory.java +++ b/src/main/java/com/ynxbd/wx/wxfactory/WxFactory.java @@ -44,6 +44,12 @@ public class WxFactory { } } + @NoArgsConstructor + public static class MedIns { + public static com.ynxbd.wx.wxfactory.medins.Client Common() { + return new com.ynxbd.wx.wxfactory.medins.Client(); + } + } /** * 响应数据 diff --git a/src/main/java/com/ynxbd/wx/wxfactory/WxMedHelper.java b/src/main/java/com/ynxbd/wx/wxfactory/WxMedHelper.java index 23027c2..638e86e 100644 --- a/src/main/java/com/ynxbd/wx/wxfactory/WxMedHelper.java +++ b/src/main/java/com/ynxbd/wx/wxfactory/WxMedHelper.java @@ -267,6 +267,7 @@ public class WxMedHelper { ); } + /** * [医保]退现金部分 */ diff --git a/src/main/java/com/ynxbd/wx/wxfactory/WxMedInsHelper.java b/src/main/java/com/ynxbd/wx/wxfactory/WxMedInsHelper.java new file mode 100644 index 0000000..3240616 --- /dev/null +++ b/src/main/java/com/ynxbd/wx/wxfactory/WxMedInsHelper.java @@ -0,0 +1,8 @@ +package com.ynxbd.wx.wxfactory; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class WxMedInsHelper { + +} diff --git a/src/main/java/com/ynxbd/wx/wxfactory/base/auth/Client.java b/src/main/java/com/ynxbd/wx/wxfactory/base/auth/Client.java index 66b829c..e67c7b0 100644 --- a/src/main/java/com/ynxbd/wx/wxfactory/base/auth/Client.java +++ b/src/main/java/com/ynxbd/wx/wxfactory/base/auth/Client.java @@ -1,10 +1,14 @@ package com.ynxbd.wx.wxfactory.base.auth; -import com.alibaba.fastjson.JSONObject; +import com.ynxbd.common.helper.common.Base64Helper; +import com.ynxbd.common.helper.common.CodeHelper; import com.ynxbd.common.helper.common.JsonHelper; import com.ynxbd.common.helper.http.OkHttpHelper; +import com.ynxbd.common.result.Result; +import com.ynxbd.common.result.ServiceException; import com.ynxbd.wx.wxfactory.WxCacheHelper; import com.ynxbd.wx.wxfactory.base.auth.models.*; +import com.ynxbd.wx.wxfactory.payment.WXPayUtility; import com.ynxbd.wx.wxfactory.utils.EmojiHelper; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -129,12 +133,98 @@ public class Client { params.put("action_info", action_info); }, null); log.info("[创建公众号二维码]{}", respJson); - if(respJson == null) { + if (respJson == null) { return null; } return JsonHelper.parseObject(respJson, QRCodeInfo.class); } + + public String getAccessToken() throws ServiceException { + AccessToken resp = WxCacheHelper.getWxAccessToken(); + if (resp == null) { + throw new ServiceException("获取token失败"); + } + + String accessToken = resp.getAccessToken(); + if (accessToken == null) { + throw new ServiceException("获取token数据为空"); + } + return accessToken; + } + + /** + * 获取用户人脸核身会话唯一标识 + * + * @return accessToken + */ + public WxVerifyId getVerifyId(String openid, String certName, String certNo) throws ServiceException { + String accessToken = getAccessToken(); + + Map cert_info = new HashMap<>(); + cert_info.put("cert_type", "IDENTITY_CARD"); + cert_info.put("cert_name", certName); // 证件姓名 + cert_info.put("cert_no", certNo); // 证件号码 + + String outSeqNo = CodeHelper.get32UUID(); + + String respJson = OkHttpHelper.postJsonStr("https://api.weixin.qq.com/cityservice/face/identify/getverifyid?access_token=" + accessToken, params -> { + params.put("out_seq_no", outSeqNo); // 业务方系统内部流水号,要求5-32个字符内,只能包含数字、大小写字母和_-字符,且在同一个appid下唯一。 + params.put("cert_info", cert_info); // 用户身份信息 + params.put("openid", openid); // 用户身份标识 + }, null); + log.info("[获取用户人脸核身会话唯一标识][{}] openid={}, certName={}, resp:{}", outSeqNo, openid, certName, respJson); + if (respJson == null) { + throw new ServiceException("[获取用户人脸核身会话唯一标识]接口调用失败"); + } + WxVerifyId info = JsonHelper.parseObject(respJson, WxVerifyId.class); + if (!info.isOk()) { + log.info("[获取用户人脸核身会话唯一标识][{}]接口调用失败 openid={}, resp={}", outSeqNo, openid, respJson); + throw new ServiceException(info.filterErrMsg()); + } + info.setOutSeqNo(outSeqNo); + + String base64Data = String.format("cert_type=%s&cert_name=%s&cert_no=%s", + Base64Helper.encode("IDENTITY_CARD"), + Base64Helper.encode("certName"), + Base64Helper.encode("certNo") + ); + info.setCertHash(CodeHelper.sha256ToHexLowerCase(base64Data)); + return info; + } + + + /** + * 查询用户人脸核身真实验证结果 + * + * @return accessToken + */ + public WxVerifyId queryVerifyInfo(String openid, String outSeqNo, String verifyId, String certHash) throws ServiceException { + String accessToken = getAccessToken(); + + String respJson = OkHttpHelper.postJsonStr("https://api.weixin.qq.com/cityservice/face/identify/queryverifyinfo?access_token=" + accessToken, params -> { + params.put("out_seq_no", outSeqNo); // 业务方系统外部流水号,必须和 getVerifyId 接口传入的一致 + params.put("verify_id", verifyId); // getVerifyId 接口返回的人脸核身会话唯一标识 + params.put("cert_hash", certHash); // 根据 getVerifyId 中传入的证件信息生成的信息摘要,计算方式见注意事项。 + params.put("openid", openid); // 用户身份标识 + }, null); + log.info("[查询用户人脸核身真实验证结果][{}] openid={}, resp:{}", outSeqNo, openid, respJson); + if (respJson == null) { + throw new ServiceException("[查询用户人脸核身真实验证结果]接口调用失败"); + } + WxVerifyId info = JsonHelper.parseObject(respJson, WxVerifyId.class); + if (!info.isOk()) { + log.info("[查询用户人脸核身真实验证结果][{}]接口调用失败 openid={}, resp={}", outSeqNo, openid, respJson); + throw new ServiceException(info.filterErrMsg()); + } + if (!info.isVerifyOk()) { // 验证未通过 + log.info("[查询用户人脸核身真实验证结果][{}]验证未通过 openid={}", outSeqNo, openid); + throw new ServiceException(WxVerifyId.FaceVerifyCodeEnum.findEnumMsg(info)); + } + return info; + } + + // public static String getSnsToken() { // if (ACCESS_TOKEN_CACHE == null) { // createAccessTokenCache(); diff --git a/src/main/java/com/ynxbd/wx/wxfactory/base/auth/models/WxVerifyId.java b/src/main/java/com/ynxbd/wx/wxfactory/base/auth/models/WxVerifyId.java new file mode 100644 index 0000000..0014a83 --- /dev/null +++ b/src/main/java/com/ynxbd/wx/wxfactory/base/auth/models/WxVerifyId.java @@ -0,0 +1,107 @@ +package com.ynxbd.wx.wxfactory.base.auth.models; + + +import lombok.*; +import org.apache.commons.lang3.ObjectUtils; + +@Getter +@Setter +@ToString +@NoArgsConstructor +public class WxVerifyId { + private Integer errCode; + private String errMsg; + private String verifyId; + private Integer expiresIn; + private String outSeqNo; + private Integer verifyRet; + private String certHash; + + // 接口请求状态 + public boolean isOk() { + return errCode != null && errCode == 0; + } + + public String filterErrMsg() { + if (ObjectUtils.isEmpty(errMsg)) { + return "接口调用失败"; + } + return errMsg; + } + + // 是否校验通过 + public boolean isVerifyOk() { + return verifyRet != null && verifyRet.equals(FaceVerifyCodeEnum.SUCCESS.code); + } + + + @AllArgsConstructor + public enum FaceVerifyCodeEnum { + // 成功 + SUCCESS(10000, "识别成功"), + + _90001(90001, "设备不支持人脸检测"), + _90002(90002, "用户取消"), + _90003(90003, "用户取消"), + _90004(90004, "用户取消"), + _90005(90005, "用户取消"), + _90006(90006, "用户取消"), + _90007(90007, "网络错误"), + _90008(90008, "相机权限未授权"), + _90009(90009, "麦克风权限未授权"), + _90010(90010, "相机和麦克风权限都未授权"), + _90011(90011, "人脸数据采集无效"), + _90012(90012, "网络错误上传失败"), + _90013(90013, "人脸数据采集无效"), + _90014(90014, "人脸数据采集无效"), + _90017(90017, "识别过程超时"), + _90018(90018, "系统错误"), + _90104(90104, "获取人脸配置失败"), + _90105(90105, "获取确认数据失败"), + _90106(90106, "相机失败"), + _90107(90107, "用户检测超时"), + _90109(90109, "设备不支持人脸检测"), + _90110(90110, "获取协议信息失败"), + _90199(90199, "用户系统错误"), + + _10001(10001, "参数错误"), + _10002(10002, "人脸特征检测失败"), + _10003(10003, "身份证号不匹配"), + _10004(10004, "比对人脸信息不匹配"), + _10005(10005, "正在检测中"), + _10006(10006, "appid 没有权限"), + _10007(10007, "后台获取图片失败"), + _10008(10008, "系统失败"), + _10010(10010, "照片质量较低"), + _10012(10012, "比对验证失败"), + _10013(10013, "系统错误"), + _10014(10014, "系统失败"), + _10015(10015, "系统失败"), + _10016(10016, "存储用户图片失败"), + _10017(10017, "非法 Id"), + _10018(10018, "用户信息不存在"), + _10020(10020, "认证超时"), + _10021(10021, "重复的请求,返回上一次的结果"), + _10026(10026, "用户身份数据不在权威源比对库中"), + _10029(10029, "请求超时"), + _10040(10040, "请求数据编码不对,必须是 UTF8 编码"), + _10041(10041, "非法用户"), + _10042(10042, "请求过于频繁,稍后再重试"), + _10045(10045, "系统失败"), + _10052(10052, "请求超时"), + _10300(10300, "未完成核身"); + + public final Integer code; + public final String msg; + + + public static String findEnumMsg(WxVerifyId info) { + for (FaceVerifyCodeEnum e : values()) { + if (e.code.equals(info.getVerifyRet())) { + return e.msg; + } + } + return "未知异常"; + } + } +} diff --git a/src/main/java/com/ynxbd/wx/wxfactory/medical/WxMedConfig.java b/src/main/java/com/ynxbd/wx/wxfactory/medical/WxMedConfig.java index aa96b89..35636b4 100644 --- a/src/main/java/com/ynxbd/wx/wxfactory/medical/WxMedConfig.java +++ b/src/main/java/com/ynxbd/wx/wxfactory/medical/WxMedConfig.java @@ -70,7 +70,7 @@ public class WxMedConfig { } - protected static String getConfigUrl(String returnUrl) { + public static String getConfigUrl(String returnUrl) { try { String url = IS_DEV ? "https://mitest.wecity.qq.com" : "https://card.wecity.qq.com"; diff --git a/src/main/java/com/ynxbd/wx/wxfactory/medins/Client.java b/src/main/java/com/ynxbd/wx/wxfactory/medins/Client.java new file mode 100644 index 0000000..2ff412b --- /dev/null +++ b/src/main/java/com/ynxbd/wx/wxfactory/medins/Client.java @@ -0,0 +1,374 @@ +package com.ynxbd.wx.wxfactory.medins; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.ynxbd.common.helper.common.ErrorHelper; +import com.ynxbd.common.helper.common.JsonHelper; +import com.ynxbd.common.helper.http.OkHttpHelper; +import com.ynxbd.common.result.JsonResult; +import com.ynxbd.wx.wxfactory.medical.WxMedConfig; +import com.ynxbd.wx.wxfactory.medical.enums.MdRefundTypeEnum; +import com.ynxbd.wx.wxfactory.medical.models.*; +import com.ynxbd.wx.wxfactory.medins.models.CreateOrder; +import com.ynxbd.wx.wxfactory.payment.OrderMIEnum; +import com.ynxbd.wx.wxfactory.utils.WxRequestHelper; +import com.ynxbd.wx.wxfactory.utils.WxSignHelper; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; + +@Slf4j +@NoArgsConstructor +public class Client { + + /** + * 免密授权地址 + * + * @param url url + */ + public String getAuthUrl(String url) { + String configUrl = WxMedConfig.getConfigUrl(url); + log.info("[医保]免密授权地址-{}", configUrl); + return configUrl; + } + + /** + * 微信医保下单 + * + * @param OrderMIEnum 订单类型 + * @param appId appId + * @param cityCode 城市编码 + * @param hospitalName 医院名称 + * @param orgNo 医疗机构编码(医保局分配给机构) + * @param channel channel + * @param openId openid + * @param payAuthNo 医保授权码 + * @param payOrdId 医保订单id + * @param outTradeNo 商户订单号 + * @param serialNo serialNo + * @param totalFee 【使用该接口下单的总金额】 单位分,使用该接口下单的总金额。 + * @param insuranceFee 医保支付金额 + * @param cashFee 自费现金部分 + * @param govFee 统筹支付 + * @param selfFee 个账支付 + * @param cardNo 证件号码 + * @param realName 真实姓名 + * @param callbackUrl 回调通知地址 + * @return MedicalPayOrder + */ + public MedicalPayOrder createOrder(OrderMIEnum OrderMIEnum, + String appId, String openId, String cityCode, String hospitalName, + String orgNo, String channel, + String payAuthNo, String payOrdId, String outTradeNo, String serialNo, + BigDecimal totalFee, BigDecimal insuranceFee, BigDecimal cashFee, BigDecimal govFee, BigDecimal selfFee, + String cardNo, String realName, + String callbackUrl, String attach, boolean isRelative, String relativeName, String relativeCardNo) { + try { + if (cashFee == null) cashFee = new BigDecimal(0); + + boolean isPureSelfPaid = totalFee.compareTo(cashFee) == 0 ; // 为纯自费支付 + + // TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756 + CreateOrder client = new CreateOrder(); + + CreateOrder.CreateOrderRequest request = new CreateOrder.CreateOrderRequest(); + request.mixPayType = CreateOrder.MixPayType.CASH_AND_INSURANCE; // 混合支付类型 + request.orderType = CreateOrder.OrderType.REG_PAY; // 订单类型 + + request.appid = appId; + request.openid = openId; + request.payer = new CreateOrder.PersonIdentification(); + request.payer.name = client.encrypt(realName); + request.payer.idDigest = client.encrypt(cardNo); + request.payer.cardType = CreateOrder.UserCardType.ID_CARD; + request.payForRelatives = isRelative; // 【是否代亲属支付】 不传默认替本人支付【代亲属支付:true 本人支付:false】 + + if (isRelative) { // 亲属身份信息 + request.relative = new CreateOrder.PersonIdentification(); + request.relative.name = client.encrypt(relativeName); + request.relative.idDigest = client.encrypt(relativeCardNo); + request.relative.cardType = CreateOrder.UserCardType.ID_CARD; + } + + request.outTradeNo = outTradeNo; // 商户订单号 + request.serialNo = serialNo; // 医疗机构订单号 + request.payOrderId = payOrdId; // 医保局返回的支付单ID + request.payAuthNo = payAuthNo; // 医保局返回的支付授权码 + request.geoLocation = "0,0"; // 经纬度未获取到用户定位时,传“0,0”。| 纯医保支付或医保自费混合支付时该字段必填,纯自费支付时禁止填写。 + request.cityId = cityCode; // 城市ID + + request.medInstName = hospitalName; // 医疗机构名称 + request.medInstNo = orgNo; + request.medInsOrderCreateTime = LocalDateTime.now().toString(); + + request.totalFee = totalFee.movePointRight(2).longValue(); + if (!isPureSelfPaid) { + request.medInsGovFee = govFee.movePointRight(2).longValue(); // 【医保统筹支付金额】纯医保支付或医保自费混合支付时该字段必填,纯自费支付时禁止填写。 + request.medInsSelfFee = selfFee.movePointRight(2).longValue(); // 【医保个账支付金额】纯医保支付或医保自费混合支付时该字段必填,纯自费支付时禁止填写。 + request.medInsOtherFee = 0L; // 【医保其他支付金额】纯医保支付或医保自费混合支付时该字段必填,纯自费支付时禁止填写。 + + long medInsCashFee = cashFee.movePointRight(2).longValue(); + request.medInsCashFee = medInsCashFee; // 【需要自费的金额】纯医保支付或医保自费混合支付时该字段必填,纯自费支付时禁止填写。 + request.wechatPayCashFee = medInsCashFee; // 【实际需要用户微信支付的金额】纯自费支付或医保自费混合支付时必填,纯医保支付时禁止填写。| 实际需要用户微信支付的金额(wechat_pay_cash_fee)= 医保结算后需要自费的金额(med_ins_cash_fee)+ 现金补充金额(cash_add_detail) - 现金减免金额(cash_reduce_detail) + } + + request.callbackUrl = callbackUrl; + request.prepayId = "wx201410272009395522657a690389285100"; // 【自费预下单ID】 微信支付预支付交易会话标识。用于后续接口调用中使用,该值有效期为2小时。纯自费支付或医保自费混合支付时必填,纯医保支付时禁止填写。 +// request.passthroughRequestContent = "{\"payAuthNo\":\"AUTH****\",\"payOrdId\":\"ORD****\",\"setlLatlnt\":\"118.096435,24.485407\"}"; + request.attach = attach; + request.channelNo = channel; + request.medInsTestEnv = false; + + CreateOrder.OrderEntity response = client.run("/v3/med-ins/orders", request); + + log.info("【微信-医保】[下单] resp={}", JsonHelper.toJsonString(response)); +// if (!response.success()) { +// return new MedicalPayOrder().createResult(jsonResult); +// } +// return jsonResult.dataMapToBean(MedicalPayOrder.class); + + return null; + } catch (Exception e) { + ErrorHelper.println(e); + } + return null; + } + + + public WxMedOrder queryOrder(String accessToken, String appId, String mchId, String mdPayKey, String outTradeNo, String medTransId) { + JsonResult jsonResult = WxRequestHelper.postMdXml(("https://api.weixin.qq.com/payinsurance/queryorder?access_token=" + accessToken), params -> { + params.put("appid", appId); + params.put("mch_id", mchId); + // 二选一 + params.put("hosp_out_trade_no", outTradeNo); // 第三方服务商订单号 outTradeNo +// params.put("med_trans_id", medTransId); // 微信生成的医疗订单号 + params.put("nonce_str", UUID.randomUUID().toString().replaceAll("-", "")); + + params.put("sign", WxSignHelper.generateSign(params, WxSignHelper.SIGN_TYPE_MD5, mdPayKey)); + log.info("[医保][查询订单] req={}", JsonHelper.toJsonString(params)); + }); + if (!jsonResult.success()) { + return new WxMedOrder().createResult(jsonResult); + } + log.info("[医保][查询订单] req={}", JsonHelper.toJsonString(jsonResult)); + WxMedOrder order = jsonResult.dataMapToBean(WxMedOrder.class); + + order.setCashFee(order.getCashFee().movePointLeft(2)); + order.setTotalFee(order.getTotalFee().movePointLeft(2)); + + order.setInsuranceFee(order.getInsuranceFee().movePointLeft(2)); + order.setInsuranceSelfFee(order.getInsuranceSelfFee().movePointLeft(2)); + + order.setInsuranceFundFee(order.getInsuranceFundFee().movePointLeft(2)); + order.setInsuranceOtherFee(order.getInsuranceOtherFee().movePointLeft(2)); + return order; + } + + + /** + * [医保]退费(注意:需要先退医保部分,才能退现金部分) + * + * @param accessToken accessToken + * @param appId appId + * @param mchId mchId + * @param mdPayKey mdPayKey + * @param outTradeNo 订单号 + * @param outRefundNo 退费订单号 + * @param payOrdId 医保订单号|对应处方上传的出参单号 + * @param cashFee 退费现金 + * @param reason 退费原因 + */ + public WxMedOrder refund(String accessToken, String appId, String mchId, String mdPayKey, String outTradeNo, String outRefundNo, String payOrdId, BigDecimal cashFee, MdRefundTypeEnum mdRefundTypeEnum, String reason) { + if (reason == null) { + reason = "申请退款"; + } + + if (cashFee == null || cashFee.compareTo(BigDecimal.ZERO) == 0) { + return new MedicalRefund().createResult("退费金额错误或为0禁止退费"); + } + String finalReason = reason; + JsonResult jsonResult = WxRequestHelper.postMdXml(("https://api.weixin.qq.com/payinsurance/refund?access_token=" + accessToken), params -> { + params.put("appid", appId); + params.put("mch_id", mchId); + // 二选一 + params.put("hosp_out_trade_no", outTradeNo); // 第三方服务商订单号 outTradeNo + // params.put("med_trans_id", medTransId); // 微信生成的医疗订单号 + + params.put("hosp_out_refund_no", outRefundNo); // 退费订单号 + params.put("nonce_str", UUID.randomUUID().toString().replaceAll("-", "")); + + JSONObject requestContent = new JSONObject(); + requestContent.put("ref_reason", finalReason); + requestContent.put("payOrdId", payOrdId); + params.put("request_content", JSON.toJSONString(requestContent)); + + +// /** +// * 不填默认全退,填写金额时则指定金额不能为0;该参数只在part_refund_type为INS_INDIVIDUAL_PART生效 +// */ +// params.put("ins_refund_fee", new BigDecimal(refundFee).movePointRight(2)); // 个账部分退款 + + if (mdRefundTypeEnum != null) { + params.put("part_refund_type", mdRefundTypeEnum.CODE); // 只退现金部分 + } + + params.put("cash_refund_fee", cashFee.movePointRight(2).intValue()); // 现金退款 + + // 医保退款必须------------------------------------------------ + params.put("cancel_bill_no", outRefundNo); // 撤销单据号 + params.put("cancel_serial_no", outRefundNo); // 撤销流水号 + + params.put("sign", WxSignHelper.generateSign(params, WxSignHelper.SIGN_TYPE_MD5, mdPayKey)); + log.info("[医保][退费] req={}", JsonHelper.toJsonString(params)); + }); + + log.info("[医保][退费] resp={}", JsonHelper.toJsonString(jsonResult)); + if (!jsonResult.success()) { + return new MedicalRefund().createResult(jsonResult); + } + return jsonResult.dataMapToBean(WxMedOrder.class); + } + + + /** + * [医保]查询退费 + * + * @param accessToken accessToken + * @param appId appId + * @param mchId mchId + * @param mdPayKey mdPayKey + */ + public MedicalRefundInfo queryRefund(String accessToken, String appId, String mchId, String mdPayKey, String outRefundNo, String outTradeNo, String medTransId) { + JsonResult jsonResult = WxRequestHelper.postMdXml(("https://api.weixin.qq.com/payinsurance/queryrefund?access_token=" + accessToken), params -> { + params.put("appid", appId); + params.put("mch_id", mchId); + // 四选一 优先级(med_refund_id > hosp_out_refund_no > hosp_out_trade_no > med_trans_id) + if (!ObjectUtils.isEmpty(outRefundNo)) { + params.put("hosp_out_refund_no", outRefundNo); // 医院退款订单号 + } + if (!ObjectUtils.isEmpty(outTradeNo)) { + params.put("hosp_out_trade_no", outTradeNo); // 第三方服务商订单号 outTradeNo + } + if (!ObjectUtils.isEmpty(medTransId)) { + params.put("med_trans_id", medTransId); // 微信生成的医疗订单号 + } + params.put("nonce_str", UUID.randomUUID().toString().replaceAll("-", "")); + + params.put("sign", WxSignHelper.generateSign(params, WxSignHelper.SIGN_TYPE_MD5, mdPayKey)); + + log.info("[医保][查询退费] req={}", JsonHelper.toJsonString(params)); + }); + if (!jsonResult.success()) { + return new MedicalRefundInfo().createResult(jsonResult); + } + return jsonResult.dataMapToBean(MedicalRefundInfo.class); + } + + + /** + * [医保]下载对账账单 + * + * @param accessToken accessToken + * @param appId appId + * @param mchId mchId + * @param mdPayKey mdPayKey + * @param billDate 对账单日期(yyyyMMdd) + * @param billType 帐单类型 (ALL,返回当日所有订单信息;默认值 SUCCESS,返回当日成功支付的订单;REFUND,返回当日退款订单) + */ + public MedicalBill downBill(String accessToken, String appId, String mchId, String mdPayKey, String billDate, String billType) { + + JsonResult jsonResult = WxRequestHelper.postMdXml(("https://api.weixin.qq.com/payinsurance/billdownload?access_token=" + accessToken), params -> { + params.put("appid", appId); + params.put("mch_id", mchId); + // + params.put("bill_date", billDate); + params.put("bill_type", billType); + + params.put("nonce_str", UUID.randomUUID().toString().replaceAll("-", "")); + params.put("sign", WxSignHelper.generateSign(params, WxSignHelper.SIGN_TYPE_MD5, mdPayKey)); + log.info("[医保][账单下载] req={}", JsonHelper.toJsonString(params)); + }); + + if (!jsonResult.success()) { + return new MedicalRefund().createResult(jsonResult); + } + log.info("[医保][账单下载] resp={}", JsonHelper.toJsonString(jsonResult)); + return jsonResult.dataMapToBean(MedicalBill.class); + } + + + /** + * 获取用户授权码等信息(注意:联系腾讯修改白名单后,调用接口会报授权码错误的提示,需切换请求路径) + * + * @param reqUrl 请求地址 + * @param openid openid + * @param qrcode 用户授权码 + * @return 用户信息 + */ + public MedicalUserInfo getUserInfo(String reqUrl, String openid, String qrcode) { + log.info("【微信-医保】[授权]请求地址-{}", reqUrl); + MedicalUserInfo info = new MedicalUserInfo(); + String timestamp = Long.toString(System.currentTimeMillis()); + + String signature = WxMedConfig.createSignature(timestamp); + if (signature == null) { + info.setMessage("signature签名错误"); + return info; + } + JSONObject respJson = OkHttpHelper.postForm(reqUrl, params -> { + params.put("qrcode", qrcode); + params.put("openid", openid); + // 【微信-医保】测试报告内容[1] + log.info("【微信-医保】[授权] req={}", JsonHelper.toJsonString(params)); + }, headers -> { + headers.set("Accept", "application/json"); + headers.set("Content-Type", "application/json"); + // + headers.set("god-portal-timestamp", timestamp); + headers.set("god-portal-signature", signature); + headers.set("god-portal-request-id", UUID.randomUUID().toString().replaceAll("-", "")); + }); + if (respJson == null) { + info.setMessage("请求用户信息失败"); + return info; + } + String code = respJson.getString("code"); + String message = respJson.getString("message"); + log.info("respJson={}", JsonHelper.toJsonString(respJson)); + if (!"0".equals(code)) { + log.error("【微信-医保】[授权]失败 resp={}", respJson); + + info.setMessage(String.format("[%s] %s", code, message)); + return info; + } + + + JSONObject longitudeLatitude = respJson.getJSONObject("user_longitude_latitude"); + if (longitudeLatitude == null) { + info.setMessage(String.format("[%s] %s, tip=%s", code, message, "经纬度异常")); + return info; + } + String longitude = longitudeLatitude.getString("longitude"); + String latitude = longitudeLatitude.getString("latitude"); + + String userName = respJson.getString("user_name"); + String payAuthNo = respJson.getString("pay_auth_no"); + String cardNo = respJson.getString("user_card_no"); + if (cardNo != null && !"".equals(cardNo)) { + info.setCardNo(cardNo); + respJson.remove("user_card_no"); + } + log.error("【微信-医保】[授权]返回 resp={}", respJson); + + info.setUserName(userName); + info.setPayAuthNo(payAuthNo); + info.setLongitude(longitude); + info.setLatitude(latitude); + info.setSuccess(true); + return info; + } +} diff --git a/src/main/java/com/ynxbd/wx/wxfactory/medins/util/WXPayUtility.java b/src/main/java/com/ynxbd/wx/wxfactory/medins/util/WXPayUtility.java new file mode 100644 index 0000000..61a212f --- /dev/null +++ b/src/main/java/com/ynxbd/wx/wxfactory/medins/util/WXPayUtility.java @@ -0,0 +1,908 @@ +package com.ynxbd.wx.wxfactory.medins.util; + +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.util.List; +import java.util.Map.Entry; + +import okhttp3.Headers; +import okhttp3.Response; +import okio.BufferedSource; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.time.DateTimeException; +import java.time.Duration; +import java.time.Instant; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.security.MessageDigest; +import java.io.InputStream; + +import org.bouncycastle.crypto.digests.SM3Digest; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.security.Security; + +public class WXPayUtility { + private static final Gson gson = new GsonBuilder() + .disableHtmlEscaping() + .addSerializationExclusionStrategy(new ExclusionStrategy() { + @Override + public boolean shouldSkipField(FieldAttributes fieldAttributes) { + final Expose expose = fieldAttributes.getAnnotation(Expose.class); + return expose != null && !expose.serialize(); + } + + @Override + public boolean shouldSkipClass(Class aClass) { + return false; + } + }) + .addDeserializationExclusionStrategy(new ExclusionStrategy() { + @Override + public boolean shouldSkipField(FieldAttributes fieldAttributes) { + final Expose expose = fieldAttributes.getAnnotation(Expose.class); + return expose != null && !expose.deserialize(); + } + + @Override + public boolean shouldSkipClass(Class aClass) { + return false; + } + }) + .create(); + private static final char[] SYMBOLS = + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); + private static final SecureRandom random = new SecureRandom(); + + /** + * 将 Object 转换为 JSON 字符串 + */ + public static String toJson(Object object) { + return gson.toJson(object); + } + + /** + * 将 JSON 字符串解析为特定类型的实例 + */ + public static T fromJson(String json, Class classOfT) throws JsonSyntaxException { + return gson.fromJson(json, classOfT); + } + + /** + * 从公私钥文件路径中读取文件内容 + * + * @param keyPath 文件路径 + * @return 文件内容 + */ + private static String readKeyStringFromPath(String keyPath) { + try { + return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * 读取 PKCS#8 格式的私钥字符串并加载为私钥对象 + * + * @param keyString 私钥文件内容,以 -----BEGIN PRIVATE KEY----- 开头 + * @return PrivateKey 对象 + */ + public static PrivateKey loadPrivateKeyFromString(String keyString) { + try { + keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s+", ""); + return KeyFactory.getInstance("RSA").generatePrivate( + new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString))); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(e); + } catch (InvalidKeySpecException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * 从 PKCS#8 格式的私钥文件中加载私钥 + * + * @param keyPath 私钥文件路径 + * @return PrivateKey 对象 + */ + public static PrivateKey loadPrivateKeyFromPath(String keyPath) { + return loadPrivateKeyFromString(readKeyStringFromPath(keyPath)); + } + + /** + * 读取 PKCS#8 格式的公钥字符串并加载为公钥对象 + * + * @param keyString 公钥文件内容,以 -----BEGIN PUBLIC KEY----- 开头 + * @return PublicKey 对象 + */ + public static PublicKey loadPublicKeyFromString(String keyString) { + try { + keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "") + .replace("-----END PUBLIC KEY-----", "") + .replaceAll("\\s+", ""); + return KeyFactory.getInstance("RSA").generatePublic( + new X509EncodedKeySpec(Base64.getDecoder().decode(keyString))); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(e); + } catch (InvalidKeySpecException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * 从 PKCS#8 格式的公钥文件中加载公钥 + * + * @param keyPath 公钥文件路径 + * @return PublicKey 对象 + */ + public static PublicKey loadPublicKeyFromPath(String keyPath) { + return loadPublicKeyFromString(readKeyStringFromPath(keyPath)); + } + + /** + * 创建指定长度的随机字符串,字符集为[0-9a-zA-Z],可用于安全相关用途 + */ + public static String createNonce(int length) { + char[] buf = new char[length]; + for (int i = 0; i < length; ++i) { + buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)]; + } + return new String(buf); + } + + /** + * 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密 + * + * @param publicKey 加密用公钥对象 + * @param plaintext 待加密明文 + * @return 加密后密文 + */ + public static String encrypt(PublicKey publicKey, String plaintext) { + final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; + + try { + Cipher cipher = Cipher.getInstance(transformation); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8))); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalArgumentException("The current Java environment does not support " + transformation, e); + } catch (InvalidKeyException e) { + throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e); + } catch (BadPaddingException | IllegalBlockSizeException e) { + throw new IllegalArgumentException("Plaintext is too long", e); + } + } + + /** + * 使用私钥按照 RSA_PKCS1_OAEP_PADDING 算法进行解密 + * + * @param privateKey 解密用私钥对象 + * @param ciphertext 待解密密文(Base64编码的字符串) + * @return 解密后明文 + */ + public static String rsaOaepDecrypt(PrivateKey privateKey, String ciphertext) { + final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; + + try { + Cipher cipher = Cipher.getInstance(transformation); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(ciphertext)); + return new String(decryptedBytes, StandardCharsets.UTF_8); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalArgumentException("The current Java environment does not support " + transformation, e); + } catch (InvalidKeyException e) { + throw new IllegalArgumentException("RSA decryption using an illegal privateKey", e); + } catch (BadPaddingException | IllegalBlockSizeException e) { + throw new IllegalArgumentException("Ciphertext decryption failed", e); + } + } + + public static String aesAeadDecrypt(byte[] key, byte[] associatedData, byte[] nonce, + byte[] ciphertext) { + final String transformation = "AES/GCM/NoPadding"; + final String algorithm = "AES"; + final int tagLengthBit = 128; + + try { + Cipher cipher = Cipher.getInstance(transformation); + cipher.init( + Cipher.DECRYPT_MODE, + new SecretKeySpec(key, algorithm), + new GCMParameterSpec(tagLengthBit, nonce)); + if (associatedData != null) { + cipher.updateAAD(associatedData); + } + return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8); + } catch (InvalidKeyException + | InvalidAlgorithmParameterException + | BadPaddingException + | IllegalBlockSizeException + | NoSuchAlgorithmException + | NoSuchPaddingException e) { + throw new IllegalArgumentException(String.format("AesAeadDecrypt with %s Failed", + transformation), e); + } + } + + /** + * 使用私钥按照指定算法进行签名 + * + * @param message 待签名串 + * @param algorithm 签名算法,如 SHA256withRSA + * @param privateKey 签名用私钥对象 + * @return 签名结果 + */ + public static String sign(String message, String algorithm, PrivateKey privateKey) { + byte[] sign; + try { + Signature signature = Signature.getInstance(algorithm); + signature.initSign(privateKey); + signature.update(message.getBytes(StandardCharsets.UTF_8)); + sign = signature.sign(); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e); + } catch (InvalidKeyException e) { + throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e); + } catch (SignatureException e) { + throw new RuntimeException("An error occurred during the sign process.", e); + } + return Base64.getEncoder().encodeToString(sign); + } + + /** + * 使用公钥按照特定算法验证签名 + * + * @param message 待签名串 + * @param signature 待验证的签名内容 + * @param algorithm 签名算法,如:SHA256withRSA + * @param publicKey 验签用公钥对象 + * @return 签名验证是否通过 + */ + public static boolean verify(String message, String signature, String algorithm, + PublicKey publicKey) { + try { + Signature sign = Signature.getInstance(algorithm); + sign.initVerify(publicKey); + sign.update(message.getBytes(StandardCharsets.UTF_8)); + return sign.verify(Base64.getDecoder().decode(signature)); + } catch (SignatureException e) { + return false; + } catch (InvalidKeyException e) { + throw new IllegalArgumentException("verify uses an illegal publickey.", e); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e); + } + } + + /** + * 根据微信支付APIv3请求签名规则构造 Authorization 签名 + * + * @param mchid 商户号 + * @param certificateSerialNo 商户API证书序列号 + * @param privateKey 商户API证书私钥 + * @param method 请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE + * @param uri 请求接口的URL + * @param body 请求接口的Body + * @return 构造好的微信支付APIv3 Authorization 头 + */ + public static String buildAuthorization(String mchid, String certificateSerialNo, + PrivateKey privateKey, + String method, String uri, String body) { + String nonce = createNonce(32); + long timestamp = Instant.now().getEpochSecond(); + + String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce, + body == null ? "" : body); + + String signature = sign(message, "SHA256withRSA", privateKey); + + return String.format( + "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," + + "timestamp=\"%d\",serial_no=\"%s\"", + mchid, nonce, signature, timestamp, certificateSerialNo); + } + + /** + * 计算输入流的哈希值 + * + * @param inputStream 输入流 + * @param algorithm 哈希算法名称,如 "SHA-256", "SHA-1" + * @return 哈希值的十六进制字符串 + */ + private static String calculateHash(InputStream inputStream, String algorithm) { + try { + MessageDigest digest = MessageDigest.getInstance(algorithm); + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + digest.update(buffer, 0, bytesRead); + } + byte[] hashBytes = digest.digest(); + StringBuilder hexString = new StringBuilder(); + for (byte b : hashBytes) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(algorithm + " algorithm not available", e); + } catch (IOException e) { + throw new RuntimeException("Error reading from input stream", e); + } + } + + /** + * 计算输入流的 SHA256 哈希值 + * + * @param inputStream 输入流 + * @return SHA256 哈希值的十六进制字符串 + */ + public static String sha256(InputStream inputStream) { + return calculateHash(inputStream, "SHA-256"); + } + + /** + * 计算输入流的 SHA1 哈希值 + * + * @param inputStream 输入流 + * @return SHA1 哈希值的十六进制字符串 + */ + public static String sha1(InputStream inputStream) { + return calculateHash(inputStream, "SHA-1"); + } + + /** + * 计算输入流的 SM3 哈希值 + * + * @param inputStream 输入流 + * @return SM3 哈希值的十六进制字符串 + */ + public static String sm3(InputStream inputStream) { + // 确保Bouncy Castle Provider已注册 + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + + try { + SM3Digest digest = new SM3Digest(); + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + digest.update(buffer, 0, bytesRead); + } + byte[] hashBytes = new byte[digest.getDigestSize()]; + digest.doFinal(hashBytes, 0); + + StringBuilder hexString = new StringBuilder(); + for (byte b : hashBytes) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } catch (IOException e) { + throw new RuntimeException("Error reading from input stream", e); + } + } + + /** + * 对参数进行 URL 编码 + * + * @param content 参数内容 + * @return 编码后的内容 + */ + public static String urlEncode(String content) { + try { + return URLEncoder.encode(content, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + /** + * 对参数Map进行 URL 编码,生成 QueryString + * + * @param params Query参数Map + * @return QueryString + */ + public static String urlEncode(Map params) { + if (params == null || params.isEmpty()) { + return ""; + } + + StringBuilder result = new StringBuilder(); + for (Entry entry : params.entrySet()) { + if (entry.getValue() == null) { + continue; + } + + String key = entry.getKey(); + Object value = entry.getValue(); + if (value instanceof List) { + List list = (List) entry.getValue(); + for (Object temp : list) { + appendParam(result, key, temp); + } + } else { + appendParam(result, key, value); + } + } + return result.toString(); + } + + /** + * 将键值对 放入返回结果 + * + * @param result 返回的query string + * @param key 属性 + * @param value 属性值 + */ + private static void appendParam(StringBuilder result, String key, Object value) { + if (result.length() > 0) { + result.append("&"); + } + + String valueString; + // 如果是基本类型、字符串或枚举,直接转换;如果是对象,序列化为JSON + if (value instanceof String || value instanceof Number || + value instanceof Boolean || value instanceof Enum) { + valueString = value.toString(); + } else { + valueString = toJson(value); + } + + result.append(key) + .append("=") + .append(urlEncode(valueString)); + } + + /** + * 从应答中提取 Body + * + * @param response HTTP 请求应答对象 + * @return 应答中的Body内容,Body为空时返回空字符串 + */ + public static String extractBody(Response response) { + if (response.body() == null) { + return ""; + } + + try { + BufferedSource source = response.body().source(); + return source.readUtf8(); + } catch (IOException e) { + throw new RuntimeException(String.format("An error occurred during reading response body. " + + "Status: %d", response.code()), e); + } + } + + /** + * 根据微信支付APIv3应答验签规则对应答签名进行验证,验证不通过时抛出异常 + * + * @param wechatpayPublicKeyId 微信支付公钥ID + * @param wechatpayPublicKey 微信支付公钥对象 + * @param headers 微信支付应答 Header 列表 + * @param body 微信支付应答 Body + */ + public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey, + Headers headers, + String body) { + String timestamp = headers.get("Wechatpay-Timestamp"); + String requestId = headers.get("Request-ID"); + try { + Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp)); + // 拒绝过期请求 + if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) { + throw new IllegalArgumentException( + String.format("Validate response failed, timestamp[%s] is expired, request-id[%s]", + timestamp, requestId)); + } + } catch (DateTimeException | NumberFormatException e) { + throw new IllegalArgumentException( + String.format("Validate response failed, timestamp[%s] is invalid, request-id[%s]", + timestamp, requestId)); + } + String serialNumber = headers.get("Wechatpay-Serial"); + if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) { + throw new IllegalArgumentException( + String.format("Validate response failed, Invalid Wechatpay-Serial, Local: %s, Remote: " + + "%s", wechatpayPublicKeyId, serialNumber)); + } + + String signature = headers.get("Wechatpay-Signature"); + String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"), + body == null ? "" : body); + + boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey); + if (!success) { + throw new IllegalArgumentException( + String.format("Validate response failed,the WechatPay signature is incorrect.%n" + + "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]", + headers.get("Request-ID"), headers, body)); + } + } + + /** + * 根据微信支付APIv3通知验签规则对通知签名进行验证,验证不通过时抛出异常 + * + * @param wechatpayPublicKeyId 微信支付公钥ID + * @param wechatpayPublicKey 微信支付公钥对象 + * @param headers 微信支付通知 Header 列表 + * @param body 微信支付通知 Body + */ + public static void validateNotification(String wechatpayPublicKeyId, + PublicKey wechatpayPublicKey, Headers headers, + String body) { + String timestamp = headers.get("Wechatpay-Timestamp"); + try { + Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp)); + // 拒绝过期请求 + if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) { + throw new IllegalArgumentException( + String.format("Validate notification failed, timestamp[%s] is expired", timestamp)); + } + } catch (DateTimeException | NumberFormatException e) { + throw new IllegalArgumentException( + String.format("Validate notification failed, timestamp[%s] is invalid", timestamp)); + } + String serialNumber = headers.get("Wechatpay-Serial"); + if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) { + throw new IllegalArgumentException( + String.format("Validate notification failed, Invalid Wechatpay-Serial, Local: %s, " + + "Remote: %s", + wechatpayPublicKeyId, + serialNumber)); + } + + String signature = headers.get("Wechatpay-Signature"); + String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"), + body == null ? "" : body); + + boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey); + if (!success) { + throw new IllegalArgumentException( + String.format("Validate notification failed, WechatPay signature is incorrect.\n" + + "responseHeader[%s]\tresponseBody[%.1024s]", + headers, body)); + } + } + + /** + * 对微信支付通知进行签名验证、解析,同时将业务数据解密。验签名失败、解析失败、解密失败时抛出异常 + * + * @param apiv3Key 商户的 APIv3 Key + * @param wechatpayPublicKeyId 微信支付公钥ID + * @param wechatpayPublicKey 微信支付公钥对象 + * @param headers 微信支付请求 Header 列表 + * @param body 微信支付请求 Body + * @return 解析后的通知内容,解密后的业务数据可以使用 Notification.getPlaintext() 访问 + */ + public static Notification parseNotification(String apiv3Key, String wechatpayPublicKeyId, + PublicKey wechatpayPublicKey, Headers headers, + String body) { + validateNotification(wechatpayPublicKeyId, wechatpayPublicKey, headers, body); + Notification notification = gson.fromJson(body, Notification.class); + notification.decrypt(apiv3Key); + return notification; + } + + /** + * 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常 + */ + public static class ApiException extends RuntimeException { + private static final long serialVersionUID = 2261086748874802175L; + + private final int statusCode; + private final String body; + private final Headers headers; + private final String errorCode; + private final String errorMessage; + + public ApiException(int statusCode, String body, Headers headers) { + super(String.format("微信支付API访问失败,StatusCode: [%s], Body: [%s], Headers: [%s]", statusCode, + body, headers)); + this.statusCode = statusCode; + this.body = body; + this.headers = headers; + + if (body != null && !body.isEmpty()) { + JsonElement code; + JsonElement message; + + try { + JsonObject jsonObject = gson.fromJson(body, JsonObject.class); + code = jsonObject.get("code"); + message = jsonObject.get("message"); + } catch (JsonSyntaxException ignored) { + code = null; + message = null; + } + this.errorCode = code == null ? null : code.getAsString(); + this.errorMessage = message == null ? null : message.getAsString(); + } else { + this.errorCode = null; + this.errorMessage = null; + } + } + + /** + * 获取 HTTP 应答状态码 + */ + public int getStatusCode() { + return statusCode; + } + + /** + * 获取 HTTP 应答包体内容 + */ + public String getBody() { + return body; + } + + /** + * 获取 HTTP 应答 Header + */ + public Headers getHeaders() { + return headers; + } + + /** + * 获取 错误码 (错误应答中的 code 字段) + */ + public String getErrorCode() { + return errorCode; + } + + /** + * 获取 错误消息 (错误应答中的 message 字段) + */ + public String getErrorMessage() { + return errorMessage; + } + } + + public static class Notification { + @SerializedName("id") + private String id; + @SerializedName("create_time") + private String createTime; + @SerializedName("event_type") + private String eventType; + @SerializedName("resource_type") + private String resourceType; + @SerializedName("summary") + private String summary; + @SerializedName("resource") + private Resource resource; + private String plaintext; + + public String getId() { + return id; + } + + public String getCreateTime() { + return createTime; + } + + public String getEventType() { + return eventType; + } + + public String getResourceType() { + return resourceType; + } + + public String getSummary() { + return summary; + } + + public Resource getResource() { + return resource; + } + + /** + * 获取解密后的业务数据(JSON字符串,需要自行解析) + */ + public String getPlaintext() { + return plaintext; + } + + private void validate() { + if (resource == null) { + throw new IllegalArgumentException("Missing required field `resource` in notification"); + } + resource.validate(); + } + + /** + * 使用 APIv3Key 对通知中的业务数据解密,解密结果可以通过 getPlainText 访问。 + * 外部拿到的 Notification 一定是解密过的,因此本方法没有设置为 public + * + * @param apiv3Key 商户APIv3 Key + */ + private void decrypt(String apiv3Key) { + validate(); + + plaintext = aesAeadDecrypt( + apiv3Key.getBytes(StandardCharsets.UTF_8), + resource.associatedData.getBytes(StandardCharsets.UTF_8), + resource.nonce.getBytes(StandardCharsets.UTF_8), + Base64.getDecoder().decode(resource.ciphertext) + ); + } + + public static class Resource { + @SerializedName("algorithm") + private String algorithm; + + @SerializedName("ciphertext") + private String ciphertext; + + @SerializedName("associated_data") + private String associatedData; + + @SerializedName("nonce") + private String nonce; + + @SerializedName("original_type") + private String originalType; + + public String getAlgorithm() { + return algorithm; + } + + public String getCiphertext() { + return ciphertext; + } + + public String getAssociatedData() { + return associatedData; + } + + public String getNonce() { + return nonce; + } + + public String getOriginalType() { + return originalType; + } + + private void validate() { + if (algorithm == null || algorithm.isEmpty()) { + throw new IllegalArgumentException("Missing required field `algorithm` in Notification" + + ".Resource"); + } + if (!Objects.equals(algorithm, "AEAD_AES_256_GCM")) { + throw new IllegalArgumentException(String.format("Unsupported `algorithm`[%s] in " + + "Notification.Resource", algorithm)); + } + + if (ciphertext == null || ciphertext.isEmpty()) { + throw new IllegalArgumentException("Missing required field `ciphertext` in Notification" + + ".Resource"); + } + + if (associatedData == null || associatedData.isEmpty()) { + throw new IllegalArgumentException("Missing required field `associatedData` in " + + "Notification.Resource"); + } + + if (nonce == null || nonce.isEmpty()) { + throw new IllegalArgumentException("Missing required field `nonce` in Notification" + + ".Resource"); + } + + if (originalType == null || originalType.isEmpty()) { + throw new IllegalArgumentException("Missing required field `originalType` in " + + "Notification.Resource"); + } + } + } + } + + /** + * 根据文件名获取对应的Content-Type + * + * @param fileName 文件名 + * @return Content-Type字符串 + */ + public static String getContentTypeByFileName(String fileName) { + if (fileName == null || fileName.isEmpty()) { + return "application/octet-stream"; + } + + // 获取文件扩展名 + String extension = ""; + int lastDotIndex = fileName.lastIndexOf('.'); + if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) { + extension = fileName.substring(lastDotIndex + 1).toLowerCase(); + } + + // 常见文件类型映射 + Map contentTypeMap = new HashMap<>(); + // 图片类型 + contentTypeMap.put("png", "image/png"); + contentTypeMap.put("jpg", "image/jpeg"); + contentTypeMap.put("jpeg", "image/jpeg"); + contentTypeMap.put("gif", "image/gif"); + contentTypeMap.put("bmp", "image/bmp"); + contentTypeMap.put("webp", "image/webp"); + contentTypeMap.put("svg", "image/svg+xml"); + contentTypeMap.put("ico", "image/x-icon"); + + // 文档类型 + contentTypeMap.put("pdf", "application/pdf"); + contentTypeMap.put("doc", "application/msword"); + contentTypeMap.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); + contentTypeMap.put("xls", "application/vnd.ms-excel"); + contentTypeMap.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + contentTypeMap.put("ppt", "application/vnd.ms-powerpoint"); + contentTypeMap.put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"); + + // 文本类型 + contentTypeMap.put("txt", "text/plain"); + contentTypeMap.put("html", "text/html"); + contentTypeMap.put("css", "text/css"); + contentTypeMap.put("js", "application/javascript"); + contentTypeMap.put("json", "application/json"); + contentTypeMap.put("xml", "application/xml"); + contentTypeMap.put("csv", "text/csv"); + + // 音视频类型 + contentTypeMap.put("mp3", "audio/mpeg"); + contentTypeMap.put("wav", "audio/wav"); + contentTypeMap.put("mp4", "video/mp4"); + contentTypeMap.put("avi", "video/x-msvideo"); + contentTypeMap.put("mov", "video/quicktime"); + + // 压缩文件类型 + contentTypeMap.put("zip", "application/zip"); + contentTypeMap.put("rar", "application/x-rar-compressed"); + contentTypeMap.put("7z", "application/x-7z-compressed"); + + return contentTypeMap.getOrDefault(extension, "application/octet-stream"); + } +} \ No newline at end of file diff --git a/src/main/java/com/ynxbd/wx/wxfactory/payment/WXPayUtility.java b/src/main/java/com/ynxbd/wx/wxfactory/payment/WXPayUtility.java index 0bb47fd..37b6a21 100644 --- a/src/main/java/com/ynxbd/wx/wxfactory/payment/WXPayUtility.java +++ b/src/main/java/com/ynxbd/wx/wxfactory/payment/WXPayUtility.java @@ -52,6 +52,7 @@ import java.util.Objects; import java.security.MessageDigest; import java.io.InputStream; +import org.apache.commons.lang3.ObjectUtils; import org.bouncycastle.crypto.digests.SM3Digest; import org.bouncycastle.jce.provider.BouncyCastleProvider; @@ -846,6 +847,8 @@ public class WXPayUtility { } } + + /** * 根据文件名获取对应的Content-Type *