| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import com.trafficaudit.holiday.HolidayNumberUtils; |
| | | import com.trafficaudit.holiday.entity.HolidayCalendar; |
| | | import com.trafficaudit.holiday.service.HolidayStatService.DailyMetrics; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Service; |
| | | import org.w3c.dom.Document; |
| | | import org.w3c.dom.Element; |
| | | import org.w3c.dom.Node; |
| | | import org.w3c.dom.NodeList; |
| | | |
| | | import javax.annotation.Resource; |
| | | import javax.xml.parsers.DocumentBuilderFactory; |
| | | import javax.xml.transform.OutputKeys; |
| | | import javax.xml.transform.Transformer; |
| | | import javax.xml.transform.TransformerFactory; |
| | | import javax.xml.transform.dom.DOMSource; |
| | | import javax.xml.transform.stream.StreamResult; |
| | | import java.io.ByteArrayInputStream; |
| | | import java.io.ByteArrayOutputStream; |
| | | import java.io.File; |
| | | import java.io.FileInputStream; |
| | | import java.io.InputStream; |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.nio.file.Files; |
| | | import java.time.LocalDate; |
| | | import java.util.ArrayList; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.function.Function; |
| | | import java.util.zip.ZipEntry; |
| | | import java.util.zip.ZipInputStream; |
| | | import java.util.zip.ZipOutputStream; |
| | | |
| | | @Slf4j |
| | | @Service |
| | | public class HolidayBulletinService { |
| | | |
| | | private static final String WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; |
| | | private static final BigDecimal EPSILON = new BigDecimal("0.0000001"); |
| | | private static final int EXCEL_ONE_DECIMAL_PERCENT = 1; |
| | | private static final int EXCEL_TWO_DECIMAL_PERCENT = 2; |
| | | private static final int EXCEL_SEVEN_DECIMAL_YI = 7; |
| | | private static final BigDecimal TEN_THOUSAND = new BigDecimal("10000"); |
| | | |
| | | @Value("${holiday.bulletin-template:docs/专项统计/重要节假日/湖北省国庆假期综合运输客流快报模板.docx}") |
| | | private String bulletinTemplatePath; |
| | | |
| | | private final HolidayStatService statService; |
| | | @Resource |
| | | private HolidayConfigService configService; |
| | | private static final ThreadLocal<Document> CURRENT_DOCUMENT = new ThreadLocal<>(); |
| | | |
| | | public HolidayBulletinService(HolidayStatService statService) { |
| | | this.statService = statService; |
| | | } |
| | | |
| | | public byte[] generate(LocalDate date) { |
| | | DailyMetrics current = statService.findMetricsByDate(date); |
| | | if (current == null) { |
| | | throw new IllegalArgumentException("所选日期不在已配置的节假日范围内"); |
| | | } |
| | | DailyMetrics previousDay = current.getDayIndex() > 1 |
| | | ? statService.ringRatioBaseMetrics(current.getYear(), current.getHolidayType(), current.getDayIndex()) : null; |
| | | HolidayCalendar previousDayConfig = configService.findPreviousDay(current.getYear(), current.getHolidayType()); |
| | | LocalDate previousDayDate = previousDayConfig == null || previousDayConfig.getHolidayDate() == null |
| | | ? current.getDate().minusDays(1) : previousDayConfig.getHolidayDate(); |
| | | DailyMetrics previousCalendarDay = previousDay != null |
| | | ? previousDay : statService.dailyMetricsByDate(previousDayDate); |
| | | DailyMetrics previousYear = statService.dailyMetrics(current.getYear() - 1, current.getHolidayType(), current.getDayIndex()); |
| | | DailyMetrics twoYearsAgo = statService.dailyMetrics(current.getYear() - 2, current.getHolidayType(), current.getDayIndex()); |
| | | DailyMetrics currentCumulative = statService.cumulativeMetrics(current.getYear(), current.getHolidayType(), current.getDayIndex()); |
| | | DailyMetrics previousYearCumulative = statService.cumulativeMetrics(current.getYear() - 1, current.getHolidayType(), current.getDayIndex()); |
| | | DailyMetrics twoYearsAgoCumulative = statService.cumulativeMetrics(current.getYear() - 2, current.getHolidayType(), current.getDayIndex()); |
| | | |
| | | if (current.getTotalPersonFlow() == null) { |
| | | throw new IllegalArgumentException("所选日期的全社会跨区域人员流动量没有足够数据,无法生成快报"); |
| | | } |
| | | |
| | | String currentTotal = formatNumber(current.getTotalPersonFlow()); |
| | | String ringRatioClause = previousDay == null ? "" : ",环比" + ratioPhrase( |
| | | current.getTotalPersonFlow(), previousDay.getTotalPersonFlow(), EXCEL_TWO_DECIMAL_PERCENT); |
| | | String paragraph1 = String.format("%d月%d日(假期第%d天),全省全社会跨区域人员流动量%s万人次%s,比2025年同期%s,比2024年同期%s。", |
| | | date.getMonthValue(), date.getDayOfMonth(), current.getDayIndex(), currentTotal, |
| | | ringRatioClause, |
| | | ratioPhrase(current.getTotalPersonFlow(), previousYear == null ? null : previousYear.getTotalPersonFlow(), EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratioPhrase(current.getTotalPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getTotalPersonFlow(), EXCEL_ONE_DECIMAL_PERCENT)); |
| | | String paragraph2 = cumulativeParagraph(currentCumulative, previousYearCumulative, twoYearsAgoCumulative); |
| | | |
| | | File template = resolveTemplateFile(); |
| | | if (!template.exists() || !template.isFile()) { |
| | | throw new IllegalArgumentException("快报模板文件不存在: " + template.getAbsolutePath()); |
| | | } |
| | | |
| | | try { |
| | | byte[] templateBytes; |
| | | try (InputStream in = new FileInputStream(template)) { |
| | | templateBytes = readAll(in); |
| | | } |
| | | byte[] documentXml = readZipEntry(templateBytes, "word/document.xml"); |
| | | Document document = parseDocument(documentXml); |
| | | CURRENT_DOCUMENT.set(document); |
| | | try { |
| | | replaceParagraph(document, text -> text.startsWith("10月") && text.contains("假期第"), paragraph1); |
| | | replaceParagraph(document, text -> text.startsWith("假期以来"), paragraph2); |
| | | fillTable(document, current, previousDay, previousCalendarDay, previousYear, twoYearsAgo, |
| | | currentCumulative, previousYearCumulative, twoYearsAgoCumulative); |
| | | byte[] replacedXml = serializeDocument(document); |
| | | return replaceZipEntry(templateBytes, "word/document.xml", replacedXml); |
| | | } finally { |
| | | CURRENT_DOCUMENT.remove(); |
| | | } |
| | | } catch (Exception e) { |
| | | throw new IllegalArgumentException("快报生成失败: " + e.getMessage(), e); |
| | | } |
| | | } |
| | | |
| | | private void fillTable(Document document, DailyMetrics current, DailyMetrics previousDay, |
| | | DailyMetrics previousCalendarDay, DailyMetrics previousYear, DailyMetrics twoYearsAgo, |
| | | DailyMetrics currentCumulative, DailyMetrics previousYearCumulative, |
| | | DailyMetrics twoYearsAgoCumulative) { |
| | | DailyMetrics firstDayRailBase = previousDay == null ? previousCalendarDay : previousDay; |
| | | DailyMetrics firstDayCivilAviationBase = previousDay == null ? previousCalendarDay : previousDay; |
| | | Element table = firstElement(document, "tbl"); |
| | | if (table == null) { |
| | | throw new IllegalArgumentException("快报模板中找不到数据表"); |
| | | } |
| | | DailyMetrics previousYearCivilCumulative = comparableCumulative( |
| | | current, previousYear, DailyMetrics::getCivilAviation); |
| | | DailyMetrics twoYearsAgoCivilCumulative = comparableCumulative( |
| | | current, twoYearsAgo, DailyMetrics::getCivilAviation); |
| | | writeTableRow(table, 2, new String[]{ |
| | | number(current.getNonBusinessPersonFlow()), |
| | | ratio(current.getNonBusinessPersonFlow(), previousDay == null ? null : previousDay.getNonBusinessPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | | ratio(current.getNonBusinessPersonFlow(), previousYear == null ? null : previousYear.getNonBusinessPersonFlow(), |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(current.getNonBusinessPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getNonBusinessPersonFlow(), |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | numberCumulative(currentCumulative.getNonBusinessPersonFlow()), |
| | | ratio(currentCumulative.getNonBusinessPersonFlow(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getNonBusinessPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | | ratio(currentCumulative.getNonBusinessPersonFlow(), |
| | | twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getNonBusinessPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT) |
| | | }); |
| | | writeTableRow(table, 3, new String[]{ |
| | | number(current.getRoad()), |
| | | ratio(current.getRoad(), previousDay == null ? null : previousDay.getRoad(), EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(current.getRoad(), previousYear == null ? null : previousYear.getRoad(), EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(current.getRoad(), twoYearsAgo == null ? null : twoYearsAgo.getRoad(), EXCEL_TWO_DECIMAL_PERCENT), |
| | | number(currentCumulative.getRoad()), |
| | | ratio(currentCumulative.getRoad(), previousYearCumulative == null ? null : previousYearCumulative.getRoad(), |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(currentCumulative.getRoad(), twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRoad(), |
| | | EXCEL_ONE_DECIMAL_PERCENT) |
| | | }); |
| | | writeTableRow(table, 4, new String[]{ |
| | | number(current.getRoadPersonFlow()), |
| | | ratio(current.getRoadPersonFlow(), previousDay == null ? null : previousDay.getRoadPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | | ratio(current.getRoadPersonFlow(), previousYear == null ? null : previousYear.getRoadPersonFlow(), |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(current.getRoadPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getRoadPersonFlow(), |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | numberCumulative(currentCumulative.getRoadPersonFlow()), |
| | | ratio(currentCumulative.getRoadPersonFlow(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getRoadPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | | ratio(currentCumulative.getRoadPersonFlow(), |
| | | twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRoadPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT) |
| | | }); |
| | | writeTableRow(table, 5, transportRow(current.getRail(), |
| | | firstDayRailBase == null ? null : firstDayRailBase.getRail(), |
| | | previousYear == null ? null : previousYear.getRail(), |
| | | twoYearsAgo == null ? null : twoYearsAgo.getRail(), |
| | | currentCumulative.getRail(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getRail(), |
| | | twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRail(), |
| | | EXCEL_ONE_DECIMAL_PERCENT)); |
| | | writeTableRow(table, 6, transportRow(current.getCivilAviation(), |
| | | firstDayCivilAviationBase == null ? null : firstDayCivilAviationBase.getCivilAviation(), |
| | | previousYear == null ? null : previousYear.getCivilAviation(), |
| | | twoYearsAgo == null ? null : twoYearsAgo.getCivilAviation(), |
| | | currentCumulative.getCivilAviation(), |
| | | previousYearCivilCumulative == null ? null : previousYearCivilCumulative.getCivilAviation(), |
| | | twoYearsAgoCivilCumulative == null ? null : twoYearsAgoCivilCumulative.getCivilAviation(), |
| | | EXCEL_ONE_DECIMAL_PERCENT)); |
| | | writeTableRow(table, 7, transportRow(current.getWater(), |
| | | previousDay == null ? null : previousDay.getWater(), |
| | | previousYear == null ? null : previousYear.getWater(), |
| | | twoYearsAgo == null ? null : twoYearsAgo.getWater(), |
| | | currentCumulative.getWater(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getWater(), |
| | | twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getWater(), |
| | | EXCEL_TWO_DECIMAL_PERCENT)); |
| | | writeTableRow(table, 8, new String[]{ |
| | | number(current.getTotalPersonFlow()), |
| | | ratio(current.getTotalPersonFlow(), previousDay == null ? null : previousDay.getTotalPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | | ratio(current.getTotalPersonFlow(), previousYear == null ? null : previousYear.getTotalPersonFlow(), |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(current.getTotalPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getTotalPersonFlow(), |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | numberCumulative(currentCumulative.getTotalPersonFlow()), |
| | | ratio(currentCumulative.getTotalPersonFlow(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | | ratio(currentCumulative.getTotalPersonFlow(), |
| | | twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getTotalPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT) |
| | | }); |
| | | } |
| | | |
| | | private String[] transportRow(BigDecimal current, BigDecimal previousDay, BigDecimal previousYear, |
| | | BigDecimal twoYearsAgo, BigDecimal currentCumulative, |
| | | BigDecimal previousYearCumulative, BigDecimal twoYearsAgoCumulative, |
| | | int ringRatioExcelScale) { |
| | | return new String[]{ |
| | | number(current), |
| | | ratio(current, previousDay, ringRatioExcelScale), |
| | | ratio(current, previousYear, EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(current, twoYearsAgo, EXCEL_ONE_DECIMAL_PERCENT), |
| | | number(currentCumulative), |
| | | ratio(currentCumulative, previousYearCumulative, EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(currentCumulative, twoYearsAgoCumulative, EXCEL_ONE_DECIMAL_PERCENT) |
| | | }; |
| | | } |
| | | |
| | | private void writeTableRow(Element table, int rowIndex, String[] values) { |
| | | List<Element> rows = childElements(table, "tr"); |
| | | if (rowIndex >= rows.size()) return; |
| | | Element row = rows.get(rowIndex); |
| | | List<Element> cells = cellRefs(row); |
| | | int logicalColumn = 0; |
| | | for (Element cell : cells) { |
| | | int span = gridSpan(cell); |
| | | if (logicalColumn >= 2) { |
| | | int valueIndex = logicalColumn - 2; |
| | | if (valueIndex < values.length) { |
| | | setCellText(cell, values[valueIndex]); |
| | | } |
| | | } |
| | | logicalColumn += span; |
| | | } |
| | | } |
| | | |
| | | private List<Element> cellRefs(Element row) { |
| | | List<Element> cells = new ArrayList<>(); |
| | | for (Element child : childElements(row, "tc")) cells.add(child); |
| | | return cells; |
| | | } |
| | | |
| | | private int gridSpan(Element cell) { |
| | | Element tcPr = firstChildElement(cell, "tcPr"); |
| | | if (tcPr == null) return 1; |
| | | Element span = firstChildElement(tcPr, "gridSpan"); |
| | | if (span == null) return 1; |
| | | String value = span.getAttributeNS(WORD_NS, "val"); |
| | | try { |
| | | return Integer.parseInt(value); |
| | | } catch (Exception e) { |
| | | return 1; |
| | | } |
| | | } |
| | | |
| | | private void setCellText(Element cell, String text) { |
| | | Element tcPr = firstChildElement(cell, "tcPr"); |
| | | Element templateParagraph = firstChildElement(cell, "p"); |
| | | Element paragraph = createElement("p"); |
| | | if (templateParagraph != null) { |
| | | Element pPr = firstChildElement(templateParagraph, "pPr"); |
| | | if (pPr != null) paragraph.appendChild(pPr.cloneNode(true)); |
| | | } |
| | | Element templateRun = templateParagraph == null ? null : firstChildElement(templateParagraph, "r"); |
| | | Element rPr = templateRun == null ? null : firstChildElement(templateRun, "rPr"); |
| | | Element run = createElement("r"); |
| | | if (rPr != null) run.appendChild(rPr.cloneNode(true)); |
| | | Element t = createElement("t"); |
| | | t.setAttributeNS("http://www.w3.org/XML/1998/namespace", "xml:space", "preserve"); |
| | | t.setTextContent(text == null ? "" : text); |
| | | run.appendChild(t); |
| | | paragraph.appendChild(run); |
| | | |
| | | List<Node> removals = new ArrayList<>(); |
| | | Node child = cell.getFirstChild(); |
| | | while (child != null) { |
| | | Node next = child.getNextSibling(); |
| | | if (child instanceof Element && "p".equals(localName(child))) { |
| | | removals.add(child); |
| | | } |
| | | child = next; |
| | | } |
| | | for (Node node : removals) cell.removeChild(node); |
| | | if (tcPr != null && tcPr.getNextSibling() != null) { |
| | | cell.insertBefore(paragraph, tcPr.getNextSibling()); |
| | | } else if (tcPr != null) { |
| | | cell.appendChild(paragraph); |
| | | } else { |
| | | cell.insertBefore(paragraph, cell.getFirstChild()); |
| | | } |
| | | } |
| | | |
| | | private void replaceParagraph(Document document, ParagraphMatcher matcher, String replacement) { |
| | | NodeList paragraphs = document.getElementsByTagNameNS(WORD_NS, "p"); |
| | | for (int i = 0; i < paragraphs.getLength(); i++) { |
| | | Element paragraph = (Element) paragraphs.item(i); |
| | | String text = paragraphText(paragraph); |
| | | if (matcher.matches(text)) { |
| | | setParagraphText(paragraph, replacement); |
| | | return; |
| | | } |
| | | } |
| | | throw new IllegalArgumentException("快报模板缺少可替换段落: " + replacement); |
| | | } |
| | | |
| | | private void setParagraphText(Element paragraph, String text) { |
| | | Element pPr = firstChildElement(paragraph, "pPr"); |
| | | Element firstRun = firstChildElement(paragraph, "r"); |
| | | Element rPr = firstRun == null ? null : firstChildElement(firstRun, "rPr"); |
| | | List<Node> removals = new ArrayList<>(); |
| | | Node child = paragraph.getFirstChild(); |
| | | while (child != null) { |
| | | Node next = child.getNextSibling(); |
| | | if (child != pPr) removals.add(child); |
| | | child = next; |
| | | } |
| | | for (Node node : removals) paragraph.removeChild(node); |
| | | |
| | | Element run = createElement("r"); |
| | | if (rPr != null) run.appendChild(rPr.cloneNode(true)); |
| | | Element t = createElement("t"); |
| | | t.setAttributeNS("http://www.w3.org/XML/1998/namespace", "xml:space", "preserve"); |
| | | t.setTextContent(text); |
| | | run.appendChild(t); |
| | | paragraph.appendChild(run); |
| | | } |
| | | |
| | | private String paragraphText(Element paragraph) { |
| | | StringBuilder builder = new StringBuilder(); |
| | | NodeList texts = paragraph.getElementsByTagNameNS(WORD_NS, "t"); |
| | | for (int i = 0; i < texts.getLength(); i++) { |
| | | builder.append(texts.item(i).getTextContent()); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | |
| | | private String ratioPhrase(BigDecimal current, BigDecimal base, int excelPercentScale) { |
| | | BigDecimal percent = bulletinPercent(current, base, excelPercentScale); |
| | | if (percent == null) return "无可比数据"; |
| | | if (percent.signum() == 0) return "持平"; |
| | | return (percent.signum() > 0 ? "增长" : "下降") + HolidayNumberUtils.format1(percent.abs()) + "%"; |
| | | } |
| | | |
| | | private DailyMetrics comparableCumulative(DailyMetrics current, DailyMetrics comparisonYear, |
| | | Function<DailyMetrics, BigDecimal> selector) { |
| | | if (current == null || comparisonYear == null) return null; |
| | | int coveredDay = 0; |
| | | for (int day = current.getDayIndex(); day >= 1; day--) { |
| | | DailyMetrics metrics = statService.dailyMetrics(current.getYear(), current.getHolidayType(), day); |
| | | if (metrics != null && selector.apply(metrics) != null) { |
| | | coveredDay = day; |
| | | break; |
| | | } |
| | | } |
| | | if (coveredDay == 0) return null; |
| | | return statService.cumulativeMetrics(comparisonYear.getYear(), current.getHolidayType(), coveredDay); |
| | | } |
| | | |
| | | private String cumulativeParagraph(DailyMetrics currentCumulative, |
| | | DailyMetrics previousYearCumulative, |
| | | DailyMetrics twoYearsAgoCumulative) { |
| | | BigDecimal total = currentCumulative == null ? null : currentCumulative.getTotalPersonFlow(); |
| | | return String.format("假期以来,全省全社会跨区域人员流动量累计%s万人次,比2025年同期%s,比2024年同期%s。", |
| | | formatNumber(displayedCumulativeWan(total)), |
| | | ratioPhrase(total, previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | | ratioPhrase(total, twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getTotalPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT)); |
| | | } |
| | | |
| | | private String ratio(BigDecimal current, BigDecimal base, int excelPercentScale) { |
| | | BigDecimal percent = bulletinPercent(current, base, excelPercentScale); |
| | | if (percent == null) return "—"; |
| | | return percent.signum() == 0 ? "0%" : percent.toPlainString() + "%"; |
| | | } |
| | | |
| | | /** |
| | | * Mirrors the source Excel percentage cell format, then applies the bulletin's one-decimal rule. |
| | | * Excel stores full precision but displays 0.0% or 0.00%; reviewers compare against that display. |
| | | */ |
| | | private BigDecimal bulletinPercent(BigDecimal current, BigDecimal base, int excelPercentScale) { |
| | | if (current == null || base == null || base.abs().compareTo(EPSILON) < 0) return null; |
| | | BigDecimal ratio = HolidayNumberUtils.divide(current, base); |
| | | if (ratio == null) return null; |
| | | BigDecimal percent = HolidayNumberUtils.multiply(ratio.subtract(BigDecimal.ONE), BigDecimal.valueOf(100)); |
| | | if (percent == null) return null; |
| | | BigDecimal displayed = percent.setScale(excelPercentScale, RoundingMode.HALF_UP); |
| | | return displayed.setScale(1, RoundingMode.HALF_UP); |
| | | } |
| | | |
| | | private String number(BigDecimal value) { |
| | | return value == null ? "—" : formatNumber(value); |
| | | } |
| | | |
| | | private String numberCumulative(BigDecimal value) { |
| | | return value == null ? "—" : formatNumber(displayedCumulativeWan(value)); |
| | | } |
| | | |
| | | /** |
| | | * Source cumulative cells U5/X5/AA5 are stored in 100-million units and displayed with seven decimals. |
| | | * Mirror that displayed precision before the bulletin converts the value to 10-thousand units. |
| | | */ |
| | | private BigDecimal displayedCumulativeWan(BigDecimal cumulativeWan) { |
| | | if (cumulativeWan == null) return null; |
| | | BigDecimal yi = cumulativeWan.divide(TEN_THOUSAND, EXCEL_SEVEN_DECIMAL_YI, RoundingMode.HALF_UP); |
| | | return HolidayNumberUtils.normalize(yi.multiply(TEN_THOUSAND)); |
| | | } |
| | | |
| | | private String formatNumber(BigDecimal value) { |
| | | if (value == null) return ""; |
| | | return HolidayNumberUtils.format2(value); |
| | | } |
| | | |
| | | private File resolveTemplateFile() { |
| | | List<File> candidates = new ArrayList<>(); |
| | | File configured = new File(bulletinTemplatePath); |
| | | candidates.add(configured); |
| | | if (!configured.isAbsolute()) { |
| | | candidates.add(new File(System.getProperty("user.dir"), bulletinTemplatePath)); |
| | | candidates.add(new File(System.getProperty("user.dir"), "../" + bulletinTemplatePath)); |
| | | } |
| | | for (File candidate : candidates) { |
| | | try { |
| | | if (candidate.exists() && candidate.isFile()) { |
| | | return candidate.getCanonicalFile(); |
| | | } |
| | | } catch (Exception ignored) { |
| | | // Keep checking. |
| | | } |
| | | } |
| | | return configured; |
| | | } |
| | | |
| | | private byte[] readZipEntry(byte[] zipBytes, String entryName) throws Exception { |
| | | try (ZipInputStream zin = new ZipInputStream(new ByteArrayInputStream(zipBytes))) { |
| | | ZipEntry entry; |
| | | while ((entry = zin.getNextEntry()) != null) { |
| | | if (entryName.equals(entry.getName())) { |
| | | return readAll(zin); |
| | | } |
| | | } |
| | | } |
| | | throw new IllegalArgumentException("模板中缺少 " + entryName); |
| | | } |
| | | |
| | | private byte[] replaceZipEntry(byte[] zipBytes, String entryName, byte[] replacement) throws Exception { |
| | | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| | | try (ZipInputStream zin = new ZipInputStream(new ByteArrayInputStream(zipBytes)); |
| | | ZipOutputStream zout = new ZipOutputStream(out)) { |
| | | ZipEntry entry; |
| | | byte[] buffer = new byte[8192]; |
| | | while ((entry = zin.getNextEntry()) != null) { |
| | | ZipEntry target = new ZipEntry(entry.getName()); |
| | | zout.putNextEntry(target); |
| | | if (entryName.equals(entry.getName())) { |
| | | zout.write(replacement); |
| | | } else { |
| | | int length; |
| | | while ((length = zin.read(buffer)) > 0) { |
| | | zout.write(buffer, 0, length); |
| | | } |
| | | } |
| | | zout.closeEntry(); |
| | | } |
| | | } |
| | | return out.toByteArray(); |
| | | } |
| | | |
| | | private Document parseDocument(byte[] xml) throws Exception { |
| | | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); |
| | | factory.setNamespaceAware(true); |
| | | return factory.newDocumentBuilder().parse(new ByteArrayInputStream(xml)); |
| | | } |
| | | |
| | | private byte[] serializeDocument(Document document) throws Exception { |
| | | Transformer transformer = TransformerFactory.newInstance().newTransformer(); |
| | | transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); |
| | | transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); |
| | | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| | | transformer.transform(new DOMSource(document), new StreamResult(out)); |
| | | return out.toByteArray(); |
| | | } |
| | | |
| | | private Element firstElement(Document document, String localName) { |
| | | NodeList list = document.getElementsByTagNameNS(WORD_NS, localName); |
| | | return list.getLength() == 0 ? null : (Element) list.item(0); |
| | | } |
| | | |
| | | private Element createElement(String localName) { |
| | | Document document = CURRENT_DOCUMENT.get(); |
| | | if (document == null) { |
| | | throw new IllegalStateException("Word document is not initialized"); |
| | | } |
| | | return document.createElementNS(WORD_NS, "w:" + localName); |
| | | } |
| | | |
| | | private List<Element> childElements(Element parent, String localName) { |
| | | List<Element> result = new ArrayList<>(); |
| | | Node child = parent.getFirstChild(); |
| | | while (child != null) { |
| | | if (child instanceof Element && localName.equals(localName(child))) { |
| | | result.add((Element) child); |
| | | } |
| | | child = child.getNextSibling(); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Element firstChildElement(Element parent, String localName) { |
| | | Node child = parent.getFirstChild(); |
| | | while (child != null) { |
| | | if (child instanceof Element && localName.equals(localName(child))) { |
| | | return (Element) child; |
| | | } |
| | | child = child.getNextSibling(); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private String localName(Node node) { |
| | | return node.getLocalName() == null ? node.getNodeName().replaceFirst("^.*:", "") : node.getLocalName(); |
| | | } |
| | | |
| | | private byte[] readAll(InputStream in) throws Exception { |
| | | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| | | byte[] buffer = new byte[8192]; |
| | | int length; |
| | | while ((length = in.read(buffer)) > 0) { |
| | | out.write(buffer, 0, length); |
| | | } |
| | | return out.toByteArray(); |
| | | } |
| | | |
| | | private interface ParagraphMatcher { |
| | | boolean matches(String text); |
| | | } |
| | | } |