Compare commits
10 Commits
1026b764dd
...
528422fed6
Author | SHA1 | Date | |
---|---|---|---|
528422fed6 | |||
6a5fb45cc2 | |||
012e66bfc8 | |||
41760e047d | |||
00c871f41e | |||
e40c4a975b | |||
a44466e846 | |||
886d2a89d6 | |||
9651ef6790 | |||
c2d8391166 |
34
pom.xml
34
pom.xml
@ -79,6 +79,40 @@
|
||||
<groupId>org.springframework.session</groupId>
|
||||
<artifactId>spring-session-data-redis</artifactId>
|
||||
</dependency>
|
||||
<!-- Sa-Token 权限认证 -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot-starter</artifactId>
|
||||
<version>1.39.0</version>
|
||||
</dependency>
|
||||
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-redis-jackson</artifactId>
|
||||
<version>1.39.0</version>
|
||||
</dependency>
|
||||
<!-- 提供Redis连接池 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
</dependency>
|
||||
<!-- 分库分表 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.shardingsphere</groupId>
|
||||
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
|
||||
<version>5.2.0</version>
|
||||
</dependency>
|
||||
<!-- websocket -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
<!-- 高性能无锁队列 -->
|
||||
<dependency>
|
||||
<groupId>com.lmax</groupId>
|
||||
<artifactId>disruptor</artifactId>
|
||||
<version>3.4.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
|
@ -62,3 +62,56 @@ ALTER TABLE picture
|
||||
-- 添加新列
|
||||
ADD COLUMN original_url varchar(512) NULL COMMENT '原图 url',
|
||||
ADD COLUMN thumbnail_url varchar(512) NULL COMMENT '缩略图 url';
|
||||
|
||||
-- 空间表
|
||||
create table if not exists space
|
||||
(
|
||||
id bigint auto_increment comment 'id' primary key,
|
||||
space_name varchar(128) null comment '空间名称',
|
||||
space_level int default 0 null comment '空间级别:0-普通版 1-专业版 2-旗舰版',
|
||||
max_size bigint default 0 null comment '空间图片的最大总大小',
|
||||
max_count bigint default 0 null comment '空间图片的最大数量',
|
||||
total_size bigint default 0 null comment '当前空间下图片的总大小',
|
||||
total_count bigint default 0 null comment '当前空间下的图片数量',
|
||||
user_id bigint not null comment '创建用户 id',
|
||||
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
|
||||
edit_time datetime default CURRENT_TIMESTAMP not null comment '编辑时间',
|
||||
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
|
||||
is_delete tinyint default 0 not null comment '是否删除',
|
||||
-- 索引设计
|
||||
index idx_userId (user_id), -- 提升基于用户的查询效率
|
||||
index idx_spaceName (space_name), -- 提升基于空间名称的查询效率
|
||||
index idx_spaceLevel (space_level) -- 提升按空间级别查询的效率
|
||||
) comment '空间' collate = utf8mb4_unicode_ci;
|
||||
|
||||
-- 添加新列
|
||||
ALTER TABLE picture
|
||||
ADD COLUMN space_id bigint null comment '空间 id(为空表示公共空间)';
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_spaceId ON picture (space_id);
|
||||
|
||||
-- 添加新列
|
||||
ALTER TABLE picture
|
||||
ADD COLUMN pic_color varchar(16) null comment '图片主色调';
|
||||
|
||||
-- 支持空间类型,添加新列
|
||||
ALTER TABLE space
|
||||
ADD COLUMN space_type int default 0 not null comment '空间类型:0-私有 1-团队';
|
||||
|
||||
CREATE INDEX idx_spaceType ON space (space_type);
|
||||
|
||||
-- 空间成员表
|
||||
create table if not exists space_user
|
||||
(
|
||||
id bigint auto_increment comment 'id' primary key,
|
||||
space_id bigint not null comment '空间 id',
|
||||
user_id bigint not null comment '用户 id',
|
||||
space_role varchar(128) default 'viewer' null comment '空间角色:viewer/editor/admin',
|
||||
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
|
||||
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
|
||||
-- 索引设计
|
||||
UNIQUE KEY uk_spaceId_userId (space_id, user_id), -- 唯一索引,用户在一个空间中只能有一个角色
|
||||
INDEX idx_spaceId (space_id), -- 提升按空间查询的性能
|
||||
INDEX idx_userId (user_id) -- 提升按用户查询的性能
|
||||
) comment '空间用户关联' collate = utf8mb4_unicode_ci;
|
@ -1,11 +1,11 @@
|
||||
package edu.whut.smilepicturebackend;
|
||||
|
||||
import org.apache.shardingsphere.spring.boot.ShardingSphereAutoConfiguration;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
@SpringBootApplication
|
||||
@SpringBootApplication(exclude = {ShardingSphereAutoConfiguration.class})
|
||||
@EnableAsync
|
||||
@MapperScan("edu.whut.smilepicturebackend.mapper")
|
||||
public class SmilePictureBackendApplication {
|
||||
|
@ -0,0 +1,87 @@
|
||||
package edu.whut.smilepicturebackend.api.aliyunai;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import edu.whut.smilepicturebackend.api.aliyunai.model.CreateOutPaintingTaskRequest;
|
||||
import edu.whut.smilepicturebackend.api.aliyunai.model.CreateOutPaintingTaskResponse;
|
||||
import edu.whut.smilepicturebackend.api.aliyunai.model.GetOutPaintingTaskResponse;
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class AliYunAiApi {
|
||||
|
||||
// 读取配置文件
|
||||
@Value("${smile-picture.aliyun.apiKey}")
|
||||
private String apiKey;
|
||||
|
||||
// 创建任务地址
|
||||
public static final String CREATE_OUT_PAINTING_TASK_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/image2image/out-painting";
|
||||
|
||||
// 查询任务状态
|
||||
public static final String GET_OUT_PAINTING_TASK_URL = "https://dashscope.aliyuncs.com/api/v1/tasks/%s";
|
||||
|
||||
|
||||
/**
|
||||
* 创建任务
|
||||
*
|
||||
* @param createOutPaintingTaskRequest
|
||||
* @return
|
||||
*/
|
||||
public CreateOutPaintingTaskResponse createOutPaintingTask(CreateOutPaintingTaskRequest createOutPaintingTaskRequest) {
|
||||
if (createOutPaintingTaskRequest == null) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "扩图参数为空");
|
||||
}
|
||||
// 发送请求
|
||||
HttpRequest httpRequest = HttpRequest.post(CREATE_OUT_PAINTING_TASK_URL)
|
||||
.header("Authorization", "Bearer " + apiKey)
|
||||
// 必须开启异步处理 enable
|
||||
.header("X-DashScope-Async", "enable")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(JSONUtil.toJsonStr(createOutPaintingTaskRequest));
|
||||
// 处理响应
|
||||
//这段代码会在 try 块结束时(正常返回或抛出异常)自动调用 httpResponse.close(),从而释放所有底层资源
|
||||
try (HttpResponse httpResponse = httpRequest.execute()) {
|
||||
if (!httpResponse.isOk()) {
|
||||
log.error("请求异常:{}", httpResponse.body());
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "AI 扩图失败");
|
||||
}
|
||||
CreateOutPaintingTaskResponse createOutPaintingTaskResponse = JSONUtil.toBean(httpResponse.body(), CreateOutPaintingTaskResponse.class);
|
||||
if (createOutPaintingTaskResponse.getCode() != null) {
|
||||
String errorMessage = createOutPaintingTaskResponse.getMessage();
|
||||
log.error("请求异常:{}", errorMessage);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "AI 扩图失败," + errorMessage);
|
||||
}
|
||||
return createOutPaintingTaskResponse;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询创建的任务结果
|
||||
*
|
||||
* @param taskId
|
||||
* @return
|
||||
*/
|
||||
public GetOutPaintingTaskResponse getOutPaintingTask(String taskId) {
|
||||
if (StrUtil.isBlank(taskId)) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "任务 ID 不能为空");
|
||||
}
|
||||
// 处理响应
|
||||
String url = String.format(GET_OUT_PAINTING_TASK_URL, taskId);
|
||||
try (HttpResponse httpResponse = HttpRequest.get(url)
|
||||
.header("Authorization", "Bearer " + apiKey)
|
||||
.execute()) {
|
||||
if (!httpResponse.isOk()) {
|
||||
log.error("请求异常:{}", httpResponse.body());
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "获取任务结果失败");
|
||||
}
|
||||
return JSONUtil.toBean(httpResponse.body(), GetOutPaintingTaskResponse.class);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
package edu.whut.smilepicturebackend.api.aliyunai.model;
|
||||
|
||||
import cn.hutool.core.annotation.Alias;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 创建扩图任务请求
|
||||
*/
|
||||
@Data
|
||||
public class CreateOutPaintingTaskRequest implements Serializable {
|
||||
|
||||
/**
|
||||
* 模型,例如 "image-out-painting"
|
||||
*/
|
||||
private String model = "image-out-painting";
|
||||
|
||||
/**
|
||||
* 输入图像信息
|
||||
*/
|
||||
private Input input;
|
||||
|
||||
/**
|
||||
* 图像处理参数
|
||||
*/
|
||||
private Parameters parameters;
|
||||
|
||||
@Data
|
||||
public static class Input {
|
||||
/**
|
||||
* 必选,图像 URL
|
||||
*/
|
||||
@Alias("image_url") //java类中一般写驼峰式的,但是大模型接收的是image_url 所以用@Alias
|
||||
private String imageUrl;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Parameters implements Serializable {
|
||||
/**
|
||||
* 可选,逆时针旋转角度,默认值 0,取值范围 [0, 359]
|
||||
*/
|
||||
private Integer angle;
|
||||
|
||||
/**
|
||||
* 可选,输出图像的宽高比,默认空字符串,不设置宽高比
|
||||
* 可选值:["", "1:1", "3:4", "4:3", "9:16", "16:9"]
|
||||
*/
|
||||
@Alias("output_ratio")
|
||||
private String outputRatio;
|
||||
|
||||
/**
|
||||
* 可选,图像居中,在水平方向上按比例扩展,默认值 1.0,范围 [1.0, 3.0]
|
||||
*/
|
||||
@Alias("x_scale")
|
||||
@JsonProperty("xScale")
|
||||
private Float xScale;
|
||||
|
||||
/**
|
||||
* 可选,图像居中,在垂直方向上按比例扩展,默认值 1.0,范围 [1.0, 3.0]
|
||||
*/
|
||||
@Alias("y_scale")
|
||||
@JsonProperty("yScale")
|
||||
private Float yScale;
|
||||
|
||||
/**
|
||||
* 可选,在图像上方添加像素,默认值 0
|
||||
*/
|
||||
@Alias("top_offset")
|
||||
private Integer topOffset;
|
||||
|
||||
/**
|
||||
* 可选,在图像下方添加像素,默认值 0
|
||||
*/
|
||||
@Alias("bottom_offset")
|
||||
private Integer bottomOffset;
|
||||
|
||||
/**
|
||||
* 可选,在图像左侧添加像素,默认值 0
|
||||
*/
|
||||
@Alias("left_offset")
|
||||
private Integer leftOffset;
|
||||
|
||||
/**
|
||||
* 可选,在图像右侧添加像素,默认值 0
|
||||
*/
|
||||
@Alias("right_offset")
|
||||
private Integer rightOffset;
|
||||
|
||||
/**
|
||||
* 可选,开启图像最佳质量模式,默认值 false
|
||||
* 若为 true,耗时会成倍增加
|
||||
*/
|
||||
@Alias("best_quality")
|
||||
private Boolean bestQuality;
|
||||
|
||||
/**
|
||||
* 可选,限制模型生成的图像文件大小,默认值 true
|
||||
* - 单边长度 <= 10000:输出图像文件大小限制为 5MB 以下
|
||||
* - 单边长度 > 10000:输出图像文件大小限制为 10MB 以下
|
||||
*/
|
||||
@Alias("limit_image_size")
|
||||
private Boolean limitImageSize;
|
||||
|
||||
/**
|
||||
* 可选,添加 "Generated by AI" 水印,默认值 true
|
||||
*/
|
||||
@Alias("add_watermark")
|
||||
private Boolean addWatermark = false;
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package edu.whut.smilepicturebackend.api.aliyunai.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 创建扩图任务响应类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CreateOutPaintingTaskResponse {
|
||||
|
||||
private Output output;
|
||||
|
||||
/**
|
||||
* 表示任务的输出信息
|
||||
*/
|
||||
@Data
|
||||
public static class Output {
|
||||
|
||||
/**
|
||||
* 任务 ID
|
||||
*/
|
||||
private String taskId;
|
||||
|
||||
/**
|
||||
* 任务状态
|
||||
* <ul>
|
||||
* <li>PENDING:排队中</li>
|
||||
* <li>RUNNING:处理中</li>
|
||||
* <li>SUSPENDED:挂起</li>
|
||||
* <li>SUCCEEDED:执行成功</li>
|
||||
* <li>FAILED:执行失败</li>
|
||||
* <li>UNKNOWN:任务不存在或状态未知</li>
|
||||
* </ul>
|
||||
*/
|
||||
private String taskStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口错误码。
|
||||
* <p>接口成功请求不会返回该参数。</p>
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 接口错误信息。
|
||||
* <p>接口成功请求不会返回该参数。</p>
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 请求唯一标识。
|
||||
* <p>可用于请求明细溯源和问题排查。</p>
|
||||
*/
|
||||
private String requestId;
|
||||
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
package edu.whut.smilepicturebackend.api.aliyunai.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 查询扩图任务响应类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GetOutPaintingTaskResponse {
|
||||
|
||||
/**
|
||||
* 请求唯一标识
|
||||
*/
|
||||
private String requestId;
|
||||
|
||||
/**
|
||||
* 输出信息
|
||||
*/
|
||||
private Output output;
|
||||
|
||||
/**
|
||||
* 表示任务的输出信息
|
||||
*/
|
||||
@Data
|
||||
public static class Output {
|
||||
|
||||
/**
|
||||
* 任务 ID
|
||||
*/
|
||||
private String taskId;
|
||||
|
||||
/**
|
||||
* 任务状态
|
||||
* <ul>
|
||||
* <li>PENDING:排队中</li>
|
||||
* <li>RUNNING:处理中</li>
|
||||
* <li>SUSPENDED:挂起</li>
|
||||
* <li>SUCCEEDED:执行成功</li>
|
||||
* <li>FAILED:执行失败</li>
|
||||
* <li>UNKNOWN:任务不存在或状态未知</li>
|
||||
* </ul>
|
||||
*/
|
||||
private String taskStatus;
|
||||
|
||||
/**
|
||||
* 提交时间
|
||||
* 格式:YYYY-MM-DD HH:mm:ss.SSS
|
||||
*/
|
||||
private String submitTime;
|
||||
|
||||
/**
|
||||
* 调度时间
|
||||
* 格式:YYYY-MM-DD HH:mm:ss.SSS
|
||||
*/
|
||||
private String scheduledTime;
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
* 格式:YYYY-MM-DD HH:mm:ss.SSS
|
||||
*/
|
||||
private String endTime;
|
||||
|
||||
/**
|
||||
* 输出图像的 URL
|
||||
*/
|
||||
private String outputImageUrl;
|
||||
|
||||
/**
|
||||
* 接口错误码
|
||||
* <p>接口成功请求不会返回该参数</p>
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 接口错误信息
|
||||
* <p>接口成功请求不会返回该参数</p>
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 任务指标信息
|
||||
*/
|
||||
private TaskMetrics taskMetrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* 表示任务的统计信息
|
||||
*/
|
||||
@Data
|
||||
public static class TaskMetrics {
|
||||
|
||||
/**
|
||||
* 总任务数
|
||||
*/
|
||||
private Integer total;
|
||||
|
||||
/**
|
||||
* 成功任务数
|
||||
*/
|
||||
private Integer succeeded;
|
||||
|
||||
/**
|
||||
* 失败任务数
|
||||
*/
|
||||
private Integer failed;
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package edu.whut.smilepicturebackend.api.imagesearch;
|
||||
import edu.whut.smilepicturebackend.api.imagesearch.model.ImageSearchResult;
|
||||
import edu.whut.smilepicturebackend.api.imagesearch.sub.GetImageFirstUrlApi;
|
||||
import edu.whut.smilepicturebackend.api.imagesearch.sub.GetImageListApi;
|
||||
import edu.whut.smilepicturebackend.api.imagesearch.sub.GetImagePageUrlApi;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
public class ImageSearchApiFacade {
|
||||
|
||||
/**
|
||||
* 搜索图片
|
||||
* @param localImagePath
|
||||
* @return
|
||||
*/
|
||||
public static List<ImageSearchResult> searchImage(String localImagePath) {
|
||||
String imagePageUrl = GetImagePageUrlApi.getImagePageUrl(localImagePath);
|
||||
String imageFirstUrl = GetImageFirstUrlApi.getImageFirstUrl(imagePageUrl);
|
||||
List<ImageSearchResult> imageList = GetImageListApi.getImageList(imageFirstUrl);
|
||||
return imageList;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
List<ImageSearchResult> imageList = searchImage("https://www.codefather.cn/logo.png");
|
||||
System.out.println("结果列表" + imageList);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package edu.whut.smilepicturebackend.api.imagesearch.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 图片搜索结果
|
||||
*/
|
||||
@Data
|
||||
public class ImageSearchResult {
|
||||
|
||||
/**
|
||||
* 缩略图地址
|
||||
*/
|
||||
private String thumbUrl;
|
||||
|
||||
/**
|
||||
* 来源地址
|
||||
*/
|
||||
private String fromUrl;
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package edu.whut.smilepicturebackend.api.imagesearch.sub;
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.select.Elements;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 获取图片列表接口的 Api(Step 2)
|
||||
*/
|
||||
@Slf4j
|
||||
public class GetImageFirstUrlApi {
|
||||
|
||||
/**
|
||||
* 获取图片列表页面地址
|
||||
*
|
||||
* @param url
|
||||
* @return
|
||||
*/
|
||||
public static String getImageFirstUrl(String url) {
|
||||
try {
|
||||
// 使用 Jsoup 获取 HTML 内容
|
||||
Document document = Jsoup.connect(url)
|
||||
.timeout(5000)
|
||||
.get();
|
||||
|
||||
// 获取所有 <script> 标签
|
||||
Elements scriptElements = document.getElementsByTag("script");
|
||||
|
||||
// 遍历找到包含 `firstUrl` 的脚本内容
|
||||
for (Element script : scriptElements) {
|
||||
String scriptContent = script.html();
|
||||
if (scriptContent.contains("\"firstUrl\"")) {
|
||||
// 正则表达式提取 firstUrl 的值
|
||||
Pattern pattern = Pattern.compile("\"firstUrl\"\\s*:\\s*\"(.*?)\"");
|
||||
Matcher matcher = pattern.matcher(scriptContent);
|
||||
if (matcher.find()) {
|
||||
String firstUrl = matcher.group(1);
|
||||
// 处理转义字符
|
||||
firstUrl = firstUrl.replace("\\/", "/");
|
||||
return firstUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "未找到 url");
|
||||
} catch (Exception e) {
|
||||
log.error("搜索失败", e);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "搜索失败");
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 请求目标 URL
|
||||
String url = "https://graph.baidu.com/s?card_key=&entrance=GENERAL&extUiData[isLogoShow]=1&f=all&isLogoShow=1&session_id=11441865424208889950&sign=1265ce97cd54acd88139901750155197&tpl_from=pc";
|
||||
String imageFirstUrl = getImageFirstUrl(url);
|
||||
System.out.println("搜索成功,结果 URL:" + imageFirstUrl);
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package edu.whut.smilepicturebackend.api.imagesearch.sub;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import edu.whut.smilepicturebackend.api.imagesearch.model.ImageSearchResult;
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 获取图片列表(step 3)
|
||||
*/
|
||||
@Slf4j
|
||||
public class GetImageListApi {
|
||||
|
||||
/**
|
||||
* 获取图片列表
|
||||
*
|
||||
* @param url
|
||||
* @return
|
||||
*/
|
||||
public static List<ImageSearchResult> getImageList(String url) {
|
||||
try {
|
||||
// 发起GET请求
|
||||
HttpResponse response = HttpUtil.createGet(url).execute();
|
||||
// 获取响应内容
|
||||
int statusCode = response.getStatus();
|
||||
String body = response.body();
|
||||
|
||||
// 处理响应
|
||||
if (statusCode == 200) {
|
||||
// 解析 JSON 数据并处理
|
||||
return processResponse(body);
|
||||
} else {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "接口调用失败");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取图片列表失败", e);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "获取图片列表失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接口响应内容
|
||||
*
|
||||
* @param responseBody 接口返回的JSON字符串
|
||||
*/
|
||||
private static List<ImageSearchResult> processResponse(String responseBody) {
|
||||
// 解析响应对象
|
||||
JSONObject jsonObject = new JSONObject(responseBody);
|
||||
if (!jsonObject.containsKey("data")) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "未获取到图片列表");
|
||||
}
|
||||
JSONObject data = jsonObject.getJSONObject("data");
|
||||
if (!data.containsKey("list")) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "未获取到图片列表");
|
||||
}
|
||||
JSONArray list = data.getJSONArray("list");
|
||||
return JSONUtil.toList(list, ImageSearchResult.class);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
String url = "https://graph.baidu.com/ajax/pcsimi?carousel=503&entrance=GENERAL&extUiData%5BisLogoShow%5D=1&inspire=general_pc&limit=30&next=2&render_type=card&session_id=11441865424208889950&sign=1265ce97cd54acd88139901750155197&tk=9d296&tpl_from=pc";
|
||||
List<ImageSearchResult> imageList = getImageList(url);
|
||||
System.out.println("搜索成功" + imageList);
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package edu.whut.smilepicturebackend.api.imagesearch.sub;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.util.URLUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.http.HttpStatus;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 获取以图搜图页面地址(step 1)
|
||||
*/
|
||||
@Slf4j
|
||||
public class GetImagePageUrlApi {
|
||||
|
||||
/**
|
||||
* 获取以图搜图页面地址
|
||||
*
|
||||
* @param imageUrl
|
||||
* @return
|
||||
*/
|
||||
/**
|
||||
* 调用百度「以图搜图」上传接口,返回结果页 URL
|
||||
*/
|
||||
public static String getImagePageUrl(String localImagePath) {
|
||||
// 1⃣ 参数准备(必须 urlencode)
|
||||
Map<String, Object> formData = new HashMap<>();
|
||||
formData.put("image", new java.io.File(localImagePath)); // 关键
|
||||
formData.put("tn", "pc");
|
||||
formData.put("from", "pc");
|
||||
formData.put("image_source", "PC_UPLOAD_URL");
|
||||
|
||||
String uploadUrl = "https://graph.baidu.com/upload?uptime=" + System.currentTimeMillis();
|
||||
|
||||
// Acs-Token 每次打开 graph.baidu.com 时都会刷新,可抓包固定或动态获取
|
||||
String acsToken = "jmM4zyI8OUixvSuWh0sCy4xWbsttVMZb9qcRTmn6SuNWg0vCO7N0s6Lffec+IY5yuqHujHmCctF9BVCGYGH0H5SH/H3VPFUl4O4CP1jp8GoAzuslb8kkQQ4a21Tebge8yhviopaiK66K6hNKGPlWt78xyyJxTteFdXYLvoO6raqhz2yNv50vk4/41peIwba4lc0hzoxdHxo3OBerHP2rfHwLWdpjcI9xeu2nJlGPgKB42rYYVW50+AJ3tQEBEROlg/UNLNxY+6200B/s6Ryz+n7xUptHFHi4d8Vp8q7mJ26yms+44i8tyiFluaZAr66/+wW/KMzOhqhXCNgckoGPX1SSYwueWZtllIchRdsvCZQ8tFJymKDjCf3yI/Lw1oig9OKZCAEtiLTeKE9/CY+Crp8DHa8Tpvlk2/i825E3LuTF8EQfzjcGpVnR00Lb4/8A";
|
||||
|
||||
try (HttpResponse resp = HttpRequest.post(uploadUrl)
|
||||
.header("User-Agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36")
|
||||
.header("Referer", "https://graph.baidu.com/") // 防 403
|
||||
.header("Acs-Token", acsToken) // 防风控
|
||||
.form(formData)
|
||||
.setFollowRedirects(true) // 跟随 302
|
||||
.timeout(5000)
|
||||
.execute()) {
|
||||
|
||||
if (resp.getStatus() != HttpStatus.HTTP_OK) {
|
||||
log.error("[百度搜图] 上传失败,HTTP={}", resp.getStatus());
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "搜图失败");
|
||||
}
|
||||
|
||||
JSONObject json = JSONUtil.parseObj(resp.body()); // 兼容字符串/数字
|
||||
if (json.getInt("status", -1) != 0) {
|
||||
String msg = json.getStr("msg", "unknown");
|
||||
log.error("[百度搜图] 接口返回错误:{}", msg);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "搜图失败");
|
||||
}
|
||||
|
||||
String rawUrl = json.getJSONObject("data").getStr("url");
|
||||
String pageUrl = URLUtil.decode(rawUrl, StandardCharsets.UTF_8);
|
||||
|
||||
if (StrUtil.isBlank(pageUrl)) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "未返回有效地址");
|
||||
}
|
||||
return pageUrl;
|
||||
} catch (Exception e) {
|
||||
log.error("[百度搜图] 上传过程异常", e);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "搜图失败");
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 测试以图搜图功能
|
||||
String imageUrl = "https://www.codefather.cn/logo.png";
|
||||
String searchResultUrl = getImagePageUrl(imageUrl);
|
||||
System.out.println("搜索成功,结果 URL:" + searchResultUrl);
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package edu.whut.smilepicturebackend.config;
|
||||
|
||||
import cn.hutool.http.ContentType;
|
||||
import cn.hutool.http.Header;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 请求包装过滤器
|
||||
*
|
||||
* @author pine
|
||||
*/
|
||||
@Order(1)
|
||||
@Component
|
||||
public class HttpRequestWrapperFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
|
||||
if (request instanceof HttpServletRequest) {
|
||||
HttpServletRequest servletRequest = (HttpServletRequest) request;
|
||||
String contentType = servletRequest.getHeader(Header.CONTENT_TYPE.getValue());
|
||||
if (ContentType.JSON.getValue().equals(contentType)) {
|
||||
// 可以再细粒度一些,只有需要进行空间权限校验的接口才需要包一层
|
||||
chain.doFilter(new RequestWrapper(servletRequest), response);
|
||||
} else {
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package edu.whut.smilepicturebackend.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.servlet.ReadListener;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* 包装请求,使 InputStream 可以重复读取
|
||||
*
|
||||
* @author pine
|
||||
*/
|
||||
@Slf4j
|
||||
public class RequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private final String body;
|
||||
|
||||
public RequestWrapper(HttpServletRequest request) {
|
||||
super(request);
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
try (InputStream inputStream = request.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
|
||||
char[] charBuffer = new char[128];
|
||||
int bytesRead = -1;
|
||||
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
|
||||
stringBuilder.append(charBuffer, 0, bytesRead);
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
body = stringBuilder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
|
||||
return new ServletInputStream() {
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return byteArrayInputStream.read();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() throws IOException {
|
||||
return new BufferedReader(new InputStreamReader(this.getInputStream()));
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return this.body;
|
||||
}
|
||||
|
||||
}
|
@ -13,8 +13,14 @@ public class MainController {
|
||||
* 健康检查
|
||||
*/
|
||||
@GetMapping("/health")
|
||||
public BaseResponse<String> health() {
|
||||
public BaseResponse<String> health() throws InterruptedException {
|
||||
// new Thread(() -> {
|
||||
// try {
|
||||
// Thread.sleep(100000L);
|
||||
// } catch (InterruptedException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// }).start();
|
||||
return ResultUtils.success("ok");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,7 +1,13 @@
|
||||
package edu.whut.smilepicturebackend.controller;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import edu.whut.smilepicturebackend.annotation.AuthCheck;
|
||||
import edu.whut.smilepicturebackend.api.aliyunai.AliYunAiApi;
|
||||
import edu.whut.smilepicturebackend.api.aliyunai.model.CreateOutPaintingTaskResponse;
|
||||
import edu.whut.smilepicturebackend.api.aliyunai.model.GetOutPaintingTaskResponse;
|
||||
import edu.whut.smilepicturebackend.api.imagesearch.ImageSearchApiFacade;
|
||||
import edu.whut.smilepicturebackend.api.imagesearch.model.ImageSearchResult;
|
||||
import edu.whut.smilepicturebackend.common.BaseResponse;
|
||||
import edu.whut.smilepicturebackend.common.DeleteRequest;
|
||||
import edu.whut.smilepicturebackend.common.ResultUtils;
|
||||
@ -9,22 +15,28 @@ import edu.whut.smilepicturebackend.constant.UserConstant;
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import edu.whut.smilepicturebackend.exception.ThrowUtils;
|
||||
import edu.whut.smilepicturebackend.manager.auth.SpaceUserAuthManager;
|
||||
import edu.whut.smilepicturebackend.manager.auth.StpKit;
|
||||
import edu.whut.smilepicturebackend.manager.auth.annotation.SaSpaceCheckPermission;
|
||||
import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserPermissionConstant;
|
||||
import edu.whut.smilepicturebackend.model.dto.picture.*;
|
||||
import edu.whut.smilepicturebackend.model.entity.Picture;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
import edu.whut.smilepicturebackend.model.enums.PictureReviewStatusEnum;
|
||||
import edu.whut.smilepicturebackend.model.vo.PictureTagCategory;
|
||||
import edu.whut.smilepicturebackend.model.vo.PictureVO;
|
||||
import edu.whut.smilepicturebackend.service.PictureService;
|
||||
import edu.whut.smilepicturebackend.service.SpaceService;
|
||||
import edu.whut.smilepicturebackend.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.util.DigestUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@ -36,14 +48,17 @@ import java.util.List;
|
||||
public class PictureController {
|
||||
private final UserService userService;
|
||||
private final PictureService pictureService;
|
||||
|
||||
private final SpaceService spaceService;
|
||||
private final AliYunAiApi aliYunAiApi;
|
||||
private final SpaceUserAuthManager spaceUserAuthManager;
|
||||
|
||||
|
||||
/**
|
||||
* 上传图片(可重新上传),前端选中图片就会调用该接口,无需前端点'创建'按钮!
|
||||
*
|
||||
*TODO:目前有个bug,用户上传图片需要审核,会跳转到一个空白的图片详情页
|
||||
*/
|
||||
@PostMapping("/upload")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_UPLOAD)
|
||||
// @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<PictureVO> uploadPicture(
|
||||
@RequestPart("file") MultipartFile multipartFile,
|
||||
@ -58,6 +73,7 @@ public class PictureController {
|
||||
* 通过 URL 上传图片(可重新上传)
|
||||
*/
|
||||
@PostMapping("/upload/url")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_UPLOAD)
|
||||
public BaseResponse<PictureVO> uploadPictureByUrl(
|
||||
@RequestBody PictureUploadRequest pictureUploadRequest,
|
||||
HttpServletRequest request) {
|
||||
@ -74,6 +90,7 @@ public class PictureController {
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/delete")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_DELETE)
|
||||
public BaseResponse<Boolean> deletePicture(@RequestBody DeleteRequest deleteRequest,
|
||||
HttpServletRequest request) {
|
||||
if (deleteRequest == null || deleteRequest.getId() <= 0) {
|
||||
@ -84,6 +101,20 @@ public class PictureController {
|
||||
return ResultUtils.success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑图片(给用户使用)或创建图片时,编辑标签、分类的时候
|
||||
*/
|
||||
@PostMapping("/edit")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_EDIT)
|
||||
public BaseResponse<Boolean> editPicture(@RequestBody PictureEditRequest pictureEditRequest, HttpServletRequest request) {
|
||||
if (pictureEditRequest == null || pictureEditRequest.getId() <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
pictureService.editPicture(pictureEditRequest, loginUser);
|
||||
return ResultUtils.success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新图片(仅管理员可用)
|
||||
*
|
||||
@ -136,6 +167,7 @@ public class PictureController {
|
||||
|
||||
/**
|
||||
* 根据 id 获取图片(封装类)
|
||||
* 这里不用sa-token的注解鉴权,因为它强制要求用户登录。故这里使用编程式注解
|
||||
*/
|
||||
@GetMapping("/get/vo")
|
||||
public BaseResponse<PictureVO> getPictureVOById(long id, HttpServletRequest request) {
|
||||
@ -143,9 +175,27 @@ public class PictureController {
|
||||
// 查询数据库
|
||||
Picture picture = pictureService.getById(id);
|
||||
ThrowUtils.throwIf(picture == null, ErrorCode.NOT_FOUND_ERROR);
|
||||
ThrowUtils.throwIf(PictureReviewStatusEnum.PASS.getValue()!=picture.getReviewStatus(),ErrorCode.NOT_FOUND_ERROR);
|
||||
// ThrowUtils.throwIf(PictureReviewStatusEnum.PASS.getValue()!=picture.getReviewStatus(),ErrorCode.NOT_FOUND_ERROR);
|
||||
// 空间权限校验
|
||||
Long spaceId = picture.getSpaceId();
|
||||
Space space = null;
|
||||
// 权限判断 —— 只对“团队空间”才要求登录 & 权限
|
||||
if (spaceId != null) {
|
||||
boolean hasPermission = StpKit.SPACE.hasPermission(SpaceUserPermissionConstant.PICTURE_VIEW); //编程式鉴权
|
||||
ThrowUtils.throwIf(!hasPermission, ErrorCode.NO_AUTH_ERROR);
|
||||
//User loginUser = userService.getLoginUser(request);
|
||||
//已经改为使用sa-token鉴权
|
||||
//pictureService.checkPictureAuth(loginUser, picture);
|
||||
space = spaceService.getById(spaceId);
|
||||
ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
|
||||
}
|
||||
// 获取权限列表
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
List<String> permissionList = spaceUserAuthManager.getPermissionList(space, loginUser);
|
||||
PictureVO pictureVO = pictureService.getPictureVO(picture, request);
|
||||
pictureVO.setPermissionList(permissionList);
|
||||
// 获取封装类
|
||||
return ResultUtils.success(pictureService.getPictureVO(picture, request));
|
||||
return ResultUtils.success(pictureVO);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -172,8 +222,25 @@ public class PictureController {
|
||||
long size = pictureQueryRequest.getPageSize();
|
||||
// 限制爬虫,一次不能请求超过20页
|
||||
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
|
||||
// 普通用户默认只能看到审核通过的数据
|
||||
pictureQueryRequest.setReviewStatus(PictureReviewStatusEnum.PASS.getValue());
|
||||
// 空间权限校验
|
||||
Long spaceId = pictureQueryRequest.getSpaceId();
|
||||
if (spaceId == null) {
|
||||
// 公开图库
|
||||
// 普通用户默认只能看到审核通过的数据
|
||||
pictureQueryRequest.setReviewStatus(PictureReviewStatusEnum.PASS.getValue());
|
||||
pictureQueryRequest.setNullSpaceId(true);
|
||||
} else {
|
||||
// 私有空间、团队空间,无需过审
|
||||
boolean hasPermission = StpKit.SPACE.hasPermission(SpaceUserPermissionConstant.PICTURE_VIEW);
|
||||
ThrowUtils.throwIf(!hasPermission, ErrorCode.NO_AUTH_ERROR);
|
||||
//已改为编程式鉴权
|
||||
// User loginUser = userService.getLoginUser(request);
|
||||
// Space space = spaceService.getById(spaceId);
|
||||
// ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
|
||||
// if (!loginUser.getId().equals(space.getUserId())) {
|
||||
// throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "没有空间权限");
|
||||
// }
|
||||
}
|
||||
// 查询数据库
|
||||
Page<Picture> picturePage = pictureService.page(new Page<>(current, size),
|
||||
pictureService.getQueryWrapper(pictureQueryRequest));
|
||||
@ -196,18 +263,6 @@ public class PictureController {
|
||||
return ResultUtils.success(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑图片(给用户使用)或创建图片时,编辑标签、分类的时候
|
||||
*/
|
||||
@PostMapping("/edit")
|
||||
public BaseResponse<Boolean> editPicture(@RequestBody PictureEditRequest pictureEditRequest, HttpServletRequest request) {
|
||||
if (pictureEditRequest == null || pictureEditRequest.getId() <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
pictureService.editPicture(pictureEditRequest, loginUser);
|
||||
return ResultUtils.success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/tag_category")
|
||||
public BaseResponse<PictureTagCategory> listPictureTagCategory() {
|
||||
@ -244,4 +299,69 @@ public class PictureController {
|
||||
int uploadCount = pictureService.uploadPictureByBatch(pictureUploadByBatchRequest, loginUser);
|
||||
return ResultUtils.success(uploadCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 以图搜图
|
||||
*/
|
||||
@PostMapping("/search/picture")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_VIEW)
|
||||
public BaseResponse<List<ImageSearchResult>> getSimilarPicture(@RequestBody SearchPictureByPictureRequest request) throws IOException {
|
||||
ThrowUtils.throwIf(request == null, ErrorCode.NO_AUTH_ERROR);
|
||||
List<ImageSearchResult> similarImage = pictureService.getSimilarPicture(request);
|
||||
return ResultUtils.success(similarImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按照颜色搜索
|
||||
*/
|
||||
@PostMapping("/search/color")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_VIEW)
|
||||
public BaseResponse<List<PictureVO>> searchPictureByColor(@RequestBody SearchPictureByColorRequest searchPictureByColorRequest, HttpServletRequest request) {
|
||||
ThrowUtils.throwIf(searchPictureByColorRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
String picColor = searchPictureByColorRequest.getPicColor();
|
||||
Long spaceId = searchPictureByColorRequest.getSpaceId();
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
List<PictureVO> pictureVOList = pictureService.searchPictureByColor(spaceId, picColor, loginUser);
|
||||
return ResultUtils.success(pictureVOList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对私人/团队空间批量编辑图片
|
||||
*/
|
||||
@PostMapping("/edit/batch")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_EDIT)
|
||||
public BaseResponse<Boolean> editPictureByBatch(@RequestBody PictureEditByBatchRequest pictureEditByBatchRequest, HttpServletRequest request) {
|
||||
ThrowUtils.throwIf(pictureEditByBatchRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
pictureService.editPictureByBatch(pictureEditByBatchRequest, loginUser);
|
||||
return ResultUtils.success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
*创建 AI 扩图任务
|
||||
* @param createPictureOutPaintingTaskRequest
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/out_painting/create_task")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_EDIT)
|
||||
public BaseResponse<CreateOutPaintingTaskResponse> createPictureOutPaintingTask(@RequestBody CreatePictureOutPaintingTaskRequest createPictureOutPaintingTaskRequest,
|
||||
HttpServletRequest request) {
|
||||
if (createPictureOutPaintingTaskRequest == null || createPictureOutPaintingTaskRequest.getPictureId() == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
CreateOutPaintingTaskResponse response = pictureService.createPictureOutPaintingTask(createPictureOutPaintingTaskRequest, loginUser);
|
||||
return ResultUtils.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 AI 扩图任务 ,前端轮询结果,当结果中有SUCCESS,那么同时取出该响应中的结果。
|
||||
*/
|
||||
@GetMapping("/out_painting/get_task")
|
||||
public BaseResponse<GetOutPaintingTaskResponse> getPictureOutPaintingTask(String taskId) {
|
||||
ThrowUtils.throwIf(StrUtil.isBlank(taskId), ErrorCode.PARAMS_ERROR);
|
||||
GetOutPaintingTaskResponse task = aliYunAiApi.getOutPaintingTask(taskId);
|
||||
return ResultUtils.success(task);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,133 @@
|
||||
package edu.whut.smilepicturebackend.controller;
|
||||
import edu.whut.smilepicturebackend.common.BaseResponse;
|
||||
import edu.whut.smilepicturebackend.common.ResultUtils;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import edu.whut.smilepicturebackend.exception.ThrowUtils;
|
||||
import edu.whut.smilepicturebackend.model.dto.space.analyze.*;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
import edu.whut.smilepicturebackend.model.vo.space.analyze.*;
|
||||
import edu.whut.smilepicturebackend.service.SpaceAnalyzeService;
|
||||
import edu.whut.smilepicturebackend.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author 程序员鱼皮 <a href="https://www.codefather.cn">编程导航原创项目</a>
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/space/analyze")
|
||||
public class SpaceAnalyzeController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
private final SpaceAnalyzeService spaceAnalyzeService;
|
||||
|
||||
/**
|
||||
* 获取空间的使用状态
|
||||
*
|
||||
* @param spaceUsageAnalyzeRequest
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/usage")
|
||||
public BaseResponse<SpaceUsageAnalyzeResponse> getSpaceUsageAnalyze(
|
||||
@RequestBody SpaceUsageAnalyzeRequest spaceUsageAnalyzeRequest,
|
||||
HttpServletRequest request) {
|
||||
ThrowUtils.throwIf(spaceUsageAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
SpaceUsageAnalyzeResponse spaceUsageAnalyze = spaceAnalyzeService.getSpaceUsageAnalyze(spaceUsageAnalyzeRequest, loginUser);
|
||||
return ResultUtils.success(spaceUsageAnalyze);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取空间图片分类分析
|
||||
*
|
||||
* @param spaceCategoryAnalyzeRequest
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/category")
|
||||
public BaseResponse<List<SpaceCategoryAnalyzeResponse>> getSpaceCategoryAnalyze(
|
||||
@RequestBody SpaceCategoryAnalyzeRequest spaceCategoryAnalyzeRequest,
|
||||
HttpServletRequest request) {
|
||||
ThrowUtils.throwIf(spaceCategoryAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
List<SpaceCategoryAnalyzeResponse> spaceCategoryAnalyze = spaceAnalyzeService.getSpaceCategoryAnalyze(spaceCategoryAnalyzeRequest, loginUser);
|
||||
return ResultUtils.success(spaceCategoryAnalyze);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取空间图片标签分析
|
||||
*
|
||||
* @param spaceTagAnalyzeRequest
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/tag")
|
||||
public BaseResponse<List<SpaceTagAnalyzeResponse>> getSpaceTagAnalyze(
|
||||
@RequestBody SpaceTagAnalyzeRequest spaceTagAnalyzeRequest,
|
||||
HttpServletRequest request) {
|
||||
ThrowUtils.throwIf(spaceTagAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
List<SpaceTagAnalyzeResponse> spaceTagAnalyze = spaceAnalyzeService.getSpaceTagAnalyze(spaceTagAnalyzeRequest, loginUser);
|
||||
return ResultUtils.success(spaceTagAnalyze);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取空间图片大小分析
|
||||
*
|
||||
* @param spaceSizeAnalyzeRequest
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/size")
|
||||
public BaseResponse<List<SpaceSizeAnalyzeResponse>> getSpaceSizeAnalyze(@RequestBody SpaceSizeAnalyzeRequest spaceSizeAnalyzeRequest,
|
||||
HttpServletRequest request) {
|
||||
ThrowUtils.throwIf(spaceSizeAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
List<SpaceSizeAnalyzeResponse> resultList = spaceAnalyzeService.getSpaceSizeAnalyze(spaceSizeAnalyzeRequest, loginUser);
|
||||
return ResultUtils.success(resultList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取空间用户行为分析
|
||||
*
|
||||
* @param spaceUserAnalyzeRequest
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/user")
|
||||
public BaseResponse<List<SpaceUserAnalyzeResponse>> getSpaceUserAnalyze(@RequestBody SpaceUserAnalyzeRequest spaceUserAnalyzeRequest,
|
||||
HttpServletRequest request) {
|
||||
ThrowUtils.throwIf(spaceUserAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
List<SpaceUserAnalyzeResponse> resultList = spaceAnalyzeService.getSpaceUserAnalyze(spaceUserAnalyzeRequest, loginUser);
|
||||
return ResultUtils.success(resultList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取空间使用排行分析
|
||||
*
|
||||
* @param spaceRankAnalyzeRequest
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/rank")
|
||||
public BaseResponse<List<Space>> getSpaceRankAnalyze(@RequestBody SpaceRankAnalyzeRequest spaceRankAnalyzeRequest,
|
||||
HttpServletRequest request) {
|
||||
ThrowUtils.throwIf(spaceRankAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
List<Space> resultList = spaceAnalyzeService.getSpaceRankAnalyze(spaceRankAnalyzeRequest, loginUser);
|
||||
return ResultUtils.success(resultList);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,213 @@
|
||||
package edu.whut.smilepicturebackend.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import edu.whut.smilepicturebackend.annotation.AuthCheck;
|
||||
import edu.whut.smilepicturebackend.common.BaseResponse;
|
||||
import edu.whut.smilepicturebackend.common.DeleteRequest;
|
||||
import edu.whut.smilepicturebackend.common.ResultUtils;
|
||||
import edu.whut.smilepicturebackend.constant.UserConstant;
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import edu.whut.smilepicturebackend.exception.ThrowUtils;
|
||||
import edu.whut.smilepicturebackend.manager.auth.SpaceUserAuthManager;
|
||||
import edu.whut.smilepicturebackend.model.dto.space.*;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
import edu.whut.smilepicturebackend.model.enums.SpaceLevelEnum;
|
||||
import edu.whut.smilepicturebackend.model.vo.SpaceVO;
|
||||
import edu.whut.smilepicturebackend.service.SpaceService;
|
||||
import edu.whut.smilepicturebackend.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author 程序员鱼皮 <a href="https://www.codefather.cn">编程导航原创项目</a>
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/space")
|
||||
public class SpaceController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
private final SpaceService spaceService;
|
||||
|
||||
private final SpaceUserAuthManager spaceUserAuthManager;
|
||||
|
||||
@PostMapping("/add")
|
||||
public BaseResponse<Long> addSpace(@RequestBody SpaceAddRequest spaceAddRequest, HttpServletRequest request) {
|
||||
ThrowUtils.throwIf(spaceAddRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
long newId = spaceService.addSpace(spaceAddRequest, loginUser);
|
||||
return ResultUtils.success(newId);
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
public BaseResponse<Boolean> deleteSpace(@RequestBody DeleteRequest deleteRequest
|
||||
, HttpServletRequest request) {
|
||||
if (deleteRequest == null || deleteRequest.getId() <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
Long id = deleteRequest.getId();
|
||||
// 判断是否存在
|
||||
Space oldSpace = spaceService.getById(id);
|
||||
ThrowUtils.throwIf(oldSpace == null, ErrorCode.NOT_FOUND_ERROR);
|
||||
// 仅本人或者管理员可删除
|
||||
spaceService.checkSpaceAuth(loginUser, oldSpace);
|
||||
// 操作数据库
|
||||
boolean result = spaceService.removeById(id);
|
||||
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
|
||||
return ResultUtils.success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新空间(仅管理员可用)
|
||||
*
|
||||
* @param spaceUpdateRequest
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/update")
|
||||
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<Boolean> updateSpace(@RequestBody SpaceUpdateRequest spaceUpdateRequest,
|
||||
HttpServletRequest request) {
|
||||
if (spaceUpdateRequest == null || spaceUpdateRequest.getId() <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
// 将实体类和 DTO 进行转换
|
||||
Space space = new Space();
|
||||
BeanUtils.copyProperties(spaceUpdateRequest, space);
|
||||
// 自动填充数据
|
||||
spaceService.fillSpaceBySpaceLevel(space);
|
||||
// 数据校验
|
||||
spaceService.validSpace(space, false); //不是新增时的操作
|
||||
// 判断是否存在
|
||||
long id = spaceUpdateRequest.getId();
|
||||
Space oldSpace = spaceService.getById(id);
|
||||
ThrowUtils.throwIf(oldSpace == null, ErrorCode.NOT_FOUND_ERROR);
|
||||
// 操作数据库
|
||||
boolean result = spaceService.updateById(space);
|
||||
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
|
||||
return ResultUtils.success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 id 获取空间(仅管理员可用)
|
||||
*/
|
||||
@GetMapping("/get")
|
||||
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<Space> getSpaceById(long id, HttpServletRequest request) {
|
||||
ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);
|
||||
// 查询数据库
|
||||
Space space = spaceService.getById(id);
|
||||
ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR);
|
||||
// 获取封装类
|
||||
return ResultUtils.success(space);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 id 获取空间(封装类)
|
||||
*/
|
||||
@GetMapping("/get/vo")
|
||||
public BaseResponse<SpaceVO> getSpaceVOById(long id, HttpServletRequest request) {
|
||||
ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);
|
||||
// 查询数据库
|
||||
Space space = spaceService.getById(id);
|
||||
ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR);
|
||||
SpaceVO spaceVO = spaceService.getSpaceVO(space, request);
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
List<String> permissionList = spaceUserAuthManager.getPermissionList(space, loginUser);
|
||||
spaceVO.setPermissionList(permissionList);
|
||||
return ResultUtils.success(spaceVO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页获取空间列表(仅管理员可用)
|
||||
*/
|
||||
@PostMapping("/list/page")
|
||||
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<Page<Space>> listSpaceByPage(@RequestBody SpaceQueryRequest spaceQueryRequest) {
|
||||
long current = spaceQueryRequest.getCurrent();
|
||||
long size = spaceQueryRequest.getPageSize();
|
||||
// 查询数据库
|
||||
Page<Space> spacePage = spaceService.page(new Page<>(current, size),
|
||||
spaceService.getQueryWrapper(spaceQueryRequest));
|
||||
return ResultUtils.success(spacePage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页获取空间列表(封装类) 用户使用
|
||||
*/
|
||||
@PostMapping("/list/page/vo")
|
||||
public BaseResponse<Page<SpaceVO>> listSpaceVOByPage(@RequestBody SpaceQueryRequest spaceQueryRequest,
|
||||
HttpServletRequest request) {
|
||||
long current = spaceQueryRequest.getCurrent();
|
||||
long size = spaceQueryRequest.getPageSize();
|
||||
// 限制爬虫
|
||||
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
|
||||
// 查询数据库
|
||||
Page<Space> spacePage = spaceService.page(new Page<>(current, size),
|
||||
spaceService.getQueryWrapper(spaceQueryRequest));
|
||||
// 获取封装类
|
||||
return ResultUtils.success(spaceService.getSpaceVOPage(spacePage, request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑空间(给用户使用)
|
||||
*/
|
||||
@PostMapping("/edit")
|
||||
public BaseResponse<Boolean> editSpace(@RequestBody SpaceEditRequest spaceEditRequest, HttpServletRequest request) {
|
||||
if (spaceEditRequest == null || spaceEditRequest.getId() <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
// 在此处将实体类和 DTO 进行转换
|
||||
Space space = new Space();
|
||||
BeanUtils.copyProperties(spaceEditRequest, space);
|
||||
// 自动填充数据
|
||||
spaceService.fillSpaceBySpaceLevel(space);
|
||||
// 设置编辑时间
|
||||
space.setEditTime(new Date());
|
||||
// 数据校验
|
||||
spaceService.validSpace(space, false);
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
// 判断是否存在
|
||||
long id = spaceEditRequest.getId();
|
||||
Space oldSpace = spaceService.getById(id);
|
||||
ThrowUtils.throwIf(oldSpace == null, ErrorCode.NOT_FOUND_ERROR);
|
||||
// 仅本人或管理员可编辑
|
||||
spaceService.checkSpaceAuth(loginUser, oldSpace);
|
||||
// 操作数据库
|
||||
boolean result = spaceService.updateById(space);
|
||||
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
|
||||
return ResultUtils.success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取空间级别列表,便于前端展示
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/list/level")
|
||||
public BaseResponse<List<SpaceLevel>> listSpaceLevel() {
|
||||
List<SpaceLevel> spaceLevelList = Arrays.stream(SpaceLevelEnum.values())
|
||||
.map(spaceLevelEnum -> new SpaceLevel(
|
||||
spaceLevelEnum.getValue(),
|
||||
spaceLevelEnum.getText(),
|
||||
spaceLevelEnum.getMaxCount(),
|
||||
spaceLevelEnum.getMaxSize()
|
||||
))
|
||||
.collect(Collectors.toList());
|
||||
return ResultUtils.success(spaceLevelList);
|
||||
}
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
package edu.whut.smilepicturebackend.controller;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
|
||||
import edu.whut.smilepicturebackend.common.BaseResponse;
|
||||
import edu.whut.smilepicturebackend.common.DeleteRequest;
|
||||
import edu.whut.smilepicturebackend.common.ResultUtils;
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import edu.whut.smilepicturebackend.exception.ThrowUtils;
|
||||
import edu.whut.smilepicturebackend.manager.auth.annotation.SaSpaceCheckPermission;
|
||||
import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserPermissionConstant;
|
||||
import edu.whut.smilepicturebackend.model.dto.spaceuser.SpaceUserAddRequest;
|
||||
import edu.whut.smilepicturebackend.model.dto.spaceuser.SpaceUserEditRequest;
|
||||
import edu.whut.smilepicturebackend.model.dto.spaceuser.SpaceUserQueryRequest;
|
||||
import edu.whut.smilepicturebackend.model.entity.SpaceUser;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
import edu.whut.smilepicturebackend.model.vo.SpaceUserVO;
|
||||
import edu.whut.smilepicturebackend.service.SpaceUserService;
|
||||
import edu.whut.smilepicturebackend.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 空间成员管理
|
||||
*/
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/spaceUser")
|
||||
@Slf4j
|
||||
public class SpaceUserController {
|
||||
|
||||
private final SpaceUserService spaceUserService;
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
/**
|
||||
* 添加成员到空间
|
||||
*/
|
||||
@PostMapping("/add")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.SPACE_USER_MANAGE)
|
||||
public BaseResponse<Long> addSpaceUser(@RequestBody SpaceUserAddRequest spaceUserAddRequest, HttpServletRequest request) {
|
||||
ThrowUtils.throwIf(spaceUserAddRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
long id = spaceUserService.addSpaceUser(spaceUserAddRequest);
|
||||
return ResultUtils.success(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从空间移除成员
|
||||
*/
|
||||
@PostMapping("/delete")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.SPACE_USER_MANAGE)
|
||||
public BaseResponse<Boolean> deleteSpaceUser(@RequestBody DeleteRequest deleteRequest,
|
||||
HttpServletRequest request) {
|
||||
if (deleteRequest == null || deleteRequest.getId() <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
long id = deleteRequest.getId();
|
||||
// 判断是否存在
|
||||
SpaceUser oldSpaceUser = spaceUserService.getById(id);
|
||||
ThrowUtils.throwIf(oldSpaceUser == null, ErrorCode.NOT_FOUND_ERROR);
|
||||
// 操作数据库
|
||||
boolean result = spaceUserService.removeById(id);
|
||||
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
|
||||
return ResultUtils.success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询某个成员在某个空间的信息
|
||||
*/
|
||||
@PostMapping("/get")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.SPACE_USER_MANAGE)
|
||||
public BaseResponse<SpaceUser> getSpaceUser(@RequestBody SpaceUserQueryRequest spaceUserQueryRequest) {
|
||||
// 参数校验
|
||||
ThrowUtils.throwIf(spaceUserQueryRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
Long spaceId = spaceUserQueryRequest.getSpaceId();
|
||||
Long userId = spaceUserQueryRequest.getUserId();
|
||||
ThrowUtils.throwIf(ObjectUtil.hasEmpty(spaceId, userId), ErrorCode.PARAMS_ERROR);
|
||||
// 查询数据库
|
||||
SpaceUser spaceUser = spaceUserService.getOne(spaceUserService.getQueryWrapper(spaceUserQueryRequest));
|
||||
ThrowUtils.throwIf(spaceUser == null, ErrorCode.NOT_FOUND_ERROR);
|
||||
return ResultUtils.success(spaceUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询成员信息列表
|
||||
*/
|
||||
@PostMapping("/list")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.SPACE_USER_MANAGE)
|
||||
public BaseResponse<List<SpaceUserVO>> listSpaceUser(@RequestBody SpaceUserQueryRequest spaceUserQueryRequest,
|
||||
HttpServletRequest request) {
|
||||
ThrowUtils.throwIf(spaceUserQueryRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
List<SpaceUser> spaceUserList = spaceUserService.list(
|
||||
spaceUserService.getQueryWrapper(spaceUserQueryRequest)
|
||||
);
|
||||
return ResultUtils.success(spaceUserService.getSpaceUserVOList(spaceUserList));
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑成员信息(设置权限)
|
||||
*/
|
||||
@PostMapping("/edit")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.SPACE_USER_MANAGE)
|
||||
public BaseResponse<Boolean> editSpaceUser(@RequestBody SpaceUserEditRequest spaceUserEditRequest,
|
||||
HttpServletRequest request) {
|
||||
if (spaceUserEditRequest == null || spaceUserEditRequest.getId() <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
}
|
||||
// 将实体类和 DTO 进行转换
|
||||
SpaceUser spaceUser = new SpaceUser();
|
||||
BeanUtils.copyProperties(spaceUserEditRequest, spaceUser);
|
||||
// 数据校验
|
||||
spaceUserService.validSpaceUser(spaceUser, false);
|
||||
// 判断是否存在
|
||||
long id = spaceUserEditRequest.getId();
|
||||
SpaceUser oldSpaceUser = spaceUserService.getById(id);
|
||||
ThrowUtils.throwIf(oldSpaceUser == null, ErrorCode.NOT_FOUND_ERROR);
|
||||
// 操作数据库
|
||||
boolean result = spaceUserService.updateById(spaceUser);
|
||||
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
|
||||
return ResultUtils.success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询我加入的团队空间列表
|
||||
*/
|
||||
@PostMapping("/list/my")
|
||||
public BaseResponse<List<SpaceUserVO>> listMyTeamSpace(HttpServletRequest request) {
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
SpaceUserQueryRequest spaceUserQueryRequest = new SpaceUserQueryRequest();
|
||||
spaceUserQueryRequest.setUserId(loginUser.getId());
|
||||
List<SpaceUser> spaceUserList = spaceUserService.list(
|
||||
spaceUserService.getQueryWrapper(spaceUserQueryRequest)
|
||||
);
|
||||
return ResultUtils.success(spaceUserService.getSpaceUserVOList(spaceUserList));
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
package edu.whut.smilepicturebackend.exception;
|
||||
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import cn.dev33.satoken.exception.NotPermissionException;
|
||||
import edu.whut.smilepicturebackend.common.BaseResponse;
|
||||
import edu.whut.smilepicturebackend.common.ResultUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -12,6 +14,17 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
@RestControllerAdvice
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(NotLoginException.class)
|
||||
public BaseResponse<?> notLoginException(NotLoginException e) {
|
||||
log.error("NotLoginException", e);
|
||||
return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(NotPermissionException.class)
|
||||
public BaseResponse<?> notPermissionExceptionHandler(NotPermissionException e) {
|
||||
log.error("NotPermissionException", e);
|
||||
return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public BaseResponse<?> businessExceptionHandler(BusinessException e) {
|
||||
|
@ -4,13 +4,17 @@ import cn.hutool.core.util.StrUtil;
|
||||
import com.qcloud.cos.COSClient;
|
||||
import com.qcloud.cos.model.*;
|
||||
import com.qcloud.cos.model.ciModel.persistence.PicOperations;
|
||||
import com.qcloud.cos.utils.IOUtils;
|
||||
import edu.whut.smilepicturebackend.config.CosClientConfig;
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
@ -189,7 +193,7 @@ public class CosManager {
|
||||
* @param originalUrl 形如 https://bucket.cos.xxx/my/prefix/file.png
|
||||
* @return my/prefix/file.png
|
||||
*/
|
||||
public static String extractUploadPath(String originalUrl) {
|
||||
public String extractUploadPath(String originalUrl) {
|
||||
try {
|
||||
// 1. 直接拿 URI 的 path 部分,例如 "//smile-picture/public/.../xxx.png"
|
||||
String path = new URI(originalUrl).getPath();
|
||||
@ -201,4 +205,29 @@ public class CosManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将COS中的文件下载到本地
|
||||
* @param filepath 文件路径,如folder/picture.jpg
|
||||
* @param localPath 本地存储路径
|
||||
*/
|
||||
public void downloadPicture(String filepath, String localPath) throws IOException {
|
||||
File file = new File(localPath);
|
||||
COSObjectInputStream cosObjectInput = null;
|
||||
try {
|
||||
COSObject cosObject = this.getObject(filepath);
|
||||
cosObjectInput = cosObject.getObjectContent();
|
||||
// 将输入流转为字节数组
|
||||
byte[] data = IOUtils.toByteArray(cosObjectInput);
|
||||
// 将字节数组写入本地文件
|
||||
FileUtil.writeBytes(data, file);
|
||||
} catch (Exception e) {
|
||||
log.error("file download error, filepath = {}", filepath, e);
|
||||
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "下载失败");
|
||||
} finally {
|
||||
if (cosObjectInput != null) {
|
||||
cosObjectInput.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,48 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth;
|
||||
import edu.whut.smilepicturebackend.model.entity.Picture;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
import edu.whut.smilepicturebackend.model.entity.SpaceUser;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* SpaceUserAuthContext
|
||||
* 表示用户在特定空间内的授权上下文,包括关联的图片、空间和用户信息。
|
||||
*/
|
||||
@Data
|
||||
public class SpaceUserAuthContext {
|
||||
|
||||
/**
|
||||
* 临时参数,不同请求对应的 id 可能不同
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 图片 ID
|
||||
*/
|
||||
private Long pictureId;
|
||||
|
||||
/**
|
||||
* 空间 ID
|
||||
*/
|
||||
private Long spaceId;
|
||||
|
||||
/**
|
||||
* 空间用户 ID
|
||||
*/
|
||||
private Long spaceUserId;
|
||||
|
||||
/**
|
||||
* 图片信息
|
||||
*/
|
||||
private Picture picture;
|
||||
|
||||
/**
|
||||
* 空间信息
|
||||
*/
|
||||
private Space space;
|
||||
|
||||
/**
|
||||
* 空间用户信息
|
||||
*/
|
||||
private SpaceUser spaceUser;
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth;
|
||||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserAuthConfig;
|
||||
import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserPermissionConstant;
|
||||
import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserRole;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
import edu.whut.smilepicturebackend.model.entity.SpaceUser;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
import edu.whut.smilepicturebackend.model.enums.SpaceRoleEnum;
|
||||
import edu.whut.smilepicturebackend.model.enums.SpaceTypeEnum;
|
||||
import edu.whut.smilepicturebackend.service.SpaceUserService;
|
||||
import edu.whut.smilepicturebackend.service.UserService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 空间成员权限管理
|
||||
*/
|
||||
@Component
|
||||
public class SpaceUserAuthManager {
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
@Resource
|
||||
private SpaceUserService spaceUserService;
|
||||
|
||||
public static final SpaceUserAuthConfig SPACE_USER_AUTH_CONFIG;
|
||||
|
||||
static {
|
||||
String json = ResourceUtil.readUtf8Str("biz/spaceUserAuthConfig.json");
|
||||
SPACE_USER_AUTH_CONFIG = JSONUtil.toBean(json, SpaceUserAuthConfig.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据角色获取权限列表
|
||||
*
|
||||
* @param spaceUserRole
|
||||
* @return
|
||||
*/
|
||||
public List<String> getPermissionsByRole(String spaceUserRole) {
|
||||
if (StrUtil.isBlank(spaceUserRole)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
SpaceUserRole role = SPACE_USER_AUTH_CONFIG.getRoles()
|
||||
.stream()
|
||||
.filter(r -> r.getKey().equals(spaceUserRole))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (role == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return role.getPermissions();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取权限列表
|
||||
*
|
||||
* @param space
|
||||
* @param loginUser
|
||||
* @return
|
||||
*/
|
||||
public List<String> getPermissionList(Space space, User loginUser) {
|
||||
if (loginUser == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
// 管理员权限
|
||||
List<String> ADMIN_PERMISSIONS = getPermissionsByRole(SpaceRoleEnum.ADMIN.getValue());
|
||||
// 公共图库
|
||||
if (space == null) {
|
||||
if (userService.isAdmin(loginUser)) {
|
||||
return ADMIN_PERMISSIONS;
|
||||
}
|
||||
return Collections.singletonList(SpaceUserPermissionConstant.PICTURE_VIEW);
|
||||
}
|
||||
SpaceTypeEnum spaceTypeEnum = SpaceTypeEnum.getEnumByValue(space.getSpaceType());
|
||||
if (spaceTypeEnum == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
// 根据空间获取对应的权限
|
||||
switch (spaceTypeEnum) {
|
||||
case PRIVATE:
|
||||
// 私有空间,仅本人或管理员有所有权限
|
||||
if (space.getUserId().equals(loginUser.getId()) || userService.isAdmin(loginUser)) {
|
||||
return ADMIN_PERMISSIONS;
|
||||
} else {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
case TEAM:
|
||||
// 团队空间,查询 SpaceUser 并获取角色和权限
|
||||
SpaceUser spaceUser = spaceUserService.lambdaQuery()
|
||||
.eq(SpaceUser::getSpaceId, space.getId())
|
||||
.eq(SpaceUser::getUserId, loginUser.getId())
|
||||
.one();
|
||||
if (spaceUser == null) {
|
||||
return new ArrayList<>();
|
||||
} else {
|
||||
return getPermissionsByRole(spaceUser.getSpaceRole());
|
||||
}
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
@ -0,0 +1,226 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth;
|
||||
|
||||
import cn.dev33.satoken.stp.StpInterface;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
import cn.hutool.http.ContentType;
|
||||
import cn.hutool.http.Header;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserPermissionConstant;
|
||||
import edu.whut.smilepicturebackend.model.entity.Picture;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
import edu.whut.smilepicturebackend.model.entity.SpaceUser;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
import edu.whut.smilepicturebackend.model.enums.SpaceRoleEnum;
|
||||
import edu.whut.smilepicturebackend.model.enums.SpaceTypeEnum;
|
||||
import edu.whut.smilepicturebackend.service.PictureService;
|
||||
import edu.whut.smilepicturebackend.service.SpaceService;
|
||||
import edu.whut.smilepicturebackend.service.SpaceUserService;
|
||||
import edu.whut.smilepicturebackend.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.*;
|
||||
|
||||
import static edu.whut.smilepicturebackend.constant.UserConstant.USER_LOGIN_STATE;
|
||||
|
||||
|
||||
/**
|
||||
* 自定义权限加载接口实现类
|
||||
*/
|
||||
@Component // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展
|
||||
@RequiredArgsConstructor
|
||||
public class StpInterfaceImpl implements StpInterface {
|
||||
|
||||
// 默认是 /api
|
||||
@Value("${server.servlet.context-path}")
|
||||
private String contextPath;
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
|
||||
private final SpaceService spaceService;
|
||||
|
||||
|
||||
private final SpaceUserService spaceUserService;
|
||||
|
||||
private final PictureService pictureService;
|
||||
|
||||
|
||||
private final SpaceUserAuthManager spaceUserAuthManager;
|
||||
|
||||
/**
|
||||
* 返回一个账号所拥有的权限码集合
|
||||
*/
|
||||
@Override
|
||||
public List<String> getPermissionList(Object loginId, String loginType) {
|
||||
// 判断 loginType,仅对类型为 "space" 进行权限校验
|
||||
if (!StpKit.SPACE_TYPE.equals(loginType)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
// 管理员权限,表示权限校验通过
|
||||
List<String> ADMIN_PERMISSIONS = spaceUserAuthManager.getPermissionsByRole(SpaceRoleEnum.ADMIN.getValue());
|
||||
// 获取上下文对象
|
||||
SpaceUserAuthContext authContext = getAuthContextByRequest();
|
||||
// 如果所有字段都为空,表示查询公共图库,可以通过
|
||||
if (isAllFieldsNull(authContext)) {
|
||||
return ADMIN_PERMISSIONS;
|
||||
}
|
||||
// 获取 userId 前面登录的时候把信息存进去过,现在取出来。
|
||||
User loginUser = (User) StpKit.SPACE.getSessionByLoginId(loginId).get(USER_LOGIN_STATE);
|
||||
if (loginUser == null) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "用户未登录");
|
||||
}
|
||||
Long userId = loginUser.getId();
|
||||
// 优先从上下文中获取 SpaceUser 对象
|
||||
SpaceUser spaceUser = authContext.getSpaceUser();
|
||||
if (spaceUser != null) {
|
||||
return spaceUserAuthManager.getPermissionsByRole(spaceUser.getSpaceRole());
|
||||
}
|
||||
// 如果有 spaceUserId,必然是团队空间,通过数据库查询 SpaceUser 对象
|
||||
Long spaceUserId = authContext.getSpaceUserId();
|
||||
if (spaceUserId != null) {
|
||||
spaceUser = spaceUserService.getById(spaceUserId);
|
||||
if (spaceUser == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未找到空间用户信息");
|
||||
}
|
||||
// 取出当前登录用户对应的 spaceUser
|
||||
SpaceUser loginSpaceUser = spaceUserService.lambdaQuery()
|
||||
.eq(SpaceUser::getSpaceId, spaceUser.getSpaceId())
|
||||
.eq(SpaceUser::getUserId, userId)
|
||||
.one();
|
||||
if (loginSpaceUser == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
// 这里会导致管理员在私有空间没有权限,可以再查一次库处理
|
||||
return spaceUserAuthManager.getPermissionsByRole(loginSpaceUser.getSpaceRole());
|
||||
}
|
||||
// 如果没有 spaceUserId,尝试通过 spaceId 或 pictureId 获取 Space 对象并处理
|
||||
Long spaceId = authContext.getSpaceId();
|
||||
if (spaceId == null) {
|
||||
// 如果没有 spaceId,通过 pictureId 获取 Picture 对象和 Space 对象
|
||||
Long pictureId = authContext.getPictureId();
|
||||
// 图片 id 也没有,则默认通过权限校验
|
||||
if (pictureId == null) {
|
||||
return ADMIN_PERMISSIONS;
|
||||
}
|
||||
Picture picture = pictureService.lambdaQuery()
|
||||
.eq(Picture::getId, pictureId)
|
||||
.select(Picture::getId, Picture::getSpaceId, Picture::getUserId)
|
||||
.one();
|
||||
if (picture == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未找到图片信息");
|
||||
}
|
||||
spaceId = picture.getSpaceId();
|
||||
// 公共图库,仅本人或管理员可操作
|
||||
if (spaceId == null) {
|
||||
if (picture.getUserId().equals(userId) || userService.isAdmin(loginUser)) {
|
||||
return ADMIN_PERMISSIONS;
|
||||
} else {
|
||||
// 不是自己的图片,仅可查看
|
||||
return Collections.singletonList(SpaceUserPermissionConstant.PICTURE_VIEW);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 获取 Space 对象
|
||||
Space space = spaceService.getById(spaceId);
|
||||
if (space == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未找到空间信息");
|
||||
}
|
||||
// 根据 Space 类型判断权限
|
||||
if (space.getSpaceType() == SpaceTypeEnum.PRIVATE.getValue()) {
|
||||
// 私有空间,仅本人或管理员有权限
|
||||
if (space.getUserId().equals(userId) || userService.isAdmin(loginUser)) {
|
||||
return ADMIN_PERMISSIONS;
|
||||
} else {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
} else {
|
||||
// 团队空间,查询 SpaceUser 并获取角色和权限
|
||||
spaceUser = spaceUserService.lambdaQuery()
|
||||
.eq(SpaceUser::getSpaceId, spaceId)
|
||||
.eq(SpaceUser::getUserId, userId)
|
||||
.one();
|
||||
if (spaceUser == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return spaceUserAuthManager.getPermissionsByRole(spaceUser.getSpaceRole());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 本项目中不使用。返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
|
||||
*/
|
||||
@Override
|
||||
public List<String> getRoleList(Object loginId, String loginType) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中获取上下文对象
|
||||
*/
|
||||
private SpaceUserAuthContext getAuthContextByRequest() {
|
||||
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
|
||||
String contentType = request.getHeader(Header.CONTENT_TYPE.getValue());
|
||||
SpaceUserAuthContext authRequest;
|
||||
// 获取请求参数
|
||||
if (ContentType.JSON.getValue().equals(contentType)) {
|
||||
String body = ServletUtil.getBody(request);
|
||||
authRequest = JSONUtil.toBean(body, SpaceUserAuthContext.class);
|
||||
} else {
|
||||
Map<String, String> paramMap = ServletUtil.getParamMap(request);
|
||||
authRequest = BeanUtil.toBean(paramMap, SpaceUserAuthContext.class);
|
||||
}
|
||||
// 根据请求路径区分 id 字段的含义
|
||||
Long id = authRequest.getId();
|
||||
if (ObjUtil.isNotNull(id)) {
|
||||
// 获取到请求路径的业务前缀,/api/picture/aaa?a=1
|
||||
String requestURI = request.getRequestURI();
|
||||
// 先替换掉上下文,剩下的就是前缀
|
||||
String partURI = requestURI.replace(contextPath + "/", "");
|
||||
// 获取前缀的第一个斜杠前的字符串
|
||||
String moduleName = StrUtil.subBefore(partURI, "/", false);
|
||||
switch (moduleName) {
|
||||
case "picture":
|
||||
authRequest.setPictureId(id);
|
||||
break;
|
||||
case "spaceUser":
|
||||
authRequest.setSpaceUserId(id);
|
||||
break;
|
||||
case "space":
|
||||
authRequest.setSpaceId(id);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
return authRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断对象的所有字段是否为空
|
||||
*
|
||||
* @param object
|
||||
* @return
|
||||
*/
|
||||
private boolean isAllFieldsNull(Object object) {
|
||||
if (object == null) {
|
||||
return true; // 对象本身为空
|
||||
}
|
||||
// 获取所有字段并判断是否所有字段都为空
|
||||
return Arrays.stream(ReflectUtil.getFields(object.getClass()))
|
||||
// 获取字段值
|
||||
.map(field -> ReflectUtil.getFieldValue(object, field))
|
||||
// 检查是否所有字段都为空
|
||||
.allMatch(ObjectUtil::isEmpty);
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth;
|
||||
|
||||
import cn.dev33.satoken.stp.StpLogic;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* StpLogic 门面类,管理项目中所有的 StpLogic 账号体系
|
||||
* 添加 @Component 注解的目的是确保静态属性 DEFAULT 和 SPACE 被初始化
|
||||
*/
|
||||
@Component
|
||||
public class StpKit {
|
||||
|
||||
public static final String SPACE_TYPE = "space";
|
||||
|
||||
/**
|
||||
* 默认原生会话对象,项目中目前没使用到
|
||||
*/
|
||||
public static final StpLogic DEFAULT = StpUtil.stpLogic;
|
||||
|
||||
/**
|
||||
* Space 会话对象,管理 Space 表所有账号的登录、权限认证
|
||||
*/
|
||||
public static final StpLogic SPACE = new StpLogic(SPACE_TYPE);
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth.annotation;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
|
||||
import edu.whut.smilepicturebackend.manager.auth.StpKit;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 空间权限认证:必须具有指定权限才能进入该方法
|
||||
* <p> 可标注在函数、类上(效果等同于标注在此类的所有方法上)
|
||||
*/
|
||||
@SaCheckPermission(type = StpKit.SPACE_TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
public @interface SaSpaceCheckPermission {
|
||||
|
||||
/**
|
||||
* 需要校验的权限码
|
||||
*
|
||||
* @return 需要校验的权限码
|
||||
*/
|
||||
@AliasFor(annotation = SaCheckPermission.class)
|
||||
String[] value() default {};
|
||||
|
||||
/**
|
||||
* 验证模式:AND | OR,默认AND
|
||||
*
|
||||
* @return 验证模式
|
||||
*/
|
||||
@AliasFor(annotation = SaCheckPermission.class)
|
||||
SaMode mode() default SaMode.AND;
|
||||
|
||||
/**
|
||||
* 在权限校验不通过时的次要选择,两者只要其一校验成功即可通过校验
|
||||
*
|
||||
* <p>
|
||||
* 例1:@SaCheckPermission(value="user-add", orRole="admin"),
|
||||
* 代表本次请求只要具有 user-add权限 或 admin角色 其一即可通过校验。
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* 例2: orRole = {"admin", "manager", "staff"},具有三个角色其一即可。 <br>
|
||||
* 例3: orRole = {"admin, manager, staff"},必须三个角色同时具备。
|
||||
* </p>
|
||||
*
|
||||
* @return /
|
||||
*/
|
||||
@AliasFor(annotation = SaCheckPermission.class)
|
||||
String[] orRole() default {};
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth.annotation;
|
||||
|
||||
import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||
import cn.dev33.satoken.strategy.SaAnnotationStrategy;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
/**
|
||||
* Sa-Token 开启注解和配置
|
||||
*/
|
||||
@Configuration
|
||||
public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
|
||||
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
|
||||
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void rewriteSaStrategy() {
|
||||
// 重写Sa-Token的注解处理器,增加注解合并功能
|
||||
SaAnnotationStrategy.instance.getAnnotation = (element, annotationClass) -> {
|
||||
return AnnotatedElementUtils.getMergedAnnotation(element, annotationClass);
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 空间成员权限配置
|
||||
*/
|
||||
@Data
|
||||
public class SpaceUserAuthConfig implements Serializable {
|
||||
|
||||
/**
|
||||
* 权限列表
|
||||
*/
|
||||
private List<SpaceUserPermission> permissions;
|
||||
|
||||
/**
|
||||
* 角色列表
|
||||
*/
|
||||
private List<SpaceUserRole> roles;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 空间成员权限
|
||||
*/
|
||||
@Data
|
||||
public class SpaceUserPermission implements Serializable {
|
||||
|
||||
/**
|
||||
* 权限键
|
||||
*/
|
||||
private String key;
|
||||
|
||||
/**
|
||||
* 权限名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 权限描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth.model;
|
||||
|
||||
/**
|
||||
* 空间成员权限常量
|
||||
*/
|
||||
public interface SpaceUserPermissionConstant {
|
||||
|
||||
/**
|
||||
* 空间用户管理权限
|
||||
*/
|
||||
String SPACE_USER_MANAGE = "spaceUser:manage";
|
||||
|
||||
/**
|
||||
* 图片查看权限
|
||||
*/
|
||||
String PICTURE_VIEW = "picture:view";
|
||||
|
||||
/**
|
||||
* 图片上传权限
|
||||
*/
|
||||
String PICTURE_UPLOAD = "picture:upload";
|
||||
|
||||
/**
|
||||
* 图片编辑权限
|
||||
*/
|
||||
String PICTURE_EDIT = "picture:edit";
|
||||
|
||||
/**
|
||||
* 图片删除权限
|
||||
*/
|
||||
String PICTURE_DELETE = "picture:delete";
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 空间成员角色
|
||||
*/
|
||||
@Data
|
||||
public class SpaceUserRole implements Serializable {
|
||||
|
||||
/**
|
||||
* 角色键
|
||||
*/
|
||||
private String key;
|
||||
|
||||
/**
|
||||
* 角色名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 权限键列表
|
||||
*/
|
||||
private List<String> permissions;
|
||||
|
||||
/**
|
||||
* 角色描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
package edu.whut.smilepicturebackend.manager.sharding;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.toolkit.SqlRunner;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
import edu.whut.smilepicturebackend.model.enums.SpaceLevelEnum;
|
||||
import edu.whut.smilepicturebackend.model.enums.SpaceTypeEnum;
|
||||
import edu.whut.smilepicturebackend.service.SpaceService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shardingsphere.driver.jdbc.core.connection.ShardingSphereConnection;
|
||||
import org.apache.shardingsphere.infra.metadata.database.rule.ShardingSphereRuleMetaData;
|
||||
import org.apache.shardingsphere.mode.manager.ContextManager;
|
||||
import org.apache.shardingsphere.sharding.api.config.ShardingRuleConfiguration;
|
||||
import org.apache.shardingsphere.sharding.api.config.rule.ShardingTableRuleConfiguration;
|
||||
import org.apache.shardingsphere.sharding.rule.ShardingRule;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.Resource;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
//@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class DynamicShardingManager {
|
||||
|
||||
private final DataSource dataSource;
|
||||
|
||||
private final SpaceService spaceService;
|
||||
|
||||
private static final String LOGIC_TABLE_NAME = "picture";
|
||||
|
||||
private static final String DATABASE_NAME = "logic_db"; // 配置文件中的数据库名称
|
||||
|
||||
@PostConstruct
|
||||
public void initialize() {
|
||||
log.info("初始化动态分表配置...");
|
||||
updateShardingTableNodes();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有动态表名,包括初始表 picture 和分表 picture_{spaceId}
|
||||
*/
|
||||
private Set<String> fetchAllPictureTableNames() {
|
||||
// 为了测试方便,直接对所有团队空间分表(实际上线改为仅对团队空间的旗舰版生效)
|
||||
Set<Long> spaceIds = spaceService.lambdaQuery()
|
||||
.eq(Space::getSpaceType, SpaceTypeEnum.TEAM.getValue())
|
||||
.list()
|
||||
.stream()
|
||||
.map(Space::getId)
|
||||
.collect(Collectors.toSet());
|
||||
Set<String> tableNames = spaceIds.stream()
|
||||
.map(spaceId -> LOGIC_TABLE_NAME + "_" + spaceId)
|
||||
.collect(Collectors.toSet());
|
||||
tableNames.add(LOGIC_TABLE_NAME); // 添加初始逻辑表
|
||||
return tableNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 ShardingSphere 的 actual-data-nodes 动态表名配置
|
||||
*/
|
||||
private void updateShardingTableNodes() {
|
||||
Set<String> tableNames = fetchAllPictureTableNames();
|
||||
// smile-picture.picture_112321321,smile-picture.picture_1123213123
|
||||
String newActualDataNodes = tableNames.stream()
|
||||
.map(tableName -> "smile-picture." + tableName) // 确保前缀合法
|
||||
.collect(Collectors.joining(","));
|
||||
log.info("动态分表 actual-data-nodes 配置: {}", newActualDataNodes);
|
||||
|
||||
ContextManager contextManager = getContextManager();
|
||||
ShardingSphereRuleMetaData ruleMetaData = contextManager.getMetaDataContexts()
|
||||
.getMetaData()
|
||||
.getDatabases()
|
||||
.get(DATABASE_NAME)
|
||||
.getRuleMetaData();
|
||||
|
||||
Optional<ShardingRule> shardingRule = ruleMetaData.findSingleRule(ShardingRule.class);
|
||||
if (shardingRule.isPresent()) {
|
||||
ShardingRuleConfiguration ruleConfig = (ShardingRuleConfiguration) shardingRule.get().getConfiguration();
|
||||
List<ShardingTableRuleConfiguration> updatedRules = ruleConfig.getTables()
|
||||
.stream()
|
||||
.map(oldTableRule -> {
|
||||
if (LOGIC_TABLE_NAME.equals(oldTableRule.getLogicTable())) {
|
||||
ShardingTableRuleConfiguration newTableRuleConfig = new ShardingTableRuleConfiguration(LOGIC_TABLE_NAME, newActualDataNodes);
|
||||
newTableRuleConfig.setDatabaseShardingStrategy(oldTableRule.getDatabaseShardingStrategy());
|
||||
newTableRuleConfig.setTableShardingStrategy(oldTableRule.getTableShardingStrategy());
|
||||
newTableRuleConfig.setKeyGenerateStrategy(oldTableRule.getKeyGenerateStrategy());
|
||||
newTableRuleConfig.setAuditStrategy(oldTableRule.getAuditStrategy());
|
||||
return newTableRuleConfig;
|
||||
}
|
||||
return oldTableRule;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
ruleConfig.setTables(updatedRules);
|
||||
contextManager.alterRuleConfiguration(DATABASE_NAME, Collections.singleton(ruleConfig));
|
||||
contextManager.reloadDatabase(DATABASE_NAME);
|
||||
log.info("动态分表规则更新成功!");
|
||||
} else {
|
||||
log.error("未找到 ShardingSphere 的分片规则配置,动态分表更新失败。");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态创建空间图片分表
|
||||
*
|
||||
* @param space
|
||||
*/
|
||||
public void createSpacePictureTable(Space space) {
|
||||
// 仅为旗舰版团队空间创建分表
|
||||
if (space.getSpaceType() == SpaceTypeEnum.TEAM.getValue() && space.getSpaceLevel() == SpaceLevelEnum.FLAGSHIP.getValue()) {
|
||||
Long spaceId = space.getId();
|
||||
String tableName = LOGIC_TABLE_NAME + "_" + spaceId;
|
||||
// 创建新表
|
||||
String createTableSql = "CREATE TABLE " + tableName + " LIKE " + LOGIC_TABLE_NAME; //like创建与逻辑表一模一样的表
|
||||
try {
|
||||
SqlRunner.db().update(createTableSql);
|
||||
// 更新分表
|
||||
updateShardingTableNodes();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("创建图片空间分表失败,空间 id = {}", space.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 ShardingSphere ContextManager
|
||||
*/
|
||||
private ContextManager getContextManager() {
|
||||
try (ShardingSphereConnection connection = dataSource.getConnection().unwrap(ShardingSphereConnection.class)) {
|
||||
return connection.getContextManager();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("获取 ShardingSphere ContextManager 失败", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package edu.whut.smilepicturebackend.manager.sharding;
|
||||
|
||||
import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue;
|
||||
import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue;
|
||||
import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* 图片分表算法
|
||||
*/
|
||||
public class PictureShardingAlgorithm implements StandardShardingAlgorithm<Long> {
|
||||
//availableTargetNames指实际表名集合 , preciseShardingValue这里指spaceid
|
||||
@Override
|
||||
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> preciseShardingValue) {
|
||||
Long spaceId = preciseShardingValue.getValue();
|
||||
String logicTableName = preciseShardingValue.getLogicTableName();
|
||||
// spaceId 为 null 表示查询所有图片
|
||||
if (spaceId == null) {
|
||||
return logicTableName;
|
||||
}
|
||||
// 根据 spaceId 动态生成分表名
|
||||
String realTableName = "picture_" + spaceId;
|
||||
if (availableTargetNames.contains(realTableName)) {
|
||||
return realTableName;
|
||||
} else {
|
||||
return logicTableName;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<Long> rangeShardingValue) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Properties getProps() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Properties properties) {
|
||||
|
||||
}
|
||||
}
|
@ -58,6 +58,7 @@ public abstract class PictureUploadTemplate {
|
||||
String uploadPath = String.format("/%s/%s/%s",projectName, uploadPathPrefix, uploadFilename);
|
||||
File file = null;
|
||||
try {
|
||||
log.info("uploadPath"+uploadPath);
|
||||
// 3. 创建临时文件,获取文件到服务器
|
||||
file = File.createTempFile(uploadPath, null);
|
||||
// 处理文件来源
|
||||
@ -71,7 +72,7 @@ public abstract class PictureUploadTemplate {
|
||||
List<CIObject> objectList = processResults.getObjectList();
|
||||
if (CollUtil.isNotEmpty(objectList)) {
|
||||
// 获取压缩之后得到的文件信息
|
||||
CIObject compressedCiObject = objectList.get(0); //第一个是压缩后的
|
||||
CIObject compressedCiObject = objectList.get(0); //第一个是压缩后的,webp格式
|
||||
// 缩略图默认等于压缩图,压缩图是必有的
|
||||
CIObject thumbnailCiObject = compressedCiObject;
|
||||
// 有生成缩略图,才获取缩略图
|
||||
|
@ -9,6 +9,7 @@ import cn.hutool.http.Method;
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import edu.whut.smilepicturebackend.exception.ThrowUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.File;
|
||||
@ -21,6 +22,7 @@ import java.util.List;
|
||||
* URL 图片上传
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class UrlPictureUpload extends PictureUploadTemplate {
|
||||
|
||||
@Override
|
||||
@ -46,14 +48,15 @@ public class UrlPictureUpload extends PictureUploadTemplate {
|
||||
* @return 不含点的扩展名,比如 "jpg","png",取不出时返回空串
|
||||
*/
|
||||
protected String fetchAndValidateExtension(String fileUrl) {
|
||||
log.info("收到的fileurl:{}",fileUrl);
|
||||
try (HttpResponse resp = HttpUtil
|
||||
.createRequest(Method.HEAD, fileUrl)
|
||||
.execute()) {
|
||||
if (resp.getStatus() != HttpStatus.HTTP_OK) {
|
||||
throw new BusinessException(
|
||||
ErrorCode.OPERATION_ERROR, "文件不存在或不可访问");
|
||||
// 网络或权限问题时直接降级
|
||||
log.warn("HEAD 请求未返回 200,status = {},将从 URL 中提取后缀", resp.getStatus());
|
||||
return "";
|
||||
}
|
||||
|
||||
// 1) Content-Type 验证 & 提取扩展名
|
||||
String ct = resp.header("Content-Type");
|
||||
String ext = "";
|
||||
@ -95,16 +98,21 @@ public class UrlPictureUpload extends PictureUploadTemplate {
|
||||
@Override
|
||||
protected String getOriginFilename(Object inputSource) {
|
||||
String fileUrl = (String) inputSource;
|
||||
// 1) HEAD 验证并拿扩展名(只这一处会发 HEAD)
|
||||
String ext = fetchAndValidateExtension(fileUrl);
|
||||
// 2) fallback:若服务器没返回类型,再从 URL 中简单截取
|
||||
// 先把 query string 去掉
|
||||
int qIdx = fileUrl.indexOf('?');
|
||||
String cleanUrl = qIdx > 0 ? fileUrl.substring(0, qIdx) : fileUrl;
|
||||
|
||||
// 1) 尝试 HEAD 拿扩展名,不可用时 ext == ""
|
||||
String ext = fetchAndValidateExtension(cleanUrl);
|
||||
|
||||
// 2) fallback:若 ext 为空,就直接返回去掉参数后的 URL
|
||||
if (StrUtil.isBlank(ext)) {
|
||||
ext = FileUtil.extName(fileUrl);
|
||||
return cleanUrl;
|
||||
} else {
|
||||
// 3) 从 cleanUrl 中拿到 baseName,再拼回去
|
||||
String base = FileUtil.mainName(FileUtil.getName(cleanUrl));
|
||||
return base + "." + ext;
|
||||
}
|
||||
ThrowUtils.throwIf(ext==null,ErrorCode.PARAMS_ERROR,"不正确的图片格式");
|
||||
// 3) 拿到 baseName,然后拼回去
|
||||
String base = FileUtil.mainName(FileUtil.getName(fileUrl));
|
||||
return StrUtil.isNotBlank(ext) ? base + "." + ext : base;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,262 @@
|
||||
package edu.whut.smilepicturebackend.manager.websocket;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import edu.whut.smilepicturebackend.manager.websocket.disruptor.PictureEditEventProducer;
|
||||
import edu.whut.smilepicturebackend.manager.websocket.model.PictureEditActionEnum;
|
||||
import edu.whut.smilepicturebackend.manager.websocket.model.PictureEditMessageTypeEnum;
|
||||
import edu.whut.smilepicturebackend.manager.websocket.model.PictureEditRequestMessage;
|
||||
import edu.whut.smilepicturebackend.manager.websocket.model.PictureEditResponseMessage;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
import edu.whut.smilepicturebackend.service.UserService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 图片编辑 WebSocket 处理器
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class PictureEditHandler extends TextWebSocketHandler {
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
@Resource
|
||||
@Lazy
|
||||
private PictureEditEventProducer pictureEditEventProducer;
|
||||
|
||||
// 每张图片的编辑状态,key: pictureId, value: 当前正在编辑的用户 ID
|
||||
private final Map<Long, Long> pictureEditingUsers = new ConcurrentHashMap<>();
|
||||
|
||||
// 保存所有连接的会话,key: pictureId, value: 所有正在编辑这张图片的用户会话集合
|
||||
private final Map<Long, Set<WebSocketSession>> pictureSessions = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 实现连接建立成功后执行的方法,保存会话到集合中,并且给其他会话发送消息
|
||||
*
|
||||
* @param session
|
||||
* @throws Exception
|
||||
*/
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||
super.afterConnectionEstablished(session);
|
||||
// 保存会话到集合中
|
||||
User user = (User) session.getAttributes().get("user");
|
||||
Long pictureId = (Long) session.getAttributes().get("pictureId");
|
||||
pictureSessions.putIfAbsent(pictureId, ConcurrentHashMap.newKeySet());
|
||||
pictureSessions.get(pictureId).add(session);
|
||||
// 构造响应,发送加入编辑的消息通知
|
||||
PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
|
||||
pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.INFO.getValue());
|
||||
String message = String.format("用户 %s 加入编辑", user.getUserName());
|
||||
pictureEditResponseMessage.setMessage(message);
|
||||
pictureEditResponseMessage.setUser(userService.getUserVO(user));
|
||||
// 广播给所有用户
|
||||
broadcastToPicture(pictureId, pictureEditResponseMessage); //自己也可以收到自己发的
|
||||
}
|
||||
|
||||
/**
|
||||
* 编写接收客户端消息的方法,根据消息类别执行不同的处理
|
||||
*
|
||||
* @param session
|
||||
* @param message
|
||||
* @throws Exception
|
||||
*/
|
||||
@Override
|
||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||
super.handleTextMessage(session, message);
|
||||
// 获取消息内容,将 JSON 转换为 PictureEditRequestMessage
|
||||
PictureEditRequestMessage pictureEditRequestMessage = JSONUtil.toBean(message.getPayload(), PictureEditRequestMessage.class);
|
||||
// 从 Session 属性中获取到公共参数
|
||||
User user = (User) session.getAttributes().get("user");
|
||||
Long pictureId = (Long) session.getAttributes().get("pictureId");
|
||||
// 根据消息类型处理消息(生产消息到 Disruptor 环形队列中)
|
||||
pictureEditEventProducer.publishEvent(pictureEditRequestMessage, session, user, pictureId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入编辑状态
|
||||
*
|
||||
* @param pictureEditRequestMessage
|
||||
* @param session
|
||||
* @param user
|
||||
* @param pictureId
|
||||
*/
|
||||
public void handleEnterEditMessage(PictureEditRequestMessage pictureEditRequestMessage, WebSocketSession session, User user, Long pictureId) throws IOException {
|
||||
// 没有用户正在编辑该图片,才能进入编辑
|
||||
if (!pictureEditingUsers.containsKey(pictureId)) {
|
||||
// 设置用户正在编辑该图片
|
||||
pictureEditingUsers.put(pictureId, user.getId());
|
||||
// 构造响应,发送加入编辑的消息通知
|
||||
PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
|
||||
pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.ENTER_EDIT.getValue());
|
||||
String message = String.format("用户 %s 开始编辑图片", user.getUserName());
|
||||
pictureEditResponseMessage.setMessage(message);
|
||||
pictureEditResponseMessage.setUser(userService.getUserVO(user));
|
||||
// 广播给所有用户
|
||||
broadcastToPicture(pictureId, pictureEditResponseMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理编辑操作
|
||||
*
|
||||
* @param pictureEditRequestMessage
|
||||
* @param session
|
||||
* @param user
|
||||
* @param pictureId
|
||||
*/
|
||||
public void handleEditActionMessage(PictureEditRequestMessage pictureEditRequestMessage, WebSocketSession session, User user, Long pictureId) throws IOException {
|
||||
// 正在编辑的用户
|
||||
Long editingUserId = pictureEditingUsers.get(pictureId);
|
||||
String editAction = pictureEditRequestMessage.getEditAction();
|
||||
PictureEditActionEnum actionEnum = PictureEditActionEnum.getEnumByValue(editAction);
|
||||
if (actionEnum == null) {
|
||||
log.error("无效的编辑动作");
|
||||
return;
|
||||
}
|
||||
// 确认是当前的编辑者
|
||||
if (editingUserId != null && editingUserId.equals(user.getId())) {
|
||||
// 构造响应,发送具体操作的通知
|
||||
PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
|
||||
pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.EDIT_ACTION.getValue());
|
||||
String message = String.format("%s 执行 %s", user.getUserName(), actionEnum.getText());
|
||||
pictureEditResponseMessage.setMessage(message);
|
||||
pictureEditResponseMessage.setEditAction(editAction);
|
||||
pictureEditResponseMessage.setUser(userService.getUserVO(user));
|
||||
// 广播给除了当前客户端之外的其他用户,否则会造成重复编辑
|
||||
broadcastToPicture(pictureId, pictureEditResponseMessage, session);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 用户退出编辑操作时,移除当前用户的编辑状态,并且向其他客户端发送消息
|
||||
*
|
||||
* @param pictureEditRequestMessage
|
||||
* @param session
|
||||
* @param user
|
||||
* @param pictureId
|
||||
*/
|
||||
public void handleExitEditMessage(PictureEditRequestMessage pictureEditRequestMessage, WebSocketSession session, User user, Long pictureId) throws IOException {
|
||||
// 正在编辑的用户
|
||||
Long editingUserId = pictureEditingUsers.get(pictureId);
|
||||
// 确认是当前的编辑者
|
||||
if (editingUserId != null && editingUserId.equals(user.getId())) {
|
||||
// 移除用户正在编辑该图片
|
||||
pictureEditingUsers.remove(pictureId);
|
||||
// 构造响应,发送退出编辑的消息通知
|
||||
PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
|
||||
pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.EXIT_EDIT.getValue());
|
||||
String message = String.format("用户 %s 退出编辑图片", user.getUserName());
|
||||
pictureEditResponseMessage.setMessage(message);
|
||||
pictureEditResponseMessage.setUser(userService.getUserVO(user));
|
||||
broadcastToPicture(pictureId, pictureEditResponseMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 连接关闭时,需要移除当前用户的编辑状态、并且从集合中删除当前会话,还可以给其他客户端发送消息通知
|
||||
*
|
||||
* @param session
|
||||
* @param status
|
||||
* @throws Exception
|
||||
*/
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
|
||||
super.afterConnectionClosed(session, status);
|
||||
// 从 Session 属性中获取到公共参数
|
||||
User user = (User) session.getAttributes().get("user");
|
||||
Long pictureId = (Long) session.getAttributes().get("pictureId");
|
||||
// 移除当前用户的编辑状态
|
||||
handleExitEditMessage(null, session, user, pictureId);
|
||||
// 删除会话
|
||||
Set<WebSocketSession> sessionSet = pictureSessions.get(pictureId);
|
||||
if (sessionSet != null) {
|
||||
sessionSet.remove(session);
|
||||
if (sessionSet.isEmpty()) {
|
||||
pictureSessions.remove(pictureId);
|
||||
}
|
||||
}
|
||||
// 通知其他用户,该用户已经离开编辑
|
||||
PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
|
||||
pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.INFO.getValue());
|
||||
String message = String.format("用户 %s 离开编辑", user.getUserName());
|
||||
pictureEditResponseMessage.setMessage(message);
|
||||
pictureEditResponseMessage.setUser(userService.getUserVO(user));
|
||||
broadcastToPicture(pictureId, pictureEditResponseMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播给该图片的所有用户(支持排除掉某个 Session)
|
||||
*
|
||||
* @param pictureId
|
||||
* @param pictureEditResponseMessage
|
||||
* @param excludeSession
|
||||
*/
|
||||
private void broadcastToPicture(Long pictureId, PictureEditResponseMessage pictureEditResponseMessage, WebSocketSession excludeSession) throws IOException {
|
||||
Set<WebSocketSession> sessionSet = pictureSessions.get(pictureId);
|
||||
if (CollUtil.isNotEmpty(sessionSet)) {
|
||||
// 创建 ObjectMapper
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
// 配置序列化:将 Long 类型转为 String,解决丢失精度问题
|
||||
SimpleModule module = new SimpleModule();
|
||||
module.addSerializer(Long.class, ToStringSerializer.instance);
|
||||
module.addSerializer(Long.TYPE, ToStringSerializer.instance); // 支持 long 基本类型
|
||||
objectMapper.registerModule(module);
|
||||
// 序列化为 JSON 字符串
|
||||
String message = objectMapper.writeValueAsString(pictureEditResponseMessage);
|
||||
TextMessage textMessage = new TextMessage(message);
|
||||
for (WebSocketSession session : sessionSet) {
|
||||
// 排除掉的 session 不发送 (比如自己发送的广播,自己不接收)
|
||||
if (excludeSession != null && session.equals(excludeSession)) {
|
||||
continue;
|
||||
}
|
||||
if (session.isOpen()) {
|
||||
session.sendMessage(textMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播给该图片的所有用户
|
||||
*
|
||||
* @param pictureId
|
||||
* @param pictureEditResponseMessage
|
||||
*/
|
||||
private void broadcastToPicture(Long pictureId, PictureEditResponseMessage pictureEditResponseMessage) throws IOException {
|
||||
broadcastToPicture(pictureId, pictureEditResponseMessage, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,28 @@
|
||||
package edu.whut.smilepicturebackend.manager.websocket;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||
|
||||
/**
|
||||
* WebSocket 配置(定义连接)
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSocket
|
||||
@RequiredArgsConstructor
|
||||
public class WebSocketConfig implements WebSocketConfigurer {
|
||||
|
||||
private final PictureEditHandler pictureEditHandler;
|
||||
|
||||
private final WsHandshakeInterceptor wsHandshakeInterceptor;
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
////当客户端在浏览器中执行new WebSocket("ws://<你的域名或 IP>:<端口>/ws/picture/edit?pictureId=123");就会由 Spring 把这个请求路由到你的 PictureEditHandler 实例。
|
||||
registry.addHandler(pictureEditHandler, "/ws/picture/edit")
|
||||
.addInterceptors(wsHandshakeInterceptor)
|
||||
.setAllowedOrigins("*");
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package edu.whut.smilepicturebackend.manager.websocket;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import edu.whut.smilepicturebackend.manager.auth.SpaceUserAuthManager;
|
||||
import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserPermissionConstant;
|
||||
import edu.whut.smilepicturebackend.model.entity.Picture;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
import edu.whut.smilepicturebackend.model.enums.SpaceTypeEnum;
|
||||
import edu.whut.smilepicturebackend.service.PictureService;
|
||||
import edu.whut.smilepicturebackend.service.SpaceService;
|
||||
import edu.whut.smilepicturebackend.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* WebSocket 拦截器,建立连接前要先校验 /如果只是公开的广播通道,不必写拦截器。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class WsHandshakeInterceptor implements HandshakeInterceptor {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
private final PictureService pictureService;
|
||||
|
||||
private final SpaceService spaceService;
|
||||
|
||||
private final SpaceUserAuthManager spaceUserAuthManager;
|
||||
|
||||
/**
|
||||
* 建立连接前要先校验
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @param wsHandler
|
||||
* @param attributes 给 WebSocketSession 会话设置属性
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
@Override
|
||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
|
||||
if (request instanceof ServletServerHttpRequest) {
|
||||
HttpServletRequest httpServletRequest = ((ServletServerHttpRequest) request).getServletRequest();
|
||||
// 从请求中获取参数
|
||||
String pictureId = httpServletRequest.getParameter("pictureId");
|
||||
if (StrUtil.isBlank(pictureId)) {
|
||||
log.error("缺少图片参数,拒绝握手");
|
||||
return false;
|
||||
}
|
||||
// 获取当前登录用户
|
||||
User loginUser = userService.getLoginUser(httpServletRequest);
|
||||
if (ObjUtil.isEmpty(loginUser)) {
|
||||
log.error("用户未登录,拒绝握手");
|
||||
return false;
|
||||
}
|
||||
// 校验用户是否有编辑当前图片的权限
|
||||
Picture picture = pictureService.getById(pictureId);
|
||||
if (ObjUtil.isEmpty(picture)) {
|
||||
log.error("图片不存在,拒绝握手");
|
||||
return false;
|
||||
}
|
||||
Long spaceId = picture.getSpaceId();
|
||||
Space space = null;
|
||||
if (spaceId != null) {
|
||||
space = spaceService.getById(spaceId);
|
||||
if (ObjUtil.isEmpty(space)) {
|
||||
log.error("图片所在空间不存在,拒绝握手");
|
||||
return false;
|
||||
}
|
||||
if (space.getSpaceType() != SpaceTypeEnum.TEAM.getValue()) {
|
||||
log.error("图片所在空间不是团队空间,拒绝握手");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
List<String> permissionList = spaceUserAuthManager.getPermissionList(space, loginUser);
|
||||
if (!permissionList.contains(SpaceUserPermissionConstant.PICTURE_EDIT)) {
|
||||
log.error("用户没有编辑图片的权限,拒绝握手");
|
||||
return false;
|
||||
}
|
||||
// 如果握手成功,设置用户登录信息等属性到 WebSocket 会话的属性 Map中
|
||||
attributes.put("user", loginUser);
|
||||
attributes.put("userId", loginUser.getId());
|
||||
attributes.put("pictureId", Long.valueOf(pictureId)); // 记得转换为 Long 类型
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package edu.whut.smilepicturebackend.manager.websocket.disruptor;
|
||||
|
||||
import edu.whut.smilepicturebackend.manager.websocket.model.PictureEditRequestMessage;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
import lombok.Data;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
/**
|
||||
* 图片编辑事件
|
||||
*/
|
||||
@Data
|
||||
public class PictureEditEvent {
|
||||
|
||||
/**
|
||||
* 消息
|
||||
*/
|
||||
private PictureEditRequestMessage pictureEditRequestMessage;
|
||||
|
||||
/**
|
||||
* 当前用户的 session
|
||||
*/
|
||||
private WebSocketSession session;
|
||||
|
||||
/**
|
||||
* 当前用户
|
||||
*/
|
||||
private User user;
|
||||
|
||||
/**
|
||||
* 图片 id
|
||||
*/
|
||||
private Long pictureId;
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package edu.whut.smilepicturebackend.manager.websocket.disruptor;
|
||||
|
||||
import cn.hutool.core.thread.ThreadFactoryBuilder;
|
||||
import com.lmax.disruptor.dsl.Disruptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 图片编辑事件 Disruptor 配置
|
||||
*/
|
||||
@Configuration
|
||||
public class PictureEditEventDisruptorConfig {
|
||||
|
||||
@Resource
|
||||
private PictureEditEventWorkHandler pictureEditEventWorkHandler;
|
||||
|
||||
@Bean("pictureEditEventDisruptor")
|
||||
public Disruptor<PictureEditEvent> messageModelRingBuffer() {
|
||||
// 定义 ringBuffer 的大小 ,小了可能数据来不及消费
|
||||
int bufferSize = 1024 * 256;
|
||||
// 创建 disruptor
|
||||
Disruptor<PictureEditEvent> disruptor = new Disruptor<>(
|
||||
PictureEditEvent::new,
|
||||
bufferSize,
|
||||
ThreadFactoryBuilder.create().setNamePrefix("pictureEditEventDisruptor").build()
|
||||
);
|
||||
// 设置消费者
|
||||
disruptor.handleEventsWithWorkerPool(pictureEditEventWorkHandler);
|
||||
// 启动 disruptor
|
||||
disruptor.start();
|
||||
return disruptor;
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package edu.whut.smilepicturebackend.manager.websocket.disruptor;
|
||||
|
||||
import com.lmax.disruptor.RingBuffer;
|
||||
import com.lmax.disruptor.dsl.Disruptor;
|
||||
import edu.whut.smilepicturebackend.manager.websocket.model.PictureEditRequestMessage;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 图片编辑事件(生产者)
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class PictureEditEventProducer {
|
||||
|
||||
@Resource
|
||||
private Disruptor<PictureEditEvent> pictureEditEventDisruptor;
|
||||
|
||||
/**
|
||||
* 发布事件
|
||||
*
|
||||
* @param pictureEditRequestMessage
|
||||
* @param session
|
||||
* @param user
|
||||
* @param pictureId
|
||||
*/
|
||||
public void publishEvent(PictureEditRequestMessage pictureEditRequestMessage, WebSocketSession session, User user, Long pictureId) {
|
||||
RingBuffer<PictureEditEvent> ringBuffer = pictureEditEventDisruptor.getRingBuffer();
|
||||
// 获取到可以防止事件的位置
|
||||
long next = ringBuffer.next();
|
||||
PictureEditEvent pictureEditEvent = ringBuffer.get(next);
|
||||
pictureEditEvent.setPictureEditRequestMessage(pictureEditRequestMessage);
|
||||
pictureEditEvent.setSession(session);
|
||||
pictureEditEvent.setUser(user);
|
||||
pictureEditEvent.setPictureId(pictureId);
|
||||
// 发布事件
|
||||
ringBuffer.publish(next);
|
||||
}
|
||||
|
||||
/**
|
||||
* 优雅停机
|
||||
*/
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
pictureEditEventDisruptor.shutdown();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,61 @@
|
||||
package edu.whut.smilepicturebackend.manager.websocket.disruptor;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.lmax.disruptor.WorkHandler;
|
||||
import edu.whut.smilepicturebackend.manager.websocket.PictureEditHandler;
|
||||
import edu.whut.smilepicturebackend.manager.websocket.model.PictureEditMessageTypeEnum;
|
||||
import edu.whut.smilepicturebackend.manager.websocket.model.PictureEditRequestMessage;
|
||||
import edu.whut.smilepicturebackend.manager.websocket.model.PictureEditResponseMessage;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
import edu.whut.smilepicturebackend.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 图片编辑事件处理器(消费者)
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class PictureEditEventWorkHandler implements WorkHandler<PictureEditEvent> {
|
||||
|
||||
private final PictureEditHandler pictureEditHandler;
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@Override
|
||||
public void onEvent(PictureEditEvent pictureEditEvent) throws Exception {
|
||||
PictureEditRequestMessage pictureEditRequestMessage = pictureEditEvent.getPictureEditRequestMessage();
|
||||
WebSocketSession session = pictureEditEvent.getSession();
|
||||
User user = pictureEditEvent.getUser();
|
||||
Long pictureId = pictureEditEvent.getPictureId();
|
||||
// 获取到消息类别
|
||||
String type = pictureEditRequestMessage.getType();
|
||||
PictureEditMessageTypeEnum pictureEditMessageTypeEnum = PictureEditMessageTypeEnum.getEnumByValue(type);
|
||||
// 根据消息类型处理消息
|
||||
switch (pictureEditMessageTypeEnum) {
|
||||
case ENTER_EDIT:
|
||||
pictureEditHandler.handleEnterEditMessage(pictureEditRequestMessage, session, user, pictureId);
|
||||
break;
|
||||
case EXIT_EDIT:
|
||||
pictureEditHandler.handleExitEditMessage(pictureEditRequestMessage, session, user, pictureId);
|
||||
break;
|
||||
case EDIT_ACTION:
|
||||
pictureEditHandler.handleEditActionMessage(pictureEditRequestMessage, session, user, pictureId);
|
||||
break;
|
||||
default:
|
||||
// 其他消息类型,返回错误提示
|
||||
PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
|
||||
pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.ERROR.getValue());
|
||||
pictureEditResponseMessage.setMessage("消息类型错误");
|
||||
pictureEditResponseMessage.setUser(userService.getUserVO(user));
|
||||
session.sendMessage(new TextMessage(JSONUtil.toJsonStr(pictureEditResponseMessage)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package edu.whut.smilepicturebackend.manager.websocket.model;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 图片编辑动作枚举
|
||||
*/
|
||||
@Getter
|
||||
public enum PictureEditActionEnum {
|
||||
|
||||
ZOOM_IN("放大操作", "ZOOM_IN"),
|
||||
ZOOM_OUT("缩小操作", "ZOOM_OUT"),
|
||||
ROTATE_LEFT("左旋操作", "ROTATE_LEFT"),
|
||||
ROTATE_RIGHT("右旋操作", "ROTATE_RIGHT");
|
||||
|
||||
private final String text;
|
||||
private final String value;
|
||||
|
||||
PictureEditActionEnum(String text, String value) {
|
||||
this.text = text;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 value 获取枚举
|
||||
*/
|
||||
public static PictureEditActionEnum getEnumByValue(String value) {
|
||||
if (value == null || value.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
for (PictureEditActionEnum actionEnum : PictureEditActionEnum.values()) {
|
||||
if (actionEnum.value.equals(value)) {
|
||||
return actionEnum;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package edu.whut.smilepicturebackend.manager.websocket.model;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 图片编辑消息类型枚举
|
||||
*/
|
||||
@Getter
|
||||
public enum PictureEditMessageTypeEnum {
|
||||
|
||||
INFO("发送通知", "INFO"),
|
||||
ERROR("发送错误", "ERROR"),
|
||||
ENTER_EDIT("进入编辑状态", "ENTER_EDIT"),
|
||||
EXIT_EDIT("退出编辑状态", "EXIT_EDIT"),
|
||||
EDIT_ACTION("执行编辑操作", "EDIT_ACTION");
|
||||
|
||||
private final String text;
|
||||
private final String value;
|
||||
|
||||
PictureEditMessageTypeEnum(String text, String value) {
|
||||
this.text = text;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 value 获取枚举
|
||||
*/
|
||||
public static PictureEditMessageTypeEnum getEnumByValue(String value) {
|
||||
if (value == null || value.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
for (PictureEditMessageTypeEnum typeEnum : PictureEditMessageTypeEnum.values()) {
|
||||
if (typeEnum.value.equals(value)) {
|
||||
return typeEnum;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package edu.whut.smilepicturebackend.manager.websocket.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 图片编辑请求消息
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PictureEditRequestMessage {
|
||||
|
||||
/**
|
||||
* 消息类型,例如 "ENTER_EDIT", "EXIT_EDIT", "EDIT_ACTION"
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 光有EDIT_ACTION不够,还要有执行的编辑动作(放大、缩小)
|
||||
*/
|
||||
private String editAction;
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package edu.whut.smilepicturebackend.manager.websocket.model;
|
||||
|
||||
import edu.whut.smilepicturebackend.model.vo.UserVO;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 图片编辑响应消息
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PictureEditResponseMessage {
|
||||
|
||||
/**
|
||||
* 消息类型,例如 "INFO", "ERROR", "ENTER_EDIT", "EXIT_EDIT", "EDIT_ACTION"
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 信息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 执行的编辑动作
|
||||
*/
|
||||
private String editAction;
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
private UserVO user;
|
||||
}
|
@ -6,7 +6,6 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
/**
|
||||
* @author 张三
|
||||
* @description 针对表【picture(图片)】的数据库操作Mapper
|
||||
* @createDate 2025-06-11 11:23:11
|
||||
* @Entity edu.whut.smilepicturebackend.model.entity.Picture
|
||||
*/
|
||||
public interface PictureMapper extends BaseMapper<Picture> {
|
||||
|
@ -0,0 +1,16 @@
|
||||
package edu.whut.smilepicturebackend.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
|
||||
/**
|
||||
* @author 张三
|
||||
* @description 针对表【space(空间)】的数据库操作Mapper
|
||||
* @Entity generator.domain.Space
|
||||
*/
|
||||
public interface SpaceMapper extends BaseMapper<Space> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,17 @@
|
||||
package edu.whut.smilepicturebackend.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import edu.whut.smilepicturebackend.model.entity.SpaceUser;
|
||||
|
||||
|
||||
/**
|
||||
* @author 张三
|
||||
* @description 针对表【space_user(空间用户关联)】的数据库操作Mapper
|
||||
* @Entity generator.domain.SpaceUser
|
||||
*/
|
||||
public interface SpaceUserMapper extends BaseMapper<SpaceUser> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -6,7 +6,6 @@ import edu.whut.smilepicturebackend.model.entity.User;
|
||||
/**
|
||||
* @author 张三
|
||||
* @description 针对表【user(用户)】的数据库操作Mapper
|
||||
* @createDate 2025-06-05 17:43:52
|
||||
*/
|
||||
|
||||
public interface UserMapper extends BaseMapper<User> {
|
||||
|
@ -0,0 +1,24 @@
|
||||
package edu.whut.smilepicturebackend.model.dto.picture;
|
||||
import edu.whut.smilepicturebackend.api.aliyunai.model.CreateOutPaintingTaskRequest;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 创建扩图任务请求
|
||||
*/
|
||||
@Data
|
||||
public class CreatePictureOutPaintingTaskRequest implements Serializable {
|
||||
|
||||
/**
|
||||
* 图片 id
|
||||
*/
|
||||
private Long pictureId;
|
||||
|
||||
/**
|
||||
* 扩图参数
|
||||
*/
|
||||
private CreateOutPaintingTaskRequest.Parameters parameters;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package edu.whut.smilepicturebackend.model.dto.picture;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图片批量编辑请求
|
||||
*/
|
||||
@Data
|
||||
public class PictureEditByBatchRequest implements Serializable {
|
||||
|
||||
/**
|
||||
* 图片 id 列表
|
||||
*/
|
||||
private List<Long> pictureIdList;
|
||||
|
||||
/**
|
||||
* 空间 id
|
||||
*/
|
||||
private Long spaceId;
|
||||
|
||||
/**
|
||||
* 分类
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
private List<String> tags;
|
||||
|
||||
/**
|
||||
* 命名规则
|
||||
*/
|
||||
private String nameRule;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -27,5 +27,10 @@ public class PictureUploadRequest implements Serializable {
|
||||
*/
|
||||
private String picName;
|
||||
|
||||
/**
|
||||
* 空间 id
|
||||
*/
|
||||
private Long spaceId;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package edu.whut.smilepicturebackend.model.dto.picture;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 按照颜色搜索图片请求
|
||||
*/
|
||||
@Data
|
||||
public class SearchPictureByColorRequest implements Serializable {
|
||||
|
||||
/**
|
||||
* 图片主色调
|
||||
*/
|
||||
private String picColor;
|
||||
|
||||
/**
|
||||
* 空间 id
|
||||
*/
|
||||
private Long spaceId;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package edu.whut.smilepicturebackend.model.dto.picture;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 以图搜图请求
|
||||
*/
|
||||
@Data
|
||||
public class SearchPictureByPictureRequest implements Serializable {
|
||||
|
||||
/**
|
||||
* 图片 id
|
||||
*/
|
||||
private Long pictureId;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package edu.whut.smilepicturebackend.model.dto.space;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 创建空间请求
|
||||
*/
|
||||
@Data
|
||||
public class SpaceAddRequest implements Serializable {
|
||||
|
||||
/**
|
||||
* 空间名称
|
||||
*/
|
||||
private String spaceName;
|
||||
|
||||
/**
|
||||
* 空间级别:0-普通版 1-专业版 2-旗舰版
|
||||
*/
|
||||
private Integer spaceLevel;
|
||||
|
||||
/**
|
||||
* 空间类型:0-私有 1-团队
|
||||
*/
|
||||
private Integer spaceType;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package edu.whut.smilepicturebackend.model.dto.space;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 编辑空间请求
|
||||
*/
|
||||
@Data
|
||||
public class SpaceEditRequest implements Serializable {
|
||||
|
||||
/**
|
||||
* 空间 id
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 空间名称
|
||||
*/
|
||||
private String spaceName;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package edu.whut.smilepicturebackend.model.dto.space;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 空间级别
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class SpaceLevel {
|
||||
|
||||
/**
|
||||
* 值
|
||||
*/
|
||||
private int value;
|
||||
|
||||
/**
|
||||
* 中文
|
||||
*/
|
||||
private String text;
|
||||
|
||||
/**
|
||||
* 最大数量
|
||||
*/
|
||||
private long maxCount;
|
||||
|
||||
/**
|
||||
* 最大容量
|
||||
*/
|
||||
private long maxSize;
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package edu.whut.smilepicturebackend.model.dto.space;
|
||||
|
||||
import edu.whut.smilepicturebackend.common.PageRequest;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 查询空间请求
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
public class SpaceQueryRequest extends PageRequest implements Serializable {
|
||||
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户 id
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 空间名称
|
||||
*/
|
||||
private String spaceName;
|
||||
|
||||
/**
|
||||
* 空间级别:0-普通版 1-专业版 2-旗舰版
|
||||
*/
|
||||
private Integer spaceLevel;
|
||||
|
||||
/**
|
||||
* 空间类型:0-私有 1-团队
|
||||
*/
|
||||
private Integer spaceType;
|
||||
|
||||
/*
|
||||
* 开始编辑时间
|
||||
*/
|
||||
private Date startEditTime;
|
||||
|
||||
/*
|
||||
* 结束编辑时间
|
||||
*/
|
||||
private Date endEditTime;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package edu.whut.smilepicturebackend.model.dto.space;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 更新空间请求
|
||||
*/
|
||||
@Data
|
||||
public class SpaceUpdateRequest implements Serializable {
|
||||
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 空间名称
|
||||
*/
|
||||
private String spaceName;
|
||||
|
||||
/**
|
||||
* 空间级别:0-普通版 1-专业版 2-旗舰版
|
||||
*/
|
||||
private Integer spaceLevel;
|
||||
|
||||
/**
|
||||
* 空间图片的最大总大小
|
||||
*/
|
||||
private Long maxSize;
|
||||
|
||||
/**
|
||||
* 空间图片的最大数量
|
||||
*/
|
||||
private Long maxCount;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package edu.whut.smilepicturebackend.model.dto.space.analyze;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 通用空间分析请求
|
||||
* queryAll ->查全空间,仅管理员 ; querypublic:查公共图库,仅管理员; spaceId,仅queryAll和querypublic都false时启用,查私人空间。
|
||||
*/
|
||||
@Data
|
||||
public class SpaceAnalyzeRequest implements Serializable {
|
||||
|
||||
/**
|
||||
* 空间 ID
|
||||
*/
|
||||
private Long spaceId;
|
||||
|
||||
/**
|
||||
* 是否查询公共图库
|
||||
*/
|
||||
private boolean queryPublic;
|
||||
|
||||
/**
|
||||
* 全空间分析
|
||||
*/
|
||||
private boolean queryAll;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package edu.whut.smilepicturebackend.model.dto.space.analyze;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 空间图片分类分析请求
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
public class SpaceCategoryAnalyzeRequest extends SpaceAnalyzeRequest {
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package edu.whut.smilepicturebackend.model.dto.space.analyze;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 空间使用排行分析请求(仅管理员)
|
||||
*/
|
||||
@Data
|
||||
public class SpaceRankAnalyzeRequest implements Serializable {
|
||||
|
||||
/**
|
||||
* 排名前 N 的空间
|
||||
*/
|
||||
private Integer topN = 10;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package edu.whut.smilepicturebackend.model.dto.space.analyze;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 空间图片大小分析请求
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
public class SpaceSizeAnalyzeRequest extends SpaceAnalyzeRequest {
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package edu.whut.smilepicturebackend.model.dto.space.analyze;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 空间图片标签分析请求
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
public class SpaceTagAnalyzeRequest extends SpaceAnalyzeRequest {
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package edu.whut.smilepicturebackend.model.dto.space.analyze;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 空间资源使用分析请求封装类
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
public class SpaceUsageAnalyzeRequest extends SpaceAnalyzeRequest {
|
||||
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package edu.whut.smilepicturebackend.model.dto.space.analyze;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 空间用户上传行为分析请求
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
public class SpaceUserAnalyzeRequest extends SpaceAnalyzeRequest {
|
||||
|
||||
/**
|
||||
* 用户 ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 时间维度:day / week / month
|
||||
*/
|
||||
private String timeDimension;
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package edu.whut.smilepicturebackend.model.dto.spaceuser;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 创建空间成员请求
|
||||
*/
|
||||
@Data
|
||||
public class SpaceUserAddRequest implements Serializable {
|
||||
|
||||
/**
|
||||
* 空间 ID
|
||||
*/
|
||||
private Long spaceId;
|
||||
|
||||
/**
|
||||
* 用户 ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 空间角色:viewer/editor/admin
|
||||
*/
|
||||
private String spaceRole;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package edu.whut.smilepicturebackend.model.dto.spaceuser;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 编辑空间成员请求
|
||||
*/
|
||||
@Data
|
||||
public class SpaceUserEditRequest implements Serializable {
|
||||
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 空间角色:viewer/editor/admin
|
||||
*/
|
||||
private String spaceRole;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package edu.whut.smilepicturebackend.model.dto.spaceuser;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 空间用户查询请求
|
||||
*/
|
||||
@Data
|
||||
public class SpaceUserQueryRequest implements Serializable {
|
||||
|
||||
/**
|
||||
* ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 空间 ID
|
||||
*/
|
||||
private Long spaceId;
|
||||
|
||||
/**
|
||||
* 用户 ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 空间角色:viewer/editor/admin
|
||||
*/
|
||||
private String spaceRole;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -79,11 +79,21 @@ public class Picture implements Serializable {
|
||||
*/
|
||||
private String picFormat;
|
||||
|
||||
/**
|
||||
* 图片主色调
|
||||
*/
|
||||
private String picColor;
|
||||
|
||||
/**
|
||||
* 创建用户 id
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 空间 id
|
||||
*/
|
||||
private Long spaceId;
|
||||
|
||||
/**
|
||||
* 审核状态:0-待审核; 1-通过; 2-拒绝
|
||||
*/
|
||||
|
@ -0,0 +1,85 @@
|
||||
package edu.whut.smilepicturebackend.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 空间
|
||||
* @TableName space
|
||||
*/
|
||||
@TableName(value ="space")
|
||||
@Data
|
||||
public class Space implements Serializable {
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 空间名称
|
||||
*/
|
||||
private String spaceName;
|
||||
|
||||
/**
|
||||
* 空间级别:0-普通版 1-专业版 2-旗舰版
|
||||
*/
|
||||
private Integer spaceLevel;
|
||||
|
||||
/**
|
||||
* 空间类型:0-私有 1-团队
|
||||
*/
|
||||
private Integer spaceType;
|
||||
|
||||
/**
|
||||
* 空间图片的最大总大小
|
||||
*/
|
||||
private Long maxSize;
|
||||
|
||||
/**
|
||||
* 空间图片的最大数量
|
||||
*/
|
||||
private Long maxCount;
|
||||
|
||||
/**
|
||||
* 当前空间下图片的总大小
|
||||
*/
|
||||
private Long totalSize;
|
||||
|
||||
/**
|
||||
* 当前空间下的图片数量
|
||||
*/
|
||||
private Long totalCount;
|
||||
|
||||
/**
|
||||
* 创建用户 id
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 编辑时间
|
||||
*/
|
||||
private Date editTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updateTime;
|
||||
|
||||
/**
|
||||
* 是否删除
|
||||
*/
|
||||
@TableLogic
|
||||
private Integer isDelete;
|
||||
|
||||
@TableField(exist = false)
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package edu.whut.smilepicturebackend.model.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.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 空间用户关联
|
||||
* @TableName space_user
|
||||
*/
|
||||
@TableName(value ="space_user")
|
||||
@Data
|
||||
public class SpaceUser implements Serializable {
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 空间 id
|
||||
*/
|
||||
private Long spaceId;
|
||||
|
||||
/**
|
||||
* 用户 id
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 空间角色:viewer/editor/admin
|
||||
*/
|
||||
private String spaceRole;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updateTime;
|
||||
|
||||
@TableField(exist = false)
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package edu.whut.smilepicturebackend.model.enums;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 空间级别枚举
|
||||
*/
|
||||
@Getter
|
||||
public enum SpaceLevelEnum {
|
||||
|
||||
COMMON("普通版", 0, 100, 100L * 1024 * 1024),
|
||||
PROFESSIONAL("专业版", 1, 1000, 1000L * 1024 * 1024),
|
||||
FLAGSHIP("旗舰版", 2, 10000, 10000L * 1024 * 1024);
|
||||
|
||||
private final String text;
|
||||
|
||||
private final int value;
|
||||
|
||||
private final long maxCount;
|
||||
|
||||
private final long maxSize;
|
||||
|
||||
|
||||
/**
|
||||
* @param text 文本
|
||||
* @param value 值
|
||||
* @param maxSize 最大图片总大小
|
||||
* @param maxCount 最大图片总数量
|
||||
*/
|
||||
SpaceLevelEnum(String text, int value, long maxCount, long maxSize) {
|
||||
this.text = text;
|
||||
this.value = value;
|
||||
this.maxCount = maxCount;
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 value 获取枚举
|
||||
*/
|
||||
public static SpaceLevelEnum getEnumByValue(Integer value) {
|
||||
if (ObjUtil.isEmpty(value)) {
|
||||
return null;
|
||||
}
|
||||
for (SpaceLevelEnum spaceLevelEnum : SpaceLevelEnum.values()) {
|
||||
if (spaceLevelEnum.value == value) {
|
||||
return spaceLevelEnum;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package edu.whut.smilepicturebackend.model.enums;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 空间角色枚举
|
||||
*/
|
||||
@Getter
|
||||
public enum SpaceRoleEnum {
|
||||
|
||||
VIEWER("浏览者", "viewer"),
|
||||
EDITOR("编辑者", "editor"),
|
||||
ADMIN("管理员", "admin");
|
||||
|
||||
private final String text;
|
||||
|
||||
private final String value;
|
||||
|
||||
SpaceRoleEnum(String text, String value) {
|
||||
this.text = text;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 value 获取枚举
|
||||
*
|
||||
* @param value 枚举值的 value
|
||||
* @return 枚举值
|
||||
*/
|
||||
public static SpaceRoleEnum getEnumByValue(String value) {
|
||||
if (ObjUtil.isEmpty(value)) {
|
||||
return null;
|
||||
}
|
||||
for (SpaceRoleEnum anEnum : SpaceRoleEnum.values()) {
|
||||
if (anEnum.value.equals(value)) {
|
||||
return anEnum;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有枚举的文本列表
|
||||
*
|
||||
* @return 文本列表
|
||||
*/
|
||||
public static List<String> getAllTexts() {
|
||||
return Arrays.stream(SpaceRoleEnum.values())
|
||||
.map(SpaceRoleEnum::getText)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有枚举的值列表
|
||||
*
|
||||
* @return 值列表
|
||||
*/
|
||||
public static List<String> getAllValues() {
|
||||
return Arrays.stream(SpaceRoleEnum.values())
|
||||
.map(SpaceRoleEnum::getValue)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package edu.whut.smilepicturebackend.model.enums;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 空间类型枚举类
|
||||
*/
|
||||
@Getter
|
||||
public enum SpaceTypeEnum {
|
||||
|
||||
PRIVATE("私有空间", 0),
|
||||
TEAM("团队空间", 1);
|
||||
|
||||
private final String text;
|
||||
|
||||
private final int value;
|
||||
|
||||
SpaceTypeEnum(String text, int value) {
|
||||
this.text = text;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 value 获取枚举
|
||||
*/
|
||||
public static SpaceTypeEnum getEnumByValue(Integer value) {
|
||||
if (ObjUtil.isEmpty(value)) {
|
||||
return null;
|
||||
}
|
||||
for (SpaceTypeEnum spaceTypeEnum : SpaceTypeEnum.values()) {
|
||||
if (spaceTypeEnum.value == value) {
|
||||
return spaceTypeEnum;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -78,12 +78,21 @@ public class PictureVO implements Serializable {
|
||||
*/
|
||||
private String picFormat;
|
||||
|
||||
/**
|
||||
* 图片主色调
|
||||
*/
|
||||
private String picColor;
|
||||
|
||||
|
||||
/**
|
||||
* 用户 id
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 空间 id
|
||||
*/
|
||||
private Long spaceId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
|
@ -0,0 +1,86 @@
|
||||
package edu.whut.smilepicturebackend.model.vo;
|
||||
import edu.whut.smilepicturebackend.model.entity.SpaceUser;
|
||||
import lombok.Data;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 空间成员响应类
|
||||
*/
|
||||
@Data
|
||||
public class SpaceUserVO implements Serializable {
|
||||
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 空间 id
|
||||
*/
|
||||
private Long spaceId;
|
||||
|
||||
/**
|
||||
* 用户 id
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 空间角色:viewer/editor/admin
|
||||
*/
|
||||
private String spaceRole;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updateTime;
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
private UserVO user;
|
||||
|
||||
/**
|
||||
* 空间信息
|
||||
*/
|
||||
private SpaceVO space;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 封装类转对象
|
||||
*
|
||||
* @param spaceUserVO
|
||||
* @return
|
||||
*/
|
||||
public static SpaceUser voToObj(SpaceUserVO spaceUserVO) {
|
||||
if (spaceUserVO == null) {
|
||||
return null;
|
||||
}
|
||||
SpaceUser spaceUser = new SpaceUser();
|
||||
BeanUtils.copyProperties(spaceUserVO, spaceUser);
|
||||
return spaceUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对象转封装类
|
||||
*
|
||||
* @param spaceUser
|
||||
* @return
|
||||
*/
|
||||
public static SpaceUserVO objToVo(SpaceUser spaceUser) {
|
||||
if (spaceUser == null) {
|
||||
return null;
|
||||
}
|
||||
SpaceUserVO spaceUserVO = new SpaceUserVO();
|
||||
BeanUtils.copyProperties(spaceUser, spaceUserVO);
|
||||
return spaceUserVO;
|
||||
}
|
||||
}
|
117
src/main/java/edu/whut/smilepicturebackend/model/vo/SpaceVO.java
Normal file
117
src/main/java/edu/whut/smilepicturebackend/model/vo/SpaceVO.java
Normal file
@ -0,0 +1,117 @@
|
||||
package edu.whut.smilepicturebackend.model.vo;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
import lombok.Data;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 空间视图
|
||||
*/
|
||||
@Data
|
||||
public class SpaceVO implements Serializable {
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 空间名称
|
||||
*/
|
||||
private String spaceName;
|
||||
|
||||
/**
|
||||
* 空间级别:0-普通版 1-专业版 2-旗舰版
|
||||
*/
|
||||
private Integer spaceLevel;
|
||||
|
||||
/**
|
||||
* 空间类型:0-私有 1-团队
|
||||
*/
|
||||
private Integer spaceType;
|
||||
|
||||
/**
|
||||
* 空间图片的最大总大小
|
||||
*/
|
||||
private Long maxSize;
|
||||
|
||||
/**
|
||||
* 空间图片的最大数量
|
||||
*/
|
||||
private Long maxCount;
|
||||
|
||||
/**
|
||||
* 当前空间下图片的总大小
|
||||
*/
|
||||
private Long totalSize;
|
||||
|
||||
/**
|
||||
* 当前空间下的图片数量
|
||||
*/
|
||||
private Long totalCount;
|
||||
|
||||
/**
|
||||
* 创建用户 id
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 编辑时间
|
||||
*/
|
||||
private Date editTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updateTime;
|
||||
|
||||
/**
|
||||
* 创建用户信息
|
||||
*/
|
||||
private UserVO user;
|
||||
|
||||
/**
|
||||
* 权限列表
|
||||
*/
|
||||
private List<String> permissionList = new ArrayList<>();
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 封装类转对象
|
||||
*
|
||||
* @param spaceVO
|
||||
* @return
|
||||
*/
|
||||
public static Space voToObj(SpaceVO spaceVO) {
|
||||
if (spaceVO == null) {
|
||||
return null;
|
||||
}
|
||||
Space space = new Space();
|
||||
BeanUtils.copyProperties(spaceVO, space);
|
||||
return space;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对象转封装类
|
||||
*
|
||||
* @param space
|
||||
* @return
|
||||
*/
|
||||
public static SpaceVO objToVo(Space space) {
|
||||
if (space == null) {
|
||||
return null;
|
||||
}
|
||||
SpaceVO spaceVO = new SpaceVO();
|
||||
BeanUtils.copyProperties(space, spaceVO);
|
||||
return spaceVO;
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package edu.whut.smilepicturebackend.model.vo.space.analyze;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 空间图片分类分析响应
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class SpaceCategoryAnalyzeResponse implements Serializable {
|
||||
|
||||
/**
|
||||
* 图片分类
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 图片数量
|
||||
*/
|
||||
private Long count;
|
||||
|
||||
/**
|
||||
* 分类图片总大小
|
||||
*/
|
||||
private Long totalSize;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package edu.whut.smilepicturebackend.model.vo.space.analyze;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 空间图片大小分析响应
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class SpaceSizeAnalyzeResponse implements Serializable {
|
||||
|
||||
/**
|
||||
* 图片大小范围
|
||||
*/
|
||||
private String sizeRange;
|
||||
|
||||
/**
|
||||
* 图片数量
|
||||
*/
|
||||
private Long count;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package edu.whut.smilepicturebackend.model.vo.space.analyze;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 空间图片标签分析响应
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class SpaceTagAnalyzeResponse implements Serializable {
|
||||
|
||||
/**
|
||||
* 标签名称
|
||||
*/
|
||||
private String tag;
|
||||
|
||||
/**
|
||||
* 使用次数
|
||||
*/
|
||||
private Long count;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package edu.whut.smilepicturebackend.model.vo.space.analyze;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 空间资源使用分析响应类
|
||||
*/
|
||||
@Data
|
||||
public class SpaceUsageAnalyzeResponse implements Serializable {
|
||||
|
||||
/**
|
||||
* 已使用大小
|
||||
*/
|
||||
private Long usedSize;
|
||||
|
||||
/**
|
||||
* 总大小
|
||||
*/
|
||||
private Long maxSize;
|
||||
|
||||
/**
|
||||
* 空间使用比例
|
||||
*/
|
||||
private Double sizeUsageRatio;
|
||||
|
||||
/**
|
||||
* 当前图片数量
|
||||
*/
|
||||
private Long usedCount;
|
||||
|
||||
/**
|
||||
* 最大图片数量
|
||||
*/
|
||||
private Long maxCount;
|
||||
|
||||
/**
|
||||
* 图片数量占比
|
||||
*/
|
||||
private Double countUsageRatio;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package edu.whut.smilepicturebackend.model.vo.space.analyze;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 空间用户上传行为分析响应
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class SpaceUserAnalyzeResponse implements Serializable {
|
||||
|
||||
/**
|
||||
* 时间区间
|
||||
*/
|
||||
private String period;
|
||||
|
||||
/**
|
||||
* 上传数量
|
||||
*/
|
||||
private Long count;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import edu.whut.smilepicturebackend.api.aliyunai.model.CreateOutPaintingTaskResponse;
|
||||
import edu.whut.smilepicturebackend.api.imagesearch.model.ImageSearchResult;
|
||||
import edu.whut.smilepicturebackend.model.dto.picture.*;
|
||||
import edu.whut.smilepicturebackend.model.entity.Picture;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
@ -11,11 +13,12 @@ import edu.whut.smilepicturebackend.model.vo.PictureVO;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author 张三
|
||||
* @description 针对表【picture(图片)】的数据库操作Service
|
||||
* @createDate 2025-06-11 11:23:11
|
||||
*/
|
||||
public interface PictureService extends IService<Picture> {
|
||||
/**
|
||||
@ -118,4 +121,34 @@ public interface PictureService extends IService<Picture> {
|
||||
* @param oldPicture
|
||||
*/
|
||||
void clearPictureFile(Picture oldPicture);
|
||||
|
||||
void checkPictureAuth(User loginUser, Picture picture);
|
||||
|
||||
List<ImageSearchResult> getSimilarPicture(SearchPictureByPictureRequest request) throws IOException;
|
||||
|
||||
/**
|
||||
* 根据颜色搜索图片
|
||||
*
|
||||
* @param spaceId
|
||||
* @param picColor
|
||||
* @param loginUser
|
||||
* @return
|
||||
*/
|
||||
List<PictureVO> searchPictureByColor(Long spaceId, String picColor, User loginUser);
|
||||
|
||||
/**
|
||||
* 批量编辑图片
|
||||
*
|
||||
* @param pictureEditByBatchRequest
|
||||
* @param loginUser
|
||||
*/
|
||||
void editPictureByBatch(PictureEditByBatchRequest pictureEditByBatchRequest, User loginUser);
|
||||
|
||||
/**
|
||||
* 创建扩图任务
|
||||
*
|
||||
* @param createPictureOutPaintingTaskRequest
|
||||
* @param loginUser
|
||||
*/
|
||||
CreateOutPaintingTaskResponse createPictureOutPaintingTask(CreatePictureOutPaintingTaskRequest createPictureOutPaintingTaskRequest, User loginUser);
|
||||
}
|
||||
|
@ -0,0 +1,65 @@
|
||||
package edu.whut.smilepicturebackend.service;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import edu.whut.smilepicturebackend.model.dto.space.analyze.*;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
import edu.whut.smilepicturebackend.model.vo.space.analyze.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SpaceAnalyzeService extends IService<Space> {
|
||||
|
||||
/**
|
||||
* 获取空间使用情况分析
|
||||
*
|
||||
* @param spaceUsageAnalyzeRequest
|
||||
* @param loginUser
|
||||
* @return
|
||||
*/
|
||||
SpaceUsageAnalyzeResponse getSpaceUsageAnalyze(SpaceUsageAnalyzeRequest spaceUsageAnalyzeRequest, User loginUser);
|
||||
|
||||
/**
|
||||
* 获取空间图片分类分析
|
||||
*
|
||||
* @param spaceCategoryAnalyzeRequest
|
||||
* @param loginUser
|
||||
* @return
|
||||
*/
|
||||
List<SpaceCategoryAnalyzeResponse> getSpaceCategoryAnalyze(SpaceCategoryAnalyzeRequest spaceCategoryAnalyzeRequest, User loginUser);
|
||||
|
||||
/**
|
||||
* 获取空间图片标签分析
|
||||
*
|
||||
* @param spaceTagAnalyzeRequest
|
||||
* @param loginUser
|
||||
* @return
|
||||
*/
|
||||
List<SpaceTagAnalyzeResponse> getSpaceTagAnalyze(SpaceTagAnalyzeRequest spaceTagAnalyzeRequest, User loginUser);
|
||||
|
||||
/**
|
||||
* 获取空间图片大小分析
|
||||
*
|
||||
* @param spaceSizeAnalyzeRequest
|
||||
* @param loginUser
|
||||
* @return
|
||||
*/
|
||||
List<SpaceSizeAnalyzeResponse> getSpaceSizeAnalyze(SpaceSizeAnalyzeRequest spaceSizeAnalyzeRequest, User loginUser);
|
||||
|
||||
/**
|
||||
* 获取空间用户上传行为分析
|
||||
*
|
||||
* @param spaceUserAnalyzeRequest
|
||||
* @param loginUser
|
||||
* @return
|
||||
*/
|
||||
List<SpaceUserAnalyzeResponse> getSpaceUserAnalyze(SpaceUserAnalyzeRequest spaceUserAnalyzeRequest, User loginUser);
|
||||
|
||||
/**
|
||||
* 空间使用排行分析(仅管理员)
|
||||
*
|
||||
* @param spaceRankAnalyzeRequest
|
||||
* @param loginUser
|
||||
* @return
|
||||
*/
|
||||
List<Space> getSpaceRankAnalyze(SpaceRankAnalyzeRequest spaceRankAnalyzeRequest, User loginUser);
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package edu.whut.smilepicturebackend.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import edu.whut.smilepicturebackend.model.dto.space.SpaceAddRequest;
|
||||
import edu.whut.smilepicturebackend.model.dto.space.SpaceQueryRequest;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
import edu.whut.smilepicturebackend.model.vo.SpaceVO;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* @author 张三
|
||||
* @description 针对表【space(空间)】的数据库操作Service
|
||||
*/
|
||||
public interface SpaceService extends IService<Space> {
|
||||
/**
|
||||
* 创建空间
|
||||
*
|
||||
* @param spaceAddRequest
|
||||
* @param loginUser
|
||||
* @return
|
||||
*/
|
||||
long addSpace(SpaceAddRequest spaceAddRequest, User loginUser);
|
||||
|
||||
/**
|
||||
* 校验空间
|
||||
*
|
||||
* @param space
|
||||
* @param add 是否为创建时检验
|
||||
*/
|
||||
void validSpace(Space space, boolean add);
|
||||
|
||||
/**
|
||||
* 获取空间包装类(单条)
|
||||
*
|
||||
* @param space
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
SpaceVO getSpaceVO(Space space, HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 获取空间包装类(分页)
|
||||
*
|
||||
* @param spacePage
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
Page<SpaceVO> getSpaceVOPage(Page<Space> spacePage, HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 获取查询对象
|
||||
*
|
||||
* @param spaceQueryRequest
|
||||
* @return
|
||||
*/
|
||||
LambdaQueryWrapper<Space> getQueryWrapper(SpaceQueryRequest spaceQueryRequest);
|
||||
|
||||
/**
|
||||
* 根据空间级别填充空间对象
|
||||
*
|
||||
* @param space
|
||||
*/
|
||||
void fillSpaceBySpaceLevel(Space space);
|
||||
|
||||
/**
|
||||
* 校验空间权限
|
||||
*
|
||||
* @param loginUser
|
||||
* @param space
|
||||
*/
|
||||
void checkSpaceAuth(User loginUser, Space space);
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package edu.whut.smilepicturebackend.service;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import edu.whut.smilepicturebackend.model.dto.spaceuser.SpaceUserAddRequest;
|
||||
import edu.whut.smilepicturebackend.model.dto.spaceuser.SpaceUserQueryRequest;
|
||||
import edu.whut.smilepicturebackend.model.entity.SpaceUser;
|
||||
import edu.whut.smilepicturebackend.model.vo.SpaceUserVO;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* @author 张三
|
||||
* @description 针对表【space_user(空间用户关联)】的数据库操作Service
|
||||
*/
|
||||
public interface SpaceUserService extends IService<SpaceUser> {
|
||||
/**
|
||||
* 创建空间成员
|
||||
*
|
||||
* @param spaceUserAddRequest
|
||||
* @return
|
||||
*/
|
||||
long addSpaceUser(SpaceUserAddRequest spaceUserAddRequest);
|
||||
|
||||
/**
|
||||
* 校验空间成员
|
||||
*
|
||||
* @param spaceUser
|
||||
* @param add 是否为创建时检验
|
||||
*/
|
||||
void validSpaceUser(SpaceUser spaceUser, boolean add);
|
||||
|
||||
/**
|
||||
* 获取空间成员包装类(单条)
|
||||
*
|
||||
* @param spaceUser
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
SpaceUserVO getSpaceUserVO(SpaceUser spaceUser, HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 获取空间成员包装类(列表)
|
||||
*
|
||||
* @param spaceUserList
|
||||
* @return
|
||||
*/
|
||||
List<SpaceUserVO> getSpaceUserVOList(List<SpaceUser> spaceUserList);
|
||||
|
||||
/**
|
||||
* 获取查询对象
|
||||
*
|
||||
* @param spaceUserQueryRequest
|
||||
* @return
|
||||
*/
|
||||
LambdaQueryWrapper<SpaceUser> getQueryWrapper(SpaceUserQueryRequest spaceUserQueryRequest);
|
||||
}
|
@ -15,7 +15,6 @@ import java.util.List;
|
||||
/**
|
||||
* @author 张三
|
||||
* @description 针对表【user(用户)】的数据库操作Service
|
||||
* @createDate 2025-06-05 17:43:52
|
||||
*/
|
||||
public interface UserService extends IService<User> {
|
||||
/**
|
||||
|
@ -2,16 +2,22 @@ package edu.whut.smilepicturebackend.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSON;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import edu.whut.smilepicturebackend.api.aliyunai.AliYunAiApi;
|
||||
import edu.whut.smilepicturebackend.api.aliyunai.model.CreateOutPaintingTaskRequest;
|
||||
import edu.whut.smilepicturebackend.api.aliyunai.model.CreateOutPaintingTaskResponse;
|
||||
import edu.whut.smilepicturebackend.api.imagesearch.ImageSearchApiFacade;
|
||||
import edu.whut.smilepicturebackend.api.imagesearch.model.ImageSearchResult;
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import edu.whut.smilepicturebackend.exception.ThrowUtils;
|
||||
@ -24,13 +30,17 @@ import edu.whut.smilepicturebackend.manager.upload.UrlPictureUpload;
|
||||
import edu.whut.smilepicturebackend.mapper.PictureMapper;
|
||||
import edu.whut.smilepicturebackend.model.dto.picture.*;
|
||||
import edu.whut.smilepicturebackend.model.entity.Picture;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
import edu.whut.smilepicturebackend.model.enums.PictureReviewStatusEnum;
|
||||
import edu.whut.smilepicturebackend.model.file.UploadPictureResult;
|
||||
import edu.whut.smilepicturebackend.model.vo.PictureVO;
|
||||
import edu.whut.smilepicturebackend.model.vo.UserVO;
|
||||
import edu.whut.smilepicturebackend.service.PictureService;
|
||||
import edu.whut.smilepicturebackend.service.SpaceService;
|
||||
import edu.whut.smilepicturebackend.service.UserService;
|
||||
import edu.whut.smilepicturebackend.utils.ColorSimilarUtils;
|
||||
import edu.whut.smilepicturebackend.utils.ColorTransformUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jsoup.Jsoup;
|
||||
@ -40,16 +50,16 @@ import org.jsoup.select.Elements;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.springframework.util.DigestUtils;
|
||||
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@ -63,11 +73,23 @@ import java.util.stream.Collectors;
|
||||
public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
||||
implements PictureService {
|
||||
private final FileManager fileManager;
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
private final FilePictureUpload filePictureUpload;
|
||||
|
||||
private final UrlPictureUpload urlPictureUpload;
|
||||
|
||||
private final MyCacheManager cacheManager;
|
||||
|
||||
private final CosManager cosManager;
|
||||
|
||||
private final SpaceService spaceService;
|
||||
|
||||
private final TransactionTemplate transactionTemplate;
|
||||
|
||||
private final AliYunAiApi aliYunAiApi;
|
||||
|
||||
@Override
|
||||
public void validPicture(Picture picture) {
|
||||
ThrowUtils.throwIf(picture == null, ErrorCode.PARAMS_ERROR);
|
||||
@ -90,7 +112,24 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
||||
public PictureVO uploadPicture(Object inputSource, PictureUploadRequest pictureUploadRequest, User loginUser) {
|
||||
// 校验参数
|
||||
ThrowUtils.throwIf(loginUser == null, ErrorCode.NO_AUTH_ERROR);
|
||||
|
||||
// 校验空间是否存在
|
||||
Long spaceId = pictureUploadRequest.getSpaceId();
|
||||
if (spaceId != null) {
|
||||
Space space = spaceService.getById(spaceId);
|
||||
ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
|
||||
// 改为使用统一的权限校验
|
||||
// 校验是否有空间的权限,仅空间管理员才能上传
|
||||
// if (!loginUser.getId().equals(space.getUserId())) {
|
||||
// throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "没有空间权限");
|
||||
// }
|
||||
// 校验额度
|
||||
if (space.getTotalCount() >= space.getMaxCount()) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "空间条数不足");
|
||||
}
|
||||
if (space.getTotalSize() >= space.getMaxSize()) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "空间大小不足");
|
||||
}
|
||||
}
|
||||
// 判断是创建还是替换
|
||||
Long pictureId = pictureUploadRequest == null ? null : pictureUploadRequest.getId();
|
||||
Picture oldPicture = null;
|
||||
@ -99,26 +138,48 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
||||
if (pictureId != null) {
|
||||
oldPicture = this.getById(pictureId);
|
||||
ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR, "图片不存在");
|
||||
// 改为使用统一的权限校验
|
||||
// 仅本人或管理员可编辑图片
|
||||
if (!oldPicture.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
|
||||
// if (!oldPicture.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
|
||||
// throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
|
||||
// }
|
||||
// 校验空间是否一致
|
||||
// 没传 spaceId,则复用原有图片的 spaceId(这样也兼容了公共图库)
|
||||
if (spaceId == null) {
|
||||
if (oldPicture.getSpaceId() != null) {
|
||||
spaceId = oldPicture.getSpaceId();
|
||||
}
|
||||
} else {
|
||||
// 传了 spaceId,必须和原图片的空间 id 一致
|
||||
if (ObjUtil.notEqual(spaceId, oldPicture.getSpaceId())) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "空间 id 不一致");
|
||||
}
|
||||
}
|
||||
}
|
||||
// 上传图片,得到图片信息
|
||||
// 按照用户 id 划分目录 => 按照空间划分目录
|
||||
String uploadPathPrefix;
|
||||
//公共图库下,每个用户有自己的userid管理的文件夹。
|
||||
uploadPathPrefix = String.format("public/%s", loginUser.getId());
|
||||
if (spaceId == null) {
|
||||
// 公共图库+用户id
|
||||
uploadPathPrefix = String.format("public/%s", loginUser.getId());
|
||||
} else {
|
||||
// 私人空间+空间id
|
||||
uploadPathPrefix = String.format("space/%s", spaceId);
|
||||
}
|
||||
// 根据 inputSource 的类型区分上传方式!!
|
||||
PictureUploadTemplate pictureUploadTemplate = filePictureUpload;
|
||||
if (inputSource instanceof String) {
|
||||
log.info("收到 upload/url 请求,url = {}", inputSource);
|
||||
pictureUploadTemplate = urlPictureUpload;
|
||||
}
|
||||
//上传到腾讯云COS上
|
||||
UploadPictureResult uploadPictureResult = pictureUploadTemplate.uploadPicture(inputSource, uploadPathPrefix);
|
||||
// 构造要入库的图片信息,将图片信息存入数据库中。
|
||||
Picture picture = new Picture();
|
||||
|
||||
// 复制同名属性(url、name、picSize、picWidth、picHeight、picScale、picFormat)
|
||||
BeanUtils.copyProperties(uploadPictureResult, picture);
|
||||
|
||||
// 支持外层pictureUploadRequest传递图片名称
|
||||
picture.setName(
|
||||
StrUtil.blankToDefault(
|
||||
@ -127,6 +188,10 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
||||
)
|
||||
);
|
||||
picture.setUserId(loginUser.getId());
|
||||
picture.setSpaceId(spaceId);
|
||||
// 转换为标准颜色
|
||||
// log.info("颜色"+uploadPictureResult.getPicColor());
|
||||
picture.setPicColor(ColorTransformUtils.getStandardColor(uploadPictureResult.getPicColor()));
|
||||
// 补充审核参数
|
||||
this.fillReviewParams(picture, loginUser);
|
||||
// 操作数据库
|
||||
@ -136,8 +201,41 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
||||
picture.setId(pictureId);
|
||||
picture.setEditTime(new Date());
|
||||
}
|
||||
boolean result = this.saveOrUpdate(picture);
|
||||
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR, "图片上传失败,数据库操作失败");
|
||||
// 计算差值,保证 oldPicture 为空时不抛异常
|
||||
long sizeDelta = picture.getPicSize() - (oldPicture == null ? 0 : oldPicture.getPicSize());
|
||||
long countDelta = (oldPicture == null ? 1 : 0);
|
||||
// 开启事务,图片上传成功和修改额度一定要同时成功或失败
|
||||
Long finalSpaceId = spaceId;
|
||||
transactionTemplate.execute(status -> {
|
||||
log.info("uploadPicture | spaceId={}, sizeDelta={}, countDelta={}",
|
||||
finalSpaceId, sizeDelta, countDelta);
|
||||
|
||||
/* ---------- 1. 保存 / 更新图片记录 ---------- */
|
||||
boolean saved = this.saveOrUpdate(picture);
|
||||
ThrowUtils.throwIf(!saved, ErrorCode.OPERATION_ERROR, "图片上传失败,数据库操作失败");
|
||||
|
||||
/* ---------- 2. 更新空间额度(只有有变化时才执行 UPDATE) ---------- */
|
||||
if (finalSpaceId != null && (sizeDelta != 0 || countDelta != 0)) {
|
||||
LambdaUpdateChainWrapper<Space> wrapper = spaceService.lambdaUpdate()
|
||||
.eq(Space::getId, finalSpaceId);
|
||||
|
||||
// 组装可变 SQL,不生成空 SET
|
||||
StringBuilder setSql = new StringBuilder();
|
||||
if (sizeDelta != 0) {
|
||||
setSql.append("total_size = total_size + ").append(sizeDelta);
|
||||
}
|
||||
if (countDelta != 0) {
|
||||
if (setSql.length() > 0) setSql.append(", ");
|
||||
setSql.append("total_count = total_count + ").append(countDelta);
|
||||
}
|
||||
wrapper.setSql(setSql.toString());
|
||||
|
||||
boolean updated = wrapper.update();
|
||||
ThrowUtils.throwIf(!updated, ErrorCode.OPERATION_ERROR, "额度更新失败");
|
||||
}
|
||||
|
||||
return picture; // 事务块返回结果
|
||||
});
|
||||
//如果是更新,清理旧的图片
|
||||
if (oldPicture != null) {
|
||||
this.clearPictureFile(oldPicture);
|
||||
@ -153,21 +251,23 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
||||
}
|
||||
|
||||
// 精简版条件构造
|
||||
qw.eq(ObjUtil.isNotEmpty(req.getId()), Picture::getId, req.getId())
|
||||
.eq(ObjUtil.isNotEmpty(req.getUserId()), Picture::getUserId, req.getUserId())
|
||||
.like(StrUtil.isNotBlank(req.getName()), Picture::getName, req.getName())
|
||||
qw.eq(ObjUtil.isNotEmpty(req.getId()), Picture::getId, req.getId())
|
||||
.eq(ObjUtil.isNotEmpty(req.getUserId()), Picture::getUserId, req.getUserId())
|
||||
.eq(ObjUtil.isNotEmpty(req.getSpaceId()), Picture::getSpaceId, req.getSpaceId()) //指定 spaceId → 查该空间图片。
|
||||
.isNull(req.isNullSpaceId(), Picture::getSpaceId) //不传则查公共图库
|
||||
.like(StrUtil.isNotBlank(req.getName()), Picture::getName, req.getName())
|
||||
.like(StrUtil.isNotBlank(req.getIntroduction()), Picture::getIntroduction, req.getIntroduction())
|
||||
.like(StrUtil.isNotBlank(req.getPicFormat()), Picture::getPicFormat, req.getPicFormat())
|
||||
.eq(ObjUtil.isNotEmpty(req.getReviewMessage()),Picture::getReviewMessage,req.getReviewMessage())
|
||||
.eq(StrUtil.isNotBlank(req.getCategory()), Picture::getCategory, req.getCategory())
|
||||
.eq(ObjUtil.isNotEmpty(req.getPicWidth()), Picture::getPicWidth, req.getPicWidth())
|
||||
.eq(ObjUtil.isNotEmpty(req.getPicHeight()), Picture::getPicHeight, req.getPicHeight())
|
||||
.eq(ObjUtil.isNotEmpty(req.getPicSize()), Picture::getPicSize, req.getPicSize())
|
||||
.eq(ObjUtil.isNotEmpty(req.getPicScale()), Picture::getPicScale, req.getPicScale())
|
||||
.eq(ObjUtil.isNotEmpty(req.getReviewStatus()),Picture::getReviewStatus,req.getReviewStatus())
|
||||
.eq(ObjUtil.isNotEmpty(req.getReviewerId()),Picture::getReviewerId,req.getReviewerId())
|
||||
.ge(ObjUtil.isNotEmpty(req.getStartEditTime()), Picture::getEditTime, req.getStartEditTime())
|
||||
.lt(ObjUtil.isNotEmpty(req.getEndEditTime()), Picture::getEditTime, req.getEndEditTime());
|
||||
.like(StrUtil.isNotBlank(req.getPicFormat()), Picture::getPicFormat, req.getPicFormat())
|
||||
.eq(ObjUtil.isNotEmpty(req.getReviewMessage()), Picture::getReviewMessage, req.getReviewMessage())
|
||||
.eq(StrUtil.isNotBlank(req.getCategory()), Picture::getCategory, req.getCategory())
|
||||
.eq(ObjUtil.isNotEmpty(req.getPicWidth()), Picture::getPicWidth, req.getPicWidth())
|
||||
.eq(ObjUtil.isNotEmpty(req.getPicHeight()), Picture::getPicHeight, req.getPicHeight())
|
||||
.eq(ObjUtil.isNotEmpty(req.getPicSize()), Picture::getPicSize, req.getPicSize())
|
||||
.eq(ObjUtil.isNotEmpty(req.getPicScale()), Picture::getPicScale, req.getPicScale())
|
||||
.eq(ObjUtil.isNotEmpty(req.getReviewStatus()), Picture::getReviewStatus, req.getReviewStatus())
|
||||
.eq(ObjUtil.isNotEmpty(req.getReviewerId()), Picture::getReviewerId, req.getReviewerId())
|
||||
.ge(ObjUtil.isNotEmpty(req.getStartEditTime()), Picture::getEditTime, req.getStartEditTime()) // >=
|
||||
.lt(ObjUtil.isNotEmpty(req.getEndEditTime()), Picture::getEditTime, req.getEndEditTime()); // <=
|
||||
|
||||
// 全字段模糊搜索
|
||||
if (StrUtil.isNotBlank(req.getSearchText())) {
|
||||
@ -204,13 +304,50 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
||||
&& !userService.isAdmin(loginUser)) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
|
||||
}
|
||||
// 操作数据库
|
||||
boolean result = this.removeById(pictureId);
|
||||
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
|
||||
// 校验权限,已改为注解鉴权
|
||||
// checkPictureAuth(loginUser, oldPicture);
|
||||
// 开启事务
|
||||
transactionTemplate.execute(status -> {
|
||||
// 操作数据库
|
||||
boolean result = this.removeById(pictureId);
|
||||
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
|
||||
// 更新空间的使用额度,释放额度
|
||||
boolean update = spaceService.lambdaUpdate()
|
||||
.eq(Space::getId, oldPicture.getSpaceId())
|
||||
.setSql("total_size = total_size - " + oldPicture.getPicSize())
|
||||
.setSql("total_count = total_count - 1")
|
||||
.update();
|
||||
ThrowUtils.throwIf(!update, ErrorCode.OPERATION_ERROR, "额度更新失败");
|
||||
return true;
|
||||
});
|
||||
//清理图片资源
|
||||
this.clearPictureFile(oldPicture);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void editPicture(PictureEditRequest pictureEditRequest, User loginUser) {
|
||||
// 在此处将实体类和 DTO 进行转换
|
||||
Picture picture = new Picture();
|
||||
BeanUtils.copyProperties(pictureEditRequest, picture);
|
||||
// 注意将 list 转为 string
|
||||
picture.setTags(JSONUtil.toJsonStr(pictureEditRequest.getTags()));
|
||||
// 设置编辑时间
|
||||
picture.setEditTime(new Date());
|
||||
// 数据校验
|
||||
this.validPicture(picture);
|
||||
// 判断是否存在
|
||||
long id = pictureEditRequest.getId();
|
||||
Picture oldPicture = this.getById(id);
|
||||
ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR);
|
||||
// 校验权限,已改为注解鉴权
|
||||
// checkPictureAuth(loginUser, oldPicture);
|
||||
// 补充审核 参数,每次编辑图片都要重新过审
|
||||
this.fillReviewParams(picture, loginUser);
|
||||
// 操作数据库
|
||||
boolean result = this.updateById(picture);
|
||||
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PictureVO getPictureVO(Picture picture, HttpServletRequest request) {
|
||||
// 对象转封装类
|
||||
@ -261,32 +398,6 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
||||
return pictureVOPage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void editPicture(PictureEditRequest pictureEditRequest, User loginUser) {
|
||||
// 在此处将实体类和 DTO 进行转换
|
||||
Picture picture = new Picture();
|
||||
BeanUtils.copyProperties(pictureEditRequest, picture);
|
||||
// 注意将 list 转为 string
|
||||
picture.setTags(JSONUtil.toJsonStr(pictureEditRequest.getTags()));
|
||||
// 设置编辑时间
|
||||
picture.setEditTime(new Date());
|
||||
// 数据校验
|
||||
this.validPicture(picture);
|
||||
// 补充审核参数,每次编辑图片都要重新过审
|
||||
this.fillReviewParams(picture, loginUser);
|
||||
// 判断是否存在
|
||||
long id = pictureEditRequest.getId();
|
||||
Picture oldPicture = this.getById(id);
|
||||
ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR);
|
||||
// 校验权限,仅本人及管理员可编辑
|
||||
if(!oldPicture.getUserId().equals(loginUser.getId())&&!userService.isAdmin(loginUser))
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
|
||||
|
||||
// 操作数据库
|
||||
boolean result = this.updateById(picture);
|
||||
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doPictureReview(PictureReviewRequest pictureReviewRequest, User loginUser) {
|
||||
// 1. 校验参数
|
||||
@ -458,9 +569,186 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
||||
// 删除缩略图
|
||||
String thumbnailUrl = oldPicture.getThumbnailUrl();
|
||||
cosManager.deleteIfNotBlank(thumbnailUrl);
|
||||
|
||||
}
|
||||
@Override
|
||||
public void checkPictureAuth(User loginUser, Picture picture) {
|
||||
Long spaceId = picture.getSpaceId();
|
||||
Long loginUserId = loginUser.getId();
|
||||
if (spaceId == null) {
|
||||
// 公共图库,仅本人或管理员可操作
|
||||
if (!picture.getUserId().equals(loginUserId) && !userService.isAdmin(loginUser)) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
|
||||
}
|
||||
} else {
|
||||
// 私有空间,仅空间管理员可操作
|
||||
if (!picture.getUserId().equals(loginUserId)) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ImageSearchResult> getSimilarPicture(SearchPictureByPictureRequest request) throws IOException {
|
||||
// 1.校验参数
|
||||
Long pictureId = request.getPictureId();
|
||||
ThrowUtils.throwIf(pictureId == null || pictureId <= 0 ,ErrorCode.NOT_FOUND_ERROR);
|
||||
|
||||
// 2.查询数据库
|
||||
Picture picture = this.getById(pictureId);
|
||||
ThrowUtils.throwIf(picture == null, ErrorCode.NOT_FOUND_ERROR);
|
||||
|
||||
// 3.提取图片在COS中的key
|
||||
|
||||
String pictureKey = cosManager.extractUploadPath(picture.getUrl());
|
||||
|
||||
// 4.下载文件 文件地址 = 当前项目路径 + 图片key(图片key带时间戳本来是唯一)
|
||||
String suffix = pictureKey.replace('/', '\\');
|
||||
String localPath = System.getProperty("user.dir") + "\\images\\" + suffix;
|
||||
cosManager.downloadPicture(pictureKey, localPath);
|
||||
|
||||
// 5.返回结果
|
||||
try {
|
||||
return ImageSearchApiFacade.searchImage(localPath);
|
||||
} finally {
|
||||
// 删除本地文件及文件夹
|
||||
File dir = new File(System.getProperty("user.dir") + "\\images\\");
|
||||
|
||||
if (dir.isDirectory()) {
|
||||
// 清空目录
|
||||
FileUtil.clean(dir);
|
||||
log.info("Directory cleaned successfully.");
|
||||
} else {
|
||||
log.info("The provided path is not a directory.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PictureVO> searchPictureByColor(Long spaceId, String picColor, User loginUser) {
|
||||
// 1. 校验参数
|
||||
ThrowUtils.throwIf(spaceId == null || StrUtil.isBlank(picColor), ErrorCode.PARAMS_ERROR);
|
||||
ThrowUtils.throwIf(loginUser == null, ErrorCode.NO_AUTH_ERROR);
|
||||
// 2. 校验空间权限
|
||||
Space space = spaceService.getById(spaceId);
|
||||
ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
|
||||
if (!space.getUserId().equals(loginUser.getId())) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "没有空间访问权限");
|
||||
}
|
||||
// 3. 查询该空间下的所有图片(必须要有主色调)
|
||||
List<Picture> pictureList = this.lambdaQuery()
|
||||
.eq(Picture::getSpaceId, spaceId)
|
||||
.isNotNull(Picture::getPicColor)
|
||||
.list();
|
||||
// 如果没有图片,直接返回空列表
|
||||
if (CollUtil.isEmpty(pictureList)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
// 将颜色字符串转换为主色调
|
||||
Color targetColor = Color.decode(picColor);
|
||||
// 4. 计算相似度并排序
|
||||
List<Picture> sortedPictureList = pictureList.stream()
|
||||
.sorted(Comparator.comparingDouble(picture -> {
|
||||
String hexColor = picture.getPicColor();
|
||||
// 没有主色调的图片会默认排序到最后
|
||||
if (StrUtil.isBlank(hexColor)) {
|
||||
return Double.MAX_VALUE;
|
||||
}
|
||||
Color pictureColor = Color.decode(hexColor);
|
||||
// 计算相似度
|
||||
// 越大越相似
|
||||
return -ColorSimilarUtils.calculateSimilarity(targetColor, pictureColor);
|
||||
}))
|
||||
.limit(12) // 取前 12 个
|
||||
.collect(Collectors.toList());
|
||||
// 5. 返回结果
|
||||
return sortedPictureList.stream()
|
||||
.map(PictureVO::objToVo)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void editPictureByBatch(PictureEditByBatchRequest pictureEditByBatchRequest, User loginUser) {
|
||||
// 1. 获取和校验参数
|
||||
List<Long> pictureIdList = pictureEditByBatchRequest.getPictureIdList();
|
||||
Long spaceId = pictureEditByBatchRequest.getSpaceId();
|
||||
String category = pictureEditByBatchRequest.getCategory();
|
||||
List<String> tags = pictureEditByBatchRequest.getTags();
|
||||
ThrowUtils.throwIf(CollUtil.isEmpty(pictureIdList), ErrorCode.PARAMS_ERROR);
|
||||
ThrowUtils.throwIf(spaceId == null, ErrorCode.PARAMS_ERROR);
|
||||
ThrowUtils.throwIf(loginUser == null, ErrorCode.NO_AUTH_ERROR);
|
||||
// 2. 校验空间权限
|
||||
Space space = spaceService.getById(spaceId);
|
||||
ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
|
||||
if (!space.getUserId().equals(loginUser.getId())) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "没有空间访问权限");
|
||||
}
|
||||
// 3. 查询指定图片(仅选择需要的字段)
|
||||
List<Picture> pictureList = this.lambdaQuery()
|
||||
.select(Picture::getId, Picture::getSpaceId)
|
||||
.eq(Picture::getSpaceId, spaceId)
|
||||
.in(Picture::getId, pictureIdList)
|
||||
.list();
|
||||
if (pictureList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 4. 更新分类和标签
|
||||
pictureList.forEach(picture -> {
|
||||
if (StrUtil.isNotBlank(category)) {
|
||||
picture.setCategory(category);
|
||||
}
|
||||
if (CollUtil.isNotEmpty(tags)) {
|
||||
picture.setTags(JSONUtil.toJsonStr(tags));
|
||||
}
|
||||
});
|
||||
// 批量重命名
|
||||
String nameRule = pictureEditByBatchRequest.getNameRule();
|
||||
fillPictureWithNameRule(pictureList, nameRule);
|
||||
// 5. 操作数据库进行批量更新
|
||||
boolean result = this.updateBatchById(pictureList);
|
||||
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR, "批量编辑失败");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreateOutPaintingTaskResponse createPictureOutPaintingTask(CreatePictureOutPaintingTaskRequest createPictureOutPaintingTaskRequest, User loginUser) {
|
||||
// 获取图片信息
|
||||
Long pictureId = createPictureOutPaintingTaskRequest.getPictureId();
|
||||
Picture picture = this.getById(pictureId);
|
||||
if (picture == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "图片不存在");
|
||||
}
|
||||
// 校验权限,已经改为使用注解鉴权
|
||||
// checkPictureAuth(loginUser, picture);
|
||||
// 创建扩图任务
|
||||
CreateOutPaintingTaskRequest createOutPaintingTaskRequest = new CreateOutPaintingTaskRequest();
|
||||
CreateOutPaintingTaskRequest.Input input = new CreateOutPaintingTaskRequest.Input();
|
||||
input.setImageUrl(picture.getUrl());
|
||||
createOutPaintingTaskRequest.setInput(input);
|
||||
createOutPaintingTaskRequest.setParameters(createPictureOutPaintingTaskRequest.getParameters());
|
||||
// 创建任务
|
||||
return aliYunAiApi.createOutPaintingTask(createOutPaintingTaskRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* nameRule 格式:图片-{序号} =>图片-1 图片-2 ...
|
||||
*
|
||||
* @param pictureList
|
||||
* @param nameRule
|
||||
*/
|
||||
private void fillPictureWithNameRule(List<Picture> pictureList, String nameRule) {
|
||||
if (StrUtil.isBlank(nameRule) || CollUtil.isEmpty(pictureList)) {
|
||||
return;
|
||||
}
|
||||
long count = 1;
|
||||
try {
|
||||
for (Picture picture : pictureList) {
|
||||
String pictureName = nameRule.replaceAll("\\{序号}", String.valueOf(count++));
|
||||
picture.setName(pictureName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("名称解析错误", e);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "名称解析错误");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -0,0 +1,308 @@
|
||||
package edu.whut.smilepicturebackend.service.impl;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import edu.whut.smilepicturebackend.exception.ThrowUtils;
|
||||
import edu.whut.smilepicturebackend.mapper.SpaceMapper;
|
||||
import edu.whut.smilepicturebackend.model.dto.space.analyze.*;
|
||||
import edu.whut.smilepicturebackend.model.entity.Picture;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
import edu.whut.smilepicturebackend.model.vo.space.analyze.*;
|
||||
import edu.whut.smilepicturebackend.service.PictureService;
|
||||
import edu.whut.smilepicturebackend.service.SpaceAnalyzeService;
|
||||
import edu.whut.smilepicturebackend.service.SpaceService;
|
||||
import edu.whut.smilepicturebackend.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author 张三
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SpaceAnalyzeServiceImpl extends ServiceImpl<SpaceMapper, Space>
|
||||
implements SpaceAnalyzeService {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
private final SpaceService spaceService;
|
||||
|
||||
private final PictureService pictureService;
|
||||
|
||||
@Override
|
||||
public SpaceUsageAnalyzeResponse getSpaceUsageAnalyze(SpaceUsageAnalyzeRequest spaceUsageAnalyzeRequest, User loginUser) {
|
||||
// 校验参数
|
||||
// 全空间或公共图库,需要从 Picture 表查询
|
||||
if (spaceUsageAnalyzeRequest.isQueryAll() || spaceUsageAnalyzeRequest.isQueryPublic()) {
|
||||
// 权限校验,仅管理员可以访问
|
||||
checkSpaceAnalyzeAuth(spaceUsageAnalyzeRequest, loginUser);
|
||||
// 统计图库的使用空间
|
||||
QueryWrapper<Picture> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.select("pic_size");
|
||||
// 补充查询范围
|
||||
fillAnalyzeQueryWrapper(spaceUsageAnalyzeRequest, queryWrapper);
|
||||
List<Object> pictureObjList = pictureService.getBaseMapper().selectObjs(queryWrapper); //不用pictureService.list(),它会查完整的一条记录,而我们仅需一列
|
||||
long usedSize = 0L;
|
||||
for (Object obj : pictureObjList) {
|
||||
// 记得把 Object 强转成 Long,再拆箱加到累加器里
|
||||
usedSize += (Long) obj;
|
||||
}
|
||||
long usedCount = pictureObjList.size();
|
||||
// 封装返回结果
|
||||
SpaceUsageAnalyzeResponse spaceUsageAnalyzeResponse = new SpaceUsageAnalyzeResponse();
|
||||
spaceUsageAnalyzeResponse.setUsedSize(usedSize);
|
||||
spaceUsageAnalyzeResponse.setUsedCount(usedCount);
|
||||
// 公共图库(或者全部空间)无数量和容量限制、也没有比例
|
||||
spaceUsageAnalyzeResponse.setMaxSize(null);
|
||||
spaceUsageAnalyzeResponse.setSizeUsageRatio(null);
|
||||
spaceUsageAnalyzeResponse.setMaxCount(null);
|
||||
spaceUsageAnalyzeResponse.setCountUsageRatio(null);
|
||||
return spaceUsageAnalyzeResponse;
|
||||
} else {
|
||||
// 特定空间可以直接从 Space 表查询!
|
||||
Long spaceId = spaceUsageAnalyzeRequest.getSpaceId();
|
||||
ThrowUtils.throwIf(spaceId == null || spaceId <= 0, ErrorCode.PARAMS_ERROR);
|
||||
// 获取空间信息
|
||||
Space space = spaceService.getById(spaceId);
|
||||
ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
|
||||
// 权限校验,仅管理员可以访问
|
||||
checkSpaceAnalyzeAuth(spaceUsageAnalyzeRequest, loginUser);
|
||||
// 封装返回结果
|
||||
SpaceUsageAnalyzeResponse spaceUsageAnalyzeResponse = new SpaceUsageAnalyzeResponse();
|
||||
spaceUsageAnalyzeResponse.setUsedSize(space.getTotalSize());
|
||||
spaceUsageAnalyzeResponse.setUsedCount(space.getTotalCount());
|
||||
spaceUsageAnalyzeResponse.setMaxSize(space.getMaxSize());
|
||||
spaceUsageAnalyzeResponse.setMaxCount(space.getMaxCount());
|
||||
// 计算比例
|
||||
double sizeUsageRatio = NumberUtil.round(space.getTotalSize() * 100.0 / space.getMaxSize(), 2).doubleValue();
|
||||
double countUsageRatio = NumberUtil.round(space.getTotalCount() * 100.0 / space.getMaxCount(), 2).doubleValue();
|
||||
spaceUsageAnalyzeResponse.setSizeUsageRatio(sizeUsageRatio);
|
||||
spaceUsageAnalyzeResponse.setCountUsageRatio(countUsageRatio);
|
||||
return spaceUsageAnalyzeResponse;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SpaceCategoryAnalyzeResponse> getSpaceCategoryAnalyze(SpaceCategoryAnalyzeRequest spaceCategoryAnalyzeRequest, User loginUser) {
|
||||
ThrowUtils.throwIf(spaceCategoryAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
// 检查权限
|
||||
checkSpaceAnalyzeAuth(spaceCategoryAnalyzeRequest, loginUser);
|
||||
|
||||
// 构造查询条件
|
||||
QueryWrapper<Picture> queryWrapper = new QueryWrapper<>();
|
||||
fillAnalyzeQueryWrapper(spaceCategoryAnalyzeRequest, queryWrapper);
|
||||
|
||||
// 使用 MyBatis Plus 分组查询
|
||||
queryWrapper.select("category", "count(*) as count", "sum(pic_size) as total_size")
|
||||
.groupBy("category");
|
||||
|
||||
// 查询并转换结果
|
||||
return pictureService.getBaseMapper().selectMaps(queryWrapper)
|
||||
.stream()
|
||||
.map(result -> {
|
||||
String category = (String) result.get("category");
|
||||
Long count = ((Number) result.get("count")).longValue();
|
||||
Long totalSize = ((Number) result.get("total_size")).longValue();
|
||||
return new SpaceCategoryAnalyzeResponse(category, count, totalSize);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SpaceTagAnalyzeResponse> getSpaceTagAnalyze(SpaceTagAnalyzeRequest spaceTagAnalyzeRequest, User loginUser) {
|
||||
ThrowUtils.throwIf(spaceTagAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
// 检查权限
|
||||
checkSpaceAnalyzeAuth(spaceTagAnalyzeRequest, loginUser);
|
||||
|
||||
// 构造查询条件
|
||||
QueryWrapper<Picture> queryWrapper = new QueryWrapper<>();
|
||||
fillAnalyzeQueryWrapper(spaceTagAnalyzeRequest, queryWrapper);
|
||||
// 查询所有符合条件的标签
|
||||
queryWrapper.select("tags");
|
||||
List<String> tagsJsonList = pictureService.getBaseMapper().selectObjs(queryWrapper)
|
||||
.stream()
|
||||
.filter(ObjUtil::isNotNull)
|
||||
.map(Object::toString)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 解析标签并统计
|
||||
Map<String, Long> tagCountMap = tagsJsonList.stream()
|
||||
// ["Java", "Python"], ["Java", "PHP"] => "Java", "Python", "Java", "PHP"
|
||||
.flatMap(tagsJson -> JSONUtil.toList(tagsJson, String.class).stream())
|
||||
.collect(Collectors.groupingBy(tag -> tag, Collectors.counting()));
|
||||
|
||||
// 转换为响应对象,按照使用次数进行排序
|
||||
return tagCountMap.entrySet().stream()
|
||||
.sorted((e1, e2) -> Long.compare(e2.getValue(), e1.getValue())) // 降序排序
|
||||
.map(entry -> new SpaceTagAnalyzeResponse(entry.getKey(), entry.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SpaceSizeAnalyzeResponse> getSpaceSizeAnalyze(SpaceSizeAnalyzeRequest spaceSizeAnalyzeRequest, User loginUser) {
|
||||
ThrowUtils.throwIf(spaceSizeAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
// 检查权限
|
||||
checkSpaceAnalyzeAuth(spaceSizeAnalyzeRequest, loginUser);
|
||||
|
||||
// 构造查询条件
|
||||
QueryWrapper<Picture> queryWrapper = new QueryWrapper<>();
|
||||
fillAnalyzeQueryWrapper(spaceSizeAnalyzeRequest, queryWrapper);
|
||||
|
||||
// 查询所有符合条件的图片大小
|
||||
queryWrapper.select("pic_size");
|
||||
// 100、120、1000
|
||||
List<Long> picSizeList = pictureService.getBaseMapper().selectObjs(queryWrapper)
|
||||
.stream()
|
||||
.filter(ObjUtil::isNotNull)
|
||||
.map(size -> (Long) size)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 定义分段范围,注意使用有序的 Map
|
||||
Map<String, Long> sizeRanges = new LinkedHashMap<>();
|
||||
sizeRanges.put("<100KB", picSizeList.stream().filter(size -> size < 100 * 1024).count());
|
||||
sizeRanges.put("100KB-500KB", picSizeList.stream().filter(size -> size >= 100 * 1024 && size < 500 * 1024).count());
|
||||
sizeRanges.put("500KB-1MB", picSizeList.stream().filter(size -> size >= 500 * 1024 && size < 1 * 1024 * 1024).count());
|
||||
sizeRanges.put(">1MB", picSizeList.stream().filter(size -> size >= 1 * 1024 * 1024).count());
|
||||
|
||||
// 转换为响应对象
|
||||
return sizeRanges.entrySet().stream()
|
||||
.map(entry -> new SpaceSizeAnalyzeResponse(entry.getKey(), entry.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据前端传来的 时间维度(按天 / 周 / 月)来动态拼出不同的 SQL 聚合字段,然后再按照这个“时间段”字段去分组和排序,最终拿到每个周期(period)里上传的图片数量
|
||||
* @param spaceUserAnalyzeRequest
|
||||
* @param loginUser
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public List<SpaceUserAnalyzeResponse> getSpaceUserAnalyze(SpaceUserAnalyzeRequest spaceUserAnalyzeRequest, User loginUser) {
|
||||
ThrowUtils.throwIf(spaceUserAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
// 检查权限
|
||||
checkSpaceAnalyzeAuth(spaceUserAnalyzeRequest, loginUser);
|
||||
|
||||
// 构造查询条件
|
||||
QueryWrapper<Picture> queryWrapper = new QueryWrapper<>();
|
||||
fillAnalyzeQueryWrapper(spaceUserAnalyzeRequest, queryWrapper);
|
||||
// 补充用户 id 查询
|
||||
Long userId = spaceUserAnalyzeRequest.getUserId();
|
||||
queryWrapper.eq(ObjUtil.isNotNull(userId), "user_id", userId);
|
||||
// 补充分析维度:每日、每周、每月
|
||||
/**
|
||||
* eg:
|
||||
* SELECT
|
||||
* DATE_FORMAT(create_time, '%Y-%m') AS period,
|
||||
* COUNT(*) AS count
|
||||
* FROM picture
|
||||
* … (其他 WHERE 条件)
|
||||
* GROUP BY period
|
||||
* ORDER BY period ASC;
|
||||
*/
|
||||
String timeDimension = spaceUserAnalyzeRequest.getTimeDimension();
|
||||
switch (timeDimension) {
|
||||
case "day":
|
||||
queryWrapper.select("DATE_FORMAT(create_time, '%Y-%m-%d') as period", "count(*) as count");
|
||||
break;
|
||||
case "week":
|
||||
queryWrapper.select("YEARWEEK(create_time) as period", "count(*) as count");
|
||||
break;
|
||||
case "month":
|
||||
queryWrapper.select("DATE_FORMAT(create_time, '%Y-%m') as period", "count(*) as count");
|
||||
break;
|
||||
default:
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "不支持的时间维度");
|
||||
}
|
||||
|
||||
// 分组排序
|
||||
queryWrapper.groupBy("period").orderByAsc("period");
|
||||
|
||||
// 查询并封装结果
|
||||
List<Map<String, Object>> queryResult = pictureService.getBaseMapper().selectMaps(queryWrapper);
|
||||
return queryResult
|
||||
.stream()
|
||||
.map(result -> {
|
||||
String period = result.get("period").toString();
|
||||
Long count = ((Number) result.get("count")).longValue();
|
||||
return new SpaceUserAnalyzeResponse(period, count);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Space> getSpaceRankAnalyze(SpaceRankAnalyzeRequest spaceRankAnalyzeRequest, User loginUser) {
|
||||
ThrowUtils.throwIf(spaceRankAnalyzeRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
|
||||
// 检查权限,仅管理员可以查看
|
||||
ThrowUtils.throwIf(!userService.isAdmin(loginUser), ErrorCode.NO_AUTH_ERROR);
|
||||
|
||||
// 构造查询条件
|
||||
QueryWrapper<Space> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.select("id", "space_name", "user_id", "total_size")
|
||||
.orderByDesc("total_size")
|
||||
.last("limit " + spaceRankAnalyzeRequest.getTopN()); // 取前 N 名
|
||||
|
||||
// 查询并封装结果
|
||||
return spaceService.list(queryWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 公共方法:校验空间分析权限
|
||||
*
|
||||
* @param spaceAnalyzeRequest
|
||||
* @param loginUser
|
||||
*/
|
||||
private void checkSpaceAnalyzeAuth(SpaceAnalyzeRequest spaceAnalyzeRequest, User loginUser) {
|
||||
boolean queryPublic = spaceAnalyzeRequest.isQueryPublic();
|
||||
boolean queryAll = spaceAnalyzeRequest.isQueryAll();
|
||||
// 全空间分析或者公共图库权限校验:仅管理员可访问
|
||||
if (queryAll || queryPublic) {
|
||||
ThrowUtils.throwIf(!userService.isAdmin(loginUser), ErrorCode.NO_AUTH_ERROR);
|
||||
} else {
|
||||
// 分析特定空间,仅本人或管理员可以访问
|
||||
Long spaceId = spaceAnalyzeRequest.getSpaceId();
|
||||
ThrowUtils.throwIf(spaceId == null, ErrorCode.PARAMS_ERROR);
|
||||
Space space = spaceService.getById(spaceId);
|
||||
ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
|
||||
spaceService.checkSpaceAuth(loginUser, space);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 公共方法:根据请求对象封装查询条件
|
||||
*
|
||||
* @param spaceAnalyzeRequest
|
||||
* @param queryWrapper
|
||||
*/
|
||||
private void fillAnalyzeQueryWrapper(SpaceAnalyzeRequest spaceAnalyzeRequest, QueryWrapper<Picture> queryWrapper) {
|
||||
// 全空间分析
|
||||
boolean queryAll = spaceAnalyzeRequest.isQueryAll();
|
||||
if (queryAll) {
|
||||
return;
|
||||
}
|
||||
// 公共图库
|
||||
boolean queryPublic = spaceAnalyzeRequest.isQueryPublic();
|
||||
if (queryPublic) {
|
||||
queryWrapper.isNull("space_id");
|
||||
return;
|
||||
}
|
||||
// 分析特定空间
|
||||
Long spaceId = spaceAnalyzeRequest.getSpaceId();
|
||||
if (spaceId != null) {
|
||||
queryWrapper.eq("space_id", spaceId);
|
||||
return;
|
||||
}
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "未指定查询范围");
|
||||
}
|
||||
}
|
@ -0,0 +1,262 @@
|
||||
package edu.whut.smilepicturebackend.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import edu.whut.smilepicturebackend.exception.ThrowUtils;
|
||||
import edu.whut.smilepicturebackend.mapper.SpaceMapper;
|
||||
|
||||
import edu.whut.smilepicturebackend.model.dto.space.SpaceAddRequest;
|
||||
import edu.whut.smilepicturebackend.model.dto.space.SpaceQueryRequest;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
import edu.whut.smilepicturebackend.model.entity.SpaceUser;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
import edu.whut.smilepicturebackend.model.enums.SpaceLevelEnum;
|
||||
import edu.whut.smilepicturebackend.model.enums.SpaceRoleEnum;
|
||||
import edu.whut.smilepicturebackend.model.enums.SpaceTypeEnum;
|
||||
import edu.whut.smilepicturebackend.model.vo.SpaceVO;
|
||||
import edu.whut.smilepicturebackend.model.vo.UserVO;
|
||||
import edu.whut.smilepicturebackend.service.SpaceService;
|
||||
import edu.whut.smilepicturebackend.service.SpaceUserService;
|
||||
import edu.whut.smilepicturebackend.service.UserService;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author 张三
|
||||
* @description 针对表【space(空间)】的数据库操作Service实现
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SpaceServiceImpl extends ServiceImpl<SpaceMapper, Space>
|
||||
implements SpaceService {
|
||||
private final UserService userService;
|
||||
// 静态锁表,JVM 级别共享
|
||||
private static final ConcurrentHashMap<Long, Object> USER_LOCKS = new ConcurrentHashMap<>();
|
||||
|
||||
private final SpaceUserService spaceUserService;
|
||||
|
||||
private final TransactionTemplate transactionTemplate;
|
||||
// @Resource
|
||||
// @Lazy
|
||||
// private DynamicShardingManager dynamicShardingManager;
|
||||
|
||||
/**
|
||||
* 创建空间 加锁和事务
|
||||
*
|
||||
* @param spaceAddRequest
|
||||
* @param loginUser
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public long addSpace(SpaceAddRequest spaceAddRequest, User loginUser) {
|
||||
// 1. 填充参数默认值
|
||||
// 转换实体类和 DTO
|
||||
Space space = new Space();
|
||||
BeanUtils.copyProperties(spaceAddRequest, space);
|
||||
if (StrUtil.isBlank(space.getSpaceName())) {
|
||||
space.setSpaceName("默认空间");
|
||||
}
|
||||
if (space.getSpaceLevel() == null) {
|
||||
space.setSpaceLevel(SpaceLevelEnum.COMMON.getValue());
|
||||
}
|
||||
if (space.getSpaceType() == null) {
|
||||
space.setSpaceType(SpaceTypeEnum.PRIVATE.getValue());
|
||||
}
|
||||
// 填充容量和大小
|
||||
this.fillSpaceBySpaceLevel(space);
|
||||
// 2. 校验参数
|
||||
this.validSpace(space, true);
|
||||
// 3. 校验权限,非管理员只能创建普通级别的空间
|
||||
Long userId = loginUser.getId();
|
||||
space.setUserId(userId);
|
||||
if (SpaceLevelEnum.COMMON.getValue() != space.getSpaceLevel() && !userService.isAdmin(loginUser)) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权限创建指定级别的空间");
|
||||
}
|
||||
// 4. 控制同一用户只能创建一个私有空间、以及一个团队空间
|
||||
//// 1. 获取或创建该 userId 对应的锁对象,避免使用String.intern()来定义锁,能避免常量池内容越来越多。
|
||||
Object lock = USER_LOCKS.computeIfAbsent(userId, id -> new Object());
|
||||
synchronized (lock) {
|
||||
try {
|
||||
// 2. 在事务内检查并创建空间
|
||||
Long newSpaceId = transactionTemplate.execute(status -> {
|
||||
boolean exists = this.lambdaQuery()
|
||||
.eq(Space::getUserId, userId)
|
||||
.eq(Space::getSpaceType, space.getSpaceType())
|
||||
.exists();
|
||||
ThrowUtils.throwIf(exists, ErrorCode.OPERATION_ERROR, "每个用户每类空间只能创建一个");
|
||||
|
||||
boolean result = this.save(space);
|
||||
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR, "保存空间到数据库失败");
|
||||
// 创建成功后,如果是团队空间,默认将创建人加入团队且视为管理员
|
||||
if (SpaceTypeEnum.TEAM.getValue() == space.getSpaceType()) {
|
||||
SpaceUser spaceUser = new SpaceUser();
|
||||
spaceUser.setSpaceId(space.getId());
|
||||
spaceUser.setUserId(userId);
|
||||
spaceUser.setSpaceRole(SpaceRoleEnum.ADMIN.getValue());
|
||||
result = spaceUserService.save(spaceUser);
|
||||
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR, "创建团队成员记录失败");
|
||||
}
|
||||
//创建分表(仅对团队空间生效)为方便部署,暂时不使用
|
||||
// dynamicShardingManager.createSpacePictureTable(space);
|
||||
return space.getId();
|
||||
});
|
||||
return Optional.ofNullable(newSpaceId).orElse(-1L);
|
||||
} finally {
|
||||
// 3. 可选:移除锁对象,防止 Map 膨胀(仅当你确定没有并发需求时才移除)
|
||||
USER_LOCKS.remove(userId, lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validSpace(Space space, boolean add) {
|
||||
ThrowUtils.throwIf(space == null, ErrorCode.PARAMS_ERROR);
|
||||
// 从对象中取值
|
||||
String spaceName = space.getSpaceName();
|
||||
Integer spaceLevel = space.getSpaceLevel();
|
||||
SpaceLevelEnum spaceLevelEnum = SpaceLevelEnum.getEnumByValue(spaceLevel);
|
||||
Integer spaceType = space.getSpaceType();
|
||||
SpaceTypeEnum spaceTypeEnum = SpaceTypeEnum.getEnumByValue(spaceType);
|
||||
|
||||
// 创建时校验
|
||||
if (add) {
|
||||
if (StrUtil.isBlank(spaceName)) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "空间名称不能为空");
|
||||
}
|
||||
if (spaceLevel == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "空间级别不能为空");
|
||||
}
|
||||
if (spaceType == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "空间类别不能为空");
|
||||
}
|
||||
}
|
||||
// 修改数据时,空间名称进行校验
|
||||
if (StrUtil.isNotBlank(spaceName) && spaceName.length() > 30) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "空间名称过长");
|
||||
}
|
||||
// 修改数据时,空间级别进行校验
|
||||
if (spaceLevel != null && spaceLevelEnum == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "空间级别不存在");
|
||||
}
|
||||
// 修改数据时,空间类别进行校验
|
||||
if (spaceType != null && spaceTypeEnum == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "空间类别不存在");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpaceVO getSpaceVO(Space space, HttpServletRequest request) {
|
||||
// 对象转封装类
|
||||
SpaceVO spaceVO = SpaceVO.objToVo(space);
|
||||
// 关联查询用户信息
|
||||
Long userId = space.getUserId();
|
||||
if (userId != null && userId > 0) {
|
||||
User user = userService.getById(userId);
|
||||
UserVO userVO = userService.getUserVO(user);
|
||||
spaceVO.setUser(userVO);
|
||||
}
|
||||
return spaceVO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<SpaceVO> getSpaceVOPage(Page<Space> spacePage, HttpServletRequest request) {
|
||||
List<Space> spaceList = spacePage.getRecords();
|
||||
Page<SpaceVO> spaceVOPage = new Page<>(spacePage.getCurrent(), spacePage.getSize(), spacePage.getTotal());
|
||||
if (CollUtil.isEmpty(spaceList)) {
|
||||
return spaceVOPage;
|
||||
}
|
||||
// 对象列表 => 封装对象列表
|
||||
List<SpaceVO> spaceVOList = spaceList.stream()
|
||||
.map(SpaceVO::objToVo)
|
||||
.collect(Collectors.toList());
|
||||
// 1. 关联查询用户信息
|
||||
// 1,2,3,4
|
||||
Set<Long> userIdSet = spaceList.stream().map(Space::getUserId).collect(Collectors.toSet());
|
||||
// 1 => user1, 2 => user2
|
||||
Map<Long, List<User>> userIdUserListMap = userService.listByIds(userIdSet).stream()
|
||||
.collect(Collectors.groupingBy(User::getId));
|
||||
// 2. 填充信息
|
||||
spaceVOList.forEach(spaceVO -> {
|
||||
Long userId = spaceVO.getUserId();
|
||||
User user = null;
|
||||
if (userIdUserListMap.containsKey(userId)) {
|
||||
user = userIdUserListMap.get(userId).get(0);
|
||||
}
|
||||
spaceVO.setUser(userService.getUserVO(user));
|
||||
});
|
||||
spaceVOPage.setRecords(spaceVOList);
|
||||
return spaceVOPage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LambdaQueryWrapper<Space> getQueryWrapper(SpaceQueryRequest req) {
|
||||
if (req == null) {
|
||||
// 请求参数为空时,抛出异常或根据业务自行处理
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");
|
||||
}
|
||||
// 创建 LambdaQueryWrapper
|
||||
LambdaQueryWrapper<Space> qw = Wrappers.lambdaQuery(Space.class);
|
||||
|
||||
// 基本等值与模糊匹配
|
||||
qw.eq(ObjUtil.isNotEmpty(req.getId()), Space::getId, req.getId())
|
||||
.eq(ObjUtil.isNotEmpty(req.getUserId()), Space::getUserId, req.getUserId())
|
||||
.like(StrUtil.isNotBlank(req.getSpaceName()), Space::getSpaceName, req.getSpaceName())
|
||||
.eq(ObjUtil.isNotEmpty(req.getSpaceLevel()), Space::getSpaceLevel, req.getSpaceLevel())
|
||||
.eq(ObjUtil.isNotEmpty(req.getSpaceType()), Space::getSpaceType,req.getSpaceType());
|
||||
|
||||
// 动态排序:将驼峰字段转成下划线,再拼到 SQL 的 ORDER BY 中
|
||||
if (StrUtil.isNotBlank(req.getSortField())) {
|
||||
String column = StrUtil.toUnderlineCase(req.getSortField());
|
||||
boolean asc = "ascend".equalsIgnoreCase(req.getSortOrder());
|
||||
qw.last("ORDER BY " + column + (asc ? " ASC" : " DESC"));
|
||||
}
|
||||
return qw;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillSpaceBySpaceLevel(Space space) {
|
||||
SpaceLevelEnum spaceLevelEnum = SpaceLevelEnum.getEnumByValue(space.getSpaceLevel());
|
||||
if (spaceLevelEnum != null) {
|
||||
long maxSize = spaceLevelEnum.getMaxSize();
|
||||
if (space.getMaxSize() == null) {
|
||||
space.setMaxSize(maxSize);
|
||||
}
|
||||
long maxCount = spaceLevelEnum.getMaxCount();
|
||||
if (space.getMaxCount() == null) {
|
||||
space.setMaxCount(maxCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkSpaceAuth(User loginUser, Space space) {
|
||||
// 仅本人或管理员可编辑
|
||||
if (!space.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,161 @@
|
||||
package edu.whut.smilepicturebackend.service.impl;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import edu.whut.smilepicturebackend.exception.ThrowUtils;
|
||||
import edu.whut.smilepicturebackend.mapper.SpaceUserMapper;
|
||||
import edu.whut.smilepicturebackend.model.dto.spaceuser.SpaceUserAddRequest;
|
||||
import edu.whut.smilepicturebackend.model.dto.spaceuser.SpaceUserQueryRequest;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
import edu.whut.smilepicturebackend.model.entity.SpaceUser;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
import edu.whut.smilepicturebackend.model.enums.SpaceRoleEnum;
|
||||
import edu.whut.smilepicturebackend.model.vo.SpaceUserVO;
|
||||
import edu.whut.smilepicturebackend.model.vo.SpaceVO;
|
||||
import edu.whut.smilepicturebackend.model.vo.UserVO;
|
||||
import edu.whut.smilepicturebackend.service.SpaceService;
|
||||
import edu.whut.smilepicturebackend.service.SpaceUserService;
|
||||
import edu.whut.smilepicturebackend.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author 张三
|
||||
* @description 针对表【space_user(空间用户关联)】的数据库操作Service实现
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SpaceUserServiceImpl extends ServiceImpl<SpaceUserMapper, SpaceUser>
|
||||
implements SpaceUserService {
|
||||
private final UserService userService;
|
||||
|
||||
@Resource
|
||||
@Lazy
|
||||
private SpaceService spaceService;
|
||||
|
||||
@Override
|
||||
public long addSpaceUser(SpaceUserAddRequest spaceUserAddRequest) {
|
||||
// 参数校验
|
||||
ThrowUtils.throwIf(spaceUserAddRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
SpaceUser spaceUser = new SpaceUser();
|
||||
BeanUtils.copyProperties(spaceUserAddRequest, spaceUser);
|
||||
validSpaceUser(spaceUser, true);
|
||||
// 数据库操作
|
||||
boolean result = this.save(spaceUser);
|
||||
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
|
||||
return spaceUser.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validSpaceUser(SpaceUser spaceUser, boolean add) {
|
||||
ThrowUtils.throwIf(spaceUser == null, ErrorCode.PARAMS_ERROR);
|
||||
// 创建时,空间 id 和用户 id 必填
|
||||
Long spaceId = spaceUser.getSpaceId();
|
||||
Long userId = spaceUser.getUserId();
|
||||
if (add) {
|
||||
ThrowUtils.throwIf(ObjectUtil.hasEmpty(spaceId, userId), ErrorCode.PARAMS_ERROR);
|
||||
User user = userService.getById(userId);
|
||||
ThrowUtils.throwIf(user == null, ErrorCode.NOT_FOUND_ERROR, "用户不存在");
|
||||
Space space = spaceService.getById(spaceId);
|
||||
ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
|
||||
}
|
||||
// 校验空间角色
|
||||
String spaceRole = spaceUser.getSpaceRole();
|
||||
SpaceRoleEnum spaceRoleEnum = SpaceRoleEnum.getEnumByValue(spaceRole);
|
||||
if (spaceRole != null && spaceRoleEnum == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "空间角色不存在");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpaceUserVO getSpaceUserVO(SpaceUser spaceUser, HttpServletRequest request) {
|
||||
// 对象转封装类
|
||||
SpaceUserVO spaceUserVO = SpaceUserVO.objToVo(spaceUser);
|
||||
// 关联查询用户信息
|
||||
Long userId = spaceUser.getUserId();
|
||||
if (userId != null && userId > 0) {
|
||||
User user = userService.getById(userId);
|
||||
UserVO userVO = userService.getUserVO(user);
|
||||
spaceUserVO.setUser(userVO);
|
||||
}
|
||||
// 关联查询空间信息
|
||||
Long spaceId = spaceUser.getSpaceId();
|
||||
if (spaceId != null && spaceId > 0) {
|
||||
Space space = spaceService.getById(spaceId);
|
||||
SpaceVO spaceVO = spaceService.getSpaceVO(space, request);
|
||||
spaceUserVO.setSpace(spaceVO);
|
||||
}
|
||||
return spaceUserVO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SpaceUserVO> getSpaceUserVOList(List<SpaceUser> spaceUserList) {
|
||||
// 判断输入列表是否为空
|
||||
if (CollUtil.isEmpty(spaceUserList)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
// 对象列表 => 封装对象列表
|
||||
List<SpaceUserVO> spaceUserVOList = spaceUserList.stream().map(SpaceUserVO::objToVo).collect(Collectors.toList());
|
||||
// 1. 收集需要关联查询的用户 ID 和空间 ID
|
||||
Set<Long> userIdSet = spaceUserList.stream().map(SpaceUser::getUserId).collect(Collectors.toSet());
|
||||
Set<Long> spaceIdSet = spaceUserList.stream().map(SpaceUser::getSpaceId).collect(Collectors.toSet());
|
||||
// 2. 批量查询用户和空间
|
||||
Map<Long, List<User>> userIdUserListMap = userService.listByIds(userIdSet).stream()
|
||||
.collect(Collectors.groupingBy(User::getId));
|
||||
Map<Long, List<Space>> spaceIdSpaceListMap = spaceService.listByIds(spaceIdSet).stream()
|
||||
.collect(Collectors.groupingBy(Space::getId));
|
||||
// 3. 填充 SpaceUserVO 的用户和空间信息
|
||||
spaceUserVOList.forEach(spaceUserVO -> {
|
||||
Long userId = spaceUserVO.getUserId();
|
||||
Long spaceId = spaceUserVO.getSpaceId();
|
||||
// 填充用户信息
|
||||
User user = null;
|
||||
if (userIdUserListMap.containsKey(userId)) {
|
||||
user = userIdUserListMap.get(userId).get(0);
|
||||
}
|
||||
spaceUserVO.setUser(userService.getUserVO(user));
|
||||
// 填充空间信息
|
||||
Space space = null;
|
||||
if (spaceIdSpaceListMap.containsKey(spaceId)) {
|
||||
space = spaceIdSpaceListMap.get(spaceId).get(0);
|
||||
}
|
||||
spaceUserVO.setSpace(SpaceVO.objToVo(space));
|
||||
});
|
||||
return spaceUserVOList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LambdaQueryWrapper<SpaceUser> getQueryWrapper(SpaceUserQueryRequest req) {
|
||||
if (req == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<SpaceUser> qw = Wrappers.lambdaQuery(SpaceUser.class);
|
||||
|
||||
qw.eq(ObjUtil.isNotEmpty(req.getId()), SpaceUser::getId, req.getId())
|
||||
.eq(ObjUtil.isNotEmpty(req.getSpaceId()), SpaceUser::getSpaceId, req.getSpaceId())
|
||||
.eq(ObjUtil.isNotEmpty(req.getUserId()), SpaceUser::getUserId, req.getUserId())
|
||||
.eq(ObjUtil.isNotEmpty(req.getSpaceRole()),SpaceUser::getSpaceRole, req.getSpaceRole());
|
||||
return qw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -14,6 +14,7 @@ import edu.whut.smilepicturebackend.constant.UserConstant;
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import edu.whut.smilepicturebackend.exception.ThrowUtils;
|
||||
import edu.whut.smilepicturebackend.manager.auth.StpKit;
|
||||
import edu.whut.smilepicturebackend.model.dto.user.UserAddRequest;
|
||||
import edu.whut.smilepicturebackend.model.dto.user.UserQueryRequest;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
@ -37,7 +38,6 @@ import java.util.stream.Collectors;
|
||||
/**
|
||||
* @author 张三
|
||||
* @description 针对表【user(用户)】的数据库操作Service实现
|
||||
* @createDate 2025-06-05 17:43:52
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@ -128,6 +128,9 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
|
||||
}
|
||||
// 4. 保存用户的登录态
|
||||
request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, user);
|
||||
// 记录用户登录态到 Sa-token,便于空间鉴权时使用,注意保证该用户信息与 SpringSession 中的信息过期时间一致
|
||||
StpKit.SPACE.login(user.getId());
|
||||
StpKit.SPACE.getSession().set(UserConstant.USER_LOGIN_STATE, user);
|
||||
return this.getLoginUserVO(user);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,63 @@
|
||||
package edu.whut.smilepicturebackend.utils;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* 工具类:计算颜色相似度
|
||||
*/
|
||||
public class ColorSimilarUtils {
|
||||
|
||||
private ColorSimilarUtils() {
|
||||
// 工具类不需要实例化
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个颜色的相似度
|
||||
*
|
||||
* @param color1 第一个颜色
|
||||
* @param color2 第二个颜色
|
||||
* @return 相似度(0到1之间,1为完全相同)
|
||||
*/
|
||||
public static double calculateSimilarity(Color color1, Color color2) {
|
||||
int r1 = color1.getRed();
|
||||
int g1 = color1.getGreen();
|
||||
int b1 = color1.getBlue();
|
||||
|
||||
int r2 = color2.getRed();
|
||||
int g2 = color2.getGreen();
|
||||
int b2 = color2.getBlue();
|
||||
|
||||
// 计算欧氏距离
|
||||
double distance = Math.sqrt(Math.pow(r1 - r2, 2) + Math.pow(g1 - g2, 2) + Math.pow(b1 - b2, 2));
|
||||
|
||||
// 计算相似度
|
||||
return 1 - distance / Math.sqrt(3 * Math.pow(255, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据十六进制颜色代码计算相似度
|
||||
*
|
||||
* @param hexColor1 第一个颜色的十六进制代码(如 0xFF0000)
|
||||
* @param hexColor2 第二个颜色的十六进制代码(如 0xFE0101)
|
||||
* @return 相似度(0到1之间,1为完全相同)
|
||||
*/
|
||||
public static double calculateSimilarity(String hexColor1, String hexColor2) {
|
||||
Color color1 = Color.decode(hexColor1);
|
||||
Color color2 = Color.decode(hexColor2);
|
||||
return calculateSimilarity(color1, color2);
|
||||
}
|
||||
|
||||
// 示例代码
|
||||
public static void main(String[] args) {
|
||||
// 测试颜色
|
||||
Color color1 = Color.decode("0xFF0000");
|
||||
Color color2 = Color.decode("0xFE0101");
|
||||
double similarity = calculateSimilarity(color1, color2);
|
||||
|
||||
System.out.println("颜色相似度为:" + similarity);
|
||||
|
||||
// 测试十六进制方法
|
||||
double hexSimilarity = calculateSimilarity("0xFF0000", "0xFE0101");
|
||||
System.out.println("十六进制颜色相似度为:" + hexSimilarity);
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package edu.whut.smilepicturebackend.utils;
|
||||
|
||||
/**
|
||||
* 颜色转换工具类
|
||||
*/
|
||||
public class ColorTransformUtils {
|
||||
|
||||
private ColorTransformUtils() {
|
||||
// 工具类不需要实例化
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取标准颜色(将数据万象的 5 位色值转为 6 位)
|
||||
*
|
||||
* @param color
|
||||
* @return
|
||||
*/
|
||||
public static String getStandardColor(String color) {
|
||||
// 每一种 rgb 色值都有可能只有一个 0,要转换为 00)
|
||||
// 如果是六位,不用转换,如果是五位,要给第三位后面加个 0
|
||||
// 示例:
|
||||
// 0x080e0 => 0x0800e
|
||||
if (color.length() == 7) {
|
||||
color = color.substring(0, 4) + "0" + color.substring(4, 7);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
}
|
63
src/main/java/picturesearch/AbstractSearchPicture.java
Normal file
63
src/main/java/picturesearch/AbstractSearchPicture.java
Normal file
@ -0,0 +1,63 @@
|
||||
package picturesearch;
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import picturesearch.enums.SearchSourceEnum;
|
||||
import picturesearch.model.SearchPictureResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 以图搜图
|
||||
*
|
||||
* @author Silas Yan 2025-03-23:09:50
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractSearchPicture {
|
||||
|
||||
/**
|
||||
* 执行搜索
|
||||
*
|
||||
* @param searchSource 搜索源
|
||||
* @param sourcePicture 源图片
|
||||
* @param randomSeed 随机种子
|
||||
* @param searchCount 搜索数量
|
||||
* @return 搜索结果
|
||||
*/
|
||||
public final List<SearchPictureResult> execute(String searchSource, String sourcePicture, Integer randomSeed, Integer searchCount) {
|
||||
log.info("开始搜索图片,搜索源:{},源图片:{},随机种子:{}", searchSource, sourcePicture, randomSeed);
|
||||
// 校验
|
||||
SearchSourceEnum searchSourceEnum = SearchSourceEnum.getEnumByKey(searchSource);
|
||||
if (searchSourceEnum == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "不支持的搜索源");
|
||||
}
|
||||
// 执行搜索
|
||||
String requestUrl = this.executeSearch(searchSourceEnum, sourcePicture);
|
||||
List<SearchPictureResult> pictureResultList = this.sendRequestGetResponse(requestUrl, randomSeed, searchCount);
|
||||
// 如果当前结果大于 searchCount 就截取
|
||||
if (pictureResultList.size() > searchCount) {
|
||||
pictureResultList = pictureResultList.subList(0, searchCount);
|
||||
}
|
||||
log.info("搜索图片结束,返回结果数量:{}", pictureResultList.size());
|
||||
return pictureResultList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据原图片获取搜索图片的列表地址
|
||||
*
|
||||
* @param searchSourceEnum 搜索源枚举
|
||||
* @param sourcePicture 源图片
|
||||
* @return 搜索图片的列表地址
|
||||
*/
|
||||
protected abstract String executeSearch(SearchSourceEnum searchSourceEnum, String sourcePicture);
|
||||
|
||||
/**
|
||||
* 发送请求获取响应
|
||||
*
|
||||
* @param requestUrl 请求地址
|
||||
* @param randomSeed 随机种子
|
||||
* @param searchCount 搜索数量
|
||||
* @return 响应结果
|
||||
*/
|
||||
protected abstract List<SearchPictureResult> sendRequestGetResponse(String requestUrl, Integer randomSeed, Integer searchCount);
|
||||
}
|
27
src/main/java/picturesearch/PictureSearchTest.java
Normal file
27
src/main/java/picturesearch/PictureSearchTest.java
Normal file
@ -0,0 +1,27 @@
|
||||
package picturesearch;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import picturesearch.impl.SoSearchPicture;
|
||||
import picturesearch.model.SearchPictureResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* 以图搜图测试
|
||||
*/
|
||||
public class PictureSearchTest {
|
||||
public static void main(String[] args) {
|
||||
// 360以图搜图
|
||||
// String imageUrl1 = "https://baolong-picture-1259638363.cos.ap-shanghai.myqcloud.com//public/10000000/2025-02-15_lzn23PuxZqt8CPB1.";
|
||||
String imageUrl1 = "https://fshare.bitday.top/api/public/dl/BL9SNN2V/store/820b2a3c-fa59-472e-b3ee-572c63c2ae91.png";
|
||||
AbstractSearchPicture soSearchPicture = new SoSearchPicture();
|
||||
List<SearchPictureResult> soResultList = soSearchPicture.execute("SO", imageUrl1, 1, 21);
|
||||
System.out.println("结果列表: " + JSONUtil.parse(soResultList));
|
||||
|
||||
// // 百度以图搜图
|
||||
// String imageUrl2 = "https://www.codefather.cn/logo.png";
|
||||
// AbstractSearchPicture baiduSearchPicture = new BaiduSearchPicture();
|
||||
// List<SearchPictureResult> baiduResultList = baiduSearchPicture.execute("BAIDU", imageUrl2, 1, 31);
|
||||
// System.out.println("结果列表" + JSONUtil.parse(baiduResultList));
|
||||
}
|
||||
}
|
69
src/main/java/picturesearch/enums/SearchSourceEnum.java
Normal file
69
src/main/java/picturesearch/enums/SearchSourceEnum.java
Normal file
@ -0,0 +1,69 @@
|
||||
package picturesearch.enums;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 搜索来源枚举
|
||||
*/
|
||||
@Getter
|
||||
public enum SearchSourceEnum {
|
||||
|
||||
SO("SO", "360", "https://st.so.com/r?src=st&srcsp=home&img_url=%s&submittype=imgurl"),
|
||||
BAIDU("BAIDU", "百度", "https://graph.baidu.com/upload?uptime=%s");
|
||||
|
||||
private final String key;
|
||||
private final String label;
|
||||
private final String url;
|
||||
|
||||
SearchSourceEnum(String key, String label, String url) {
|
||||
this.key = key;
|
||||
this.label = label;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 KEY 获取枚举
|
||||
*
|
||||
* @param key 状态键值
|
||||
* @return 枚举对象,未找到时返回 null
|
||||
*/
|
||||
public static SearchSourceEnum of(String key) {
|
||||
if (ObjUtil.isEmpty(key)) return null;
|
||||
return ArrayUtil.firstMatch(e -> e.getKey().equals(key), values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 KEY 获取枚举
|
||||
*
|
||||
* @param key KEY
|
||||
* @return 枚举
|
||||
*/
|
||||
public static SearchSourceEnum getEnumByKey(String key) {
|
||||
if (ObjUtil.isEmpty(key)) {
|
||||
return null;
|
||||
}
|
||||
for (SearchSourceEnum anEnum : SearchSourceEnum.values()) {
|
||||
if (anEnum.key.equals(key)) {
|
||||
return anEnum;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有有效的 KEY 列表
|
||||
*
|
||||
* @return 有效 KEY 集合(不可变列表)
|
||||
*/
|
||||
public static List<String> keys() {
|
||||
return Arrays.stream(values())
|
||||
.map(SearchSourceEnum::getKey)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
201
src/main/java/picturesearch/impl/BaiduSearchPicture.java
Normal file
201
src/main/java/picturesearch/impl/BaiduSearchPicture.java
Normal file
@ -0,0 +1,201 @@
|
||||
package picturesearch.impl;
|
||||
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.util.URLUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.http.HttpStatus;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.select.Elements;
|
||||
import org.springframework.stereotype.Component;
|
||||
import picturesearch.AbstractSearchPicture;
|
||||
import picturesearch.enums.SearchSourceEnum;
|
||||
import picturesearch.model.SearchPictureResult;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 百度以图搜图实现
|
||||
* <p>
|
||||
* 说明: 百度的以图搜图默认返回 30 条
|
||||
*
|
||||
* @author Silas Yan 2025-03-23:11:09
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class BaiduSearchPicture extends AbstractSearchPicture {
|
||||
/**
|
||||
* 根据原图片获取搜索图片的列表地址
|
||||
*
|
||||
* @param searchSourceEnum 搜索源枚举
|
||||
* @param sourcePicture 源图片
|
||||
* @return 搜索图片的列表地址
|
||||
*/
|
||||
@Override
|
||||
protected String executeSearch(SearchSourceEnum searchSourceEnum, String sourcePicture) {
|
||||
String searchUrl = String.format(searchSourceEnum.getUrl(), System.currentTimeMillis());
|
||||
log.info("[百度搜图]搜图地址:{}", searchUrl);
|
||||
try {
|
||||
String pageUrl = getPageUrl(searchUrl, sourcePicture);
|
||||
return getListUrl(pageUrl);
|
||||
} catch (Exception e) {
|
||||
log.error("[百度搜图]搜图失败", e);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "搜图失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送请求获取响应
|
||||
*
|
||||
* @param requestUrl 请求地址
|
||||
* @param randomSeed 随机种子
|
||||
* @param searchCount 搜索数量
|
||||
* @return 响应结果
|
||||
*/
|
||||
@Override
|
||||
protected List<SearchPictureResult> sendRequestGetResponse(String requestUrl, Integer randomSeed, Integer searchCount) {
|
||||
log.info("[百度搜图]搜图地址:{}, 随机种子: {}, 搜索数量: {}", requestUrl, randomSeed, searchCount);
|
||||
if (searchCount == null) searchCount = 30;
|
||||
List<SearchPictureResult> resultList = new ArrayList<>();
|
||||
int currentWhileNum = 0;
|
||||
int targetWhileNum = searchCount / 30 + 1;
|
||||
while (currentWhileNum < targetWhileNum && resultList.size() < searchCount) {
|
||||
if (randomSeed == null) randomSeed = RandomUtil.randomInt(1, 20);
|
||||
log.info("[百度搜图]当前随机种子: {}, 当前结果数量: {}", randomSeed, resultList.size());
|
||||
String URL = requestUrl + "&page=" + randomSeed;
|
||||
try (HttpResponse response = HttpUtil.createGet(URL).execute()) {
|
||||
// 判断响应状态
|
||||
if (HttpStatus.HTTP_OK != response.getStatus()) {
|
||||
log.error("[百度搜图]搜图失败,响应状态码:{}", response.getStatus());
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "搜图失败");
|
||||
}
|
||||
// 解析响应, 处理响应结果
|
||||
JSONObject body = JSONUtil.parseObj(response.body());
|
||||
if (!body.containsKey("data")) {
|
||||
log.error("[百度搜图]搜图失败,未获取到图片数据");
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "未获取到图片列表");
|
||||
}
|
||||
JSONObject data = body.getJSONObject("data");
|
||||
if (!data.containsKey("list")) {
|
||||
log.error("[百度搜图]搜图失败,未获取到图片数据");
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "未获取到图片列表");
|
||||
}
|
||||
JSONArray baiduResult = data.getJSONArray("list");
|
||||
for (Object o : baiduResult) {
|
||||
JSONObject so = (JSONObject) o;
|
||||
SearchPictureResult pictureResult = new SearchPictureResult();
|
||||
pictureResult.setImageUrl(so.getStr("thumbUrl"));
|
||||
pictureResult.setImageKey(so.getStr("contsign"));
|
||||
resultList.add(pictureResult);
|
||||
}
|
||||
currentWhileNum++;
|
||||
} catch (Exception e) {
|
||||
log.error("[百度搜图]搜图失败", e);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "搜图失败");
|
||||
} finally {
|
||||
randomSeed++;
|
||||
}
|
||||
}
|
||||
log.info("[百度搜图]最终结果数量: {}", resultList.size());
|
||||
return resultList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片页面地址
|
||||
*
|
||||
* @param searchUrl 搜索地址
|
||||
* @param sourcePicture 源图片
|
||||
* @return 图片页面地址
|
||||
*/
|
||||
public static String getPageUrl(String searchUrl, String sourcePicture) {
|
||||
Map<String, Object> formData = new HashMap<>();
|
||||
formData.put("image", sourcePicture);
|
||||
formData.put("tn", "pc");
|
||||
formData.put("from", "pc");
|
||||
formData.put("image_source", "PC_UPLOAD_URL");
|
||||
String acsToken = "jmM4zyI8OUixvSuWh0sCy4xWbsttVMZb9qcRTmn6SuNWg0vCO7N0s6Lffec+IY5yuqHujHmCctF9BVCGYGH0H5SH/H3VPFUl4O4CP1jp8GoAzuslb8kkQQ4a21Tebge8yhviopaiK66K6hNKGPlWt78xyyJxTteFdXYLvoO6raqhz2yNv50vk4/41peIwba4lc0hzoxdHxo3OBerHP2rfHwLWdpjcI9xeu2nJlGPgKB42rYYVW50+AJ3tQEBEROlg/UNLNxY+6200B/s6Ryz+n7xUptHFHi4d8Vp8q7mJ26yms+44i8tyiFluaZAr66/+wW/KMzOhqhXCNgckoGPX1SSYwueWZtllIchRdsvCZQ8tFJymKDjCf3yI/Lw1oig9OKZCAEtiLTeKE9/CY+Crp8DHa8Tpvlk2/i825E3LuTF8EQfzjcGpVnR00Lb4/8A";
|
||||
try (HttpResponse response = HttpRequest.post(searchUrl).form(formData)
|
||||
.header("Acs-Token", acsToken).timeout(5000).execute()) {
|
||||
// 判断响应状态
|
||||
if (HttpStatus.HTTP_OK != response.getStatus()) {
|
||||
log.error("[百度搜图]搜图失败,响应状态码:{}", response.getStatus());
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "搜图失败");
|
||||
}
|
||||
// 解析响应
|
||||
JSONObject body = JSONUtil.parseObj(response.body());
|
||||
if (!body.getInt("status").equals(0)) {
|
||||
log.error("[百度搜图]搜图失败,响应内容为空");
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "搜图失败");
|
||||
}
|
||||
JSONObject data = JSONUtil.parseObj(body.getStr("data"));
|
||||
String rawUrl = data.getStr("url");
|
||||
if (StrUtil.isEmpty(rawUrl)) {
|
||||
log.error("[百度搜图]搜图失败,地址为空");
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "搜图失败");
|
||||
}
|
||||
String decodeUrl = URLUtil.decode(rawUrl, StandardCharsets.UTF_8);
|
||||
if (StrUtil.isEmpty(decodeUrl)) {
|
||||
log.error("[百度搜图]搜图失败,未获取到图片页面地址");
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "搜图失败");
|
||||
}
|
||||
return decodeUrl;
|
||||
} catch (Exception e) {
|
||||
log.error("[百度搜图]搜图失败", e);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "搜索失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片列表地址
|
||||
*
|
||||
* @param resultUrl 结果页面地址
|
||||
* @return 图片列表地址
|
||||
*/
|
||||
private static String getListUrl(String resultUrl) {
|
||||
try {
|
||||
// 使用 Jsoup 获取 HTML 内容
|
||||
Document document = Jsoup.connect(resultUrl).timeout(5000).get();
|
||||
// 获取所有 <script> 标签
|
||||
Elements scriptElements = document.getElementsByTag("script");
|
||||
// 遍历找到包含 `firstUrl` 的脚本内容
|
||||
String firstUrl = null;
|
||||
for (Element script : scriptElements) {
|
||||
String scriptContent = script.html();
|
||||
if (scriptContent.contains("\"firstUrl\"")) {
|
||||
// 正则表达式提取 firstUrl 的值
|
||||
Pattern pattern = Pattern.compile("\"firstUrl\"\\s*:\\s*\"(.*?)\"");
|
||||
Matcher matcher = pattern.matcher(scriptContent);
|
||||
if (matcher.find()) {
|
||||
// 处理转义字符
|
||||
firstUrl = matcher.group(1).replace("\\/", "/");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (StrUtil.isEmpty(firstUrl)) {
|
||||
log.error("[百度搜图]搜图失败,未找到图片元素");
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "搜图失败");
|
||||
}
|
||||
return firstUrl;
|
||||
} catch (Exception e) {
|
||||
log.error("[百度搜图]搜图失败", e);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "搜图失败");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user