当前位置:   article > 正文

SpringCloudAlibaba 综合项目实战工业级PaaS云平台第二课 逆向工程和接口开发_工业级paas云平台+springcloudalibaba 综合项目实战 课件

工业级paas云平台+springcloudalibaba 综合项目实战 课件

第六章 数据库逆向工程整合SpringCloud Alibaba+编码分层规范

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

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

  • 介绍

    • 底层是模板引擎技术,可以自定义生成的java类模板
  • 基础版mybatis-genarator

  • 进阶版mybatis-plus-genarator

  • 添加依赖

    • 统一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-->
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 代码(标记TODO的记得修改)
public class MyBatisPlusGenerator {

    public static void main(String[] args) {
        //1. 全局配置
        GlobalConfig config = new GlobalConfig();
        // 是否支持AR模式
        config.setActiveRecord(true)
                // 作者
                .setAuthor("二当家小D")
                // 生成路径,最好使用绝对路径,window路径是不一样的
                //TODO  TODO  TODO  TODO
                .setOutputDir("/Users/xdclass/Desktop/demo/src/main/java")
                // 文件覆盖
                .setFileOverride(true)
                // 主键策略
                .setIdType(IdType.AUTO)

                .setDateType(DateType.ONLY_DATE)
                // 设置生成的service接口的名字的首字母是否为I,默认Service是以I开头的
                .setServiceName("%sService")

                //实体类结尾名称
                .setEntityName("%sDO")

                //生成基本的resultMap
                .setBaseResultMap(true)

                //不使用AR模式
                .setActiveRecord(false)

                //生成基本的SQL片段
                .setBaseColumnList(true);

        //2. 数据源配置
        DataSourceConfig dsConfig = new DataSourceConfig();
        // 设置数据库类型
        dsConfig.setDbType(DbType.MYSQL)
                .setDriverName("com.mysql.cj.jdbc.Driver")
                //TODO  TODO  TODO  TODO
                .setUrl("jdbc:mysql://127.0.0.1:3306/xdclass_user?useSSL=false")
                .setUsername("root")
                .setPassword("xdclass.net");

        //3. 策略配置globalConfiguration中
        StrategyConfig stConfig = new StrategyConfig();

        //全局大写命名
        stConfig.setCapitalMode(true)
                // 数据库表映射到实体的命名策略
                .setNaming(NamingStrategy.underline_to_camel)

                //使用lombok
                .setEntityLombokModel(true)

                //使用restcontroller注解
                .setRestControllerStyle(true)

                // 生成的表, 支持多表一起生成,以数组形式填写
                //TODO  TODO  TODO  TODO
                .setInclude("user","address");

        //4. 包名策略配置
        PackageConfig pkConfig = new PackageConfig();
        pkConfig.setParent("net.xdclass")
                .setMapper("mapper")
                .setService("service")
                .setController("controller")
                .setEntity("model")
                .setXml("mapper");

        //5. 整合配置
        AutoGenerator ag = new AutoGenerator();
        ag.setGlobalConfig(config)
                .setDataSource(dsConfig)
                .setStrategy(stConfig)
                .setPackageInfo(pkConfig);

        //6. 执行操作
        ag.execute();
        System.out.println("======= 小滴课堂 Done 相关代码生成完毕  ========");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 导入生成好的代码
    • model (为啥不放common项目,如果是确定每个服务都用到的依赖或者类才放到common项目)
    • mapper 类接口拷贝
    • resource/mapper文件夹 xml脚本拷贝
    • controller
    • service 不拷贝
第2集 项目编码规范介绍-专用名词和POJO实体类约定

简介:项目编码规范介绍和POJO实体类约定

  • 注意
    • 使用起来和普通版的mybatis generator一样,但是这个纯代码,不用复杂xml配置
    • 任何框架,不要使用过多的侵入或者框架定制化深的内容,防止后续改动耦合性高,成本大
  • N方库说明
一方库: 本工程内部子项目模块依赖的库(jar 包)。
二方库: 公司内部发布到中央仓库,可供公司内部其它应用依赖的库(jar包)。
三方库: 公司之外的开源库(jar 包)。
  • 1
  • 2
  • 3
  • POJO实体类
POJO(Plain Ordinary Java Object): 在手册中,POJO 专指只有 setter / getter / toString的简单类,包括DO/DTO/BO/VO等, 禁止命名成xxxPOJO
  • 1
  • 各个层级约束规范
A) Service/DAO层方法命名规约
	1) 获取单个对象的方法用get做前缀。
	2) 获取多个对象的方法用list做前缀,复数形式结尾如:listObjects。 
	3) 获取统计值的方法用count做前缀。
	4) 插入的方法用save/insert做前缀。
	5) 删除的方法用remove/delete做前缀。
	6) 修改的方法用update做前缀。

