标签搜索

目 录CONTENT

文章目录

springcloudAlibaba+devops

高大北
2022-12-16 / 0 评论 / 0 点赞 / 179 阅读 / 47,598 字 / 正在检测是否收录...
链接: https://pan.baidu.com/s/1X7h9IS5rS5AcZdaa2VIRpg?pwd=47u5 提取码: 47u5 
--来自百度网盘超级会员v5的分享

第一章 必备方法论

第1集 用户增长的数据分析模型AARRR

springcloudAlibaba&rancher【后端专题】
简介:用户增长的数据分析模型AARRR

  • 什么是AARRR用户增长模型

    • AARRR是Acquisition、Activation、Retention、Revenue、Referral 五个单词的缩写,对应用户生命周期中的5个重要环节。
    • 通俗来说就是一个产品从0~1到100的方法论
    • 指引产品运营在不同的产品运营阶段,思考哪些关键节点,更好各个节点的指标数据
  • AARRR详细解释

    • 获取:新用户首单免费/低价(瑞幸、拼多多)、厂商预装(手机)、买量投放
    • 激活:app推送、短信推送、产品价值激活
    • 留存:签到、活动短信推送、平台价值提供
    • 收益:平台广告、电商变现、付费会员、融资、软件服务
    • 传播:好友助力、分享抽奖、兄弟砍我一刀

第2集 SWOT态势分析法-个人能力与技术解决方案

SWOT态势分析法-个人能力与技术解决方案

  • 什么是SWOT态势分析

    • 官方:用来确定企业自身的竞争优势、劣势、外部市场的机会和威胁,从而将公司的战略与公司内部资源、外部环境有机地结合起来的一种科学的分析方法
    • 4个单词的缩写 优势=strength、劣势=weakness、机会=opportunity、威胁=threats
      • 优势和弱势是内部环境的分析,机会和威胁是对于外部环境的分析
    外部的机会正好是你的优势,赶紧利用起来
    
    外部的机会但是你的劣势,需要改进 
    
    自身具有优势但外部存在威胁,就需要时刻思考、保持警惕
    
    是威胁又是你的劣势,就规避并消除
    
    • 案例应用场景

    • 个人做技术Leader能力分析(工作3年,高级java,技术能力不错,项目组长刚离职)

      • 优势:技术不错、对公司业务熟悉(个人内部)

      • 劣势:项目管理能力不足、PPT汇报能力不足(个人内部)

      • 机会: 独立负责的项目把控、直接和领导汇报,成为管理层(个人外部)

      • 威胁:项目的规范机制没有建立、项目的核心难点没有攻破、加班比较多(个人外部)

    • 技术解决方案分析(团队熟悉RabbitMQ,新来的组长熟悉RocketMQ,技术选型思考)

      • 优势:RabbitMQ团队多人用过、AMQP跨语言、模型API丰富(团队内部)
      • 劣势:阅读过源码的人过少, Erlang开发,二次修改不容易,项目组长对这个不熟悉(团队内部)
      • 机会:项目可以快速上线,减少采坑(团队外部)
      • 威胁:未来可能有更强大的MQ产品出现或公司改动架构(团队外部)
  • 总结:根据SWOT进行充分分析,然后进行取舍选择,考虑更全面(对比没用这个分析你会怎么选择)

第3集 备方法论SMART衡量需求、工作的利器

简介 SMART衡量需求、工作的利器

  • 什么是SMART方法论

    • 源于国外管理大师的《管理的实践》

    • 是为了利于员工更加明确高效地工作,更是为了管理者将来对员工实施绩效考核提供了考核目标和考核标准,使考核更加科学化、规范化

    • 是5个单词的缩写

      • SMART原则【目标管理、设置】
      • Specific:目标要具体
      • Measurable:目标成果要可衡量(量化)
      • Attainable:目标要可实现,避免过高/过低
      • Relevant:与其他目标有一定的相关性
      • Time bound:目标必须有明确的期限
    • 意义:在制定工作目标或者任务目标时,考虑一下目标与计划是不是SMART化的。只有具备SMART化的计划才是具有良好可实施性的,也才能指导保证计划得以实现

第二章 基础环境准备

第1集 微服务拆分和技术栈版本说明

  • Maven聚合工程拆分

    • dcloud-common
      • 公共依赖包
    • dcloud-app
      • Flink+Kafka实时计算
    • dcloud-account
      • 账号+流量包微服务
    • dcloud-data
      • 数据可视化微服务
    • dcloud-gateway
      • 业务网关
    • dcloud-link
      • 短链微服务
    • dcloud-shop
      • 流量包商品+支付微服务
  • 微服务技术栈+前置中间件版本说明

    • JDK11
    • SpringBoot 2.5.5
    • SpringCloud 2020.0.4
    • AlibabaCloud 2021.1
    • Sharding-JDBC 4.1.1
    • Mysql 8.0
    • Nacos 2.0.2
    • Redis 6.2.4
    • RabbitQM 3.8.15
    • Kafka : wurstmeister/kafka:2.13-2.7.0
      • 为啥有RabbitMQ还要有Kafka(单机写入TPS约在百万条/秒,最大的优点,就是吞吐量高)
      • 一个是业务MQ、一个大数据流式处理的MQ,建议分开
    • 还有更多的中间件用的时候再安装

第2集 Docker安装

#按照依赖
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命令时会说找不到配置文件!

第3集 云服务器基础设施安装之Mysql8.0+Redis6.X安装

简介:云服务器基础设施安装之Mysql8.0+Redis6.X安装

  • Mysql8.0安装
#安装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;
  • Redis6安装
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

工具测试连接

第4集 云服务器基础设施安装之Nacos2.x+Mysql8配置持久化-避坑

简介:云服务器基础设施安装之Nacos2.x+Mysql8配置持久化-避坑

  • Nacos持久化SQL数据脚本

    • 在资料里面
    • 默认登录
      • 账户nacos
      • 密码 nacos
  • 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
    

第5集 云服务器基础设施安装之RabbitMQ安装

简介:云服务器基础设施安装之RabbitMQ安装

  • 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

第6集 项目创建

  • Maven聚合工程拆分
    • dcloud-common
      • 公共依赖包
    • dcloud-app
      • Flink+Kafka实时计算
    • dcloud-account
      • 账号+流量包微服务
    • dcloud-data
      • 数据可视化微服务
    • dcloud-gateway
      • 业务网关
    • dcloud-link
      • 短链微服务
    • dcloud-shop
      * 流量包商品+支付微服务
  • pom
    <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>
  • 配置gitignore文件
    • 根目录创建文件 .gitignore
# 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*

第7集 dcloud-common通用模块配置

简介:短链平台dcloud-common通用模块配置

  • pom文件配置

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

第8集 统一接口响应协议-响应工具类封装

简介:统一接口响应协议和响应工具类封装

  • 统一业务状态码 BizCodeEnum开发
    • 状态码定义约束,共6位数,前三位代表服务,后3位代表接口
    • 比如 商品服务210
  • 统一业务状态码 BizCodeEnum开发
    • 状态码定义约束,共6位数,前三位代表服务,后3位代表接口
    • 比如 商品服务210
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;
    }
}

  • 接口统一协议 JsonData工具类开发

@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());
    }
}

第9集 微服务自定义全局异常+处理器handler开发

简介:自定义全局异常+处理器开发

  • 自定义全局异常
