Commit da06d17c by zhangxingmin

Merge remote-tracking branch 'origin/dev_zxm' into test

parents 3c19d30b 2baef10c
package com.yd.oss.api.controller;
import com.yd.common.result.Result;
import com.yd.oss.api.service.ApiExcelService;
import com.yd.oss.feign.client.ApiExcelFeignClient;
import com.yd.oss.feign.request.ApiOssExportAppointmentExcelRequest;
import com.yd.oss.feign.response.ApiOssExcelParseResponse;
import com.yd.oss.feign.response.ApiOssExportAppointmentExcelResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* Excel接口信息
*
* @author zxm
* @since 2025-07-31
*/
@RestController
@RequestMapping("/excel")
@Validated
public class ApiExcelController implements ApiExcelFeignClient {
@Autowired
private ApiExcelService apiExcelService;
/**
* 导出excel-预约信息
* @param request
* @return
*/
@Override
public Result<ApiOssExportAppointmentExcelResponse> exportAppointment(ApiOssExportAppointmentExcelRequest request) {
return apiExcelService.exportAppointment(request);
}
/**
* 通用-Excel解析(支持多sheet页解析)
* @param file
* @param sheetClassNames
* @return
*/
@Override
public Result<ApiOssExcelParseResponse> parse(MultipartFile file, String[] sheetClassNames) {
return apiExcelService.parse(file,sheetClassNames);
}
}
package com.yd.oss.api.service;
import com.yd.common.result.Result;
import com.yd.oss.feign.request.ApiOssExportAppointmentExcelRequest;
import com.yd.oss.feign.response.ApiOssExcelParseResponse;
import com.yd.oss.feign.response.ApiOssExportAppointmentExcelResponse;
import org.springframework.web.multipart.MultipartFile;
public interface ApiExcelService {
Result<ApiOssExportAppointmentExcelResponse> exportAppointment(ApiOssExportAppointmentExcelRequest request);
Result<ApiOssExcelParseResponse> parse(MultipartFile file, String[] sheetClassNames);
}
package com.yd.oss.api.service.impl;
import com.yd.common.exception.BusinessException;
import com.yd.common.result.Result;
import com.yd.oss.api.service.ApiExcelService;
import com.yd.oss.feign.request.ApiOssExportAppointmentExcelRequest;
import com.yd.oss.feign.response.ApiOssExcelParseResponse;
import com.yd.oss.feign.response.ApiOssExportAppointmentExcelResponse;
import com.yd.oss.service.service.AppointmentExcelService;
import com.yd.oss.service.service.ExcelParserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
@Slf4j
@Service
public class ApiExcelServiceImpl implements ApiExcelService {
@Autowired
private AppointmentExcelService appointmentExcelService;
@Autowired
private ExcelParserService excelParserService;
/**
* 导出excel-预约信息
* @param request
* @return
*/
@Override
public Result<ApiOssExportAppointmentExcelResponse> exportAppointment(ApiOssExportAppointmentExcelRequest request) {
ApiOssExportAppointmentExcelResponse response = new ApiOssExportAppointmentExcelResponse();
String url = appointmentExcelService.exportAppointment(request.getData(),
request.getTemplateType(),
request.getAppointmentBizId());
response.setUrl(url);
return Result.success(response);
}
/**
* 通用-Excel解析(支持多sheet页解析)
* @param file
* @param sheetClassNames
* @return
*/
@Override
public Result<ApiOssExcelParseResponse> parse(MultipartFile file,
String[] sheetClassNames) {
ApiOssExcelParseResponse response = new ApiOssExcelParseResponse();
try {
// 将类名字符串转换为Class对象
Class<?>[] sheetClasses = new Class<?>[sheetClassNames.length];
for (int i = 0; i < sheetClassNames.length; i++) {
sheetClasses[i] = Class.forName(sheetClassNames[i]);
}
// 解析Excel
Map<Integer, Object> result = excelParserService.parseExcelWithMultipleSheets(file, sheetClasses);
response.setMap(result);
return Result.success(response);
} catch (Exception e) {
throw new BusinessException("Excel解析异常!");
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="ExternalSystem" externalSystem="Maven" />
<component name="FacetManager">
<facet type="Spring" name="Spring">
<configuration />
</facet>
<facet type="web" name="Web">
<configuration>
<webroots />
<sourceRoots>
<root url="file://$MODULE_DIR$/src/main/java" />
<root url="file://$MODULE_DIR$/src/main/resources" />
</sourceRoots>
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
<output url="file://$MODULE_DIR$/../target/classes" />
<output-test url="file://$MODULE_DIR$/../target/test-classes" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="jdk" jdkName="1.8" jdkType="JavaSDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
package com.yd.oss.feign.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// Excel集合注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelCollection {
Class<?> type(); // 集合元素类型
int startRow(); // 集合起始行
int endRow() default -1; // 集合结束行(-1表示自动检测)
}
package com.yd.oss.feign.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// Excel字段注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelField {
String name(); // 标题名称
int titleRow(); // 标题所在行
int titleCol(); // 标题所在列
int valueRow() default -1; // 值所在行(默认与标题同行)
int valueCol() default -1; // 值所在列(默认标题列+1)
String dateFormat() default "yyyy/MM/dd"; // 日期格式
}
package com.yd.oss.feign.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// Excel Sheet注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ExcelSheet {
int sheetIndex() default 0; // Sheet索引
String sheetName() default ""; // Sheet名称
}
package com.yd.oss.feign.client;
import com.yd.common.result.Result;
import com.yd.oss.feign.fallback.ApiExcelFeignFallbackFactory;
import com.yd.oss.feign.request.ApiOssExportAppointmentExcelRequest;
import com.yd.oss.feign.response.ApiOssExcelParseResponse;
import com.yd.oss.feign.response.ApiOssExportAppointmentExcelResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
/**
* Excel信息Feign客户端
*/
@FeignClient(name = "yd-oss-api", fallbackFactory = ApiExcelFeignFallbackFactory.class)
public interface ApiExcelFeignClient {
/**
* 导出excel-预约信息
* @return
*/
@PostMapping("/export/appointment")
Result<ApiOssExportAppointmentExcelResponse> exportAppointment(@Validated @RequestBody ApiOssExportAppointmentExcelRequest request);
/**
* 通用-Excel解析(支持多sheet页解析)
* @return
*/
@PostMapping(value = "/parse-excel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
Result<ApiOssExcelParseResponse> parse(@RequestPart("file") MultipartFile file,
@RequestPart("sheetClassNames") String[] sheetClassNames);
}
package com.yd.oss.feign.fallback;
import com.yd.common.result.Result;
import com.yd.oss.feign.client.ApiExcelFeignClient;
import com.yd.oss.feign.request.ApiOssExcelParseRequest;
import com.yd.oss.feign.request.ApiOssExportAppointmentExcelRequest;
import com.yd.oss.feign.response.ApiOssExcelParseResponse;
import com.yd.oss.feign.response.ApiOssExportAppointmentExcelResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
/**
* Excel信息Feign降级处理
*/
@Slf4j
@Component
public class ApiExcelFeignFallbackFactory implements FallbackFactory<ApiExcelFeignClient> {
@Override
public ApiExcelFeignClient create(Throwable cause) {
return new ApiExcelFeignClient() {
@Override
public Result<ApiOssExportAppointmentExcelResponse> exportAppointment(ApiOssExportAppointmentExcelRequest request) {
return null;
}
@Override
public Result<ApiOssExcelParseResponse> parse(MultipartFile file, String[] sheetClassNames) {
return null;
}
};
}
}
package com.yd.oss.feign.request;
import lombok.Data;
@Data
public class ApiOssExcelParseRequest {
/**
* Sheet对应类的全限定名
*/
private String[] sheetClassNames;
}
package com.yd.oss.feign.request;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.util.Map;
@Data
public class ApiOssExportAppointmentExcelRequest {
/**
* 需要导出的数据
*/
private Map<String, Object> data;
/**
* 生成文件的模板类型
*/
@NotBlank(message = "生成文件的模板类型不能为空")
private String templateType;
/**
* 预约信息主表唯一业务ID
*/
private String appointmentBizId;
}
package com.yd.oss.feign.response;
import lombok.Data;
import java.util.Map;
@Data
public class ApiOssExcelParseResponse {
/**
* 返回解析后的数据
*/
private Map<Integer, Object> map;
}
package com.yd.oss.feign.response;
import lombok.Data;
@Data
public class ApiOssExportAppointmentExcelResponse {
/**
* 访问路径
*/
private String url;
}
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="ExternalSystem" externalSystem="Maven" />
<component name="FacetManager">
<facet type="Spring" name="Spring">
<configuration />
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
<output url="file://$MODULE_DIR$/../target/classes" />
<output-test url="file://$MODULE_DIR$/../target/test-classes" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="jdk" jdkName="1.8" jdkType="JavaSDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
......@@ -107,5 +107,33 @@
<groupId>org.apache.xmlbeans</groupId>
<artifactId>xmlbeans</artifactId>
</dependency>
<!-- EasyPOI核心依赖 -->
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-web</artifactId>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-annotation</artifactId>
</dependency>
<!-- EasyPOI模板功能需要的额外依赖 -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.jxls</groupId>
<artifactId>jxls</artifactId>
</dependency>
<dependency>
<groupId>org.jxls</groupId>
<artifactId>jxls-poi</artifactId>
</dependency>
</dependencies>
</project>
......@@ -18,14 +18,19 @@ public class OssConfig {
@Autowired
private IOssProviderService ossProviderService;
//默认的OSS提供商
private OssProvider currentProvider;
//默认存储桶名称
private String defaultBucket;
//默认服务端点
private String defaultEndpoint;
@PostConstruct
public void init() {
try {
this.currentProvider = ossProviderService.getDefaultProvider();
this.defaultBucket = currentProvider.getBucketName();
this.defaultEndpoint = currentProvider.getEndpoint();
log.info("默认OSS提供商初始化成功: {}", currentProvider.getName());
} catch (Exception e) {
log.error("默认OSS提供商初始化失败", e);
......@@ -49,6 +54,11 @@ public class OssConfig {
}
@Bean
public String defaultEndpoint() {
return defaultEndpoint;
}
@Bean
public OssProvider currentProvider() {
return currentProvider;
}
......
package com.yd.oss.service.service;
import java.util.Map;
public interface AppointmentExcelService {
String exportAppointment(Map<String, Object> data,
String templateType,
String appointmentBizId);
}
package com.yd.oss.service.service;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
public interface ExcelParserService {
Map<Integer, Object> parseExcelWithMultipleSheets(MultipartFile file, Class<?>... sheetClasses) throws Exception;
}
package com.yd.oss.service.service;
import org.apache.poi.ss.usermodel.Workbook;
import java.io.File;
import java.io.IOException;
public interface ExcelService {
File downloadTemplateToTempFile(String ossObjectKey) throws IOException;
File saveWorkbookToTempFile(Workbook workbook) throws IOException;
}
......@@ -3,7 +3,6 @@ package com.yd.oss.service.service;
import com.yd.oss.service.dto.FileMetadata;
import com.yd.oss.service.dto.UploadResult;
import com.yd.oss.service.model.OssProvider;
import java.io.InputStream;
import java.time.Duration;
......@@ -65,5 +64,9 @@ public interface OssService {
// 获取上传结果(包含文件信息和访问URL)
UploadResult getUploadResult(String fileKey, Duration expiration);
String upload(byte[] content, String fileName);
String upload(InputStream inputStream, String fileName);
}
......@@ -2,6 +2,7 @@ package com.yd.oss.service.service.impl;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.CannedAccessControlList;
import com.aliyun.oss.model.OSSObject;
import com.aliyun.oss.model.ObjectMetadata;
import com.aliyun.oss.model.PutObjectRequest;
......@@ -49,6 +50,9 @@ public class AliYunOssServiceImpl implements OssService {
private String defaultBucket; // 注入默认存储桶
@Autowired
private String defaultEndpoint; // 注入默认服务端点
@Autowired
private OssProvider currentProvider; // 注入当前提供商
/**
......@@ -714,4 +718,72 @@ public class AliYunOssServiceImpl implements OssService {
file.setId(-1L); // 使用无效ID
return file;
}
/**
* 上传字节数组到OSS并设置为公共读权限
* @param content 文件内容字节数组
* @param fileName 文件名
* @return 公共访问URL
*/
public String upload(byte[] content, String fileName) {
try {
// 生成唯一文件名
String objectName = "appointment/excel/" + UUID.randomUUID() + "/" + fileName;
// 创建上传请求
PutObjectRequest putObjectRequest = new PutObjectRequest(
defaultBucket,
objectName,
new ByteArrayInputStream(content)
);
// 上传文件
ossClient.putObject(putObjectRequest);
// 设置对象访问权限为公共读
ossClient.setObjectAcl(defaultBucket, objectName, CannedAccessControlList.PublicRead);
// 构造公共访问URL
String publicUrl = "https://" + defaultBucket + "." + defaultEndpoint + "/" + objectName;
return publicUrl;
} catch (Exception e) {
throw new RuntimeException("上传文件到OSS失败", e);
}
}
/**
* 上传输入流到OSS并设置为公共读权限
* @param inputStream 文件输入流
* @param fileName 文件名
* @return 公共访问URL
*/
public String upload(InputStream inputStream, String fileName) {
try {
// 生成唯一文件名
String objectName = "appointment/excel/" + UUID.randomUUID() + "/" + fileName;
// 创建上传请求
PutObjectRequest putObjectRequest = new PutObjectRequest(
defaultBucket,
objectName,
inputStream
);
// 上传文件
ossClient.putObject(putObjectRequest);
// 设置对象访问权限为公共读
ossClient.setObjectAcl(defaultBucket, objectName, CannedAccessControlList.PublicRead);
// 构造公共访问URL
String publicUrl = "https://" + defaultBucket + "." + defaultEndpoint + "/" + objectName;
return publicUrl;
} catch (Exception e) {
throw new RuntimeException("上传文件到OSS失败", e);
}
}
}
package com.yd.oss.service.service.impl;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.TemplateExportParams;
import com.yd.common.utils.CommonUtil;
import com.yd.common.utils.DateUtil;
import com.yd.oss.service.dto.FileProdDto;
import com.yd.oss.service.service.AppointmentExcelService;
import com.yd.oss.service.service.ExcelService;
import com.yd.oss.service.service.IFileTemplateService;
import com.yd.oss.service.service.OssService;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.*;
import java.util.*;
/**
* 预约Excel服务类
*/
@Slf4j
@Service
public class AppointmentExcelServiceImpl implements AppointmentExcelService {
@Resource
private OssService ossService;
@Resource
private ExcelService excelService;
@Resource
private IFileTemplateService iFileTemplateService;
/**
* 导出预约信息并上传到OSS
* @param data
* @param ossObjectKey
* @param fileName
* @return
*/
public String exportAndUploadToOss(Map<String, Object> data, String ossObjectKey, String fileName) {
File tempFile = null;
File processedFile = null;
try {
// 1. 从OSS下载模板到临时文件
tempFile = excelService.downloadTemplateToTempFile(ossObjectKey);
// 2. 使用临时文件路径创建TemplateExportParams
TemplateExportParams params = new TemplateExportParams(tempFile.getAbsolutePath());
params.setSheetName(new String[]{"預約信息及個人資料", "健康信息及聲明", "財務資料分析(FNA)"});
params.setScanAllsheet(true);
// 3. 导出Excel
Workbook workbook = ExcelExportUtil.exportExcel(params, data);
// 4. 处理受益人动态行
processedFile = processBeneficiaryRows(workbook, data);
// 5. 重新读取处理后的工作簿
workbook.close();
workbook = WorkbookFactory.create(processedFile);
// 6. 将Workbook转换为字节数组
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
workbook.write(outputStream);
byte[] excelBytes = outputStream.toByteArray();
// 7. 上传到OSS
String ossUrl = ossService.upload(excelBytes, fileName);
// 8. 关闭资源
outputStream.close();
workbook.close();
return ossUrl;
} catch (Exception e) {
throw new RuntimeException("从OSS模板导出失败", e);
} finally {
// 9. 删除临时文件
if (tempFile != null && tempFile.exists()) {
tempFile.delete();
}
if (processedFile != null && processedFile.exists()) {
processedFile.delete();
}
}
}
/**
* 处理受益人动态行
* @param workbook
* @param data
* @return
* @throws IOException
*/
public File processBeneficiaryRows(Workbook workbook, Map<String, Object> data) throws IOException {
Sheet sheet = workbook.getSheet("預約信息及個人資料");
// 查找受益人数据区域的起始行和结束行
int beneficiaryTitleRowNum = -1;
int secondHolderRowNum = -1;
for (Row row : sheet) {
for (Cell cell : row) {
if (cell.getCellType() == CellType.STRING) {
String value = cell.getStringCellValue();
if ("受益人资料".equals(value.trim())) {
beneficiaryTitleRowNum = row.getRowNum();
} else if ("第二持有人资料(只适用儿童单)".equals(value.trim())) {
secondHolderRowNum = row.getRowNum();
break;
}
}
}
if (beneficiaryTitleRowNum != -1 && secondHolderRowNum != -1) {
break;
}
}
if (beneficiaryTitleRowNum == -1 || secondHolderRowNum == -1) {
throw new RuntimeException("未找到受益人资料或第二持有人资料区域");
}
// 获取第二持有人资料中英文姓名的列宽作为参考
int englishNameColumnWidth = -1;
Row secondHolderRow = sheet.getRow(secondHolderRowNum + 2); // 假设第二持有人资料在标题行后两行
if (secondHolderRow != null) {
Cell englishNameCell = secondHolderRow.getCell(3); // 英文姓名通常在D列(索引3)
if (englishNameCell != null) {
englishNameColumnWidth = sheet.getColumnWidth(3); // 获取D列的宽度
}
}
// 获取受益人列表
List<Map<String, Object>> beneficiaries = (List<Map<String, Object>>) data.get("beneficiaryList");
if (beneficiaries == null || beneficiaries.isEmpty()) {
return excelService.saveWorkbookToTempFile(workbook);
}
// 创建样式 - 保持与模板一致的边框
CellStyle dataStyle = workbook.createCellStyle();
dataStyle.setBorderTop(BorderStyle.THIN);
dataStyle.setBorderBottom(BorderStyle.THIN);
dataStyle.setBorderLeft(BorderStyle.THIN);
dataStyle.setBorderRight(BorderStyle.THIN);
// 创建浅黄色背景样式 (#F4F492)
CellStyle yellowBgStyle = workbook.createCellStyle();
yellowBgStyle.cloneStyleFrom(dataStyle);
yellowBgStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
yellowBgStyle.setFillForegroundColor(IndexedColors.LIGHT_YELLOW.getIndex());
// 创建浅橙色背景样式 (#F8CBAD)
CellStyle orangeBgStyle = workbook.createCellStyle();
orangeBgStyle.cloneStyleFrom(dataStyle);
orangeBgStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
orangeBgStyle.setFillForegroundColor(IndexedColors.LIGHT_ORANGE.getIndex());
// 计算需要插入的行数
int existingBeneficiaryRows = secondHolderRowNum - beneficiaryTitleRowNum - 1; // 减去标题行
int neededBeneficiaryRows = beneficiaries.size() * 4 + (beneficiaries.size() - 1); // 每个受益人占4行,加上空行
int rowsToInsert = neededBeneficiaryRows - existingBeneficiaryRows;
if (rowsToInsert > 0) {
// 下移后续内容,包括第二持有人资料
sheet.shiftRows(secondHolderRowNum, sheet.getLastRowNum(), rowsToInsert, true, false);
secondHolderRowNum += rowsToInsert; // 更新第二持有人资料的行号
} else if (rowsToInsert < 0) {
// 上移后续内容
sheet.shiftRows(secondHolderRowNum, sheet.getLastRowNum(), rowsToInsert, true, false);
secondHolderRowNum += rowsToInsert; // 更新第二持有人资料的行号
}
// 清除模板中的示例数据(保留"受益人资料"标题行)
for (int i = beneficiaryTitleRowNum + 1; i < secondHolderRowNum; i++) {
Row row = sheet.getRow(i);
if (row != null) {
sheet.removeRow(row);
}
}
// 插入新的受益人数据
int currentRow = beneficiaryTitleRowNum + 1; // 从标题行下一行开始(空行)
for (int i = 0; i < beneficiaries.size(); i++) {
Map<String, Object> beneficiary = beneficiaries.get(i);
// 1. 添加受益人标题行
Row titleRow = sheet.createRow(currentRow++);
Cell titleCell = titleRow.createCell(0);
titleCell.setCellValue("受益人" + (i + 1));
titleCell.setCellStyle(dataStyle);
// 2. 添加姓名行
Row nameRow = sheet.createRow(currentRow++);
// 中文姓名
Cell nameLabelCell = nameRow.createCell(0);
nameLabelCell.setCellValue("中文姓名");
nameLabelCell.setCellStyle(dataStyle);
Cell nameValueCell = nameRow.createCell(1);
nameValueCell.setCellValue((String) beneficiary.get("beneficiaryName"));
nameValueCell.setCellStyle(yellowBgStyle); // 应用浅黄色背景
// 英文姓名
Cell enNameLabelCell = nameRow.createCell(2);
enNameLabelCell.setCellValue("英文姓名 (同护照)");
enNameLabelCell.setCellStyle(dataStyle);
Cell enNameValueCell = nameRow.createCell(3);
enNameValueCell.setCellValue((String) beneficiary.get("beneficiaryNameEn"));
enNameValueCell.setCellStyle(yellowBgStyle); // 应用浅黄色背景
// 设置英文姓名列宽与第二持有人资料一致
if (englishNameColumnWidth != -1) {
sheet.setColumnWidth(3, englishNameColumnWidth);
}
// 3. 添加生日/ID行
Row infoRow = sheet.createRow(currentRow++);
// 出生日期
Cell birthLabelCell = infoRow.createCell(0);
birthLabelCell.setCellValue("出生日期 (西元 年/月/日)");
birthLabelCell.setCellStyle(dataStyle);
Cell birthValueCell = infoRow.createCell(1);
birthValueCell.setCellValue(DateUtil.formatDate(beneficiary.get("beneficiaryBirthTime")));
birthValueCell.setCellStyle(yellowBgStyle); // 应用浅黄色背景
// 身份证号码
Cell idLabelCell = infoRow.createCell(2);
idLabelCell.setCellValue("身份证号码");
idLabelCell.setCellStyle(dataStyle);
Cell idValueCell = infoRow.createCell(3);
idValueCell.setCellValue((String) beneficiary.get("beneficiaryIdNumber"));
idValueCell.setCellStyle(yellowBgStyle); // 应用浅黄色背景
// 4. 添加关系/比例行
Row relationRow = sheet.createRow(currentRow++);
// 與受保人關係
Cell relationLabelCell = relationRow.createCell(0);
relationLabelCell.setCellValue("與受保人關係");
relationLabelCell.setCellStyle(dataStyle);
Cell relationValueCell = relationRow.createCell(1);
relationValueCell.setCellValue((String) beneficiary.get("beneficiaryInsurantRel"));
relationValueCell.setCellStyle(orangeBgStyle); // 应用浅橙色背景
// 受益比例
Cell percentLabelCell = relationRow.createCell(2);
percentLabelCell.setCellValue("受益比例 (%)");
percentLabelCell.setCellStyle(dataStyle);
Cell percentValueCell = relationRow.createCell(3);
percentValueCell.setCellValue(CommonUtil.formatPercentage(beneficiary.get("beneficiaryBenefitRatio")));
percentValueCell.setCellStyle(yellowBgStyle); // 应用浅黄色背景
// 5. 在受益人之间添加空行(除了最后一个受益人)
if (i < beneficiaries.size() - 1) {
currentRow++; // 添加空行
}
}
// 确保第二持有人资料上方有一行空行
secondHolderRow = sheet.getRow(secondHolderRowNum);
if (secondHolderRow != null) {
sheet.shiftRows(secondHolderRowNum, sheet.getLastRowNum(), 1, true, false);
// 创建一个空行
Row emptyRow = sheet.createRow(secondHolderRowNum);
for (int i = 0; i < 4; i++) {
Cell cell = emptyRow.createCell(i);
cell.setCellValue("");
}
}
return excelService.saveWorkbookToTempFile(workbook);
}
/**
* 导出excel-预约信息
* @param data
* @param templateType
* @param appointmentBizId
* @return
*/
@Override
public String exportAppointment(Map<String, Object> data,
String templateType,
String appointmentBizId) {
// 获取模板信息
FileProdDto fileProdDto = iFileTemplateService.getFileProd("", templateType);
// 生成文件名
String fileName = "预约信息_" + appointmentBizId + "_" + System.currentTimeMillis() + ".xlsx";
// 导出并上传到OSS
return exportAndUploadToOss(data, fileProdDto.getFileKey(), fileName);
}
}
package com.yd.oss.service.service.impl;
import com.yd.oss.feign.annotation.ExcelCollection;
import com.yd.oss.feign.annotation.ExcelField;
import com.yd.oss.feign.annotation.ExcelSheet;
import com.yd.oss.service.service.ExcelParserService;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* Excel通用解析器
*/
@Service
public class ExcelParserServiceImpl implements ExcelParserService {
private static final Logger log = LoggerFactory.getLogger(ExcelParserServiceImpl.class);
/**
* 解析Excel文件(多Sheet页版本)
* @param file 上传的Excel文件
* @param sheetClasses 每个Sheet页对应的类类型
* @return 包含所有Sheet页解析结果的Map,key为Sheet索引
*/
@Override
public Map<Integer, Object> parseExcelWithMultipleSheets(MultipartFile file, Class<?>... sheetClasses) throws Exception {
Map<Integer, Object> resultMap = new HashMap<>();
Workbook workbook = WorkbookFactory.create(file.getInputStream());
try {
for (Class<?> clazz : sheetClasses) {
ExcelSheet sheetAnnotation = clazz.getAnnotation(ExcelSheet.class);
if (sheetAnnotation == null) {
log.warn("类 {} 缺少@ExcelSheet注解,跳过处理", clazz.getSimpleName());
continue;
}
Sheet sheet;
if (!sheetAnnotation.sheetName().isEmpty()) {
sheet = workbook.getSheet(sheetAnnotation.sheetName());
} else {
sheet = workbook.getSheetAt(sheetAnnotation.sheetIndex());
}
if (sheet == null) {
log.warn("Sheet {} 不存在",
!sheetAnnotation.sheetName().isEmpty() ?
sheetAnnotation.sheetName() : sheetAnnotation.sheetIndex());
continue;
}
Object instance = clazz.getDeclaredConstructor().newInstance();
// 解析普通字段
parseFieldsWithMergedCells(sheet, instance);
// 解析集合字段
parseCollectionFields(sheet, instance);
resultMap.put(sheetAnnotation.sheetIndex(), instance);
}
return resultMap;
} finally {
workbook.close();
}
}
/**
* 解析单个Sheet页
* @param file
* @param clazz
* @param <T>
* @return
* @throws Exception
*/
public <T> T parseExcel(MultipartFile file, Class<T> clazz) throws Exception {
Workbook workbook = WorkbookFactory.create(file.getInputStream());
try {
// 获取Sheet信息
ExcelSheet sheetAnnotation = clazz.getAnnotation(ExcelSheet.class);
Sheet sheet;
if (sheetAnnotation != null) {
if (!sheetAnnotation.sheetName().isEmpty()) {
sheet = workbook.getSheet(sheetAnnotation.sheetName());
} else {
sheet = workbook.getSheetAt(sheetAnnotation.sheetIndex());
}
} else {
sheet = workbook.getSheetAt(0); // 默认第一个Sheet
}
if (sheet == null) {
throw new RuntimeException("Sheet not found");
}
T instance = clazz.getDeclaredConstructor().newInstance();
// 解析普通字段
parseFieldsWithMergedCells(sheet, instance);
// 解析集合字段
parseCollectionFields(sheet, instance);
return instance;
} finally {
workbook.close();
}
}
/**
* 处理合并单元格的字段解析
* @param sheet
* @param instance
*/
private void parseFieldsWithMergedCells(Sheet sheet, Object instance) {
// 获取所有合并区域
List<CellRangeAddress> mergedRegions = sheet.getMergedRegions();
for (Field field : instance.getClass().getDeclaredFields()) {
ExcelField excelField = field.getAnnotation(ExcelField.class);
if (excelField != null) {
try {
int valueRow = excelField.valueRow() >= 0 ? excelField.valueRow() : excelField.titleRow();
int valueCol = excelField.valueCol() >= 0 ? excelField.valueCol() : excelField.titleCol() + 1;
Row row = sheet.getRow(valueRow);
if (row == null) continue;
// 检查是否为合并单元格
CellRangeAddress mergedRegion = findMergedRegion(mergedRegions, valueRow, valueCol);
String cellValue;
if (mergedRegion != null) {
// 合并单元格,取第一个单元格的值
Cell firstCell = sheet.getRow(mergedRegion.getFirstRow())
.getCell(mergedRegion.getFirstColumn());
cellValue = getCellValueAsString(firstCell);
} else {
// 普通单元格
Cell cell = row.getCell(valueCol);
cellValue = cell != null ? getCellValueAsString(cell) : null;
}
if (cellValue == null || cellValue.trim().isEmpty()) {
continue;
}
// 转换单元格值
Object value = convertStringValue(cellValue, field.getType(), excelField.dateFormat());
// 设置字段值
field.setAccessible(true);
field.set(instance, value);
} catch (Exception e) {
log.warn("设置字段 {} 值失败: {}", field.getName(), e.getMessage());
}
}
}
}
/**
* 查找包含指定行列的合并区域
* @param mergedRegions
* @param row
* @param column
* @return
*/
private CellRangeAddress findMergedRegion(List<CellRangeAddress> mergedRegions, int row, int column) {
for (CellRangeAddress region : mergedRegions) {
if (region.isInRange(row, column)) {
return region;
}
}
return null;
}
/**
* 将单元格值转换为字符串
* @param cell 单元格对象
* @return 单元格值的字符串表示,若单元格为null则返回null
*/
private String getCellValueAsString(Cell cell) {
if (cell == null) {
return null;
}
switch (cell.getCellType()) {
case STRING:
return cell.getStringCellValue().trim();
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
// 日期类型转换为字符串(可根据需要调整格式)
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(cell.getDateCellValue());
} else {
// 数字类型避免科学计数法,转换为字符串
return String.valueOf(cell.getNumericCellValue());
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
try {
return cell.getStringCellValue().trim();
} catch (Exception e) {
// 公式计算结果不是字符串时,尝试获取数值
return String.valueOf(cell.getNumericCellValue());
}
default:
return "";
}
}
/**
* 解析普通字段
* @param sheet
* @param instance
*/
private void parseFields(Sheet sheet, Object instance) {
for (Field field : instance.getClass().getDeclaredFields()) {
ExcelField excelField = field.getAnnotation(ExcelField.class);
if (excelField != null) {
try {
// 获取值单元格
int valueRow = excelField.valueRow() >= 0 ? excelField.valueRow() : excelField.titleRow();
int valueCol = excelField.valueCol() >= 0 ? excelField.valueCol() : excelField.titleCol() + 1;
Row row = sheet.getRow(valueRow);
if (row == null) continue;
Cell cell = row.getCell(valueCol);
if (cell == null) continue;
// 转换单元格值
Object value = convertCellValue(cell, field.getType(), excelField.dateFormat());
// 设置字段值
field.setAccessible(true);
field.set(instance, value);
} catch (Exception e) {
log.warn("设置字段 {} 值失败: {}", field.getName(), e.getMessage());
}
}
}
}
/**
* 解析集合字段(增强版,支持动态结束行检测)
* @param sheet
* @param instance
* @throws Exception
*/
private void parseCollectionFields(Sheet sheet, Object instance) throws Exception {
for (Field field : instance.getClass().getDeclaredFields()) {
ExcelCollection collectionAnnotation = field.getAnnotation(ExcelCollection.class);
if (collectionAnnotation != null) {
List<Object> collectionData = new ArrayList<>();
// 确定结束行(支持自动检测)
int endRow = collectionAnnotation.endRow() >= 0 ?
collectionAnnotation.endRow() : findCollectionEndRow(sheet, collectionAnnotation);
// 遍历行,解析集合元素
for (int rowNum = collectionAnnotation.startRow(); rowNum <= endRow; rowNum++) {
Row row = sheet.getRow(rowNum);
if (row == null || isRowEmpty(row)) {
continue; // 跳过空行
}
try {
Object element = collectionAnnotation.type().getDeclaredConstructor().newInstance();
boolean hasData = false;
// 解析元素字段
for (Field elementField : collectionAnnotation.type().getDeclaredFields()) {
ExcelField excelField = elementField.getAnnotation(ExcelField.class);
if (excelField != null) {
// 计算值所在列
int valueCol = excelField.valueCol() >= 0 ?
excelField.valueCol() : excelField.titleCol();
Cell cell = row.getCell(valueCol);
if (cell != null) {
Object value = convertCellValue(cell, elementField.getType(), excelField.dateFormat());
elementField.setAccessible(true);
elementField.set(element, value);
hasData = true;
}
}
}
if (hasData) {
collectionData.add(element);
}
} catch (Exception e) {
log.warn("解析集合元素失败: {}", e.getMessage());
}
}
// 设置集合字段值
field.setAccessible(true);
field.set(instance, collectionData);
}
}
}
/**
* 自动检测集合的结束行
* @param sheet
* @param collectionAnnotation
* @return
*/
private int findCollectionEndRow(Sheet sheet, ExcelCollection collectionAnnotation) {
int endRow = collectionAnnotation.startRow();
int maxEmptyRows = 5; // 连续空行的最大数量
// 获取所有需要检查的列
Set<Integer> columnsToCheck = new HashSet<>();
for (Field field : collectionAnnotation.type().getDeclaredFields()) {
ExcelField excelField = field.getAnnotation(ExcelField.class);
if (excelField != null) {
int col = excelField.valueCol() >= 0 ? excelField.valueCol() : excelField.titleCol();
columnsToCheck.add(col);
}
}
if (columnsToCheck.isEmpty()) {
return sheet.getLastRowNum(); // 如果没有字段注解,返回最后一行
}
int emptyRowCount = 0;
for (int rowNum = collectionAnnotation.startRow(); rowNum <= sheet.getLastRowNum(); rowNum++) {
Row row = sheet.getRow(rowNum);
if (row == null) {
emptyRowCount++;
if (emptyRowCount >= maxEmptyRows) {
return rowNum - maxEmptyRows; // 返回连续空行前的最后一行
}
continue;
}
boolean hasData = false;
for (int col : columnsToCheck) {
Cell cell = row.getCell(col);
if (cell != null && cell.getCellType() != CellType.BLANK) {
hasData = true;
break;
}
}
if (hasData) {
emptyRowCount = 0;
endRow = rowNum;
} else {
emptyRowCount++;
if (emptyRowCount >= maxEmptyRows) {
return endRow; // 返回最后有数据的行
}
}
}
return sheet.getLastRowNum();
}
/**
* 单元格值转换
* @param cell
* @param targetType
* @param dateFormat
* @return
*/
private Object convertCellValue(Cell cell, Class<?> targetType, String dateFormat) {
switch (cell.getCellType()) {
case STRING:
return convertStringValue(cell.getStringCellValue().trim(), targetType, dateFormat);
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue();
} else {
return convertNumericValue(cell.getNumericCellValue(), targetType);
}
case BOOLEAN:
return cell.getBooleanCellValue();
case FORMULA:
try {
return convertStringValue(cell.getStringCellValue().trim(), targetType, dateFormat);
} catch (Exception e) {
return cell.getCellFormula();
}
default:
return null;
}
}
private Object convertStringValue(String value, Class<?> targetType, String dateFormat) {
if (value == null || value.isEmpty()) return null;
if (targetType == String.class) {
return value;
} else if (targetType == Integer.class || targetType == int.class) {
return Integer.parseInt(value);
} else if (targetType == Long.class || targetType == long.class) {
return Long.parseLong(value);
} else if (targetType == Double.class || targetType == double.class) {
return Double.parseDouble(value);
} else if (targetType == BigDecimal.class) {
return new BigDecimal(value);
} else if (targetType == Date.class) {
try {
SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
return sdf.parse(value);
} catch (ParseException e) {
log.warn("日期格式转换失败: {}", value);
return null;
}
} else if (targetType == Boolean.class || targetType == boolean.class) {
return "是".equals(value) || "YES".equalsIgnoreCase(value) || "TRUE".equalsIgnoreCase(value);
}
return value;
}
private Object convertNumericValue(double value, Class<?> targetType) {
if (targetType == Integer.class || targetType == int.class) {
return (int) value;
} else if (targetType == Long.class || targetType == long.class) {
return (long) value;
} else if (targetType == Double.class || targetType == double.class) {
return value;
} else if (targetType == BigDecimal.class) {
return BigDecimal.valueOf(value);
} else if (targetType == Date.class) {
return DateUtil.getJavaDate(value);
}
return value;
}
private boolean isRowEmpty(Row row) {
for (Cell cell : row) {
if (cell != null && cell.getCellType() != CellType.BLANK) {
return false;
}
}
return true;
}
}
package com.yd.oss.service.service.impl;
import com.aliyun.oss.OSS;
import com.aliyun.oss.model.OSSObject;
import com.yd.oss.service.service.ExcelService;
import com.yd.oss.service.service.OssService;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Excel服务实现类
*/
@Slf4j
@Service
public class ExcelServiceImpl implements ExcelService {
@Resource
private OssService ossService;
@Autowired
private OSS ossClient; // 注入OSS客户端
@Autowired
private String defaultBucket; // 注入默认存储桶
/**
* 从OSS下载模板到临时文件
* @param ossObjectKey
* @return
* @throws IOException
*/
@Override
public File downloadTemplateToTempFile(String ossObjectKey) throws IOException {
// 创建临时文件
Path tempPath = Files.createTempFile("template", ".xlsx");
File tempFile = tempPath.toFile();
tempFile.deleteOnExit(); // JVM退出时删除
try (OSSObject ossObject = ossClient.getObject(defaultBucket, ossObjectKey);
InputStream inputStream = ossObject.getObjectContent();
FileOutputStream outputStream = new FileOutputStream(tempFile)) {
// 复制流到文件
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
}
return tempFile;
}
/**
* 保存工作簿到临时文件
* @param workbook
* @return
* @throws IOException
*/
@Override
public File saveWorkbookToTempFile(Workbook workbook) throws IOException {
Path tempPath = Files.createTempFile("processed", ".xlsx");
File tempFile = tempPath.toFile();
tempFile.deleteOnExit();
try (FileOutputStream fos = new FileOutputStream(tempFile)) {
workbook.write(fos);
}
return tempFile;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="ExternalSystem" externalSystem="Maven" />
<component name="FacetManager">
<facet type="Spring" name="Spring">
<configuration />
</facet>
<facet type="web" name="Web">
<configuration>
<webroots />
<sourceRoots>
<root url="file://$MODULE_DIR$/src/main/java" />
<root url="file://$MODULE_DIR$/src/main/resources" />
</sourceRoots>
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
<output url="file://$MODULE_DIR$/../target/classes" />
<output-test url="file://$MODULE_DIR$/../target/test-classes" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="jdk" jdkName="1.8" jdkType="JavaSDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4" />
\ No newline at end of file
<module type="JAVA_MODULE" version="4">
<component name="ExternalSystem" externalSystem="Maven" />
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
<output url="file://$MODULE_DIR$/target/classes" />
<output-test url="file://$MODULE_DIR$/target/test-classes" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment