package com.xiaopeng.account.service.impl; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.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.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.xiaopeng.account.mapper.XpAppUserCouponMapper; import com.xiaopeng.account.model.dto.ReceiveCouponDto; import com.xiaopeng.account.model.dto.SendCouponDto; import com.xiaopeng.account.model.query.XpUserCouponListQuery; import com.xiaopeng.account.model.vo.XpAppUserCouponListVo; import com.xiaopeng.account.model.vo.XpAppUserCouponTotalVo; import com.xiaopeng.account.model.vo.XpAppUserCouponVo; import com.xiaopeng.account.service.IXpAppUserCouponService; import com.xiaopeng.account.util.holiday.HolidayUtil; import com.xiaopeng.account.vo.CouponVo; import com.xiaopeng.account.vo.UserCouponListVo; import com.xiaopeng.api.dto.account.NavigationCouponPickerRequest; import com.xiaopeng.api.dto.account.UcUserInfo; import com.xiaopeng.api.dto.account.UseUserCouponDto; import com.xiaopeng.api.enums.NavigationCouponBubbleType; import com.xiaopeng.api.feignClient.operation.XpActicityClient; import com.xiaopeng.api.feignClient.pushMessage.SubscribeMessageClient; import com.xiaopeng.api.model.operation.XpActicityStrategy; import com.xiaopeng.api.query.account.XpAppUserCouponPageListQuery; import com.xiaopeng.api.vo.account.NavigationCouponItemVo; import com.xiaopeng.api.vo.account.NavigationCouponPickerVo; import com.xiaopeng.api.feignClient.account.UserResourcesClient; import com.xiaopeng.api.feignClient.operation.XpCouponClient; import com.xiaopeng.api.feignClient.operation.XpWhiteListClient; import com.xiaopeng.api.model.account.XpAppUserCoupon; import com.xiaopeng.api.model.operation.XpCoupon; import com.xiaopeng.api.vo.pushMessage.SendSubscribeMessageVo; import com.xiaopeng.common.core.domain.R; import com.xiaopeng.common.core.enums.MessageTemplateEnum; import com.xiaopeng.common.core.utils.DateUtils; import com.xiaopeng.common.core.web.page.PageInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; /** *

* 用户优惠券 服务实现类 *

* * @author mitao * @since 2026-02-03 */ @Service public class XpAppUserCouponServiceImpl extends ServiceImpl implements IXpAppUserCouponService { @Autowired private HolidayUtil holidayUtil; @Autowired private XpCouponClient xpCouponClient; @Autowired private XpWhiteListClient xpWhiteListClient; @Autowired private UserResourcesClient userResourcesClient; @Resource private SubscribeMessageClient subscribeMessageClient; @Resource private XpActicityClient xpActicityClient; @Override public PageInfo pageList(XpAppUserCouponPageListQuery query) { PageInfo pageInfo = new PageInfo<>(query.getPageNum(),query.getPageSize()); List list = this.baseMapper.pageList(pageInfo,query); pageInfo.setRecords(list); return pageInfo; } /** * 查询未使用、还有24小时就要过期的数据 * @return */ @Override public List findListExpirationTime(String appUserId){ LocalDateTime now = LocalDateTime.now(); LocalDateTime expirationThreshold = now.plusHours(24); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(XpAppUserCoupon::getDeleted, 0); wrapper.eq(XpAppUserCoupon::getStatus, 1); if(appUserId != null){ wrapper.eq(XpAppUserCoupon::getAppUserId, appUserId); } // - 券失效前 24 小时 wrapper.ge(XpAppUserCoupon::getExpirationTime, now); wrapper.le(XpAppUserCoupon::getExpirationTime, expirationThreshold); // 过滤 sendMsgTimeWx 不是当天发送的消息,避免重复发送 wrapper.isNull(XpAppUserCoupon::getSendMsgTimeWx) .or() .lt(XpAppUserCoupon::getSendMsgTimeWx, now.toLocalDate().atStartOfDay()); return super.list(wrapper); } @Override public boolean updateExpirationTime(List appUserIds){ LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.in(XpAppUserCoupon::getId, appUserIds); updateWrapper.set(XpAppUserCoupon::getSendMsgTimeWx, LocalDateTime.now()); return this.update(updateWrapper); } /** * 获取用户优惠券列表 * * @param query * @param uid * @return */ @Override public XpAppUserCouponVo userCouponList(XpUserCouponListQuery query, String uid) { Page page = new Page<>(query.getPageNum(), query.getPageSize()); Page list = baseMapper.userCouponList(page, query, Long.valueOf(uid)); List records = list.getRecords(); for (XpAppUserCouponListVo record : records) { XpCoupon data = xpCouponClient.getCouponById(record.getOperationCouponId()).getData(); if(null != data){ record.setConditionAmount(data.getConditionalAmount()); } } XpAppUserCouponVo vo = new XpAppUserCouponVo(); vo.setTotal((int) page.getTotal()); vo.setList(records); return vo; } /** * 获取用户优惠券数量total * * @param uid * @return */ @Override public XpAppUserCouponTotalVo userCouponTotal(String uid) { List couponList = baseMapper.selectList(new LambdaQueryWrapper().eq(XpAppUserCoupon::getAppUserId, uid)); XpAppUserCouponTotalVo vo = new XpAppUserCouponTotalVo(); // vo.setAwaitReceiveNum((int) couponList.stream().filter(e -> e.getStatus() == 0).count()); vo.setAvailableNum((int) couponList.stream().filter(e -> e.getStatus() == 1).count()); vo.setUsedNum((int) couponList.stream().filter(e -> e.getStatus() == 2).count()); vo.setExpiredNum((int) couponList.stream().filter(e -> e.getStatus() == 3).count()); return vo; } /** * 领取优惠券 * * @param dto * @param uid * @return */ @Override public boolean receive(ReceiveCouponDto dto, String uid) { XpAppUserCoupon coupon = baseMapper.selectById(dto.getCouponId()); if (coupon.getAppUserId().toString().equals(uid)) { coupon.setStatus(1); return updateById(coupon); } return false; } /** * 时间规则 * * @param couponList * @return */ @Override public List timeRule(List couponList) { List xpAppUserCoupons = new ArrayList<>(); for (XpAppUserCoupon coupon : couponList) { String timeJson = coupon.getTimeJson(); Boolean inOperationTime = isInOperationTime(timeJson); if (inOperationTime) { xpAppUserCoupons.add(coupon); } } return xpAppUserCoupons; } @Async("couponSendExecutor") @Override public boolean saveInfo_activity(UseUserCouponDto dto) { R coupon = xpCouponClient.getCouponById(dto.getCouponId()); if (coupon != null) { XpCoupon xpCoupon = coupon.getData(); for (Long userId : dto.getUserIds()) { UcUserInfo userByMobile = userResourcesClient.getBaseUserByUid(String.valueOf(userId)); XpAppUserCoupon xpAppUserCoupon = new XpAppUserCoupon(); xpAppUserCoupon.setFullMoney(xpCoupon.getConditionalAmount()); xpAppUserCoupon.setMoney(xpCoupon.getAmount()); xpAppUserCoupon.setCouponId(dto.getCouponId()); xpAppUserCoupon.setActivityId(dto.getActivityId()); xpAppUserCoupon.setAppUserId(userId); xpAppUserCoupon.setExpirationTime(LocalDateTime.now().plusDays(7)); xpAppUserCoupon.setStatus(1); xpAppUserCoupon.setCouponType(xpCoupon.getCouponType()); xpAppUserCoupon.setPhone(userByMobile.getMobile()); xpAppUserCoupon.setCouponName(xpCoupon.getCouponName()); xpAppUserCoupon.setOpenCityId("0"); xpAppUserCoupon.setTimeJson("[{\"time\": [\"00:00-23:59\"], \"type\": 3, \"typeStr\": 0}]"); baseMapper.insert(xpAppUserCoupon); //发送微信通知 SendSubscribeMessageVo vo = new SendSubscribeMessageVo(); vo.setUserId(xpAppUserCoupon.getAppUserId()); vo.setTemplateId(MessageTemplateEnum.B.getTemplateId()); String json = MessageTemplateEnum.B.getRemarks(); json = json.replace("活动名称", dto.getActivityName()); json = json.replace("开始时间", dto.getActivityStartTime().toString() ); json = json.replace("结束时间", dto.getActivityEndTime().toString() ); json = json.replace("温馨提示", dto.getActivityRemarks()); vo.setJson(json); subscribeMessageClient.sendSubscribeMessage(vo); } } return true; } /** * 发送优惠券 * * @param dto * @return */ @Override @Async("couponSendExecutor") public R send(SendCouponDto dto) { R coupon = xpCouponClient.getCouponById(dto.getCouponId()); if (coupon != null) { XpCoupon xpCoupon = coupon.getData(); if (dto.getPhone() == null || dto.getPhone().isEmpty()) { // 全部发放 R> allWhiteList = xpWhiteListClient.getAllWhiteList(); if (allWhiteList != null) { List phoneList = allWhiteList.getData(); dto.setPhone(phoneList); } } for (String phone : dto.getPhone()) { UcUserInfo userByMobile = userResourcesClient.getUserByMobile(phone); if (userByMobile != null) { Long uid = userByMobile.getUid(); XpAppUserCoupon xpAppUserCoupon = new XpAppUserCoupon(); xpAppUserCoupon.setFullMoney(xpCoupon.getConditionalAmount()); xpAppUserCoupon.setMoney(xpCoupon.getAmount()); xpAppUserCoupon.setCouponId(dto.getCouponId()); xpAppUserCoupon.setActivityId(dto.getActivityId()); xpAppUserCoupon.setAppUserId(uid); xpAppUserCoupon.setExpirationTime(LocalDateTime.now().plusDays(7)); xpAppUserCoupon.setStatus(1); xpAppUserCoupon.setCouponType(xpCoupon.getCouponType()); xpAppUserCoupon.setPhone(phone); xpAppUserCoupon.setCouponName(xpCoupon.getCouponName()); xpAppUserCoupon.setOpenCityId("0"); xpAppUserCoupon.setTimeJson("[{\"time\": [\"00:00-23:59\"], \"type\": 3, \"typeStr\": 0}]"); baseMapper.insert(xpAppUserCoupon); sendMsg(uid, xpCoupon, xpAppUserCoupon.getExpirationTime()); } } } return null; } private void sendMsg(Long uid, XpCoupon xpCoupon, LocalDateTime expirationTime ) { //发送微信通知 SendSubscribeMessageVo vo = new SendSubscribeMessageVo(); vo.setUserId(uid); vo.setTemplateId(MessageTemplateEnum.D.getTemplateId()); String json = MessageTemplateEnum.D.getRemarks(); json = json.replace("优惠券名称", xpCoupon.getCouponName()); json = json.replace("券有效期", expirationTime.toString() ); json = json.replace("使用规则", xpCoupon.getCouponDescription()); vo.setJson(json); subscribeMessageClient.sendSubscribeMessage(vo); } @Override public Integer getUserCouponNum(String uid) { if (uid == null) { return 0; } Long num = baseMapper.selectCount(new LambdaQueryWrapper().eq(XpAppUserCoupon::getAppUserId, uid).eq(XpAppUserCoupon::getStatus, 1).lt(XpAppUserCoupon::getExpirationTime, LocalDateTime.now())); return num.intValue(); } @Override public Boolean holding(Long couponId) { return baseMapper.selectCount(new LambdaQueryWrapper().eq(XpAppUserCoupon::getCouponId, couponId).ne(XpAppUserCoupon::getStatus, 0)) > 0; } /** * 是否在运营内 * * @param timeJson 规则json字符串 * @return */ private Boolean isInOperationTime(String timeJson) { JSONArray jsonArray = JSONArray.parseArray(timeJson); Integer holidayToday = holidayUtil.isHolidayToday(); for (Object o : jsonArray) { JSONObject jsonObject = JSONObject.parseObject(o.toString()); String type = jsonObject.getString("type"); if (type.equals(holidayToday.toString())) { JSONArray time = jsonObject.getJSONArray("time"); // 判断当前时间 是否在time时间段中 for (Object object : time) { String[] timeArray = object.toString().split("-"); String startTime = timeArray[0]; String endTime = timeArray[1]; boolean currentTimeInRange = DateUtils.isCurrentTimeInRange(startTime, endTime); if (currentTimeInRange) { return true; } } } else if (type.equals("3")) { // 判断是什么类型 0每天 1-7 想起 String typeStr = jsonObject.getString("typeStr"); // 查看今天周几 Integer week = DateUtils.getWeek(); // 包含每天 或者 包含当天 if (typeStr.contains("0") || typeStr.contains(week.toString())) { JSONArray time = jsonObject.getJSONArray("time"); // 判断当前时间 是否在time时间段中 for (Object object : time) { String[] timeArray = object.toString().split("-"); String startTime = timeArray[0]; String endTime = timeArray[1]; boolean currentTimeInRange = DateUtils.isCurrentTimeInRange(startTime, endTime); if (currentTimeInRange) { return true; } } } } } return false; } @Override public NavigationCouponPickerVo navigationCouponPicker(Long userId, NavigationCouponPickerRequest req) { NavigationCouponPickerVo vo = new NavigationCouponPickerVo(); double orderAmount = req.getOrderAmount() == null ? 0d : req.getOrderAmount(); List couponList = baseMapper.selectList(new LambdaQueryWrapper() .eq(XpAppUserCoupon::getStatus, 1) .eq(XpAppUserCoupon::getAppUserId, userId) .apply("(FIND_IN_SET({0}, open_city_id) or open_city_id='0')", req.getOpenCityId())); couponList = timeRule(couponList); Long bestId = null; double bestEff = -1d; LocalDateTime bestExp = null; for (XpAppUserCoupon c : couponList) { if (!couponAmountAvailable(c, orderAmount)) { continue; } double eff = effectiveDiscountAmount(c, orderAmount); if (eff <= 0) { continue; } LocalDateTime exp = c.getExpirationTime(); if (bestId == null || eff > bestEff + 1e-6 || (Math.abs(eff - bestEff) < 1e-6 && exp != null && bestExp != null && exp.isBefore(bestExp)) || (Math.abs(eff - bestEff) < 1e-6 && bestExp == null && exp != null)) { bestId = c.getId(); bestEff = eff; bestExp = exp; } } vo.setRecommendedUserCouponId(bestId); Long selected = req.getSelectedUserCouponId(); Long applied; if (selected != null && selected == 0L) { applied = null; } else if (selected != null && selected > 0L) { XpAppUserCoupon chosen = couponList.stream().filter(c -> selected.equals(c.getId())).findFirst().orElse(null); if (chosen != null && couponAmountAvailable(chosen, orderAmount)) { applied = selected; } else { applied = null; } } else { applied = bestId; } vo.setAppliedUserCouponId(applied); long availCount = couponList.stream().filter(c -> couponAmountAvailable(c, orderAmount)).count(); if (availCount == 0) { vo.setBubbleType(NavigationCouponBubbleType.HIDDEN); List hiddenItems = buildNavigationItems(couponList, orderAmount, bestId, applied); vo.setItems(hiddenItems); vo.setListTotalCount(hiddenItems.size()); return vo; } if (applied == null) { vo.setBubbleType(NavigationCouponBubbleType.MULTI_AVAILABLE); vo.setAvailableCouponCount((int) availCount); } else { vo.setBubbleType(NavigationCouponBubbleType.SELECTED); XpAppUserCoupon ap = couponList.stream().filter(c -> applied.equals(c.getId())).findFirst().orElse(null); vo.setSelectedDiscountAmount(ap != null ? effectiveDiscountAmount(ap, orderAmount) : 0d); } List items = buildNavigationItems(couponList, orderAmount, bestId, applied); vo.setItems(items); vo.setListTotalCount(items.size()); return vo; } private List buildNavigationItems(List couponList, double orderAmount, Long bestId, Long appliedId) { List raw = new ArrayList<>(); for (XpAppUserCoupon c : couponList) { NavigationCouponItemVo it = new NavigationCouponItemVo(); it.setUserCouponId(c.getId()); it.setCouponName(c.getCouponName()); it.setCouponType(c.getCouponType()); it.setDiscountMoney(c.getMoney() != null ? c.getMoney().doubleValue() : 0d); it.setFullMoney(c.getFullMoney() != null ? c.getFullMoney().doubleValue() : null); it.setExpireTime(c.getExpirationTime()); boolean ok = couponAmountAvailable(c, orderAmount); it.setAvailable(ok); if (c.getCouponType() != null && c.getCouponType() == 2) { it.setThresholdLabel("满" + moneyStr(c.getFullMoney()) + "元可用"); if (!ok) { it.setUnavailableReason("未满足使用门槛"); } } else { it.setThresholdLabel("无门槛"); } it.setBestOffer(bestId != null && bestId.equals(c.getId())); it.setSelected(appliedId != null && appliedId.equals(c.getId())); raw.add(it); } List available = raw.stream().filter(x -> Boolean.TRUE.equals(x.getAvailable())) .sorted(Comparator .comparing((NavigationCouponItemVo x) -> !Boolean.TRUE.equals(x.getBestOffer())) .thenComparing((NavigationCouponItemVo x) -> -effectiveDiscountAmount(findCoupon(couponList, x.getUserCouponId()), orderAmount)) .thenComparing(NavigationCouponItemVo::getExpireTime, Comparator.nullsLast(Comparator.naturalOrder()))) .collect(Collectors.toList()); List unavailable = raw.stream().filter(x -> !Boolean.TRUE.equals(x.getAvailable())) .sorted(Comparator.comparing(NavigationCouponItemVo::getExpireTime, Comparator.nullsLast(Comparator.naturalOrder()))) .collect(Collectors.toList()); available.addAll(unavailable); return available; } private static XpAppUserCoupon findCoupon(List list, Long id) { return list.stream().filter(c -> Objects.equals(c.getId(), id)).findFirst().orElse(null); } private static String moneyStr(BigDecimal b) { if (b == null) { return "0"; } return b.stripTrailingZeros().toPlainString(); } private static boolean couponAmountAvailable(XpAppUserCoupon c, double orderAmount) { if (c.getCouponType() == null || c.getCouponType() == 1) { return true; } if (c.getCouponType() == 2) { return c.getFullMoney() != null && orderAmount >= c.getFullMoney().doubleValue(); } return true; } private static double effectiveDiscountAmount(XpAppUserCoupon c, double orderAmount) { if (c == null || c.getMoney() == null) { return 0d; } double m = c.getMoney().doubleValue(); if (!couponAmountAvailable(c, orderAmount)) { return 0d; } return Math.min(orderAmount, m); } /** * 获取可用优惠券 * @param orderMoney * @param uid * @return */ @Override public UserCouponListVo getUserCouponList(BigDecimal orderMoney, String uid) { List usableCouponList = new ArrayList<>(); List notUsableCouponList = new ArrayList<>(); List list = this.list(new QueryWrapper().eq("app_user_id", Long.valueOf(uid)).eq("status", 1).eq("is_deleted", 0)); for (XpAppUserCoupon xpAppUserCoupon : list) { XpCoupon data = xpCouponClient.getCouponById(xpAppUserCoupon.getCouponId()).getData(); if(null == data){ continue; } XpActicityStrategy acticityStrategy = xpActicityClient.getXpActicityStrategy(xpAppUserCoupon.getActivityId(), data.getId()).getData(); CouponVo vo = new CouponVo(); vo.setId(xpAppUserCoupon.getId()); vo.setConditionalAmount(data.getConditionalAmount()); vo.setAmount(data.getAmount()); vo.setCouponType(data.getCouponType()); vo.setCouponName(data.getCouponName()); vo.setCouponDesc(data.getCouponDescription()); vo.setEndTime(xpAppUserCoupon.getExpirationTime().toString()); vo.setUnavailable(""); vo.setStackable(""); Integer effectiveRules = acticityStrategy.getEffectiveRules(); if(1 == effectiveRules){ vo.setEffectAndInvalid(""); } if(2 == effectiveRules){ } if(3 == effectiveRules){ } if(orderMoney.compareTo(data.getConditionalAmount()) >= 0){ usableCouponList.add(vo); }else{ notUsableCouponList.add(vo); } } UserCouponListVo userCouponListVo = new UserCouponListVo(); userCouponListVo.setUsableCouponList(usableCouponList); userCouponListVo.setNotUsableCouponList(notUsableCouponList); return userCouponListVo; } }