为了体验从 0 到 1 的微服务改造过程,我们先使用 Spring Boot 搭建一个基础版的优惠券平台项目,等学习到 Spring Cloud 的时候,我们就在这个项目之上做微服务化改造,将 Spring Cloud 的各个组件像添砖加瓦一样集成到项目里。上一章节我们介绍了优惠券平台的功能模块,得知在用户领取优惠券的过程当中,优惠券是通过券模板来生成的,因此,优惠券模板服务是整个项目的底层基础服务。

目录

1 项目总体结构

1.1 搭建项目结构

1.2 添加 Maven 主依赖项

2 搭建 coupon-template-serv 模块

2.1 编写 coupon-template-serv 依赖项

2.2 搭建 coupon-template-api 子模块

①  通过 IDEA 搭建子项目

②  补充 template-api 依赖项

③  编写优惠券相关实体类

2.3 搭建 coupon-template-dao 子模块

①  补充 template-dao 依赖项

②  创建优惠券模板数据库对象

③  创建 CouponTemplateDao CRUD 接口

2.4 搭建 coupon-template-impl 子模块

①  补充 template-impl 依赖项

② 编写 template-impl 接口层

③ 编写 template-impl 启动类

④ 创建 template-impl 配置文件

3 搭建 coupon-calculation-serv 模块

3.1 编写 coupon-calculation-serv 依赖项

3.2 搭建 coupon-calculation-api 子模块

① 补充 calculation-api 依赖项

② 编写订单信息封装实体类

3.3 搭建 coupon-calculation-impl 子模块

① 补充 calculation-impl 依赖项

② 通过模板设计模式编写 template

③ 编写 calculation-impl 的 service 层

4 搭建 coupon-customer-serv 模块

4.1 编写 coupon-customer-serv 依赖项

4.2 搭建 coupon-customer-api 子模块

① 补充 customer-api 依赖项

② 编写 customer-api 请求参数封装类

4.3 搭建 coupon-customer-dao 子模块

① 补充 customer-dao 依赖项

② 创建优惠券数据库对象

③ 编写 customer-dao 接口类

4.4 搭建 coupon-customer-impl 子模块

① 补充 customer-impl 依赖项

③ 编写 customer-impl service 层

④ 编写 customer-impl Controller 层

⑤ 编写 customer-impl 启动类

5 总结 

项目源码:尹煜 / coupon-yinyu · GitCode

1 项目总体结构

首先我们来看看整体的项目结构是怎样搭建的

1.1 搭建项目结构

整个优惠券平台项目从 Maven 模块管理的角度划分为了多个模块:

在顶层项目 yinyu-coupon 之下有四个子模块:

coupon-template-serv: 创建、查找、克隆、删除优惠券模板coupon-calculation-serv:计算优惠后的订单价格、试算每个优惠券的优惠幅度coupon-customer-serv:通过调用 template 和 calculation 服务,实现用户领取优惠券、模拟计算最优惠的券、删除优惠券、下订单等操作middleware:存放一些与业务无关的平台类组件

在大型的微服务项目里,每一个子模块通常都存放在独立的 Git 仓库中,为了方便下载代码,所有模块的代码都打包在一个代码仓库【代码仓库-原版】,这里可以找到课程各阶段对应的源代码,本人也会在项目结束时讲代码上传。

在每一个以“-serv”结尾的业务子模块中,以内部分层的角度对其做了进一步拆分,以 coupon-template-serv 为例,它内部包含了三个子模块:

coupon-template-api:存放公共 POJO 类或者对外接口的子模块coupon-template-dao:存放数据库实体类和 Dao 层的子模块coupon-template-impl:核心业务逻辑的实现层,对外提供 REST API

你会发现,我们把 coupon-template-api 作为一个单独的模块,这样做的好处是:当某个上游服务需要获取 coupon-template-serv 的接口参数时,只要导入轻量级的 coupon-template-api 模块,就能够获取接口中定义的 Request 和 Response 的类模板,不需要引入多余的依赖项(比如 Dao 层或者 Service 层)。

这就是开闭原则的应用,它使各个模块间的职责和边界划分更加清晰,降低耦合的同时也更加利于依赖管理。

搭建好项目的结构之后(建议第一步搭建项目结构!),接下来我们借助 Maven 工具将需要的依赖包导入到项目中。

1.2 添加 Maven 主依赖项

需要注意的是,添加 Maven 依赖项需要遵循“从上到下”的原则,也就是从顶层项目 yinyu-coupon 开始,顺藤摸瓜直到 coupon-template-serv 下的子模块。首先,我们来看看顶层 geekbang-coupon 依赖项的编写。

编写 yinyu-coupon 依赖项 

yinyu-coupon 是整个实战项目的顶层项目,只用完成一个任务:管理子模块和定义 Maven 依赖项的版本。这就像一个公司的大 boss 一样,只用制定方向战略,琐碎的业务就交给下面人(子模块)来办就好了。

路径:pom.xml

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">

org.springframework.boot

spring-boot-starter-parent

2.4.2

4.0.0

yinyu-coupon

com.yinyu

pom

1.0-SNAPSHOT

coupon-template-serv

coupon-calculation-serv

coupon-customer-serv

middleware

8

8

org.springframework.cloud

spring-cloud-dependencies

2020.0.1

pom

import

com.alibaba.cloud

spring-cloud-alibaba-dependencies

2021.1

pom

import

org.apache.commons

commons-lang3

3.0

org.apache.commons

commons-collections4

4.0

commons-codec

commons-codec

1.9

com.alibaba

fastjson

1.2.31

org.projectlombok

lombok

1.18.20

jakarta.validation

jakarta.validation-api

2.0.2

com.google.guava

guava

16.0

在 pom 文件里有以下三个重点标签:

Ⅰ < parent > 标签

在 parent 标签中指定了 yinyu-coupon 项目的“父级依赖”为 spring-boot-starter-parent,这样一来,spring-boot-starter-parent 里定义的 Spring Boot 组件版本信息就会被自动带到子模块中。这种做法也是大多数 Spring Boot 项目的通用做法,不仅降低了依赖项管理的成本,也不需要担心各个组件间的兼容性问题。

Ⅱ < packaging > 标签

maven 的打包类型有三种:jar、war 和 pom。当我们指定 packaging 类型为 pom 时,意味着当前模块是一个“boss”,它只用关注顶层战略,即定义依赖项版本和整合子模块,不包含具体的业务实现。

Ⅲ < dependencymanagement > 标签

这个标签的作用和 < parent > 标签类似,两者都是将版本信息向下传递。dependencymanagement 是 boss 们定义顶层战略的地方,我们可以在这里定义各个依赖项的版本,当子项目需要引入这些依赖项的时候,只用指定 groupId 和 artifactId 即可,不用管 version 里该写哪个版本。

2 搭建 coupon-template-serv 模块

2.1 编写 coupon-template-serv 依赖项

coupon-template-serv 是大 boss 下面的一个小头目,和 yinyu-coupon 一样,它的 packaging 类型也是 pom。我们说过 boss 只用管顶层战略,因此 coupon-temolate-serv 的 pom 文件内容很简单,只是定义了父级项目和子模块。

