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
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");
|
|
}
|
|
}
|
|
|