diff --git a/src/main/java/com/crrc/crrcmall/config/RuleConfigProvider.java b/src/main/java/com/crrc/crrcmall/config/RuleConfigProvider.java new file mode 100644 index 0000000..2a6578a --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/config/RuleConfigProvider.java @@ -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 getForbiddenWords() { + // TODO: 替换为从数据库查询 + return Arrays.asList("国产", "中车", "联系", "咨询", "详询客服销售", "电话", "微信号"); + } + + /** + * 获取运费规则 (规则 12) + */ + public String getRequiredShippingFee() { + return "全国包邮"; + } + + /** + * 获取配送区域规则 (规则 13) + */ + public String getRequiredDeliveryArea() { + return "不限配送区域"; + } +} diff --git a/src/main/java/com/crrc/crrcmall/controller/AuditController.java b/src/main/java/com/crrc/crrcmall/controller/AuditController.java new file mode 100644 index 0000000..ddc37a9 --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/controller/AuditController.java @@ -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); + } +} diff --git a/src/main/java/com/crrc/crrcmall/controller/PriceController.java b/src/main/java/com/crrc/crrcmall/controller/PriceController.java new file mode 100644 index 0000000..6fc75d5 --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/controller/PriceController.java @@ -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); + } +} diff --git a/src/main/java/com/crrc/crrcmall/dto/audit/AuditCheckRequestDto.java b/src/main/java/com/crrc/crrcmall/dto/audit/AuditCheckRequestDto.java new file mode 100644 index 0000000..37656c9 --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/dto/audit/AuditCheckRequestDto.java @@ -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 customList; +} diff --git a/src/main/java/com/crrc/crrcmall/dto/audit/AuditContext.java b/src/main/java/com/crrc/crrcmall/dto/audit/AuditContext.java new file mode 100644 index 0000000..b43255c --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/dto/audit/AuditContext.java @@ -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 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; +} diff --git a/src/main/java/com/crrc/crrcmall/dto/audit/AuditSearchRequestDto.java b/src/main/java/com/crrc/crrcmall/dto/audit/AuditSearchRequestDto.java new file mode 100644 index 0000000..32ed440 --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/dto/audit/AuditSearchRequestDto.java @@ -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; +} diff --git a/src/main/java/com/crrc/crrcmall/dto/audit/CustomItemRequestDto.java b/src/main/java/com/crrc/crrcmall/dto/audit/CustomItemRequestDto.java new file mode 100644 index 0000000..548e200 --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/dto/audit/CustomItemRequestDto.java @@ -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; +} diff --git a/src/main/java/com/crrc/crrcmall/dto/price/PriceResultRequestDto.java b/src/main/java/com/crrc/crrcmall/dto/price/PriceResultRequestDto.java new file mode 100644 index 0000000..10eca44 --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/dto/price/PriceResultRequestDto.java @@ -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; +} diff --git a/src/main/java/com/crrc/crrcmall/entity/AuditResultDetail.java b/src/main/java/com/crrc/crrcmall/entity/AuditResultDetail.java new file mode 100644 index 0000000..b5e264a --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/entity/AuditResultDetail.java @@ -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; +} diff --git a/src/main/java/com/crrc/crrcmall/entity/AuditRuleConfig.java b/src/main/java/com/crrc/crrcmall/entity/AuditRuleConfig.java new file mode 100644 index 0000000..2f306e2 --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/entity/AuditRuleConfig.java @@ -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; +} diff --git a/src/main/java/com/crrc/crrcmall/entity/AuditTask.java b/src/main/java/com/crrc/crrcmall/entity/AuditTask.java new file mode 100644 index 0000000..435986a --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/entity/AuditTask.java @@ -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; +} diff --git a/src/main/java/com/crrc/crrcmall/service/AuditRuleService.java b/src/main/java/com/crrc/crrcmall/service/AuditRuleService.java new file mode 100644 index 0000000..fab36d4 --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/service/AuditRuleService.java @@ -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 rawCustomList); +} diff --git a/src/main/java/com/crrc/crrcmall/service/AuditService.java b/src/main/java/com/crrc/crrcmall/service/AuditService.java new file mode 100644 index 0000000..a888504 --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/service/AuditService.java @@ -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); +} diff --git a/src/main/java/com/crrc/crrcmall/service/PriceService.java b/src/main/java/com/crrc/crrcmall/service/PriceService.java new file mode 100644 index 0000000..ccd55b9 --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/service/PriceService.java @@ -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); +} diff --git a/src/main/java/com/crrc/crrcmall/service/SearchService.java b/src/main/java/com/crrc/crrcmall/service/SearchService.java index 0efb68e..ad6c306 100644 --- a/src/main/java/com/crrc/crrcmall/service/SearchService.java +++ b/src/main/java/com/crrc/crrcmall/service/SearchService.java @@ -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); } diff --git a/src/main/java/com/crrc/crrcmall/service/impl/AuditRuleServiceImpl.java b/src/main/java/com/crrc/crrcmall/service/impl/AuditRuleServiceImpl.java new file mode 100644 index 0000000..0abe0c2 --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/service/impl/AuditRuleServiceImpl.java @@ -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 rawCustomList) { + AuditDetailResponseVo response = new AuditDetailResponseVo(); + // TODO: 实际应生成 UUID + response.setTaskId("TASK-MOCK-" + System.currentTimeMillis()); + response.setGoodsId(context.getGoodsId()); + + // 初始状态:假设全部通过,后续根据校验结果更新 + response.setStatus("SUCCESSED"); + + List 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; + } +} diff --git a/src/main/java/com/crrc/crrcmall/service/impl/AuditServiceImpl.java b/src/main/java/com/crrc/crrcmall/service/impl/AuditServiceImpl.java new file mode 100644 index 0000000..6bf235b --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/service/impl/AuditServiceImpl.java @@ -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 TASK_DB = new ConcurrentHashMap<>(); + // 模拟数据库存储 (Result) + private static final Map 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) 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; + } +} diff --git a/src/main/java/com/crrc/crrcmall/service/impl/PriceServiceImpl.java b/src/main/java/com/crrc/crrcmall/service/impl/PriceServiceImpl.java new file mode 100644 index 0000000..9adbe22 --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/service/impl/PriceServiceImpl.java @@ -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; + } +} diff --git a/src/main/java/com/crrc/crrcmall/service/impl/SearchServiceImpl.java b/src/main/java/com/crrc/crrcmall/service/impl/SearchServiceImpl.java index 06d2a32..2dba1b8 100644 --- a/src/main/java/com/crrc/crrcmall/service/impl/SearchServiceImpl.java +++ b/src/main/java/com/crrc/crrcmall/service/impl/SearchServiceImpl.java @@ -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(); + } } diff --git a/src/main/java/com/crrc/crrcmall/vo/audit/AuditCheckResponseVo.java b/src/main/java/com/crrc/crrcmall/vo/audit/AuditCheckResponseVo.java new file mode 100644 index 0000000..056c645 --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/vo/audit/AuditCheckResponseVo.java @@ -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; +} diff --git a/src/main/java/com/crrc/crrcmall/vo/audit/AuditDetailResponseVo.java b/src/main/java/com/crrc/crrcmall/vo/audit/AuditDetailResponseVo.java new file mode 100644 index 0000000..a3ec9db --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/vo/audit/AuditDetailResponseVo.java @@ -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 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; + } +} diff --git a/src/main/java/com/crrc/crrcmall/vo/price/PriceImportResponseVo.java b/src/main/java/com/crrc/crrcmall/vo/price/PriceImportResponseVo.java new file mode 100644 index 0000000..79e87c0 --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/vo/price/PriceImportResponseVo.java @@ -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; +} diff --git a/src/main/java/com/crrc/crrcmall/vo/price/PriceResultResponseVo.java b/src/main/java/com/crrc/crrcmall/vo/price/PriceResultResponseVo.java new file mode 100644 index 0000000..ae95823 --- /dev/null +++ b/src/main/java/com/crrc/crrcmall/vo/price/PriceResultResponseVo.java @@ -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; +}