[TOC]
1 购物车
用户选择商品放入购物车,如果未登录,跳转到登录页先登录。然后将商品放入购物车。
1.1 购物车分析
(1)需求分析
用户在商品详细页点击加入购物车,提交商品SKU编号和购买数量,添加到购物车。购物车展示页面如下:
(2)购物车实现思路
我们实现的是用户登录后的购物车,用户将商品加入购物车的时候,直接将要加入购物车的详情存入到Redis即可。每次查看购物车的时候直接从Redis中获取。
(3)表结构分析
用户登录后将商品加入购物车,需要存储商品详情以及购买数量,购物车详情表如下:
数据中orderitem表:
CREATE TABLE `order_item_` (
`id` varchar(20) COLLATE utf8_bin NOT NULL COMMENT 'ID',
`category_id1` int(11) DEFAULT NULL COMMENT '1级分类',
`category_id2` int(11) DEFAULT NULL COMMENT '2级分类',
`category_id3` int(11) DEFAULT NULL COMMENT '3级分类',
`spu_id` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT 'SPU_ID',
`sku_id` bigint(20) NOT NULL COMMENT 'SKU_ID',
`order_id` bigint(20) NOT NULL COMMENT '订单ID',
`name` varchar(200) COLLATE utf8_bin DEFAULT NULL COMMENT '商品名称',
`price` int(20) DEFAULT NULL COMMENT '单价',
`num` int(10) DEFAULT NULL COMMENT '数量',
`money` int(20) DEFAULT NULL COMMENT '总金额',
`pay_money` int(11) DEFAULT NULL COMMENT '实付金额',
`image` varchar(200) COLLATE utf8_bin DEFAULT NULL COMMENT '图片地址',
`weight` int(11) DEFAULT NULL COMMENT '重量',
`post_fee` int(11) DEFAULT NULL COMMENT '运费',
`is_return` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '是否退货',
PRIMARY KEY (`id`),
KEY `item_id` (`sku_id`),
KEY `order_id` (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
购物车详情表其实就是订单详情表结构,只是目前临时存储数据到Redis,等用户下单后才将数据从Redis取出存入到MySQL中。
1.2 搭建订单购物车微服务
因为购物车功能比较简单,这里我们把订单和购物车微服务放在一个工程下
工程结构
1.2.1 pom.xml
legou-order/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>legou-parent</artifactId>
<groupId>com.lxs</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>legou-order</artifactId>
<packaging>pom</packaging>
<modules>
<module>legou-order-interface</module>
<module>legou-order-service</module>
</modules>
</project>
legou-order/legou-order-interface/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>legou-order</artifactId>
<groupId>com.lxs</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>legou-order-interface</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.lxs</groupId>
<artifactId>legou-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.core.Starter</mainClass>
<layout>ZIP</layout>
<classifier>exec</classifier>
<includeSystemScope>true</includeSystemScope>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
legou-order/legou-order-service/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>legou-order</artifactId>
<groupId>com.lxs</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>legou-order-service</artifactId>
<dependencies>
<!-- redis 使用-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>com.lxs</groupId>
<artifactId>legou-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.lxs</groupId>
<artifactId>legou-order-interface</artifactId>
<version>${project.version}</version>
</dependency>
<!--商品微服务-->
<dependency>
<groupId>com.lxs</groupId>
<artifactId>legou-item-instance</artifactId>
<version>${project.version}</version>
</dependency>
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--oauth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
</project>
1.2.2 启动器配置文件
public.key 拷贝即可
legou-order/legou-order-service/src/main/resources/bootstrap.yml
spring:
application:
name: order-service
# 多个接口上的@FeignClient(“相同服务名”)会报错,overriding is disabled。
# 设置 为true ,即 允许 同名
main:
allow-bean-definition-overriding: true
config-repo/order-service.yml
server:
port: 9009
spring:
redis:
host: 192.168.220.110
port: 6379
mybatis-plus:
mapper-locations: classpath*:mybatis/*/*.xml
type-aliases-package: com.lxs.legou.*.po
configuration:
# 下划线驼峰转换
map-underscore-to-camel-case: true
lazy-loading-enabled: true
aggressive-lazy-loading: false
logging:
#file: demo.log
pattern:
console: "%d - %msg%n"
level:
org.springframework.web: debug
com.lxs: debug
security:
oauth2:
resource:
jwt:
key-uri: http://localhost:9098/oauth/token_key #如果使用JWT,可以获取公钥用于 token 的验签
legou-order/legou-order-service/src/main/java/com/legou/order/OrderApplication.java
package com.legou.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableCircuitBreaker
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
1.2.3 配置类
配置类和其他资源微服务工程类似,拷贝修改即可
legou-order/legou-order-service/src/main/java/com/legou/order/config/MybatisPlusConfig.java
package com.legou.order.config;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.optimize.JsqlParserCountOptimize;
import com.github.pagehelper.PageInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("com.lxs.legou.order.dao")
public class MybatisPlusConfig {
/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
// 开启 count 的 join 优化,只针对 left join !!!
return new PaginationInterceptor().setCountSqlParser(new JsqlParserCountOptimize(true));
}
@Bean
public PageInterceptor pageInterceptor() {
return new PageInterceptor();
}
}
legou-order/legou-order-service/src/main/java/com/legou/order/config/JwtConfig.java
package com.legou.order.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.util.FileCopyUtils;
import java.io.IOException;
@Configuration
public class JwtConfig {
public static final String public_cert = "public.key";
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Bean
@Qualifier("tokenStore")
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter);
}
@Bean
protected JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource(public_cert);
String publicKey;
try {
publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
}catch (IOException e) {
throw new RuntimeException(e);
}
converter.setVerifierKey(publicKey); //设置校验公钥
converter.setSigningKey("lxsong"); //设置证书签名密码,否则报错
return converter;
}
}
legou-order/legou-order-service/src/main/java/com/legou/order/config/ResourceServerConfiguration.java
package com.legou.order.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/**").permitAll();
// .antMatchers("/**").authenticated();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore);
}
}
1.3 添加购物车
1.3.1 思路分析
用户添加购物车,将要加入购物车的商品存入到Redis中即可。一个用户可以将多件商品加入购物车,存储到Redis中的数据可以采用Hash类型。
选Hash类型可以将用户的用户名作为namespace,将指定商品加入购物车,则往对应的namespace中增加一个key和value,key是商品ID,value是加入购物车的商品详情,如下图:
1.3.2 代码实现
1.3.0 实体类
订单主表
package com.lxs.legou.order.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.lxs.legou.core.po.BaseEntity;
import lombok.Data;
import java.util.Date;
@Data
@TableName("order_")
public class Order extends BaseEntity {
@TableField("total_num_")
private Integer totalNum;//数量合计
@TableField("total_money_")
private Integer totalMoney;//金额合计
@TableField("pre_money_")
private Integer preMoney;//优惠金额
@TableField("post_fee_")
private Integer postFee;//邮费
@TableField("pay_money_")
private Integer payMoney;//实付金额
@TableField("pay_type_")
private String payType;//支付类型,1、在线支付、0 货到付款
@TableField("create_time_")
private Date createTime;//订单创建时间
@TableField("update_time_")
private Date updateTime;//订单更新时间
@TableField("pay_time_")
private Date payTime;//付款时间
@TableField("consign_time_")
private Date consignTime;//发货时间
@TableField("end_time_")
private Date endTime;//交易完成时间
@TableField("close_time_")
private Date closeTime;//交易关闭时间
@TableField("shipping_name_")
private String shippingName;//物流名称
@TableField("shipping_code_")
private String shippingCode;//物流单号
@TableField("username_")
private String username;//用户名称
@TableField("buyer_message_")
private String buyerMessage;//买家留言
@TableField("buyer_rate_")
private String buyerRate;//是否评价
@TableField("receiver_contact_")
private String receiverContact;//收货人
@TableField("receiver_mobile_")
private String receiverMobile;//收货人手机
@TableField("receiver_address_")
private String receiverAddress;//收货人地址
@TableField("source_type_")
private String sourceType;//订单来源:1:web,2:app,3:微信公众号,4:微信小程序 5 H5手机页面
@TableField("transaction_id_")
private String transactionId;//交易流水号
@TableField("order_status_")
private String orderStatus;//订单状态,0:未完成,1:已完成,2:已退货
@TableField("pay_status_")
private String payStatus;//支付状态,0:未支付,1:已支付,2:支付失败
@TableField("consign_status_")
private String consignStatus;//发货状态,0:未发货,1:已发货,2:已收货
@TableField("is_delete_")
private String isDelete;//是否删除
}
订单明细
package com.lxs.legou.order.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.lxs.legou.core.po.BaseEntity;
import lombok.Data;
@Data
@TableName("order_item_")
public class OrderItem extends BaseEntity {
@TableField("category_id1_")
private Long categoryId1;//1级分类
@TableField("category_id2_")
private Long categoryId2;//2级分类
@TableField("category_id3_")
private Long categoryId3;//3级分类
@TableField("spu_id_")
private Long spuId;//SPU_ID
@TableField("sku_id_")
private Long skuId;//SKU_ID
@TableField("order_id_")
private String orderId;//订单ID
@TableField("name_")
private String name;//商品名称
@TableField("price_")
private Long price;//单价
@TableField("num_")
private Integer num;//数量
@TableField("money_")
private Long money;//总金额
@TableField("pay_money_")
private Long payMoney;//实付金额
@TableField("image_")
private String image;//图片地址
@TableField("weight_")
private Integer weight;//重量
@TableField("post_fee_")
private Integer postFee;//运费
@TableField("is_return_")
private String isReturn;//是否退货,0:未退货,1:已退货
}
1.3.2.1 Feign客户端代理
legou-order/legou-order-service/src/main/java/com/legou/order/client/SkuClient.java
package com.legou.order.client;
import com.lxs.legou.item.api.SkuApi;
import com.lxs.legou.item.po.Sku;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@FeignClient(name = "item-service", fallback = SkuClient.SkuClientFallback.class)
public interface SkuClient extends SkuApi {
@Component
@RequestMapping("/sku-fallback")
//这个可以避免容器中requestMapping重复
class SkuClientFallback implements SkuClient {
private static final Logger LOGGER = LoggerFactory.getLogger(SkuClientFallback.class);
@Override
public List<Sku> selectSkusBySpuId(Long spuId) {
LOGGER.error("异常发生,进入fallback方法");
return null;
}
@Override
public Sku edit(Long id) {
LOGGER.error("异常发生,进入fallback方法");
return null;
}
}
}
legou-order/legou-order-service/src/main/java/com/legou/order/client/SpuClient.java
package com.legou.order.client;
import com.lxs.legou.item.api.SpuApi;
import com.lxs.legou.item.po.Spu;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@FeignClient(name = "item-service", fallback = SpuClient.SpuClientFallback.class)
public interface SpuClient extends SpuApi {
@Component
@RequestMapping("/spu-fallback") //这个可以避免容器中requestMapping重复
class SpuClientFallback implements SpuClient {
private static final Logger LOGGER = LoggerFactory.getLogger(SpuClientFallback.class);
@Override
public List<Spu> selectAll() {
LOGGER.error("异常发生,进入fallback方法");
return null;
}
@Override
public Spu edit(Long id) {
LOGGER.error("异常发生,进入fallback方法");
return null;
}
}
}
1.3.2.2 业务层
业务层接口
legou-order/legou-order-service/src/main/java/com/legou/order/service/CartService.java
package com.legou.order.service;
import com.lxs.legou.order.po.OrderItem;
import java.util.List;
public interface CartService {
/**
* 添加购物车
* @param id sku的ID
* @param num 购买的数量
* @param username 购买的商品的用户名
*/
void add(Long id, Integer num, String username);
}
业务层接口实现类
legou-order/legou-order-service/src/main/java/com/legou/order/service/impl/CartServiceImpl.java
package com.legou.order.service.impl;
import com.legou.order.client.SkuClient;
import com.legou.order.client.SpuClient;
import com.legou.order.service.CartService;
import com.lxs.legou.item.po.Sku;
import com.lxs.legou.item.po.Spu;
import com.lxs.legou.order.po.OrderItem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CartServiceImpl implements CartService {
@Autowired
private SkuClient skuFeign;
@Autowired
private SpuClient spuFeign;
@Autowired
private RedisTemplate redisTemplate;
@Override
public void add(Long id, Integer num, String username) {
//1.根据商品的SKU的ID 获取sku的数据
Sku data = skuFeign.edit(id);
if (data != null) {
//2.根据sku的数据对象 获取 该SKU对应的SPU的数据
Long spuId = data.getSpuId();
Spu spu = spuFeign.edit(spuId);
//3.将数据存储到 购物车对象(order_item)中
OrderItem orderItem = new OrderItem();
orderItem.setCategoryId1(spu.getCid1());
orderItem.setCategoryId2(spu.getCid2());
orderItem.setCategoryId3(spu.getCid3());
orderItem.setSpuId(spu.getId());
orderItem.setSkuId(id);
orderItem.setName(data.getTitle());//商品的名称 sku的名称
orderItem.setPrice(data.getPrice());//sku的单价
orderItem.setNum(num);//购买的数量
orderItem.setPayMoney(orderItem.getNum() * orderItem.getPrice());//单价* 数量
orderItem.setImage(data.getImages());//商品的图片dizhi
//4.数据添加到redis中 key:用户名 field:sku的ID value:购物车数据(order_item)
redisTemplate.boundHashOps("Cart_" + username).put(id, orderItem);// hset key field value hget key field
}
}
}
1.3.2.3 控制层
legou-order/legou-order-service/src/main/java/com/legou/order/controller/CartController.java
package com.legou.order.controller;
import com.legou.order.config.TokenDecode;
import com.legou.order.service.CartService;
import com.lxs.legou.order.po.OrderItem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.List;
@RestController
@RequestMapping("/cart")
@CrossOrigin
public class CartController {
@Autowired
private CartService cartService;
@Autowired
private TokenDecode tokenDecode;
/**
* 添加购物车
*
* @param id 要购买的商品的SKU的ID
* @param num 要购买的数量
* @return
*/
@RequestMapping("/add")
public ResponseEntity add(Long id, Integer num) throws IOException {
//springsecurity 获取当前的用户名 传递service
String username = "lxs";
// Map<String, String> userInfo = tokenDecode.getUserInfo();
// String username = userInfo.get("username");
System.out.println("用户名:"+username);
cartService.add(id, num, username);
return ResponseEntity.ok("添加成功");
}
}
测试添加购物车,效果如下:
请求地址http://localhost:9009/cart/add?num=6&id=2868393
Redis缓存中的数据
1.3.2.4 feign调用异常
可以使用fallbackFactory打印feign调用异常
- 产生FallbackFactory组件
- 在FeignClient注解中通过fallbackFactory属性指定配置上面的组件
package com.legou.order.client;
import com.lxs.legou.item.api.SkuApi;
import com.lxs.legou.item.po.Sku;
import feign.hystrix.FallbackFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@FeignClient(name = "item-service", /*fallback = SkuClient.SkuClientFallback.class*/ fallbackFactory = SkuClient.SkuClientFallbackFactory.class)
public interface SkuClient extends SkuApi {
@Component
@RequestMapping("/sku-fallback")
//这个可以避免容器中requestMapping重复
class SkuClientFallback implements SkuClient {
private static final Logger LOGGER = LoggerFactory.getLogger(SkuClientFallback.class);
@Override
public List<Sku> selectSkusBySpuId(Long spuId) {
LOGGER.error("异常发生,进入fallback方法");
return null;
}
@Override
public Sku edit(Long id) {
LOGGER.error("异常发生,进入fallback方法");
return null;
}
}
@Component
@RequestMapping("/sku-fallback-factory")
class SkuClientFallbackFactory implements FallbackFactory<SkuClient> {
Logger logger = LoggerFactory.getLogger(SkuClientFallbackFactory.class);
@Override
public SkuClient create(Throwable throwable) {
throwable.printStackTrace();
logger.error(throwable.getMessage());
return new SkuClient() {
@Override
public List<Sku> selectSkusBySpuId(Long spuId) {
return null;
}
@Override
public Sku edit(Long id) {
return null;
}
};
}
}
}
1.4 购物车列表
1.4.1 思路分析
接着我们实现一次购物车列表操作。因为存的时候是根据用户名往Redis中存储用户的购物车数据的,所以我们这里可以将用户的名字作为key去Redis中查询对应的数据。
1.4.2 代码实现
(1)业务层
业务层接口
/**
* 从redis中获取当前的用户的购物车的列表数据
* @param username
* @return
*/
List<OrderItem> list(String username);
业务层接口实现类
@Override
public List<OrderItem> list(String username) {
List<OrderItem> orderItemList = redisTemplate.boundHashOps("Cart_" + username).values();
return orderItemList;
}
(2)控制层
@RequestMapping("/list")
public ResponseEntity<List<OrderItem>> list() throws IOException {
String username = "lxs";
// Map<String, String> userInfo = tokenDecode.getUserInfo();
// String username = userInfo.get("username");
System.out.println("哇塞::用户名:"+username);
List<OrderItem> orderItemList = cartService.list(username);
return new ResponseEntity<>(orderItemList, HttpStatus.OK);
}
(3)测试
使用Postman访问 GET http://localhost:9009/cart/list ,效果如下:
1.4.3 问题处理
(1)删除商品购物车
我们发现个问题,就是用户将商品加入购物车,无论数量是正负,都会执行添加购物车,如果数量如果<=0,应该移除该商品的。
修改changgou-service-order的com.changgou.order.service.impl.CartServiceImpl的add方法,添加如下代码:
if(num<=0){
//删除掉原来的商品
redisTemplate.boundHashOps("Cart_" + username).delete(id);
return;
}
2 微服务之间认证
2.1 传递管理员令牌
使用场景:我们在授权中心微服务调用用户微服务时可以直接生成管理员令牌,通过header传递到用户微服务,之前没有实现是因为为了测试方便我们在用户为服务中设置了未认证也可以访问用户微服务
开启认证访问后,处理逻辑应该为:
代码实现:
产生管理员令牌的工具方法:
auth-center/src/main/java/com/service/auth/serviceauth/utils/AdminToken.java
package com.service.auth.serviceauth.utils;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaSigner;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import java.io.IOException;
import java.security.KeyPair;
import java.security.interfaces.RSAPrivateKey;
import java.util.HashMap;
import java.util.Map;
public class AdminToken {
public static String adminToken() throws IOException {
//证书文件
String key_location = "lxsong.jks";
//密钥库密码
String keystore_password = "lxsong";
//访问证书路径
ClassPathResource resource = new ClassPathResource(key_location);
//密钥工厂
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource, keystore_password.toCharArray());
//密钥的密码,此密码和别名要匹配
String keypassword = "lxsong";
//密钥别名
String alias = "lxsong";
//密钥对(密钥和公钥)
KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias, keypassword.toCharArray());
//私钥
RSAPrivateKey aPrivate = (RSAPrivateKey) keyPair.getPrivate();
//定义payload信息
Map<String, Object> tokenMap = new HashMap<String, Object>();
tokenMap.put("user_name", "admin");
tokenMap.put("client_id", "client");
tokenMap.put("authorities", new String[] {"ROLE_ADMIN"});
//生成jwt令牌
Jwt jwt = JwtHelper.encode(new ObjectMapper().writeValueAsString(tokenMap), new RsaSigner(aPrivate));
//取出jwt令牌
String token = jwt.getEncoded();
return token;
}
}
Feign拦截器
auth-center/src/main/java/com/service/auth/serviceauth/interceptor/TokenRequestInterceptor.java
package com.service.auth.serviceauth.interceptor;
import com.service.auth.serviceauth.utils.AdminToken;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class TokenRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
String token = null;
try {
token = AdminToken.adminToken();
} catch (IOException e) {
e.printStackTrace();
}
requestTemplate.header("Authorization", "Bearer " + token);
}
}
2.2 传递当前用户令牌
使用场景:购物车功能已经做完了,但用户我们都是硬编码写死的。用户要想将商品加入购物车,必须得先登录授权,然后通过header传递令牌到购物车微服务,购物车微服务通过Feign拦截器添加令牌传递到商品微服务,如下图所示:
(1)创建拦截器
legou-order/legou-order-service/src/main/java/com/legou/order/interceptor/MyFeignInterceptor.java
package com.legou.order.interceptor;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
@Component
public class MyFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
//1.获取请求对象
HttpServletRequest request = requestAttributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
//2.获取请求对象中的所有的头信息(请求传递过来的)
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();//头的名称
String value = request.getHeader(name);//头名称对应的值
System.out.println("name:" + name + "::::::::value:" + value);
//3.将头信息传递给fegin (restTemplate)
requestTemplate.header(name,value);
}
}
}
}
}
(3)测试
我们发现这块的ServletRequestAttributes始终为空,RequestContextHolder.getRequestAttributes()该方法是从ThreadLocal变量里面取得相应信息的,当hystrix断路器的隔离策略为THREAD时,是无法取得ThreadLocal中的值。
解决方案:hystrix隔离策略换为SEMAPHORE
config-repo/application.yml
feign:
hystrix:
enabled: true #开启熔断
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 60000 #熔断超时时间
strategy: SEMAPHORE
再次测试,效果如下:
2.3 获取用户数据
2.3.1 数据分析
用户登录后,数据会封装到SecurityContextHolder.getContext().getAuthentication()
里面,我们可以将数据从这里面取出,然后转换成OAuth2AuthenticationDetails
,在这里面可以获取到令牌信息、令牌类型等,代码如下:
这里的tokenValue是加密之后的令牌数据,remoteAddress是用户的IP信息,tokenType是令牌类型。
我们可以获取令牌加密数据后,使用公钥对它进行解密,如果能解密说明说句无误,如果不能解密用户也没法执行到这一步。解密后可以从明文中获取用户信息。
2.3.2 代码实现
(1) 工具类
legou-order/legou-order-service/src/main/java/com/legou/order/config/TokenDecode.java
package com.legou.order.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.stream.Collectors;
@Component
public class TokenDecode {
private static final String PUBLIC_KEY = "public.key";
@Autowired
private ObjectMapper objectMapper;
// 获取令牌
public String getToken() {
OAuth2AuthenticationDetails authentication = (OAuth2AuthenticationDetails) SecurityContextHolder.getContext().getAuthentication().getDetails();
String tokenValue = authentication.getTokenValue();
return tokenValue;
}
/**
* 获取当前的登录的用户的用户信息
*
* @return
*/
public Map<String, String> getUserInfo() throws IOException {
//1.获取令牌
String token = getToken();
//2.解析令牌 公钥
String pubKey = getPubKey();
Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(pubKey));
String claims = jwt.getClaims();//{}
System.out.println(claims);
//3.返回
// Map<String,String> map = JSON.parseObject(claims, Map.class);
Map<String, String> map = objectMapper.readValue(claims, Map.class);
return map;
}
private String getPubKey() {
Resource resource = new ClassPathResource(PUBLIC_KEY);
try {
InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
BufferedReader br = new BufferedReader(inputStreamReader);
return br.lines().collect(Collectors.joining("\n"));
} catch (IOException ioe) {
return null;
}
}
}
(2) 控制器
(5)测试
使用postman携带令牌访问