ソースを参照

Merge branch 'sit' of http://8.152.195.41:3000/alien/alien_cloud into sit

dujian 1 ヶ月 前
コミット
115fa3980e

+ 8 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/LifeDiscountCouponFriendRuleVo.java

@@ -64,5 +64,13 @@ public class LifeDiscountCouponFriendRuleVo extends LifeDiscountCouponFriendRule
     @ApiModelProperty(value = "折扣率(0-100,用于折扣券,例如80表示8折,仅折扣券有值)")
     private BigDecimal discountRate;
 
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    @ApiModelProperty(value = "开始领取时间(仅优惠券有值,代金券为null)")
+    private Date beginGetDate;
+
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    @ApiModelProperty(value = "结束领取时间(仅优惠券有值,代金券为null)")
+    private Date endGetDate;
+
     private List<LifeDiscountCouponFriendRuleDetailVo> lifeDiscountCouponFriendRuleDetailVos;
 }

+ 12 - 4
alien-entity/src/main/java/shop/alien/mapper/LifeDiscountCouponStoreFriendMapper.java

@@ -45,7 +45,9 @@ public interface LifeDiscountCouponStoreFriendMapper extends BaseMapper<LifeDisc
             "1 as type,\n" +
             "null as voucherId,\n" +
             "ldc.coupon_type couponType,\n" +
-            "ldc.discount_rate discountRate\n" +
+            "ldc.discount_rate discountRate,\n" +
+            "ldc.begin_get_date beginGetDate,\n" +
+            "ldc.end_get_date endGetDate\n" +
             "from  life_discount_coupon_store_friend ldcsf\n" +
             "left join life_discount_coupon ldc\n" +
             "on ldc.id = ldcsf.coupon_id and ldc.delete_flag = 0\n" +
@@ -62,7 +64,9 @@ public interface LifeDiscountCouponStoreFriendMapper extends BaseMapper<LifeDisc
             "null couponId,\n" +
             "ldcsf.single_qty couponNum,\n" +
             "4 as type,\n" +
-            "lc.id voucherId\n" +
+            "lc.id voucherId,\n" +
+            "null as beginGetDate,\n" +
+            "null as endGetDate\n" +
             "from life_discount_coupon_store_friend ldcsf\n" +
             "left join life_coupon lc on lc.id = ldcsf.voucher_id and lc.delete_flag = 0\n" +
             "left join store_info si on CAST(si.id AS CHAR) COLLATE utf8mb4_unicode_ci = lc.store_id and si.delete_flag = 0\n" +
@@ -79,7 +83,9 @@ public interface LifeDiscountCouponStoreFriendMapper extends BaseMapper<LifeDisc
             "1 as type,\n" +
             "null as voucherId,\n" +
             "ldc.coupon_type couponType,\n" +
-            "ldc.discount_rate discountRate\n" +
+            "ldc.discount_rate discountRate,\n" +
+            "ldc.begin_get_date beginGetDate,\n" +
+            "ldc.end_get_date endGetDate\n" +
             "from  life_discount_coupon_store_friend ldcsf\n" +
             "left join life_discount_coupon ldc\n" +
             "on ldc.id = ldcsf.coupon_id and ldc.delete_flag = 0\n" +
@@ -98,7 +104,9 @@ public interface LifeDiscountCouponStoreFriendMapper extends BaseMapper<LifeDisc
             "null couponId,\n" +
             "ldcsf.single_qty couponNum,\n" +
             "4 as type,\n" +
-            "lc.id voucherId\n" +
+            "lc.id voucherId,\n" +
+            "null as beginGetDate,\n" +
+            "null as endGetDate\n" +
             "from life_discount_coupon_store_friend ldcsf\n" +
             "left join life_coupon lc on lc.id = ldcsf.voucher_id and lc.delete_flag = 0\n" +
             "left join store_user su\n" +

+ 43 - 2
alien-second/src/main/java/shop/alien/second/service/impl/SecondGoodsServiceImpl.java

@@ -32,6 +32,7 @@ import shop.alien.mapper.*;
 import shop.alien.mapper.second.*;
 import shop.alien.second.feign.AlienStoreFeign;
 import shop.alien.second.service.*;
+import shop.alien.second.util.SecondHandSearchAiUtils;
 import shop.alien.util.common.Constants;
 import shop.alien.util.common.VideoUtils;
 import shop.alien.util.common.safe.*;
