【Spring Cloud Alibaba 温故而知新】(六)模拟电商 - 用户账户微服务

目录

9.1.1 用户身份登录统一拦截

  • 微服务请求鉴权

9.1.1.1 创建电商服务模块父模块

  1. 创建 sca-commerce-service 是 sca-commerce 的子项目
  2. 创建 sca-commerce-service-config 是 sca-commerce-service 的子项目
微服务的架构图

在这里插入图片描述

sca-commerce-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>sca-commerce</artifactId>
        <groupId>com.edcode.commerce</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>sca-commerce-service</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>sca-commerce-service-config</module>
    </modules>
    <packaging>pom</packaging> <!-- 父模块不能 jar 只能是 pom -->

    <!-- 模块名及描述信息 -->
    <name>sca-commerce-service</name>
    <description>电商服务模块父模块</description>

</project>
sca-commerce-service-config

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>sca-commerce-service</artifactId>
        <groupId>com.edcode.commerce</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>sca-commerce-service-config</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <!-- 模块名及描述信息 -->
    <name>sca-commerce-service-config</name>
    <description>服务通用配置</description>

    <dependencies>
        <!-- 引入 mvc-config, 变成 web 应用 -->
        <dependency>
            <groupId>com.edcode.commerce</groupId>
            <artifactId>sca-commerce-mvc-config</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

9.1.1.2 利用 ThreadLocal 做用户身份统一登录拦截

sca-commerce-service-config

使用 ThreadLocal 去单独存储每一个线程携带的 LoginUserInfo 信息
package com.edcode.commerce.filter;

import com.edcode.commerce.vo.LoginUserInfo;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description
 *
 * 使用 ThreadLocal 去单独存储每一个线程携带的 LoginUserInfo 信息
 * 要及时的清理我们保存到 ThreadLocal 中的用户信息:
 * 1. 保证没有资源泄露
 * 2. 保证线程在重用时, 不会出现数据混乱
 *
 */
public class AccessContext {

    /**
     * 用户信息保存在 ThreadLocal 里面
     */
    private static final ThreadLocal<LoginUserInfo> LOGIN_USER_INFO = new ThreadLocal<>();

    /**
     * 从 ThreadLocal 获取用户信息
     */
    public static LoginUserInfo getLoginUserInfo() {
        return LOGIN_USER_INFO.get();
    }

    /**
     * 从拦截器或者 jwt 信息, set 到 ThreadLocal
     */
    public static void setLoginUserInfo(LoginUserInfo loginUserInfo) {
        LOGIN_USER_INFO.set(loginUserInfo);
    }

    /**
     * 清除 ThreadLocal 里面的当前线程用户信息 (必需要有,不然会 OOM )
     */
    public static void clearLoginUserInfo() {
        LOGIN_USER_INFO.remove();
    }
}

ThreadLocal 在面试经常会给问到,所以此处可以当做一个实践回答,ThreadLocal 在项目做了什么和怎么用。

用户身份统一登录拦截
package com.edcode.commerce.filter;

import com.edcode.commerce.constant.CommonConstant;
import com.edcode.commerce.util.TokenParseUtil;
import com.edcode.commerce.vo.LoginUserInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 用户身份统一登录拦截
 */
@SuppressWarnings("all")
@Slf4j
@Component
public class LoginUserInfoInterceptor implements HandlerInterceptor {

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

        // 先尝试从 http header 里面拿到 token
        String token = request.getHeader(CommonConstant.JWT_USER_INFO_KEY);

        LoginUserInfo loginUserInfo = null;
        try {
            loginUserInfo = TokenParseUtil.parseUserInfoFromToken(token);
        } catch (Exception ex) {
            log.error("解析登录用户信息错误: [{}]", ex.getMessage(), ex);
        }

        // 如果程序走到这里, 说明 header 中没有 token 信息 (这个判断基本不会跳进来,Gateway已经拦截过了)
        if (null == loginUserInfo) {
            throw new RuntimeException("无法分析当前登录用户");
        }

        log.info("设置登录用户信息: [{}]", request.getRequestURI());

        // 设置当前请求上下文, 把用户信息填充进去
        AccessContext.setLoginUserInfo(loginUserInfo);

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 【postHandle 请求执行之后,返回之前】 不需要返回之前修改
    }

    /**
     * 在请求完全结束后调用, 常用于清理资源等工作
     * */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 判断 ThreadLocal 是否有用户信息
        if (null != AccessContext.getLoginUserInfo()) {
            // 清除 ThreadLocal 里面用户信息
            AccessContext.clearLoginUserInfo();
        }
    }
}
Web Mvc 配置
package com.edcode.commerce.conf;

import com.edcode.commerce.filter.LoginUserInfoInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description Web Mvc 配置
 */
@Configuration
public class ScaWebMvcConfig extends WebMvcConfigurationSupport {

    /**
     * 添加拦截器配置
     */
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        // 添加【用户身份统一登录拦截】
        registry.addInterceptor(new LoginUserInfoInterceptor()).addPathPatterns("/**").order(0);
    }

}

9.2.1 集成 Swagger2 实现代码即文档

9.2.1.1 集成 Swagger2 的步骤与原理

  • 如何集成 Swagger2
    • 在 pom.xml 文件中添加 Swagger2 所需要的依赖配置
    • 配置 Swagger2,指定扫描包与描述展示信息
    • Web MVC 配置:让 MVC 加载 Swagger 的静态资源

9.2.1.2 Maven 依赖

访问 https://mvnrepository.com/ 查找最新的 Swagger2 依赖
在这里插入图片描述
pom.xml

<dependencies>
    <!-- swagger 用于定义 API 文档 -->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>3.0.0</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>3.0.0</version>
    </dependency>
    <!-- 美化 swagger -->
    <dependency>
        <groupId>com.github.xiaoymin</groupId>
        <artifactId>swagger-bootstrap-ui</artifactId>
        <version>1.9.6</version>
    </dependency>
</dependencies>

