You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

207 lines
12 KiB

package com.mydreamjob.skill;
import com.mydreamjob.common.api.ApiResponse;
import com.mydreamjob.common.exception.BusinessException;
import com.mydreamjob.common.exception.ErrorCode;
import com.mydreamjob.profile.ProfileService;
import com.mydreamjob.skill.dto.SkillCloudResponse;
import com.mydreamjob.skill.dto.SkillLearningPathResponse;
import com.mydreamjob.skill.dto.SkillMatchResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.constraints.Positive;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 行业技能词云、个人匹配度与学习路径接口。
*/
@Tag(name = "行业技能词云", description = "按行业、职位和技能分类查询热门技能、个人匹配度与学习路径")
@RestController
@Validated
@RequiredArgsConstructor
public class SkillCloudController {
private final SkillCloudQueryService skillCloudQueryService;
private final SkillMatchService skillMatchService;
private final SkillLearningPathService skillLearningPathService;
private final ProfileService profileService;
@Operation(
operationId = "getWordCloud",
summary = "查询行业技能词云",
description = "返回当前筛选范围内的全部技能,按热度降序排列;前端取前十条作为 TOP10。仅 STUDENT 可访问。"
)
@SecurityRequirement(name = "bearerAuth")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "成功返回筛选范围内的全部技能",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
name = "success",
value = "{\"code\":200,\"message\":\"ok\",\"data\":{\"industry\":{\"id\":1,\"code\":\"INTERNET\",\"name\":\"互联网\"},\"occupation\":{\"id\":1,\"code\":\"FRONTEND_DEVELOPER\",\"name\":\"前端开发工程师\"},\"category\":null,\"total\":1,\"items\":[{\"rank\":1,\"skillId\":26,\"skillCode\":\"REACT\",\"nameZh\":\"React\",\"nameEn\":\"React\",\"sourceType\":\"O*NET\",\"sourceCode\":\"15-1254.00|React|2.E.7.h\",\"categoryId\":103,\"categoryName\":\"前端框架\",\"heat\":4.38,\"heatSourceCode\":\"15-1254.00\",\"heatElementId\":\"2.E.7.h\",\"heatProxy\":true}]}}"
)
)
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "400", description = "筛选参数缺失、非正整数或节点关系非法",
content = @Content(schema = @Schema(implementation = ApiResponse.class))
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "401", description = "未登录",
content = @Content(schema = @Schema(implementation = ApiResponse.class))
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "403", description = "已登录但不是 STUDENT 角色",
content = @Content(schema = @Schema(implementation = ApiResponse.class))
)
})
@GetMapping("/skills/word-cloud")
public ResponseEntity<ApiResponse<SkillCloudResponse>> getWordCloud(
@Parameter(description = "行业 ID,必填正整数", required = true, example = "1")
@Positive(message = "industryId must be positive")
@RequestParam Long industryId,
@Parameter(description = "职位 ID,必填正整数", required = true, example = "1")
@Positive(message = "occupationId must be positive")
@RequestParam Long occupationId,
@Parameter(description = "技能分类 ID,可选正整数;省略时返回职位下全部分类", required = false, example = "101")
@Positive(message = "categoryId must be positive")
@RequestParam(required = false) Long categoryId,
HttpServletRequest request) {
Long userId = resolveAuthenticatedStudent();
SkillCloudResponse data = skillCloudQueryService.query(industryId, occupationId, categoryId);
return ResponseEntity.ok(ApiResponse.ok(data, getRequestId(request)));
}
@Operation(
operationId = "getSkillMatch",
summary = "查询个人技能匹配度",
description = "使用当前 STUDENT 的个人技能,与筛选结果热度 TOP10 比对并返回命中、缺失和匹配度。仅 STUDENT 可访问。"
)
@SecurityRequirement(name = "bearerAuth")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "成功返回个人技能匹配结果",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
name = "success",
value = "{\"code\":200,\"message\":\"ok\",\"data\":{\"matchScore\":50,\"matchedCount\":1,\"totalCount\":2,\"matchedSkills\":[{\"rank\":1,\"skillId\":26,\"skillCode\":\"REACT\",\"nameZh\":\"React\",\"nameEn\":\"React\",\"sourceType\":\"O*NET\",\"sourceCode\":\"15-1254.00|React|2.E.7.h\",\"categoryId\":103,\"categoryName\":\"前端框架\",\"heat\":4.38,\"heatSourceCode\":\"15-1254.00\",\"heatElementId\":\"2.E.7.h\",\"heatProxy\":true}],\"missingSkills\":[{\"rank\":2,\"skillId\":31,\"skillCode\":\"DOCKER\",\"nameZh\":\"Docker\",\"nameEn\":\"Docker\",\"sourceType\":\"O*NET\",\"sourceCode\":\"15-1299.08|Docker|2.E.7.h\",\"categoryId\":105,\"categoryName\":\"工程化工具\",\"heat\":4.12,\"heatSourceCode\":\"15-1299.08\",\"heatElementId\":\"2.E.7.h\",\"heatProxy\":true}]}}"
)
)
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "400", description = "筛选参数缺失、非正整数或节点关系非法",
content = @Content(schema = @Schema(implementation = ApiResponse.class))
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "401", description = "未登录",
content = @Content(schema = @Schema(implementation = ApiResponse.class))
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "403", description = "已登录但不是 STUDENT 角色",
content = @Content(schema = @Schema(implementation = ApiResponse.class))
)
})
@GetMapping("/skills/match")
public ResponseEntity<ApiResponse<SkillMatchResponse>> getMatch(
@Parameter(description = "行业 ID,必填正整数", required = true, example = "1")
@Positive(message = "industryId must be positive")
@RequestParam Long industryId,
@Parameter(description = "职位 ID,必填正整数", required = true, example = "1")
@Positive(message = "occupationId must be positive")
@RequestParam Long occupationId,
@Parameter(description = "技能分类 ID,可选正整数;省略时使用职位下全部技能", required = false, example = "101")
@Positive(message = "categoryId must be positive")
@RequestParam(required = false) Long categoryId,
HttpServletRequest request) {
Long userId = resolveAuthenticatedStudent();
SkillMatchResponse data = skillMatchService.match(userId, industryId, occupationId, categoryId);
return ResponseEntity.ok(ApiResponse.ok(data, getRequestId(request)));
}
@Operation(
operationId = "getLearningPath",
summary = "生成个人学习路径",
description = "仅针对个人 TOP10 中未命中的技能,按热度顺序返回优先级和通用学习建议。仅 STUDENT 可访问。"
)
@SecurityRequirement(name = "bearerAuth")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "成功返回缺失技能的学习路径;没有缺口时列表为空",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
name = "success",
value = "{\"code\":200,\"message\":\"ok\",\"data\":{\"total\":1,\"items\":[{\"priority\":1,\"skillId\":26,\"skillName\":\"React\",\"categoryId\":103,\"categoryName\":\"前端框架\",\"suggestion\":\"先理解 React 的核心概念与生命周期,再通过一个小型项目练习集成。\"}]}}"
)
)
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "400", description = "筛选参数缺失、非正整数或节点关系非法",
content = @Content(schema = @Schema(implementation = ApiResponse.class))
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "401", description = "未登录",
content = @Content(schema = @Schema(implementation = ApiResponse.class))
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "403", description = "已登录但不是 STUDENT 角色",
content = @Content(schema = @Schema(implementation = ApiResponse.class))
)
})
@GetMapping("/skills/learning-path")
public ResponseEntity<ApiResponse<SkillLearningPathResponse>> getLearningPath(
@Parameter(description = "行业 ID,必填正整数", required = true, example = "1")
@Positive(message = "industryId must be positive")
@RequestParam Long industryId,
@Parameter(description = "职位 ID,必填正整数", required = true, example = "1")
@Positive(message = "occupationId must be positive")
@RequestParam Long occupationId,
@Parameter(description = "技能分类 ID,可选正整数;省略时使用职位下全部技能", required = false, example = "101")
@Positive(message = "categoryId must be positive")
@RequestParam(required = false) Long categoryId,
HttpServletRequest request) {
Long userId = resolveAuthenticatedStudent();
SkillLearningPathResponse data = skillLearningPathService.generate(
userId, industryId, occupationId, categoryId);
return ResponseEntity.ok(ApiResponse.ok(data, getRequestId(request)));
}
private Long resolveAuthenticatedStudent() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null
|| !authentication.isAuthenticated()
|| "anonymousUser".equals(authentication.getName())) {
throw new BusinessException(ErrorCode.UNAUTHORIZED);
}
try {
Long userId = Long.valueOf(authentication.getName());
profileService.assertStudentRole(userId);
return userId;
} catch (NumberFormatException exception) {
throw new BusinessException(ErrorCode.UNAUTHORIZED);
}
}
private String getRequestId(HttpServletRequest request) {
return request.getHeader("X-Request-Id");
}
}