@@ -156,6 +157,11 @@ public class SecondGoodsServiceImpl extends ServiceImpl<SecondGoodsMapper, Secon
     private final PlatformSecondTradeService platformSecondTradeService;
 
     /**
+     * 二手搜索 AI 接口工具,用于调用 life-manager 智能搜索
+     */
+    private final SecondHandSearchAiUtils secondHandSearchAiUtils;
+
+    /**
      * 风控配置属性,包含各种风控规则的配置参数
      */
     @Autowired
@@ -1433,8 +1439,32 @@ public class SecondGoodsServiceImpl extends ServiceImpl<SecondGoodsMapper, Secon
         secondGoodsVo.setShieldedGoodsIds(shieldedGoodsIds);
         secondGoodsVo.setUserIdList(userIdList);
 
-        // 获取搜索结果分页列表
-        IPage<SecondGoodsVo> searchGoodsList = getSecondGoodsVoIPage(page, secondGoodsVo, shieldedGoodsIds, userIdList);
+        // 优先调用 AI 二手搜索接口;有搜索词且 AI 返回有效结果时直接使用 AI 数据并处理成对应格式,否则走本地分页查询
+        IPage<SecondGoodsVo> searchGoodsList = null;
+        if (!StringUtils.isEmpty(secondGoodsVo.getSearchData())) {
+            String releaseTimeStr = null;
+            if (secondGoodsVo.getReleaseTime() != null) {
+                releaseTimeStr = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").format(secondGoodsVo.getReleaseTime());
+            }
+            SecondHandSearchAiUtils.SecondHandSearchResult aiResult = secondHandSearchAiUtils.search(
+                    secondGoodsVo.getSearchData(),
+                    secondGoodsVo.getCurrentLatitude(),
+                    secondGoodsVo.getCurrentLongitude(),
+                    (int) page.getCurrent(),
+                    (int) page.getSize(),
+                    secondGoodsVo.getOrderType(),
+                    releaseTimeStr,
+                    shieldedGoodsIds,
+                    userIdList);
+            if (aiResult != null && CollectionUtil.isNotEmpty(aiResult.getRecords())) {
+                searchGoodsList = buildPageFromAiResult(page, aiResult);
+            }
+        }
+        if (searchGoodsList == null) {
+//            log.info("使用本地查询");
+//            searchGoodsList = getSecondGoodsVoIPage(page, secondGoodsVo, shieldedGoodsIds, userIdList);
+            return searchGoodsList = new Page<SecondGoodsVo>();
+        }
 
         // 判空
         if (CollectionUtil.isNotEmpty(searchGoodsList.getRecords())) {
@@ -1451,6 +1481,17 @@ public class SecondGoodsServiceImpl extends ServiceImpl<SecondGoodsMapper, Secon
     }
 
     /**
+     * 将 AI 搜索返回的 SecondGoodsVo 列表组装为分页结果(AI 已按 shieldedGoodsIds、userIdList 过滤,此处仅封装分页)
+     */
+    private IPage<SecondGoodsVo> buildPageFromAiResult(IPage<SecondGoodsVo> page,
+                                                       SecondHandSearchAiUtils.SecondHandSearchResult aiResult) {
+        Page<SecondGoodsVo> resultPage = new Page<>(page.getCurrent(), page.getSize());
+        resultPage.setRecords(aiResult.getRecords() != null ? aiResult.getRecords() : Collections.emptyList());
+        resultPage.setTotal(aiResult.getTotal());
+        return resultPage;
+    }
+
+    /**
      * 查询搜索结果
      * @param page 分页参数
      * @param secondGoodsVo 查询参数

+ 208 - 0
alien-second/src/main/java/shop/alien/second/util/SecondHandSearchAiUtils.java

@@ -0,0 +1,208 @@
+package shop.alien.second.util;
+
+import com.alibaba.fastjson2.JSONArray;
+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.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+import shop.alien.entity.second.vo.SecondGoodsVo;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 二手商品搜索 AI 接口调用工具
+ * 调用 life-manager 的 second_hand/search 接口进行智能搜索
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+@RefreshScope
+public class SecondHandSearchAiUtils {
+
+    private final RestTemplate restTemplate;
+    private final AiTaskUtils aiTaskUtils;
+
+    @Value("${second-hand-search.base-url:http://124.93.18.180:9000/ai/life-manager/api/v1/second_hand/search}")
+    private String secondHandSearchUrl;
+
+    /**
+     * 调用 AI 二手搜索接口(先获取 token 再请求)
+     *
+     * @param query            搜索关键词,如 "我想要二手手机"
+     * @param latitude         纬度
+     * @param longitude        经度
+     * @param page             页码,从 1 开始
+     * @param pageSize         每页条数
+     * @param distanceOrder    距离排序,如 1
+     * @param releaseTime      发布时间,ISO 格式如 "2025-12-01T00:00:00",可为 null
+     * @param shieldedGoodsIds 屏蔽商品 id 列表
+     * @param userIdList       拉黑用户 id 列表
+     * @return 搜索结果的 SecondGoodsVo 列表与总数;若调用失败返回 null
+     */
+    public SecondHandSearchResult search(String query, Double latitude, Double longitude, int page, int pageSize,
+                                         Integer distanceOrder, String releaseTime,
+                                         List<Integer> shieldedGoodsIds, List<Integer> userIdList) {
+        if (query == null) {
+            query = "";
+        }
+        if (latitude == null) {
+            latitude = 39.914885;
+        }
+        if (longitude == null) {
+            longitude = 116.403874;
+        }
+
+        String token = aiTaskUtils.getAccessToken();
+        if (token == null) {
+            log.warn("获取 AI 服务 token 失败,无法调用二手搜索接口");
+            return null;
+        }
+
+        log.info("调用 AI 二手搜索接口, url={}, query={}, page={}, pageSize={}", secondHandSearchUrl, query, page, pageSize);
+
+        Map<String, Object> body = new HashMap<>();
+        body.put("query", query);
+        body.put("latitude", latitude);
+        body.put("longitude", longitude);
+        body.put("page", page);
+        body.put("page_size", pageSize);
+        if (distanceOrder != null) {
+            body.put("distance_order", distanceOrder);
+        }
+        if (releaseTime != null && !releaseTime.isEmpty()) {
+            body.put("release_time", releaseTime);
+        }
+        body.put("shieldedGoodsIds", shieldedGoodsIds != null ? shieldedGoodsIds : new ArrayList<>());
+        body.put("userIdList", userIdList != null ? userIdList : new ArrayList<>());
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        headers.set("Authorization", "Bearer " + token);
+        HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(body, headers);
+
+        try {
+            ResponseEntity<String> response = restTemplate.postForEntity(secondHandSearchUrl, requestEntity, String.class);
+            if (response.getStatusCode() != HttpStatus.OK || response.getBody() == null) {
+                log.warn("AI 二手搜索接口返回异常, status={}", response.getStatusCode());
+                return null;
+            }
+            return parseSearchResponse(response.getBody());
+        } catch (Exception e) {
+            log.error("调用 AI 二手搜索接口失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 解析 AI 接口返回体,将 list 中每项转为 SecondGoodsVo
+     * 兼容常见格式:data.list[]、data.items[],字段支持 id/title/price 等及下划线命名
+     */
+    private SecondHandSearchResult parseSearchResponse(String responseBody) {
+        try {
+            JSONObject root = JSONObject.parseObject(responseBody);
+            JSONObject data = root.getJSONObject("data");
+            if (data == null) {
+                data = root;
+            }
+            long total = data.getLongValue("total");
+            JSONArray list = data.getJSONArray("list");
+            if (list == null) {
+                list = data.getJSONArray("items");
+            }
+            List<SecondGoodsVo> records = new ArrayList<>();
+            if (list != null && !list.isEmpty()) {
+                for (int i = 0; i < list.size(); i++) {
+                    Object item = list.get(i);
+                    if (item instanceof JSONObject) {
+                        SecondGoodsVo vo = jsonItemToSecondGoodsVo((JSONObject) item);
+                        if (vo != null) {
+                            records.add(vo);
+                        }
+                    }
+                }
+            }
+            return new SecondHandSearchResult(records, total);
+        } catch (Exception e) {
+            log.warn("解析 AI 二手搜索返回失败, body={}", responseBody, e);
+            return null;
+        }
+    }
+
+    /**
+     * 将 AI 返回的单条 JSON 转为 SecondGoodsVo(兼容 id/user_id/title/price 等及下划线命名)
+     */
+    private SecondGoodsVo jsonItemToSecondGoodsVo(JSONObject o) {
+        Integer id = o.getInteger("id");
+        if (id == null) {
+            return null;
+        }
+        SecondGoodsVo vo = new SecondGoodsVo();
+        vo.setId(id);
+        vo.setUserId(o.getInteger("user_id") != null ? o.getInteger("user_id") : o.getInteger("userId"));
+        vo.setTitle(o.getString("title"));
+        vo.setHomeImage(o.getString("home_image") != null ? o.getString("home_image") : o.getString("homeImage"));
+        vo.setDescription(o.getString("description"));
+        String priceStr = o.getString("price");
+        if (priceStr != null) {
+            vo.setPrice(priceStr);
+        }
+        if (o.get("price") instanceof Number) {
+            vo.setAmount(o.getBigDecimal("price"));
+        }
+        vo.setPosition(o.getString("position"));
+        vo.setAddressText(o.getString("address_text") != null ? o.getString("address_text") : o.getString("addressText"));
+        vo.setLikeCount(o.getInteger("like_count") != null ? o.getInteger("like_count") : o.getInteger("likeCount"));
+        vo.setCollectCount(o.getInteger("collect_count") != null ? o.getInteger("collect_count") : o.getInteger("collectCount"));
+        vo.setCategoryOneId(o.getInteger("category_one_id") != null ? o.getInteger("category_one_id") : o.getInteger("categoryOneId"));
+        vo.setCategoryTwoId(o.getInteger("category_two_id") != null ? o.getInteger("category_two_id") : o.getInteger("categoryTwoId"));
+        vo.setLabel(o.getString("label"));
+        vo.setTopic(o.getString("topic"));
+        vo.setGoodsStatus(o.getInteger("goods_status") != null ? o.getInteger("goods_status") : o.getInteger("goodsStatus"));
+        vo.setReleaseTime(o.getDate("release_time") != null ? o.getDate("release_time") : o.getDate("releaseTime"));
+        vo.setCreatedTime(o.getDate("created_time") != null ? o.getDate("created_time") : o.getDate("createdTime"));
+        if (o.get("distance") != null) {
+            vo.setDistance(o.get("distance") instanceof Number ? String.valueOf(o.getDouble("distance")) : o.getString("distance"));
+        }
+        if (o.getJSONArray("image_urls") != null) {
+            vo.setImageUrls(o.getJSONArray("image_urls").toJavaList(String.class));
+        } else if (o.getJSONArray("imageUrls") != null) {
+            vo.setImageUrls(o.getJSONArray("imageUrls").toJavaList(String.class));
+        } else if (o.getJSONArray("img_url") != null) {
+            vo.setImgUrl(o.getJSONArray("img_url").toJavaList(String.class));
+        }
+        vo.setUserName(o.getString("user_name") != null ? o.getString("user_name") : o.getString("userName"));
+        vo.setUserPhone(o.getString("user_phone") != null ? o.getString("user_phone") : o.getString("userPhone"));
+        vo.setUserImage(o.getString("user_image") != null ? o.getString("user_image") : o.getString("userImage"));
+        vo.setCategoryOneName(o.getString("category_one_name") != null ? o.getString("category_one_name") : o.getString("categoryOneName"));
+        vo.setCategoryTwoName(o.getString("category_two_name") != null ? o.getString("category_two_name") : o.getString("categoryTwoName"));
+        return vo;
+    }
+
+    /**
+     * AI 搜索返回结果:当前页 SecondGoodsVo 列表 + 总条数
+     */
+    public static class SecondHandSearchResult {
+        private final List<SecondGoodsVo> records;
+        private final long total;
+
+        public SecondHandSearchResult(List<SecondGoodsVo> records, long total) {
+            this.records = records != null ? records : new ArrayList<>();
+            this.total = total;
+        }
+
+        public List<SecondGoodsVo> getRecords() {
+            return records;
+        }
+
+        public long getTotal() {
+            return total;
+        }
+    }
+}

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

@@ -319,7 +319,7 @@ public class OperationalActivityServiceImpl implements OperationalActivityServic
                 }
 
                 String auditText = (auditParam != null && auditParam.has("text")) ? auditParam.get("text").asText() : "";
-                JsonNode imagesNode = (auditParam != null) ? auditParam.get("ali_urls") : null;
+                JsonNode imagesNode = (auditParam != null) ? auditParam.get("image_urls") : null;
 
                 List<String> imageUrls = (imagesNode != null && imagesNode.isArray())
                         ? StreamSupport.stream(imagesNode.spliterator(), false)

+ 7 - 4
alien-store/src/main/java/shop/alien/store/service/impl/CommonRatingServiceImpl.java

@@ -166,22 +166,25 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
             boolean condition2Passed = false; // 条件2:有小票
             String auditReason = null; // 审核原因
             
-            // 检查条件1:用户是否在该店铺打过卡
+            // 检查条件1:用户是否在该店铺打过卡(只统计审核通过的打卡)
+            // checkFlag: 0-未审核, 1-审核中, 2-审核通过, 3-审核拒绝
+            // 只有审核通过的打卡(checkFlag=2)才算成功打卡
             try {
                 LambdaQueryWrapper<StoreClockIn> clockInWrapper = new LambdaQueryWrapper<>();
                 clockInWrapper.eq(StoreClockIn::getUserId, commonRating.getUserId())
                         .eq(StoreClockIn::getStoreId, commonRating.getBusinessId())
                         .eq(StoreClockIn::getDeleteFlag, 0)
+                        .eq(StoreClockIn::getCheckFlag, 2) // 只统计审核通过的打卡(2-审核通过)
                         .last("LIMIT 1");
                 Integer clockInCount = storeClockInMapper.selectCount(clockInWrapper);
                 condition1Passed = clockInCount != null && clockInCount > 0;
                 if (condition1Passed) {
-                    log.info("评论审核条件1通过:用户在该店铺打卡,userId={}, storeId={}", 
+                    log.info("评论审核条件1通过:用户在该店铺有审核通过的打卡记录,userId={}, storeId={}", 
                             commonRating.getUserId(), commonRating.getBusinessId());
                 } else {
                     // 打卡审核不通过,记录审核原因
-                    auditReason = "用户未在该店铺打过卡";
-                    log.info("评论审核条件1未通过:用户未在该店铺打卡,userId={}, storeId={}", 
+                    auditReason = "用户未在该店铺打过卡或打卡审核未通过";
+                    log.info("评论审核条件1未通过:用户未在该店铺有审核通过的打卡记录,userId={}, storeId={}", 
                             commonRating.getUserId(), commonRating.getBusinessId());
                 }
             } catch (Exception e) {

+ 84 - 23
alien-store/src/main/java/shop/alien/store/service/impl/LifeDiscountCouponServiceImpl.java

@@ -4,6 +4,7 @@ import com.alibaba.nacos.common.utils.CollectionUtils;
 import com.aliyun.tea.utils.StringUtils;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -18,10 +19,12 @@ import shop.alien.entity.store.vo.LifeDiscountCouponStoreFriendVo;
 import shop.alien.entity.store.vo.LifeDiscountCouponUserWebVo;
 import shop.alien.entity.store.vo.LifeDiscountCouponVo;
 import shop.alien.mapper.*;
+import shop.alien.store.config.BaseRedisService;
 import shop.alien.store.service.LifeDiscountCouponQuantumRulesService;
 import shop.alien.store.service.LifeDiscountCouponService;
 import shop.alien.store.service.LifeDiscountCouponUserService;
 import shop.alien.util.common.constant.DiscountCouponEnum;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
 import java.text.SimpleDateFormat;
@@ -72,6 +75,8 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
 
     private final LifeCollectMapper lifeCollectMapper;
 
+    private final BaseRedisService baseRedisService;
+
     @Override
     public boolean addDiscountCoupon(LifeDiscountCouponDto lifeDiscountCouponDto) {
         try {
@@ -1855,21 +1860,23 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
      * @return 发放的优惠券数量
      */
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public int issueCouponsForStoreCollect(Integer userId, String storeId, String collectId) {
         if (userId == null || StringUtils.isEmpty(storeId)) {
             return 0;
         }
 
-        // 如果提供了收藏记录ID,检查是否已发放过
-        if (!StringUtils.isEmpty(collectId)) {
-            LifeCollect lifeCollect = lifeCollectMapper.selectById(collectId);
-            if (lifeCollect != null && lifeCollect.getCouponIssuedFlag() != null && lifeCollect.getCouponIssuedFlag() == 1) {
-                // 该收藏记录已发放过优惠券,不再重复发放
+        // 分布式锁:防止并发重复发放
+        String lockKey = "coupon:collect:" + userId + ":" + storeId;
+        String identifier = null;
+        try {
+            // 获取分布式锁,锁超时时间30秒,获取锁超时时间5秒
+            identifier = baseRedisService.lock(lockKey, 30000, 5000);
+            if (StringUtils.isEmpty(identifier)) {
+                // 获取锁失败,可能正在处理中,直接返回0避免重复发放
                 return 0;
             }
-        }
 
-        try {
             LocalDate now = LocalDate.now();
             
             // 查询该店铺所有开启领取的优惠券(有效期内、有库存、未删除、收藏可领)
@@ -1903,27 +1910,50 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
                 return 0;
             }
 
-            // 检查用户已领取的优惠券ID列表(避免重复发放)
+            // 查询用户已通过收藏店铺领取的优惠券(只检查 issueSource=2,不检查其他来源)
+            // 业务规则:好评赠券和收藏领券不冲突,可以分别领取
+            // 因此只检查是否通过收藏领取过,不检查好评等其他方式
+            // 获取所有可发放的优惠券ID列表
+            List<Integer> availableCouponIds = couponByType.values().stream()
+                    .map(LifeDiscountCoupon::getId)
+                    .collect(Collectors.toList());
+            
             LambdaQueryWrapper<LifeDiscountCouponUser> userCouponWrapper = new LambdaQueryWrapper<>();
             userCouponWrapper.eq(LifeDiscountCouponUser::getUserId, userId)
                     .eq(LifeDiscountCouponUser::getDeleteFlag, 0)
-                    .in(LifeDiscountCouponUser::getCouponId, 
-                        couponByType.values().stream().map(LifeDiscountCoupon::getId).collect(Collectors.toList()));
+                    .eq(LifeDiscountCouponUser::getIssueSource, 2) // 只检查收藏店铺领取的(issueSource=2)
+                    .in(LifeDiscountCouponUser::getCouponId, availableCouponIds);
             List<LifeDiscountCouponUser> existingCoupons = lifeDiscountCouponUserMapper.selectList(userCouponWrapper);
             Set<Integer> existingCouponIds = existingCoupons.stream()
                     .map(LifeDiscountCouponUser::getCouponId)
                     .filter(Objects::nonNull)
                     .collect(Collectors.toSet());
+            
+            // 通过已领取的优惠券ID查询对应的优惠券类型,建立类型到已领取优惠券的映射
+            final Set<Integer> existingCouponTypes;
+            if (!existingCouponIds.isEmpty()) {
+                LambdaQueryWrapper<LifeDiscountCoupon> existingCouponQuery = new LambdaQueryWrapper<>();
+                existingCouponQuery.in(LifeDiscountCoupon::getId, existingCouponIds)
+                        .isNotNull(LifeDiscountCoupon::getCouponType);
+                List<LifeDiscountCoupon> existingCouponList = lifeDiscountCouponMapper.selectList(existingCouponQuery);
+                existingCouponTypes = existingCouponList.stream()
+                        .map(LifeDiscountCoupon::getCouponType)
+                        .filter(Objects::nonNull)
+                        .collect(Collectors.toSet());
+            } else {
+                existingCouponTypes = new HashSet<>();
+            }
+            
+            // 过滤掉用户已通过收藏领取过的优惠券类型
+            // 业务规则:收藏时每种类型只发放一张,但好评等其他方式可以另外领取
+            final Set<Integer> finalExistingCouponTypes = existingCouponTypes;
+            couponByType.entrySet().removeIf(entry -> finalExistingCouponTypes.contains(entry.getKey()));
 
             // 发放每种类型的优惠券(每种一张)
+            // 注意:此时 couponByType 已经过滤掉了用户已领取过的类型,所以直接发放即可
             int issuedCount = 0;
             for (Map.Entry<Integer, LifeDiscountCoupon> entry : couponByType.entrySet()) {
                 LifeDiscountCoupon coupon = entry.getValue();
-                
-                // 如果用户已领取过该优惠券,跳过
-                if (existingCouponIds.contains(coupon.getId())) {
-                    continue;
-                }
 
                 // 再次检查库存(防止并发问题)
                 if (coupon.getSingleQty() == null || coupon.getSingleQty() <= 0) {
@@ -1931,16 +1961,32 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
                 }
 
                 try {
+                    // 重新查询优惠券信息,确保获取最新的库存(防止并发问题)
+                    LifeDiscountCoupon latestCoupon = lifeDiscountCouponMapper.selectById(coupon.getId());
+                    if (latestCoupon == null || latestCoupon.getSingleQty() == null || latestCoupon.getSingleQty() <= 0) {
+                        continue;
+                    }
+                    
+                    // 检查用户是否已经领取过该优惠券(双重检查,防止并发)
+                    LambdaQueryWrapper<LifeDiscountCouponUser> duplicateCheck = new LambdaQueryWrapper<>();
+                    duplicateCheck.eq(LifeDiscountCouponUser::getUserId, userId)
+                            .eq(LifeDiscountCouponUser::getCouponId, latestCoupon.getId())
+                            .eq(LifeDiscountCouponUser::getDeleteFlag, 0);
+                    long duplicateCount = lifeDiscountCouponUserMapper.selectCount(duplicateCheck);
+                    if (duplicateCount > 0) {
+                        continue;
+                    }
+                    
                     // 创建用户优惠券记录
                     LifeDiscountCouponUser lifeDiscountCouponUser = new LifeDiscountCouponUser();
-                    lifeDiscountCouponUser.setCouponId(coupon.getId());
+                    lifeDiscountCouponUser.setCouponId(latestCoupon.getId());
                     lifeDiscountCouponUser.setUserId(userId);
                     lifeDiscountCouponUser.setReceiveTime(new Date());
                     
                     // 设置过期时间:优先使用validDate,如果为null则使用endDate
-                    LocalDate expirationTime = coupon.getValidDate();
-                    if (expirationTime == null && coupon.getEndDate() != null) {
-                        expirationTime = coupon.getEndDate();
+                    LocalDate expirationTime = latestCoupon.getValidDate();
+                    if (expirationTime == null && latestCoupon.getEndDate() != null) {
+                        expirationTime = latestCoupon.getEndDate();
                     }
                     lifeDiscountCouponUser.setExpirationTime(expirationTime);
                     lifeDiscountCouponUser.setStatus(Integer.parseInt(DiscountCouponEnum.WAITING_USED.getValue()));
@@ -1950,11 +1996,21 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
                     // 插入用户优惠券记录
                     lifeDiscountCouponUserMapper.insert(lifeDiscountCouponUser);
                     
-                    // 扣减库存
-                    coupon.setSingleQty(coupon.getSingleQty() - 1);
-                    lifeDiscountCouponMapper.updateById(coupon);
+                    // 使用乐观锁扣减库存:通过 WHERE single_qty > 0 确保库存充足
+                    LambdaUpdateWrapper<LifeDiscountCoupon> updateWrapper = new LambdaUpdateWrapper<>();
+                    updateWrapper.eq(LifeDiscountCoupon::getId, latestCoupon.getId())
+                            .gt(LifeDiscountCoupon::getSingleQty, 0) // 确保库存大于0
+                            .setSql("single_qty = single_qty - 1");
+                    int updateResult = lifeDiscountCouponMapper.update(null, updateWrapper);
                     
-                    issuedCount++;
+                    if (updateResult > 0) {
+                        // 库存扣减成功
+                        issuedCount++;
+                    } else {
+                        // 库存扣减失败(可能库存已被其他请求扣减),回滚用户优惠券记录
+                        lifeDiscountCouponUser.setDeleteFlag(1);
+                        lifeDiscountCouponUserMapper.updateById(lifeDiscountCouponUser);
+                    }
                 } catch (Exception e) {
                     // 单个优惠券发放失败不影响其他优惠券的发放
                     // 静默处理,不记录日志,避免日志过多
@@ -1977,6 +2033,11 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
         } catch (Exception e) {
             // 异常时静默处理,不影响收藏功能
             return 0;
+        } finally {
+            // 释放分布式锁
+            if (!StringUtils.isEmpty(identifier)) {
+                baseRedisService.unlock(lockKey, identifier);
+            }
         }
     }
 }