瀏覽代碼

Merge branch 'sit' into uat-20260202

dujian 3 周之前
父節點
當前提交
6de9386e10

+ 1 - 2
alien-entity/src/main/java/shop/alien/mapper/StoreInfoMapper.java

@@ -182,8 +182,7 @@ public interface StoreInfoMapper extends BaseMapper<StoreInfo> {
             "left join store_dictionary e on e.type_name = 'storeType' and e.dict_id = a.store_type and e.delete_flag = 0 " +
             "${ew.customSqlSegment} " +
             "and a.store_position is not null and a.store_position != '' " +
-            "and ROUND(ST_Distance_Sphere(ST_GeomFromText(CONCAT('POINT(', REPLACE(#{position}, ',', ' '), ')' )), ST_GeomFromText(CONCAT('POINT(', REPLACE(a.store_position, ',', ' '), ')' ))) / 1000, 2) <= 1 " +
-            "order by distance3 asc limit 20")
+            "order by distance3 asc")
     List<StoreInfoVo> getMoreRecommendedStores(@Param(Constants.WRAPPER) QueryWrapper<StoreInfoVo> queryWrapper, @Param("position") String position);
 
     /**

+ 23 - 0
alien-lawyer/src/main/java/shop/alien/lawyer/service/impl/LawyerUserServiceImpl.java

@@ -22,6 +22,7 @@ import shop.alien.lawyer.service.LawyerLegalProblemScenarioService;
 import shop.alien.lawyer.service.LawyerServiceAreaService;
 import shop.alien.lawyer.service.LawyerUserSearchHistoryService;
 import shop.alien.lawyer.service.LawyerUserService;
+import shop.alien.lawyer.util.ai.LawyerLicenseVerifyUtil;
 import shop.alien.mapper.*;
 import shop.alien.util.common.ListToPage;
 import shop.alien.util.excel.EasyExcelUtil;
@@ -54,6 +55,7 @@ public class LawyerUserServiceImpl extends ServiceImpl<LawyerUserMapper, LawyerU
     private final LawFirmPaymentMapper lawFirmPaymentmapper;
     private final OrderReviewMapper orderReviewMapper;
     private final LifeNoticeMapper lifeNoticeMapper;
+    private final LawyerLicenseVerifyUtil lawyerLicenseVerifyUtil;
 
     @Override
     public R<IPage<LawyerUser>> getLawyerUserList(int pageNum, int pageSize, String name, String phone, Integer status) {
@@ -198,6 +200,7 @@ public class LawyerUserServiceImpl extends ServiceImpl<LawyerUserMapper, LawyerU
         if (lawyerId != null) {
             queryWrapper.eq("id", lawyerId);
         }
+        log.info("getRecommendedLawyerList lawyerId::",lawyerId);
 
         // 分类筛选:通过律师服务领域关联表查询
         if (categoryId != null) {
@@ -207,6 +210,7 @@ public class LawyerUserServiceImpl extends ServiceImpl<LawyerUserMapper, LawyerU
                 return R.data(new Page<>(pageNum, pageSizeNum));
             }
             queryWrapper.in("id", lawyerIds);
+            log.info("getRecommendedLawyerList lawyerIds::",lawyerIds);
         }
 
         // 排序:优先推荐律师 -> 在线律师 -> 注册时间
@@ -218,6 +222,8 @@ public class LawyerUserServiceImpl extends ServiceImpl<LawyerUserMapper, LawyerU
         Page<LawyerUser> pageObj = new Page<>(pageNum, pageSizeNum);
         IPage<LawyerUser> pageResult = this.page(pageObj, queryWrapper);
 
+        log.info("getRecommendedLawyerList pageResult:",pageResult);
+
         // 为每个律师设置关联的法律问题场景列表
         if (pageResult.getRecords() != null && !pageResult.getRecords().isEmpty()) {
             setLawyerScenarios(pageResult.getRecords());
@@ -632,6 +638,12 @@ public class LawyerUserServiceImpl extends ServiceImpl<LawyerUserMapper, LawyerU
             log.warn("更新律师用户信息失败:参数为空或律师ID为空");
             return R.fail("律师ID不能为空");
         }
+        LawyerUser existing = lawyerUserMapper.selectById(lawyerUserVo.getId());
+        if (existing == null || (existing.getDeleteFlag() != null && existing.getDeleteFlag() == 1)) {
+            log.warn("更新律师用户信息失败:律师不存在,律师ID={}", lawyerUserVo.getId());
+            return R.fail("律师不存在");
+        }
+
         LawyerUser lawyerUser = new LawyerUser();
         lawyerUser.setId(lawyerUserVo.getId());
 
@@ -719,6 +731,17 @@ public class LawyerUserServiceImpl extends ServiceImpl<LawyerUserMapper, LawyerU
             hasUpdate = true;
         }
 
+        // 修改执业证、姓名或头像时,先走 AI 执业证核验(与库中数据合并后校验)
+        if (lawyerUserVo.getCertificateImage() != null || lawyerUserVo.getName() != null || lawyerUserVo.getHeadImg() != null) {
+            String certUrl = lawyerUserVo.getCertificateImage() != null ? lawyerUserVo.getCertificateImage() : existing.getCertificateImage();
+            String name = lawyerUserVo.getName() != null ? lawyerUserVo.getName() : existing.getName();
+            String headUrl = lawyerUserVo.getHeadImg() != null ? lawyerUserVo.getHeadImg() : existing.getHeadImg();
+            LawyerLicenseVerifyUtil.VerifyResult licenseResult = lawyerLicenseVerifyUtil.verify(certUrl, name, headUrl);
+            if (!licenseResult.isPassed()) {
+                return R.fail("执业证核验失败");
+            }
+        }
+
 // 只有当有字段需要更新时才执行更新操作
         if (hasUpdate) {
 

+ 155 - 0
alien-lawyer/src/main/java/shop/alien/lawyer/util/ai/LawyerLicenseVerifyUtil.java

@@ -0,0 +1,155 @@
+package shop.alien.lawyer.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.cloud.context.config.annotation.RefreshScope;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.Objects;
+
+/**
+ * 律师执业证/资格证 AI 核验(入驻时调用)
+ */
+@Slf4j
+@Component
+@RefreshScope
+@RequiredArgsConstructor
+public class LawyerLicenseVerifyUtil {
+
+    private final RestTemplate restTemplate;
+    private final AiAuthTokenUtil aiAuthTokenUtil;
+
+    @Value("${ai.service.lawyer-license-verify-url:http://124.93.18.180:9000/ai/multimodal-services/api/v1/lawyer-license/verify}")
+    private String lawyerLicenseVerifyUrl;
+
+    public static class VerifyResult {
+        private final boolean passed;
+        private final String message;
+
+        public VerifyResult(boolean passed, String message) {
+            this.passed = passed;
+            this.message = message;
+        }
+
+        public boolean isPassed() {
+            return passed;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+    }
+
+    /**
+     * 核验执业证照片、姓名与头像是否与证件一致
+     *
+     * @param certificateImageUrl 执业证照片 URL
+     * @param realName            待核验姓名
+     * @param userAvatarUrl       律师头像 URL
+     */
+    public VerifyResult verify(String certificateImageUrl, String realName, String userAvatarUrl) {
+        if (!StringUtils.hasText(certificateImageUrl) || !StringUtils.hasText(realName) || !StringUtils.hasText(userAvatarUrl)) {
+            return new VerifyResult(false, "请完整上传律师执业证照片、本人头像,并填写与证件一致的真实姓名。");
+        }
+        try {
+            String accessToken = aiAuthTokenUtil.getAccessToken();
+            if (!StringUtils.hasText(accessToken)) {
+                log.error("律师执业证核验失败:无法获取 AI 服务访问令牌");
+                return new VerifyResult(false, "执业证核验服务暂不可用,请稍后重试。如多次失败请联系平台客服。");
+            }
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            headers.set("Authorization", "Bearer " + accessToken);
+
+            JSONObject body = new JSONObject();
+            body.put("image_url", certificateImageUrl.trim());
+            body.put("text", realName.trim());
+            body.put("user_avatar_url", userAvatarUrl.trim());
+
+            String json = Objects.requireNonNull(body.toJSONString());
+            HttpEntity<String> entity = new HttpEntity<>(json, headers);
+
+            log.info("调用律师执业证核验接口:url={}", lawyerLicenseVerifyUrl);
+            ResponseEntity<String> response = restTemplate.postForEntity(lawyerLicenseVerifyUrl, entity, String.class);
+
+            if (response.getStatusCode() != HttpStatus.OK || !StringUtils.hasText(response.getBody())) {
+                log.error("律师执业证核验 HTTP 异常:status={}", response.getStatusCode());
+                return new VerifyResult(false, "执业证核验服务响应异常,请稍后重试。");
+            }
+
+            JSONObject root = JSONObject.parseObject(response.getBody());
+            return parseResult(root);
+
+        } catch (Exception e) {
+            log.error("律师执业证核验调用异常", e);
+            return new VerifyResult(false, "执业证核验服务暂时不可用,请稍后重试。");
+        }
+    }
+
+    private VerifyResult parseResult(JSONObject root) {
+        Integer code = root.getInteger("code");
+        String msg = root.getString("message");
+        log.warn("律师执业证核验接口业务错误:code={}, message={}", code, msg);
+        if (code != null && code == 422) {
+            return new VerifyResult(false, "提交资料不完整或格式有误,请检查执业证照片、头像与姓名后重试。");
+        }
+        JSONObject data = root.getJSONObject("data");
+        if (data == null) {
+            log.warn("律师执业证核验返回 data 为空");
+            return new VerifyResult(false, "执业证核验结果异常,请稍后重试。");
+        }
+        if (isPass(data)) {
+            log.info("律师执业证核验通过");
+            return new VerifyResult(true, null);
+        }
+        String userMessage = buildUserFacingFailure(data);
+        log.warn("律师执业证核验未通过:{}", userMessage);
+        return new VerifyResult(false, userMessage);
+    }
+
+    private static boolean isPass(JSONObject data) {
+        Integer i = data.getInteger("is_pass");
+        if (i != null) {
+            return i == 1;
+        }
+        return Boolean.TRUE.equals(data.getBoolean("is_pass"));
+    }
+
+    /**
+     * 面向用户的失败说明(结合接口 reason 与分项标志)
+     */
+    private String buildUserFacingFailure(JSONObject data) {
+        String reason = data.getString("reason");
+        Boolean isLawyerLicense = data.getBoolean("is_lawyer_license");
+        Boolean nameMatch = data.getBoolean("name_match");
+        Boolean faceMatch = data.getBoolean("face_match");
+
+        StringBuilder sb = new StringBuilder();
+        sb.append("律师执业证核验未通过。");
+
+        if (Boolean.FALSE.equals(isLawyerLicense)) {
+            sb.append("上传图片未能识别为有效的律师执业证或律师资格证,请拍摄证件原件清晰、四角完整的照片后重新上传。");
+        } else if (Boolean.FALSE.equals(nameMatch)) {
+            sb.append("证件所载姓名与您填写的姓名不一致,请填写与证件完全一致的真实姓名。");
+        } else if (Boolean.FALSE.equals(faceMatch)) {
+            sb.append("本人头像与证件照人像不一致,请使用本人近期清晰正面头像。");
+        } else {
+            sb.append("请确保证件信息清晰可辨,且姓名、头像与执业证记载一致。");
+        }
+
+        if (StringUtils.hasText(reason)) {
+            sb.append("(").append(reason.trim()).append(")");
+        }
+        return sb.toString();
+    }
+}

+ 19 - 5
alien-store-platform/src/main/java/shop/alien/storeplatform/controller/OperationalActivityController.java

@@ -32,8 +32,22 @@ public class OperationalActivityController {
 
     private final OperationalActivityService activityService;
 
-    @ApiOperation(value = "创建运营活动", notes = "支持配置优惠券和代金券作为奖励,可同时配置或二选一")
+    @ApiOperation(value = "AI生成运营活动海报图片", notes = "仅调用 AI 生成横版/竖版图片 URL,不入库。uploadImgType 须为 2;入参与创建活动一致(活动名称、时间、规则、imgDescribe 等)。生成后请将返回的 activityTitleImg、activityDetailImg 中的 imgUrl 随创建接口一并提交。")
     @ApiOperationSupport(order = 1)
+    @PostMapping("/generatePromotionImages")
+    public R<StoreOperationalActivityDTO> generatePromotionImages(@RequestBody StoreOperationalActivityDTO dto) {
+        log.info("OperationalActivityController.generatePromotionImages: dto={}", dto);
+        try {
+            StoreOperationalActivityDTO result = activityService.generateActivityPromotionImages(dto);
+            return R.data(result, "图片生成成功");
+        } catch (Exception e) {
+            log.error("OperationalActivityController.generatePromotionImages ERROR: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation(value = "创建运营活动(提交保存)", notes = "内容审核后入库;AI 生图请先发 /generatePromotionImages,再将返回的图片 URL 填入 dto 后调用本接口。支持配置优惠券和代金券作为奖励。")
+    @ApiOperationSupport(order = 2)
     @PostMapping("/create")
     public R<String> createActivity(@RequestBody StoreOperationalActivityDTO dto) {
         log.info("OperationalActivityController.createActivity: dto={}", dto);
@@ -59,7 +73,7 @@ public class OperationalActivityController {
     }
 
     @ApiOperation(value = "更新运营活动", notes = "支持更新优惠券和代金券配置")
-    @ApiOperationSupport(order = 2)
+    @ApiOperationSupport(order = 3)
     @PostMapping("/update")
     public R<OperationalActivityUpdateResultVo> updateActivity(@RequestBody StoreOperationalActivityDTO dto) {
         log.info("OperationalActivityController.updateActivity: dto={}", dto);
@@ -78,7 +92,7 @@ public class OperationalActivityController {
     }
 
     @ApiOperation("删除运营活动")
-    @ApiOperationSupport(order = 3)
+    @ApiOperationSupport(order = 4)
     @ApiImplicitParams({
             @ApiImplicitParam(name = "id", value = "活动ID", dataType = "Integer", paramType = "query", required = true)
     })
@@ -98,7 +112,7 @@ public class OperationalActivityController {
     }
 
     @ApiOperation(value = "根据ID获取活动详情", notes = "返回活动详细信息,包括优惠券和代金券的名称、ID、数量等信息")
-    @ApiOperationSupport(order = 4)
+    @ApiOperationSupport(order = 5)
     @ApiImplicitParams({
             @ApiImplicitParam(name = "id", value = "活动ID", dataType = "Integer", paramType = "query", required = true)
     })
@@ -115,7 +129,7 @@ public class OperationalActivityController {
     }
 
     @ApiOperation(value = "根据商户ID获取活动列表", notes = "分页查询活动列表,返回活动基本信息、优惠券和代金券信息、活动图片等")
-    @ApiOperationSupport(order = 5)
+    @ApiOperationSupport(order = 6)
     @ApiImplicitParams({
             @ApiImplicitParam(name = "storeId", value = "商户ID", dataType = "Integer", paramType = "query", required = true)
     })

+ 10 - 1
alien-store-platform/src/main/java/shop/alien/storeplatform/service/OperationalActivityService.java

@@ -15,7 +15,16 @@ import java.util.List;
 public interface OperationalActivityService {
 
     /**
-     * 创建运营活动
+     * AI 生成运营活动海报图片(仅调 AI,不入库)。
+     * 要求 uploadImgType=2,返回的 dto 中 activityTitleImg、activityDetailImg 的 imgUrl 会被填充。
+     *
+     * @param dto 活动文案与图片描述等(与创建时一致,用于拼 prompt)
+     * @return 回填图片 URL 后的 dto
+     */
+    StoreOperationalActivityDTO generateActivityPromotionImages(StoreOperationalActivityDTO dto);
+
+    /**
+     * 创建运营活动(提交保存,不再在本方法内调用 AI 生成图片,请事先调用 generateActivityPromotionImages 或本地上传)。
      *
      * @param dto 活动信息
      * @return 创建结果

+ 59 - 61
alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/OperationalActivityServiceImpl.java

@@ -115,6 +115,58 @@ public class OperationalActivityServiceImpl implements OperationalActivityServic
             + "图片描述:%s";
 
     @Override
+    public StoreOperationalActivityDTO generateActivityPromotionImages(StoreOperationalActivityDTO dto) {
+        log.info("OperationalActivityServiceImpl.generateActivityPromotionImages: dto={}", dto);
+        if (dto.getUploadImgType() == null || dto.getUploadImgType() != 2) {
+            throw new IllegalArgumentException("AI 生成图片需 uploadImgType=2");
+        }
+        if (dto.getActivityTitleImg() == null) {
+            dto.setActivityTitleImg(new StoreImg());
+        }
+        if (dto.getActivityDetailImg() == null) {
+            dto.setActivityDetailImg(new StoreImg());
+        }
+        String accessToken = getToken();
+        if (accessToken == null || accessToken.isEmpty()) {
+            throw new IllegalStateException("获取AI服务access_token失败,无法生成促销图片");
+        }
+        String authorization = "Bearer " + accessToken;
+        String filled = String.format(
+                tpl,
+                dto.getActivityName(),
+                dto.getStartTime(),
+                dto.getEndTime(),
+                dto.getParticipationLimit(),
+                dto.getActivityRule(),
+                dto.getCouponQuantity() != null ? dto.getCouponQuantity() : 0,
+                dto.getVoucherQuantity() != null ? dto.getVoucherQuantity() : 0,
+                dto.getImgDescribe()
+        );
+        JsonNode imgResponse = alienAIFeign.generatePromotionImage(authorization, Collections.singletonMap("text", filled));
+        applyPromotionImageResponse(imgResponse, dto);
+        return dto;
+    }
+
+    private void applyPromotionImageResponse(JsonNode imgResponse, StoreOperationalActivityDTO dto) {
+        if (imgResponse == null || !imgResponse.has("data")) {
+            return;
+        }
+        JsonNode data = imgResponse.get("data");
+        if (data.has("banner_image")) {
+            JsonNode bannerImage = data.get("banner_image");
+            if (bannerImage.has("ali_url") && !bannerImage.get("ali_url").isNull()) {
+                dto.getActivityTitleImg().setImgUrl(bannerImage.get("ali_url").asText());
+            }
+        }
+        if (data.has("vertical_image")) {
+            JsonNode verticalImage = data.get("vertical_image");
+            if (verticalImage.has("ali_url") && !verticalImage.get("ali_url").isNull()) {
+                dto.getActivityDetailImg().setImgUrl(verticalImage.get("ali_url").asText());
+            }
+        }
+    }
+
+    @Override
     public int createActivity(StoreOperationalActivityDTO dto) {
         log.info("OperationalActivityServiceImpl.createActivity: dto={}", dto);
 
@@ -203,44 +255,6 @@ public class OperationalActivityServiceImpl implements OperationalActivityServic
                             log.error("发送活动审核通知失败,activityId={}, error={}", activity.getId(), e.getMessage(), e);
                         }
                     }
-                    // 使用用户描述和页面输入框其他信息,让AI生成海报图片。
-                    if (dto.getUploadImgType() == 2) {
-                        // 格式化输入AI参数
-                        ObjectNode requestBody = objectMapper.createObjectNode();
-                        String filled = String.format(
-                                tpl,
-                                dto.getActivityName(),
-                                dto.getStartTime(),
-                                dto.getEndTime(),
-                                dto.getParticipationLimit(),
-                                dto.getActivityRule(),
-                                dto.getCouponQuantity() != null ? dto.getCouponQuantity() : 0,
-                                dto.getVoucherQuantity() != null ? dto.getVoucherQuantity() : 0,
-                                dto.getImgDescribe()
-                        );
-                        requestBody.put("text", filled);
-                        JsonNode imgResponse = alienAIFeign.generatePromotionImage(authorization, Collections.singletonMap("text", filled));
-                        // 解析响应
-                        if (imgResponse.has("data")) {
-                            JsonNode data = imgResponse.get("data");
-                            // 提取横向图(banner_image)的图片URL
-                            if (data.has("banner_image")) {
-                                JsonNode bannerImage = data.get("banner_image");
-                                if (bannerImage.has("ali_url") && !bannerImage.get("ali_url").isNull()) {
-                                    String bannerImageUrl = bannerImage.get("ali_url").asText();
-                                    dto.getActivityTitleImg().setImgUrl(bannerImageUrl);
-                                }
-                            }
-                            // 提取竖向图(vertical_image)的图片URL
-                            if (data.has("vertical_image")) {
-                                JsonNode verticalImage = data.get("vertical_image");
-                                if (verticalImage.has("ali_url") && !verticalImage.get("ali_url").isNull()) {
-                                    String verticalImageUrl = verticalImage.get("ali_url").asText();
-                                    dto.getActivityDetailImg().setImgUrl(verticalImageUrl);
-                                }
-                            }
-                        }
-                    }
                 }
             } catch (Exception e) {
                 // AI调用失败,也可以添加数据
@@ -367,8 +381,12 @@ public class OperationalActivityServiceImpl implements OperationalActivityServic
                 sendActivityAuditNotice(activity, auditResult.isPassed(), auditResult.getFailureReason());
                 // 使用用户描述和页面输入框其他信息,让AI生成海报图片。
                 if (dto.getUploadImgType() == 2) {
-                    // 格式化输入AI参数
-                    ObjectNode requestBody = objectMapper.createObjectNode();
+                    if (dto.getActivityTitleImg() == null) {
+                        dto.setActivityTitleImg(new StoreImg());
+                    }
+                    if (dto.getActivityDetailImg() == null) {
+                        dto.setActivityDetailImg(new StoreImg());
+                    }
                     String filled = String.format(
                             tpl,
                             dto.getActivityName(),
@@ -380,28 +398,8 @@ public class OperationalActivityServiceImpl implements OperationalActivityServic
                             dto.getVoucherQuantity() != null ? dto.getVoucherQuantity() : 0,
                             dto.getImgDescribe()
                     );
-                    requestBody.put("text", filled);
                     JsonNode imgResponse = alienAIFeign.generatePromotionImage(authorization, Collections.singletonMap("text", filled));
-                    // 解析响应
-                    if (imgResponse.has("data")) {
-                        JsonNode data = imgResponse.get("data");
-                        // 提取横向图(banner_image)的图片URL
-                        if (data.has("banner_image")) {
-                            JsonNode bannerImage = data.get("banner_image");
-                            if (bannerImage.has("ali_url") && !bannerImage.get("ali_url").isNull()) {
-                                String bannerImageUrl = bannerImage.get("ali_url").asText();
-                                dto.getActivityTitleImg().setImgUrl(bannerImageUrl);
-                            }
-                        }
-                        // 提取竖向图(vertical_image)的图片URL
-                        if (data.has("vertical_image")) {
-                            JsonNode verticalImage = data.get("vertical_image");
-                            if (verticalImage.has("ali_url") && !verticalImage.get("ali_url").isNull()) {
-                                String verticalImageUrl = verticalImage.get("ali_url").asText();
-                                dto.getActivityDetailImg().setImgUrl(verticalImageUrl);
-                            }
-                        }
-                    }
+                    applyPromotionImageResponse(imgResponse, dto);
                 }
             }
         } catch (Exception e) {

+ 2 - 1
alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/StoreBusinessServiceImpl.java

@@ -1203,9 +1203,10 @@ public class StoreBusinessServiceImpl extends ServiceImpl<StoreInfoMapper, Store
         List<StoreStaffConfig> storeStaffConfigs = storeStaffConfigMapper.selectList(lambdaQueryWrapper);
         result.setEmployeeList(storeStaffConfigs);
 
-        // 该用户的打卡记录
+        // 该用户的打卡记录(仅审核通过 check_flag=2)
         LambdaQueryWrapper<StoreClockIn> clockInWrapper = new LambdaQueryWrapper<>();
         clockInWrapper.eq(StoreClockIn::getUserId, userId);
+        clockInWrapper.eq(StoreClockIn::getCheckFlag, 2);
         List<StoreClockIn> clockInList = storeClockInMapper.selectList(clockInWrapper);
 
         List<StoreClockIn> clockStoreList = clockInList.stream().filter(item -> item.getStoreId() == Integer.parseInt(storeId)).collect(Collectors.toList());

+ 6 - 2
alien-store/src/main/java/shop/alien/store/controller/AiSearchController.java

@@ -223,9 +223,13 @@ public class AiSearchController {
         // 坑:查询出来的是拉黑的商户id,不是商铺id 😊彻底疯狂
         List<String> blockedIds = lifeBlacklists.stream().map(x -> x.getBlockedId()).collect(Collectors.toList());
         List<Integer> collect = new ArrayList<>();
-        if(blockedIds.size()>0){
+        if (blockedIds.size() > 0) {
             List<StoreUser> storeUsers = storeUserMapper.selectBatchIds(blockedIds);
-            collect = storeUsers.stream().filter(x -> StringUtils.isNotBlank(x.getStoreId().toString())).map(x -> x.getStoreId()).collect(Collectors.toList());
+            // store_id 可能为 null,不可对 null 调用 toString()
+            collect = storeUsers.stream()
+                    .map(StoreUser::getStoreId)
+                    .filter(Objects::nonNull)
+                    .collect(Collectors.toList());
         }
 
         List<StoreInfoVo> storeInfoList = new ArrayList<>();

+ 9 - 1
alien-store/src/main/java/shop/alien/store/controller/StoreInfoController.java

@@ -1891,7 +1891,15 @@ public class StoreInfoController {
     private boolean hasStaffData(Integer storeId) {
         try {
             List<StaffTitleGroupVo> groups = storeStaffConfigService.queryStaffListByTitle(storeId);
-            return CollectionUtils.isNotEmpty(groups);
+            if (CollectionUtils.isEmpty(groups)) {
+                return false;
+            }
+            for (StaffTitleGroupVo group : groups) {
+                if (group != null && CollectionUtils.isNotEmpty(group.getStaffList())) {
+                    return true;
+                }
+            }
+            return false;
         } catch (Exception e) {
             log.warn("查询人员数据异常 storeId={}, {}", storeId, e.getMessage());
             return false;

+ 2 - 1
alien-store/src/main/java/shop/alien/store/service/LifeUserStoreService.java

@@ -419,9 +419,10 @@ public class LifeUserStoreService {
         // 员工
         returnMap.put("employeeList", employeeList);
 
-        // 该用户的打卡记录
+        // 该用户的打卡记录(仅审核通过 check_flag=2)
         LambdaQueryWrapper<StoreClockIn> clockInWrapper = new LambdaQueryWrapper<>();
         clockInWrapper.eq(StoreClockIn::getUserId, userId);
+        clockInWrapper.eq(StoreClockIn::getCheckFlag, 2);
         List<StoreClockIn> clockInList = storeClockInMapper.selectList(clockInWrapper);
 
         List<StoreClockIn> clockStoreList = clockInList.stream().filter(item -> item.getStoreId() == Integer.parseInt(storeId)).collect(Collectors.toList());

+ 122 - 37
alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java

@@ -47,6 +47,7 @@ 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.JwtUtil;
 import shop.alien.util.common.constant.CouponStatusEnum;
 import shop.alien.util.common.constant.CouponTypeEnum;
 import shop.alien.util.common.constant.OcrTypeEnum;
@@ -2031,9 +2032,10 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
         List<StoreStaffConfig> storeStaffConfigs = storeStaffConfigMapper.selectList(lambdaQueryWrapper);
         result.setEmployeeList(storeStaffConfigs);
 
-        // 该用户的打卡记录
+        // 该用户的打卡记录(仅审核通过 check_flag=2,未审核/审核中/拒绝均不计入)
         LambdaQueryWrapper<StoreClockIn> clockInWrapper = new LambdaQueryWrapper<>();
         clockInWrapper.eq(StoreClockIn::getUserId, userId);
+        clockInWrapper.eq(StoreClockIn::getCheckFlag, 2);
         List<StoreClockIn> clockInList = storeClockInMapper.selectList(clockInWrapper);
 
         List<StoreClockIn> clockStoreList = clockInList.stream().filter(item -> item.getStoreId() == Integer.parseInt(storeId)).collect(Collectors.toList());
@@ -5071,9 +5073,6 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
             return Collections.emptyList();
         }
 
-        QueryWrapper<StoreInfoVo> queryWrapper = new QueryWrapper<>();
-        queryWrapper.eq("a.delete_flag", 0).eq("b.delete_flag", 0);
-        //如果查询未过期
         // 获取当前时刻
         Date currentDate = new Date();
         // 获取当前日期和时间
@@ -5085,49 +5084,134 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
         calendar.set(Calendar.MILLISECOND, 0);
         // 加上 30 天
         calendar.add(Calendar.DAY_OF_MONTH, 30);
-        // 如果 expiration_time 为空则不做过期判断;如果不为空则要求大于当前时间
-        queryWrapper.and(w -> w.isNull("a.expiration_time")
-                .or()
-                .gt("a.expiration_time", currentDate));
-
-        // 如果 food_licence_expiration_time 为空则不做过期判断;如果不为空则要求大于当前时间
-        queryWrapper.and(w -> w.isNull("a.food_licence_expiration_time")
-                .or()
-                .gt("a.food_licence_expiration_time", currentDate));
+        // 构建position参数(格式:经度,纬度)
+        String position = lon + "," + lat;
 
-        // 构建一级分类
-        if (StringUtils.isNotEmpty(businessSection)) {
-            queryWrapper.eq("a.business_section", businessSection);
-            // 构建二级分类
-            if (StringUtils.isNotEmpty(businessTypes)) {
-                queryWrapper.eq("a.business_types", businessTypes);
-                // 构建三级分类
-                if (StringUtils.isNotEmpty(businessClassify)) {
-                    // 解析businessClassify参数(格式:1,2,3)
-                    String[] classifyArray = businessClassify.split(",");
-                    // 使用FIND_IN_SET函数检查数据库字段是否包含参数中的任何一个值
-                    queryWrapper.and(wrapper -> {
-                        for (int i = 0; i < classifyArray.length; i++) {
-                            String classify = classifyArray[i].trim();
-                            if (StringUtils.isNotEmpty(classify)) {
-                                if (i == 0) {
-                                    wrapper.apply("FIND_IN_SET({0}, a.business_classify) > 0", classify);
-                                } else {
-                                    wrapper.or().apply("FIND_IN_SET({0}, a.business_classify) > 0", classify);
+        // 先查询当前用户好友店铺ID(互相关注的商户)
+        Set<Integer> friendStoreIds = new HashSet<>();
+        try {
+            // 从上下文获取当前登录用户ID -> 查询手机号 -> 组装粉丝ID
+            JSONObject currentUserInfo = JwtUtil.getCurrentUserInfo();
+            if (currentUserInfo != null) {
+                Integer currentUserId = currentUserInfo.getInteger("userId");
+                if (currentUserId != null) {
+                    LifeUser currentUser = lifeUserMapper.selectById(currentUserId);
+                    if (currentUser != null && StringUtils.isNotEmpty(currentUser.getUserPhone())) {
+                        String myFansId = "user_" + currentUser.getUserPhone();
+
+                        // 我关注的店铺
+                        LambdaQueryWrapper<LifeFans> myFollowWrapper = new LambdaQueryWrapper<>();
+                        myFollowWrapper.eq(LifeFans::getFansId, myFansId)
+                                .likeRight(LifeFans::getFollowedId, "store_")
+                                .eq(LifeFans::getDeleteFlag, 0);
+                        List<LifeFans> myFollows = lifeFansMapper.selectList(myFollowWrapper);
+
+                        // 过滤出互相关注(店铺也关注我)的店铺phoneId:store_xxx
+                        Set<String> mutualStorePhones = new HashSet<>();
+                        if (!CollectionUtils.isEmpty(myFollows)) {
+                            List<String> followedStoreIds = myFollows.stream()
+                                    .map(LifeFans::getFollowedId)
+                                    .filter(Objects::nonNull)
+                                    .collect(Collectors.toList());
+
+                            if (!followedStoreIds.isEmpty()) {
+                                // 查询这些店铺是否关注我
+                                LambdaQueryWrapper<LifeFans> reciprocalWrapper = new LambdaQueryWrapper<>();
+                                reciprocalWrapper.in(LifeFans::getFansId, followedStoreIds)
+                                        .eq(LifeFans::getFollowedId, myFansId)
+                                        .eq(LifeFans::getDeleteFlag, 0);
+                                List<LifeFans> reciprocals = lifeFansMapper.selectList(reciprocalWrapper);
+                                Set<String> reciprocalFansIds = reciprocals.stream()
+                                        .map(LifeFans::getFansId)
+                                        .filter(Objects::nonNull)
+                                        .collect(Collectors.toSet());
+
+                                // 互相关注的店铺 phoneId(store_开头)
+                                for (String followedId : followedStoreIds) {
+                                    if (reciprocalFansIds.contains(followedId)) {
+                                        mutualStorePhones.add(followedId);
+                                    }
                                 }
                             }
                         }
-                    });
+
+                        if (!mutualStorePhones.isEmpty()) {
+                            // 提取店铺手机号
+                            Set<String> storePhones = mutualStorePhones.stream()
+                                    .map(pid -> {
+                                        int idx = pid.indexOf('_');
+                                        return idx != -1 ? pid.substring(idx + 1) : pid;
+                                    })
+                                    .collect(Collectors.toSet());
+
+                            // 根据店铺手机号查询 store_user,提取 store_id
+                            LambdaQueryWrapper<StoreUser> storeUserWrapper = new LambdaQueryWrapper<>();
+                            storeUserWrapper.in(StoreUser::getPhone, storePhones)
+                                    .eq(StoreUser::getDeleteFlag, 0);
+                            List<StoreUser> storeUsers = storeUserMapper.selectList(storeUserWrapper);
+                            friendStoreIds = storeUsers.stream()
+                                    .map(StoreUser::getStoreId)
+                                    .filter(Objects::nonNull)
+                                    .collect(Collectors.toSet());
+                        }
+                    }
                 }
             }
+        } catch (Exception e) {
+            // 好友优先不影响主流程
+            log.warn("更多推荐-好友店铺优先排序失败,忽略该步骤。error={}", e.getMessage());
         }
 
-        // 构建position参数(格式:经度,纬度)
-        String position = lon + "," + lat;
-        List<StoreInfoVo> storeInfoVoList = storeInfoMapper.getMoreRecommendedStores(queryWrapper, position);
+        // 查询好友店铺(不限制 business_section)
+        List<StoreInfoVo> friendStoreList = Collections.emptyList();
+        if (!friendStoreIds.isEmpty()) {
+            QueryWrapper<StoreInfoVo> friendQueryWrapper = new QueryWrapper<>();
+            friendQueryWrapper.eq("a.delete_flag", 0).eq("b.delete_flag", 0);
+            friendQueryWrapper.and(w -> w.isNull("a.expiration_time")
+                    .or()
+                    .gt("a.expiration_time", currentDate));
+            friendQueryWrapper.and(w -> w.isNull("a.food_licence_expiration_time")
+                    .or()
+                    .gt("a.food_licence_expiration_time", currentDate));
+            friendQueryWrapper.in("a.id", friendStoreIds);
+            friendStoreList = storeInfoMapper.getMoreRecommendedStores(friendQueryWrapper, position);
+        }
+
+        // 查询当前类型店铺(仅按 business_section)
+        QueryWrapper<StoreInfoVo> currentTypeQueryWrapper = new QueryWrapper<>();
+        currentTypeQueryWrapper.eq("a.delete_flag", 0).eq("b.delete_flag", 0);
+        currentTypeQueryWrapper.and(w -> w.isNull("a.expiration_time")
+                .or()
+                .gt("a.expiration_time", currentDate));
+        currentTypeQueryWrapper.and(w -> w.isNull("a.food_licence_expiration_time")
+                .or()
+                .gt("a.food_licence_expiration_time", currentDate));
+        if (StringUtils.isNotEmpty(businessSection)) {
+            currentTypeQueryWrapper.eq("a.business_section", businessSection);
+        }
+        List<StoreInfoVo> currentTypeStoreList = storeInfoMapper.getMoreRecommendedStores(currentTypeQueryWrapper, position);
+
+        // 合并:好友店铺在前,当前类型店铺在后;按店铺ID去重
+        Map<Integer, StoreInfoVo> mergedMap = new LinkedHashMap<>();
+        if (!CollectionUtils.isEmpty(friendStoreList)) {
+            for (StoreInfoVo vo : friendStoreList) {
+                mergedMap.put(vo.getId(), vo);
+            }
+        }
+        if (!CollectionUtils.isEmpty(currentTypeStoreList)) {
+            for (StoreInfoVo vo : currentTypeStoreList) {
+                mergedMap.putIfAbsent(vo.getId(), vo);
+            }
+        }
+        List<StoreInfoVo> storeInfoVoList = new ArrayList<>(mergedMap.values());
         if (CollectionUtils.isEmpty(storeInfoVoList)) {
             return Collections.emptyList();
         }
+
+        // 好友店铺 + 当前类型店铺,最多回显30个
+        if (storeInfoVoList.size() > 30) {
+            storeInfoVoList = new ArrayList<>(storeInfoVoList.subList(0, 30));
+        }
         // 提前查询所有需要的字典数据
         List<StoreInfoVo> collect = storeInfoVoList.stream().filter(record -> StringUtils.isNotEmpty(record.getStoreType())).collect(Collectors.toList());
         Set<String> allTypes = collect.stream().map(StoreInfoVo::getStoreType).flatMap(type -> Arrays.stream(type.split(","))).collect(Collectors.toSet());
@@ -5992,9 +6076,10 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
             result.setIsFollowed(0); // 用户ID或店铺信息为空,默认未关注
         }
 
-        // 该用户的打卡记录
+        // 该用户的打卡记录(仅审核通过 check_flag=2)
         LambdaQueryWrapper<StoreClockIn> clockInWrapper = new LambdaQueryWrapper<>();
         clockInWrapper.eq(StoreClockIn::getUserId, userId);
+        clockInWrapper.eq(StoreClockIn::getCheckFlag, 2);
         List<StoreClockIn> clockInList = storeClockInMapper.selectList(clockInWrapper);
 
         List<StoreClockIn> clockStoreList = clockInList.stream().filter(item -> item.getStoreId() == Integer.parseInt(storeId)).collect(Collectors.toList());

+ 7 - 1
alien-store/src/main/java/shop/alien/store/service/impl/UserReservationServiceImpl.java

@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.BeanUtils;
 import org.springframework.context.annotation.Lazy;
@@ -1158,7 +1159,12 @@ public class UserReservationServiceImpl extends ServiceImpl<UserReservationMappe
         if (order.getReservationId() != null) {
             UserReservation r = this.getById(order.getReservationId());
             if (r != null) {
-                vo.setMerchantCancelReason(r.getReason());
+                String reservationReason = r.getReason();
+                vo.setMerchantCancelReason(reservationReason);
+                // 与 merchantCancelReason 同源:订单表未落库退款原因时,用预约单原因展示(如商家取消)
+                if (StringUtils.isBlank(order.getRefundReason()) && StringUtils.isNotBlank(reservationReason)) {
+                    order.setRefundReason(reservationReason);
+                }
             }
         }
         return vo;