路径:coupon-template-serv\pom.xml

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">

yinyu-coupon

com.yinyu

1.0-SNAPSHOT

../pom.xml

4.0.0

coupon-template-serv

pom

coupon-template-api

coupon-template-dao

coupon-template-impl

8

8

2.2 搭建 coupon-template-api 子模块

coupon-template-api 模块存放了接口 Request 和 Response 的类模板,是另两个子模块需要依赖的公共类库,所以我就先从 coupon-template-api 开始项目构建。

coupon-template-api 模块是专门用来存放公共类的仓库,我把 REST API 接口的服务请求和服务返回对象的 POJO 类放到了里面。在微服务领域,将外部依赖的 POJO 类或者 API 接口层单独打包是一种通用做法,这样就可以给外部依赖方提供一个“干净”(不包含非必要依赖)的接口包,为远程服务调用(RPC)提供支持。

①  通过 IDEA 搭建子项目

Ⅰ右键选择新建项目

Ⅱ 选择通过 Maven 创建

Ⅲ 填写相关信息

Ⅳ 新建软件包

②  补充 template-api 依赖项

通过 IDEA 搭建完 coupon-template-api 子项目后,只需要在 coupon-template-api 项目的 pom 文件中,添加了少量的“工具类”依赖,比如 lombok、guava 和 validation-api 包等通用组件。

路径:coupon-template-serv\coupon-template-api\pom.xml

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">

coupon-template-serv

com.yinyu

1.0-SNAPSHOT

4.0.0

coupon-template-api

8

8

org.apache.commons

commons-lang3

org.apache.commons

commons-collections4

commons-codec

commons-codec

com.alibaba

fastjson

org.projectlombok

lombok

jakarta.validation

jakarta.validation-api

com.google.guava

guava

③  编写优惠券相关实体类

首先,我们需要定义一个用来表示优惠券类型的 enum 对象,创建一个名为 CouponType 的枚举类。

路径:coupon-template-serv\coupon-template-api\src\main\java\com\yinyu\coupon\template\api\enums\CounponType.java

@Getter

@AllArgsConstructor

public enum CounponType {

UNKNOWN("unknown", "0"),

MONEY_OFF("满减券", "1"),

DISCOUNT("打折", "2"),

RANDOM_DISCOUNT("随机减", "3"),

LONELY_NIGHT_MONEY_OFF("寂寞午夜double券", "4"),

ANTI_PUA("PUA加倍奉还券", "5");

private String description;

// 存在数据库里的最终code

private String code;

public static CounponType convert(String code) {

return Stream.of(values())

.filter(couponType -> couponType.code.equalsIgnoreCase(code))

.findFirst()

.orElse(UNKNOWN);

}

}

CouponType 类定义了多个不同类型的优惠券,convert 方法可以根据优惠券的编码返回对应的枚举对象。这里还有一个“Unknown”类型的券,它专门用来对付故意输错 code 的恶意请求。

接下来,我们创建两个用来定义优惠券模板规则的类,分别是 TemplateRule 和 Discount。

路径:coupon-template-api\src\main\java\com\yinyu\coupon\template\api\beans\rules

TemplateRule 包含了两个规则,一是领券规则,包括每个用户可领取的数量和券模板的过期时间;二是券模板的计算规则。

@Builder

@Data

@NoArgsConstructor

@AllArgsConstructor

public class TemplateRule {

/** 可以享受的折扣 */

private Discount discount;

// 每个人最多可以领券数量

private Integer limitation;

// 过期时间

private Long deadline;

}

这里推荐使用一键三连的 lombok 注解自动生成基础代码,它们分别是 Data、NoArgsConstructor 、AllArgsConstructor 和 Builder 。其中,@Data 注解自动生成 getter、setter、toString 等方法,后两个注解分别生成无参构造器和全参构造器,省时省力省地盘,@Builder 则是用于简化实体的构建~

TemplateRule 中的 Discount 成员变量定义了使用优惠券的规则,代码如下:

@Builder

@Data

@NoArgsConstructor

@AllArgsConstructor

public class Discount {

// 对于满减券 - quota是减掉的钱数,单位是分

// 对于打折券 - quota是折扣(以100表示原价),90就是打9折, 95就是95折

// 对于随机立减券 - quota是最高的随机立减额

// 对于晚间特别优惠券 - quota是日间优惠额,晚间优惠翻倍

private Long quota;

// 订单最低要达到多少钱才能用优惠券,单位为分

private Long threshold;

}

从上面代码中可以看出,我使用 Long 来表示“金额”。对于境内电商行业来说,金额往往是以分为单位的,这样我们可以直接使用 Long 类型参与金额的计算,比如 100 就代表 100 分,也就是一块钱。这比使用 Double 到处转换 BigDecimal 省了很多事儿。

路径:coupon-template-api\src\main\java\com\yinyu\coupon\template\api\beans

创建一个名为 CouponTemplateInfo 的类,用来创建优惠券模板,代码如下:

/**

* 创建优惠券模板

*/

@Data

@NoArgsConstructor

@AllArgsConstructor

@Builder

public class CouponTemplateInfo {

private Long id;

@NotNull

private String name;

// 优惠券描述

@NotNull

private String desc;

// 优惠券类型

@NotNull

private String type;

// 适用门店 - 若无则为全店通用券

private Long shopId;

/** 优惠券规则 */

@NotNull

private TemplateRule rule;

private Boolean available;

}

在上面的代码中,我们应用了 jakarta.validate-api 组件的注解 @NotNull,对参数是否为 Null 进行了校验。如果请求参数为空,那么接口会自动返回 Bad Request 异常。当然,jakarta 组件还有很多可以用来做判定验证的注解,合理使用可以节省大量编码工作,提高代码可读性。

此外,你还会发现,CouponTemplateInfo 内封装了优惠券模板的基本信息,我们可以把优惠券模板当做一个“模具”,每一张优惠券都经由模具来制造,被制造出来的优惠券则使用 CouponInfo 对象来封装。CouponInfo 对象包含了优惠券的模板信息、领券用户 ID、适用门店 ID 等属性。

/**

* 封装优惠券信息

*/

@Data

@NoArgsConstructor

@AllArgsConstructor

@Builder

public class CouponInfo {

private Long id;

private Long templateId;

private Long userId;

private Long shopId;

private Integer status;

private CouponTemplateInfo template;

}

到这里我们就完成了 coupon-template-api 项目的搭建,下面我们开始搭建 Dao 层模块:coupon-template-dao。它主要负责和数据库的对接、读取。

2.3 搭建 coupon-template-dao 子模块

①  补充 template-dao 依赖项

通过 IDEA 搭建子项目后,我们把必要的依赖项添加到 coupon-template-dao 项目中,比较关键的 maven 依赖项有以下几个。

coupon-template-api: 引入 api 包下的公共类spring-boot-starter-data-jpa: 添加 spring-data-jpa 的功能进行 CRUD 操作mysql-connector-java: 引入 mysql 驱动包,驱动版本尽量与 mysql 版本保持一致

路径:coupon-template-serv\coupon-template-dao\pom.xml

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">

coupon-template-serv

com.yinyu

