目录
商城业务-订单服务-页面环境搭建
商城业务-订单服务-整合SpringSession
商城业务-订单服务-订单基本概念
商城业务-订单服务-订单登录拦截
商城业务-订单服务-订单确认页模型抽取
商城业务-订单服务-订单确认页数据获取
商城业务-订单服务-Feign远程调用丢失请求头问题
商城业务-订单服务-Feign异步调用丢失请求头问题
商城业务-订单服务-bug修改
商城业务-订单服务-订单确认页渲染
商城业务-订单服务-订单确认页库存查询
商城业务-订单服务-订单确认页模拟运费效果
商城业务-订单服务-订单确认页细节显示
商城业务-订单服务-接口幂等性讨论
商城业务-订单服务-订单确认页完成
商城业务-订单服务-原子验令牌
商城业务-订单服务-构造订单数据
商城业务-订单服务-构造订单项数据
商城业务-订单服务-订单验价
商城业务-订单服务-保存订单数据
商城业务-订单服务-锁定库存
商城业务-订单服务-提交订单的问题
1.将订单服务注册到注册中心去
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
org.springframework.boot
spring-boot-starter-thymeleaf
spring:
thymeleaf:
cache: false # thymeleaf缓存关闭
3.在/mydata/nginx/html/static下面创建order文件夹,在order文件夹下分别创建detail(订单详情)、list(订单列表)、confirm(确认订单)、pay(支付订单)文件夹,用于存放订单相关的静态资源
4. 将index.html依次复制到templates并进行更名,修改其中的请求资源路径
并加入thymeleaf的名称空间
5.配置订单服务的域名
6.配置网关
7. 编写Controller访问订单页面
出现错误:confirm.html 报 Unfinished block structure
解决方案: 将/*删除即可
1.Redis默认使用lettuce作为客户端可能导致内存泄漏,因此需要排除lettuce依赖,使用jedis作为客户端或者使用高版本的Redis依赖即可解决内存泄漏问题
解决方案1:
org.springframework.boot
spring-boot-starter-data-redis
redis.clients
jedis
解决方案2: 我使用时官方已经修复了lettuce内存泄漏的问题,因此,我采用的是方案2
org.springframework.boot
spring-boot-starter-data-redis
配置 Redis
spring:
redis:
host: 192.168.56.22
port: 6379
2. 导入Session依赖
org.springframework.session
spring-session-data-redis
配置 Session的存储类型
# 会话存储类型
spring.session.store-type=redis
编写Session配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
@Configuration
public class GulimallSessionConfig {
/**
* 子域问题共享解决
*/
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULIMALLSESSION");
return cookieSerializer;
}
/**
* 使用json序列化方式来序列化对象数据到redis中
*/
@Bean
public RedisSerializer
编写线程池配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Configuration
public class MyThreadPoolConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties threadPoolConfigProperties){
return new ThreadPoolExecutor(threadPoolConfigProperties.getCoreThreadSize(),
threadPoolConfigProperties.getMaxThreadSize(),
threadPoolConfigProperties.getKeepAliveTime(), TimeUnit.SECONDS,new LinkedBlockingQueue<>(100000),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
}
}
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {
private Integer coreThreadSize;
private Integer maxThreadSize;
private Integer keepAliveTime;
}
配置
gulimall.thread.core-thread-size=20
gulimall.thread.max-thread-size=200
gulimall.thread.keep-alive-time=10
使用@EnableRedisHttpSession让Session启作用
3. 登录回显
我的订单路径跳转
订单服务中的数据流向如下图所示:
1.路由跳转,点击去结算跳转至订单确认页
拦截器的编写:
import com.atguigu.common.constant.SessionAttrKeyConstant;
import com.atguigu.common.vo.MemberRespVo;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(SessionAttrKeyConstant.LOGIN_USER);
if (attribute != null){
// 登录成功后,将用户信息存储至ThreadLocal中方便其它服务获取用户信息
loginUser.set(attribute);
return true;
}else {
// 未登录请先登录
request.getSession().setAttribute("msg","请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
需要配置以下配置类拦截器才会生效:
import com.atguigu.gulimall.order.interceptor.LoginUserInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
@Autowired
LoginUserInterceptor loginUserInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 配置CartInterceptor拦截器拦截所有请求
registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
}
}
未登录消息提醒 回显
1.购物车计算价格存在小bug,未选中的商品不应该加入总价的计算
解决方案:
①用户地址信息,数据来源:ums_member_receive_address
② 商品项信息,之前编写CartItem
③ 优惠券信息,使用京豆的形式增加用户的积分
④ 订单总额和应付总额信息
3. 编写Vo
1.创建出返回会员地址列表的方法,方便后续的远程服务调用
3. 查询购物车时,需要查询实时的商品价格,因此,编写通过skuId查询商品价格的接口
4. 远程调用商品服务,查询商品的实时价格
5. 查询购物车接口编写
注意细节:①需要过滤选中的商品②Redis中的购物车商品的价格可能是很久之前的需要实时查询商品的价格
6. 远程调用购物车服务,查询购物车中的商品列表
7. 价格获取方法编写
为了防止用户重复提交提订单,需要编写一个令牌(Token)
出现问题:远程调用购物车服务,购物车认为未登录
出现问题的原因:feign构建的新请求未把老请求头给带过来
解决方案:feign在创建RequestTemplate之前会调用很多RequestInterceptor,可以利用RequestInterceptor将老请求头给加上
配置类如下:
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
@Configuration
public class GuliFeignConfig {
@Bean
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 1.通过RequestContextHolder拿到老请求
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletRequest oldRequest = servletRequestAttributes.getRequest();
// 2.同步请求头数据
String cookie = oldRequest.getHeader("Cookie");
template.header("Cookie",cookie);
}
};
}
}
1.注入线程池
2.使用异步编排,各个任务彼此之间互不相关,但是需要等待各个任务处理完成
出现问题: 异步任务执行远程调用时会丢失请求上下文,oldRequest会为null
出现问题的原因: 当我们不使用异步编排的时候也就是单线程执行的时候,请求上下文持有器即:RequestContextHolder采用的是ThreadLocal存储请求对象。当我们采用异步编排时,而是多个线程去执行,新建的线程会丢失请求对象。
解决方案: 每个新建的线程都去添加之前请求的数据
出现问题:
org.thymeleaf.exceptions.TemplateInputException: Error resolving template [user/cart], template might not exist or might not be accessible by any of the configured Template Resolvers
解决方案:
1.收货人信息回显
2. 商品信息回显
3. 商品总件数、总金额、应付金额回显
总件数计算:
1.库存服务中查询库存的方法之前已经编写好了
2. 远程服务接口调用编写
3. Vo编写
4. 编写异步任务查询库存信息
编写Map用于封装库存信息
出现空指针异常:无需共享数据就不用做以下操作了
1.远程服务调用查询地址接口编写
2.编写获取邮费的接口
为div绑定class方便找到,自定义def属性存储默认地址值,默认地址为1,否则为0
空格代表子元素
自定义属性存储地址Id
为运费定义一个id,用于运费的回显
为应付总额定义一个id,用于计算应付总额的回显
为p标签绑定单击事件
查询运费时连同地址信息一起返回,也就是选中地址的地址信息回显
1.编写vo
2.改写实现类
3. 信息回显
什么是接口幂等性?
假设网络很慢,用户多次点击提交订单,有可能会导致数据库中插入了多条订单记录,为了避免订单的重复提交,用专业的术语就称之为接口幂等性,通俗点讲就是用户提交一次和用户提交一百次的结果是一样的,数据库中只会有一条订单记录。
那些情况需要防止?
什么情况需要接口幂等性?
幂等解决方案
1.Token机制
数据库悲观锁的使用场景:当我们查询库存信息时可以使用悲观锁锁住这条记录确保别人拿不到。
数据库乐观锁的使用场景:当我们减库存操作时,带上version=1执行成功此时version=2,但是由于网络原因没有返回执行成功标识,下一次请求过来还是带上的是version=1就无法对库存进行操作。
对订单号设置唯一约束
右击表点击设计表
Redis set的防重场景:每个数据的MD5加密后的值唯一,网盘就可以根据上传的数据进行MD5加密,将加密后的数据存储至Redis的set里,下次你上传同样的东西时先会去set进行判断是否存在,存在就不处理。
防重表的应用场景:当我们去解库存的时候,先去防重表里插入一条数据,当请求再次过来的时候,先去防重表里插入数据,只有当插入成功才能进行下一步操作。
Nginx为每一个请求设置唯一id可以用作链路追踪,看这个请求请求了那些服务
1.订单服务的执行流程如下图所示
2. 防重令牌的编写
①注入StringRedisTemplate
② 编写订单服务常量即防重令牌前缀,格式:order:token:userId
③ 防重令牌存储
3. 提交页面数据Vo的编写
选中一个商品,点击去结算
回到购物车,再选中另一个商品
回到结算页,点击结算
说明:京东的结算页中的商品信息是实时获取的,结算的时候会去购物车中再去获取一遍,因此,提交页面的数据Vo没必要提交商品信息
6.编写提交订单数据接口
1.提交订单返回结果Vo编写
2. 接口编写
验证令牌的核心:保证令牌的比较和删除的原子性
解决方案:使用脚本
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
脚本执行的返回结果:
0:代表令牌校验失败
1:代表令牌成功删除即成功
execute(arg1,arg2,arg3)参数解释:
arg1:用DefaultRedisScript的构造器封装脚本和返回值类型
arg2:数组,用于存放Redis中token的key
arg3:用于比较的token即浏览器存储的token
T:返回值的类型
1.订单创建To的编写
2. 创建订单方法编写
①订单状态枚举类的编写
② IDWorker中的getTimeId()生成时间id,不重复,用于充当订单号
④ 远程服务调用获取地址和运费信息
⑤ 使用ThreadLocal,实现同一线程共享数据
⑥ 将构造订单数据的代码抽取成方法
1.远程服务调用,通过skuId获取Spu信息
1.计算单个购物项的真实价格
2.设置订单的价格
3.订单其它信息设置
4. 验价
1.保存订单和订单项数据
①保存订单和订单项以及锁库存操作处于事务当中,出现异常需要回滚
②注入orderItemService
③保存
锁库存逻辑
远程服务调用锁定库存
1.锁库存Vo编写
将订单服务的OrderLockVo和OrderItemVo复制到库存服务中
2. 锁库存响应Vo编写
3. 锁库存异常类的编写
4. 库存不足异常状态码编写
5.为库存表的锁库存字段设置默认值:0
6. 查询库存接口编写
指定抛出此异常时一定要回滚,不指定也会回滚默认运行时异常都会回滚
内部类保存商品在那些仓库有库存以及锁库存数量
锁库存实现
1.订单号显示
2.应付金额回显
出现问题:orderSn长度过长
解决方案:数据库的表中的对应字段长度增大
3.提交订单消息回显
4. 为了确保锁库存失败后,订单和订单项也能回滚,需要抛出异常
出现问题:库存不足消息回显失败,一开始我以为是addFlashAttribute()方法失效了,因为,浏览器中的session里面并没有值
后面我才想起里,并不是这样的,我用了Spring- Session,所以获取到的session都是redis中存储的session,redis中的session是有addFlashAttribute添加属性的key的,因此,我查看log以及结合之前的代码发现很有可能存储的是set,直接存是存是存储不了的,答案确实是set。
解决方案: 以map的形式存储数据