373 lines
17 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package edu.whut.smilepicturebackend.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
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.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.manager.FileManager;
import edu.whut.smilepicturebackend.manager.upload.FilePictureUpload;
import edu.whut.smilepicturebackend.manager.upload.PictureUploadTemplate;
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.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.UserService;
import lombok.RequiredArgsConstructor;
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.beans.BeanUtils;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author 张三
* @description 针对表【picture(图片)】的数据库操作Service实现
* @createDate 2025-06-11 11:23:11
*/
@Slf4j
@Service
@RequiredArgsConstructor
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;
@Override
public void validPicture(Picture picture) {
ThrowUtils.throwIf(picture == null, ErrorCode.PARAMS_ERROR);
// 从对象中取值
Long id = picture.getId();
String url = picture.getUrl();
String introduction = picture.getIntroduction();
// 修改数据时id 不能为空,有参数则校验
ThrowUtils.throwIf(ObjUtil.isNull(id), ErrorCode.PARAMS_ERROR, "id 不能为空");
// 如果传递了 url才校验
if (StrUtil.isNotBlank(url)) {
ThrowUtils.throwIf(url.length() > 1024, ErrorCode.PARAMS_ERROR, "url 过长");
}
if (StrUtil.isNotBlank(introduction)) {
ThrowUtils.throwIf(introduction.length() > 800, ErrorCode.PARAMS_ERROR, "简介过长");
}
}
@Override
public PictureVO uploadPicture(Object inputSource, PictureUploadRequest pictureUploadRequest, User loginUser) {
// 校验参数
ThrowUtils.throwIf(loginUser == null, ErrorCode.NO_AUTH_ERROR);
// 判断是新增还是删除
Long pictureId = null;
if (pictureUploadRequest != null) {
pictureId = pictureUploadRequest.getId();
}
// 如果是更新,判断图片是否存在
if (pictureId != null) {
Picture 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);
}
}
// 上传图片,得到图片信息
String uploadPathPrefix;
//公共图库下每个用户有自己的userid管理的文件夹。
uploadPathPrefix = String.format("public/%s", loginUser.getId());
// 根据 inputSource 的类型区分上传方式
PictureUploadTemplate pictureUploadTemplate = filePictureUpload;
if (inputSource instanceof String) {
pictureUploadTemplate = urlPictureUpload;
}
UploadPictureResult uploadPictureResult = pictureUploadTemplate.uploadPicture(inputSource, uploadPathPrefix);
// 构造要入库的图片信息
Picture picture = new Picture();
// 复制同名属性url、name、picSize、picWidth、picHeight、picScale、picFormat
BeanUtils.copyProperties(uploadPictureResult, picture);
// 支持外层pictureUploadRequest传递图片名称
picture.setName(
StrUtil.blankToDefault(
pictureUploadRequest == null ? null : pictureUploadRequest.getPicName(),
uploadPictureResult.getName()
)
);
picture.setUserId(loginUser.getId());
// 补充审核参数
this.fillReviewParams(picture, loginUser);
// 操作数据库
// 如果 pictureId 不为空,表示更新,否则是新增
if (pictureId != null) {
// 如果是更新,需要补充 id 和编辑时间
picture.setId(pictureId);
picture.setEditTime(new Date());
}
boolean result = this.saveOrUpdate(picture);
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR, "图片上传失败,数据库操作失败");
return PictureVO.objToVo(picture);
}
@Override
public LambdaQueryWrapper<Picture> getQueryWrapper(PictureQueryRequest req) {
LambdaQueryWrapper<Picture> qw = Wrappers.lambdaQuery(Picture.class);
if (req == null) {
return qw;
}
// 精简版条件构造
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())
.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());
// 全字段模糊搜索
if (StrUtil.isNotBlank(req.getSearchText())) {
qw.and(w -> w
.like(Picture::getName, req.getSearchText())
.or()
.like(Picture::getIntroduction, req.getSearchText())
);
}
// JSON 数组 tags 查询
if (CollUtil.isNotEmpty(req.getTags())) {
req.getTags().forEach(tag ->
qw.like(Picture::getTags, "\"" + tag + "\"")
);
}
// 动态排序:转下划线字段名并手工拼接
if (StrUtil.isNotBlank(req.getSortField())) {
String column = StrUtil.toUnderlineCase(req.getSortField());
String direction = "ascend".equalsIgnoreCase(req.getSortOrder()) ? "ASC" : "DESC";
qw.last("ORDER BY " + column + " " + direction);
}
return qw;
}
@Override
public PictureVO getPictureVO(Picture picture, HttpServletRequest request) {
// 对象转封装类
PictureVO pictureVO = PictureVO.objToVo(picture);
// 关联查询用户信息
Long userId = picture.getUserId();
if (userId != null && userId > 0) {
User user = userService.getById(userId);
UserVO userVO = userService.getUserVO(user);
pictureVO.setUser(userVO);
}
return pictureVO;
}
/**
* 分页获取图片封装
*/
@Override
public Page<PictureVO> getPictureVOPage(Page<Picture> picturePage, HttpServletRequest request) {
//从 Page<Picture> 中拿到原始记录和分页元数据
List<Picture> pictureList = picturePage.getRecords();
Page<PictureVO> pictureVOPage = new Page<>(picturePage.getCurrent(), picturePage.getSize(), picturePage.getTotal());
if (CollUtil.isEmpty(pictureList)) {
return pictureVOPage;
}
// 实体到 VO 的基本映射
List<PictureVO> pictureVOList = pictureList.stream()
.map(PictureVO::objToVo)
.collect(Collectors.toList());
//批量拉取关联的用户信息
//先从所有 Picture 里摘出不重复的 userId。
//调 userService.listByIds(...) 一次性把这些用户都查出来,避免 N+1 查询。
//再把结果按 userId 分组,方便快速根据 ID 拿到对应的 User 对象。
Set<Long> userIdSet = pictureList.stream().map(Picture::getUserId).collect(Collectors.toSet());
// 1 => user1, 2 => user2
Map<Long, List<User>> userIdUserListMap = userService.listByIds(userIdSet).stream()
.collect(Collectors.groupingBy(User::getId));
// 把用户信息填充到 VO
pictureVOList.forEach(pictureVO -> {
Long userId = pictureVO.getUserId();
User user = null;
if (userIdUserListMap.containsKey(userId)) {
user = userIdUserListMap.get(userId).get(0);
}
pictureVO.setUser(userService.getUserVO(user));
});
pictureVOPage.setRecords(pictureVOList);
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. 校验参数
ThrowUtils.throwIf(pictureReviewRequest == null, ErrorCode.PARAMS_ERROR);
Long id = pictureReviewRequest.getId();
Integer reviewStatus = pictureReviewRequest.getReviewStatus();
PictureReviewStatusEnum reviewStatusEnum = PictureReviewStatusEnum.getEnumByValue(reviewStatus);
String reviewMessage = pictureReviewRequest.getReviewMessage();
if (id == null || reviewStatusEnum == null || PictureReviewStatusEnum.REVIEWING.equals(reviewStatusEnum)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 2. 判断图片是否存在
Picture oldPicture = this.getById(id);
ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR);
// 3. 校验审核状态是否重复,已是改状态
if (oldPicture.getReviewStatus().equals(reviewStatus)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请勿重复审核");
}
// 4. 数据库操作
Picture updatePicture = new Picture();
BeanUtil.copyProperties(pictureReviewRequest, updatePicture);
updatePicture.setReviewerId(loginUser.getId()); //审核人
updatePicture.setReviewTime(new Date());
boolean result = this.updateById(updatePicture);
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
}
/**
* 填充审核参数
*
* @param picture
* @param loginUser
*/
@Override
public void fillReviewParams(Picture picture, User loginUser) {
if (userService.isAdmin(loginUser)) {
// 管理员自动过审
picture.setReviewStatus(PictureReviewStatusEnum.PASS.getValue());
picture.setReviewerId(loginUser.getId());
picture.setReviewMessage("管理员自动过审");
picture.setReviewTime(new Date());
} else {
// 非管理员,无论是编辑还是创建默认都是待审核
picture.setReviewStatus(PictureReviewStatusEnum.REVIEWING.getValue());
}
}
//爬取网落图片可以用ai分析标签
@Override
public Integer uploadPictureByBatch(PictureUploadByBatchRequest pictureUploadByBatchRequest, User loginUser) {
// 校验参数
String searchText = pictureUploadByBatchRequest.getSearchText();
Integer count = pictureUploadByBatchRequest.getCount();
ThrowUtils.throwIf(count > 30, ErrorCode.PARAMS_ERROR, "最多 30 条");
// 名称前缀默认等于搜索关键词
String namePrefix = pictureUploadByBatchRequest.getNamePrefix();
if (StrUtil.isBlank(namePrefix)) {
namePrefix = searchText;
}
// 抓取内容
String fetchUrl = String.format("https://cn.bing.com/images/async?q=%s&mmasync=1", searchText);
//最全的html文档
Document document;
try {
document = Jsoup.connect(fetchUrl).get();
} catch (IOException e) {
log.error("获取页面失败", e);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "获取页面失败");
}
// 解析内容
Element div = document.getElementsByClass("dgControl").first();
if (ObjUtil.isEmpty(div)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "获取元素失败");
}
Elements imgElementList = div.select("img.mimg");
// 遍历元素,依次处理上传图片
int uploadCount = 0;
for (Element imgElement : imgElementList) {
String fileUrl = imgElement.attr("src");
if (StrUtil.isBlank(fileUrl)) {
//并不是所有图片链接都是正确的
log.info("当前链接为空,已跳过:{}", fileUrl);
continue;
}
// 处理图片的地址,防止转义或者和对象存储冲突的问题
// codefather.cn?yupi=dog应该只保留 codefather.cn
int questionMarkIndex = fileUrl.indexOf("?");
if (questionMarkIndex > -1) {
fileUrl = fileUrl.substring(0, questionMarkIndex);
}
// 上传图片
PictureUploadRequest pictureUploadRequest = new PictureUploadRequest();
pictureUploadRequest.setFileUrl(fileUrl);
pictureUploadRequest.setPicName(namePrefix + (uploadCount + 1));
try {
PictureVO pictureVO = this.uploadPicture(fileUrl, pictureUploadRequest, loginUser);
log.info("图片上传成功id = {}", pictureVO.getId());
uploadCount++;
} catch (Exception e) {
log.error("图片上传失败", e);
continue;
}
if (uploadCount >= count) {
break;
}
}
return uploadCount;
}
}