1.0-SNAPSHOT

../pom.xml

4.0.0

coupon-template-dao

8

8

${project.groupId}

coupon-template-api

${project.version}

org.springframework.boot

spring-boot-starter-validation

mysql

mysql-connector-java

8.0.21

runtime

org.springframework.boot

spring-boot-starter-data-jpa

②  创建优惠券模板数据库对象

路径:

coupon-template-serv\coupon-template-dao\src\main\java\com\yinyu\coupon\template\dao\entity

/**

* 优惠券模板

*/

@Data

@NoArgsConstructor

@AllArgsConstructor

@Entity

@Builder

@EntityListeners(AuditingEntityListener.class)

@Table(name = "coupon_template")

public class CouponTemplate implements Serializable {

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

@Column(name = "id", nullable = false)

private Long id;

// 状态是否可用

@Column(name = "available", nullable = false)

private Boolean available;

@Column(name = "name", nullable = false)

private String name;

// 适用门店-如果为空,则为全店满减券

@Column(name = "shop_id")

private Long shopId;

@Column(name = "description", nullable = false)

private String description;

// 优惠券类型

@Column(name = "type", nullable = false)

@Convert(converter = CouponTypeConverter.class)

private CounponType category;

// 创建时间,通过@CreateDate注解自动填值(需要配合@JpaAuditing注解在启动类上生效)

@CreatedDate

@Column(name = "created_time", nullable = false)

private Date createdTime;

// 优惠券核算规则,平铺成JSON字段

@Column(name = "rule", nullable = false)

@Convert(converter = RuleConverter.class)

private TemplateRule rule;

}

在 CouponTemplate 上,我们运用了 javax.persistence 包和 Spring JPA 包的标准注解,对数据库字段进行了映射,我挑几个关键注解说道一下。

Entity:声明了“数据库实体”对象,它是数据库 Table 在程序中的映射对象;Table:指定了 CouponTemplate 对应的数据库表的名称;ID/GeneratedValue:ID 注解将某个字段定义为唯一主键,GeneratedValue 注解指定了主键生成策略;Column:指定了每个类属性和数据库字段的对应关系,该注解还支持非空检测、对 update 和 create 语句进行限制等功能;CreatedDate:自动填充当前数据的创建时间;Convert:如果数据库中存放的是 code、string、数字等等标记化对象,可以使用 Convert 注解指定一个继承自 AttributeConverter 的类,将 DB 里存的内容转化成一个 Java 对象。

③  创建 CouponTemplateDao CRUD 接口

最后,我们来到定义 DAO 的地方,借助 Spring Data 的强大功能,我们只通过接口名称就可以声明一系列的 DB 层操作。

路径:

coupon-template-serv\coupon-template-dao\src\main\java\com\yinyu\coupon\template\dao\CouponTemplateDao.java

public interface CouponTemplateDao

extends JpaRepository {

// 根据Shop ID查询出所有券模板

List findAllByShopId(Long shopId);

// IN查询 + 分页支持的语法

Page findAllByIdIn(List Id, Pageable page);

// 根据shop ID + 可用状态查询店铺有多少券模板

Integer countByShopIdAndAvailable(Long shopId, Boolean available);

// 将优惠券设置为不可用

@Modifying

@Query("update CouponTemplate c set c.available = 0 where c.id = :id")

int makeCouponUnavailable(@Param("id") Long id);

}

其实,“增删改” 这些方法都在 CouponTemplateDao 所继承的 JpaRepository 类中。这个父类就像一个百宝箱,内置了各种各样的数据操作方法。我们可以通过内置的 save 方法完成对象的创建和更新,也可以使用内置的 delete 方法删除数据。

此外,它还提供了对“查询场景”的丰富支持,除了通过 ID 查询以外,我们还可以使用三种不同的方式查询数据。

通过接口名查询:将查询语句写到接口的名称中通过 Example 对象查询:构造一个模板对象,使用 findAll 方法来查询自定义查询:通过 Query 注解自定义复杂查询语句

在 CouponTemplateDao 中,第一个方法 findAllByShopId 就是通过接口名查询的例子,jpa 使用了一种约定大于配置的思想,你只需要把要查询的字段定义在接口的方法名中,在你发起调用时后台就会自动转化成可执行的 SQL 语句。构造方法名的过程需要遵循 < 起手式 >By< 查询字段 >< 连接词 > 的结构。

起手式:以 find 开头表示查询,以 count 开头表示计数查询字段:字段名要保持和 Entity 类中定义的字段名称一致连接词:每个字段之间可以用 And、Or、Before、After 等一些列丰富的连词串成一个查询语句

以接口名查询的方式虽然很省事儿,但它面对复杂查询却力不从心,一来容易导致接口名称过长,二来维护起来也挺吃力的。所以,对于复杂查询,我们可以使用自定义 SQL、或者 Example 对象查找的方式。

关于自定义 SQL,你可以参考 CouponTemplateDao 中的 makeCouponUnavailable 方法,我将 SQL 语句定义在了 Query 注解中,通过参数绑定的方式从接口入参处获取查询参数,这种方式是最接近 SQL 编码的 CRUD 方式。

Example 查询的方式也很简单,构造一个 CouponTemplate 的对象,将你想查询的字段值填入其中,做成一个查询模板,调用 Dao 层的 findAll 方法即可,这里留给你自己动手验证。

couponTemplate.setName("查询名称");

templateDao.findAll(Example.of(couponTemplate));

现在,API 和 Dao 层都已经准备就绪,万事俱备只差最后的业务逻辑层了,接下来我们去搭建 coupon-template-impl 模块。

2.4 搭建 coupon-template-impl 子模块

①  补充 template-impl 依赖项

coupon-template-impl 是 coupon-template-serv 下的一个子模块,也是实现业务逻辑的地方。从依赖管理的角度,它引入了 coupon-template-api 和 coupon-template-dao 两个内部依赖项到 pom.xml。

路径:coupon-template-serv\coupon-template-impl\pom.xml

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">

coupon-template-serv

com.yinyu

1.0-SNAPSHOT

../pom.xml

4.0.0

coupon-template-impl

8

8

${project.groupId}

coupon-template-api

${project.version}

${project.groupId}

coupon-template-dao

${project.version}

com.alibaba

fastjson

org.springframework.boot

spring-boot-starter-web

commons-codec

commons-codec

com.google.guava

guava

org.springframework.boot

spring-boot-starter-actuator

② 编写 template-impl 接口层

首先我们来定义 Service 层的接口类:CouponTemplateService。在这个接口中,我们定义了优惠券创建、查找优惠券和修改优惠券可用状态的方法。

public interface CouponTemplateService {

// 创建优惠券模板

CouponTemplateInfo createTemplate(CouponTemplateInfo request);

CouponTemplateInfo cloneTemplate(Long templateId);

// 模板查询(分页)

PagedCouponTemplateInfo search(TemplateSearchParams request);

// 通过模板ID查询优惠券模板

CouponTemplateInfo loadTemplateInfo(Long id);

// 让优惠券模板无效

void deleteTemplate(Long id);

// 批量查询

// Map是模板ID,key是模板详情

Map getTemplateInfoMap(Collection ids);

}