9.2.1.3 编写 Swagger 配置类

package com.edcode.commerce.conf;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description Swagger 配置类
 *
 * ip:port/swagger-ui.html
 *
 * ip:port/doc.html
 *
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {

    /**
     * Swagger 实例 Bean 是 Docket, 所以通过配置 Docket 实例来配置 Swagger
     */
    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.SWAGGER_2)
                // 展示在 Swagger 页面上的自定义工程描述信息
                .apiInfo(apiInfo())
                // 选择展示哪些接口
                .select()
                // 只有 com.edcode.ecommerce 包内的才去展示
                .apis(RequestHandlerSelectors.basePackage("com.edcode.commerce"))
                .paths(PathSelectors.any())
                .build();
    }

    /**
     * Swagger 的描述信息
     */
    public ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("sca-commerce-service")
                .description("sca-commerce-springcloud-service")
                .contact(new Contact(
                        "eddie", "blog.eddilee.cn", "xxx@qq.com"
                ))
                .version("1.0")
                .build();
    }
}

9.3.1.1 配置 MVC 加载 Swagger 的静态资源

package com.edcode.commerce.conf;

import com.edcode.commerce.filter.LoginUserInfoInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description Web Mvc 配置
 */
@Configuration
public class ScaWebMvcConfig extends WebMvcConfigurationSupport {

    /**
     * 添加拦截器配置
     */
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        // 添加【用户身份统一登录拦截】
        registry.addInterceptor(new LoginUserInfoInterceptor()).addPathPatterns("/**").order(0);
    }

    /**
     * 让 MVC 加载 Swagger 的静态资源
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").
                addResourceLocations("classpath:/static/");
        registry.addResourceHandler("swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("doc.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
        super.addResourceHandlers(registry);
    }

}

9.3.1.2 校验是否是白名单 swagger2 接口

为 swagger2 接口,添加到拦截器里面,如果是 swagger2 接口的请求就不需要带有身份验证信息

package com.edcode.commerce.filter;

import com.edcode.commerce.constant.CommonConstant;
import com.edcode.commerce.util.TokenParseUtil;
import com.edcode.commerce.vo.LoginUserInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 用户身份统一登录拦截
 */
@SuppressWarnings("all")
@Slf4j
@Component
public class LoginUserInfoInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 部分请求不需要带有身份信息, 即白名单
        if (checkWhiteListUrl(request.getRequestURI())) {
            return true;
        }

        // 先尝试从 http header 里面拿到 token
        String token = request.getHeader(CommonConstant.JWT_USER_INFO_KEY);

        LoginUserInfo loginUserInfo = null;
        try {
            loginUserInfo = TokenParseUtil.parseUserInfoFromToken(token);
        } catch (Exception ex) {
            log.error("解析登录用户信息错误: [{}]", ex.getMessage(), ex);
        }

        // 如果程序走到这里, 说明 header 中没有 token 信息 (这个判断基本不会跳进来,Gateway已经拦截过了)
        if (null == loginUserInfo) {
            throw new RuntimeException("无法分析当前登录用户");
        }

        log.info("设置登录用户信息: [{}]", request.getRequestURI());

        // 设置当前请求上下文, 把用户信息填充进去
        AccessContext.setLoginUserInfo(loginUserInfo);

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 【postHandle 请求执行之后,返回之前】 不需要返回之前修改
    }

    /**
     * 在请求完全结束后调用, 常用于清理资源等工作
     * */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 判断 ThreadLocal 是否有用户信息
        if (null != AccessContext.getLoginUserInfo()) {
            // 清除 ThreadLocal 里面用户信息
            AccessContext.clearLoginUserInfo();
        }
    }

    /**
     * 校验是否是白名单接口
     * swagger2 接口
     * */
    private boolean checkWhiteListUrl(String url) {
        return StringUtils.containsAny(
                url,
                "springfox", "swagger", "v2",
                "webjars", "doc.html"
        );
    }
}

9.3.1.3 集成 Swagger2 的步骤与原理

  • Swagger2 的实现原理
    • 项目启动,Spring 初始化上下文,加载 Swagger 相关的 Bean,并由此扫描系统中需要生成的 API 文档注解获取注释信息

在这里插入图片描述

9.4.1 用户账户微服务功能设计

  • 用户账户微服务功能逻辑架构
    • Tips:微服务功能设计看需求、看业务设定、看个人喜好

在这里插入图片描述

9.4.1.1 创建用户账户微服务模块

架构图

在这里插入图片描述

sca-commerce-account-service

bootstrap.yml
server:
  port: 8003
  servlet:
    context-path: /scacommerce-account-service

spring:
  application:
    name: sca-commerce-account-service
  cloud:
    nacos:
      # 服务注册发现
      discovery:
        enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
        #server-addr: ${NACOS_ADDR:127.0.0.1}:8848
        server-addr: ${NACOS_ADDR_1:127.0.0.1}:8848,${NACOS_ADDR_2:127.0.0.1}:8849,${NACOS_ADDR_3:127.0.0.1}:8850 # Nacos 服务器地址
        namespace: ${NAMESPACE_ID:1adcfdd8-5763-4768-9a15-9c7157988950}
        metadata:
          management:
            context-path: ${server.servlet.context-path}/actuator
  kafka:
    bootstrap-servers: ${KAFKA_SERVER:127.0.0.1}:${KAFKA_PORT:9092}
    producer:
      retries: 3
    consumer:
      auto-offset-reset: latest
  zipkin:
    sender:
      type: ${ZIPKIN_KAFKA_SENDER:web} # 默认是 web
    base-url: http://${ZIPKIN_URL:localhost}:${ZIPKIN_PORT:9411}/
  sleuth:
    sampler:
      # RateLimitingSampler 抽样策略,设置了限速采样,spring.sleuth.sampler.probability 属性值无效
      rate: 100 # 每秒间隔接受的 trace 量
      # Probability 抽样策略
      probability: 1.0 # 采样比例,1.0 表示 100%, 默认:0.1
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: none
    properties:
      hibernate.show_sql: true
      hibernate.format_sql: true
    open-in-view: false
  datasource:
    # 数据源
    url: jdbc:mysql://${MYSQL_HOST:127.0.0.1}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:sca_commerce}?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false
    username: ${MYSQL_USERNAME:root}
    password: ${MYSQL_PASSWORD:123456}
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 连接池
    hikari:
      maximum-pool-size: 8
      minimum-idle: 4
      idle-timeout: 30000
      connection-timeout: 30000
      max-lifetime: 45000
      auto-commit: true
      pool-name: ImoocEcommerceHikariCP

