Sfoglia il codice sorgente

Merge branch 'sit' into sit-OrderFood

lutong 1 mese fa
parent
commit
49a329882d

+ 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;
 }

+ 68 - 13
alien-entity/src/main/java/shop/alien/mapper/LifeBrowseRecordMapper.java

@@ -30,18 +30,73 @@ public interface LifeBrowseRecordMapper extends BaseMapper<LifeBrowseRecord> {
             "Order by lbr.liulan_time desc")
     List<Map<String, Object>> getGroupBuyBrowseRecordByUserId(String userId);
 
-    @Select("select lbr.id, lud.id dynamicsId, IFNULL(lud.cover_image, '') coverImage, lbr.liulan_date liulanDate, " +
-            "lud.phone_id phoneId, lud.title, lud.context, lud.image_path imagePath, lud.address, " +
-            "lud.address_context addressContext, lud.address_name addressName, lud.liulan_count liulanCount, " +
-            "lud.dianzan_count dianzanCount, lud.type, lud.draft, lud.created_time createdTime, lud.updated_time updatedTime, " +
-            "lud.address_province addressProvince, lud.top_status topStatus, lud.top_time topTime, " +
-            "lud.enable_status enableStatus, lud.expert_id expertId, lud.business_id businessId, " +
-            "lud.transfer_count transferCount, lud.reality_count realityCount, lud.check_flag checkFlag, " +
-            "lud.ai_task_id aiTaskId, lud.reason, lud.delete_flag deleteFlag\n" +
-            "from life_browse_record lbr \n" +
-            "inner join life_user_dynamics lud on lud.id = lbr.dynamics_id \n" +
-            "and lud.delete_flag = 0 and lbr.delete_flag = 0 \n" +
-            "where lbr.user_id = #{userId} and lbr.dynamics_id is not null\n" +
-            "Order by lbr.liulan_time desc")
+    @Select("SELECT " +
+            "lbr.id, " +
+            "lud.id dynamicsId, " +
+            "IFNULL(lud.cover_image, '') coverImage, " +
+            "lbr.liulan_date liulanDate, " +
+            "lud.phone_id phoneId, " +
+            "lud.title, " +
+            "lud.context, " +
+            "lud.image_path imagePath, " +
+            "lud.address, " +
+            "lud.address_context addressContext, " +
+            "lud.address_name addressName, " +
+            "lud.liulan_count liulanCount, " +
+            "lud.dianzan_count dianzanCount, " +
+            "lud.type, " +
+            "lud.draft, " +
+            "lud.created_time createdTime, " +
+            "lud.updated_time updatedTime, " +
+            "lud.address_province addressProvince, " +
+            "lud.top_status topStatus, " +
+            "lud.top_time topTime, " +
+            "lud.enable_status enableStatus, " +
+            "lud.expert_id expertId, " +
+            "lud.business_id businessId, " +
+            "lud.transfer_count transferCount, " +
+            "lud.reality_count realityCount, " +
+            "lud.check_flag checkFlag, " +
+            "lud.ai_task_id aiTaskId, " +
+            "lud.reason, " +
+            "lud.delete_flag deleteFlag, " +
+            "si1.id storeId, " +
+            "si1.store_name storeName, " +
+            "si1.score_avg scoreAvg, " +
+            "si1.business_section businessSection, " +
+            "si1.business_section_name businessSectionName, " +
+            "ROUND(IFNULL(si1.score_one, 0), 2) scoreOne, " +
+            "ROUND(IFNULL(si1.score_two, 0), 2) scoreTwo, " +
+            "ROUND(IFNULL(si1.score_three, 0), 2) scoreThree, " +
+            "IFNULL(cr.ratingCount, 0) ratingCount " +
+            "FROM " +
+            "life_browse_record lbr " +
+            "INNER JOIN life_user_dynamics lud ON lud.id = lbr.dynamics_id " +
+            "AND lud.delete_flag = 0 " +
+            "AND lbr.delete_flag = 0 " +
+            "LEFT JOIN store_user su ON SUBSTRING_INDEX(lud.phone_id, '_', -1) = su.phone " +
+            "AND su.delete_flag = 0 " +
+            "AND SUBSTRING_INDEX(lud.phone_id, '_', 1) = 'store' " +
+            "LEFT JOIN store_info si1 ON si1.id = su.store_id " +
+            "AND si1.delete_flag = 0 " +
+            "LEFT JOIN ( " +
+            "SELECT " +
+            "business_id, " +
+            "count(*) AS ratingCount " +
+            "FROM " +
+            "common_rating " +
+            "WHERE " +
+            "business_type = 1 " +
+            "AND delete_flag = 0 " +
+            "AND audit_status = 1 " +
+            "AND is_show = 1 " +
+            "GROUP BY " +
+            "business_id " +
+            ") cr ON cr.business_id = si1.id " +
+            "WHERE " +
+            "lbr.user_id = #{userId} " +
+            "AND lbr.dynamics_id IS NOT NULL " +
+            "ORDER BY " +
+            "lbr.liulan_time DESC")
     List<Map<String, Object>> getDynamicsBrowseRecordByUserId(String userId);
 }