/**
 * 全局异常处理
 */
@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("全局异常,未知错误");
        }

    }
}

第10集 common通用工具和时间格式化工具类讲解

简介: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;
    }
}
  • Json序列化工具类封装

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;
    }

}
  • common工具大集合
@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);
        }

    }

}

第三章 账号微服务-基础配置

第1集 账号微服务和流量包数据库表

账号微服务和流量包数据库表

  • 索引规范
    • 主键索引名为 pk_字段名; pk即 primary key;
    • 唯一索引名为 uk_字段名;uk 即 unique key
    • 普通索引名则为 idx_字段名;idx 即index 的简称
  • account表
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;
  • traffic_task 流量包任务表
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;

第2集 项目引入Mybatis-plus-generator代码自动生成工具

简介:介绍Mybatis-plus-generator代码自动化生成工具

  • 介绍
    • 底层是模板引擎技术,可以自定义生成的java类模板
  • 基础版mybatis-genarator
  • 进阶版mybatis-plus-genarator
  • 注意
    • 使用起来和普通版的mybatis generator一样,但是这个纯代码,不用复杂xml配置
    • 任何框架,不要使用过多的侵入或者框架定制化深的内容,防止后续改动耦合性高,成本大
  • 添加依赖
    • 统一Common项目添加,各个项目测试类里面配置
    <!-- 代码自动生成依赖 begin -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.4.1</version>
        </dependency>
        <!-- velocity -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.0</version>
        </dependency>
        <!-- 代码自动生成依赖 end-->
  • 代码(标记TODO的记得修改)
public class MyBatisPlusGenerator {

    public static void main(String[] args) {
        //1. 全局配置
        GlobalConfig config = new GlobalConfig();
        // 是否支持AR模式
        config.setActiveRecord(true)
                // 作者
                .setAuthor("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("相关代码生成完毕");
    }
}
  • 导入生成好的代码

    • model (为啥不放common项目,如果是确定每个服务都用到的依赖或者类才放到common项目)
    • mapper 类接口拷贝
    • resource/mapper文件夹 xml脚本拷贝
    • controller
    • service 不拷贝
  • Mybatis plus配置控制台打印日志

第3集 账号微服务注册Nacos+配置文件增加

简介:账号微服务注册Nacos+配置文件增加

  • 启动账号微服务

    • 排除sharding-jdbc依赖
       <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>
    
    • 增加main函数主类
    @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
  • 多个微服务增加配置+代码生成配置映入

第四章 账号微服务注册模块+短信验证码+阿里云OSS开发实战

第1集 账号微服务短信验证码发送工具类封装实战

简介:账号微服务短信验证码发送工具类封装实战

        <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>
  • RestTemplate配置

  • SmsConfig配置

#----------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;
}

  • SmsComponent工具类封装
@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());
        }
    }

}

第2集 高并发下异步请求解决方案- @Async注解应用实战

简介:高并发下异步请求解决方案一- @Async组件应用实战

  • 问题

    • 由于发送短信涉及到网络通信, 因此sendMessage方法可能会有一些延迟. 为了改善用户体验, 我们可以使用异步发送短信的方法
  • 什么是异步任务

    • 异步调用是相对于同步调用而言的,同步调用是指程序按预定顺序一步步执行,每一步必须等到上一步执行完后才能执行,异步调用则无需等待上一步程序执行完即可执行
    • 多线程就是一种实现异步调用的方式
    • MQ也是一种宏观上的异步
  • 使用场景

    • 适用于处理log、发送邮件、短信……等
    • 涉及到网络IO调用等操作
  • 使用方式

    • 启动类里面使用@EnableAsync注解开启功能,自动扫描
    • 定义异步任务类并使用@Component标记组件被容器扫描,异步方法加上@Async
  • 注意:@Async失效情况

    • 注解@Async的方法不是public方法

    • 注解@Async的返回值只能为void或者Future

    • 注解@Async方法使用static修饰也会失效

    • spring无法扫描到异步类,没加注解@Async 或 @EnableAsync注解

    • 调用方与被调方不能在同一个类

      • Spring 在扫描bean的时候会扫描方法上是否包含@Async注解,动态地生成一个子类(即proxy代理类),当这个有注解的方法被调用的时候,实际上是由代理类来调用的,代理类在调用时增加异步作用
      • 如果这个有注解的方法是被同一个类中的其他方法调用的,那么该方法的调用并没有通过代理类,而是直接通过原来的那个 bean,所以就失效了
      • 所以调用方与被调方不能在同一个类,主要是使用了动态代理,同一个类的时候直接调用,不是通过生成的动态代理类调用
      • 一般将要异步执行的方法单独抽取成一个类
    • 类中需要使用@Autowired或@Resource等注解自动注入,不能自己手动new对象

    • 在Async 方法上标注@Transactional是没用的,但在Async 方法调用的方法上标注@Transactional 是有效的

第3集 自定义线程池

  • 大家的疑惑 使用线程池的时候搞混淆ThreadPoolTaskExecutor和ThreadPoolExecutor

    • ThreadPoolExecutor,这个类是JDK中的线程池类,继承自Executor,里面有一个execute()方法,用来执行线程,线程池主要提供一个线程队列,队列中保存着所有等待状态的线程,避免了创建与销毁的额外开销

    • ThreadPoolTaskExecutor,是spring包下的,是Spring为我们提供的线程池类

      • Spring异步线程池的接口类是TaskExecutor,本质还是java.util.concurrent.Executor
  • 解决方式

    • spring会先搜索TaskExecutor类型的bean或者名字为taskExecutor的Executor类型的bean,
    • 所以我们最好来自定义一个线程池,加入Spring IOC容器里面,即可覆盖
@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")
  • 总结【方便记忆】
    • 先是CorePoolSize是否满足,然后是Queue阻塞队列是否满,最后才是MaxPoolSize是否满足

第4集 ThreadPoolTaskExecutor线程池的面试题你知道怎么回答不

简介:ThreadPoolTaskExecutor线程池的面试题你知道怎么回答不

  • 请你说下 ThreadPoolTaskExecutor线程池 有哪几个重要参数,什么时候会创建线程

    • 查看核心线程池是否已满,不满就创建一条线程执行任务,否则执行第二步。
    • 查看阻塞队列是否已满,不满就将任务存储在阻塞队列中,否则执行第三步。
    • 查看线程池是否已满,即是否达到最大线程池数,不满就创建一条线程执行任务,否则就按照策略处理无法执行的任务。
  • 高并发下核心线程怎么设置?

    • 分IO密集还是CPU密集

      • CPU密集设置为跟核心数一样大小
      • IO密集型设置为2倍CPU核心数
    • 非固定,根据实际情况压测进行调整,俗称【调参程序员】【调参算法工程师】

第5集 图形验证码开发之谷歌kaptcha引入

简介:谷歌开源kaptcha图形验证码开发

  • Kaptcha 框架介绍 谷歌开源的一个可高度配置的实用验证码生成工具

    • 验证码的字体/大小/颜色
    • 验证码内容的范围(数字,字母,中文汉字!)
    • 验证码图片的大小,边框,边框粗细,边框颜色
    • 验证码的干扰线
    • 验证码的样式(鱼眼样式、3D、普通模糊)
  • 聚合工程依赖添加(使用国内baomidou二次封装的springboot整合starter)

            <!--kaptcha依赖包-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>kaptcha-spring-boot-starter</artifactId>
                <version>1.1.0</version>
            </dependency>
  • 账号微服务添加
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>kaptcha-spring-boot-starter</artifactId>
            </dependency>
  • 开发配置(任何框架和springboot整合基本都是)