# 暴露端点
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
启动类
package com.edcode.commerce;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 用户账户微服务启动入口
 *
 * localhost:8003/scacommerce-account-service/doc.html
 * localhost:8003/scacommerce-account-service/swagger-ui.html
 */
@EnableJpaAuditing // 审计功能
@SpringBootApplication
@EnableDiscoveryClient
public class AccountApplication {

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

9.5.1 数据表及ORM过程 - 地址数据表

9.5.1.1 地址数据表语句

-- 创建 t_scacommerce_address 数据表
CREATE TABLE IF NOT EXISTS `sca_commerce`.`t_scacommerce_address` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `user_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '用户 id',
  `username` varchar(64) NOT NULL DEFAULT '' COMMENT '用户名',
  `phone` varchar(64) NOT NULL DEFAULT '' COMMENT '电话号码',
  `province` varchar(64) NOT NULL DEFAULT '' COMMENT '省',
  `city` varchar(64) NOT NULL DEFAULT '' COMMENT '市',
  `address_detail` varchar(256) NOT NULL DEFAULT '' COMMENT '详细地址',
  `create_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='用户地址表';

使用 IDEA EasyCode 插件生成实体类

【Spring Cloud Alibaba 温故而知新】(三)授权、鉴权中心微服务 章节有 EasyCode 使用说明与 JPA 模板提供

9.5.1.2 地址数据表 - 逆向生成的实体类

package com.edcode.commerce.entity;

import java.util.Date;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Entity;
import javax.persistence.GenerationType;
import org.hibernate.annotations.GenericGenerator;
import java.io.Serializable;
import javax.persistence.EntityListeners;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;

/**
 * @description 用户地址表(t_scacommerce_address)表实体类
 * @author eddie.lee
 * @date 2021-11-04 12:44:32
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "t_scacommerce_address")
@EntityListeners(AuditingEntityListener.class) // 作用:自动更新时间, 需要配合 @EnableJpaAuditing 使用
public class ScaCommerceAddress implements Serializable {

	/**
	 * 自增主键
	 */
	@Id
	@Column(name = "id", nullable = false)
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	/**
	 * 用户 id
	 */
	@Column(name = "user_id", nullable = false)
	private Long userId;

	/**
	 * 用户名
	 */
	@Column(name = "username", nullable = false)
	private String username;

	/**
	 * 电话号码
	 */
	@Column(name = "phone", nullable = false)
	private String phone;

	/**
	 * 省
	 */
	@Column(name = "province", nullable = false)
	private String province;

	/**
	 * 市
	 */
	@Column(name = "city", nullable = false)
	private String city;

	/**
	 * 详细地址
	 */
	@Column(name = "address_detail", nullable = false)
	private String addressDetail;

	/**
	 * 创建时间
	 */
	@Column(name = "create_time", nullable = false)
	private Date createTime;

	/**
	 * 更新时间
	 */
	@Column(name = "update_time", nullable = false)
	private Date updateTime;

}

实体类出现红色波浪线,警告需要加数据源,如果强迫症的小伙伴可以自行添加

在这里插入图片描述
选择数据源
在这里插入图片描述

9.5.1.3 地址 - DAO

package com.edcode.commerce.dao;

import com.edcode.commerce.entity.ScaCommerceAddress;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description ScaCommerceAddress Dao 接口定义
 */
public interface ScaCommerceAddressDao extends JpaRepository<ScaCommerceAddress, Long> {

	/**
	 * 根据 用户 id 查询地址信息
	 */
	List<ScaCommerceAddress> findAllByUserId(Long userId);
}

9.5.1.4 创建一个对外的 sdk 子工程

sca-commerce-service-sdk

架构图

在这里插入图片描述

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>sca-commerce-service</artifactId>
        <groupId>com.edcode.commerce</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>sca-commerce-service-sdk</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <!-- 模块名及描述信息 -->
    <name>sca-commerce-service-sdk</name>
    <description>服务模块 SDK</description>

</project>
用户地址信息
package com.edcode.commerce.account;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;
import java.util.List;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 返回给 - 用户地址信息
 */
@ApiModel(description = "用户地址信息")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AddressInfo {

    @ApiModelProperty(value = "地址所属用户 id")
    private Long userId;

    @ApiModelProperty(value = "地址详细信息")
    private List<AddressItem> addressItems;

    /**
     * 单个的地址信息
     */
    @ApiModel(description = "用户的单个地址信息")
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class AddressItem {

        @ApiModelProperty(value = "地址表主键 id")
        private Long id;

        @ApiModelProperty(value = "用户姓名")
        private String username;

        @ApiModelProperty(value = "电话")
        private String phone;

        @ApiModelProperty(value = "省")
        private String province;

        @ApiModelProperty(value = "市")
        private String city;

        @ApiModelProperty(value = "详细的地址")
        private String addressDetail;

        @ApiModelProperty(value = "创建时间")
        private Date createTime;

        @ApiModelProperty(value = "更新时间")
        private Date updateTime;

        public AddressItem(Long id) {
            this.id = id;
        }

        /**
         * 将 AddressItem 转换成 UserAddress
         */
        public UserAddress toUserAddress() {
            UserAddress userAddress = new UserAddress();
            userAddress.setUsername(this.username);
            userAddress.setPhone(this.phone);
            userAddress.setProvince(this.province);
            userAddress.setCity(this.city);
            userAddress.setAddressDetail(this.addressDetail);
            return userAddress;
        }
    }
}