+ 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" +

+ 17 - 10
alien-entity/src/main/resources/mapper/LifeFeedbackMapper.xml

@@ -172,26 +172,32 @@
     <select id="selectWebFeedbackDetail" resultType="shop.alien.entity.store.vo.LifeFeedbackDetailVo">
         SELECT
             f.id,
-            u.nick_name AS nickName,
-            u.phone AS phone,
+            CASE f.feedback_source
+                WHEN 0 THEN lu.user_name
+                ELSE u.nick_name
+                END AS nickName,
+            CASE f.feedback_source
+                WHEN 0 THEN lu.user_phone
+                ELSE u.phone
+                END AS phone,
             f.staff_id AS staffId,
             CONCAT(IFNULL(s.user_name, '')) AS staffInfo,
             f.feedback_source AS feedbackSource,
             CASE f.feedback_source
                 WHEN 0 THEN '用户端'
                 WHEN 1 THEN '商家端'
-            END AS feedbackSourceName,
+                END AS feedbackSourceName,
             f.feedback_way AS feedbackWay,
             CASE f.feedback_way
                 WHEN 0 THEN '用户反馈'
-                WHEN 1 THEN 'AI识别'
-            END AS feedbackWayName,
+                WHEN 1 THEN 'AI 识别'
+                END AS feedbackWayName,
             f.feedback_type AS feedbackType,
             CASE f.feedback_type
-                WHEN 0 THEN 'bug反馈'
+                WHEN 0 THEN 'bug 反馈'
                 WHEN 1 THEN '优化反馈'
                 WHEN 2 THEN '新增功能反馈'
-            END AS feedbackTypeName,
+                END AS feedbackTypeName,
             f.feedback_time AS feedbackTime,
             f.content,
             f.contact_way AS contactWay,
@@ -199,10 +205,11 @@
             CASE f.handle_status
                 WHEN 0 THEN '处理中'
                 WHEN 1 THEN '已解决'
-            END AS handleStatusName
+                END AS handleStatusName
         FROM life_feedback f
-        LEFT JOIN store_user u ON f.user_id = u.id
-        LEFT JOIN life_sys s ON f.staff_id = s.id
+                 LEFT JOIN store_user u ON f.user_id = u.id
+                 LEFT JOIN life_user lu ON f.user_id = lu.id
+                 LEFT JOIN life_sys s ON f.staff_id = s.id
         WHERE f.id = #{feedbackId}
     </select>
 

+ 47 - 1
alien-store/src/main/java/shop/alien/store/controller/AiChatController.java

@@ -9,6 +9,7 @@ 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.HttpMethod;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.util.StringUtils;
@@ -16,6 +17,8 @@ import org.springframework.web.bind.annotation.CrossOrigin;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.client.HttpServerErrorException;
+import org.springframework.web.util.UriComponentsBuilder;
 import org.springframework.web.client.RestTemplate;
 import shop.alien.store.util.ai.AiAuthTokenUtil;
 
@@ -35,9 +38,11 @@ public class AiChatController {
 
     private final RestTemplate restTemplate;
 
-    @Value("${third-party-ai-chat.base-url:http://192.168.2.250:9100/ai/life-manager/api/v1/question_classification/classify_and_route}")
+    @Value("${third-party-ai-chat.base-url:http://124.93.18.180:9100/ai/life-manager/api/v1/question_classification/classify_and_route}")
     private String aiChatUrl;
 
+    @Value("${third-party-ai-chat.history-url:http://124.93.18.180:9100/ai/life-manager/api/v1/chat_history}")
+    private String aiHistoryChatUrl;
 
     /**
      * 调用 AI 服务,获取聊天结果
@@ -71,4 +76,45 @@ public class AiChatController {
         }
         return  ResponseEntity.badRequest().body(null);
     }
+
+    /**
+     * 调用 AI 服务,获取聊天结果
+     *
+     * @return 聊天结果
+     */
+    @RequestMapping("/aiChatHistory")
+    public ResponseEntity<String> aiChatHistory(@RequestParam("user_id") String user_id,@RequestParam("page") String page,@RequestParam("rounds_per_page") String rounds_per_page){
+        String accessToken = aiAuthTokenUtil.getAccessToken();
+        JSONObject data = new JSONObject();
+        if (!StringUtils.hasText(accessToken)) {
+            data.put("fail","登录失败");
+            return ResponseEntity.badRequest().body(data.toJSONString());
+        }
+
+        HttpHeaders aiHeaders = new HttpHeaders();
+        aiHeaders.set("Authorization", "Bearer " + accessToken);
+        HttpEntity<Void> request = new HttpEntity<>(aiHeaders);
+
+        String url = UriComponentsBuilder.fromHttpUrl(aiHistoryChatUrl)
+                .queryParam("user_id", user_id)
+                .queryParam("page", page)
+                .queryParam("rounds_per_page", rounds_per_page)
+                .build()
+                .toUriString();
+
+        try {
+            log.info("调用AI聊天历史接口(GET) url={}, user_id={}, page={}, rounds_per_page={}", url, user_id, page, rounds_per_page);
+            ResponseEntity<String> stringResponseEntity = restTemplate.exchange(url, HttpMethod.GET, request, String.class);
+            return stringResponseEntity;
+        } catch (HttpServerErrorException e) {
+            String responseBody = e.getResponseBodyAsString();
+            log.error("调用AI聊天历史接口 5xx异常 url={}, status={}, responseBody={}", url, e.getStatusCode(), responseBody, e);
+            data.put("fail", "AI服务异常: " + e.getStatusCode() + ", " + (StringUtils.hasText(responseBody) ? responseBody : e.getStatusText()));
+            return ResponseEntity.status(e.getStatusCode()).body(data.toJSONString());
+        } catch (Exception e) {
+            log.error("调用AI聊天历史接口 接口异常 url={}", url, e);
+            data.put("fail", "调用AI服务失败: " + e.getMessage());
+            return ResponseEntity.badRequest().body(data.toJSONString());
+        }
+    }
 }

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

