支付流程分析
1.用户下单之后,订单数据会存入到MySQL中,同时会将订单对应的支付日志存入到Redis,以队列的方式存储。
2.用户下单后,进入支付页面,支付页面调用支付系统,从微信支付获取二维码数据,并在页面生成支付二维码。
3.用户扫码支付后,微信支付服务器会通调用前预留的回调地址,并携带支付状态信息。
4.支付系统接到支付状态信息后,将支付状态信息发送给RabbitMQ
5.订单系统监听RabbitMQ中的消息获取支付状态,并根据支付状态修改订单状态
6.为了防止网络问题导致notifyurl没有接到对应数据,定时任务定时获取Redis中队列数据去微信支付接口查询状态,并定时更新对应状态。
购物车数据存redis,订单数据不存redis,因为现在微信同一个订单号可以生成多次二维码
流层以下图为准:
我们通过订单系统下单,然后订单系统调用支付系统去向微信支付的服务器发送请求,然后获取二
维码返回给用户,然后订单系统就开始监听MQ。
用户扫码支付后,支付系统将支付状态存进MQ中。订单系统检测到用户已经付钱了,就将订单设
为已支付,然后存进MySQL中。
可能会因为网络问题导致订单系统获取不到支付状态,所以订单系统会定时向微信支付服务器发送
请求去查询订单状态。
二维码创建(了解)
qrious是一款基于HTML5 Canvas的纯JS二维码生成插件。通过qrious.js可以快速生成各种二维
码,你可以控制二维码的尺寸颜色,还可以将生成的二维码进行Base64编码。
qrious.js二维码插件的可用配置参数如下:
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
background | String | “white” | 二维码的背景颜色。 |
foreground | String | “black” | 二维码的前景颜色。 |
level | String | “L” | 二维码的误差校正级别(L, M, Q, H)。 |
mime | String | “image/png” | 二维码输出为图片时的MIME类型。 |
size | Number | 100 | 二维码的尺寸,单位像素。 |
value | String | "" | 需要编码为二维码的值 |
例子:注意要引入qrious.js
<html>
<head>
<title>二维码入门小demo</title>
</head>
<body>
<img id="qrious">
<script src="qrious.js"></script>
<script>
var qr = new QRious({
element:document.getElementById('qrious'),
size:250,
level:'H',
value:'http://www.itheima.com'
});
</script>
</body>
</html>
微信扫码支付简介
微信扫码支付是商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模
式。该模式适用于PC网站支付、实体店单品或订单支付、媒体广告支付等场景。
申请步骤:(了解)
第一步:注册公众号(类型须为:服务号)
请根据营业执照类型选择以下主体注册:个体工商户| 企业/公司| 政府| 媒体| 其他类型。
第二步:认证公众号
公众号认证后才可申请微信支付,认证费:300元/次。
第三步:提交资料申请微信支付
登录公众平台,点击左侧菜单【微信支付】,开始填写资料等待审核,审核时间为1-5个工作日内。
第四步:开户成功,登录商户平台进行验证
资料审核通过后,请登录联系人邮箱查收商户号和密码,并登录商户平台填写财付通备付金打的小额资金数额,完成账户验证。
第五步:在线签署协议
本协议为线上电子协议,签署后方可进行交易及资金结算,签署完立即生效。
开发文档
微信支付接口调用的整体思路:
按API要求组装参数,以XML方式发送(POST)给微信支付接口(URL),微信支付接口也是以
XML方式给予响应。程序根据返回的结果(其中包括支付URL)生成二维码或判断订单状态。
在线微信支付开发文档:
https://pay.weixin.qq.com/wiki/doc/api/index.html
1. appid:微信公众账号或开放平台APP的唯一标识
2. mch_id:商户号 (配置文件中的partner)
3. partnerkey:商户密钥
4. sign:数字签名, 根据微信官方提供的密钥和一套算法生成的一个加密信息, 就是为了保证交易的安全性
微信支付还有模式一和模式二,
模式二适合线上支付,支付一次二维码就不能用了,
模式一的二维码可以一直使用,比如用于自动售货机。
现在官网上直接是开发指引,没有区分模式一模式二
微信支付流程分析
微信支付SDK
使用微信支付SDK,在maven工程中引入依赖
HttpClient工具类
HttpClient是Apache Jakarta Common下的子项目,用来提供高效的、最新的、功能丰富的支持HTTP协议的客户端编程工具包,并且它支
持HTTP协议最新的版本和建议。
HttpClient已经应用在很多的项目中,比如Apache Jakarta上很著名的另外两个开源项目Cactus和HTMLUnit都使用了HttpClient。
HttpClient通俗的讲就是模拟了浏览器的行为,如果我们需要在后端向某一地址提交数据获取结果,就可以使用HttpClient
关于HttpClient(原生)具体的使用不属于我们本章的学习内容,我们这里这里为了简化HttpClient的使用,提供了工具类HttpClient(对原
生HttpClient进行了封装)
HttpClient工具类代码:
public class HttpClient {
private String url;
private Map<String, String> param;
private int statusCode;
private String content;
private String xmlParam;
private boolean isHttps;
public boolean isHttps() {
return isHttps;
}
public void setHttps(boolean isHttps) {
this.isHttps = isHttps;
}
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 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) {
SSLContext sslContext = new SSLContextBuilder()
.loadTrustMaterial(null, new TrustStrategy() {
// 信任所有
@Override
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 ParseException, IOException {
return content;
}
}
HttpClient工具类使用的步骤
HttpClient client=new HttpClient(请求的url地址);
client.setHttps(true);//是否是https协议
client.setXmlParam(xmlParam);//发送的xml数据
client.post();//执行post请求
String result = client.getContent(); //获取结果
将HttpClient工具包放到common工程下并引入依赖,引入依赖后就可以直接使用上述的工具包了。
支付微服务搭建
(1)创建changgou-service-pay
(2)application.yml
创建application.yml,配置文件如下:
server:
port: 18089
spring:
application:
name: pay
main:
allow-bean-definition-overriding: true
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
#hystrix 配置
hystrix:
command:
default:
execution:
timeout:
#如果enabled设置为false,则请求超时交给ribbon控制
enabled: true
isolation:
strategy: SEMAPHORE
#微信支付信息配置
weixin:
appid: wx8397f8696b538317
partner: 1473426802
partnerkey: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
notifyurl: http://www.itcast.cn
appid: 微信公众账号或开放平台APP的唯一标识
partner:财付通平台的商户账号
partnerkey:财付通平台的商户密钥
notifyurl: 回调地址
(3)启动类创建
在changgou-service-pay
中创建com.changgou.WeixinPayApplication
,代码如下:
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
@EnableEurekaClient
public class WeixinPayApplication {
public static void main(String[] args) {
SpringApplication.run(WeixinPayApplication.class,args);
}
}
在支付页面上生成支付二维码,并显示订单号和金额,用户拿出手机,打开微信扫描页面上的二维码,然后在微信中完成支付
实现思路
通过HttpClient工具类实现对远程支付接口的调用。
接口链接:https://api.mch.weixin.qq.com/pay/unifiedorder
具体参数参见“统一下单”API, 构建参数发送给统一下单的url ,返回的信息中有支付url,根据url生成二维码,显示的订单号和金额也在返回
的信息中。
统一下单API讲解:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_1
代码实现
控制层
创建com.changgou.controller.WeixinPayController
,主要调用WeixinPayService的方法获取创建二维码的信息
@RestController
@RequestMapping(value = "/weixin/pay")
@CrossOrigin
public class WeixinPayController {
@Autowired
private WeixinPayService weixinPayService;
/***
* 创建二维码
* @return
*/
@RequestMapping(value = "/create/native")
public Result createNative(String outtradeno, String money){
Map<String,String> resultMap = weixinPayService.createNative(outtradeno,money);
return new Result(true, StatusCode.OK,"创建二维码预付订单成功!",resultMap);
}
}
业务层
新增com.changgou.service.WeixinPayService
接口
public interface WeixinPayService {
/*****
* 创建二维码
* @param out_trade_no : 客户端自定义订单编号
* @param total_fee : 交易金额,单位:分
* @return
*/
public Map createNative(String out_trade_no, String total_fee);
}
创建com.changgou.service.impl.WeixinPayServiceImpl
类,并发送Post请求获取预支付信息,包含二维码扫码支付地址
@Service
public class WeixinPayServiceImpl implements WeixinPayService {
@Value("${weixin.appid}")
private String appid;
@Value("${weixin.partner}")
private String partner;
@Value("${weixin.partnerkey}")
private String partnerkey;
@Value("${weixin.notifyurl}")
private String notifyurl;
/****
* 创建二维码
* @param out_trade_no : 客户端自定义订单编号
* @param total_fee : 交易金额,单位:分
* @return
*/
@Override
public Map createNative(String out_trade_no, String total_fee){
try {
//1、封装参数
Map param = new HashMap();
param.put("appid", appid); //应用ID
param.put("mch_id", partner); //商户ID号
param.put("nonce_str", WXPayUtil.generateNonceStr()); //随机数
param.put("body", "畅购"); //订单描述
param.put("out_trade_no",out_trade_no); //商户订单号
param.put("total_fee", total_fee); //交易金额
param.put("spbill_create_ip", "127.0.0.1"); //终端IP
param.put("notify_url", notifyurl); //回调地址
param.put("trade_type", "NATIVE"); //交易类型
//2、将参数转成xml字符,并携带签名
String paramXml = WXPayUtil.generateSignedXml(param, partnerkey);
///3、执行请求
HttpClient httpClient = new HttpClient("https://api.mch.weixin.qq.com/pay/unifiedorder");
httpClient.setHttps(true);
httpClient.setXmlParam(paramXml);
httpClient.post();
//4、获取参数
String content = httpClient.getContent();
Map<String, String> stringMap = WXPayUtil.xmlToMap(content);
System.out.println("stringMap:"+stringMap);
//5、获取部分页面所需参数
Map<String,String> dataMap = new HashMap<String,String>();
dataMap.put("code_url",stringMap.get("code_url"));
dataMap.put("out_trade_no",out_trade_no);
dataMap.put("total_fee",total_fee);
return dataMap;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
@Value注解将配置文件中的几个参数注入
浏览器测试:http://localhost:18089/weixin/pay/create/native?outtradeno=1999996660000&money=1
这里我们订单号随便填的,金额暂时写死,后续开发我们再对接业务系统得到订单号和金额
"code_url":"weixin://wxpay/bizpayurl?pr=mriccNqzz"
打开支付页面/pay.html,修改value路径(改为上面测试结果中的code_url),然后打开,会出现二维码,可以扫码试试
检测支付状态(有时候可能因为网络原因导致支付状态没有及时返回到我们的服务器中,这个时候就要手动地去查询)
当用户支付成功后跳转到成功页面
当返回异常时跳转到错误页面
实现思路
我们通过HttpClient工具类实现对远程支付接口的调用。
接口链接:https://api.mch.weixin.qq.com/pay/orderquery
具体参数参见“查询订单”API, 我们在controller方法中轮询调用查询订单(间隔3秒),当返回状态为success时,我们会在controller方法返
回结果。前端代码收到结果后跳转到成功页面。
https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_2
代码实现
控制层
在com.changgou.controller.WeixinPayController
新增方法,用于查询支付状态
/***
* 查询支付状态
* @param outtradeno
* @return
*/
@GetMapping(value = "/status/query")
public Result queryStatus(String outtradeno){
Map<String,String> resultMap = weixinPayService.queryPayStatus(outtradeno);
return new Result(true,StatusCode.OK,"查询状态成功!",resultMap);
}
业务层
修改com.changgou.service.WeixinPayService
,新增方法
/***
* 查询订单状态
* @param out_trade_no : 客户端自定义订单编号
* @return
*/
public Map queryPayStatus(String out_trade_no);
在com.changgou.pay.service.impl.WeixinPayServiceImpl中增加实现方法
/***
* 查询订单状态
* @param out_trade_no : 客户端自定义订单编号
* @return
*/
@Override
public Map queryPayStatus(String out_trade_no) {
try {
//1.封装参数
Map param = new HashMap();
param.put("appid",appid); //应用ID
param.put("mch_id",partner); //商户号
param.put("out_trade_no",out_trade_no); //商户订单编号
param.put("nonce_str",WXPayUtil.generateNonceStr()); //随机字符
//2、将参数转成xml字符,并携带签名
String paramXml = WXPayUtil.generateSignedXml(param,partnerkey);
//3、发送请求
HttpClient httpClient = new HttpClient("https://api.mch.weixin.qq.com/pay/orderquery");
httpClient.setHttps(true);
httpClient.setXmlParam(paramXml);
httpClient.post();
//4、获取返回值,并将返回值转成Map
String content = httpClient.getContent();
return WXPayUtil.xmlToMap(content);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
测试:http://localhost:18089/weixin/pay/status/query?outtradeno=1999996660000
内网穿透
注册好花生壳,账号:ZHOUSHUVIP,密码:ZHOUSHU1***10**
配置:
https://console.hsk.oray.com/forward
测试:用http://41095008th.zicp.vip:25911/替换http://localhost:18089
http://41095008th.zicp.vip:25911/weixin/pay/status/query?outtradeno=1999996660000
补充对订单的操作:
Redis存储订单信息(总结的时候,提到意义已经不大了,现在微信允许一个订单号生成多次的二
维码)
每次添加订单后,会根据订单检查用户是否是否支付成功,我们不建议每次都操作数据库,每次操作数据库会增加数据库的负载,我们可
以选择将用户的订单信息存入一份到Redis中,提升读取速度。
修改changgou-service-order
微服务的com.changgou.order.service.impl.OrderServiceImpl
类中的add
方法,如果是线上支付,将用户订
单数据存入到Redis中,由于每次创建二维码,需要用到订单编号 ,所以也需要将添加的订单信息返回。
/**
* 增加Order
* 金额校验:后台校验
* @param order
*/
@Override
public Order add(Order order){
//...略
//修改库存
skuFeign.decrCount(order.getUsername());
//添加用户积分
userFeign.addPoints(2);
//线上支付,记录订单
if(order.getPayType().equalsIgnoreCase("1")){
//将支付记录存入到Reids namespace key value
redisTemplate.boundHashOps("Order").put(order.getId(),order);
}
//删除购物车信息
//redisTemplate.delete("Cart_" + order.getUsername());
return order;
}
修改com.changgou.order.controller.OrderController
的add方法,将订单对象返回,因为页面需要获取订单的金额和订单号用于创建二
维码,代码如下:
支付后修改订单状态
订单支付成功后,需要修改订单状态并持久化到数据库,修改订单的同时,需要将Redis中的订单删除,所以修改订单状态需要将订单日志
也传过来,实现代码如下:
修改com.changgou.order.service.OrderService,添加修改订单状态方法,代码如下:
/***
* 根据订单ID修改订单状态
* @param transactionid 交易流水号
* @param paytime 支付时间
* @param orderId 订单号
*/
void updateStatus(String orderId,String paytime,String transactionid);
修改com.changgou.order.service.impl.OrderServiceImpl,添加修改订单状态实现方法,代码如下:
/***
* 订单修改
* 1.修改支付时间
* 2.修改支付状态
* @param orderId
* @param transactionid 微信支付的交易流水号
* @param paytime 支付时间
*/
@Override
public void updateStatus(String orderId,String paytime,String transactionid) throws Exception {
SimpleDateFormat s = new SimpleDateFormat("yyyyMMddHHmmss");
Date payTimeInfo = s.parse(paytime);
//1.修改订单
Order order = orderMapper.selectByPrimaryKey(orderId);
order.setUpdateTime(new Date()); //时间也可以从微信接口返回过来,这里为了方便,我们就直接使用当前时间了
order.setPayTime(payTimeInfo); //支付时间
order.setTransactionId(transactionid); //交易流水号
order.setPayStatus("1"); //已支付
orderMapper.updateByPrimaryKeySelective(order);
//2.删除Redis中的订单记录
redisTemplate.boundHashOps("Order").delete(orderId);
}
删除订单
如果用户订单支付失败了,或者支付超时了,我们需要删除用户订单,删除订单的同时需要回滚库存,
修改changgou-service-order
的com.changgou.order.service.OrderService,添加删除订单方法,我们只需要将订单id传入进来即可实现
/***
* 删除订单操作
* @param id
*/
void deleteOrder(String id);
修改changgou-service-order
的com.changgou.order.service.impl.OrderServiceImpl,添加删除订单实现方法
/***
* 订单的删除操作(改状态)
*/
@Override
public void deleteOrder(String id) {
//改状态
Order order = (Order) redisTemplate.boundHashOps("Order").get(id);
order.setUpdateTime(new Date());
order.setPayStatus("2"); //支付失败
orderMapper.updateByPrimaryKeySelective(order);
//删除缓存
redisTemplate.boundHashOps("Order").delete(id);
回滚库存,调用goods微服务,具体实现findByOrderId、findBySkuIds、updateMap没有写,后面再完善
List<OrderItem> orderItems = orderItemMapper.findByOrderId(order.getId());
List<Long> skuIds = new ArrayList<>();
for (OrderItem orderItem : orderItems) {
skuIds.add(orderItem.getSkuId());
}
List<Sku> skuList = skuFeign.findBySkuIds(order.getSkuIds()).getData(); //数据库中对应的sku集合
Map<Long, Sku> skuMap = skuList.stream().collect(Collectors.toMap(Sku::getId, a -> a));
for (OrderItem orderItem : orderItems) {
Sku sku = skuMap.get(orderItem.getSkuId());
sku.setNum(sku.getNum()+orderItem.getNum()); //加库存
}
skuFeign.updateMap(skuMap);
}
支付结果回调通知
每次实现支付之后,微信支付都会将用户支付结果返回到指定路径,而指定路径是指创建二维码的时候填写的notifyurl
参数,
响应的数据以及相关文档参考一下地址:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_7&index=8
返回参数分析
通知参数如下:
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
返回状态码 | return_code | 是 | String(16) | SUCCESS | SUCCESS |
返回信息 | return_msg | 是 | String(128) | OK | OK |
以下字段在return_code为SUCCESS的时候有返回
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
公众账号ID | appid | 是 | String(32) | wx8888888888888888 | 微信分配的公众账号ID(企业号corpid即为此appId) |
业务结果 | result_code | 是 | String(16) | SUCCESS | SUCCESS/FAIL |
商户订单号 | out_trade_no | 是 | String(32) | 1212321211201407033568112322 | 商户系统内部订单号 |
微信支付订单号 | transaction_id | 是 | String(32) | 1217752501201407033233368018 | 微信支付订单号 |
响应分析
回调地址接收到数据后,需要响应信息给微信服务器,告知已经收到数据,不然微信服务器会再次发送4次请求推送支付信息。
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
返回状态码 | return_code | 是 | String(16) | SUCCESS | 请按示例值填写 |
返回信息 | return_msg | 是 | String(128) | OK | 请按示例值填写 |
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
</xml>
回调接收数据实现
修改changgou-service-pay
微服务的com.changgou.pay.controller.WeixinPayController,添加回调方法
/***
* 支付回调
* @param request
* @return
*/
@RequestMapping(value = "/notify/url")
public String notifyUrl(HttpServletRequest request){
InputStream inStream;
try {
//读取支付回调数据
inStream = request.getInputStream();
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, len);
}
outSteam.close();
inStream.close();
// 将支付回调数据转换成xml字符串
String result = new String(outSteam.toByteArray(), "utf-8");
//将xml字符串转换成Map结构
Map<String, String> map = WXPayUtil.xmlToMap(result);
//响应数据设置
Map respMap = new HashMap();
respMap.put("return_code","SUCCESS");
respMap.put("return_msg","OK");
return WXPayUtil.mapToXml(respMap);
} catch (Exception e) {
e.printStackTrace();
//记录错误日志
}
return null;
}
修改application.yml文件
测试:
注意要Debug模式启动支付微服务,回调方法中断点
http://41095008th.zicp.vip:25911/weixin/pay/create/native?outtradeno=19999966600001&money=1
"code_url":"weixin://wxpay/bizpayurl?pr=8nNqCQQzz"
浏览器打开,生成二维码
微信扫码支付成功后,会自动跳转回调方法:
http://41095008th.zicp.vip:25911/weixin/pay/notify/url
<xml>
<appid><![CDATA[wx8397f8696b538317]]></appid>
<bank_type><![CDATA[COMM_CREDIT]]></bank_type>
<cash_fee><![CDATA[1]]></cash_fee>
<fee_type><![CDATA[CNY]]></fee_type>
<is_subscribe><![CDATA[N]]></is_subscribe>
<mch_id><![CDATA[1473426802]]></mch_id>
<nonce_str><![CDATA[66739ce61a164451ae68968b400402e4]]></nonce_str>
<openid><![CDATA[oNpSGwZOZ68aQNLadCc6Vc_QUQqA]]></openid>
<out_trade_no><![CDATA[19999966600001]]></out_trade_no>
<result_code><![CDATA[SUCCESS]]></result_code>
<return_code><![CDATA[SUCCESS]]></return_code>
<sign><![CDATA[43E08D336882618E42B8B1C4B8E1DAE3]]></sign>
<time_end><![CDATA[20210711113743]]></time_end>
<total_fee>1</total_fee>
<trade_type><![CDATA[NATIVE]]></trade_type>
<transaction_id><![CDATA[4200001186202107116409146871]]></transaction_id>
</xml>
支付系统是独立于其他系统的服务,不做相关业务逻辑操作,只做支付处理,所以回调地址接收微信服务返回的支付状态后,
立即将消息发送给RabbitMQ,订单系统再监听支付状态数据,根据状态数据做出修改订单状态或者删除订单操作。
补充docker rabbitmq:
容器启动之后就可以访问web管理端了http://宿主机IP:15672,默认创建了一个 guest 用户,密码也是guest
提供的虚拟机中已经存在队列和交换机,删了等下重新建
实际开发中需要手动在这里创建队列和交换机 (考虑用程序创建可能会报错)
备注:如果docker中没有安装rabbitmq,可以自己建
docker pull docker.io/rabbitmq:3.7-management # 下载rabbitmq的镜像
docker run --name rabbitmq -d -p 15672:15672 -p 5672:5672 4b23cfb64730 # 安装rabbitmq,最后面的id是docker images查出来的
docker exec -it rabbitmq /bin/bash # 进入到rabbitmq中
rabbitmqctl add_user root 123456 # 添加一个名为root的用户,密码为123456
rabbitmqctl set_permissions -p / root ".*" ".*" ".*" # 赋予root用户所有权限
rabbitmqctl set_user_tags root administrator # 赋予root用户administrator角色
发送支付状态
(1)集成RabbitMQ
修改支付微服务,集成RabbitMQ,添加如下依赖:
<dependencies>
<!--加入ampq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>
我们建议在后台手动创建队列,并绑定队列。
如果使用程序创建队列,可以按照如下方式实现,修改application.yml,配置支付队列和交换机信息,代码如下:
#位置支付交换机和队列
mq:
pay:
exchange:
order: exchange.order
queue:
order: queue.order
routing:
key: queue.order
rabbitmq监听配置
创建队列以及交换机并让队列和交换机绑定,MQConfig
package com.changgou.mq;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
@Configuration
public class MQConfig {
@Autowired
private Environment env;
/***
* 创建DirectExchange交换机
* @return
*/
@Bean
public DirectExchange basicExchange(){
return new DirectExchange(env.getProperty("mq.pay.exchange.order"), true,false);
}
/***
* 创建队列
* @return
*/
@Bean(name = "queueOrder")
public Queue queueOrder(){
return new Queue(env.getProperty("mq.pay.queue.order"), true);
}
/****
* 队列绑定到交换机上
* @return
*/
@Bean
public Binding basicBinding(){
return BindingBuilder.bind(queueOrder()).to(basicExchange()).with(env.getProperty("mq.pay.routing.key"));
}
}
发送MQ消息
修改WeixinPayController中回调方法,在接到支付信息后,立即将支付信息发送给RabbitMQ,代码如下:
@Value("${mq.pay.exchange.order}")
private String exchange;
@Value("${mq.pay.queue.order}")
private String queue;
@Value("${mq.pay.routing.key}")
private String routing;
@Autowired
private RabbitTemplate rabbitTemplate;
//将消息发送给RabbitMQ
rabbitTemplate.convertAndSend(exchange,routing, JSON.toJSONString(map));
整个代码如下:
package com.changgou.controller;
import com.alibaba.fastjson.JSON;
import com.changgou.service.WeixinPayService;
import com.github.wxpay.sdk.WXPayUtil;
import entity.Result;
import entity.StatusCode;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping(value = "/weixin/pay")
@CrossOrigin
public class WeixinPayController {
@Value("${mq.pay.exchange.order}")
private String exchange;
@Value("${mq.pay.queue.order}")
private String queue;
@Value("${mq.pay.routing.key}")
private String routing;
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private WeixinPayService weixinPayService;
/***
* 创建二维码
* @return
*/
@RequestMapping(value = "/create/native")
public Result createNative(String outtradeno, String money){
Map<String,String> resultMap = weixinPayService.createNative(outtradeno,money);
return new Result(true, StatusCode.OK,"创建二维码预付订单成功!",resultMap);
}
/***
* 查询支付状态
* @param outtradeno
* @return
*/
@GetMapping(value = "/status/query")
public Result queryStatus(String outtradeno){
Map<String,String> resultMap = weixinPayService.queryPayStatus(outtradeno);
return new Result(true,StatusCode.OK,"查询状态成功!",resultMap);
}
/***
* 支付回调
* @param request
* @return
*/
@RequestMapping(value = "/notify/url")
public String notifyUrl(HttpServletRequest request){
InputStream inStream;
try {
//读取支付回调数据
inStream = request.getInputStream();
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, len);
}
outSteam.close();
inStream.close();
// 将支付回调数据转换成xml字符串
String result = new String(outSteam.toByteArray(), "utf-8");
//将xml字符串转换成Map结构
Map<String, String> map = WXPayUtil.xmlToMap(result);
//将消息发送给RabbitMQ
rabbitTemplate.convertAndSend(exchange,routing, JSON.toJSONString(map));
//响应数据设置
Map respMap = new HashMap();
respMap.put("return_code","SUCCESS");
respMap.put("return_msg","OK");
return WXPayUtil.mapToXml(respMap);
} catch (Exception e) {
e.printStackTrace();
//记录错误日志
}
return null;
}
}
监听MQ消息处理订单
在订单微服务中,我们需要监听MQ支付状态消息,并实现订单数据操作。
集成RabbitMQ
在订单微服务中,先集成RabbitMQ,再监听队列消息。
在pom.xml中引入如下依赖:
<!--加入ampq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
在application.yml中配置rabbitmq配置,代码如下:
在application.yml中配置队列名字,代码如下:
#位置支付交换机和队列
mq:
pay:
queue:
order: queue.order
监听消息修改订单
在订单微服务于中创建com.changgou.order.listener.OrderMessagelistener,并在该类中consumeMessage方法,用于监听消息,
并根据支付状态处理订单,代码如下:
package com.changgou.order.listener;
import com.alibaba.fastjson.JSON;
import com.changgou.order.service.OrderService;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@RabbitListener(queues = {"${mq.pay.queue.order}"})
public class OrderMessagelistener {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private OrderService orderService;
@Autowired
private WeChatPayFeign weChatPayFeign;
/***
* 接收消息
*/
@RabbitHandler
public void consumeMessage(String msg){
//将数据转成Map
Map<String,String> resultMap = JSON.parseObject(msg,Map.class);
System.out.println("监听到的支付结果"+resultMap);
//return_code=SUCCESS
String return_code = resultMap.get("return_code");
//业务结果
String result_code = resultMap.get("result_code");
//业务结果 result_code=SUCCESS/FAIL,修改订单状态
if(return_code.equalsIgnoreCase("success") ){
//获取订单号
String outtradeno = resultMap.get("out_trade_no");
//业务结果
if(result_code.equalsIgnoreCase("success")){
if(outtradeno != null){
//修改订单状态 out_trade_no
orderService.updateStatus(outtradeno,resultMap.get("time_end"),resultMap.get("transaction_id"));
}
}else{
//支付失败,调用微信api取消关闭订单
weChatPayFeign.closeOrder(outtradeno);
//删除订单(实际修改订单状态),回滚库存
orderService.deleteOrder(outtradeno);
}
}
}
}
取消(关闭)订单
用微信支付开发文档中提供的取消(关闭)订单API:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_3
应用场景
以下情况需要调用关单接口:商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付;
系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口。
注意:订单生成后不能马上调用关单接口,最短调用时间间隔为5分钟。
WeixinPayController
/**
* 取消订单
* @return
*/
@RequestMapping(value = "/cancel/order")
public Result<Map<String,String>> closeOrder(String outtradeno) throws Exception {
Map<String,String> resultMap = weixinPayService.cancelOrder(outtradeno);
return new Result(true, StatusCode.OK,"订单取消成功",resultMap);
}
/**
* 取消订单
* @param orderId
* @return
*/
Map cancelOrder(String orderId);
@Override
public Map cancelOrder(String orderId) {
try {
Map map = new HashMap( );
map.put("out_trade_no",orderId );
String paramXml = WXPayUtil.generateSignedXml(map,partnerkey);
HttpClient httpClient = new HttpClient("https://api.mch.weixin.qq.com/pay/closeorder");
httpClient.setHttps(true);
httpClient.setXmlParam(paramXml);
httpClient.post();
String content = httpClient.getContent();
return WXPayUtil.xmlToMap(content);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
想让监听支付结果的方法里调用取消订单的方法,可以使用 Feign调用:
新建一个api工程
package com.changgou.pay.feign;
import entity.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.Map;
@FeignClient("weixinpay")
@RequestMapping("/weixin/pay")
public interface WeChatPayFeign {
/**
* 关闭订单
* @param outtradeno 商户订单号
* @return
* @throws Exception
*/
@RequestMapping("/cancel/order")
Result<Map<String,String>> closeOrder(String outtradeno);
}
备注:也可以不用 HttpClient httpClient = new HttpClient("");这种方式
在pom.xml中引入微信支付的SDK
<!--微信支付-->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
https://github.com/objcoding/WXPay-SDK-Java
https://blog.csdn.net/remsqks/article/details/108586479
上面取消(关闭)订单代码传的参数不全,改进如下:
关闭支付
用户如果半个小时没有支付,我们会关闭支付订单,但在关闭之前,需要先关闭微信支付,防止中
途用户支付。
修改支付微服务的WeixinPayService,添加关闭支付方法,代码如下:
/***
* 关闭支付
* @param orderId
* @return
*/
Map<String,String> closePay(Long orderId) throws Exception;
修改WeixinPayServiceImpl,实现关闭微信支付方法,代码如下:
/***
* 关闭微信支付
* @param orderId
* @return
* @throws Exception
*/
@Override
public Map<String, String> closePay(Long orderId) throws Exception {
//参数设置
Map<String,String> paramMap = new HashMap<String,String>();
paramMap.put("appid",appid); //应用ID
paramMap.put("mch_id",partner); //商户编号
paramMap.put("nonce_str",WXPayUtil.generateNonceStr());//随机字符
paramMap.put("out_trade_no",String.valueOf(orderId)); //商家的唯一编号
//将Map数据转成XML字符
String xmlParam = WXPayUtil.generateSignedXml(paramMap,partnerkey);
//确定url
String url = "https://api.mch.weixin.qq.com/pay/closeorder";
//发送请求
HttpClient httpClient = new HttpClient(url);
//https
httpClient.setHttps(true);
//提交参数
httpClient.setXmlParam(xmlParam);
//提交
httpClient.post();
//获取返回数据
String content = httpClient.getContent();
//将返回数据解析成Map
return WXPayUtil.xmlToMap(content);
}
现在还有个问题,如果用户下单 30 分钟后还没有支付,需要把订单取消。(超过5分钟取消订
单,是可以实现的,微信规定5分钟内不可取消)
超时订单处理思路
RabbitMQ延时消息队列(文档参考第14天文档)
延时队列介绍
延时队列即放置在该队列里面的消息是不需要立即消费的,而是等待一段时间之后取出消费。
那么,为什么需要延迟消费呢?我们来看以下的场景
网上商城下订单后30分钟后没有完成支付,取消订单(如:淘宝、去哪儿网)
系统创建了预约之后,需要在预约时间到达前一小时提醒被预约的双方参会
系统中的业务失败之后,需要重试
这些场景都非常常见,我们可以思考,比如第二个需求,系统创建了预约之后,需要在预约时间到达前一小时提醒被预约的双方参会。那么一天之中肯定是会有很多个预约
的,时间也是不一定的,假设现在有1点 2点 3点 三个预约,如何让系统知道在当前时间等于0点 1点 2点给用户发送信息呢,是不是需要一个轮询,一直去查看所有的预约,
比对当前的系统时间和预约提前一小时的时间是否相等呢?这样做非常浪费资源而且轮询的时间间隔不好控制。如果我们使用延时消息队列呢,我们在创建时把需要通知的
预约放入消息中间件中,并且设置该消息的过期时间,等过期时间到达时再取出消费即可。
Rabbitmq实现延时队列一般而言有两种形式:
第一种方式:利用两个特性: Time To Live(TTL)、Dead Letter Exchanges(DLX)[A队列过期->转发给B队列]
第二种方式:利用rabbitmq中的插件x-delay-message
TTL DLX实现延时队列
TTL
RabbitMQ可以针对队列设置x-expires(则队列中所有的消息都有相同的过期时间)或者针对Message设置x-message-ttl(对消息进行单独设置,
每条消息TTL可以不同),来控制消息的生存时间,如果超时(两者同时设置以最先到期的时间为准),则消息变为dead letter(死信)
Dead Letter Exchanges(DLX)
RabbitMQ的Queue可以配置x-dead-letter-exchange和x-dead-letter-routing-key(可选)两个参数,如果队列内出现了dead letter,
则按照这两个参数重新路由转发到指定的队列。
x-dead-letter-exchange:出现dead letter之后将dead letter重新发送到指定exchange
x-dead-letter-routing-key:出现dead letter之后将dead letter重新按照指定的routing-key发送
新建QueueConfig
package com.changgou.order.mq.queue;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 延时队列
*/
@Configuration
public class QueueConfig {
/**
* 创建Queue1,延时队列,会过期,过期后将数据发送给Queue2
* @return
*/
@Bean
public Queue orderDelayQueue() {
return QueueBuilder.durable("orderDelayQueue")
.withArgument("x-dead-letter-exchange", "orderListenerExchange") // orderDelayQueue队列信息会过期,过期后进入到死信队列
.withArgument("x-dead-letter-routing-key", "orderListenerQueue")
.build();
}
/**
* 创建Queue2
*/
@Bean
public Queue orderListenerQueue() {
return new Queue("orderListenerQueue", true);
}
/***
* 创建交换机
*/
@Bean
public DirectExchange orderListenerExchange(){
return new DirectExchange("orderListenerExchange");
}
/***
* 对列Queue2绑定Exchange
*/
@Bean
public Binding orderListenerBinding(Queue orderListenerQueue, Exchange orderListenerExchange) {
return BindingBuilder.bind(orderListenerQueue)
.to(orderListenerExchange)
.with("orderListenerQueue").noargs();
}
}
OrderServiceImpl增加发送延时消息
public String add(Order order){
//1.获取购物车的相关数据(redis)
Map cartMap = cartService.list(order.getUsername());
List<OrderItem> orderItemList = (List<OrderItem>) cartMap.get("orderItemList");
//2.统计计算:总金额,总数量
//3.填充订单数据并保存到tb_order
order.setTotalNum((Integer) cartMap.get("totalNum"));
order.setTotalMoney((Integer) cartMap.get("totalMoney"));
order.setPayMoney((Integer) cartMap.get("totalMoney"));
order.setCreateTime(new Date());
order.setUpdateTime(new Date());
order.setBuyerRate("0"); // 0:未评价 1:已评价
order.setSourceType("1"); //1:WEB
order.setOrderStatus("0"); //0:未完成 1:已完成 2:已退货
order.setPayStatus("0"); //0:未支付 1:已支付
order.setConsignStatus("0"); //0:未发货 1:已发货
String orderId = idWorker.nextId()+"";
order.setId(orderId);
orderMapper.insertSelective(order);
//4.填充订单项数据并保存到tb_order_item
for (OrderItem orderItem : orderItemList) {
orderItem.setId(idWorker.nextId()+"");
orderItem.setIsReturn("0"); //0:未退货 1:已退货
orderItem.setOrderId(orderId);
orderItemMapper.insertSelective(orderItem);
}
//扣减库存并增加销量
skuFeign.decrCount(order.getUsername());
//int i =1/0;
//添加任务数据
System.out.println("向订单数据库中的任务表去添加任务数据");
Task task = new Task();
task.setCreateTime(new Date());
task.setUpdateTime(new Date());
task.setMqExchange(RabbitMQConfig.EX_BUYING_ADDPOINTUSER);
task.setMqRoutingkey(RabbitMQConfig.CG_BUYING_ADDPOINT_KEY);
Map map = new HashMap();
map.put("username",order.getUsername());
map.put("orderId",orderId);
map.put("point",order.getPayMoney());
task.setRequestBody(JSON.toJSONString(map));
taskMapper.insertSelective(task);
//5.删除购物车数据(redis)
redisTemplate.delete("cart_"+order.getUsername());
//发送延迟消息
。。。。。。
return orderId;
}
新建监听DelayMessageListener
测试OK
实际开发中应该监听后执行相关操作:
@Component
@RabbitListener(queues = "orderListenerQueue")
public class DelayMessageListener {
@Autowired
private OrderService orderService;
@Autowired
private PayFeign payFeign;
/**
* 队列监听 取出队列中的订单信息
* 判断用户是否支付成功 如果支付成功则修改订单状态
* 如果支付失败 则将库存回滚 同时向微信服务器发送请求 关闭该订单的微信支付
* @param message
* @throws Exception
*/
@RabbitHandler
public void getMessage(String message) throws Exception {
Map<String, String> map = JSON.parseObject(message, Map.class);
//返回状态码
String return_code = map.get("return_code");
//订单号(订单id)
String out_trade_no = map.get("out_trade_no");
String transaction_id = map.get("transaction_id");
//如果状态码为success
if ("SUCCESS".equals(return_code)) {
//业务结果
String result_code = map.get("result_code");
if ("SUCCESS".equals(result_code)) {
//如果交易成功 修改订单状态
try {
orderService.updateStatus(out_trade_no,map.get("time_end"),transaction_id);
} catch (ParseException e) {
e.printStackTrace();
}
} else {
//如果交易失败 则库存回滚
orderService.deleteOrder(out_trade_no);
//关闭支付
payFeign.closeOrder(out_trade_no);
}
}
}
}
定时处理订单状态
第一种方法
https://blog.csdn.net/mingwulipo/article/details/103195582
第二种方法用redis实现延时队列
参考:
https://zhuanlan.zhihu.com/p/87113913
https://blog.csdn.net/hyy147/article/details/90259590
第三种方法:用两个队列
https://blog.csdn.net/weixin_41750142/article/details/115730745
https://blog.csdn.net/weixin_43461520/article/details/108524375
https://www.cnblogs.com/lylife/p/7881950.html
https://blog.csdn.net/sdTAyhn/article/details/105116347
畅购全部代码参考地址:
https://gitee.com/dai15/changgou
https://github.com/CodeHaotian/changgou
https://github.com/SuperGabriel/changgou
https://gitee.com/SixteenWords/changgou?_from=gitee_search
购物车:
package com.changgou.order.service.impl;
import cn.hutool.core.map.MapUtil;
import com.changgou.goods.feign.SkuFeign;
import com.changgou.goods.feign.SpuFeign;
import com.changgou.goods.pojo.Sku;
import com.changgou.goods.pojo.Spu;
import com.changgou.order.pojo.OrderItem;
import com.changgou.order.service.CartService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* @Author: Haotian
* @Date: 2020/2/26 19:04
* @Description: 购物车服务实现
*/
@Service
public class CartServiceImpl implements CartService {
private static final String CART = "cart_";
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private SkuFeign skuFeign;
@Autowired
private SpuFeign spuFeign;
@Override
public void addCart(String skuId, Integer number, String username) {
//1.查询redis中相对应的商品信息
OrderItem orderItem = (OrderItem) redisTemplate.boundHashOps( CART + username ).get( skuId );
if (orderItem != null) {
//2.如果当前商品在redis中的存在,则更新商品的数量与价钱
orderItem.setNum( orderItem.getNum() + number );
if (orderItem.getNum() <= 0) {
//商品数量等于0,删除该商品
redisTemplate.boundHashOps( CART + username ).delete( skuId );
return;
}
orderItem.setMoney( orderItem.getNum() * orderItem.getPrice() );
orderItem.setPayMoney( orderItem.getNum() * orderItem.getPrice() );
} else {
//3.如果当前商品在redis中不存在,将商品添加到redis中
Sku sku = skuFeign.findById( skuId ).getData();
Spu spu = spuFeign.findSpuById( sku.getSpuId() ).getData();
//封装orderItem
orderItem = this.sku2OrderItem( sku, spu, number );
}
//更新数据
redisTemplate.boundHashOps( CART + username ).put( skuId, orderItem );
}
@Override
public Map<String, Object> list(String username) {
List<OrderItem> orderItemList = redisTemplate.boundHashOps( CART + username ).values();
//商品总数量
Integer totalNum = 0;
//商品总价格
Integer totalMoney = 0;
for (OrderItem orderItem : Objects.requireNonNull( orderItemList )) {
totalNum += orderItem.getNum();
totalMoney += orderItem.getMoney();
}
return MapUtil.<String, Object>builder()
.put( "orderItemList", orderItemList )
.put( "totalNum", totalNum )
.put( "totalMoney", totalMoney ).build();
}
/**
* 封装购物车商品数据
*
* @param sku sku
* @param spu spu
* @param number 数量
* @return 购物车商品数据
*/
private OrderItem sku2OrderItem(Sku sku, Spu spu, Integer number) {
return OrderItem.builder()
.spuId( sku.getSpuId() )
.skuId( sku.getId() )
.name( sku.getName() )
.price( sku.getPrice() )
.num( number )
.money( sku.getPrice() * number )
.payMoney( sku.getPrice() * number )
.image( sku.getImage() )
.weight( sku.getWeight() * number )
.categoryId1( spu.getCategory1Id() )
.categoryId2( spu.getCategory2Id() )
.categoryId3( spu.getCategory3Id() ).build();
}
}
订单代码:
package com.changgou.order.service.impl;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fescar.spring.annotation.GlobalTransactional;
import com.changgou.goods.feign.SkuFeign;
import com.changgou.order.config.RabbitMqConfig;
import com.changgou.order.constant.OrderStatusEnum;
import com.changgou.order.dao.*;
import com.changgou.order.exception.OrderException;
import com.changgou.order.pojo.*;
import com.changgou.order.service.CartService;
import com.changgou.order.service.OrderService;
import com.changgou.pay.feign.PayFeign;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;
import javax.validation.constraints.NotNull;
import java.time.LocalDate;
import java.util.*;
/**
* @Author: Haotian
* @Date: 2020/2/15 22:26
* @Description: 分类服务实现
**/
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
/**
* 微信交易状态返回字段
*/
private static final String TRADE_STATE = "trade_state";
@Autowired
private OrderMapper orderMapper;
@Autowired
private CartService cartService;
@Autowired
private OrderItemMapper orderItemMapper;
@Autowired
private TaskMapper taskMapper;
@Autowired
private OrderLogMapper orderLogMapper;
@Autowired
private OrderConfigMapper orderConfigMapper;
@Autowired
private SkuFeign skuFeign;
@Autowired
private PayFeign payFeign;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RabbitTemplate rabbitTemplate;
private Snowflake snowflake = IdUtil.createSnowflake( 1, 1 );
@Override
public List<Order> findAll() {
return orderMapper.selectAll();
}
@Override
public Order findById(String id) {
return orderMapper.selectByPrimaryKey( id );
}
@Override
@GlobalTransactional(name = "order_add")
public String addOrder(Order order) {
//1.获取购物车的相关数据 → redis 中取
Map<String, Object> cartMap = cartService.list( order.getUsername() );
List<OrderItem> orderItemList = (List<OrderItem>) cartMap.get( "orderItemList" );
//2.统计计算:总金额,总数量
//3.填充订单数据并保存到tb_order表
String orderId = snowflake.nextIdStr();
order.setId( orderId );
order.setTotalNum( (Integer) cartMap.get( "totalNum" ) );
order.setTotalMoney( (Integer) cartMap.get( "totalMoney" ) );
order.setPayMoney( (Integer) cartMap.get( "totalMoney" ) );
order.setCreateTime( new Date() );
order.setUpdateTime( new Date() );
order.setBuyerRate( "0" );
order.setSourceType( "1" );
order.setOrderStatus( "0" );
order.setPayStatus( "0" );
order.setConsignStatus( "0" );
orderMapper.insertSelective( order );
//4.填充订单项数据并保存到tb_order_item
for (OrderItem orderItem : orderItemList) {
orderItem.setId( snowflake.nextIdStr() );
orderItem.setIsReturn( "0" );
orderItem.setOrderId( orderId );
orderItemMapper.insertSelective( orderItem );
}
//5.扣减库存
skuFeign.decrCount( order.getUsername() );
//int i = 1 / 0;
//Fixme: 2020/3/3 17:58 如果下单就进行积分添加,关闭订单时积分必须回滚,或者将添加积分任务放在支付成功后进行
//6.添加任务数据
log.info( "开始向订单数据库的任务表添加任务数据" );
//构件mq消息体内容
Map<String, Object> map = MapUtil.<String, Object>builder()
.put( "username", order.getUsername() )
.put( "orderId", orderId )
.put( "point", order.getPayMoney() ).build();
Task task = Task.builder().
createTime( new Date() ).updateTime( new Date() )
.mqExchange( RabbitMqConfig.EX_BUYING_ADD_POINT_USER )
.mqRoutingkey( RabbitMqConfig.CG_BUYING_ADD_POINT_KEY )
.requestBody( JSON.toJSONString( map ) ).build();
taskMapper.insertSelective( task );
//7.从redis中删除购物车数据
redisTemplate.delete( "cart_" + order.getUsername() );
//8.发送延迟消息
rabbitTemplate.convertAndSend( "", RabbitMqConfig.QUEUE_ORDER_CREATE, orderId );
return orderId;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void closeOrder(String orderId) {
//1.更据订单id查询订单相关信息
log.info( "开始执行关闭订单业务,当前订单id:{}", orderId );
Order order = orderMapper.selectByPrimaryKey( orderId );
if (ObjectUtil.isEmpty( order )) {
throw new OrderException( OrderStatusEnum.NOT_FOUND_ORDER );
}
if (!"0".equals( order.getPayStatus() )) {
log.info( "当前订单无需关闭" );
return;
}
//2.基于微信查询订单信息
log.info( "开始根据订单号:{}从微信查询相关信息", orderId );
Map<String, String> wxQueryMap = Convert.toMap( String.class, String.class, payFeign.queryOrder( orderId ).getData() );
String payStatus = wxQueryMap.get( TRADE_STATE );
//3.如果订单为已支付,补偿消息
if ("SUCCESS".equals( payStatus )) {
this.updatePayStatus( orderId, wxQueryMap.get( "transaction_id" ) );
log.info( "消息补偿成功" );
}
//4.如未支付,关闭订单
if ("NOTPAY".equals( payStatus )) {
order.setUpdateTime( new Date() );
order.setOrderStatus( "4" );
orderMapper.updateByPrimaryKey( order );
//记录日志
OrderLog orderLog = OrderLog.builder()
.id( snowflake.nextIdStr() )
.operater( "system" )
.operateTime( new Date() )
.orderStatus( "4" )
.orderId( order.getId() ).build();
orderLogMapper.insertSelective( orderLog );
//回滚库存
OrderItem orderItem = OrderItem.builder().orderId( order.getId() ).build();
List<OrderItem> orderItemList = orderItemMapper.select( orderItem );
for (OrderItem item : orderItemList) {
skuFeign.resumeStockNumber( item.getSkuId(), item.getNum() );
}
//关闭微信订单
payFeign.closeOrder( orderId );
log.info( "关闭订单" );
}
}
@Override
public void confirmTask(String orderId, String operator) {
Order order = orderMapper.selectByPrimaryKey( orderId );
if (ObjectUtil.isEmpty( order )) {
throw new OrderException( OrderStatusEnum.NOT_FOUND_ORDER );
}
if (!"1".equals( order.getConsignStatus() )) {
throw new OrderException( OrderStatusEnum.ORDER_IS_DELIVERY );
}
order.setConsignStatus( "2" );
order.setOrderStatus( "3" );
order.setUpdateTime( new Date() );
order.setEndTime( new Date() );
orderMapper.updateByPrimaryKey( order );
//记录订单日志
OrderLog orderLog = OrderLog.builder()
.id( snowflake.nextIdStr() )
.operater( operator )
.operateTime( new Date() )
.orderStatus( "3" )
.consignStatus( "2" )
.orderId( orderId ).build();
orderLogMapper.insertSelective( orderLog );
}
@Override
@Transactional(rollbackFor = Exception.class)
public void autoTack() {
//1.从订单配置表中获取订单自动确认时间点
OrderConfig orderConfig = orderConfigMapper.selectByPrimaryKey( 1 );
//2.得到当前时间节点,向前数 ( 订单自动确认的时间节点 ) 天,作为过期的时间节点
LocalDate now = LocalDate.now();
LocalDate date = now.plusDays( -orderConfig.getTakeTimeout() );
//3.从订单表中获取相关符合条件的数据 (发货时间小于过期时间,收货状态为未确认 )
Example example = new Example( Order.class );
Example.Criteria criteria = example.createCriteria();
criteria.andLessThan( "consignTime", date );
criteria.andEqualTo( "orderStatus", "2" );
List<Order> orderList = orderMapper.selectByExample( example );
//4.执行确认收货
for (Order order : orderList) {
this.confirmTask( order.getId(), "system" );
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> batchSend(List<Order> orderList) {
Map<String, Object> result = new HashMap<>();
for (Order order : orderList) {
List<String> list = new ArrayList<>();
String orderId = order.getId();
boolean flag = true;
//1.判断参数是否为空
if (StrUtil.isEmpty( order.getShippingCode() ) || StrUtil.isEmpty( order.getShippingName() )) {
list.add( String.format( "订单号%s,请输入对应的运单号或物流公司名称", orderId ) );
flag = false;
}
//2.校验订单状态
Order or = orderMapper.selectByPrimaryKey( orderId );
if (!"0".equals( or.getConsignStatus() ) || !"1".equals( or.getOrderStatus() )) {
list.add( String.format( "订单号%s,订单状态不合法", or.getId() ) );
flag = false;
}
if (!flag) {
continue;
}
//3.修改订单状态为已发货
or.setOrderStatus( "2" );
or.setConsignStatus( "1" );
or.setConsignTime( new Date() );
or.setUpdateTime( new Date() );
orderMapper.selectByPrimaryKey( or );
//4.记录订单日志
OrderLog orderLog = OrderLog.builder()
.id( snowflake.nextIdStr() )
.operater( "admin" )
.operateTime( new Date() )
.orderStatus( "2" )
.consignStatus( "1" )
.orderId( orderId ).build();
orderLogMapper.insertSelective( orderLog );
result.put( "flag", list.size() == 0 );
result.put( "message", list.size() == 0 ? "批处理成功" : "批处理失败" );
result.put( "data", list );
}
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateOrder(Order order) {
orderMapper.updateByPrimaryKeySelective( order );
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteById(String id) {
orderMapper.deleteByPrimaryKey( id );
}
@Override
public List<Order> findList(@NotNull Map<String, Object> searchMap) {
return orderMapper.selectByExample( getExample( searchMap ) );
}
@Override
public Page<Order> findPage(@NotNull Map<String, Object> searchMap, Integer pageNum, Integer pageSize) {
return PageHelper
.startPage( pageNum, pageSize )
.doSelectPage( () -> orderMapper.selectByExample( getExample( searchMap ) ) );
}
@Override
public List<OrderInfoCount> findAllInfoCount(Date startTime, Date endTime) {
//得到当前时间
DateTime nowDate = DateUtil.date();
if (startTime == null) {
//默认开始时间=当前时间前一周当天凌晨
startTime = DateUtil.beginOfDay( DateUtil.offsetWeek( nowDate, -1 ) );
}
if (endTime == null) {
//默认结束时间=当前时间
endTime = nowDate;
}
//判断结束时间是否 > 当前时间
int compare = DateUtil.compare( endTime, nowDate );
if (compare > 0) {
endTime = nowDate;
}
//数量过少,统一增加查看效果
int number = 100;
//统计信息集合
List<OrderInfoCount> dataList = new ArrayList<>();
dataList.add( OrderInfoCount.builder()
.name( "待付款订单" )
.value( orderMapper.waitPayMoneyCount( startTime, endTime ) + number ).build() );
dataList.add( OrderInfoCount.builder()
.name( "待发货订单" )
.value( orderMapper.waitSendGoodsCount( startTime, endTime ) + number ).build() );
dataList.add( OrderInfoCount.builder()
.name( "已发货订单" )
.value( orderMapper.shippedGoodsCount( startTime, endTime ) + number ).build() );
dataList.add( OrderInfoCount.builder()
.name( "已完成订单" )
.value( orderMapper.completedCount( startTime, endTime ) + number ).build() );
dataList.add( OrderInfoCount.builder()
.name( "已关闭订单" )
.value( orderMapper.closeOrderCount( startTime, endTime ) + number ).build() );
return dataList;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updatePayStatus(String orderId, String transactionId) {
//1.查询订单
Order order = orderMapper.selectByPrimaryKey( orderId );
if (ObjectUtil.isNotEmpty( order ) && "0".equals( order.getPayStatus() )) {
//2.修改订单的支付状态
order.setPayStatus( "1" );
order.setOrderStatus( "1" );
order.setUpdateTime( new Date() );
order.setPayTime( new Date() );
order.setTransactionId( transactionId );
orderMapper.updateByPrimaryKey( order );
//3.记录订单日志
OrderLog orderLog = OrderLog.builder()
.id( snowflake.nextIdStr() )
.operater( "system" )
.operateTime( new Date() )
.orderStatus( "1" )
.payStatus( "1" )
.orderId( orderId )
.remarks( "交易流水号:" + transactionId ).build();
orderLogMapper.insert( orderLog );
}
}
/**
* 条件拼接
*
* @param searchMap 查询条件
* @return example 条件对象
*/
private Example getExample(@NotNull Map<String, Object> searchMap) {
Example example = new Example( Order.class );
Example.Criteria criteria = example.createCriteria();
if (searchMap != null) {
// 订单id
String id = Convert.toStr( searchMap.get( "id" ) );
if (StrUtil.isNotEmpty( id )) {
criteria.andEqualTo( "id", id );
}
// 支付类型,1、在线支付、0 货到付款
String payType = Convert.toStr( searchMap.get( "payType" ) );
if (StrUtil.isNotEmpty( payType )) {
criteria.andEqualTo( "payType", payType );
}
// 物流名称
String shippingName = Convert.toStr( searchMap.get( "shippingName" ) );
if (StrUtil.isNotEmpty( shippingName )) {
criteria.andLike( "shippingName", "%" + shippingName + "%" );
}
// 物流单号
String shippingCode = Convert.toStr( searchMap.get( "shippingCode" ) );
if (StrUtil.isNotEmpty( shippingCode )) {
criteria.andLike( "shippingCode", "%" + shippingCode + "%" );
}
// 用户名称
String username = Convert.toStr( searchMap.get( "username" ) );
if (StrUtil.isNotEmpty( username )) {
criteria.andLike( "username", "%" + username + "%" );
}
// 买家留言
String buyerMessage = Convert.toStr( searchMap.get( "buyerMessage" ) );
if (StrUtil.isNotEmpty( buyerMessage )) {
criteria.andLike( "buyerMessage", "%" + buyerMessage + "%" );
}
// 是否评价
String buyerRate = Convert.toStr( searchMap.get( "buyerRate" ) );
if (StrUtil.isNotEmpty( buyerRate )) {
criteria.andLike( "buyerRate", "%" + buyerRate + "%" );
}
// 收货人
String receiverContact = Convert.toStr( searchMap.get( "receiverContact" ) );
if (StrUtil.isNotEmpty( receiverContact )) {
criteria.andLike( "receiverContact", "%" + receiverContact + "%" );
}
// 收货人手机
String receiverMobile = Convert.toStr( searchMap.get( "receiverMobile" ) );
if (StrUtil.isNotEmpty( receiverMobile )) {
criteria.andLike( "receiverMobile", "%" + receiverMobile + "%" );
}
// 收货人地址
String receiverAddress = Convert.toStr( searchMap.get( "receiverAddress" ) );
if (StrUtil.isNotEmpty( receiverAddress )) {
criteria.andLike( "receiverAddress", "%" + receiverAddress + "%" );
}
// 订单来源:1:web,2:app,3:微信公众号,4:微信小程序 5 H5手机页面
String sourceType = Convert.toStr( searchMap.get( "sourceType" ) );
if (StrUtil.isNotEmpty( sourceType )) {
criteria.andEqualTo( "sourceType", sourceType );
}
// 交易流水号
String transactionId = Convert.toStr( searchMap.get( "transactionId" ) );
if (StrUtil.isNotEmpty( transactionId )) {
criteria.andLike( "transactionId", "%" + transactionId + "%" );
}
// 订单状态
String orderStatus = Convert.toStr( searchMap.get( "orderStatus" ) );
if (StrUtil.isNotEmpty( orderStatus )) {
criteria.andEqualTo( "orderStatus", orderStatus );
}
// 支付状态
String payStatus = Convert.toStr( searchMap.get( "payStatus" ) );
if (StrUtil.isNotEmpty( payStatus )) {
criteria.andEqualTo( "payStatus", payStatus );
}
// 发货状态
String consignStatus = Convert.toStr( searchMap.get( "consignStatus" ) );
if (StrUtil.isNotEmpty( consignStatus )) {
criteria.andEqualTo( "consignStatus", consignStatus );
}
// 是否删除
String isDelete = Convert.toStr( searchMap.get( "isDelete" ) );
if (StrUtil.isNotEmpty( isDelete )) {
criteria.andEqualTo( "isDelete", isDelete );
}
// 数量合计
String totalNum = Convert.toStr( searchMap.get( "totalNum" ) );
if (StrUtil.isNotEmpty( totalNum )) {
criteria.andEqualTo( "totalNum", totalNum );
}
// 金额合计
String totalMoney = Convert.toStr( searchMap.get( "totalMoney" ) );
if (StrUtil.isNotEmpty( totalMoney )) {
criteria.andEqualTo( "totalMoney", totalMoney );
}
// 优惠金额
String preMoney = Convert.toStr( searchMap.get( "preMoney" ) );
if (StrUtil.isNotEmpty( preMoney )) {
criteria.andEqualTo( "preMoney", preMoney );
}
// 邮费
String postFee = Convert.toStr( searchMap.get( "postFee" ) );
if (StrUtil.isNotEmpty( postFee )) {
criteria.andEqualTo( "postFee", postFee );
}
// 实付金额
String payMoney = Convert.toStr( searchMap.get( "payMoney" ) );
if (StrUtil.isNotEmpty( payMoney )) {
criteria.andEqualTo( "payMoney", payMoney );
}
}
return example;
}
}