2025-06-06 19:28:06 +08:00
|
|
|
|
# 智能协同云图库
|
|
|
|
|
|
2025-06-16 19:37:23 +08:00
|
|
|
|
## 待完善功能:
|
|
|
|
|
|
2025-06-06 19:28:06 +08:00
|
|
|
|
用户模块扩展功能:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-06-16 19:37:23 +08:00
|
|
|
|
2.JWT校验,可能要同时改前端,把userId保存到ThreadLocal中
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3.目前这些标签写死了,可以用redis、数据库进行动态设置。(根据点击次数)
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
@GetMapping("/tag_category")
|
|
|
|
|
public BaseResponse<PictureTagCategory> listPictureTagCategory() {
|
|
|
|
|
PictureTagCategory pictureTagCategory = new PictureTagCategory();
|
|
|
|
|
List<String> tagList = Arrays.asList("热门", "搞笑", "生活", "高清", "艺术", "校园", "背景", "简历", "创意");
|
|
|
|
|
List<String> categoryList = Arrays.asList("模板", "电商", "表情包", "素材", "海报");
|
|
|
|
|
pictureTagCategory.setTagList(tagList);
|
|
|
|
|
pictureTagCategory.setCategoryList(categoryList);
|
|
|
|
|
return ResultUtils.success(pictureTagCategory);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4.图片审核扩展
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5.爬图扩展
|
|
|
|
|
|
|
|
|
|
2)记录从哪里爬的
|
|
|
|
|
|
|
|
|
|
4)bing直接搜可能也是缩略图,可能模拟手点一次图片,再爬会清晰一点
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
6.缓存扩展
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
图片压缩
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
文件秒传,md5校验,如果已有,直接返回url,不用重新上传(图片场景不必使用)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
分片上传和断点续传:[对象存储 上传对象_腾讯云](https://cloud.tencent.com/document/product/436/65935#0c1fbdc5-64c1-4224-9aa5-92fbd0ae6780)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CDN内容分发,后期项目上线之后搞一下。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
浏览器缓存
|
|
|
|
|
|
|
|
|
|
是服务器(或 CDN/静态文件服务器)在返回资源时下发给浏览器的。
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
用户空间扩展:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
2025-06-19 19:56:47 +08:00
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
图片编辑
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
AI扩图
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-06-16 19:37:23 +08:00
|
|
|
|
|
|
|
|
|
|
2025-06-06 19:28:06 +08:00
|
|
|
|
下载MybatisX插件,可以从数据表直接生成Bean、Mapper、Service,选项设置如下:
|
|
|
|
|
|
|
|
|
|
注意,勾选Actual Column生成的Bean和表中字段一模一样,取消勾选会进行驼峰转换,即user_name->userName
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
下载GenerateSerailVersionUID插件,可以右键->generate->生成序列ID:
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
private static final long serialVersionUID = -1321880859645675653L;
|
|
|
|
|
```
|
|
|
|
|
|
2025-06-11 10:48:37 +08:00
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-06-16 19:37:23 +08:00
|
|
|
|
**创建图片的业务流程**
|
2025-06-11 10:48:37 +08:00
|
|
|
|
创建图片主要是包括两个过程:第一个过程是上传图片文件本身,第二个过程是将图片信息上传到数据库。
|
|
|
|
|
|
|
|
|
|
有两种常见的处理方式:
|
|
|
|
|
|
2025-06-16 19:37:23 +08:00
|
|
|
|
1.先上传再提交数据(大多数的处理方式):用户直接上传图片,系统自动生成图片的**url存储地址**;然后在用户填写其它相关信息并提交后才将图片记录保存到数据库中。
|
|
|
|
|
2.上传图片时直接记录图片信息:云图库平台中图片作为核心资源,只要用户将图片上传成功就应该把这个图片上传到数据库中(即用户上传图片后系统应该立即生成图片的完整数据记录和其它元信息,这里元信息指的是图片的一些基础信息,这些信息应该是在图片上传成功后就能够解析出来),无需等待用户上传提交图片信息就会立即存入数据库中,这样会使整个交互过程更加轻量。这样的话用户只需要再上传图片的其它信息即可,这样就相当于用户对已有的图片信息进行编辑。
|
2025-06-11 10:48:37 +08:00
|
|
|
|
当然我们也可以对用户进行一些限制,比如说当用户上传过多的图片资源时就禁止该用户继续上传图片资源。
|
|
|
|
|
|
|
|
|
|
|
2025-06-16 19:37:23 +08:00
|
|
|
|
|
|
|
|
|
## 优化
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-06-20 11:57:50 +08:00
|
|
|
|
## 收获
|
2025-06-16 19:37:23 +08:00
|
|
|
|
|
2025-06-20 11:57:50 +08:00
|
|
|
|
### 胡图工具类hutool
|
2025-06-16 19:37:23 +08:00
|
|
|
|
|
2025-06-20 11:57:50 +08:00
|
|
|
|
`ObjUtil.isNotNull(Object obj)`,仅判断对象是否 **不为 `null`**,不关心对象内容是否为空,比如空字符串 `""`、空集合 `[]`、数字 `0` 等都算是“非 null”。
|
2025-06-19 19:56:47 +08:00
|
|
|
|
|
2025-06-20 11:57:50 +08:00
|
|
|
|
`ObjUtil.isNotEmpty(Object obj)` 判断对象是否 **不为 null 且非“空”**
|
2025-06-19 19:56:47 +08:00
|
|
|
|
|
2025-06-20 11:57:50 +08:00
|
|
|
|
- 对不同类型的对象判断逻辑不同:
|
|
|
|
|
- `CharSequence`(String):长度大于 0
|
|
|
|
|
- `Collection`:size > 0
|
|
|
|
|
- `Map`:非空
|
|
|
|
|
- `Array`:长度 > 0
|
|
|
|
|
- 其它对象:只判断是否为 null(默认不认为“空”)
|
2025-06-19 19:56:47 +08:00
|
|
|
|
|
2025-06-16 19:37:23 +08:00
|
|
|
|
|
|
|
|
|
|
2025-06-20 11:57:50 +08:00
|
|
|
|
`StrUtil.isNotEmpty(String str) `只要不是 `null` 且长度大于 0 就算“非空”。
|
|
|
|
|
|
|
|
|
|
`StrUtil.isNotBlank(String str)` 不仅要非 `null`,还要**不能只包含空格、换行、Tab 等空白字符**
|
|
|
|
|
|
|
|
|
|
`StrUtil.hasBlank(CharSequence... strs)`只要 **至少一个字符串是 blank(空或纯空格)**就返回 `true`,底层其实就是对每个参数调用 `StrUtil.isBlank(...)`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
`CollUtil.isNotEmpty(Collection<?> coll)`用于判断 **集合(Collection)是否非空**,功能类似于 `ObjUtil.isNotEmpty(...)`
|
|
|
|
|
|
|
|
|
|
### 多级缓存
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
2025-06-16 19:37:23 +08:00
|
|
|
|
|
|
|
|
|
### Redis+Session
|
|
|
|
|
|
|
|
|
|
之前我们每次重启服务器都要重新登陆,既然已经整合了 `Redis`,不妨使用 `Redis` 管理` Session`,更好地维护登录态。
|
|
|
|
|
|
|
|
|
|
1)先在 `Maven `中引入 `spring-session-data-redis` 库:
|
|
|
|
|
|
|
|
|
|
```xml
|
|
|
|
|
<!-- Spring Session + Redis -->
|
|
|
|
|
<dependency>
|
|
|
|
|
<groupId>org.springframework.session</groupId>
|
|
|
|
|
<artifactId>spring-session-data-redis</artifactId>
|
|
|
|
|
</dependency>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
2)修改 `application.yml` 配置文件,更改`Session`的存储方式和过期时间:
|
|
|
|
|
|
|
|
|
|
既要设置redis能存30天,发给前端的cookie也要30天有效期。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
spring:
|
|
|
|
|
# session 配置
|
|
|
|
|
session:
|
|
|
|
|
store-type: redis
|
|
|
|
|
# session 30 天过期
|
|
|
|
|
timeout: 2592000
|
|
|
|
|
server:
|
|
|
|
|
port: 8123
|
|
|
|
|
servlet:
|
|
|
|
|
context-path: /api
|
|
|
|
|
# cookie 30 天过期
|
|
|
|
|
session:
|
|
|
|
|
cookie:
|
|
|
|
|
max-age: 2592000
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 为什么用 `ConcurrentHashMap<Long,Object>` 管理锁更优?
|
|
|
|
|
|
|
|
|
|
1. **避免污染常量池**
|
|
|
|
|
`String.intern()` 会把每一个不同的 `userId` 字符串都放到 JVM 的字符串常量池里,随着用户量增长,常量池里的内容会越来越多,可能导致元空间(MetaSpace)/永久代(PermGen)压力过大。
|
|
|
|
|
2. **显式可控的锁生命周期**
|
|
|
|
|
- 用 `ConcurrentHashMap` 明确地管理——「只要 map 里有这个 key,就有对应的锁对象;不需要时可以删掉。」
|
|
|
|
|
- 相比之下,`intern()` 后的字符串对象由 JVM 常量池管理,代码里很难清理,存在内存泄漏风险。
|
|
|
|
|
3. **高并发性能更好**
|
|
|
|
|
- `ConcurrentHashMap` 内部采用分段锁或 Node 锁定(取决于 JDK 版本),即便高并发下往 map 里 `computeIfAbsent` 也能保持较高吞吐。
|
|
|
|
|
- `synchronized (lock)` 本身只锁定单个用户对应的那把锁,不影响其他用户;结合 `ConcurrentHashMap` 的高并发特性,整体性能比直接在一个全局 `HashMap` + `synchronized` 好得多。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 锁+事务可能出现的问题
|
|
|
|
|
|
|
|
|
|
**`@Transactional`(声明式)**
|
|
|
|
|
|
|
|
|
|
- 事务在方法入口打开,很可能在拿锁前就占用连接/数据库资源,导致“空跑事务”+“资源耗尽”。
|
|
|
|
|
- 依赖代理,存在自调用失效的坑。
|
|
|
|
|
|
|
|
|
|
**`transactionTemplate.execute()`(编程式)**
|
|
|
|
|
|
|
|
|
|
- 锁先行→事务后发,确保高并发下只有一个连接/事务进数据库,极大降低资源竞争。
|
|
|
|
|
- 全程显式,放到哪儿就是哪儿,杜绝自调用/代理链带来的隐患。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**锁+事务`@Transactional`一起可能出现问题:**
|
|
|
|
|
|
|
|
|
|
**线程 A**
|
|
|
|
|
|
|
|
|
|
- 进入方法,Spring AOP 拦截,**立即开启事务**
|
|
|
|
|
- 走到 `synchronized(lock)`,拿到锁
|
|
|
|
|
- 在锁里执行 `exists` → `save`(但真正的 “提交” 要等到方法返回后才做)
|
|
|
|
|
- 退出 `synchronized` 块,方法继续执行(其实已经没别的逻辑了)
|
|
|
|
|
- 方法返回,事务拦截器这时才 **提交**
|
|
|
|
|
|
|
|
|
|
**线程 B**(并发进来)
|
|
|
|
|
|
|
|
|
|
- 等待 AOP 代理,进入同一个方法,**也会马上开启自己的事务**
|
|
|
|
|
- 在入口就拿到一个新的连接/事务上下文
|
|
|
|
|
- 然后遇到 `synchronized(lock)`,**在这里阻塞** 等 A 释放锁
|
|
|
|
|
- A 一旦走出 `synchronized`,B 立刻拿到锁——但此时 A **还没真正提交**(提交在方法尾被拦截器做)
|
|
|
|
|
- B 在锁里执行 `exists`:因为 A 的改动还在 A 的未提交事务里,**默认隔离级别(READ_COMMITTED)下看不到**,所以 `exists` 会返回 `false`
|
|
|
|
|
- B 就继续 `save`,结果就可能插入重复记录,或者引发唯一索引冲突
|
|
|
|
|
|
2025-06-20 11:57:50 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### RBAC模型
|
|
|
|
|
|
|
|
|
|
团队空间:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
一般来说,标准的` RBAC` 实现需要 5 张表:用户表、角色表、权限表、用户角色关联表、角色权限关联表,还是有一定开发成本的。由于我们的项目中,团队空间不需要那么多角色,可以简化`RBAC` 的实现方式,比如将角色和权限直接定义到配置文件中。
|
|
|
|
|
|
|
|
|
|
本项目角色:
|
|
|
|
|
|
|
|
|
|
| 角色 | 描述 |
|
|
|
|
|
| ------ | ---------------------------- |
|
|
|
|
|
| 浏览者 | 仅可查看空间中的图片内容 |
|
|
|
|
|
| 编辑者 | 可查看、上传和编辑图片内容 |
|
|
|
|
|
| 管理员 | 拥有管理空间和成员的所有权限 |
|
|
|
|
|
|
|
|
|
|
本项目权限:
|
|
|
|
|
|
|
|
|
|
| 权限键 | 功能名称 | 描述 |
|
|
|
|
|
| -------------- | -------- | ---------------------------- |
|
|
|
|
|
| spaceUsername | 成员管理 | 管理空间成员,添加或移除成员 |
|
|
|
|
|
| picture:view | 查看图片 | 查看空间中的图片内容 |
|
|
|
|
|
| picture:upload | 上传图片 | 上传图片到空间中 |
|
|
|
|
|
| picture:edit | 修改图片 | 编辑已上传的图片信息 |
|
|
|
|
|
| picture:delete | 删除图片 | 删除空间中的图片 |
|
|
|
|
|
|
|
|
|
|
角色权限映射:
|
|
|
|
|
|
|
|
|
|
| 角色 | 对应权限键 | 可执行功能 |
|
|
|
|
|
| ------ | ------------------------------------------------------------ | ------------------------------------------------ |
|
|
|
|
|
| 浏览者 | picture:view | 查看图片 |
|
|
|
|
|
| 编辑者 | picture:view, picture:upload, picture:edit, picture:delete | 查看图片、上传图片、修改图片、删除图片 |
|
|
|
|
|
| 管理员 | spaceUsername, picture:view, picture:upload, picture:edit, picture:delete | 成员管理、查看图片、上传图片、修改图片、删除图片 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RBAC 只是一种权限设计模型,我们在 Java 代码中如何实现权限校验呢?
|
|
|
|
|
|
|
|
|
|
1)最直接的方案是像之前校验私有空间权限一样,封装个团队空间的权限校验方法;或者类似用户权限校验一样,写个注解 + AOP 切面。
|
|
|
|
|
|
|
|
|
|
2)对于复杂的角色和权限管理,可以选用现成的第三方权限校验框架来实现,编写一套权限校验规则代码后,就能整体管理系统的权限校验逻辑了。( Sa-Token)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 分库分表
|
|
|
|
|
|
|
|
|
|
如果某团队空间的图片数量比较多,可以对其数据进行单独的管理。
|
|
|
|
|
|
|
|
|
|
1、图片信息数据
|
|
|
|
|
可以给每个团队空间单独创建一张图片表 picture_{spaceId},也就是分库分表中的分表,而不是和公共图库、私有空间的图片混在一起。这样不仅查询空间内的图片效率更高,还便于整体管理和清理空间。但是要注意,仅对**旗舰版**空间生效,**否则分表的数量会特别多**,反而可能影响性能。
|
|
|
|
|
|
|
|
|
|
要实现的是会随着新增空间不断增加分表数量的**动态分表**,会使用分库分表框架 **Apache ShardingSphere** 带大家实现。
|
|
|
|
|
2、图片文件数据
|
|
|
|
|
|
|
|
|
|
已经实现隔离,存到COS上的不同桶内。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|