首先要做的当然是换成自己风格的站名和内容啦。
跟踪前端代码后发现配置是来自后端接口,想着既然入库了,那应该有对应的管理页面吧,果然找到了,就是…演示账号不允许操作!那么接下来要干的事就很明显了,把这个用户搞定!
切换到idea发现工作台存在一行报错,根据报错跳转到对应的代码,发现这鉴权方式没见过啊!赶紧面向百度编程。
Sa-token文档地址:https://sa-token.dev33.cn/
大概了解了下这个框架,简直是懒人福音啊x。然后发现页面上就有用户管理 + 修改密码,那么事情就变得简单了。
因为预想中配置的文件服务器是minio,作者只附了本地和七牛两种方式,那么改造开始。
全局搜索图片上传方式
,找到对应绑定的字段,加上minio。PS:阿里oss原本也是没有的,但是跟踪后端代码发现字典值2对应的是阿里oss,就先加上了。
跟踪/file/upload
接口可以发现,后端是根据fileUploadWay 配置字段决定调用哪个上传策略。
private void getFileUploadWay() {strategy = FileUploadModelEnum.getStrategy(systemConfigService.getCustomizeOne().getFileUploadWay());
}
跟踪FileUploadModelEnum发现是个枚举类,那么先加上minio的枚举。
先在pom.xml引入minio。
io.minio minio 8.2.2
原作者的配置方式是在数据库加入字段,这种方式不太习惯,所以这边minio的配置都加入到配置文件中,后续使用**@Value**注入。
#============================Minio配置信息===================================
minio:url: http://ip:9000accessKey: minio账号secretKey: minio密码bucketName: 桶名称preurl: http://预览地址
随后仿造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;@PostConstructprivate void init(){client = MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();}@Overridepublic 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*/@Overridepublic Boolean deleteFile(String ...key) {if (key.length > 0) {//批量删除Iterable deleteObjects = Arrays.stream(key).map(s -> new DeleteObject(s)).collect(Collectors.toList());Iterable> results = client.removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(deleteObjects).build());for (Result result : results) {try {result.get();} catch (Exception e) {logger.error(e.getMessage());e.printStackTrace();}}}return true;}}
先在入口添加一下注解,再使用swagger调用测试,PS:记得先登录

为啥要用中间表呢,主要是想保护minio的端口。上传和下载都通过代码进行,就不能通过文件层级猜到别的文件路径。以及防止minio突然暴露什么漏洞。【当然如果是项目上用这个才不管呢!】
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='文件表';
/*** 代码生成器*/
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接口的首字母Igc.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 + "!");}
}
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();
}
public class TbFilesController {private final TbFilesService filesService;private final MinioUploadStrategyImpl minioUploadStrategy;/*** 预览* @param id* @return*/@SaIgnore@GetMapping("/preview/{id}")@ResponseBodypublic ResponseEntity preview(@PathVariable Long id){TbFiles file = filesService.getDetail(id);//设置头文件Content-typeHttpHeaders 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)使用了ResponseEntity作为返回对象,使用HttpServletResponse的话,Content-type变更了也会被Spring框架自动更改为application/json。查找资料的时候看到很多使用**@GetMapping的produces属性,但是这样就固定了Content-type的内容。
2)不能使用下载的方式获取预览流,标签中放入地址后虽然接口调用成功了,但是图是裂开的。
3)接口校验忽略接口 @SaIgnore 这个注解是sa-token 1.29版本没有的。这里升级到了1.32版本。当然也可以改WebMvcConfig文件中的sa-token**拦截器。
上传后查看数据库,已经入库。
调用preview方法
!!!
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'
!!!
老实说,写了那么久代码第一次遇到warning提示。。
大意就是:默认的SimpleAsyncTaskExecutor已不适用,请自定义一个TaskExecutor。那就加呗,WebMvcConfig加入下列代码。
@Bean
public ThreadPoolTaskExecutor mvcTaskExecutor() {ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();taskExecutor.setCorePoolSize(100);taskExecutor.setMaxPoolSize(100);return taskExecutor;
}@Overridepublic void configureAsyncSupport(AsyncSupportConfigurer configurer) {configurer.setTaskExecutor(mvcTaskExecutor());
}
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";}
}
查看官网推荐配置,cv一份。
先在百度搜索资源站配置好自己的网站: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/
修改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 ids) {String param = "";for (Long id : ids) {param += sourceUrl + id + "\n";}HttpEntity 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;
}
第二天能够查看头一天的推送情况。