@Configuration
public class CaptchaConfig {

    /**
     * 验证码配置
     * Kaptcha配置类名
     * 
     * @return
     */
    @Bean
    @Qualifier("captchaProducer")
    public DefaultKaptcha kaptcha() {
        DefaultKaptcha kaptcha = new DefaultKaptcha();
        Properties properties = new Properties();
//    properties.setProperty(Constants.KAPTCHA_BORDER, "yes");
//    properties.setProperty(Constants.KAPTCHA_BORDER_COLOR, "220,220,220");
//    //properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR, "38,29,12");
//    properties.setProperty(Constants.KAPTCHA_IMAGE_WIDTH, "147");
//    properties.setProperty(Constants.KAPTCHA_IMAGE_HEIGHT, "34");
//    properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, "25");
//    //properties.setProperty(Constants.KAPTCHA_SESSION_KEY, "code");
        //验证码个数
        properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
//    properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Courier");
        //字体间隔
        properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_SPACE,"8");
        //干扰线颜色
//    properties.setProperty(Constants.KAPTCHA_NOISE_COLOR, "white");
        //干扰实现类
        properties.setProperty(Constants.KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
        //图片样式
        properties.setProperty(Constants.KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.WaterRipple");
        //文字来源
        properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, "0123456789");
        Config config = new Config(properties);
        kaptcha.setConfig(config);
        return kaptcha;
    }
}

  • 开发一个Controller使用测试

    @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失败");
        }
    }

第6集 池化思想应用-Redis6.X配置连接池实战

简介:池化思想应用-Redis6.X配置连接池实战

  • 连接池好处

    • 使用连接池不用每次都走三次握手、每次都关闭Jedis
    • 相对于直连,使用相对麻烦,在资源管理上需要很多参数来保证,规划不合理也会出现问题
    • 如果pool已经分配了maxActive个jedis实例,则此时pool的状态就成exhausted了
  • 连接池配置 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;
        }
    }
    

第7集 图形验证码加入缓存+Try-with-resource知识巩固

简介:账号微服务开发图形验证码接口+Try-with-resource知识巩固

  • redis做隔离, 多集群:核心集群和非核心集群,高并发集群和非高并发集群

    • 资源隔离
    • 数据保护
    • 提高性能
    • key规范:业务划分,冒号隔离
      • account-service:captcha:xxxx
      • 长度不能过长
  • 验证码接口开发

    @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;
    }

  • 什么是try-with-resources
    • 资源的关闭很多⼈停留在旧的流程上,jdk7新特性就有, 但是很多⼈以为是jdk8的
    • 在try( …)⾥声 明的资源,会在try-catch代码块结束后⾃动关闭掉
    • 注意点
      • 实现了AutoCloseable接⼝的类,在try()⾥声明该类实例的时候,try结束后⾃动调⽤的 close⽅法,这个动作会早于finally⾥调⽤的⽅法
      • 不管是否出现异常,try()⾥的实例都会被调⽤close⽅法
      • try⾥⾯可以声明多个⾃动关闭的对象,越早声明的对象,会越晚被close掉

第8集 账号微服务之注册短信验证码接口开发

  • redisKey
public class RedisKey {
//    public static final Locale CHECK_CODE_KEY = ;
    /**
     * 第一个类型
     * 第二个唯一标识
     */
    public static final String CHECK_CODE_KEY = "code:%s:%s";
}
  • controller
    @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);
        }

    }
  • service
public interface NotifyService {
    /**
     * 发送验证码
     * @param userRegiser
     * @param to
     * @return
     */
    JsonData sendCode(SendCodeEnum userRegiser, String to) throws ExecutionException, InterruptedException;
}

  • serviceimpl
 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();
    }

第9集 XSpringFileStorage整合oss集成和测试存储服务

简介:XSpringFileStorage整合oss集成和测试存储服务

使用:X Spring File Storage整合oss

  • 地址:https://spring-file-storage.xuyanwu.cn/#/快速入门

  • 添加阿里云OSS的SDK

    • 地址:https://help.aliyun.com/document_detail/32008.html

    • 添加maven依赖

      • 底层聚合工程添加版本
         <!-- spring-file-storage 必须要引入 -->
          <dependency>
              <groupId>cn.xuyanwu</groupId>
              <artifactId>spring-file-storage</artifactId>
              <version>0.5.0</version>
          </dependency>
          <!-- 阿里云 OSS 不使用的情况下可以不引入 -->
          <dependency>
              <groupId>com.aliyun.oss</groupId>
              <artifactId>aliyun-sdk-oss</artifactId>
              <version>3.15.1</version>
          </dependency>
      
      • 用户微服务添加
        <!-- spring-file-storage 必须要引入 -->
          <dependency>
              <groupId>cn.xuyanwu</groupId>
              <artifactId>spring-file-storage</artifactId>
          </dependency>
          <!-- 阿里云 OSS 不使用的情况下可以不引入 -->
          <dependency>
              <groupId>com.aliyun.oss</groupId>
              <artifactId>aliyun-sdk-oss</artifactId>
          </dependency>
      
  • 用户微服务配置OSS

spring
  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 500MB
#阿里云OSS配置
  #X Spring File Storage
  file-storage:
    aliyun-oss: # 阿里云 OSS ,不使用的情况下可以不写
      - platform: aliyun-oss-1 # 存储平台标识
        enable-storage: true  # 启用存储
        access-key: 123
        secret-key: 123
        end-point: oss-cn-beijing.aliyuncs.com
        bucket-name: gtflog
        domain: https://gtflog.oss-cn-beijing.aliyuncs.com/
        base-path: hy/ # 基础路径
  • sql
-- 这里使用的是 mysql
CREATE TABLE `file_detail`
(
    `id`                varchar(32)  NOT NULL COMMENT '文件id',
    `url`               varchar(512) NOT NULL COMMENT '文件访问地址',
    `size`              bigint(20)   DEFAULT NULL COMMENT '文件大小,单位字节',
    `filename`          varchar(256) DEFAULT NULL COMMENT '文件名称',
    `original_filename` varchar(256) DEFAULT NULL COMMENT '原始文件名',
    `base_path`         varchar(256) DEFAULT NULL COMMENT '基础存储路径',
    `path`              varchar(256) DEFAULT NULL COMMENT '存储路径',
    `ext`               varchar(32)  DEFAULT NULL COMMENT '文件扩展名',
    `content_type`      varchar(32)  DEFAULT NULL COMMENT 'MIME类型',
    `platform`          varchar(32)  DEFAULT NULL COMMENT '存储平台',
    `th_url`            varchar(512) DEFAULT NULL COMMENT '缩略图访问路径',
    `th_filename`       varchar(256) DEFAULT NULL COMMENT '缩略图名称',
    `th_size`           bigint(20)   DEFAULT NULL COMMENT '缩略图大小,单位字节',
    `th_content_type`   varchar(32)  DEFAULT NULL COMMENT '缩略图MIME类型',
    `object_id`         varchar(32)  DEFAULT NULL COMMENT '文件所属对象id',
    `object_type`       varchar(32)  DEFAULT NULL COMMENT '文件所属对象类型,例如用户头像,评价图片',
    `attr`              text COMMENT '附加属性',
    `create_time`       datetime     DEFAULT NULL COMMENT '创建时间',
    PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8
  ROW_FORMAT = DYNAMIC COMMENT ='文件记录表';

  • service
/**
 * @author gtf
 * @date 2022/11/22 15:41
 */
public interface FileService {

    /**
     * 文件上传
     *
     * @param file
     * @return
     */
    String uploadPlatform(MultipartFile file);
}
  • serviceImpl(实现了 FileRecord 接口文件上传完毕后会自动保存到数据库)
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();
    }
}

