完成帖子图片插入操作

This commit is contained in:
柏码の讲师 2023-10-08 23:17:57 +08:00
parent e6c765c1ca
commit b84cbdc380
9 changed files with 227 additions and 16 deletions

View File

@ -4,6 +4,7 @@ import com.example.entity.RestBean;
import com.example.service.ImageService; import com.example.service.ImageService;
import com.example.utils.Const; import com.example.utils.Const;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@ -32,4 +33,21 @@ public class ImageController {
return RestBean.failure(400, "头像上传失败!"); return RestBean.failure(400, "头像上传失败!");
} }
} }
@PostMapping("/cache")
public RestBean<String> uploadImage(@RequestParam("file") MultipartFile file,
@RequestAttribute(Const.ATTR_USER_ID) int id,
HttpServletResponse response) throws IOException {
if(file.getSize() > 1024 * 1024 * 5)
return RestBean.failure(400, "图片不能大于5MB");
log.info("正在进行图片上传操作...");
String url = service.uploadImage(id, file);
if(url != null) {
log.info("成功上传图片,大小: "+file.getSize());
return RestBean.success(url);
} else {
response.setStatus(400);
return RestBean.failure(400, "图片上传失败!");
}
}
} }

View File

@ -16,9 +16,10 @@ public class ObjectController {
@Resource @Resource
ImageService service; ImageService service;
@GetMapping("/images/avatar/**") @GetMapping("/images/**")
public void imagesFetch(HttpServletRequest request, HttpServletResponse response) throws Exception { public void imagesFetch(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setHeader("Cache-Control", "max-age=2592000"); response.setHeader("Cache-Control", "max-age=2592000");
response.setHeader("Content-Type", "image/jpg");
this.fetchImage(request, response); this.fetchImage(request, response);
} }

View File

@ -0,0 +1,16 @@
package com.example.entity.dto;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Date;
@Data
@AllArgsConstructor
@TableName("db_image_store")
public class StoreImage {
Integer uid;
String name;
Date time;
}

View File

@ -0,0 +1,9 @@
package com.example.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.entity.dto.StoreImage;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ImageStoreMapper extends BaseMapper<StoreImage> {
}

View File

@ -1,11 +1,14 @@
package com.example.service; package com.example.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.entity.dto.StoreImage;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
public interface ImageService { public interface ImageService extends IService<StoreImage> {
String uploadAvatar(int id, MultipartFile file) throws IOException; String uploadAvatar(int id, MultipartFile file) throws IOException;
String uploadImage(int id, MultipartFile file) throws IOException;
void fetchImageFromMinio(OutputStream stream, String imagePath) throws Exception; void fetchImageFromMinio(OutputStream stream, String imagePath) throws Exception;
} }

View File

@ -1,26 +1,30 @@
package com.example.service.impl; package com.example.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.entity.dto.Account; import com.example.entity.dto.Account;
import com.example.entity.dto.StoreImage;
import com.example.mapper.AccountMapper; import com.example.mapper.AccountMapper;
import com.example.mapper.ImageStoreMapper;
import com.example.service.ImageService; import com.example.service.ImageService;
import io.minio.GetObjectArgs; import io.minio.*;
import io.minio.GetObjectResponse;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.compress.utils.IOUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
@Service @Service
public class ImageServiceImpl implements ImageService { public class ImageServiceImpl extends ServiceImpl<ImageStoreMapper, StoreImage> implements ImageService {
@Resource @Resource
MinioClient client; MinioClient client;
@ -28,6 +32,11 @@ public class ImageServiceImpl implements ImageService {
@Resource @Resource
AccountMapper mapper; AccountMapper mapper;
@Resource
StringRedisTemplate template;
SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd");
@Override @Override
public void fetchImageFromMinio(OutputStream stream, String imagePath) throws Exception { public void fetchImageFromMinio(OutputStream stream, String imagePath) throws Exception {
GetObjectArgs args = GetObjectArgs.builder() GetObjectArgs args = GetObjectArgs.builder()
@ -38,6 +47,30 @@ public class ImageServiceImpl implements ImageService {
IOUtils.copy(object, stream); IOUtils.copy(object, stream);
} }
@Override
public String uploadImage(int id, MultipartFile file) throws IOException {
if(!limitUpload(id)) return null;
String imageName = UUID.randomUUID().toString().replace("-", "");
Date date = new Date();
imageName = "/cache/" + format.format(date) + "/" + imageName;
PutObjectArgs args = PutObjectArgs.builder()
.bucket("test")
.stream(file.getInputStream(), file.getSize(), -1)
.object(imageName)
.build();
try {
client.putObject(args);
if(this.save(new StoreImage(id, imageName, date))){
return imageName;
} else {
return null;
}
}catch (Exception e) {
log.error("图片上传出现问题: "+e.getMessage(), e);
return null;
}
}
@Override @Override
public String uploadAvatar(int id, MultipartFile file) throws IOException { public String uploadAvatar(int id, MultipartFile file) throws IOException {
//使用随机UUID作为图片名字而不是直接用户ID是因为用户更改头像之后可以立即更新缓存 //使用随机UUID作为图片名字而不是直接用户ID是因为用户更改头像之后可以立即更新缓存
@ -53,6 +86,8 @@ public class ImageServiceImpl implements ImageService {
if(mapper.update(null, Wrappers.<Account>update() if(mapper.update(null, Wrappers.<Account>update()
.eq("id", id) .eq("id", id)
.set("avatar", imageName)) > 0) { .set("avatar", imageName)) > 0) {
String avatar = mapper.selectById(id).getAvatar();
this.deleteOldAvatar(avatar);
return imageName; return imageName;
} else { } else {
return null; return null;
@ -62,4 +97,28 @@ public class ImageServiceImpl implements ImageService {
return null; return null;
} }
} }
private void deleteOldAvatar(String avatar) throws Exception {
if(avatar == null || avatar.isEmpty()) return;
RemoveObjectArgs remove = RemoveObjectArgs.builder()
.bucket("test")
.object(avatar)
.build();
client.removeObject(remove);
}
private boolean limitUpload(int id){
String key = "image:upload:"+id;
String s = template.opsForValue().get(key);
if(s == null) {
template.opsForValue().set(key, "1", 1, TimeUnit.HOURS);
} else {
if(Integer.parseInt(s) > 20) {
return false;
} else {
template.opsForValue().increment(key);
}
}
return true;
}
} }

View File

@ -14,6 +14,8 @@
"axios": "^1.4.0", "axios": "^1.4.0",
"element-plus": "^2.3.9", "element-plus": "^2.3.9",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"quill-image-resize-vue": "^1.0.4",
"quill-image-super-solution-module": "^2.0.1",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-router": "^4.2.4" "vue-router": "^4.2.4"
}, },
@ -1825,6 +1827,21 @@
"lodash.isequal": "^4.5.0" "lodash.isequal": "^4.5.0"
} }
}, },
"node_modules/quill-image-resize-vue": {
"version": "1.0.4",
"resolved": "https://registry.npmmirror.com/quill-image-resize-vue/-/quill-image-resize-vue-1.0.4.tgz",
"integrity": "sha512-/Nakepctw5PWzY7ebQ0GQgZh6uA0RtW8wLUrkiLRTl0ql7DJ0OhRCEHThsjJO+8HgsfTu3ohLUbOthnsImStJg==",
"dependencies": {
"lodash": "^4.17.4",
"quill": "^1.2.2",
"raw-loader": "^0.5.1"
}
},
"node_modules/quill-image-super-solution-module": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/quill-image-super-solution-module/-/quill-image-super-solution-module-2.0.1.tgz",
"integrity": "sha512-mXN7uN0R3CtnF1m0MkMxTKNVSUXPWmnJmQ+kC/RNa9dlJoC4NS0xQhHB+JRoR5eKZzywBxkVSHHygrskwBMSug=="
},
"node_modules/quill/node_modules/fast-diff": { "node_modules/quill/node_modules/fast-diff": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmmirror.com/fast-diff/-/fast-diff-1.1.2.tgz", "resolved": "https://registry.npmmirror.com/fast-diff/-/fast-diff-1.1.2.tgz",
@ -1843,6 +1860,11 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/raw-loader": {
"version": "0.5.1",
"resolved": "https://registry.npmmirror.com/raw-loader/-/raw-loader-0.5.1.tgz",
"integrity": "sha512-sf7oGoLuaYAScB4VGr0tzetsYlS8EJH6qnTCfQ/WVEa89hALQ4RQfCKt5xCyPQKPDUbVUAIP1QsxAwfAjlDp7Q=="
},
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",

