diff --git a/src/main/java/com/ynxbd/common/action/pay/JYPayAction.java b/src/main/java/com/ynxbd/common/action/pay/JYPayAction.java index e72e188..7b80e85 100644 --- a/src/main/java/com/ynxbd/common/action/pay/JYPayAction.java +++ b/src/main/java/com/ynxbd/common/action/pay/JYPayAction.java @@ -1,52 +1,135 @@ package com.ynxbd.common.action.pay; -import com.ynxbd.bcm.config.BCMHelper; +import com.alibaba.fastjson.JSONObject; import com.ynxbd.common.action.base.BaseAction; -import com.ynxbd.common.bean.pay.PayResult; -import com.ynxbd.common.helper.common.DateHelper; +import com.ynxbd.common.bean.enums.MerchantEnum; +import com.ynxbd.common.bean.pay.ReqOrder; +import com.ynxbd.common.helper.common.HttpHelper; +import com.ynxbd.common.helper.common.JsonHelper; import com.ynxbd.common.result.Result; +import com.ynxbd.common.result.ResultEnum; +import com.ynxbd.jy.bean.JYOrder; +import com.ynxbd.jy.bean.JYPayNotify; +import com.ynxbd.jy.bean.JYPayRSAUtil; +import com.ynxbd.jy.bean.enums.JYBusinessTypeEnum; +import com.ynxbd.jy.bean.enums.JYOrderTypeEnum; +import com.ynxbd.jy.helper.JYPayConfig; +import com.ynxbd.jy.helper.JYPayHelper; +import com.ynxbd.wx.wxfactory.utils.WxRespHelper; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; import org.apache.struts2.convention.annotation.Action; import org.apache.struts2.convention.annotation.Namespace; import java.math.BigDecimal; +import java.util.Map; @Slf4j @Namespace("/jy_pay") public class JYPayAction extends BaseAction { /** - * 【交行】处方支付通知 - * - * @return 是否成功 + * [京颐支付]JSAPI预下单 */ - @Action("notify") - public Result notify(String notifyType) { -// if (ObjectUtils.isEmpty(notifyType)) { -// Result.respJson(BCMHelper.respError(String.format("【京颐】获取回调参数失败,结束请求 notifyType={%s}", notifyType))); -// return null; + @Action("jsapi_prepay") + public Result jsapi_prepay(String payOpenId, String openid, String patientId, BigDecimal totalFee) { + String merOrderNum = MerchantEnum.JY.createOutTradeNo(); + log.info("[京颐支付]测试 outTradeNo={}, totalFee={}", merOrderNum, totalFee); + if (totalFee == null || ObjectUtils.isEmpty(openid) || ObjectUtils.isEmpty(patientId)) { + return Result.error(ResultEnum.PARAM_IS_DEFECT); + } + + String ip = HttpHelper.getIpAddress(request); + String title = "支付测试"; + + ReqOrder req = new ReqOrder(); + req.setPatientName("小王"); + req.setPatientGender("男"); + req.setPatientAge("18"); + req.setPatientTel("15559603353"); + req.setTreatNum("T001"); + req.setPatientIdCardNo("532331199910053810"); + req.setPatientId("1000001"); + + JYOrder jyOrder = new JYOrder(); + jyOrder.orderTypeEnum = JYOrderTypeEnum._001; // 挂号费 + jyOrder.businessTypeEnum = JYBusinessTypeEnum.B102; + + req.setJyOrder(jyOrder); + JSONObject prepay = JYPayHelper.createOrder(MerchantEnum.JY, title, merOrderNum, totalFee.toString(), PEnum.REG.CODE, ip, payOpenId, openid, req); + if (prepay == null) { + return Result.error(ResultEnum.PAY_ERROR_ORDER); // 下单异常 + } + return Result.success(prepay); + } + +// /** +// * [京颐支付]JSAPI预下单 +// */ +// @Action("refund_test") +// public Result refund_test(String outRefundNo, BigDecimal refundFee, String outTradeNo, String bankMerchantNo, String tradeNo) { +// +// log.info("[京颐支付]退费测试 outRefundNo={}, refundFee={}, outTradeNo={}, bankMerchantNo={}, tradeNo={}", +// outRefundNo, refundFee, outTradeNo, bankMerchantNo, tradeNo); +// +// ReqOrder req = new ReqOrder(); +// req.setPatientName("小王"); +// req.setPatientGender("男"); +// req.setPatientAge("18"); +// req.setPatientTel("15559603353"); +// req.setTreatNum("T001"); +// req.setPatientIdCardNo("532331199910053810"); +// req.setPatientId("1000001"); +// +// JSONObject prepay = JYPayHelper.refundPayOrder(JYBusinessTypeEnum.B102, req, outRefundNo, refundFee, outTradeNo, bankMerchantNo, tradeNo); +// if (prepay == null) { +// return Result.error(); // 下单异常 // } +// return Result.success(prepay); +// } + + /** + * [京颐支付]订单查询 + */ + @Action("query_order") + public Result query_order(String outTradeNo, String merOrderNum, String tradeNo) { + log.info("[京颐支付]订单查询 outTradeNo={}, merOrderNum={}, tradeNo={}", + outTradeNo, merOrderNum, tradeNo); + + JSONObject prepay = JYPayHelper.queryPayOrderInfo(outTradeNo, merOrderNum, tradeNo); + if (prepay == null) { + return Result.error(ResultEnum.DATA_NOT_FOUND); + } + return Result.success(prepay); + } + + + /** + * 【京颐】支付|退款通知 + */ + @Action("notify") + public Result notify(String notifyType) { try { - PayResult notifyInfo = BCMHelper.payNotify(request); - Result.respJson(BCMHelper.respOk()); // 成功 + PEnum pEnum = PEnum.toEnum(notifyType); + if (pEnum == null) { + return Result.respXml(WxRespHelper.respOk()); + } - String openid = notifyInfo.getOpenid(); - String bankTransNo = notifyInfo.getTransId(); - String outTradeNo = notifyInfo.getOutTradeNo(); - BigDecimal totalFee = notifyInfo.getTotalFee(); + Map params = reqParamsToMap(); + String sign = JYPayRSAUtil.createSign(params, JYPayConfig.PRIVATE_KEY); - String payInfo = notifyInfo.getInfo(); - String payDate = DateHelper.getCurDate(); - String payTime = DateHelper.getCurTime(); - String bankMerchantNo = notifyInfo.getBankMerchantNo(); - log.info("【京颐】[{}] 收到通知 outTradeNo={}, bankTransNo={}, bankMerchantNo={}", notifyType, outTradeNo, bankTransNo, bankMerchantNo); + log.info("[sign]{}", sign); + JYPayNotify notifyInfo = reqParamsToBean(JYPayNotify.class); - switch (PEnum.toEnum(notifyType)) { + log.info("【京颐】[{}]收到[{}]通知-{}", pEnum.NAME, (notifyInfo.isRefundOrder() ? "退款" : "支付"), JsonHelper.toJsonString(notifyInfo)); + Result.respXml(WxRespHelper.respOk()); // 收到请求-解析成功 + + switch (pEnum) { case RECIPE: // openid = new RecipeService().queryOpenIdByOutTradeNo(outTradeNo); // new RecipeService().recipeListPay(MerchantEnum.JY, totalFee.toString(), openid, outTradeNo, bankTransNo, bankMerchantNo, payDate, payTime, payInfo); -// + break; case REG: // 挂号 diff --git a/src/main/java/com/ynxbd/common/action/pay/PayAction.java b/src/main/java/com/ynxbd/common/action/pay/PayAction.java index 54ff054..c280314 100644 --- a/src/main/java/com/ynxbd/common/action/pay/PayAction.java +++ b/src/main/java/com/ynxbd/common/action/pay/PayAction.java @@ -6,7 +6,10 @@ import com.ynxbd.common.bean.ConfigSelfHelp; import com.ynxbd.common.bean.RecipeHelp; import com.ynxbd.common.bean.SelfHelp; import com.ynxbd.common.bean.enums.MerchantEnum; -import com.ynxbd.common.bean.pay.*; +import com.ynxbd.common.bean.pay.Order; +import com.ynxbd.common.bean.pay.PayInHosp; +import com.ynxbd.common.bean.pay.Recipe; +import com.ynxbd.common.bean.pay.Register; import com.ynxbd.common.config.interceptor.AesDecode; import com.ynxbd.common.dao.SelfHelpDao; import com.ynxbd.common.dao.his.HisRecipeDao; @@ -22,10 +25,6 @@ import com.ynxbd.common.result.ResultEnum; import com.ynxbd.common.result.ServiceException; import com.ynxbd.common.service.*; import com.ynxbd.common.service.params.RequestParams; -import com.ynxbd.jy.bean.JYOrder; -import com.ynxbd.jy.bean.enums.JYBusinessTypeEnum; -import com.ynxbd.jy.bean.enums.JYOrderTypeEnum; -import com.ynxbd.jy.helper.JYPayHelper; import com.ynxbd.wx.config.WeChatConfig; import com.ynxbd.wx.wxfactory.AesWxHelper; import com.ynxbd.wx.wxfactory.WxPayHelper; @@ -139,8 +138,7 @@ public class PayAction extends BaseAction { String body = "诊间支付 患者:" + patientId + "\n门诊号:" + treatNum; JSONObject respJson = PayService.createOrder(merchantEnum, payOpenId, openid, patientId, totalFee, outTradeNo, PEnum.RECIPE.CODE, ip, body); - // 支付异常 - if (respJson == null) { + if (respJson == null) { // 预下单异常 return Result.error(ResultEnum.PAY_ERROR); } return Result.success(respJson); @@ -1040,39 +1038,5 @@ public class PayAction extends BaseAction { // } - /** - * [京颐支付]测试 - */ - @Action("jyPay") - public Result jyPay(String payOpenId, String openid, String patientId, BigDecimal totalFee) { - String outTradeNo = MerchantEnum.JY.createOutTradeNo(); - log.info("[京颐支付]测试 outTradeNo={}, totalFee={}", outTradeNo, totalFee); - if (totalFee == null || ObjectUtils.isEmpty(openid) || ObjectUtils.isEmpty(patientId)) { - return Result.error(ResultEnum.PARAM_IS_DEFECT); - } - String ip = HttpHelper.getIpAddress(request); - String title = "支付测试"; - - ReqOrder req = new ReqOrder(); - req.setPatientName("小王"); - req.setPatientGender("男"); - req.setPatientAge("18"); - req.setPatientTel("15559603353"); - req.setTreatNum("T001"); - req.setPatientIdCardNo("532331199910053810"); - req.setPatientId("1000001"); - - JYOrder jyOrder = new JYOrder(); - jyOrder.orderTypeEnum = JYOrderTypeEnum._001; // 挂号费 - jyOrder.businessTypeEnum = JYBusinessTypeEnum.B102; - - req.setJyOrder(jyOrder); - JSONObject order = JYPayHelper.createOrder(MerchantEnum.JY, title, outTradeNo, totalFee.toString(), PEnum.REG.CODE, ip, payOpenId, openid, req); -// if (respJson == null) { -// return Result.error(ResultEnum.PAY_ERROR); // 支付异常 -// } -// respJson.put("outTradeNo", outTradeNo); - return Result.success(order); - } } diff --git a/src/main/java/com/ynxbd/common/action/xk/XKAction.java b/src/main/java/com/ynxbd/common/action/xk/XKAction.java index cc8341c..ba6e922 100644 --- a/src/main/java/com/ynxbd/common/action/xk/XKAction.java +++ b/src/main/java/com/ynxbd/common/action/xk/XKAction.java @@ -3,7 +3,9 @@ package com.ynxbd.common.action.xk; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.ynxbd.common.action.base.BaseAction; +import com.ynxbd.common.bean.xk.HisRxItem; import com.ynxbd.common.dao.his.HisXKDao; +import com.ynxbd.common.helper.common.JsonHelper; import com.ynxbd.common.result.JsonResult; import com.ynxbd.common.result.Result; import com.ynxbd.common.result.ResultEnum; @@ -49,19 +51,19 @@ public class XKAction extends BaseAction { String ybAttrib = body.getString("ybAttrib"); String bzCode = body.getString("bzCode"); String bzName = body.getString("bzName"); - JSONArray rxJsonArr = body.getJSONArray("rxJsonArr"); + String rxJson = body.getString("rxJsonArr"); log.info("[开方]doctCode={}, treatNum={}, rxType={}", doctCode, treatNum, rxType); if (ObjectUtils.isEmpty(doctCode) || ObjectUtils.isEmpty(treatNum) || ObjectUtils.isEmpty(rxType)) { return Result.error(ResultEnum.PARAM_IS_DEFECT); } - System.out.println(rxJsonArr); - if (rxJsonArr.isEmpty()) { + List hisRxItems = JsonHelper.parseArray(rxJson, HisRxItem.class); + if (hisRxItems.isEmpty()) { return Result.error(ResultEnum.PARAM_IS_DEFECT); } - JsonResult resp = new HisXKDao().createHisRx(doctCode, treatNum, rxType, diagnose, weight, note, ybAttrib, bzCode, bzName, rxJsonArr); + JsonResult resp = new HisXKDao().createHisRx(doctCode, treatNum, rxType, diagnose, weight, note, ybAttrib, bzCode, bzName, hisRxItems); if (!resp.success()) { return Result.error(resp.getMessage()); } diff --git a/src/main/java/com/ynxbd/common/bean/xk/HisRxItem.java b/src/main/java/com/ynxbd/common/bean/xk/HisRxItem.java new file mode 100644 index 0000000..847ada0 --- /dev/null +++ b/src/main/java/com/ynxbd/common/bean/xk/HisRxItem.java @@ -0,0 +1,82 @@ +package com.ynxbd.common.bean.xk; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; + +@Setter +@Getter +@ToString +@NoArgsConstructor +public class HisRxItem implements Serializable { + private static final long serialVersionUID = 2026082712724000001L; + + // 项目编码 + @JSONField(ordinal = 1) + private String xmCode; + // 项目名称 + @JSONField(ordinal = 2) + private String xmName; + // 科目编码 + @JSONField(ordinal = 3) + private String kmCode; + // 科目名称 + @JSONField(ordinal = 4) + private String kmName; + // 项目单位 + @JSONField(ordinal = 5) + private String xmUnit; + // 项目单价 + @JSONField(ordinal = 6) + private String xmPrice; + // 项目数量 + @JSONField(ordinal = 7) + private String xmCount; + // 项目金额 + @JSONField(ordinal = 8) + private String xmMoney; + // 项目标识 + @JSONField(ordinal = 9) + private Integer xmFlag; + // 药品剂量 + @JSONField(ordinal = 10) + private String ypJl; + // 药品剂量单位 + @JSONField(ordinal = 11) + private String ypJlUnit; + // 用法频次 qd/bid等 + @JSONField(ordinal = 12) + private String yzUsedMethod; + // 每日次数 + @JSONField(ordinal = 13) + private String yzCountForDay; + // 每周次数 + @JSONField(ordinal = 14) + private String yzCountForWeek; + // 用药途径:口服 + @JSONField(ordinal = 15) + private String ypPath; + // 药房编码 + @JSONField(ordinal = 16) + private String yfCode; + // 药房名称 + @JSONField(ordinal = 17) + private String yfName; + // 医保类别编码 + @JSONField(ordinal = 18) + private String yblb; + // 医保类别名称 + @JSONField(ordinal = 19) + private String yblbName; + // 总数量 + @JSONField(ordinal = 20) + private String totCount; + // 备注 + @JSONField(ordinal = 21) + private String note; + +} diff --git a/src/main/java/com/ynxbd/common/dao/RegisterDao.java b/src/main/java/com/ynxbd/common/dao/RegisterDao.java index c5c7cd4..8ad237a 100644 --- a/src/main/java/com/ynxbd/common/dao/RegisterDao.java +++ b/src/main/java/com/ynxbd/common/dao/RegisterDao.java @@ -291,7 +291,7 @@ public class RegisterDao { if (reg.getPayMoney() == null) { reg.setPayMoney(reg.getTotalFee()); } - String sql = "insert into register(updateTime, hisStatus, payStatus, openid, patientId, idCardNo, outTradeNo, bankTransNo, tradeNo, regFee, clinicFee, totalFee, payMoney, address, deptCode, deptName, doctCode, doctName, payDeviceId, clinicCode, regType, regDate, regCode, tid, tName, begTime, endTime, treatNum, queueNum, sourceId, tel, payWay, authCode, invoiceTransNo, hisTransNo, operateUser, HospitalArea, subDeptCode, payMchCode, payOpenId)" + String sql = "insert into register(updateTime, hisStatus, payStatus, openid, patientId, idCardNo, outTradeNo, bankTransNo, tradeNo, regFee, clinicFee, totalFee, payMoney, address, deptCode, deptName, doctCode, doctName, payDeviceId, clinicCode, regType, regDate, regCode, tid, tName, begTime, endTime, treatNum, queueNum, sourceId, tel, payWay, authCode, invoiceTransNo, hisTransNo, operateUser, hospitalArea, subDeptCode, payMchCode, payOpenId)" + " values (now(),?,?,?,?,?,?,?,?,?,?, ?,?,?,?,?,?,?,?,?,?, ?,?,?,?,?,?,?,?,?,?, ?,?,?,?,?,?,?,?,?)"; return DataBase.insert(sql, ps -> { ps.setInt(1, reg.getHisStatus()); diff --git a/src/main/java/com/ynxbd/common/dao/his/HisXKDao.java b/src/main/java/com/ynxbd/common/dao/his/HisXKDao.java index d25d4e4..73a3d71 100644 --- a/src/main/java/com/ynxbd/common/dao/his/HisXKDao.java +++ b/src/main/java/com/ynxbd/common/dao/his/HisXKDao.java @@ -2,10 +2,7 @@ package com.ynxbd.common.dao.his; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; -import com.ynxbd.common.bean.xk.HisRxAdmRoute; -import com.ynxbd.common.bean.xk.HisRxDrug; -import com.ynxbd.common.bean.xk.HisRxPharmDict; -import com.ynxbd.common.bean.xk.HisRxUseCount; +import com.ynxbd.common.bean.xk.*; import com.ynxbd.common.helper.common.JsonHelper; import com.ynxbd.common.his.HisEnum; import com.ynxbd.common.his.HisFactory; @@ -38,21 +35,7 @@ public class HisXKDao { return new ArrayList<>(); } - public JsonResult createHisRx(String doctCode, String treatNum, String rxType, String diagnose, String weight, String note, String ybAttrib, String bzCode, String bzName, JSONArray rxJsonArr) { - for (int i = 0; i < rxJsonArr.size(); i++) { - JSONObject item = rxJsonArr.getJSONObject(i); - if (item == null) { - continue; - } - String yblb = item.getString("YBLB"); - if (ObjectUtils.isEmpty(yblb)) { - item.put("YBLB", ""); - } - String yblbName = item.getString("YBLBNAME"); - if (ObjectUtils.isEmpty(yblbName)) { - item.put("YBLBNAME", ""); - } - } + public JsonResult createHisRx(String doctCode, String treatNum, String rxType, String diagnose, String weight, String note, String ybAttrib, String bzCode, String bzName, List hisRxItems) { JsonResult jsonResult = HisFactory.His().getJsonResult(HisEnum.HIS_XK_RX_CREATE, params -> { params.put("YSCode", doctCode); // 开单医生编码 params.put("MZNum", treatNum); // 门诊号 @@ -63,7 +46,7 @@ public class HisXKDao { params.put("YBAttrib", ybAttrib); // 医保类别 params.put("BZCode", bzCode); // 病种编码 params.put("BZName", bzName); // 病种名称 - params.put("Jarray", JsonHelper.toJsonString(rxJsonArr)); + params.put("Jarray", JsonHelper.toJsonStringEmpty(hisRxItems)); }); log.info("[开方]返回 resp={}", JsonHelper.toJsonString(jsonResult.getDataMap())); return jsonResult; diff --git a/src/main/java/com/ynxbd/common/helper/common/JsonHelper.java b/src/main/java/com/ynxbd/common/helper/common/JsonHelper.java index 04a76dd..eba5c3c 100644 --- a/src/main/java/com/ynxbd/common/helper/common/JsonHelper.java +++ b/src/main/java/com/ynxbd/common/helper/common/JsonHelper.java @@ -7,6 +7,7 @@ import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.TypeReference; import com.alibaba.fastjson.serializer.PascalNameFilter; import com.alibaba.fastjson.serializer.SerializerFeature; +import com.alibaba.fastjson.serializer.ValueFilter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; @@ -29,6 +30,22 @@ public class JsonHelper { return JSON.toJSONString(data); } + /** + * 对象转JSON[空值转空字符串] + * + * @param data data + * @return json + */ + public static String toJsonStringEmpty(Object data) { + ValueFilter nullToEmptyFilter = (object, name, value) -> { + if (value == null) { + return ""; + } + return value; + }; + return JSON.toJSONString(data, nullToEmptyFilter, SerializerFeature.WriteNonStringKeyAsString, SerializerFeature.WriteMapNullValue); + } + /** * 支持返回大写json * diff --git a/src/main/java/com/ynxbd/common/service/PayService.java b/src/main/java/com/ynxbd/common/service/PayService.java index 5bc1f92..e1cb187 100644 --- a/src/main/java/com/ynxbd/common/service/PayService.java +++ b/src/main/java/com/ynxbd/common/service/PayService.java @@ -3,7 +3,6 @@ package com.ynxbd.common.service; import com.alibaba.fastjson.JSONObject; import com.ynxbd.ali.helper.AliHelper; import com.ynxbd.bcm.config.BCMHelper; -import com.ynxbd.common.action.pay.JYPayAction; import com.ynxbd.common.action.pay.PEnum; import com.ynxbd.common.bean.enums.MerchantEnum; import com.ynxbd.common.bean.pay.*; @@ -558,7 +557,6 @@ public class PayService { } else if (MerchantEnum.JY.equals(merchantEnum)) { // 京颐 return JYPayHelper.createOrder(merchantEnum, title, outTradeNo, totalFee, notifyType, ip, payOpenId, msgOpenId, new ReqOrder()); - } return null; } diff --git a/src/main/java/com/ynxbd/jy/bean/JYOrder.java b/src/main/java/com/ynxbd/jy/bean/JYOrder.java index 715a4d8..abf3824 100644 --- a/src/main/java/com/ynxbd/jy/bean/JYOrder.java +++ b/src/main/java/com/ynxbd/jy/bean/JYOrder.java @@ -1,7 +1,6 @@ package com.ynxbd.jy.bean; -import com.ynxbd.common.bean.pay.Order; import com.ynxbd.common.bean.pay.ReqOrder; import com.ynxbd.jy.bean.enums.JYBusinessTypeEnum; import com.ynxbd.jy.bean.enums.JYOrderTypeDetailEnum; diff --git a/src/main/java/com/ynxbd/jy/bean/JYPayNotify.java b/src/main/java/com/ynxbd/jy/bean/JYPayNotify.java new file mode 100644 index 0000000..15a940d --- /dev/null +++ b/src/main/java/com/ynxbd/jy/bean/JYPayNotify.java @@ -0,0 +1,71 @@ +package com.ynxbd.jy.bean; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; + +@Slf4j +@Getter +@Setter +@ToString +@NoArgsConstructor +public class JYPayNotify { + // 交易代码 M 支付:001; + private String tradeCode; + // 应用编号 M 第三方应用接入唯一编号,由后台设置 + private String applicationId; + // 应用商户编号 M 应用商户唯一编号,请按照商户规则使用正确的商户号 + private String merchantId; + // 返回码 M 0000为成功,其他为失败 + private String respCode; + // 商户订单号 M 由商户端(应用方、接入方)生成的订单号,如:00000100005115 (不大于64位长度),不许有重复。 + private String merOrderNum; + // 支付平台订单号 M 如:00000100005115 + private String orderId; + // 支付平台流水号 M 如:00000100005115 + private String orderTransId; + // 支付网关订单号 F 第三方支付平台(微信/支付宝/银行等)记录的商户订单号 + private String threeTradeNo; + // 支付网关流水号 F 第三方支付平台(微信/支付宝/银行等)记录的商户流水号 + private String threeTransNo; + // 银行流水号 F 第三方支付平台(微信/支付宝/银行等)的原始流水号 + private String finalTransNo; + // 订单金额 M 精确到分,例如100 + private Long orderAmt; + // 交易时间 M YYYYMMDDHHMMSS(商户订单提交时间) | 退款交易时间 + private String tranDateTime; + // 支付时间 M YYYYMMDDHHMMSS(订单支付时间) + private String paymentTime; + // 订单类型 M 如:挂号费、医疗费、药品费,详见字典#_数据字典 + private String orderType; + // 订单状态 M 02:已支付04:已冲正06:已退款,详见字典#订单状态 + private String orderStatus; + // 微信公众号用户标识 F 微信公众号用户标识,只有微信公众平台支付方式会返回,比如微信扫码支付 + private String openId; + // 商户ID F 应用方对账可能需要 + private String appId; + // 商户号 F 应用方对账可能需要 + private String merchantNo; + // 数字签名 M 本平台生成的签名,供接入方验证签名 生成签名规则参考#_加签验签规则说明 + private String signValue; + + // 退款==================================================== + // 商户退款流水号,由应用端(应用方、接入方)生成,要求唯一。对应入参 + private String refundTransId; + // 退款金额,精确到分,例如100 + private Long refundAmt; + // 交易状态,退款交易状态,详见数据字典#交易流水状态 + private String tranStatus; + // 交易代码,002 撤销 004 退费 005 冲正,详细请参考数据字典#交易代码 + private String tranCode; + // 支付方式,如:UPOP_APP, UPOP_WEB + private String payType; + + // 是否为退费订单 + public boolean isRefundOrder() { + return !ObjectUtils.isEmpty(refundTransId); + } +} diff --git a/src/main/java/com/ynxbd/jy/bean/JYPayRSAUtil.java b/src/main/java/com/ynxbd/jy/bean/JYPayRSAUtil.java new file mode 100644 index 0000000..b63cff1 --- /dev/null +++ b/src/main/java/com/ynxbd/jy/bean/JYPayRSAUtil.java @@ -0,0 +1,569 @@ +package com.ynxbd.jy.bean; + +import com.ynxbd.jy.helper.JYPayConfig; +import lombok.extern.slf4j.Slf4j; + +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.*; +import java.util.Map.Entry; + +@Slf4j +public class JYPayRSAUtil { + +// public static void main(String[] args) { +// Map packageParams = new HashMap<>(); +// packageParams.put("mch_id", "sddda"); +// packageParams.put("appid", "sss"); +// packageParams.put("xingming", "张三"); +// packageParams.put("b", "b"); +// String signValue = createSign(packageParams, "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAPupORdqoiZrdkwUlwjcrVjlvgbVCJsznugvQXIdx2yVNTrSJOZUMF2ALOUqEwEVG/V126JVF+aBJ4y52oiCfqGicSPqsvxeQ1iErKnu1KKMQ9flw9EIvb2LUicLtcG8ah6cfj4elij7XWk1QDpXyPy9KMBZ0G10sHHWs+mTgRTNAgMBAAECgYBWMD2STH3eJJ2uLrsPWOyxbnVnR5yb8tDwegay+4nqISvKyl/C1oB99UDq9z7s9vrNVSD0dqop0n/mDVzPtcmCCg/ue4lV3kj4rCDWVCc1Ooyjcu1JKCruOgrCdL72EwUeriBtXs+alGmU0KMrzpvUnSpKb6rcwMkwcvpsCii8TQJBAP/FVxmY4Z9HZz2GJuudHyokwTUUnCFJ198G5ttLhLAo0m5wCSKo8Aw8k0MZHwB9pHbHPrEjHatu+T9Re1jthXsCQQD74vCxlCyz7xPQKSoAv0JOLKlQmy1nycb7D8O5jUdY7t9PYbDS8kg0wgprvLuJNaj+KU3PKu3FGtCJ2PJSkKhXAkBKw4+bTuIc4g1eoZk85DfY5NeJQZYF8QTmD6lpzuBhcf1ZBOzWsdVBbm+JfI1gYlmx0roTo3UnyI+ZH6NTslKDAkBdD5JnuVvuupSyYpL/JaqEoWGIw1w6LUVTupZQdo72n4JnnKsstJDS1SHufhM3dVjTVkVGFhE0slx7oWid8o4PAkEAr9L1uUWyhXbcVo50dHC9e2OiDSmUPVZvzVj2AKJU1dz3lI+ZLLLdzCgdRNngT5CPz8DRttXCGLjiQ52P4g3atQ=="); +// System.out.println(signValue); +// } + + public static void main(String[] args) throws Exception { + Map packageParams = new HashMap<>(); + packageParams.put("appId", "wx2a6376841d35ad5c"); + packageParams.put("applicationId", "30007"); + packageParams.put("finalTransNo", "4500000373202608316223118562"); + packageParams.put("merOrderNum", "JYba651f16d83a48a44167f975f022"); + packageParams.put("merchantId", "20007"); + packageParams.put("merchantNo", "1112699888"); + packageParams.put("openId", "oiext28gL8UzoST4jQxcqcGTjc1g"); + packageParams.put("orderAmt", "1"); + packageParams.put("orderId", "IIPS7432026083116014812802354822"); + packageParams.put("orderTransId", "SN3752026083116014813302421029"); + packageParams.put("orderType", "001"); + packageParams.put("payType", "WX_PUBLIC"); + packageParams.put("paymentTime", "20260831160155"); + packageParams.put("refundOrder", "false"); + packageParams.put("respCode", "0000"); + packageParams.put("signValue", "LGi1a/qL1YsYf7lvNwnDsCQ5fyhlqLb3olM/YHqREH1BTxQa7XRW2mttBHGpendKYn4jK1dOvlYs7q96+KJJ/IvVz3GAIcFsg888qJAgG4/OdCajowuOxSQN/em02tsGq6lSERhccNkVCmbY/0aL2JgAAQXzKwnXZlodmqLCUXw="); + packageParams.put("threeTradeNo", "4500000373202608316223118562"); + packageParams.put("threeTransNo", "4500000373202608316223118562"); + packageParams.put("tranDateTime", "20260831160148"); + +// String stringA = "applicationId=2017051721001004270219620138&backURL=http://xian.kyee.com.cn:8621/qy_iips_pay/notify/payNotify&merOrderNum=MER000000000000000004&merchantId=12312312312&orderAmt=1&orderType=001&payType=WX_CODE"; +// String privateKey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAPupORdqoiZrdkwUlwjcrVjlvgbVCJsznugvQXIdx2yVNTrSJOZUMF2ALOUqEwEVG/V126JVF+aBJ4y52oiCfqGicSPqsvxeQ1iErKnu1KKMQ9flw9EIvb2LUicLtcG8ah6cfj4elij7XWk1QDpXyPy9KMBZ0G10sHHWs+mTgRTNAgMBAAECgYBWMD2STH3eJJ2uLrsPWOyxbnVnR5yb8tDwegay+4nqISvKyl/C1oB99UDq9z7s9vrNVSD0dqop0n/mDVzPtcmCCg/ue4lV3kj4rCDWVCc1Ooyjcu1JKCruOgrCdL72EwUeriBtXs+alGmU0KMrzpvUnSpKb6rcwMkwcvpsCii8TQJBAP/FVxmY4Z9HZz2GJuudHyokwTUUnCFJ198G5ttLhLAo0m5wCSKo8Aw8k0MZHwB9pHbHPrEjHatu+T9Re1jthXsCQQD74vCxlCyz7xPQKSoAv0JOLKlQmy1nycb7D8O5jUdY7t9PYbDS8kg0wgprvLuJNaj+KU3PKu3FGtCJ2PJSkKhXAkBKw4+bTuIc4g1eoZk85DfY5NeJQZYF8QTmD6lpzuBhcf1ZBOzWsdVBbm+JfI1gYlmx0roTo3UnyI+ZH6NTslKDAkBdD5JnuVvuupSyYpL/JaqEoWGIw1w6LUVTupZQdo72n4JnnKsstJDS1SHufhM3dVjTVkVGFhE0slx7oWid8o4PAkEAr9L1uUWyhXbcVo50dHC9e2OiDSmUPVZvzVj2AKJU1dz3lI+ZLLLdzCgdRNngT5CPz8DRttXCGLjiQ52P4g3atQ=="; +// String signValue = createSign(packageParams, JYPayConfig.PRIVATE_KEY); + + String signValue = createSign(packageParams, JYPayConfig.PRIVATE_KEY); + System.out.println("======="); + System.out.println(signValue); + } + + public static String createSign(Map packageParams, String privateKey) { + String sourceJson = getSignString(packageParams, false); + System.out.println("RSA加密前的Json:" + sourceJson); + String sign = ""; + try { + sign = JYPayRSAUtil.sign(sourceJson, privateKey); + } catch (Exception e) { + log.error("生成签名异常"); + } + return sign; + } + + /** + * 从map得到一个key1=value1&key2=value2的格式字符串(主要用于签名运算), 1.排除map中value为空的entry + * 2.排除签名字段 3.key按字典顺序 + * + * @param map map + * @param isSortedMap 是否是有序的map + * @return sign + */ + public static String getSignString(Map map, boolean isSortedMap) { + if (map == null || map.isEmpty()) { + throw new IllegalArgumentException("map must be not empty"); + } + + SortedMap sortedMap; + if (isSortedMap && map instanceof SortedMap) { + sortedMap = (SortedMap) map; + } else { + sortedMap = getNewTreeMap(true); + sortedMap.putAll(map); + } + + StringBuilder sb = new StringBuilder(); + for (Entry e : sortedMap.entrySet()) { + String k = e.getKey(); + String v; + if (e.getValue() instanceof String[]) { + v = String.valueOf(((String[]) e.getValue())[0]); + } else { + v = String.valueOf(e.getValue()); + } + + if (null != v && !"".equals(v) && !"signValue".equals(k) && !"privateKey".equals(k)) { + sb.append("&").append(k).append("=").append(v); + } + } + sb = sb.length() > 0 ? sb.deleteCharAt(0) : sb; + return sb.toString(); + } + + /** + *
+     * 描述:获取一个key按指定顺序排序的TreeMap
+     * @param  flag:true按key升序排列的TreeMap;false按key降序排列的TreeMap
+     * @return 返回按key指定顺序排列的TreeMap
+     * returnType:TreeMap
+     * 
+ */ + public static , V> TreeMap getNewTreeMap(boolean flag) { + TreeMap treeMap; + if (flag) { + treeMap = new TreeMap<>(Comparable::compareTo); + } else { + treeMap = new TreeMap<>(Comparator.reverseOrder()); + } + return treeMap; + } + + /** + * 加密解密使用的算法名称 + */ + public static final String KEY_ALGORITHM = "RSA"; + + /** + * 数字签名使用的算法名称(MD2withRSA、MD5withRSA或SHA1withRSA、SHA256WithRSA) + */ + public static final String SIGNATURE_ALGORITHM = "SHA256WithRSA"; + + /** + * RSA最大加密明文大小 + */ + private static final int MAX_ENCRYPT_BLOCK = 117; + + /** + * RSA最大解密密文大小 + */ + private static final int MAX_DECRYPT_BLOCK = 128; + + /** + *
+     * 描述:用私钥对信息生成签名(默认字符集)
+     * 作者:fengqianning
+     * 时间:2016年5月4日下午9:18:04
+     * @param content 要生成签名的字符串
+     * @param privateKey base64加密的私钥字符串
+     * @return
+     * @throws Exception
+     * returnType:String
+     * 
+ */ + public static String sign(String content, String privateKey) + throws Exception { + return sign(content, privateKey, "UTF-8"); + } + + /** + *
+     * 描述:用私钥对信息生成签名(指定字符集)
+     * 作者:fengqianning
+     * 时间:2016年5月4日下午9:15:08
+     * @param content 要生成签名的字符串
+     * @param privateKey base64加密的私钥字符串
+     * @param input_charset 要生成签名的字符串对应的字符集
+     * @return
+     * @throws Exception
+     * returnType:String
+     * 
+ */ + public static String sign(String content, String privateKey, + String input_charset) throws Exception { + // 用私钥对信息生成数字签名 + Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); + signature.initSign(getPrivateKey(privateKey)); + signature.update(content.getBytes(input_charset)); +// return Base64Util.getBase64StringByByteArray(signature.sign()); + return encode(signature.sign()); + } + + /** + *
+     * 描述:校验数字签名,验签成功返回true失败返回false(默认字符集)
+     * 作者:fengqianning
+     * 时间:2016年5月4日下午9:53:02
+     * @param content 要验签的字符串
+     * @param publicKey base64加密的公钥字符串
+     * @param sign 要验签的字符串签名
+     * @return
+     * @throws Exception
+     * returnType:boolean
+     * 
+ */ + public static boolean verify(String content, String publicKey, String sign) throws Exception { + return verify(content, publicKey, "UTF-8", sign); + } + + /** + *
+     * 描述:校验数字签名,验签成功返回true失败返回false(指定字符集)
+     * 作者:fengqianning
+     * 时间:2016年5月4日下午9:53:02
+     * @param content 要验签的字符串
+     * @param publicKey base64加密的公钥字符串
+     * @param input_charset 要验签的字符串对应的字符集
+     * @param sign 要验签的字符串签名
+     * @return
+     * @throws Exception
+     * returnType:boolean
+     * 
+ */ + public static boolean verify(String content, String publicKey, String input_charset, String sign) throws Exception { + Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); + signature.initVerify(getPublicKey(publicKey)); + signature.update(content.getBytes(input_charset)); + if (isStrBlank(sign)) { + return false; + } + // 验证签名是否正常 + return signature.verify(decode(sign)); + } + + /** + * + * 描述: 判断字符串是否为 空 JIRA单号: KYEEAPP-947
+ * 创建人: tianlong
+ * + * @param str + * @return bool + * @since Ver 1.1 + */ + public static boolean isStrBlank(String str) { + return (null == str || "".equals(str.trim()) || "NULL".equalsIgnoreCase(str.trim())); + } + + + /** + *
+     * 描述:根据base64加密的私钥字符串生成RSA私钥对象
+     * 作者:fengqianning
+     * 时间:2016年5月4日下午9:41:50
+     * @param privateKey base64加密的私钥字符串
+     * @return
+     * @throws Exception
+     * returnType:PrivateKey
+     * 
+ */ + private static PrivateKey getPrivateKey(String privateKey) throws Exception { + // 解密由base64编码的私钥 +// byte[] keyBytes = Base64Util.getByteArrayByBase64String(privateKey); + byte[] keyBytes = decode(privateKey); + // 构造PKCS8EncodedKeySpec对象 + PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes); + // KEY_ALGORITHM 指定的加密算法 + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + // 取私钥匙对象 + PrivateKey priKey = keyFactory.generatePrivate(pkcs8KeySpec); + return priKey; + } + + /** + *
+     * 描述:根据base64加密的公钥字符串生成RSA公钥对象
+     * 作者:fengqianning
+     * 时间:2016年5月4日下午9:48:52
+     * @param publicKey base64加密的公钥字符串
+     * @return
+     * @throws Exception
+     * returnType:PublicKey
+     * 
+ */ + private static PublicKey getPublicKey(String publicKey) throws Exception { + // 解密由base64编码的公钥 + byte[] keyBytes = decode(publicKey); + // 构造X509EncodedKeySpec对象 + X509EncodedKeySpec x509keySpec = new X509EncodedKeySpec(keyBytes); + // KEY_ALGORITHM 指定的加密算法 + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + // 取公钥匙对象 + PublicKey pubKey = keyFactory.generatePublic(x509keySpec); + return pubKey; + } + + static private final int BASELENGTH = 128; + static private final int LOOKUPLENGTH = 64; + static private final int TWENTYFOURBITGROUP = 24; + static private final int EIGHTBIT = 8; + static private final int SIXTEENBIT = 16; + static private final int FOURBYTE = 4; + static private final int SIGN = -128; + static private final char PAD = '='; + static private final boolean fDebug = false; + static final private byte[] base64Alphabet = new byte[BASELENGTH]; + static final private char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH]; + + static { + Arrays.fill(base64Alphabet, (byte) -1); + for (int i = 'Z'; i >= 'A'; i--) { + base64Alphabet[i] = (byte) (i - 'A'); + } + for (int i = 'z'; i >= 'a'; i--) { + base64Alphabet[i] = (byte) (i - 'a' + 26); + } + + for (int i = '9'; i >= '0'; i--) { + base64Alphabet[i] = (byte) (i - '0' + 52); + } + + base64Alphabet['+'] = 62; + base64Alphabet['/'] = 63; + + for (int i = 0; i <= 25; i++) { + lookUpBase64Alphabet[i] = (char) ('A' + i); + } + + for (int i = 26, j = 0; i <= 51; i++, j++) { + lookUpBase64Alphabet[i] = (char) ('a' + j); + } + + for (int i = 52, j = 0; i <= 61; i++, j++) { + lookUpBase64Alphabet[i] = (char) ('0' + j); + } + lookUpBase64Alphabet[62] = (char) '+'; + lookUpBase64Alphabet[63] = (char) '/'; + + } + + private static boolean isWhiteSpace(char octect) { + return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9); + } + + private static boolean isPad(char octect) { + return (octect == PAD); + } + + private static boolean isData(char octect) { + return (octect < BASELENGTH && base64Alphabet[octect] != -1); + } + + /** + * Encodes hex octects into Base64 + * + * @param binaryData Array containing binaryData + * @return Encoded Base64 array + */ + public static String encode(byte[] binaryData) { + + if (binaryData == null) { + return null; + } + + int lengthDataBits = binaryData.length * EIGHTBIT; + if (lengthDataBits == 0) { + return ""; + } + + int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP; + int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP; + int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets; + char[] encodedData = new char[numberQuartet * 4]; + + byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0; + + int encodedIndex = 0; + int dataIndex = 0; + if (fDebug) { + //edit begin 支付模块打印整改 zhoufeng 2014年12月2日09:32:19 + //KYEEAPP-1060 + // HLogger.info("number of triplets = " + numberTriplets); + //edit end + } + + for (int i = 0; i < numberTriplets; i++) { + b1 = binaryData[dataIndex++]; + b2 = binaryData[dataIndex++]; + b3 = binaryData[dataIndex++]; + + if (fDebug) { + //edit begin 支付模块打印整改 zhoufeng 2014年12月2日09:32:19 + //KYEEAPP-1060 + // HLogger.info("b1= " + b1 + ", b2= " + b2 + ", b3= " + b3); + //edit end + } + + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc); + + if (fDebug) { + //edit begin 支付模块打印整改 zhoufeng 2014年12月2日09:32:19 + //KYEEAPP-1060 + // HLogger.info("val2 = " + val2); + // HLogger.info("k4 = " + (k << 4)); + // HLogger.info("vak = " + (val2 | (k << 4))); + //edit end + } + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f]; + } + + // form integral number of 6-bit groups + if (fewerThan24bits == EIGHTBIT) { + b1 = binaryData[dataIndex]; + k = (byte) (b1 & 0x03); + if (fDebug) { + //edit begin 支付模块打印整改 zhoufeng 2014年12月2日09:32:19 + //KYEEAPP-1060 + // HLogger.info("b1=" + b1); + // HLogger.info("b1<<2 = " + (b1 >> 2)); + //edit end + } + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4]; + encodedData[encodedIndex++] = PAD; + encodedData[encodedIndex++] = PAD; + } else if (fewerThan24bits == SIXTEENBIT) { + b1 = binaryData[dataIndex]; + b2 = binaryData[dataIndex + 1]; + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2]; + encodedData[encodedIndex++] = PAD; + } + + return new String(encodedData); + } + + /** + * Decodes Base64 data into octects + * + * @param encoded string containing Base64 data + * @return Array containind decoded data. + */ + public static byte[] decode(String encoded) { + + if (encoded == null) { + return null; + } + + char[] base64Data = encoded.toCharArray(); + // remove white spaces + int len = removeWhiteSpace(base64Data); + + if (len % FOURBYTE != 0) { + return null;//should be divisible by four + } + + int numberQuadruple = (len / FOURBYTE); + + if (numberQuadruple == 0) { + return new byte[0]; + } + + byte[] decodedData = null; + byte b1 = 0, b2 = 0, b3 = 0, b4 = 0; + char d1 = 0, d2 = 0, d3 = 0, d4 = 0; + + int i = 0; + int encodedIndex = 0; + int dataIndex = 0; + decodedData = new byte[(numberQuadruple) * 3]; + + for (; i < numberQuadruple - 1; i++) { + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++])) + || !isData((d3 = base64Data[dataIndex++])) + || !isData((d4 = base64Data[dataIndex++]))) { + return null; + }//if found "no data" just return null + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + } + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))) { + return null;//if found "no data" just return null + } + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + + d3 = base64Data[dataIndex++]; + d4 = base64Data[dataIndex++]; + if (!isData((d3)) || !isData((d4))) {//Check if they are PAD characters + if (isPad(d3) && isPad(d4)) { + if ((b2 & 0xf) != 0)//last 4 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 1]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + return tmp; + } else if (!isPad(d3) && isPad(d4)) { + b3 = base64Alphabet[d3]; + if ((b3 & 0x3) != 0)//last 2 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 2]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + return tmp; + } else { + return null; + } + } else { //No PAD e.g 3cQl + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + + } + + return decodedData; + } + + /** + * remove WhiteSpace from MIME containing encoded Base64 data. + * + * @param data the byte array of base64 data (with WS) + * @return the new length + */ + private static int removeWhiteSpace(char[] data) { + if (data == null) { + return 0; + } + + // count characters that's not whitespace + int newSize = 0; + int len = data.length; + for (int i = 0; i < len; i++) { + if (!isWhiteSpace(data[i])) { + data[newSize++] = data[i]; + } + } + return newSize; + } + +} + diff --git a/src/main/java/com/ynxbd/jy/bean/JYResult.java b/src/main/java/com/ynxbd/jy/bean/JYResult.java new file mode 100644 index 0000000..b36abe4 --- /dev/null +++ b/src/main/java/com/ynxbd/jy/bean/JYResult.java @@ -0,0 +1,96 @@ +package com.ynxbd.jy.bean; + +import com.alibaba.fastjson.JSONObject; +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; + +@Slf4j +@Getter +@Setter +@ToString +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class JYResult { + private String code; + private String message; + private String success; + private String status; + + private JSONObject data; + + public JYResult(JSONObject resp) { + if (resp == null) { + return; + } + this.code = resp.getString("resultCode"); + this.success = resp.getString("success"); + this.status = resp.getString("status"); + this.message = resp.getString("message"); + this.data = resp; + } + + public boolean isSuccess() { + return "true".equals(success); + } + + + private JSONObject logError(String tip) { + log.error("[京颐]预下单返回[{}]为空", tip == null ? "" : tip); + return null; + } + + public JSONObject toWxPrepay() { + if (this.data == null) { + return null; + } + String appId = this.data.getString("wxAppId"); + String timeStamp = this.data.getString("wxTimeStamp"); // 时间戳 + String nonceStr = this.data.getString("wxUuid"); // 随机字符串 + String packagePrepayId = this.data.getString("tn"); // 预支付交易会话标识 + String signType = this.data.getString("wxSignType"); // 签名类型 + String paySign = this.data.getString("wxSign"); // 签名值 + + String outTradeNo = this.data.getString("orderId"); + String bankTransNo = this.data.getString("orderTransId"); + + if (ObjectUtils.isEmpty(appId)) { + return logError("appId"); + } + + if (ObjectUtils.isEmpty(timeStamp)) { + return logError("时间戳"); + } + + if (ObjectUtils.isEmpty(nonceStr)) { + return logError("随机字符串"); + } + + if (ObjectUtils.isEmpty(packagePrepayId)) { + return logError("预支付交易会话标识"); + } + + if (ObjectUtils.isEmpty(signType)) { + return logError("签名类型"); + } + + if (ObjectUtils.isEmpty(paySign)) { + return logError("签名值"); + } + + if (ObjectUtils.isEmpty(outTradeNo)) { + return logError("商户订单号"); + } + + JSONObject prepayInfo = new JSONObject(); // 预下单信息 + prepayInfo.put("appId", appId); + prepayInfo.put("timeStamp", timeStamp); + prepayInfo.put("nonceStr", nonceStr); + prepayInfo.put("package", "prepay_id=" + packagePrepayId); + + prepayInfo.put("signType", signType); + prepayInfo.put("paySign", paySign); + prepayInfo.put("outTradeNo", outTradeNo); + prepayInfo.put("bankTransNo", bankTransNo); + return prepayInfo; + } +} diff --git a/src/main/java/com/ynxbd/jy/helper/JYPayConfig.java b/src/main/java/com/ynxbd/jy/helper/JYPayConfig.java index 8b48fd9..067cb62 100644 --- a/src/main/java/com/ynxbd/jy/helper/JYPayConfig.java +++ b/src/main/java/com/ynxbd/jy/helper/JYPayConfig.java @@ -20,6 +20,8 @@ public class JYPayConfig { public final static String PRIVATE_KEY; + public final static String PUBLIC_KEY; + static { XmlConfigHelper config = new XmlConfigHelper().read("jy-config.xml", "pay"); @@ -28,6 +30,7 @@ public class JYPayConfig { MERCHANT_ID = config.getString("merchant_id"); HOSP_ID = config.getString("hosp_id"); PRIVATE_KEY = config.getString("private_key"); + PUBLIC_KEY = config.getString("public_key"); if (URL == null) { log.error("[京颐-支付]配置文件读取失败"); diff --git a/src/main/java/com/ynxbd/jy/helper/JYPayHelper.java b/src/main/java/com/ynxbd/jy/helper/JYPayHelper.java index 6dad6e9..67d7309 100644 --- a/src/main/java/com/ynxbd/jy/helper/JYPayHelper.java +++ b/src/main/java/com/ynxbd/jy/helper/JYPayHelper.java @@ -3,10 +3,15 @@ package com.ynxbd.jy.helper; import com.alibaba.fastjson.JSONObject; import com.ynxbd.common.bean.enums.MerchantEnum; import com.ynxbd.common.bean.pay.ReqOrder; +import com.ynxbd.common.helper.common.Base64Helper; +import com.ynxbd.common.helper.common.DateHelper; import com.ynxbd.common.helper.common.JsonHelper; +import com.ynxbd.common.helper.common.URLHelper; import com.ynxbd.common.helper.http.OkHttpHelper; import com.ynxbd.common.helper.rsa.RSAHelper; import com.ynxbd.jy.bean.JYOrder; +import com.ynxbd.jy.bean.JYResult; +import com.ynxbd.jy.bean.enums.JYBusinessTypeEnum; import com.ynxbd.wx.config.WeChatConfig; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; @@ -24,24 +29,33 @@ import java.util.stream.Collectors; @Slf4j public class JYPayHelper { + public static JYResult createRequest(String url, Map params) { + JSONObject respJson = OkHttpHelper.postForm(JYPayConfig.URL + url, o -> { + o.putAll(params); + }, headers -> { + headers.add("content-type", "application/x-www-form-urlencoded"); + }); + return new JYResult(respJson); + } + /** * 商户创建订单 * * @param merchantEnum 枚举 * @param title 商品标题 * @param totalFee 订单金额 - * @param outTradeNo 订单号 + * @param merOrderNum 应用商户订单号 * @param notifyType 回调类型 * @param ip ip地址 * @param payOpenId 用于下单的openId * @param msgOpenId 用于推送的openId * @return 成功返回 json, 否则返回null */ - public static JSONObject createOrder(MerchantEnum merchantEnum, String title, String outTradeNo, String totalFee, String notifyType, String ip, String payOpenId, String msgOpenId, ReqOrder reqOrder) { + public static JSONObject createOrder(MerchantEnum merchantEnum, String title, String merOrderNum, String totalFee, String notifyType, String ip, String payOpenId, String msgOpenId, ReqOrder reqOrder) { try { - if (ObjectUtils.isEmpty(payOpenId) || ObjectUtils.isEmpty(outTradeNo) + if (ObjectUtils.isEmpty(payOpenId) || ObjectUtils.isEmpty(merOrderNum) || merchantEnum == null || totalFee == null || notifyType == null) { - log.info("【京硕】下单参数缺失"); + log.error("【京硕】预下单参数缺失"); return null; } @@ -49,46 +63,44 @@ public class JYPayHelper { try { cents = new BigDecimal(totalFee).movePointRight(2).toString(); // 金额转换=>单位分 } catch (Exception e) { - log.info("【京硕】下单金额转换异常:", e); + log.error("【京硕】预下单金额转换异常:", e); return null; } - log.info("【京硕】下单 {},{},{},{},{}", title, outTradeNo, totalFee, ip, merchantEnum.NOTIFY_URL); - + log.info("【京硕】预下单 {},{},{},{},{}", title, merOrderNum, totalFee, ip, merchantEnum.NOTIFY_URL); - log.info("hospitalId={}, applicationId={}, merchantI={}", JYPayConfig.HOSP_ID, JYPayConfig.APPLICATION_ID, JYPayConfig.MERCHANT_ID); JYOrder jyOrder = reqOrder.jyOrder; - - Map req = new HashMap<>(); - req.put("openId", payOpenId); - req.put("hospitalId", JYPayConfig.HOSP_ID); // 医院ID - req.put("applicationId", JYPayConfig.APPLICATION_ID); - req.put("merchantId", JYPayConfig.MERCHANT_ID); - req.put("merOrderNum", outTradeNo); // 商户订单号 - req.put("hisOrderNum", null); // HIS订单号:可以为发票号[该接口若不传,在业务完成后调用2.1.6接口] - req.put("hisTransNum", null); // HIS流水号:可以为发票号[该接口若不传,在业务完成后调用2.1.6接口] + Map params = new HashMap<>(); + params.put("openId", payOpenId); + params.put("hospitalId", JYPayConfig.HOSP_ID); // 医院ID + params.put("applicationId", JYPayConfig.APPLICATION_ID); + params.put("merchantId", JYPayConfig.MERCHANT_ID); + params.put("merOrderNum", merOrderNum); // 商户订单号 + params.put("hisOrderNum", null); // HIS订单号:可以为发票号[该接口若不传,在业务完成后调用2.1.6接口] + params.put("hisTransNum", null); // HIS流水号:可以为发票号[该接口若不传,在业务完成后调用2.1.6接口] // 金额 - req.put("orderAmt", cents); // 交易金额:只接收整数,单位:分,例如100表示壹圆整;支付宝医保支付传总金额 - - req.put("orderType", jyOrder.orderTypeEnum.CODE); // 如:001:挂号费 002:医药费; - req.put("backURL", WeChatConfig.getBaseURL() + merchantEnum.NOTIFY_URL); // 商户异步回调地址:后台发起异步回调地址, 扫码枪支付方式无需回调的可以不 -// params.put("backParamsType", "key-value"); // 商户异步回调入参格式:后台发起异步回调时参数格式,取值有:key-value和json,默认值是key-value。 - req.put("payType", "WX_PUBLIC"); // 如:UPOP_APP, UPOP_WEB;详见附录5.1[微信公众号支付(WX_PUBLIC)] - req.put("bussinessType", jyOrder.businessTypeEnum.CODE); // 业务类型:综合支付对应用权限控制是否必传 最大不超过4位,详见数据字典5.2 该接口若不传,需要调用2.1.6接口再次更新 - req.put("patientName", reqOrder.getPatientName()); // 微信医保支付必传。其余支付方式若该接口若不传,需要调用2.1.6接口再次更新 - req.put("patientGender", reqOrder.getPatientGender()); // 患者性别。综合支付对应用权限控制是否必传 1男 2女 9其他 最大不超过4位 该接口若不传,需要调用2.1.6接口再次更新 - req.put("patientAge", reqOrder.getPatientAge()); // 患者年龄。综合支付对应用权限控制是否必传 该接口若不传,需要调用2.1.6接口再次更新 - req.put("patientIdNo", reqOrder.getPatientIdCardNo()); // 微信医保支付必传。其余支付方式若该接口若不传,需要调用2.1.6接口再次更新 - req.put("patientPhoneNumber", reqOrder.getPatientTel()); // 综合支付对应用权限控制是否必传 最大不超过16位 该接口若不传,需要调用2.1.6接口再次更新 - req.put("patientId", reqOrder.getPatientId()); // 患者ID:综合支付对应用权限控制是否必传 最大不超过32位 该接口若不传,需要调用2.1.6接口再次更新 - req.put("patientCardNo", reqOrder.getPatientId()); // 患者卡号:综合支付对应用权限控制是否必传 最大不超过32位该接口若不传,需要调用2.1.6接口再次更新 - req.put("clinicNo", reqOrder.getTreatNum()); // 门诊号:综合支付对应用权限控制是否必传 最大不超过32位该接口若不传,需要调用2.1.6接口再次更新 - req.put("inpNo", reqOrder.getTreatNum()); // 住院号:综合支付对应用权限控制是否必传 最大不超过32位该接口若不传,需要调用2.1.6接口再次更新 - req.put("hisTransDoneTime", null); // HIS业务完成时间:综合支付对应用权限控制是否必传 YYYY-MM-DD HH:MM:SS 该接口若不传,需要调用2.1.6接口再次更新 - req.put("hisOperatorNo", jyOrder.getHisOperatorNo()); // HIS操作员编号:综合支付对应用权限控制是否必传HIS操作员编号和HIS操作员姓名必须成对出现该接口若不传,需要调用2.1.6接口再次更新 - req.put("hisOperatorName", jyOrder.getHisOperatorName()); // HIS操作员姓名:综合支付对应用权限控制是否必传 HIS操作员编号和HIS操作员姓名必须成对出现 该接口若不传,需要调用2.1.6接口再次更新 - req.put("receiptNo", null); // 综合支付对应用权限控制是否必传最大不超过200位 该接口若不传,需要调用2.1.6接口再次更新 - req.put("remarks", ""); // 备注 建行渠道格式为json,参考样式:{"REMARK1":"REM1","REMARK2":"REM2"} 农行渠道参考样式:{"TerminalNo":"1234567"} + params.put("orderAmt", cents); // 交易金额:只接收整数,单位:分,例如100表示壹圆整;支付宝医保支付传总金额 + + params.put("orderType", jyOrder.orderTypeEnum.CODE); // 如:001:挂号费 002:医药费; + params.put("backURL", WeChatConfig.getBaseURL() + merchantEnum.NOTIFY_URL + "?notifyType=" + notifyType); // 商户异步回调地址:后台发起异步回调地址, 扫码枪支付方式无需回调的可以不 + params.put("backParamsType", "key-value"); // 商户异步回调入参格式:后台发起异步回调时参数格式,取值有:key-value和json,默认值是key-value。 + + params.put("payType", "WX_PUBLIC"); // 如:UPOP_APP, UPOP_WEB;详见附录5.1[微信公众号支付(WX_PUBLIC)] + params.put("bussinessType", jyOrder.businessTypeEnum.CODE); // 业务类型:综合支付对应用权限控制是否必传 最大不超过4位,详见数据字典5.2 该接口若不传,需要调用2.1.6接口再次更新 + params.put("patientName", reqOrder.getPatientName()); // 微信医保支付必传。其余支付方式若该接口若不传,需要调用2.1.6接口再次更新 + params.put("patientGender", reqOrder.getPatientGender()); // 患者性别。综合支付对应用权限控制是否必传 1男 2女 9其他 最大不超过4位 该接口若不传,需要调用2.1.6接口再次更新 + params.put("patientAge", reqOrder.getPatientAge()); // 患者年龄。综合支付对应用权限控制是否必传 该接口若不传,需要调用2.1.6接口再次更新 + params.put("patientIdNo", reqOrder.getPatientIdCardNo()); // 微信医保支付必传。其余支付方式若该接口若不传,需要调用2.1.6接口再次更新 + params.put("patientPhoneNumber", reqOrder.getPatientTel()); // 综合支付对应用权限控制是否必传 最大不超过16位 该接口若不传,需要调用2.1.6接口再次更新 + params.put("patientId", reqOrder.getPatientId()); // 患者ID:综合支付对应用权限控制是否必传 最大不超过32位 该接口若不传,需要调用2.1.6接口再次更新 + params.put("patientCardNo", reqOrder.getPatientId()); // 患者卡号:综合支付对应用权限控制是否必传 最大不超过32位该接口若不传,需要调用2.1.6接口再次更新 + params.put("clinicNo", reqOrder.getTreatNum()); // 门诊号:综合支付对应用权限控制是否必传 最大不超过32位该接口若不传,需要调用2.1.6接口再次更新 + params.put("inpNo", reqOrder.getTreatNum()); // 住院号:综合支付对应用权限控制是否必传 最大不超过32位该接口若不传,需要调用2.1.6接口再次更新 + params.put("hisTransDoneTime", null); // HIS业务完成时间:综合支付对应用权限控制是否必传 YYYY-MM-DD HH:MM:SS 该接口若不传,需要调用2.1.6接口再次更新 + params.put("hisOperatorNo", jyOrder.getHisOperatorNo()); // HIS操作员编号:综合支付对应用权限控制是否必传HIS操作员编号和HIS操作员姓名必须成对出现该接口若不传,需要调用2.1.6接口再次更新 + params.put("hisOperatorName", jyOrder.getHisOperatorName()); // HIS操作员姓名:综合支付对应用权限控制是否必传 HIS操作员编号和HIS操作员姓名必须成对出现 该接口若不传,需要调用2.1.6接口再次更新 + params.put("receiptNo", null); // 综合支付对应用权限控制是否必传最大不超过200位 该接口若不传,需要调用2.1.6接口再次更新 + params.put("remarks", ""); // 备注 建行渠道格式为json,参考样式:{"REMARK1":"REM1","REMARK2":"REM2"} 农行渠道参考样式:{"TerminalNo":"1234567"} // 医保支付====================================== // params.put("orderTotalAmt", cents); // orderTotalAmt:微信医保支付必传,有社保交易数据时,订单总金额=自费实际金额+社保金额+减免金额 @@ -105,23 +117,24 @@ public class JYPayHelper { // params.put("isDept", "4"); // 医保标识:is_dept=4 医保电子凭证支付; // params.put("serialNo", reqOrder.getSerialNo()); // 医院HIS系统订单号 // params.put("requestContent", reqOrder.getRequestContent()); // 参考医保结构体(医疗机构透传医保) 示例: {"payAuthNo":"AUTH****","payOrdId":"ORD****","setlLatlnt":"118.096435,24.485407"} payAuthNo:授权码 payOrdId:对应处方上传的出参单号 setlLatlnt:用户地理定位信息, 经纬度信息,如“118.096435,24.485407”。未获取到用户定位时,传“0,0”。 -//// params.put("extendInfo", ""); //亲情移动支付时,为必填。 (就诊亲属姓名md5 - rel_user_name_md5,就诊亲属身份证件号md5 - rel_user_card_no_md5 ) { "rel_user_name_md5": "XXX","rel_user_card_no_md5": "XXX"} MD5为32位小写,uft-8编码(身份证中如果有字母需要转成大写再做MD5) +// params.put("extendInfo", ""); //亲情移动支付时,为必填。 (就诊亲属姓名md5 - rel_user_name_md5,就诊亲属身份证件号md5 - rel_user_card_no_md5 ) { "rel_user_name_md5": "XXX","rel_user_card_no_md5": "XXX"} MD5为32位小写,uft-8编码(身份证中如果有字母需要转成大写再做MD5) - String signValue = createSign(req); + String signValue = createSign(params); if (signValue == null) { return null; } - req.put("signValue", signValue); // 数字签名:由接入方生成的签名,支付平台根据该字段对入参进行验签;签名生成规则参考4加签验签规则 + params.put("signValue", signValue); // 数字签名:由接入方生成的签名,支付平台根据该字段对入参进行验签;签名生成规则参考4加签验签规则 - log.info("req={}", JSONObject.toJSONString(req)); - JSONObject resp = OkHttpHelper.postForm(JYPayConfig.URL + "/qy_iips_pay/paycore/prePayOrder", params -> { - params.putAll(req); - }, headers -> { - headers.add("content-type", "application/x-www-form-urlencoded"); - }); - log.info("【京硕】下单返回 {}", JsonHelper.toJsonString(resp)); + log.info("【京硕】预下单-入参-{}", JSONObject.toJSONString(params)); + JYResult result = createRequest("/qy_iips_pay/paycore/prePayOrder", params); + log.info("【京硕】预下单-响应-{}", JsonHelper.toJsonString(result)); + if (!result.isSuccess()) { + log.error("【京硕】预下单-失败:{}", result.getMessage()); + return null; + } + return result.toWxPrepay(); } catch (Exception e) { - log.error("【京硕】下单异常信息:", e); + log.error("【京硕】预下单-异常:", e); } return null; } @@ -130,44 +143,38 @@ public class JYPayHelper { /** * 支付状态查询 * - * @param merchantEnum 枚举 - * @param title 商品标题 - * @param totalFee 订单金额 - * @param outTradeNo 订单号 * @return 成功返回 json, 否则返回null */ - public static JSONObject queryPayOrderInfo(MerchantEnum merchantEnum, String title, String totalFee, String outTradeNo, String bankTransNo, String tradeNo) { + public static JSONObject queryPayOrderInfo(String outTradeNo, String merOrderNum, String tradeNo) { try { - log.info("【京硕】支付状态查询 {},{},{},{}", title, outTradeNo, totalFee, merchantEnum.NOTIFY_URL); - if (ObjectUtils.isEmpty(outTradeNo) && ObjectUtils.isEmpty(bankTransNo) && ObjectUtils.isEmpty(tradeNo)) { - return null; - } - - if (ObjectUtils.isEmpty(outTradeNo) && (ObjectUtils.isEmpty(bankTransNo) || ObjectUtils.isEmpty(tradeNo))) { + log.info("【京硕】支付状态查询 {},{},{}", outTradeNo, merOrderNum, tradeNo); + if (ObjectUtils.isEmpty(outTradeNo) && ObjectUtils.isEmpty(merOrderNum) && ObjectUtils.isEmpty(tradeNo)) { return null; } - Map req = new HashMap<>(); - req.put("applicationId", JYPayConfig.APPLICATION_ID); - req.put("merchantId", JYPayConfig.MERCHANT_ID); - req.put("merOrderNum", outTradeNo); // 商户订单号 - req.put("orderId", bankTransNo); // 支付平台订单号 - req.put("outTransNo", tradeNo); // 应用流水号:应用流水号outTransNo”和“支付平台订单号orderId”这两个参数必须同时传。该参数指“应用方流水号”,不许有重复 + Map params = new HashMap<>(); + params.put("applicationId", JYPayConfig.APPLICATION_ID); + params.put("merchantId", JYPayConfig.MERCHANT_ID); + params.put("orderId", outTradeNo); // 支付平台订单号 + params.put("merOrderNum", merOrderNum); // 应用方订单号 + params.put("outTransNo", tradeNo); // 应用流水号:应用流水号outTransNo”和“支付平台订单号orderId”这两个参数必须同时传。该参数指“应用方流水号”,不许有重复 - String signValue = createSign(req); + String signValue = createSign(params); if (signValue == null) { return null; } - req.put("signValue", signValue); // 应用流水号:应用流水号outTransNo”和“支付平台订单号orderId”这两个参数必须同时传。该参数指“应用方流水号”,不许有重复 + params.put("signValue", signValue); // 应用流水号:应用流水号outTransNo”和“支付平台订单号orderId”这两个参数必须同时传。该参数指“应用方流水号”,不许有重复 - JSONObject resp = OkHttpHelper.postJson(JYPayConfig.URL + "/qy_iips_pay/paycore/queryPayOrderInfo", params -> { - params.putAll(req); - }, headers -> { - headers.add("content-type", "application/x-www-form-urlencoded"); - }); - log.info("【京硕】支付状态查询返回 {}", JsonHelper.toJsonString(resp)); + log.info("【京硕】支付状态查询-入参-{}", JSONObject.toJSONString(params)); + JYResult result = createRequest("/qy_iips_pay/paycore/queryPayOrderInfo", params); + log.info("【京硕】支付状态查询-响应-{}", JsonHelper.toJsonString(result)); + if (!result.isSuccess()) { + log.error("【京硕】支付状态查询-失败:{}", result.getMessage()); + return null; + } + return result.getData(); } catch (Exception e) { - log.error("【京硕】支付状态查询异常信息:", e); + log.error("【京硕】支付状态查询-异常:", e); } return null; } @@ -176,44 +183,75 @@ public class JYPayHelper { /** * 退款申请 * - * @param merchantEnum 枚举 - * @param title 商品标题 - * @param totalFee 订单金额 - * @param outTradeNo 订单号 * @return 成功返回 json, 否则返回null */ - public static JSONObject refundPayOrder(MerchantEnum merchantEnum, String title, String totalFee, String outTradeNo, String bankTransNo, String tradeNo, String sign) { + public static JSONObject refundPayOrder(JYBusinessTypeEnum businessTypeEnum, ReqOrder patient, String outRefundNo, BigDecimal refundFee, String outTradeNo, String bankMerchantNo, String tradeNo) { try { - log.info("【京硕】退款申请 {},{},{},{}", title, outTradeNo, totalFee, merchantEnum.NOTIFY_URL); - if (ObjectUtils.isEmpty(outTradeNo) && ObjectUtils.isEmpty(bankTransNo) && ObjectUtils.isEmpty(tradeNo)) { + log.info("【京硕】退款申请 {},{},{},{}", outTradeNo, bankMerchantNo, tradeNo, refundFee); + if (ObjectUtils.isEmpty(outTradeNo) && ObjectUtils.isEmpty(bankMerchantNo) && ObjectUtils.isEmpty(tradeNo)) { return null; } - if (ObjectUtils.isEmpty(outTradeNo) && (ObjectUtils.isEmpty(bankTransNo) || ObjectUtils.isEmpty(tradeNo))) { + String refundCents; + try { + if (refundFee == null) { + return null; + } + refundCents = refundFee.movePointRight(2).toString(); // 金额转换=>单位分 + } catch (Exception e) { + log.info("【京硕】退款申请-金额转换异常:", e); return null; } - Map req = new HashMap<>(); - req.put("applicationId", JYPayConfig.APPLICATION_ID); - req.put("merchantId", JYPayConfig.MERCHANT_ID); - req.put("merOrderNum", outTradeNo); // 商户订单号 - req.put("orderId", bankTransNo); // 支付平台订单号 - req.put("outTransNo", tradeNo); // 应用流水号:应用流水号outTransNo”和“支付平台订单号orderId”这两个参数必须同时传。该参数指“应用方流水号”,不许有重复 - req.put("signValue", sign); // 应用流水号:应用流水号outTransNo”和“支付平台订单号orderId”这两个参数必须同时传。该参数指“应用方流水号”,不许有重复 - req.put("refundStateSync", "0"); // 退费状态是否同步返回[0:否,1:是,不传默认为否 若传值为1,则退款状态不明确,开始轮询] - req.put("requestContent", "0"); // 微信医保支付必传。示例:{“payOrdId”:”******”,“ref_reason”:”******”ref_reason  退款原因payOrdId: 对应处方上传的出参单号 - - String signValue = createSign(req); + + Map params = new HashMap<>(); + params.put("applicationId", JYPayConfig.APPLICATION_ID); + params.put("merchantId", JYPayConfig.MERCHANT_ID); + + params.put("refundAmt", refundCents); // 微信医保和支付宝医保只支持全部退款(支付宝医保为总金额)。只接收整数,单位:分,例如100表示壹圆整; + params.put("refundTransId", outRefundNo); // 由应用方生成的退款流水号,要求唯一。同一笔退款流水若重复发送,该流水号不能变,否则平台会认为发起一笔新的退款流水。 + params.put("orderId", outTradeNo); // 支付平台订单号 + params.put("merOrderNum", bankMerchantNo); // 应用方自己生成的订单唯一标识[BankMerchantNo] + +// req.put("hisOrderNum", hisOrderNum); // HIS订单号-综合支付对应用权限控制是否必传可以为发票号,作为his订单的唯一标识,方便对账该接口若不传,可以在业务完成后调用2.1.6接口 + params.put("hisTransNum", tradeNo); // HIS流水号-综合支付对应用权限控制是否必传可以为发票号,作为his流水的唯一标识,方便对账该接口若不传,需要在业务完成后调用2.1.6接口 + + params.put("refundPayType", "CASH_CASH"); + params.put("bussinessType", businessTypeEnum.CODE); + // 用户信息 + params.put("patientName", patient.getPatientName()); + params.put("patientGender", patient.getPatientGender()); + params.put("patientAge", patient.getPatientAge()); + params.put("patientIdNo", patient.getPatientIdCardNo()); + params.put("patientPhoneNumber", patient.getPatientTel()); + params.put("patientId", patient.getPatientId()); + params.put("patientCardNo", patient.getPatientId()); + + params.put("clinicNo", patient.getTreatNum()); // 门诊号 + params.put("inpNo", patient.getTreatNum()); // 住院号 + + params.put("hisTransDoneTime", DateHelper.getCurDateTime()); +// req.put("hisOperatorNo", ""); // HIS操作员编号 +// req.put("hisOperatorName", ""); // HIS操作员姓名 + + params.put("receiptNo", null); // HIS票据号 + params.put("hospitalId", JYPayConfig.HOSP_ID); // 医院ID + + params.put("refundStateSync", "0"); // 退费状态是否同步返回[0:否,1:是,不传默认为否 若传值为1,则退款状态不明确,开始轮询] + + String signValue = createSign(params); if (signValue == null) { return null; } - req.put("signValue", signValue); // 数字签名:由接入方生成的签名,支付平台根据该字段对入参进行验签;签名生成规则参考4加签验签规则 + params.put("signValue", signValue); // 数字签名:由接入方生成的签名,支付平台根据该字段对入参进行验签;签名生成规则参考4加签验签规则 - JSONObject resp = OkHttpHelper.postJson(JYPayConfig.URL + "/qy_iips_pay/paycore/refundPayOrder", params -> { - params.putAll(req); - }, headers -> { - headers.add("content-type", "application/x-www-form-urlencoded"); - }); - log.info("【京硕】退款申请返回 {}", JsonHelper.toJsonString(resp)); + log.info("【京硕】退款申请-入参-{}", JSONObject.toJSONString(params)); + JYResult result = createRequest("/qy_iips_pay/paycore/refundPayOrder", params); + log.info("【京硕】退款申请-响应-{}", JsonHelper.toJsonString(result)); + if (!result.isSuccess()) { + log.error("【京硕】退款申请-失败:{}", result.getMessage()); + return null; + } + return result.getData(); } catch (Exception e) { log.error("【京硕】退款申请异常信息:", e); } @@ -243,13 +281,13 @@ public class JYPayHelper { JSONObject resp = OkHttpHelper.postJson(JYPayConfig.URL + "/qy_iips_pay/paycore/modifyBusinessInfo", params -> { params.put("applicationId", JYPayConfig.APPLICATION_ID); // 应用编号 params.put("merchantId", JYPayConfig.MERCHANT_ID); // 应用商户编号 -// params.put("merOrderNum", merOrderNum); // 商户订单号 -// params.put("refundTransId", refundTransId); // 商户退款流水号,非支付交易必传 -// params.put("orderId", orderId); // 支付平台订单号 -// params.put("orderTransId", orderTransId); // 支付平台流水号 -// params.put("hisOrderNum", hisOrderNum); // HIS订单号,对账场景必传 -// params.put("hisTransNum", hisTransNum); // HIS交易流水号,对账场景必传 -// params.put("hisTransAmt", hisTransAmt); // 交易金额,单位:分,整数 +// params.put("merOrderNum", merOrderNum); // 商户订单号 +// params.put("refundTransId", refundTransId); // 商户退款流水号,非支付交易必传 +// params.put("orderId", orderId); // 支付平台订单号 +// params.put("orderTransId", orderTransId); // 支付平台流水号 +// params.put("hisOrderNum", hisOrderNum); // HIS订单号,对账场景必传 +// params.put("hisTransNum", hisTransNum); // HIS交易流水号,对账场景必传 +// params.put("hisTransAmt", hisTransAmt); // 交易金额,单位:分,整数 // params.put("bussinessType", bussinessType); // 业务类型,最大4位 // params.put("patientName", patientName); // 患者姓名 // params.put("patientGender", patientGender); // 患者性别:1男 2女 9其他 @@ -306,6 +344,32 @@ public class JYPayHelper { .collect(Collectors.joining("&")); } +// /** +// * 【可选】排除signValue自身(标准支付签名规范推荐) +// * +// * @param params 原始参数map +// * @return 待签名字符串 +// */ +// public static String buildSign(Map params) { +// return params.entrySet().stream() +// // 1.过滤signValue;值不为null;转字符串后非空 +// .filter(entry -> !"signValue".equals(entry.getKey())) +// .filter(entry -> { +// Object val = entry.getValue(); +// if (val == null) { +// return false; +// } +// String strVal = String.valueOf(val).trim(); +// return !strVal.isEmpty(); +// }) +// // 2.按照key字典升序排序 +// .sorted(Map.Entry.comparingByKey()) +// // 3.key=value +// .map(entry -> entry.getKey() + "=" + String.valueOf(entry.getValue()).trim()) +// .collect(Collectors.joining("&")); +// } + + /** * 使用私钥按照指定算法进行签名 * diff --git a/src/main/java/com/ynxbd/wx/config/WeChatConfig.java b/src/main/java/com/ynxbd/wx/config/WeChatConfig.java index 91afb2c..59c6ead 100644 --- a/src/main/java/com/ynxbd/wx/config/WeChatConfig.java +++ b/src/main/java/com/ynxbd/wx/config/WeChatConfig.java @@ -60,6 +60,8 @@ public class WeChatConfig { // 医共体主体小程序AppId public static final String GMC_MINI_APP_ID; public static final String GMC_MINI_APP_SECRET; + // 是否不加载退费证书(京颐为true) + public static final boolean IS_NOT_LOAD_REFUND_CERT; static { ProperHelper config = new ProperHelper().read("wx.properties"); @@ -89,6 +91,7 @@ public class WeChatConfig { DEV_OPEN_IDS = config.getString("wx.dev_open_ids", ""); IS_RECIPE_PREPAY = config.getBoolean("wx.is_recipe_prepay", false); IS_DEV = config.getBoolean("wx.is_dev", false); + IS_NOT_LOAD_REFUND_CERT = config.getBoolean("wx.is_not_load_refund_cert", false); // 医共体 IS_ENABLE_GMC = config.getBoolean("wx.gmc.is_enable", false); diff --git a/src/main/java/com/ynxbd/wx/wxfactory/WxPayHelper.java b/src/main/java/com/ynxbd/wx/wxfactory/WxPayHelper.java index 901a502..952fd0f 100644 --- a/src/main/java/com/ynxbd/wx/wxfactory/WxPayHelper.java +++ b/src/main/java/com/ynxbd/wx/wxfactory/WxPayHelper.java @@ -57,13 +57,17 @@ public class WxPayHelper { static { try { - String mchId = WeChatConfig.MCH_ID; - if (!ObjectUtils.isEmpty(mchId)) { - String clazzPath = Objects.requireNonNull(WxPayHelper.class.getClassLoader().getResource("")).getPath(); - if (clazzPath == null) { - log.info("【微信】未找到证书"); + if (WeChatConfig.IS_NOT_LOAD_REFUND_CERT) { + log.info("【微信】不加载证书..."); + } else { + String mchId = WeChatConfig.MCH_ID; + if (!ObjectUtils.isEmpty(mchId)) { + String clazzPath = Objects.requireNonNull(WxPayHelper.class.getClassLoader().getResource("")).getPath(); + if (clazzPath == null) { + log.error("【微信】未找到证书"); + } + LocalHttpClient.initMchKeyStore(mchId, clazzPath + API_CLIENT_CERT_NAME); } - LocalHttpClient.initMchKeyStore(mchId, clazzPath + API_CLIENT_CERT_NAME); } } catch (Exception e) { ErrorHelper.println(e); diff --git a/src/main/resources/wx.properties b/src/main/resources/wx.properties index 143a229..8b43a81 100644 --- a/src/main/resources/wx.properties +++ b/src/main/resources/wx.properties @@ -15,7 +15,6 @@ wx.web_static_path=/web # \u9000\u8D39\u5BC6\u7801 wx.password=ynxbd@6910 - # \u662F\u5426\u4E3A\u6D4B\u8BD5\u73AF\u5883================================= wx.is_dev=true