B) 领域模型命名规约
  1) 数据对象:xxxDO,xxx即为数据表名。
  2) 一般数据传输对象:xxxDTO,xxx为业务领域相关的名称,项目里面也用VO。 
  3) 展示对象:xxxVO,也就是响应给前端的实体包装类。
  4) 接收前端json对象请求的命名为 XXXRequest
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 更多开发规范,可以参考阿里巴巴编码手册(资料里面有)
    • 有兴趣的同学也可以安装IDEA编码扫描插件,不过前期可以先不用开启,会影响电脑卡顿和强迫症

第七章 用户微服务基础模块开发+工具类封装+SwaggerUI3.0整合

第1集 用户微服务项目开发之收货地址查询接口开发

简介:用户微服务数据库配置和查询个人收货地址接口

  • 配置文件配置 application.yml
server:
  port: 9001

spring:
  application:
    name: xdclass-user-service

#数据库配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/xdclass_user?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: xdclass.net
    
#配置plus打印sql日志
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl


#设置日志级别,ERROR/WARN/INFO/DEBUG,默认是INFO以上才显示
logging:
  level:
    root: INFO
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • springboot启动类注解
@SpringBootApplication
@MapperScan("net.xdclass.mapper")
  • 1
  • 2
  • 查询地址详情接口(目的:测试连接数据库)
第2集 微服务项目整合SwaggerUI3.0生成文档

简介:微服务整合SwaggerUI3.0接口文档

  • Postman配置多环境操作

    • 方便未来切换环境
      • 开发
      • 测试
      • 预发布
      • 线上
        在这里插入图片描述
  • common项目 新增SwaggerUI3.0依赖,parent项目已经声明了版本

       <!--swagger ui接口文档依赖-->
       <dependency>
           <groupId>io.springfox</groupId>
           <artifactId>springfox-boot-starter</artifactId>
       </dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • SwaggerConfiguration配置类开发
@Component
@EnableOpenApi
@Data
public class SwaggerConfiguration {


    @Bean
    public Docket webApiDoc(){


        return new Docket(DocumentationType.OAS_30)
                .groupName("用户端接口文档")
                .pathMapping("/")
                // 定义是否开启swagger,false为关闭,可以通过变量控制,线上关闭
                .enable(true)
                //配置api文档元信息
                .apiInfo(apiInfo())
                // 选择哪些接口作为swagger的doc发布
                .select()
                .apis(RequestHandlerSelectors.basePackage("net.xdclass"))
                //正则匹配请求路径,并分配至当前分组
                .paths(PathSelectors.ant("/api/**"))
                .build();
    }



    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("1024电商平台")
                .description("微服务接口文档")
                .contact(new Contact("小滴课堂-二当家小D", "https://xdclass.net", "794666918@qq.com"))
                .version("12")
                .build();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • AddressController配置接口文档
    • 访问地址 http://localhost:9001/swagger-ui/index.html#/
第3集 微服务项目整合SwaggerUI3.0接口文档分组和Header头定义

简介:SwaggerUI3.0接口文档分组和Header头定义

