Parcourir la source

Merge remote-tracking branch 'origin/sit' into uat-20260202

dujian il y a 1 jour
Parent
commit
a2bf770ed2
44 fichiers modifiés avec 1480 ajouts et 1145 suppressions
  1. 2 2
      alien-dining/src/main/java/shop/alien/dining/controller/DiningCouponController.java
  2. 8 2
      alien-dining/src/main/java/shop/alien/dining/service/impl/DiningCouponServiceImpl.java
  3. 20 10
      alien-dining/src/main/java/shop/alien/dining/service/impl/DiningServiceImpl.java
  4. 5 5
      alien-dining/src/main/java/shop/alien/dining/service/impl/StoreOrderServiceImpl.java
  5. 8 0
      alien-entity/src/main/java/shop/alien/entity/store/LifeDiscountCoupon.java
  6. 23 2
      alien-entity/src/main/java/shop/alien/entity/store/LifeDiscountCouponStoreFriend.java
  7. 10 6
      alien-entity/src/main/java/shop/alien/entity/store/dto/LifeDiscountCouponDto.java
  8. 14 7
      alien-entity/src/main/java/shop/alien/entity/store/dto/LifeDiscountCouponStoreFriendDto.java
  9. 6 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/AvailableCouponVO.java
  10. 18 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/LifeDiscountCouponFriendRuleVo.java
  11. 6 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/LifeDiscountCouponVo.java
  12. 47 0
      alien-entity/src/main/java/shop/alien/mapper/LifeCollectMapper.java
  13. 2 2
      alien-entity/src/main/java/shop/alien/mapper/LifeDiscountCouponFriendRuleDetailMapper.java
  14. 10 0
      alien-entity/src/main/java/shop/alien/mapper/LifeDiscountCouponMapper.java
  15. 7 0
      alien-entity/src/main/java/shop/alien/mapper/LifeDiscountCouponQuantumRulesMapper.java
  16. 18 10
      alien-entity/src/main/java/shop/alien/mapper/LifeDiscountCouponStoreFriendMapper.java
  17. 7 0
      alien-entity/src/main/java/shop/alien/mapper/LifeDiscountCouponUnavailableRulesMapper.java
  18. 16 2
      alien-entity/src/main/resources/mapper/LifeDiscountCouponMapper.xml
  19. 9 0
      alien-entity/src/main/resources/mapper/LifeDiscountCouponQuantumRulesMapper.xml
  20. 9 1
      alien-entity/src/main/resources/mapper/LifeDiscountCouponStoreFriendMapper.xml
  21. 4 0
      alien-entity/src/main/resources/mapper/LifeDiscountCouponUnavailableRulesMapper.xml
  22. 2 2
      alien-entity/src/main/resources/mapper/LifeUserOrderMapper.xml
  23. 1 1
      alien-job/src/main/java/shop/alien/job/store/LifeCouponJob.java
  24. 2 0
      alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/LifeCouponPlatformServiceImpl.java
  25. 94 53
      alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/LifeDiscountCouponPlatformServiceImpl.java
  26. 2 2
      alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/OperationalActivityServiceImpl.java
  27. 97 19
      alien-store/src/main/java/shop/alien/store/controller/LifeCollectController.java
  28. 22 12
      alien-store/src/main/java/shop/alien/store/controller/LifeDiscountCouponController.java
  29. 22 27
      alien-store/src/main/java/shop/alien/store/controller/LifeDiscountCouponStoreFriendController.java
  30. 3 13
      alien-store/src/main/java/shop/alien/store/service/LifeDiscountCouponService.java
  31. 30 28
      alien-store/src/main/java/shop/alien/store/service/LifeDiscountCouponStoreFriendService.java
  32. 1 1
      alien-store/src/main/java/shop/alien/store/service/LifeUserOrderService.java
  33. 18 21
      alien-store/src/main/java/shop/alien/store/service/impl/CommonRatingServiceImpl.java
  34. 341 420
      alien-store/src/main/java/shop/alien/store/service/impl/LifeDiscountCouponServiceImpl.java
  35. 497 463
      alien-store/src/main/java/shop/alien/store/service/impl/LifeDiscountCouponStoreFriendServiceImpl.java
  36. 27 10
      alien-store/src/main/java/shop/alien/store/service/impl/LifeDiscountCouponUserServiceImpl.java
  37. 2 0
      alien-store/src/main/java/shop/alien/store/service/impl/LifeGroupPackageServiceImpl.java
  38. 2 2
      alien-store/src/main/java/shop/alien/store/service/impl/OperationalActivityServiceImpl.java
  39. 2 2
      alien-store/src/main/java/shop/alien/store/service/impl/StoreOperationalActivityServiceImpl.java
  40. 9 2
      alien-store/src/main/java/shop/alien/store/service/impl/StorePlatformBenefitsServiceImpl.java
  41. 9 9
      alien-store/src/main/java/shop/alien/store/service/impl/StoreReservationServiceImpl.java
  42. 9 7
      alien-store/src/main/java/shop/alien/store/service/impl/TrackEventServiceImpl.java
  43. 17 2
      alien-util/src/main/java/shop/alien/util/coupon/DiscountCouponExpirationUtil.java
  44. 22 0
      alien-util/src/main/java/shop/alien/util/coupon/LifeDiscountCouponStock.java

+ 2 - 2
alien-dining/src/main/java/shop/alien/dining/controller/DiningCouponController.java

