1.认证服务
1.配置auth的环境
2.点击验证码的60s倒数
思路
①点击之后把对应的text切换是多少s之后才能再次点击,然后通过setTimeOut(“xx()”,1000)进行对方法重复调用,并且一个num记录60s时间递减。
拓展与坑
①如果重复点击a,问题就是会多次调用递减函数,解决办法就是通过加上一个class并且用于对比是不是已经被使用了。
var num=60
$(function () {
$("#sendCode").click(function(){
//判断是否有这个class
if($("#sendCode").hasClass("disabled")){
}else{
timeChangeStyle();
}
})
})
function timeChangeStyle(){
$("#sendCode").attr("class","disabled")
if(num==0){
num=60
$("#sendCode").text("点击发送验证码")
$("#sendCode").attr("class","");
}else{
var str=num+"s,后才能再次点击"
$("#sendCode").text(str);
setTimeout("timeChangeStyle()",1000);
}
num--;
}
3.阿里云发送验证码
思路
①直接调用api
②对于可以修改的数据可以通过绑定properties来进行赋值。需要单独抽取一个方法放到Component里面去,这样的好处就是如果我们要使用其他接口,只需要修改一部分就可以了。
@Component
@Data
@ConfigurationProperties(prefix = "spring.alicloud.sms")
public class SmsComponent {
private String host;
private String path;
public void sendSms(String phone, String code) {
String method = "POST";
String appcode = "xxxxx";
Map<String, String> headers = new HashMap<String, String>();
headers.put("Authorization", "APPCODE " + appcode);
//根据API的要求,定义相对应的Content-Type
headers.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
Map<String, String> querys = new HashMap<String, String>();
Map<String, String> bodys = new HashMap<String, String>();
bodys.put("callbackUrl", "http://test.dev.esandcloud.com");
bodys.put("channel", "0");
bodys.put("mobile", phone);
bodys.put("templateID", "0000000");
bodys.put("templateParamSet", code+",1");
try {
/**
* 重要提示如下:
* HttpUtils请从
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
* 下载
*
* 相应的依赖请参照
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
*/
HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
System.out.println(response.toString());
//获取response的body
//System.out.println(EntityUtils.toString(response.getEntity()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.应用到项目中的短信发送
思路
①前端点击发送验证码之后发送ajax请求到后台
②后台在auth服务中创建con处理远程调用third的短信服务,定义好常量的缓存前缀和出错之后的异常enum。
③uuid生成code然后补上时间并存入redis。
④为了解决重复刷新取短信的问题,可以通过获取redis存入code后面的时间,然后获取当前时间相减,那么就能够知道是否已经过了60s。如果是那么就可以重新获取,如果不是那么就不能重新获取。
@Controller
public class LoginController {
@Autowired
ThirdPartyFeignService thirdPartyFeignService;
@Autowired
StringRedisTemplate redisTemplate;
@ResponseBody
@RequestMapping("/sms/sendSms")
public R sendSms(@RequestParam("phone")String phone){
System.out.println("短信发送");
//1.先去取出看看是否过期
String code = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
if(!StringUtils.isEmpty(code)){
String[] s = code.split("_");
if(System.currentTimeMillis()-Long.parseLong(s[1])<60000){
return R.error(BizCodeEnume.VALID_SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.VALID_SMS_CODE_EXCEPTION.getMsg());
}
}
code = UUID.randomUUID().toString().substring(0, 5)+"_"+System.currentTimeMillis();
//2.redis保存code进去
redisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,code,10, TimeUnit.MINUTES);
thirdPartyFeignService.sendSms(phone,code);
return R.ok();
}
}
5.注册
思路
①先有一个vo对象获取前端传过来的数据,然后后台接收数据并且通过jsr303来校验数据的合法性
②然后就是通过redirectAttributes来进行重定向的数据传输。通过bindingResult来获取是否出现结果错误。
③前端主要就是修改路径,加上name,并且取消submit的点击事件。还要修改tips的样式。
拓展与坑
①你会发现如果没有输入任何东西,它会报duplicate的问题,主要就是数据出现两个错误,但是key确是一样的
@RequestMapping("/regist")
public String regist(@Valid RegistVo registVo, BindingResult result, RedirectAttributes model){
System.out.println("我执行了");
if(result.hasErrors()){
Map<String, String> map = result.getFieldErrors().stream().collect(Collectors.toMap(fieldError -> {
return fieldError.getField();
}, fieldError -> {
return fieldError.getDefaultMessage();
}));
model.addFlashAttribute("errors",map);
return "redirect:http://auth.gulimall.com/reg.html";
}
return "redirect:http://auth.gulimall.com/login.html";
}
6.远程调用member注册与异常映射
思路
①首先就是在auth这里验证验证码是否正确,如果正确那么就远程调用来注册这个账号
②注册账号先查询会员的默认级别,然后就是检验username和phone是否重复出现,如果没有那么就存入
③检查是否出现这个地方可以自己创建异常类并且抛出交给controller处理。
拓展
①声明抛出异常的好处就是能够让别人调用的时候知道需不需要处理这个异常。
@Override
public void regist(MemberRegistVo memberRegistVo) {
MemberEntity memberEntity = new MemberEntity();
//1.查找会员等级
MemberLevelEntity memberLevelEntity=memberLevelService.getDefaultLevel();
memberEntity.setLevelId(memberLevelEntity.getId());
//1.查询用户名和电话是否存在
this.usernameExist(memberRegistVo.getUsername());
memberEntity.setUsername(memberRegistVo.getUsername());
this.phoneExist(memberRegistVo.getPhone());
memberEntity.setMobile(memberRegistVo.getPhone());
memberEntity.setPassword(memberRegistVo.getPassword());
this.baseMapper.insert(memberEntity);
}
@Override
public void usernameExist(String username) {
int count = this.count(new QueryWrapper<MemberEntity>().eq("username", username));
if(count>0){
throw new UsernameExistException();
}
}
@Override
public void phoneExist(String phone) {
int count = this.count(new QueryWrapper<MemberEntity>().eq("phone", phone));
if(count>0){
throw new PhoneExistException();
}
}
7.MD5加密和盐值加密
简介
- md5加密的特点就是不可逆,摘要加密,抗修改性(修改哪怕一个字节都会区别很大)
- 盐值加密,就是为了让MD5加密变得更可靠,不被彩虹表直接对应上
@Test
public void cry(){
// Md5
// String s = Md5Crypt.md5Crypt("123456".getBytes());
// System.out.println(s);
BCryptPasswordEncoder encoder=new BCryptPasswordEncoder();
String encode = encoder.encode("1234");
System.out.println(encode);
System.out.println("->"+encoder.matches("1234",encode));
}
8.远程调用
思路
①先判断redis验证码是否正确,如果正确那么就删掉原来的
②然后就是远程调用,如果成功,那么就跳转到登录页面,如果不是那么就跳转到注册页面。
③可能会遇到超时问题,解决办法就是设置ribbon的值。让ribbon等久一点。
拓展与坑
①如果发现FeignException$InternalServerError错误那么可能就是参数错误,也可能是你调用的服务出现问题,我出现的问题是sql语句写错了
9.登录
思路
①登录通过auth服务调用member服务,根据loginacct和password来进行对用户查询。现根据唯一的loginacct查询是否有这个用户,如果有那么就可以对比密码,如果没有那么就返回空。如果密码错误也是返回空,最后就是跳转到首页或是跳转到登录页面进行回显操作。
拓展和思考
①这里的错误信息可以通过枚举类来完成msg和code的赋值,就不需要自己手打上去防止错误。
member
@Override
public MemberEntity getLogin(MemberLoginVo memberLoginVo) {
//1.获取vo的数据
String loginacct = memberLoginVo.getLoginacct();
String passwordVo = memberLoginVo.getPassword();
//2.查询数据库是否存在这个用户名
MemberEntity memberEntity = this.baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("username", loginacct).or().eq("mobile", loginacct));
if(memberEntity==null){
return null;
}else{
//3.如果存在那么就对比密码是否正确,如果正确返回对象
String passwordDb = memberEntity.getPassword();
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
boolean matches = encoder.matches(passwordVo, passwordDb);
if(matches){
return memberEntity;
}else{
return null;
}
}
}
10.OAuth2.0
简介
它是一个认证协议标准。
流程(qq为例)
①先通过用户的认证,其实就是我们输入账号密码或者是扫二维码,然后发送authenticate请求到qq服务器
②然后就是qq服务器认证,返回令牌重定向到客户端的网页位置
③最后客户端就能够通过令牌访问qq服务器获取开放的信息。
11.社交登录,微博授权登录(gitee代替)
思路
①在gitee的设置创建一个第三方应用,它会提供clientid和clienturi,还有就是这里的回调地址一定要按照项目的来,因为如果回调地址写错,那么就会访问错误。
②首先是访问https://gitee.com/oauth/authorize?response_type=code&client_id=xxx&&redirect_uri=http://gulimall.com&state=123&scope=xxx获取对应的code
③然后访问
https://gitee.com/oauth/token?grant_type=authorization_code&client_id=xxxxx&client_secret=xxx&code=xxx&redirect_uri=http://gulimall.com/success获取令牌④获取token之后就可以访问用户信息,带上令牌信息
https://gitee.com/api/v5/user?access_token=xxx
拓展与坑
①一开始以为只能用微博处理,但是后来发现微信、gitee都可以,只要理解本质,所有的api只不过换个头不换本质,思路比代码更重要,应用是最好了解原理的方式。
12.OAuth2Controller
思路
①前端点击gitee之后就会跳转到登录授权页面,登录之后就会回调这个用户授权成功的函数。
②这个函数就卸载OAuth2Controller,主要是为了通过接收code去换取token
13.gitee的登录
思路
①auth远程调用member来进行登录。并且给出令牌信息
②通过令牌获取用户数据保存到数据库,并且更新令牌和过期时间。如果用户存在那么就只做更新令牌和过期时间,如果是用户不存在那么就需要进行插入。
拓展与坑
①host包括了https
②整个OAuth2的思路其实是这样的
先从客户端发送请求->用户授权页面->用户授权->gitee服务器->服务器重定向到回调函数并且带着code->然后就是通过code再次发送请求->gitee服务器->用code获取令牌->客户端登录成功->利用令牌获取用户信息(其实就是发送各种获取用户信息的请求)关键就是过程理解
url->code(需要clientId)->token(code+clientId+clientSercret)->用户信息
@Override
public MemberEntity giteeLogin(SocialUser socialUser) throws IOException {
HashMap<String, String> map = new HashMap<>();
map.put("access_token",socialUser.getAccess_token());
//1.先查询所有的信息
HttpResponse response = null;
try {
response = HttpUtils.doGet("gitee.com", "/api/v5/user", "get", new HashMap<>(null), map);
} catch (Exception e) {
e.printStackTrace();
return null;
}
if(response.getStatusLine().getStatusCode()==200){
//2.先查询对应数据库中是否存在这个对象
String giteeUserJson = EntityUtils.toString(response.getEntity());
JSONObject giteeUserObject = JSON.parseObject(giteeUserJson);
String socialUid = giteeUserObject.getString("id");
//3.根据id查询数据库是否存在这个user
MemberEntity memberEntity = this.baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", socialUid));
//4.如果它不是空的
if(memberEntity!=null){
//存在,更新令牌和时间
MemberEntity update = new MemberEntity();
update.setId(memberEntity.getId());
update.setExpireIn(socialUser.getExpires_in());
update.setAccessToken(socialUser.getAccess_token());
this.baseMapper.updateById(update);
memberEntity.setExpireIn(socialUser.getExpires_in());
memberEntity.setAccessToken(socialUser.getAccess_token());
return memberEntity;
}else{
//如果不存在,那么就插入新的数据进去
MemberEntity regist = new MemberEntity();
regist.setNickname(giteeUserObject.getString("name"));
regist.setExpireIn(socialUser.getExpires_in());
regist.setAccessToken(socialUser.getAccess_token());
regist.setSocialUid(giteeUserObject.getString("id"));
this.baseMapper.insert(regist);
return regist;
}
}else{
return null;
}
}
14.session共享问题
①session在分布式,同一个服务,但是在服务器集群中的时候就会发现,如果在一台服务器中存入session用户信息,但是另一台服务器是没有的
②保存sessionid的cookie是不能够进行跨域的,也就是说如果现在需要重定向到另一个域名的页面,那么这个cookie就会不见
解决方案
- 第一种就是同步session到所有的服务器,问题就是每次都需要处理这个复制过程,而且需要存同一服务所有服务器的session,内存严重不足
- 第二种就是存入cookie,容易被窃取,而且cookie存入量很小,还有跨域问题
- 第三种就是hash一致性,其实就是可以通过负载均衡通过hash算法完成对浏览器和服务器之间的一个固定映射,但是出现的问题就是单点故障
- 最后一种就是通过中间件redis来完成session存储,安全,而且容易水平扩展。问题就是增加中间件就需要增加网络带宽,传输数据的用去的时间。而且cookie的域名范围问题,需要通过子域名来解决。
15.使用springsession
思路
- 加入product和auth的springsession和redis的依赖
- 然后就是配置yml,启用什么类型的session存储
- 接着就是开启注解@EnableRedisHttpSession
拓展与坑
①对象序列化包括了包名,如果找不到对应包的对象是不能反序列化的,解决办法就是把对象放到一个通用common服务里面.
仍然存在问题
- cookie的域名范围domain问题
- 第二个就是能不能转换成json字符串存储,这样就不需要序列化了
解决方案:直接加上CookieSerilizer和RedisSerilizer。设置好domain,初始化使用json的redis序列化器
@Configuration
public class GuliSessionConfig {
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
//1.设置域名范围
cookieSerializer.setDomainName("gulimall.com");
//2.色哈值cookie名称
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer redisSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
}
16.SpringSession的原理
- 两个重要的类,一个是SessionRepositoryFilter,另一个是操作redis的类RedisIndexedSessionRepository。
- 原理其实就是filter构造的时候把repository放进来了,并且通过父类方法的doFilter来调用子类重写方法doFilterInternal,这个主要作用就是把request和response包装到一个wrapperreq和wrapperres的包装类里面,使用了装饰者模式。因为每次调用都要通过req的getSession来获取session,然后包装类就重写这个方法,获取session,然后利用repository操作redis把session存入数据库。
装饰者模式简介
其实就相当于是衣服,通过布或者是纸来包住,布和纸就是所谓的装饰者,只需要纸和布都继承一个component,并且内置衣服,就可以布里面带上纸包住衣服。也可以是布包住布包住纸再包住衣服这样的套娃操作。具体就要看书了。
最后就是调整一下页面的显示,如果访问login.html的话那么就判断是否有用户存在session,如果有那么就跳转到首页,如果没有那么就跳转到登录页面
17.单点登录
简介
其实就是多个系统,比如谷粒商城,谷粒学院,还有尚筹网,只要其中一个登录那么其他所有系统都能够登录。这种就是单点登录。
思路
①通过给重定向的地址后面加上参数token,然后通过token解析之后去到redis中取出用户的状态。
②client先查看是否有token参数,如果没有就看看session里面是否有loginUser,如果都没有那么就->auth认证中心获取登录页->登录->auth登录处理,生成token返回->client再次查询是否有token,如果有那么就去redis中取出用户状态,并且放进这个domain下的session。
上面的思路仍然存在一个问题,就是只有一个系统登录了,但是其他系统还是无法感知到现在已经登陆。
解决方案
①可以把第一次访问auth的登录生成的token放到本domain下,然后另一个系统来访问的时候就来查询这个cookie是否存在,如果存在,那么就认为已经登陆就没有必要返回登录页面,直接返回到请求的页面。
总结:其实就是client->请求->登录页->client获取返回的token,而且登录服务器中存入cookie保存这个token->client2访问->登录服务器发现存在这个cookie->直接返回到client2自己的系统页面->并且通过远程访问来获取userInfo(带上token)。
整合helloController的逻辑,其实就是看有没有token,如果没有那么就跳去sso.com的登录页去登录获取,如果有那么就远程调用获取loginUser信息,并且放入session展示信息。
@Controller
public class HelloController {
@Value("${sso.server.url}")
private String serverUrl;
@ResponseBody
@RequestMapping("hello")
public String hello(){
return "hello";
}
@RequestMapping("employees")
public String employee(Model model, HttpSession session,
@RequestParam(value = "token",required = false)String token){
if(token!=null){
System.out.println("我被调用了");
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> user = restTemplate.getForEntity("http://sso.com:8080/userInfo?token="+token,String.class);
session.setAttribute("loginUser",user.getBody());
}
Object loginUser = session.getAttribute("loginUser");
if(loginUser==null){
return "redirect:"+serverUrl+"?redirect_url=http://client1.com:8081/employees";
}else{
List<String> list=new ArrayList<>();
list.add("张三");
list.add("李四");
model.addAttribute("employees",list);
return "index";
}
}
}
LoginController的逻辑就是先跳到登录页,查询有没有这个sso_token在cookie,如果有那么就重定向回去原来的页面,不去登录页。如果没有那么就要登录,并且创建token把token和用户信息存入redis。然后再带上token重定向到原来的页面。也就是本质那就是通过参数传递这个token。
@Controller
public class LoginController {
@Autowired
StringRedisTemplate redisTemplate;
@ResponseBody
@GetMapping("userInfo")
public String getUserInfo(@RequestParam("token")String token){
//直接从redis中查并且对比
String user = redisTemplate.opsForValue().get(token);
return user;
}
@RequestMapping("/login.html")
public String loginhtml(@RequestParam("redirect_url")String redirectUrl, Model model,
@CookieValue(value = "sso_token",required = false)String ssoToken){
//如果有访问痕迹那么就是已经登陆过了。
if(!StringUtils.isEmpty(ssoToken)){
return "redirect:"+redirectUrl+"?token="+ssoToken;
}
model.addAttribute("url",redirectUrl);
return "login";
}
@RequestMapping("login")
public String login(
@RequestParam("username")String username,
@RequestParam("password")String password,
@RequestParam("url")String url,
HttpServletResponse response
){
if(!StringUtils.isEmpty(username)&&!StringUtils.isEmpty(password)){
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(token,"张把");
//把这个token保存在当前域名
Cookie cookie = new Cookie("sso_token", token);
response.addCookie(cookie);
return "redirect:"+url+"?token="+token;
}
return "login";
}
}
2.购物车
1.搭建环境
2.购物车的需求
- 添加商品
- 修改商品数量和总价
- 删除商品
- 在线和离线购物车
- 选中与不选中商品
购物车的数据结构可以用list吗?
答案是不可以,原因是商品多,遍历花费的时间多。所以采用了hash结构。
Map<String,Map<String,CartItem>>第一个String k1是用户购物车,第二个k2是标记商品的key。最后才是购物项。
用mysql还是redis?
用redis,原因是读写次数多。而且redis数据结构能够支撑hash,快速读写,返回。
3.编写Vo
思路
①主要就是模仿网页的上面有的属性来写,然后就是要自己来手动计算这些总价,总数量和总类型。通过get的重写来获取
CartItem
public class CartItem {
private Long skuId;
private String title;
private Boolean check=true;
private String image;
private List<String> skuAttr;
private BigDecimal price;
private Integer count;
private BigDecimal totalPrice;
public Long getSkuId() {
return skuId;
}
public void setSkuId(Long skuId) {
this.skuId = skuId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Boolean getCheck() {
return check;
}
public void setCheck(Boolean check) {
this.check = check;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public List<String> getSkuAttr() {
return skuAttr;
}
public void setSkuAttr(List<String> skuAttr) {
this.skuAttr = skuAttr;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public BigDecimal getTotalPrice() {
BigDecimal totalCheckPrice = this.price.multiply(new BigDecimal("" + this.count));
return totalCheckPrice;
}
public void setTotalPrice(BigDecimal totalPrice) {
this.totalPrice = totalPrice;
}
}
Cart
public class Cart {
private List<CartItem> items;
private Integer countNum;
private Integer countType;
private BigDecimal reduce;
private BigDecimal totalAmount;
public List<CartItem> getItems() {
return items;
}
public void setItems(List<CartItem> items) {
this.items = items;
}
public Integer getCountNum() {
int count=0;
if(this.items!=null&&this.items.size()>0){
for (CartItem item : items) {
count+=item.getCount();
}
}
return count;
}
public void setCountNum(Integer countNum) {
this.countNum = countNum;
}
public Integer getCountType() {
int count=0;
if(this.items!=null&&this.items.size()>0){
for (CartItem item : items) {
count+=1;
}
}
return count;
}
public void setCountType(Integer countType) {
this.countType = countType;
}
public BigDecimal getReduce() {
return reduce;
}
public void setReduce(BigDecimal reduce) {
this.reduce = reduce;
}
public BigDecimal getTotalAmount() {
BigDecimal totalPrice=new BigDecimal(0);
if(this.items!=null&&this.items.size()>0){
for (CartItem item : items) {
totalPrice = totalPrice.add(new BigDecimal(item.getTotalPrice() + ""));
}
}
BigDecimal subtract = totalPrice.subtract(new BigDecimal(getReduce() + ""));
return subtract;
}
public void setTotalAmount(BigDecimal totalAmount) {
this.totalAmount = totalAmount;
}
}
4.ThreadLocal用户身份鉴别
需求:主要来辨别用户到底是临时用户和还是已经登陆的用户
思路
①通过拦截器,获取session查看是否存在这个用户MemberResVo信息。如果存在那么就存入userId,并且在遍历cookie找到对应的user-key购物车key,并且set进userInfoTo中,如果发现没有这个user-key那么就创建一个set进userInfoTO中。
②然后就是处理完con之后的拦截,主要就是添加cookie通过userInfoTo设置的user-key。设置好domain。但是这个userInfoTo怎么获取?可以通过ThreadLocal获取
拓展与坑
①ThreadLocal可以贯穿从拦截器->con->ser->dao这样的过程,反过来也是一样的,最后要经过拦截器,这个时候ThreadLocal带的值会一直存在,可以通过这个来获取这个UserInfoTo(之所以是TO是因为在各种类中运输。)
②如果要添加拦截器,需要通过config类实现WebConfigurer来重写方法addInterceptor来加入拦截器,最后还需要加入需要拦截的路径。
interceptor
public class CartInterceptor implements HandlerInterceptor {
public static ThreadLocal<UserInfoTo> threadLocal=new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//0.创建一个临时保存的userInfoTo
UserInfoTo userInfoTo = new UserInfoTo();
//1.获取session
HttpSession session = request.getSession();
MemberResVo loginUser = (MemberResVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
//2.判断user是否存在
if(loginUser!=null){
userInfoTo.setUserId(loginUser.getId());
}
//3.遍历cookies看看是否存在这个用户的购物车key
Cookie[] cookies = request.getCookies();
if(cookies!=null&&cookies.length>0){
for (Cookie cookie : cookies) {
String cookieName = cookie.getName();
if(cookieName.equals(CartConstant.TEMP_USER_COOKIE_NAME)){
userInfoTo.setUserKey(cookie.getValue());
}
}
}
//4.如果这个to的key空的那么就要设置一个key,和临时用户提示
if(userInfoTo.getUserKey()==null){
String user_key = UUID.randomUUID().toString();
userInfoTo.setUserKey(user_key);
userInfoTo.setTempUser(true);
}
threadLocal.set(userInfoTo);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//1.取出这个userInfoTO,判断是不是临时用户,如果是那么就设置一个user-key的cookie
UserInfoTo userInfoTo = threadLocal.get();
if(userInfoTo.getTempUser()){
Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
cookie.setDomain("gulimall.com");
response.addCookie(cookie);
}
}
}
5.处理前端跳转细节
6.添加到购物车
需求:把商品通过购物车项的形式加入到购物车。
思路
1.购物车是谁?购物车就是redis下面的hash数据结构。
2.怎么处理,首先就是接受前端的skuid和count也就是商品的数量,接着就是通过远程调用获取skuInfo的信息给cartItem赋值,再远程调用获取attrlist。可以通过异步编排的方式来同时完成这几个远程调用。
3.存入redis的时候首先需要知道这是一个什么用户。可以通过userInfoTo来得知,如果有userid,那么userid就是redis的key,也就是购物车的key。如果没有那么就要使用临时生成的userkey作为购物车的key,保存商品。商品以skuId来作为自己的hash的key,value就是cartItem的json字符串。
拓展与坑
1.第一个就是存入redis最好就是json不需要进行序列化
2.第二个就是类似于购物车这类型的数据,要用到hash保存,不用list的原因就是查询很难,删除很麻烦。
3.最后就是进来首先就是判断商品是否存在,如果存在那么就只取出并修改数量再存入redis,如果没有那么就直接添加商品到redis就可以了。
4.还有一个细节问题就是关于请求转发的添加购物车问题,它会让购物车刷新页面的时候不断添加,解决办法就是通过重定向来完成这个页面的跳转的问题。
5.关于RedirectAttributes的方法,第一个flash是隐藏的数据,addAttr就是直接把这个数据放到了url的后面
CartSer
@Override
public CartItem addToCart(String skuId, Integer num) throws ExecutionException, InterruptedException {
//0.判断是什么用户,然后通过key或者是id作为redis的key保存,获取redis操作类
BoundHashOperations<String, Object, Object> ops = getOps();
String o = (String) ops.get(skuId);
//如果不存在那么就添加
if(StringUtils.isEmpty(o)){
//1.创建这个购物车项
CartItem cartItem = new CartItem();
CompletableFuture<Void> skuInfoFuture = CompletableFuture.runAsync(() -> {
//2.远程调用获取skuInfo
R skuInfoRes = productFeignService.info(Long.parseLong(skuId));
if (skuInfoRes.getCode() == 0) {
SkuInfoVo skuInfo = skuInfoRes.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
cartItem.setCheck(true);
cartItem.setCount(num);
cartItem.setImage(skuInfo.getSkuDefaultImg());
cartItem.setPrice(skuInfo.getPrice());
cartItem.setSkuId(Long.parseLong(skuId));
cartItem.setTitle(skuInfo.getSkuTitle());
cartItem.setTotalPrice(cartItem.getTotalPrice());
}
}, executor);
CompletableFuture<Void> attrFuture = CompletableFuture.runAsync(() -> {
//3.远程调用获取attr
R stringlistRes = productFeignService.stringlist(Long.parseLong(skuId));
if (stringlistRes.getCode() == 0) {
List<String> stringlist = stringlistRes.getData("stringlist", new TypeReference<List<String>>() {
});
cartItem.setSkuAttr(stringlist);
}
}, executor);
//异步编排,同时执行远程访问
CompletableFuture.allOf(attrFuture,skuInfoFuture).get();
//4.存入redis
String cartItemJson= JSON.toJSONString(cartItem);
ops.put(skuId,cartItemJson);
return cartItem;
}else{
CartItem cartItem = JSON.parseObject(o, CartItem.class);
cartItem.setCount(cartItem.getCount()+num);
String cartItemJson = JSON.toJSONString(cartItem);
ops.put(skuId,cartItemJson);
return cartItem;
}
}
private BoundHashOperations<String, Object, Object> getOps() {
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
String cartKey=new String();
if(userInfoTo.getUserId()!=null){
//如果存在id那么就是登录状态
cartKey=CART_PREFIX+userInfoTo.getUserId();
}else{
//如果不是就是临时key
cartKey=CART_PREFIX+userInfoTo.getUserKey();
}
BoundHashOperations<String, Object, Object> boundHashOps = redisTemplate.boundHashOps(cartKey);
return boundHashOps;
}
CartCon
@RequestMapping("addToCart")
public String addToCart(@RequestParam("num")Integer num,
@RequestParam("skuId")String skuId,
RedirectAttributes redirectAttributes) throws ExecutionException, InterruptedException {
//添加的商品项
cartService.addToCart(skuId,num);
redirectAttributes.addAttribute("skuId",skuId);
return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
}
@GetMapping("addToCartSuccess.html")
public String addToCartSuccess(@RequestParam("skuId")Long skuId,Model model){
CartItem cartItem=cartService.getCartItemBySkuId(skuId);
model.addAttribute("item",cartItem);
return "success";
}
7.合并购物车
思路
①获取购物车,第一个就是先查看是否登录了,如果登录那么就先拼接key,然后先获取临时购物车的所有CartItem通过redis的boundHashOps的values获取,遍历并转换成json->CartItem,接着就是通过之前写的方法addToCart加入到登录的购物车,然后清除。
②接着如果是没有登录那么就直接获取所有的临时购物车的值,并且放到购物车。
③最后去到页面进行渲染和展示,如果是decimal,那么可以通过#numbers.formatDecimal来完成限制。
@Override
public Cart getCart() throws ExecutionException, InterruptedException {
Cart cart = new Cart();
cart.setReduce(new BigDecimal(0));
//1.判断用户是否登录
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
String cartKey=new String();
if(userInfoTo.getUserId()!=null){
//1.如果登录,
cartKey=CART_PREFIX+userInfoTo.getUserId();
String tempCartKey = CART_PREFIX + userInfoTo.getUserKey();
//2.先合并购物车
List<CartItem> tempCartItemList = getCartItemListByCartKey(tempCartKey);
for (CartItem cartItem : tempCartItemList) {
//添加到user的位置。因为这个时候已经登录被拦截器把userId放入infoTO中
addToCart(cartItem.getSkuId().toString(),cartItem.getCount());
//清除临时购物车
this.clearCartItem(tempCartKey);
}
//3.获取购物车
List<CartItem> userCartItemList = getCartItemListByCartKey(cartKey);
cart.setItems(userCartItemList);
return cart;
}else{
//临时购物车
cartKey=CART_PREFIX+userInfoTo.getUserKey();
List<CartItem> cartItemList = getCartItemListByCartKey(cartKey);
cart.setItems(cartItemList);
return cart;
}
}
/**
* 清除临时购物车
* @param tempCartKey
*/
@Override
public void clearCartItem(String tempCartKey) {
redisTemplate.delete(tempCartKey);
}
/**
/*获取购物车中所有的CartItem
*/
private List<CartItem> getCartItemListByCartKey( String cartKey) {
BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(cartKey);
//获取购物车中所有的值,json->cartItem;
List<Object> values = ops.values();
List<CartItem> collect = values.stream().map(jsonObject -> {
String json = (String) jsonObject;
CartItem cartItem = JSON.parseObject(json, CartItem.class);
return cartItem;
}).collect(Collectors.toList());
return collect;
}
8.check选中点击事件和修改数量的点击事件还有删除事件
思路
①点击购物车列表的check之后,需要触发事件修改状态,要获取商品id和状态信息。
②后台直接从redis中获取这个数据,并且修改再改回json存回进去。
③修改数量基本上也是一样的思路,点击加减都会触发时间,并且获取count和skuId交给后台直接处理
④删除事件直接绑定事件,第一个负责绑定skuId,第二个删除按钮直接发送删除请求。
/**
* 修改check状态
* @param skuId
* @param check
*/
@Override
public void checkItem(Long skuId, Integer check) {
BoundHashOperations<String, Object, Object> ops = getOps();
CartItem cartItem = getCartItemBySkuId(skuId);
cartItem.setCheck(check==1);
String cartItemJson = JSON.toJSONString(cartItem);
ops.put(skuId.toString(),cartItemJson);
}
@Override
public void changeCount(Long skuId, Integer num) {
CartItem cartItem = getCartItemBySkuId(skuId);
cartItem.setCount(num);
BoundHashOperations<String, Object, Object> ops = getOps();
ops.put(skuId.toString(), JSON.toJSONString(cartItem));
}