package com.rj.service.impl; import com.alibaba.dashscope.aigc.generation.Generation; import com.alibaba.dashscope.aigc.generation.GenerationParam; import com.alibaba.dashscope.aigc.generation.GenerationResult; import com.alibaba.dashscope.common.Message; import com.alibaba.dashscope.common.Role; import com.alibaba.dashscope.exception.InputRequiredException; import com.alibaba.dashscope.exception.NoApiKeyException; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.fasterxml.jackson.databind.ObjectMapper; import com.rj.dto.hxr.HxrAdminUserApiContext; import com.rj.dto.hxr.HxrUserRow; import com.rj.entity.LbDailyUserTradeReport; import com.rj.entity.LbOrderRow; import com.rj.entity.LbPurchaseApply; import com.rj.entity.LbUser; import com.rj.entity.MemberInfo; import com.rj.util.IsoWorkdayUtils; import com.rj.mapper.LbDailyUserTradeReportMapper; import com.rj.mapper.LbOrderRowMapper; import com.rj.mapper.LbPurchaseApplyMapper; import com.rj.mapper.LbUserMapper; import com.rj.mapper.MemberInfoMapper; import com.rj.service.HxrAdminUserService; import com.rj.service.ILbPurchaseApplyService; import com.rj.service.ILbThirdIntegrationConfigService; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.xssf.usermodel.XSSFCellStyle; import org.apache.poi.xssf.usermodel.XSSFColor; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Service; import java.awt.Color; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @Slf4j @Service public class LbPurchaseApplyServiceImpl extends ServiceImpl implements ILbPurchaseApplyService { @Autowired private LbDailyUserTradeReportMapper lbDailyUserTradeReportMapper; @Autowired private HxrAdminUserService hxrAdminUserService; @Autowired private ILbThirdIntegrationConfigService lbThirdIntegrationConfigService; @Autowired private LbUserMapper lbUserMapper; @Autowired private MemberInfoMapper memberInfoMapper; @Autowired private LbOrderRowMapper lbOrderRowMapper; @Autowired private ObjectMapper objectMapper; @Value("${langchain4j.open-ai.chat-model.model-name:qwen-plus}") private String dashScopeChatModel; private static final String ORDER_DATA_TYPE_DETAIL = "detail_data"; private static final DateTimeFormatter ORDER_DAY_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final DateTimeFormatter ORDER_DAY_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter TRY_DATE_MM_DD = DateTimeFormatter.ofPattern("MM.dd"); private static final DateTimeFormatter PRIVILEGE_RANGE_FMT = DateTimeFormatter.ofPattern("MM.dd"); private static final DateTimeFormatter EXPORT_FILENAME_TS = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); /** 试用期包含的工作日天数(周一至周五),周六日不计入。 */ private static final int TRIAL_WEEKDAY_COUNT = 3; /** 同事特权开通提醒中「特权开通日期」包含的工作日天数(开通日次日顺延至工作日起算,共 2 个工作日)。 */ private static final int COLLEAGUE_PRIVILEGE_WEEKDAY_COUNT = 2; private static final String COLLEAGUE_VIP_GROUP_LABEL = "特权提醒"; private static final String PARSE_SYSTEM_PROMPT = """ 你是信息抽取助手。用户会提供一段「进货申请」相关的自然语言订货信息。 请只输出一个 JSON 对象,不要 markdown 代码块,不要解释性文字。 字段均为可选;无法从原文推断的字段请省略或设为 null。 tryDate 试用日期:若同时给出 applyDate,服务端会按 3 个工作日规则重算并覆盖本字段;模型可省略 tryDate。 字段名与含义(JSON 键名必须完全一致): applyUser 申请人姓名; applyPhone 申请人电话; tryDate 试用日期(可省略;有 applyDate 时由服务端按 3 个工作日重算); age 年龄(整数); workExperience 从业经历; colleagueName 同事姓名; colleaguePhone 同事电话; teamLeader 团队长; teamBigLeader 双收益团队长; applyDate 申请日期,格式 yyyy-MM-dd,如果年的信息为空,请用系统的当前年替换。 不要输出 id、tenantId、createTime、updateTime。 """; @Override public Map parseFromOrderInfoByLlm(String orderInfo, String tenantId) { Map result = new HashMap<>(); try { if (orderInfo == null || orderInfo.trim().isEmpty()) { result.put("success", false); result.put("message", "订货信息不能为空"); return result; } if (tenantId == null || tenantId.trim().isEmpty()) { result.put("success", false); result.put("message", "tenantId不能为空"); return result; } String apiKey = System.getenv("DASHSCOPE_API_KEY"); if (apiKey == null || apiKey.isEmpty()) { result.put("success", false); result.put("message", "未配置环境变量 DASHSCOPE_API_KEY,无法调用大模型"); return result; } Generation gen = new Generation(); Message systemMsg = Message.builder() .role(Role.SYSTEM.getValue()) .content(PARSE_SYSTEM_PROMPT) .build(); Message userMsg = Message.builder() .role(Role.USER.getValue()) .content("订货信息如下:\n" + orderInfo.trim()) .build(); GenerationParam param = GenerationParam.builder() .apiKey(apiKey) .model(dashScopeChatModel) .messages(Arrays.asList(systemMsg, userMsg)) .resultFormat(GenerationParam.ResultFormat.MESSAGE) .build(); GenerationResult call = gen.call(param); String raw = call.getOutput().getChoices().get(0).getMessage().getContent(); if (raw == null || raw.trim().isEmpty()) { result.put("success", false); result.put("message", "大模型返回内容为空"); return result; } String json = normalizeJson(raw); LbPurchaseApply entity = objectMapper.readValue(json, LbPurchaseApply.class); log.debug( "parseFromOrderInfoByLlm:LLM JSON 解析后 applyDate={} tryDate={}", entity.getApplyDate(), entity.getTryDate()); entity.setTenantId(tenantId.trim()); entity.setId(null); entity.setCreateTime(null); entity.setUpdateTime(null); fillTryDateFromApplyDate(entity); result.put("success", true); result.put("message", "解析成功"); result.put("data", entity); return result; } catch (NoApiKeyException e) { log.error("DashScope API Key 异常", e); result.put("success", false); result.put("message", "API密钥异常:" + e.getMessage()); return result; } catch (InputRequiredException e) { log.error("DashScope 入参异常", e); result.put("success", false); result.put("message", "调用大模型入参错误:" + e.getMessage()); return result; } catch (Exception e) { log.error("订货信息大模型解析失败", e); result.put("success", false); result.put("message", "解析失败:" + e.getMessage()); return result; } } /** * 试用日期:只要有 {@code applyDate},一律按服务端规则计算(覆盖模型可能错填的 tryDate)。 * 试用期为连续 {@link #TRIAL_WEEKDAY_COUNT} 个工作日(周一至周五),周六日为无效日不计入。 * 展示起始为申请日 applyDate(日历日),结束日为从「申请日及之后首个工作日」起计的第 3 个工作日(含该首日)。 * 若申请日年份不是当年,先按 {@link LocalDate#withYear(int)} 校正为当年同月同日(闰年 2 月 29 日在平年变为 2 月 28 日)。 */ private void fillTryDateFromApplyDate(LbPurchaseApply entity) { if (entity == null) { return; } LocalDate applyDate = entity.getApplyDate(); if (applyDate == null) { log.info("试用日期:applyDate 为空,不覆盖,保留 LLM tryDate={}", entity.getTryDate()); return; } int currentYear = LocalDate.now().getYear(); if (applyDate.getYear() != currentYear) { applyDate = applyDate.withYear(currentYear); entity.setApplyDate(applyDate); } String existingTryDate = entity.getTryDate(); LocalDate trialFirstWeekday = firstWeekdayOnOrAfter(applyDate); LocalDate endDate = endOfInclusiveWeekdaySpan(trialFirstWeekday, TRIAL_WEEKDAY_COUNT, log); String computed = applyDate.format(TRY_DATE_MM_DD) + " 到 " + endDate.format(TRY_DATE_MM_DD); entity.setTryDate(computed); } /** 申请日及之后第一个周一至周五(申请日本身为工作日时即为当天)。 */ private static LocalDate firstWeekdayOnOrAfter(LocalDate d) { LocalDate x = d; while (IsoWorkdayUtils.isWeekend(x)) { x = x.plusDays(1); } return x; } /** * 从 {@code firstWeekday} 起连续 {@code inclusiveWeekdays} 个工作日(仅周一至周五,含起点当日),返回该段最后一天。 * 若起点落在周六日,会先顺延到下一工作日再计数,且返回值保证不为周六日。 */ private static LocalDate endOfInclusiveWeekdaySpan(LocalDate firstWeekday, int inclusiveWeekdays, Logger logger) { if (inclusiveWeekdays < 1) { throw new IllegalArgumentException("inclusiveWeekdays must be >= 1"); } LocalDate d = firstWeekday; while (IsoWorkdayUtils.isWeekend(d)) { if (logger != null && logger.isDebugEnabled()) { logger.debug( "trialSpan:起点为周末,顺延 cursor={} dow={} isoDow={}", d, d.getDayOfWeek(), IsoWorkdayUtils.isoDayOfWeek(d)); } d = d.plusDays(1); } int weekdaysRemaining = inclusiveWeekdays; int guard = 0; while (true) { boolean weekend = IsoWorkdayUtils.isWeekend(d); if (logger != null && logger.isDebugEnabled()) { logger.debug( "trialSpan:cursor={} dow={} isoDow={} weekend={} weekdaysRemaining={}", d, d.getDayOfWeek(), IsoWorkdayUtils.isoDayOfWeek(d), weekend, weekdaysRemaining); } if (!weekend) { weekdaysRemaining--; if (weekdaysRemaining == 0) { return d; } } d = d.plusDays(1); guard++; if (guard > 400) { throw new IllegalStateException( "trialSpan:超过 400 步仍未结束,firstWeekday=" + firstWeekday + " inclusiveWeekdays=" + inclusiveWeekdays); } } } private static String normalizeJson(String content) { if (content == null) { return ""; } String trimmed = content.trim(); if (trimmed.startsWith("```")) { int firstLineBreak = trimmed.indexOf('\n'); int lastFence = trimmed.lastIndexOf("```"); if (firstLineBreak >= 0 && lastFence > firstLineBreak) { trimmed = trimmed.substring(firstLineBreak + 1, lastFence).trim(); } } return trimmed; } @Override public Map add(LbPurchaseApply entity) { Map result = new HashMap<>(); try { if (entity.getTenantId() == null || entity.getTenantId().trim().isEmpty()) { result.put("success", false); result.put("message", "tenantId不能为空"); return result; } if (entity.getApplyUser() == null || entity.getApplyUser().trim().isEmpty()) { result.put("success", false); result.put("message", "applyUser不能为空"); return result; } if (entity.getApplyPhone() == null || entity.getApplyPhone().trim().isEmpty()) { result.put("success", false); result.put("message", "applyPhone不能为空"); return result; } if (entity.getApplyDate() == null) { result.put("success", false); result.put("message", "applyDate不能为空"); return result; } String tenantId = entity.getTenantId().trim(); String phone = entity.getApplyPhone().trim(); entity.setTenantId(tenantId); entity.setApplyPhone(phone); LocalDateTime now = LocalDateTime.now(); LambdaQueryWrapper phoneQuery = new LambdaQueryWrapper<>(); phoneQuery.eq(LbPurchaseApply::getTenantId, tenantId) .eq(LbPurchaseApply::getApplyPhone, phone) .orderByDesc(LbPurchaseApply::getId) .last("LIMIT 1"); LbPurchaseApply existing = this.getOne(phoneQuery); boolean ok; if (existing != null) { entity.setId(existing.getId()); entity.setCreateTime(existing.getCreateTime()); entity.setUpdateTime(now); ok = this.updateById(entity); result.put("success", ok); result.put("message", ok ? "按申请人电话已更新" : "更新失败"); } else { entity.setId(null); if (entity.getCreateTime() == null) { entity.setCreateTime(now); } entity.setUpdateTime(now); ok = this.save(entity); result.put("success", ok); result.put("message", ok ? "新增成功" : "新增失败"); } if (ok) { result.put("data", this.getById(entity.getId())); } return result; } catch (Exception e) { result.put("success", false); result.put("message", "新增异常:" + e.getMessage()); return result; } } @Override public Map update(LbPurchaseApply entity) { Map result = new HashMap<>(); try { if (entity.getId() == null) { result.put("success", false); result.put("message", "id不能为空"); return result; } entity.setUpdateTime(LocalDateTime.now()); boolean ok = this.updateById(entity); result.put("success", ok); result.put("message", ok ? "编辑成功" : "编辑失败"); if (ok) { result.put("data", this.getById(entity.getId())); } return result; } catch (Exception e) { result.put("success", false); result.put("message", "编辑异常:" + e.getMessage()); return result; } } @Override public Map deleteById(Long id) { Map result = new HashMap<>(); try { boolean ok = this.removeById(id); result.put("success", ok); result.put("message", ok ? "删除成功" : "删除失败"); return result; } catch (Exception e) { result.put("success", false); result.put("message", "删除异常:" + e.getMessage()); return result; } } @Override public Map pageQuery(Integer current, Integer size, String tenantId, String applyUser, String applyPhone, String colleagueName, String teamLeader, String teamBigLeader, LocalDate applyDate) { Map result = new HashMap<>(); try { if (current == null || current < 1) { current = 1; } if (size == null || size < 1) { size = 10; } LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); if (tenantId != null && !tenantId.trim().isEmpty()) { queryWrapper.eq(LbPurchaseApply::getTenantId, tenantId.trim()); } if (applyUser != null && !applyUser.trim().isEmpty()) { queryWrapper.like(LbPurchaseApply::getApplyUser, applyUser.trim()); } if (applyPhone != null && !applyPhone.trim().isEmpty()) { queryWrapper.like(LbPurchaseApply::getApplyPhone, applyPhone.trim()); } if (colleagueName != null && !colleagueName.trim().isEmpty()) { queryWrapper.like(LbPurchaseApply::getColleagueName, colleagueName.trim()); } if (teamLeader != null && !teamLeader.trim().isEmpty()) { queryWrapper.like(LbPurchaseApply::getTeamLeader, teamLeader.trim()); } if (teamBigLeader != null && !teamBigLeader.trim().isEmpty()) { queryWrapper.like(LbPurchaseApply::getTeamBigLeader, teamBigLeader.trim()); } if (applyDate != null) { queryWrapper.eq(LbPurchaseApply::getApplyDate, applyDate); } queryWrapper.orderByDesc(LbPurchaseApply::getUpdateTime) ; Page page = this.page(new Page<>(current, size), queryWrapper); fillLinkedLbUsers(page.getRecords()); fillLatestTradeOrders(page.getRecords()); result.put("success", true); result.put("message", "查询成功"); result.put("data", page.getRecords()); result.put("total", page.getTotal()); result.put("current", page.getCurrent()); result.put("size", page.getSize()); result.put("pages", page.getPages()); return result; } catch (Exception e) { result.put("success", false); result.put("message", "查询异常:" + e.getMessage()); return result; } } /** * 按租户 + 手机号关联 {@link LbUser}:申请人 {@code applyPhone}、推荐人 {@code colleaguePhone} 分别对应 * {@link LbPurchaseApply#}、{@link LbPurchaseApply#}(仅返回脱敏后的用户字段)。 */ private void fillLinkedLbUsers(List records) { if (records == null || records.isEmpty()) { return; } Set mobiles = new HashSet<>(); for (LbPurchaseApply record : records) { String applyPhone = trimToNull(record.getApplyPhone()); if (applyPhone != null) { mobiles.add(applyPhone); } String colleaguePhone = trimToNull(record.getColleaguePhone()); if (colleaguePhone != null) { mobiles.add(colleaguePhone); } } if (mobiles.isEmpty()) { return; } LambdaQueryWrapper userQuery = new LambdaQueryWrapper<>(); userQuery.in(LbUser::getMobile, mobiles); List lbUsers = lbUserMapper.selectList(userQuery); Map userByTenantMobile = new HashMap<>(); for (LbUser user : lbUsers) { String mobile = trimToNull(user.getMobile()); if (mobile == null) { continue; } userByTenantMobile.put(buildTenantMobileKey(user.getTenantId(), mobile), toPublicUserView(user)); } for (LbPurchaseApply record : records) { String applyPhone = trimToNull(record.getApplyPhone()); if (applyPhone != null) { record.setApplicantUser(userByTenantMobile.get(buildTenantMobileKey(record.getTenantId(), applyPhone))); } String colleaguePhone = trimToNull(record.getColleaguePhone()); if (colleaguePhone != null) { record.setReferrerUser(userByTenantMobile.get(buildTenantMobileKey(record.getTenantId(), colleaguePhone))); } } } /** * 按租户 + 申请人手机号关联 {@link LbOrderRow}:{@code apply_phone} 匹配 {@code buyer_phone}, * 取 pay_time 最近的一条已成交明细订单。 */ private void fillLatestTradeOrders(List records) { if (records == null || records.isEmpty()) { return; } Set buyerPhones = new HashSet<>(); Set tenantIds = new HashSet<>(); for (LbPurchaseApply record : records) { String applyPhone = trimToNull(record.getApplyPhone()); if (applyPhone != null) { buyerPhones.add(applyPhone); } String tenantId = trimToNull(record.getTenantId()); if (tenantId != null) { tenantIds.add(tenantId); } } if (buyerPhones.isEmpty()) { return; } LambdaQueryWrapper orderQuery = new LambdaQueryWrapper<>(); orderQuery.eq(LbOrderRow::getDataType, ORDER_DATA_TYPE_DETAIL) .in(LbOrderRow::getBuyerPhone, buyerPhones) .isNotNull(LbOrderRow::getPayTime); if (tenantIds.size() == 1) { orderQuery.eq(LbOrderRow::getTenantId, tenantIds.iterator().next()); } else if (tenantIds.size() > 1) { orderQuery.in(LbOrderRow::getTenantId, tenantIds); } List orderRows = lbOrderRowMapper.selectList(orderQuery); if (orderRows == null || orderRows.isEmpty()) { return; } Map latestByTenantPhone = new HashMap<>(); for (LbOrderRow row : orderRows) { if (!isPaidOrderRow(row)) { continue; } String buyerPhone = trimToNull(row.getBuyerPhone()); if (buyerPhone == null) { continue; } String key = buildTenantMobileKey(row.getTenantId(), buyerPhone); LbOrderRow prev = latestByTenantPhone.get(key); if (prev == null || isOrderPayTimeNewer(row, prev)) { latestByTenantPhone.put(key, row); } } for (LbPurchaseApply record : records) { String applyPhone = trimToNull(record.getApplyPhone()); if (applyPhone == null) { continue; } LbOrderRow latest = latestByTenantPhone.get(buildTenantMobileKey(record.getTenantId(), applyPhone)); if (latest != null) { record.setLatestTradeOrder(latest); }else { record.setLatestTradeOrder(new LbOrderRow()); } } } private static boolean isPaidOrderRow(LbOrderRow row) { String payTime = row.getPayTime(); return payTime != null && !payTime.trim().isEmpty(); } private static boolean isOrderPayTimeNewer(LbOrderRow order, LbOrderRow prev) { LocalDateTime payTime = parseOrderTime(payTimeOrBuyTime(order)); LocalDateTime prevPayTime = parseOrderTime(payTimeOrBuyTime(prev)); if (payTime == null) { return false; } if (prevPayTime == null) { return true; } int cmp = payTime.compareTo(prevPayTime); if (cmp != 0) { return cmp > 0; } Long id = order.getId(); Long prevId = prev.getId(); if (id == null) { return false; } if (prevId == null) { return true; } return id > prevId; } private static String payTimeOrBuyTime(LbOrderRow row) { if (row.getPayTime() != null && !row.getPayTime().trim().isEmpty()) { return row.getPayTime().trim(); } return row.getBuyTime() == null ? null : row.getBuyTime().trim(); } private static LocalDateTime parseOrderTime(String timeText) { if (timeText == null || timeText.isEmpty()) { return null; } try { return LocalDateTime.parse(timeText, ORDER_DAY_TIME_FMT); } catch (DateTimeParseException ignored) { // continue } try { return LocalDate.parse(timeText.substring(0, Math.min(10, timeText.length())), ORDER_DAY_FMT) .atStartOfDay(); } catch (DateTimeParseException ignored) { // continue } try { return LocalDateTime.parse(timeText, DateTimeFormatter.ISO_LOCAL_DATE_TIME); } catch (DateTimeParseException e) { return null; } } /** * 分页关联用户时仅暴露业务所需字段,其余置空或 0,避免泄露手机号、密码等隐私。 */ private static LbUser toPublicUserView(LbUser source) { if (source == null) { return null; } LbUser view = new LbUser(); view.setId(source.getId()); view.setViptime(source.getViptime()); view.setIsVip(source.getIsVip()); view.setTodayBuyTotal(source.getTodayBuyTotal()); view.setTodaySellTotal(source.getTodaySellTotal()); view.setJoinTime(source.getJoinTime()); view.setMaxOrder(source.getMaxOrder()); return view; } private static String buildTenantMobileKey(String tenantId, String mobile) { String tenantPart = tenantId == null ? "" : tenantId.trim(); String mobilePart = mobile == null ? "" : mobile.trim(); return tenantPart + "#" + mobilePart; } private static String trimToNull(String value) { if (value == null) { return null; } String trimmed = value.trim(); return trimmed.isEmpty() ? null : trimmed; } @Override public Map listNeedPrivilegeRecommenders(LocalDate startDate, LocalDate endDate) { Map result = new HashMap<>(); try { if (startDate == null || endDate == null) { result.put("success", false); result.put("message", "开始日期和结束日期不能为空"); return result; } if (endDate.isBefore(startDate)) { result.put("success", false); result.put("message", "结束日期不能早于开始日期"); return result; } LambdaQueryWrapper purchaseQuery = new LambdaQueryWrapper<>(); purchaseQuery.ge(LbPurchaseApply::getApplyDate, startDate) .le(LbPurchaseApply::getApplyDate, endDate) .orderByDesc(LbPurchaseApply::getApplyDate) .orderByDesc(LbPurchaseApply::getApplyDate) ; List purchaseInfo = this.list(purchaseQuery); if (purchaseInfo == null || purchaseInfo.isEmpty()) { result.put("success", true); result.put("message", "查询成功"); result.put("data", List.of()); result.put("total", 0); return result; } Set applyUsers = new HashSet<>(); for (LbPurchaseApply item : purchaseInfo) { if (item.getApplyUser() != null && !item.getApplyUser().trim().isEmpty()) { applyUsers.add(item.getApplyUser().trim()); } } if (applyUsers.isEmpty()) { result.put("success", true); result.put("message", "查询成功"); result.put("data", List.of()); result.put("total", 0); return result; } LambdaQueryWrapper reportQuery = new LambdaQueryWrapper<>(); reportQuery.in(LbDailyUserTradeReport::getNickname, applyUsers) .select(LbDailyUserTradeReport::getNickname); List matchedReports = lbDailyUserTradeReportMapper.selectList(reportQuery); Set matchedNicknames = new HashSet<>(); for (LbDailyUserTradeReport report : matchedReports) { if (report.getNickname() != null && !report.getNickname().trim().isEmpty()) { matchedNicknames.add(report.getNickname().trim()); } } List needPrivilegeList = new ArrayList<>(); for (LbPurchaseApply item : purchaseInfo) { String applyUser = item.getApplyUser() == null ? null : item.getApplyUser().trim(); if (applyUser != null && !applyUser.isEmpty() && matchedNicknames.contains(applyUser)) { needPrivilegeList.add(item); } } result.put("success", true); result.put("message", "查询成功"); result.put("data", needPrivilegeList); result.put("total", needPrivilegeList.size()); result.put("startDate", startDate); result.put("endDate", endDate); return result; } catch (Exception e) { result.put("success", false); result.put("message", "查询异常:" + e.getMessage()); return result; } } @Override public Map syncHxrAdminUserVipByApplyDateRange( LocalDate applyDateStart, LocalDate applyDateEnd, String tenantId, Integer maxOrder) { Map result = new HashMap<>(); try { if (applyDateStart == null || applyDateEnd == null) { result.put("success", false); result.put("message", "申请开始日期与结束日期不能为空"); return result; } if (applyDateEnd.isBefore(applyDateStart)) { result.put("success", false); result.put("message", "结束日期不能早于开始日期"); return result; } if (tenantId == null || tenantId.trim().isEmpty()) { result.put("success", false); result.put("message", "租户id不能为空"); return result; } if (maxOrder == null) { result.put("success", false); result.put("message", "max_order不能为空"); return result; } LambdaQueryWrapper q = new LambdaQueryWrapper<>(); q.eq(LbPurchaseApply::getTenantId, tenantId.trim()) .ge(LbPurchaseApply::getApplyDate, applyDateStart) .le(LbPurchaseApply::getApplyDate, applyDateEnd); List rows = this.list(q); if (rows == null || rows.isEmpty()) { result.put("success", true); result.put("message", "该条件下无申请记录,未调用第三方"); result.put("totalRecords", 0); result.put("details", List.of()); result.put("applyDateStart", applyDateStart); result.put("applyDateEnd", applyDateEnd); result.put("tenantId", tenantId.trim()); return result; } List> details = new ArrayList<>(); int ok = 0; int fail = 0; for (LbPurchaseApply row : rows) { Map one = new HashMap<>(); one.put("applyId", row.getId()); one.put("applyDate", row.getApplyDate()); String phone = row.getApplyPhone() != null ? row.getApplyPhone().trim() : ""; one.put("applyPhone", phone); if (phone.isEmpty()) { one.put("success", false); one.put("message", "apply_phone 为空,跳过"); fail++; details.add(one); continue; } if (row.getApplyDate() == null) { one.put("success", false); one.put("message", "applyDate 为空,跳过"); fail++; details.add(one); continue; } try { LocalDateTime vipTime = HxrAdminUserService.computeVipExpireTime(row.getApplyDate(), 2); String vipTimeStr = HxrAdminUserService.formatVipTime(vipTime); one.put("viptime", vipTimeStr); Optional userOpt = hxrAdminUserService.fetchFirstUserByMobile(phone); if (userOpt.isEmpty()) { one.put("success", false); one.put("message", "user/select 无用户或接口失败"); fail++; details.add(one); continue; } long hxrUserId = userOpt.get().id(); one.put("hxrUserId", hxrUserId); boolean updated = hxrAdminUserService.updateUserVipFields(hxrUserId, maxOrder, vipTimeStr); if (updated) { one.put("success", true); one.put("message", "user/update 成功"); ok++; } else { one.put("success", false); one.put("message", "user/update 失败"); fail++; } } catch (Exception ex) { one.put("success", false); one.put("message", "调用异常:" + ex.getMessage()); fail++; } details.add(one); } result.put("success", fail == 0); result.put("message", fail == 0 ? "全部处理成功" : "部分或全部失败,见 details"); result.put("totalRecords", rows.size()); result.put("successCount", ok); result.put("failCount", fail); result.put("maxOrder", maxOrder); result.put("details", details); result.put("applyDateStart", applyDateStart); result.put("applyDateEnd", applyDateEnd); result.put("tenantId", tenantId.trim()); return result; } catch (Exception e) { result.put("success", false); result.put("message", "处理异常:" + e.getMessage()); return result; } } @Override public Map updateHxrAdminUserVipByUserIds( String tenantId, List userIds, Integer isVip, String vipTime, Integer maxOrder) { Map result = new HashMap<>(); try { if (tenantId == null || tenantId.trim().isEmpty()) { result.put("success", false); result.put("message", "租户id不能为空"); return result; } if (userIds == null || userIds.isEmpty()) { result.put("success", false); result.put("message", "用户id列表不能为空"); return result; } if (isVip == null || (isVip != 0 && isVip != 1)) { result.put("success", false); result.put("message", "isVip 必须为 0 或 1"); return result; } if (maxOrder == null) { result.put("success", false); result.put("message", "maxOrder不能为空"); return result; } String vipTimeStr = vipTime != null ? vipTime.trim() : ""; if (isVip == 1 && vipTimeStr.isEmpty()) { result.put("success", false); result.put("message", "开通特权时 vipTime 不能为空"); return result; } String tid = tenantId.trim(); Optional apiContextOpt = lbThirdIntegrationConfigService.resolveAdminUserApiContext(tid); if (apiContextOpt.isEmpty()) { result.put("success", false); result.put("message", "未找到该租户的第三方集成配置,或配置未启用、URL/凭证不完整(请检查 lb_third_integration_config)"); result.put("totalRecords", userIds.size()); result.put("successCount", 0); result.put("failCount", userIds.size()); result.put("details", List.of()); result.put("tenantId", tid); return result; } HxrAdminUserApiContext apiContext = apiContextOpt.get(); List> details = new ArrayList<>(); int ok = 0; int fail = 0; for (Long userId : userIds) { Map one = new HashMap<>(); one.put("userId", userId); one.put("isVip", isVip); one.put("viptime", vipTimeStr); one.put("maxOrder", maxOrder); if (userId == null) { one.put("success", false); one.put("message", "用户id为空,跳过"); fail++; details.add(one); continue; } try { Integer maxOrderForApi = maxOrder == 0 ? null : maxOrder; boolean updated = hxrAdminUserService.updateUserVipFields( userId, maxOrderForApi, vipTimeStr, isVip, apiContext); if (updated) { one.put("success", true); one.put("message", "user/update 成功"); ok++; } else { one.put("success", false); one.put("message", "user/update 失败"); fail++; } } catch (Exception ex) { one.put("success", false); one.put("message", "调用异常:" + ex.getMessage()); fail++; } details.add(one); } result.put("success", fail == 0); result.put("message", fail == 0 ? "全部处理成功" : "部分或全部失败,见 details"); result.put("totalRecords", userIds.size()); result.put("successCount", ok); result.put("failCount", fail); result.put("isVip", isVip); result.put("vipTime", vipTimeStr); result.put("maxOrder", maxOrder); result.put("details", details); result.put("tenantId", tenantId.trim()); return result; } catch (Exception e) { result.put("success", false); result.put("message", "处理异常:" + e.getMessage()); return result; } } @Override public Map updateHxrAdminUserResellStatusByUserIds( String tenantId, List userIds, Boolean isResellEnabled, Integer isResell, Boolean statusEnabled, Integer status) { Map result = new HashMap<>(); try { if (tenantId == null || tenantId.trim().isEmpty()) { result.put("success", false); result.put("message", "租户id不能为空"); return result; } if (userIds == null || userIds.isEmpty()) { result.put("success", false); result.put("message", "用户id列表不能为空"); return result; } String validationError = validateResellStatusParams(isResellEnabled, isResell, statusEnabled, status); if (validationError != null) { result.put("success", false); result.put("message", validationError); return result; } String tid = tenantId.trim(); Optional apiContextOpt = lbThirdIntegrationConfigService.resolveAdminUserApiContext(tid); if (apiContextOpt.isEmpty()) { return buildApiContextMissingResult(tid, userIds.size()); } List ids = new ArrayList<>(userIds.size()); for (Long userId : userIds) { ids.add(userId == null ? null : String.valueOf(userId)); } return doUpdateHxrAdminUserResellStatus(tid, ids, apiContextOpt.get(), Boolean.TRUE.equals(isResellEnabled), isResell, Boolean.TRUE.equals(statusEnabled), status); } catch (Exception e) { result.put("success", false); result.put("message", "处理异常:" + e.getMessage()); return result; } } @Override public Map updateHxrAdminUserResellStatusForAllMembers( String tenantId, Boolean isResellEnabled, Integer isResell, Boolean statusEnabled, Integer status) { Map result = new HashMap<>(); try { if (tenantId == null || tenantId.trim().isEmpty()) { result.put("success", false); result.put("message", "租户id不能为空"); return result; } String validationError = validateResellStatusParams(isResellEnabled, isResell, statusEnabled, status); if (validationError != null) { result.put("success", false); result.put("message", validationError); return result; } // 读取 member_info 全部用户(该表为第三方库镜像,无 tenant_id,已在租户拦截器忽略名单中) // 过滤掉昵称中包含 "**" 的记录(脱敏/无效数据) LambdaQueryWrapper memberQuery = new LambdaQueryWrapper<>(); // memberQuery.and(w -> w.isNull(MemberInfo::getNickname) // .or().notLike(MemberInfo::getNickname, "**")); List members = memberInfoMapper.selectList(memberQuery); if (members == null || members.isEmpty()) { result.put("success", false); result.put("message", "member_info 表无用户数据"); return result; } List userIds = new ArrayList<>(members.size()); for (MemberInfo member : members) { userIds.add(member == null ? null : member.getId()); } String tid = tenantId.trim(); Optional apiContextOpt = lbThirdIntegrationConfigService.resolveAdminUserApiContext(tid); if (apiContextOpt.isEmpty()) { return buildApiContextMissingResult(tid, userIds.size()); } return doUpdateHxrAdminUserResellStatus(tid, userIds, apiContextOpt.get(), Boolean.TRUE.equals(isResellEnabled), isResell, Boolean.TRUE.equals(statusEnabled), status); } catch (Exception e) { result.put("success", false); result.put("message", "处理异常:" + e.getMessage()); return result; } } /** 校验寄卖/状态参数:至少一项生效,生效字段的值必须为 0 或 1;返回 null 表示校验通过。 */ private static String validateResellStatusParams( Boolean isResellEnabled, Integer isResell, Boolean statusEnabled, Integer status) { boolean resellEffect = Boolean.TRUE.equals(isResellEnabled); boolean statusEffect = Boolean.TRUE.equals(statusEnabled); if (!resellEffect && !statusEffect) { return "isResellEnabled 与 statusEnabled 至少一项需为 true"; } if (resellEffect && (isResell == null || (isResell != 0 && isResell != 1))) { return "isResell 生效时其值必须为 0 或 1"; } if (statusEffect && (status == null || (status != 0 && status != 1))) { return "status 生效时其值必须为 0 或 1"; } return null; } /** 组装「未找到租户第三方集成配置」的失败结果。 */ private static Map buildApiContextMissingResult(String tid, int total) { Map result = new HashMap<>(); result.put("success", false); result.put("message", "未找到该租户的第三方集成配置,或配置未启用、URL/凭证不完整(请检查 lb_third_integration_config)"); result.put("totalRecords", total); result.put("successCount", 0); result.put("failCount", total); result.put("details", List.of()); result.put("tenantId", tid); return result; } /** * 逐个用户调用 hxrd user/update(v1 后台接口)修改 is_resell/status; * 未生效字段传 null,第三方提交时不携带、保持原值。 */ private Map doUpdateHxrAdminUserResellStatus( String tid, List userIds, HxrAdminUserApiContext apiContext, boolean resellEffect, Integer isResell, boolean statusEffect, Integer status) { Map result = new HashMap<>(); Integer isResellForApi = resellEffect ? isResell : null; Integer statusForApi = statusEffect ? status : null; List> details = new ArrayList<>(); int ok = 0; int fail = 0; for (String userId : userIds) { Map one = new HashMap<>(); one.put("userId", userId); if (resellEffect) { one.put("isResell", isResellForApi); } if (statusEffect) { one.put("status", statusForApi); } if (userId == null || userId.trim().isEmpty()) { one.put("success", false); one.put("message", "用户id为空,跳过"); fail++; details.add(one); continue; } try { boolean updated = hxrAdminUserService.updateUserResellStatusFields( userId, isResellForApi, statusForApi, apiContext); if (updated) { one.put("success", true); one.put("message", "user/update 成功"); ok++; } else { one.put("success", false); one.put("message", "user/update 失败"); fail++; } } catch (Exception ex) { one.put("success", false); one.put("message", "调用异常:" + ex.getMessage()); fail++; } details.add(one); } result.put("success", fail == 0); result.put("message", fail == 0 ? "全部处理成功" : "部分或全部失败,见 details"); result.put("totalRecords", userIds.size()); result.put("successCount", ok); result.put("failCount", fail); result.put("isResellEnabled", resellEffect); if (resellEffect) { result.put("isResell", isResellForApi); } result.put("statusEnabled", statusEffect); if (statusEffect) { result.put("status", statusForApi); } result.put("details", details); result.put("tenantId", tid); return result; } @Override public Map syncHxrAdminColleagueVipByApplyDateRange( LocalDate applyDateStart, LocalDate applyDateEnd, String tenantId) { Map result = new HashMap<>(); try { ColleagueVipContext ctx = resolveColleagueVipContext(applyDateStart, applyDateEnd, tenantId); if (ctx.validationError() != null) { result.put("success", false); result.put("message", ctx.validationError()); return result; } List rows = ctx.rows(); if (rows.isEmpty()) { result.put("success", true); result.put("message", "该条件下无申请记录,未调用第三方"); result.put("totalRecords", 0); result.put("details", List.of()); result.put("applyDateStart", applyDateStart); result.put("applyDateEnd", applyDateEnd); result.put("tenantId", tenantId.trim()); return result; } String tid = tenantId.trim(); Optional apiContextOpt = lbThirdIntegrationConfigService.resolveAdminUserApiContext(tid); if (apiContextOpt.isEmpty()) { result.put("success", false); result.put("message", "未找到该租户的第三方集成配置,或配置未启用、URL/凭证不完整(请检查 lb_third_integration_config)"); result.put("totalRecords", rows.size()); result.put("successCount", 0); result.put("failCount", rows.size()); result.put("details", List.of()); result.put("applyDateStart", applyDateStart); result.put("applyDateEnd", applyDateEnd); result.put("tenantId", tid); return result; } HxrAdminUserApiContext apiContext = apiContextOpt.get(); Set tradeMatchedApplyPhones = ctx.tradeMatchedApplyPhones(); List> details = new ArrayList<>(); int ok = 0; int fail = 0; for (LbPurchaseApply row : rows) { Map one = new HashMap<>(); one.put("applyId", row.getId()); one.put("applyDate", row.getApplyDate()); String applyUserName = row.getApplyUser() != null ? row.getApplyUser().trim() : ""; one.put("applyUserName", applyUserName); String phone = row.getColleaguePhone() != null ? row.getColleaguePhone().trim() : ""; one.put("colleaguePhone", phone); String skipReason = colleagueVipSkipReason(row, tradeMatchedApplyPhones); if (skipReason != null) { one.put("success", false); one.put("message", skipReason); fail++; details.add(one); continue; } try { LocalDateTime vipTime = HxrAdminUserService.computeVipExpireTime(row.getApplyDate()); String vipTimeStr = HxrAdminUserService.formatVipTime(vipTime); one.put("viptime", vipTimeStr); Optional userOpt = hxrAdminUserService.fetchFirstUserByMobile(phone, apiContext); if (userOpt.isEmpty()) { one.put("success", false); one.put("message", "user/select 无用户或接口失败"); fail++; details.add(one); continue; } HxrUserRow hxrUser = userOpt.get(); long hxrUserId = hxrUser.id(); one.put("hxrUserId", hxrUserId); Integer maxOrder = hxrUser.maxOrder(); if (maxOrder == null) { one.put("success", false); one.put("message", "user/select 未返回 max_order,跳过"); fail++; details.add(one); continue; } one.put("maxOrder", maxOrder); boolean updated = hxrAdminUserService.updateUserVipFields(hxrUserId, maxOrder, vipTimeStr, 1, apiContext); if (updated) { one.put("success", true); one.put("message", "user/update 成功"); ok++; } else { one.put("success", false); one.put("message", "user/update 失败"); fail++; } } catch (Exception ex) { one.put("success", false); one.put("message", "调用异常:" + ex.getMessage()); fail++; } details.add(one); } result.put("success", fail == 0); result.put("message", fail == 0 ? "全部处理成功" : "部分或全部失败,见 details"); result.put("totalRecords", rows.size()); result.put("successCount", ok); result.put("failCount", fail); result.put("details", details); result.put("applyDateStart", applyDateStart); result.put("applyDateEnd", applyDateEnd); result.put("tenantId", tenantId.trim()); return result; } catch (Exception e) { result.put("success", false); result.put("message", "处理异常:" + e.getMessage()); return result; } } @Override public void exportColleagueVipExcel( LocalDate applyDateStart, LocalDate applyDateEnd, String tenantId, HttpServletResponse response) { try { ColleagueVipContext ctx = resolveColleagueVipContext(applyDateStart, applyDateEnd, tenantId); if (ctx.validationError() != null) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, ctx.validationError()); return; } List exportRows = new ArrayList<>(); for (LbPurchaseApply row : ctx.rows()) { if (colleagueVipSkipReason(row, ctx.tradeMatchedApplyPhones()) == null) { exportRows.add(row); } } try (Workbook workbook = new XSSFWorkbook()) { Sheet sheet = workbook.createSheet("开通特权提醒"); CellStyle headerStyle = createHeaderStyle(workbook); CellStyle groupStyle = createGroupStyle(workbook); CellStyle dataStyle = createDataStyle(workbook); String[] headers = {"提醒", "昵称", "手机号", "特权开通日期", "开通日期"}; 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; if (!exportRows.isEmpty()) { int groupStartRow = rowIndex; for (LbPurchaseApply item : exportRows) { Row row = sheet.createRow(rowIndex); row.setHeightInPoints(20); createExportCell(row, 1, formatColleagueVipNickname(item), dataStyle); createExportCell(row, 2, nullToEmpty(item.getColleaguePhone()), dataStyle); createExportCell(row, 3, formatColleaguePrivilegeRange(item.getApplyDate()), dataStyle); createExportCell(row, 4, formatColleagueVipActivationDate(item.getApplyDate()), dataStyle); rowIndex++; } int groupEndRow = rowIndex - 1; Row firstRow = sheet.getRow(groupStartRow); createExportCell(firstRow, 0, "特权开通", groupStyle); if (groupEndRow > groupStartRow) { sheet.addMergedRegion(new CellRangeAddress(groupStartRow, groupEndRow, 0, 0)); } } adjustExportColumnWidths(sheet, headers); String filename = "特权提醒_" + LocalDateTime.now().format(EXPORT_FILENAME_TS) + ".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(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded); try (ServletOutputStream os = response.getOutputStream()) { workbook.write(os); os.flush(); } } } catch (Exception e) { log.error("同事特权开通提醒导出异常", e); try { response.reset(); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); response.setContentType("text/plain;charset=UTF-8"); response.getWriter().write("导出异常:" + e.getMessage()); } catch (Exception ignored) { // ignore } } } private record ColleagueVipContext( String validationError, List rows, Set tradeMatchedApplyPhones) {} private ColleagueVipContext resolveColleagueVipContext( LocalDate applyDateStart, LocalDate applyDateEnd, String tenantId) { if (applyDateStart == null || applyDateEnd == null) { return new ColleagueVipContext("申请开始日期与结束日期不能为空", List.of(), Set.of()); } if (applyDateEnd.isBefore(applyDateStart)) { return new ColleagueVipContext("结束日期不能早于开始日期", List.of(), Set.of()); } if (tenantId == null || tenantId.trim().isEmpty()) { return new ColleagueVipContext("租户id不能为空", List.of(), Set.of()); } LambdaQueryWrapper q = new LambdaQueryWrapper<>(); q.eq(LbPurchaseApply::getTenantId, tenantId.trim()) .ge(LbPurchaseApply::getApplyDate, applyDateStart) .le(LbPurchaseApply::getApplyDate, applyDateEnd); List rows = this.list(q); if (rows == null) { rows = List.of(); } Set applyPhones = new HashSet<>(); for (LbPurchaseApply row : rows) { String applyPhone = trimToNull(row.getApplyPhone()); if (applyPhone != null) { applyPhones.add(applyPhone); } } Set tradeMatchedApplyPhones = new HashSet<>(); if (!applyPhones.isEmpty()) { String buyTimeStart = applyDateStart.atStartOfDay().format(ORDER_DAY_TIME_FMT); String buyTimeEnd = applyDateEnd.atTime(23, 59, 59).format(ORDER_DAY_TIME_FMT); LambdaQueryWrapper orderQuery = new LambdaQueryWrapper<>(); orderQuery.eq(LbOrderRow::getTenantId, tenantId.trim()) .eq(LbOrderRow::getDataType, ORDER_DATA_TYPE_DETAIL) .ge(LbOrderRow::getBuyTime, buyTimeStart) .le(LbOrderRow::getBuyTime, buyTimeEnd) .in(LbOrderRow::getBuyerPhone, applyPhones) .isNotNull(LbOrderRow::getPayTime) .select(LbOrderRow::getBuyerPhone); List orderRows = lbOrderRowMapper.selectList(orderQuery); if (orderRows != null) { for (LbOrderRow orderRow : orderRows) { String buyerPhone = trimToNull(orderRow.getBuyerPhone()); if (buyerPhone != null) { tradeMatchedApplyPhones.add(buyerPhone); } } } } return new ColleagueVipContext(null, rows, tradeMatchedApplyPhones); } private static String colleagueVipSkipReason(LbPurchaseApply row, Set tradeMatchedApplyPhones) { String applyPhone = row.getApplyPhone() != null ? row.getApplyPhone().trim() : ""; if (applyPhone.isEmpty()) { return "apply_phone 为空,跳过"; } if (!tradeMatchedApplyPhones.contains(applyPhone)) { return "申请人手机号在 lb_order_row(buyer_phone)中未找到已支付订单,说明未进货,推荐人不能享受特权,跳过开通特权"; } String phone = row.getColleaguePhone() != null ? row.getColleaguePhone().trim() : ""; if (phone.isEmpty()) { return "colleague_phone 为空,跳过"; } if (row.getApplyDate() == null) { return "applyDate 为空,跳过"; } return null; } private static String formatColleagueVipNickname(LbPurchaseApply item) { String colleagueName = item.getColleagueName(); if (colleagueName != null && !colleagueName.trim().isEmpty()) { return colleagueName.trim(); } return item.getApplyUser() == null ? "" : item.getApplyUser().trim(); } /** 同事特权「开通日期」:申请日加 1 个自然日,若落在周六日则顺延到下一工作日。 */ private static LocalDate computeColleagueVipActivationDate(LocalDate applyDate) { if (applyDate == null || applyDate.equals(LocalDate.MIN)) { return null; } return firstWeekdayOnOrAfter(applyDate.plusDays(1)); } /** 同事特权「特权开通日期」起点:开通日加 1 个自然日,若落在周六日则顺延到下一工作日。 */ private static LocalDate computeColleagueVipPrivilegeStart(LocalDate activationDate) { if (activationDate == null) { return null; } return firstWeekdayOnOrAfter(activationDate.plusDays(1)); } private static String formatColleagueVipActivationDate(LocalDate applyDate) { LocalDate activation = computeColleagueVipActivationDate(applyDate); if (activation == null) { return ""; } return activation.getYear() + "/" + activation.getMonthValue() + "/" + activation.getDayOfMonth(); } private static String formatColleaguePrivilegeRange(LocalDate applyDate) { if (applyDate == null) { return ""; } LocalDate activation = computeColleagueVipActivationDate(applyDate); if (activation == null) { return ""; } LocalDate start = computeColleagueVipPrivilegeStart(activation); LocalDate endDate = endOfInclusiveWeekdaySpan(start, COLLEAGUE_PRIVILEGE_WEEKDAY_COUNT, null); start = start.minusDays(1); endDate = endDate.minusDays(1); return start.format(PRIVILEGE_RANGE_FMT) + "-" + endDate.format(PRIVILEGE_RANGE_FMT); } @Override public void exportExcel(LocalDate applyDateStart, LocalDate applyDateEnd, String tenantId, HttpServletResponse response) { try { if (tenantId == null || tenantId.trim().isEmpty()) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "tenantId不能为空"); return; } if (applyDateStart == null || applyDateEnd == null) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "申请开始日期与结束日期不能为空"); return; } if (applyDateEnd.isBefore(applyDateStart)) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "结束日期不能早于开始日期"); return; } LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(LbPurchaseApply::getTenantId, tenantId.trim()) .ge(LbPurchaseApply::getApplyDate, applyDateStart) .le(LbPurchaseApply::getApplyDate, applyDateEnd) .orderByAsc(LbPurchaseApply::getApplyDate) .orderByAsc(LbPurchaseApply::getId); List rows = this.list(queryWrapper); LinkedHashMap> grouped = new LinkedHashMap<>(); for (LbPurchaseApply row : rows) { LocalDate key = row.getApplyDate(); if (key == null) { key = LocalDate.MIN; } grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(row); } try (Workbook workbook = new XSSFWorkbook()) { Sheet sheet = workbook.createSheet("进货申请"); CellStyle headerStyle = createHeaderStyle(workbook); CellStyle groupStyle = createGroupStyle(workbook); CellStyle dataStyle = createDataStyle(workbook); String[] headers = { "日期和人数", "序号", "昵称", "手机号", "特权开通日期", "推荐人", "手机号", "领导人", "开通日期" }; 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 (Map.Entry> entry : grouped.entrySet()) { LocalDate applyDate = entry.getKey(); List groupRows = entry.getValue(); int groupStartRow = rowIndex; String groupLabel = formatDatePeopleLabel(applyDate, groupRows.size()); int serial = 1; for (LbPurchaseApply item : groupRows) { Row row = sheet.createRow(rowIndex); row.setHeightInPoints(20); createExportCell(row, 1, String.valueOf(serial), dataStyle); createExportCell(row, 2, nullToEmpty(item.getApplyUser()), dataStyle); createExportCell(row, 3, nullToEmpty(item.getApplyPhone()), dataStyle); createExportCell(row, 4, formatPrivilegeRange(item), dataStyle); createExportCell(row, 5, formatRecommender(item), dataStyle); createExportCell(row, 6, nullToEmpty(item.getColleaguePhone()), dataStyle); createExportCell(row, 7, nullToEmpty(item.getTeamLeader()), dataStyle); createExportCell(row, 8, formatActivationDate(item.getApplyDate()), dataStyle); serial++; rowIndex++; } int groupEndRow = rowIndex - 1; Row firstRow = sheet.getRow(groupStartRow); createExportCell(firstRow, 0, groupLabel, groupStyle); if (groupEndRow > groupStartRow) { sheet.addMergedRegion(new CellRangeAddress(groupStartRow, groupEndRow, 0, 0)); } } adjustExportColumnWidths(sheet, headers); String filename = "进货申请_" + LocalDateTime.now().format(EXPORT_FILENAME_TS) + ".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(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded); try (ServletOutputStream os = response.getOutputStream()) { workbook.write(os); os.flush(); } } } catch (Exception e) { log.error("进货申请导出异常", e); try { response.reset(); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); response.setContentType("text/plain;charset=UTF-8"); response.getWriter().write("导出异常:" + e.getMessage()); } catch (Exception ignored) { // ignore } } } private static String formatDatePeopleLabel(LocalDate applyDate, int count) { if (applyDate == null || applyDate.equals(LocalDate.MIN)) { return "未知日期招商" + count + "人"; } return applyDate.getMonthValue() + "." + applyDate.getDayOfMonth() + "招商" + count + "人"; } private static String formatPrivilegeRange(LbPurchaseApply item) { LocalDate applyDate = item.getApplyDate(); if (applyDate == null) { return item.getTryDate() != null ? item.getTryDate().trim() : ""; } LocalDate start = firstWeekdayOnOrAfter(applyDate); LocalDate endDate = endOfInclusiveWeekdaySpan(start, TRIAL_WEEKDAY_COUNT, null); return start.format(PRIVILEGE_RANGE_FMT) + " 到 " + endDate.format(PRIVILEGE_RANGE_FMT); } private static String formatRecommender(LbPurchaseApply item) { String name = item.getColleagueName(); if (name == null || name.trim().isEmpty()) { return ""; } return name.trim(); } private static String formatActivationDate(LocalDate applyDate) { if (applyDate == null || applyDate.equals(LocalDate.MIN)) { return ""; } LocalDate activation = firstWeekdayOnOrAfter(applyDate); return activation.getYear() + "/" + activation.getMonthValue() + "/" + activation.getDayOfMonth(); } private static CellStyle createHeaderStyle(Workbook workbook) { CellStyle style = workbook.createCellStyle(); Font font = workbook.createFont(); font.setBold(true); style.setFont(font); style.setAlignment(HorizontalAlignment.CENTER); style.setVerticalAlignment(VerticalAlignment.CENTER); style.setFillPattern(FillPatternType.SOLID_FOREGROUND); ((XSSFCellStyle) style).setFillForegroundColor(new XSSFColor(new Color(255, 217, 102), null)); setThinBorders(style); return style; } private static CellStyle createGroupStyle(Workbook workbook) { CellStyle style = workbook.createCellStyle(); style.setAlignment(HorizontalAlignment.CENTER); style.setVerticalAlignment(VerticalAlignment.CENTER); style.setFillPattern(FillPatternType.SOLID_FOREGROUND); ((XSSFCellStyle) style).setFillForegroundColor(new XSSFColor(new Color(255, 230, 204), null)); setThinBorders(style); return style; } private static CellStyle createDataStyle(Workbook workbook) { CellStyle style = workbook.createCellStyle(); style.setAlignment(HorizontalAlignment.CENTER); style.setVerticalAlignment(VerticalAlignment.CENTER); setThinBorders(style); return style; } private static void setThinBorders(CellStyle style) { style.setBorderTop(BorderStyle.THIN); style.setBorderBottom(BorderStyle.THIN); style.setBorderLeft(BorderStyle.THIN); style.setBorderRight(BorderStyle.THIN); } 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 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 = 0; 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 i = 0; i < columnCount; i++) { int width = Math.min(255 * 256, (maxWidths[i] + 3) * 256); sheet.setColumnWidth(i, width); } } 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; } }