由于这部分比较简单,就是通过 CouponTemplateDao 层来实现优惠券模板的增删改查,这里就不展开介绍实现层代码了,你可以参考源码中的 CouponTemplateServiceImpl 类。

接下来,我们创建 CouponTemplateController 类对外暴露 REST API,可以借助 spring-web 注解来完成,具体代码如下。

@Slf4j

@RestController

@RequestMapping("/template")

public class CouponTemplateController {

@Autowired

private CouponTemplateService couponTemplateService;

// 创建优惠券

@PostMapping("/addTemplate")

public CouponTemplateInfo addTemplate(@Valid @RequestBody CouponTemplateInfo request) {

log.info("Create coupon template: data={}", request);

return couponTemplateService.createTemplate(request);

}

@PostMapping("/cloneTemplate")

public CouponTemplateInfo cloneTemplate(@RequestParam("id") Long templateId) {

log.info("Clone coupon template: data={}", templateId);

return couponTemplateService.cloneTemplate(templateId);

}

// 读取优惠券

@GetMapping("/getTemplate")

public CouponTemplateInfo getTemplate(@RequestParam("id") Long id){

log.info("Load template, id={}", id);

return couponTemplateService.loadTemplateInfo(id);

}

// 批量获取

@GetMapping("/getBatch")

public Map getTemplateInBatch(@RequestParam("ids") Collection ids) {

log.info("getTemplateInBatch: {}", JSON.toJSONString(ids));

return couponTemplateService.getTemplateInfoMap(ids);

}

// 搜索模板

@PostMapping("/search")

public PagedCouponTemplateInfo search(@Valid @RequestBody TemplateSearchParams request) {

log.info("search templates, payload={}", request);

return couponTemplateService.search(request);

}

// 优惠券无效化

@DeleteMapping("/deleteTemplate")

public void deleteTemplate(@RequestParam("id") Long id){

log.info("Load template, id={}", id);

couponTemplateService.deleteTemplate(id);

}

}

在这里,Controller 类中的注解来自 spring-boot-starter-web 依赖项,通过这些注解将服务以 RESTful 接口的方式对外暴露。现在,我们来了解下上述代码里,服务寻址过程中的三个重要注解:

RestController:用来声明一个 Controller 类,加载到 Spring Boot 上下文RequestMapping:指定当前类中所有方法在 URL 中的访问路径的前缀Post/Get/PutMapping:定义当前方法的 HTTP Method 和访问路径

③ 编写 template-impl 启动类

项目启动类是最后的代码部分,我们在 com.yinyu.coupon.template 下创建一个 Application 类作为启动程序的入口,并在这个类的头上安上 SpringBoot 的启动注解。

@SpringBootApplication

@EnableJpaAuditing

@ComponentScan(basePackages = {"com.yinyu"})

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

SpringBootApplication 注解会自动开启包路径扫描,并启动一系列的自动装配流程(AutoConfig)。在默认情况下,Spring Boot 框架会扫描启动类所在 package 下的所有类,并在上下文中创建受托管的 Bean 对象,如果我们想加载额外的扫包路径,只用添加 ComponentScan 注解并指定 path 即可。

④ 创建 template-impl 配置文件

所有代码环节全部完工后,我们还剩最后的画龙点睛之笔:创建配置文件 application.yml,它位于 src/main/resources 文件夹下。Spring Boot 支持多种格式的配置文件,这里我们顺应主流,使用 yml 格式。

# 项目的启动端口

server:

port: 20000

spring:

application:

# 定义项目名称

name: coupon-template-serv

datasource:

# mysql数据源

username: root

# password: 这里写上你自己的密码

url: jdbc:mysql://127.0.0.1:3306/geekbang_coupon_db?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&zeroDateTimeBehavior=convertToNull&serverTimezone=UTC

# 指定数据源DataSource类型

type: com.zaxxer.hikari.HikariDataSource

driver-class-name: com.mysql.cj.jdbc.Driver

# 数据库连接池参数配置,比如池子大小、超时时间、是否自动提交等等

hikari:

pool-name: GeekbangCouponHikari

connection-timeout: 5000

idle-timeout: 30000

maximum-pool-size: 10

minimum-idle: 5

max-lifetime: 60000

auto-commit: true

jpa:

show-sql: true

hibernate:

# 在生产环境全部为none,防止ddl结构被自动执行,破坏生产数据

ddl-auto: none

# 在日志中打印经过格式化的SQL语句

properties:

hibernate.format_sql: true

hibernate.show_sql: true

open-in-view: false

在配置文件中,有一个地方需要你多加注意,那就是 jdbc 连接串(spring.datasource.url)。不同版本的 MySQL 对连接串中的参数有不同的要求。

好,到这里,我们优惠券平台项目的第一个模块 coupon-template-serv 就搭建完成了,你可以在本地启动项目并通过 Postman 发起调用。

3 搭建 coupon-calculation-serv 模块

之前我们搭建了 coupon-template-serv 模块,实现了优惠券模板的创建和批量查询等功能,相信你已经对如何使用 Spring Boot 搭建应用驾轻就熟了。今天我们就来搭建优惠券平台项目的另外两个模块,coupon-calculation-serv(优惠计算服务)和 coupon-customer-serv(用户服务),组建一个完整的实战项目应用(middleware 模块将在 Spring Cloud 环节进行搭建)。

3.1 编写 coupon-calculation-serv 依赖项

coupon-calculation-serv 提供了用于计算订单的优惠信息的接口,它是一个典型的“计算密集型”服务。所谓计算密集型服务一般具备下面的两个特征:

不吃网络 IO 和磁盘空间运行期主要占用 CPU、内存等计算资源

在做大型应用架构的时候,我们通常会把计算密集型服务与 IO/ 存储密集型服务分割开来,这样做的一个主要原因是提高资源利用率。

比如说,我们有一个计算密集型的微服务 A 和一个 IO 密集型微服务 B,大促峰值流量到来的时候,如果微服务 A 面临的压力比较大,我可以专门调配高性能 CPU 和内存等“计算类”的资源去定向扩容 A 集群;如果微服务 B 压力吃紧了,我可以定向调拨云上的存储资源分配给 B 集群,这样就实现了一种“按需分配”。

假如微服务 A 和微服务 B 合二为一变成了一个服务,那么在分配资源的时候就无法做到定向调拨,全链路压测环节也难以精准定位各项性能指标,这难免出现资源浪费的情况。这也是为什么,我要把优惠计算这个服务单独拿出来的原因。

现在,我们开始着手搭建 coupon-calculation-serv 下的子模块。和 coupon-template-serv 结构类似,coupon-calculation-serv 下面也分了若干个子模块,包括 API 层和业务逻辑层。API 层定义了公共的 POJO 类,业务逻辑层主要实现优惠价格计算业务。因为 calculation 服务并不需要访问数据库,所以没有 DAO 模块。

路径:coupon-calculation-serv\pom.xml

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">

geekbang-coupon

com.geekbang

1.0-SNAPSHOT

4.0.0

coupon-calculation-serv

pom

coupon-calculation-impl

coupon-calculation-api

8

8

