🤵‍♂️ 个人主页:@rain雨雨编程

😄微信公众号:rain雨雨编程

✍🏻作者简介:持续分享机器学习,爬虫,数据分析
🐋 希望大家多多支持,我们一起进步!
如果文章对你有帮助的话,
欢迎评论 💬点赞👍🏻 收藏 📂加关注+

目录

图片功能扩展

本节重点

一、图片搜索 - 基础属性搜索

需求分析

方案设计

后端开发

二、图片搜索 - 以图搜图

需求分析

方案设计

1、第三方 API

2、数据抓取

后端开发

1、数据模型开发

2、API 开发

3、图片搜索服务(门面模式)

4、接口开发

三、图片搜索 - 颜色搜索

需求分析

方案设计

1、整体流程

2、怎么获取图片主色调?

3、如何计算颜色相似度?

后端开发

1、补充颜色字段

2、存储颜色

3、颜色相似度计算

4、颜色查询服务

5、接口开发

四、图片批量管理

需求分析

方案设计

1、批处理操作优化

2、批量重命名

扩展知识 - Spring 表达式

后端开发

1、批量修改信息

2、批量重命名


本节重点

为了吸引用户使用我们平台的私有空间作为个人相册,需要提供更多功能。

本节我们重点对图片功能进行扩展,包括:

  • 图片搜索

  • 基础属性搜索

  • 以图搜图

  • 颜色搜索

  • 图片分享(略)

  • 链接分享

  • 扫码分享

  • 图片批量管理

  • 批量修改信息

  • 批量重命名

有了这些功能,用户能够更高效地管理和分享平台上的图片资源,进一步提升使用体验。

一、图片搜索 - 基础属性搜索

需求分析

我们可以提供多种搜索维度,帮用户更快地找到自己空间的图片。

将搜索维度按优先级进行排序,优先级高的会展示在靠前的位置:

  • 关键词:同时搜索名称和简介

  • 标签

  • 分类

  • 编辑时间(开始时间与结束时间)

  • 图片名称

  • 图片简介

  • 图片宽度

  • 图片高度

  • 图片格式

方案设计

后端可以直接复用原有的分页获取图片列表接口,并在此基础上增加相应的搜索条件,以支持更灵活的筛选。

前端可以针对不同类型的搜索维度选用特定的表单项组件,来提高搜索的体验。

  • 关键词:文本输入框

  • 标签:下拉选择框

  • 分类:下拉选择框

  • 编辑时间:日期选择器

  • 图片名称:文本输入框

  • 图片简介:文本输入框

  • 图片宽度:数字输入框

  • 图片高度:数字输入框

  • 图片格式:文本输入框 / 下拉选择框 参考其他网站,日期的选择最好能够提供预设的时间范围:

后端开发

其他的搜索条件基本都已经有了,还需要支持按照编辑时间搜索。

  1. 为了支持按编辑时间进行搜索,需要在请求类 PictureQueryRequest 中添加开始和结束编辑时间字段:

/**
 * 开始编辑时间
 */
private Date startEditTime;

/**
 * 结束编辑时间
 */
private Date endEditTime;
  1. 更新图片服务的 getQueryWrapper 方法

在处理查询时,补充按编辑时间筛选的逻辑:

Date startEditTime = pictureQueryRequest.getStartEditTime();
Date endEditTime = pictureQueryRequest.getEndEditTime();
queryWrapper.ge(ObjUtil.isNotEmpty(startEditTime), "editTime", startEditTime);
queryWrapper.lt(ObjUtil.isNotEmpty(endEditTime), "editTime", endEditTime);

二、图片搜索 - 以图搜图

需求分析

用户可以使用一张图片来搜索相似的图片,相比传统的关键词搜索,能够更精确地找到与上传图片内容相似的图片。

为了获得更多的搜索结果,我们的需求是从 全网搜索图片,而不是只在自己的图库中搜索。

注意,该功能不用局限于私有空间,公共图库也可以使用。

方案设计

主要有 2 种方案:第三方 API 以及数据抓取(爬虫)

1、第三方 API

如果想从自建的图库中搜索:可以使用百度 AI 提供的图片搜索 API,参考官方文档

Bing 以图搜图 API: 利用必应的图库,可以从全网进行搜索,而且可以免费使用,参考官方文档

