4 changed files with 457 additions and 0 deletions
@ -0,0 +1,82 @@ |
|||
package org.dromara.demo.domain; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 检查项实体类(平铺结构中的最小单位) |
|||
* 每个字段说明如下: |
|||
*/ |
|||
@Data |
|||
@AllArgsConstructor |
|||
@NoArgsConstructor |
|||
public class CheckItem { |
|||
|
|||
/** |
|||
* 检查项目(用于合并单元格) |
|||
* 示例值:"01 站房环境保障情况" |
|||
* 多个 CheckItem 可共享相同的 checkProject,表示属于同一类别 |
|||
*/ |
|||
private String checkProject; |
|||
|
|||
/** |
|||
* 具体检查子项描述 |
|||
* 示例值:"站房清洁与规范管理" |
|||
* 表示该项目下的具体考核条目 |
|||
*/ |
|||
private String checkItem; |
|||
|
|||
/** |
|||
* 该项满分分数 |
|||
* 示例值:2 |
|||
* 表示该检查项的总分为 2 分 |
|||
*/ |
|||
private Integer itemScore; |
|||
|
|||
/** |
|||
* 实际获得分数 |
|||
* 示例值:1 |
|||
* 表示本次检查实际得分为 1 分 |
|||
*/ |
|||
private Integer obtainedScore; |
|||
|
|||
/** |
|||
* 评分细则列表(前端传入的多条依据) |
|||
* 示例值: |
|||
* [ |
|||
* "1)站房环境脏,有明显灰尘和异味;", |
|||
* "2)物品摆放不整齐;" |
|||
* ] |
|||
* 每条为独立字符串,导出时需换行显示 |
|||
*/ |
|||
private List<String> scoringBasis; |
|||
|
|||
/** |
|||
* 扣分规则说明 |
|||
* 示例值:"一项不合格扣0.5分,扣完为止" |
|||
* 用于说明扣分逻辑,通常显示在评分细则下方 |
|||
*/ |
|||
private String deductionRule; |
|||
|
|||
/** |
|||
* 格式化评分说明(用于导出 Word) |
|||
* 将评分细则和扣分规则拼接成带换行符的字符串 |
|||
* |
|||
* @return 格式化后的完整说明文本 |
|||
*/ |
|||
public String formatScoringExplanation() { |
|||
StringBuilder sb = new StringBuilder(); |
|||
if (this.scoringBasis != null) { |
|||
for (String basis : this.scoringBasis) { |
|||
sb.append(basis).append("\n"); |
|||
} |
|||
} |
|||
if (this.deductionRule != null && !this.deductionRule.trim().isEmpty()) { |
|||
sb.append(this.deductionRule); |
|||
} |
|||
return sb.toString().trim(); |
|||
} |
|||
} |
@ -0,0 +1,67 @@ |
|||
package org.dromara.demo.domain; |
|||
|
|||
import lombok.Data; |
|||
import java.time.LocalDate; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 监理服务现场技术核查评分报告数据模型 |
|||
* 注意:checkItems 是平铺列表,每个 CheckItem 都包含 checkProject 字段 |
|||
* 用于后续按 checkProject 值进行合并单元格(Word 或前端表格) |
|||
*/ |
|||
@Data |
|||
public class ReportData { |
|||
|
|||
/** 报告标题,如:监理服务现场技术核查评分报告 */ |
|||
private String reportTitle; |
|||
|
|||
/** 站点名称,如:浦东新区张江空气质量监测站 */ |
|||
private String siteName; |
|||
|
|||
/** 所属区或管委会,如:浦东新区 */ |
|||
private String district; |
|||
|
|||
/** 所属街道、乡或镇,如:张江镇 */ |
|||
private String street; |
|||
|
|||
/** 运维单位,如:上海环保科技有限公司 */ |
|||
private String operationUnit; |
|||
|
|||
/** 检查时间,格式:yyyy-MM-dd */ |
|||
private LocalDate checkTime; |
|||
|
|||
/** 检查人员,多人用顿号或逗号分隔,如:张三、李四 */ |
|||
private String checkPersonnel; |
|||
|
|||
/** |
|||
* 检查项列表(平铺结构) |
|||
* 每个 CheckItem 自带 checkProject 字段 |
|||
* 多个 CheckItem 可能具有相同的 checkProject,用于合并单元格 |
|||
*/ |
|||
private List<CheckItem> checkItems; |
|||
|
|||
/** 总分 */ |
|||
private Integer totalScore; |
|||
|
|||
/** 备注 */ |
|||
private String remarks; |
|||
|
|||
public int getTotalPossibleScore() { |
|||
return getCheckItems().stream() |
|||
.mapToInt(CheckItem::getItemScore) |
|||
.sum(); |
|||
} |
|||
|
|||
public int getTotalObtainedScore() { |
|||
return getCheckItems().stream() |
|||
.mapToInt(CheckItem::getObtainedScore) |
|||
.sum(); |
|||
} |
|||
|
|||
// 可选:返回格式化字符串 "85/100"
|
|||
public String getFormattedTotalScore() { |
|||
int obtained = getTotalObtainedScore(); |
|||
int total = getTotalPossibleScore(); |
|||
return obtained + "/" + total; |
|||
} |
|||
} |
@ -0,0 +1,303 @@ |
|||
package org.dromara.demo.domain; |
|||
|
|||
import cn.hutool.json.JSONUtil; |
|||
import org.apache.poi.xwpf.usermodel.*; |
|||
import org.dromara.demo.domain.ReportData; |
|||
import org.dromara.demo.domain.CheckItem; |
|||
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblWidth; |
|||
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblWidth; |
|||
|
|||
import java.io.FileOutputStream; |
|||
import java.io.IOException; |
|||
import java.io.File; |
|||
import java.math.BigInteger; |
|||
import java.time.format.DateTimeFormatter; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.stream.Collectors; |
|||
|
|||
public class WordReportExporter { |
|||
|
|||
// 获取桌面路径(跨平台)
|
|||
private static String getDesktopPath() { |
|||
return "D:\\Users\\14212\\Desktop"; |
|||
} |
|||
|
|||
public static void export(ReportData reportData) throws IOException { |
|||
System.out.println(JSONUtil.toJsonStr(reportData)); |
|||
|
|||
String desktopPath = getDesktopPath(); |
|||
String filename = "数据购买监理服务现场技术核查评分表.docx"; |
|||
String fullPath = desktopPath + File.separator + filename; |
|||
|
|||
XWPFDocument document = new XWPFDocument(); |
|||
|
|||
// 1. 添加标题
|
|||
XWPFParagraph titlePara = document.createParagraph(); |
|||
titlePara.setAlignment(ParagraphAlignment.CENTER); |
|||
XWPFRun titleRun = titlePara.createRun(); |
|||
titleRun.setText(reportData.getReportTitle()); |
|||
titleRun.setBold(true); |
|||
titleRun.setFontSize(16); |
|||
titleRun.setFontFamily("宋体"); |
|||
|
|||
// 2. 添加基本信息
|
|||
addBasicInfo(document, reportData); |
|||
|
|||
// 3. 添加评分表格
|
|||
addScoreTable(document, reportData); |
|||
|
|||
// 4. 添加总分和备注
|
|||
addTotalAndRemarks(document, reportData); |
|||
|
|||
// 保存到桌面
|
|||
try (FileOutputStream out = new FileOutputStream(fullPath)) { |
|||
document.write(out); |
|||
} |
|||
document.close(); |
|||
|
|||
System.out.println("✅ 报告已成功导出到桌面:"); |
|||
System.out.println("📄 " + fullPath); |
|||
} |
|||
|
|||
private static void addBasicInfo(XWPFDocument document, ReportData reportData) { |
|||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); |
|||
|
|||
// 站点名称
|
|||
XWPFParagraph p = document.createParagraph(); |
|||
p.setSpacingAfter(10); |
|||
|
|||
XWPFRun label = p.createRun(); |
|||
label.setFontFamily("宋体"); |
|||
label.setFontSize(12); |
|||
label.setBold(true); |
|||
label.setText("站点名称:"); |
|||
|
|||
XWPFRun value = p.createRun(); |
|||
value.setFontFamily("宋体"); |
|||
value.setFontSize(12); |
|||
value.setBold(false); |
|||
value.setText(reportData.getSiteName()); |
|||
|
|||
// 区(管委会)
|
|||
p = document.createParagraph(); |
|||
p.setSpacingAfter(10); |
|||
|
|||
label = p.createRun(); |
|||
label.setFontFamily("宋体"); |
|||
label.setFontSize(12); |
|||
label.setBold(true); |
|||
label.setText("区(管委会):"); |
|||
|
|||
value = p.createRun(); |
|||
value.setFontFamily("宋体"); |
|||
value.setFontSize(12); |
|||
value.setBold(false); |
|||
value.setText(reportData.getDistrict()); |
|||
|
|||
// 街道(乡、镇)
|
|||
p = document.createParagraph(); |
|||
p.setSpacingAfter(10); |
|||
|
|||
label = p.createRun(); |
|||
label.setFontFamily("宋体"); |
|||
label.setFontSize(12); |
|||
label.setBold(true); |
|||
label.setText("街道(乡、镇):"); |
|||
|
|||
value = p.createRun(); |
|||
value.setFontFamily("宋体"); |
|||
value.setFontSize(12); |
|||
value.setBold(false); |
|||
value.setText(reportData.getStreet()); |
|||
|
|||
// 运维单位
|
|||
p = document.createParagraph(); |
|||
p.setSpacingAfter(10); |
|||
|
|||
label = p.createRun(); |
|||
label.setFontFamily("宋体"); |
|||
label.setFontSize(12); |
|||
label.setBold(true); |
|||
label.setText("运维单位:"); |
|||
|
|||
value = p.createRun(); |
|||
value.setFontFamily("宋体"); |
|||
value.setFontSize(12); |
|||
value.setBold(false); |
|||
value.setText(reportData.getOperationUnit()); |
|||
|
|||
// 检查时间
|
|||
p = document.createParagraph(); |
|||
p.setSpacingAfter(10); |
|||
|
|||
label = p.createRun(); |
|||
label.setFontFamily("宋体"); |
|||
label.setFontSize(12); |
|||
label.setBold(true); |
|||
label.setText("检查时间:"); |
|||
|
|||
value = p.createRun(); |
|||
value.setFontFamily("宋体"); |
|||
value.setFontSize(12); |
|||
value.setBold(false); |
|||
value.setText(reportData.getCheckTime().format(formatter)); |
|||
|
|||
// 检查人员
|
|||
p = document.createParagraph(); |
|||
p.setSpacingAfter(10); |
|||
|
|||
label = p.createRun(); |
|||
label.setFontFamily("宋体"); |
|||
label.setFontSize(12); |
|||
label.setBold(true); |
|||
label.setText("检查人员:"); |
|||
|
|||
value = p.createRun(); |
|||
value.setFontFamily("宋体"); |
|||
value.setFontSize(12); |
|||
value.setBold(false); |
|||
value.setText(reportData.getCheckPersonnel()); |
|||
} |
|||
|
|||
private static void addScoreTable(XWPFDocument document, ReportData reportData) { |
|||
List<CheckItem> items = reportData.getCheckItems(); |
|||
if (items == null || items.isEmpty()) return; |
|||
|
|||
XWPFTable table = document.createTable(); |
|||
CTTblWidth width = table.getCTTbl().getTblPr().addNewTblW(); |
|||
width.setW(BigInteger.valueOf(9000)); |
|||
width.setType(STTblWidth.DXA); |
|||
|
|||
// 表头
|
|||
XWPFTableRow header = table.getRow(0); |
|||
setCellTextAndStyle(header.getCell(0), "检查项目", true); |
|||
header.addNewTableCell(); |
|||
setCellTextAndStyle(header.getCell(1), "检查子项", true); |
|||
header.addNewTableCell(); |
|||
setCellTextAndStyle(header.getCell(2), "满分分数", true); |
|||
header.addNewTableCell(); |
|||
setCellTextAndStyle(header.getCell(3), "实际得分", true); |
|||
header.addNewTableCell(); |
|||
setCellTextAndStyle(header.getCell(4), "评分说明", true); |
|||
|
|||
// 按 checkProject 分组
|
|||
Map<String, List<CheckItem>> grouped = items.stream() |
|||
.collect(Collectors.groupingBy(CheckItem::getCheckProject)); |
|||
|
|||
for (Map.Entry<String, List<CheckItem>> entry : grouped.entrySet()) { |
|||
List<CheckItem> groupItems = entry.getValue(); |
|||
String project = entry.getKey(); |
|||
|
|||
for (int i = 0; i < groupItems.size(); i++) { |
|||
CheckItem item = groupItems.get(i); |
|||
XWPFTableRow row = table.createRow(); |
|||
|
|||
if (i == 0) { |
|||
XWPFTableCell cell = row.getCell(0); |
|||
cell.getCTTc().addNewTcPr().addNewVMerge().setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge.RESTART); |
|||
setCellTextAndStyle(cell, project, false); |
|||
} else { |
|||
XWPFTableCell cell = row.getCell(0); |
|||
cell.getCTTc().addNewTcPr().addNewVMerge().setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge.CONTINUE); |
|||
cell.setText(""); |
|||
} |
|||
|
|||
setCellTextAndStyle(row.getCell(1), item.getCheckItem(), false); |
|||
setCellTextAndStyle(row.getCell(2), String.valueOf(item.getItemScore()), false); |
|||
setCellTextAndStyle(row.getCell(3), String.valueOf(item.getObtainedScore()), false); |
|||
setCellTextAndStyle(row.getCell(4), item.formatScoringExplanation(), false); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void setCellTextAndStyle(XWPFTableCell cell, String text, boolean isHeader) { |
|||
cell.getCTTc().addNewTcPr().addNewTcW().setW(BigInteger.valueOf(1800)); |
|||
XWPFParagraph p = cell.getParagraphs().get(0); |
|||
p.setSpacingAfter(50); |
|||
XWPFRun r = p.createRun(); |
|||
r.setText(text); |
|||
r.setFontFamily("宋体"); |
|||
r.setFontSize(10); |
|||
if (isHeader) { |
|||
r.setBold(true); |
|||
} |
|||
} |
|||
|
|||
private static void addTotalAndRemarks(XWPFDocument document, ReportData reportData) { |
|||
XWPFParagraph totalPara = document.createParagraph(); |
|||
XWPFRun totalRun = totalPara.createRun(); |
|||
totalRun.setBold(true); |
|||
totalRun.setText("总分:" + reportData.getTotalScore()); |
|||
|
|||
XWPFParagraph remarksPara = document.createParagraph(); |
|||
XWPFRun remarksRun = remarksPara.createRun(); |
|||
remarksRun.setText("备注:" + reportData.getRemarks()); |
|||
|
|||
} |
|||
|
|||
// ==================== 测试入口 ====================
|
|||
public static void main(String[] args) { |
|||
try { |
|||
ReportData report = new ReportData(); |
|||
|
|||
report.setReportTitle("数据购买监理服务现场技术核查评分表"); |
|||
report.setSiteName("杭州西湖空气质量监测站"); |
|||
report.setDistrict("西湖区"); |
|||
report.setStreet("龙翔桥"); |
|||
report.setOperationUnit("国研信息"); |
|||
report.setCheckTime(java.time.LocalDate.now()); |
|||
report.setCheckPersonnel("张三、李四"); |
|||
//report.setTotalScore(95);
|
|||
report.setRemarks("注1:《环境空气环境空气气态污染物(SO2、NO2、O3、CO)连续自动监测系统技术要求及检测方法HJ654-2013》\n" + |
|||
"*标项一包含的项目,标项二不包含。\n" + |
|||
"04质量控制效果中相关质控因子有缺项的,分值按比例分摊。\n"); |
|||
|
|||
CheckItem item1 = new CheckItem(); |
|||
item1.setCheckProject("01 站房环境保障情况"); |
|||
item1.setCheckItem("站房清洁与规范管理"); |
|||
item1.setItemScore(2); |
|||
item1.setObtainedScore(1); |
|||
item1.setScoringBasis(List.of( |
|||
"1)站房环境脏,有明显灰尘和异味;", |
|||
"2)物品摆放不整齐;" |
|||
)); |
|||
item1.setDeductionRule("一项不合格扣0.5分,扣完为止(评分规则)"); |
|||
|
|||
CheckItem item2 = new CheckItem(); |
|||
item2.setCheckProject("01 站房环境保障情况"); |
|||
item2.setCheckItem("仪器运行状态检查"); |
|||
item2.setItemScore(3); |
|||
item2.setObtainedScore(3); |
|||
item2.setScoringBasis(List.of("1)仪器运行正常;")); |
|||
item2.setDeductionRule("发现异常未处理扣1分"); |
|||
|
|||
CheckItem item3 = new CheckItem(); |
|||
item3.setCheckProject("02 站房环境保障情况"); |
|||
item3.setCheckItem("仪器运行状态检查"); |
|||
item3.setItemScore(3); |
|||
item3.setObtainedScore(3); |
|||
item3.setScoringBasis(List.of("1)仪器运行正常;")); |
|||
item3.setDeductionRule("发现异常未处理扣1分"); |
|||
|
|||
report.setCheckItems(List.of(item1, item2, item3)); |
|||
|
|||
// 🔢 动态计算总分
|
|||
int totalScore = report.getCheckItems().stream() |
|||
.mapToInt(CheckItem::getObtainedScore) |
|||
.sum(); |
|||
int maxScore = report.getCheckItems().stream() |
|||
.mapToInt(CheckItem::getItemScore) |
|||
.sum(); |
|||
|
|||
// ✅ 设置为格式化字符串(推荐)
|
|||
report.setTotalScore(totalScore); |
|||
// ✅ 默认导出到桌面
|
|||
WordReportExporter.export(report); |
|||
|
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
System.err.println("❌ 导出失败:" + e.getMessage()); |
|||
} |
|||
} |
|||
} |
Loading…
Reference in new issue