第10集 账号微服务头像上传阿里云OSS接口和PostMan测试

简介: 账号微服务头像上传阿里云OSS接口和PostMan测试

  • 文件上传流程

    • 先上传文件,返回url地址,再和普通表单一并提交(推荐这种,更加灵活,失败率低)
    • 文件和普通表单一并提交(设计流程比较多,容易超时和失败)
  • 注意:默认SpringBoot最大文件上传是1M,大家测试的时候记得关注下

  • 开发controller

    • @requestPart注解 接收文件以及其他更为复杂的数据类型
   /**
       * 上传用户头像
       *
       * 默认文件大小 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);
  
      }

第五章 账号微服务注册-登录功能开发完善

第1集 账号微服务注册功能业务介绍和代码编写

简介:账号微服务注册接口介绍和业务代码编写

  • 微服务注册接口开发

    • 请求实体类编写
    • controller
    • service
      • 手机验证码验证
      • 密码加密(TODO)
      • 账号唯一性检查(TODO)
      • 插入数据库
      • 新注册用户福利发放(TODO)
    • mapper
  • controller

@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;
    }
  • service
  public enum AuthTypeEnum {

    /**
     * 默认级别
     */
    DEFAULT,

    /**
     * 实名制
     */
    REALNAME,

    /**
     * 企业
     */
    ENTERPRISE;

}
  • NotifyService

    /**
     * 验证码校验
     * @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;
    }
  • AccountService
    /**
     * 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) {

    }
}
  • mapper

第2集 账号微服务开发之登录模块逻辑和解密

简介:账号微服务登录模块开发

  • 核心逻辑
    • 通过phone找数据库记录
    • 获取盐,和当前传递的密码就行加密后匹配
    • 生成token令牌
  • controller
@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;
    }
  • service
@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);
        }
    }

第3集 登录校验Json Web Token实战之封装通用方法

讲解:引入相关依赖并开发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;
        }
    }
}

第5集 登录拦截器InterceptorConfig拦截和放行路径开发配置

简介:用户微服务登录拦截器路径配置和开发


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


    }
}

第六章 流量包模块-海量数据下的分库分表《青铜玩法》

第1集 账号微服务-流量包模块水平分表策略配置



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;
  • pom
        <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

第2集 分布式ID生成器Snowflake自定义wrokId实战

简介: 分布式ID生成器Snowflake自定义wrokId实战

  • 进阶:动态指定sharding jdbc 的雪花算法中的属性work.id属性
    • 使用sharding-jdbc中的使用IP后几位来做workId, 但在某些情况下会出现生成重复ID的情况
      • 解决办法时
        • 在启动时给每个服务分配不同的workId, 引入redis/zk都行,缺点就是多了依赖
        • 启动程序的时候,通过JVM参数去控制,覆盖变量
@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
  • 需求
    • 用户注册-生成的account_no需要是long类型,且全局唯一
  • 利用Sharding-Jdbc封装id生成器
public class IDUtil {

    private static SnowflakeShardingKeyGenerator shardingKeyGenerator = new SnowflakeShardingKeyGenerator();

    /**
     * 雪花算法生成器,配置workId,避免重复
     *
     * 10进制 654334919987691526
     * 64位 0000100100010100101010100010010010010110000000000000000000000110
     *
     * {@link SnowFlakeWordIdConfig}
     *
     * @return
     */
    public static Comparable<?> geneSnowFlakeID(){
        return shardingKeyGenerator.generateKey();
    }
}
  • 修改注册时账号生成策略

第七章 短链服务

第1集 Guava框架里面的Murmur哈希算法测试

简介: Guava框架里面的Murmur哈希算法测试

  • 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);

        }

    }
  • CommonUtil工具类
  /**
     * murmur hash算法
     * @param param
     * @return
     */
    public static long murmurHash32(String param){
        long murmur32 = Hashing.murmur3_32().hashUnencodedChars(param).padToLong();
        return murmur32;
    }

第2集 短链生成组件ShortLinkComponent封装

简介: 短链生成组件ShortLinkComponent封装

  • 创建短链组件类 ShortLinkComponent
    /**
     * 创建短链
     * @param originalUrl
     * @return db编码+6位短链编码
     */
    public String createShortLinkCode(String originalUrl){
        long murmur32 = CommonUtil.murmurHash32(originalUrl);
        //转62进制
        String shortLinkCode = encodeToBase62(murmur32);
        return code;
    }
  • 10进制转62进制
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;
    }
  • 为什么要用62进制转换,不是64进制?
    • 62进制转换是因为62进制转换后只含数字+小写+大写字母
    • 而64进制转换会含有/、+这样的符号(不符合正常URL的字符)
    • 10进制转62进制可以缩短字符,如果我们要6位字符的话,已经有560亿个组合了

第3集 MybatisPlus逆向工具生成短链服务相关java对象


  • 短链-分组
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;
  • 导入生成好的代码
    • model (为啥不放common项目,如果是确定每个服务都用到的依赖或者类才放到common项目)
    • mapper 类接口拷贝
    • resource/mapper文件夹 xml脚本拷贝
    • controller
    • service 不拷贝

第4集 分组crud

crud

  • InterceptorConfig
@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");
    }
}
  • Request
@Data
public class LinkGroupAddRequest  {

    /**
     * 组名
     */
    private String title;
}
@Data
public class LinkGroupUpdateRequest {

    /**
     * 组id
     */
    private Long id;
    /**
     * 组名
     */
    private String title;
}
  • controller
    @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);

    }

  • service
    /**
     * 新增分组
     * @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;
    }
}
  • Manager
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()));
    }
}

  • properties
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分库分表解决方案讲解

  • 数据量预估

    • 首年日活用户: 10万
    • 首年日新增短链数据:10万*50 = 500万
    • 年新增短链数:500万 * 365天 = 18.2亿
    • 往高的算就是100亿,支撑3年
  • 分库分表策略

    • 分库分表

      • 16个库, 每个库64个表,总量就是 1024个表
    • 分片键:短链码 code

      • 比如 g1.fit/92AEva 的短链码 92AEva
    • 分库分表算法:短链码进行hash取模

      库ID = 短链码hash值 % 库数量  
      表ID = 短链码hash值 / 库数量  % 表数量
      
  • 优点

    • 保证数据较均匀的分散落在不同的库、表中,可以有效的避免热点数据集中问题,
    • 分库分表方式清晰易懂
  • 问题

    • 扩容不是很方便,需要数据迁移
    • 需要一次性建立16个库, 每个库64个表,总量就是 1024个表,浪费资源
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}

第6集 短链服务-分库扩容免数据迁移解决方案讲解《黄金玩法》

简介: 短链服务-分库免迁移扩容解决方案讲解《黄金玩法》

  • 短链码
    • 比如 g1.fit/92AEva 的短链码 92AEva
  • 如何做?
    • 从短链码入手-增加库表位
    • 类似案例-阿里这边商品订单号-里面也包括了库表信息(规则不能说)
/**
 * 自定义数据源
 * 配置分片策略
 */
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;
    }
}