UserAddress

package com.edcode.commerce.account;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 用户地址信息
 */
@ApiModel(description = "用户地址信息")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserAddress {

    @ApiModelProperty(value = "用户姓名")
    private String username;

    @ApiModelProperty(value = "电话")
    private String phone;

    @ApiModelProperty(value = "省")
    private String province;

    @ApiModelProperty(value = "市")
    private String city;

    @ApiModelProperty(value = "详细的地址")
    private String addressDetail;

}

sca-commerce-account-service 添加 sdk 的 Maven依赖
<dependency>
    <groupId>com.edcode.commerce</groupId>
    <artifactId>sca-commerce-service-sdk</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

9.6.1 数据表及ORM过程 - 用户账户余额表

9.6.1.1 用户账户余额表语句

-- 创建 t_scacommerce_balance 数据表
CREATE TABLE IF NOT EXISTS `sca_commerce`.`t_scacommerce_balance` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `user_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '用户 id',
  `balance` bigint(20) NOT NULL DEFAULT 0 COMMENT '账户余额',
  `create_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `user_id_key` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='用户账户余额表';

9.6.1.2 用户账户余额表 - 逆向生成实体类

package com.edcode.commerce.entity;
 
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Entity;
import javax.persistence.GenerationType;
import org.hibernate.annotations.GenericGenerator;
import java.io.Serializable;
import javax.persistence.EntityListeners;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
 
/**
 * @description 用户账户余额表(t_scacommerce_balance)表实体类
 * @author eddie.lee
 * @date 2021-11-04 12:44:59
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "t_scacommerce_balance")
@EntityListeners(AuditingEntityListener.class) // 作用:自动更新时间, 需要配合 @EnableJpaAuditing 使用
public class ScaCommerceBalance implements Serializable {
 

    /**
     * 自增主键
     */
    @Id
    @Column(name = "id", nullable = false)
	@GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /**
     * 用户 id
     */
    @Column(name = "user_id", nullable = false)
    private Long userId;

    /**
     * 账户余额
     */
    @Column(name = "balance", nullable = false)
    private Long balance;

    /**
     * 创建时间
     */
    @Column(name = "create_time", nullable = false)
    private Date createTime;

    /**
     * 更新时间
     */
    @Column(name = "update_time", nullable = false)
    private Date updateTime;

}

9.6.1.3 用户账户余额 - DAO

package com.edcode.commerce.dao;

import com.edcode.commerce.entity.ScaCommerceBalance;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description ScaCommerceBalance Dao 接口定义
 */
public interface ScaCommerceBalanceDao extends JpaRepository<ScaCommerceBalance, Long> {

	/**
	 * 根据 userId 查询 ScaCommerceBalance 对象
	 */
	ScaCommerceBalance findByUserId(Long userId);

}

9.6.1.4 sdk 创建交互的账户余额信息实体

package com.edcode.commerce.account;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 用户账户余额信息
 */
@ApiModel(description = "用户账户余额信息")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BalanceInfo {

    @ApiModelProperty(value = "用户主键 id")
    private Long userId;

    @ApiModelProperty(value = "用户账户余额")
    private Long balance;
}

9.7.1 用户地址与余额服务接口定义

9.7.1.1 主键 ids

为返回的数据 id 做一个实体类,在 sca-commerce-service-sdk

package com.edcode.commerce.common;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 主键 ids
 */
@ApiModel(description = "通用 id 对象")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TableId {

    @ApiModelProperty(value = "数据表记录主键")
    private List<Id> ids;

    @ApiModel(description = "数据表记录主键对象")
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Id {
        @ApiModelProperty(value = "数据表记录主键")
        private Long id;
    }
}

9.7.1.2 用户地址相关服务接口定义

package com.edcode.commerce.service;

import com.edcode.commerce.account.AddressInfo;
import com.edcode.commerce.common.TableId;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 用户地址相关服务接口定义
 */
public interface IAddressService {

	/**
	 * 创建用户地址信息
	 */
	TableId createAddressInfo(AddressInfo addressInfo);

	/**
	 * 获取当前登录的用户地址信息
	 */
	AddressInfo getCurrentAddressInfo();

	/**
	 * 通过 id 获取用户地址信息, id 是 ScaCommerceAddress 表的主键
	 */
	AddressInfo getAddressInfoById(Long id);

	/**
	 * 通过 TableId 获取用户地址信息
	 */
	AddressInfo getAddressInfoByTableId(TableId tableId);
}

9.7.1.3 用于余额相关的服务接口定义

package com.edcode.commerce.service;

import com.edcode.commerce.account.BalanceInfo;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 用于余额相关的服务接口定义
 */
public interface IBalanceService {

	/**
	 * 获取当前用户余额信息
	 */
	BalanceInfo getCurrentUserBalanceInfo();

	/**
	 * >扣减用户余额
	 * 
	 * @param balanceInfo
	 *            代表想要扣减的余额
	 */
	BalanceInfo deductBalance(BalanceInfo balanceInfo);

}

9.8.1 用户地址相关服务接口实现

  • 获取用户信息,不要相信前端传过来的,使用 AccessContext 获取
  • 在编码时候,使用 stream().map 可以更方便的转换映射,不需要 for 循环的写法去做
package com.edcode.commerce.service.impl;

import com.alibaba.fastjson.JSON;
import com.edcode.commerce.account.AddressInfo;
import com.edcode.commerce.common.TableId;
import com.edcode.commerce.dao.ScaCommerceAddressDao;
import com.edcode.commerce.entity.ScaCommerceAddress;
import com.edcode.commerce.filter.AccessContext;
import com.edcode.commerce.service.IAddressService;
import com.edcode.commerce.vo.LoginUserInfo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 用户地址相关服务接口实现
 */
@Slf4j
@Service
@Transactional(rollbackFor = Exception.class)
@RequiredArgsConstructor
public class AddressServiceImpl implements IAddressService {

