环境搭建
1.整理前端页面以及静态资源环境
2.配置域名
3.nginx配置 静态资源去 /usr/share/nginx/html里找,动态资源去gulimall,并且由于nginx会丢失host配置Host
[root@wuyimin conf.d]# cat gulimall.conf
server {
listen 80;
server_name gulimall.com *.gulimall.com;
location /static {
root /usr/share/nginx/html;
}
#charset koi8-r;
#access_log /var/log/nginx/log/host.access.log main;
location / {
proxy_pass http://gulimall;
proxy_set_header Host $host;
}
}
这里主要配置upstream(上游), 配置代理服务器为网关模块
[root@wuyimin conf]# cat nginx.conf
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
upstream gulimall{
server 192.168.10.100:88;
}
include /etc/nginx/conf.d/*.conf;
}
4.网关配置
5.themeleaf模板引擎依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
关闭缓存
6.测试页面
@Controller
public class HelloController {
@GetMapping("/{page}.html")
public String listPage(@PathVariable("page")String page){
return page;
}
}
整合SpringSession
1.导入依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
2.配置文件
3.配置类
package com.wuyimin.gulimall.order.config;
/**
* @ Author wuyimin
* @ Date 2021/8/23-14:50
* @ Description
*/
@Configuration
public class RedisSessionConfig {
@Bean // redis的json序列化
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
@Bean // cookie
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("GULISESSIONID"); // cookie的键
serializer.setDomainName("gulimall.com"); // 扩大session作用域,也就是cookie的有效域
return serializer;
}
}
4.线程池配置
配置类
package com.wuyimin.gulimall.order.config;
/**
* @ Author wuyimin
* @ Date 2021/8/20-18:39
* @ Description
*/
@Configuration
public class MyThreadConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool){
return new ThreadPoolExecutor(pool.getCoreSize(),pool.getMaxSize(),pool.getKeepAliveTime(), TimeUnit.SECONDS,
new LinkedBlockingDeque<>(100000), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
}
package com.wuyimin.gulimall.order.config;
/**
* @ Author wuyimin
* @ Date 2021/8/20-18:42
* @ Description
*/
@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {
private Integer coreSize;
private Integer maxSize;
private Integer keepAliveTime;
}
线程池配置文件
#配置线程池
gulimall:
thread:
core-size: 20
max-size: 200
keep-alive-time: 10
5.RabbitMQ配置--》见上一篇博客
6.配置redis
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置文件
spring:
redis:
host: 192.168.116.128
7.启用redisSession
package com.wuyimin.gulimall.order;
@EnableDiscoveryClient
@SpringBootApplication
@EnableRabbit//开启RabbitMQ
@EnableRedisHttpSession
public class GulimallOrderApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallOrderApplication.class, args);
}
}
订单的基本概念
电商系统设计信息流,资金流,物流,订单系统作为中枢将三者有机结合起来
订单应该包含的信息:用户信息,订单信息,商品信息,促销信息,支付信息,物流信息
订单的状态应该包含:
- 待付款状态:(对库存进行锁定,配置支付超时时间,超时后自动取消订单)
- 已付款状态/待发货:(联动仓库进行调拨,配货。。)
- 待收货/已发货:(同步物流信息)
- 已完成:(用户确认收货,订单交易完成)
- 已取消:(用户在付款之前都可以取消订单)
- 售后中:(用户在付款后申请退款,或者商家发货之后会用户申请退换货)
- 售后也同时存在多种状态:发起售后后,订单状态为待审核,商家审核完毕后,跟新到待退货,等待用户寄回,商家收货后订单状态更新为待退款,退款到用户账户之后订单状态更新为售后成功
订单登录拦截
拦截器配置类--添加一个拦截器
package com.wuyimin.gulimall.order.config;
/**
* @ Author wuyimin
* @ Date 2021/8/26-10:53
* @ Description
*/
@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {
@Autowired
LoginUserInterceptor loginUserInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");//拦截所有请求
}
}
拦截器--拦截未登录的用户
package com.wuyimin.gulimall.order.interceptor;
/**
* @ Author wuyimin
* @ Date 2021/8/26-10:52
* @ Description 拦截未登录用户
*/
@Component //放入容器中
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser=new ThreadLocal<>();//方便其他请求拿到
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();//获取session
MemberRespVo attribute = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if(attribute!=null){
//已经登录
loginUser.set(attribute);
return true;
}else{
//给前端用户的提示
request.getSession().setAttribute("msg","请先进行登录");
//未登录
response.sendRedirect("http://auth.gulimall.com/login.html");//重定向到登录页
return false;
}
}
}
前置知识
听说看完这篇就可以和面试官扯cookie,session和token了
Cookie:
Session:
1)浏览器端第一次发送请求到服务器端,服务器端创建一个Session,同时会创建一个特殊的Cookie(name为JSESSIONID的固定值,value为session对象的ID),然后将该Cookie发送至浏览器端
(2)浏览器端发送第N(N>1)次请求到服务器端,浏览器端访问服务器端时就会携带该name为JSESSIONID的Cookie对象
(3)服务器端根据name为JSESSIONID的Cookie的value(sessionId),去查询Session对象,从而区分不同用户。
name为JSESSIONID的Cookie不存在(关闭或更换浏览器),返回1中重新去创建Session与特殊的Cookie
name为JSESSIONID的Cookie存在,根据value中的SessionId去寻找session对象
value为SessionId不存在**(Session对象默认存活30分钟)**,返回1中重新去创建Session与特殊的Cookie
value为SessionId存在,返回session对象
Cookie和Session的区别:
(1)cookie数据存放在客户的浏览器上,session数据放在服务器上
(2)cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,如果主要考虑到安全应当使用session
(3)session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,如果主要考虑到减轻服务器性能方面,应当使用COOKIE
(4)单个cookie在客户端的限制是3K,就是说一个站点在客户端存放的COOKIE不能3K。
(5)所以:将登陆信息等重要信息存放为SESSION;其他信息如果需要保留,可以放在COOKIE中
重定向与转发的区别:原文链接2
地址栏
转发:不变,不会显示出转向的地址
重定向:会显示转向之后的地址
请求
重定向:至少提交了两次请求
数据
转发:对request对象的信息不会丢失,因此可以在多个页面交互过程中实现请求数据的共享
重定向:request信息将丢失
原理
转发(服务器行为):是在服务器内部控制权的转移,是由服务器区请求,客户端并不知道是怎样转移的,因此客户端浏览器的地址不会显示出转向的地址。
重定向(浏览器/客户端行为):是服务器告诉了客户端要转向哪个地址,客户端再自己去请求转向的地址,因此会显示转向后的地址,也可以理解浏览器至少进行了两次的访问请求。
订单确认页模型抽取
package com.wuyimin.gulimall.order.vo;
/**
* @ Author wuyimin
* @ Date 2021/8/26-11:47
* @ Description 订单确认页需要的数据
*/
public class OrderConfirmVo { // 跳转到确认页时需要携带的数据模型。
@Getter
@Setter
/** 会员收获地址列表 **/
private List<MemberAddressVo> memberAddressVos;
@Getter @Setter
/** 所有选中的购物项 **/
private List<OrderItemVo> items;
/** 发票记录 **/
@Getter @Setter
/** 优惠券(会员积分) **/
private Integer integration;
/** 防止重复提交的令牌 **/
@Getter @Setter
private String orderToken;
@Getter @Setter
Map<Long,Boolean> stocks;
public Integer getCount() { // 总件数
Integer count = 0;
if (items != null && items.size() > 0) {
for (OrderItemVo item : items) {
count += item.getCount();
}
}
return count;
}
/** 计算订单总额**/
//BigDecimal total;
public BigDecimal getTotal() {
BigDecimal totalNum = BigDecimal.ZERO;
if (items != null && items.size() > 0) {
for (OrderItemVo item : items) {
//计算当前商品的总价格
BigDecimal itemPrice = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
//再计算全部商品的总价格
totalNum = totalNum.add(itemPrice);
}
}
return totalNum;
}
/** 应付价格 **/
//BigDecimal payPrice;
public BigDecimal getPayPrice() {
return getTotal();
}
}
package com.wuyimin.gulimall.order.vo;
/**
* @ Author wuyimin
* @ Date 2021/8/26-11:48
* @ Description 会员收货地址
*/
@Data
public class MemberAddressVo {
private Long id;
/**
* member_id
*/
private Long memberId;
/**
* 收货人姓名
*/
private String name;
/**
* 电话
*/
private String phone;
/**
* 邮政编码
*/
private String postCode;
/**
* 省份/直辖市
*/
private String province;
/**
* 城市
*/
private String city;
/**
* 区
*/
private String region;
/**
* 详细地址(街道)
*/
private String detailAddress;
/**
* 省市区代码
*/
private String areacode;
/**
* 是否默认
*/
private Integer defaultStatus;
}
package com.wuyimin.gulimall.order.vo;
@Data
public class OrderItemVo {
private Long skuId;
private String title;
private String image;
private List<String> skuAttr;//套餐信息
private BigDecimal price;
private Integer count;
private BigDecimal totalPrice;
}
controller
package com.wuyimin.gulimall.order.web;
/**
* @ Author wuyimin
* @ Date 2021/8/26-10:46
* @ Description
*/
@Controller
public class OrderWebController {
@Autowired
OrderService orderService;
//跳到订单确认列
@GetMapping("/toTrade")
public String toTrade(Model model){
OrderConfirmVo confirmVo=orderService.confirmOrder();
model.addAttribute("orderConfirmData",confirmVo);
return "confirm";
}
}
订单确认页数据获取
1.获取用户地址(远程调用)记得在order主类上开启远程调用注解
远程调用的member的controller, 这里偷懒懒得写进impl里了
package com.wuyimin.gulimall.member.controller;
@GetMapping("/{memberId}/address")
public List<MemberReceiveAddressEntity> getAddress(@PathVariable("memberId") Long id){
List<MemberReceiveAddressEntity> list = memberReceiveAddressService.list(new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id", id));
return list;
}
远程调用接口
package com.wuyimin.gulimall.order.feign;
/**
* @ Author wuyimin
* @ Date 2021/8/26-12:06
* @ Description
*/
@FeignClient("gulimall-member")
public interface MemberFeignService {
@GetMapping("/member/memberreceiveaddress/{memberId}/address")
List<MemberAddressVo> getAddress(@PathVariable("memberId") Long id);
}
2.获取购物项(远程调用)
package com.wuyimin.gulimall.order.feign;
/**
* @ Author wuyimin
* @ Date 2021/8/26-13:03
* @ Description
*/
@FeignClient("gulimall-cart")
public interface CartFeignService {
@GetMapping("/currentUserCartItems")
List<OrderItemVo> getCurrentUserCartItems();
}
由于这里类上我们没有写restController注解,所以在方法上写ResponseBody来传递json
package com.wuyimin.gulimall.cart.controller;
@GetMapping("/currentUserCartItems")
@ResponseBody
public List<CartItem> getCurrentUserCartItems(){
return cartService.getUserCartItems();
}
package com.wuyimin.gulimall.cart.service.impl;
@Override
public List<CartItem> getUserCartItems() {
//获取用户信息
UserInfoVo userInfoVo = CartInterceptor.threadLocal.get();
if(userInfoVo.getUserId()==null){
return null;
}else{
String loginKey = CartConstant.CART_PREFIX + userInfoVo.getUserId();
List<CartItem> cartItems = getCartItems(loginKey);
//过滤掉没有勾选的信息
List<CartItem> cartItemList = cartItems.stream().filter(i -> i.getCheck()).collect(Collectors.toList());
//所有勾选的项目的id远程调用所需
List<Long> skuIds = cartItemList.stream().map(CartItem::getSkuId).collect(Collectors.toList());
//远程调用product服务,查询最新的商品信息
HashMap<Long, BigDecimal> newPrices = productFeignService.getNewPrices(skuIds);
for (CartItem cartItem : cartItemList) {
if(newPrices.containsKey(cartItem.getSkuId())){
cartItem.setPrice(newPrices.get(cartItem.getSkuId()));
}
}
return cartItems;
}
}
在获取购物项的时候我们需要获取最新的价格信息,这里cart模块又需要远程调用product模块
package com.wuyimin.gulimall.cart.feign;
/**
* @ Author wuyimin
* @ Date 2021/8/24-13:15
* @ Description
*/
@FeignClient("gulimall-product")
public interface ProductFeignService {
@RequestMapping("/product/skuinfo/info/{skuId}")
R info(@PathVariable("skuId") Long skuId);
@GetMapping("/product/skusaleattrvalue/stringlist/{skuId}")
List<String> getSkuAttrValues(@PathVariable("skuId") Long skuId);
@RequestMapping("/product/skuinfo/getNewPrices")
R getNewPrices(@RequestBody Long[] ids);
}
package com.wuyimin.gulimall.product.app;
//获得最新的价格
@RequestMapping("/getNewPrices")
public R getNewPrices(@RequestBody Long[] ids){
List<SkuInfoEntity> skuInfoEntities = skuInfoService.list(new QueryWrapper<SkuInfoEntity>().in("sku_id", ids));
HashMap<Long, BigDecimal> map = new HashMap<>();
for (SkuInfoEntity skuInfoEntity : skuInfoEntities) {
map.put(skuInfoEntity.getSkuId(),skuInfoEntity.getPrice());
}
return R.ok().put("data",map);
}
总方法confirmOrder
package com.wuyimin.gulimall.order.service.impl;
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {
@Override
public OrderConfirmVo confirmOrder() {
OrderConfirmVo confirmVo = new OrderConfirmVo();
//从拦截器里获得用户信息
MemberRespVo loginUser = LoginUserInterceptor.loginUser.get();
//远程查询用户地址信息
List<MemberAddressVo> address = memberFeignService.getAddress(loginUser.getId());
confirmVo.setMemberAddressVos(address);
//远程查询用户的购物车
List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(currentUserCartItems);
//查询用户积分
Integer integration = loginUser.getIntegration();
confirmVo.setIntegration(integration);
//其他数据自动计算
return confirmVo;
}
}
Feign远程调用丢失请求头的问题
feign
远程调用的请求头中没有含有JSESSIONID
的cookie
,所以也就不能得到服务端的session
数据,也就没有用户数据,cart认为没登录,获取不了用户信息
我们追踪远程调用的源码,可以在SynchronousMethodHandler.targetRequest()方法中看到他会遍历容器中的RequestInterceptor
进行封装
Request targetRequest(RequestTemplate template) {
for (RequestInterceptor interceptor : requestInterceptors) {
interceptor.apply(template);
}
return target.apply(template);
}
根据追踪源码,我们可以知道我们可以通过给容器中注入RequestInterceptor,从而给远程调用转发时带上cookie
但是在feign的调用过程中,会使用容器中的RequestInterceptor对RequestTemplate进行处理,因此我们可以通过向容器中导入定制的RequestInterceptor为请求加上cookie。
package com.wuyimin.gulimall.order.config;
/**
* @ Author wuyimin
* @ Date 2021/8/26-16:55
* @ Description
*/
@Configuration
public class OrderFeignConfig {
//这个拦截器方法会在远程调用之前触发
@Bean
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
//源码里这个方法就是从threadLocal里拿
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();//这里获取到的是老请求
//同步请求头数据--同步cookie
template.header("Cookie",request.getHeader("Cookie"));//Feign创建的新请求
}
};
}
}
Feign异步调用丢失请求头的问题
在我们增加了异步操作以后,请求头又丢失了
因为异步编排的原因,他会丢掉ThreadLocal中原来线程的数据,从而获取不到loginUser,这种情况下我们可以在方法内的局部变量中先保存原来线程的信息,在异步编排的新线程中拿着局部变量的值重新设置到新线程中即可。
由于RequestContextHolder使用ThreadLocal共享数据,所以在开启异步时获取不到老请求的信息,自然也就无法共享cookie了
修改配置类代码,增加判断不为空的条件
package com.wuyimin.gulimall.order.config;
/**
* @ Author wuyimin
* @ Date 2021/8/26-16:55
* @ Description
*/
@Configuration
public class OrderFeignConfig {
//这个拦截器方法会在远程调用之前触发
@Bean
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
//源码里这个方法就是从threadLocal里拿
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if(requestAttributes!=null){
HttpServletRequest request = requestAttributes.getRequest();//这里获取到的是老请求
if(request!=null){
//同步请求头数据--同步cookie
template.header("Cookie",request.getHeader("Cookie"));//Feign创建的新请求
}
}
}
};
}
}
增加主方法异步和设置threadLocal
package com.wuyimin.gulimall.order.service.impl;
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
//从拦截器里获得用户信息
MemberRespVo loginUser = LoginUserInterceptor.loginUser.get();
//主线程里先把threadLocal的值取出来,因为下面异步就取不到了
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> futureAddress = CompletableFuture.runAsync(() -> {
//远程查询用户地址信息
//子线程里拿到父线程的threadLocal里储存的信息
RequestContextHolder.setRequestAttributes(requestAttributes);
List<MemberAddressVo> address = memberFeignService.getAddress(loginUser.getId());
confirmVo.setMemberAddressVos(address);
}, executor);
CompletableFuture<Void> futureItems = CompletableFuture.runAsync(() -> {
//远程查询用户的购物车
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(currentUserCartItems);
}, executor);
//查询用户积分
Integer integration = loginUser.getIntegration();
confirmVo.setIntegration(integration);
//其他数据自动计算
//等待异步任务完成
CompletableFuture.allOf(futureAddress,futureItems).get();
return confirmVo;
}
Bug调试-FeignException$MethodNotAllowed
修改后的代码
package com.wuyimin.gulimall.product.app;
//获得最新的价格
@RequestMapping("/getNewPrices")
public R getNewPrices(@RequestBody Long[] ids){
List<SkuInfoEntity> skuInfoEntities = skuInfoService.list(new QueryWrapper<SkuInfoEntity>().in("sku_id", ids));
HashMap<Long, BigDecimal> map = new HashMap<>();
for (SkuInfoEntity skuInfoEntity : skuInfoEntities) {
map.put(skuInfoEntity.getSkuId(),skuInfoEntity.getPrice());
}
return R.ok().put("data",map);
}
1.SpringCloud中微服务之间的调用,传递参数时需要加相应的注解。用到的主要是三个注解@RequestBody,@RequestParam(),@PathVariable()
2.get和post请求中对于传递单个引用类型的参数,比如String,Integer....用@RequestParam(),括号中一定要有值(参数的别名)。调用方需要加注解,被调用方不需要加。当然加上也不会出错。被调用方的参数名和调用方的别名保持一致即可。
3.post请求中对于javaBean,map,list类型的参数的传递,用@RequestBody,调用方不需要加注解,被调用方加注解即可。
注:get请求中使用@RequestBody会出错,同时也不能传递javaBean,map,list类型的参数
当我们返回json数据的时候必须要加上注解responseBody和restController注解
库存查询:
远程调用接口:
package com.wuyimin.gulimall.order.feign;
/**
* @ Author wuyimin
* @ Date 2021/8/26-20:11
* @ Description
*/
@FeignClient("gulimall-ware")
public interface WareFeignService {
@PostMapping("/ware/waresku/hasstock")
R getSkuHasStock(@RequestBody List<Long> skuIds);//运用之前已经写过的方法,这个方法会返回有库存的商品列表
}
修改confirmOrder方法
package com.wuyimin.gulimall.order.service.impl;
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo loginUser = LoginUserInterceptor.loginUser.get();
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> futureAddress = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
List<MemberAddressVo> address = memberFeignService.getAddress(loginUser.getId());
confirmVo.setMemberAddressVos(address);
}, executor);
CompletableFuture<Void> futureItems = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(currentUserCartItems);
}, executor).thenRunAsync(()->{
//获取所有的库存信息
List<OrderItemVo> items = confirmVo.getItems();
List<Long> collect=items.stream().map(i->i.getSkuId()).collect(Collectors.toList());
R r = wareFeignService.getSkuHasStock(collect);
List<SkuStockVo> data = r.getData(new TypeReference<List<SkuStockVo>>() {
});
if(data!=null){
Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(map);
}
},executor);
Integer integration = loginUser.getIntegration();
confirmVo.setIntegration(integration);
CompletableFuture.allOf(futureAddress,futureItems).get();
return confirmVo;
}
模拟运费
package com.wuyimin.gulimall.ware.controller;
@GetMapping("/fare")
public R getFare(@RequestParam("addrId")Long addrId){
FareVo fare =wareInfoService.getFare(addrId);
return R.ok().put("data",fare);
}
getFare方法
package com.wuyimin.gulimall.ware.service.impl;
//根据用户的收货地址计算运费
@Override
public FareVo getFare(Long addrId) {
FareVo fareVo = new FareVo();
R info = memberFeignService.info(addrId);
MemberAddressVo addressData = info.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {
});
if(addressData!=null){
//直接拿手机号的最后一位信息当运费信息得了
String phone = addressData.getPhone();
String fare = phone.substring(phone.length() - 1, phone.length());
fareVo.setAddressVo(addressData);
fareVo.setFare(new BigDecimal(fare));
return fareVo;
}
return null;
}
在查询运费的时候我们需要拿到用户的地址信息(远程调用会员服务)
package com.wuyimin.gulimall.ware.feign;
/**
* @ Author wuyimin
* @ Date 2021/8/27-9:58
* @ Description
*/
@FeignClient("gulimall-member")
public interface MemberFeignService {
@RequestMapping("/member/memberreceiveaddress/info/{id}")
R info(@PathVariable("id") Long id);
}
抽取的vo,地址对应运费
package com.wuyimin.gulimall.ware.vo;
@Data
public class FareVo {
private MemberAddressVo addressVo;
private BigDecimal fare;
}
接口幂等性
用户对于同一操作发起的一次或者多次请求结果应该是一致的
数据库层面--添加数据库字段unique限制订单号成唯一约束
业务层面
token机制-验证码,只有验证码核对通过才可以发送请求
令牌什么时候删除:
业务结束完以后删除:不能保证幂等性,如果两个请求很快进来了,那么都能创建订单
业务结束前删除令牌:前端带来一个token,如果相同就直接删除令牌,再调用业务逻辑,如果redis里的数据没来的及删,也不能导致幂等性,所以获取令牌,对比和删除操作必须是一个原子操作,使用lua脚本可以完成此操作
数据库的悲观锁和乐观锁:
悲观锁:select * from xxx where id=1 for update 这里的id必须是主键或者唯一索引,悲观锁使用的时候一般伴随事务,数据锁定的时间可能会很长,需要一句实际情况使用
乐观锁:update goods set count=count-1,version=version+1 where good_id=2 and version=1根据version版本,也就是操作库存前先获取商品的version号,操作的时候带有此version号,乐观锁适合放在更新场景中使用,处理读多写少的问题
防重表:使用订单号作为去重表的唯一索引,插入去重表后再进行业务操作,且保证去重表和业务表操作在同一个事务,同一个数据库中。
全局唯一请求id:调用接口的时候,生成唯一一个id,redis将数据保存到集合中(去重),存在即使处理过,可以通过nginx设置每个请求的唯一id
解决订单提交的幂等性
添加防重令牌
package com.wuyimin.gulimall.order.service.impl;
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
//从拦截器里获得用户信息
MemberRespVo loginUser = LoginUserInterceptor.loginUser.get();
//主线程里先把threadLocal的值取出来,因为下面异步就取不到了
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> futureAddress = CompletableFuture.runAsync(() -> {
//远程查询用户地址信息
//子线程里拿到父线程的threadLocal里储存的信息
RequestContextHolder.setRequestAttributes(requestAttributes);
List<MemberAddressVo> address = memberFeignService.getAddress(loginUser.getId());
confirmVo.setMemberAddressVos(address);
}, executor);
CompletableFuture<Void> futureItems = CompletableFuture.runAsync(() -> {
//远程查询用户的购物车
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(currentUserCartItems);
}, executor).thenRunAsync(()->{
//获取所有的库存信息
List<OrderItemVo> items = confirmVo.getItems();
List<Long> collect=items.stream().map(i->i.getSkuId()).collect(Collectors.toList());
R r = wareFeignService.getSkuHasStock(collect);
List<SkuStockVo> data = r.getData(new TypeReference<List<SkuStockVo>>() {
});
if(data!=null){
Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(map);
}
},executor);
//查询用户积分
Integer integration = loginUser.getIntegration();
confirmVo.setIntegration(integration);
//其他数据自动计算
//防重令牌
String token= UUID.randomUUID().toString().replace("-","");
redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+loginUser.getId(),token,30, TimeUnit.MINUTES);
confirmVo.setOrderToken(token);
//等待异步任务完成
CompletableFuture.allOf(futureAddress,futureItems).get();
return confirmVo;
}
}
抽取提交订单的vo
package com.wuyimin.gulimall.order.vo;
/**
* @ Author wuyimin
* @ Date 2021/8/27-12:03
* @ Description
*/
@Data
@ToString
public class OrderSubmitVo {
/** 收获地址的id **/
private Long addrId;
/** 支付方式 **/
private Integer payType;
//无需提交要购买的商品,去购物车再获取一遍
//优惠、发票
/** 防重令牌 **/
private String orderToken;
/** 应付价格 **/
private BigDecimal payPrice;
/** 订单备注 **/
private String remarks;
//用户相关的信息,直接去session中取出即可
}
成功后转发至支付页面携带的数据Vo
package com.wuyimin.gulimall.order.vo;
/**
* @ Author wuyimin
* @ Date 2021/8/27-12:08
* @ Description
*/
@Data
public class SubmitOrderResponseVo {
// 该实体为order表的映射
private OrderEntity order;
/** 错误状态码 **/
private Integer code;
}
订单创建后应该返回的vo
package com.wuyimin.gulimall.order.vo;
/**
* @ Author wuyimin
* @ Date 2021/8/27-15:09
* @ Description 订单创建成功后需要返回的数据
*/
@Data
public class OrderCreateVo {
private OrderEntity order;
private List<OrderItemEntity> orderItems;
/** 订单计算的应付价格 **/
private BigDecimal payPrice;
/** 运费 **/
private BigDecimal fare;
}
下单功能
下单功能对应的controller
package com.wuyimin.gulimall.order.web;
@PostMapping("/submitOrder")
public String submitOrder(OrderSubmitVo submitVo, Model model, RedirectAttributes redirectAttributes){
try {
// 去OrderServiceImpl服务里验证和下单
SubmitOrderResponseVo responseVo = orderService.submitOrder(submitVo);
// 下单失败回到订单重新确认订单信息
if(responseVo.getCode() == 0){
// 下单成功去支付响应
model.addAttribute("submitOrderResp", responseVo);
// 支付页
return "pay";
}else{
String msg = "下单失败";
switch (responseVo.getCode()){
case 1: msg += "订单信息过期,请刷新在提交";break;
case 2: msg += "订单商品价格发送变化,请确认后再次提交";break;
case 3: msg += "商品库存不足";break;
}
redirectAttributes.addFlashAttribute("msg", msg);
// 重定向
return "redirect:http://order.gulimall.com/toTrade";
}
} catch (Exception e) {
if (e instanceof NoStockException){
String message = e.getMessage();
redirectAttributes.addFlashAttribute("msg", message);
}
return "redirect:http://order.gulimall.com/toTrade";
}
}
submitOrder主方法
该方法的结构如下
package com.wuyimin.gulimall.order.service.impl;
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo orderSubmitVo) {
threadLocal.set(orderSubmitVo);
SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
//下单:去创建订单,检验令牌,检验价格,锁定库存
//1.验证令牌
String orderToken = orderSubmitVo.getOrderToken();//页面传递过来的值
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
String key = OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId();//redis里存的key
//lua脚本保证原子性 返回1代表删除成功,0代表删除失败
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long res = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(key), orderToken);
if(res==1){
//2.验证成功--下单创建订单,检验令牌,检验价格,锁库存
OrderCreateVo order = createOrder();
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = orderSubmitVo.getPayPrice();
if(Math.abs(payAmount.subtract(payPrice).doubleValue())<0.01){
//3.金额对比成功
//保存信息
saveOrder(order);
//锁定库存,只要有异常就回滚订单数据
WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
wareSkuLockVo.setOrderSn(order.getOrder().getOrderSn());
List<OrderItemVo> locks=order.getOrderItems().stream().map(item->{
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setSkuId(item.getSkuId());
orderItemVo.setCount(item.getSkuQuantity());
orderItemVo.setTitle(item.getSkuName());
return orderItemVo;
}).collect(Collectors.toList());
wareSkuLockVo.setLocks(locks);
//远程锁库存操作
R r = wareFeignService.orderLockStock(wareSkuLockVo);
if(r.getCode()==0){
//锁定成功了
responseVo.setOrder(order.getOrder());
responseVo.setCode(0);
return responseVo;
}else{
//锁定失败了
responseVo.setCode(3);
throw new NoStockException();
}
}else{
responseVo.setCode(2);
return responseVo;
}
}else{
//验证失败
return responseVo;
}
}
原子验证使用到的lua脚本
if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end
java中对其的使用,第一个参数指定返回类型和脚本,第二个参数指定Key[1]传入一个集合,第三个参数指定对比的值
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long res = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(key), orderToken);
订单状态枚举类
package com.wuyimin.gulimall.order.enume;
public enum OrderStatusEnum {
CREATE_NEW(0,"待付款"),
PAYED(1,"已付款"),
SENDED(2,"已发货"),
RECIEVED(3,"已完成"),
CANCLED(4,"已取消"),
SERVICING(5,"售后中"),
SERVICED(6,"售后完成");
private Integer code;
private String msg;
OrderStatusEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
接下来是对这个大方法里的一些方法的介绍
1.createOrder创建订单方法
//创建订单
private OrderCreateVo createOrder(){
String timeId = IdWorker.getTimeId();
OrderCreateVo orderCreateVo = new OrderCreateVo();
//1.生成订单号
OrderEntity orderEntity = buildOrder(timeId);
orderCreateVo.setOrder(orderEntity);
//2.获取到所有的订单项
List<OrderItemEntity> orderItemEntities = buildOrderItems(timeId);
orderCreateVo.setOrderItems(orderItemEntities);
//3.计算价格相关
computerPrice(orderEntity,orderItemEntities);
return orderCreateVo;
}
buildOrder方法
private OrderEntity buildOrder(String timeId) {
//创建订单号
OrderEntity orderEntity=new OrderEntity();
orderEntity.setOrderSn(timeId);
//获取收货地址信息
OrderSubmitVo orderSubmitVo = threadLocal.get();
//远程调用
R fare = wareFeignService.getFare(orderSubmitVo.getAddrId());
FareVo fareVo = fare.getData(new TypeReference<FareVo>() {
});
orderEntity.setFreightAmount(fareVo.getFare());
orderEntity.setReceiverCity(fareVo.getAddressVo().getCity());
orderEntity.setReceiverDetailAddress(fareVo.getAddressVo().getDetailAddress());
orderEntity.setReceiverName(fareVo.getAddressVo().getName());
orderEntity.setBillReceiverPhone(fareVo.getAddressVo().getPhone());
orderEntity.setReceiverProvince(fareVo.getAddressVo().getProvince());
orderEntity.setMemberId(fareVo.getAddressVo().getMemberId());
orderEntity.setMemberUsername(fareVo.getAddressVo().getName());
return orderEntity;
}
远程调用的getFare接口,这个接口是之前写的
package com.wuyimin.gulimall.ware.controller;
@GetMapping("/fare")
public R getFare(@RequestParam("addrId")Long addrId){
FareVo fare =wareInfoService.getFare(addrId);
return R.ok().put("data",fare);
}
buildOrderItems方法
//构建全部的订单项目
private List<OrderItemEntity> buildOrderItems(String timeId) {
List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
if(currentUserCartItems!=null&¤tUserCartItems.size()>0){
List<OrderItemEntity> collect = currentUserCartItems.stream().map(i -> {
OrderItemEntity orderItemEntity = buildOrderItem(i);
orderItemEntity.setOrderSn(timeId);//这里只放订单号
return orderItemEntity;
}).collect(Collectors.toList());
return collect;
}
return null;
}
getCurrentUserItems远程调用接口,这个远程接口也是以前写的
package com.wuyimin.gulimall.cart.controller;
@GetMapping("/currentUserCartItems")
@ResponseBody
public List<CartItem> getCurrentUserCartItems(){
return cartService.getUserCartItems();
}
buildOrderItem方法
//构建特定的订单项
private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
OrderItemEntity itemEntity = new OrderItemEntity();
// 1.订单信息: 订单号
// 已经在items里设置了
// 2.商品spu信息
Long skuId = cartItem.getSkuId();
// 远程获取spu的信息
R r = productFeignService.getSpuInfoBySkuId(skuId);
SpuInfoVo spuInfo = r.getData(new TypeReference<SpuInfoVo>() {
});
itemEntity.setSpuId(spuInfo.getId());
itemEntity.setSpuBrand(spuInfo.getBrandId().toString());
itemEntity.setSpuName(spuInfo.getSpuName());
itemEntity.setCategoryId(spuInfo.getCatalogId());
// 3.商品的sku信息
itemEntity.setSkuId(cartItem.getSkuId());
itemEntity.setSkuName(cartItem.getTitle());
itemEntity.setSkuPic(cartItem.getImage());
itemEntity.setSkuPrice(cartItem.getPrice());
// 把一个集合按照指定的字符串进行分割得到一个字符串
// 属性list生成一个string
String skuAttr = StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(), ";");
itemEntity.setSkuAttrsVals(skuAttr);
itemEntity.setSkuQuantity(cartItem.getCount());
// 4.积分信息 买的数量越多积分越多 成长值越多
itemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount())).intValue());
itemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount())).intValue());
// 5.订单项的价格信息 优惠金额
itemEntity.setPromotionAmount(new BigDecimal("0.0")); // 促销打折
itemEntity.setCouponAmount(new BigDecimal("0.0")); // 优惠券
itemEntity.setIntegrationAmount(new BigDecimal("0.0")); // 积分
// 当前订单项的原价
BigDecimal orign = itemEntity.getSkuPrice().multiply(new BigDecimal(itemEntity.getSkuQuantity().toString()));
// 减去各种优惠的价格
BigDecimal subtract =
orign.subtract(itemEntity.getCouponAmount()) // 优惠券逻辑没有写,应该去coupon服务查用户的sku优惠券
.subtract(itemEntity.getPromotionAmount()) // 官方促销
.subtract(itemEntity.getIntegrationAmount()); // 京豆/积分
itemEntity.setRealAmount(subtract);
return itemEntity;
}
getSpuInfoBySkuId远程调用方法
@Autowired
private SkuInfoService skuInfoService;
@GetMapping("/skuId/{id}")
public R getSpuInfoBySkuId(@PathVariable("id") Long skuId){
SpuInfoEntity entity=new SpuInfoEntity();
SkuInfoEntity skuInfoEntity = skuInfoService.getById(skuId);
Long spuId = skuInfoEntity.getSpuId();
SpuInfoEntity res = spuInfoService.getById(spuId);
return R.ok().setData(res);
}
computePrice方法
private void computerPrice(OrderEntity orderEntity, List<OrderItemEntity> items) {
// 叠加每一个订单项的金额
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");
// 总价
BigDecimal totalPrice = new BigDecimal("0.0");
for (OrderItemEntity item : items) { // 这段逻辑不是特别合理,最重要的是累积总价,别的可以跳过
// 优惠券的金额
coupon = coupon.add(item.getCouponAmount());
// 积分优惠的金额
integration = integration.add(item.getIntegrationAmount());
// 打折的金额
promotion = promotion.add(item.getPromotionAmount());
BigDecimal realAmount = item.getRealAmount();
totalPrice = totalPrice.add(realAmount);
// 购物获取的积分、成长值
gift.add(new BigDecimal(item.getGiftIntegration().toString()));
growth.add(new BigDecimal(item.getGiftGrowth().toString()));
}
// 1.订单价格相关 总额、应付总额
orderEntity.setTotalAmount(totalPrice);
orderEntity.setPayAmount(totalPrice.add(orderEntity.getFreightAmount()));
orderEntity.setPromotionAmount(promotion);
orderEntity.setIntegrationAmount(integration);
orderEntity.setCouponAmount(coupon);
// 设置积分、成长值
orderEntity.setIntegration(gift.intValue());
orderEntity.setGrowth(growth.intValue());
// 设置订单的删除状态
orderEntity.setDeleteStatus(OrderStatusEnum.CREATE_NEW.getCode());
}
2.saveOrder方法--保存订单信息
//保存订单信息
private void saveOrder(OrderCreateVo orderCreateTo) {
OrderEntity order = orderCreateTo.getOrder();
order.setCreateTime(new Date());
order.setModifyTime(new Date());
this.save(order);
orderItemService.saveBatch(orderCreateTo.getOrderItems());
}
3.锁定仓库远程方法orderLockStock
package com.wuyimin.gulimall.ware.controller;
@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {
@Autowired
private WareSkuService wareSkuService;
@PostMapping("/lock/order")
public R orderLockStock(@RequestBody WareSkuLockVo vo){
Boolean results= null;
try {
results = wareSkuService.orderLockStock(vo);
} catch (NoStockException e) {
return R.error(BizCodeEnum.NO_STOCK_EXCEPTION.getCode(),BizCodeEnum.NO_STOCK_EXCEPTION.getMsg());
}
return R.ok().setData(results);
}
}
orderLockStock方法
package com.wuyimin.gulimall.ware.service.impl;
//为订单锁定库存
@Override
@Transactional(rollbackFor =NoStockException.class )//不写class也可以,默认是运行时异常都会回滚
public Boolean orderLockStock(WareSkuLockVo vo) {
//1.找到每个商品在那个仓库都有库存
List<OrderItemVo> locks = vo.getLocks();
List<SkuWareHasStock> skuWareHasStocks = locks.stream().map(item -> {
SkuWareHasStock stock = new SkuWareHasStock();
Long skuId = item.getSkuId();
stock.setSkuId(skuId);
stock.setNum(item.getCount());
//查询这个商品在哪有库存
List<Long> list = wareSkuDao.listWareIdHasStock(skuId);
stock.setWareId(list);
return stock;
}).collect(Collectors.toList());
//2.锁定库存
for (SkuWareHasStock skuWareHasStock : skuWareHasStocks) {
Boolean skuStocked=false;//当前商品是否锁住
Long skuId = skuWareHasStock.getSkuId();
List<Long> wareIds = skuWareHasStock.getWareId();
if(wareIds==null||wareIds.size()==0){
//没有任何仓库有这个商品的库存,订单失败,全部回滚
throw new NoStockException(skuId);
}
for (Long wareId : wareIds) {
Long count= wareSkuDao.lockSkuStock(skuId,wareId,skuWareHasStock.getNum());
if(count==1){
skuStocked=true;//锁定成功
break;
}else{
//当前仓库锁定失败,尝试下一个仓库
}
}
if(!skuStocked){
//当前商品所有仓库都没有锁住
throw new NoStockException(skuId);
}
}
//能走到这就说明锁定成功了
return true;
}
listWareIdHasStock方法和lockSkuStock方法
<update id="lockSkuStock">
update wms_ware_sku set stock_locked=stock_locked+#{num}
where sku_Id=#{skuId} and ware_id=#{wareId} and stock-stock_locked>=#{num}
</update>
<select id="listWareIdHasStock" resultType="java.lang.Long">
select ware_id from wms_ware_sku where sku_id=#{skuId} and stock-stock_locked>0
</select>