购物车服务
一、环境搭建
- 新增本地域名
cart.achangmall.com
- 修改对应pom配置,与此次服务间版本对应
- 引入公共依赖
- 修改springboot版本
- 修改springcloud版本
- 修改页面对应的资源地址,修改只选nginx的静态地址,如:
- 主启动类com.achang.achangmall.cart.AchangmallCartApplication
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableFeignClients
@EnableDiscoveryClient
public class AchangmallCartApplication {
public static void main(String[] args) {
SpringApplication.run(AchangmallCartApplication.class, args);
}
}
- 配置文件achangmall-cart/src/main/resources/application.yaml
spring:
cloud:
loadbalancer:
ribbon:
enabled: false
nacos:
discovery:
server-addr: localhost:8848
thymeleaf:
cache: false
redis:
host: 192.168.109.101
port: 6379
application:
name: achangmall-cart
server:
port: 30000
- 网关配置路由映射achangmall-gateway/src/main/resources/application.yml
- id: cart_route
uri: lb://achangmall-cart
predicates:
- Host=cart.achangmall.com
- 启动服务测试,访问:
http://cart.achangmall.com/
二、业务
- com.achang.achangmall.cart.vo.CartVo
public class CartVo {
List<CartItemVo> items;
private Integer countNum;
private Integer countType;
private BigDecimal totalAmount;
private BigDecimal reduce = new BigDecimal("0.00");;
public List<CartItemVo> getItems() {
return items;
}
public void setItems(List<CartItemVo> items) {
this.items = items;
}
public Integer getCountNum() {
int count = 0;
if (items != null && items.size() > 0) {
for (CartItemVo item : items) {
count += item.getCount();
}
}
return count;
}
public Integer getCountType() {
int count = 0;
if (items != null && items.size() > 0) {
for (CartItemVo item : items) {
count += 1;
}
}
return count;
}
public BigDecimal getTotalAmount() {
BigDecimal amount = new BigDecimal("0");
if (!CollectionUtils.isEmpty(items)) {
for (CartItemVo cartItem : items) {
if (cartItem.getCheck()) {
amount = amount.add(cartItem.getTotalPrice());
}
}
}
return amount.subtract(getReduce());
}
public BigDecimal getReduce() {
return reduce;
}
public void setReduce(BigDecimal reduce) {
this.reduce = reduce;
}
}
- com.achang.achangmall.cart.vo.CartItemVo
public class CartItemVo {
private Long skuId;
private Boolean check = true;
private String title;
private String image;
private List<String> skuAttrValues;
private BigDecimal price;
private Integer count;
private BigDecimal totalPrice;
public Long getSkuId() {
return skuId;
}
public void setSkuId(Long skuId) {
this.skuId = skuId;
}
public Boolean getCheck() {
return check;
}
public void setCheck(Boolean check) {
this.check = check;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public List<String> getSkuAttrValues() {
return skuAttrValues;
}
public void setSkuAttrValues(List<String> skuAttrValues) {
this.skuAttrValues = skuAttrValues;
}
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() {
return this.price.multiply(new BigDecimal("" + this.count));
}
public void setTotalPrice(BigDecimal totalPrice) {
this.totalPrice = totalPrice;
}
}
- com.achang.achangmall.cart.vo.SkuInfoVo
@Data
public class SkuInfoVo {
private Long skuId;
private Long spuId;
private String skuName;
private String skuDesc;
private Long catalogId;
private Long brandId;
private String skuDefaultImg;
private String skuTitle;
private String skuSubtitle;
private BigDecimal price;
private Long saleCount;
}
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
dependency>
- com.achang.achangmall.cart.config.AchangmallSessionConfig
@Configuration
@EnableRedisHttpSession
public class AchangmallSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("achangmall.com");
cookieSerializer.setCookieName("ACHANGSESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
- com.achang.achangmall.cart.config.MyThreadConfig
@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
@Configuration
public class MyThreadConfig {
@Bean
public ThreadPoolExecutor executor(ThreadPoolConfigProperties pool) {
return new ThreadPoolExecutor(
pool.getCoreSize(),
pool.getMaxSize(),
pool.getKeepAliveTime(),
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(100000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
}
}
- com.achang.achangmall.cart.config.ThreadPoolConfigProperties
@ConfigurationProperties(prefix = "achangmall.thread")
@Data
public class ThreadPoolConfigProperties {
private Integer coreSize;
private Integer maxSize;
private Integer keepAliveTime;
}
- com.achang.achangmall.cart.exception.RuntimeExceptionHandler
@ControllerAdvice
public class RuntimeExceptionHandler {
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public R handler(RuntimeException exception) {
return R.error(exception.getMessage());
}
@ExceptionHandler(CartExceptionHandler.class)
public R userHandler(CartExceptionHandler exception) {
return R.error("购物车无此商品");
}
}
- com.achang.achangmall.cart.exception.CartExceptionHandler
public class CartExceptionHandler extends RuntimeException {
}
- com.achang.achangmall.cart.feign.ProductFeignService
@FeignClient("achangmall-product")
public interface ProductFeignService {
@RequestMapping("/product/skuinfo/info/{skuId}")
R getInfo(@PathVariable("skuId") Long skuId);
@GetMapping(value = "/product/skusaleattrvalue/stringList/{skuId}")
List<String> getSkuSaleAttrValues(@PathVariable("skuId") Long skuId);
@GetMapping(value = "/product/skuinfo/{skuId}/price")
BigDecimal getPrice(@PathVariable("skuId") Long skuId);
}
- com.achang.achangmall.cart.intercept.CartIntercept
@Data
public class UserInfoTo {
private Long userId;
private String userKey;
private Boolean tempUser = false;
}
- 拦截器com.achang.achangmall.cart.intercept.CartIntercept
@Component
public class CartIntercept implements HandlerInterceptor {
public static ThreadLocal<UserInfoTo> toThreadLocal = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
UserInfoTo userInfoTo = new UserInfoTo();
HttpSession session = request.getSession();
MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(LOGIN_USER);
if (memberResponseVo != null) {
userInfoTo.setUserId(memberResponseVo.getId());
}
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
String name = cookie.getName();
if (name.equals(TEMP_USER_COOKIE_NAME)) {
userInfoTo.setUserKey(cookie.getValue());
userInfoTo.setTempUser(true);
}
}
}
if (StringUtils.isEmpty(userInfoTo.getUserKey())) {
String uuid = UUID.randomUUID().toString();
userInfoTo.setUserKey(uuid);
}
toThreadLocal.set(userInfoTo);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
UserInfoTo userInfoTo = toThreadLocal.get();
if (!userInfoTo.getTempUser()) {
Cookie cookie = new Cookie(TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
cookie.setDomain("achangmall.com");
cookie.setMaxAge(TEMP_USER_COOKIE_TIMEOUT);
response.addCookie(cookie);
}
toThreadLocal.remove();
}
}
- com.achang.achangmall.cart.to.UserInfoTo
@Data
public class UserInfoTo {
private Long userId;
private String userKey;
private Boolean tempUser = false;
}
- com.achang.common.constant.CartConstant
public class CartConstant {
public final static String TEMP_USER_COOKIE_NAME = "user-key";
public final static int TEMP_USER_COOKIE_TIMEOUT = 60*60*24*30;
public final static String CART_PREFIX = "achangmall:cart:";
}
- com.achang.achangmall.cart.intercept.AchangmallWebConfig配置拦截器,让其生效
@Configuration
public class AchangmallWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CartIntercept())
.addPathPatterns("/**");
}
}
- achangmall-product/src/main/resources/mapper/product/SkuSaleAttrValueDao.xml
<select id="getSkuSaleAttrValuesAsStringList" resultType="java.lang.String">
SELECT
CONCAT( attr_name, ":", attr_value )
FROM
pms_sku_sale_attr_value
WHERE
sku_id = #{skuId}
select>
- com.achang.achangmall.product.service.impl.SkuSaleAttrValueServiceImpl
@Override
public List<String> getSkuSaleAttrValuesAsStringList(Long skuId) {
SkuSaleAttrValueDao baseMapper = this.baseMapper;
List<String> stringList = baseMapper.getSkuSaleAttrValuesAsStringList(skuId);
return stringList;
}
- com.achang.achangmall.product.app.SkuSaleAttrValueController
@GetMapping(value = "/stringList/{skuId}")
public List<String> getSkuSaleAttrValues(@PathVariable("skuId") Long skuId) {
List<String> stringList = skuSaleAttrValueService.getSkuSaleAttrValuesAsStringList(skuId);
return stringList;
}
- com.achang.achangmall.cart.controller.CartController
@Controller
public class CartController {
@Resource
private CartService cartService;
@GetMapping(value = "/currentUserCartItems")
@ResponseBody
public List<CartItemVo> getCurrentCartItems() {
List<CartItemVo> cartItemVoList = cartService.getUserCartItems();
return cartItemVoList;
}
@GetMapping(value = "/cart.html")
public String cartListPage(Model model) throws ExecutionException, InterruptedException {
CartVo cartVo = cartService.getCart();
model.addAttribute("cart",cartVo);
return "cartList";
}
@GetMapping(value = "/addCartItem")
public String addCartItem(@RequestParam("skuId") Long skuId,
@RequestParam("num") Integer num,
RedirectAttributes attributes) throws ExecutionException, InterruptedException {
cartService.addToCart(skuId,num);
attributes.addAttribute("skuId",skuId);
return "redirect:http://cart.achangmall.com/addToCartSuccessPage.html";
}
@GetMapping(value = "/addToCartSuccessPage.html")
public String addToCartSuccessPage(@RequestParam("skuId") Long skuId,Model model) {
CartItemVo cartItemVo = cartService.getCartItem(skuId);
model.addAttribute("cartItem",cartItemVo);
return "success";
}
@GetMapping(value = "/checkItem")
public String checkItem(@RequestParam(value = "skuId") Long skuId,
@RequestParam(value = "checked") Integer checked) {
cartService.checkItem(skuId,checked);
return "redirect:http://cart.achangmall.com/cart.html";
}
@GetMapping(value = "/countItem")
public String countItem(@RequestParam(value = "skuId") Long skuId,
@RequestParam(value = "num") Integer num) {
cartService.changeItemCount(skuId,num);
return "redirect:http://cart.achangmall.com/cart.html";
}
@GetMapping(value = "/deleteItem")
public String deleteItem(@RequestParam("skuId") Integer skuId) {
cartService.deleteIdCartInfo(skuId);
return "redirect:http://cart.achangmall.com/cart.html";
}
}
- com.achang.achangmall.cart.service.impl.CartServiceImpl
@Slf4j
@Service("cartService")
public class CartServiceImpl implements CartService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ProductFeignService productFeignService;
@Autowired
private ThreadPoolExecutor executor;
@Override
public CartItemVo addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
String productRedisValue = (String) cartOps.get(skuId.toString());
if (StringUtils.isEmpty(productRedisValue)) {
CartItemVo cartItemVo = new CartItemVo();
CompletableFuture<Void> getSkuInfoFuture = CompletableFuture.runAsync(() -> {
R productSkuInfo = productFeignService.getInfo(skuId);
SkuInfoVo skuInfo = productSkuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
cartItemVo.setSkuId(skuInfo.getSkuId());
cartItemVo.setTitle(skuInfo.getSkuTitle());
cartItemVo.setImage(skuInfo.getSkuDefaultImg());
cartItemVo.setPrice(skuInfo.getPrice());
cartItemVo.setCount(num);
}, executor);
CompletableFuture<Void> getSkuAttrValuesFuture = CompletableFuture.runAsync(() -> {
List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);
cartItemVo.setSkuAttrValues(skuSaleAttrValues);
}, executor);
CompletableFuture.allOf(getSkuInfoFuture, getSkuAttrValuesFuture).get();
String cartItemJson = JSON.toJSONString(cartItemVo);
cartOps.put(skuId.toString(), cartItemJson);
return cartItemVo;
} else {
CartItemVo cartItemVo = JSON.parseObject(productRedisValue, CartItemVo.class);
cartItemVo.setCount(cartItemVo.getCount() + num);
String cartItemJson = JSON.toJSONString(cartItemVo);
cartOps.put(skuId.toString(),cartItemJson);
return cartItemVo;
}
}
@Override
public CartItemVo getCartItem(Long skuId) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
String redisValue = (String) cartOps.get(skuId.toString());
CartItemVo cartItemVo = JSON.parseObject(redisValue, CartItemVo.class);
return cartItemVo;
}
@Override
public CartVo getCart() throws ExecutionException, InterruptedException {
CartVo cartVo = new CartVo();
UserInfoTo userInfoTo = CartIntercept.toThreadLocal.get();
if (userInfoTo.getUserId() != null) {
String cartKey = CART_PREFIX + userInfoTo.getUserId();
String temptCartKey = CART_PREFIX + userInfoTo.getUserKey();
List<CartItemVo> tempCartItems = getCartItems(temptCartKey);
if (tempCartItems != null) {
for (CartItemVo item : tempCartItems) {
addToCart(item.getSkuId(),item.getCount());
}
clearCartInfo(temptCartKey);
}
List<CartItemVo> cartItems = getCartItems(cartKey);
cartVo.setItems(cartItems);
} else {
String cartKey = CART_PREFIX + userInfoTo.getUserKey();
List<CartItemVo> cartItems = getCartItems(cartKey);
cartVo.setItems(cartItems);
}
return cartVo;
}
private BoundHashOperations<String, Object, Object> getCartOps() {
UserInfoTo userInfoTo = CartIntercept.toThreadLocal.get();
String cartKey = "";
if (userInfoTo.getUserId() != null) {
cartKey = CART_PREFIX + userInfoTo.getUserId();
} else {
cartKey = CART_PREFIX + userInfoTo.getUserKey();
}
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
return operations;
}
private List<CartItemVo> getCartItems(String cartKey) {
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
List<Object> values = operations.values();
if (values != null && values.size() > 0) {
List<CartItemVo> cartItemVoStream = values.stream().map((obj) -> {
String str = (String) obj;
CartItemVo cartItem = JSON.parseObject(str, CartItemVo.class);
return cartItem;
}).collect(Collectors.toList());
return cartItemVoStream;
}
return null;
}
@Override
public void clearCartInfo(String cartKey) {
redisTemplate.delete(cartKey);
}
@Override
public void checkItem(Long skuId, Integer check) {
CartItemVo cartItem = getCartItem(skuId);
cartItem.setCheck(check == 1?true:false);
String redisValue = JSON.toJSONString(cartItem);
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
cartOps.put(skuId.toString(),redisValue);
}
@Override
public void changeItemCount(Long skuId, Integer num) {
CartItemVo cartItem = getCartItem(skuId);
cartItem.setCount(num);
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
String redisValue = JSON.toJSONString(cartItem);
cartOps.put(skuId.toString(),redisValue);
}
@Override
public void deleteIdCartInfo(Integer skuId) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
cartOps.delete(skuId.toString());
}
@Override
public List<CartItemVo> getUserCartItems() {
List<CartItemVo> cartItemVoList = new ArrayList<>();
UserInfoTo userInfoTo = CartIntercept.toThreadLocal.get();
if (userInfoTo.getUserId() == null) {
return null;
} else {
String cartKey = CART_PREFIX + userInfoTo.getUserId();
List<CartItemVo> cartItems = getCartItems(cartKey);
if (cartItems == null) {
throw new CartExceptionHandler();
}
cartItemVoList = cartItems.stream()
.filter(items -> items.getCheck())
.map(item -> {
BigDecimal price = productFeignService.getPrice(item.getSkuId());
item.setPrice(price);
return item;
})
.collect(Collectors.toList());
}
return cartItemVoList;
}
}