Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
W
wangxiaolu-sfa-module-operation
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
sfa
wangxiaolu-sfa-module-operation
Commits
a53d3dea
提交
a53d3dea
authored
12月 15, 2025
作者:
000516
浏览文件
操作
浏览文件
下载
差异文件
解决与QA导入导出功能冲突
上级
e424d35d
2f584700
隐藏空白字符变更
内嵌
并排
正在显示
32 个修改的文件
包含
3042 行增加
和
1 行删除
+3042
-1
pom.xml
pom.xml
+5
-0
ExportColumnConfig.java
...ain/java/com/sfa/operation/config/ExportColumnConfig.java
+64
-0
OssConfigProperties.java
...in/java/com/sfa/operation/config/OssConfigProperties.java
+74
-0
ApExportExcelController.java
...ation/controller/sales/excel/ApExportExcelController.java
+38
-0
ApImportExcelController.java
...ation/controller/sales/excel/ApImportExcelController.java
+36
-0
ISalesApDisplayDao.java
...om/sfa/operation/domain/sales/dao/ISalesApDisplayDao.java
+10
-0
SalesApDisplayDaoImpl.java
...peration/domain/sales/dao/impl/SalesApDisplayDaoImpl.java
+57
-1
SalesApDisplayMapper.java
...a/operation/domain/sales/mapper/SalesApDisplayMapper.java
+2
-0
ExportAPType.java
src/main/java/com/sfa/operation/enums/ExportAPType.java
+37
-0
ImportApType.java
src/main/java/com/sfa/operation/enums/ImportApType.java
+37
-0
ApExportExcelStrategyFactory.java
...m/sfa/operation/factory/ApExportExcelStrategyFactory.java
+53
-0
ApImportExcelStrategyFactory.java
...m/sfa/operation/factory/ApImportExcelStrategyFactory.java
+61
-0
SalesApDisplayImportExcelDto.java
...ration/pojo/sales/excel/SalesApDisplayImportExcelDto.java
+169
-0
ImportApExcelRequest.java
...fa/operation/pojo/sales/request/ImportApExcelRequest.java
+15
-0
SalesApRequest.java
.../com/sfa/operation/pojo/sales/request/SalesApRequest.java
+10
-0
SalesApDisplayVo.java
...ava/com/sfa/operation/pojo/sales/vo/SalesApDisplayVo.java
+93
-0
IApDisplayCoreService.java
...om/sfa/operation/service/sales/IApDisplayCoreService.java
+5
-0
IApDisplayQueryService.java
...m/sfa/operation/service/sales/IApDisplayQueryService.java
+4
-0
IExportExcelService.java
...a/operation/service/sales/export/IExportExcelService.java
+17
-0
IImportExcelService.java
...a/operation/service/sales/export/IImportExcelService.java
+16
-0
ExportExcelServiceImpl.java
...ion/service/sales/export/impl/ExportExcelServiceImpl.java
+44
-0
ImportExcelServiceImpl.java
...ion/service/sales/export/impl/ImportExcelServiceImpl.java
+114
-0
ApDisplayCoreServiceImpl.java
...peration/service/sales/impl/ApDisplayCoreServiceImpl.java
+14
-0
ApDisplayQueryServiceImpl.java
...eration/service/sales/impl/ApDisplayQueryServiceImpl.java
+14
-0
IExportApExcelStrategy.java
...va/com/sfa/operation/strategy/IExportApExcelStrategy.java
+55
-0
IImportApExcelStrategy.java
...va/com/sfa/operation/strategy/IImportApExcelStrategy.java
+69
-0
NormalDisplayExportStrategyImpl.java
...trategy/impl/exports/NormalDisplayExportStrategyImpl.java
+141
-0
NormalDisplayImportStrategyImpl.java
...trategy/impl/imports/NormalDisplayImportStrategyImpl.java
+366
-0
EntityUpdateCheckUtil.java
...a/com/sfa/operation/util/excel/EntityUpdateCheckUtil.java
+68
-0
ExcelStyleUtils.java
...in/java/com/sfa/operation/util/excel/ExcelStyleUtils.java
+279
-0
ExcelUtils.java
src/main/java/com/sfa/operation/util/excel/ExcelUtils.java
+984
-0
SalesApDisplayMapper.xml
src/main/resources/mapper/sales/SalesApDisplayMapper.xml
+91
-0
没有找到文件。
pom.xml
浏览文件 @
a53d3dea
...
...
@@ -94,6 +94,11 @@
<groupId>
com.taobao
</groupId>
<artifactId>
taobao-sdk-java
</artifactId>
</dependency>
<dependency>
<groupId>
com.aliyun.oss
</groupId>
<artifactId>
aliyun-sdk-oss
</artifactId>
</dependency>
</dependencies>
<build>
...
...
src/main/java/com/sfa/operation/config/ExportColumnConfig.java
0 → 100644
浏览文件 @
a53d3dea
package
com
.
sfa
.
operation
.
config
;
import
com.sfa.operation.util.excel.ExcelStyleUtils
;
import
lombok.AllArgsConstructor
;
import
lombok.Data
;
import
lombok.NoArgsConstructor
;
import
org.apache.poi.ss.usermodel.DataValidation
;
import
java.util.List
;
/**
* @Author: DouXinYu
* @Date: 2025-12-08 16:43
* @Description: AP导出列配置
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public
class
ExportColumnConfig
{
// 字段名
private
String
fieldName
;
// 表头名
private
String
headerName
;
// 格式(如:日期 yyyy-mm)
private
String
format
;
// 列样式
private
ExcelStyleUtils
.
ExcelStyle
style
;
// 合法值列表(null/空则不验证)
private
List
<
String
>
validationValidOptions
;
// 输入提示标题(可选)
private
String
validationPromptTitle
;
// 输入提示内容(可选)
private
String
validationPromptMsg
;
// 错误提示标题(可选)
private
String
validationErrorTitle
;
// 错误提示内容(可选)
private
String
validationErrorMsg
;
// 错误级别(默认阻止非法输入)
private
int
validationErrorStyle
=
DataValidation
.
ErrorStyle
.
STOP
;
// 验证生效起始行(默认第1行,跳过表头)
private
int
validationStartRow
=
1
;
private
int
validationEndRow
=
1048575
;
// 数字最小值(null则不限制)
private
Double
validationNumberMin
;
// 数字最大值(null则不限制)
private
Double
validationNumberMax
;
// 是否允许小数(默认整数)
private
boolean
validationNumberAllowDecimal
=
false
;
// 标记:是否启用数字验证
private
boolean
isNumberValidation
=
false
;
// 标记:是否启用条件样式
private
boolean
conditionalStyling
=
false
;
public
ExportColumnConfig
(
String
fieldName
,
String
headerName
,
String
format
,
ExcelStyleUtils
.
ExcelStyle
style
)
{
this
.
fieldName
=
fieldName
;
this
.
headerName
=
headerName
;
this
.
format
=
format
;
this
.
style
=
style
;
}
}
src/main/java/com/sfa/operation/config/OssConfigProperties.java
0 → 100644
浏览文件 @
a53d3dea
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
;
}
}
}
src/main/java/com/sfa/operation/controller/sales/excel/ApExportExcelController.java
0 → 100644
浏览文件 @
a53d3dea
package
com
.
sfa
.
operation
.
controller
.
sales
.
excel
;
import
com.sfa.common.core.domain.R
;
import
com.sfa.common.core.enums.ECode
;
import
com.sfa.common.security.annotation.SalesPermissionCheck
;
import
com.sfa.operation.pojo.sales.request.SalesApRequest
;
import
com.sfa.operation.service.sales.export.IExportExcelService
;
import
com.sfa.operation.strategy.IExportApExcelStrategy
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.web.bind.annotation.*
;
import
javax.servlet.http.HttpServletResponse
;
/**
* @Author: DouXinYu
* @Date: 2025-12-05 17:31
* @Description: 导出excel控制类
*/
@Slf4j
@RestController
@RequestMapping
(
"/sales/export"
)
public
class
ApExportExcelController
{
@Autowired
private
IExportExcelService
exportApExcelService
;
/**
* 导出常规陈列的excel表格zh
* @param salesApRequest 查询参数
* @return 导出文件
*/
@GetMapping
(
"/download"
)
@SalesPermissionCheck
(
value
=
SalesPermissionCheck
.
CheckType
.
USER
)
public
R
exportApDisplayExcel
(
SalesApRequest
salesApRequest
,
HttpServletResponse
response
)
{
return
exportApExcelService
.
exportApDisplayExcel
(
salesApRequest
,
response
);
}
}
src/main/java/com/sfa/operation/controller/sales/excel/ApImportExcelController.java
0 → 100644
浏览文件 @
a53d3dea
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
);
}
}
src/main/java/com/sfa/operation/domain/sales/dao/ISalesApDisplayDao.java
浏览文件 @
a53d3dea
package
com
.
sfa
.
operation
.
domain
.
sales
.
dao
;
import
com.sfa.common.core.web.domain.PageInfo
;
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.response.SalesApDisplayDto
;
import
org.springframework.data.repository.query.Param
;
import
java.util.List
;
/**
* @author : liqiulin
...
...
@@ -19,4 +24,9 @@ public interface ISalesApDisplayDao {
Object
queryDeptAPReport
(
SalesApWq
build
);
Object
queryDistAPReport
(
SalesApWq
build
);
List
<
SalesApDisplay
>
queryDataListByCondition
(
SalesApWq
build
);
List
<
SalesApDisplay
>
queryByCondition
(
List
<
SalesApDisplayImportExcelDto
>
validDtoList
);
boolean
batchUpdate
(
List
<
SalesApDisplay
>
updateEntityList
);
}
src/main/java/com/sfa/operation/domain/sales/dao/impl/SalesApDisplayDaoImpl.java
浏览文件 @
a53d3dea
...
...
@@ -2,6 +2,7 @@ package com.sfa.operation.domain.sales.dao.impl;
import
com.baomidou.dynamic.datasource.annotation.DS
;
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.sfa.common.core.enums.ECode
;
import
com.sfa.common.core.exception.CheckedException
;
...
...
@@ -13,13 +14,17 @@ import com.sfa.operation.domain.sales.dao.ISalesApDisplayDao;
import
com.sfa.operation.domain.sales.entity.SalesApDisplay
;
import
com.sfa.operation.domain.sales.mapper.SalesApDisplayMapper
;
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.util.excel.EntityUpdateCheckUtil
;
import
org.springframework.beans.BeanUtils
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.stereotype.Service
;
import
org.springframework.transaction.annotation.Transactional
;
import
org.springframework.util.CollectionUtils
;
import
java.util.*
;
import
java.util.function.Consumer
;
/**
* @author : liqiulin
...
...
@@ -28,7 +33,7 @@ import java.util.*;
*/
@DS
(
"bi"
)
@Service
public
class
SalesApDisplayDaoImpl
implements
ISalesApDisplayDao
{
public
class
SalesApDisplayDaoImpl
implements
ISalesApDisplayDao
{
@Autowired
private
SalesApDisplayMapper
salesapdisMapper
;
...
...
@@ -79,6 +84,57 @@ public class SalesApDisplayDaoImpl implements ISalesApDisplayDao {
return
r
;
}
@Override
public
List
<
SalesApDisplay
>
queryDataListByCondition
(
SalesApWq
build
)
{
LambdaQueryWrapper
<
SalesApDisplay
>
queryWrapper
=
buildWq
(
build
);
List
<
SalesApDisplay
>
apDisplayList
=
salesapdisMapper
.
selectList
(
queryWrapper
);
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
boolean
batchUpdate
(
List
<
SalesApDisplay
>
updateEntityList
)
{
int
affectRows
=
0
;
List
<
List
<
SalesApDisplay
>>
batchList
=
EntityUpdateCheckUtil
.
splitList
(
updateEntityList
,
1000
);
for
(
List
<
SalesApDisplay
>
batch
:
batchList
)
{
// 批量更新
affectRows
=
salesapdisMapper
.
batchUpdate
(
batch
);
}
return
affectRows
>
0
;
}
private
LambdaQueryWrapper
<
SalesApDisplay
>
buildWq
(
SalesApWq
salesApWq
)
{
LambdaQueryWrapper
<
SalesApDisplay
>
qw
=
new
LambdaQueryWrapper
<>();
if
(
StringUtils
.
isNotBlank
(
salesApWq
.
getDealerCode
()))
{
...
...
src/main/java/com/sfa/operation/domain/sales/mapper/SalesApDisplayMapper.java
浏览文件 @
a53d3dea
...
...
@@ -3,6 +3,7 @@ package com.sfa.operation.domain.sales.mapper;
import
com.sfa.operation.domain.sales.entity.SalesApDisplay
;
import
com.baomidou.mybatisplus.core.mapper.BaseMapper
;
import
com.sfa.operation.domain.sales.wq.SalesApWq
;
import
org.springframework.data.repository.query.Param
;
import
org.springframework.stereotype.Repository
;
import
java.util.List
;
...
...
@@ -28,6 +29,7 @@ public interface SalesApDisplayMapper extends BaseMapper<SalesApDisplay> {
List
<
Map
<
String
,
Object
>>
queryDistAPHZReport
(
SalesApWq
build
);
List
<
Map
<
String
,
Object
>>
queryDeptAPHZReportDQ
(
SalesApWq
build
);
int
batchUpdate
(
@Param
(
"list"
)
List
<
SalesApDisplay
>
validEntities
);
}
...
...
src/main/java/com/sfa/operation/enums/ExportAPType.java
0 → 100644
浏览文件 @
a53d3dea
package
com
.
sfa
.
operation
.
enums
;
import
lombok.Getter
;
/**
* @Author: DouXinYu
* @Date: 2025-12-05 18:31
* @Description: ap导出页面类型枚举类(与策略对应)
*/
@Getter
public
enum
ExportAPType
{
/**
* 常规陈列策略
*/
NORMAL_DISPLAY_EXPORT
(
"normalDisplayExportStrategy"
),
/**
* 档期计划策略
*/
PROMOTION_PLAN_EXPORT
(
"promotionPlanExportStrategy"
),
/**
* 零食陈列策略
*/
SNACK_DISPLAY_EXPORT
(
"snackDisplayExportStrategy"
),
/**
* 三米两秒策略
*/
THREE_METER_TWO_SECONDS_EXPORT
(
"threeMeterTwoSecondsExportStrategy"
),
/**
* 六小金刚策略
*/
SIX_KINGkONG_EXPORT
(
"sixKingKongExportStrategy"
);
private
final
String
strategy
;
ExportAPType
(
String
strategy
)
{
this
.
strategy
=
strategy
;
}
}
src/main/java/com/sfa/operation/enums/ImportApType.java
0 → 100644
浏览文件 @
a53d3dea
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
;
}
}
src/main/java/com/sfa/operation/factory/ApExportExcelStrategyFactory.java
0 → 100644
浏览文件 @
a53d3dea
package
com
.
sfa
.
operation
.
factory
;
import
com.sfa.operation.enums.ExportAPType
;
import
com.sfa.operation.strategy.IExportApExcelStrategy
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.stereotype.Component
;
import
java.util.HashMap
;
import
java.util.Map
;
/**
* @Author: DouXinYu
* @Date: 2025-12-08 16:49
* @Description: AP导出excel策略工厂类
*/
@Slf4j
@Component
public
class
ApExportExcelStrategyFactory
{
private
final
Map
<
String
,
IExportApExcelStrategy
>
exportApExcelStrategyMap
;
//构造器注入
@Autowired
public
ApExportExcelStrategyFactory
(
Map
<
String
,
IExportApExcelStrategy
>
exportApExcelStrategyMap
)
{
this
.
exportApExcelStrategyMap
=
exportApExcelStrategyMap
;
}
public
IExportApExcelStrategy
getStrategy
(
String
exportApType
)
{
if
(
exportApType
==
null
||
exportApType
.
trim
().
isEmpty
())
{
log
.
error
(
"AP导出Excel策略工厂:传入的导出类型为空!"
);
throw
new
IllegalArgumentException
(
"传入的导出类型为空!"
);
}
ExportAPType
typeEnum
;
try
{
typeEnum
=
ExportAPType
.
valueOf
(
exportApType
.
toUpperCase
());
}
catch
(
IllegalArgumentException
e
)
{
log
.
error
(
"AP导出Excel策略工厂:传入的导出类型不存在!目标类型为:{}"
,
exportApType
);
throw
new
IllegalArgumentException
(
"传入的导出类型不存在!"
);
}
// 从枚举中获取策略Bean名称,再从Map中查找策略
String
strategyBeanName
=
typeEnum
.
getStrategy
();
IExportApExcelStrategy
strategy
=
exportApExcelStrategyMap
.
get
(
strategyBeanName
);
if
(
strategy
==
null
)
{
log
.
error
(
"AP导出Excel策略工厂:未找到对应的导出策略!目标策略Bean名称为:{}"
,
strategyBeanName
);
throw
new
IllegalArgumentException
(
"未找到对应的导出策略!"
);
}
log
.
info
(
"AP导出Excel策略工厂:找到对应的导出策略!目标策略Bean名称为:{}"
,
strategyBeanName
);
return
strategy
;
}
}
src/main/java/com/sfa/operation/factory/ApImportExcelStrategyFactory.java
0 → 100644
浏览文件 @
a53d3dea
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
;
}
}
src/main/java/com/sfa/operation/pojo/sales/excel/SalesApDisplayImportExcelDto.java
0 → 100644
浏览文件 @
a53d3dea
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
;
/**
* 备注
* 类型:String
*/
private
String
remark
;
}
src/main/java/com/sfa/operation/pojo/sales/request/ImportApExcelRequest.java
0 → 100644
浏览文件 @
a53d3dea
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
;
}
src/main/java/com/sfa/operation/pojo/sales/request/SalesApRequest.java
浏览文件 @
a53d3dea
...
...
@@ -62,6 +62,16 @@ public class SalesApRequest {
*/
private
String
rqStatus
;
/**
* 当前查询的页面类型
* 1.常规陈列 :NORMAL_DISPLAY_EXPORT
* 2.档期计划 :PROMOTION_PLAN_EXPORT
* 3.零食陈列 :SNACK_DISPLAY_EXPORT
* 4.三米两秒 :THREE_METER_TWO_SECONDS_EXPORT
* 5.六小金刚 :SIX_KINGkONG_EXPORT
*/
private
String
pageType
;
// ######################## 通用查询 ########################
/**
...
...
src/main/java/com/sfa/operation/pojo/sales/vo/SalesApDisplayVo.java
0 → 100644
浏览文件 @
a53d3dea
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
;
/**
* 备注信息
*
*/
private
String
remark
;
}
src/main/java/com/sfa/operation/service/sales/IApDisplayCoreService.java
浏览文件 @
a53d3dea
package
com
.
sfa
.
operation
.
service
.
sales
;
import
com.sfa.operation.domain.sales.entity.SalesApDisplay
;
import
com.sfa.operation.pojo.sales.request.SalesApRequest
;
import
java.util.List
;
/**
* @author : liqiulin
* @date : 2025-09-16 16
...
...
@@ -19,4 +22,6 @@ public interface IApDisplayCoreService {
void
putDisplayJDetail
(
SalesApRequest
request
);
void
putPromotionDetail
(
SalesApRequest
request
);
boolean
batchUpdate
(
List
<
SalesApDisplay
>
updateEntityList
);
}
src/main/java/com/sfa/operation/service/sales/IApDisplayQueryService.java
浏览文件 @
a53d3dea
package
com
.
sfa
.
operation
.
service
.
sales
;
import
com.sfa.common.core.web.domain.PageInfo
;
import
com.sfa.operation.domain.sales.entity.SalesApDisplay
;
import
com.sfa.operation.pojo.sales.excel.SalesApDisplayImportExcelDto
;
import
com.sfa.operation.pojo.sales.request.SalesApRequest
;
import
java.util.List
;
...
...
@@ -28,4 +30,6 @@ public interface IApDisplayQueryService {
Object
queryDeptAPReport
(
SalesApRequest
request
);
Object
queryDistAPReport
(
SalesApRequest
request
);
List
<
SalesApDisplay
>
queryDataListByCondition
(
SalesApRequest
build
);
List
<
SalesApDisplay
>
queryByCondition
(
List
<
SalesApDisplayImportExcelDto
>
queryParam
);
}
src/main/java/com/sfa/operation/service/sales/export/IExportExcelService.java
0 → 100644
浏览文件 @
a53d3dea
package
com
.
sfa
.
operation
.
service
.
sales
.
export
;
import
com.sfa.common.core.domain.R
;
import
com.sfa.operation.pojo.sales.request.SalesApRequest
;
import
javax.servlet.http.HttpServletResponse
;
/**
* @Author: DouXinYu
* @Date: 2025-12-09 14:20
* @Description:
*/
public
interface
IExportExcelService
{
R
exportApDisplayExcel
(
SalesApRequest
salesApRequest
,
HttpServletResponse
response
);
}
src/main/java/com/sfa/operation/service/sales/export/IImportExcelService.java
0 → 100644
浏览文件 @
a53d3dea
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
);
}
src/main/java/com/sfa/operation/service/sales/export/impl/ExportExcelServiceImpl.java
0 → 100644
浏览文件 @
a53d3dea
package
com
.
sfa
.
operation
.
service
.
sales
.
export
.
impl
;
import
com.sfa.common.core.domain.R
;
import
com.sfa.operation.factory.ApExportExcelStrategyFactory
;
import
com.sfa.operation.pojo.sales.request.SalesApRequest
;
import
com.sfa.operation.service.sales.export.IExportExcelService
;
import
com.sfa.operation.strategy.IExportApExcelStrategy
;
import
com.sfa.operation.util.excel.ExcelUtils
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.stereotype.Service
;
import
javax.servlet.http.HttpServletResponse
;
/**
* @Author: DouXinYu
* @Date: 2025-12-09 14:20
* @Description: 导出excel服务实现类
*/
@Slf4j
@Service
public
class
ExportExcelServiceImpl
implements
IExportExcelService
{
@Autowired
private
ApExportExcelStrategyFactory
exportApExcelStrategyFactory
;
@Override
public
R
exportApDisplayExcel
(
SalesApRequest
salesApRequest
,
HttpServletResponse
response
)
{
try
{
IExportApExcelStrategy
strategy
=
exportApExcelStrategyFactory
.
getStrategy
(
salesApRequest
.
getPageType
());
String
fileNamePrefix
=
strategy
.
getExportFileNamePrefix
();
log
.
info
(
"导出前缀为:{}的excel"
,
fileNamePrefix
);
byte
[]
excelBytesArray
=
strategy
.
generateExcel
(
salesApRequest
);
ExcelUtils
.
exportExcelBytesToResponse
(
excelBytesArray
,
response
,
fileNamePrefix
);
log
.
info
(
"常规陈列数据导出成功!"
);
return
R
.
ok
(
R
.
SUCCESS
,
"数据导出成功"
)
;
}
catch
(
Exception
e
)
{
log
.
error
(
"常规陈列数据导出异常!具体错误原因:"
,
e
);
return
R
.
fail
(
R
.
FAIL
,
"数据导出失败,请联系后台管理员处理!"
);
}
}
}
src/main/java/com/sfa/operation/service/sales/export/impl/ImportExcelServiceImpl.java
0 → 100644
浏览文件 @
a53d3dea
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
);
}
}
src/main/java/com/sfa/operation/service/sales/impl/ApDisplayCoreServiceImpl.java
浏览文件 @
a53d3dea
...
...
@@ -3,6 +3,7 @@ package com.sfa.operation.service.sales.impl;
import
cn.hutool.core.date.DatePattern
;
import
com.sfa.common.security.utils.SecurityUtils
;
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.response.*
;
import
com.sfa.operation.service.sales.IApDisplayCoreService
;
...
...
@@ -12,6 +13,7 @@ import org.springframework.stereotype.Service;
import
java.text.ParseException
;
import
java.util.Date
;
import
java.util.List
;
import
java.util.Objects
;
/**
...
...
@@ -78,6 +80,16 @@ public class ApDisplayCoreServiceImpl implements IApDisplayCoreService {
salesApDisplayJDao
.
updateDetail
(
djDto
);
}
/**
*
* @param updateEntityList
* @return
*/
@Override
public
boolean
batchUpdate
(
List
<
SalesApDisplay
>
updateEntityList
)
{
return
salesApDisplayDao
.
batchUpdate
(
updateEntityList
);
}
@Override
public
void
putPromotionDetail
(
SalesApRequest
request
)
{
// 修改DB 日期值为null
...
...
@@ -110,4 +122,6 @@ public class ApDisplayCoreServiceImpl implements IApDisplayCoreService {
e
.
printStackTrace
();
}
}
}
src/main/java/com/sfa/operation/service/sales/impl/ApDisplayQueryServiceImpl.java
浏览文件 @
a53d3dea
...
...
@@ -5,11 +5,14 @@ import cn.hutool.core.date.DateUtil;
import
com.sfa.common.core.utils.bean.BeanUtils
;
import
com.sfa.common.core.web.domain.PageInfo
;
import
com.sfa.operation.domain.sales.dao.*
;
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.service.sales.IApDisplayQueryService
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.stereotype.Service
;
import
java.util.List
;
/**
* @author : liqiulin
...
...
@@ -76,10 +79,21 @@ public class ApDisplayQueryServiceImpl implements IApDisplayQueryService {
return
salesApDisplayDao
.
queryDistAPReport
(
build
(
request
));
}
@Override
public
List
<
SalesApDisplay
>
queryDataListByCondition
(
SalesApRequest
request
)
{
return
salesApDisplayDao
.
queryDataListByCondition
(
build
(
request
));
}
@Override
public
List
<
SalesApDisplay
>
queryByCondition
(
List
<
SalesApDisplayImportExcelDto
>
validDtoList
)
{
return
salesApDisplayDao
.
queryByCondition
(
validDtoList
);
}
private
SalesApWq
build
(
SalesApRequest
salesApRequest
){
SalesApWq
salesApWq
=
new
SalesApWq
();
BeanUtils
.
copyProperties
(
salesApRequest
,
salesApWq
);
salesApWq
.
setSalesMonth
(
salesApRequest
.
getSalesMonth
()
!=
null
?
DateUtil
.
parse
(
salesApRequest
.
getSalesMonth
()
+
"-01"
,
DatePattern
.
NORM_DATE_PATTERN
)
:
null
);
return
salesApWq
;
}
}
src/main/java/com/sfa/operation/strategy/IExportApExcelStrategy.java
0 → 100644
浏览文件 @
a53d3dea
package
com
.
sfa
.
operation
.
strategy
;
import
com.sfa.operation.config.ExportColumnConfig
;
import
com.sfa.operation.pojo.sales.request.SalesApRequest
;
import
com.sfa.operation.util.excel.ExcelUtils
;
import
java.util.List
;
/**
* @Author: DouXinYu
* @Date: 2025-12-08 16:17
* @Description: 策略接口
*/
public
interface
IExportApExcelStrategy
{
/**
* 获取导出列配置
*
* @return 导出列配置
*/
List
<
ExportColumnConfig
>
getExportColumnConfig
();
/**
* 查询数据
*
* @param salesApRequest 请求参数
* @return 查询结果
*/
List
<?>
queryData
(
SalesApRequest
salesApRequest
);
/**
* 获取导出sheet名称
*/
String
getExportSheetName
();
/**
* 获取导出文件名前缀
*
* @return 导出文件名前缀
*/
String
getExportFileNamePrefix
();
/**
* 生成excel
*
* @param salesApRequest 请求参数
* @return excel字节数组
*/
default
byte
[]
generateExcel
(
SalesApRequest
salesApRequest
)
throws
Exception
{
return
ExcelUtils
.
generateExcelBytes
(
this
.
getExportColumnConfig
(),
this
.
queryData
(
salesApRequest
),
this
.
getExportSheetName
()
);
}
}
src/main/java/com/sfa/operation/strategy/IImportApExcelStrategy.java
0 → 100644
浏览文件 @
a53d3dea
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
""
;
}
}
src/main/java/com/sfa/operation/strategy/impl/exports/NormalDisplayExportStrategyImpl.java
0 → 100644
浏览文件 @
a53d3dea
package
com
.
sfa
.
operation
.
strategy
.
impl
.
exports
;
import
com.sfa.operation.config.ExportColumnConfig
;
import
com.sfa.operation.pojo.sales.request.SalesApRequest
;
import
com.sfa.operation.service.sales.IApDisplayQueryService
;
import
com.sfa.operation.strategy.IExportApExcelStrategy
;
import
com.sfa.operation.util.excel.ExcelStyleUtils
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.stereotype.Component
;
import
java.util.ArrayList
;
import
java.util.Arrays
;
import
java.util.List
;
/**
* @Author: DouXinYu
* @Date: 2025-12-08 17:24
* @Description: 常规陈列策略接口实现类
*/
@Component
(
"normalDisplayExportStrategy"
)
public
class
NormalDisplayExportStrategyImpl
implements
IExportApExcelStrategy
{
private
static
final
String
EXPORT_SHEET_NAME
=
"常规陈列"
;
private
static
final
String
EXPORT_FILE_NAME_PREFIX
=
"常规陈列"
;
@Autowired
private
IApDisplayQueryService
apDisplayQueryService
;
/**
* 获取导出列配置
* @return 列配置
*/
@Override
public
List
<
ExportColumnConfig
>
getExportColumnConfig
()
{
List
<
ExportColumnConfig
>
column
=
new
ArrayList
<>();
//处理填报列的规则限制
//主货架形式-实际
ExportColumnConfig
actualMainShelfType
=
new
ExportColumnConfig
(
"actualMainShelfType"
,
"主货架形式(实际)"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
LIGHT_BLUE_BG
);
actualMainShelfType
.
setValidationValidOptions
(
Arrays
.
asList
(
"3纵"
,
"4纵"
,
"5纵"
,
"6纵"
,
"7纵"
,
"8纵及以上"
));
actualMainShelfType
.
setValidationErrorTitle
(
"输入错误"
);
actualMainShelfType
.
setValidationErrorMsg
(
"主货架形式必须输入”3纵、4纵、5纵、6纵、7纵、8纵及以上“之一!"
);
actualMainShelfType
.
setConditionalStyling
(
true
);
//主货架数量-实际
ExportColumnConfig
actualMainShelfQty
=
new
ExportColumnConfig
(
"actualMainShelfQty"
,
"主货架数量(实际)"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
LIGHT_BLUE_BG
);
actualMainShelfQty
.
setNumberValidation
(
true
);
actualMainShelfQty
.
setValidationNumberMin
(
0.0
);
actualMainShelfQty
.
setValidationNumberMax
(
999999999999999999.999999999999999999
);
actualMainShelfQty
.
setValidationErrorTitle
(
"输入错误"
);
actualMainShelfQty
.
setValidationErrorMsg
(
"主货架数量必须输入大于等于0的数字!"
);
actualMainShelfQty
.
setConditionalStyling
(
true
);
//端架数量-实际
ExportColumnConfig
actualEndCapQty
=
new
ExportColumnConfig
(
"actualEndCapQty"
,
"端架数量(实际)"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
LIGHT_BLUE_BG
);
actualEndCapQty
.
setNumberValidation
(
true
);
actualEndCapQty
.
setValidationNumberMin
(
0.0
);
actualEndCapQty
.
setValidationNumberMax
(
999999999999999999.999999999999999999
);
actualEndCapQty
.
setValidationErrorTitle
(
"输入错误"
);
actualEndCapQty
.
setValidationErrorMsg
(
"端架数量必须输入大于等于0的数字!"
);
actualEndCapQty
.
setConditionalStyling
(
true
);
//地堆面积-实际
ExportColumnConfig
actualFloorStackArea
=
new
ExportColumnConfig
(
"actualFloorStackArea"
,
"地堆平米数(实际)"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
LIGHT_GREY_BG
);
actualFloorStackArea
.
setValidationValidOptions
(
Arrays
.
asList
(
"0"
,
"0.5"
,
"0.8"
,
"1"
,
"2"
,
"3"
,
"4"
,
"5"
,
"6"
,
"8"
));
actualFloorStackArea
.
setValidationErrorTitle
(
"输入错误"
);
actualFloorStackArea
.
setValidationErrorMsg
(
"地堆面积必须输入0、0.5、0.8、1、2、3、4、5、6、8之一!"
);
actualFloorStackArea
.
setConditionalStyling
(
true
);
//地堆数量-实际
ExportColumnConfig
actualFloorStackQty
=
new
ExportColumnConfig
(
"actualFloorStackQty"
,
"地堆数量(实际)"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
LIGHT_GREY_BG
);
actualFloorStackQty
.
setValidationValidOptions
(
Arrays
.
asList
(
"0"
,
"1"
,
"2"
,
"3"
,
"4"
,
"5"
));
actualFloorStackQty
.
setValidationErrorTitle
(
"输入错误"
);
actualFloorStackQty
.
setValidationErrorMsg
(
"地堆数量必须输入0、1、2、3、4、5之一!"
);
actualFloorStackQty
.
setConditionalStyling
(
true
);
//多点陈列数量形式-实际
ExportColumnConfig
actualMultiDisplay
=
new
ExportColumnConfig
(
"actualMultiDisplay"
,
"多点陈列数量形式(实际)"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
LIGHT_BLUE_BG
);
actualMultiDisplay
.
setValidationValidOptions
(
Arrays
.
asList
(
"执行与计划一致"
,
"执行与计划不一致"
));
actualMultiDisplay
.
setValidationErrorTitle
(
"输入错误"
);
actualMultiDisplay
.
setValidationErrorMsg
(
"多点陈列数量形式必须输入“执行与计划一致”或“执行与计划不一致”!"
);
actualMultiDisplay
.
setConditionalStyling
(
true
);
//挂条数量形式-实际
ExportColumnConfig
actualHangingStripQuantityForm
=
new
ExportColumnConfig
(
"actualHangingStripQuantityForm"
,
"挂条数量形式(实际)"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
LIGHT_BLUE_BG
);
actualHangingStripQuantityForm
.
setValidationValidOptions
(
Arrays
.
asList
(
"执行与计划一致"
,
"执行与计划不一致"
));
actualHangingStripQuantityForm
.
setValidationErrorTitle
(
"输入错误"
);
actualHangingStripQuantityForm
.
setValidationErrorMsg
(
"挂条数量形式必须输入“执行与计划一致”或“执行与计划不一致”!"
);
actualHangingStripQuantityForm
.
setConditionalStyling
(
true
);
column
.
add
(
new
ExportColumnConfig
(
"sadId"
,
"程序编号-请忽略"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
UNMODIFIABLE
));
column
.
add
(
new
ExportColumnConfig
(
"salesMonth"
,
"计划月份"
,
"YYYY-MM"
,
ExcelStyleUtils
.
ExcelStyle
.
UNMODIFIABLE
));
column
.
add
(
new
ExportColumnConfig
(
"regionName"
,
"销售大区"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
UNMODIFIABLE
));
column
.
add
(
new
ExportColumnConfig
(
"districtName"
,
"销售战区"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
UNMODIFIABLE
));
column
.
add
(
new
ExportColumnConfig
(
"dealerCode"
,
"经销商代码"
,
"@"
,
ExcelStyleUtils
.
ExcelStyle
.
CHANGE_TEXT_STYLE
));
column
.
add
(
new
ExportColumnConfig
(
"dealerName"
,
"经销商名称"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
UNMODIFIABLE
));
column
.
add
(
new
ExportColumnConfig
(
"storeCode"
,
"门店编码"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
UNMODIFIABLE
));
column
.
add
(
new
ExportColumnConfig
(
"storeName"
,
"门店名称"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
UNMODIFIABLE
));
column
.
add
(
new
ExportColumnConfig
(
"lineName"
,
"系统名称"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
UNMODIFIABLE
));
column
.
add
(
new
ExportColumnConfig
(
"plannedMainShelfType"
,
"主货架形式(计划)"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
UNMODIFIABLE
));
column
.
add
(
actualMainShelfType
);
column
.
add
(
new
ExportColumnConfig
(
"plannedMainShelfQty"
,
"主货架数量(计划)"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
UNMODIFIABLE
));
column
.
add
(
actualMainShelfQty
);
column
.
add
(
new
ExportColumnConfig
(
"plannedEndCapQty"
,
"端架数量(计划)"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
UNMODIFIABLE
));
column
.
add
(
actualEndCapQty
);
column
.
add
(
new
ExportColumnConfig
(
"plannedFloorStackArea"
,
"地堆平米数(计划)"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
UNMODIFIABLE
));
column
.
add
(
actualFloorStackArea
);
column
.
add
(
new
ExportColumnConfig
(
"plannedFloorStackQty"
,
"地堆数量(计划)"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
UNMODIFIABLE
));
column
.
add
(
actualFloorStackQty
);
column
.
add
(
new
ExportColumnConfig
(
"plannedMultiDisplay"
,
"多点陈列数量形式(计划)"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
UNMODIFIABLE
));
column
.
add
(
actualMultiDisplay
);
column
.
add
(
new
ExportColumnConfig
(
"plannedHangingStripQuantityForm"
,
"挂条数量形式(计划)"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
UNMODIFIABLE
));
column
.
add
(
actualHangingStripQuantityForm
);
column
.
add
(
new
ExportColumnConfig
(
"remark"
,
"备注"
,
""
,
ExcelStyleUtils
.
ExcelStyle
.
DEFAULT_STYLE
));
for
(
ExportColumnConfig
exportColumnConfig
:
column
)
{
System
.
out
.
println
(
exportColumnConfig
.
toString
());
}
return
column
;
}
/**
* 查询数据
* @param salesApRequest 请求参数
* @return 根据条换查询出的数据列表
*/
@Override
public
List
<?>
queryData
(
SalesApRequest
salesApRequest
)
{
return
apDisplayQueryService
.
queryDataListByCondition
(
salesApRequest
);
}
@Override
public
String
getExportSheetName
()
{
return
EXPORT_SHEET_NAME
;
}
@Override
public
String
getExportFileNamePrefix
()
{
return
EXPORT_FILE_NAME_PREFIX
;
}
}
src/main/java/com/sfa/operation/strategy/impl/imports/NormalDisplayImportStrategyImpl.java
0 → 100644
浏览文件 @
a53d3dea
package
com
.
sfa
.
operation
.
strategy
.
impl
.
imports
;
import
com.alibaba.nacos.shaded.com.google.gson.reflect.TypeToken
;
import
com.baomidou.dynamic.datasource.annotation.DS
;
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
@DS
(
"bi"
)
@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
{
// 获取文件流
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
public
String
updateDisplay
(
List
<
SalesApDisplayImportExcelDto
>
dtoList
)
{
if
(
dtoList
==
null
||
dtoList
.
isEmpty
())
{
return
"更新失败"
;
}
List
<
SalesApDisplay
>
salesApDisplayList
=
buildUpdateEntityList
(
dtoList
);
if
(
salesApDisplayList
.
isEmpty
())
{
return
"更新失败"
;
}
return
salesApDisplayCoreService
.
batchUpdate
(
salesApDisplayList
)
?
"更新成功"
:
"更新失败"
;
}
@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
src/main/java/com/sfa/operation/util/excel/EntityUpdateCheckUtil.java
0 → 100644
浏览文件 @
a53d3dea
package
com
.
sfa
.
operation
.
util
.
excel
;
import
com.baomidou.mybatisplus.annotation.FieldStrategy
;
import
com.baomidou.mybatisplus.annotation.TableId
;
import
com.baomidou.mybatisplus.annotation.TableField
;
import
org.slf4j.Logger
;
import
org.slf4j.LoggerFactory
;
import
java.lang.reflect.Field
;
import
java.util.ArrayList
;
import
java.util.List
;
/**
* 实体更新字段校验工具
*/
public
class
EntityUpdateCheckUtil
{
private
static
final
Logger
log
=
LoggerFactory
.
getLogger
(
EntityUpdateCheckUtil
.
class
);
/**
* 校验实体是否有「非主键的有效更新字段」
* @param entity 待校验实体
* @return true=有有效字段,false=无有效字段
*/
public
static
<
T
>
boolean
hasValidUpdateField
(
T
entity
)
{
if
(
entity
==
null
)
{
return
false
;
}
Class
<?>
clazz
=
entity
.
getClass
();
Field
[]
fields
=
clazz
.
getDeclaredFields
();
boolean
hasUpdateField
=
false
;
for
(
Field
field
:
fields
)
{
field
.
setAccessible
(
true
);
String
fieldName
=
field
.
getName
();
try
{
Object
fieldValue
=
field
.
get
(
entity
);
// 跳过主键字段(带@TableId注解)
if
(
field
.
isAnnotationPresent
(
TableId
.
class
))
{
continue
;
}
// 跳过不参与更新的字段(如@TableField(updateStrategy = FieldStrategy.NEVER))
TableField
tableField
=
field
.
getAnnotation
(
TableField
.
class
);
if
(
tableField
!=
null
&&
tableField
.
updateStrategy
().
equals
(
FieldStrategy
.
NEVER
))
{
continue
;
}
// 存在非空的更新字段
if
(
fieldValue
!=
null
)
{
hasUpdateField
=
true
;
break
;
}
}
catch
(
IllegalAccessException
e
)
{
log
.
error
(
"反射校验实体字段失败:fieldName={}"
,
fieldName
,
e
);
}
}
return
hasUpdateField
;
}
public
static
<
T
>
List
<
List
<
T
>>
splitList
(
List
<
T
>
list
,
int
batchSize
)
{
List
<
List
<
T
>>
batches
=
new
ArrayList
<>();
for
(
int
i
=
0
;
i
<
list
.
size
();
i
+=
batchSize
)
{
int
end
=
Math
.
min
(
i
+
batchSize
,
list
.
size
());
batches
.
add
(
list
.
subList
(
i
,
end
));
}
return
batches
;
}
}
\ No newline at end of file
src/main/java/com/sfa/operation/util/excel/ExcelStyleUtils.java
0 → 100644
浏览文件 @
a53d3dea
package
com
.
sfa
.
operation
.
util
.
excel
;
import
com.sfa.operation.config.ExportColumnConfig
;
import
org.apache.commons.collections4.CollectionUtils
;
import
org.apache.poi.ss.usermodel.*
;
import
org.apache.poi.ss.util.CellRangeAddressList
;
import
java.util.List
;
/**
* @Author: DouXinYu
* @Date: 2025-12-08 12:08
* @Description: 配置导出的EXCEL样式
*/
public
class
ExcelStyleUtils
{
public
enum
ExcelStyle
{
// 不可修改
UNMODIFIABLE
,
// 浅蓝色
LIGHT_BLUE_BG
,
// 浅灰色
LIGHT_GREY_BG
,
//默认
DEFAULT_STYLE
,
//红色
RED_STYLE
,
//表头
HEADER_STYLE
,
//修改数字列为文本样式
CHANGE_TEXT_STYLE
}
/**
* 获取表格样式
*
* @param workbook 工作表格
* @param style 样式
* @return 已经设置好的样式
*/
/**
* 获取表格样式
*
* @param workbook 工作表格
* @param style 样式
* @return 已经设置好的样式
*/
public
static
CellStyle
getStyle
(
Workbook
workbook
,
ExcelStyle
style
)
{
CellStyle
cellStyle
=
workbook
.
createCellStyle
();
Font
font
=
workbook
.
createFont
();
// 设置通用样式属性
cellStyle
.
setFillPattern
(
FillPatternType
.
SOLID_FOREGROUND
);
cellStyle
.
setAlignment
(
HorizontalAlignment
.
CENTER
);
cellStyle
.
setVerticalAlignment
(
VerticalAlignment
.
CENTER
);
cellStyle
.
setBorderBottom
(
BorderStyle
.
THIN
);
cellStyle
.
setBorderTop
(
BorderStyle
.
THIN
);
cellStyle
.
setBorderLeft
(
BorderStyle
.
THIN
);
cellStyle
.
setBorderRight
(
BorderStyle
.
THIN
);
switch
(
style
)
{
// 不可修改
case
UNMODIFIABLE:
cellStyle
.
setFillForegroundColor
(
IndexedColors
.
GREY_25_PERCENT
.
getIndex
());
cellStyle
.
setLocked
(
true
);
font
.
setBold
(
true
);
cellStyle
.
setFont
(
font
);
break
;
// 浅蓝色
case
LIGHT_BLUE_BG:
cellStyle
.
setFillForegroundColor
(
IndexedColors
.
PALE_BLUE
.
getIndex
());
cellStyle
.
setLocked
(
false
);
break
;
// 浅灰色
case
LIGHT_GREY_BG:
cellStyle
.
setFillForegroundColor
(
IndexedColors
.
GREY_25_PERCENT
.
getIndex
());
cellStyle
.
setLocked
(
true
);
break
;
// 红色样式
case
RED_STYLE:
font
.
setColor
(
IndexedColors
.
RED
.
getIndex
());
font
.
setFontName
(
"微软雅黑"
);
font
.
setFontHeightInPoints
((
short
)
11
);
cellStyle
.
setFont
(
font
);
cellStyle
.
setFillForegroundColor
(
IndexedColors
.
WHITE
.
getIndex
());
cellStyle
.
setLocked
(
false
);
break
;
// 表头样式
case
HEADER_STYLE:
font
.
setBold
(
true
);
font
.
setFontName
(
"微软雅黑"
);
font
.
setFontHeightInPoints
((
short
)
12
);
cellStyle
.
setFont
(
font
);
cellStyle
.
setFillForegroundColor
(
IndexedColors
.
GREY_25_PERCENT
.
getIndex
());
cellStyle
.
setLocked
(
false
);
break
;
case
CHANGE_TEXT_STYLE:
cellStyle
.
setFillForegroundColor
(
IndexedColors
.
GREY_25_PERCENT
.
getIndex
());
cellStyle
.
setDataFormat
(
workbook
.
getCreationHelper
().
createDataFormat
().
getFormat
(
"@"
));
cellStyle
.
setLocked
(
true
);
font
.
setBold
(
true
);
cellStyle
.
setFont
(
font
);
break
;
// 默认
case
DEFAULT_STYLE:
default
:
cellStyle
.
setFillForegroundColor
(
IndexedColors
.
WHITE
.
getIndex
());
cellStyle
.
setLocked
(
false
);
break
;
}
return
cellStyle
;
}
/**
* 匹配不可修改样式的列
*
* @param headerName 表头名称
* @param unmodifiableHeaderList 不可修改的列名
* @return 匹配到的样式
*/
public
static
ExcelStyle
matchUnmodifiedStyle
(
String
headerName
,
List
<
String
>
unmodifiableHeaderList
)
{
return
isHeaderInList
(
headerName
,
unmodifiableHeaderList
)
?
ExcelStyle
.
UNMODIFIABLE
:
ExcelStyle
.
DEFAULT_STYLE
;
}
/**
* 匹配浅蓝色样式的列
*
* @param headerName 表头名称
* @param lightBlueHeaderList 浅蓝色列名
* @return 匹配到的样式
*/
public
static
ExcelStyle
matchLightBlueStyle
(
String
headerName
,
List
<
String
>
lightBlueHeaderList
)
{
return
isHeaderInList
(
headerName
,
lightBlueHeaderList
)
?
ExcelStyle
.
LIGHT_BLUE_BG
:
ExcelStyle
.
DEFAULT_STYLE
;
}
/**
* 匹配浅灰色样式的列
*
* @param headerName 表头名称
* @param lightGreyHeaderList 浅灰色列名
* @return 匹配到的样式
*/
public
static
ExcelStyle
matchLightGreyStyle
(
String
headerName
,
List
<
String
>
lightGreyHeaderList
)
{
return
isHeaderInList
(
headerName
,
lightGreyHeaderList
)
?
ExcelStyle
.
LIGHT_GREY_BG
:
ExcelStyle
.
DEFAULT_STYLE
;
}
/**
* 判断表头是否在列表中
*
* @param headerName 表头名称
* @param headerList 表头列表
* @return 是否在列表中
*/
public
static
boolean
isHeaderInList
(
String
headerName
,
List
<
String
>
headerList
)
{
if
(
headerList
==
null
||
CollectionUtils
.
isEmpty
(
headerList
))
{
return
false
;
}
if
(
headerName
==
null
||
headerName
.
trim
().
isEmpty
())
{
return
false
;
}
if
(!
headerList
.
contains
(
headerName
))
{
return
false
;
}
return
headerList
.
contains
(
headerName
);
}
/**
* 添加列数据验证
*
* @param sheet 工作表
* @param columnIndex 列索引
* @param config 列配置
*/
public
static
void
addColumnDataValidation
(
Sheet
sheet
,
int
columnIndex
,
ExportColumnConfig
config
)
{
// 空值校验:Sheet或配置为空,直接返回(避免空指针)
if
(
sheet
==
null
||
config
==
null
)
{
return
;
}
// 获取核心对象:工作簿、数据验证助手
Workbook
workbook
=
sheet
.
getWorkbook
();
DataValidationHelper
helper
=
sheet
.
getDataValidationHelper
();
// 声明约束对象(后续根据验证类型赋值)
DataValidationConstraint
constraint
=
null
;
// 数字验证分支
if
(
config
.
isNumberValidation
())
{
// 获取数字验证的最值配置
Double
min
=
config
.
getValidationNumberMin
();
Double
max
=
config
.
getValidationNumberMax
();
// 校验最值配置:至少配置一个才生效
if
(
min
==
null
&&
max
==
null
)
{
return
;
}
// 选择数字类型:整数/小数
// DECIMAL=2(小数),INTEGER=1(整数)
int
numericType
=
config
.
isValidationNumberAllowDecimal
()
?
DataValidationConstraint
.
ValidationType
.
DECIMAL
:
DataValidationConstraint
.
ValidationType
.
INTEGER
;
// 转换最值为字符串(POI要求的参数格式)
String
minStr
=
(
min
==
null
)
?
""
:
min
.
toString
();
String
maxStr
=
(
max
==
null
)
?
""
:
max
.
toString
();
// 根据最值配置,构建不同的数字约束
if
(
min
!=
null
&&
max
!=
null
)
{
// 介于min和max之间(OperatorType.BETWEEN = 1)
constraint
=
helper
.
createNumericConstraint
(
numericType
,
DataValidationConstraint
.
OperatorType
.
BETWEEN
,
minStr
,
maxStr
);
}
else
if
(
min
!=
null
)
{
// 大于等于min(OperatorType.GREATER_OR_EQUAL = 6)
constraint
=
helper
.
createNumericConstraint
(
numericType
,
DataValidationConstraint
.
OperatorType
.
GREATER_OR_EQUAL
,
minStr
,
""
);
}
else
{
// 小于等于max(OperatorType.LESS_OR_EQUAL = 7)
constraint
=
helper
.
createNumericConstraint
(
numericType
,
DataValidationConstraint
.
OperatorType
.
LESS_OR_EQUAL
,
""
,
maxStr
);
}
}
// 列表验证分支(原有逻辑)
else
if
(
config
.
getValidationValidOptions
()
!=
null
&&
!
config
.
getValidationValidOptions
().
isEmpty
())
{
// 获取列表合法值
List
<
String
>
validOptions
=
config
.
getValidationValidOptions
();
// 构建列表约束(仅允许输入列表中的值)
constraint
=
helper
.
createExplicitListConstraint
(
validOptions
.
toArray
(
new
String
[
0
])
);
}
// 无有效约束,直接返回
if
(
constraint
==
null
)
{
return
;
}
// 配置验证生效的行范围(默认从第1行开始,跳过表头)
CellRangeAddressList
addressList
=
new
CellRangeAddressList
(
config
.
getValidationStartRow
(),
config
.
getValidationEndRow
(),
columnIndex
,
columnIndex
);
// 创建数据验证规则(绑定约束+生效范围)
DataValidation
validation
=
helper
.
createValidation
(
constraint
,
addressList
);
// 配置错误提示(非法输入时弹窗)
if
(
config
.
getValidationErrorTitle
()
!=
null
&&
config
.
getValidationErrorMsg
()
!=
null
)
{
validation
.
setShowErrorBox
(
true
);
validation
.
setErrorStyle
(
config
.
getValidationErrorStyle
());
// 设置错误弹窗的标题和内容
validation
.
createErrorBox
(
config
.
getValidationErrorTitle
(),
config
.
getValidationErrorMsg
());
}
// 配置输入提示(鼠标选中单元格时的引导提示)
if
(
config
.
getValidationPromptTitle
()
!=
null
&&
config
.
getValidationPromptMsg
()
!=
null
)
{
validation
.
setShowPromptBox
(
true
);
// 设置输入提示的标题和内容
validation
.
createPromptBox
(
config
.
getValidationPromptTitle
(),
config
.
getValidationPromptMsg
());
}
// 将验证规则添加到Sheet,使其生效
sheet
.
addValidationData
(
validation
);
}
}
src/main/java/com/sfa/operation/util/excel/ExcelUtils.java
0 → 100644
浏览文件 @
a53d3dea
package
com
.
sfa
.
operation
.
util
.
excel
;
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.baomidou.mybatisplus.annotation.FieldStrategy
;
import
com.baomidou.mybatisplus.annotation.TableField
;
import
com.baomidou.mybatisplus.annotation.TableId
;
import
com.sfa.operation.config.ExportColumnConfig
;
import
com.sfa.operation.config.OssConfigProperties
;
import
lombok.extern.slf4j.Slf4j
;
import
org.apache.commons.collections4.CollectionUtils
;
import
org.apache.commons.lang3.StringUtils
;
import
org.apache.poi.hssf.usermodel.HSSFWorkbook
;
import
org.apache.poi.ss.usermodel.*
;
import
org.apache.poi.ss.util.CellRangeAddressList
;
import
org.apache.poi.xssf.streaming.SXSSFWorkbook
;
import
org.apache.poi.xssf.usermodel.XSSFWorkbook
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.stereotype.Component
;
import
javax.annotation.PostConstruct
;
import
javax.servlet.http.HttpServletResponse
;
import
java.io.*
;
import
java.math.BigDecimal
;
import
java.net.URLDecoder
;
import
java.net.URLEncoder
;
import
java.nio.charset.StandardCharsets
;
import
java.text.SimpleDateFormat
;
import
java.util.ArrayList
;
import
java.util.Date
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.concurrent.ConcurrentHashMap
;
import
java.util.function.BiFunction
;
/**
* @Author: DouXinYu
* @Date: 2025-12-05 16:56
* @Description: AP导出excel工具类
*/
@Slf4j
@Component
public
class
ExcelUtils
{
//缓存样式
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
;
public
static
String
extractFileNameFromUrl
(
String
filePathUrl
)
{
if
(
StringUtils
.
isBlank
(
filePathUrl
))
{
return
"未知文件"
;
}
try
{
// 先尝试URL解码,处理包含特殊字符或中文的文件名
String
decodedUrl
=
URLDecoder
.
decode
(
filePathUrl
,
StandardCharsets
.
UTF_8
.
name
());
// 方案1:标准文件路径(file:///C:/Users/xxx/Desktop/data.xlsx)
if
(
decodedUrl
.
startsWith
(
"file:"
))
{
return
new
File
(
decodedUrl
.
substring
(
6
)).
getName
();
}
// 方案2:普通绝对/相对路径(如"C:\\Users\\xxx\\Desktop\\data.xlsx"或"./data.xlsx")
if
(
decodedUrl
.
contains
(
"."
)
||
decodedUrl
.
contains
(
"/"
))
{
return
new
File
(
decodedUrl
).
getName
();
}
// 方案3:URL路径(如"http://example.com/path/data.xlsx?version=1")
if
(
decodedUrl
.
contains
(
"://"
))
{
String
pathPart
=
decodedUrl
.
substring
(
decodedUrl
.
indexOf
(
"://"
)
+
3
);
if
(
pathPart
.
contains
(
"/"
))
{
pathPart
=
pathPart
.
substring
(
pathPart
.
indexOf
(
"/"
)
+
1
);
}
// 移除查询参数(?之后的部分)
if
(
pathPart
.
contains
(
"?"
))
{
pathPart
=
pathPart
.
substring
(
0
,
pathPart
.
indexOf
(
"?"
));
}
return
new
File
(
pathPart
).
getName
();
}
// 默认兜底方案:直接当作文件名处理
return
decodedUrl
;
}
catch
(
Exception
e
)
{
log
.
warn
(
"解析文件名异常,使用原始名称:{},错误:{}"
,
filePathUrl
,
e
.
getMessage
());
return
filePathUrl
;
}
}
@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响应流)
*
* @param excelBytes 生成的Excel字节数组
* @param response Http响应对象
* @param fileNamePrefix 导出文件名前缀(如"常规陈列")
* @throws Exception 导出异常
*/
public
static
void
exportExcelBytesToResponse
(
byte
[]
excelBytes
,
HttpServletResponse
response
,
String
fileNamePrefix
)
throws
Exception
{
if
(
excelBytes
==
null
||
excelBytes
.
length
==
0
)
{
throw
new
Exception
(
"导出Excel失败:生成的Excel字节数组为空"
);
}
if
(
StringUtils
.
isBlank
(
fileNamePrefix
))
{
fileNamePrefix
=
"导出文件"
;
}
// 1. 设置响应头(解决中文乱码、指定文件类型)
String
fileName
=
String
.
format
(
"%s_%s.xlsx"
,
fileNamePrefix
,
new
SimpleDateFormat
(
"yyyyMMdd"
).
format
(
new
Date
()));
// URLEncoder编码兼容所有浏览器
String
encodedFileName
=
URLEncoder
.
encode
(
fileName
,
String
.
valueOf
(
StandardCharsets
.
UTF_8
));
response
.
setContentType
(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
);
response
.
setCharacterEncoding
(
StandardCharsets
.
UTF_8
.
name
());
response
.
setHeader
(
"Content-Disposition"
,
"attachment;filename="
+
encodedFileName
);
// 禁止缓存
response
.
setHeader
(
"Pragma"
,
"no-cache"
);
response
.
setHeader
(
"Cache-Control"
,
"no-store, no-cache, must-revalidate"
);
response
.
setDateHeader
(
"Expires"
,
0
);
// 设置文件大小(可选,提升下载体验)
response
.
setContentLength
(
excelBytes
.
length
);
// 2. 将字节数组写入响应流
try
(
OutputStream
outputStream
=
response
.
getOutputStream
())
{
outputStream
.
write
(
excelBytes
);
outputStream
.
flush
();
}
catch
(
Exception
e
)
{
log
.
error
(
"写入Excel响应流失败:{}"
,
e
.
getMessage
(),
e
);
throw
new
Exception
(
"导出Excel失败:"
+
e
.
getMessage
());
}
}
/**
* 生成Excel字节流
*
* @param exportColumnConfigList 表头配置
* @param dataList 数据列表
* @param sheetName sheetName
* @return byte[] excel 数组
* @throws IOException
*/
public
static
byte
[]
generateExcelBytes
(
List
<
ExportColumnConfig
>
exportColumnConfigList
,
List
<?>
dataList
,
String
sheetName
)
throws
IOException
{
//数据校验
if
(
CollectionUtils
.
isEmpty
(
exportColumnConfigList
))
{
throw
new
IOException
(
"导出Excel失败:没有配置导出列"
);
}
if
(
CollectionUtils
.
isEmpty
(
dataList
))
{
throw
new
IOException
(
"导出Excel失败:没有获取到相关数据,请检查条件后重试"
);
}
if
(
sheetName
==
null
||
sheetName
.
trim
().
isEmpty
())
{
throw
new
IOException
(
"导出Excel失败:没有配置sheetName"
);
}
// 根据sheetName创建工作表
try
(
// 创建工作簿
Workbook
workbook
=
createExcelWorkbook
(
dataList
);
// 创建文件字节数组
ByteArrayOutputStream
outputStream
=
new
ByteArrayOutputStream
();)
{
try
{
Sheet
sheet
=
workbook
.
createSheet
(
sheetName
);
// 创建表头
createHeader
(
sheet
,
exportColumnConfigList
);
// 创建数据行
createDataRows
(
sheet
,
dataList
,
exportColumnConfigList
);
// 处理需要验证的列
for
(
int
i
=
0
;
i
<
exportColumnConfigList
.
size
();
i
++)
{
ExportColumnConfig
config
=
exportColumnConfigList
.
get
(
i
);
// 给列配置数据验证(跳过表头)
ExcelStyleUtils
.
addColumnDataValidation
(
sheet
,
i
,
config
);
if
(
isColumnProtected
(
config
))
{
// 为该列所有数据行添加只读验证
// 从第1行开始(跳过表头)
addReadOnlyValidation
(
sheet
,
i
,
1
,
dataList
.
size
());
}
}
workbook
.
write
(
outputStream
);
return
outputStream
.
toByteArray
();
}
finally
{
//如果创建的是 sxssf 清理临时文件
if
(
workbook
instanceof
SXSSFWorkbook
)
{
((
SXSSFWorkbook
)
workbook
).
dispose
();
}
workbook
.
close
();
}
}
catch
(
IOException
e
)
{
log
.
error
(
"生成Excel文件失败:{}"
,
e
.
getMessage
(),
e
);
throw
new
IOException
(
"生成Excel文件失败:"
+
e
.
getMessage
());
}
}
/* ============================================================导出工具方法=========================================================== */
/**
* 创建工作簿
*
* @param dataList 数据列表
* @return 工作簿
*/
private
static
Workbook
createExcelWorkbook
(
List
<?>
dataList
)
{
if
(
dataList
.
size
()
>
1000
)
{
SXSSFWorkbook
sxssfWorkbook
=
new
SXSSFWorkbook
(
100
);
sxssfWorkbook
.
setCompressTempFiles
(
true
);
return
sxssfWorkbook
;
}
else
{
return
new
XSSFWorkbook
();
}
}
/**
* 创建表头
*
* @param sheet 工作表
* @param exportColumnConfigList 表头配置
*/
public
static
void
createHeader
(
Sheet
sheet
,
List
<
ExportColumnConfig
>
exportColumnConfigList
)
{
Workbook
workbook
=
sheet
.
getWorkbook
();
Row
headerRow
=
sheet
.
createRow
(
0
);
// 调整表头行高(适配换行)
headerRow
.
setHeightInPoints
(
40
);
for
(
int
i
=
0
;
i
<
exportColumnConfigList
.
size
();
i
++)
{
ExportColumnConfig
config
=
exportColumnConfigList
.
get
(
i
);
Cell
cell
=
headerRow
.
createCell
(
i
);
String
originalHeader
=
config
.
getHeaderName
()
==
null
?
""
:
config
.
getHeaderName
();
// 处理表头文本:拆分括号内容+换行
String
processedHeader
=
processHeaderText
(
originalHeader
);
cell
.
setCellValue
(
processedHeader
);
// 使用 HEADER_STYLE 作为表头的基础样式
CellStyle
headerStyle
=
cacheStyle
(
workbook
,
ExcelStyleUtils
.
ExcelStyle
.
HEADER_STYLE
);
cell
.
setCellStyle
(
headerStyle
);
cell
.
getCellStyle
().
setWrapText
(
true
);
// 表头强制锁定(不可修改)
cell
.
getCellStyle
().
setLocked
(
true
);
// 设置初始列宽
String
headerText
=
config
.
getHeaderName
();
if
(
headerText
!=
null
)
{
int
maxWidth
=
calculateTextWidth
(
processedHeader
);
// 至少20个字符宽度
sheet
.
setColumnWidth
(
i
,
Math
.
max
(
maxWidth
,
20
*
256
));
}
}
// 为表头行添加只读验证(防止用户修改表头)
for
(
int
i
=
0
;
i
<
exportColumnConfigList
.
size
();
i
++)
{
ExportColumnConfig
exportColumnConfig
=
exportColumnConfigList
.
get
(
i
);
if
(
exportColumnConfig
.
getValidationPromptTitle
()
!=
null
&&
exportColumnConfig
.
getValidationPromptMsg
()
!=
null
)
{
addHeaderPromptValidation
(
sheet
,
i
,
exportColumnConfig
);
}
// 只保护第0行(表头行)
addReadOnlyValidation
(
sheet
,
i
,
HEADER_ROW
,
HEADER_ROW
);
}
}
/**
* 创建数据行
*
* @param sheet 工作表
* @param dataList 数据列表
* @param exportColumnConfigList 表头配置
*/
public
static
void
createDataRows
(
Sheet
sheet
,
List
<?>
dataList
,
List
<
ExportColumnConfig
>
exportColumnConfigList
)
{
Workbook
workbook
=
sheet
.
getWorkbook
();
for
(
int
i
=
0
;
i
<
dataList
.
size
();
i
++)
{
// 创建数据行
Row
dataRow
=
sheet
.
createRow
(
i
+
1
);
// 获取数据
Object
data
=
dataList
.
get
(
i
);
// 渲染数据列
for
(
int
j
=
0
;
j
<
exportColumnConfigList
.
size
();
j
++)
{
// 获取数据列的配置
ExportColumnConfig
exportColumnConfig
=
exportColumnConfigList
.
get
(
j
);
Cell
cell
=
dataRow
.
createCell
(
j
);
// 处理条件样式
CellStyle
style
;
if
(
exportColumnConfig
.
isConditionalStyling
()
&&
j
>
0
)
{
// 检查前一列是否有值
Cell
previousCell
=
dataRow
.
getCell
(
j
-
1
);
boolean
hasPreviousValue
=
false
;
if
(
previousCell
!=
null
)
{
switch
(
previousCell
.
getCellType
())
{
case
STRING:
hasPreviousValue
=
previousCell
.
getStringCellValue
()
!=
null
&&
!
previousCell
.
getStringCellValue
().
trim
().
isEmpty
();
break
;
case
NUMERIC:
hasPreviousValue
=
true
;
break
;
case
BOOLEAN:
hasPreviousValue
=
true
;
break
;
default
:
}
}
// 根据前一列是否有值来设置样式
if
(
hasPreviousValue
)
{
style
=
cacheStyle
(
workbook
,
ExcelStyleUtils
.
ExcelStyle
.
LIGHT_BLUE_BG
);
}
else
{
style
=
cacheStyle
(
workbook
,
ExcelStyleUtils
.
ExcelStyle
.
DEFAULT_STYLE
);
}
}
else
{
// 使用配置的默认样式
style
=
cacheStyle
(
workbook
,
exportColumnConfig
.
getStyle
());
}
cell
.
setCellStyle
(
style
);
Object
value
=
getFieldValueByOgnl
(
exportColumnConfig
.
getFieldName
(),
data
);
// 设置单元格值并应用格式
setCellValueWithFormat
(
cell
,
value
,
exportColumnConfig
.
getFormat
());
}
}
}
/**
* 添加表头提示
*
* @param sheet 工作表
* @param columnIndex 列索引
* @param config 表头配置
*/
private
static
void
addHeaderPromptValidation
(
Sheet
sheet
,
int
columnIndex
,
ExportColumnConfig
config
)
{
DataValidationHelper
helper
=
sheet
.
getDataValidationHelper
();
// 创建总是为真的约束
DataValidationConstraint
constraint
=
helper
.
createCustomConstraint
(
"FALSE"
);
// 仅对表头行(第0行)应用
CellRangeAddressList
addressList
=
new
CellRangeAddressList
(
HEADER_ROW
,
HEADER_ROW
,
columnIndex
,
columnIndex
);
DataValidation
validation
=
helper
.
createValidation
(
constraint
,
addressList
);
// 设置提示信息
validation
.
setShowPromptBox
(
true
);
validation
.
createPromptBox
(
config
.
getValidationPromptTitle
(),
config
.
getValidationPromptMsg
());
validation
.
setShowErrorBox
(
true
);
validation
.
createErrorBox
(
"禁止修改"
,
"单元格内容禁止修改!"
);
validation
.
setErrorStyle
(
DataValidation
.
ErrorStyle
.
STOP
);
validation
.
setEmptyCellAllowed
(
false
);
validation
.
setSuppressDropDownArrow
(
false
);
sheet
.
addValidationData
(
validation
);
}
/**
* 根据字段名获取字段值
*
* @param fieldName 字段名
* @param data 数据对象
* @return 字段值
*/
private
static
Object
getFieldValueByOgnl
(
String
fieldName
,
Object
data
)
{
// 避免空指针访问
if
(
data
==
null
||
fieldName
==
null
||
fieldName
.
trim
().
isEmpty
())
{
return
""
;
}
try
{
// 使用 BeanUtils 获取属性值
return
BeanUtil
.
getProperty
(
data
,
fieldName
);
}
catch
(
Exception
e
)
{
log
.
error
(
"获取字段值失败:{}"
,
fieldName
,
e
);
// 空值兜底,避免崩溃
return
""
;
}
}
/**
* 设置单元格值并应用格式(修复:日期/数字格式处理)
*
* @param cell 单元格
* @param value 数据值
* @param format 格式(如yyyy-MM-dd、0.00)
*/
private
static
void
setCellValueWithFormat
(
Cell
cell
,
Object
value
,
String
format
)
{
if
(
value
==
null
)
{
cell
.
setCellValue
(
""
);
return
;
}
// 特殊处理:如果格式为 "@" 或者字段名包含某些关键词,则强制作为文本处理
if
(
"@"
.
equals
(
format
))
{
cell
.
setCellValue
(
value
.
toString
());
return
;
}
// 1. 日期类型
if
(
value
instanceof
Date
)
{
if
(
format
!=
null
&&
!
format
.
isEmpty
())
{
SimpleDateFormat
sdf
=
new
SimpleDateFormat
(
format
);
cell
.
setCellValue
(
sdf
.
format
((
Date
)
value
));
}
else
{
cell
.
setCellValue
((
Date
)
value
);
}
return
;
}
// 2. 数值类型(整数/小数):设置为数值单元格,保证数值校验生效
try
{
double
numericValue
=
Double
.
parseDouble
(
value
.
toString
());
cell
.
setCellValue
(
numericValue
);
// 数值格式(如保留2位小数)
if
(
format
!=
null
&&
!
format
.
isEmpty
())
{
DataFormat
formatInstance
=
cell
.
getSheet
().
getWorkbook
().
createDataFormat
();
CellStyle
numericStyle
=
cell
.
getSheet
().
getWorkbook
().
createCellStyle
();
numericStyle
.
setDataFormat
(
formatInstance
.
getFormat
(
format
));
cell
.
setCellStyle
(
numericStyle
);
}
return
;
}
catch
(
NumberFormatException
e
)
{
// 非数值类型,走字符串逻辑
}
// 3. 字符串类型
cell
.
setCellValue
(
value
.
toString
());
}
/**
* 计算文本宽度(支持换行)
*
* @param text 文本内容
* @return 宽度值
*/
private
static
int
calculateTextWidth
(
String
text
)
{
if
(
text
==
null
||
text
.
isEmpty
())
{
return
10
*
256
;
}
String
[]
lines
=
text
.
split
(
"\n"
);
int
maxLineLength
=
0
;
for
(
String
line
:
lines
)
{
maxLineLength
=
Math
.
max
(
maxLineLength
,
line
.
length
());
}
// 每个字符大约占用256单位宽度,增加一些缓冲
return
maxLineLength
*
256
+
1000
;
}
/**
* 缓存样式避免内存溢出
*
* @param workbook 工作簿
* @param style 目标样式
* @return 缓存的样式
*/
private
static
CellStyle
cacheStyle
(
Workbook
workbook
,
ExcelStyleUtils
.
ExcelStyle
style
)
{
ExcelStyleUtils
.
ExcelStyle
targetStyle
=
style
==
null
?
ExcelStyleUtils
.
ExcelStyle
.
DEFAULT_STYLE
:
style
;
// 使用更稳定的缓存键
String
styleKey
=
System
.
identityHashCode
(
workbook
)
+
"_"
+
targetStyle
.
name
();
if
(!
CACHE_CELL_STYLE
.
containsKey
(
styleKey
))
{
CellStyle
cellStyle
=
ExcelStyleUtils
.
getStyle
(
workbook
,
targetStyle
);
CACHE_CELL_STYLE
.
put
(
styleKey
,
cellStyle
);
return
cellStyle
;
}
return
CACHE_CELL_STYLE
.
get
(
styleKey
);
}
/**
* 处理表头文本:拆分括号内容+换行
*
* @param originalText 原始文本
* @return 处理后的文本
*/
private
static
String
processHeaderText
(
String
originalText
)
{
if
(
originalText
==
null
||
originalText
.
isEmpty
())
{
return
""
;
}
// 替换中文括号及其内容为换行形式
String
result
;
if
(
originalText
.
contains
(
"("
)
&&
originalText
.
contains
(
")"
))
{
// 中文括号替换为换行
result
=
originalText
.
replaceAll
(
"((.*?))"
,
"\n$1"
);
}
else
if
(
originalText
.
contains
(
"("
)
&&
originalText
.
contains
(
")"
))
{
// 英文括号替换为换行
result
=
originalText
.
replaceAll
(
"\\((.*?)\\)"
,
"\n$1"
);
}
else
if
(
originalText
.
contains
(
"-"
))
{
// 横线替换为换行(内容)
result
=
originalText
.
replaceAll
(
"-(.+)"
,
"\n($1)"
);
}
else
if
(
originalText
.
contains
(
"_"
))
{
result
=
originalText
.
replaceAll
(
"_(.+)"
,
"\n($1)"
);
}
else
{
result
=
originalText
;
}
return
result
;
}
/**
* 为特定单元格区域添加只读数据验证
*
* @param sheet 工作表
* @param columnIndex 列索引
* @param startRow 起始行
* @param endRow 结束行
*/
private
static
void
addReadOnlyValidation
(
Sheet
sheet
,
int
columnIndex
,
int
startRow
,
int
endRow
)
{
DataValidationHelper
helper
=
sheet
.
getDataValidationHelper
();
// 创建自定义公式验证,始终返回false(禁止输入)
DataValidationConstraint
constraint
=
helper
.
createCustomConstraint
(
"FALSE"
);
CellRangeAddressList
addressList
=
new
CellRangeAddressList
(
startRow
,
endRow
,
columnIndex
,
columnIndex
);
DataValidation
validation
=
helper
.
createValidation
(
constraint
,
addressList
);
validation
.
setShowErrorBox
(
true
);
validation
.
setErrorStyle
(
DataValidation
.
ErrorStyle
.
STOP
);
validation
.
createErrorBox
(
"禁止修改"
,
"此单元格内容不可修改"
);
validation
.
setEmptyCellAllowed
(
false
);
validation
.
setSuppressDropDownArrow
(
false
);
sheet
.
addValidationData
(
validation
);
}
/**
* 判断列是否需要保护
*
* @param config 列配置
* @return 是否需要保护
*/
private
static
boolean
isColumnProtected
(
ExportColumnConfig
config
)
{
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
;
}
}
src/main/resources/mapper/sales/SalesApDisplayMapper.xml
浏览文件 @
a53d3dea
...
...
@@ -711,6 +711,7 @@
group by region_name) sp
on ar.region_name = sp.region_name order by ar.region_name
</select>
<select
id=
"queryDeptAPHZReportDQ"
parameterType=
"com.sfa.operation.domain.sales.wq.SalesApWq"
resultType=
"java.util.Map"
>
SELECT '合计:' as regionName,
d.主货架计划网点数 as planMsStoreCnt,
...
...
@@ -1193,4 +1194,94 @@
</where>
) sp;
</select>
<!-- 批量更新SQL -->
<update
id=
"batchUpdate"
>
UPDATE sales_ap_display
<trim
prefix=
"SET"
suffixOverrides=
","
>
<!-- 实际主货架类型 -->
actual_main_shelf_type = CASE
<foreach
collection=
"list"
item=
"item"
separator=
""
>
WHEN sad_id = #{item.sadId} THEN #{item.actualMainShelfType}
</foreach>
END,
<!-- 实际主货架数量 -->
actual_main_shelf_qty = CASE
<foreach
collection=
"list"
item=
"item"
separator=
""
>
WHEN sad_id = #{item.sadId} THEN #{item.actualMainShelfQty}
</foreach>
END,
<!-- 实际主货架执行状态 -->
actual_main_shelf_executed = CASE
<foreach
collection=
"list"
item=
"item"
separator=
""
>
WHEN sad_id = #{item.sadId} THEN #{item.actualMainShelfExecuted}
</foreach>
END,
<!-- 实际端架数量 -->
actual_end_cap_qty = CASE
<foreach
collection=
"list"
item=
"item"
separator=
""
>
WHEN sad_id = #{item.sadId} THEN #{item.actualEndCapQty}
</foreach>
END,
<!-- 实际端架执行状态 -->
actual_end_cap_executed = CASE
<foreach
collection=
"list"
item=
"item"
separator=
""
>
WHEN sad_id = #{item.sadId} THEN #{item.actualEndCapExecuted}
</foreach>
END,
<!-- 实际堆头面积 -->
actual_floor_stack_area = CASE
<foreach
collection=
"list"
item=
"item"
separator=
""
>
WHEN sad_id = #{item.sadId} THEN #{item.actualFloorStackArea}
</foreach>
END,
<!-- 实际堆头数量 -->
actual_floor_stack_qty = CASE
<foreach
collection=
"list"
item=
"item"
separator=
""
>
WHEN sad_id = #{item.sadId} THEN #{item.actualFloorStackQty}
</foreach>
END,
<!-- 实际堆头执行状态 -->
actual_floor_stack_executed = CASE
<foreach
collection=
"list"
item=
"item"
separator=
""
>
WHEN sad_id = #{item.sadId} THEN #{item.actualFloorStackExecuted}
</foreach>
END,
<!-- 实际多点陈列 -->
actual_multi_display = CASE
<foreach
collection=
"list"
item=
"item"
separator=
""
>
WHEN sad_id = #{item.sadId} THEN #{item.actualMultiDisplay}
</foreach>
END,
<!-- 实际多点陈列执行状态 -->
actual_multi_display_executed = CASE
<foreach
collection=
"list"
item=
"item"
separator=
""
>
WHEN sad_id = #{item.sadId} THEN #{item.actualMultiDisplayExecuted}
</foreach>
END,
<!-- 实际挂条数量 -->
actual_hanging_strip_quantity_form = CASE
<foreach
collection=
"list"
item=
"item"
separator=
""
>
WHEN sad_id = #{item.sadId} THEN #{item.actualHangingStripQuantityForm}
</foreach>
END,
<!-- 挂条执行状态 -->
hanging_strip_executed = CASE
<foreach
collection=
"list"
item=
"item"
separator=
""
>
WHEN sad_id = #{item.sadId} THEN #{item.hangingStripExecuted}
</foreach>
END,
<!-- 备注 -->
remark = CASE
<foreach
collection=
"list"
item=
"item"
separator=
""
>
WHEN sad_id = #{item.sadId} THEN #{item.remark}
</foreach>
END
</trim>
<!-- 只更新传入的主键范围(避免全表更新) -->
WHERE sad_id IN
<foreach
collection=
"list"
item=
"item"
open=
"("
separator=
","
close=
")"
>
#{item.sadId}
</foreach>
</update>
</mapper>
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论