    private final ScaCommerceAddressDao addressDao;

	/**
	 * 存储多个地址信息
	 */
	@Override
	public TableId createAddressInfo(AddressInfo addressInfo) {

        // 不能直接从参数中获取用户的 id 信息
		LoginUserInfo loginUserInfo = AccessContext.getLoginUserInfo();

        // jdk7写法:将传递的参数转换成实体对象
//		List<AddressInfo.AddressItem> addressItems = addressInfo.getAddressItems();
//		List<ScaCommerceAddress> scaCommerceAddresses = new ArrayList<>();
//
//		for (AddressInfo.AddressItem a : addressItems) {
//			scaCommerceAddresses.add(ScaCommerceAddress.to(loginUserInfo.getId(), a));
//		}

        // jdk8写法(将对象转换为其他对象):将传递的参数转换成实体对象
        List<ScaCommerceAddress> scaCommerceAddresses = addressInfo.getAddressItems().stream()
                .map(a -> ScaCommerceAddress.to(loginUserInfo.getId(), a))
                .collect(Collectors.toList());

        // 保存到数据表并把返回记录的 id 给调用方
        List<ScaCommerceAddress> savedRecords = addressDao.saveAll(scaCommerceAddresses);

        List<Long> ids = savedRecords.stream().map(ScaCommerceAddress::getId).collect(Collectors.toList());

        log.info("创建地址信息: [{}], [{}]", loginUserInfo.getId(), JSON.toJSONString(ids));

		return new TableId(
				// TODO TableId.Id::new
				ids.stream().map(TableId.Id::new).collect(Collectors.toList())
		);
	}

	@Override
	public AddressInfo getCurrentAddressInfo() {
        LoginUserInfo loginUserInfo = AccessContext.getLoginUserInfo();

        // 根据 userId 查询到用户的地址信息, 再实现转换
        List<ScaCommerceAddress> scaCommerceAddresses = addressDao.findAllByUserId(
                loginUserInfo.getId()
        );
        List<AddressInfo.AddressItem> addressItems = scaCommerceAddresses.stream()
//                .map(a -> a.toAddressItem())
                .map(ScaCommerceAddress::toAddressItem)
                .collect(Collectors.toList());

        return new AddressInfo(loginUserInfo.getId(), addressItems);
	}

	@Override
	public AddressInfo getAddressInfoById(Long id) {

		ScaCommerceAddress scaCommerceAddress = addressDao.findById(id).orElse(null);
		if (null == scaCommerceAddress) {
			throw new RuntimeException("地址不存在");
		}

		return new AddressInfo(
				scaCommerceAddress.getUserId(),
				Collections.singletonList(scaCommerceAddress.toAddressItem())
		);
	}

	@Override
	public AddressInfo getAddressInfoByTableId(TableId tableId) {

		List<Long> ids = tableId.getIds().stream()
//				.map(e -> e.getId()).collect(Collectors.toList());
				.map(TableId.Id::getId).collect(Collectors.toList());
		log.info("按表id获取地址信息: [{}]", JSON.toJSONString(ids));

		List<ScaCommerceAddress> scaCommerceAddresses = addressDao.findAllById(ids);
		if (CollectionUtils.isEmpty(scaCommerceAddresses)) {
			return new AddressInfo(-1L, Collections.emptyList());
		}

		List<AddressInfo.AddressItem> addressItems = scaCommerceAddresses.stream()
//				.map(s -> s.toAddressItem())
				.map(ScaCommerceAddress::toAddressItem)
				.collect(Collectors.toList());

		return new AddressInfo(
				scaCommerceAddresses.get(0).getUserId(), addressItems
		);
	}

}

9.9.1 用户地址服务接口可用性测试(测试用例)

9.9.1.1 用户账户微服务环境可用性校验

package com.edcode.commerce;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 用户账户微服务环境可用性校验
 */
@SpringBootTest
@RunWith(SpringRunner.class)
public class AccountApplicationTest {

    @Test
    public void contextLoad() {

    }

}

9.9.1.2 测试用例基类, 填充登录用户信息

package com.edcode.commerce.service;

import com.edcode.commerce.filter.AccessContext;
import com.edcode.commerce.vo.LoginUserInfo;
import org.junit.After;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 测试用例基类, 填充登录用户信息
 */
@SpringBootTest
@RunWith(SpringRunner.class)
public class BaseTest {

    protected final LoginUserInfo loginUserInfo = new LoginUserInfo(10L,"eddie_k2@qq.com");

    @Before
    public void init() {
        AccessContext.setLoginUserInfo(loginUserInfo);
    }

    @After
    public void destroy() {
        AccessContext.clearLoginUserInfo();
    }

}

9.9.1.3 用户地址相关服务功能测试

package com.edcode.commerce.service;

import com.alibaba.fastjson.JSON;
import com.edcode.commerce.account.AddressInfo;
import com.edcode.commerce.common.TableId;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Collections;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 用户地址相关服务功能测试
 */
@Slf4j
public class AddressServiceTest extends BaseTest {

	@Autowired
	private IAddressService addressService;

	/**
	 * 测试创建用户地址信息
	 */
	@Test
	public void testCreateAddressInfo() {
		AddressInfo.AddressItem addressItem = new AddressInfo.AddressItem();
		addressItem.setUsername("EddieTest");
		addressItem.setPhone("12345678910");
		addressItem.setProvince("广东省");
		addressItem.setCity("广州市");
		addressItem.setAddressDetail("广州塔");

		log.info("测试创建地址信息: [{}]", JSON.toJSONString(addressService.createAddressInfo(
		        new AddressInfo(loginUserInfo.getId(), Collections.singletonList(addressItem))
        )));
	}

	/**
	 * 测试获取当前登录用户地址信息
	 */
	@Test
	public void testGetCurrentAddressInfo() {
		log.info("测试获取当前用户信息: [{}]", JSON.toJSONString(
				addressService.getCurrentAddressInfo()
		));
	}

