完成购物车页面之后,点击购物车页面的“去结算”按钮,跳转到订单结算页。
欢迎访问加群:1107019965,学习更多的知识
接下来,先搭建订单系统:
pom.xml:
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>com.atguigugroupId>
<artifactId>gmallartifactId>
<version>0.0.1-SNAPSHOTversion>
parent>
<groupId>com.atguigugroupId>
<artifactId>gmall-orderartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>gmall-ordername>
<description>谷粒商城订单系统description>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>com.atguigugroupId>
<artifactId>gmall-commonartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
<dependency>
<groupId>com.atguigugroupId>
<artifactId>gmall-pms-interfaceartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
<dependency>
<groupId>com.atguigugroupId>
<artifactId>gmall-sms-interfaceartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
<dependency>
<groupId>com.atguigugroupId>
<artifactId>gmall-wms-interfaceartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
<dependency>
<groupId>com.atguigugroupId>
<artifactId>gmall-ums-interfaceartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.amqpgroupId>
<artifactId>spring-rabbit-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
bootstrap.yml:
spring:
application:
name: order-service
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
application.yml:
server:
port: 18091
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719
zipkin:
base-url: http://localhost:9411
discovery-client-enabled: false
sender:
type: web
sleuth:
sampler:
probability: 1
redis:
host: 172.16.116.100
rabbitmq:
host: 172.16.116.100
virtual-host: /fengge
username: fengge
password: fengge
listener:
simple:
acknowledge-mode: manual
prefetch: 1
thymeleaf:
cache: false
feign:
sentinel:
enabled: true
logging:
level:
com.atguigu.gmall: debug
启动类:
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
public class GmallOrderApplication {
public static void main(String[] args) {
SpringApplication.run(GmallOrderApplication.class, args);
}
}
gmall-gateway网关配置添加订单路由:
nginx配置:
hosts文件中添加order.gmall.com映射。
参照gmall-cart购物车中的统一验证
LoginInterceptor:
@Component
public class LoginInterceptor implements HandlerInterceptor {
private static final ThreadLocal<UserInfo> THREAD_LOCAL = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
UserInfo userInfo = new UserInfo();
// 获取请求头信息
Long userId = Long.valueOf(request.getHeader("userId"));
userInfo.setUserId(userId);
// 传递给后续业务
THREAD_LOCAL.set(userInfo);
return true;
}
/**
* 封装了一个获取线程局部变量值的静态方法
*
* @return
*/
public static UserInfo getUserInfo() {
return THREAD_LOCAL.get();
}
/**
* 在视图渲染完成之后执行,经常在完成方法中释放资源
*
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 调用删除方法,是必须选项。因为使用的是tomcat线程池,请求结束后,线程不会结束。
// 如果不手动删除线程变量,可能会导致内存泄漏
THREAD_LOCAL.remove();
}
}
WebMvcConfig:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/order/pay/**");
}
}
UserInfo:
@Data
public class UserInfo {
private Long userId;
private String userKey;
}
创建订单也是一个很复杂的业务功能,关系到很多远程调用,这里也可以通过异步调用优化业务。
添加线程池配置类:
@Configuration
public class ThreadPoolConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor(
@Value("${thread.pool.coreSize}")Integer coreSize,
@Value("${thread.pool.maxSize}")Integer maxSize,
@Value("${thread.pool.keepalive}")Integer keepalive,
@Value("${thread.pool.blockQueueSize}")Integer blockQueueSize
){
return new ThreadPoolExecutor(coreSize, maxSize, keepalive, TimeUnit.SECONDS, new ArrayBlockingQueue<>(blockQueueSize));
}
}
在application.yml中添加线程池配置:
thread:
pool:
coreSize: 100
maxSize: 500
keepalive: 60
blockQueueSize: 1000
购物车页面点击去结算按钮,应该发送请求到controller获取结算页需要的数据。
需要获取的数据模型,可以参照jd结算页,如下:
可以发现订单结算页,包含以下信息:
OrderConfirmVO:
@Data
public class OrderConfirmVo {
// 收货地址列表
private List<UserAddressEntity> addresses;
// 送货清单,根据购物车页面传递过来的skuIds查询
private List<OrderItemVo> items;
// 用户的购物积分信息,ums_member表中的integration字段
private Integer bounds;
// 防重的唯一标识
private String orderToken;
}
注意:需要引入gmall-ums-interface依赖,并把UserAddressEntity对象移到gmall-ums-interface工程中去。
OrderItemVO:(参照Cart对象)
@Data
public class OrderItemVo {
private Long skuId;
private String title;
private String defaultImage;
private BigDecimal price;
private Integer count;
private BigDecimal weight;
private List<SkuAttrValueEntity> saleAttrs; // 销售属性
private List<ItemSaleVo> sales; // 营销信息
private Boolean store = false; // 库存信息
}
结合数据模型,需要的远程接口如下:
在gmall-ums中的UserAddressController中添加根据用户id查询收货地址的数据接口:
@GetMapping("user/{userId}")
public ResponseVo<List<UserAddressEntity>> queryAddressesByUserId(@RequestParam("userId")Long userId){
List<UserAddressEntity> addressEntities = this.userAddressService.list(new QueryWrapper<UserAddressEntity>().eq("user_id", userId));
return ResponseVo.ok(addressEntities);
}
在gmall-ums-interface工程中的GmallUmsApi添加feign方法:
/**
* 根据用户id查询收货地址
* @param userId
* @return
*/
@GetMapping("ums/useraddress/user/{userId}")
public ResponseVo<List<UserAddressEntity>> queryAddressesByUserId(@RequestParam("userId")Long userId);
在gmall-ums中的UserController已有根据用户id查询用户信息的方法(用户信息中包含积分信息):
在gmall-ums-interface工程中的GmallUmsApi添加feign方法:
/**
* 根据id查询用户信息
* @param id
* @return
*/
@GetMapping("ums/user/{id}")
public ResponseVo<UserEntity> queryUserById(@PathVariable("id") Long id);
在gmall-carts工程的CartController方法中添加如下方法:
@GetMapping("check/{userId}")
@ResponseBody
public ResponseVo<List<Cart>> queryCheckedCarts(@PathVariable("userId")Long userId){
List<Cart> carts = this.cartService.queryCheckedCarts(userId);
return ResponseVo.ok(carts);
}
在CartService中添加如下方法:
public List<Cart> queryCheckedCarts(Long userId) {
String key = KEY_PREFIX + userId;
BoundHashOperations<String, Object, Object> hashOps = this.redisTemplate.boundHashOps(key);
List<Object> cartJsons = hashOps.values();
if (CollectionUtils.isEmpty(cartJsons)) {
return null;
}
return cartJsons.stream().map(cartJson -> JSON.parseObject(cartJson.toString(), Cart.class)).filter(cart -> cart.getCheck()).collect(Collectors.toList());
}
创建gmall-cart-interface工程,并添加gmallCartApi接口及Cart实体类:
public interface GmallCartApi {
@GetMapping("check/{userId}")
public ResponseVo<List<Cart>> queryCheckedCarts(@PathVariable("userId")Long userId);
}
在gmall-order工程中添加feign接口:
@FeignClient("cart-service")
public interface GmallCartClient extends GmallCartApi {
}
@FeignClient("pms-service")
public interface GmallPmsClient extends GmallPmsApi {
}
@FeignClient("sms-service")
public interface GmallSmsClient extends GmallSmsApi {
}
@FeignClient("ums-service")
public interface GmallUmsClient extends GmallUmsApi {
}
@FeignClient("wms-service")
public interface GmallWmsClient extends GmallWmsApi {
}
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("confirm")
public ResponseVo<OrderConfirmVo> confirm(){
OrderConfirmVo confirmVo = this.orderService.confirm();
return ResponseVo.ok(confirmVo);
}
}
OrderService:
@Service
public class OrderService {
@Autowired
private GmallPmsClient pmsClient;
@Autowired
private GmallSmsClient smsClient;
@Autowired
private GmallUmsClient umsClient;
@Autowired
private GmallCartClient cartClient;
@Autowired
private GmallWmsClient wmsClient;
@Autowired
private StringRedisTemplate redisTemplate;
private static final String KEY_PREFIX = "order:token:";
@Autowired
private ThreadPoolExecutor threadPoolExecutor;
/**
* 订单确认页
* 由于存在大量的远程调用,这里使用异步编排做优化
* @return
*/
public OrderConfirmVo confirm() {
OrderConfirmVo confirmVo = new OrderConfirmVo();
UserInfo userInfo = LoginInterceptor.getUserInfo();
Long userId = userInfo.getUserId();
// 查询送货清单
CompletableFuture<List<Cart>> cartCompletableFuture = CompletableFuture.supplyAsync(() -> {
ResponseVo<List<Cart>> listResponseVo = this.cartClient.queryCheckedCarts(userId);
List<Cart> carts = listResponseVo.getData();
if (CollectionUtils.isEmpty(carts)) {
throw new OrderException("没有选中的购物车信息!");
}
return carts;
}, threadPoolExecutor);
CompletableFuture<Void> itemCompletableFuture = cartCompletableFuture.thenAcceptAsync(carts -> {
List<OrderItemVo> items = carts.stream().map(cart -> {
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setSkuId(cart.getSkuId());
orderItemVo.setCount(cart.getCount().intValue());
// 根据skuId查询sku
CompletableFuture<Void> skuCompletableFuture = CompletableFuture.runAsync(() -> {
ResponseVo<SkuEntity> skuEntityResponseVo = this.pmsClient.querySkuById(cart.getSkuId());
SkuEntity skuEntity = skuEntityResponseVo.getData();
orderItemVo.setTitle(skuEntity.getTitle());
orderItemVo.setPrice(skuEntity.getPrice());
orderItemVo.setDefaultImage(skuEntity.getDefaultImage());
orderItemVo.setWeight(new BigDecimal(skuEntity.getWeight()));
}, threadPoolExecutor);
// 查询销售属性
CompletableFuture<Void> saleAttrCompletableFuture = CompletableFuture.runAsync(() -> {
ResponseVo<List<SkuAttrValueEntity>> skuAttrValueResponseVo = this.pmsClient.querySkuAttrValuesBySkuId(cart.getSkuId());
List<SkuAttrValueEntity> skuAttrValueEntities = skuAttrValueResponseVo.getData();
orderItemVo.setSaleAttrs(skuAttrValueEntities);
}, threadPoolExecutor);
// 根据skuId查询营销信息
CompletableFuture<Void> saleCompletableFuture = CompletableFuture.runAsync(() -> {
ResponseVo<List<ItemSaleVo>> itemSaleVoResponseVo = this.smsClient.querySalesBySkuId(cart.getSkuId());
List<ItemSaleVo> itemSaleVos = itemSaleVoResponseVo.getData();
orderItemVo.setSales(itemSaleVos);
}, threadPoolExecutor);
// 根据 skuId查询库存信息
CompletableFuture<Void> storeCompletableFuture = CompletableFuture.runAsync(() -> {
ResponseVo<List<WareSkuEntity>> wareSkuResponseVo = this.wmsClient.queryWareSkusBySkuId(cart.getSkuId());
List<WareSkuEntity> wareSkuEntities = wareSkuResponseVo.getData();
if (!CollectionUtils.isEmpty(wareSkuEntities)) {
orderItemVo.setStore(wareSkuEntities.stream().anyMatch(wareSkuEntity -> wareSkuEntity.getStock() - wareSkuEntity.getStockLocked() > 0));
}
}, threadPoolExecutor);
CompletableFuture.allOf(skuCompletableFuture, saleAttrCompletableFuture, saleCompletableFuture, storeCompletableFuture).join();
return orderItemVo;
}).collect(Collectors.toList());
confirmVo.setItems(items);
}, threadPoolExecutor);
// 查询收货地址列表
CompletableFuture<Void> addressCompletableFuture = CompletableFuture.runAsync(() -> {
ResponseVo<List<UserAddressEntity>> addressesResponseVo = this.umsClient.queryAddressesByUserId(userId);
List<UserAddressEntity> addresses = addressesResponseVo.getData();
confirmVo.setAddresses(addresses);
}, threadPoolExecutor);
// 查询用户的积分信息
CompletableFuture<Void> boundsCompletableFuture = CompletableFuture.runAsync(() -> {
ResponseVo<UserEntity> userEntityResponseVo = this.umsClient.queryUserById(userId);
UserEntity userEntity = userEntityResponseVo.getData();
if (userEntity != null) {
confirmVo.setBounds(userEntity.getIntegration());
}
}, threadPoolExecutor);
// 防重的唯一标识
CompletableFuture<Void> tokenCompletableFuture = CompletableFuture.runAsync(() -> {
String timeId = IdWorker.getTimeId();
this.redisTemplate.opsForValue().set(KEY_PREFIX + timeId, timeId);
confirmVo.setOrderToken(timeId);
}, threadPoolExecutor);
CompletableFuture.allOf(itemCompletableFuture, addressCompletableFuture, boundsCompletableFuture, tokenCompletableFuture).join();
return confirmVo;
}
}
访问:http://order.gmall.com/confirm
响应数据:
{
"code": 0,
"msg": null,
"data": {
"addresses": [
{
"id": 1,
"userId": 2,
"name": "柳岩",
"phone": "13812345678",
"postCode": "200122",
"province": "上海",
"city": "上海市",
"region": "松江区",
"address": "大江商厦6层",
"defaultStatus": 1
},
{
"id": 2,
"userId": 2,
"name": "锋哥",
"phone": "18612345678",
"postCode": "200112",
"province": "北京",
"city": "北京市",
"region": "昌平区",
"address": "宏福科技园",
"defaultStatus": 0
}
],
"orderItems": [
{
"skuId": 5,
"title": "华为 HUAWEI Mate 30 5G 麒麟990 4000万超感光徕卡影像双超级快充8GB+128GB亮黑色5G全网通游戏手机",
"defaultImage": "https://ggmall.oss-cn-shanghai.aliyuncs.com/2020-03-21/a3a0a224-caad-4af2-8eec-f62c0e49a51f_e9ad9735fc3f0686.jpg",
"price": 5000.0000,
"count": 3,
"skuAttrValue": [
{
"id": 13,
"skuId": 5,
"attrId": 3,
"attrName": "机身颜色",
"attrValue": "黑色",
"sort": 0
},
{
"id": 14,
"skuId": 5,
"attrId": 4,
"attrName": "运行内存",
"attrValue": "8G",
"sort": 0
},
{
"id": 15,
"skuId": 5,
"attrId": 5,
"attrName": "机身存储",
"attrValue": "128G",
"sort": 0
}
],
"itemSaleVo": [
{
"type": "积分",
"desc": "成长积分赠送1000.0000,购物积分赠送1000.0000"
},
{
"type": "满减",
"desc": "满5000.0000减100.0000"
},
{
"type": "打折",
"desc": "满2件打0.90折"
}
],
"weight": 500,
"store": null
},
{
"skuId": 6,
"title": "华为 HUAWEI Mate 30 5G 麒麟990 4000万超感光徕卡影像双超级快充8GB+256GB亮黑色5G全网通游戏手机",
"defaultImage": "https://ggmall.oss-cn-shanghai.aliyuncs.com/2020-03-21/f053e068-e43d-46e7-8d67-4964abb240eb_802254cca298ae79 (1).jpg",
"price": 6000.0000,
"count": 2,
"skuAttrValue": [
{
"id": 16,
"skuId": 6,
"attrId": 3,
"attrName": "机身颜色",
"attrValue": "黑色",
"sort": 0
},
{
"id": 17,
"skuId": 6,
"attrId": 4,
"attrName": "运行内存",
"attrValue": "8G",
"sort": 0
},
{
"id": 18,
"skuId": 6,
"attrId": 5,
"attrName": "机身存储",
"attrValue": "256G",
"sort": 0
}
],
"itemSaleVo": [
{
"type": "积分",
"desc": "成长积分赠送2000.0000,购物积分赠送2000.0000"
},
{
"type": "满减",
"desc": "满6000.0000减1500.0000"
},
{
"type": "打折",
"desc": "满2件打8.00折"
}
],
"weight": 510,
"store": null
}
],
"bounds": 2000,
"orderToken": "202004122044380421249317500785655809"
}
}
改造controller方法跳转到trade.html页面:
@GetMapping("confirm")
public String confirm(Model model){
OrderConfirmVo confirmVo = this.orderService.confirm();
model.addAttribute("confirmVo", confirmVo);
return "trade";
}
trade.html总体结构:
包含
主内容结构如下:
页面模板:
<div class="step-cont">
<div class="addressInfo">
<ul class="addr-detail">
<li class="addr-item">
<div class="choose-address" v-for="address in addresses" :key="address.id">
<div class="con name" :class="address.selected ? 'selected' : ''"><a href="javascript:;" @click="selectAddress(address)"><em>{{address.name}}em><span
title="点击取消选择">span>a>div>
<div class="con address">
<span class="place">{{address.province}}span>
<span class="place">{{address.city}}span>
<span class="place">{{address.region}}span>
<span class="place">{{address.address}}span>
<span class="phone">{{address.phone}}span>
<span class="base" v-if="address.defaultStatus == 1">默认地址span>
div>
<div class="clearfix">div>
div>
li>
ul>
div>
div>
vuejs处理:
let app = new Vue({
el: "#app",
data: {
addresses: [[${confirmVo.addresses}]], // 收获地址列表
order: { // 收集订单提交信息
orderToken: [[${confirmVo.orderToken}]], // 订单编号
address: {}, // 用户选中的收货地址
bounds: [[${confirmVo.bounds}]], // 积分信息
items: [[${confirmVo.items}]] // 送货清单
}
},
created(){
// 处理默认地址选中状态
let addresses = this.addresses
addresses.forEach(address => {
address.selected = false
if (address.defaultStatus) {
address.selected = true
this.order.address = address
}
})
this.addresses = addresses
},
methods: {
selectAddress(address){ // 选择地址方法
this.addresses.forEach(address => { // 重置所有地址的选中状态
address.selected = false
})
address.selected = true
this.order.address = address
}
}
})
html模板如下:
<div class="step-cont">
<ul class="payType">
<li :class="order.payType == 1 ? 'selected' : ''" typeValue="1" @click="order.payType=1">在线支付<span title="点击取消选择">span>li>
<li :class="order.payType == 2 ? 'selected' : ''" typeValue="0" @click="order.payType=2">货到付款<span title="点击取消选择">span>li>
<input type="hidden" id="payType" value="1">
ul>
div>
vuejs数据模型如下:
<div class="step-cont">
<ul class="send-detail">
<li>
<div class="sendType">
<span>配送方式:span>
<ul>
<li>
<div class="con express">{{order.deliveryCompany}}div>
<div class="con delivery">配送时间:预计8月10日(周三)09:00-15:00送达div>
li>
ul>
div>
<div class="sendGoods">
<span>商品清单:span>
<ul class="yui3-g" v-for="item in order.items" :key="item.skuId">
<li class="yui3-u-1-6">
<span><img :src="item.defaultImage" width="150px" height="200px"/>span>
li>
<li class="yui3-u-7-12">
<div class="desc">
<span>{{item.title}}span><br>
<span v-for="saleAttr in item.saleAttrs">{{saleAttr.attrName}}:{{saleAttr.attrValue}} span>
div>
<div class="seven">7天无理由退货div>
li>
<li class="yui3-u-1-12">
<div class="price">¥{{item.price.toFixed(2)}}div>
li>
<li class="yui3-u-1-12">
<div class="num">X{{item.count}}div>
li>
<li class="yui3-u-1-12">
<div class="exit" v-text="item.store ? '有货' : '无货'">有货div>
li>
ul>
div>
<div class="buyMessage">
<span>买家留言:span>
<textarea placeholder="建议留言前先与商家沟通确认" class="remarks-cont">textarea>
div>
li>
ul>
div>
vuejs数据模型处理:
html模板内容:
<div class="cardInfo">
<div class="step-tit">
<h5>使用优惠/抵用h5>
div>
<div class="step-cont">
<span>购买积分:{{order.bounds}}span>
div>
div>
对应vue数据模型:
html模板如下:
<div class="order-summary">
<div class="static fr">
<div class="list">
<span><i class="number">{{total}}i>件商品,总商品金额span>
<em class="allprice">¥{{order.totalPrice.toFixed(2)}}em>
div>
<div class="list">
<span>返现:span>
<em class="money">{{returnMoney.toFixed(2)}}em>
div>
<div class="list">
<span>运费:span>
<em class="transport">{{postFee.toFixed(2)}}em>
div>
div>
div>
金额都保留了两位小数。
totalPrice总价格:在created方法中进行了计算
total总件数:在computed计算属性中进行了计算
html模板:
<div class="clearfix trade">
<div class="fc-price">应付金额: <span class="price">¥{{payAmount.toFixed(2)}}span>div>
<div class="fc-receiverInfo">
寄送至:
<span id="receive-address">{{order.address.city}}{{order.address.region}}{{order.address.address}}span>
收货人:<span id="receive-name">{{order.address.name}}span>
<span id="receive-phone">{{order.address.phone}}span>
div>
div>
vuejs对应的数据模型:
收货地址在收件人信息
已经处理过了,这里可以直接使用。这里还有个应付金额
通过计算属性完成计算:
vuejs中提供了提交订单的方法:
submitOrder(){
axios.post('http://order.gmall.com/submit', this.order).then(({data}) => {
if (data.code === 0) {
window.location = 'http://payment.gmall.com/pay.html?orderToken=' + data.data
} else {
alert(data.data)
}
}).catch(({response}) => { // 提交订单失败,弹出错误信息
alert(response.data.message);
})
}
当用户点击提交订单按钮,应该收集页面数据提交到后台并生成订单数据。
请求路径:http://order.gmall.com/submit
需要把该路径添加到网关配置中,进行登录拦截
订单确认页,需要提交的数据:
@Data
public class OrderSubmitVO {
private String orderToken; // 防重
private BigDecimal totalPrice; // 总价,校验价格变化
private UserAddressEntity address; // 收货人信息
private Integer payType; // 支付方式
private String deliveryCompany; // 配送方式
private List<OrderItemVO> items; // 订单详情信息
private Integer bounds; // 积分信息
// 发票信息TODO
// 营销信息TODO
}
为了保证验库存和锁库存的原子性,这里直接把验证和锁定库存封装到一个方法中,并在方法中使用分布式锁,防止多人同时锁库存。
gmall-wms工程的pom.xml中引入redisson的依赖:
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.11.2version>
dependency>
gmall-wms工程的config目录下需要添加redisson的配置文件,参照gmall-index工程:
给gmall-wms-interface工程添加实体类:
@Data
public class SkuLockVO {
private Long skuId; // 锁定的商品id
private Integer count; // 购买的数量
private Boolean lock; // 锁定状态
private Long wareSkuId; // 锁定成功时,锁定的仓库id
private String orderToken; // 方便以订单为单位缓存订单的锁定信息
}
gmall-wms工程的WareSkuController添加方法:
@PostMapping("check/lock")
public ResponseVo<List<SkuLockVO>> checkAndLock(@RequestBody List<SkuLockVO> lockVOS){
List<SkuLockVO> skuLockVOS = this.wareSkuService.checkAndLock(lockVOS);
return ResponseVo.ok(skuLockVOS);
}
在添加WareSkuService的接口方法:
List<SkuLockVO> checkAndLock(List<SkuLockVO> lockVOS);
在WareSkuServiceImpl实现类中添加方法:
@Autowired
private WareSkuMapper wareSkuMapper;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RedissonClient redissonClient;
private static final String KEY_PREFIX = "store:lock:";
@Transactional
@Override
public List<SkuLockVO> checkAndLock(List<SkuLockVO> lockVOS) {
if (CollectionUtils.isEmpty(lockVOS)) {
return null;
}
lockVOS.forEach(lockVO -> {
// 每一个商品验库存并锁库存
this.checkLock(lockVO);
});
// 如果有一个商品锁定失败了,所有已经成功锁定的商品要解库存
List<SkuLockVO> successLockVO = lockVOS.stream().filter(SkuLockVO::getLock).collect(Collectors.toList());
List<SkuLockVO> errorLockVO = lockVOS.stream().filter(skuLockVO -> !skuLockVO.getLock()).collect(Collectors.toList());
if (!CollectionUtils.isEmpty(errorLockVO)) {
successLockVO.forEach(lockVO -> {
this.wareSkuMapper.unlockStock(lockVO.getWareSkuId(), lockVO.getCount());
});
return lockVOS;
}
// 把库存的锁定信息保存到redis中,以方便将来解锁库存
String orderToken = lockVOS.get(0).getOrderToken();
this.redisTemplate.opsForValue().set(KEY_PREFIX + orderToken, JSON.toJSONString(lockVOS));
return null; // 如果都锁定成功,不需要展示锁定情况
}
private void checkLock(SkuLockVO skuLockVO){
RLock fairLock = this.redissonClient.getFairLock("lock:" + skuLockVO.getSkuId());
fairLock.lock();
// 验库存
List<WareSkuEntity> wareSkuEntities = this.wareSkuMapper.checkStock(skuLockVO.getSkuId(), skuLockVO.getCount());
if (CollectionUtils.isEmpty(wareSkuEntities)) {
skuLockVO.setLock(false); // 库存不足,锁定失败
fairLock.unlock(); // 程序返回之前,一定要释放锁
return ;
}
// 锁库存。一般会根据运输距离,就近调配。这里就锁定第一个仓库的库存
if(this.wareSkuMapper.lockStock(wareSkuEntities.get(0).getId(), skuLockVO.getCount()) == 1){
skuLockVO.setLock(true); // 锁定成功
skuLockVO.setWareSkuId(wareSkuEntities.get(0).getId());
} else {
skuLockVO.setLock(false);
}
fairLock.unlock();
}
在WareSkuMapper中添加方法:
@Mapper
public interface WareSkuMapper extends BaseMapper<WareSkuEntity> {
// 验库存方法
List<WareSkuEntity> checkStock(@Param("skuId") Long skuId, @Param("count") Integer count);
// 锁库存
int lockStock(@Param("id") Long id, @Param("count") Integer count);
// 解库存
int unlockStock(@Param("id") Long id, @Param("count") Integer count);
}
在WareSkuMapper对应的xml中添加映射:
<mapper namespace="com.atguigu.gmall.wms.mapper.WareSkuMapper">
<select id="checkStock" resultMap="WareSkuEntity">
select * from wms_ware_sku where stock-stock_locked>=#{count} and sku_id=#{skuId}
select>
<update id="lockStock">
update wms_ware_sku set stock_locked = stock_locked + #{count} where id = #{id}
update>
<update id="unlockStock">
update wms_ware_sku set stock_locked = stock_locked - #{count} where id = #{id}
update>
mapper>
在gmall-wms-interface中的GmallWmsApi中添加接口方法
@PostMapping("wms/waresku/check/lock")
public ResponseVo<List<SkuLockVO>> checkAndLock(@RequestBody List<SkuLockVO> lockVOS);
把OrderSubmitVO和OrderItemVO数据模型移动到gmall-oms-interface中
添加api接口:
public interface GmallOmsApi {
@PostMapping("oms/order/{userId}")
public ResponseVo<OrderEntity> saveOrder(@RequestBody OrderSubmitVO orderSubmitVO, @PathVariable("userId")Long userId);
}
该api接口对应的实现如下:
给OrderController添加如下方法:
@PostMapping("{userId}")
public ResponseVo<OrderEntity> saveOrder(@RequestBody OrderSubmitVO orderSubmitVO, @PathVariable("userId")Long userId){
OrderEntity orderEntity = this.orderService.saveOrder(orderSubmitVO, userId);
return ResponseVo.ok(orderEntity);
}
给OrderService接口添加如下方法:
OrderEntity saveOrder(OrderSubmitVO orderSubmitVO, Long userId);
给OrderServiceImpl实现类添加如下方法:
@Autowired
private OrderItemMapper orderItemMapper;
@Transactional
@Override
public OrderEntity saveOrder(OrderSubmitVO orderSubmitVO, Long userId) {
// 保存订单
OrderEntity orderEntity = new OrderEntity();
BeanUtils.copyProperties(orderSubmitVO, orderEntity);
orderEntity.setOrderSn(orderSubmitVO.getOrderToken());
orderEntity.setUserId(userId);
orderEntity.setCreateTime(new Date());
orderEntity.setTotalAmount(orderSubmitVO.getTotalPrice());
orderEntity.setPayAmount(orderSubmitVO.getTotalPrice());
orderEntity.setPayType(orderSubmitVO.getPayType());
orderEntity.setStatus(0);
orderEntity.setDeliveryCompany(orderSubmitVO.getDeliveryCompany());
this.save(orderEntity);
// 保存订单详情
List<OrderItemVO> orderItems = orderSubmitVO.getItems();
for (OrderItemVO orderItem : orderItems) {
OrderItemEntity itemEntity = new OrderItemEntity();
// 订单信息
itemEntity.setOrderId(orderEntity.getId());
itemEntity.setOrderSn(orderEntity.getOrderSn());
// 需要远程查询spu信息 TODO
// 设置sku信息
itemEntity.setSkuId(orderItem.getSkuId());
itemEntity.setSkuName(orderItem.getTitle());
itemEntity.setSkuPrice(orderItem.getPrice());
itemEntity.setSkuQuantity(orderItem.getCount().intValue());
//需要远程查询优惠信息 TODO
this.orderItemMapper.insert(itemEntity);
}
return orderEntity;
}
提交订单分以下几个基本步骤:
OrderController添加方法:
/**
* 提交订单返回订单id
* @param orderSubmitVO
* @return
*/
@PostMapping("submit")
@ResponseBody
public ResponseVo<Object> submit(@RequestBody OrderSubmitVo submitVo){
OrderEntity orderEntity = this.orderService.submit(submitVo);
return ResponseVo.ok(orderEntity.getOrderSn());
}
OrderService中添加方法:
@Autowired
private GmallOmsClient omsClient;
@Autowired
private RabbitTemplate rabbitTemplate;
public OrderEntity submit(OrderSubmitVO submitVO) {
// 1.防重
String orderToken = submitVO.getOrderToken();
String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('del', KEYS[1]) " +
"else return 0 end";
Boolean flag = this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(KEY_PREFIX + orderToken), orderToken);
if (!flag) {
throw new OrderException("您多次提交过快,请稍后再试!");
}
// 2.验价
BigDecimal totalPrice = submitVO.getTotalPrice(); // 获取页面上的价格
List<OrderItemVO> items = submitVO.getItems(); // 订单详情
if (CollectionUtils.isEmpty(items)) {
throw new OrderException("您没有选中的商品,请选择要购买的商品!");
}
// 遍历订单详情,获取数据库价格,计算实时总价
BigDecimal currentTotalPrice = items.stream().map(item -> {
ResponseVo<SkuEntity> skuEntityResp = this.pmsClient.querySkuById(item.getSkuId());
SkuEntity skuEntity = skuEntityResp.getData();
if (skuEntity != null) {
return skuEntity.getPrice().multiply(item.getCount());
}
return new BigDecimal(0);
}).reduce((t1, t2) -> t1.add(t2)).get();
if (totalPrice.compareTo(currentTotalPrice) != 0) {
throw new OrderException("页面已过期,刷新后再试!");
}
// 3.验库存并锁库存
List<SkuLockVO> lockVOS = items.stream().map(item -> {
SkuLockVO skuLockVO = new SkuLockVO();
skuLockVO.setSkuId(item.getSkuId());
skuLockVO.setCount(item.getCount().intValue());
skuLockVO.setOrderToken(submitVO.getOrderToken());
return skuLockVO;
}).collect(Collectors.toList());
ResponseVo<List<SkuLockVO>> skuLockResp = this.wmsClient.checkAndLock(lockVOS);
List<SkuLockVO> skuLockVOS = skuLockResp.getData();
if (!CollectionUtils.isEmpty(skuLockVOS)){
throw new OrderException("手慢了,商品库存不足:" + JSON.toJSONString(skuLockVOS));
}
// order:此时服务器宕机
// 4.下单
UserInfo userInfo = LoginInterceptor.getUserInfo();
Long userId = userInfo.getUserId();
OrderEntity orderEntity = null;
try {
ResponseVo<OrderEntity> orderEntityResp = this.omsClient.saveOrder(submitVO, userId);// feign(请求,响应)超时
orderEntity = orderEntityResp.getData();
} catch (Exception e) {
e.printStackTrace();
// 如果订单创建失败,立马释放库存
this.rabbitTemplate.convertAndSend("ORDER-EXCHANGE", "stock.unlock", orderToken);
}
// 5.删除购物车。异步发送消息给购物车,删除购物车
Map<String, Object> map = new HashMap<>();
map.put("userId", userId);
List<Long> skuIds = items.stream().map(OrderItemVO::getSkuId).collect(Collectors.toList());
map.put("skuIds", JSON.toJSONString(skuIds));
this.rabbitTemplate.convertAndSend("ORDER-EXCHANGE", "cart.delete", map);
return orderEntity;
}
引入rabbitmq依赖及配置rabbitmq的链接信息略。。。。
@Component
public class CartListener {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private GmallPmsClient pmsClient;
private static final String KEY_PREFIX = "cart:info:";
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "order-cart-queue", durable = "true"),
exchange = @Exchange(value = "order-exchange", ignoreDeclarationExceptions = "true", type = ExchangeTypes.TOPIC),
key = {"cart.delete"}
))
public void deleteCart(Map<String, Object> map, Channel channel, Message message) throws IOException {
try {
Long userId = (Long)map.get("userId");
String skuIdString = map.get("skuIds").toString();
List<String> skuIds = JSON.parseArray(skuIdString, String.class);
BoundHashOperations<String, Object, Object> hashOps = this.redisTemplate.boundHashOps(KEY_PREFIX + userId);
hashOps.delete(skuIds.toArray());
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
e.printStackTrace();
if (message.getMessageProperties().getRedelivered()){
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
} else {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}
}
@Component
public class StockListener {
@Autowired
private WareSkuMapper wareSkuMapper;
@Autowired
private StringRedisTemplate redisTemplate;
private static final String KEY_PREFIX = "stock:lock:";
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "order-stock-queue", durable = "true"),
exchange = @Exchange(value = "order-exchange", ignoreDeclarationExceptions = "true", type = ExchangeTypes.TOPIC),
key = {"stock.unlock"}
))
public void listener(String orderToken, Channel channel, Message message) throws IOException {
try {
// 获取redis中该订单的锁定库存信息
String json = this.redisTemplate.opsForValue().get(KEY_PREFIX + orderToken);
if (StringUtils.isNotBlank(json)){
// 反序列化获取库存的锁定信息
List<SkuLockVo> skuLockVos = JSON.parseArray(json, SkuLockVo.class);
// 遍历并解锁库存信息
skuLockVos.forEach(skuLockVo -> {
this.wareSkuMapper.unlock(skuLockVo.getWareSkuId(), skuLockVo.getCount());
});
// 删除redis中库存锁定信息
this.redisTemplate.delete(KEY_PREFIX + orderToken);
}
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
e.printStackTrace();
if (message.getMessageProperties().getRedelivered()){
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
} else {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}
}
如果用户下单后一直不支付,库存处于锁定状态,陷入店家商品卖不出,买家无法购买的情况。所以,需要定时关单。
常用解决方案:
缺点:消耗系统内存,增加了数据库的压力,存在较大的时间误差
声明延时队列的方式,使用如下参数:
arguments.put("x-dead-letter-exchange", "dlx名称");
arguments.put("x-dead-letter-routing-key", "routingkey");
arguments.put("x-message-ttl", "过期时间");
使用延时队列完成定时关单的流程如下:
为了防止在gmall-oms中订单创建成功,而gmall-order中获取响应时网络故障,或删除购物车时失败导致的关单消息发送失败,我们应该在gmall-oms创建订单的方法中发送消息,和订单创建使用一个本地事务,要么都成功要么都失败。
在gmall-oms工程中添加rabbitmq的依赖并添加rabbitmq的配置, 略。。。
在gmall-oms工程的config目录下添加rabbitmq的配置类:
@Configuration
public class RabbitMqConfig {
// 声明延时交换机:order-exchange
// 声明延时队列
@Bean
public Queue ttlQueue(){
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-message-ttl", 60000);
arguments.put("x-dead-letter-exchange", "order-exchange");
arguments.put("x-dead-letter-routing-key", "order.dead");
return new Queue("order-ttl-queue", true, false, false, arguments);
}
// 把延时队列绑定到交换机
@Bean
public Binding ttlBinding(){
return new Binding("order-ttl-queue", Binding.DestinationType.QUEUE, "order-exchange", "order.create", null);
}
// 声明死信交换机:order-exchange
// 声明死信队列
@Bean
public Queue deadQueue(){
return new Queue("order-dead-queue", true, false, false);
}
// 把死信队列绑定到死信交换机
@Bean
public Binding binding(){
return new Binding("order-dead-queue", Binding.DestinationType.QUEUE, "order-exchange", "order.dead", null);
}
}
1.发送延时消息
在订单创建成功后,发送消息到延时队列。在gmall-oms工程中的OrderServiceImpl实现类保存订单的最后:
2.监听器处理消息
@Component
public class OrderListener {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RabbitTemplate rabbitTemplate;
@RabbitListener(queues = {"order-dead-queue"})
public void closeOrder(String orderToken, Channel channel, Message message) throws IOException {
try {
// 更新订单状态,更新为4
// 执行关单操作,如果返回值是1,说明执行关单成功,再去执行解锁库存的操作;如果返回值是0,是说明执行关单失败
if(this.orderMapper.closeOrder(orderToken) == 1){
// 解锁库存
this.rabbitTemplate.convertAndSend("order-exchange", "stock.unlock", orderToken);
}
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e){
if (message.getMessageProperties().getRedelivered()){
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
} else {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}
}
给OrderMapper接口添加接口方法:
@Mapper
public interface OrderMapper extends BaseMapper<OrderEntity> {
public int closeOrder(String orderToken);
}
给OrderMapper.xml添加映射:
<mapper namespace="com.atguigu.gmall.oms.mapper.OrderMapper">
<update id="closeOrder">
update oms_order set `status`=4 where order_sn=#{orderToken} and `status`=0
update>
mapper>
在下单的5个步骤中:如果验库存并锁库存成功,还没来得及执行下单操作,服务器就宕机了,怎么办?
此时,库存已经被锁住,而下单操作还没有执行,这部分锁定的库存无法解锁。所以库存也需要像订单一样定时解锁。
锁定成功要定时解锁库存,在gmall-wms工程WareSkuServiceImpl的checkAndLock验库存并锁库存方法中,库存锁定成功,在返回之前发送一个延时消息。
在gmall-wms工程中添加RabbitMqConfig的配置类,配置延时队列及死信队列
@Configuration
public class RabbitMqConfig {
// 声明延时交换机:借用order-exchange
// 声明延时队列
@Bean
public Queue ttlQueue(){
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-message-ttl", 90000);
arguments.put("x-dead-letter-exchange", "order-exchange");
arguments.put("x-dead-letter-routing-key", "stock.unlock");
return new Queue("stock-ttl-queue", true, false, false, arguments);
}
// 把延时队列绑定到交换机
@Bean
public Binding ttlBinding(){
return new Binding("stock-ttl-queue", Binding.DestinationType.QUEUE, "order-exchange", "stock.ttl", null);
}
// 声明死信交换机:借用order-exchange
// 声明死信队列:借用order-stock-queue
// 把死信队列绑定到死信交换机:注解中已绑定
}
解锁库存的监听器在4.3.3章节中
已实现
asicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}
}
给OrderMapper接口添加接口方法:
```java
@Mapper
public interface OrderMapper extends BaseMapper {
public int closeOrder(String orderToken);
}
给OrderMapper.xml添加映射:
<mapper namespace="com.atguigu.gmall.oms.mapper.OrderMapper">
<update id="closeOrder">
update oms_order set `status`=4 where order_sn=#{orderToken} and `status`=0
update>
mapper>
在下单的5个步骤中:如果验库存并锁库存成功,还没来得及执行下单操作,服务器就宕机了,怎么办?
此时,库存已经被锁住,而下单操作还没有执行,这部分锁定的库存无法解锁。所以库存也需要像订单一样定时解锁。
锁定成功要定时解锁库存,在gmall-wms工程WareSkuServiceImpl的checkAndLock验库存并锁库存方法中,库存锁定成功,在返回之前发送一个延时消息。
在gmall-wms工程中添加RabbitMqConfig的配置类,配置延时队列及死信队列
@Configuration
public class RabbitMqConfig {
// 声明延时交换机:借用order-exchange
// 声明延时队列
@Bean
public Queue ttlQueue(){
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-message-ttl", 90000);
arguments.put("x-dead-letter-exchange", "order-exchange");
arguments.put("x-dead-letter-routing-key", "stock.unlock");
return new Queue("stock-ttl-queue", true, false, false, arguments);
}
// 把延时队列绑定到交换机
@Bean
public Binding ttlBinding(){
return new Binding("stock-ttl-queue", Binding.DestinationType.QUEUE, "order-exchange", "stock.ttl", null);
}
// 声明死信交换机:借用order-exchange
// 声明死信队列:借用order-stock-queue
// 把死信队列绑定到死信交换机:注解中已绑定
}
解锁库存的监听器在4.3.3章节中
已实现