第十二章:订单服务
一、RabbitMQ环境的搭建
1)、安装RabbitMQ
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management
2)、访问15672端口登录后台管理界面
默认的用户名和密码都是guest
3)、为order订单访问引入RabbitMQ服务
1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2.配置环境
spring:
rabbitmq:
host: 自己的地址
port: 5672
virtual-host: /
publisher-confirm-type: correlated
publisher-returns: true
template:
mandatory: true
listener:
simple:
acknowledge-mode: manual
3.添加@EnableRabbit注解
二、订单模块的前端整合
和前面一样,这里省略
三、整合spring session
使登录信息同步
1)、引入session的依赖
<!--redis缓存-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
2)、引入session配置和线程池配置
spring.session.store-type=redis
gulimall.thread.core-size=20
gulimall.thread.max-size=200
gulimall.thread.keep-alive-time=10
spring.redis.host=101.43.79.94
3)、开启注解
@EnableRedisHttpSession
四、配置拦截器
1)、拦截order项目所有请求
@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {
@Autowired
LoginUserInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor).addPathPatterns("/**");
}
}
2)、设置拦截没登录的请求
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// /order/order/status/2948294820984028420
String uri = request.getRequestURI();
AntPathMatcher antPathMatcher = new AntPathMatcher();
boolean match = antPathMatcher.match("/order/order/status/**", uri);
boolean match1 = antPathMatcher.match("/payed/notify", uri);
if(match || match1){
return true;
}
MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if(attribute!=null){
loginUser.set(attribute);
return true;
}else {
//没登录就去登录
request.getSession().setAttribute("msg","请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
五、确认订单业务
1)、封装vo
//订单确认页需要用的数据
public class OrderConfirmVo {
//// 收货地址,ums_member_receive_address表
@Setter @Getter
List<MemberAddressVo> address;
//所有选中的购物项
@Setter @Getter
List<OrderItemVo> items;
//发票记录....
//优惠券信息...
@Setter @Getter
Integer integration;
@Setter @Getter
Map<Long,Boolean> stocks;
//防重令牌
@Setter @Getter
String orderToken;
public Integer getCount(){
Integer i = 0 ;
if(items!=null){
for (OrderItemVo item : items) {
i+=item.getCount();
}
}
return i;
}
// BigDecimal total;//订单总额
public BigDecimal getTotal() {
BigDecimal sum = new BigDecimal("0");
if(items!=null){
for (OrderItemVo item : items) {
BigDecimal multiply = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
sum = sum.add(multiply);
}
}
return sum;
}
// BigDecimal payPrice;
public BigDecimal getPayPrice() {
return getTotal();
}
//应付价格
}
2)、获取member的收货地址
@GetMapping("/{memeberId}/addresses")
public List<MemberReceiveAddressEntity> getAddress(@PathVariable("memeberId") Long memberId){
return memberReceiveAddressService.getAddress(memberId);
}
3)、获取购物项
@GetMapping("/currentUserCartItems")
@ResponseBody
public List<CartItem> getCurrentUserCartItems(){
return cartService.getUserCartItems();
}
4)、查询用户积分
//3、查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
5)、请求头问题
feign远程调用丢失请求头问题:
远程调用新创建的请求会丢失cookie等请求头,需要为其添加请求头
@Configuration
public class GuliFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor(){
@Override
public void apply(RequestTemplate template) {
//1、RequestContextHolder拿到刚进来的这个请求
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if(attributes!=null){
System.out.println("RequestInterceptor线程...."+Thread.currentThread().getId());
HttpServletRequest request = attributes.getRequest(); //老请求
if(request != null){
//同步请求头数据,Cookie
String cookie = request.getHeader("Cookie");
//给新请求同步了老请求的cookie
template.header("Cookie",cookie);
}
}
}
};
}
}
6)、异步编排
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//1、远程查询所有的收货地址列表
System.out.println("member线程...." + Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddress(address);
}, executor);
7)、查询库存
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
//2、远程查询购物车所有选中的购物项
System.out.println("cart线程...." + Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
//feign在远程调用之前要构造请求,调用很多的拦截器
//RequestInterceptor interceptor : requestInterceptors
}, executor).thenRunAsync(() -> {
List<OrderItemVo> items = confirmVo.getItems();
List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
//TODO 一定要启动库存服务,否则库存查不出。
R hasStock = wmsFeignService.getSkusHasStock(collect);
List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
});
if (data != null) {
Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(map);
}
}, executor);
高亮显示
function highlight() {
$(".addr-item p").css({"border": "2px solid gray"})
$(".addr-item p[def='1']").css({"border": "2px solid red"})
}
$(".addr-item p").click(function () {
$(".addr-item p").attr("def", "0")
$(this).attr("def", "1");
highlight();
//获取到当前的地址id
var addrId = $(this).attr("addrId");
//发送ajax获取运费信息
getFare(addrId);
});
8)、获取物流信息,设置运费
@GetMapping("/fare")
public R getFare(@RequestParam("addrId") Long addrId){
FareVo fare = wareInfoService.getFare(addrId);
return R.ok().setData(fare);
}
function getFare(addrId) {
//给表单回填选择的地址
$("#addrIdInput").val(addrId);
$.get("http://gulimall.com/api/ware/wareinfo/fare?addrId=" + addrId, function (resp) {
console.log(resp);
//fareEle
$("#fareEle").text(resp.data.fare);
var total = [[${orderConfirmData.total}]];
//设置运费等
var payPrice = total * 1 + resp.data.fare * 1;
$("#payPriceEle").text(payPrice);
$("#payPriceInput").val(payPrice);
//设置收货人信息
$("#recieveAddressEle").text(resp.data.address.province + " " + resp.data.address.detailAddress);
$("#recieverEle").text(resp.data.address.name);
})
}
9)、防重令牌
//TODO 5、防重令牌
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId(), token, 30, TimeUnit.MINUTES);
confirmVo.setOrderToken(token);
CompletableFuture.allOf(getAddressFuture, cartFuture).get();
六、下单业务
1)、验证令牌
//1、验证令牌【令牌的对比和删除必须保证原子性】
//0令牌失败 - 1删除成功
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
String orderToken = vo.getOrderToken();
//原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
if (result == 0L) {
//令牌验证失败
response.setCode(1);
return response;
} else {
//令牌验证成功
//下单:去创建订单,验令牌,验价格,锁库存...
}
2)、生成订单
private OrderCreateTo createOrder() {
OrderCreateTo createTo = new OrderCreateTo();
//1、生成订单号
String orderSn = IdWorker.getTimeId();
//创建订单号
OrderEntity orderEntity = buildOrder(orderSn);
//2、获取到所有的订单项
List<OrderItemEntity> itemEntities = buildOrderItems(orderSn);
//3、计算价格、积分等相关
computePrice(orderEntity, itemEntities);
createTo.setOrder(orderEntity);
createTo.setOrderItems(itemEntities);
return createTo;
}
private OrderEntity buildOrder(String orderSn) {
MemberRespVo respVo = LoginUserInterceptor.loginUser.get();
OrderEntity entity = new OrderEntity();
entity.setOrderSn(orderSn);
entity.setMemberId(respVo.getId());
OrderSubmitVo submitVo = confirmVoThreadLocal.get();
//获取收货地址信息
R fare = wmsFeignService.getFare(submitVo.getAddrId());
FareVo fareResp = fare.getData(new TypeReference<FareVo>() {
});
//设置运费信息
entity.setFreightAmount(fareResp.getFare());
//设置收货人信息
entity.setReceiverCity(fareResp.getAddress().getCity());
entity.setReceiverDetailAddress(fareResp.getAddress().getDetailAddress());
entity.setReceiverName(fareResp.getAddress().getName());
entity.setReceiverPhone(fareResp.getAddress().getPhone());
entity.setReceiverPostCode(fareResp.getAddress().getPostCode());
entity.setReceiverProvince(fareResp.getAddress().getProvince());
entity.setReceiverRegion(fareResp.getAddress().getRegion());
//设置订单的相关状态信息
entity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
entity.setAutoConfirmDay(7);
return entity;
}
3)、获取所有订单项
/**
* 构建所有订单项数据
*
* @return
*/
private List<OrderItemEntity> buildOrderItems(String orderSn) {
//最后确定每个购物项的价格
List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
if (currentUserCartItems != null && currentUserCartItems.size() > 0) {
List<OrderItemEntity> itemEntities = currentUserCartItems.stream().map(cartItem -> {
OrderItemEntity itemEntity = buildOrderItem(cartItem);
itemEntity.setOrderSn(orderSn);
return itemEntity;
}).collect(Collectors.toList());
return itemEntities;
}
return null;
}
/**
* 构建某一个订单项
*
* @param cartItem
*
* @return
*/
private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
OrderItemEntity itemEntity = new OrderItemEntity();
//1、订单信息:订单号 v
//2、商品的SPU信息 V
Long skuId = cartItem.getSkuId();
R r = productFeignService.getSpuInfoBySkuId(skuId);
SpuInfoVo data = r.getData(new TypeReference<SpuInfoVo>() {
});
itemEntity.setSpuId(data.getId());
itemEntity.setSpuBrand(data.getBrandId().toString());
itemEntity.setSpuName(data.getSpuName());
itemEntity.setCategoryId(data.getCatalogId());
//3、商品的sku信息 v
itemEntity.setSkuId(cartItem.getSkuId());
itemEntity.setSkuName(cartItem.getTitle());
itemEntity.setSkuPic(cartItem.getImage());
itemEntity.setSkuPrice(cartItem.getPrice());
String skuAttr = StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(), ";");
itemEntity.setSkuAttrsVals(skuAttr);
itemEntity.setSkuQuantity(cartItem.getCount());
//4、优惠信息[不做]
//5、积分信息
itemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
itemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
//6、订单项的价格信息
itemEntity.setPromotionAmount(new BigDecimal("0"));
itemEntity.setCouponAmount(new BigDecimal("0"));
itemEntity.setIntegrationAmount(new BigDecimal("0"));
//当前订单项的实际金额。 总额-各种优惠
BigDecimal orign = itemEntity.getSkuPrice().multiply(new BigDecimal(itemEntity.getSkuQuantity().toString()));
BigDecimal subtract = orign.subtract(itemEntity.getCouponAmount())
.subtract(itemEntity.getPromotionAmount())
.subtract(itemEntity.getIntegrationAmount());
itemEntity.setRealAmount(subtract);
return itemEntity;
}
4)、获取金额
private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> itemEntities) {
BigDecimal total = new BigDecimal("0.0");
BigDecimal coupon = new BigDecimal("0.0");
BigDecimal integration = new BigDecimal("0.0");
BigDecimal promotion = new BigDecimal("0.0");
BigDecimal gift = new BigDecimal("0.0");
BigDecimal growth = new BigDecimal("0.0");
//订单的总额,叠加每一个订单项的总额信息
for (OrderItemEntity entity : itemEntities) {
coupon = coupon.add(entity.getCouponAmount());
integration = integration.add(entity.getIntegrationAmount());
promotion = promotion.add(entity.getPromotionAmount());
total = total.add(entity.getRealAmount());
gift = gift.add(new BigDecimal(entity.getGiftIntegration().toString()));
// Integer growth = entity.getGiftGrowth();
growth = growth.add(new BigDecimal(entity.getGiftGrowth().toString()));
}
//1、订单价格相关
orderEntity.setTotalAmount(total);
//应付总额
orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
orderEntity.setPromotionAmount(promotion);
orderEntity.setIntegrationAmount(integration);
orderEntity.setCouponAmount(coupon);
//设置积分等信息
orderEntity.setIntegration(gift.intValue());
orderEntity.setGrowth(growth.intValue());
orderEntity.setDeleteStatus(0);//未删除
}
5)、保存订单
/**
* 保存订单数据
*
* @param order
*/
private void saveOrder(OrderCreateTo order) {
OrderEntity orderEntity = order.getOrder();
orderEntity.setModifyTime(new Date());
this.save(orderEntity);
List<OrderItemEntity> orderItems = order.getOrderItems();
orderItemService.saveBatch(orderItems);
}
6)、锁定库存
/**
* 为某个订单锁定库存
* <p>
* (rollbackFor = NoStockException.class)
* 默认只要是运行时异常都会回滚
*
* @param vo 库存解锁的场景
* 1)、下订单成功,订单过期没有支付被系统自动取消、被用户手动取消。都要解锁库存
* <p>
* <p>
* 2)、下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。
* 之前锁定的库存就要自动解锁。
*
* @return
*/
@Transactional
@Override
public Boolean orderLockStock(WareSkuLockVo vo) {
/**
* 保存库存工作单的详情。
* 追溯。
*/
WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
taskEntity.setOrderSn(vo.getOrderSn());
orderTaskService.save(taskEntity);
//1、按照下单的收货地址,找到一个就近仓库,锁定库存。
//1、找到每个商品在哪个仓库都有库存
List<OrderItemVo> locks = vo.getLocks();
List<SkuWareHasStock> collect = locks.stream().map(item -> {
SkuWareHasStock stock = new SkuWareHasStock();
Long skuId = item.getSkuId();
stock.setSkuId(skuId);
stock.setNum(item.getCount());
//查询这个商品在哪里有库存
List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
stock.setWareId(wareIds);
return stock;
}).collect(Collectors.toList());
//2、锁定库存
for (SkuWareHasStock hasStock : collect) {
Boolean skuStocked = false;
Long skuId = hasStock.getSkuId();
List<Long> wareIds = hasStock.getWareId();
if (wareIds == null || wareIds.size() == 0) {
//没有任何仓库有这个商品的库存
throw new NoStockException(skuId);
}
//1、如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ
//2、锁定失败。前面保存的工作单信息就回滚了。发送出去的消息,即使要解锁记录,由于去数据库查不到id,所以就不用解锁
// 1: 1 - 2 - 1 2:2-1-2 3:3-1-1(x)
for (Long wareId : wareIds) {
//成功就返回1,否则就是0
Long count = wareSkuDao.lockSkuStock(skuId, wareId, hasStock.getNum());
if (count == 1) {
skuStocked = true;
//TODO 告诉MQ库存锁定成功
WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity(null, skuId, "", hasStock.getNum(), taskEntity.getId(), wareId, 1);
orderTaskDetailService.save(entity);
StockLockedTo lockedTo = new StockLockedTo();
lockedTo.setId(taskEntity.getId());
StockDetailTo stockDetailTo = new StockDetailTo();
BeanUtils.copyProperties(entity, stockDetailTo);
//只发id不行,防止回滚以后找不到数据
lockedTo.setDetail(stockDetailTo);
// rabbitTemplate
rabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked", lockedTo);
break;
} else {
//当前仓库锁失败,重试下一个仓库
}
}
if (skuStocked == false) {
//当前商品所有仓库都没有锁住
throw new NoStockException(skuId);
}
}
//3、肯定全部都是锁定成功过的
return true;
}
七、分布式事务
1)、seata环境准备
1.为数据库添加回调undo_log
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
2.下载包
下载seata-server-1.3.0
2)、整合环境
1.导入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
2.注册配置中心
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "gulimall-seata"
serverAddr = "端口:8848"
group = "DEFAULT_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
3.配置file
store {
## store mode: file、db、redis
mode = "file"
## file store property
file {
## store location dir
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
maxBranchSessionSize = 16384
# globe session size , if exceeded throws exceptions
maxGlobalSessionSize = 512
# file buffer size , if exceeded allocate new buffer
fileWriteBufferCacheSize = 16384
# when recover batch read size
sessionReloadReadSize = 100
# async, sync
flushDiskMode = async
}
4.配置seataconfig
@Configuration
public class MySeataConfig {
@Autowired
DataSourceProperties dataSourceProperties;
@Bean
public DataSource dataSource(DataSourceProperties dataSourceProperties){
//properties.initializeDataSourceBuilder().type(type).build();
HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
if (StringUtils.hasText(dataSourceProperties.getName())) {
dataSource.setPoolName(dataSourceProperties.getName());
}
return new DataSourceProxy(dataSource);
}
}
5.为分布式微服务使用代理数据源
@EnableAspectJAutoProxy(exposeProxy = true)
6.为分布式服务添加注解
全局事务添加全局注解
@GlobalTransactional //高并发
分支事务添加普通注解
@Transactional
八、消息队列
1)、创建信息队列MyMQconfig
@Configuration
public class MyMQConfig {
//@Bean Binding,Queue,Exchange
/**
* 容器中的 Binding,Queue,Exchange 都会自动创建(RabbitMQ没有的情况)
* RabbitMQ 只要有。@Bean声明属性发生变化也不会覆盖
* @return
*/
@Bean
public Queue orderDelayQueue() {
Map<String,Object> arguments = new HashMap<>();
/**
* x-dead-letter-exchange: order-event-exchange
* x-dead-letter-routing-key: order.release.order
* x-message-ttl: 60000
*/
arguments.put("x-dead-letter-exchange","order-event-exchange");
arguments.put("x-dead-letter-routing-key","order.release.order");
arguments.put("x-message-ttl",60000);
//String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
Queue queue = new Queue("order.delay.queue", true, false, false,arguments);
return queue;
}
@Bean
public Queue orderReleaseOrderQueue() {
Queue queue = new Queue("order.release.order.queue", true, false, false);
return queue;
}
@Bean
public Exchange orderEventExchange() {
//String name, boolean durable, boolean autoDelete, Map<String, Object> arguments
return new TopicExchange("order-event-exchange",true,false);
}
@Bean
public Binding orderCreateOrderBingding() {
//String destination, DestinationType destinationType, String exchange, String routingKey,
// Map<String, Object> arguments
return new Binding("order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",
null);
}
@Bean
public Binding orderReleaseOrderBingding() {
return new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null);
}
/**
* 订单释放直接和库存释放进行绑定
* @return
*/
@Bean
public Binding orderReleaseOtherBingding() {
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.other.#",
null);
}
@Bean
public Queue orderSeckillOrderQueue(){
//String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
return new Queue("order.seckill.order.queue",true,false,false);
}
@Bean
public Binding orderSeckillOrderQueueBinding(){
/**
* String destination, DestinationType destinationType, String exchange, String routingKey,
* Map<String, Object> arguments
*/
return new Binding("order.seckill.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.seckill.order",
null);
}
2)、为ware配置MQ
1.引入包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2.配置开始注解
@EnableRabbit
spring:
rabbitmq:
host: 101.43.79.94
port: 5672
virtual-host: /
publisher-confirm-type: correlated
publisher-returns: true
template:
mandatory: true
listener:
simple:
acknowledge-mode: manual
3.配置类
通上面的MyMQconfig
@Configuration
public class MyRabbitConfig {
/**
* 使用JSON序列化机制,进行消息转换
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
// @RabbitListener(queues = "stock.release.stock.queue")
// public void handle(Message message){
//
// }
@Bean
public Exchange stockEventExchange(){
//String name, boolean durable, boolean autoDelete, Map<String, Object> arguments
return new TopicExchange("stock-event-exchange",true,false);
}
@Bean
public Queue stockReleaseStockQueue(){
//String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
return new Queue("stock.release.stock.queue",true,false,false);
}
@Bean
public Queue stockDelayQueue(){
/**
* x-dead-letter-exchange: stock-event-exchange
* x-dead-letter-routing-key: order.release.order
* x-message-ttl: 60000
*/
Map<String,Object> args = new HashMap<>();
args.put("x-dead-letter-exchange","stock-event-exchange");
args.put("x-dead-letter-routing-key","stock.release");
args.put("x-message-ttl",120000);
return new Queue("stock.delay.queue",true,false,false,args);
}
@Bean
public Binding stockReleaseBinding(){
/**
* String destination, DestinationType destinationType, String exchange, String routingKey,
* Map<String, Object> arguments
*/
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.release.#",
null);
}
@Bean
public Binding stockLockedBinding(){
/**
* String destination, DestinationType destinationType, String exchange, String routingKey,
* Map<String, Object> arguments
*/
return new Binding("stock.delay.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.locked",
null);
}
}
3)、解锁库存
@Override
public void unlockStock(StockLockedTo to) {
StockDetailTo detail = to.getDetail();
Long detailId = detail.getId();
//解锁
//1、查询数据库关于这个订单的锁定库存信息。
// 有:证明库存锁定成功了
// 解锁:订单情况。
// 1、没有这个订单。必须解锁
// 2、有这个订单。不是解锁库存。
// 订单状态: 已取消:解锁库存
// 没取消:不能解锁
// 没有:库存锁定失败了,库存回滚了。这种情况无需解锁
WareOrderTaskDetailEntity byId = orderTaskDetailService.getById(detailId);
if (byId != null) {
//解锁
Long id = to.getId();
WareOrderTaskEntity taskEntity = orderTaskService.getById(id);
String orderSn = taskEntity.getOrderSn();//根据订单号查询订单的状态
R r = orderFeignService.getOrderStatus(orderSn);
if (r.getCode() == 0) {
//订单数据返回成功
OrderVo data = r.getData(new TypeReference<OrderVo>() {
});
if (data == null || data.getStatus() == 4) {
//订单不存在
//订单已经被取消了。才能解锁库存
//detailId
if (byId.getLockStatus() == 1) {
//当前库存工作单详情,状态1 已锁定但是未解锁才可以解锁
unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);
}
}
} else {
//消息拒绝以后重新放到队列里面,让别人继续消费解锁。
throw new RuntimeException("远程服务失败");
}
} else {
//无需解锁
}
}
4)、监听
监听自动解锁
@Service
@RabbitListener(queues = "stock.release.stock.queue")
public class StockReleaseListener {
@Autowired
WareSkuService wareSkuService;
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
System.out.println("收到解锁库存的消息...");
try{
//当前消息是否被第二次及以后(重新)派发过来了。
// Boolean redelivered = message.getMessageProperties().getRedelivered();
wareSkuService.unlockStock(to);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
@RabbitHandler
public void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {
System.out.println("订单关闭准备解锁库存...");
try{
wareSkuService.unlockStock(orderTo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
监听关单
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {
@Autowired
OrderService orderService;
@RabbitHandler
public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {
System.out.println("收到过期的订单信息:准备关闭订单"+entity.getOrderSn()+"==>"+entity.getId());
try{
orderService.closeOrder(entity);
//手动调用支付宝收单;
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
九、整合支付宝付款
1)、引入阿里支付的服务
1.引入包
<!-- 导入支付宝的SDK-->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.21.10.ALL</version>
</dependency>
2.封装支付工具类AlipayTemplate
修改为自己的密钥
2)、支付业务
1.抽取支付的vo
@Data
public class PayVo {
private String out_trade_no; // 商户订单号 必填
private String subject; // 订单名称 必填
private String total_amount; // 付款金额 必填
private String body; // 商品描述 可空
}
2.修改前端页面,完善支付逻辑
<li>
<img src="/static/order/pay/img/zhifubao.png" style="weight:auto;height:30px;" alt="">
<a th:href="'http://order.gulimall.com/payOrder?orderSn='+${submitOrderResp.order.orderSn}">支付宝</a>
</li>
@Controller
public class PayWebController {
@Autowired
AlipayTemplate alipayTemplate;
@Autowired
OrderService orderService;
/**
* 1、将支付页让浏览器展示。
* 2、支付成功以后,我们要跳到用户的订单列表页
* @param orderSn
* @return
* @throws AlipayApiException
*/
@ResponseBody
@GetMapping(value = "/payOrder",produces = "text/html")
public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {
// PayVo payVo = new PayVo();
// payVo.setBody();//订单的备注
// payVo.setOut_trade_no();//订单号
// payVo.setSubject();//订单的主题
// payVo.setTotal_amount();
PayVo payVo = orderService.getOrderPay(orderSn);
//返回的是一个页面。将此页面直接交给浏览器就行
String pay = alipayTemplate.pay(payVo);
System.out.println(pay);
return pay;
}
}
3)、整合会员服务
1.引入thymeleaf模板和redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
2.添加配置
3.添加拦截器
和之前一样,为登录无法访问
4.添加网关
- id: gulimall-member-route
uri: lb://gulimall-member
predicates:
- Host=member.gulimall.com
5)、渲染订单服务
1.业务逻辑
@Controller
public class MemberWebController {
@Autowired
OrderFeignService orderFeignService;
/**
* 订单分页查询
* @param pageNum
* @param model
* @return
*/
@GetMapping("/memberOrder.html")
public String memberOrderPage(@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum,
Model model, HttpServletRequest request){
//获取到支付宝给我们传来的所有请求数据;
// request。验证签名,如果正确可以去修改。
//查出当前登录的用户的所有订单列表数据
Map<String,Object> page =new HashMap<>();
page.put("page",pageNum.toString());
//
R r = orderFeignService.listWithItem(page);
System.out.println(JSON.toJSONString(r));
model.addAttribute("orders",r);
return "orderList";
}
}
2.网络的请求拦截器
配置GuliFeignConfig,和之前的一样,保存传入的cookies
6)、异步通知
1.配置通知业务
@RestController
public class OrderPayedListener {
@Autowired
AlipayTemplate alipayTemplate;
@Autowired
OrderService orderService;
/**
* 支付宝成功异步通知
* @param request
* @return
*/
@PostMapping("/payed/notify")
public String handleAlipayed(PayAsyncVo vo,HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {
//只要我们收到了支付宝给我们异步的通知,告诉我们订单支付成功。返回success,支付宝就再也不通知
// Map<String, String[]> map = request.getParameterMap();
// for (String key : map.keySet()) {
// String value = request.getParameter(key);
// System.out.println("参数名:"+key+"==>参数值:"+value);
// }
//验签
Map<String,String> params = new HashMap<String,String>();
Map<String,String[]> requestParams = request.getParameterMap();
for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
//乱码解决,这段代码在出现乱码时使用
// valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
}
boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(), alipayTemplate.getCharset(), alipayTemplate.getSign_type()); //调用SDK验证签名
if(signVerified){
System.out.println("签名验证成功...");
String result = orderService.handlePayResult(vo);
return result;
}else {
System.out.println("签名验证失败...");
return "error";
}
}
}
2.配置nginx
location /payed/ {
proxy_set_header Host order.gulimall.com;
proxy_pass http://gulimall;
}
Field error in object 'payAsyncVo' on field 'notify_time'问题
添加全局时间的配置
spring.mvc.format.date=yyyy-MM-dd HH:mm:ss
7)、收单
30min没支付,自动收单
private String timeout = "30m";
alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
+ "\"total_amount\":\""+ total_amount +"\","
+ "\"subject\":\""+ subject +"\","
+ "\"body\":\""+ body +"\","
+ "\"timeout_express\":\""+timeout+"\","
+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");