	/**
	 * 测试通过 id 获取用户地址信息
	 */
	@Test
	public void testGetAddressInfoById() {

		log.info("测试按id获取地址信息: [{}]", JSON.toJSONString(
				addressService.getAddressInfoById(1L)
		));
	}

	/**
	 * 测试通过 TableId 获取用户地址信息
	 */
	@Test
	public void testGetAddressInfoByTableId() {

		log.info("测试按表id获取地址信息: [{}]", JSON.toJSONString(
				addressService.getAddressInfoByTableId(
						new TableId(Collections.singletonList(new TableId.Id(1L)))
				)
		));
	}

}

9.10.1 用户余额相关服务接口实现

package com.edcode.commerce.service;

import com.edcode.commerce.account.BalanceInfo;
import com.edcode.commerce.dao.ScaCommerceBalanceDao;
import com.edcode.commerce.entity.ScaCommerceBalance;
import com.edcode.commerce.filter.AccessContext;
import com.edcode.commerce.vo.LoginUserInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 用于余额相关服务接口实现
 */
@Slf4j
@Service
@Transactional(rollbackFor = Exception.class)
public class BalanceServiceImpl implements IBalanceService {

    private final ScaCommerceBalanceDao balanceDao;

    public BalanceServiceImpl(ScaCommerceBalanceDao balanceDao) {
        this.balanceDao = balanceDao;
    }

    @Override
    public BalanceInfo getCurrentUserBalanceInfo() {

        LoginUserInfo loginUserInfo = AccessContext.getLoginUserInfo();
        BalanceInfo balanceInfo = new BalanceInfo(
                loginUserInfo.getId(), 0L
        );

        ScaCommerceBalance scaCommerceBalance = balanceDao.findByUserId(loginUserInfo.getId());
        if (null != scaCommerceBalance) {
            balanceInfo.setBalance(scaCommerceBalance.getBalance());
        } else {
            // 如果还没有用户余额记录, 这里创建出来,余额设定为0即可 (懒加载的思想)
            ScaCommerceBalance newBalance = new ScaCommerceBalance();
            newBalance.setUserId(loginUserInfo.getId());
            newBalance.setBalance(0L);
            log.info("初始化用户余额记录: [{}]", balanceDao.save(newBalance).getId());
        }

        return balanceInfo;
    }

    @Override
    public BalanceInfo deductBalance(BalanceInfo balanceInfo) {

        LoginUserInfo loginUserInfo = AccessContext.getLoginUserInfo();

        // 扣减用户余额的一个基本原则: 扣减额 <= 当前用户余额
        ScaCommerceBalance scaCommerceBalance = balanceDao.findByUserId(loginUserInfo.getId());

        // 当前余额 减去 传入的用户余额 小于 0 就抛出异常
        if (null == scaCommerceBalance || scaCommerceBalance.getBalance() - balanceInfo.getBalance() < 0) {
            throw new RuntimeException("用户余额不足!");
        }

        // 重新设置扣减之后的余额
        Long sourceBalance = scaCommerceBalance.getBalance();
        scaCommerceBalance.setBalance(scaCommerceBalance.getBalance() - balanceInfo.getBalance());
        // 重新设置后保存到数据库
        ScaCommerceBalance scaCommerceBalance1 = balanceDao.save(scaCommerceBalance);
        log.info("扣除余额: [{}], [{}], [{}]",
                // 获取保存之后的主键id
                scaCommerceBalance1.getId(),
                // 源余额
                sourceBalance,
                balanceInfo.getBalance());

        return new BalanceInfo(
                scaCommerceBalance.getUserId(),
                scaCommerceBalance.getBalance()
        );
    }
}

9.11.1 用户余额服务接口可用性测试(测试用例)

9.11.1.1 用于余额相关服务测试

package com.edcode.commerce.service;

import com.alibaba.fastjson.JSON;
import com.edcode.commerce.account.BalanceInfo;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 用于余额相关服务测试
 */
@Slf4j
public class BalanceServiceTest extends BaseTest {

	@Autowired
	private IBalanceService balanceService;

	/**
	 * 测试获取当前用户的余额信息
	 */
	@Test
	public void testGetCurrentUserBalanceInfo() {

		log.info("测试获取当前用户余额信息: [{}]",
				JSON.toJSONString(balanceService.getCurrentUserBalanceInfo()));
	}

	/**
	 * 测试扣减用于余额
	 */
	@Test
	public void testDeductBalance() {

		BalanceInfo balanceInfo = new BalanceInfo();
		balanceInfo.setUserId(loginUserInfo.getId());
		balanceInfo.setBalance(1000L);

		log.info("测试扣除余额: [{}]", JSON.toJSONString(balanceService.deductBalance(balanceInfo)));
	}
}

9.11.1.2 测试扣减用于余额需要修改数据库

  • 在第一次创建一条余额数据的时候,Balance 为零
  • 在【测试扣减余额】的时候,会提示:“用户余额不足!”
  • 需要修改余额比你扣减的余额大
update t_scacommerce_balance
set balance = 10000
where id = 1;

最终效果

在这里插入图片描述

9.12.1 用户账户微服务对外HTTP接口

9.12.1.1 用户地址服务 - Controller

package com.edcode.commerce.controller;

import com.edcode.commerce.account.AddressInfo;
import com.edcode.commerce.common.TableId;
import com.edcode.commerce.service.IAddressService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 用户地址服务
 */
@Api(tags = "用户地址服务")
@Slf4j
@RestController
@RequestMapping("/address")
@RequiredArgsConstructor
public class AddressController {

	private final IAddressService addressService;

	/**
	 * value 是简述, notes 是详细的描述信息
	 */
	@ApiOperation(value = "创建", notes = "创建用户地址信息", httpMethod = "POST")
	@PostMapping("/create-address")
	public TableId createAddressInfo(@RequestBody AddressInfo addressInfo) {
		return addressService.createAddressInfo(addressInfo);
	}