@@ -431,7 +431,7 @@ public class StoreUserController {
     public R<String> deleteStoreAccountInfo(@RequestBody StoreUser storeUser) {
         log.info("StoreUserController.deleteStoreAccountInfo? id={}", storeUser.getId());
         String errMsg = storeUserService.deleteStoreAccountInfo(storeUser);
-        if (errMsg == null || errMsg.isEmpty()) {
+        if ("删除成功".equals(errMsg)) {
             return R.success("删除成功");
         }
         return R.fail(errMsg);

+ 3 - 0
alien-store/src/main/java/shop/alien/store/service/LifeUserDynamicsService.java

@@ -746,6 +746,9 @@ public class LifeUserDynamicsService extends ServiceImpl<LifeUserDynamicsMapper,
                      StoreInfo storeInfo=storeInfoMapper.getStoreNameByPhone(phoneIdNew);
                      if(storeInfo != null){
                          lifeUserDynamicsVo.setStoreName(storeInfo.getStoreName());
+                         lifeUserDynamicsVo.setBusinessSection(storeInfo.getBusinessSection().toString());
+                         lifeUserDynamicsVo.setBusinessTypeName(storeInfo.getBusinessTypeName());
+                         lifeUserDynamicsVo.setStoreUserId(storeInfo.getId().toString());
                      }
                 } else if (lifeUserDynamicsVo.getType().equals("1")) {
                     String phoneIdNew = lifeUserDynamicsVo.getPhoneId().substring(5);

+ 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) {

+ 86 - 24
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,24 +1860,26 @@ 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();
             
-            // 查询该店铺所有开启领取的优惠券(有效期内、有库存、未删除)
+            // 查询该店铺所有开启领取的优惠券(有效期内、有库存、未删除、收藏可领
             LambdaQueryWrapper<LifeDiscountCoupon> queryWrapper = new LambdaQueryWrapper<>();
             queryWrapper.eq(LifeDiscountCoupon::getStoreId, storeId)
                     .eq(LifeDiscountCoupon::getGetStatus, 1) // 开启领取
@@ -1880,6 +1887,7 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
                     .gt(LifeDiscountCoupon::getSingleQty, 0) // 有库存
                     .ge(LifeDiscountCoupon::getEndGetDate, now) // 未过期
                     .eq(LifeDiscountCoupon::getCouponStatus, 1) // 正式状态(非草稿)
+                    .eq(LifeDiscountCoupon::getAttentionCanReceived, 1) // 收藏可领(只有为1时才可以领取)
                     .isNotNull(LifeDiscountCoupon::getCouponType) // 必须有优惠券类型
                     .orderByDesc(LifeDiscountCoupon::getCreatedTime); // 按创建时间降序
             
@@ -1902,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) {
@@ -1930,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()));
@@ -1949,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) {
                     // 单个优惠券发放失败不影响其他优惠券的发放
                     // 静默处理,不记录日志,避免日志过多
@@ -1976,6 +2033,11 @@ public class LifeDiscountCouponServiceImpl extends ServiceImpl<LifeDiscountCoupo
         } catch (Exception e) {
             // 异常时静默处理,不影响收藏功能
             return 0;
+        } finally {
+            // 释放分布式锁
+            if (!StringUtils.isEmpty(identifier)) {
+                baseRedisService.unlock(lockKey, identifier);
+            }
         }
     }
 }