• 拾壹博客拆解改造,页面元素替换(二)


    页面元素替换

    首先要做的当然是换成自己风格的站名和内容啦。

    1、网站配置

    跟踪前端代码后发现配置是来自后端接口,想着既然入库了,那应该有对应的管理页面吧,果然找到了,就是…演示账号不允许操作!那么接下来要干的事就很明显了,把这个用户搞定!
    image.png

    2、账号配置

    切换到idea发现工作台存在一行报错,根据报错跳转到对应的代码,发现这鉴权方式没见过啊!赶紧面向百度编程。
    image.png
    Sa-token文档地址:https://sa-token.dev33.cn/
    大概了解了下这个框架,简直是懒人福音啊x。然后发现页面上就有用户管理 + 修改密码,那么事情就变得简单了。
    image.png

    3、文件上传

    因为预想中配置的文件服务器是minio,作者只附了本地和七牛两种方式,那么改造开始。

    增加minio标签选项

    全局搜索图片上传方式,找到对应绑定的字段,加上minio。PS:阿里oss原本也是没有的,但是跟踪后端代码发现字典值2对应的是阿里oss,就先加上了。
    image.png

    后端代码

    跟踪/file/upload接口可以发现,后端是根据fileUploadWay 配置字段决定调用哪个上传策略。

    private void getFileUploadWay() {
    	strategy = FileUploadModelEnum.getStrategy(systemConfigService.getCustomizeOne().getFileUploadWay());
    }
    
    • 1
    • 2
    • 3

    跟踪FileUploadModelEnum发现是个枚举类,那么先加上minio的枚举。
    image.png
    先在pom.xml引入minio

    
    <dependency>
        <groupId>io.miniogroupId>
        <artifactId>minioartifactId>
        <version>8.2.2version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    原作者的配置方式是在数据库加入字段,这种方式不太习惯,所以这边minio的配置都加入到配置文件中,后续使用**@Value**注入。

    #============================Minio配置信息===================================
    minio:
      url: http://ip:9000
      accessKey: minio账号
      secretKey: minio密码
      bucketName: 桶名称
      preurl: http://预览地址
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    随后仿造aliUploadStrategyImpl创建minio对应的service。

    package com.shiyi.strategy.imp;
    
    import com.shiyi.strategy.FileUploadStrategy;
    import io.minio.MinioClient;
    import io.minio.PutObjectArgs;
    import io.minio.RemoveObjectsArgs;
    import io.minio.Result;
    import io.minio.messages.DeleteError;
    import io.minio.messages.DeleteObject;
    import org.apache.commons.lang3.StringUtils;
    import org.apache.commons.lang3.time.DateFormatUtils;
    import org.apache.velocity.shaded.commons.io.FilenameUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Service;
    import org.springframework.web.multipart.MultipartFile;
    
    import javax.annotation.PostConstruct;
    import java.util.Arrays;
    import java.util.Date;
    import java.util.UUID;
    import java.util.stream.Collectors;
    
    @Service("minioUploadStrategyImpl")
    public class MinioUploadStrategyImpl implements FileUploadStrategy {
    
        private final Logger logger = LoggerFactory.getLogger(MinioUploadStrategyImpl.class);
    
        /**
         * 服务地址
         */
        @Value("${minio.url}")
        private String url;
    
        /**
         * 预览路径前缀
         */
        @Value("${minio.preurl}")
        private String preurl;
    
        /**
         * 用户名
         */
        @Value("${minio.accessKey}")
        private String accessKey;
    
        /**
         * 密码
         */
        @Value("${minio.secretKey}")
        private String secretKey;
    
        /**
         * 存储桶名称
         */
        @Value("${minio.bucketName}")
        private String bucketName;
    
        private static MinioClient client = null;
    
        @PostConstruct
        private void init(){
            client = MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();
        }
    
        @Override
        public String fileUpload(MultipartFile file,String suffix) {
            String fileName = null;
            try {
            	String extension = FilenameUtils.getExtension(file.getOriginalFilename());
    
                fileName = DateFormatUtils.format(new Date(), "yyyy/MM/dd") + "/" + UUID.randomUUID() + "." + extension;
                PutObjectArgs args = PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(fileName)
                        .stream(file.getInputStream(), file.getSize(), -1)
                        .contentType(file.getContentType())
                        .build();
                client.putObject(args);
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            return preurl + "/" + bucketName + "/" + fileName;
        }
    
        /**
         * 删除文件 -- minio
         *
         * @param key   文件url
         * @return      ResponseResult
         */
        @Override
        public Boolean deleteFile(String ...key) {
            if (key.length > 0) {
                //批量删除
                Iterable<DeleteObject> deleteObjects = Arrays.stream(key).map(s -> new DeleteObject(s)).collect(Collectors.toList());
    
                Iterable<Result<DeleteError>> results = client.removeObjects(
                        RemoveObjectsArgs.builder()
                                .bucket(bucketName)
                                .objects(deleteObjects)
                                .build()
                );
    
                for (Result<DeleteError> result : results) {
                    try {
                        result.get();
                    } catch (Exception e) {
                        logger.error(e.getMessage());
                        e.printStackTrace();
                    }
                }
            }
    
            return true;
        }
    
    }
    
    
    • 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
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121

    先在入口添加一下注解,再使用swagger调用测试,PS:记得先登录
    ![image.png](https://img-blog.csdnimg.cn/img_convert/25de1564a7752823ed66d8d1f751c3fb.png#averageHue=#2c2c2b&clientId=u5ccd04dd-8879-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=199&id=u6c178023&margin=[object Object]&name=image.png&originHeight=199&originWidth=2033&originalType=binary&ratio=1&rotation=0&showTitle=false&size=57089&status=done&style=none&taskId=u7ed14748-0b8e-4b09-b099-6807f1e7572&title=&width=2033)
    image.png

    文件中间表

    为啥要用中间表呢,主要是想保护minio的端口。上传和下载都通过代码进行,就不能通过文件层级猜到别的文件路径。以及防止minio突然暴露什么漏洞。【当然如果是项目上用这个才不管呢!】

    1. 建表语句
    CREATE TABLE `tb_files` (
      `id` bigint(20) NOT NULL COMMENT '主键id',
      `user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
      `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
      `preview_file` varchar(128) DEFAULT NULL COMMENT '文件minio地址',
      `file_name` varchar(512) DEFAULT NULL COMMENT '原文件名称',
      `content_type` varchar(50) DEFAULT NULL COMMENT '文件类型',
      `is_static` tinyint(1) DEFAULT '0' COMMENT '是否静态资源',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='文件表';
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. 随便抄一个代码生成器
    /**
     * 代码生成器
     */
    public class CodeGenerator {
    
        public static void main(String[] args) {
            // 1、创建代码生成器
            AutoGenerator mpg = new AutoGenerator();
            // 2、全局配置
            GlobalConfig gc = new GlobalConfig();
            String projectPath = System.getProperty("user.dir");
    
            gc.setOutputDir(projectPath + "/src/main/java");
            gc.setAuthor("dingx");
            gc.setOpen(false); //生成后是否打开资源管理器
            gc.setFileOverride(false); //重新生成时文件是否覆盖
            /*
             * mp生成service层代码,默认接口名称第一个字母有 I
             * UcenterService
             * */
            gc.setServiceName("%sService"); //去掉Service接口的首字母I
            gc.setIdType(IdType.ASSIGN_ID); //主键策略
            gc.setDateType(DateType.ONLY_DATE);//定义生成的实体类中日期类型
            gc.setSwagger2(true);//开启Swagger2模式
            mpg.setGlobalConfig(gc);
    
            // 3、数据源配置
            DataSourceConfig dsc = new DataSourceConfig();
            dsc.setUrl("jdbc:mysql://ip:port/schema?serverTimezone=GMT%2B8");
            dsc.setDriverName("com.mysql.cj.jdbc.Driver");
            dsc.setUsername("root");
            dsc.setPassword("pwd");
            dsc.setDbType(DbType.MYSQL);
            mpg.setDataSource(dsc);
    
            // 4、包配置
            PackageConfig pc = new PackageConfig();
    //        pc.setModuleName(scanner("模块名")); //模块名
            pc.setParent("com.shiyi");
            pc.setController("controller");
            pc.setEntity("entity");
            pc.setService("service");
            pc.setMapper("mapper");
            mpg.setPackageInfo(pc);
    
            // 5、策略配置
            StrategyConfig strategy = new StrategyConfig();
            strategy.setInclude(scanner("表名"));
            strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表映射到实体的命名策略
            strategy.setTablePrefix(pc.getModuleName() + "_"); //生成实体时去掉表前缀
            strategy.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略
            strategy.setEntityLombokModel(true); // lombok 模型 @Accessors(chain =true) setter链式操作
            strategy.setRestControllerStyle(true); //restful api风格控制器
            strategy.setControllerMappingHyphenStyle(true); //url中驼峰转连字符
            mpg.setStrategy(strategy);
    
            // 6、执行
            mpg.execute();
        }
    
        private static String scanner(String tip) {
            Scanner scanner = new Scanner(System.in);
            System.out.println(("请输入" + tip + ":"));
            if (scanner.hasNext()) {
                String ipt = scanner.next();
                if (StringUtils.isNotBlank(ipt)) {
                    return ipt;
                }
            }
            throw new MybatisPlusException("请输入正确的" + tip + "!");
        }
    }
    
    • 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
    1. 改造上传代码
    private final TbFilesService tbFilesService;
    
    @Override
    public String fileUpload(MultipartFile file,String suffix) {
        String fileName;
        TbFiles tbFile = null;
        try {
            String extension = getExtension(file);
    
            fileName = DateFormatUtils.format(new Date(), "yyyy/MM/dd") + "/" + UUID.randomUUID() + "." + extension;
    
            //保存上传文件记录
            tbFile = new TbFiles(file.getOriginalFilename(), fileName, file.getContentType());
            if (!tbFilesService.save(tbFile)){
                throw new RuntimeException("插入文件失败");
            }
    
            PutObjectArgs args = PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(fileName)
                    .stream(file.getInputStream(), file.getSize(), -1)
                    .contentType(file.getContentType())
                    .build();
            client.putObject(args);
        } catch (Exception e) {
            e.printStackTrace();
        }
    
        return preurl + "/" + tbFile.getId();
    }
    
    • 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
    1. 增加预览接口
    public class TbFilesController {
    
        private final TbFilesService filesService;
    
        private final MinioUploadStrategyImpl minioUploadStrategy;
    
        /**
         * 预览
         * @param id
         * @return
         */
        @SaIgnore
        @GetMapping("/preview/{id}")
        @ResponseBody
        public ResponseEntity<StreamingResponseBody> preview(@PathVariable Long id){
            TbFiles file = filesService.getDetail(id);
    
            //设置头文件Content-type
            HttpHeaders headers = new HttpHeaders();
    
            // 发送给客户端的数据
            // 设置编码
            if (StringUtils.isNotBlank(file.getContentType())) {
                headers.setContentType(MediaType.valueOf(file.getContentType()));
            }
    
            //构造返回体
            return ResponseEntity.ok()
                    .headers(headers)
                    .body(outputStream -> {
                        try (InputStream inputStream = minioUploadStrategy.downloadFile(file.getPreviewFile())){
                            IOUtils.copy(inputStream, outputStream);
                        } catch (Exception e){
                            e.printStackTrace();
                        }
                    });
        }
    }
    
    • 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

    这里遇到的坑:
    1)使用了ResponseEntity作为返回对象,使用HttpServletResponse的话,Content-type变更了也会被Spring框架自动更改为application/json。查找资料的时候看到很多使用**@GetMappingproduces属性,但是这样就固定了Content-type的内容。
    2)不能使用下载的方式获取预览流,
    标签中放入地址后虽然接口调用成功了,但是图是裂开的。
    3)接口校验忽略接口 @SaIgnore 这个注解是sa-token 1.29版本没有的。这里升级到了
    1.32版本。当然也可以改WebMvcConfig文件中的sa-token**拦截器。

    1. 功能测试

    上传后查看数据库,已经入库。
    image.png
    调用preview方法
    image.png

    1. 一个警告
    !!!
    An Executor is required to handle java.util.concurrent.Callable return values.
    Please, configure a TaskExecutor in the MVC config under "async support".
    The SimpleAsyncTaskExecutor currently in use is not suitable under load.
    -------------------------------
    Request URI: '/dingx/data/files/preview/1594875366335397890'
    !!!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    老实说,写了那么久代码第一次遇到warning提示。。
    大意就是:默认的SimpleAsyncTaskExecutor已不适用,请自定义一个TaskExecutor。那就加呗,WebMvcConfig加入下列代码。

    @Bean
    public ThreadPoolTaskExecutor mvcTaskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(100);
        taskExecutor.setMaxPoolSize(100);
        return taskExecutor;
    }
    
    @Override
        public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        configurer.setTaskExecutor(mvcTaskExecutor());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    Nginx强化配置

    nginx缓存

    proxy_cache_path /root/cache levels=1:2 keys_zone=xd_cache:10m max_size=1g inactive=60m use_temp_path=off;
    
    server {
    
        location /{
            ... 
            proxy_cache xd_cache;
            proxy_cache_valid 200 304 10m;
            proxy_cache_valid 404 1m; 
            proxy_cache_key $host$uri$is_args$args;
            add_header Nginx-Cache "$upstream_cache_status";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    配置讲解:
    1. /root/cache:本地路径,用来设置Nginx缓存资源的存放地址
    2. levels=1:2 :默认所有缓存文件都放在上面指定的根路径中,可能影响缓存的性能,推荐指定为 2
    3. 级目录来存储缓存文件;1和2表示用1位和2位16进制来命名目录名称。第一级目录用1位16进制命名,如a;第二级目录用2位16进制命名,如3a。所以此例中一级目录有16个,二级目录有16*16=256个,总目录数为16256=4096个。
    4. 当levels=1:1:1时,表示是三级目录,且每级目录数均为16个
    5. key_zone:在共享内存中定义一块存储区域来存放缓存的 key 和 metadata
    6. max_size :最大 缓存空间, 如果不指定会使用掉所有磁盘空间。当达到 disk 上限后,会删除最少使用的 cache
    7. inactive:某个缓存在inactive指定的时间内如果不访问,将会从缓存中删除
    8. proxy_cache_valid:配置nginx cache中的缓存文件的缓存时间,proxy_cache_valid 200 304 2m 对于状态为200和304的缓存文件的缓存时间是2分钟
    9. use_temp_path:建议为 off,则 nginx 会将缓存文件直接写入指定的 cache 文件中
    10. proxy_cache:启用proxy cache,并指定key_zone,如果proxy_cache off表示关闭掉缓存
    11. add_header Nging-Cache “$upstream_cache_status”:用于前端判断是否是缓存,miss、hit、expired(缓存过期)、updating(更新,使用旧的应答),还原nginx配置,只保留upstream模块
    注意:
    1. nginx缓存过期影响的优先级进行排序为:inactvie > 源服务器端Expires/max-age > proxy_cache_valid
    2. 如果出现 Permission denied 修改nginx.conf,将第一行修改为 user root
    3. 默认情况下GET请求及HEAD请求会被缓存,而POST请求不会被缓存,并非全部都要缓存,可以过滤部分路径不用缓存

    image.png

    vue项目部署至nginx,路由404

    查看官网推荐配置,cv一份。
    image.png

    文章SEO

    先在百度搜索资源站配置好自己的网站:http://data.zz.baidu.com/linksubmit/index
    找到普通收录,在配置文件中增加配置项

    baidu:
      url: http://data.zz.baidu.com/urls?site=blog.dinganwang.top&token=
      sourceurl: https://blog.dinganwang.top/articles/
    
    • 1
    • 2
    • 3

    修改articleSeo方法。作者这边是用for循环实现的批量推送,emmmm老实说有点怪,所以稍微改了下。

    @Value("${baidu.url}")
    private String baiduUrl;
    
    @Value("${baidu.sourceurl}")
    private String sourceUrl;
    
    private final static String SUCCESS = "success";
    private final static String REMAIN = "remain";
    
    /**
     *  文章百度推送
     * @return
     */
    @Override
    public ResponseResult articleSeo(List<Long> ids) {
        String param = "";
    
        for (Long id : ids) {
            param += sourceUrl + id + "\n";
        }
    
        HttpEntity<String> entity = new HttpEntity<>(param.trim(), createBdHeader());
        String res = restTemplate.postForObject(baiduUrl, entity, String.class);
        JSONObject JO = JSONObject.parseObject(res);
        if (JO.getInteger(SUCCESS) > 0){
            return ResponseResult.success("成功推送【" + JO.getInteger(SUCCESS) + "】条,剩余量【" + JO.getInteger(REMAIN) + "】条");
        }else {
            return ResponseResult.error("推送失败!");
        }
    }
    
    /**
     * 构造百度seo头文件
     * @return
     */
    private static HttpHeaders createBdHeader(){
        HttpHeaders headers = new HttpHeaders();
        headers.add("Host", "data.zz.baidu.com");
        headers.add("User-Agent", "curl/7.12.1");
        headers.add("Content-Length", "83");
        headers.add("Content-Type", "text/plain");
        return headers;
    }
    
    • 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

    第二天能够查看头一天的推送情况。
    image.png

  • 相关阅读:
    rm -rf 真是删库跑路的一把好手
    钉钉微应用 - - - - - 钉钉内打开新页签
    Python实战项目:并夕夕版飞机大战(源码分享)(文章较短,直接上代码)
    软考知识点:流水线
    设计模式---代理模式
    【洛谷】P3835 【模板】可持久化平衡树
    优秀工具|使用Reqable替换处理过的动态混淆js
    SAP MM学习笔记27- 购买依赖(采购申请)
    安卓毕业设计app项目源码基于Uniapp实现的美食餐厅订餐点餐
    源码构建LAMP环境-2
  • 原文地址:https://blog.csdn.net/qq_16253859/article/details/128146435