package com.mydreamjob.onlineresume; import com.fasterxml.jackson.databind.*; import com.mydreamjob.common.api.ApiResponse; import com.mydreamjob.common.exception.*; import com.mydreamjob.common.logging.RequestIdAccessor; import com.mydreamjob.security.CurrentUserAccessor; import io.swagger.v3.oas.annotations.*; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import jakarta.validation.constraints.*; import lombok.RequiredArgsConstructor; import org.springframework.http.*; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.nio.charset.StandardCharsets; @RestController @RequiredArgsConstructor @Validated @RequestMapping("/profile/me/online-resume") @Tag(name = "在线简历", description = "仅当前 STUDENT 本人;不修改账号登录信息,不读取附件正文") public class OnlineResumeController { private final OnlineResumeService service; private final OnlineResumeDocxWriter docx; private final CurrentUserAccessor currentUser; private final RequestIdAccessor requestIds; private final ObjectMapper mapper; @Schema(name = "OnlineResumeEntryRequest") @lombok.Data public static class EntryRequest { @NotNull @PositiveOrZero private Long revision; @NotNull @Schema(implementation = Object.class, oneOf = {OnlineResumeContent.Education.class, OnlineResumeContent.Work.class, OnlineResumeContent.Project.class, OnlineResumeContent.Skill.class}) private JsonNode entry; } @GetMapping @Operation(operationId = "getMyOnlineResume", summary = "读取本人完整在线简历", description = "无保存记录时 revision=0,从已有资料、求职偏好和账号联系方式组装,不写库。attachment 仅返回文件元数据及鉴权下载地址。phone/email 为本人简历联系方式,不下发到企业候选人接口。") public ApiResponse get(HttpServletRequest request) { return ApiResponse.ok(service.get(currentUser.requireUserId()), requestIds.get(request)); } @PutMapping @Operation(operationId = "saveMyOnlineResume", summary = "保存完整在线简历", description = "携带最近 GET 的 revision;content 完整替换,空数组删除该类全部条目,null/空字符串可清空可选字段。新条目省略 id,既有条目保留 id。成功 revision+1;过期 revision 返回409/6002。同步既有个人资料和求职偏好,不改登录手机号/邮箱、附件及隐私授权。") public ApiResponse save(@Valid @RequestBody OnlineResumeSaveRequest body, HttpServletRequest request) { return ApiResponse.ok(service.save(currentUser.requireUserId(), body), requestIds.get(request)); } @GetMapping("/{section}") @Operation(operationId = "listMyOnlineResumeEntries", summary = "读取某类简历条目", description = "section 仅 education-experiences/work-experiences/project-experiences/skills;revision 从整份简历 GET 获取。") public ApiResponse> list(@PathVariable String section, HttpServletRequest request) { java.util.List result = new java.util.ArrayList<>(); entries(service.get(currentUser.requireUserId()), section).forEach(result::add); return ApiResponse.ok(result, requestIds.get(request)); } @GetMapping("/{section}/{id}") @Operation(operationId = "getMyOnlineResumeEntry", summary = "读取本人简历条目详情", description = "只在本人指定类别内寻找条目,不存在返回404/6003。") public ApiResponse detail(@PathVariable String section, @PathVariable String id, HttpServletRequest request) { for (JsonNode e : entries(service.get(currentUser.requireUserId()), section)) if (id.equals(e.path("id").asText())) return ApiResponse.ok(e, requestIds.get(request)); throw new BusinessException(ErrorCode.ONLINE_RESUME_ENTRY_NOT_FOUND); } @PostMapping("/{section}") @Operation(operationId = "createMyOnlineResumeEntry", summary = "新增简历条目", description = "section 仅 education-experiences/work-experiences/project-experiences/skills;entry 为相应类型,新条目不传 id;携带整份简历 revision,返回更新后的完整简历。") public ApiResponse create(@PathVariable String section, @Valid @RequestBody EntryRequest body, HttpServletRequest request) { return ApiResponse.ok(service.editEntry(currentUser.requireUserId(), section, null, body.getRevision(), body.getEntry(), false), requestIds.get(request)); } @PutMapping("/{section}/{id}") @Operation(operationId = "updateMyOnlineResumeEntry", summary = "替换简历条目", description = "entry 完整替换本人条目;id 可省略或须与 URL 一致;携带整份简历 revision。") public ApiResponse update(@PathVariable String section, @PathVariable String id, @Valid @RequestBody EntryRequest body, HttpServletRequest request) { return ApiResponse.ok(service.editEntry(currentUser.requireUserId(), section, id, body.getRevision(), body.getEntry(), false), requestIds.get(request)); } @DeleteMapping("/{section}/{id}") @Operation(operationId = "deleteMyOnlineResumeEntry", summary = "删除简历条目", description = "query revision 必填;不存在返回404/6003,过期版本返回409/6002;返回更新后的完整简历。") public ApiResponse delete(@PathVariable String section, @PathVariable String id, @RequestParam @Min(0) Long revision, HttpServletRequest request) { return ApiResponse.ok(service.editEntry(currentUser.requireUserId(), section, id, revision, null, true), requestIds.get(request)); } @GetMapping(value = "/download", produces = "application/vnd.openxmlformats-officedocument.wordprocessingml.document") @Operation(operationId = "downloadMyOnlineResume", summary = "下载在线简历 DOCX", description = "从当前已保存的在线内容生成 Word 文件,不含上传附件,不写文件、不创建附件版本;浏览器应携带鉴权获取 Blob,未保存编辑需先 PUT。") public ResponseEntity download() { byte[] bytes = docx.write(service.get(currentUser.requireUserId()).content()); return ResponseEntity.ok().cacheControl(CacheControl.noStore()) .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment().filename("个人简历.docx", StandardCharsets.UTF_8).build().toString()) .contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.wordprocessingml.document")) .contentLength(bytes.length).body(bytes); } private JsonNode entries(OnlineResumeView view, String section) { return mapper.valueToTree(switch (section) { case "education-experiences" -> view.content().getEducationExperiences(); case "work-experiences" -> view.content().getWorkExperiences(); case "project-experiences" -> view.content().getProjectExperiences(); case "skills" -> view.content().getSkills(); default -> throw new BusinessException(ErrorCode.ONLINE_RESUME_ENTRY_NOT_FOUND); }); } }