瀏覽代碼

feat(store): 新增AI服务店铺审核与门头识别功能

- 新增 AiApproveStoreInfo VO 类用于封装店铺审核请求参数
- 实现 AiAuthTokenUtil 工具类获取AI服务认证Token
- 在 StoreInfoController 中新增 aiApproveStoreInfo 和 getStoreOcrData 接口
- 扩展 StoreInfoService 及其实现类,支持调用AI服务进行门店审核和OCR识别
- 配置 RestTemplate 并注入相关依赖以支持HTTP请求
- 添加 third-party 配置项用于AI服务地址管理
- 引入 OcrTypeEnum 常量支持OCR类型判断
- 完成OCR识别数据查询逻辑与AI接口对接处理
- 实现AI审核状态回写至门店信息表的功能
- 增加异常处理确保AI服务调用稳定性
Lhaibo 1 周之前
父節點
當前提交
1c1260ce56

+ 34 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/AiApproveStoreInfo.java

@@ -0,0 +1,34 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 店铺审核 - AI 服务请求体
+ */
+@Data
+public class AiApproveStoreInfo {
+
+    @ApiModelProperty(value = "用户id")
+    private String userId;
+
+    @ApiModelProperty(value = "商户名称(公司/门店名称)")
+    private String merchant_name;
+
+    @ApiModelProperty(value = "经营范围 / 业务描述")
+    private String business_scope;
+
+    @ApiModelProperty(value = "联系人姓名")
+    private String contact_name;
+
+    @ApiModelProperty(value = "联系人手机号")
+    private String contact_phone;
+
+    @ApiModelProperty(value = "联系人邮箱")
+    private String contact_email;
+
+    @ApiModelProperty(value = "证照/资质门头照片列表")
+    private List<String> license_images;
+}

+ 35 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreInfoController.java

@@ -684,4 +684,39 @@ public class StoreInfoController {
         return R.data(list);
     }
 
+    @ApiOperation(value = "AI服务-店铺审核")
+    @ApiOperationSupport(order = 15)
+    @PostMapping("/aiApproveStoreInfo")
+    public R<Boolean> aiApproveStoreInfo(@RequestBody AiApproveStoreInfo aiApproveStoreInfo) {
+        log.info("StoreInfoController.aiApproveStoreInfo");
+        try {
+            storeInfoService.aiApproveStoreInfo(aiApproveStoreInfo);
+            return R.success("店铺审核完成");
+        } catch (Exception e) {
+            log.error("AI服务-店铺审核异常", e);
+            return R.fail("店铺审核失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "AI服务-门头识别")
+    @ApiOperationSupport(order = 16)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeUserId", value = "门店用户id", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "imageUrl", value = "图片URL", dataType = "String", paramType = "query", required = true)
+    })
+    @GetMapping("/getStoreOcrData")
+    public R<Map<String, Object>> getStoreOcrData(@RequestParam("storeUserId") String storeUserId,
+                                                  @RequestParam("imageUrl") String imageUrl) {
+        log.info("StoreInfoController.getStoreOcrData?storeId={},imageUrl={}", storeUserId, imageUrl);
+        if (storeUserId == null || storeUserId.trim().isEmpty() || imageUrl == null || imageUrl.trim().isEmpty()) {
+            return R.fail("门店ID与图片URL不能为空");
+        }
+        Map<String, Object> ocrData = null;
+        try {
+            ocrData = storeInfoService.getStoreOcrData(storeUserId, imageUrl);
+        } catch (Exception e) {
+            return R.fail("未查询到OCR识别数据");
+        }
+        return R.data(ocrData);
+    }
 }

+ 11 - 0
alien-store/src/main/java/shop/alien/store/service/StoreInfoService.java

@@ -279,4 +279,15 @@ public interface StoreInfoService extends IService<StoreInfo> {
      *
      */
     int foodLicenceType(int id);
+
+    /**
+     * 根据门店及图片地址查询最新OCR识别数据
+     *
+     * @param storeUserId  门店ID
+     * @param imageUrl 图片URL
+     * @return OCR识别记录
+     */
+    Map<String, Object> getStoreOcrData(String storeUserId, String imageUrl);
+
+    void aiApproveStoreInfo(AiApproveStoreInfo aiApproveStoreInfo);
 }

+ 113 - 3
alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java

@@ -14,12 +14,12 @@ import lombok.RequiredArgsConstructor;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.data.geo.Point;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
+import org.springframework.http.*;
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
+import org.springframework.web.client.RestTemplate;
 import org.springframework.web.multipart.MultipartFile;
 import org.springframework.web.multipart.MultipartRequest;
 import shop.alien.entity.store.*;
