- 第11章 订单
- 第12章 分布式事务解决方案
- 第13章 微信扫码支付
- 第14章 订单处理
- 第15章-秒杀前端
- 2 秒杀商品存入缓存-重点
- 3 秒杀商品-首页-了解
- 第16章-秒杀后端
第11章 订单
角色: 订单模块,后端开发工程师。
课程内容
完成订单结算页渲染
完成用户下单实现
完成库存变更实现
1 订单结算页

1.1 收件地址分析
1展示 打开前台 order.html

收件地址分析 td_address表。表结构分析:

CREATE TABLE `tb_address` (`id` int(11) NOT NULL AUTO_INCREMENT,`username` varchar(50) DEFAULT NULL COMMENT '用户名',`provinceid` varchar(20) DEFAULT NULL COMMENT '省',`cityid` varchar(20) DEFAULT NULL COMMENT '市',`areaid` varchar(20) DEFAULT NULL COMMENT '县/区',`phone` varchar(20) DEFAULT NULL COMMENT '电话',`address` varchar(200) DEFAULT NULL COMMENT '详细地址',`contact` varchar(50) DEFAULT NULL COMMENT '联系人',`is_default` varchar(1) DEFAULT NULL COMMENT '是否是默认 1默认 0否',`alias` varchar(50) DEFAULT NULL COMMENT '别名',PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=66 DEFAULT CHARSET=utf8;
我们可以根据用户登录名去tb_address表中查询对应的数据。
1.2 实现用户收件地址查询
1.2.1 代码实现 ydles-service-user
1需改com.ydles.user.service.AddressService接口,添加根据用户名字查询用户收件地址信息,代码如下:
//根据username 查询public List<Address> list(String username);
2业务层接口实现类
修改com.ydles.user.service.impl.AddressServiceImpl类,添加根据用户查询用户收件地址信息实现方法,如下代码:
@Overridepublic List<Address> list(String username) {Address address=new Address();address.setUsername(username);List<Address> addressList = addressMapper.select(address);return addressList;}
3控制层
修改com.ydles.user.controller.AddressController,添加根据用户名查询用户收件信息方法,代码如下:
@AutowiredTokenDecode tokenDecode;//根据username 查询 lsit<Address>@GetMapping("/list")public List<Address> list(){//当前登录人String username = tokenDecode.getUserInfo().get("username");List<Address> addressList = addressService.list(username);return addressList;}
4创建TokenDecode
com.ydles.UserApplication中创建TokenDecode,代码如下:
@Beanpublic TokenDecode tokenDecode(){return new TokenDecode();}
测试:通过网关访问,修改网关配置信息
routes:- id: ydles_goods_routeuri: lb://goodspredicates:- Path=/api/album/**,/api/brand/**,/api/cache/**,/api/categoryBrand/**,/api/category/**,/api/para/**,/api/pref/**,/api/sku/**,/api/spec/**,/api/spu/**,/api/stockBack/**,/api/template/**filters:#- PrefixPath=/brand- StripPrefix=1#用户微服务- id: ydles_user_routeuri: lb://userpredicates:- Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/**filters:- StripPrefix=1#认证微服务- id: ydles_oauth_useruri: lb://user-authpredicates:- Path=/api/oauth/**filters:- StripPrefix=1#订单微服务- id: ydles_order_routeuri: lb://orderpredicates:- Path=/api/cart/**,/api/categoryReport/**,/api/orderConfig/**,/api/order/**,/api/orderItem/**,/api/orderLog/**,/api/preferential/**,/api/returnCause/**,/api/returnOrder/**,/api/returnOrderItem/**filters:- StripPrefix=1#购物车订单渲染微服务- id: ydles_order_web_routeuri: lb://order-webpredicates:- Path=/api/wcart/**,/api/worder/**filters:- StripPrefix=1
先登录 Postman访问 http://localhost:8001/api/oauth/login
再请求数据 Postman访问 http://localhost:8001/api/address/list

1.3 页面模板渲染

购物车这块也使用的是模板渲染,用户先请求经过微服务网关,微服务网关转发到订单购物车模板渲染服务,模板渲染服务条用用户微服务和订单购物车微服务查询用户收件地址和购物车清单,然后到页面显示。
1.3.1 准备工作
(1)静态资源导入
将资料中的order.html拷贝到ydles-web-order工程的templates中。

(2)页面跳转实现
在ydles-web-order中创建com.ydles.order.controller.OrderController实现页面跳转,代码如下:
@Controller@RequestMapping("/worder")public class OrderController {@RequestMapping("/ready/order")public String readyOrder(Model model) {return "order";}}
(3)网关配置
修改ydles-gateway-web的application.yml文件,将订单的路由过滤地址添加上去,代码如下:

#购物车订单渲染微服务- id: ydles_order_web_routeuri: lb://order-webpredicates:- Path=/api/wcart/**,/api/worder/**filters:- StripPrefix=1
同时不要忘了把该地址添加到登录过滤地址中,修改com.ydles.filter.URLFilter,在orderFilterPath里添加/api/worder/**过滤,代码如下:

1.3.2 信息查询-重点
需求:
因为一会儿要调用ydles-service-user查询用户的收件地址信息,调用ydles-service-order查询购物车清单信息,所以我们需要创建Feign。购物车的Feign之前已经创建过了,所以只需要创建用户地址相关的即可。
(1)用户地址信息查询
在ydles-service-user-api中创建AddressFeign,代码如下:
@FeignClient(name="user")public interface AddressFeign {/**** 查询用户的收件地址信息* @return*/@GetMapping(value = "/list")public Result<List<Address>> list();}
(2) ydles_web_order项目启动类OrderWebApplication加入调用的feign包
<dependency><groupId>com.ydles</groupId><artifactId>ydles_service_user_api</artifactId><version>1.0-SNAPSHOT</version></dependency>
@SpringBootApplication@EnableEurekaClient@EnableFeignClients(basePackages = {"com.ydles.order.feign","com.ydles.user.feign"})public class OrderWebApplication {
(3)查询购物车和用户收件地址信息
修改ydles-web-order中的com.ydles.order.controller.OrderController的readyOrder方法,在该方法中,使用feign调用查询收件地址信息和用户购物车信息,代码如下:
@Controller@RequestMapping("/worder")public class OrderController {@Autowiredprivate AddressFeign addressFeign;@Autowiredprivate CartFeign cartFeign;@RequestMapping("/ready/order")public String readyOrder(Model model) {//1收件人的地址信息List<Address> addressList = addressFeign.list().getData();model.addAttribute("address",addressList);//2购物车信息Map map = cartFeign.list();List<OrderItem> orderItemList = (List<OrderItem>) map.get("orderItemList");Integer totalMoney = (Integer) map.get("totalMoney");Integer totalNum = (Integer) map.get("totalNum");model.addAttribute("carts",orderItemList);model.addAttribute("totalMoney",totalMoney);model.addAttribute("totalNum",totalNum);return "order";}}
(3)数据回显-了解
修改order.html,与静态原型中的order.html打开对比。
2 <html xmlns:th="http://www.thymeleaf.org">
82 <div class="cart py-container" id="app">
97<div class="choose-address" th:each="addr:${address}"><div class="con name " th:@click="|chooseAddr('${addr.contact}','${addr.phone}','${addr.address}')|" th:classappend="${addr.isDefault}==1?'selected':''"><a href="javascript:;" ><em th:text="${addr.contact}"></em> <span title="点击取消选择"></span></a></div><div class="con address"><span class="place"><em th:text="${addr.address}"></em> </span><span class="phone"><em th:text="${addr.phone}"></em> </span><span class="base" th:if="${addr.isDefault}==1">默认地址</span></div><div class="clearfix"></div></div>
118<ul class="payType"><li class="selected" th:@click="|order.payType=1|">在线支付<span title="点击取消选择"></span></li><li th:@click="|order.payType=0|">货到付款<span title="点击取消选择"></span></li></ul>
141<ul class="yui3-g" th:each="cart,cartsList:${carts}"><li class="yui3-u-1-9"><span><img th:src="${cart.image}"/></span></li><li class="yui3-u-5-12"><div class="desc" th:text="${cart.name}"></div><div class="seven">7天无理由退货</div></li><li class="yui3-u-1-12"><div class="price" th:text="${cart.price}"></div></li><li class="yui3-u-1-12"><div class="num" th:text="${cart.num}"></div></li><li class="yui3-u-1-12"><div class="num" th:text="${cart.num}*${cart.price}"></div></li><li class="yui3-u-1-12"><div class="exit">有货</div></li></ul>
193<div class="fc-receiverInfo">寄送至:<span id="receive-address">{{order.receiveAddress}}</span>收货人:<span id="receive-name">{{order.receiveContact}}</span><span id="receive-phone">{{order.receiveMobile}}</span></div>
201<div class="list"><span><i class="number"><em th:text="${totalNum}"></em></i>件商品,商品总金额</span><em class="allprice">¥<em th:text="${totalMoney}"></em></em></div>
211<div class="clearfix trade"><div class="fc-price">应付金额: <span class="final-price">¥<em th:text="${totalMoney}"></em></span></div></div>
318<script th:inline="javascript">var app = new Vue({el:"#app",data:{order:{'receiveContact': [[${deAddr.contact}]],'receiveMobile': [[${deAddr.phone}]],'receiveAddress': [[${deAddr.address}]],'payType':1}},methods:{chooseAddr: function (contact,mobile,address){app.$set(app.order,'receiveContact',contact);app.$set(app.order,'receiveMobile',mobile);app.$set(app.order,'receiveAddress',address);}}})</script>
测试:先登录,再访问
http://localhost:8001/api/worder/ready/order

1.3.3 记录选中收件人-了解
收件人动态展示
73 <div class="cart py-container" id="app">
371-395<script th:inline="javascript">var app = new Vue({el:"#app",data:{order:{'receiveContact':[[${deAddr.contact}]],'receiveMobile':[[${deAddr.phone}]],'receiveAddress':[[${deAddr.address}]],'payType':1}},methods:{chooseAddr:function (contact,mobile,address) {app.$set(app.order,'receiveContact',contact);app.$set(app.order,'receiveMobile',mobile);app.$set(app.order,'receiveAddress',address);},add:function () {axios.post('/api/worder/add',this.order).then(function (response) {if (response.data.flag){//添加订单成功alert("添加订单成功");} else{alert("添加订单失败");}})}}})</script>
199-207<div class="clearfix trade"><div class="fc-price">应付金额: <span class="price">¥<em th:text="${totalMoney}"></em></span></div><div class="fc-receiverInfo">寄送至:<span id="receive-address">{{order.receiveAddress}}</span>收货人:<span id="receive-name">{{order.receiveContact}}</span><span id="receive-phone">{{order.receiveMobile}}</span></div></div>
后端设置默认收件人
//默认收件人信息for (Address address : addressList) {if ("1".equals(address.getIsDefault())){//默认收件人model.addAttribute("deAddr",address);break;}}
前端:
375order:{'receiveContact':[[${deAddr.contact}]],'receiveMobile':[[${deAddr.phone}]],'receiveAddress':[[${deAddr.address}]],'payType':1}
支付方式
109-114<div class="step-cont"><ul class="payType"><li class="selected" th:@click="|order.payType=1|">在线支付<span title="点击取消选择"></span></li><li th:@click="|order.payType=0|">货到付款<span title="点击取消选择"></span></li></ul></div>
购物车跳转结算页面
cart.html
<a class="sum-btn" href="/api/worder/ready/order" target="_blank">结算</a>
2 下单-重点
2.1 业务分析
点击提交订单的时候,会立即创建订单数据,创建订单数据会将数据存入到2张表中,分别是订单表和订单明细表,此处还需要修改商品对应的库存数量。

订单表结构如下:
CREATE TABLE `tb_order` (`id` varchar(50) COLLATE utf8_bin NOT NULL COMMENT '订单id',`total_num` int(11) DEFAULT NULL COMMENT '数量合计',`total_money` int(11) DEFAULT NULL COMMENT '金额合计',`pre_money` int(11) DEFAULT NULL COMMENT '优惠金额',`post_fee` int(11) DEFAULT NULL COMMENT '邮费',`pay_money` int(11) DEFAULT NULL COMMENT '实付金额',`pay_type` varchar(1) COLLATE utf8_bin DEFAULT NULL COMMENT '支付类型,1、在线支付、0 货到付款',`create_time` datetime DEFAULT NULL COMMENT '订单创建时间',`update_time` datetime DEFAULT NULL COMMENT '订单更新时间',`pay_time` datetime DEFAULT NULL COMMENT '付款时间',`consign_time` datetime DEFAULT NULL COMMENT '发货时间',`end_time` datetime DEFAULT NULL COMMENT '交易完成时间',`close_time` datetime DEFAULT NULL COMMENT '交易关闭时间',`shipping_name` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT '物流名称',`shipping_code` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT '物流单号',`username` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '用户名称',`buyer_message` varchar(1000) COLLATE utf8_bin DEFAULT NULL COMMENT '买家留言',`buyer_rate` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '是否评价',`receiver_contact` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '收货人',`receiver_mobile` varchar(12) COLLATE utf8_bin DEFAULT NULL COMMENT '收货人手机',`receiver_address` varchar(200) COLLATE utf8_bin DEFAULT NULL COMMENT '收货人地址',`source_type` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '订单来源:1:web,2:app,3:微信公众号,4:微信小程序 5 H5手机页面',`transaction_id` varchar(30) COLLATE utf8_bin DEFAULT NULL COMMENT '交易流水号',`order_status` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '订单状态,0:未完成,1:已完成,2:已退货',`pay_status` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '支付状态,0:未支付,1:已支付,2:支付失败',`consign_status` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '发货状态,0:未发货,1:已发货,2:已收货',`is_delete` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '是否删除',PRIMARY KEY (`id`),KEY `create_time` (`create_time`),KEY `status` (`order_status`),KEY `payment_type` (`pay_type`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
订单明细表结构如下:
CREATE TABLE `tb_order_item` (`id` varchar(50) COLLATE utf8_bin NOT NULL COMMENT 'ID',`category_id1` int(11) DEFAULT NULL COMMENT '1级分类',`category_id2` int(11) DEFAULT NULL COMMENT '2级分类',`category_id3` int(11) DEFAULT NULL COMMENT '3级分类',`spu_id` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT 'SPU_ID',`sku_id` bigint(20) NOT NULL COMMENT 'SKU_ID',`order_id` bigint(20) NOT NULL COMMENT '订单ID',`name` varchar(200) COLLATE utf8_bin DEFAULT NULL COMMENT '商品名称',`price` int(20) DEFAULT NULL COMMENT '单价',`num` int(10) DEFAULT NULL COMMENT '数量',`money` int(20) DEFAULT NULL COMMENT '总金额',`pay_money` int(11) DEFAULT NULL COMMENT '实付金额',`image` varchar(200) COLLATE utf8_bin DEFAULT NULL COMMENT '图片地址',`weight` int(11) DEFAULT NULL COMMENT '重量',`post_fee` int(11) DEFAULT NULL COMMENT '运费',`is_return` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '是否退货,0:未退货,1:已退货',PRIMARY KEY (`id`),KEY `item_id` (`sku_id`),KEY `order_id` (`order_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
2.2 下单实现
下单的时候,先往tb_order表中增加数据,再往tb_order_item表中增加数据。
2.2.1 代码实现
这里先修改ydles-service-order微服务,实现下单操作,这里会生成订单号,我们首先需要在启动类中创建一个IdWorker对象。
在com.ydles.OrderApplication中创建IdWorker,代码如下:
@Beanpublic IdWorker idWorker() {return new IdWorker(workerId, datacenterId);}
(1)业务层
实现逻辑:
1)获取所有购物车
2)统计计算:总金额,总支付金额,总数量
3)填充订单数据并保存
4)获取每一个购物项保存到orderItem
5)删除购物车中数据
修改订单微服务添加com.ydles.order.service.impl.OrderServiceImpl,代码如下:
/*** 下单* @param order* 缺啥补啥*/@Overridepublic boolean add(Order order){//1 获取 购物车信息Map cartMap = cartService.list(order.getUsername());//map.put("orderItemList",orderItemList);Integer totalNum = (Integer) cartMap.get("totalNum");Integer totalMoney = (Integer) cartMap.get("totalMoney");//2 order表里存数据String orderId = idWorker.nextId()+"";order.setId(orderId);order.setTotalNum(totalNum);order.setTotalMoney(totalMoney);//作业:优惠金额 怎么算 本店满50-5 跨店 300-50//作业:邮费金额 怎么算order.setPayMoney(totalMoney);order.setCreateTime(new Date());order.setUpdateTime(new Date());//作业:买家留言order.setBuyerRate("0");order.setSourceType("1");order.setOrderStatus("0");order.setPayStatus("0");order.setConsignStatus("0");order.setIsDelete("0");orderMapper.insertSelective(order);//3 orderItem表里存数据List<OrderItem> orderItemList = (List<OrderItem>) cartMap.get("orderItemList");for (OrderItem orderItem : orderItemList) {orderItem.setOrderId(orderId);orderItem.setPostFee(0);orderItem.setIsReturn("0");orderItemMapper.insertSelective(orderItem);}//4 删除购物车信息stringRedisTemplate.delete(CART+order.getUsername());return true;}
(2)控制层
修改ydles-service-order微服务,修改com.ydles.order.controller.OrderController类,代码如下:
@AutowiredTokenDecode tokenDecode;/**** 下单* @param order* @return*/@PostMappingpublic Result add(@RequestBody Order order){String username = tokenDecode.getUserInfo().get("username");order.setUsername(username);orderService.add(order);return new Result(true,StatusCode.OK,"添加成功");}
2.2.2 渲染服务对接

我们需要在模板渲染端调用订单微服务实现下单操作,下单操作需要调用订单微服务,所以需要创建对应的Feign。
(1)Feign创建
修改ydles-service-order-api,添加OrderFeign,代码如下:
@FeignClient(name = "order")public interface OrderFeign {@PostMapping("/order")public Result add(@RequestBody Order order);}
(2)下单调用
修改ydles-web-order的com.ydles.order.controller.OrderController添加下单方法,代码如下:
@Autowiredprivate OrderFeign orderFeign;@PostMapping("/add")@ResponseBodypublic Result add(@RequestBody Order order){Result result = orderFeign.add(order);return result;}
(3)页面调用
order.html
383-392add:function () {axios.post('/api/worder/add',this.order).then(function (response) {if (response.data.flag){//添加订单成功alert("添加订单成功");} else{alert("添加订单失败");}})}
209<a class="sui-btn btn-danger btn-xlarge" href="javascript:void(0)" @click="add()">提交订单</a>
点击提交订单调用
保存订单测试,表数据变化如下:
tb_order表数据:

tb_order_item表数据:

2.3 库存变更
2.3.1 业务分析
上面操作只实现了下单操作,但对应的库存还没跟着一起减少,我们在下单之后,应该调用商品微服务,将下单的商品库存减少,销量增加。每次订单微服务只需要将用户名传到商品微服务,商品微服务通过用户名到Redis中查询对应的购物车数据,然后执行库存减少,库存减少需要控制当前商品库存>=销售数量。
如何控制库存数量>=购买数量呢?其实可以通过SQL语句实现,每次减少数量之前,加个条件判断。
where num>=#{num}即可。
商品服务需要查询购物车数据,所以需要引入订单的api,在pom.xml中添加如下依赖:
<!--order api 依赖--><dependency><groupId>com.ydles</groupId><artifactId>ydles_service_order_api</artifactId><version>1.0-SNAPSHOT</version></dependency>
2.3.2 代码实现
要调用其他微服务,需要将头文件中的令牌数据携带到其他微服务中取,所以我们不能使用hystrix的多线程模式,修改ydles-service-order的applicatin.yml配置,代码如下:
#hystrix 配置hystrix:command:default:execution:isolation:thread:timeoutInMilliseconds: 10000strategy: SEMAPHORE
每次还需要使用拦截器添加头文件信息,添加拦截器,代码如下:
(1)Dao层
修改ydles-service-goods微服务的com.ydles.goods.dao.SkuMapper接口,增加库存递减方法,代码如下:
/*** 递减库存* @param orderItem* @return*/@Update("UPDATE tb_sku SET num=num-#{num},sale_num=sale_num+#{num} WHERE id=#{skuId} AND num>=#{num}")int decrCount(OrderItem orderItem);
(2)业务层
修改ydles-service-goods微服务的com.ydles.goods.service.SkuService接口,添加如下方法:
/**** 库存递减* @param username*/void decrCount(String username);
修改ydles-service-goods微服务的com.ydles.goods.service.impl.SkuServiceImpl实现类,添加一个实现方法,代码如下:
@Autowiredprivate RedisTemplate redisTemplate;/**** 库存递减* @param username*/@Overridepublic void decrCount(String username) {//获取购物车数据List<OrderItem> orderItems = redisTemplate.boundHashOps("Cart_" + username).values();//循环递减for (OrderItem orderItem : orderItems) {//递减库存int count = skuMapper.decrCount(orderItem);if(count<=0){throw new RuntimeException("库存不足,递减失败!");}}}
(3)控制层
修改ydles-service-goods的com.ydles.goods.controller.SkuController类,添加库存递减方法,代码如下:
/**** 库存递减* @param username* @return*/@PostMapping(value = "/decr/count")public Result decrCount(@RequestParam("username") String username){//库存递减skuService.decrCount(username);return new Result(true,StatusCode.OK,"库存递减成功!");}
(4)创建feign
同时在ydles-service-goods-api工程添加com.ydles.goods.feign.SkuFeign的实现,代码如下:
/**** 库存递减* @param username* @return*/@PostMapping(value = "/decr/count")Result decrCount(@RequestParam(value = "username") String username);
2.3.3 调用库存递减
需求:
ydles_service_order服务调用 ydles_service_goods服务, ydles_web_order服务的Application启动类
都需要添加下面拦截器
@Beanpublic FeignInterceptor feignInterceptor(){return new FeignInterceptor();}
ydles_service_goods加入配置信息
spring:application:name: goodsredis:host: 192.168.200.128
调用库存递减
修改ydles-service-order微服务的com.ydles.order.service.impl.OrderServiceImpl类的add方法,增加库存递减的调用。
先注入SkuFeign
@Autowiredprivate SkuFeign skuFeign;
再调用库存递减方法
//库存减库存skuFeign.decrCount(order.getUsername());
完整代码如下:
/*** 增加* @param order*/@Overridepublic Boolean add(Order order){//查询出用户的所有购物车Map orderItemMap = cartService.list(order.getUsername());//统计计算Integer totalMoney = Integer.parseInt(String.valueOf(orderItemMap.get("totalPrice")));Integer num = Integer.parseInt(String.valueOf(orderItemMap.get("totalNum")));//购买商品数量order.setTotalNum(num);//购买金额order.setTotalMoney(totalMoney);//支付金额order.setPayMoney(totalMoney);//优惠金额order.setPreMoney(totalMoney);//其他数据完善order.setCreateTime(new Date());order.setUpdateTime(order.getCreateTime());order.setBuyerRate("0"); //0:未评价,1:已评价order.setSourceType("1"); //来源,1:WEBorder.setOrderStatus("0"); //0:未完成,1:已完成,2:已退货order.setPayStatus("0"); //0:未支付,1:已支付,2:支付失败order.setConsignStatus("0"); //0:未发货,1:已发货,2:已收货order.setId(idWorker.nextId());int count = orderMapper.insertSelective(order);//保存待支付订单到redis中redisTemplate.boundValueOps(Constants.ORDER_PAY + order.getUsername()).set(order);//添加订单明细List<OrderItem> orderItemList = (List<OrderItem>)orderItemMap.get("orderItemList");for (OrderItem orderItem : orderItemList) {orderItem.setId(idWorker.nextId());orderItem.setIsReturn("0");orderItem.setOrderId(order.getId());orderItemMapper.insertSelective(orderItem);}//扣减库存skuFeign.decrCount(order.getUsername());//清除Redis缓存购物车数据redisTemplate.delete("Cart_"+order.getUsername());return true;}
测试
1购物车添加商品
http://localhost:8001/api/wcart/add?skuId=1450862568724758528&num=5

2查看购物车
http://localhost:8001/api/wcart/list

3点击结算


4点击提交订单

tb_order 有数据

tb_order_item也有数据

tb_sku表查看库存和销量是否更改

库存减少前,查询数据库Sku数据如下:95库存,5销量 库存94,销量6

2.4 增加积分(学员练习)
tb_user表
`points` int(11) DEFAULT NULL COMMENT '积分',
需求:
在增加订单的时候,同时添加用户积分。与减库存道理相同。
(1)dao层
修改ydles-service-user微服务的com.ydles.user.dao.UserMapper接口,增加用户积分方法,代码如下:
/**** 增加用户积分* @param username* @param pint* @return*/@Update("UPDATE tb_user SET points=points+#{point} WHERE username=#{username}")int addUserPoints(@Param("username") String username, @Param("point") Integer pint);
(2)业务层
修改ydles-service-user微服务的com.ydles.user.service.UserService接口,代码如下:
/**** 添加用户积分* @param username* @param pint* @return*/int addUserPoints(String username,Integer pint);
修改ydles-service-user微服务的com.ydles.user.service.impl.UserServiceImpl,增加添加积分方法实现,代码如下:
/**** 修改用户积分* @param username* @param pint* @return*/@Overridepublic int addUserPoints(String username, Integer pint) {return userMapper.addUserPoints(username,pint);}
(3)控制层
修改ydles-service-user微服务的com.ydles.user.controller.UserController,添加增加用户积分方法,代码如下:
@Autowiredprivate TokenDecode tokenDecode;/**** 增加用户积分* @param points:要添加的积分*/@GetMapping(value = "/points/add")public Result addPoints(Integer points){//获取用户名Map<String, String> userMap = tokenDecode.getUserInfo();String username = userMap.get("username");//添加积分userService.addUserPoints(username,points);return new Result(true,StatusCode.OK,"添加积分成功!");}
(4)Feign添加
修改ydles-service-user-api工程,修改com.ydles.user.feign.UserFeign,添加增加用户积分方法,代码如下:
/**** 添加用户积分* @param points* @return*/@GetMapping(value = "/points/add")Result addPoints(@RequestParam(value = "points")Integer points);
4.4.2 增加积分调用
修改ydles-service-order,添加ydles-service-user-api的依赖,修改pom.xml,添加如下依赖:
<!--user api 依赖--><dependency><groupId>com.ydles</groupId><artifactId>ydles-service-user-api</artifactId><version>1.0-SNAPSHOT</version></dependency>
在加订单的时候,同时添加用户积分,修改ydles-service-order微服务的com.ydles.order.service.impl.OrderServiceImpl下单方法,增加调用添加积分方法
修改ydles-service-order的启动类com.ydles.OrderApplication,添加feign的包路径
总结:
1订单结算页



2下单


第12章 分布式事务解决方案
角色:架构师
学习目标:
- 能够说出cap定理
- 能够说出BASE定理
- 能够说出常见的分布式事务解决方案
- 能够说出seata框架如何在项目中实现分布式事务
1.分布式事务解决方案
刚才我们编写的扣减库存与保存订单是在两个服务中存在的,如果扣减库存后订单保存失败了是不会回滚的,这样就会造成数据不一致的情况,这其实就是我们所说的分布式事务的问题,接下来我们来学习分布式事务的解决方案。
1.1 本地事务与分布式事务
1.1.1 事务
数据库事务(简称:事务,Transaction)是指数据库执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
事务拥有以下四个特性,习惯上被称为ACID特性:
原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态是指数据库中的数据应满足完整性约束。除此之外,一致性还有另外一层语义,就是事务的中间状态不能被观察到(这层语义也有说应该属于原子性)。
隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行,如同只有这一个操作在被数据库所执行一样。
持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。在事务结束时,此操作将不可逆转。

作业:隔离级别 传播机制
1.1.2 本地事务
起初,事务仅限于对单一数据库资源的访问控制,架构服务化以后,事务的概念延伸到了服务中。倘若将一个单一的服务操作作为一个事务,那么整个服务操作只能涉及一个单一的数据库资源,这类基于单个服务单一数据库资源访问的事务,被称为本地事务(Local Transaction)。
解决方案:@Transactional

1.1.3 分布式事务
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上,且属于不同的应用,分布式事务需要保证这些操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。最早的分布式事务应用架构很简单,不涉及服务间的访问调用,仅仅是服务内操作涉及到对多个数据库资源的访问。

当一个服务操作访问不同的数据库资源,又希望对它们的访问具有事务特性时,就需要采用分布式事务来协调所有的事务参与者。对于上面介绍的分布式事务应用架构,尽管一个服务操作会访问多个数据库资源,但是毕竟整个事务还是控制在单一服务的内部。如果一个服务操作需要调用另外一个服务,这时的事务就需要跨越多个服务了。在这种情况下,起始于某个服务的事务在调用另外一个服务的时候,需要以某种机制流转到另外一个服务,从而使被调用的服务访问的资源也自动加入到该事务当中来。下图反映了这样一个跨越多个服务的分布式事务:

如果将上面这两种场景(一个服务可以调用多个数据库资源,也可以调用其他服务)结合在一起,对此进行延伸,整个分布式事务的参与者将会组成如下图所示的树形拓扑结构。在一个跨服务的分布式事务中,事务的发起者和提交均系同一个,它可以是整个调用的客户端,也可以是客户端最先调用的那个服务。

较之基于单一数据库资源访问的本地事务,分布式事务的应用架构更为复杂。在不同的分布式应用架构下,实现一个分布式事务要考虑的问题并不完全一样,比如对多资源的协调、事务的跨服务传播等,实现机制也是复杂多变。
只要是涉及到多个微服务之间远程调用的话,那就回涉及到分布式事务。
分布式事务的作用:
保证每个事务的数据一致性。
1.2 分布式事务相关理论
1.2.1 CAP定理-重点

CAP定理是在 1998年加州大学的计算机科学家 Eric Brewer (埃里克.布鲁尔)提出,分布式系统有三个指标
- Consistency 强一致性
- Availability 可用性
- Partition tolerance 分区容错
它们的第一个字母分别是 C、A、P。Eric Brewer 说,这三个指标不可能同时做到。这个结论就叫做 CAP 定理。
因为:强一致性和可用性 互斥!

真实情况:
1 ac 传统项目。ssm管理系统,一个程序,一个数据库。
2 cp 信息重要的场景。手机银行转账。转圈圈,在做数据同步,这段时间内,可用性是没有的。
3 ap 互联网。 放弃强一致性,慢慢数据同步。

分区容错 Partition tolerance
**理解: 分布式系统集群中, 一个机器坏掉不应该影响其他机器**
大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。

上图中,G1 和 G2 是两台跨区的服务器。G1 向 G2 发送一条消息,G2 可能无法收到。系统设计的时候,必须考虑到这种情况。
一般来说,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们,剩下的 C 和 A 无法同时做到。
可用性 Availability
**理解: 一个请求, 必须返回一个响应**
Availability 中文叫做”可用性”,意思是只要收到用户的请求,服务器就必须给出回应。
用户可以选择向 G1 或 G2 发起读操作。不管是哪台服务器,只要收到请求,就必须告诉用户,到底是 v0 还是 v1,否则就不满足可用性。

一致性 Consistency
理解: 一定能读取到最新的数据
Consistency 中文叫做”一致性”。意思是,写操作之后的读操作,必须返回该值。
举例来说,某条记录是 v0,用户向 G1 发起一个写操作,将其改为 v1。

问题是,用户有可能向 G2 发起读操作,由于 G2 的值没有发生变化,因此返回的是 v0。G1 和 G2 读操作的结果不一致,这就不满足一致性了。

为了让 G2 也能变为 v1,就要在 G1 写操作的时候,让 G1 向 G2 发送一条消息,要求 G2 也改成 v1。

一致性和可用性的矛盾
一致性(C)和可用性(A),为什么不可能同时成立?答案很简单,因为可能通信失败(即出现分区容错)。
如果保证 G2 的一致性,那么 G1 必须在写操作时,锁定 G2 的读操作和写操作。只有数据同步后,才能重新开放读写。锁定期间,G2 不能读写,没有可用性(CP)。
如果保证 G2 的可用性,那么势必不能锁定 G2,所以一致性不成立(AP)。
综上所述,G2 无法同时做到一致性和可用性。系统设计时只能选择一个目标。如果追求一致性,那么无法保证所有节点的可用性;如果追求所有节点的可用性,那就没法做到一致性。
1.2.2 BASE理论
BASE:全称:Basically Available(基本可用),Soft state(软状态),和 Eventually consistent(最终一致性)三个短语的缩写,来自 ebay 的架构师提出。BASE 理论是对 CAP 中一致性和可用性权衡的结果,其来源于对大型互联网分布式实践的总结,是基于 CAP 定理逐步演化而来的。其核心思想是:
既是无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。
Basically Available(基本可用)
理解: 允许服务降级或者允许响应时间受到一定损失
什么是基本可用呢?假设系统,出现了不可预知的故障,但还是能用,相比较正常的系统而言:
- 响应时间上的损失:正常情况下的搜索引擎 0.5 秒即返回给用户结果,而基本可用的搜索引擎可以在 1 秒作用返回结果。
- 功能上的损失:在一个电商网站上,正常情况下,用户可以顺利完成每一笔订单,但是到了大促期间,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。
Soft state(软状态)
理解: 允许同步数据的时候出现一定时间延迟
什么是软状态呢?相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种 “硬状态”。
软状态指的是:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。
Eventually consistent(最终一致性)
理解: 经过一段时间的同步数据之后,最终都能够达到一个一致的状态
系统能够保证在没有其他新的更新操作的情况下,数据最终一定能够达到一致的状态,因此所有客户端对系统的数据访问最终都能够获取到最新的值。
1.3 分布式事务解决方案-面试
1.XA两段提交(低效率)-2PC
2.TCC三段提交(3段,高效率[不推荐(补偿代码)])
3.本地消息表(MQ+Table)
4.事务消息(RocketMQ[alibaba])
5.Seata(alibaba)
6.RabbitMQ的ACK机制实现分布式事务(作业)
1.3.1 基于XA协议的两阶段提交 2pc
首先我们来简要看下分布式事务处理的XA规范 :

可知XA规范中分布式事务有AP,RM,TM组成:
其中应用程序(Application Program ,简称AP):AP定义事务边界(定义事务开始和结束)并访问事务边界内的资源。
资源管理器(Resource Manager,简称RM):Rm管理计算机共享的资源,许多软件都可以去访问这些资源,资源包含比如数据库、文件系统、打印机服务器等。
事务管理器(Transaction Manager ,简称TM):负责管理全局事务,分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚、失败恢复等。
二阶段协议:
第一阶段TM要求所有的RM准备提交对应的事务分支,询问RM是否有能力保证成功的提交事务分支,RM根据自己的情况,如果判断自己进行的工作可以被提交,那就对工作内容进行持久化,并给TM回执OK;否者给TM的回执NO。RM在发送了否定答复并回滚了已经的工作后,就可以丢弃这个事务分支信息了。
第二阶段TM根据阶段1各个RM prepare的结果,决定是提交还是回滚事务。如果所有的RM都prepare成功,那么TM通知所有的RM进行提交;如果有RM prepare回执NO的话,则TM通知所有RM回滚自己的事务分支。
也就是TM与RM之间是通过两阶段提交协议进行交互的.
优点: 尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)
缺点: 实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
流程看我的图:
mysql

悲观锁 mysql行级锁 oracle表级锁
两阶段提交协议(Two Phase Commitment Protocol)中,涉及到两种角色
一个事务协调者(coordinator):负责协调多个参与者进行事务投票及提交(回滚)
多个事务参与者(participants):即本地事务执行者
总共处理步骤有两个
(1)投票阶段(voting phase):协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。参与者将告知协调者自己的决策:同意(事务参与者本地事务执行成功,但未提交)或取消(本地事务执行故障);
(2)提交阶段(commit phase):收到参与者的通知后,协调者再向参与者发出通知,根据反馈情况决定各参与者是否要提交还是回滚;
1.3.2 TCC补偿机制
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:
- Try 阶段主要是对业务系统做检测及资源预留
- Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
- Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
例如: A要向 B 转账,思路大概是:
我们有一个本地方法,里面依次调用1、首先在 Try 阶段,要先调用远程接口把 B和 A的钱给冻结起来。2、在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。3、如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。
优点: 相比两阶段提交,可用性比较强
缺点: 数据的一致性要差一些。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。
流程看我的图:

1.3.3 消息最终一致性-重点
消息最终一致性应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理,这种思路是来源于ebay。我们可以从下面的流程图中看出其中的一些细节:

基本思路就是:
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。
缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
流程看我的图:
需求:下单同时,减库存
参与者A 订单服务

1订单服务
1.1 订单表插入数据,订单消息表添加数据101。本地事务,可以控制住。1.2 定时任务:扫描订单消息表---》mq发一条消息,订单信息
2商品服务
2.1监听消息队列,收到消息订单详情。2.2sku 减库存,接受到商品订单的消息表。本地事务,可以控制住。2.3 定时任务:扫描接受到商品订单的消息表---》mq发一条消息,我已经减库存了2.4 mq发一条消息 接受到商品订单的消息表 101 staus 2 已经发了消息了
3订单服务
3.1监听消息队列(已经减库存了) 订单消息表 删除
2. 分布式事务框架seata
2.1 seata简介
Seata(原名Fescar) 是阿里18年开源的分布式事务的框架。Fescar的开源对分布式事务框架领域影响很大。作为开源大户,Fescar来自阿里的GTS,经历了好几次双十一的考验,一经开源便颇受关注。后来Fescar改名为Seata。 [https://github.com/seata/seata](https://github.com/seata/seata)
Fescar虽然是二阶段提交协议的分布式事务,但是其解决了XA的一些缺点:
- 单点问题:虽然目前Fescar(0.4.2)还是单server的,但是Fescar官方预计将会在0.5.x中推出HA-Cluster,到时候就可以解决单点问题。
- 同步阻塞:Fescar的二阶段,其再第一阶段的时候本地事务就已经提交释放资源了,不会像XA会再两个prepare和commit阶段资源都锁住,并且Fescar,commit是异步操作,也是提升性能的一大关键。
- 数据不一致:如果出现部分commit失败,那么fescar-server会根据当前的事务模式和分支事务的返回状态的结果来进行不同的重试策略。并且fescar的本地事务会在一阶段的时候进行提交,其实单看数据库来说在commit的时候数据库已经是一致的了。
- 只能用于单一数据库: Fescar提供了三种模式,AT和TCC和混合模式。在AT模式下事务资源可以是任何支持ACID的数据库,在TCC模式下事务资源没有限制,可以是缓存,可以是文件,可以是其他的等等。当然这两个模式也可以混用。
同时Fescar也保留了接近0业务入侵的优点,只需要简单的配置Fescar的数据代理和加个注解,加一个Undolog表,就可以达到我们想要的目的。
2.2 实现原理
Fescar将一个本地事务做为一个分布式事务分支,所以若干个分布在不同微服务中的本地事务共同组成了一个全局事务,结构如下。
官帮助文档: https://seata.io/zh-cn/index.html

TM:全局事务管理器,在标注开启fescar分布式事务的服务端开启,并将全局事务发送到TC事务控制端管理
TC:事务控制中心,控制全局事务的提交或者回滚。这个组件需要独立部署维护,目前只支持单机版本,后续迭代计划会有集群版本
RM:资源管理器,主要负责分支事务的上报,本地事务的管理
一段话简述其实现过程:服务起始方发起全局事务并注册到TC。在调用协同服务时,协同服务的事务分支事务会先完成阶段一的事务提交或回滚,并生成事务回滚的undo_log日志,同时注册当前协同服务到TC并上报其事务状态,归并到同一个业务的全局事务中。此时若没有问题继续下一个协同服务的调用,期间任何协同服务的分支事务回滚,都会通知到TC,TC在通知全局事务包含的所有已完成一阶段提交的分支事务回滚。如果所有分支事务都正常,最后回到全局事务发起方时,也会通知到TC,TC在通知全局事务包含的所有分支删除回滚日志。在这个过程中为了解决写隔离和度隔离的问题会涉及到TC管理的全局锁。
(增加订单(branchId=901),减库存(branchId=109)) xid=101
2.3 Fescar模式
Fescar对分布式事务的实现提供了3种模式,AT模式和TCC模式、saga模式:
2.3.1 AT模式
AT模式:主要关注多 DB 访问的数据一致性,实现起来比较简单,对业务的侵入较小,但性能没有TCC高,这种模式推荐大家使用。
AT模式部分代码如下:不需要关注执行状态,对业务代码侵入较小。
/*** 此代码为示例代码, 不需要演示, 主要看AT和TCC代码的区别使用*/@GlobalTransactional(timeoutMills = 300000, name = "dubbo-demo-tx")public void purchase(String userId, String commodityCode, int orderCount) {LOGGER.info("purchase begin ... xid: " + RootContext.getXID());storageService.deduct(commodityCode, orderCount);orderService.create(userId, commodityCode, orderCount);throw new RuntimeException("AT 模式发生异常,回滚事务");}
AT模式的核心是对业务无侵入,是一种改进后的两阶段提交,其设计思路如图:
第一阶段:

核心在于对业务sql进行解析,转换成undolog,两阶段提交往往对资源的锁定需要持续到第二阶段实际的提交或者回滚操作,而有了回滚日志之后,可以在第一阶段释放对资源的锁定,降低了锁范围,提高效率,即使第二阶段发生异常需要回滚,只需找对undolog中对应数据并反解析成sql来达到回滚目的。Seata通过代理数据源将业务sql的执行解析成undolog来与业务数据的更新同时入库,达到了对业务无侵入的效果。
第二阶段:
如果决议是全局提交,此时分支事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚日志),Phase2 可以非常快速地完成。

如果决议是全局回滚,RM 收到协调器发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。

2.3.3 TCC模式
TCC模式:TCC补偿机制,对代码造成一定的侵入,实现难度较大,这种方式不推荐,不过TCC模式的特点是性能高。
TCC模式部分代码如下:可以看到执行事务回滚,都需要根据不同阶段执行的状态判断,侵入了业务代码。
/*** 此代码为示例代码, 不需要演示, 主要看AT和TCC代码的区别使用* 转账操作* @param from 扣钱账户* @param to 加钱账户* @param amount 转账金额* @return*/@Override@GlobalTransactionalpublic boolean transfer(final String from, final String to, final double amount) {//扣钱参与者,一阶段执行boolean ret = firstTccAction.prepareMinus(null, from, amount);if(!ret){//扣钱参与者,一阶段失败; 回滚本地事务和分布式事务throw new RuntimeException("账号:["+from+"] 预扣款失败");}//加钱参与者,一阶段执行ret = secondTccAction.prepareAdd(null, to, amount);if(!ret){throw new RuntimeException("账号:["+to+"] 预收款失败");}System.out.println(String.format("transfer amount[%s] from [%s] to [%s] finish.", String.valueOf(amount), from, to));return true;}
2.3.3 Saga模式
详见 https://seata.io/zh-cn/docs/dev/mode/saga-mode.html
3 Seata案例
3.1准备工作
1导入资料中的ydles_common_fescar。注意总父工程中加入模块
<module>ydles_common_fescar</module>
2观察模块中的三个类。3数据库ydles_order中的undo_log表为记录相关操作的表

4资料中的fescar-server-0.4.2解压,bin目录中双击fescar-server.bat。**注意**:这是fescar的服务,并且放到一个短目录才能执行。

3.2分布式事务错误演示
1order服务 com.ydles.order.service.impl 的 add方法中增加一个错误 本地事务控制注解增加@Transactional
//减库存skuFeign.decrCount(order.getUsername());int i=1/0;// 5)删除购物车中数据redisTemplate.delete("cart_"+order.getUsername());
2goods 服务 com.ydles.goods.service.impl decrCount减库存方法上增加本地事务控制注解@Transactional
3查看数据库 tb_order tb_order_item中没数据。


tb_sku查看一条数据库存量 100
4登陆购物车并登陆 http://localhost:8001/api/wcart/list
5往购物车添加数据 http://localhost:8001/api/wcart/add?skuId=1450862568724758528&num=5
6购物车页面点击结算到订单页面

7点击提交订单

8代码中 order服务报错

9观察数据库
tb_order tb_order_item没变


tb_sku 1450862568724758528商品库存减少了5

10 为什么
order goods服务是两个服务,即使加上@Transactional也是本地事务控制。

3.3 分布式事务正确演示
1 goods order 增加fescar依赖
<dependency><groupId>com.ydles</groupId><artifactId>ydles_common_fescar</artifactId><version>1.0-SNAPSHOT</version></dependency>
2在订单微服务的OrderServiceImpl的add方法上增加@GlobalTransactional(name = “order_add”)注解

我的购物车

到结算页

下单失败了

3order 服务重启 观察 fescar控制台输出
4goods 服务重启 观察 fescar控制台输出
5重新提交订单 依然失败 但库存不扣减了

4消息队列实现分布式事务—-整个项目的重点
1业务流程-重点
需求:(一次下单—-》用户积分增加一次) 事务

1 order
1.1 order task.本地事务控制。1.2 spring-task扫表 task-----》mq发消息
2user
2.1监听消息 62.2 8相当于锁 11释放锁2.3 mq(另一个队列) task 积分加上了
3order
3.1监听消息 task_his 加上,后期查看,task 删除
2代码实现
2.1准备
1order中添加两张表
tb_task 任务表tb_task_his 历史任务表
2order-api中添加对应的实体类
package com.ydles.order.pojo;import javax.persistence.Column;import javax.persistence.Id;import javax.persistence.Table;import java.util.Date;@Table(name = "tb_task")public class Task {@Idprivate Long id;@Column(name = "create_time")private Date createTime;@Column(name = "update_time")private Date updateTime;@Column(name = "delete_time")private Date deleteTime;@Column(name = "task_type")private String taskType;@Column(name = "mq_exchange")private String mqExchange;@Column(name = "mq_routingkey")private String mqRoutingkey;@Column(name = "request_body")private String requestBody;@Column(name = "status")private String status;@Column(name = "errormsg")private String errormsg;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public Date getCreateTime() {return createTime;}public void setCreateTime(Date createTime) {this.createTime = createTime;}public Date getUpdateTime() {return updateTime;}public void setUpdateTime(Date updateTime) {this.updateTime = updateTime;}public Date getDeleteTime() {return deleteTime;}public void setDeleteTime(Date deleteTime) {this.deleteTime = deleteTime;}public String getTaskType() {return taskType;}public void setTaskType(String taskType) {this.taskType = taskType;}public String getMqExchange() {return mqExchange;}public void setMqExchange(String mqExchange) {this.mqExchange = mqExchange;}public String getMqRoutingkey() {return mqRoutingkey;}public void setMqRoutingkey(String mqRoutingkey) {this.mqRoutingkey = mqRoutingkey;}public String getRequestBody() {return requestBody;}public void setRequestBody(String requestBody) {this.requestBody = requestBody;}public String getStatus() {return status;}public void setStatus(String status) {this.status = status;}public String getErrormsg() {return errormsg;}public void setErrormsg(String errormsg) {this.errormsg = errormsg;}}
package com.ydles.order.pojo;import javax.persistence.Column;import javax.persistence.Id;import javax.persistence.Table;import java.util.Date;@Table(name = "tb_task_his")public class TaskHis {@Idprivate Long id;@Column(name = "create_time")private Date createTime;@Column(name = "update_time")private Date updateTime;@Column(name = "delete_time")private Date deleteTime;@Column(name = "task_type")private String taskType;@Column(name = "mq_exchange")private String mqExchange;@Column(name = "mq_routingkey")private String mqRoutingkey;@Column(name = "request_body")private String requestBody;@Column(name = "status")private String status;@Column(name = "errormsg")private String errormsg;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public Date getCreateTime() {return createTime;}public void setCreateTime(Date createTime) {this.createTime = createTime;}public Date getUpdateTime() {return updateTime;}public void setUpdateTime(Date updateTime) {this.updateTime = updateTime;}public Date getDeleteTime() {return deleteTime;}public void setDeleteTime(Date deleteTime) {this.deleteTime = deleteTime;}public String getTaskType() {return taskType;}public void setTaskType(String taskType) {this.taskType = taskType;}public String getMqExchange() {return mqExchange;}public void setMqExchange(String mqExchange) {this.mqExchange = mqExchange;}public String getMqRoutingkey() {return mqRoutingkey;}public void setMqRoutingkey(String mqRoutingkey) {this.mqRoutingkey = mqRoutingkey;}public String getRequestBody() {return requestBody;}public void setRequestBody(String requestBody) {this.requestBody = requestBody;}public String getStatus() {return status;}public void setStatus(String status) {this.status = status;}public String getErrormsg() {return errormsg;}public void setErrormsg(String errormsg) {this.errormsg = errormsg;}}
3 ydles_user新增积分日志表


4 ydles_service_user_api添加实体类 PointLog
package com.ydles.user.pojo;import javax.persistence.Table;@Table(name = "tb_point_log")public class PointLog {private String orderId;private String userId;private Integer point;public String getOrderId() {return orderId;}public void setOrderId(String orderId) {this.orderId = orderId;}public String getUserId() {return userId;}public void setUserId(String userId) {this.userId = userId;}public Integer getPoint() {return point;}public void setPoint(Integer point) {this.point = point;}}
5rabbitMQ
1 order 服务 导包
<dependency><groupId>org.springframework.amqp</groupId><artifactId>spring-rabbit</artifactId></dependency>
2config 下 rabbitMQ配置类
package com.ydles.order.config;import org.springframework.amqp.core.*;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class RabbitMQConfig {//添加积分任务交换机public static final String EX_BUYING_ADDPOINTUSER = "ex_buying_addpointuser";//添加积分消息队列public static final String CG_BUYING_ADDPOINT = "cg_buying_addpoint";//完成添加积分消息队列public static final String CG_BUYING_FINISHADDPOINT = "cg_buying_finishaddpoint";//添加积分路由keypublic static final String CG_BUYING_ADDPOINT_KEY = "addpoint";//完成添加积分路由keypublic static final String CG_BUYING_FINISHADDPOINT_KEY = "finishaddpoint";//声明交换机@Bean(EX_BUYING_ADDPOINTUSER)public Exchange EX_BUYING_ADDPOINTUSER(){return ExchangeBuilder.directExchange(EX_BUYING_ADDPOINTUSER).durable(true).build();}//声明队列@Bean(CG_BUYING_ADDPOINT)public Queue CG_BUYING_ADDPOINT(){Queue queue = new Queue(CG_BUYING_ADDPOINT);return queue;}@Bean(CG_BUYING_FINISHADDPOINT)public Queue CG_BUYING_FINISHADDPOINT(){Queue queue = new Queue(CG_BUYING_FINISHADDPOINT);return queue;}//队列绑定交换机@Beanpublic Binding BINDING_CG_BUYING_ADDPOINT(@Qualifier(CG_BUYING_ADDPOINT) Queue queue, @Qualifier(EX_BUYING_ADDPOINTUSER)Exchange exchange){return BindingBuilder.bind(queue).to(exchange).with(CG_BUYING_ADDPOINT_KEY).noargs();}@Beanpublic Binding BINDING_CG_BUYING_FINISHADDPOINT(@Qualifier(CG_BUYING_FINISHADDPOINT) Queue queue,@Qualifier(EX_BUYING_ADDPOINTUSER)Exchange exchange){return BindingBuilder.bind(queue).to(exchange).with(CG_BUYING_FINISHADDPOINT_KEY).noargs();}}
3配置文件 rabbitmq
rabbitmq:host: 192.168.200.128
2.2 订单服务逻辑
需求:

1dao层 增加mapper
package com.ydles.order.dao;import com.ydles.order.pojo.Task;import tk.mybatis.mapper.common.Mapper;public interface TaskMapper extends Mapper<Task> {}
package com.ydles.order.dao;import com.ydles.order.pojo.TaskHis;import tk.mybatis.mapper.common.Mapper;public interface TaskHisMapper extends Mapper<TaskHis> {}
2下单逻辑中增加任务
@AutowiredTaskMapper taskMapper;
// 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);
3定时任务,定时发送任务表信息到mq
启动类增加
@EnableScheduling //开启定时任务
新建task包,加入定时任务类
@Componentpublic class QueryPointTask {@Autowiredprivate TaskMapper taskMapper;@Autowiredprivate RabbitTemplate rabbitTemplate;@Scheduled(cron = "0/2 * * * * ?")public void queryTask(){//1. 获取小于系统当前时间的数据List<Task> taskList = taskMapper.findTaskLessThanCurrentTime(new Date());if (taskList != null && taskList.size()>0){//2.将任务发送到消息队列上for (Task task : taskList) {rabbitTemplate.convertAndSend(RabbitMQConfig.EX_BUYING_ADDPOINTUSER, RabbitMQConfig.CG_BUYING_ADDPOINT_KEY, JSON.toJSONString(task));System.out.println("订单服务向添加积分队列发送了一条消息");}}}}
taskmapper中自定义查询小于当前时间的方法
@Select("select * from tb_task where update_time<#{currentTime}")@Results({@Result(column = "create_time",property = "createTime"),@Result(column = "update_time",property = "updateTime"),@Result(column = "delete_time",property = "deleteTime"),@Result(column = "task_type",property = "taskType"),@Result(column = "mq_exchange",property = "mqExchange"),@Result(column = "mq_routingkey",property = "mqRoutingkey"),@Result(column = "request_body",property = "requestBody"),@Result(column = "status",property = "status"),@Result(column = "errormsg",property = "errormsg")})List<Task> findTaskLessThanCurrentTime(Date currentTime);
2.3 用户服务逻辑
需求:

1配置文件增加redis rabbitMQ配置
spring:application:name: userdatasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://192.168.200.128:3306/ydles_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTCusername: rootpassword: rootmain:allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册redis:host: 192.168.200.128rabbitmq:host: 192.168.200.128
2导入mq依赖
<dependency><groupId>org.springframework.amqp</groupId><artifactId>spring-rabbit</artifactId></dependency>
3mq配置类
RabbitMQConfig ,从order服务copy
4添加依赖
<dependency><groupId>com.ydles</groupId><artifactId>ydles_service_order_api</artifactId><version>1.0-SNAPSHOT</version></dependency>
4监听mq队列
com.ydles.user.listener创建AddPointListener
package com.ydles.user.listener;import com.alibaba.fastjson.JSON;import com.ydles.order.pojo.Task;import com.ydles.user.config.RabbitMQConfig;import com.ydles.user.service.UserService;import org.apache.commons.lang.StringUtils;import org.springframework.amqp.rabbit.annotation.RabbitListener;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.Component;@Componentpublic class AddPointListener {@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate UserService userService;@Autowiredprivate RabbitTemplate rabbitTemplate;@RabbitListener(queues = RabbitMQConfig.CG_BUYING_ADDPOINT)public void receiveAddPointMessage(String message){System.out.println("用户服务接收到了任务消息");//转换消息Task task = JSON.parseObject(message, Task.class);if (task == null || StringUtils.isEmpty(task.getRequestBody())){return;}//判断redis中当前的任务是否存在Object value = redisTemplate.boundValueOps(task.getId()).get();if (value != null){return;}}
5更新积分方法实现
1userService 定义接口
int updateUserPoint(Task task);
实现类
@Override@Transactionalpublic int updateUserPoint(Task task) {System.out.println("用户服务现在开始对任务进行处理");//1.从task中获取相关数据Map map = JSON.parseObject(task.getRequestBody(), Map.class);String username = map.get("username").toString();String orderId = map.get("orderId").toString();int point = (int) map.get("point");//2.判断当前的任务是否操作过PointLog pointLog = pointLogMapper.findPointLogByOrderId(orderId);if (pointLog != null){return 0;}//3.将任务存入到redis中redisTemplate.boundValueOps(task.getId()).set("exist",30, TimeUnit.SECONDS);//4.修改用户积分int result = userMapper.updateUserPoint(username,point);if (result<=0){return 0;}//5.记录积分日志信息pointLog = new PointLog();pointLog.setUserId(username);pointLog.setOrderId(orderId);pointLog.setPoint(point);result = pointLogMapper.insertSelective(pointLog);if (result <= 0){return 0;}//6.删除redis中的任务信息redisTemplate.delete(task.getId());System.out.println("用户服务完成了更改用户积分的操作");return 1;}
2dao层新建mapper,查询当前任务是否操作过
package com.ydles.user.dao;import com.ydles.user.pojo.PointLog;import org.apache.ibatis.annotations.Param;import org.apache.ibatis.annotations.Select;import tk.mybatis.mapper.common.Mapper;public interface PointLogMapper extends Mapper<PointLog> {@Select("select * from tb_point_log where order_id =#{orderId}")PointLog findPointLogByOrderId(@Param("orderId") String orderId);}
3userMapper新增一个修改用户积分方法
package com.ydles.user.dao;import com.ydles.user.pojo.User;import org.apache.ibatis.annotations.Param;import org.apache.ibatis.annotations.Update;import tk.mybatis.mapper.common.Mapper;public interface UserMapper extends Mapper<User> {@Update("update tb_user set points=points+#{point} where username=#{username}")int updateUserPoint(@Param("username")String username, @Param("point") int point);}
4AddPointListener完善
package com.ydles.user.listener;import com.alibaba.fastjson.JSON;import com.ydles.order.pojo.Task;import com.ydles.user.config.RabbitMQConfig;import com.ydles.user.service.UserService;import org.apache.commons.lang.StringUtils;import org.springframework.amqp.rabbit.annotation.RabbitListener;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.Component;@Componentpublic class AddPointListener {@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate UserService userService;@Autowiredprivate RabbitTemplate rabbitTemplate;@RabbitListener(queues = RabbitMQConfig.CG_BUYING_ADDPOINT)public void receiveAddPointMessage(String message){System.out.println("用户服务接收到了任务消息");//1转换消息Task task = JSON.parseObject(message, Task.class);if (task == null || StringUtils.isEmpty(task.getRequestBody())){return;}//2判断redis中当前的任务是否存在Object value = redisTemplate.boundValueOps(task.getId()).get();if (value != null){return;}//3更新用户积分int result = userService.updateUserPoint(task);if (result == 0){return;}//4向订单服务返回通知消息rabbitTemplate.convertAndSend(RabbitMQConfig.EX_BUYING_ADDPOINTUSER,RabbitMQConfig.CG_BUYING_FINISHADDPOINT_KEY,JSON.toJSONString(task));System.out.println("用户服务向完成添加积分队列发送了一条消息");}}
2.4订单服务收尾
需求:

建立监听类DelTaskListener
package com.ydles.order.listener;import com.alibaba.fastjson.JSON;import com.ydles.order.config.RabbitMQConfig;import com.ydles.order.pojo.Task;import com.ydles.order.service.TaskService;import org.springframework.amqp.rabbit.annotation.RabbitListener;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;@Componentpublic class DelTaskListener {@Autowiredprivate TaskService taskService;@RabbitListener(queues = RabbitMQConfig.CG_BUYING_FINISHADDPOINT)public void receiveDelTaskMessage(String message){System.out.println("订单服务接收到了删除任务操作的消息");Task task = JSON.parseObject(message, Task.class);//删除原有的任务数据,并向历史任务表中添加记录taskService.delTask(task);}}
创建taskService写出删除任务的方法
public interface TaskService {void delTask(Task task);}
实现taskService
package com.ydles.order.service.impl;import com.ydles.order.dao.TaskHisMapper;import com.ydles.order.dao.TaskMapper;import com.ydles.order.pojo.Task;import com.ydles.order.pojo.TaskHis;import com.ydles.order.service.TaskService;import org.springframework.beans.BeanUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.util.Date;@Servicepublic class TaskServiceImpl implements TaskService {@Autowiredprivate TaskHisMapper taskHisMapper;@Autowiredprivate TaskMapper taskMapper;@Override@Transactionalpublic void delTask(Task task) {//1.记录删除时间task.setDeleteTime(new Date());Long taskId = task.getId();task.setId(null);//2bean拷贝TaskHis taskHis = new TaskHis();BeanUtils.copyProperties(task,taskHis);//3记录历史任务数据taskHisMapper.insertSelective(taskHis);//4删除原有任务数据//taskMapper.deleteByPrimaryKey(id);task.setId(id);taskMapper.delete(task);System.out.println("订单服务完成了添加历史任务并删除原有任务的操作");}}
2.5效果测试
order user服务以dubug打开。
从头到最后一步一步测试。关键点:
1order下单任务表中数据

2order 定时任务扫描表,发信息

3user收到信息,检查redis,修改积分


4order收到成功消息

总结:
1分布式事务解决方案
事务:ACID 面试必问,隔离级别,传播行为
本地事务:@transactional
分布式事务
2解决方案
2.1xa 2pc mysql

2.2 tcc

2.3 消息队列

3Seata
4下单 使用消息队列实现分布式事务

第13章 微信扫码支付
角色: 支付相关模块的开发工程师,难。
学习目标:
- 能够根据微信支付的开发文档调用微信支付的api
- 完成统一下单生成微信支付二维码功能
- 完成支付回调的逻辑处理
- 完成推送支付通知功能
1. 微信支付快速入门

native 支付文档:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1
1.1 微信支付申请(了解)
第一步:注册公众号(类型须为:服务号)
请根据营业执照类型选择以下主体注册:个体工商户| 企业/公司| 政府| 媒体| 其他类型。
第二步:认证公众号
公众号认证后才可申请微信支付,认证费:300元/次。
第三步:提交资料申请微信支付
登录公众平台,点击左侧菜单【微信支付】,开始填写资料等待审核,审核时间为1-5个工作日内。
第四步:开户成功,登录商户平台进行验证
资料审核通过后,请登录联系人邮箱查收商户号和密码,并登录商户平台填写财付通备付金打的小额资金数额,完成账户验证。
第五步:在线签署协议
本协议为线上电子协议,签署后方可进行交易及资金结算,签署完立即生效。
本课程已经提供好“元动力教育”的微信支付账号,学员无需申请。
完成上述步骤,你可以得到调用API用到的账号和密钥
!!!!真实地测试,所以,我们写的接口,钱都会给我们元动力。钱一定设置成1分钱。
appid:微信公众账号或开放平台APP的唯一标识 wxababcd122d1618eb
mch_id:商户号 1611671554
key:商户密钥 ydlclass66666688888YDLCLASS66688
1.2 微信支付开发文档与SDK
在线微信支付开发文档:
https://pay.weixin.qq.com/wiki/doc/api/index.html
https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_1
微信支付接口调用的整体思路:
按API要求组装参数,以XML方式发送(POST)给微信支付接口(URL),微信支付接口也是以XML方式给予响应。程序根据返回的结果(其中包括支付URL)生成二维码或判断订单状态。
我们解压从官网下载的sdk ,安装到本地仓库
com.github.wxpay.sdk.WXPay类下提供了对应的方法:
| 方法名 | 说明 |
|---|---|
| microPay | 刷卡支付 |
| unifiedOrder | 统一下单 |
| orderQuery | 查询订单 |
| reverse | 撤销订单 |
| closeOrder | 关闭订单 |
| refund | 申请退款 |
| refundQuery | 查询退款 |
| downloadBill | 下载对账单 |
| report | 交易保障 |
| shortUrl | 转换短链接 |
| authCodeToOpenid | 授权码查询openid |
测试工程
1创建test_pay ,导入依赖。将我的仓库com.github.wxpay相关包发给学生。
<dependencies><dependency><groupId>com.github.wxpay</groupId><artifactId>wxpay-sdk</artifactId><version>3.0.9</version></dependency><dependency><groupId>commons-logging</groupId><artifactId>commons-logging</artifactId><version>1.2</version></dependency></dependencies>
2创建配置类 注意:包名必须为com.github.wxpay.sdk
package com.github.wxpay.sdk;import java.io.InputStream;public class MyConfig extends WXPayConfig {//正式public String getAppID() {return "wxababcd122d1618eb";}public String getMchID() {return "1611671554";}//测试public String getKey() {return "ydlclass66666688888YDLCLASS66688";}//正式//public String getKey() {// return "0c804332f41cbf77b014d7096ae900f6";//}public InputStream getCertStream() {return null;}public IWXPayDomain getWXPayDomain() {return new IWXPayDomain() {public void report(String s, long l, Exception e) {}public DomainInfo getDomain(WXPayConfig wxPayConfig) {return new DomainInfo("api.mch.weixin.qq.com", true);}};}}
3测试类 包名随意
package ydles.test;import com.github.wxpay.sdk.MyConfig;import com.github.wxpay.sdk.WXPay;import java.util.HashMap;import java.util.Map;/*** creste by ydles.itcast*/public class PayTest {public static void main(String[] args) throws Exception {MyConfig myConfig=new MyConfig();WXPay wxPay=new WXPay(myConfig);//根据官方文档 必填值封装数据Map<String,String> map = new HashMap<>();map.put("body", "元动力二奢");map.put("out_trade_no", "123654");map.put("total_fee", "1");map.put("spbill_create_ip", "127.0.0.1");map.put("notify_url", "http://www.baidu.com");map.put("trade_type", "NATIVE");Map<String, String> resultMap = wxPay.unifiedOrder(map);System.out.println(resultMap);}}
4得到
{nonce_str=l1pTBT12JvPIN0lO, code_url=weixin://wxpay/bizpayurl?pr=PFNjrIXzz, appid=wxababcd122d1618eb, sign=FF218956CA8C00145F4BACBC0AA843012E2DE2498FAAD4809769EB367582340A, trade_type=NATIVE, return_msg=OK, result_code=SUCCESS, mch_id=1611671554, return_code=SUCCESS, prepay_id=wx15174918803690023343d15a9737140000}
QRcode
1打开资源文件夹 weixinpay.html

支付图片为qrcode.js生成。修改源代码为本人生成的,即可微信扫描。
222 qrcode.makeCode("weixin://wxpay/bizpayurl?pr=PFNjrIXzz");

2. 微信支付二维码
2.1 需求分析
用户在提交订单后,如果是选择支付方式为微信支付,那应该跳转到微信支付二维码页面,用户扫描二维码可以进行支付,金额与订单金额相同。
流程:商品详情页——-》购物车——-》订单——》支付方式pay.html———》微信支付页weixinpay.html————》支付成功

2.2 实现思路
前端页面向后端传递订单号,后端根据订单号查询订单,检查是否为当前用户的未支付订单,如果是则根据订单号和金额生成支付url返给前端,前端得到支付url生成支付二维码。
2.3 代码实现
2.3.1 提交订单跳转支付页
订单——》支付方式pay.html
1更新ydles_service_order
OrderServiceImpl中add() ,设置返回值为订单Id
public String add(Order order)
//最后返回return order.getId();
OrderService
String add(Order order);
OrderController
String orderId = orderService.add(order);return new Result(true,StatusCode.OK,"添加成功",orderId);
2修改web-order中提交订单页面,下单成功跳转至选择支付页面。
if (response.data.flag){//添加订单成功alert("添加订单成功");var orderId = response.data.data;location.href="/api/worder/toPayPage?orderId="+orderId;} else{alert("添加订单失败");}
3在OrderController中新增方法,用于跳转支付页
@GetMapping("/toPayPage")public String toPayPage(String orderId,Model model){//获取到订单的相关信息Order order = orderFeign.findById(orderId).getData();model.addAttribute("orderId",orderId);model.addAttribute("payMoney",order.getPayMoney());return "pay";}
4order服务中已经有方法:orderController中 根据订单id查询订单信息 findById
/**** 根据ID查询数据* @param id* @return*/@GetMapping("/{id}")public Result findById(@PathVariable String id){Order order = orderService.findById(id);return new Result(true,StatusCode.OK,"查询成功",order);}
把它暴露出去 OrderFeign
@FeignClient(name = "order")public interface OrderFeign {@PostMapping("/order")public Result add(@RequestBody Order order);@GetMapping("/order/{id}")public Result<Order> findById(@PathVariable String id);}
5相关页面放至resources
6测试
登陆 http://localhost:8001/api/oauth/toLogin
添加商品: http://localhost:8001/api/wcart/add?skuId=1450862568724758528&num=5
查看购物车: http://localhost:8001/api/wcart/list

点击结算

点击提交订单,跳转至支付页面

2.3.2 支付微服务权限集成

1service二级目录下,新建微服务ydles_service_pay
2依赖
<dependencies><dependency><groupId>com.ydles</groupId><artifactId>ydles_common</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>com.github.wxpay</groupId><artifactId>wxpay-sdk</artifactId><version>3.0.9</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency></dependencies>
3配置文件 application.yml
server:port: 9010spring:application:name: payrabbitmq:host: 192.168.200.128main:allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册eureka:client:service-url:defaultZone: http://127.0.0.1:6868/eurekainstance:prefer-ip-address: truewxpay:notify_url: http://itlils.cross.echosite.cn/wxpay/notify #回调地址
4微信支付配置类,copy。注意包名
package com.github.wxpay.sdk;import java.io.InputStream;/*** @Created by IT李老师* 公主号 “IT李哥交朋友”* 个人微 itlils* 微信支付配置类,放着商户的相关信息*/public class MyConfig extends WXPayConfig{@OverrideString getAppID() {return "wxababcd122d1618eb";}@OverrideString getMchID() {return "1611671554";}@OverrideString getKey() {return "ydlclass66666688888YDLCLASS66688";}@OverrideInputStream getCertStream() {return null;}@OverrideIWXPayDomain getWXPayDomain() {return new IWXPayDomain() {@Overridepublic void report(String s, long l, Exception e) {}@Overridepublic DomainInfo getDomain(WXPayConfig wxPayConfig) {return new DomainInfo("api.mch.weixin.qq.com", true);}};}}
5启动类 com.ydles.pay下
package com.ydles.pay;import com.github.wxpay.sdk.MyConfig;import com.github.wxpay.sdk.WXPay;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.EnableEurekaClient;import org.springframework.context.annotation.Bean;@SpringBootApplication@EnableEurekaClientpublic class PayApplication {public static void main(String[] args) {SpringApplication.run(PayApplication.class,args);}@Beanpublic WXPay wxPay(){try {return new WXPay(new MyConfig());} catch (Exception e) {e.printStackTrace();return null;}}}
2.3.3 支付微服务-下单
ydles_service_pay服务
(1)创建com.ydles.pay.service包,包下创建接口WxPayService
package com.ydles.pay.service;import java.util.Map;/*** @Created by IT李老师* 公主号 “IT李哥交朋友”* 个人微 itlils*/public interface WxPayService {//微信下单要二维码public Map nativePay(String orderId,Integer money);}
(2)创建com.ydles.pay.service.impl 包 ,新增服务类WxPayServiceImpl 拓展:涉及钱的计算,注意用BigDecimal https://www.cnblogs.com/zhangyinhua/p/11545305.html
package com.ydles.pay.service.impl;import com.github.wxpay.sdk.MyConfig;import com.github.wxpay.sdk.WXPay;import com.ydles.pay.service.WxPayService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.math.BigDecimal;import java.util.HashMap;import java.util.Map;/*** @Created by IT李老师* 公主号 “IT李哥交朋友”* 个人微 itlils*/@Servicepublic class WxPayServiceImpl implements WxPayService {@AutowiredWXPay wxPay;@Overridepublic Map nativePay(String orderId, Integer money) {try {//根据官方文档 请求接口Map<String, String> reqData=new HashMap<>();reqData.put("body","动力二奢下单支付");reqData.put("out_trade_no", orderId);//金钱计算 BigDecimal:金额和计算都用他的BigDecimal yuan=new BigDecimal("0.01");BigDecimal beishu=new BigDecimal(100);BigDecimal fen = yuan.multiply(beishu);fen=fen.setScale(0,BigDecimal.ROUND_UP);reqData.put("total_fee", String.valueOf(fen));reqData.put("spbill_create_ip", "192.168.1.1");reqData.put("notify_url", "http://www.baidu.com");reqData.put("trade_type", "NATIVE");Map<String, String> resultMap = wxPay.unifiedOrder(reqData);System.out.println(resultMap);return resultMap;}catch (Exception e){e.printStackTrace();return null;}}}
(3)创建com.ydles.pay.controller包 ,新增WxPayController
package com.ydles.pay.controller;import com.ydles.entity.Result;import com.ydles.entity.StatusCode;import com.ydles.pay.service.WxPayService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import java.util.Map;/*** @Created by IT李老师* 公主号 “IT李哥交朋友”* 个人微 itlils*/@RestController@RequestMapping("/wxpay")public class WxPayController {@AutowiredWxPayService wxPayService;//微信下单@GetMapping("/nativePay")public Result<Map> nativePay(@RequestParam("orderId")String orderId,@RequestParam("money")Integer money){Map map = wxPayService.nativePay(orderId, money);return new Result(true, StatusCode.OK,"微信下单成功",map);}}
测试:http://localhost:9010/wxpay/nativePay?orderId=123321&money=1 订单号可能重复,大家换一下就好了。

2.3.3 支付渲染页面微服务
支付方式pay.html———》微信支付页weixinpay.html
页面需要调用我们的微服务,所以:

(1)新增ydles_service_pay_api模块 ,pom.xml中加入依赖
<dependencies><dependency><groupId>com.ydles</groupId><artifactId>ydles_common</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies>
新增com.ydles.pay.feign包,包下创建接口
package com.ydles.pay.feign;import com.ydles.entity.Result;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import java.util.Map;@FeignClient(name = "pay")public interface PayFeign {@GetMapping("/wxpay/nativePay")public Result nativePay(@RequestParam("orderId") String orderId, @RequestParam("money") Integer money);}
(2)ydles_web_order的pom.xml加入依赖
<dependency><groupId>com.ydles</groupId><artifactId>ydles_service_pay_api</artifactId><version>1.0-SNAPSHOT</version></dependency>
启动类增加扫包
@EnableFeignClients(basePackages = {"com.ydles.pay.feign","com.ydles.order.feign","com.ydles.user.feign"})

ydles_web_order新增PayController
package com.ydles.order.controller;import com.ydles.pay.feign.WxPayFeign;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import java.util.Map;@Controller@RequestMapping("/wxpay")public class PayController {@Autowiredprivate OrderFeign orderFeign;@Autowiredprivate PayFeign payFeign;//跳转到微信支付二维码页面@GetMappingpublic String wxPay(String orderId , Model model){//1.根据orderid查询订单,如果订单不存在,跳转到错误页面Result<Order> orderResult = orderFeign.findById(orderId);if (orderResult.getData() == null){return "fail";}//2.根据订单的支付状态进行判断,如果不是未支付的订单,跳转到错误页面Order order = orderResult.getData();if (!"0".equals(order.getPayStatus())){return "fail";}//3.基于payFeign调用统计下单接口,并获取返回结果Result payResult = payFeign.nativePay(orderId, order.getPayMoney());if (payResult.getData() == null){return "fail";}//4.封装结果数据Map payMap = (Map) payResult.getData();payMap.put("orderId",orderId);payMap.put("payMoney",order.getPayMoney());model.addAllAttributes(payMap);return "wxpay";}
(4)将静态原型中wxpay.html拷贝到templates文件夹下作为模板,修改模板,部分代码如下:
二维码地址渲染
222 qrcode.makeCode([[${code_url}]]);
查看显示订单号与金额
<h4 class="fl tit-txt"><span class="success-icon"></span><span class="success-info" th:text="|订单提交成功,请您及时付款!订单号:${orderId}|"></span></h4><span class="fr"><em class="sui-lead">应付金额:</em><em class="orange money" th:text="${#numbers.formatDecimal(payMoney/100.0,1,2)}"></em>元</span>
pay.html
109 <li><a th:href="|/api/wxpay?orderId=${orderId}|"><img src="/img/_/pay3.jpg"></a> </li>

ydles_gateway_web项目的application.yml文件加入
#购物车订单渲染微服务- id: ydles_order_web_routeuri: lb://order-webpredicates:- Path=/api/wcart/**,/api/worder/**,/api/wxpay/**filters:- StripPrefix=1
查看ydles_gateway_web项目的UrlFilter.java中
public static String orderFilterPath = "/api/wpay,/api/wpay/**,/api/worder/**,/api/user/**,
测试
1重启gatewaty-web service-pay web-order
2添加商品: http://localhost:8001/api/wcart/add?skuId=1450862568724758528&num=5

3查看购物车: http://localhost:8001/api/wcart/list

4点击:结算—-》提交订单—》选择微信支付—》跳到二维码页面—》扫描二维码,你将损失1分钱。

3. 支付回调逻辑处理

3.1 需求分析
在完成支付后,修改订单状态为已支付,并记录订单日志。
3.2 实现思路
(1)接受微信支付平台的回调信息(xml)
<xml><appid><![CDATA[wx8397f8696b538317]]></appid><bank_type><![CDATA[CFT]]></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[c6bea293399a40e0a873df51e667f45a]]></nonce_str><openid><![CDATA[oNpSGwbtNBQROpN_dL8WUZG3wRkM]]></openid><out_trade_no><![CDATA[1553063775279]]></out_trade_no><result_code><![CDATA[SUCCESS]]></result_code><return_code><![CDATA[SUCCESS]]></return_code><sign><![CDATA[DD4E5DF5AF8D8D8061B0B8BF210127DE]]></sign><time_end><![CDATA[20190320143646]]></time_end><total_fee>1</total_fee><trade_type><![CDATA[NATIVE]]></trade_type><transaction_id><![CDATA[4200000248201903206581106357]]></transaction_id></xml>
(2)收到通知后,调用查询接口查询订单。
(3)如果支付结果为成功,则调用修改订单状态和记录订单日志的方法。
3.3 代码实现
3.3.1 内网穿透工具
natapp https://natapp.cn/

3.3.2下载配置文件和客户端
如何使用:https://natapp.cn/article/natapp_newbie
解压资料,将配置文件与程序平行放置。修改配置文件。


3.3.3 启动
双击natapp.exe

外网地址默认映射本地的80端口 可以修改

3.3.4 测试
1ydles_service_pay 微服务 WXPayController 增加一个测试方法
@RequestMapping("/notify")public void notifyLogic() {System.out.println("支付成功回调");}
2重启ydles_service_pay
3访问 http://localhost:9010/wxpay/notify 程序打印输出

4访问 http://lizihao.cross.echosite.cn/wxpay/notify 程序打印输出

3.3.5 接收回调信息
1 修改支付微服务配置文件
wxpay:notify_url: http://mp93g5.natappfree.cc/wxpay/notify #回调地址
2修改WxPayServiceImpl ,引入
@Value("${wxpay.notify_url}")private String notifyUrl;
3修改WxPayServiceImpl 的nativePay方法
map.put("notify_url",notifyUrl);//回调地址
4测试:
4.1重启ydles_service_pay
4.2添加商品: http://localhost:8001/api/wcart/add?skuId=1450862568724758528&num=5
4.3查看购物车: http://localhost:8001/api/wcart/list

4.4点击:结算—-》提交订单—》选择微信支付—》跳到二维码页面—》扫描二维码 查看可以回调多次

为什么多次回调? https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_7&index=8
注意:1、同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。2、后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信会判定本次通知失败,重新发送通知,直到成功为止(在通知一直不成功的情况下,微信总共会发起多次通知,通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m),但微信不保证通知最终一定能成功。
3.3.6 处理回调通知-重点
1资源文件夹 ConvertUtils 放到 common工程的utils包下
2微信支付平台发送给回调地址的是二进制流,我们需要提取二进制流转换为字符串,这个字符串就是xml格式。
修改notify方法
/*** 回调*/@RequestMapping("/notify")public String wxPayNotify(HttpServletRequest request, HttpServletResponse response) throws IOException {System.out.println("支付成功回调。。。。");try {//1输入流转换为字符串String xml = ConvertUtils.convertToString(request.getInputStream());System.out.println(xml);//如果成功,给微信支付一个成功的响应//响应数据设置//2给微信一个结果通知response.setContentType("text/xml");String data = "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";response.getWriter().write(data);} catch (Exception e) {e.printStackTrace();}}
注意:1、同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。2、后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信会判定本次通知失败,重新发送通知,直到成功为止(在通知一直不成功的情况下,微信总共会发起10次通知,通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m),但微信不保证通知最终一定能成功。
测试后,在控制台看到输出的消息
访问到 notify接口了!<xml><appid><![CDATA[wxababcd122d1618eb]]></appid><bank_type><![CDATA[OTHERS]]></bank_type><cash_fee><![CDATA[1]]></cash_fee><fee_type><![CDATA[CNY]]></fee_type><is_subscribe><![CDATA[Y]]></is_subscribe><mch_id><![CDATA[1611671554]]></mch_id><nonce_str><![CDATA[WNXFCMMCQSYiM09qrr4nwDH41iJnwuSs]]></nonce_str><openid><![CDATA[o9tV755anFQYm27bYNC1ALQ5Ovfs]]></openid><out_trade_no><![CDATA[1473316925973991424]]></out_trade_no><result_code><![CDATA[SUCCESS]]></result_code><return_code><![CDATA[SUCCESS]]></return_code><sign><![CDATA[E6A69A88C986B64411ACB2DA7E945EB48BF6391C06B0972DA6AEC80C00FC74F9]]></sign><time_end><![CDATA[20211221233851]]></time_end><total_fee>1</total_fee><trade_type><![CDATA[NATIVE]]></trade_type><transaction_id><![CDATA[4200001414202112216382710751]]></transaction_id></xml>
我们可以将此xml字符串,转换为map,提取其中的out_trade_no(订单号),根据订单号修改订单状态。
3.3.7 收到微信通知后的逻辑
支付服务:查询订单验证通知

微信方面查询订单如何做 https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_2

(1)WxPayService新增方法定义
/*** 查询订单* @param orderId* @return*/Map queryOrder(String orderId);
(2)WxPayServiceImpl实现方法
@Overridepublic Map queryOrder(String orderId) {try{Map<String ,String> map = new HashMap();map.put("out_trade_no",orderId);Map<String, String> resultMap = wxPay.orderQuery(map);return resultMap;}catch (Exception e){e.printStackTrace();return null;}}
(3)修改notify方法
下单:

下单通知成功与否在哪儿? https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_7&index=8

我们的订单号在哪儿? 在下单后微信回调的请求里。


=================================================================================================
查询:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_2
查询结果如何?

查询订单结果结果如何?

此次查询下单我们商品的订单号是什么?

此次支付动作的id在哪儿?

@RequestMapping("/wxpay")@RestControllerpublic class WXPayController {@Autowiredprivate WXPayService wxPayService;@Autowiredprivate RabbitTemplate rabbitTemplate;//下单@GetMapping("/nativePay")public Result nativePay(@RequestParam("orderId") String orderId, @RequestParam("money") Integer money) {Map resultMap = wxPayService.nativePay(orderId, money);return new Result(true, StatusCode.OK, "", resultMap);}@RequestMapping("/notify")public void notifyLogic(HttpServletRequest request, HttpServletResponse response) {System.out.println("支付成功回调");try {//1输入流转换为字符串String xml = ConvertUtils.convertToString(request.getInputStream());System.out.println(xml);//3基于微信发送的通知内容,完成后续的业务逻辑处理Map<String, String> map = WXPayUtil.xmlToMap(xml);if ("SUCCESS".equals(map.get("result_code"))) {//查询订单Map result = wxPayService.queryOrder(map.get("out_trade_no"));System.out.println("查询订单结果:" + result);if ("SUCCESS".equals(result.get("result_code"))) {//将订单的消息发送到mq'Map message = new HashMap();message.put("orderId", result.get("out_trade_no"));message.put("transactionId", result.get("transaction_id"));//消息的发送rabbitTemplate.convertAndSend("", RabbitMQConfig.ORDER_PAY, JSON.toJSONString(message));} else {//输出错误原因System.out.println(map.get("err_code_des"));}} else {//输出错误原因System.out.println(map.get("err_code_des"));}//2给微信一个结果通知response.setContentType("text/xml");String data = "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";response.getWriter().write(data);} catch (Exception e) {e.printStackTrace();}}}
config包下增加rebbitMQ配置类。并检查依赖和配置文件。
package com.ydles.pay.config;import org.springframework.amqp.core.Queue;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class RabbitMQConfig {public static final String ORDER_PAY="order_pay";@Beanpublic Queue queue(){return new Queue(ORDER_PAY);}}
3.3.8 订单服务修改订单状态
需求:

1rabbitMQ配置类,把支付服务定义的队列定义
public static final String ORDER_PAY="order_pay";@Beanpublic Queue queue(){return new Queue(ORDER_PAY);}
2 com.ydles.order.listener包下创建OrderPayListener
@Componentpublic class OrderPayListener {@Autowiredprivate OrderService orderService;@RabbitListener(queues = RabbitMQConfig.ORDER_PAY)public void receivePayMessage(String message) {System.out.println("接收到了订单支付的消息:" + message);Map map = JSON.parseObject(message, Map.class);//调用业务层,完成订单数据库的修改orderService.updatePayStatus((String) map.get("orderId"), (String) map.get("transactionId"));}}
3OrderService接口新增方法定义
/*** 修改订单状态为已支付* @param orderId* @param transactionId*/void updatePayStatus(String orderId,String transactionId);
4OrderServiceImpl新增方法实现
@Autowiredprivate OrderLogMapper orderLogMapper;@Overridepublic void updatePayStatus(String orderId, String transactionId) {Order order = orderMapper.selectByPrimaryKey(orderId);if(order!=null && "0".equals(order.getPayStatus())){ //存在订单且状态为0order.setPayStatus("1");order.setOrderStatus("1");order.setUpdateTime(new Date());order.setPayTime(new Date());order.setTransactionId(transactionId);//微信返回的交易流水号orderMapper.updateByPrimaryKeySelective(order);//记录订单变动日志OrderLog orderLog=new OrderLog();orderLog.setId( idWorker.nextId()+"" );orderLog.setOperater("system");// 系统orderLog.setOperateTime(new Date());//当前日期orderLog.setOrderStatus("1");orderLog.setPayStatus("1");orderLog.setRemarks("支付流水号"+transactionId);orderLog.setOrderId(order.getId());orderLogMapper.insertSelective(orderLog);}}
3.3.9 整个流程测试
1重启ydles_service_pay ydles_service_order
2添加商品: http://localhost:8001/api/cart/addCart?skuId=100000006163&num=10
3查看购物车: http://localhost:8001/api/wcart/list
4点击:结算—-》提交订单—》选择微信支付—》跳到二维码页面—》扫描二维码
4.1扫码支付之前,查看订单状态
4.1扫码支付之后,查看订单状态

订单日志表,也有一条数据了

4. 推送支付通知
4.1 需求分析
当用户完成扫码支付后,跳转到支付成功页面
4.2 服务端推送方案
需求:我们需要将支付的结果通知前端页面,其实就是我们通过所说的服务器端推送,主要有三种实现方案
(1)Ajax 短轮询 setInterval
Ajax 轮询主要通过页面端的 JS 定时异步刷新任务来实现数据的加载
如果我们使用ajax短轮询方式,需要后端提供方法,通过调用微信支付接口实现根据订单号查询支付状态的方法(参见查询订单API) 。 前端每间隔三秒查询一次,如果后端返回支付成功则执行页面跳转。
缺点:这种方式实时效果较差,而且对服务端的压力也较大。
(2)长轮询
长轮询主要也是通过 Ajax 机制,但区别于传统的 Ajax 应用,长轮询的服务器端会在没有数据时阻塞请求直到有新的数据产生或者请求超时才返回,之后客户端再重新建立连接获取数据。
如果使用长轮询,也同样需要后端提供方法,通过调用微信支付接口实现根据订单号查询支付状态的方法,只不过循环是写在后端的。
缺点:长轮询服务端会长时间地占用资源,如果消息频繁发送的话会给服务端带来较大的压力。
(3)WebSocket 双向通信
WebSocket 是 HTML5 中一种新的通信协议,能够实现浏览器与服务器之间全双工通信。如果浏览器和服务端都支持 WebSocket 协议的话,该方式实现的消息推送无疑是最高效、简洁的。并且最新版本的 IE、Firefox、Chrome 等浏览器都已经支持 WebSocket 协议,Apache Tomcat 7.0.27 以后的版本也开始支持 WebSocket。
4.3 RabbitMQ Web STOMP 插件
借助于 RabbitMQ 的 Web STOMP 插件,实现浏览器与服务端的全双工通信。从本质上说,RabbitMQ 的 Web STOMP 插件也是利用 WebSocket 对 STOMP 协议进行了一次桥接,从而实现浏览器与服务端的双向通信。

4.3.1 STOMP协议
STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议。前身是TTMP协议(一个简单的基于文本的协议),专为消息中间件设计。它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议由于设计简单,易于开发客户端,因此在多种语言和多种平台上得到广泛地应用。
4.3.2 插件安装
1查询所有docker 容器
docker ps

2我们进入rabbitmq容器
docker exec -it 410a37e15588 /bin/bash

执行下面的命令开启stomp插件
rabbitmq-plugins enable rabbitmq_web_stomp rabbitmq_web_stomp_examples

退出容器
exit

将当前的容器提交为新的镜像
docker commit 410a37e15588 rabbitmq:stomp

停止当前的容器
docker stop 410a37e15588

删除当前的容器
docker rm 410a37e15588

根据新的镜像重新创建容器
docker run -di --name=ydles_rabbitmq1 -p 5671:5617 -p 5672:5672 -p 4369:4369 -p 15671:15671 -p 15672:15672 -p 25672:25672 -p 15670:15670 -p 15674:15674 rabbitmq:stomp
设置容器开机自动启动
docker update --restart=always 54b0eea9edbb #根据上面生成的容器号改变

插件已经安转好了http://192.168.200.128:15670/

4.3.3 消息推送测试
1观察wxpay.html
<script src="/js/plugins/qrcode.min.js"></script><script src="/js/plugins/stomp.min.js"></script><script type="text/javascript" th:inline="javascript">let qrcode = new QRCode(document.getElementById("qrcode"), {width : 240,height : 240});qrcode.makeCode("weixin://wxpay/bizpayurl?pr=XzofHwG");let client = Stomp.client('ws://192.168.200.128:15674/ws');let on_connect = function(x) {id = client.subscribe("/exchange/paynotify", function(d) {});};let on_error = function() {console.log('error');};client.connect('guest', 'guest', on_connect, on_error, '/');</script>
所以我们需要创建一个交换机paynotify
destination 在 RabbitMQ Web STOM 中进行了相关的定义,根据使用场景的不同,主要有以下 4 种:
- 1./exchange/
对于 SUBCRIBE frame,destination 一般为/exchange//[/pattern] 的形式。该 destination 会创建一个唯一的、自动删除的、名为的 queue,并根据 pattern 将该 queue 绑定到所给的 exchange,实现对该队列的消息订阅。
对于 SEND frame,destination 一般为/exchange//[/routingKey] 的形式。这种情况下消息就会被发送到定义的 exchange 中,并且指定了 routingKey。
2./queue/
对于 SUBCRIBE frame,destination 会定义的共享 queue,并且实现对该队列的消息订阅。
对于 SEND frame,destination 只会在第一次发送消息的时候会定义的共享 queue。该消息会被发送到默认的 exchange 中,routingKey 即为。3./amq/queue/
这种情况下无论是 SUBCRIBE frame 还是 SEND frame 都不会产生 queue。但如果该 queue 不存在,SUBCRIBE frame 会报错。
对于 SUBCRIBE frame,destination 会实现对队列的消息订阅。
对于 SEND frame,消息会通过默认的 exhcange 直接被发送到队列中。4./topic/
对于 SUBCRIBE frame,destination 创建出自动删除的、非持久的 queue 并根据 routingkey 为绑定到 amq.topic exchange 上,同时实现对该 queue 的订阅。
对于 SEND frame,消息会被发送到 amq.topic exchange 中,routingKey 为。
2 http://192.168.200.128:15672/#/exchanges 新建交换机。名字:paynotify 。类型 :fanout。

3修改wxpay.html
<script src="/js/plugins/qrcode.min.js"></script><script src="/js/plugins/stomp.min.js"></script><script type="text/javascript" th:inline="javascript">let qrcode = new QRCode(document.getElementById("qrcode"), {width : 240,height : 240});qrcode.makeCode("weixin://wxpay/bizpayurl?pr=XzofHwG"); //自己的微信图片let client = Stomp.client('ws://192.168.200.128:15674/ws');let on_connect = function(x) {id = client.subscribe("/exchange/paynotify", function(d) {alert(d.body);});};let on_error = function() {console.log('error');};client.connect('guest', 'guest', on_connect, on_error, '/');</script>
4将wxpay.html放到static中。因为对比静态页面文件夹,页面获取的是平级的js。打开页面,观察console。如果有错,将static打包,发给大家。
页面效果

5尝试发送一条消息
点击rabbitMQ队列交换机页面的paynotify

点击发送消息

效果

6新建用户
为了安全,我们在页面上不能用我们的rabbitmq的超级管理员用户guest,所以我们需要在rabbitmq中新建一个普通用户webguest(普通用户无法登录管理后台)

设置虚拟目录权限,打开这个用户权限页:

点击设置权限

页面用户名和密码修改
client.connect('webguest', 'webguest', on_connect, on_error, '/');
刷新页面,还可以看到连接信息即可。

4.4 代码实现
实现思路:后端在收到回调通知后发送订单号给mq(paynotify交换器),前端通过stomp连接到mq订阅

1 修改notifyLogic方法,在"SUCCESS".equals(result.get("result_code")) 后添加
if ("SUCCESS".equals(result.get("result_code"))) {//将订单的消息发送到mq'Map message = new HashMap();message.put("orderId", result.get("out_trade_no"));message.put("transactionId", result.get("transaction_id"));//消息的发送rabbitTemplate.convertAndSend("", RabbitMQConfig.ORDER_PAY, JSON.toJSONString(message));//完成双向通信rabbitTemplate.convertAndSend("paynotify","", result.get("out_trade_no"));}
2 ydles_web_order的PayController中新增跳转支付成功接口
//支付成功跳转@GetMapping("/topaysuccess")public String topaysuccess(String payMoney, Model model) {model.addAttribute("payMoney", payMoney);return "paysuccess";}

3 修改ydles_web_order项目的wxpay.html ,渲染js代码订单号和支付金额部分
<script src="/js/plugins/qrcode.min.js"></script><script src="/js/plugins/stomp.min.js"></script><script type="text/javascript" th:inline="javascript">let qrcode = new QRCode(document.getElementById("qrcode"), {width: 240,height: 240});qrcode.makeCode([[${code_url}]]);let client = Stomp.client('ws://192.168.200.128:15674/ws');let on_connect = function (x) {id = client.subscribe("/exchange/paynotify", function (d) {alert(d.body);let orderId = [[${orderId}]]if (d.body == orderId) {//跳转页面location.href = "/api/wxpay/toPaySuccess?payMoney=" + [[${payMoney}]]}});};let on_error = function () {console.log('error');};client.connect('webguest', 'webguest', on_connect, on_error, '/');</script>
(3)将paysuccess.html拷贝到templates文件夹 。
5. 测试访问路径
1重启ydles_service_pay ydles_service_order
2添加商品: http://localhost:8001/api/cart/addCart?skuId=100000006163&num=10
3查看购物车: http://localhost:8001/api/wcart/list
4点击:结算—-》提交订单—》选择微信支付—》跳到二维码页面—》扫描二维码
5 支付完成 回传订单数据
6确定后,跳转至支付成功页面

总结:
1了解微信支付怎么做
第三方接口平台对接:第三方官网文档一步一步做
2申请支付的二维码

3支付回推

natapp 内网穿透

4用户回推 支付成功消息


第14章 订单处理
角色:还是订单组,资深架构师。
课程内容:
- 通过rabbitmq的延迟消息完成超时订单处理
- 完成批量发货功能,了解第三方物流系统
- 完成自动收货功能
1. 超时未支付订单处理-重点
1.1 需求分析
超过限定时间并未支付的订单,我们需要进行超时订单的处理:先调用微信支付api,查询该订单的支付状态。如果未支付调用关闭订单的api,并修改订单状态为已关闭,并回滚库存数。如果该订单已经支付,则做补偿操作(修改订单状态和记录)。
1.2 实现思路
如何获取超过限定时间的订单?我们可以使用延迟消息队列(死信队列)来实现。
所谓延迟消息队列,就是消息的生产者发送的消息并不会立刻被消费,而是在设定的时间之后才可以消费。
我们可以在订单创建时发送一个延迟消息,消息为订单号,系统会在限定时间之后取出这个消息,然后查询订单的支付状态,根据结果做出相应的处理。
1.3 rabbitmq延迟消息
使用RabbitMQ来实现延迟消息必须先了解RabbitMQ的两个概念:消息的TTL和死信Exchange,通过这两者的组合来实现上述需求。
1.3.1 消息的TTL(Time To Live)
消息的TTL就是消息的存活时间。RabbitMQ可以对队列和消息分别设置TTL。对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。
我们创建一个队列queue.temp,在Arguments 中添加x-message-ttl 为5000 (单位是毫秒),那每一个进入这个队列的消息在5秒后会消失。

1.3.2 死信交换器 Dead Letter Exchanges
面试:什么时候死信?
一个消息在满足如下条件下,会进死信交换机,记住这里是交换机而不是队列,一个交换机可以对应很多队列。
(1) 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用。
(2)上面的消息的TTL到了,消息过期了。
(3)队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信交换机上。
Dead Letter Exchange其实就是一种普通的exchange,和创建其他exchange没有两样。只是在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到Dead Letter Exchange中去。

我们现在可以测试一下延迟队列。
(1)创建死信交换器 exchange.ordertimeout (fanout)
(2)创建队列queue.ordertimeout
(3)建立死信交换器 exchange.ordertimeout 与队列queue.ordertimeout 之间的绑定
(4)创建队列queue.ordercreate,Arguments添加
x-message-ttl=10000
x-dead-letter-exchange: exchange.ordertimeout
(5)测试:向queue.ordercreate队列添加消息,等待10秒后消息从queue.ordercreate队列消失,


1.4 代码实现

1.4.1 微信支付-关闭订单

(1)WxPayController新增方法
/*** 关闭微信订单* @param orderId* @return*/@PutMapping("/close/{orderId}")public Result closeOrder(@PathVariable String orderId){Map map = wxPayService.closeOrder( orderId );return new Result( true,StatusCode.OK,"",map );}
(2)ydles_service_pay的WxPayService新增方法定义
/*** 关闭订单* @param orderId* @return*/Map closeOrder(String orderId);
(3)ydles_service_pay的 WxPayServiceImpl实现该方法
@Overridepublic Map closeOrder(String orderId) {Map map=new HashMap( );map.put( "out_trade_no",orderId );try {return wxPay.closeOrder( map );} catch (Exception e) {e.printStackTrace();return null;}}
(4)ydles_service_pay_api的WxPayFeign新增方法
/*** 关闭微信订单* @param orderId* @return*/@PutMapping("/wxpay/close/{orderId}")public Result closeOrder(@PathVariable("orderId") String orderId);
1.4.2 微信支付-查询订单
(1)WxPayController新增方法
/*** 查询微信订单* @param orderId* @return*/@GetMapping("/query/{orderId}")public Result queryOrder(@PathVariable String orderId){Map map = wxPayService.queryOrder( orderId );return new Result( true,StatusCode.OK,"",map );}
(2)WxPayFeign新增方法
/*** 查询微信订单* @param orderId* @return*/@GetMapping("/wxpay/query/{orderId}")public Result queryOrder(@PathVariable("orderId") String orderId);
商品服务回滚库存

(1)dao
//回滚库存@Update("update tb_sku set num=num+#{num},sale_num=sale_num-#{num} where id=#{skuId}")void resumeStockNum(@Param("skuId") String skuId, @Param("num") Integer num);
(2)service
//回滚库存void resumeStockNum(String skuId,Integer num);
(3)serivceImpl
@Overridepublic void resumeStockNum(String skuId, Integer num) {skuMapper.resumeStockNum(skuId,num);}
(4)controller
//回滚库存@PutMapping(value = "/resumeStockNum")public Result resumeStockNum(@RequestParam("skuId") String skuId, @RequestParam("num")Integer num){skuService.resumeStockNum(skuId,num);return new Result(true,StatusCode.OK,"回滚库存成功!");}
(4)skuFeign
@PutMapping(value = "/sku/resumeStockNum")public Result resumeStockNum(@RequestParam("skuId") String skuId, @RequestParam("num")Integer num);
1.4.3 订单关闭逻辑

如果为未支付,查询微信订单
如果确认为未支付,调用关闭本地订单( 修改订单表的订单状态、记录订单日志、恢复商品表库存)和微信订单的逻辑。
如果为已支付进行状态补偿。
(1)ydles_service_order新增依赖
<dependency><groupId>com.ydles</groupId><artifactId>ydles_service_pay_api</artifactId><version>1.0-SNAPSHOT</version></dependency>
(2)ydles_service_order的OrderService新增方法定义
/*** 关闭订单* @param orderId*/void closeOrder(String orderId);
(3)OrderServiceImpl实现该方法
实现逻辑:
1)根据id查询订单信息,判断订单是否存在,订单支付状态是否为未支付
2)基于微信查询订单支付状态
2.1)如果为success,则修改订单状态
2.2)如果为未支付,则修改订单,新增日志,恢复库存,关闭订单
@AutowiredPayFeign payFeign;//关闭订单@Override@Transactionalpublic void closeOrder(String orderId) {System.out.println("关闭订单开启了:"+orderId);Order order = orderMapper.selectByPrimaryKey(orderId);if(order==null){throw new RuntimeException("这笔订单不存在!");}if(!order.getOrderStatus().equals("0")){System.out.println("这笔订单不用关闭");return;}System.out.println("关闭订单逻辑通过校验:"+orderId);//1支付服务 微信查询订单Map<String, String> wxQueryMap = payFeign.queryOrder(orderId).getData();//2.1支付了 order表修改if(wxQueryMap.get("trade_state").equals("SUCCESS")){updatePayStatus(orderId,wxQueryMap.get("transaction_id"));System.out.println("已支付"+orderId);}//2.2未支付 关闭订单微信 内部回滚库存 订单状态关闭if(wxQueryMap.get("trade_state").equals("NOTPAY")){//1关闭订单微信payFeign.closeOrder(orderId);//2订单状态关闭System.out.println("本项目关闭订单了");order.setOrderStatus("9");//订单状态 0下单 1支付 2发货 3收货 4退货 9关闭order.setCloseTime(new Date());orderMapper.updateByPrimaryKeySelective(order);//orderLog表 新增数据OrderLog orderLog = new OrderLog();orderLog.setId(idWorker.nextId()+"");orderLog.setOperater("system");orderLog.setOperateTime(new Date());orderLog.setOrderId(orderId);orderLog.setOrderStatus("9");orderLog.setPayStatus("0");orderLog.setConsignStatus("0");orderLog.setRemarks("超时未支付!");orderLogMapper.insertSelective(orderLog);//3内部回滚库存//查出来这笔订单的所有购物项OrderItem orderItem=new OrderItem();orderItem.setOrderId(orderId);List<OrderItem> orderItemList = orderItemMapper.select(orderItem);for (OrderItem orderItem1 : orderItemList) {skuFeign.resumeStock(orderItem1.getSkuId(),orderItem1.getNum());}}//还有各种支付状态 都需要考虑。REFUND CLOSED USERPAYING}
1.4.4 延迟消息处理

从消息队列queue.ordertimeout 中提取消息
(1)修改OrderServiceImpl的add方法,追加代码,实现mq发送
rabbitTemplate.convertAndSend( "","queue.ordercreate", orderId);
(2)ydles_service_order新建监听类
@Componentpublic class OrderTimeoutListener {@Autowiredprivate OrderService orderService;/*** 更新支付状态* @param orderId*/@RabbitListener(queues = "queue.ordertimeout")public void closeOrder(String orderId){System.out.println("接收到关闭订单消息:"+orderId);try {orderService.closeOrder( orderId );} catch (Exception e) {e.printStackTrace();}}}
测试关键点:
1 10秒后,能不能order服务接受消息
2 微信支付了,本地order没支付 —-》下单,回调接口写错。
3 没支付————-》二维码页面,等10秒。

2. 订单批量发货
角色:店主
2.1 批量发货业务逻辑
2.1.1 需求分析
实现批量发货的业务逻辑

2.1.2 代码实现
(1)OrderController新增方法
[{orderId:8345,shipping_name:顺丰,shipping_code:123,.......},{},{}]
/*** 批量发货* @param orders 订单列表*/@PostMapping("/batchSend")public Result batchSend( @RequestBody List<Order> orders){orderService.batchSend( orders );return new Result( true,StatusCode.OK,"发货成功" );}
(2)OrderService新增方法定义
/*** 批量发货* @param orders*/void batchSend(List<Order> orders);
(3)OrderServiceImpl实现该方法
@Override@Transactionalpublic void batchSend(List<Order> orderList) {//循环1 物流公司和物流单号 不能为空for (Order order : orderList) {if(order.getId()==null){throw new RuntimeException("订单号为空!");}if(order.getShippingName()==null||order.getShippingCode()==null){throw new RuntimeException("物流公司或单号为空!");}}//循环2 查询订单状态 校验for (Order order : orderList) {Order queryOrder = orderMapper.selectByPrimaryKey(order.getId());if(!queryOrder.getOrderStatus().equals("1")||!queryOrder.getConsignStatus().equals("0")){throw new RuntimeException("订单状态不对,不能发货!");}}//循环3 发货for (Order order : orderList) {order.setOrderStatus("2");order.setConsignStatus("1");order.setUpdateTime(new Date());order.setConsignTime(new Date());orderMapper.updateByPrimaryKeySelective(order);//orderLog表 新增数据OrderLog orderLog = new OrderLog();orderLog.setId(idWorker.nextId()+"");orderLog.setOperater("店小二");orderLog.setOperateTime(new Date());orderLog.setOrderId(order.getId());orderLog.setOrderStatus("2");orderLog.setPayStatus("1");orderLog.setConsignStatus("1");orderLog.setRemarks("批量发货");orderLogMapper.insertSelective(orderLog);}}
2.2 对接第三方物流(了解)
当我们在电商平台购买了商品后,一般会非常关心商品的物流轨迹。那这些信息是如何获取的呢?我们需要对接第三方的物流系统。比较常用的有菜鸟物流、快递鸟等。
我们这里推荐使用快递鸟 http://www.kdniao.com
我们可以使用快递鸟提供的以下接口:
(1)预约取件API
预约取件API为用户提供了在线下单,预约快递员上门揽件的功能,为用户解决在线发货需求。
我们可以在实现批量发货功能后调用预约取件API
(2)即时查询API
物流查询API提供实时查询物流轨迹的服务,用户提供运单号和快递公司,即可查询当前时刻的最新物流轨迹。
用户可以在用户中心调用此API完成物流信息的查询,电商平台也可以调用此API完成运单的跟踪。

3. 确认收货与自动收货
3.1 确认收货
3.1.1 需求分析与实现思路
当物流公司将货物送到了用户收货地址之后,需要用户点击确认收货,当用户点击了确认收货之后,会修改订单状态为已完成
3.1.2 代码实现
(1)OrderController新增方法
/*** 确认收货* @param orderId 订单号* @param operator 操作者* @return*/@PutMapping("/take/{orderId}/operator/{operator}")public Result take(@PathVariable String orderId, @PathVariable String operator){orderService.take( orderId,operator );return new Result( true,StatusCode.OK,"" );}
(2)OrderService新增方法定义
/*** 确认收货* @param orderId* @param operator*/void confirmTask(String orderId,String operator);
(3)OrderServiceImpl实现该方法
@Overridepublic void confirmTask(String orderId, String operator) {Order order = orderMapper.selectByPrimaryKey( orderId );if(order==null){throw new RuntimeException( "订单不存在" );}if( !"1".equals( order.getConsignStatus() )){throw new RuntimeException( "订单未发货" );}order.setConsignStatus("2"); //已送达order.setOrderStatus( "3" );//已完成order.setUpdateTime( new Date() );order.setEndTime( new Date() );//交易结束orderMapper.updateByPrimaryKeySelective( order );//记录订单变动日志OrderLog orderLog=new OrderLog();orderLog.setId( idWorker.nextId()+"" );orderLog.setOperateTime(new Date());//当前日期orderLog.setOperater( operator );//系统?管理员?用户?orderLog.setOrderStatus("3");orderLog.setOrderId(order.getId());orderLogMapper.insertSelective(orderLog);}
3.2 自动收货处理
3.2.1 需求分析
如果用户在15天(可以在订单配置表中配置)没有确认收货,系统将自动收货。如何实现?我们这里采用定时任务springTask来实现。

逻辑:每天凌晨2点,查询order,发货15天,自动收货。
每天凌晨1点,删除生产环境,删除log。
技术:
spring—->quartz
spring task 定时任务
xxl-job
3.2.2 Cron表达式
"0 * * * * *"
Cron表达式是一个字符串,字符串分为七个部分,每一个域代表一个含义。
Cron表达式7个域格式为: 秒 分 小时 日 月 星期几 年
Cron表达式6个域格式为: 秒 分 小时 日 月 周
| 序号 | 说明 | 是否必填 | 允许填写的值 | 允许的通配符 |
|---|---|---|---|---|
| 1 | 秒 | 是 | 0-59 | , - * / |
| 2 | 分 | 是 | 0-59 | , - * / |
| 3 | 小时 | 是 | 0-23 | , - * / |
| 4 | 日 | 是 | 1-31 | , - * ? / L W |
| 5 | 月 | 是 | 1-12或JAN-DEC | , - * / |
| 6 | 星期几 | 是 | 1-7或SUN-SAT | , - * ? / L W |
| 7 | 年 | 否 | empty 或1970-2099 | , - * / |
使用说明:
通配符说明:* 表示所有值. 例如:在分的字段上设置 "*",表示每一分钟都会触发。? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为"?" 具体设置为 0 0 0 10 * ?- 表示区间。例如 在小时上设置 "10-12",表示 10,11,12点都会触发。, 表示指定多个值,例如在周字段上设置 "MON,WED,FRI" 表示周一,周三和周五触发/ 用于递增触发。如在秒上面设置"5/15" 表示从5秒开始,每增15秒触发(5,20,35,50)。 在月字段上设置'1/3'所示每月1号开始,每隔三天触发一次。L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于"7"或"SAT"。如果在"L"前加上数字,则表示该数据的最后一个。例如在周字段上设置"6L"这样的格式,则表示“本月最后一个星期五"W 表示离指定日期的最近那个工作日(周一至周五). 例如在日字段上设置"15W",表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 "1W",它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,"W"前只能设置具体的数字,不允许区间"-").# 序号(表示每月的第几个周几),例如在周字段上设置"6#3"表示在每月的第三个周六.注意如果指定"#5",正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了) ;
常用表达式
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时0 0 12 ? * WED 表示每个星期三中午12点"0 0 12 * * ?" 每天中午12点触发"0 15 10 ? * *" 每天上午10:15触发"0 15 10 * * ?" 每天上午10:15触发"0 15 10 * * ? *" 每天上午10:15触发"0 15 10 * * ? 2005" 2005年的每天上午10:15触发"0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发"0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发"0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发"0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发"0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44触发"0 15 10 ? * MON-FRI" 周一至周五的上午10:15触发"0 15 10 15 * ?" 每月15日上午10:15触发"0 15 10 L * ?" 每月最后一日的上午10:15触发"0 15 10 ? * 6L" 每月的最后一个星期五上午10:15触发"0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发"0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发
3.2.3 代码实现

3.2.3.1 发送消息
(1)创建order_tack队列 。
(2)创建工程ydles_task,引入依赖
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.amqp</groupId><artifactId>spring-rabbit</artifactId></dependency></dependencies>
(3)创建配置文件 application.yml
server:port: 9202spring:application:name: taskrabbitmq:host: 192.168.200.128
(4)创建启动类 com.ydles.task
@SpringBootApplication@EnableSchedulingpublic class TaskApplication {public static void main(String[] args) {SpringApplication.run( TaskApplication.class,args );}}
@EnableScheduling 注解用于开启任务调度
(5)创建com.ydles.task包,包下创建类OrderTask
@Componentpublic class OrderTask {@Autowiredprivate RabbitTemplate rabbitTemplate;/*** 订单自动收货*/@Scheduled(cron = "0 0 0 * * ?")public void autoTake(){System.out.println(new Date( ) );rabbitTemplate.convertAndSend( "","order_tack","-" );}}
3.2.3.2 接收消息

(1)ydles_service_order工程,编写消息监听类
@Componentpublic class OrderTackListener {@Autowiredprivate OrderService orderService;@RabbitListener(queues = "order_tack")public void autoTack(String message){System.out.println("收到自动确认收货消息");orderService.autoTack(); //自动确认收货}}
(2)OrderService新增方法定义
/*** 自动确认收货*/void autoTack();
(3)OrderServiceImpl实现此方法
实现思路:
1)从订单配置表中获取订单自动确认期限
2)得到当前日期向前数(订单自动确认期限)天。作为过期时间节点
3)从订单表中获取过期订单(发货时间小于过期时间,且为未确认收货状态)
4)循环批量处理,执行确认收货
@AutowiredOrderConfigMapper orderConfigMapper;@Overridepublic void autoTack() {//1 从配置表中获取15天值OrderConfig orderConfig = orderConfigMapper.selectByPrimaryKey("1");Integer takeTimeout = orderConfig.getTakeTimeout();//15//2 推算拿几号之前发货的订单LocalDate now=LocalDate.now();//当前LocalDate date = now.plusDays(-takeTimeout);System.out.println(date);//3 查询发货超过15天的订单Example example=new Example(Order.class);Example.Criteria criteria = example.createCriteria();criteria.andEqualTo("orderStatus","2");criteria.andLessThan("consignTime", date);List<Order> orderList = orderMapper.selectByExample(example);//4 循环 把这些订单 收货for (Order order : orderList) {take(order.getId(),"system");}}
总结:
1超时未支付订单处理

2订单批量发货(店主)

3确认收货与自动收货
手动:/take/{orderId}/operator/{operator}自动:

第15章-秒杀前端
角色:独立模块 秒杀模块。架构师。
课程内容:
- 秒杀业务分析
1.什么是秒杀2.秒杀实现的流程->架构分析流程->重点3.业务流程
- 秒杀商品压入Redis缓存
秒杀商品存入到Redis来提升访问速度1.秒杀列表数据2.秒杀详情页数据
- Spring定时任务了解-定时将秒杀商品存入到Redis中
定时将秒杀商品存入到Redis缓存
秒杀商品频道页实现-秒杀商品列表页
秒杀商品详情页实现
1 秒杀业务分析
1.1 需求分析
所谓“秒杀”,就是网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。通俗一点讲就是网络商家为促销等目的组织的网上限时抢购活动。由于商品价格低廉,往往一上架就被抢购一空,有时只用一秒钟。
秒杀商品通常有两种限制:库存限制、时间限制。
需求:
(1)秒杀频道首页列出秒杀商品(4)点击立即抢购实现秒杀下单,下单时扣减库存。当库存为0或不在活动期范围内时无法秒杀。(5)秒杀下单成功,直接跳转到支付页面(微信扫码),支付成功,跳转到成功页,填写收货地址、电话、收件人等信息,完成订单。(6)当用户秒杀下单5分钟内未支付,取消预订单,调用微信支付的关闭订单接口,恢复库存。

1.2 表结构说明
秒杀商品信息表
CREATE TABLE `tb_seckill_goods` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`goods_id` bigint(20) DEFAULT NULL COMMENT 'spu ID',`item_id` bigint(20) DEFAULT NULL COMMENT 'sku ID',`title` varchar(100) DEFAULT NULL COMMENT '标题',`small_pic` varchar(150) DEFAULT NULL COMMENT '商品图片',`price` decimal(10,2) DEFAULT NULL COMMENT '原价格',`cost_price` decimal(10,2) DEFAULT NULL COMMENT '秒杀价格',`seller_id` varchar(100) DEFAULT NULL COMMENT '商家ID',`create_time` datetime DEFAULT NULL COMMENT '添加日期',`check_time` datetime DEFAULT NULL COMMENT '审核日期',`status` char(1) DEFAULT NULL COMMENT '审核状态,0未审核,1审核通过,2审核不通过',`start_time` datetime DEFAULT NULL COMMENT '开始时间',`end_time` datetime DEFAULT NULL COMMENT '结束时间',`num` int(11) DEFAULT NULL COMMENT '秒杀商品数',`stock_count` int(11) DEFAULT NULL COMMENT '剩余库存数',`introduction` varchar(2000) DEFAULT NULL COMMENT '描述',PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

秒杀订单表
CREATE TABLE `tb_seckill_order` (`id` bigint(20) NOT NULL COMMENT '主键',`seckill_id` bigint(20) DEFAULT NULL COMMENT '秒杀商品ID',`money` decimal(10,2) DEFAULT NULL COMMENT '支付金额',`user_id` varchar(50) DEFAULT NULL COMMENT '用户',`seller_id` varchar(50) DEFAULT NULL COMMENT '商家',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`pay_time` datetime DEFAULT NULL COMMENT '支付时间',`status` char(1) DEFAULT NULL COMMENT '状态,0未支付,1已支付',`receiver_address` varchar(200) DEFAULT NULL COMMENT '收货人地址',`receiver_mobile` varchar(20) DEFAULT NULL COMMENT '收货人电话',`receiver` varchar(20) DEFAULT NULL COMMENT '收货人',`transaction_id` varchar(30) DEFAULT NULL COMMENT '交易流水',PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;

1.3 秒杀需求分析==拓展内容
秒杀技术实现核心思想是运用缓存减少数据库瞬间的访问压力!读取商品详细信息时运用缓存,当用户点击抢购时减少缓存中的库存数量,当库存数为0时或活动期结束时,同步到数据库。 产生的秒杀预订单也不会立刻写到数据库中,而是先写到缓存,当用户付款成功后再写入数据库。
当然,上面实现的思路只是一种最简单的方式,并未考虑其中一些问题,例如并发状况容易产生的问题。我们看看下面这张思路更严谨的图:

2 秒杀商品存入缓存-重点

秒杀商品由B端存入Mysql,设置定时任务,每隔一段时间就从Mysql中将符合条件的数据从Mysql中查询出来并存入缓存中,redis以Hash类型进行数据存储。
KEY的设计:
seckill_goods_2021122812 mapid SecKillGoods1 Prada包包3 guccy包包seckill_goods_2021122814 mapid SecKillGoods2 lv包包seckill_goods_2021122816 mapseckill_goods_2021122818 mapseckill_goods_2021122820 map
我们这里秒杀商品列表和秒杀商品详情都是从Redis中取出来的,所以我们首先要将符合参与秒杀的商品定时查询出来,并将数据存入到Redis缓存中。
数据存储类型我们可以选择Hash类型。
秒杀分页列表这里可以通过获取redisTemplate.boundHashOps(key).values()获取结果数据。
秒杀商品详情,可以通过redisTemplate.boundHashOps(key).get(key)获取详情。
2.1 秒杀服务搭建
我们将商品数据压入到Reids缓存,可以在秒杀工程的服务工程中完成,可以按照如下步骤实现:
1.查询活动没结束的所有秒杀商品1)状态必须为审核通过 status=12)商品库存个数>03)活动没有结束 endTime>=now()4)在Redis中没有该商品的缓存5)执行查询获取对应的结果集2.将活动没有结束的秒杀商品入库
我们首先搭建一个秒杀服务工程,然后按照上面步骤实现。
搭建ydles-service-seckill,作为秒杀工程的服务提供工程。
1)新建服务ydles_service_seckill
2)添加依赖信息,详情如下:
<dependencies><dependency><groupId>com.ydles</groupId><artifactId>ydles_common_db</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency><groupId>com.ydles</groupId><artifactId>ydles_service_order_api</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>com.ydles</groupId><artifactId>ydles_service_seckill_api</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>com.ydles</groupId><artifactId>ydles_service_goods_api</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>org.springframework.amqp</groupId><artifactId>spring-rabbit</artifactId></dependency><!--oauth依赖--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId></dependency></dependencies>
- ydles_service_seckill_api创建
依赖
<dependencies><dependency><groupId>com.ydles</groupId><artifactId>ydles_common</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies>
添加包 com.ydles.seckill.feign com.ydles.seckill.pojo
pojo中将资料中的两个实体类放入,对应数据库的两张表

- 回到ydles_service_seckill工程。添加启动类 com.ydles.seckill
@SpringBootApplication@EnableEurekaClient@EnableFeignClients@MapperScan(basePackages = {"com.ydles.seckill.dao"})@EnableSchedulingpublic class SeckillApplication {public static void main(String[] args) {SpringApplication.run(SeckillApplication.class,args);}@Beanpublic IdWorker idWorker(){return new IdWorker(1,1);}//设置redistemplate的序列化@Beanpublic RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {// 1.创建 redisTemplate 模版RedisTemplate<Object, Object> template = new RedisTemplate<>();// 2.关联 redisConnectionFactorytemplate.setConnectionFactory(redisConnectionFactory);// 3.创建 序列化类GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class);// 6.序列化类,对象映射设置// 7.设置 value 的转化格式和 key 的转化格式template.setValueSerializer(genericToStringSerializer);template.setKeySerializer(new StringRedisSerializer());template.afterPropertiesSet();return template;}}
- 添加dao层的两个mapper
public interface SeckillGoodsMapper extends Mapper<SeckillGoods> {}
public interface SeckillOrderMapper extends Mapper<SeckillOrder> {}
- 添加application.yml
server:port: 9016spring:jackson:time-zone: GMT+8application:name: seckilldatasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://192.168.200.128:3306/ydles_seckill?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT%2b8username: rootpassword: rootmain:allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册redis:host: 192.168.200.128rabbitmq:host: 192.168.200.128eureka:client:service-url:defaultZone: http://127.0.0.1:6868/eurekainstance:prefer-ip-address: truefeign:hystrix:enabled: trueclient:config:default: #配置全局的feign的调用超时时间 如果 有指定的服务配置 默认的配置不会生效connectTimeout: 60000 # 指定的是 消费者 连接服务提供者的连接超时时间 是否能连接 单位是毫秒readTimeout: 20000 # 指定的是调用服务提供者的 服务 的超时时间() 单位是毫秒#hystrix 配置hystrix:command:default:execution:timeout:#如果enabled设置为false,则请求超时交给ribbon控制enabled: trueisolation:strategy: SEMAPHOREthread:# 熔断器超时时间,默认:1000/毫秒timeoutInMilliseconds: 20000
添加公钥 copy认证服务的公钥
添加Oauth配置类 config包
@Configuration@EnableResourceServer//开启方法上的PreAuthorize注解@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)public class ResourceServerConfig extends ResourceServerConfigurerAdapter {//公钥private static final String PUBLIC_KEY = "public.key";/**** 定义JwtTokenStore* @param jwtAccessTokenConverter* @return*/@Beanpublic TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {return new JwtTokenStore(jwtAccessTokenConverter);}/**** 定义JJwtAccessTokenConverter* @return*/@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter() {JwtAccessTokenConverter converter = new JwtAccessTokenConverter();converter.setVerifierKey(getPubKey());return converter;}/*** 获取非对称加密公钥 Key* @return 公钥 Key*/private String getPubKey() {Resource resource = new ClassPathResource(PUBLIC_KEY);try {InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());BufferedReader br = new BufferedReader(inputStreamReader);return br.lines().collect(Collectors.joining("\n"));} catch (IOException ioe) {return null;}}/**** Http安全配置,对每个到达系统的http请求链接进行校验* @param http* @throws Exception*/@Overridepublic void configure(HttpSecurity http) throws Exception {//所有请求必须认证通过http.authorizeRequests().anyRequest().authenticated(); //其他地址需要认证授权}}
- 更改网关路径过滤类,添加秒杀工程过滤信息

- 更改网关配置文件,添加请求路由转发
#秒杀微服务- id: ydles_seckill_routeuri: lb://seckillpredicates:- Path=/api/seckill/**filters:- StripPrefix=1
2.2 时间操作
2.2.1 秒杀商品时间段分析

根据产品原型图结合秒杀商品表设计可以得知,秒杀商品是存在开始时间与结束时间的,当前秒杀商品是按照秒杀时间段进行显示,如果当前时间在符合条件的时间段范围之内,则用户可以秒杀购买当前时间段之内的秒杀商品。
缓存数据加载思路:定义定时任务,每天凌晨会进行当天所有时间段秒杀商品预加载。并且在B端进行限制,添加秒杀商品的话,只能添加当前日期+1的时间限制,比如说:当前日期为8月5日,则添加秒杀商品时,开始时间必须为6日的某一个时间段,否则不能添加。
2.2.2 秒杀商品时间段计算
将资源/DateUtil.java添加到公共服务中。基于当前工具类可以进行时间段的计算。
在该工具类中,进行时间计算测试:
public static void main(String[] args) {//定义存储结果的集合List<Date> dateList = new ArrayList<>();//获取本日凌晨时间点Date currentData = toDayStartHour(new Date());//循环12次 (因为要获取每隔两个时间为一个时间段的值)for (int i=0;i<12;i++){dateList.add(addDateHour(currentData,i*2));}for (Date date : dateList) {SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");String format = simpleDateFormat.format(date);System.out.println(format);}}
测试结果:

2.2.3 当前业务整体流程分析
1.定时任务:查询所有符合条件的秒杀商品1) 获取时间段集合并循环遍历出每一个时间段2) 获取每一个时间段名称,用于后续redis中key的设置3) 状态必须为审核通过 status=14) 商品库存个数>05) 秒杀商品开始时间>=当前时间段6) 秒杀商品结束<当前时间段+2小时7) 排除之前已经加载到Redis缓存中的商品数据8) 执行查询获取对应的结果集2.将秒杀商品存入缓存
2.3 代码实现
2.3.1 更改启动类,添加开启定时任务注解
@EnableScheduling
2.3.2 时间菜单分析

我们将商品数据从数据库中查询出来,并存入Redis缓存,但页面每次显示的时候,只显示当前正在秒杀以及往后延时2个小时、4个小时、6个小时、8个小时的秒杀商品数据。我们要做的第一个事是计算出秒杀时间菜单,这个菜单是从后台获取的。
这个时间菜单的计算我们来分析下,可以先求出当前时间的凌晨,然后每2个小时后作为下一个抢购的开始时间,这样可以分出12个抢购时间段,如下:
00:00-02:0002:00-04:0004:00-06:0006:00-08:0008:00-10:0010:00-12:0012:00-14:0014:00-16:0016:00-18:0018:00-20:0020:00-22:0022:00-00:00
而现实的菜单只需要计算出当前时间在哪个时间段范围,该时间段范围就属于正在秒杀的时间段,而后面即将开始的秒杀时间段的计算也就出来了,可以在当前时间段基础之上+2小时、+4小时、+6小时、+8小时。
关于时间菜单的运算,在给出的DateUtil包里已经实现,代码如下:
/**** 获取时间菜单* @return*/public static List<Date> getDateMenus(){//定义一个List<Date>集合,存储所有时间段List<Date> dates = getDates(12);//判断当前时间属于哪个时间范围Date now = new Date();for (Date cdate : dates) {//开始时间<=当前时间<开始时间+2小时if(cdate.getTime()<=now.getTime() && now.getTime()<addDateHour(cdate,2).getTime()){now = cdate;break;}}//当前需要显示的时间菜单List<Date> dateMenus = new ArrayList<Date>();for (int i = 0; i <5 ; i++) {dateMenus.add(addDateHour(now,i*2));}return dateMenus;}/**** 指定时间往后N个时间间隔* @param hours* @return*/public static List<Date> getDates(int hours) {List<Date> dates = new ArrayList<Date>();//循环12次Date date = toDayStartHour(new Date()); //凌晨for (int i = 0; i <hours ; i++) {//每次递增2小时,将每次递增的时间存入到List<Date>集合中dates.add(addDateHour(date,i*2));}return dates;}
2.3.2 定义定时任务类
秒杀工程新建task包,并新建任务类SeckillGoodsPushTask
业务逻辑:
/*** 1.查询所有符合条件的秒杀商品* 1) 获取时间段集合并循环遍历出每一个时间段* 2) 获取每一个时间段名称,用于后续redis中key的设置* 3) 状态必须为审核通过 status=1* 4) 商品库存个数>0* 5) 秒杀商品开始时间>=当前时间段* 6) 秒杀商品结束<当前时间段+2小时* 7) 排除之前已经加载到Redis缓存中的商品数据* 8) 执行查询获取对应的结果集* 2.将秒杀商品存入缓存*/
注意:用到很多工具类,了解功能即可
/*** 添加秒杀秒伤定时任务*/@Componentpublic class SeckillGoodsPushTask {@Autowiredprivate SeckillGoodsMapper seckillGoodsMapper;@Autowiredprivate RedisTemplate redisTemplate;//redis key开头public static final String SECKILL_GOODS_KEY = "seckill_goods_";/*** 定时将秒杀商品存入redis* 暂定为30秒一次,正常业务为每天凌晨触发*/@Scheduled(cron = "0/30 * * * * ?")public void loadSecKillGoodsToRedis() {/*** 1.查询所有符合条件的秒杀商品* 1) 获取时间段集合并循环遍历出每一个时间段* 2) 获取每一个时间段名称,用于后续redis中key的设置* 3) 状态必须为审核通过 status=1* 4) 商品库存个数>0* 5) 秒杀商品开始时间>=当前时间段* 6) 秒杀商品结束<当前时间段+2小时* 7) 排除之前已经加载到Redis缓存中的商品数据* 8) 执行查询获取对应的结果集* 2.将秒杀商品存入缓存*///1) 获取时间段集合并循环遍历出每一个时间段List<Date> dateMenus = DateUtil.getDateMenus(); // 5个for (Date dateMenu : dateMenus) {SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 2) 获取每一个时间段名称,用于后续redis中key的设置String redisExtName = DateUtil.date2Str(dateMenu);Example example = new Example(SeckillGoods.class);Example.Criteria criteria = example.createCriteria();// 3) 状态必须为审核通过 status=1criteria.andEqualTo("status", "1");// 4) 商品库存个数>0 gtcriteria.andGreaterThan("stockCount", 0);// 5) 秒杀商品开始时间>=当前时间段 gtecriteria.andGreaterThanOrEqualTo("startTime", simpleDateFormat.format(dateMenu));// 6) 秒杀商品结束<当前时间段+2小时 ltcriteria.andLessThan("endTime", simpleDateFormat.format(DateUtil.addDateHour(dateMenu, 2)));// 7) 排除之前已经加载到Redis缓存中的商品数据Set keys = redisTemplate.boundHashOps(SECKILL_GOODS_KEY + redisExtName).keys();//key field valueif (keys != null && keys.size() > 0) {criteria.andNotIn("id", keys);}// 8) 执行查询获取对应的结果集List<SeckillGoods> seckillGoodsList = seckillGoodsMapper.selectByExample(example);//2.将秒杀商品存入缓存for (SeckillGoods seckillGoods : seckillGoodsList) {redisTemplate.opsForHash().put(SECKILL_GOODS_KEY + redisExtName, seckillGoods.getId(), seckillGoods);}}}}
测试:
1修改一条数据,满足搜索条件

2结果查看redis

3 秒杀商品-首页-了解

秒杀商品首页会显示处于秒杀中以及未开始秒杀的商品。
3.1 秒杀首页实现分析

秒杀首页需要显示不同时间段的秒杀商品信息,然后当用户选择不同的时间段,查询该时间段下的秒杀商品,实现过程分为两大过程:
1) 加载时间菜单2)加载时间菜单下秒杀商品信息
3.1.1 加载时间菜单分析

每2个小时就会切换一次抢购活动,所以商品发布的时候,我们将时间定格在2小时内抢购,每次发布商品的时候,商品抢购开始时间和结束时间是这2小时的边界。
每2小时会有一批商品参与抢购,所以我们可以将24小时切分为12个菜单,每个菜单都是个2小时的时间段,当前选中的时间菜单需要根据当前时间判断,判断当前时间属于哪个秒杀时间段,然后将该时间段作为选中的第1个时间菜单。
3.1.2 加载对应秒杀商品分析

进入首页时,到后台查询时间菜单信息,然后将第1个菜单的时间段作为key,在Redis中查询秒杀商品集合,并显示到页面,页面每次点击切换不同时间段菜单的时候,都将时间段传入到后台,后台根据时间段获取对应的秒杀商品集合。
3.2 秒杀渲染服务 - 渲染秒杀首页
3.2.1 新建秒杀渲染服务
1)创建工程ydles_web_seckill,用于秒杀页面渲染
- 添加依赖
<dependencies><dependency><groupId>com.ydles</groupId><artifactId>ydles_service_seckill_api</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency></dependencies>
- 添加启动类 com.ydles.seckill.web
@SpringBootApplication@EnableDiscoveryClient@EnableFeignClients(basePackages = "com.ydles.seckill.feign")public class WebSecKillApplication {public static void main(String[] args) {SpringApplication.run(WebSecKillApplication.class,args);}@Beanpublic FeignInterceptor feignInterceptor(){return new FeignInterceptor();}/*** 设置 redisTemplate 的序列化设置* @param redisConnectionFactory* @return*/@Beanpublic RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {// 1.创建 redisTemplate 模版RedisTemplate<Object, Object> template = new RedisTemplate<>();// 2.关联 redisConnectionFactorytemplate.setConnectionFactory(redisConnectionFactory);// 3.创建 序列化类GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class);// 6.序列化类,对象映射设置// 7.设置 value 的转化格式和 key 的转化格式template.setValueSerializer(genericToStringSerializer);template.setKeySerializer(new StringRedisSerializer());template.afterPropertiesSet();return template;}}
- 添加application.yml
server:port: 9104eureka:client:service-url:defaultZone: http://127.0.0.1:6868/eurekainstance:prefer-ip-address: truefeign:hystrix:enabled: truespring:jackson:time-zone: GMT+8thymeleaf:cache: falseapplication:name: seckill-webmain:allow-bean-definition-overriding: trueredis:host: 192.168.200.128#hystrix 配置hystrix:command:default:execution:timeout:#如果enabled设置为false,则请求超时交给ribbon控制enabled: trueisolation:strategy: SEMAPHOREthread:timeoutInMilliseconds: 60000#请求处理的超时时间ribbon:ReadTimeout: 4000#请求连接的超时时间ConnectTimeout: 3000
- 添加静态化资源

6)对接网关
#秒杀渲染微服务- id: ydles_seckill_web_routeuri: lb://seckill-webpredicates:- Path=/api/wseckillgoods/**filters:- StripPrefix=1
3.3 时间菜单实现
时间菜单显示,先运算出每2小时一个抢购,就需要实现12个菜单,可以先计算出每个时间的临界值,然后根据当前时间判断需要显示12个时间段菜单中的哪个菜单,再在该时间菜单的基础之上往后挪4个菜单,一直显示5个时间菜单。
3.3.1 时间菜单获取
ydles_web_seckill com.ydles.seckill.web.controller 新增控制类SecKillGoodsController
@Controller@RequestMapping("/wseckillgoods")public class SecKillGoodsController {@Autowiredprivate SecKillGoodsFeign secKillGoodsFeign;//跳转秒杀首页@RequestMapping("/toIndex")public String toIndex(){return "seckill-index";}//获取秒杀时间段集合信息@RequestMapping("/timeMenus")@ResponseBodypublic List<String> dateMenus(){//获取当前时间段相关的信息集合List<Date> dateMenus = DateUtil.getDateMenus(); //5个//返回值List<String> result = new ArrayList<>();SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//格式化时间段for (Date dateMenu : dateMenus) {String format = simpleDateFormat.format(dateMenu);result.add(format);}return result;}}
3.3.2 页面加载时间菜单
修改seckill-index.html

333var app = new Vue({el: '#app',data() {return {goodslist: [],dateMenus:[]}},methods:{loadMenus:function () {axios.get("/api/wseckillgoods/timeMenus").then(function (response) {app.dateMenus=response.data;//查询当前时间段对应的秒杀商品})}},created:function () {this.loadMenus();}})
1查看以下代码
114 - 123<div class="item-time active"v-for="(item,index) in dateMenus"><div class="time-clock">{{item}}</div><div class="time-state-on"><span class="on-text" v-if="index==0">快抢中</span><span class="on-over" v-if="index==0">距离结束:01:02:03</span><span class="on-text" v-if="index>0">即将开始</span><span class="on-over" v-if="index>0">距离开始:03:02:01</span></div></div>
2通过网关访问,需要将 moment.min.js 放入网关的静态资源中

3启动web-seckill 重启,访问 http://localhost:8001/api/wseckillgoods/toIndex
效果如下:

3.3.3 时间格式化
上面菜单循环输出后,会出现如上图效果,时间格式全部不对,我们需要引入一个moment.min.js来格式化时间。
1)引入moment.min.js
2)添加过滤器
356//过滤器Vue.filter("dateFilter", function(date, formatPattern){return moment(date).format(formatPattern || "YYYY-MM-DD HH:mm:ss");});
- 取值格式化
94<div class="time-clock">{{item | dateFilter('HH:mm')}}</div>
重启webseckill,重新访问:http://localhost:8001/api/wseckillgoods/toIndex 。时间菜单效果如下

3.3.4 选中实现 -了解
3.3.4.1 思路分析

根据原型图,是让当前第一个时间菜单为选中状态,并且加载第一个菜单对应的数据。
我们可以先定义一个ctime=0,用来记录当前选中的菜单下标,因为默认第一个选中,第一个下标为0,所以初始值为0,每次点击对应菜单的时候,将被点击的菜单的下标值赋值给ctime,然后在每个菜单上判断,下标=ctime则让该菜单选中。
3.3.4.2 代码实现
1)定义ctime=0
var app = new Vue({el: '#app',data() {return {goodslist: [],dateMenus:[],ctime:0 //当前时间菜单选中的下标}}})
2)页面样式控制:
114-123<div class="item-time " v-for="(item,index) in dateMenus" :class="['item-time',index==ctime?'active':'']" @click="ctime=index;"><div class="time-clock">{{item | dateFilter('HH:mm')}}</div><div class="time-state-on"><span class="on-text" v-if="index==0">快抢中</span><span class="on-over" v-if="index==0">距离结束:01:02:34</span><span class="on-text" v-if="index>0">即将开始</span><span class="on-over" v-if="index>0">距离开始:01:02:34</span></div></div>
重启webseckill,重新访问:http://localhost:8001/api/wseckillgoods/toIndex 。时间菜单效果如下

3.3.5 倒计时实现
3.3.5.1 倒计时实现
3.3.5.1.1 基础数据显示
定义一个集合,用于存放五个时间段的倒计时时间差,集合中每一个角标都对应一个倒计时时间差,比如:集合角标为0,对应第一个倒计时时间差。集合角标为1,对应第二个倒计时时间差,依次类推。

alltimes:[5555555,66666666,77777777,888888888,9999999999]
2从该集合中获取内容,并更新倒计时时间

118 <span class="on-over" v-if="index==0">距离结束:{{alltimes[index]}}</span>122 <span class="on-over" v-if="index>0">距离开始:{{alltimes[index]}}</span>
重启webseckill,重新访问:http://localhost:8001/api/wseckillgoods/toIndex 。时间菜单效果如下

3.3.5.1.2 每个时间差倒计时实现
周期执行函数用法如下:
window.setInterval(function(){//要做的事},1000);
结束执行周期函数用法如下:
window.clearInterval(timers);
具体代码如下:

354//时间差递减let timers = window.setInterval(function () {for(var i=0;i<app.alltimes.length;i++){//时间递减app.$set(app.alltimes,i,app.alltimes[i]-1000);if(app.alltimes[i]<=0){//停止倒计时window.clearInterval(timers);//当任意一个时间<=0的时候,需要重新刷新菜单,并刷新对应的数据app.loadMenus();}}},1000);
重启webseckill,重新访问:http://localhost:8001/api/wseckillgoods/toIndex 。时间菜单效果如下可以发现每一个时间段的时间都在每秒递减。

3.3.5.1.3 倒计时时间格式化
将此工具引入页面js方法中,用于时间计算。注意和loadMenus方法平行
369-383//将毫秒转换成时分秒timedown:function(num) {var oneSecond = 1000;var oneMinute=oneSecond*60;var oneHour=oneMinute*60//小时var hours =Math.floor(num/oneHour);//分钟var minutes=Math.floor((num%oneHour)/oneMinute);//秒var seconds=Math.floor((num%oneMinute)/oneSecond);//拼接时间格式var str = hours+':'+minutes+':'+seconds;return str;}
修改时间差显示设置

117-123<div class="time-state-on"><span class="on-text" v-if="index==0">快抢中</span><span class="on-over" v-if="index==0">距离结束:{{timedown(alltimes[index])}}</span><span class="on-text" v-if="index>0">即将开始</span><span class="on-over" v-if="index>0">距离开始:{{timedown(alltimes[index])}}</span></div>
重新访问进行测试。效果如下:

3.3.5.1.4 正确倒计时时间显示
现在页面中,对于倒计时时间集合内的数据,暂时写的为假数据,现在需要让集合内容的数据是经过计算得出的。第一个是距离结束时间倒计时,后面的4个都是距离开始倒计时,每个倒计时其实就是2个时差,计算方式如下:
第1个时差:第2个抢购开始时间-当前时间,距离结束时间第2个时差:第2个抢购开始时间-当前时间,距离开始时间第3个时差:第3个抢购开始时间-当前时间,距离开始时间第4个时差:第4个抢购开始时间-当前时间,距离开始时间第5个时差:第5个抢购开始时间-当前时间,距离开始时间

loadMenus:function () {axios.get("/wseckill/timeMenus").then(function (response) {app.dateMenus=response.data;//查询当前时间段对应的秒杀商品353-362 //循环所有时间菜单for(var i=0;i<app.dateMenus.length;i++){//运算每个时间菜单倒计时时间差if (i==0){//距离结束时间var x =i+1;app.$set(app.alltimes,i,new Date(app.dateMenus[x]).getTime()-new Date().getTime());} else{//距离开始时间app.$set(app.alltimes,i,new Date(app.dateMenus[i]).getTime()-new Date().getTime());}}//时间差递减let timers = window.setInterval(function () {for(var i=0;i<app.alltimes.length;i++){//时间递减app.$set(app.alltimes,i,app.alltimes[i]-1000);}},1000);})}
清空alltimes数据
alltimes:[]
重启webseckill,重新访问:http://localhost:8001/api/wseckillgoods/toIndex 。

3.4 加载秒杀商品实现

当前已经完成了秒杀时间段菜单的显示,那么当用户在切换不同的时间段的时候,需要按照用户所选择的时间去显示相对应时间段下的秒杀商品。
3.4.1 秒杀服务-查询秒杀商品列表

3.4.1.1 秒杀服务- service
public interface SecKillGoodsService {//查询秒杀商品列表List<SeckillGoods> list(String time);}
实现类
@Servicepublic class SecKillGoodsServiceImpl implements SecKillGoodsService {@Autowiredprivate RedisTemplate redisTemplate;private static final String SECKILL_KEY = "seckill_goods_";/*** 查询秒杀商品列表* @param time* @return*/@Overridepublic List<SeckillGoods> list(String time) {return redisTemplate.boundHashOps(SECKILL_KEY+time).values();}}
3.4.1.2 秒杀服务-controller
@RestController@RequestMapping("/seckillgoods")public class SecKillController {@Autowiredprivate SecKillGoodsService secKillGoodsService;/*** 查询秒杀商品列表* @param time* @return*/@RequestMapping("/list")public Result<List<SeckillGoods>> list(@RequestParam("time") String time){List<SeckillGoods> seckillGoodsList = secKillGoodsService.list(time);return new Result<List<SeckillGoods>>(true, StatusCode.OK,"查询秒杀商品成功",seckillGoodsList);}}
3.4.1.3 杀服务Api- feign接口定义
@FeignClient(name="seckill")public interface SecKillFeign {/*** 查询秒杀商品列表* @param time* @return*/@RequestMapping("/seckillgoods/list")public Result<List<SeckillGoods>> list(@RequestParam("time") String time);}
3.4.1.4 查询秒杀商品放行-想想为什么
更改秒杀微服务的ResourceServerConfig类,对查询方法放行
@Overridepublic void configure(HttpSecurity http) throws Exception {//所有请求必须认证通过http.authorizeRequests()//下边的路径放行.antMatchers("/seckillgoods/list/**"). //配置地址放行permitAll().anyRequest().authenticated(); //其他地址需要认证授权}
3.4.2 秒杀渲染服务-查询秒杀商品列表

oauth2 :1 导包 2公钥 3配置类
调用feign:1导包 api 2EnableFeignClients 3feign拦截器
3.4.2.1 更新ydles_web_seckill的启动类
添加feign接口扫描
@EnableFeignClients(basePackages = "com.ydles.seckill.feign")
3.4.2.2 更新ydles_web_seckill的SecKillGoodsController
注入secKillFeign,并添加获取秒杀商品列表方法实现
/*** 获取秒杀商品列表* 默认当前时间*/@RequestMapping("/list")@ResponseBodypublic Result<List<SeckillGoods>> list(String time){String timeStr = DateUtil.formatStr(time);System.out.println(timeStr);return secKillGoodsFeign.list(timeStr);}
3.4.2.3 更新secKill-index.html。添加按照时间查询方法

395-402//按照时间查询秒杀商品列表searchList:function (time) {axios.get('/api/wseckillgoods/list?time='+time).then(function (response) {if (response.data.flag){app.goodslist = response.data.data;}})}
3.4.2.4 更新secKill-index.html。 加载页面时,默认当前时间查询

352-353//查询当前时间段对应的秒杀商品app.searchList(app.dateMenus[0]);
3.4.2.5 更新secKill-index.html。切换时间菜单,查询秒杀商品

114-117<div class="item-time "v-for="(item,index) in dateMenus":class="['item-time',index==ctime?'active':'']"@click="ctime=index;searchList(item)">
重启秒杀和秒杀页面服务, http://localhost:8001/api/wseckillgoods/toIndex

3.5 抢购按钮
因为当前业务设定为用户秒杀商品为sku,所以当用户点击立即抢购按钮的时候,则直接进行下单操作。
3.5.1 js定义
在秒杀首页添加下单方法
407-416//秒杀下单add:function(id){app.msg ='正在下单';axios.get("/api/wseckillorder/add?time="+moment(app.dateMenus[0]).format("YYYYMMDDHH")+"&id="+id).then(function (response) {if (response.data.flag){app.msg='抢单成功,即将进入支付!';}else{app.msg='抢单失败';}})}
3.5.2 调用下单方法
修改抢购按钮,添加事件
156<a class='sui-btn btn-block btn-buy' href='javascript:void(0)' @click="add(item.id)">立即抢购</a>
3.5.3 秒杀web页面 新增Controller
SecKillOrderController
@RestController@RequestMapping("/wseckillorder")public class SecKillOrderController {@RequestMapping("/add")public Result add(@RequestParam("time") String time, @RequestParam("id")Long id){System.out.println("进入秒杀订单逻辑了!");return null;}}
网关配置:
#秒杀渲染微服务- id: ydles_seckill_web_routeuri: lb://seckill-webpredicates:- Path=/api/wseckillgoods/**,/api/wseckillorder/**filters:- StripPrefix=1
测试:重启秒杀页面,网关服务, 访问http://localhost:8001/api/wseckillgoods/toIndex ,点击立即抢购

后台打印:

总结:
1秒杀业务
三高:
控制:时间段 数量
单独:数据库 微服务 页面
2秒杀商品 存入缓存

3首页

第16章-秒杀后端
角色:架构师
测试:功能测试 压力测试 1万/s 20万/s
1实现秒杀异步下单,掌握如何保证生产者&消费者消息不丢失
2实现防止恶意刷单
3实现防止相同商品重复秒杀
4实现秒杀下单接口隐藏
5实现下单接口限流
1 秒杀异步下单-重点-难点
用户在下单的时候,需要基于JWT令牌信息进行登陆人信息认证,确定当前订单是属于谁的。
为什么要异步下单:针对秒杀的特殊业务场景,仅仅依靠对象缓存或者页面静态化等技术去解决服务端压力还是远远不够。对于数据库压力还是很大,所以需要异步下单,异步是最好的解决办法,但会带来一些额外的程序上的复杂性。
流程:异步 service_seckill接收下单消息—-》MQ ———》service_consume 完成剩余操作

1.1 秒杀服务-下单实现

1)将tokenDecode工具类从order工程放入秒杀服务并声明Bean, ydles_service_seckill服务

启动类添加
@Beanpublic TokenDecode tokenDecode(){return new TokenDecode();}
2)新建下单controller并声明方法
ydles_service_seckill服务
@RestController@CrossOrigin@RequestMapping("/seckillorder")public class SecKillOrderController {@Autowiredprivate TokenDecode tokenDecode;@Autowiredprivate SecKillOrderService secKillOrderService;/*** 秒杀下单* @param time 当前时间段* @param id 秒杀商品id* @return*/@RequestMapping("/add")public Result add(@RequestParam("time") String time, @RequestParam("id") Long id){//获取当前登陆人String username = tokenDecode.getUserInfo().get("username");boolean result = secKillOrderService.add(id,time,username);if (result){return new Result(true, StatusCode.OK,"下单成功");}else{return new Result(false,StatusCode.ERROR,"下单失败");}}}
3) 新建service接口
ydles_service_seckill服务
public interface SecKillOrderService {/*** 秒杀下单* @param id 商品id* @param time 时间段* @param username 登陆人姓名* @return*/boolean add(Long id, String time, String username);}

4)更改预加载秒杀商品
当预加载秒杀商品的时候,提前加载每一个商品的库存信息,后续减库存操作也会先预扣减缓存中的库存再异步扣减mysql数据。
预扣减库存会基于redis原子性操作实现

//秒杀商品库存头public static final String SECKILL_GOODS_STOCK_COUNT_KEY="seckill_goods_stock_count_";//加载秒杀商品的库存redisTemplate.opsForValue().set(SECKILL_GOODS_STOCK_COUNT_KEY + seckillGoods.getId(), seckillGoods.getStockCount());
5)秒杀下单业务层实现
业务逻辑:
获取秒杀商品数据与库存量数据,如果没有库存则抛出异常
预扣减库存,如果扣完库存量<0,删除商品数据与库存数据
如果库存量>=0,创建秒杀订单,并存入redis
基于mq异步方式完成与mysql数据同步(最终一致性)
注意:库存数据从redis中取出,转换成String
package com.ydles.seckill.service.impl;import com.ydles.seckill.pojo.SeckillGoods;import com.ydles.seckill.pojo.SeckillOrder;import com.ydles.seckill.service.SecKillOrderService;import com.ydles.util.IdWorker;import org.apache.commons.lang.StringUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Service;import java.util.Date;/*** @Created by IT李老师* 公主号 “IT李哥交朋友”* 个人微 itlils*/@Servicepublic class SecKillOrderServiceImpl implements SecKillOrderService {@AutowiredRedisTemplate redisTemplate;@AutowiredIdWorker idWorker;//redis 秒杀商品key开头public static final String SECKILL_GOODS_KEY = "seckill_goods_";//秒杀商品库存key头public static final String SECKILL_GOODS_STOCK_COUNT_KEY = "seckill_goods_stock_count_";//秒杀下单@Overridepublic boolean add(Long id, String time, String username) {//1redis获取商品的数据以及库存量,如果没有,抛出异常SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps(SECKILL_GOODS_KEY + time).get(id);if (seckillGoods == null) {return false;}//redisTemplate用的是string序列化String redisStock = (String) redisTemplate.boundValueOps(SECKILL_GOODS_STOCK_COUNT_KEY + id).get();if (StringUtils.isEmpty(redisStock)) {return false;}int stock = Integer.parseInt(redisStock);if (stock <= 0) {return false;}//2预扣减库存,如果扣成0,删除商品信息和库存信息//Integer integer = (Integer) redisTemplate.opsForValue().get(SECKILL_GOODS_STOCK_COUNT_KEY + id);//100//integer=integer-1;//99//redisTemplate.boundValueOps(SECKILL_GOODS_STOCK_COUNT_KEY + id).set(integer);//99Long decrement = redisTemplate.opsForValue().decrement(SECKILL_GOODS_STOCK_COUNT_KEY + id);if(decrement<=0){//如果扣成0,删除商品信息redisTemplate.boundHashOps(SECKILL_GOODS_KEY + time).delete(id);//库存信息redisTemplate.delete(SECKILL_GOODS_STOCK_COUNT_KEY + id);}//3生成秒杀订单SeckillOrder seckillOrder=new SeckillOrder();seckillOrder.setId(idWorker.nextId());seckillOrder.setSeckillId(id);seckillOrder.setMoney(seckillGoods.getCostPrice());seckillOrder.setUserId(username);seckillOrder.setSeckillId(Long.parseLong(seckillGoods.getSellerId()));seckillOrder.setCreateTime(new Date());seckillOrder.setStatus("0");//4订单数据往mq发return false;}}
1.2 生产者保证消息不丢失—面试常问问题-重点

1rabbitMQ 工作流程。
生产者:tcp —->channel—> exchang
消费者:tcp ———>channal———>queue 一个消费者监听一个 队列
绑定关系:交换机类型——绑定到队列——routingkey
2持久化
按照现有rabbitMQ的相关知识,生产者会发送消息到达消息服务器。但是在实际生产环境下,消息生产者发送的消息很有可能当到达了消息服务器之后,由于消息服务器的问题导致消息丢失,如宕机。因为消息服务器默认会将消息存储在内存中。一旦消息服务器宕机,则消息会产生丢失。因此要保证生产者的消息不丢失,要开始持久化策略。
rabbitMQ持久化:1. 交换机持久化2. 队列持久化3. 消息持久化
3rabbitmq数据保护机制
但是如果仅仅只是开启这两部分的持久化,也很有可能造成消息丢失。因为消息服务器很有可能在持久化的过程中出现宕机。因此需要通过数据保护机制来保证消息一定会成功进行持久化,否则将一直进行消息发送。
RabbitMQ数据保护机制1 事务机制事务机制采用类数据库的事务机制进行数据保护,当消息到达消息服务器,首先会开启一个事务,接着进行数据磁盘持久化,只有持久化成功才会进行事务提交,向消息生产者返回成功通知,消息生产者一旦接收成功通知则不会再发送此条消息。当出现异常,则返回失败通知.消息生产者一旦接收失败通知,则继续发送该条消息。事务机制虽然能够保证数据安全,但是此机制采用的是同步机制,会产生系统间消息阻塞,影响整个系统的消息吞吐量。从而导致整个系统的性能下降,因此不建议使用。2 confirm机制confirm模式需要基于channel进行设置, 一旦某条消息被投递到队列之后,消息队列就会发送一个确认信息给生产者,如果队列与消息是可持久化的, 那么确认消息会等到消息成功写入到磁盘之后发出.confirm的性能高,主要得益于它是异步的.生产者在将第一条消息发出之后等待确认消息的同时也可以继续发送后续的消息.当确认消息到达之后,就可以通过回调方法处理这条确认消息. 如果MQ服务宕机了,则会返回nack消息. 生产者同样在回调方法中进行后续处理。

拓展资料: https://www.cnblogs.com/vipstone/p/9350075.html
4即使我们设计得这么完美,也不能保证数据100%全部发送、接受。
99.9999999% 机器运行成功率。 aws—->paypal youtube
1.2.1 开启confirm机制
ydles_service_seckill服务
1)更改秒杀服务配置文件
rabbitmq:host: 192.168.200.128publisher-confirms: true #开启confirm机制
2)开启队列持久化 rabbit配置文件
rabbitMq使用:1导包 2配置文件 3配置类
@Configurationpublic class RabbitMQConfig {//秒杀商品订单消息public static final String SECKILL_ORDER_QUEUE="seckill_order";//声明队列,并开启持久化@Beanpublic Queue queue(){/*** 第一个参数:队列名称* 第二个参数:是否开启队列持久化*/return new Queue(SECKILL_ORDER_QUEUE,true);}}
3)消息持久化源码查看





的确,在消息convert转换的时候,设置属性是持久化的。
4)增强rabbitTemplate config包下新建。重要:此类相当于我们手动完成confirm逻辑!
package com.ydles.seckill.config;import com.alibaba.fastjson.JSON;import org.springframework.amqp.rabbit.connection.CorrelationData;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.Component;import java.util.HashMap;import java.util.Map;import java.util.UUID;/*** @Created by IT李老师* 公主号 “IT李哥交朋友”* 个人微 itlils* 消息待发器*/@Componentpublic class ConfirmMessageSender implements RabbitTemplate.ConfirmCallback{@AutowiredRabbitTemplate rabbitTemplate;@AutowiredRedisTemplate redisTemplate;//redis 放的时候 开头的keypublic static final String MESSAGE_CONFIRM_KEY="message_confirm_";//规定 有个构造器public ConfirmMessageSender(RabbitTemplate rabbitTemplate){//本类和容器中的rabbitTemplate 建立联系this.rabbitTemplate=rabbitTemplate;//使用rabbitTemplate发的消息,必须有回调,回调给本类rabbitTemplate.setConfirmCallback(this);}//回调方法 exchange异步告知这条消息发送成功@Overridepublic void confirm(CorrelationData correlationData, boolean ack, String cause) {if(ack){//发送成功 删除掉存储空间的数据redisTemplate.delete(correlationData.getId());redisTemplate.delete(MESSAGE_CONFIRM_KEY+correlationData.getId());}else {//发送失败 重发Map<String, String> map = redisTemplate.boundHashOps(MESSAGE_CONFIRM_KEY + correlationData.getId()).entries();String exchange = map.get("exchange");String routingKey = map.get("routingKey");String message = map.get("message");//重发rabbitTemplate.convertAndSend(exchange,routingKey,message);//预警 调用第三方接口:发短信,发邮件给到运维技术组长。log.warn}}//自定义发送的方法public void send(String exchange,String routingKey,String message){//设置消息的唯一idCorrelationData correlationData=new CorrelationData(UUID.randomUUID().toString());//运维快速的看哪个msg有问题了redisTemplate.boundValueOps(correlationData.getId()).set(message);//保存发送的信息到 redisMap<String, String> map=new HashMap<>();map.put("exchange",exchange);map.put("routingKey",routingKey);map.put("message",message);redisTemplate.boundHashOps(MESSAGE_CONFIRM_KEY+correlationData.getId()).putAll(map);//真正发送rabbitTemplate.convertAndSend(exchange,routingKey,message,correlationData);}}
1.2.2发送消息
更改下单业务层实现
ydles_service_seckill服务SecKillOrderServiceImpl类
@Autowiredprivate ConfirmMessageSender confirmMessageSender;
//发送消息confirmMessageSender.sendMessage("", RabbitMQConfig.SECKILL_ORDER_KEY, JSON.toJSONString(seckillOrder));
测试:三个关键位置打断点:
1add方法

2confirmSender 的send

3confirmSender 回调方法

4基于网关访问 修改网关路由
#秒杀微服务- id: ydles_seckill_routeuri: lb://seckillpredicates:- Path=/api/seckill/**,/api/seckillorder/**filters:- StripPrefix=1
5重启网关、秒杀服务。
登陆: http://localhost:8001/api/oauth/toLogin
根据时间 模拟秒杀: http://localhost:8001/api/seckillorder/add?time=2022-01-02 18:00:00&id=1
1.3 秒杀下单服务-更新库存库
1.3.1 异步下单服务ydles_service_consume
1)添加依赖
<dependencies><dependency><groupId>com.ydles</groupId><artifactId>ydles_common_db</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency><groupId>com.ydles</groupId><artifactId>ydles_service_order_api</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>com.ydles</groupId><artifactId>ydles_service_seckill_api</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>com.ydles</groupId><artifactId>ydles_service_goods_api</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>org.springframework.amqp</groupId><artifactId>spring-rabbit</artifactId></dependency></dependencies>
2)新建application.yml
server:port: 9022spring:jackson:time-zone: GMT+8application:name: sec-consumedatasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://192.168.200.128:3306/ydles_seckill?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT%2b8username: rootpassword: rootmain:allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册redis:host: 192.168.200.128rabbitmq:host: 192.168.200.128eureka:client:service-url:defaultZone: http://127.0.0.1:6868/eurekainstance:prefer-ip-address: truefeign:hystrix:enabled: trueclient:config:default: #配置全局的feign的调用超时时间 如果 有指定的服务配置 默认的配置不会生效connectTimeout: 60000 # 指定的是 消费者 连接服务提供者的连接超时时间 是否能连接 单位是毫秒readTimeout: 20000 # 指定的是调用服务提供者的 服务 的超时时间() 单位是毫秒#hystrix 配置hystrix:command:default:execution:timeout:#如果enabled设置为false,则请求超时交给ribbon控制enabled: trueisolation:strategy: SEMAPHOREthread:# 熔断器超时时间,默认:1000/毫秒timeoutInMilliseconds: 20000
3)新建启动类 com.ydles.consume
@SpringBootApplication@EnableDiscoveryClient@MapperScan(basePackages = {"com.ydles.consume.dao"})public class OrderConsumerApplication {public static void main(String[] args) {SpringApplication.run(OrderConsumerApplication.class,args);}}
1.3.2 消费者手动ACK下单实现—面试重点-认真听

按照现有RabbitMQ知识,可以得知当消息消费者成功接收到消息后,会进行消费并自动通知消息服务器将该条消息删除。此种方式的实现使用的是消费者自动应答机制。但是此种方式非常的不安全。在生产环境下,当消息消费者接收到消息,很有可能在处理消息的过程中出现意外情况从而导致消息丢失,因为如果使用自动应答机制是非常不安全。我们需要确保消费者当把消息成功处理完成之后,消息服务器才会将该条消息删除。此时要实现这种效果的话,就需要将**自动应答转换为手动应答**,只有在消息消费者将消息处理完,才会通知消息服务器将该条消息删除。
1)更改配置文件
rabbitmq:host: 192.168.200.128listener:simple:acknowledge-mode: manual #手动
mq配置类
@Configurationpublic class RabbitMQConfig {//秒杀商品订单消息public static final String SECKILL_ORDER_QUEUE="seckill_order";//声明队列,并开启持久化@Beanpublic Queue queue(){/*** 第一个参数:队列名称* 第二个参数:是否开启队列持久化*/return new Queue(SECKILL_ORDER_QUEUE,true);}}
2)定义监听类 回顾rabbitMQ与springboot整合时,形参Channel channel, Message message意义。
com.ydles.consumer.listener
package com.ydles.consume.listener;import com.alibaba.fastjson.JSON;import com.rabbitmq.client.Channel;import com.ydles.consume.config.RabbitMQConfig;import com.ydles.consume.service.SeckillOrderService;import com.ydles.seckill.pojo.SeckillOrder;import org.springframework.amqp.core.Message;import org.springframework.amqp.rabbit.annotation.RabbitListener;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.io.IOException;/*** @Created by IT李老师* 公主号 “IT李哥交朋友”* 个人微 itlils*/@Componentpublic class SecKillOrderListener {@AutowiredSeckillOrderService seckillOrderService;@RabbitListener(queues = RabbitMQConfig.SECKILL_ORDER_QUEUE)public void receiveMsg(Message message, Channel channel){String msgStr = new String(message.getBody());System.out.println("接受到了秒杀订单"+msgStr);//1监听 orderSeckillOrder seckillOrder = JSON.parseObject(message.getBody(), SeckillOrder.class);//2先做业务逻辑int result = seckillOrderService.createOrder(seckillOrder);if(result>0){//2.1没问题 告诉mq收到消息了,可删try {channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);} catch (IOException e) {e.printStackTrace();//log.error}}else {//2.2有问题 告诉mq没收到消息,重回队列try {channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);} catch (IOException e) {e.printStackTrace();//log.error}}}}
3)定义业务层接口与实现类

public interface SecKillOrderService {int createOrder(SeckillOrder seckillOrder);}
@Servicepublic class SecKillOrderServiceImpl implements SecKillOrderService {@Autowiredprivate SeckillGoodsMapper seckillGoodsMapper;@Autowiredprivate SeckillOrderMapper seckillOrderMapper;/*** 同步数据库,更改库存,添加订单* @param seckillOrder* @return*/@Override@Transactionalpublic int createOrder(SeckillOrder seckillOrder) {//更改库存int result = seckillGoodsMapper.updateStockCountById(seckillOrder.getId());if (result<=0){return result;}//添加订单result = seckillOrderMapper.insertSelective(seckillOrder);if (result<=0){return result;}return result;}}
4)dao层两个mapper从秒杀服务copy
5)启动类扫包
@MapperScan(basePackages = {"com.ydles.consumer.dao"})
6)完善 SeckillGoodsMapper
public interface SeckillGoodsMapper extends Mapper<SeckillGoods> {@Update("update tb_seckill_goods set stock_count=stock_count-1 where id=#{id} and stock_count>=1")int updateStockCount(@Param("id") Long id);}
数据库字段unsigned介绍unsigned-----无符号,修饰int 、charALTER TABLE tb_seckill_goods MODIFY COLUMN stock_count int(11) UNSIGNED DEFAULT NULL COMMENT '剩余库存数';
1.5 流量削峰
在秒杀这种高并发的场景下,每秒都有可能产生几万甚至十几万条消息,如果没有对消息处理量进行任何限制的话,很有可能因为过多的消息堆积从而导致消费者宕机的情况。因此官网建议对每一个消息消费者都设置处理消息总数(**消息抓取总数**)。

消息抓取总数的值,设置过大或者过小都不好,过小的话,会导致整个系统消息吞吐能力下降,造成性能浪费。过大的话,则很有可能导致消息过多,导致整个系统OOM(out of memory)内存溢出。因此官网建议每一个消费者将该值设置在**100-300**之间。
1)更新消费者。
ydles_service_consume服务SecKillOrderListener监听器
/*** 预抓取总数*/try {channel.basicQos(300);} catch (IOException e) {e.printStackTrace();}
1.6 秒杀渲染服务-下单实现

1)ydles_service_seckill_api服务定义feign接口
@FeignClient(name="seckill")public interface SecKillOrderFeign {/*** 秒杀下单* @param time 当前时间段* @param id 秒杀商品id* @return*/@RequestMapping("/seckillorder/add")public Result add(@RequestParam("time") String time, @RequestParam("id") Long id);}
2)ydles_web_seckill服务定义controller
@RestController@RequestMapping("/wseckillorder")public class SecKillOrderController {@Autowiredprivate SecKillOrderFeign secKillOrderFeign;@RequestMapping("/add")public Result add(@RequestParam("time") String time, @RequestParam("id")Long id){// System.out.println("进入秒杀订单逻辑了!");Result result = secKillOrderFeign.add(time, id);return result;}}
测试: 重启相关服务,登陆,秒杀 http://localhost:8001/api/wseckillgoods/toIndex
查看秒杀商品表和秒杀订单表数据变化。redis变化。


1.7秒杀商品真实数量获取
需求:

1从秒杀任务类中拿出 redis秒杀商品库存头
//秒杀商品库存头public static final String SECKILL_GOODS_STOCK_COUNT_KEY="seckill_goods_stock_count_";
2修改获取数据的业务方法 SecKillGoodsServiceImpl
@Servicepublic class SecKillGoodsServiceImpl implements SecKillGoodsService {@Autowiredprivate RedisTemplate redisTemplate;private static final String SECKILL_KEY = "seckill_goods_";//秒杀商品库存头public static final String SECKILL_GOODS_STOCK_COUNT_KEY="seckill_goods_stock_count_";/*** 查询秒杀商品列表** @param time* @return*/@Overridepublic List<SeckillGoods> list(String time) {List<SeckillGoods> list = redisTemplate.boundHashOps(SECKILL_KEY + time).values();//更新库存数据的来源for (SeckillGoods seckillGoods : list) {String value = (String) redisTemplate.opsForValue().get(SECKILL_GOODS_STOCK_COUNT_KEY+seckillGoods.getId());seckillGoods.setStockCount(Integer.parseInt(value));}return list;}}
测试: 重启相关服务,登陆,秒杀 http://localhost:8001/api/wseckillgoods/toIndex

2 防止恶意刷单解决
在生产场景下,很有可能会存在某些用户恶意刷单的情况出现。这样的操作对于系统而言,会导致业务出错、脏数据、后端访问压力大等问题的出现。一般要解决这个问题的话,需要前端进行控制,同时后端也需要进行控制。后端实现可以通过Redis incrde 原子性递增来进行解决。
2.1 更新秒杀服务下单

ydles_service_seckill服务的SecKillOrderServiceImpl类
@Overridepublic boolean add(Long id, String time, String username) {//防止用户恶意刷单String result = this.preventRepeatCommit(username, id);if ("fail".equals(result)){return false;}
2.2 防重方法实现
/*** 防止用户恶意刷单* @param username 用户名* @param id 秒杀商品id* @return*/private String preventRepeatCommit(String username,Long id){String redis_key = "seckill_user_"+username+"_id_"+id;long count = redisTemplate.opsForValue().increment(redis_key, 1);if (count == 1){//代表当前用户是第一次访问.//对当前的key设置一个五分钟的有效期redisTemplate.expire(redis_key,5, TimeUnit.MINUTES);return "success";}if (count>1){return "fail";}return "fail";}
3 防止相同商品重复秒杀
需求:一个userID,只能买一个秒杀商品。
解决:秒杀订单表根据userName和秒杀商品Id控制。

3.1 修改下单业务层实现
ydles_service_seckill服务的SecKillOrderServiceImpl类
//防止重复购买SeckillOrder querySeckillOrder=seckillOrderMapper.getOrderInfoByUserNameAndGoodsId(username,id);if(querySeckillOrder!=null){return false;}
3.2 dao层新增查询方法
public interface SeckillOrderMapper extends Mapper<SeckillOrder> {/*** 查询秒杀订单信息* @param username* @param id* @return*/@Select("select * from tb_seckill_order where user_id=#{username} and seckill_id=#{id}")SeckillOrder getSecKillOrderByUserNameAndGoodsId(String username, Long id);}
4 秒杀下单接口隐藏
背景:当前虽然可以确保用户只有在登录的情况下才可以进行秒杀下单,但是无法方法有一些恶意的用户在登录了之后,猜测秒杀下单的接口地址进行恶意刷单。所以需要对秒杀接口地址进行隐藏。
需求:在用户每一次点击抢购的时候,都首先去生成一个随机数并存入redis,接着用户携带着这个随机数去访问秒杀下单,下单接口首先会从redis中获取该随机数进行匹配,如果匹配成功,则进行后续下单操作,如果匹配不成功,则认定为非法访问。
这样可有防止黑客恶意大量调取后端的秒杀下单接口。

4.1 将随机数工具类放入common工程中

RandomUtil.java放入util包下

public class RandomUtil {public static String getRandomString() {int length = 15;String base = "abcdefghijklmnopqrstuvwxyz0123456789";Random random = new Random();StringBuffer sb = new StringBuffer();for (int i = 0; i < length; i++) {int number = random.nextInt(base.length());sb.append(base.charAt(number));}return sb.toString();}public static void main(String[] args) {String randomString = RandomUtil.getRandomString();System.out.println(randomString);}}
4.2 秒杀渲染服务定义随机数接口
1ydles_web_seckill服务 util包下放入资料中的CookieUtil.java工具类
2ydles_web_seckill服务WSecKillOrderController类
@AutowiredRedisTemplate redisTemplate;/*** 生成随机数作为接口令牌, 有效期10秒* @return*/@GetMapping("/getToken")@ResponseBodypublic String getToken() {//获取随机字符串String randomString = RandomUtil.getRandomString();//获取jtiString cookieValue = this.readCookie();//短令牌作为key, 随机字符串作为valueredisTemplate.opsForValue().set("randomcode_" + cookieValue, randomString, 10, TimeUnit.SECONDS);//返回随机字符串return randomString;}/*** 读取cookie获取jti短令牌** @return*/private String readCookie() {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String jti = CookieUtil.readCookie(request, "uid").get("uid");return jti;}
4.3 js修改
修改js下单方法
//秒杀下单
add:function (id) {
axios.get("/api/wseckillorder/getToken").then(function (response) {
var random =response.data;
axios.get("/api/wseckillorder/add?time="+moment(app.dateMenus[0]).format("YYYYMMDDHH")+"&id="+id+"&random="+random).then(function (response) {
if (response.data.flag){
alert("抢单成功,即将进入支付");
} else{
alert("抢单失败");
}
})
})
}
4.4 秒杀渲染服务更改

修改秒杀渲染服务下单接口
@RestController
@RequestMapping("/wseckillorder")
public class WSecKillOrderController {
@Autowired
private SecKillOrderFeign secKillOrderFeign;
@Autowired
private RedisTemplate redisTemplate;
/**
* 秒杀下单
* @param time
* @param id
* @return
*/
@RequestMapping("/add")
public Result add(@RequestParam("time") String time, @RequestParam("id") Long id, String random) {
String cookieValue = this.readCookie();
String redisRandomCode = (String) redisTemplate.opsForValue().get("randomcode_" + cookieValue);
if (StringUtils.isEmpty(redisRandomCode)) {
return new Result(false, StatusCode.ERROR, "下单失败");
}
if (!random.equals(redisRandomCode)) {
return new Result(false, StatusCode.ERROR, "下单失败");
}
Result result = secKillOrderFeign.add(time, id);
return result;
}
5秒杀下单接口限流-难点-不是重点
因为秒杀的特殊业务场景,生产场景下,还有可能要对秒杀下单接口进行访问流量控制,防止过多的请求进入到后端服务器。对于限流的实现方式,我们之前已经接触过通过nginx限流,网关限流。但是他们都是对一个大的服务进行访问限流,如果现在只是要对某一个服务中的接口方法进行限流呢?这里推荐使用google提供的guava工具包中的RateLimiter进行实现,其内部是基于令牌桶算法进行限流计算。
1限流 技术:guava
2自定义注解:@interface 基础的元注解信息
3aop 注解方式面向切面编程:前置增强、后置增强、环绕增强、最终增强
4springmvc 自带 HttpServletResponse response
5流 首先定义出来,最后关流
ydles_web_seckill服务:
1添加依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.0-jre</version>
</dependency>
2自定义限流注解
com.ydles.seckill.web.aspect
@Inherited
@Documented
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME) //不仅保存到class文件中,并且jvm加载class之后,该注解仍然存在
public @interface AccessLimit {
}
3自定义切面类
com.ydles.seckill.web.aspect包下
package com.ydles.seckill.web.aspect;
import com.alibaba.fastjson.JSON;
import com.google.common.util.concurrent.RateLimiter;
import com.ydles.entity.Result;
import com.ydles.entity.StatusCode;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* @Created by IT李老师
* 公主号 “IT李哥交朋友”
* 个人微 itlils
*/
@Component
@Scope
@Aspect //标明切面类
public class AccessLimitAop {
@Autowired
private HttpServletResponse response;
//设置令牌的生成速率
private RateLimiter rateLimiter = RateLimiter.create(2.0); //每秒生成两个令牌存入桶中
@Pointcut("@annotation(com.ydles.seckill.web.aspect.AccessLimit)")
public void limit(){
}
@Around("limit()")
public Object around(ProceedingJoinPoint proceedingJoinPoint){
//限流逻辑
//拿令牌
boolean result = rateLimiter.tryAcquire();
Object object = null;
if(result){
//拿到令牌了
try {
//切面往后走
object = proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}else {
//拿不到令牌
Result<Object> objectResult = new Result<>(false, StatusCode.ACCESSLIMIT, "限流了,请售后再试");
String msg = JSON.toJSONString(objectResult);
//将信息写回到用户的浏览器
writeMsg(response,msg);
}
return object;
}
//给用户写回数据
public void writeMsg(HttpServletResponse response,String msg){
ServletOutputStream outputStream=null;
try {
response.setContentType("application/json;charset=utf-8");
outputStream = response.getOutputStream();
outputStream.write(msg.getBytes("utf-8"));
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
4使用自定义限流注解
@AccessLimit放在下单方法上即可

总结:
1秒杀后台异步下单

mq 保证消息不丢
1交换机 队列 消息 持久化
2生产者:comfirm机制

3消费者:手动ack
2防止恶意刷单
redis:key username_id 5min
3防止相同商品秒杀
数据库order查询 拒绝
4秒杀接口隐藏

5秒杀 接口限流
自定义注解 aop
