流程梳理
依赖和工具类
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>common_utils</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
配置文件
#??????????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;
@Override
public void afterPropertiesSet() throws Exception {
APPID = appid;
PARTNER = partner;
PARTNERKEY = partnerkey;
CERT = cert;
}
}
http 请求封装
/**
* http请求客户端
*/
public class HttpClient {
private String url;
private Map<String, String> 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<String, String> 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<String, String> map) {
param = map;
}
public void addParameter(String key, String value) {
if (param == null)
param = new HashMap<String, String>();
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("?");
else
url.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<NameValuePair> nvps = new LinkedList<NameValuePair>();
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;
}
}
生成二维码
后端实现
根据订单号生成
/**
* 根据订单号下单,生成支付链接
*/
@Override
public 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<String, String> 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中展示
<el-dialog
:visible.sync="dialogPayVisible"
style="text-align: left"
:append-to-body="true"
width="500px"
@close="closeDialog"
>
<div class="container">
<div class="operate-view" style="height: 350px">
<div class="wrapper wechat">
<div>
<!-- 显示支付二维码 -->
<qriously :value="payObj.codeUrl" :size="220" />
<!-- <img src="images/weixin.jpg" alt="" /> -->
<div
style="
text-align: center;
line-height: 25px;
margin-bottom: 40px;
"
>
请使用微信扫一扫<br />
扫描二维码支付
</div>
</div>
</div>
</div>
</div>
</el-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<String, String> 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 实现
/**
* 支付成功
*/
@Override
public void paySuccess(String outTradeNo, Integer paymentType, Map<String, String> 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<String, Object> 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<PaymentInfo> 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<PaymentInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("out_trade_no", outTradeNo);
baseMapper.update(paymentInfoUpd, queryWrapper);
}
效果演示
点击支付后出现二维码
扫码后成功返回,不扫码会一直定时器调用支付状态查询接口,这块我其实感觉抛出错误的话,界面展示不是很人性化
取消预约
取消订单分两种情况:
1、未支付取消订单,直接通知医院更新取消预约状态
2、已支付取消订单,先退款给用户,然后通知医院更新取消预约状态
@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 实现
@Override
public 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<String, Object> 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<String, Object> param = new HashMap<String, Object>() {{
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的整合
微信退款的实现
简单列一下实现方法,比较固定,和前面生成二维码接口类似
@Override
public 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<String, String> paramMap = new HashMap<>(8);
paramMap.put("appid", ConstantPropertiesUtils.APPID); //公众账号ID
paramMap.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<String, String> 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