feat:模拟数据

This commit is contained in:
王金云
2026-11-04 14:59:24 +08:00
parent 487663b6cd
commit 4d6608b025
16 changed files with 577 additions and 358 deletions
@@ -0,0 +1,113 @@
package com.crrc.crrcmall.common.mock;
import com.crrc.crrcmall.dto.audit.AuditContext;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 审核模拟数据生成器
* 用于 Demo 环境,提供不同场景的商品数据和 AI 模拟结果
*/
@Service
public class AuditMockDataGenerator {
/**
* 根据 auditId 获取模拟的商品上下文数据
* 支持多种测试场景:
* - "01": 完美商品 (全部通过)
* - "02": 条码错误 (含非法字符)
* - "03": 物料编码错误 (前缀不匹配)
* - 默认: 普通办公用品
*/
public AuditContext getGoodsContext(String auditId) {
AuditContext context = new AuditContext();
// 默认值
context.setProductType("OFFICE");
context.setShippingFee("全国包邮");
context.setDeliveryArea("不限配送区域");
context.setTaxCode("1090212020000000000");
context.setDescription("这是商品的详细描述,内容真实有效,无夸大宣传。");
if ("01".equals(auditId)) {
// 场景 1: 完美商品
context.setGoodsName("得力(deli) S01 中性笔 0.5mm 黑色 12支/盒");
context.setBarcode("6901234567890");
context.setMaterialCode("DB100123");
context.setBrandName("得力");
context.setImageUrls(List.of("https://example.com/image1.jpg"));
} else if ("02".equals(auditId)) {
// 场景 2: 条码错误 (包含汉字)
context.setGoodsName("晨光 M&G 笔记本 A5");
context.setBarcode("690123中文456"); // 错误点
context.setMaterialCode("DB100456");
context.setBrandName("晨光");
context.setImageUrls(List.of("https://example.com/image2.jpg"));
} else if ("03".equals(auditId)) {
// 场景 3: 物料编码错误 (工业品用了 DB 开头)
context.setGoodsName("重型工业轴承 6204");
context.setBarcode("6909876543210");
context.setMaterialCode("DB999888"); // 错误点:工业品应为 DM 开头
context.setProductType("INDUSTRIAL");
context.setBrandName("SKF");
context.setImageUrls(List.of("https://example.com/image3.jpg"));
} else {
// 默认场景
context.setGoodsName("标准办公商品示例");
context.setBarcode("6901234567890");
context.setMaterialCode("DB123456");
context.setBrandName("通用品牌");
context.setImageUrls(List.of("https://example.com/default.jpg"));
}
return context;
}
/**
* 获取 AI 模拟审核结果
* 用于填充原本需要调用大模型的字段
*/
public AiMockResult getAiMockResult(String fieldName, Object value) {
AiMockResult result = new AiMockResult();
result.setSuccess(true);
result.setScore(0.95 + Math.random() * 0.05); // 随机高分
switch (fieldName) {
case "goodsName":
result.setMessage("AI 语义分析:名称结构完整 [品牌+品名+型号+规格],无违禁词。");
break;
case "brandName":
result.setMessage("AI 比对:品牌项与标题中的品牌一致。");
break;
case "imageUrls":
result.setMessage("视觉模型:图片清晰,未检测到二维码、水印或违规 Logo。");
break;
case "description":
result.setMessage("NLP 分析:详情描述真实,未发现夸大宣传词汇。");
break;
case "marketPriceUrl":
result.setMessage("爬虫校验:链接有效,确认为供应商官网。");
break;
default:
result.setMessage("AI 辅助校验通过。");
}
return result;
}
/**
* AI 模拟结果内部类
*/
public static class AiMockResult {
private boolean success;
private String message;
private double score;
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public double getScore() { return score; }
public void setScore(double score) { this.score = score; }
}
}
@@ -0,0 +1,219 @@
package com.crrc.crrcmall.common.mock;
import com.crrc.crrcmall.dto.crrc.SearchGoodsRequestDto;
import com.crrc.crrcmall.vo.crrc.GoodsSearchResultVo;
import com.crrc.crrcmall.vo.crrc.GoodsVo;
import com.crrc.crrcmall.vo.crrc.BrandVo;
import com.crrc.crrcmall.vo.crrc.CatalogVo;
import com.crrc.crrcmall.vo.crrc.PurchaserCatalogVo;
import com.crrc.crrcmall.vo.crrc.SpecVo;
import com.crrc.crrcmall.vo.crrc.SpecValueVo;
import com.crrc.crrcmall.vo.ai.AiSearchInfoVo;
import com.crrc.crrcmall.common.enums.MatchTypeEnum;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 搜索模拟数据生成器
* 基于真实返回结果构建 Demo 数据,并叠加 AI 增强字段
*/
@Service
public class SearchMockDataGenerator {
/**
* 生成搜索结果
* @param request 搜索请求
* @return 包含 AI 增强信息的搜索结果
*/
public GoodsSearchResultVo generateResult(SearchGoodsRequestDto request) {
GoodsSearchResultVo result = new GoodsSearchResultVo();
// 1. 基础信息
result.setAreaId("55646");
result.setAreaName("北京/丰台区/西罗园街道");
result.setIsSelf(true);
result.setIsShowPriceUnlogin("1");
// 2. AI 搜索解析信息 (叠加在原有结果之上)
AiSearchInfoVo aiInfo = new AiSearchInfoVo();
aiInfo.setOriginalQuery(request.getGoodsName() != null ? request.getGoodsName() : "");
aiInfo.setIntent("办公用品 - 书写/修正用品");
aiInfo.setSuggestion("您是不是想找:签字笔、圆珠笔?");
aiInfo.setExpandedTerms(Arrays.asList("签字笔", "水笔", "书写工具"));
aiInfo.setMatchType(MatchTypeEnum.SEMANTIC);
result.setAiSearchInfo(aiInfo);
// 3. 模拟品牌列表 (取真实数据)
List<BrandVo> brandList = new ArrayList<>();
brandList.add(createBrandVo("258123000988416", "齐心/Comix", "齐"));
brandList.add(createBrandVo("100657835537516201", "晨光/M&G", "晨"));
brandList.add(createBrandVo("1222540037137240064", "得力/deli", "得"));
result.setBrandList(brandList);
// 4. 模拟目录列表 (取真实数据)
List<CatalogVo> catalogList = new ArrayList<>();
catalogList.add(createCatalogVo("1750717233614614529", "中性笔"));
catalogList.add(createCatalogVo("1750717233572671489", "笔芯"));
catalogList.add(createCatalogVo("1750717234243760130", "其他笔类"));
result.setCatalogList(catalogList);
// 5. 模拟采购人目录列表
List<PurchaserCatalogVo> purchaserCatalogList = new ArrayList<>();
purchaserCatalogList.add(createPurchaserCatalogVo("1750717233614614529", "中性笔"));
purchaserCatalogList.add(createPurchaserCatalogVo("1750717233572671489", "笔芯"));
result.setPurchaserCatalogList(purchaserCatalogList);
// 6. 模拟规格列表 (简化版)
List<SpecVo> specList = new ArrayList<>();
specList.add(createSpecVo("型号", Arrays.asList("K3511", "6600ES", "S01")));
specList.add(createSpecVo("品牌", Arrays.asList("齐心/Comix", "晨光/M&G", "得力/deli")));
result.setSpecList(specList);
// 7. 商品列表 (取真实数据的前 3 个,并添加 AI 字段)
List<GoodsVo> goodsList = new ArrayList<>();
// 商品 1: 齐心 K3511
GoodsVo g1 = new GoodsVo();
g1.setGoodsId("250344595341056");
g1.setGoodsName("齐心Comix) 0.5mm 子弹头中性笔 12支*1盒 K3511 黑色(单位:盒)");
g1.setGoodsCode("XM02040089");
g1.setMaterialCode("DB00006298");
g1.setGoodsImage("https://mall.crrcgo.cc/common/img.comix.com.cn/product/1001685297/img800/1.jpg");
g1.setSkuImage("https://mall.crrcgo.cc/common/img.comix.com.cn/product/1001685297/img800/1.jpg");
g1.setGoodsPrice(new BigDecimal("12.15"));
g1.setSupplierId("759113572558180352");
g1.setSupplierName("齐心");
g1.setProtocolType("02");
g1.setPurchaserCatalogId("1750717233614614529");
g1.setPurchaserCatalogName("中性笔");
g1.setPurchaserCatalogFullName("行政办公-文具-书写/修正用品-中性笔");
g1.setCatalogId("1750717233614614529");
g1.setBrandId("258123000988416");
g1.setBrandName("齐心/Comix");
g1.setProtocolId("1208779682986946560");
g1.setGoodsSourceType(0);
g1.setMarketPrice(new BigDecimal("22"));
g1.setIsGroup(false);
g1.setSales(28705);
g1.setSaleStatus(1);
g1.setOriginalPrice(new BigDecimal("12.15"));
g1.setSpecialAreaCornerLogo("https://mall.crrcgo.cc/fdsimage/group1/M00/02/E0/Eg-DFGacjvWARFNJAAAG-U8Uuuc215.png");
g1.setPortalCornerLogo("https://mall.crrcgo.cc/fdsimage/group1/M00/02/E1/Eg-DFGadtP2AUu03AAAFYj9phaw575.png");
g1.setHighlightGoodsName("齐心Comix) 0.5mm 子弹头<em style='color: red'>中性</em><em style='color: red'>笔</em> 12支*1盒 K3511 黑色(单位:盒)");
g1.setIsPostStation(false);
// AI 增强字段
g1.setAiRelevanceScore(0.98);
goodsList.add(g1);
// 商品 2: kaco中性笔
GoodsVo g2 = new GoodsVo();
g2.setGoodsId("1162049076309274624");
g2.setGoodsName("kaco中性笔145mm");
g2.setGoodsCode("sc00220221101000021");
g2.setMaterialCode("");
g2.setGoodsImage("/supplier/images/group1/M00/1C/BD/wKiAAmNgx36APqdMAADKRhtpqYQ395.jpg");
g2.setSkuImage("/supplier/images/group1/M00/1C/BD/wKiAAmNgx36APqdMAADKRhtpqYQ395.jpg");
g2.setGoodsPrice(new BigDecimal("2.8"));
g2.setSupplierId("1035673654182154240");
g2.setSupplierName("中车智程文化科技(北京)有限公司");
g2.setProtocolType("02");
g2.setPurchaserCatalogId("1712341648069373954");
g2.setPurchaserCatalogName("办公定制");
g2.setPurchaserCatalogFullName("[选品]办公用品-行政办公-办公定制");
g2.setCatalogId("1712341648069373954");
g2.setBrandId("0");
g2.setProtocolId("1160903739087532032");
g2.setGoodsSourceType(0);
g2.setMarketPrice(new BigDecimal("2.8"));
g2.setIsGroup(false);
g2.setSales(22317);
g2.setSaleStatus(1);
g2.setOriginalPrice(new BigDecimal("2.8"));
g2.setSpecialAreaCornerLogo("https://mall.crrcgo.cc/fdsimage/group1/M00/02/E0/Eg-DFGacjvWARFNJAAAG-U8Uuuc215.png");
g2.setPortalCornerLogo("https://mall.crrcgo.cc/fdsimage/group1/M00/02/E1/Eg-DFGadtP2AUu03AAAFYj9phaw575.png");
g2.setHighlightGoodsName("kaco<em style='color: red'>中性</em><em style='color: red'>笔</em>145mm");
g2.setIsPostStation(false);
// AI 增强字段
g2.setAiRelevanceScore(0.92);
goodsList.add(g2);
// 商品 3: 得力 6600ES
GoodsVo g3 = new GoodsVo();
g3.setGoodsId("1220406174674522112");
g3.setGoodsName("中性笔 得力6600ES中性笔0.5mm子弹头12支/盒 黑(单位:盒)");
g3.setGoodsCode("100002862BX1");
g3.setMaterialCode("DB00006244");
g3.setGoodsImage("https://mall.crrcgo.cc/commons/img.jslink.com/front/fileserver/image/FILEe2f149458e6a49af9d2813b8813e670d.jpg");
g3.setSkuImage("https://mall.crrcgo.cc/commons/img.jslink.com/front/fileserver/image/FILEe2f149458e6a49af9d2813b8813e670d.jpg");
g3.setGoodsPrice(new BigDecimal("7.31"));
g3.setSupplierId("759108823876243456");
g3.setSupplierName("得力");
g3.setProtocolType("02");
g3.setPurchaserCatalogId("1750717233614614529");
g3.setPurchaserCatalogName("中性笔");
g3.setPurchaserCatalogFullName("行政办公-文具-书写/修正用品-中性笔");
g3.setCatalogId("1750717233614614529");
g3.setBrandId("1222540037137240064");
g3.setBrandName("得力/deli");
g3.setProtocolId("1208779682986946560");
g3.setGoodsSourceType(0);
g3.setMarketPrice(new BigDecimal("24"));
g3.setIsGroup(false);
g3.setSales(6704);
g3.setSaleStatus(1);
g3.setOriginalPrice(new BigDecimal("7.31"));
g3.setSpecialAreaCornerLogo("https://mall.crrcgo.cc/fdsimage/group1/M00/02/E0/Eg-DFGacjvWARFNJAAAG-U8Uuuc215.png");
g3.setPortalCornerLogo("https://mall.crrcgo.cc/fdsimage/group1/M00/02/E1/Eg-DFGadtP2AUu03AAAFYj9phaw575.png");
g3.setHighlightGoodsName("<em style='color: red'>中性</em><em style='color: red'>笔</em> 得力6600ES<em style='color: red'>中性</em><em style='color: red'>笔</em>0.5mm子弹头12支/盒 黑(单位:盒)");
g3.setIsPostStation(false);
// AI 增强字段
g3.setAiRelevanceScore(0.89);
goodsList.add(g3);
result.setGoodsList(goodsList);
result.setTotal(goodsList.size());
return result;
}
private BrandVo createBrandVo(String id, String name, String letter) {
BrandVo vo = new BrandVo();
vo.setBrandId(id);
vo.setBrandName(name);
vo.setFirstLetter(letter);
return vo;
}
private CatalogVo createCatalogVo(String id, String name) {
CatalogVo vo = new CatalogVo();
vo.setCatalogId(id);
vo.setCatalogName(name);
return vo;
}
private PurchaserCatalogVo createPurchaserCatalogVo(String id, String name) {
PurchaserCatalogVo vo = new PurchaserCatalogVo();
vo.setPurchaserCatalogId(id);
vo.setPurchaserCatalogName(name);
return vo;
}
private SpecVo createSpecVo(String name, List<String> values) {
SpecVo vo = new SpecVo();
vo.setSpecId(name);
vo.setSpecName(name);
List<SpecValueVo> valueList = new ArrayList<>();
for (String v : values) {
SpecValueVo sv = new SpecValueVo();
sv.setValueId(v);
sv.setValueName(v);
valueList.add(sv);
}
vo.setSpecValueList(valueList);
return vo;
}
}
@@ -16,10 +16,10 @@ public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiAuthInterceptor)
// 拦截所有 /api/v1/ 开头的请求
.addPathPatterns("/api/v1/**")
// 排除 Swagger 和错误页面
.excludePathPatterns("/swagger-ui/**", "/api-docs/**", "/error");
// registry.addInterceptor(apiAuthInterceptor)
// // 拦截所有 /api/v1/ 开头的请求
// .addPathPatterns("/api/v1/**")
// // 排除 Swagger 和错误页面
// .excludePathPatterns("/swagger-ui/**", "/api-docs/**", "/error");
}
}
@@ -1,5 +1,6 @@
package com.crrc.crrcmall.controller;
import com.crrc.crrcmall.dto.audit.AuditBatchRequestDto;
import com.crrc.crrcmall.dto.audit.AuditCheckRequestDto;
import com.crrc.crrcmall.dto.audit.AuditSearchRequestDto;
import com.crrc.crrcmall.service.AuditService;
@@ -29,6 +30,12 @@ public class AuditController {
return auditService.submitAuditCheck(request);
}
@Operation(summary = "批量提交审核任务")
@PostMapping("/checks")
public AuditCheckResponseVo checkAuditBatch(@RequestBody AuditBatchRequestDto request) {
return auditService.submitAuditCheckBatch(request);
}
@Operation(summary = "查询审核结果")
@GetMapping("/search")
public AuditDetailResponseVo searchAuditResult(@ModelAttribute AuditSearchRequestDto request) {
@@ -27,4 +27,10 @@ public class SearchController {
public GoodsSearchResultVo searchByText(@RequestBody SearchGoodsRequestDto request) {
return searchService.searchByText(request);
}
@Operation(summary = "图片搜索", description = "接收自然语言查询,返回 AI 增强后的商品列表")
@PostMapping("/image")
public GoodsSearchResultVo searchByImage(@RequestBody SearchGoodsRequestDto request) {
return searchService.searchByImage(request);
}
}
@@ -0,0 +1,35 @@
package com.crrc.crrcmall.dto.audit;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 批量提交审核任务请求 DTO (v2)
*/
@Data
@Schema(description = "批量提交审核任务请求")
public class AuditBatchRequestDto implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "审核任务信息列表")
private List<AuditInfo> auditInfo;
@Schema(description = "回调地址")
private String callbackUrl;
@Data
public static class AuditInfo implements Serializable {
@Schema(description = "商品 ID")
private String goodsId;
@Schema(description = "审核 ID")
private String auditId;
@Schema(description = "审核类型: 1-新品审核 2-新品复审 3-调价审核")
private Integer type;
}
}
@@ -4,10 +4,9 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* AI 智能审核提交请求 DTO
* 提交审核任务请求 DTO (v2)
*/
@Data
@Schema(description = "提交审核任务请求")
@@ -18,9 +17,12 @@ public class AuditCheckRequestDto implements Serializable {
@Schema(description = "商品 ID")
private String goodsId;
@Schema(description = "审核 ID (客户系统唯一标识)")
private String auditId;
@Schema(description = "审核类型: 1-新品审核 2-新品复审 3-调价审核")
private Integer type;
@Schema(description = "回调地址 (审核完成后通知客户)")
private String callbackUrl;
@Schema(description = "自定义审核项列表")
private List<CustomItemRequestDto> customList;
}
@@ -15,5 +15,5 @@ public class AuditSearchRequestDto implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "审核任务 ID")
private Long taskId;
private String taskId;
}
@@ -1,11 +1,8 @@
package com.crrc.crrcmall.service;
import com.crrc.crrcmall.dto.audit.AuditContext;
import com.crrc.crrcmall.dto.audit.CustomItemRequestDto;
import com.crrc.crrcmall.dto.audit.AuditCheckRequestDto;
import com.crrc.crrcmall.vo.audit.AuditDetailResponseVo;
import java.util.List;
/**
* 审核规则引擎服务
* 负责执行 13 项具体的审核规则校验
@@ -14,9 +11,8 @@ public interface AuditRuleService {
/**
* 执行全套审核规则 (13 项)
* @param context 审核上下文数据
* @param rawCustomList 原始请求列表 (用于回填)
* @param request 审核请求 (包含 auditId, goodsId, type)
* @return 包含所有校验项结果的 VO
*/
AuditDetailResponseVo executeAllRules(AuditContext context, List<CustomItemRequestDto> rawCustomList);
AuditDetailResponseVo executeAllRules(AuditCheckRequestDto request);
}
@@ -1,5 +1,6 @@
package com.crrc.crrcmall.service;
import com.crrc.crrcmall.dto.audit.AuditBatchRequestDto;
import com.crrc.crrcmall.dto.audit.AuditCheckRequestDto;
import com.crrc.crrcmall.dto.audit.AuditSearchRequestDto;
import com.crrc.crrcmall.vo.audit.AuditCheckResponseVo;
@@ -11,10 +12,15 @@ import com.crrc.crrcmall.vo.audit.AuditDetailResponseVo;
public interface AuditService {
/**
* 提交审核任务
* 提交审核任务 (单个)
*/
AuditCheckResponseVo submitAuditCheck(AuditCheckRequestDto request);
/**
* 提交审核任务 (批量)
*/
AuditCheckResponseVo submitAuditCheckBatch(AuditBatchRequestDto request);
/**
* 查询审核结果
*/
@@ -1,8 +1,8 @@
package com.crrc.crrcmall.service.impl;
import com.crrc.crrcmall.config.RuleConfigProvider;
import com.crrc.crrcmall.common.mock.AuditMockDataGenerator;
import com.crrc.crrcmall.dto.audit.AuditCheckRequestDto;
import com.crrc.crrcmall.dto.audit.AuditContext;
import com.crrc.crrcmall.dto.audit.CustomItemRequestDto;
import com.crrc.crrcmall.service.AuditRuleService;
import com.crrc.crrcmall.vo.audit.AuditDetailResponseVo;
import lombok.extern.slf4j.Slf4j;
@@ -16,278 +16,125 @@ import java.util.regex.Pattern;
/**
* 审核规则引擎实现类
* 负责执行 13 项具体的审核规则校验
*/
@Slf4j
@Service
public class AuditRuleServiceImpl implements AuditRuleService {
@Autowired
private RuleConfigProvider ruleConfigProvider;
private AuditMockDataGenerator mockDataGenerator;
@Override
public AuditDetailResponseVo executeAllRules(AuditContext context, List<CustomItemRequestDto> rawCustomList) {
public AuditDetailResponseVo executeAllRules(AuditCheckRequestDto request) {
// 1. 从 Mock 生成器获取商品数据 (模拟从数据库读取)
AuditContext context = mockDataGenerator.getGoodsContext(request.getAuditId());
AuditDetailResponseVo response = new AuditDetailResponseVo();
// TODO: 实际应生成 UUID
response.setType(request.getType());
response.setTaskId("TASK-MOCK-" + System.currentTimeMillis());
response.setGoodsId(context.getGoodsId());
// 初始状态:假设全部通过,后续根据校验结果更新
response.setStatus("SUCCESSED");
response.setStatus(1); // 1: Completed
List<AuditDetailResponseVo.CustomItemResponseVo> results = new ArrayList<>();
List<AuditDetailResponseVo.AuditGroupVo> groupList = new ArrayList<>();
AuditDetailResponseVo.AuditGroupVo group = new AuditDetailResponseVo.AuditGroupVo();
group.setAuditId(request.getAuditId());
group.setGoodsId(request.getGoodsId());
// 1. 校验商品名称 (规则 1) - AI
results.add(checkName(context));
List<AuditDetailResponseVo.AuditDetailItemVo> detailList = new ArrayList<>();
// 2. 校验商品条码 (规则 2) - 硬规则
results.add(checkBarcode(context));
// 2. 执行硬规则校验 (基于获取到的 Context 数据)
detailList.add(buildItem("barcode", "商品条码", context.getBarcode(), checkBarcode(context)));
detailList.add(buildItem("taxCode", "税码", context.getTaxCode(), checkTaxCode(context)));
detailList.add(buildItem("materialCode", "物料编码", context.getMaterialCode(), checkMaterialCode(context)));
detailList.add(buildItem("shippingFee", "运费", context.getShippingFee(), checkShipping(context)));
detailList.add(buildItem("deliveryArea", "配送区域", context.getDeliveryArea(), checkDeliveryArea(context)));
// 3. 校验品牌项 (规则 3) - AI
results.add(checkBrand(context));
// 3. 执行 AI 规则模拟 (使用 Mock 生成器生成逼真的 AI 结果)
detailList.add(buildAiItem("goodsName", "商品名称", context.getGoodsName()));
detailList.add(buildAiItem("brandName", "品牌项", context.getBrandName()));
detailList.add(buildAiItem("imageUrls", "商品图片", context.getImageUrls()));
detailList.add(buildAiItem("description", "详情描述", context.getDescription()));
// 4. 校验税码 (规则 4) - 硬规则
results.add(checkTaxCode(context));
// 5. 校验物料编码 (规则 5) - 硬规则
results.add(checkMaterialCode(context));
// 6. 校验商品目录 (规则 6) - AI
results.add(checkCatalog(context));
// 7. 校验商品图片 (规则 7) - AI/Vision
results.add(checkImages(context));
// 8. 校验市场价格链接 (规则 8) - 爬虫/AI
results.add(checkMarketUrl(context));
// 9. 校验电脑端详情描述 (规则 9) - AI
results.add(checkDescription(context));
// 10. 校验京东价格链接 (规则 10) - 爬虫
results.add(checkJdUrl(context));
// 11. 校验价格 (规则 11) - 硬规则 + TODO
results.add(checkPrice(context));
// 12. 校验运费 (规则 12) - 硬规则
results.add(checkShipping(context));
// 13. 校验配送区域 (规则 13) - 硬规则
results.add(checkDeliveryArea(context));
response.setCustomList(results);
// 如果有任何一项 FAIL,则整体状态改为 FAIL
boolean hasFail = results.stream().anyMatch(r -> "FAIL".equals(r.getCheckStatus()));
if (hasFail) {
response.setStatus("FAIL");
}
group.setAuditDetailList(detailList);
groupList.add(group);
response.setAuditList(groupList);
return response;
}
/**
* 构建硬规则结果项
*/
private AuditDetailResponseVo.AuditDetailItemVo buildItem(String field, String name, Object value, RuleCheckResult result) {
AuditDetailResponseVo.AuditDetailItemVo item = new AuditDetailResponseVo.AuditDetailItemVo();
item.setCustomField(field);
item.setCustomKey(name);
item.setCustomValue(value);
item.setAiIntent(result.message);
item.setAiRelevanceScore(result.score);
item.setCheckType(1); // 1: Hard Rule
item.setAiIntentType(1);
item.setAuditStatus(result.isSuccess ? 0 : 1);
return item;
}
/**
* 规则 2:商品条码校验
* 要求:条码真实有效,与商品信息一致;非必填项可为空。禁止:填写条码以外的数字、汉字、字母等内容。
* 构建 AI 规则结果项 (模拟)
*/
private AuditDetailResponseVo.CustomItemResponseVo checkBarcode(AuditContext context) {
AuditDetailResponseVo.CustomItemResponseVo res = buildBaseVo("barcode", "商品条码");
private AuditDetailResponseVo.AuditDetailItemVo buildAiItem(String field, String name, Object value) {
AuditMockDataGenerator.AiMockResult aiResult = mockDataGenerator.getAiMockResult(field, value);
AuditDetailResponseVo.AuditDetailItemVo item = new AuditDetailResponseVo.AuditDetailItemVo();
item.setCustomField(field);
item.setCustomKey(name);
item.setCustomValue(value);
item.setAiIntent(aiResult.getMessage());
item.setAiRelevanceScore(aiResult.getScore());
item.setCheckType(2); // 2: AI Rule
item.setAiIntentType(1);
item.setAuditStatus(aiResult.isSuccess() ? 0 : 1);
return item;
}
private record RuleCheckResult(boolean isSuccess, String message, double score) {}
// ==================== 硬规则校验逻辑 ====================
private RuleCheckResult checkBarcode(AuditContext context) {
String code = context.getBarcode();
if (StringUtils.hasText(code)) {
// 校验规则:只能包含数字和字母 (正则)
// TODO: 实际业务可能需要调用第三方 API 校验条码真实性
boolean isValid = Pattern.matches("^[a-zA-Z0-9]+$", code);
if (!isValid) {
res.setCheckStatus("FAIL");
res.setAiIntent("条码包含非法字符(如汉字或特殊符号)");
} else {
res.setCheckStatus("PASS");
res.setAiIntent("格式校验通过");
}
} else {
res.setCheckStatus("PASS"); // 非必填
res.setAiIntent("条码为空,跳过校验");
if (StringUtils.hasText(code) && Pattern.matches("^[a-zA-Z0-9]+$", code)) {
return new RuleCheckResult(true, "格式校验通过", 1.0);
}
return res;
return new RuleCheckResult(false, "条码格式错误 (含非法字符)", 0.0);
}
/**
* 规则 4:税码校验
* 要求:税码分类正确,且不得为空。
*/
private AuditDetailResponseVo.CustomItemResponseVo checkTaxCode(AuditContext context) {
AuditDetailResponseVo.CustomItemResponseVo res = buildBaseVo("taxCode", "税码");
if (!StringUtils.hasText(context.getTaxCode())) {
res.setCheckStatus("FAIL");
res.setAiIntent("税码不能为空");
} else {
res.setCheckStatus("PASS");
res.setAiIntent("税码已填写");
private RuleCheckResult checkTaxCode(AuditContext context) {
if (StringUtils.hasText(context.getTaxCode())) {
return new RuleCheckResult(true, "税码已填写", 1.0);
}
return res;
return new RuleCheckResult(false, "税码不能为空", 0.0);
}
/**
* 规则 5:物料编码校验
* 要求:工业品以 DM 开头,办公用品以 DB 开头。
*/
private AuditDetailResponseVo.CustomItemResponseVo checkMaterialCode(AuditContext context) {
AuditDetailResponseVo.CustomItemResponseVo res = buildBaseVo("materialCode", "物料编码");
private RuleCheckResult checkMaterialCode(AuditContext context) {
String code = context.getMaterialCode();
String prefix = "OFFICE".equalsIgnoreCase(context.getProductType()) ? "DB" : "DM";
if (!StringUtils.hasText(code)) {
res.setCheckStatus("FAIL");
res.setAiIntent("物料编码不能为空");
return res;
if (StringUtils.hasText(code) && code.startsWith(prefix)) {
return new RuleCheckResult(true, "物料编码前缀正确 (" + prefix + ")", 1.0);
}
return new RuleCheckResult(false, "物料编码前缀错误 (期望 " + prefix + " 开头)", 0.0);
}
// 获取当前商品类别的期望前缀 (从配置读取)
String expectedPrefix = ruleConfigProvider.getMaterialCodePrefix(context.getProductType());
if (code.startsWith(expectedPrefix)) {
res.setCheckStatus("PASS");
res.setAiIntent("物料编码前缀正确 (" + expectedPrefix + ")");
} else {
res.setCheckStatus("FAIL");
res.setAiIntent("物料编码前缀错误。当前类目 [" + context.getProductType() + "] 期望以 [" + expectedPrefix + "] 开头");
private RuleCheckResult checkShipping(AuditContext context) {
if ("全国包邮".equals(context.getShippingFee())) {
return new RuleCheckResult(true, "运费符合标准", 1.0);
}
return res;
return new RuleCheckResult(false, "运费不符合要求", 0.0);
}
/**
* 规则 12:运费校验
* 要求:全国包邮
*/
private AuditDetailResponseVo.CustomItemResponseVo checkShipping(AuditContext context) {
AuditDetailResponseVo.CustomItemResponseVo res = buildBaseVo("shippingFee", "运费");
String required = ruleConfigProvider.getRequiredShippingFee();
if (required.equals(context.getShippingFee())) {
res.setCheckStatus("PASS");
res.setAiIntent("运费配置符合标准");
} else {
res.setCheckStatus("FAIL");
res.setAiIntent("运费不符合要求,应为 [" + required + "]");
private RuleCheckResult checkDeliveryArea(AuditContext context) {
if ("不限配送区域".equals(context.getDeliveryArea())) {
return new RuleCheckResult(true, "配送区域符合标准", 1.0);
}
return res;
}
/**
* 规则 13:配送区域校验
* 要求:不限配送区域
*/
private AuditDetailResponseVo.CustomItemResponseVo checkDeliveryArea(AuditContext context) {
AuditDetailResponseVo.CustomItemResponseVo res = buildBaseVo("deliveryArea", "配送区域");
String required = ruleConfigProvider.getRequiredDeliveryArea();
if (required.equals(context.getDeliveryArea())) {
res.setCheckStatus("PASS");
res.setAiIntent("配送区域符合标准");
} else {
res.setCheckStatus("FAIL");
res.setAiIntent("配送区域不符合要求,应为 [" + required + "]");
}
return res;
}
/**
* 1. 校验结构 [品牌 + 名称 + 型号 + 规格]
* 2. 校验违禁词 (从 RuleConfigProvider 获取配置)
*/
private AuditDetailResponseVo.CustomItemResponseVo checkName(AuditContext context) {
AuditDetailResponseVo.CustomItemResponseVo res = buildBaseVo("goodsName", "商品名称");
// TODO: 1. 校验结构 [品牌 + 名称 + 型号 + 规格]
// TODO: 2. 校验违禁词 (从 RuleConfigProvider 获取配置)
res.setCheckStatus("WAITING");
res.setAiIntent("待 AI 语义分析");
return res;
}
/**
* 校验品牌项与商品名称中的品牌是否一致
*/
private AuditDetailResponseVo.CustomItemResponseVo checkBrand(AuditContext context) {
AuditDetailResponseVo.CustomItemResponseVo res = buildBaseVo("brandName", "品牌项");
// TODO: 校验品牌项与商品名称中的品牌是否一致
res.setCheckStatus("WAITING");
res.setAiIntent("待 AI 比对");
return res;
}
/**
* 校验类目归属合理性,防跨类
*/
private AuditDetailResponseVo.CustomItemResponseVo checkCatalog(AuditContext context) {
AuditDetailResponseVo.CustomItemResponseVo res = buildBaseVo("catalogId", "商品目录");
// TODO: 校验类目归属合理性,防跨类
res.setCheckStatus("WAITING");
res.setAiIntent("待 AI 类目映射");
return res;
}
/**
* 校验商品图片清晰度、无二维码、无违规 Logo
*/
private AuditDetailResponseVo.CustomItemResponseVo checkImages(AuditContext context) {
AuditDetailResponseVo.CustomItemResponseVo res = buildBaseVo("imageUrls", "商品图片");
// TODO: 调用 Qwen-VL 校验图片清晰度、无二维码、无违规 Logo
res.setCheckStatus("WAITING");
res.setAiIntent("待视觉模型分析");
return res;
}
/**
* 校验链接有效性,是否为官网
*/
private AuditDetailResponseVo.CustomItemResponseVo checkMarketUrl(AuditContext context) {
AuditDetailResponseVo.CustomItemResponseVo res = buildBaseVo("marketPriceUrl", "市场价格链接");
// TODO: 校验链接有效性,是否为官网
res.setCheckStatus("WAITING");
res.setAiIntent("待爬虫校验");
return res;
}
/**
* 校验描述真实性、无夸大、无违禁词
*/
private AuditDetailResponseVo.CustomItemResponseVo checkDescription(AuditContext context) {
AuditDetailResponseVo.CustomItemResponseVo res = buildBaseVo("description", "电脑端详情描述");
// TODO: 校验描述真实性、无夸大、无违禁词
res.setCheckStatus("WAITING");
res.setAiIntent("待 AI 文本分析");
return res;
}
/**
* 校验是否为京东自营链接,是否有效
*/
private AuditDetailResponseVo.CustomItemResponseVo checkJdUrl(AuditContext context) {
AuditDetailResponseVo.CustomItemResponseVo res = buildBaseVo("jdPriceUrl", "京东价格链接");
// TODO: 校验是否为京东自营链接,是否有效
res.setCheckStatus("WAITING");
res.setAiIntent("待京东链接校验");
return res;
}
/**
* 校验 价格 <= 入围价格
*/
private AuditDetailResponseVo.CustomItemResponseVo checkPrice(AuditContext context) {
AuditDetailResponseVo.CustomItemResponseVo res = buildBaseVo("price", "价格 [单价]");
// TODO: 校验 价格 <= 入围价格
// TODO: 获取 context.getTargetPrice() 进行比对
res.setCheckStatus("WAITING");
res.setAiIntent("待入围价格数据接入后校验");
return res;
}
private AuditDetailResponseVo.CustomItemResponseVo buildBaseVo(String field, String name) {
AuditDetailResponseVo.CustomItemResponseVo vo = new AuditDetailResponseVo.CustomItemResponseVo();
vo.setCustomField(field);
vo.setCustomKey(name);
vo.setCheckStatus("PASS"); // 默认通过,失败时在具体方法中修改
return vo;
return new RuleCheckResult(false, "配送区域不符合要求", 0.0);
}
}
@@ -1,9 +1,8 @@
package com.crrc.crrcmall.service.impl;
import com.crrc.crrcmall.dto.audit.AuditBatchRequestDto;
import com.crrc.crrcmall.dto.audit.AuditCheckRequestDto;
import com.crrc.crrcmall.dto.audit.AuditContext;
import com.crrc.crrcmall.dto.audit.AuditSearchRequestDto;
import com.crrc.crrcmall.dto.audit.CustomItemRequestDto;
import com.crrc.crrcmall.entity.AuditTask;
import com.crrc.crrcmall.service.AuditRuleService;
import com.crrc.crrcmall.service.AuditService;
@@ -14,7 +13,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -39,10 +37,7 @@ public class AuditServiceImpl implements AuditService {
public AuditCheckResponseVo submitAuditCheck(AuditCheckRequestDto request) {
String taskId = UUID.randomUUID().toString().replace("-", "");
// 1. 数据转换:将 customList 转换为结构化 Context
AuditContext context = convertToContext(request);
// 2. 创建任务记录 (模拟 INSERT INTO audit_task)
// 1. 创建任务记录
AuditTask task = new AuditTask();
task.setTaskId(taskId);
task.setGoodsId(request.getGoodsId());
@@ -51,21 +46,20 @@ public class AuditServiceImpl implements AuditService {
task.setCreateTime(LocalDateTime.now());
TASK_DB.put(taskId, task);
// 3. 异步执行审核
// 2. 异步执行审核
CompletableFuture.runAsync(() -> {
try {
log.info("开始异步审核任务: {}", taskId);
// 调用规则引擎
AuditDetailResponseVo result = auditRuleService.executeAllRules(context, request.getCustomList());
result.setTaskId(taskId); // 绑定 TaskId
log.info("开始异步审核任务: {}, Type: {}, AuditId: {}", taskId, request.getType(), request.getAuditId());
// 调用规则引擎 (内部会使用 MockDataGenerator 生成数据)
AuditDetailResponseVo result = auditRuleService.executeAllRules(request);
// 保存结果
RESULT_DB.put(taskId, result);
// 更新任务状态
task.setStatus("COMPLETED");
task.setCompleteTime(LocalDateTime.now());
// 保存结果 (模拟 INSERT INTO audit_result_detail)
RESULT_DB.put(taskId, result);
// TODO: 触发 Callback 通知客户
// httpClient.post(request.getCallbackUrl(), result);
@@ -75,7 +69,44 @@ public class AuditServiceImpl implements AuditService {
}
});
// 4. 立即返回
// 3. 立即返回
AuditCheckResponseVo response = new AuditCheckResponseVo();
response.setTaskId(taskId);
response.setStatus("ACCEPTED");
return response;
}
@Override
public AuditCheckResponseVo submitAuditCheckBatch(AuditBatchRequestDto request) {
String taskId = UUID.randomUUID().toString().replace("-", "");
AuditTask task = new AuditTask();
task.setTaskId(taskId);
task.setStatus("PROCESSING");
task.setCreateTime(LocalDateTime.now());
TASK_DB.put(taskId, task);
CompletableFuture.runAsync(() -> {
try {
log.info("开始批量异步审核任务: {}, Count: {}", taskId, request.getAuditInfo().size());
for (AuditBatchRequestDto.AuditInfo info : request.getAuditInfo()) {
AuditCheckRequestDto singleReq = new AuditCheckRequestDto();
singleReq.setGoodsId(info.getGoodsId());
singleReq.setAuditId(info.getAuditId());
singleReq.setType(info.getType());
singleReq.setCallbackUrl(request.getCallbackUrl());
AuditDetailResponseVo result = auditRuleService.executeAllRules(singleReq);
RESULT_DB.put(taskId + "_" + info.getAuditId(), result);
}
task.setStatus("COMPLETED");
task.setCompleteTime(LocalDateTime.now());
} catch (Exception e) {
log.error("批量审核任务执行失败: {}", taskId, e);
task.setStatus("FAILED");
}
});
AuditCheckResponseVo response = new AuditCheckResponseVo();
response.setTaskId(taskId);
response.setStatus("ACCEPTED");
@@ -85,50 +116,6 @@ public class AuditServiceImpl implements AuditService {
@Override
public AuditDetailResponseVo getAuditResult(AuditSearchRequestDto request) {
// 从模拟 DB 中查询
return RESULT_DB.getOrDefault(String.valueOf(request.getTaskId()), null);
}
/**
* 辅助方法:将 Key-Value 列表转换为结构化对象
*/
private AuditContext convertToContext(AuditCheckRequestDto request) {
AuditContext context = new AuditContext();
context.setGoodsId(request.getGoodsId());
context.setCallbackUrl(request.getCallbackUrl());
if (request.getCustomList() != null) {
for (CustomItemRequestDto item : request.getCustomList()) {
String key = item.getCustomField();
Object value = item.getCustomValue();
// 简单的映射逻辑 (实际可根据 customType 进一步细分)
switch (key) {
case "goodsName": context.setGoodsName(String.valueOf(value)); break;
case "barcode": context.setBarcode(String.valueOf(value)); break;
case "brandName": context.setBrandName(String.valueOf(value)); break;
case "taxCode": context.setTaxCode(String.valueOf(value)); break;
case "materialCode": context.setMaterialCode(String.valueOf(value)); break;
case "catalogId":
if (value instanceof Number) context.setCatalogId(((Number) value).longValue());
break;
case "imageUrls":
// 假设传入的是逗号分隔的字符串或 List
if (value instanceof List) context.setImageUrls((List<String>) value);
else if (value instanceof String) context.setImageUrls(List.of((String) value));
break;
case "description": context.setDescription(String.valueOf(value)); break;
case "marketPriceUrl": context.setMarketPriceUrl(String.valueOf(value)); break;
case "jdPriceUrl": context.setJdPriceUrl(String.valueOf(value)); break;
case "price":
if (value instanceof Number) context.setPrice(((Number) value).doubleValue());
break;
case "shippingFee": context.setShippingFee(String.valueOf(value)); break;
case "deliveryArea": context.setDeliveryArea(String.valueOf(value)); break;
case "productType": context.setProductType(String.valueOf(value)); break;
// TODO: 其他字段映射
}
}
}
return context;
return RESULT_DB.getOrDefault(request.getTaskId(), null);
}
}
@@ -1,41 +1,33 @@
package com.crrc.crrcmall.service.impl;
import com.crrc.crrcmall.common.mock.SearchMockDataGenerator;
import com.crrc.crrcmall.dto.crrc.SearchGoodsRequestDto;
import com.crrc.crrcmall.service.SearchService;
import com.crrc.crrcmall.vo.crrc.GoodsSearchResultVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
/**
* 搜索服务实现类
* 搜索服务实现类 (Demo 模式)
* 目前通过 Mock 数据生成器返回搜索结果,方便演示 AI 搜索效果
*/
@Slf4j
@Service
public class SearchServiceImpl implements SearchService {
@Autowired
private SearchMockDataGenerator mockDataGenerator;
@Override
public GoodsSearchResultVo searchByText(SearchGoodsRequestDto request) {
// TODO: 1. 调用 AI 意图识别模块
// TODO: 2. 调用 ES 混合检索
// TODO: 3. 组装数据
// 目前返回空数据结构
GoodsSearchResultVo result = new GoodsSearchResultVo();
result.setGoodsList(new ArrayList<>());
result.setBrandList(new ArrayList<>());
result.setCatalogList(new ArrayList<>());
return result;
log.info("执行 AI 文本搜索,关键词:{}", request.getGoodsName());
// 调用 Mock 生成器返回演示数据
return mockDataGenerator.generateResult(request);
}
@Override
public GoodsSearchResultVo searchByImage(SearchGoodsRequestDto request) {
// TODO: 1. 图片向量化
// TODO: 2. 调用 ES 混合检索
// TODO: 3. 组装数据
return new GoodsSearchResultVo();
return mockDataGenerator.generateResult(request);
}
}
@@ -1,9 +1,5 @@
package com.crrc.crrcmall.vo.ai;
/**
* @Author:Jinyun.Wang
* @Date:2026/11/2
**/
import com.crrc.crrcmall.common.enums.MatchTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -7,7 +7,7 @@ import java.io.Serializable;
import java.util.List;
/**
* 审核详情响应 VO
* 审核详情响应 VO (v2 - 嵌套结构)
*/
@Data
@Schema(description = "审核详情响应")
@@ -15,22 +15,32 @@ public class AuditDetailResponseVo implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "任务 ID")
@Schema(description = "审核类型: 1-新品审核 2-新品复审 3-调价审核")
private Integer type;
@Schema(description = "审核任务 ID")
private String taskId;
@Schema(description = "商品 ID")
private String goodsId;
@Schema(description = "状态: 1 已完成 2 未检测")
private Integer status;
@Schema(description = "状态: SUCCESSED/WAITING")
private String status;
@Schema(description = "审核明细列表")
private List<CustomItemResponseVo> customList;
@Schema(description = "审核分组列表 (按 auditId 分组)")
private List<AuditGroupVo> auditList;
@Data
public static class CustomItemResponseVo implements Serializable {
@Schema(description = "类型")
private String type;
public static class AuditGroupVo implements Serializable {
@Schema(description = "商品 ID")
private String goodsId;
@Schema(description = "审核 ID")
private String auditId;
@Schema(description = "审核明细列表")
private List<AuditDetailItemVo> auditDetailList;
}
@Data
public static class AuditDetailItemVo implements Serializable {
@Schema(description = "字段标识")
private String customField;
@Schema(description = "字段显示名")
@@ -41,13 +51,13 @@ public class AuditDetailResponseVo implements Serializable {
private Object customType;
@Schema(description = "AI 反馈")
private String aiIntent;
@Schema(description = "AI 反馈类型")
@Schema(description = "AI 反馈类型: 1-格式 2-逻辑 3-图片")
private Integer aiIntentType;
@Schema(description = "AI 匹配度")
private Double aiRelevanceScore;
@Schema(description = "规则类型")
@Schema(description = "规则类型: 1-硬规则 2-AI 规则")
private Integer checkType;
@Schema(description = "规则类型")
private String checkStatus;
@Schema(description = "检测状态: 0 成功 1 失败")
private Integer auditStatus;
}
}
@@ -46,4 +46,7 @@ public class GoodsSearchResultVo implements Serializable {
@Schema(description = "ai规则说明")
private AiSearchInfoVo aiSearchInfo;
@Schema(description = "条数")
private int total;
}