2、数据抓取

利用已有的以图搜图网站,通过数据抓取的方式实时查询搜图网站的返回结果。

为了让大家学习到更多知识,此处我们选择这种方案。

以百度搜图网站为例,我们可以先体验一遍流程,并且对接口进行分析:

1)进到百度图片搜索,通过 url 上传图片,发现接口:https://graph.baidu.com/upload?uptime= ,该接口的返回值为 “以图搜图的页面地址”

2)访问上一步得到的 页面地址,可以在返回值中找到 firstUrl

3)访问 firstUrl,就能得到 JSON 格式的相似图片列表,里面包含了图片的缩略图和原图地址:

💡 友情提示,这种方式只适合学习使用!注意不要给目标网站带来压力!!否则后果自负!!!

后端开发

新建 api 包,由于项目可能会用到多个 api,可以将每个 api 都放在 api 目录下的一个包中。比如图片搜索 api 的相关代码,全部放在 api.imagesearch 包下。

1、数据模型开发

imagesearch.model 包中,新建一个图片搜索结果类,用于接受 API 的返回值:

@Data
public class ImageSearchResult {

    /**
     * 缩略图地址
     */
    private String thumbUrl;

    /**
     * 来源地址
     */
    private String fromUrl;
}
2、API 开发

根据方案,我们要调用多个 API,每个子 API 可以作为一个静态类来实现,统一放在 imagesearch.sub 包中,并且每个类都包含一个 main 方法,用于进行本地测试。

1)获取以图搜图的页面地址

通过向百度发送 POST 请求,获取给定图片 URL 的相似图片页面地址。

@Slf4j
public class GetImagePageUrlApi {

    /**
     * 获取图片页面地址
     *
     * @param imageUrl
     * @return
     */
    public static String getImagePageUrl(String imageUrl) {
        // 1. 准备请求参数
        Map<String, Object> formData = new HashMap<>();
        formData.put("image", imageUrl);
        formData.put("tn", "pc");
        formData.put("from", "pc");
        formData.put("image_source", "PC_UPLOAD_URL");
        // 获取当前时间戳
        long uptime = System.currentTimeMillis();
        // 请求地址
        String url = "https://graph.baidu.com/upload?uptime=" + uptime;

        try {
            // 2. 发送 POST 请求到百度接口
            HttpResponse response = HttpRequest.post(url)
                    .form(formData)
                    .timeout(5000)
                    .execute();
            // 判断响应状态
            if (HttpStatus.HTTP_OK != response.getStatus()) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR, "接口调用失败");
            }
            // 解析响应
            String responseBody = response.body();
            Map<String, Object> result = JSONUtil.toBean(responseBody, Map.class);

            // 3. 处理响应结果
            if (result == null || !Integer.valueOf(0).equals(result.get("status"))) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR, "接口调用失败");
            }
            Map<String, Object> data = (Map<String, Object>) result.get("data");
            String rawUrl = (String) data.get("url");
            // 对 URL 进行解码
            String searchResultUrl = URLUtil.decode(rawUrl, StandardCharsets.UTF_8);
            // 如果 URL 为空
            if (searchResultUrl == null) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR, "未返回有效结果");
            }
            return searchResultUrl;
        } 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 result = getImagePageUrl(imageUrl);
        System.out.println("搜索成功,结果 URL:" + result);
    }
}

2)获取图片列表页面地址

通过 jsoup 爬取 HTML 页面,提取其中包含 firstUrlJavaScript 脚本,并返回图片列表的页面地址。

@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=16250747570487381669&sign=1265ce97cd54acd88139901733452612&tpl_from=pc";
        String imageFirstUrl = getImageFirstUrl(url);
        System.out.println("搜索成功,结果 URL:" + imageFirstUrl);
    }
}

3)获取图片列表

通过调用百度接口返回的 JSON 数据,提取出其中的图片列表并返回。

@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=16250747570487381669&sign=1265ce97cd54acd88139901733452612&tk=4caaa&tpl_from=pc";
        List<ImageSearchResult> imageList = getImageList(url);
        System.out.println("搜索成功" + imageList);
    }
}
3、图片搜索服务(门面模式)

