云图库笔记

这里记录下云图库项目的笔记,方便自己日后回顾

项目初始化

后端通用代码

  1. 异常枚举类、自定义业务异常类(继承RuntimeException)
  2. 封装包装类、封装返回类
  3. 全局异常处理器(使用@RestControllerAdvice和@ExceptionHandler注解)
  4. 请求包装类
  5. 全局跨域配置

前端项目初始化

1.使用Vue3(组合式API)

用户模块

用户注册

代码实现

  1. 新建注册请求对象
  2. 先通过用户名判断数据库中有没有重名的,然后对密码进行md5加密

用户登录

流程图

  1. 新建登录请求对象
  2. 校验参数等操作,将用户信息保存到表中
  3. 将用户信息保存到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);

获取当前登录用户信息

  1. 因为登录成功后已经保存在session里面了,所以直接从session里面取就好了,即request.getSession().getAttribute(USER_LOGIN_STATE);
  2. 返回时注意用户脱敏,可以新建一个UserVo类。

用户注销

  1. 用户注销,即退出登录,直接从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切面 + 自定义权限校验注解

  1. 定义注解
java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthCheck {
    /**
     * 必须有一个角色
     */
    String mustRole() default "";
}
  1. 利用切面(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();
    }
}
  1. 在目标方法上应用注解
java 复制代码
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
  1. 在启动类上添加 @EnableAspectJAutoProxy 注解
java 复制代码
@EnableAspectJAutoProxy(exposeProxy = true)

用户管理

  1. 新增
  2. 修改
  3. 更新
  4. 查询
  5. 分页查询
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;
    }
}

图片模块

表结构设计

  1. 字段设计
  • 基础字段:url、分类、标签、简介...
  • 属性字段:长度、宽度、宽高比、格式...
  • 索引:名称、简介、分类、标签、用户id...,方便模糊查询

为什么要用对象存储

简单的方式就是将文件上传到后端项目所在的服务器,但是有以下缺点:

  • 不利于扩展:毕竟服务器存储有限
  • 不利于迁移:若后端项目要换服务器,则对应的图片文件也要迁移
  • 不够安全:
  • 不利于管理:

后端操作对象存储大体流程

  1. 导入依赖
  2. 定义好配置文件CosClientConfig、CosManager
  3. 定义FileController类和文件上传下载方法

图片上传下载

  1. 定义图片实体、图片请求类和响应类

  2. 定义FileManager类,该类是CosManager类的完善版,添加了参数校验等信息

  3. 定义上传图片方法

    • 先校验图片是否满足条件
    • 在调用cos的图片上传方法
    • 上传成功后得到图片信息并保存到数据库中
    • 不论上传失败还是成功都要删除临时文件

图片管理

  1. 【管理员】根据id删除图片

  2. 【管理员】更新图片

  3. 【管理员】分页获取图片列表(不需要脱敏和限制条数)

    先查出所有对应的用户列表,再遍历图片列表并赋值对应的用户信息

    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));
    }};
  4. 【管理员】根据id获取图片(不需要脱敏)

  5. 分页获取图片列表(需要脱敏和限制条数,并做了防爬虫处理)

  6. 根据id获取图片(需要脱敏)

  7. 修改图片

用户传图

通过url导入图片

  1. 下载图片:使用Hutool中的HttpUtil.downloadFile
  2. 校验图片: 格式、大小、
  3. 上传图片:
    在该模块中使用了模板方法设计模式

批量抓取和获取图片

两种获取图片的方式

  • 请求到完整的页面内容后,对HTML结构进行解析(Jsoup),获取图片路径,在进行下载
  • 得到获取图片数据的接口
    怎么判断路径中需要带哪些参数呢?
    查看对应的路径中有哪些参数,逐步删掉用不到的,如果不带哪个参数报错了,那这个参数就是必须的。

图片优化部分

放入缓存(读多写少的场景)

图片查询

缓存中value值存的为一个Page对象,因为不用缓存的时候存的就是一个Page对象,存Page对象前端可以直接用。

  1. 本地缓存-Caffeine

    比分布式缓存速度更快,但是无法在多个服务器上共享。

    适用场景

    • 小型数据集
    • 不需要服务器间共享
    • 高频、低延迟场景
  2. 分布式存储-redis

    分布式存储是指将数据分布存储在多台服务器上

    redis存储优点:

    • 高性能:因为基于缓存
    • 丰富的数据结构: 支持字符串、列表、集合、哈希
    • 分布式支持:

    查询条件key的设置:将查询条件设为key,但是直接将查询条件当做key太长,可以将其转化为JSON然后使用MD5压缩算法。

    value的设置:1.转化为json字符串2.转化为二进制或其他存储

    过期时间设置:设置为5~60分钟即可

  3. 多级存储

  • 本地缓存+分布式缓存