View File

@ -14,6 +14,8 @@
"axios": "^1.4.0", "axios": "^1.4.0",
"element-plus": "^2.3.9", "element-plus": "^2.3.9",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"quill-image-resize-vue": "^1.0.4",
"quill-image-super-solution-module": "^2.0.1",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-router": "^4.2.4" "vue-router": "^4.2.4"
}, },

View File

@ -1,16 +1,23 @@
<script setup> <script setup>
import {Document} from "@element-plus/icons-vue"; import {Document} from "@element-plus/icons-vue";
import {QuillEditor} from "@vueup/vue-quill"; import {QuillEditor, Quill} from "@vueup/vue-quill";
import {reactive} from "vue"; import {reactive, ref} from "vue";
import '@vueup/vue-quill/dist/vue-quill.snow.css'; import '@vueup/vue-quill/dist/vue-quill.snow.css';
import {accessHeader} from "@/net";
import axios from "axios";
import {ElMessage} from "element-plus";
import ImageResize from "quill-image-resize-vue";
import { ImageExtend, QuillWatch } from "quill-image-super-solution-module";
defineProps({ show: Boolean }) defineProps({ show: Boolean })
const emit = defineEmits(['close']) const emit = defineEmits(['close'])
const refEditor = ref()
const editor = reactive({ const editor = reactive({
type: 1, type: 1,
title: '', title: '',
text: '' text: '',
uploading: false
}) })
const types = [ const types = [
{id: 1, name: '日常闲聊', desc: '在这里分享你的各种日常'}, {id: 1, name: '日常闲聊', desc: '在这里分享你的各种日常'},
@ -19,12 +26,76 @@ const types = [
{id: 4, name: '恋爱官宣', desc: '向大家展示你的恋爱成果'}, {id: 4, name: '恋爱官宣', desc: '向大家展示你的恋爱成果'},
{id: 5, name: '踩坑记录', desc: '将你遇到的坑分享给大家,防止其他人再次入坑'}, {id: 5, name: '踩坑记录', desc: '将你遇到的坑分享给大家,防止其他人再次入坑'},
] ]
function initEditor() {
refEditor.value.setContents('', 'user')
editor.title = ''
editor.type = 1
}
function submitTopic(){
console.info(editor.text)
}
Quill.register('modules/imageResize', ImageResize);
Quill.register('modules/ImageExtend', ImageExtend)
const editorOption = {
placeholder: "今天分享点什么呢?",
modules: {
toolbar: {
container: [
"bold", "italic", "underline", "strike","clean",
{color: []}, {'background': []},
{size: ["small", false, "large", "huge"]},
{ header: [1, 2, 3, 4, 5, 6, false] },
{list: "ordered"}, {list: "bullet"}, {align: []},
"blockquote", "code-block", "link", "image",
{ indent: '-1' }, { indent: '+1' }
],
handlers: {
'image': function () {
QuillWatch.emit(this.quill.id)
}
}
},
imageResize: {
modules: [ 'Resize', 'DisplaySize' ]
},
ImageExtend: {
action: axios.defaults.baseURL + '/api/image/cache',
name: 'file',
size: 5,
loading: true,
accept: 'image/png, image/jpeg',
response: (resp) => {
if(resp.data) {
return axios.defaults.baseURL + '/images' + resp.data
} else {
return null
}
},
methods: 'POST',
headers: xhr => {
xhr.setRequestHeader('Authorization', accessHeader().Authorization);
},
start: () => editor.uploading = true,
success: () => {
ElMessage.success('图片上传成功!')
editor.uploading = false
},
error: () => {
ElMessage.warning('图片上传失败,请联系管理员!')
editor.uploading = false
}
}
}
}
</script> </script>
<template> <template>
<div> <div>
<el-drawer direction="btt" :model-value="show" <el-drawer direction="btt" :model-value="show"
:size="650" :size="650" @open="initEditor"
@close="emit('close')" @close="emit('close')"
:show-close="false"> :show-close="false">
<template #header> <template #header>
@ -43,18 +114,21 @@ const types = [
<el-input placeholder="请输入帖子标题..." :prefix-icon="Document" v-model="editor.title"/> <el-input placeholder="请输入帖子标题..." :prefix-icon="Document" v-model="editor.title"/>
</div> </div>
</div> </div>
<div style="margin-top: 15px;height: 410px"> <div style="margin-top: 15px;height: 450px;border-radius: 5px;overflow: hidden"
<quill-editor ref="refEditor" v-model:content="editor.text" :options="editorOption" content-type="delta"/> v-loading="editor.uploading"
element-loading-text="正在上传图片,请稍后...">
<quill-editor ref="refEditor" v-model:content="editor.text"
:options="editorOption" content-type="delta"
style="height: calc(100% - 44px)"/>
</div> </div>
<div style="display: flex;justify-content: space-between;margin-top: 50px"> <div style="display: flex;justify-content: space-between;margin-top: 10px">
<div style="color: grey;font-size: 13px"> <div style="color: grey;font-size: 13px">
当前字数: 666最大支持20000字 当前字数: 666最大支持20000字
</div> </div>
<div> <div>
<el-button type="success" plain>立即发布</el-button> <el-button @click="submitTopic" type="success" plain>立即发布</el-button>
</div> </div>
</div> </div>
</el-drawer> </el-drawer>
</div> </div>
</template> </template>
@ -76,4 +150,11 @@ const types = [
border-radius: 0 0 5px 5px; border-radius: 0 0 5px 5px;
border-color: var(--el-border-color); border-color: var(--el-border-color);
} }
:deep(.ql-editor.ql-blank::before){
color: grey;
font-style: normal;
}
:deep(.ql-editor) {
font-size: 14px;
}
</style> </style>