	@ApiOperation(value = "当前用户", notes = "获取当前登录用户地址信息", httpMethod = "GET")
	@GetMapping("/current-address")
	public AddressInfo getCurrentAddressInfo() {
		return addressService.getCurrentAddressInfo();
	}

	@ApiOperation(value = "获取用户地址信息", notes = "通过 id 获取用户地址信息, id 是 ScaCommerceAddress 表的主键", httpMethod = "GET")
	@GetMapping("/address-info")
	public AddressInfo getAddressInfoById(@RequestParam Long id) {
		return addressService.getAddressInfoById(id);
	}

	@ApiOperation(value = "获取用户地址信息", notes = "通过 TableId 获取用户地址信息", httpMethod = "POST")
	@PostMapping("/address-info-by-table-id")
	public AddressInfo getAddressInfoByTablesId(@RequestBody TableId tableId) {
		return addressService.getAddressInfoByTableId(tableId);
	}
}

9.12.1.2 用户余额服务 - Controller

package com.edcode.commerce.controller;

import com.edcode.commerce.account.BalanceInfo;
import com.edcode.commerce.service.IBalanceService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 用户余额服务
 */
@Api(tags = "用户余额服务")
@Slf4j
@RestController
@RequestMapping("/balance")
@RequiredArgsConstructor
public class BalanceController {

	private final IBalanceService balanceService;

	@ApiOperation(value = "当前用户", notes = "获取当前用户余额信息", httpMethod = "GET")
	@GetMapping("/current-balance")
	public BalanceInfo getCurrentUserBalanceInfo() {
		return balanceService.getCurrentUserBalanceInfo();
	}

	@ApiOperation(value = "扣减", notes = "扣减用于余额", httpMethod = "PUT")
	@PutMapping("/deduct-balance")
	public BalanceInfo deductBalance(@RequestBody BalanceInfo balanceInfo) {
		return balanceService.deductBalance(balanceInfo);
	}
}

9.13.1 验证用户账户微服务功能可用性(上)

9.13.1.1 account-address.http

### 创建用户地址信息
POST http://127.0.0.1:9001/edcode/scacommerce-account-service/address/create-address
Content-Type: application/json
sca-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJzY2EtY29tbWVyY2UtdXNlciI6IntcImlkXCI6MTEsXCJ1c2VybmFtZVwiOlwiZWRkaWVAcXEuY29tXCJ9IiwianRpIjoiNGNiMTg2NWYtN2EzMy00MjY2LWJiYjctYjdlZDY2NWZiZTAwIiwiZXhwIjoxNjM2NDczNjAwfQ.SUoIOcN5o9na1YKHeK0zTx2IWXjxIvu5Kkw3ndh7zLz61G1d-KJv3Ncmpi-PsALFWcQBxoWouZQIWoIaCufwsBLMCqP0xFD-iGdxcDK-pz03TOQgGwj4ncOFFdpoR3T3Qy-mnUTb2BJK18pC322GcilVO1aBlQ2aAFas78_bJe8nHEibetBJ68i5zRHX6jG3DAd33Lry1cUaQIAdimwBCaSld9bAv6dk3qpgzIs6AakDz_ctogSDhYCWT3yG2NRwGa4CSrz2QxxjcP-46FtGFty9w_EHJxP7h8QTMQs5JIFI7oQxFujEE9iMGASZsRy2Hd2iDj9rzlQlX5Cs9jpAnw

{
  "userId": 10,
  "addressItems": [
    {
      "username": "Eddie_Lee",
      "phone": "12345678910",
      "province": "广东省",
      "city": "广州市",
      "addressDetail": "天河区"
    }
  ]
}

### 当前登录用户地址信息
GET http://127.0.0.1:9001/edcode/scacommerce-account-service/address/current-address
Accept: application/json
sca-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJzY2EtY29tbWVyY2UtdXNlciI6IntcImlkXCI6MTEsXCJ1c2VybmFtZVwiOlwiZWRkaWVAcXEuY29tXCJ9IiwianRpIjoiNGNiMTg2NWYtN2EzMy00MjY2LWJiYjctYjdlZDY2NWZiZTAwIiwiZXhwIjoxNjM2NDczNjAwfQ.SUoIOcN5o9na1YKHeK0zTx2IWXjxIvu5Kkw3ndh7zLz61G1d-KJv3Ncmpi-PsALFWcQBxoWouZQIWoIaCufwsBLMCqP0xFD-iGdxcDK-pz03TOQgGwj4ncOFFdpoR3T3Qy-mnUTb2BJK18pC322GcilVO1aBlQ2aAFas78_bJe8nHEibetBJ68i5zRHX6jG3DAd33Lry1cUaQIAdimwBCaSld9bAv6dk3qpgzIs6AakDz_ctogSDhYCWT3yG2NRwGa4CSrz2QxxjcP-46FtGFty9w_EHJxP7h8QTMQs5JIFI7oQxFujEE9iMGASZsRy2Hd2iDj9rzlQlX5Cs9jpAnw

### 通过 id 获取用户地址信息
GET http://127.0.0.1:9001/edcode/scacommerce-account-service/address/address-info?id=1
Accept: application/json
sca-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJzY2EtY29tbWVyY2UtdXNlciI6IntcImlkXCI6MTEsXCJ1c2VybmFtZVwiOlwiZWRkaWVAcXEuY29tXCJ9IiwianRpIjoiNGNiMTg2NWYtN2EzMy00MjY2LWJiYjctYjdlZDY2NWZiZTAwIiwiZXhwIjoxNjM2NDczNjAwfQ.SUoIOcN5o9na1YKHeK0zTx2IWXjxIvu5Kkw3ndh7zLz61G1d-KJv3Ncmpi-PsALFWcQBxoWouZQIWoIaCufwsBLMCqP0xFD-iGdxcDK-pz03TOQgGwj4ncOFFdpoR3T3Qy-mnUTb2BJK18pC322GcilVO1aBlQ2aAFas78_bJe8nHEibetBJ68i5zRHX6jG3DAd33Lry1cUaQIAdimwBCaSld9bAv6dk3qpgzIs6AakDz_ctogSDhYCWT3yG2NRwGa4CSrz2QxxjcP-46FtGFty9w_EHJxP7h8QTMQs5JIFI7oQxFujEE9iMGASZsRy2Hd2iDj9rzlQlX5Cs9jpAnw