这里我们运用一种设计模式来提供图片搜索服务。门面模式通过提供一个统一的接口来简化多个接口的调用,使得客户端不需要关注内部的具体实现。

我们可以将多个 API 整合到一个门面类中,简化调用过程。在 imagesearch 包下新建门面类,整合几个接口的调用:

@Slf4j
public class ImageSearchApiFacade {

    /**
     * 搜索图片
     *
     * @param imageUrl
     * @return
     */
    public static List<ImageSearchResult> searchImage(String imageUrl) {
        String imagePageUrl = GetImagePageUrlApi.getImagePageUrl(imageUrl);
        String imageFirstUrl = GetImageFirstUrlApi.getImageFirstUrl(imagePageUrl);
        List<ImageSearchResult> imageList = GetImageListApi.getImageList(imageFirstUrl);
        return imageList;
    }

    public static void main(String[] args) {
        // 测试以图搜图功能
        String imageUrl = "https://www.codefather.cn/logo.png";
        List<ImageSearchResult> resultList = searchImage(imageUrl);
        System.out.println("结果列表" + resultList);
    }
}
4、接口开发

开发请求类:

@Data
public class SearchPictureByPictureRequest implements Serializable {

    /**
     * 图片 id
     */
    private Long pictureId;

    private static final long serialVersionUID = 1L;
}

开发接口:

/**
 * 以图搜图
 */
@PostMapping("/search/picture")
public BaseResponse<List<ImageSearchResult>> searchPictureByPicture(@RequestBody SearchPictureByPictureRequest searchPictureByPictureRequest) {
    ThrowUtils.throwIf(searchPictureByPictureRequest == null, ErrorCode.PARAMS_ERROR);
    Long pictureId = searchPictureByPictureRequest.getPictureId();
    ThrowUtils.throwIf(pictureId == null || pictureId <= 0, ErrorCode.PARAMS_ERROR);
    Picture oldPicture = pictureService.getById(pictureId);
    ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR);
    List<ImageSearchResult> resultList = ImageSearchApiFacade.searchImage(oldPicture.getUrl());
    return ResultUtils.success(resultList);
}

三、图片搜索 - 颜色搜索

需求分析

能够按照颜色搜索空间内 主色调 最相似的图片,在设计、创意和电商领域有广泛应用。

参考其他网站的颜色搜图效果:

此处我们将该功能限定在空间内使用,主要是考虑到公共图库的图片数量可能非常庞大,直接进行颜色匹配会导致搜索速度较慢,影响用户体验。

方案设计

需要思考几个问题:

  1. 整体业务流程

  2. 怎么获取图片主色调?

  3. 怎么设计搜索算法?

1、整体流程

为了提升性能,避免每次搜索都实时计算图片主色调,建议在图片上传成功后立即提取主色调并存储到数据库的独立字段中。

完整流程如下:

  1. 提取图片颜色:通过图像处理技术(云服务 API 或者 OpenCV 图像处理库)提取图片的颜色特征,可以采用主色调、颜色直方图等方法表示图片的颜色特征。此处我们采用主色调,便于理解。

  2. 存储颜色特征:将提取的颜色数据存储到数据库中,以便后续快速检索。

  3. 用户查询输入:用户通过颜色选择器、RGB 值输入、或预定义颜色名称指定颜色查询条件。

  4. 计算相似度:根据用户指定的颜色,与数据库中的颜色特征进行相似度计算(如欧氏距离、余弦相似度等方法)。

  5. 返回结果:由于空间内的图片数量相对较少,可以按照图片与目标颜色的相似度进行排序,优先返回最符合用户要求的图片,而不是仅返回完全符合指定色调的图片。

2、怎么获取图片主色调?

我们存储图片使用的 COS 对象存储服务已经帮我们整合了数据万象,自带获取图片主色调的功能,参考文档 💡 在使用云服务功能前,我们可以详细了解下服务的相关限制,比如 数据万象的限制,一般情况下达不到限制。

除了方便之外,这个功能属于基础图片处理,官方提供的免费额度较高,适合学习测试:

💡 一般我们做项目时,尽可能减少新依赖或服务的引入,会让成本更可控。比如看到腾讯云 COS 有现成的支持和免费额度,就已经是我们的首选解决方案,无需考虑第三方 API,可能会带来的额外限制和兼容性问题(比如我们的图片开启防盗链,可能就解析不到)。