第7集 短链服务-分表扩容免数据迁移解决方案讲解《黄金玩法》

短链服务-分表扩容免数据迁移解决方案讲解《黄金玩法》

  • CustomTablePreciseShardingAlgorithm

/**
 * 自定义数据源-分表
 */
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

第8集 短链服务-短链码配置生成库表位实战

简介: 短链服务-短链码配置生成库表位实战

  • 分库位
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();

第9集 短链服务-Manager层模块CRUD开发

简介: 短链服务-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;
    }
}

第10集 短链服务-短链URL跳转302跳转接口开发实战

简介: 短链URL 跳转302跳转接口开发实战

  • 需求

    • 接收一个短链码
    • 解析获取原始地址
    • 302进行跳转
  • 编码实战

  • ShortLinkStateEnum
public enum ShortLinkStateEnum {

    /**
     * 锁定
     */
    LOCK,

    /**
     * 可用
     */
    ACTIVE;

}

  • ShortLinkVO

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;


}

  • ShortLinkService
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;
    }
}   
  • LinkApiController
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);


    }


}

第11集 短链服务-分库分表策略+冗余双写库表架构设计

简介: 短链服务-分库分表冗余双写库表架构设计

  • 分片键:

    • 分库PartitionKey:account_no
    • 分表PartitionKey:group_id
  • 接口访问量

    • C端解析,访问量大
    • B端查询,访问量少,单个表的存储数据可以多点
  • 冗余双写库表设计 group_code_mapping (short_link一样)

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;
  • 短链域名表(前期不分库分表,默认ds0)
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;
    • group_code_mapping
    • domain
  • 拷贝model/mapper/xml

  • 常用接口

    • 新增
    • 详情
    • 删除
    • 分页
    • 更新状态
  • 其他接口用的时候再更新

  • GroupCodeMappingVO
@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;


}

  • DomainTypeEnum
public enum  DomainTypeEnum {

   /**
    * 自建
    */
   CUSTOM,

   /**
    * 官方
    */
   OFFICIAL;
}


  • DomainManager

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));
    }
}


  • GroupCodeMappingManager
@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;
    }

}

第12集 短链服务-Domain短链域名模块开发

简介: 短链服务-Domain短链域名模块开发

  • DomainVO


@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;


}

  • 开发Controller-Service-Manager层接口
@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);
    }

}
  • service

public interface DomainService {


    /**
     * 列举全部可用域名
     * @return
     */
    List<DomainVO> listAll();

}
  • DomainServiceImpl
@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;

    }

}


第13集 短链服务-sharding-jdbc默认数据源配置实战

简介: 短链服务-分库分表默认数据源配置实战

  • 某些表并不需要进行分表分库,未配置分片规则的表将通过默认数据源定位
#----------配置默认数据库,比如短链域名,不分库分表--------------
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}
  • domain模块单元测试

第八章 短链服务-冗余双写MQ架构和开发实战

第1集 交换机类型

交换机类型

  • 交换机类型
    • Direct Exchange 定向
      • 将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配
      • 例子:如果一个队列绑定到该交换机上要求路由键 “aabb”,则只有被标记为“aabb”的消息才被转发,不会转发aabb.cc,也不会转发gg.aabb,只会转发aabb
      • 处理路由健
    • Fanout Exchange 广播
      • 只需要简单的将队列绑定到交换机上,一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息
      • Fanout交换机转发消息是最快的,用于发布订阅,广播形式,中文是扇形
      • 不处理路由健
    • Topic Exchange 通配符
      • 主题交换机是一种发布/订阅的模式,结合了直连交换机与扇形交换机的特点
      • 将路由键和某模式进行匹配。此时队列需要绑定要一个模式上
      • 符号“#”匹配一个或多个词,符号“*”匹配不多不少一个词
      • 例子:因此“abc.#”能够匹配到“abc.def.ghi”,但是“abc.*” 只会匹配到“abc.def”。

第2集 冗余双写MQ架构RabbitMQ配置开发实战

简介: 冗余双写MQ架构RabbitMQ配置开发实战

  • mq配置

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);

    }
}


第3集 冗余双写MQ架构-短链和mapping消费者配置

简介: 冗余双写MQ架构-短链和mapping消费者配置

  • ShortLinkAddLinkMQListener
@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);

    }



}


  • ShortLinkAddMappingMQListener
@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);

    }
}

第4集 冗余双写MQ架构-消费者配置自动创建队列和集群测试

简介: 冗余双写MQ架构-MQ消费者配置自动创建队列

  • controller-service层开发
  • ShortLinkAddRequest
@Data
public class ShortLinkAddRequest {


    /**
     * 组
     */
    private Long groupId;

    /**
     * 短链标题
     */
    private String title;

    /**
     * 原生url
     */
    private String originalUrl;

    /**
     * 域名id
     */
    private Long domainId;

    /**
     * 域名类型
     */
    private String domainType;

    /**
     * 过期时间
     */
    private Date expired;

}

  • ShortLinkController

@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;
   }


}
  • service

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();
   }
  • 配置文件配置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=auto
  • 大家遇到的问题 (不会自动创建队列)

    • 加了@bean配置交换机和queue,启动项目却没自动化创建队列
    • RabbitMQ懒加载模式, 需要配置消费者监听才会创建,
    @RabbitListener(queues = "short_link.add.link.queue")
    
  • 另外种方式(若Mq中无相应名称的队列,会自动创建Queue)

    @RabbitListener(queuesToDeclare = { @Queue("short_link.add.link.queue") })
    
  • 链路测试-多节点启动

第5集 冗余双写架构-MQ消费者异常重试处理方案链路讲解

简介: 冗余双写架构-MQ消费者异常处理方案讲解

  • 消费者异常情况处理
    • 业务代码自己重试
    • 组件重试
  • 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=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
  • 解决方式:RepublishMessageRecoverer
    • 消费消息重试一定次数后,用特定的routingKey转发到指定的交换机中,方便后续排查和告警
    • 注意
      • 消息消费确认使用自动确认方式
##----------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
  • RabbitMQErrorConfig
@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("告警成功,发送通知短信");

    }
}

第6集 冗余双写架构-商家创建短链-C/b端消费者开发实战

简介: 冗余双写架构-商家创建短链-C/b端消费者开发实战

  • 前置配置
public enum  EventMessageType {

    /**
     * 短链创建
     */
    SHORT_LINK_ADD,


    /**
     * 短链创建 C端
     */
    SHORT_LINK_ADD_LINK,

    /**
     * 短链创建 B端
     */
    SHORT_LINK_ADD_MAPPING;
}

  • mq