@@ -40,10 +40,12 @@ import shop.alien.store.service.*;
 import shop.alien.store.util.CommonConstant;
 import shop.alien.store.util.FileUploadUtil;
 import shop.alien.store.util.GroupConstant;
+import shop.alien.store.util.ai.AiAuthTokenUtil;
 import shop.alien.util.ali.AliOSSUtil;
 import shop.alien.util.common.DistanceUtil;
 import shop.alien.util.common.constant.CouponStatusEnum;
 import shop.alien.util.common.constant.CouponTypeEnum;
+import shop.alien.util.common.constant.OcrTypeEnum;
 import shop.alien.util.common.constant.OrderStatusEnum;
 
 import javax.annotation.Resource;
@@ -137,6 +139,12 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
 
     private final LifeBlacklistMapper lifeBlacklistMapper;
 
+    private final OcrImageUploadMapper ocrImageUploadMapper;
+
+    private final AiAuthTokenUtil aiAuthTokenUtil;
+
+    private final RestTemplate restTemplate;
+
     /** 商户证照历史记录数据访问对象 */
     private final StoreLicenseHistoryMapper licenseHistoryMapper;
 
@@ -150,6 +158,12 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
     @Value("${spring.web.resources.excel-generate-path}")
     private String excelGeneratePath;
 
+    @Value("${third-party-identification.base-url}")
+    private String identificationPath;
+
+    @Value("${third-party-applications.base-url}")
+    private String applicationsPath;
+
     /**
      * 懒得查, 留着导出Excel
      */
@@ -2389,5 +2403,101 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
         }
     }
 
+    @Override
+    public void aiApproveStoreInfo(AiApproveStoreInfo aiApproveStoreInfo) {
+        HttpHeaders aiHeaders = new HttpHeaders();
+        aiHeaders.setContentType(MediaType.APPLICATION_JSON);
+        aiHeaders.set("Authorization", "Bearer " + aiAuthTokenUtil.getAccessToken());
+
+        HttpEntity<AiApproveStoreInfo> request = new HttpEntity<>(aiApproveStoreInfo, aiHeaders);
+        ResponseEntity<String> response = null;
+        try {
+            response = restTemplate.postForEntity(applicationsPath, request, String.class);
+            if (response.getStatusCodeValue() != 200) {
+                throw new RuntimeException("AI门店审核接口调用失败 http状态:" + response.getStatusCode());
+            }
+            if (StringUtils.isNotEmpty(response.getBody())) {
+                JSONObject jsonObject = JSONObject.parseObject(response.getBody());
+                if (jsonObject.getInteger("code") == 200) {
+                    JSONObject data = jsonObject.getJSONObject("data");
+                    List<StoreInfo> storeInfos = storeInfoMapper.selectList(new LambdaQueryWrapper<StoreInfo>()
+                            .eq(StoreInfo::getCreatedUserId, aiApproveStoreInfo.getUserId()).eq(StoreInfo::getStoreApplicationStatus, 0).eq(StoreInfo::getDeleteFlag, 0));
+                    for (StoreInfo storeInfo : storeInfos) {
+                        if ("approved".equals(data.getString("status"))) {
+                            storeInfo.setStoreApplicationStatus(1);
+                        } else if ("rejected".equals(data.getString("status"))) {
+                            storeInfo.setStoreApplicationStatus(2);
+                        } else {
+                            System.out.println("未知状态");
+                        }
+                        storeInfoMapper.updateById(storeInfo);
+                    }
+                } else {
+                    throw new RuntimeException("AI门店审核接口调用失败 code:" + jsonObject.getInteger("code"));
+                }
+            }
+        } catch (Exception e){
+            throw new RuntimeException("调用门店审核接口异常", e);
+        }
+    }
+
+    @Override
+    public Map<String, Object> getStoreOcrData(String storeUserId, String imageUrl) {
+        Map<String, Object> map = new HashMap<>();
+        LambdaQueryWrapper<OcrImageUpload> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(OcrImageUpload::getStoreUserId, storeUserId)
+                .eq(OcrImageUpload::getOcrType, OcrTypeEnum.BUSINESS_LICENSE.getCode());
+        OcrImageUpload ocrImageUploads = ocrImageUploadMapper.selectOne(queryWrapper);
+        if(ocrImageUploads== null){
+            throw new RuntimeException("未找到OCI识别数据!");
+        }
+        String accessToken = aiAuthTokenUtil.getAccessToken();
+
+        HttpHeaders aiHeaders = new HttpHeaders();
+        aiHeaders.setContentType(MediaType.APPLICATION_JSON);
+        aiHeaders.set("Authorization", "Bearer " + accessToken);
+        Map<String, Object> jsonBody = new HashMap<>();
+        List<String> imageUrls = new ArrayList<>();
+        imageUrls.add(imageUrl);
+        jsonBody.put("image_urls", imageUrls);
+        String ocrResult = ocrImageUploads.getOcrResult();
+        JSONObject jsonObject = JSONObject.parseObject(ocrResult);
+        jsonBody.put("merchant_name", jsonObject.get("companyName"));
+
+        HttpEntity<Map<String, Object>> request = new HttpEntity<>(jsonBody, aiHeaders);
+        ResponseEntity<String> response = null;
+        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
+        factory.setConnectTimeout(5000);
+        factory.setReadTimeout(60000);
+        restTemplate.setRequestFactory(factory);
+        try {
+            response = restTemplate.postForEntity(identificationPath, request, String.class);
+            if (response.getStatusCodeValue() != 200) {
+                throw new RuntimeException("AI内容审核接口调用失败 http状态:" + response.getStatusCode());
+            }
 
+        } catch (Exception e) {
+            throw new RuntimeException("AI接口调用失败");
+        }
+        com.alibaba.fastjson2.JSONObject responseNode = com.alibaba.fastjson2.JSONObject.parseObject(response.getBody());
+        if (responseNode == null) {
+            throw new RuntimeException("AI接口调用失败,响应内容为空");
+        } else {
+            Integer code = responseNode.getInteger("code");
+            if (code == 200) {
+                Map<String, Object> dataMap = (Map<String, Object>) responseNode.get("data");
+                if (Objects.nonNull(dataMap)) {
+                    // 获取match_results(JSON中是数组,反序列化为List<Map>)
+                    List<Map<String, Object>> matchResults = (List<Map<String, Object>>) dataMap.get("match_results");
+                    map.put("overall_match", dataMap.get("overall_match"));
+                    if (Objects.nonNull(matchResults) && !matchResults.isEmpty()) {
+                        map.put("match_reason", matchResults.get(0).get("match_reason"));
+                    }
+                }
+            } else {
+                throw new RuntimeException("AI接口调用失败,错误码: " + code);
+            }
+        }
+        return map;
+    }
 }

