diff --git a/src/main/java/com/aida/seller/module/listing/entity/ListingWatermarkImageEntity.java b/src/main/java/com/aida/seller/module/listing/entity/ListingWatermarkImageEntity.java new file mode 100644 index 0000000..c445842 --- /dev/null +++ b/src/main/java/com/aida/seller/module/listing/entity/ListingWatermarkImageEntity.java @@ -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; +} diff --git a/src/main/java/com/aida/seller/module/listing/mapper/ListingWatermarkImageMapper.java b/src/main/java/com/aida/seller/module/listing/mapper/ListingWatermarkImageMapper.java new file mode 100644 index 0000000..782ebb7 --- /dev/null +++ b/src/main/java/com/aida/seller/module/listing/mapper/ListingWatermarkImageMapper.java @@ -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 { + + int deleteByListingId(@Param("listingId") Long listingId); + + List selectByListingId(@Param("listingId") Long listingId); +} diff --git a/src/main/java/com/aida/seller/module/listing/service/ListingServiceImpl.java b/src/main/java/com/aida/seller/module/listing/service/ListingServiceImpl.java index 9553a55..174584e 100644 --- a/src/main/java/com/aida/seller/module/listing/service/ListingServiceImpl.java +++ b/src/main/java/com/aida/seller/module/listing/service/ListingServiceImpl.java @@ -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 implements ListingService { private final ListingImageMapper listingImageMapper; + private final ListingWatermarkImageMapper listingWatermarkImageMapper; private final OrderItemMapper orderItemMapper; private final RedisTemplate redisTemplate; private final MinioUtil minioUtil; + private final ImageWatermarkUtil imageWatermarkUtil; @Override @Transactional(rollbackFor = Exception.class) @@ -68,6 +73,7 @@ public class ListingServiceImpl extends ServiceImpl oldWatermarks = Map.of(); if (dto.getId() == null) { entity.setStatus(dto.getStatus()); this.save(entity); @@ -86,6 +92,15 @@ public class ListingServiceImpl extends ServiceImpl() .eq(ListingImageEntity::getListingId, dto.getId())); listingId = dto.getId(); + + if (Objects.equals(dto.getStatus(), 1)) { + List 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 oldWatermarks) { + Set watermarkCategories = Set.of( + ImageCategoryEnum.COVER.getCode(), + ImageCategoryEnum.MAIN_PRODUCT.getCode(), + ImageCategoryEnum.PRODUCT.getCode(), + ImageCategoryEnum.SKETCH.getCode(), + ImageCategoryEnum.APPAREL.getCode()); + + List images = listingImageMapper.selectList( + new LambdaQueryWrapper() + .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) { diff --git a/src/main/java/com/aida/seller/module/listing/service/impl/ListingMallServiceImpl.java b/src/main/java/com/aida/seller/module/listing/service/impl/ListingMallServiceImpl.java index 51bd8a0..3632b57 100644 --- a/src/main/java/com/aida/seller/module/listing/service/impl/ListingMallServiceImpl.java +++ b/src/main/java/com/aida/seller/module/listing/service/impl/ListingMallServiceImpl.java @@ -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 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 watermarks = listingWatermarkImageMapper.selectByListingId(id); + if (!watermarks.isEmpty()) { + // 以 originalUrl 为 key,水印图 logical path 为 value,构建查询 map + Map watermarkMap = watermarks.stream() + .collect(Collectors.toMap( + ListingWatermarkImageEntity::getOriginalUrl, + ListingWatermarkImageEntity::getWatermarkedUrl, + (a, b) -> a + )); + for (Map.Entry> entry : imageMap.entrySet()) { + List urls = entry.getValue(); + if (urls == null || urls.isEmpty()) { + continue; + } + // 遍历该类别的所有图片 URL,尝试从 watermarkMap 中找到对应的水印图 + // 命中则替换为水印图的 presigned URL;未命中则保留原 URL(说明该图未生成水印或不在水印表) + List 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() .eq(DesignerEntity::getUserId, entity.getSellerId()) @@ -292,14 +327,6 @@ public class ListingMallServiceImpl extends ServiceImpl apparelUrls = imageMap.get("apparel"); - if (!CollectionUtils.isEmpty(apparelUrls)) { - List watermarkedUrls = apparelUrls.parallelStream() - .map(imageWatermarkUtil::applyWatermark) - .toList(); - imageMap.put("apparel", watermarkedUrls); - } - return vo; } diff --git a/src/main/java/com/aida/seller/util/ImageWatermarkUtil.java b/src/main/java/com/aida/seller/util/ImageWatermarkUtil.java index dbded08..153c8f9 100644 --- a/src/main/java/com/aida/seller/util/ImageWatermarkUtil.java +++ b/src/main/java/com/aida/seller/util/ImageWatermarkUtil.java @@ -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 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); - } - } } diff --git a/src/main/resources/db/schema.sql b/src/main/resources/db/schema.sql index 1375a41..cb3c168 100644 --- a/src/main/resources/db/schema.sql +++ b/src/main/resources/db/schema.sql @@ -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='商品图片水印表'; diff --git a/src/main/resources/mapper/ListingWatermarkImageMapper.xml b/src/main/resources/mapper/ListingWatermarkImageMapper.xml new file mode 100644 index 0000000..169a429 --- /dev/null +++ b/src/main/resources/mapper/ListingWatermarkImageMapper.xml @@ -0,0 +1,16 @@ + + + + + + DELETE FROM seller_listing_watermark_image + WHERE listing_id = #{listingId} AND deleted = 0 + + + + + diff --git a/src/main/resources/mapper/OrderItemMapper.xml b/src/main/resources/mapper/OrderItemMapper.xml index e962964..f2968d8 100644 --- a/src/main/resources/mapper/OrderItemMapper.xml +++ b/src/main/resources/mapper/OrderItemMapper.xml @@ -53,7 +53,7 @@ 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