springbootAI 1.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. spring/__init__.py +66 -0
  2. spring/ai/__init__.py +78 -0
  3. spring/ai/advisors.py +139 -0
  4. spring/ai/annotations.py +74 -0
  5. spring/ai/autoconfig.py +481 -0
  6. spring/ai/core.py +391 -0
  7. spring/ai/etl.py +188 -0
  8. spring/ai/memory.py +109 -0
  9. spring/ai/observability.py +129 -0
  10. spring/ai/providers.py +789 -0
  11. spring/ai/resilience.py +258 -0
  12. spring/ai/tools.py +106 -0
  13. spring/ai/vectorstore.py +303 -0
  14. spring/annotations/__init__.py +188 -0
  15. spring/annotations/cache.py +126 -0
  16. spring/annotations/cloud.py +207 -0
  17. spring/annotations/conditional.py +272 -0
  18. spring/annotations/core.py +864 -0
  19. spring/annotations/messaging.py +107 -0
  20. spring/aop/__init__.py +4 -0
  21. spring/aop/cloud_aop.py +404 -0
  22. spring/aop/comprehensive_aop.py +1015 -0
  23. spring/aop/method_interceptor.py +19 -0
  24. spring/aop/proxy_factory.py +55 -0
  25. spring/cloud/__init__.py +76 -0
  26. spring/cloud/discovery.py +364 -0
  27. spring/cloud/feign.py +469 -0
  28. spring/cloud/gateway.py +452 -0
  29. spring/cloud/load_balancer.py +149 -0
  30. spring/cloud/seata.py +557 -0
  31. spring/cloud/sentinel.py +525 -0
  32. spring/cloud/tracer.py +337 -0
  33. spring/config/__init__.py +21 -0
  34. spring/config/binding.py +206 -0
  35. spring/config/config_loader.py +405 -0
  36. spring/context/__init__.py +13 -0
  37. spring/context/application_context.py +589 -0
  38. spring/context/bean_definition.py +70 -0
  39. spring/context/bean_factory.py +1052 -0
  40. spring/context/registry.py +58 -0
  41. spring/context/scanner.py +106 -0
  42. spring/core/__init__.py +3 -0
  43. spring/core/graceful_shutdown.py +196 -0
  44. spring/core/typing_utils.py +50 -0
  45. spring/csv/__init__.py +52 -0
  46. spring/csv/annotations.py +402 -0
  47. spring/csv/converters.py +69 -0
  48. spring/csv/easy_csv.py +95 -0
  49. spring/csv/exceptions.py +27 -0
  50. spring/csv/reader.py +195 -0
  51. spring/csv/writer.py +155 -0
  52. spring/data/__init__.py +54 -0
  53. spring/data/page.py +181 -0
  54. spring/data/repository.py +274 -0
  55. spring/data/specification.py +228 -0
  56. spring/datasource/__init__.py +66 -0
  57. spring/datasource/annotations.py +133 -0
  58. spring/datasource/context.py +69 -0
  59. spring/datasource/dynamic.py +148 -0
  60. spring/event/__init__.py +7 -0
  61. spring/event/publisher.py +69 -0
  62. spring/excel/__init__.py +51 -0
  63. spring/excel/annotations.py +405 -0
  64. spring/excel/converters.py +231 -0
  65. spring/excel/easy_excel.py +94 -0
  66. spring/excel/exceptions.py +31 -0
  67. spring/excel/reader.py +254 -0
  68. spring/excel/style.py +95 -0
  69. spring/excel/writer.py +197 -0
  70. spring/i18n/__init__.py +97 -0
  71. spring/i18n/accessor.py +94 -0
  72. spring/i18n/auto_config.py +177 -0
  73. spring/i18n/holder.py +106 -0
  74. spring/i18n/locale.py +152 -0
  75. spring/i18n/locale_resolver.py +367 -0
  76. spring/i18n/message_source.py +250 -0
  77. spring/i18n/middleware.py +79 -0
  78. spring/i18n/properties.py +168 -0
  79. spring/i18n/sources.py +255 -0
  80. spring/logging/__init__.py +1 -0
  81. spring/logging/loguru_logger.py +228 -0
  82. spring/main.py +378 -0
  83. spring/messaging/__init__.py +1 -0
  84. spring/messaging/rabbitmq.py +302 -0
  85. spring/monitoring/__init__.py +1 -0
  86. spring/monitoring/prometheus.py +199 -0
  87. spring/orm/__init__.py +258 -0
  88. spring/orm/database.py +222 -0
  89. spring/orm/ddl_auto.py +1217 -0
  90. spring/orm/migration.py +419 -0
  91. spring/orm/mybatis_integration.py +400 -0
  92. spring/orm/pymybatis/__init__.py +86 -0
  93. spring/orm/pymybatis/annotations/__init__.py +30 -0
  94. spring/orm/pymybatis/annotations/annotations.py +332 -0
  95. spring/orm/pymybatis/cache/__init__.py +47 -0
  96. spring/orm/pymybatis/cache/cache.py +371 -0
  97. spring/orm/pymybatis/cache/redis_cache.py +434 -0
  98. spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
  99. spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
  100. spring/orm/pymybatis/configuration.py +525 -0
  101. spring/orm/pymybatis/core/__init__.py +10 -0
  102. spring/orm/pymybatis/core/sql_session.py +1382 -0
  103. spring/orm/pymybatis/core/sql_session_factory.py +76 -0
  104. spring/orm/pymybatis/dialect/__init__.py +9 -0
  105. spring/orm/pymybatis/dialect/dialect.py +445 -0
  106. spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
  107. spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
  108. spring/orm/pymybatis/interceptor/__init__.py +31 -0
  109. spring/orm/pymybatis/interceptor/interceptor.py +427 -0
  110. spring/orm/pymybatis/mapper/__init__.py +9 -0
  111. spring/orm/pymybatis/mapper/mapper.py +540 -0
  112. spring/orm/pymybatis/metrics/__init__.py +41 -0
  113. spring/orm/pymybatis/metrics/metrics.py +595 -0
  114. spring/orm/pymybatis/pool/__init__.py +9 -0
  115. spring/orm/pymybatis/pool/connection_pool.py +711 -0
  116. spring/orm/pymybatis/security/__init__.py +19 -0
  117. spring/orm/pymybatis/security/access_control.py +415 -0
  118. spring/orm/pymybatis/security/password_encoder.py +293 -0
  119. spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
  120. spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
  121. spring/orm/pymybatis/transaction/__init__.py +9 -0
  122. spring/orm/pymybatis/transaction/transaction.py +288 -0
  123. spring/orm/pymybatis/type_handler/__init__.py +37 -0
  124. spring/orm/pymybatis/type_handler/type_handler.py +473 -0
  125. spring/orm/pymybatis/version.py +9 -0
  126. spring/orm/pymybatis/xml_parser/__init__.py +9 -0
  127. spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
  128. spring/retry/__init__.py +12 -0
  129. spring/retry/retry_annotations.py +71 -0
  130. spring/retry/retry_decorator.py +155 -0
  131. spring/scheduling/__init__.py +3 -0
  132. spring/scheduling/scheduler.py +389 -0
  133. spring/security/__init__.py +39 -0
  134. spring/security/jwt_utils.py +281 -0
  135. spring/security/replay_protection.py +206 -0
  136. spring/security/secret_manager.py +226 -0
  137. spring/security/security_aop.py +248 -0
  138. spring/security/security_context.py +172 -0
  139. spring/test/__init__.py +45 -0
  140. spring/test/slicing.py +341 -0
  141. spring/tracing/__init__.py +11 -0
  142. spring/tracing/skywalking.py +229 -0
  143. spring/tx/__init__.py +52 -0
  144. spring/tx/events.py +172 -0
  145. spring/tx/synchronization.py +143 -0
  146. spring/utils/__init__.py +5 -0
  147. spring/utils/banner.py +32 -0
  148. spring/utils/logger.py +73 -0
  149. spring/utils/redis_client.py +526 -0
  150. spring/validation/__init__.py +55 -0
  151. spring/validation/aop.py +141 -0
  152. spring/validation/constraints.py +357 -0
  153. spring/validation/exceptions.py +55 -0
  154. spring/validation/validator.py +139 -0
  155. spring/web/__init__.py +12 -0
  156. spring/web/actuator.py +319 -0
  157. spring/web/exception_handler.py +61 -0
  158. spring/web/health.py +399 -0
  159. spring/web/interceptor.py +91 -0
  160. spring/web/result.py +44 -0
  161. spring/web/swagger.py +601 -0
  162. spring/web/web_context.py +755 -0
  163. spring/websocket/__init__.py +86 -0
  164. spring/websocket/annotations.py +169 -0
  165. spring/websocket/broker.py +238 -0
  166. spring/websocket/exceptions.py +26 -0
  167. spring/websocket/handler.py +243 -0
  168. spring/websocket/router.py +526 -0
  169. spring/websocket/session.py +216 -0
  170. springbootai-1.8.0.dist-info/METADATA +2796 -0
  171. springbootai-1.8.0.dist-info/RECORD +175 -0
  172. springbootai-1.8.0.dist-info/WHEEL +5 -0
  173. springbootai-1.8.0.dist-info/entry_points.txt +2 -0
  174. springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
  175. springbootai-1.8.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,419 @@
