package com.rongyichuang.review.service;
|
|
import com.rongyichuang.activity.entity.Activity;
|
import com.rongyichuang.activity.repository.ActivityRepository;
|
import com.rongyichuang.player.repository.ActivityPlayerRepository;
|
import com.rongyichuang.review.dto.response.ReviewExportResponse;
|
import com.rongyichuang.judge.service.CosService;
|
import com.rongyichuang.player.entity.ActivityPlayer;
|
import com.rongyichuang.activity.repository.ActivityPlayerRatingRepository;
|
import com.rongyichuang.activity.repository.ActivityPlayerRatingItemRepository;
|
import com.rongyichuang.rating.repository.RatingItemRepository;
|
import com.rongyichuang.rating.entity.RatingItem;
|
import com.rongyichuang.judge.repository.JudgeRepository;
|
import com.rongyichuang.judge.entity.Judge;
|
import org.slf4j.Logger;
|
import org.slf4j.LoggerFactory;
|
import org.springframework.stereotype.Service;
|
|
import java.io.File;
|
import java.io.ByteArrayOutputStream;
|
import java.io.FileOutputStream;
|
import java.io.OutputStreamWriter;
|
import java.io.PrintWriter;
|
import java.nio.charset.StandardCharsets;
|
import java.time.LocalDateTime;
|
import java.time.format.DateTimeFormatter;
|
import java.util.List;
|
import java.util.ArrayList;
|
import java.util.Map;
|
import java.util.HashMap;
|
import java.util.Optional;
|
import java.util.UUID;
|
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipOutputStream;
|
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
|
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
|
import org.apache.poi.xwpf.usermodel.XWPFRun;
|
import org.apache.poi.xwpf.usermodel.XWPFTable;
|
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
|
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPr;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblWidth;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblGrid;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblGridCol;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblWidth;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblBorders;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBorder;
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder;
|
import java.math.BigInteger;
|
|
@Service
|
public class ReviewExportService {
|
private static final Logger log = LoggerFactory.getLogger(ReviewExportService.class);
|
|
private final ActivityRepository activityRepository;
|
private final ActivityPlayerRepository activityPlayerRepository;
|
private final ActivityPlayerRatingRepository activityPlayerRatingRepository;
|
private final ActivityPlayerRatingItemRepository activityPlayerRatingItemRepository;
|
private final RatingItemRepository ratingItemRepository;
|
private final JudgeRepository judgeRepository;
|
private final CosService cosService;
|
|
public ReviewExportService(ActivityRepository activityRepository,
|
ActivityPlayerRepository activityPlayerRepository,
|
ActivityPlayerRatingRepository activityPlayerRatingRepository,
|
ActivityPlayerRatingItemRepository activityPlayerRatingItemRepository,
|
RatingItemRepository ratingItemRepository,
|
JudgeRepository judgeRepository,
|
CosService cosService) {
|
this.activityRepository = activityRepository;
|
this.activityPlayerRepository = activityPlayerRepository;
|
this.activityPlayerRatingRepository = activityPlayerRatingRepository;
|
this.activityPlayerRatingItemRepository = activityPlayerRatingItemRepository;
|
this.ratingItemRepository = ratingItemRepository;
|
this.judgeRepository = judgeRepository;
|
this.cosService = cosService;
|
}
|
|
/**
|
* 导出评审结果ZIP(占位实现:生成简单的README汇总并上传,返回URL)
|
*/
|
public ReviewExportResponse exportReviewZip(Long activityId, List<Long> stageIds) {
|
try {
|
// 1) 校验活动
|
Optional<Activity> activityOpt = activityRepository.findById(activityId);
|
if (activityOpt.isEmpty()) {
|
return new ReviewExportResponse(false, null, "活动不存在: " + activityId);
|
}
|
Activity activity = activityOpt.get();
|
|
// 2) 组织导出文件名
|
// 仅保留字母、数字、连字符,以及所有Unicode字母字符(涵盖中文等);其他替换为下划线
|
String safeName = activity.getName() != null ? activity.getName().replaceAll("[^\\p{L}\\p{N}-]", "_") : "activity";
|
String ts = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
|
String fileName = String.format("review-%s-%s.zip", safeName, ts);
|
|
// 3) 构建临时ZIP
|
File zipFile = new File(System.getProperty("java.io.tmpdir"), "ryc-export-" + UUID.randomUUID() + ".zip");
|
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
|
// README(避免在此处关闭ZipOutputStream)
|
ZipEntry readme = new ZipEntry("README.txt");
|
zos.putNextEntry(readme);
|
StringBuilder readmeSb = new StringBuilder();
|
readmeSb.append("蓉易创评审导出\n");
|
readmeSb.append("活动: ").append(activity.getName()).append(" (ID=").append(activity.getId()).append(")\n");
|
readmeSb.append("导出时间: ").append(LocalDateTime.now()).append("\n");
|
if (stageIds != null && !stageIds.isEmpty()) {
|
readmeSb.append("阶段ID: ").append(stageIds).append("\n");
|
// 简要统计每个阶段报名人数(占位统计)
|
for (Long stageId : stageIds) {
|
int playerCount = activityPlayerRepository.findByStageId(stageId).size();
|
readmeSb.append("阶段 ").append(stageId).append(" 报名人数: ").append(playerCount).append("\n");
|
}
|
} else {
|
// 主活动报名人数(不区分阶段)
|
int playerCount = activityPlayerRepository.findByActivityId(activityId).size();
|
readmeSb.append("活动报名人数: ").append(playerCount).append("\n");
|
}
|
byte[] readmeBytes = readmeSb.toString().getBytes(StandardCharsets.UTF_8);
|
zos.write(readmeBytes);
|
zos.closeEntry();
|
|
// 生成每个选手的DOCX评分表,打包到ZIP中(基础实现)
|
if (stageIds != null && !stageIds.isEmpty()) {
|
for (Long stageId : stageIds) {
|
Activity stage = activityRepository.findById(stageId).orElse(null);
|
String stageName = stage != null ? stage.getName() : ("阶段" + stageId);
|
List<ActivityPlayer> players = activityPlayerRepository.findByStageIdAndStateWithPlayerOrderByCreateTimeDesc(stageId, 1);
|
for (ActivityPlayer ap : players) {
|
addPlayerDocToZip(zos, activity, stageName, ap);
|
}
|
}
|
} else {
|
List<ActivityPlayer> players = activityPlayerRepository.findByActivityIdWithPlayerAndRegion(activityId);
|
for (ActivityPlayer ap : players) {
|
String stageName = ap.getStageId() != null ? activityRepository.findById(ap.getStageId()).map(Activity::getName).orElse("阶段" + ap.getStageId()) : "主活动";
|
addPlayerDocToZip(zos, activity, stageName, ap);
|
}
|
}
|
}
|
|
// 4) 上传到COS
|
String key = cosService.uploadLocalFile(zipFile, fileName);
|
String url = cosService.getFileUrl(key);
|
log.info("评审导出上传完成,COS Key: {}, URL: {}", key, url);
|
|
// 5) 写入 t_activity.review_export_url
|
if (stageIds != null && !stageIds.isEmpty()) {
|
for (Long stageId : stageIds) {
|
Activity stage = activityRepository.findById(stageId).orElse(null);
|
if (stage != null) {
|
stage.setReviewExportUrl(url);
|
activityRepository.save(stage);
|
}
|
}
|
} else {
|
activity.setReviewExportUrl(url);
|
activityRepository.save(activity);
|
}
|
|
// 删除临时文件
|
try { zipFile.delete(); } catch (Exception ignore) {}
|
|
return new ReviewExportResponse(true, url, "导出成功");
|
} catch (Exception ex) {
|
log.error("导出评审ZIP失败", ex);
|
return new ReviewExportResponse(false, null, "导出失败: " + ex.getMessage());
|
}
|
}
|
|
private void addPlayerDocToZip(ZipOutputStream zos, Activity activity, String stageName, ActivityPlayer ap) throws Exception {
|
String projectName = ap.getProjectName() != null ? ap.getProjectName() : "未命名项目";
|
String playerName = ap.getPlayer() != null && ap.getPlayer().getName() != null ? ap.getPlayer().getName() : ("选手" + ap.getPlayerId());
|
|
// 生成DOCX内容
|
XWPFDocument doc = new XWPFDocument();
|
// 设置页面为A4并应用合理页边距,保证打印版式
|
applyPageSettings(doc);
|
// 标题
|
XWPFParagraph title = doc.createParagraph();
|
title.setAlignment(ParagraphAlignment.CENTER);
|
XWPFRun tr = title.createRun();
|
tr.setText("评审评分表");
|
tr.setBold(true);
|
tr.setFontSize(18);
|
|
// 基本信息
|
XWPFParagraph p1 = doc.createParagraph();
|
XWPFRun r1 = p1.createRun();
|
r1.setText("活动:" + safeText(activity.getName()) + " 阶段:" + safeText(stageName));
|
|
XWPFParagraph p2 = doc.createParagraph();
|
XWPFRun r2 = p2.createRun();
|
r2.setText("项目:" + safeText(projectName) + " 选手:" + safeText(playerName));
|
|
// 查询所有评委评分
|
List<com.rongyichuang.activity.entity.ActivityPlayerRating> ratings = activityPlayerRatingRepository.findByActivityPlayerId(ap.getId());
|
if (ratings == null || ratings.isEmpty()) {
|
// 无评分时,仍然生成评分项模板表格,便于打印或线下评分
|
XWPFParagraph pEmpty = doc.createParagraph();
|
XWPFRun re = pEmpty.createRun();
|
re.setText("尚无评分(以下为评分项模板)");
|
re.setItalic(true);
|
|
// 使用活动的评分方案生成空表格(设置表格宽度和列宽,避免表格挤压)
|
XWPFTable table = doc.createTable();
|
applyTableLayout(table, new int[]{6072, 1500, 1500});
|
XWPFTableRow header = table.getRow(0);
|
ensureCells(header, 3);
|
setCellText(header.getCell(0), "评分项");
|
setCellText(header.getCell(1), "得分");
|
setCellText(header.getCell(2), "满分");
|
|
// 优先使用阶段的评分方案;如果选手没有阶段或阶段不存在,则回退到主活动评分方案
|
Long schemeIdToUse = null;
|
if (ap.getStageId() != null) {
|
try {
|
Activity stageEntity = activityRepository.findById(ap.getStageId()).orElse(null);
|
if (stageEntity != null && stageEntity.getRatingSchemeId() != null) {
|
schemeIdToUse = stageEntity.getRatingSchemeId();
|
}
|
} catch (Exception ignore) {}
|
}
|
if (schemeIdToUse == null) {
|
schemeIdToUse = activity.getRatingSchemeId();
|
}
|
|
List<RatingItem> items = schemeIdToUse != null
|
? ratingItemRepository.findBySchemeIdOrderByOrderNo(schemeIdToUse)
|
: new ArrayList<>();
|
for (RatingItem item : items) {
|
XWPFTableRow row = table.createRow();
|
ensureCells(row, 3);
|
setCellText(row.getCell(0), safeText(item.getName()));
|
setCellText(row.getCell(1), "-");
|
setCellText(row.getCell(2), item.getMaxScore() != null ? String.valueOf(item.getMaxScore()) : "-");
|
}
|
} else {
|
for (com.rongyichuang.activity.entity.ActivityPlayerRating rating : ratings) {
|
XWPFParagraph judgeTitle = doc.createParagraph();
|
XWPFRun jr = judgeTitle.createRun();
|
String judgeName = rating.getJudgeId() != null ? judgeRepository.findById(rating.getJudgeId()).map(Judge::getName).orElse("评委" + rating.getJudgeId()) : "未知评委";
|
jr.setText("评委:" + safeText(judgeName) + " 总分:" + (rating.getTotalScore() != null ? rating.getTotalScore() : "-") );
|
jr.setBold(true);
|
|
// 构建评分项表格
|
XWPFTable table = doc.createTable();
|
applyTableLayout(table, new int[]{6072, 1500, 1500});
|
XWPFTableRow header = table.getRow(0);
|
ensureCells(header, 3);
|
setCellText(header.getCell(0), "评分项");
|
setCellText(header.getCell(1), "得分");
|
setCellText(header.getCell(2), "满分");
|
|
List<RatingItem> items = rating.getRatingSchemeId() != null ? ratingItemRepository.findBySchemeIdOrderByOrderNo(rating.getRatingSchemeId()) : new ArrayList<>();
|
// 查询评分项得分
|
List<com.rongyichuang.activity.entity.ActivityPlayerRatingItem> ratingItems = activityPlayerRatingItemRepository.findByActivityPlayerRatingId(rating.getId());
|
Map<Long, java.math.BigDecimal> scoreMap = new HashMap<>();
|
for (com.rongyichuang.activity.entity.ActivityPlayerRatingItem ri : ratingItems) {
|
scoreMap.put(ri.getRatingItemId(), ri.getScore());
|
}
|
for (RatingItem item : items) {
|
XWPFTableRow row = table.createRow();
|
ensureCells(row, 3);
|
setCellText(row.getCell(0), safeText(item.getName()));
|
java.math.BigDecimal s = scoreMap.get(item.getId());
|
setCellText(row.getCell(1), s != null ? s.toPlainString() : "-");
|
setCellText(row.getCell(2), item.getMaxScore() != null ? String.valueOf(item.getMaxScore()) : "-");
|
}
|
}
|
}
|
|
// 在文档底部添加专家评审签字区域(参考模板格式)
|
addSignatureSection(doc);
|
|
// 写入到ZIP
|
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
doc.write(baos);
|
String fileBase = sanitizeFileName(stageName) + "/" + sanitizeFileName(projectName + "-" + playerName) + ".docx";
|
String entryName = "players/" + fileBase;
|
ZipEntry entry = new ZipEntry(entryName);
|
zos.putNextEntry(entry);
|
zos.write(baos.toByteArray());
|
zos.closeEntry();
|
}
|
doc.close();
|
}
|
|
private String sanitizeFileName(String name) {
|
return name == null ? "unknown" : name.replaceAll("[^\\p{L}\\p{N}-]", "_");
|
}
|
|
private String safeText(String text) {
|
return text == null ? "-" : text;
|
}
|
|
/**
|
* 设置表格的总宽度和列宽,避免出现列宽过窄导致内容挤在一起
|
* @param table 表格
|
* @param colWidthsTwips 每列宽度(单位:twips),示例:{6072, 1500, 1500}
|
*/
|
private void applyTableLayout(XWPFTable table, int[] colWidthsTwips) {
|
// 设置表格总宽度(页面标准宽度约 9072 twips)
|
CTTbl ctTbl = table.getCTTbl();
|
CTTblPr tblPr = ctTbl.getTblPr() == null ? ctTbl.addNewTblPr() : ctTbl.getTblPr();
|
CTTblWidth tblW = tblPr.isSetTblW() ? tblPr.getTblW() : tblPr.addNewTblW();
|
tblW.setType(STTblWidth.DXA);
|
int total = 0;
|
for (int w : colWidthsTwips) total += w;
|
tblW.setW(BigInteger.valueOf(total));
|
|
// 设置列网格(列宽)
|
CTTblGrid grid = ctTbl.getTblGrid() == null ? ctTbl.addNewTblGrid() : ctTbl.getTblGrid();
|
// 清理已有列定义(如果有)
|
while (grid.sizeOfGridColArray() > 0) {
|
grid.removeGridCol(0);
|
}
|
for (int w : colWidthsTwips) {
|
CTTblGridCol col = grid.addNewGridCol();
|
col.setW(BigInteger.valueOf(w));
|
}
|
|
// 设置表格边框,保证打印效果清晰
|
CTTblBorders borders = tblPr.isSetTblBorders() ? tblPr.getTblBorders() : tblPr.addNewTblBorders();
|
CTBorder border = borders.isSetInsideH() ? borders.getInsideH() : borders.addNewInsideH();
|
border.setVal(STBorder.SINGLE);
|
border.setSz(BigInteger.valueOf(8)); // 8八分之一点 = 1pt
|
border.setColor("000000");
|
border = borders.isSetInsideV() ? borders.getInsideV() : borders.addNewInsideV();
|
border.setVal(STBorder.SINGLE);
|
border.setSz(BigInteger.valueOf(8));
|
border.setColor("000000");
|
border = borders.isSetTop() ? borders.getTop() : borders.addNewTop();
|
border.setVal(STBorder.SINGLE);
|
border.setSz(BigInteger.valueOf(8));
|
border.setColor("000000");
|
border = borders.isSetBottom() ? borders.getBottom() : borders.addNewBottom();
|
border.setVal(STBorder.SINGLE);
|
border.setSz(BigInteger.valueOf(8));
|
border.setColor("000000");
|
border = borders.isSetLeft() ? borders.getLeft() : borders.addNewLeft();
|
border.setVal(STBorder.SINGLE);
|
border.setSz(BigInteger.valueOf(8));
|
border.setColor("000000");
|
// 修复右边框未设置粗细的问题,确保四周边框一致
|
border = borders.isSetRight() ? borders.getRight() : borders.addNewRight();
|
border.setVal(STBorder.SINGLE);
|
border.setSz(BigInteger.valueOf(8));
|
border.setColor("000000");
|
}
|
|
/**
|
* 确保一行有指定数量的单元格
|
*/
|
private void ensureCells(XWPFTableRow row, int cellCount) {
|
int existing = row.getTableCells().size();
|
for (int i = existing; i < cellCount; i++) {
|
row.addNewTableCell();
|
}
|
}
|
|
/**
|
* 设置单元格文本并为该单元格设置宽度(与网格匹配)
|
*/
|
private void setCellText(XWPFTableCell cell, String text) {
|
// 直接设置文本
|
try {
|
// 清空默认段落,避免重复(某些情况下单元格可能没有默认段落)
|
if (cell.getParagraphs().size() > 0) {
|
cell.removeParagraph(0);
|
}
|
} catch (Exception ignore) {}
|
XWPFParagraph p = cell.addParagraph();
|
p.setAlignment(ParagraphAlignment.LEFT);
|
XWPFRun run = p.createRun();
|
run.setFontSize(12);
|
// 使用中文字体,保证中文显示效果更接近模板
|
try {
|
run.setFontFamily("宋体");
|
} catch (Exception ignore) {}
|
run.setText(text);
|
|
// 为单元格设置宽度(DXA)以防在某些Word版本中未应用表格网格宽度
|
CTTcPr tcPr = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();
|
CTTblWidth w = tcPr.isSetTcW() ? tcPr.getTcW() : tcPr.addNewTcW();
|
w.setType(STTblWidth.DXA);
|
// 宽度值由所在列决定,这里不重复设置具体数值,避免与表格网格冲突
|
}
|
|
/**
|
* 添加专家评审签字区域(两列:签字、日期)
|
*/
|
private void addSignatureSection(XWPFDocument doc) {
|
// 留出一定的上边距
|
XWPFParagraph spacer = doc.createParagraph();
|
spacer.setSpacingBefore(200);
|
|
XWPFTable signTable = doc.createTable(1, 2);
|
applyTableLayout(signTable, new int[]{6500, 2572});
|
XWPFTableRow row = signTable.getRow(0);
|
|
ensureCells(row, 2);
|
setCellText(row.getCell(0), "专家评审签字:_______________");
|
setCellText(row.getCell(1), "签字日期:__________");
|
}
|
|
/**
|
* 设置页面大小与页边距(A4,默认边距约1英寸)
|
*/
|
private void applyPageSettings(XWPFDocument doc) {
|
try {
|
// A4 尺寸:宽 11907 twips(21cm),高 16840 twips(29.7cm)
|
var body = doc.getDocument().getBody();
|
var sectPr = body.isSetSectPr() ? body.getSectPr() : body.addNewSectPr();
|
var pgSz = sectPr.isSetPgSz() ? sectPr.getPgSz() : sectPr.addNewPgSz();
|
pgSz.setW(BigInteger.valueOf(11907));
|
pgSz.setH(BigInteger.valueOf(16840));
|
|
// 页边距:约 1 英寸(1440 twips),可根据需要微调
|
var pgMar = sectPr.isSetPgMar() ? sectPr.getPgMar() : sectPr.addNewPgMar();
|
pgMar.setLeft(BigInteger.valueOf(1440));
|
pgMar.setRight(BigInteger.valueOf(1440));
|
pgMar.setTop(BigInteger.valueOf(1440));
|
pgMar.setBottom(BigInteger.valueOf(1440));
|
} catch (Exception ignore) {
|
// 兼容性考虑:不影响文档生成
|
}
|
}
|
}
|