Compare commits

...

10 Commits

108 changed files with 6363 additions and 104 deletions

34
pom.xml
View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,20 @@
package edu.whut.smilepicturebackend.api.imagesearch.model;
import lombok.Data;
/**
* 图片搜索结果
*/
@Data
public class ImageSearchResult {
/**
* 缩略图地址
*/
private String thumbUrl;
/**
* 来源地址
*/
private String fromUrl;
}

View File

@ -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;
/**
* 获取图片列表接口的 ApiStep 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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
// 有生成缩略图才获取缩略图

View File

@ -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 请求未返回 200status = {},将从 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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> {

View File

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

View File

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

View File

@ -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> {

View File

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

View File

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

View File

@ -27,5 +27,10 @@ public class PictureUploadRequest implements Serializable {
*/
private String picName;
/**
* 空间 id
*/
private Long spaceId;
private static final long serialVersionUID = 1L;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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-拒绝
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -78,12 +78,21 @@ public class PictureVO implements Serializable {
*/
private String picFormat;
/**
* 图片主色调
*/
private String picColor;
/**
* 用户 id
*/
private Long userId;
/**
* 空间 id
*/
private Long spaceId;
/**
* 创建时间

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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> {
/**

View File

@ -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();
// 复制同名属性urlnamepicSizepicWidthpicHeightpicScalepicFormat
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, "名称解析错误");
}
}
}

View File

@ -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");
// 1001201000
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, "未指定查询范围");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View 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