From 09dbbb2c240b94b5d5bdde69e94bb0829142579b Mon Sep 17 00:00:00 2001 From: liujiang <569804566@qq.com> Date: Fri, 1 Aug 2025 17:42:42 +0800 Subject: [PATCH] =?UTF-8?q?master=EF=BC=9A=E5=95=86=E5=93=81=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=8F=8A=E7=BC=96=E8=BE=91=E8=B0=83=E6=95=B4=E5=9B=BE?= =?UTF-8?q?=E5=8C=85=E5=8E=8B=E7=BC=A9=E7=8A=B6=E6=80=81=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-common/pom.xml | 7 ++ .../com/ruoyi/common/utils/AdValidator.java | 52 ++++++++++++++ .../com/ruoyi/common/utils/HtmlValidator.java | 51 ++++++++++++++ .../ruoyi/xkt/domain/StoreProductFile.java | 24 ++----- .../xkt/enums/StoreProdFileZipStatus.java | 37 ++++++++++ .../service/impl/StoreProductServiceImpl.java | 69 +++++++++++-------- 6 files changed, 192 insertions(+), 48 deletions(-) create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/AdValidator.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/HtmlValidator.java create mode 100644 xkt/src/main/java/com/ruoyi/xkt/enums/StoreProdFileZipStatus.java diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml index 92d9535c1..d6fc361e9 100644 --- a/ruoyi-common/pom.xml +++ b/ruoyi-common/pom.xml @@ -186,6 +186,13 @@ javax.servlet-api + + + org.jsoup + jsoup + 1.16.1 + + \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/AdValidator.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/AdValidator.java new file mode 100644 index 000000000..a91a617d0 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/AdValidator.java @@ -0,0 +1,52 @@ +package com.ruoyi.common.utils; + +import java.util.*; + +/** + * @author liujiang + * @version v1.0 + * @date 2025/8/1 14:35 + */ +public class AdValidator { + + /** + * 检查标题是否包含任意敏感词(全词匹配) + * + * @param prodTitle 待校验的标题 + * @return true-包含敏感词, false-不包含 + */ + public static boolean containsProhibitedWord(String prodTitle) { + for (String word : AdValidator.PROHIBITED_WORDS_SET) { + if (prodTitle.contains(word)) { + return true; + } + } + return false; + } + + /** + * 检查标题并返回匹配到的所有敏感词 + * + * @param prodTitle 待校验的标题 + * @return 匹配到的敏感词列表(若无则返回空列表) + */ + public static List findProhibitedWords(String prodTitle) { + List matchedWords = new ArrayList<>(); + for (String word : AdValidator.PROHIBITED_WORDS_SET) { + if (prodTitle.contains(word)) { + matchedWords.add(word); + } + } + return matchedWords; + } + + // 使用 HashSet 存储违禁词(全局唯一,避免重复初始化) + private static final Set PROHIBITED_WORDS_SET = new HashSet<>(Arrays.asList( + "最", "最佳", "最优", "最好", "最大", "最高", "最便宜", "最时尚", "最舒适", "最流行", + "最先进", "最顶级", "首选", "唯一", "独家", "全网第一", "行业领先", "销量冠军", + "100%", "绝对", "无敌", "完美", "终极", "极致", "巅峰", "史无前例", "遥遥领先", + "微信", "QQ", "加V", "私聊", "联系客服", "电话", "下单", "购买", + "小红书", "抖音", "微博", "官网", "网址", "二维码" + )); + +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/HtmlValidator.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/HtmlValidator.java new file mode 100644 index 000000000..646d2b048 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/HtmlValidator.java @@ -0,0 +1,51 @@ +package com.ruoyi.common.utils; + +import org.jsoup.Jsoup; +import org.jsoup.safety.Safelist; + +/** + * @author liujiang + * @version v1.0 + * @date 2025/8/1 16:25 + */ +public class HtmlValidator { + + // 定义允许的 HTML 标签和属性(基于 UniApp rich-text 组件规则) + private static final Safelist ALLOWED_HTML = Safelist.relaxed() + .addTags( + "a", "abbr", "address", "article", "aside", "b", "bdi", "bdo", "big", "blockquote", + "br", "caption", "center", "cite", "code", "col", "colgroup", "dd", "del", "div", + "dl", "dt", "em", "fieldset", "font", "footer", "h1", "h2", "h3", "h4", "h5", "h6", + "header", "hr", "i", "img", "ins", "label", "legend", "li", "mark", "nav", "ol", "p", + "pre", "q", "rt", "ruby", "s", "section", "small", "span", "strong", "sub", "sup", + "table", "tbody", "td", "tfoot", "th", "thead", "tr", "tt", "u", "ul" + ) + .addAttributes("a", "href", "title", "target") + .addAttributes("img", "src", "alt", "height", "width") + .addAttributes("table", "width", "height", "colspan", "rowspan", "align", "valign") + .addAttributes("td", "colspan", "rowspan", "align", "valign") + .addAttributes("th", "colspan", "rowspan", "align", "valign") + .addAttributes("font", "color", "face", "size") + .addAttributes(":all", "class", "id", "style"); // 允许全局属性 + + /** + * 检查 HTML 是否合法(仅包含允许的标签和属性) + * + * @param html 输入的富文本 + * @return true=合法,false=包含非法标签或属性 + */ + public static boolean isValidHtml(String html) { + return Jsoup.isValid(html, ALLOWED_HTML); + } + + /** + * 清理 HTML,移除非法标签和属性 + * + * @param html 输入的富文本 + * @return 清理后的安全 HTML + */ + public static String sanitizeHtml(String html) { + return Jsoup.clean(html, ALLOWED_HTML); + } + +} diff --git a/xkt/src/main/java/com/ruoyi/xkt/domain/StoreProductFile.java b/xkt/src/main/java/com/ruoyi/xkt/domain/StoreProductFile.java index 6d6d450bb..1f724dc6f 100644 --- a/xkt/src/main/java/com/ruoyi/xkt/domain/StoreProductFile.java +++ b/xkt/src/main/java/com/ruoyi/xkt/domain/StoreProductFile.java @@ -6,8 +6,6 @@ import com.ruoyi.common.core.domain.XktBaseEntity; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; import java.math.BigDecimal; @@ -65,21 +63,9 @@ public class StoreProductFile extends XktBaseEntity { @Excel(name = "排序") private Integer orderNum; - @Override - public String toString() { - return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) - .append("id", getId()) - .append("storeProdId", getStoreProdId()) - .append("fileId", getFileId()) - .append("fileType", getFileType()) - .append("fileSize", getFileSize()) - .append("orderNum", getOrderNum()) - .append("version", getVersion()) - .append("delFlag", getDelFlag()) - .append("createBy", getCreateBy()) - .append("createTime", getCreateTime()) - .append("updateBy", getUpdateBy()) - .append("updateTime", getUpdateTime()) - .toString(); - } + /** + * 图包处理状态 图包状态[1:非图包/不处理 2:待处理 3:处理中 4:已处理 5:处理异常] + */ + private Integer picZipStatus; + } diff --git a/xkt/src/main/java/com/ruoyi/xkt/enums/StoreProdFileZipStatus.java b/xkt/src/main/java/com/ruoyi/xkt/enums/StoreProdFileZipStatus.java new file mode 100644 index 000000000..5eb225d8a --- /dev/null +++ b/xkt/src/main/java/com/ruoyi/xkt/enums/StoreProdFileZipStatus.java @@ -0,0 +1,37 @@ +package com.ruoyi.xkt.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 商品图包压缩类型 + * + * @author liujiang + * @date 2025-04-02 23:42 + */ +@Getter +@AllArgsConstructor +public enum StoreProdFileZipStatus { + + NON_ZIP(1, "非图包/不处理"), + PENDING(2, "待处理"), + PROCESSING(3, "处理中"), + PROCESSED(4, "已处理"), + FAILED(5, "处理异常"); + + private final Integer value; + private final String label; + + // 根据 value 获取对应的枚举实例 + public static StoreProdFileZipStatus of(Integer value) { + if (value == null) { + throw new IllegalArgumentException("图包状态值不能为空"); + } + for (StoreProdFileZipStatus status : values()) { + if (status.value.equals(value)) { + return status; + } + } + throw new IllegalArgumentException("无效的图包状态值: " + value); + } +} diff --git a/xkt/src/main/java/com/ruoyi/xkt/service/impl/StoreProductServiceImpl.java b/xkt/src/main/java/com/ruoyi/xkt/service/impl/StoreProductServiceImpl.java index ec957750b..6bdee2f35 100644 --- a/xkt/src/main/java/com/ruoyi/xkt/service/impl/StoreProductServiceImpl.java +++ b/xkt/src/main/java/com/ruoyi/xkt/service/impl/StoreProductServiceImpl.java @@ -25,7 +25,9 @@ import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.user.CaptchaException; import com.ruoyi.common.exception.user.CaptchaExpireException; +import com.ruoyi.common.utils.AdValidator; import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.HtmlValidator; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.framework.es.EsClientWrapper; import com.ruoyi.framework.oss.OSSClientWrapper; @@ -202,18 +204,19 @@ public class StoreProductServiceImpl implements IStoreProductService { @Override @Transactional public int create(StoreProdDTO createDTO) throws IOException { - - // TODO 富文本标签过滤 - // TODO 富文本标签过滤 - // TODO 富文本标签过滤 - // TODO 富文本标签过滤 - // TODO 富文本标签过滤 - // TODO 富文本标签过滤 - // TODO 富文本标签过滤 - // 用户是否为档口管理者或子账户 if (!SecurityUtils.isAdmin() && !SecurityUtils.isStoreManagerOrSub(createDTO.getStoreId())) { throw new ServiceException("当前用户非档口管理者或子账号,无权限操作!", HttpStatus.ERROR); + } // 校验标题中是否包含违禁词 + List prohibitedWords = AdValidator.findProhibitedWords(createDTO.getProdTitle().trim()); + if (CollectionUtils.isNotEmpty(prohibitedWords)) { + throw new ServiceException("商品标题含有违禁词: " + String.join(",", prohibitedWords), HttpStatus.ERROR); + } + // 校验富文本标签是否合法 + boolean isValid = HtmlValidator.isValidHtml(createDTO.getDetail()); + if (!isValid) { + // 清理 HTML + createDTO.setDetail(HtmlValidator.sanitizeHtml(createDTO.getDetail())); } // 组装StoreProduct数据 StoreProduct storeProd = BeanUtil.toBean(createDTO, StoreProduct.class).setVoucherDate(DateUtils.getNowDate()) @@ -247,17 +250,21 @@ public class StoreProductServiceImpl implements IStoreProductService { @Override @Transactional public int update(final Long storeProdId, StoreProdDTO updateDTO) throws IOException { - - - // TODO 富文本标签过滤 - // TODO 富文本标签过滤 - // TODO 富文本标签过滤 - // TODO 富文本标签过滤 - // TODO 富文本标签过滤 - // TODO 富文本标签过滤 - // TODO 富文本标签过滤 - - + // 用户是否为档口管理者或子账户 + if (!SecurityUtils.isAdmin() && !SecurityUtils.isStoreManagerOrSub(updateDTO.getStoreId())) { + throw new ServiceException("当前用户非档口管理者或子账号,无权限操作!", HttpStatus.ERROR); + } + // 校验标题中是否包含违禁词 + List prohibitedWords = AdValidator.findProhibitedWords(updateDTO.getProdTitle().trim()); + if (CollectionUtils.isNotEmpty(prohibitedWords)) { + throw new ServiceException("商品标题含有违禁词: " + String.join(",", prohibitedWords), HttpStatus.ERROR); + } + // 校验富文本标签是否合法 + boolean isValid = HtmlValidator.isValidHtml(updateDTO.getDetail()); + if (!isValid) { + // 清理 HTML + updateDTO.setDetail(HtmlValidator.sanitizeHtml(updateDTO.getDetail())); + } StoreProduct storeProd = Optional.ofNullable(this.storeProdMapper.selectOne(new LambdaQueryWrapper() .eq(StoreProduct::getId, storeProdId).eq(StoreProduct::getDelFlag, Constants.UNDELETED))) .orElseThrow(() -> new ServiceException("档口商品不存在!", HttpStatus.ERROR)); @@ -310,7 +317,9 @@ public class StoreProductServiceImpl implements IStoreProductService { Map fileMap = fileList.stream().collect(Collectors.toMap(SysFile::getFileName, SysFile::getId)); // 档口文件(商品主图、主图视频、下载的商品详情) List prodFileList = fileDTOList.stream().map(x -> BeanUtil.toBean(x, StoreProductFile.class) - .setFileId(fileMap.get(x.getFileName())).setStoreProdId(storeProd.getId()).setStoreId(updateDTO.getStoreId())) + .setFileId(fileMap.get(x.getFileName())).setStoreProdId(storeProd.getId()).setStoreId(updateDTO.getStoreId()) + .setPicZipStatus(Objects.equals(x.getFileType(), FileType.DOWNLOAD.getValue()) + ? StoreProdFileZipStatus.PENDING.getValue() : StoreProdFileZipStatus.NON_ZIP.getValue())) .collect(Collectors.toList()); this.storeProdFileMapper.insert(prodFileList); // 档口类目属性 @@ -425,7 +434,9 @@ public class StoreProductServiceImpl implements IStoreProductService { Map fileMap = fileList.stream().collect(Collectors.toMap(SysFile::getFileName, SysFile::getId)); // 档口文件(商品主图、主图视频、下载的商品详情) List prodFileList = fileDTOList.stream().map(x -> BeanUtil.toBean(x, StoreProductFile.class) - .setFileId(fileMap.get(x.getFileName())).setStoreProdId(storeProd.getId()).setStoreId(createDTO.getStoreId())) + .setFileId(fileMap.get(x.getFileName())).setStoreProdId(storeProd.getId()).setStoreId(createDTO.getStoreId()) + .setPicZipStatus(Objects.equals(x.getFileType(), FileType.DOWNLOAD.getValue()) + ? StoreProdFileZipStatus.PENDING.getValue() : StoreProdFileZipStatus.NON_ZIP.getValue())) .collect(Collectors.toList()); this.storeProdFileMapper.insert(prodFileList); // 档口类目属性 @@ -447,9 +458,9 @@ public class StoreProductServiceImpl implements IStoreProductService { /** * 新增档口商品颜色等 * - * @param createDTO 入参 + * @param createDTO 入参 * @param storeProdId 档口商品ID - * @param storeId 档口ID + * @param storeId 档口ID */ private void createProdColor(StoreProdDTO createDTO, Long storeProdId, Long storeId) { // 处理档口所有颜色 @@ -571,9 +582,9 @@ public class StoreProductServiceImpl implements IStoreProductService { public List prepareGetPicPackDownloadUrl(Long storeProductId) { Assert.notNull(storeProductId); List fileIds = storeProdFileMapper.selectList(Wrappers.lambdaQuery(StoreProductFile.class) - .eq(StoreProductFile::getStoreProdId, storeProductId) - .in(StoreProductFile::getFileType, FileType.picPackValues()) - .eq(XktBaseEntity::getDelFlag, UNDELETED)) + .eq(StoreProductFile::getStoreProdId, storeProductId) + .in(StoreProductFile::getFileType, FileType.picPackValues()) + .eq(XktBaseEntity::getDelFlag, UNDELETED)) .stream() .map(StoreProductFile::getFileId) .filter(Objects::nonNull) @@ -583,7 +594,7 @@ public class StoreProductServiceImpl implements IStoreProductService { } List files = fileMapper.selectByIds(fileIds); return files.stream() - .filter(o-> UNDELETED.equals(o.getDelFlag())) + .filter(o -> UNDELETED.equals(o.getDelFlag())) .map(o -> { PicPackSimpleDTO dto = BeanUtil.toBean(o, PicPackSimpleDTO.class); dto.setFileId(o.getId()); @@ -879,7 +890,7 @@ public class StoreProductServiceImpl implements IStoreProductService { /** * 给设置了所有商品优惠的客户创建优惠 * - * @param colorList 档口商品颜色列表 + * @param colorList 档口商品颜色列表 * @param storeProdId 档口商品ID */ private void createStoreCusDiscount(List colorList, Long storeProdId) {