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
spring/cloud/seata.py ADDED
@@ -0,0 +1,557 @@
1
+ """
2
+ 分布式事务模块
3
+ 集成真实 Seata,并提供仅供开发验证的 HTTP 补偿协调器
4
+
5
+ 支持三种模式:
6
+ - local: 本地模式,仅追踪事务状态(默认)
7
+ - http: 实验性 HTTP 补偿模式,不提供 AT 强一致性,禁止用于生产
8
+ - distributed: 真实Seata Server模式(需要seata SDK)
9
+
10
+ 实验性 HTTP 补偿模式工作原理:
11
+ 1. TM(事务发起方)开启全局事务,生成XID
12
+ 2. Feign调用远程服务时,通过 X-TX-XID header 传递XID
13
+ 3. RM(分支事务方)注册分支到TC(内嵌协调器)
14
+ 4. TM 提交时通知所有分支提交;回滚时通知所有分支回滚
15
+ 5. 分支服务暴露 /seata/branch/{branchId}/commit 和 /seata/branch/{branchId}/rollback 端点
16
+
17
+ 注意:分布式模式(distributed)要启用完整的分布式事务功能,请:
18
+ 1. 安装Seata Server(https://seata.io/zh-cn/docs/overview/what-is-seata.html)
19
+ 2. 安装并验证与本适配层 API 兼容的企业 Seata Python SDK
20
+ 3. 配置registry.conf和file.conf
21
+ 4. 在启动时设置SEATA_ENABLED=true
22
+ """
23
+ import logging
24
+ import time
25
+ import threading
26
+ import uuid
27
+ import json
28
+ from contextvars import ContextVar
29
+ from typing import Dict, Any, Optional, List, Callable
30
+ from urllib import request as urlrequest
31
+ from urllib.error import URLError, HTTPError
32
+
33
+ # 可选导入Seata
34
+ try:
35
+ import seata
36
+ from seata.rm import DataSourceProxy
37
+ from seata.tm import GlobalTransaction
38
+ from seata.core.context.RootContext import RootContext
39
+ _seata_available = True
40
+ except ImportError:
41
+ seata = None
42
+ DataSourceProxy = None
43
+ GlobalTransaction = None
44
+ RootContext = None
45
+ _seata_available = False
46
+
47
+ logger = logging.getLogger("Spring.Cloud.Seata")
48
+
49
+
50
+ class BranchStatus:
51
+ """分支事务状态"""
52
+ REGISTERED = "REGISTERED"
53
+ COMMITTED = "COMMITTED"
54
+ ROLLED_BACK = "ROLLED_BACK"
55
+ FAILED = "FAILED"
56
+
57
+
58
+ class SeataTransactionManager:
59
+ """Seata事务管理器(支持local/http/distributed三种模式)"""
60
+
61
+ _instance = None
62
+ _lock = threading.Lock()
63
+
64
+ def __new__(cls, *args, **kwargs):
65
+ if cls._instance is None:
66
+ with cls._lock:
67
+ if cls._instance is None:
68
+ cls._instance = super().__new__(cls)
69
+ return cls._instance
70
+
71
+ def __init__(self, server_addr: str = "localhost:8091", application_id: str = "",
72
+ transaction_group: str = "my_tx_group", mode: str = "local"):
73
+ if hasattr(self, '_initialized'):
74
+ return
75
+ self.server_addr = server_addr
76
+ self.application_id = application_id
77
+ self.transaction_group = transaction_group
78
+ self.mode = mode # 'local', 'http', or 'distributed'
79
+ self._transaction_context: ContextVar[Optional[Dict[str, Any]]] = ContextVar(
80
+ 'springpy_seata_transaction_context', default=None
81
+ )
82
+ self._seata_client_initialized = False
83
+ self._initialized = True
84
+
85
+ # 实验性 HTTP 模式:进程内状态只用于开发验证,不承诺故障恢复。
86
+ self._global_transactions: Dict[str, Dict] = {}
87
+ self._branches: Dict[str, List[Dict]] = {} # xid -> [branch]
88
+ self._gt_lock = threading.Lock()
89
+
90
+ # 分支事务回调注册(本地分支,用于同进程服务调用)
91
+ self._branch_callbacks: Dict[str, Dict[str, Callable]] = {}
92
+ self._cb_lock = threading.Lock()
93
+
94
+ if mode == "distributed":
95
+ self.set_mode(mode)
96
+
97
+ def configure(self, server_addr: str = "localhost:8091", application_id: str = "",
98
+ transaction_group: str = "my_tx_group", mode: str = "local") -> None:
99
+ """重新配置单例;初始化入口不能依赖第二次构造调用。"""
100
+ self.server_addr = server_addr
101
+ self.application_id = application_id
102
+ self.transaction_group = transaction_group
103
+ self.set_mode(mode)
104
+
105
+ def _get_context(self) -> Dict[str, Any]:
106
+ return self._transaction_context.get() or {}
107
+
108
+ def _set_context(self, **values: Any) -> None:
109
+ current = dict(self._get_context())
110
+ current.update(values)
111
+ self._transaction_context.set(current)
112
+
113
+ def _init_seata_client(self):
114
+ """初始化 Seata 客户端;分布式模式禁止静默降级。"""
115
+ if not _seata_available:
116
+ raise RuntimeError("seata.mode=distributed requires a compatible Seata Python SDK")
117
+ if not self.application_id:
118
+ raise RuntimeError("seata.application_id is required in distributed mode")
119
+ try:
120
+ # 设置Seata配置环境变量
121
+ import os
122
+ os.environ.setdefault('SEATA_IP', self.server_addr.split(':')[0])
123
+ os.environ.setdefault('SEATA_PORT', self.server_addr.split(':')[1] if ':' in self.server_addr else '8091')
124
+ os.environ.setdefault('SEATA_APPLICATION_ID', self.application_id)
125
+ os.environ.setdefault('SEATA_TX_GROUP', self.transaction_group)
126
+
127
+ logger.info(f"[Seata] Initializing Seata client with server: {self.server_addr}, application_id: {self.application_id}")
128
+
129
+ if hasattr(seata, 'init'):
130
+ seata.init()
131
+ elif hasattr(seata, 'config') and hasattr(seata.config, 'init'):
132
+ seata.config.init()
133
+ else:
134
+ raise RuntimeError("installed Seata SDK does not expose a supported init API")
135
+
136
+ self._seata_client_initialized = True
137
+ logger.info("[Seata] Client initialized successfully in distributed mode")
138
+ except Exception as e:
139
+ self._seata_client_initialized = False
140
+ raise RuntimeError(f"failed to initialize distributed Seata client: {e}") from e
141
+
142
+ def begin_transaction(self, timeout: int = 60000, name: str = "") -> str:
143
+ """
144
+ 开启分布式事务
145
+
146
+ 支持三种模式:
147
+ - local: 仅追踪事务上下文
148
+ - http: 实验性 HTTP 补偿模式,生成本地 XID,提交/回滚通过 HTTP 通知分支
149
+ - distributed: 使用真实Seata Server
150
+
151
+ Args:
152
+ timeout: 事务超时时间(毫秒)
153
+ name: 事务名称
154
+
155
+ Returns:
156
+ 事务ID (XID)
157
+ """
158
+ # 检查是否已经在事务中
159
+ context = self._get_context()
160
+ if context.get('in_transaction', False):
161
+ logger.warning("Nested transaction detected, returning current tx_id")
162
+ return context.get('tx_id', "")
163
+
164
+ # 生成事务ID
165
+ tx_id = uuid.uuid4().hex
166
+
167
+ # 设置事务上下文
168
+ self._transaction_context.set({
169
+ 'in_transaction': True,
170
+ 'tx_id': tx_id,
171
+ 'status': 'BEGIN',
172
+ 'timeout': timeout,
173
+ 'start_time': time.time(),
174
+ 'name': name,
175
+ })
176
+
177
+ # 实验性 HTTP 补偿模式:注册进程内全局事务
178
+ if self.mode == "http":
179
+ with self._gt_lock:
180
+ self._global_transactions[tx_id] = {
181
+ 'xid': tx_id,
182
+ 'name': name,
183
+ 'status': 'BEGIN',
184
+ 'start_time': time.time(),
185
+ 'timeout': timeout,
186
+ }
187
+ self._branches[tx_id] = []
188
+ logger.info(f"[Seata-HTTP] Begin global transaction: {tx_id}")
189
+
190
+ elif self.mode == "distributed":
191
+ if not (_seata_available and self._seata_client_initialized):
192
+ self._cleanup_context()
193
+ raise RuntimeError("distributed Seata client is not initialized")
194
+ try:
195
+ GlobalTransaction.begin(timeout, name)
196
+ seata_tx_id = RootContext.getXID()
197
+ if not seata_tx_id:
198
+ raise RuntimeError("Seata transaction began without an XID")
199
+ tx_id = seata_tx_id
200
+ self._set_context(tx_id=tx_id)
201
+ logger.info(f"[Seata] Begin global transaction (distributed): {tx_id}")
202
+ return tx_id
203
+ except Exception as e:
204
+ self._cleanup_context()
205
+ raise RuntimeError(f"failed to begin distributed Seata transaction: {e}") from e
206
+
207
+ else:
208
+ logger.info(f"[Seata] Begin transaction (local context): {tx_id}")
209
+
210
+ return tx_id
211
+
212
+ def register_branch(self, xid: str, branch_id: str = "", resource_id: str = "",
213
+ callback_url: str = "", commit_cb: Callable = None,
214
+ rollback_cb: Callable = None, service_name: str = "") -> str:
215
+ """
216
+ 注册分支事务(实验性 HTTP 补偿模式)
217
+
218
+ Args:
219
+ xid: 全局事务ID
220
+ branch_id: 分支ID(自动生成如果为空)
221
+ resource_id: 资源标识(如数据库表名)
222
+ callback_url: 远程回调URL(用于跨服务调用),如 http://order-service/seata/branch
223
+ commit_cb: 本地提交回调函数
224
+ rollback_cb: 本地回滚回调函数
225
+ service_name: 服务名
226
+
227
+ Returns:
228
+ branch_id
229
+ """
230
+ if not branch_id:
231
+ branch_id = uuid.uuid4().hex[:16]
232
+
233
+ if self.mode == 'http':
234
+ with self._gt_lock:
235
+ if xid not in self._global_transactions:
236
+ raise ValueError(f"Unknown or completed experimental HTTP transaction: {xid}")
237
+
238
+ branch = {
239
+ 'branch_id': branch_id,
240
+ 'xid': xid,
241
+ 'resource_id': resource_id,
242
+ 'callback_url': callback_url,
243
+ 'service_name': service_name or self.application_id,
244
+ 'status': BranchStatus.REGISTERED,
245
+ 'registered_at': time.time(),
246
+ }
247
+
248
+ with self._gt_lock:
249
+ if xid not in self._branches:
250
+ self._branches[xid] = []
251
+ self._branches[xid].append(branch)
252
+
253
+ # 注册本地回调
254
+ if commit_cb or rollback_cb:
255
+ with self._cb_lock:
256
+ self._branch_callbacks[branch_id] = {
257
+ 'commit': commit_cb,
258
+ 'rollback': rollback_cb,
259
+ }
260
+
261
+ logger.info(f"[Seata-HTTP] Branch registered: xid={xid[:16]}... branch_id={branch_id[:16]}... "
262
+ f"service={service_name} url={callback_url}")
263
+ return branch_id
264
+
265
+ def _notify_branch(self, branch: dict, action: str) -> bool:
266
+ """通知分支事务提交或回滚"""
267
+ branch_id = branch['branch_id']
268
+ # 本地回调优先
269
+ with self._cb_lock:
270
+ cb = self._branch_callbacks.get(branch_id)
271
+ if cb:
272
+ fn = cb.get('commit') if action == 'commit' else cb.get('rollback')
273
+ if fn:
274
+ try:
275
+ fn(branch['xid'], branch_id)
276
+ return True
277
+ except Exception as e:
278
+ logger.error(f"[Seata-HTTP] Local branch {action} failed: {e}")
279
+ return False
280
+
281
+ # HTTP回调
282
+ url = branch.get('callback_url')
283
+ if url:
284
+ try:
285
+ full_url = f"{url.rstrip('/')}/{branch_id}/{action}"
286
+ req = urlrequest.Request(full_url, method='POST',
287
+ data=json.dumps({'xid': branch['xid'], 'branchId': branch_id}).encode(),
288
+ headers={'Content-Type': 'application/json'})
289
+ resp = urlrequest.urlopen(req, timeout=5)
290
+ return resp.status == 200
291
+ except Exception as e:
292
+ logger.error(f"[Seata-HTTP] HTTP branch {action} failed for {branch_id[:16]}: {e}")
293
+ return False
294
+ logger.error(
295
+ f"[Seata-HTTP] Branch {branch_id[:16]} has no {action} callback; failing closed"
296
+ )
297
+ return False
298
+
299
+ def commit_transaction(self, tx_id: str) -> bool:
300
+ """
301
+ 提交分布式事务
302
+
303
+ Args:
304
+ tx_id: 事务ID
305
+
306
+ Returns:
307
+ 是否成功
308
+ """
309
+ try:
310
+ context = self._get_context()
311
+ current_tx_id = context.get('tx_id', "")
312
+ if current_tx_id and current_tx_id != tx_id:
313
+ logger.error(f"Transaction mismatch: expected {tx_id}, got {current_tx_id}")
314
+ return False
315
+
316
+ start_time = context.get('start_time', 0)
317
+ timeout = context.get('timeout', 60000)
318
+ duration = (time.time() - start_time) * 1000
319
+
320
+ if duration > timeout:
321
+ logger.error(f"Transaction timeout: {duration}ms > {timeout}ms")
322
+ self.rollback_transaction(tx_id)
323
+ return False
324
+
325
+ # 实验性 HTTP 补偿模式:通知所有分支提交
326
+ if self.mode == "http":
327
+ all_ok = True
328
+ with self._gt_lock:
329
+ branches = list(self._branches.get(tx_id, []))
330
+ transaction = self._global_transactions.get(tx_id)
331
+ if transaction is None:
332
+ logger.error(f"[Seata-HTTP] Unknown transaction: {tx_id}")
333
+ return False
334
+ transaction['status'] = 'COMMITTING'
335
+ for branch in branches:
336
+ ok = self._notify_branch(branch, 'commit')
337
+ branch['status'] = BranchStatus.COMMITTED if ok else BranchStatus.FAILED
338
+ if not ok:
339
+ all_ok = False
340
+ with self._gt_lock:
341
+ self._global_transactions[tx_id]['status'] = 'COMMITTED' if all_ok else 'PARTIAL_COMMIT'
342
+ if all_ok:
343
+ self._cleanup_http_transaction(tx_id)
344
+ logger.info(f"[Seata-HTTP] Commit transaction {tx_id[:16]}... branches={len(branches)} success={all_ok}")
345
+ return all_ok
346
+
347
+ # 分布式模式
348
+ if self.mode == "distributed":
349
+ if not (_seata_available and self._seata_client_initialized):
350
+ logger.error("Distributed Seata client is not initialized; commit rejected")
351
+ return False
352
+ try:
353
+ GlobalTransaction.commit()
354
+ self._set_context(status='COMMITTED')
355
+ logger.info(f"[Seata] Commit global transaction: {tx_id}, duration={duration:.2f}ms")
356
+ return True
357
+ except Exception as e:
358
+ logger.error(f"[Seata] Failed to commit global transaction: {e}. Rolling back...")
359
+ self.rollback_transaction(tx_id)
360
+ return False
361
+
362
+ # 本地模式
363
+ self._set_context(status='COMMITTED')
364
+ logger.info(f"[Seata] Commit transaction (local): {tx_id}, duration={duration:.2f}ms")
365
+ return True
366
+ finally:
367
+ self._cleanup_context()
368
+
369
+ def rollback_transaction(self, tx_id: str) -> bool:
370
+ """
371
+ 回滚分布式事务
372
+
373
+ Args:
374
+ tx_id: 事务ID
375
+
376
+ Returns:
377
+ 是否成功
378
+ """
379
+ try:
380
+ current_tx_id = self._get_context().get('tx_id', "")
381
+ if current_tx_id and current_tx_id != tx_id:
382
+ logger.error(f"Transaction mismatch: expected {tx_id}, got {current_tx_id}")
383
+ return False
384
+
385
+ # 实验性 HTTP 补偿模式:通知所有分支回滚
386
+ if self.mode == "http":
387
+ all_ok = True
388
+ with self._gt_lock:
389
+ branches = list(self._branches.get(tx_id, []))
390
+ if tx_id in self._global_transactions:
391
+ self._global_transactions[tx_id]['status'] = 'ROLLING_BACK'
392
+ for branch in branches:
393
+ ok = self._notify_branch(branch, 'rollback')
394
+ branch['status'] = BranchStatus.ROLLED_BACK if ok else BranchStatus.FAILED
395
+ if not ok:
396
+ all_ok = False
397
+ with self._gt_lock:
398
+ if tx_id in self._global_transactions:
399
+ self._global_transactions[tx_id]['status'] = 'ROLLED_BACK' if all_ok else 'PARTIAL_ROLLBACK'
400
+ if all_ok:
401
+ self._cleanup_http_transaction(tx_id)
402
+ logger.info(f"[Seata-HTTP] Rollback transaction {tx_id[:16]}... branches={len(branches)} success={all_ok}")
403
+ return all_ok
404
+
405
+ # 分布式模式
406
+ if self.mode == "distributed":
407
+ if not (_seata_available and self._seata_client_initialized):
408
+ logger.error("Distributed Seata client is not initialized; rollback failed")
409
+ return False
410
+ try:
411
+ GlobalTransaction.rollback()
412
+ self._set_context(status='ROLLED_BACK')
413
+ logger.info(f"[Seata] Rollback global transaction: {tx_id}")
414
+ return True
415
+ except Exception as e:
416
+ logger.error(f"[Seata] Failed to rollback global transaction: {e}")
417
+ return False
418
+
419
+ # 本地模式
420
+ self._set_context(status='ROLLED_BACK')
421
+ logger.info(f"[Seata] Rollback transaction (local): {tx_id}")
422
+ return True
423
+ finally:
424
+ self._cleanup_context()
425
+
426
+ def _cleanup_context(self):
427
+ """清理事务上下文"""
428
+ self._transaction_context.set(None)
429
+
430
+ # 清理Seata上下文
431
+ if _seata_available and RootContext:
432
+ try:
433
+ RootContext.unbindXID()
434
+ except Exception:
435
+ pass
436
+
437
+ def _cleanup_http_transaction(self, tx_id: str) -> None:
438
+ """成功完成后移除协调状态和本地回调,避免长期进程内存增长。"""
439
+ with self._gt_lock:
440
+ branches = self._branches.pop(tx_id, [])
441
+ self._global_transactions.pop(tx_id, None)
442
+ with self._cb_lock:
443
+ for branch in branches:
444
+ self._branch_callbacks.pop(branch['branch_id'], None)
445
+
446
+ def is_in_transaction(self) -> bool:
447
+ """检查是否在事务中"""
448
+ return bool(self._get_context().get('in_transaction', False))
449
+
450
+ def get_current_tx_id(self) -> str:
451
+ """获取当前事务ID"""
452
+ # 优先从Seata获取
453
+ if _seata_available and RootContext:
454
+ try:
455
+ seata_tx_id = RootContext.getXID()
456
+ if seata_tx_id:
457
+ return seata_tx_id
458
+ except Exception:
459
+ pass
460
+
461
+ return self._get_context().get('tx_id', "")
462
+
463
+ def get_transaction_status(self) -> str:
464
+ """获取当前事务状态"""
465
+ return self._get_context().get('status', "NONE")
466
+
467
+ def get_mode(self) -> str:
468
+ """获取当前事务模式"""
469
+ return self.mode
470
+
471
+ def set_mode(self, mode: str):
472
+ """设置事务模式 (local/http/distributed)"""
473
+ if mode not in ["local", "http", "distributed"]:
474
+ raise ValueError("Mode must be 'local', 'http' or 'distributed'")
475
+
476
+ if mode == "distributed":
477
+ # 即使初始化失败也保持 distributed,后续事务必须失败关闭,
478
+ # 不能沿用之前的 local 模式继续执行核心业务。
479
+ self.mode = mode
480
+ if not self._seata_client_initialized:
481
+ self._init_seata_client()
482
+ return
483
+ self.mode = mode
484
+
485
+ def get_transaction_info(self) -> Dict[str, Any]:
486
+ """获取当前事务信息(用于调试/监控)"""
487
+ with self._gt_lock:
488
+ return {
489
+ 'mode': self.mode,
490
+ 'active_global_tx': len(self._global_transactions),
491
+ 'active_branches': sum(len(b) for b in self._branches.values()),
492
+ 'in_transaction': self.is_in_transaction(),
493
+ 'current_xid': self.get_current_tx_id(),
494
+ }
495
+
496
+ @staticmethod
497
+ def get_xid_from_headers(headers: Dict[str, str]) -> str:
498
+ """从HTTP请求头中提取XID"""
499
+ if not headers:
500
+ return ""
501
+ return (headers.get('X-TX-XID') or headers.get('X-Seata-XID') or
502
+ headers.get('x-tx-xid') or headers.get('x-seata-xid') or "")
503
+
504
+ @staticmethod
505
+ def inject_xid_headers(headers: Dict[str, str], xid: str) -> Dict[str, str]:
506
+ """将XID注入到HTTP请求头(供Feign使用)"""
507
+ if xid:
508
+ headers['X-TX-XID'] = xid
509
+ headers['X-Seata-XID'] = xid
510
+ return headers
511
+
512
+
513
+ # 创建全局Seata事务管理器实例
514
+ seata_manager = SeataTransactionManager()
515
+
516
+
517
+ def init_seata(config: dict) -> None:
518
+ """
519
+ 初始化Seata配置
520
+
521
+ Args:
522
+ config: 配置字典,包含server_addr, application_id, transaction_group, mode等
523
+
524
+ 配置说明:
525
+ server_addr: Seata Server地址,默认 localhost:8091
526
+ application_id: 应用ID,必填(分布式模式)
527
+ transaction_group: 事务分组,默认 my_tx_group
528
+ mode: 事务模式,可选 'local'(默认)或 'distributed'
529
+
530
+ 分布式模式要求:
531
+ 1. 部署Seata Server
532
+ 2. 创建seata_undo_log表(MySQL示例):
533
+ CREATE TABLE IF NOT EXISTS `seata_undo_log` (
534
+ `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
535
+ `branch_id` BIGINT(20) NOT NULL COMMENT '分支事务ID',
536
+ `xid` VARCHAR(100) NOT NULL COMMENT '全局事务ID',
537
+ `context` VARCHAR(128) NOT NULL COMMENT '上下文',
538
+ `rollback_info` LONGBLOB NOT NULL COMMENT '回滚信息',
539
+ `log_status` INT(11) NOT NULL COMMENT '状态',
540
+ `log_created` DATETIME NOT NULL COMMENT '创建时间',
541
+ `log_modified` DATETIME NOT NULL COMMENT '修改时间',
542
+ PRIMARY KEY (`id`),
543
+ UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
544
+ ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='Seata回滚日志表';
545
+ """
546
+ mode = str(config.get('mode', 'local')).lower()
547
+ if mode == 'http' and not config.get('experimental_http_enabled', False):
548
+ raise ValueError(
549
+ "seata.mode=http is an experimental best-effort compensation mode; "
550
+ "set seata.experimental_http_enabled=true only for development tests"
551
+ )
552
+ seata_manager.configure(
553
+ server_addr=config.get('server_addr', 'localhost:8091'),
554
+ application_id=config.get('application_id', ''),
555
+ transaction_group=config.get('transaction_group', 'my_tx_group'),
556
+ mode=mode,
557
+ )