package com.mydreamjob.onlineresume; import org.springframework.stereotype.Component; import java.io.*; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.zip.*; /** 生成真正的 OOXML 文档;用户文本只作为文字写入,无外链、宏或文件读取。 */ @Component public class OnlineResumeDocxWriter { public byte[] write(OnlineResumeContent c) { List lines = new ArrayList<>(); var b = c.getBasicInfo(); lines.add(b.getRealName() == null || b.getRealName().isBlank() ? "个人简历" : b.getRealName() + "的简历"); lines.add(join(b.getPhone(), b.getEmail(), b.getLocation())); lines.add(join(b.getEducation(), b.getSchool(), b.getGraduationYear())); var j = c.getJobIntention(); lines.add("求职意向"); lines.add(join(j.getTargetPosition(), String.join("、", j.getPreferredCities()))); if (j.getExpectedSalaryMin() != null || j.getExpectedSalaryMax() != null) lines.add("期望月薪(元):" + Objects.toString(j.getExpectedSalaryMin(), "不限") + "—" + Objects.toString(j.getExpectedSalaryMax(), "不限")); lines.add(join(String.join("、", j.getEmploymentTypes()), String.join("、", j.getPreferredIndustries()), String.join("、", j.getPreferredJobTypes()))); if (j.getJobSeekingStatus() != null) lines.add("求职状态:" + switch (j.getJobSeekingStatus()) { case LOOKING -> "正在求职"; case OPEN_TO_OPPORTUNITIES -> "考虑机会"; case NOT_LOOKING -> "暂不求职"; case STUDENT -> "在校学生"; }); if (j.getAvailableFrom() != null) lines.add("可到岗日期:" + j.getAvailableFrom()); if (j.getEmploymentStatus() != null) lines.add("当前状态:" + switch (j.getEmploymentStatus()) { case EMPLOYED -> "在职"; case UNEMPLOYED -> "离职/未就业"; case STUDENT -> "在校"; }); if (j.isRemoteAllowed()) lines.add("接受远程工作"); lines.add("教育经历"); for (var e : c.getEducationExperiences()) { lines.add(join(e.getSchool(), e.getMajor(), e.getDegree(), dates(e))); lines.add(e.getDescription()); } lines.add("工作经历"); for (var e : c.getWorkExperiences()) { String type = "INTERNSHIP".equals(e.getExperienceType()) ? "实习" : "FULL_TIME".equals(e.getExperienceType()) ? "正式工作" : "类型未区分"; lines.add(join(e.getCompany(), e.getPosition(), type, dates(e))); lines.add(e.getDescription()); } lines.add("项目经历"); for (var e : c.getProjectExperiences()) { lines.add(join(e.getName(), e.getRole(), dates(e))); lines.add(e.getDescription()); } lines.add("技能特长"); for (var e : c.getSkills()) { String level = e.getLevel() == null ? null : switch (e.getLevel()) { case BEGINNER -> "入门"; case FAMILIAR -> "熟悉"; case PROFICIENT -> "熟练"; case EXPERT -> "精通"; }; lines.add(join(e.getName(), level, e.getYears() == null ? null : e.getYears() + "年")); lines.add(e.getDescription()); } lines.add("自我评价"); lines.add(c.getSelfEvaluation()); StringBuilder body = new StringBuilder(); for (String text : lines) { if (text == null || text.isBlank()) continue; for (String line : text.split("\\R", -1)) body.append("") .append(xml(line)).append(""); } body.append(""); try (var out = new ByteArrayOutputStream(); var zip = new ZipOutputStream(out, StandardCharsets.UTF_8)) { add(zip, "[Content_Types].xml", ""); add(zip, "_rels/.rels", ""); add(zip, "word/document.xml", "" + body + ""); zip.finish(); return out.toByteArray(); } catch (IOException e) { throw new IllegalStateException("cannot generate resume document", e); } } private String dates(OnlineResumeContent.DatedEntry e) { return Objects.toString(e.getStartDate(), "") + "—" + (e.isOngoing() ? "至今" : Objects.toString(e.getEndDate(), "")); } private String join(Object... fields) { return Arrays.stream(fields).filter(Objects::nonNull).map(Object::toString).filter(s -> !s.isBlank()).collect(java.util.stream.Collectors.joining(" · ")); } private String xml(String s) { StringBuilder clean = new StringBuilder(); s.codePoints().filter(c -> c == 9 || c == 10 || c == 13 || c >= 32 && c <= 0xd7ff || c >= 0xe000 && c <= 0xfffd || c >= 0x10000 && c <= 0x10ffff).forEach(clean::appendCodePoint); return clean.toString().replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """).replace("'", "'"); } private void add(ZipOutputStream zip, String name, String text) throws IOException { zip.putNextEntry(new ZipEntry(name)); zip.write(text.getBytes(StandardCharsets.UTF_8)); zip.closeEntry(); } }