| | |
| | | package cn.lili.modules.lmk.service.impl; |
| | | |
| | | import cn.hutool.core.bean.BeanUtil; |
| | | import cn.hutool.core.date.DateUtil; |
| | | import cn.lili.common.exception.ServiceException; |
| | | import cn.lili.common.security.AuthUser; |
| | | import cn.lili.common.security.context.UserContext; |
| | | import cn.lili.modules.lmk.domain.entity.StoreCoupon; |
| | | import cn.lili.modules.lmk.domain.entity.StoreCouponSingle; |
| | | import cn.lili.modules.lmk.enums.general.PrizeStatusEnum; |
| | | import cn.lili.modules.lmk.enums.general.StoreCouponStausEnum; |
| | | import cn.lili.modules.lmk.service.StoreCouponService; |
| | | import cn.lili.modules.lmk.service.StoreCouponSingleService; |
| | | import cn.lili.modules.order.order.entity.dto.CouponExportDetailDTO; |
| | | import cn.lili.modules.order.order.entity.dto.StoreCouponClaimRecordDTO; |
| | | import cn.lili.modules.order.order.entity.enums.ClaimStatusEnum; |
| | | import cn.lili.modules.promotion.entity.dos.MemberCoupon; |
| | | import cn.lili.modules.promotion.entity.dto.search.MemberCouponSearchParams; |
| | | import cn.lili.modules.promotion.entity.vos.MemberCouponVO; |
| | | import cn.lili.modules.promotion.service.MemberCouponService; |
| | | import cn.lili.rocketmq.RocketmqSendCallbackBuilder; |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import cn.lili.modules.lmk.domain.entity.StoreCouponClaimRecord; |
| | | import cn.lili.modules.lmk.mapper.StoreCouponClaimRecordMapper; |
| | | import cn.lili.modules.lmk.service.StoreCouponClaimRecordService; |
| | | import cn.lili.base.Result; |
| | | import com.baomidou.mybatisplus.core.toolkit.Wrappers; |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import cn.lili.modules.lmk.domain.form.StoreCouponClaimRecordForm; |
| | | import cn.lili.modules.lmk.domain.vo.StoreCouponClaimRecordVO; |
| | | import cn.lili.modules.lmk.domain.query.StoreCouponClaimRecordQuery; |
| | | import org.apache.poi.ss.usermodel.Cell; |
| | | import org.apache.poi.ss.usermodel.Row; |
| | | import org.apache.poi.ss.usermodel.Sheet; |
| | | import org.apache.poi.xssf.usermodel.XSSFWorkbook; |
| | | import org.redisson.api.RLock; |
| | | import org.redisson.api.RedissonClient; |
| | | import org.springframework.stereotype.Service; |
| | | import lombok.RequiredArgsConstructor; |
| | | import cn.lili.utils.PageUtil; |
| | | import org.springframework.beans.BeanUtils; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.transaction.support.TransactionSynchronization; |
| | | import org.springframework.transaction.support.TransactionSynchronizationManager; |
| | | import org.springframework.util.Assert; |
| | | |
| | | import javax.servlet.ServletOutputStream; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.net.URLEncoder; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Objects; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | |
| | | public class StoreCouponClaimRecordServiceImpl extends ServiceImpl<StoreCouponClaimRecordMapper, StoreCouponClaimRecord> implements StoreCouponClaimRecordService { |
| | | |
| | | private final StoreCouponClaimRecordMapper storeCouponClaimRecordMapper; |
| | | |
| | | private final RedissonClient redissonClient; |
| | | private static final String STORE_COUPON_CLAIM = "store_coupon_claim:"; |
| | | private final MemberCouponService memberCouponService; |
| | | private final StoreCouponService storeCouponService; |
| | | private final StoreCouponSingleService storeCouponSingleService; |
| | | /** |
| | | * 添加 |
| | | * @param form |
| | |
| | | .collect(Collectors.toList()); |
| | | return Result.ok().data(vos); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Result claimCoupon(String id) { |
| | | AuthUser currentUser = UserContext.getCurrentUser(); |
| | | if (currentUser == null) { |
| | | throw new ServiceException("当前用户没有登录无法领取"); |
| | | } |
| | | String userId = currentUser.getId(); |
| | | String nickName = currentUser.getNickName(); |
| | | |
| | | //锁住礼品码id |
| | | RLock redissonLock = redissonClient.getLock(STORE_COUPON_CLAIM + id); |
| | | try { |
| | | redissonLock.lock(); |
| | | LambdaQueryWrapper<StoreCouponSingle> forUpdate = Wrappers.<StoreCouponSingle>lambdaQuery() |
| | | .eq(StoreCouponSingle::getId, id).last("FOR UPDATE"); |
| | | StoreCouponSingle storeCouponSingle = storeCouponSingleService.getOne(forUpdate); |
| | | if (storeCouponSingle == null) { |
| | | throw new ServiceException("当前礼品码不存在"); |
| | | } |
| | | if (!ClaimStatusEnum.NOT_CLAIM.name().equals(storeCouponSingle.getClaimStatus())) { |
| | | throw new ServiceException("当前礼品码状态异常"); |
| | | } |
| | | LambdaQueryWrapper<StoreCouponSingle> claimListQuery = Wrappers.<StoreCouponSingle>lambdaQuery() |
| | | .eq(StoreCouponSingle::getClaimUserId, userId) |
| | | .eq(StoreCouponSingle::getStoreCoupRef, storeCouponSingle.getStoreCoupRef()) |
| | | .eq(StoreCouponSingle::getClaimStatus, ClaimStatusEnum.CLAIM.name()); |
| | | List<StoreCouponSingle> claimList = storeCouponSingleService.list(claimListQuery); |
| | | if (!claimList.isEmpty()) { |
| | | throw new ServiceException("已经领取过该类型的礼品码无法领取"); |
| | | } |
| | | //处理幂等问题限制一个用户只能该店铺领取一种优惠卷 |
| | | LambdaQueryWrapper<StoreCouponClaimRecord> memCoupon = Wrappers.<StoreCouponClaimRecord>lambdaQuery() |
| | | .eq(StoreCouponClaimRecord::getUserId, userId) |
| | | .eq(StoreCouponClaimRecord::getCouponId, storeCouponSingle.getCouponId()); |
| | | List<StoreCouponClaimRecord> list = this.list(memCoupon); |
| | | if (!list.isEmpty()){ |
| | | throw new ServiceException("当前用户已经领取过了无法再次领取"); |
| | | } |
| | | //更新单品被领取的记录 |
| | | storeCouponSingle.setClaimStatus(ClaimStatusEnum.CLAIM.name()); |
| | | storeCouponSingle.setClaimUserId(userId); |
| | | storeCouponSingle.setClaimUserName(nickName); |
| | | //校验是否在单品卷类领取过 |
| | | LambdaQueryWrapper<StoreCoupon> storeCoupQuery = Wrappers.<StoreCoupon>lambdaQuery() |
| | | .eq(StoreCoupon::getId, storeCouponSingle.getStoreCoupRef()).last("FOR UPDATE"); |
| | | StoreCoupon storeCoupon = storeCouponService.getOne(storeCoupQuery); |
| | | if (storeCoupon == null) { |
| | | throw new ServiceException("当前店铺优惠卷不存在"); |
| | | } |
| | | if (!StoreCouponStausEnum.ENABLE.name().equals(storeCoupon.getStatus())) { |
| | | throw new ServiceException("当前店铺优惠卷状态异常"); |
| | | } |
| | | //领取对应的优惠卷写入记录 |
| | | MemberCoupon memberCoupon = memberCouponService.receiveCoupon(storeCouponSingle.getCouponId(), userId, nickName); |
| | | String memberCouponId = memberCoupon.getId(); |
| | | storeCouponSingle.setMemberCouponId(memberCouponId); |
| | | storeCouponSingleService.updateById(storeCouponSingle); |
| | | StoreCouponClaimRecord storeCouponClaimRecord = getStoreCouponClaimRecord(storeCouponSingle, userId); |
| | | storeCouponClaimRecord.setMemberCouponId(memberCouponId); |
| | | this.save(storeCouponClaimRecord); |
| | | LambdaUpdateWrapper<StoreCoupon> updateStoreCoupon = Wrappers.<StoreCoupon>lambdaUpdate().eq(StoreCoupon::getId, storeCoupon.getId()) |
| | | .set(StoreCoupon::getCouponClaimNum, storeCoupon.getCouponClaimNum() + 1) |
| | | .ge(StoreCoupon::getCouponNum, storeCoupon.getCouponClaimNum() + 1); |
| | | boolean update = storeCouponService.update(updateStoreCoupon); |
| | | if (!update) { |
| | | throw new ServiceException("更新失败"); |
| | | } |
| | | //领取成功返回优惠卷id用于跳转购物使用 |
| | | return Result.ok().data(storeCouponSingle.getCouponId()); |
| | | |
| | | } finally { |
| | | TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { |
| | | @Override |
| | | public void afterCommit() { |
| | | if (redissonLock.isHeldByCurrentThread()) { |
| | | redissonLock.unlock(); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void afterCompletion(int status) { |
| | | // 确保即使在事务回滚的情况下也能释放锁 |
| | | if (redissonLock.isHeldByCurrentThread()) { |
| | | redissonLock.unlock(); |
| | | } |
| | | } |
| | | |
| | | }); |
| | | |
| | | } |
| | | } |
| | | private XSSFWorkbook initCouponExportData(List<StoreCouponClaimRecordVO> list) { |
| | | // 转换VO为DTO(如果DTO与VO字段一致,可直接使用VO简化代码) |
| | | List<StoreCouponClaimRecordDTO> dtos = new ArrayList<>(); |
| | | for (StoreCouponClaimRecordVO vo : list) { |
| | | StoreCouponClaimRecordDTO dto = new StoreCouponClaimRecordDTO(); |
| | | BeanUtil.copyProperties(vo, dto); |
| | | dtos.add(dto); |
| | | } |
| | | System.out.println("-----------------------"); |
| | | System.out.println(dtos); |
| | | XSSFWorkbook workbook = new XSSFWorkbook(); |
| | | Sheet sheet = workbook.createSheet("优惠券领取记录"); |
| | | // 创建表头 |
| | | Row header = sheet.createRow(0); |
| | | String[] headers = { |
| | | "会员名称", "优惠券名称", "发布店铺", "面额/折扣", |
| | | "获取方式", "会员优惠券状态", "优惠券类型", |
| | | "使用起始时间", "截止时间" |
| | | }; |
| | | for (int i = 0; i < headers.length; i++) { |
| | | Cell cell = header.createCell(i); |
| | | cell.setCellValue(headers[i]); |
| | | } |
| | | |
| | | // 填充数据(增加空值处理,避免NPE) |
| | | for (int i = 0; i < dtos.size(); i++) { |
| | | StoreCouponClaimRecordDTO dto = dtos.get(i); |
| | | Row row = sheet.createRow(i + 1); |
| | | |
| | | // 1. 会员名称(可能为null) |
| | | row.createCell(0).setCellValue(Objects.nonNull(dto.getMemberName()) ? dto.getMemberName() : ""); |
| | | |
| | | // 2. 优惠券名称(可能为null) |
| | | row.createCell(1).setCellValue(Objects.nonNull(dto.getCouponName()) ? dto.getCouponName() : ""); |
| | | |
| | | // 3. 发布店铺(处理platform特殊值,默认空字符串) |
| | | String storeName = dto.getStoreName(); |
| | | if ("platform".equals(storeName)) { |
| | | row.createCell(2).setCellValue("平台"); |
| | | } else { |
| | | row.createCell(2).setCellValue(Objects.nonNull(storeName) ? storeName : ""); |
| | | } |
| | | |
| | | // 4. 面额/折扣(优先显示折扣,其次显示面额,避免覆盖) |
| | | Cell amountCell = row.createCell(3); |
| | | if (Objects.nonNull(dto.getDiscount())) { |
| | | amountCell.setCellValue(dto.getDiscount() + "折"); |
| | | } else if (Objects.nonNull(dto.getPrice())) { |
| | | amountCell.setCellValue(dto.getPrice() + "元"); // 统一用"元"更规范 |
| | | } else { |
| | | amountCell.setCellValue(""); // 均为空时显示空 |
| | | } |
| | | |
| | | |
| | | // 4. 获取方式(补充默认未知状态,覆盖所有枚举值) |
| | | String getType = dto.getGetType(); |
| | | String getTypeDesc; |
| | | switch (getType) { |
| | | case "FREE": |
| | | getTypeDesc = "免费获取"; |
| | | break; |
| | | case "ACTIVITY": |
| | | getTypeDesc = "活动获取"; |
| | | break; |
| | | case "INSIDE": // 注意:原代码lime是颜色,实际枚举应为INSIDE |
| | | getTypeDesc = "内购"; |
| | | break; |
| | | default: |
| | | getTypeDesc = "未知"; |
| | | } |
| | | row.createCell(4).setCellValue(getTypeDesc); |
| | | |
| | | // 5. 会员优惠券状态(覆盖所有可能状态) |
| | | String status = dto.getMemberCouponStatus(); |
| | | String statusDesc; |
| | | switch (status) { |
| | | case "NEW": |
| | | statusDesc = "已领取"; |
| | | break; |
| | | case "USED": |
| | | statusDesc = "已使用"; |
| | | break; |
| | | case "EXPIRE": |
| | | statusDesc = "已过期"; |
| | | break; |
| | | case "CLOSED": |
| | | statusDesc = "已作废"; |
| | | break; |
| | | default: |
| | | statusDesc = "未知状态"; |
| | | } |
| | | row.createCell(5).setCellValue(statusDesc); |
| | | |
| | | // 6. 优惠券类型(补充默认处理) |
| | | String couponType = dto.getCouponType(); |
| | | String couponTypeDesc; |
| | | if ("DISCOUNT".equals(couponType)) { |
| | | couponTypeDesc = "打折"; |
| | | } else if ("PRICE".equals(couponType)) { |
| | | couponTypeDesc = "减免现金"; |
| | | } else { |
| | | couponTypeDesc = "未知类型"; |
| | | } |
| | | row.createCell(6).setCellValue(couponTypeDesc); |
| | | |
| | | |
| | | // 10. 使用起始时间(处理null,格式化时间) |
| | | Cell startTimeCell = row.createCell(7); |
| | | if (Objects.nonNull(dto.getStartTime())) { |
| | | startTimeCell.setCellValue(DateUtil.formatDateTime(dto.getStartTime())); |
| | | } else { |
| | | startTimeCell.setCellValue(""); |
| | | } |
| | | |
| | | // 11. 截止时间(同上) |
| | | Cell endTimeCell = row.createCell(8); |
| | | if (Objects.nonNull(dto.getEndTime())) { |
| | | endTimeCell.setCellValue(DateUtil.formatDateTime(dto.getEndTime())); |
| | | } else { |
| | | endTimeCell.setCellValue(""); |
| | | } |
| | | } |
| | | |
| | | return workbook; |
| | | } |
| | | |
| | | @Override |
| | | public void queryExportCoupon(HttpServletResponse response, StoreCouponClaimRecordQuery query) { |
| | | List<StoreCouponClaimRecordVO> exportData = baseMapper.getExportData(query); |
| | | XSSFWorkbook workbook = initCouponExportData(exportData); |
| | | |
| | | try { |
| | | // 设置响应头 |
| | | String fileName = URLEncoder.encode("优惠券领取记录", "UTF-8"); |
| | | response.setContentType("application/vnd.ms-excel;charset=UTF-8"); |
| | | response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx"); |
| | | |
| | | ServletOutputStream out = response.getOutputStream(); |
| | | workbook.write(out); |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } finally { |
| | | try { |
| | | workbook.close(); |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private static StoreCouponClaimRecord getStoreCouponClaimRecord(StoreCouponSingle storeCouponSingle, String userId) { |
| | | StoreCouponClaimRecord storeCouponClaimRecord = new StoreCouponClaimRecord(); |
| | | storeCouponClaimRecord.setCouponId(storeCouponSingle.getCouponId()); |
| | | storeCouponClaimRecord.setCouponName(storeCouponSingle.getCouponName()); |
| | | storeCouponClaimRecord.setStoreId(storeCouponSingle.getStoreId()); |
| | | storeCouponClaimRecord.setStoreName(storeCouponSingle.getStoreName()); |
| | | storeCouponClaimRecord.setUserId(userId); |
| | | storeCouponClaimRecord.setStoreCouponId(storeCouponSingle.getStoreCoupRef()); |
| | | return storeCouponClaimRecord; |
| | | } |
| | | } |