|
|
@@ -2,11 +2,13 @@ package com.etotem.cfc.service;
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.etotem.cfc.entity.Coupon;
|
|
|
-import com.etotem.cfc.entity.CouponGrantLog;
|
|
|
-import com.etotem.cfc.entity.UserCoupon;
|
|
|
-import com.etotem.cfc.mapper.CouponGrantLogMapper;
|
|
|
+import com.etotem.cfc.entity.FamilyCoupon;
|
|
|
+import com.etotem.cfc.entity.FamilyCouponGrantLog;
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
import com.etotem.cfc.mapper.CouponMapper;
|
|
|
-import com.etotem.cfc.mapper.UserCouponMapper;
|
|
|
+import com.etotem.cfc.mapper.FamilyCouponGrantLogMapper;
|
|
|
+import com.etotem.cfc.mapper.FamilyCouponMapper;
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
import org.springframework.context.annotation.Lazy;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
@@ -29,26 +31,79 @@ public class CouponService {
|
|
|
private CouponMapper couponMapper;
|
|
|
|
|
|
@Resource
|
|
|
- private UserCouponMapper userCouponMapper;
|
|
|
+ private FamilyCouponMapper familyCouponMapper;
|
|
|
|
|
|
@Resource
|
|
|
- private CouponGrantLogMapper couponGrantLogMapper;
|
|
|
+ private FamilyCouponGrantLogMapper familyCouponGrantLogMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private UserMapper userMapper;
|
|
|
|
|
|
@Resource
|
|
|
@Lazy
|
|
|
private MembershipService membershipService;
|
|
|
|
|
|
+ // ----------------------------------------------------------------
|
|
|
+ // 内部辅助方法
|
|
|
+ // ----------------------------------------------------------------
|
|
|
+
|
|
|
+ /** userId → familyId(用于查询当前用户所在家庭) */
|
|
|
+ private Long getFamilyId(Long userId) {
|
|
|
+ User user = userMapper.selectById(userId);
|
|
|
+ if (user == null || user.getFamilyId() == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return user.getFamilyId();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 构建与 CouponService.listMyCoupons 相同格式的响应 Map */
|
|
|
+ private Map<String, Object> toCouponVO(FamilyCoupon fc) {
|
|
|
+ Coupon coupon = couponMapper.selectById(fc.getCouponId());
|
|
|
+ if (coupon == null) return null;
|
|
|
+ Date now = new Date();
|
|
|
+ String status = fc.getStatus();
|
|
|
+ if ("AVAILABLE".equals(status) && !"ACTIVE".equals(coupon.getStatus())) {
|
|
|
+ status = "DISABLED";
|
|
|
+ } else if ("AVAILABLE".equals(status) && coupon.getValidUntil() != null && now.after(coupon.getValidUntil())) {
|
|
|
+ status = "EXPIRED";
|
|
|
+ }
|
|
|
+ Map<String, Object> item = new HashMap<>();
|
|
|
+ // 对外字段名保持 userCouponId / id 等原有契约,前端不感知内部实现变化
|
|
|
+ item.put("userCouponId", fc.getId());
|
|
|
+ item.put("id", fc.getId());
|
|
|
+ item.put("couponId", coupon.getId());
|
|
|
+ item.put("name", coupon.getName());
|
|
|
+ item.put("type", coupon.getType());
|
|
|
+ item.put("value", coupon.getValue());
|
|
|
+ item.put("discountRate", coupon.getDiscountRate());
|
|
|
+ item.put("minSpend", coupon.getMinSpend());
|
|
|
+ item.put("applicableTo", coupon.getApplicableTo());
|
|
|
+ item.put("validFrom", coupon.getValidFrom());
|
|
|
+ item.put("validUntil", coupon.getValidUntil());
|
|
|
+ item.put("status", status);
|
|
|
+ item.put("receivedAt", fc.getReceivedAt());
|
|
|
+ item.put("usedAt", fc.getUsedAt());
|
|
|
+ item.put("orderId", fc.getOrderId());
|
|
|
+ return item;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ----------------------------------------------------------------
|
|
|
+ // 对外保留接口(签名不变,内部改为读 family_coupon)
|
|
|
+ // ----------------------------------------------------------------
|
|
|
+
|
|
|
public List<Coupon> listAvailable(Long userId) {
|
|
|
- List<UserCoupon> userCoupons = userCouponMapper.selectList(
|
|
|
- new LambdaQueryWrapper<UserCoupon>()
|
|
|
- .eq(UserCoupon::getUserId, userId)
|
|
|
- .eq(UserCoupon::getStatus, "AVAILABLE"));
|
|
|
- if (userCoupons.isEmpty()) {
|
|
|
+ Long familyId = getFamilyId(userId);
|
|
|
+ if (familyId == null) return new ArrayList<>();
|
|
|
+ List<FamilyCoupon> familyCoupons = familyCouponMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<FamilyCoupon>()
|
|
|
+ .eq(FamilyCoupon::getFamilyId, familyId)
|
|
|
+ .eq(FamilyCoupon::getStatus, "AVAILABLE"));
|
|
|
+ if (familyCoupons.isEmpty()) {
|
|
|
return new ArrayList<>();
|
|
|
}
|
|
|
List<Long> couponIds = new ArrayList<>();
|
|
|
- for (UserCoupon uc : userCoupons) {
|
|
|
- couponIds.add(uc.getCouponId());
|
|
|
+ for (FamilyCoupon fc : familyCoupons) {
|
|
|
+ couponIds.add(fc.getCouponId());
|
|
|
}
|
|
|
Date now = new Date();
|
|
|
return couponMapper.selectList(
|
|
|
@@ -62,6 +117,8 @@ public class CouponService {
|
|
|
|
|
|
@Transactional
|
|
|
public String claim(Long userId, Long couponId) {
|
|
|
+ Long familyId = getFamilyId(userId);
|
|
|
+ if (familyId == null) return "未加入家庭";
|
|
|
Coupon coupon = couponMapper.selectById(couponId);
|
|
|
if (coupon == null) {
|
|
|
return "优惠券不存在";
|
|
|
@@ -80,20 +137,15 @@ public class CouponService {
|
|
|
if (coupon.getValidUntil() != null && now.after(coupon.getValidUntil())) {
|
|
|
return "优惠券已过期";
|
|
|
}
|
|
|
- // 非会员也可领取会员券,但在使用时会校验会员身份
|
|
|
- Long count = userCouponMapper.selectCount(
|
|
|
- new LambdaQueryWrapper<UserCoupon>()
|
|
|
- .eq(UserCoupon::getUserId, userId)
|
|
|
- .eq(UserCoupon::getCouponId, couponId));
|
|
|
+ // 去重:同一个家庭同一张券只发一次(同 grantType 的幂等也通过 grant 方法保证)
|
|
|
+ Long count = familyCouponMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<FamilyCoupon>()
|
|
|
+ .eq(FamilyCoupon::getFamilyId, familyId)
|
|
|
+ .eq(FamilyCoupon::getCouponId, couponId));
|
|
|
if (count != null && count > 0) {
|
|
|
return "已领取过该优惠券";
|
|
|
}
|
|
|
- UserCoupon uc = new UserCoupon();
|
|
|
- uc.setUserId(userId);
|
|
|
- uc.setCouponId(couponId);
|
|
|
- uc.setStatus("AVAILABLE");
|
|
|
- uc.setReceivedAt(new Date());
|
|
|
- userCouponMapper.insert(uc);
|
|
|
+ issueToFamily(couponId, familyId);
|
|
|
coupon.setUsedCount(coupon.getUsedCount() == null ? 1 : coupon.getUsedCount() + 1);
|
|
|
couponMapper.updateById(coupon);
|
|
|
return "领取成功";
|
|
|
@@ -109,11 +161,13 @@ public class CouponService {
|
|
|
}
|
|
|
|
|
|
public Integer apply(Long userId, Long userCouponId, String orderType, Integer orderAmount) {
|
|
|
- UserCoupon uc = userCouponMapper.selectById(userCouponId);
|
|
|
- if (uc == null || !uc.getUserId().equals(userId) || !"AVAILABLE".equals(uc.getStatus())) {
|
|
|
+ Long familyId = getFamilyId(userId);
|
|
|
+ if (familyId == null) return null;
|
|
|
+ FamilyCoupon fc = familyCouponMapper.selectById(userCouponId);
|
|
|
+ if (fc == null || !fc.getFamilyId().equals(familyId) || !"AVAILABLE".equals(fc.getStatus())) {
|
|
|
return null;
|
|
|
}
|
|
|
- Coupon coupon = couponMapper.selectById(uc.getCouponId());
|
|
|
+ Coupon coupon = couponMapper.selectById(fc.getCouponId());
|
|
|
if (coupon == null) {
|
|
|
return null;
|
|
|
}
|
|
|
@@ -149,23 +203,23 @@ public class CouponService {
|
|
|
|
|
|
@Transactional
|
|
|
public void markUsed(Long userCouponId, Long orderId) {
|
|
|
- UserCoupon uc = userCouponMapper.selectById(userCouponId);
|
|
|
- if (uc != null) {
|
|
|
- uc.setStatus("USED");
|
|
|
- uc.setUsedAt(new Date());
|
|
|
- uc.setOrderId(orderId);
|
|
|
- userCouponMapper.updateById(uc);
|
|
|
+ FamilyCoupon fc = familyCouponMapper.selectById(userCouponId);
|
|
|
+ if (fc != null) {
|
|
|
+ fc.setStatus("USED");
|
|
|
+ fc.setUsedAt(new Date());
|
|
|
+ fc.setOrderId(orderId);
|
|
|
+ familyCouponMapper.updateById(fc);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@Transactional
|
|
|
public void revert(Long userCouponId) {
|
|
|
- UserCoupon uc = userCouponMapper.selectById(userCouponId);
|
|
|
- if (uc != null) {
|
|
|
- uc.setStatus("AVAILABLE");
|
|
|
- uc.setUsedAt(null);
|
|
|
- uc.setOrderId(null);
|
|
|
- userCouponMapper.updateById(uc);
|
|
|
+ FamilyCoupon fc = familyCouponMapper.selectById(userCouponId);
|
|
|
+ if (fc != null) {
|
|
|
+ fc.setStatus("AVAILABLE");
|
|
|
+ fc.setUsedAt(null);
|
|
|
+ fc.setOrderId(null);
|
|
|
+ familyCouponMapper.updateById(fc);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -177,22 +231,23 @@ public class CouponService {
|
|
|
return couponMapper.selectById(couponId);
|
|
|
}
|
|
|
|
|
|
+ /** 单条发放到家庭(替代旧 issueToUser) */
|
|
|
@Transactional
|
|
|
- public void issueToUser(Long couponId, Long userId) {
|
|
|
- UserCoupon uc = new UserCoupon();
|
|
|
- uc.setCouponId(couponId);
|
|
|
- uc.setUserId(userId);
|
|
|
- uc.setStatus("AVAILABLE");
|
|
|
- uc.setReceivedAt(new Date());
|
|
|
- userCouponMapper.insert(uc);
|
|
|
+ public void issueToFamily(Long couponId, Long familyId) {
|
|
|
+ FamilyCoupon fc = new FamilyCoupon();
|
|
|
+ fc.setCouponId(couponId);
|
|
|
+ fc.setFamilyId(familyId);
|
|
|
+ fc.setStatus("AVAILABLE");
|
|
|
+ fc.setReceivedAt(new Date());
|
|
|
+ familyCouponMapper.insert(fc);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 发放优惠券并落流水(幂等)。
|
|
|
+ * 发放优惠券并落流水(幂等,家庭维度)。
|
|
|
* period 非空时先查流水:已存在则跳过(PERIODIC 防重用);唯一键冲突由事务回滚保证一致性。
|
|
|
*/
|
|
|
@Transactional
|
|
|
- public boolean grant(Long userId, Long couponId, String grantType, String period, String source) {
|
|
|
+ public boolean grantFamily(Long familyId, Long couponId, String grantType, String period, String source) {
|
|
|
Coupon coupon = couponMapper.selectById(couponId);
|
|
|
if (coupon == null) {
|
|
|
return false;
|
|
|
@@ -201,12 +256,12 @@ public class CouponService {
|
|
|
return false;
|
|
|
}
|
|
|
if (period != null) {
|
|
|
- Long exists = couponGrantLogMapper.selectCount(
|
|
|
- new LambdaQueryWrapper<CouponGrantLog>()
|
|
|
- .eq(CouponGrantLog::getUserId, userId)
|
|
|
- .eq(CouponGrantLog::getCouponId, couponId)
|
|
|
- .eq(CouponGrantLog::getGrantType, grantType)
|
|
|
- .eq(CouponGrantLog::getPeriod, period));
|
|
|
+ Long exists = familyCouponGrantLogMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<FamilyCouponGrantLog>()
|
|
|
+ .eq(FamilyCouponGrantLog::getFamilyId, familyId)
|
|
|
+ .eq(FamilyCouponGrantLog::getCouponId, couponId)
|
|
|
+ .eq(FamilyCouponGrantLog::getGrantType, grantType)
|
|
|
+ .eq(FamilyCouponGrantLog::getPeriod, period));
|
|
|
if (exists != null && exists > 0) {
|
|
|
return false;
|
|
|
}
|
|
|
@@ -214,46 +269,64 @@ public class CouponService {
|
|
|
int quantity = coupon.getGrantQuantity() != null && coupon.getGrantQuantity() > 0
|
|
|
? coupon.getGrantQuantity() : 1;
|
|
|
for (int i = 0; i < quantity; i++) {
|
|
|
- issueToUser(couponId, userId);
|
|
|
+ issueToFamily(couponId, familyId);
|
|
|
}
|
|
|
- CouponGrantLog log = new CouponGrantLog();
|
|
|
- log.setUserId(userId);
|
|
|
+ FamilyCouponGrantLog log = new FamilyCouponGrantLog();
|
|
|
+ log.setFamilyId(familyId);
|
|
|
log.setCouponId(couponId);
|
|
|
log.setGrantType(grantType);
|
|
|
log.setPeriod(period);
|
|
|
log.setQuantity(quantity);
|
|
|
log.setSource(source);
|
|
|
- couponGrantLogMapper.insert(log);
|
|
|
+ familyCouponGrantLogMapper.insert(log);
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
/** JOIN: 开通/续费指定等级会员时发放 */
|
|
|
@Transactional
|
|
|
- public void grantJoinCoupons(Long userId, String levelCode, String source) {
|
|
|
+ public void grantFamilyJoinCoupons(Long familyId, String levelCode, String source) {
|
|
|
List<Coupon> coupons = couponMapper.selectList(
|
|
|
new LambdaQueryWrapper<Coupon>()
|
|
|
.eq(Coupon::getGrantType, "JOIN")
|
|
|
.eq(Coupon::getGrantLevelCode, levelCode)
|
|
|
.and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE")));
|
|
|
if (coupons.isEmpty()) {
|
|
|
- logger.warn("未找到 JOIN 赠券模板: userId={}, levelCode={}", userId, levelCode);
|
|
|
+ logger.warn("未找到 JOIN 赠券模板: familyId={}, levelCode={}", familyId, levelCode);
|
|
|
return;
|
|
|
}
|
|
|
for (Coupon c : coupons) {
|
|
|
- grant(userId, c.getId(), "JOIN", "JOIN", source);
|
|
|
+ grantFamily(familyId, c.getId(), "JOIN", "JOIN", source);
|
|
|
+ }
|
|
|
+ logger.info("发放 JOIN 赠券完成: familyId={}, levelCode={}, count={}", familyId, levelCode, coupons.size());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 兼容旧签名(标记废弃,保留供降级) */
|
|
|
+ @Deprecated
|
|
|
+ public void grantJoinCoupons(Long userId, String levelCode, String source) {
|
|
|
+ Long familyId = getFamilyId(userId);
|
|
|
+ if (familyId != null) {
|
|
|
+ grantFamilyJoinCoupons(familyId, levelCode, source);
|
|
|
}
|
|
|
- logger.info("发放 JOIN 赠券完成: userId={}, levelCode={}, count={}", userId, levelCode, coupons.size());
|
|
|
}
|
|
|
|
|
|
- /** POPULATION: 新增家庭成员时发放(目标账户由调用方决定) */
|
|
|
+ /** POPULATION: 新增家庭成员时发放 */
|
|
|
@Transactional
|
|
|
- public void grantPopulationCoupons(Long userId, Long memberId) {
|
|
|
+ public void grantFamilyPopulationCoupons(Long familyId, Long memberId) {
|
|
|
List<Coupon> coupons = couponMapper.selectList(
|
|
|
new LambdaQueryWrapper<Coupon>()
|
|
|
.eq(Coupon::getGrantType, "POPULATION")
|
|
|
.and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE")));
|
|
|
for (Coupon c : coupons) {
|
|
|
- grant(userId, c.getId(), "POPULATION", null, "member:" + memberId);
|
|
|
+ grantFamily(familyId, c.getId(), "POPULATION", null, "member:" + memberId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 兼容旧签名(标记废弃,保留供降级) */
|
|
|
+ @Deprecated
|
|
|
+ public void grantPopulationCoupons(Long userId, Long memberId) {
|
|
|
+ Long familyId = getFamilyId(userId);
|
|
|
+ if (familyId != null) {
|
|
|
+ grantFamilyPopulationCoupons(familyId, memberId);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -291,16 +364,18 @@ public class CouponService {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 查询用户尚未拥有的可兑换优惠券(用于结算页直接兑换)
|
|
|
+ * 查询家庭尚未拥有的可兑换优惠券(用于结算页直接兑换)
|
|
|
* @param productId 商品ID,非空时只返回该商品绑定的券
|
|
|
*/
|
|
|
public List<Coupon> listUnownedExchangeable(Long userId, Long productId) {
|
|
|
+ Long familyId = getFamilyId(userId);
|
|
|
+ if (familyId == null) return new ArrayList<>();
|
|
|
Date now = new Date();
|
|
|
- List<Long> ownedIds = userCouponMapper.selectList(
|
|
|
- new LambdaQueryWrapper<UserCoupon>()
|
|
|
- .eq(UserCoupon::getUserId, userId)
|
|
|
- .eq(UserCoupon::getStatus, "AVAILABLE"))
|
|
|
- .stream().map(UserCoupon::getCouponId).collect(java.util.stream.Collectors.toList());
|
|
|
+ List<Long> ownedIds = familyCouponMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<FamilyCoupon>()
|
|
|
+ .eq(FamilyCoupon::getFamilyId, familyId)
|
|
|
+ .eq(FamilyCoupon::getStatus, "AVAILABLE"))
|
|
|
+ .stream().map(FamilyCoupon::getCouponId).collect(java.util.stream.Collectors.toList());
|
|
|
LambdaQueryWrapper<Coupon> wrapper = new LambdaQueryWrapper<Coupon>()
|
|
|
.gt(Coupon::getPointsPrice, 0)
|
|
|
.and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE"))
|
|
|
@@ -322,45 +397,32 @@ public class CouponService {
|
|
|
.and(w -> w.isNull(Coupon::getStatus).or().eq(Coupon::getStatus, "ACTIVE")));
|
|
|
}
|
|
|
|
|
|
- /** 我的优惠券:返回 user_coupon + coupon 联合数据,含 status/receivedAt/usedAt */
|
|
|
+ /**
|
|
|
+ * 我的优惠券:返回 family_coupon + coupon 联合数据,含 status/receivedAt/usedAt
|
|
|
+ * 对外字段名保持不变(userCouponId = family_coupon.id),前端不感知内部变化
|
|
|
+ */
|
|
|
public List<Map<String, Object>> listMyCoupons(Long userId) {
|
|
|
- List<UserCoupon> userCoupons = userCouponMapper.selectList(
|
|
|
- new LambdaQueryWrapper<UserCoupon>()
|
|
|
- .eq(UserCoupon::getUserId, userId)
|
|
|
- .orderByDesc(UserCoupon::getReceivedAt));
|
|
|
- if (userCoupons.isEmpty()) {
|
|
|
+ Long familyId = getFamilyId(userId);
|
|
|
+ if (familyId == null) return new ArrayList<>();
|
|
|
+ List<FamilyCoupon> familyCoupons = familyCouponMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<FamilyCoupon>()
|
|
|
+ .eq(FamilyCoupon::getFamilyId, familyId)
|
|
|
+ .orderByDesc(FamilyCoupon::getReceivedAt));
|
|
|
+ if (familyCoupons.isEmpty()) {
|
|
|
return new ArrayList<>();
|
|
|
}
|
|
|
- Date now = new Date();
|
|
|
List<Map<String, Object>> result = new ArrayList<>();
|
|
|
- for (UserCoupon uc : userCoupons) {
|
|
|
- Coupon coupon = couponMapper.selectById(uc.getCouponId());
|
|
|
- if (coupon == null) continue;
|
|
|
- // 过期判断
|
|
|
- String status = uc.getStatus();
|
|
|
- if ("AVAILABLE".equals(status) && !"ACTIVE".equals(coupon.getStatus())) {
|
|
|
- status = "DISABLED";
|
|
|
- } else if ("AVAILABLE".equals(status) && coupon.getValidUntil() != null && now.after(coupon.getValidUntil())) {
|
|
|
- status = "EXPIRED";
|
|
|
- }
|
|
|
- Map<String, Object> item = new HashMap<>();
|
|
|
- item.put("userCouponId", uc.getId());
|
|
|
- item.put("id", uc.getId());
|
|
|
- item.put("couponId", coupon.getId());
|
|
|
- item.put("name", coupon.getName());
|
|
|
- item.put("type", coupon.getType());
|
|
|
- item.put("value", coupon.getValue());
|
|
|
- item.put("discountRate", coupon.getDiscountRate());
|
|
|
- item.put("minSpend", coupon.getMinSpend());
|
|
|
- item.put("applicableTo", coupon.getApplicableTo());
|
|
|
- item.put("validFrom", coupon.getValidFrom());
|
|
|
- item.put("validUntil", coupon.getValidUntil());
|
|
|
- item.put("status", status);
|
|
|
- item.put("receivedAt", uc.getReceivedAt());
|
|
|
- item.put("usedAt", uc.getUsedAt());
|
|
|
- item.put("orderId", uc.getOrderId());
|
|
|
- result.add(item);
|
|
|
+ for (FamilyCoupon fc : familyCoupons) {
|
|
|
+ Map<String, Object> vo = toCouponVO(fc);
|
|
|
+ if (vo != null) result.add(vo);
|
|
|
}
|
|
|
return result;
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询指定会员等级的活跃家庭 ID 列表(供 CouponGrantTask 周期补发)
|
|
|
+ */
|
|
|
+ public List<Long> listActiveMemberFamilyIds(String levelCode) {
|
|
|
+ return membershipService.listActiveMemberFamilyIds(levelCode);
|
|
|
+ }
|
|
|
}
|