Files
smartDriveEE/src/main/java/com/rj/service/impl/LbPurchaseApplyServiceImpl.java

1172 lines
52 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.HxrUserRow;
import com.rj.entity.LbDailyUserTrade;
import com.rj.entity.LbDailyUserTradeReport;
import com.rj.entity.LbPurchaseApply;
import com.rj.util.IsoWorkdayUtils;
import com.rj.mapper.LbDailyUserTradeMapper;
import com.rj.mapper.LbDailyUserTradeReportMapper;
import com.rj.mapper.LbPurchaseApplyMapper;
import com.rj.service.HxrAdminUserService;
import com.rj.service.ILbPurchaseApplyService;
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.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 LbDailyUserTradeMapper lbDailyUserTradeMapper;
@Autowired
private HxrAdminUserService hxrAdminUserService;
@Autowired
private ObjectMapper objectMapper;
@Value("${langchain4j.open-ai.chat-model.model-name:qwen-plus}")
private String dashScopeChatModel;
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;
/** 同事特权开通提醒中「特权开通日期」包含的工作日天数(与 syncHxrAdminColleagueVip 的 viptime 规则一致)。 */
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(
"parseFromOrderInfoByLlmLLM 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(
"trialSpancursor={} 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)
.orderByDesc(LbPurchaseApply::getCreateTime);
Page<LbPurchaseApply> page = this.page(new Page<>(current, size), queryWrapper);
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;
}
}
@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(), 3);
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> 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;
}
Set<String> tradeMatchedNicknames = ctx.tradeMatchedNicknames();
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, tradeMatchedNicknames);
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);
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);
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.tradeMatchedNicknames()) == 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, formatActivationDate(item.getApplyDate()), dataStyle);
rowIndex++;
}
int groupEndRow = rowIndex - 1;
Row firstRow = sheet.getRow(groupStartRow);
createExportCell(firstRow, 0, COLLEAGUE_VIP_GROUP_LABEL, 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> tradeMatchedNicknames) {}
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> applyUserNames = new HashSet<>();
for (LbPurchaseApply row : rows) {
if (row.getColleagueName() != null && !row.getColleagueName().trim().isEmpty()
&& row.getApplyUser() != null && !row.getApplyUser().trim().isEmpty()) {
applyUserNames.add(row.getApplyUser().trim());
}
}
Set<String> tradeMatchedNicknames = new HashSet<>();
if (!applyUserNames.isEmpty()) {
LambdaQueryWrapper<LbDailyUserTrade> tradeQuery = new LambdaQueryWrapper<>();
tradeQuery.eq(LbDailyUserTrade::getTenantId, tenantId.trim())
.in(LbDailyUserTrade::getNickname, applyUserNames)
.select(LbDailyUserTrade::getNickname);
List<LbDailyUserTrade> matchedTrades = lbDailyUserTradeMapper.selectList(tradeQuery);
for (LbDailyUserTrade trade : matchedTrades) {
if (trade.getNickname() != null && !trade.getNickname().trim().isEmpty()) {
tradeMatchedNicknames.add(trade.getNickname().trim());
}
}
}
return new ColleagueVipContext(null, rows, tradeMatchedNicknames);
}
private static String colleagueVipSkipReason(LbPurchaseApply row, Set<String> tradeMatchedNicknames) {
String applyUserName = row.getApplyUser() != null ? row.getApplyUser().trim() : "";
if (applyUserName.isEmpty()) {
return "colleague_name 为空,跳过";
}
if (!tradeMatchedNicknames.contains(applyUserName)) {
return "申请人姓名在 lb_daily_user_trade 中未找到 ,说明今天没进货 ,推荐人不能享受特权,跳过开通特权";
}
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();
}
private static String formatColleaguePrivilegeRange(LocalDate applyDate) {
if (applyDate == null) {
return "";
}
LocalDate start = firstWeekdayOnOrAfter(applyDate);
LocalDate endDate = endOfInclusiveWeekdaySpan(start, COLLEAGUE_PRIVILEGE_WEEKDAY_COUNT, null);
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);
Map<String, Integer> maxOrderByColleaguePhone = loadColleagueMaxOrderCache(rows);
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, maxOrderByColleaguePhone), 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 Map<String, Integer> loadColleagueMaxOrderCache(List<LbPurchaseApply> rows) {
Map<String, Integer> cache = new HashMap<>();
Set<String> phones = new HashSet<>();
for (LbPurchaseApply row : rows) {
if (row.getColleaguePhone() != null && !row.getColleaguePhone().trim().isEmpty()) {
phones.add(row.getColleaguePhone().trim());
}
}
for (String phone : phones) {
try {
Optional<HxrUserRow> userOpt = hxrAdminUserService.fetchFirstUserByMobile(phone);
userOpt.map(HxrUserRow::maxOrder).ifPresent(maxOrder -> cache.put(phone, maxOrder));
} catch (Exception ex) {
log.debug("导出时查询推荐人 max_order 失败phone={}", phone, ex);
}
}
return cache;
}
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, Map<String, Integer> maxOrderByPhone) {
String name = item.getColleagueName();
if (name == null || name.trim().isEmpty()) {
return "";
}
String trimmedName = name.trim();
String phone = item.getColleaguePhone() == null ? "" : item.getColleaguePhone().trim();
if (!phone.isEmpty()) {
Integer maxOrder = maxOrderByPhone.get(phone);
if (maxOrder != null) {
return trimmedName + " " + maxOrder;
}
}
return trimmedName;
}
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;
}
}