package com.mydreamjob.integration; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashSet; import java.util.Set; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 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.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.web.servlet.MockMvc; import org.testcontainers.containers.MySQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** 服务器真实版本的全迁移、Hibernate validate 和接口集合门禁,不能静默跳过。 */ @Testcontainers(disabledWithoutDocker = false) @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("mysql-it") class LxqModulesMySql57IntegrationTest { @Container static final MySQLContainer MYSQL = new MySQLContainer<>("mysql:5.7.36") .withCommand("--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci") .withDatabaseName("module57_test") .withUsername("module_test").withPassword("module-test-only"); @DynamicPropertySource static void properties(DynamicPropertyRegistry r) { r.add("spring.datasource.url", MYSQL::getJdbcUrl); r.add("spring.datasource.username", MYSQL::getUsername); r.add("spring.datasource.password", MYSQL::getPassword); } @MockBean RedisTemplate redis; @Autowired JdbcTemplate jdbc; @Autowired MockMvc mvc; @Autowired ObjectMapper mapper; @Autowired com.mydreamjob.messaging.MessageRepository messages; @Autowired jakarta.persistence.EntityManagerFactory entityManagerFactory; @Autowired com.mydreamjob.application.ApplicationService applications; @Test void studentApplicationSummaryUsesOneBatchQueryOnMysql57() { String key = "summary57-" + java.util.UUID.randomUUID().toString().substring(0, 8); jdbc.update("INSERT INTO users (phone,role,is_active,is_verified) VALUES (?,'STUDENT',1,1)", key); Long user = jdbc.queryForObject("SELECT id FROM users WHERE phone=?", Long.class, key); Long company = null; Long job = null; try { jdbc.update("INSERT INTO companies (owner_user_id,name,verified,created_at,updated_at) " + "VALUES (?,?,1,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", user, key); company = jdbc.queryForObject("SELECT id FROM companies WHERE owner_user_id=?", Long.class, user); jdbc.update("INSERT INTO jobs (company_id,title,description,city,employment_type,status,salary_min,salary_max,created_at,updated_at) " + "VALUES (?,?,'fixture','上海','FULL_TIME','CLOSED',12000,24000,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", company,key); job = jdbc.queryForObject("SELECT id FROM jobs WHERE company_id=?", Long.class, company); jdbc.update("INSERT INTO applications (job_id,student_user_id,status,created_at,updated_at) " + "VALUES (?,?,'SUBMITTED',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", job,user); var stats = entityManagerFactory.unwrap(org.hibernate.SessionFactory.class).getStatistics(); boolean wasEnabled = stats.isStatisticsEnabled(); stats.setStatisticsEnabled(true); try { long before = stats.getPrepareStatementCount(); var page = applications.listStudentApplicationViews(user, null, 0, 1); assertThat(stats.getPrepareStatementCount() - before).isEqualTo(3); assertThat(page.getTotalElements()).isEqualTo(1); assertThat(page.getContent().get(0)).containsEntry("jobTitle",key) .containsEntry("companyName",key).containsEntry("city","上海") .containsEntry("salaryMin",12000).containsEntry("salaryMax",24000) .containsEntry("jobStatus","CLOSED").containsEntry("status","SUBMITTED") .containsEntry("employmentType","FULL_TIME"); } finally { stats.setStatisticsEnabled(wasEnabled); } } finally { if (job != null) { jdbc.update("DELETE FROM applications WHERE job_id=?",job); jdbc.update("DELETE FROM jobs WHERE id=?",job); } if (company != null) jdbc.update("DELETE FROM companies WHERE id=?",company); jdbc.update("DELETE FROM users WHERE id=?",user); } } @Test void recentWindowUnreadUsesOneBoundedQueryOnMysql57() { String phone = "q57-" + java.util.UUID.randomUUID().toString().substring(0, 8); jdbc.update("INSERT INTO users (phone,role,is_active,is_verified) VALUES (?,'STUDENT',1,1)", phone); Long user = jdbc.queryForObject("SELECT id FROM users WHERE phone=?", Long.class, phone); java.util.List conversations = new java.util.ArrayList<>(); try { for (int c = 0; c < 2; c++) { String title = phone + "-" + c; jdbc.update("INSERT INTO conversations (title,type,created_by,created_at,updated_at) " + "VALUES (?,'DIRECT',?,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", title, user); Long id = jdbc.queryForObject("SELECT id FROM conversations WHERE title=?", Long.class, title); conversations.add(id); java.util.List batch = new java.util.ArrayList<>(); for (int i = 0; i < 600; i++) batch.add(new Object[]{id,user,"legacy-" + i, i % 20 == 0 ? "RECALLED" : "SENT"}); jdbc.batchUpdate("INSERT INTO messages (conversation_id,sender_user_id,content,status,created_at) " + "VALUES (?,?,?,?,'2026-10-15 08:00:00')", batch); jdbc.update("INSERT INTO message_reads (message_id,reader_user_id,read_at) " + "SELECT id,?,CURRENT_TIMESTAMP FROM messages WHERE conversation_id=? " + "AND content IN ('legacy-50','legacy-150','legacy-250','legacy-350','legacy-450','legacy-550')", user,id); } var stats = entityManagerFactory.unwrap(org.hibernate.SessionFactory.class).getStatistics(); boolean wasEnabled = stats.isStatisticsEnabled(); stats.setStatisticsEnabled(true); try { long before = stats.getPrepareStatementCount(); var counts = messages.countUnreadByConversationIds(conversations, user); assertThat(stats.getPrepareStatementCount() - before).as("两会话合计只发一条 SQL").isEqualTo(1); assertThat(counts).hasSize(2); assertThat(counts).extracting(com.mydreamjob.messaging.MessageRepository.ConversationUnreadCount::getConversationId) .containsExactlyInAnyOrderElementsOf(conversations); assertThat(counts).extracting(com.mydreamjob.messaging.MessageRepository.ConversationUnreadCount::getUnreadCount) .containsOnly(470L); assertThat(messages.countUnreadByConversationIds(java.util.List.of(), user)).isEmpty(); assertThat(messages.countUnreadByConversationIds(java.util.List.of(conversations.get(0)), user)) .extracting(com.mydreamjob.messaging.MessageRepository.ConversationUnreadCount::getUnreadCount) .containsExactly(470L); assertThat(messages.countUnreadByConversationIds( java.util.List.of(conversations.get(0), conversations.get(0)), user)) .extracting(com.mydreamjob.messaging.MessageRepository.ConversationUnreadCount::getUnreadCount) .containsExactly(470L); } finally { stats.setStatisticsEnabled(wasEnabled); } } finally { for (Long id : conversations) jdbc.update("DELETE FROM conversations WHERE id=?", id); jdbc.update("DELETE FROM users WHERE id=?", user); } } @Test void fullMigrationAndSeedsValidateOnActualServerVersion() { assertThat(jdbc.queryForObject("select version()", String.class)).startsWith("5.7.36"); assertThat(jdbc.queryForObject("select count(*) from flyway_schema_history where success=0", Long.class)).isZero(); assertThat(jdbc.queryForObject("select count(*) from flyway_schema_history where version in ('47','48','49','50','51','52','53') and success=1", Long.class)).isEqualTo(7); assertThat(jdbc.queryForObject("select count(*) from salary_city_policy", Long.class)).isEqualTo(359); assertThat(jdbc.queryForObject("select count(*) from occupation_skills where importance_is_proxy not in (0,1)", Long.class)).isZero(); assertThat(jdbc.queryForObject("select count(*) from onboarding_checklist_items", Long.class)).isEqualTo(12); assertThat(jdbc.queryForObject("select count(*) from salary_calculation_history", Long.class)).isZero(); } @Test void runtimeAndStaticOperationsRemainIdentical() throws Exception { JsonNode runtime = mapper.readTree(mvc.perform(get("/v3/api-docs")) .andExpect(status().isOk()).andReturn().getResponse().getContentAsByteArray()); JsonNode contract = mapper.readTree(Files.readString(Path.of("../docs/openapi-rc.json"))); assertThat(operations(runtime)).hasSize(242).isEqualTo(operations(contract)); assertThat(runtime.path("paths").size()).isEqualTo(199); assertThat(runtime.path("paths").path("/conversations/{id}/events")) .isEqualTo(contract.path("paths").path("/conversations/{id}/events")); for (String field : Set.of("clientMessageId", "version")) { String pointer = "/components/schemas/MessageResponse/properties/" + field; assertThat(runtime.at(pointer).isMissingNode()).as(pointer).isFalse(); assertThat(contract.at(pointer)).as(pointer).isEqualTo(runtime.at(pointer)); } String requestField = "/components/schemas/SendMessageRequest/properties/clientMessageId"; assertThat(runtime.at(requestField).isMissingNode()).isFalse(); assertThat(contract.at(requestField)).isEqualTo(runtime.at(requestField)); } private Set operations(JsonNode document) { Set result = new HashSet<>(); document.path("paths").fields().forEachRemaining(path -> path.getValue().fieldNames().forEachRemaining(method -> { if (Set.of("get", "post", "put", "patch", "delete", "head", "options", "trace").contains(method)) result.add(method + " " + path.getKey()); })); return result; } }