3.2 搭建 coupon-calculation-api 子模块

① 补充 calculation-api 依赖项

通过 IDEA 搭建子项目后,如果 coupon-calculation-serv 需要计算订单的优惠价格,那就得知道当前订单用了什么优惠券。封装了优惠券信息的 Java 类 CouponInfo 位于 coupon-template-api 包下,因此我们需要把 coupon-template-api 的依赖项加入到 coupon-calculation-api 中。

路径:coupon-calculation-serv\pom.xml

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">

coupon-calculation-serv

com.yinyu

1.0-SNAPSHOT

4.0.0

coupon-calculation-api

8

8

${project.groupId}

coupon-template-api

${project.version}

② 编写订单信息封装实体类

路径:coupon-calculation-api\src\main\java\com\yinyu\coupon\calculation\api\beans\Product.java

@Data

@NoArgsConstructor

@AllArgsConstructor

public class Product {

// 你可以试着搭建一个商品中心,用来存储商品信息,逐步完善这个应用

private Long productId;

// 商品的价格

private long price;

// 商品在购物车里的数量

private Integer count;

// 商品销售的门店

private Long shopId;

}

在上面的源码中,我们看到 ShoppingCart 订单类中使用了 Product 对象,来封装当前订单的商品列表。在 Product 类中包含了商品的单价、商品数量,以及当前商品的门店 ID。

路径:

coupon-calculation-api\src\main\java\com\yinyu\coupon\calculation\api\beans\ShoppingCart.java

@Data

@NoArgsConstructor

@AllArgsConstructor

public class Product {

// 你可以试着搭建一个商品中心,用来存储商品信息,逐步完善这个应用

private Long productId;

// 商品的价格

private long price;

// 商品在购物车里的数量

private Integer count;

// 商品销售的门店

private Long shopId;

}

在电商领域中,商品的数量通常不能以 Integer 整数来表示,这是因为只有标品才能以整数计件。对于像蔬菜、肉类等非标品来说,它们的计件单位并不是“个”。所以在实际项目中,尤其是零售行业的业务系统里,计件单位要允许小数位的存在。而我们的实战项目为了简化业务,就假定所有商品都是“标品”了。

在下单的时候,你可能有多张优惠券可供选择,你需要通过“价格试算”来模拟计算每张优惠券可以扣减的金额,进而选择最优惠的券来使用。SimulationOrder 和 SimulationResponse 分别代表了“价格试算”的订单类,以及返回的计算结果 Response。我们来看一下这两个类的源码。

路径:

coupon-calculation-api\src\main\java\com\yinyu\coupon\calculation\api\beans\SimulationOrder.java

// 试算最优的优惠券

@Data

@NoArgsConstructor

@AllArgsConstructor

public class SimulationOrder {

@NotEmpty

private List products;

@NotEmpty

private List couponIDs;

private List couponInfos;

@NotNull

private Long userId;

}

路径:

coupon-calculation-api\src\main\java\com\yinyu\coupon\calculation\api\beans\SimulationResponse.java

@Data

@NoArgsConstructor

public class SimulationResponse {

// 最省钱的coupon

private Long bestCouponId;

// 每一个coupon对应的order价格

private Map couponToOrderPrice = Maps.newHashMap();

}

到这里,coupon-calculation-api 模块就搭建好了。因为 calculation 服务不需要访问数据库,所以我们就不用搭建 dao 模块了,直接来实现 coupon-calculation-impl 业务层的代码逻辑。

3.3 搭建 coupon-calculation-impl 子模块

① 补充 calculation-impl 依赖项

路径:coupon-calculation-serv\coupon-calculation-impl\pom.xml

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">

coupon-calculation-serv

com.yinyu

1.0-SNAPSHOT

4.0.0

coupon-calculation-impl

8

8

${project.groupId}

coupon-template-api

${project.version}

${project.groupId}

coupon-calculation-api

${project.version}

org.springframework.boot

spring-boot-starter-web

从 coupon-template-api 和 coupon-calculation-api 两个依赖项中,你可以拿到订单优惠计算过程用到的 POJO 对象。接下来,我们可以动手实现优惠计算逻辑了。

② 通过模板设计模式编写 template

在搭建优惠计算业务逻辑的过程中,我运用了模板设计模式来封装计算逻辑。模板模式是一种基于抽象类的设计模式,它的思想很简单,就是将共性的算法骨架部分上升到抽象层,将个性部分延迟到子类中去实现。

在搭建优惠计算业务逻辑的过程中,我运用了模板设计模式来封装计算逻辑。模板模式是一种基于抽象类的设计模式,它的思想很简单,就是将共性的算法骨架部分上升到抽象层,将个性部分延迟到子类中去实现。

优惠券类型有很多种,比如满减券、打折券、随机立减等等,这些券的计算流程(共性部分)是相同的,但具体的计算规则(个性部分)是不同的。我将共性的部分抽象成了 AbstractRuleTemplate 抽象类,将各个券的差异性计算方式做成了抽象类的子类。

让我们看一下计算逻辑的类结构图:

在这张图里,顶层接口 RuleTemplate 定义了 calculate 方法,抽象模板类 AbstractRuleTemplate 将通用的模板计算逻辑在 calculate 方法中实现,同时它还定义了一个抽象方法 calculateNewPrice 作为子类的扩展点。各个具体的优惠计算类通过继承 AbstractRuleTemplate,并实现 calculateNewPrice 来编写自己的优惠计算方式。

我们先来看一下 AbstractRuleTemplate 抽象类的代码,走读 calculate 模板方法中的计算逻辑实现。

路径:

coupon-calculation-serv\coupon-calculation-impl\src\main\java\com\yinyu\coupon\calculation\template\AbstractRuleTemplate.java

/**

* 定义通用的计算逻辑

*/

@Slf4j

public abstract class AbstractRuleTemplate implements RuleTemplate {

@Override

public ShoppingCart calculate(ShoppingCart order) {

// 获取订单总价

Long orderTotalAmount = getTotalPrice(order.getProducts());

// 获取以shopId为维度的价格统计

Map sumAmount = this.getTotalPriceGroupByShop(order.getProducts());

// 以下规则只做单个优惠券的计算

CouponTemplateInfo template = order.getCouponInfos().get(0).getTemplate();

// 最低消费限制

Long threshold = template.getRule().getDiscount().getThreshold();

// 优惠金额或者打折比例

Long quota = template.getRule().getDiscount().getQuota();

// 当前优惠券适用的门店ID,如果为空则作用于全店券

Long shopId = template.getShopId();

// 如果优惠券未指定shopId,shopTotalAmount=orderTotalAmount

// 如果指定了shopId,则shopTotalAmount=对应门店下商品总价

Long shopTotalAmount = (shopId == null) ? orderTotalAmount : sumAmount.get(shopId);

// 如果不符合优惠券使用标准, 则直接按原价走,不使用优惠券

if (shopTotalAmount == null || shopTotalAmount < threshold) {

log.warn("Totals of amount not meet, ur coupons are not applicable to this order");

order.setCost(orderTotalAmount);

order.setCouponInfos(Collections.emptyList());

return order;

}

// 子类中计算新的价格

Long newCost = calculateNewPrice(orderTotalAmount, shopTotalAmount, quota);

// 订单价格不能小于最低价格

if (newCost < minCost()) {

newCost = minCost();

}

order.setCost(newCost);

log.debug("original price={}, new price={}", orderTotalAmount, newCost);

return order;

}

// 金额计算具体逻辑,延迟到子类实现

abstract protected Long calculateNewPrice(Long orderTotalAmount, Long shopTotalAmount, Long quota);

// 计算订单总价

protected long getTotalPrice(List products) {

return products.stream()

.mapToLong(product -> product.getPrice() * product.getCount())

.sum();

}

// 根据门店维度计算每个门店下商品价格

// key = shopId

// value = 门店商品总价

protected Map getTotalPriceGroupByShop(List products) {

Map groups = products.stream()

.collect(Collectors.groupingBy(m -> m.getShopId(),

Collectors.summingLong(p -> p.getPrice() * p.getCount()))

);

return groups;

}

// 每个订单最少必须支付1分钱

protected long minCost() {

return 1L;

}

protected long convertToDecimal(Double value) {

return new BigDecimal(value).setScale(0, RoundingMode.HALF_UP).longValue();

}

}

