package com.battery.business.web; import static org.junit.jupiter.api.Assertions.*; import java.nio.file.Files; import java.nio.file.Path; import java.sql.DriverManager; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import com.alibaba.fastjson2.JSON; import com.battery.business.domain.TTestDataRevisionAction; import com.battery.business.mapper.TTestMapper; import com.battery.business.service.ITestConsoleDataService; import com.battery.business.service.impl.TestConsoleDataServiceImpl; import com.battery.business.service.impl.TTestServiceImpl; import com.battery.business.service.impl.TestDataQueryServiceImpl; import org.apache.ibatis.datasource.unpooled.UnpooledDataSource; import org.flywaydb.core.Flyway; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.SqlSessionTemplate; import org.springframework.aop.framework.ProxyFactory; import org.springframework.core.io.ClassPathResource; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; import org.springframework.transaction.interceptor.TransactionInterceptor; /** 仅在脚本提供的随机隔离库内验证真实 MySQL 迁移、事务和甲方全量样例。 */ @EnabledIfEnvironmentVariable(named = "WORKBOOK_TEST_SCHEMA", matches = "ticket04_import_[a-f0-9]{32}") class ConsoleWorkbookImportMysqlTest { @Test void fullWorkbookMigrationTransactionRevisionAndWarningIsolation() throws Exception { String schema = System.getenv("WORKBOOK_TEST_SCHEMA"); String user = System.getenv("WORKBOOK_TEST_USER"), password = System.getenv("WORKBOOK_TEST_PASSWORD"); String base = "jdbc:mysql://127.0.0.1:6134/"; String options = "?useSSL=false&allowPublicKeyRetrieval=true&connectionTimeZone=UTC"; try (var admin = DriverManager.getConnection(base + options, user, password); var sql = admin.createStatement()) { sql.execute("create database " + schema + " character set utf8mb4 collate utf8mb4_0900_ai_ci"); try { String url = base + schema + options; var configuration = Flyway.configure().dataSource(url, user, password).locations("classpath:db/migration-final").cleanDisabled(true); configuration.target("8").load().migrate(); var flyway = configuration.target("9").load(); assertEquals(1, flyway.migrate().migrationsExecuted); assertEquals(0, flyway.migrate().migrationsExecuted); var source = new UnpooledDataSource("com.mysql.cj.jdbc.Driver", url, user, password); var jdbc = new JdbcTemplate(source); jdbc.execute("insert into t_test(test_id,test_name,creator_id,principal_id,test_type_id,test_purpose,test_status,reception_status) values(1,'三 Sheet 验收',1,1,1,'隔离测试','COMPLETED','ENDED')"); jdbc.execute("insert into t_detection_cabinet(cabinet_id,adapter_code,cabinet_code,cabinet_name,vendor_device_type,vendor_device_id) values(1,'adapter-a','CAB1','检测柜',27,1)"); jdbc.execute("insert into t_detection_channel(channel_id,adapter_code,cabinet_id,devtype,devid,subdevid,chlid) values(11,'adapter-a',1,27,1,1,2)"); jdbc.execute("insert into t_test_battery(test_battery_id,test_id,group_id,battery_no,collector_id) values(4,1,1,'BAT-1',1)"); jdbc.execute("insert into t_test_channel_binding(binding_id,test_id,test_battery_id,channel_id) values(5,1,4,11)"); jdbc.execute("insert into t_test_warning_threshold(test_id,metric_code,max_value,unit,severity) values(1,'TEMPERATURE',1,'C','WARNING')"); var factory = new SqlSessionFactoryBean(); factory.setDataSource(source); factory.setTypeAliasesPackage("com.battery.business.domain"); factory.setMapperLocations(new ClassPathResource("mapper/battery/TTestMapper.xml")); var mapper = new SqlSessionTemplate(factory.getObject()).getMapper(TTestMapper.class); var proxy = new ProxyFactory(new TestConsoleDataServiceImpl(mapper)); proxy.addAdvice(new TransactionInterceptor(new DataSourceTransactionManager(source), new AnnotationTransactionAttributeSource())); var service = (ITestConsoleDataService) proxy.getProxy(); var controller = new TTestController() { @Override public Long getUserId() { return 1L; } @Override public String getUsername() { return "验收操作员"; } }; ReflectionTestUtils.setField(controller, "consoleDataService", service); var tests = new TTestServiceImpl(); ReflectionTestUtils.setField(tests, "testMapper", mapper); ReflectionTestUtils.setField(controller, "testDataQueryService", new TestDataQueryServiceImpl(mapper, tests)); var sample = JSON.parseObject(Files.readString(Path.of(System.getenv("TICKET03_SAMPLE_JSON")))); Map>> sheets = new LinkedHashMap<>(); for (String type : List.of("REALTIME", "STEP", "CYCLE")) sheets.put(type, (List) sample.getJSONArray(type).toJavaList(Map.class)); // 在最后一类写入制造数据库错误,确认前两类和修订一起回滚。 jdbc.execute("alter table t_test_cycle_record add constraint workbook_probe_failure check (cycle_no <> 2)"); assertThrows(RuntimeException.class, () -> submit(controller, sheets)); assertTrue(mapper.selectPersistedFinalRealtimeRecords(1L).isEmpty()); assertTrue(mapper.selectPersistedFinalStepRecords(1L).isEmpty()); assertTrue(mapper.selectPersistedFinalCycleRecords(1L).isEmpty()); assertTrue(service.selectActions(new TTestDataRevisionAction()).isEmpty()); jdbc.execute("alter table t_test_cycle_record drop constraint workbook_probe_failure"); var imported = submit(controller, sheets); assertEquals(7767, imported.get("importedCount")); assertEquals(7605, mapper.selectPersistedFinalRealtimeRecords(1L).size()); assertEquals(129, mapper.selectPersistedFinalStepRecords(1L).size()); assertEquals(33, mapper.selectPersistedFinalCycleRecords(1L).size()); // 通过正式查询入口验证真实 JDBC 时间类型,避免内存库时间类型掩盖接口偏差。 var json = tools.jackson.databind.json.JsonMapper.builder().findAndAddModules().build(); for (var result : List.of(controller.persistedRecords(1L), controller.persistedSteps(1L), controller.persistedCycles(1L))) { var rows = json.readTree(json.writeValueAsString(result)).get("data"); for (var row : rows) for (String field : List.of("deviceTime", "recordedAt", "absoluteTime", "serverReceivedAt")) { var value = row.get(field); if (value != null && !value.isNull()) assertTrue(value.isIntegralNumber(), field + ": " + value); } } var firstRecord = json.readTree(json.writeValueAsString(controller.persistedRecords(1L))).get("data").get(7604); assertEquals(1683517331000L, firstRecord.get("deviceTime").longValue()); var actions = service.selectActions(new TTestDataRevisionAction()); assertEquals(3, actions.size()); assertEquals(3, ((List) imported.get("actionIds")).size()); for (var item : actions) { var revision = service.selectActionById(item.getActionId()); assertEquals("三 Sheet 共用原因", revision.getReason()); int expected = switch (revision.getDataType()) { case "REALTIME" -> 7605; case "STEP" -> 129; default -> 33; }; assertEquals((long) expected, revision.getAffectedRecordCount()); assertNull(revision.getIdentitySummary()); assertNull(revision.getChangesSummary()); assertNull(revision.getBeforeContentJson()); // 页面只返回各类摘要,数据库仍保留该类全部导入明细。 var changes = JSON.parseArray(jdbc.queryForObject("select cast(changes_summary as char) from t_test_data_revision_action where action_id=?", String.class, revision.getActionId())); assertEquals(expected, changes.size()); } for (String type : sheets.keySet()) { var query = new TTestDataRevisionAction(); query.setTestId(1L); query.setDataType(type); assertEquals(1, service.selectActions(query).size()); } var retry = submit(controller, sheets); assertEquals(0, retry.get("importedCount")); assertEquals(7767, retry.get("skippedCount")); assertNull(retry.get("actionId")); sheets.get("CYCLE").get(32).put("dischargeCapacity", 99); assertThrows(RuntimeException.class, () -> submit(controller, sheets)); assertEquals(3, service.selectActions(new TTestDataRevisionAction()).size()); assertTrue(mapper.selectWarningExceptions(1L, null).isEmpty()); assertEquals("COMPLETED", mapper.selectTestById(1L).getTestStatus()); assertEquals("ENDED", mapper.selectTestById(1L).getReceptionStatus()); } finally { sql.execute("drop database " + schema); } } } private com.ruoyi.common.core.domain.AjaxResult submit(TTestController controller, Map>> sheets) { return controller.dataAction(1L, new TTestController.ConsoleDataActionRequest("ALL", "IMPORT", "三 Sheet 导入", "[]", JSON.toJSONString(sheets), "三 Sheet 共用原因", List.of())); } }