  • 接口文档分组,用户端、管理后台
    @Bean
    public Docket adminApiDoc(){
        return new Docket(DocumentationType.OAS_30)
                .groupName("管理端接口文档")
                .pathMapping("/")
                // 定义是否开启swagger,false为关闭,可以通过变量控制,线上关闭
                .enable(true)
                //配置api文档元信息
                .apiInfo(apiInfo())
                // 选择哪些接口作为swagger的doc发布
                .select()
                .apis(RequestHandlerSelectors.basePackage("net.xdclass"))
                //正则匹配请求路径,并分配至当前分组
                .paths(PathSelectors.ant("/admin/**"))

                .build();
    }


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 请求需要自定义http头,比如登录token令牌
    @Bean
    public Docket webApiDoc(){


        return new Docket(DocumentationType.OAS_30)
                .groupName("用户端接口文档")
                .pathMapping("/")
                // 定义是否开启swagger,false为关闭,可以通过变量控制,线上关闭
                .enable(true)
                //配置api文档元信息
                .apiInfo(apiInfo())
                // 选择哪些接口作为swagger的doc发布
                .select()
                .apis(RequestHandlerSelectors.basePackage("net.xdclass"))
                //正则匹配请求路径,并分配至当前分组
                .paths(PathSelectors.ant("/api/**"))
                //正则匹配请求路径,并分配至当前分组,当前所有接口
                .paths(PathSelectors.any())

                .build()

                //新版swagger3.0配置
                .globalRequestParameters(getGlobalRequestParameters())
                .globalResponses(HttpMethod.GET, getGlobalResponseMessage())
                .globalResponses(HttpMethod.POST, getGlobalResponseMessage());

    }
    
   
   /**
     * 生成全局通用参数, 支持配置多个响应参数
     * @return
     */
    private List<RequestParameter> getGlobalRequestParameters() {
        List<RequestParameter> parameters = new ArrayList<>();
        parameters.add(new RequestParameterBuilder()
                .name("token")
                .description("登录令牌")
                .in(ParameterType.HEADER)
                .query(q -> q.model(m -> m.scalarModel(ScalarType.STRING)))
                .required(false)
                .build());

//        parameters.add(new RequestParameterBuilder()
//                .name("version")
//                .description("版本号")
//                .required(true)
//                .in(ParameterType.HEADER)
//                .query(q -> q.model(m -> m.scalarModel(ScalarType.STRING)))
//                .required(false)
//                .build());
        return parameters;
    }

