创建子模块service作为父模块。pom示例
<?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>e-commerce-springcloud</artifactId>
<groupId>com.taluohui.ecommerce</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>e-commerce-service</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>e-commerce-service-config</module>
</modules>
<!-- 模块名及描述信息 -->
<name>e-commerce-service</name>
<description>电商服务模块父模块</description>
</project>
用户身份登录统一拦截
在service下面,创建子模块service-config,pom示例
<?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>e-commerce-service</artifactId>
<groupId>com.taluohui.ecommerce</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>e-commerce-service-config</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!-- 模块名及描述信息 -->
<name>e-commerce-service-config</name>
<description>服务通用配置</description>
<dependencies>
<dependency>
<groupId>com.taluohui.ecommerce</groupId>
<artifactId>e-commerce-mvc-config</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
创建filter包,在filter包里,创建AccessContext类,这个类用于将传入的用户信息保存在ThreadLocal中,方便之后的服务使用。
/**
* <h1>使用 ThreadLocal 去单独存储每一个线程携带的 LoginUserInfo 信息</h1>
* 要及时的清理我们保存到 ThreadLocal 中的用户信息:
* 1. 保证没有资源泄露
* 2. 保证线程在重用时, 不会出现数据混乱
* */
public class AccessContext {
private static final ThreadLocal<LoginUserInfo> loginUserInfo = new ThreadLocal<>();
public static LoginUserInfo getLoginUserInfo() {
return loginUserInfo.get();
}
public static void setLoginUserInfo(LoginUserInfo loginUserInfo_) {
loginUserInfo.set(loginUserInfo_);
}
public static void clearLoginUserInfo() {
loginUserInfo.remove();
}
}
在filter中创建过滤器LoginUserInfoInterceptor,用于从Header中拿到包含用户信息的Token,再从Token中拿到对应的用户信息,并将用户信息通过AccessContext保存在ThreadLocal中。还要再请求结束时,清理ThreadLocal中的用户信息,防止内存泄漏。
@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("parse login user info error: [{}]", ex.getMessage(), ex);
}
// 如果程序走到这里, 说明 header 中没有 token 信息
if (null == loginUserInfo) {
throw new RuntimeException("can not parse current login user");
}
log.info("set login user info: [{}]", request.getRequestURI());
// 设置当前请求上下文, 把用户信息填充进去
AccessContext.setLoginUserInfo(loginUserInfo);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
}
/**
* <h2>在请求完全结束后调用, 常用于清理资源等工作</h2>
* */
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
if (null != AccessContext.getLoginUserInfo()) {
AccessContext.clearLoginUserInfo();
}
}
}
创建conf包,创建TaluohuiWebMvcConfig类,让过滤器生效
/**
* <h1>Web Mvc 配置</h1>
* */
@Configuration
public class TaluohuiWebMvcConfig extends WebMvcConfigurationSupport {
/**
* <h2>添加拦截器配置</h2>
* */
@Override
protected void addInterceptors(InterceptorRegistry registry) {
// 添加用户身份统一登录拦截的拦截器
registry.addInterceptor(new LoginUserInfoInterceptor())
.addPathPatterns("/**").order(0);
}
}
集成Swagger2实现代码即文档
因为我们需要暴露的接口都在功能模块中,只需要在service模块中加入依赖
<!-- swagger 用于定义 API 文档 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!-- 美化 swagger -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.3</version>
</dependency>
第二步:配置Swagger2 ,指定扫描包与描述展示信息,在conf包下,创建Swagger 配置类
/**
* <h1>Swagger 配置类</h1>
* 原生: /swagger-ui.html
* 美化: /doc.html
* */
@Configuration
@EnableSwagger2
public class SwaggerConfig {
/**
* <h2>Swagger 实例 Bean 是 Docket, 所以通过配置 Docket 实例来配置 Swagger</h2>
* */
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
// 展示在 Swagger 页面上的自定义工程描述信息
.apiInfo(apiInfo())
// 选择展示哪些接口
.select()
// 只有 com.taluohui.ecommerce 包内的才去展示
.apis(RequestHandlerSelectors.basePackage("com.taluohui.ecommerce"))
.paths(PathSelectors.any())
.build();
}
/**
* <h2>Swagger 的描述信息</h2>
* */
public ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("shuai-micro-service")
.description("e-commerce-springcloud-service")
.contact(new Contact(
"shuai", "www.taluohui.com", "taluohui@163.com"
))
.version("1.0")
.build();
}
}
第三步:Web MVC配置:让MVC加载Swagger静态资源,需要在之前的Web Mvc 配置类TaluohuiWebMvcConfig中加一段
/**
* <h2>让 MVC 加载 Swagger 的静态资源</h2>
* */
@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);
}
第四步:需要在之前的过滤器LoginUserInfoInterceptor中添加白名单
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 部分请求不需要带有身份信息, 即白名单
if (checkWhiteListUrl(request.getRequestURI())) {
return true;
}
·············
}
/**
* <h2>校验是否是白名单接口</h2>
* swagger2 接口
* */
private boolean checkWhiteListUrl(String url) {
return StringUtils.containsAny(
url,
"springfox", "swagger", "v2",
"webjars", "doc.html"
);
}
用户账户微服务
在service模块下创建子模块account-service
pom示例如下
<?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>e-commerce-service</artifactId>
<groupId>com.taluohui.ecommerce</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>e-commerce-account-service</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!-- 模块名及描述信息 -->
<name>e-commerce-account-service</name>
<description>账户服务</description>
<dependencies>
<!-- spring cloud alibaba nacos discovery 依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- zipkin = spring-cloud-starter-sleuth + spring-cloud-sleuth-zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.5.0.RELEASE</version>
</dependency>
<!-- Java Persistence API, ORM 规范 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL 驱动, 注意, 这个需要与 MySQL 版本对应 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.12</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.taluohui.ecommerce</groupId>
<artifactId>e-commerce-service-config</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.taluohui.ecommerce</groupId>
<artifactId>e-commerce-service-sdk</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<!--
SpringBoot的Maven插件, 能够以Maven的方式为应用提供SpringBoot的支持,可以将
SpringBoot应用打包为可执行的jar或war文件, 然后以通常的方式运行SpringBoot应用
-->
<build>
<finalName>${artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
添加配置文件
server:
port: 8003
servlet:
context-path: /ecommerce-account-service
spring:
application:
name: e-commerce-account-service
cloud:
nacos:
discovery:
enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
server-addr: 1.15.247.9:8848
# server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850 # Nacos 服务器地址
namespace: 22d40198-8462-499d-a7fe-dbb2da958648
metadata:
management:
context-path: ${server.servlet.context-path}/actuator
kafka:
bootstrap-servers: 1.15.247.9:9092
producer:
retries: 3
consumer:
auto-offset-reset: latest
sleuth:
sampler:
probability: 1.0 # 采样比例, 1.0 表示 100%, 默认是 0.1
zipkin:
sender:
type: kafka # 默认是 web
base-url: http://1.15.247.9:9411/
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://1.15.247.9:3306/ecommerce?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: Cjw970404
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
启动类AccountApplication
/**
* <h1>用户账户微服务启动入口</h1>
* 127.0.0.1:8003/ecommerce-account-service/doc.html
*/
@EnableJpaAuditing
@SpringBootApplication
@EnableDiscoveryClient
public class AccountApplication {
public static void main(String[] args) {
SpringApplication.run(AccountApplication.class, args);
}
}
sql语句
-- 创建 t_ecommerce_address 数据表
CREATE TABLE IF NOT EXISTS `ecommerce`.`t_ecommerce_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='用户地址表';
实体类
/**
* <h1>用户地址表实体类定义</h1>
* */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "t_ecommerce_address")
public class EcommerceAddress {
/** 自增主键 */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
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;
/** 创建时间 */
@CreatedDate
@Column(name = "create_time", nullable = false)
private Date createTime;
/** 更新时间 */
@LastModifiedDate
@Column(name = "update_time", nullable = false)
private Date updateTime;
/**
* <h2>根据 userId + AddressItem 得到 EcommerceAddress</h2>
* */
public static EcommerceAddress to(Long userId, AddressInfo.AddressItem addressItem) {
EcommerceAddress ecommerceAddress = new EcommerceAddress();
ecommerceAddress.setUserId(userId);
ecommerceAddress.setUsername(addressItem.getUsername());
ecommerceAddress.setPhone(addressItem.getPhone());
ecommerceAddress.setProvince(addressItem.getProvince());
ecommerceAddress.setCity(addressItem.getCity());
ecommerceAddress.setAddressDetail(addressItem.getAddressDetail());
return ecommerceAddress;
}
/**
* <h2>将 EcommerceAddress 对象转成 AddressInfo</h2>
* */
public AddressInfo.AddressItem toAddressItem() {
AddressInfo.AddressItem addressItem = new AddressInfo.AddressItem();
addressItem.setId(this.id);
addressItem.setUsername(this.username);
addressItem.setPhone(this.phone);
addressItem.setProvince(this.province);
addressItem.setCity(this.city);
addressItem.setAddressDetail(this.addressDetail);
addressItem.setCreateTime(this.createTime);
addressItem.setUpdateTime(this.updateTime);
return addressItem;
}
}
dao,使用JPA
/**
* <h1>EcommerceAddress Dao 接口定义</h1>
* */
public interface EcommerceAddressDao extends JpaRepository<EcommerceAddress, Long> {
/**
* <h2>根据 用户 id 查询地址信息</h2>
* */
List<EcommerceAddress> findAllByUserId(Long UserId);
}
在service模块下创建子模块service-sdk
专门用于存储微服务之间传递的值对象
pom
<?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>e-commerce-service</artifactId>
<groupId>com.taluohui.ecommerce</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>e-commerce-service-sdk</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!-- 模块名及描述信息 -->
<name>e-commerce-service-sdk</name>
<description>服务模块 SDK</description>
</project>
创建一个对应account微服务的文件夹account
用户地址信息对象类
/**
* <h1>用户地址信息</h1>
* */
@ApiModel(description = "用户地址信息")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AddressInfo {
@ApiModelProperty(value = "地址所属用户 id")
private Long userId;
@ApiModelProperty(value = "地址详细信息")
private List<AddressItem> addressItems;
/**
* <h2>单个的地址信息</h2>
* */
@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;
}
/**
* <h2>将 AddressItem 转换成 UserAddress</h2>
* */
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;
}
}
}
用户地址信息对象类
/**
* <h1>用户地址信息</h1>
* */
@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;
}
用户余额信息
sql
-- 创建 t_ecommerce_balance 数据表
CREATE TABLE IF NOT EXISTS `ecommerce`.`t_ecommerce_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='用户账户余额表';
实体类
/**
* <h1>用户账户余额表实体类定义</h1>
* */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "t_ecommerce_balance")
public class EcommerceBalance {
/** 自增主键 */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
/** 用户 id */
@Column(name = "user_id", nullable = false)
private Long userId;
/** 账户余额 */
@Column(name = "balance", nullable = false)
private Long balance;
/** 创建时间 */
@CreatedDate
@Column(name = "create_time", nullable = false)
private Date createTime;
/** 更新时间 */
@LastModifiedDate
@Column(name = "update_time", nullable = false)
private Date updateTime;
}
Dao
/**
* <h1>EcommerceBalance Dao 接口定义</h1>
* */
public interface EcommerceBalanceDao extends JpaRepository<EcommerceBalance, Long> {
/** 根据 userId 查询 EcommerceBalance 对象 */
EcommerceBalance findByUserId(Long userId);
}
在sdk模块中的account包下,也创建一个用于服务之间传值的类
/**
* <h1>用户账户余额信息</h1>
* */
@ApiModel(description = "用户账户余额信息")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BalanceInfo {
@ApiModelProperty(value = "用户主键 id")
private Long userId;
@ApiModelProperty(value = "用户账户余额")
private Long balance;
}
下面是做service层的业务操作
首先要在sdk模块中定义通用的实体类,哪怕是只有一个参数,也要定义成类,方便以后的扩展。
/**
* <h1>主键 ids</h1>
* */
@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;
}
}
定义service层用户地址功能的接口
/**
* <h1>用户地址相关服务接口定义</h1>
* */
public interface IAddressService {
/**
* <h2>创建用户地址信息</h2>
* */
TableId createAddressInfo(AddressInfo addressInfo);
/**
* <h2>获取当前登录的用户地址信息</h2>
* */
AddressInfo getCurrentAddressInfo();
/**
* <h2>通过 id 获取用户地址信息, id 是 EcommerceAddress 表的主键</h2>
* */
AddressInfo getAddressInfoById(Long id);
/**
* <h2>通过 TableId 获取用户地址信息</h2>
* */
AddressInfo getAddressInfoByTableId(TableId tableId);
}
用户余额的接口
/**
* <h2>用于余额相关的服务接口定义</h2>
* */
public interface IBalanceService {
/**
* <h2>获取当前用户余额信息</h2>
* */
BalanceInfo getCurrentUserBalanceInfo();
/**
* <h2>扣减用户余额</h2>
* @param balanceInfo 代表想要扣减的余额
* */
BalanceInfo deductBalance(BalanceInfo balanceInfo);
}
实现地址功能的service
/**
* <h1>用户地址相关服务接口实现</h1>
* */
@Slf4j
@Service
@Transactional(rollbackFor = Exception.class)
public class AddressServiceImpl implements IAddressService {
final EcommerceAddressDao addressDao;
public AddressServiceImpl(EcommerceAddressDao addressDao) {
this.addressDao = addressDao;
}
/**
* <h2>存储多个地址信息</h2>
* */
@Override
public TableId createAddressInfo(AddressInfo addressInfo) {
// 不能直接从参数中获取用户的 id 信息
LoginUserInfo loginUserInfo = AccessContext.getLoginUserInfo();
// 将传递的参数转换成实体对象
List<EcommerceAddress> ecommerceAddresses = addressInfo.getAddressItems().stream()
.map(a -> EcommerceAddress.to(loginUserInfo.getId(), a))
.collect(Collectors.toList());
// 保存到数据表并把返回记录的 id 给调用方
List<EcommerceAddress> savedRecords = addressDao.saveAll(ecommerceAddresses);
List<Long> ids = savedRecords.stream()
.map(EcommerceAddress::getId).collect(Collectors.toList());
log.info("create address info: [{}], [{}]", loginUserInfo.getId(),
JSON.toJSONString(ids));
return new TableId(
ids.stream().map(TableId.Id::new).collect(Collectors.toList())
);
}
@Override
public AddressInfo getCurrentAddressInfo() {
LoginUserInfo loginUserInfo = AccessContext.getLoginUserInfo();
// 根据 userId 查询到用户的地址信息, 再实现转换
List<EcommerceAddress> ecommerceAddresses = addressDao.findAllByUserId(
loginUserInfo.getId()
);
List<AddressInfo.AddressItem> addressItems = ecommerceAddresses.stream()
.map(EcommerceAddress::toAddressItem)
.collect(Collectors.toList());
return new AddressInfo(loginUserInfo.getId(), addressItems);
}
@Override
public AddressInfo getAddressInfoById(Long id) {
EcommerceAddress ecommerceAddress = addressDao.findById(id).orElse(null);
if (null == ecommerceAddress) {
throw new RuntimeException("address is not exist");
}
return new AddressInfo(
ecommerceAddress.getUserId(),
Collections.singletonList(ecommerceAddress.toAddressItem())
);
}
@Override
public AddressInfo getAddressInfoByTableId(TableId tableId) {
List<Long> ids = tableId.getIds().stream()
.map(TableId.Id::getId).collect(Collectors.toList());
log.info("get address info by table id: [{}]", JSON.toJSONString(ids));
List<EcommerceAddress> ecommerceAddresses = addressDao.findAllById(ids);
if (CollectionUtils.isEmpty(ecommerceAddresses)) {
return new AddressInfo(-1L, Collections.emptyList());
}
List<AddressInfo.AddressItem> addressItems = ecommerceAddresses.stream()
.map(EcommerceAddress::toAddressItem)
.collect(Collectors.toList());
return new AddressInfo(
ecommerceAddresses.get(0).getUserId(), addressItems
);
}
}
测试可用性
/**
* <h1>用户账户微服务环境可用性校验</h1>
* */
@SpringBootTest
@RunWith(SpringRunner.class)
public class AccountApplicationTest {
@Test
public void contextLoad() {
}
}
创建service包,在里面添加测试基类,用于往LoginUserInfo里面添加用户信息
/**
* <h1>测试用例基类, 填充登录用户信息</h1>
* */
@SpringBootTest
@RunWith(SpringRunner.class)
public abstract class BaseTest {
protected final LoginUserInfo loginUserInfo = new LoginUserInfo(
10L, "shuai"
);
/**
* <h2>在测试之前将用户信息塞入LoginUserInfo</h2>
*/
@Before
public void init() {
AccessContext.setLoginUserInfo(loginUserInfo);
}
/**
* <h2>在测试之后将用户信息清除</h2>
*/
@After
public void destroy() {
AccessContext.clearLoginUserInfo();
}
}
创建地址功能的测试类
/**
* <h1>用户地址相关服务功能测试</h1>
* */
@Slf4j
public class AddressServiceTest extends BaseTest {
@Autowired
private IAddressService addressService;
/**
* <h2>测试创建用户地址信息</h2>
* */
@Test
public void testCreateAddressInfo() {
AddressInfo.AddressItem addressItem = new AddressInfo.AddressItem();
addressItem.setUsername("Qinyi");
addressItem.setPhone("18800000001");
addressItem.setProvince("上海市");
addressItem.setCity("上海市");
addressItem.setAddressDetail("陆家嘴");
log.info("test create address info: [{}]", JSON.toJSONString(
addressService.createAddressInfo(
new AddressInfo(loginUserInfo.getId(),
Collections.singletonList(addressItem))
)
));
}
/**
* <h2>测试获取当前登录用户地址信息</h2>
* */
@Test
public void testGetCurrentAddressInfo() {
log.info("test get current user info: [{}]", JSON.toJSONString(
addressService.getCurrentAddressInfo()
));
}
/**
* <h2>测试通过 id 获取用户地址信息</h2>
* */
@Test
public void testGetAddressInfoById() {
log.info("test get address info by id: [{}]", JSON.toJSONString(
addressService.getAddressInfoById(10L)
));
}
/**
* <h2>测试通过 TableId 获取用户地址信息</h2>
* */
@Test
public void testGetAddressInfoByTableId() {
log.info("test get address info by table id: [{}]", JSON.toJSONString(
addressService.getAddressInfoByTableId(
new TableId(Collections.singletonList(new TableId.Id(10L)))
)
));
}
}
实现余额功能的service
/**
* <h1>用于余额相关服务接口实现</h1>
*/
@Slf4j
@Service
@Transactional(rollbackFor = Exception.class)
public class BalanceServiceImpl implements IBalanceService {
@Autowired
EcommerceBalanceDao balanceDao;
/**
* <h2>获取当前用户余额信息</h2>
*/
@Override
public BalanceInfo getCurrentUserBalanceInfo() {
LoginUserInfo loginUserInfo = AccessContext.getLoginUserInfo();
BalanceInfo balanceInfo = new BalanceInfo(
loginUserInfo.getId(), 0L
);
EcommerceBalance ecommerceBalance = balanceDao.findByUserId(loginUserInfo.getId());
if(null != ecommerceBalance) {
balanceInfo.setBalance(ecommerceBalance.getBalance());
} else {
// 如果还没有用户余额记录, 这里创建出来,余额设定为0即可
EcommerceBalance newBalance = new EcommerceBalance();
newBalance.setUserId(loginUserInfo.getId());
newBalance.setBalance(0L);
log.info("init user balance record: [{}]",
balanceDao.save(newBalance).getId());
}
return balanceInfo;
}
/**
* <h2>扣减用户余额</h2>
*
* @param balanceInfo 代表想要扣减的余额
*/
@Override
public BalanceInfo deductBalance(BalanceInfo balanceInfo) {
LoginUserInfo loginUserInfo = AccessContext.getLoginUserInfo();
// 扣减用户余额的一个基本原则: 扣减额 <= 当前用户余额
EcommerceBalance ecommerceBalance =
balanceDao.findByUserId(loginUserInfo.getId());
if (null == ecommerceBalance
|| ecommerceBalance.getBalance() - balanceInfo.getBalance() < 0
) {
throw new RuntimeException("user balance is not enough!");
}
Long sourceBalance = ecommerceBalance.getBalance();
ecommerceBalance.setBalance(ecommerceBalance.getBalance() - balanceInfo.getBalance());
log.info("deduct balance: [{}], [{}], [{}]",
balanceDao.save(ecommerceBalance).getId(), sourceBalance,
balanceInfo.getBalance());
return new BalanceInfo(
ecommerceBalance.getUserId(),
ecommerceBalance.getBalance()
);
}
}
测试可用性
/**
* <h1>用于余额相关服务测试</h1>
* */
@Slf4j
public class BalanceServiceTest extends BaseTest {
@Autowired
private IBalanceService balanceService;
/**
* <h2>测试获取当前用户的余额信息</h2>
* */
@Test
public void testGetCurrentUserBalanceInfo() {
log.info("test get current user balance info: [{}]", JSON.toJSONString(
balanceService.getCurrentUserBalanceInfo()
));
}
/**
* <h2>测试扣减用于余额</h2>
* */
@Test
public void testDeductBalance() {
BalanceInfo balanceInfo = new BalanceInfo();
balanceInfo.setUserId(loginUserInfo.getId());
balanceInfo.setBalance(1000L);
log.info("test deduct balance: [{}]", JSON.toJSONString(
balanceService.deductBalance(balanceInfo)
));
}
}
Controller层的实现
/**
* <h1>用户地址服务 Controller</h1>
* */
@Api(tags = "用户地址服务")
@Slf4j
@RestController
@RequestMapping("/address")
public class AddressController {
private final IAddressService addressService;
public AddressController(IAddressService addressService) {
this.addressService = 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 是 EcommerceAddress 表的主键",
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);
}
}
/**
* <h1>用户余额服务 Controller</h1>
* */
@Api(tags = "用户余额服务")
@Slf4j
@RestController
@RequestMapping("/balance")
public class BalanceController {
private final IBalanceService balanceService;
public BalanceController(IBalanceService balanceService) {
this.balanceService = 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);
}
}
用户账户微服务已经编写完毕,需要将服务配置到gateway上。打开nacos的配置界面,在gateway的配置里再添加一段配置
此时开启gateway服务和用户账户服务,测试账户服务的可用性