package com.mydreamjob.job; import com.mydreamjob.common.api.ApiResponse; import com.mydreamjob.common.exception.BusinessException; import com.mydreamjob.common.exception.ErrorCode; import com.mydreamjob.company.Company; import com.mydreamjob.company.CompanyService; import com.mydreamjob.job.dto.JobCreateRequest; import com.mydreamjob.job.dto.JobSearchItem; import com.mydreamjob.job.dto.JobStatusRequest; import com.mydreamjob.job.dto.JobUpdateRequest; import com.mydreamjob.security.CurrentUserAccessor; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; /** * 岗位管理接口 * 企业端:创建、编辑、状态流转(需 JWT + 企业归属校验) * 公开端:岗位列表、详情(无需认证) */ @Tag(name = "岗位", description = "岗位创建、管理、查询") @RestController @RequestMapping("/jobs") @RequiredArgsConstructor public class JobController { /** R01-A6:requestId 只来自过滤器规范化后的唯一产出点。 */ private final com.mydreamjob.common.logging.RequestIdAccessor requestIdAccessor; private final JobService jobService; private final CompanyService companyService; private final CurrentUserAccessor currentUserAccessor; // ── 企业端:创建岗位 ──────────────────────────────────────── @Operation(summary = "创建岗位", description = "仅企业账号可调用,新岗位默认 DRAFT 状态") @PostMapping public ResponseEntity>> createJob( @Valid @RequestBody JobCreateRequest req, HttpServletRequest request) { Long companyId = resolveCompanyId(); Map view = jobService.toView(jobService.createJob(companyId, req)); return ResponseEntity.ok(ApiResponse.ok(view, requestIdAccessor.get(request))); } // ── 企业端:编辑岗位 ──────────────────────────────────────── @Operation(summary = "更新岗位", description = "仅岗位所属企业可修改") @PutMapping("/{jobId}") public ResponseEntity>> updateJob( @PathVariable Long jobId, @Valid @RequestBody JobUpdateRequest req, HttpServletRequest request) { Long companyId = resolveCompanyId(); Map view = jobService.toView(jobService.updateJob(companyId, jobId, req)); return ResponseEntity.ok(ApiResponse.ok(view, requestIdAccessor.get(request))); } // ── 企业端:状态流转 ──────────────────────────────────────── @Operation(summary = "发布/关闭岗位", description = "DRAFT→PUBLISHED,PUBLISHED→CLOSED;禁止非法回退") @PatchMapping("/{jobId}/status") public ResponseEntity>> updateStatus( @PathVariable Long jobId, @Valid @RequestBody JobStatusRequest req, HttpServletRequest request) { Long companyId = resolveCompanyId(); Map view = jobService.toView(jobService.transitionStatus(companyId, jobId, req)); return ResponseEntity.ok(ApiResponse.ok(view, requestIdAccessor.get(request))); } // ── 企业端:查询岗位详情 ──────────────────────────────────── @Operation(summary = "查询岗位详情", description = "企业可查看自己管理的岗位详情") @GetMapping("/{jobId}") public ResponseEntity>> getJob( @PathVariable Long jobId, HttpServletRequest request) { Long companyId = resolveCompanyId(); Map view = jobService.toView(jobService.getCompanyJob(companyId, jobId)); return ResponseEntity.ok(ApiResponse.ok(view, requestIdAccessor.get(request))); } // ── 公开端:岗位列表 ──────────────────────────────────────── @Operation(summary = "查询已发布岗位列表", description = "仅返回 PUBLISHED 岗位,支持关键词、城市、用工类型、薪资筛选和分页") @GetMapping public ResponseEntity>> listJobs( @RequestParam(required = false) String keyword, @RequestParam(required = false) String city, @RequestParam(required = false) String employmentType, @RequestParam(required = false) Integer salaryMin, @RequestParam(required = false) Integer salaryMax, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size, HttpServletRequest request) { if (size > 50) size = 50; Page> result = jobService.listPublishedJobs(keyword, city, employmentType, salaryMin, salaryMax, page, size); Map data = new HashMap<>(); data.put("items", result.getContent()); data.put("total", result.getTotalElements()); data.put("page", result.getNumber()); data.put("size", result.getSize()); data.put("totalPages", result.getTotalPages()); return ResponseEntity.ok(ApiResponse.ok(data, requestIdAccessor.get(request))); } // ── 公开端:岗位详情 ──────────────────────────────────────── @Operation(summary = "查询岗位详情(公开)", description = "仅返回 PUBLISHED 岗位详情;CLOSED 也可查看详情") @GetMapping("/public/{jobId}") public ResponseEntity>> getPublicJob( @PathVariable Long jobId, HttpServletRequest request) { Map view = jobService.getPublicJob(jobId); return ResponseEntity.ok(ApiResponse.ok(view, requestIdAccessor.get(request))); } // ── 公开端:高级搜索 ──────────────────────────────────────── @Operation(summary = "岗位高级搜索", description = "仅返回 PUBLISHED 岗位,支持关键词、城市、用工类型、薪资、公司筛选、排序和分页") @GetMapping("/search") public ResponseEntity>> searchJobs( @RequestParam(required = false) String keyword, @RequestParam(required = false) String city, @RequestParam(required = false) String employmentType, @RequestParam(required = false) Integer salaryMin, @RequestParam(required = false) Integer salaryMax, @RequestParam(required = false) Long companyId, @RequestParam(required = false) String sort, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size, HttpServletRequest request) { Page result = jobService.searchJobs( keyword, city, employmentType, salaryMin, salaryMax, companyId, sort, page, size); Map data = new HashMap<>(); data.put("items", result.getContent()); data.put("total", result.getTotalElements()); data.put("page", result.getNumber()); data.put("size", result.getSize()); data.put("totalPages", result.getTotalPages()); return ResponseEntity.ok(ApiResponse.ok(data, requestIdAccessor.get(request))); } // ── 公开端:筛选选项 ──────────────────────────────────────── @Operation(summary = "岗位筛选选项", description = "返回当前 PUBLISHED 岗位可用的城市、用工类型和薪资范围") @GetMapping("/filters") public ResponseEntity>> getFilters( HttpServletRequest request) { Map options = jobService.getFilterOptions(); return ResponseEntity.ok(ApiResponse.ok(options, requestIdAccessor.get(request))); } // ── 辅助方法 ──────────────────────────────────────────────── private Long resolveCompanyId() { // 身份只来自 JWT/SecurityContext;非法主体统一为未登录语义(401),不抛 NumberFormatException Long userId = currentUserAccessor.requireUserId(); Company company = companyService.findCompanyByOwnerUserId(userId); if (company == null) { throw new BusinessException(ErrorCode.COMPANY_NOT_FOUND); } return company.getId(); } }