@@ -48,8 +48,8 @@ public class DiningCouponController {
             @ApiImplicitParam(name = "page", value = "分页页数", dataType = "Integer", paramType = "query", required = false),
             @ApiImplicitParam(name = "size", value = "分页条数", dataType = "Integer", paramType = "query", required = false),
             @ApiImplicitParam(name = "tabType", value = "分页类型(0:全部(未使用),1:即将过期,2:已使用,3:已过期)", dataType = "String", paramType = "query", required = true),
-            @ApiImplicitParam(name = "type", value = "券类型(不传:优惠券+代金券都返回,1:仅优惠券查 life_discount_coupon,4:仅代金券查 life_coupon)", dataType = "Integer", paramType = "query", required = false),
-            @ApiImplicitParam(name = "couponType", value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选,仅当type不为4时有效)", dataType = "Integer", paramType = "query", required = false),
+            @ApiImplicitParam(name = "type", value = "券类型(不传或1:优惠券;代金券 type=4 已下线)", dataType = "Integer", paramType = "query", required = false),
+            @ApiImplicitParam(name = "couponType", value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)", dataType = "Integer", paramType = "query", required = false),
             @ApiImplicitParam(name = "storeId", value = "商铺ID,可为空,传则仅返回该商铺的优惠券", dataType = "String", paramType = "query", required = false)
     })
     public R<List<LifeDiscountCouponVo>> getUserCouponList(

+ 8 - 2
alien-dining/src/main/java/shop/alien/dining/service/impl/DiningCouponServiceImpl.java

@@ -118,7 +118,9 @@ public class DiningCouponServiceImpl implements DiningCouponService {
             userWrapper.eq(LifeDiscountCouponUser::getUserId, userId);
             userWrapper.eq(LifeDiscountCouponUser::getStatus, 0); // 0:待使用
             userWrapper.eq(LifeDiscountCouponUser::getDeleteFlag, 0);
-            userWrapper.ge(LifeDiscountCouponUser::getExpirationTime, LocalDate.now()); // 未过期
+            userWrapper.and(w -> w.isNull(LifeDiscountCouponUser::getExpirationTime)
+                    .or()
+                    .ge(LifeDiscountCouponUser::getExpirationTime, LocalDate.now()));
             List<LifeDiscountCouponUser> userCoupons = lifeDiscountCouponUserMapper.selectList(userWrapper);
 
             if (userCoupons == null || userCoupons.isEmpty()) {
@@ -290,7 +292,9 @@ public class DiningCouponServiceImpl implements DiningCouponService {
             userWrapper.eq(LifeDiscountCouponUser::getUserId, userId);
             userWrapper.eq(LifeDiscountCouponUser::getStatus, 0); // 0:待使用
             userWrapper.eq(LifeDiscountCouponUser::getDeleteFlag, 0);
-            userWrapper.ge(LifeDiscountCouponUser::getExpirationTime, LocalDate.now()); // 未过期
+            userWrapper.and(w -> w.isNull(LifeDiscountCouponUser::getExpirationTime)
+                    .or()
+                    .ge(LifeDiscountCouponUser::getExpirationTime, LocalDate.now()));
             List<LifeDiscountCouponUser> userCoupons = lifeDiscountCouponUserMapper.selectList(userWrapper);
 
             if (userCoupons == null || userCoupons.isEmpty()) {
@@ -422,6 +426,8 @@ public class DiningCouponServiceImpl implements DiningCouponService {
         vo.setCreatedTime(coupon.getCreatedTime());
         vo.setCouponType(coupon.getCouponType());
         vo.setDiscountRate(coupon.getDiscountRate());
+        vo.setLongTermValid(coupon.getLongTermValid());
+        vo.setUnlimitedQty(coupon.getUnlimitedQty());
         return vo;
     }
 

+ 20 - 10
alien-dining/src/main/java/shop/alien/dining/service/impl/DiningServiceImpl.java

@@ -15,6 +15,7 @@ import shop.alien.dining.config.BaseRedisService;
 import shop.alien.dining.service.CartService;
 import shop.alien.dining.service.DiningService;
 import shop.alien.util.coupon.DiscountCouponExpirationUtil;
+import shop.alien.util.coupon.LifeDiscountCouponStock;
 
 import java.math.BigDecimal;
 import java.time.LocalDate;
@@ -239,7 +240,9 @@ public class DiningServiceImpl implements DiningService {
         wrapper.eq(LifeDiscountCoupon::getStoreId, String.valueOf(storeId));
         wrapper.eq(LifeDiscountCoupon::getDeleteFlag, 0);
         wrapper.eq(LifeDiscountCoupon::getGetStatus, 1); // 开启领取
-        wrapper.gt(LifeDiscountCoupon::getSingleQty, 0); // 有库存
+        wrapper.and(w -> w.eq(LifeDiscountCoupon::getUnlimitedQty, 1)
+                .or()
+                .gt(LifeDiscountCoupon::getSingleQty, 0));
         LocalDate now = LocalDate.now();
         wrapper.le(LifeDiscountCoupon::getStartDate, now);
         wrapper.ge(LifeDiscountCoupon::getEndDate, now);
@@ -267,8 +270,12 @@ public class DiningServiceImpl implements DiningService {
             vo.setMinimumSpendingAmount(coupon.getMinimumSpendingAmount());
             vo.setEndDate(coupon.getEndDate());
             vo.setIsReceived(finalReceivedCouponIds.contains(coupon.getId()));
-            vo.setIsAvailable(coupon.getSingleQty() > 0 && coupon.getEndDate().isAfter(now) || coupon.getEndDate().isEqual(now));
-            // 设置优惠券类型和折扣率(如果需要,可以在VO中添加这些字段)
+            boolean hasStock = LifeDiscountCouponStock.hasTemplateStockRemaining(coupon.getUnlimitedQty(), coupon.getSingleQty());
+            boolean inEnd = coupon.getEndDate() != null
+                    && (coupon.getEndDate().isAfter(now) || coupon.getEndDate().isEqual(now));
+            vo.setIsAvailable(hasStock && inEnd);
+            vo.setLongTermValid(coupon.getLongTermValid());
+            vo.setUnlimitedQty(coupon.getUnlimitedQty());
             return vo;
         }).collect(Collectors.toList());
     }
@@ -277,7 +284,7 @@ public class DiningServiceImpl implements DiningService {
     public boolean receiveCoupon(Integer couponId, Integer userId) {
         log.info("领取优惠券, couponId={}, userId={}", couponId, userId);
 
-        LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(couponId);
+        LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(couponId);
         if (coupon == null) {
             throw new RuntimeException("优惠券不存在");
         }
@@ -291,8 +298,8 @@ public class DiningServiceImpl implements DiningService {
             throw new RuntimeException("您已领取过该优惠券");
         }
 
-        // 检查库存
-        if (coupon.getSingleQty() == null || coupon.getSingleQty() <= 0) {
+        // 检查库存(发行量不限则跳过)
+        if (!LifeDiscountCouponStock.hasTemplateStockRemaining(coupon.getUnlimitedQty(), coupon.getSingleQty())) {
             throw new RuntimeException("优惠券已领完");
         }
 
@@ -303,6 +310,7 @@ public class DiningServiceImpl implements DiningService {
         userCoupon.setReceiveTime(new Date());
         userCoupon.setExpirationTime(DiscountCouponExpirationUtil.resolveLifeDiscountCouponExpiration(
                 userCoupon.getReceiveTime(),
+                coupon.getLongTermValid(),
                 coupon.getSpecifiedDay(),
                 coupon.getExpirationDate(),
                 coupon.getValidDate(),
@@ -312,8 +320,10 @@ public class DiningServiceImpl implements DiningService {
         lifeDiscountCouponUserMapper.insert(userCoupon);
 
         // 更新库存
-        coupon.setSingleQty(coupon.getSingleQty() - 1);
-        lifeDiscountCouponMapper.updateById(coupon);
+        if (!LifeDiscountCouponStock.isUnlimitedQty(coupon.getUnlimitedQty()) && coupon.getSingleQty() != null) {
+            coupon.setSingleQty(coupon.getSingleQty() - 1);
+            lifeDiscountCouponMapper.updateById(coupon);
+        }
 
         return true;
     }
@@ -417,7 +427,7 @@ public class DiningServiceImpl implements DiningService {
                 vo.setCouponName(bestCoupon.getName());
                 // 根据优惠券类型计算优惠金额
                 // 需要查询优惠券详情来计算折扣券的优惠金额
-                LifeDiscountCoupon couponDetail = lifeDiscountCouponMapper.selectById(bestCoupon.getId());
+                LifeDiscountCoupon couponDetail = lifeDiscountCouponMapper.selectByIdIncludeDeleted(bestCoupon.getId());
                 BigDecimal discountAmount = calculateDiscountAmount(couponDetail, cart.getTotalAmount().add(tablewareFee));
                 BigDecimal totalAmount = cart.getTotalAmount().add(tablewareFee);
                 vo.setDiscountAmount(discountAmount);
@@ -599,7 +609,7 @@ public class DiningServiceImpl implements DiningService {
 
         // 查询优惠券名称
         if (order.getCouponId() != null) {
-            shop.alien.entity.store.LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(order.getCouponId());
+            shop.alien.entity.store.LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(order.getCouponId());
             if (coupon != null) {
                 vo.setCouponName(coupon.getName());
             }

+ 5 - 5
alien-dining/src/main/java/shop/alien/dining/service/impl/StoreOrderServiceImpl.java

@@ -160,7 +160,7 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
             }
 
             // 验证优惠券(只查询一次)
-            coupon = lifeDiscountCouponMapper.selectById(dto.getCouponId());
+            coupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(dto.getCouponId());
             if (coupon == null) {
                 throw new RuntimeException("优惠券不存在");
             }
@@ -455,7 +455,7 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         
         if (order.getCouponId() != null) {
             // 查询优惠券信息
-            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(order.getCouponId());
+            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(order.getCouponId());
             if (coupon == null) {
                 throw new RuntimeException("优惠券不存在");
             }
@@ -917,7 +917,7 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
 
         if (couponId != null) {
             // 验证优惠券
-            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(couponId);
+            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(couponId);
             if (coupon == null) {
                 throw new RuntimeException("优惠券不存在");
             }
@@ -1451,7 +1451,7 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         BigDecimal nominalValue = null;
         BigDecimal minimumSpendingAmount = null;
         if (order.getCouponId() != null) {
-            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(order.getCouponId());
+            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(order.getCouponId());
             if (coupon != null) {
                 couponName = coupon.getName();
                 couponType = coupon.getCouponType();
@@ -1613,7 +1613,7 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         // 3. 查询优惠券信息(如果有)
         String couponName = null;
         if (order.getCouponId() != null) {
-            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(order.getCouponId());
+            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(order.getCouponId());
             if (coupon != null) {
                 couponName = coupon.getName();
             }

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

@@ -49,6 +49,10 @@ public class LifeDiscountCoupon extends Model<LifeDiscountCoupon> {
     @TableField("expiration_date")
     private Integer expirationDate;
 
+    @ApiModelProperty(value = "领取后是否长期有效:0-否,1-是")
+    @TableField("long_term_valid")
+    private Integer longTermValid;
+
     @ApiModelProperty(value = "开始日期")
     @TableField(value = "start_date", updateStrategy = FieldStrategy.IGNORED)
     private LocalDate startDate;
@@ -61,6 +65,10 @@ public class LifeDiscountCoupon extends Model<LifeDiscountCoupon> {
     @TableField(value = "single_qty")
     private Integer singleQty;
 
+    @ApiModelProperty(value = "发行数量是否不限:0-否,1-是")
+    @TableField("unlimited_qty")
+    private Integer unlimitedQty;
+
     @ApiModelProperty(value = "补充说明")
     @TableField("supplementary_instruction")
     private String supplementaryInstruction;

+ 23 - 2
alien-entity/src/main/java/shop/alien/entity/store/LifeDiscountCouponStoreFriend.java

@@ -24,6 +24,7 @@ import java.util.Date;
 @EqualsAndHashCode(callSuper = false)
 @Accessors(chain = true)
 @ApiModel(value = "LifeDiscountCouponStoreFriend对象", description = "优惠券商户发放好友优惠券关系表	")
+@TableName("life_discount_coupon_store_friend")
 public class LifeDiscountCouponStoreFriend extends Model<LifeDiscountCouponStoreFriend> {
 
     private static final long serialVersionUID = 1L;
@@ -32,7 +33,7 @@ public class LifeDiscountCouponStoreFriend extends Model<LifeDiscountCouponStore
     @TableId(value = "id", type = IdType.AUTO)
     private Integer id;
 
-    @ApiModelProperty(value = "店铺ID(store_info.id),收到券的店铺")
+    @ApiModelProperty(value = "【兼容旧列】接收方店铺,与 receiver_store_id 一致,对应库字段 store_user_id(命名易误导)")
     @TableField("store_user_id")
     private Integer storeUserId;
 
@@ -44,14 +45,34 @@ public class LifeDiscountCouponStoreFriend extends Model<LifeDiscountCouponStore
     @TableField("voucher_id")
     private String voucherId;
 
-    @ApiModelProperty(value = "店铺用户ID(store_user.id),送券的用户")
+    @ApiModelProperty(value = "【兼容旧列】赠送方商户用户(store_user.id),与 sender_user_id 一致")
     @TableField("friend_store_user_id")
     private Integer friendStoreUserId;
 
+    @ApiModelProperty(value = "接收方店铺(store_info.id)")
+    @TableField("receiver_store_id")
+    private Integer receiverStoreId;
+
+    @ApiModelProperty(value = "接收方商户用户(store_user.id),可为空")
+    @TableField("receiver_user_id")
+    private Integer receiverUserId;
+
+    @ApiModelProperty(value = "赠送方店铺(store_info.id)")
+    @TableField("sender_store_id")
+    private Integer senderStoreId;
+
+    @ApiModelProperty(value = "赠送方商户用户(store_user.id)")
+    @TableField("sender_user_id")
+    private Integer senderUserId;
+
     @ApiModelProperty(value = "有效期(天)")
     @TableField("expiration_date")
     private Integer expirationDate;
 
+    @ApiModelProperty(value = "领取后是否长期有效:0-否,1-是(赠券时自优惠券模板快照)")
+    @TableField("long_term_valid")
+    private Integer longTermValid;
+
     @ApiModelProperty(value = "开始日期")
     @TableField("start_date")
     private LocalDate startDate;

+ 10 - 6
alien-entity/src/main/java/shop/alien/entity/store/dto/LifeDiscountCouponDto.java

@@ -1,7 +1,5 @@
 package shop.alien.entity.store.dto;
 
-import com.baomidou.mybatisplus.annotation.FieldFill;
-import com.baomidou.mybatisplus.annotation.TableField;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
@@ -11,12 +9,12 @@ import shop.alien.entity.store.LifeDiscountCouponQuantumRules;
 
 import java.math.BigDecimal;
 import java.time.LocalDate;
-import java.util.Date;
 import java.util.List;
 
 /**
  * <p>
- * 优惠券表DTO
+ * 门店优惠券入参 DTO,对应表 {@code life_discount_coupon}(满减/折扣券模板)。<br/>
+ * 与代金券表 {@code life_coupon} 无关;代金券请走其它业务接口。
  * </p>
  *
  * @author ssk
@@ -25,7 +23,7 @@ import java.util.List;
 @Data
 @EqualsAndHashCode(callSuper = false)
 @Accessors(chain = true)
-@ApiModel(value="LifeDiscountCoupon对象", description="优惠券表")
+@ApiModel(value = "LifeDiscountCouponDto", description = "门店优惠券入参(life_discount_coupon,满减/折扣)。门店接口不生成 life_coupon 代金券。")
 public class LifeDiscountCouponDto {
 
     private static final long serialVersionUID = 1L;
@@ -45,6 +43,9 @@ public class LifeDiscountCouponDto {
     @ApiModelProperty(value = "有效期(天)")
     private Integer expirationDate;
 
+    @ApiModelProperty(value = "领取后是否长期有效:0-否,1-是")
+    private Integer longTermValid;
+
     @ApiModelProperty(value = "开始日期")
     private LocalDate startDate;
 
@@ -54,6 +55,9 @@ public class LifeDiscountCouponDto {
     @ApiModelProperty(value = "库存(优惠券数量)")
     private Integer singleQty;
 
+    @ApiModelProperty(value = "发行数量是否不限:0-否,1-是")
+    private Integer unlimitedQty;
+
     @ApiModelProperty(value = "补充说明")
     private String supplementaryInstruction;
 
@@ -66,7 +70,7 @@ public class LifeDiscountCouponDto {
     @ApiModelProperty(value = "最低消费")
     private BigDecimal minimumSpendingAmount;
 
-    @ApiModelProperty(value = "类型   1-优惠券  2-红包 3-平台优惠券")
+    @ApiModelProperty(value = "业务大类:门店新增接口请固定为 1(优惠券);勿传 2/3(其它端)或误当作代金券")
     private Integer type;
 
     @ApiModelProperty(value = "收藏可领(0:否,1:是)")

+ 14 - 7
alien-entity/src/main/java/shop/alien/entity/store/dto/LifeDiscountCouponStoreFriendDto.java

@@ -1,6 +1,7 @@
 package shop.alien.entity.store.dto;
 
 import com.baomidou.mybatisplus.extension.activerecord.Model;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
@@ -36,22 +37,28 @@ public class LifeDiscountCouponStoreFriendDto extends Model<LifeDiscountCouponSt
     @ApiModelProperty(value = "券id,优惠券时传(对应 life_discount_coupon.id)")
     private Integer couponId;
 
-    @ApiModelProperty(value = "代金券id,代金券时传(对应 life_coupon.id)")
-    private String voucherId;
-
-    @ApiModelProperty(value = "好友店铺id")
+    @ApiModelProperty(value = "好友店铺 id(接收方 store_info.id)")
     private Integer friendStoreUserId;
 
+    @ApiModelProperty(value = "接收方商户用户 id(可选,store_user.id)")
+    private Integer receiverUserId;
+
     @ApiModelProperty(value = "设置发放好友店铺可用优惠券列表")
     private List<LifeDiscountCouponStoreFriendDto> couponIds;
 
-    @ApiModelProperty(value = "有效期(天)")
+    /**
+     * 赠券接口(setFriendCoupon)中由服务端按 couponId 查询 life_discount_coupon 写入,禁止客户端指定。
+     */
+    @JsonIgnore
+    @ApiModelProperty(hidden = true, value = "已忽略:有效期天数由服务端根据优惠券模板填充")
     private Integer expirationDate;
 
-    @ApiModelProperty(value = "开始日期")
+    @JsonIgnore
+    @ApiModelProperty(hidden = true, value = "已忽略:开始日期由服务端根据优惠券模板填充")
     private LocalDate startDate;
 
-    @ApiModelProperty(value = "结束日期")
+    @JsonIgnore
+    @ApiModelProperty(hidden = true, value = "已忽略:结束日期由服务端根据优惠券模板填充")
     private LocalDate endDate;
 
     @ApiModelProperty(value = "库存(优惠券数量)")

+ 6 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/AvailableCouponVO.java

@@ -37,4 +37,10 @@ public class AvailableCouponVO {
 
     @ApiModelProperty(value = "是否可用(库存>0且未过期)")
     private Boolean isAvailable;
+
+    @ApiModelProperty(value = "领取后是否长期有效:0-否,1-是(与 life_discount_coupon 一致)")
+    private Integer longTermValid;
+
+    @ApiModelProperty(value = "发行数量是否不限:0-否,1-是")
+    private Integer unlimitedQty;
 }

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

@@ -2,6 +2,7 @@ package shop.alien.entity.store.vo;
 
 import com.baomidou.mybatisplus.annotation.*;
 import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
@@ -33,6 +34,15 @@ public class LifeDiscountCouponFriendRuleVo extends LifeDiscountCouponFriendRule
     @ApiModelProperty(value = "img")
     private String imgUrl;
 
+    /**
+     * 列表展示「对方」商户的 store_user.id(我收到=赠送方,我送出=接收方),用于精准匹配 head_img;
+     * 非库表字段,仅为接口拼装,不序列化到 JSON。
+     */
+    @JsonIgnore
+    @TableField(exist = false)
+    @ApiModelProperty(hidden = true, value = "对方商户用户ID(store_user.id),内部用于头像")
+    private Integer counterpartyStoreUserId;
+
     @ApiModelProperty(value = "优惠券名称")
     private String couponName;
 
@@ -72,5 +82,13 @@ public class LifeDiscountCouponFriendRuleVo extends LifeDiscountCouponFriendRule
     @ApiModelProperty(value = "结束领取时间(仅优惠券有值,代金券为null)")
     private Date endGetDate;
 
+    @TableField(exist = false)
+    @ApiModelProperty(value = "领取后有效期天数,对应优惠券表 life_discount_coupon.expiration_date")
+    private Integer expirationDate;
+
+    @TableField(exist = false)
+    @ApiModelProperty(value = "领取后是否长期有效:0-否,1-是(无时间上限;优先 ldcsf 快照,COALESCE 模板 long_term_valid)")
+    private Integer longTermValid;
+
     private List<LifeDiscountCouponFriendRuleDetailVo> lifeDiscountCouponFriendRuleDetailVos;
 }

+ 6 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/LifeDiscountCouponVo.java

@@ -66,6 +66,9 @@ public class LifeDiscountCouponVo {
     @ApiModelProperty(value = "有效期(天)")
     private Integer expirationDate;
 
+    @ApiModelProperty(value = "领取后是否长期有效:0-否,1-是")
+    private Integer longTermValid;
+
     @ApiModelProperty(value = "开始日期")
     private LocalDate startDate;
 
@@ -75,6 +78,9 @@ public class LifeDiscountCouponVo {
     @ApiModelProperty(value = "库存(优惠券数量)")
     private Integer singleQty;
 
+    @ApiModelProperty(value = "发行数量是否不限:0-否,1-是")
+    private Integer unlimitedQty;
+
     @ApiModelProperty(value = "补充说明")
     private String supplementaryInstruction;
 

+ 47 - 0
alien-entity/src/main/java/shop/alien/mapper/LifeCollectMapper.java

@@ -1,12 +1,59 @@
 package shop.alien.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+import shop.alien.entity.second.vo.SecondGoodsRecommendVo;
 import shop.alien.entity.store.LifeCollect;
 
+import java.util.List;
+
 /**
  * 收藏
  */
 @Mapper
 public interface LifeCollectMapper extends BaseMapper<LifeCollect> {
+
+    /**
+     * 获取收藏的二级商品
+     * @param page
+     * @param userId
+     * @param s
+     * @param s1
+     * @return
+     */
+    @Select("<script>" +
+            "SELECT " +
+            "  sg.id, " +
+            "  sg.user_id, " +
+            "  sg.title, " +
+            "  sg.price, " +
+            "  sg.price AS amount, " +
+            "  sg.home_image, " +
+            "  sg.video_first_frame, " +
+            "  sg.collect_count, " +
+            "  lc.created_time, " +
+            // 距离计算字段(你原有的逻辑)
+            "  ROUND(ST_Distance_Sphere(ST_GeomFromText(CONCAT('POINT(', REPLACE(#{position}, ',', ' '), ')' )), ST_GeomFromText(CONCAT('POINT(', REPLACE(sg.position, ',', ' '), ')' ))) / 1000, 2) AS dist " +
+            "FROM second_goods sg " +
+            // 内连接:只查询【当前用户】收藏的商品
+            "INNER JOIN life_collect lc " +
+            "  ON lc.business_id = sg.id " +
+            "  AND lc.business_type = 1 " +
+            "  AND lc.delete_flag = 0 " +
+            "  AND lc.user_id = #{s} " +
+            "WHERE sg.delete_flag = 0 " +
+            // 动态排除屏蔽商品(空集合不拼接,避免SQL报错)
+            "<if test=\"shieldedGoodsIds != null and !shieldedGoodsIds.isEmpty()\">" +
+            "  AND sg.id NOT IN " +
+            "  <foreach collection='shieldedGoodsIds' item='id' open='(' separator=',' close=')'>" +
+            "    #{id}" +
+            "  </foreach>" +
+            "</if>" +
+            "GROUP BY sg.id " +
+            // 按收藏时间倒序排序
+            "ORDER BY lc.created_time DESC" +
+            "</script>")
+    IPage<SecondGoodsRecommendVo> collectSecondGoodsByPage(IPage<SecondGoodsRecommendVo> page, Integer userId, String s, String position, List<Integer> shieldedGoodsIds);
 }

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

@@ -21,11 +21,11 @@ public interface LifeDiscountCouponFriendRuleDetailMapper extends BaseMapper<Lif
 
     void insertList(List<LifeDiscountCouponFriendRuleDetail> lifeDiscountCouponFriendRuleDetailList);
 
-    @Select("select si.store_name storeName,ldc.name couponName,ldc.id couponId,ldc.coupon_type couponType,sum(ldcsf.single_qty) couponNum,ldcsf.friend_store_user_id friendStoreUserId from life_discount_coupon_store_friend ldcsf left join life_discount_coupon ldc on ldcsf.coupon_id = ldc.id left join store_user su on ldcsf.friend_store_user_id = su.id left join store_info si on su.store_id = si.id ${ew.customSqlSegment}")
+    @Select("select si.store_name storeName,ldc.name couponName,ldc.id couponId,ldc.coupon_type couponType,sum(ldcsf.single_qty) couponNum,COALESCE(ldcsf.sender_user_id, ldcsf.friend_store_user_id) friendStoreUserId from life_discount_coupon_store_friend ldcsf left join life_discount_coupon ldc on ldcsf.coupon_id = ldc.id left join store_user su on COALESCE(ldcsf.sender_user_id, ldcsf.friend_store_user_id) = su.id left join store_info si on si.id = COALESCE(ldcsf.sender_store_id, su.store_id) and si.delete_flag = 0 ${ew.customSqlSegment}")
     List<LifeDiscountCouponFriendRuleDetailVo> getReceivedFriendCouponList(@Param(Constants.WRAPPER) QueryWrapper<LifeDiscountCouponFriendRuleDetailVo> queryWrapper);
 
     /** 查询收到的代金券列表(life_coupon,voucher_id 不为空) */
-    @Select("select si.store_name storeName,lc.name couponName,lc.id voucherId,sum(ldcsf.single_qty) couponNum,ldcsf.friend_store_user_id friendStoreUserId from life_discount_coupon_store_friend ldcsf left join life_coupon lc on ldcsf.voucher_id = lc.id left join store_user su on ldcsf.friend_store_user_id = su.id left join store_info si on su.store_id = si.id ${ew.customSqlSegment}")
+    @Select("select si.store_name storeName,lc.name couponName,lc.id voucherId,sum(ldcsf.single_qty) couponNum,COALESCE(ldcsf.sender_user_id, ldcsf.friend_store_user_id) friendStoreUserId from life_discount_coupon_store_friend ldcsf left join life_coupon lc on ldcsf.voucher_id = lc.id left join store_user su on COALESCE(ldcsf.sender_user_id, ldcsf.friend_store_user_id) = su.id left join store_info si on su.store_id = si.id ${ew.customSqlSegment}")
     List<LifeDiscountCouponFriendRuleDetailVo> getReceivedFriendVoucherList(@Param(Constants.WRAPPER) QueryWrapper<LifeDiscountCouponFriendRuleDetailVo> queryWrapper);
 
     /** 一条 SQL 同时查规则下的优惠券(type=1)与代金券(type=4),按 type 区分 */

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/LifeDiscountCouponMapper.java

@@ -76,4 +76,14 @@ public interface LifeDiscountCouponMapper extends BaseMapper<LifeDiscountCoupon>
      * @return 符合条件的 life_discount_coupon 列表
      */
     List<LifeDiscountCoupon> selectListSingleTable(@Param("storeId") String storeId, @Param("couponStatus") Integer couponStatus, @Param("type") Integer type, @Param("couponType") Integer couponType);
+
+    /**
+     * 按主键查询优惠券模板(含逻辑删除),用于已派发券展示名称/面额/规则等场景
+     */
+    LifeDiscountCoupon selectByIdIncludeDeleted(@Param("id") Integer id);
+
+    /**
+     * 批量按主键查询优惠券模板(含逻辑删除),用于好友赠券列表等需保留已删模板快照的场景
+     */
+    List<LifeDiscountCoupon> selectByIdsIncludeDeleted(@Param("ids") List<Integer> ids);
 }

+ 7 - 0
alien-entity/src/main/java/shop/alien/mapper/LifeDiscountCouponQuantumRulesMapper.java

@@ -1,8 +1,11 @@
 package shop.alien.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Param;
 import shop.alien.entity.store.LifeDiscountCouponQuantumRules;
 
+import java.util.List;
+
 /**
  * <p>
  * 优惠券时间段规则表 Mapper 接口
@@ -13,4 +16,8 @@ import shop.alien.entity.store.LifeDiscountCouponQuantumRules;
  */
 public interface LifeDiscountCouponQuantumRulesMapper extends BaseMapper<LifeDiscountCouponQuantumRules> {
 
+    /**
+     * 按优惠券模板 id 查询时间段规则(含逻辑删除)
+     */
+    List<LifeDiscountCouponQuantumRules> listByDiscountCouponIdIncludeDeleted(@Param("discountCouponId") Integer discountCouponId);
 }

+ 18 - 10
alien-entity/src/main/java/shop/alien/mapper/LifeDiscountCouponStoreFriendMapper.java

@@ -24,12 +24,14 @@ import java.util.List;
 public interface LifeDiscountCouponStoreFriendMapper extends BaseMapper<LifeDiscountCouponStoreFriend> {
 
     @Select("select ldc.store_id,ldc.start_date,ldc.end_date,\n" +
+            "ldc.begin_get_date,ldc.end_get_date,ldc.coupon_status,\n" +
+            "ldc.long_term_valid,ldc.unlimited_qty,\n" +
             "si.store_name,\n" +
             "ldc.name,ldc.get_status,\n" +
             "ldcsf.single_qty,ldcsf.coupon_id,ldcsf.release_type,ldcsf.id give_coupon_id\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" +
+            "on ldc.id = ldcsf.coupon_id\n" +
             "left join store_info si\n" +
             "on si.id = ldc.store_id and si.delete_flag = 0\n" +
             "${ew.customSqlSegment}")
@@ -47,12 +49,16 @@ public interface LifeDiscountCouponStoreFriendMapper extends BaseMapper<LifeDisc
             "ldc.coupon_type couponType,\n" +
             "ldc.discount_rate discountRate,\n" +
             "ldc.begin_get_date beginGetDate,\n" +
-            "ldc.end_get_date endGetDate\n" +
+            "ldc.end_get_date endGetDate,\n" +
+            "ldc.expiration_date expirationDate,\n" +
+            "COALESCE(ldc.long_term_valid, ldcsf.long_term_valid) longTermValid,\n" +
+            "COALESCE(ldcsf.sender_user_id, ldcsf.friend_store_user_id) counterpartyStoreUserId\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" +
+            "on ldc.id = ldcsf.coupon_id\n" +
+            "left join store_user su_cp on su_cp.id = COALESCE(ldcsf.sender_user_id, ldcsf.friend_store_user_id) and su_cp.delete_flag = 0\n" +
             "left join store_info si\n" +
-            "on CAST(si.id AS CHAR) COLLATE utf8mb4_unicode_ci = ldc.store_id and si.delete_flag = 0\n" +
+            "on si.id = COALESCE(ldcsf.sender_store_id, su_cp.store_id, CAST(ldc.store_id AS UNSIGNED)) and si.delete_flag = 0\n" +
             "${ew.customSqlSegment}")
     List<LifeDiscountCouponFriendRuleVo> getReceivedSendFriendCouponList(@Param(Constants.WRAPPER) QueryWrapper<LifeDiscountCouponFriendRuleVo> lifeDiscountCouponFriendRuleVoQueryWrapper);
 
@@ -85,14 +91,16 @@ public interface LifeDiscountCouponStoreFriendMapper extends BaseMapper<LifeDisc
             "ldc.coupon_type couponType,\n" +
             "ldc.discount_rate discountRate,\n" +
             "ldc.begin_get_date beginGetDate,\n" +
-            "ldc.end_get_date endGetDate\n" +
+            "ldc.end_get_date endGetDate,\n" +
+            "ldc.expiration_date expirationDate,\n" +
+            "COALESCE(ldc.long_term_valid, ldcsf.long_term_valid) longTermValid,\n" +
+            "ldcsf.receiver_user_id counterpartyStoreUserId\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" +
-            "left join store_user su\n" +
-            "on su.id = ldcsf.friend_store_user_id and su.delete_flag = 0\n" +
+            "on ldc.id = ldcsf.coupon_id\n" +
+            "left join store_user su_cp on su_cp.id = ldcsf.receiver_user_id and su_cp.delete_flag = 0\n" +
             "left join store_info si\n" +
-            "on si.id = su.store_id and si.delete_flag = 0\n" +
+            "on si.id = COALESCE(ldcsf.receiver_store_id, su_cp.store_id, ldcsf.store_user_id) and si.delete_flag = 0\n" +
             "${ew.customSqlSegment}")
     List<LifeDiscountCouponFriendRuleVo> getReceivedSendFriendCouponListwzhy(@Param(Constants.WRAPPER) QueryWrapper<LifeDiscountCouponFriendRuleVo> lifeDiscountCouponFriendRuleVoQueryWrapper);
 
@@ -110,7 +118,7 @@ public interface LifeDiscountCouponStoreFriendMapper extends BaseMapper<LifeDisc
             "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" +
-            "on su.id = ldcsf.friend_store_user_id and su.delete_flag = 0\n" +
+            "on COALESCE(ldcsf.sender_user_id, ldcsf.friend_store_user_id) = su.id and su.delete_flag = 0\n" +
             "left join store_info si\n" +
             "on si.id = su.store_id and si.delete_flag = 0\n" +
             "${ew.customSqlSegment}")

+ 7 - 0
alien-entity/src/main/java/shop/alien/mapper/LifeDiscountCouponUnavailableRulesMapper.java

@@ -1,8 +1,11 @@
 package shop.alien.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Param;
 import shop.alien.entity.store.LifeDiscountCouponUnavailableRules;
 
+import java.util.List;
+
 /**
  * <p>
  * 优惠券禁用规则表 Mapper 接口
@@ -13,4 +16,8 @@ import shop.alien.entity.store.LifeDiscountCouponUnavailableRules;
  */
 public interface LifeDiscountCouponUnavailableRulesMapper extends BaseMapper<LifeDiscountCouponUnavailableRules> {
 
+    /**
+     * 按优惠券模板 id 查询禁用规则(含逻辑删除)
+     */
+    List<LifeDiscountCouponUnavailableRules> listByDiscountCouponIdIncludeDeleted(@Param("discountCouponId") Integer discountCouponId);
 }

+ 16 - 2
alien-entity/src/main/resources/mapper/LifeDiscountCouponMapper.xml

@@ -37,7 +37,8 @@
         WHERE dis.store_id = #{storeId}
           AND dis.type = 4
           AND dis.coupon_status = #{couponStatus}
-          AND dis.single_qty != 0
+          AND dis.delete_flag = 0
+          AND (dis.unlimited_qty = 1 OR (dis.single_qty IS NOT NULL AND dis.single_qty != 0))
     </select>
 
     <!-- 单表查询 life_discount_coupon:type 不为 4 时使用 -->
@@ -46,7 +47,8 @@
         FROM life_discount_coupon
         WHERE store_id = #{storeId}
           AND coupon_status = #{couponStatus}
-          AND single_qty != 0
+          AND delete_flag = 0
+          AND (unlimited_qty = 1 OR (single_qty IS NOT NULL AND single_qty != 0))
         <if test="type != null">
           AND type = #{type}
         </if>
@@ -55,4 +57,16 @@
         </if>
     </select>
 
+    <select id="selectByIdIncludeDeleted" resultType="shop.alien.entity.store.LifeDiscountCoupon">
+        SELECT * FROM life_discount_coupon WHERE id = #{id}
+    </select>
+
+    <select id="selectByIdsIncludeDeleted" resultType="shop.alien.entity.store.LifeDiscountCoupon">
+        SELECT * FROM life_discount_coupon
+        WHERE id IN
+        <foreach collection="ids" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
+
 </mapper>

+ 9 - 0
alien-entity/src/main/resources/mapper/LifeDiscountCouponQuantumRulesMapper.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="shop.alien.mapper.LifeDiscountCouponQuantumRulesMapper">
+
+    <select id="listByDiscountCouponIdIncludeDeleted" resultType="shop.alien.entity.store.LifeDiscountCouponQuantumRules">
+        SELECT * FROM life_discount_coupon_quantum_rules WHERE discount_coupon_id = #{discountCouponId}
+    </select>
+
+</mapper>

+ 9 - 1
alien-entity/src/main/resources/mapper/LifeDiscountCouponStoreFriendMapper.xml

@@ -9,6 +9,10 @@
         <result column="coupon_id" property="couponId" />
         <result column="voucher_id" property="voucherId" />
         <result column="friend_store_user_id" property="friendStoreUserId" />
+        <result column="receiver_store_id" property="receiverStoreId" />
+        <result column="receiver_user_id" property="receiverUserId" />
+        <result column="sender_store_id" property="senderStoreId" />
+        <result column="sender_user_id" property="senderUserId" />
         <result column="delete_flag" property="deleteFlag" />
         <result column="created_time" property="createdTime" />
         <result column="updated_time" property="updatedTime" />
@@ -18,7 +22,11 @@
 
     <!-- 通用查询结果列 -->
     <sql id="Base_Column_List">
-        id, store_user_id, coupon_id, voucher_id, friend_store_user_id, delete_flag, created_time, updated_time, created_user_id, updated_user_id
+        id, store_user_id, coupon_id, voucher_id, friend_store_user_id,
+        receiver_store_id, receiver_user_id, sender_store_id, sender_user_id,
+        expiration_date, long_term_valid, start_date, end_date, single_qty,
+        delete_flag, created_time, updated_time, created_user_id, updated_user_id,
+        release_type
     </sql>
 
 </mapper>

+ 4 - 0
alien-entity/src/main/resources/mapper/LifeDiscountCouponUnavailableRulesMapper.xml

@@ -21,4 +21,8 @@
         id, discount_coupon_id, voucher_id, unavailable_rule_type, unavailable_rule_value, delete_flag, created_time, updated_time, created_user_id, updated_user_id
     </sql>
 
+    <select id="listByDiscountCouponIdIncludeDeleted" resultType="shop.alien.entity.store.LifeDiscountCouponUnavailableRules">
+        SELECT * FROM life_discount_coupon_unavailable_rules WHERE discount_coupon_id = #{discountCouponId}
+    </select>
+
 </mapper>

+ 2 - 2
alien-entity/src/main/resources/mapper/LifeUserOrderMapper.xml

@@ -120,7 +120,7 @@
         left join order_coupon_middle ocm on ocm.order_id = luo.id and ocm.delete_flag = 0
         left join total_coupon tc on tc.coupon_id = ocm.coupon_id and tc.coupon_type = luo.coupon_type
         left join life_discount_coupon_user ldcu on ldcu.id = luo.quan_id and ldcu.delete_flag = 0
-        left join life_discount_coupon ldc on ldc.id = ldcu.coupon_id and ldc.delete_flag = 0
+        left join life_discount_coupon ldc on ldc.id = ldcu.coupon_id
         left join life_user lu on lu.id = luo.user_id and lu.delete_flag = 0
         left join store_img simg on simg.id = tc.image_id and simg.delete_flag = 0
         left join store_business_info sbi on sbi.store_id = si.id and sbi.delete_flag = 0
@@ -153,7 +153,7 @@
         left join order_coupon_middle ocm on ocm.order_id = luo.id and ocm.delete_flag = 0
         inner join total_coupon tc on tc.coupon_id = ocm.coupon_id and tc.coupon_type = luo.coupon_type
         left join life_discount_coupon_user ldcu on ldcu.id = luo.quan_id and ldcu.delete_flag = 0
-        left join life_discount_coupon ldc on ldc.id = ldcu.coupon_id and ldc.delete_flag = 0
+        left join life_discount_coupon ldc on ldc.id = ldcu.coupon_id
         left join life_user lu on lu.id = luo.user_id and lu.delete_flag = 0
         left join store_img simg on simg.id = tc.image_id and simg.delete_flag = 0
         left join store_business_info sbi on sbi.store_id = si.id and sbi.delete_flag = 0

+ 1 - 1
alien-job/src/main/java/shop/alien/job/store/LifeCouponJob.java

@@ -65,7 +65,7 @@ public class LifeCouponJob {
         int sendCount = 0;
         for (LifeDiscountCouponUser userCoupon : userCoupons) {
             try {
-                LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(userCoupon.getCouponId());
+                LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(userCoupon.getCouponId());
                 if (coupon == null) {
                     log.warn("优惠券不存在, couponId={}", userCoupon.getCouponId());
                     continue;

+ 2 - 0
alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/LifeCouponPlatformServiceImpl.java

@@ -375,6 +375,8 @@ public class LifeCouponPlatformServiceImpl extends ServiceImpl<LifeCouponMapper,
                     break;
             }
         }
+        vo.setLongTermValid(0);
+        vo.setUnlimitedQty(0);
         return vo;
     }
 

+ 94 - 53
alien-store-platform/src/main/java/shop/alien/storeplatform/service/impl/LifeDiscountCouponPlatformServiceImpl.java

@@ -18,6 +18,8 @@ import shop.alien.mapper.*;
 import shop.alien.storeplatform.service.LifeDiscountCouponPlatformService;
 import shop.alien.storeplatform.service.LifeDiscountCouponQuantumRulesPlatformService;
 import shop.alien.util.common.constant.DiscountCouponEnum;
+import shop.alien.util.coupon.DiscountCouponExpirationUtil;
+import shop.alien.util.coupon.LifeDiscountCouponStock;
 
 import java.math.BigDecimal;
 import java.time.Instant;
@@ -76,6 +78,16 @@ public class LifeDiscountCouponPlatformServiceImpl extends ServiceImpl<LifeDisco
             } else {
                 lifeDiscountCoupon.setMinimumSpendingAmount(BigDecimal.ZERO);
             }
+            if (lifeDiscountCoupon.getLongTermValid() == null) {
+                lifeDiscountCoupon.setLongTermValid(0);
+            }
+            if (Integer.valueOf(1).equals(lifeDiscountCouponDto.getCouponStatus())
+                    && !Integer.valueOf(1).equals(lifeDiscountCoupon.getLongTermValid())
+                    && !DiscountCouponExpirationUtil.templateHasPositiveDayValidity(
+                    lifeDiscountCoupon.getSpecifiedDay(), lifeDiscountCoupon.getExpirationDate())) {
+                throw new IllegalArgumentException("请配置有效期天数(expirationDate 或 specifiedDay)或开启长期有效");
+            }
+            clearTemplateObsoleteAbsoluteUsePeriod(lifeDiscountCoupon);
             // 根据开始领取时间判断可领取状态
             // 判断是否在领取时间内
 
@@ -83,28 +95,18 @@ public class LifeDiscountCouponPlatformServiceImpl extends ServiceImpl<LifeDisco
             Instant instant = now.toInstant();
             ZoneId zoneId = ZoneId.systemDefault();
             LocalDate localNow = instant.atZone(zoneId).toLocalDate();
-            if (lifeDiscountCouponDto.getCouponStatus() == 1 && !StringUtils.isEmpty(lifeDiscountCoupon.getBeginGetDate()) || !StringUtils.isEmpty(lifeDiscountCoupon.getEndGetDate())) {
+            if (Integer.valueOf(1).equals(lifeDiscountCouponDto.getCouponStatus())
+                    && lifeDiscountCoupon.getBeginGetDate() != null
+                    && lifeDiscountCoupon.getEndGetDate() != null) {
                 int startResult = localNow.compareTo(lifeDiscountCoupon.getBeginGetDate());
                 int endResult = localNow.compareTo(lifeDiscountCoupon.getEndGetDate());
-                if ((lifeDiscountCouponDto.getCouponStatus() != null && lifeDiscountCouponDto.getCouponStatus() == 0) || (startResult < 0 || endResult > 0)) {
+                if (startResult < 0 || endResult > 0) {
                     lifeDiscountCoupon.setGetStatus(0);
                 } else {
                     lifeDiscountCoupon.setGetStatus(1);
                 }
             }
 
-            // 设置有效期
-            String specifiedDay = lifeDiscountCoupon.getSpecifiedDay();
-            if (!StringUtils.isEmpty(specifiedDay)) {
-                int sDay = Integer.parseInt(specifiedDay);
-                if (sDay > 0 && lifeDiscountCouponDto.getEndGetDate() != null) {
-                    Date endGetDate = Date.from(lifeDiscountCouponDto.getEndGetDate().atStartOfDay(ZoneId.systemDefault()).toInstant());
-                    Date validDate = addDaysToDateJava8(endGetDate, sDay);
-                    LocalDate validDateLocalDate = validDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
-                    lifeDiscountCoupon.setValidDate(validDateLocalDate);
-                }
-            }
-
             lifeDiscountCouponMapper.insert(lifeDiscountCoupon);
             //发布优惠券规则信息
             //周中规则保存
@@ -194,30 +196,20 @@ public class LifeDiscountCouponPlatformServiceImpl extends ServiceImpl<LifeDisco
             Instant instant = now.toInstant();
             ZoneId zoneId = ZoneId.systemDefault();
             LocalDate localNow = instant.atZone(zoneId).toLocalDate();
-            if (lifeDiscountCouponDto.getCouponStatus() == 1 && !StringUtils.isEmpty(lifeDiscountCoupon.getBeginGetDate()) || !StringUtils.isEmpty(lifeDiscountCoupon.getEndGetDate())) {
+            if (Integer.valueOf(1).equals(lifeDiscountCouponDto.getCouponStatus())
+                    && lifeDiscountCoupon.getBeginGetDate() != null
+                    && lifeDiscountCoupon.getEndGetDate() != null) {
                 int startResult = localNow.compareTo(lifeDiscountCoupon.getBeginGetDate());
                 int endResult = localNow.compareTo(lifeDiscountCoupon.getEndGetDate());
-                if ((lifeDiscountCouponDto.getCouponStatus() != null && lifeDiscountCouponDto.getCouponStatus() == 0) || (startResult < 0 || endResult > 0)) {
+                if (startResult < 0 || endResult > 0) {
                     lifeDiscountCoupon.setGetStatus(0);
                 } else {
                     lifeDiscountCoupon.setGetStatus(1);
                 }
-            } else if (lifeDiscountCouponDto.getCouponStatus() == 0) {
+            } else if (lifeDiscountCouponDto.getCouponStatus() != null && lifeDiscountCouponDto.getCouponStatus() == 0) {
                 lifeDiscountCoupon.setGetStatus(0);
             }
 
-            // 设置有效期
-            String specifiedDay = lifeDiscountCouponDto.getSpecifiedDay();
-            if (!StringUtils.isEmpty(specifiedDay)) {
-                int sDay = Integer.parseInt(specifiedDay);
-                if (sDay > 0 && lifeDiscountCouponDto.getEndGetDate() != null) {
-                    Date endGetDate = Date.from(lifeDiscountCouponDto.getEndGetDate().atStartOfDay(ZoneId.systemDefault()).toInstant());
-                    Date validDate = addDaysToDateJava8(endGetDate, sDay);
-                    LocalDate validDateLocalDate = validDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
-                    lifeDiscountCoupon.setValidDate(validDateLocalDate);
-                }
-            }
-
             lifeDiscountCouponMapper.updateById(lifeDiscountCoupon);
             //发布优惠券规则信息
             //先删除之前所有规则
@@ -347,14 +339,17 @@ public class LifeDiscountCouponPlatformServiceImpl extends ServiceImpl<LifeDisco
      */
     @Override
     public LifeDiscountCouponVo getCounponDetailById(String counponId, UserLoginInfo userLoginInfo) {
-        LifeDiscountCoupon lifeDiscountCoupon = lifeDiscountCouponMapper.selectById(counponId);
+        int couponIdInt = Integer.parseInt(counponId);
+        LifeDiscountCoupon lifeDiscountCoupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(couponIdInt);
         LifeDiscountCouponVo lifeDiscountCouponVo = new LifeDiscountCouponVo();
-        lifeDiscountCouponVo.setCouponId(Integer.parseInt(counponId));
+        lifeDiscountCouponVo.setCouponId(couponIdInt);
+        if (lifeDiscountCoupon == null) {
+            return lifeDiscountCouponVo;
+        }
         BeanUtils.copyProperties(lifeDiscountCoupon, lifeDiscountCouponVo);
         //查规则信息
-        LambdaQueryWrapper<LifeDiscountCouponUnavailableRules> rulesLambdaQueryWrapper = new LambdaQueryWrapper<>();
-        rulesLambdaQueryWrapper.eq(LifeDiscountCouponUnavailableRules::getDiscountCouponId, counponId);
-        List<LifeDiscountCouponUnavailableRules> lifeDiscountCouponUnavailableRules = lifeDiscountCouponUnavailableRulesMapper.selectList(rulesLambdaQueryWrapper);
+        List<LifeDiscountCouponUnavailableRules> lifeDiscountCouponUnavailableRules =
+                lifeDiscountCouponUnavailableRulesMapper.listByDiscountCouponIdIncludeDeleted(couponIdInt);
 
         List<String> weeklyDisabledList = new ArrayList<>();
         List<String> holidayDisabledList = new ArrayList<>();
@@ -375,9 +370,8 @@ public class LifeDiscountCouponPlatformServiceImpl extends ServiceImpl<LifeDisco
         lifeDiscountCouponVo.setClaimRule(claimRule);
 
         // 查询时间段规则信息
-        LambdaQueryWrapper<LifeDiscountCouponQuantumRules> quantumRulesLambdaQueryWrapper = new LambdaQueryWrapper<>();
-        quantumRulesLambdaQueryWrapper.eq(LifeDiscountCouponQuantumRules::getDiscountCouponId, counponId);
-        List<LifeDiscountCouponQuantumRules> lifeDiscountCouponQuantumRulesList = lifeDiscountCouponQuantumRulesMapper.selectList(quantumRulesLambdaQueryWrapper);
+        List<LifeDiscountCouponQuantumRules> lifeDiscountCouponQuantumRulesList =
+                lifeDiscountCouponQuantumRulesMapper.listByDiscountCouponIdIncludeDeleted(couponIdInt);
         List<LifeDiscountCouponQuantumRules> availableTimeQuantumList = new ArrayList<>();
         List<LifeDiscountCouponQuantumRules> customizeUnavailableTimeQuantumList = new ArrayList<>();
         for (LifeDiscountCouponQuantumRules lifeDiscountCouponQuantumRules : lifeDiscountCouponQuantumRulesList) {
@@ -468,17 +462,20 @@ public class LifeDiscountCouponPlatformServiceImpl extends ServiceImpl<LifeDisco
         }
         //获取当日日期
         Date now = new Date();
+        LocalDate pureLocalDate = now.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
         //如果根据类型查询
         if (!StringUtils.isEmpty(couponStatus) && couponStatus.equals(1)) {//进行中
-            //开始时间小于当前时间
-            lifeDiscountCouponLambdaQueryWrapper.le(LifeDiscountCoupon::getBeginGetDate, getPureDate(now));
-            //结束时间大于当前时间
-            lifeDiscountCouponLambdaQueryWrapper.ge(LifeDiscountCoupon::getEndGetDate, getPureDate(now));
-            lifeDiscountCouponLambdaQueryWrapper.gt(LifeDiscountCoupon::getSingleQty, 0);
-
-            //不要已暂停关闭领取的
-            lifeDiscountCouponLambdaQueryWrapper.eq(LifeDiscountCoupon::getGetStatus, 1);
-            lifeDiscountCouponLambdaQueryWrapper.eq(LifeDiscountCoupon::getCouponStatus, 1);
+            lifeDiscountCouponLambdaQueryWrapper.eq(LifeDiscountCoupon::getGetStatus, 1)
+                    .eq(LifeDiscountCoupon::getCouponStatus, 1)
+                    .and(w -> w.eq(LifeDiscountCoupon::getUnlimitedQty, 1)
+                            .or()
+                            .gt(LifeDiscountCoupon::getSingleQty, 0))
+                    .and(w -> w.and(w2 -> w2.isNull(LifeDiscountCoupon::getBeginGetDate)
+                                    .or()
+                                    .le(LifeDiscountCoupon::getBeginGetDate, pureLocalDate))
+                            .and(w2 -> w2.isNull(LifeDiscountCoupon::getEndGetDate)
+                                    .or()
+                                    .ge(LifeDiscountCoupon::getEndGetDate, pureLocalDate)));
         } else if (!StringUtils.isEmpty(couponStatus) && couponStatus.equals(2)) {//已结束
             //结束时间小于当前时间
             lifeDiscountCouponLambdaQueryWrapper.lt(LifeDiscountCoupon::getEndGetDate, getPureDate(now));
@@ -507,7 +504,8 @@ public class LifeDiscountCouponPlatformServiceImpl extends ServiceImpl<LifeDisco
                 if (lifeDiscountCoupon.getCouponStatus() == 0) {
                     lifeDiscountCouponVo.setStatus(Integer.parseInt(DiscountCouponEnum.DRAFT.getValue()));
                     lifeDiscountCouponVo.setStatusDesc("草稿");
-                } else if (lifeDiscountCoupon.getSingleQty() == null || lifeDiscountCoupon.getSingleQty() == 0) {
+                } else if (!LifeDiscountCouponStock.hasTemplateStockRemaining(
+                        lifeDiscountCoupon.getUnlimitedQty(), lifeDiscountCoupon.getSingleQty())) {
                     lifeDiscountCouponVo.setStatus(Integer.parseInt(DiscountCouponEnum.HAVE_SOLD_OUT.getValue()));
                     lifeDiscountCouponVo.setStatusDesc("已售罄");
                 } else if (startResult < 0) {
@@ -524,12 +522,13 @@ public class LifeDiscountCouponPlatformServiceImpl extends ServiceImpl<LifeDisco
                     lifeDiscountCouponVo.setStatusDesc("进行中");
                 }
             } else {
-                if (lifeDiscountCoupon.getCouponStatus() == 0) {
-                    lifeDiscountCouponVo.setStatus(Integer.parseInt(DiscountCouponEnum.DRAFT.getValue()));
-                    lifeDiscountCouponVo.setStatusDesc("草稿");
-                } else {
-                    continue;
-                }
+                int st = resolvePlatformListStatusWithoutGetWindow(
+                        lifeDiscountCoupon.getCouponStatus(),
+                        lifeDiscountCoupon.getGetStatus(),
+                        lifeDiscountCoupon.getUnlimitedQty(),
+                        lifeDiscountCoupon.getSingleQty());
+                lifeDiscountCouponVo.setStatus(st);
+                lifeDiscountCouponVo.setStatusDesc(platformStatusDesc(st));
             }
             // 查询三个规则
             List<LifeDiscountCouponUnavailableRules> discountCouponId = lifeDiscountCouponUnavailableRulesMapper.selectList(new QueryWrapper<LifeDiscountCouponUnavailableRules>().eq("discount_coupon_id", lifeDiscountCoupon.getId()));
@@ -560,4 +559,46 @@ public class LifeDiscountCouponPlatformServiceImpl extends ServiceImpl<LifeDisco
         lifeDiscountCouponVoIPage.setRecords(lifeDiscountCouponVos);
         return lifeDiscountCouponVoIPage;
     }
+
+    private int resolvePlatformListStatusWithoutGetWindow(Integer couponStatus, Integer getStatus,
+                                                          Integer unlimitedQty, Integer singleQty) {
+        if (couponStatus != null && couponStatus == 0) {
+            return Integer.parseInt(DiscountCouponEnum.DRAFT.getValue());
+        }
+        if (!LifeDiscountCouponStock.hasTemplateStockRemaining(unlimitedQty, singleQty)) {
+            return Integer.parseInt(DiscountCouponEnum.HAVE_SOLD_OUT.getValue());
+        }
+        if (getStatus == null || getStatus.toString().equals(DiscountCouponEnum.NO_GET.getValue())) {
+            return Integer.parseInt(DiscountCouponEnum.SUSPEND_GET.getValue());
+        }
+        return Integer.parseInt(DiscountCouponEnum.UNDER_WAY.getValue());
+    }
+
+    private static String platformStatusDesc(int st) {
+        if (st == Integer.parseInt(DiscountCouponEnum.DRAFT.getValue())) {
+            return "草稿";
+        }
+        if (st == Integer.parseInt(DiscountCouponEnum.UNDER_WAY.getValue())) {
+            return "进行中";
+        }
+        if (st == Integer.parseInt(DiscountCouponEnum.FINISHED.getValue())) {
+            return "已结束";
+        }
+        if (st == Integer.parseInt(DiscountCouponEnum.HAVE_NOT_STARTED.getValue())) {
+            return "未开始";
+        }
+        if (st == Integer.parseInt(DiscountCouponEnum.SUSPEND_GET.getValue())) {
+            return "已暂停";
+        }
+        if (st == Integer.parseInt(DiscountCouponEnum.HAVE_SOLD_OUT.getValue())) {
+            return "已售罄";
+        }
+        return "";
+    }
+
+    private static void clearTemplateObsoleteAbsoluteUsePeriod(LifeDiscountCoupon c) {
+        c.setStartDate(null);
+        c.setEndDate(null);
+        c.setValidDate(null);
+    }
 }

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

@@ -379,7 +379,7 @@ public class OperationalActivityServiceImpl implements OperationalActivityServic
 
         // 设置优惠券名称和类型(判空处理)
         if (activity.getCouponId() != null) {
-            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(activity.getCouponId());
+            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(activity.getCouponId());
             if (coupon != null) {
                 vo.setCouponName(coupon.getName());
                 // 设置优惠券类型(满减券/折扣券):1=满减券,2=折扣券
@@ -461,7 +461,7 @@ public class OperationalActivityServiceImpl implements OperationalActivityServic
 
             // 设置优惠券名称(判空处理)
             if (activity.getCouponId() != null) {
-                LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(activity.getCouponId());
+                LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(activity.getCouponId());
                 if (coupon != null) {
                     vo.setCouponName(coupon.getName());
                 }

+ 97 - 19
alien-store/src/main/java/shop/alien/store/controller/LifeCollectController.java

@@ -1,27 +1,38 @@
 package shop.alien.store.controller;
 
+import cn.hutool.core.collection.CollectionUtil;
+import com.alibaba.fastjson.JSONObject;
 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 io.swagger.annotations.*;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.poi.util.StringUtil;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.StringUtils;
 import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.second.SecondGoods;
+import shop.alien.entity.second.vo.SecondCommentVo;
+import shop.alien.entity.second.vo.SecondGoodsRecommendVo;
+import shop.alien.entity.second.vo.SecondGoodsVo;
 import shop.alien.entity.store.*;
 import shop.alien.entity.store.vo.StoreInfoVo;
 import shop.alien.mapper.*;
 import shop.alien.mapper.second.SecondGoodsMapper;
+import shop.alien.mapper.second.SecondRecommendMapper;
 import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.config.GaoDeMapUtil;
 import shop.alien.store.service.LifeDiscountCouponService;
+import shop.alien.util.common.Constants;
+import shop.alien.util.common.JwtUtil;
 import shop.alien.util.common.ListToPage;
 
+import java.math.BigDecimal;
 import java.text.DecimalFormat;
 import java.util.*;
 import java.util.stream.Collectors;
@@ -39,6 +50,11 @@ public class LifeCollectController {
 
     private final LifeCollectMapper lifeCollectMapper;
 
+
+    private SecondRecommendMapper secondRecommendMapper;
+
+    private final LifeUserMapper lifeUserMapper;
+
     private final SecondGoodsMapper secondGoodsMapper;
 
     private final StoreInfoMapper storeInfoMapper;
@@ -55,8 +71,6 @@ public class LifeCollectController {
 
     private final LifeGroupBuyMainMapper lifeGroupBuyMainMapper;
 
-    private final LifeDiscountCouponService lifeDiscountCouponService;
-
     @ApiOperation("收藏列表")
     @ApiOperationSupport(order = 1)
     @ApiImplicitParams({@ApiImplicitParam(name = "page", value = "分页页数", dataType = "String", paramType = "query", required = true),
@@ -233,23 +247,6 @@ public class LifeCollectController {
             LambdaQueryWrapper<LifeCollect> queryWrapper = new LambdaQueryWrapper<>();
             queryWrapper.eq(LifeCollect::getStoreId, lifeCollect.getStoreId());
             storeInfoMapper.update(null, new LambdaUpdateWrapper<StoreInfo>().eq(StoreInfo::getId, lifeCollect.getStoreId()).set(StoreInfo::getCollectNum, lifeCollectMapper.selectCount(queryWrapper)));
-            
-            // 收藏店铺时自动发放优惠券(每种类型一张)
-            if (StringUtils.hasText(lifeCollect.getUserId())) {
-                try {
-                    Integer userId = Integer.parseInt(lifeCollect.getUserId());
-                    // 传入收藏记录ID,用于更新发放标记
-                    int issuedCount = lifeDiscountCouponService.issueCouponsForStoreCollect(userId, lifeCollect.getStoreId(), lifeCollect.getId());
-                    if (issuedCount > 0) {
-                        log.info("收藏店铺自动发放优惠券成功,userId={}, storeId={}, collectId={}, issuedCount={}", 
-                                userId, lifeCollect.getStoreId(), lifeCollect.getId(), issuedCount);
-                    }
-                } catch (NumberFormatException e) {
-                    log.warn("收藏店铺发放优惠券失败:用户ID格式错误,userId={}, storeId={}", lifeCollect.getUserId(), lifeCollect.getStoreId());
-                } catch (Exception e) {
-                    log.error("收藏店铺发放优惠券异常,userId={}, storeId={}, error={}", lifeCollect.getUserId(), lifeCollect.getStoreId(), e.getMessage(), e);
-                }
-            }
         }
 
 
@@ -310,4 +307,85 @@ public class LifeCollectController {
         return R.success("取消收藏成功");
     }
 
+
+    @ApiOperation("收藏商品列表")
+    @PostMapping("/collectSecondGoodsByPage")
+    public R<IPage<SecondGoodsRecommendVo>> collectSecondGoodsByPage(
+            @RequestParam(value = "pageNum", required = false) Integer pageNum,
+            @RequestParam(value = "pageSize", required = false) Integer pageSize,
+            @RequestParam(value = "longitude", required = false) String longitude,
+            @RequestParam(value = "latitude", required = false) String latitude) throws Exception {
+        log.info("LifeCollectController.cancelCollect?pageNum={},pageSize={},longitude={},latitude={}", pageNum, pageSize, longitude, latitude);
+        try {
+            IPage<SecondGoodsRecommendVo> page = new Page<>(pageNum, pageSize);
+            JSONObject data = JwtUtil.getCurrentUserInfo();
+            String phoneId = null;
+            Integer userId = null;
+            if (data != null) {
+                phoneId = data.getString("phone");
+                userId = data.getInteger("userId");
+            }
+            if (StringUtil.isBlank(phoneId)) {
+                return null;
+            }
+            LifeUser lifeUser = lifeUserMapper.selectById(userId);
+            // 获取商品屏蔽列表
+            List<SecondGoods> shieldedGoodsList = getShieldedGoodsList(userId);
+            // 提取屏蔽商品ID
+            List<Integer> shieldedGoodsIds = shieldedGoodsList.stream()
+                    .map(SecondGoods::getId)
+                    .collect(Collectors.toList());
+
+//            QueryWrapper<SecondGoodsVo> queryWrapper = new QueryWrapper<>();
+//            queryWrapper
+//                    // 可以查看已删除的商品数据
+//                    .eq("sg.delete_flag", Constants.DeleteFlag.NOT_DELETED)
+//                    .eq("lc.delete_flag", Constants.DeleteFlag.NOT_DELETED)
+//                    .notIn(CollectionUtil.isNotEmpty(shieldedGoodsIds), "sg.id", shieldedGoodsIds)
+//                    .eq("lc.user_id", "user_"+lifeUser.getUserPhone())
+//                    .orderByDesc("lc.created_time");
+
+            IPage<SecondGoodsRecommendVo> list = lifeCollectMapper.collectSecondGoodsByPage(page, userId, "user_" + phoneId, longitude + "," + latitude, shieldedGoodsIds);
+            List<Integer> idList = list.getRecords().stream() // 创建流
+                    .map(obj -> obj.getId())   // 提取每个元素的 ID
+                    .collect(Collectors.toList());
+            if (CollectionUtil.isEmpty(idList)) {
+                return R.data(list);
+            }
+//            List<SecondCommentVo> commentList =secondRecommendMapper.querySecondCommentInfo(idList);
+            list.getRecords().forEach(item -> {
+                // 距离拼接
+                if (StringUtil.isNotBlank(item.getDist())) {
+                    item.setPosition("距离" + item.getDist() + "km");
+                }
+                // 价格保留两位小数
+                item.setPrice(item.getAmount() != null ? item.getAmount().setScale(2, BigDecimal.ROUND_HALF_UP).toString() : null);
+
+                // 评论列表
+//                List<SecondCommentVo> cList = new ArrayList<>();
+//                commentList.forEach(comment -> {
+//                    if (item.getId().equals(comment.getBusinessId())) {
+//                        cList.add(comment);
+//                    }
+//                });
+//                if (cList.size() > 0) {
+//                    item.setCommentList(cList);
+//                }
+            });
+            return R.data(list);
+        } catch (Exception e){
+            log.error("收藏商品列表 Error Mgs={}", e.getMessage());
+            throw new Exception(e);
+        }
+    }
+
+    /**
+     * 获取商品屏蔽列表
+     * @param userId 用户ID
+     * @return 商品屏蔽列表
+     */
+    private List<SecondGoods> getShieldedGoodsList(Integer userId) {
+        // 调用mapper方法
+        return secondGoodsMapper.getShieldedGoodsList(userId);
+    }
 }

+ 22 - 12
alien-store/src/main/java/shop/alien/store/controller/LifeDiscountCouponController.java

@@ -43,21 +43,25 @@ public class LifeDiscountCouponController {
 
     private final LifeDiscountCouponUserService lifeDiscountCouponUserService;
 
-    @ApiOperation("发布优惠券")
+    @ApiOperation(value = "新增门店优惠券", notes = "仅写入 life_discount_coupon(满减/折扣券)。不创建代金券。couponStatus=0 表示草稿。")
     @ApiOperationSupport(order = 1)
     @PostMapping("/addDiscountCoupon")
     public R<Boolean> addDiscountCoupon(@ApiIgnore @TokenInfo UserLoginInfo userLoginInfo, @RequestBody LifeDiscountCouponDto lifeDiscountCouponDto) {
-        log.info("LifeDiscountCouponController.addDiscountCoupon?lifeDiscountCoupon={}", lifeDiscountCouponDto.toString());
+        log.info("LifeDiscountCouponController.addDiscountCoupon userId={} name={} couponStatus={}",
+                userLoginInfo.getUserId(), lifeDiscountCouponDto.getName(), lifeDiscountCouponDto.getCouponStatus());
         try {
             boolean save = lifeDiscountCouponService.addDiscountCoupon(lifeDiscountCouponDto);
             if (!save) {
-                return R.fail("发布失败");
+                return R.fail("新增失败");
             }
+        } catch (IllegalArgumentException e) {
+            log.warn("LifeDiscountCouponController.addDiscountCoupon biz: {}", e.getMessage());
+            return R.fail(e.getMessage());
         } catch (Exception e) {
-            log.error("LifeDiscountCouponController.addDiscountCoupon ERROR Msg={}", e.getMessage());
-            return R.fail("发布失败");
+            log.error("LifeDiscountCouponController.addDiscountCoupon ERROR Msg={}", e.getMessage(), e);
+            return R.fail("新增失败");
         }
-        return R.success("发布成功");
+        return R.success("新增成功");
     }
 
     @ApiOperation("修改优惠券信息")
@@ -70,8 +74,11 @@ public class LifeDiscountCouponController {
             if (!edited) {
                 return R.fail("修改失败");
             }
+        } catch (IllegalArgumentException e) {
+            log.warn("LifeDiscountCouponController.editDiscountCoupon biz: {}", e.getMessage());
+            return R.fail(e.getMessage());
         } catch (Exception e) {
-            log.error("LifeDiscountCouponController.editDiscountCoupon ERROR Msg={}", e.getMessage());
+            log.error("LifeDiscountCouponController.editDiscountCoupon ERROR Msg={}", e.getMessage(), e);
             return R.fail("修改失败");
         }
         return R.success("修改成功");
@@ -87,8 +94,11 @@ public class LifeDiscountCouponController {
             if (!deleted) {
                 return R.fail("删除失败");
             }
+        } catch (IllegalArgumentException e) {
+            log.warn("LifeDiscountCouponController.deleteDiscountCoupon biz: {}", e.getMessage());
+            return R.fail(e.getMessage());
         } catch (Exception e) {
-            log.error("LifeDiscountCouponController.deleteDiscountCoupon ERROR Msg={}", e.getMessage());
+            log.error("LifeDiscountCouponController.deleteDiscountCoupon ERROR Msg={}", e.getMessage(), e);
             return R.fail("删除失败");
         }
         return R.success("删除成功");
@@ -212,8 +222,8 @@ public class LifeDiscountCouponController {
     @ApiImplicitParams({@ApiImplicitParam(name = "page", value = "分页页数", dataType = "Integer", paramType = "query", required = false),
             @ApiImplicitParam(name = "size", value = "分页条数", dataType = "Integer", paramType = "query", required = false),
             @ApiImplicitParam(name = "tabType", value = "分页类型(0:全部(未使用),1:即将过期,2:已使用,3:已过期)", dataType = "String", paramType = "query", required = true),
-            @ApiImplicitParam(name = "type", value = "券类型(不传:优惠券+代金券都返回,1:仅优惠券查 life_discount_coupon,4:仅代金券查 life_coupon)", dataType = "Integer", paramType = "query", required = false),
-            @ApiImplicitParam(name = "couponType", value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选,仅当type不为4时有效)", dataType = "Integer", paramType = "query", required = false),
+            @ApiImplicitParam(name = "type", value = "券类型(不传或传1:仅优惠券;代金券 type=4 已下线)", dataType = "Integer", paramType = "query", required = false),
+            @ApiImplicitParam(name = "couponType", value = "优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选)", dataType = "Integer", paramType = "query", required = false),
             @ApiImplicitParam(name = "storeId", value = "商铺ID,可为空,传则仅返回该商铺的优惠券", dataType = "String", paramType = "query", required = false)
     })
     public R<List<LifeDiscountCouponVo>> getUserCouponList(@ApiIgnore @TokenInfo UserLoginInfo userLoginInfo,
@@ -234,8 +244,8 @@ public class LifeDiscountCouponController {
             if (size < 1 || size > 100) {
                 size = 10;
             }
-            if (type != null && type != 1 && type != 4) {
-                return R.fail("券类型参数错误,必须为1(优惠券)或4(代金券)");
+            if (type != null && type != 1) {
+                return R.fail("券类型参数错误:仅支持不传或1(优惠券);代金券已下线");
             }
             if (couponType != null && couponType != 1 && couponType != 2) {
                 return R.fail("优惠券类型参数错误,必须为1(满减券)或2(折扣券)");

+ 22 - 27
alien-store/src/main/java/shop/alien/store/controller/LifeDiscountCouponStoreFriendController.java

@@ -22,9 +22,7 @@ import java.util.List;
 import java.util.Map;
 
 /**
- * <p>
- * 优惠券商户发放好友优惠券关系表	 前端控制器
- * </p>
+ * 商户间好友优惠券:设置可向好友发的券、赠券、以及赠券记录/规则等。
  *
  * @author ssk
  * @since 2025-02-19
@@ -52,13 +50,14 @@ public class LifeDiscountCouponStoreFriendController {
         }
     }
 
-@TrackEvent(
-          eventType = "COUPON_GIVE",
-           eventCategory = "COUPON",
-           storeId = "#{#lifeDiscountCouponStoreFriendDto.storeId}",
-           targetType = "COUPON"
-   )
-    @ApiOperation("给好友发放优惠券")
+    @TrackEvent(
+            eventType = "COUPON_GIVE",
+            eventCategory = "COUPON",
+            storeId = "#{#lifeDiscountCouponStoreFriendDto.storeId}",
+            targetType = "COUPON"
+    )
+    @ApiOperation(value = "向好友店铺赠送优惠券", notes = "校验库存后扣减 life_discount_coupon;新增或合并 life_discount_coupon_store_friend。"
+            + " 领取后有效天数、长期有效、活动起止等均以服务端按 couponId 查询模板为准,请求中勿传有效期字段(传入也会被忽略)。")
     @ApiOperationSupport(order = 2)
     @PostMapping("/setFriendCoupon")
     public R<Boolean> setFriendCoupon(@ApiIgnore @TokenInfo UserLoginInfo userLoginInfo, @RequestBody LifeDiscountCouponStoreFriendDto lifeDiscountCouponStoreFriendDto) {
@@ -117,7 +116,6 @@ public class LifeDiscountCouponStoreFriendController {
         return R.success("操作成功");
     }
 
-
     @ApiOperation("删除好友优惠券")
     @ApiOperationSupport(order = 2)
     @DeleteMapping("/deleteLifeDiscountCouponStoreFriend/{id}")
@@ -161,13 +159,13 @@ public class LifeDiscountCouponStoreFriendController {
         return R.data(lifeDiscountCouponStoreFriendService.getRuleById(id));
     }
 
-    @ApiOperation("获取收到的好友优惠券列表。couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券。type=4 返回代金券,否则返回优惠券")
+    @ApiOperation("获取收到的好友优惠券列表(仅优惠券模板)。条件对齐 receiver_store_id;按赠送方分组时依赖 sender_user_id。type=4 返回空;couponType 筛选满减/折扣。")
     @GetMapping("/getReceivedFriendCouponList")
     @ApiImplicitParams({
-            @ApiImplicitParam(name = "storeId", value = "店铺id", dataType = "String", paramType = "query", required = true),
-            @ApiImplicitParam(name = "friendStoreUserId", value = "好友店铺用户id", dataType = "String", paramType = "query", required = false),
-            @ApiImplicitParam(name = "type", value = "类型:4=仅代金券,不传=全部(优惠券+代金券)(可选)", dataType = "Integer", paramType = "query", required = false),
-            @ApiImplicitParam(name = "couponType", value = "优惠券类型:1=满减,2=折扣,不传=全部优惠券(可选,仅当type不为4时有效)", dataType = "Integer", paramType = "query", required = false)
+            @ApiImplicitParam(name = "storeId", value = "当前接收方店铺 store_info.id", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "friendStoreUserId", value = "可选,筛选指定赠送方商户 store_user.id", dataType = "String", paramType = "query", required = false),
+            @ApiImplicitParam(name = "type", value = "传 4 返回空(历史参数)", dataType = "Integer", paramType = "query", required = false),
+            @ApiImplicitParam(name = "couponType", value = "1=满减,2=折扣,不传=全部", dataType = "Integer", paramType = "query", required = false)
     })
     public R<List<LifeDiscountCouponFriendRuleDetailVo>> getReceivedFriendCouponList(@RequestParam(value = "storeId") String storeId,
                                                                                      @RequestParam(value = "friendStoreUserId", required = false) String friendStoreUserId,
@@ -199,13 +197,15 @@ public class LifeDiscountCouponStoreFriendController {
         }
     }
 
-    @ApiOperation("查询赠券记录(商户送给商户券)。queryType=1查询我收到的,queryType=2查询我送出的。type=4仅代金券,不传返回全部(优惠券+代金券)。couponType=1仅满减券,couponType=2仅折扣券,不传返回全部优惠券")
+    @ApiOperation(value = "赠券历史列表(商户↔商户)", notes = "按当前登录商户查询好友间赠送记录。"
+            + " queryType=1:我收到的,展示对方赠送方店铺;queryType=2:我送出的,展示对方接收方店铺。"
+            + " 返回含券名称、面额、expirationDate、longTermValid 等;模板已逻辑删除时仍补全展示。type=4 时返回空列表(兼容旧客户端)。")
     @ApiImplicitParams({
-            @ApiImplicitParam(name = "storeUserId", value = "当前登录店铺用户id(必填)", dataType = "String", paramType = "query", required = true),
-            @ApiImplicitParam(name = "queryType", value = "查询类型:1=我收到的(所有好友赠送给我的),2=我送出的(我送给所有好友的)(必填)", dataType = "Integer", paramType = "query", required = true),
-            @ApiImplicitParam(name = "storeName", value = "店铺名称模糊查询(可选)", dataType = "String", paramType = "query", required = false),
-            @ApiImplicitParam(name = "type", value = "4=仅代金券,不传=全部(优惠券+代金券)(可选)", dataType = "Integer", paramType = "query", required = false),
-            @ApiImplicitParam(name = "couponType", value = "优惠券类型:1=仅满减,2=仅折扣,不传=全部优惠券(可选,仅当type不为4时有效)", dataType = "Integer", paramType = "query", required = false)
+            @ApiImplicitParam(name = "storeUserId", value = "当前登录商户 store_user.id", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "queryType", value = "1=我收到的,2=我送出的", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "storeName", value = "对方店铺名称模糊筛选(可选)", dataType = "String", paramType = "query", required = false),
+            @ApiImplicitParam(name = "type", value = "传 4 返回空(历史参数)", dataType = "Integer", paramType = "query", required = false),
+            @ApiImplicitParam(name = "couponType", value = "1=仅满减,2=仅折扣,不传=全部", dataType = "Integer", paramType = "query", required = false)
     })
     @GetMapping("/getReceivedSendFriendCouponList")
     public R<List<LifeDiscountCouponFriendRuleVo>> getReceivedSendFriendCouponList(@RequestParam(value = "storeUserId") String storeUserId,
@@ -215,10 +215,5 @@ public class LifeDiscountCouponStoreFriendController {
                                                                                    @RequestParam(value = "couponType", required = false) Integer couponType) {
         log.info("LifeDiscountCouponStoreFriendController.getReceivedSendFriendCouponList?storeUserId={},queryType={},storeName={},type={},couponType={}", storeUserId, queryType, storeName, type, couponType);
         return R.data(lifeDiscountCouponStoreFriendService.getReceivedSendFriendCouponList(storeUserId, queryType, storeName, type, couponType));
-
-
-
-
-
     }
 }

+ 3 - 13
alien-store/src/main/java/shop/alien/store/service/LifeDiscountCouponService.java

@@ -27,7 +27,7 @@ import java.util.Map;
 public interface LifeDiscountCouponService extends IService<LifeDiscountCoupon> {
 
     /**
-     * 发布优惠券
+     * 新增门店优惠券(仅 life_discount_coupon:满减/折扣;不创建 life_coupon 代金券)
      */
     boolean addDiscountCoupon(LifeDiscountCouponDto lifeDiscountCouponDto);
 
@@ -75,8 +75,8 @@ public interface LifeDiscountCouponService extends IService<LifeDiscountCoupon>
 
     /**
      * 获取该用户优惠券列表
-     * @param type 不传:优惠券+代金券都返回;1:仅优惠券(查 life_discount_coupon);4:仅代金券(查 life_coupon)
-     * @param couponType 优惠券类型:1=满减券,2=折扣券,null=全部优惠券(仅当type不为4时有效)
+     * @param type 不传或1:仅用户优惠券(life_discount_coupon);4 已不再支持
+     * @param couponType 优惠券类型:1=满减券,2=折扣券,null=全部
      * @param storeId 商铺ID,可为空,传则仅返回该商铺的优惠券
      */
     List<LifeDiscountCouponVo> getUserCouponList(UserLoginInfo userLoginInfo, int page, int size, String tabType, Integer type, Integer couponType, String storeId);
@@ -179,14 +179,4 @@ public interface LifeDiscountCouponService extends IService<LifeDiscountCoupon>
      * @return List<LifeDiscountCoupon>
      */
     List<LifeDiscountCoupon> getPlatformCoupon(Integer couponId, Integer couponType);
-
-    /**
-     * 收藏店铺时自动发放优惠券(每种类型一张)
-     *
-     * @param userId   用户ID
-     * @param storeId  店铺ID
-     * @param collectId 收藏记录ID(用于更新发放标记)
-     * @return 发放的优惠券数量
-     */
-    int issueCouponsForStoreCollect(Integer userId, String storeId, String collectId);
 }

+ 30 - 28
alien-store/src/main/java/shop/alien/store/service/LifeDiscountCouponStoreFriendService.java

@@ -27,7 +27,12 @@ public interface LifeDiscountCouponStoreFriendService extends IService<LifeDisco
     List<LifeDiscountCouponStoreFriendVo> getFriendCouponList(UserLoginInfo userLoginInfo, String friendUserId);
 
     /**
-     * 设置该好友店铺发放什么优惠券
+     * 向好友店铺赠送优惠券(POST /setFriendCoupon)。
+     * <ul>
+     *   <li>扣减或校验 {@code life_discount_coupon} 库存;</li>
+     *   <li>插入或合并 {@code life_discount_coupon_store_friend}(同券、同赠送方、同接收店铺合并 single_qty);</li>
+     *   <li>快照:{@code expiration_date}、{@code long_term_valid}、活动起止等<strong>仅</strong>根据请求中的 couponId 查询 {@code life_discount_coupon} 写入,不使用请求体中的效期字段。</li>
+     * </ul>
      */
     boolean setFriendCoupon(UserLoginInfo userLoginInfo, LifeDiscountCouponStoreFriendDto lifeDiscountCouponStoreFriendDto);
 
@@ -60,11 +65,9 @@ public interface LifeDiscountCouponStoreFriendService extends IService<LifeDisco
     void delFriendCouponRule(String id);
 
     /**
-     * 查询收到的赠券列表。type=4 返回代金券(life_coupon),否则返回优惠券(life_discount_coupon)
-     */
-    /**
-     * 获取收到的好友优惠券列表
-     * @param couponType 优惠券类型:1=满减券,2=折扣券,null=全部优惠券(仅当type不为4时有效)
+     * 获取收到的好友优惠券列表(仅 life_discount_coupon)。type=4 为历史参数,将返回空列表。
+     *
+     * @param couponType 优惠券类型:1=满减券,2=折扣券,null=全部
      */
     List<LifeDiscountCouponFriendRuleDetailVo> getReceivedFriendCouponList(String storeId, String friendStoreUserId, Integer type, Integer couponType);
 
@@ -73,35 +76,34 @@ public interface LifeDiscountCouponStoreFriendService extends IService<LifeDisco
     LifeDiscountCouponFriendRuleVo getRuleById(String id);
 
     /**
-     * 查询赠券记录(商户送给商户券)
-     * @param storeUserId 当前登录店铺用户id(必填)
-     * @param queryType 查询类型:1=我收到的(所有好友赠送给我的),2=我送出的(我送给所有好友的)(必填)
-     * @param storeName 店铺名称模糊查询(可选)
-     * @param type 4=仅代金券,不传=全部(优惠券+代金券)(可选)
-     * @param couponType 优惠券类型:1=仅满减券,2=仅折扣券,不传=全部优惠券(可选,仅当type不为4时有效)
-     * @return 赠券列表
+     * 赠券历史列表(GET /getReceivedSendFriendCouponList):商户间好友赠送记录。
+     * <ul>
+     *   <li>{@code queryType=1}:我收到的,列表展示<strong>对方赠送方</strong>店铺与头像;</li>
+     *   <li>{@code queryType=2}:我送出的,展示<strong>对方接收方</strong>店铺与头像;</li>
+     *   <li>仅 {@code life_discount_coupon}({@code coupon_id});{@code type=4} 时返回空(兼容旧客户端);</li>
+     *   <li>模板逻辑删除时通过按 id 查询含删除行补全券信息。</li>
+     * </ul>
+     *
+     * @param storeUserId 当前登录商户 {@code store_user.id}(必填)
+     * @param queryType   1=我收到的,2=我送出的
+     * @param storeName   对方店铺名称模糊筛选(可选)
+     * @param type        传 4 返回空列表(历史参数)
+     * @param couponType  1=仅满减,2=仅折扣,null=全部
      */
     List<LifeDiscountCouponFriendRuleVo> getReceivedSendFriendCouponList(String storeUserId, Integer queryType, String storeName, Integer type, Integer couponType);
 
     /**
-     * 好评送券:用户好评且AI审核通过后,按运营活动配置的优惠券/代金券ID发放到用户券包,发放成功后扣减库存;按券类型发送对应通知。
-     * 方法内部会查询活动配置。
-     *
-     * @param userId   评价用户ID(life用户)
-     * @param storeId  店铺ID(businessId)
-     * @return 发放数量(优惠券+代金券),0表示未发放
+     * 好评送券:用户好评且AI审核通过后,按运营活动配置的优惠券模板发放到用户券包。
      */
     int issueCouponForGoodRating(Integer userId, Integer storeId);
     
     /**
-     * 好评送券:直接按指定的优惠券/代金券ID发放到用户券包,发放成功后扣减库存;按券类型发送对应通知。
-     * 用于支持多个活动同时生效的场景。
-     *
-     * @param userId   评价用户ID(life用户)
-     * @param storeId  店铺ID(businessId)
-     * @param couponId 优惠券ID(life_discount_coupon),为null则不发优惠券
-     * @param voucherId 代金券ID(life_coupon),为null则不发代金券
-     * @return 发放数量(优惠券+代金券),0表示未发放
+     * 好评送券:按优惠券模板 ID(life_discount_coupon)发放到用户券包(默认 1 张)。
+     */
+    int issueCouponForGoodRating(Integer userId, Integer storeId, Integer couponId);
+
+    /**
+     * 好评送券:按运营活动 {@code couponQuantity} 写入对应张数用户券;{@code null} 按 1 张(兼容旧数据),{@code 0} 或负数表示不发。
      */
-    int issueCouponForGoodRating(Integer userId, Integer storeId, Integer couponId, Integer voucherId);
+    int issueCouponForGoodRating(Integer userId, Integer storeId, Integer couponId, Integer couponQuantity);
 }

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

@@ -802,7 +802,7 @@ public class LifeUserOrderService extends ServiceImpl<LifeUserOrderMapper, LifeU
             //将优惠券使用时间存入
             lifeDiscountCouponUser.setUseTime(date);
             lifeDiscountCouponUserMapper.updateById(lifeDiscountCouponUser);
-            lifeDiscountCoupon = lifeDiscountCouponMapper.selectById(lifeDiscountCouponUser.getCouponId());
+            lifeDiscountCoupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(lifeDiscountCouponUser.getCouponId());
             
             // 根据优惠券类型计算优惠金额
             Integer couponType = lifeDiscountCoupon.getCouponType();

+ 18 - 21
alien-store/src/main/java/shop/alien/store/service/impl/CommonRatingServiceImpl.java

@@ -592,41 +592,38 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
                     // 遍历所有活动,分别处理每个活动
                     int totalIssuedCount = 0;
                     for (StoreOperationalActivity activity : activities) {
-                        // 跳过没有配置优惠券或代金券的活动
-                        if (activity.getCouponId() == null && activity.getVoucherId() == null) {
-                            log.info("CommonRatingService 好评送券跳过:活动未配置优惠券或代金券,activityId={}, userId={}, storeId={}", 
+                        // 跳过没有配置优惠券的活动
+                        if (activity.getCouponId() == null || activity.getCouponId() <= 0) {
+                            log.info("CommonRatingService 好评送券跳过:活动未配置优惠券,activityId={}, userId={}, storeId={}", 
                                     activity.getId(), commonRating.getUserId(), businessId);
                             continue;
                         }
                         
-                        // 检查参与次数限制(每个活动单独计算
+                        // 参与次数限制(每条活动单独计;统计含本次已过审好评
                         Integer limit = activity.getParticipationLimit();
-                        Integer couponQuantity = activity.getCouponQuantity();
-                        if (limit != null && limit > 0 && couponQuantity != null && couponQuantity > 0) {
-                            // 审核成功之前的评论不计算在参与次数中(只统计审核成功时间在活动审核时间之后的评论)
+                        if (limit != null && limit > 0) {
                             int passedCount = commonRatingMapper.countPassedGoodRatingsByUserAndStore(
                                     commonRating.getUserId(), businessId, activity.getAuditTime());
-                            if (passedCount > limit && passedCount > couponQuantity) {
-                                log.info("CommonRatingService 好评送券跳过:超过运营活动参与次数 activityId={}, participation_limit={}, count={}, userId={}, storeId={}, activityStartTime={}",
-                                        activity.getId(), limit, passedCount, commonRating.getUserId(), businessId, activity.getStartTime());
-                                continue; // 跳过该活动,继续处理下一个
+                            if (passedCount > limit) {
+                                log.info("CommonRatingService 好评送券跳过:超过运营活动参与次数 activityId={}, participation_limit={}, count={}, userId={}, storeId={}",
+                                        activity.getId(), limit, passedCount, commonRating.getUserId(), businessId);
+                                continue;
                             }
                         }
-                        
-                        // 发放优惠券/代金券(使用4参数版本,直接传入活动配置的券ID)
+
                         int issuedCount = lifeDiscountCouponStoreFriendService.issueCouponForGoodRating(
-                                Integer.valueOf(Math.toIntExact(commonRating.getUserId())), businessId, 
-                                activity.getCouponId(), activity.getVoucherId());
+                                Integer.valueOf(Math.toIntExact(commonRating.getUserId())), businessId,
+                                activity.getCouponId(), activity.getCouponQuantity());
                         
                         if (issuedCount > 0) {
                             totalIssuedCount += issuedCount;
-                            log.info("CommonRatingService 好评送券成功:activityId={}, 发放数量={}, userId={}, storeId={}, couponId={}, voucherId={}",
-                                    activity.getId(), issuedCount, commonRating.getUserId(), businessId, 
-                                    activity.getCouponId(), activity.getVoucherId());
+                            log.info("CommonRatingService 好评送券成功:activityId={}, 发放数量={}, userId={}, storeId={}, couponId={}",
+                                    activity.getId(), issuedCount, commonRating.getUserId(), businessId,
+                                    activity.getCouponId());
                         } else {
-                            log.warn("CommonRatingService 好评送券失败:activityId={}, 发放数量=0, userId={}, storeId={}, couponId={}, voucherId={}",
-                                    activity.getId(), commonRating.getUserId(), businessId, 
-                                    activity.getCouponId(), activity.getVoucherId());
+                            log.warn("CommonRatingService 好评送券失败:activityId={}, 发放数量=0, userId={}, storeId={}, couponId={}",
+                                    activity.getId(), commonRating.getUserId(), businessId,
+                                    activity.getCouponId());
                         }
                     }
                     

Fichier diff supprimé car celui-ci est trop grand
+ 341 - 420
alien-store/src/main/java/shop/alien/store/service/impl/LifeDiscountCouponServiceImpl.java


+ 497 - 463
alien-store/src/main/java/shop/alien/store/service/impl/LifeDiscountCouponStoreFriendServiceImpl.java

@@ -23,6 +23,7 @@ import shop.alien.store.config.WebSocketProcess;
 import shop.alien.store.service.LifeDiscountCouponStoreFriendService;
 import shop.alien.util.common.constant.DiscountCouponEnum;
 import shop.alien.util.coupon.DiscountCouponExpirationUtil;
+import shop.alien.util.coupon.LifeDiscountCouponStock;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
@@ -35,31 +36,33 @@ import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.stream.Collectors;
 
 /**
- * <p>
- * 优惠券商户发放好友优惠券关系表	 服务实现类
- * </p>
- *
- * @author ssk
- * @since 2025-02-19
+ * 好友赠券领域:配置可赠券、执行赠送(扣库存 + 写 ldcsf)、消费后发券、赠券历史查询等。
  */
 @Slf4j
 @Service
 @RequiredArgsConstructor
 public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDiscountCouponStoreFriendMapper, LifeDiscountCouponStoreFriend> implements LifeDiscountCouponStoreFriendService {
 
+    /** {@link #getReceivedSendFriendCouponList} 的 queryType:我收到的 */
+    private static final int GIFT_HISTORY_QUERY_RECEIVED = 1;
+    /** {@link #getReceivedSendFriendCouponList} 的 queryType:我送出的 */
+    private static final int GIFT_HISTORY_QUERY_SENT = 2;
+
     final String STORE_PREFIX = "store_";
 
+    /** 单次好评发券的张数上限,防止运营误填特大数字 */
+    private static final int GOOD_RATING_ISSUE_COPIES_HARD_CAP = 50;
+
     private final StoreUserMapper storeUserMapper;
 
     private final LifeFansMapper lifeFansMapper;
 
     private final LifeDiscountCouponMapper lifeDiscountCouponMapper;
 
-    private final LifeCouponMapper lifeCouponMapper;
-
     private final LifeDiscountCouponStoreFriendMapper lifeDiscountCouponStoreFriendMapper;
 
     private final LifeDiscountCouponUserMapper lifeDiscountCouponUserMapper;
@@ -145,14 +148,10 @@ public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDi
         return result;
     }
 
-    @Override
     /**
-     * 为好友设置优惠券,涉及优惠券库存检查、发放记录处理和库存更新等操作。
-     *
-     * @param userLoginInfo 当前用户的登录信息
-     * @param lifeDiscountCouponStoreFriendDto 包含要设置的优惠券信息的 DTO 对象
-     * @return 如果所有优惠券都成功设置,返回 true;否则返回 false
+     * 向好友店铺赠送优惠券:按券列表逐笔调用 {@link #handleDiscountFriendCoupon}(库存 + ldcsf)。
      */
+    @Override
     public boolean setFriendCoupon(UserLoginInfo userLoginInfo, LifeDiscountCouponStoreFriendDto lifeDiscountCouponStoreFriendDto) {
         try {
             List<LifeDiscountCouponStoreFriendDto> coupons = lifeDiscountCouponStoreFriendDto.getCouponIds();
@@ -160,16 +159,9 @@ public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDi
                 return true;
             }
             for (LifeDiscountCouponStoreFriendDto couponDto : coupons) {
-                if (StringUtils.isNotEmpty(couponDto.getVoucherId())) {
-                    // 代金券:入参为 voucherId,对应 life_coupon 表
-                    if (!handleVoucherFriendCoupon(userLoginInfo, lifeDiscountCouponStoreFriendDto, couponDto)) {
-                        return false;
-                    }
-                } else if (couponDto.getCouponId() != null) {
-                    // 优惠券:入参为 couponId,对应 life_discount_coupon 表
-                    if (!handleDiscountFriendCoupon(userLoginInfo, lifeDiscountCouponStoreFriendDto, couponDto)) {
-                        return false;
-                    }
+                if (couponDto.getCouponId() != null
+                        && !handleDiscountFriendCoupon(userLoginInfo, lifeDiscountCouponStoreFriendDto, couponDto)) {
+                    return false;
                 }
             }
             return true;
@@ -179,91 +171,154 @@ public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDi
         }
     }
 
-    /** 处理代金券(life_coupon)发放好友 */
-    private boolean handleVoucherFriendCoupon(UserLoginInfo userLoginInfo, LifeDiscountCouponStoreFriendDto dto, LifeDiscountCouponStoreFriendDto couponDto) {
-        LifeCoupon lifeCoupon = lifeCouponMapper.selectById(couponDto.getVoucherId());
-        if (lifeCoupon == null) {
-            return false;
+    private static Integer parseStoreIdInt(String raw) {
+        if (raw == null) {
+            return null;
         }
-        int qty = couponDto.getSingleQty() != null ? couponDto.getSingleQty() : 0;
-        if (lifeCoupon.getSingleQty() == null || (lifeCoupon.getSingleQty() - qty) < 0) {
-            return false;
+        String t = raw.trim();
+        if (t.isEmpty()) {
+            return null;
         }
-        StoreUser storeUser = storeUserMapper.selectOne(new LambdaQueryWrapper<StoreUser>().eq(StoreUser::getStoreId, lifeCoupon.getStoreId()));
-        if (storeUser == null) {
-            return false;
+        try {
+            return Integer.parseInt(t);
+        } catch (NumberFormatException e) {
+            return null;
         }
-        LambdaQueryWrapper<LifeDiscountCouponStoreFriend> wrapper = new LambdaQueryWrapper<LifeDiscountCouponStoreFriend>()
-                .eq(LifeDiscountCouponStoreFriend::getVoucherId, couponDto.getVoucherId())
-                .eq(LifeDiscountCouponStoreFriend::getStoreUserId, dto.getFriendStoreUserId())
-                .eq(LifeDiscountCouponStoreFriend::getFriendStoreUserId, userLoginInfo.getUserId());
-        LifeDiscountCouponStoreFriend friend = lifeDiscountCouponStoreFriendMapper.selectOne(wrapper);
-
-        if (friend == null) {
-            friend = new LifeDiscountCouponStoreFriend();
-            friend.setVoucherId(couponDto.getVoucherId());
-            friend.setCouponId(null);
-            friend.setExpirationDate(lifeCoupon.getExpirationDate());
-            if (lifeCoupon.getStartDate() != null) {
-                friend.setStartDate(lifeCoupon.getStartDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
-            }
-            if (lifeCoupon.getEndDate() != null) {
-                friend.setEndDate(lifeCoupon.getEndDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
-            }
-            friend.setFriendStoreUserId(storeUser.getId());
-            friend.setSingleQty(qty);
-            friend.setStoreUserId(dto.getFriendStoreUserId());
-            friend.setReleaseType(1);
-            lifeDiscountCouponStoreFriendMapper.insert(friend);
-        } else {
-            friend.setSingleQty(friend.getSingleQty() + qty);
-            lifeDiscountCouponStoreFriendMapper.updateById(friend);
+    }
+
+    /**
+     * 同券同赠送人同接收店铺:新旧列任一命中即合并 single_qty(仅 life_discount_coupon 优惠券模板)。
+     */
+    private LambdaQueryWrapper<LifeDiscountCouponStoreFriend> duplicateGiftRowWrapper(
+            Integer couponId, Integer receiverStoreId, int senderUserId) {
+        LambdaQueryWrapper<LifeDiscountCouponStoreFriend> w = new LambdaQueryWrapper<>();
+        if (couponId == null) {
+            w.eq(LifeDiscountCouponStoreFriend::getId, -1);
+            return w;
         }
-        lifeCoupon.setSingleQty(lifeCoupon.getSingleQty() - qty);
-        lifeCouponMapper.updateById(lifeCoupon);
-        sendFriendCouponNotice(dto.getFriendStoreUserId(), userLoginInfo, qty, true);
-        return true;
+        w.eq(LifeDiscountCouponStoreFriend::getCouponId, couponId)
+                .isNull(LifeDiscountCouponStoreFriend::getVoucherId);
+        w.apply("( (receiver_store_id = {0} AND sender_user_id = {1}) OR (receiver_store_id IS NULL AND store_user_id = {0} AND friend_store_user_id = {1}) )",
+                receiverStoreId, senderUserId);
+        return w;
+    }
+
+    /**
+     * 赠送方 / 接收方新旧列对齐:receiver/store_user_id、sender/friend_store_user_id 同源,便于历史脚本与 Mapper 仍可读。
+     */
+    private void applyGiftParties(LifeDiscountCouponStoreFriend row, int senderUserId, Integer receiverStoreId, Integer receiverUserId) {
+        StoreUser sender = storeUserMapper.selectById(senderUserId);
+        Integer senderStoreId = sender != null ? sender.getStoreId() : null;
+        row.setSenderUserId(senderUserId);
+        row.setSenderStoreId(senderStoreId);
+        row.setReceiverStoreId(receiverStoreId);
+        row.setReceiverUserId(receiverUserId);
+        row.setFriendStoreUserId(senderUserId);
+        row.setStoreUserId(receiverStoreId);
+    }
+
+    /**
+     * 写入/更新 {@link LifeDiscountCouponStoreFriend} 时,有效期相关字段仅以「按 couponId 查到的」{@link LifeDiscountCoupon} 为准(请求体 DTO 对应属性已 JsonIgnore)。
+     */
+    private static void applyGiftValiditySnapshotFromTemplate(LifeDiscountCouponStoreFriend row, LifeDiscountCoupon template) {
+        row.setExpirationDate(template.getExpirationDate());
+        row.setLongTermValid(template.getLongTermValid());
+        row.setStartDate(template.getStartDate());
+        row.setEndDate(template.getEndDate());
+    }
+
+    /**
+     * 接收方商户用户:请求体显式传入优先;未传时按接收方店铺取一条未删除的商户账号(与赠券通知选取逻辑一致,便于四字段落库)。
+     */
+    private Integer resolveReceiverUserId(Integer receiverStoreId, Integer explicitReceiverUserId) {
+        if (explicitReceiverUserId != null) {
+            return explicitReceiverUserId;
+        }
+        if (receiverStoreId == null) {
+            return null;
+        }
+        List<StoreUser> list = storeUserMapper.selectList(new LambdaQueryWrapper<StoreUser>()
+                .eq(StoreUser::getStoreId, receiverStoreId)
+                .eq(StoreUser::getDeleteFlag, 0)
+                .orderByAsc(StoreUser::getId)
+                .last("LIMIT 1"));
+        if (CollectionUtils.isEmpty(list)) {
+            return null;
+        }
+        return list.get(0).getId();
+    }
+
+    /**
+     * 接收方维度筛选:优先 {@code receiver_store_id},兼容旧列 {@code store_user_id}(均为 store_info.id)。
+     */
+    private void applyLdcsfReceiverStoreFilter(QueryWrapper<?> q, int receiverStoreInfoId) {
+        q.and(w -> w.eq("ldcsf.receiver_store_id", receiverStoreInfoId)
+                .or(sub -> sub.isNull("ldcsf.receiver_store_id").eq("ldcsf.store_user_id", receiverStoreInfoId)));
+    }
+
+    /**
+     * 赠送方维度筛选:优先 {@code sender_user_id},兼容旧列 {@code friend_store_user_id}(均为 store_user.id)。
+     */
+    private void applyLdcsfSenderMerchantFilter(QueryWrapper<?> q, int senderStoreUserId) {
+        q.and(w -> w.eq("ldcsf.sender_user_id", senderStoreUserId)
+                .or(sub -> sub.isNull("ldcsf.sender_user_id").eq("ldcsf.friend_store_user_id", senderStoreUserId)));
     }
 
     /** 处理优惠券(life_discount_coupon)发放好友 */
     private boolean handleDiscountFriendCoupon(UserLoginInfo userLoginInfo, LifeDiscountCouponStoreFriendDto lifeDiscountCouponStoreFriendDto, LifeDiscountCouponStoreFriendDto couponDto) {
         LifeDiscountCoupon lifeDiscountCoupon = lifeDiscountCouponMapper.selectById(couponDto.getCouponId());
-        if (lifeDiscountCoupon == null || (lifeDiscountCoupon.getSingleQty() - couponDto.getSingleQty()) < 0) {
+        Integer receiverStoreId = lifeDiscountCouponStoreFriendDto.getFriendStoreUserId();
+        if (lifeDiscountCoupon == null || receiverStoreId == null) {
             return false;
         }
-        StoreUser storeUser = storeUserMapper.selectOne(new LambdaQueryWrapper<StoreUser>().eq(StoreUser::getStoreId, lifeDiscountCoupon.getStoreId()));
+        Integer receiverUserId = resolveReceiverUserId(receiverStoreId, lifeDiscountCouponStoreFriendDto.getReceiverUserId());
+        int sendQty = couponDto.getSingleQty() != null ? couponDto.getSingleQty() : 0;
+        if (!LifeDiscountCouponStock.isUnlimitedQty(lifeDiscountCoupon.getUnlimitedQty())) {
+            if (lifeDiscountCoupon.getSingleQty() == null
+                    || (lifeDiscountCoupon.getSingleQty() - sendQty) < 0) {
+                return false;
+            }
+        }
+        Integer tplStoreId = parseStoreIdInt(lifeDiscountCoupon.getStoreId());
+        if (tplStoreId == null) {
+            return false;
+        }
+        Integer tplMerchantCount = storeUserMapper.selectCount(new LambdaQueryWrapper<StoreUser>()
+                .eq(StoreUser::getStoreId, tplStoreId).eq(StoreUser::getDeleteFlag, 0));
+        if (tplMerchantCount == null || tplMerchantCount == 0) {
+            return false;
+        }
+
         LifeDiscountCouponStoreFriend lifeDiscountCouponStoreFriend = lifeDiscountCouponStoreFriendMapper.selectOne(
-                new LambdaQueryWrapper<LifeDiscountCouponStoreFriend>()
-                        .eq(LifeDiscountCouponStoreFriend::getCouponId, couponDto.getCouponId())
-                        .eq(LifeDiscountCouponStoreFriend::getStoreUserId, lifeDiscountCouponStoreFriendDto.getFriendStoreUserId())
-                        .eq(LifeDiscountCouponStoreFriend::getFriendStoreUserId, userLoginInfo.getUserId())
+                duplicateGiftRowWrapper(couponDto.getCouponId(), receiverStoreId, userLoginInfo.getUserId())
         );
 
         if (lifeDiscountCouponStoreFriend == null) {
             lifeDiscountCouponStoreFriend = new LifeDiscountCouponStoreFriend();
-            BeanUtils.copyProperties(lifeDiscountCouponStoreFriendDto, lifeDiscountCouponStoreFriend);
             lifeDiscountCouponStoreFriend.setCouponId(couponDto.getCouponId());
             lifeDiscountCouponStoreFriend.setVoucherId(null);
-            lifeDiscountCouponStoreFriend.setExpirationDate(lifeDiscountCoupon.getExpirationDate());
-            lifeDiscountCouponStoreFriend.setStartDate(lifeDiscountCoupon.getStartDate());
-            lifeDiscountCouponStoreFriend.setEndDate(lifeDiscountCoupon.getEndDate());
-            lifeDiscountCouponStoreFriend.setFriendStoreUserId(storeUser.getId());
+            applyGiftValiditySnapshotFromTemplate(lifeDiscountCouponStoreFriend, lifeDiscountCoupon);
             lifeDiscountCouponStoreFriend.setSingleQty(couponDto.getSingleQty());
-            lifeDiscountCouponStoreFriend.setStoreUserId(lifeDiscountCouponStoreFriendDto.getFriendStoreUserId());
             lifeDiscountCouponStoreFriend.setReleaseType(1);
+            applyGiftParties(lifeDiscountCouponStoreFriend, userLoginInfo.getUserId(), receiverStoreId, receiverUserId);
             lifeDiscountCouponStoreFriendMapper.insert(lifeDiscountCouponStoreFriend);
         } else {
+            applyGiftParties(lifeDiscountCouponStoreFriend, userLoginInfo.getUserId(), receiverStoreId, receiverUserId);
+            applyGiftValiditySnapshotFromTemplate(lifeDiscountCouponStoreFriend, lifeDiscountCoupon);
             lifeDiscountCouponStoreFriend.setSingleQty(lifeDiscountCouponStoreFriend.getSingleQty() + couponDto.getSingleQty());
             lifeDiscountCouponStoreFriendMapper.updateById(lifeDiscountCouponStoreFriend);
         }
-        lifeDiscountCoupon.setSingleQty(lifeDiscountCoupon.getSingleQty() - couponDto.getSingleQty());
+        if (!LifeDiscountCouponStock.isUnlimitedQty(lifeDiscountCoupon.getUnlimitedQty())
+                && lifeDiscountCoupon.getSingleQty() != null) {
+            lifeDiscountCoupon.setSingleQty(lifeDiscountCoupon.getSingleQty() - sendQty);
+        }
         lifeDiscountCouponMapper.updateById(lifeDiscountCoupon);
-        sendFriendCouponNotice(lifeDiscountCouponStoreFriendDto.getFriendStoreUserId(), userLoginInfo, couponDto.getSingleQty(), false);
+        sendFriendCouponNotice(lifeDiscountCouponStoreFriendDto.getFriendStoreUserId(), userLoginInfo, couponDto.getSingleQty());
         return true;
     }
 
-    /** 发送赠券通知:isVoucher true=代金券,false=优惠券 */
-    private void sendFriendCouponNotice(Integer friendStoreUserId, UserLoginInfo userLoginInfo, int qty, boolean isVoucher) {
+    /** 发送赠券通知 */
+    private void sendFriendCouponNotice(Integer friendStoreUserId, UserLoginInfo userLoginInfo, int qty) {
         if (friendStoreUserId == null) {
             return;
         }
@@ -287,8 +342,7 @@ public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDi
             }
         }
         LifeNotice lifeMessage = new LifeNotice();
-        String couponTypeName = isVoucher ? "代金券" : "优惠券";
-        String text = "您的好友" + storeName + "送了您" + qty + "张" + couponTypeName + ",快去使用吧!";
+        String text = "您的好友" + storeName + "送了您" + qty + "张优惠券,快去使用吧!";
         JSONObject jsonObject = new JSONObject();
         jsonObject.put("message", text);
         lifeMessage.setReceiverId("store_" + friendPhone);
@@ -351,8 +405,10 @@ public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDi
 
             // 根据店铺ID从数据库中查询该店铺为好友设定的所有优惠券信息
             List<LifeDiscountCouponStoreFriend> lifeDiscountCouponStoreFriends = lifeDiscountCouponStoreFriendMapper.selectList(
-                    // 使用LambdaQueryWrapper构建查询条件,筛选出店铺ID等于指定店铺ID的优惠券记录,并且发布状态为已发布
-                    new LambdaQueryWrapper<LifeDiscountCouponStoreFriend>().eq(LifeDiscountCouponStoreFriend::getStoreUserId, storeId)
+                    new LambdaQueryWrapper<LifeDiscountCouponStoreFriend>()
+                            .and(w -> w.eq(LifeDiscountCouponStoreFriend::getReceiverStoreId, storeId)
+                                    .or(sub -> sub.isNull(LifeDiscountCouponStoreFriend::getReceiverStoreId)
+                                            .eq(LifeDiscountCouponStoreFriend::getStoreUserId, storeId)))
                             .eq(LifeDiscountCouponStoreFriend::getReleaseType, 1)
                             .in(LifeDiscountCouponStoreFriend::getCouponId, couponList));
 
@@ -370,7 +426,9 @@ public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDi
                     // 设置查询条件:优惠券的结束日期大于等于当前日期,即优惠券在有效期内
                     queryWrapper.ge(LifeDiscountCoupon::getValidDate, currentDate);
                     // 设置查询条件:优惠券还有库存
-                    queryWrapper.gt(LifeDiscountCoupon::getSingleQty, 0);
+                    queryWrapper.and(w -> w.eq(LifeDiscountCoupon::getUnlimitedQty, 1)
+                            .or()
+                            .gt(LifeDiscountCoupon::getSingleQty, 0));
 
                     // 根据上述查询条件从数据库中查询符合条件的优惠券信息
                     LifeDiscountCoupon lifeDiscountCoupon = lifeDiscountCouponMapper.selectOne(queryWrapper);
@@ -392,6 +450,7 @@ public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDi
                             lifeDiscountCouponUser.setReceiveTime(new Date());
                             lifeDiscountCouponUser.setExpirationTime(DiscountCouponExpirationUtil.resolveLifeDiscountCouponExpiration(
                                     lifeDiscountCouponUser.getReceiveTime(),
+                                    lifeDiscountCoupon.getLongTermValid(),
                                     lifeDiscountCoupon.getSpecifiedDay(),
                                     lifeDiscountCoupon.getExpirationDate(),
                                     lifeDiscountCoupon.getValidDate(),
@@ -509,9 +568,12 @@ public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDi
 
             // 根据店铺ID从数据库中查询该店铺为好友设定的所有优惠券信息
             List<LifeDiscountCouponStoreFriend> lifeDiscountCouponStoreFriends = lifeDiscountCouponStoreFriendMapper.selectList(
-                    // 使用LambdaQueryWrapper构建查询条件,筛选出店铺ID等于指定店铺ID的优惠券记录,并且发布状态为已发布
-                    new LambdaQueryWrapper<LifeDiscountCouponStoreFriend>().eq(LifeDiscountCouponStoreFriend::getStoreUserId, storeId)
+                    new LambdaQueryWrapper<LifeDiscountCouponStoreFriend>()
+                            .and(w -> w.eq(LifeDiscountCouponStoreFriend::getReceiverStoreId, storeId)
+                                    .or(sub -> sub.isNull(LifeDiscountCouponStoreFriend::getReceiverStoreId)
+                                            .eq(LifeDiscountCouponStoreFriend::getStoreUserId, storeId)))
                             .eq(LifeDiscountCouponStoreFriend::getReleaseType, 1)
+                            .gt(LifeDiscountCouponStoreFriend::getSingleQty, 0)
    //                         .in(LifeDiscountCouponStoreFriend::getCouponId, couponList)
             );
 
@@ -527,15 +589,17 @@ public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDi
                     // 设置查询条件:优惠券的ID等于当前遍历到的优惠券ID
                     queryWrapper.eq(LifeDiscountCoupon::getId, coupon.getCouponId());
                     // 设置查询条件:优惠券的结束日期大于等于当前日期,即优惠券在有效期内
-                    queryWrapper.ge(LifeDiscountCoupon::getValidDate, currentDate);
+//                    queryWrapper.ge(LifeDiscountCoupon::getValidDate, currentDate);
                     // 设置查询条件:优惠券还有库存
-                    queryWrapper.gt(LifeDiscountCoupon::getSingleQty, 0);
+                    queryWrapper.and(w -> w.eq(LifeDiscountCoupon::getUnlimitedQty, 1)
+                            .or()
+                            .gt(LifeDiscountCoupon::getSingleQty, 0));
 
                     // 根据上述查询条件从数据库中查询符合条件的优惠券信息
                     LifeDiscountCoupon lifeDiscountCoupon = lifeDiscountCouponMapper.selectOne(queryWrapper);
 
                     // 如果查询到符合条件的优惠券,说明该优惠券可领
-                    if (lifeDiscountCoupon != null) {
+                    if (lifeDiscountCoupon != null  ) {
                         // 设定本次领取优惠券的数量,这里固定为1张
                         Integer receiveQuantity = 1;
 
@@ -650,14 +714,9 @@ public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDi
     @Override
     public LifeDiscountCouponFriendRuleVo saveFriendCouponRule(LifeDiscountCouponFriendRule lifeDiscountCouponFriendRule) {
         List<LifeDiscountCouponFriendRuleDetail> details = lifeDiscountCouponFriendRule.getDetails();
-        // 按列表中是否传入代金券id区分:有 voucherId 走代金券逻辑,否则走优惠券逻辑
         if (ObjectUtils.isNotEmpty(details)) {
             for (LifeDiscountCouponFriendRuleDetail detail : details) {
-                if (detail.getVoucherId() != null && !detail.getVoucherId().trim().isEmpty()) {
-                    detail.setCouponId(null);
-                } else {
-                    detail.setVoucherId(null);
-                }
+                detail.setVoucherId(null);
             }
         }
         if (ObjectUtils.isNotEmpty(lifeDiscountCouponFriendRule.getId())) {
@@ -698,21 +757,17 @@ public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDi
      */
     @Override
     public List<LifeDiscountCouponFriendRuleDetailVo> getReceivedFriendCouponList(String storeId, String friendStoreUserId, Integer type, Integer couponType) {
-        QueryWrapper<LifeDiscountCouponFriendRuleDetailVo> queryWrapper = new QueryWrapper<>();
-        queryWrapper.eq("ldcsf.store_user_id", storeId);
-        queryWrapper.eq("ldcsf.delete_flag", 0);
         if (Integer.valueOf(4).equals(type)) {
-            // 代金券:创建时为启用状态 2,需查出 release_type=1 与 2 的 LifeDiscountCouponStoreFriend 数据
-            queryWrapper.in("ldcsf.release_type", 1, 2);
-            queryWrapper.isNotNull("ldcsf.voucher_id");
-            if (StringUtils.isEmpty(friendStoreUserId)) {
-                queryWrapper.groupBy("ldcsf.friend_store_user_id").orderByDesc("couponNum");
-            } else {
-                queryWrapper.eq("ldcsf.friend_store_user_id", friendStoreUserId).groupBy("ldcsf.voucher_id").orderByDesc("couponNum");
-            }
-            return lifeDiscountCouponFriendRuleDetailMapper.getReceivedFriendVoucherList(queryWrapper);
+            return new ArrayList<>();
+        }
+        QueryWrapper<LifeDiscountCouponFriendRuleDetailVo> queryWrapper = new QueryWrapper<>();
+        try {
+            applyLdcsfReceiverStoreFilter(queryWrapper, Integer.parseInt(storeId.trim()));
+        } catch (NumberFormatException e) {
+            log.warn("getReceivedFriendCouponList 非法 storeId={}", storeId);
+            return new ArrayList<>();
         }
-        // 优惠券:保持原逻辑,只查 release_type=1
+        queryWrapper.eq("ldcsf.delete_flag", 0);
         queryWrapper.eq("ldcsf.release_type", 1);
         queryWrapper.isNotNull("ldcsf.coupon_id");
         //如果指定了优惠券类型(满减券或折扣券),添加筛选条件
@@ -720,9 +775,10 @@ public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDi
             queryWrapper.eq("ldc.coupon_type", couponType);
         }
         if (StringUtils.isEmpty(friendStoreUserId)) {
-            queryWrapper.groupBy("ldcsf.friend_store_user_id").orderByDesc("couponNum");
+            queryWrapper.groupBy("COALESCE(ldcsf.sender_user_id, ldcsf.friend_store_user_id)").orderByDesc("couponNum");
         } else {
-            queryWrapper.eq("ldcsf.friend_store_user_id", friendStoreUserId).groupBy("ldcsf.coupon_id").orderByDesc("couponNum");
+            appendFriendSenderFilter(queryWrapper, friendStoreUserId);
+            queryWrapper.groupBy("ldcsf.coupon_id").orderByDesc("couponNum");
         }
         return lifeDiscountCouponFriendRuleDetailMapper.getReceivedFriendCouponList(queryWrapper);
     }
@@ -796,7 +852,6 @@ public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDi
             
             // 如果规则表中没有 couponType,尝试从关联的优惠券中获取
             if (ruleVo.getCouponType() == null && ObjectUtils.isNotEmpty(lifeDiscountCouponFriendRuleDetails)) {
-                // 查找第一个优惠券(非代金券)的 couponType
                 for (LifeDiscountCouponFriendRuleDetailVo detail : lifeDiscountCouponFriendRuleDetails) {
                     if (detail.getCouponId() != null && detail.getCouponType() != null) {
                         ruleVo.setCouponType(detail.getCouponType());
@@ -809,189 +864,241 @@ public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDi
     }
 
     /**
-     * 代金券类型常量
+     * 赠券历史(GET /getReceivedSendFriendCouponList):根据「我收到 / 我送出」筛 ldcsf,再补模板与头像。
      */
-    private static final Integer TYPE_VOUCHER = 4;
-
     @Override
     public List<LifeDiscountCouponFriendRuleVo> getReceivedSendFriendCouponList(String storeUserId, Integer queryType, String storeName, Integer type, Integer couponType) {
-        // 参数校验
-        if (StringUtils.isEmpty(storeUserId)) {
-            log.warn("getReceivedSendFriendCouponList 参数错误:storeUserId 不能为空");
-            return new ArrayList<>();
-        }
-        if (queryType == null || (queryType != 1 && queryType != 2)) {
-            log.warn("getReceivedSendFriendCouponList 参数错误:queryType 必须为 1 或 2,当前值={}", queryType);
+        if (!validateGiftHistoryQueryParams(storeUserId, queryType, couponType)) {
             return new ArrayList<>();
         }
-        // 校验couponType参数(1=满减券,2=折扣券)
-        if (couponType != null && couponType != 1 && couponType != 2) {
-            log.warn("getReceivedSendFriendCouponList 参数错误:couponType 必须为 1(满减券)或 2(折扣券),当前值={}", couponType);
+
+        if (Integer.valueOf(4).equals(type)) {
             return new ArrayList<>();
         }
 
-        // 判断是否仅查询代金券
-        boolean voucherOnly = TYPE_VOUCHER.equals(type);
+        boolean isReceivedByMe = (queryType == GIFT_HISTORY_QUERY_RECEIVED);
+        return queryCouponList(isReceivedByMe, storeUserId, storeName, couponType);
+    }
 
-        // 确定查询类型
-        // 根据表结构(实体类注释):
-        // store_user_id: store_info的ID(店铺ID),收到券的店铺
-        // friend_store_user_id: store_user的ID(店铺用户ID),送券的用户
-        // 前端传入的 storeUserId 是当前登录账号的 store_user.id(店铺用户ID)
-        // queryType = 1: 我收到的(所有好友赠送给我的)-> store_user_id = 我的店铺ID(我是收到券的店铺,需要从店铺用户ID转换为店铺ID)
-        // queryType = 2: 我送出的(我送给所有好友的)-> friend_store_user_id = 我的店铺用户ID(我是送券的用户)
-        boolean isReceivedByMe = (queryType == 1);
+    private boolean validateGiftHistoryQueryParams(String storeUserId, Integer queryType, Integer couponType) {
+        if (StringUtils.isEmpty(storeUserId)) {
+            log.error("getReceivedSendFriendCouponList 参数错误:storeUserId 不能为空");
+            return false;
+        }
+        if (queryType == null || (queryType != GIFT_HISTORY_QUERY_RECEIVED && queryType != GIFT_HISTORY_QUERY_SENT)) {
+            log.error("getReceivedSendFriendCouponList 参数错误:queryType 必须为 {} 或 {},当前值={}", GIFT_HISTORY_QUERY_RECEIVED, GIFT_HISTORY_QUERY_SENT, queryType);
+            return false;
+        }
+        if (couponType != null && couponType != 1 && couponType != 2) {
+            log.error("getReceivedSendFriendCouponList 参数错误:couponType 必须为 1(满减券)或 2(折扣券),当前值={}", couponType);
+            return false;
+        }
+        return true;
+    }
 
-        return queryCouponList(isReceivedByMe, storeUserId, storeName, type, voucherOnly, couponType);
+    /**
+     * 仅优惠券模板(coupon_id):拼条件 → 查列表 → 含逻辑删模板补全 → 对方头像 → 按 ldcsf.created_time 倒序(新的在前)。
+     */
+    private List<LifeDiscountCouponFriendRuleVo> queryCouponList(boolean isReceivedByMe, String storeUserId, String storeName, Integer couponType) {
+        QueryWrapper<LifeDiscountCouponFriendRuleVo> couponQuery = buildBaseQueryWrapper(isReceivedByMe, storeUserId, storeName);
+        couponQuery.isNotNull("ldcsf.coupon_id");
+        // couponType:须保留「模板已逻辑删除导致 JOIN 不到 ldc」的记录,改为用含删除快照的批量查询再筛
+        if (couponType != null) {
+            couponQuery.and(w -> w.eq("ldc.coupon_type", couponType)
+                    .or(sub -> sub.isNull("ldc.id").isNotNull("ldcsf.coupon_id")));
+        }
+        couponQuery.orderByDesc("ldcsf.created_time");
+        List<LifeDiscountCouponFriendRuleVo> couponList = isReceivedByMe
+                ? lifeDiscountCouponStoreFriendMapper.getReceivedSendFriendCouponList(couponQuery)
+                : lifeDiscountCouponStoreFriendMapper.getReceivedSendFriendCouponListwzhy(couponQuery);
+
+        mergeCouponTemplatesIncludeDeleted(couponList, couponType);
+        fillStoreAvatar(couponList);
+        return couponList;
     }
 
     /**
-     * 查询赠券列表(抽取公共方法,减少代码重复)
-     *
-     * @param isReceivedByMe true=我收到的(所有好友赠送给我的),false=我送出的(我送给所有好友的)
-     * @param storeUserId     当前登录店铺用户ID
-     * @param storeName       店铺名称(模糊查询)
-     * @param type            类型参数(4=代金券,其他=优惠券,null=全部)
-     * @param voucherOnly     是否仅查询代金券
-     * @param couponType      优惠券类型(1=满减券,2=折扣券,null=全部优惠券)
-     * @return 赠券列表
+     * 使用单表直查(含 delete_flag=1 的已删模板),覆盖/补全列表中的券属性,避免商家逻辑删除模板后好友记录无法展示券信息。
+     * 当传入 couponTypeFilter 时,在补全后按模板真实 coupon_type 再过滤(兼容 JOIN 不到 ldc 的行)。
      */
-    private List<LifeDiscountCouponFriendRuleVo> queryCouponList(boolean isReceivedByMe, String storeUserId, String storeName, Integer type, boolean voucherOnly, Integer couponType) {
-        List<LifeDiscountCouponFriendRuleVo> result;
-        
-        // 仅查询代金券
-        if (voucherOnly) {
-            QueryWrapper<LifeDiscountCouponFriendRuleVo> queryWrapper = buildBaseQueryWrapper(isReceivedByMe, storeUserId, storeName);
-            queryWrapper.isNotNull("ldcsf.voucher_id");
-            result = isReceivedByMe
-                    ? lifeDiscountCouponStoreFriendMapper.getReceivedSendFriendCouponListVoucher(queryWrapper)
-                    : lifeDiscountCouponStoreFriendMapper.getReceivedSendFriendCouponListwzhyVoucher(queryWrapper);
-        } else {
-            // 查询优惠券
-            QueryWrapper<LifeDiscountCouponFriendRuleVo> couponQuery = buildBaseQueryWrapper(isReceivedByMe, storeUserId, storeName);
-            couponQuery.isNotNull("ldcsf.coupon_id");
-            // 如果指定了优惠券类型(满减券或折扣券),添加筛选条件
-            if (couponType != null) {
-                couponQuery.eq("ldc.coupon_type", couponType);
+    private void mergeCouponTemplatesIncludeDeleted(List<LifeDiscountCouponFriendRuleVo> list, Integer couponTypeFilter) {
+        if (CollectionUtils.isEmpty(list)) {
+            return;
+        }
+        List<Integer> couponIds = list.stream()
+                .map(LifeDiscountCouponFriendRuleVo::getCouponId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+        if (couponIds.isEmpty()) {
+            return;
+        }
+        List<LifeDiscountCoupon> templates = lifeDiscountCouponMapper.selectByIdsIncludeDeleted(couponIds);
+        Map<Integer, LifeDiscountCoupon> byId = templates.stream()
+                .collect(Collectors.toMap(LifeDiscountCoupon::getId, t -> t, (a, b) -> a));
+        for (LifeDiscountCouponFriendRuleVo vo : list) {
+            Integer cid = vo.getCouponId();
+            if (cid == null) {
+                continue;
             }
-            List<LifeDiscountCouponFriendRuleVo> couponList = isReceivedByMe
-                    ? lifeDiscountCouponStoreFriendMapper.getReceivedSendFriendCouponList(couponQuery)
-                    : lifeDiscountCouponStoreFriendMapper.getReceivedSendFriendCouponListwzhy(couponQuery);
-
-            // 如果 type 不为 null,说明只查询优惠券,直接返回
-            if (type != null) {
-                result = couponList;
-            } else {
-                // type 为 null,需要合并优惠券和代金券
-                QueryWrapper<LifeDiscountCouponFriendRuleVo> voucherQuery = buildBaseQueryWrapper(isReceivedByMe, storeUserId, storeName);
-                voucherQuery.isNotNull("ldcsf.voucher_id");
-                List<LifeDiscountCouponFriendRuleVo> voucherList = isReceivedByMe
-                        ? lifeDiscountCouponStoreFriendMapper.getReceivedSendFriendCouponListVoucher(voucherQuery)
-                        : lifeDiscountCouponStoreFriendMapper.getReceivedSendFriendCouponListwzhyVoucher(voucherQuery);
-
-                // 合并列表
-                result = new ArrayList<>(couponList);
-                result.addAll(voucherList);
+            LifeDiscountCoupon t = byId.get(cid);
+            if (t != null) {
+                applyDiscountCouponTemplateToFriendRuleVo(vo, t);
             }
         }
+        if (couponTypeFilter != null) {
+            list.removeIf(vo -> {
+                LifeDiscountCoupon t = byId.get(vo.getCouponId());
+                return t == null || !couponTypeFilter.equals(t.getCouponType());
+            });
+        }
+    }
 
-        // 批量查询商户头像并填充
-        fillStoreAvatar(result);
-        
-        // 排序
-        return sortByEndDate(result);
+    private void applyDiscountCouponTemplateToFriendRuleVo(LifeDiscountCouponFriendRuleVo vo, LifeDiscountCoupon t) {
+        vo.setCouponName(t.getName());
+        vo.setNominalValue(t.getNominalValue());
+        vo.setMinimumSpendingAmount(t.getMinimumSpendingAmount());
+        vo.setExpirationDate(t.getExpirationDate());
+        vo.setLongTermValid(t.getLongTermValid());
+        vo.setCouponType(t.getCouponType());
+        vo.setDiscountRate(t.getDiscountRate());
+        Date valid = localDateToDate(t.getValidDate());
+        vo.setEndDate(valid);
+        vo.setValidDate(valid);
+        vo.setBeginGetDate(localDateToDate(t.getBeginGetDate()));
+        vo.setEndGetDate(localDateToDate(t.getEndGetDate()));
+    }
+
+    private static Date localDateToDate(LocalDate d) {
+        if (d == null) {
+            return null;
+        }
+        return Date.from(d.atStartOfDay(ZoneId.systemDefault()).toInstant());
     }
 
     /**
-     * 批量查询商户头像并填充到结果中
-     * 只从 store_user.head_img 获取商户头像,不涉及店铺信息表(store_info)
-     *
-     * @param couponList 券列表
+     * 批量查询商户头像并填充到结果中(对方店铺展示对方商户头像,不展示当前登录人自己)
+     * 优先按「对方 store_user.id」取 head_img;缺失时再按对方店铺 id 取店内主号等头像。
      */
     private void fillStoreAvatar(List<LifeDiscountCouponFriendRuleVo> couponList) {
         if (CollectionUtils.isEmpty(couponList)) {
             return;
         }
 
-        // 收集所有的店铺ID(storeId是Integer类型)
-        List<Integer> storeIds = couponList.stream()
-                .map(LifeDiscountCouponFriendRuleVo::getStoreId)
-                .filter(storeId -> storeId != null)
+        List<Integer> counterpartyUserIds = couponList.stream()
+                .map(LifeDiscountCouponFriendRuleVo::getCounterpartyStoreUserId)
+                .filter(Objects::nonNull)
                 .distinct()
                 .collect(Collectors.toList());
 
-        if (storeIds.isEmpty()) {
-            log.warn("没有有效的店铺ID,无法查询商户头像");
-            return;
+        Map<Integer, String> userIdToHeadImg = new HashMap<>();
+        if (!counterpartyUserIds.isEmpty()) {
+            LambdaQueryWrapper<StoreUser> byId = new LambdaQueryWrapper<>();
+            byId.in(StoreUser::getId, counterpartyUserIds).eq(StoreUser::getDeleteFlag, 0);
+            for (StoreUser u : storeUserMapper.selectList(byId)) {
+                if (u.getHeadImg() != null && !u.getHeadImg().trim().isEmpty()) {
+                    userIdToHeadImg.put(u.getId(), u.getHeadImg());
+                }
+            }
         }
 
-        // 批量查询商户头像(只从 store_user.head_img 获取,优先取主账号头像)
-        LambdaQueryWrapper<StoreUser> userWrapper = new LambdaQueryWrapper<>();
-        userWrapper.in(StoreUser::getStoreId, storeIds)
-                .eq(StoreUser::getDeleteFlag, 0)
-                .isNotNull(StoreUser::getHeadImg)
-                .ne(StoreUser::getHeadImg, "")
-                .orderByAsc(StoreUser::getAccountType) // 主账号(1)排在前面,null值排在最后
-                .orderByAsc(StoreUser::getId);
-        List<StoreUser> storeUsers = storeUserMapper.selectList(userWrapper);
+        List<Integer> storeIdsForFallback = couponList.stream()
+                .filter(vo -> {
+                    Integer uid = vo.getCounterpartyStoreUserId();
+                    if (uid != null && userIdToHeadImg.containsKey(uid)) {
+                        return false;
+                    }
+                    return vo.getStoreId() != null;
+                })
+                .map(LifeDiscountCouponFriendRuleVo::getStoreId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
 
-        // 构建店铺ID -> 头像的映射(优先主账号)
         Map<Integer, String> storeAvatarMap = new HashMap<>();
-        for (StoreUser user : storeUsers) {
-            if (user.getStoreId() != null && user.getHeadImg() != null && !user.getHeadImg().trim().isEmpty()) {
+        if (!storeIdsForFallback.isEmpty()) {
+            LambdaQueryWrapper<StoreUser> userWrapper = new LambdaQueryWrapper<>();
+            userWrapper.in(StoreUser::getStoreId, storeIdsForFallback)
+                    .eq(StoreUser::getDeleteFlag, 0)
+                    .isNotNull(StoreUser::getHeadImg)
+                    .ne(StoreUser::getHeadImg, "")
+                    .orderByAsc(StoreUser::getAccountType)
+                    .orderByAsc(StoreUser::getId);
+            for (StoreUser user : storeUserMapper.selectList(userWrapper)) {
+                if (user.getStoreId() == null || user.getHeadImg() == null || user.getHeadImg().trim().isEmpty()) {
+                    continue;
+                }
                 Integer storeId = user.getStoreId();
-                // 如果该店铺还没有头像,或者当前用户是主账号(account_type = 1),则设置头像
-                // 主账号优先:如果已存在头像但当前是主账号,则覆盖
                 if (!storeAvatarMap.containsKey(storeId)) {
-                    // 该店铺还没有头像,直接设置
                     storeAvatarMap.put(storeId, user.getHeadImg());
                 } else if (user.getAccountType() != null && user.getAccountType() == 1) {
-                    // 该店铺已有头像,但当前是主账号,优先使用主账号头像
                     storeAvatarMap.put(storeId, user.getHeadImg());
                 }
             }
         }
 
-        // 填充头像到结果中
         for (LifeDiscountCouponFriendRuleVo vo : couponList) {
+            Integer uid = vo.getCounterpartyStoreUserId();
+            if (uid != null) {
+                String byUser = userIdToHeadImg.get(uid);
+                if (byUser != null) {
+                    vo.setImgUrl(byUser);
+                    continue;
+                }
+            }
             if (vo.getStoreId() != null) {
-                String avatar = storeAvatarMap.get(vo.getStoreId());
-                if (avatar != null) {
-                    vo.setImgUrl(avatar);
+                String byStore = storeAvatarMap.get(vo.getStoreId());
+                if (byStore != null) {
+                    vo.setImgUrl(byStore);
                 }
             }
         }
     }
 
+    private void appendFriendSenderFilter(QueryWrapper<LifeDiscountCouponFriendRuleDetailVo> queryWrapper, String friendStoreUserId) {
+        if (friendStoreUserId == null) {
+            queryWrapper.eq("1", "0");
+            return;
+        }
+        String t = friendStoreUserId.trim();
+        if (t.isEmpty()) {
+            queryWrapper.eq("1", "0");
+            return;
+        }
+        try {
+            applyLdcsfSenderMerchantFilter(queryWrapper, Integer.parseInt(t));
+        } catch (NumberFormatException e) {
+            queryWrapper.eq("1", "0");
+        }
+    }
+
     /**
-     * 构建基础查询条件
+     * 构建基础查询条件(与 {@code life_discount_coupon_store_friend} 四列语义对齐:
+     * 我收到的 → {@code receiver_store_id} 或旧列 {@code store_user_id};
+     * 我送出的 → {@code sender_user_id} 或旧列 {@code friend_store_user_id})。
      *
-     * @param isReceivedByMe true=我收到的(所有好友赠送给我的),false=我送出的(我送给所有好友的)
-     * @param storeUserId     当前登录店铺用户ID
-     * @param storeName       店铺名称(模糊查询)
+     * @param isReceivedByMe true=我收到的,false=我送出的
+     * @param storeUserId     当前登录账号 store_user.id
+     * @param storeName       对方店铺名称(模糊,对应联表 si.store_name
      * @return QueryWrapper
      */
     private QueryWrapper<LifeDiscountCouponFriendRuleVo> buildBaseQueryWrapper(boolean isReceivedByMe, String storeUserId, String storeName) {
         QueryWrapper<LifeDiscountCouponFriendRuleVo> queryWrapper = new QueryWrapper<>();
-        // 根据表结构(实体类注释):
-        // store_user_id: store_info的ID(店铺ID),收到券的店铺
-        // friend_store_user_id: store_user的ID(店铺用户ID),送券的用户
-        // 前端传入的 storeUserId 是当前登录账号的 store_user.id(店铺用户ID)
         if (isReceivedByMe) {
-            // 我收到的:查询所有好友赠送给我的券(我是收到券的店铺)
-            // store_user_id = 我的店铺ID(store_info.id),需要从店铺用户ID转换为店铺ID
             StoreUser storeUser = storeUserMapper.selectById(storeUserId);
             if (storeUser != null && storeUser.getStoreId() != null) {
-                queryWrapper.eq("ldcsf.store_user_id", storeUser.getStoreId());
+                applyLdcsfReceiverStoreFilter(queryWrapper, storeUser.getStoreId());
             } else {
-                // 如果查询不到店铺用户,返回空结果
-                queryWrapper.eq("1", "0"); // 永远不匹配的条件
-                log.warn("buildBaseQueryWrapper 查询不到店铺用户,storeUserId={}", storeUserId);
+                queryWrapper.eq("1", "0");
+                log.error("buildBaseQueryWrapper 查询不到店铺用户,storeUserId={}", storeUserId);
             }
         } else {
-            // 我送出的:查询我送给所有好友的券(我是送券的用户)
-            // friend_store_user_id = 我的店铺用户ID(store_user.id)
-            queryWrapper.eq("ldcsf.friend_store_user_id", storeUserId);
+            try {
+                applyLdcsfSenderMerchantFilter(queryWrapper, Integer.parseInt(storeUserId.trim()));
+            } catch (NumberFormatException e) {
+                queryWrapper.eq("1", "0");
+                log.error("buildBaseQueryWrapper 非法 storeUserId={}", storeUserId);
+            }
         }
         queryWrapper.eq("ldcsf.delete_flag", 0);
         // 店铺名称模糊查询
@@ -1002,55 +1109,11 @@ public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDi
     }
 
     /**
-     * 按有效期降序排序(null 值排在最后),如果有效期为 null 则按创建时间降序排序
-     *
-     * @param list 待排序列表
-     * @return 排序后的列表
-     */
-    private List<LifeDiscountCouponFriendRuleVo> sortByEndDate(List<LifeDiscountCouponFriendRuleVo> list) {
-        if (CollectionUtils.isEmpty(list)) {
-            return list;
-        }
-        list.sort((a, b) -> {
-            // 优先按有效期(validDate)排序,如果为 null 则按创建时间(endDate)排序
-            Date validDateA = a.getValidDate();
-            Date validDateB = b.getValidDate();
-            Date endDateA = a.getEndDate();
-            Date endDateB = b.getEndDate();
-            
-            // 如果两个都有有效期,按有效期排序
-            if (validDateA != null && validDateB != null) {
-                return validDateB.compareTo(validDateA); // 降序:有效期晚的在前
-            }
-            // 如果只有一个有有效期,有有效期的排在前面
-            if (validDateA != null) {
-                return -1;
-            }
-            if (validDateB != null) {
-                return 1;
-            }
-            // 如果都没有有效期,按创建时间排序
-            if (endDateA == null && endDateB == null) {
-                return 0;
-            }
-            if (endDateA == null) {
-                return 1;
-            }
-            if (endDateB == null) {
-                return -1;
-            }
-            // 降序排序(最新的在前)
-            return endDateB.compareTo(endDateA);
-        });
-        return list;
-    }
-
-    /**
-     * 好评送券:用户好评且AI审核通过后,按运营活动配置的优惠券/代金券ID发放到用户券包,发放成功后扣减库存;按券类型发送对应通知。
+     * 好评送券:用户好评且AI审核通过后,按运营活动配置的优惠券发放到用户券包。
      *
      * @param userId  评价用户ID(life用户)
      * @param storeId 店铺ID(businessId)
-     * @return 发放数量(优惠券+代金券),0表示未发放
+     * @return 发放张数,0 表示未发放
      */
     @Override
     public int issueCouponForGoodRating(Integer userId, Integer storeId) {
@@ -1068,7 +1131,7 @@ public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDi
                 .orderByDesc(StoreOperationalActivity::getId)
                 .last("LIMIT 1");
         StoreOperationalActivity activity = storeOperationalActivityMapper.selectOne(activityWrapper);
-        if (activity == null || (activity.getCouponId() == null && activity.getVoucherId() == null)) {
+        if (activity == null || activity.getCouponId() == null || activity.getCouponId() <= 0) {
             return 0;
         }
         
@@ -1078,222 +1141,193 @@ public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDi
             // 只统计活动开始时间之后的好评,活动开始前的评论不计入参与次数
             int passedCount = commonRatingMapper.countPassedGoodRatingsByUserAndStore(
                     Long.valueOf(userId), storeId, activity.getStartTime());
-            if (passedCount >= limit) {
+            if (passedCount > limit) {
                 log.info("issueCouponForGoodRating 好评送券跳过:超过运营活动参与次数 participation_limit={}, count={}, userId={}, storeId={}, activityStartTime={}",
                         limit, passedCount, userId, storeId, activity.getStartTime());
                 return 0;
             }
         }
-        
-        // 调用4参数方法进行发放
-        return issueCouponForGoodRating(userId, storeId, activity.getCouponId(), activity.getVoucherId());
+
+        return issueCouponForGoodRating(userId, storeId, activity.getCouponId(), activity.getCouponQuantity());
+    }
+
+    private static int resolveGoodRatingCouponCopies(Integer couponQuantity) {
+        if (couponQuantity == null) {
+            return 1;
+        }
+        if (couponQuantity < 1) {
+            return 0;
+        }
+        return Math.min(couponQuantity, GOOD_RATING_ISSUE_COPIES_HARD_CAP);
     }
 
     /**
-     * 好评送券:按优惠券/代金券ID发放到用户券包,发放成功后扣减库存;根据是优惠券还是代金券发送对应通知。
+     * 好评送券:按优惠券模板 ID 发放到用户券包,扣减库存并通知。
      */
-    public int issueCouponForGoodRating(Integer userId, Integer storeId, Integer couponId, Integer voucherId) {
+    @Override
+    public int issueCouponForGoodRating(Integer userId, Integer storeId, Integer couponId) {
+        return issueCouponForGoodRating(userId, storeId, couponId, null);
+    }
+
+    @Override
+    public int issueCouponForGoodRating(Integer userId, Integer storeId, Integer couponId, Integer couponQuantity) {
         if (userId == null || storeId == null) {
             return 0;
         }
-        if ((couponId == null || couponId <= 0) && (voucherId == null || voucherId <= 0)) {
+        if (couponId == null || couponId <= 0) {
             return 0;
         }
         int commenterUserId = userId.intValue();
+        int copiesWanted = resolveGoodRatingCouponCopies(couponQuantity);
+
         StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
         LifeUser lifeUser = lifeUserMapper.selectById(commenterUserId);
-        int grantedCoupon = 0;
-        int grantedVoucher = 0;
-        String couponName = null;  // 优惠券名称
-        String voucherName = null;  // 代金券名称
-        LifeDiscountCoupon lifeDiscountCoupon = null;  // 保存优惠券对象,用于通知消息
-
-        // 按优惠券ID发放:发放1张,扣减库存,发优惠券通知
-        if (couponId != null && couponId > 0) {
-            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(couponId);
-            if (coupon != null && String.valueOf(storeId).equals(coupon.getStoreId())
-                    && coupon.getSingleQty() != null && coupon.getSingleQty() > 0) {
-                try {
-                    LifeDiscountCouponUser lifeDiscountCouponUser = new LifeDiscountCouponUser();
-                    lifeDiscountCouponUser.setCouponId(coupon.getId());
-                    lifeDiscountCouponUser.setUserId(commenterUserId);
-                    lifeDiscountCouponUser.setReceiveTime(new Date());
-                    lifeDiscountCouponUser.setExpirationTime(DiscountCouponExpirationUtil.resolveLifeDiscountCouponExpiration(
-                            lifeDiscountCouponUser.getReceiveTime(),
-                            coupon.getSpecifiedDay(),
-                            coupon.getExpirationDate(),
-                            coupon.getValidDate(),
-                            coupon.getEndDate(),
-                            ZoneId.systemDefault()));
-                    lifeDiscountCouponUser.setStatus(Integer.parseInt(DiscountCouponEnum.WAITING_USED.getValue()));
-                    lifeDiscountCouponUser.setDeleteFlag(0);
-                    lifeDiscountCouponUser.setIssueSource(3); // 3-好评送券
-                    lifeDiscountCouponUserMapper.insert(lifeDiscountCouponUser);
-                    coupon.setSingleQty(coupon.getSingleQty() - 1);
-// 1. 获取开始日期
-                    LocalDate beginDate = coupon.getBeginGetDate();
-// 2. 获取字符串类型的天数
-                    String specifiedDayStr = coupon.getSpecifiedDay();
-
-// 3. 基础校验
-                    if (beginDate == null) {
-                        throw new IllegalArgumentException("优惠券开始日期不能为空");
-                    }
-                    if (specifiedDayStr == null || specifiedDayStr.isEmpty()) {
-                        throw new IllegalArgumentException("有效天数不能为空");
-                    }
 
-// 4. 字符串转整数 + 合法性校验
-                    int days;
-                    try {
-                        days = Integer.parseInt(specifiedDayStr);
-                    } catch (NumberFormatException e) {
-                        throw new IllegalArgumentException("有效天数必须是纯数字:" + specifiedDayStr);
-                    }
-                    if (days <= 0) {
-                        throw new IllegalArgumentException("有效天数必须大于0");
-                    }
+        LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(couponId);
+        if (coupon == null || !String.valueOf(storeId).equals(coupon.getStoreId())) {
+            return 0;
+        }
+        if (!LifeDiscountCouponStock.hasTemplateStockRemaining(coupon.getUnlimitedQty(), coupon.getSingleQty())) {
+            return 0;
+        }
 
-// 5. 计算结束日期并赋值
-                    LocalDate endDate = beginDate.plusDays(days);
-                    coupon.setEndGetDate(endDate);
-                    lifeDiscountCouponMapper.updateById(coupon);
-                    grantedCoupon = 1;
-                    couponName = coupon.getName();  // 保存优惠券名称
-                    lifeDiscountCoupon = coupon;  // 保存优惠券对象,用于通知消息
-                } catch (Exception e) {
-                    log.error("发放优惠券失败,userId={}, storeId={}, couponId={}, error={}", userId, storeId, couponId, e.getMessage(), e);
-                }
+        ZoneId zone = ZoneId.systemDefault();
+        if (!Integer.valueOf(1).equals(coupon.getLongTermValid())) {
+            LocalDate beginDate = coupon.getBeginGetDate();
+            if (beginDate == null) {
+                beginDate = LocalDate.now(zone);
+                coupon.setBeginGetDate(beginDate);
+            }
+            String specifiedDayStr = coupon.getSpecifiedDay();
+            if (specifiedDayStr == null || specifiedDayStr.isEmpty()) {
+                log.error("发放优惠券失败(非长期券缺少有效天数),userId={}, storeId={}, couponId={}", userId, storeId, couponId);
+                return 0;
+            }
+            int days;
+            try {
+                days = Integer.parseInt(specifiedDayStr.trim());
+            } catch (NumberFormatException e) {
+                log.error("发放优惠券失败(有效天数非法),userId={}, storeId={}, couponId={}, specifiedDay={}",
+                        userId, storeId, couponId, specifiedDayStr, e);
+                return 0;
             }
+            if (days <= 0) {
+                log.error("发放优惠券失败(有效天数须>0),userId={}, storeId={}, couponId={}", userId, storeId, couponId);
+                return 0;
+            }
+            coupon.setEndGetDate(beginDate.plusDays(days));
         }
 
-        // 按代金券ID发放:发放1张,扣减库存,发代金券通知
-        if (voucherId != null && voucherId > 0) {
-            LifeCoupon lifeCoupon = lifeCouponMapper.selectById(voucherId);
-            if (lifeCoupon != null && String.valueOf(storeId).equals(lifeCoupon.getStoreId())
-                    && lifeCoupon.getSingleQty() != null && lifeCoupon.getSingleQty() > 0) {
-                try {
-                    LifeDiscountCouponUser lifeDiscountCouponUser = new LifeDiscountCouponUser();
-                    lifeDiscountCouponUser.setVoucherId(lifeCoupon.getId());
-                    lifeDiscountCouponUser.setUserId(commenterUserId);
-                    lifeDiscountCouponUser.setReceiveTime(new Date());
-                    lifeDiscountCouponUser.setExpirationTime(DiscountCouponExpirationUtil.resolveLifeCouponVoucherExpiration(
-                            lifeDiscountCouponUser.getReceiveTime(),
-                            lifeCoupon.getExpirationDate(),
-                            lifeCoupon.getEndDate(),
-                            ZoneId.systemDefault()));
-                    lifeDiscountCouponUser.setStatus(Integer.parseInt(DiscountCouponEnum.WAITING_USED.getValue()));
-                    lifeDiscountCouponUser.setDeleteFlag(0);
-                    lifeDiscountCouponUser.setIssueSource(3); // 3-好评送券(代金券)
-                    lifeDiscountCouponUserMapper.insert(lifeDiscountCouponUser);
-                    lifeCoupon.setSingleQty(lifeCoupon.getSingleQty() - 1);
-                    lifeCouponMapper.updateById(lifeCoupon);
-                    grantedVoucher = 1;
-                    voucherName = lifeCoupon.getName();  // 保存代金券名称
-                } catch (Exception e) {
-                    log.error("发放代金券失败,userId={}, storeId={}, voucherId={}, error={}", userId, storeId, voucherId, e.getMessage(), e);
+        int grantedCoupon = 0;
+        for (int i = 0; i < copiesWanted; i++) {
+            if (!LifeDiscountCouponStock.hasTemplateStockRemaining(coupon.getUnlimitedQty(), coupon.getSingleQty())) {
+                break;
+            }
+            try {
+                Date receiveTime = new Date();
+                LifeDiscountCouponUser lifeDiscountCouponUser = new LifeDiscountCouponUser();
+                lifeDiscountCouponUser.setCouponId(coupon.getId());
+                lifeDiscountCouponUser.setUserId(commenterUserId);
+                lifeDiscountCouponUser.setReceiveTime(receiveTime);
+                lifeDiscountCouponUser.setExpirationTime(DiscountCouponExpirationUtil.resolveLifeDiscountCouponExpiration(
+                        receiveTime,
+                        coupon.getLongTermValid(),
+                        coupon.getSpecifiedDay(),
+                        coupon.getExpirationDate(),
+                        coupon.getValidDate(),
+                        coupon.getEndDate(),
+                        zone));
+                lifeDiscountCouponUser.setStatus(Integer.parseInt(DiscountCouponEnum.WAITING_USED.getValue()));
+                lifeDiscountCouponUser.setDeleteFlag(0);
+                lifeDiscountCouponUser.setIssueSource(3); // 3-好评送券
+                lifeDiscountCouponUserMapper.insert(lifeDiscountCouponUser);
+                if (!LifeDiscountCouponStock.isUnlimitedQty(coupon.getUnlimitedQty())
+                        && coupon.getSingleQty() != null) {
+                    coupon.setSingleQty(coupon.getSingleQty() - 1);
                 }
+                grantedCoupon++;
+            } catch (Exception e) {
+                log.error("发放优惠券失败,userId={}, storeId={}, couponId={}, index={}, error={}",
+                        userId, storeId, couponId, i, e.getMessage(), e);
+                break;
+            }
+        }
+
+        if (grantedCoupon > 0) {
+            try {
+                lifeDiscountCouponMapper.updateById(coupon);
+            } catch (Exception e) {
+                log.error("更新优惠券模板失败 userId={}, storeId={}, couponId={}, error={}", userId, storeId, couponId, e.getMessage(), e);
             }
         }
 
-        // 根据发放的是优惠券还是代金券发送对应通知(明确区分券类型和名称)
-        if (lifeUser != null && storeInfo != null) {
-            if (grantedCoupon > 0 && couponName != null) {
-                LifeNotice couponNotice = new LifeNotice();
-                couponNotice.setSenderId("system");
-                couponNotice.setReceiverId("user_" + lifeUser.getUserPhone());
-                
-                // 构建优惠券详情信息
-                String couponDetailText = "";
-                if (lifeDiscountCoupon != null) {
-                    Integer couponType = lifeDiscountCoupon.getCouponType();
-                    if (couponType != null && couponType == 2) {
-                        // 折扣券:discountRate 为实付比例 0-100,与券模板一致(10=付10%=1折,80=8折)
-                        BigDecimal discountRate = lifeDiscountCoupon.getDiscountRate();
-                        if (discountRate != null) {
-                            BigDecimal zheShu = discountRate.divide(BigDecimal.TEN, 2, RoundingMode.HALF_UP);
-                            String zheShuText = zheShu.stripTrailingZeros().toPlainString();
-                            couponDetailText = "(" + zheShuText + "折";
-                            if (lifeDiscountCoupon.getMinimumSpendingAmount() != null) {
-                                couponDetailText += ",满" + lifeDiscountCoupon.getMinimumSpendingAmount() + "可用";
-                            }
-                            couponDetailText += ")";
+        LifeDiscountCoupon lifeDiscountCoupon = grantedCoupon > 0 ? coupon : null;
+        String couponName = grantedCoupon > 0 ? coupon.getName() : null;
+
+        if (lifeUser != null && storeInfo != null && grantedCoupon > 0 && couponName != null) {
+            LifeNotice couponNotice = new LifeNotice();
+            couponNotice.setSenderId("system");
+            couponNotice.setReceiverId("user_" + lifeUser.getUserPhone());
+
+            String couponDetailText = "";
+            if (lifeDiscountCoupon != null) {
+                Integer cType = lifeDiscountCoupon.getCouponType();
+                if (cType != null && cType == 2) {
+                    BigDecimal discountRate = lifeDiscountCoupon.getDiscountRate();
+                    if (discountRate != null) {
+                        BigDecimal zheShu = discountRate.divide(BigDecimal.TEN, 2, RoundingMode.HALF_UP);
+                        String zheShuText = zheShu.stripTrailingZeros().toPlainString();
+                        couponDetailText = "(" + zheShuText + "折";
+                        if (lifeDiscountCoupon.getMinimumSpendingAmount() != null) {
+                            couponDetailText += ",满" + lifeDiscountCoupon.getMinimumSpendingAmount() + "可用";
                         }
-                    } else {
-                        // 满减券(默认)
-                        if (lifeDiscountCoupon.getNominalValue() != null) {
-                            couponDetailText = "(满减" + lifeDiscountCoupon.getNominalValue() + "元";
-                            if (lifeDiscountCoupon.getMinimumSpendingAmount() != null) {
-                                couponDetailText += ",满" + lifeDiscountCoupon.getMinimumSpendingAmount() + "可用";
-                            }
-                            couponDetailText += ")";
+                        couponDetailText += ")";
+                    }
+                } else {
+                    if (lifeDiscountCoupon.getNominalValue() != null) {
+                        couponDetailText = "(满减" + lifeDiscountCoupon.getNominalValue() + "元";
+                        if (lifeDiscountCoupon.getMinimumSpendingAmount() != null) {
+                            couponDetailText += ",满" + lifeDiscountCoupon.getMinimumSpendingAmount() + "可用";
                         }
+                        couponDetailText += ")";
                     }
                 }
-                
-                // 明确标注是优惠券,并包含券的具体名称和详情
-                String couponText = "您对店铺「" + storeInfo.getStoreName() + "」的好评已通过审核,已为您发放优惠券「" + couponName + "」" + couponDetailText + ",快去我的券包查看吧~";
-                JSONObject couponJson = new JSONObject();
-                couponJson.put("message", couponText);
-                couponJson.put("couponType", "优惠券");  // 明确标识券类型
-                couponJson.put("couponName", couponName);  // 券名称
-                if (lifeDiscountCoupon != null) {
-                    couponJson.put("couponTypeValue", lifeDiscountCoupon.getCouponType());  // 优惠券类型值(1-满减券,2-折扣券)
-                }
-                couponNotice.setContext(couponJson.toJSONString());
-                couponNotice.setNoticeType(1);
-                couponNotice.setTitle("好评送优惠券");  // 标题明确标注是优惠券
-                couponNotice.setIsRead(0);
-                couponNotice.setDeleteFlag(0);
-                lifeNoticeMapper.insert(couponNotice);
-                
-                // WebSocket 实时推送通知
-                try {
-                    WebSocketVo websocketVo = new WebSocketVo();
-                    websocketVo.setSenderId("system");
-                    websocketVo.setReceiverId("user_" + lifeUser.getUserPhone());
-                    websocketVo.setCategory("notice");
-                    websocketVo.setNoticeType("1");
-                    websocketVo.setIsRead(0);
-                    websocketVo.setText(JSONObject.from(couponNotice).toJSONString());
-                    webSocketProcess.sendMessage("user_" + lifeUser.getUserPhone(), JSONObject.from(websocketVo).toJSONString());
-                } catch (Exception e) {
-                    log.error("好评送优惠券 WebSocket 推送失败,userId={}, storeId={}, couponId={}, error={}", userId, storeId, couponId, e.getMessage(), e);
-                }
             }
-            if (grantedVoucher > 0 && voucherName != null) {
-                LifeNotice voucherNotice = new LifeNotice();
-                voucherNotice.setSenderId("system");
-                voucherNotice.setReceiverId("user_" + lifeUser.getUserPhone());
-                // 明确标注是代金券,并包含券的具体名称
-                String voucherText = "您对店铺「" + storeInfo.getStoreName() + "」的好评已通过审核,已为您发放代金券「" + voucherName + "」,快去我的券包查看吧~";
-                JSONObject voucherJson = new JSONObject();
-                voucherJson.put("message", voucherText);
-                voucherJson.put("couponType", "代金券");  // 明确标识券类型
-                voucherJson.put("couponName", voucherName);  // 券名称
-                voucherNotice.setContext(voucherJson.toJSONString());
-                voucherNotice.setNoticeType(1);
-                voucherNotice.setTitle("好评送代金券");  // 标题明确标注是代金券
-                voucherNotice.setIsRead(0);
-                voucherNotice.setDeleteFlag(0);
-                lifeNoticeMapper.insert(voucherNotice);
-                
-                // WebSocket 实时推送通知
-                try {
-                    WebSocketVo websocketVo = new WebSocketVo();
-                    websocketVo.setSenderId("system");
-                    websocketVo.setReceiverId("user_" + lifeUser.getUserPhone());
-                    websocketVo.setCategory("notice");
-                    websocketVo.setNoticeType("1");
-                    websocketVo.setIsRead(0);
-                    websocketVo.setText(JSONObject.from(voucherNotice).toJSONString());
-                    webSocketProcess.sendMessage("user_" + lifeUser.getUserPhone(), JSONObject.from(websocketVo).toJSONString());
-                } catch (Exception e) {
-                    log.error("好评送代金券 WebSocket 推送失败,userId={}, storeId={}, voucherId={}, error={}", userId, storeId, voucherId, e.getMessage(), e);
-                }
+
+            String couponText = grantedCoupon > 1
+                    ? "您对店铺「" + storeInfo.getStoreName() + "」的好评已通过审核,已为您发放优惠券「" + couponName + "」"
+                    + couponDetailText + "共" + grantedCoupon + "张,快去我的券包查看吧~"
+                    : "您对店铺「" + storeInfo.getStoreName() + "」的好评已通过审核,已为您发放优惠券「" + couponName + "」"
+                    + couponDetailText + ",快去我的券包查看吧~";
+            JSONObject couponJson = new JSONObject();
+            couponJson.put("message", couponText);
+            couponJson.put("couponType", "优惠券");
+            couponJson.put("couponName", couponName);
+            couponJson.put("grantCount", grantedCoupon);
+            if (lifeDiscountCoupon != null) {
+                couponJson.put("couponTypeValue", lifeDiscountCoupon.getCouponType());
+            }
+            couponNotice.setContext(couponJson.toJSONString());
+            couponNotice.setNoticeType(1);
+            couponNotice.setTitle("好评送优惠券");
+            couponNotice.setIsRead(0);
+            couponNotice.setDeleteFlag(0);
+            lifeNoticeMapper.insert(couponNotice);
+
+            try {
+                WebSocketVo websocketVo = new WebSocketVo();
+                websocketVo.setSenderId("system");
+                websocketVo.setReceiverId("user_" + lifeUser.getUserPhone());
+                websocketVo.setCategory("notice");
+                websocketVo.setNoticeType("1");
+                websocketVo.setIsRead(0);
+                websocketVo.setText(JSONObject.from(couponNotice).toJSONString());
+                webSocketProcess.sendMessage("user_" + lifeUser.getUserPhone(), JSONObject.from(websocketVo).toJSONString());
+            } catch (Exception e) {
+                log.error("好评送优惠券 WebSocket 推送失败,userId={}, storeId={}, couponId={}, error={}", userId, storeId, couponId, e.getMessage(), e);
             }
         }
 
-        return grantedCoupon + grantedVoucher;
+        return grantedCoupon;
     }
 }

+ 27 - 10
alien-store/src/main/java/shop/alien/store/service/impl/LifeDiscountCouponUserServiceImpl.java

@@ -22,6 +22,7 @@ import shop.alien.store.config.BaseRedisService;
 import shop.alien.store.service.LifeDiscountCouponUserService;
 import shop.alien.util.common.constant.DiscountCouponEnum;
 import shop.alien.util.coupon.DiscountCouponExpirationUtil;
+import shop.alien.util.coupon.LifeDiscountCouponStock;
 
 import java.time.LocalDate;
 import java.time.ZoneId;
@@ -62,35 +63,48 @@ public class LifeDiscountCouponUserServiceImpl extends ServiceImpl<LifeDiscountC
             }
 
             Integer receiveQuantity = lifeDiscountCouponUserDto.getReceiveQuantity();
+            int receiveQty = (receiveQuantity == null || receiveQuantity < 1) ? 1 : receiveQuantity;
+
             LifeDiscountCoupon lifeDiscountCoupon = lifeDiscountCouponMapper.selectById(lifeDiscountCouponUserDto.getCouponId());
+            if (lifeDiscountCoupon == null) {
+                return R.fail("优惠券不存在");
+            }
             // 判断是否在领取时间内
             Date now = new Date();
             ZoneId zoneId = ZoneId.systemDefault();
             LocalDate localNow = now.toInstant().atZone(zoneId).toLocalDate();
-            int startResult = localNow.compareTo(lifeDiscountCoupon.getBeginGetDate());
-            if (startResult < 0) {
+            LocalDate beginGet = lifeDiscountCoupon.getBeginGetDate();
+            if (beginGet != null && localNow.isBefore(beginGet)) {
                 return R.fail("该优惠劵活动时间未开始");
             }
-            int getStatus = lifeDiscountCoupon.getGetStatus();
-            if (getStatus == 0) {
+            LocalDate endGet = lifeDiscountCoupon.getEndGetDate();
+            if (endGet != null && localNow.isAfter(endGet)) {
+                return R.fail("该优惠劵领取时间已结束");
+            }
+            Integer getStatusFlag = lifeDiscountCoupon.getGetStatus();
+            if (getStatusFlag == null || Integer.parseInt(DiscountCouponEnum.NO_GET.getValue()) == getStatusFlag) {
                 return R.fail("该优惠劵已经关闭领取");
             }
 
-            if(lifeDiscountCoupon.getSingleQty() != null && lifeDiscountCoupon.getSingleQty() <= 0){
+            if (!LifeDiscountCouponStock.hasTemplateStockRemaining(
+                    lifeDiscountCoupon.getUnlimitedQty(), lifeDiscountCoupon.getSingleQty())) {
                 return R.fail("已发放完毕");
             }
 
-            if(lifeDiscountCoupon.getSingleQty() != null && lifeDiscountCoupon.getSingleQty() < receiveQuantity){
+            if (!LifeDiscountCouponStock.isUnlimitedQty(lifeDiscountCoupon.getUnlimitedQty())
+                    && lifeDiscountCoupon.getSingleQty() != null
+                    && lifeDiscountCoupon.getSingleQty() < receiveQty) {
                 return R.fail("已发放完毕");
             }
 
             //判断领取数量
-            for (int i = 0; i < receiveQuantity; i++) {
+            for (int i = 0; i < receiveQty; i++) {
                 LifeDiscountCouponUser lifeDiscountCouponUser = new LifeDiscountCouponUser();
                 BeanUtils.copyProperties(lifeDiscountCouponUserDto, lifeDiscountCouponUser);
                 lifeDiscountCouponUser.setReceiveTime(new Date());
                 lifeDiscountCouponUser.setExpirationTime(DiscountCouponExpirationUtil.resolveLifeDiscountCouponExpiration(
                         lifeDiscountCouponUser.getReceiveTime(),
+                        lifeDiscountCoupon.getLongTermValid(),
                         lifeDiscountCoupon.getSpecifiedDay(),
                         lifeDiscountCoupon.getExpirationDate(),
                         lifeDiscountCoupon.getValidDate(),
@@ -102,9 +116,12 @@ public class LifeDiscountCouponUserServiceImpl extends ServiceImpl<LifeDiscountC
                 lifeDiscountCouponUser.setIssueSource(1); // 1-手动领取
                 lifeDiscountCouponUserMapper.insert(lifeDiscountCouponUser);
             }
-            //削减该优惠券库存
-            lifeDiscountCoupon.setSingleQty(lifeDiscountCoupon.getSingleQty() - receiveQuantity);
-            lifeDiscountCouponMapper.updateById(lifeDiscountCoupon);
+            //削减该优惠券库存(发行量不限则不扣减)
+            if (!LifeDiscountCouponStock.isUnlimitedQty(lifeDiscountCoupon.getUnlimitedQty())
+                    && lifeDiscountCoupon.getSingleQty() != null) {
+                lifeDiscountCoupon.setSingleQty(lifeDiscountCoupon.getSingleQty() - receiveQty);
+                lifeDiscountCouponMapper.updateById(lifeDiscountCoupon);
+            }
 
             //是否领取优惠券收藏店铺
             Integer attentionCanReceived = lifeDiscountCoupon.getAttentionCanReceived();

+ 2 - 0
alien-store/src/main/java/shop/alien/store/service/impl/LifeGroupPackageServiceImpl.java

@@ -152,6 +152,7 @@ public class LifeGroupPackageServiceImpl extends ServiceImpl<LifeGroupPackageMap
                                     lifeDiscountCouponUser.setReceiveTime(new Date());
                                     lifeDiscountCouponUser.setExpirationTime(DiscountCouponExpirationUtil.resolveLifeDiscountCouponExpiration(
                                             lifeDiscountCouponUser.getReceiveTime(),
+                                            insertedCoupon.getLongTermValid(),
                                             insertedCoupon.getSpecifiedDay(),
                                             insertedCoupon.getExpirationDate(),
                                             insertedCoupon.getValidDate(),
@@ -288,6 +289,7 @@ public class LifeGroupPackageServiceImpl extends ServiceImpl<LifeGroupPackageMap
                                     lifeDiscountCouponUser.setReceiveTime(new Date());
                                     lifeDiscountCouponUser.setExpirationTime(DiscountCouponExpirationUtil.resolveLifeDiscountCouponExpiration(
                                             lifeDiscountCouponUser.getReceiveTime(),
+                                            insertedCoupon.getLongTermValid(),
                                             insertedCoupon.getSpecifiedDay(),
                                             insertedCoupon.getExpirationDate(),
                                             insertedCoupon.getValidDate(),

+ 2 - 2
alien-store/src/main/java/shop/alien/store/service/impl/OperationalActivityServiceImpl.java

@@ -180,7 +180,7 @@ public class OperationalActivityServiceImpl implements OperationalActivityServic
 
             // 设置优惠券名称(判空处理)
             if (activity.getCouponId() != null) {
-                LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(activity.getCouponId());
+                LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(activity.getCouponId());
                 if (coupon != null) {
                     vo.setCouponName(coupon.getName());
                     vo.setCouponType(coupon.getType());
@@ -225,7 +225,7 @@ public class OperationalActivityServiceImpl implements OperationalActivityServic
 
         // 设置优惠券名称(判空处理)
         if (activity.getCouponId() != null) {
-            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(activity.getCouponId());
+            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(activity.getCouponId());
             if (coupon != null) {
                 vo.setCouponName(coupon.getName());
                 vo.setCouponType(coupon.getType());

+ 2 - 2
alien-store/src/main/java/shop/alien/store/service/impl/StoreOperationalActivityServiceImpl.java

@@ -93,7 +93,7 @@ public class StoreOperationalActivityServiceImpl implements StoreOperationalActi
 
         // 设置优惠券名称(判空处理)
         if (activity.getCouponId() != null) {
-            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(activity.getCouponId());
+            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(activity.getCouponId());
             if (coupon != null) {
                 vo.setCouponName(coupon.getName());
             }
@@ -143,7 +143,7 @@ public class StoreOperationalActivityServiceImpl implements StoreOperationalActi
 
         // 设置优惠券名称(判空处理)
         if (activity.getCouponId() != null) {
-            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(activity.getCouponId());
+            LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(activity.getCouponId());
             if (coupon != null) {
                 vo.setCouponName(coupon.getName());
             }

+ 9 - 2
alien-store/src/main/java/shop/alien/store/service/impl/StorePlatformBenefitsServiceImpl.java

@@ -19,6 +19,7 @@ import shop.alien.store.service.StorePlatformBenefitsService;
 import shop.alien.util.common.JwtUtil;
 import shop.alien.util.common.constant.DiscountCouponEnum;
 import shop.alien.util.coupon.DiscountCouponExpirationUtil;
+import shop.alien.util.coupon.LifeDiscountCouponStock;
 
 import java.time.ZoneId;
 import java.util.ArrayList;
@@ -248,8 +249,13 @@ public class StorePlatformBenefitsServiceImpl extends ServiceImpl<StorePlatformB
 //            throw new RuntimeException("优惠券库存不足");
 //        }
         
-        // 更新库存:减1
-        lifeDiscountCoupon.setSingleQty(lifeDiscountCoupon.getSingleQty() - 1);
+        if (!LifeDiscountCouponStock.isUnlimitedQty(lifeDiscountCoupon.getUnlimitedQty())) {
+            Integer q = lifeDiscountCoupon.getSingleQty();
+            if (q == null || q <= 0) {
+                throw new RuntimeException("优惠券库存不足");
+            }
+            lifeDiscountCoupon.setSingleQty(q - 1);
+        }
         lifeDiscountCoupon.setUpdatedTime(new Date());
         int updateResult = lifeDiscountCouponMapper.updateById(lifeDiscountCoupon);
 
@@ -263,6 +269,7 @@ public class StorePlatformBenefitsServiceImpl extends ServiceImpl<StorePlatformB
         lifeDiscountCouponUser.setReceiveTime(new Date());
         lifeDiscountCouponUser.setExpirationTime(DiscountCouponExpirationUtil.resolveLifeDiscountCouponExpiration(
                 lifeDiscountCouponUser.getReceiveTime(),
+                lifeDiscountCoupon.getLongTermValid(),
                 lifeDiscountCoupon.getSpecifiedDay(),
                 lifeDiscountCoupon.getExpirationDate(),
                 lifeDiscountCoupon.getValidDate(),

+ 9 - 9
alien-store/src/main/java/shop/alien/store/service/impl/StoreReservationServiceImpl.java

@@ -762,12 +762,12 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
             throw new RuntimeException("预约已过期,无法核销");
         }
 
-        if(order.getOrderCostType() == 1){
-            //调用退款
-            MerchantPaymentStrategy strategy = merchantPaymentStrategyFactory.getStrategy(order.getPaymentMethod());
-            strategy.refund(order.getStoreId(), order.getOutTradeNo(), order.getDepositAmount().toString(), "已扫码到店商家退款", 3);
-
-        }
+//        if(order.getOrderCostType() == 1){
+//            //调用退款
+//            MerchantPaymentStrategy strategy = merchantPaymentStrategyFactory.getStrategy(order.getPaymentMethod());
+//            strategy.refund(order.getStoreId(), order.getOutTradeNo(), order.getDepositAmount().toString(), "已扫码到店商家退款", 3);
+//
+//        }
 
         // 更新预约状态为已到店(status = 2)
         reservation.setStatus(2); // 已到店
@@ -785,9 +785,9 @@ public class StoreReservationServiceImpl extends ServiceImpl<StoreReservationMap
         }
 
         //发放好友优惠券
-        LifeDiscountCouponStoreFriendDto lifeDiscountCouponStoreFriendDto = new LifeDiscountCouponStoreFriendDto();
-        lifeDiscountCouponStoreFriendDto.setOrderId(Integer.parseInt(order.getId().toString()));
-        lifeDiscountCouponStoreFriendService.newIssueFriendCoupon(lifeDiscountCouponStoreFriendDto);
+//        LifeDiscountCouponStoreFriendDto lifeDiscountCouponStoreFriendDto = new LifeDiscountCouponStoreFriendDto();
+//        lifeDiscountCouponStoreFriendDto.setOrderId(Integer.parseInt(order.getId().toString()));
+//        lifeDiscountCouponStoreFriendService.newIssueFriendCoupon(lifeDiscountCouponStoreFriendDto);
 
         // 核销成功后,发送订金退款短信和通知
         try {

+ 9 - 7
alien-store/src/main/java/shop/alien/store/service/impl/TrackEventServiceImpl.java

@@ -512,7 +512,7 @@ public class TrackEventServiceImpl extends ServiceImpl<StoreTrackEventMapper, St
 
             BigDecimal giveToFriendAmount = giveToFriendRecords.stream()
                     .map(record -> {
-                        LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(record.getCouponId());
+                        LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(record.getCouponId());
                         BigDecimal faceValue = coupon != null && coupon.getNominalValue() != null ? coupon.getNominalValue() : BigDecimal.ZERO;
                         int qty = record.getSingleQty() != null && record.getSingleQty() > 0 ? record.getSingleQty() : 0;
                         return faceValue.multiply(BigDecimal.valueOf(qty));
@@ -535,7 +535,7 @@ public class TrackEventServiceImpl extends ServiceImpl<StoreTrackEventMapper, St
             BigDecimal giveToFriendUseAmount = couponUsers.stream()
                     .filter(cu -> cu.getStatus() != null && cu.getStatus() == 1)
                     .map(cu -> {
-                        LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(cu.getCouponId());
+                        LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(cu.getCouponId());
                         return coupon != null && coupon.getNominalValue() != null ? coupon.getNominalValue() : BigDecimal.ZERO;
                     })
                     .reduce(BigDecimal.ZERO, BigDecimal::add);
@@ -561,7 +561,7 @@ public class TrackEventServiceImpl extends ServiceImpl<StoreTrackEventMapper, St
             // 好友赠送金额合计(好友赠送的优惠券面值总和)
             BigDecimal friendGiveAmount = friendCoupons.stream()
                     .map(fc -> {
-                        LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(fc.getCouponId());
+                        LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(fc.getCouponId());
                         return coupon != null && coupon.getNominalValue() != null ? coupon.getNominalValue() : BigDecimal.ZERO;
                     })
                     .reduce(BigDecimal.ZERO, BigDecimal::add);
@@ -682,7 +682,9 @@ public class TrackEventServiceImpl extends ServiceImpl<StoreTrackEventMapper, St
             
             // 查询好友赠送的优惠券/代金券(累计数据)
             LambdaQueryWrapper<LifeDiscountCouponStoreFriend> wrapper = new LambdaQueryWrapper<>();
-            wrapper.in(LifeDiscountCouponStoreFriend::getFriendStoreUserId, storeUserIds)
+            wrapper.and(w -> w.in(LifeDiscountCouponStoreFriend::getSenderUserId, storeUserIds)
+                            .or(sub -> sub.isNull(LifeDiscountCouponStoreFriend::getSenderUserId)
+                                    .in(LifeDiscountCouponStoreFriend::getFriendStoreUserId, storeUserIds)))
                     .lt(LifeDiscountCouponStoreFriend::getCreatedTime, endDate)
                     .eq(LifeDiscountCouponStoreFriend::getDeleteFlag, 0);
             List<LifeDiscountCouponStoreFriend> storeFriends = lifeDiscountCouponStoreFriendMapper.selectList(wrapper);
@@ -690,7 +692,7 @@ public class TrackEventServiceImpl extends ServiceImpl<StoreTrackEventMapper, St
             // 过滤指定类型的优惠券
             return storeFriends.stream()
                     .filter(sf -> {
-                        LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(sf.getCouponId());
+                        LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(sf.getCouponId());
                         return coupon != null && coupon.getType() != null && coupon.getType().equals(type);
                     })
                     .collect(Collectors.toList());
@@ -749,7 +751,7 @@ public class TrackEventServiceImpl extends ServiceImpl<StoreTrackEventMapper, St
             
             return usedCoupons.stream()
                     .map(cu -> {
-                        LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectById(cu.getCouponId());
+                        LifeDiscountCoupon coupon = lifeDiscountCouponMapper.selectByIdIncludeDeleted(cu.getCouponId());
                         return coupon != null && coupon.getNominalValue() != null ? coupon.getNominalValue() : BigDecimal.ZERO;
                     })
                     .reduce(BigDecimal.ZERO, BigDecimal::add);
@@ -825,7 +827,7 @@ public class TrackEventServiceImpl extends ServiceImpl<StoreTrackEventMapper, St
             // 好友赠送金额合计(好友赠送的代金券面值总和)
             BigDecimal friendGiveAmount = friendVouchers.stream()
                     .map(fv -> {
-                        LifeDiscountCoupon voucher = lifeDiscountCouponMapper.selectById(fv.getCouponId());
+                        LifeDiscountCoupon voucher = lifeDiscountCouponMapper.selectByIdIncludeDeleted(fv.getCouponId());
                         return voucher != null && voucher.getNominalValue() != null ? voucher.getNominalValue() : BigDecimal.ZERO;
                     })
                     .reduce(BigDecimal.ZERO, BigDecimal::add);

+ 17 - 2
alien-util/src/main/java/shop/alien/util/coupon/DiscountCouponExpirationUtil.java

@@ -5,7 +5,8 @@ import java.time.ZoneId;
 import java.util.Date;
 
 /**
- * 用户优惠券/代金券到期日:以领取时间为起点叠加有效期天数;无有效天数配置时再回退模板上的绝对截止日期。
+ * 优惠券用户侧到期日:<b>默认以领取日为起点</b>,叠加模板天数({@code specifiedDay} / {@code expirationDate});
+ * 新建模板不应再写死用券起止;模板上的 {@code valid_date}、{@code end_date} 等仅作旧数据回退。
  */
 public final class DiscountCouponExpirationUtil {
 
@@ -15,15 +16,21 @@ public final class DiscountCouponExpirationUtil {
     /**
      * 满减、折扣等 life_discount_coupon 用户券:领取日 + 有效期(天)。
      * 天数优先取模板 {@code specifiedDay},其次 {@code expirationDate} 整型天数字段。
-     * 若均未配置正整数天数,则依次回退模板 {@code validDate}、{@code endDate}。
+     * 若均未配置正整数天数,则依次回退模板 {@code validDate}、{@code endDate}(兼容历史数据)。
+     *
+     * @param longTermValid 模板为 1 时表示长期有效,用户券 {@code expiration_time} 记为 {@code null}
      */
     public static LocalDate resolveLifeDiscountCouponExpiration(
             Date receiveTime,
+            Integer longTermValid,
             String specifiedDay,
             Integer expirationDate,
             LocalDate templateValidDate,
             LocalDate templateEndDate,
             ZoneId zoneId) {
+        if (Integer.valueOf(1).equals(longTermValid)) {
+            return null;
+        }
         ZoneId z = zoneId != null ? zoneId : ZoneId.systemDefault();
         Date rt = receiveTime != null ? receiveTime : new Date();
         LocalDate receiveDate = rt.toInstant().atZone(z).toLocalDate();
@@ -73,4 +80,12 @@ public final class DiscountCouponExpirationUtil {
         }
         return null;
     }
+
+    /**
+     * 模板是否配置了「领取后 N 天有效」的正整数天数({@code specifiedDay} 或 {@code expirationDate})。
+     * 与 {@link #resolveLifeDiscountCouponExpiration} 取天数的规则一致。
+     */
+    public static boolean templateHasPositiveDayValidity(String specifiedDay, Integer expirationDate) {
+        return parseValidityDayCount(specifiedDay, expirationDate) != null;
+    }
 }

+ 22 - 0
alien-util/src/main/java/shop/alien/util/coupon/LifeDiscountCouponStock.java

@@ -0,0 +1,22 @@
+package shop.alien.util.coupon;
+
+/**
+ * 优惠券模板发行量:是否与「不限」等业务字段配合。
+ */
+public final class LifeDiscountCouponStock {
+
+    private LifeDiscountCouponStock() {
+    }
+
+    public static boolean isUnlimitedQty(Integer unlimitedQtyFlag) {
+        return Integer.valueOf(1).equals(unlimitedQtyFlag);
+    }
+
+    /** 模板是否仍可被领取(不限量视为始终有存量) */
+    public static boolean hasTemplateStockRemaining(Integer unlimitedQtyFlag, Integer singleQty) {
+        if (isUnlimitedQty(unlimitedQtyFlag)) {
+            return true;
+        }
+        return singleQty != null && singleQty > 0;
+    }
+}

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff