资料下载
链接: https://pan.baidu.com/s/1hZlZ6GKImnml4dKRljYk8A 提取码: 9w4e
DevOps即Development和Operations的组合词,是一组过程、方法与系统的统称,用于促进开发应用程序或软件工程、技术运营和质量保障QA部门之间的沟通、协作与整合。
开发-测试-运维 交集 一专多能+配合提效
科普下: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 线上购买线下体验,比如美团上领取优惠券,到店消费
简介:微服务技术对比和选择,版本说明
全家桶+轻松嵌入第三方组件(Netflix 奈飞)
官网:https://spring.io/projects/spring-cloud
配套
通信方式:http restful
注册中心:eureka
配置中心:config
断路器:hystrix
网关:zuul/gateway
分布式追踪系统:sleuth+zipkin
全家桶+阿里生态多个组件组合+SpringCloud支持
官网 https://spring.io/projects/spring-cloud-alibaba
配套
通信方式:http restful
服务注册发现:Nacos
服务限流降级:Sentinel
分布配置中心:Nacos
服务网关:SpringCloud Gateway
服务之间调用:Feign、Ribbon
链路追踪:Sleuth+Zipkin
官网 https://spring.io/projects/spring-cloud-alibaba#overview
SpringCloud和AlibabaCloud组件存在很大交集,互相配合
SpringCloud很多组件是基于第三方整合,目前多个已经不更新了,比如zuul、eureka、hystrix等
AlibabaCloud 提供一站式微服务解决方法,已经和SpringCloud进行了整合,组件互相支持
我们也习惯称为 Spring Cloud Alibaba
简介:使用Maven聚合工程创建微服务架构
<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>
生成公钥
classes-user-service进行测试
<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>
<dependency>
<groupId>net.classes</groupId>
<artifactId>classes-common</artifactId>
<version>1.0-SNAPSHOT</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>
常见注解
简介:增强版ORM框架 mybatis plus介绍
背景
介绍
SpringBoot整合
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
简介:介绍Mybatis-plus-generator代码自动化生成工具
介绍
基础版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='电商-公司收发货地址表';
添加依赖
<!-- 代码自动生成依赖 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-->
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 相关代码生成完毕 ========");
}
}
简介:用户微服务数据库配置和查询个人收货地址接口
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
@SpringBootApplication
@MapperScan("net.classes.mapper")
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);
}
}
<!--swagger ui接口文档依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
</dependency>
@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();
}
简介:SwaggerUI3.0接口文档分组和Header头定义
<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();
}
@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;
}
}
简介:统一接口响应协议和响应工具类封装
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;
}
}
@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());
}
}
简介:自定义全局异常+处理器开发
/**
* 自定义业务异常
*
* @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);
}
}
}
简介:微服务项目集成Spring Boot Test单元测试
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
@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());
}
}
#配置plus打印sql日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
简介:谷歌开源kaptcha图形验证码开发
<!--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>
@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;
}
}
@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失败");
}
}
}
简介:Linux环境下docker部署+redis安装
#依次运行以下命令添加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 并配置密码
docker run -itd --name classes-redis -p 8000:6379 redis --requirepass 123456
Options | Mean |
---|---|
-i | 以交互模式运行容器,通常与 -t 同时使用; |
-t | 为容器重新分配一个伪输入终端,通常与 -i 同时使用; |
-d | 后台运行容器,并返回容器ID; |
简介:用户微服务开发图形验证码接口
spring:
application:
name: classes-user-service
redis:
host: 127.0.0.1
password:
port: 6379
<!--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;
}
/**
* 获取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;
}
<!--发送邮件-->
<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
@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());
}
}
简介:注册邮箱验证码接口开发
/**
* 支持手机号、邮箱发送验证码
* @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);
}
}
public enum SendCodeEnum {
/**
* 用户注册
*/
USER_REGISTER
}
/**
* @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);
}
}
/**
* 获取验证码随机数
*
* @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();
}
}
public class CacheKey {
/**
* %s类型
* %s接受的号码
*/
public static final String CHECK_CODE_KEY = "code:%s:%s";
}
简介:注册邮箱验证码防刷落地和整体测试
@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);
}
}
简介:分布式文件存储常见解决方案介绍
目前业界比较多这个解决方案,这边就挑选几个介绍下
是在 Apache License v2.0 下发布的对象存储服务器,学习成本低,安装运维简单,主流语言的客户端整合都有, 号称最强的对象存储文件服务器,且可以和容器化技术docker/k8s等结合,社区活跃但不够成熟,业界参考资料较少
官网:https://docs.min.io/cn/
一个开源的轻量级分布式文件系统,比较少的客户端可以整合,目前主要是C和java客户端,在一些互联网创业公司中有应用比较多,没有官方文档,社区不怎么活跃.
架构+部署结构复杂,出问题定位比较难定位,可以说是fastdfs零件的组装过程,需要去理解fastDFS的架构设计,才能够正确的安装部署
选云厂商理由
选开源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
步骤
总结
总体操作很流畅,支持单机和集群部署,多个方面都是目前比较强的,
对于有需求不能或不使用云厂商提供的存储服务,例如阿里云的oss、七牛云的对象存储等,可以通过自建minio对象存储集群的方式
简介:XSpringFileStorage整合oss集成和测试存储服务
使用:X Spring File Storage整合oss
地址:https://spring-file-storage.xuyanwu.cn/#/快速入门
添加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/ # 基础路径
-- 这里使用的是 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 ='文件记录表';
/**
* @author gtf
* @date 2022/11/22 15:41
*/
public interface FileService {
/**
* 文件上传
*
* @param file
* @return
*/
String uploadPlatform(MultipartFile file);
}
@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();
}
}
简介:用户微服务头像上传接口和SwaggerUI提效
@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, "用户头像");
简介:用户微服务注册接口介绍和业务代码编写
/**
* 校验验证码
* @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;
}
<!-- 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>
@ApiOperation("用户注册")
@PostMapping("reister")
public JsonData reister(@ApiParam("用户注册对象") @RequestBody UserReisterRequest userReisterRequest) {
return userService.reister(userReisterRequest);
}
/**
* @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();
}
简介:用户微服务登录模块开发
/**
* 登录
* @param loginRequest
* @return
*/
@PostMapping("login")
public JsonData register(@RequestBody UserLoginRequest loginRequest){
JsonData jsonData = userService.login(loginRequest);
return jsonData;
}
@Data
public class UserLoginRequest {
/**
* 邮箱
*/
private String mail;
/**
* 密码
*/
private String pwd;
}
/**
* 用户登陆
* @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);
}
}
简介:分布式应用的登录检验解决方案 JWT讲解 json web token
什么是JWT
{
id:888,
name:'小w',
expire:10000
}
funtion 加密(object, appsecret){
xxxx
return base64( token);
}
function 解密(token ,appsecret){
xxxx
//成功返回true,失败返回false
}
JWT格式组成 头部、负载、签名
关于jwt客户端存储
讲解:引入相关依赖并开发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;
}
}
}
@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);
简介:用户微服务登录拦截器开发
@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数据
*
* @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数据异常");
}
}
@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);
}
}
简介:用户微服务个人信息查询接口开发
@ApiOperation("个人信息查询")
@GetMapping("detail")
public JsonData detail(){
UserVO userVO= userService.findUserDeail();
return JsonData.buildSuccess(userVO);
}
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;
}
/**
* @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;
}
简介:用户微服务新增收货地址模块开发
@ApiOperation("新增收货地址")
@PostMapping("add")
public JsonData add(@RequestBody AddressAddRequest addressAddRequest){
int rows= addressService.add(addressAddRequest);
return JsonData.buildSuccess(rows);
}
@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;
}
/**
* 新增收获地址
* @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;
}
public enum AddressStatusEnum {
/**
* 默认收获地址
*/
DEFAULT_STATUS(1),
/**
* 非默认收货地址
*/
COMMON_STATUS(0);
private int status;
private AddressStatusEnum(int status) {
this.status = status;
}
public int getStatus() {
return status;
}
}
简介:用户微服务收货地址查找接口开发和删除地址接口
@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);
}
@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;
}
/**
* 删除收获地址
* @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;
}
/**
* 通用操作码
*/
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,"用户头像文件上传失败");
简介:用户微服务列举指定用户全部收货地址接口开发
@ApiOperation("查询索引的收获地址")
@PostMapping("list")
public JsonData findUserAllAddress() {
List<AddressVO> addressVOS = addressService.findUserAllAddress();
return JsonData.buildSuccess(addressVOS);
}
/**
* 查找用户所有收货地址
* @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;
}
简介:优惠券微服务介绍和效果体验
#优惠券表
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;
简介:Mybatis-plus-generator代码自动化生成微服务相关类 直接复制用户服务的就行
简介:Mybatis-plus-分页插件配置+优惠劵列表开发
@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;
}
}
@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);
}
@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;
}
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;
}
/**
* 分页查询优惠券
* @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;
}
}
简介:登录拦截器配置和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);
}
}
简介:C端用户领劵接口核心业务逻辑开发
<!--分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.10.1</version>
</dependency>
@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;
}
@ApiOperation("领取优惠券")
@GetMapping("/add/promotionCoupon/{couponId}")
public JsonData addPromotionCoupon(@PathVariable("couponId") long couponID){
return couponService.addCoupon(couponID, CouponCategoryEnum.PROMOTION);
}
/**
* @author gtf
* @date 2022/11/23 22:42
*/
public enum CouponStateEnum {
//使用状态 可用 NEW,已使用USED,过期 EXPIRED;
NEW,
USED,
EXPIRED;
}
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;
}
简介:微服务个人领券记录分页接口
@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);
}
/**
* @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;
}
/**
* 分野查询个人
* @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;
}
}
简介:微服务个人领券记录详情接口
@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);
}
/**
* 查看详情
* @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);
}
简介:开发用户注册拉新领劵接口
@ApiOperation("rpc-新用户注册")
@PostMapping("/new_user_coupon")
public JsonData addNewUserConpon(@RequestBody NewUserConponRequest newUserConponRequest) {
return couponService.initNewuserCoupon(newUserConponRequest);
}
@Data
public class NewUserConponRequest {
private long userId;
private String name;
}
/**
* 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();
}
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;
商品微服务+MybatisPlusGenerator代码自动生成 复制用户服务的即可
轮播图列表接口开发
@ApiOperation("轮播图列表")
@GetMapping("list")
public JsonData list() {
return JsonData.buildSuccess(bannerService.list());
}
/**
* @author gtf
* @date 2022/11/24 13:52
*/
@Data
public class BannerVO {
private Integer id;
/**
* 图片
*/
private String img;
/**
* 跳转地址
*/
private String url;
/**
* 权重
*/
private Integer weight;
}
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;
}
}
商品列表分页接口开发
@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);
}
}
/**
* @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;
}
/**
* 分页查询
* @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;
}
}
商品微服务-商品详情接口开发和拦截器配置
@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);
}
}
@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);
}
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;
}
简介:购物车redis数据结构讲解
@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));
}
}
/**
* @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;
}
}
简介:添加购物车接口开发和方法抽取
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
/**
* 购物车key是用户唯一标识
*/
public static final String CAR_KEY = "cart:%s";
@Data
public class CarItemRequest {
/**
* sp id
*/
private Long productId;
/**
* 数量
*/
private Integer buyNum;
}
@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();
}
}
/**
* 添加商品到购物车
* @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;
}
}
简介:redis乱码问题和清空购物车接口开发
@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;
}
@DeleteMapping("clear")
public JsonData clearMyCart(){
carService.clear();
return JsonData.buildSuccess();
}
/**
* 清空
*/
void clear();
@Override
public void clear() {
redisTemplate.delete(getCartKey());
}
@GetMapping("myCart")
public JsonData findMyCart() {
CartVO cartVO = carService.getMyCart();
return JsonData.buildSuccess(cartVO);
}
/**
* 根据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;
}
CartVO getMyCart();
@Override
public CartVO getMyCart() {
//获取全部购物项
List<CartItemVO> cartItemVOList = buildCartItem(false);
//封装
CartVO cartVO = new CartVO();
cartVO.setCartItems(cartItemVOList);
return cartVO;
}
简介:购物车-删除购物项和修改购物车数量接口开发
@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);
}
@PostMapping("chage")
public JsonData chageItemNum(@RequestBody CarItemRequest carItemRequest) {
carService.chageItemNum(carItemRequest);
return JsonData.buildSuccess();
}
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));
}
数据库建立
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;
简介:Mybatis-plus-generator代码自动化生成微服务相关类 拷贝用户服务的
简介:项目相关配置整合和拦截器配置
/**
* 客户端枚举类
*/
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);
}
}
简介:订单微服务-创建订单伪代码编写
@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;
}
/**
* 防重提交
* 用户微服务-确认收货地址
* 商品微服务-获取最新购物项和价格
* 订单验价
* 优惠券微服务-获取优惠券
* 验证价格
* 锁定优惠券
* 锁定商品库存
* 创建订单对象
* 创建子订单对象
* 发送延迟消息-用于自动关单
* 创建支付信息-对接三方支付
* @param confirmOrderRequest
* @return
*/
@Override
public JsonData confirmOrder(ConfirmOrderRequest confirmOrderRequest) {
return null;
}
简介:注册中心Docker容器化部署Nacos
docker pull nacos/nacos-server
docker images
docker run --env MODE=standalone --name classes-nacos -d -p 8848:8848 nacos/nacos-server
//查看日志
docker logs -f
http://ip:8848/nacos
简介:Nacos注册中心配置mysql持久化
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}
简介:微服务引入Nacos注册中心和Feign远程调用
<!--添加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
简介:拉新业务,用户微服务和优惠券微服务之间的通讯
@Data
public class NewUserCouponRequest {
private long userId;
private String name;
}
@FeignClient(name = "classes-coupon-service")
public interface CouponFeignService {
/**
* 新用户注册发放优惠券
* @param newUserCouponRequest
* @return
*/
@PostMapping("/api/coupon/v1/new_user_coupon")
JsonData addNewUserCoupon(@RequestBody NewUserCouponRequest newUserCouponRequest);
}
/**
* 用户注册 初始化福利信息 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());
}
简介:分布式事务介绍和产生原因
什么是分布式事务
事务指的就是一个操作单元,在这个操作单元中的所有操作最终要保持一致的行为,要么所有操作都成功,要么所有的操作都被撤销
分两种:
一个是本地事务:本地事物其实可以认为是数据库提供的事务机
一个是分布式事务
指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用
分布式事务需要保证这些小操作要么全部成功,要么全部失败。
本质上来说,分布式事务就是为了保证不同数据库的数据一致性
产生的原因
简介:分布式事务下数据最终一致性-BASE理论介绍
CAP 中的一致性和可用性进行一个权衡的结果,核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性, 来自 ebay 的架构师提出
简介:讲解分布式事务常见解决方案概览
简介:讲解分布式事务常见核心概讲解
前置知识
是X/Open 这个组织定义的一套分布式事务的标准,也就是定义了规范和 API 接口,由各个厂商进行具体的实现
DTP 是分布式事物处理(Distributed Transaction Processing)的简称
XA是由X/Open组织提出的分布式事务规范。
XA规范主要定义了(全局)事务管理器(TM)和(局 部)资源管理器(RM)之间的接口
主流的数据库产品都实现了XA接口,是一个双向的系统接口,在事务管理器以及多个资源管理器之间作为通信桥梁
Java Transaction API,java根据XA规范提供的事务处理标准
application, 应用程序也就是业务层,微服务等
Resource Manager,资源管理器。一般是数据库,也可以是其他资源管理器,比如消息队列,文件系统
Transaction Manager ,事务管理器、事务协调者,负责接收来自用户程序(AP)发起的 XA 事务指令,并调度和协调参与事务的所有 RM(数据库),确保事务正确完成
事务模型
在分布式系统中,每一个机器节点能够明确知道自己在进行事务操作过程中的 结果是成功还是失败,但无法直接获取到其他分布式节点的操作结果
当一个事务操作跨越多个分布式节点的时候,为了保持事务处理的 ACID 特性,
需要引入一个“协调者”(TM)来统一调度所有分布式节点的执行逻辑,这些被调度的分布式节点被称为 AP。
TM 负责调度 AP 的行为,并最终决定这些 AP 是否要把事务真正进行提交到(RM)
简介:讲解XA实现分布式事务的原理
XA协议规范-实现分布式事务的原理如下
一般习惯称为 两阶段提交协议(The two-phase commit protocol,2PC)
是XA用于在全局事务中协调多个资源的机制,MySql5.5以上开始支持
准备阶段:
事务管理器给每个参与者都发送Prepared消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。
提交阶段:
总结
简介:讲解TCC柔性事务的解决方案
含义 | 操作方法 |
---|---|
预留业务资源/数据效验 | Try |
确认执行业务操作,提交数据,不做任何业务检查,try成功,confirm必定成功,需保证幂等 | Confirm |
取消执行业务操作,回滚数据,需保证幂等,也是常说的补偿性事务 | Cancel |
简介:讲解分布式事务的解决方案之一事务消息
简介:讲解第三方支付平台和微服务之间的交互
简介:讲解分布式事务框架Seata介绍
分布式事务框架
为啥选择Seata呢
什么是Seata
是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。
经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。
2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备
分布式事务框架Seata核心组件和术语介绍
简介:讲解分布式事务框架Seata核心组件和术语
简介:讲解分布式事务框架Seata事务处理过程描述
在每个应用需要分布式事务的业务库中创建这张表,这个表的核心作用是将业务数据在更新前后的数据镜像组织成回滚日志,保存在UNDO_LOG表中,以便业务异常能随时回滚
简介:介绍Seata的AT模式流程
Seata
有四种模式: (简单了解即可,深入的话看分布式事务专题)
AT
AT模式可以应对大多数的业务场景,并且基本可以做到无业务入侵、开发者无感知
用户只需关心自己的 业务SQL. AT 模式分为两个阶段,可以认为是2PC
Seata 会拦截“业务 SQL”,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据
在业务数据更新之后,再将其保存成“after image”,最后生成行锁
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性
Seata
框架自动生成提交或者回滚二阶段提交: 因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将阶段一保存的快照数据和行锁删掉,完成数据清理即可。
二阶段回滚: 还原业务数据, 回滚方式便是用“before image”还原业务数据;
但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”
如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理
TCC
Sage
XA
简介:讲解新版本Seata分布式事务的部署和安装
基于AT模式
-- 注意此处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服务端
Linux/Mac/Windows服务器安装
TC需要存储全局事务和分支事务的记录,支持三种存储模式
问题:
简介:SpringCloudAlibaba整合分布式事务seata框架
<!-- <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);
// }
// }
//}
简介:自定义全局异常下分布式事务失效解决方案
配置了全局异常处理,所以rpc一定会有返回值, 所以在每个全局事务方法最后, 需要判断rpc是否发生异常
发生异常则抛出 RuntimeException或者子类
简介:分布式事务事务的另一种解决方案,下单商品库存的扣减
下单伪代码
* * 防重提交
* * 用户微服务-确认收货地址
* * 商品微服务-获取最新购物项和价格
* * 订单验价
* * 优惠券微服务-获取优惠券
* * 验证价格
* * 锁定优惠券
* * 锁定商品库存
* * 创建订单对象
* * 创建子订单对象
* * 发送延迟消息-用于自动关单
* * 创建支付信息-对接三方支付
核心逻辑
简介:触类旁通-抽取架构业务模型-转移到优惠券记录扣减回收
lock_state
‘锁定状态锁定LOCK-完成 FINISH-取消CANCEL’,
简介:商品库存锁定和优惠券记录锁定任务表设计
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
简介: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 间内部通信口
访问管理界面
注意事项!!!!
CentOS 7 以上默认使用的是firewall作为防火墙
查看防火墙状态
firewall-cmd --state
停止firewall
systemctl stop firewalld.service
禁止firewall开机启动
systemctl disable firewalld.service
简介:讲解RabbitMQ的的死信队列+ TTL
简介:讲解RabbitMQ的延迟队列和应用场景
简介:项目整合RabbitMQ依赖和配置
<!--引入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
简介:商品下单锁定优惠券记录模块开发
@Data
public class CouponRecordMessage {
private Long messageId;
/**
* 订单号
*/
private String outTradeNo;
/**
* 锁定id
*/
private Long taskId;
}
#自定义消息队列配置,发送锁定库存消息-》延迟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
@Data
public class LockCouponRecordRequest {
/**
* 记录id
*/
private List<Long> lockCouponRecordIds;
/**
* 订单号
*/
private String orderOutTradeNo;
}
/**
* 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);
}
}
@ApiOperation("rpc-锁定优惠券记录")
@PostMapping("lock_records")
public JsonData lockCouponRecords(@RequestBody LockCouponRecordRequest recordRequest) {
JsonData data = couponRecordService.lockCouponRecords(recordRequest);
return JsonData.buildSuccess(data);
}
@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");
}
}
简介:优惠券回收开发
优惠券解锁记录场景
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();
// }
}
}
/**
* 释放优惠券
* @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;
}
}
简介: 订单微服务-确认收货地址模块开发
/**
* 服务之间传递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空指针异常");
}
};
}
/**
* @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);
}
@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;
}
简介:订单微服务下单获取最新价格开发
-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;
}
@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);
}
@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));
}
}
@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;
}
商品验价和优惠券的抵扣功能开发
@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);
}
@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;
}
/**
* * 防重提交
* * 用户微服务-确认收货地址
* * 商品微服务-获取最新购物项和价格
* * 订单验价
* * 优惠券微服务-获取优惠券
* * 验证价格
* * 锁定优惠券
* * 锁定商品库存
* * 创建订单对象
* * 创建子订单对象
* * 发送延迟消息-用于自动关单
* * 创建支付信息-对接三方支付
*
* @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;
}
下单锁定优惠券
/**
* @author gtf
* @date 2022/11/25 16:32
*/
@Data
public class LockCouponRecordRequest {
/**
* 记录id
*/
private List<Long> lockCouponRecordIds;
/**
* 订单号
*/
private String orderOutTradeNo;
}
@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);
}
}
}
创建商品订单和订单项模块开发
/**
* @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);
}
@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;
}
@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;
}
/**
* 批量插入
*
* @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>
订单超时未支付-发送定时关单功能
@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);
}
}
@Data
public class OrderMessage {
private Long messageId;
private String OutTradeNo;
}
@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;
}
订单超时未支付-监听定时关单功能
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);
}
}
}
常用的第三方支付和聚合支付平台介绍
简介:支付宝沙箱环境介绍和应用权限申请
蚂蚁沙箱环境 (Beta) 是协助开发者进行接口功能开发及主要功能联调的辅助环境
在开发者应用上线审核前,开发者可以根据自身需求,先在沙箱环境中了解、组合和调试各种开放接口,进行开发调试工作,从而帮助开发者在应用上线审核完成后,能更快速、更顺利的完成线上调试和验收
Beta 测试阶段每周日中午 12 点至每周一中午 12 点为维护时间,在此时间内沙箱环境部分功能可能不可用,敬请谅解。
买家信息
买家账号nipwys6876@sandbox.com
登录密码111111
支付密码111111
用户UID2088102179297615
用户名称沙箱环境
证件类型IDENTITY_CARD
证件账号450521194204227073
商户账号tvadvd7895@sandbox.com
登录密码111111
商户PID2088102178888580
项目依赖包添加和样例代码
https://opendocs.alipay.com/open/54/cyz7do
<!-- 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>
简介:支付宝开发助手简介和秘钥生成工具下载
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";
}
/**
* 支付宝配置
*/
@Configuration
@Data
public class PayUrlConfig {
/**
* 支付成功页面跳转
*/
@Value("${alipay.success_return_url}")
private String alipaySuccessReturnUrl;
/**
* 支付成功,回调通知
*/
@Value("${alipay.callback_url}")
private String alipayCallbackUrl;
}
#支付宝配置
alipay:
#支付成功的跳转页面
success_return_url: https://classes.net
#支付宝通知回调接口
callback_url: https://classes.net
手机网站支付宝支付样例代码+单例设计模式应用
/**
* 测试支付方法
*/
@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("调用失败");
}
}
设计模式最佳实践-第三方支付对接-工厂+策略模式
@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;
}
/**
* 支付查询策略
*/
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 "";
}
}
/**
* 支付策略上下文
*/
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;
}
}
@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 "";
}
}
策略设计模式-支付宝支付下单策略编码实战
@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;
}
策略设计模式-支付宝支付查询策略编码实战
@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;
}
简介:支付结果通知回调地址配置和接口开发
/**
* 支付回调通知 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";
}
/**
* 将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;
}
/***
* 支付通知结果更新订单状态
* @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项目开发和配置
<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);
}
}
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
简介:微服务配置中心引入和配置
<!--配置中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
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容器化部署
docker pull bladex/sentinel-dashboard:latest
docker images
docker run --name sentinel -d -p 8858:8858 镜像id
http://公网ip:8858
# 登录密码默认sentinel/sentinel
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
spring:
cloud:
sentinel:
transport:
dashboard: 192.168.31.17:8858
port: 9999
#dashboard: 8858 控制台端口
#port: 9999 本地启的端口,随机选个不能被占用的,与dashboard进行数据交互,会在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互, 若被占用,则开始+1一次扫描
简介:微服务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);
}
}
生产环境-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打包插件配置
微服务采用容器化部署->本地推送镜像到镜像仓库->Paas容器云管理平台拉取部署
SpringBoot打包插件配置
<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>
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 install -Dmaven.test.skip=true dockerfile:build
本地运行docker镜像
docker run --name classes-coupon -d -p 9002:9002 镜像id
docker logs -f 容器id
简介:掌握Docker仓库的知识
#登录阿里云镜像仓
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 "=======构建脚本执行完毕====="
简介:容器编排管理平台Rancher介绍和概念讲解
什么是Rancher
特性
版本说明
最新1.6版本,各个组件相对稳定,使用起来便捷很多
文档:https://rancher.com/docs/rancher/v1.6/zh/
网络要求比较高,安装组件需要大量的下载包模块,部分组件需要 网络加速 访问海外
简介:服务器安装Rancher机器准备和环境安装
#安装并运行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容器化部署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
简介:Rancher容器化部署Redis分布式缓存
docker run -itd --name classes-redis -p 8000:6379 redis --requirepass 123456 -v /data/redis/data:/data
redis-server --appendonly yes --requirepass 123456
简介:Rancher容器化部署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
镜像版本 rabbitmq:3.8.14-management
简介:Rancher容器化部署数据库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
简介:Rancher容器化导入微服务数据库和Nacos持久化配置导入
Nacos持久化数据库建立
nacos数据库脚本
INSERT INTO `users` (`username`, `password`, `enabled`)
VALUES
('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', 1);
微服务业务数据库导入
数据库还有其他的基础设施
一般会单独的linux用户去操作,非root,但也是个独立的用户
数据库脚本在本章本集资料里面,如果大家导入失败,可以用自己本地的,也可以粘贴部分建表语句一个个执行
简介:Rancher容器化部署注册中心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