dubbo项目开发全记录

应该说这个项目完成的不是特别理想,有一些模块还没完成,但单点登录、redis缓存、购物车的设计部分值得参考,以及项目如何分包,结构的描述都挺详细的,对于微服务分布式入门来说,还是挺有价值的。
可以访问我个人网站hofe 的个人网站,阅读体验更佳,需要源代码,有疑问的也可在评论区留言

项目介绍

用到的技术
微服务架构:Dubbo+zookeeper
中间件:SpringBoot+SpringMVC+Sprint+MyBatis
数据库:MySQL
分布式文件系统:FastDFS(未完成)
搜索引擎:Solr(未完成)
分布式缓存:Redis
消息中间件:RabbitMQ(未完成)

一、项目骨架搭建

1.1 新建一个maven项目以及多个maven模块

只作为一个目录使用,不需要配置pom,可以删除src文件。

如在dubbo-shop项目下新建shop-service模块,将Parent设为none。同样步骤创建shop-basic、shop-api、shop-web模块。
在这里插入图片描述
在这里插入图片描述

1.1.1 在maven模块下新建maven模块

在shop-basic模块中新建entity,需要注意的是entity模块目录在shop-basic至下。同理创建common、mapper模块
在这里插入图片描述

1.1.2 在maven模块下新建springboot模块

在shop-service下新建springboot模块。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.2 初始项目结构

dubbo-shop

  • shop-api
  • shop-service
    • profuct-service(SpringBoot)
  • shop-web
    • backend(SpringBoot)
  • shop-basic
    • entity
    • common 存放公共组件如dao的泛型接口
    • dao
      在这里插入图片描述

1.3 basic中的公用组件

1.3.1 common组件

IBaseDao和IBaseService

由于增删查改等操作比较频繁且可重用性大,故在common模块中新建com.hofe.common.base目录,新建dao与service接口;在com.hofe.common.impl中实现service接口的抽象类。
dao与service接口代码相同

public interface IBaseDao<T> {

    T queryById(Long id);

    List<T> queryAllByLimit(int offset, int limit);

    List<T> queryAll();

    int insert(T record);


    int update(T record);


    int deleteById(Long id);
}
serviceImpl

由于使用泛型,无法返回准确类型,故将其abstract抽象,由继承的子类返回类型。

import com.hofe.common.base.IBaseDao;
import com.hofe.common.base.IBaseService;

import java.util.List;

public abstract class BaseServiceImpl<T> implements IBaseService<T> {

    public abstract IBaseDao<T> getBaseDao();

    public T queryById(Long id) {
        return getBaseDao().queryById(id);
    }

    public List<T> queryAllByLimit(int offset, int limit) {
        return getBaseDao().queryAllByLimit(offset, limit);
    }

    public List<T> queryAll(){
        return getBaseDao().queryAll();
    }

    public int insert(T record) {
        return getBaseDao().insert(record);
    }

    public int update(T record) {
        return getBaseDao().update(record);
    }

    public int deleteById(Long id) {
        return getBaseDao().deleteById(id);
    }

1.4 记录下用到的端口号

dubbo管理中心端口:2180
zookeeper端口:2181
fdfs文件服务器:
tracker server port: 22122
storage_server: 23000
RabbitMQ: 15672(admin:admin)

二、商品类别接口服务

2.1 basic模块配置

这一模块我后来回头来看,其实不是特别需要,虽然重用了很多代码,但对小规模的项目来说,可以不需要,不利于项目的维护。

2.4.1 entity配置

可以用easycode插件根据数据库生成实体类。注意类需要实现Serializable接口

2.4.2 dao配置

dao类

public interface TProductTypeDao extends IBaseDao<TProductType>{
}

由于IBaseService有具体实现故这里可以省略。

dao.xml
这里需要注意的是xml配置的路径是否正确、是否在bean扫描路径下、表字段名name、desc会冲突等问题
在这里插入图片描述
pom配置
由于用到entity、common中的基本类,故需引入模块;

<dependencies>

        <!--集成mybatis-->
        <!--集成事务-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>


        <dependency>
            <groupId>com.hofe</groupId>
            <artifactId>entity</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.hofe</groupId>
            <artifactId>common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

2.2 api接口模块配置

api服务接口
集成common中的基本业务接口,后续该接口由Service实现。

import com.hofe.common.base.IBaseService;
import com.hofe.entity.TProductType;

// 商品类别接口, 集成common中的Service,通过泛型确定类,再由service模块实现api模块的这个接口
public interface IProductTypeService extends IBaseService<TProductType>{
}

pom配置
由于需要TProductType、IBaseService故引入entity、common依赖。

