diff --git a/src/main/java/com/rj/controller/LbUserCouponController.java b/src/main/java/com/rj/controller/LbUserCouponController.java index 141b50c..033b5eb 100644 --- a/src/main/java/com/rj/controller/LbUserCouponController.java +++ b/src/main/java/com/rj/controller/LbUserCouponController.java @@ -6,11 +6,13 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; import lombok.Data; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.math.BigDecimal; import java.util.Map; @RestController @@ -69,13 +71,16 @@ public class LbUserCouponController { @RequestParam(required = false) String userNickname, @RequestParam(required = false) String parentPhone, @RequestParam(required = false) String parentNickname, + @RequestParam(required = false) String dataType, @Parameter(description = "优惠日期起,格式 yyyy-MM-dd") @RequestParam(required = false) String couponDateStart, @Parameter(description = "优惠日期止,格式 yyyy-MM-dd") - @RequestParam(required = false) String couponDateEnd) { + @RequestParam(required = false) String couponDateEnd, + @Parameter(description = "优惠券金额最小值,大于此值") + @RequestParam(required = false) BigDecimal couponAmountMin) { Map result = lbUserCouponService.pageQuery( current, size, tenantId, userId, userPhone, userNickname, - parentPhone, parentNickname, couponDateStart, couponDateEnd + parentPhone, parentNickname, dataType, couponDateStart, couponDateEnd, couponAmountMin ); Boolean success = (Boolean) result.get("success"); if (success != null && success) { @@ -98,4 +103,45 @@ public class LbUserCouponController { } return ResponseEntity.badRequest().body(result); } + + @PostMapping("/monthlyStatistic") + @Operation(summary = "按月统计用户优惠券金额") + public ResponseEntity> monthlyStatistic( + @Parameter(description = "租户ID", required = true) + @RequestParam String tenantId, + @Parameter(description = "优惠日期起,格式 yyyy-MM-dd") + @RequestParam(required = false) String couponDateStart, + @Parameter(description = "优惠日期止,格式 yyyy-MM-dd") + @RequestParam(required = false) String couponDateEnd) { + Map result = lbUserCouponService.monthlyStatistic(tenantId, couponDateStart, couponDateEnd); + Boolean success = (Boolean) result.get("success"); + if (success != null && success) { + return ResponseEntity.ok(result); + } + return ResponseEntity.badRequest().body(result); + } + + @GetMapping("/export") + @Operation(summary = "导出用户优惠券Excel", description = "按照分页查询的相同条件导出用户优惠券数据") + public void export( + @RequestParam(required = false) String tenantId, + @RequestParam(required = false) String userId, + @RequestParam(required = false) String userPhone, + @RequestParam(required = false) String userNickname, + @RequestParam(required = false) String parentPhone, + @RequestParam(required = false) String parentNickname, + @RequestParam(required = false) String dataType, + @Parameter(description = "优惠日期起,格式 yyyy-MM-dd") + @RequestParam(required = false) String couponDateStart, + @Parameter(description = "优惠日期止,格式 yyyy-MM-dd") + @RequestParam(required = false) String couponDateEnd, + @Parameter(description = "优惠券金额最小值,大于此值") + @RequestParam(required = false) BigDecimal couponAmountMin, + HttpServletResponse response) { + lbUserCouponService.exportExcel( + tenantId, userId, userPhone, userNickname, + parentPhone, parentNickname, dataType, + couponDateStart, couponDateEnd, couponAmountMin, response + ); + } } diff --git a/src/main/java/com/rj/entity/LbUserCoupon.java b/src/main/java/com/rj/entity/LbUserCoupon.java index 4775b87..b962e5c 100644 --- a/src/main/java/com/rj/entity/LbUserCoupon.java +++ b/src/main/java/com/rj/entity/LbUserCoupon.java @@ -56,6 +56,10 @@ public class LbUserCoupon implements Serializable { @Schema(description = "优惠日期") private LocalDate couponDate; + @TableField("data_type") + @Schema(description = "数据类型") + private String dataType; + @TableField("create_time") @Schema(description = "创建日期") private LocalDateTime createTime; diff --git a/src/main/java/com/rj/mapper/LbUserCouponMapper.java b/src/main/java/com/rj/mapper/LbUserCouponMapper.java index be3cbfe..52b7999 100644 --- a/src/main/java/com/rj/mapper/LbUserCouponMapper.java +++ b/src/main/java/com/rj/mapper/LbUserCouponMapper.java @@ -3,7 +3,23 @@ package com.rj.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.rj.entity.LbUserCoupon; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; @Mapper public interface LbUserCouponMapper extends BaseMapper { + + @Select("SELECT user_id, ANY_VALUE(user_phone) AS user_phone, ANY_VALUE(user_nickname) AS user_nickname, " + + "ANY_VALUE(parent_phone) AS parent_phone, ANY_VALUE(parent_nickname) AS parent_nickname, " + + "YEAR(coupon_date) AS year, MONTH(coupon_date) AS month, " + + "SUM(coupon_amount) AS total_amount " + + "FROM lb_user_coupon " + + "WHERE tenant_id = #{tenantId} " + + "AND coupon_date >= #{startDate} " + + "AND coupon_date <= #{endDate} " + + "GROUP BY user_id, YEAR(coupon_date), MONTH(coupon_date)") + List> selectMonthlyAggregation(String tenantId, LocalDate startDate, LocalDate endDate); } diff --git a/src/main/java/com/rj/service/ILbGoodsService.java b/src/main/java/com/rj/service/ILbGoodsService.java index 84b0b60..c9b9251 100644 --- a/src/main/java/com/rj/service/ILbGoodsService.java +++ b/src/main/java/com/rj/service/ILbGoodsService.java @@ -54,11 +54,11 @@ public interface ILbGoodsService extends IService { * @param token 失效时,按账号凭证自动登录并刷新 {@code lb_buy_account.token_front} */ default Map rushBuy(String tenantId, String token, Integer maxBuyCount) { - return rushBuy(tenantId, token, maxBuyCount, null, null, null, null); + return rushBuy(tenantId, token, maxBuyCount, null, null, null, null, null); } default Map rushBuy(String tenantId, String token, Integer maxBuyCount, String rushBuyAccountLabel) { - return rushBuy(tenantId, token, maxBuyCount, rushBuyAccountLabel, null, null, null); + return rushBuy(tenantId, token, maxBuyCount, rushBuyAccountLabel, null, null, null, null); } default Map rushBuy( @@ -67,7 +67,7 @@ public interface ILbGoodsService extends IService { Integer maxBuyCount, String rushBuyAccountLabel, LbBuyAccountRushBuyContext tokenRefreshContext) { - return rushBuy(tenantId, token, maxBuyCount, rushBuyAccountLabel, tokenRefreshContext, null, null); + return rushBuy(tenantId, token, maxBuyCount, rushBuyAccountLabel, tokenRefreshContext, null, null, null); } Map rushBuy( @@ -77,5 +77,6 @@ public interface ILbGoodsService extends IService { String rushBuyAccountLabel, LbBuyAccountRushBuyContext tokenRefreshContext, LbRushBuyGoodsCoordinator goodsCoordinator, - String accountId); + String accountId, + Integer maxGrabAmount); } diff --git a/src/main/java/com/rj/service/ILbUserCouponService.java b/src/main/java/com/rj/service/ILbUserCouponService.java index 9bc8d6e..c71196a 100644 --- a/src/main/java/com/rj/service/ILbUserCouponService.java +++ b/src/main/java/com/rj/service/ILbUserCouponService.java @@ -2,7 +2,9 @@ package com.rj.service; import com.baomidou.mybatisplus.extension.service.IService; import com.rj.entity.LbUserCoupon; +import jakarta.servlet.http.HttpServletResponse; +import java.math.BigDecimal; import java.util.Map; public interface ILbUserCouponService extends IService { @@ -21,8 +23,10 @@ public interface ILbUserCouponService extends IService { String userNickname, String parentPhone, String parentNickname, + String dataType, String couponDateStart, - String couponDateEnd); + String couponDateEnd, + BigDecimal couponAmountMin); /** * 同步用户优惠券 @@ -32,4 +36,41 @@ public interface ILbUserCouponService extends IService { * @return 同步结果 */ Map syncUserCoupons(String tenantId, Integer recentDays); + + /** + * 按月统计用户优惠券金额 + * + * @param tenantId 租户ID + * @param couponDateStart 优惠日期起 + * @param couponDateEnd 优惠日期止 + * @return 统计结果 + */ + Map monthlyStatistic(String tenantId, String couponDateStart, String couponDateEnd); + + /** + * 导出用户优惠券Excel + * + * @param tenantId 租户ID + * @param userId 用户ID + * @param userPhone 用户手机号 + * @param userNickname 用户昵称 + * @param parentPhone 用户上级手机号 + * @param parentNickname 上级昵称 + * @param dataType 数据类型 + * @param couponDateStart 优惠日期起 + * @param couponDateEnd 优惠日期止 + * @param couponAmountMin 优惠券金额最小值 + * @param response HTTP响应 + */ + void exportExcel(String tenantId, + String userId, + String userPhone, + String userNickname, + String parentPhone, + String parentNickname, + String dataType, + String couponDateStart, + String couponDateEnd, + BigDecimal couponAmountMin, + HttpServletResponse response); } diff --git a/src/main/java/com/rj/service/impl/LbBuyAccountServiceImpl.java b/src/main/java/com/rj/service/impl/LbBuyAccountServiceImpl.java index 849d7f9..7e9ff52 100644 --- a/src/main/java/com/rj/service/impl/LbBuyAccountServiceImpl.java +++ b/src/main/java/com/rj/service/impl/LbBuyAccountServiceImpl.java @@ -369,6 +369,7 @@ public class LbBuyAccountServiceImpl resultMessage = attachAccountLabel(accountLabel, formatPreRushBuyFailure("token_front不能为空")); } else { Integer maxGrabCount = account.getMaxGrabCount(); + Integer maxGrabAmount = account.getMaxGrabAmount(); if (maxGrabCount == null || maxGrabCount <= 0) { resultMessage = attachAccountLabel(accountLabel, formatPreRushBuyFailure("maxGrabCount必须大于0")); } else { @@ -387,7 +388,8 @@ public class LbBuyAccountServiceImpl accountLabel, tokenRefreshContext, goodsCoordinator, - account.getId()); + account.getId(), + maxGrabAmount); item.put("rushBuyResult", rushBuyResult); rushSuccess = rushBuyResult.get("success") instanceof Boolean ? (Boolean) rushBuyResult.get("success") diff --git a/src/main/java/com/rj/service/impl/LbGoodsServiceImpl.java b/src/main/java/com/rj/service/impl/LbGoodsServiceImpl.java index f51be04..bc89e1d 100644 --- a/src/main/java/com/rj/service/impl/LbGoodsServiceImpl.java +++ b/src/main/java/com/rj/service/impl/LbGoodsServiceImpl.java @@ -528,7 +528,8 @@ public class LbGoodsServiceImpl extends ServiceImpl impl String rushBuyAccountLabel, LbBuyAccountRushBuyContext tokenRefreshContext, LbRushBuyGoodsCoordinator goodsCoordinator, - String accountId) { + String accountId, + Integer maxGrabAmount) { Map result = new HashMap<>(); String accountTag = formatRushBuyAccountLogTag(rushBuyAccountLabel); try { @@ -577,7 +578,9 @@ public class LbGoodsServiceImpl extends ServiceImpl impl int failCount = 0; int skippedCount = 0; int attemptCount = 0; + int successTotalAmount = 0; boolean stoppedByMax = false; + boolean stoppedByMaxAmount = false; boolean stoppedByMaxAttempts = false; boolean stoppedByDailyLimit = false; boolean stoppedByLoginRequired = false; @@ -602,6 +605,10 @@ public class LbGoodsServiceImpl extends ServiceImpl impl stoppedByMax = true; break; } + if (maxGrabAmount != null && maxGrabAmount > 0 && successTotalAmount >= maxGrabAmount) { + stoppedByMaxAmount = true; + break; + } if (attemptCount >= maxAttempts) { stoppedByMaxAttempts = true; break; @@ -655,6 +662,7 @@ public class LbGoodsServiceImpl extends ServiceImpl impl if (buyResult.success()) { successCount++; + successTotalAmount += goods.getTotalMoney().intValue(); Thread.sleep(3000); } else { failCount++; @@ -726,6 +734,17 @@ public class LbGoodsServiceImpl extends ServiceImpl impl details.removeIf(d -> !Boolean.TRUE.equals(d.get("success"))); skippedCount = 0; failCount = 0; + successTotalAmount = details.stream() + .mapToInt(d -> { + Object totalMoney = d.get("totalMoney"); + if (totalMoney instanceof BigDecimal) { + return ((BigDecimal) totalMoney).intValue(); + } else if (totalMoney instanceof Number) { + return ((Number) totalMoney).intValue(); + } + return 0; + }) + .sum(); restartAfterTokenRefresh = true; if (accountTag.isEmpty()) { log.info("token 已刷新,重新抢购所有货品"); @@ -771,6 +790,10 @@ public class LbGoodsServiceImpl extends ServiceImpl impl ? HxrAdminBuyService.MSG_ACTIVITY_ENDED + ",已停止抢购;成功 " + successCount + " 笔,失败 " + failCount + " 笔" : HxrAdminBuyService.MSG_ACTIVITY_ENDED); + } else if (stoppedByMaxAmount) { + result.put("message", successCount > 0 + ? "已达到最大抢单金额,已停止抢购;成功 " + successCount + " 笔,失败 " + failCount + " 笔,成功总金额 " + successTotalAmount + : "抢购未成功"); } else { result.put("message", successCount > 0 ? "抢购完成,成功 " + successCount + " 笔,失败 " + failCount + " 笔" @@ -779,10 +802,12 @@ public class LbGoodsServiceImpl extends ServiceImpl impl result.put("tenantId", tid); result.put("successCount", successCount); result.put("failCount", failCount); + result.put("successTotalAmount", successTotalAmount); result.put("skippedCount", skippedCount); result.put("attemptCount", attemptCount); result.put("maxAttempts", maxAttempts); result.put("stoppedByMax", stoppedByMax); + result.put("stoppedByMaxAmount", stoppedByMaxAmount); result.put("stoppedByMaxAttempts", stoppedByMaxAttempts); result.put("stoppedByDailyLimit", stoppedByDailyLimit); result.put("stoppedByLoginRequired", stoppedByLoginRequired); diff --git a/src/main/java/com/rj/service/impl/LbUserCouponServiceImpl.java b/src/main/java/com/rj/service/impl/LbUserCouponServiceImpl.java index 709ebfb..ef685c7 100644 --- a/src/main/java/com/rj/service/impl/LbUserCouponServiceImpl.java +++ b/src/main/java/com/rj/service/impl/LbUserCouponServiceImpl.java @@ -42,6 +42,30 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.Set; +import java.util.stream.Collectors; + +import jakarta.servlet.http.HttpServletResponse; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFCellStyle; +import org.apache.poi.xssf.usermodel.XSSFColor; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; @Slf4j @Service @@ -328,37 +352,60 @@ public class LbUserCouponServiceImpl int savedCount = 0; LocalDateTime now = LocalDateTime.now(); + String userId = String.valueOf(user.getId()); + + // 批量查询该用户已存在的优惠券日期,避免N+1查询 + Set existingCouponDates = this.list(new LambdaQueryWrapper() + .eq(LbUserCoupon::getTenantId, tenantId) + .eq(LbUserCoupon::getUserId, userId) + .select(LbUserCoupon::getCouponDate)) + .stream() + .map(c -> c.getCouponDate() != null ? c.getCouponDate().toString() : "") + .collect(Collectors.toSet()); for (JsonNode item : listNode) { try { - LbUserCoupon coupon = new LbUserCoupon(); - coupon.setId(UUID.randomUUID().toString()); - coupon.setTenantId(tenantId); - coupon.setUserId(String.valueOf(user.getId())); - coupon.setUserPhone(user.getMobile()); - coupon.setUserNickname(user.getNickname()); - coupon.setParentPhone(user.getPmobile()); - coupon.setParentNickname(user.getPname()); - - // 从JSON中解析金额和日期 String moneyStr = item.path("money").asText("0"); - coupon.setCouponAmount(new BigDecimal(moneyStr)); + BigDecimal couponAmount = new BigDecimal(moneyStr); String createdAtStr = item.path("created_at").asText(null); + LocalDate couponDate = null; if (createdAtStr != null && !createdAtStr.isBlank()) { try { - LocalDate couponDate = LocalDate.parse(createdAtStr.substring(0, 10), DATE_FORMATTER); - coupon.setCouponDate(couponDate); + couponDate = LocalDate.parse(createdAtStr.substring(0, 10), DATE_FORMATTER); } catch (Exception e) { log.warn("日期解析失败 created_at={}", createdAtStr); } } + if (couponDate == null) { + log.warn("优惠券日期为空,跳过 item={}", item); + continue; + } + + // 内存检查是否已存在同一天同一用户的记录 + if (existingCouponDates.contains(couponDate.toString())) { + log.info("已存在同一天同一用户的优惠券记录,跳过 userId={} couponDate={}", userId, couponDate); + continue; + } + + LbUserCoupon coupon = new LbUserCoupon(); + coupon.setId(UUID.randomUUID().toString()); + coupon.setTenantId(tenantId); + coupon.setUserId(userId); + coupon.setUserPhone(user.getMobile()); + coupon.setUserNickname(user.getNickname()); + coupon.setParentPhone(user.getPmobile()); + coupon.setParentNickname(user.getPname()); + coupon.setDataType("data_detail"); + coupon.setCouponAmount(couponAmount); + coupon.setCouponDate(couponDate); coupon.setCreateTime(now); coupon.setUpdateTime(now); this.save(coupon); savedCount++; + existingCouponDates.add(couponDate.toString()); } catch (Exception e) { log.warn("保存优惠券记录异常 item={}", item, e); } @@ -419,6 +466,9 @@ public class LbUserCouponServiceImpl if (entity.getParentNickname() != null) { entity.setParentNickname(entity.getParentNickname().trim()); } + if (entity.getDataType() != null) { + entity.setDataType(entity.getDataType().trim()); + } if (entity.getCouponAmount() == null) { entity.setCouponAmount(BigDecimal.ZERO); } @@ -517,8 +567,10 @@ public class LbUserCouponServiceImpl String userNickname, String parentPhone, String parentNickname, + String dataType, String couponDateStart, - String couponDateEnd) { + String couponDateEnd, + BigDecimal couponAmountMin) { Map result = new HashMap<>(); try { if (current == null || current < 1) { @@ -547,6 +599,9 @@ public class LbUserCouponServiceImpl if (parentNickname != null && !parentNickname.trim().isEmpty()) { queryWrapper.like(LbUserCoupon::getParentNickname, parentNickname.trim()); } + if (dataType != null && !dataType.trim().isEmpty()) { + queryWrapper.eq(LbUserCoupon::getDataType, dataType.trim()); + } LocalDate start = parseDate(couponDateStart); if (couponDateStart != null && !couponDateStart.trim().isEmpty() && start == null) { @@ -566,9 +621,11 @@ public class LbUserCouponServiceImpl if (end != null) { queryWrapper.le(LbUserCoupon::getCouponDate, end); } + if (couponAmountMin != null) { + queryWrapper.gt(LbUserCoupon::getCouponAmount, couponAmountMin); + } - queryWrapper.orderByDesc(LbUserCoupon::getUpdateTime) - .orderByDesc(LbUserCoupon::getCreateTime); + queryWrapper.orderByDesc(LbUserCoupon::getCouponAmount); Page page = this.page(new Page<>(current, size), queryWrapper); result.put("success", true); @@ -586,4 +643,281 @@ public class LbUserCouponServiceImpl return result; } } + + @Override + public Map monthlyStatistic(String tenantId, String couponDateStart, String couponDateEnd) { + Map result = new HashMap<>(); + try { + if (tenantId == null || tenantId.trim().isEmpty()) { + result.put("success", false); + result.put("message", "tenantId不能为空"); + return result; + } + + LocalDate start = parseDate(couponDateStart); + if (couponDateStart != null && !couponDateStart.trim().isEmpty() && start == null) { + result.put("success", false); + result.put("message", "couponDateStart 格式错误,请使用 yyyy-MM-dd"); + return result; + } + LocalDate end = parseDate(couponDateEnd); + if (couponDateEnd != null && !couponDateEnd.trim().isEmpty() && end == null) { + result.put("success", false); + result.put("message", "couponDateEnd 格式错误,请使用 yyyy-MM-dd"); + return result; + } + + if (start == null) { + start = LocalDate.now().minusMonths(1); + } + if (end == null) { + end = LocalDate.now(); + } + + List> aggregationList = getBaseMapper().selectMonthlyAggregation(tenantId.trim(), start, end); + + if (aggregationList == null || aggregationList.isEmpty()) { + result.put("success", true); + result.put("message", "暂无数据"); + result.put("savedCount", 0); + return result; + } + + LocalDateTime now = LocalDateTime.now(); + List couponsToSave = new ArrayList<>(); + + for (Map item : aggregationList) { + LbUserCoupon coupon = new LbUserCoupon(); + coupon.setId(UUID.randomUUID().toString()); + coupon.setTenantId(tenantId.trim()); + coupon.setUserId((String) item.get("user_id")); + coupon.setUserPhone((String) item.get("user_phone")); + coupon.setUserNickname((String) item.get("user_nickname")); + coupon.setParentPhone((String) item.get("parent_phone")); + coupon.setParentNickname((String) item.get("parent_nickname")); + + int year = ((Number) item.get("year")).intValue(); + int month = ((Number) item.get("month")).intValue(); + coupon.setCouponDate(LocalDate.of(year, month, 1)); + + BigDecimal totalAmount = (BigDecimal) item.get("total_amount"); + coupon.setCouponAmount(totalAmount != null ? totalAmount : BigDecimal.ZERO); + + coupon.setDataType("data_month_static"); + coupon.setCreateTime(now); + coupon.setUpdateTime(now); + + couponsToSave.add(coupon); + } + + this.saveBatch(couponsToSave); + + result.put("success", true); + result.put("message", "统计完成"); + result.put("savedCount", couponsToSave.size()); + result.put("data", couponsToSave); + return result; + + } catch (Exception e) { + log.error("用户优惠券按月统计异常", e); + result.put("success", false); + result.put("message", "统计异常:" + e.getMessage()); + return result; + } + } + + @Override + public void exportExcel(String tenantId, + String userId, + String userPhone, + String userNickname, + String parentPhone, + String parentNickname, + String dataType, + String couponDateStart, + String couponDateEnd, + BigDecimal couponAmountMin, + HttpServletResponse response) { + try { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + if (tenantId != null && !tenantId.trim().isEmpty()) { + queryWrapper.eq(LbUserCoupon::getTenantId, tenantId.trim()); + } + if (userId != null && !userId.trim().isEmpty()) { + queryWrapper.eq(LbUserCoupon::getUserId, userId.trim()); + } + if (userPhone != null && !userPhone.trim().isEmpty()) { + queryWrapper.like(LbUserCoupon::getUserPhone, userPhone.trim()); + } + if (userNickname != null && !userNickname.trim().isEmpty()) { + queryWrapper.like(LbUserCoupon::getUserNickname, userNickname.trim()); + } + if (parentPhone != null && !parentPhone.trim().isEmpty()) { + queryWrapper.like(LbUserCoupon::getParentPhone, parentPhone.trim()); + } + if (parentNickname != null && !parentNickname.trim().isEmpty()) { + queryWrapper.like(LbUserCoupon::getParentNickname, parentNickname.trim()); + } + if (dataType != null && !dataType.trim().isEmpty()) { + queryWrapper.eq(LbUserCoupon::getDataType, dataType.trim()); + } + + LocalDate start = parseDate(couponDateStart); + if (start != null) { + queryWrapper.ge(LbUserCoupon::getCouponDate, start); + } + LocalDate end = parseDate(couponDateEnd); + if (end != null) { + queryWrapper.le(LbUserCoupon::getCouponDate, end); + } + if (couponAmountMin != null) { + queryWrapper.gt(LbUserCoupon::getCouponAmount, couponAmountMin); + } + + queryWrapper.orderByAsc(LbUserCoupon::getUserNickname) + .orderByDesc(LbUserCoupon::getCouponAmount); + + List dataList = this.list(queryWrapper); + + try (Workbook workbook = new XSSFWorkbook()) { + Sheet sheet = workbook.createSheet("用户优惠券"); + + CellStyle headerStyle = createHeaderStyle(workbook); + CellStyle dataStyle = createDataStyle(workbook); + + String[] headers = {"租户ID", "用户ID", "用户手机号", "用户昵称", "上级手机号", "上级昵称", + "优惠券金额", "优惠日期", "数据类型", "创建时间", "更新时间"}; + Row headerRow = sheet.createRow(0); + headerRow.setHeightInPoints(22); + for (int i = 0; i < headers.length; i++) { + createExportCell(headerRow, i, headers[i], headerStyle); + } + + int rowIndex = 1; + for (LbUserCoupon item : dataList) { + Row row = sheet.createRow(rowIndex); + row.setHeightInPoints(20); + createExportCell(row, 0, nullToEmpty(item.getTenantId()), dataStyle); + createExportCell(row, 1, nullToEmpty(item.getUserId()), dataStyle); + createExportCell(row, 2, nullToEmpty(item.getUserPhone()), dataStyle); + createExportCell(row, 3, nullToEmpty(item.getUserNickname()), dataStyle); + createExportCell(row, 4, nullToEmpty(item.getParentPhone()), dataStyle); + createExportCell(row, 5, nullToEmpty(item.getParentNickname()), dataStyle); + createExportCell(row, 6, formatAmount(item.getCouponAmount()), dataStyle); + createExportCell(row, 7, formatDate(item.getCouponDate()), dataStyle); + createExportCell(row, 8, nullToEmpty(item.getDataType()), dataStyle); + createExportCell(row, 9, formatDateTime(item.getCreateTime()), dataStyle); + createExportCell(row, 10, formatDateTime(item.getUpdateTime()), dataStyle); + rowIndex++; + } + + adjustExportColumnWidths(sheet, headers); + + String filename = "用户优惠券_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx"; + String encoded = URLEncoder.encode(filename, StandardCharsets.UTF_8).replaceAll("\\+", "%20"); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + encoded + "\""); + + workbook.write(response.getOutputStream()); + } + + } catch (IOException e) { + log.error("导出用户优惠券Excel异常", e); + try { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "导出异常:" + e.getMessage()); + } catch (IOException ignored) { + } + } catch (Exception e) { + log.error("导出用户优惠券Excel异常", e); + try { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "导出异常:" + e.getMessage()); + } catch (IOException ignored) { + } + } + } + + private static CellStyle createHeaderStyle(Workbook workbook) { + CellStyle headerStyle = workbook.createCellStyle(); + Font font = workbook.createFont(); + font.setBold(true); + headerStyle.setFont(font); + headerStyle.setAlignment(HorizontalAlignment.CENTER); + headerStyle.setVerticalAlignment(VerticalAlignment.CENTER); + headerStyle.setBorderTop(BorderStyle.THIN); + headerStyle.setBorderBottom(BorderStyle.THIN); + headerStyle.setBorderLeft(BorderStyle.THIN); + headerStyle.setBorderRight(BorderStyle.THIN); + headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + ((XSSFCellStyle) headerStyle).setFillForegroundColor(new XSSFColor(new java.awt.Color(255, 217, 102), null)); + return headerStyle; + } + + private static CellStyle createDataStyle(Workbook workbook) { + CellStyle dataStyle = workbook.createCellStyle(); + dataStyle.setAlignment(HorizontalAlignment.CENTER); + dataStyle.setVerticalAlignment(VerticalAlignment.CENTER); + dataStyle.setBorderTop(BorderStyle.THIN); + dataStyle.setBorderBottom(BorderStyle.THIN); + dataStyle.setBorderLeft(BorderStyle.THIN); + dataStyle.setBorderRight(BorderStyle.THIN); + return dataStyle; + } + + private static void createExportCell(Row row, int col, String text, CellStyle style) { + Cell cell = row.createCell(col); + cell.setCellValue(text == null ? "" : text); + cell.setCellStyle(style); + } + + private static String nullToEmpty(String value) { + return value == null ? "" : value; + } + + private static String formatAmount(BigDecimal amount) { + return amount == null ? "" : amount.toPlainString(); + } + + private static String formatDate(LocalDate date) { + return date == null ? "" : date.format(DATE_FORMATTER); + } + + private static String formatDateTime(LocalDateTime dateTime) { + return dateTime == null ? "" : dateTime.format(DATE_TIME_FORMATTER); + } + + private static void adjustExportColumnWidths(Sheet sheet, String[] headers) { + int columnCount = headers.length; + int[] maxWidths = new int[columnCount]; + for (int i = 0; i < columnCount; i++) { + maxWidths[i] = calcExportDisplayWidth(headers[i]); + } + for (int r = 1; r <= sheet.getLastRowNum(); r++) { + Row row = sheet.getRow(r); + if (row == null) { + continue; + } + for (int c = 0; c < columnCount; c++) { + Cell cell = row.getCell(c); + if (cell != null && cell.getCellType() == CellType.STRING) { + maxWidths[c] = Math.max(maxWidths[c], calcExportDisplayWidth(cell.getStringCellValue())); + } + } + } + for (int c = 0; c < columnCount; c++) { + int width = (maxWidths[c] + 2) * 256; + sheet.setColumnWidth(c, Math.min(width, 255 * 256)); + } + } + + private static int calcExportDisplayWidth(String text) { + if (text == null || text.isEmpty()) { + return 0; + } + int width = 0; + for (char ch : text.toCharArray()) { + width += ch > 127 ? 2 : 1; + } + return width; + } }