feat(biji-houdaun): 实现用户注册、登录、 Markdown 文件和图片上传功能
- 新增用户注册、登录接口及服务实现 - 添加 Markdown 文件创建、更新接口及服务实现 - 实现图片上传、获取接口及服务实现 - 集成 Snowflake ID 生成器 - 添加全局异常处理和统一返回结果封装 - 配置跨域访问和静态资源处理 - 实现基础的 XSS 防护
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
package com.test.bijihoudaun.common.advice;
|
||||
|
||||
import com.test.bijihoudaun.common.exception.BaseException;
|
||||
|
||||
import com.test.bijihoudaun.common.response.R;
|
||||
import com.test.bijihoudaun.common.response.ResultCode;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 处理业务异常
|
||||
*/
|
||||
@ExceptionHandler(BaseException.class)
|
||||
public R<Void> handleBaseException(BaseException e, HttpServletRequest request) {
|
||||
return R.fail(e.getResultCode().getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文件大小超出限制异常
|
||||
*/
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
public R<String> handleFileSizeLimitExceeded() {
|
||||
return R.fail("文件大小超过限制");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理参数校验异常
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public R<List<String>> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
|
||||
List<String> errors = e.getBindingResult().getFieldErrors()
|
||||
.stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.collect(Collectors.toList());
|
||||
return R.fail(ResultCode.VALIDATE_FAILED.getCode(), errors.get(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理绑定异常
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
public R<List<String>> handleBindException(BindException e) {
|
||||
List<String> errors = e.getBindingResult().getFieldErrors()
|
||||
.stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.collect(Collectors.toList());
|
||||
return R.fail(ResultCode.VALIDATE_FAILED.getCode(), errors.get(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理其他异常
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public R<Void> handleException(Exception e) {
|
||||
return R.fail(ResultCode.FAILED.getCode(), "系统繁忙,请稍后再试");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.test.bijihoudaun.common.exception;
|
||||
|
||||
import com.test.bijihoudaun.common.response.ResultCode;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 基础业务异常类
|
||||
*/
|
||||
@Getter
|
||||
public class BaseException extends RuntimeException {
|
||||
private final ResultCode resultCode;
|
||||
|
||||
public BaseException(ResultCode resultCode) {
|
||||
super(resultCode.getMsg());
|
||||
this.resultCode = resultCode;
|
||||
}
|
||||
|
||||
public BaseException(ResultCode resultCode, String message) {
|
||||
super(message);
|
||||
this.resultCode = resultCode;
|
||||
}
|
||||
|
||||
public BaseException(ResultCode resultCode, Throwable cause) {
|
||||
super(resultCode.getMsg(), cause);
|
||||
this.resultCode = resultCode;
|
||||
}
|
||||
|
||||
public BaseException(ResultCode resultCode, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.resultCode = resultCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.test.bijihoudaun.common.response;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ProblemDetail;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* 统一返回结果类
|
||||
* @param <T> 数据类型
|
||||
*/
|
||||
@Data
|
||||
public class R<T> implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Integer code; // 状态码
|
||||
private String msg; // 消息
|
||||
private T data; // 数据
|
||||
|
||||
// 成功返回
|
||||
public static <T> R<T> success() {
|
||||
return success(null);
|
||||
}
|
||||
|
||||
public static <T> R<T> success(T data) {
|
||||
R<T> r = new R<>();
|
||||
r.setCode(ResultCode.SUCCESS.getCode());
|
||||
r.setMsg(ResultCode.SUCCESS.getMsg());
|
||||
r.setData(data);
|
||||
return r;
|
||||
}
|
||||
|
||||
// 分页成功返回
|
||||
public static <T> R<T> pageSuccess(long total, T data) {
|
||||
PageR<T> r = new PageR<>();
|
||||
r.setCode(ResultCode.SUCCESS.getCode());
|
||||
r.setMsg(ResultCode.SUCCESS.getMsg());
|
||||
r.setData(data);
|
||||
r.setTotal(total);
|
||||
return r;
|
||||
}
|
||||
|
||||
// 失败返回
|
||||
public static <T> R<T> fail() {
|
||||
return fail(ResultCode.FAILED);
|
||||
}
|
||||
|
||||
public static <T> R<T> fail(ResultCode resultCode) {
|
||||
return fail(resultCode.getCode(), resultCode.getMsg());
|
||||
}
|
||||
|
||||
public static <T> R<T> fail(String msg) {
|
||||
return fail(ResultCode.FAILED.getCode(), msg);
|
||||
}
|
||||
|
||||
public static <T> R<T> fail(Integer code, String msg) {
|
||||
R<T> r = new R<>();
|
||||
r.setCode(code);
|
||||
r.setMsg(msg);
|
||||
return r;
|
||||
}
|
||||
|
||||
// 转换为ProblemDetail
|
||||
public static ProblemDetail toProblemDetail(ResultCode resultCode, HttpStatus status) {
|
||||
ProblemDetail problemDetail = ProblemDetail.forStatus(status);
|
||||
problemDetail.setType(URI.create("https://api.test.com/errors/" + resultCode.name().toLowerCase()));
|
||||
problemDetail.setTitle(resultCode.getMsg());
|
||||
problemDetail.setDetail(resultCode.getMsg());
|
||||
problemDetail.setProperty("code", resultCode.getCode());
|
||||
return problemDetail;
|
||||
}
|
||||
|
||||
// 分页返回结果类
|
||||
private static class PageR<T> extends R<T> {
|
||||
private long total;
|
||||
|
||||
public long getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
public void setTotal(long total) {
|
||||
this.total = total;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.test.bijihoudaun.common.response;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 返回状态码枚举
|
||||
*/
|
||||
@Getter
|
||||
public enum ResultCode {
|
||||
// 基础状态码
|
||||
SUCCESS(200, "操作成功"),
|
||||
FAILED(500, "操作失败"),
|
||||
VALIDATE_FAILED(400, "参数校验失败"),
|
||||
UNAUTHORIZED(401, "未授权"),
|
||||
FORBIDDEN(403, "禁止访问"),
|
||||
NOT_FOUND(404, "资源不存在"),
|
||||
|
||||
// 参数相关错误
|
||||
PARAM_IS_INVALID(1001, "参数无效"),
|
||||
PARAM_IS_BLANK(1002, "参数为空"),
|
||||
PARAM_TYPE_BIND_ERROR(1003, "参数类型错误"),
|
||||
PARAM_NOT_COMPLETE(1004, "参数缺失"),
|
||||
|
||||
// 加密相关错误
|
||||
ENCRYPTION_FAILED(2001, "加密失败"),
|
||||
DECRYPTION_FAILED(2002, "解密失败"),
|
||||
KEY_GENERATION_FAILED(2003, "密钥生成失败"),
|
||||
INVALID_KEY_FORMAT(2004, "密钥格式无效"),
|
||||
|
||||
// 编码相关
|
||||
BASE64_ENCODE_FAILED(4001, "Base64编码失败"),
|
||||
BASE64_DECODE_FAILED(4002, "Base64解码失败"),
|
||||
|
||||
|
||||
|
||||
// ID生成相关
|
||||
ID_GENERATION_FAILED(3001, "ID生成失败"),
|
||||
UUID_GENERATION_FAILED(3002, "UUID生成失败"),
|
||||
SNOWFLAKE_PARAM_INVALID(3003, "雪花ID参数无效"), // 用于workerId/datacenterId越界
|
||||
CLOCK_BACKWARD_ERROR(3004, "系统时钟回拨异常"), // 处理时钟回拨场景
|
||||
SEQUENCE_OVERFLOW(3005, "ID序列号溢出"); // 序列号超过4095时的异常
|
||||
|
||||
|
||||
private final Integer code;
|
||||
private final String msg;
|
||||
|
||||
ResultCode(Integer code, String msg) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.test.bijihoudaun.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOrigins("*")
|
||||
.allowedMethods("*")
|
||||
.allowedHeaders("*")
|
||||
.maxAge(3600); // 预检请求缓存时间
|
||||
;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("/uploads/**")
|
||||
.addResourceLocations("file:uploads/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.test.bijihoudaun.controller;
|
||||
|
||||
|
||||
import com.test.bijihoudaun.entity.Image;
|
||||
import com.test.bijihoudaun.service.ImageService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/images")
|
||||
public class ImageController {
|
||||
|
||||
@Autowired
|
||||
private ImageService imageService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Image> uploadImage(
|
||||
@RequestParam Integer userId,
|
||||
@RequestParam(required = false) Integer markdownId,
|
||||
@RequestParam("file") MultipartFile file) {
|
||||
|
||||
try {
|
||||
Image image = imageService.uploadImage(userId, markdownId, file);
|
||||
return ResponseEntity.ok(image);
|
||||
} catch (IOException e) {
|
||||
return ResponseEntity.status(500).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.test.bijihoudaun.controller;
|
||||
|
||||
import com.test.bijihoudaun.entity.MarkdownFile;
|
||||
import com.test.bijihoudaun.service.MarkdownFileService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/markdown")
|
||||
public class MarkdownController {
|
||||
|
||||
@Autowired
|
||||
private MarkdownFileService markdownFileService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<MarkdownFile> createMarkdown(
|
||||
@RequestParam Integer userId,
|
||||
@RequestParam String title,
|
||||
@RequestParam String fileName,
|
||||
@RequestBody String content) {
|
||||
|
||||
MarkdownFile file = markdownFileService.createMarkdownFile(
|
||||
userId, title, fileName, content);
|
||||
|
||||
return ResponseEntity.ok(file);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<MarkdownFile> updateMarkdown(
|
||||
@PathVariable Integer id,
|
||||
@RequestBody String content) {
|
||||
|
||||
MarkdownFile file = markdownFileService.updateMarkdownContent(id, content);
|
||||
|
||||
if (file != null) {
|
||||
return ResponseEntity.ok(file);
|
||||
}
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.test.bijihoudaun.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("image")
|
||||
public class Image {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Integer id;
|
||||
private Integer userId;
|
||||
private Integer markdownId;
|
||||
|
||||
@TableField("original_name")
|
||||
private String originalName;
|
||||
|
||||
@TableField("stored_name")
|
||||
private String storedName;
|
||||
|
||||
private String url;
|
||||
private Integer size;
|
||||
|
||||
@TableField("content_type")
|
||||
private String contentType;
|
||||
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.test.bijihoudaun.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("markdown_file")
|
||||
public class MarkdownFile {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Integer id;
|
||||
private Integer userId;
|
||||
private String title;
|
||||
|
||||
@TableField("file_name")
|
||||
private String fileName;
|
||||
|
||||
private String content;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.test.bijihoudaun.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("user")
|
||||
public class User {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Integer id;
|
||||
private String username;
|
||||
private String password;
|
||||
private String email;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.test.bijihoudaun.interceptor;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HtmlUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
public class XSSInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
// 清理请求参数中的XSS内容
|
||||
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||
parameterMap.forEach((key, values) -> {
|
||||
String[] newValues = Arrays.stream(values)
|
||||
.map(v -> StrUtil.isBlank(v) ? v : HtmlUtil.filter(v))
|
||||
.toArray(String[]::new);
|
||||
request.setAttribute("filtered_" + key, newValues);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.test.bijihoudaun.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.bijihoudaun.entity.Image;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ImageMapper extends BaseMapper<Image> {
|
||||
// 自定义方法:根据用户ID获取图片列表
|
||||
// List<Image> findByUserId(Integer userId);
|
||||
|
||||
// 自定义方法:根据Markdown文件ID获取关联图片
|
||||
// List<Image> findByMarkdownId(Integer markdownId);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.test.bijihoudaun.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.bijihoudaun.entity.MarkdownFile;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface MarkdownFileMapper extends BaseMapper<MarkdownFile> {
|
||||
// 自定义方法:根据用户ID获取文件列表
|
||||
// List<MarkdownFile> findByUserId(Integer userId);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.test.bijihoudaun.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.bijihoudaun.entity.User;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface UserMapper extends BaseMapper<User> {
|
||||
// 自定义查询方法示例
|
||||
// @Select("SELECT * FROM user WHERE username = #{username}")
|
||||
// User findByUsername(String username);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.test.bijihoudaun.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.test.bijihoudaun.entity.Image;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public interface ImageService extends IService<Image> {
|
||||
/**
|
||||
* 上传图片
|
||||
* @param userId 用户ID
|
||||
* @param markdownId Markdown文件ID(可选)
|
||||
* @param file 图片文件
|
||||
* @return 上传的图片对象
|
||||
* @throws IOException 文件操作异常
|
||||
*/
|
||||
Image uploadImage(Integer userId, Integer markdownId, MultipartFile file) throws IOException;
|
||||
|
||||
/**
|
||||
* 删除图片
|
||||
* @param id 图片ID
|
||||
* @param userId 用户ID(用于权限验证)
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
boolean deleteImage(Integer id, Integer userId);
|
||||
|
||||
/**
|
||||
* 获取用户的图片列表
|
||||
* @param userId 用户ID
|
||||
* @return 图片列表
|
||||
*/
|
||||
List<Image> getUserImages(Integer userId);
|
||||
|
||||
/**
|
||||
* 获取Markdown文件关联的图片
|
||||
* @param markdownId Markdown文件ID
|
||||
* @return 图片列表
|
||||
*/
|
||||
List<Image> getMarkdownImages(Integer markdownId);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.test.bijihoudaun.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.test.bijihoudaun.entity.MarkdownFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface MarkdownFileService extends IService<MarkdownFile> {
|
||||
/**
|
||||
* 创建Markdown文件
|
||||
* @param userId 用户ID
|
||||
* @param title 文件标题
|
||||
* @param fileName 文件名
|
||||
* @param content 文件内容
|
||||
* @return 创建的文件对象
|
||||
*/
|
||||
MarkdownFile createMarkdownFile(Integer userId, String title, String fileName, String content);
|
||||
|
||||
/**
|
||||
* 更新Markdown内容
|
||||
* @param id 文件ID
|
||||
* @param content 新内容
|
||||
* @return 更新后的文件对象
|
||||
*/
|
||||
MarkdownFile updateMarkdownContent(Integer id, String content);
|
||||
|
||||
/**
|
||||
* 获取用户的所有Markdown文件
|
||||
* @param userId 用户ID
|
||||
* @return 文件列表
|
||||
*/
|
||||
List<MarkdownFile> getUserFiles(Integer userId);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.test.bijihoudaun.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.test.bijihoudaun.entity.User;
|
||||
|
||||
public interface UserService extends IService<User> {
|
||||
/**
|
||||
* 用户注册
|
||||
* @param username 用户名
|
||||
* @param password 密码
|
||||
* @param email 邮箱
|
||||
* @return 注册成功的用户
|
||||
*/
|
||||
User register(String username, String password, String email);
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
* @param username 用户名
|
||||
* @param password 密码
|
||||
* @return 登录成功的用户
|
||||
*/
|
||||
User login(String username, String password);
|
||||
|
||||
/**
|
||||
* 用户删除
|
||||
* @param id 用户id
|
||||
*/
|
||||
void deleteUser(Integer id);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.test.bijihoudaun.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.test.bijihoudaun.entity.Image;
|
||||
import com.test.bijihoudaun.mapper.ImageMapper;
|
||||
import com.test.bijihoudaun.service.ImageService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class ImageServiceImpl
|
||||
extends ServiceImpl<ImageMapper, Image>
|
||||
implements ImageService {
|
||||
|
||||
@Value("${file.upload-dir}")
|
||||
private String uploadDir;
|
||||
|
||||
@Override
|
||||
public Image uploadImage(Integer userId, Integer markdownId, MultipartFile file) throws IOException {
|
||||
// 创建上传目录
|
||||
Path uploadPath = Paths.get(uploadDir);
|
||||
if (!Files.exists(uploadPath)) {
|
||||
Files.createDirectories(uploadPath);
|
||||
}
|
||||
|
||||
// 生成唯一文件名
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
|
||||
String storedName = UUID.randomUUID() + extension;
|
||||
|
||||
// 保存文件
|
||||
Path filePath = uploadPath.resolve(storedName);
|
||||
Files.copy(file.getInputStream(), filePath);
|
||||
|
||||
// 创建图片实体
|
||||
Image image = new Image();
|
||||
image.setUserId(userId);
|
||||
image.setMarkdownId(markdownId);
|
||||
image.setOriginalName(originalFilename);
|
||||
image.setStoredName(storedName);
|
||||
image.setUrl("/uploads/" + storedName);
|
||||
image.setSize((int) file.getSize());
|
||||
image.setContentType(file.getContentType());
|
||||
image.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
this.save(image);
|
||||
return image;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteImage(Integer id, Integer userId) {
|
||||
QueryWrapper<Image> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("id", id)
|
||||
.eq("user_id", userId);
|
||||
|
||||
Image image = this.getOne(queryWrapper);
|
||||
if (image == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 删除文件系统中的图片
|
||||
Path filePath = Paths.get(uploadDir, image.getStoredName());
|
||||
Files.deleteIfExists(filePath);
|
||||
|
||||
// 删除数据库记录
|
||||
this.removeById(id);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("删除图片失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Image> getUserImages(Integer userId) {
|
||||
QueryWrapper<Image> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("user_id", userId)
|
||||
.orderByDesc("created_at");
|
||||
return this.list(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Image> getMarkdownImages(Integer markdownId) {
|
||||
QueryWrapper<Image> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("markdown_id", markdownId)
|
||||
.orderByDesc("created_at");
|
||||
return this.list(queryWrapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.test.bijihoudaun.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.test.bijihoudaun.entity.MarkdownFile;
|
||||
import com.test.bijihoudaun.mapper.MarkdownFileMapper;
|
||||
import com.test.bijihoudaun.service.MarkdownFileService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class MarkdownFileServiceImpl
|
||||
extends ServiceImpl<MarkdownFileMapper, MarkdownFile>
|
||||
implements MarkdownFileService {
|
||||
|
||||
@Override
|
||||
public MarkdownFile createMarkdownFile(Integer userId, String title, String fileName, String content) {
|
||||
MarkdownFile file = new MarkdownFile();
|
||||
file.setUserId(userId);
|
||||
file.setTitle(title);
|
||||
file.setFileName(fileName);
|
||||
file.setContent(content);
|
||||
file.setCreatedAt(LocalDateTime.now());
|
||||
file.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
this.save(file);
|
||||
return file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MarkdownFile updateMarkdownContent(Integer id, String content) {
|
||||
MarkdownFile file = this.getById(id);
|
||||
if (file != null) {
|
||||
file.setContent(content);
|
||||
file.setUpdatedAt(LocalDateTime.now());
|
||||
this.updateById(file);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MarkdownFile> getUserFiles(Integer userId) {
|
||||
QueryWrapper<MarkdownFile> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("user_id", userId)
|
||||
.orderByDesc("updated_at");
|
||||
return this.list(queryWrapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.test.bijihoudaun.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.test.bijihoudaun.entity.User;
|
||||
import com.test.bijihoudaun.mapper.UserMapper;
|
||||
import com.test.bijihoudaun.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
|
||||
|
||||
@Autowired
|
||||
private UserMapper userMapper;
|
||||
|
||||
@Override
|
||||
public User register(String username, String password, String email) {
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(User::getUsername, username);
|
||||
if (this.count(queryWrapper) > 0) {
|
||||
return null;
|
||||
}
|
||||
queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(User::getEmail, email);
|
||||
if (this.count(queryWrapper) > 0) {
|
||||
return null;
|
||||
}
|
||||
User user = new User();
|
||||
user.setUsername(username);
|
||||
user.setPassword(password);
|
||||
user.setEmail(email);
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
userMapper.insert(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public User login(String username, String password) {
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(User::getUsername, username)
|
||||
.eq(User::getPassword, password);
|
||||
return userMapper.selectOne(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteUser(Integer id) {
|
||||
userMapper.deleteById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.test.bijihoudaun.util;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 实体复制工具类
|
||||
*/
|
||||
public class EntityCopier {
|
||||
|
||||
/**
|
||||
* 复制实体属性到另一个实体
|
||||
* @param source 源实体
|
||||
* @param target 目标实体
|
||||
* @param <S> 源类型
|
||||
* @param <T> 目标类型
|
||||
*/
|
||||
public static <S, T> void copyEntityToEntity(S source, T target) {
|
||||
BeanUtil.copyProperties(source, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制实体属性到目标类的新实例
|
||||
* @param source 源实体
|
||||
* @param targetClass 目标类
|
||||
* @param <S> 源类型
|
||||
* @param <T> 目标类型
|
||||
* @return 目标类的新实例
|
||||
*/
|
||||
public static <S, T> T copyEntityToClass(S source, Class<T> targetClass) {
|
||||
return BeanUtil.copyProperties(source, targetClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制实体列表到目标类列表
|
||||
* @param sourceList 源列表
|
||||
* @param targetClass 目标类
|
||||
* @param <S> 源类型
|
||||
* @param <T> 目标类型
|
||||
* @return 目标类的新列表
|
||||
*/
|
||||
public static <S, T> List<T> copyList(List<S> sourceList, Class<T> targetClass) {
|
||||
return sourceList.stream()
|
||||
.map(source -> copyEntityToClass(source, targetClass))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.test.bijihoudaun.util;
|
||||
|
||||
public class RandomString {
|
||||
|
||||
// 生成指定长度的随机字符串
|
||||
public static String generateRandomString(int length) {
|
||||
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
int randomIndex = (int) (Math.random() * chars.length());
|
||||
result.append(chars.charAt(randomIndex));
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.test.bijihoudaun.util;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SnowflakeIdGenerator {
|
||||
|
||||
// 起始时间戳(2023-01-01 00:00:00)
|
||||
private static final long EPOCH = 1672531200000L;
|
||||
|
||||
// 机器ID位数
|
||||
private static final long WORKER_ID_BITS = 5L;
|
||||
// 数据中心ID位数
|
||||
private static final long DATACENTER_ID_BITS = 5L;
|
||||
// 最大机器ID (0-31)
|
||||
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
|
||||
// 最大数据中心ID (0-31)
|
||||
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);
|
||||
// 序列号位数
|
||||
private static final long SEQUENCE_BITS = 12L;
|
||||
|
||||
// 机器ID左移位数
|
||||
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
|
||||
// 数据中心ID左移位数
|
||||
private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
|
||||
// 时间戳左移位数
|
||||
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
|
||||
// 序列号掩码(4095)
|
||||
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
|
||||
|
||||
private final long workerId;
|
||||
private final long datacenterId;
|
||||
|
||||
private long sequence = 0L;
|
||||
private long lastTimestamp = -1L;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param workerId 机器ID
|
||||
* @param datacenterId 数据中心ID
|
||||
*/
|
||||
public SnowflakeIdGenerator(
|
||||
@Value("${worker.id:0}") long workerId,
|
||||
@Value("${datacenter.id:0}") long datacenterId) {
|
||||
|
||||
// 校验机器ID范围
|
||||
if (workerId > MAX_WORKER_ID || workerId < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Worker ID 必须在 0 到 %d 之间", MAX_WORKER_ID));
|
||||
}
|
||||
|
||||
// 校验数据中心ID范围
|
||||
if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Datacenter ID 必须在 0 到 %d 之间", MAX_DATACENTER_ID));
|
||||
}
|
||||
|
||||
this.workerId = workerId;
|
||||
this.datacenterId = datacenterId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成雪花ID
|
||||
*
|
||||
* @return 雪花ID
|
||||
*/
|
||||
public synchronized long nextId() {
|
||||
long timestamp = currentTime();
|
||||
|
||||
// 处理时钟回拨
|
||||
if (timestamp < lastTimestamp) {
|
||||
throw new RuntimeException(
|
||||
String.format("时钟回拨拒绝生成ID。当前时间: %d, 最后时间: %d",
|
||||
timestamp, lastTimestamp));
|
||||
}
|
||||
|
||||
// 同一毫秒内生成ID
|
||||
if (lastTimestamp == timestamp) {
|
||||
sequence = (sequence + 1) & SEQUENCE_MASK;
|
||||
// 当前毫秒序列号用完
|
||||
if (sequence == 0) {
|
||||
timestamp = nextMillis(lastTimestamp);
|
||||
}
|
||||
}
|
||||
// 新毫秒重置序列号
|
||||
else {
|
||||
sequence = 0L;
|
||||
}
|
||||
|
||||
lastTimestamp = timestamp;
|
||||
|
||||
// 组合ID各部分
|
||||
return ((timestamp - EPOCH) << TIMESTAMP_SHIFT)
|
||||
| (datacenterId << DATACENTER_ID_SHIFT)
|
||||
| (workerId << WORKER_ID_SHIFT)
|
||||
| sequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成雪花ID,转化成字符串类型
|
||||
*
|
||||
* @return 雪花ID-字符串类型
|
||||
*/
|
||||
public synchronized String nextIdStr() {
|
||||
return Long.toString(nextId());
|
||||
}
|
||||
|
||||
// 等待下一毫秒
|
||||
private long nextMillis(long lastTimestamp) {
|
||||
long timestamp = currentTime();
|
||||
while (timestamp <= lastTimestamp) {
|
||||
timestamp = currentTime();
|
||||
}
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
// 获取当前毫秒时间戳
|
||||
private long currentTime() {
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.test.bijihoudaun.util;
|
||||
|
||||
import com.fasterxml.uuid.Generators;
|
||||
import com.fasterxml.uuid.impl.TimeBasedGenerator;
|
||||
|
||||
/**
|
||||
* uuidV7
|
||||
*/
|
||||
public class UuidV7 {
|
||||
/**
|
||||
* 生成uuidV7,生成的uuid有-
|
||||
*/
|
||||
public static String uuid() {
|
||||
TimeBasedGenerator generator = Generators.timeBasedGenerator();
|
||||
return generator.generate().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取uuidV7,生成的uuid无-
|
||||
*/
|
||||
public static String uuidNoHyphen() {
|
||||
return uuid().replace("-", "");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -7,6 +7,10 @@ spring:
|
||||
multipart:
|
||||
max-file-size: 10MB # ???????5MB
|
||||
max-request-size: 10MB # ???????5MB
|
||||
file:
|
||||
upload-dir: uploads
|
||||
|
||||
|
||||
#??
|
||||
server:
|
||||
port: 8083
|
||||
@@ -19,3 +23,9 @@ worker:
|
||||
# ????ID (0~31)
|
||||
datacenter:
|
||||
id: 1
|
||||
|
||||
# MyBatis-Plus??
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath:mapper/*.xml
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
|
||||
BIN
mydatabase.db
BIN
mydatabase.db
Binary file not shown.
36
sql/data.sql
Normal file
36
sql/data.sql
Normal file
@@ -0,0 +1,36 @@
|
||||
-- 用户表
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL,
|
||||
email TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Markdown文件表
|
||||
CREATE TABLE IF NOT EXISTS markdown_file (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
file_name TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 图片表
|
||||
CREATE TABLE IF NOT EXISTS image (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
markdown_id INTEGER,
|
||||
original_name TEXT NOT NULL,
|
||||
stored_name TEXT NOT NULL UNIQUE,
|
||||
url TEXT NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
content_type TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (markdown_id) REFERENCES markdown_file(id) ON DELETE SET NULL
|
||||
);
|
||||
Reference in New Issue
Block a user