链接: https://pan.baidu.com/s/1X7h9IS5rS5AcZdaa2VIRpg?pwd=47u5 提取码: 47u5
--来自百度网盘超级会员v5的分享
springcloudAlibaba&rancher【后端专题】 简介:用户增长的数据分析模型AARRR
SWOT态势分析法-个人能力与技术解决方案
什么是SWOT态势分析
外部的机会正好是你的优势,赶紧利用起来
外部的机会但是你的劣势,需要改进
自身具有优势但外部存在威胁,就需要时刻思考、保持警惕
是威胁又是你的劣势,就规避并消除
总结:根据SWOT进行充分分析,然后进行取舍选择,考虑更全面(对比没用这个分析你会怎么选择)
简介 SMART衡量需求、工作的利器
#按照依赖
yum install -y yum-utils device-mapper-persistent-data lvm2
#配置yum源(比较慢,不用)
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
#配置yum源 使用国内的
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
#查看版本
yum list docker-ce --showduplicates | sort -r
#1. 安装docker
yum -y install docker-ce-20.10.10-3.el7
#2. 查看docker版本
docker -v
#3. 启动docker
systemctl start docker
#4. 查看docker 启动状态
systemctl status docker
检查安装结果。
docker info
启动使用Docker
systemctl start docker #运行Docker守护进程
systemctl stop docker #停止Docker守护进程
systemctl restart docker #重启Docker守护进程
docker ps查看容器
docker stop 容器id
修改镜像仓库
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
注意:不使用1.13.1版本,该版本在jenkins使用docker命令时会说找不到配置文件!
简介:云服务器基础设施安装之Mysql8.0+Redis6.X安装
#安装mysql8,让容器使用宿主机的时间,容器时间与宿主机时间同步
docker run \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \
-v /home/data/mysql/data:/var/lib/mysql:rw \
-v /etc/localtime:/etc/localtime:ro \
--name classes_mysql \
--restart=always \
-d mysql:8.0
#Mysql工具连接测试
#连接数配置
show variables like '%max_connections%';
set GLOBAL max_connections=5000;
set GLOBAL mysqlx_max_connections=5000;
docker run -itd --name classes-redis1 -p 6379:6379 -v /mydata/redis/data:/data redis:6.2.4 --requirepass 123456
进入容器的redis
docker exec -it 容器id redis-cli
工具测试连接
简介:云服务器基础设施安装之Nacos2.x+Mysql8配置持久化-避坑
Nacos持久化SQL数据脚本
Nacos2.x安装(生产环境让运维人员配置网络,不暴露公网)
开源版本的 Nacos server 配置中,不会对客户端鉴权,即任何能访问 Nacos server 的用户,都可以直接获取 Nacos 中存储的配置,假如一个黑客攻进了企业内网,就能获取所有的业务配置,这样肯定会有安全隐患。
比如请求
http://124.221.200.246:8848/nacos/v1/cs/configs?dataId=dcloud-account-service-dev.yaml&group=DEFAULT_GROUP
需要先开启 Nacos server 的鉴权,在 Nacos server 上修改 application.properties 中的 nacos.core.auth.enabled 值为 true 即可
docker run -d \
-e NACOS_AUTH_ENABLE=true \
-e MODE=standalone \
-e JVM_XMS=128m \
-e JVM_XMX=128m \
-e JVM_XMN=128m \
-p 8848:8848 \
-e SPRING_DATASOURCE_PLATFORM=mysql \
-e MYSQL_SERVICE_HOST=124.221.200.246 \
-e MYSQL_SERVICE_PORT=3306 \
-e MYSQL_SERVICE_USER=root \
-e MYSQL_SERVICE_PASSWORD=123456 \
-e MYSQL_SERVICE_DB_NAME=nacos_config \
-e MYSQL_SERVICE_DB_PARAM='characterEncoding=utf8&connectTimeout=10000&socketTimeout=30000&autoReconnect=true&useSSL=false' \
--restart=always \
--privileged=true \
-v /home/data/nacos/logs:/home/nacos/logs \
--name classes_nacos_auth \
nacos/nacos-server:2.0.2
简介:云服务器基础设施安装之RabbitMQ安装
docker run -d --name rabbit_mq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=123456 -p 15672:15672 -p 5672:5672 rabbitmq:3.8.15-management
#网络安全组记得开放端口
4369 erlang 发现口
5672 client 端通信口
15672 管理界面 ui 端口
25672 server 间内部通信口
访问管理界面
ip:15672
<properties>
<!--JDK版本,如果是jdk8则这里是 1.8-->
<java.version>11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<spring.boot.version>2.5.5</spring.boot.version>
<spring.cloud.version>2020.0.4</spring.cloud.version>
<alibaba.cloud.version>2021.1</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>
<xxl-job.version>2.3.0</xxl-job.version>
<aliyun.oss.version>3.10.2</aliyun.oss.version>
<captcha.version>1.1.0</captcha.version>
<docker.image.prefix>dcloud</docker.image.prefix>
<redission.version>3.10.1</redission.version>
<jwt.version>0.7.0</jwt.version>
<sharding-jdbc.version>4.1.1</sharding-jdbc.version>
<!--跳过单元测试-->
<skipTests>true</skipTests>
<junit.version>4.12</junit.version>
<druid.version>1.1.16</druid.version>
</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>
<!--验证码kaptcha依赖包-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>kaptcha-spring-boot-starter</artifactId>
<version>${captcha.version}</version>
</dependency>
<!--阿里云oss-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>${aliyun.oss.version}</version>
</dependency>
<!-- JWT相关 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<!--分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>${redission.version}</version>
</dependency>
<!--https://mvnrepository.com/artifact/org.apache.shardingsphere/sharding-jdbc-spring-boot-starter-->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>${sharding-jdbc.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.xuxueli/xxl-job-core -->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>${xxl-job.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.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.1</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<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>
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
.DS_Store
.idea
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
简介:短链平台dcloud-common通用模块配置
<dependencies>
<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>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!--单元测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--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>
<!-- 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>
<!-- JWT相关 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<!--redisson分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
</dependency>
<!--Hoxton.M2版本之后不再使用Ribbon而是使用spring-cloud-loadbalancer,所以不引入spring-cloud-loadbalancer会报错,所以加入spring-cloud-loadbalancer依赖 并且在nacos中排除ribbon依赖,不然loadbalancer无效 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--配置中心, 留坑,后续用的时候再讲-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--Feign远程调用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--限流依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--限流持久化到nacos-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--Springboot项目整合spring-kafka依赖包配置-->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<!--引入AMQP-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--spring cache依赖包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.xuxueli/xxl-job-core -->
<!--分布式调度-->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<!-- 代码自动生成依赖 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-->
</dependencies>
简介:统一接口响应协议和响应工具类封装
public enum BizCodeEnum {
/**
* 短链分组
*/
GROUP_REPEAT(23001,"分组名重复"),
GROUP_OPER_FAIL(23503,"分组名操作失败"),
GROUP_NOT_EXIST(23404,"分组不存在"),
/**
*验证码
*/
CODE_TO_ERROR(240001,"接收号码不合规"),
CODE_LIMITED(240002,"验证码发送过快"),
CODE_ERROR(240003,"验证码错误"),
CODE_CAPTCHA_ERROR(240101,"图形验证码错误"),
/**
* 账号
*/
ACCOUNT_REPEAT(250001,"账号已经存在"),
ACCOUNT_UNREGISTER(250002,"账号不存在"),
ACCOUNT_PWD_ERROR(250003,"账号或者密码错误"),
ACCOUNT_UNLOGIN(250004,"账号未登录"),
/**
* 短链
*/
SHORT_LINK_NOT_EXIST(260404,"短链不存在"),
/**
* 订单
*/
ORDER_CONFIRM_PRICE_FAIL(280002,"创建订单-验价失败"),
ORDER_CONFIRM_REPEAT(280008,"订单恶意-重复提交"),
ORDER_CONFIRM_TOKEN_EQUAL_FAIL(280009,"订单令牌缺少"),
ORDER_CONFIRM_NOT_EXIST(280010,"订单不存在"),
/**
* 支付
*/
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,"认证控制"),
/**
* 流量包操作
*/
TRAFFIC_FREE_NOT_EXIST(600101,"免费流量包不存在,联系客服"),
TRAFFIC_REDUCE_FAIL(600102,"流量不足,扣减失败"),
TRAFFIC_EXCEPTION(600103,"流量包数据异常,用户无流量包"),
/**
* 通用操作码
*/
OPS_REPEAT(110001,"重复操作"),
OPS_NETWORK_ADDRESS_ERROR(110002,"网络地址错误"),
/**
* 文件相关
*/
FILE_UPLOAD_USER_IMG_FAIL(700101,"用户头像文件上传失败");
@Getter
private String message;
@Getter
private int code;
private BizCodeEnum(int code, String message){
this.code = code;
this.message = message;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class JsonData {
/**
* 状态码 0 表示成功
*/
private Integer code;
/**
* 数据
*/
private Object data;
/**
* 描述
*/
private String msg;
/**
* 获取远程调用数据
* 注意事项:
* 支持多单词下划线专驼峰(序列化和反序列化)
*
*
* @param typeReference
* @param <T>
* @return
*/
public <T> T getData(TypeReference<T> typeReference){
return JSON.parseObject(JSON.toJSONString(data),typeReference);
}
/**
* 成功,不传入数据
* @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.getMessage());
}
}
简介:自定义全局异常+处理器开发
/**
* 全局异常处理
*/
@Data
public class BizException extends RuntimeException {
private Integer code;
private String msg;
public BizException(Integer code, String message) {
super(message);
this.code = code;
this.msg = message;
}
public BizException(BizCodeEnum bizCodeEnum) {
super(bizCodeEnum.getMsg());
this.code = bizCodeEnum.getCode();
this.msg = bizCodeEnum.getMsg();
}
}
@ControllerAdvice
@Slf4j
public class ExceptionHandle {
@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);
return JsonData.buildError("全局异常,未知错误");
}
}
}
简介:common通用工具和时间格式化工具类讲解
public class TimeUtil {
/**
* 默认日期格式
*/
private static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss";
/**
* 默认日期格式
*/
private static final DateTimeFormatter DEFAULT_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_PATTERN);
private static final ZoneId DEFAULT_ZONE_ID = ZoneId.systemDefault();
/**
* LocalDateTime 转 字符串,指定日期格式
* @param time
* @param pattern
* @return
*/
public static String format(LocalDateTime localDateTime,String pattern){
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
String timeStr = formatter.format(localDateTime.atZone(DEFAULT_ZONE_ID));
return timeStr;
}
/**
* Date 转 字符串, 指定日期格式
* @param time
* @param pattern
* @return
*/
public static String format(Date time,String pattern){
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
String timeStr = formatter.format(time.toInstant().atZone(DEFAULT_ZONE_ID));
return timeStr;
}
/**
* Date 转 字符串,默认日期格式
* @param time
* @return
*/
public static String format(Date time){
String timeStr = DEFAULT_DATE_TIME_FORMATTER.format(time.toInstant().atZone(DEFAULT_ZONE_ID));
return timeStr;
}
/**
* timestamp 转 字符串,默认日期格式
*
* @param time
* @return
*/
public static String format(long timestamp) {
String timeStr = DEFAULT_DATE_TIME_FORMATTER.format(new Date(timestamp).toInstant().atZone(DEFAULT_ZONE_ID));
return timeStr;
}
/**
* 字符串 转 Date
*
* @param time
* @return
*/
public static Date strToDate(String time) {
LocalDateTime localDateTime = LocalDateTime.parse(time, DEFAULT_DATE_TIME_FORMATTER);
return Date.from(localDateTime.atZone(DEFAULT_ZONE_ID).toInstant());
}
/**
* 获取当天剩余的秒数,用于流量包过期配置
* @param currentDate
* @return
*/
public static Integer getRemainSecondsOneDay(Date currentDate) {
LocalDateTime midnight = LocalDateTime.ofInstant(currentDate.toInstant(),
ZoneId.systemDefault()).plusDays(1).withHour(0).withMinute(0)
.withSecond(0).withNano(0);
LocalDateTime currentDateTime = LocalDateTime.ofInstant(currentDate.toInstant(),
ZoneId.systemDefault());
long seconds = ChronoUnit.SECONDS.between(currentDateTime, midnight);
return (int) seconds;
}
}
public class JsonUtil {
private static final ObjectMapper mapper = new ObjectMapper();
static {
//设置可用单引号
mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
//序列化的时候序列对象的所有属性
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
//反序列化的时候如果多了其他属性,不抛出异常
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//如果是空对象的时候,不抛异常
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
//取消时间的转化格式,默认是时间戳,可以取消,同时需要设置要表现的时间格式
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
/**
* 对象转为Json字符串
* @param data
* @return
*/
public static String obj2Json(Object obj) {
String jsonStr = null;
try {
jsonStr = mapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return jsonStr;
}
/**
* json字符串转为对象
* @param str
* @param valueType
* @return
*/
public static <T> T json2Obj(String jsonStr, Class<T> beanType) {
T obj = null;
try {
obj = mapper.readValue(jsonStr, beanType);
} catch (Exception e){
e.printStackTrace();
}
return obj;
}
/**
* json数据转换成pojo对象list
* @param jsonData
* @param beanType
* @return
*/
public static <T> List<T> json2List(String jsonData, Class<T> beanType) {
JavaType javaType = mapper.getTypeFactory().constructParametricType(List.class, beanType);
try {
List<T> list = mapper.readValue(jsonData, javaType);
return list;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 对象转为byte数组
* @param data
* @return
*/
public static byte[] obj2Bytes(Object obj) {
byte[] byteArr = null;
try {
byteArr = mapper.writeValueAsBytes(obj);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return byteArr;
}
/**
* byte数组转为对象
* @param byteArr
* @param valueType
* @return
*/
public static <T> T bytes2Obj(byte[] byteArr, Class<T> beanType) {
T obj = null;
try {
obj = mapper.readValue(byteArr, beanType);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
}
@Slf4j
public class CommonUtil {
/**
* 获取ip
*
* @param request
* @return
*/
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) {
// "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress = "";
}
return ipAddress;
}
/**
* 获取全部请求头
* @param request
* @return
*/
public static Map<String, String> getAllRequestHeader(HttpServletRequest request){
Enumeration<String> headerNames = request.getHeaderNames();
Map<String, String> map = new HashMap<>();
while (headerNames.hasMoreElements()) {
String key = (String)headerNames.nextElement();
//根据名称获取请求头的值
String value = request.getHeader(key);
map.put(key,value);
}
return map;
}
/**
* MD5加密
*
* @param data
* @return
*/
public static String MD5(String data) {
try {
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;
}
/**
* 获取验证码随机数
*
* @param length
* @return
*/
public static String getRandomCode(int length) {
String sources = "0123456789";
Random random = new Random();
StringBuilder sb = new StringBuilder();
for (int j = 0; j < length; j++) {
sb.append(sources.charAt(random.nextInt(9)));
}
return sb.toString();
}
/**
* 获取当前时间戳
*
* @return
*/
public static long getCurrentTimestamp() {
return System.currentTimeMillis();
}
/**
* 生成uuid
*
* @return
*/
public static String generateUUID() {
return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
}
/**
* 获取随机长度的串
*
* @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();
}
/**
* 响应json数据给前端
*
* @param response
* @param obj
*/
public static void sendJsonMessage(HttpServletResponse response, Object obj) {
response.setContentType("application/json; charset=utf-8");
try (PrintWriter writer = response.getWriter()) {
writer.print(JsonUtil.obj2Json(obj));
response.flushBuffer();
} catch (IOException e) {
log.warn("响应json数据给前端异常:{}",e);
}
}
/**
* 响应html
*
* @param response
* @param jsonData
*/
private static void writeData(HttpServletResponse response, JsonData jsonData) {
try {
response.setContentType("text/html;charset=UTF8");
response.getWriter().write(jsonData.getData().toString());
response.getWriter().flush();
response.getWriter().close();
} catch (IOException e) {
log.error("写出Html异常:{}", e);
}
}
}
账号微服务和流量包数据库表
CREATE TABLE `account` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`account_no` bigint DEFAULT NULL,
`head_img` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '头像',
`phone` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '手机号',
`pwd` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码',
`secret` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '盐,用于个人敏感信息处理',
`mail` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '邮箱',
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户名',
`auth` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '认证级别,DEFAULT,REALNAME,ENTERPRISE,访问次数不一样',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_phone` (`phone`) USING BTREE,
UNIQUE KEY `uk_account` (`account_no`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE `traffic` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`day_limit` int DEFAULT NULL COMMENT '每天限制多少条,短链',
`day_used` int DEFAULT NULL COMMENT '当天用了多少条,短链',
`total_limit` int DEFAULT NULL COMMENT '总次数,活码才用',
`account_no` bigint DEFAULT NULL COMMENT '账号',
`out_trade_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '订单号',
`level` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '产品层级:FIRST青铜、SECOND黄金、THIRD钻石',
`expired_date` date DEFAULT NULL COMMENT '过期日期',
`plugin_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '插件类型',
`product_id` bigint DEFAULT NULL COMMENT '商品主键',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_trade_no` (`out_trade_no`,`account_no`) USING BTREE,
KEY `idx_account_no` (`account_no`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE `traffic_task` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`account_no` bigint DEFAULT NULL,
`traffic_id` bigint DEFAULT NULL,
`use_times` int DEFAULT NULL,
`lock_state` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '锁定状态锁定LOCK 完成FINISH-取消CANCEL',
`message_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一标识',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_msg_id` (`message_id`) USING BTREE,
KEY `idx_release` (`account_no`,`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
简介:介绍Mybatis-plus-generator代码自动化生成工具
<!-- 代码自动生成依赖 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("gtf")
// 生成路径,最好使用绝对路径,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://124.221.200.246:3306/dcloud_account?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai")
.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("account","traffic","traffic_task");
//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("相关代码生成完毕");
}
}
简介:账号微服务注册Nacos+配置文件增加
启动账号微服务
<dependency>
<groupId>net.classes</groupId>
<artifactId>dcloud-common</artifactId>
<version>1.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
@MapperScan("net.classes.mapper")
@EnableTransactionManagement
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
@EnableAsync
public class AccountApplication {
public static void main(String[] args) {
SpringApplication.run(AccountApplication.class, args);
}
}
server:
port: 8001
spring:
application:
name: dcloud-account
cloud:
nacos:
discovery:
server-addr: 124.221.200.246:8848
username: nacos
password: nacos
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://124.221.200.246:3306/dcloud_account?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
#配置plus打印sql日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
简介:账号微服务短信验证码发送工具类封装实战
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.6.0</version>
</dependency>
#----------alisms短信配置--------------
aliyun:
sms:
access-keyId: 123
access-key-secret: 2313
template-id: 123
sign-name: 123
/**
* @author gtf
* @date 2022/12/2 15:05
*/
@ConfigurationProperties(prefix = "aliyun.sms")
@Configuration
@Data
public class SmsConfig {
private String accessKeyId;
private String accessKeySecret;
private String templateId;
private String signName;
}
@Component
public class SmsComponent {
@Autowired
private SmsConfig smsConfig;
@Async
public void send(String to, String templateId, String value) throws ExecutionException, InterruptedException {
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", smsConfig.getAccessKeyId(), smsConfig.getAccessKeySecret());
/** use STS Token
DefaultProfile profile = DefaultProfile.getProfile(
"<your-region-id>", // The region ID
"<your-access-key-id>", // The AccessKey ID of the RAM account
"<your-access-key-secret>", // The AccessKey Secret of the RAM account
"<your-sts-token>"); // STS Token
**/
IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setPhoneNumbers(to);
request.setSignName(smsConfig.getSignName());
request.setTemplateCode(templateId);
Map<String, Object> objectMap = new HashMap<>();
objectMap.put("code",value);
request.setTemplateParam(JSON.toJSONString(objectMap));
try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println(new Gson().toJson(response));
} catch (ServerException e) {
e.printStackTrace();
} catch (ClientException e) {
System.out.println("ErrCode:" + e.getErrCode());
System.out.println("ErrMsg:" + e.getErrMsg());
System.out.println("RequestId:" + e.getRequestId());
}
}
}
简介:高并发下异步请求解决方案一- @Async组件应用实战
@Configuration
@EnableAsync
public class ThreadPoolTaskConfig {
@Bean("threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
//线程池创建的核心线程数,线程池维护线程的最少数量,即使没有任务需要执行,也会一直存活
//如果设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
threadPoolTaskExecutor.setCorePoolSize(16);
//最大线程池数量,当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
//当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
threadPoolTaskExecutor.setMaxPoolSize(64);
//缓存队列(阻塞队列)当核心线程数达到最大时,新任务会放在队列中排队等待执行
threadPoolTaskExecutor.setQueueCapacity(124);
//当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
//允许线程空闲时间60秒,当maxPoolSize的线程在空闲时间到达的时候销毁
//如果allowCoreThreadTimeout=true,则会直到线程数量=0
threadPoolTaskExecutor.setKeepAliveSeconds(30);
//spring 提供的 ThreadPoolTaskExecutor 线程池,是有setThreadNamePrefix() 方法的。
//jdk 提供的ThreadPoolExecutor 线程池是没有 setThreadNamePrefix() 方法的
threadPoolTaskExecutor.setThreadNamePrefix("自带Async前缀:");
threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CallerRunsPolicy():交由调用方线程运行,比如 main 线程;如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行
//AbortPolicy():该策略是线程池的默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
//DiscardPolicy():如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常
//DiscardOldestPolicy():丢弃队列中最老的任务,队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
}
//使用实战, 启动类可以不加@EnableAsync,改上面加
@Async("threadPoolTaskExecutor")
简介:ThreadPoolTaskExecutor线程池的面试题你知道怎么回答不
简介:谷歌开源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;
}
}
@GetMapping("captcha")
public void getCaptcha(HttpServletResponse response, HttpServletRequest request) {
//获取验证码内容
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失败");
}
}
简介:池化思想应用-Redis6.X配置连接池实战
连接池好处
连接池配置 common项目
<!--redis客户端-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
配置Redis连接
redis:
client-type: jedis
host: 124.221.200.246
password: 123456
port: 6379
jedis:
pool:
# 连接池最大连接数(使用负值表示没有限制)
max-active: 100
# 连接池中的最大空闲连接
max-idle: 100
# 连接池中的最小空闲连接
min-idle: 100
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: 60000
序列化配置
@Configuration
public class RedisTemplateConfiguration {
/**
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置key和value的序列化规则
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// 设置hashKey和hashValue的序列化规则
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}
简介:账号微服务开发图形验证码接口+Try-with-resource知识巩固
@Autowired
private Producer captchaProducer;
@Autowired
private StringRedisTemplate redisTemplate;
/**
*临时使用10分钟有效,方便测试
*/
private static final long CAPTCHA_CODE_EXPIRED = 60 * 1000 * 10;
/**
* 获取图形验证码
* @param request
* @param response
*/
@GetMapping("captcha")
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) {
String captchaText = captchaProducer.createText();
log.info("图形验证码:{}", captchaText);
//存储
redisTemplate.opsForValue().set(getCaptchaKey(request),
captchaText, CAPTCHA_CODE_EXPIRED, TimeUnit.MILLISECONDS);
BufferedImage bufferedImage = captchaProducer.createImage(captchaText);
try (ServletOutputStream outputStream = response.getOutputStream()){
ImageIO.write(bufferedImage, "jpg", outputStream);
outputStream.flush();
} catch (IOException e) {
log.error("获取图形验证码异常:{}", e);
}
}
/**
* 拼接key
*
* @param request
* @return
*/
public String getCaptchaKey(HttpServletRequest request) {
String ipAddr = CommonUtil.getIpAddr(request);
String userAgent = request.getHeader("User-Agent");
String key = "account-service:captcha" + CommonUtil.MD5(ipAddr + userAgent);
return key;
}
public class RedisKey {
// public static final Locale CHECK_CODE_KEY = ;
/**
* 第一个类型
* 第二个唯一标识
*/
public static final String CHECK_CODE_KEY = "code:%s:%s";
}
@PostMapping("send_code")
public JsonData sendCode(@RequestBody SendCodeRequest sendCodeRequest, HttpServletRequest request) throws ExecutionException, InterruptedException {
String captchaKey = getCaptchaKey(request);
String code = redisTemplate.opsForValue().get(captchaKey);
String captcha = sendCodeRequest.getCaptcha();
if (code != null && captcha != null && captcha.equalsIgnoreCase(code)) {
redisTemplate.delete(captchaKey);
JsonData jsonData = notifyService.sendCode(SendCodeEnum.USER_REGISER, sendCodeRequest.getTo());
return JsonData.buildSuccess(jsonData);
} else {
return JsonData.buildResult(BizCodeEnum.CODE_CAPTCHA_ERROR);
}
}
public interface NotifyService {
/**
* 发送验证码
* @param userRegiser
* @param to
* @return
*/
JsonData sendCode(SendCodeEnum userRegiser, String to) throws ExecutionException, InterruptedException;
}
private static final long CODE_EXPIRED = 1000 * 60 * 10;
@Override
public JsonData sendCode(SendCodeEnum sendCodeEnum, String to) throws ExecutionException, InterruptedException {
String cacheKey = String.format(RedisKey.CHECK_CODE_KEY, sendCodeEnum.name(), to);
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
//如果不为空,再判断是否是60秒内重复发送 0122_232131321314132
if (StringUtils.isNotBlank(cacheValue)) {
long ttl = Long.parseLong(cacheKey.split("_")[1]);
//当前时间戳-验证码发送的时间戳,如果小于60秒,则不给重复发送
long leftTime = CommonUtil.getCurrentTimestamp() - ttl;
if (leftTime < (1000 * 60)) {
log.info("重复发送短信验证码,时间间隔:{}秒", leftTime);
return JsonData.buildResult(BizCodeEnum.CODE_LIMITED);
}
}
String code = CommonUtil.getRandomCode(6);
//生成拼接好验证码
String value = code + "_" + CommonUtil.getCurrentTimestamp();
redisTemplate.opsForValue().set(cacheKey, value, CODE_EXPIRED, TimeUnit.MILLISECONDS);
if (CheckUtil.isEmail(to)) {
//发送邮箱验证码 TODO
} else if (CheckUtil.isPhone(to)) {
//发送手机验证码
log.info("code:{}", code);
smsComponent.send(to, smsConfig.getTemplateId(), code);
}
return JsonData.buildSuccess();
}
简介: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);
}
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Dict;
import cn.xuyanwu.spring.file.storage.FileInfo;
import cn.xuyanwu.spring.file.storage.FileStorageService;
import cn.xuyanwu.spring.file.storage.recorder.FileRecorder;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import net.classes.mapper.FileDetailMapper;
import net.classes.model.FileDetailDO;
import net.classes.service.FileService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.util.Date;
/**
* @author gtf
* @date 2022/11/22 15:42
*/
@Service
@Slf4j
public class FileStorageServiceImpl extends ServiceImpl<FileDetailMapper, FileDetailDO> implements FileRecorder, FileService {
@Autowired(required = false)
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();
}
}
简介: 账号微服务头像上传接口和PostMan测试
/**
* 上传用户头像
*
* 默认文件大小 1M,超过会报错
*
* @param file
* @return
*/
@PostMapping(value = "upload")
public JsonData uploadHeaderImg(@RequestPart("file") MultipartFile file){
String result = fileService.uploadPlatform(file);
return result != null?JsonData.buildSuccess(result):JsonData.buildResult(BizCodeEnum.FILE_UPLOAD_USER_IMG_FAIL);
}
简介:账号微服务注册接口介绍和业务代码编写
@Data
public class AccountRegisterRequest {
/**
* 头像
*/
private String headImg;
/**
* 手机号
*/
private String phone;
/**
* 密码
*/
private String pwd;
/**
* 邮箱
*/
private String mail;
/**
* 用户名
*/
private String username;
/**
* 短信验证码
*/
private String code;
}
/**
* 用户注册
* @param registerRequest
* @return
*/
@PostMapping("register")
public JsonData register(@RequestBody AccountRegisterRequest registerRequest){
JsonData jsonData = accountService.register(registerRequest);
return jsonData;
}
public enum AuthTypeEnum {
/**
* 默认级别
*/
DEFAULT,
/**
* 实名制
*/
REALNAME,
/**
* 企业
*/
ENTERPRISE;
}
/**
* 验证码校验
* @param userRegister
* @param phone
* @param code
* @return
*/
boolean checkCode(SendCodeEnum userRegister, String phone, String code);
/**
* 校验验证码
*
* @param userRegister
* @param phone
* @param code
* @return
*/
@Override
public boolean checkCode(SendCodeEnum userRegister, String phone, String code) {
String cacheKey = String.format(RedisKey.CHECK_CODE_KEY, userRegister.name(), phone);
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
if (StringUtils.isNotBlank(cacheValue)) {
String cacheCode = cacheValue.split("_")[0];
if (cacheCode.equalsIgnoreCase(code)) {
return true;
}
}
return false;
}
/**
* zhuce
* @param registerRequest
* @return
*/
JsonData register(AccountRegisterRequest registerRequest);
/**
* @author gtf
* @date 2022/12/2 11:20
*/
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Autowired
private NotifyService notifyService;
@Autowired
private AccountManager accountManager;
/**
* 手机验证码验证
* 密码加密(TODO)
* 账号唯一性检查(TODO)
* 插入数据库
* 新注册用户福利发放(TODO)
*
* @param registerRequest
* @return
*/
@Override
public JsonData register(AccountRegisterRequest registerRequest) {
boolean checkCode =false;
//判断验证码
if(StringUtils.isNotBlank(registerRequest.getPhone())){
checkCode = notifyService.checkCode(SendCodeEnum.USER_REGISTER,registerRequest.getPhone(),registerRequest.getCode());
}
//验证码错误
if(!checkCode){
return JsonData.buildResult(BizCodeEnum.CODE_ERROR);
}
AccountDO accountDO = new AccountDO();
BeanUtils.copyProperties(registerRequest,accountDO);
//认证级别
accountDO.setAuth(AuthTypeEnum.DEFAULT.name());
//生成唯一的账号 TODO
accountDO.setAccountNo(CommonUtil.getCurrentTimestamp());
//设置密码 秘钥 盐
accountDO.setSecret("$1$"+CommonUtil.getStringNumRandom(8));
String cryptPwd = Md5Crypt.md5Crypt(registerRequest.getPwd().getBytes(),accountDO.getSecret());
accountDO.setPwd(cryptPwd);
int rows = accountManager.insert(accountDO);
log.info("rows:{},注册成功:{}",rows,accountDO);
//用户注册成功,发放福利 TODO
userRegisterInitTask(accountDO);
return JsonData.buildSuccess();
}
/**
* 用户初始化,发放福利:流量包 TODO
* @param accountDO
*/
private void userRegisterInitTask(AccountDO accountDO) {
}
}
简介:账号微服务登录模块开发
@Data
public class AccountLoginRequest {
private String phone;
private String pwd;
}
/**
* 用户登录
* @param request
* @return
*/
@PostMapping("login")
public JsonData login(@RequestBody AccountLoginRequest request){
JsonData jsonData = accountService.login(request);
return jsonData;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser {
/**
* 账号
*/
private long accountNo;
/**
* 用户名
*/
private String username;
/**
* 头像
*/
private String headImg;
/**
* 邮箱
*/
private String mail;
/**
* 手机号
*/
private String phone;
/**
* 认证级别
*/
private String auth;
}
/**
* denglu
* @param request
* @return
*/
JsonData login(AccountLoginRequest request);
/**
* 1、根据手机号去找
* 2、有的话,则用秘钥+用户传递的明文密码,进行加密,再和数据库的密文进行匹配
*
* @param request
* @return
*/
@Override
public JsonData login(AccountLoginRequest request) {
List<AccountDO> accountDOList = accountManager.findByPhone(request.getPhone());
if(accountDOList!=null && accountDOList.size() ==1){
AccountDO accountDO = accountDOList.get(0);
String md5Crypt = Md5Crypt.md5Crypt(request.getPwd().getBytes(), accountDO.getSecret());
if(md5Crypt.equalsIgnoreCase(accountDO.getPwd())){
LoginUser loginUser = LoginUser.builder().build();
return JsonData.buildSuccess();
}else {
return JsonData.buildResult(BizCodeEnum.ACCOUNT_PWD_ERROR);
}
}else {
return JsonData.buildResult(BizCodeEnum.ACCOUNT_UNREGISTER);
}
}
讲解:引入相关依赖并开发JWT工具类, 开发生产token和校验token的办法
聚合工程加入版本依赖,common项目加入相关依赖
<!-- JWT相关 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
common项目中封装生产token和解密方法
@Slf4j
public class JWTUtil {
/**
* 主题
*/
private static final String SUBJECT = "classes";
/**
* 加密密钥
*/
private static final String SECRET = "classes.classes";
/**
* 令牌前缀
*/
private static final String TOKNE_PREFIX = "dcloud-link";
/**
* token过期时间,7天
*/
private static final long EXPIRED = 1000 * 60 * 60 * 24 * 7;
/**
* 生成token
*
* @param loginUser
* @return
*/
public static String geneJsonWebTokne(LoginUser loginUser) {
if (loginUser == null) {
throw new NullPointerException("对象为空");
}
String token = Jwts.builder().setSubject(SUBJECT)
//配置payload
.claim("head_img", loginUser.getHeadImg())
.claim("account_no", loginUser.getAccountNo())
.claim("username", loginUser.getUsername())
.claim("mail", loginUser.getMail())
.claim("phone", loginUser.getPhone())
.claim("auth", loginUser.getAuth())
.setIssuedAt(new Date())
.setExpiration(new Date(CommonUtil.getCurrentTimestamp() + EXPIRED))
.signWith(SignatureAlgorithm.HS256, SECRET).compact();
token = TOKNE_PREFIX + token;
return token;
}
/**
* 解密jwt
*
* @param token
* @return
*/
public static Claims checkJWT(String token) {
try {
final Claims claims = Jwts.parser().setSigningKey(SECRET)
.parseClaimsJws(token.replace(TOKNE_PREFIX, "")).getBody();
return claims;
} catch (Exception e) {
log.error("jwt 解密失败");
return null;
}
}
}
简介:用户微服务登录拦截器路径配置和开发
package net.classes.Interceptor;
import com.alibaba.nacos.common.utils.HttpMethod;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import net.classes.enums.BizCodeEnum;
import net.classes.model.LoginUser;
import net.classes.utils.CommonUtil;
import net.classes.utils.JWTUtil;
import net.classes.utils.JsonData;
import org.junit.platform.commons.util.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (HttpMethod.OPTIONS.toString().equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpStatus.NO_CONTENT.value());
return true;
}
String accessToken = request.getHeader("token");
if (StringUtils.isBlank(accessToken)) {
accessToken = request.getParameter("token");
}
if (StringUtils.isNotBlank(accessToken)) {
Claims claims = JWTUtil.checkJWT(accessToken);
if (claims == null) {
//未登录
CommonUtil.sendJsonMessage(response, JsonData.buildResult(BizCodeEnum.ACCOUNT_UNLOGIN));
return false;
}
Long accountNo = Long.parseLong(claims.get("account_no").toString());
String headImg = (String) claims.get("head_img");
String username = (String) claims.get("username");
String mail = (String) claims.get("mail");
String phone = (String) claims.get("phone");
String auth = (String) claims.get("auth");
LoginUser loginUser = LoginUser.builder()
.accountNo(accountNo)
.auth(auth)
.phone(phone)
.headImg(headImg)
.mail(mail)
.username(username)
.build();
//request.setAttribute("loginUser",loginUser);
//通过threadlocal
threadLocal.set(loginUser);
return true;
}
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
threadLocal.remove();
}
}
package net.classes.config;
import lombok.extern.slf4j.Slf4j;
import net.classes.Interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@Slf4j
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
//添加拦截的路径
.addPathPatterns("/api/account/*/**", "/api/traffic/*/**")
//排除不拦截
.excludePathPatterns("/api/account/*/register", "/api/account/*/upload", "/api/account/*/login", "/api/notify/v1/captcha", "/api/notify/*/send_code");
}
}
CREATE TABLE `traffic_0` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`day_limit` int DEFAULT NULL COMMENT '每天限制多少条,短链',
`day_used` int DEFAULT NULL COMMENT '当天用了多少条,短链',
`total_limit` int DEFAULT NULL COMMENT '总次数,活码才用',
`account_no` bigint DEFAULT NULL COMMENT '账号',
`out_trade_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '订单号',
`level` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '产品层级:FIRST青铜、SECOND黄金、THIRD钻石',
`expired_date` date DEFAULT NULL COMMENT '过期日期',
`plugin_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '插件类型',
`product_id` bigint DEFAULT NULL COMMENT '商品主键',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_trade_no` (`out_trade_no`,`account_no`) USING BTREE,
KEY `idx_account_no` (`account_no`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE `traffic_1` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`day_limit` int DEFAULT NULL COMMENT '每天限制多少条,短链',
`day_used` int DEFAULT NULL COMMENT '当天用了多少条,短链',
`total_limit` int DEFAULT NULL COMMENT '总次数,活码才用',
`account_no` bigint DEFAULT NULL COMMENT '账号',
`out_trade_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '订单号',
`level` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '产品层级:FIRST青铜、SECOND黄金、THIRD钻石',
`expired_date` date DEFAULT NULL COMMENT '过期日期',
`plugin_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '插件类型',
`product_id` bigint DEFAULT NULL COMMENT '商品主键',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_trade_no` (`out_trade_no`,`account_no`) USING BTREE,
KEY `idx_account_no` (`account_no`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
<dependency>
<groupId>net.classes</groupId>
<artifactId>dcloud-common</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- <exclusions>-->
<!-- <exclusion>-->
<!-- <groupId>org.apache.shardingsphere</groupId>-->
<!-- <artifactId>sharding-jdbc-spring-boot-starter</artifactId>-->
<!-- </exclusion>-->
<!-- </exclusions>-->
</dependency>
# 数据源 ds0 第一个数据库
shardingsphere:
datasource:
ds0:
connectionTimeoutMilliseconds: 30000
driver-class-name: com.mysql.cj.jdbc.Driver
idleTimeoutMilliseconds: 60000
jdbc-url: jdbc:mysql://124.221.200.246:3306/dcloud_account?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
maintenanceIntervalMilliseconds: 30000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
minPoolSize: 50
password: 123456
type: com.zaxxer.hikari.HikariDataSource
username: root
names: ds0
props:
# 打印执行的数据库以及语句
sql:
show: true
sharding:
tables:
traffic:
# 指定traffic表的数据分布情况,配置数据节点,行表达式标识符使用 ${...} 或 $->{...},但前者与 Spring 本身的文件占位符冲突,所以在 Spring 环境中建议使用 $->{...}
actual-data-nodes: ds0.traffic_$->{0..1}
# 水平分表策略+行表达式分片
table-strategy:
inline:
algorithm-expression: traffic_$->{account_no % 2}
sharding-column: account_no
简介: 分布式ID生成器Snowflake自定义wrokId实战
@Configuration
public class SnowFlakeWordIdConfig {
/**
* 动态指定sharding jdbc 的雪花算法中的属性work.id属性
* 通过调用System.setProperty()的方式实现,可用容器的 id 或者机器标识位
* workId最大值 1L << 100,就是1024,即 0<= workId < 1024
* {@link SnowflakeShardingKeyGenerator#getWorkerId()}
*
*/
static {
try {
InetAddress ip4 = Inet4Address.getLocalHost();
String addressIp = ip4.getHostAddress();
System.setProperty("workerId", (Math.abs(addressIp.hashCode())%1024)+"");
} catch (UnknownHostException e) {
throw new BizException(BizCodeEnum.OPS_NETWORK_ADDRESS_ERROR);
}
}
}
#id生成策略
key-generator:
column: id
props:
worker:
id: ${workerId}
#id生成策略
type: SNOWFLAKE
public class IDUtil {
private static SnowflakeShardingKeyGenerator shardingKeyGenerator = new SnowflakeShardingKeyGenerator();
/**
* 雪花算法生成器,配置workId,避免重复
*
* 10进制 654334919987691526
* 64位 0000100100010100101010100010010010010110000000000000000000000110
*
* {@link SnowFlakeWordIdConfig}
*
* @return
*/
public static Comparable<?> geneSnowFlakeID(){
return shardingKeyGenerator.generateKey();
}
}
简介: Guava框架里面的Murmur哈希算法测试
@Test
public void testMurmurHash() {
for (int i = 0; i < 50; i++) {
int num1 = random.nextInt(1000000);
int num2 = random.nextInt(1000000);
int num3 = random.nextInt(1000000);
String originalUrl = num1 + "classes+" + num2 + ".net" + num3;
long murmur3_32 = Hashing.murmur3_32().hashUnencodedChars(originalUrl).padToLong();
System.out.println("murmur3_32="+murmur3_32);
}
}
/**
* murmur hash算法
* @param param
* @return
*/
public static long murmurHash32(String param){
long murmur32 = Hashing.murmur3_32().hashUnencodedChars(param).padToLong();
return murmur32;
}
简介: 短链生成组件ShortLinkComponent封装
/**
* 创建短链
* @param originalUrl
* @return db编码+6位短链编码
*/
public String createShortLinkCode(String originalUrl){
long murmur32 = CommonUtil.murmurHash32(originalUrl);
//转62进制
String shortLinkCode = encodeToBase62(murmur32);
return code;
}
private static final String CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
/**
* 10进制转62进制
* @param num
* @return
*/
private static String encodeToBase62(long num) {
//StringBuffer:线程安全; StringBuilder:线程不安全
StringBuffer sb = new StringBuffer();
do {
int i = (int) (num % 62);
sb.append(CHARS.charAt(i));
num /= 62;
// num = num/ 62;
} while (num > 0);
String value = sb.reverse().toString();
return value;
}
CREATE TABLE `link_group` (
`id` bigint unsigned NOT NULL,
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '组名',
`account_no` bigint DEFAULT NULL COMMENT '账号唯一编号',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE `short_link` (
`id` bigint unsigned NOT NULL ,
`group_id` bigint DEFAULT NULL COMMENT '组',
`title` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '短链标题',
`original_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '原始url地址',
`domain` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '短链域名',
`code` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '短链压缩码',
`sign` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '长链的md5码,方便查找',
`expired` datetime DEFAULT NULL COMMENT '过期时间,长久就是-1',
`account_no` bigint DEFAULT NULL COMMENT '账号唯一编号',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`del` int unsigned NOT NULL COMMENT '0是默认,1是删除',
`state` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '状态,lock是锁定不可用,active是可用',
`link_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '链接产品层级:FIRST 免费青铜、SECOND黄金、THIRD钻石',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
crud
@Configuration
@Slf4j
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
//添加拦截的路径
.addPathPatterns("/api/linkGroup/*/**", "/api/link/*/**", "/api/domain/*/**")
//排除不拦截
.excludePathPatterns("/api/account/*/register", "/api/account/*/upload", "/api/account/*/login", "/api/notify/v1/captcha", "/api/notify/*/send_code");
}
}
@Data
public class LinkGroupAddRequest {
/**
* 组名
*/
private String title;
}
@Data
public class LinkGroupUpdateRequest {
/**
* 组id
*/
private Long id;
/**
* 组名
*/
private String title;
}
@Autowired
private LinkGroupService linkGroupService;
/**
* 创建分组
* @param addRequest
* @return
*/
@PostMapping("/add")
public JsonData add(@RequestBody LinkGroupAddRequest addRequest){
int rows = linkGroupService.add(addRequest);
return rows == 1 ? JsonData.buildSuccess():JsonData.buildResult(BizCodeEnum.GROUP_ADD_FAIL);
}
/**
* 根据id删除分组
* @param groupId
* @return
*/
@DeleteMapping("/del/{group_id}")
public JsonData del(@PathVariable("group_id") Long groupId){
int rows = linkGroupService.del(groupId);
return rows == 1 ? JsonData.buildSuccess():JsonData.buildResult(BizCodeEnum.GROUP_NOT_EXIST);
}
/**
* 根据id找详情
* @param groupId
* @return
*/
@GetMapping("detail/{group_id}")
public JsonData detail(@PathVariable("group_id") Long groupId){
LinkGroupVO linkGroupVO = linkGroupService.detail(groupId);
return JsonData.buildSuccess(linkGroupVO);
}
/**
* 列出用户全部分组
* @return
*/
@GetMapping("list")
public JsonData findUserAllLinkGroup(){
List<LinkGroupVO> list = linkGroupService.listAllGroup();
return JsonData.buildSuccess(list);
}
/**
* 列出用户全部分组
* @return
*/
@PutMapping("update")
public JsonData update(@RequestBody LinkGroupUpdateRequest request){
int rows = linkGroupService.updateById(request);
return rows == 1 ? JsonData.buildSuccess():JsonData.buildResult(BizCodeEnum.GROUP_OPER_FAIL);
}
/**
* 新增分组
* @param addRequest
* @return
*/
int add(LinkGroupAddRequest addRequest);
/**
* 删除分组
* @param groupId
* @return
*/
int del(Long groupId);
/**
* 详情
* @param groupId
* @return
*/
LinkGroupVO detail(Long groupId);
/**
* 列出用户全部分组
* @return
*/
List<LinkGroupVO> listAllGroup();
/**
* 更新组名
* @param request
* @return
*/
int updateById(LinkGroupUpdateRequest request);
@Slf4j
@Service
public class LinkGroupImpl implements LinkGroupService {
@Autowired
private LinkGroupManager linkGroupManager;
@Override
public int add(LinkGroupAddRequest addRequest) {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
LinkGroupDO linkGroupDO = new LinkGroupDO();
linkGroupDO.setTitle(addRequest.getTitle());
linkGroupDO.setAccountNo(accountNo);
int rows = linkGroupManager.add(linkGroupDO);
return rows;
}
@Override
public int del(Long groupId) {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
return linkGroupManager.del(groupId, accountNo);
}
@Override
public LinkGroupVO detail(Long groupId) {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
LinkGroupDO linkGroupDO = linkGroupManager.detail(groupId, accountNo);
LinkGroupVO linkGroupVO = new LinkGroupVO();
// mapStruct
BeanUtils.copyProperties(linkGroupDO, linkGroupVO);
return linkGroupVO;
}
@Override
public List<LinkGroupVO> listAllGroup() {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
List<LinkGroupDO> linkGroupDOList = linkGroupManager.listAllGroup(accountNo);
List<LinkGroupVO> groupVOList = linkGroupDOList.stream().map(obj -> {
LinkGroupVO linkGroupVO = new LinkGroupVO();
BeanUtils.copyProperties(obj, linkGroupVO);
return linkGroupVO;
}).collect(Collectors.toList());
return groupVOList;
}
@Override
public int updateById(LinkGroupUpdateRequest request) {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
LinkGroupDO linkGroupDO = new LinkGroupDO();
linkGroupDO.setTitle(request.getTitle());
linkGroupDO.setId(request.getId());
linkGroupDO.setAccountNo(accountNo);
int rows = linkGroupManager.updateById(linkGroupDO);
return rows;
}
}
public interface LinkGroupManager {
int add(LinkGroupDO linkGroupDO);
int del(Long groupId, Long accountNo);
LinkGroupDO detail(Long groupId, Long accountNo);
List<LinkGroupDO> listAllGroup(Long accountNo);
int updateById(LinkGroupDO linkGroupDO);
}
@Component
public class LinkGroupManagerImpl implements LinkGroupManager {
@Autowired
private LinkGroupMapper linkGroupMapper;
@Override
public int add(LinkGroupDO linkGroupDO) {
return linkGroupMapper.insert(linkGroupDO);
}
@Override
public int del(Long groupId, Long accountNo) {
return linkGroupMapper.delete(new QueryWrapper<LinkGroupDO>().eq("id",groupId).eq("account_no",accountNo));
}
@Override
public LinkGroupDO detail(Long groupId, Long accountNo) {
return linkGroupMapper.selectOne(new QueryWrapper<LinkGroupDO>().eq("id",groupId).eq("account_no",accountNo));
}
@Override
public List<LinkGroupDO> listAllGroup(Long accountNo) {
return linkGroupMapper.selectList(new QueryWrapper<LinkGroupDO>().eq("account_no",accountNo));
}
@Override
public int updateById(LinkGroupDO linkGroupDO) {
return linkGroupMapper.update(linkGroupDO,new QueryWrapper<LinkGroupDO>().eq("id",linkGroupDO.getId()).eq("account_no",linkGroupDO.getAccountNo()));
}
}
server.port=8003
spring.application.name=dcloud-link
#??????
spring.cloud.nacos.discovery.server-addr=124.221.200.246:8848
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
#-------分表-------
spring.shardingsphere.datasource.ds0.connectionTimeoutMilliseconds=30000
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.ds0.idleTimeoutMilliseconds=60000
spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://124.221.200.246:3306/dcloud_link?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
spring.shardingsphere.datasource.ds0.maintenanceIntervalMilliseconds=30000
spring.shardingsphere.datasource.ds0.maxLifetimeMilliseconds=1800000
spring.shardingsphere.datasource.ds0.maxPoolSize=50
spring.shardingsphere.datasource.ds0.minPoolSize=50
spring.shardingsphere.datasource.ds0.password=123456
spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.names=ds0
spring.shardingsphere.props.sql.show=true
# link_group id生成
spring.shardingsphere.sharding.tables.link_group.key-generator.column=id
spring.shardingsphere.sharding.tables.link_group.key-generator.props.worker.id=${workerId}
spring.shardingsphere.sharding.tables.link_group.key-generator.type=SNOWFLAKE
简介: 短链服务-ShortLink分库分表解决方案讲解
数据量预估
分库分表策略
分库分表
分片键:短链码 code
分库分表算法:短链码进行hash取模
库ID = 短链码hash值 % 库数量
表ID = 短链码hash值 / 库数量 % 表数量
优点
问题
server.port=8003
spring.application.name=dcloud-link
#??????
spring.cloud.nacos.discovery.server-addr=124.221.200.246:8848
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
#-------shardingsphere配置-------
spring.shardingsphere.datasource.names=ds0,ds1
spring.shardingsphere.props.sql.show=true
# ds0配置
spring.shardingsphere.datasource.ds0.connectionTimeoutMilliseconds=30000
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.ds0.idleTimeoutMilliseconds=60000
spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://124.221.200.246:3306/dcloud_link_0?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
spring.shardingsphere.datasource.ds0.maintenanceIntervalMilliseconds=30000
spring.shardingsphere.datasource.ds0.maxLifetimeMilliseconds=1800000
spring.shardingsphere.datasource.ds0.maxPoolSize=50
spring.shardingsphere.datasource.ds0.minPoolSize=50
spring.shardingsphere.datasource.ds0.password=123456
spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds0.username=root
# ds1配置
spring.shardingsphere.datasource.ds1.connectionTimeoutMilliseconds=30000
spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.ds1.idleTimeoutMilliseconds=60000
spring.shardingsphere.datasource.ds1.jdbc-url=jdbc:mysql://124.221.200.246:3306/dcloud_link_1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
spring.shardingsphere.datasource.ds1.maintenanceIntervalMilliseconds=30000
spring.shardingsphere.datasource.ds1.maxLifetimeMilliseconds=1800000
spring.shardingsphere.datasource.ds1.maxPoolSize=50
spring.shardingsphere.datasource.ds1.minPoolSize=50
spring.shardingsphere.datasource.ds1.password=123456
spring.shardingsphere.datasource.ds1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds1.username=root
#---------短连组水平分库,水平分表(一个裤一张表)-------------#
# link_group id生成
spring.shardingsphere.sharding.tables.link_group.key-generator.column=id
spring.shardingsphere.sharding.tables.link_group.key-generator.props.worker.id=${workerId}
spring.shardingsphere.sharding.tables.link_group.key-generator.type=SNOWFLAKE
# 行表达式分库
#分片键
spring.shardingsphere.sharding.tables.link_group.database-strategy.inline.sharding-column=account_no
spring.shardingsphere.sharding.tables.link_group.database-strategy.inline.algorithm-expression=ds$->{account_no%2}
简介: 短链服务-分库免迁移扩容解决方案讲解《黄金玩法》
/**
* 自定义数据源
* 配置分片策略
*/
public class CustomDBPreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {
/**
* @param availableTargetNames 数据源集合
* 在分库时值为所有分片库的集合 databaseNames
* 分表时为对应分片库中所有分片表的集合 tablesNames
* @param shardingValue 分片属性,包括
* logicTableName 为逻辑表,
* columnName 分片健(字段),
* value 为从 SQL 中解析出的分片健的值
* @return
*/
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) {
//获取短链码第一位,即库位
String codePrefix = shardingValue.getValue().substring(0, 1);
for (String targetName : availableTargetNames) {
//获取库名的最后一位,真实配置的ds
String targetNameSuffix = targetName.substring(targetName.length() - 1);
//如果一致则返回
if (codePrefix.equals(targetNameSuffix)) {
return targetName;
}
}
//抛异常
// throw new BizException(BizCodeEnum.DB_ROUTE_NOT_FOUND);
return null;
}
}
短链服务-分表扩容免数据迁移解决方案讲解《黄金玩法》
/**
* 自定义数据源-分表
*/
public class CustomTablePreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {
/**
* @param availableTargetNames 数据源集合
* 在分库时值为所有分片库的集合 databaseNames
* 分表时为对应分片库中所有分片表的集合 tablesNames
* @param shardingValue 分片属性,包括
* logicTableName 为逻辑表,
* columnName 分片健(字段),
* value 为从 SQL 中解析出的分片健的值
* @return
*/
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) {
//获取逻辑表名
String targetName = availableTargetNames.iterator().next();
String value = shardingValue.getValue();
//短链码最后一位
String codePrefix = value.substring(value.length() - 1);
//拼装actual table
return targetName + "_" + codePrefix;
}
}
# 水平分表策略,自定义策略。
# 自定义表 真实库.逻辑表
spring.shardingsphere.sharding.tables.short_link.actual-data-nodes=ds0.short_link,ds1.short_link,dsa.short_link
spring.shardingsphere.sharding.tables.short_link.table-strategy.standard.sharding-column=code
spring.shardingsphere.sharding.tables.short_link.table-strategy.standard.precise-algorithm-class-name=net.classes.config.CustomTablePreciseShardingAlgorithm
简介: 短链服务-短链码配置生成库表位实战
public class ShardingDBConfig {
/**
* 存储数据库位置编号
*/
private static final List<String> dbPrefixList = new ArrayList<>();
private static Random random = new Random();
//配置启用那些库的前缀
static {
dbPrefixList.add("0");
dbPrefixList.add("1");
dbPrefixList.add("a");
}
/**
* 获取随机的前缀
* @return
*/
public static String getRandomDBPrefix(){
int index = random.nextInt(dbPrefixList.size());
return dbPrefixList.get(index);
}
}
public class ShardingTableConfig {
/**
* 存储数据表位置编号
*/
private static final List<String> tableSuffixList = new ArrayList<>();
private static Random random = new Random();
//配置启用那些表的后缀
static {
tableSuffixList.add("0");
tableSuffixList.add("a");
}
/**
* 获取随机的后缀
* @return
*/
public static String getRandomTableSuffix(){
int index = random.nextInt(tableSuffixList.size());
return tableSuffixList.get(index);
}
}
String code = ShardingDBConfig.getRandomDBPrefix() + shortLinkCode + ShardingTableConfig.getRandomTablePrefix();
简介: 短链服务-Manager层模块CRUD开发
public interface ShortLinkManager {
/**
* 新增
* @param shortLinkDO
* @return
*/
int addShortLink(ShortLinkDO shortLinkDO);
/**
* 根据短链码找短链
* @param shortLinkCode
* @return
*/
ShortLinkDO findByShortLinCode(String shortLinkCode);
/**
* 删除
* @param shortLinkCode
* @param accountNo
* @return
*/
int del(String shortLinkCode,Long accountNo);
}
@Component
@Slf4j
public class ShortLinkManagerImpl implements ShortLinkManager {
@Autowired
private ShortLinkMapper shortLinkMapper;
@Override
public int addShortLink(ShortLinkDO shortLinkDO) {
return shortLinkMapper.insert(shortLinkDO);
}
@Override
public ShortLinkDO findByShortLinCode(String shortLinkCode) {
ShortLinkDO shortLinkDO = shortLinkMapper.selectOne(
new QueryWrapper<ShortLinkDO>().eq("code", shortLinkCode));
return shortLinkDO;
}
@Override
public int del(String shortLinkCode, Long accountNo) {
ShortLinkDO shortLinkDO = new ShortLinkDO();
shortLinkDO.setDel(1);
int rows = shortLinkMapper.update(shortLinkDO,
new QueryWrapper<ShortLinkDO>().eq("code", shortLinkCode).eq("account_no", accountNo));
return rows;
}
}
简介: 短链URL 跳转302跳转接口开发实战
public enum ShortLinkStateEnum {
/**
* 锁定
*/
LOCK,
/**
* 可用
*/
ACTIVE;
}
package net.classes.vo;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = false)
public class ShortLinkVO implements Serializable {
private Long id;
/**
* 组
*/
private Long groupId;
/**
* 短链标题
*/
private String title;
/**
* 原始url地址
*/
private String originalUrl;
/**
* 短链域名
*/
private String domain;
/**
* 短链压缩码
*/
private String code;
/**
* 长链的md5码,方便查找
*/
private String sign;
/**
* 过期时间,长久就是-1
*/
private Date expired;
/**
* 账号唯一编号
*/
private Long accountNo;
/**
* 创建时间
*/
private Date gmtCreate;
/**
* 修改时间
*/
private Date gmtModified;
/**
* 0是默认,1是删除
*/
private Integer del;
/**
* 状态,lock是锁定不可用,active是可用
*/
private String state;
/**
* 链接产品层级:FIRST 免费青铜、SECOND黄金、THIRD钻石
*/
private String linkType;
}
public interface ShortLinkService {
/**
* 解析短链
* @param shortLinkCode
* @return
*/
ShortLinkVO parseShortLinkCode(String shortLinkCode);
}
@Service
@Slf4j
public class ShortLinkServiceImpl implements ShortLinkService {
@Autowired
private ShortLinkManager shortLinkManager;
@Override
public ShortLinkVO parseShortLinkCode(String shortLinkCode) {
ShortLinkDO shortLinkDO = shortLinkManager.findByShortLinCode(shortLinkCode);
if(shortLinkDO == null){
return null;
}
ShortLinkVO shortLinkVO = new ShortLinkVO();
BeanUtils.copyProperties(shortLinkDO,shortLinkVO);
return shortLinkVO;
}
}
Controller
@Slf4j
public class LinkApiController {
@Autowired
private ShortLinkService shortLinkService;
/**
* 解析 301还是302,这边是返回http code是302
* <p>
* 知识点一,为什么要用 301 跳转而不是 302 呐?
* <p>
* 301 是永久重定向,302 是临时重定向。
* <p>
* 短地址一经生成就不会变化,所以用 301 是同时对服务器压力也会有一定减少
* <p>
* 但是如果使用了 301,无法统计到短地址被点击的次数。
* <p>
* 所以选择302虽然会增加服务器压力,但是有很多数据可以获取进行分析
*
* @param linkCode
* @return
*/
@GetMapping(path = "/{shortLinkCode}")
public void dispatch(@PathVariable(name = "shortLinkCode") String shortLinkCode,
HttpServletRequest request, HttpServletResponse response) {
try {
log.info("短链码:{}", shortLinkCode);
//判断短链码是否合规
if (isLetterDigit(shortLinkCode)) {
//查找短链
ShortLinkVO shortLinkVO = shortLinkService.parseShortLinkCode(shortLinkCode);
//判断是否过期和可用
if (isVisitable(shortLinkVO)) {
response.setHeader("Location", shortLinkVO.getOriginalUrl());
//302跳转
response.setStatus(HttpStatus.FOUND.value());
} else {
response.setStatus(HttpStatus.NOT_FOUND.value());
return;
}
}
} catch (Exception e) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
}
/**
* 判断短链是否可用
*
* @param shortLinkVO
* @return
*/
private static boolean isVisitable(ShortLinkVO shortLinkVO) {
if ((shortLinkVO != null && shortLinkVO.getExpired().getTime() > CommonUtil.getCurrentTimestamp())) {
if (ShortLinkStateEnum.ACTIVE.name().equalsIgnoreCase(shortLinkVO.getState())) {
return true;
}
} else if ((shortLinkVO != null && shortLinkVO.getExpired().getTime() == -1)) {
if (ShortLinkStateEnum.ACTIVE.name().equalsIgnoreCase(shortLinkVO.getState())) {
return true;
}
}
return false;
}
/**
* 仅包括数字和字母
*
* @param str
* @return
*/
private static boolean isLetterDigit(String str) {
String regex = "^[a-z0-9A-Z]+$";
return str.matches(regex);
}
}
简介: 短链服务-分库分表冗余双写库表架构设计
CREATE TABLE `group_code_mapping_0` (
`id` bigint unsigned NOT NULL,
`group_id` bigint DEFAULT NULL COMMENT '组',
`title` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '短链标题',
`original_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '原始url地址',
`domain` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '短链域名',
`code` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '短链压缩码',
`sign` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '长链的md5码,方便查找',
`expired` datetime DEFAULT NULL COMMENT '过期时间,长久就是-1',
`account_no` bigint DEFAULT NULL COMMENT '账号唯一编号',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`del` int unsigned NOT NULL COMMENT '0是默认,1是删除',
`state` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '状态,lock是锁定不可用,active是可用',
`link_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '链接产品层级:FIRST 免费青铜、SECOND黄金、THIRD钻石',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE `domain` (
`id` bigint unsigned NOT NULL ,
`account_no` bigint DEFAULT NULL COMMENT '用户自己绑定的域名',
`domain_type` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '域名类型,自建custom, 官方offical',
`value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`del` int(1) unsigned zerofill DEFAULT '0' COMMENT '0是默认,1是禁用',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@Data
@EqualsAndHashCode(callSuper = false)
public class GroupCodeMappingVO implements Serializable {
private Long id;
/**
* 组
*/
private Long groupId;
/**
* 短链标题
*/
private String title;
/**
* 原始url地址
*/
private String originalUrl;
/**
* 短链域名
*/
private String domain;
/**
* 短链压缩码
*/
private String code;
/**
* 长链的md5码,方便查找
*/
private String sign;
/**
* 过期时间,长久就是-1
*/
private Date expired;
/**
* 账号唯一编号
*/
private Long accountNo;
/**
* 创建时间
*/
private Date gmtCreate;
/**
* 修改时间
*/
private Date gmtModified;
/**
* 0是默认,1是删除
*/
private Integer del;
/**
* 状态,lock是锁定不可用,active是可用
*/
private String state;
/**
* 链接产品层级:FIRST 免费青铜、SECOND黄金、THIRD钻石
*/
private String linkType;
}
public enum DomainTypeEnum {
/**
* 自建
*/
CUSTOM,
/**
* 官方
*/
OFFICIAL;
}
public interface DomainManager {
/**
* 查找详情
* @param id
* @param accountNO
* @return
*/
DomainDO findById(Long id, Long accountNO);
/**
* 查找详情
* @param id
* @param domainTypeEnum
* @return
*/
DomainDO findByDomainTypeAndID(Long id, DomainTypeEnum domainTypeEnum);
/**
* 新增
* @param domainDO
* @return
*/
int addDomain(DomainDO domainDO);
/**
* 列举全部官方域名
* @return
*/
List<DomainDO> listOfficialDomain();
/**
* 列举全部自定义域名
* @return
*/
List<DomainDO> listCustomDomain(Long accountNo);
}
@Component
@Slf4j
public class DomainManagerImpl implements DomainManager {
@Autowired
private DomainMapper domainMapper;
@Override
public DomainDO findById(Long id, Long accountNO) {
return domainMapper.selectOne(new QueryWrapper<DomainDO>().eq("id", id).eq("account_no", accountNO));
}
@Override
public DomainDO findByDomainTypeAndID(Long id, DomainTypeEnum domainTypeEnum) {
return domainMapper.selectOne(new QueryWrapper<DomainDO>().eq("id", id).eq("domain_type", domainTypeEnum.name()));
}
@Override
public int addDomain(DomainDO domainDO) {
return domainMapper.insert(domainDO);
}
@Override
public List<DomainDO> listOfficialDomain() {
return domainMapper.selectList(new QueryWrapper<DomainDO>().eq("domain_type", DomainTypeEnum.OFFICIAL.name()));
}
@Override
public List<DomainDO> listCustomDomain(Long accountNo) {
return domainMapper.selectList(new QueryWrapper<DomainDO>()
.eq("domain_type", DomainTypeEnum.CUSTOM.name())
.eq("account_no", accountNo));
}
}
@Component
@Slf4j
public class GroupCodeMappingManagerImpl implements GroupCodeMappingManager {
@Autowired
private GroupCodeMappingMapper groupCodeMappingMapper;
@Override
public GroupCodeMappingDO findByGroupIdAndMappingId(Long mappingId, Long accountNo, Long groupId) {
GroupCodeMappingDO groupCodeMappingDO = groupCodeMappingMapper.selectOne(new QueryWrapper<GroupCodeMappingDO>()
.eq("id", mappingId).eq("account_no", accountNo)
.eq("group_id", groupId));
return groupCodeMappingDO;
}
@Override
public int add(GroupCodeMappingDO groupCodeMappingDO) {
return groupCodeMappingMapper.insert(groupCodeMappingDO);
}
@Override
public int del(String shortLinkCode, Long accountNo, Long groupId) {
int rows = groupCodeMappingMapper.update(null, new UpdateWrapper<GroupCodeMappingDO>()
.eq("code", shortLinkCode).eq("account_no", accountNo)
.eq("group_id", groupId).set("del", 1));
return rows;
}
@Override
public Map<String, Object> pageShortLinkByGroupId(Integer page, Integer size, Long accountNo, Long groupId) {
Page<GroupCodeMappingDO> pageInfo = new Page<>(page, size);
Page<GroupCodeMappingDO> groupCodeMappingDOPage = groupCodeMappingMapper.selectPage(pageInfo, new QueryWrapper<GroupCodeMappingDO>().eq("account_no", accountNo)
.eq("group_id", groupId));
Map<String, Object> pageMap = new HashMap<>(3);
pageMap.put("total_record", groupCodeMappingDOPage.getTotal());
pageMap.put("total_page", groupCodeMappingDOPage.getPages());
pageMap.put("current_data", groupCodeMappingDOPage.getRecords()
.stream().map(obj -> beanProcess(obj)).collect(Collectors.toList()));
return pageMap;
}
@Override
public int updateGroupCodeMappingState(Long accountNo, Long groupId, String shortLinkCode, ShortLinkStateEnum shortLinkStateEnum) {
int rows = groupCodeMappingMapper.update(null, new UpdateWrapper<GroupCodeMappingDO>()
.eq("code", shortLinkCode).eq("account_no", accountNo)
.eq("group_id", groupId).set("state", shortLinkStateEnum.name()));
return rows;
}
private GroupCodeMappingVO beanProcess(GroupCodeMappingDO groupCodeMappingDO) {
GroupCodeMappingVO groupCodeMappingVO = new GroupCodeMappingVO();
BeanUtils.copyProperties(groupCodeMappingDO, groupCodeMappingVO);
return groupCodeMappingVO;
}
}
简介: 短链服务-Domain短链域名模块开发
@Data
@EqualsAndHashCode(callSuper = false)
public class DomainVO implements Serializable {
private Long id;
/**
* 用户自己绑定的域名
*/
private Long accountNo;
/**
* 域名类型,自建custom, 官方offical
*/
private String domainType;
private String value;
/**
* 0是默认,1是禁用
*/
private Integer del;
private Date gmtCreate;
private Date gmtModified;
}
@RestController
@RequestMapping("/api/domain/v1")
public class DomainController {
@Autowired
private DomainService domainService;
/**
* 查询全部可用域名
*
* @return
*/
@GetMapping("/list")
public JsonData listAll() {
List<DomainVO> list = domainService.listAll();
return JsonData.buildSuccess(list);
}
}
public interface DomainService {
/**
* 列举全部可用域名
* @return
*/
List<DomainVO> listAll();
}
@Service
@Slf4j
public class DomainServiceImpl implements DomainService {
@Autowired
private DomainManager domainManager;
@Override
public List<DomainVO> listAll() {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
List<DomainDO> customDomainList = domainManager.listCustomDomain(accountNo);
List<DomainDO> officialDomainList = domainManager.listOfficialDomain();
customDomainList.addAll(officialDomainList);
return customDomainList.stream().map(obj-> beanProcess(obj)).collect(Collectors.toList());
}
private DomainVO beanProcess(DomainDO domainDO){
DomainVO domainVO = new DomainVO();
BeanUtils.copyProperties(domainDO,domainVO);
return domainVO;
}
}
简介: 短链服务-分库分表默认数据源配置实战
#----------配置默认数据库,比如短链域名,不分库分表--------------
spring.shardingsphere.sharding.default-data-source-name=ds0
#默认id生成策略
spring.shardingsphere.sharding.default-key-generator.column=id
spring.shardingsphere.sharding.default-key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.default-key-generator.props.worker.id=${workerId}
交换机类型
简介: 冗余双写MQ架构RabbitMQ配置开发实战
package net.classes.config;
import lombok.Data;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@Data
public class RabbitMQConfig {
/**
* 交换机
*/
private String shortLinkEventExchange="short_link.event.exchange";
/**
* 创建交换机 Topic类型
* 一般一个微服务一个交换机
* @return
*/
@Bean
public Exchange shortLinkEventExchange(){
return new TopicExchange(shortLinkEventExchange,true,false);
}
//新增短链相关配置====================================
/**
* 新增短链 队列
*/
private String shortLinkAddLinkQueue="short_link.add.link.queue";
/**
* 新增短链映射 队列
*/
private String shortLinkAddMappingQueue="short_link.add.mapping.queue";
/**
* 新增短链具体的routingKey,【发送消息使用】
*/
private String shortLinkAddRoutingKey="short_link.add.link.mapping.routing.key";
/**
* topic类型的binding key,用于绑定队列和交换机,是用于 link 消费者
*/
private String shortLinkAddLinkBindingKey="short_link.add.link.*.routing.key";
/**
* topic类型的binding key,用于绑定队列和交换机,是用于 mapping 消费者
*/
private String shortLinkAddMappingBindingKey="short_link.add.*.mapping.routing.key";
/**
* 新增短链api队列和交换机的绑定关系建立
*/
@Bean
public Binding shortLinkAddApiBinding(){
return new Binding(shortLinkAddLinkQueue,Binding.DestinationType.QUEUE, shortLinkEventExchange,shortLinkAddLinkBindingKey,null);
}
/**
* 新增短链mapping队列和交换机的绑定关系建立
*/
@Bean
public Binding shortLinkAddMappingBinding(){
return new Binding(shortLinkAddMappingQueue,Binding.DestinationType.QUEUE, shortLinkEventExchange,shortLinkAddMappingBindingKey,null);
}
/**
* 新增短链api 普通队列,用于被监听
*/
@Bean
public Queue shortLinkAddLinkQueue(){
return new Queue(shortLinkAddLinkQueue,true,false,false);
}
/**
* 新增短链mapping 普通队列,用于被监听
*/
@Bean
public Queue shortLinkAddMappingQueue(){
return new Queue(shortLinkAddMappingQueue,true,false,false);
}
}
简介: 冗余双写MQ架构-短链和mapping消费者配置
@Component
@Slf4j
@RabbitListener(queuesToDeclare = { @Queue("short_link.add.link.queue") })
public class ShortLinkAddLinkMQListener {
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkAddLinkMQListener message消息内容:{}",message);
try{
//TODO 处理业务逻辑
}catch (Exception e){
//处理业务异常,还有进行其他操作,比如记录失败原因
log.error("消费失败:{}",eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}",eventMessage);
//确认消息消费成功
//channel.basicAck(tag,false);
}
}
@Component
@Slf4j
@RabbitListener(queuesToDeclare = { @Queue("short_link.add.mapping.queue") })
public class ShortLinkAddMappingMQListener {
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkAddMappingMQListener message消息内容:{}",message);
try{
//TODO 处理业务逻辑
}catch (Exception e){
//处理业务异常,还有进行其他操作,比如记录失败原因
log.error("消费失败:{}",eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}",eventMessage);
//确认消息消费成功
//channel.basicAck(tag,false);
}
}
简介: 冗余双写MQ架构-MQ消费者配置自动创建队列
@Data
public class ShortLinkAddRequest {
/**
* 组
*/
private Long groupId;
/**
* 短链标题
*/
private String title;
/**
* 原生url
*/
private String originalUrl;
/**
* 域名id
*/
private Long domainId;
/**
* 域名类型
*/
private String domainType;
/**
* 过期时间
*/
private Date expired;
}
@RestController
@RequestMapping("/api/link/v1")
public class ShortLinkController {
@Autowired
private ShortLinkService shortLinkService;
@PostMapping("add")
public JsonData createShortLink(@RequestBody ShortLinkAddRequest request){
JsonData jsonData = shortLinkService.createShortLink(request);
return jsonData;
}
}
public enum EventMessageType {
/**
* 短链创建
*/
SHORT_LINK_ADD;
}
/**
* 创建短链
* @param request
* @return
*/
JsonData createShortLink(ShortLinkAddRequest request);
@Override
public JsonData createShortLink(ShortLinkAddRequest request) {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
EventMessage eventMessage = EventMessage.builder().accountNo(accountNo)
.content(JsonUtil.obj2Json(request))
.messageId(IDUtil.geneSnowFlakeID().toString())
.eventMessageType(EventMessageType.SHORT_LINK_ADD.name())
.build();
rabbitTemplate.convertAndSend(rabbitMQConfig.getShortLinkEventExchange(), rabbitMQConfig.getShortLinkAddRoutingKey(),eventMessage);
return JsonData.buildSuccess();
}
##----------rabbit配置--------------
spring.rabbitmq.host=124.221.200.246
spring.rabbitmq.port=5672
#需要手工创建虚拟主机
spring.rabbitmq.virtual-host=dev
spring.rabbitmq.username=admin
spring.rabbitmq.password=123456
#消息确认方式,manual(手动ack) 和auto(自动ack)
spring.rabbitmq.listener.simple.acknowledge-mode=auto
大家遇到的问题 (不会自动创建队列)
@RabbitListener(queues = "short_link.add.link.queue")
另外种方式(若Mq中无相应名称的队列,会自动创建Queue)
@RabbitListener(queuesToDeclare = { @Queue("short_link.add.link.queue") })
链路测试-多节点启动
简介: 冗余双写架构-MQ消费者异常处理方案讲解
##----------rabbit配置--------------
spring.rabbitmq.host=124.221.200.246
spring.rabbitmq.port=5672
#需要手工创建虚拟主机
spring.rabbitmq.virtual-host=dev
spring.rabbitmq.username=admin
spring.rabbitmq.password=123456
#消息确认方式,manual(手动ack) 和auto(自动ack)
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#开启重试,消费者代码不能添加try catch捕获不往外抛异常
spring.rabbitmq.listener.simple.retry.enabled=true
#最大重试次数
spring.rabbitmq.listener.simple.retry.max-attempts=4
# 重试消息的时间间隔,5秒
spring.rabbitmq.listener.simple.retry.initial-interval=5000
##----------rabbit配置--------------
spring.rabbitmq.host=124.221.200.246
spring.rabbitmq.port=5672
#需要手工创建虚拟主机
spring.rabbitmq.virtual-host=dev
spring.rabbitmq.username=admin
spring.rabbitmq.password=123456
#消息确认方式,manual(手动ack) 和auto(自动ack)
spring.rabbitmq.listener.simple.acknowledge-mode=auto
#开启重试,消费者代码不能添加try catch捕获不往外抛异常
spring.rabbitmq.listener.simple.retry.enabled=true
#最大重试次数
spring.rabbitmq.listener.simple.retry.max-attempts=4
# 重试消息的时间间隔,5秒
spring.rabbitmq.listener.simple.retry.initial-interval=5000
@Configuration
@Data
public class RabbitMQErrorConfig {
private String shortLinkErrorExchange = "short_link.error.exchange";
private String shortLinkErrorQueue = "short_link.error.queue";
private String shortLinkErrorRoutingKey = "short_link.error.routing.key";
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 异常交换机
* @return
*/
@Bean
public TopicExchange errorTopicExchange(){
return new TopicExchange(shortLinkErrorExchange,true,false);
}
/**
* 异常队列
* @return
*/
@Bean
public Queue errorQueue(){
return new Queue(shortLinkErrorQueue,true);
}
/**
* 队列与交换机进行绑定
* @return
*/
@Bean
public Binding BindingErrorQueueAndExchange(Queue errorQueue,TopicExchange errorTopicExchange){
return BindingBuilder.bind(errorQueue).to(errorTopicExchange).with(shortLinkErrorRoutingKey);
}
/**
* 配置 RepublishMessageRecoverer
* 用途:消息重试一定次数后,用特定的routingKey转发到指定的交换机中,方便后续排查和告警
*
* 顶层是 MessageRecoverer接口,多个实现类
*
* @return
*/
@Bean
public MessageRecoverer messageRecoverer(){
return new RepublishMessageRecoverer(rabbitTemplate,shortLinkErrorExchange,shortLinkErrorRoutingKey);
}
}
@Component
@Slf4j
@RabbitListener(queuesToDeclare = { @Queue("short_link.error.queue") })
public class ShortLinkErrorMQListener {
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.error("告警:监听到消息ShortLinkErrorMQListener eventMessage消息内容:{}",eventMessage);
log.error("告警:Message:{}",message);
log.error("告警成功,发送通知短信");
}
}
简介: 冗余双写架构-商家创建短链-C/b端消费者开发实战
public enum EventMessageType {
/**
* 短链创建
*/
SHORT_LINK_ADD,
/**
* 短链创建 C端
*/
SHORT_LINK_ADD_LINK,
/**
* 短链创建 B端
*/
SHORT_LINK_ADD_MAPPING;
}
@Component
@Slf4j
@RabbitListener(queuesToDeclare = { @Queue("short_link.add.mapping.queue") })
public class ShortLinkAddMappingMQListener {
@Autowired
private ShortLinkService shortLinkService;
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkAddMappingMQListener message消息内容:{}",message);
try{
eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_ADD_LINK.name());
shortLinkService.handlerAddShortLink(eventMessage);
}catch (Exception e){
//处理业务异常,还有进行其他操作,比如记录失败原因
log.error("消费失败:{}",eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}",eventMessage);
//确认消息消费成功
//channel.basicAck(tag,false);
}
}
@Component
@Slf4j
@RabbitListener(queuesToDeclare = { @Queue("short_link.add.mapping.queue") })
public class ShortLinkAddMappingMQListener {
@Autowired
private ShortLinkService shortLinkService;
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkAddMappingMQListener message消息内容:{}",message);
try{
eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_ADD_LINK.name());
shortLinkService.handlerAddShortLink(eventMessage);
}catch (Exception e){
//处理业务异常,还有进行其他操作,比如记录失败原因
log.error("消费失败:{}",eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}",eventMessage);
//确认消息消费成功
//channel.basicAck(tag,false);
}
}
/**
* 处理短链新增逻辑
* <p>
* //判断短链域名是否合法
* //判断组名是否合法
* //生成长链摘要
* //生成短链码
* //加锁
* //查询短链码是否存在
* //构建短链对象
* //保存数据库
*
* @param eventMessage
* @return
*/
@Override
public boolean handlerAddShortLink(EventMessage eventMessage) {
Long accountNo = eventMessage.getAccountNo();
String messageType = eventMessage.getEventMessageType();
ShortLinkAddRequest addRequest = JsonUtil.json2Obj(eventMessage.getContent(), ShortLinkAddRequest.class);
//短链域名校验
DomainDO domainDO = checkDomain(addRequest.getDomainType(), addRequest.getDomainId(), accountNo);
//校验组是否合法
LinkGroupDO linkGroupDO = checkLinkGroup(addRequest.getGroupId(), accountNo);
//长链摘要
String originalUrlDigest = CommonUtil.MD5(addRequest.getOriginalUrl());
//生成短链码
String shortLinkCode = shortLinkComponent.createShortLinkCode(addRequest.getOriginalUrl());
//TODO 加锁
//先判断是否短链码被占用
ShortLinkDO ShortLinCodeDOInDB = shortLinkManager.findByShortLinCode(shortLinkCode);
if(ShortLinCodeDOInDB == null){
//C端处理
if (EventMessageType.SHORT_LINK_ADD_LINK.name().equalsIgnoreCase(messageType)) {
ShortLinkDO shortLinkDO = ShortLinkDO.builder()
.accountNo(accountNo)
.code(shortLinkCode)
.title(addRequest.getTitle())
.originalUrl(addRequest.getOriginalUrl())
.domain(domainDO.getValue())
.groupId(linkGroupDO.getId())
.expired(addRequest.getExpired())
.sign(originalUrlDigest)
.state(ShortLinkStateEnum.ACTIVE.name())
.del(0)
.build();
shortLinkManager.addShortLink(shortLinkDO);
return true;
} else if (EventMessageType.SHORT_LINK_ADD_MAPPING.name().equalsIgnoreCase(messageType)) {
//B端处理
GroupCodeMappingDO groupCodeMappingDO = GroupCodeMappingDO.builder()
.accountNo(accountNo)
.code(shortLinkCode)
.title(addRequest.getTitle())
.originalUrl(addRequest.getOriginalUrl())
.domain(domainDO.getValue())
.groupId(linkGroupDO.getId())
.expired(addRequest.getExpired())
.sign(originalUrlDigest)
.state(ShortLinkStateEnum.ACTIVE.name())
.del(0)
.build();
groupCodeMappingManager.add(groupCodeMappingDO);
return true;
}
}
return false;
}
/**
* 校验域名
*
* @param domainType
* @param domainId
* @param accountNo
* @return
*/
private DomainDO checkDomain(String domainType, Long domainId, Long accountNo) {
DomainDO domainDO;
if (DomainTypeEnum.CUSTOM.name().equalsIgnoreCase(domainType)) {
domainDO = domainManager.findById(domainId, accountNo);
} else {
domainDO = domainManager.findByDomainTypeAndID(domainId, DomainTypeEnum.OFFICIAL);
}
Assert.notNull(domainDO, "短链域名不合法");
return domainDO;
}
/**
* 校验组名
*
* @param groupId
* @param accountNo
* @return
*/
private LinkGroupDO checkLinkGroup(Long groupId, Long accountNo) {
LinkGroupDO linkGroupDO = linkGroupManager.detail(groupId, accountNo);
Assert.notNull(linkGroupDO, "组名不合法");
return linkGroupDO;
}
简介: MurmurHash短链码改进之生成固定库表位编码实战
/**
* 获取随机的后缀
* @return
*/
public static String getRandomTableSuffix(String code){
int hashCode = code.hashCode();
int num = Math.abs(hashCode) % tableSuffixList.size();
return tableSuffixList.get(num);
}
/**
* 获取随机的前缀
* @return
*/
public static String getRandomDBPrefix(String code){
int hashCode = code.hashCode();
int num = Math.abs(hashCode) % dbPrefixList.size();
return dbPrefixList.get(num);
}
简介: 同个URL生成不唯一短链码问题和解决方案编码实战
/**
* URL增加前缀
* @param url
* @return
*/
public static String addUrlPrefix(String url){
return IDUtil.geneSnowFlakeID()+"&"+url;
}
/**
* URL移除前缀
* @param url
* @return
*/
public static String removeUrlPrefix(String url){
String originalUrl = url.substring(url.indexOf("&")+1);
return originalUrl;
}
/**
* 如果短链码重复,则调用这个方法
* url前缀编号递增1,如果还是用雪花算法,则容易C和B端不一致,所以采用原先的id递增1
* @param url
* @return
*/
public static String addUrlPrefixVersion(String url){
String result = url.substring(0,url.indexOf("&"));
//原始地址
String originalUrl = url.substring(url.indexOf("&")+1);
//新id编号
Long newIdValue = Long.parseLong(result)+1;
return newIdValue+"&"+originalUrl;
}
简介:基于Redislua实现分布式锁
#-------redis连接配置-------
spring.redis.client-type=jedis
spring.redis.host=124.221.200.246
spring.redis.password=123456
spring.redis.port=6379
spring.redis.jedis.pool.max-active=100
spring.redis.jedis.pool.max-idle=100
spring.redis.jedis.pool.min-idle=100
spring.redis.jedis.pool.max-wait=60000
@Override
public boolean handlerAddShortLink(EventMessage eventMessage) {
Long accountNo = eventMessage.getAccountNo();
String messageType = eventMessage.getEventMessageType();
ShortLinkAddRequest addRequest = JsonUtil.json2Obj(eventMessage.getContent(), ShortLinkAddRequest.class);
//短链域名校验
DomainDO domainDO = checkDomain(addRequest.getDomainType(), addRequest.getDomainId(), accountNo);
//校验组是否合法
LinkGroupDO linkGroupDO = checkLinkGroup(addRequest.getGroupId(), accountNo);
//长链摘要
String originalUrlDigest = CommonUtil.MD5(addRequest.getOriginalUrl());
//短链码重复标记
boolean duplicateCodeFlag = false;
//生成短链码
String shortLinkCode = shortLinkComponent.createShortLinkCode(addRequest.getOriginalUrl());
//加锁
//key1是短链码,ARGV[1]是accountNo,ARGV[2]是过期时间
String script = "if redis.call('EXISTS',KEYS[1])==0 then redis.call('set',KEYS[1],ARGV[1]); redis.call('expire',KEYS[1],ARGV[2]); return 1;" +
" elseif redis.call('get',KEYS[1]) == ARGV[1] then return 2;" +
" else return 0; end;";
Long result = redisTemplate.execute(new
DefaultRedisScript<>(script, Long.class), Arrays.asList(shortLinkCode), accountNo, 100);
//加锁成功
if (result > 0) {
//C端处理
if (EventMessageType.SHORT_LINK_ADD_LINK.name().equalsIgnoreCase(messageType)) {
//先判断是否短链码被占用
ShortLinkDO shortLinCodeDOInDB = shortLinkManager.findByShortLinCode(shortLinkCode);
if (shortLinCodeDOInDB == null) {
ShortLinkDO shortLinkDO = ShortLinkDO.builder()
.accountNo(accountNo).code(shortLinkCode)
.title(addRequest.getTitle()).originalUrl(addRequest.getOriginalUrl())
.domain(domainDO.getValue()).groupId(linkGroupDO.getId())
.expired(addRequest.getExpired()).sign(originalUrlDigest)
.state(ShortLinkStateEnum.ACTIVE.name()).del(0).build();
shortLinkManager.addShortLink(shortLinkDO);
return true;
} else {
log.error("C端短链码重复:{}", eventMessage);
duplicateCodeFlag = true;
}
} else if (EventMessageType.SHORT_LINK_ADD_MAPPING.name().equalsIgnoreCase(messageType)) {
//B端处理
GroupCodeMappingDO groupCodeMappingDOInDB = groupCodeMappingManager.findByCodeAndGroupId(shortLinkCode, linkGroupDO.getId(), accountNo);
if (groupCodeMappingDOInDB == null) {
GroupCodeMappingDO groupCodeMappingDO = GroupCodeMappingDO.builder()
.accountNo(accountNo).code(shortLinkCode).title(addRequest.getTitle())
.originalUrl(addRequest.getOriginalUrl())
.domain(domainDO.getValue()).groupId(linkGroupDO.getId())
.expired(addRequest.getExpired()).sign(originalUrlDigest)
.state(ShortLinkStateEnum.ACTIVE.name()).del(0).build();
groupCodeMappingManager.add(groupCodeMappingDO);
return true;
} else {
log.error("B端短链码重复:{}", eventMessage);
duplicateCodeFlag = true;
}
}
} else {
//加锁失败,自旋100毫秒,再调用; 失败的可能是短链码已经被占用,需要重新生成
log.error("加锁失败:{}", eventMessage);
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
}
duplicateCodeFlag = true;
}
if (duplicateCodeFlag) {
String newOriginalUrl = CommonUtil.addUrlPrefixVersion(addRequest.getOriginalUrl());
addRequest.setOriginalUrl(newOriginalUrl);
eventMessage.setContent(JsonUtil.obj2Json(addRequest));
log.warn("短链码报错失败,重新生成:{}", eventMessage);
handlerAddShortLink(eventMessage);
}
return false;
}
##---------- 组+短链码mapping表,策略:分库+分表--------------
# 先进行水平分库,然后再水平分表, 水平分库策略,行表达式分片
spring.shardingsphere.sharding.tables.group_code_mapping.database-strategy.inline.sharding-column=account_no
spring.shardingsphere.sharding.tables.group_code_mapping.database-strategy.inline.algorithm-expression=ds$->{account_no % 2}
# 分表策略+行表达式分片
spring.shardingsphere.sharding.tables.group_code_mapping.actual-data-nodes=ds$->{0..1}.group_code_mapping_$->{0..1}
spring.shardingsphere.sharding.tables.group_code_mapping.table-strategy.inline.sharding-column=group_id
spring.shardingsphere.sharding.tables.group_code_mapping.table-strategy.inline.algorithm-expression=group_code_mapping_$->{group_id % 2}
短链服务-B端接口-分页查找短链开发实战
简介: 短链服务-B端接口-分页查找短链开发实战
@Data
public class ShortLinkPageRequest {
private int page;
private int size;
private long groupId;
}
/**
* 分页查找短链
*
* @return
*/
@RequestMapping("page")
public JsonData pageShortLinkByGroupId(@RequestBody ShortLinkPageRequest request) {
Map<String, Object> pageResult = shortLinkService.pageShortLinkByGroupId(request);
return JsonData.buildSuccess(pageResult);
}
@Override
public Map<String, Object> pageShortLinkByGroupId(Integer page, Integer size, Long accountNo, Long groupId) {
Page<GroupCodeMappingDO> pageInfo = new Page<>(page, size);
Page<GroupCodeMappingDO> groupCodeMappingDOPage = groupCodeMappingMapper.selectPage(pageInfo, new QueryWrapper<GroupCodeMappingDO>().eq("account_no", accountNo)
.eq("group_id", groupId));
Map<String, Object> pageMap = new HashMap<>(3);
pageMap.put("total_record", groupCodeMappingDOPage.getTotal());
pageMap.put("total_page", groupCodeMappingDOPage.getPages());
pageMap.put("current_data", groupCodeMappingDOPage.getRecords()
.stream().map(obj -> beanProcess(obj)).collect(Collectors.toList()));
return pageMap;
}
简介: 短链服务-删除和更新Controller层开发实战
@Data
public class ShortLinkDelRequest {
/**
* 组
*/
private Long groupId;
/**
* 映射id
*/
private Long mappingId;
/**
* 短链码
*/
private String code;
}
@Data
public class ShortLinkUpdateRequest {
/**
* 组
*/
private Long groupId;
/**
* 映射id
*/
private Long mappingId;
/**
* 短链码
*/
private String code;
/**
* 标题
*/
private String title;
/**
* 域名id
*/
private Long domainId;
/**
* 域名类型
*/
private String domainType;
}
/**
* 删除短链
* @param request
* @return
*/
@PostMapping("del")
public JsonData del(@RequestBody ShortLinkDelRequest request){
JsonData jsonData = shortLinkService.del(request);
return jsonData;
}
/**
* 更新短链
* @param request
* @return
*/
@PostMapping("update")
public JsonData update(@RequestBody ShortLinkUpdateRequest request){
JsonData jsonData = shortLinkService.update(request);
return jsonData;
}
@Override
public boolean handleUpdateShortLink(EventMessage eventMessage) {
Long accountNo = eventMessage.getAccountNo();
String messageType = eventMessage.getEventMessageType();
ShortLinkUpdateRequest request = JsonUtil.json2Obj(eventMessage.getContent(), ShortLinkUpdateRequest.class);
//校验短链域名
DomainDO domainDO = checkDomain(request.getDomainType(), request.getDomainId(), accountNo);
//C端处理
if(EventMessageType.SHORT_LINK_UPDATE_LINK.name().equalsIgnoreCase(messageType)){
ShortLinkDO shortLinkDO = ShortLinkDO.builder().code(request.getCode()).title(request.getTitle())
.domain(domainDO.getValue())
.accountNo(accountNo).build();
int rows = shortLinkManager.update(shortLinkDO);
log.debug("更新C端短链,rows={}",rows);
return true;
} else if(EventMessageType.SHORT_LINK_UPDATE_MAPPING.name().equalsIgnoreCase(messageType)){
//B端处理
GroupCodeMappingDO groupCodeMappingDO = GroupCodeMappingDO.builder().id(request.getMappingId()).groupId(request.getGroupId())
.accountNo(accountNo)
.title(request.getTitle())
.domain(domainDO.getValue())
.build();
int rows = groupCodeMappingManager.update(groupCodeMappingDO);
log.debug("更新B端短链,rows={}",rows);
return true;
}
return false;
}
@Override
public boolean handleDelShortLink(EventMessage eventMessage) {
Long accountNo = eventMessage.getAccountNo();
String messageType = eventMessage.getEventMessageType();
ShortLinkDelRequest request = JsonUtil.json2Obj(eventMessage.getContent(), ShortLinkDelRequest.class);
//C端解析
if(EventMessageType.SHORT_LINK_DEL_LINK.name().equalsIgnoreCase(messageType)){
ShortLinkDO shortLinkDO = ShortLinkDO.builder().code(request.getCode()).accountNo(accountNo).build();
int rows = shortLinkManager.del(shortLinkDO);
log.debug("删除C端短链:{}",rows);
return true;
}else if(EventMessageType.SHORT_LINK_DEL_MAPPING.name().equalsIgnoreCase(messageType)){
//B端处理
GroupCodeMappingDO groupCodeMappingDO = GroupCodeMappingDO.builder()
.id(request.getMappingId()).accountNo(accountNo)
.groupId(request.getGroupId()).build();
int rows = groupCodeMappingManager.del(groupCodeMappingDO);
log.debug("删除B端短链:{}",rows);
return true;
}
return false;
}
@Component
@Slf4j
@RabbitListener(queuesToDeclare = { @Queue("short_link.del.link.queue") })
public class ShortLinkDelLinkMQListener {
@Autowired
private ShortLinkService shortLinkService;
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkDelLinkMQListener message消息内容:{}",message);
try{
eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_DEL_LINK.name());
shortLinkService.handleDelShortLink(eventMessage);
}catch (Exception e){
//处理业务异常,还有进行其他操作,比如记录失败原因
log.error("消费失败:{}",eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}",eventMessage);
}
}
@Component
@Slf4j
@RabbitListener(queuesToDeclare = { @Queue("short_link.del.mapping.queue") })
public class ShortLinkDelMappingMQListener {
@Autowired
private ShortLinkService shortLinkService;
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkDelMappingMQListener message消息内容:{}",message);
try{
eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_DEL_MAPPING.name());
shortLinkService.handleDelShortLink(eventMessage);
}catch (Exception e){
//处理业务异常,还有进行其他操作,比如记录失败原因
log.error("消费失败:{}",eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}",eventMessage);
}
}
@Component
@Slf4j
@RabbitListener(queuesToDeclare = { @Queue("short_link.update.link.queue") })
public class ShortLinkUpdateLinkMQListener {
@Autowired
private ShortLinkService shortLinkService;
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkUpdateLinkMQListener message消息内容:{}",message);
try{
eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_UPDATE_LINK.name());
shortLinkService.handleUpdateShortLink(eventMessage);
}catch (Exception e){
//处理业务异常,还有进行其他操作,比如记录失败原因
log.error("消费失败:{}",eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}",eventMessage);
}
}
@Component
@Slf4j
@RabbitListener(queuesToDeclare = { @Queue("short_link.update.mapping.queue") })
public class ShortLinkUpdateMappingMQListener {
@Autowired
private ShortLinkService shortLinkService;
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkUpdateMappingMQListener message消息内容:{}",message);
try{
eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_UPDATE_MAPPING.name());
shortLinkService.handleUpdateShortLink(eventMessage);
}catch (Exception e){
//处理业务异常,还有进行其他操作,比如记录失败原因
log.error("消费失败:{}",eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}",eventMessage);
}
}
简介: 流量包商品服务-数据库表介绍
CREATE TABLE `product` (
`id` bigint NOT NULL,
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '商品标题',
`detail` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '详情',
`img` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '图片',
`level` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '产品层级:FIRST青铜、SECOND黄金、THIRD钻石',
`old_amount` decimal(16,0) DEFAULT NULL COMMENT '原价',
`amount` decimal(16,0) DEFAULT NULL COMMENT '现价',
`plugin_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '工具类型 short_link、qrcode',
`day_times` int DEFAULT NULL COMMENT '日次数:短链类型',
`total_times` int DEFAULT NULL COMMENT '总次数:活码才有',
`valid_day` int DEFAULT NULL COMMENT '有效天数',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
INSERT INTO `dcloud_shop`.`product` (`id`, `title`, `detail`, `img`, `level`, `old_amount`, `amount`, `plugin_type`, `day_times`, `total_times`, `valid_day`, `gmt_modified`, `gmt_create`) VALUES (1, '青铜会员-默认', '数据查看支持||日生成短链{{dayTimes}}次||限制跳转50次||默认域名', NULL, 'FIRST', 19, 0, 'SHORT_LINK', 2, NULL, 1, '2021-10-14 17:33:44', '2021-10-11 10:49:35');
INSERT INTO `dcloud_shop`.`product` (`id`, `title`, `detail`, `img`, `level`, `old_amount`, `amount`, `plugin_type`, `day_times`, `total_times`, `valid_day`, `gmt_modified`, `gmt_create`) VALUES (2, '黄金会员-月度', '数据查看支持||日生成短链{{dayTimes}}次||限制不限制||默认域名', NULL, 'SECOND', 99, 1, 'SHORT_LINK', 5, NULL, 30, '2021-10-19 14:36:28', '2021-10-11 10:57:47');
INSERT INTO `dcloud_shop`.`product` (`id`, `title`, `detail`, `img`, `level`, `old_amount`, `amount`, `plugin_type`, `day_times`, `total_times`, `valid_day`, `gmt_modified`, `gmt_create`) VALUES (3, '黑金会员-月度', '数据查看支持||日生成短链{{dayTimes}}次||限制不限制||自定义域名', NULL, 'THIRD', 199, 2, 'SHORT_LINK', 8, NULL, 30, '2021-10-19 14:36:30', '2021-10-11 11:01:13');
简介: 流量包商品服务-商品列表和详情接口链路开发
@Data
@EqualsAndHashCode(callSuper = false)
public class ProductVO {
private Long id;
/**
* 商品标题
*/
private String title;
/**
* 详情
*/
private String detail;
/**
* 图片
*/
private String img;
/**
* 产品层级:FIRST青铜、SECOND黄金、THIRD钻石
*/
private String level;
/**
* 原价
*/
private BigDecimal oldAmount;
/**
* 现价
*/
private BigDecimal amount;
/**
* 工具类型 short_link、qrcode
*/
private String pluginType;
/**
* 日次数:短链类型
*/
private Integer dayTimes;
/**
* 总次数:活码才有
*/
private Integer totalTimes;
/**
* 有效天数
*/
private Integer validDay;
}
/**
* 查看商品列表接口
* @return
*/
@GetMapping("list")
public JsonData list(){
List<ProductVO> list = productService.list();
return list.size()>1?JsonData.buildSuccess(list):JsonData.buildError("失败");
}
/**
* 查看商品详情
* @param productId
* @return
*/
@GetMapping("detail/{product_id}")
public JsonData detail(@PathVariable("product_id") long productId){
ProductVO productVO = productService.findDetailById(productId);
return JsonData.buildSuccess(productVO);
}
List<ProductVO> list();
ProductVO findDetailById(long productId);
@Service
@Slf4j
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductManager productManager;
@Override
public List<ProductVO> list() {
List<ProductDO> list = productManager.list();
List<ProductVO> collect = list.stream().map( obj -> beanProcess(obj) ).collect(Collectors.toList());
return collect;
}
@Override
public ProductVO findDetailById(long productId) {
ProductDO productDO = productManager.findDetailById(productId);
ProductVO productVO = beanProcess(productDO);
return productVO;
}
private ProductVO beanProcess(ProductDO productDO) {
ProductVO productVO = new ProductVO();
BeanUtils.copyProperties(productDO, productVO);
return productVO;
}
}
简介: 流量包订单-数据库表介绍和实体类生成
CREATE TABLE `product_order` (
`id` bigint NOT NULL,
`product_id` bigint DEFAULT NULL COMMENT '订单类型',
`product_title` varchar(64) DEFAULT NULL COMMENT '商品标题',
`product_amount` decimal(16,2) DEFAULT NULL COMMENT '商品单价',
`product_snapshot` varchar(2048) DEFAULT NULL COMMENT '商品快照',
`buy_num` int DEFAULT NULL COMMENT '购买数量',
`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 '账号昵称',
`account_no` bigint DEFAULT NULL COMMENT '用户id',
`del` int DEFAULT '0' COMMENT '0表示未删除,1表示已经删除',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`bill_type` varchar(32) DEFAULT NULL COMMENT '发票类型:0->不开发票;1->电子发票;2->纸质发票',
`bill_header` varchar(200) DEFAULT NULL COMMENT '发票抬头',
`bill_content` varchar(200) DEFAULT NULL COMMENT '发票内容',
`bill_receiver_phone` varchar(32) DEFAULT NULL COMMENT '发票收票人电话',
`bill_receiver_email` varchar(200) DEFAULT NULL COMMENT '发票收票人邮箱',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_query` (`out_trade_no`,`account_no`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
简介: 流量包订单-数据库表分库分表讲解和配置
server.port=8005
spring.application.name=dcloud-shop-service
#----------服务注册和发现--------------
spring.cloud.nacos.discovery.server-addr=124.221.200.246:8848:8848
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
#-------分库分表数据源配置-------
spring.shardingsphere.datasource.names=ds0
spring.shardingsphere.datasource.ds0.connectionTimeoutMilliseconds=30000
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.ds0.idleTimeoutMilliseconds=60000
spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://124.221.200.246:3306/dcloud_shop?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
spring.shardingsphere.datasource.ds0.maintenanceIntervalMilliseconds=30000
spring.shardingsphere.datasource.ds0.maxLifetimeMilliseconds=1800000
spring.shardingsphere.datasource.ds0.maxPoolSize=50
spring.shardingsphere.datasource.ds0.minPoolSize=50
spring.shardingsphere.datasource.ds0.password=123456
spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.props.sql.show=true
#----------配置默认数据库,比如短链域名,不分库分表--------------
spring.shardingsphere.sharding.default-data-source-name=ds0
#默认id生成策略
spring.shardingsphere.sharding.default-key-generator.column=id
spring.shardingsphere.sharding.default-key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.default-key-generator.props.worker.id=${workerId}
# 指定product_order表的数据分布情况,配置数据节点,行表达式标识符使用 ${...} 或 $->{...},但前者与 Spring 本身的文件占位符冲突,所以在 Spring 环境中建议使用 $->{...}
spring.shardingsphere.sharding.tables.product_order.actual-data-nodes=ds0.product_order_$->{0..1}
#水平分表策略+行表达式分片
spring.shardingsphere.sharding.tables.product_order.table-strategy.inline.algorithm-expression=product_order_$->{ account_no % 2 }
spring.shardingsphere.sharding.tables.product_order.table-strategy.inline.sharding-column=account_no
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
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;
/**
* 用户标识
*/
private Long accountNo;
}
@Data
@EqualsAndHashCode(callSuper = false)
public class ProductOrderVO implements Serializable {
private Long id;
/**
* 订单类型
*/
private Long productId;
/**
* 商品标题
*/
private String productTitle;
/**
* 商品单价
*/
private BigDecimal productAmount;
/**
* 商品快照
*/
private String productSnapshot;
/**
* 购买数量
*/
private Integer buyNum;
/**
* 订单唯一标识
*/
private String outTradeNo;
/**
* NEW 未支付订单,PAY已经支付订单,CANCEL超时取消订单
*/
private String state;
/**
* 订单生成时间
*/
private Date createTime;
/**
* 订单总金额
*/
private BigDecimal totalAmount;
/**
* 订单实际支付价格
*/
private BigDecimal payAmount;
/**
* 支付类型,微信-银行-支付宝
*/
private String payType;
/**
* 账号昵称
*/
private String nickname;
/**
* 用户id
*/
private Long accountNo;
/**
* 0表示未删除,1表示已经删除
*/
private Integer del;
/**
* 更新时间
*/
private Date gmtModified;
/**
* 创建时间
*/
private Date gmtCreate;
/**
* 发票类型:0->不开发票;1->电子发票;2->纸质发票
*/
private String billType;
/**
* 发票抬头
*/
private String billHeader;
/**
* 发票内容
*/
private String billContent;
/**
* 发票收票人电话
*/
private String billReceiverPhone;
/**
* 发票收票人邮箱
*/
private String billReceiverEmail;
}
public enum BillTypeEnum {
/**
* 不用发票
*/
NO_BILL,
/**
* 纸质发票
*/
PAPER_BILL,
/**
* 电子发票
*/
ELE_BILL;
}
public enum ClientTypeEnum {
/**
* app支付
*/
APP,
/**
* PC网页
*/
PC,
/**
* 移动端H5
*/
H5;
}
public enum ProductOrderPayTypeEnum {
WECHAT_APY,
ALI_PAY,
BANK;
}
public enum ProductOrderStateEnum {
/**
* 未支付
*/
NEW,
/**
* 已经支付
*/
PAY,
/**
* 超时取消
*/
CANCEL;
}
public class TimeConstant {
/**
* 默认30分钟超时未支付,单位毫秒
*/
public static final long ORDER_PAY_TIMEOUT_MILLS =1000 * 60 * 30;
}
@Data
public class ConfirmOrderRequest {
/**
* 商品id
*/
private Long productId;
/**
* 购买数量
*/
private Integer buyNum;;
/**
* 终端类型
*/
private String clientType;
/**
* 支付类型,微信-银行-支付宝
*/
private String payType;
/**
* 订单总金额
*/
private BigDecimal totalAmount;
/**
* 订单实际支付价格
*/
private BigDecimal payAmount;
/**
* 防重令牌
*/
private String token;
/**
* 发票类型:0->不开发票;1->电子发票;2->纸质发票
*/
private String billType;
/**
* 发票抬头
*/
private String billHeader;
/**
* 发票内容
*/
private String billContent;
/**
* 发票收票人电话
*/
private String billReceiverPhone;
/**
* 发票收票人邮箱
*/
private String billReceiverEmail;
}
@RestController
@RequestMapping("/api/order/v1")
@Slf4j
public class ProductOrderController {
@Autowired
private ProductOrderService productOrderService;
/**
* 分页接口
*
* @return
*/
@GetMapping("page")
public JsonData page(
@RequestParam(value = "page", defaultValue = "1") int page,
@RequestParam(value = "size", defaultValue = "10") int size,
@RequestParam(value = "state", required = false) String state
) {
Map<String, Object> pageResult = productOrderService.page(page, size, state);
return JsonData.buildSuccess(pageResult);
}
/**
* 查询订单状态
*
* @param outTradeNo
* @return
*/
@GetMapping("query_state")
public JsonData queryState(@RequestParam(value = "out_trade_no") String outTradeNo) {
String state = productOrderService.queryProductOrderState(outTradeNo);
return StringUtils.isBlank(state) ?
JsonData.buildResult(BizCodeEnum.ORDER_CONFIRM_NOT_EXIST) : JsonData.buildSuccess(state);
}
/**
* 下单接口
* @param orderRequest
* @param response
*/
@PostMapping("confirm")
public void confirmOrder(@RequestBody ConfirmOrderRequest orderRequest, HttpServletResponse response) {
JsonData jsonData = productOrderService.confirmOrder(orderRequest);
if (jsonData.getCode() == 0) {
//端类型
String client = orderRequest.getClientType();
//支付类型
String payType = orderRequest.getPayType();
//如果是支付宝支付,跳转网页,sdk除非
if (payType.equalsIgnoreCase(ProductOrderPayTypeEnum.ALI_PAY.name())) {
if (client.equalsIgnoreCase(ClientTypeEnum.PC.name())) {
CommonUtil.sendHtmlMessage(response, jsonData);
} else if (client.equalsIgnoreCase(ClientTypeEnum.APP.name())) {
} else if (client.equalsIgnoreCase(ClientTypeEnum.H5.name())) {
}
} else if (payType.equalsIgnoreCase(ProductOrderPayTypeEnum.WECHAT_APY.name())) {
//微信支付
CommonUtil.sendJsonMessage(response, jsonData);
}
} else {
log.error("创建订单失败{}", jsonData.toString());
CommonUtil.sendJsonMessage(response, jsonData);
}
}
}
public interface ProductOrderService {
Map<String,Object> page(int page, int size, String state);
String queryProductOrderState(String outTradeNo);
JsonData confirmOrder(ConfirmOrderRequest orderRequest);
}
@Service
@Slf4j
public class ProductOrderServiceImpl implements ProductOrderService {
@Autowired
private ProductOrderManager productOrderManager;
@Autowired
private ProductManager productManager;
@Override
public Map<String, Object> page(int page, int size, String state) {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
Map<String, Object> pageResult = productOrderManager.page(page, size, accountNo, state);
return pageResult;
}
@Override
public String queryProductOrderState(String outTradeNo) {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
ProductOrderDO productOrderDO = productOrderManager.findByOutTradeNoAndAccountNo(outTradeNo, accountNo);
if(productOrderDO == null){
return "";
}else {
return productOrderDO.getState();
}
}
/**
* * 重防提交(TODO)
* * 获取最新的流量包价格
* * 订单验价
* * 如果有优惠券或者其他抵扣
* * 验证前端显示和后台计算价格
* * 创建订单对象保存数据库
* * 发送延迟消息-用于自动关单(TODO)
* * 创建支付信息-对接三方支付(TODO)
* * 回调更新订单状态(TODO)
* * 支付成功创建流量包(TODO)
* @param orderRequest
* @return
*/
@Override
@Transactional
public JsonData confirmOrder(ConfirmOrderRequest orderRequest) {
LoginUser loginUser = LoginInterceptor.threadLocal.get();
String orderOutTradeNo = CommonUtil.getStringNumRandom(32);
ProductDO productDO = productManager.findDetailById(orderRequest.getProductId());
//验证价格
this.checkPrice(productDO,orderRequest);
//创建订单
ProductOrderDO productOrderDO = this.saveProductOrder(orderRequest,loginUser,orderOutTradeNo,productDO);
//创建支付对象
PayInfoVO payInfoVO = PayInfoVO.builder().accountNo(loginUser.getAccountNo())
.outTradeNo(orderOutTradeNo).clientType(orderRequest.getClientType())
.payType(orderRequest.getPayType()).title(productDO.getTitle()).description("")
.payFee(orderRequest.getPayAmount()).orderPayTimeoutMills(TimeConstant.ORDER_PAY_TIMEOUT_MILLS)
.build();
//发送延迟消息 TODO
//调用支付信息 TODO
return null;
}
private ProductOrderDO saveProductOrder(ConfirmOrderRequest orderRequest, LoginUser loginUser, String orderOutTradeNo, ProductDO productDO) {
ProductOrderDO productOrderDO = new ProductOrderDO();
//设置用户信息
productOrderDO.setAccountNo(loginUser.getAccountNo());
productOrderDO.setNickname(loginUser.getUsername());
//设置商品信息
productOrderDO.setProductId(productDO.getId());
productOrderDO.setProductTitle(productDO.getTitle());
productOrderDO.setProductSnapshot(JsonUtil.obj2Json(productDO));
productOrderDO.setProductAmount(productDO.getAmount());
//设置订单信息
productOrderDO.setBuyNum(orderRequest.getBuyNum());
productOrderDO.setOutTradeNo(orderOutTradeNo);
productOrderDO.setCreateTime(new Date());
productOrderDO.setDel(0);
//发票信息
productOrderDO.setBillType(BillTypeEnum.valueOf(orderRequest.getBillType()).name());
productOrderDO.setBillHeader(orderRequest.getBillHeader());
productOrderDO.setBillReceiverPhone(orderRequest.getBillReceiverPhone());
productOrderDO.setBillReceiverEmail(orderRequest.getBillReceiverEmail());
productOrderDO.setBillContent(orderRequest.getBillContent());
//实际支付总价
productOrderDO.setPayAmount(orderRequest.getPayAmount());
//总价,没使用优惠券
productOrderDO.setTotalAmount(orderRequest.getTotalAmount());
//订单状态
productOrderDO.setState(ProductOrderStateEnum.NEW.name());
//支付类型
productOrderDO.setPayType(ProductOrderPayTypeEnum.valueOf(orderRequest.getPayType()).name());
//插入数据库
productOrderManager.add(productOrderDO);
return productOrderDO;
}
private void checkPrice(ProductDO productDO, ConfirmOrderRequest orderRequest) {
//后端计算价格
BigDecimal bizTotal = BigDecimal.valueOf(orderRequest.getBuyNum()).multiply(productDO.getAmount());
//前端传递总价和后端计算总价格是否一致, 如果有优惠券,也在这里进行计算
if( bizTotal.compareTo(orderRequest.getPayAmount()) !=0 ){
log.error("验证价格失败{}",orderRequest);
throw new BizException(BizCodeEnum.ORDER_CONFIRM_PRICE_FAIL);
}
}
}
订单防重提交-自定义注解开发
/**
* 自定义防重提交
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
/**
* 防重提交,支持两种,一个是方法参数,一个是令牌
*/
enum Type { PARAM, TOKEN }
/**
* 默认防重提交,是方法参数
* @return
*/
Type limitType() default Type.PARAM;
/**
* 加锁过期时间,默认是5秒
* @return
*/
long lockTime() default 5;
}
public class RedisKey {
// public static final Locale CHECK_CODE_KEY = ;
/**
* 第一个类型
* 第二个唯一标识
*/
public static final String CHECK_CODE_KEY = "code:%s:%s";
/**
* 提交订单令牌的缓存key
*/
public static final String SUBMIT_ORDER_TOKEN_KEY = "order:submit:%s:%s";
}
@Configuration
public class RedissionConfiguration {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private String redisPort;
@Value("${spring.redis.password}")
private String redisPwd;
/**
* 配置分布式锁的redisson
* @return
*/
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
//单机方式
config.useSingleServer().setPassword(redisPwd).setAddress("redis://"+redisHost+":"+redisPort);
//集群
//config.useClusterServers().addNodeAddress("redis://192.31.21.1:6379","redis://192.31.21.2:6379")
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
/**
* 集群模式
* 备注:可以用"rediss://"来启用SSL连接
*/
/*@Bean
public RedissonClient redissonClusterClient() {
Config config = new Config();
config.useClusterServers().setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒
.addNodeAddress("redis://127.0.0.1:7000")
.addNodeAddress("redis://127.0.0.1:7002");
RedissonClient redisson = Redisson.create(config);
return redisson;
}*/
}
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RedissonClient redissonClient;
/**
* 定义 @Pointcut注解表达式, 通过特定的规则来筛选连接点, 就是Pointcut,选中那几个你想要的方法
* 在程序中主要体现为书写切入点表达式(通过通配、正则表达式)过滤出特定的一组 JointPoint连接点
* <p>
* 方式一:@annotation:当执行的方法上拥有指定的注解时生效(我们采用这)
* 方式二:execution:一般用于指定方法的执行
*/
@Pointcut("@annotation(repeatSubmit)")
public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {
}
/**
* 环绕通知, 围绕着方法执行
*
* @param joinPoint
* @param noRepeatSubmit
* @return
* @throws Throwable
* @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。
* <p>
* 方式一:单用 @Around("execution(* net.xdclass.controller.*.*(..))")可以
* 方式二:用@Pointcut和@Around联合注解也可以(我们采用这个)
* <p>
* <p>
* 两种方式
* 方式一:加锁 固定时间内不能重复提交
* <p>
* 方式二:先请求获取token,这边再删除token,删除成功则是第一次提交
*/
@Around("pointCutNoRepeatSubmit(repeatSubmit)")
public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
//用于记录成功或者失败
boolean res = false;
//防重提交类型
String type = repeatSubmit.limitType().name();
if (type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {
//方式一,参数形式防重提交
long lockTime = repeatSubmit.lockTime();
String ipAddr = CommonUtil.getIpAddr(request);
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
Method method = methodSignature.getMethod();
String className = method.getDeclaringClass().getName();
String key = "order-server:repeat_submit:"+CommonUtil.MD5(String.format("%s-%s-%s-%s",ipAddr,className,method,accountNo));
//加锁
//res = redisTemplate.opsForValue().setIfAbsent(key, "1", lockTime, TimeUnit.SECONDS);
RLock lock = redissonClient.getLock(key);
// 尝试加锁,最多等待0秒,上锁以后5秒自动解锁 [lockTime默认为5s, 可以自定义]
res = lock.tryLock(0,lockTime,TimeUnit.SECONDS);
} else {
//方式二,令牌形式防重提交
String requestToken = request.getHeader("request-token");
if (StringUtils.isBlank(requestToken)) {
throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_EQUAL_FAIL);
}
String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY, accountNo, requestToken);
/**
* 提交表单的token key
* 方式一:不用lua脚本获取再判断,之前是因为 key组成是 order:submit:accountNo, value是对应的token,所以需要先获取值,再判断
* 方式二:可以直接key是 order:submit:accountNo:token,然后直接删除成功则完成
*/
res = redisTemplate.delete(key);
}
if (!res) {
log.error("请求重复提交");
return null;
}
log.info("环绕通知执行前");
Object obj = joinPoint.proceed();
log.info("环绕通知执行后");
return obj;
}
}
简介:RabbitMQ死信队列-延迟消息知识点回顾
消息有哪几种情况成为死信
下单接口-超时关闭订单消费者
@Component
@Slf4j
@RabbitListener(queuesToDeclare = {@Queue("order.close.queue")})
public class ProductOrderMQListener {
@Autowired
private ProductOrderService productOrderService;
@RabbitHandler
public void productOrderHandler(EventMessage eventMessage, Message message, Channel channel){
log.info("监听到消息ProductOrderMQListener messsage消息内容:{}",message);
try{
//关闭订单
productOrderService.closeProductOrder(eventMessage);
}catch (Exception e){
log.error("消费者失败:{}",eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}",eventMessage);
}
}
/**
* //延迟消息的时间 需要比订单过期 时间长一点,这样就不存在查询的时候,用户还能支付成功
*
* //查询订单是否存在,如果已经支付则正常结束
* //如果订单未支付,主动调用第三方支付平台查询订单状态
* //确认未支付,本地取消订单
* //如果第三方平台已经支付,主动的把订单状态改成已支付,造成该原因的情况可能是支付通道回调有问题,然后触发支付后的动作,如何触发?RPC还是?
* @param eventMessage
*/
@Override
public boolean closeProductOrder(EventMessage eventMessage) {
String outTradeNo = eventMessage.getBizId();
Long accountNo = eventMessage.getAccountNo();
ProductOrderDO productOrderDO = productOrderManager.findByOutTradeNoAndAccountNo(outTradeNo, accountNo);
if(productOrderDO == null){
//订单不存在
log.warn("订单不存在");
return true;
}
if(productOrderDO.getState().equalsIgnoreCase(ProductOrderStateEnum.PAY.name())){
//已经支付
log.info("直接确认消息,订单已经支付:{}",eventMessage);
return true;
}
//未支付,需要向第三方支付平台查询状态
if(productOrderDO.getState().equalsIgnoreCase(ProductOrderStateEnum.NEW.name())){
//向第三方查询状态
PayInfoVO payInfoVO = new PayInfoVO();
payInfoVO.setPayType(productOrderDO.getPayType());
payInfoVO.setOutTradeNo(outTradeNo);
payInfoVO.setAccountNo(accountNo);
//TODO 需要向第三方支付平台查询状态
String payResult = "";
if(StringUtils.isBlank(payResult)){
//如果为空,则未支付成功,本地取消订单
productOrderManager.updateOrderPayState(outTradeNo,accountNo,ProductOrderStateEnum.CANCEL.name(),ProductOrderStateEnum.NEW.name());
log.info("未支付成功,本地取消订单:{}",eventMessage);
}else {
//支付成功,主动把订单状态更新成支付
log.warn("支付成功,但是微信回调通知失败,需要排查问题:{}",eventMessage);
productOrderManager.updateOrderPayState(outTradeNo,accountNo,ProductOrderStateEnum.PAY.name(),ProductOrderStateEnum.NEW.name());
//触发支付成功后的逻辑, TODO
}
}
return true;
}
简介:微信支付-Maven依赖加入和代码参数准备
#商户号
pay.wechat.mch-id=1601644442
#公众号id 需要和商户号绑定
pay.wechat.wx-pay-appid=wx5beac15ca207c40c
#商户证书序列号,需要和证书对应
pay.wechat.mch-serial-no=7064ADC5FE84CA2A3DDE71A692E39602DEB96E61
#api密钥
pay.wechat.api-v3-key=peYcTwRF581UOdaUqoPOeHzJ8FgHgsnJ
#商户私钥路径(微信服务端会根据证书序列号,找到证书获取公钥进行解密数据)
pay.wechat.private-key-path=classpath:/cert/apiclient_key.pem
#支付成功页面跳转
pay.wechat.success-return-url=https://classes.net
#支付成功,回调通知
pay.wechat.callback-url=http://api.open1024.com/shop-server/api/callback/order/v1/wechat
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.3.0</version>
</dependency>
@Data
@Configuration
@ConfigurationProperties(prefix = "pay.wechat")
public class WechatPayConfig {
/**
* 商户号
*/
private String mchId;
/**
* 公众号id 需要和商户号绑定
*/
private String wxPayAppid;
/**
* 商户证书序列号,需要和证书对应
*/
private String mchSerialNo;
/**
* API V3密钥
*/
private String apiV3Key;
/**
* 商户私钥路径(微信服务端会根据证书序列号,找到证书获取公钥进行解密数据)
*/
private String privateKeyPath;
/**
* 支付成功页面跳转
*/
private String successReturnUrl;
/**
* 支付成功,回调通知
*/
private String callbackUrl;
}
简介:微信支付-商户私钥证书代码读取开发实战
@Configuration
@Slf4j
public class PayBeanConfig {
@Autowired
private WechatPayConfig payConfig;
/**
* 加载秘钥
*
* @return
* @throws IOException
*/
public PrivateKey getPrivateKey() throws IOException {
InputStream inputStream = new ClassPathResource(payConfig.getPrivateKeyPath()
.replace("classpath:", "")).getInputStream();
String content = new BufferedReader(new InputStreamReader(inputStream))
.lines().collect(Collectors.joining(System.lineSeparator()));
try {
String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
KeyFactory kf = KeyFactory.getInstance("RSA");
PrivateKey finalPrivateKey = kf.generatePrivate(
new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
return finalPrivateKey;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持RSA", e);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("无效的密钥格式");
}
}
/**
* 定时获取微信签名验证器,自动获取微信平台证书(证书里面包括微信平台公钥)
*
* @return
*/
@Bean
public ScheduledUpdateCertificatesVerifier getCertificatesVerifier() throws IOException {
// 使用定时更新的签名验证器,不需要传入证书
ScheduledUpdateCertificatesVerifier verifier = null;
verifier = new ScheduledUpdateCertificatesVerifier(
new WechatPay2Credentials(payConfig.getMchId(),
new PrivateKeySigner(payConfig.getMchSerialNo(),
getPrivateKey())),
payConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
return verifier;
}
/**
* 获取http请求对象,会自动的处理签名和验签,
* 并进行证书自动更新
*
* @return
*/
@Bean("wechatPayClient")
public CloseableHttpClient getWechatPayClient(ScheduledUpdateCertificatesVerifier verifier) throws IOException {
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(payConfig.getMchId(),payConfig.getMchSerialNo() , getPrivateKey())
.withValidator(new WechatPay2Validator(verifier));
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
return httpClient;
}
}
public class WechatPayApi {
/**
* 微信支付主机地址
*/
public static final String HOST = "https://api.mch.weixin.qq.com";
/**
* Native下单
*/
public static final String NATIVE_ORDER = HOST+ "/v3/pay/transactions/native";
/**
* Native订单状态查询, 根据商户订单号查询
*/
public static final String NATIVE_QUERY = HOST+ "/v3/pay/transactions/out-trade-no/%s?mchid=%s";
/**
* 关闭订单接口
*/
public static final String NATIVE_CLOSE_ORDER = HOST+ "/v3/pay/transactions/out-trade-no/%s/close";
/**
* 申请退款接口
*/
public static final String NATIVE_REFUND_ORDER = HOST+ "/v3/refund/domestic/refunds";
/**
* 退款状态查询接口
*/
public static final String NATIVE_REFUND_QUERY = HOST+ "/v3/refund/domestic/refunds/%s";
}
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ShopApplication.class)
@Slf4j
public class WechatPayTest {
@Autowired
private PayBeanConfig payBeanConfig;
@Autowired
private WechatPayConfig payConfig;
@Autowired
private CloseableHttpClient wechatPayClient;
@Test
public void testLoadPrivateKey() throws IOException {
log.info(payBeanConfig.getPrivateKey().getAlgorithm());
}
/**
* 快速验证统一下单接口
* @throws IOException
*/
@Test
public void testNativeOrder() throws IOException {
String outTradeNo = CommonUtil.getStringNumRandom(32);
/**
* {
* "mchid": "1900006XXX",
* "out_trade_no": "native12177525012014070332333",
* "appid": "wxdace645e0bc2cXXX",
* "description": "Image形象店-深圳腾大-QQ公仔",
* "notify_url": "https://weixin.qq.com/",
* "amount": {
* "total": 1,
* "currency": "CNY"
* }
* }
*/
JSONObject payObj = new JSONObject();
payObj.put("mchid",payConfig.getMchId());
payObj.put("out_trade_no",outTradeNo);
payObj.put("appid",payConfig.getWxPayAppid());
payObj.put("description","老王和冰冰的红包");
payObj.put("notify_url",payConfig.getCallbackUrl());
//订单总金额,单位为分。
JSONObject amountObj = new JSONObject();
amountObj.put("total",100);
amountObj.put("currency","CNY");
payObj.put("amount",amountObj);
//附属参数,可以用在回调
payObj.put("attach","{\"accountNo\":"+888+"}");
String body = payObj.toJSONString();
log.info("请求参数:{}",body);
StringEntity entity = new StringEntity(body,"utf-8");
entity.setContentType("application/json");
HttpPost httpPost = new HttpPost(WechatPayApi.NATIVE_ORDER);
httpPost.setHeader("Accept","application/json");
httpPost.setEntity(entity);
try(CloseableHttpResponse response = wechatPayClient.execute(httpPost)){
//响应码
int statusCode = response.getStatusLine().getStatusCode();
//响应体
String responseStr = EntityUtils.toString(response.getEntity());
log.info("下单响应码:{},响应体:{}",statusCode,responseStr);
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 根据商户号订单号查询订单支付状态
*
* {"amount":{"payer_currency":"CNY","total":100},"appid":"wx5beac15ca207c40c",
* "mchid":"1601644442","out_trade_no":"fRAv2Ccpd8GxNEpKAt36X0fdL7WYbn0F",
* "promotion_detail":[],"scene_info":{"device_id":""},
* "trade_state":"NOTPAY","trade_state_desc":"订单未支付"}
*
* @throws IOException
*/
@Test
public void testNativeQuery() throws IOException {
String outTradeNo = "fRAv2Ccpd8GxNEpKAt36X0fdL7WYbn0F";
String url = String.format(WechatPayApi.NATIVE_QUERY,outTradeNo,payConfig.getMchId());
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("Accept","application/json");
try(CloseableHttpResponse response = wechatPayClient.execute(httpGet)){
//响应码
int statusCode = response.getStatusLine().getStatusCode();
//响应体
String responseStr = EntityUtils.toString(response.getEntity());
log.info("查询响应码:{},响应体:{}",statusCode,responseStr);
}catch (Exception e){
e.printStackTrace();
}
}
@Test
public void testNativeCloseOrder() throws IOException {
String outTradeNo = "fRAv2Ccpd8GxNEpKAt36X0fdL7WYbn0F";
JSONObject payObj = new JSONObject();
payObj.put("mchid",payConfig.getMchId());
String body = payObj.toJSONString();
log.info("请求参数:{}",body);
//将请求参数设置到请求对象中
StringEntity entity = new StringEntity(body,"utf-8");
entity.setContentType("application/json");
String url = String.format(WechatPayApi.NATIVE_CLOSE_ORDER,outTradeNo);
HttpPost httpPost = new HttpPost(url);
httpPost.setHeader("Accept","application/json");
httpPost.setEntity(entity);
try(CloseableHttpResponse response = wechatPayClient.execute(httpPost)){
//响应码
int statusCode = response.getStatusLine().getStatusCode();
log.info("关闭订单响应码:{},无响应体",statusCode);
}catch (Exception e){
e.printStackTrace();
}
}
/**
* {"amount":{"currency":"CNY","discount_refund":0,"from":[],
*
* "payer_refund":10,"payer_total":100,"refund":10,
* "settlement_refund":10,"settlement_total":100,"total":100},
* "channel":"ORIGINAL","create_time":"2022-01-18T14:38:20+08:00",
* "funds_account":"AVAILABLE","out_refund_no":"unln6N45W2dJuhhDbe9zCx9m5wxHU9xT",
*
* "out_trade_no":"XH5U0QvInSNK2GPPwAMl2pVRmkKYPYzi","promotion_detail":[],
* "refund_id":"50300400552022011816562288005","status":"PROCESSING",
* "transaction_id":"4200001374202201184851061356","user_received_account":"民生银行信用卡5022"}
*
* @throws IOException
*/
@Test
public void testNativeRefundOrder() throws IOException {
String outTradeNo = "HkPfPY0q3GwuYYUou0wfUnX34iRNYxXX";
String refundNo = CommonUtil.getStringNumRandom(32);
// 请求body参数
JSONObject refundObj = new JSONObject();
//订单号
refundObj.put("out_trade_no", outTradeNo);
//退款单编号,商户系统内部的退款单号,商户系统内部唯一,
// 只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔
refundObj.put("out_refund_no", refundNo);
refundObj.put("reason","商品已售完");
refundObj.put("notify_url", payConfig.getCallbackUrl());
JSONObject amountObj = new JSONObject();
//退款金额
amountObj.put("refund", 10);
//实际支付的总金额
amountObj.put("total", 100);
amountObj.put("currency", "CNY");
refundObj.put("amount", amountObj);
String body = refundObj.toJSONString();
log.info("请求参数:{}",body);
StringEntity entity = new StringEntity(body,"utf-8");
entity.setContentType("application/json");
HttpPost httpPost = new HttpPost(WechatPayApi.NATIVE_REFUND_ORDER);
httpPost.setHeader("Accept","application/json");
httpPost.setEntity(entity);
try(CloseableHttpResponse response = wechatPayClient.execute(httpPost)){
//响应码
int statusCode = response.getStatusLine().getStatusCode();
//响应体
String responseStr = EntityUtils.toString(response.getEntity());
log.info("申请订单退款响应码:{},响应体:{}",statusCode,responseStr);
}catch (Exception e){
e.printStackTrace();
}
}
/**
* {"amount":{"currency":"CNY","discount_refund":0,"from":[],"payer_refund":10,
*
* "payer_total":100,"refund":10,"settlement_refund":10,"settlement_total":100,"total":100},
*
* "channel":"ORIGINAL","create_time":"2022-01-18T15:18:15+08:00","funds_account":"AVAILABLE",
*
* "out_refund_no":"leZlKkz6jTj7I4Sd2F04HdHLPRhXg0RK","out_trade_no":"HkPfPY0q3GwuYYUou0wfUnX34iRNYxXX",
*
* "promotion_detail":[],"refund_id":"50302000602022011816573309663","status":"SUCCESS",
*
* "success_time":"2022-01-18T15:18:24+08:00","transaction_id":"4200001392202201187404576924",
*
* "user_received_account":"民生银行信用卡5022"}
* @throws IOException
*/
@Test
public void testNativeRefundQuery() throws IOException {
String refundNo = "leZlKkz6jTj7I4Sd2F04HdHLPRhXg0RK";
String url = String.format(WechatPayApi.NATIVE_REFUND_QUERY,refundNo);
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("Accept","application/json");
try(CloseableHttpResponse response = wechatPayClient.execute(httpGet)){
//响应码
int statusCode = response.getStatusLine().getStatusCode();
//响应体
String responseStr = EntityUtils.toString(response.getEntity());
log.info("查询订单退款 响应码:{},响应体:{}",statusCode,responseStr);
}catch (Exception e){
e.printStackTrace();
}
}
}
简介:多渠道支付对接-策略模式+工厂模式编码实战
public interface PayStrategy {
/**
* 统一下单接口
* @param payInfoVO
* @return
*/
String unifiedOrder(PayInfoVO payInfoVO);
/**
* 退款接口
* @param payInfoVO
* @return
*/
default String refund(PayInfoVO payInfoVO){ return ""; }
/**
* 查询支付状态
* @param payInfoVO
* @return
*/
default String queryPayStatus(PayInfoVO payInfoVO){ return ""; }
/**
* 关闭订单
* @param payInfoVO
* @return
*/
default String closeOrder(PayInfoVO payInfoVO){ return ""; }
}
public class PayStrategyContext {
private PayStrategy payStrategy;
public PayStrategyContext(PayStrategy payStrategy){
this.payStrategy = payStrategy;
}
/**
* 根据策略对象,执行不同的下单接口
* @return
*/
public String executeUnifiedOrder(PayInfoVO payInfoVO){
return payStrategy.unifiedOrder(payInfoVO);
}
/**
* 根据策略对象,执行不同的退款接口
* @return
*/
public String executeRefund(PayInfoVO payInfoVO){
return payStrategy.refund(payInfoVO);
}
/**
* 根据策略对象,执行不同的关闭接口
* @return
*/
public String executeCloseOrder(PayInfoVO payInfoVO){
return payStrategy.closeOrder(payInfoVO);
}
/**
* 根据策略对象,执行不同的查询订单状态接口
* @return
*/
public String executeQueryPayStatus(PayInfoVO payInfoVO){
return payStrategy.queryPayStatus(payInfoVO);
}
}
@Service
@Slf4j
public class JdPayStrategy implements PayStrategy{
@Override
public String unifiedOrder(PayInfoVO payInfoVO) {
return null;
}
@Override
public String refund(PayInfoVO payInfoVO) {
return null;
}
@Override
public String queryPayStatus(PayInfoVO payInfoVO) {
return null;
}
@Override
public String closeOrder(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.ALI_PAY.name().equals(payType)) {
//支付宝支付
PayStrategyContext payStrategyContext = new PayStrategyContext(aliPayStrategy);
return payStrategyContext.executeUnifiedOrder(payInfoVO);
} else if(ProductOrderPayTypeEnum.WECHAT_PAY.name().equals(payType)){
//微信支付
PayStrategyContext payStrategyContext = new PayStrategyContext(wechatPayStrategy);
return payStrategyContext.executeUnifiedOrder(payInfoVO);
}
return "";
}
/**
* 关闭订单
* @param payInfoVO
* @return
*/
public String closeOrder(PayInfoVO payInfoVO){
String payType = payInfoVO.getPayType();
if (ProductOrderPayTypeEnum.ALI_PAY.name().equals(payType)) {
//支付宝支付
PayStrategyContext payStrategyContext = new PayStrategyContext(aliPayStrategy);
return payStrategyContext.executeCloseOrder(payInfoVO);
} else if(ProductOrderPayTypeEnum.WECHAT_PAY.name().equals(payType)){
//微信支付
PayStrategyContext payStrategyContext = new PayStrategyContext(wechatPayStrategy);
return payStrategyContext.executeCloseOrder(payInfoVO);
}
return "";
}
/**
* 查询支付状态
* @param payInfoVO
* @return
*/
public String queryPayStatus(PayInfoVO payInfoVO){
String payType = payInfoVO.getPayType();
if (ProductOrderPayTypeEnum.ALI_PAY.name().equals(payType)) {
//支付宝支付
PayStrategyContext payStrategyContext = new PayStrategyContext(aliPayStrategy);
return payStrategyContext.executeQueryPayStatus(payInfoVO);
} else if(ProductOrderPayTypeEnum.WECHAT_PAY.name().equals(payType)){
//微信支付
PayStrategyContext payStrategyContext = new PayStrategyContext(wechatPayStrategy);
return payStrategyContext.executeQueryPayStatus(payInfoVO);
}
return "";
}
/**
* 退款接口
* @param payInfoVO
* @return
*/
public String refund(PayInfoVO payInfoVO){
String payType = payInfoVO.getPayType();
if (ProductOrderPayTypeEnum.ALI_PAY.name().equals(payType)) {
//支付宝支付
PayStrategyContext payStrategyContext = new PayStrategyContext(aliPayStrategy);
return payStrategyContext.executeRefund(payInfoVO);
} else if(ProductOrderPayTypeEnum.WECHAT_PAY.name().equals(payType)){
//微信支付
PayStrategyContext payStrategyContext = new PayStrategyContext(wechatPayStrategy);
return payStrategyContext.executeRefund(payInfoVO);
}
return "";
}
}
@Controller
@RequestMapping("/api/callback/order/v1/")
@Slf4j
public class PayCallbackController {
@Autowired
private WechatPayConfig wechatPayConfig;
@Autowired
private ProductOrderService productOrderService;
@Autowired
private ScheduledUpdateCertificatesVerifier verifier;
/**
* * 获取报文
* <p>
* * 验证签名(确保是微信传输过来的)
* <p>
* * 解密(AES对称解密出原始数据)
* <p>
* * 处理业务逻辑
* <p>
* * 响应请求
*
* @param request
* @param response
* @return
*/
@RequestMapping("wechat")
@ResponseBody
public Map<String, String> wehcatPayCallback(HttpServletRequest request, HttpServletResponse response) {
//获取报文
String body = getRequestBody(request);
//随机串
String nonceStr = request.getHeader("Wechatpay-Nonce");
//微信传递过来的签名
String signature = request.getHeader("Wechatpay-Signature");
//证书序列号(微信平台)
String serialNo = request.getHeader("Wechatpay-Serial");
//时间戳
String timestamp = request.getHeader("Wechatpay-Timestamp");
//构造签名串
//应答时间戳\n
//应答随机串\n
//应答报文主体\n
String signStr = Stream.of(timestamp, nonceStr, body).collect(Collectors.joining("\n", "", "\n"));
Map<String, String> map = new HashMap<>(2);
try {
//验证签名是否通过
boolean result = verifiedSign(serialNo, signStr, signature);
if(result){
//解密数据
String plainBody = decryptBody(body);
log.info("解密后的明文:{}",plainBody);
Map<String, String> paramsMap = convertWechatPayMsgToMap(plainBody);
//处理业务逻辑 TODO
//响应微信
map.put("code", "SUCCESS");
map.put("message", "成功");
}
} catch (Exception e) {
log.error("微信支付回调异常:{}", e);
}
return map;
}
/**
* 转换body为map
* @param plainBody
* @return
*/
private Map<String,String> convertWechatPayMsgToMap(String plainBody){
Map<String,String> paramsMap = new HashMap<>(2);
JSONObject jsonObject = JSONObject.parseObject(plainBody);
//商户订单号
paramsMap.put("out_trade_no",jsonObject.getString("out_trade_no"));
//交易状态
paramsMap.put("trade_state",jsonObject.getString("trade_state"));
//附加数据
paramsMap.put("account_no",jsonObject.getJSONObject("attach").getString("accountNo"));
return paramsMap;
}
/**
* 解密body的密文
*
* "resource": {
* "original_type": "transaction",
* "algorithm": "AEAD_AES_256_GCM",
* "ciphertext": "",
* "associated_data": "",
* "nonce": ""
* }
*
* @param body
* @return
*/
private String decryptBody(String body) throws UnsupportedEncodingException, GeneralSecurityException {
AesUtil aesUtil = new AesUtil(wechatPayConfig.getApiV3Key().getBytes("utf-8"));
JSONObject object = JSONObject.parseObject(body);
JSONObject resource = object.getJSONObject("resource");
String ciphertext = resource.getString("ciphertext");
String associatedData = resource.getString("associated_data");
String nonce = resource.getString("nonce");
return aesUtil.decryptToString(associatedData.getBytes("utf-8"),nonce.getBytes("utf-8"),ciphertext);
}
/**
* 验证签名
*
* @param serialNo 微信平台-证书序列号
* @param signStr 自己组装的签名串
* @param signature 微信返回的签名
* @return
* @throws UnsupportedEncodingException
*/
private boolean verifiedSign(String serialNo, String signStr, String signature) throws UnsupportedEncodingException {
return verifier.verify(serialNo, signStr.getBytes("utf-8"), signature);
}
/**
* 读取请求数据流
*
* @param request
* @return
*/
private String getRequestBody(HttpServletRequest request) {
StringBuffer sb = new StringBuffer();
try (ServletInputStream inputStream = request.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
log.error("读取数据流异常:{}", e);
}
return sb.toString();
}
}
@Controller
@RequestMapping("/api/callback/order/v1/")
@Slf4j
public class PayCallbackController {
@Autowired
private WechatPayConfig wechatPayConfig;
@Autowired
private ProductOrderService productOrderService;
@Autowired
private ScheduledUpdateCertificatesVerifier verifier;
/**
* * 获取报文
* <p>
* * 验证签名(确保是微信传输过来的)
* <p>
* * 解密(AES对称解密出原始数据)
* <p>
* * 处理业务逻辑
* <p>
* * 响应请求
*
* @param request
* @param response
* @return
*/
@RequestMapping("wechat")
@ResponseBody
public Map<String, String> wehcatPayCallback(HttpServletRequest request, HttpServletResponse response) {
//获取报文
String body = getRequestBody(request);
//随机串
String nonceStr = request.getHeader("Wechatpay-Nonce");
//微信传递过来的签名
String signature = request.getHeader("Wechatpay-Signature");
//证书序列号(微信平台)
String serialNo = request.getHeader("Wechatpay-Serial");
//时间戳
String timestamp = request.getHeader("Wechatpay-Timestamp");
//构造签名串
//应答时间戳\n
//应答随机串\n
//应答报文主体\n
String signStr = Stream.of(timestamp, nonceStr, body).collect(Collectors.joining("\n", "", "\n"));
Map<String, String> map = new HashMap<>(2);
try {
//验证签名是否通过
boolean result = verifiedSign(serialNo, signStr, signature);
if(result){
//解密数据
String plainBody = decryptBody(body);
log.info("解密后的明文:{}",plainBody);
Map<String, String> paramsMap = convertWechatPayMsgToMap(plainBody);
//处理业务逻辑
productOrderService.processOrderCallbackMsg(ProductOrderPayTypeEnum.WECHAT_PAY,paramsMap);
//响应微信
map.put("code", "SUCCESS");
map.put("message", "成功");
}
} catch (Exception e) {
log.error("微信支付回调异常:{}", e);
}
return map;
}
/**
* 转换body为map
* @param plainBody
* @return
*/
private Map<String,String> convertWechatPayMsgToMap(String plainBody){
Map<String,String> paramsMap = new HashMap<>(2);
JSONObject jsonObject = JSONObject.parseObject(plainBody);
//商户订单号
paramsMap.put("out_trade_no",jsonObject.getString("out_trade_no"));
//交易状态
paramsMap.put("trade_state",jsonObject.getString("trade_state"));
//附加数据
paramsMap.put("account_no",jsonObject.getJSONObject("attach").getString("accountNo"));
return paramsMap;
}
/**
* 解密body的密文
*
* "resource": {
* "original_type": "transaction",
* "algorithm": "AEAD_AES_256_GCM",
* "ciphertext": "",
* "associated_data": "",
* "nonce": ""
* }
*
* @param body
* @return
*/
private String decryptBody(String body) throws UnsupportedEncodingException, GeneralSecurityException {
AesUtil aesUtil = new AesUtil(wechatPayConfig.getApiV3Key().getBytes("utf-8"));
JSONObject object = JSONObject.parseObject(body);
JSONObject resource = object.getJSONObject("resource");
String ciphertext = resource.getString("ciphertext");
String associatedData = resource.getString("associated_data");
String nonce = resource.getString("nonce");
return aesUtil.decryptToString(associatedData.getBytes("utf-8"),nonce.getBytes("utf-8"),ciphertext);
}
/**
* 验证签名
*
* @param serialNo 微信平台-证书序列号
* @param signStr 自己组装的签名串
* @param signature 微信返回的签名
* @return
* @throws UnsupportedEncodingException
*/
private boolean verifiedSign(String serialNo, String signStr, String signature) throws UnsupportedEncodingException {
return verifier.verify(serialNo, signStr.getBytes("utf-8"), signature);
}
/**
* 读取请求数据流
*
* @param request
* @return
*/
private String getRequestBody(HttpServletRequest request) {
StringBuffer sb = new StringBuffer();
try (ServletInputStream inputStream = request.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
log.error("读取数据流异常:{}", e);
}
return sb.toString();
}
}
简介:账号服务-RabbitMQ相关配置开发实战
##----------rabbit配置--------------
spring.rabbitmq.host=124.221.200.246
spring.rabbitmq.port=5672
#需要手工创建虚拟主机
spring.rabbitmq.virtual-host=dev
spring.rabbitmq.username=admin
spring.rabbitmq.password=123456
#消息确认方式,manual(手动ack) 和auto(自动ack)
spring.rabbitmq.listener.simple.acknowledge-mode=auto
#开启重试,消费者代码不能添加try catch捕获不往外抛异常
spring.rabbitmq.listener.simple.retry.enabled=true
#最大重试次数
spring.rabbitmq.listener.simple.retry.max-attempts=4
# 重试消息的时间间隔,5秒
spring.rabbitmq.listener.simple.retry.initial-interval=5000
流量包crud
public interface TrafficManager {
/**
* 新增流量包
* @param trafficDO
* @return
*/
int add(TrafficDO trafficDO);
/**
* 分页查询可用的流量包
* @param page
* @param size
* @param accountNo
* @return
*/
IPage<TrafficDO> pageAvailable(int page, int size, Long accountNo);
/**
* 查找详情
* @param trafficId
* @param accountNo
* @return
*/
TrafficDO findByIdAndAccountNo(Long trafficId,Long accountNo);
/**
* 增加某个流量包天使用次数
* @param currentTrafficId
* @param accountNo
* @param dayUsedTimes
* @return
*/
int addDayUsedTimes(long currentTrafficId, Long accountNo, int dayUsedTimes);
}
@Component
@Slf4j
public class TrafficManagerImpl implements TrafficManager {
@Autowired
private TrafficMapper trafficMapper;
@Override
public int add(TrafficDO trafficDO) {
return trafficMapper.insert(trafficDO);
}
@Override
public IPage<TrafficDO> pageAvailable(int page, int size, Long accountNo) {
Page<TrafficDO> pageInfo = new Page<>(page, size);
String today = TimeUtil.format(new Date(), "yyyy-MM-dd");
Page<TrafficDO> trafficDOPage = trafficMapper.selectPage(pageInfo, new QueryWrapper<TrafficDO>()
.eq("account_no", accountNo).ge("expired_date", today).orderByDesc("gmt_create"));
return trafficDOPage;
}
@Override
public TrafficDO findByIdAndAccountNo(Long trafficId, Long accountNo) {
TrafficDO trafficDO = trafficMapper.selectOne(new QueryWrapper<TrafficDO>()
.eq("account_no", accountNo).eq("id", trafficId));
return trafficDO;
}
/**
* 给某个流量包增加天使用次数
*
* @param currentTrafficId
* @param accountNo
* @param dayUsedTimes
* @return
*/
@Override
public int addDayUsedTimes(long currentTrafficId, Long accountNo, int dayUsedTimes) {
return trafficMapper.update(null, new UpdateWrapper<TrafficDO>()
.eq("account_no", accountNo)
.eq("id", currentTrafficId).set("day_used", dayUsedTimes));
}
}
流量包权益发放业务逻辑开发
@Component
@RabbitListener(queuesToDeclare = {
@Queue("order.traffic.queue")
})
@Slf4j
public class TrafficMQListener {
@Autowired
private TrafficService trafficService;
@RabbitHandler
public void trafficHandler(EventMessage eventMessage, Message message, Channel channel){
log.info("监听到消息trafficHandler:{}",eventMessage);
try{
trafficService.handleTrafficMessage(eventMessage);
}catch (Exception e){
log.error("消费者失败:{}",eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}",eventMessage);
}
}
@Service
@Slf4j
public class TrafficServiceImpl implements TrafficService {
@Autowired
private TrafficManager trafficManager;
@Override
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
public void handleTrafficMessage(EventMessage eventMessage) {
String messageType = eventMessage.getEventMessageType();
if(EventMessageType.PRODUCT_ORDER_PAY.name().equalsIgnoreCase(messageType)){
//订单已经支付,新增流量
String content = eventMessage.getContent();
Map<String, Object> orderInfoMap = JsonUtil.json2Obj(content,Map.class);
//还原订单商品信息
Long accountNo = (Long)orderInfoMap.get("accountNo");
String outTradeNo = (String)orderInfoMap.get("outTradeNo");
Integer buyNum = (Integer)orderInfoMap.get("buyNum");
String productStr = (String) orderInfoMap.get("product");
ProductVO productVO = JsonUtil.json2Obj(productStr, ProductVO.class);
log.info("商品信息:{}",productVO);
//流量包有效期
LocalDateTime expiredDateTime = LocalDateTime.now().plusDays(productVO.getValidDay());
Date date = Date.from(expiredDateTime.atZone(ZoneId.systemDefault()).toInstant());
//构建流量包对象
TrafficDO trafficDO = TrafficDO.builder()
.accountNo(accountNo)
.dayLimit(productVO.getDayTimes() * buyNum)
.dayUsed(0)
.totalLimit(productVO.getTotalTimes())
.pluginType(productVO.getPluginType())
.level(productVO.getLevel())
.productId(productVO.getId())
.outTradeNo(outTradeNo)
.expiredDate(date).build();
int rows = trafficManager.add(trafficDO);
log.info("消费消息新增流量包:rows={},trafficDO={}",rows,trafficDO);
}
}
}
@Configuration
@Slf4j
public class RabbitMQConfig {
/**
* 消息转换器
* @return
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
//================流量包处理:用户初始化福利==================================
/**
* 交换机
*/
private String trafficEventExchange = "traffic.event.exchange";
/**
* 用户注册 免费流量包新增 队列
*/
private String trafficFreeInitQueue = "traffic.free_init.queue";
/**
* 用户注册 免费流量包新增 队列路由key
*
*/
private String trafficFreeInitRoutingKey = "traffic.free_init.routing.key";
/**
* 创建交换机 Topic类型
* 一般一个微服务一个交换机
* @return
*/
@Bean
public Exchange trafficEventExchange(){
return new TopicExchange(trafficEventExchange,true,false);
}
/**
* 队列的绑定关系建立:新用户注册免费流量包
* @return
*/
@Bean
public Binding trafficFreeInitBinding(){
return new Binding(trafficFreeInitQueue,Binding.DestinationType.QUEUE, trafficEventExchange,trafficFreeInitRoutingKey,null);
}
/**
* 免费流量包队列
*/
@Bean
public Queue trafficFreeInitQueue(){
return new Queue(trafficFreeInitQueue,true,false,false);
}
}
/**
* 用户初始化,发放福利:流量包
* @param accountDO
*/
private void userRegisterInitTask(AccountDO accountDO) {
EventMessage eventMessage = EventMessage.builder()
.messageId(IDUtil.geneSnowFlakeID().toString())
.accountNo(accountDO.getAccountNo())
.eventMessageType(EventMessageType.TRAFFIC_FREE_INIT.name())
.bizId(FREE_TRAFFIC_PRODUCT_ID.toString())
.build();
//发送发放流量包消息
rabbitTemplate.convertAndSend(rabbitMQConfig.getTrafficEventExchange(),
rabbitMQConfig.getTrafficFreeInitRoutingKey(),eventMessage);
}
分布式调度XXl-Job搭建-Docker部署服务端
步骤
步骤二:部署server
docker run -d -e PARAMS="--spring.datasource.url=jdbc:mysql://124.221.200.246:3306/xxl_job?Unicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai \
--spring.datasource.username=root \
--spring.datasource.password=123456 \
--xxl.job.accessToken=classes.net" \
-p 8080:8080 \
--name xxl-job-admin --restart=always xuxueli/xxl-job-admin:2.2.0
简介:AlibabaCloud微服务整合XXL-Job依赖实战
<xxl-job.version>2.2.0</xxl-job.version>
<!-- https://mvnrepository.com/artifact/com.xuxueli/xxl-job-core -->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>${xxl-job.version}</version>
</dependency>
<!--分布式调度-->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
</dependency>
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="true" scanPeriod="1 seconds">
<contextName>logback</contextName>
<property name="log.path" value="./data/logs/xxl-job/app.log"/>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}.%d{yyyy-MM-dd}.zip</fileNamePattern>
</rollingPolicy>
<encoder>
<pattern>%date %level [%thread] %logger{36} [%file : %line] %msg%n
</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="file"/>
</root>
</configuration>
#----------xxl-job配置--------------
logging.config=classpath:logback.xml
#调度中心部署地址,多个配置逗号分隔 "http://address01,http://address02"
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
#执行器token,非空时启用 xxl-job, access token
xxl.job.accessToken=classes.net
# 执行器app名称,和控制台那边配置一样的名称,不然注册不上去
xxl.job.executor.appname=traffic-app-executor
# [选填]执行器注册:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。
#从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
xxl.job.executor.address=
#[选填]执行器IP :默认为空表示自动获取IP(即springboot容器的ip和端口,可以自动获取,也可以指定),多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务",
xxl.job.executor.ip=
# [选填]执行器端口号:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
xxl.job.executor.port=9999
#执行器日志文件存储路径,需要对该路径拥有读写权限;为空则使用默认路径
xxl.job.executor.logpath=./data/logs/xxl-job/executor
#执行器日志保存天数
xxl.job.executor.logretentiondays=30
@Configuration
@Slf4j
public class XxlJobConfig {
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.executor.appname}")
private String appName;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
//旧版的有bug
//@Bean(initMethod = "start", destroyMethod = "destroy")
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
log.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appName);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
}
简介:付费流量包过期淘汰需求开发-整合分布式调度XXL-Job
流量包删除接口
delete FROM traffic where expired_date <= now()
@Component
@Slf4j
public class TrafficJobHandler {
@Autowired
private TrafficService trafficService;
/**
* 过期流量包处理
* @param param
* @return
*/
@XxlJob(value = "trafficExpiredHandler",init = "init",destroy = "destroy")
public ReturnT<String> execute(String param){
log.info(" execute 任务方法触发成功,删除过期流量包");
trafficService.deleteExpireTraffic();
return ReturnT.SUCCESS;
}
private void init(){
log.info(" MyJobHandler init >>>>>");
}
private void destroy(){
log.info(" MyJobHandler destroy >>>>>");
}
}
海量数据下流量包更新-惰性策略编码开发实战
/**
* 新增流量包
* @param trafficDO
* @return
*/
int add(TrafficDO trafficDO);
/**
* 分页查询可用的流量包
* @param page
* @param size
* @param accountNo
* @return
*/
IPage<TrafficDO> pageAvailable(int page, int size, Long accountNo);
/**
* 查找详情
* @param trafficId
* @param accountNo
* @return
*/
TrafficDO findByIdAndAccountNo(Long trafficId,Long accountNo);
/**
* 删除过期流量包
* @return
*/
boolean deleteExpireTraffic();
/**
* 查找可用的短链流量包(未过期),包括免费流量包
* @param accountNo
* @return
*/
List<TrafficDO> selectAvailableTraffics(Long accountNo);
/**
* 给某个流量包增加使用次数
*
* @param currentTrafficId
* @param accountNo
* @param usedTimes
* @return
*/
int addDayUsedTimes(Long accountNo, Long trafficId, Integer usedTimes) ;
/**
* 恢复流量包使用当天次数
* @param accountNo
* @param trafficId
* @param useTimes
*/
int releaseUsedTimes(Long accountNo, Long trafficId, Integer useTimes);
/**
* 批量更新流量包使用次数为0
* @param accountNo
* @param unUpdatedTrafficIds
*/
int batchUpdateUsedTimes(Long accountNo, List<Long> unUpdatedTrafficIds);
<!--给某个流量包增加天使用次数-->
<update id="addDayUsedTimes">
update traffic set day_used = day_used + #{usedTimes}
where id = #{trafficId} and account_no = #{accountNo}
and (day_limit - day_used) >= #{usedTimes} limit 1
</update>
<!--恢复流量包-->
<update id="releaseUsedTimes">
update traffic set day_used = day_used - #{usedTimes}
where id = #{trafficId} and account_no = #{accountNo}
and (day_used - #{usedTimes}) >= 0 limit 1;
</update>
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UseTrafficVO {
/**
* 天剩余可用总次数 = 总次数 - 已用
*/
private Integer dayTotalLeftTimes;
/**
* 当前使用的流量包
*/
private TrafficDO currentTrafficDO;
/**
* 记录没过期,但是今天没更新的流量包id
*/
private List<Long> unUpdatedTrafficIds;
}
/**
* * 查询用户全部可用流量包
* * 遍历用户可用流量包
* * 判断是否更新-用日期判断
* * 没更新的流量包后加入【待更新集合】中
* * 增加【今天剩余可用总次数】
* * 已经更新的判断是否超过当天使用次数
* * 如果没超过则增加【今天剩余可用总次数】
* * 超过则忽略
*
* * 更新用户今日流量包相关数据
* * 扣减使用的某个流量包使用次数
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
public JsonData reduce(UseTrafficRequest trafficRequest) {
Long accountNo = trafficRequest.getAccountNo();
//处理流量包,筛选出未更新流量包,当前使用的流量包
UseTrafficVO useTrafficVO = processTrafficList(accountNo);
log.info("今天可用总次数:{},当前使用流量包:{}",useTrafficVO.getDayTotalLeftTimes(),useTrafficVO.getCurrentTrafficDO());
if(useTrafficVO.getCurrentTrafficDO() == null){
return JsonData.buildResult(BizCodeEnum.TRAFFIC_REDUCE_FAIL);
}
log.info("待更新流量包列表:{}",useTrafficVO.getUnUpdatedTrafficIds());
if(useTrafficVO.getUnUpdatedTrafficIds().size()>0){
//更新今日流量包
trafficManager.batchUpdateUsedTimes(accountNo,useTrafficVO.getUnUpdatedTrafficIds());
}
//先更新,再扣减当前使用的流量包
int rows = trafficManager.addDayUsedTimes(accountNo,useTrafficVO.getCurrentTrafficDO().getId(),1);
if(rows != 1){
throw new BizException(BizCodeEnum.TRAFFIC_REDUCE_FAIL);
}
return JsonData.buildSuccess();
}
private UseTrafficVO processTrafficList(Long accountNo) {
//全部流量包
List<TrafficDO> list = trafficManager.selectAvailableTraffics(accountNo);
if(list == null || list.size()==0){ throw new BizException(BizCodeEnum.TRAFFIC_EXCEPTION); }
//天剩余可用总次数
Integer dayTotalLeftTimes = 0;
//当前使用
TrafficDO currentTrafficDO = null;
//没过期,但是今天没更新的流量包id列表
List<Long> unUpdatedTrafficIds = new ArrayList<>();
//今天日期
String todayStr = TimeUtil.format(new Date(),"yyyy-MM-dd");
for(TrafficDO trafficDO : list){
String trafficUpdateDate = TimeUtil.format(trafficDO.getGmtModified(),"yyyy-MM-dd");
if(todayStr.equalsIgnoreCase(trafficUpdateDate)){
//已经更新 天剩余可用总次数 = 总次数 - 已用
int dayLeftTimes = trafficDO.getDayLimit() - trafficDO.getDayUsed();
dayTotalLeftTimes = dayTotalLeftTimes+dayLeftTimes;
//选取当次使用流量包
if(dayLeftTimes>0 && currentTrafficDO==null){
currentTrafficDO = trafficDO;
}
}else {
//未更新
dayTotalLeftTimes = dayTotalLeftTimes + trafficDO.getDayLimit();
//记录未更新的流量包
unUpdatedTrafficIds.add(trafficDO.getId());
//选取当次使用流量包
if(currentTrafficDO == null){
currentTrafficDO = trafficDO;
}
}
}
UseTrafficVO useTrafficVO = new UseTrafficVO(dayTotalLeftTimes,currentTrafficDO,unUpdatedTrafficIds);
return useTrafficVO;
}
private TrafficVO beanProcess(TrafficDO trafficDO) {
TrafficVO trafficVO = new TrafficVO();
BeanUtils.copyProperties(trafficDO,trafficVO);
return trafficVO;
}
简介:短链服务-创建短链和流量包业务联动开发
/**
* 1天的可用的总流量包
*/
public static final String DAY_TOTAL_TRAFFIC = "lock:traffic:day_total:%s";
//往redis设置下总流量包次数,短链服务那边递减即可; 如果有新增流量包,则删除这个key
long leftSeconds = TimeUtil.getRemainSecondsOneDay(new Date());
String totalTrafficTimesKey = String.format(RedisKey.DAY_TOTAL_TRAFFIC,accountNo);
redisTemplate.opsForValue().set(totalTrafficTimesKey,
useTrafficVO.getDayTotalLeftTimes()-1,leftSeconds, TimeUnit.SECONDS);
简介: 流量包锁定任务表设计和创建讲解
CREATE TABLE `traffic_task` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`account_no` bigint DEFAULT NULL,
`traffic_id` bigint DEFAULT NULL,
`use_times` int DEFAULT NULL,
`lock_state` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '锁定状态锁定LOCK 完成FINISH-取消CANCEL',
`biz_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '唯一标识',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_biz_id` (`biz_id`) USING BTREE,
KEY `idx_release` (`account_no`,`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
简介:短链平台-数据可视化整体链路讲解
数据分层 | 分层描述 | 数据生成计算工具 | 存储 |
---|---|---|---|
ODS | 原生数据,短链访问基本信息 | SpringBoot生成 | Kafka |
DWD | 对 ODS 层做数据清洗和规范化,新老访客标记等 | Flink | Kafka |
DWM | 对DWD数据进一步加工补齐数据,独立访客统计,操作系统/ip/城市,做宽表 | Flink | kafka |
DWS | 对DWM进行处理,多流合并,分组|聚合|开窗|统计,形成主题宽表 | Flink | ClickHouse |
ADS | 从ClickHouse中读取数据,根据需求进行筛选聚合,可视化展示 | ClickHouseSql | web可视化展示 |
简介: Docker容器化部署Kafka+Zookeeper实战
docker run -d --name zookeeper -p 2181:2181 -t wurstmeister/zookeeper
docker run -d --name classes_kafka \
-p 9092:9092 \
-e KAFKA_BROKER_ID=0 \
--env KAFKA_HEAP_OPTS=-Xmx256M \
--env KAFKA_HEAP_OPTS=-Xms128M \
-e KAFKA_ZOOKEEPER_CONNECT=124.221.200.246:2181 \
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://124.221.200.246:9092 \
-e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 wurstmeister/kafka:2.13-2.7.0
#创建一个主题:
kafka-topics.sh --create --zookeeper 124.221.200.246:2181 --replication-factor 1 --partitions 1 --topic mykafka
数据日志采集开发实战之发送Kafka消息
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_HOME" value="./data/logs/link" />
<!--采用打印到控制台,记录日志的方式-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date %level [%thread] %logger{36} [%file : %line] %msg%n</pattern>
</encoder>
</appender>
<!-- 采用保存到日志文件 记录日志的方式-->
<appender name="rollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/link.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/link-%d{yyyy-MM-dd}.log</fileNamePattern>
</rollingPolicy>
<encoder>
<pattern>%date %level [%thread] %logger{36} [%file : %line] %msg%n</pattern>
</encoder>
</appender>
<!-- 指定某个类单独打印日志 -->
<logger name="net.classes.service.impl.LogServiceImpl"
level="INFO" additivity="false">
<appender-ref ref="rollingFile" />
<appender-ref ref="console" />
</logger>
<root level="info" additivity="false">
<appender-ref ref="console" />
</root>
</configuration>
public interface LogService {
/**
* 记录日志
* @param request
* @param shortLinkCode
* @param accountNo
* @return
*/
void recordShortLinkLog(HttpServletRequest request,String shortLinkCode,Long accountNo);
}
@Service
@Slf4j
public class LogServiceImpl implements LogService {
private static final String TOPIC_NAME = "ods_link_visit_topic";
@Autowired
private KafkaTemplate kafkaTemplate;
@Override
public void recordShortLinkLog(HttpServletRequest request, String shortLinkCode, Long accountNo) {
//ip、浏览器信息
String ip = CommonUtil.getIpAddr(request);
//全部请求头
Map<String,String> headerMap = CommonUtil.getAllRequestHeader(request);
Map<String,String> availableMap = new HashMap<>();
availableMap.put("user-agent",headerMap.get("user-agent"));
availableMap.put("referer",headerMap.get("referer"));
availableMap.put("accountNo",accountNo.toString());
LogRecord logRecord = LogRecord.builder()
//日志类型
.event(LogTypeEnum.SHORT_LINK_TYPE.name())
//日志内容
.data(availableMap)
//客户端ip
.ip(ip)
//产生时间
.ts(CommonUtil.getCurrentTimestamp())
//业务唯一标识
.bizId(shortLinkCode).build();
String jsonLog = JsonUtil.obj2Json(logRecord);
//打印控制台
log.info(jsonLog);
//发送kafka
kafkaTemplate.send(TOPIC_NAME,jsonLog);
}
}
#----------kafka配置--------------
spring.kafka.bootstrap-servers=124.221.200.246:9092
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
创建topic
./kafka-topics.sh --create --zookeeper 124.221.200.246:2181 --replication-factor 1 --partitions 1 --topic ods_link_visit_topic
查看topic
./kafka-topics.sh --list --zookeeper 124.221.200.246:2181
删除topic
./kafka-topics.sh --zookeeper 124.221.200.246:2181 --delete --topic ods_link_visit_topic
消费者消费消息
./kafka-console-consumer.sh --bootstrap-server 124.221.200.246:9092 --from-beginning --topic ods_link_visit_topic
生产者发送消息
./kafka-console-producer.sh --broker-list 124.221.200.246:9092 --topic ods_link_visit_topic
简介: Flink实时计算项目搭建和依赖配置引入
<properties>
<encoding>UTF-8</encoding>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<scala.version>2.12</scala.version>
<flink.version>1.13.1</flink.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
<!--flink客户端-->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<!--scala版本-->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-scala_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<!--java版本-->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-java</artifactId>
<version>${flink.version}</version>
</dependency>
<!--streaming的scala版本-->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-scala_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<!--streaming的java版本-->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<!--Flink web ui-->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-runtime-web_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<!--使用 RocksDBStateBackend 需要加依赖-->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-statebackend-rocksdb_${scala.version}</artifactId>
<version>1.13.1</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<!--flink cep依赖包-->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-cep_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<!--redis connector-->
<dependency>
<groupId>org.apache.bahir</groupId>
<artifactId>flink-connector-redis_2.11</artifactId>
<version>1.0</version>
</dependency>
<!--kafka connector-->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<!--日志输出-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
<scope>runtime</scope>
</dependency>
<!--json依赖包-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.44</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-jdbc_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
</dependencies>
<!-- 指定仓库位置,先从aliyun找,找不到再从apache仓库找 -->
<repositories>
<repository>
<id>aliyun</id>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</repository>
<repository>
<id>apache</id>
<url>https://repository.apache.org/content/repositories/snapshots/</url>
</repository>
</repositories>
<build>
<finalName>classes-flink</finalName>
<plugins>
<!--默认编译版本比较低,所以用compiler插件,指定项目源码的jdk版本,编译后的jdk版本和编码,-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${file.encoding}</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
@Slf4j
public class DwdShortLinkLogApp {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStream<String> ds = env.socketTextStream("127.0.0.1", 8888);
ds.print();
env.execute();
}
}
@Slf4j
public class KafkaUtil {
/**
* kafka的broker地址
*/
private static String KAFKA_SERVER = null;
static {
Properties properties = new Properties();
InputStream in = KafkaUtil.class.getClassLoader().getResourceAsStream("application.properties");
try {
properties.load(in);
} catch (IOException e) {
log.error("加载kafka配置文件失败,{}",e);
}
//获取key配置对应的value
KAFKA_SERVER = properties.getProperty("kafka.servers");
}
/**
* 获取flink的kafka消费者
* @param topic
* @param groupId
* @return
*/
public static FlinkKafkaConsumer<String> getKafkaConsumer(String topic, String groupId){
Properties props = new Properties();
props.setProperty(ConsumerConfig.GROUP_ID_CONFIG,groupId);
props.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,KAFKA_SERVER);
return new FlinkKafkaConsumer<String>(topic,new SimpleStringSchema(),props);
}
/**
* 获取flink的kafka生产者
* @param topic
* @return
*/
public static FlinkKafkaProducer<String> getKafkaProducer(String topic){
return new FlinkKafkaProducer<String>(KAFKA_SERVER,topic,new SimpleStringSchema());
}
}
DwdShortLinkLogApp
@Slf4j
public class DwdShortLinkLogApp {
/**
* 定义source topic
*/
public static final String SOURCE_TOPIC = "ods_link_visit_topic";
/**
* 定义sink topic
*/
public static final String SINK_TOPIC = "dwd_link_visit_topic";
/**
* 定义消费者组
*/
public static final String GROUP_ID = "dwd_short_link_group";
public static void main(String [] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStream<String> ds = env.socketTextStream("127.0.0.1",8888);
//FlinkKafkaConsumer<String> kafkaConsumer = KafkaUtil.getKafkaConsumer(SOURCE_TOPIC, GROUP_ID);
//DataStreamSource<String> ds = env.addSource(kafkaConsumer);
ds.print();
//数据补齐
SingleOutputStreamOperator<JSONObject> jsonDS = ds.flatMap(new FlatMapFunction<String, JSONObject>() {
@Override
public void flatMap(String value, Collector<JSONObject> out) throws Exception {
JSONObject jsonObject = JSON.parseObject(value);
//生成设备唯一id
String udid = getDeviceId(jsonObject);
jsonObject.put("udid",udid);
String referer = getReferer(jsonObject);
jsonObject.put("referer",referer);
out.collect(jsonObject);
}
});
//分组
KeyedStream<JSONObject, String> keyedStream = jsonDS.keyBy(new KeySelector<JSONObject, String>() {
@Override
public String getKey(JSONObject value) throws Exception {
return value.getString("udid");
}
});
//识别 richMap open函数,做状态存储的初始化
SingleOutputStreamOperator<String> jsonDSWithVisitorState = keyedStream.map(new VistorMapFunction());
jsonDSWithVisitorState.print("ods新老访客");
//存储到dwd
FlinkKafkaProducer<String> kafkaProducer = KafkaUtil.getKafkaProducer(SINK_TOPIC);
jsonDSWithVisitorState.addSink(kafkaProducer);
env.execute();
}
/**
* 提取referer
* @param jsonObject
* @return
*/
public static String getReferer(JSONObject jsonObject){
JSONObject dataJsonObj = jsonObject.getJSONObject("data");
if(dataJsonObj.containsKey("referer")){
String referer = dataJsonObj.getString("referer");
if(StringUtils.isNotBlank(referer)){
try {
URL url = new URL(referer);
return url.getHost();
}catch (MalformedURLException e) {
log.error("提取referer失败:{}",e);
}
}
}
return "";
}
/**
* 生成设备唯一id
* @param jsonObject
* @return
*/
public static String getDeviceId(JSONObject jsonObject){
Map<String,String> map = new TreeMap<>();
try {
map.put("ip",jsonObject.getString("ip"));
map.put("event",jsonObject.getString("event"));
map.put("bizId",jsonObject.getString("bizId"));
String userAgent = jsonObject.getJSONObject("data").getString("user-agent");
map.put("userAgent",userAgent);
String deviceId = DeviceUtil.geneWebUniqueDeviceId(map);
return deviceId;
}catch (Exception e){
log.error("生成唯一deviceid异常:{}",jsonObject);
return null;
}
}
}
留存数据
{"ip":"141.123.11.31","ts":1646145133665,"event":"SHORT_LINK_TYPE","udid":null,"bizId":"026m8O3a","data":{"referer":null,"accountNo":"693100647796441088","user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36"}}
简介: 浏览器头User-Agent提取工具UserAgentUtils讲解
<!-- https://mvnrepository.com/artifact/eu.bitwalker/UserAgentUtils -->
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
<version>1.21</version>
</dependency>
/**
* 获取浏览器对象
* @param agent
* @return
*/
public static Browser getBrowser(String agent){
UserAgent userAgent = UserAgent.parseUserAgentString(agent);
return userAgent.getBrowser();
}
/**
* 获取操作系统
* @param agent
* @return
*/
public static OperatingSystem getOperationSystem(String agent){
UserAgent userAgent = UserAgent.parseUserAgentString(agent);
return userAgent.getOperatingSystem();
}
/**
* 获取浏览器名称
* @param agent
* @return Firefox、Chrome
*/
public static String getBrowserName(String agent){
return getBrowser(agent).getGroup().getName();
}
/**
* 获取设备类型
* @param agent
* @return MOBILE、COMPUTER
*/
public static String getDeviceType(String agent){
return getOperationSystem(agent).getDeviceType().toString();
}
/**
* 获取os: windwos、IOS、Android
* @param agent
* @return
*/
public static String getOS(String agent){
return getOperationSystem(agent).getGroup().getName();
}
/**
* 获取设备厂家
* @param agent
* @return
*/
public static String getDeviceManufacturer(String agent){
return getOperationSystem(agent).getManufacturer().toString();
}
/**
* 操作系统版本
* @param userAgent
* @return Android 1.x、Intel Mac OS X 10.15
*/
public static String getOSVersion(String userAgent) {
String osVersion = "";
if(StringUtils.isBlank(userAgent)) {
return osVersion;
}
String[] strArr = userAgent.substring(userAgent.indexOf("(")+1,
userAgent.indexOf(")")).split(";");
if(null == strArr || strArr.length == 0) {
return osVersion;
}
osVersion = strArr[1];
return osVersion;
}
/**
* 解析对象
* @param agent
* @return
*/
public static DeviceInfoDO getDeviceInfo(String agent){
UserAgent userAgent = UserAgent.parseUserAgentString(agent);
Browser browser = userAgent.getBrowser();
OperatingSystem operatingSystem = userAgent.getOperatingSystem();
String browserName = browser.getGroup().getName();
String os = operatingSystem.getGroup().getName();
String manufacture = operatingSystem.getManufacturer().toString();
String deviceType = operatingSystem.getDeviceType().toString();
DeviceInfoDO deviceInfoDO = DeviceInfoDO.builder().browserName(browserName)
.deviceManufacturer(manufacture)
.deviceType(deviceType)
.os(os)
.osVersion(getOSVersion(agent))
.build();
return deviceInfoDO;
}
#各个节点上传到新建文件夹
/usr/local/software/*
#安装
sudo rpm -ivh *.rpm
#启动
systemctl start clickhouse-server
#停止
systemctl stop clickhouse-server
#重启
systemctl restart clickhouse-server
#状态查看
sudo systemctl status clickhouse-server
#查看端口占用,如果命令不存在 yum install -y lsof
lsof -i :8123
#查看日志
tail -f /var/log/clickhouse-server/clickhouse-server.log
tail -f /var/log/clickhouse-server/clickhouse-server.err.log
#开启远程访问,取消下面的注释
vim /etc/clickhouse-server/config.xml
#编辑配置文件
<listen_host>0.0.0.0</listen_host>
#重启
systemctl restart clickhouse-server
其他安装方式
docker run -d --name classes_clickhouse --ulimit nofile=262144:262144 \
-p 8123:8123 -p 9000:9000 -p 9009:9009 --privileged=true \
-v /mydata/docker/clickhouse/log:/var/log/clickhouse-server \
-v /mydata/docker/clickhouse/data:/var/lib/clickhouse clickhouse/clickhouse-server:22.2.3.5
<dependencies>
<!--网关依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Spring Cloud 2020 中重磅推荐的负载均衡器 Spring Cloud LoadBalancer 简称 SCL-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>3.0.4</version>
</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>-->
<!--坑:spring-cloud-dependencies 2020.0.0 默认不在加载bootstrap配置文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--限流依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--限流持久化到nacos-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!-- 需要加 servlet包,不然配置跨域会找不到类 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
</dependencies>
避坑一
<!--坑:spring-cloud-dependencies 2020.0.0 默认不在加载bootstrap配置文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
闭坑二
Spring Cloud Gateway 注册到了 Nacos 无法发现服务,报503 Service Unavailable
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>3.0.4</version>
</dependency>
添加启动类
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
简介:微服务Docker打包插件介绍和配置实战
微服务采用容器化部署->本地推送镜像到镜像仓库->Paas容器云管理平台拉取部署
<docker.image.prefix>dcloud</docker.image.prefix>
<build>
<finalName>dcloud-account</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>
微服务 编写
FROM adoptopenjdk/openjdk11:jre11u-nightly
COPY target/dcloud-account.jar dcloud-account.jar
ENTRYPOINT ["java","-jar","/dcloud-account.jar"]
简介:多个微服务Docker镜像打包实战
多个微服务本地镜像打包
mvn install -Dmaven.test.skip=true dockerfile:build
注意点
问题点:如果发现运行的镜像不是最新的
本地运行docker镜像
docker run -d --name dcloud-account -d -p 9002:9002 镜像id
docker run -d --name dcloud-gateway -d -p 8888:8888 ef63ed47a694
docker run -d --name dcloud-account -d -p 8001:8001 377f672117aa
docker run -d --name dcloud-data -d -p 8002:8002 92c924874f35
docker run -d --name dcloud-link -d -p 8003:8003 9118421aa9c9
docker run -d --name dcloud-shop -d -p 8005:8005 db91bfbb7baa
docker logs -f 容器id
什么是Jenkins
Linux云服务器部署Jenkins
安装Docker
# 1.先安装yml
yum install -y yum-utils device-mapper-persistent-data lvm2
# 2.设置阿里云镜像
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# 3.查看可安装的docker版本
yum list docker-ce --showduplicates | sort -r
#4. 安装docker
yum -y install docker-ce-20.10.10-3.el7
#5. 查看docker版本
docker -v
#6. 启动docker
systemctl start docker
#7. 查看docker 启动状态
systemctl status docker
#查看端口占用命令安装
yum install -y lsof
安装Jenkins
mkdir -p /root/docker/jenkins
docker run -d \
-u root \
--name classes_jenkins \
-p 9302:8080 \
-v /root/docker/jenkins:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /usr/bin/docker:/usr/bin/docker \
jenkins/jenkins:2.319.3-lts-jdk11
第一行:表示将该容器在后台运行
第二行:表示使用root用户来运行容器
第三行:表示给这个容器命名,后面可以通过这个名字来管理容器
第四行:表示将主机的9302端口映射到8080端口上,后面就可以通过主机ip:9302来访问Jenkins,端口是可以更改的,根据自行需要
第五行:表示将本地/root/docker/jenkins目录映射为/var/jenkins_home目录,这就是第二步中的持久化目录。
第六、七行:表示把本地/var/run/docker.sock文件映射在容器中/var/run/docker.sock文件。这一步的目的就是为了把容器中的Jenkins可以与主机Docker进行通讯。
第八行:指定使用哪一个镜像和标签
Jenkins安装和查看运行情况
docker ps来查看是否运行
在浏览器输入ip+端口号,我这里是 192.168.101.190:9302 , 即可进入到Jenkins登录页面
获取登录Jenkins的密码, 把获取的密码复制上去
cat /root/docker/jenkins/secrets/initialAdminPassword
容器内部配置JDK
/opt/java/openjdk
插件页面下载插件
echo "登录阿里云镜像"
docker login --username=小高同学java registry.cn-hangzhou.aliyuncs.com --password=Iphone
echo "构建dcloud-common"
cd dcloud-common
mvn install
ls -alh
ls -alh
cd dcloud-account
ls -alh
echo "账号服务构建开始"
mvn install -Dmaven.test.skip=true dockerfile:build
docker tag dcloud/dcloud-account:latest registry.cn-hangzhou.aliyuncs.com/classes-d-cloud/dcloud-account:v1.1
docker push registry.cn-hangzhou.aliyuncs.com/classes-d-cloud/dcloud-account:v1.1
mvn clean
echo "账号服务构建推送成功"
echo "=======构建脚本执行完毕====="
简介:云服务器Docker容器化部署Rancher2.x实战
安装Docker
# 1.先安装yml
yum install -y yum-utils device-mapper-persistent-data lvm2
# 2.设置阿里云镜像
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
#3. 安装docker
yum -y install docker-ce-20.10.10-3.el7
#4. 查看docker版本
docker -v
#5. 启动docker
systemctl start docker
#6. 查看docker 启动状态
systemctl status docker
安装Rancher
mkdir -p /data/rancher_home/rancher
mkdir -p /data/rancher_home/auditlog
docker run -d --privileged --restart=unless-stopped -p 80:80 -p 443:443 \
-v /data/rancher_home/rancher:/var/lib/rancher \
-v /data/rancher_home/auditlog:/var/log/auditlog \
--name classes_rancher1 rancher/rancher:v2.5.7
登录Rancher
配置像加速地址
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://lmxe9ddk.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
简介:Rancher2.X部署中间件-Mysql8.0
注意
部署Mysql
需要【两个节点】
配置
镜像:mysql:8.0
环境变量:
MYSQL_ROOT_PASSWORD=123456
路径映射
/home/data/mysql/data
/var/lib/mysql:rw
/etc/localtime
/etc/localtime:ro
在创建 Docker 容器时,加上 “-v /etc/localtime:/etc/localtime:ro” 参数
让容器使用宿主机的时间,容器时间与宿主机时间同步,:ro 指定该 volume 为只读
简介:Ranche2.X部署Nacos和调整JVM内存实战
镜像:nacos/nacos-server:2.0.2
端口:8848:8848
环境变量:
JVM_XMN=128m
JVM_XMS=128m
JVM_XMX=128m
MODE=standalone
MYSQL_SERVICE_DB_NAME=nacos_config
MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=10000&socketTimeout=30000&autoReconnect=true&useSSL=false
MYSQL_SERVICE_HOST=112.74.107.230
MYSQL_SERVICE_PASSWORD=123456
MYSQL_SERVICE_PORT=3306
MYSQL_SERVICE_USER=root
NACOS_AUTH_ENABLE=true
SPRING_DATASOURCE_PLATFORM=mysql
-Xms:初始堆大小
-Xmx:最大堆大小
-Xmn:新生代大小
简介:Ranche2.X部署XXL-Job和Redis6实战
注意
部署XXL-Job
镜像:xuxueli/xxl-job-admin:2.2.0
端口:8080:8080
环境变量:
PARAMS=--spring.datasource.url=jdbc:mysql://112.74.107.230:3306/xxl_job?Unicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai \
--spring.datasource.username=root \
--spring.datasource.password=123456 \
--xxl.job.accessToken=classes.net
部署Redis
镜像:redis:6.2.4
端口:6379:6379
数据卷:
/mydata/redis/data
/data
入口命令
redis-server --requirepass classes.net
简介:Ranche2.X部署RabbitMQ和ClickHouse实战
镜像:rabbitmq:3.8.15-management
端口:15672:15672
5672:5672
环境变量:
RABBITMQ_DEFAULT_PASS=password
RABBITMQ_DEFAULT_USER=admin
镜像:clickhouse/clickhouse-server:22.1.4.30
端口:8123:8123 9000:9000 9009:9009
nofile=262144
privileged=true
ulimit=262144
1.log卷
clickhouse-log
/mydata/docker/clickhouse/log
/var/log/clickhouse-server
2.data卷
clickhouse-data
/mydata/docker/clickhouse/data
/var/lib/clickhouse
CREATE TABLE default.visit_stats
(
`code` String,
`referer` String,
`is_new` UInt64,
`account_no` UInt64,
`province` String,
`city` String,
`ip` String,
`browser_name` String,
`os` String,
`device_type` String,
`pv` UInt64,
`uv` UInt64,
`start_time` DateTime,
`end_time` DateTime,
`ts` UInt64
)
ENGINE = ReplacingMergeTree(ts)
PARTITION BY toYYYYMMDD(start_time)
ORDER BY (
start_time,
end_time,
code,
province,
city,
referer,
is_new,
ip,
browser_name,
os,
device_type);
简介:Ranche2.X部署Zookeeper和Kafka
镜像:wurstmeister/zookeeper
端口:2181:2181
简介:Rancher2.X部署Skywalking-OAP-Server+UI
镜像:apache/skywalking-oap-server:8.5.0-es7
端口:12800:12800 11800:11800
环境变量:
TZ=Asia/Shanghai
SW_ES_PASSWORD=elastic
SW_ES_USER=elastic
SW_STORAGE=elasticsearch7
SW_STORAGE_ES_CLUSTER_NODES=120.76.231.139:9200
镜像:apache/skywalking-ui:8.5.0
端口:8080:8000 (左边是容器端口,右边是宿主机端口)
环境变量(oap是上面定义的容器服务名称)
SW_OAP_ADDRESS=oap:12800
TZ=Asia/Shanghai
简介:账号整合Nacos配置中心开发和配置
<!--配置中心, 留坑,后续用的时候再讲,解决方式,看springboot官方文档版本更新说明-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--坑:spring-cloud-dependencies 2020.0.0 默认不在加载bootstrap配置文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
配置文件优先级讲解
配置实操
spring.application.name=dcloud-account-service
spring.cloud.nacos.config.server-addr=120.25.217.15:8848
spring.cloud.nacos.config.file-extension=properties
spring.cloud.nacos.config.enabled=true
spring.cloud.nacos.config.username=nacos
spring.cloud.nacos.config.password=nacos
spring.cloud.nacos.config.namespace=public
spring.cloud.nacos.config.config-long-poll-timeout=600000
spring.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 类型。
Nacos日志打印Bug,配置文件新增
logging.level.com.alibaba.nacos.client.config.impl=WARN
logging.level.root=INFO
注意
部分同学如果出现 config dta not exist
建议重启nacos
除开上述问题,如果还是拉取不到配置(保持和课程版本,文件名一样先)
重新构建下项目
mvn clean package -U
然后重启IDEA
简介: 前后端联调-前端不能识别雪花算法id解决
问题
@JsonSerialize(using = ToStringSerializer.class)
@TableId
private Long id;
前端 解决方式
npm install json-bigint
#代码封装
axios.defaults.transformResponse = [
function (data) {
const json = JSONBIG({
storeAsString: true
})
const res = json.parse(data)
return res
}
]
或
axios.defaults.transformResponse = [
function (data) {
const json = JsonBigint({
storeAsString:true
})
const res = json.parse(data)
return res
}
]
axios.create({
baseURL: 'http://baidu.com',
timeout: 5000,
timeoutErrorMessage: '请求时间过长,请联系后端或者优化请求',
})