尚医通-微信支付
创始人
2024-03-13 03:03:57
0

流程梳理

image-20221202102602753

依赖和工具类

com.github.wxpaywxpay-sdk0.0.3
com.examplecommon_utils0.0.1-SNAPSHOT

配置文件

#??????????appid
weixin.appid=wx74862e0dfcf69954
#?????
weixin.partner=1558950191
#???key
weixin.partnerkey=T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
#??????
weixin.cert=/Users/zhouzhou/IdeaProjects/yygh_parent/cert/apiclient_cert.p12

常量读取

@Component
public class ConstantPropertiesUtils implements InitializingBean {@Value("${weixin.appid}")private String appid;@Value("${weixin.partner}")private String partner;@Value("${weixin.partnerkey}")private String partnerkey;@Value("${weixin.cert}")private String cert;public static String APPID;public static String PARTNER;public static String PARTNERKEY;public static String CERT;@Overridepublic void afterPropertiesSet() throws Exception {APPID = appid;PARTNER = partner;PARTNERKEY = partnerkey;CERT = cert;}
}

http 请求封装

/*** http请求客户端*/
public class HttpClient {private String url;private Map param;private int statusCode;private String content;private String xmlParam;private boolean isHttps;private boolean isCert = false;//证书密码 微信商户号(mch_id)private String certPassword;public boolean isHttps() {return isHttps;}public void setHttps(boolean isHttps) {this.isHttps = isHttps;}public boolean isCert() {return isCert;}public void setCert(boolean cert) {isCert = cert;}public String getXmlParam() {return xmlParam;}public void setXmlParam(String xmlParam) {this.xmlParam = xmlParam;}public HttpClient(String url, Map param) {this.url = url;this.param = param;}public HttpClient(String url) {this.url = url;}public String getCertPassword() {return certPassword;}public void setCertPassword(String certPassword) {this.certPassword = certPassword;}public void setParameter(Map map) {param = map;}public void addParameter(String key, String value) {if (param == null)param = new HashMap();param.put(key, value);}public void post() throws ClientProtocolException, IOException {HttpPost http = new HttpPost(url);setEntity(http);execute(http);}public void put() throws ClientProtocolException, IOException {HttpPut http = new HttpPut(url);setEntity(http);execute(http);}public void get() throws ClientProtocolException, IOException {if (param != null) {StringBuilder url = new StringBuilder(this.url);boolean isFirst = true;for (String key : param.keySet()) {if (isFirst)url.append("?");elseurl.append("&");url.append(key).append("=").append(param.get(key));}this.url = url.toString();}HttpGet http = new HttpGet(url);execute(http);}/*** set http post,put param*/private void setEntity(HttpEntityEnclosingRequestBase http) {if (param != null) {List nvps = new LinkedList();for (String key : param.keySet())nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数}if (xmlParam != null) {http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));}}private void execute(HttpUriRequest http) throws ClientProtocolException,IOException {CloseableHttpClient httpClient = null;try {if (isHttps) {if(isCert) {FileInputStream inputStream = new FileInputStream(new File(ConstantPropertiesUtils.CERT));KeyStore keystore = KeyStore.getInstance("PKCS12");char[] partnerId2charArray = certPassword.toCharArray();keystore.load(inputStream, partnerId2charArray);SSLContext sslContext = SSLContexts.custom().loadKeyMaterial(keystore, partnerId2charArray).build();SSLConnectionSocketFactory sslsf =new SSLConnectionSocketFactory(sslContext,new String[] { "TLSv1" },null,SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();} else {SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {// 信任所有public boolean isTrusted(X509Certificate[] chain,String authType)throws CertificateException {return true;}}).build();SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext);httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();}} else {httpClient = HttpClients.createDefault();}CloseableHttpResponse response = httpClient.execute(http);try {if (response != null) {if (response.getStatusLine() != null)statusCode = response.getStatusLine().getStatusCode();HttpEntity entity = response.getEntity();// 响应内容content = EntityUtils.toString(entity, Consts.UTF_8);}} finally {response.close();}} catch (Exception e) {e.printStackTrace();} finally {httpClient.close();}}public int getStatusCode() {return statusCode;}public String getContent() throws IOException {return content;}
}

生成二维码

后端实现

根据订单号生成