图片上传

  1. 压缩图片格式
  • 转化为webp的格式
  1. 使用数据万象服务的压缩方案
  2. 文件秒传
  • 客户端生成文件唯一标识,
  • 服务端校验文件指纹,若存在则直接返回文件的存储路径,否则接受并存储新文件并建立文件指纹
  1. 断点续传
  • 使用数据万象的服务

图片加载

  1. 上传图片时同时生成一份缩略图,前端展示时展示缩略图
  2. 前端懒加载
  3. 使用渐进式加载:展示缩略图,当用户点击图片时再去加载清晰图(Ant Design Vue有组件支持)
  4. CDN:CDN(内容分发网络)是通过将图片文件分发到全球各地的节,点,用户访问时从离自己最近的节点获取资源的技
    术,常用于文件资源或后端动态请求的网络加速,也能大幅分摊源站的压力、支持更多请求同时访问,是性能提
    升的利器。

图片存储

  1. 数据降沉:长时间未访问的数据自动迁移到低频访问存储。
  2. 清理策略:
  • 立即清理:
  • 手动清理:由管理员触发清除动作
  • 定期清理:通过定时任务自动触发清理任务
  • 惰性清理: 只有当资源需求增加(例如存储空间不足)时采取触发清理动作。

空间模块

  1. 创建空间时加锁

图片功能扩展

以图搜图

使用Jsoup,先获取页面内容,在获取图片列表,在将图片列表转化为图片类。

颜色搜索

  1. 提取图片颜色:使用opencv图片处理技术得到主色调
  2. 保存图像特征:将主色调保存到数据库中
  3. 用户查询:用户输入要查询颜色的主色调
  4. 计算相似度:计算数据库中颜色与用户输入颜色的相似度,使用欧几里得距离算法分析
  5. 返回结果

开发过程中使用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的多账号认证

怎么判断某空间用户有哪些权限。

  1. 先定义一共有哪些权限和每个角色拥有哪些权限,为方便存取,这里用permission.json文件存储的
  2. 定义空间用户-权限配置类,改类里面有List permission和List roles 两个属性,分别表示权限列表和角色列表
  3. 定义权限类和角色类,权限类里面有权限键、权限值、权限描述等三个字段;角色类中有角色键、角色值、角色所拥有的权限、描述四个字段
  4. 定义权限枚举类,方便后续校验使用
  5. 定义空间用户权限管理类,在这里类中读取permission.json文件并定义根据空间用户角色获取对应权限的方法。

怎么判断用户访问的是公共图库、私有空间还是公共空间呢

通过获取请求路径中的关键词,例如访问公共图库时路径中有picture、访问私有空间时有spaceUser关键字、访问公共空间时带有space关键字。

但是问题是HttpServlet的body是一个流,只支持读取一次,为解决这个问题定义了请求包装类和请求包装类过滤器。

定义权限校验注解

定义完注解后,对于必须要登录才能使用的功能,可以添加权限校验注解,对于一下不登录就可以使用的功能,例如公共图库查看,可以使用编程式权限校验,在代码中控制。

分库分表

静态分表

在设计阶段,分库的规则和数量就已经固定了,不会根据业务的增长动态调整

动态分表

可以根据业务需求或数量动态增加,表的结构和规则是运行时动态生成的,需要编写分表规则。

协同操作

WebSocket

  1. 先编写拦截器,需要继承HandShakeInterceptor类
  2. 编写处理器:在连接成功、连接关闭、接收到客户端消息时进行相应的处理。
  • 需要维护两个字段,分别为:

    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<>();
  • 编写广播方法

  • 编写连接建立后执行的方法,包括进入编辑、正在编辑、离开遍历的方法

  1. 编写断开连接方法
相关推荐
航Hang*几秒前
第七章:综合布线技术 —— 设备间子系统的设计与施工
网络·笔记·学习·期末·复习
航Hang*18 分钟前
第六章:综合布线技术 —— 干线子系统的设计与施工
网络·笔记·学习·期末·复习
不吃茄子啦23 分钟前
天干地支对应五行与赤马红羊
笔记
d111111111d27 分钟前
STM32 HAL库定时器PWM输出全攻略:从零到精准控制
笔记·stm32·单片机·嵌入式硬件·学习
三块可乐两块冰30 分钟前
【第二十六周】机器学习笔记二十五
人工智能·笔记·机器学习
扑火的小飞蛾34 分钟前
【macOS】n8n 安装配置笔记
笔记·macos
hssfscv36 分钟前
Javaweb学习笔记——JDBC和Mybatis
笔记·学习·mybatis
羊小猪~~40 分钟前
数据库学习笔记(十八)--事务
数据库·笔记·后端·sql·学习·mysql
W|J1 小时前
ES 学习笔记
笔记·学习·elasticsearch
张人玉1 小时前
西门子 S7 PLC 通信 WPF 应用分析笔记
笔记·c#·wpf·plc