这里记录下云图库项目的笔记,方便自己日后回顾
项目初始化
后端通用代码
- 异常枚举类、自定义业务异常类(继承RuntimeException)
- 封装包装类、封装返回类
- 全局异常处理器(使用@RestControllerAdvice和@ExceptionHandler注解)
- 请求包装类
- 全局跨域配置
前端项目初始化
1.使用Vue3(组合式API)
用户模块
用户注册
代码实现
- 新建注册请求对象
- 先通过用户名判断数据库中有没有重名的,然后对密码进行md5加密
用户登录
流程图

- 新建登录请求对象
- 校验参数等操作,将用户信息保存到表中
- 将用户信息保存到session中
java
String encryptPassword = getEncryptPassword(userPassword);
QueryWrapper<User> userQueryWrapper = new QueryWrapper<User>()
.eq("userAccount", userAccount)
.eq("userPassword", encryptPassword);
User user = this.baseMapper.selectOne(userQueryWrapper);
// 3.若账号不存在
if (user == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号不存在");
}
// 4. 账号脱敏
LoginUserVO loginUserVO = getLoginUserVO(user);
// 5. 记录用户登录态
request.getSession().setAttribute(USER_LOGIN_STATE, user);
获取当前登录用户信息
- 因为登录成功后已经保存在session里面了,所以直接从session里面取就好了,即request.getSession().getAttribute(USER_LOGIN_STATE);
- 返回时注意用户脱敏,可以新建一个UserVo类。
用户注销
- 用户注销,即退出登录,直接从session中把当前登录用户的信息删除掉即可
java
public boolean userLogout(HttpServletRequest request) {
if (request == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求为空");
}
// 判断是否登录
Object attribute = request.getSession().getAttribute(USER_LOGIN_STATE);
if (attribute != null) {
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, "用户未登录");
}
// 移除登录态
request.getSession().removeAttribute(USER_LOGIN_STATE);
return true;
}
用户权限控制
思路:使用 spring AOP切面 + 自定义权限校验注解
- 定义注解
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthCheck {
/**
* 必须有一个角色
*/
String mustRole() default "";
}
- 利用切面(AOP)处理注解
java
@Aspect
@Component
public class AuthInterceptor {
@Autowired
private UserService userService;
// 在这里编写拦截器逻辑
// 可以使用 @Before、@After、@Around 等注解来定义拦截点和处理逻辑
@Around("@annotation(authCheck)")
public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {
String role = authCheck.mustRole();
// 获取当前登录用户
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
User loginUser = userService.getLoginUser(request);
UserRoleEnum.getUserRoleEnum(role);
// 不需要权限,放行
if (role == null) {
return joinPoint.proceed();
}
// 获取当前登录用户的权限
UserRoleEnum userRoleEnum = UserRoleEnum.getUserRoleEnum(loginUser.getUserRole());
// 权限不足,抛出异常
if (userRoleEnum == null) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
// 要求必须是管理员权限,但是当前用户不是管理员,抛出异常
if (UserRoleEnum.ADMIN.equals(role) && !UserRoleEnum.ADMIN.equals(userRoleEnum)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
return joinPoint.proceed();
}
}
- 在目标方法上应用注解
java
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
- 在启动类上添加 @EnableAspectJAutoProxy 注解
java
@EnableAspectJAutoProxy(exposeProxy = true)
用户管理
- 新增
- 修改
- 更新
- 查询
- 分页查询
java
// 传入一个page对象和一个query对象
Page<User> userPage = userService.page(new Page<>(current, pageSize), userService.getQueryWrapper(userQueryRequest));
- 分页查询问题:若后端id字段范围过大,则前端精度丢失,因为前端js的精度范围有限,为解决这个问题在后端使用全局JSON配置,在后端向前端传值时,将Long型转化为字符串
java
@JsonComponent
public class JsonConfig {
/**
* 添加 Long 转 json 精度丢失的配置
*/
@Bean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
SimpleModule module = new SimpleModule();
module.addSerializer(Long.class, ToStringSerializer.instance);
module.addSerializer(Long.TYPE, ToStringSerializer.instance);
objectMapper.registerModule(module);
return objectMapper;
}
}
图片模块
表结构设计
- 字段设计
- 基础字段:url、分类、标签、简介...
- 属性字段:长度、宽度、宽高比、格式...
- 索引:名称、简介、分类、标签、用户id...,方便模糊查询
为什么要用对象存储
简单的方式就是将文件上传到后端项目所在的服务器,但是有以下缺点:
- 不利于扩展:毕竟服务器存储有限
- 不利于迁移:若后端项目要换服务器,则对应的图片文件也要迁移
- 不够安全:
- 不利于管理:
后端操作对象存储大体流程
- 导入依赖
- 定义好配置文件CosClientConfig、CosManager
- 定义FileController类和文件上传下载方法
图片上传下载
-
定义图片实体、图片请求类和响应类
-
定义FileManager类,该类是CosManager类的完善版,添加了参数校验等信息
-
定义上传图片方法
- 先校验图片是否满足条件
- 在调用cos的图片上传方法
- 上传成功后得到图片信息并保存到数据库中
- 不论上传失败还是成功都要删除临时文件
图片管理
-
【管理员】根据id删除图片
-
【管理员】更新图片
-
【管理员】分页获取图片列表(不需要脱敏和限制条数)
先查出所有对应的用户列表,再遍历图片列表并赋值对应的用户信息
java//1.关联查询用户信息 Set<Long> useridset = picturelist.stream().map(Picture::getUserid).collect(Collectors.toSet()); Map<Long, List<User>> userIdUserListMap = userServicte.listByIds(useridset).stream( .collect(Collectors.groupingBy(User: getid)); // 2.填充信息 pictureVOList.forEach(pictureVO ->{ Long userid = pictureVO.getUserId(); User user = null; if(userIdUserListMap.containsKey(userId)) { user = userIdUserListMap.get(userId).get(0); pictureVO.setUser(userService.getUserVO(user)); }}; -
【管理员】根据id获取图片(不需要脱敏)
-
分页获取图片列表(需要脱敏和限制条数,并做了防爬虫处理)
-
根据id获取图片(需要脱敏)
-
修改图片
用户传图
通过url导入图片
- 下载图片:使用Hutool中的HttpUtil.downloadFile
- 校验图片: 格式、大小、
- 上传图片:
在该模块中使用了模板方法设计模式
批量抓取和获取图片
两种获取图片的方式
- 请求到完整的页面内容后,对HTML结构进行解析(Jsoup),获取图片路径,在进行下载
- 得到获取图片数据的接口
怎么判断路径中需要带哪些参数呢?
查看对应的路径中有哪些参数,逐步删掉用不到的,如果不带哪个参数报错了,那这个参数就是必须的。
图片优化部分
放入缓存(读多写少的场景)
图片查询
缓存中value值存的为一个Page对象,因为不用缓存的时候存的就是一个Page对象,存Page对象前端可以直接用。
-
本地缓存-Caffeine
比分布式缓存速度更快,但是无法在多个服务器上共享。
适用场景
- 小型数据集
- 不需要服务器间共享
- 高频、低延迟场景
-
分布式存储-redis
分布式存储是指将数据分布存储在多台服务器上
redis存储优点:
- 高性能:因为基于缓存
- 丰富的数据结构: 支持字符串、列表、集合、哈希
- 分布式支持:
查询条件key的设置:将查询条件设为key,但是直接将查询条件当做key太长,可以将其转化为JSON然后使用MD5压缩算法。
value的设置:1.转化为json字符串2.转化为二进制或其他存储
过期时间设置:设置为5~60分钟即可
-
多级存储
- 本地缓存+分布式缓存
图片上传
- 压缩图片格式
- 转化为webp的格式
- 使用数据万象服务的压缩方案
- 文件秒传
- 客户端生成文件唯一标识,
- 服务端校验文件指纹,若存在则直接返回文件的存储路径,否则接受并存储新文件并建立文件指纹
- 断点续传
- 使用数据万象的服务
图片加载
- 上传图片时同时生成一份缩略图,前端展示时展示缩略图
- 前端懒加载
- 使用渐进式加载:展示缩略图,当用户点击图片时再去加载清晰图(Ant Design Vue有组件支持)
- CDN:CDN(内容分发网络)是通过将图片文件分发到全球各地的节,点,用户访问时从离自己最近的节点获取资源的技
术,常用于文件资源或后端动态请求的网络加速,也能大幅分摊源站的压力、支持更多请求同时访问,是性能提
升的利器。
图片存储
- 数据降沉:长时间未访问的数据自动迁移到低频访问存储。
- 清理策略:
- 立即清理:
- 手动清理:由管理员触发清除动作
- 定期清理:通过定时任务自动触发清理任务
- 惰性清理: 只有当资源需求增加(例如存储空间不足)时采取触发清理动作。
空间模块
- 创建空间时加锁
图片功能扩展
以图搜图
使用Jsoup,先获取页面内容,在获取图片列表,在将图片列表转化为图片类。
颜色搜索
- 提取图片颜色:使用opencv图片处理技术得到主色调
- 保存图像特征:将主色调保存到数据库中
- 用户查询:用户输入要查询颜色的主色调
- 计算相似度:计算数据库中颜色与用户输入颜色的相似度,使用欧几里得距离算法分析
- 返回结果
开发过程中使用Color类和前端的vue3-colorpicker组件来实现
图片分享
使用Ant Design Vue的组件Modal和QRCode组件
图片批处理
若数量太大,可以使用线程池+分批+并发
java
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (int i = 0; i < pictureList.size(); i += batchSize) {
List<Picture> batch = pictureList.subList(i, Math.min(i + batchSize, pictureList.size()));
// 异步处理每批数据
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
batch.forEach(picture -> {
// 编辑分类和标签
if (request.getCategory() != null) {
picture.setCategory(request.getCategory());
}
if (request.getTags() != null) {
picture.setTags(String.join(",", request.getTags()));
}
});
boolean result = this.updateBatchById(batch);
if (!result) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "批量更新图片失败");
}
}, customExecutor);
futures.add(future);
}
AI图片编辑
使用VueCropper组件,上传修改后的图片时前端将blob对象转化为File对象,然后调用图片上传方法。
团队空间
RBAC权限控制
Sa-Token
Sa-Token默认将数据保存在内存中,避免了序列化和反序列化带来的性能消耗,但是缺点是服务重启后数据会丢失且无法下分布式环境下共享数据。
解决方法:让Sa-Token整合redis并使用jackson序列化
sa-token的多账号认证
怎么判断某空间用户有哪些权限。
- 先定义一共有哪些权限和每个角色拥有哪些权限,为方便存取,这里用permission.json文件存储的
- 定义空间用户-权限配置类,改类里面有List permission和List roles 两个属性,分别表示权限列表和角色列表
- 定义权限类和角色类,权限类里面有权限键、权限值、权限描述等三个字段;角色类中有角色键、角色值、角色所拥有的权限、描述四个字段
- 定义权限枚举类,方便后续校验使用
- 定义空间用户权限管理类,在这里类中读取permission.json文件并定义根据空间用户角色获取对应权限的方法。
怎么判断用户访问的是公共图库、私有空间还是公共空间呢
通过获取请求路径中的关键词,例如访问公共图库时路径中有picture、访问私有空间时有spaceUser关键字、访问公共空间时带有space关键字。
但是问题是HttpServlet的body是一个流,只支持读取一次,为解决这个问题定义了请求包装类和请求包装类过滤器。
定义权限校验注解
定义完注解后,对于必须要登录才能使用的功能,可以添加权限校验注解,对于一下不登录就可以使用的功能,例如公共图库查看,可以使用编程式权限校验,在代码中控制。
分库分表
静态分表
在设计阶段,分库的规则和数量就已经固定了,不会根据业务的增长动态调整
动态分表
可以根据业务需求或数量动态增加,表的结构和规则是运行时动态生成的,需要编写分表规则。
协同操作
WebSocket
- 先编写拦截器,需要继承HandShakeInterceptor类
- 编写处理器:在连接成功、连接关闭、接收到客户端消息时进行相应的处理。
-
需要维护两个字段,分别为:
java// 每张图片的编辑状态,key: pictureId, value: 当前正在编辑的用户 ID private final Map<Long, Long> pictureEditingUsers = new ConcurrentHashMap<>(); // 保存所有连接的会话,key: pictureId, value: 用户会话集合 private final Map<Long, Set<WebSocketSession>> pictureSessions = new ConcurrentHashMap<>(); -
编写广播方法
-
编写连接建立后执行的方法,包括进入编辑、正在编辑、离开遍历的方法
- 编写断开连接方法