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.
 
 
 

399 lines
18 KiB

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<String, Object> 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<OnboardingChecklistItem> 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<StudentOnboardingProgress> 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<StudentOnboardingProgress> 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<CompanyKnowledge> 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<Object[]> 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<Object[]> 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<String> 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<Object[]> 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<String> 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;
}
};
}
}
}