3、如何计算颜色相似度?

数据库不支持直接按照颜色检索,用 like 检索又不符合颜色的特性。所以可以使用一些算法来解决。

此处使用 欧几里得距离 算法:颜色可以用 RGB 值表示,可以通过计算两种颜色 RGB 值之间的欧几里得距离来判断它们的相似度。

公式:

解释:

  • R1, G1, B1:第一个颜色的 RGB 分量(红色、绿色、蓝色)。

  • R2, G2, B2:第二个颜色的 RGB 分量。

  • d:两个颜色之间的欧几里得距离。 距离越小,表示颜色越相似;距离越大,表示颜色越不同。

还有一些其他的方法,需要用到时自己在网上调研即可:

  • 余弦相似度 (Cosine Similarity),

  • 曼哈顿距离 (Manhattan Distance)

  • Jaccard 相似度 (Jaccard Similarity)

  • 平均颜色差异 (Mean Color Difference)

  • 哈希算法 (Color Hashing)

  • 色调、饱和度和亮度 (HSL) 差异

后端开发

1、补充颜色字段

1)图片表新增字段,执行 SQL

ALTER TABLE picture
    ADD COLUMN picColor varchar(16) null comment '图片主色调';

2)每次新增字段时,都要修改 PictureMapper.xml 以支持新字段的查询。

Picture 实体类、PictureVO 包装类、UploadPictureResult 上传图片结果类也需要补充新字段:

/**
 * 图片主色调
 */
private String picColor;
2、存储颜色

1)修改 PictureUploadTemplatebuildResult 方法,直接从 ImageInfo 对象中就能获得主色调:

uploadPictureResult.setPicColor(imageInfo.getAve());

注意两个 buildResult 方法都要修改,其中一个 buildResult 方法要补充 imageInfo 参数,修改的代码如下:

private UploadPictureResult buildResult(String originFilename, CIObject compressedCiObject, CIObject thumbnailCiObject, ImageInfo imageInfo) {
    UploadPictureResult uploadPictureResult = new UploadPictureResult();
    int picWidth = compressedCiObject.getWidth();
    int picHeight = compressedCiObject.getHeight();
    double picScale = NumberUtil.round(picWidth * 1.0 / picHeight, 2).doubleValue();
    uploadPictureResult.setPicName(FileUtil.mainName(originFilename));
    uploadPictureResult.setPicWidth(picWidth);
    uploadPictureResult.setPicHeight(picHeight);
    uploadPictureResult.setPicScale(picScale);
    uploadPictureResult.setPicFormat(compressedCiObject.getFormat());
    uploadPictureResult.setPicColor(imageInfo.getAve());
    uploadPictureResult.setPicSize(compressedCiObject.getSize().longValue());
    // 设置图片为压缩后的地址
    uploadPictureResult.setUrl(cosClientConfig.getHost() + "/" + compressedCiObject.getKey());
    // 设置缩略图
    uploadPictureResult.setThumbnailUrl(cosClientConfig.getHost() + "/" + thumbnailCiObject.getKey());
    return uploadPictureResult;
}

获取到的值格式为十六进制,如图:

2)图片服务的 uploadPicture 中补充设置 picColor,从而将该字段保存到数据库中:

picture.setPicColor(uploadPictureResult.getPicColor());
3、颜色相似度计算

新建 utils 包,直接利用 AI 来编写工具类:

/**
 * 工具类:计算颜色相似度
 */
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);
    }
}
4、颜色查询服务

为了让大家学习更清晰,在图片服务中新编写按颜色查询图片的方法 searchPictureByColor,不和其他的搜索条件放在一起。

按照方案设计中的流程开发,代码如下:

@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 (!loginUser.getId().equals(space.getUserId())) {
        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 Collections.emptyList();
    }
    // 将目标颜色转为 Color 对象
    Color targetColor = Color.decode(picColor);
    // 4. 计算相似度并排序
    List<Picture> sortedPictures = 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);
            }))
            // 取前 12 个
            .limit(12)
            .collect(Collectors.toList());

    // 转换为 PictureVO
    return sortedPictures.stream()
            .map(PictureVO::objToVo)
            .collect(Collectors.toList());
}

