feat:超时关单和释放库存完善

This commit is contained in:
haoxr 2021-03-16 20:19:50 +08:00
parent 8288c0ddfe
commit bc2b3881cd
37 changed files with 248 additions and 769 deletions

View File

@ -14,32 +14,18 @@ import javax.validation.constraints.Size;
@Data
public class OrderSubmitInfoDTO {
/**
* 用户选择地址id
*/
@NotBlank(message = "请选择收货地址")
private String addressId;
/**
* 如果携带skuId则表示该订单通过直接下单方式生成
* 否则从购物车中生成 -- 清空购物车
*/
private Long skuId;
/**
* 直接下单时商品数量
*/
private Integer skuNum;
/**
* 优惠券id
*/
private Long payAmount;
private String couponId;
@NotBlank(message = "请选择收货地址")
private String addressId;
@Size(max = 500, message = "订单备注长度不能超过500")
private String remark;
private Long payAmount;
}

View File

@ -12,7 +12,6 @@ import java.util.List;
public class OrderConfirmVO extends BaseVO {
/**
* 订单总额
*/
@ -21,28 +20,12 @@ public class OrderConfirmVO extends BaseVO {
/**
* 商品列表
* 订单商品
*/
@Getter
@Setter
private List<OrderItemVO> items;
// 优惠券信息
@Getter
@Setter
private List<CouponVO> coupons;
// 积分信息
@Getter
@Setter
private Integer integration;
/**
* 应付价格
*/
@Setter
private Long payAmount;
public Long getTotalPrice() {
Long total = 0L;
@ -51,12 +34,4 @@ public class OrderConfirmVO extends BaseVO {
}
return total;
}
public Long getPayPrice() {
Long total = 0L;
if (items != null && items.size() > 0) {
total = items.stream().mapToLong(OrderItemVO::getSubtotal).sum();
}
return total;
}
}

View File

@ -55,8 +55,6 @@
<artifactId>common-rabbitmq</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>

View File

@ -0,0 +1,81 @@
package com.youlai.mall.oms.config;
import com.youlai.mall.oms.constant.OmsConstants;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @author huawei
* @email huawei_code@163.com
* @date 2021/1/17
* @desc rabbitmq 业务相关配置类
* 订单相关业务统一使用 order-event-exchange 交换机
* 1. 订单创建成功发送消息到创建订单的路由
* 2. 创建订单的路由转发消息给延时队列延时队列的延时时间就是订单从创建到支付过程允许的最大等待时间延时队列不能有消费者即消息不能被消费
* 3. 延时时间一到消息被转入DLX死信路由
* 4. 死信路由把死信消息转发给死信队列
* 5. 订单系统监听死信队列获取到死信消息后执行关单解库存操作
*/
@Configuration
@Slf4j
@AllArgsConstructor
public class RabbitMQConfig {
private RabbitTemplate rabbitTemplate;
/**
* 定义交换机
*/
@Bean
public Exchange exchange() {
return new TopicExchange("order_event_exchange", true, false);
}
/**
* 延时队列
*/
@Bean
public Queue delayQueue() {
// 延时队列的消息过期了会自动触发消息的转发根据routingKey发送到指定的exchange中exchange路由到死信队列
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "order_event_exchange");
args.put("x-dead-letter-routing-key", "order:close"); // 死信路由Key
args.put("x-message-ttl", 60000); // 单位毫秒配置1分钟测试使用
return new Queue("order.delay.queue", true, false, false, args);
}
/**
* 延时队列绑定交换机路由键order.create
* 订单提交时会发送routingKey=order.create.order的消息至exchange然后会被路由到上面的delayQueue延时队列延时队列没有消费者到期后会降消息转发
*/
@Bean
public Binding delayQueueBinding() {
return new Binding("order.delay.queue", Binding.DestinationType.QUEUE,"order_event_exchange","order.create",null);
}
/**
* 死信队列普通队列
*/
@Bean
public Queue closeOrderQueue() {
return new Queue("order.close.queue", true, false, false);
}
/**
* 死信队列绑定交换机
* 其中死信路由的routingKey=order:close和延时队列的routingKey一致延时队列过期时将消息发送给exchangeexchange再路由到死信队列
*/
@Bean
public Binding closeOrderQueueBinding() {
return new Binding("order.close.queue", Binding.DestinationType.QUEUE,"order_event_exchange","order:close",null);
}
}

View File

@ -4,7 +4,6 @@ import com.google.common.collect.Lists;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.Order;
import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.OAuthBuilder;

View File