    /**
     * 生成通用响应信息
     * @return
     */
    private List<Response> getGlobalResponseMessage() {
        List<Response> responseList = new ArrayList<>();
        responseList.add(new ResponseBuilder().code("4xx").description("请求错误,根据code和msg检查").build());
        return responseList;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
第4集 统一接口响应协议-响应工具类封装

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

  • 统一业务状态码 BizCodeEnum开发
    • 状态码定义约束,共6位数,前三位代表服务,后3位代表接口
    • 比如 商品服务210,购物车是220、用户服务230,403代表权限
/**
 * 小滴课堂,愿景:让技术不再难学
 *
 * @Description 状态码定义约束,共6位数,前三位代表服务,后4位代表接口
 *  比如 商品服务210,购物车是220、用户服务230,403代表权限
 *
 **/
public enum  BizCodeEnum {


    /**
     * 通用操作码
     */
    OPS_REPEAT(110001,"重复操作"),

  	/**
     *验证码
     */
    CODE_TO_ERROR(240001,"接收号码不合规"),
    CODE_LIMITED(240002,"验证码发送过快"),
    CODE_ERROR(240003,"验证码错误"),
    CODE_CAPTCHA(240101,"图形验证码错误"),

    /**
     * 账号
     */
    ACCOUNT_REPEAT(250001,"账号已经存在"),
    ACCOUNT_UNREGISTER(250002,"账号不存在"),
    ACCOUNT_PWD_ERROR(250003,"账号或者密码错误");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 接口统一协议 JsonData工具类开发
@Data
@AllArgsConstructor
@NoArgsConstructor
public class JsonData {

    /**
     * 状态码 0 表示成功,1表示处理中,-1表示失败
     */

    private Integer code;
    /**
     * 数据
     */
    private Object data;
    /**
     * 描述
     */
    private String msg;


    /**
     * 成功,传入数据
     * @return
     */
    public static JsonData buildSuccess() {
        return new JsonData(0, null, null);
    }

    /**
     *  成功,传入数据
     * @param data
     * @return
     */
    public static JsonData buildSuccess(Object data) {
        return new JsonData(0, data, null);
    }

    /**
     * 失败,传入描述信息
     * @param msg
     * @return
     */
    public static JsonData buildError(String msg) {
        return new JsonData(-1, null, msg);
    }

   
    /**
     * 自定义状态码和错误信息
     * @param code
     * @param msg
     * @return
     */
    public static JsonData buildCodeAndMsg(int code, String msg) {
        return new JsonData(code, null, msg);
    }

    /**
     * 传入枚举,返回信息
     * @param codeEnum
     * @return
     */
    public static JsonData buildResult(BizCodeEnum codeEnum){
        return JsonData.buildCodeAndMsg(codeEnum.getCode(),codeEnum.getMessage());
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
第5集 微服务自定义全局异常+处理器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();
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 自定义异常处理器
@ControllerAdvice
@Slf4j
public class ExceptionHandle {

    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public JsonData Handle(Exception e) {

        if (e instanceof BizException) {
            BizException bizException = (BizException) e;
            log.info("[业务异常]{}", e);
            return JsonData.buildError(bizException.getMsg(), bizException.getCode());

        } else {
            log.info("[系统异常]{}", e);
            return JsonData.buildError("全局异常,未知错误");
        }

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
第6集 微服务项目集成Spring Boot Test单元测试和sql日志打印

简介:微服务项目集成Spring Boot Test单元测试

  • 需求分析->设计->开发->测试->上线
    • 单元测试: 完成最小的软件设计单元的验证工作,目标是确保模块被正确的编码
  • Spring Boot Test 是在Spring Test之上的再次封装, 使用@SpringBootTest后,Spring将加载所有被管理的bean,等同于启动了整个服务

  • common项目添加依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • xdclass-user-service项目新建测试
@RunWith(SpringRunner.class)
@SpringBootTest(classes = UserApplication.class)
@Slf4j
public class AddressTest {

    @Autowired
    private AddressService addressService;


    @Test
    public void testAddressDetail(){

        AddressDO addressDO = addressService.detail(1L);
        log.info(addressDO.toString());
    }


}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • Mybatis plus配置控制台打印日志
#配置plus打印sql日志
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  • 1
  • 2
  • 3
  • 4

第八章 用户微服务注册模块需求-安全攻防的那些事情

第1集 用户微服务注册需求介绍

简介:介绍微服务注册功能和流程介绍

  • 功能需求

    • 使用邮箱注册,已经注册的邮箱不能重复注册
    • 密码不能使用简单的MD5加密
    • 用户上传头像需要用文件存储
  • 功能演示

    • 手机号 13113777888
    • 邮箱 794666918@qq.com
  • 安全需求

    • 高并发下邮箱唯一性
    • 注册邮箱或者手机验证码不能被恶意调用
    • 头像文件存储访问方便扩容和管理
  • 针对每个功能,初级开发和高级开发的思路是不一样

    • 产品经理提业务需求
    • 安全需求就是自己的经验,不然最终背锅的还是自己
第2集 验证码安全需求-灰色产业短信邮箱轰炸机介绍

简介:从邮箱短信验证码介绍老一代的轰炸机

  • 什么是短信-邮箱轰炸机:
手机短信轰炸机是批量、循环给手机无限发送各种网站的注册验证码短信的方法。
  • 1
  • 美好的初衷-发明的由来
最早发明是用来整治街头广告电话号泛滥的一种手段,采用“手机短信轰炸机”软件可无限发送垃圾短信到牛皮癣小广告的手机号码上,使对方的手机快速消耗电量,变成高频率振动棒,且无法正常使用。

“短信轰炸机”可严厉打击城市“牛皮癣”,还城市明净容颜。
  • 1
  • 2
  • 3
  • 灰色产业的目光-也就是部分不法分子利用
某次大型程序员相亲现场-老王得罪了小王, 小王不爽,就道听途说知道了”短信轰炸机“,1天50元,轰炸了5天还打折300元。

一天内接到来自全国各地数千个陌生电话短信的轰炸骚扰,导致个人通讯中断,被工作生活受到严重影响,连刚相亲到的女友没没法联系上了。
  • 1
  • 2
  • 3

⚠️郑重声明⚠️:本集内容只限于小滴课堂用于教学提高业务系统安全性,切勿使用课程思路进行攻击破坏或者获取利益,如产生的一切后果与本人和所属公司无关。

《刑法》第二百八十五条 【非法侵入计算机信息系统罪;非法获取计算机信息系统数据、非法控制计算机信息系统罪】违反国家规定,侵入国家事务、国防建设、尖端科学技术领域的计算机信息系统的,处三年以下有期徒刑或者拘役。

违反国家规定,侵入前款规定以外的计算机信息系统或者采用其他技术手段,获取该计算机信息系统中存储、处理或者传输的数据,或者对该计算机信息系统实施非法控制,情节严重的,处三年以下有期徒刑或者拘役,并处或者单处罚金;情节特别严重的,处三年以上七年以下有期徒刑,并处罚金。

【提供侵入、非法控制计算机信息系统程序、工具罪】提供专门用于侵入、非法控制计算机信息系统的程序、工具,或者明知他人实施侵入、非法控制计算机信息系统的违法犯罪行为而为其提供程序、工具,情节严重的,依照前款的规定处罚

天网恢恢,疏而不漏,一切操作都是可以被追寻到的,ip/设备/网络/基站/监控等等,总有你想不到的方式抓到你
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 原理
很多人都用手机注册一些网站的验证了,比如手机验证码。先填手机号,然后发一条验证码过去,输入验证码,完成验证,注册成功。

* 寻找大量肉鸡网站,寻找发送验证码的请求接口

* 如果找不到接口,也可以使用自动化UI工具触发

* 编写程序和调度任务,相关脚本录入数据库

* 输入目标手机号或者邮箱,触发攻击
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 公司带来的损失
    • 短信一条5分钱,如果被大量盗刷大家自己计算
    • 邮箱通知不用钱,但被大量盗刷,带宽、连接等都被占用,导致无法正常使用
第3集 自动化脚本 - 程序员和灰色产业的攻与防

简介:开发人员和灰色产业的那些事情

  • 请大家先看一个自动化脚本技术视频

    • 第2分35秒:https://xdclass.net/#/dplayer?video_id=16&e_id=1000
    • 延伸
      • 抢票
      • 稀缺的酒、鞋等
  • 案例二:你需要考虑的是批量注册账号

朋友圈、群经常能看到某平台上的点赞、刷粉等业务,比如某d音、某w信

很大程度就是平台一开始研发的时候,没关注安全账号体系,

灰色产业嗅觉灵敏:时刻顶着大平台的产品,一出现就会大量注册账号(因为安全防范最弱),公司看到注册用户指数级上涨更开心

谁能知道,BAT的大厂的产品 几十亿甚至几百亿账号里面有多少是僵尸号
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 如何避免自己的网站成为”肉鸡“或者被刷呢

    • 增加图形验证码(开发人员)
    • 单IP请求次数限制(开发人员)
    • 限制号码发送(一般短信提供商会做)
  • 是否可以一劳永逸???

    • 没有百分百的安全,验证码是可以破解的,ip也是可以租用代理ip的

    • 攻防永远是有的,只过加大了攻击者的成本,ROI划不过来自然就放弃了

第4集 图形验证码开发之谷歌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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 用户服务添加
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>kaptcha-spring-boot-starter</artifactId>
            </dependency>
  • 1
  • 2
  • 3
  • 4
  • 开发配置(任何框架和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;
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 开发一个Controller使用测试
第5集 Linux服务器Docker安装和部署容器化Redis

简介:Linux环境下docker部署+redis安装

  • Linux环境下安装Docker
    • 阿里云购买地址:https://www.aliyun.com/minisite/goods?taskCode=pintuan20201212&recordId=292600&userCode=r5saexap
#依次运行以下命令添加yum源
yum update
yum install epel-release -y
yum clean all
yum list


#安装并运行Docker。
yum install docker-io -y
systemctl start docker

#检查安装结果。
docker info

#启动使用Docker
systemctl start docker     #运行Docker守护进程
systemctl stop docker      #停止Docker守护进程
systemctl restart docker   #重启Docker守护进程


#修改镜像仓库
vim /etc/docker/daemon.json
#改为下面内容,然后重启docker
{
"debug":true,"experimental":true,
"registry-mirrors":["https://pb5bklzr.mirror.aliyuncs.com","https://hub-mirror.c.163.com","https://docker.mirrors.ustc.edu.cn"]
}

#查看信息
docker info
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • docker部署redis 并配置密码

    • 如果访问不了,记得看防火墙/网络安全组端口是否开放
    • 源码安装redis的话默认不能远程访问
    • docker安装redis可以远程访问
    docker run -itd --name xdclass-redis -p 8000:6379 redis --requirepass 123456
    
    • 1
    OptionsMean
    -i以交互模式运行容器,通常与 -t 同时使用;
    -t为容器重新分配一个伪输入终端,通常与 -i 同时使用;
    -d后台运行容器,并返回容器ID;
第6集 用户微服务开发图形验证码加入缓存

简介:用户微服务开发图形验证码接口

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

    • 资源隔离
    • 数据保护
    • 提高性能
    • key规范:业务划分,冒号隔离
      • user-service:captcha:xxxx
      • 长度不能过长
  • 大家一定要有自己的工具集

  • 用户微服务配置Redis

spring:
  application:
    name: xdclass-user-service
  redis:
    host: 112.74.55.160
    password: 123456
    port: 8000
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 验证码接口开发
/**
     *临时使用10分钟有效,方便测试
     */
    private static final long CAPTCHA_CODE_EXPIRED = 60 * 1000 * 10;

    /**
     * 获取图形验证码
     * @param request
     * @param response
     */
    @ApiOperation("获取图形验证码")
    @GetMapping("captcha")
    public void getCaptcha(HttpServletRequest request, HttpServletResponse response){

        String cacheKey = getCaptchaKey(request);

        String capText = captchaProducer.createText();

        //存储     redisTemplate.opsForValue().set(cacheKey,capText,CAPTCHA_CODE_EXPIRED,TimeUnit.MILLISECONDS);

        BufferedImage bi = captchaProducer.createImage(capText);
        ServletOutputStream out = null;
        try {
            response.setDateHeader("Expires", 0);
            response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
            response.addHeader("Cache-Control", "create_date-check=0, pre-check=0");
            response.setHeader("Pragma", "no-cache");
            out = response.getOutputStream();
            ImageIO.write(bi, "jpg", out);
            out.flush();
            out.close();

        } catch (IOException e) {
            log.error("获取验证码失败:{}",e);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • CommonUtil工具类
    /**
     * 获取ip
     * @param request
     * @return
     */
    public static String getIpAddr(HttpServletRequest request) {
        String ipAddress = null;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getRemoteAddr();
                if (ipAddress.equals("127.0.0.1")) {
                    // 根据网卡取本机配置的IP
                    InetAddress inet = null;
                    try {
                        inet = InetAddress.getLocalHost();
                    } catch (UnknownHostException e) {
                        e.printStackTrace();
                    }
                    ipAddress = inet.getHostAddress();
                }
            }
            // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ipAddress != null && ipAddress.length() > 15) {
                // "***.***.***.***".length()
                // = 15
                if (ipAddress.indexOf(",") > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                }
            }
        } catch (Exception e) {
            ipAddress="";
        }
        return ipAddress;
    }



    public static String MD5(String data)  {
        try {
            java.security.MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] array = md.digest(data.getBytes("UTF-8"));
            StringBuilder sb = new StringBuilder();
            for (byte item : array) {
                sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
            }

            return sb.toString().toUpperCase();
        } catch (Exception exception) {
        }
        return null;

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
第7集 邮件发送介绍和项目整合spring邮箱starter

简介:讲解发送邮件的基础知识和项目整合邮箱配置

  • 邮件发送的基本过程与概念
    • 邮件服务器 :类似于现实生活中的邮局,它主要负责接收用户投递过来的邮件,并把邮件投递到邮件接收者的电子邮箱中
    • 电子邮箱 :用户在邮件服务器上申请的一个账户
      • from:xxx@xx.com  ----发件人
      • to:xxx@xx.com   ----收件人
      • subject:hello     ----主题
      • body: 欢迎来到小滴课堂 -----内容体
  • 邮件传输协议
    • SMTP协议:全称为 Simple Mail Transfer Protocol,简单邮件传输协议。它定义了邮件客户端软件和SMTP邮件服务器之间,以及两台SMTP邮件服务器之间的通信规则
    • POP3协议:全称为 Post Office Protocol,邮局协议。它定义了邮件客户端软件和POP3邮件服务器的通信 规则
    • IMAP协议:全称为 Internet Message Access Protocol,Internet消息访问协议,它是对POP3协议一种扩展,也是定义了邮件客户端软件和IMAP邮件服务器的通信规则
  • 账号准备和配置

    • https://mail.126.com/
    • 发送端使用网易邮箱
    • 使用邮箱授权码发送
      • HGCKFIBFYWRIPGCC
  • user-service项目配置添加依赖

 <!--发送邮件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 配置文件
#邮箱服务配置  
  mail:
      host: smtp.126.com #发送邮件服务器
      username: waitforxy@126.com #发送邮件的邮箱地址
      password: CJVYSJOTRXUSSEJE #客户端授权码,不是邮箱密码,网易的是自己设置的
      from: waitforxy@126.com # 发送邮件的地址,和上面username一致

      properties.mail.smtp.starttls.enable: true
      properties.mail.smtp.starttls.required: true
      properties.mail.smtp.ssl.enable: true
      default-encoding: utf-8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 用户微服务 service封装
@Service
@Slf4j
public class MailServiceImpl implements MailService {

    /**
     * Spring Boot 提供了一个发送邮件的简单抽象,直接注入即可使用
     */
    @Autowired
    private JavaMailSender mailSender;

    /**
     * 配置文件中的发送邮箱
     */
    @Value("${spring.mail.from}")
    private String from;

    @Override
    public void sendSimpleMail(String to, String subject, String content) {
        //创建SimpleMailMessage对象
        SimpleMailMessage message = new SimpleMailMessage();
        //邮件发送人
        message.setFrom(from);
        //邮件接收人
        message.setTo(to);
        //邮件主题
        message.setSubject(subject);
        //邮件内容
        message.setText(content);
        //发送邮件
        mailSender.send(message);
        log.info("邮件发成功:{}",message.toString());
    }


}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
第8集 用户微服务之注册邮箱验证码接口开发

简介:注册邮箱验证码接口开发

  • 接口开发
 /**
     * 支持手机号、邮箱发送验证码
     * @return
     */
    @ApiOperation("发送验证码")
    @GetMapping("send_code")
    public JsonData sendRegisterCode(@ApiParam("收信人") @RequestParam(value = "to", required = true)String to,
                                     @ApiParam("图形验证码") @RequestParam(value = "captcha", required = true)String  captcha,
                                     HttpServletRequest request){

        String key = getCaptchaKey(request);
        String cacheCaptcha = redisTemplate.opsForValue().get(key);

        if(captcha!=null && cacheCaptcha!=null && cacheCaptcha.equalsIgnoreCase(captcha)) {
            redisTemplate.delete(key);
            JsonData jsonData = notifyService.sendCode(SendCodeEnum.USER_REGISTER,to);
            return jsonData;
        }else {
            return JsonData.buildResult(BizCodeEnum.CODE_CAPTCHA);
        }

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • service层
  @Override
    public JsonData sendCode(SendCodeEnum sendCodeType, String to) {
      
        if(CheckUtil.isEmail(to)){
            //邮箱验证码
            mailService.sendSimpleMail(to,SUBJECT,String.format(CONTENT,code));
            return JsonData.buildSuccess();

        }else if(CheckUtil.isPhone(to)){
            //短信验证码
        }

        return JsonData.buildResult(BizCodeEnum.CODE_TO_ERROR);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 邮箱工具类正则
public class CheckUtil {

    /**
     * 邮箱正则
     */
    private static final Pattern MAIL_PATTERN = Pattern.compile("^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$");

    /**
     * 手机号正则,暂时未用
     */
    private static final Pattern PHONE_PATTERN = Pattern.compile("^((13[0-9])|(15[^4,\\D])|(18[0,5-9]))\\d{8}$");

    /**
     * @param email
     * @return
     */
    public static  boolean isEmail(String email) {
        if (null == email || "".equals(email)) {
            return false;
        }
        Matcher m = MAIL_PATTERN.matcher(email);
        return m.matches();
    }

    /**
     * 暂时未用
     * @param phone
     * @return
     */
    public static boolean isPhone(String phone) {
        if (null == phone || "".equals(phone)) {
            return false;
        }
        Matcher m = PHONE_PATTERN.matcher(phone);
        return m.matches();

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
第9集 关于注册邮箱验证码防刷设计方案你能想到几个

简介:注册邮箱验证码防刷方案你能想到几个

  • 需求:一定时间内禁止重复发送邮件,大家想下有哪几种实现方式

    • 方式一:前端增加校验倒计时,不到60秒按钮不给点击

      • 简单
      • 不安全,存在绕过的情况
    • 方式二:增加Redis存储,发送的时候设置下额外的key,并且60秒后过期

      • 非原子操作,存在不一致性

      • 增加的额外的key - value存储,浪费空间

      • /**
         * 前置:判断是否重复发送
         *
         * 1、存储验证码到缓存
         *
         * 2、发送邮箱验证码
         *
         * 后置:存储发送记录
         **/
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
    • 方式三:基于原先的key拼装时间戳

      • 好处:满足了当前节点内的原子性,也满足业务需求
第10集 注册邮箱验证码防刷代码落地+整体测试

简介:注册邮箱验证码防刷落地和整体测试

			public JsonData sendCode(SendCodeEnum sendCodeEnum, String to) {

        String cacheKey = String.format(CacheKey.CHECK_CODE_KEY,sendCodeEnum.name(),to);

        String cacheValue = redisTemplate.opsForValue().get(cacheKey);

        //如果不为空,则判断是否60秒内重复发送
        if(StringUtils.isNotBlank(cacheValue)){
            long ttl = Long.parseLong(cacheValue.split("_")[1]);
            //当前时间戳-验证码发送时间戳,如果小于60秒,则不给重复发送
            if(CommonUtil.getCurrentTimestamp() - ttl < 1000*60){
                log.info("重复发送验证码,时间间隔:{} 秒",(CommonUtil.getCurrentTimestamp()-ttl)/1000);
                return JsonData.buildResult(BizCodeEnum.CODE_LIMITED);
            }
        }

        //拼接验证码 2322_324243232424324
        String code = CommonUtil.getRandomCode(6);

        String value = code+"_"+CommonUtil.getCurrentTimestamp();

        redisTemplate.opsForValue().set(cacheKey,value,CODE_EXPIRED,TimeUnit.MILLISECONDS);

        if(CheckUtil.isEmail(to)){
            //邮箱验证码
            mailService.sendMail(to,SUBJECT,String.format(CONTENT,code));

            return JsonData.buildSuccess();

        } else if(CheckUtil.isPhone(to)){
            //短信验证码

        }

        return JsonData.buildResult(BizCodeEnum.CODE_TO_ERROR);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 整体测试和总结

先的key拼装时间戳

* 好处:满足了当前节点内的原子性,也满足业务需求
  • 1
第10集 注册邮箱验证码防刷代码落地+整体测试

简介:注册邮箱验证码防刷落地和整体测试

			public JsonData sendCode(SendCodeEnum sendCodeEnum, String to) {

        String cacheKey = String.format(CacheKey.CHECK_CODE_KEY,sendCodeEnum.name(),to);

        String cacheValue = redisTemplate.opsForValue().get(cacheKey);

        //如果不为空,则判断是否60秒内重复发送
        if(StringUtils.isNotBlank(cacheValue)){
            long ttl = Long.parseLong(cacheValue.split("_")[1]);
            //当前时间戳-验证码发送时间戳,如果小于60秒,则不给重复发送
            if(CommonUtil.getCurrentTimestamp() - ttl < 1000*60){
                log.info("重复发送验证码,时间间隔:{} 秒",(CommonUtil.getCurrentTimestamp()-ttl)/1000);
                return JsonData.buildResult(BizCodeEnum.CODE_LIMITED);
            }
        }

        //拼接验证码 2322_324243232424324
        String code = CommonUtil.getRandomCode(6);

        String value = code+"_"+CommonUtil.getCurrentTimestamp();

        redisTemplate.opsForValue().set(cacheKey,value,CODE_EXPIRED,TimeUnit.MILLISECONDS);

        if(CheckUtil.isEmail(to)){
            //邮箱验证码
            mailService.sendMail(to,SUBJECT,String.format(CONTENT,code));

            return JsonData.buildSuccess();

        } else if(CheckUtil.isPhone(to)){
            //短信验证码

        }

        return JsonData.buildResult(BizCodeEnum.CODE_TO_ERROR);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 整体测试和总结
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/621274
推荐阅读
相关标签
  

闽ICP备14008679号