Files
smartDriveEE/src/main/java/com/rj/service/impl/LbPurchaseApplyServiceImpl.java
2026-09-12 21:05:35 +08:00

1736 lines
75 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<LbPurchaseApplyMapper, LbPurchaseApply>
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<String, Object> parseFromOrderInfoByLlm(String orderInfo, String tenantId) {
Map<String, Object> 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<String, Object> add(LbPurchaseApply entity) {
Map<String, Object> 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<LbPurchaseApply> 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<String, Object> update(LbPurchaseApply entity) {
Map<String, Object> 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<String, Object> deleteById(Long id) {
Map<String, Object> 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<String, Object> pageQuery(Integer current,
Integer size,
String tenantId,
String applyUser,
String applyPhone,
String colleagueName,
String teamLeader,
String teamBigLeader,
LocalDate applyDate) {
Map<String, Object> result = new HashMap<>();
try {
if (current == null || current < 1) {
current = 1;
}
if (size == null || size < 1) {
size = 10;
}
LambdaQueryWrapper<LbPurchaseApply> 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<LbPurchaseApply> 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<LbPurchaseApply> records) {
if (records == null || records.isEmpty()) {
return;
}
Set<String> 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<LbUser> userQuery = new LambdaQueryWrapper<>();
userQuery.in(LbUser::getMobile, mobiles);
List<LbUser> lbUsers = lbUserMapper.selectList(userQuery);
Map<String, LbUser> 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<LbPurchaseApply> records) {
if (records == null || records.isEmpty()) {
return;
}
Set<String> buyerPhones = new HashSet<>();
Set<String> 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<LbOrderRow> 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<LbOrderRow> orderRows = lbOrderRowMapper.selectList(orderQuery);
if (orderRows == null || orderRows.isEmpty()) {
return;
}
Map<String, LbOrderRow> 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<String, Object> listNeedPrivilegeRecommenders(LocalDate startDate, LocalDate endDate) {
Map<String, Object> 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<LbPurchaseApply> purchaseQuery = new LambdaQueryWrapper<>();
purchaseQuery.ge(LbPurchaseApply::getApplyDate, startDate)
.le(LbPurchaseApply::getApplyDate, endDate)
.orderByDesc(LbPurchaseApply::getApplyDate)
.orderByDesc(LbPurchaseApply::getApplyDate) ;
List<LbPurchaseApply> 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<String> 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<LbDailyUserTradeReport> reportQuery = new LambdaQueryWrapper<>();
reportQuery.in(LbDailyUserTradeReport::getNickname, applyUsers)
.select(LbDailyUserTradeReport::getNickname);
List<LbDailyUserTradeReport> matchedReports = lbDailyUserTradeReportMapper.selectList(reportQuery);
Set<String> matchedNicknames = new HashSet<>();
for (LbDailyUserTradeReport report : matchedReports) {
if (report.getNickname() != null && !report.getNickname().trim().isEmpty()) {
matchedNicknames.add(report.getNickname().trim());
}
}
List<LbPurchaseApply> 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<String, Object> syncHxrAdminUserVipByApplyDateRange(
LocalDate applyDateStart, LocalDate applyDateEnd, String tenantId, Integer maxOrder) {
Map<String, Object> 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<LbPurchaseApply> q = new LambdaQueryWrapper<>();
q.eq(LbPurchaseApply::getTenantId, tenantId.trim())
.ge(LbPurchaseApply::getApplyDate, applyDateStart)
.le(LbPurchaseApply::getApplyDate, applyDateEnd);
List<LbPurchaseApply> 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<Map<String, Object>> details = new ArrayList<>();
int ok = 0;
int fail = 0;
for (LbPurchaseApply row : rows) {
Map<String, Object> 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<HxrUserRow> 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<String, Object> updateHxrAdminUserVipByUserIds(
String tenantId, List<Long> userIds, Integer isVip, String vipTime, Integer maxOrder) {
Map<String, Object> 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<HxrAdminUserApiContext> 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<Map<String, Object>> details = new ArrayList<>();
int ok = 0;
int fail = 0;
for (Long userId : userIds) {
Map<String, Object> 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<String, Object> updateHxrAdminUserResellStatusByUserIds(
String tenantId, List<Long> userIds,
Boolean isResellEnabled, Integer isResell,
Boolean statusEnabled, Integer status) {
Map<String, Object> 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<HxrAdminUserApiContext> apiContextOpt =
lbThirdIntegrationConfigService.resolveAdminUserApiContext(tid);
if (apiContextOpt.isEmpty()) {
return buildApiContextMissingResult(tid, userIds.size());
}
List<String> 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<String, Object> updateHxrAdminUserResellStatusForAllMembers(
String tenantId,
Boolean isResellEnabled, Integer isResell,
Boolean statusEnabled, Integer status) {
Map<String, Object> 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<MemberInfo> memberQuery = new LambdaQueryWrapper<>();
// memberQuery.and(w -> w.isNull(MemberInfo::getNickname)
// .or().notLike(MemberInfo::getNickname, "**"));
List<MemberInfo> members = memberInfoMapper.selectList(memberQuery);
if (members == null || members.isEmpty()) {
result.put("success", false);
result.put("message", "member_info 表无用户数据");
return result;
}
List<String> userIds = new ArrayList<>(members.size());
for (MemberInfo member : members) {
userIds.add(member == null ? null : member.getId());
}
String tid = tenantId.trim();
Optional<HxrAdminUserApiContext> 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<String, Object> buildApiContextMissingResult(String tid, int total) {
Map<String, Object> 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<String, Object> doUpdateHxrAdminUserResellStatus(
String tid,
List<String> userIds,
HxrAdminUserApiContext apiContext,
boolean resellEffect, Integer isResell,
boolean statusEffect, Integer status) {
Map<String, Object> result = new HashMap<>();
Integer isResellForApi = resellEffect ? isResell : null;
Integer statusForApi = statusEffect ? status : null;
List<Map<String, Object>> details = new ArrayList<>();
int ok = 0;
int fail = 0;
for (String userId : userIds) {
Map<String, Object> 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<String, Object> syncHxrAdminColleagueVipByApplyDateRange(
LocalDate applyDateStart, LocalDate applyDateEnd, String tenantId) {
Map<String, Object> 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<LbPurchaseApply> 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<HxrAdminUserApiContext> 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<String> tradeMatchedApplyPhones = ctx.tradeMatchedApplyPhones();
List<Map<String, Object>> details = new ArrayList<>();
int ok = 0;
int fail = 0;
for (LbPurchaseApply row : rows) {
Map<String, Object> 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<HxrUserRow> 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<LbPurchaseApply> 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<LbPurchaseApply> rows,
Set<String> 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<LbPurchaseApply> q = new LambdaQueryWrapper<>();
q.eq(LbPurchaseApply::getTenantId, tenantId.trim())
.ge(LbPurchaseApply::getApplyDate, applyDateStart)
.le(LbPurchaseApply::getApplyDate, applyDateEnd);
List<LbPurchaseApply> rows = this.list(q);
if (rows == null) {
rows = List.of();
}
Set<String> applyPhones = new HashSet<>();
for (LbPurchaseApply row : rows) {
String applyPhone = trimToNull(row.getApplyPhone());
if (applyPhone != null) {
applyPhones.add(applyPhone);
}
}
Set<String> 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<LbOrderRow> 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<LbOrderRow> 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<String> 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<LbPurchaseApply> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(LbPurchaseApply::getTenantId, tenantId.trim())
.ge(LbPurchaseApply::getApplyDate, applyDateStart)
.le(LbPurchaseApply::getApplyDate, applyDateEnd)
.orderByAsc(LbPurchaseApply::getApplyDate)
.orderByAsc(LbPurchaseApply::getId);
List<LbPurchaseApply> rows = this.list(queryWrapper);
LinkedHashMap<LocalDate, List<LbPurchaseApply>> 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<LocalDate, List<LbPurchaseApply>> entry : grouped.entrySet()) {
LocalDate applyDate = entry.getKey();
List<LbPurchaseApply> 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;
}
}