完成帖子图片插入操作
This commit is contained in:
parent
e6c765c1ca
commit
b84cbdc380
@ -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, "图片上传失败!");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -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> {
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
22
my-project-frontend/package-lock.json
generated
22
my-project-frontend/package-lock.json
generated
@ -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",
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user