在上面的源码中,我们看到大部分计算逻辑都在抽象类中做了实现,子类只要实现 calculateNewPrice 方法完成属于自己的订单价格计算就好。我们以满减规则类为例来看一下它的实现,其他的规则类见源码。

路径:

coupon-calculation-serv\coupon-calculation-impl\src\main\java\com\yinyu\coupon\calculation\template\impl\MoneyOffTemplate.java

/**

* 满减优惠券计算规则

*/

@Slf4j

@Component

public class MoneyOffTemplate extends AbstractRuleTemplate implements RuleTemplate {

@Override

protected Long calculateNewPrice(Long totalAmount, Long shopAmount, Long quota) {

// benefitAmount是扣减的价格

// 如果当前门店的商品总价

Long benefitAmount = shopAmount < quota ? shopAmount : quota;

return totalAmount - benefitAmount;

}

}

在上面的源码中,我们看到子类业务的逻辑非常简单清爽。通过模板设计模式,我在抽象类中封装了共性逻辑,在子类中扩展了可变逻辑,每个子类只用关注自己的特定实现即可,使得代码逻辑变得更加清晰,大大降低了代码冗余。

随着业务发展,你的优惠券模板类型可能会进一步增加,比如赠品券、随机立减券等等,如果当前的抽象类无法满足新的需求,本项目通过建立多级抽象类的方式进一步增加抽象层次,不断将共性不变的部分抽取为抽象层。

路径:

coupon-calculation-serv\coupon-calculation-impl\src\main\java\com\yinyu\coupon\calculation\template\RuleTemplate.java

public interface RuleTemplate {

// 计算优惠券

ShoppingCart calculate(ShoppingCart settlement);

}

路径:

coupon-calculation-serv\coupon-calculation-impl\src\main\java\com\yinyu\coupon\calculation\template\CouponTemplateFactory.java

// 工厂方法根据订单中的优惠券信息,返回对应的Template用于计算优惠价

@Component

@Slf4j

public class CouponTemplateFactory {

@Autowired

private MoneyOffTemplate moneyOffTemplate;

@Autowired

private DiscountTemplate discountTemplate;

@Autowired

private RandomReductionTemplate randomReductionTemplate;

@Autowired

private LonelyNightTemplate lonelyNightTemplate;

@Autowired

private DummyTemplate dummyTemplate;

@Autowired

private AntiPauTemplate antiPauTemplate;

public RuleTemplate getTemplate(ShoppingCart order) {

// 不使用优惠券

if (CollectionUtils.isEmpty(order.getCouponInfos())) {

// dummy模板直接返回原价,不进行优惠计算

return dummyTemplate;

}

// 获取优惠券的类别

// 目前每个订单只支持单张优惠券

CouponTemplateInfo template = order.getCouponInfos().get(0).getTemplate();

CounponType category = CounponType.convert(template.getType());

switch (category) {

// 订单满减券

case MONEY_OFF:

return moneyOffTemplate;

// 随机立减券

case RANDOM_DISCOUNT:

return randomReductionTemplate;

// 午夜下单优惠翻倍

case LONELY_NIGHT_MONEY_OFF:

return lonelyNightTemplate;

// 打折券

case DISCOUNT:

return discountTemplate;

case ANTI_PUA:

return antiPauTemplate;

// 未知类型的券模板

default:

return dummyTemplate;

}

}

}

③ 编写 calculation-impl 的 service 层

创建完优惠计算逻辑,我们接下来看一下 Service 层的代码实现逻辑。Service 层的 calculateOrderPrice 代码非常简单,通过 CouponTemplateFactory 工厂类获取到具体的计算规则,然后调用 calculate 计算订单价格就好了。simulate 方法实现了订单价格试算,帮助用户在下单之前了解每个优惠券可以扣减的金额,从而选出最省钱的那个券。

路径:

coupon-calculation-serv\coupon-calculation-impl\src\main\java\com\yinyu\coupon\calculation\controller\service\CouponCalculationServiceImpl.java

@Slf4j

@Service

public class CouponCalculationServiceImpl implements CouponCalculationService {

@Autowired

private CouponTemplateFactory couponProcessorFactory;

// 优惠券结算

// 这里通过Factory类决定使用哪个底层Rule,底层规则对上层透明

@Override

public ShoppingCart calculateOrderPrice(@RequestBody ShoppingCart cart) {

log.info("calculate order price: {}", JSON.toJSONString(cart));

RuleTemplate ruleTemplate = couponProcessorFactory.getTemplate(cart);

return ruleTemplate.calculate(cart);

}

// 对所有优惠券进行试算,看哪个最赚钱

@Override

public SimulationResponse simulateOrder(@RequestBody SimulationOrder order) {

SimulationResponse response = new SimulationResponse();

Long minOrderPrice = Long.MAX_VALUE;

// 计算每一个优惠券的订单价格

for (CouponInfo coupon : order.getCouponInfos()) {

ShoppingCart cart = new ShoppingCart();

cart.setProducts(order.getProducts());

cart.setCouponInfos(Lists.newArrayList(coupon));

cart = couponProcessorFactory.getTemplate(cart).calculate(cart);

Long couponId = coupon.getId();

Long orderPrice = cart.getCost();

// 设置当前优惠券对应的订单价格

response.getCouponToOrderPrice().put(couponId, orderPrice);

// 比较订单价格,设置当前最优优惠券的ID

if (minOrderPrice > orderPrice) {

response.setBestCouponId(coupon.getId());

minOrderPrice = orderPrice;

}

}

return response;

}

}

在上面的源码中,我们看到,优惠券结算方法不用关心订单上使用的优惠券是满减券还是打折券,因为工厂方法会将子类转为顶层接口 RuleTemplate 返回。在写代码的过程中,我们也要有这样一种意识,就是尽可能对上层业务屏蔽其底层业务复杂度,底层具体业务逻辑的修改对上层是无感知的,这其实也是开闭原则的思想。