@ -1,145 +0,0 @@
package com.youlai.mall.oms.config.rabbitmq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
/**
* @author huawei
* @desc rabbitmq 业务相关配置类
* 订单相关业务统一使用 order-event-exchange 交换机
* order.delay.queue 队列用于存放需要延时多长时间处理的订单不需要被消费
* order.release.queue 队列用于真正执行处理订单需要被业务消费如果订单还没有付款需要自动关闭订单并释放库存
* <p>
* order.create.order 建立交换机与 order.delay.queue 队列的绑定关系目的是将新创建的订单放入延时队列中
* order.release.order 建立交换机与 order.release.queue 队列的绑定关系目的是将延时队列中到达延时时间的订单执行释放操作
* <p>
* 自动创建相关 交换机队列路由键
* @email huawei_code@163.com
* @date 2021/1/17
*/
@Configuration
@Slf4j
public class OmsRabbitConfig {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 订单死信-延时队列
*
* @return
*/
@Bean
public Queue orderDelayQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", OmsRabbitConstants.ORDER_EVENT_EXCHANGE);
args.put("x-dead-letter-routing-key", OmsRabbitConstants.ORDER_RELEASE_ORDER_KEY);
args.put("x-message-ttl", 60000);
Queue queue = new Queue(OmsRabbitConstants.ORDER_DELAY_QUEUE, true, false, false, args);
return queue;
}
/**
* 释放订单队列
* 订单系统从该队列中取出订单order判断是否超时未支付
*
* @return
*/
@Bean
public Queue orderReleaseQueue() {
return new Queue(OmsRabbitConstants.ORDER_RELEASE_QUEUE, true, false, false);
}
/**
* Topic类型 订单交换机
* @return
*/
@Bean
public Exchange orderEventExchange() {
return new TopicExchange(OmsRabbitConstants.ORDER_EVENT_EXCHANGE, true, false);
}
/**
* 建立 order.delay.queue 队列与交换机绑定
* @return
*/
@Bean
public Binding orderCreateOrderBinding() {
//String destination, DestinationType destinationType, String exchange, String routingKey,
// @Nullable Map<String, Object> arguments
return new Binding(OmsRabbitConstants.ORDER_DELAY_QUEUE, Binding.DestinationType.QUEUE, OmsRabbitConstants.ORDER_EVENT_EXCHANGE, OmsRabbitConstants.ORDER_CREATE_ORDER_KEY, null);
}
/**
* 建立 order.release.queue 与交换机绑定
* @return
*/
@Bean
public Binding orderReleaseOrderBinding() {
return new Binding(OmsRabbitConstants.ORDER_RELEASE_QUEUE, Binding.DestinationType.QUEUE, OmsRabbitConstants.ORDER_EVENT_EXCHANGE, OmsRabbitConstants.ORDER_RELEASE_ORDER_KEY, null);
}
/**
* 生产者投递消息后如果Broker收到消息后会给生产者一个ACK生产者通过ACK可以确认这条消息是否正常发送到Broker这种方式是消息可靠性投递的核心
* 步骤1yaml文件中添加配置 spring.rabbitmq.publisher-confirm-type: correlated
* 步骤2编写代码
*/
@PostConstruct
public void setConfirmCallback() {
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* @param correlationData 发送消息时指定的唯一关联数据消息id
* @param ack 投递结果
* @param cause 失败原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
log.info("消息投递到交换机成功:[correlationData={}]", correlationData);
} else {
log.error("消息投递到交换机成功:[correlationData={},原因={}]", correlationData, cause);
}
//TODO 根据ACK状态做对应的消息更新操作
}
});
}
/**
*
* 注意下面两项必须同时配置
* # 开启阶段二(消息从E->Q)的确认回调 Exchange --> Queue returnCallback
* spring.rabbitmq.publisher-returns=true
*
* #为true,则交换机处理消息到路由失败则会返回给生产者
* spring.rabbitmq.template.mandatory=true
*/
@PostConstruct
public void setQueueCallback() {
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.error("路由到队列失败,[消息内容:{},交换机:{},路由件:{},回复码:{},回复文本:{}]",
returnedMessage.getMessage(), returnedMessage.getExchange(),
returnedMessage.getRoutingKey(), returnedMessage.getReplyCode(), returnedMessage.getReplyText());
}
});
}
}

View File

@ -1,22 +0,0 @@
package com.youlai.mall.oms.config.rabbitmq;
/**
* @author huawei
* @desc RabbitMQ 相关常量
* @email huawei_code@163.com
* @date 2021/1/17
*/
public class OmsRabbitConstants {
public static final String ORDER_EVENT_EXCHANGE = "order_event_exchange";
public static final String ORDER_DELAY_QUEUE = "order.delay.queue";
public static final String ORDER_RELEASE_QUEUE = "order.release.queue";
public static final String ORDER_CREATE_ORDER_KEY = "order.create.order";
public static final String ORDER_RELEASE_ORDER_KEY = "order.release.order";
public static final Integer ORDER_RELEASE_TTL = 60000;
}

View File