1
+ """
2
+ 数据库迁移管理器 (Database Migration Manager)
3
+
4
+ 类 Flyway 风格的轻量数据库迁移工具:
5
+ - 基于 SQL 文件版本号管理
6
+ - 自动追踪已执行迁移(schema_version 表)
7
+ - 支持 checksum 校验防止篡改
8
+ - 支持 MySQL/PostgreSQL/SQLite
9
+ - 迁移文件命名: V{version}__{description}.sql
10
+ """
11
+
12
+ import os
13
+ import re
14
+ import hashlib
15
+ import logging
16
+ import time
17
+ from typing import List, Dict, Optional, Tuple
18
+ from pathlib import Path
19
+
20
+ logger = logging.getLogger("Spring.ORM.Migration")
21
+
22
+ # 迁移文件命名模式: V1__init.sql, V2__add_users_table.sql
23
+ _MIGRATION_PATTERN = re.compile(r'^V(\d+(?:\.\d+)?)__(.+)\.sql$', re.IGNORECASE)
24
+ # 版本号分隔符
25
+ _VERSION_SPLIT = re.compile(r'[._]')
26
+
27
+
28
+ class MigrationError(Exception):
29
+ """迁移执行错误"""
30
+ pass
31
+
32
+
33
+ class MigrationState:
34
+ """迁移状态"""
35
+ PENDING = "PENDING"
36
+ SUCCESS = "SUCCESS"
37
+ FAILED = "FAILED"
38
+
39
+
40
+ class MigrationRecord:
41
+ """单条迁移记录"""
42
+ __slots__ = ('version', 'description', 'script', 'checksum',
43
+ 'installed_on', 'execution_time', 'success')
44
+
45
+ def __init__(self, version: str, description: str, script: str,
46
+ checksum: str, execution_time: float = 0.0,
47
+ success: bool = True):
48
+ self.version = version
49
+ self.description = description
50
+ self.script = script
51
+ self.checksum = checksum
52
+ self.installed_on = time.time()
53
+ self.execution_time = execution_time
54
+ self.success = success
55
+
56
+
57
+ class MigrationManager:
58
+ """
59
+ 数据库迁移管理器
60
+
61
+ Usage:
62
+ manager = MigrationManager(connection_pool, migrations_dir="sql/migrations")
63
+ manager.migrate() # 执行所有待执行迁移
64
+ """
65
+
66
+ def __init__(self, connection_pool, migrations_dir: str,
67
+ dialect: str = "mysql", table_name: str = "schema_version"):
68
+ self.pool = connection_pool
69
+ self.migrations_dir = Path(migrations_dir)
70
+ self.dialect = dialect.lower()
71
+ self.table_name = table_name
72
+ self._ensure_version_table()
73
+
74
+ def _ensure_version_table(self) -> None:
75
+ """确保 schema_version 表存在"""
76
+ ddl_map = {
77
+ 'mysql': f"""
78
+ CREATE TABLE IF NOT EXISTS `{self.table_name}` (
79
+ `version` VARCHAR(50) NOT NULL PRIMARY KEY,
80
+ `description` VARCHAR(200) NOT NULL,
81
+ `script` VARCHAR(500) NOT NULL,
82
+ `checksum` VARCHAR(64) NOT NULL,
83
+ `installed_on` BIGINT NOT NULL,
84
+ `execution_time` INT NOT NULL,
85
+ `success` TINYINT(1) NOT NULL DEFAULT 1
86
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
87
+ """,
88
+ 'postgresql': f"""
89
+ CREATE TABLE IF NOT EXISTS "{self.table_name}" (
90
+ "version" VARCHAR(50) PRIMARY KEY,
91
+ "description" VARCHAR(200) NOT NULL,
92
+ "script" VARCHAR(500) NOT NULL,
93
+ "checksum" VARCHAR(64) NOT NULL,
94
+ "installed_on" BIGINT NOT NULL,
95
+ "execution_time" INTEGER NOT NULL,
96
+ "success" BOOLEAN NOT NULL DEFAULT TRUE
97
+ )
98
+ """,
99
+ 'sqlite': f"""
100
+ CREATE TABLE IF NOT EXISTS "{self.table_name}" (
101
+ "version" TEXT PRIMARY KEY,
102
+ "description" TEXT NOT NULL,
103
+ "script" TEXT NOT NULL,
104
+ "checksum" TEXT NOT NULL,
105
+ "installed_on" INTEGER NOT NULL,
106
+ "execution_time" INTEGER NOT NULL,
107
+ "success" INTEGER NOT NULL DEFAULT 1
108
+ )
109
+ """,
110
+ }
111
+
112
+ ddl = ddl_map.get(self.dialect)
113
+ if not ddl:
114
+ logger.warning(f"Migration: dialect '{self.dialect}' not officially supported, trying generic")
115
+ ddl = ddl_map['mysql']
116
+
117
+ conn = None
118
+ try:
119
+ pooled = self.pool.get_connection()
120
+ conn = pooled.connection
121
+ cursor = conn.cursor()
122
+ for stmt in ddl.split(';'):
123
+ stmt = stmt.strip()
124
+ if stmt:
125
+ cursor.execute(stmt)
126
+ conn.commit()
127
+ cursor.close()
128
+ self.pool.return_connection(pooled)
129
+ except Exception as e:
130
+ if conn:
131
+ try:
132
+ conn.rollback()
133
+ except Exception:
134
+ pass
135
+ raise MigrationError(f"Failed to create version table: {e}") from e
136
+
137
+ def _get_applied_versions(self) -> Dict[str, MigrationRecord]:
138
+ """获取已执行的迁移版本"""
139
+ conn = None
140
+ try:
141
+ pooled = self.pool.get_connection()
142
+ conn = pooled.connection
143
+ cursor = conn.cursor()
144
+ if self.dialect == 'mysql':
145
+ cursor.execute(f"SELECT version, description, script, checksum, installed_on, execution_time, success FROM `{self.table_name}` WHERE success = 1")
146
+ else:
147
+ cursor.execute(f'SELECT version, description, script, checksum, installed_on, execution_time, success FROM "{self.table_name}" WHERE success = 1')
148
+
149
+ applied = {}
150
+ for row in cursor.fetchall():
151
+ if isinstance(row, dict):
152
+ rec = MigrationRecord(
153
+ version=str(row['version']),
154
+ description=row['description'],
155
+ script=row['script'],
156
+ checksum=row['checksum'],
157
+ execution_time=row.get('execution_time', 0),
158
+ success=bool(row.get('success', True))
159
+ )
160
+ else:
161
+ rec = MigrationRecord(
162
+ version=str(row[0]),
163
+ description=row[1],
164
+ script=row[2],
165
+ checksum=row[3],
166
+ execution_time=row[4] if len(row) > 4 else 0,
167
+ success=bool(row[6]) if len(row) > 6 else True
168
+ )
169
+ applied[rec.version] = rec
170
+ cursor.close()
171
+ self.pool.return_connection(pooled)
172
+ return applied
173
+ except Exception as e:
174
+ if conn:
175
+ try:
176
+ conn.rollback()
177
+ except Exception:
178
+ pass
179
+ raise MigrationError(f"Failed to read applied versions: {e}") from e
180
+
181
+ def _discover_migrations(self) -> List[Tuple[str, str, str, str]]:
182
+ """发现迁移文件,返回 [(version, description, filename, content_checksum)]"""
183
+ if not self.migrations_dir.exists():
184
+ logger.warning(f"Migration directory not found: {self.migrations_dir}")
185
+ return []
186
+
187
+ migrations = []
188
+ for f in sorted(self.migrations_dir.iterdir()):
189
+ if not f.is_file():
190
+ continue
191
+ match = _MIGRATION_PATTERN.match(f.name)
192
+ if not match:
193
+ continue
194
+ version = match.group(1)
195
+ description = match.group(2).replace('_', ' ')
196
+ content = f.read_text(encoding='utf-8')
197
+ checksum = hashlib.sha256(content.encode('utf-8')).hexdigest()[:63]
198
+ migrations.append((version, description, f.name, content, checksum))
199
+
200
+ # 按版本号排序
201
+ def _version_key(item):
202
+ parts = []
203
+ for p in _VERSION_SPLIT.split(item[0]):
204
+ try:
205
+ parts.append((0, int(p)))
206
+ except ValueError:
207
+ parts.append((1, p))
208
+ return parts
209
+
210
+ migrations.sort(key=_version_key)
211
+ return [(m[0], m[1], m[2], m[4]) for m in migrations]
212
+
213
+ def _version_tuple(self, version: str) -> tuple:
214
+ """版本号转可比较tuple"""
215
+ parts = []
216
+ for p in _VERSION_SPLIT.split(version):
217
+ try:
218
+ parts.append(int(p))
219
+ except ValueError:
220
+ parts.append(p)
221
+ return tuple(parts)
222
+
223
+ def _execute_migration(self, version: str, description: str,
224
+ script_name: str, sql_content: str,
225
+ checksum: str) -> MigrationRecord:
226
+ """执行单条迁移"""
227
+ conn = None
228
+ start = time.monotonic()
229
+ try:
230
+ pooled = self.pool.get_connection()
231
+ conn = pooled.connection
232
+ cursor = conn.cursor()
233
+
234
+ # 按分号分割语句(简单分割,不处理存储过程中的分号)
235
+ statements = []
236
+ current = []
237
+ for line in sql_content.split('\n'):
238
+ stripped = line.strip()
239
+ # 跳过注释
240
+ if stripped.startswith('--') or stripped.startswith('#'):
241
+ continue
242
+ current.append(line)
243
+ if stripped.endswith(';'):
244
+ stmt = '\n'.join(current).strip().rstrip(';')
245
+ if stmt:
246
+ statements.append(stmt)
247
+ current = []
248
+ # 最后一条可能没有分号
249
+ remaining = '\n'.join(current).strip()
250
+ if remaining:
251
+ statements.append(remaining)
252
+
253
+ for stmt in statements:
254
+ stmt = stmt.strip()
255
+ if stmt:
256
+ logger.debug(f"Executing migration SQL: {stmt[:100]}...")
257
+ cursor.execute(stmt)
258
+
259
+ elapsed = time.monotonic() - start
260
+
261
+ # 记录迁移
262
+ if self.dialect == 'mysql':
263
+ cursor.execute(
264
+ f"INSERT INTO `{self.table_name}` (version, description, script, checksum, installed_on, execution_time, success) VALUES (%s, %s, %s, %s, %s, %s, %s)",
265
+ (version, description, script_name, checksum, int(time.time()), int(elapsed * 1000), 1)
266
+ )
267
+ else:
268
+ ph = '?' if self.dialect == 'sqlite' else '%s'
269
+ table_ref = f'"{self.table_name}"'
270
+ cursor.execute(
271
+ f'INSERT INTO {table_ref} (version, description, script, checksum, installed_on, execution_time, success) VALUES ({ph}, {ph}, {ph}, {ph}, {ph}, {ph}, {ph})',
272
+ (version, description, script_name, checksum, int(time.time()), int(elapsed * 1000), 1)
273
+ )
274
+
275
+ conn.commit()
276
+ cursor.close()
277
+ self.pool.return_connection(pooled)
278
+
279
+ record = MigrationRecord(version, description, script_name, checksum, elapsed, True)
280
+ logger.info(f"Migration V{version} ({description}) applied successfully in {elapsed:.2f}s")
281
+ return record
282
+
283
+ except Exception as e:
284
+ elapsed = time.monotonic() - start
285
+ if conn:
286
+ try:
287
+ conn.rollback()
288
+ except Exception:
289
+ pass
290
+ logger.error(f"Migration V{version} failed after {elapsed:.2f}s: {e}")
291
+ raise MigrationError(f"Migration V{version} ({description}) failed: {e}") from e
292
+
293
+ def migrate(self, baseline: bool = False) -> List[MigrationRecord]:
294
+ """
295
+ 执行所有待执行的迁移
296
+
297
+ Args:
298
+ baseline: 如果为True,将现有数据库标记为已完成初始迁移(用于在已有数据库上启用迁移)
299
+
300
+ Returns:
301
+ 本次执行的迁移记录列表
302
+ """
303
+ applied = self._get_applied_versions()
304
+ discovered = self._discover_migrations()
305
+
306
+ if not discovered:
307
+ logger.info("No migration files found")
308
+ return []
309
+
310
+ # 校验已执行迁移的checksum
311
+ for version, rec in applied.items():
312
+ for disc_ver, desc, script, checksum in discovered:
313
+ if disc_ver == version and rec.checksum != checksum:
314
+ raise MigrationError(
315
+ f"Checksum mismatch for migration V{version}! "
316
+ f"Applied checksum={rec.checksum}, current={checksum}. "
317
+ f"Migration files must not be modified after application."
318
+ )
319
+
320
+ executed = []
321
+ for version, description, script, checksum in discovered:
322
+ if version in applied:
323
+ continue
324
+
325
+ if baseline and not executed:
326
+ # baseline模式:将第一条之前的视为已baseline
327
+ logger.info(f"Baseline: marking migrations up to V{version} as applied")
328
+ baseline_conn = None
329
+ try:
330
+ pooled = self.pool.get_connection()
331
+ baseline_conn = pooled.connection
332
+ cursor = baseline_conn.cursor()
333
+ if self.dialect == 'mysql':
334
+ cursor.execute(
335
+ f"INSERT IGNORE INTO `{self.table_name}` (version, description, script, checksum, installed_on, execution_time, success) VALUES (%s, %s, %s, %s, %s, %s, %s)",
336
+ (version, description, script, checksum, int(time.time()), 0, 1)
337
+ )
338
+ else:
339
+ ph = '?' if self.dialect == 'sqlite' else '%s'
340
+ cursor.execute(
341
+ f'INSERT INTO "{self.table_name}" (version, description, script, checksum, installed_on, execution_time, success) SELECT {ph},{ph},{ph},{ph},{ph},{ph},{ph} WHERE NOT EXISTS (SELECT 1 FROM "{self.table_name}" WHERE version = {ph})',
342
+ (version, description, script, checksum, int(time.time()), 0, 1, version)
343
+ )
344
+ baseline_conn.commit()
345
+ cursor.close()
346
+ self.pool.return_connection(pooled)
347
+ except Exception:
348
+ if baseline_conn:
349
+ try:
350
+ baseline_conn.rollback()
351
+ except Exception:
352
+ pass
353
+ baseline = False
354
+ continue
355
+
356
+ record = self._execute_migration(version, description, script, self._read_sql(script), checksum)
357
+ executed.append(record)
358
+
359
+ if not executed:
360
+ logger.info(f"All {len(applied)} migrations are up to date")
361
+ else:
362
+ logger.info(f"Applied {len(executed)} new migration(s)")
363
+
364
+ return executed
365
+
366
+ def _read_sql(self, script_name: str) -> str:
367
+ """读取SQL文件内容"""
368
+ path = self.migrations_dir / script_name
369
+ return path.read_text(encoding='utf-8')
370
+
371
+ def status(self) -> Dict:
372
+ """获取迁移状态"""
373
+ applied = self._get_applied_versions()
374
+ discovered = self._discover_migrations()
375
+
376
+ migrations = []
377
+ for version, description, script, checksum in discovered:
378
+ state = MigrationState.SUCCESS if version in applied else MigrationState.PENDING
379
+ if version in applied and applied[version].checksum != checksum:
380
+ state = "CHECKSUM_MISMATCH"
381
+ migrations.append({
382
+ 'version': version,
383
+ 'description': description,
384
+ 'script': script,
385
+ 'state': state,
386
+ })
387
+
388
+ pending = [m for m in migrations if m['state'] == MigrationState.PENDING]
389
+ return {
390
+ 'total': len(migrations),
391
+ 'applied': len(applied),
392
+ 'pending': len(pending),
393
+ 'migrations': migrations,
394
+ }
395
+
396
+ def repair(self) -> int:
397
+ """修复失败的迁移记录(标记为可重试)"""
398
+ conn = None
399
+ try:
400
+ pooled = self.pool.get_connection()
401
+ conn = pooled.connection
402
+ cursor = conn.cursor()
403
+ if self.dialect == 'mysql':
404
+ cursor.execute(f"DELETE FROM `{self.table_name}` WHERE success = 0")
405
+ else:
406
+ cursor.execute(f'DELETE FROM "{self.table_name}" WHERE success = 0')
407
+ deleted = cursor.rowcount
408
+ conn.commit()
409
+ cursor.close()
410
+ self.pool.return_connection(pooled)
411
+ logger.info(f"Repaired {deleted} failed migration record(s)")
412
+ return deleted
413
+ except Exception as e:
414
+ if conn:
415
+ try:
416
+ conn.rollback()
417
+ except Exception:
418
+ pass
419
+ raise MigrationError(f"Repair failed: {e}") from e