提交 db2605b2 authored 作者: douxy's avatar douxy

增加店内执行计划导出/入功能: 1.增加导入功能

上级 5f2ada56
...@@ -94,6 +94,11 @@ ...@@ -94,6 +94,11 @@
<groupId>com.taobao</groupId> <groupId>com.taobao</groupId>
<artifactId>taobao-sdk-java</artifactId> <artifactId>taobao-sdk-java</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>
......
package com.sfa.operation.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @Author: DouXinYu
* @Date: 2025-12-11 15:18
* @Description: OSS配置属性
*/
@Data
@Component
@ConfigurationProperties(prefix = "aliyun")
public class OssConfigProperties{
/**
* 访问密钥ID
*/
private String accessKeyId;
/**
* 访问密钥
*/
private String accessKeySecret;
/**
* OSS配置属性
*/
private Oss oss = new Oss();
/**
* OSS其他配置项
*/
@Data
public static class Oss{
/**
* 地域ID
*/
private String regionId;
/**
* 存储空间名称
*/
private String bucketName;
/**
* 静态网站访问地址
*/
private String webJsLink;
/**
* 角色授权
*/
private String stsRoleArm;
private String sessionName;
/**
* 静态网站访问地址前缀
*/
private static final String END_POINT_PREFIX = "https://";
/**
* 静态网站访问地址后缀
*/
private static final String END_POINT_SUFFIX = ".aliyuncs.com";
/**
* 获取静态网站访问地址
* @return 静态网站访问地址
*/
public String getEndPoint(){
return END_POINT_PREFIX + this.getRegionId() + END_POINT_SUFFIX;
}
}
}
package com.sfa.operation.controller.sales.excel;
import com.sfa.common.core.domain.R;
import com.sfa.operation.pojo.sales.request.ImportApExcelRequest;
import com.sfa.operation.service.sales.export.IImportExcelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author: DouXinYu
* @Date: 2025-12-10 15:21
* @Description: AP导入excel(填报)控制类
*/
@RestController
@RequestMapping("/sales/import")
public class ApImportExcelController {
@Autowired
private IImportExcelService importExcelService;
@PostMapping("/upload")
public R importApDisplayExcel(@RequestBody ImportApExcelRequest request) {
return importExcelService.importApExcel(request);
}
@PostMapping("/update")
public R updateApDisplayExcel(@RequestBody ImportApExcelRequest request) {
return importExcelService.updateApEntity(request);
}
}
...@@ -3,6 +3,7 @@ package com.sfa.operation.domain.sales.dao; ...@@ -3,6 +3,7 @@ package com.sfa.operation.domain.sales.dao;
import com.sfa.common.core.web.domain.PageInfo; import com.sfa.common.core.web.domain.PageInfo;
import com.sfa.operation.domain.sales.entity.SalesApDisplay; import com.sfa.operation.domain.sales.entity.SalesApDisplay;
import com.sfa.operation.domain.sales.wq.SalesApWq; import com.sfa.operation.domain.sales.wq.SalesApWq;
import com.sfa.operation.pojo.sales.excel.SalesApDisplayImportExcelDto;
import com.sfa.operation.pojo.sales.response.SalesApDisplayDto; import com.sfa.operation.pojo.sales.response.SalesApDisplayDto;
import java.util.List; import java.util.List;
...@@ -22,4 +23,9 @@ public interface ISalesApDisplayDao { ...@@ -22,4 +23,9 @@ public interface ISalesApDisplayDao {
Object queryDeptAPReport(SalesApWq build); Object queryDeptAPReport(SalesApWq build);
List<SalesApDisplay> queryDataListByCondition(SalesApWq build); List<SalesApDisplay> queryDataListByCondition(SalesApWq build);
List<SalesApDisplay> queryByCondition(List<SalesApDisplayImportExcelDto> validDtoList);
int batchUpdate(List<SalesApDisplay> updateEntityList);
} }
...@@ -2,6 +2,7 @@ package com.sfa.operation.domain.sales.dao.impl; ...@@ -2,6 +2,7 @@ package com.sfa.operation.domain.sales.dao.impl;
import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.sfa.common.core.enums.ECode; import com.sfa.common.core.enums.ECode;
import com.sfa.common.core.exception.CheckedException; import com.sfa.common.core.exception.CheckedException;
...@@ -13,13 +14,16 @@ import com.sfa.operation.domain.sales.dao.ISalesApDisplayDao; ...@@ -13,13 +14,16 @@ import com.sfa.operation.domain.sales.dao.ISalesApDisplayDao;
import com.sfa.operation.domain.sales.entity.SalesApDisplay; import com.sfa.operation.domain.sales.entity.SalesApDisplay;
import com.sfa.operation.domain.sales.mapper.SalesApDisplayMapper; import com.sfa.operation.domain.sales.mapper.SalesApDisplayMapper;
import com.sfa.operation.domain.sales.wq.SalesApWq; import com.sfa.operation.domain.sales.wq.SalesApWq;
import com.sfa.operation.pojo.sales.excel.SalesApDisplayImportExcelDto;
import com.sfa.operation.pojo.sales.response.SalesApDisplayDto; import com.sfa.operation.pojo.sales.response.SalesApDisplayDto;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import java.util.*; import java.util.*;
import java.util.function.Consumer;
/** /**
* @author : liqiulin * @author : liqiulin
...@@ -74,6 +78,52 @@ public class SalesApDisplayDaoImpl implements ISalesApDisplayDao { ...@@ -74,6 +78,52 @@ public class SalesApDisplayDaoImpl implements ISalesApDisplayDao {
return apDisplayList; return apDisplayList;
} }
@Override
public List<SalesApDisplay> queryByCondition(List<SalesApDisplayImportExcelDto> validDtoList) {
// 构建精准匹配的QueryWrapper(每个DTO对应一组AND条件,组间OR连接)
QueryWrapper<SalesApDisplay> wrapper = new QueryWrapper<>();
// 标记是否是第一个OR条件(避免多余的前置OR)
boolean isFirstCondition = true;
for (SalesApDisplayImportExcelDto dto : validDtoList) {
// 字段为空时不参与条件匹配(或根据业务要求改为IS NULL)
Long sadId = dto.getSadId();
String regionName = StringUtils.trimToNull(dto.getRegionName());
String dealerName = StringUtils.trimToNull(dto.getDealerName());
String lineName = StringUtils.trimToNull(dto.getLineName());
// 构建单DTO的AND组合条件
Consumer<QueryWrapper<SalesApDisplay>> singleDtoCondition = w -> {
w.eq(sadId != null, "sad_id", sadId)
.eq(regionName != null, "region_name", regionName)
.eq(dealerName != null, "dealer_name", dealerName)
.eq(lineName != null, "line_name", lineName);
};
// 多条件OR拼接(第一个条件不加OR,避免SQL语法错误)
if (isFirstCondition) {
singleDtoCondition.accept(wrapper);
isFirstCondition = false;
} else {
wrapper.or(singleDtoCondition);
}
}
return salesapdisMapper.selectList(wrapper);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int batchUpdate(List<SalesApDisplay> updateEntityList) {
if (CollectionUtils.isEmpty(updateEntityList)){
return 0;
}
int count = 0;
for (SalesApDisplay salesApDisplay : updateEntityList) {
count += salesapdisMapper.updateById(salesApDisplay);
}
return count;
}
private LambdaQueryWrapper<SalesApDisplay> buildWq(SalesApWq salesApWq) { private LambdaQueryWrapper<SalesApDisplay> buildWq(SalesApWq salesApWq) {
LambdaQueryWrapper<SalesApDisplay> qw = new LambdaQueryWrapper<>(); LambdaQueryWrapper<SalesApDisplay> qw = new LambdaQueryWrapper<>();
if (StringUtils.isNotBlank(salesApWq.getDealerCode())) { if (StringUtils.isNotBlank(salesApWq.getDealerCode())) {
......
package com.sfa.operation.enums;
import lombok.Getter;
/**
* @Author: DouXinYu
* @Date: 2025-12-10 16:15
* @Description: 店内执行填报 导入 类型枚举
*/
@Getter
public enum ImportApType {
/**
* 常规陈列导入策略
*/
NORMAL_DISPLAY_IMPORT("normalDisplayImportStrategy"),
/**
* 档期计划导入策略
*/
PROMOTION_PLAN_IMPORT("promotionPlanImportStrategy"),
/**
* 零食陈列导入策略
*/
SNACK_DISPLAY_IMPORT("snackDisplayImportStrategy"),
/**
* 三米两秒导入策略
*/
THREE_METER_TWO_SECONDS_IMPORT("threeMeterSecondsImportStrategy"),
/**
* 六小金刚导入策略
*/
SIX_KINGkONG_IMPORT("sixKingKongImportStrategy");
private final String importStrategy;
ImportApType(String importStrategy) {
this.importStrategy = importStrategy;
}
}
...@@ -26,8 +26,6 @@ public class ApExportExcelStrategyFactory { ...@@ -26,8 +26,6 @@ public class ApExportExcelStrategyFactory {
public IExportApExcelStrategy getStrategy(String exportApType) { public IExportApExcelStrategy getStrategy(String exportApType) {
System.out.println(exportApExcelStrategyMap.size());
System.out.println(exportApType);
if (exportApType == null || exportApType.trim().isEmpty()) { if (exportApType == null || exportApType.trim().isEmpty()) {
log.error("AP导出Excel策略工厂:传入的导出类型为空!"); log.error("AP导出Excel策略工厂:传入的导出类型为空!");
throw new IllegalArgumentException("传入的导出类型为空!"); throw new IllegalArgumentException("传入的导出类型为空!");
...@@ -40,9 +38,8 @@ public class ApExportExcelStrategyFactory { ...@@ -40,9 +38,8 @@ public class ApExportExcelStrategyFactory {
throw new IllegalArgumentException("传入的导出类型不存在!"); throw new IllegalArgumentException("传入的导出类型不存在!");
} }
// 3. 从枚举中获取策略Bean名称,再从Map中查找策略 // 从枚举中获取策略Bean名称,再从Map中查找策略
String strategyBeanName = typeEnum.getStrategy(); String strategyBeanName = typeEnum.getStrategy();
System.out.println(strategyBeanName);
IExportApExcelStrategy strategy = exportApExcelStrategyMap.get(strategyBeanName); IExportApExcelStrategy strategy = exportApExcelStrategyMap.get(strategyBeanName);
if (strategy == null) { if (strategy == null) {
log.error("AP导出Excel策略工厂:未找到对应的导出策略!目标策略Bean名称为:{}", strategyBeanName); log.error("AP导出Excel策略工厂:未找到对应的导出策略!目标策略Bean名称为:{}", strategyBeanName);
......
package com.sfa.operation.factory;
import com.sfa.operation.enums.ImportApType;
import com.sfa.operation.strategy.IImportApExcelStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @Author: DouXinYu
* @Date: 2025-12-10 16:23
* @Description: 店内执行上报 - 填报- 导入策略工厂类
*/
@Slf4j
@Component
public class ApImportExcelStrategyFactory {
private final Map<String, IImportApExcelStrategy> importApExcelStrategyMap;
//构造器注入
@Autowired
public ApImportExcelStrategyFactory(Map<String, IImportApExcelStrategy> importApExcelStrategyMap) {
this.importApExcelStrategyMap = importApExcelStrategyMap;
}
/**
* 获取策略
* @param importApType 点击导入的类型
* @return 对应策略实现类
*/
public IImportApExcelStrategy getStrategy(String importApType) {
log.info("店内执行填报导入策略注入开始,目标类型为:{}", importApType);
// 参数校验
if (importApType == null || importApType.trim().isEmpty()){
log.error("店内执行填报导入策略注入失败,失败原因:传入的导入类型为空!");
return null;
}
//转换为枚举类型
ImportApType targetType;
try {
targetType = ImportApType.valueOf(importApType);
} catch (IllegalArgumentException e) {
log.error("店内执行填报导入策略注入失败,失败原因:传入的导入类型不存在!目标类型为:{}", importApType);
return null;
}
// 从枚举中获取策略Bean名称,再从Map中查找策略
String beanName = targetType.getImportStrategy();
IImportApExcelStrategy strategy = importApExcelStrategyMap.get(beanName);
if (strategy == null) {
log.error("店内执行填报导入策略注入失败,失败原因:未找到对应的导入策略!目标策略Bean名称为:{}", beanName);
return null;
}
log.info("店内执行填报导入策略注入成功,目标策略Bean名称为:{}", beanName);
return strategy;
}
}
package com.sfa.operation.pojo.sales.excel;
import lombok.Data;
/**
* @Author: DouXinYu
* @Date: 2025-12-10 16:05
* @Description: 店内执行上报 - 填报 - 常规陈列 导入excel数据传输对象
*/
@Data
public class SalesApDisplayImportExcelDto {
/**
* 主键ID
* 类型:Long
*/
private Long sadId;
/**
* 计划月份
* 注意:Excel中是"YYYY-MM"字符串,导入时需转换为Date类型
*/
private String salesMonth;
/**
* 销售大区
* 类型:String
*/
private String regionName;
/**
* 销售战区
* 类型:String
*/
private String districtName;
/**
* 经销商代码
* 类型:String
*/
private String dealerCode;
/**
* 经销商名称
* 类型:String
*/
private String dealerName;
/**
* 门店编码
* 类型:String
*/
private String storeCode;
/**
* 门店名称
* 类型:String
*/
private String storeName;
/**
* 系统名称
* 类型:String
*/
private String lineName;
/**
* 计划主货架-形式
* 类型:String
*/
private String plannedMainShelfType;
/**
* 实际主货架-形式
* 类型:String
*/
private String actualMainShelfType;
/**
* 计划主货架-数量
* 类型:Integer
*/
private Integer plannedMainShelfQty;
/**
* 实际主货架-数量
* 类型:Integer
*/
private Integer actualMainShelfQty;
/**
* 计划端架-数量
* 类型:Double
*/
private Double plannedEndCapQty;
/**
* 实际端架-数量
* 类型:Double
*/
private Double actualEndCapQty;
/**
* 计划地堆-平米数(㎡)
* 类型:Double
*/
private Double plannedFloorStackArea;
/**
* 实际地堆-平米数(㎡)
* 类型:Double
*/
private Double actualFloorStackArea;
/**
* 计划地堆-数量
* 类型:Integer
*/
private Integer plannedFloorStackQty;
/**
* 实际地堆-数量
* 类型:Integer
*/
private Integer actualFloorStackQty;
/**
* 计划多点陈列-数量+形式
* 类型:String
*/
private String plannedMultiDisplay;
/**
* 实际多点陈列-数量+形式
* 类型:String
*/
private String actualMultiDisplay;
/**
* 计划挂条-数量+形式
* 类型:String
*/
private String plannedHangingStripQuantityForm;
/**
* 实际挂条-数量+形式
* 类型:String
*/
private String actualHangingStripQuantityForm;
/**
* 错误信息
* 类型:String
*/
private String errorMsg;
/**
* 行号
* 类型:Integer
*/
private Integer rowNum;
}
package com.sfa.operation.pojo.sales.request;
import lombok.Data;
/**
* @Author: DouXinYu
* @Date: 2025-12-11 16:12
* @Description: 店内执行上报 - 导入Excel请求参数
*/
@Data
public class ImportApExcelRequest {
private String importApType;
private String importApFilePath;
private String uuid;
}
package com.sfa.operation.pojo.sales.vo;
import lombok.Data;
/**
* @Author: DouXinYu
* @Date: 2025-12-10 15:31
* @Description: 店内执行-填报要更新到表的字段
*/
@Data
public class SalesApDisplayVo {
/**
* 主键ID
*/
private Long sadId;
/**
* 主货架形式(实际)
*/
private String actualMainShelfType;
/**
* 主货架数量(实际)
*/
private Integer actualMainShelfQty;
/**
* 实际-主货架是否执行
* 执行主货架形式 >= 计划主货架形式 && 执行主货架数量 >= 计划主货架数量"
* 执行/未执行
*/
private String actualMainShelfExecuted;
/**
* 端架数量(实际)
*/
private Integer actualEndCapQty;
/**
* 实际-架是否执行
* 执行端架数量 >= 计划端架数量
*/
private String actualEndCapExecuted;
/**
* 地堆平米数(实际)
*/
private Double actualFloorStackArea;
/**
* 地堆数量(实际)
*/
private Integer actualFloorStackQty;
/**
* 实际-地堆是否执行
* 执行平米数 >= 计划平米数 && 执行数量 >= 计划数量"
*/
private String actualFloorStackExecuted;
/**
* 多点陈列数量+形式(实际)
*/
private String actualMultiDisplay;
/**
* 实际-多点陈列是否执行
*
* actualMultiDisplay的值如下时:
* 执行与计划一致:执行
* 执行与计划不一致:未执行
*/
private String actualMultiDisplayExecuted;
/**
* 挂条数量+形式(实际)
*/
private String actualHangingStripQuantityForm;
/**
* 实际-挂条是否执行
*
* actualHangingStripQuantityForm的值如下时:
* 执行与计划一致:执行
* 执行与计划不一致:未执行
*/
private String hangingStripExecuted;
}
package com.sfa.operation.service.sales; package com.sfa.operation.service.sales;
import com.sfa.operation.domain.sales.entity.SalesApDisplay;
import com.sfa.operation.pojo.sales.request.SalesApRequest; import com.sfa.operation.pojo.sales.request.SalesApRequest;
import java.util.List;
/** /**
* @author : liqiulin * @author : liqiulin
* @date : 2025-09-16 16 * @date : 2025-09-16 16
...@@ -19,4 +22,6 @@ public interface IApDisplayCoreService { ...@@ -19,4 +22,6 @@ public interface IApDisplayCoreService {
void putDisplayJDetail(SalesApRequest request); void putDisplayJDetail(SalesApRequest request);
void putPromotionDetail(SalesApRequest request); void putPromotionDetail(SalesApRequest request);
int batchUpdate(List<SalesApDisplay> updateEntityList);
} }
...@@ -2,7 +2,7 @@ package com.sfa.operation.service.sales; ...@@ -2,7 +2,7 @@ package com.sfa.operation.service.sales;
import com.sfa.common.core.web.domain.PageInfo; import com.sfa.common.core.web.domain.PageInfo;
import com.sfa.operation.domain.sales.entity.SalesApDisplay; import com.sfa.operation.domain.sales.entity.SalesApDisplay;
import com.sfa.operation.domain.sales.wq.SalesApWq; import com.sfa.operation.pojo.sales.excel.SalesApDisplayImportExcelDto;
import com.sfa.operation.pojo.sales.request.SalesApRequest; import com.sfa.operation.pojo.sales.request.SalesApRequest;
import java.util.List; import java.util.List;
...@@ -29,4 +29,6 @@ public interface IApDisplayQueryService { ...@@ -29,4 +29,6 @@ public interface IApDisplayQueryService {
Object queryDeptAPReport(SalesApRequest request); Object queryDeptAPReport(SalesApRequest request);
List<SalesApDisplay> queryDataListByCondition(SalesApRequest build); List<SalesApDisplay> queryDataListByCondition(SalesApRequest build);
List<SalesApDisplay> queryByCondition(List<SalesApDisplayImportExcelDto> queryParam);
} }
package com.sfa.operation.service.sales.export;
import com.sfa.common.core.domain.R;
import com.sfa.operation.pojo.sales.request.ImportApExcelRequest;
/**
* @Author: DouXinYu
* @Date: 2025-12-12 13:11
* @Description: 导入excel服务接口
*/
public interface IImportExcelService {
R importApExcel(ImportApExcelRequest request);
R updateApEntity(ImportApExcelRequest request);
}
package com.sfa.operation.service.sales.export.impl;
import com.alibaba.fastjson2.JSONObject;
import com.sfa.common.core.domain.R;
import com.sfa.operation.factory.ApImportExcelStrategyFactory;
import com.sfa.operation.pojo.sales.request.ImportApExcelRequest;
import com.sfa.operation.service.sales.export.IImportExcelService;
import com.sfa.operation.strategy.IImportApExcelStrategy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @Author: DouXinYu
* @Date: 2025-12-12 13:11
* @Description: 导入excel服务实现类
*/
@Slf4j
@Service
public class ImportExcelServiceImpl implements IImportExcelService {
@Autowired
private ApImportExcelStrategyFactory apImportExcelStrategyFactory;
@Autowired
private StringRedisTemplate stringRedisTemplate;
public static final String REDIS_KEY_PREFIX = "import_excel_ap:";
/**
* 店内执行上上报 -导入方法
*
* @param request 导入excel请求参数
* @return 导入结果
*/
@Override
public R importApExcel(ImportApExcelRequest request) {
// 参数校验
if (request == null || request.getImportApFilePath() == null || request.getImportApFilePath().trim().isEmpty()){
return R.fail("导入文件路径不能为空!");
}
if (request.getImportApType() == null || request.getImportApType().trim().isEmpty()){
return R.fail("导入类型不能为空!");
}
// 获取策略
IImportApExcelStrategy strategy = apImportExcelStrategyFactory.getStrategy(request.getImportApType());
if (strategy == null){
return R.fail("未找到对应的导入策略!");
}
// 执行导入的数据验证(根据不同的策略独自设计验证)
Map<String, Object> result = strategy.execute(request.getImportApFilePath());
Integer failCount = (Integer) result.get("failCount");
//failCount>0 时 返回错误信息
if (failCount>0 ) {
log.error("导入失败,失败条数:{}",failCount);
return R.fail(result);
} else {
String uuid = (String) result.getOrDefault("uuid", "");
String redisKey = REDIS_KEY_PREFIX + uuid;
//将数据保存躁redis中
stringRedisTemplate.opsForValue().set(redisKey, JSONObject.toJSONString(result), 30, TimeUnit.MINUTES);
log.info("数据保存至redis中,redisKey={},result={}", redisKey, JSONObject.toJSONString(result));
}
return R.ok(result);
}
/**
* 前端点击确认后 更新数据
* @param request 导入excel请求参数
* @return 导入结果
*/
@Override
public R updateApEntity(ImportApExcelRequest request) {
if (request.getImportApType() == null || request.getImportApType().trim().isEmpty()){
return R.fail("导入类型不能为空!");
}
if (request.getUuid() == null || request.getUuid().trim().isEmpty()){
return R.fail("导入数据标识不能为空!");
}
// 获取策略
IImportApExcelStrategy strategy = apImportExcelStrategyFactory.getStrategy(request.getImportApType());
if (strategy == null){
return R.fail("未找到对应的导入策略!");
}
//从redis获取数据
String redisKey = REDIS_KEY_PREFIX+ request.getUuid();
String redisValue = stringRedisTemplate.opsForValue().get(redisKey);
//解析jsonToDtoList
List list = strategy.getTransactionJsonToObject(redisValue);
//批量更新
String result = strategy.updateDisplay(list);
if ("更新失败".equals(result)) {
return R.fail(result);
}
return R.ok(result);
}
}
...@@ -3,6 +3,7 @@ package com.sfa.operation.service.sales.impl; ...@@ -3,6 +3,7 @@ package com.sfa.operation.service.sales.impl;
import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.DatePattern;
import com.sfa.common.security.utils.SecurityUtils; import com.sfa.common.security.utils.SecurityUtils;
import com.sfa.operation.domain.sales.dao.*; import com.sfa.operation.domain.sales.dao.*;
import com.sfa.operation.domain.sales.entity.SalesApDisplay;
import com.sfa.operation.pojo.sales.request.SalesApRequest; import com.sfa.operation.pojo.sales.request.SalesApRequest;
import com.sfa.operation.pojo.sales.response.*; import com.sfa.operation.pojo.sales.response.*;
import com.sfa.operation.service.sales.IApDisplayCoreService; import com.sfa.operation.service.sales.IApDisplayCoreService;
...@@ -12,6 +13,7 @@ import org.springframework.stereotype.Service; ...@@ -12,6 +13,7 @@ import org.springframework.stereotype.Service;
import java.text.ParseException; import java.text.ParseException;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Objects; import java.util.Objects;
/** /**
...@@ -78,6 +80,16 @@ public class ApDisplayCoreServiceImpl implements IApDisplayCoreService { ...@@ -78,6 +80,16 @@ public class ApDisplayCoreServiceImpl implements IApDisplayCoreService {
salesApDisplayJDao.updateDetail(djDto); salesApDisplayJDao.updateDetail(djDto);
} }
/**
*
* @param updateEntityList
* @return
*/
@Override
public int batchUpdate(List<SalesApDisplay> updateEntityList) {
return salesApDisplayDao.batchUpdate(updateEntityList);
}
@Override @Override
public void putPromotionDetail(SalesApRequest request) { public void putPromotionDetail(SalesApRequest request) {
// 修改DB 日期值为null // 修改DB 日期值为null
...@@ -110,4 +122,6 @@ public class ApDisplayCoreServiceImpl implements IApDisplayCoreService { ...@@ -110,4 +122,6 @@ public class ApDisplayCoreServiceImpl implements IApDisplayCoreService {
e.printStackTrace(); e.printStackTrace();
} }
} }
} }
...@@ -2,23 +2,17 @@ package com.sfa.operation.service.sales.impl; ...@@ -2,23 +2,17 @@ package com.sfa.operation.service.sales.impl;
import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import com.sfa.common.core.utils.StringUtils;
import com.sfa.common.core.utils.bean.BeanUtils; import com.sfa.common.core.utils.bean.BeanUtils;
import com.sfa.common.core.web.domain.PageInfo; import com.sfa.common.core.web.domain.PageInfo;
import com.sfa.operation.domain.feishu.dao.IQinceMarketEmployeeDao;
import com.sfa.operation.domain.sales.dao.*; import com.sfa.operation.domain.sales.dao.*;
import com.sfa.operation.domain.sales.entity.SalesApDisplay; import com.sfa.operation.domain.sales.entity.SalesApDisplay;
import com.sfa.operation.domain.sales.wq.SalesApWq; import com.sfa.operation.domain.sales.wq.SalesApWq;
import com.sfa.operation.pojo.sales.excel.SalesApDisplayImportExcelDto;
import com.sfa.operation.pojo.sales.request.SalesApRequest; import com.sfa.operation.pojo.sales.request.SalesApRequest;
import com.sfa.operation.service.sales.IApDisplayQueryService; import com.sfa.operation.service.sales.IApDisplayQueryService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.awt.dnd.Autoscroll;
import java.util.Collections;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
/** /**
...@@ -86,6 +80,11 @@ public class ApDisplayQueryServiceImpl implements IApDisplayQueryService { ...@@ -86,6 +80,11 @@ public class ApDisplayQueryServiceImpl implements IApDisplayQueryService {
return salesApDisplayDao.queryDataListByCondition(build(request)); return salesApDisplayDao.queryDataListByCondition(build(request));
} }
@Override
public List<SalesApDisplay> queryByCondition(List<SalesApDisplayImportExcelDto> validDtoList) {
return salesApDisplayDao.queryByCondition(validDtoList);
}
private SalesApWq build(SalesApRequest salesApRequest){ private SalesApWq build(SalesApRequest salesApRequest){
SalesApWq salesApWq = new SalesApWq(); SalesApWq salesApWq = new SalesApWq();
BeanUtils.copyProperties(salesApRequest,salesApWq); BeanUtils.copyProperties(salesApRequest,salesApWq);
......
package com.sfa.operation.strategy;
import com.sfa.operation.config.ExportColumnConfig;
import com.sfa.operation.domain.sales.entity.SalesApDisplay;
import com.sfa.operation.pojo.sales.excel.SalesApDisplayImportExcelDto;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
/**
* @Author: DouXinYu
* @Date: 2025-12-10 16:21
* @Description: 店内执行填报 - 导入策略接口
*/
public interface IImportApExcelStrategy<T> {
/**
* 获取导入列配置(复用导出列配置)
*/
List<ExportColumnConfig> getImportColumnConfig();
/**
* 解析Excel文件流为DTO列表
*/
List<T> parseExcelToDtoList(InputStream inputStream, String flePathUrl,Map<String, List<T>> errorMap);
/**
* 预处理DTO数据(补充关联数据)
*/
void preprocessData(List<T> dtoList);
/**
* 数据库批量操作(新增/更新)
*/
int queryAndBatchOperate(List<T> dtoList);
/**
* 构建更新实体列表
*/
List<?>buildUpdateEntityList(List<T> dtoList);
/**
* 查询数据库数据
*/
List<?> queryData(List<T> dto);
/**
* 更新数据库数据
*/
String updateDisplay(List<T> dtoList);
List<T> getTransactionJsonToObject(String json);
/**
* 导入核心执行方法
*/
Map<String, Object> execute(String flePathUrl);
/**
* 获取导入Sheet名称(默认方法)
*/
default String getImportSheetName() {
return "";
}
}
package com.sfa.operation.strategy.impl; package com.sfa.operation.strategy.impl.exports;
import com.sfa.common.core.constant.RoleConstants;
import com.sfa.common.core.enums.ECode;
import com.sfa.common.core.exception.CheckedException;
import com.sfa.common.security.utils.SecurityUtils;
import com.sfa.operation.config.ConstantValue;
import com.sfa.operation.config.ExportColumnConfig; import com.sfa.operation.config.ExportColumnConfig;
import com.sfa.operation.pojo.sales.request.SalesApRequest; import com.sfa.operation.pojo.sales.request.SalesApRequest;
import com.sfa.operation.service.qc.IQinceMarketEmployeeService;
import com.sfa.operation.service.sales.IApDisplayQueryService; import com.sfa.operation.service.sales.IApDisplayQueryService;
import com.sfa.operation.strategy.IExportApExcelStrategy; import com.sfa.operation.strategy.IExportApExcelStrategy;
import com.sfa.operation.util.excel.ExcelStyleUtils; import com.sfa.operation.util.excel.ExcelStyleUtils;
import com.sfa.system.api.domain.SysRole;
import com.sfa.system.api.model.LoginUser;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional;
/** /**
* @Author: DouXinYu * @Author: DouXinYu
...@@ -34,10 +25,6 @@ public class NormalDisplayExportStrategyImpl implements IExportApExcelStrategy { ...@@ -34,10 +25,6 @@ public class NormalDisplayExportStrategyImpl implements IExportApExcelStrategy {
@Autowired @Autowired
private IApDisplayQueryService apDisplayQueryService; private IApDisplayQueryService apDisplayQueryService;
@Autowired
private IQinceMarketEmployeeService qinceMarketEmployeeService;
@Autowired
private ConstantValue constantValue;
/** /**
* 获取导出列配置 * 获取导出列配置
......
package com.sfa.operation.strategy.impl.imports;
import com.alibaba.nacos.shaded.com.google.gson.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.sfa.operation.config.ExportColumnConfig;
import com.sfa.operation.domain.sales.entity.SalesApDisplay;
import com.sfa.operation.pojo.sales.excel.SalesApDisplayImportExcelDto;
import com.sfa.operation.pojo.sales.vo.SalesApDisplayVo;
import com.sfa.operation.service.sales.impl.ApDisplayCoreServiceImpl;
import com.sfa.operation.service.sales.impl.ApDisplayQueryServiceImpl;
import com.sfa.operation.strategy.IImportApExcelStrategy;
import com.sfa.operation.strategy.impl.exports.NormalDisplayExportStrategyImpl;
import com.sfa.operation.util.excel.ExcelUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.io.InputStream;
import java.util.*;
import java.util.stream.Collectors;
/**
* @Author: DouXinYu
* @Date: 2025-12-11 17:31
* @Description: AP导入excel(填报)控制类
*/
@Slf4j
@Component("normalDisplayImportStrategy")
public class NormalDisplayImportStrategyImpl implements IImportApExcelStrategy<SalesApDisplayImportExcelDto> {
@Autowired
private NormalDisplayExportStrategyImpl normalDisplayExportStrategy;
@Autowired
private ApDisplayQueryServiceImpl salesApQueryDisplayService;
@Autowired
private ApDisplayCoreServiceImpl salesApDisplayCoreService;
@Override
public List<ExportColumnConfig> getImportColumnConfig() {
return normalDisplayExportStrategy.getExportColumnConfig();
}
@Override
public List<SalesApDisplayImportExcelDto> parseExcelToDtoList(InputStream inputStream, String filePathUrl,
Map<String, List<SalesApDisplayImportExcelDto>> errorMap) {
// 基础校验
if (inputStream == null || StringUtils.isBlank(filePathUrl)) {
SalesApDisplayImportExcelDto errorDto = buildSimpleErrorDto("文件流/URL为空");
errorMap.put("参数异常", Collections.singletonList(errorDto));
// 修改:返回含错误DTO的列表
return Collections.singletonList(errorDto);
}
// 复用ExcelUtils解析(内置列校验/错误收集)
String fileName = ExcelUtils.parseFileNameFromOssUrl(filePathUrl);
// String fileName = ExcelUtils.extractFileNameFromUrl(filePathUrl);
List<ExportColumnConfig> columnConfigs = getImportColumnConfig();
// 基础校验
return ExcelUtils.readApExcelWithColumnConfig(inputStream, fileName, columnConfigs, errorMap, SalesApDisplayImportExcelDto.class);
}
@Override
public void preprocessData(List<SalesApDisplayImportExcelDto> dtoList) {
if (CollectionUtils.isEmpty(dtoList)) {
return;
}
dtoList.forEach(dto -> {
// 修改:初始化errorMsg为空字符串,避免null
if (dto.getErrorMsg() == null) {
dto.setErrorMsg("");
}
StringBuilder errorMsg = new StringBuilder();
if (dto.getSadId() == null) {
errorMsg.append("序号不能为空;");
}
// 合并错误信息
if (errorMsg.length() > 0) {
String newError = errorMsg.toString().replaceAll(";$", "");
dto.setErrorMsg(StringUtils.isBlank(dto.getErrorMsg())
? newError
: dto.getErrorMsg() + ";" + newError);
}
});
}
@Override
public int queryAndBatchOperate(List<SalesApDisplayImportExcelDto> dtoList) {
if (CollectionUtils.isEmpty(dtoList)) {
return 0;
}
// 过滤有效数据(无预处理错误)
List<SalesApDisplayImportExcelDto> validDtoList = dtoList.stream()
.filter(d -> StringUtils.isBlank(d.getErrorMsg()))
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(validDtoList)) {
return 0;
}
// 先进行查询验证,确保所有数据都能在数据库中找到对应记录
int successCount = 0;
List<SalesApDisplay> salesApDisplayList = queryData(validDtoList);
Map<String, SalesApDisplay> displayMap = new HashMap<>();
for (SalesApDisplay display : salesApDisplayList) {
String matchKey = buildMatchKey(
display.getSadId(),
display.getRegionName(),
display.getDealerName(),
display.getLineName()
);
displayMap.put(matchKey, display);
}
for (SalesApDisplayImportExcelDto dto : validDtoList) {
String dtoMatchKey = buildMatchKey(
dto.getSadId(),
dto.getRegionName(),
dto.getDealerName(),
dto.getLineName()
);
if (displayMap.containsKey(dtoMatchKey)) {
successCount++;
} else {
// 错误提示仅显示「序号为{sadId}的数据不存在」
String errorMsg = dto.getSadId() != null
? "序号为" + dto.getSadId() + "的数据不存在"
: "序号为空的数据不存在"; // sadId为空时的兜底提示
// 保留原有错误信息拼接逻辑(如有其他错误,追加当前提示)
dto.setErrorMsg(StringUtils.isNotBlank(dto.getErrorMsg())
? dto.getErrorMsg() + ";" + errorMsg
: errorMsg);
}
}
return successCount;
}
@Override
public List<SalesApDisplay>buildUpdateEntityList(List<SalesApDisplayImportExcelDto> dtoList) {
// 空值快速返回
if (CollectionUtils.isEmpty(dtoList)) {
return Collections.emptyList();
}
List<SalesApDisplay> updateEntityList = new ArrayList<>();
// 批量查询所有DTO对应的计划数据,避免循环内重复查询
List<SalesApDisplay> salesApDisplayList = queryData(dtoList);
Map<String, SalesApDisplay> displayMap = new HashMap<>();
for (SalesApDisplay display : salesApDisplayList) {
String matchKey = buildMatchKey(
display.getSadId(),
display.getRegionName(),
display.getDealerName(),
display.getLineName()
);
displayMap.put(matchKey, display);
}
for (SalesApDisplayImportExcelDto dto : dtoList) {
SalesApDisplayVo salesApDisplayVo = new SalesApDisplayVo();
BeanUtils.copyProperties(dto, salesApDisplayVo);
// 从Map中获取当前DTO对应的计划数据
SalesApDisplay salesApDisplay = displayMap.get(buildMatchKey(
dto.getSadId(),
dto.getRegionName(),
dto.getDealerName(),
dto.getLineName()
));
if (salesApDisplay == null) {
log.warn("序号为{}的DTO无匹配计划数据,跳过处理", dto.getSadId());
continue;
}
// 主货架执行状态计算
if (salesApDisplay.getPlannedMainShelfType() != null && salesApDisplay.getPlannedMainShelfQty() != null
&& salesApDisplayVo.getActualMainShelfType() != null && salesApDisplayVo.getActualMainShelfQty() != null) {
boolean mainShelfTypeMatch = salesApDisplayVo.getActualMainShelfType().equals(salesApDisplay.getPlannedMainShelfType());
boolean mainShelfQtySufficient = salesApDisplayVo.getActualMainShelfQty() >= salesApDisplay.getPlannedMainShelfQty();
salesApDisplayVo.setActualMainShelfExecuted((mainShelfTypeMatch && mainShelfQtySufficient) ? "执行" : "未执行");
}
// 端架执行状态计算
if (salesApDisplayVo.getActualEndCapQty() != null && salesApDisplay.getPlannedEndCapQty() != null) {
salesApDisplayVo.setActualEndCapExecuted(
salesApDisplayVo.getActualEndCapQty() >= salesApDisplay.getPlannedEndCapQty() ? "执行" : "未执行"
);
}
// 地堆执行状态计算
if (salesApDisplay.getPlannedFloorStackArea() != null && salesApDisplay.getPlannedFloorStackQty() != null
&& salesApDisplayVo.getActualFloorStackArea() != null && salesApDisplayVo.getActualFloorStackQty() != null) {
boolean areaSufficient = salesApDisplayVo.getActualFloorStackArea() >= salesApDisplay.getPlannedFloorStackArea();
boolean qtySufficient = salesApDisplayVo.getActualFloorStackQty() >= salesApDisplay.getPlannedFloorStackQty();
salesApDisplayVo.setActualFloorStackExecuted((areaSufficient && qtySufficient) ? "执行" : "未执行");
}
// 多点陈列执行状态
if (salesApDisplay.getPlannedMultiDisplay() != null && salesApDisplayVo.getActualMultiDisplay() != null) {
salesApDisplayVo.setActualMultiDisplayExecuted(
StringUtils.equals("执行与计划一致", salesApDisplayVo.getActualMultiDisplay()) ? "执行" : "未执行"
);
}
// 挂条执行状态字段赋值错误
if (salesApDisplay.getPlannedHangingStripQuantityForm() != null && salesApDisplayVo.getActualHangingStripQuantityForm() != null) {
salesApDisplayVo.setHangingStripExecuted(
StringUtils.equals("执行与计划一致", salesApDisplayVo.getActualHangingStripQuantityForm()) ? "执行" : "未执行"
);
}
// VO转实体逻辑
SalesApDisplay updatedEntity = new SalesApDisplay();
BeanUtils.copyProperties(salesApDisplayVo, updatedEntity);
updateEntityList.add(updatedEntity);
}
return updateEntityList;
}
@Override
public Map<String, Object> execute(String filePathUrl) {
Map<String, Object> resultMap = new HashMap<>(5);
InputStream inputStream = null;
try {
// 获取文件流(修改:文件流为空时构建错误DTO)
inputStream = ExcelUtils.getOssFileInputStream(filePathUrl);
if (inputStream == null) {
SalesApDisplayImportExcelDto errorDto = buildSimpleErrorDto("文件流获取失败:OSS文件不存在或权限不足");
List<SalesApDisplayImportExcelDto> errorList = Collections.singletonList(errorDto);
resultMap.put("uuid", UUID.randomUUID().toString());
resultMap.put("table", errorList);
resultMap.put("successCount", 0);
resultMap.put("failCount", errorList.size());
resultMap.put("errorMsg", "文件流获取失败:OSS文件不存在或权限不足");
return resultMap;
}
// 解析Excel
Map<String, List<SalesApDisplayImportExcelDto>> tempErrorMap = new HashMap<>();
List<SalesApDisplayImportExcelDto> dtoList = parseExcelToDtoList(inputStream, filePathUrl, tempErrorMap);
// 数据预处理
preprocessData(dtoList);
//批量验证/操作)
int successCount = queryAndBatchOperate(dtoList);
// 5封装返回结果
resultMap.put("uuid", UUID.randomUUID().toString());
resultMap.put("table", dtoList);
resultMap.put("successCount", successCount);
resultMap.put("failCount", dtoList.size() - successCount);
resultMap.put("errorMsg", "");
} catch (Exception e) {
log.error("常规陈列导入失败,filePathUrl={}", filePathUrl, e);
// 修改:异常时构建错误DTO,存入table
SalesApDisplayImportExcelDto errorDto = buildSimpleErrorDto("导入失败:" + e.getMessage());
List<SalesApDisplayImportExcelDto> errorList = Collections.singletonList(errorDto);
resultMap.put("uuid", UUID.randomUUID().toString());
resultMap.put("table", errorList);
resultMap.put("successCount", 0);
resultMap.put("failCount", errorList.size());
resultMap.put("errorMsg", errorList.get(0).getErrorMsg());
} finally {
// 关闭流(不变)
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception e) {
log.error("关闭文件流失败", e);
}
}
}
return resultMap;
}
@Override
public List<SalesApDisplay> queryData(List<SalesApDisplayImportExcelDto> dto) {
// 查询数据库 此处查询正常只会返回一个对象
return salesApQueryDisplayService.queryByCondition(dto);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String updateDisplay(List<SalesApDisplayImportExcelDto> dtoList) {
if (dtoList == null || dtoList.isEmpty()) {
return "更新失败";
}
List<SalesApDisplay> salesApDisplayList = buildUpdateEntityList(dtoList);
if (salesApDisplayList.isEmpty()) {
return "更新失败";
}
int i = salesApDisplayCoreService.batchUpdate(salesApDisplayList);
return i > 0 ? "更新成功" : "更新失败";
}
@Override
public List<SalesApDisplayImportExcelDto> getTransactionJsonToObject(String json) {
if (StringUtils.isBlank(json)) {
return Collections.emptyList();
}
try {
JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject();
// 获取table字段
if (jsonObject.has("table") && !jsonObject.get("table").isJsonNull()) {
Gson gson = new Gson();
String tableJson = jsonObject.get("table").toString();
return gson.fromJson(tableJson, new TypeToken<List<SalesApDisplayImportExcelDto>>(){}.getType());
}
return Collections.emptyList();
} catch (Exception e) {
log.error("解析JSON失败: {}", json, e);
return Collections.emptyList();
}
}
/**
* 统一构建组合匹配键(保证DTO和查询结果的键规则一致)
*/
private String buildMatchKey(Long sadId, String regionName, String dealerName, String lineName) {
return (sadId == null ? "" : sadId) + "_"
+ (regionName == null ? "" : regionName.trim()) + "_"
+ (dealerName == null ? "" : dealerName.trim()) + "_"
+ (lineName == null ? "" : lineName.trim());
}
/**
* 构建错误DTO(仅用于参数异常场景)
*/
public SalesApDisplayImportExcelDto buildSimpleErrorDto(String errorMsg) {
SalesApDisplayImportExcelDto dto = new SalesApDisplayImportExcelDto();
dto.setErrorMsg(errorMsg);
return dto;
}
}
\ No newline at end of file
package com.sfa.operation.util.excel; package com.sfa.operation.util.excel;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.GetObjectRequest;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.auth.sts.AssumeRoleRequest;
import com.aliyuncs.auth.sts.AssumeRoleResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.sfa.operation.config.ExportColumnConfig; import com.sfa.operation.config.ExportColumnConfig;
import com.sfa.operation.config.OssConfigProperties;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList; import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.xssf.streaming.SXSSFWorkbook; import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired;
import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream; import java.io.*;
import java.io.IOException; import java.math.BigDecimal;
import java.io.OutputStream; import java.net.URLDecoder;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
/** /**
* @Author: DouXinYu * @Author: DouXinYu
...@@ -29,10 +45,315 @@ import java.util.concurrent.ConcurrentHashMap; ...@@ -29,10 +45,315 @@ import java.util.concurrent.ConcurrentHashMap;
* @Description: AP导出excel工具类 * @Description: AP导出excel工具类
*/ */
@Slf4j @Slf4j
@Component
public class ExcelUtils { public class ExcelUtils {
//缓存样式
private static final Logger logger = LoggerFactory.getLogger(ExcelUtils.class);
private static final ConcurrentHashMap<String, CellStyle> CACHE_CELL_STYLE = new ConcurrentHashMap<>(); private static final ConcurrentHashMap<String, CellStyle> CACHE_CELL_STYLE = new ConcurrentHashMap<>();
//表头行(默认为0)
private static final int HEADER_ROW = 0;
private static OssConfigProperties staticOssConfig;
//OSS配置
@Autowired
private OssConfigProperties ossConfig;
@PostConstruct
public void init() {
staticOssConfig = this.ossConfig;
}
/* ============================================================导入部分=========================================================== */
public static InputStream getLocalFileInputStream(String filePath) throws Exception {
// 参数校验
if (StringUtils.isBlank(filePath)) {
throw new IllegalArgumentException("本地文件路径不能为空");
}
File localFile = new File(filePath);
// 检查文件是否存在
if (!localFile.exists()) {
String errorMsg = String.format("本地文件不存在:%s", filePath);
log.error(errorMsg);
throw new Exception(errorMsg);
}
// 检查是否是文件(而非目录)
if (!localFile.isFile()) {
String errorMsg = String.format("路径不是有效文件:%s", filePath);
log.error(errorMsg);
throw new Exception(errorMsg);
}
// 检查文件可读权限
if (!localFile.canRead()) {
String errorMsg = String.format("本地文件不可读取(权限不足):%s", filePath);
log.error(errorMsg);
throw new Exception(errorMsg);
}
try {
// 创建文件输入流并返回
FileInputStream fis = new FileInputStream(localFile);
log.info("成功获取本地文件输入流:{}", filePath);
return fis;
} catch (IOException e) {
String errorMsg = String.format("读取本地文件失败:%s,错误:%s", filePath, e.getMessage());
log.error(errorMsg, e);
throw new Exception(errorMsg, e);
}
}
/**
* 获取OSS文件输入流
* @param ossFileUrl OSS文件URL
* @return OSS文件输入流
* @throws Exception 获取OSS文件输入流失败
*/
public static InputStream getOssFileInputStream(String ossFileUrl) throws Exception {
// 参数校验
if (StringUtils.isBlank(ossFileUrl)) {
throw new IllegalArgumentException("OSS文件URL不能为空");
}
OssConfigProperties.Oss oss = staticOssConfig.getOss();
if (oss == null || StringUtils.isBlank(oss.getRegionId()) || StringUtils.isBlank(oss.getBucketName())) {
throw new IllegalStateException("OSS基础配置未初始化(regionId/bucketName为空)");
}
// 解析OSS URL:提取文件路径(objectKey)
String objectKey = parseObjectKeyFromUrl(ossFileUrl, oss.getWebJsLink());
if (StringUtils.isBlank(objectKey)) {
throw new Exception("解析OSS文件路径失败:" + ossFileUrl);
}
//获取STS临时凭证
AssumeRoleResponse.Credentials stsCredentials = getStsCredentials();
//用STS临时凭证创建OSS客户端
OSS ossClient = null;
try {
// Endpoint
ossClient = new OSSClientBuilder().build(
"https://oss-" + oss.getRegionId() + ".aliyuncs.com",
// STS临时AK
stsCredentials.getAccessKeyId(),
// STS临时SK
stsCredentials.getAccessKeySecret(),
// STS Token
stsCredentials.getSecurityToken()
);
// 校验文件是否存在
if (!ossClient.doesObjectExist(oss.getBucketName(), objectKey)) {
throw new Exception(String.format("OSS文件不存在:bucket=%s, path=%s", oss.getBucketName(), objectKey));
}
// 获取输入流(调用时注意关闭流)
return ossClient.getObject(new GetObjectRequest(oss.getBucketName(), objectKey)).getObjectContent();
} catch (Exception e) {
log.error("获取OSS文件失败:url={}", ossFileUrl, e);
throw new Exception("OSS文件读取失败:" + e.getMessage(), e);
} finally {
if (ossClient != null) {
// 关闭客户端
ossClient.shutdown();
}
}
}
/**
* 导入数据
* @param inputStream 导入文件的输入流
* @param fileName 导入文件名
* @param columnConfigs 导入列配置
* @param errorMap 错误信息
* @param dtoClass 目标DTO类型
* @return 导入数据
* @param <T> 目标DTO泛型
*/
public static <T> List<T> readApExcelWithColumnConfig(InputStream inputStream,
String fileName,
List<ExportColumnConfig> columnConfigs,
Map<String, List<T>> errorMap,
Class<T> dtoClass) {
log.info("开始导入数据(复用列配置校验),目标DTO类型:{}", dtoClass.getSimpleName());
// 初始化空错误列表(基础方法需要)
List<String> baseErrorList = new ArrayList<>();
// 调用基础导入方法,传入自定义行解析器
List<T> dtoList = readApExcel(inputStream, fileName, baseErrorList, (row, rowNum) -> {
// 初始化当前行数据和错误信息
List<T> rowData = new ArrayList<>();
StringBuilder rowErrorMsg = new StringBuilder();
T dto = null;
// 反射创建DTO实例
try {
dto = dtoClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
log.error("创建DTO实例失败,类型:{},错误:{}", dtoClass.getName(), e.getMessage(), e);
return null;
}
// 逐列解析+校验(复用ExportColumnConfig规则)
for (int colIndex = 0; colIndex < columnConfigs.size(); colIndex++) {
ExportColumnConfig config = columnConfigs.get(colIndex);
String fieldName = config.getFieldName();
Cell cell = row.getCell(colIndex);
String cellValue = getCellStringValue(cell);
// 记录原始行数据,用于错误Map
rowData.add((T) cellValue);
// 字段赋值(兼容数值/字符串类型)
try {
if (StringUtils.isNotBlank(cellValue)) {
if (config.isNumberValidation()) {
// 数值类型字段:转换为BigDecimal赋值
Double numericValue = Double.parseDouble(cellValue);
BeanUtil.setProperty(dto, fieldName, new BigDecimal(numericValue));
} else {
// 字符串类型字段:直接赋值
BeanUtil.setProperty(dto, fieldName, cellValue);
}
}
} catch (Exception e) {
log.warn("字段赋值失败,字段名:{},值:{},错误:{}", fieldName, cellValue, e.getMessage());
}
// 原有校验规则(数值范围/枚举值)
if (config.isNumberValidation() && StringUtils.isNotBlank(cellValue)) {
// 数值范围校验
try {
BigDecimal value = new BigDecimal(cellValue);
if (value.compareTo(new BigDecimal(config.getValidationNumberMin())) < 0
|| value.compareTo(new BigDecimal(config.getValidationNumberMax())) > 0) {
rowErrorMsg.append(config.getValidationErrorMsg()).append(";");
}
} catch (Exception e) {
rowErrorMsg.append(config.getValidationErrorMsg()).append(";");
}
} else if (CollectionUtils.isNotEmpty(config.getValidationValidOptions()) && StringUtils.isNotBlank(cellValue)) {
// 枚举值校验
if (!config.getValidationValidOptions().contains(cellValue)) {
rowErrorMsg.append(config.getValidationErrorMsg()).append(";");
}
}
}
// 收集错误
if (rowErrorMsg.length() > 0) {
String errorKey = rowErrorMsg.toString();
errorMap.computeIfAbsent(errorKey, k -> new ArrayList<>()).addAll(rowData);
// 给DTO设置错误信息(需DTO包含errorMsg字段)
try {
BeanUtil.setProperty(dto, "errorMsg", errorKey);
} catch (Exception e) {
log.warn("DTO[{}]无errorMsg字段,跳过错误信息赋值", dtoClass.getSimpleName());
}
} else {
// 即使没有错误,也要确保errorMsg字段有默认值
try {
BeanUtil.setProperty(dto, "errorMsg", "");
} catch (Exception e) {
log.debug("DTO[{}]无errorMsg字段或设置默认值失败", dtoClass.getSimpleName());
}
}
// 返回DTO(即使有错误也返回)
return dto;
});
// 打印基础错误(如文件格式、空Sheet)
if (CollectionUtils.isNotEmpty(baseErrorList)) {
log.warn("导入基础校验错误:{}", String.join(";", baseErrorList));
throw new RuntimeException(String.join(";", baseErrorList));
}
return dtoList;
}
/**
* 基础AP Excel导入方法(通用行解析,无业务耦合)
* @param inputStream Excel文件流
* @param fileName 文件名(校验格式)
* @param errorList 基础错误列表(文件格式、空Sheet等)
* @param rowParser 行解析器(自定义每行解析逻辑)
* @param <T> 解析结果类型
* @return 解析后的列表
*/
private static <T> List<T> readApExcel(InputStream inputStream,
String fileName,
List<String> errorList,
BiFunction<Row, Integer, T> rowParser) {
log.info("开始基础Excel导入解析,文件名:{}", fileName);
List<T> dataList = new ArrayList<>();
Workbook workbook = null;
try {
// 1. 校验文件格式
if (fileName == null || (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx"))) {
errorList.add("文件格式错误,仅支持.xls和.xlsx");
return dataList;
}
// 2. 创建Workbook
if (fileName.endsWith(".xls")) {
workbook = new HSSFWorkbook(inputStream);
} else {
workbook = new XSSFWorkbook(inputStream);
}
// 3. 校验Sheet
Sheet sheet = workbook.getSheetAt(0);
if (sheet == null) {
errorList.add("获取sheet为空,导入失败,请检查文件!");
return dataList;
}
int lastRowNum = sheet.getLastRowNum();
if (lastRowNum <= HEADER_ROW) {
errorList.add("获取数据行数小于等于表头行数,文件内无内容,导入失败,请检查文件!");
return dataList;
}
// 4. 逐行解析
for (int rowIndex = HEADER_ROW + 1; rowIndex <= lastRowNum; rowIndex++) {
Row row = sheet.getRow(rowIndex);
if (row == null) {
// 调整:移除行号,仅保留纯错误提示
errorList.add("数据行为空,跳过处理");
continue;
}
T rowData = rowParser.apply(row, rowIndex + 1);
if (rowData != null) {
dataList.add(rowData);
}
}
} catch (IOException e) {
log.error("导入Excel文件失败:{}", e.getMessage(), e);
errorList.add("文件读取失败:" + e.getMessage());
} finally {
// 5. 关闭流
try {
if (workbook != null) {
workbook.close();
}
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
log.error("Excel流关闭异常", e);
}
}
return dataList;
}
/* ============================================================导出部分=========================================================== */
/** /**
* 将Excel字节数组导出为文件(写入Http响应流) * 将Excel字节数组导出为文件(写入Http响应流)
...@@ -70,13 +391,13 @@ public class ExcelUtils { ...@@ -70,13 +391,13 @@ public class ExcelUtils {
outputStream.write(excelBytes); outputStream.write(excelBytes);
outputStream.flush(); outputStream.flush();
} catch (Exception e) { } catch (Exception e) {
logger.error("写入Excel响应流失败:{}", e.getMessage(), e); log.error("写入Excel响应流失败:{}", e.getMessage(), e);
throw new Exception("导出Excel失败:" + e.getMessage()); throw new Exception("导出Excel失败:" + e.getMessage());
} }
} }
/** /**
* 生成Excel文件 * 生成Excel字节流
* *
* @param exportColumnConfigList 表头配置 * @param exportColumnConfigList 表头配置
* @param dataList 数据列表 * @param dataList 数据列表
...@@ -112,7 +433,7 @@ public class ExcelUtils { ...@@ -112,7 +433,7 @@ public class ExcelUtils {
for (int i = 0; i < exportColumnConfigList.size(); i++) { for (int i = 0; i < exportColumnConfigList.size(); i++) {
ExportColumnConfig config = exportColumnConfigList.get(i); ExportColumnConfig config = exportColumnConfigList.get(i);
// 给列配置数据验证(跳过表头) // 给列配置数据验证(跳过表头)
ExcelStyleUtils.addColumnDataValidation(sheet,i, config); ExcelStyleUtils.addColumnDataValidation(sheet, i, config);
if (isColumnProtected(config)) { if (isColumnProtected(config)) {
// 为该列所有数据行添加只读验证 // 为该列所有数据行添加只读验证
// 从第1行开始(跳过表头) // 从第1行开始(跳过表头)
...@@ -130,12 +451,14 @@ public class ExcelUtils { ...@@ -130,12 +451,14 @@ public class ExcelUtils {
} }
} catch (IOException e) { } catch (IOException e) {
logger.error("生成Excel文件失败:{}", e.getMessage(), e); log.error("生成Excel文件失败:{}", e.getMessage(), e);
throw new IOException("生成Excel文件失败:" + e.getMessage()); throw new IOException("生成Excel文件失败:" + e.getMessage());
} }
} }
/* ============================================================导出工具方法=========================================================== */
/** /**
* 创建工作簿 * 创建工作簿
* *
...@@ -188,11 +511,11 @@ public class ExcelUtils { ...@@ -188,11 +511,11 @@ public class ExcelUtils {
// 为表头行添加只读验证(防止用户修改表头) // 为表头行添加只读验证(防止用户修改表头)
for (int i = 0; i < exportColumnConfigList.size(); i++) { for (int i = 0; i < exportColumnConfigList.size(); i++) {
ExportColumnConfig exportColumnConfig = exportColumnConfigList.get(i); ExportColumnConfig exportColumnConfig = exportColumnConfigList.get(i);
if (exportColumnConfig.getValidationPromptTitle() != null && exportColumnConfig.getValidationPromptMsg() != null){ if (exportColumnConfig.getValidationPromptTitle() != null && exportColumnConfig.getValidationPromptMsg() != null) {
addHeaderPromptValidation(sheet, i, exportColumnConfig); addHeaderPromptValidation(sheet, i, exportColumnConfig);
} }
// 只保护第0行(表头行) // 只保护第0行(表头行)
addReadOnlyValidation(sheet, i, 0, 0); addReadOnlyValidation(sheet, i, HEADER_ROW, HEADER_ROW);
} }
} }
...@@ -274,7 +597,7 @@ public class ExcelUtils { ...@@ -274,7 +597,7 @@ public class ExcelUtils {
DataValidationConstraint constraint = helper.createCustomConstraint("FALSE"); DataValidationConstraint constraint = helper.createCustomConstraint("FALSE");
// 仅对表头行(第0行)应用 // 仅对表头行(第0行)应用
CellRangeAddressList addressList = new CellRangeAddressList(0, 0, columnIndex, columnIndex); CellRangeAddressList addressList = new CellRangeAddressList(HEADER_ROW, HEADER_ROW, columnIndex, columnIndex);
DataValidation validation = helper.createValidation(constraint, addressList); DataValidation validation = helper.createValidation(constraint, addressList);
...@@ -306,7 +629,7 @@ public class ExcelUtils { ...@@ -306,7 +629,7 @@ public class ExcelUtils {
// 使用 BeanUtils 获取属性值 // 使用 BeanUtils 获取属性值
return BeanUtil.getProperty(data, fieldName); return BeanUtil.getProperty(data, fieldName);
} catch (Exception e) { } catch (Exception e) {
logger.error("获取字段值失败:{}", fieldName, e); log.error("获取字段值失败:{}", fieldName, e);
// 空值兜底,避免崩溃 // 空值兜底,避免崩溃
return ""; return "";
} }
...@@ -414,19 +737,19 @@ public class ExcelUtils { ...@@ -414,19 +737,19 @@ public class ExcelUtils {
return ""; return "";
} }
// 替换中文括号及其内容为换行形式 // 替换中文括号及其内容为换行形式
String result ; String result;
if (originalText.contains("(") && originalText.contains(")")) { if (originalText.contains("(") && originalText.contains(")")) {
// 中文括号替换为换行 // 中文括号替换为换行
result = originalText.replaceAll("((.*?))", "\n$1"); result = originalText.replaceAll("((.*?))", "\n$1");
} else if (originalText.contains("(") && originalText.contains(")")){ } else if (originalText.contains("(") && originalText.contains(")")) {
// 英文括号替换为换行 // 英文括号替换为换行
result = originalText.replaceAll("\\((.*?)\\)", "\n$1"); result = originalText.replaceAll("\\((.*?)\\)", "\n$1");
}else if (originalText.contains("-")){ } else if (originalText.contains("-")) {
// 横线替换为换行(内容) // 横线替换为换行(内容)
result = originalText.replaceAll("-(.+)", "\n($1)"); result = originalText.replaceAll("-(.+)", "\n($1)");
}else if (originalText.contains("_")){ } else if (originalText.contains("_")) {
result = originalText.replaceAll("_(.+)", "\n($1)"); result = originalText.replaceAll("_(.+)", "\n($1)");
}else { } else {
result = originalText; result = originalText;
} }
...@@ -469,5 +792,152 @@ public class ExcelUtils { ...@@ -469,5 +792,152 @@ public class ExcelUtils {
return config.getStyle() == ExcelStyleUtils.ExcelStyle.UNMODIFIABLE || config.getStyle() == ExcelStyleUtils.ExcelStyle.CHANGE_TEXT_STYLE; return config.getStyle() == ExcelStyleUtils.ExcelStyle.UNMODIFIABLE || config.getStyle() == ExcelStyleUtils.ExcelStyle.CHANGE_TEXT_STYLE;
} }
/* ================================================导入工具方法=================================================== */
/**
* 获取单元格内容
*
* @param cell 单元格
* @return 单元格内容
*/
public static String getCellStringValue(Cell cell) {
if (cell == null) {
return "";
}
CellType cellType = cell.getCellType();
switch (cellType) {
case STRING:
return cell.getStringCellValue().trim();
case NUMERIC:
// 处理日期/数字
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue().toString();
} else {
// 避免科学计数法,去除末尾的.0
return String.valueOf(cell.getNumericCellValue()).replaceAll("\\.0*$", "");
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
try {
return String.valueOf(cell.getNumericCellValue()).replaceAll("\\.0*$", "");
} catch (IllegalStateException e) {
return cell.getStringCellValue().trim();
}
default:
return "";
}
}
/**
* 判断单元格是否有值(与导出时的条件样式判断逻辑完全一致)
* @param cell 单元格
* @return true=有值,false=无值
*/
private static boolean isCellHasValue(Cell cell) {
if (cell == null) {
return false;
}
switch (cell.getCellType()) {
case STRING:
return StringUtils.isNotBlank(cell.getStringCellValue().trim());
case NUMERIC:
// 数值类型无论值是多少都算有值
return true;
case BOOLEAN:
// 布尔类型算有值
return true;
default:
return false;
}
}
/**
* 获取STS临时凭证
* @return STS临时凭证
* @throws ClientException
*/
private static AssumeRoleResponse.Credentials getStsCredentials() throws ClientException {
OssConfigProperties.Oss oss = staticOssConfig.getOss();
// 构建STS客户端
IClientProfile profile = DefaultProfile.getProfile(
oss.getRegionId(),
staticOssConfig.getAccessKeyId(),
staticOssConfig.getAccessKeySecret()
);
DefaultAcsClient client = new DefaultAcsClient(profile);
// 构建STS请求
AssumeRoleRequest request = new AssumeRoleRequest();
// POST方式发送请求
request.setMethod(MethodType.POST);
// 你的STS角色ARN
request.setRoleArn(oss.getStsRoleArm());
request.setRoleSessionName(oss.getSessionName());
// 获取STS响应
AssumeRoleResponse response = client.getAcsResponse(request);
AssumeRoleResponse.Credentials credentials = response.getCredentials();
if (credentials == null) {
throw new ClientException("STS临时凭证获取失败");
}
log.info("STS临时凭证获取成功,过期时间:{}", credentials.getExpiration());
return credentials;
}
/**
* 从OSS文件URL中解析出ObjectKey
* @param ossFileUrl OSS文件URL
* @param ossDomain OSS域名
* @return ObjectKey
*/
private static String parseObjectKeyFromUrl(String ossFileUrl, String ossDomain) {
if (StringUtils.isBlank(ossFileUrl) || StringUtils.isBlank(ossDomain)) {
return null;
}
String domainPrefix = "//" + ossDomain + "/";
if (ossFileUrl.contains(domainPrefix)) {
return ossFileUrl.split(domainPrefix)[1];
}
// 兼容直接传入objectKey的场景(如path/file.xlsx)
if (!ossFileUrl.startsWith("http")) {
return ossFileUrl;
}
return null;
}
/**
* 从OSS文件URL中解析出文件名
* @param ossUrl OSS文件URL
* @return 文件名
*/
public static String parseFileNameFromOssUrl(String ossUrl) {
//参数校验
if (StringUtils.isBlank(ossUrl) || !ossUrl.contains("oss-") || !ossUrl.contains(".aliyuncs.com")) {
log.warn("非有效阿里云OSS URL,使用通用默认文件名:{}", ossUrl);
// 通用默认名,不绑定具体业务
return "Excel导入文件";
}
// 解析文件名:按"/"分割取URL最后一段
String[] urlParts = ossUrl.split("/");
String fileName = urlParts[urlParts.length - 1];
// - 解析结果为空 → 通用前缀+时间戳+默认后缀
// - 无Excel后缀 → 补充.xlsx(适配Excel导入通用场景)
if (StringUtils.isBlank(fileName)) {
fileName = "Excel导入文件_" + System.currentTimeMillis() + ".xlsx";
} else if (!fileName.endsWith(".xlsx") && !fileName.endsWith(".xls")) {
// 保留原文件名前缀 + 补充Excel后缀(更友好)
fileName = fileName + ".xlsx";
}
log.info("从OSS URL解析通用文件名完成:{} → {}", ossUrl, fileName);
return fileName;
}
} }
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论