完成 Service 层后,我们接下来新建一个 CouponCalculationController 类,对外暴露 2 个 POST 接口,第一个接口完成订单优惠价格计算,第二个接口完成优惠券价格试算。

路径:

coupon-calculation-serv\coupon-calculation-impl\src\main\java\com\yinyu\coupon\calculation\controller\CouponCalculationController.java

@Slf4j

@RestController

@RequestMapping("calculator")

public class CouponCalculationController {

@Autowired

private CouponCalculationService calculationService;

// 优惠券结算

@PostMapping("/checkout")

@ResponseBody

public ShoppingCart calculateOrderPrice(@RequestBody ShoppingCart settlement) {

log.info("do calculation: {}", JSON.toJSONString(settlement));

return calculationService.calculateOrderPrice(settlement);

}

// 优惠券列表挨个试算

// 给客户提示每个可用券的优惠额度,帮助挑选

@PostMapping("/simulate")

@ResponseBody

public SimulationResponse simulate(@RequestBody SimulationOrder simulator) {

log.info("do simulation: {}", JSON.toJSONString(simulator));

return calculationService.simulateOrder(simulator);

}

}

好了,现在你已经完成了所有业务逻辑的源码。最后一步画龙点睛,你还需要为 coupon-calculation-impl 应用创建一个 Application 启动类并添加 application.yml 配置项。因为它并不需要访问数据库,所以你不需要在配置文件或者启动类注解上添加 spring-data 的相关内容。

到这里,我们就完成了优惠计算服务的搭建工作,你可以到代码仓库中查看完整的 coupon-calculation-serv 源码实现。

4 搭建 coupon-customer-serv 模块

coupon-customer-serv 是一个服务于用户的子模块,它的结构和 coupon-template-serv 一样,包含了 API 层、DAO 层和业务逻辑层。它实现了用户领券、用户优惠券查找和订单结算功能。为了简化业务逻辑,我在源码里省略了“用户注册”等业务功能,使用 userId 来表示一个已注册的用户。

4.1 编写 coupon-customer-serv 依赖项

路径:coupon-customer-serv\pom.xml

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">

yinyu-coupon

com.yinyu

1.0-SNAPSHOT

../pom.xml

4.0.0

coupon-customer-serv

pom

coupon-customer-api

coupon-customer-dao

coupon-customer-impl

8

8

4.2 搭建 coupon-customer-api 子模块

按照惯例,我们先从 API 层开始搭建,搭建 coupon-customer-api 的过程非常简单。

① 补充 customer-api 依赖项

首先,我们需要把 coupon-template-api 和 coupon-calculation-api 这两个服务的依赖项添加到 coupon-customer-api 的 pom 依赖中,这样一来 customer 服务就可以引用到这两个服务的 Request 和 Response 对象了。

路径:coupon-customer-serv\coupon-customer-api\pom.xml

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">

coupon-customer-serv

com.yinyu

1.0-SNAPSHOT

4.0.0

coupon-customer-api

8

8

${project.groupId}

coupon-template-api

${project.version}

${project.groupId}

coupon-calculation-api

${project.version}

② 编写 customer-api 请求参数封装类

接下来,我们在 API 子模块中创建一个 RequestCoupon 类,作为用户领取优惠券的请求参数,通过传入用户 ID 和优惠券模板 ID,用户可以领取一张由指定模板打造的优惠券。另一个类是 SearchCoupon,用来封装优惠券查询的请求参数。

package com.yinyu.coupon.customer.api.beans;

@Data

@NoArgsConstructor

@AllArgsConstructor

public class RequestCoupon {

// 用户领券

@NotNull

private Long userId;

// 券模板ID

@NotNull

private Long couponTemplateId;

}

package com.yinyu.coupon.customer.api.beans;

@Data

@NoArgsConstructor

@AllArgsConstructor

public class SearchCoupon {

@NotNull

private Long userId;

private Long shopId;

private Integer couponStatus;

}

4.3 搭建 coupon-customer-dao 子模块

我在 DAO 子模块中创建了一个 Coupon 数据库实体对象用于保存用户领到的优惠券,并按照 spring-data-jpa 规范创建了一个 CouponDAO 接口用来提供 CRUD 操作。

① 补充 customer-dao 依赖项

路径:coupon-customer-serv\coupon-customer-dao\pom.xml

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">

coupon-customer-serv

com.yinyu

1.0-SNAPSHOT

4.0.0

coupon-customer-dao

8

8

${project.groupId}

coupon-customer-api

${project.version}

org.springframework.boot

spring-boot-starter-data-jpa

org.springframework.boot

spring-boot-starter-validation

mysql

mysql-connector-java

8.0.21

runtime

② 创建优惠券数据库对象

路径:coupon-customer-dao\src\main\java\com\yinyu\coupon\customer\dao\entity\Coupon.java

@Builder

@Data

@NoArgsConstructor

@AllArgsConstructor

@Entity

@EntityListeners(AuditingEntityListener.class)

@Table(name = "coupon")

public class Coupon {

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

@Column(name = "id", nullable = false)

private Long id;

// 对应的模板ID - 不使用one to one映射

// 不推荐使用级联查询的原因是为了防止滥用而导致的DB性能问题

@Column(name = "template_id", nullable = false)

private Long templateId;

// 所有者的用户ID

@Column(name = "user_id", nullable = false)

private Long userId;

// 冗余一个shop id方便查找

@Column(name = "shop_id")

private Long shopId;

// 优惠券的使用/未使用状态

@Column(name = "status", nullable = false)

@Convert(converter = CouponStatusConverter.class)

private CouponStatus status;

// 被Transient标记的属性是不会被持久化的

@Transient

private CouponTemplateInfo templateInfo;

// 获取时间自动生成

@CreatedDate

@Column(name = "created_time", nullable = false)

private Date createdTime;

}

③ 编写 customer-dao 接口类

public interface CouponDao extends JpaRepository {

long countByUserIdAndTemplateId(Long userId, Long templateId);

}

在上面的源码中,我们只创建了一个接口用于 count 计算,至于其他增删改查功能则统一由父类 JpaRepository 一手包办了。spring-data-jpa 沿袭了 spring 框架的简约风,大道至简解放双手,整个 Spring 框架从诞生至今,也一直都在朝着不断简化的方向发展。到这里,coupon-customer-dao 层的代码就写完了,接下来我们去搞定最后一个子模块 coupon-customer-impl 业务逻辑层。

4.4 搭建 coupon-customer-impl 子模块

既然 coupon-customer-impl 需要调用 template 和 calculation 两个服务,在没有进入微服务化改造之前,我们只能先暂时委屈一下 template 和 calculation,将它俩作为 customer 服务的一部分,做成一个三合一的单体应用,之后单体应用会被拆分成独立的微服务模块。

① 补充 customer-impl 依赖项

将 template、calculation 的依赖项添加到 coupon-customer-impl 的配置文件中,注意这里我们添加的可不是 API 接口层的依赖,而是 Impl 接口实现层的依赖。

路径:coupon-customer-serv\coupon-customer-impl\pom.xml

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">

coupon-customer-serv

com.yinyu

1.0-SNAPSHOT

4.0.0

coupon-customer-impl

8

8

