页面元素替换
首先要做的当然是换成自己风格的站名和内容啦。
1、网站配置
跟踪前端代码后发现配置是来自后端接口,想着既然入库了,那应该有对应的管理页面吧,果然找到了,就是…演示账号不允许操作!那么接下来要干的事就很明显了,把这个用户搞定!
2、账号配置
切换到idea发现工作台存在一行报错,根据报错跳转到对应的代码,发现这鉴权方式没见过啊!赶紧面向百度编程。
Sa-token文档地址:https://sa-token.dev33.cn/
大概了解了下这个框架,简直是懒人福音啊x。然后发现页面上就有用户管理 + 修改密码,那么事情就变得简单了。
3、文件上传
因为预想中配置的文件服务器是minio,作者只附了本地和七牛两种方式,那么改造开始。
增加minio标签选项
全局搜索图片上传方式
,找到对应绑定的字段,加上minio。PS:阿里oss原本也是没有的,但是跟踪后端代码发现字典值2对应的是阿里oss,就先加上了。
后端代码
跟踪/file/upload
接口可以发现,后端是根据fileUploadWay 配置字段决定调用哪个上传策略。
private void getFileUploadWay() {
strategy = FileUploadModelEnum.getStrategy(systemConfigService.getCustomizeOne().getFileUploadWay());
}
跟踪FileUploadModelEnum发现是个枚举类,那么先加上minio的枚举。
先在pom.xml引入minio。
<!-- Minio -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.2.2</version>
</dependency>
原作者的配置方式是在数据库加入字段,这种方式不太习惯,所以这边minio的配置都加入到配置文件中,后续使用**@Value**注入。
#============================Minio配置信息===================================
minio:
url: http://ip:9000
accessKey: 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;
@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;
}
}
先在入口添加一下注解,再使用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)
文件中间表
为啥要用中间表呢,主要是想保护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接口的首字母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 + "!");
}
}
- 改造上传代码
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}")
@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)使用了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;
}
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
configurer.setTaskExecutor(mvcTaskExecutor());
}
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";
}
}
配置讲解:
- /root/cache:本地路径,用来设置Nginx缓存资源的存放地址
- levels=1:2 :默认所有缓存文件都放在上面指定的根路径中,可能影响缓存的性能,推荐指定为 2
- 级目录来存储缓存文件;1和2表示用1位和2位16进制来命名目录名称。第一级目录用1位16进制命名,如a;第二级目录用2位16进制命名,如3a。所以此例中一级目录有16个,二级目录有16*16=256个,总目录数为16256=4096个。
- 当levels=1:1:1时,表示是三级目录,且每级目录数均为16个
- key_zone:在共享内存中定义一块存储区域来存放缓存的 key 和 metadata
- max_size :最大 缓存空间, 如果不指定会使用掉所有磁盘空间。当达到 disk 上限后,会删除最少使用的 cache
- inactive:某个缓存在inactive指定的时间内如果不访问,将会从缓存中删除
- proxy_cache_valid:配置nginx cache中的缓存文件的缓存时间,proxy_cache_valid 200 304 2m 对于状态为200和304的缓存文件的缓存时间是2分钟
- use_temp_path:建议为 off,则 nginx 会将缓存文件直接写入指定的 cache 文件中
- proxy_cache:启用proxy cache,并指定key_zone,如果proxy_cache off表示关闭掉缓存
- add_header Nging-Cache “$upstream_cache_status”:用于前端判断是否是缓存,miss、hit、expired(缓存过期)、updating(更新,使用旧的应答),还原nginx配置,只保留upstream模块
注意:
- nginx缓存过期影响的优先级进行排序为:inactvie > 源服务器端Expires/max-age > proxy_cache_valid
- 如果出现 Permission denied 修改nginx.conf,将第一行修改为 user root
- 默认情况下GET请求及HEAD请求会被缓存,而POST请求不会被缓存,并非全部都要缓存,可以过滤部分路径不用缓存
vue项目部署至nginx,路由404
查看官网推荐配置,cv一份。
文章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/
修改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;
}
第二天能够查看头一天的推送情况。