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 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 getQueryWrapper(PictureQueryRequest req) { LambdaQueryWrapper 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 getPictureVOPage(Page picturePage, HttpServletRequest request) { //从 Page 中拿到原始记录和分页元数据 List pictureList = picturePage.getRecords(); Page pictureVOPage = new Page<>(picturePage.getCurrent(), picturePage.getSize(), picturePage.getTotal()); if (CollUtil.isEmpty(pictureList)) { return pictureVOPage; } // 实体到 VO 的基本映射 List pictureVOList = pictureList.stream() .map(PictureVO::objToVo) .collect(Collectors.toList()); //批量拉取关联的用户信息 //先从所有 Picture 里摘出不重复的 userId。 //调 userService.listByIds(...) 一次性把这些用户都查出来,避免 N+1 查询。 //再把结果按 userId 分组,方便快速根据 ID 拿到对应的 User 对象。 Set userIdSet = pictureList.stream().map(Picture::getUserId).collect(Collectors.toSet()); // 1 => user1, 2 => user2 Map> 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; } }