${project.groupId}

coupon-customer-dao

${project.version}

${project.groupId}

coupon-calculation-impl

${project.version}

${project.groupId}

coupon-template-impl

${project.version}

org.springframework.boot

spring-boot-starter-web

org.springframework.boot

spring-boot-maven-plugin

repackage

③ 编写 customer-impl service 层

CouponCustomerService 是业务逻辑层的接口抽象,添加了几个方法,用来实现用户领券、查询优惠券、下单核销优惠券、优惠券试算等功能。

package com.yinyu.coupon.customer.service.intf;

// 用户对接服务

public interface CouponCustomerService {

// 领券接口

Coupon requestCoupon(RequestCoupon request);

// 核销优惠券

ShoppingCart placeOrder(ShoppingCart info);

// 优惠券金额试算

SimulationResponse simulateOrderPrice(SimulationOrder order);

void deleteCoupon(Long userId, Long couponId);

// 查询用户优惠券

List findCoupon(SearchCoupon request);

}

这里,我以 placeOrder 方法为例,带你走读一下它的源码。如果你对其它方法的源码感兴趣,可以到代码仓库找到 Spring Boot 急速落地篇的 CouponCustomerServiceImpl 类,查看源代码。

placeOrder 方法实现了用户下单 + 优惠券核销的功能,我们来看一下它的实现逻辑。

@Override

@Transactional

public ShppingCart placeOrder(ShppingCart order) {

// 购物车为空,丢出异常

if (CollectionUtils.isEmpty(order.getProducts())) {

log.error("invalid check out request, order={}", order);

throw new IllegalArgumentException("cart is empty");

}

Coupon coupon = null;

if (order.getCouponId() != null) {

// 如果有优惠券就把它查出来,看是不是属于当前用户并且可用

Coupon example = Coupon.builder().userId(order.getUserId())

.id(order.getCouponId())

.status(CouponStatus.AVAILABLE)

.build();

coupon = couponDao.findAll(Example.of(example)).stream()

.findFirst()

// 如果当前用户查不到可用优惠券,就抛出错误

.orElseThrow(() -> new RuntimeException("Coupon not found"));

// 优惠券有了,再把它的券模板信息查出

// 券模板里的Discount规则会在稍后用于订单价格计算

CouponInfo couponInfo = CouponConverter.convertToCoupon(coupon);

couponInfo.setTemplate(templateService.loadTemplateInfo(coupon.getTemplateId()));

order.setCouponInfos(Lists.newArrayList(couponInfo));

}

// 调用calculation服务使用优惠后的订单价格

ShppingCart checkoutInfo = calculationService.calculateOrderPrice(order);

if (coupon != null) {

// 如果优惠券没有被结算掉,而用户传递了优惠券,报错提示该订单满足不了优惠条件

if (CollectionUtils.isEmpty(checkoutInfo.getCouponInfos())) {

log.error("cannot apply coupon to order, couponId={}", coupon.getId());

throw new IllegalArgumentException("coupon is not applicable to this order");

}

log.info("update coupon status to used, couponId={}", coupon.getId());

coupon.setStatus(CouponStatus.USED);

couponDao.save(coupon);

}

return checkoutInfo;

}

在上面的源码中,我们看到 Coupon 对象的构造使用了 Builder 链式编程的风格,这是得益于在 Coupon 类上面声明的 Lombok 的 Builder 注解,只用一个 Builder 注解就能享受链式构造的体验。

④ 编写 customer-impl Controller 层

搞定了业务逻辑层后,接下来轮到 Controller 部分了,我在 CouponCustomerController 中对外暴露了几个服务,这些服务调用 CouponCustomerServiceImpl 中的方法实现各自的业务逻辑。

package com.yinyu.coupon.customer.controller;

@Slf4j

@RestController

@RequestMapping("coupon-customer")

public class CouponCustomerController {

@Autowired

private CouponCustomerService customerService;

@PostMapping("requestCoupon")

public Coupon requestCoupon(@Valid @RequestBody RequestCoupon request) {

return customerService.requestCoupon(request);

}

// 用户删除优惠券

@DeleteMapping("deleteCoupon")

public void deleteCoupon(@RequestParam("userId") Long userId,

@RequestParam("couponId") Long couponId) {

customerService.deleteCoupon(userId, couponId);

}

// 用户模拟计算每个优惠券的优惠价格

@PostMapping("simulateOrder")

public SimulationResponse simulate(@Valid @RequestBody SimulationOrder order) {

return customerService.simulateOrderPrice(order);

}

// ResponseEntity - 指定返回状态码 - 可以作为一个课后思考题

@PostMapping("placeOrder")

public ShoppingCart checkout(@Valid @RequestBody ShoppingCart info) {

return customerService.placeOrder(info);

}

// 实现的时候最好封装一个search object类

@PostMapping("findCoupon")

public List findCoupon(@Valid @RequestBody SearchCoupon request) {

return customerService.findCoupon(request);

}

}

⑤ 编写 customer-impl 启动类

以上,就是所有的业务逻辑代码部分了。接下来你只需要完成启动类和配置文件,就可以启动项目做测试了。我先来带你看一下启动类的部分:

package com.yinyu.coupon.customer;

@SpringBootApplication

@EnableJpaAuditing

@ComponentScan(basePackages = {"com.yinyu"})

@EnableTransactionManagement

//用于扫描Dao @Repository

@EnableJpaRepositories(basePackages = {"com.yinyu"})

//用于扫描JPA实体类 @Entity,默认扫本包当下路径

@EntityScan(basePackages = {"com.geekbang"})

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

在上面的源码中,我们看到很多注解上都注明了 com.yinyu作为包路径。之所以这么做,是因为 Spring Boot 的潜规则是将当前启动类类所在 package 作为扫包路径。

如果你的 Application 在 com.yinyu.customer 下,而你在项目中又需要加载来自 com.yinyu.template 下的类资源,就必须额外声明扫包路径,否则只有在 com.yinyu.customer 和其子路径之下的资源才会被加载。

关于配置项的部分,你可以直接把 coupon-template-impl 的配置文件 application.yml 照搬过来,不过,要记得把里面配置的 spring.application.name 改成 coupon-customer-serv。

好,到这里,我们优惠券平台项目的 Spring Boot 版本就搭建完成了。现在,coupon-customer-serv 已经成了一个三合一的单体应用,你只要在本地启动这一个应用,就可以调用 customer、template 和 calculation 三个服务的功能。

5 总结

我们来回顾一下这两节 Spring Boot 实战课的重点内容。通过这两节课,我带你搭建了完整的 Spring Boot 版优惠券平台的三个子模块。为了让项目结构更加清晰,我用分层设计的思想将每个模块拆分成 API 层、DAO 层和业务层。在搭建过程中,我们使用 spring-data-jpa 搞定了数据层,短短几行代码就能实现复杂的 CRUD 操作;使用 spring-web 搭建了 Controller 层,对外暴露了 RESTFul 风格的接口。

接下来我们将进入 Spring Cloud 基础的学习,将学习使用 Nacos、Loadbalancer 和 OpenFeign 组件来搭建基于微服务架构的跨服务调用。

相关链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: