提交 f30ace05 作者: lixiaojuan

打包

上级 96886500
# 构建阶段
FROM swr.cn-southwest-2.myhuaweicloud.com/wd/maven:3.8.5-openjdk-17 AS builder
USER root
WORKDIR /workspace
COPY . .
RUN mvn clean install -DskipTests
# 运行时阶段:直接用精简版 openjdk 镜像
FROM swr.cn-southwest-2.myhuaweicloud.com/wd/openjdk:17-fontconfig
MAINTAINER lixingyu
USER root
WORKDIR /workspace
# 复制 jar 包
COPY --from=builder /workspace/target/*.jar /workspace/app.jar
# 环境变量
ARG ENV_NAME
ARG NACOS_SERVER
ARG NACOS_NAMESPACE
ARG NACOS_REGISTER_IP
ARG PORT
ENV APP_OPTS1="-Dspring.profiles.active=${ENV_NAME}"
ENV APP_OPTS2="-Djasypt.encryptor.password=1@wdLkj90#chMsdzxA%2024"
ENV APP_OPTS3="-Dfile.encoding=utf-8"
ENV APP_OPTS4="-Duser.timezone=Asia/Shanghai"
# 无头模式避免 AWT 调用
ENV APP_OPTS9="-Djava.awt.headless=true"
ENV APP_OPTS5="-DNACOS_SERVER=${NACOS_SERVER}"
ENV APP_OPTS6="-DNACOS_NAMESPACE=${NACOS_NAMESPACE}"
ENV APP_OPTS7="-DNACOS_REGISTER_IP=${NACOS_REGISTER_IP}"
ENV JVM_OPTS="-Xmx2024M -Xms256M"
EXPOSE ${PORT}
ENTRYPOINT ["sh","-c","java $APP_OPTS1 $APP_OPTS2 $APP_OPTS3 $APP_OPTS4 $APP_OPTS9 $APP_OPTS5 $APP_OPTS6 $APP_OPTS7 $JVM_OPTS -jar /workspace/app.jar"]
\ No newline at end of file
#!/bin/bash
# build.sh - 功能包括:镜像构建,推送镜像,处理none镜像。
# author: lixingyu
# 使用方法:
# $ ./build.sh <env_name> <build_timestamp>
# 参数:
# env_name:环境变量 build_timestamp:构建时间戳
set -eu
# 获取脚本的绝对路径(包括文件名)
script_path=$(readlink -f "$0")
# 获取脚本所在的目录的绝对路径
script_dir=$(dirname "$script_path")
# 函数定义
function print_usage {
# 打印使用说明
sed -n '2,7p' "$0"
}
function handle {
local env_name=$1
local build_timestamp=$2
echo "执行的环境变量: ${env_name}"
echo "执行的构建时间戳: ${build_timestamp}"
if [ ! -f "$script_dir/env/env.${env_name}" ]; then
echo "错误: 文件 'env.${env_name}' 不存在." >&2
exit 1
fi
. $script_dir/env/env.${env_name}
local username="${repo_username}"
local passwd="${repo_passwd}"
local name="${svc_name}-${svc_env}"
local version=${version}-${build_timestamp}
local internal_ip="${internal_ip}"
echo "构建名称: ${name}"
echo "构建端口: ${svc_port}"
echo "部署节点: ${internal_ip}"
echo "构建版本: ${version}"
echo "推送仓库: ${domain}/${namespace}"
echo "完整镜像: ${domain}/${namespace}/${name}:${version}"
echo "---构建镜像---"
docker build --build-arg NACOS_REGISTER_IP=${internal_ip} --build-arg NACOS_SERVER=${nacos_server} --build-arg NACOS_NAMESPACE=${nacos_namespace} \
--build-arg ENV_NAME=${svc_env} --build-arg PORT=${svc_port} -f ./build/Dockerfile -t ${domain}/${namespace}/${name}:${version} .
echo "---推送镜像---"
docker login -u=$username -p=${passwd} ${domain}
docker push ${domain}/${namespace}/${name}:${version}
echo "---清理none镜像---"
docker container prune -f
docker image prune -af
}
# 主程序入口点
function main {
if [ "$#" -ne 2 ]; then
print_usage
echo "错误: 需要提供两个个参数 <env_name> <build_timestamp>" >&2
exit 1
fi
local env_name=$1
local build_timestamp=$2
handle "${env_name}" "${build_timestamp}"
}
# 错误处理
trap 'echo 发生了错误,脚本中断.' ERR
# 调用主函数
main "$@"
# 镜像仓库
## 版本 T1.0.0: T为测试版,R为稳定版
version="T1.0.0" # 可自定义
## 镜像仓库地址
domain="swr.cn-southwest-2.myhuaweicloud.com"
## 镜像分组,按部门区分
namespace="wd"
# 服务配置
## 部署节点的内网IP
internal_ip=server-1-95-67-224.ciglobal.cn
## 数据挂载根目录
root_dir="/zzsn"
## 需根据日志配置填写,比如:logback-spring.xml
svc_logs="/workspace/logs"
## 环境变量 test or prod
svc_env="prod"
## 服务名
svc_name="excel-export-service"
## 服务端口
svc_port="9120"
# nacos配置
## 往nacos注册IP,配置文件使用变量 NACOS_REGISTER_IP
## nacos服务地址, 配置文件使用 NACOS_SERVER
nacos_server="server-1-95-77-159.ciglobal.cn:8848"
## nacos命名空间, 配置文件使用 NACOS_NAMESPACE
nacos_namespace="smartProd"
# 资源限制
## cpu
limit_cpu=1.0
limit_mem=2g
......@@ -8,7 +8,7 @@ namespace="wd"
# 服务配置
## 部署节点的内网IP
internal_ip=server-1-95-14-24.ciglobal.cn
internal_ip=server-1-95-77-159.ciglobal.cn
## 数据挂载根目录
root_dir="/zzsn"
## 需根据日志配置填写,比如:logback-spring.xml
......@@ -18,7 +18,7 @@ svc_env="test"
## 服务名
svc_name="excel-export-service"
## 服务端口
svc_port="8089"
svc_port="9120"
# nacos配置
## 往nacos注册IP,配置文件使用变量 NACOS_REGISTER_IP
......
#!/bin/bash
# run.sh - 功能包括:运行容器,处理none镜像。
# author: lixingyu
# 使用方法:
# $ ./run.sh <env_name> <build_timestamp>
# 参数:
# env_name:环境变量 build_timestamp:构建时间戳
set -eu
# 获取脚本的绝对路径(包括文件名)
script_path=$(readlink -f "$0")
# 获取脚本所在的目录的绝对路径
script_dir=$(dirname "$script_path")
# 函数定义
function print_usage {
# 打印使用说明
sed -n '2,7p' "$0"
}
function handle {
local env_name=$1
local build_timestamp=$2
echo "执行的环境变量: ${env_name}"
echo "执行的构建时间戳: ${build_timestamp}"
if [ ! -f "$script_dir/env/env.${env_name}" ]; then
echo "错误: 文件 'env.${env_name}' 不存在." >&2
exit 1
fi
. $script_dir/env/env.${env_name}
local dir="${root_dir}/${svc_name}/${svc_env}"
local name="${svc_name}-${svc_env}"
local version=${version}-${build_timestamp}
local internal_ip="${internal_ip}"
echo "---运行容器: ${name}:${version}---"
# 构建 JSON 数据
json_data="{ \
\"container_name\": \"$name\", \
\"image_version\": \"$domain/$namespace/$name:$version\", \
\"ports\": [\"$svc_port:$svc_port\"], \
\"mount_infos\": [\"/etc/localtime:/etc/localtime:ro\", \"$dir/logs:$svc_logs\"], \
\"cpu_count\": $limit_cpu, \
\"memory_limit\": \"$limit_mem\", \
\"ulimit\": \"nofile=65535:65535\" \
}"
# 执行 curl 请求,并将响应结果存储在 response 变量中
response=$(curl -s -X POST -H "Content-Type: application/json" -H "X-API-Key: uOyKfp20pdM3MFhr3KAQBoe1UHCaZLUeeLephB57MPvGXTY05Eis5eaxta6fEtpa" -d "$json_data" "http://$internal_ip:10080/start-container")
echo "响应结果: ${response}"
# 检查响应中是否包含 "succeed"
if echo "$response" | grep -q "successfully"; then
echo "部署成功"
exit 0
else
echo "部署失败"
exit 1
fi
}
# 主程序入口点
function main {
if [ "$#" -ne 2 ]; then
print_usage
echo "错误: 需要提供两个个参数 <env_name> <build_timestamp>" >&2
exit 1
fi
local env_name=$1
local build_timestamp=$2
handle "${env_name}" "${build_timestamp}"
}
# 错误处理
trap 'echo 发生了错误,脚本中断.' ERR
# 调用主函数
main "$@"
......@@ -121,7 +121,7 @@
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
<version>5.4.1</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
......@@ -131,7 +131,7 @@
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
<version>5.4.1</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
......
......@@ -21,6 +21,7 @@ import java.io.IOException;
import java.net.URLEncoder;
import java.util.*;
import com.zzsn.excelexportservice.dto.ExportDataResponse.Header;
/**
* @Author: lxj
* @Date: 2025/9/18 11:07
......@@ -29,6 +30,12 @@ import com.zzsn.excelexportservice.dto.ExportDataResponse.Header;
@Slf4j
@RequiredArgsConstructor
public class ExcelExportService {
static {
// 设置无头模式,避免字体配置问题
System.setProperty("java.awt.headless", "true");
}
@Resource
private CmsSearch cmsSearch;
......@@ -39,66 +46,124 @@ public class ExcelExportService {
sendError(response, "导出失败: 入参为空");
return;
}
SXSSFWorkbook workbook = new SXSSFWorkbook(1000);
workbook.setCompressTempFiles(true);
for (ExportReq exportReq : exportReqList) {
ExportDataResponse resp = null;
String serviceName = exportReq.getServiceName();
Map<String, Object> queryParams = exportReq.getQueryParams();
ExportStrategy strategy = exportStrategyFactory.getStrategy(serviceName, exportReq.getApiPath());
if (strategy == null) {
log.error("No strategy found for service: {}, path: {}", serviceName, exportReq.getApiPath());
sendError(response, "导出失败: 未找到匹配的服务策略");
return;
}
SXSSFWorkbook workbook = null;
try {
workbook = new SXSSFWorkbook(1000);
workbook.setCompressTempFiles(true);
for (ExportReq exportReq : exportReqList) {
ExportDataResponse resp = null;
String serviceName = exportReq.getServiceName();
Map<String, Object> queryParams = exportReq.getQueryParams() != null
? exportReq.getQueryParams()
: new HashMap<>();
ExportStrategy strategy = exportStrategyFactory.getStrategy(serviceName, exportReq.getApiPath());
if (strategy == null) {
log.error("No strategy found for service: {}, path: {}", serviceName, exportReq.getApiPath());
sendError(response, "导出失败: 未找到匹配的服务策略");
return;
}
resp = strategy.execute(request, queryParams, exportReq);
resp = strategy.execute(request, queryParams, exportReq);
if (resp == null || resp.getCode() != 0) {
log.error("导出数据失败, serviceName: {}, apiPath: {}, errorMsg: {}",
serviceName, exportReq.getApiPath(), resp != null ? resp.getMsg() : "null");
sendError(response, resp != null ? resp.getMsg() : "导出失败: 未知错误");
return;
}
if (resp == null || resp.getCode() != 0) {
log.error("导出数据失败, serviceName: {}, apiPath: {}, errorMsg: {}",
serviceName, exportReq.getApiPath(), resp != null ? resp.getMsg() : "null");
sendError(response, resp != null ? resp.getMsg() : "导出失败: 未知错误");
return;
}
// 创建 sheet
Sheet sheet = workbook.createSheet(
StringUtils.isNotBlank(exportReq.getSheetName())
? exportReq.getSheetName()
: "Sheet_" + serviceName);
// 安全创建 sheet
String sheetName = generateSafeSheetName(
StringUtils.isNotBlank(exportReq.getSheetName())
? exportReq.getSheetName()
: "Sheet_" + serviceName
);
Sheet sheet = workbook.createSheet(sheetName);
int startRow = 0;
int endCol = 13;
// 判断是否有图片
if (StringUtils.isNotBlank(exportReq.getBase64Img())) {
log.info("开始插入图片------->" + System.currentTimeMillis());
drawImg(workbook, sheet, exportReq.getBase64Img(), startRow, endCol);
startRow++; // 图片占用一行,从下一行开始写标题
}
int startRow = 0;
int endCol = 13;
List<Map<String, Object>> dataList = resp.getDataList() != null
? resp.getDataList()
: new ArrayList<>();
List<Header> headers = resp.getHeaders() != null
? resp.getHeaders()
: new ArrayList<>();
// 判断是否有图片
if (StringUtils.isNotBlank(exportReq.getBase64Img())) {
log.info("开始插入图片------->" + System.currentTimeMillis());
drawImg(workbook, sheet, exportReq.getBase64Img(), startRow, endCol);
startRow++; // 图片占用一行,从下一行开始写标题
}
// 写标题
log.info("开始插入标题------->" + System.currentTimeMillis());
handlerExcelTitle(workbook, sheet, startRow, headers);
List<Map<String, Object>> dataList = resp.getDataList();
List<Header> headers = resp.getHeaders();
// 写数据
log.info("开始写入数据------->" + System.currentTimeMillis());
handlerExcelData(workbook, sheet, startRow + 1, headers, dataList);
// 写标
log.info("开始插入标题------->" + System.currentTimeMillis());
handlerExcelTitle(workbook, sheet, startRow, headers);
// 设置手动列宽,避免自动列宽计算导致的字体问
setupManualColumnWidth(sheet, headers.size());
}
// 写数据
log.info("开始写入数据------->" + System.currentTimeMillis());
handlerExcelData(workbook, sheet, startRow + 1, headers, dataList);
// 最后写到 response
log.info("生成excel结束------->" + System.currentTimeMillis());
writeExcelToResponse("导出", workbook, response);
} catch (Exception e) {
log.error("导出Excel异常", e);
sendError(response, "导出失败: " + e.getMessage());
} finally {
// 确保资源释放
if (workbook != null) {
try {
workbook.close();
} catch (IOException e) {
log.warn("关闭workbook异常", e);
}
workbook.dispose();
}
}
}
// 最后写到 response
log.info("生成excel结束------->" + System.currentTimeMillis());
writeExcelToResponse("导出", workbook, response);
/**
* 生成安全的sheet名称
*/
private String generateSafeSheetName(String name) {
if (StringUtils.isBlank(name)) {
return "Sheet1";
}
// 移除Excel不允许的字符:\ / * [ ] : ?
String safeName = name.replaceAll("[\\\\/*\\[\\]:?]", "");
// 限制长度在31个字符以内
return safeName.length() > 31 ? safeName.substring(0, 31) : safeName;
}
/**
* 设置手动列宽
*/
private void setupManualColumnWidth(Sheet sheet, int columnCount) {
try {
// 根据列数设置合适的宽度,避免自动列宽计算
for (int i = 0; i < columnCount; i++) {
sheet.setColumnWidth(i, 5000); // 设置固定宽度
}
} catch (Exception e) {
log.warn("设置列宽失败,使用默认宽度", e);
}
}
@SuppressWarnings("unchecked")
private <T> T getParam(Map<String, Object> params, String key, T defaultVal) {
if (params == null) {
return defaultVal;
}
Object val = params.get(key);
if (val == null) {
return defaultVal;
......@@ -111,62 +176,77 @@ public class ExcelExportService {
}
}
private static void sendError(HttpServletResponse response, String msg) {
response.reset();
response.setStatus(1);
response.setContentType("text/plain;charset=UTF-8");
try {
response.getWriter().write(msg != null ? msg : "未知错误");
} catch (IOException e) {
e.printStackTrace();
private static void sendError(HttpServletResponse response, String msg) {
try {
response.reset();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().write(msg != null ? msg : "未知错误");
} catch (IOException e) {
log.error("发送错误响应失败", e);
}
}
}
// 插入图片(Base64)
private void drawImg(SXSSFWorkbook workbook, Sheet sheet, String base64Img, int rowIndex, int endCol) {
Row firstRow = sheet.createRow(rowIndex);
for (int i = 0; i < endCol; i++) {
firstRow.createCell(i);
}
firstRow.setHeightInPoints(230);
// 合并单元格
sheet.addMergedRegion(new CellRangeAddress(rowIndex, rowIndex, 0, endCol));
if (base64Img != null && !base64Img.isBlank()) {
int start = base64Img.indexOf(",");
if (start != -1) {
base64Img = base64Img.substring(start + 1);
try {
Row firstRow = sheet.createRow(rowIndex);
for (int i = 0; i < endCol; i++) {
firstRow.createCell(i);
}
byte[] imgBytes = Base64.getDecoder().decode(base64Img);
firstRow.setHeightInPoints(230);
// 合并单元格
sheet.addMergedRegion(new CellRangeAddress(rowIndex, rowIndex, 0, endCol));
if (StringUtils.isNotBlank(base64Img)) {
int start = base64Img.indexOf(",");
String imgData = (start != -1) ? base64Img.substring(start + 1) : base64Img;
if (StringUtils.isNotBlank(imgData)) {
byte[] imgBytes = Base64.getDecoder().decode(imgData);
Drawing<?> drawingPatriarch = sheet.createDrawingPatriarch();
ClientAnchor anchor = drawingPatriarch.createAnchor(0, 0, 0, 0, 0, rowIndex, endCol, rowIndex + 1);
int pictureIdx = workbook.addPicture(imgBytes, Workbook.PICTURE_TYPE_PNG);
drawingPatriarch.createPicture(anchor, pictureIdx);
Drawing<?> drawingPatriarch = sheet.createDrawingPatriarch();
ClientAnchor anchor = drawingPatriarch.createAnchor(0, 0, 0, 0, 0, rowIndex, endCol, rowIndex + 1);
int pictureIdx = workbook.addPicture(imgBytes, Workbook.PICTURE_TYPE_PNG);
drawingPatriarch.createPicture(anchor, pictureIdx);
}
}
} catch (Exception e) {
log.warn("插入图片失败", e);
}
}
// 写入标题
private void handlerExcelTitle(Workbook workbook, Sheet sheet, int rowIndex, List<Header> headers) {
Row row = sheet.createRow(rowIndex);
CellStyle titleStyle = getTitleStyle(workbook);
if (headers != null && !headers.isEmpty()) {
if (headers == null || headers.isEmpty()) {
return;
}
try {
Row row = sheet.createRow(rowIndex);
CellStyle titleStyle = getTitleStyle(workbook);
for (int i = 0; i < headers.size(); i++) {
Cell cell = row.createCell(i);
cell.setCellStyle(titleStyle);
cell.setCellValue(headers.get(i).getTitle());
String title = headers.get(i) != null ? headers.get(i).getTitle() : "";
cell.setCellValue(title != null ? title : "");
}
} catch (Exception e) {
log.warn("写入标题失败", e);
}
}
// Title 样式
private CellStyle getTitleStyle(Workbook workbook) {
Font font = workbook.createFont();
font.setFontName("Courier New");
font.setBold(true);
CellStyle style = workbook.createCellStyle();
style.setFont(font);
try {
Font font = workbook.createFont();
font.setFontName("Arial"); // 使用通用字体
font.setBold(true);
style.setFont(font);
} catch (Exception e) {
log.warn("创建标题样式失败,使用默认样式", e);
}
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
return style;
......@@ -175,54 +255,71 @@ private static void sendError(HttpServletResponse response, String msg) {
// 写入数据
private void handlerExcelData(Workbook workbook, Sheet sheet, int startRowIndex,
List<Header> headers, List<Map<String, Object>> dataList) {
CellStyle dataStyle = getDataStyle(workbook);
if (headers != null && !headers.isEmpty() && dataList != null && !dataList.isEmpty()) {
if (headers == null || headers.isEmpty() || dataList == null || dataList.isEmpty()) {
return;
}
try {
CellStyle dataStyle = getDataStyle(workbook);
for (int j = 0; j < dataList.size(); j++) {
Row row = sheet.createRow(startRowIndex + j);
Map<String, Object> dataMap = dataList.get(j);
if (dataMap == null) {
continue;
}
for (int i = 0; i < headers.size(); i++) {
Cell cell = row.createCell(i);
cell.setCellStyle(dataStyle);
Object value = dataMap.get(headers.get(i).getField());
Header header = headers.get(i);
if (header == null) {
cell.setCellValue("");
continue;
}
String field = header.getField();
Object value = dataMap.get(field);
cell.setCellValue(value == null ? "" : value.toString());
}
}
} catch (Exception e) {
log.warn("写入数据失败", e);
}
}
// 数据样式
private CellStyle getDataStyle(Workbook workbook) {
Font font = workbook.createFont();
font.setFontName("Courier New");
CellStyle style = workbook.createCellStyle();
style.setFont(font);
try {
Font font = workbook.createFont();
font.setFontName("Arial"); // 使用通用字体
style.setFont(font);
} catch (Exception e) {
log.warn("创建数据样式失败,使用默认样式", e);
}
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
return style;
}
// 将数据写入 HttpServletResponse(流式,不占用内存)
// 将数据写入 HttpServletResponse
private void writeExcelToResponse(String fileNamePrefix, SXSSFWorkbook workbook, HttpServletResponse response) {
try {
response.reset();
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-disposition",
"attachment;filename=" + URLEncoder.encode(fileNamePrefix + System.currentTimeMillis() + ".xlsx", "UTF-8"));
String fileName = URLEncoder.encode(
(fileNamePrefix != null ? fileNamePrefix : "导出") + System.currentTimeMillis() + ".xlsx",
"UTF-8"
);
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
workbook.write(response.getOutputStream());
response.getOutputStream().flush();
} catch (IOException e) {
log.error("写入响应流失败", e);
sendError(response, "导出 Excel 异常:" + e.getMessage());
} finally {
// 清理临时文件
try {
workbook.close();
} catch (IOException ignored) {
}
workbook.dispose();
}
}
}
}
\ No newline at end of file
server:
port: 8082
port: 9120
spring:
application:
name: excel-export-service
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论