    <dependencies>
        <dependency>
            <groupId>com.hofe</groupId>
            <artifactId>common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.hofe</groupId>
            <artifactId>entity</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

2.3 服务提供者配置

启动类
加入@EnableDubbo注解以及需要加入@MapperScan(“com.hofe.dao”)注解

@SpringBootApplication
@EnableDubbo
@MapperScan("com.hofe.dao")
public class ProductServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(ProductServiceApplication.class, args);
	}

}

接口Service实现类
依赖Dao层,调用相应dao执行具体业务。这里的getBaseDao()方法用于dao被调用时返回具体dao类型(抽象类、接口用的泛型)

@Component
@Service    // dubbo的service
public class ProductTypeService extends BaseServiceImpl<TProductType> implements IProductTypeService {

    @Autowired
    private TProductTypeDao productTypeDao;

    @Override
    public IBaseDao<TProductType> getBaseDao() {
        return productTypeDao;
    }
}

application.yml


server:
  port: 8080
dubbo:
  application:
    name: product-service
  registry:
    protocol: zookeeper
    address: ip:2181
  protocol: 28801

spring:
  datasource: # 数据库配置
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://ip:3306/dubbo_shop?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&failOverReadOnly=false&maxReconnects=10
    username: root
    password: ***
    hikari:
      maximum-pool-size: 10 # 最大连接池数
      max-lifetime: 1770000

