feat:增加部分审核规则验证
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
package com.crrc.crrcmall.config;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 规则配置提供者
|
||||
* 模拟从数据库读取配置项。后续只需修改方法体,注入 Mapper 即可无缝对接数据库。
|
||||
*/
|
||||
@Component
|
||||
public class RuleConfigProvider {
|
||||
|
||||
/**
|
||||
* 获取物料编码前缀配置
|
||||
* @param category 商品类目类型 (INDUSTRIAL/OFFICE)
|
||||
* @return 期望的前缀 (工业品:DM, 办公用品:DB)
|
||||
*/
|
||||
public String getMaterialCodePrefix(String category) {
|
||||
// TODO: 替换为从数据库 audit_rule_config 表查询
|
||||
if ("INDUSTRIAL".equalsIgnoreCase(category)) {
|
||||
return "DM";
|
||||
} else {
|
||||
return "DB"; // 默认办公用品
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取违禁词列表 (规则 1, 8, 9)
|
||||
*/
|
||||
public List<String> getForbiddenWords() {
|
||||
// TODO: 替换为从数据库查询
|
||||
return Arrays.asList("国产", "中车", "联系", "咨询", "详询客服销售", "电话", "微信号");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运费规则 (规则 12)
|
||||
*/
|
||||
public String getRequiredShippingFee() {
|
||||
return "全国包邮";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配送区域规则 (规则 13)
|
||||
*/
|
||||
public String getRequiredDeliveryArea() {
|
||||
return "不限配送区域";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.crrc.crrcmall.controller;
|
||||
|
||||
import com.crrc.crrcmall.dto.audit.AuditCheckRequestDto;
|
||||
import com.crrc.crrcmall.dto.audit.AuditSearchRequestDto;
|
||||
import com.crrc.crrcmall.service.AuditService;
|
||||
import com.crrc.crrcmall.vo.audit.AuditCheckResponseVo;
|
||||
import com.crrc.crrcmall.vo.audit.AuditDetailResponseVo;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* AI 智能审核控制器
|
||||
*/
|
||||
@Tag(name = "AI 审核服务")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/audit")
|
||||
public class AuditController {
|
||||
|
||||
private final AuditService auditService;
|
||||
|
||||
public AuditController(AuditService auditService) {
|
||||
this.auditService = auditService;
|
||||
}
|
||||
|
||||
@Operation(summary = "提交审核任务")
|
||||
@PostMapping("/check")
|
||||
public AuditCheckResponseVo checkAudit(@RequestBody AuditCheckRequestDto request) {
|
||||
return auditService.submitAuditCheck(request);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询审核结果")
|
||||
@GetMapping("/search")
|
||||
public AuditDetailResponseVo searchAuditResult(@ModelAttribute AuditSearchRequestDto request) {
|
||||
return auditService.getAuditResult(request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.crrc.crrcmall.controller;
|
||||
|
||||
import com.crrc.crrcmall.dto.price.PriceResultRequestDto;
|
||||
import com.crrc.crrcmall.service.PriceService;
|
||||
import com.crrc.crrcmall.vo.price.PriceImportResponseVo;
|
||||
import com.crrc.crrcmall.vo.price.PriceResultResponseVo;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 选品比价控制器
|
||||
*/
|
||||
@Tag(name = "选品比价服务")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/price")
|
||||
public class PriceController {
|
||||
|
||||
private final PriceService priceService;
|
||||
|
||||
public PriceController(PriceService priceService) {
|
||||
this.priceService = priceService;
|
||||
}
|
||||
|
||||
@Operation(summary = "导入比价数据")
|
||||
@PostMapping("/import")
|
||||
public PriceImportResponseVo importPriceData(@RequestParam("file") MultipartFile file) {
|
||||
return priceService.importPriceExcel(file);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询比价结果")
|
||||
@GetMapping("/result")
|
||||
public PriceResultResponseVo getPriceResult(@ModelAttribute PriceResultRequestDto request) {
|
||||
return priceService.getPriceResult(request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* AI 智能审核提交请求 DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "提交审核任务请求")
|
||||
public class AuditCheckRequestDto implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "商品 ID")
|
||||
private String goodsId;
|
||||
|
||||
@Schema(description = "回调地址 (审核完成后通知客户)")
|
||||
private String callbackUrl;
|
||||
|
||||
@Schema(description = "自定义审核项列表")
|
||||
private List<CustomItemRequestDto> customList;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.crrc.crrcmall.dto.audit;
|
||||
|
||||
import lombok.Data;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审核上下文 (内部转换对象)
|
||||
* 用于将杂乱的 CustomItemRequestDto 列表转换为结构化数据,方便规则校验
|
||||
*/
|
||||
@Data
|
||||
public class AuditContext {
|
||||
|
||||
private String goodsId;
|
||||
private String callbackUrl;
|
||||
|
||||
// === 核心校验字段 (对应 13 项审核规则) ===
|
||||
|
||||
// 1. 商品名称
|
||||
private String goodsName;
|
||||
|
||||
// 2. 商品条码
|
||||
private String barcode;
|
||||
|
||||
// 3. 品牌项
|
||||
private String brandName;
|
||||
|
||||
// 4. 税码
|
||||
private String taxCode;
|
||||
|
||||
// 5. 物料编码
|
||||
private String materialCode;
|
||||
|
||||
// 6. 商品目录 (类目)
|
||||
private Long catalogId;
|
||||
private String catalogName;
|
||||
|
||||
// 7. 商品图片 (URL 列表)
|
||||
private List<String> imageUrls;
|
||||
|
||||
// 8. 电脑端详情描述
|
||||
private String description;
|
||||
|
||||
// 9. 市场价格链接 (官网)
|
||||
private String marketPriceUrl;
|
||||
|
||||
// 10. 京东价格链接
|
||||
private String jdPriceUrl;
|
||||
|
||||
// 11. 价格 [单价(含税)]
|
||||
private Double price;
|
||||
|
||||
// 12. 运费
|
||||
private String shippingFee;
|
||||
|
||||
// 13. 配送区域
|
||||
private String deliveryArea;
|
||||
|
||||
// === 扩展字段 (用于辅助校验) ===
|
||||
|
||||
// 商品类型标识 (用于区分工业品/办公用品)
|
||||
private String productType;
|
||||
|
||||
// TODO: 入围价格 (待明确字段来源)
|
||||
private Double targetPrice;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.crrc.crrcmall.dto.audit;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 审核结果查询请求 DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "审核结果查询请求")
|
||||
public class AuditSearchRequestDto implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "审核任务 ID")
|
||||
private Long taskId;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.crrc.crrcmall.dto.audit;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 审核明细项 DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "审核明细项")
|
||||
public class CustomItemRequestDto implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "类型: JD/CRRC")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "字段标识 (如 goodsName, goodsPrice)")
|
||||
private String customField;
|
||||
|
||||
@Schema(description = "字段显示名")
|
||||
private String customKey;
|
||||
|
||||
@Schema(description = "本次申报值")
|
||||
private Object customValue;
|
||||
|
||||
@Schema(description = "所属分组 (如 price, saleInfo)")
|
||||
private Object customType;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.crrc.crrcmall.dto.price;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 比价结果查询请求 DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "比价结果查询请求")
|
||||
public class PriceResultRequestDto implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "任务 ID")
|
||||
private Long taskId;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.crrc.crrcmall.entity;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 审核结果详情实体 (模拟数据库表:audit_result_detail)
|
||||
* 用于记录 13 项规则中每一项的具体审核结果和 AI 反馈
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "审核结果详情")
|
||||
public class AuditResultDetail implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "主键 ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "关联的任务 ID")
|
||||
private String taskId;
|
||||
|
||||
@Schema(description = "关联的字段标识 (如 goodsName, goodsPrice)")
|
||||
private String customField;
|
||||
|
||||
@Schema(description = "规则编码")
|
||||
private String ruleCode;
|
||||
|
||||
@Schema(description = "规则名称")
|
||||
private String ruleName;
|
||||
|
||||
@Schema(description = "审核结果: PASS-通过, FAIL-不通过, WARN-警告")
|
||||
private String checkStatus;
|
||||
|
||||
@Schema(description = "AI 反馈/判定依据")
|
||||
private String aiIntent;
|
||||
|
||||
@Schema(description = "AI 匹配度/置信度 (0.0 - 1.0)")
|
||||
private Double aiRelevanceScore;
|
||||
|
||||
@Schema(description = "AI 反馈类型: 1-格式校验 2-语义/逻辑校验 3-图片内容校验")
|
||||
private Integer aiIntentType;
|
||||
|
||||
@Schema(description = "规则类型: 1-硬规则 2-AI 规则 3-爬虫/外部校验")
|
||||
private Integer checkType;
|
||||
|
||||
@Schema(description = "错误/警告详细信息")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.crrc.crrcmall.entity;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 审核规则配置实体 (模拟数据库表:audit_rule_config)
|
||||
* 用于统一管理 13 项审核规则的配置项(如物料前缀、违禁词、阈值等)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "审核规则配置")
|
||||
public class AuditRuleConfig implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "主键 ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "规则编码 (唯一标识)")
|
||||
private String ruleCode;
|
||||
|
||||
@Schema(description = "规则名称")
|
||||
private String ruleName;
|
||||
|
||||
@Schema(description = "配置内容 (JSON 格式或字符串)")
|
||||
private String configValue;
|
||||
|
||||
@Schema(description = "是否启用: 1-启用 0-禁用")
|
||||
private Integer isActive;
|
||||
|
||||
@Schema(description = "规则描述")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.crrc.crrcmall.entity;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 审核任务实体 (模拟数据库表:audit_task)
|
||||
* 用于记录每一次审核请求的元数据和状态
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "审核任务")
|
||||
public class AuditTask implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "主键 ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "任务 ID (UUID)")
|
||||
private String taskId;
|
||||
|
||||
@Schema(description = "商品 ID")
|
||||
private String goodsId;
|
||||
|
||||
@Schema(description = "回调地址")
|
||||
private String callbackUrl;
|
||||
|
||||
@Schema(description = "审核状态: PENDING-待处理, PROCESSING-审核中, COMPLETED-已完成, FAILED-失败")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "完成时间")
|
||||
private LocalDateTime completeTime;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.crrc.crrcmall.service;
|
||||
|
||||
import com.crrc.crrcmall.dto.audit.AuditContext;
|
||||
import com.crrc.crrcmall.dto.audit.CustomItemRequestDto;
|
||||
import com.crrc.crrcmall.vo.audit.AuditDetailResponseVo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审核规则引擎服务
|
||||
* 负责执行 13 项具体的审核规则校验
|
||||
*/
|
||||
public interface AuditRuleService {
|
||||
|
||||
/**
|
||||
* 执行全套审核规则 (13 项)
|
||||
* @param context 审核上下文数据
|
||||
* @param rawCustomList 原始请求列表 (用于回填)
|
||||
* @return 包含所有校验项结果的 VO
|
||||
*/
|
||||
AuditDetailResponseVo executeAllRules(AuditContext context, List<CustomItemRequestDto> rawCustomList);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.crrc.crrcmall.service;
|
||||
|
||||
import com.crrc.crrcmall.dto.audit.AuditCheckRequestDto;
|
||||
import com.crrc.crrcmall.dto.audit.AuditSearchRequestDto;
|
||||
import com.crrc.crrcmall.vo.audit.AuditCheckResponseVo;
|
||||
import com.crrc.crrcmall.vo.audit.AuditDetailResponseVo;
|
||||
|
||||
/**
|
||||
* 审核服务接口
|
||||
*/
|
||||
public interface AuditService {
|
||||
|
||||
/**
|
||||
* 提交审核任务
|
||||
*/
|
||||
AuditCheckResponseVo submitAuditCheck(AuditCheckRequestDto request);
|
||||
|
||||
/**
|
||||
* 查询审核结果
|
||||
*/
|
||||
AuditDetailResponseVo getAuditResult(AuditSearchRequestDto request);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.crrc.crrcmall.service;
|
||||
|
||||
import com.crrc.crrcmall.dto.price.PriceResultRequestDto;
|
||||
import com.crrc.crrcmall.vo.price.PriceImportResponseVo;
|
||||
import com.crrc.crrcmall.vo.price.PriceResultResponseVo;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 比价服务接口
|
||||
*/
|
||||
public interface PriceService {
|
||||
|
||||
/**
|
||||
* 导入比价 Excel
|
||||
*/
|
||||
PriceImportResponseVo importPriceExcel(MultipartFile file);
|
||||
|
||||
/**
|
||||
* 查询比价结果
|
||||
*/
|
||||
PriceResultResponseVo getPriceResult(PriceResultRequestDto request);
|
||||
}
|
||||
@@ -9,9 +9,16 @@ import com.crrc.crrcmall.vo.crrc.GoodsSearchResultVo;
|
||||
public interface SearchService {
|
||||
|
||||
/**
|
||||
* 文本语义搜索
|
||||
* 文本语义/向量搜索
|
||||
* @param request 搜索请求参数
|
||||
* @return 搜索结果
|
||||
*/
|
||||
GoodsSearchResultVo searchByText(SearchGoodsRequestDto request);
|
||||
|
||||
/**
|
||||
* 图片向量搜索
|
||||
* @param request 搜索请求参数
|
||||
* @return 搜索结果
|
||||
*/
|
||||
GoodsSearchResultVo searchByImage(SearchGoodsRequestDto request);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
package com.crrc.crrcmall.service.impl;
|
||||
|
||||
import com.crrc.crrcmall.config.RuleConfigProvider;
|
||||
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;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 审核规则引擎实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AuditRuleServiceImpl implements AuditRuleService {
|
||||
|
||||
@Autowired
|
||||
private RuleConfigProvider ruleConfigProvider;
|
||||
|
||||
@Override
|
||||
public AuditDetailResponseVo executeAllRules(AuditContext context, List<CustomItemRequestDto> rawCustomList) {
|
||||
AuditDetailResponseVo response = new AuditDetailResponseVo();
|
||||
// TODO: 实际应生成 UUID
|
||||
response.setTaskId("TASK-MOCK-" + System.currentTimeMillis());
|
||||
response.setGoodsId(context.getGoodsId());
|
||||
|
||||
// 初始状态:假设全部通过,后续根据校验结果更新
|
||||
response.setStatus("SUCCESSED");
|
||||
|
||||
List<AuditDetailResponseVo.CustomItemResponseVo> results = new ArrayList<>();
|
||||
|
||||
// 1. 校验商品名称 (规则 1) - AI
|
||||
results.add(checkName(context));
|
||||
|
||||
// 2. 校验商品条码 (规则 2) - 硬规则
|
||||
results.add(checkBarcode(context));
|
||||
|
||||
// 3. 校验品牌项 (规则 3) - AI
|
||||
results.add(checkBrand(context));
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 规则 2:商品条码校验
|
||||
* 要求:条码真实有效,与商品信息一致;非必填项可为空。禁止:填写条码以外的数字、汉字、字母等内容。
|
||||
*/
|
||||
private AuditDetailResponseVo.CustomItemResponseVo checkBarcode(AuditContext context) {
|
||||
AuditDetailResponseVo.CustomItemResponseVo res = buildBaseVo("barcode", "商品条码");
|
||||
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("条码为空,跳过校验");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规则 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("税码已填写");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规则 5:物料编码校验
|
||||
* 要求:工业品以 DM 开头,办公用品以 DB 开头。
|
||||
*/
|
||||
private AuditDetailResponseVo.CustomItemResponseVo checkMaterialCode(AuditContext context) {
|
||||
AuditDetailResponseVo.CustomItemResponseVo res = buildBaseVo("materialCode", "物料编码");
|
||||
String code = context.getMaterialCode();
|
||||
|
||||
if (!StringUtils.hasText(code)) {
|
||||
res.setCheckStatus("FAIL");
|
||||
res.setAiIntent("物料编码不能为空");
|
||||
return res;
|
||||
}
|
||||
|
||||
// 获取当前商品类别的期望前缀 (从配置读取)
|
||||
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 + "] 开头");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规则 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 + "]");
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.crrc.crrcmall.service.impl;
|
||||
|
||||
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;
|
||||
import com.crrc.crrcmall.vo.audit.AuditCheckResponseVo;
|
||||
import com.crrc.crrcmall.vo.audit.AuditDetailResponseVo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 审核服务实现 (异步 + 内存模拟 DB)
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AuditServiceImpl implements AuditService {
|
||||
|
||||
@Autowired
|
||||
private AuditRuleService auditRuleService;
|
||||
|
||||
// 模拟数据库存储 (Task)
|
||||
private static final Map<String, AuditTask> TASK_DB = new ConcurrentHashMap<>();
|
||||
// 模拟数据库存储 (Result)
|
||||
private static final Map<String, AuditDetailResponseVo> RESULT_DB = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public AuditCheckResponseVo submitAuditCheck(AuditCheckRequestDto request) {
|
||||
String taskId = UUID.randomUUID().toString().replace("-", "");
|
||||
|
||||
// 1. 数据转换:将 customList 转换为结构化 Context
|
||||
AuditContext context = convertToContext(request);
|
||||
|
||||
// 2. 创建任务记录 (模拟 INSERT INTO audit_task)
|
||||
AuditTask task = new AuditTask();
|
||||
task.setTaskId(taskId);
|
||||
task.setGoodsId(request.getGoodsId());
|
||||
task.setCallbackUrl(request.getCallbackUrl());
|
||||
task.setStatus("PROCESSING");
|
||||
task.setCreateTime(LocalDateTime.now());
|
||||
TASK_DB.put(taskId, task);
|
||||
|
||||
// 3. 异步执行审核
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
log.info("开始异步审核任务: {}", taskId);
|
||||
// 调用规则引擎
|
||||
AuditDetailResponseVo result = auditRuleService.executeAllRules(context, request.getCustomList());
|
||||
result.setTaskId(taskId); // 绑定 TaskId
|
||||
|
||||
// 更新任务状态
|
||||
task.setStatus("COMPLETED");
|
||||
task.setCompleteTime(LocalDateTime.now());
|
||||
|
||||
// 保存结果 (模拟 INSERT INTO audit_result_detail)
|
||||
RESULT_DB.put(taskId, result);
|
||||
|
||||
// TODO: 触发 Callback 通知客户
|
||||
// httpClient.post(request.getCallbackUrl(), result);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("审核任务执行失败: {}", taskId, e);
|
||||
task.setStatus("FAILED");
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 立即返回
|
||||
AuditCheckResponseVo response = new AuditCheckResponseVo();
|
||||
response.setTaskId(taskId);
|
||||
response.setStatus("ACCEPTED");
|
||||
return response;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.crrc.crrcmall.service.impl;
|
||||
|
||||
import com.crrc.crrcmall.dto.price.PriceResultRequestDto;
|
||||
import com.crrc.crrcmall.service.PriceService;
|
||||
import com.crrc.crrcmall.vo.price.PriceImportResponseVo;
|
||||
import com.crrc.crrcmall.vo.price.PriceResultResponseVo;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 比价服务实现
|
||||
*/
|
||||
@Service
|
||||
public class PriceServiceImpl implements PriceService {
|
||||
|
||||
@Override
|
||||
public PriceImportResponseVo importPriceExcel(MultipartFile file) {
|
||||
// TODO: 1. 解析 Excel 文件
|
||||
// TODO: 2. 生成 TaskID 并保存原始数据
|
||||
// TODO: 3. 触发比价分析流程
|
||||
|
||||
PriceImportResponseVo res = new PriceImportResponseVo();
|
||||
res.setTaskId(99887766L);
|
||||
res.setStatus("SUCCESSED");
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PriceResultResponseVo getPriceResult(PriceResultRequestDto request) {
|
||||
// TODO: 1. 根据 TaskID 查询比价结果
|
||||
// TODO: 2. 返回 AI 核价建议
|
||||
|
||||
PriceResultResponseVo res = new PriceResultResponseVo();
|
||||
res.setSkuCode("SKU-001");
|
||||
res.setProductName("测试商品");
|
||||
res.setAiMarketPrice(100.0);
|
||||
res.setAiSuggestion("价格合理");
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.crrc.crrcmall.service.impl;
|
||||
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.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -10,6 +11,7 @@ import java.util.ArrayList;
|
||||
/**
|
||||
* 搜索服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SearchServiceImpl implements SearchService {
|
||||
|
||||
@@ -27,4 +29,13 @@ public class SearchServiceImpl implements SearchService {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GoodsSearchResultVo searchByImage(SearchGoodsRequestDto request) {
|
||||
// TODO: 1. 图片向量化
|
||||
// TODO: 2. 调用 ES 混合检索
|
||||
// TODO: 3. 组装数据
|
||||
|
||||
return new GoodsSearchResultVo();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.crrc.crrcmall.vo.audit;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 提交审核响应 VO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "提交审核响应")
|
||||
public class AuditCheckResponseVo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "任务 ID")
|
||||
private String taskId;
|
||||
|
||||
@Schema(description = "状态")
|
||||
private String status;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.crrc.crrcmall.vo.audit;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审核详情响应 VO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "审核详情响应")
|
||||
public class AuditDetailResponseVo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "任务 ID")
|
||||
private String taskId;
|
||||
|
||||
@Schema(description = "商品 ID")
|
||||
private String goodsId;
|
||||
|
||||
@Schema(description = "状态: SUCCESSED/WAITING")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "审核明细列表")
|
||||
private List<CustomItemResponseVo> customList;
|
||||
|
||||
@Data
|
||||
public static class CustomItemResponseVo implements Serializable {
|
||||
@Schema(description = "类型")
|
||||
private String type;
|
||||
@Schema(description = "字段标识")
|
||||
private String customField;
|
||||
@Schema(description = "字段显示名")
|
||||
private String customKey;
|
||||
@Schema(description = "申报值")
|
||||
private Object customValue;
|
||||
@Schema(description = "所属分组")
|
||||
private Object customType;
|
||||
@Schema(description = "AI 反馈")
|
||||
private String aiIntent;
|
||||
@Schema(description = "AI 反馈类型")
|
||||
private Integer aiIntentType;
|
||||
@Schema(description = "AI 匹配度")
|
||||
private Double aiRelevanceScore;
|
||||
@Schema(description = "规则类型")
|
||||
private Integer checkType;
|
||||
@Schema(description = "规则类型")
|
||||
private String checkStatus;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.crrc.crrcmall.vo.price;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 导入比价响应 VO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "导入比价响应")
|
||||
public class PriceImportResponseVo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "任务 ID")
|
||||
private Long taskId;
|
||||
|
||||
@Schema(description = "状态")
|
||||
private String status;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.crrc.crrcmall.vo.price;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 比价结果响应 VO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "比价结果响应")
|
||||
public class PriceResultResponseVo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "商品编码")
|
||||
private String skuCode;
|
||||
|
||||
@Schema(description = "商品名称")
|
||||
private String productName;
|
||||
|
||||
@Schema(description = "商品封面地址")
|
||||
private String productImageUrl;
|
||||
|
||||
@Schema(description = "商品公司")
|
||||
private String productCompany;
|
||||
|
||||
@Schema(description = "供应商申报价")
|
||||
private Double supplierPrice;
|
||||
|
||||
@Schema(description = "市场均价")
|
||||
private Double aiMarketPrice;
|
||||
|
||||
@Schema(description = "偏离度")
|
||||
private Double priceDiffRate;
|
||||
|
||||
@Schema(description = "AI 核价建议")
|
||||
private String aiSuggestion;
|
||||
|
||||
@Schema(description = "风险标签")
|
||||
private String riskTag;
|
||||
}
|
||||
Reference in New Issue
Block a user