package com.mydreamjob.onboarding; import com.mydreamjob.auth.User; import com.mydreamjob.auth.UserRepository; import com.mydreamjob.company.Company; import com.mydreamjob.company.CompanyRepository; import com.mydreamjob.job.Job; import com.mydreamjob.job.JobRepository; import com.mydreamjob.review.JobReview; import com.mydreamjob.review.JobReviewDimensionScore; import com.mydreamjob.review.JobReviewDimensionScoreRepository; import com.mydreamjob.review.JobReviewRepository; import org.hibernate.boot.model.relational.Namespace; import org.hibernate.boot.model.relational.Sequence; import org.hibernate.mapping.Table; import org.junit.jupiter.api.BeforeEach; import org.hibernate.tool.schema.spi.SchemaFilter; import org.hibernate.tool.schema.spi.SchemaFilterProvider; 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.jdbc.core.JdbcTemplate; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest @TestPropertySource(properties = { "spring.datasource.url=jdbc:h2:mem:integrationdb_onboarding_repository;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE", "spring.datasource.driver-class-name=org.h2.Driver", "spring.datasource.username=sa", "spring.datasource.password=", "spring.jpa.hibernate.ddl-auto=validate", "spring.jpa.properties.hibernate.hbm2ddl.schema_filter_provider=com.mydreamjob.onboarding.OnboardingRepositoryIntegrationTest$OnboardingSchemaFilterProvider" }) @ActiveProfiles("integration-test") @Transactional class OnboardingRepositoryIntegrationTest { private static final AtomicLong PHONE_SEQ = new AtomicLong(900); @Autowired private OnboardingChecklistItemRepository checklistItemRepository; @Autowired private StudentOnboardingProgressRepository progressRepository; @Autowired private CompanyKnowledgeRepository knowledgeRepository; @Autowired private JobReviewRepository reviewRepository; @Autowired private JobReviewDimensionScoreRepository dimensionScoreRepository; @Autowired private UserRepository userRepository; @Autowired private CompanyRepository companyRepository; @Autowired private JobRepository jobRepository; @Autowired private JdbcTemplate jdbcTemplate; @MockBean private RedisTemplate redisTemplate; @BeforeEach void clearData() { progressRepository.deleteAll(); knowledgeRepository.deleteAll(); dimensionScoreRepository.deleteAll(); reviewRepository.deleteAll(); jobRepository.deleteAll(); checklistItemRepository.deleteAll(); companyRepository.deleteAll(); userRepository.deleteAll(); } @Test void effectiveChecklistReturnsActiveStandardAndCompanyItemsInStableOrder() { Long companyAId = createCompany("清单企业A"); Long companyBId = createCompany("清单企业B"); OnboardingChecklistItem standardActive = saveChecklistItem( "STANDARD", 0L, null, "standard-visible", "通用可见项", 100, true, null, null); saveChecklistItem("STANDARD", 0L, null, "standard-inactive", "通用停用项", 1, false, null, null); OnboardingChecklistItem companyFirst = saveChecklistItem( "COMPANY", companyAId, companyAId, "company-a-first", "企业A首项", 50, true, "company-a-first-key", "a".repeat(64)); OnboardingChecklistItem companySecond = saveChecklistItem( "COMPANY", companyAId, companyAId, "company-a-second", "企业A次项", 50, true, "company-a-second-key", "b".repeat(64)); saveChecklistItem("COMPANY", companyAId, companyAId, "company-a-inactive", "企业A停用项", 40, false, null, null); saveChecklistItem("COMPANY", companyBId, companyBId, "company-b-visible", "企业B可见项", 10, true, "company-b-key", "c".repeat(64)); List result = checklistItemRepository.findEffectiveChecklistItems(companyAId); assertThat(result) .extracting(OnboardingChecklistItem::getId) .containsExactly(companyFirst.getId(), companySecond.getId(), standardActive.getId()); assertThat(result) .extracting(OnboardingChecklistItem::getItemKey) .containsExactly("company-a-first", "company-a-second", "standard-visible"); } @Test void requestHashIsMappedAsChar64() { Long companyId = createCompany("哈希企业"); String hash = "0123456789abcdef".repeat(4); OnboardingChecklistItem saved = saveChecklistItem( "COMPANY", companyId, companyId, "company-hash", "哈希映射项", 200, true, "company-hash-key", hash); checklistItemRepository.flush(); assertThat(checklistItemRepository.findById(saved.getId())) .get() .extracting(OnboardingChecklistItem::getRequestHash) .isEqualTo(hash); String typeName = jdbcTemplate.queryForObject( "SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS " + "WHERE UPPER(TABLE_NAME) = 'ONBOARDING_CHECKLIST_ITEMS' " + "AND UPPER(COLUMN_NAME) = 'REQUEST_HASH'", String.class); Integer columnSize = jdbcTemplate.queryForObject( "SELECT CHARACTER_MAXIMUM_LENGTH FROM INFORMATION_SCHEMA.COLUMNS " + "WHERE UPPER(TABLE_NAME) = 'ONBOARDING_CHECKLIST_ITEMS' " + "AND UPPER(COLUMN_NAME) = 'REQUEST_HASH'", Integer.class); assertThat(typeName).isNotNull().startsWith("CHAR").doesNotContain("VARCHAR"); assertThat(columnSize).isEqualTo(64); } @Test void progressBatchAndUniqueLookupsAreScopedToStudentAndItem() { Long studentOne = createUser(User.Role.STUDENT, "进度学生A"); Long studentTwo = createUser(User.Role.STUDENT, "进度学生B"); OnboardingChecklistItem firstItem = saveChecklistItem( "STANDARD", 0L, null, "progress-item-1", "进度项1", 10, true, null, null); OnboardingChecklistItem secondItem = saveChecklistItem( "STANDARD", 0L, null, "progress-item-2", "进度项2", 20, true, null, null); OnboardingChecklistItem thirdItem = saveChecklistItem( "STANDARD", 0L, null, "progress-item-3", "进度项3", 30, true, null, null); LocalDateTime completedAt = LocalDateTime.of(2026, 9, 16, 8, 0); progressRepository.save(StudentOnboardingProgress.builder() .studentUserId(studentOne).itemId(firstItem.getId()).completed(true) .completedAt(completedAt).build()); progressRepository.save(StudentOnboardingProgress.builder() .studentUserId(studentOne).itemId(secondItem.getId()).completed(false) .completedAt(null).build()); progressRepository.save(StudentOnboardingProgress.builder() .studentUserId(studentTwo).itemId(firstItem.getId()).completed(true) .completedAt(LocalDateTime.of(2026, 9, 16, 9, 0)).build()); progressRepository.flush(); List batch = progressRepository.findByStudentUserIdAndItemIdIn( studentOne, List.of(firstItem.getId(), secondItem.getId(), thirdItem.getId())); assertThat(batch) .extracting(StudentOnboardingProgress::getItemId) .containsExactlyInAnyOrder(firstItem.getId(), secondItem.getId()); assertThat(batch) .filteredOn(progress -> progress.getItemId().equals(firstItem.getId())) .singleElement() .extracting(StudentOnboardingProgress::getCompletedAt) .isEqualTo(completedAt); Optional unique = progressRepository.findByStudentUserIdAndItemId( studentOne, firstItem.getId()); assertThat(unique).isPresent(); assertThat(unique.orElseThrow().getCompleted()).isTrue(); assertThat(progressRepository.findByStudentUserIdAndItemId(studentOne, thirdItem.getId())).isEmpty(); assertThat(progressRepository.findByStudentUserIdAndItemId(studentTwo, secondItem.getId())).isEmpty(); } @Test void companyKnowledgeLookupIsScopedToCompany() { Long companyId = createCompany("知识企业"); Long otherCompanyId = createCompany("其他知识企业"); knowledgeRepository.save(CompanyKnowledge.builder() .companyId(companyId) .culture("重视透明沟通") .workPolicies("弹性工作制") .onboardingGuide("前台报到") .build()); knowledgeRepository.flush(); Optional found = knowledgeRepository.findByCompanyId(companyId); assertThat(found).isPresent(); assertThat(found.orElseThrow().getCulture()).isEqualTo("重视透明沟通"); assertThat(found.orElseThrow().getWorkPolicies()).isEqualTo("弹性工作制"); assertThat(found.orElseThrow().getOnboardingGuide()).isEqualTo("前台报到"); assertThat(knowledgeRepository.findByCompanyId(otherCompanyId)).isEmpty(); } @Test void companyReviewAggregatesIncludeOnlyPublishedApprovedReviews() { Long companyId = createCompany("评价企业A"); Long otherCompanyId = createCompany("评价企业B"); Long studentOne = createUser(User.Role.STUDENT, "评价学生A"); Long studentTwo = createUser(User.Role.STUDENT, "评价学生B"); Long studentThree = createUser(User.Role.STUDENT, "评价学生C"); Long hiddenStudent = createUser(User.Role.STUDENT, "评价学生D"); Long otherStudent = createUser(User.Role.STUDENT, "评价学生E"); Long firstJobId = createJob(companyId, "评价岗位A1"); Long secondJobId = createJob(companyId, "评价岗位A2"); Long otherJobId = createJob(otherCompanyId, "评价岗位B"); JobReview five = saveReview(firstJobId, studentOne, 5, "[\"成长空间\",\"团队氛围\"]", "PUBLISHED", JobReview.MODERATION_APPROVED); JobReview four = saveReview(secondJobId, studentTwo, 4, "[\"成长空间\"]", "PUBLISHED", JobReview.MODERATION_AUTO_APPROVED); JobReview three = saveReview(firstJobId, studentThree, 3, "[\"团队氛围\"]", "PUBLISHED", JobReview.MODERATION_APPROVED); JobReview hidden = saveReview(firstJobId, hiddenStudent, 1, "[\"薪资透明\"]", "PUBLISHED", JobReview.MODERATION_PENDING); saveReview(otherJobId, otherStudent, 2, "[\"其他企业标签\"]", "PUBLISHED", JobReview.MODERATION_APPROVED); dimensionScoreRepository.saveAll(List.of( dimensionScore(five.getId(), "TEAM_ATMOSPHERE", 5), dimensionScore(five.getId(), "GROWTH", 5), dimensionScore(four.getId(), "TEAM_ATMOSPHERE", 4), dimensionScore(four.getId(), "GROWTH", 4), dimensionScore(three.getId(), "TEAM_ATMOSPHERE", 3), dimensionScore(hidden.getId(), "TEAM_ATMOSPHERE", 1) )); dimensionScoreRepository.flush(); List summary = reviewRepository.countAndAveragePublishedRatingByCompanyId(companyId); assertThat(summary).hasSize(1); assertThat(((Number) summary.get(0)[0]).longValue()).isEqualTo(3); assertThat(((Number) summary.get(0)[1]).doubleValue()).isEqualTo(4.0); List distribution = reviewRepository.findPublishedRatingDistributionByCompanyId(companyId); assertThat(distribution).hasSize(3); assertThat(((Number) distribution.get(0)[0]).intValue()).isEqualTo(3); assertThat(((Number) distribution.get(0)[1]).longValue()).isEqualTo(1); assertThat(((Number) distribution.get(1)[0]).intValue()).isEqualTo(4); assertThat(((Number) distribution.get(1)[1]).longValue()).isEqualTo(1); assertThat(((Number) distribution.get(2)[0]).intValue()).isEqualTo(5); assertThat(((Number) distribution.get(2)[1]).longValue()).isEqualTo(1); List tags = reviewRepository.findPublishedTagsByCompanyId(companyId); assertThat(tags).hasSize(3); assertThat(tags).allSatisfy(tag -> assertThat(tag).doesNotContain("薪资透明", "其他企业标签")); assertThat(tags).anySatisfy(tag -> assertThat(tag).contains("成长空间").contains("团队氛围")); assertThat(tags).anySatisfy(tag -> assertThat(tag).contains("成长空间").doesNotContain("团队氛围")); assertThat(tags).anySatisfy(tag -> assertThat(tag).contains("团队氛围").doesNotContain("成长空间")); List dimensions = dimensionScoreRepository.aggregatePublishedDimensionsByCompanyId(companyId); assertThat(dimensions).hasSize(2); assertThat(dimensions.get(0)[0]).isEqualTo("GROWTH"); assertThat(((Number) dimensions.get(0)[1]).doubleValue()).isEqualTo(4.5); assertThat(((Number) dimensions.get(0)[2]).longValue()).isEqualTo(2); assertThat(dimensions.get(1)[0]).isEqualTo("TEAM_ATMOSPHERE"); assertThat(((Number) dimensions.get(1)[1]).doubleValue()).isEqualTo(4.0); assertThat(((Number) dimensions.get(1)[2]).longValue()).isEqualTo(3); } private Long createCompany(String name) { Long ownerId = createUser(User.Role.COMPANY, name + "负责人"); return companyRepository.save(Company.builder() .ownerUserId(ownerId) .name(name) .verified(true) .build()).getId(); } private Long createUser(User.Role role, String nickname) { User user = userRepository.save(User.builder() .phone("175" + String.format("%08d", PHONE_SEQ.incrementAndGet())) .nickname(nickname) .role(role) .roles(Set.of(role)) .active(true) .verified(true) .build()); return user.getId(); } private Long createJob(Long companyId, String title) { return jobRepository.save(Job.builder() .companyId(companyId) .title(title) .description("岗位描述") .city("上海") .employmentType("FULL_TIME") .status("PUBLISHED") .build()).getId(); } private OnboardingChecklistItem saveChecklistItem( String scopeType, Long scopeId, Long companyId, String itemKey, String title, int sortOrder, boolean active, String idempotencyKey, String requestHash) { return checklistItemRepository.save(OnboardingChecklistItem.builder() .scopeType(scopeType) .scopeId(scopeId) .companyId(companyId) .itemKey(itemKey) .title(title) .description(title + "说明") .category("OTHER") .sortOrder(sortOrder) .required(false) .active(active) .idempotencyKey(idempotencyKey) .requestHash(requestHash) .build()); } private JobReview saveReview( Long jobId, Long studentUserId, int rating, String tags, String status, String moderationStatus) { return reviewRepository.save(JobReview.builder() .jobId(jobId) .studentUserId(studentUserId) .rating(rating) .content("评价内容") .tags(tags) .status(status) .moderationStatus(moderationStatus) .build()); } private JobReviewDimensionScore dimensionScore(Long reviewId, String dimensionCode, int score) { return JobReviewDimensionScore.builder() .reviewId(reviewId) .dimensionCode(dimensionCode) .score(score) .build(); } public static class OnboardingSchemaFilterProvider implements SchemaFilterProvider { private static final Set VALIDATED_TABLES = Set.of( "onboarding_checklist_items", "student_onboarding_progress", "company_knowledge" ); @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; } }; } } }