资料下载
链接: https://pan.baidu.com/s/1hZlZ6GKImnml4dKRljYk8A 提取码: 9w4e
第一章 架构方法论+DevOps+领域驱动设计
第1集 什么是swot
- 什么是swot
- S代表Strength,意思是企业(或部门、个人)目前的优势
- W代表Weakness,意思是企业(或部门、个人)目前的劣势。
- O代表Opportunities,意思是企业(或部门、个人)目前面临的机会。
- T代表Threatens,意思是企业(或部门、个人)目前面临威胁。
第2集 DDD领域驱动设计
- 什么是DDD
- 就是领域专家
第3集 devops
DevOps即Development和Operations的组合词,是一组过程、方法与系统的统称,用于促进开发应用程序或软件工程、技术运营和质量保障QA部门之间的沟通、协作与整合。
开发-测试-运维 交集
一专多能+配合提效
第4集 B2C、B2B、C2C、O2O, B表企业,C代表个人(或者说终端消费用户),O代表线上,也可以代表线下
科普下:B2C、B2B、C2C、O2O, B表企业,C代表个人(或者说终端消费用户),O代表线上,也可以代表线下。
最通俗易懂的解释
B2C:Business To Consumer 企业和个人之间的交易,好比天猫超市、考拉海购、京东超市、小米有品,属于B2C模式
B2B:Business To Business 企业和企业之间的交易,比如阿里巴巴1688,产品供应链的上下游
C2C:Consumer To Consumer 个人和个人之见的交易,闲鱼卖二手、58同城交易、朋友圈个人微商
O2O:Online To Offline 线上购买线下体验,比如美团上领取优惠券,到店消费
第二章 技术选择和创建聚合工程项目
第1集 为什么选择SpringCloudAlibaba作为技术和版本说明
简介:微服务技术对比和选择,版本说明
- SpringCloud
全家桶+轻松嵌入第三方组件(Netflix 奈飞)
官网:https://spring.io/projects/spring-cloud
配套
通信方式:http restful
注册中心:eureka
配置中心:config
断路器:hystrix
网关:zuul/gateway
分布式追踪系统:sleuth+zipkin
- Spring Cloud Alibaba
全家桶+阿里生态多个组件组合+SpringCloud支持
官网 https://spring.io/projects/spring-cloud-alibaba
配套
通信方式:http restful
服务注册发现:Nacos
服务限流降级:Sentinel
分布配置中心:Nacos
服务网关:SpringCloud Gateway
服务之间调用:Feign、Ribbon
链路追踪:Sleuth+Zipkin
- 为什么要选择SpringCloud和AlibabaCloud的区别
官网 https://spring.io/projects/spring-cloud-alibaba#overview
SpringCloud和AlibabaCloud组件存在很大交集,互相配合
SpringCloud很多组件是基于第三方整合,目前多个已经不更新了,比如zuul、eureka、hystrix等
AlibabaCloud 提供一站式微服务解决方法,已经和SpringCloud进行了整合,组件互相支持
我们也习惯称为 Spring Cloud Alibaba
第2集 Maven聚合工程创建微服务项目实战
简介:使用Maven聚合工程创建微服务架构
-
maven聚合工程
- classes-common
- classes-product-service
- classes-user-service
- classes-order-service
- classes-coupon-service
- classes-gateway
-
创建项目(记得删除聚合工程src目录)
-
添加依赖
<modelVersion>4.0.0</modelVersion>
<groupId>net.classes</groupId>
<artifactId>classes-1024-shop</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>classes-common</module>
<module>classes-product-service</module>
<module>classes-user-service</module>
<module>classes-order-service</module>
<module>classes-coupon-service</module>
<module>classes-gateway</module>
</modules>
<!-- 一般来说父级项目的packaging都为pom,packaging默认类型jar类型-->
<packaging>pom</packaging>
<properties>
<java.version>11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<spring.boot.version>2.3.3.RELEASE</spring.boot.version>
<spring.cloud.version>Hoxton.SR8</spring.cloud.version>
<alibaba.cloud.version>2.2.1.RELEASE</alibaba.cloud.version>
<mybatisplus.boot.starter.version>3.4.0</mybatisplus.boot.starter.version>
<lombok.version>1.18.16</lombok.version>
<commons.lang3.version>3.9</commons.lang3.version>
<commons.codec.version>1.15</commons.codec.version>
<springfox.boot.starter.version>3.0.0</springfox.boot.starter.version>
<docker.image.prefix>classes-cloud</docker.image.prefix>
<!--跳过单元测试-->
<skipTests>true</skipTests>
</properties>
<!--锁定版本-->
<dependencyManagement>
<dependencies>
<!--https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-dependencies/2.3.3.RELEASE-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-dependencies/Hoxton.SR8-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-alibaba-dependencies/2.2.1.RELEASE-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${alibaba.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--mybatis plus和springboot整合-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatisplus.boot.starter.version}</version>
</dependency>
<!--https://mvnrepository.com/artifact/org.projectlombok/lombok/1.18.16-->
<!--scope=provided,说明它只在编译阶段生效,不需要打入包中, Lombok在编译期将带Lombok注解的Java文件正确编译为完整的Class文件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons.lang3.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
<!--用于加密-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commons.codec.version}</version>
</dependency>
<!--接口文档依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>${springfox.boot.starter.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 代码库 -->
<repositories>
<repository>
<id>maven-ali</id>
<url>http://maven.aliyun.com/nexus/content/groups/public//</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
<checksumPolicy>fail</checksumPolicy>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<!--module不用添加打包版本信息-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>
第3集 开源中国Git仓库创建
生成公钥
- 本地生成公钥 ssh-keygen -t rsa -C “652133917@qq.com”
配置到gitee
第4集 Lombok插件常见注解实战
-
classes-user-service进行测试
- common项目添加
- 后续各个项目都依赖的包,可以统一在common项目里面添加管理,
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!--项目中添加 spring-boot-starter--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
- user-service项目引入common项目
<dependency> <groupId>net.classes</groupId> <artifactId>classes-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
- 聚合工程的pom文件 修改Lombok的作用域范围
<!--https://mvnrepository.com/artifact/org.projectlombok/lombok/1.18.16--> <!--scope=provided,说明它只在编译阶段生效,不需要打入包中, Lombok在编译期将带Lombok注解的Java文件正确编译为完整的Class文件--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> <!--<scope>provided</scope>--> </dependency>
- common项目添加
-
常见注解
-
@Getter/@Setter
- 作用类上,生成所有成员变量的getter/setter方法
- 作用于成员变量上,生成该成员变量的getter/setter方法
-
@ToString
- 作用于类,覆盖默认的toString()方法
-
@EqualsAndHashCode
- 作用于类,覆盖默认的equals和hashCode
-
@NoArgsConstructor
- 生成无参构造器
-
@AllArgsConstructor
- 生成全参构造器
-
@Data
- 作用于类上,是以下注解的集合
- @ToString
- @EqualsAndHashCode
- @Getter
- @Setter
- @RequiredArgsConstructor
- 作用于类上,是以下注解的集合
-
@Log / @Slf4j
-
作用于类上,生成日志变量, 用于记录日志, 已经在web项目中添加log依赖
-
如果不生效,记得检查下面的配置,另外重新更新下lombok插件,重启idea
-
-
第5集 增强版ORM框架-Mybatis plus介绍
简介:增强版ORM框架 mybatis plus介绍
-
背景
- 写一个数据库表的crud接口,编写实体类-》编写Controller-》编写Service-》编写DAO-》-》编写XML文件
- 特别是管理后台,多数都是简单的CRUD,用普通的mybatis有的鸡肋
-
介绍
- 官网 https://baomidou.com/
- 是一个 Mybatis 的增强工具,在 Mybatis 的基础上只做增强不做改变,为简化开发、提高效率而生
- 是怎么增强的呢?已经封装好了一些crud方法,我们不需要再写xml了,直接调用这些方法就行,类似JPA但优于JPA
-
SpringBoot整合
- 添加依赖(common项目里面添加,user-service项目里面测试)
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency>
第6集 项目引入Mybatis-plus-generator代码自动生成工具
简介:介绍Mybatis-plus-generator代码自动化生成工具
-
介绍
- 底层是模板引擎技术,可以自定义生成的java类模板
-
基础版mybatis-genarator
-
进阶版mybatis-plus-genarator
-
创建classes_user数据库
-
用户服务数据库 (其他用到再增加)
- 用户表
CREATE TABLE `user` ( `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(128) DEFAULT NULL COMMENT '昵称', `pwd` varchar(124) DEFAULT NULL COMMENT '密码', `head_img` varchar(524) DEFAULT NULL COMMENT '头像', `slogan` varchar(524) DEFAULT NULL COMMENT '用户签名', `sex` tinyint(2) DEFAULT '1' COMMENT '0表示女,1表示男', `points` int(10) DEFAULT '0' COMMENT '积分', `create_time` datetime DEFAULT NULL, `mail` varchar(64) DEFAULT NULL COMMENT '邮箱', `secret` varchar(12) DEFAULT NULL COMMENT '盐,用于个人敏感信息处理', PRIMARY KEY (`id`), UNIQUE KEY `mail_idx` (`mail`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
- 收货地址表
CREATE TABLE `address` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `user_id` bigint(20) DEFAULT NULL COMMENT '用户id', `default_status` int(1) DEFAULT NULL COMMENT '是否默认收货地址:0->否;1->是', `receive_name` varchar(64) DEFAULT NULL COMMENT '收发货人姓名', `phone` varchar(64) DEFAULT NULL COMMENT '收货人电话', `province` varchar(64) DEFAULT NULL COMMENT '省/直辖市', `city` varchar(64) DEFAULT NULL COMMENT '市', `region` varchar(64) DEFAULT NULL COMMENT '区', `detail_address` varchar(200) DEFAULT NULL COMMENT '详细地址', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=39 DEFAULT CHARSET=utf8mb4 COMMENT='电商-公司收发货地址表';
-
添加依赖
- 统一Common项目添加,各个项目测试类里面配置
<!-- 代码自动生成依赖 begin -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<!-- velocity -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
<!-- 代码自动生成依赖 end-->
- 代码(标记TODO的记得修改)
public class MyBatisPlusGenerator {
public static void main(String[] args) {
//1. 全局配置
GlobalConfig config = new GlobalConfig();
// 是否支持AR模式
config.setActiveRecord(true)
// 作者
.setAuthor("classes")
// 生成路径,最好使用绝对路径,window路径是不一样的
//TODO TODO TODO TODO
.setOutputDir("/Users/classes/Desktop/demo/src/main/java")
// 文件覆盖
.setFileOverride(true)
// 主键策略
.setIdType(IdType.AUTO)
.setDateType(DateType.ONLY_DATE)
// 设置生成的service接口的名字的首字母是否为I,默认Service是以I开头的
.setServiceName("%sService")
//实体类结尾名称
.setEntityName("%sDO")
//生成基本的resultMap
.setBaseResultMap(true)
//不使用AR模式
.setActiveRecord(false)
//生成基本的SQL片段
.setBaseColumnList(true);
//2. 数据源配置
DataSourceConfig dsConfig = new DataSourceConfig();
// 设置数据库类型
dsConfig.setDbType(DbType.MYSQL)
.setDriverName("com.mysql.cj.jdbc.Driver")
//TODO TODO TODO TODO
.setUrl("jdbc:mysql://127.0.0.1:3306/classes_user?useSSL=false")
.setUsername("root")
.setPassword("123456");
//3. 策略配置globalConfiguration中
StrategyConfig stConfig = new StrategyConfig();
//全局大写命名
stConfig.setCapitalMode(true)
// 数据库表映射到实体的命名策略
.setNaming(NamingStrategy.underline_to_camel)
//使用lombok
.setEntityLombokModel(true)
//使用restcontroller注解
.setRestControllerStyle(true)
// 生成的表, 支持多表一起生成,以数组形式填写
//TODO TODO TODO TODO
.setInclude("user","address");
//4. 包名策略配置
PackageConfig pkConfig = new PackageConfig();
pkConfig.setParent("net.classes")
.setMapper("mapper")
.setService("service")
.setController("controller")
.setEntity("model")
.setXml("mapper");
//5. 整合配置
AutoGenerator ag = new AutoGenerator();
ag.setGlobalConfig(config)
.setDataSource(dsConfig)
.setStrategy(stConfig)
.setPackageInfo(pkConfig);
//6. 执行操作
ag.execute();
System.out.println("======= Done 相关代码生成完毕 ========");
}
}
- 导入生成好的代码
- model (为啥不放common项目,如果是确定每个服务都用到的依赖或者类才放到common项目)
- mapper 类接口拷贝
- resource/mapper文件夹 xml脚本拷贝
- controller
- service 不拷贝
第三章 用户微服务基础模块开发+工具类封装+SwaggerUI3.0整合
第1集 用户微服务项目开发之收货地址查询接口开发
简介:用户微服务数据库配置和查询个人收货地址接口
- 配置文件配置 application.yml
server:
port: 9001
spring:
application:
name: classes-user-service
#数据库配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/classes_user?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: classes.net
#配置plus打印sql日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#设置日志级别,ERROR/WARN/INFO/DEBUG,默认是INFO以上才显示
logging:
level:
root: INFO
- springboot启动类注解
@SpringBootApplication
@MapperScan("net.classes.mapper")
- service
public interface AddressService {
AddressDO detail(Long id);
}
@Service
public class AddressServiceImpl implements AddressService {
@Autowired
private AddressMapper addressMapper;
@Override
public AddressDO detail(Long id) {
return addressMapper.selectOne(new QueryWrapper<AddressDO>().eq("id", id));
}
}
- 控制层
@RestController
@RequestMapping("/api/address/v1/")
public class AddressController {
@Autowired
private AddressService addressService;
@GetMapping("find/{address_id}")
public Object detail(@PathVariable("address_id") Long address_id) {
return addressService.detail(address_id);
}
}
- 查询地址详情接口(目的:测试连接数据库)
第2集 微服务项目整合用户端SwaggerUI3.0生成文档
- common项目 新增SwaggerUI3.0依赖,parent项目已经声明了版本
<!--swagger ui接口文档依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
</dependency>
- SwaggerConfiguration配置类开发
@Component
@EnableOpenApi
@Data
public class SwaggerConfiguration {
@Bean
public Docket webApiDoc(){
return new Docket(DocumentationType.OAS_30)
.groupName("用户端接口文档")
.pathMapping("/")
// 定义是否开启swagger,false为关闭,可以通过变量控制,线上关闭
.enable(true)
//配置api文档元信息
.apiInfo(apiInfo())
// 选择哪些接口作为swagger的doc发布
.select()
.apis(RequestHandlerSelectors.basePackage("net.classes"))
//正则匹配请求路径,并分配至当前分组
.paths(PathSelectors.ant("/api/**"))
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("电商平台")
.description("微服务接口文档")
.contact(new Contact("classes", "https://classes.net", "123456@qq.com"))
.version("12")
.build();
}
- AddressController配置接口文档
第3集 微服务项目整合SwaggerUI3.0接口文档分组和Header头定义
简介:SwaggerUI3.0接口文档分组和Header头定义
- 增强ui,在swagger配置类;添加@EnableKnife4j注解
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
- 接口文档分组,用户端、管理后台
@Bean
public Docket adminApiDoc(){
return new Docket(DocumentationType.OAS_30)
.groupName("管理端接口文档")
.pathMapping("/")
// 定义是否开启swagger,false为关闭,可以通过变量控制,线上关闭
.enable(true)
//配置api文档元信息
.apiInfo(apiInfo())
// 选择哪些接口作为swagger的doc发布
.select()
.apis(RequestHandlerSelectors.basePackage("net.classes"))
//正则匹配请求路径,并分配至当前分组
.paths(PathSelectors.ant("/admin/**"))
.build();
}
- 请求需要自定义http头,比如登录token令牌
@Bean
public Docket webApiDoc(){
return new Docket(DocumentationType.OAS_30)
.groupName("用户端接口文档")
.pathMapping("/")
// 定义是否开启swagger,false为关闭,可以通过变量控制,线上关闭
.enable(true)
//配置api文档元信息
.apiInfo(apiInfo())
// 选择哪些接口作为swagger的doc发布
.select()
.apis(RequestHandlerSelectors.basePackage("net.classes"))
//正则匹配请求路径,并分配至当前分组
.paths(PathSelectors.ant("/api/**"))
//正则匹配请求路径,并分配至当前分组,当前所有接口
.paths(PathSelectors.any())
.build()
//新版swagger3.0配置
.globalRequestParameters(getGlobalRequestParameters())
.globalResponses(HttpMethod.GET, getGlobalResponseMessage())
.globalResponses(HttpMethod.POST, getGlobalResponseMessage());
}
/**
* 生成全局通用参数, 支持配置多个响应参数
* @return
*/
private List<RequestParameter> getGlobalRequestParameters() {
List<RequestParameter> parameters = new ArrayList<>();
parameters.add(new RequestParameterBuilder()
.name("token")
.description("登录令牌")
.in(ParameterType.HEADER)
.query(q -> q.model(m -> m.scalarModel(ScalarType.STRING)))
.required(false)
.build());
// parameters.add(new RequestParameterBuilder()
// .name("version")
// .description("版本号")
// .required(true)
// .in(ParameterType.HEADER)
// .query(q -> q.model(m -> m.scalarModel(ScalarType.STRING)))
// .required(false)
// .build());
return parameters;
}
/**
* 生成通用响应信息
* @return
*/
private List<Response> getGlobalResponseMessage() {
List<Response> responseList = new ArrayList<>();
responseList.add(new ResponseBuilder().code("4xx").description("请求错误,根据code和msg检查").build());
return responseList;
}
}
第4集 统一接口响应协议-响应工具类封装
简介:统一接口响应协议和响应工具类封装
- 统一业务状态码 BizCodeEnum开发
- 状态码定义约束,共6位数,前三位代表服务,后3位代表接口
- 比如 商品服务210,购物车是220、用户服务230,403代表权限
public enum BizCodeEnum {
/**
* 通用操作码
*/
OPS_REPEAT(110001, "重复操作"),
/**
* 验证码
*/
CODE_TO_ERROR(240001, "接收号码不合规"),
CODE_LIMITED(240002, "验证码发送过快"),
CODE_ERROR(240003, "验证码错误"),
CODE_CAPTCHA(240101, "图形验证码错误"),
/**
* 账号
*/
ACCOUNT_REPEAT(250001, "账号已经存在"),
ACCOUNT_UNREGISTER(250002, "账号不存在"),
ACCOUNT_PWD_ERROR(250003, "账号或者密码错误");
@Getter
private String msg;
@Getter
private int code;
private BizCodeEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
- 接口统一协议 JsonData工具类开发
@Data
@AllArgsConstructor
@NoArgsConstructor
public class JsonData {
/**
* 状态码 0 表示成功,1表示处理中,-1表示失败
*/
private Integer code;
/**
* 数据
*/
private Object data;
/**
* 描述
*/
private String msg;
/**
* 成功,传入数据
*
* @return
*/
public static JsonData buildSuccess() {
return new JsonData(0, null, null);
}
/**
* 成功,传入数据
*
* @param data
* @return
*/
public static JsonData buildSuccess(Object data) {
return new JsonData(0, data, null);
}
/**
* 失败,传入描述信息
*
* @param msg
* @return
*/
public static JsonData buildError(String msg) {
return new JsonData(-1, null, msg);
}
/**
* 自定义状态码和错误信息
*
* @param code
* @param msg
* @return
*/
public static JsonData buildCodeAndMsg(int code, String msg) {
return new JsonData(code, null, msg);
}
/**
* 传入枚举,返回信息
*
* @param codeEnum
* @return
*/
public static JsonData buildResult(BizCodeEnum codeEnum) {
return JsonData.buildCodeAndMsg(codeEnum.getCode(), codeEnum.getMsg());
}
}
第5集 微服务自定义全局异常+处理器handler开发
简介:自定义全局异常+处理器开发
- 自定义全局业务异常
/**
* 自定义业务异常
*
* @author gtf
* @date 2022/11/22 11:22
*/
@Data
public class BizException extends RuntimeException {
private int code;
private String msg;
public BizException(int code, String msg) {
super(msg);
this.code = code;
this.msg = msg;
}
public BizException(BizCodeEnum bizCodeEnum) {
super(bizCodeEnum.getMsg());
this.code = bizCodeEnum.getCode();
this.msg = bizCodeEnum.getMsg();
}
}
- 自定义异常处理器
/**
* 自定义异常处理器
* @author gtf
* @date 2022/11/22 11:26
*/
@ControllerAdvice
@Slf4j
public class CustomExceptionHandle {
@ExceptionHandler(value = Exception.class)
@ResponseBody
public JsonData handle(Exception e) {
if (e instanceof BizException) {
BizException bizException = (BizException) e;
log.error("业务异常 {}", e);
return JsonData.buildCodeAndMsg(bizException.getCode(), bizException.getMsg());
} else {
log.error(e.getMessage());
log.error("非业务异常 {}", e);
return JsonData.buildError("非业务异常,全局异常,未知错误{}"+e);
}
}
}
第6集 微服务项目集成Spring Boot Test单元测试和sql日志打印
简介:微服务项目集成Spring Boot Test单元测试
- 需求分析->设计->开发->测试->上线
- 单元测试: 完成最小的软件设计单元的验证工作,目标是确保模块被正确的编码
-
Spring Boot Test 是在Spring Test之上的再次封装, 使用@SpringBootTest后,Spring将加载所有被管理的bean,等同于启动了整个服务
-
common项目添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
- classes-user-service项目新建测试
@RunWith(SpringRunner.class)
@SpringBootTest(classes = UserApplication.class)
@Slf4j
public class AddressTest {
@Autowired
private AddressService addressService;
@Test
public void testAddressDetail(){
AddressDO addressDO = addressService.detail(1L);
log.info(addressDO.toString());
}
}
- Mybatis plus配置控制台打印日志
#配置plus打印sql日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
第四章 用户微服务注册/登陆模块
第1集 图形验证码开发之谷歌kaptcha引入
简介:谷歌开源kaptcha图形验证码开发
-
Kaptcha 框架介绍 谷歌开源的一个可高度配置的实用验证码生成工具
- 验证码的字体/大小/颜色
- 验证码内容的范围(数字,字母,中文汉字!)
- 验证码图片的大小,边框,边框粗细,边框颜色
- 验证码的干扰线
- 验证码的样式(鱼眼样式、3D、普通模糊)
-
聚合工程依赖添加(使用国内baomidou二次封装的springboot整合starter)
<!--kaptcha依赖包-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>kaptcha-spring-boot-starter</artifactId>
<version>1.1.0</version>
</dependency>
- 用户服务添加
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>kaptcha-spring-boot-starter</artifactId>
</dependency>
- 开发配置(任何框架和springboot整合基本都是)
@Configuration
public class CaptchaConfig {
/**
* 验证码配置
* Kaptcha配置类名
*
* @return
*/
@Bean
@Qualifier("captchaProducer")
public DefaultKaptcha kaptcha() {
DefaultKaptcha kaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// properties.setProperty(Constants.KAPTCHA_BORDER, "yes");
// properties.setProperty(Constants.KAPTCHA_BORDER_COLOR, "220,220,220");
// //properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR, "38,29,12");
// properties.setProperty(Constants.KAPTCHA_IMAGE_WIDTH, "147");
// properties.setProperty(Constants.KAPTCHA_IMAGE_HEIGHT, "34");
// properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, "25");
// //properties.setProperty(Constants.KAPTCHA_SESSION_KEY, "code");
//验证码个数
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
// properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Courier");
//字体间隔
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_SPACE,"8");
//干扰线颜色
// properties.setProperty(Constants.KAPTCHA_NOISE_COLOR, "white");
//干扰实现类
properties.setProperty(Constants.KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
//图片样式
properties.setProperty(Constants.KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.WaterRipple");
//文字来源
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, "0123456789");
Config config = new Config(properties);
kaptcha.setConfig(config);
return kaptcha;
}
}
- 开发一个Controller使用测试
@Api(tags = "Notify 模块")
@RestController
@RequestMapping("/api/notify/v1/")
@Slf4j
public class NotifyController {
@Autowired
private Producer captchaProducer;
@GetMapping("captcha")
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) {
//获取验证码内容
String text = captchaProducer.createText();
log.info("验证码内容{}", text);
BufferedImage bufferedImage = captchaProducer.createImage(text);
ServletOutputStream outputStream = null;
try {
outputStream = response.getOutputStream();
ImageIO.write(bufferedImage, "jpg", outputStream);
outputStream.flush();
outputStream.close();
} catch (Exception e) {
log.error("getCaptcha失败");
}
}
}
第2集 Linux服务器Docker安装和部署容器化Redis
简介:Linux环境下docker部署+redis安装
- Linux环境下安装Docker
#依次运行以下命令添加yum源
yum update
yum install epel-release -y
yum clean all
yum list
#安装并运行Docker。
yum install docker-io -y
systemctl start docker
#检查安装结果。
docker info
#启动使用Docker
systemctl start docker #运行Docker守护进程
systemctl stop docker #停止Docker守护进程
systemctl restart docker #重启Docker守护进程
#修改镜像仓库
vim /etc/docker/daemon.json
#改为下面内容,然后重启docker
{
"debug":true,"experimental":true,
"registry-mirrors":["https://pb5bklzr.mirror.aliyuncs.com","https://hub-mirror.c.163.com","https://docker.mirrors.ustc.edu.cn"]
}
#查看信息
docker info
-
docker部署redis 并配置密码
- 如果访问不了,记得看防火墙/网络安全组端口是否开放
- 源码安装redis的话默认不能远程访问
- docker安装redis可以远程访问
docker run -itd --name classes-redis -p 8000:6379 redis --requirepass 123456
Options Mean -i 以交互模式运行容器,通常与 -t 同时使用; -t 为容器重新分配一个伪输入终端,通常与 -i 同时使用; -d 后台运行容器,并返回容器ID;
第3集 用户微服务开发图形验证码加入缓存
简介:用户微服务开发图形验证码接口
- 用户微服务配置Redis
spring:
application:
name: classes-user-service
redis:
host: 127.0.0.1
password:
port: 6379
- common聚合工程依赖配置
<!--redis客户端-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
- 验证码接口开发
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 临时使用10分钟有效,方便测试
*/
private static final long CAPTCHA_CODE_EXPIRED = 60 * 1000 * 10;
@GetMapping("captcha")
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) {
//获取验证码内容
String text = captchaProducer.createText();
log.info("验证码内容{}", text);
//存储
redisTemplate.opsForValue().set(getCaptchaKey(request), text, CAPTCHA_CODE_EXPIRED, TimeUnit.MILLISECONDS);
BufferedImage bufferedImage = captchaProducer.createImage(text);
ServletOutputStream outputStream = null;
try {
outputStream = response.getOutputStream();
ImageIO.write(bufferedImage, "jpg", outputStream);
outputStream.flush();
outputStream.close();
} catch (Exception e) {
log.error("getCaptcha失败");
}
}
/**
* 拼接key
*
* @param request
* @return
*/
public String getCaptchaKey(HttpServletRequest request) {
String ipAddr = CommonUtil.getIpAddr(request);
String userAgent = request.getHeader("User-Agent");
String key = "user-service:captcha" + CommonUtil.MD5(ipAddr + userAgent);
return key;
}
- CommonUtil工具类
/**
* 获取ip
* @param request
* @return
*/
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) {
// "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress="";
}
return ipAddress;
}
public static String MD5(String data) {
try {
java.security.MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
} catch (Exception exception) {
}
return null;
}
第4集 项目整合spring邮箱starter
-
账号准备和配置
- https://mail.126.com/
- 发送端使用网易邮箱
- 使用邮箱授权码发送
- 1111
-
user-service项目配置添加依赖
<!--发送邮件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
- 配置文件
#邮箱服务配置
mail:
host: smtp.126.com #发送邮件服务器
username: 123@126.com #发送邮件的邮箱地址
password: 123 #客户端授权码,不是邮箱密码,网易的是自己设置的
from: 123@126.com # 发送邮件的地址,和上面username一致
properties.mail.smtp.starttls.enable: true
properties.mail.smtp.starttls.required: true
properties.mail.smtp.ssl.enable: true
default-encoding: utf-8
- 用户微服务 service封装
@Service
@Slf4j
public class MailServiceImpl implements MailService {
/**
* Spring Boot 提供了一个发送邮件的简单抽象,直接注入即可使用
*/
@Autowired
private JavaMailSender mailSender;
/**
* 配置文件中的发送邮箱
*/
@Value("${spring.mail.from}")
private String from;
@Override
public void sendSimpleMail(String to, String subject, String content) {
//创建SimpleMailMessage对象
SimpleMailMessage message = new SimpleMailMessage();
//邮件发送人
message.setFrom(from);
//邮件接收人
message.setTo(to);
//邮件主题
message.setSubject(subject);
//邮件内容
message.setText(content);
//发送邮件
mailSender.send(message);
log.info("邮件发成功:{}", message.toString());
}
}
第5集 用户微服务之注册邮箱验证码接口开发
简介:注册邮箱验证码接口开发
- 接口开发
/**
* 支持手机号、邮箱发送验证码
* @return
*/
@ApiOperation("发送验证码")
@GetMapping("send_code")
public JsonData sendRegisterCode(@ApiParam("收信人") @RequestParam(value = "to", required = true)String to,
@ApiParam("图形验证码") @RequestParam(value = "captcha", required = true)String captcha,
HttpServletRequest request){
String key = getCaptchaKey(request);
String cacheCaptcha = redisTemplate.opsForValue().get(key);
if(captcha!=null && cacheCaptcha!=null && cacheCaptcha.equalsIgnoreCase(captcha)) {
redisTemplate.delete(key);
JsonData jsonData = notifyService.sendCode(SendCodeEnum.USER_REGISTER,to);
return jsonData;
}else {
return JsonData.buildResult(BizCodeEnum.CODE_CAPTCHA);
}
}
- enum
public enum SendCodeEnum {
/**
* 用户注册
*/
USER_REGISTER
}
- service层
/**
* @author gtf
* @date 2022/11/22 14:41
*/
@Service
@Slf4j
public class notifyServiceImpl implements NotifyService {
@Autowired
private MailService mailService;
public static final String SUBJECT = "这是一个验证码";
public static final String CONTENT = "您的验证码为%s,有效时间60s";
@Override
public JsonData sendCode(SendCodeEnum sendCodeType, String to) {
String code = CommonUtil.getRandomCode(6);
if (CheckUtil.isEmail(to)) {
//邮箱验证码
mailService.sendSimpleMail(to, SUBJECT, String.format(CONTENT, code));
return JsonData.buildSuccess();
} else if (CheckUtil.isPhone(to)) {
//短信验证码
}
log.info("邮箱验证码{}", code);
return JsonData.buildResult(BizCodeEnum.CODE_TO_ERROR);
}
}
- CommonUtil添加随机验证码
/**
* 获取验证码随机数
*
* @param length 指定长度
* @return
*/
public static String getRandomCode(int length) {
String source = "0123456789";
Random random = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(source.charAt(random.nextInt(9)));
}
return sb.toString();
}
- 邮箱工具类正则
public class CheckUtil {
/**
* 邮箱正则
*/
private static final Pattern MAIL_PATTERN = Pattern.compile("^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$");
/**
* 手机号正则,暂时未用
*/
private static final Pattern PHONE_PATTERN = Pattern.compile("^((13[0-9])|(15[^4,\\D])|(18[0,5-9]))\\d{8}$");
/**
* @param email
* @return
*/
public static boolean isEmail(String email) {
if (null == email || "".equals(email)) {
return false;
}
Matcher m = MAIL_PATTERN.matcher(email);
return m.matches();
}
/**
* 暂时未用
* @param phone
* @return
*/
public static boolean isPhone(String phone) {
if (null == phone || "".equals(phone)) {
return false;
}
Matcher m = PHONE_PATTERN.matcher(phone);
return m.matches();
}
}
- constant
public class CacheKey {
/**
* %s类型
* %s接受的号码
*/
public static final String CHECK_CODE_KEY = "code:%s:%s";
}
第6集 注册邮箱验证码防刷代码落地+整体测试
简介:注册邮箱验证码防刷落地和整体测试
@Service
@Slf4j
public class notifyServiceImpl implements NotifyService {
@Autowired
private MailService mailService;
@Autowired
private StringRedisTemplate redisTemplate;
public static final String SUBJECT = "这是一个验证码";
public static final String CONTENT = "您的验证码为%s,有效时间60s";
//十分钟有效
public static final int CODE_EXPIRED = 60 * 1000 * 10;
@Override
public JsonData sendCode(SendCodeEnum sendCodeType, String to) {
String cacheKey = String.format(CacheKey.CHECK_CODE_KEY, sendCodeType.name(), to);
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
//如果不为null,判断是否60s重复发送
if (StringUtils.isNoneBlank(cacheValue)) {
// 截取获取事件
long ttl = Long.parseLong(cacheValue.split("_")[1]);
//当前时间戳-验证码发送时间戳 如果小于60s不给发送
if (CommonUtil.getCaURRENTtimesTamp() - ttl < 1000 * 60) {
log.info("小于时间间隔{}", (CommonUtil.getCaURRENTtimesTamp() - ttl) / 1000);
return JsonData.buildResult(BizCodeEnum.CODE_LIMITED);
}
}
String code = CommonUtil.getRandomCode(6);
//拼接验证码 验证码+时间戳
String value = code + "_" + CommonUtil.getCaURRENTtimesTamp();
redisTemplate.opsForValue().set(cacheKey, value, CODE_EXPIRED, TimeUnit.MILLISECONDS);
if (CheckUtil.isEmail(to)) {
//邮箱验证码
mailService.sendSimpleMail(to, SUBJECT, String.format(CONTENT, code));
return JsonData.buildSuccess();
} else if (CheckUtil.isPhone(to)) {
//短信验证码
}
log.info("邮箱验证码{}", code);
return JsonData.buildResult(BizCodeEnum.CODE_TO_ERROR);
}
}
第7集 分布式文件存储业界常见解决方案介绍
简介:分布式文件存储常见解决方案介绍
-
目前业界比较多这个解决方案,这边就挑选几个介绍下
- MinIO
是在 Apache License v2.0 下发布的对象存储服务器,学习成本低,安装运维简单,主流语言的客户端整合都有, 号称最强的对象存储文件服务器,且可以和容器化技术docker/k8s等结合,社区活跃但不够成熟,业界参考资料较少 官网:https://docs.min.io/cn/
- FastDFS
一个开源的轻量级分布式文件系统,比较少的客户端可以整合,目前主要是C和java客户端,在一些互联网创业公司中有应用比较多,没有官方文档,社区不怎么活跃. 架构+部署结构复杂,出问题定位比较难定位,可以说是fastdfs零件的组装过程,需要去理解fastDFS的架构设计,才能够正确的安装部署
- 云厂商
-
阿里云OSS
-
七牛云
-
腾讯云
-
亚马逊云
-
CDN最强:Akamai https://www.akamai.com/cn
-
-
选云厂商理由
- 优点:开发简单,功能强大,容易维护(不同网络下图片质量、水印、加密策略、扩容、加速)
- 缺点:要钱, 个性化处理,未来转移比较复杂,不排除有些厂商会提供一键迁移工具
-
选开源MinIO的理由
- 优点:功能强大、可以根据业务做二次的定制,新一代分布式文件存储系统,容器化结合强大,更重要的是免费(购买磁盘、内存、带宽)
- 缺点:自己需要有专门的团队进行维护、扩容等
第8集 自建分布式文件存储MinIO容器化部署初体验
简介:分布式文件存储MinIO容器化部署初体验
-
Docker容器化部署(用于测试体验)
docker run -p 9000:9000 \ --name minio_classes \ -v /Users/classes/Desktop/test:/data \ -e "MINIO_ROOT_USER=AKIAIOSFODNN7EXAMPLE" \ -e "MINIO_ROOT_PASSWORD=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" \ minio/minio server /data
-
步骤
- 访问控制台
- 创建bucket
- 上传文件
- 预览
-
总结
总体操作很流畅,支持单机和集群部署,多个方面都是目前比较强的,
对于有需求不能或不使用云厂商提供的存储服务,例如阿里云的oss、七牛云的对象存储等,可以通过自建minio对象存储集群的方式
第9集 XSpringFileStorage整合oss集成和测试存储服务
简介:XSpringFileStorage整合oss集成和测试存储服务
使用:X Spring File Storage整合oss
-
添加阿里云OSS的SDK
-
添加maven依赖
- 底层聚合工程添加版本
<!-- spring-file-storage 必须要引入 --> <dependency> <groupId>cn.xuyanwu</groupId> <artifactId>spring-file-storage</artifactId> <version>0.5.0</version> </dependency> <!-- 阿里云 OSS 不使用的情况下可以不引入 --> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.15.1</version> </dependency>
- 用户微服务添加
<!-- spring-file-storage 必须要引入 --> <dependency> <groupId>cn.xuyanwu</groupId> <artifactId>spring-file-storage</artifactId> </dependency> <!-- 阿里云 OSS 不使用的情况下可以不引入 --> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> </dependency>
-
用户微服务配置OSS
spring
servlet:
multipart:
max-file-size: 100MB
max-request-size: 500MB
#阿里云OSS配置
#X Spring File Storage
file-storage:
aliyun-oss: # 阿里云 OSS ,不使用的情况下可以不写
- platform: aliyun-oss-1 # 存储平台标识
enable-storage: true # 启用存储
access-key: 123
secret-key: 123
end-point: oss-cn-beijing.aliyuncs.com
bucket-name: gtflog
domain: https://gtflog.oss-cn-beijing.aliyuncs.com/
base-path: hy/ # 基础路径
- sql
-- 这里使用的是 mysql
CREATE TABLE `file_detail`
(
`id` varchar(32) NOT NULL COMMENT '文件id',
`url` varchar(512) NOT NULL COMMENT '文件访问地址',
`size` bigint(20) DEFAULT NULL COMMENT '文件大小,单位字节',
`filename` varchar(256) DEFAULT NULL COMMENT '文件名称',
`original_filename` varchar(256) DEFAULT NULL COMMENT '原始文件名',
`base_path` varchar(256) DEFAULT NULL COMMENT '基础存储路径',
`path` varchar(256) DEFAULT NULL COMMENT '存储路径',
`ext` varchar(32) DEFAULT NULL COMMENT '文件扩展名',
`content_type` varchar(32) DEFAULT NULL COMMENT 'MIME类型',
`platform` varchar(32) DEFAULT NULL COMMENT '存储平台',
`th_url` varchar(512) DEFAULT NULL COMMENT '缩略图访问路径',
`th_filename` varchar(256) DEFAULT NULL COMMENT '缩略图名称',
`th_size` bigint(20) DEFAULT NULL COMMENT '缩略图大小,单位字节',
`th_content_type` varchar(32) DEFAULT NULL COMMENT '缩略图MIME类型',
`object_id` varchar(32) DEFAULT NULL COMMENT '文件所属对象id',
`object_type` varchar(32) DEFAULT NULL COMMENT '文件所属对象类型,例如用户头像,评价图片',
`attr` text COMMENT '附加属性',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB
DEFAULT CHARSET = utf8
ROW_FORMAT = DYNAMIC COMMENT ='文件记录表';
- service
/**
* @author gtf
* @date 2022/11/22 15:41
*/
public interface FileService {
/**
* 文件上传
*
* @param file
* @return
*/
String uploadPlatform(MultipartFile file);
}
- serviceImpl(实现了 FileRecord 接口文件上传完毕后会自动保存到数据库)
@Service
@Slf4j
public class FileStorageServiceImpl extends ServiceImpl<FileDetailMapper, FileDetailDO> implements FileRecorder, FileService {
@Autowired
private FileStorageService fileStorageService;
/**
* 保存文件信息到数据库
*/
@SneakyThrows
@Override
public boolean record(FileInfo info) {
FileDetailDO detail = BeanUtil.copyProperties(info, FileDetailDO.class, "attr");
//这是手动获 取附加属性字典 并转成 json 字符串,方便存储在数据库中
if (info.getAttr() != null) {
detail.setAttr(new ObjectMapper().writeValueAsString(info.getAttr()));
}
boolean b = save(detail);
if (b) {
info.setId(detail.getId());
}
return b;
}
/**
* 根据 url 查询文件信息
*/
@SneakyThrows
@Override
public FileInfo getByUrl(String url) {
FileDetailDO detail = getOne(new QueryWrapper<FileDetailDO>().eq("url", url));
FileInfo info = BeanUtil.copyProperties(detail, FileInfo.class, "attr");
//这是手动获取数据库中的 json 字符串 并转成 附加属性字典,方便使用
if (StringUtils.isNotBlank(detail.getAttr())) {
info.setAttr(new ObjectMapper().readValue(detail.getAttr(), Dict.class));
}
return info;
}
/**
* 根据 url 删除文件信息
*/
@Override
public boolean delete(String url) {
return remove(new QueryWrapper<FileDetailDO>().eq("url", url));
}
/**
* 文件上传并返回url
*
* @param file
* @return
*/
@Override
public String uploadPlatform(MultipartFile file) {
// 上传到指定的存储平台
FileInfo upload = fileStorageService.of(file)
.setPlatform("aliyun-oss-1") // 使用指定的存储平台
.setPath( DateUtil.format(new Date(), "yyyy/MM/dd") + "/")
.upload();
//异步保存文件到数据库
// record(upload);
//返回成功的url
return upload.getUrl();
}
}
启动类添加@EnableFileStorage
第10集 用户微服务头像上传阿里云OSS接口和SwaggerUI提效
简介:用户微服务头像上传阿里云OSS接口和SwaggerUI提效
-
文件上传流程
- 先上传文件,返回url地址,再和普通表单一并提交(推荐这种,更加灵活,失败率低)
- 文件和普通表单一并提交(设计流程比较多,容易超时和失败)
-
注意:默认SpringBoot最大文件上传是1M,大家测试的时候记得关注下
-
开发controller
- @requestPart注解 接收文件以及其他更为复杂的数据类型
- 比如 XXX(@RequestPart(“file”) MultipartFile file, @RequestPart(“userVO”) UserVO userVO) 复杂协议
@RestController
@RequestMapping("/api/user/v1/")
@Api(tags = "user 模块")
public class UserController {
@Autowired
private FileService fileService;
@ApiOperation("img上传")
@PostMapping("upload")
public JsonData uploadUserImg(@ApiParam(value = "文件上传", required = true) @RequestPart("file") MultipartFile file) {
String url = fileService.uploadPlatform(file);
return url != null ? JsonData.buildSuccess(url) : JsonData.buildResult(BizCodeEnum.FILE_UPLOAD_USER_IMG_FALL);
}
}
/**
* 文件上传相关
*/
FILE_UPLOAD_USER_IMG_FALL(600101, "用户头像");
第11集 用户微服务注册功能代码编写md5加盐
简介:用户微服务注册接口介绍和业务代码编写
-
微服务注册接口开发
- 请求实体类编写
- controller
- service
- 邮箱验证码验证
- 密码加密(TODO)
- 账号唯一性检查(TODO)
- 插入数据库
- 新注册用户福利发放(TODO)
- mapper
-
校验code
notifyServiceImpl
/**
* 校验验证码
* @param sendCodeEnum
* @param to
* @param code
* @return
*/
boolean checkcode(SendCodeEnum sendCodeEnum,String to,String code);
/**
* 校验验证码
*
* @param sendCodeEnum
* @param to
* @param code
* @return
*/
@Override
public boolean checkcode(SendCodeEnum sendCodeEnum, String to, String code) {
String cacheKey = String.format(CacheKey.CHECK_CODE_KEY, sendCodeEnum.name(), to);
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
if (StringUtils.isNoneBlank(cacheValue)) {
String cacheCode = cacheValue.split("_")[0];
redisTemplate.delete(cacheKey);
if (cacheCode.equals(code)) {
return true;
}
}
return false;
}
- 聚合工程pom文件和common项目添加依赖检查
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--用于加密-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
- controller
@ApiOperation("用户注册")
@PostMapping("reister")
public JsonData reister(@ApiParam("用户注册对象") @RequestBody UserReisterRequest userReisterRequest) {
return userService.reister(userReisterRequest);
}
- service
/**
* @author gtf
* @date 2022/11/22 17:17
*/
public interface UserService {
/**
*
* @param request
* @return
*/
JsonData reister(UserReisterRequest request);
}
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private NotifyService notifyService;
/**
* * 邮箱验证码验证
* * 密码加密(TODO)
* * 账号唯一性检查(TODO)
* * 插入数据库
* * 新注册用户福利发放(TODO)
*
* @param reisterRequest
* @return
*/
@Override
public JsonData reister(UserReisterRequest reisterRequest) {
boolean checkCode = false;
//校验验证码
if (StringUtils.isNoneBlank(reisterRequest.getMail())) {
checkCode = notifyService.checkcode(SendCodeEnum.USER_REGISTER, reisterRequest.getMail(), reisterRequest.getCode());
}
if (!checkCode) {
return JsonData.buildResult(BizCodeEnum.CODE_ERROR);
}
UserDO userDO = new UserDO();
BeanUtils.copyProperties(reisterRequest, userDO);
userDO.setCreateTime(new Date());
userDO.setSlogan("签名");
//设置密码
//生成密钥/盐
userDO.setSecret("$1$" + CommonUtil.getStringNumRandom(8));
//设置密码+盐处理
String crpytPwd = Md5Crypt.md5Crypt(reisterRequest.getPwd().getBytes(StandardCharsets.UTF_8), userDO.getSecret());
userDO.setPwd(crpytPwd);
//账户唯一性检查 todo
if (checkUnique(userDO.getMail())) {
int rows = userMapper.insert(userDO);
log.info("影响行数{},注册成功", rows, userDO.toString());
//初始化信息,发放福利
userRegisterInitTask(userDO);
return JsonData.buildSuccess();
}
return JsonData.buildResult(BizCodeEnum.ACCOUNT_REPEAT);
}
/**
* 账户唯一性检查
*
* @param mail
* @return
*/
private boolean checkUnique(String mail) {
QueryWrapper<UserDO> mail1 = new QueryWrapper<UserDO>().eq("mail", mail);
List<UserDO> userDOS = userMapper.selectList(mail1);
return userDOS.size() > 0 ? false : true;
}
/**
* 用户注册 初始化福利信息 todo
*
* @param userDO
*/
private void userRegisterInitTask(UserDO userDO) {
}
}
- 工具类
/**
* 生成指定长度随机字母和数字
*
* @param length
* @return
*/
private static final String ALL_CHAR_NUM = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
public static String getStringNumRandom(int length) {
//生成随机数字和字母,
Random random = new Random();
StringBuilder saltString = new StringBuilder(length);
for (int i = 1; i <= length; ++i) {
saltString.append(ALL_CHAR_NUM.charAt(random.nextInt(ALL_CHAR_NUM.length())));
}
return saltString.toString();
}
第12集 用户微服务开发之登录模块逻辑和解密
简介:用户微服务登录模块开发
- 核心逻辑
- 通过mail找数据库记录
- 获取盐,和当前传递的密码就行加密后匹配
- 生成token令牌
- controller
/**
* 登录
* @param loginRequest
* @return
*/
@PostMapping("login")
public JsonData register(@RequestBody UserLoginRequest loginRequest){
JsonData jsonData = userService.login(loginRequest);
return jsonData;
}
- model
@Data
public class UserLoginRequest {
/**
* 邮箱
*/
private String mail;
/**
* 密码
*/
private String pwd;
}
- service
/**
* 用户登陆
* @param userLoginRequest
* @return
*/
JsonData login(UserLoginRequest userLoginRequest);
/**
* 登录
*
* @param vo
* @return
*/
@Override
public JsonData login(UserLoginRequest loginRequest) {
List<UserDO> list = userMapper.selectList(
new QueryWrapper<UserDO>().eq("mail", loginRequest.getMail()));
if (list != null && list.size() == 1) {
UserDO userDO = list.get(0);
String cryptPwd = Md5Crypt.md5Crypt(loginRequest.getPwd().getBytes(), userDO.getSecret());
if (cryptPwd.equals(userDO.getPwd())) {
//生成token令牌
return JsonData.buildSuccess();
}
//密码错误
return JsonData.buildResult(BizCodeEnum.ACCOUNT_PWD_ERROR);
} else {
//未注册
return JsonData.buildResult(BizCodeEnum.ACCOUNT_UNREGISTER);
}
}
第13集 分布式应用下登录检验解决方案 JWT讲解
简介:分布式应用的登录检验解决方案 JWT讲解 json web token
-
什么是JWT
- JWT 是一个开放标准,它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名
- 简单来说: 就是通过一定规范来生成token,然后可以通过解密算法逆向解密token,这样就可以获取用户信息
{ id:888, name:'小w', expire:10000 } funtion 加密(object, appsecret){ xxxx return base64( token); } function 解密(token ,appsecret){ xxxx //成功返回true,失败返回false }
-
优点
- 生产的token可以包含基本信息,比如id、用户昵称、头像等信息,避免再次查库
- 存储在客户端,不占用服务端的内存资源
-
缺点
-
token是经过base64编码,所以可以解码,因此token加密前的对象不应该包含敏感信息,如用户权限,密码等
-
如果没有服务端存储,则不能做登录失效处理,除非服务端改秘钥
-
-
JWT格式组成 头部、负载、签名
- header+payload+signature
- 头部:主要是描述签名算法
- 负载:主要描述是加密对象的信息,如用户的id等,也可以加些规范里面的东西,如iss签发者,exp 过期时间,sub 面向的用户
- 签名:主要是把前面两部分进行加密,防止别人拿到token进行base解密后篡改token
- header+payload+signature
-
关于jwt客户端存储
- 可以存储在cookie,localstorage和sessionStorage里面
第14集 登录校验Json Web Token实战之封装通用方法
讲解:引入相关依赖并开发JWT工具类, 开发生产token和校验token的办法
-
聚合工程加入版本依赖,common项目加入相关依赖
<!-- JWT相关 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jwt</artifactId> <version>0.7.0</version> </dependency>
-
common项目中封装生产token方法
/**
* @author gtf
* @date 2022/11/23 10:07
*/
public class JWTUtil {
/**
* 70天过期
*/
private static final long EXPIRE = 1000 * 60 * 60 * 24 * 7 * 10;
/**
* 加密的密钥
*/
private static final String SECRET = "classes.net";
/**
* token/令牌前缀
*/
private static final String TOKEN_PREFIX = "classes1024";
/**
* SUBJECT 颁布人
*/
private static final String SUBJECT = "classes.net";
/**
* 生成token
*
* @param loginUser
* @return
*/
public static String geneJsonWebToken(LoginUser loginUser) {
if (loginUser != null) {
throw new NullPointerException();
}
Long userId = loginUser.getId();
String token = Jwts.builder()
//SUBJECT 颁布人
.setSubject(SUBJECT)
.claim("head_img", loginUser.getHeadImg())
.claim("id", userId)
.claim("name", loginUser.getName())
.claim("mail", loginUser.getMail())
//发布时间
.setIssuedAt(new Date())
//过期时间
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
//签名算法
.signWith(SignatureAlgorithm.HS256, SECRET).compact();
token = TOKEN_PREFIX + token;
return token;
}
/**
* 校验token的方法
*
* @param token
* @return
*/
public static Claims checkJWT(String token) {
try {
final Claims claims = Jwts.parser().setSigningKey(SECRET)
.parseClaimsJws(token.replace(TOKEN_PREFIX, "")).getBody();
return claims;
} catch (Exception e) {
return null;
}
}
}
- loginuser
@Data
public class LoginUser {
private Long id;
/**
* 昵称
*/
private String name;
/**
* 头像
*/
private String headImg;
/**
* 用户签名
*/
private String slogan;
/**
* 0表示女,1表示男
*/
private Integer sex;
/**
* 积分
*/
private Integer points;
private Date createTime;
/**
* 邮箱
*/
private String mail;
}
- 登陆的实现类添加
//生成token令牌
LoginUser loginUser = new LoginUser();
BeanUtils.copyProperties(userDO, loginUser);
String token = JWTUtil.geneJsonWebToken(loginUser);
log.info("token{}", token);
第15集 用户微服务之通用登录拦截器开发
简介:用户微服务登录拦截器开发
- 开发登录拦截器
- 解密JWT
- 传递登录用户信息
- attribute传递
- threadLocal传递
- LoginInterceptor
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<LoginUser>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String accessToken = request.getHeader("token");
if (accessToken == null) {
accessToken = request.getParameter("token");
}
//不为空 解密
if (StringUtils.isNoneBlank(accessToken)) {
Claims claims = JWTUtil.checkJWT(accessToken);
if (claims == null) {
//未登陆
CommonUtil.sendJsonMessage(response, JsonData.buildResult(BizCodeEnum.ACCOUNT_UNLOGIN));
return false;
}
Long id = Long.valueOf(claims.get("id").toString());
String head_img = claims.get("head_img").toString();
String name = claims.get("name").toString();
String mail = claims.get("mail").toString();
LoginUser build = LoginUser.builder().id(id).headImg(head_img).name(name).mail(mail).build();
threadLocal.set(build);
return true;
}
CommonUtil.sendJsonMessage(response, JsonData.buildResult(BizCodeEnum.ACCOUNT_UNLOGIN));
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
- 发送json的工具
/**
* 响应json数据
*
* @param response
* @param obj
*/
public static void sendJsonMessage(HttpServletResponse response, Object obj) {
ObjectMapper objectMapper = new ObjectMapper();
String contentType = "application-json; charset=utf-8";
response.setContentType(contentType);
try (PrintWriter writer = response.getWriter()) {
writer.println(objectMapper.writeValueAsString(obj));
response.flushBuffer();
} catch (IOException e) {
e.printStackTrace();
log.warn("响应json数据异常");
}
}
- 用户服务添加InterceptorConfig
@Configuration
@Slf4j
public class InterceptorConfig implements WebMvcConfigurer {
@Bean
LoginInterceptor loginInterceptor() {
return new LoginInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor())
//拦截
.addPathPatterns("/api/user/*/**","/api/address/*/**")
//放行
.excludePathPatterns("/api/user/*/reister"
,"/api/user/*/upload"
,"/api/user/*/login"
,"/api/notify/*/**");
WebMvcConfigurer.super.addInterceptors(registry);
}
}
第16集 用户微服务之个人信息查询接口开发
简介:用户微服务个人信息查询接口开发
- controller
@ApiOperation("个人信息查询")
@GetMapping("detail")
public JsonData detail(){
UserVO userVO= userService.findUserDeail();
return JsonData.buildSuccess(userVO);
}
- service
UserVO findUserDeail();
/**
* 查看用户信息
* @return
*/
@Override
public UserVO findUserDeail() {
LoginUser loginUser = LoginInterceptor.threadLocal.get();
Long userId = loginUser.getId();
UserDO userDO = userMapper.selectOne(new QueryWrapper<UserDO>().eq("id", userId));
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO,userVO);
return userVO;
}
- userVO
/**
* @author gtf
* @date 2022/11/23 15:03
*/
@Data
public class UserVO {
private Long id;
/**
* 昵称
*/
private String name;
/**
* 头像
*/
private String headImg;
/**
* 用户签名
*/
private String slogan;
/**
* 0表示女,1表示男
*/
private Integer sex;
/**
* 积分
*/
private Integer points;
private Date createTime;
/**
* 邮箱
*/
private String mail;
}
第17集 用户微服务之新增收货地址模块接口开发
简介:用户微服务新增收货地址模块开发
-
需求
- 用户收货地址,可以进行新增
- 有个默认的收货地址
-
控制层
@ApiOperation("新增收货地址")
@PostMapping("add")
public JsonData add(@RequestBody AddressAddRequest addressAddRequest){
int rows= addressService.add(addressAddRequest);
return JsonData.buildSuccess(rows);
}
- AddressAddRequest
@Data
public class AddressAddRequest {
/**
* 是否默认收货地址:0->否;1->是
*/
private Integer defaultStatus;
/**
* 收发货人姓名
*/
private String receiveName;
/**
* 收货人电话
*/
private String phone;
/**
* 省/直辖市
*/
private String province;
/**
* 市
*/
private String city;
/**
* 区
*/
private String region;
/**
* 详细地址
*/
private String detailAddress;
}
- service
/**
* 新增收获地址
* @param addressAddRequest
* @return
*/
int add(AddressAddRequest addressAddRequest);
@Override
public int add(AddressAddRequest addressAddRequest) {
LoginUser loginUser = LoginInterceptor.threadLocal.get();
AddressDO addressDO = new AddressDO();
addressDO.setCreateTime(new Date());
BeanUtils.copyProperties(addressAddRequest, addressDO);
addressDO.setUserId(loginUser.getId());
//判断是否有默认收获地址
if (addressAddRequest.getDefaultStatus() == AddressStatusEnum.DEFAULT_STATUS.getStatus()) {
AddressDO addressDO1 = addressMapper.selectOne(new QueryWrapper<AddressDO>().eq("user_id", loginUser.getId()).eq("default_status", AddressStatusEnum.DEFAULT_STATUS.getStatus()));
if (addressDO1 != null) {
addressDO1.setDefaultStatus(0);
addressMapper.update(addressDO1, new QueryWrapper<AddressDO>().eq("user_id", loginUser.getId()).eq("id", addressDO1.getId()));
}
}
int rows = addressMapper.insert(addressDO);
return rows;
}
- enum
public enum AddressStatusEnum {
/**
* 默认收获地址
*/
DEFAULT_STATUS(1),
/**
* 非默认收货地址
*/
COMMON_STATUS(0);
private int status;
private AddressStatusEnum(int status) {
this.status = status;
}
public int getStatus() {
return status;
}
}
第18集 收货地址模块-查找接口开发和删除地址接口开发
简介:用户微服务收货地址查找接口开发和删除地址接口
- controller
@ApiOperation("根据id查找地址")
@GetMapping("find/{address_id}")
public JsonData detail(@ApiParam(name = "address_id", value = "地址id", required = true) @PathVariable("address_id") Long address_id) {
AddressVO detail = addressService.detail(address_id);
return detail == null ? JsonData.buildResult(BizCodeEnum.ADDRESS_NO_EXITS) : JsonData.buildSuccess(detail);
}
@ApiOperation("根据id删除地址")
@DeleteMapping("del/{address_id}")
public JsonData del(@ApiParam(name = "address_id", value = "地址id", required = true) @PathVariable("address_id") Long address_id) {
int rows = addressService.delete(address_id);
return rows == 1 ? JsonData.buildSuccess() : JsonData.buildResult(BizCodeEnum.ADDRESS_DEL_FAIL);
}
- addressvo
@Data
public class AddressVO {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户id
*/
private Long userId;
/**
* 是否默认收货地址:0->否;1->是
*/
private Integer defaultStatus;
/**
* 收发货人姓名
*/
private String receiveName;
/**
* 收货人电话
*/
private String phone;
/**
* 省/直辖市
*/
private String province;
/**
* 市
*/
private String city;
/**
* 区
*/
private String region;
/**
* 详细地址
*/
private String detailAddress;
}
- service
/**
* 删除收获地址
* @param address_id
* @return
*/
int delete(Long address_id);
AddressVO detail(Long id);
@Override
public AddressVO detail(Long id) {
AddressDO addressDO = addressMapper.selectOne(new QueryWrapper<AddressDO>().eq("id", id));
if (addressDO == null) {
return null;
}
AddressVO addressVO = new AddressVO();
BeanUtils.copyProperties(addressDO, addressVO);
return addressVO;
}
@Override
public int delete(Long address_id) {
LoginUser loginUser = LoginInterceptor.threadLocal.get();
int rows = addressMapper.delete(new QueryWrapper<AddressDO>().eq("id", address_id).eq("user_id", loginUser.getId()));
return rows;
}
- 通用返回状态码BizCodeEnum
/**
* 通用操作码
*/
OPS_REPEAT(110001,"重复操作"),
/**
* 购物车
*/
CART_FAIL(220001,"添加购物车失败"),
/**
*验证码
*/
CODE_TO_ERROR(240001,"接收号码不合规"),
CODE_LIMITED(240002,"验证码发送过快"),
CODE_ERROR(240003,"验证码错误"),
CODE_CAPTCHA(240101,"图形验证码错误"),
/**
* 账号
*/
ACCOUNT_REPEAT(250001,"账号已经存在"),
ACCOUNT_UNREGISTER(250002,"账号不存在"),
ACCOUNT_PWD_ERROR(250003,"账号或者密码错误"),
ACCOUNT_UNLOGIN(250004,"账号未登陆" ),
/**
* 优惠券
*/
COUPON_CONDITION_ERROR(270001,"优惠券条件错误"),
COUPON_UNAVAILABLE(270002,"没有可用的优惠券"),
COUPON_NO_EXITS(270003,"优惠券不存在"),
COUPON_NO_STOCK(270005,"优惠券库存不足"),
COUPON_OUT_OF_LIMIT(270006,"优惠券领取超过限制次数"),
COUPON_OUT_OF_TIME(270407,"优惠券不在领取时间范围"),
COUPON_GET_FAIL(270407,"优惠券领取失败"),
COUPON_RECORD_LOCK_FAIL(270409,"优惠券锁定失败"),
/**
* 订单
*/
ORDER_CONFIRM_COUPON_FAIL(280001,"创建订单-优惠券使用失败,不满足价格条件"),
ORDER_CONFIRM_PRICE_FAIL(280002,"创建订单-验价失败"),
ORDER_CONFIRM_LOCK_PRODUCT_FAIL(280003,"创建订单-商品库存不足锁定失败"),
ORDER_CONFIRM_ADD_STOCK_TASK_FAIL(280004,"创建订单-新增商品库存锁定任务"),
ORDER_CONFIRM_TOKEN_NOT_EXIST(280008,"订单令牌缺少"),
ORDER_CONFIRM_TOKEN_EQUAL_FAIL(280009,"订单令牌不正确"),
ORDER_CONFIRM_NOT_EXIST(280010,"订单不存在"),
ORDER_CONFIRM_CART_ITEM_NOT_EXIST(280011,"购物车商品项不存在"),
/**
* 收货地址
*/
ADDRESS_ADD_FAIL(290001,"新增收货地址失败"),
ADDRESS_DEL_FAIL(290002,"删除收货地址失败"),
ADDRESS_NO_EXITS(290003,"地址不存在"),
/**
* 支付
*/
PAY_ORDER_FAIL(300001,"创建支付订单失败"),
PAY_ORDER_CALLBACK_SIGN_FAIL(300002,"支付订单回调验证签失败"),
PAY_ORDER_CALLBACK_NOT_SUCCESS(300003,"创建支付订单失败"),
PAY_ORDER_NOT_EXIST(300005,"订单不存在"),
PAY_ORDER_STATE_ERROR(300006,"订单状态不正常"),
PAY_ORDER_PAY_TIMEOUT(300007,"订单支付超时"),
/**
* 流控操作
*/
CONTROL_FLOW(500101,"限流控制"),
CONTROL_DEGRADE(500201,"降级控制"),
CONTROL_AUTH(500301,"认证控制"),
/**
* 文件相关
*/
FILE_UPLOAD_USER_IMG_FAIL(600101,"用户头像文件上传失败");
第19集 列举指定用户全部收货地址接口开发
简介:用户微服务列举指定用户全部收货地址接口开发
- controller
@ApiOperation("查询索引的收获地址")
@PostMapping("list")
public JsonData findUserAllAddress() {
List<AddressVO> addressVOS = addressService.findUserAllAddress();
return JsonData.buildSuccess(addressVOS);
}
- service
/**
* 查找用户所有收货地址
* @return
*/
List<AddressVO> findUserAllAddress();
/**
* 查找用户所有的收货地址
*
* @return
*/
@Override
public List<AddressVO> findUserAllAddress() {
LoginUser loginUser = LoginInterceptor.threadLocal.get();
List<AddressDO> addressDOS = addressMapper.selectList(new QueryWrapper<AddressDO>().eq("user_id", loginUser.getId()));
List<AddressVO> addressVOList = addressDOS.stream().map(obj -> {
AddressVO addressVO = new AddressVO();
BeanUtils.copyProperties(obj, addressVO);
return addressVO;
}).collect(Collectors.toList());
return addressVOList;
}
第五章 优惠券服务开发
第1集 数据库建立
简介:优惠券微服务介绍和效果体验
- 大课优惠券业务需求介绍
- 新用户注册-发放后端配置的新人优惠券
- 用户可以主动领取优惠券
- 下单可以选择对应的优惠券抵扣
- 支持满减优惠券-无门槛优惠券两种
- 多种元数据配置
- 类型:无门槛、满减等
- 每人领劵次数限制
- 发券总量控制
- 优惠券开始时间和结束时间
- 优惠券状态配置
- 核心知识:
- 高并发下扣减劵库存
- 超发
- 单人超领取
- 高并发下扣减劵库存
- 数据库表介绍
#优惠券表
CREATE TABLE `coupon` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`category` varchar(11) DEFAULT NULL COMMENT '优惠卷类型[NEW_USER注册赠券,TASK任务卷,PROMOTION促销劵]',
`publish` varchar(11) DEFAULT NULL COMMENT '发布状态, PUBLISH发布,DRAFT草稿,OFFLINE下线',
`coupon_img` varchar(524) DEFAULT NULL COMMENT '优惠券图片',
`coupon_title` varchar(128) DEFAULT NULL COMMENT '优惠券标题',
`price` decimal(16,2) DEFAULT NULL COMMENT '抵扣价格',
`user_limit` int(11) DEFAULT NULL COMMENT '每人限制张数',
`start_time` datetime DEFAULT NULL COMMENT '优惠券开始有效时间',
`end_time` datetime DEFAULT NULL COMMENT '优惠券失效时间',
`publish_count` int(11) DEFAULT NULL COMMENT '优惠券总量',
`stock` int(11) DEFAULT '0' COMMENT '库存',
`create_time` datetime DEFAULT NULL,
`condition_price` decimal(16,2) DEFAULT NULL COMMENT '满多少才可以使用',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8mb4;
#优惠券领劵记录
CREATE TABLE `coupon_record` (
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
`coupon_id` bigint(11) DEFAULT NULL COMMENT '优惠券id',
`create_time` datetime DEFAULT NULL COMMENT '创建时间获得时间',
`use_state` varchar(32) DEFAULT NULL COMMENT '使用状态 可用 NEW,已使用USED,过期 EXPIRED;',
`user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
`user_name` varchar(128) DEFAULT NULL COMMENT '用户昵称',
`coupon_title` varchar(128) DEFAULT NULL COMMENT '优惠券标题',
`start_time` datetime DEFAULT NULL COMMENT '开始时间',
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
`order_id` bigint(11) DEFAULT NULL COMMENT '订单id',
`price` decimal(16,2) DEFAULT NULL COMMENT '抵扣价格',
`condition_price` decimal(16,2) DEFAULT NULL COMMENT '满多少才可以使用',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=141 DEFAULT CHARSET=utf8mb4;
- 测试数据准备
第2集 优惠券微服务+MybatisPlusGenerator代码自动生成工具
简介:Mybatis-plus-generator代码自动化生成微服务相关类
直接复制用户服务的就行
- 导入生成好的代码
- model (为啥不放common项目,如果是确定每个服务都用到的依赖或者类才放到common项目)
- mapper 类接口拷贝
- resource/mapper文件夹 xml脚本拷贝
- controller
- service 不拷贝
第3集 新版MybatisPlus分页插件配置+优惠券分页列表接口
简介:Mybatis-plus-分页插件配置+优惠劵列表开发
- MybatisPlus分页插件配置(Common项目配置)
@Configuration
public class MybatisPlusPageConfig {
/* 旧版本配置
@Bean
public PaginationInterceptor paginationInterceptor(){
return new PaginationInterceptor();
}*/
/**
* 新的分页插件,一缓和二缓遵循mybatis的规则,
* 需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
- 优惠券列表分页接口开发
- controller
@ApiOperation("分页查询")
@GetMapping("page")
public JsonData pageCoupon(@RequestParam(value = "page", defaultValue = "1") int page, @RequestParam(value = "size", defaultValue = "10") int size
) {
Map<String, Object> objectMap = couponService.pageCpouponActivity(page, size);
return JsonData.buildSuccess(objectMap);
}
- vo
@Data
public class CouponVO {
private static final long serialVersionUID = 1L;
/**
* id
*/
private Long id;
/**
* 优惠卷类型[NEW_USER注册赠券,TASK任务卷,PROMOTION促销劵]
*/
private String category;
/**
* 发布状态, PUBLISH发布,DRAFT草稿,OFFLINE下线
*/
private String publish;
/**
* 优惠券图片
*/
private String couponImg;
/**
* 优惠券标题
*/
private String couponTitle;
/**
* 抵扣价格
*/
private BigDecimal price;
/**
* 每人限制张数
*/
private Integer userLimit;
/**
* 优惠券开始有效时间
*/
private Date startTime;
/**
* 优惠券失效时间
*/
private Date endTime;
/**
* 优惠券总量
*/
private Integer publishCount;
/**
* 库存
*/
private Integer stock;
private Date createTime;
/**
* 满多少才可以使用
*/
private BigDecimal conditionPrice;
}
- enum
public enum CouponCategoryEnum {
//优惠卷类型[NEW_USER注册赠券,TASK任务卷,PROMOTION促销劵]
NEW_USER,
TASK,
PROMOTION;
}
/**
* 优惠券状态
*
* @author gtf
* @date 2022/11/23 17:37
*/
public enum CouponPublishEnum {
PUBLISH,
/**
* 草稿
*/
DRAFT,
/**
* 下线
*/
OFFLINE;
}
- service
/**
* 分页查询优惠券
* @param page
* @param size
* @return
*/
Map<String,Object> pageCpouponActivity(int page,int size);
@Service
@Slf4j
public class CouponServiceImpl implements CouponService {
@Autowired
private CouponMapper couponMapper;
@Override
public Map<String, Object> pageCpouponActivity(int page, int size) {
Page<CouponDO> page1 = new Page<>(page, size);
IPage<CouponDO> couponDOPage = couponMapper.selectPage(page1, new QueryWrapper<CouponDO>().eq("publish", CouponPublishEnum.PUBLISH.name()).eq("category", promotion.name()).orderByDesc("create_time"));
Map<String, Object> pageMap = new HashMap<>(3);
pageMap.put("total_record", couponDOPage.getTotal());
pageMap.put("total_page", couponDOPage.getPages());
pageMap.put("current_data", couponDOPage.getRecords().stream().map(obj -> beanProcess(obj)).collect(Collectors.toList()));
return pageMap;
}
private CouponVO beanProcess(CouponDO obj) {
CouponVO couponVO = new CouponVO();
BeanUtils.copyProperties(obj, couponVO);
return couponVO;
}
}
第4集 优惠券微服务-登录拦截器配置
简介:登录拦截器配置和SwaggerUI接口文档配置
- 配置登录拦截器
@RestController
@RequestMapping("/api/coupon/v1")
@Api(value = "coupon", tags = "coupon")
public class CouponController {
@Autowired
private CouponService couponService;
@ApiOperation("分页查询")
@GetMapping("page_coupon")
public JsonData pageCoupon(@RequestParam(value = "page", defaultValue = "1") int page, @RequestParam(value = "size", defaultValue = "10") int size
) {
Map<String, Object> objectMap = couponService.pageCpouponActivity(page, size);
return JsonData.buildSuccess(objectMap);
}
}
第5集 简单-优惠券微服务-C端领劵接口核心校验业务逻辑开发
简介:C端用户领劵接口核心业务逻辑开发
<!--分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.10.1</version>
</dependency>
- 创建redisson客户端
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private String redisPort;
@Value("${spring.redis.password}")
private String redisPwd;
/**
* 配置分布式锁
* @return
*/
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
//单机模式
//config.useSingleServer().setPassword("123456").setAddress("redis://8.129.113.233:3308");
config.useSingleServer().setPassword(redisPwd).setAddress("redis://"+redisHost+":"+redisPort);
//集群模式
//config.useClusterServers()
//.setScanInterval(2000)
//.addNodeAddress("redis://10.0.29.30:6379", "redis://10.0.29.95:6379")
// .addNodeAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
- controller
@ApiOperation("领取优惠券")
@GetMapping("/add/promotionCoupon/{couponId}")
public JsonData addPromotionCoupon(@PathVariable("couponId") long couponID){
return couponService.addCoupon(couponID, CouponCategoryEnum.PROMOTION);
}
- CouponStateEnum
/**
* @author gtf
* @date 2022/11/23 22:42
*/
public enum CouponStateEnum {
//使用状态 可用 NEW,已使用USED,过期 EXPIRED;
NEW,
USED,
EXPIRED;
}
- service
JsonData addCoupon(long couponID, CouponCategoryEnum promotion);
/**
* 查找优惠券是否存在
* 校验优惠券是否可以领取:时间 库存 超出限制
* 扣减库存
* 保存领券记录
*
* @param couponID
* @param promotion
* @return
*/
@Override
public JsonData addCoupon(long couponID, CouponCategoryEnum promotion) {
LoginUser loginUser = LoginInterceptor.threadLocal.get();
String lockKey = "lock:coupon:" + couponID;
RLock lock = redissonClient.getLock(lockKey);
lock.lock();
log.info("加索成功");
try {
//查找优惠券是否存在
CouponDO couponDO = couponMapper.selectOne(new QueryWrapper<CouponDO>().eq("id", couponID).eq("category", CouponCategoryEnum.PROMOTION.name()).eq("publish", CouponPublishEnum.PUBLISH.name()));
//校验优惠券是否可以领取
this.checkCoupon(couponDO, loginUser.getId());
//构建领券记录
CouponRecordDO couponRecordDO = new CouponRecordDO();
BeanUtils.copyProperties(couponDO, couponRecordDO);
couponRecordDO.setCreateTime(new Date());
couponRecordDO.setUseState(CouponStateEnum.NEW.name());
couponRecordDO.setUserId(loginUser.getId());
couponRecordDO.setUserName(loginUser.getName());
couponRecordDO.setCouponId(couponDO.getId());
couponRecordDO.setId(null);
//扣减库存
//高并发下扣减劵库存,采用乐观锁,当前stock做版本号,version,一次只能领取1张
int rows = couponMapper.reduceStock(couponID);
if (rows == 1) {
//库存扣件成功,保存记录
couponRecordMapper.insert(couponRecordDO);
} else {
log.warn("发放失败");
throw new BizException(BizCodeEnum.COUPON_NO_STOCK);
}
} catch (Exception e) {
lock.unlock();
}
return JsonData.buildSuccess();
}
/**
* 校验是否可以领取
*
* @param couponDO
* @param id
*/
private void checkCoupon(CouponDO couponDO, Long id) {
//查找优惠券是否存在
if (couponDO == null) {
throw new BizException(BizCodeEnum.COUPON_NO_EXITS);
}
//是否在发布状态
if (!couponDO.getPublish().equals(CouponPublishEnum.PUBLISH.name())) {
throw new BizException(BizCodeEnum.COUPON_GET_FAIL);
}
//判断库存是否足够
if (couponDO.getStock() <= 0) {
throw new BizException(BizCodeEnum.COUPON_NO_STOCK);
}
//是否在领取时间范围
long time = CommonUtil.getCaURRENTtimesTamp();
long beg = couponDO.getStartTime().getTime();
long end = couponDO.getEndTime().getTime();
if (time < beg || time > end) {
throw new BizException(BizCodeEnum.COUPON_OUT_OF_TIME);
}
//用户是否超过领取限制
Integer recordNum = couponRecordMapper.selectCount(new QueryWrapper<CouponRecordDO>().eq("coupon_id", couponDO.getId()).eq("user_id", id));
if (recordNum >= couponDO.getUserLimit()) {
throw new BizException(BizCodeEnum.COUPON_OUT_OF_LIMIT);
}
}
private CouponVO beanProcess(CouponDO obj) {
CouponVO couponVO = new CouponVO();
BeanUtils.copyProperties(obj, couponVO);
return couponVO;
}
第6集 优惠券微服务领劵记录分页模块开发
简介:微服务个人领券记录分页接口
-
领劵记录查询开发
-
controllrrt
@ApiOperation("分野查询个人")
@GetMapping("page")
public JsonData page(@RequestParam(value = "page", defaultValue = "1") int page, @RequestParam(value = "size", defaultValue = "10") int size){
Map<String, Object> objectMap = couponRecordService.page(page, size);
return JsonData.buildSuccess(objectMap);
}
- CouponRecordVO
/**
* @author gtf
* @date 2022/11/24 10:01
*/
@Data
public class CouponRecordVO {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 优惠券id
*/
private Long couponId;
/**
* 创建时间获得时间
*/
private Date createTime;
/**
* 使用状态 可用 NEW,已使用USED,过期 EXPIRED;
*/
private String useState;
/**
* 用户id
*/
private Long userId;
/**
* 用户昵称
*/
private String userName;
/**
* 优惠券标题
*/
private String couponTitle;
/**
* 开始时间
*/
private Date startTime;
/**
* 结束时间
*/
private Date endTime;
/**
* 订单id
*/
private Long orderId;
/**
* 抵扣价格
*/
private BigDecimal price;
/**
* 满多少才可以使用
*/
private BigDecimal conditionPrice;
}
- service
/**
* 分野查询个人
* @param page
* @param size
* @return
*/
Map<String, Object> page(int page, int size);
@Slf4j
@Service
public class CouponRecordServiceImpl implements CouponRecordService {
@Autowired
private CouponRecordMapper couponRecordMapper;
@Override
public Map<String, Object> page(int page, int size) {
LoginUser loginUser = LoginInterceptor.threadLocal.get();
Page<CouponRecordDO> pageInfo = new Page<CouponRecordDO>(page, size);
IPage<CouponRecordDO> couponRecordDOPage = couponRecordMapper.selectPage(pageInfo, new QueryWrapper<CouponRecordDO>().eq("user_id", loginUser.getId())
.orderByDesc("create_time"));
Map<String, Object> pageMap = new HashMap<>(3);
pageMap.put("total_record", couponRecordDOPage.getTotal());
pageMap.put("total_page", couponRecordDOPage.getPages());
pageMap.put("current_data", couponRecordDOPage.getRecords().stream().map(obj -> beanProcess(obj)).collect(Collectors.toList()));
return pageMap;
}
private CouponRecordVO beanProcess(CouponRecordDO obj) {
CouponRecordVO couponRecordVO = new CouponRecordVO();
BeanUtils.copyProperties(obj, couponRecordVO);
return couponRecordVO;
}
}
第7集 优惠券微服务领劵记录详情接口开发
简介:微服务个人领券记录详情接口
- controller
@ApiOperation("查看详情")
@GetMapping("detail/{record_id}")
public JsonData couponRecordDetail(@PathVariable("record_id") Long record_id) {
CouponRecordVO couponRecordVO = couponRecordService.findById(record_id);
return couponRecordVO==null?JsonData.buildResult(BizCodeEnum.COUPON_NO_EXITS):JsonData.buildSuccess(couponRecordVO);
}
- service
/**
* 查看详情
* @param record_id
* @return
*/
CouponRecordVO findById(Long record_id);
@Override
public CouponRecordVO findById(Long record_id) {
LoginUser loginUser = LoginInterceptor.threadLocal.get();
CouponRecordDO couponRecordDO = couponRecordMapper.selectOne(new QueryWrapper<CouponRecordDO>().eq("user_id", loginUser.getId()).eq("id", record_id)
);
return beanProcess(couponRecordDO);
}
第8集 新用户注册-发放拉新领劵接口开发
简介:开发用户注册拉新领劵接口
- controller
@ApiOperation("rpc-新用户注册")
@PostMapping("/new_user_coupon")
public JsonData addNewUserConpon(@RequestBody NewUserConponRequest newUserConponRequest) {
return couponService.initNewuserCoupon(newUserConponRequest);
}
- NewUserConponRequest
@Data
public class NewUserConponRequest {
private long userId;
private String name;
}
- service
/**
* rpc-新用户注册
* @param newUserConponRequest
* @return
*/
JsonData initNewuserCoupon(NewUserConponRequest newUserConponRequest);
@Transactional
public JsonData initNewuserCoupon(NewUserConponRequest newUserConponRequest) {
LoginUser loginUser = new LoginUser();
loginUser.setId(newUserConponRequest.getUserId());
loginUser.setName(newUserConponRequest.getName());
LoginInterceptor.threadLocal.set(loginUser);
//查询新用户的优惠券列表
List<CouponDO> couponDOList = couponMapper.selectList(new QueryWrapper<CouponDO>().eq("category", CouponCategoryEnum.NEW_USER.name()));
for (CouponDO couponDO : couponDOList) {
//发放
//迷瞪行 需要加索
this.addCoupon(couponDO.getId(), CouponCategoryEnum.NEW_USER);
}
return JsonData.buildSuccess();
}
- InterceptorConfig放行 “/api/coupon/*/new_user_coupon”
第六章 商品微服务开发
第1集 数据库建立
CREATE TABLE `banner` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`img` varchar(524) DEFAULT NULL COMMENT '图片',
`url` varchar(524) DEFAULT NULL COMMENT '跳转地址',
`weight` int(11) DEFAULT NULL COMMENT '权重',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `product` (
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(128) DEFAULT NULL COMMENT '标题',
`cover_img` varchar(128) DEFAULT NULL COMMENT '封面图',
`detail` varchar(256) DEFAULT '' COMMENT '详情',
`old_price` decimal(16,2) DEFAULT NULL COMMENT '老价格',
`price` decimal(16,2) DEFAULT NULL COMMENT '新价格',
`stock` int(11) DEFAULT NULL COMMENT '库存',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`lock_stock` int(11) DEFAULT '0' COMMENT '锁定库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
第2集 商品微服务+MybatisPlusGenerator代码自动生成
商品微服务+MybatisPlusGenerator代码自动生成
复制用户服务的即可
第3集 轮播图列表接口开发
轮播图列表接口开发
- controller
@ApiOperation("轮播图列表")
@GetMapping("list")
public JsonData list() {
return JsonData.buildSuccess(bannerService.list());
}
- BannerVO
/**
* @author gtf
* @date 2022/11/24 13:52
*/
@Data
public class BannerVO {
private Integer id;
/**
* 图片
*/
private String img;
/**
* 跳转地址
*/
private String url;
/**
* 权重
*/
private Integer weight;
}
- service
List<BannerVO> list();
@Slf4j
@Service
public class BannerServiceImpl implements BannerService {
@Autowired
private BannerMapper bannerMapper;
@Override
public List<BannerVO> list() {
List<BannerDO> weight = bannerMapper.selectList(new QueryWrapper<BannerDO>().orderByAsc("weight"));
List<BannerVO> bannerVOList = weight.stream().map(obj -> beanProcess(obj)).collect(Collectors.toList());
return bannerVOList;
}
private BannerVO beanProcess(BannerDO obj) {
BannerVO bannerVO = new BannerVO();
BeanUtils.copyProperties(obj, bannerVO);
return bannerVO;
}
}
第4集 商品列表分页接口开发
商品列表分页接口开发
- controller
@RestController
@RequestMapping("/api/product/v1/")
@Api(tags = "product",value = "product")
public class ProductController {
@Autowired
private ProductService productService;
@ApiOperation("分页查询")
@GetMapping("page_product")
public JsonData pageProduct(@RequestParam(value = "page", defaultValue = "1") int page, @RequestParam(value = "size", defaultValue = "10") int size
) {
Map<String, Object> objectMap = productService.page(page, size);
return JsonData.buildSuccess(objectMap);
}
}
- ProductVO
/**
* @author gtf
* @date 2022/11/24 14:05
*/
@Data
public class ProductVO {
private Long id;
/**
* 标题
*/
private String title;
/**
* 封面图
*/
private String coverImg;
/**
* 详情
*/
private String detail;
/**
* 老价格
*/
private BigDecimal oldPrice;
/**
* 新价格
*/
private BigDecimal price;
/**
* 库存
*/
private Integer stock;
/**
* 创建时间
*/
private Date createTime;
/**
* 锁定库存
*/
private Integer lockStock;
}
- service
/**
* 分页查询
* @param page
* @param size
* @return
*/
Map<String, Object> page(int page, int size);
@Service
@Slf4j
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductMapper productMapper;
@Override
public Map<String, Object> page(int page, int size) {
Page<ProductDO> page1 = new Page<>(page, size);
IPage<ProductDO> productDOPage = productMapper.selectPage(page1, new QueryWrapper<ProductDO>());
HashMap<String, Object> pageMap = new HashMap<>(3);
pageMap.put("total_record", productDOPage.getTotal());
pageMap.put("total_page", productDOPage.getPages());
pageMap.put("current_data", productDOPage.getRecords().stream().map(obj -> beanProcess(obj)).collect(Collectors.toList()));
return pageMap;
}
private ProductVO beanProcess(ProductDO obj) {
ProductVO productVO = new ProductVO();
BeanUtils.copyProperties(obj, productVO);
productVO.setStock(obj.getStock()-obj.getLockStock());
return productVO;
}
}
第5集 商品微服务-商品详情接口开发和拦截器配置
商品微服务-商品详情接口开发和拦截器配置
- 拦截器
@Configuration
@Slf4j
public class InterceptorConfig implements WebMvcConfigurer {
@Bean
LoginInterceptor loginInterceptor() {
return new LoginInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor())
//拦截
.addPathPatterns("/api/banner/*/**","/api/product/*/**");
// //放行
// .excludePathPatterns("/api/user/*/reister"
// ,"/api/user/*/upload"
// ,"/api/user/*/login"
// ,"/api/notify/*/**");
WebMvcConfigurer.super.addInterceptors(registry);
}
}
- controller
@GetMapping("detail/{product_id}")
@ApiOperation("detail")
public JsonData detail(@PathVariable("product_id") Long product_id) {
ProductVO productVO = productService.findDetailByProductId(product_id);
return JsonData.buildSuccess(productVO);
}
- service
ProductVO findDetailByProductId(Long product_id);
@Override
public ProductVO findDetailByProductId(Long product_id) {
ProductDO id = productMapper.selectOne(new QueryWrapper<ProductDO>().eq("id", product_id));
ProductVO productVO = new ProductVO();
BeanUtils.copyProperties(id, productVO);
return productVO;
}
第七章 购物车微服务开发
第1集 购物车redis数据结构讲解
简介:购物车redis数据结构讲解
-
购物车数据结构介绍
- 一个购物车里面,存在多个购物项
- 所以 购物车结构是一个双层Map:
- Map<String,Map<String,String>>
- 第一层Map,Key是用户id
- 第二层Map,Key是购物车中商品id,值是购物车数据
-
对应redis里面的存储
- redis里面有多种数据结构,应该使用哪种?
- 答案是 hash结构
-
CartItemVO
@Data
public class CartItemVO {
/**
* sp id
*/
private Long productId;
/**
* 数量
*/
private Integer buyNum;
/**
* sp 标题
*/
private String productTitle;
/**
* sp 图片
*/
private String productImg;
/**
* 单价
*/
private BigDecimal amont;
/**
* 总价
*/
private BigDecimal totalAmont;
public Long getProductId() {
return productId;
}
public void setProductId(Long productId) {
this.productId = productId;
}
public Integer getBuyNum() {
return buyNum;
}
public void setBuyNum(Integer buyNum) {
this.buyNum = buyNum;
}
public String getProductTitle() {
return productTitle;
}
public void setProductTitle(String productTitle) {
this.productTitle = productTitle;
}
public String getProductImg() {
return productImg;
}
public void setProductImg(String productImg) {
this.productImg = productImg;
}
public BigDecimal getAmont() {
return amont;
}
public void setAmont(BigDecimal amont) {
this.amont = amont;
}
/**
* 总价格
*
* @return
*/
public BigDecimal getTotalAmont() {
return this.amont.multiply(new BigDecimal(this.buyNum));
}
}
- CartVO
/**
* @author gtf
* @date 2022/11/24 14:30
*/
public class CartVO {
/**
* 购物项
*/
private List<CartItemVO> cartItems;
/**
* 总数
*/
private Integer totalNum;
/**
* 总价格
*/
private BigDecimal totalPrice;
/**
* 实际支付的价格
*/
private BigDecimal realPayPrice;
/**
* 总件数
*
* @return
*/
public Integer getTotalNum() {
if (this.cartItems != null) {
int total = cartItems.stream().mapToInt(CartItemVO::getBuyNum).sum();
return total;
}
return 0;
}
/**
* 总价格
*
* @return
*/
public BigDecimal getTotalPrice() {
BigDecimal bigDecimal = new BigDecimal(0);
if (this.cartItems != null) {
for (CartItemVO cartItemVO : cartItems) {
BigDecimal itemVOTotalAmont = cartItemVO.getTotalAmont();
bigDecimal = bigDecimal.add(itemVOTotalAmont);
}
}
return totalPrice;
}
/**
* 实际支付的价格 需要重新计算
* @return
*/
public BigDecimal getRealPayPrice() {
BigDecimal bigDecimal = new BigDecimal(0);
if (this.cartItems != null) {
for (CartItemVO cartItemVO : cartItems) {
BigDecimal itemVOTotalAmont = cartItemVO.getTotalAmont();
bigDecimal = bigDecimal.add(itemVOTotalAmont);
}
}
return totalPrice;
}
public List<CartItemVO> getCartItems() {
return cartItems;
}
public void setCartItems(List<CartItemVO> cartItems) {
this.cartItems = cartItems;
}
}
第2集 添加购物车接口开发和方法抽取
简介:添加购物车接口开发和方法抽取
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
- CacheKey
/**
* 购物车key是用户唯一标识
*/
public static final String CAR_KEY = "cart:%s";
- CarItemRequest
@Data
public class CarItemRequest {
/**
* sp id
*/
private Long productId;
/**
* 数量
*/
private Integer buyNum;
}
- CarController
@RestController
@RequestMapping("/api/car/v1/")
@Api(tags = "car", value = "car")
public class CarController {
@Autowired
private CarService carService;
@PostMapping("add")
public JsonData addToCart(@RequestBody CarItemRequest carItemRequest) {
carService.addToCart(carItemRequest);
return JsonData.buildSuccess();
}
}
- service
/**
* 添加商品到购物车
* @param carItemRequest
*/
void addToCart(CarItemRequest carItemRequest);
@Service
@Slf4j
public class CarServiceImpl implements CarService {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ProductService productService;
@Override
public void addToCart(CarItemRequest carItemRequest) {
Long productId = carItemRequest.getProductId();
Integer buyNum = carItemRequest.getBuyNum();
//获取购物车
BoundHashOperations<String, Object, Object> myCart = getMyCartOps();
Object cacheObj = myCart.get(productId);
String result = "";
if (cacheObj != null) {
result = (String) cacheObj;
}
if (StringUtils.isBlank(result)) {
//不存在新建
CartItemVO cartItemVO = new CartItemVO();
ProductVO productVO = productService.findDetailByProductId(carItemRequest.getProductId());
if (productVO == null) {
throw new BizException(BizCodeEnum.CART_FAIL);
}
cartItemVO.setAmont(productVO.getPrice());
cartItemVO.setProductImg(productVO.getCoverImg());
cartItemVO.setProductTitle(productVO.getTitle());
cartItemVO.setBuyNum(carItemRequest.getBuyNum());
myCart.put(carItemRequest.getProductId(), JSON.toJSONString(cartItemVO));
} else {
//存在修改数量
CartItemVO cartItemVO = JSON.parseObject(result, CartItemVO.class);
cartItemVO.setBuyNum(cartItemVO.getBuyNum()+buyNum);
myCart.put(carItemRequest.getProductId(), JSON.toJSONString(cartItemVO));
}
}
/**
* 抽取我的购物车
*
* @return
*/
public BoundHashOperations<String, Object, Object> getMyCartOps() {
String cartKey = getCartKey();
return redisTemplate.boundHashOps(cartKey);
}
/**
* 获取购物车缓存key
*
* @return
*/
public String getCartKey() {
LoginUser loginUser = LoginInterceptor.threadLocal.get();
String cartKey = String.format(CacheKey.CAR_KEY, loginUser.getId());
return cartKey;
}
}
第3集 购物车功能开发-redis乱码问题和清空购物车接口开发
简介:redis乱码问题和清空购物车接口开发
- redis存储乱码问题
- 默认使用JdkSerializationRedisSerializer进行序列化
- 修改key-value序列化方式,hash结构不修改
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(factory);
RedisSerializer redisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(redisSerializer);
redisTemplate.setValueSerializer(redisSerializer);
return redisTemplate;
}
- 清空购物车接口开发
- controller
@DeleteMapping("clear")
public JsonData clearMyCart(){
carService.clear();
return JsonData.buildSuccess();
}
/**
* 清空
*/
void clear();
@Override
public void clear() {
redisTemplate.delete(getCartKey());
}
第4集 查看我的购物车
- controller
@GetMapping("myCart")
public JsonData findMyCart() {
CartVO cartVO = carService.getMyCart();
return JsonData.buildSuccess(cartVO);
}
- ProductService
/**
* 根据id批量查询商品
* @param cartItemIdList
* @return
*/
List<ProductVO> findProductIdBatch(List<Long> cartItemIdList);
@Override
public List<ProductVO> findProductIdBatch(List<Long> cartItemIdList) {
List<ProductDO> productDOList = productMapper.selectList(new QueryWrapper<ProductDO>().in("id", cartItemIdList));
List<ProductVO> collect = productDOList.stream().map(obj -> beanProcess(obj)).collect(Collectors.toList());
return collect;
}
- CarService
CartVO getMyCart();
@Override
public CartVO getMyCart() {
//获取全部购物项
List<CartItemVO> cartItemVOList = buildCartItem(false);
//封装
CartVO cartVO = new CartVO();
cartVO.setCartItems(cartItemVOList);
return cartVO;
}
第5集 购物车功能开发-删除和修改购物项接口
简介:购物车-删除购物项和修改购物车数量接口开发
- 删除购物项接口开发
@DeleteMapping("delete/{product_id}")
public JsonData delItem(@PathVariable("product_id") Long product_id){
carService.deleteItem(product_id);
return JsonData.buildSuccess();
}
void deleteItem(Long product_id);
/**
* 删除购物项
* @param product_id
*/
@Override
public void deleteItem(Long product_id) {
BoundHashOperations<String, Object, Object> myCartOps = getMyCartOps();
myCartOps.delete(product_id);
}
- 修改购物车商品数量接口开发
- controller
@PostMapping("chage")
public JsonData chageItemNum(@RequestBody CarItemRequest carItemRequest) {
carService.chageItemNum(carItemRequest);
return JsonData.buildSuccess();
}
- service
void chageItemNum(CarItemRequest carItemRequest);
/**
* 修改数量
*
* @param carItemRequest
*/
@Override
public void chageItemNum(CarItemRequest carItemRequest) {
BoundHashOperations<String, Object, Object> myCartOps = getMyCartOps();
Object o = myCartOps.get(carItemRequest.getProductId());
if (o == null) {
throw new BizException(BizCodeEnum.CART_FAIL);
}
CartItemVO cartItemVO = JSON.parseObject((String) o, CartItemVO.class);
cartItemVO.setBuyNum(carItemRequest.getBuyNum());
myCartOps.put(carItemRequest.getProductId(), JSON.toJSONString(cartItemVO));
}
第八章 订单微服务开发
第1集 数据库建立
数据库建立
- 订单表
CREATE TABLE `product_order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`out_trade_no` varchar(64) DEFAULT NULL COMMENT '订单唯一标识',
`state` varchar(11) DEFAULT NULL COMMENT 'NEW 未支付订单,PAY已经支付订单,CANCEL超时取消订单',
`create_time` datetime DEFAULT NULL COMMENT '订单生成时间',
`total_amount` decimal(16,2) DEFAULT NULL COMMENT '订单总金额',
`pay_amount` decimal(16,2) DEFAULT NULL COMMENT '订单实际支付价格',
`pay_type` varchar(64) DEFAULT NULL COMMENT '支付类型,微信-银行-支付宝',
`nickname` varchar(64) DEFAULT NULL COMMENT '昵称',
`head_img` varchar(524) DEFAULT NULL COMMENT '头像',
`user_id` int(11) DEFAULT NULL COMMENT '用户id',
`del` int(5) DEFAULT '0' COMMENT '0表示未删除,1表示已经删除',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`order_type` varchar(32) DEFAULT NULL COMMENT '订单类型 DAILY普通单,PROMOTION促销订单',
`receiver_address` varchar(1024) DEFAULT NULL COMMENT '收货地址 json存储',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2439 DEFAULT CHARSET=utf8mb4;
- 订单项表
CREATE TABLE `product_order_item` (
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
`product_order_id` bigint(11) DEFAULT NULL COMMENT '订单号',
`out_trade_no` varchar(32) DEFAULT NULL,
`product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
`product_name` varchar(128) DEFAULT NULL COMMENT '商品名称',
`product_img` varchar(524) DEFAULT NULL COMMENT '商品图片',
`buy_num` int(11) DEFAULT NULL COMMENT '购买数量',
`create_time` datetime DEFAULT NULL,
`total_amount` decimal(16,2) DEFAULT NULL COMMENT '购物项商品总价格',
`amount` decimal(16,0) DEFAULT NULL COMMENT '购物项商品单价',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=194 DEFAULT CHARSET=utf8mb4;
第2集 订单微服务+MybatisPlusGenerator代码自动生成工具
简介:Mybatis-plus-generator代码自动化生成微服务相关类
拷贝用户服务的
第3集 订单微服务项目基本配置整合-拦截器-枚举类配置
简介:项目相关配置整合和拦截器配置
- 枚举类配置
/**
* 客户端枚举类
*/
public enum ClientType {
/**
* 原生应用
*/
APP,
/**
* 电脑端
*/
PC,
/**
* 网页
*/
H5
}
public enum ProductOrderPayTypeEnum {
/**
* 微信支付
*/
WECHAT,
/**
* 支付支付
*/
ALIPAY,
/**
* 银行卡支付
*/
BANK;
}
public enum ProductOrderStateEnum {
/**
* 未支付订单
*/
NEW,
/**
* 已经支付订单
*/
PAY,
/**
* 超时取消订单
*/
CANCEL;
}
public enum ProductOrderTypeEnum {
/**
* 普通订单
*/
DAILY,
/**
* 促销订单
*/
PROMOTION;
}
- 拦截器
@Configuration
@Slf4j
public class InterceptorConfig implements WebMvcConfigurer {
@Bean
LoginInterceptor loginInterceptor() {
return new LoginInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor())
//拦截
.addPathPatterns("/api/order/*/**")
// //放行
.excludePathPatterns("/api/callback/*/**","/api/order/*/query");
WebMvcConfigurer.super.addInterceptors(registry);
}
}
第4集 订单微服务-创建订单service伪代码编写
简介:订单微服务-创建订单伪代码编写
- service编写伪代码
- 防重提交
- 用户微服务-确认收货地址
- 商品微服务-获取最新购物项和价格
- 订单验价
- 优惠券微服务-获取优惠券
- 验证价格
- 锁定优惠券
- 锁定商品库存
- 创建订单对象
- 创建子订单对象
- 发送延迟消息-用于自动关单
- 创建支付信息-对接三方支付
- ConfirmOrderRequest
@Data
public class ConfirmOrderRequest {
/**
* 购物车使用的优惠券,集满减劵
*
* 注意:如果传空或者小于0,则不用优惠券
*/
@JsonProperty("coupon_record_id")
private Long couponRecordId;
/**
* 最终购买的商品列表
* 传递id,购买数量从购物车中读取
*/
@JsonProperty("product_ids")
private List<Long> productIdList;
/**
* 支付方式
*/
@JsonProperty("pay_type")
private String payType;
/**
* 端类型
*/
@JsonProperty("client_type")
private String clientType;
/**
* 收货地址id
*/
@JsonProperty("address_id")
private long addressId;
/**
* 总价格,前端传递,后端需要验价
*/
@JsonProperty("total_amount")
private BigDecimal totalAmount;
/**
* 实际支付的价格,
* 如果用了优惠劵,则是减去优惠券后端价格,如果没的话,则是totalAmount一样
*/
@JsonProperty("real_pay_amount")
private BigDecimal realPayAmount;
/**
* 防重令牌
*/
@JsonProperty("token")
private String token;
}
- service
/**
* 防重提交
* 用户微服务-确认收货地址
* 商品微服务-获取最新购物项和价格
* 订单验价
* 优惠券微服务-获取优惠券
* 验证价格
* 锁定优惠券
* 锁定商品库存
* 创建订单对象
* 创建子订单对象
* 发送延迟消息-用于自动关单
* 创建支付信息-对接三方支付
* @param confirmOrderRequest
* @return
*/
@Override
public JsonData confirmOrder(ConfirmOrderRequest confirmOrderRequest) {
return null;
}
第九章 微服务注册中心Nacos和feign远程调用开发
第1集 微服务注册中心Docker容器化部署Nacos
简介:注册中心Docker容器化部署Nacos
- docker拉取镜像
docker pull nacos/nacos-server
- 查看镜像
docker images
- 启动Nacos
docker run --env MODE=standalone --name classes-nacos -d -p 8848:8848 nacos/nacos-server
//查看日志
docker logs -f
- 访问Nacos(记得开放网络安全组)
http://ip:8848/nacos
第2集 微服务Nacos注册中心配置Mysql持久化
简介:Nacos注册中心配置mysql持久化
-
nacos数据库脚本
- 导入数据库脚本(资料里面有)
- 新增一个用户 nacos/nacos
INSERT INTO `users` (`username`, `password`, `enabled`)
VALUES
('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', 1);
docker exec -it aaa8718e59b7 bash
vim conf/application.properties
# spring
server.servlet.contextPath=${SERVER_SERVLET_CONTEXTPATH:/nacos} server.contextPath=/nacos server.port=${NACOS_APPLICATION_PORT:8848}
# 这里改为mysql
spring.datasource.platform=mysql
nacos.cmdb.dumpTaskInterval=3600
nacos.cmdb.eventTaskInterval=10
nacos.cmdb.labelTaskInterval=300
nacos.cmdb.loadDataAtStart=false
db.num=${MYSQL_DATABASE_NUM:1}
# 这里=号后面改为这种 192.168.45.19是电脑的局域网ip,并非127.0.0.1和localhost,怎么看都话自行度娘
db.url.0=jdbc:mysql://192.168.45.19:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
# 这里改为数据库用户名和密码
db.user=root
db.password=123456
### The auth system to use, currently only 'nacos' is supported: nacos.core.auth.system.type=${NACOS_AUTH_SYSTEM_TYPE:nacos}
- 访问
第3集 多个微服务服务引入Nacos注册中心和Feign依赖
简介:微服务引入Nacos注册中心和Feign远程调用
- common项目添加依赖
<!--添加nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--Feign远程调用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 服务配置注册中心
cloud:
#注册中心地址
nacos:
discovery:
server-addr: 192.168.31.17:8848
- 启动类添加
@EnableFeignClients
@EnableDiscoveryClient
第4集 拉新业务-用户微服务和优惠券微服务之间的通讯
简介:拉新业务,用户微服务和优惠券微服务之间的通讯
- 用户微服务调用优惠券微服务开发
- NewUserCouponRequest
@Data
public class NewUserCouponRequest {
private long userId;
private String name;
}
- feign
@FeignClient(name = "classes-coupon-service")
public interface CouponFeignService {
/**
* 新用户注册发放优惠券
* @param newUserCouponRequest
* @return
*/
@PostMapping("/api/coupon/v1/new_user_coupon")
JsonData addNewUserCoupon(@RequestBody NewUserCouponRequest newUserCouponRequest);
}
- UserServiceImpl
/**
* 用户注册 初始化福利信息 todo
*
* @param userDO
*/
private void userRegisterInitTask(UserDO userDO) {
NewUserCouponRequest newUserCouponRequest = new NewUserCouponRequest();
newUserCouponRequest.setUserId(userDO.getId());
newUserCouponRequest.setName(userDO.getName());
JsonData jsonData = couponFeignService.addNewUserCoupon(newUserCouponRequest);
log.info("发放新用户结果",jsonData.toString());
}
第十章 微服务核心知识之分布式事务的那些事情
第1集 关于分布式事务的由来你知识多少
简介:分布式事务介绍和产生原因
-
什么是分布式事务
- 事务
事务指的就是一个操作单元,在这个操作单元中的所有操作最终要保持一致的行为,要么所有操作都成功,要么所有的操作都被撤销 分两种: 一个是本地事务:本地事物其实可以认为是数据库提供的事务机 一个是分布式事务
- 分布式事务
指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。 简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用 分布式事务需要保证这些小操作要么全部成功,要么全部失败。 本质上来说,分布式事务就是为了保证不同数据库的数据一致性
-
产生的原因
- 业务发展,数据库的拆分-分库分表
- SOA和微服务架构的使用
- 多个微服务之间调用异常
- 网络异常、请求超时、数据库异常、程序宕机等
第2集 分布式事务下数据最终一致性-CAP的权衡结果 BASE理论
简介:分布式事务下数据最终一致性-BASE理论介绍
- 什么是Base理论
CAP 中的一致性和可用性进行一个权衡的结果,核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性, 来自 ebay 的架构师提出
-
Basically Available(基本可用)
- 假设系统,出现了不可预知的故障,但还是能用, 可能会有性能或者功能上的影响,比如RT是10ms,变成50ms
-
Soft state(软状态)
- 允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时
-
Eventually consistent(最终一致性)
- 系统能够保证在没有其他新的更新操作的情况下,数据最终一定能够达到一致的状态,因此所有客户端对系统的数据访问最终都能够获取到最新的值
- 关于数据一致性
- 强一致:操作后的能立马一致且可以访问
- 弱一致:容忍部分或者全部访问不到
- 最终一致:弱一致性经过多一段时间后,都一致且正常
第3集 分布式事务的常见解决方案概览
简介:讲解分布式事务常见解决方案概览
-
常见分布式事务解决方案
- 2PC 和 3PC
- 两阶段提交, 基于XA协议
- TCC
- Try、Confirm、Cancel
- 事务消息
- 最大努力通知型
- 2PC 和 3PC
-
分布式事务分类
- 刚性事务:遵循ACID
- 柔性事务:遵循BASE理论
-
分布式事务框架
- TX-LCN:支持2PC、TCC等多种模式
- https://github.com/codingapi/tx-lcn
- 更新慢(个人感觉处于停滞状态)
- Seata:支持 AT、TCC、SAGA 和 XA 多种模式
- https://github.com/seata/seata
- 背靠阿里,专门团队推广
- 阿里云商业化产品GTS
- RocketMq:自带事务消息解决分布式事务
- TX-LCN:支持2PC、TCC等多种模式
第5集 分布式事务的常见核心概念知识讲解
简介:讲解分布式事务常见核心概讲解
-
前置知识
- X/OpenDTP 事务模型
是X/Open 这个组织定义的一套分布式事务的标准,也就是定义了规范和 API 接口,由各个厂商进行具体的实现 DTP 是分布式事物处理(Distributed Transaction Processing)的简称
- XA协议
XA是由X/Open组织提出的分布式事务规范。 XA规范主要定义了(全局)事务管理器(TM)和(局 部)资源管理器(RM)之间的接口 主流的数据库产品都实现了XA接口,是一个双向的系统接口,在事务管理器以及多个资源管理器之间作为通信桥梁
- JTA
Java Transaction API,java根据XA规范提供的事务处理标准
- AP
application, 应用程序也就是业务层,微服务等
- RM
Resource Manager,资源管理器。一般是数据库,也可以是其他资源管理器,比如消息队列,文件系统
- TM
Transaction Manager ,事务管理器、事务协调者,负责接收来自用户程序(AP)发起的 XA 事务指令,并调度和协调参与事务的所有 RM(数据库),确保事务正确完成
-
事务模型
在分布式系统中,每一个机器节点能够明确知道自己在进行事务操作过程中的 结果是成功还是失败,但无法直接获取到其他分布式节点的操作结果
当一个事务操作跨越多个分布式节点的时候,为了保持事务处理的 ACID 特性,
需要引入一个“协调者”(TM)来统一调度所有分布式节点的执行逻辑,这些被调度的分布式节点被称为 AP。
TM 负责调度 AP 的行为,并最终决定这些 AP 是否要把事务真正进行提交到(RM)
第6集 XA实现分布式事务的原理-两阶段提交2PC流程解析
简介:讲解XA实现分布式事务的原理
-
XA协议规范-实现分布式事务的原理如下
-
一般习惯称为 两阶段提交协议(The two-phase commit protocol,2PC)
-
是XA用于在全局事务中协调多个资源的机制,MySql5.5以上开始支持
-
准备阶段:
事务管理器给每个参与者都发送Prepared消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。
- Undo日志是记录修改前的数据,用于数据库回滚
- Redo日志是记录修改后的数据,用于提交事务后写入数据
-
提交阶段:
- 如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息,否则发送提交(Commit)消息;
- 参与者根据事务管理器的指令执行【提交】或者【回滚】操作,并释放事务处理过程中使用的锁资源
- 注意:必须在最后阶段释放锁资源。
-
-
总结
- XA协议简单,数据库支持XA协议,开发使用成本比较低
- 对业务侵⼊很小,最⼤的优势就是对使⽤⽅透明
- 用户可以像使⽤本地事务⼀样使⽤基于 XA 协议的分布式事务,能够严格保障事务 ACID 特性
- 事务执⾏过程中需要将所需资源全部锁定,也就是俗称的刚性事务
- 刚性事务:遵循ACID
- 柔性事务:遵循BASE理论
- 性能不理想,占用锁资源比较多,高并发常见下无法满足
- 商业付费数据库支持好,mysql目前支持不是很完善
- 基于 XA 协议的 除了2PC,还有 3PC等
- 三段提交(3PC)是二阶段提交(2PC)的一种改进版本 ,为解决两阶段提交协议的阻塞问题
- 采用超时机制,解决TM故障后RM的阻塞问题,但与此同时却多了一次网络通信,性能上也不理想
- 2PC和3PC目前使用不是很多,只做简单了解即可
第7集 分布式事务的解决方案之一-柔性事务-TCC介绍
简介:讲解TCC柔性事务的解决方案
- 什么是TCC柔性事务
- 刚性事务:遵循ACID
- 柔性事务:遵循BASE理论
- TCC:
- 将事务提交分为
- Try:完成所有业务检查( 一致性 ) ,预留必须业务资源( 准隔离性 )
- Confirm :对业务系统做确认提交,默认 Confirm阶段不会出错的 即只要Try成功,Confirm一定成功
- Cancel : 业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放, 进行补偿性
- TCC 事务和 2PC 的类似,Try为第一阶段,Confirm - Cancel为第二阶段,它对事务的提交/回滚是通过执行一段 confirm/cancel 业务逻辑来实现,并且也并没有全局事务来把控整个事务逻辑
- 将事务提交分为
含义 | 操作方法 |
---|---|
预留业务资源/数据效验 | Try |
确认执行业务操作,提交数据,不做任何业务检查,try成功,confirm必定成功,需保证幂等 | Confirm |
取消执行业务操作,回滚数据,需保证幂等,也是常说的补偿性事务 | Cancel |
- 优点:
- 它把事务运行过程分成 Try、Confirm/Cancel 两个阶段
- 每个阶段由业务代码控制,这样事务的锁力度可以完全自由控制
- 不存在资源阻塞的问题,每个方法都直接进行事务的提交
- 缺点
- 在业务层编写代码实现的两阶段提交,原本一个方法,现在却需要三个方法来支持
- 对业务的侵入性很强,不能很好的复用
- 注意:使用TCC时要注意Try - Confirm - Cancel 3个操作的幂等控制,由于网络原因或者重试操作都有可能导致这几个操作的重复执行
第8集 分布式事务的解决方案之一事务消息
简介:讲解分布式事务的解决方案之一事务消息
-
事务消息
- 消息队列提供类似Open XA的分布式事务功能,通过消息队列事务消息能达到分布式事务的最终一致
-
半事务消息
- 暂不能投递的消息,发送方已经成功地将消息发送到了消息队列服务端,但是服务端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半事务消息。
-
消息回查
- 由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,消息队列服务端通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit或是Rollback),该询问过程即消息回查
第9集 * 目前较为主流的MQ,比如ActiveMQ、RabbitMQ、Kafka、RocketMQ等,只有RocketMQ支持事务消息
-
如果其他队列需要事务消息,可以开发个消息服务,自行实现半消息和回查功能
-
好处
- 事务消息不仅可以实现应用之间的解耦,又能保证数据的最终一致性
- 同时将传统的大事务可以被拆分为小事务,能提升效率
- 不会因为某一个关联应用的不可用导致整体回滚,从而最大限度保证核心系统的可用性
-
缺点
- 不能实时保证数据一致性
- 极端情况下需要人工补偿,比如 假如生产者成功处理本地业务,消费者始终消费不成功
第10集 最终一致性的体现-第三方支付平台和微服务之间的通讯
简介:讲解第三方支付平台和微服务之间的交互
-
支付业务
- 支付宝支付
- 微信支付
- 其他支付
-
多个服务之间通信,怎么保证分布式事务呢?
-
利用最终一致性思想,也叫柔性事务解决方案
第11集 阿里巴巴开源分布式事务框架Seata介绍
简介:讲解分布式事务框架Seata介绍
-
分布式事务框架
- TX-LCN:支持2PC、TCC等多种模式
- https://github.com/codingapi/tx-lcn
- 更新慢(个人感觉处于停滞状态)
- Seata:支持 AT、TCC、SAGA 和 XA 多种模式
- https://github.com/seata/seata
- 背靠阿里,专门团队推广
- TX-LCN:支持2PC、TCC等多种模式
-
为啥选择Seata呢
- git地址
- 背靠阿里,专门团队推广和技术团队
- 已经线上大规模使用且没出现重大漏洞
- 和主流微服务框架轻松整合
- 社区活跃度高、文档齐全、功能强大
-
什么是Seata
- 一个开源的分布式事务框架, 由阿里中间件团队发起的开源项目Fescar,后更名为Seata
- 中文文档地址
- 详细介绍
是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。 在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。 经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。 2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备
- 文档比较详细,如果大家把上一章分布式事务的核心知识:
- XA协议的2PC、TCC、事务消息掌握,那学起来会很轻松
分布式事务框架Seata核心组件和术语介绍
简介:讲解分布式事务框架Seata核心组件和术语
-
Seata主要由三个重要组件组成:
-
TC:Transaction Coordinator 事务协调器,管理全局的分支事务的状态,用于全局性事务的提交和回滚。
-
TM:Transaction Manager 事务管理器,用于开启、提交或者回滚【全局事务】。
-
RM:Resource Manager 资源管理器,用于分支事务上的资源管理,向TC注册分支事务,上报分支事务的状态,接受TC的命令来提交或者回滚分支事务
- 传统XA协议实现2PC方案的 RM 是在数据库层,RM本质上就是数据库自身;
- Seata的RM是以jar包的形式嵌入在应用程序里面
-
架构:TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端
第4集 Seata框架里面分布式事务的生命周期过程描述
简介:讲解分布式事务框架Seata事务处理过程描述
- 一个典型的事务过程包括:
- A服务的TM 向 TC 申请开启(Begin)一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
- A服务的RM向TC注册分支事务
- A服务执行分支事务,对数据库做操作
- A服务开始远程调用B服务,并把XID 在微服务调用链路的上下文中传播。
- B服务的RM向TC注册分支事务,并将其纳入XID对应的全局事务的管辖
- B服务执行分支事务,向数据库做操作
- 全局事务调用链处理完毕,TM 根据有无异常向 TC 发起针对 XID 的全局提交(Commit)或回滚(Rollback)决议。
- TC 调度 XID 下管辖的全部分支事务完成提交(Commit)或回滚(Rollback)请求。
- Seata 实现分布式事务,关键角色UNDO_LOG(回滚日志记录表)
在每个应用需要分布式事务的业务库中创建这张表,这个表的核心作用是将业务数据在更新前后的数据镜像组织成回滚日志,保存在UNDO_LOG表中,以便业务异常能随时回滚
第5集 分布式事务框架Seata的AT模式流程介绍
简介:介绍Seata的AT模式流程
-
Seata
有四种模式: (简单了解即可,深入的话看分布式事务专题)-
AT
-
AT模式可以应对大多数的业务场景,并且基本可以做到无业务入侵、开发者无感知
-
用户只需关心自己的 业务SQL. AT 模式分为两个阶段,可以认为是2PC
- 一阶段:执行用户SQL
Seata 会拦截“业务 SQL”,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据 在业务数据更新之后,再将其保存成“after image”,最后生成行锁 以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性
- 二阶段:
Seata
框架自动生成提交或者回滚
二阶段提交: 因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将阶段一保存的快照数据和行锁删掉,完成数据清理即可。 二阶段回滚: 还原业务数据, 回滚方式便是用“before image”还原业务数据; 但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image” 如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理
-
-
TCC
-
Sage
-
XA
-
第6集 新版本-分布式事务框架Seata 服务端部署安装最佳实践
简介:讲解新版本Seata分布式事务的部署和安装
-
基于AT模式
- 创建undo_log表, 每个库都需要
- http://seata.io/zh-cn/docs/dev/mode/at-mode.html
-- 注意此处0.7.0+ 增加字段 context CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-
下载部署Seata的TC服务端
- http://seata.io/zh-cn/blog/download.html
- 我们下载1.3就行,不要下载最新的(除非对新的很有把握不出问题)
-
Linux/Mac/Windows服务器安装
- 解压
- 修改jvm内存(默认是2g,防止内存不够)
- ./seata-server.sh 启动,默认是8091端口(记得防火墙开放端口,也可以nohup守护进程启动)
-
TC需要存储全局事务和分支事务的记录,支持三种存储模式
- file模式 (默认):性能高, 适合单机模式,在内存中读写,并持久化到本地文件中
- 在 bin/sessionStore/root.data文件
- db模式 :性能较差,适合tc集群模式
- redis模式:性能教高,适合tc集群模式
- file模式 (默认):性能高, 适合单机模式,在内存中读写,并持久化到本地文件中
-
问题:
- seata 在 JDK11下运行报错
- 解决: 下载下来的seata 默认没有存放日志文件的目录, 手动创建seata/logs/seata_gc.log 目录和文件
第7集 SpringCloudAlibaba微服务整合Seata分布式事务框架
简介:SpringCloudAlibaba整合分布式事务seata框架
- common项目添加依赖
- 出现的问题:no available service ‘null’ found, please make sure registry config correct
- 安装的服务端版本必须要和你客户端的版本保持一样
<!-- <dependency>-->
<!-- <groupId>com.alibaba</groupId>-->
<!-- <artifactId>fastjson</artifactId>-->
<!-- <version>1.2.80</version>-->
<!-- </dependency>-->
<!--alibaba微服务整合分布式事务,上面的方式不行 mvn 包冲突-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--alibaba微服务整合分布式事务,这个方式才行-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
- 各个微服务配置文件修改
#seata配置
seata:
tx-service-group: ${spring.application.name}-group
service:
grouplist:
classes: 127.0.0.1:8091
vgroup-mapping:
classes-user-service-group: classes
- 注释掉全局异常
//
///**
// * 自定义异常处理器
// * @author gtf
// * @date 2022/11/22 11:26
// */
//@ControllerAdvice
//@Slf4j
//public class CustomExceptionHandle {
// @ExceptionHandler(value = Exception.class)
// @ResponseBody
// public JsonData handle(Exception e) {
//
// if (e instanceof BizException) {
// BizException bizException = (BizException) e;
// log.error("业务异常 {}", e);
// return JsonData.buildCodeAndMsg(bizException.getCode(), bizException.getMsg());
// } else {
// log.error(e.getMessage());
// log.error("非业务异常 {}", e);
// return JsonData.buildError("非业务异常,全局异常,未知错误{}"+e);
// }
// }
//}
- 开发测试接口
- 测试分布式事务
- TM入口service的方法增加 @GlobalTransactional 注解
- 启动类 @EnableTransactionManagement
第8集 全局异常下微服务整合Seata分布式事务失效解决方案
简介:自定义全局异常下分布式事务失效解决方案
- 问题: 微服务场景下,配置了统一全局异常处理,导致seata在AT模式下无法正常回滚问题
- 如果使用Feign 配置了容错类(fallback)或者容错工厂(fallbackFactory),也是一样的问题
- 原因:服务A调用服务B, 服务B发生异常,由于全局异常处理的存在(@ControllerAdvice), seata 无法拦截到B服务的异常,从而导致分布式事务未生效
- 解决思路
配置了全局异常处理,所以rpc一定会有返回值, 所以在每个全局事务方法最后, 需要判断rpc是否发生异常
发生异常则抛出 RuntimeException或者子类
- 方式一:RPC接口不配置全局异常
- 方式二:利用AOP切面解决
- 方式三:程序代码各自判断RPC响应码是否正常,再抛出异常
第十一章 分布式事务的另一种解决方案
第1集 分布式事务的另一种解决方案-下单商品库存扣减
简介:分布式事务事务的另一种解决方案,下单商品库存的扣减
-
下单伪代码
* * 防重提交 * * 用户微服务-确认收货地址 * * 商品微服务-获取最新购物项和价格 * * 订单验价 * * 优惠券微服务-获取优惠券 * * 验证价格 * * 锁定优惠券 * * 锁定商品库存 * * 创建订单对象 * * 创建子订单对象 * * 发送延迟消息-用于自动关单 * * 创建支付信息-对接三方支付
-
核心逻辑
- 用下单:锁库存和优惠券记录
- 下单异常或者超时未支付:释放库存和优惠券记录
第2集 抽取架构业务模型-优惠券记录扣减-释放流程解析
简介:触类旁通-抽取架构业务模型-转移到优惠券记录扣减回收
-
核心逻辑
- 用下单:锁库存和优惠券记录
- 下单异常或者超时未支付:释放库存和优惠券记录
-
锁定记录状态
lock_state
‘锁定状态锁定LOCK-完成 FINISH-取消CANCEL’,
第3集 商品库存锁定和优惠券记录锁定任务表设计
简介:商品库存锁定和优惠券记录锁定任务表设计
- 下单锁库存任务表设计
CREATE TABLE `product_task` (
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
`product_id` bigint(11) DEFAULT NULL COMMENT '商品id',
`buy_num` int(11) DEFAULT NULL COMMENT '购买数量',
`product_name` varchar(128) DEFAULT NULL COMMENT '商品标题',
`lock_state` varchar(32) DEFAULT NULL COMMENT '锁定状态锁定LOCK 完成FINISH-取消CANCEL',
`out_trade_no` varchar(32) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
- 优惠券记录锁定任务表设计
CREATE TABLE `coupon_task` (
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
`coupon_record_id` bigint(11) DEFAULT NULL COMMENT '优惠券记录id',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`out_trade_no` varchar(64) DEFAULT NULL COMMENT '订单号',
`lock_state` varchar(32) DEFAULT NULL COMMENT '锁定状态 锁定LOCK-完成FINISH 取消CANCEL',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
coupon服务基础代码生成 coupon_task
product_task服务代码生成 product_task
第4集 基于Linux服务器安装RabbitMQ容器化部署
简介:Docker安装RabbitMQ消息队列
- 阿里云安装RabbitMQ
- 阿里云折扣地址 https://www.aliyun.com/minisite/goods?userCode=r5saexap&share_source=copy_link
- 最少 2核4g或者推荐 2核8g(用家人账号购买,接近1折,初次买1年或者3年)
- 登录个人的Linux服务器
- Docker安装RabbitMQ
#拉取镜像
docker pull rabbitmq:3.8.12-management-alpine
docker run -d --hostname rabbit_host1 --name xd_rabbit -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=password -p 15672:15672 -p 5672:5672 rabbitmq:3.8.12-management-alpine
#介绍
-d 以守护进程方式在后台运行
-p 15672:15672 management 界面管理访问端口
-p 5672:5672 amqp 访问端口
--name:指定容器名
--hostname:设定容器的主机名,它会被写到容器内的 /etc/hostname 和 /etc/hosts,作为容器主机IP的别名,并且将显示在容器的bash中
-e 参数
RABBITMQ_DEFAULT_USER 用户名
RABBITMQ_DEFAULT_PASS 密码
- 主要端口介绍
4369 erlang 发现口
5672 client 端通信口
15672 管理界面 ui 端口
25672 server 间内部通信口
-
访问管理界面
- ip:15672
-
注意事项!!!!
- Linux服务器检查防火墙是否关闭
- 云服务器检查网络安全组是否开放端口
CentOS 7 以上默认使用的是firewall作为防火墙 查看防火墙状态 firewall-cmd --state 停止firewall systemctl stop firewalld.service 禁止firewall开机启动 systemctl disable firewalld.service
第5集 延迟队列核心知识之RabbitMQ死信队列 + TTL回顾
简介:讲解RabbitMQ的的死信队列+ TTL
-
什么是TTL
- time to live 消息存活时间
- 如果消息在存活时间内未被消费,则会别清除
- RabbitMQ支持两种ttl设置
- 单独消息进行配置ttl
- 整个队列进行配置ttl(居多)
-
什么是rabbitmq的死信队列
- 没有被及时消费的消息存放的队列
-
什么是rabbitmq的死信交换机
- Dead Letter Exchange(死信交换机,缩写:DLX)当消息成为死信后,会被重新发送到另一个交换机,这个交换机就是DLX死信交换机。
-
消息有哪几种情况成为死信
- 消费者拒收消息**(basic.reject/ basic.nack)**,并且没有重新入队 requeue=false
- 消息在队列中未被消费,且超过队列或者消息本身的过期时间TTL(time-to-live)
- 队列的消息长度达到极限
- 结果:消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列
第6集 RabbitMQ 延迟队列介绍和应用场景回顾
简介:讲解RabbitMQ的延迟队列和应用场景
-
什么是延迟队列
- 一种带有延迟功能的消息队列,Producer 将消息发送到消息队列 服务端,但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某一个时间投递到 Consumer 进行消费,该消息即定时消息
-
使用场景
- 通过消息触发一些定时任务,比如在某一固定时间点向用户发送提醒消息
- 用户登录之后5分钟给用户做分类推送、用户多少天未登录给用户做召回推送;
- 消息生产和消费有时间窗口要求:比如在天猫电商交易中超时未支付关闭订单的场景,在订单创建时会发送一条 延时消息。这条消息将会在 30 分钟以后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。 如支付未完成,则关闭订单。如已完成支付则忽略
-
Cloud微服务大课训练营里面的应用
- 优惠券回收
- 商品库存回收
-
业界的一些实现方式
- 定时任务高精度轮训
- 采用RocketMQ自带延迟消息功能
- RabbitMQ本身是不支持延迟队列的,怎么办?
- 结合死信队列的特性,就可以做到延迟消息
- 操作
- 消息生产
- 投递到普通的topic交换机
- 消息过期,进入死信交换机
- 消息消费
- 消费者监听死信交换机的队列
- 消息生产
第7集 SpringCloudAlibaba微服务整合RabbitMQ依赖和配置
简介:项目整合RabbitMQ依赖和配置
- 什么是Spring-AMQP
- 官网:https://spring.io/projects/spring-amqp
- Spring 框架的AMQP消息解决方案,提供模板化的发送和接收消息的抽象层,提供基于消息驱动的 POJO的消息监听等
- 提供不依赖于任何特定的AMQP代理实现或客户端库通用的抽象,最终用户代码将很容易实现更易替换、添加和删除AMQP,因为它可以只针对抽象层来开发
- 总之就是提高我们的框架整合消息队列的效率,SpringBoot为更方便开发RabbitMQ推出了starter,
- 我们使用 spring-boot-starter-amqp 进行开发
- common项目添加依赖
<!--引入AMQP-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
- 各个微服务添加配置
#消息队列
rabbitmq:
host: 192.168.31.17
port: 5672
virtual-host: /
password: guest
username: guest
#开启手动确认消息
listener:
simple:
acknowledge-mode: manual
- 其他自定义配置后续添加
第8集 商品下单锁定优惠券记录模块开发
简介:商品下单锁定优惠券记录模块开发
- 锁定消息 CouponRecordMessage
@Data
public class CouponRecordMessage {
private Long messageId;
/**
* 订单号
*/
private String outTradeNo;
/**
* 锁定id
*/
private Long taskId;
}
- mq配置
#自定义消息队列配置,发送锁定库存消息-》延迟exchange-》lock.queue-》死信exchange-》release.queue
mqconfig:
#延迟队列,不能被监听消费
coupon_release_delay_queue: coupon.release.delay.queue
#延迟队列的消息过期后转发的队列
coupon_release_queue: coupon.release.queue
#交换机
coupon_event_exchange: coupon.event.exchange
#进入延迟队列的路由key
coupon_release_delay_routing_key: coupon.release.delay.routing.key
#消息过期,进入释放死信队列的key
coupon_release_routing_key: coupon.release.routing.key
#消息过期时间,毫秒,测试改为15秒
ttl: 15000
- LockCouponRecordRequest锁定优惠券
@Data
public class LockCouponRecordRequest {
/**
* 记录id
*/
private List<Long> lockCouponRecordIds;
/**
* 订单号
*/
private String orderOutTradeNo;
}
- service
/**
* rpc-锁定优惠券记录
* @param recordRequest
* @return
*/
JsonData lockCouponRecords(LockCouponRecordRequest recordRequest);
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RabbitMQConfig rabbitMQConfig;
/**
* 锁定优惠券记录
* task表插入记录
* 发送延迟消息
*
* @param recordRequest
* @return
*/
@Override
public JsonData lockCouponRecords(LockCouponRecordRequest recordRequest) {
//获取登陆用户
LoginUser loginUser = LoginInterceptor.threadLocal.get();
String orderOutTradeNo = recordRequest.getOrderOutTradeNo();
List<Long> lockCouponRecordIds = recordRequest.getLockCouponRecordIds();
//优惠券锁定
int updateRows = couponRecordMapper.lockUseStateBatch(loginUser.getId(), CouponStateEnum.USED.name(), lockCouponRecordIds);
//task表插入记录
List<CouponTaskDO> couponTaskDOList = lockCouponRecordIds.stream().map(obj -> {
CouponTaskDO couponTaskDO = new CouponTaskDO();
couponTaskDO.setCreateTime(new Date());
couponTaskDO.setOutTradeNo(orderOutTradeNo);
couponTaskDO.setCouponRecordId(obj);
couponTaskDO.setLockState(StockTaskStateEnum.LOCK.name());
return couponTaskDO;
}).collect(Collectors.toList());
int insertRows = couponTaskMapper.insertBatch(couponTaskDOList);
log.info("锁定{}", updateRows);
log.info("新增记录{}", insertRows);
if (updateRows == insertRows) {
//发送延迟消息
for (CouponTaskDO couponTaskDO : couponTaskDOList) {
CouponRecordMessage couponRecordMessage = new CouponRecordMessage();
couponRecordMessage.setTaskId(couponTaskDO.getId());
couponRecordMessage.setOutTradeNo(orderOutTradeNo);
rabbitTemplate.convertAndSend(rabbitMQConfig.getEventExchange(), rabbitMQConfig.getCouponReleaseDelayRoutingKey(), couponRecordMessage);
log.info("锁定消息发送成功", couponRecordMessage.toString());
}
return JsonData.buildSuccess();
} else {
throw new BizException(BizCodeEnum.COUPON_RECORD_LOCK_FAIL);
}
}
- controller
@ApiOperation("rpc-锁定优惠券记录")
@PostMapping("lock_records")
public JsonData lockCouponRecords(@RequestBody LockCouponRecordRequest recordRequest) {
JsonData data = couponRecordService.lockCouponRecords(recordRequest);
return JsonData.buildSuccess(data);
}
- test
@RunWith(SpringRunner.class)
@SpringBootTest(classes = CouponApplication.class)
@Slf4j
public class MQTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void sendDelayMsg(){
rabbitTemplate.convertAndSend("coupon.event.exchange","coupon.release.delay.routing.key","this is coupon record lock msg");
}
}
第9集 优惠券微服务延迟消息消费-释放优惠券功能开发
简介:优惠券回收开发
-
优惠券消费MQ监听器开发
-
流程梳理
优惠券解锁记录场景
1、超时未支付,比如30分钟则订单失效关闭
2、下单成功,创建订单业务失败,订单回滚
库存解锁防止继续支付:
1、30分支付超时则无法支付订单
2、订单31分延迟消息(比订单超时大几分钟)
->查询订单状态-向第三方支付查询订单状态,只有未支付状态,且本地订单状态是NEW,才修改本地订单状态为取消CANCEL,其他业务才可以解锁对应的库存库存
3、商品、优惠券库存32分延迟消息(比订单超时大几分钟)
->查询订单状态-订单不存在,解锁库存
->查询订单状态
1)订单状态为取消CANCEL的情况,才可以解锁库存,确认消息接收;
2)订单状态为未支付NEW的情况,则不解锁库存,不修改订单状态,重新投递消息或者拒收;
(避免网络延迟到 导致订单关单消息,比库存解锁还慢,没更新订单状态)
3)如果是订单已经支付则修改库存task工作单状态,确认消息接收;
注意:延迟队列一定要开启手动的ack机制,防止解锁失败,消息丢失,也要防止多次解锁
解锁库存的时候:修改状态和修改对应库存task工作单状态应该是同个事务,防止其中一个失败
mq
@Slf4j
@Component
@RabbitListener(queues = "${mqconfig.coupon_release_queue}")
public class CouponMQListener {
@Autowired
private CouponRecordService couponRecordService;
@Autowired
private RedissonClient redissonClient;
@RabbitHandler
public void releaseCouponRecord(String recordMessage, Message message, Channel channel) throws IOException {
long msgTag = message.getMessageProperties().getDeliveryTag();
channel.basicReject(msgTag,true);
}
/**
*
* 重复消费-幂等性
*
* 消费失败,重新入队后最大重试次数:
* 如果消费失败,不重新入队,可以记录日志,然后插到数据库人工排查
* @param recordMessage
* @param message
* @param channel
* @throws IOException
*/
@RabbitHandler
public void releaseCouponRecord(CouponRecordMessage recordMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息:releaseCouponRecord消息内容:{}", recordMessage);
long msgTag = message.getMessageProperties().getDeliveryTag();
boolean flag = couponRecordService.releaseCouponRecord(recordMessage);
//防止同个解锁任务并发进入;如果是串行消费不用加锁;加锁有利也有弊,看项目业务逻辑而定
//Lock lock = redissonClient.getLock("lock:coupon_record_release:"+recordMessage.getTaskId());
//lock.lock();
try {
if (flag) {
//确认消息消费成功,丢低消息
channel.basicAck(msgTag, false);
}else {
log.error("释放优惠券失败 flag=false,{}",recordMessage);
channel.basicReject(msgTag,true);
}
} catch (IOException e) {
log.error("释放优惠券记录异常:{},msg:{}",e,recordMessage);
channel.basicReject(msgTag,true);
}
// finally {
// lock.unlock();
// }
}
}
- service
/**
* 释放优惠券
* @param recordMessage
* @return
*/
boolean releaseCouponRecord(CouponRecordMessage recordMessage);
/**
* 释放优惠券
* 查询工作单是否存在
* 查询订单状态
*
* @param recordMessage
* @return
*/
@Override
@Transactional
public boolean releaseCouponRecord(CouponRecordMessage recordMessage) {
//查询工作单是否存在
CouponTaskDO couponTaskDO = couponTaskMapper.selectOne(new QueryWrapper<CouponTaskDO>().eq("id", recordMessage.getTaskId()));
if (couponTaskDO == null) {
log.warn("工作单不存在");
return true;
}
if (couponTaskDO.getLockState().equalsIgnoreCase(StockTaskStateEnum.LOCK.name())) {
//查询订单状态
JsonData jsonData = productOrderFeignService.queryProductOrderState(recordMessage.getOutTradeNo());
if (jsonData.getCode() == 0) {
//正常响应,判断订单状态
String state = jsonData.getData().toString();
if (ProductOrderStateEnum.NEW.name().equalsIgnoreCase(state)) {
//状态未new,重新投递消息
log.warn("状态未new,重新投递消息");
return false;
}
//已经支付
if (ProductOrderStateEnum.PAY.name().equalsIgnoreCase(state)) {
//更新task状态 修改task状态未finish
couponTaskDO.setLockState(StockTaskStateEnum.FINISH.name());
couponTaskMapper.update(couponTaskDO, new QueryWrapper<CouponTaskDO>().eq("id", recordMessage.getTaskId()));
log.warn("更新task状态 修改task状态未finish");
return true;
}
}
//订单不存在,或者订单呗取消,确认消息,修改task状态未cancel,回复优惠券使用记录new
log.warn("订单不存在,或者订单呗取消,确认消息");
couponTaskDO.setLockState(StockTaskStateEnum.CACEL.name());
couponTaskMapper.update(couponTaskDO, new QueryWrapper<CouponTaskDO>().eq("id", recordMessage.getTaskId()));
//恢复优惠券记录
couponRecordMapper.updateState(couponTaskDO.getCouponRecordId(), CouponStateEnum.NEW.name());
return true;
} else {
log.warn("工作单状态重复消费");
return true;
}
}
第十一章 订单微服务下单和验价设计和开发
第1集 订单微服务下单逻辑之确认收货地址模开发
简介: 订单微服务-确认收货地址模块开发
- 服务之间传递token
/**
* 服务之间传递token
*
* @return
*/
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor() {
return template -> {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
if (null == request) {
return;
}
log.info(request.getHeaderNames().toString());
template.header("token", request.getHeader("token"));
} else {
log.warn("requestInterceptor获取Header空指针异常");
}
};
}
- feign
/**
* @author gtf
* @date 2022/11/28 10:38
*/
@FeignClient(name = "classes-user-service")
public interface UserFeignService {
/**
* 查询用户地址
* @param addressId
* @return
*/
@GetMapping("/api/address/v1/find/{address_id}")
JsonData detail(@PathVariable("address_id")long addressId);
}
- service
@Override
public JsonData confirmOrder(ConfirmOrderRequest orderRequest) {
LoginUser loginUser = LoginInterceptor.threadLocal.get();
String orderOutTradeNo = CommonUtil.getStringNumRandom(32);
ProductOrderAddressVO addressVO = this.getUserAddress(orderRequest.getAddressId());
log.info("收货地址信息:{}",addressVO);
return null;
}
/**
* 获取收货地址详情
* @param addressId
* @return
*/
private ProductOrderAddressVO getUserAddress(long addressId) {
JsonData addressData = userFeignService.detail(addressId);
if(addressData.getCode() !=0){
log.error("获取收获地址失败,msg:{}",addressData);
throw new BizException(BizCodeEnum.ADDRESS_NO_EXITS);
}
// ProductOrderAddressVO addressVO = addressData.getData(new TypeReference<>(){});
ProductOrderAddressVO addressVO = JSON.parseObject((String) JSON.toJSONString(addressData.getData()), ProductOrderAddressVO.class);
return addressVO;
}
第2集 获取商品最新价格开发
简介:订单微服务下单获取最新价格开发
-cart服务
@PostMapping("confirm_order_cart_items")
public JsonData confirmOrderCartItem(@RequestBody List<Long> productList) {
List<CartItemVO> cartItemVOS = carService.confirmOrderCartItems(productList);
return JsonData.buildSuccess(cartItemVOS);
}
@Override
public List<CartItemVO> confirmOrderCartItems(List<Long> productList) {
//获取全部
List<CartItemVO> cartItemVOS = buildCartItem(true);
//过滤,清空
List<CartItemVO> cartItemResult = cartItemVOS.stream().filter(obj -> {
if (productList.contains(obj.getProductId())) {
this.deleteItem(obj.getProductId());
return true;
}
return false;
}).collect(Collectors.toList());
return cartItemResult;
}
-
order
-
feign
@FeignClient(name = "classes-product-service")
public interface ProductFeignService {
/**
* 获取最新sp信息
*
* @param productList
* @return
*/
@PostMapping("/api/car/v1/confirm_order_cart_items")
JsonData confirmOrderCartItem(@RequestBody List<Long> productList);
}
- OrderItemVO
@Data
public class OrderItemVO {
/**
* sp id
*/
private Long productId;
/**
* 数量
*/
private Integer buyNum;
/**
* sp 标题
*/
private String productTitle;
/**
* sp 图片
*/
private String productImg;
/**
* 单价
*/
private BigDecimal amont;
/**
* 总价
*/
private BigDecimal totalAmont;
public Long getProductId() {
return productId;
}
public void setProductId(Long productId) {
this.productId = productId;
}
public Integer getBuyNum() {
return buyNum;
}
public void setBuyNum(Integer buyNum) {
this.buyNum = buyNum;
}
public String getProductTitle() {
return productTitle;
}
public void setProductTitle(String productTitle) {
this.productTitle = productTitle;
}
public String getProductImg() {
return productImg;
}
public void setProductImg(String productImg) {
this.productImg = productImg;
}
public BigDecimal getAmont() {
return amont;
}
public void setAmont(BigDecimal amont) {
this.amont = amont;
}
/**
* 总价格
*
* @return
*/
public BigDecimal getTotalAmont() {
return this.amont.multiply(new BigDecimal(this.buyNum));
}
}
- service
@Override
public JsonData confirmOrder(ConfirmOrderRequest orderRequest) {
LoginUser loginUser = LoginInterceptor.threadLocal.get();
String orderOutTradeNo = CommonUtil.getStringNumRandom(32);
//收货地址信息
ProductOrderAddressVO addressVO = this.getUserAddress(orderRequest.getAddressId());
log.info("收货地址信息:{}", addressVO);
//获取商品
List<Long> productIdList = orderRequest.getProductIdList();
//获取最新
JsonData jsonData = productFeignService.confirmOrderCartItem(productIdList);
List<OrderItemVO> orderItemList = null;
try {
String s = JSON.toJSONString(jsonData.getData());
orderItemList = JSON.parseArray(s, OrderItemVO.class);
} catch (Exception e) {
throw new BizException(BizCodeEnum.ORDER_CONFIRM_CART_ITEM_NOT_EXIST);
}
return null;
}
第3集 商品验价和优惠券的抵扣功能开发
商品验价和优惠券的抵扣功能开发
- feign
@FeignClient(name = "classes-coupon-service")
public interface CouponFeignService {
@GetMapping("/api/coupon_record/v1/detail/{record_id}")
public JsonData couponRecordDetail(@PathVariable("record_id") Long record_id);
}
- CouponRecordVO
@Data
public class CouponRecordVO {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 优惠券id
*/
private Long couponId;
/**
* 创建时间获得时间
*/
private Date createTime;
/**
* 使用状态 可用 NEW,已使用USED,过期 EXPIRED;
*/
private String useState;
/**
* 用户id
*/
private Long userId;
/**
* 用户昵称
*/
private String userName;
/**
* 优惠券标题
*/
private String couponTitle;
/**
* 开始时间
*/
private Date startTime;
/**
* 结束时间
*/
private Date endTime;
/**
* 订单id
*/
private Long orderId;
/**
* 抵扣价格
*/
private BigDecimal price;
/**
* 满多少才可以使用
*/
private BigDecimal conditionPrice;
}
- service
/**
* * 防重提交
* * 用户微服务-确认收货地址
* * 商品微服务-获取最新购物项和价格
* * 订单验价
* * 优惠券微服务-获取优惠券
* * 验证价格
* * 锁定优惠券
* * 锁定商品库存
* * 创建订单对象
* * 创建子订单对象
* * 发送延迟消息-用于自动关单
* * 创建支付信息-对接三方支付
*
* @param orderRequest
* @return
*/
@Override
public JsonData confirmOrder(ConfirmOrderRequest orderRequest) {
LoginUser loginUser = LoginInterceptor.threadLocal.get();
String orderOutTradeNo = CommonUtil.getStringNumRandom(32);
//收货地址信息
ProductOrderAddressVO addressVO = this.getUserAddress(orderRequest.getAddressId());
log.info("收货地址信息:{}", addressVO);
//获取商品
List<Long> productIdList = orderRequest.getProductIdList();
//获取最新
JsonData jsonData = productFeignService.confirmOrderCartItem(productIdList);
List<OrderItemVO> orderItemList = null;
try {
String s = JSON.toJSONString(jsonData.getData());
orderItemList = JSON.parseArray(s, OrderItemVO.class);
} catch (Exception e) {
throw new BizException(BizCodeEnum.ORDER_CONFIRM_CART_ITEM_NOT_EXIST);
}
//验证价格,减去优惠券
this.checkPrice(orderItemList, orderRequest);
return null;
}
/**
* 验证价格
* 统计全部商品价格-优惠券价格
*
* @param orderItemList
* @param orderRequest
*/
private void checkPrice(List<OrderItemVO> orderItemList, ConfirmOrderRequest orderRequest) {
//统计全部商品价格
BigDecimal realPayAmount = new BigDecimal(0);
// long count = orderItemList.stream().count();
if (orderItemList != null) {
for (OrderItemVO orderItemVO : orderItemList) {
BigDecimal itemREalPayAmount = orderItemVO.getTotalAmont();
realPayAmount.add(itemREalPayAmount);
}
}
//获取优惠券 判断是否可以使用
CouponRecordVO couponRecordVO = getCartCouponCord(orderRequest.getCouponRecordId());
//计算购物车价格,是否满足条件
if (couponRecordVO != null) {
//计算是否满足
if (realPayAmount.compareTo(couponRecordVO.getConditionPrice()) < 0) {
throw new BizException(BizCodeEnum.ORDER_CONFIRM_COUPON_FAIL);
}
if (couponRecordVO.getPrice().compareTo(realPayAmount) > 0) {
realPayAmount = BigDecimal.ZERO;
} else {
realPayAmount.subtract(couponRecordVO.getPrice());
}
}
//验价
if (realPayAmount.compareTo(orderRequest.getRealPayAmount()) != 0) {
log.error("验价失败");
throw new BizException(BizCodeEnum.ORDER_CONFIRM_PRICE_FAIL);
}
}
/**
* 获取优惠券 判断是否可以使用
*
* @param couponRecordId
* @return
*/
private CouponRecordVO getCartCouponCord(Long couponRecordId) {
if (couponRecordId == null || couponRecordId < 0) {
return null;
}
JsonData jsonData = couponFeignService.couponRecordDetail(couponRecordId);
if (jsonData.getCode() != 0) {
throw new BizException(BizCodeEnum.COUPON_NO_EXITS);
}
if (jsonData.getCode() == 0) {
CouponRecordVO couponRecordVO = JSON.parseObject(JSON.toJSONString(jsonData.getData()), CouponRecordVO.class);
if (!couponAvailable(couponRecordVO)) {
log.info("使用失败");
throw new BizException(BizCodeEnum.COUPON_CONDITION_ERROR);
}
return couponRecordVO;
}
return null;
}
/**
* 判断优惠券是否可以
*
* @param couponRecordVO
* @return
*/
private boolean couponAvailable(CouponRecordVO couponRecordVO) {
if (couponRecordVO.getUseState().equalsIgnoreCase(CouponStateEnum.NEW.name())) {
long caURRENTtimesTamp = CommonUtil.getCaURRENTtimesTamp();
long end = couponRecordVO.getEndTime().getTime();
long beg = couponRecordVO.getStartTime().getTime();
if (caURRENTtimesTamp >= beg && caURRENTtimesTamp <= end) {
return true;
}
}
return false;
}
第4集 下单锁定优惠券
下单锁定优惠券
- LockCouponRecordRequest
/**
* @author gtf
* @date 2022/11/25 16:32
*/
@Data
public class LockCouponRecordRequest {
/**
* 记录id
*/
private List<Long> lockCouponRecordIds;
/**
* 订单号
*/
private String orderOutTradeNo;
}
- service
@Override
public JsonData confirmOrder(ConfirmOrderRequest orderRequest) {
LoginUser loginUser = LoginInterceptor.threadLocal.get();
String orderOutTradeNo = CommonUtil.getStringNumRandom(32);
//收货地址信息
ProductOrderAddressVO addressVO = this.getUserAddress(orderRequest.getAddressId());
log.info("收货地址信息:{}", addressVO);
//获取商品
List<Long> productIdList = orderRequest.getProductIdList();
//获取最新
JsonData jsonData = productFeignService.confirmOrderCartItem(productIdList);
List<OrderItemVO> orderItemList = null;
try {
String s = JSON.toJSONString(jsonData.getData());
orderItemList = JSON.parseArray(s, OrderItemVO.class);
} catch (Exception e) {
throw new BizException(BizCodeEnum.ORDER_CONFIRM_CART_ITEM_NOT_EXIST);
}
//验证价格,减去优惠券
this.checkPrice(orderItemList, orderRequest);
//锁定优惠券
this.couponRecords(orderRequest, orderOutTradeNo);
return null;
}
/**
* 锁定优惠券
*
* @param orderRequest
* @param orderOutTradeNo
*/
private void couponRecords(ConfirmOrderRequest orderRequest, String orderOutTradeNo) {
ArrayList<Long> objects = new ArrayList<>();
if (orderRequest.getCouponRecordId() > 0) {
objects.add(orderRequest.getCouponRecordId());
LockCouponRecordRequest lockCouponRecordRequest = new LockCouponRecordRequest();
lockCouponRecordRequest.setLockCouponRecordIds(objects);
lockCouponRecordRequest.setOrderOutTradeNo(orderOutTradeNo);
//发起锁定请求
JsonData jsonData = couponFeignService.lockCouponRecords(lockCouponRecordRequest);
if (jsonData.getCode() != 0) {
throw new BizException(BizCodeEnum.COUPON_RECORD_LOCK_FAIL);
}
}
}
第5集 创建商品订单和订单项模块开发
创建商品订单和订单项模块开发
- CouponFeignService
/**
* @author gtf
* @date 2022/11/28 15:32
*/
@FeignClient(name = "classes-coupon-service")
public interface CouponFeignService {
@GetMapping("/api/coupon_record/v1/detail/{record_id}")
public JsonData couponRecordDetail(@PathVariable("record_id") Long record_id);
@PostMapping("/api/coupon_record/v1/lock_records")
JsonData lockCouponRecords(@RequestBody LockCouponRecordRequest lockCouponRecordRequest);
}
- ProductOrderDO
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("product_order")
public class ProductOrderDO implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 订单唯一标识
*/
private String outTradeNo;
/**
* NEW 未支付订单,PAY已经支付订单,CANCEL超时取消订单
*/
private String state;
/**
* 订单生成时间
*/
private Date createTime;
/**
* 订单总金额
*/
private BigDecimal totalAmount;
/**
* 订单实际支付价格
*/
private BigDecimal payAmount;
/**
* 支付类型,微信-银行-支付宝
*/
private String payType;
/**
* 昵称
*/
private String nickname;
/**
* 头像
*/
private String headImg;
/**
* 用户id
*/
private Long userId;
/**
* 0表示未删除,1表示已经删除
*/
private Integer del;
/**
* 更新时间
*/
private Date updateTime;
/**
* 订单类型 DAILY普通单,PROMOTION促销订单
*/
private String orderType;
/**
* 收货地址 json存储
*/
private String receiverAddress;
}
- service
@Override
public JsonData confirmOrder(ConfirmOrderRequest orderRequest) {
LoginUser loginUser = LoginInterceptor.threadLocal.get();
String orderOutTradeNo = CommonUtil.getStringNumRandom(32);
//收货地址信息
ProductOrderAddressVO addressVO = this.getUserAddress(orderRequest.getAddressId());
log.info("收货地址信息:{}", addressVO);
//获取商品
List<Long> productIdList = orderRequest.getProductIdList();
//获取最新
JsonData jsonData = productFeignService.confirmOrderCartItem(productIdList);
List<OrderItemVO> orderItemList = null;
try {
String s = JSON.toJSONString(jsonData.getData());
orderItemList = JSON.parseArray(s, OrderItemVO.class);
} catch (Exception e) {
throw new BizException(BizCodeEnum.ORDER_CONFIRM_CART_ITEM_NOT_EXIST);
}
//验证价格,减去优惠券
this.checkPrice(orderItemList, orderRequest);
//锁定优惠券
this.couponRecords(orderRequest, orderOutTradeNo);
//创建订单
ProductOrderDO productOrderDO = this.saveProductOrder(orderRequest, loginUser, orderOutTradeNo, addressVO);
//创建订单项
this.saveProductOrderItems(orderOutTradeNo, productOrderDO.getId(), orderItemList);
return null;
}
/**
* 创建订单项
*
* @param orderOutTradeNo
* @param id
* @param orderItemList
*/
private void saveProductOrderItems(String orderOutTradeNo, Long orderId, List<OrderItemVO> orderItemList) {
List<ProductOrderItemDO> collect = orderItemList.stream().map(obj -> {
ProductOrderItemDO item = new ProductOrderItemDO();
item.setBuyNum(obj.getBuyNum());
item.setCreateTime(new Date());
item.setProductId(obj.getProductId());
item.setOutTradeNo(orderOutTradeNo);
item.setProductImg(obj.getProductImg());
item.setProductName(obj.getProductTitle());
item.setAmount(obj.getAmont());
item.setTotalAmount(obj.getTotalAmont());
item.setProductOrderId(orderId);
return item;
}).collect(Collectors.toList());
productOrderItemMapper.insertBatch(collect);
}
/**
* 创建订单
*
* @param orderRequest
* @param loginUser
* @param orderOutTradeNo
* @param addressVO
*/
private ProductOrderDO saveProductOrder(ConfirmOrderRequest orderRequest, LoginUser loginUser, String orderOutTradeNo, ProductOrderAddressVO addressVO) {
ProductOrderDO productOrderDO = new ProductOrderDO();
productOrderDO.setUserId(loginUser.getId());
productOrderDO.setHeadImg(loginUser.getHeadImg());
productOrderDO.setNickname(loginUser.getName());
productOrderDO.setOutTradeNo(orderOutTradeNo);
productOrderDO.setCreateTime(new Date());
productOrderDO.setDel(0);
productOrderDO.setOrderType(ProductOrderTypeEnum.DAILY.name());
productOrderDO.setPayType(ProductOrderPayTypeEnum.valueOf(orderRequest.getPayType()).name());
productOrderDO.setPayAmount(orderRequest.getRealPayAmount());
productOrderDO.setTotalAmount(orderRequest.getTotalAmount());
productOrderDO.setState(CouponStateEnum.NEW.name());
productOrderDO.setReceiverAddress(JSON.toJSONString(addressVO));
int insert = productOrderMapper.insert(productOrderDO);
return productOrderDO;
}
- mapper
/**
* 批量插入
*
* @param orderItemDOList
*/
void insertBatch(@Param("orderItemDOList") List<ProductOrderItemDO> orderItemDOList);
<insert id="insertBatch">
insert into product_order_item
(
<include refid="Base_Column_List_No_Id">
</include>
)
values
<foreach collection="orderItemDOList" index="index" item="item" separator=",">
(
#{item.productOrderId},
#{item.outTradeNo},
#{item.productId},
#{item.productName},
#{item.productImg},
#{item.buyNum},
#{item.createTime},
#{item.totalAmount},
#{item.amount}
)
</foreach>
</insert>
第6集 订单超时未支付-发送定时关单功能
订单超时未支付-发送定时关单功能
- RabbitMQConfig
@Configuration
@Data
public class RabbitMQConfig {
/**
* 交换机
*/
@Value("${mqconfig.order_event_exchange}")
private String eventExchange;
/**
* 延迟队列
*/
@Value("${mqconfig.order_close_delay_queue}")
private String orderCloseDelayQueue;
/**
* 关单队列
*/
@Value("${mqconfig.order_close_queue}")
private String orderCloseQueue;
/**
* 进入延迟队列的路由key
*/
@Value("${mqconfig.order_close_delay_routing_key}")
private String orderCloseDelayRoutingKey;
/**
* 进入死信队列的路由key
*/
@Value("${mqconfig.order_close_routing_key}")
private String orderCloseRoutingKey;
/**
* 过期时间
*/
@Value("${mqconfig.ttl}")
private Integer ttl;
/**
* 消息转换器
* @return
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
/**
* 创建交换机 Topic类型,也可以用dirct路由
* 一般一个微服务一个交换机
* @return
*/
@Bean
public Exchange orderEventExchange(){
return new TopicExchange(eventExchange,true,false);
}
/**
* 延迟队列
*/
@Bean
public Queue orderCloseDelayQueue(){
Map<String,Object> args = new HashMap<>(3);
args.put("x-dead-letter-exchange",eventExchange);
args.put("x-dead-letter-routing-key",orderCloseRoutingKey);
args.put("x-message-ttl",ttl);
return new Queue(orderCloseDelayQueue,true,false,false,args);
}
/**
* 死信队列,普通队列,用于被监听
*/
@Bean
public Queue orderCloseQueue(){
return new Queue(orderCloseQueue,true,false,false);
}
/**
* 第一个队列,即延迟队列的绑定关系建立
* @return
*/
@Bean
public Binding orderCloseDelayBinding(){
return new Binding(orderCloseDelayQueue,Binding.DestinationType.QUEUE,eventExchange,orderCloseDelayRoutingKey,null);
}
/**
* 死信队列绑定关系建立
* @return
*/
@Bean
public Binding orderCloseBinding(){
return new Binding(orderCloseQueue,Binding.DestinationType.QUEUE,eventExchange,orderCloseRoutingKey,null);
}
}
- OrderMessage
@Data
public class OrderMessage {
private Long messageId;
private String OutTradeNo;
}
- service
@Override
public JsonData confirmOrder(ConfirmOrderRequest orderRequest) {
LoginUser loginUser = LoginInterceptor.threadLocal.get();
String orderOutTradeNo = CommonUtil.getStringNumRandom(32);
//收货地址信息
ProductOrderAddressVO addressVO = this.getUserAddress(orderRequest.getAddressId());
log.info("收货地址信息:{}", addressVO);
//获取商品
List<Long> productIdList = orderRequest.getProductIdList();
//获取最新
JsonData jsonData = productFeignService.confirmOrderCartItem(productIdList);
List<OrderItemVO> orderItemList = null;
try {
String s = JSON.toJSONString(jsonData.getData());
orderItemList = JSON.parseArray(s, OrderItemVO.class);
} catch (Exception e) {
throw new BizException(BizCodeEnum.ORDER_CONFIRM_CART_ITEM_NOT_EXIST);
}
//验证价格,减去优惠券
this.checkPrice(orderItemList, orderRequest);
//锁定优惠券
this.couponRecords(orderRequest, orderOutTradeNo);
//创建订单
ProductOrderDO productOrderDO = this.saveProductOrder(orderRequest, loginUser, orderOutTradeNo, addressVO);
//创建订单项
this.saveProductOrderItems(orderOutTradeNo, productOrderDO.getId(), orderItemList);
//发送延迟消息
OrderMessage orderMessage = new OrderMessage();
orderMessage.setOutTradeNo(orderOutTradeNo);
rabbitTemplate.convertAndSend(rabbitMQConfig.getEventExchange(), rabbitMQConfig.getOrderCloseDelayRoutingKey(), orderMessage);
//创建支付
return null;
}
第7集 订单超时未支付-监听定时关单功能
订单超时未支付-监听定时关单功能
- service
boolean closeProductOrder(OrderMessage orderMessage);
/**
* 定时关单
*
* @param orderMessage
* @return
*/
@Override
public boolean closeProductOrder(OrderMessage orderMessage) {
ProductOrderDO productOrderDO = productOrderMapper.selectOne(new QueryWrapper<ProductOrderDO>().eq("Out_trade_no", orderMessage.getOutTradeNo()));
if (productOrderDO == null) {
log.warn("订单不存在");
return true;
}
if (productOrderDO.getState().equalsIgnoreCase(ProductOrderStateEnum.PAY.name())) {
log.warn("订单已经支付");
return true;
}
//三方查询
String payResult = "";
//结果是null,就是未支付
if (StringUtils.isBlank(payResult)) {
productOrderMapper.updateOrderPayState(productOrderDO.getOutTradeNo(), ProductOrderStateEnum.CANCEL.name(), ProductOrderStateEnum.NEW.name());
return true;
} else {
// 支付成功,将订单改为已经支付
productOrderMapper.updateOrderPayState(productOrderDO.getOutTradeNo(), ProductOrderStateEnum.NEW.name(), ProductOrderStateEnum.CANCEL.name());
log.warn("支付成功,将订单改为已经支付");
return true;
}
}
-mq
@Slf4j
@Component
@RabbitListener(queues = "${mqconfig.order_close_queue}")
public class ProductOrderMQListener {
@Autowired
private ProductOrderService productOrderService;
/**
*
* 消费重复消息,幂等性保证
* @param orderMessage
* @param message
* @param channel
* @throws IOException
*/
@RabbitHandler
public void closeProductOrder(OrderMessage orderMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息:closeProductOrder:{}",orderMessage);
long msgTag = message.getMessageProperties().getDeliveryTag();
try{
boolean flag = productOrderService.closeProductOrder(orderMessage);
if(flag){
channel.basicAck(msgTag,false);
}else {
channel.basicReject(msgTag,true);
}
}catch (IOException e){
log.error("定时关单失败:",orderMessage);
channel.basicReject(msgTag,true);
}
}
}
第8集
第十二章 第三方支付平台对接-蚂蚁金服开放平台接入
第1集 常用的第三方支付和聚合支付平台介绍
常用的第三方支付和聚合支付平台介绍
-
什么是第三方支付
-
第三方支付是指具备一定实力和信誉保障的独立机构,采用与各大银行签约的方式,通过与银行支付结算系统接口对接而促成交易双方进行交易的网络支付模式
-
通俗的例子:
- 支付宝,微信支付,百度钱包,PayPal(主要是欧美国家)
- 拉卡拉(中国最大线下便民金融服务提供商)
-
优点
- 支付平台降低了政府、企业、事业单位直连银行的成本,满足了企业专注发展在线业务的收付要求。
- 使用方便。对支付者而言,他所面对的是友好的界面,不必考虑背后复杂的技术操作过程
-
缺点
- 风险问题,在电子支付流程中,资金都会在第三方支付服务商处滞留即出现所谓的资金沉淀,如缺乏有效的流动性管理,则可能存在资金安全和支付的风险
- 电子支付经营资格的认知、保护和发展问题
-
-
什么是聚合支付也叫第四方支付
- 聚合支付是相对之前的第三方支付而言的,作为对第三方支付平台服务的拓展,第三方支付是介于银行和商户之间的,而聚合支付是介于第三方支付和商户之间
- 出现的场景
- 一堆第三方支付出现,并通过大量的钱补贴线上商家使用它们的支付,导致商户收银台堆满各种,POS机器,扫码设备,商户还需要去各家支付公司申请账号,结算等
- 聚合支付产品,其实聚合的是一种支付能力(支付宝支付、微信支付、百度钱包、ApplePay……),将这些收款能力聚合在一起,统一打包提供给电商网站或一些线下商家
- 解决的问题
- 聚合支付公司提供的二维码,支付多种方式支付,不再是一种,各个公司的竞争,就是支付渠道和方式的支持
-
蚂蚁金服开放平台
-
网页移动应用开发指南
-
手机网站支付文档地址:
第2集 支付宝沙箱环境介绍和应用权限申请
简介:支付宝沙箱环境介绍和应用权限申请
- 支付宝沙箱环境介绍
蚂蚁沙箱环境 (Beta) 是协助开发者进行接口功能开发及主要功能联调的辅助环境
在开发者应用上线审核前,开发者可以根据自身需求,先在沙箱环境中了解、组合和调试各种开放接口,进行开发调试工作,从而帮助开发者在应用上线审核完成后,能更快速、更顺利的完成线上调试和验收
-
沙箱地址:https://openhome.alipay.com/platform/appDaily.htm?tab=info
-
支付接入:有时不稳定,或者一直报错等等,一般就是支付宝沙箱环境问题
Beta 测试阶段每周日中午 12 点至每周一中午 12 点为维护时间,在此时间内沙箱环境部分功能可能不可用,敬请谅解。
-
APPID: 2016101000650728
-
买家信息
买家信息
买家账号nipwys6876@sandbox.com
登录密码111111
支付密码111111
用户UID2088102179297615
用户名称沙箱环境
证件类型IDENTITY_CARD
证件账号450521194204227073
- 商家信息
商户账号tvadvd7895@sandbox.com
登录密码111111
商户PID2088102178888580
-
项目依赖包添加和样例代码
-
<!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-sdk-java --> <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.10.218.ALL</version> </dependency>
第3集 支付宝开发助手简介和非对称加密钥生成工具及代码配置
简介:支付宝开发助手简介和秘钥生成工具下载
-
支付宝开发助手简介
- 支付宝开放平台开发助手即密钥生成工具,用于对应用的客户端服务端之间的交互进行加密保护。
- 工具主要功能有生成密钥、签名、验签、格式转换、密钥匹配、智能反馈、开放社区
- https://opendocs.alipay.com/open/291/introduce
- AlipayConfig
public class AlipayConfig {
/**
* 支付宝网关地址 TODO
*/
public static final String PAY_GATEWAY = "https://openapi.alipaydev.com/gateway.do";
/**
* 支付宝 APPID TODO
*/
public static final String APPID = "2016101000650728";
/**
* 应用私钥 TODO
*/
public static final String APP_PRI_KEY = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCH7Kqqac7+FTagIciZQ1u0g7MicCZJtakItz//CyZAoFg93RAzsfQDFTjmGWqj/+3Hi9e/8Rco4KaAtSdv3FYkS4Gq2sv2AL3cclGntdr0kM3pJTyKF3mMVSxcTgcHldNgQTeFiMEyqeBlF4qEyJwKuNuAk4kLLND3hnFCQlsF6LpI4p25fa5GJSFHUDTW088EsXQsnZfsGSO1ECIJkkGsZVdnagaWaoShjRAWspgRCNbjUnLS5hTegwhnrqvMPvSVHd00wQkKnXJ6KQRAhltELtZngfpRFch8Dr+Mq+WB6APt8t9Qp58KfR5Vp1I+cecTFWvr80mi4gBX+Gu5sn15AgMBAAECggEAeqi69l/XeHiiO+1gvdYIOqUikXBNTPUPHhtoN/rSwT0xhFoqlcv/3IxZNWQ4VNjOteVfhAnHkY7xEnLnrM1UOxqcYBdkOZI/w2CyiTrV5R+LgdqlgCRg/p+aEY4kT9cH0fPoa6uWVObx5ahRyzPRhOd+xc/duuTtioGbUWTaCUDDDJVlQTw7fl6CxLROOpfj1Qb9tCOxFL3PavYmk/osTIwvbP9w0gmqQMs90XhxPQD+bSCdeS3iviZse7K/kaUihKnFPdtUb0IutPxWMWA+oV0ELdezQBH2VJYJbZwVaSuVYFjA+lfOIdUFPK76EWYvOnXCdgl8fEGi+kmZLG6WkQKBgQDGoCJ4equOf8N9pxpLzkeZaqhDWg9upOusm2XDkXFMWgGLhk//8BLppM6aWDV2J7wCNyjsNlkbmOg+D0ya7xlvIpN7gNGV707O7dozocNMrpPmKr/vIC6RLh8jnTxN8jGs/z0V692LtWis2nEn9Y4NJeDBlnoFJCy8gvjoeGTpDQKBgQCvL/Hr093fi0LGfxQww4FhsY3dWTRd9KgrH/7vb07cv9Zc0q3hS778KuTPHC5ZIlijLac7MLJfnR2y3KKMZsf++ql4aH9pBLWCVFIx33gHVetkeZE1JkaElwPI64d9iDyaWNN77HRDNWpK+ZpB4gGBCh7/HY+XqGn518q6MJCzHQKBgAzBohUcw8HeIL8EKWMu91g6Cl5w/Ua83snyHQIHaEBgE3NTh/XHBF/vDrnI6n4RJTj7M+yfvO9RzbCWqPPWYUrK7K/k4REBEo2lpvrj67gUCjmhCzyfU35NeQB/i6zx3hDfP5wVzt+3IebgDJ5lXd8oTJwCPwnvfdQJkVTUzp5NAoGBAIR8su2RlftIW2C4nHFgeYmDePFMVDE2JLQwh2FWgYKqxhf+8Kcw3KfiXJZRDrA0LGqDzTQTWOK8dMhe2cNqu5eWw/GevbSTbh25XUwAX8rUbKfY1Dsozi3Z82/Qx+/kx0hHIvFWWnq1e8RlzgZDQLXxDI4NMhoUsMjVLKjwr431AoGAc89SjaQSEoXSF6LudY11vBiyESzMIUm/ORsd7GxCpm1vtheBW5k0C3ex05H9+8HkEI50twOYEXD/wwR4Py3P/FzFs/BWfinKrCE1iy1L7liCGnwpTWQ6MxMeQvBgtlZmqcpn6zc5hY0PP4noaTPqTYULdu72b/TJGHpYH6MDsqk=";
/**
* 支付宝公钥 TODO
*/
public static final String ALIPAY_PUB_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl8oPwDr9IEhAJ7gy8mp68oyCv4qF4YZWQCL//07XitAbhjZ9nexentQXMZhowszPcbvZFbX7RrV90gREIONop3Cu229ISvZwDlOuxrS3mWMj3F93JNotd4g3lS61lxG2mCzZmMWPQ/IKeHwt1EpF559EYtyxDrePDPutxoGGYq1vTom4Tw+IoEcY/XgxYZgDO8nJuynnxW2G7rfTwCT+tcQB6y9pknoXIjV1+qkr2tBQML1QgZN1bUFrrC1AuhCF0K9K5eYNOPfH4w3dvScW9Kd+NX/J2Qa7hnQWU7JCHh/om7e2wm0ISCy1VS8Dc0BxkJCwPxWP/FFE/1vd9q0mHQIDAQAB";
/**
* 签名类型
*/
public static final String SIGN_TYPE = "RSA2";
/**
* 字符编码
*/
public static final String CHARSET = "UTF-8";
/**
* 返回参数格式
*/
public static final String FORMAT = "json";
}
- PayUrlConfig
/**
* 支付宝配置
*/
@Configuration
@Data
public class PayUrlConfig {
/**
* 支付成功页面跳转
*/
@Value("${alipay.success_return_url}")
private String alipaySuccessReturnUrl;
/**
* 支付成功,回调通知
*/
@Value("${alipay.callback_url}")
private String alipayCallbackUrl;
}
- yml
#支付宝配置
alipay:
#支付成功的跳转页面
success_return_url: https://classes.net
#支付宝通知回调接口
callback_url: https://classes.net
第4集 手机网站支付宝支付样例代码+单例设计模式应用
手机网站支付宝支付样例代码+单例设计模式应用
- 编写样例代码
- 测试参数配置使用
/**
* 测试支付方法
*/
@GetMapping("test_pay")
public void testAlipay(HttpServletResponse response) throws AlipayApiException, IOException {
HashMap<String,String> content = new HashMap<>();
//商户订单号,64个字符以内、可包含字母、数字、下划线;需保证在商户端不重复
String no = UUID.randomUUID().toString();
log.info("订单号:{}",no);
content.put("out_trade_no", no);
content.put("product_code", "FAST_INSTANT_TRADE_PAY");
//订单总金额,单位为元,精确到小数点后两位
content.put("total_amount", String.valueOf("111.99"));
//商品标题/交易标题/订单标题/订单关键字等。 注意:不可使用特殊字符,如 /,=,& 等。
content.put("subject", "杯子");
//商品描述,可空
content.put("body", "好的杯子");
// 该笔订单允许的最晚付款时间,逾期将关闭交易。取值范围:1m~15d。m-分钟,h-小时,d-天,1c-当天(1c-当天的情况下,无论交易何时创建,都在0点关闭)。 该参数数值不接受小数点, 如 1.5h,可转换为 90m。
content.put("timeout_express", "5m");
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
request.setBizContent(JSON.toJSONString(content));
request.setNotifyUrl(payUrlConfig.getAlipayCallbackUrl());
request.setReturnUrl(payUrlConfig.getAlipaySuccessReturnUrl());
AlipayTradeWapPayResponse alipayResponse = AlipayConfig.getInstance().pageExecute(request);
if(alipayResponse.isSuccess()){
System.out.println("调用成功");
String form = alipayResponse.getBody();
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write(form);
response.getWriter().flush();
response.getWriter().close();
} else {
System.out.println("调用失败");
}
}
第5集 设计模式最佳实践-第三方支付对接-工厂+策略模式
设计模式最佳实践-第三方支付对接-工厂+策略模式
- PayInfoVO
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PayInfoVO {
/**
* 订单号
*/
private String outTradeNo;
/**
* 订单总金额
*/
private BigDecimal payFee;
/**
* 支付类型 微信-支付宝-银行-其他
*/
private String payType;
/**
* 端类型 APP/H5/PC
*/
private String clientType;
/**
* 标题
*/
private String title;
/**
* 描述
*/
private String description;
/**
* 订单支付超时时间,毫秒
*/
private long orderPayTimeoutMills;
}
- 支付查询策略 PayStrategy
/**
* 支付查询策略
*/
public interface PayStrategy {
/**
* 下单
*
* @return
*/
String unifiedorder(PayInfoVO payInfoVO);
/**
* 退款
*
* @param payInfoVO
* @return
*/
default String refund(PayInfoVO payInfoVO) {
return "";
}
/**
* 查询支付是否成功
*
* @param payInfoVO
* @return
*/
default String queryPaySuccess(PayInfoVO payInfoVO) {
return "";
}
}
- 策略上下文 PayStrategyContext
/**
* 支付策略上下文
*/
public class PayStrategyContext {
private PayStrategy payStrategy;
public PayStrategyContext(PayStrategy payStrategy){
this.payStrategy = payStrategy;
}
/**
* 根据支付策略,调用不同的支付
* @param payInfoVO
* @return
*/
public String executeUnifiedorder(PayInfoVO payInfoVO){
return this.payStrategy.unifiedorder(payInfoVO);
}
/**
* 根据支付的策略,调用不同的查询订单支持状态
* @param payInfoVO
* @return
*/
public String executeQueryPaySuccess(PayInfoVO payInfoVO){
return this.payStrategy.queryPaySuccess(payInfoVO);
}
}
- 具体策略实现
@Slf4j
@Service
public class AlipayStrategy implements PayStrategy {
@Autowired
private PayUrlConfig payUrlConfig;
@Override
public String unifiedorder(PayInfoVO payInfoVO) {
return null;
}
@Override
public String refund(PayInfoVO payInfoVO) {
return null;
}
@Override
public String queryPaySuccess(PayInfoVO payInfoVO) {
return null;
}
}
@Slf4j
@Service
public class WechatPayStrategy implements PayStrategy {
@Override
public String unifiedorder(PayInfoVO payInfoVO) {
return null;
}
@Override
public String refund(PayInfoVO payInfoVO) {
return null;
}
@Override
public String queryPaySuccess(PayInfoVO payInfoVO) {
return null;
}
}
- 简单工厂类 PayFactory
@Component
@Slf4j
public class PayFactory {
@Autowired
private AlipayStrategy alipayStrategy;
@Autowired
private WechatPayStrategy wechatPayStrategy;
/**
* 创建支付,简单工程模式
*
* @param payInfoVO
* @return
*/
public String pay(PayInfoVO payInfoVO) {
String payType = payInfoVO.getPayType();
if (ProductOrderPayTypeEnum.ALIPAY.name().equalsIgnoreCase(payType)) {
//支付宝支付
PayStrategyContext payStrategyContext = new PayStrategyContext(alipayStrategy);
return payStrategyContext.executeUnifiedorder(payInfoVO);
} else if (ProductOrderPayTypeEnum.WECHAT.name().equalsIgnoreCase(payType)) {
//微信支付 暂未实现
PayStrategyContext payStrategyContext = new PayStrategyContext(wechatPayStrategy);
return payStrategyContext.executeUnifiedorder(payInfoVO);
}
return "";
}
/**
* 查询订单支付状态
* <p>
* 支付成功返回非空,其他返回空
*
* @param payInfoVO
* @return
*/
public String queryPaySuccess(PayInfoVO payInfoVO) {
String payType = payInfoVO.getPayType();
if (ProductOrderPayTypeEnum.ALIPAY.name().equalsIgnoreCase(payType)) {
//支付宝支付
PayStrategyContext payStrategyContext = new PayStrategyContext(alipayStrategy);
return payStrategyContext.executeQueryPaySuccess(payInfoVO);
} else if (ProductOrderPayTypeEnum.WECHAT.name().equalsIgnoreCase(payType)) {
//微信支付 暂未实现
PayStrategyContext payStrategyContext = new PayStrategyContext(wechatPayStrategy);
return payStrategyContext.executeQueryPaySuccess(payInfoVO);
}
return "";
}
}
第6集 策略设计模式-支付宝支付下单策略编码实战
策略设计模式-支付宝支付下单策略编码实战
@Override
public String unifiedorder(PayInfoVO payInfoVO) {
HashMap<String, String> content = new HashMap<>();
//商户订单号,64个字符以内、可包含字母、数字、下划线;需保证在商户端不重复
// String no = UUID.randomUUID().toString();
log.info("订单号:{}", payInfoVO.getOutTradeNo());
content.put("out_trade_no", payInfoVO.getOutTradeNo());
content.put("product_code", "FAST_INSTANT_TRADE_PAY");
//订单总金额,单位为元,精确到小数点后两位
content.put("total_amount", payInfoVO.getPayFee().toString());
//商品标题/交易标题/订单标题/订单关键字等。 注意:不可使用特殊字符,如 /,=,& 等。
content.put("subject", payInfoVO.getTitle());
//商品描述,可空
content.put("body", payInfoVO.getDescription());
// 该笔订单允许的最晚付款时间,逾期将关闭交易。取值范围:1m~15d。m-分钟,h-小时,d-天,1c-当天(1c-当天的情况下,无论交易何时创建,都在0点关闭)。 该参数数值不接受小数点, 如 1.5h,可转换为 90m。
double timeOut = Math.floor(payInfoVO.getOrderPayTimeoutMills() / 1000 * 60);
//二次支付,判断订单关闭
if (timeOut < 1) {
throw new BizException(BizCodeEnum.PAY_ORDER_PAY_TIMEOUT);
}
content.put("timeout_express", Double.valueOf(timeOut) + "m");
//判断支付类型
String clientType = payInfoVO.getClientType();
String form = "";
try {
//h5
if (clientType.equalsIgnoreCase(ClientType.H5.name())) {
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
request.setBizContent(JSON.toJSONString(content));
request.setNotifyUrl(payUrlConfig.getAlipayCallbackUrl());
request.setReturnUrl(payUrlConfig.getAlipaySuccessReturnUrl());
AlipayTradeWapPayResponse alipayResponse = AlipayConfig.getInstance().pageExecute(request);
if (alipayResponse.isSuccess()) {
System.out.println("调用成功");
form = alipayResponse.getBody();
} else {
System.out.println("调用失败");
log.error("h5 调用失败");
throw new BizException(BizCodeEnum.PAY_ORDER_CALLBACK_NOT_SUCCESS);
}
}
//pc
if (clientType.equalsIgnoreCase(ClientType.PC.name())) {
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setBizContent(JSON.toJSONString(content));
request.setNotifyUrl(payUrlConfig.getAlipayCallbackUrl());
request.setReturnUrl(payUrlConfig.getAlipaySuccessReturnUrl());
AlipayTradePagePayResponse alipayResponse = AlipayConfig.getInstance().pageExecute(request);
if (alipayResponse.isSuccess()) {
System.out.println("调用成功");
form = alipayResponse.getBody();
} else {
System.out.println("pc调用失败");
log.error("pc 调用失败");
throw new BizException(BizCodeEnum.PAY_ORDER_CALLBACK_NOT_SUCCESS);
}
}
//app
if (clientType.equalsIgnoreCase(ClientType.PC.name())) {
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
request.setBizContent(JSON.toJSONString(content));
request.setNotifyUrl(payUrlConfig.getAlipayCallbackUrl());
request.setReturnUrl(payUrlConfig.getAlipaySuccessReturnUrl());
AlipayTradeAppPayResponse alipayResponse = AlipayConfig.getInstance().pageExecute(request);
if (alipayResponse.isSuccess()) {
System.out.println("调用成功");
form = alipayResponse.getBody();
} else {
System.out.println("app调用失败");
log.error("app调用失败");
throw new BizException(BizCodeEnum.PAY_ORDER_CALLBACK_NOT_SUCCESS);
}
}
} catch (AlipayApiException e) {
throw new BizException(BizCodeEnum.PAY_ORDER_CALLBACK_NOT_SUCCESS);
}
return form;
}
第7集 策略设计模式-支付宝支付查询策略编码实战
策略设计模式-支付宝支付查询策略编码实战
@Override
public String queryPaySuccess(PayInfoVO payInfoVO) {
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
HashMap<String, Object> map = new HashMap<>();
map.put("out_trade_no", payInfoVO.getOutTradeNo());
request.setBizContent(JSON.toJSONString(map));
AlipayTradeQueryResponse response = null;
try {
response = AlipayConfig.getInstance().execute(request);
if (response.isSuccess()) {
System.out.println("调用成功");
log.info("查询成功", response.getBody());
return response.getTradeStatus();
} else {
System.out.println("调用失败");
log.error("查询异常", response.getBody());
}
} catch (AlipayApiException e) {
log.error("查询异常", response.getBody());
}
return null;
}
第8集 支付宝支付结果通知回调地址配置和接口开发
简介:支付结果通知回调地址配置和接口开发
- 补充支付宝手机支付文档:https://opendocs.alipay.com/open/203/105286
- 支付宝沙箱环境配置支付结果通知回调地址
- 配置文件配置支付结果通知回调地址
- controller接口开发
- controller
/**
* 支付回调通知 post方式
* @param request
* @param response
* @return
*/
@PostMapping("alipay")
public String alipayCallback(HttpServletRequest request, HttpServletResponse response){
//将异步通知中收到的所有参数存储到map中
Map<String,String> paramsMap = CommonUtil.convertRequestParamsToMap(request);
log.info("支付宝回调通知结果:{}",paramsMap);
//调用SDK验证签名
try {
boolean signVerified = AlipaySignature.rsaCheckV1(paramsMap, AlipayConfig.ALIPAY_PUB_KEY, AlipayConfig.CHARSET, AlipayConfig.SIGN_TYPE);
if(signVerified){
JsonData jsonData = productOrderService.handlerOrderCallbackMsg(ProductOrderPayTypeEnum.ALIPAY,paramsMap);
if(jsonData.getCode() == 0){
//通知结果确认成功,不然会一直通知,八次都没返回success就认为交易失败
return "success";
}
}
} catch (AlipayApiException e) {
log.info("支付宝回调验证签名失败:异常:{},参数:{}",e,paramsMap);
}
return "failure";
}
- convertRequestParamsToMap
/**
* 将request中的参数转换成Map
* @param request
* @return
*/
public static Map<String, String> convertRequestParamsToMap(HttpServletRequest request) {
Map<String, String> paramsMap = new HashMap<>(16);
Set<Map.Entry<String, String[]>> entrySet = request.getParameterMap().entrySet();
for (Map.Entry<String, String[]> entry : entrySet) {
String name = entry.getKey();
String[] values = entry.getValue();
int size = values.length;
if (size == 1) {
paramsMap.put(name, values[0]);
} else {
paramsMap.put(name, "");
}
}
return paramsMap;
}
- service
/***
* 支付通知结果更新订单状态
* @param paramsMap
* @return
*/
@Override
public JsonData handlerOrderCallbackMsg(ProductOrderPayTypeEnum payType, Map<String, String> paramsMap) {
if(payType.name().equalsIgnoreCase(ProductOrderPayTypeEnum.ALIPAY.name())){
//支付宝支付
//获取商户订单号
String outTradeNo = paramsMap.get("out_trade_no");
//交易的状态
String tradeStatus = paramsMap.get("trade_status");
if("TRADE_SUCCESS".equalsIgnoreCase(tradeStatus) || "TRADE_FINISHED".equalsIgnoreCase(tradeStatus)){
//更新订单状态
productOrderMapper.updateOrderPayState(outTradeNo,ProductOrderStateEnum.PAY.name(),ProductOrderStateEnum.NEW.name());
return JsonData.buildSuccess();
}
} else if(payType.name().equalsIgnoreCase(ProductOrderPayTypeEnum.WECHAT.name())){
//微信支付 TODO
}
return JsonData.buildResult(BizCodeEnum.PAY_ORDER_CALLBACK_NOT_SUCCESS);
}
- 下单支付功能开发
- 消费者功能完善
- 查询订单状态
第十三章 Gateway网关引入和功能测试
第1集 Gateway项目开发和配置
Gateway项目开发和配置
- pom
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--添加nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--配置中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
</dependencies>
- 启动类
@SpringBootApplication
@EnableDiscoveryClient
public class GateWayApplication {
public static void main(String[] args) {
SpringApplication.run(GateWayApplication.class, args);
}
}
- application.yml
server:
port: 8889
spring:
application:
name: api-gateway
cloud:
#注册中心地址
nacos:
discovery:
server-addr: 192.168.31.17:8848
gateway:
routes: #数组形式
- id: user-service #商品服务 路由唯一标识
uri: lb://classes-user-service #从nocas进行转发
order: 1 #优先级,数字越小优先级越高
predicates: #断言 配置哪个路径才转发,前端访问路径统一加上XXX-server,网关判断转发对应的服务,如果是回调业务记得修改
- Path=/user-server/**
filters: #过滤器,请求在传递过程中通过过滤器修改
- StripPrefix=1 #去掉第一层前缀,转发给后续的路径
- id: coupon-service #商品服务 路由唯一标识
uri: lb://classes-coupon-service #从nocas进行转发
order: 2 #优先级,数字越小优先级越高
predicates: #断言 配置哪个路径才转发,前端访问路径统一加上XXX-server,网关判断转发对应的服务,如果是回调业务记得修改
- Path=/coupon-server/**
filters: #过滤器,请求在传递过程中通过过滤器修改
- StripPrefix=1 #去掉第一层前缀,转发给后续的路径
- id: product-service #商品服务 路由唯一标识
uri: lb://classes-product-service #从nocas进行转发
order: 3 #优先级,数字越小优先级越高
predicates: #断言 配置哪个路径才转发,前端访问路径统一加上XXX-server,网关判断转发对应的服务,如果是回调业务记得修改
- Path=/product-server/**
filters: #过滤器,请求在传递过程中通过过滤器修改
- StripPrefix=1 #去掉第一层前缀,转发给后续的路径
- id: order-service #商品服务 路由唯一标识
uri: lb://classes-order-service #从nocas进行转发
order: 4 #优先级,数字越小优先级越高
predicates: #断言 配置哪个路径才转发,前端访问路径统一加上XXX-server,网关判断转发对应的服务,如果是回调业务记得修改
- Path=/order-server/**
filters: #过滤器,请求在传递过程中通过过滤器修改
- StripPrefix=1 #去掉第一层前缀,转发给后续的路径
#开启网关拉取nacos的服务
discovery:
locator:
enabled: true
#设置日志级别,ERROR/WARN/INFO/DEBUG,默认是INFO以上才显示
logging:
level:
root: INFO
com.alibaba.nacos.client.config.impl: WARN
第2集 Nacos配置中心开发和配置
简介:微服务配置中心引入和配置
- 配置中心引入-common项目添加
<!--配置中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
- 配置文件优先级讲解
- 不能使用原先的application.yml, 需要使用bootstrap.yml作为配置文件
- 配置读取优先级 bootstrap.yml > application.yml
- 配置实操
- 服务迁移配置
- 增加bootstrap.yml
spring:
application:
name: classes-order-service
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 #Nacos配置中心地址
file-extension: yaml #文件拓展格式
profiles:
active: dev
dataId组成,在 Nacos Spring Cloud 中,dataId 的完整格式如下
${prefix}-${spring.profiles.active}.${file-extension}
prefix 默认为 spring.application.name 的值
spring.profiles.active 即为当前环境对应的 profile
当 spring.profiles.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 ${prefix}.${file-extension}
file-exetension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。目前只支持 properties 和 yaml 类型。
classes-product-service-dev.yaml
第十四章 微服务配置中心引入和Sentinel限流配置
第1集 高并发下的限流神器Sentinel容器化部署
简介:Sentinel容器化部署
-
什么是Sentinel
-
阿里巴巴开源的分布式系统流控工具
-
以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性
-
丰富的应用场景:消息削峰填谷、集群流量控制、实时熔断下游不可用应用等
-
完备的实时监控:Sentinel 同时提供实时的监控功能
-
提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合
-
-
Sentinel 分为两个部分
- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo、Spring Cloud 等框架也有较好的支持。
- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
-
docker容器化部署sentinel
docker pull bladex/sentinel-dashboard:latest
- 查看镜像
docker images
- 启动Sentinel
docker run --name sentinel -d -p 8858:8858 镜像id
- 访问Sentinel(记得检查防火墙或者网络安全组)
http://公网ip:8858
# 登录密码默认sentinel/sentinel
第2集 微服务整合Sentinel项目依赖配置**
- common项目添加依赖(网关也需要添加)
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- 多个微服务接入Sentinel配置
spring:
cloud:
sentinel:
transport:
dashboard: 192.168.31.17:8858
port: 9999
#dashboard: 8858 控制台端口
#port: 9999 本地启的端口,随机选个不能被占用的,与dashboard进行数据交互,会在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互, 若被占用,则开始+1一次扫描
- 微服务注册上去后,由于Sentinel是懒加载模式,所以需要访问微服务后才会在控制台出现
第3集 网关+微服务Sentinel限流实战和问题引出
简介:微服务Sentinel限流配置实战和问题引出
-
优惠券列表接口限流配置
- 其他的根据实际情况配置
-
默认是在微服务内存中,重启后就丢失
- 解决:持久化配置到配置中心Nacos, 并落地数据库
-
流控协议不明显
- Blocked by Sentinel (flow limiting)
-
影响接口性能的点
- 代码
- CPU、内存
- 带宽、IO
第4集 服务整合Sentinel自定义降级异常数据开发实战
简介:微服务整合Sentinel自定义降级异常数据开发实战
-
异常种类
FlowException //限流异常 DegradeException //降级异常 ParamFlowException //参数限流异常 SystemBlockException //系统负载异常 AuthorityException //授权异常
-
【新版】实现BlockExceptionHandler并且重写handle方法
@Component
public class SentinelBlockException implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
JsonData jsonData=null;
if (e instanceof FlowException){
jsonData=JsonData.buildResult(BizCodeEnum.CONTROL_FLOW);
}
if (e instanceof DegradeException){
jsonData=JsonData.buildResult(BizCodeEnum.CONTROL_DEGRADE);
}
if (e instanceof AuthorityException){
jsonData=JsonData.buildResult(BizCodeEnum.CONTROL_DEGRADE);
}
CommonUtil.sendJsonMessage(httpServletResponse,jsonData);
}
}
第5集 生产环境-Sentinel流控规则持久化到nacos配置中心
生产环境-Sentinel流控规则持久化到nacos配置中心
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
- 配置持久化数据源
#流控面板ip
sentinel:
transport:
dashboard: 192.168.31.17:8858
#流控规则持久化到nacos配置中心
datasource:
ds1:
nacos:
server-addr: 1192.168.31.17:8848
data-id: ${spring.application.name}.json
group-id: DEFAULT_GROUP
data-type: json
rule-type: flow
- 配置中心配置
[
{
"resource":"/test",
"controlBehavior":0,
"count":2,
"grade":1,
"limitApp":"default",
"strategy":0
},
{
"resource":"/api/coupon/v1/page_coupon",
"controlBehavior":0,
"count":1,
"grade":1,
"limitApp":"default",
"strategy":0
}
]
- 解释
resource:资源名,
limitApp:流控针对的调用来源,若为 default 则不区分调用来源
grade:限流类型(QPS 或并发线程数),0代表根据并发数量来限流,1代表根据QPS来进行流量控制
count:限流阈值
strategy:调用关系限流策略
controlBehavior:流量控制效果(直接拒绝、Warm Up、匀速排队)
clusterMode:是否为集群模式,存在问题
第十五章 平台容器化部署Docker镜像打包
第1集 微服务Docker打包插件介绍和配置实战
简介:微服务Docker打包插件配置
-
微服务采用容器化部署->本地推送镜像到镜像仓库->Paas容器云管理平台拉取部署
-
SpringBoot打包插件配置
- 聚合工程pom添加全局变量
<docker.image.prefix>classes-cloud</docker.image.prefix>
- 每个微服务都添加依赖(服务名记得修改)
<build> <finalName>alibaba-cloud-user</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <!--需要加这个,不然打包镜像找不到启动文件--> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> <configuration> <fork>true</fork> <addResources>true</addResources> </configuration> </plugin> <plugin> <groupId>com.spotify</groupId> <artifactId>dockerfile-maven-plugin</artifactId> <version>1.4.10</version> <configuration> <repository>${docker.image.prefix}/${project.artifactId}</repository> <buildArgs> <JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE> </buildArgs> </configuration> </plugin> </plugins> </build>
第2集 Dockerfile
Dockerfile
#FROM adoptopenjdk/openjdk11:ubi
FROM adoptopenjdk/openjdk11:jre11u-nightly
VOLUME /tmp
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
-
多个微服务本地镜像打包
- 步骤一:最外层 mvn clean install
- 步骤二:去到子模块pom文件下
mvn install -Dmaven.test.skip=true dockerfile:build
-
本地运行docker镜像
docker run --name classes-coupon -d -p 9002:9002 镜像id
- 查看容器运行日志
docker logs -f 容器id
第3集 容器化部署必备Docker公有+阿里云私有镜像仓库
简介:掌握Docker仓库的知识
- 为啥要用镜像仓库
- 官方公共镜像仓库和私有镜像仓库(画图)
- 公共镜像仓库:
- 官方:https://hub.docker.com/,基于各个软件开发或者有软件提供商开发的
- 非官方:其他组织或者公司开发的镜像,供大家免费试用
- 私有镜像仓库:
- 用于存放公司内部的镜像,不提供给外部试用;
- 公共镜像仓库:
- 开通阿里云私有镜像仓库
- 登录阿里云账号访问地址:
- 初次使用会提示开通
-
阿里云镜像仓-创建仓库
-
微服镜像推送私有镜像仓测试
-
打tag + 推送
-
编写shell脚本自动化构建推送镜像仓
#登录阿里云镜像仓
docker login --username=釉釉cxy registry.cn-shenzhen.aliyuncs.com --password=classes.net168
#构建整个项目,或者单独构建common项目,避免依赖未被构建上去
cd ../classes-common
mvn install
#构建网关
cd ../classes-gateway
mvn install -Dmaven.test.skip=true dockerfile:build
docker tag classes-cloud/classes-gateway:latest registry.cn-shenzhen.aliyuncs.com/1024-cloud/api-gateway:v1.2
docker push registry.cn-shenzhen.aliyuncs.com/1024-cloud/api-gateway:v1.2
echo "网关构建推送成功"
#用户服务
cd ../classes-user-service
mvn install -Dmaven.test.skip=true dockerfile:build
docker tag classes-cloud/classes-user-service:latest registry.cn-shenzhen.aliyuncs.com/1024-cloud/classes-user-service:v1.2
docker push registry.cn-shenzhen.aliyuncs.com/1024-cloud/classes-user-service:v1.2
echo "用户服务构建推送成功"
#商品服务
cd ../classes-product-service
mvn install -Dmaven.test.skip=true dockerfile:build
docker tag classes-cloud/classes-product-service:latest registry.cn-shenzhen.aliyuncs.com/1024-cloud/classes-product-service:v1.2
docker push registry.cn-shenzhen.aliyuncs.com/1024-cloud/classes-product-service:v1.2
echo "商品服务构建推送成功"
#订单服务
cd ../classes-order-service
mvn install -Dmaven.test.skip=true dockerfile:build
docker tag classes-cloud/classes-order-service:latest registry.cn-shenzhen.aliyuncs.com/1024-cloud/classes-order-service:v1.2
docker push registry.cn-shenzhen.aliyuncs.com/1024-cloud/classes-order-service:v1.2
echo "订单服务构建推送成功"
#优惠券服务
cd ../classes-coupon-service
mvn install -Dmaven.test.skip=true dockerfile:build
docker tag classes-cloud/classes-coupon-service:latest registry.cn-shenzhen.aliyuncs.com/1024-cloud/classes-coupon-service:v1.2
docker push registry.cn-shenzhen.aliyuncs.com/1024-cloud/classes-coupon-service:v1.2
echo "优惠券服务构建推送成功"
echo "=======构建脚本执行完毕====="
第十六章 工业级PaaS容器管理平台Rancher入门到部署实战
第1集 容器编排管理平台Rancher介绍和概念讲解
简介:容器编排管理平台Rancher介绍和概念讲解
-
什么是Rancher
- 地址:https://rancher.com/
- 是一个开源的企业级PaaS容器管理平台。通过Rancher,企业再也不必自己使用一系列的开源软件去从头搭建容器服务平台
- Rancher提供了在生产环境中使用的管理Docker和Kubernetes的全栈化容器部署与管理平台
-
特性
- 基础设施编排
- 可以使用任何公有云或者私有云的Linux主机资源
- Linux主机可以是虚拟机,也可以是物理机
- Rancher仅需要主机有CPU,内存,本地磁盘和网络资源,从Rancher的角度来说,一台云厂商提供的云主机和一台自己的物理机是一样的
- 容器编排与调度
- 包含了当前全部主流的编排调度引擎,例如Docker Swarm, Kubernetes, 和Mesos
- 同一个用户可以创建Swarm或者Kubernetes集群。并且可以使用原生的Swarm或者Kubernetes工具管理应用
- 除了Swarm,Kubernetes和Mesos之外,Rancher还支持自己的Cattle容器编排调度引擎
- Cattle被广泛用于编排Rancher自己的基础设施服务以及用于Swarm集群,Kubernetes集群和Mesos集群的配置,管理与升级
- 基础设施编排
-
版本说明
- 1.x
最新1.6版本,各个组件相对稳定,使用起来便捷很多 文档:https://rancher.com/docs/rancher/v1.6/zh/
- 2.x
网络要求比较高,安装组件需要大量的下载包模块,部分组件需要 网络加速 访问海外
第2集 阿里云服务器安装Rancher机器准备和环境安装
简介:阿里云服务器安装Rancher机器准备和环境安装
- Linux环境下安装Docker
#安装并运行Docker。
yum install docker-io -y
systemctl start docker
#检查安装结果。
docker info
#启动使用Docker
systemctl start docker #运行Docker守护进程
systemctl stop docker #停止Docker守护进程
systemctl restart docker #重启Docker守护进程
#修改镜像仓库
vim /etc/docker/daemon.json
#改为下面内容,然后重启docker
{
"debug":true,"experimental":true,
"registry-mirrors":["https://pb5bklzr.mirror.aliyuncs.com","https://hub-mirror.c.163.com","https://docker.mirrors.ustc.edu.cn"]
}
#查看信息
docker info
第3集 Docker容器化部署Rancher和通信模型介绍
简介:Docker容器化部署Rancher和通信模型介绍
- 容器化部署Rancher
CentOS 7.0默认使用的是firewall作为防火墙
查看防火墙状态
firewall-cmd --state
停止firewall
systemctl stop firewalld.service
禁止firewall开机启动
systemctl disable firewalld.service
安装rancher
docker run -d --restart=unless-stopped -p 8888:8080 rancher/server
-
通信模型介绍
- Client-Server模式
- 每个Linux主机是Client端,需要连接到Rancher的Server端
-
网络安全组开放8888端口
-
控制台 右下角切换语言为中文
-
配置账号密码:系统管理-》访问控制
第4集 Rancher容器化部署分布式缓存-Redis6和密码配置
简介:Rancher容器化部署Redis分布式缓存
-
部署redis
-
配置
docker run -itd --name classes-redis -p 8000:6379 redis --requirepass 123456 -v /data/redis/data:/data
- rancher配置
redis-server --appendonly yes --requirepass 123456
第5集 Rancher容器化部署消息队列RabbitMQ
简介:Rancher容器化部署RabbitMQ消息队列
- 部署RabbitMQ
- 配置
docker run -d --hostname rabbit_host1 --name rabbitmq1 -p 15672:15672 -p 5672:5672 -e RABBITMQ_DEFAULT_USER=guest -e RABBITMQ_DEFAULT_PASS=guest --privileged=true -v /usr/local/rabbitmq/1/lib:/var/lib/rabbitmq -v /usr/local/rabbitmq/1/log:/var/log/rabbitmq rabbitmq:management
- rancher配置
镜像版本 rabbitmq:3.8.14-management
第6集 Rancher容器化部署数据库MySQL和持久化配置
简介:Rancher容器化部署数据库Mysql
-
部署数据库mysql
-
配置
docker run -p 3306:3306 --name classes_mysql \
-v /usr/local/docker/mysql/conf:/etc/mysql \
-v /usr/local/docker/mysql/logs:/var/log/mysql \
-v /usr/local/docker/mysql/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-d mysql:5.7
第7集 微服务业务数据库和Nacos持久化数据库导入
简介:Rancher容器化导入微服务数据库和Nacos持久化配置导入
-
Nacos持久化数据库建立
-
nacos数据库脚本
- 导入数据库脚本(本章本集资料里面)
- 新增一个用户 nacos/nacos
INSERT INTO `users` (`username`, `password`, `enabled`) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', 1);
-
-
微服务业务数据库导入
-
数据库还有其他的基础设施
-
一般会单独的linux用户去操作,非root,但也是个独立的用户
-
数据库脚本在本章本集资料里面,如果大家导入失败,可以用自己本地的,也可以粘贴部分建表语句一个个执行
第8集 Rancher容器化部署注册中心Nacos和数据库持久化配置
简介:Rancher容器化部署注册中心Nacos和数据库持久化配置
-
部署Nacos
-
配置
docker run -d \
-e MODE=standalone \
-e SPRING_DATASOURCE_PLATFORM=mysql \
-e MYSQL_SERVICE_HOST=192.168.0.104 \
-e MYSQL_SERVICE_PORT=3306 \
-e MYSQL_SERVICE_USER=root \
-e MYSQL_SERVICE_PASSWORD=123456 \
-e MYSQL_SERVICE_DB_NAME=nacos \
-p 8848:8848 \
--restart=always \
--name nacos \
nacos/nacos-server:latest
- 查看容器内部的日志-》控制台输出