完成帖子发表功能
This commit is contained in:
parent
b8b9466fcf
commit
e487de0071
@ -14,13 +14,13 @@ import com.example.service.AccountDetailsService;
|
||||
import com.example.service.AccountPrivacyService;
|
||||
import com.example.service.AccountService;
|
||||
import com.example.utils.Const;
|
||||
import com.example.utils.ControllerUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Validated
|
||||
@RestController
|
||||
@ -36,6 +36,9 @@ public class AccountController {
|
||||
@Resource
|
||||
AccountPrivacyService privacyService;
|
||||
|
||||
@Resource
|
||||
ControllerUtils utils;
|
||||
|
||||
@GetMapping("/info")
|
||||
public RestBean<AccountVO> info(@RequestAttribute(Const.ATTR_USER_ID) int id){
|
||||
Account account = service.findAccountById(id);
|
||||
@ -60,13 +63,13 @@ public class AccountController {
|
||||
@PostMapping("/modify-email")
|
||||
public RestBean<Void> modifyEmail(@RequestAttribute(Const.ATTR_USER_ID) int id,
|
||||
@RequestBody @Valid ModifyEmailVO vo){
|
||||
return this.messageHandle(() -> service.modifyEmail(id, vo));
|
||||
return utils.messageHandle(() -> service.modifyEmail(id, vo));
|
||||
}
|
||||
|
||||
@PostMapping("/change-password")
|
||||
public RestBean<Void> changePassword(@RequestAttribute(Const.ATTR_USER_ID) int id,
|
||||
@RequestBody @Valid ChangePasswordVO vo){
|
||||
return this.messageHandle(() -> service.changePassword(id, vo));
|
||||
return utils.messageHandle(() -> service.changePassword(id, vo));
|
||||
}
|
||||
|
||||
@PostMapping("/save-privacy")
|
||||
@ -80,12 +83,4 @@ public class AccountController {
|
||||
public RestBean<AccountPrivacyVO> privacy(@RequestAttribute(Const.ATTR_USER_ID) int id){
|
||||
return RestBean.success(privacyService.accountPrivacy(id).asViewObject(AccountPrivacyVO.class));
|
||||
}
|
||||
|
||||
private <T> RestBean<T> messageHandle(Supplier<String> action){
|
||||
String message = action.get();
|
||||
if(message == null)
|
||||
return RestBean.success();
|
||||
else
|
||||
return RestBean.failure(400, message);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import com.example.entity.vo.request.ConfirmResetVO;
|
||||
import com.example.entity.vo.request.EmailRegisterVO;
|
||||
import com.example.entity.vo.request.EmailResetVO;
|
||||
import com.example.service.AccountService;
|
||||
import com.example.utils.ControllerUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
@ -15,8 +16,6 @@ import jakarta.validation.constraints.Pattern;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 用于验证相关Controller包含用户的注册、重置密码等操作
|
||||
*/
|
||||
@ -29,6 +28,9 @@ public class AuthorizeController {
|
||||
@Resource
|
||||
AccountService accountService;
|
||||
|
||||
@Resource
|
||||
ControllerUtils utils;
|
||||
|
||||
/**
|
||||
* 请求邮件验证码
|
||||
* @param email 请求邮件
|
||||
@ -41,7 +43,7 @@ public class AuthorizeController {
|
||||
public RestBean<Void> askVerifyCode(@RequestParam @Email String email,
|
||||
@RequestParam @Pattern(regexp = "(register|reset|modify)") String type,
|
||||
HttpServletRequest request){
|
||||
return this.messageHandle(() ->
|
||||
return utils.messageHandle(() ->
|
||||
accountService.registerEmailVerifyCode(type, String.valueOf(email), request.getRemoteAddr()));
|
||||
}
|
||||
|
||||
@ -53,7 +55,7 @@ public class AuthorizeController {
|
||||
@PostMapping("/register")
|
||||
@Operation(summary = "用户注册操作")
|
||||
public RestBean<Void> register(@RequestBody @Valid EmailRegisterVO vo){
|
||||
return this.messageHandle(() ->
|
||||
return utils.messageHandle(() ->
|
||||
accountService.registerEmailAccount(vo));
|
||||
}
|
||||
|
||||
@ -65,7 +67,7 @@ public class AuthorizeController {
|
||||
@PostMapping("/reset-confirm")
|
||||
@Operation(summary = "密码重置确认")
|
||||
public RestBean<Void> resetConfirm(@RequestBody @Valid ConfirmResetVO vo){
|
||||
return this.messageHandle(() -> accountService.resetConfirm(vo));
|
||||
return utils.messageHandle(() -> accountService.resetConfirm(vo));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -76,21 +78,7 @@ public class AuthorizeController {
|
||||
@PostMapping("/reset-password")
|
||||
@Operation(summary = "密码重置操作")
|
||||
public RestBean<Void> resetPassword(@RequestBody @Valid EmailResetVO vo){
|
||||
return this.messageHandle(() ->
|
||||
return utils.messageHandle(() ->
|
||||
accountService.resetEmailAccountPassword(vo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 针对于返回值为String作为错误信息的方法进行统一处理
|
||||
* @param action 具体操作
|
||||
* @return 响应结果
|
||||
* @param <T> 响应结果类型
|
||||
*/
|
||||
private <T> RestBean<T> messageHandle(Supplier<String> action){
|
||||
String message = action.get();
|
||||
if(message == null)
|
||||
return RestBean.success();
|
||||
else
|
||||
return RestBean.failure(400, message);
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,16 @@
|
||||
package com.example.controller;
|
||||
|
||||
import com.example.entity.RestBean;
|
||||
import com.example.entity.vo.request.TopicCreateVO;
|
||||
import com.example.entity.vo.response.TopicTypeVO;
|
||||
import com.example.entity.vo.response.WeatherVO;
|
||||
import com.example.service.TopicService;
|
||||
import com.example.service.WeatherService;
|
||||
import com.example.utils.Const;
|
||||
import com.example.utils.ControllerUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -23,6 +25,9 @@ public class ForumController {
|
||||
@Resource
|
||||
TopicService topicService;
|
||||
|
||||
@Resource
|
||||
ControllerUtils utils;
|
||||
|
||||
@GetMapping("/weather")
|
||||
public RestBean<WeatherVO> weather(double latitude, double longitude){
|
||||
WeatherVO weatherVO = service.fetchWeather(latitude, longitude);
|
||||
@ -38,4 +43,10 @@ public class ForumController {
|
||||
.map(type -> type.asViewObject(TopicTypeVO.class))
|
||||
.toList());
|
||||
}
|
||||
|
||||
@PostMapping("/create-topic")
|
||||
public RestBean<Void> createTopic(@Valid @RequestBody TopicCreateVO vo,
|
||||
@RequestAttribute(Const.ATTR_USER_ID) int id) {
|
||||
return utils.messageHandle(() -> topicService.createTopic(id, vo));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
package com.example.entity.dto;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@TableName("db_topic")
|
||||
public class Topic {
|
||||
@TableId(type = IdType.AUTO)
|
||||
Integer id;
|
||||
String title;
|
||||
String content;
|
||||
Integer uid;
|
||||
Integer type;
|
||||
Date time;
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.example.entity.vo.request;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
@Data
|
||||
public class TopicCreateVO {
|
||||
@Min(1)
|
||||
@Max(5)
|
||||
int type;
|
||||
@Length(max = 30)
|
||||
String title;
|
||||
JSONObject content;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.example.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.example.entity.dto.Topic;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface TopicMapper extends BaseMapper<Topic> {
|
||||
}
|
@ -1,9 +1,13 @@
|
||||
package com.example.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.example.entity.dto.Topic;
|
||||
import com.example.entity.dto.TopicType;
|
||||
import com.example.entity.vo.request.TopicCreateVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TopicService {
|
||||
public interface TopicService extends IService<Topic> {
|
||||
List<TopicType> listType();
|
||||
String createTopic(int uid, TopicCreateVO vo);
|
||||
}
|
||||
|
@ -7,11 +7,12 @@ import com.example.entity.dto.StoreImage;
|
||||
import com.example.mapper.AccountMapper;
|
||||
import com.example.mapper.ImageStoreMapper;
|
||||
import com.example.service.ImageService;
|
||||
import com.example.utils.Const;
|
||||
import com.example.utils.FlowUtils;
|
||||
import io.minio.*;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.compress.utils.IOUtils;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@ -20,7 +21,6 @@ import java.io.OutputStream;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@ -33,7 +33,7 @@ public class ImageServiceImpl extends ServiceImpl<ImageStoreMapper, StoreImage>
|
||||
AccountMapper mapper;
|
||||
|
||||
@Resource
|
||||
StringRedisTemplate template;
|
||||
FlowUtils flowUtils;
|
||||
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd");
|
||||
|
||||
@ -49,7 +49,8 @@ public class ImageServiceImpl extends ServiceImpl<ImageStoreMapper, StoreImage>
|
||||
|
||||
@Override
|
||||
public String uploadImage(int id, MultipartFile file) throws IOException {
|
||||
if(!limitUpload(id)) return null;
|
||||
String key = Const.FORUM_IMAGE_COUNTER + id;
|
||||
if(flowUtils.limitPeriodCounterCheck(key, 20, 3600)) return null;
|
||||
String imageName = UUID.randomUUID().toString().replace("-", "");
|
||||
Date date = new Date();
|
||||
imageName = "/cache/" + format.format(date) + "/" + imageName;
|
||||
@ -106,19 +107,4 @@ public class ImageServiceImpl extends ServiceImpl<ImageStoreMapper, StoreImage>
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,61 @@
|
||||
package com.example.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.example.entity.dto.Topic;
|
||||
import com.example.entity.dto.TopicType;
|
||||
import com.example.entity.vo.request.TopicCreateVO;
|
||||
import com.example.mapper.TopicMapper;
|
||||
import com.example.mapper.TopicTypeMapper;
|
||||
import com.example.service.TopicService;
|
||||
import com.example.utils.Const;
|
||||
import com.example.utils.FlowUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class TopicServiceImpl implements TopicService {
|
||||
public class TopicServiceImpl extends ServiceImpl<TopicMapper, Topic> implements TopicService {
|
||||
|
||||
@Resource
|
||||
TopicTypeMapper typeMapper;
|
||||
|
||||
@Resource
|
||||
FlowUtils flowUtils;
|
||||
|
||||
@Override
|
||||
public List<TopicType> listType() {
|
||||
return typeMapper.selectList(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createTopic(int uid, TopicCreateVO vo) {
|
||||
if(!this.textLimitCheck(vo.getContent()))
|
||||
return "文章长度过大,发文失败!";
|
||||
String key = Const.FORUM_TOPIC_CREATE_COUNTER + uid;
|
||||
if(flowUtils.limitPeriodCounterCheck(key, 3, 3600))
|
||||
return "发文频繁,请稍后再试!";
|
||||
Topic topic = new Topic();
|
||||
BeanUtils.copyProperties(vo, topic);
|
||||
topic.setContent(vo.getContent().toJSONString());
|
||||
topic.setUid(uid);
|
||||
topic.setTime(new Date());
|
||||
if(this.save(topic)) {
|
||||
return null;
|
||||
} else {
|
||||
return "内部错误,请联系管理员";
|
||||
}
|
||||
}
|
||||
|
||||
private boolean textLimitCheck(JSONObject object) {
|
||||
if(object == null) return false;
|
||||
long length = 0;
|
||||
for (Object op : object.getJSONArray("ops")) {
|
||||
length += JSONObject.from(op).getString("insert").length();
|
||||
}
|
||||
return length <= 20000;
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.example.entity.vo.response.WeatherVO;
|
||||
import com.example.service.WeatherService;
|
||||
import com.example.utils.Const;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@ -41,7 +42,7 @@ public class WeatherServiceImpl implements WeatherService {
|
||||
if(geo == null) return null;
|
||||
JSONObject location = geo.getJSONArray("location").getJSONObject(0);
|
||||
int id = location.getInteger("id");
|
||||
String key = "weather:"+id;
|
||||
String key = Const.FORUM_WEATHER_CACHE +id;
|
||||
String cache = template.opsForValue().get(key);
|
||||
if(cache != null)
|
||||
return JSONObject.parseObject(cache).to(WeatherVO.class);
|
||||
|
@ -22,5 +22,8 @@ public final class Const {
|
||||
public final static String MQ_MAIL = "mail";
|
||||
//用户角色
|
||||
public final static String ROLE_DEFAULT = "user";
|
||||
|
||||
//论坛相关计数限制
|
||||
public final static String FORUM_IMAGE_COUNTER = "image:upload:";
|
||||
public final static String FORUM_WEATHER_CACHE = "weather:cache:";
|
||||
public final static String FORUM_TOPIC_CREATE_COUNTER = "topic:create:";
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
package com.example.utils;
|
||||
|
||||
import com.example.entity.RestBean;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Component
|
||||
public class ControllerUtils {
|
||||
public <T> RestBean<T> messageHandle(Supplier<String> action){
|
||||
String message = action.get();
|
||||
if(message == null)
|
||||
return RestBean.success();
|
||||
else
|
||||
return RestBean.failure(400, message);
|
||||
}
|
||||
}
|
@ -19,6 +19,8 @@ public class FlowUtils {
|
||||
@Resource
|
||||
StringRedisTemplate template;
|
||||
|
||||
private static final LimitAction defaultAction = overclock -> overclock;
|
||||
|
||||
/**
|
||||
* 针对于单次频率限制,请求成功后,在冷却时间内不得再次进行请求,如3秒内不能再次发起请求
|
||||
* @param key 键
|
||||
@ -26,7 +28,7 @@ public class FlowUtils {
|
||||
* @return 是否通过限流检查
|
||||
*/
|
||||
public boolean limitOnceCheck(String key, int blockTime){
|
||||
return this.internalCheck(key, 1, blockTime, (overclock) -> false);
|
||||
return this.internalCheck(key, 1, blockTime, defaultAction);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -63,6 +65,17 @@ public class FlowUtils {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 限制某一段时间内的请求次数,如3秒内限制请求20次
|
||||
* @param counterKey 计数键
|
||||
* @param frequency 请求频率
|
||||
* @param period 计数周期
|
||||
* @return 是否通过限流检查
|
||||
*/
|
||||
public boolean limitPeriodCounterCheck(String counterKey, int frequency, int period){
|
||||
return this.internalCheck(counterKey, frequency, period, defaultAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部使用请求限制主要逻辑
|
||||
* @param key 计数键
|
||||
|
@ -1,16 +1,16 @@
|
||||
<script setup>
|
||||
import {Document} from "@element-plus/icons-vue";
|
||||
import {QuillEditor, Quill} from "@vueup/vue-quill";
|
||||
import {reactive, ref} from "vue";
|
||||
import {computed, reactive, ref} from "vue";
|
||||
import '@vueup/vue-quill/dist/vue-quill.snow.css';
|
||||
import {accessHeader, get} from "@/net";
|
||||
import {accessHeader, get, post} 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 })
|
||||
const emit = defineEmits(['close'])
|
||||
const emit = defineEmits(['close', 'created'])
|
||||
|
||||
const refEditor = ref()
|
||||
const editor = reactive({
|
||||
@ -29,9 +29,40 @@ function initEditor() {
|
||||
}
|
||||
|
||||
function submitTopic(){
|
||||
console.info(editor.text)
|
||||
const text = deltaToText(editor.text)
|
||||
if(text.length > 20000) {
|
||||
ElMessage.warning('字数超出限制,无法发布主题!')
|
||||
return
|
||||
}
|
||||
if(!editor.title) {
|
||||
ElMessage.warning('请填写标题!')
|
||||
return
|
||||
}
|
||||
if(!editor.type) {
|
||||
ElMessage.warning('请选择一个合适的帖子类型!')
|
||||
return
|
||||
}
|
||||
post('/api/forum/create-topic', {
|
||||
type: editor.type,
|
||||
title: editor.title,
|
||||
content: editor.text
|
||||
}, () => {
|
||||
ElMessage.success("帖子发表成功!")
|
||||
emit('created')
|
||||
})
|
||||
}
|
||||
|
||||
function deltaToText(delta) {
|
||||
if(!delta.ops) return ""
|
||||
let str = ''
|
||||
for (let ops of delta.ops) {
|
||||
str += ops.insert
|
||||
}
|
||||
return str.replace(/\s/g, "")
|
||||
}
|
||||
|
||||
const contentLength = computed(() => deltaToText(editor.text).length)
|
||||
|
||||
Quill.register('modules/imageResize', ImageResize);
|
||||
Quill.register('modules/ImageExtend', ImageExtend)
|
||||
const editorOption = {
|
||||
@ -106,7 +137,8 @@ const editorOption = {
|
||||
</el-select>
|
||||
</div>
|
||||
<div style="flex: 1">
|
||||
<el-input placeholder="请输入帖子标题..." :prefix-icon="Document" v-model="editor.title"/>
|
||||
<el-input maxlength="30" placeholder="请输入帖子标题..."
|
||||
:prefix-icon="Document" v-model="editor.title"/>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 15px;height: 450px;border-radius: 5px;overflow: hidden"
|
||||
@ -118,7 +150,7 @@ const editorOption = {
|
||||
</div>
|
||||
<div style="display: flex;justify-content: space-between;margin-top: 10px">
|
||||
<div style="color: grey;font-size: 13px">
|
||||
当前字数: 666(最大支持20000字)
|
||||
当前字数: {{ contentLength }}(最大支持20000字)
|
||||
</div>
|
||||
<div>
|
||||
<el-button @click="submitTopic" type="success" plain>立即发布</el-button>
|
||||
|
@ -98,7 +98,7 @@ navigator.geolocation.getCurrentPosition(position => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<topic-editor :show="editor" @close="editor = false"/>
|
||||
<topic-editor :show="editor" @close="editor = false" @created="editor = false"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user