1.介紹
本文主要是介紹前后端分離的上傳下載,后端使用的是SpringBoot,持久層用的是mybatis-plus,前端用的Vue,UI用的elementUI,測試了一下,文本,圖片,excel,都是可以上傳下載的,前端就是一個頁面,后端就是一個controller頁面,就可以解決
代碼不清晰的話,我把這個前端的vue 跟 后端的controller代碼,以及本篇文章的MD版本放到了這個鏈接里 https://wwa.lanzous.com/b0cqr478f 密碼:19qb
1.1前端樣子
2.前端
2.1先分享一下前端的全部代碼
<template>
<div>
<center>
</center>
<div style="float: left;margin-left: 100px">
<!-- <router-link to="/reportTools/customQuery" style="color: darkblue">-->
<!-- 上一步-->
<!-- </router-link>-->
<el-button type="primary" v-on:click="tocustomQuery" >上一步</el-button>
</div>
<div style="float: right;margin-right: 100px">
<el-button type="primary" v-on:click="saveResource" >保存</el-button>
</div>
<br>
<h1>基本信息設置</h1>
<br>
<br>
<center>
<div style="margin-right: 170px">
<el-form ref="form" :model="form" label-width="120px" >
<el-form-item label="報表名稱中文:" style="width: 470px;">
<el-input v-model="form.reportName" ></el-input>
</el-form-item>
<el-form-item label="報表名稱英文:" style="width: 470px;">
<el-input v-model="form.reportEnName"></el-input>
</el-form-item>
<el-form-item label="報表名稱中英文:" style="width: 470px;">
<el-input v-model="form.reportChEnName"></el-input>
</el-form-item>
<el-form-item label="報表備注:" style="width: 470px;">
<el-input v-model="form.remarks"></el-input>
</el-form-item>
<el-form-item label="功能編號" style="width: 470px;">
<el-input v-model="form.code" ></el-input>
</el-form-item>
<el-form-item label="編輯模板:" style="width: 570px;">
<el-input placeholder="模板地址" style="width: 200px" disabled></el-input>
<el-upload class="upload-demo"
:action="uploadUrl"
:before-upload="handleBeforeUpload"
:on-error="handleUploadError"
:before-remove="beforeRemove"
multiple
:limit="5"
:on-exceed="handleExceed"
:file-list="fileList">
<el-button size="small" type="primary">點擊上傳</el-button>
</el-upload>
<a href="http://localhost:8080/file/download?fileName=test.xls">下載附件</a>
</el-form-item>
<el-form-item label="模板備注:" style="width: 480px;">
模板用於下載,打印EXCEL時使用。其中{ {CURRENT_USER} }表示當前用戶名,
{ {TITLE} }表示表頭,{ {DATA} }表示數據行,{ {FOOTER} }表示頁腳區域。
導出的內容不可改動,頁眉頁腳區域可以自由編輯。
</el-form-item>
<el-form-item >
<el-select v-model="form.isPublic" style="width: 350px;">
<el-option label="私有查詢" value="2" ></el-option>
<el-option label="公共查詢" value="1"></el-option>
</el-select>
</el-form-item>
<el-form-item >
<el-checkbox label="是否打印" v-model="form.isPrinting" true-label="1" false-label="2"></el-checkbox>
<el-checkbox label="是否導出" v-model="form.isExport" true-label="1" false-label="2"></el-checkbox>
<el-form-item type="hidden" style="width: 470px;">
</el-form-item>
</el-form-item>
</el-form>
</div>
</center>
</div>
</template>
<script>
import axios from 'axios'
import { uuid } from 'vue-uuid';
export default {
data() {
return {
form: {
reportName:'',
reportEnName:'',
reportChEnName:'',
remarks:'',
code:'',
isPublic:'1',
isPrinting:"1",
isExport:"1",
datas:[{},{},{},{}],
A0010011:'',
A0010012:'',
A0010013:'',
A0010014:'',
},
// uuid1:'',
// cusList:{},
uploadUrl: 'file/upload',
fileList: [],
}
},
created() {
console.log("上個頁面傳來的是",sessionStorage)
// this.handleUUIDv1()
this.form.code = this.$uuid.v1()
console.log("頁面刷新后新產生的uuid是",this.form.code)
},
watch:{
reportName: function (newVal,oldVal) {//新值和原值
console.log("name改變了 a:"+newVal+" b:"+oldVal)
}
},
methods: {
//使用uuid
// handleUUIDv1() {
// this.uuid1 = this.$uuid.v1()
// },
//點擊多選按鈕true和false變成1或者2
reset(form){
this.$refs['form'].resetFields();
},
//獲取上個頁面傳來的數據
getcustomQuery() {
let reportName=this.form.reportName
sessionStorage.setItem("reportName",reportName)
let reportEnName = this.form.reportEnName
sessionStorage.setItem("reportEnName",reportEnName)
let reportChEnName = this.form.reportChEnName
sessionStorage.setItem("reportChEnName",reportChEnName)
let remarks = this.form.remarks
sessionStorage.setItem("remarks",remarks)
let code = this.form.code
sessionStorage.setItem("code",code)
let isPublic = this.form.isPublic
sessionStorage.setItem("isPublic",isPublic)
let isPrinting = this.form.isPrinting
sessionStorage.setItem("isPrinting",isPrinting)
let isExport = this.form.isExport
sessionStorage.setItem("isExport",isExport)
console.log("上個頁面傳來的是",sessionStorage)
},
// this.$router.push('/reportTools/cusMain'),點擊保存發送鏈接 並跳轉
tocustomQuery(){
this.$router.push('/reportTools/customQuery')
},
saveResource(){
console.log("當前頁面表單的數據",this.form)
// sessionStorage.setItem("params",JSON.stringify(this.form))
console.log("sessionStorage中的數據",sessionStorage)
this.getcustomQuery()
this.$http.post("http://localhost:8080/reporttools/report-show/addReportShow",sessionStorage).then(
resp=>{
console.log(resp)
//this.$router.push('/reportTools/cusMain')
})
},
handleExceed(files, fileList) {
this.$message.warning(`當前限制選擇 5 個文件,本次選擇了 ${files.length} 個文件,共選擇了 ${files.length + fileList.length} 個文件`);
},
beforeRemove(file, fileList) {
return this.$confirm(`確定移除 ${ file.name }?`);
},
handleUploadError(error, file) {
console.log("文件上傳出錯:"+error)
},
//測試上傳文件(注意web的上下文)
handleBeforeUpload(file){
console.log("開始上傳,上傳的文件為:"+file)
let formData = new FormData();
formData.append("multipartFiles", file);
axios({
method: 'post',
url: 'file/upload',
data: formData,
headers: {'Content-Type': 'multipart/form-data' }
}).then((res) => {
console.log("文件上傳返回:"+res)
}).catch(error => {
console.log("文件上傳異常:"+error)
})
},
}
}
</script>
<style scoped>
a {
text-decoration: none;
}
</style>
2.2分享一下前端template層的代碼
2.2.1上傳下載template全部代碼
<el-upload class="upload-demo"
:action="uploadUrl"
:before-upload="handleBeforeUpload"
:on-error="handleUploadError"
:before-remove="beforeRemove"
multiple
:limit="5"
:on-exceed="handleExceed"
:file-list="fileList">
<el-button size="small" type="primary">點擊上傳</el-button>
</el-upload>
<a href="http://localhost:8080/file/download?fileName=test.xls">下載附件</a>
2.2.2 上傳代碼
<el-upload class="upload-demo"
:action="uploadUrl"
:before-upload="handleBeforeUpload"
:on-error="handleUploadError"
:before-remove="beforeRemove"
multiple
:limit="5"
:on-exceed="handleExceed"
:file-list="fileList">
<el-button size="small" type="primary">點擊上傳</el-button>
</el-upload>
2.2.3下載代碼
下載代碼就是一個a標簽
<a href="http://localhost:8080/file/download?fileName=test.xls">下載附件</a>
2.3分享一下前端script代碼
<script>
import axios from 'axios'
import { uuid } from 'vue-uuid';
export default {
data() {
return {
form: {
reportName:'',
reportEnName:'',
reportChEnName:'',
remarks:'',
code:'',
isPublic:'1',
isPrinting:"1",
isExport:"1",
datas:[{},{},{},{}],
A0010011:'',
A0010012:'',
A0010013:'',
A0010014:'',
},
// uuid1:'',
// cusList:{},
uploadUrl: 'file/upload',
fileList: [],
}
},
created() {
console.log("上個頁面傳來的是",sessionStorage)
// this.handleUUIDv1()
this.form.code = this.$uuid.v1()
console.log("頁面刷新后新產生的uuid是",this.form.code)
},
watch:{
reportName: function (newVal,oldVal) {//新值和原值
console.log("name改變了 a:"+newVal+" b:"+oldVal)
}
},
methods: {
//使用uuid
// handleUUIDv1() {
// this.uuid1 = this.$uuid.v1()
// },
//點擊多選按鈕true和false變成1或者2
reset(form){
this.$refs['form'].resetFields();
},
//獲取上個頁面傳來的數據
getcustomQuery() {
let reportName=this.form.reportName
sessionStorage.setItem("reportName",reportName)
let reportEnName = this.form.reportEnName
sessionStorage.setItem("reportEnName",reportEnName)
let reportChEnName = this.form.reportChEnName
sessionStorage.setItem("reportChEnName",reportChEnName)
let remarks = this.form.remarks
sessionStorage.setItem("remarks",remarks)
let code = this.form.code
sessionStorage.setItem("code",code)
let isPublic = this.form.isPublic
sessionStorage.setItem("isPublic",isPublic)
let isPrinting = this.form.isPrinting
sessionStorage.setItem("isPrinting",isPrinting)
let isExport = this.form.isExport
sessionStorage.setItem("isExport",isExport)
console.log("上個頁面傳來的是",sessionStorage)
},
// this.$router.push('/reportTools/cusMain'),點擊保存發送鏈接 並跳轉
tocustomQuery(){
this.$router.push('/reportTools/customQuery')
},
saveResource(){
console.log("當前頁面表單的數據",this.form)
// sessionStorage.setItem("params",JSON.stringify(this.form))
console.log("sessionStorage中的數據",sessionStorage)
this.getcustomQuery()
this.$http.post("http://localhost:8080/reporttools/report-show/addReportShow",sessionStorage).then(
resp=>{
console.log(resp)
//this.$router.push('/reportTools/cusMain')
})
},
handleExceed(files, fileList) {
this.$message.warning(`當前限制選擇 5 個文件,本次選擇了 ${files.length} 個文件,共選擇了 ${files.length + fileList.length} 個文件`);
},
beforeRemove(file, fileList) {
return this.$confirm(`確定移除 ${ file.name }?`);
},
handleUploadError(error, file) {
console.log("文件上傳出錯:"+error)
},
//測試上傳文件(注意web的上下文)
handleBeforeUpload(file){
console.log("開始上傳,上傳的文件為:"+file)
let formData = new FormData();
formData.append("multipartFiles", file);
axios({
method: 'post',
url: 'http://localhost:8080/file/upload',
data: formData,
headers: {'Content-Type': 'multipart/form-data' }
}).then((res) => {
console.log("文件上傳返回:"+res)
}).catch(error => {
console.log("文件上傳異常:"+error)
})
},
}
}
</script>
<style scoped>
a {
text-decoration: none;
}
</style>
2.3.1上傳script全部代碼
主要是對前端做一些限制
handleExceed(files, fileList) {
this.$message.warning(`當前限制選擇 5 個文件,本次選擇了 ${files.length} 個文件,共選擇了 ${files.length + fileList.length} 個文件`);
},
beforeRemove(file, fileList) {
return this.$confirm(`確定移除 ${ file.name }?`);
},
handleUploadError(error, file) {
console.log("文件上傳出錯:"+error)
},
//測試上傳文件(注意web的上下文)
handleBeforeUpload(file){
console.log("開始上傳,上傳的文件為:"+file)
let formData = new FormData();
formData.append("multipartFiles", file);
axios({
method: 'post',
url: 'http://localhost:8080/file/upload',
data: formData,
headers: {'Content-Type': 'multipart/form-data' }
}).then((res) => {
console.log("文件上傳返回:"+res)
}).catch(error => {
console.log("文件上傳異常:"+error)
})
},
2.3.2在data的return里添加
uploadUrl: 'file/upload',
fileList: [],
也就是在data層的 return里寫
data(){
return {
uploadUrl: 'http://localhost:8080/file/upload',
fileList: [],
}
}
就這樣前端就完成了
后端主要就是一個controller層
2.3.3Axios的引入
很重要的一個,因為我的寫法不一樣可能有不兼容的地方,有的vue不能這么寫,所以需要引入一下
直接引入就可以,在script里面,
不會就看我上面分享的
import axios from 'axios'
3.后端
3.1后端全部代碼
package com.ciic.reporter.updownload.controller;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@RestController
@RequestMapping("file")
public class CiicUpDownloadController {
private final static String rootPath = "E:/attachment/";
@RequestMapping("/upload")
public Object uploadFile(@RequestParam("file") MultipartFile[] multipartFiles){
File fileDir = new File(rootPath);
if (!fileDir.exists() && !fileDir.isDirectory()) {
fileDir.mkdirs();
}
try {
if (multipartFiles != null && multipartFiles.length > 0) {
for(int i = 0;i<multipartFiles.length;i++){
try {
//以原來的名稱命名,覆蓋掉舊的,這里也可以使用UUID之類的方式命名,這里就沒有處理了
String storagePath = rootPath+multipartFiles[i].getOriginalFilename();
System.out.println("上傳的文件:" + multipartFiles[i].getName() + "," + multipartFiles[i].getContentType() + "," + multipartFiles[i].getOriginalFilename()
+",保存的路徑為:" + storagePath);
// 3種方法: 第1種
// Streams.copy(multipartFiles[i].getInputStream(), new FileOutputStream(storagePath), true);
// 第2種
Path path = Paths.get(storagePath);
Files.write(path,multipartFiles[i].getBytes());
// 第3種
// multipartFiles[i].transferTo(new File(storagePath));
} catch (IOException e) {
e.printStackTrace();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
//前端可以通過狀態碼,判斷文件是否上傳成功
return "文件上傳成功";
}
/**
*
* @param fileName 文件名
* @param response
* @return
*/
@RequestMapping("/download")
public Object downloadFile(@RequestParam String fileName, HttpServletResponse response){
OutputStream os = null;
InputStream is= null;
try {
// 取得輸出流
os = response.getOutputStream();
// 清空輸出流
response.reset();
response.setContentType("application/x-download;charset=utf-8");
//Content-Disposition中指定的類型是文件的擴展名,並且彈出的下載對話框中的文件類型圖片是按照文件的擴展名顯示的,點保存后,文件以filename的值命名,
// 保存類型以Content中設置的為准。注意:在設置Content-Disposition頭字段之前,一定要設置Content-Type頭字段。
//把文件名按UTF-8取出,並按ISO8859-1編碼,保證彈出窗口中的文件名中文不亂碼,中文不要太多,最多支持17個中文,因為header有150個字節限制。
response.setHeader("Content-Disposition", "attachment;filename="+ new String(fileName.getBytes("utf-8"),"ISO8859-1"));
//讀取流
File f = new File(rootPath+fileName);
is = new FileInputStream(f);
if (is == null) {
System.out.println("下載附件失敗,請檢查文件“" + fileName + "”是否存在");
return "下載附件失敗,請檢查文件“" + fileName + "”是否存在";
}
//復制
IOUtils.copy(is, response.getOutputStream());
response.getOutputStream().flush();
} catch (IOException e) {
return "下載附件失敗,error:"+e.getMessage();
}
//文件的關閉放在finally中
finally
{
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (os != null) {
os.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
//其實,這個返回什么都不重要
return "下載成功";
}
}
3.2后端定義一個class類
@RestController
@RequestMapping("file")
public class CiicUpDownloadController {
}
3.3后端定義一個上傳的路徑
private final static String rootPath = "E:/attachment/";
3.3.1加上上面定義的類也就是
@RestController
@RequestMapping("file")
public class CiicUpDownloadController {
private final static String rootPath = "E:/attachment/";
}
3.4后端定義上傳方法
@RequestMapping("/upload")
public Object uploadFile(@RequestParam("file") MultipartFile[] multipartFiles){
File fileDir = new File(rootPath);
if (!fileDir.exists() && !fileDir.isDirectory()) {
fileDir.mkdirs();
}
try {
if (multipartFiles != null && multipartFiles.length > 0) {
for(int i = 0;i<multipartFiles.length;i++){
try {
//以原來的名稱命名,覆蓋掉舊的,這里也可以使用UUID之類的方式命名,這里就沒有處理了
String storagePath = rootPath+multipartFiles[i].getOriginalFilename();
System.out.println("上傳的文件:" + multipartFiles[i].getName() + "," + multipartFiles[i].getContentType() + "," + multipartFiles[i].getOriginalFilename()
+",保存的路徑為:" + storagePath);
// 3種方法: 第1種
// Streams.copy(multipartFiles[i].getInputStream(), new FileOutputStream(storagePath), true);
// 第2種
Path path = Paths.get(storagePath);
Files.write(path,multipartFiles[i].getBytes());
// 第3種
// multipartFiles[i].transferTo(new File(storagePath));
} catch (IOException e) {
e.printStackTrace();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
//前端可以通過狀態碼,判斷文件是否上傳成功
return "文件上傳成功";
}
3.4.3加上之前的路徑和類就是
@RestController
@RequestMapping("file")
public class CiicUpDownloadController {
private final static String rootPath = "E:/attachment/";
@RequestMapping("/upload")
public Object uploadFile(@RequestParam("file") MultipartFile[] multipartFiles){
File fileDir = new File(rootPath);
if (!fileDir.exists() && !fileDir.isDirectory()) {
fileDir.mkdirs();
}
try {
if (multipartFiles != null && multipartFiles.length > 0) {
for(int i = 0;i<multipartFiles.length;i++){
try {
//以原來的名稱命名,覆蓋掉舊的,這里也可以使用UUID之類的方式命名,這里就沒有處理了
String storagePath = rootPath+multipartFiles[i].getOriginalFilename();
System.out.println("上傳的文件:" + multipartFiles[i].getName() + "," + multipartFiles[i].getContentType() + "," + multipartFiles[i].getOriginalFilename()
+",保存的路徑為:" + storagePath);
// 3種方法: 第1種
// Streams.copy(multipartFiles[i].getInputStream(), new FileOutputStream(storagePath), true);
// 第2種
Path path = Paths.get(storagePath);
Files.write(path,multipartFiles[i].getBytes());
// 第3種
// multipartFiles[i].transferTo(new File(storagePath));
} catch (IOException e) {
e.printStackTrace();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
//前端可以通過狀態碼,判斷文件是否上傳成功
return "文件上傳成功";
}
}
3.5后端定義下載方法
/**
*
* @param fileName 文件名
* @param response
* @return
*/
@RequestMapping("/download")
public Object downloadFile(@RequestParam String fileName, HttpServletResponse response){
OutputStream os = null;
InputStream is= null;
try {
// 取得輸出流
os = response.getOutputStream();
// 清空輸出流
response.reset();
response.setContentType("application/x-download;charset=utf-8");
//Content-Disposition中指定的類型是文件的擴展名,並且彈出的下載對話框中的文件類型圖片是按照文件的擴展名顯示的,點保存后,文件以filename的值命名,
// 保存類型以Content中設置的為准。注意:在設置Content-Disposition頭字段之前,一定要設置Content-Type頭字段。
//把文件名按UTF-8取出,並按ISO8859-1編碼,保證彈出窗口中的文件名中文不亂碼,中文不要太多,最多支持17個中文,因為header有150個字節限制。
response.setHeader("Content-Disposition", "attachment;filename="+ new String(fileName.getBytes("utf-8"),"ISO8859-1"));
//讀取流
File f = new File(rootPath+fileName);
is = new FileInputStream(f);
if (is == null) {
System.out.println("下載附件失敗,請檢查文件“" + fileName + "”是否存在");
return "下載附件失敗,請檢查文件“" + fileName + "”是否存在";
}
//復制
IOUtils.copy(is, response.getOutputStream());
response.getOutputStream().flush();
} catch (IOException e) {
return "下載附件失敗,error:"+e.getMessage();
}
//文件的關閉放在finally中
finally
{
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (os != null) {
os.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
//其實,這個返回什么都不重要
return "下載成功";
}
3.5.1加上之前定義的路徑類和上傳方法就是
package com.ciic.reporter.updownload.controller;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@RestController
@RequestMapping("file")
public class CiicUpDownloadController {
private final static String rootPath = "E:/attachment/";
@RequestMapping("/upload")
public Object uploadFile(@RequestParam("file") MultipartFile[] multipartFiles){
File fileDir = new File(rootPath);
if (!fileDir.exists() && !fileDir.isDirectory()) {
fileDir.mkdirs();
}
try {
if (multipartFiles != null && multipartFiles.length > 0) {
for(int i = 0;i<multipartFiles.length;i++){
try {
//以原來的名稱命名,覆蓋掉舊的,這里也可以使用UUID之類的方式命名,這里就沒有處理了
String storagePath = rootPath+multipartFiles[i].getOriginalFilename();
System.out.println("上傳的文件:" + multipartFiles[i].getName() + "," + multipartFiles[i].getContentType() + "," + multipartFiles[i].getOriginalFilename()
+",保存的路徑為:" + storagePath);
// 3種方法: 第1種
// Streams.copy(multipartFiles[i].getInputStream(), new FileOutputStream(storagePath), true);
// 第2種
Path path = Paths.get(storagePath);
Files.write(path,multipartFiles[i].getBytes());
// 第3種
// multipartFiles[i].transferTo(new File(storagePath));
} catch (IOException e) {
e.printStackTrace();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
//前端可以通過狀態碼,判斷文件是否上傳成功
return "文件上傳成功";
}
/**
*
* @param fileName 文件名
* @param response
* @return
*/
@RequestMapping("/download")
public Object downloadFile(@RequestParam String fileName, HttpServletResponse response){
OutputStream os = null;
InputStream is= null;
try {
// 取得輸出流
os = response.getOutputStream();
// 清空輸出流
response.reset();
response.setContentType("application/x-download;charset=utf-8");
//Content-Disposition中指定的類型是文件的擴展名,並且彈出的下載對話框中的文件類型圖片是按照文件的擴展名顯示的,點保存后,文件以filename的值命名,
// 保存類型以Content中設置的為准。注意:在設置Content-Disposition頭字段之前,一定要設置Content-Type頭字段。
//把文件名按UTF-8取出,並按ISO8859-1編碼,保證彈出窗口中的文件名中文不亂碼,中文不要太多,最多支持17個中文,因為header有150個字節限制。
response.setHeader("Content-Disposition", "attachment;filename="+ new String(fileName.getBytes("utf-8"),"ISO8859-1"));
//讀取流
File f = new File(rootPath+fileName);
is = new FileInputStream(f);
if (is == null) {
System.out.println("下載附件失敗,請檢查文件“" + fileName + "”是否存在");
return "下載附件失敗,請檢查文件“" + fileName + "”是否存在";
}
//復制
IOUtils.copy(is, response.getOutputStream());
response.getOutputStream().flush();
} catch (IOException e) {
return "下載附件失敗,error:"+e.getMessage();
}
//文件的關閉放在finally中
finally
{
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (os != null) {
os.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
//其實,這個返回什么都不重要
return "下載成功";
}
}
謝謝大家的閱讀,原創不易,喜歡就隨手點個贊👍,這將是我最強的寫作動力。如果覺得文章對你有點幫助,還挺有趣,就關注一下我的公眾號「仵航」