上述代码有 2 个小细节:

  1. 我们提前把目标颜色从字符串转为 color 对象,而不是每计算一张图都重新转换一次对象。

  2. 最后将 Picture 转为 PictureVO 时,不要调用 service 中的转换方法,会额外查询用户信息,这是没必要的。

5、接口开发

1)请求封装类 SearchPictureByColorRequest,需要传入空间 id 和主色调:

@Data
public class SearchPictureByColorRequest implements Serializable {

    /**
     * 图片主色调
     */
    private String picColor;

    /**
     * 空间 id
     */
    private Long spaceId;

    private static final long serialVersionUID = 1L;
}

2)开发接口:

@PostMapping("/search/color")
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> result = pictureService.searchPictureByColor(spaceId, picColor, loginUser);
    return ResultUtils.success(result);
}

四、图片批量管理

需求分析

用户可以对私有空间内的图片进行批量修改,包括:

  • 批量修改信息:修改图片的标签和分类

  • 批量重命名:批量修改图片名称

方案设计

1、批处理操作优化

批量操作的实现并不难,首先查询出空间内所有的图片,然后最简单的方式就是 for 循环遍历一下嘛!

但如果想让批量操作更快、更稳定地完成,我们需要注意几点:

  1. 数据校验:校验参数的合法性,并且校验用户是否具有空间的访问权限,确保操作安全。

  2. 查询优化:查询图片时,仅选择所需的字段(如 idspaceId),减少数据库开销。

  3. 事务:确保批量操作具有原子性,如果有一条更新失败,那么需要对这一批操作进行回滚,避免数据不一致

  4. 批量更新:利用 MyBatis-Plus 提供的updateBatchById方法进行批量更新,而不是 for 循环多次操作数据库,从而提高性能并降低操作时间。

此外,如果要处理的数据量非常大(上千条),为了进一步优化性能,还可以结合使用线程池、分批处理和并发编程,提升大规模操作的效率。还可以通过添加日志来记录批处理操作的执行情况,提高可观测性。

2、批量重命名

最简单的实现是将所有图片都修改为同一个名称,但这样不够有区分度。所以我们可以定义一个动态生成规则,允许用户在重命名时填写动态变量(占位符)。比如用户输入图片_{序号},其中{序号}就是动态变量,每个图片的序号都不同,会从 1 开始持续递增。

后端可以使用字符串替换方法来处理 {序号} 占位符,适用于比较简单的场景,如果动态生成规则很复杂,可以使用模板引擎技术。

扩展知识 - Spring 表达式

提到动态替换内容,这里顺便分享一下 Spring 表达式技术。

Spring 表达式语言(Spring Expression Language,简称 SpEL)用于在 Spring 配置文件或 Java 代码中动态地查询和操作对象。SpEL 可以在运行时解析表达式,并执行对 Java 对象的访问、操作和计算,支持丰富的功能,如条件判断、方法调用、属性访问、集合处理、正则表达式等。

举一些语法示例:

#{user.name}   // 访问 user 对象的 name 属性
#{person.address.city}  // 访问嵌套对象地址中的 city 属性
#{user.getFullName()}   // 调用 user 对象的 getFullName() 方法
#{user.age > 18 ? 'Adult' : 'Child'}  // 根据 age 判断是否为成年人

举例一个应用场景,比如缓存注解中,使用表达式根据方法参数动态生成缓存的 key:

/**
 * 根据用户 ID 获取用户信息,并将结果缓存。
 * 使用 SpEL 动态生成缓存的 key,加入用户 ID 和请求的语言(locale)。
 *
 * @param userId 用户 ID
 * @param locale 当前语言环境(如 en, zh)
 * @return 用户信息
 */
@Cacheable(value = "users", key = "#userId + ':' + #locale")
public String getUserInfo(Long userId, String locale) {
    // 模拟数据库查询
    System.out.println("Fetching user info from DB...");
    return "User " + userId + " info in " + locale + " language";
}

它的实现方式可就不是字符串替换这么简单了,而是用到了 AST 抽象语法树来对字符串进行解析,大家要对这种思路有个印象。

后端开发

1、批量修改信息

1)开发请求类,接受图片id列表等字段:

@Data
public class PictureEditByBatchRequest implements Serializable {

    /**
     * 图片 id 列表
     */
    private List<Long> pictureIdList;

