优化水印
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package com.aida.seller.module.listing.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 商品图片水印实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("seller_listing_watermark_image")
|
||||
public class ListingWatermarkImageEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 主键ID */
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
/** 商品ID */
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long listingId;
|
||||
|
||||
/** 图片类别: cover/main_product/product/sketch/apparel */
|
||||
private String category;
|
||||
|
||||
/** 原图 logical path */
|
||||
private String originalUrl;
|
||||
|
||||
/** 加水印图的 logical path */
|
||||
private String watermarkedUrl;
|
||||
|
||||
/** 创建时间 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/** 是否删除:0-否,1-是 */
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.aida.seller.module.listing.mapper;
|
||||
|
||||
import com.aida.seller.module.listing.entity.ListingWatermarkImageEntity;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商品图片水印 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface ListingWatermarkImageMapper extends BaseMapper<ListingWatermarkImageEntity> {
|
||||
|
||||
int deleteByListingId(@Param("listingId") Long listingId);
|
||||
|
||||
List<ListingWatermarkImageEntity> selectByListingId(@Param("listingId") Long listingId);
|
||||
}
|
||||
@@ -6,12 +6,15 @@ import com.aida.seller.common.exception.BusinessException;
|
||||
import com.aida.seller.module.listing.dto.*;
|
||||
import com.aida.seller.module.listing.entity.ListingEntity;
|
||||
import com.aida.seller.module.listing.entity.ListingImageEntity;
|
||||
import com.aida.seller.module.listing.entity.ListingWatermarkImageEntity;
|
||||
import com.aida.seller.module.listing.enums.ImageCategoryEnum;
|
||||
import com.aida.seller.module.listing.enums.ListingStatusEnum;
|
||||
import com.aida.seller.module.listing.enums.DesignForEnum;
|
||||
import com.aida.seller.module.listing.enums.ProductCategoryEnum;
|
||||
import com.aida.seller.module.listing.mapper.ListingImageMapper;
|
||||
import com.aida.seller.module.listing.mapper.ListingMapper;
|
||||
import com.aida.seller.module.listing.mapper.ListingWatermarkImageMapper;
|
||||
import com.aida.seller.util.ImageWatermarkUtil;
|
||||
import com.aida.seller.module.listing.vo.ListingPageVO;
|
||||
import com.aida.seller.module.order.mapper.OrderItemMapper;
|
||||
import com.aida.seller.module.order.entity.OrderItemEntity;
|
||||
@@ -44,9 +47,11 @@ import java.util.stream.Collectors;
|
||||
public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity> implements ListingService {
|
||||
|
||||
private final ListingImageMapper listingImageMapper;
|
||||
private final ListingWatermarkImageMapper listingWatermarkImageMapper;
|
||||
private final OrderItemMapper orderItemMapper;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final MinioUtil minioUtil;
|
||||
private final ImageWatermarkUtil imageWatermarkUtil;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -68,6 +73,7 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
|
||||
}
|
||||
|
||||
Long listingId;
|
||||
Map<String, String> oldWatermarks = Map.of();
|
||||
if (dto.getId() == null) {
|
||||
entity.setStatus(dto.getStatus());
|
||||
this.save(entity);
|
||||
@@ -86,6 +92,15 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
|
||||
listingImageMapper.delete(new LambdaQueryWrapper<ListingImageEntity>()
|
||||
.eq(ListingImageEntity::getListingId, dto.getId()));
|
||||
listingId = dto.getId();
|
||||
|
||||
if (Objects.equals(dto.getStatus(), 1)) {
|
||||
List<ListingWatermarkImageEntity> oldWmList = listingWatermarkImageMapper.selectByListingId(listingId);
|
||||
oldWatermarks = oldWmList.stream()
|
||||
.collect(Collectors.toMap(ListingWatermarkImageEntity::getOriginalUrl,
|
||||
ListingWatermarkImageEntity::getWatermarkedUrl,
|
||||
(a, b) -> a));
|
||||
listingWatermarkImageMapper.deleteByListingId(listingId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!CollectionUtils.isEmpty(dto.getImages())) {
|
||||
@@ -98,6 +113,10 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
|
||||
update.setCover(minioUtil.convertToLogicalPath(cover));
|
||||
this.updateById(update);
|
||||
}
|
||||
|
||||
if (Objects.equals(dto.getStatus(), 1)) {
|
||||
generateWatermarks(listingId, oldWatermarks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,6 +278,45 @@ public class ListingServiceImpl extends ServiceImpl<ListingMapper, ListingEntity
|
||||
return null;
|
||||
}
|
||||
|
||||
private void generateWatermarks(Long listingId, Map<String, String> oldWatermarks) {
|
||||
Set<String> watermarkCategories = Set.of(
|
||||
ImageCategoryEnum.COVER.getCode(),
|
||||
ImageCategoryEnum.MAIN_PRODUCT.getCode(),
|
||||
ImageCategoryEnum.PRODUCT.getCode(),
|
||||
ImageCategoryEnum.SKETCH.getCode(),
|
||||
ImageCategoryEnum.APPAREL.getCode());
|
||||
|
||||
List<ListingImageEntity> images = listingImageMapper.selectList(
|
||||
new LambdaQueryWrapper<ListingImageEntity>()
|
||||
.eq(ListingImageEntity::getListingId, listingId)
|
||||
.in(ListingImageEntity::getCategory, watermarkCategories));
|
||||
|
||||
for (ListingImageEntity img : images) {
|
||||
String originalUrl = img.getImageUrl();
|
||||
String watermarkedUrl;
|
||||
|
||||
String existingWm = oldWatermarks.get(originalUrl);
|
||||
if (existingWm != null) {
|
||||
watermarkedUrl = existingWm;
|
||||
} else {
|
||||
watermarkedUrl = imageWatermarkUtil.applyWatermark(originalUrl);
|
||||
watermarkedUrl = minioUtil.convertToLogicalPath(watermarkedUrl);
|
||||
}
|
||||
|
||||
ListingWatermarkImageEntity watermark = new ListingWatermarkImageEntity();
|
||||
watermark.setListingId(listingId);
|
||||
watermark.setCategory(img.getCategory());
|
||||
watermark.setOriginalUrl(originalUrl);
|
||||
watermark.setWatermarkedUrl(watermarkedUrl);
|
||||
|
||||
try {
|
||||
listingWatermarkImageMapper.insert(watermark);
|
||||
} catch (Exception e) {
|
||||
// 唯一索引冲突,忽略
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setPopupReminder(Long sellerId) {
|
||||
|
||||
@@ -8,17 +8,18 @@ import com.aida.seller.module.designer.mapper.DesignerMapper;
|
||||
import com.aida.seller.module.listing.dto.ListingMallQueryDTO;
|
||||
import com.aida.seller.module.listing.entity.ListingEntity;
|
||||
import com.aida.seller.module.listing.entity.ListingImageEntity;
|
||||
import com.aida.seller.module.listing.entity.ListingWatermarkImageEntity;
|
||||
import com.aida.seller.module.listing.enums.DesignForEnum;
|
||||
import com.aida.seller.module.listing.enums.ImageCategoryEnum;
|
||||
import com.aida.seller.module.listing.mapper.ListingImageMapper;
|
||||
import com.aida.seller.module.listing.mapper.ListingMallMapper;
|
||||
import com.aida.seller.module.listing.mapper.ListingWatermarkImageMapper;
|
||||
import com.aida.seller.module.listing.vo.ListingDetailVO;
|
||||
import com.aida.seller.module.listing.vo.ListingMallVO;
|
||||
import com.aida.seller.module.order.entity.OrderItemEntity;
|
||||
import com.aida.seller.module.order.entity.OrderItemImageEntity;
|
||||
import com.aida.seller.module.order.mapper.OrderItemImageMapper;
|
||||
import com.aida.seller.module.order.mapper.OrderItemMapper;
|
||||
import com.aida.seller.util.ImageWatermarkUtil;
|
||||
import com.aida.seller.util.MinioUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
@@ -44,17 +45,17 @@ import com.aida.seller.module.listing.vo.ListingMallVO;
|
||||
public class ListingMallServiceImpl extends ServiceImpl<ListingMallMapper, ListingEntity> implements com.aida.seller.module.listing.service.ListingMallService {
|
||||
|
||||
private final MinioUtil minioUtil;
|
||||
private final ImageWatermarkUtil imageWatermarkUtil;
|
||||
private final ListingImageMapper listingImageMapper;
|
||||
private final ListingWatermarkImageMapper listingWatermarkImageMapper;
|
||||
private final DesignerMapper designerMapper;
|
||||
private final OrderItemImageMapper orderItemImageMapper;
|
||||
private final OrderItemMapper orderItemMapper;
|
||||
|
||||
public ListingMallServiceImpl(MinioUtil minioUtil, ImageWatermarkUtil imageWatermarkUtil, ListingImageMapper listingImageMapper, DesignerMapper designerMapper,
|
||||
public ListingMallServiceImpl(MinioUtil minioUtil, ListingImageMapper listingImageMapper, ListingWatermarkImageMapper listingWatermarkImageMapper, DesignerMapper designerMapper,
|
||||
OrderItemImageMapper orderItemImageMapper, OrderItemMapper orderItemMapper) {
|
||||
this.minioUtil = minioUtil;
|
||||
this.imageWatermarkUtil = imageWatermarkUtil;
|
||||
this.listingImageMapper = listingImageMapper;
|
||||
this.listingWatermarkImageMapper = listingWatermarkImageMapper;
|
||||
this.designerMapper = designerMapper;
|
||||
this.orderItemImageMapper = orderItemImageMapper;
|
||||
this.orderItemMapper = orderItemMapper;
|
||||
@@ -252,6 +253,40 @@ public class ListingMallServiceImpl extends ServiceImpl<ListingMallMapper, Listi
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤3:从数据库读取预生成的水印记录,将原图 URL 替换为水印图 URL
|
||||
List<ListingWatermarkImageEntity> watermarks = listingWatermarkImageMapper.selectByListingId(id);
|
||||
if (!watermarks.isEmpty()) {
|
||||
// 以 originalUrl 为 key,水印图 logical path 为 value,构建查询 map
|
||||
Map<String, String> watermarkMap = watermarks.stream()
|
||||
.collect(Collectors.toMap(
|
||||
ListingWatermarkImageEntity::getOriginalUrl,
|
||||
ListingWatermarkImageEntity::getWatermarkedUrl,
|
||||
(a, b) -> a
|
||||
));
|
||||
for (Map.Entry<String, List<String>> entry : imageMap.entrySet()) {
|
||||
List<String> urls = entry.getValue();
|
||||
if (urls == null || urls.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
// 遍历该类别的所有图片 URL,尝试从 watermarkMap 中找到对应的水印图
|
||||
// 命中则替换为水印图的 presigned URL;未命中则保留原 URL(说明该图未生成水印或不在水印表)
|
||||
List<String> watermarkedUrls = urls.stream()
|
||||
.map(url -> {
|
||||
try {
|
||||
String logicalPath = minioUtil.convertToLogicalPath(url);
|
||||
String wmUrl = watermarkMap.get(logicalPath);
|
||||
if (wmUrl != null) {
|
||||
return minioUtil.getImageUrl(wmUrl, CommonConstants.MINIO_PATH_TIMEOUT);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return url;
|
||||
})
|
||||
.toList();
|
||||
imageMap.put(entry.getKey(), watermarkedUrls);
|
||||
}
|
||||
}
|
||||
|
||||
DesignerEntity designer = designerMapper.selectOne(
|
||||
new LambdaQueryWrapper<DesignerEntity>()
|
||||
.eq(DesignerEntity::getUserId, entity.getSellerId())
|
||||
@@ -292,14 +327,6 @@ public class ListingMallServiceImpl extends ServiceImpl<ListingMallMapper, Listi
|
||||
}
|
||||
}
|
||||
|
||||
List<String> apparelUrls = imageMap.get("apparel");
|
||||
if (!CollectionUtils.isEmpty(apparelUrls)) {
|
||||
List<String> watermarkedUrls = apparelUrls.parallelStream()
|
||||
.map(imageWatermarkUtil::applyWatermark)
|
||||
.toList();
|
||||
imageMap.put("apparel", watermarkedUrls);
|
||||
}
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,6 @@ import java.awt.geom.AffineTransform;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@@ -22,17 +20,10 @@ public class ImageWatermarkUtil {
|
||||
private final WatermarkProperties watermarkProperties;
|
||||
private final MinioUtil minioUtil;
|
||||
|
||||
private static final String WATERMARK_CACHE_PREFIX = "watermark:";
|
||||
private static final int PRESIGNED_URL_EXPIRE_SECONDS = 7 * 24 * 60 * 60;
|
||||
|
||||
/**
|
||||
* 全局水印结果缓存,key = 原图 logical path hash,value = 带水印图片的 logical path
|
||||
*/
|
||||
private final Map<String, String> watermarkCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 对指定 MinIO 资源添加平铺文字水印,返回带水印图片的 presigned URL。
|
||||
* 相同原图结果会被缓存 30 天。
|
||||
* 对指定 MinIO 资源添加平铺文字水印,返回带水印图片的 presigned URL(有效期 7 天)。
|
||||
*/
|
||||
public String applyWatermark(String minioResource) {
|
||||
try {
|
||||
@@ -40,13 +31,6 @@ public class ImageWatermarkUtil {
|
||||
? minioUtil.getLogicalPathFromPresignedUrl(minioResource)
|
||||
: minioResource.trim();
|
||||
|
||||
String cacheKey = String.valueOf(logicalPath.hashCode());
|
||||
|
||||
String cachedPath = watermarkCache.get(cacheKey);
|
||||
if (cachedPath != null) {
|
||||
return minioUtil.getImageUrl(cachedPath, PRESIGNED_URL_EXPIRE_SECONDS);
|
||||
}
|
||||
|
||||
try (InputStream originalStream = minioUtil.downloadFile(logicalPath);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
|
||||
@@ -55,8 +39,6 @@ public class ImageWatermarkUtil {
|
||||
detectContentType(logicalPath));
|
||||
|
||||
String newPath = uploadWatermarkedImage(watermarkedBytes, logicalPath);
|
||||
watermarkCache.put(cacheKey, newPath);
|
||||
|
||||
return minioUtil.getImageUrl(newPath, PRESIGNED_URL_EXPIRE_SECONDS);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -141,18 +123,4 @@ public class ImageWatermarkUtil {
|
||||
if (lower.endsWith(".webp")) return "image/webp";
|
||||
return "image/jpeg";
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理指定原图的水印缓存(供外部在原图更新时调用)
|
||||
*/
|
||||
public void evictCache(String minioResource) {
|
||||
try {
|
||||
String logicalPath = minioUtil.isPresignedUrl(minioResource)
|
||||
? minioUtil.getLogicalPathFromPresignedUrl(minioResource)
|
||||
: minioResource.trim();
|
||||
watermarkCache.remove(String.valueOf(logicalPath.hashCode()));
|
||||
} catch (Exception e) {
|
||||
log.warn("清理水印缓存失败 resource={}", minioResource, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,3 +112,17 @@ CREATE TABLE seller_order_item_image (
|
||||
deleted INT(1) DEFAULT 0 COMMENT '是否删除:0-否,1-是',
|
||||
INDEX idx_listing_buyer (listing_id, buyer_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单商品图片快照表';
|
||||
|
||||
-- 商品图片水印表
|
||||
CREATE TABLE seller_listing_watermark_image (
|
||||
id BIGINT PRIMARY KEY COMMENT '主键ID',
|
||||
listing_id BIGINT NOT NULL COMMENT '商品ID',
|
||||
category VARCHAR(50) NOT NULL COMMENT '图片类别: cover/main_product/product/sketch/apparel',
|
||||
original_url VARCHAR(500) NOT NULL COMMENT '原图 logical path',
|
||||
watermarked_url VARCHAR(500) NOT NULL COMMENT '加水印图的 logical path',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
deleted INT(1) DEFAULT 0 COMMENT '是否删除:0-否,1-是',
|
||||
UNIQUE KEY uk_listing_category (listing_id, category, original_url),
|
||||
INDEX idx_listing_id (listing_id),
|
||||
INDEX idx_deleted (deleted)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品图片水印表';
|
||||
|
||||
16
src/main/resources/mapper/ListingWatermarkImageMapper.xml
Normal file
16
src/main/resources/mapper/ListingWatermarkImageMapper.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.aida.seller.module.listing.mapper.ListingWatermarkImageMapper">
|
||||
|
||||
<delete id="deleteByListingId">
|
||||
DELETE FROM seller_listing_watermark_image
|
||||
WHERE listing_id = #{listingId} AND deleted = 0
|
||||
</delete>
|
||||
|
||||
<select id="selectByListingId" resultType="com.aida.seller.module.listing.entity.ListingWatermarkImageEntity">
|
||||
SELECT id, listing_id, category, original_url, watermarked_url, create_time
|
||||
FROM seller_listing_watermark_image
|
||||
WHERE listing_id = #{listingId} AND deleted = 0
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
<update id="incrementSalesVolumeByOrderIds">
|
||||
UPDATE seller_listing
|
||||
SET sales_volume = sales_volume + 1
|
||||
SET sales_volume = sales_volume + 1, update_time = update_time
|
||||
WHERE id IN (
|
||||
SELECT DISTINCT listing_id FROM seller_order_item
|
||||
WHERE order_id IN
|
||||
|
||||
Reference in New Issue
Block a user