package cn.iocoder.yudao.module.promotion.service.coupon; import cn.hutool.core.collection.CollStreamUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; import cn.iocoder.yudao.module.member.api.user.MemberUserApi; import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO; import cn.iocoder.yudao.module.promotion.convert.coupon.CouponConvert; import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO; import cn.iocoder.yudao.module.promotion.dal.mysql.coupon.CouponMapper; import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum; import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTakeTypeEnum; import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityTypeEnum; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*; import static java.util.Arrays.asList; /** * 优惠劵 Service 实现类 * * @author 芋道源码 */ @Slf4j @Service @Validated public class CouponServiceImpl implements CouponService { @Resource private CouponTemplateService couponTemplateService; @Resource private CouponMapper couponMapper; @Resource private MemberUserApi memberUserApi; @Override public void useCoupon(Long id, Long userId, Long orderId) { // 校验状态 CouponDO coupon = couponMapper.selectByIdAndUserId(id, userId); if (ObjectUtil.notEqual(coupon.getStatus(), CouponStatusEnum.UNUSED.getStatus())) { throw exception(COUPON_STATUS_NOT_UNUSED); } // 校验有效期;为避免定时器没跑,实际优惠劵已经过期 if (!LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime())) { throw exception(COUPON_VALID_TIME_NOT_NOW); } // 更新状态 int updateCount = couponMapper.updateByIdAndStatus(id, CouponStatusEnum.UNUSED.getStatus(), new CouponDO().setStatus(CouponStatusEnum.USED.getStatus()) .setUseOrderId(orderId).setUseTime(LocalDateTime.now())); if (updateCount == 0) { throw exception(COUPON_STATUS_NOT_UNUSED); } } @Override public void returnUsedCoupon(Long id) { // 校验存在 CouponDO coupon = couponMapper.selectById(id); if (coupon == null) { throw exception(COUPON_NOT_EXISTS); } // 校验状态 if (ObjectUtil.notEqual(coupon.getStatus(), CouponStatusEnum.USED.getStatus())) { throw exception(COUPON_STATUS_NOT_USED); } // 退还 Integer status = LocalDateTimeUtils.beforeNow(coupon.getValidEndTime()) ? CouponStatusEnum.EXPIRE.getStatus() // 退还时可能已经过期了 : CouponStatusEnum.UNUSED.getStatus(); int updateCount = couponMapper.updateByIdAndStatus(id, CouponStatusEnum.USED.getStatus(), new CouponDO().setStatus(status)); if (updateCount == 0) { throw exception(COUPON_STATUS_NOT_USED); } // TODO 增加优惠券变动记录? } @Override @Transactional public void deleteCoupon(Long id) { // 校验存在 CouponDO coupon = validateCouponExists(id); // 更新优惠劵 int deleteCount = couponMapper.delete(id, asList(CouponStatusEnum.UNUSED.getStatus(), CouponStatusEnum.EXPIRE.getStatus())); if (deleteCount == 0) { throw exception(COUPON_DELETE_FAIL_USED); } // 减少优惠劵模板的领取数量 -1 couponTemplateService.updateCouponTemplateTakeCount(coupon.getTemplateId(), -1); } @Override @Transactional(rollbackFor = Exception.class) public Map> takeCoupon(Long templateId, Set userIds, CouponTakeTypeEnum takeType) { CouponTemplateDO template = couponTemplateService.getCouponTemplate(templateId); return takeCoupon(template, userIds, takeType); } private Map> takeCoupon(CouponTemplateDO template, Set userIds, CouponTakeTypeEnum takeType) { // 1. 过滤掉达到领取限制的用户 removeTakeLimitUser(userIds, template); // 2. 校验优惠劵是否可以领取 validateCouponTemplateCanTake(template, userIds, takeType); // 3. 批量保存优惠劵 List couponList = convertList(userIds, userId -> CouponConvert.INSTANCE.convert(template, userId)); couponMapper.insertBatch(couponList); // 4. 增加优惠劵模板的领取数量 couponTemplateService.updateCouponTemplateTakeCount(template.getId(), userIds.size()); return convertMultiMap(couponList, CouponDO::getUserId, CouponDO::getId); } @Override public List takeCouponsByAdmin(Map giveCoupons, Long userId) { if (CollUtil.isEmpty(giveCoupons)) { return Collections.emptyList(); } List couponIds = new ArrayList<>(); // 循环发放 for (Map.Entry entry : giveCoupons.entrySet()) { try { for (int i = 0; i < entry.getValue(); i++) { Map> userCouponIdsMap = getSelf().takeCoupon(entry.getKey(), CollUtil.newHashSet(userId), CouponTakeTypeEnum.ADMIN); findAndThen(userCouponIdsMap, userId, couponIds::addAll); } } catch (Exception e) { log.error("[takeCouponsByAdmin][coupon({}) 优惠券发放失败 userId({})]", entry, userId, e); } } return couponIds; } @Override public void invalidateCouponsByAdmin(List giveCouponIds, Long userId) { // 循环收回 for (Long couponId : giveCouponIds) { // couponId 为空或 0 则跳过 if (null == couponId || couponId <= 0) { continue; } try { getSelf().invalidateCoupon(couponId, userId); } catch (Exception e) { log.error("[invalidateCouponsByAdmin][couponId({}) 收回优惠券失败]", couponId, e); } } } /** * 【管理员】收回优惠券 * * @param couponId 模版编号 * @param userId 用户编号 */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW) // 每次调用开启一个新的事务,避免在一个大的事务里面 public void invalidateCoupon(Long couponId, Long userId) { if (couponId == null || couponId <= 0) { return; } // 1.1 校验优惠券 CouponDO coupon = couponMapper.selectByIdAndUserId(couponId, userId); if (coupon == null) { throw exception(COUPON_NOT_EXISTS); } // 1.2 校验模板 CouponTemplateDO couponTemplate = couponTemplateService.getCouponTemplate(coupon.getTemplateId()); if (couponTemplate == null) { throw exception(COUPON_TEMPLATE_NOT_EXISTS); } // 1.3 校验优惠券是否已经使用,如若使用则先不管 if (ObjUtil.equal(coupon.getStatus(), CouponStatusEnum.USED.getStatus())) { log.info("[invalidateCoupon][coupon({}) 已经使用,无法作废]", couponId); return; } // 2.1 减少优惠劵模板的领取数量 couponTemplateService.updateCouponTemplateTakeCount(couponTemplate.getId(), -1); // 2.2 作废优惠劵 couponMapper.deleteById(couponId); } @Override @Transactional(rollbackFor = Exception.class) public void takeCouponByRegister(Long userId) { List templates = couponTemplateService.getCouponTemplateListByTakeType(CouponTakeTypeEnum.REGISTER); for (CouponTemplateDO template : templates) { takeCoupon(template, CollUtil.newHashSet(userId), CouponTakeTypeEnum.REGISTER); } } @Override public int expireCoupon() { // 1. 查询待过期的优惠券 List list = couponMapper.selectListByStatusAndValidEndTimeLe( CouponStatusEnum.UNUSED.getStatus(), LocalDateTime.now()); if (CollUtil.isEmpty(list)) { return 0; } // 2. 遍历执行 int count = 0; for (CouponDO coupon : list) { try { boolean success = expireCoupon(coupon); if (success) { count++; } } catch (Exception e) { log.error("[expireCoupon][coupon({}) 更新为已过期失败]", coupon.getId(), e); } } return count; } /** * 过期单个优惠劵 * * @param coupon 优惠劵 * @return 是否过期成功 */ private boolean expireCoupon(CouponDO coupon) { // 更新记录状态 int updateRows = couponMapper.updateByIdAndStatus(coupon.getId(), CouponStatusEnum.UNUSED.getStatus(), new CouponDO().setStatus(CouponStatusEnum.EXPIRE.getStatus())); if (updateRows == 0) { log.error("[expireCoupon][coupon({}) 更新为已过期失败]", coupon.getId()); return false; } log.info("[expireCoupon][coupon({}) 更新为已过期成功]", coupon.getId()); return true; } /** * 校验优惠券是否可以领取 * * @param couponTemplate 优惠券模板 * @param userIds 领取人列表 * @param takeType 领取方式 */ private void validateCouponTemplateCanTake(CouponTemplateDO couponTemplate, Set userIds, CouponTakeTypeEnum takeType) { // 如果所有用户都领取过,则抛出异常 if (CollUtil.isEmpty(userIds)) { throw exception(COUPON_TEMPLATE_USER_ALREADY_TAKE); } // 校验模板 if (couponTemplate == null) { throw exception(COUPON_TEMPLATE_NOT_EXISTS); } // 校验领取方式 if (ObjUtil.notEqual(couponTemplate.getTakeType(), takeType.getType())) { throw exception(COUPON_TEMPLATE_CANNOT_TAKE); } // 校验剩余发放数量是否充足(仅在 CouponTakeTypeEnum.USER 用户领取时) // 关联案例:https://t.zsxq.com/mElGQ、https://t.zsxq.com/6pLzr if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType()) && !couponTemplateService.isTotalCountUnlimited(couponTemplate.getTotalCount()) // 校验不限制总发放数量 && couponTemplate.getTakeCount() > couponTemplate.getTotalCount()) { // 已领取数量 > 总发放数量 throw exception(COUPON_TEMPLATE_NOT_ENOUGH); } // 校验"固定日期"的有效期类型是否过期 if (CouponTemplateValidityTypeEnum.DATE.getType().equals(couponTemplate.getValidityType())) { if (LocalDateTimeUtils.beforeNow(couponTemplate.getValidEndTime())) { throw exception(COUPON_TEMPLATE_EXPIRED); } } } /** * 过滤掉达到领取上线的用户 * * @param userIds 用户编号数组 * @param couponTemplate 优惠劵模版 */ private void removeTakeLimitUser(Set userIds, CouponTemplateDO couponTemplate) { if (couponTemplateService.isTakeLimitCountUnlimited(couponTemplate.getTakeLimitCount())) { return; } // 查询已领过券的用户 List alreadyTakeCoupons = couponMapper.selectListByTemplateIdAndUserId(couponTemplate.getId(), userIds); if (CollUtil.isEmpty(alreadyTakeCoupons)) { return; } // 移除达到领取限制的用户 Map userTakeCountMap = CollStreamUtil.groupBy(alreadyTakeCoupons, CouponDO::getUserId, Collectors.summingInt(c -> 1)); userIds.removeIf(userId -> MapUtil.getInt(userTakeCountMap, userId, 0) >= couponTemplate.getTakeLimitCount()); } //======================= 查询相关 ======================= @Override public Long getUnusedCouponCount(Long userId) { return couponMapper.selectCountByUserIdAndStatus(userId, CouponStatusEnum.UNUSED.getStatus()); } @Override public PageResult getCouponPage(CouponPageReqVO pageReqVO) { // 获得用户编号 if (StrUtil.isNotEmpty(pageReqVO.getNickname())) { List users = memberUserApi.getUserListByNickname(pageReqVO.getNickname()); if (CollUtil.isEmpty(users)) { return PageResult.empty(); } pageReqVO.setUserIds(convertSet(users, MemberUserRespDTO::getId)); } // 分页查询 return couponMapper.selectPage(pageReqVO); } @Override public List getCouponList(Long userId, Integer status) { return couponMapper.selectListByUserIdAndStatus(userId, status); } @Override public Map getTakeCountMapByTemplateIds(Collection templateIds, Long userId) { if (CollUtil.isEmpty(templateIds)) { return Collections.emptyMap(); } return couponMapper.selectCountByUserIdAndTemplateIdIn(userId, templateIds); } @Override public Map getUserCanCanTakeMap(Long userId, List templates) { // 1. 未登录时,都显示可以领取 Map userCanTakeMap = convertMap(templates, CouponTemplateDO::getId, templateId -> true); if (userId == null) { return userCanTakeMap; } // 2.1 过滤领取数量无限制的 Set templateIds = convertSet(templates, CouponTemplateDO::getId, template -> !couponTemplateService.isTakeLimitCountUnlimited(template.getTakeLimitCount())); // 2.2 检查用户领取的数量是否超过限制 if (CollUtil.isNotEmpty(templateIds)) { Map couponTakeCountMap = this.getTakeCountMapByTemplateIds(templateIds, userId); for (CouponTemplateDO template : templates) { Integer takeCount = couponTakeCountMap.get(template.getId()); userCanTakeMap.put(template.getId(), takeCount == null || takeCount < template.getTakeLimitCount()); } } return userCanTakeMap; } @Override public CouponDO getCoupon(Long userId, Long id) { return couponMapper.selectByIdAndUserId(id, userId); } private CouponDO validateCouponExists(Long id) { CouponDO coupon = couponMapper.selectById(id); if (coupon == null) { throw exception(COUPON_NOT_EXISTS); } return coupon; } /** * 获得自身的代理对象,解决 AOP 生效问题 * * @return 自己 */ private CouponServiceImpl getSelf() { return SpringUtil.getBean(getClass()); } }