mybatis:
  # 指定别名设置的包为所有entity
  type-aliases-package: cn.hofe.entity
  configuration:
    map-underscore-to-camel-case: true # 驼峰命名规范
  mapper-locations: # mapper映射文件位置
    - classpath:mapper/*.xml

pom配置
加入api、dao依赖包也会加入它们各自的依赖包;所以即使用到mybatis也可以不用引入依赖。

<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>

		<!-- Dubbo依赖-->
		<dependency>
			<groupId>com.alibaba.boot</groupId>
			<artifactId>dubbo-spring-boot-starter</artifactId>
			<version>0.2.0</version>
		</dependency>

		<dependency>
			<groupId>com.hofe</groupId>
			<artifactId>product-api</artifactId>
			<version>1.0-SNAPSHOT</version>
		</dependency>
		<dependency>
			<groupId>com.hofe</groupId>
			<artifactId>dao</artifactId>
			<version>1.0-SNAPSHOT</version>
		</dependency>
		
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

2.3 服务消费者配置

启动类
在启动类中添加@EnableDubbo注解

import com.alibaba.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@EnableDubbo
@SpringBootApplication
public class BackendApplication {

	public static void main(String[] args) {
		SpringApplication.run(BackendApplication.class, args);
	}

}

controller
在Controller中依赖处添加@Reference应用。将由服务提供者的Service执行具体业务。

import com.alibaba.dubbo.config.annotation.Reference;
import com.hofe.api.product.IProductTypeService;
import com.hofe.entity.TProductType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("productType")
public class ProductTypeController {

    @Reference
    private IProductTypeService productTypeService;     // 引用 Servic模块的服务

    @GetMapping("list")
    public List<TProductType> productTypeList(){
        return productTypeService.queryAll();
    }
}

application.yml

server:
  port: 9090
dubbo:
  application:
    name: backend
  registry:
    protocol: zookeeper
    address: ip:2181

pom配置
由于需要用到服务消费者故需引入api模块

<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>

		<!-- Dubbo依赖-->
		<dependency>
			<groupId>com.alibaba.boot</groupId>
			<artifactId>dubbo-spring-boot-starter</artifactId>
			<version>0.2.0</version>
		</dependency>

		<dependency>
			<groupId>com.hofe</groupId>
			<artifactId>product-api</artifactId>
			<version>1.0-SNAPSHOT</version>
		</dependency>

	</dependencies>

三、RabbitMQ消息队列(未完成)

3.1 消息队列的应用场景

异步、削峰、解耦。详情点击
(1) 通过异步处理提高系统性能(削峰、减少响应所需时间)
比如秒杀背景下的商品下单,在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即返回,再由消息队列的消费者进程从消息队列中获取数据,异步的对数据库进行操作。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。

消息队列具有很好的削峰作用的功能——即通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。

用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败。因此使用消息队列进行异步处理之后,需要适当修改业务流程进行配合,比如用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功,以免交易纠纷。这就类似我们平时手机订火车票和电影票。

(2) 降低系统耦合性

我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。

通过将系统同步交互改为异步交互提高系统的处理的吞吐量
发送者:
1、声明交换机
2、发送消息到交换机

接受者者:
1、声明队列
2、建立队列与交换机的绑定关系
3、创建一个类的方法来监听队列,接收消息

四、单点登陆

4.1 登录方案设计

4.1.1 前后端未分离下的登陆系统的设计

Session
服务端提供了一种叫 Session 的机制,对于每个用户的请求,会生成一个唯一的标识。当程序需要为某个客户端的请求创建一个 session 的时候,服务器首先检查这个客户端的请求是否包含了一个 session 标识(session id)。如果已包含一个 session id 则说明以前已经为客户端创建过 session,服务器就按照 session id 把这个 session 检索出来使用。
如果客户端请求不包含 session id,则为此客户端创建一个session 并且生成一个与此 session 相关联的 session id,session id 的值是一个既不会重复,又不容易被找到规律的字符串。
Cookie
浏览器提供了一种叫 cookie 的机制,保存当前会话的唯一标识。每次 HTTP 请求,客户端都会发送相应的 cookie 信息到服务端。客户端第一次请求,由于 cookie 中并没有携带 session id,服务端会创建一个session id,并写入到客户端的 cookie 中。以后每次请求,客户端都会携带这个 id 发给服务器端。这样一来,便解决了无状态的问题。

通过设置浏览器的cookie(sessionid),映射服务器的用户session来判断用户登录状况:每次点击登录按钮时后台的response将sessionid的cookie添加到浏览器,后面所有访问请求都带着该cookie,shiro的相关方法就会获取cookie匹配session确定用户登录信息。

4.1.2前后端分离下的登陆系统设计

前后端分离的核心概念是后端仅返回前端所需的数据,不再渲染HTML页面,前端HTML页面通过AJAX调用后端的RESTFUL API接口并使用JSON数据进行交互

目前大多数都采用请求头携带 Token 的形式。

1、首次登录时,后端服务器判断用户账号密码正确之后,根据用户id、用户名、定义好的秘钥、过期时间 生成 token ,返回给前端
2、前端拿到后端返回的 token ,存储在 localStroage 里
3、前端每次路由跳转, 判断 localStroage 有无 token ,没有则跳转到登录页,有则请求获取用户信息,改变登录状态
4、每次请求接口,在 请求头里携带 token
5、后端接口 判断 请求头有无 token,没有或者 token 过期,返回401
6、前端得到 401 状态码,重定向 到登录页面

4.1.3 微服务分布式下的登陆系统设计

单点登录就是在A系统登录以后,跳转到B系统,此时可以直接访问B系统的资源,即只需要登录一次,就可以访问其他相互信任的应用系统,免除多次登录的烦恼。实现单点登录说到底就是要解决如何产生和存储那个信任,再就是其他系统如何验证这个信任的有效性,因此要点也就以下两个:存储信任、验证信任。

4.2 服务消费者配置

SSOController.java
用户登录
通过前端传递的user信息,与数据库交互判断是否合法,若合法生成uuid,创建名为"user_token",内容为uuid的cookie,设置cookie的域名为父域名,这样所有子域名系统都可以访问该cookie,解决cookie的跨域问题等等,并将其通过HttpServletResponse添加cookie到浏览器中。同时,通过redisTemplate设置key为user:token:uuid,值为该用户信息的redis数据,并设置过期时间,这样redis服务器就会保存本次登陆的信息。

@PostMapping("checkLogin")
    public String checkLogin(TUser user, HttpServletResponse response){
        TUser currentUser = userService.checkLogin(user);
        if(currentUser == null){
            return "fail";
        }
        String uuid = UUID.randomUUID().toString();
        // 在浏览器中可以看到user_token对应的uuid
        Cookie cookie = new Cookie("user_token", uuid);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        // redis中的key位user:token:uuid
        StringBuilder redisKey = new StringBuilder("user:token:").append(uuid);

        redisTemplate.setValueSerializer(new StringRedisSerializer());
        // 客户端得到uuid,就可以用user:token:uuid从redis中取得currentUser信息
        redisTemplate.opsForValue().set(redisKey, currentUser);
        //设置有效期
        redisTemplate.expire(redisKey.toString(), 30, TimeUnit.MINUTES);
        response.addCookie(cookie);

        return "login success";
    }

判断是否登录
通过HttpServletRequest获取请求报文带来的cookies信息,判断是否包含user_token,若包含则获取该cookie对应的uuid,通过键user:token:uuid获取redis保存的用户信息,并刷新其有效期。

@PostMapping("checkIsLogin")
    public String checkIsLogin(HttpServletRequest request){
        Cookie[] cookies = request.getCookies();
        if(cookies == null){
            return "用户未登陆";
        }
        for (Cookie cookie : cookies){
            if("user_token".equals(cookie.getName())){
                String uuid = cookie.getValue();
                StringBuilder redisKey = new StringBuilder("user:token:").append(uuid);

                TUser currentUser = (TUser) redisTemplate.opsForValue().get(redisKey.toString());
                if(currentUser != null){
                    //刷新有效期
                    redisTemplate.expire(redisKey.toString(), 30, TimeUnit.MINUTES);
                    return currentUser.getUsername();
                }
            }
        }
        return "用户为登陆";
    }

注销
通过uuid新建cookie,并设置其失效,重新添加回浏览器的客户端,同时也要删除其对应的redis数据。

@PostMapping("logout")
    public String logout(@CookieValue(name = "user_token", required = false) String uuid,
                         HttpServletResponse response){

        if(uuid == null){
            return "注销失败";
        }

        Cookie cookie = new Cookie("user_token", uuid);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        // 使cookie失效
        cookie.setMaxAge(0);
        response.addCookie(cookie);

        // 删除redis
        StringBuilder redisKey = new StringBuilder("user:token:").append(uuid);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.delete(redisKey.toString());

        return "注销成功";
    }

pom配置
与普通的消费者配置差不多,只是多了redis引入。

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>

application.yml配置


server:
  port: 9099
dubbo:
  application:
    name: sso
  registry:
    protocol: zookeeper
    address: ip:2181
  consumer:
    timeout: 30000
spring:
  redis:
    host: ip
    port: 6379
    jedis:
      pool:
        max-active: 20

4.3 服务提供者配置

UserService.java
和普通的提供者一样,数据库的增删查改操作。

application.yml配置

dubbo:
  application:
    name: user-service
  registry:
    protocol: zookeeper
    address: ip:2181
  protocol: 28808
  provider:
    timeout: 30000

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://ip:3306/dubbo_shop?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&failOverReadOnly=false&maxReconnects=10
    username: root
    password: 123456
    hikari:
      maximum-pool-size: 10 
      max-lifetime: 1770000

mybatis:
  # entity路径
  type-aliases-package: com.hofe
  configuration:
    map-underscore-to-camel-case: true
  mapper-locations: # mapper资源路径
    - classpath:mapper/*.xml

五、购物车

5.1 购物车方案设计

需要实现以下功能:
1、未登录时可操作购物车且退出浏览器之后依然存在信息
2、未登录时购物车支持增删查改、排序商品
3、登陆时自动合并未登录与登陆时的商品,且未登录时购物车商品将清空。
4、商品信息变更,购物车也需变更
5、已登录购物车在任何地方信息都一样

未登录购物车实现方案一:cookie
用list保存购物车中的商品信息和对应数量的对象,再将其转化为json,最后用cookie保存购物车和list的键值对。但如果商品信息变更,cookie保存的是商品的信息,则无法修改,故最好改成商品id与数量的对应关系。
不足:
1、展示购物车需要查看数据库,可以把20%的商品放到缓存中(缓存预热)
2、cookie的物理上限4k
3、更新删除操作需要遍历

未登录购物车实现方案二:cookie+redis
购物车保存信息:每一项(id+数量+操作时间),购物车包含多项。后端使用redis存储购物车信息,并做热门商品的缓存。前端cookie保存信息简化,只作保存凭证(user_cart----uuid)。这样后端也省略了json转换。cookie不是会话级cookie,否则退出浏览器即消失。
不足:更新需要遍历

已登录购物车方案一:数据库
每个用户一辆购物车,字段需要有id\user_id\product_id\count\update,每行是一条购物车记录,user_id和product_id是唯一约束。

已登录购物车方案二:redis
每个用户一辆购物车,user:cart:uuid改为user:cart:userid
控制层通过判断用户是否登录决定用user:cart:uuid还是user:cart:userid,从客户端到controller之间可以加个拦截器,任何时候都放行,只记录状态,放入request中。
好处:性能
坏处:虽然是持久化机制,但可能有数据间歇性丢失

合并购物车方案:
通过user:cart:uuid/userId的方式判断有无登陆状态下的购物车,通过遍历两种购物车,用map存商品id对应的表项,最后合并成一个list,同时还需清除原先的cookie,因为未登录状态下的购物车信息是由cookie保存的。

5.2 服务提供者配置

未登录时购物车的Service

package com.hofe.cartservice.service;

import com.alibaba.dubbo.config.annotation.Service;
import com.hofe.api.cart.ICartService;
import com.hofe.api.cart.pojo.CartItem;


@Service
public class CartServiceImpl implements ICartService {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private TProductDao productDao;

    @Override
    public String add(String key, Long productId, Integer count) {
        List<CartItem> cart = (List<CartItem>) redisTemplate.opsForValue().get(key);
        // 购物车不存在
        if(cart == null){
            cart = new ArrayList<>();
            cart.add(new CartItem(productId, count, new Date()));
            redisTemplate.opsForValue().set(key, cart);
            redisTemplate.expire(key, 30, TimeUnit.DAYS);
            return "添加成功: "+productId+"目前数量: "+count;
        }
        // 购物车存在
        for (CartItem cartItem : cart){
            if(cartItem.getProductId().longValue() == productId.longValue()){
                cartItem.setCount(cartItem.getCount()+count);
                cartItem.setUpdateTime(new Date());
                redisTemplate.opsForValue().set(key, cart);
                redisTemplate.expire(key, 30, TimeUnit.DAYS);
                return "添加成功: "+productId+"目前数量: "+count;
            }
        }
        // 购物车不存在该商品
        cart.add(new CartItem(productId, count, new Date()));
        redisTemplate.opsForValue().set(key, cart);
        redisTemplate.expire(key, 30, TimeUnit.DAYS);
        return "添加成功: "+productId+"目前数量: "+count;
    }

    @Override
    public String updateCount(String key, Long productId, Integer count) {

        List<CartItem> carts = (List<CartItem>) redisTemplate.opsForValue().get(key);
        if(carts != null){
            for (CartItem item :carts){
                if(productId.longValue() == item.getProductId().longValue()){
                    item.setCount(item.getCount()+count);
                    item.setUpdateTime(new Date());
                    redisTemplate.opsForValue().set(key, item);
                    redisTemplate.expire(key, 30, TimeUnit.DAYS);
                    return "更新成功: "+productId+"目前数量: "+count;
                }
            }
        }
        return "更新失败";
    }

    @Override
    public String del(String key, Long productId) {
        List<CartItem> carts = (List<CartItem>) redisTemplate.opsForValue().get(key);
        if(carts != null){
            for(CartItem item : carts){
                if(item.getProductId().longValue() == productId.longValue()){
                    carts.remove(item);
                    redisTemplate.opsForValue().set(key, carts);
                    redisTemplate.expire(key, 30, TimeUnit.DAYS);
                    return "删除成功";
                }
            }
        }
        return "删除失败";
    }

    @Override
    public List<CartItemVo> list(String key) {
        List<CartItem> cartItems = (List<CartItem>) redisTemplate.opsForValue().get(key);
        if(cartItems == null){
            return new ArrayList<CartItemVo>();
        }
        List<CartItemVo> cartItemVos = new ArrayList<>(cartItems.size());
        for(CartItem cartItem : cartItems){
            // 将redis存储的List<CartItem>加入到List<CartItemVo>中
            CartItemVo cartItemVo = new CartItemVo();
            cartItemVo.setCount(cartItem.getCount());
            cartItemVo.setUpdateTime(cartItem.getUpdateTime());
            StringBuilder stringBuilder = new StringBuilder("productId:").append(cartItem.getProductId());
            TProduct product = (TProduct) redisTemplate.opsForValue().get(stringBuilder.toString());
            if(product == null){
                // redis中没有,则查询数据库
                product = productDao.queryById(cartItem.getProductId());
                redisTemplate.opsForValue().set(stringBuilder.toString(), product);
                // 不是热门商品,不用设置太久
                redisTemplate.expire(stringBuilder.toString(), 60, TimeUnit.MINUTES);
            }
            cartItemVo.setProduct(product);
            cartItemVos.add(cartItemVo);
        }
        Collections.sort(cartItemVos, new Comparator<CartItemVo>() {
            @Override
            public int compare(CartItemVo o1, CartItemVo o2) {
                return (int) (o2.getUpdateTime().getTime()-o1.getUpdateTime().getTime());
            }
        });
        return cartItemVos;
    }
}

合并购物车

@Override
    public List merge(String nologinKey, String loginKey) {
        List<CartItem> noLoginCart = (List<CartItem>) redisTemplate.opsForValue().get(nologinKey);
        if(noLoginCart == null){
            // 不存在未登陆购物车
            return new ArrayList();
        }
        List<CartItem> loginCart = (List<CartItem>) redisTemplate.opsForValue().get(loginKey);
        if(loginCart == null){
            // 不存在登陆购物车,则将未登录时购物车加入已登录时购物车
            redisTemplate.opsForValue().set(loginKey, noLoginCart);
            redisTemplate.expire(loginKey, 30, TimeUnit.DAYS);
        }
        // 两者都存在
        HashMap<Long, CartItem> map = new HashMap<>();
        for(CartItem cartItem :noLoginCart){
            map.put(cartItem.getProductId(), cartItem);
        }
        for(CartItem cartItem :loginCart){
            CartItem item = map.get(cartItem.getProductId());
            if(item == null) {
                map.put(cartItem.getProductId(), cartItem);
            }else{
                item.setCount(cartItem.getCount()+item.getCount());
            }
        }
        // hashmap---->list
        Collection<CartItem> values = map.values();
        List<CartItem> list = new ArrayList<>(values);

        // list写入已登录购物车,删除未登录购物车
        redisTemplate.delete(noLoginCart);
        // 已登录购物车加入redis
        redisTemplate.opsForValue().set(loginKey, loginCart);
        redisTemplate.expire(loginKey, 30, TimeUnit.DAYS);
        return list;
    }

5.3 服务消费者配置

未登录时购物车的Controller

package com.hofe.cart.controller;

import com.alibaba.dubbo.config.annotation.Reference;

@RestController
@RequestMapping("cart")
public class CartController {

    @Reference
    private ICartService cartService;

    @GetMapping("hello")
    public String hello(){
        return "hello world";
    }

    @GetMapping("add/{productId}/{count}")
    public String add(@PathVariable("productId") Long productId,
                          @PathVariable("count") Integer count,
                          @CookieValue(name = "user_cart",required = false) String uuid,
                          HttpServletResponse response){

        if(uuid == null){
            uuid = UUID.randomUUID().toString();
        }

        StringBuilder key = new StringBuilder("user:cart:").append(uuid);

        //写cookie到客户端,更新有效期
        Cookie cookie = new Cookie("user_cart", uuid);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(30*24*60*60);
        response.addCookie(cookie);

        return cartService.add(key.toString(),productId,count);
    }

    @GetMapping("update/{productId}/{count}")
    public String update(@PathVariable("productId") Long productId,
                      @PathVariable("count") Integer count,
                      @CookieValue(name = "user_cart",required = false) String uuid,
                      HttpServletResponse response){

        if(uuid == null && "".equals(uuid)){
            return "更新失败";
        }

        StringBuilder key = new StringBuilder("user:cart:").append(uuid);

        //写cookie到客户端,更新有效期
        Cookie cookie = new Cookie("user_cart", uuid);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(30*24*60*60);
        response.addCookie(cookie);

        return cartService.updateCount(key.toString(),productId,count);
    }

    @GetMapping("delete/{productId}")
    public String delete(@PathVariable("productId") Long productId,
                             @CookieValue(name = "user_cart",required = false) String uuid,
                             HttpServletResponse response){

        if(uuid == null && "".equals(uuid)){
            return "删除失败";
        }

        StringBuilder key = new StringBuilder("user:cart:").append(uuid);

        //写cookie到客户端,更新有效期
        Cookie cookie = new Cookie("user_cart", uuid);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(30*24*60*60);
        response.addCookie(cookie);

        return cartService.del(key.toString(), productId);
    }


    @GetMapping("list")
    public List list(@CookieValue(name = "user_cart",required = false) String uuid,
                     HttpServletResponse response){

        if(uuid == null){
            uuid = UUID.randomUUID().toString();
        }

        StringBuilder key = new StringBuilder("user:cart:").append(uuid);

        //写cookie到客户端,更新有效期
        Cookie cookie = new Cookie("user_cart", uuid);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(30*24*60*60);
        response.addCookie(cookie);

        return cartService.list(key.toString());
    }

}

已登录时购物车的Controller
在客户端发起请求到controller之间加入拦截器,用于获取用户登录状态,并将结果封装在HttpServletRequest request中

配置(加入拦截器)

@Configuration
public class WebConfig implements WebMvcConfigurer{

    @Autowired
    private AuthInterceptor authInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(authInterceptor).addPathPatterns("/**");
    }
}

拦截器


@Component
public class AuthInterceptor implements HandlerInterceptor {

    @Reference
    private IUserService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

        // 获取用户登录状态
        Cookie[] cookies = request.getCookies();
        if(cookies == null){
            return true;
        }
        for (Cookie cookie :cookies){
            if("user_token".equals(cookie.getName())){
                String uuid = cookie.getValue();
                TUser user = userService.checkIsLogin(uuid);
                if(user != null){
                    request.setAttribute("user", user);
                    return true;
                }
            }
        }
        // controller级别用httpclient接口

        // 无论是否登录,都放行到购物车
        return true;
    }
}

controller
如果已登录,则RedisKey为user:cart:userId;否则为user:cart:uuid

@GetMapping("add/{productId}/{count}")
    public String add(@PathVariable("productId") Long productId,
                          @PathVariable("count") Integer count,
                          @CookieValue(name = "user_cart",required = false) String uuid,
                          HttpServletResponse response,
                          HttpServletRequest request){

        String key = "";
        // 通过拦截器获取的登陆状态
        TUser user = (TUser) request.getAttribute("user");
        if(user != null){
            key = new StringBuilder("user:cart:").append(user.getId()).toString();
        }else{
            if(uuid == null){
                uuid = UUID.randomUUID().toString();
            }
            key = new StringBuilder("user:cart:").append(uuid).toString();
        }


        //写cookie到客户端,更新有效期
        Cookie cookie = new Cookie("user_cart", uuid);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(30*24*60*60);
        response.addCookie(cookie);

        return cartService.add(key.toString(),productId,count);
    }

合并购物车,修改已登录购物车的商品记录,同时清除未登录时购物车的记录

@RequestMapping("merge")
    public List merge(@CookieValue(name = "user_cart",required = false) String uuid,
                      HttpServletResponse response, HttpServletRequest request) {

        String key = "";
        // 通过拦截器获取的登陆状态
        TUser user = (TUser) request.getAttribute("user");
        if (user == null) {
            return new ArrayList();
        }

        if (uuid == null || "".equals(uuid)) {
            return new ArrayList();
        }

        String loginKey = new StringBuilder("user:cart:").append(user.getId()).toString();
        String noLoginKey = new StringBuilder("user:cart:").append(uuid).toString();

        List merge = cartService.merge(noLoginKey, loginKey);

        //写cookie到客户端,清除未登录时购物车的id
        Cookie cookie = new Cookie("user_cart", uuid);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(0);
        response.addCookie(cookie);

        return merge;
    }

六、购物车优化

6.1 回顾下之前购物车方案设计

首先在客户端向服务器发起请求后,通过拦截器获取当前是否登录,如果登陆的话就可以获得cookie: user_token对应的value(用户信息),无论登陆与否都放行。控制层接收请求,并将cookie: user_cart对应的value(购物车信息)注入到uuid中。
在这里插入图片描述
获取拦截器封装在request中的用户信息,如果不为null,说明用户已登录,那这时候需要在用户的购物车中添加记录,故传递redis: user:cart:userid更改在redis中保存的用户购物车信息;如果用户未登录,则在客户端中的购物车中添加记录,先传递redis: user:cart:uuid用于变更redis中保存的客户端购物车信息,同时需要修改客户端保存的cookie,因为未登录时的购物车是通过客户端保存的cookie的uuid区别不同客户端的。
在这里插入图片描述
当controller调用service时传入key(userid或uuid)及记录,service首先判断客户端或者用户是否已有购物车,如果没有则新建一个list作为购物车;如果存在购物车,就判断购物车中是否有该条记录,有的话则修改记录的商品数,没有的话则新增该商品记录,并存入到redis;存入到未登录的购物车还是已登录的购物车由传递来的key(userid或uuid)决定。
在这里插入图片描述
用户未登陆时加入购物车的商品需要合并到登陆后的购物车中,首先获取uuid代表客户端的购物车和userid代表用户的购物车,调用合并函数将两辆购物车合并,同时需要清除客户端的购物车信息,通过将cookie存活时间设置为0即可。
在这里插入图片描述
merge函数通过传递进来的uuid和userid获取redis中保存的数据,如果客户端购物车为空,则不用进行合并;如果如果用户购物车为空,则将未登录的购物车加入到已登录购物车的redis记录中,并删除未登录购物车的记录。
在这里插入图片描述
如果两者都不为空,则先遍历未登录购物车获取productId即记录,将其加入map;再遍历登陆购物车,判断map中是否存在相同的productId,存在则修改其对应的数量,如果不存在则加入map。最后通过collection将map的value即商品记录转成list返回,同时删除未登录购物车对应的redis记录。
在这里插入图片描述

6.2 优化的购物车设计方案

之前的设计方案存在着更新删除需要遍历List的缺点,故改用Hash存储,可直接通过Key获得Value,而不需要再遍历列表。

七、订单模块(未完成)

7.1 方案设计

这一模块有几个过程:购物车结算->订单确认->生成订单->确认并支付。
购物车结算->确认页面
点击购物车结算后,跳转至确认页面,订单确认页面需要有用户地址信息、货物详情。故需要用户先登录,当用户访问确认也时,加入拦截器将页面从定向到登陆页面,记录referer,当用户登录成功后跳转回referer页面;货物详情通过uuid或者userid获得。

订单确认->生成订单
订单基本表和订单明细表:订单基本表需要有id/orderId/userId/transfer_no/name/phone/address(历史快照)/status/total_money/pay_type;订单明细表需要有id/order_id/product_id/count/name/price(历史快照)。

订单编号:唯一性、有序性(查询)、时间性。采用基于时间戳的方式+userID生成

库存的扣减:库存预扣减、超时未支付的检测。采用定时任务now-create_time。

选择购物车的部分商品下单:获取用户勾选id集合–等于批量删除,根据id集合选择购物车的部分商品信息,才将商品信息转换为订单。

安全问题
短信平台验证签名正确与否。发送签名:将用户id、token、时间戳通过MD5加密算法计算出sign发送给短信平台。短信平台验证签名:通过传递过来的sign获取用户id、token、时间戳,重新计算MD5判断是否与sign一致。缺陷:只保证了调用合法,明文传输无法保证内容合法。

对接支付宝
调用支付宝接口需要先生成RAS密钥,包含应用私钥与应用公钥。生存密钥后才可以获取支付宝公钥。

八出现的问题

问题:在service中因为无法找到dao实现类导致的自动注入失败

Field productTypeDao in com.hofe.productservice.service.ProductTypeService required a bean of type 'com.hofe.dao.TProductTypeDao' that could not be found.

The injection point has the following annotations:
	- @org.springframework.beans.factory.annotation.Autowired(required=true)

原因:考虑Spring未实例化对象
找不到bean的配置,以往是使用bean.xml文件进行配置,SpringBoot使用注解,就需要在入口程序中添加@MapperScan(“dao路径”)注解;同时也要考虑在注解方式下dao是否@mapper,xml方式下.xml文件存放路径是否正确;以及考虑application.yml中是否正确配置数据源。
可去掉Service上的@Component注解
在这里插入图片描述
在这里插入图片描述

问题:找不到dao中的方法

Invalid bound statement (not found): com.hofe.dao.TProductTypeDao.queryAll
在这里插入图片描述
解决办法:
在dao的pom中添加以下配置,标明xml配置存放路径

<build>
        <resources>
            <resource>
                <directory>resources</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
        </resources>
    </build>

或者在service的application.yml指明mybatis配置

mybatis:
  # 指定别名设置的包为所有entity
  type-aliases-package: cn.hofe.entity
  configuration:
    map-underscore-to-camel-case: true # 驼峰命名规范
  mapper-locations: # mapper映射文件位置
    - classpath:mapper/*.xml

问题:字段名为name、desc时查询出错

原因:数据表字段名不要用name,desc会与msql自带语法冲突
解决方法:1、在要用到关键字的地方加反引号标识;2、修改表字段名

问题:@Reference、@Service注解错误
原因:错用了java的注解导致Service无法正常注入

import com.alibaba.dubbo.config.annotation.Service;
import com.alibaba.dubbo.config.annotation.Reference;

问题:平级转树形结构问题

遇到需要返回树形结构的json数据,如多级菜单、多级类别等,可以使用VO与resultMap搭配,mybatis递归查询。需要注意的是,resultMap中的collection字段的column=“字段名”,不需要加{};如需传递多个参数可加如{id, code}。这一字段代表的意思是第一次执行queryAllByCategory之后递归查询用到的parentId是这个category_type字段。
在这里插入图片描述
树形结构的数据,可以不用外键进行关联;外键关联之后,无法将顶级的parentId设置值,只能默认null。

以下链接可参考:
https://www.hangge.com/blog/cache/detail_2715.html
https://blog.csdn.net/janet796/article/details/79500349
https://blog.csdn.net/qq_38164123/article/details/94358131

问题:

This application has no explicit mapping for /error, so you are seeing this as a fallback.
There was an unexpected error (type=Internal Server Error, status=500).
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'parentId' in 'class java.lang.Integer' org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException

在使用choose之类条件控制语句的时候,要注意它判断的是传进来的参数的拥有的字段,如果传进来是个user可以使用;如果只传进来username,则无法控制password条件
在这里插入图片描述

问题:dubbo超时

Invoke remote method timeout. method: queryCategorys, provider: dubbo:
默认超时时间是1000ms,120条数据加上递归就超时了,可以在消费者和提供者端设置timeout=30000

问题:SSO单点登录模块跨域问题

认证模块通过了,可其他模块在发送ajax请求时还是无法携带cookie。
在SSO登录之后,浏览器返回cookie,其携带着uuid信息,其他系统在发送ajax请求时,携带这个cookie信息就可免登录。但需要在Controller的类或者方法上加上注解@CrossOrigin(origins = “*”, allowCredentials = “true”),允许所有端口的访问,并允许携带cookie
前端ajax代码需要withCredentials:true

&ajax({
	url:"http://localhost:9098/sso/checkIsLogin",
	xhrFields:{withCredentials:true},
	crossDomain:true
})

需要注意的是,cart.hofe.com并不能保存shop.hofe.com的cookie,故设置的时候要设置父域名cookie.setDomain(“hofe.com”)

原创文章 22 获赞 10 访问量 2321

猜你喜欢

转载自blog.csdn.net/qq_41011723/article/details/105536492
今日推荐