You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

960 lines
44 KiB

2 months ago
2 months ago
2 months ago
2 months ago
3 weeks ago
2 months ago
3 weeks ago
2 months ago
2 months ago
2 months ago
3 weeks ago
2 months ago
3 weeks ago
2 months ago
3 weeks ago
2 months ago
3 weeks ago
2 months ago
3 weeks ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
3 weeks ago
2 months ago
  1. package com.xujie.sys.common.utils;
  2. import lombok.Setter;
  3. import org.apache.poi.ooxml.POIXMLDocumentPart;
  4. import org.apache.poi.ss.usermodel.*;
  5. import org.apache.poi.ss.util.CellRangeAddress;
  6. import org.apache.poi.xssf.usermodel.*;
  7. import java.io.IOException;
  8. import java.io.InputStream;
  9. import java.math.BigDecimal;
  10. import java.math.RoundingMode;
  11. import java.util.*;
  12. import java.util.regex.Matcher;
  13. import java.util.regex.Pattern;
  14. public class ExcelTemplateALPHA {
  15. final private static Pattern PATTERN_HDR_FIELD = Pattern.compile("\\$\\{(.*?)}", Pattern.MULTILINE);
  16. final private static Pattern PATTERN_DTL_FIELD = Pattern.compile("#\\{(.*?)}", Pattern.MULTILINE);
  17. private XSSFWorkbook workbook;
  18. private Map<String, Object> variables = new HashMap<>();
  19. private List<Map<String, Object>> listVariables = new ArrayList<>();
  20. // 存储需要合并的单元格区域信息:[列表起始索引, 列表结束索引, 列索引]
  21. private List<int[]> mergeRegions = new ArrayList<>();
  22. // 存储需要设置行高的行信息:[起始行索引, 结束行索引, 行高]
  23. private List<int[]> rowHeights = new ArrayList<>();
  24. // 是否下移形状格式
  25. @Setter
  26. private boolean moveShape = false;
  27. // 是否下移印章(印章是图片)
  28. @Setter
  29. private boolean moveSeal = false;
  30. // 是否设置单元格样式
  31. @Setter
  32. private boolean cellStyle = false;
  33. // 是否设置单元格样式 针对RFID不需要序号定制
  34. @Setter
  35. private boolean cellStyle2 = false;
  36. // 是否设置合并单元格样式
  37. @Setter
  38. private boolean rangeStyle = false;
  39. // 价格靠右 发票
  40. @Setter
  41. private boolean priceRight = false;
  42. // 价格靠右 发票
  43. @Setter
  44. private boolean invoiceLie = false;
  45. // 发票价格列是否使用固定小数位(单价5位、总价2位)
  46. @Setter
  47. private boolean fixedInvoicePriceScale = false;
  48. // 报关单价格列是否固定两位小数(净重/单价/总价)
  49. @Setter
  50. private boolean fixedDeclarationPriceScale = false;
  51. // 数字靠右 箱单
  52. @Setter
  53. private boolean intRight = false;
  54. // 报关单
  55. @Setter
  56. private boolean delRight = false;
  57. // 箱单
  58. @Setter
  59. private boolean boxFlag = false;
  60. private ExcelTemplateALPHA(){}
  61. public static ExcelTemplateALPHA load(InputStream template) throws IOException {
  62. ExcelTemplateALPHA excelTemplate = new ExcelTemplateALPHA();
  63. excelTemplate.workbook = new XSSFWorkbook(template);
  64. return excelTemplate;
  65. }
  66. public void addVar(String key, Object value){
  67. variables.put(key, value);
  68. }
  69. public void addVarAll(Map values){
  70. variables.putAll(values);
  71. }
  72. public void addListVar(Map<String, Object> row){
  73. listVariables.add(row);
  74. }
  75. public void addListVarAll(Collection rows){
  76. listVariables.addAll(rows);
  77. }
  78. /**
  79. * 添加需要合并的单元格区域基于列表索引
  80. * @param startListIndex 列表起始索引相对于listVariables
  81. * @param endListIndex 列表结束索引相对于listVariables
  82. * @param colIndex 列索引
  83. */
  84. public void addMergeRegion(int startListIndex, int endListIndex, int colIndex) {
  85. if (startListIndex <= endListIndex) {
  86. mergeRegions.add(new int[]{startListIndex, endListIndex, colIndex, colIndex, 0}); // 格式:[startRow, endRow, startCol, endCol, alignType]
  87. }
  88. }
  89. /**
  90. * 添加合并区域并指定对齐方式
  91. * @param startListIndex 起始行索引
  92. * @param endListIndex 结束行索引
  93. * @param colIndex 列索引
  94. * @param alignType 对齐方式0=右对齐1=左对齐2=居中
  95. */
  96. public void addMergeRegion(int startListIndex, int endListIndex, int colIndex, int alignType) {
  97. if (startListIndex <= endListIndex) {
  98. mergeRegions.add(new int[]{startListIndex, endListIndex, colIndex, colIndex, alignType});
  99. }
  100. }
  101. /**
  102. * 添加跨列的合并区域支持横向和纵向同时合并
  103. * @param startListIndex 起始行索引
  104. * @param endListIndex 结束行索引
  105. * @param startColIndex 起始列索引
  106. * @param endColIndex 结束列索引
  107. * @param alignType 对齐方式0=右对齐1=左对齐2=居中
  108. */
  109. public void addMergeRegion(int startListIndex, int endListIndex, int startColIndex, int endColIndex, int alignType) {
  110. if (startListIndex <= endListIndex && startColIndex <= endColIndex) {
  111. mergeRegions.add(new int[]{startListIndex, endListIndex, startColIndex, endColIndex, alignType});
  112. }
  113. }
  114. /**
  115. * 设置行高
  116. * @param startListIndex 起始行索引相对于listVariables
  117. * @param endListIndex 结束行索引相对于listVariables
  118. * @param height 行高单位
  119. */
  120. public void setRowHeight(int startListIndex, int endListIndex, int height) {
  121. if (startListIndex <= endListIndex) {
  122. rowHeights.add(new int[]{startListIndex, endListIndex, height});
  123. }
  124. }
  125. public void clearAll(){
  126. variables.clear();
  127. listVariables.clear();
  128. mergeRegions.clear();
  129. rowHeights.clear();
  130. moveShape = false;
  131. moveSeal = false;
  132. cellStyle = false;
  133. cellStyle2 = false;
  134. rangeStyle = false;
  135. priceRight = false;
  136. fixedInvoicePriceScale = false;
  137. fixedDeclarationPriceScale = false;
  138. intRight = false;
  139. delRight = false;
  140. }
  141. private void applyMultilineStyleIfNeeded(XSSFCell cell, String content) {
  142. if (cell == null || content == null || !content.contains("\n")) {
  143. return;
  144. }
  145. XSSFCellStyle baseStyle = cell.getCellStyle();
  146. XSSFCellStyle multilineStyle = workbook.createCellStyle();
  147. if (baseStyle != null) {
  148. multilineStyle.cloneStyleFrom(baseStyle);
  149. }
  150. multilineStyle.setWrapText(true);
  151. multilineStyle.setVerticalAlignment(VerticalAlignment.TOP);
  152. cell.setCellStyle(multilineStyle);
  153. XSSFRow row = cell.getRow();
  154. if (row == null) {
  155. return;
  156. }
  157. int lineCount = content.split("\n", -1).length;
  158. if (lineCount <= 1) {
  159. return;
  160. }
  161. float defaultRowHeight = row.getSheet().getDefaultRowHeightInPoints();
  162. float currentRowHeight = row.getHeightInPoints();
  163. float targetHeight = Math.max(currentRowHeight, defaultRowHeight * lineCount);
  164. if (targetHeight > currentRowHeight) {
  165. row.setHeightInPoints(targetHeight);
  166. }
  167. }
  168. private boolean findAndRemoveMergedRegion(XSSFSheet sheet, int rowIndex) {
  169. for (int mi = 0; mi < sheet.getMergedRegions().size(); mi++) {
  170. if (sheet.getMergedRegions().get(mi).getFirstRow() == rowIndex) {
  171. sheet.removeMergedRegion(mi);
  172. return true;
  173. }
  174. }
  175. return false;
  176. }
  177. public XSSFWorkbook render(int index) throws IOException {
  178. XSSFSheet sheet = workbook.getSheetAt(index);
  179. CellCopyPolicy copyPolicy = new CellCopyPolicy();
  180. int dtlRowIndex = -1;
  181. // find detail rows
  182. for (int i = 0; i < sheet.getLastRowNum(); i++) {
  183. XSSFRow row = sheet.getRow(i);
  184. if (row == null) {
  185. continue;
  186. }
  187. for (int j = 0; j < row.getLastCellNum(); j++) {
  188. XSSFCell c = sheet.getRow(i).getCell(j);
  189. if (c == null) {
  190. continue;
  191. }
  192. String cellValue = c.toString();
  193. Matcher matcherDtl = PATTERN_DTL_FIELD.matcher(cellValue);
  194. if (matcherDtl.find()) {
  195. dtlRowIndex = i;
  196. break;
  197. }
  198. }
  199. if (dtlRowIndex >= 0) {
  200. break;
  201. }
  202. }
  203. if (dtlRowIndex < sheet.getLastRowNum() && listVariables.size() > 1) {
  204. //循环列表以下区域整体下移
  205. int _rows = sheet.getLastRowNum();
  206. for(int i = _rows; i > dtlRowIndex; i--){
  207. sheet.copyRows(i, i, i + listVariables.size() -1 , copyPolicy);
  208. }
  209. for(int i = 0; i < listVariables.size() - 1; i++) {
  210. int destRowIndex = dtlRowIndex + 1 + i;
  211. sheet.createRow(destRowIndex); //clear destination row
  212. while(findAndRemoveMergedRegion(sheet, destRowIndex)){
  213. continue;
  214. }
  215. sheet.copyRows(dtlRowIndex, dtlRowIndex, destRowIndex, copyPolicy);
  216. }
  217. }
  218. // 遍历所有形状 将列表下方的形状坐标也下移
  219. if (moveShape) {
  220. int listStartRow = 24; // Excel 第25行(0-based)
  221. int moveOffset = listVariables.size() - 1;
  222. for (POIXMLDocumentPart part : sheet.getRelations()) {
  223. if (!(part instanceof XSSFDrawing)) {
  224. continue;
  225. }
  226. XSSFDrawing drawing = (XSSFDrawing) part;
  227. for (XSSFShape shape : drawing.getShapes()) {
  228. if (!(shape instanceof XSSFSimpleShape)) {
  229. continue;
  230. }
  231. XSSFSimpleShape simpleShape = (XSSFSimpleShape) shape;
  232. ClientAnchor anchor = (ClientAnchor) simpleShape.getAnchor();
  233. if (anchor != null && anchor.getRow1() >= listStartRow) {
  234. anchor.setRow1(anchor.getRow1() + moveOffset);
  235. if (anchor instanceof XSSFClientAnchor) {
  236. XSSFClientAnchor xAnchor = (XSSFClientAnchor) anchor;
  237. try {
  238. xAnchor.setRow2(xAnchor.getRow2() + moveOffset);
  239. } catch (Exception ignore) {
  240. // row2 不存在,忽略
  241. }
  242. }
  243. }
  244. }
  245. }
  246. }
  247. // 遍历所有图片 将列表下方的形状坐标也下移
  248. if (moveSeal) {
  249. List<POIXMLDocumentPart> relations = sheet.getRelations();
  250. for (POIXMLDocumentPart part : relations) {
  251. if (part instanceof XSSFDrawing) {
  252. XSSFDrawing drawing = (XSSFDrawing) part;
  253. List<XSSFShape> shapes = drawing.getShapes();
  254. for (XSSFShape shape : shapes) {
  255. // 只处理图片类型的形状
  256. if (shape instanceof XSSFPicture) {
  257. XSSFPicture picture = (XSSFPicture) shape;
  258. // 调整行坐标实现下移
  259. ClientAnchor anchor = (ClientAnchor) picture.getAnchor();
  260. if (anchor.getRow1() > 10) { // 只下移位于10行之后的图片
  261. anchor.setRow1(anchor.getRow1() + listVariables.size() - 1);
  262. anchor.setRow2(anchor.getRow2() + listVariables.size() - 1);
  263. }
  264. }
  265. }
  266. }
  267. }
  268. }
  269. Set<Integer> dtlRows = new LinkedHashSet<>();
  270. List<Integer> boxRows = new ArrayList<>();
  271. Set<Integer> poNoCols = new HashSet<>();
  272. Set<Integer> invoiceUnitPriceCols = new HashSet<>();
  273. Set<Integer> invoiceTotalPriceCols = new HashSet<>();
  274. Set<Integer> declarationFixedPriceCols = new HashSet<>();
  275. Set<Integer> declarationQtyCols = new HashSet<>();
  276. //整体填值
  277. for (int i = 0; i <= sheet.getLastRowNum(); i++) {
  278. XSSFRow row = sheet.getRow(i);
  279. if (row == null) {
  280. continue;
  281. }
  282. for (int j = 0; j < row.getLastCellNum(); j++) {
  283. XSSFCell c = sheet.getRow(i).getCell(j);
  284. if (c == null) {
  285. continue;
  286. }
  287. String cellValue = c.toString();
  288. if ("#{pn}".equals(cellValue) || "#{levy}".equals(cellValue) || "#{artNo}".equals(cellValue)) {
  289. dtlRows.add(i);
  290. }
  291. if ("#{cmcInvoice}".equals(cellValue)) {
  292. boxRows.add(i);
  293. }
  294. // 仅记录明细模板行上的po_no列,避免同名占位符误伤其它数字列
  295. if (i == dtlRowIndex && "#{po_no}".equalsIgnoreCase(cellValue)) {
  296. poNoCols.add(j);
  297. }
  298. if (i == dtlRowIndex) {
  299. Matcher matcherDtlField = PATTERN_DTL_FIELD.matcher(cellValue);
  300. while (matcherDtlField.find()) {
  301. String dtlField = matcherDtlField.group(1);
  302. if (isInvoiceUnitPriceField(dtlField)) {
  303. invoiceUnitPriceCols.add(j);
  304. } else if (isInvoiceTotalPriceField(dtlField)) {
  305. invoiceTotalPriceCols.add(j);
  306. } else if (isDeclarationFixedPriceField(dtlField)) {
  307. declarationFixedPriceCols.add(j);
  308. } else if (isDeclarationQtyField(dtlField)) {
  309. declarationQtyCols.add(j);
  310. }
  311. }
  312. }
  313. Matcher matcherHdr = PATTERN_HDR_FIELD.matcher(cellValue);
  314. String result = c.getStringCellValue();
  315. boolean hasHeaderPlaceholder = false;
  316. while (matcherHdr.find()) {
  317. hasHeaderPlaceholder = true;
  318. for (int ri = 1; ri <= matcherHdr.groupCount(); ri++) {
  319. String field = matcherHdr.group(ri);
  320. Object value = variables.getOrDefault(field, "");
  321. result = result.replace(matcherHdr.group(0), String.valueOf(value));
  322. }
  323. boolean isVoyageCell = "${voyage}".equals(cellValue);
  324. boolean isHsCodeDescCell = cellValue.contains("${hs_code_desc}");
  325. if ("${phone1}".equals(cellValue) || "${phone2}".equals(cellValue) || "${hs_code}".equals(cellValue)
  326. || "${pickupAddress}".equals(cellValue) || isVoyageCell || isHsCodeDescCell) {
  327. c.setCellValue(result); // 字符串
  328. if ("${pickupAddress}".equals(cellValue) || isVoyageCell) {
  329. applyMultilineCellStyle(c, result, isVoyageCell);
  330. }
  331. if (isHsCodeDescCell) {
  332. applyHsCodeDescRichText(c, result);
  333. }
  334. } else {
  335. try {
  336. // 更严格的数字检查
  337. String cleanResult = result.replace(",", "").trim();
  338. if (!cleanResult.isEmpty() && !cleanResult.equals("-") && !cleanResult.equals(".")) {
  339. double num = Double.parseDouble(cleanResult);
  340. if (Double.isFinite(num)) {
  341. c.setCellValue(num); // 数值
  342. // goods_total_qty 根据实际小数位动态设置格式
  343. if ("${goods_total_qty}".equals(cellValue)) {
  344. XSSFCellStyle fmtStyle = workbook.createCellStyle();
  345. fmtStyle.cloneStyleFrom(c.getCellStyle());
  346. DataFormat dataFormat = workbook.createDataFormat();
  347. fmtStyle.setDataFormat(dataFormat.getFormat(buildDecimalFormat(num)));
  348. c.setCellStyle(fmtStyle);
  349. }
  350. } else {
  351. c.setCellValue(result); // 字符串
  352. }
  353. } else {
  354. c.setCellValue(result); // 字符串
  355. }
  356. } catch (NumberFormatException e) {
  357. c.setCellValue(result); // 字符串
  358. }
  359. }
  360. }
  361. if (hasHeaderPlaceholder) {
  362. applyMultilineStyleIfNeeded(c, result);
  363. }
  364. Matcher matcherDtl = PATTERN_DTL_FIELD.matcher(cellValue);
  365. while (matcherDtl.find()) {
  366. for (int ri = 1; ri <= matcherDtl.groupCount(); ri++) {
  367. String field = matcherDtl.group(ri);
  368. // 检查数组边界,避免越界异常
  369. int listIndex = i - dtlRowIndex;
  370. if (listIndex >= 0 && listIndex < listVariables.size()) {
  371. c.setCellValue(cellValue.replace(matcherDtl.group(0), String.valueOf(listVariables.get(listIndex).getOrDefault(field, ""))));
  372. }
  373. }
  374. }
  375. // 设置样式
  376. if (cellStyle && dtlRowIndex >= 0 && i >= dtlRowIndex && i < dtlRowIndex + listVariables.size()) {
  377. XSSFCellStyle style = c.getCellStyle();
  378. style.setBorderBottom(BorderStyle.NONE);
  379. style.setBorderTop(BorderStyle.NONE);
  380. style.setWrapText(true);
  381. style.setAlignment(HorizontalAlignment.LEFT);
  382. c.setCellStyle(style);
  383. if (rangeStyle && j < 4) {
  384. for (int mi = 0; mi < 3; mi++) {
  385. XSSFCell nextc = sheet.getRow(i).getCell(c.getColumnIndex()+mi+1);
  386. if (nextc == null) {
  387. continue;
  388. }
  389. nextc.setCellStyle(style);
  390. }
  391. }
  392. }
  393. }
  394. }
  395. // 设置明细样式,因为明细是动态添加的,会有样式和模版偏差,此处主要是第2、3列的样式需要设置
  396. if (cellStyle) {
  397. for (Integer dtlRow : dtlRows) {
  398. XSSFCell c1 = sheet.getRow(dtlRow).getCell(cellStyle2?0:1);
  399. if (c1 == null) {
  400. continue;
  401. }
  402. XSSFCellStyle style = workbook.createCellStyle();
  403. style.setBorderRight(BorderStyle.NONE);
  404. style.setBorderLeft(BorderStyle.MEDIUM); // 实线
  405. style.setBorderBottom(BorderStyle.NONE);
  406. style.setBorderTop(BorderStyle.NONE);
  407. Font font = workbook.createFont();
  408. font.setFontName("Arial"); // 设置字体
  409. font.setFontHeightInPoints((short) 10); // 设置字号
  410. style.setFont(font);
  411. style.setVerticalAlignment(VerticalAlignment.TOP);
  412. style.setWrapText(true);
  413. c1.setCellStyle(style);
  414. XSSFCell c2 = sheet.getRow(dtlRow).getCell(cellStyle2?1:2);
  415. if (c2 == null) {
  416. continue;
  417. }
  418. XSSFCellStyle style2 = workbook.createCellStyle();
  419. style2.setBorderRight(BorderStyle.MEDIUM);
  420. style2.setBorderLeft(BorderStyle.NONE);
  421. style2.setBorderBottom(BorderStyle.NONE);
  422. style2.setBorderTop(BorderStyle.NONE);
  423. Font font2 = workbook.createFont();
  424. font2.setFontName("Arial"); // 设置字体
  425. font2.setFontHeightInPoints((short) 10); // 设置字号
  426. style2.setFont(font2);
  427. style2.setVerticalAlignment(VerticalAlignment.TOP);
  428. style2.setAlignment(HorizontalAlignment.LEFT);
  429. style2.setWrapText(true);
  430. c2.setCellStyle(style2);
  431. if (priceRight) { //仅供发票excel使用,第6、7、8是价格列居右
  432. for (int i = 6; i < (invoiceLie?11:9); i++) {
  433. XSSFRow row = sheet.getRow(dtlRow);
  434. if (row == null) {
  435. continue;
  436. }
  437. XSSFCell c7 = row.getCell(i);
  438. if (c7 == null) {
  439. continue;
  440. }
  441. // 尝试把字符串转成数值
  442. if (c7.getCellType() == CellType.STRING) {
  443. String strVal = c7.getStringCellValue();
  444. if (strVal != null && !strVal.trim().isEmpty()) {
  445. try {
  446. double num = Double.parseDouble(strVal.replace(",", ""));
  447. c7.setCellValue(num); // 转换为数值写回
  448. } catch (NumberFormatException e) {
  449. // 如果不是数字就保留原字符串
  450. System.out.println("非数字,保持原值: " + strVal);
  451. }
  452. }
  453. }
  454. // 创建样式
  455. XSSFCellStyle style7 = workbook.createCellStyle();
  456. style7.setBorderRight(BorderStyle.MEDIUM);
  457. style7.setBorderLeft(BorderStyle.NONE);
  458. style7.setBorderBottom(BorderStyle.NONE);
  459. style7.setBorderTop(BorderStyle.NONE);
  460. Font font7 = workbook.createFont();
  461. font7.setFontName("Arial");
  462. font7.setFontHeightInPoints((short) 10);
  463. style7.setFont(font7);
  464. style7.setVerticalAlignment(VerticalAlignment.TOP);
  465. style7.setAlignment(HorizontalAlignment.RIGHT);
  466. // 设置千分位格式(根据单元格实际小数位动态决定格式)
  467. DataFormat dataFormat = workbook.createDataFormat();
  468. String numFmt;
  469. if (fixedInvoicePriceScale && invoiceUnitPriceCols.contains(i)) {
  470. numFmt = "#,##0.00000";
  471. } else if (fixedInvoicePriceScale && invoiceTotalPriceCols.contains(i)) {
  472. numFmt = "#,##0.00";
  473. } else if (c7.getCellType() == CellType.NUMERIC) {
  474. numFmt = buildDecimalFormat(c7.getNumericCellValue());
  475. } else {
  476. numFmt = "#,##0.00";
  477. }
  478. style7.setDataFormat(dataFormat.getFormat(numFmt));
  479. c7.setCellStyle(style7);
  480. }
  481. }
  482. if (intRight) { //仅供箱单excel使用
  483. // 覆盖到第10列(索引9),保证9、10列按数字处理
  484. for (int i = 4; i < 11; i++) {
  485. XSSFRow row = sheet.getRow(dtlRow);
  486. if (row == null) {
  487. continue;
  488. }
  489. XSSFCell c7 = row.getCell(i);
  490. if (c7 == null) {
  491. continue;
  492. }
  493. boolean isPoNoColumn = poNoCols.contains(i);
  494. BigDecimal numericValue = null;
  495. // 尝试把字符串转成数值
  496. if (!isPoNoColumn && c7.getCellType() == CellType.STRING) {
  497. String strVal = c7.getStringCellValue();
  498. BigDecimal parsed = parseNumericString(strVal);
  499. if (parsed != null) {
  500. numericValue = parsed;
  501. c7.setCellValue(parsed.doubleValue()); // 转换为数值写回
  502. }
  503. } else if (!isPoNoColumn && c7.getCellType() == CellType.NUMERIC) {
  504. numericValue = BigDecimal.valueOf(c7.getNumericCellValue());
  505. }
  506. // 创建样式
  507. XSSFCellStyle style7 = workbook.createCellStyle();
  508. style7.setBorderRight(BorderStyle.MEDIUM);
  509. style7.setBorderLeft(i==4?BorderStyle.MEDIUM:BorderStyle.NONE);
  510. style7.setBorderBottom(BorderStyle.NONE);
  511. style7.setBorderTop(BorderStyle.NONE);
  512. // 判断单元格的值是否小于等于0,如果小于等于0则加粗
  513. boolean isBold = false;
  514. if (numericValue != null && numericValue.compareTo(BigDecimal.ZERO) <= 0) {
  515. isBold = true;
  516. }
  517. Font font7 = workbook.createFont();
  518. font7.setFontName("Arial");
  519. font7.setFontHeightInPoints((short) 10);
  520. if (isBold) {
  521. font7.setBold(true);
  522. }
  523. style7.setFont(font7);
  524. style7.setVerticalAlignment(VerticalAlignment.TOP);
  525. style7.setAlignment(HorizontalAlignment.CENTER);
  526. DataFormat dataFormat = workbook.createDataFormat();
  527. if (isPoNoColumn) {
  528. // po_no 是编号列,必须按文本处理,不能套用千分位
  529. style7.setDataFormat(dataFormat.getFormat("@"));
  530. } else {
  531. // 设置千分位格式(根据单元格实际小数位动态决定格式)
  532. String numFmt2;
  533. if (numericValue != null) {
  534. // 去掉末尾0后按实际小数位显示:20.000000 -> 20,0.150000 -> 0.15
  535. numFmt2 = buildDecimalFormatByDecimal(numericValue);
  536. } else if (c7.getCellType() == CellType.NUMERIC) {
  537. numFmt2 = buildDecimalFormat(c7.getNumericCellValue());
  538. } else {
  539. numFmt2 = "#,##0";
  540. }
  541. style7.setDataFormat(dataFormat.getFormat(numFmt2));
  542. }
  543. c7.setCellStyle(style7);
  544. }
  545. // net_weight列(J列,索引9)保持顶部右对齐
  546. /*XSSFRow row = sheet.getRow(dtlRow);
  547. if (row != null) {
  548. XSSFCell netWeightCell = row.getCell(9);
  549. if (netWeightCell != null) {
  550. XSSFCellStyle netWeightStyle = workbook.createCellStyle();
  551. netWeightStyle.cloneStyleFrom(netWeightCell.getCellStyle());
  552. netWeightStyle.setVerticalAlignment(VerticalAlignment.TOP);
  553. netWeightStyle.setAlignment(HorizontalAlignment.RIGHT);
  554. netWeightCell.setCellStyle(netWeightStyle);
  555. }
  556. }*/
  557. }
  558. }
  559. }
  560. if (delRight) {
  561. for (Integer dtlRow : dtlRows) {
  562. for (int i = 0; i < 17; i++) {
  563. XSSFRow row = sheet.getRow(dtlRow);
  564. if (row == null) {
  565. continue;
  566. }
  567. XSSFCell c7 = row.getCell(i);
  568. if (c7 == null) {
  569. continue;
  570. }
  571. // 创建样式
  572. XSSFCellStyle style = workbook.createCellStyle();
  573. style.setBorderRight(i==16?BorderStyle.THIN:BorderStyle.NONE);
  574. style.setBorderLeft(i==0?BorderStyle.THIN:BorderStyle.NONE);
  575. style.setBorderBottom(BorderStyle.THIN);
  576. style.setBorderTop(BorderStyle.NONE);
  577. Font font = workbook.createFont();
  578. font.setFontName("Arial");
  579. font.setFontHeightInPoints((short) 10);
  580. style.setFont(font);
  581. style.setVerticalAlignment(VerticalAlignment.CENTER);
  582. style.setAlignment(HorizontalAlignment.LEFT);
  583. c7.setCellStyle(style);
  584. boolean declarationFixedTwoScaleColumn = fixedDeclarationPriceScale && declarationFixedPriceCols.contains(i);
  585. boolean declarationQtyColumn = fixedDeclarationPriceScale && declarationQtyCols.contains(i);
  586. boolean declarationNumberColumn = fixedDeclarationPriceScale
  587. ? (declarationFixedTwoScaleColumn || declarationQtyColumn)
  588. : (i == 6 || i == 11);
  589. if (declarationNumberColumn) {
  590. BigDecimal numericValue = null;
  591. if (c7.getCellType() == CellType.STRING) {
  592. numericValue = parseNumericString(c7.getStringCellValue());
  593. } else if (c7.getCellType() == CellType.NUMERIC) {
  594. numericValue = BigDecimal.valueOf(c7.getNumericCellValue());
  595. }
  596. if (numericValue != null) {
  597. if (declarationFixedTwoScaleColumn) {
  598. numericValue = numericValue.setScale(2, RoundingMode.HALF_UP);
  599. }
  600. c7.setCellValue(numericValue.doubleValue());
  601. }
  602. // 创建样式
  603. XSSFCellStyle style7 = workbook.createCellStyle();
  604. style7.setBorderRight(BorderStyle.NONE);
  605. style7.setBorderLeft(BorderStyle.NONE);
  606. style7.setBorderBottom(BorderStyle.THIN);
  607. style7.setBorderTop(BorderStyle.NONE);
  608. Font font7 = workbook.createFont();
  609. font7.setFontName("Arial");
  610. font7.setFontHeightInPoints((short) 10);
  611. style7.setFont(font7);
  612. style7.setVerticalAlignment(VerticalAlignment.CENTER);
  613. style7.setAlignment(HorizontalAlignment.RIGHT);
  614. // 设置千分位格式(根据单元格实际小数位动态决定格式)
  615. DataFormat dataFormat = workbook.createDataFormat();
  616. String numFmt3;
  617. if (declarationFixedTwoScaleColumn) {
  618. numFmt3 = "#,##0.00";
  619. } else if (declarationQtyColumn) {
  620. numFmt3 = "#,##0";
  621. } else if (c7.getCellType() == CellType.NUMERIC) {
  622. numFmt3 = buildDecimalFormat(c7.getNumericCellValue());
  623. } else {
  624. numFmt3 = "#,##0.00";
  625. }
  626. style7.setDataFormat(dataFormat.getFormat(numFmt3));
  627. c7.setCellStyle(style7);
  628. }
  629. }
  630. }
  631. }
  632. if (boxFlag) {
  633. for (Integer dtlRow : boxRows) {
  634. for (int i = 1; i < 6; i++) {
  635. if (i==1 || i==4 || i==5) {
  636. XSSFRow row = sheet.getRow(dtlRow);
  637. if (row == null) {
  638. continue;
  639. }
  640. XSSFCell c7 = row.getCell(i);
  641. if (c7 == null) {
  642. continue;
  643. }
  644. // 尝试把字符串转成数值
  645. if (c7.getCellType() == CellType.STRING) {
  646. String strVal = c7.getStringCellValue();
  647. if (strVal != null && !strVal.trim().isEmpty()) {
  648. try {
  649. double num = Double.parseDouble(strVal.replace(",", ""));
  650. c7.setCellValue(num); // 转换为数值写回
  651. } catch (NumberFormatException e) {
  652. // 如果不是数字就保留原字符串
  653. System.out.println("非数字,保持原值: " + strVal);
  654. }
  655. }
  656. }
  657. // 创建样式
  658. XSSFCellStyle style7 = workbook.createCellStyle();
  659. style7.setBorderRight(BorderStyle.THIN);
  660. style7.setBorderLeft(BorderStyle.THIN);
  661. style7.setBorderBottom(BorderStyle.THIN);
  662. style7.setBorderTop(BorderStyle.THIN);
  663. Font font7 = workbook.createFont();
  664. font7.setFontName("DengXian"); // 等线
  665. font7.setFontHeightInPoints((short) 11); // 11号
  666. style7.setFont(font7);
  667. style7.setVerticalAlignment(VerticalAlignment.CENTER);
  668. style7.setAlignment(HorizontalAlignment.RIGHT);
  669. // 设置千分位格式(根据单元格实际小数位动态决定格式)
  670. DataFormat dataFormat = workbook.createDataFormat();
  671. String numFmt4 = (c7.getCellType() == CellType.NUMERIC)
  672. ? buildDecimalFormat(c7.getNumericCellValue())
  673. : "#,##0";
  674. style7.setDataFormat(dataFormat.getFormat(numFmt4));
  675. c7.setCellStyle(style7);
  676. }
  677. }
  678. }
  679. }
  680. // 处理合并单元格 - 将列表索引转换为实际行号并合并
  681. if (dtlRowIndex >= 0 && !mergeRegions.isEmpty()) {
  682. for (int[] region : mergeRegions) {
  683. int startRow = dtlRowIndex + region[0];
  684. int endRow = dtlRowIndex + region[1];
  685. int startCol = region[2];
  686. int endCol = region.length >= 4 ? region[3] : region[2]; // 支持跨列,如果没有指定endCol则等于startCol
  687. boolean needMerge = startRow < endRow || startCol < endCol;
  688. if (needMerge) {
  689. // 创建新的合并区域(支持横向和纵向合并)
  690. CellRangeAddress mergeRange = new CellRangeAddress(startRow, endRow, startCol, endCol);
  691. // 检查并删除与新合并区域冲突的已存在合并区域
  692. List<Integer> toRemove = new ArrayList<>();
  693. for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
  694. CellRangeAddress existingRegion = sheet.getMergedRegion(i);
  695. if (existingRegion != null && mergeRange.intersects(existingRegion)) {
  696. toRemove.add(i);
  697. }
  698. }
  699. // 从后往前删除,避免索引变化
  700. for (int i = toRemove.size() - 1; i >= 0; i--) {
  701. sheet.removeMergedRegion(toRemove.get(i));
  702. }
  703. // 添加新的合并区域
  704. sheet.addMergedRegion(mergeRange);
  705. }
  706. // 根据是否实际合并设置对齐方式:
  707. // 1) 已合并:垂直居中 + 水平居中
  708. // 2) 未合并(单格):垂直顶对齐 + 水平居中
  709. XSSFRow row = sheet.getRow(startRow);
  710. if (row != null) {
  711. XSSFCell cell = row.getCell(startCol);
  712. if (cell != null) {
  713. XSSFCellStyle mergeStyle = workbook.createCellStyle();
  714. mergeStyle.cloneStyleFrom(cell.getCellStyle());
  715. mergeStyle.setVerticalAlignment(needMerge ? VerticalAlignment.CENTER : VerticalAlignment.TOP);
  716. mergeStyle.setAlignment(HorizontalAlignment.CENTER);
  717. cell.setCellStyle(mergeStyle);
  718. }
  719. }
  720. }
  721. }
  722. // 处理行高设置
  723. if (dtlRowIndex >= 0 && !rowHeights.isEmpty()) {
  724. for (int[] heightInfo : rowHeights) {
  725. int startRow = dtlRowIndex + heightInfo[0];
  726. int endRow = dtlRowIndex + heightInfo[1];
  727. int height = heightInfo[2];
  728. for (int rowIdx = startRow; rowIdx <= endRow; rowIdx++) {
  729. XSSFRow row = sheet.getRow(rowIdx);
  730. if (row != null) {
  731. // 设置行高(Excel行高单位是1/20点,所以需要乘以20)
  732. row.setHeight((short) (height * 20));
  733. }
  734. }
  735. }
  736. }
  737. return workbook;
  738. }
  739. private void applyMultilineCellStyle(XSSFCell cell, String value, boolean voyageCell) {
  740. if (cell == null || value == null || !value.contains("\n")) {
  741. return;
  742. }
  743. XSSFCellStyle wrapStyle = workbook.createCellStyle();
  744. wrapStyle.cloneStyleFrom(cell.getCellStyle());
  745. wrapStyle.setWrapText(true);
  746. wrapStyle.setVerticalAlignment(VerticalAlignment.TOP);
  747. wrapStyle.setAlignment(HorizontalAlignment.LEFT);
  748. if (voyageCell) {
  749. XSSFFont baseFont = workbook.getFontAt(cell.getCellStyle().getFontIndexAsInt());
  750. XSSFFont monoFont = workbook.createFont();
  751. monoFont.setFontName("Courier New");
  752. monoFont.setFontHeight(baseFont.getFontHeight());
  753. monoFont.setBold(false);
  754. monoFont.setColor(baseFont.getColor());
  755. wrapStyle.setFont(monoFont);
  756. }
  757. cell.setCellStyle(wrapStyle);
  758. XSSFRow row = cell.getRow();
  759. if (row != null) {
  760. int lineCount = value.split("\n", -1).length;
  761. CellRangeAddress mergedRegion = getMergedRegionForCell(cell);
  762. int defaultHeight = row.getSheet().getDefaultRowHeight();
  763. if (mergedRegion != null && mergedRegion.getFirstRow() != mergedRegion.getLastRow()) {
  764. int rowSpan = mergedRegion.getLastRow() - mergedRegion.getFirstRow() + 1;
  765. int targetLines = Math.max(2, Math.min(lineCount, 15));
  766. int totalTargetHeight = defaultHeight * targetLines;
  767. short perRowHeight = (short) Math.max(defaultHeight,
  768. Math.min(defaultHeight * 4, (totalTargetHeight + rowSpan - 1) / rowSpan));
  769. for (int rowIndex = mergedRegion.getFirstRow(); rowIndex <= mergedRegion.getLastRow(); rowIndex++) {
  770. XSSFRow mergedRow = row.getSheet().getRow(rowIndex);
  771. if (mergedRow == null) {
  772. mergedRow = row.getSheet().createRow(rowIndex);
  773. }
  774. if (mergedRow.getHeight() < perRowHeight) {
  775. mergedRow.setHeight(perRowHeight);
  776. }
  777. }
  778. } else {
  779. short targetHeight = (short) (defaultHeight * Math.max(2, Math.min(lineCount, 6)));
  780. if (row.getHeight() < targetHeight) {
  781. row.setHeight(targetHeight);
  782. }
  783. }
  784. }
  785. }
  786. private CellRangeAddress getMergedRegionForCell(XSSFCell cell) {
  787. if (cell == null || cell.getSheet() == null) {
  788. return null;
  789. }
  790. int rowIndex = cell.getRowIndex();
  791. int colIndex = cell.getColumnIndex();
  792. for (CellRangeAddress region : cell.getSheet().getMergedRegions()) {
  793. if (region.isInRange(rowIndex, colIndex)) {
  794. return region;
  795. }
  796. }
  797. return null;
  798. }
  799. private void applyHsCodeDescRichText(XSSFCell cell, String value) {
  800. if (cell == null || value == null || !value.contains("货物描述")) {
  801. return;
  802. }
  803. XSSFCellStyle wrapStyle = workbook.createCellStyle();
  804. wrapStyle.cloneStyleFrom(cell.getCellStyle());
  805. wrapStyle.setWrapText(true);
  806. wrapStyle.setVerticalAlignment(VerticalAlignment.TOP);
  807. wrapStyle.setAlignment(HorizontalAlignment.LEFT);
  808. cell.setCellStyle(wrapStyle);
  809. String titleText = "货物描述:";
  810. int titleStart = value.indexOf(titleText);
  811. if (titleStart < 0) {
  812. titleText = "货物描述:";
  813. titleStart = value.indexOf(titleText);
  814. }
  815. if (titleStart < 0) {
  816. return;
  817. }
  818. int titleEnd = titleStart + titleText.length();
  819. XSSFRichTextString richText = new XSSFRichTextString(value);
  820. XSSFFont baseFont = workbook.getFontAt(cell.getCellStyle().getFontIndexAsInt());
  821. XSSFFont titleFont = workbook.createFont();
  822. titleFont.setFontName(baseFont.getFontName());
  823. titleFont.setFontHeight(baseFont.getFontHeight());
  824. titleFont.setColor(baseFont.getColor());
  825. titleFont.setBold(true);
  826. XSSFFont valueFont = workbook.createFont();
  827. valueFont.setFontName(baseFont.getFontName());
  828. valueFont.setFontHeightInPoints((short) 10);
  829. valueFont.setColor(baseFont.getColor());
  830. valueFont.setBold(false);
  831. richText.applyFont(titleStart, titleEnd, titleFont);
  832. if (titleEnd < value.length()) {
  833. richText.applyFont(titleEnd, value.length(), valueFont);
  834. }
  835. cell.setCellValue(richText);
  836. }
  837. private boolean isInvoiceUnitPriceField(String field) {
  838. return "unitPrice".equalsIgnoreCase(field) || "tp".equalsIgnoreCase(field);
  839. }
  840. private boolean isInvoiceTotalPriceField(String field) {
  841. return "totalPrice".equalsIgnoreCase(field) || "ttl_amount".equalsIgnoreCase(field);
  842. }
  843. private boolean isDeclarationFixedPriceField(String field) {
  844. return "net_weight".equalsIgnoreCase(field)
  845. || "unit_price".equalsIgnoreCase(field)
  846. || "total_price".equalsIgnoreCase(field);
  847. }
  848. private boolean isDeclarationQtyField(String field) {
  849. return "qty".equalsIgnoreCase(field);
  850. }
  851. private BigDecimal parseNumericString(String value) {
  852. if (value == null) {
  853. return null;
  854. }
  855. String clean = value.replace(",", "").trim();
  856. if (clean.isEmpty() || "-".equals(clean) || ".".equals(clean)) {
  857. return null;
  858. }
  859. try {
  860. return new BigDecimal(clean);
  861. } catch (NumberFormatException e) {
  862. return null;
  863. }
  864. }
  865. private String buildDecimalFormatByDecimal(BigDecimal value) {
  866. if (value == null) {
  867. return "#,##0";
  868. }
  869. int scale = Math.max(0, value.stripTrailingZeros().scale());
  870. if (scale <= 0) {
  871. return "#,##0";
  872. }
  873. return "#,##0." + "0".repeat(scale);
  874. }
  875. /**
  876. * 根据数值的实际小数位数动态生成 Excel 数字格式串带千分位
  877. * 1.0 "#,##0"12.34 "#,##0.00"0.00123 "#,##0.00000"
  878. * 使用 Double.toString() BigDecimal 再去尾零避免浮点误差导致位数虚高
  879. */
  880. private String buildDecimalFormat(double value) {
  881. if (value == 0) {
  882. return "#,##0";
  883. }
  884. BigDecimal bd = new BigDecimal(Double.toString(Math.abs(value))).stripTrailingZeros();
  885. int scale = Math.max(0, bd.scale());
  886. if (scale == 0) {
  887. return "#,##0";
  888. }
  889. return "#,##0." + "0".repeat(scale);
  890. }
  891. }