feat(biji-houdaun): 实现用户注册、登录、 Markdown 文件和图片上传功能

- 新增用户注册、登录接口及服务实现
- 添加 Markdown 文件创建、更新接口及服务实现
- 实现图片上传、获取接口及服务实现
- 集成 Snowflake ID 生成器
- 添加全局异常处理和统一返回结果封装
- 配置跨域访问和静态资源处理
- 实现基础的 XSS 防护
This commit is contained in:
ikmkj
2025-06-16 20:20:08 +08:00
parent 8a4bf2d245
commit 1ef2e116a6
27 changed files with 1049 additions and 0 deletions

View File

@@ -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(), "系统繁忙,请稍后再试");
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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/");
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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("-", "");
}
}

View File

@@ -7,6 +7,10 @@ spring:
multipart: multipart:
max-file-size: 10MB # ???????5MB max-file-size: 10MB # ???????5MB
max-request-size: 10MB # ???????5MB max-request-size: 10MB # ???????5MB
file:
upload-dir: uploads
#?? #??
server: server:
port: 8083 port: 8083
@@ -19,3 +23,9 @@ worker:
# ????ID (0~31) # ????ID (0~31)
datacenter: datacenter:
id: 1 id: 1
# MyBatis-Plus??
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true

Binary file not shown.

36
sql/data.sql Normal file
View 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
);