|
|
@@ -0,0 +1,263 @@
|
|
|
+package shop.alien.store.service.impl;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.http.*;
|
|
|
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
|
|
+import org.springframework.scheduling.annotation.Async;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.web.client.RestTemplate;
|
|
|
+import shop.alien.entity.store.StoreImg;
|
|
|
+import shop.alien.entity.store.StoreInfo;
|
|
|
+import shop.alien.entity.storePlatform.StoreLicenseHistory;
|
|
|
+import shop.alien.mapper.StoreImgMapper;
|
|
|
+import shop.alien.mapper.StoreInfoMapper;
|
|
|
+import shop.alien.mapper.storePlantform.StoreLicenseHistoryMapper;
|
|
|
+import shop.alien.store.util.ai.AiAuthTokenUtil;
|
|
|
+
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 证照审核异步服务
|
|
|
+ * 用于异步处理证照审核,避免阻塞主流程
|
|
|
+ *
|
|
|
+ * @author system
|
|
|
+ * @since 2026-01-14
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class LicenseAuditAsyncService {
|
|
|
+
|
|
|
+ private final AiAuthTokenUtil aiAuthTokenUtil;
|
|
|
+ private final RestTemplate restTemplate;
|
|
|
+ private final StoreInfoMapper storeInfoMapper;
|
|
|
+ private final StoreLicenseHistoryMapper licenseHistoryMapper;
|
|
|
+ private final StoreImgMapper storeImgMapper;
|
|
|
+
|
|
|
+ @Value("${third-party-license-review.base-url}")
|
|
|
+ private String licenseReviewPath;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 异步调用证照审核AI接口并处理审核结果
|
|
|
+ * 1. 调用AI接口获取审核结果
|
|
|
+ * 2. 如果是营业执照(licenseStatus=1),解析并存入到期时间
|
|
|
+ * 3. 根据审核结果(is_expired或is_valid)更新store_license_history表的审核状态
|
|
|
+ *
|
|
|
+ * @param storeId 门店ID
|
|
|
+ * @param licenseStatus 证照类型:1-营业执照,2-其他资质证明
|
|
|
+ * @param imageUrl 证照图片URL
|
|
|
+ */
|
|
|
+ @Async("taskExecutor")
|
|
|
+ public void validateLicenseExpiryAndUpdate(Integer storeId, Integer licenseStatus, String imageUrl) {
|
|
|
+ if (storeId == null || licenseStatus == null || StringUtils.isEmpty(imageUrl)) {
|
|
|
+ log.warn("证照审核参数不完整,storeId={}, licenseStatus={}, imageUrl={}", storeId, licenseStatus, imageUrl);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ String accessToken = aiAuthTokenUtil.getAccessToken();
|
|
|
+ if (StringUtils.isEmpty(accessToken)) {
|
|
|
+ log.error("获取AI访问令牌失败,门店ID:{}", storeId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ HttpHeaders aiHeaders = new HttpHeaders();
|
|
|
+ aiHeaders.setContentType(MediaType.APPLICATION_JSON);
|
|
|
+ aiHeaders.set("Authorization", "Bearer " + accessToken);
|
|
|
+
|
|
|
+ // 构建请求体
|
|
|
+ Map<String, Object> jsonBody = new HashMap<>();
|
|
|
+ jsonBody.put("image_url", imageUrl);
|
|
|
+
|
|
|
+ HttpEntity<Map<String, Object>> request = new HttpEntity<>(jsonBody, aiHeaders);
|
|
|
+
|
|
|
+ // 设置超时时间
|
|
|
+ HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
|
|
|
+ factory.setConnectTimeout(10000); // 连接超时10秒
|
|
|
+ factory.setReadTimeout(120000); // 读取超时120秒(2分钟)
|
|
|
+ restTemplate.setRequestFactory(factory);
|
|
|
+
|
|
|
+ ResponseEntity<String> response = restTemplate.postForEntity(licenseReviewPath, request, String.class);
|
|
|
+
|
|
|
+ if (response.getStatusCodeValue() != 200) {
|
|
|
+ log.error("证照审核接口调用失败,门店ID:{},http状态:{}", storeId, response.getStatusCode());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (StringUtils.isEmpty(response.getBody())) {
|
|
|
+ log.error("证照审核接口返回内容为空,门店ID:{}", storeId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONObject responseNode = JSONObject.parseObject(response.getBody());
|
|
|
+ if (responseNode == null) {
|
|
|
+ log.error("证照审核接口响应解析失败,门店ID:{}", storeId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Integer code = responseNode.getInteger("code");
|
|
|
+ if (code == null || code != 200) {
|
|
|
+ String message = responseNode.getString("message");
|
|
|
+ log.error("证照审核接口调用失败,门店ID:{},错误码: {}, 错误信息: {}", storeId, code, message);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> dataMap = (Map<String, Object>) responseNode.get("data");
|
|
|
+ if (dataMap == null) {
|
|
|
+ log.warn("证照审核接口返回data为空,门店ID:{}", storeId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Boolean isValid = (Boolean) dataMap.get("is_valid");
|
|
|
+ String expiryDateStr = (String) dataMap.get("expiry_date");
|
|
|
+ Boolean isExpired = (Boolean) dataMap.get("is_expired");
|
|
|
+ Integer remainingDays = (Integer) dataMap.get("remaining_days");
|
|
|
+ String reason = (String) dataMap.get("reason");
|
|
|
+ String licenseType = (String) dataMap.get("license_type");
|
|
|
+
|
|
|
+ String licenseTypeName = licenseStatus == 1 ? "营业执照" : "其他资质证明";
|
|
|
+ log.info("{}证照审核结果,门店ID:{},图片URL:{},is_valid={},expiry_date={},is_expired={},remaining_days={},license_type={}",
|
|
|
+ licenseTypeName, storeId, imageUrl, isValid, expiryDateStr, isExpired, remainingDays, licenseType);
|
|
|
+
|
|
|
+ // 如果是营业执照,解析并存入到期时间
|
|
|
+ if (licenseStatus == 1 && StringUtils.isNotEmpty(expiryDateStr)) {
|
|
|
+ try {
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
+ Date expiryDate = sdf.parse(expiryDateStr);
|
|
|
+
|
|
|
+ StoreInfo updateStoreInfo = new StoreInfo();
|
|
|
+ updateStoreInfo.setId(storeId);
|
|
|
+ updateStoreInfo.setBusinessLicenseExpirationTime(expiryDate);
|
|
|
+ storeInfoMapper.updateById(updateStoreInfo);
|
|
|
+ log.info("营业执照到期时间已更新,门店ID:{},到期时间:{}", storeId, expiryDateStr);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("解析营业执照到期时间失败,门店ID:{},expiryDate:{}", storeId, expiryDateStr, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 判断审核结果
|
|
|
+ boolean needReject = false;
|
|
|
+ boolean needApprove = false;
|
|
|
+ String rejectReason = null;
|
|
|
+
|
|
|
+ if (licenseStatus == 1) {
|
|
|
+ // 营业执照的审核逻辑
|
|
|
+ if (!"business_license".equals(licenseType)) {
|
|
|
+ // 上传的图片不是营业执照licenseType="business_license"
|
|
|
+ needReject = true;
|
|
|
+ rejectReason = "上传的图片不是营业执照";
|
|
|
+ } else if (Boolean.TRUE.equals(isExpired)) {
|
|
|
+ // 营业执照已过期
|
|
|
+ needReject = true;
|
|
|
+ rejectReason = "已过期";
|
|
|
+ } else if (Boolean.TRUE.equals(isValid) && "business_license".equals(licenseType) && Boolean.FALSE.equals(isExpired)) {
|
|
|
+ // 营业执照有效且未过期,审核通过
|
|
|
+ needApprove = true;
|
|
|
+ }
|
|
|
+ } else if (licenseStatus == 2) {
|
|
|
+ // 其他资质证明的审核逻辑
|
|
|
+ if (Boolean.FALSE.equals(isValid)) {
|
|
|
+ // 上传的不是证件图片
|
|
|
+ needReject = true;
|
|
|
+ rejectReason = "上传的图片含非证件图片";
|
|
|
+ } else if (Boolean.TRUE.equals(isValid)) {
|
|
|
+ // 证件有效,审核通过
|
|
|
+ needApprove = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新store_license_history表的审核状态
|
|
|
+ if (licenseStatus == 2) {
|
|
|
+ // 其他资质证明:img_url字段包含多个URL(用逗号分隔),需要查找包含当前图片URL的记录
|
|
|
+ LambdaUpdateWrapper<StoreLicenseHistory> updateWrapper = new LambdaUpdateWrapper<>();
|
|
|
+ updateWrapper.eq(StoreLicenseHistory::getStoreId, storeId)
|
|
|
+ .eq(StoreLicenseHistory::getLicenseStatus, licenseStatus)
|
|
|
+ .eq(StoreLicenseHistory::getLicenseExecuteStatus, 2) // 2-审核中
|
|
|
+ .like(StoreLicenseHistory::getImgUrl, imageUrl) // 使用like查找包含该图片URL的记录
|
|
|
+ .eq(StoreLicenseHistory::getDeleteFlag, 0);
|
|
|
+
|
|
|
+ if (needReject) {
|
|
|
+ // 审核拒绝:一张图片审核拒绝或失败就设置状态为审核拒绝
|
|
|
+ updateWrapper.set(StoreLicenseHistory::getLicenseExecuteStatus, 3) // 3-审核拒绝
|
|
|
+ .set(StoreLicenseHistory::getReasonRefusal, rejectReason);
|
|
|
+ licenseHistoryMapper.update(null, updateWrapper);
|
|
|
+ log.info("{}AI审核拒绝,门店ID:{},图片URL:{},拒绝原因:{}", licenseTypeName, storeId, imageUrl, rejectReason);
|
|
|
+
|
|
|
+ // 审核拒绝时,删除store_img表中的记录(逻辑删除),避免前端展示审核拒绝的图片
|
|
|
+ LambdaUpdateWrapper<StoreImg> deleteImgWrapper = new LambdaUpdateWrapper<>();
|
|
|
+ deleteImgWrapper.eq(StoreImg::getStoreId, storeId)
|
|
|
+ .eq(StoreImg::getImgType, 35) // 其他资质证明对应35
|
|
|
+ .eq(StoreImg::getImgUrl, imageUrl)
|
|
|
+ .eq(StoreImg::getDeleteFlag, 0)
|
|
|
+ .set(StoreImg::getDeleteFlag, 1);
|
|
|
+ storeImgMapper.update(null, deleteImgWrapper);
|
|
|
+ log.info("{}AI审核拒绝,已删除store_img记录,门店ID:{},图片URL:{}", licenseTypeName, storeId, imageUrl);
|
|
|
+ } else if (needApprove) {
|
|
|
+ // 审核通过:需要检查所有图片是否都审核通过
|
|
|
+ // 先查询当前记录,获取所有图片URL
|
|
|
+ StoreLicenseHistory currentHistory = licenseHistoryMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<StoreLicenseHistory>()
|
|
|
+ .eq(StoreLicenseHistory::getStoreId, storeId)
|
|
|
+ .eq(StoreLicenseHistory::getLicenseStatus, licenseStatus)
|
|
|
+ .eq(StoreLicenseHistory::getLicenseExecuteStatus, 2)
|
|
|
+ .like(StoreLicenseHistory::getImgUrl, imageUrl)
|
|
|
+ .eq(StoreLicenseHistory::getDeleteFlag, 0)
|
|
|
+ .last("LIMIT 1")
|
|
|
+ );
|
|
|
+
|
|
|
+ if (currentHistory != null && StringUtils.isNotEmpty(currentHistory.getImgUrl())) {
|
|
|
+ // 检查是否所有图片都已审核通过
|
|
|
+ // 由于是异步审核,每张图片都会调用此方法,所以这里只记录单张图片的审核通过
|
|
|
+ // 如果需要等所有图片都审核通过才更新状态,需要额外的机制来跟踪
|
|
|
+ log.info("{}AI审核通过,门店ID:{},图片URL:{}", licenseTypeName, storeId, imageUrl);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 营业执照:精确匹配imgUrl
|
|
|
+ LambdaUpdateWrapper<StoreLicenseHistory> updateWrapper = new LambdaUpdateWrapper<>();
|
|
|
+ updateWrapper.eq(StoreLicenseHistory::getStoreId, storeId)
|
|
|
+ .eq(StoreLicenseHistory::getLicenseStatus, licenseStatus)
|
|
|
+ .eq(StoreLicenseHistory::getLicenseExecuteStatus, 2) // 2-审核中
|
|
|
+ .eq(StoreLicenseHistory::getImgUrl, imageUrl)
|
|
|
+ .eq(StoreLicenseHistory::getDeleteFlag, 0);
|
|
|
+
|
|
|
+ if (needReject) {
|
|
|
+ // 审核拒绝
|
|
|
+ updateWrapper.set(StoreLicenseHistory::getLicenseExecuteStatus, 3) // 3-审核拒绝
|
|
|
+ .set(StoreLicenseHistory::getReasonRefusal, rejectReason);
|
|
|
+ licenseHistoryMapper.update(null, updateWrapper);
|
|
|
+ log.info("{}AI审核拒绝,门店ID:{},图片URL:{},拒绝原因:{}", licenseTypeName, storeId, imageUrl, rejectReason);
|
|
|
+
|
|
|
+ // 审核拒绝时,删除store_img表中的记录(逻辑删除),避免前端展示审核拒绝的图片
|
|
|
+ LambdaUpdateWrapper<StoreImg> deleteImgWrapper = new LambdaUpdateWrapper<>();
|
|
|
+ deleteImgWrapper.eq(StoreImg::getStoreId, storeId)
|
|
|
+ .eq(StoreImg::getImgType, 14) // 营业执照对应14
|
|
|
+ .eq(StoreImg::getImgUrl, imageUrl)
|
|
|
+ .eq(StoreImg::getDeleteFlag, 0)
|
|
|
+ .set(StoreImg::getDeleteFlag, 1);
|
|
|
+ storeImgMapper.update(null, deleteImgWrapper);
|
|
|
+ log.info("{}AI审核拒绝,已删除store_img记录,门店ID:{},图片URL:{}", licenseTypeName, storeId, imageUrl);
|
|
|
+ } else if (needApprove) {
|
|
|
+ // 审核通过
|
|
|
+ updateWrapper.set(StoreLicenseHistory::getLicenseExecuteStatus, 1); // 1-审核通过
|
|
|
+ licenseHistoryMapper.update(null, updateWrapper);
|
|
|
+ log.info("{}AI审核通过,门店ID:{},图片URL:{}", licenseTypeName, storeId, imageUrl);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ String licenseTypeName = licenseStatus == 1 ? "营业执照" : "其他资质证明";
|
|
|
+ log.error("调用{}证照审核接口异常,门店ID:{},图片URL:{}", licenseTypeName, storeId, imageUrl, e);
|
|
|
+ // AI审核失败不影响业务流程,继续执行
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|