【Java开发】Spring Cloud 03 :Spring Boot 项目搭建

news2024/12/23 22:59:49

为了体验从 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 总结 


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

<?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>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.2</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>

    <artifactId>yinyu-coupon</artifactId>
    <groupId>com.yinyu</groupId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>

    <modules>
        <module>coupon-template-serv</module>
        <module>coupon-calculation-serv</module>
        <module>coupon-customer-serv</module>
        <module>middleware</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencyManagement>
    <!-- 子项目会继承父项目中的依赖。子项目在需要依赖时,先从自身pom中查找,如果没有找到,就从父项目的pom中查找,与Java中的继承机制类似。 -->
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>2020.0.1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2021.1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.0</version>
            </dependency>

            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-collections4</artifactId>
                <version>4.0</version>
            </dependency>

            <dependency>
                <groupId>commons-codec</groupId>
                <artifactId>commons-codec</artifactId>
                <version>1.9</version>
            </dependency>

            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.31</version>
            </dependency>

            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.20</version>
            </dependency>

            <dependency>
                <groupId>jakarta.validation</groupId>
                <artifactId>jakarta.validation-api</artifactId>
                <version>2.0.2</version>
            </dependency>

            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>16.0</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

在 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

<?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>yinyu-coupon</artifactId>
        <groupId>com.yinyu</groupId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <modelVersion>4.0.0</modelVersion>

    <artifactId>coupon-template-serv</artifactId>
    <packaging>pom</packaging>

    <modules>
        <module>coupon-template-api</module>
        <module>coupon-template-dao</module>
        <module>coupon-template-impl</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

</project>

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

<?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>coupon-template-serv</artifactId>
        <groupId>com.yinyu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>coupon-template-api</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
        </dependency>

        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>


        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
        </dependency>

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </dependency>
    </dependencies>
</project>

③  编写优惠券相关实体类

首先,我们需要定义一个用来表示优惠券类型的 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

<?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>coupon-template-serv</artifactId>
        <groupId>com.yinyu</groupId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>coupon-template-dao</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- DAO层理论上不应该依赖API层,这里懒省事儿用了一套POJO从前到后传递 -->
        <!-- 正确的做法是dao层定义自己的DTO,然后上层用一个converter再转化成API里的类 -->
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>coupon-template-api</artifactId>
            <version>${project.version}</version>
        </dependency>

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

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.21</version>
            <scope>runtime</scope>
        </dependency>

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

</project>

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

