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