|
|
@@ -6,21 +6,33 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
+import org.apache.pdfbox.pdmodel.PDDocument;
|
|
|
+import org.apache.pdfbox.pdmodel.PDPage;
|
|
|
+import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
|
|
+import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
|
|
+import org.apache.pdfbox.pdmodel.font.PDType0Font;
|
|
|
import org.dromara.common.core.exception.ServiceException;
|
|
|
import org.dromara.common.core.utils.MapstructUtils;
|
|
|
import org.dromara.common.core.utils.StringUtils;
|
|
|
import org.dromara.common.json.utils.JsonUtils;
|
|
|
import org.dromara.common.mybatis.core.page.PageQuery;
|
|
|
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
|
|
+import org.dromara.common.oss.core.OssClient;
|
|
|
+import org.dromara.common.oss.entity.UploadResult;
|
|
|
+import org.dromara.common.oss.factory.OssFactory;
|
|
|
import org.dromara.main.domain.MainBackCandidate;
|
|
|
import org.dromara.main.domain.MainBackCategory;
|
|
|
import org.dromara.main.domain.MainBackClause;
|
|
|
import org.dromara.main.domain.MainBackOrder;
|
|
|
import org.dromara.main.domain.MainBackRecord;
|
|
|
import org.dromara.main.domain.MainOrder;
|
|
|
+import org.dromara.main.domain.MainPosition;
|
|
|
+import org.dromara.main.domain.MainPostCandidateReview;
|
|
|
import org.dromara.main.domain.bo.MainBackOrderBo;
|
|
|
import org.dromara.main.domain.bo.PortalCheckOrderCreateBo;
|
|
|
import org.dromara.main.domain.MainStudent;
|
|
|
+import org.dromara.main.domain.MainStudentEducation;
|
|
|
+import org.dromara.main.domain.MainStudentExperience;
|
|
|
import org.dromara.main.domain.Payment;
|
|
|
import org.dromara.main.domain.vo.MainBackClauseVo;
|
|
|
import org.dromara.main.domain.vo.MainBackOrderCandidateVo;
|
|
|
@@ -31,6 +43,10 @@ import org.dromara.main.mapper.MainBackClauseMapper;
|
|
|
import org.dromara.main.mapper.MainBackOrderMapper;
|
|
|
import org.dromara.main.mapper.MainBackRecordMapper;
|
|
|
import org.dromara.main.mapper.MainOrderMapper;
|
|
|
+import org.dromara.main.mapper.MainPositionMapper;
|
|
|
+import org.dromara.main.mapper.MainPostCandidateReviewMapper;
|
|
|
+import org.dromara.main.mapper.MainStudentEducationMapper;
|
|
|
+import org.dromara.main.mapper.MainStudentExperienceMapper;
|
|
|
import org.dromara.main.mapper.MainStudentMapper;
|
|
|
import org.dromara.main.mapper.PaymentMapper;
|
|
|
import org.dromara.main.service.ICompanyAccountService;
|
|
|
@@ -43,8 +59,14 @@ import org.dromara.system.service.ISysTenantService;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
+import java.io.ByteArrayInputStream;
|
|
|
+import java.io.ByteArrayOutputStream;
|
|
|
+import java.io.IOException;
|
|
|
import java.math.BigDecimal;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.Path;
|
|
|
import java.time.LocalDateTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.Comparator;
|
|
|
import java.util.HashMap;
|
|
|
@@ -63,6 +85,11 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
|
|
|
private static final String RECORD_STATUS_PENDING_AUTH = "未完成";
|
|
|
private static final String RECORD_STATUS_AUTHORIZED = "已授权";
|
|
|
private static final String RECORD_STATUS_UNAUTHORIZED = "未授权";
|
|
|
+ private static final DateTimeFormatter REPORT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
|
|
+ private static final float REPORT_MARGIN = 52F;
|
|
|
+ private static final float REPORT_TITLE_SIZE = 18F;
|
|
|
+ private static final float REPORT_HEADING_SIZE = 13F;
|
|
|
+ private static final float REPORT_BODY_SIZE = 10.5F;
|
|
|
private static final Set<String> AUTHORIZATION_TERMINAL_STATUSES =
|
|
|
new HashSet<>(List.of(RECORD_STATUS_AUTHORIZED, RECORD_STATUS_UNAUTHORIZED));
|
|
|
|
|
|
@@ -73,6 +100,10 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
|
|
|
private final MainBackClauseMapper mainBackClauseMapper;
|
|
|
private final MainBackCandidateMapper mainBackCandidateMapper;
|
|
|
private final MainStudentMapper mainStudentMapper;
|
|
|
+ private final MainStudentEducationMapper mainStudentEducationMapper;
|
|
|
+ private final MainStudentExperienceMapper mainStudentExperienceMapper;
|
|
|
+ private final MainPostCandidateReviewMapper mainPostCandidateReviewMapper;
|
|
|
+ private final MainPositionMapper mainPositionMapper;
|
|
|
private final PaymentMapper paymentMapper;
|
|
|
private final SysTenantMapper sysTenantMapper;
|
|
|
private final ISysTenantService tenantService;
|
|
|
@@ -516,6 +547,41 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
|
|
|
return buildPortalCandidateVo(record);
|
|
|
}
|
|
|
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public String generatePortalCandidateReport(Long recordId, String tenantId) {
|
|
|
+ if (recordId == null) {
|
|
|
+ throw new ServiceException("候选人记录不能为空");
|
|
|
+ }
|
|
|
+ if (StringUtils.isBlank(tenantId)) {
|
|
|
+ throw new ServiceException("未登录或token已失效");
|
|
|
+ }
|
|
|
+
|
|
|
+ MainBackRecord record = recordMapper.selectById(recordId);
|
|
|
+ if (record == null) {
|
|
|
+ throw new ServiceException("候选人记录不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ MainBackOrder backOrder = baseMapper.selectById(record.getOrderId());
|
|
|
+ if (backOrder == null || !StringUtils.equals(backOrder.getTenantId(), tenantId)) {
|
|
|
+ throw new ServiceException("无权查看该候选人的报告");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (StringUtils.isNotBlank(record.getReportUrl())) {
|
|
|
+ return record.getReportUrl();
|
|
|
+ }
|
|
|
+
|
|
|
+ ReportContext context = buildReportContext(record, backOrder);
|
|
|
+ byte[] pdfBytes = buildReportPdf(context);
|
|
|
+ String reportUrl = uploadReportPdf(record, context, pdfBytes);
|
|
|
+
|
|
|
+ MainBackRecord update = new MainBackRecord();
|
|
|
+ update.setId(record.getId());
|
|
|
+ update.setReportUrl(reportUrl);
|
|
|
+ recordMapper.updateById(update);
|
|
|
+ return reportUrl;
|
|
|
+ }
|
|
|
+
|
|
|
private void fillPortalFields(MainBackOrderVo vo, MainOrder mainOrder) {
|
|
|
Long count = recordMapper.selectCount(
|
|
|
new LambdaQueryWrapper<MainBackRecord>()
|
|
|
@@ -779,6 +845,387 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
|
|
|
return defaultValue;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ private ReportContext buildReportContext(MainBackRecord record, MainBackOrder backOrder) {
|
|
|
+ MainBackCandidate candidate = mainBackCandidateMapper.selectById(record.getCandidateId());
|
|
|
+ MainStudent student = candidate == null ? null : mainStudentMapper.selectById(candidate.getStudentId());
|
|
|
+ MainPostCandidateReview review = mainPostCandidateReviewMapper.selectOne(
|
|
|
+ Wrappers.<MainPostCandidateReview>lambdaQuery()
|
|
|
+ .eq(MainPostCandidateReview::getCandidateId, record.getCandidateId())
|
|
|
+ .last("limit 1")
|
|
|
+ );
|
|
|
+ MainStudentEducation education = candidate == null ? null : mainStudentEducationMapper.selectOne(
|
|
|
+ Wrappers.<MainStudentEducation>lambdaQuery()
|
|
|
+ .eq(MainStudentEducation::getStudentId, candidate.getStudentId())
|
|
|
+ .orderByDesc(MainStudentEducation::getEndTime)
|
|
|
+ .orderByDesc(MainStudentEducation::getCreateTime)
|
|
|
+ .last("limit 1")
|
|
|
+ );
|
|
|
+ MainStudentExperience experience = candidate == null ? null : mainStudentExperienceMapper.selectOne(
|
|
|
+ Wrappers.<MainStudentExperience>lambdaQuery()
|
|
|
+ .eq(MainStudentExperience::getStudentId, candidate.getStudentId())
|
|
|
+ .orderByDesc(MainStudentExperience::getEndTime)
|
|
|
+ .orderByDesc(MainStudentExperience::getCreateTime)
|
|
|
+ .last("limit 1")
|
|
|
+ );
|
|
|
+
|
|
|
+ Long postId = review != null && review.getPostId() != null ? review.getPostId() : candidate == null ? null : candidate.getPostId();
|
|
|
+ MainPosition position = postId == null ? null : mainPositionMapper.selectById(postId);
|
|
|
+ SysTenantVo tenant = tenantService.queryByTenantId(backOrder.getTenantId());
|
|
|
+
|
|
|
+ ReportContext context = new ReportContext();
|
|
|
+ context.student = student;
|
|
|
+ context.review = review;
|
|
|
+ context.education = education;
|
|
|
+ context.experience = experience;
|
|
|
+ context.position = position;
|
|
|
+ context.record = record;
|
|
|
+ context.backOrder = backOrder;
|
|
|
+ context.companyName = tenant == null ? null : tenant.getCompanyName();
|
|
|
+ return context;
|
|
|
+ }
|
|
|
+
|
|
|
+ private byte[] buildReportPdf(ReportContext context) {
|
|
|
+ try (PDDocument document = new PDDocument();
|
|
|
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
|
|
+ PDType0Font font = loadChineseFont(document);
|
|
|
+ ReportWriter writer = new ReportWriter(document, font);
|
|
|
+
|
|
|
+ writer.addTitle("背景调查报告");
|
|
|
+ writer.addKeyValueSection("候选人基本信息", List.of(
|
|
|
+ "姓名:" + safe(context.student == null ? null : context.student.getName()),
|
|
|
+ "应聘职位:" + safe(resolvePositionName(context)),
|
|
|
+ "调查时间:" + safe(resolveSurveyTime(context))
|
|
|
+ ));
|
|
|
+ writer.addKeyValueSection("学历核实", List.of(
|
|
|
+ "毕业院校:" + safe(resolveSchoolName(context)),
|
|
|
+ "学历证书编号:" + "",
|
|
|
+ "学信网核实:" + safe(resolveEducationVerification(context))
|
|
|
+ ));
|
|
|
+ writer.addKeyValueSection("最近工作单位(上家公司)", List.of(
|
|
|
+ "公司名称:" + safe(context.experience == null ? null : context.experience.getCompany()),
|
|
|
+ "岗位名称:" + safe(resolveLatestJobTitle(context)),
|
|
|
+ "在职时间:" + safe(resolveEmploymentPeriod(context)),
|
|
|
+ "任职状态:" + safe(resolveEmploymentStatus(context)),
|
|
|
+ "离职原因:" + safe(context.review == null ? null : context.review.getLeaveReason())
|
|
|
+ ));
|
|
|
+ writer.addParagraphSection("工作职责/信息来源", safe(resolveWorkContent(context)));
|
|
|
+ writer.addParagraphSection("综合评价", safe(context.review == null ? null : context.review.getTotalRemark()));
|
|
|
+ writer.addRatingSection("能力评价", List.of(
|
|
|
+ buildAbilityLine(context.review == null ? null : context.review.getAbilityAName(),
|
|
|
+ context.review == null ? null : context.review.getAbilityARate(),
|
|
|
+ context.review == null ? null : context.review.getAbilityARemark()),
|
|
|
+ buildAbilityLine(context.review == null ? null : context.review.getAbilityBName(),
|
|
|
+ context.review == null ? null : context.review.getAbilityBRate(),
|
|
|
+ context.review == null ? null : context.review.getAbilityBRemark()),
|
|
|
+ buildAbilityLine(context.review == null ? null : context.review.getAbilityCName(),
|
|
|
+ context.review == null ? null : context.review.getAbilityCRate(),
|
|
|
+ context.review == null ? null : context.review.getAbilityCRemark())
|
|
|
+ ));
|
|
|
+ writer.addQuestionAnswerSection("模板问答整理", List.of(
|
|
|
+ new QaLine("请问你和候选人是什么时候认识的?他的职务是什么?具体工作职责有哪些方面?",
|
|
|
+ safe(resolveKnownAnswer(context))),
|
|
|
+ new QaLine("你觉得他在工作方面有哪些优势?比较擅长哪方面的工作?", ""),
|
|
|
+ new QaLine("他在职业道德方面的表现怎么样?你有没有看到或听到关于他不太好的现象和评价?", ""),
|
|
|
+ new QaLine("请问他人际关系怎么样?和其他同事相处有没有问题?", ""),
|
|
|
+ new QaLine("你对他的总体评价如何?你觉得他哪些方面需要提高和改进呢?",
|
|
|
+ safe(context.review == null ? null : context.review.getTotalRemark()))
|
|
|
+ ));
|
|
|
+ writer.addParagraphSection("备注", safe(context.companyName));
|
|
|
+ writer.close();
|
|
|
+
|
|
|
+ document.save(outputStream);
|
|
|
+ return outputStream.toByteArray();
|
|
|
+ } catch (IOException e) {
|
|
|
+ throw new ServiceException("生成报告PDF失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private PDType0Font loadChineseFont(PDDocument document) throws IOException {
|
|
|
+ List<String> fontPaths = List.of(
|
|
|
+ "C:/Windows/Fonts/msyh.ttc",
|
|
|
+ "C:/Windows/Fonts/simsun.ttc",
|
|
|
+ "C:/Windows/Fonts/simhei.ttf"
|
|
|
+ );
|
|
|
+ for (String fontPath : fontPaths) {
|
|
|
+ Path path = Path.of(fontPath);
|
|
|
+ if (Files.exists(path)) {
|
|
|
+ try {
|
|
|
+ return PDType0Font.load(document, Files.newInputStream(path), true);
|
|
|
+ } catch (IOException ignored) {
|
|
|
+ // 继续尝试下一个可用字体
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ throw new ServiceException("未找到可用的中文字体文件,无法生成报告PDF");
|
|
|
+ }
|
|
|
+
|
|
|
+ private String uploadReportPdf(MainBackRecord record, ReportContext context, byte[] pdfBytes) {
|
|
|
+ try (ByteArrayInputStream inputStream = new ByteArrayInputStream(pdfBytes)) {
|
|
|
+ OssClient ossClient = OssFactory.instance();
|
|
|
+ String key = "report/check/" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM"))
|
|
|
+ + "/" + record.getId() + "-" + sanitizeFileName(safe(context.student == null ? null : context.student.getName()), "candidate")
|
|
|
+ + ".pdf";
|
|
|
+ UploadResult uploadResult = ossClient.upload(inputStream, key, (long) pdfBytes.length, "application/pdf");
|
|
|
+ return uploadResult.getUrl();
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new ServiceException("上传报告PDF失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolvePositionName(ReportContext context) {
|
|
|
+ if (context.position != null && StringUtils.isNotBlank(context.position.getPostName())) {
|
|
|
+ return context.position.getPostName();
|
|
|
+ }
|
|
|
+ return resolveLatestJobTitle(context);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveLatestJobTitle(ReportContext context) {
|
|
|
+ return context.experience == null ? null : context.experience.getJobTitle();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveSurveyTime(ReportContext context) {
|
|
|
+ if (context.record.getFinishTime() != null) {
|
|
|
+ return context.record.getFinishTime().format(REPORT_TIME_FORMATTER);
|
|
|
+ }
|
|
|
+ if (context.record.getCreateTime() != null) {
|
|
|
+ return context.record.getCreateTime().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime()
|
|
|
+ .format(REPORT_TIME_FORMATTER);
|
|
|
+ }
|
|
|
+ return LocalDateTime.now().format(REPORT_TIME_FORMATTER);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveSchoolName(ReportContext context) {
|
|
|
+ if (context.education != null && StringUtils.isNotBlank(context.education.getSchool())) {
|
|
|
+ return context.education.getSchool();
|
|
|
+ }
|
|
|
+ return context.student == null ? null : context.student.getSchoolName();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveEducationVerification(ReportContext context) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveEmploymentPeriod(ReportContext context) {
|
|
|
+ if (context.review != null && (context.review.getEntryTime() != null || context.review.getLeaveTime() != null)) {
|
|
|
+ return joinPeriod(
|
|
|
+ context.review.getEntryTime() == null ? null : cn.hutool.core.date.DateUtil.format(context.review.getEntryTime(), "yyyy-MM-dd"),
|
|
|
+ context.review.getLeaveTime() == null ? null : cn.hutool.core.date.DateUtil.format(context.review.getLeaveTime(), "yyyy-MM-dd")
|
|
|
+ );
|
|
|
+ }
|
|
|
+ if (context.experience != null) {
|
|
|
+ return joinPeriod(context.experience.getStartTime(), context.experience.getEndTime());
|
|
|
+ }
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ private String joinPeriod(String start, String end) {
|
|
|
+ if (StringUtils.isBlank(start) && StringUtils.isBlank(end)) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ return safe(start) + " 至 " + safe(end);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveEmploymentStatus(ReportContext context) {
|
|
|
+ if (context.review == null || StringUtils.isBlank(context.review.getEmploymentStatus())) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ return "left".equalsIgnoreCase(context.review.getEmploymentStatus()) ? "已离职" : "在职";
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveWorkContent(ReportContext context) {
|
|
|
+ return context.experience == null ? null : context.experience.getWorkContent();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveKnownAnswer(ReportContext context) {
|
|
|
+ List<String> parts = new ArrayList<>();
|
|
|
+ if (context.experience != null && StringUtils.isNotBlank(context.experience.getStartTime())) {
|
|
|
+ parts.add("可确认最晚于 " + context.experience.getStartTime() + " 已在前司任职");
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(resolveLatestJobTitle(context))) {
|
|
|
+ parts.add("岗位为" + resolveLatestJobTitle(context));
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(resolveWorkContent(context))) {
|
|
|
+ parts.add("工作职责包括:" + resolveWorkContent(context));
|
|
|
+ }
|
|
|
+ return String.join(";", parts);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildAbilityLine(String name, BigDecimal rate, String remark) {
|
|
|
+ if (StringUtils.isBlank(name) && rate == null && StringUtils.isBlank(remark)) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ sb.append(safe(name));
|
|
|
+ if (rate != null) {
|
|
|
+ sb.append("(").append(rate.stripTrailingZeros().toPlainString()).append("分)");
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(remark)) {
|
|
|
+ sb.append(":").append(remark);
|
|
|
+ }
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String safe(String value) {
|
|
|
+ return value == null ? "" : value;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String sanitizeFileName(String value, String fallback) {
|
|
|
+ String source = StringUtils.isBlank(value) ? fallback : value;
|
|
|
+ return source.replaceAll("[\\\\/:*?\"<>|\\s]+", "_");
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class ReportContext {
|
|
|
+ private MainStudent student;
|
|
|
+ private MainPostCandidateReview review;
|
|
|
+ private MainStudentEducation education;
|
|
|
+ private MainStudentExperience experience;
|
|
|
+ private MainPosition position;
|
|
|
+ private MainBackRecord record;
|
|
|
+ private MainBackOrder backOrder;
|
|
|
+ private String companyName;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class QaLine {
|
|
|
+ private final String question;
|
|
|
+ private final String answer;
|
|
|
+
|
|
|
+ private QaLine(String question, String answer) {
|
|
|
+ this.question = question;
|
|
|
+ this.answer = answer;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class ReportWriter {
|
|
|
+ private final PDDocument document;
|
|
|
+ private final PDType0Font font;
|
|
|
+ private PDPage page;
|
|
|
+ private PDPageContentStream stream;
|
|
|
+ private float y;
|
|
|
+
|
|
|
+ private ReportWriter(PDDocument document, PDType0Font font) throws IOException {
|
|
|
+ this.document = document;
|
|
|
+ this.font = font;
|
|
|
+ newPage();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void addTitle(String title) throws IOException {
|
|
|
+ writeWrapped(title, REPORT_TITLE_SIZE, true, 1.2F, 10F);
|
|
|
+ y -= 8F;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void addKeyValueSection(String heading, List<String> lines) throws IOException {
|
|
|
+ addHeading(heading);
|
|
|
+ for (String line : lines) {
|
|
|
+ writeWrapped(line, REPORT_BODY_SIZE, false, 1.5F, 4F);
|
|
|
+ }
|
|
|
+ y -= 4F;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void addParagraphSection(String heading, String content) throws IOException {
|
|
|
+ addHeading(heading);
|
|
|
+ writeWrapped(StringUtils.isBlank(content) ? "" : content, REPORT_BODY_SIZE, false, 1.6F, 6F);
|
|
|
+ y -= 4F;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void addRatingSection(String heading, List<String> lines) throws IOException {
|
|
|
+ addHeading(heading);
|
|
|
+ for (String line : lines) {
|
|
|
+ if (StringUtils.isBlank(line)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ writeWrapped("• " + line, REPORT_BODY_SIZE, false, 1.6F, 4F);
|
|
|
+ }
|
|
|
+ y -= 4F;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void addQuestionAnswerSection(String heading, List<QaLine> lines) throws IOException {
|
|
|
+ addHeading(heading);
|
|
|
+ for (QaLine line : lines) {
|
|
|
+ writeWrapped("问:" + safeLine(line.question), REPORT_BODY_SIZE, true, 1.5F, 2F);
|
|
|
+ writeWrapped("答:" + safeLine(line.answer), REPORT_BODY_SIZE, false, 1.6F, 6F);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void addHeading(String heading) throws IOException {
|
|
|
+ ensureSpace(REPORT_HEADING_SIZE * 2.2F);
|
|
|
+ writeWrapped(heading, REPORT_HEADING_SIZE, true, 1.3F, 6F);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void writeWrapped(String text, float fontSize, boolean bold, float lineFactor, float bottomGap) throws IOException {
|
|
|
+ List<String> lines = wrapText(safeLine(text), fontSize);
|
|
|
+ if (lines.isEmpty()) {
|
|
|
+ lines = List.of("");
|
|
|
+ }
|
|
|
+ float lineHeight = fontSize * lineFactor;
|
|
|
+ ensureSpace(lineHeight * lines.size() + bottomGap);
|
|
|
+ for (String line : lines) {
|
|
|
+ stream.beginText();
|
|
|
+ stream.setFont(font, fontSize);
|
|
|
+ stream.newLineAtOffset(REPORT_MARGIN, y);
|
|
|
+ stream.showText(line);
|
|
|
+ stream.endText();
|
|
|
+ y -= lineHeight;
|
|
|
+ }
|
|
|
+ y -= bottomGap;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<String> wrapText(String text, float fontSize) throws IOException {
|
|
|
+ float width = PDRectangle.A4.getWidth() - REPORT_MARGIN * 2;
|
|
|
+ List<String> lines = new ArrayList<>();
|
|
|
+ if (StringUtils.isBlank(text)) {
|
|
|
+ lines.add("");
|
|
|
+ return lines;
|
|
|
+ }
|
|
|
+ StringBuilder current = new StringBuilder();
|
|
|
+ for (char ch : text.toCharArray()) {
|
|
|
+ if (ch == '\n') {
|
|
|
+ lines.add(current.toString());
|
|
|
+ current.setLength(0);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String candidate = current.toString() + ch;
|
|
|
+ float candidateWidth = font.getStringWidth(candidate) / 1000 * fontSize;
|
|
|
+ if (candidateWidth > width && current.length() > 0) {
|
|
|
+ lines.add(current.toString());
|
|
|
+ current.setLength(0);
|
|
|
+ }
|
|
|
+ current.append(ch);
|
|
|
+ }
|
|
|
+ if (!current.isEmpty()) {
|
|
|
+ lines.add(current.toString());
|
|
|
+ }
|
|
|
+ return lines;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ensureSpace(float neededHeight) throws IOException {
|
|
|
+ if (y - neededHeight < REPORT_MARGIN) {
|
|
|
+ newPage();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void newPage() throws IOException {
|
|
|
+ closeCurrentStream();
|
|
|
+ page = new PDPage(PDRectangle.A4);
|
|
|
+ document.addPage(page);
|
|
|
+ stream = new PDPageContentStream(document, page);
|
|
|
+ y = PDRectangle.A4.getHeight() - REPORT_MARGIN;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void closeCurrentStream() throws IOException {
|
|
|
+ if (stream != null) {
|
|
|
+ stream.close();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void close() throws IOException {
|
|
|
+ closeCurrentStream();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String safeLine(String value) {
|
|
|
+ return value == null ? "" : value.replace('\t', ' ');
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
|