/*** 根据订单号下单,生成支付链接*/@Overridepublic Map createNative(Long orderId) {try {Map payMap = (Map) redisTemplate.opsForValue().get(orderId.toString());if(null != payMap) return payMap;//根据id获取订单信息OrderInfo order = orderService.getById(orderId);// 保存交易记录paymentService.savePaymentInfo(order, PaymentTypeEnum.WEIXIN.getStatus());//1、设置参数Map paramMap = new HashMap();paramMap.put("appid", ConstantPropertiesUtils.APPID);paramMap.put("mch_id", ConstantPropertiesUtils.PARTNER);paramMap.put("nonce_str", WXPayUtil.generateNonceStr());String body = order.getReserveDate() + "就诊" + order.getDepname();paramMap.put("body", body);paramMap.put("out_trade_no", order.getOutTradeNo());//paramMap.put("total_fee", order.getAmount().multiply(new BigDecimal("100")).longValue()+"");paramMap.put("total_fee", "1"); // 为了测试 设置为 0.01 元paramMap.put("spbill_create_ip", "127.0.0.1");paramMap.put("notify_url", "http://guli.shop/api/order/weixinPay/weixinNotify");paramMap.put("trade_type", "NATIVE");//2、HTTPClient来根据URL访问第三方接口并且传递参数HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/unifiedorder");//client设置参数client.setXmlParam(WXPayUtil.generateSignedXml(paramMap, ConstantPropertiesUtils.PARTNERKEY));client.setHttps(true);client.post();//3、返回第三方的数据String xml = client.getContent();Map resultMap = WXPayUtil.xmlToMap(xml);//4、封装返回结果集Map map = new HashMap<>();System.out.println(map);map.put("orderId", orderId);map.put("totalFee", order.getAmount());map.put("resultCode", resultMap.get("result_code"));map.put("codeUrl", resultMap.get("code_url"));if(null != resultMap.get("result_code")) {//微信支付二维码2小时过期,可采取2小时未支付取消订单redisTemplate.opsForValue().set(orderId.toString(), map, 1000, TimeUnit.MINUTES);}return map;} catch (Exception e) {throw new RuntimeException(e);}}

值得说的是,这里加入了 redis ,避免统一订单号多次生成二维码,和短信验证码登录这块的思路是一致的

再就是二维码调用流程,这块基本上是固定的,思路和调用短信服务差不多,都是调用第三方服务api

前端实现

npm install vue-qriously

全局引入

import VueQrious from 'vue-qriously'
Vue.use(VueQrious)

dialog中展示

请使用微信扫一扫
扫描二维码支付

js调用

//生成支付二维码pay() {this.dialogPayVisible = true;weixinApi.createNative(this.orderId).then((response) => {this.payObj = response.data;if (this.payObj.codeUrl == "") {this.dialogPayVisible = false;this.$message.error("支付错误");} else {this.timer = setInterval(() => {this.queryPayStatus(this.orderId);}, 3000);}});},//查询支付状态queryPayStatus(orderId) {weixinApi.queryPayStatus(orderId).then((response) => {if (response.message == "支付中") {return;}clearInterval(this.timer);window.location.reload();});},

这里需要注意的是,生成二维码后,添加一个定时器,查询支付状态

查询支付状态

@ApiOperation(value = "查询支付状态")@GetMapping("/queryPayStatus/{orderId}")public Result queryPayStatus(@ApiParam(name = "orderId", value = "订单id", required = true)@PathVariable("orderId") Long orderId) {//调用查询接口Map resultMap = weixinPayService.queryPayStatus(orderId, PaymentTypeEnum.WEIXIN.name());if (resultMap == null) {//出错return Result.fail().message("支付出错");}if ("SUCCESS".equals(resultMap.get("trade_state"))) {//如果成功//更改订单状态,处理支付结果String out_trade_no = resultMap.get("out_trade_no");paymentService.paySuccess(out_trade_no, PaymentTypeEnum.WEIXIN.getStatus(), resultMap);return Result.ok().message("支付成功");}return Result.ok().message("支付中");}

Service 实现

/*** 支付成功*/@Overridepublic void paySuccess(String outTradeNo, Integer paymentType, Map paramMap) {PaymentInfo paymentInfo = this.getPaymentInfo(outTradeNo, paymentType);if (null == paymentInfo) {throw new HospitalException(ResultCodeEnum.PARAM_ERROR);}if (!Objects.equals(paymentInfo.getPaymentStatus(), PaymentStatusEnum.UNPAID.getStatus())) {return;}//修改支付状态PaymentInfo paymentInfoUpd = new PaymentInfo();paymentInfoUpd.setPaymentStatus(PaymentStatusEnum.PAID.getStatus());paymentInfoUpd.setTradeNo(paramMap.get("transaction_id"));paymentInfoUpd.setCallbackTime(new Date());paymentInfoUpd.setCallbackContent(paramMap.toString());this.updatePaymentInfo(outTradeNo, paymentInfoUpd);//修改订单状态OrderInfo orderInfo = orderService.getById(paymentInfo.getOrderId());orderInfo.setOrderStatus(OrderStatusEnum.PAID.getStatus());orderService.updateById(orderInfo);// 调用医院接口,通知更新支付状态SignInfoVo signInfoVo= hospFeignClient.getSignInfoVo(orderInfo.getHoscode());if (null == signInfoVo) {throw new HospitalException(ResultCodeEnum.PARAM_ERROR);}Map reqMap = new HashMap<>();reqMap.put("hoscode", orderInfo.getHoscode());reqMap.put("hosRecordId", orderInfo.getHosRecordId());reqMap.put("timestamp", HttpRequestHelper.getTimestamp());String sign = HttpRequestHelper.getSign(reqMap, signInfoVo.getSignKey());reqMap.put("sign", sign);JSONObject result = HttpRequestHelper.sendRequest(reqMap, signInfoVo.getApiUrl() + "/order/updatePayStatus");if (result.getInteger("code") != 200) {throw new HospitalException(result.getString("message"), ResultCodeEnum.FAIL.getCode());}}/*** 获取支付记录*/private PaymentInfo getPaymentInfo(String outTradeNo, Integer paymentType) {QueryWrapper queryWrapper = new QueryWrapper<>();queryWrapper.eq("out_trade_no", outTradeNo);queryWrapper.eq("payment_type", paymentType);return baseMapper.selectOne(queryWrapper);}/*** 更改支付记录*/private void updatePaymentInfo(String outTradeNo, PaymentInfo paymentInfoUpd) {QueryWrapper queryWrapper = new QueryWrapper<>();queryWrapper.eq("out_trade_no", outTradeNo);baseMapper.update(paymentInfoUpd, queryWrapper);}

效果演示

image-20221202111003523

点击支付后出现二维码

扫码后成功返回,不扫码会一直定时器调用支付状态查询接口,这块我其实感觉抛出错误的话,界面展示不是很人性化

取消预约

取消订单分两种情况:

1、未支付取消订单,直接通知医院更新取消预约状态

2、已支付取消订单,先退款给用户,然后通知医院更新取消预约状态

image-20221202114201264
@ApiOperation(value = "取消预约")
@GetMapping("auth/cancelOrder/{orderId}")
public Result cancelOrder(@ApiParam(name = "orderId", value = "订单id", required = true)@PathVariable("orderId") Long orderId) {return Result.ok(orderService.cancelOrder(orderId));
}

service 实现

@Overridepublic Boolean cancelOrder(Long orderId) {OrderInfo orderInfo = this.getById(orderId);//1. 当前时间大于退号时间,不能取消预约DateTime quitTime = new DateTime(orderInfo.getQuitTime());if (quitTime.isBeforeNow()) {throw new HospitalException(ResultCodeEnum.CANCEL_ORDER_NO);}// 2.向医院系统发送取消预约的请求SignInfoVo signInfoVo = hospFeignClient.getSignInfoVo(orderInfo.getHoscode());if (null == signInfoVo) {throw new HospitalException(ResultCodeEnum.PARAM_ERROR);}Map reqMap = new HashMap<>();reqMap.put("hoscode", orderInfo.getHoscode());reqMap.put("hosRecordId", orderInfo.getHosRecordId());reqMap.put("timestamp", HttpRequestHelper.getTimestamp());String sign = HttpRequestHelper.getSign(reqMap, signInfoVo.getSignKey());reqMap.put("sign", sign);JSONObject result = HttpRequestHelper.sendRequest(reqMap, signInfoVo.getApiUrl() + "/order/updateCancelStatus");if (result.getInteger("code") != 200) {throw new HospitalException(result.getString("message"), ResultCodeEnum.FAIL.getCode());} else {//3. 如果成功 判断是否支付 已经支付则退款if (orderInfo.getOrderStatus().intValue() == OrderStatusEnum.PAID.getStatus().intValue()) {//已支付 退款boolean isRefund = weixinService.refund(orderId);if (!isRefund) {throw new HospitalException(ResultCodeEnum.CANCEL_ORDER_FAIL);}}//4.更改订单状态orderInfo.setOrderStatus(OrderStatusEnum.CANCLE.getStatus());this.updateById(orderInfo);//5.发送mq信息更新预约数 我们与下单成功更新预约数使用相同的mq信息,不设置可预约数与剩余预约数,接收端可预约数加1即可OrderMqVo orderMqVo = new OrderMqVo();orderMqVo.setScheduleId(orderInfo.getScheduleId());//同时设置短信提示MsmVo msmVo = new MsmVo();msmVo.setPhone(orderInfo.getPatientPhone());msmVo.setTemplateCode("SMS_194640722");String reserveDate = new DateTime(orderInfo.getReserveDate()).toString("yyyy-MM-dd") + (orderInfo.getReserveTime() == 0 ? "上午" : "下午");Map param = new HashMap() {{put("title", orderInfo.getHosname() + "|" + orderInfo.getDepname() + "|" + orderInfo.getTitle());put("reserveDate", reserveDate);put("name", orderInfo.getPatientName());}};msmVo.setParam(param);orderMqVo.setMsmVo(msmVo);rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_ORDER, MqConst.ROUTING_ORDER, orderMqVo);}return true;}

这块的逻辑还是比较复杂的,主要看两个地方

  • 微信退款的实现
  • mq的整合

微信退款的实现

简单列一下实现方法,比较固定,和前面生成二维码接口类似

@Overridepublic Boolean refund(Long orderId) {try {PaymentInfo paymentInfoQuery = paymentService.getPaymentInfo(orderId, PaymentTypeEnum.WEIXIN.getStatus());RefundInfo refundInfo = refundInfoService.saveRefundInfo(paymentInfoQuery);if (refundInfo.getRefundStatus().intValue() == RefundStatusEnum.REFUND.getStatus().intValue()) {return true;}Map paramMap = new HashMap<>(8);paramMap.put("appid", ConstantPropertiesUtils.APPID);       //公众账号IDparamMap.put("mch_id", ConstantPropertiesUtils.PARTNER);   //商户编号paramMap.put("nonce_str", WXPayUtil.generateNonceStr());paramMap.put("transaction_id", paymentInfoQuery.getTradeNo()); //微信订单号paramMap.put("out_trade_no", paymentInfoQuery.getOutTradeNo()); //商户订单编号paramMap.put("out_refund_no", "tk" + paymentInfoQuery.getOutTradeNo()); //商户退款单号
//       paramMap.put("total_fee",paymentInfoQuery.getTotalAmount().multiply(new BigDecimal("100")).longValue()+"");
//       paramMap.put("refund_fee",paymentInfoQuery.getTotalAmount().multiply(new BigDecimal("100")).longValue()+"");paramMap.put("total_fee", "1");paramMap.put("refund_fee", "1");String paramXml = WXPayUtil.generateSignedXml(paramMap, ConstantPropertiesUtils.PARTNERKEY);HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/secapi/pay/refund");client.setXmlParam(paramXml);client.setHttps(true);client.setCert(true);client.setCertPassword(ConstantPropertiesUtils.PARTNER);client.post();
//3、返回第三方的数据String xml = client.getContent();Map resultMap = WXPayUtil.xmlToMap(xml);if (null != resultMap && WXPayConstants.SUCCESS.equalsIgnoreCase(resultMap.get("result_code"))) {refundInfo.setCallbackTime(new Date());refundInfo.setTradeNo(resultMap.get("refund_id"));refundInfo.setRefundStatus(RefundStatusEnum.REFUND.getStatus());refundInfo.setCallbackContent(JSONObject.toJSONString(resultMap));refundInfoService.updateById(refundInfo);return true;}return false;} catch (Exception e) {e.printStackTrace();}return false;}

mq的整合

这块还是复用之前的方式,具体可以看我上一篇博客 尚医通-预约下单中rabbitmq的使用_周周写不完的代码的博客-CSDN博客

简单看一下订单生成和订单取消是如何复用一套逻辑的

先看订单生成

OrderMqVo orderMqVo = new OrderMqVo();
orderMqVo.setScheduleId(scheduleId);
orderMqVo.setReservedNumber(reservedNumber);
orderMqVo.setAvailableNumber(availableNumber);

对比下订单取消

OrderMqVo orderMqVo = new OrderMqVo();
orderMqVo.setScheduleId(orderInfo.getScheduleId());

看下 监听器

if (null != orderMqVo.getAvailableNumber()) {//下单成功更新预约数Schedule schedule = scheduleService.getScheduleId(orderMqVo.getScheduleId());schedule.setReservedNumber(orderMqVo.getReservedNumber());schedule.setAvailableNumber(orderMqVo.getAvailableNumber());scheduleService.update(schedule);
} else {//取消预约更新预约数Schedule schedule = scheduleService.getScheduleId(orderMqVo.getScheduleId());int availableNumber = schedule.getAvailableNumber().intValue() + 1;schedule.setAvailableNumber(availableNumber);scheduleService.update(schedule);
}

可以看到 订单取消不会设置 AvailableNumber,只设置了 id

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
群晖外网访问终极解决方法:IP... 写在前面的话 受够了群晖的quickconnet的小水管了,急需一个新的解决方法&#x...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
Azure构建流程(Power... 这可能是由于配置错误导致的问题。请检查构建流程任务中的“发布构建制品”步骤,确保正确配置了“Arti...