    /**
     * 空间 id
     */
    private Long spaceId;

    /**
     * 分类
     */
    private String category;

    /**
     * 标签
     */
    private List<String> tags;

    private static final long serialVersionUID = 1L;
}

2)开发批量修改图片服务,依次完成参数校验、空间权限校验、图片查询、批量更新操作:

@Override
@Transactional(rollbackFor = Exception.class)
public void editPictureByBatch(PictureEditByBatchRequest pictureEditByBatchRequest, User loginUser) {
    List<Long> pictureIdList = pictureEditByBatchRequest.getPictureIdList();
    Long spaceId = pictureEditByBatchRequest.getSpaceId();
    String category = pictureEditByBatchRequest.getCategory();
    List<String> tags = pictureEditByBatchRequest.getTags();

    // 1. 校验参数
    ThrowUtils.throwIf(spaceId == null || CollUtil.isEmpty(pictureIdList), 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 (!loginUser.getId().equals(space.getUserId())) {
        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));
        }
    });

    // 5. 批量更新
    boolean result = this.updateBatchById(pictureList);
    ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
}

💡 对于我们的项目来说,由于用户要处理的数据量不大,上述代码已经能够满足需求。但如果要处理大量数据,可以使用线程池 + 分批 + 并发进行优化,参考代码如下:

@Resource
private ThreadPoolExecutor customExecutor;

/**
 * 批量编辑图片分类和标签
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void batchEditPictureMetadata(PictureBatchEditRequest request, Long spaceId, Long loginUserId) {
    // 参数校验
    validateBatchEditRequest(request, spaceId, loginUserId);

    // 查询空间下的图片
    List<Picture> pictureList = this.lambdaQuery()
            .eq(Picture::getSpaceId, spaceId)
            .in(Picture::getId, request.getPictureIds())
            .list();

    if (pictureList.isEmpty()) {
        throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "指定的图片不存在或不属于该空间");
    }

    // 分批处理避免长事务
    int batchSize = 100;
    List<CompletableFuture<Void>> futures = new ArrayList<>();
    for (int i = 0; i < pictureList.size(); i += batchSize) {
        List<Picture> batch = pictureList.subList(i, Math.min(i + batchSize, pictureList.size()));

        // 异步处理每批数据
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            batch.forEach(picture -> {
                // 编辑分类和标签
                if (request.getCategory() != null) {
                    picture.setCategory(request.getCategory());
                }
                if (request.getTags() != null) {
                    picture.setTags(String.join(",", request.getTags()));
                }
            });
            boolean result = this.updateBatchById(batch);
            if (!result) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR, "批量更新图片失败");
            }
        }, customExecutor);

        futures.add(future);
    }

    // 等待所有任务完成
    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
}

此外,还可以多记录日志,或者让返回结果更加详细,比如更新成功了多少条数据之类的。

3)开发接口

@PostMapping("/edit/batch")
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);
}
2、批量重命名

直接复用批量修改信息的方法,在这基础上做增强,补充对图片名称的修改。

1)批量编辑请求类 PictureEditByBatchRequest 补充字段:

/**
 * 命名规则
 */
private String nameRule;

2)批量修改方法补充图片名称:

// 批量重命名
String nameRule = pictureEditByBatchRequest.getNameRule();
fillPictureWithNameRule(pictureList, nameRule);

编写填充图片名称的方法,使用字符串的 replaceAll 方法替换动态变量:

/**
 * nameRule 格式:图片{序号}
 *
 * @param pictureList
 * @param nameRule
 */
private void fillPictureWithNameRule(List<Picture> pictureList, String nameRule) {
    if (CollUtil.isEmpty(pictureList) || StrUtil.isBlank(nameRule)) {
        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, "名称解析错误");
    }
}

文章持续跟新,可以微信搜一搜公众号  rain雨雨编程 ],第一时间阅读,涉及数据分析,机器学习,Java编程,爬虫,实战项目等。

Logo

脑启社区是一个专注类脑智能领域的开发者社区。欢迎加入社区,共建类脑智能生态。社区为开发者提供了丰富的开源类脑工具软件、类脑算法模型及数据集、类脑知识库、类脑技术培训课程以及类脑应用案例等资源。

更多推荐