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.引入依赖
org.springframework.boot spring-boot-starter-amqp
2.配置环境
spring:
rabbitmq:
host: 自己的地址
port: 5672
virtual-host: /
publisher-/confirm/i-type: correlated
publisher-returns: true
template:
mandatory: true
listener:
simple:
acknowledge-mode: manual
3.添加@EnableRabbit注解
二、订单模块的前端整合和前面一样,这里省略
三、整合spring session使登录信息同步
1)、引入session的依赖
org.springframework.boot
spring-boot-starter-data-redis
io.lettuce
lettuce-core
redis.clients
jedis
org.springframework.session
spring-session-data-redis
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("
private List buildOrderItems(String orderSn) {
//最后确定每个购物项的价格
List currentUserCartItems = cartFeignService.getCurrentUserCartItems();
if (currentUserCartItems != null && currentUserCartItems.size() > 0) {
List itemEntities = currentUserCartItems.stream().map(cartItem -> {
OrderItemEntity itemEntity = buildOrderItem(cartItem);
itemEntity.setOrderSn(orderSn);
return itemEntity;
}).collect(Collectors.toList());
return itemEntities;
}
return null;
}
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() {
});
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, ListitemEntities) { 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)、保存订单
private void saveOrder(OrderCreateTo order) {
OrderEntity orderEntity = order.getOrder();
orderEntity.setModifyTime(new Date());
this.save(orderEntity);
List orderItems = order.getOrderItems();
orderItemService.saveBatch(orderItems);
}
6)、锁定库存
@Transactional
@Override
public Boolean orderLockStock(WareSkuLockVo vo) {
WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
taskEntity.setOrderSn(vo.getOrderSn());
orderTaskService.save(taskEntity);
//1、按照下单的收货地址,找到一个就近仓库,锁定库存。
//1、找到每个商品在哪个仓库都有库存
List locks = vo.getLocks();
List collect = locks.stream().map(item -> {
SkuWareHasStock stock = new SkuWareHasStock();
Long skuId = item.getSkuId();
stock.setSkuId(skuId);
stock.setNum(item.getCount());
//查询这个商品在哪里有库存
List 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 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.导入依赖
com.alibaba.cloud spring-cloud-starter-alibaba-seata
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
@Bean
public Queue orderDelayQueue() {
Map arguments = new HashMap<>();
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 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 arguments
return new TopicExchange("order-event-exchange",true,false);
}
@Bean
public Binding orderCreateOrderBingding() {
//String destination, DestinationType destinationType, String exchange, String routingKey,
// Map 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);
}
@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 arguments
return new Queue("order.seckill.order.queue",true,false,false);
}
@Bean
public Binding orderSeckillOrderQueueBinding(){
return new Binding("order.seckill.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.seckill.order",
null);
}
2)、为ware配置MQ
1.引入包
org.springframework.boot spring-boot-starter-amqp
2.配置开始注解
@EnableRabbit
spring:
rabbitmq:
host: 101.43.79.94
port: 5672
virtual-host: /
publisher-/confirm/i-type: correlated
publisher-returns: true
template:
mandatory: true
listener:
simple:
acknowledge-mode: manual
3.配置类
通上面的MyMQconfig
@Configuration
public class MyRabbitConfig {
@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 arguments
return new TopicExchange("stock-event-exchange",true,false);
}
@Bean
public Queue stockReleaseStockQueue(){
//String name, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
return new Queue("stock.release.stock.queue",true,false,false);
}
@Bean
public Queue stockDelayQueue(){
Map 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(){
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.release.#",
null);
}
@Bean
public Binding stockLockedBinding(){
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() {
});
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.引入包
com.alipay.sdk
alipay-sdk-java
4.21.10.ALL
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.修改前端页面,完善支付逻辑
@Controller
public class PayWebController {
@Autowired
AlipayTemplate alipayTemplate;
@Autowired
OrderService orderService;
@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
org.springframework.boot spring-boot-starter-thymeleaforg.springframework.boot spring-boot-starter-data-redisio.lettuce lettuce-coreredis.clients jedisorg.springframework.session spring-session-data-redis
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;
@GetMapping("/memberOrder.html")
public String memberOrderPage(@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum,
Model model, HttpServletRequest request){
//获取到支付宝给我们传来的所有请求数据;
// request。验证签名,如果正确可以去修改。
//查出当前登录的用户的所有订单列表数据
Map 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;
@PostMapping("/payed/notify")
public String handleAlipayed(PayAsyncVo vo,HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {
//只要我们收到了支付宝给我们异步的通知,告诉我们订单支付成功。返回success,支付宝就再也不通知
// Map map = request.getParameterMap();
// for (String key : map.keySet()) {
// String value = request.getParameter(key);
// System.out.println("参数名:"+key+"==>参数值:"+value);
// }
//验签
Map params = new HashMap();
Map requestParams = request.getParameterMap();
for (Iterator 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"}");