### 获取用户地址信息
POST http://127.0.0.1:9001/edcode/scacommerce-account-service/address/address-info-by-table-id
Content-Type: application/json
sca-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJzY2EtY29tbWVyY2UtdXNlciI6IntcImlkXCI6MTEsXCJ1c2VybmFtZVwiOlwiZWRkaWVAcXEuY29tXCJ9IiwianRpIjoiNGNiMTg2NWYtN2EzMy00MjY2LWJiYjctYjdlZDY2NWZiZTAwIiwiZXhwIjoxNjM2NDczNjAwfQ.SUoIOcN5o9na1YKHeK0zTx2IWXjxIvu5Kkw3ndh7zLz61G1d-KJv3Ncmpi-PsALFWcQBxoWouZQIWoIaCufwsBLMCqP0xFD-iGdxcDK-pz03TOQgGwj4ncOFFdpoR3T3Qy-mnUTb2BJK18pC322GcilVO1aBlQ2aAFas78_bJe8nHEibetBJ68i5zRHX6jG3DAd33Lry1cUaQIAdimwBCaSld9bAv6dk3qpgzIs6AakDz_ctogSDhYCWT3yG2NRwGa4CSrz2QxxjcP-46FtGFty9w_EHJxP7h8QTMQs5JIFI7oQxFujEE9iMGASZsRy2Hd2iDj9rzlQlX5Cs9jpAnw

{
  "ids": [
    {
      "id": 1
    },
    {
      "id": 2
    }
  ]
}

###
  1. sca-commerce-user 定义是 Token key 在 CommonConstant 里面
  2. 清空一下数据库,保持干净状态 TRUNCATE xxxx
  3. 如果出现 401 之类的,通常是 token过期,在请求一次:src/main/resources/http/authority.http

9.14.1 验证用户账户微服务功能可用性(下)

9.14.1.1 account-balance.http


### com.edcode.commerce.constant.CommonConstant.JWT_USER_INFO_KEY = sca-commerce-user
### TRUNCATE `t_scacommerce_balance`

### 获取当前用户余额信息
GET http://127.0.0.1:9001/edcode/scacommerce-account-service/balance/current-balance
Accept: application/json
sca-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJzY2EtY29tbWVyY2UtdXNlciI6IntcImlkXCI6MTEsXCJ1c2VybmFtZVwiOlwiZWRkaWVAcXEuY29tXCJ9IiwianRpIjoiNGNiMTg2NWYtN2EzMy00MjY2LWJiYjctYjdlZDY2NWZiZTAwIiwiZXhwIjoxNjM2NDczNjAwfQ.SUoIOcN5o9na1YKHeK0zTx2IWXjxIvu5Kkw3ndh7zLz61G1d-KJv3Ncmpi-PsALFWcQBxoWouZQIWoIaCufwsBLMCqP0xFD-iGdxcDK-pz03TOQgGwj4ncOFFdpoR3T3Qy-mnUTb2BJK18pC322GcilVO1aBlQ2aAFas78_bJe8nHEibetBJ68i5zRHX6jG3DAd33Lry1cUaQIAdimwBCaSld9bAv6dk3qpgzIs6AakDz_ctogSDhYCWT3yG2NRwGa4CSrz2QxxjcP-46FtGFty9w_EHJxP7h8QTMQs5JIFI7oQxFujEE9iMGASZsRy2Hd2iDj9rzlQlX5Cs9jpAnw

### 扣减用户余额
PUT http://127.0.0.1:9001/edcode/scacommerce-account-service/balance/deduct-balance
Content-Type: application/json
sca-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJzY2EtY29tbWVyY2UtdXNlciI6IntcImlkXCI6MTEsXCJ1c2VybmFtZVwiOlwiZWRkaWVAcXEuY29tXCJ9IiwianRpIjoiNGNiMTg2NWYtN2EzMy00MjY2LWJiYjctYjdlZDY2NWZiZTAwIiwiZXhwIjoxNjM2NDczNjAwfQ.SUoIOcN5o9na1YKHeK0zTx2IWXjxIvu5Kkw3ndh7zLz61G1d-KJv3Ncmpi-PsALFWcQBxoWouZQIWoIaCufwsBLMCqP0xFD-iGdxcDK-pz03TOQgGwj4ncOFFdpoR3T3Qy-mnUTb2BJK18pC322GcilVO1aBlQ2aAFas78_bJe8nHEibetBJ68i5zRHX6jG3DAd33Lry1cUaQIAdimwBCaSld9bAv6dk3qpgzIs6AakDz_ctogSDhYCWT3yG2NRwGa4CSrz2QxxjcP-46FtGFty9w_EHJxP7h8QTMQs5JIFI7oQxFujEE9iMGASZsRy2Hd2iDj9rzlQlX5Cs9jpAnw

{
  "userId": 10,
  "balance": 2000
}

###

9.15.1 用户账户微服务总结

9.15.1.1 用户账户微服务总结

  • 微服务开始之前的准备工作
    • 用户身份登录拦截
      • 在请求进入 service 之前解析 header 中的 token 信息,并填充用户信息到上下文中
      • 在请求结束之后,清理掉上下文中的用户信息
      • 对于一些特定的 HTTP 请求不要拦截(即白名单)
    • 代码即文档:引入 Swagger
      • pom.xml 中添加依赖配置
      • 自定义配置 Swagger
ThreadLocal存储用户信息
AccessContext
LoginUserInfoInterceptor 拦截器

9.15.1.2 微服务模块的设计思想

  • 微服务模块应该是低耦合,尽可能多的重用代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

eddie_k2

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值