@ -1,7 +1,10 @@
package com.youlai.mall.oms.common;
public interface RedisConstants {
package com.youlai.mall.oms.constant;
/**
* @author hxr
* @date 2021-03-16
*/
public interface OmsConstants {
Long REDIS_KEY_TIME_OUT = 3600 * 24L;
String CART_KEY = "cart:";
@ -9,4 +12,5 @@ public interface RedisConstants {
String TOKEN_VERIFY = "token_verify:";
String BUSINESS_NO_PREFIX = "businessno:";
}

View File

@ -41,8 +41,9 @@ public class OrderPayController {
@ApiOperation("订单支付")
@PostMapping
@ApiImplicitParams({
@ApiImplicitParam(name = "payType", value = "支付方式", paramType = "query", dataType = "Integer"),
@ApiImplicitParam(name = "orderId", value = "订单ID", paramType = "query", dataType = "Long")
@ApiImplicitParam(name = "orderId", value = "订单ID", paramType = "query", dataType = "Long"),
@ApiImplicitParam(name = "payType", value = "支付方式", paramType = "query", dataType = "Integer")
})
public Result pay(
Integer payType,
@ -50,14 +51,13 @@ public class OrderPayController {
) {
PayTypeEnum payTypeEnum = PayTypeEnum.getValue(payType);
boolean result = false;
switch (payTypeEnum) {
case ALIPAY:
case WEIXIN:
// TODO
break;
case BALANCE:
orderPayService.payWithBalance(orderId);
orderPayService.payWithBalance(orderId);
break;
default:
return Result.failed("系统暂不支持该支付方式~");

View File

@ -0,0 +1,56 @@
package com.youlai.mall.oms.listener;
import com.rabbitmq.client.Channel;
import com.youlai.mall.oms.service.IOrderService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @author hxr
* @date 2021-03-16
*/
@Component
@AllArgsConstructor
@Slf4j
public class RabbitMQListener {
IOrderService orderService;
RabbitTemplate rabbitTemplate;
/**
* 订单超时未支付关闭订单释放库存
* @param orderSn
* @param message
* @param channel
*/
@RabbitListener(queues = "order.close.queue")
public void closeOrder(String orderSn, Message message, Channel channel) {
try {
if (orderService.closeOrder(orderSn)) {
// 如果关单成功发送消息释放库存
rabbitTemplate.convertAndSend("product_event_change", "inventory:unlock", orderSn);
} else {
// 如果关单失败则订单可能已经被处理直接手动ACK确认消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
}
} catch (IOException e) {
// 消费失败后重新入队
try {
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
} catch (IOException ioException) {
log.error("释放库存失败,orderSn=[{}]", orderSn);
}
}
}
}

View File

@ -41,7 +41,7 @@ public interface IOrderService extends IService<OmsOrder> {
*
* @param orderSn 订单号
*/
boolean autoCancelOrder(String orderSn);
boolean closeOrder(String orderSn);
/**
* 取消订单接口

View File

@ -17,7 +17,7 @@ import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import static com.youlai.mall.oms.common.RedisConstants.CART_KEY;
import static com.youlai.mall.oms.constant.OmsConstants.CART_KEY;
/**

View File

@ -1,15 +1,11 @@
package com.youlai.mall.oms.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.common.result.Result;
import com.youlai.common.result.ResultCode;
import com.youlai.common.web.exception.BizException;
import com.youlai.common.web.util.RequestUtils;
import com.youlai.mall.oms.dao.OrderPayDao;
import com.youlai.mall.oms.enums.PayTypeEnum;
import com.youlai.mall.oms.enums.OrderStatusEnum;
import com.youlai.mall.oms.enums.PayTypeEnum;
import com.youlai.mall.oms.pojo.domain.OmsOrder;
import com.youlai.mall.oms.pojo.domain.OmsOrderPay;
import com.youlai.mall.oms.pojo.vo.PayInfoVO;
@ -17,13 +13,11 @@ import com.youlai.mall.oms.service.IOrderLogService;
import com.youlai.mall.oms.service.IOrderPayService;
import com.youlai.mall.oms.service.IOrderService;
import com.youlai.mall.ums.api.app.MemberFeignService;
import com.youlai.mall.ums.pojo.dto.MemberDTO;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Date;
@ -48,7 +42,7 @@ public class OrderPayServiceImpl extends ServiceImpl<OrderPayDao, OmsOrderPay> i
// 2. 远程调用查询会员余额
Long userId = RequestUtils.getUserId();
Long balance = memberFeignService.getUserById(userId).getData().getBalance();
Long balance = memberFeignService.getBalance(userId).getData();
if (Long.compare(balance, order.getPayAmount()) == -1) {
throw new BizException("会员余额不足,请先充值");
@ -81,26 +75,16 @@ public class OrderPayServiceImpl extends ServiceImpl<OrderPayDao, OmsOrderPay> i
@Override
public PayInfoVO getPayInfo(Long orderId) {
Long userId = RequestUtils.getUserId();
PayInfoVO payInfoVO = new PayInfoVO();
// 1获取订单应支付金额
OmsOrder omsOrder = orderService.getByOrderId(orderId);
payInfoVO.setPayAmount(omsOrder.getPayAmount());
// 2获取会员余额
try {
Result<MemberDTO> memberInfo = memberFeignService.getUserById(RequestUtils.getUserId());
if (memberInfo != null && memberInfo.getCode().equals(ResultCode.SUCCESS.getCode())) {
MemberDTO data = memberInfo.getData();
if (data != null) {
payInfoVO.setBalance(data.getBalance());
} else {
log.error("获取会员信息失败,userId={}", userId);
}
}
} catch (Exception e) {
log.error("获取会员余额失败,userId={}", userId, e);
}
Long userId = RequestUtils.getUserId();
Long balance = memberFeignService.getBalance(userId).getData();
payInfoVO.setBalance(balance);
return payInfoVO;
}

View File

@ -1,74 +0,0 @@
package com.youlai.mall.oms.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.rabbitmq.client.Channel;
import com.youlai.common.result.Result;
import com.youlai.common.result.ResultCode;
import com.youlai.common.web.exception.BizException;
import com.youlai.mall.oms.config.rabbitmq.OmsRabbitConstants;
import com.youlai.mall.oms.enums.OrderStatusEnum;
import com.youlai.mall.oms.pojo.domain.OmsOrder;
import com.youlai.mall.oms.pojo.domain.OmsOrderItem;
import com.youlai.mall.oms.service.IOrderItemService;
import com.youlai.mall.oms.service.OrderRabbitService;
import com.youlai.mall.oms.service.IOrderService;
import com.youlai.mall.pms.api.app.InventoryFeignService;
import com.youlai.mall.pms.pojo.dto.InventoryDTO;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RabbitListener(queues = OmsRabbitConstants.ORDER_RELEASE_QUEUE)
@AllArgsConstructor
@Slf4j
public class OrderRabbitServiceImpl implements OrderRabbitService {
private IOrderService orderService;
private IOrderItemService orderItemService;
private InventoryFeignService inventoryFeignService;
/**
* 接收超时订单消息
* 订单已支付 - 确认接收消息
* 订单未支付 - 关闭订单释放库存
* 出现异常消费消息失败将数据重新放入队列等待下次消费
*
* @param orderSn
*/
@Override
@RabbitHandler
@GlobalTransactional(rollbackFor = Exception.class)
public void releaseOrder(String orderSn, Message message, Channel channel) {
long msgTag = message.getMessageProperties().getDeliveryTag();
log.info("获取到消息msgTag={}message={}body={}", msgTag, message.toString(), orderSn);
try {
OmsOrder order = orderService.getOne(new LambdaQueryWrapper<OmsOrder>()
.eq(OmsOrder::getOrderSn, orderSn));
if (order.getStatus().equals(OrderStatusEnum.PENDING_PAYMENT.getCode())) { // 待支付订单超时未支付系统自动取消
if (orderService.autoCancelOrder(orderSn)) {
List<OmsOrderItem> orderItems = orderItemService.getByOrderId(order.getId());
List<InventoryDTO> items = orderItems.stream().map(item -> InventoryDTO.builder()
.skuId(item.getSkuId())
.num(item.getSkuQuantity()).build())
.collect(Collectors.toList());
Result result = inventoryFeignService.unlockInventory(items);
if (!StrUtil.equals(result.getCode(), ResultCode.SUCCESS.getCode())) {
throw new BizException("关闭订单失败,释放库存错误");
}
}
}
channel.basicAck(msgTag, false);
} catch (Exception e) {
throw new BizException("关闭订单失败orderSn=" + orderSn);
}
}
}

View File

@ -15,7 +15,7 @@ import com.youlai.common.result.ResultCode;
import com.youlai.common.web.exception.BizException;
import com.youlai.common.web.util.BeanMapperUtils;
import com.youlai.common.web.util.RequestUtils;
import com.youlai.mall.oms.config.rabbitmq.OmsRabbitConstants;
import com.youlai.mall.oms.constant.OmsConstants;
import com.youlai.mall.oms.dao.OrderDao;
import com.youlai.mall.oms.enums.OrderStatusEnum;
import com.youlai.mall.oms.enums.OrderTypeEnum;
@ -114,13 +114,14 @@ public class OrderServiceImpl extends ServiceImpl<OrderDao, OmsOrder> implements
threadLocal.set(submitInfoDTO);
OrderSubmitInfoDTO submitInfo = threadLocal.get();
log.info("创建订单实体类submit:{}", submitInfo);
log.info("订单提交信息:{}", submitInfo);
OmsOrder order = new OmsOrder();
order.setOrderSn(IdWorker.getTimeId())
.setRemark(submitInfo.getRemark())
.setStatus(OrderStatusEnum.PENDING_PAYMENT.getCode())
.setSourceType(OrderTypeEnum.APP.getCode())
.setUserId(RequestUtils.getUserId());
log.debug("完善后的订单信息:{}", order.toString());
orderBO.setOrder(order);
}, executor);
@ -131,16 +132,21 @@ public class OrderServiceImpl extends ServiceImpl<OrderDao, OmsOrder> implements
orderItemFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(attributes);
threadLocal.set(submitInfoDTO);
log.info("创建订单商品submitInfoDTO:{}", submitInfoDTO);
OrderSubmitInfoDTO submitInfo = threadLocal.get();
log.info("创建订单商品submitInfo:{}", submitInfo);
List<OmsOrderItem> orderItems;
log.info("判断结果,{}", submitInfoDTO.getSkuId() != null);
if (submitInfoDTO.getSkuId() != null) { // 直接下单
log.info("直接下单");
orderItems = new ArrayList<>();
orderItems.add(OmsOrderItem.builder().skuId(submitInfo.getSkuId()).skuQuantity(submitInfo.getSkuNum()).build());
} else { // 购物车下单
log.info("准备获取购物车");
CartVO cart = cartService.getCart();
log.info("购物车:{}", cart.toString());
orderItems = cart.getItems().stream().map(cartItem -> OmsOrderItem.builder().skuId(cartItem.getSkuId())
.skuQuantity(cartItem.getNum())
.build()
@ -150,14 +156,15 @@ public class OrderServiceImpl extends ServiceImpl<OrderDao, OmsOrder> implements
List<Long> skuIds = orderItems.stream().map(item -> item.getSkuId())
.collect(Collectors.toList());
List<SkuDTO> skus = inventoryFeignService.listBySkuIds(skuIds).getData();
if (CollectionUtil.isEmpty(skus)) {
List<SkuDTO> skuList = inventoryFeignService.listBySkuIds(skuIds).getData();
if (CollectionUtil.isEmpty(skuList)) {
throw new BizException("订单商品库存为空");
}
for (OmsOrderItem orderItem : orderItems) {
skus.stream().filter(sku -> sku.getId().equals(orderItem.getSkuId())).findFirst()
skuList.stream().filter(sku -> sku.getId().equals(orderItem.getSkuId())).findFirst()
.ifPresent(skuItem -> {
BeanUtil.copyProperties(skuItem, orderItem);
orderItem.setSkuPrice(skuItem.getPrice());
orderItem.setSkuTotalPrice(orderItem.getSkuPrice() * orderItem.getSkuQuantity());
});
}
@ -172,8 +179,8 @@ public class OrderServiceImpl extends ServiceImpl<OrderDao, OmsOrder> implements
threadLocal.set(submitInfoDTO);
String addressId = threadLocal.get().getAddressId();
log.info("获取订单地址信息addressId{}", addressId);
UmsAddressDTO userAddress = memberFeignService.getAddressById(addressId).getData();
log.debug("获取用户地址:{}", userAddress.toString());
if (userAddress == null) {
throw new BizException("提交订单失败,无法获取用户地址信息");
}
@ -262,11 +269,8 @@ public class OrderServiceImpl extends ServiceImpl<OrderDao, OmsOrder> implements
cartService.deleteSelectedItem();
}
// 将订单放入定时队列中超时未支付系统自动关单释放库存
rabbitTemplate.convertAndSend(OmsRabbitConstants.ORDER_EVENT_EXCHANGE,
OmsRabbitConstants.ORDER_CREATE_ORDER_KEY,
orderBO.getOrder().getOrderSn()
);
// 将订单放入延时队列超时未支付系统自动关单
rabbitTemplate.convertAndSend("order_event_exchange", "order:create", orderBO.getOrder().getOrderSn());
// 保存日志
orderLogService.addOrderLogs(orderId,
@ -283,11 +287,10 @@ public class OrderServiceImpl extends ServiceImpl<OrderDao, OmsOrder> implements
@Override
public boolean autoCancelOrder(String orderSn) {
log.info("订单超时未支付系统自动关闭orderSn={}", orderSn);
public boolean closeOrder(String orderSn) {
OmsOrder order = this.getOne(new LambdaQueryWrapper<OmsOrder>().eq(OmsOrder::getOrderSn, orderSn));
if (!OrderStatusEnum.PENDING_PAYMENT.getCode().equals(order.getStatus())) {
throw new BizException("系统自动取消订单失败");
return false;
}
order.setStatus(OrderStatusEnum.AUTO_CANCEL.getCode());
this.updateById(order);

View File

@ -13,8 +13,8 @@ import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import static com.youlai.mall.oms.common.RedisConstants.REDIS_KEY_TIME_OUT;
import static com.youlai.mall.oms.common.RedisConstants.TOKEN_VERIFY;
import static com.youlai.mall.oms.constant.OmsConstants.REDIS_KEY_TIME_OUT;
import static com.youlai.mall.oms.constant.OmsConstants.TOKEN_VERIFY;
/**

View File

@ -26,13 +26,13 @@ public interface InventoryFeignService {
/**
* 锁定库存
*/
@PatchMapping("/api.app/v1/skus/batch/lock_inventory")
@PutMapping("/api.app/v1/skus/batch/lock_inventory")
Result lockInventory(@RequestBody List<InventoryDTO> list);
/**
* 解锁库存
*/
@PatchMapping("/api.app/v1/skus/batch/unlock_inventory")
@PutMapping("/api.app/v1/skus/batch/unlock_inventory")
Result<Boolean> unlockInventory(@RequestBody List<InventoryDTO> list);

View File

@ -1,8 +1,10 @@
package com.youlai.mall.pms.pojo.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @description 库存数量
@ -11,6 +13,8 @@ import lombok.Data;
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InventoryDTO {
@ApiModelProperty("库存ID")

View File

@ -44,6 +44,11 @@
<artifactId>common-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>common-rabbitmq</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>

View File

@ -4,8 +4,8 @@ package com.youlai.mall.pms.common.constant;
* @author haoxr
* @date 2021-02-28 20:10
*/
public interface RedisConstants {
public interface PmsConstants {
String PRODUCT_INVENTORY_PREFIX = "product:inventory:";
String PRODUCT_INVENTORY_PREFIX = "pms:inventory:";
}

View File

@ -43,7 +43,7 @@ public class SkuController {
@ApiOperation(value = "锁定库存", httpMethod = "PUT")
@ApiImplicitParam(name = "list", value = "锁定库存", required = true, paramType = "body", dataType = "InventoryNumDTO")
@PatchMapping("/batch/_lock")
@PutMapping("/batch/lock_inventory")
public Result<Boolean> lockInventory(@RequestBody List<InventoryDTO> list) {
boolean result = iPmsSkuService.lockInventory(list);
return Result.judge(result);
@ -52,7 +52,7 @@ public class SkuController {
@ApiOperation(value = "解锁库存", httpMethod = "PUT")
@ApiImplicitParam(name = "list", value = "释放库存", required = true, paramType = "body", dataType = "InventoryNumDTO")
@PatchMapping("/batch/_unlock")
@PutMapping("/batch/unlock_inventory")
public Result<Boolean> unlockInventory(@RequestBody List<InventoryDTO> list) {
boolean result = iPmsSkuService.unlockInventory(list);
return Result.judge(result);

View File

@ -0,0 +1,20 @@
package com.youlai.mall.pms.listener;
import org.springframework.stereotype.Component;
/**
* @author hxr
* @date 2021-03-16
*/
@Component
public class InventoryListener {
/**
* 支付成功减库存
*/
public void minusInventory(){
}
}

View File

@ -5,7 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.common.web.exception.BizException;
import com.youlai.mall.pms.common.constant.RedisConstants;
import com.youlai.mall.pms.common.constant.PmsConstants;
import com.youlai.mall.pms.mapper.PmsSkuMapper;
import com.youlai.mall.pms.pojo.domain.PmsSku;
import com.youlai.mall.pms.pojo.dto.SkuDTO;
@ -75,7 +75,7 @@ public class PmsSkuServiceImpl extends ServiceImpl<PmsSkuMapper, PmsSku> impleme
public Integer getInventoryById(Long id) {
Integer inventory = 0;
// ->缓存
Object cacheVal = redisTemplate.opsForValue().get(RedisConstants.PRODUCT_INVENTORY_PREFIX + id);
Object cacheVal = redisTemplate.opsForValue().get(PmsConstants.PRODUCT_INVENTORY_PREFIX + id);
if (cacheVal != null) {
inventory = Convert.toInt(cacheVal);
return inventory;
@ -89,7 +89,7 @@ public class PmsSkuServiceImpl extends ServiceImpl<PmsSkuMapper, PmsSku> impleme
if (pmsSku != null) {
inventory = pmsSku.getInventory();
// ->缓存
redisTemplate.opsForValue().set(RedisConstants.PRODUCT_INVENTORY_PREFIX + id, inventory);
redisTemplate.opsForValue().set(PmsConstants.PRODUCT_INVENTORY_PREFIX + id, inventory);
}
return inventory;

View File

@ -50,6 +50,13 @@ public interface MemberFeignService {
@PutMapping("/api.app/v1/users/{id}/balance")
Result updateBalance(@PathVariable Long id, @RequestParam Long balance);
/**
* 获取会员余额
*/
@GetMapping("/api.app/v1/users/{id}/balance")
Result<Long> getBalance(@PathVariable Long id);
}

View File

@ -108,4 +108,17 @@ public class UserController {
boolean result = iUmsUserService.updateById(user);
return Result.judge(result);
}
@ApiOperation(value = "获取会员余额", httpMethod = "GET")
@ApiImplicitParam(name = "id", value = "会员ID", required = true, paramType = "path", dataType = "Long")
@GetMapping("/{id}/balance")
public Result<Long> updateBalance(@PathVariable Long id) {
Long balance = 0l;
UmsUser user = iUmsUserService.getById(id);
if (user != null) {
balance = user.getBalance();
}
return Result.success(balance);
}
}

View File

@ -15,7 +15,6 @@
<module>youlai-auth</module>
<module>youlai-gateway</module>
<module>youlai-registry</module>
<module>youlai-test</module>
<module>mall-sms</module>
<module>mall-ums</module>
<module>mall-pms</module>

View File

@ -13,7 +13,6 @@ public class FieldFillHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("自动填充时间字段");
this.setFieldValByName("gmtCreate", new Date(), metaObject);
this.setFieldValByName("gmtModified", new Date(), metaObject);
}

View File

@ -5,11 +5,18 @@
<!-- 自定义属性 -->
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="default"/>
<!-- 彩色日志依赖的渲染类 -->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
<conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
<conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
<!-- 彩色日志格式 -->
<property name="LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />
<!-- 输出到控制台-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS}|%level|%logger|%thread|%X{traceId}|%m%n
<Pattern>${LOG_PATTERN}</Pattern>
<charset>UTF-8</charset>
</pattern>
</encoder>
</appender>

View File

@ -1,169 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>youlai-mall</artifactId>
<groupId>com.youlai</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>youlai-test</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>oms-api</artifactId>
<version>${youlai.version}</version>
</dependency>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>pms-api</artifactId>
<version>${youlai.version}</version>
</dependency>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>ums-api</artifactId>
<version>${youlai.version}</version>
</dependency>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>common-core</artifactId>
<version>${youlai.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>common-mybatis</artifactId>
<version>${youlai.version}</version>
</dependency>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>common-web</artifactId>
<version>${youlai.version}</version>
</dependency>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>common-redis</artifactId>
<version>${youlai.version}</version>
</dependency>
<dependency>
<groupId>com.youlai</groupId>
<artifactId>common-rabbitmq</artifactId>
<version>${youlai.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!-- 排除依赖 指定版本和服务器端一致 -->
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.6.RELEASE</version>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign-core</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
</dependencies>
</project>

View File

@ -1,77 +0,0 @@
package com.youlai.test.config;
import com.google.common.collect.Lists;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.OAuthBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
import java.util.ArrayList;
import java.util.List;
/**
* @Author haoxr
* @Date 2021-02-25 15:36
* @Version 1.0.0
*/
@Configuration
@EnableSwagger2WebMvc
@Import(BeanValidatorPluginsConfiguration.class)
public class SwaggerConfiguration {
@Bean
public Docket restApi() {
//schema
List<GrantType> grantTypes=new ArrayList<>();
//密码模式
String passwordTokenUrl="http://localhost:9999/youlai-auth/oauth/token";
ResourceOwnerPasswordCredentialsGrant resourceOwnerPasswordCredentialsGrant=new ResourceOwnerPasswordCredentialsGrant(passwordTokenUrl);
grantTypes.add(resourceOwnerPasswordCredentialsGrant);
OAuth oAuth=new OAuthBuilder().name("oauth2")
.grantTypes(grantTypes).build();
//context
//scope方位
List<AuthorizationScope> scopes=new ArrayList<>();
scopes.add(new AuthorizationScope("read","read resources"));
scopes.add(new AuthorizationScope("write","write resources"));
scopes.add(new AuthorizationScope("reads","read all resources"));
scopes.add(new AuthorizationScope("writes","write all resources"));
SecurityReference securityReference=new SecurityReference("oauth2",scopes.toArray(new AuthorizationScope[]{}));
SecurityContext securityContext=new SecurityContext(Lists.newArrayList(securityReference),PathSelectors.ant("/**"));
//schemas
List<SecurityScheme> securitySchemes=Lists.newArrayList(oAuth);
//securyContext
List<SecurityContext> securityContexts=Lists.newArrayList(securityContext);
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.youlai.mall.oms.controller"))
.paths(PathSelectors.any())
.build()
.securityContexts(securityContexts)
.securitySchemes(securitySchemes)
.apiInfo(apiInfo());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("测试接口")
.description("<div style='font-size:14px;color:red;'>Seata、ElasticSearch、RabbitMQ、Redis测试</div>")
.termsOfServiceUrl("https://www.youlai.store")
.contact(new Contact("yl", "https://github.com/hxrui", "1490493387@qq.com"))
.license("Open Source")
.licenseUrl("https://www.apache.org/licenses/LICENSE-2.0")
.version("1.0.0")
.build();
}
}

View File

@ -1,38 +0,0 @@
package com.youlai.test.seata.controller;
import com.youlai.common.result.Result;
import com.youlai.test.seata.dto.OrderDTO;
import com.youlai.test.seata.service.IOrderService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author haoxr
* @description 订单提交全局事务一致性测试 1.扣減庫存 2. 减账户余额 3.更改订单状态已完成
* @createTime 2021/3/13 11:10
*/
@Api(tags = "【Seata】订单提交")
@RequestMapping("/api.admin/v1/orders")
@Slf4j
@AllArgsConstructor
public class OrderController {
private IOrderService orderService;
@ApiOperation(value = "订单提交", httpMethod = "POST")
@PostMapping
public Result submit(OrderDTO order) {
boolean status;
if (order.getOpenTransaction()) {
status = orderService.saveWithGlobalTransactional(order);
} else {
status = orderService.save(order);
}
return Result.judge(status);
}
}

View File

@ -1,24 +0,0 @@
package com.youlai.test.seata.dto;
import lombok.Data;
/**
* @author haoxr
* @description TODO
* @createTime 2021/3/13 11:31
*/
@Data
public class OrderDTO {
private Long orderId;
private Long userId;
private Long skuId;
private Integer skuNum;
private Long skuPrice;
/**
* 是否开启事务 0-关闭 1-开启
*/
private Boolean openTransaction;
}

View File

@ -1,16 +0,0 @@
package com.youlai.test.seata.service;
import com.youlai.test.seata.dto.OrderDTO;
/**
* @author haoxr
* @description TODO
* @createTime 2021/3/13 11:16
*/
public interface IOrderService {
boolean save(OrderDTO order);
boolean saveWithGlobalTransactional(OrderDTO order);
}

View File

@ -1,61 +0,0 @@
package com.youlai.test.seata.service.impl;
import com.youlai.mall.oms.api.OrderFeignService;
import com.youlai.mall.oms.enums.OrderStatusEnum;
import com.youlai.mall.pms.api.admin.InventoryFeignService;
import com.youlai.mall.ums.api.admin.MemberFeignService;
import com.youlai.test.seata.dto.OrderDTO;
import com.youlai.test.seata.service.IOrderService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* @author haoxr
* @description TODO
* @createTime 2021/3/13 11:16
*/
@Slf4j
@AllArgsConstructor
public class OrderServiceImpl implements IOrderService {
private InventoryFeignService inventoryFeignService;
private MemberFeignService memberFeignService;
private OrderFeignService orderFeignService;
@Override
public boolean save(OrderDTO order) {
log.info("========================扣减库存::Begin======================");
inventoryFeignService.deductInventory(order.getSkuId(), order.getSkuNum());
log.info("========================扣减库存::End======================");
log.info("========================扣减账户余额::Begin======================");
memberFeignService.deductBalance(order.getUserId(), order.getSkuNum() * order.getSkuPrice());
log.info("========================扣减账户余额::End======================");
log.info("========================修改订单状态::Begin======================");
orderFeignService.updateOrderStatus(order.getOrderId(), OrderStatusEnum.FINISHED.getCode());
log.info("========================修改订单状态::End======================");
return true;
}
@Override
public boolean saveWithGlobalTransactional(OrderDTO order) {
log.info("========================扣减库存::Begin======================");
inventoryFeignService.deductInventory(order.getSkuId(), order.getSkuNum());
log.info("========================扣减库存::End======================");
log.info("========================扣减账户余额::Begin======================");
memberFeignService.deductBalance(order.getUserId(), order.getSkuNum() * order.getSkuPrice());
log.info("========================扣减账户余额::End======================");
log.info("========================修改订单状态::Begin======================");
orderFeignService.updateOrderStatus(order.getOrderId(), OrderStatusEnum.FINISHED.getCode());
log.info("========================修改订单状态::End======================");
return true;
}
}

View File

@ -1,14 +0,0 @@
server:
port: 7474
spring:
application:
name: youlai-test
cloud:
nacos:
discovery:
server-addr: http://localhost:8848
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
file-extension: yaml

View File

@ -1,18 +0,0 @@
server:
port: 8603
spring:
application:
name: mall-oms
cloud:
nacos:
discovery:
server-addr: http://c.youlai.store:8848
namespace: prod_namespace_id
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
file-extension: yaml
namespace: prod_namespace_id

View File

@ -1,3 +0,0 @@
spring:
profiles:
active: dev