@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);

    }



}

  • C/b端消费者开发实战
 /**
     * 处理短链新增逻辑
     * <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;
    }

第8集 MurmurHash短链码改进之生成固定库表位编码实战

简介: 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);
    }

第7集 同个URL生成不唯一短链码问题和解决方案编码实战

简介: 同个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;
    }

第8集 短链码基于Redis lua实现分布式锁

简介:基于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

  • handlerAddShortLink
    @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表
##---------- 组+短链码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;
    }

第9集 短链服务-删除和更新Controller层开发实战

简介: 短链服务-删除和更新Controller层开发实战

  • request

@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;

}
  • controller

 /**
     * 删除短链
     * @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;
    }
  • service
  @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;
    }


第10集 冗余双写MQ实现-删除/更新短链-交换机和队列绑定配置实战

  • ShortLinkDelLinkMQListener
@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);

    }



}

  • ShortLinkDelMappingMQListener
@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);

    }



}
  • ShortLinkUpdateLinkMQListener
@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);
    }
}

第九章 流量包商品服务与订单

第1集 流量包商品服务-数据库表介绍和实体类生成

简介: 流量包商品服务-数据库表介绍

  • 数据库表介绍
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');
  • MybatisPlus实体类生成

第2集 流量包商品服务-商品列表和详情接口链路开发

简介: 流量包商品服务-商品列表和详情接口链路开发

  • ProductVO
@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;
}
  • ProductController


    /**
     * 查看商品列表接口
     * @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);
    }
  • ProductService


    List<ProductVO> list();

    ProductVO findDetailById(long productId);
  • ProductServiceImpl
@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;
    }

}

第3集 流量包订单-数据库表介绍和实体类生成

简介: 流量包订单-数据库表介绍和实体类生成

  • 数据库表
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;
  • 数据库实体类生成

第4集 流量包订单-数据库表分库分表讲解和配置

简介: 流量包订单-数据库表分库分表讲解和配置

  • 业务需求

    • 用户查看自己的订单列表
  • 数据存储需求(都是前期规划,上线前可以调整分库分表策略和数量)

    • 未来2年,短链平台累计5百万用户
    • 付费流􏰀包记录:
      • 一个用户10条/年,总􏰀就是5千万条/年,两年是1亿
      • 单表不超过1千万数据,需要分10张表
      • 进一步延伸,进行水平分表,比如 2张表、4张表、8张 表、16张表
    • 分表数􏰀:线上分16张表,本地分2张表即可
  • 分片key

    • account_no作为partitionKey

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


第5集 下单

  • vo

@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;


}

  • enum
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;
}
  • TimeConstant
public class TimeConstant {

    /**
     * 默认30分钟超时未支付,单位毫秒
     */
    public static final long ORDER_PAY_TIMEOUT_MILLS =1000 * 60 * 30;

}
  • ConfirmOrderRequest

@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;
}
  • ProductOrderController

@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);
        }

    }


}


  • service

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);
        }

    }


}

第6集 订单防重提交-自定义注解开发

订单防重提交-自定义注解开发

  • RepeatSubmit
/**
* 自定义防重提交
*/
@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;

}

  • rediskey

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";
}
  • RedissionConfiguration
@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;
    }*/



}
  • RepeatSubmitAspect
@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;

    }


}

第7集 RabbitMQ死信队列-延迟消息知识点回顾

简介:RabbitMQ死信队列-延迟消息知识点回顾

  • 什么是rabbitmq的死信队列
    • 没有被及时消费的消息存放的队列
  • 什么是rabbitmq的死信交换机
    • Dead Letter Exchange(死信交换机,缩写:DLX)当消息成为死信后,会被重新发送到另一个交换机,这个交换机就是DLX死信交换机。

消息有哪几种情况成为死信

  • 消费者拒收消息**(basic.reject/ basic.nack)**,并且没有重新入队 requeue=false

  • 消息在队列中未被消费,且超过队列或者消息本身的过期时间TTL(time-to-live)

  • 队列的消息长度达到极限

  • 结果:消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列

  • 什么是延迟队列

    • 一种带有延迟功能的消息队列,Producer 将消息发送到消息队列 服务端,但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某一个时间投递到 Consumer 进行消费,该消息即定时消息
  • 业界的一些实现方式

    • 定时任务高精度轮训
    • 采用RocketMQ自带延迟消息功能
    • RabbitMQ本身是不支持延迟队列的,怎么办?
      • 结合死信队列的特性,就可以做到延迟消息

第8集 下单接口-超时关闭订单消费者

下单接口-超时关闭订单消费者

  • mq