路径:

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<CouponTemplate, Long> {

    // 根据Shop ID查询出所有券模板
    List<CouponTemplate> findAllByShopId(Long shopId);

    // IN查询 + 分页支持的语法
    Page<CouponTemplate> findAllByIdIn(List<Long> 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

<?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>coupon-template-serv</artifactId>
        <groupId>com.yinyu</groupId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>coupon-template-impl</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>coupon-template-api</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>coupon-template-dao</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!--阿里巴巴开源的Java对象和JSON格式字符串的快速转换的工具库-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>

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

        <!-- 用来处理常用的编码方法的工具类包,例如SHA1、MD5、Base64,URL,Soundx等等 -->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </dependency>

        <!-- Actuator 对微服务端点进行管理和配置监控 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

    </dependencies>

    <!-- 如果要以java -jar命令执行这个应用,记得把这个注解打开
         如果这个项目是作为其它项目的依赖,那么就不要添加下面这个注解。

         在Spring Boot阶段,由于我们是通过customer服务来跑单体应用,所以不用加
         在Spring Cloud阶段,以java -jar 运行项目必须在各个impl项目的pom中加上这个注解
    -->
    <!--    <build>
            <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>

② 编写 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<Long, CouponTemplateInfo> getTemplateInfoMap(Collection<Long> 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<Long, CouponTemplateInfo> getTemplateInBatch(@RequestParam("ids") Collection<Long> 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

<?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>geekbang-coupon</artifactId>
        <groupId>com.geekbang</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>coupon-calculation-serv</artifactId>
    <packaging>pom</packaging>
    <modules>
        <module>coupon-calculation-impl</module>
        <module>coupon-calculation-api</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

</project>

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

<?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>coupon-calculation-serv</artifactId>
        <groupId>com.yinyu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>coupon-calculation-api</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>coupon-template-api</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>
    
</project>

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

路径: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<Product> products;

    @NotEmpty
    private List<Long> couponIDs;

    private List<CouponInfo> 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<Long, Long> 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

<?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>coupon-calculation-serv</artifactId>
        <groupId>com.yinyu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>coupon-calculation-impl</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>coupon-template-api</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>coupon-calculation-api</artifactId>
            <version>${project.version}</version>
        </dependency>

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

    <!-- 如果要以java -jar命令执行这个应用,记得把这个Plugins注解打开
         如果这个项目是作为其它项目的依赖,那么就不要添加下面这个注解。

         在Spring Boot阶段,由于我们是通过customer服务来跑单体应用,所以不用加
         在Spring Cloud阶段,以java -jar 运行项目必须在各个impl项目的pom中加上这个注解
     -->
    <!--    <build>
            <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>

从 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<Long, Long> 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<Product> products) {
        return products.stream()
                .mapToLong(product -> product.getPrice() * product.getCount())
                .sum();
    }

    // 根据门店维度计算每个门店下商品价格
    // key = shopId
    // value = 门店商品总价
    protected Map<Long, Long> getTotalPriceGroupByShop(List<Product> products) {
        Map<Long, Long> 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是扣减的价格
        // 如果当前门店的商品总价<quota,那么最多只能扣减shopAmount的钱数
        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

<?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>yinyu-coupon</artifactId>
        <groupId>com.yinyu</groupId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <modelVersion>4.0.0</modelVersion>

    <artifactId>coupon-customer-serv</artifactId>
    <packaging>pom</packaging>

    <modules>
        <module>coupon-customer-api</module>
        <module>coupon-customer-dao</module>
        <module>coupon-customer-impl</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

</project>

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

<?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>coupon-customer-serv</artifactId>
        <groupId>com.yinyu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>coupon-customer-api</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>coupon-template-api</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>coupon-calculation-api</artifactId>
            <version>${project.version}</version>
        </dependency>

    </dependencies>

</project>

② 编写 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

<?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>coupon-customer-serv</artifactId>
        <groupId>com.yinyu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>coupon-customer-dao</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- DAO层理论上不应该依赖API层,我这里懒省事儿用了一套POJO从前到后传递 -->
        <!-- 正确的做法是dao层定义自己的DTO,然后上层用一个converter再转化成API里的类 -->
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>coupon-customer-api</artifactId>
            <version>${project.version}</version>
        </dependency>

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

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

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.21</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

</project>

② 创建优惠券数据库对象

路径: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<Coupon, Long> {

    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

<?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>coupon-customer-serv</artifactId>
        <groupId>com.yinyu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>coupon-customer-impl</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>coupon-customer-dao</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>coupon-calculation-impl</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>coupon-template-impl</artifactId>
            <version>${project.version}</version>
        </dependency>

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

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

③ 编写 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<CouponInfo> 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<CouponInfo> 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 组件来搭建基于微服务架构的跨服务调用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/147335.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

jacoco:java代码覆盖率实践

文章目录一、jacoco基本了解二、实践准备三、jacoco使用3.1 插桩3.2 dump:覆盖率文件导出3.3 report:可视化报告3.4 merge:合并覆盖率文件四、相关命令扩展4.1 javaagent4.2 dump4.3 merge4.4 report五、资源链接一、jacoco基本了解 jacoco是一款面向java的代码覆盖率工具&…

linux系统中C++中构造与析构函数以及this的使用方法

大家好&#xff0c;今天主要和大家聊一聊&#xff0c;C里面的基本语法结构以及对应的操作方法。 目录 第一&#xff1a;构造函数与析构函数 第二&#xff1a;this指针 第一&#xff1a;构造函数与析构函数 什么是构造函数&#xff1f;构造函数在对象实例化时被系统自动调用&a…

xshell连接Linux一直失败解决方法

文章目录解决对象方法配置防火墙关闭Linux防火墙关闭Windows防火墙xshell连接Linux一直失败解决方法 解决对象 可能出现以下两个问题&#xff1a; Linux防火墙已关闭和Windows防火墙已经关闭配置好 vim /etc/sysconfig/network-scripts/ifcfg-ens33 方法 配置 这个是最容易…

linux系统中实现C++中继承和重载的方法

大家好&#xff0c;今天主要和大家聊一聊&#xff0c;如何实现C中继承和重载的功能。 第一&#xff1a;C中的继承功能 面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类&#xff0c;这使得创建和维护一个应用程序变得更容易。这样做&#xff0…

CLRNet: Cross Layer Refinement Network for Lane Detection

Paper name CLRNet: Cross Layer Refinement Network for Lane Detection Paper Reading Note URL: https://arxiv.org/pdf/2203.10350.pdf TL;DR CVPR 2022 文章&#xff0c;自动驾驶公司飞步科技与浙大联合出品。lane anchor-based 方案&#xff0c;在多个数据集上取得 …

Linux中的哈希表:基于双链表的内核模块

1. 前言Linux内核中选取双向链表作为其基本的数据结构&#xff0c;并将其嵌入到其他的数据结构中&#xff0c;使得其他的数据结构不必再一一实现其各自的双链表结构。实现了双链表结构的统一&#xff0c;同时可以演化出其他复杂数据结构。本文对linux中基于双链表实现的哈希表进…

java springboot+mybatis电影售票网站管理系统前台+后台设计和实现

java springbootmybatis电影售票网站管理系统前台后台设计和实现 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言…

RS—|遥感数字图像处理编程练习(python)

目录一&#xff1a;模拟计算图像直方图和累计直方图二&#xff1a;计算图像的均值、标准差、相关系数和协方差三&#xff1a;利用模板进行卷积运算四&#xff1a;获取彩色图像的直方图五&#xff1a;图像直方图均衡化一&#xff1a;模拟计算图像直方图和累计直方图 ① 调用的p…

【雷达入门 | FMCW毫米波雷达系统的性能参数分析】

本文编辑&#xff1a;调皮哥的小助理 FMCW毫米波雷达系统的性能参数主要包含&#xff1a; (1)距离估计、距离分辨率、距离精度、最大探测距离; (2)速度估计、速度分辨率、速度精度、最大不模糊速度&#xff1b; (3)角度估计、角度分辨率、角度精度、最大角度范围。 分析以及…

微服务框架SpringCloud从入门到通神(持续更新)

SpringCloud——>SpringBoot——>JavaWeb 微服务技术栈导学1 哔站up黑马程序员主讲老师&#xff0c;一上来就给介绍了SpringCloud出现的背景&#xff1a;微服务是分布式架构的一种&#xff0c;分布式架构就是要把服务做拆分&#xff0c;而SpringCloud只是解决了服务拆分式…

FTP协议原理简析

FTP服务器默认使用TCP协议的20、21端口与客户端进行通信。21端口用于建立控制连接&#xff0c;并传输FTP指令。20端口用于建立数据连接&#xff0c;传输数据流。 一&#xff1a;FTP功能简介 1&#xff1a;FTP服务器能够进行档案的传输与管理功能&#xff1b; 2&#xff1a;可以…

招生简章 | 欢迎报考中科院空天院网络信息体系技术重点实验室(七室)

官方公众号链接&#xff1a;招生简章 | 欢迎报考中科院空天院网络信息体系技术重点实验室&#xff08;七室&#xff09; 招生简章 | 欢迎报考中科院空天院网络信息体系技术重点实验室&#xff08;七室&#xff09; 中国科学院空天信息创新研究院&#xff08;简称空天院&#x…

【实战篇】38 # 如何使用数据驱动框架 D3.js 绘制常用数据图表?

说明 【跟月影学可视化】学习笔记。 图表库 vs 数据驱动框架 图表库只要调用 API 就能展现内容&#xff0c;灵活性不高&#xff0c;对数据格式要求也很严格&#xff0c;但方便数据驱动框架需要手动去完成内容的呈现&#xff0c;灵活&#xff0c;不受图表类型对应 API 的制约…

Smart Finance成为火必投票竞选项目,参与投票获海量奖励

最近&#xff0c;Huobi推出了新一期的“投票上币”活动&#xff0c;即用户可以通过HT为候选项目投票&#xff0c;在投票截止后&#xff0c;符合条件的优质项目将直接上线Huobi。而Smart Finance成为了新一期投票上币活动的竞选项目之一&#xff0c;并备受行业关注&#xff0c;与…

C++ 命令模式

什么是命令模式&#xff1f; 将请求转换为一个包含与请求相关的所有信息的独立对象。从而使你可以用不同的请求方法进行参数化&#xff0c;并且能够对请求进行排队、记录请求日志以及撤销请求操作。命令模式属于行为设计模式 如何理解命令模式 命令模式很像我们订外卖&#…

Hudi(10):Hudi集成Spark之并发控制

目录 0. 相关文章链接 1. Hudi支持的并发控制 1.1. MVCC 1.2. OPTIMISTIC CONCURRENCY 2. 使用并发写方式 3. 使用Spark DataFrame并发写入 4. 使用Delta Streamer并发写入 0. 相关文章链接 Hudi文章汇总 1. Hudi支持的并发控制 1.1. MVCC Hudi的表操作&#xff0c;如…

阿里云 EDAS Java服务日志中打印调用链TraceId

最近要搭建阿里云的日志服务SLS&#xff0c;收集服务日志&#xff0c;进行统一的搜索查询。但遇到一个问题如何在日志中打印链路的TraceId&#xff0c;本文章记录一下对EDAS免费的解决方法。 先看一下阿里官方文档 业务日志关联调用链的TraceId信息 从文档上看&#xff0c;想要…

基于SSM的资源发布系统

项目介绍&#xff1a; 该系统基于SSM技术&#xff0c;数据层为MyBatis&#xff0c;数据库使用mysql&#xff0c;MVC模式&#xff0c;B/S架构&#xff0c;具有完整的业务逻辑。系统共分为管理员&#xff0c;用户两种角色&#xff0c;主要功能&#xff1a;登陆注册&#xff0c;用…

数据结构:跳表

文章目录跳表跳表的由来单链表的查找效率太低提高单链表的查找效率跳表的时间复杂度分析跳表的空间复杂度分析跳表的插入操作跳表的删除操作跳表索引动态更新跳表 对链表进行改造&#xff0c;在链表上加多级索引的结构就是跳表&#xff0c;使其可以支持类似“二分”的查找算法。…

Redis查询之RediSearch和RedisJSON讲解

文章目录1 Redis查询1.1 RedisMod介绍1.2 安装Redis1.3 RediSearchRedisJSON安装1.3.1 下载安装1.3.2 修改配置1.4 RedisJSON操作1.4.1 基本操作1.4.1.1 保存操作JSON.SET1.4.1.2 读取操作JSON.GET1.4.1.3 批量读取操作JSON.MGET1.4.1.4 删除操作JSON.DEL1.4.1.5 其他命令1.4.1…