+ 78 - 0
alien-store/src/main/java/shop/alien/store/util/ai/AiAuthTokenUtil.java

@@ -0,0 +1,78 @@
+package shop.alien.store.util.ai;
+
+import com.alibaba.fastjson2.JSONObject;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * AI 服务鉴权配置
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class AiAuthTokenUtil {
+
+    private final RestTemplate restTemplate;
+
+    @Value("${third-party-login.base-url}")
+    private String loginUrl;
+
+    @Value("${third-party-user-name.base-url}")
+    private String userName;
+
+    @Value("${third-party-pass-word.base-url}")
+    private String passWord;
+
+    /**
+     * 登录 AI 服务,获取 token
+     *
+     * @return accessToken
+     */
+    public String getAccessToken() {
+        log.info("登录Ai服务获取token...{}", loginUrl);
+        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
+        formData.add("username", userName);
+        formData.add("password", passWord);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+        HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(formData, headers);
+
+        ResponseEntity<String> response;
+        try {
+            log.info("请求Ai服务登录接口===================>");
+            response = restTemplate.postForEntity(loginUrl, requestEntity, String.class);
+        } catch (Exception e) {
+            log.error("请求AI服务登录接口失败", e);
+            return null;
+        }
+
+        if (response != null && response.getStatusCode() == HttpStatus.OK) {
+            String body = response.getBody();
+            log.info("请求Ai服务登录成功 postForEntity.getBody()\t{}", body);
+            if (StringUtils.hasText(body)) {
+                JSONObject jsonObject = JSONObject.parseObject(body);
+                if (jsonObject != null) {
+                    JSONObject dataJson = jsonObject.getJSONObject("data");
+                    if (dataJson != null) {
+                        return dataJson.getString("access_token");
+                    }
+                }
+            }
+            log.warn("AI服务登录响应解析失败 body: {}", body);
+            return null;
+        }
+
+        log.error("请求AI服务 登录接口失败 http状态:{}", response != null ? response.getStatusCode() : null);
+        return null;
+    }
+}
+
+