@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);


    }


}

  • service
 /**
     * //延迟消息的时间 需要比订单过期 时间长一点,这样就不存在查询的时候,用户还能支付成功
     *
     * //查询订单是否存在,如果已经支付则正常结束
     * //如果订单未支付,主动调用第三方支付平台查询订单状态
     *     //确认未支付,本地取消订单
     *     //如果第三方平台已经支付,主动的把订单状态改成已支付,造成该原因的情况可能是支付通道回调有问题,然后触发支付后的动作,如何触发?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;
    }

第十章 微信支付

第1集 微信支付-Maven依赖加入和代码参数准备

简介:微信支付-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>
  • config
@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;
}

第2集 微信支付-商户私钥证书代码读取开发实战

简介:微信支付-商户私钥证书代码读取开发实战


@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;
    }



}

第3集 快速验证

  • WechatPayApi
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";

}

  • WechatPayTest

@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();
        }
    }
}

第4集 多渠道支付对接-策略模式+工厂模式编码实战

简介:多渠道支付对接-策略模式+工厂模式编码实战

  • 策略接口开发 PayStrategy

  • 策略上下文 PayStrategyContext开发

  • 具体支付策略开发 AlipayStrategy、WechatPayStrategy

  • 简单工厂类开发

  • PayStrategy 策略类

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

}
  • PayStrategyContext 策略上下文
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 "";
    }
}

第5集 微信支付V3版本回调+验签开发实战

@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();
    }
}

第6集 微信支付回调通知

@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();

    }


}

第十一章 账户服务

第1集 账号服务-RabbitMQ相关配置开发实战

简介:账号服务-RabbitMQ相关配置开发实战

  • 账号服务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

第2集 流量包crud

流量包crud

  • TrafficManager

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);


}

  • TrafficManagerImpl
@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));
    }
}

第3集 流量包权益发放业务逻辑开发

流量包权益发放业务逻辑开发

  • TrafficMQListener
@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);

    }

}


  • TrafficServiceImpl
@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);

        }


    }



}


第4集 免费流量包发送

  • RabbitMQConfig
@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);

    }

}

  • userRegisterInitTask
    /**
     * 用户初始化,发放福利:流量包
     * @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);

    }

第十二章 流量包过期/扣减

第1集 分布式调度XXl-Job搭建-Docker部署服务端

分布式调度XXl-Job搭建-Docker部署服务端

  • 步骤

    • 步骤一:数据库脚本(使用mysql8)
      • xxl_job 的数据库里有如下几个表
      • xxl_job_group:执行器信息表,用于维护任务执行器的信息
      • xxl_job_info:调度扩展信息表,主要是用于保存xxl-job的调度任务的扩展信息,比如说像任务分组、任务名、机器的地址等等
      • xxl_job_lock:任务调度锁表
      • xxl_job_log:日志表,主要是用在保存xxl-job任务调度历史信息,像调度结果、执行结果、调度入参等等
      • xxl_job_log_report:日志报表,会存储xxl-job任务调度的日志报表,会在调度中心里的报表功能里使用到
      • xxl_job_logglue:任务的GLUE日志,用于保存GLUE日志的更新历史变化,支持GLUE版本的回溯功能
      • xxl_job_registry:执行器的注册表,用在维护在线的执行器与调度中心的地址信息
      • xxl_job_user:系统的用户表
  • 步骤二:部署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
    

第2集 AlibabaCloud微服务整合XXL-Job依赖实战

简介: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>
  • common项目添加依赖
        <!--分布式调度-->
        <dependency>
            <groupId>com.xuxueli</groupId>
            <artifactId>xxl-job-core</artifactId>
        </dependency>
  • 新增logback.xml
<?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>
  • application.properties配置文件
#----------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;
    }
}

第3集 付费流量包过期淘汰需求开发-整合分布式调度XXL-Job

简介:付费流量包过期淘汰需求开发-整合分布式调度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 >>>>>");
    }

}


第4集 海量数据下流量包更新-惰性策略编码开发实战

海量数据下流量包更新-惰性策略编码开发实战

  • 伪代码

    • 查询用户全部可用流量包

    • 遍历用户可用流量包

      • 判断是否更新-用日期判断
        • 没更新的流量包后加入【待更新集合】中
          • 增加【今天剩余可用总次数】
        • 已经更新的判断是否超过当天使用次数
          • 如果没超过则增加【今天剩余可用总次数】
          • 超过则忽略
    • 更新用户今日流量包相关数据

    • 扣减使用的某个流量包使用次数

  • 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);



    /**
     * 删除过期流量包
     * @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>
  • UseTrafficVO

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UseTrafficVO {
    /**
     * 天剩余可用总次数 = 总次数 - 已用
     */
    private Integer dayTotalLeftTimes;


    /**
     * 当前使用的流量包
     */
    private TrafficDO currentTrafficDO;


    /**
     * 记录没过期,但是今天没更新的流量包id
     */
    private List<Long> unUpdatedTrafficIds;
}
  • TrafficController

   /**
    * * 查询用户全部可用流量包
    * * 遍历用户可用流量包
    *   * 判断是否更新-用日期判断
    *     * 没更新的流量包后加入【待更新集合】中
    *       * 增加【今天剩余可用总次数】
    *     * 已经更新的判断是否超过当天使用次数
    *       * 如果没超过则增加【今天剩余可用总次数】
    *       * 超过则忽略
    *
    * * 更新用户今日流量包相关数据
    * * 扣减使用的某个流量包使用次数
    * @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;
   }

第5集 高并发下-创建短链和流量包业务联动开发

简介:短链服务-创建短链和流量包业务联动开发

  • 需求

    • 创建短链
    • 扣减流量包
    • 发送MQ
  • 编码实战

    /**
     * 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);

第6集 流量包锁定任务表设计和创建讲解

简介: 流量包锁定任务表设计和创建讲解

  • 流量包锁定任务表
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;
  • 改动

    • 不叫messageId,存在误解改为bizId(存储短链码,检查是否创建短链成功)
    • POJO类改动
    • 创建LOCK状态枚举类

第十三章 大数据

第1集 短链平台-数据可视化整体链路讲解和命名规范

简介:短链平台-数据可视化整体链路讲解

  • 短链平台整体链路

  • 数据分层处理概述

数据分层 分层描述 数据生成计算工具 存储
ODS 原生数据,短链访问基本信息 SpringBoot生成 Kafka
DWD 对 ODS 层做数据清洗和规范化,新老访客标记等 Flink Kafka
DWM 对DWD数据进一步加工补齐数据,独立访客统计,操作系统/ip/城市,做宽表 Flink kafka
DWS 对DWM进行处理,多流合并,分组|聚合|开窗|统计,形成主题宽表 Flink ClickHouse
ADS 从ClickHouse中读取数据,根据需求进行筛选聚合,可视化展示 ClickHouseSql web可视化展示
  • 命名规范
    • ODS层命名为ods_表名|主题名
    • DWD层命名为dwd_表名|主题名
    • DWM层命名为dwm_表名|主题名
    • DWS层命名为dws_表名|主题名

第3集 Docker容器化部署Kafka+Zookeeper实战

简介: Docker容器化部署Kafka+Zookeeper实战

  • 部署zk
docker run -d --name zookeeper -p 2181:2181 -t wurstmeister/zookeeper
  • 部署kafka
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的Container ID,进入容器内部,创建topic
    • docker exec -it ${CONTAINER ID} /bin/bash
    • 进入kafka默认目录 cd /opt/kafka 就跟一般的kafka一样了
#创建一个主题:    
kafka-topics.sh --create --zookeeper 124.221.200.246:2181 --replication-factor 1 --partitions 1 --topic mykafka

第3集 数据日志采集开发实战之发送Kafka消息

数据日志采集开发实战之发送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>

  • LogService
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);

   }
}

  • Kafak依赖和配置
#----------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
  • Kafka命令
创建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

第4集 Flink实时计算项目搭建和依赖配置引入

简介: 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();


    }


}

第5集 代码复用性提升-Kafka工具类封装开发实战

@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());
    }

}

第6集 DwdShortLinkLogApp

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;
        }

    }



}

  • 留存数据

    • 通过nc -lk测试
    {"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"}}
    

第7集 浏览器头User-Agent提取工具UserAgentUtils

简介: 浏览器头User-Agent提取工具UserAgentUtils讲解

  • UserAgentUtils工具介绍
    • 一个用来解析 User-Agent 字符串的 Java 类库,
    • 可以识别 浏览器名字,浏览器组,浏览器类型,浏览器版本,浏览器的渲染引擎,android和ios设备的类型
    • 超过150种不同的浏览器; 7种不同的浏览器类型; 9种不同的Web应用
    • 超过60种不同的操作系统; 6种不同的设备类型; 9种不同的渲染引擎;
  • 工具类依赖
 <!-- 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;
    }

第8集Linux云服务器-ClickHouse部署安装实战

  • RPM包安装
    • Linux机器安装ClickHouse,版本:ClickHouse 22.1.2.2,保持一致即可
      • 文档地址:https://clickhouse.com/docs/zh/getting-started/install/
      • 课程资料提供安装包,上传到Linux服务器
        • 直接使用rpm -ivh后面跟上所有的包安装就可以了
        • 基本上不缺少其他依赖,安装之后clickhouse会自动加到systemd启动当中
#各个节点上传到新建文件夹
/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
  • 网络安全组记得开放http端口是8123,tcp端口是9000, 同步端口9009

    • 常规企业内网通信则不用,我们是阿里云部署,本地测试
    • web可视化界面:http://ip:port/play
  • 通过ClickHouse可视化工具连接

    • 课程资料文件夹提供软件
      • win和mac苹果都有
  • 其他安装方式

    • Docker
    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
    

第十四章 新版Gateway网关实战和避坑指南

第1集 新版Gateway网关搭建配置实战和避坑指南

  • 添加配置

    <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);
    }

}

第2集 微服务Docker打包插件介绍和Dockerfile编写

简介:微服务Docker打包插件介绍和配置实战

  • 微服务采用容器化部署->本地推送镜像到镜像仓库->Paas容器云管理平台拉取部署

    • SpringBoot打包插件配置
    • 聚合工程pom添加全局变量
    <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"]
    

第3集 微服务Docker镜像打包实战和注意事项

简介:多个微服务Docker镜像打包实战

  • 多个微服务本地镜像打包

    • 步骤一:最外层 mvn clean install
    • 步骤二:去到子模块pom文件下
    mvn install -Dmaven.test.skip=true dockerfile:build
    
  • 注意点

    • 本地电脑要安装Docker,才可以构建成功,Mac、Win都是,网上搜索博文进行安装
    • 安装失败或者不想安装则不用,因为这个只是常规操作,接下去会有DevOps自动化构建链路
  • 问题点:如果发现运行的镜像不是最新的

    • 项目的路径一定不要有中文和空格
    • 建议mvn clean install 构建下项目,
    • 再把本地历史docker镜像删除,再重新构建打包镜像
  • 本地运行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

第十五章 devops

第1集 jenkins

  • 什么是Jenkins

    • 是一个开源的、提供友好操作界面的持续集成(CI)工具,主要用于持续、自动的构建/测试软件项目、监控外部任务的运行,用Java语言编写,可在Tomcat等流行的servlet容器中运行,也可独立运行
    • 官方文档 https://www.jenkins.io/
    • 小滴课堂有专门的专题视频
  • Linux云服务器部署Jenkins

    • 在使用Jenkins自动化部署之前,首先安装Docker容器
  • 安装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

    • 创建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登录页面

      • 网络安全组记得开放 9302 端口
    • 获取登录Jenkins的密码, 把获取的密码复制上去

      cat /root/docker/jenkins/secrets/initialAdminPassword
      
  • 容器内部配置JDK

    • 路径为主机Jenkins容器内部里的JAVA_HOME,也就是echo $JAVA_HOME查看JAVA_HOME路径
    /opt/java/openjdk
    
  • 插件页面下载插件

    • Maven Integration、docker Pipeline、docker API 、docker、docker commons

第2集 Jenkins构建微服务脚本编写实战

  • 配置git

  • Pre Steps

echo "登录阿里云镜像"
docker login --username=小高同学java registry.cn-hangzhou.aliyuncs.com --password=Iphone
echo "构建dcloud-common"
cd dcloud-common
mvn install
ls -alh
  • Post Steps
  • Run only if build succeeds
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 "=======构建脚本执行完毕====="

第3集 云服务器Docker容器化部署Rancher2.x实战

简介:云服务器Docker容器化部署Rancher2.x实战

  • 注意事项

    • 基于Docker镜像安装Rancher,阿里云 Linux CentOS 7.8 + Docker-20.10.10

      • 2核8g内存+5M带宽
    • 选择Rancher机器可以把【内存+带宽】弄大点,因为Rancher部署好后需要下载很多的镜像,不要因为内存问题导致部署没成功

  • 安装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

    • 创建Rancher挂载目录
    mkdir -p /data/rancher_home/rancher
    mkdir -p /data/rancher_home/auditlog
    
    • 部署Rancher
    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

    • 启动成功rancher后, 可以打开浏览器输入IP地址来进入Rancher
    • 登录地址为:http://+IP ,如:https://47.99.155.140
    • 配置账号密码,填写完账号密码后直接Continue即可。
  • 配置阿里云镜像加速地址

    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
    

第4集 Rancher2.X配置私有镜像仓库实战

  • 私有镜像仓库配置
    集群进去
  • 在顶部的导航栏找到-资源->密文
  • 最右边有添加凭证按钮,点击进去
    • 添加镜像库凭证
    • 成功添加镜像库凭证

第5集 Rancher2.X部署中间件-Mysql8.0

简介:Rancher2.X部署中间件-Mysql8.0

  • 注意

    • 常规互联网企业里面的中间件,运维工程师负责搭建
    • 部分组件可以选择直接部署:容器、源码编译,并非一定要某个方式
      • 比如某些组件需要特定的版本,但是平台存在不兼容或者配置方式不支持等
  • 部署Mysql

    • 需要【两个节点】

      • 一个是Nacos和Xxl-Job使用
        • 名称 midware-mysql
        • 端口 :3306:3306
      • 一个是业务微服务使用,如果有需要可以每个微服务一个节点
        • 名称:service-mysql
        • 端口 :3307:3306
    • 配置

      镜像: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 为只读
      

第6集 Ranche2.X部署Nacos和调整JVM内存实战

简介:Ranche2.X部署Nacos和调整JVM内存实战

  • 部署Nacos
镜像: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:新生代大小

第7集 Ranche2.X部署XXL-Job和Redis6实战

简介:Ranche2.X部署XXL-Job和Redis6实战

  • 注意

    • 很多中间件并非要Rancher部署,常规源码或者Docker部署就行
    • 课程方便管理为主则用Rancher部署
    • 实际公司里面,运维负责搭建安装部署,开发人员使用即可
  • 部署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

    • 在rancher中设置的,redis密码不是环境变量,而是命令入口配置
    • 网络安全组记得开放端口
    镜像:redis:6.2.4
    端口:6379:6379
    
    数据卷:
    /mydata/redis/data
    /data
    
    入口命令
    redis-server --requirepass classes.net
    

第8集 anche2.X部署RabbitMQ和ClickHouse实战

简介:Ranche2.X部署RabbitMQ和ClickHouse实战

  • 部署RabbitMQ
镜像:rabbitmq:3.8.15-management
端口:15672:15672
      5672:5672

环境变量:
RABBITMQ_DEFAULT_PASS=password
RABBITMQ_DEFAULT_USER=admin
  • 部署ClickHouse
镜像: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
  • dbeaver连接测试
    • 默认http端口是8123,tcp端口是9000, 同步端口9009
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);

第9集 Ranche2.X部署Zookeeper和Kafka

简介:Ranche2.X部署Zookeeper和Kafka

  • 部署Zookeeper
镜像:wurstmeister/zookeeper
端口:2181:2181

第10集 Rancher2.X部署Skywalking-OAP-Server+UI

简介:Rancher2.X部署Skywalking-OAP-Server+UI

  • 部署Skywalking-OAP-Server
镜像: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
  • 部署Skywalking-UI
镜像:apache/skywalking-ui:8.5.0
端口:8080:8000 (左边是容器端口,右边是宿主机端口)

环境变量(oap是上面定义的容器服务名称)
SW_OAP_ADDRESS=oap:12800
TZ=Asia/Shanghai

第十六章 整合Nacos配置中心和域名配置

第1集 账号服务整合Nacos配置中心开发和配置

简介:账号整合Nacos配置中心开发和配置

  • common项目引入配中心(已经加入)
 <!--配置中心, 留坑,后续用的时候再讲,解决方式,看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>
  • 配置文件优先级讲解

    • 不能使用原先的application.properties, 需要使用 bootstrap.properties作为配置文件
    • 配置读取优先级 bootstrap.properties > application.properties
  • 配置实操

    • 服务迁移配置 增加bootstrap.properties
    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
    

第2集 前后端联调-前端不能识别雪花算法id解决方案

简介: 前后端联调-前端不能识别雪花算法id解决

  • 问题

    • 雪花算法生成的id作为主键时,因为其长度为19位

    • 而前端JS一般能处理16位,如果不处理的话在前端会造成精度丢失,最后两位会变成00

    • 后端 解决方式

    • 直接把id类型改为String就行,使用JackSon包的注解

    • 对应的实体类主键属性加入注解@JsonSerialize

    @JsonSerialize(using = ToStringSerializer.class)
    @TableId
    private Long id;
    
  • 前端 解决方式

    • 前端使用 json-bigint 模块进行处理,一般都是用axios数据请求
    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: '请求时间过长,请联系后端或者优化请求',
    })
    
0

评论区