From 0c778249cc1aae4102ebe2915506497ba102600f Mon Sep 17 00:00:00 2001 From: pwklppg <123456@.com> Date: Tue, 15 Sep 2026 09:26:35 +0800 Subject: [PATCH] feat: add skill taxonomy repositories --- .../skill/domain/OccupationSkill.java | 66 +++++ .../com/mydreamjob/skill/domain/Skill.java | 56 +++++ .../mydreamjob/skill/domain/SkillAlias.java | 41 +++ .../skill/domain/SkillCategory.java | 50 ++++ .../skill/domain/SkillIndustry.java | 47 ++++ .../skill/domain/SkillOccupation.java | 56 +++++ .../skill/domain/SkillSourceVersion.java | 50 ++++ .../skill/dto/SkillCloudSkillProjection.java | 21 ++ .../repository/OccupationSkillRepository.java | 71 ++++++ .../repository/SkillAliasRepository.java | 15 ++ .../repository/SkillCategoryRepository.java | 13 + .../repository/SkillIndustryRepository.java | 7 + .../repository/SkillOccupationRepository.java | 13 + .../skill/repository/SkillRepository.java | 15 ++ .../SkillSourceVersionRepository.java | 7 + .../skill/SkillTaxonomyRepositoryTest.java | 234 ++++++++++++++++++ 16 files changed, 762 insertions(+) create mode 100644 mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/OccupationSkill.java create mode 100644 mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/Skill.java create mode 100644 mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillAlias.java create mode 100644 mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillCategory.java create mode 100644 mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillIndustry.java create mode 100644 mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillOccupation.java create mode 100644 mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillSourceVersion.java create mode 100644 mydreamjob-server/src/main/java/com/mydreamjob/skill/dto/SkillCloudSkillProjection.java create mode 100644 mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/OccupationSkillRepository.java create mode 100644 mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillAliasRepository.java create mode 100644 mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillCategoryRepository.java create mode 100644 mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillIndustryRepository.java create mode 100644 mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillOccupationRepository.java create mode 100644 mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillRepository.java create mode 100644 mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillSourceVersionRepository.java create mode 100644 mydreamjob-server/src/test/java/com/mydreamjob/skill/SkillTaxonomyRepositoryTest.java diff --git a/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/OccupationSkill.java b/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/OccupationSkill.java new file mode 100644 index 0000000..9b77bba --- /dev/null +++ b/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/OccupationSkill.java @@ -0,0 +1,66 @@ +package com.mydreamjob.skill.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "occupation_skills") +public class OccupationSkill { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "occupation_id", nullable = false) + private Long occupationId; + + @Column(name = "skill_id", nullable = false) + private Long skillId; + + @Column(name = "category_id", nullable = false) + private Long categoryId; + + @Column(name = "importance", precision = 4, scale = 2) + private BigDecimal importance; + + @Column(name = "importance_source_code", length = 20) + private String importanceSourceCode; + + @Column(name = "importance_element_id", length = 30) + private String importanceElementId; + + @JdbcTypeCode(SqlTypes.TINYINT) + @Column(name = "importance_is_proxy", nullable = false) + private Boolean importanceIsProxy; + + @Column(name = "relationship_type", nullable = false, length = 20) + private String relationshipType; + + @Column(name = "source_version_id", nullable = false) + private Long sourceVersionId; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; +} diff --git a/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/Skill.java b/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/Skill.java new file mode 100644 index 0000000..0d84071 --- /dev/null +++ b/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/Skill.java @@ -0,0 +1,56 @@ +package com.mydreamjob.skill.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "skills") +public class Skill { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "code", nullable = false, length = 120) + private String code; + + @Column(name = "name_zh", nullable = false, length = 180) + private String nameZh; + + @Column(name = "name_en", length = 180) + private String nameEn; + + @Column(name = "source_type", nullable = false, length = 20) + private String sourceType; + + @Column(name = "source_code", nullable = false, length = 180) + private String sourceCode; + + @Column(name = "source_version_id", nullable = false) + private Long sourceVersionId; + + @Column(name = "sort_order", nullable = false) + private Integer sortOrder; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; +} diff --git a/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillAlias.java b/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillAlias.java new file mode 100644 index 0000000..0d05f04 --- /dev/null +++ b/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillAlias.java @@ -0,0 +1,41 @@ +package com.mydreamjob.skill.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "skill_aliases") +public class SkillAlias { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "skill_id", nullable = false) + private Long skillId; + + @Column(name = "alias", nullable = false, length = 180) + private String alias; + + @Column(name = "normalized_alias", nullable = false, length = 180) + private String normalizedAlias; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; +} diff --git a/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillCategory.java b/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillCategory.java new file mode 100644 index 0000000..dd58b08 --- /dev/null +++ b/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillCategory.java @@ -0,0 +1,50 @@ +package com.mydreamjob.skill.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "skill_categories") +public class SkillCategory { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "occupation_id", nullable = false) + private Long occupationId; + + @Column(name = "code", nullable = false, length = 100) + private String code; + + @Column(name = "name_zh", nullable = false, length = 120) + private String nameZh; + + @Column(name = "suggestion_template_code", nullable = false, length = 30) + private String suggestionTemplateCode; + + @Column(name = "sort_order", nullable = false) + private Integer sortOrder; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; +} diff --git a/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillIndustry.java b/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillIndustry.java new file mode 100644 index 0000000..7fbd239 --- /dev/null +++ b/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillIndustry.java @@ -0,0 +1,47 @@ +package com.mydreamjob.skill.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "skill_industries") +public class SkillIndustry { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "code", nullable = false, length = 80) + private String code; + + @Column(name = "name_zh", nullable = false, length = 120) + private String nameZh; + + @Column(name = "source_code", length = 120) + private String sourceCode; + + @Column(name = "sort_order", nullable = false) + private Integer sortOrder; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; +} diff --git a/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillOccupation.java b/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillOccupation.java new file mode 100644 index 0000000..4a83dac --- /dev/null +++ b/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillOccupation.java @@ -0,0 +1,56 @@ +package com.mydreamjob.skill.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "skill_occupations") +public class SkillOccupation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "industry_id", nullable = false) + private Long industryId; + + @Column(name = "code", nullable = false, length = 100) + private String code; + + @Column(name = "name_zh", nullable = false, length = 150) + private String nameZh; + + @Column(name = "name_en", length = 180) + private String nameEn; + + @Column(name = "source_code", nullable = false, length = 150) + private String sourceCode; + + @Column(name = "source_version_id", nullable = false) + private Long sourceVersionId; + + @Column(name = "sort_order", nullable = false) + private Integer sortOrder; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; +} diff --git a/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillSourceVersion.java b/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillSourceVersion.java new file mode 100644 index 0000000..498cebd --- /dev/null +++ b/mydreamjob-server/src/main/java/com/mydreamjob/skill/domain/SkillSourceVersion.java @@ -0,0 +1,50 @@ +package com.mydreamjob.skill.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "skill_source_versions") +public class SkillSourceVersion { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "source_name", nullable = false, length = 100) + private String sourceName; + + @Column(name = "source_version", nullable = false, length = 150) + private String sourceVersion; + + @Column(name = "license", nullable = false, length = 255) + private String license; + + @Column(name = "attribution_url", nullable = false, length = 500) + private String attributionUrl; + + @Column(name = "imported_at", nullable = false) + private LocalDateTime importedAt; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; +} diff --git a/mydreamjob-server/src/main/java/com/mydreamjob/skill/dto/SkillCloudSkillProjection.java b/mydreamjob-server/src/main/java/com/mydreamjob/skill/dto/SkillCloudSkillProjection.java new file mode 100644 index 0000000..d2d3619 --- /dev/null +++ b/mydreamjob-server/src/main/java/com/mydreamjob/skill/dto/SkillCloudSkillProjection.java @@ -0,0 +1,21 @@ +package com.mydreamjob.skill.dto; + +import java.math.BigDecimal; + +public record SkillCloudSkillProjection( + Long skillId, + String skillCode, + String nameZh, + String nameEn, + String sourceType, + String sourceCode, + Long categoryId, + String categoryName, + BigDecimal importance, + String importanceSourceCode, + String importanceElementId, + Boolean importanceIsProxy, + Integer skillSortOrder, + Integer categorySortOrder +) { +} diff --git a/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/OccupationSkillRepository.java b/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/OccupationSkillRepository.java new file mode 100644 index 0000000..828fc02 --- /dev/null +++ b/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/OccupationSkillRepository.java @@ -0,0 +1,71 @@ +package com.mydreamjob.skill.repository; + +import com.mydreamjob.skill.domain.OccupationSkill; +import com.mydreamjob.skill.dto.SkillCloudSkillProjection; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +public interface OccupationSkillRepository extends JpaRepository { + + long countByOccupationId(Long occupationId); + + long countByOccupationIdAndCategoryId(Long occupationId, Long categoryId); + + @Transactional(readOnly = true) + @Query(""" + SELECT new com.mydreamjob.skill.dto.SkillCloudSkillProjection( + os.skillId, + s.code, + s.nameZh, + s.nameEn, + s.sourceType, + s.sourceCode, + c.id, + c.nameZh, + os.importance, + os.importanceSourceCode, + os.importanceElementId, + os.importanceIsProxy, + s.sortOrder, + c.sortOrder + ) + FROM OccupationSkill os, Skill s, SkillCategory c + WHERE os.skillId = s.id + AND os.categoryId = c.id + AND os.occupationId = :occupationId + """) + List findSkillProjectionsByOccupationId( + @Param("occupationId") Long occupationId); + + @Transactional(readOnly = true) + @Query(""" + SELECT new com.mydreamjob.skill.dto.SkillCloudSkillProjection( + os.skillId, + s.code, + s.nameZh, + s.nameEn, + s.sourceType, + s.sourceCode, + c.id, + c.nameZh, + os.importance, + os.importanceSourceCode, + os.importanceElementId, + os.importanceIsProxy, + s.sortOrder, + c.sortOrder + ) + FROM OccupationSkill os, Skill s, SkillCategory c + WHERE os.skillId = s.id + AND os.categoryId = c.id + AND os.occupationId = :occupationId + AND os.categoryId = :categoryId + """) + List findSkillProjectionsByOccupationIdAndCategoryId( + @Param("occupationId") Long occupationId, + @Param("categoryId") Long categoryId); +} diff --git a/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillAliasRepository.java b/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillAliasRepository.java new file mode 100644 index 0000000..f674913 --- /dev/null +++ b/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillAliasRepository.java @@ -0,0 +1,15 @@ +package com.mydreamjob.skill.repository; + +import com.mydreamjob.skill.domain.SkillAlias; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +public interface SkillAliasRepository extends JpaRepository { + + Optional findByNormalizedAlias(String normalizedAlias); + + List findAllBySkillIdInOrderByIdAsc(Collection skillIds); +} diff --git a/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillCategoryRepository.java b/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillCategoryRepository.java new file mode 100644 index 0000000..e83f478 --- /dev/null +++ b/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillCategoryRepository.java @@ -0,0 +1,13 @@ +package com.mydreamjob.skill.repository; + +import com.mydreamjob.skill.domain.SkillCategory; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface SkillCategoryRepository extends JpaRepository { + + boolean existsByIdAndOccupationId(Long id, Long occupationId); + + Optional findByIdAndOccupationId(Long id, Long occupationId); +} diff --git a/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillIndustryRepository.java b/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillIndustryRepository.java new file mode 100644 index 0000000..fc80d54 --- /dev/null +++ b/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillIndustryRepository.java @@ -0,0 +1,7 @@ +package com.mydreamjob.skill.repository; + +import com.mydreamjob.skill.domain.SkillIndustry; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SkillIndustryRepository extends JpaRepository { +} diff --git a/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillOccupationRepository.java b/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillOccupationRepository.java new file mode 100644 index 0000000..172b22c --- /dev/null +++ b/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillOccupationRepository.java @@ -0,0 +1,13 @@ +package com.mydreamjob.skill.repository; + +import com.mydreamjob.skill.domain.SkillOccupation; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface SkillOccupationRepository extends JpaRepository { + + boolean existsByIdAndIndustryId(Long id, Long industryId); + + Optional findByIdAndIndustryId(Long id, Long industryId); +} diff --git a/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillRepository.java b/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillRepository.java new file mode 100644 index 0000000..507f7a6 --- /dev/null +++ b/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillRepository.java @@ -0,0 +1,15 @@ +package com.mydreamjob.skill.repository; + +import com.mydreamjob.skill.domain.Skill; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +public interface SkillRepository extends JpaRepository { + + Optional findByCode(String code); + + List findAllByIdInOrderByIdAsc(Collection ids); +} diff --git a/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillSourceVersionRepository.java b/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillSourceVersionRepository.java new file mode 100644 index 0000000..a22a7a4 --- /dev/null +++ b/mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/SkillSourceVersionRepository.java @@ -0,0 +1,7 @@ +package com.mydreamjob.skill.repository; + +import com.mydreamjob.skill.domain.SkillSourceVersion; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SkillSourceVersionRepository extends JpaRepository { +} diff --git a/mydreamjob-server/src/test/java/com/mydreamjob/skill/SkillTaxonomyRepositoryTest.java b/mydreamjob-server/src/test/java/com/mydreamjob/skill/SkillTaxonomyRepositoryTest.java new file mode 100644 index 0000000..7f3dfe0 --- /dev/null +++ b/mydreamjob-server/src/test/java/com/mydreamjob/skill/SkillTaxonomyRepositoryTest.java @@ -0,0 +1,234 @@ +package com.mydreamjob.skill; + +import com.mydreamjob.skill.domain.Skill; +import com.mydreamjob.skill.domain.SkillAlias; +import com.mydreamjob.skill.dto.SkillCloudSkillProjection; +import com.mydreamjob.skill.repository.OccupationSkillRepository; +import com.mydreamjob.skill.repository.SkillAliasRepository; +import com.mydreamjob.skill.repository.SkillCategoryRepository; +import com.mydreamjob.skill.repository.SkillIndustryRepository; +import com.mydreamjob.skill.repository.SkillOccupationRepository; +import com.mydreamjob.skill.repository.SkillRepository; +import com.mydreamjob.skill.repository.SkillSourceVersionRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.hibernate.boot.model.relational.Namespace; +import org.hibernate.boot.model.relational.Sequence; +import org.hibernate.mapping.Table; +import org.hibernate.tool.schema.spi.SchemaFilter; +import org.hibernate.tool.schema.spi.SchemaFilterProvider; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@TestPropertySource(properties = { + "spring.datasource.url=jdbc:h2:mem:integrationdb_skill_repository;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE", + "spring.jpa.hibernate.ddl-auto=validate", + "spring.jpa.properties.hibernate.hbm2ddl.schema_filter_provider=com.mydreamjob.skill.SkillTaxonomyRepositoryTest$SkillTaxonomySchemaFilterProvider" +}) +@ActiveProfiles("integration-test") +class SkillTaxonomyRepositoryTest { + + @Autowired + private SkillSourceVersionRepository sourceVersionRepository; + + @Autowired + private SkillIndustryRepository industryRepository; + + @Autowired + private SkillOccupationRepository occupationRepository; + + @Autowired + private SkillCategoryRepository categoryRepository; + + @Autowired + private SkillRepository skillRepository; + + @Autowired + private SkillAliasRepository skillAliasRepository; + + @Autowired + private OccupationSkillRepository occupationSkillRepository; + + @MockBean + private RedisTemplate redisTemplate; + + @Test + void entityMappingsAreValidatedAndSeedCountsMatchV36() { + assertThat(sourceVersionRepository.count()).isEqualTo(2L); + assertThat(industryRepository.count()).isEqualTo(3L); + assertThat(occupationRepository.count()).isEqualTo(10L); + assertThat(categoryRepository.count()).isEqualTo(68L); + assertThat(skillRepository.count()).isEqualTo(72L); + assertThat(skillAliasRepository.count()).isEqualTo(72L); + assertThat(occupationSkillRepository.count()).isEqualTo(139L); + } + + @Test + void associationChecksAcceptOnlyConsistentTaxonomyPaths() { + assertThat(industryRepository.existsById(1L)).isTrue(); + assertThat(occupationRepository.existsByIdAndIndustryId(1L, 1L)).isTrue(); + assertThat(categoryRepository.existsByIdAndOccupationId(101L, 1L)).isTrue(); + + assertThat(occupationRepository.existsByIdAndIndustryId(1L, 2L)).isFalse(); + assertThat(categoryRepository.existsByIdAndOccupationId(101L, 2L)).isFalse(); + assertThat(occupationRepository.existsByIdAndIndustryId(999L, 1L)).isFalse(); + assertThat(categoryRepository.existsByIdAndOccupationId(999L, 1L)).isFalse(); + } + + @Test + void projectionUsesWholeOccupationWhenCategoryMissingAndFiltersWhenProvided() { + List occupationSkills = + occupationSkillRepository.findSkillProjectionsByOccupationId(1L); + + assertThat(occupationSkills).hasSize(15); + assertThat(occupationSkills) + .extracting(SkillCloudSkillProjection::skillId) + .containsExactlyInAnyOrder( + 3L, 4L, 7L, 8L, 26L, 27L, 28L, 35L, + 54L, 55L, 38L, 39L, 32L, 24L, 50L); + + List languageSkills = + occupationSkillRepository.findSkillProjectionsByOccupationIdAndCategoryId(1L, 101L); + + assertThat(languageSkills).hasSize(2); + assertThat(languageSkills) + .extracting(SkillCloudSkillProjection::skillId) + .containsExactlyInAnyOrder(3L, 4L); + assertThat(languageSkills) + .extracting(SkillCloudSkillProjection::categoryId) + .containsOnly(101L); + + assertThat(occupationSkillRepository + .findSkillProjectionsByOccupationIdAndCategoryId(2L, 101L)) + .isEmpty(); + } + + @Test + void projectionCarriesOnetProvenanceNullImportanceAndSortOrders() { + SkillCloudSkillProjection react = occupationSkillRepository + .findSkillProjectionsByOccupationIdAndCategoryId(1L, 103L) + .stream() + .filter(item -> item.skillId().equals(26L)) + .findFirst() + .orElseThrow(); + + assertThat(react.skillCode()).isEqualTo("REACT"); + assertThat(react.nameZh()).isEqualTo("React"); + assertThat(react.nameEn()).isEqualTo("React"); + assertThat(react.sourceType()).isEqualTo("O*NET"); + assertThat(react.sourceCode()).isEqualTo("15-1254.00|React|2.E.7.h"); + assertThat(react.categoryName()).isEqualTo("前端框架"); + assertThat(react.importance()).isEqualByComparingTo(new BigDecimal("4.88")); + assertThat(react.importanceSourceCode()).isEqualTo("15-1254.00"); + assertThat(react.importanceElementId()).isEqualTo("4.A.3.b.1"); + assertThat(react.importanceIsProxy()).isTrue(); + assertThat(react.skillSortOrder()).isEqualTo(26); + assertThat(react.categorySortOrder()).isEqualTo(3); + + SkillCloudSkillProjection figma = occupationSkillRepository + .findSkillProjectionsByOccupationIdAndCategoryId(1L, 104L) + .stream() + .filter(item -> item.skillId().equals(50L)) + .findFirst() + .orElseThrow(); + + assertThat(figma.sourceType()).isEqualTo("O*NET"); + assertThat(figma.sourceCode()).isEqualTo("15-1254.00|Figma|2.E.7.d"); + assertThat(figma.importance()).isNull(); + assertThat(figma.importanceSourceCode()).isNull(); + assertThat(figma.importanceElementId()).isNull(); + assertThat(figma.importanceIsProxy()).isFalse(); + } + + @Test + void aliasAndSkillLookupsReturnOnlyRequestedRecords() { + Optional normalizedAlias = skillAliasRepository.findByNormalizedAlias("react"); + assertThat(normalizedAlias).isPresent(); + assertThat(normalizedAlias.orElseThrow().getSkillId()).isEqualTo(26L); + + List aliases = skillAliasRepository + .findAllBySkillIdInOrderByIdAsc(List.of(3L, 26L)); + assertThat(aliases).hasSize(2); + assertThat(aliases) + .extracting(SkillAlias::getSkillId) + .containsExactly(3L, 26L); + + Optional react = skillRepository.findByCode("REACT"); + assertThat(react).isPresent(); + assertThat(react.orElseThrow().getSourceType()).isEqualTo("O*NET"); + + List skills = skillRepository.findAllByIdInOrderByIdAsc(List.of(4L, 3L)); + assertThat(skills) + .extracting(Skill::getId) + .containsExactly(3L, 4L); + } + + /** + * The legacy H2 schema has TINYINT/Boolean mismatches outside the V35 taxonomy. + * Scope validation to the seven T4 tables without changing historical migrations or entities. + */ + public static class SkillTaxonomySchemaFilterProvider implements SchemaFilterProvider { + + private static final Set VALIDATED_TABLES = Set.of( + "skill_source_versions", + "skill_industries", + "skill_occupations", + "skill_categories", + "skills", + "skill_aliases", + "occupation_skills" + ); + + @Override + public SchemaFilter getCreateFilter() { + return SchemaFilter.ALL; + } + + @Override + public SchemaFilter getDropFilter() { + return SchemaFilter.ALL; + } + + @Override + public SchemaFilter getTruncatorFilter() { + return SchemaFilter.ALL; + } + + @Override + public SchemaFilter getMigrateFilter() { + return SchemaFilter.ALL; + } + + @Override + public SchemaFilter getValidateFilter() { + return new SchemaFilter() { + @Override + public boolean includeNamespace(Namespace namespace) { + return true; + } + + @Override + public boolean includeTable(Table table) { + return VALIDATED_TABLES.contains(table.getName().toLowerCase(Locale.ROOT)); + } + + @Override + public boolean includeSequence(Sequence sequence) { + return false; + } + }; + } + } +}