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.
- spring/__init__.py +66 -0
- spring/ai/__init__.py +78 -0
- spring/ai/advisors.py +139 -0
- spring/ai/annotations.py +74 -0
- spring/ai/autoconfig.py +481 -0
- spring/ai/core.py +391 -0
- spring/ai/etl.py +188 -0
- spring/ai/memory.py +109 -0
- spring/ai/observability.py +129 -0
- spring/ai/providers.py +789 -0
- spring/ai/resilience.py +258 -0
- spring/ai/tools.py +106 -0
- spring/ai/vectorstore.py +303 -0
- spring/annotations/__init__.py +188 -0
- spring/annotations/cache.py +126 -0
- spring/annotations/cloud.py +207 -0
- spring/annotations/conditional.py +272 -0
- spring/annotations/core.py +864 -0
- spring/annotations/messaging.py +107 -0
- spring/aop/__init__.py +4 -0
- spring/aop/cloud_aop.py +404 -0
- spring/aop/comprehensive_aop.py +1015 -0
- spring/aop/method_interceptor.py +19 -0
- spring/aop/proxy_factory.py +55 -0
- spring/cloud/__init__.py +76 -0
- spring/cloud/discovery.py +364 -0
- spring/cloud/feign.py +469 -0
- spring/cloud/gateway.py +452 -0
- spring/cloud/load_balancer.py +149 -0
- spring/cloud/seata.py +557 -0
- spring/cloud/sentinel.py +525 -0
- spring/cloud/tracer.py +337 -0
- spring/config/__init__.py +21 -0
- spring/config/binding.py +206 -0
- spring/config/config_loader.py +405 -0
- spring/context/__init__.py +13 -0
- spring/context/application_context.py +589 -0
- spring/context/bean_definition.py +70 -0
- spring/context/bean_factory.py +1052 -0
- spring/context/registry.py +58 -0
- spring/context/scanner.py +106 -0
- spring/core/__init__.py +3 -0
- spring/core/graceful_shutdown.py +196 -0
- spring/core/typing_utils.py +50 -0
- spring/csv/__init__.py +52 -0
- spring/csv/annotations.py +402 -0
- spring/csv/converters.py +69 -0
- spring/csv/easy_csv.py +95 -0
- spring/csv/exceptions.py +27 -0
- spring/csv/reader.py +195 -0
- spring/csv/writer.py +155 -0
- spring/data/__init__.py +54 -0
- spring/data/page.py +181 -0
- spring/data/repository.py +274 -0
- spring/data/specification.py +228 -0
- spring/datasource/__init__.py +66 -0
- spring/datasource/annotations.py +133 -0
- spring/datasource/context.py +69 -0
- spring/datasource/dynamic.py +148 -0
- spring/event/__init__.py +7 -0
- spring/event/publisher.py +69 -0
- spring/excel/__init__.py +51 -0
- spring/excel/annotations.py +405 -0
- spring/excel/converters.py +231 -0
- spring/excel/easy_excel.py +94 -0
- spring/excel/exceptions.py +31 -0
- spring/excel/reader.py +254 -0
- spring/excel/style.py +95 -0
- spring/excel/writer.py +197 -0
- spring/i18n/__init__.py +97 -0
- spring/i18n/accessor.py +94 -0
- spring/i18n/auto_config.py +177 -0
- spring/i18n/holder.py +106 -0
- spring/i18n/locale.py +152 -0
- spring/i18n/locale_resolver.py +367 -0
- spring/i18n/message_source.py +250 -0
- spring/i18n/middleware.py +79 -0
- spring/i18n/properties.py +168 -0
- spring/i18n/sources.py +255 -0
- spring/logging/__init__.py +1 -0
- spring/logging/loguru_logger.py +228 -0
- spring/main.py +378 -0
- spring/messaging/__init__.py +1 -0
- spring/messaging/rabbitmq.py +302 -0
- spring/monitoring/__init__.py +1 -0
- spring/monitoring/prometheus.py +199 -0
- spring/orm/__init__.py +258 -0
- spring/orm/database.py +222 -0
- spring/orm/ddl_auto.py +1217 -0
- spring/orm/migration.py +419 -0
- spring/orm/mybatis_integration.py +400 -0
- spring/orm/pymybatis/__init__.py +86 -0
- spring/orm/pymybatis/annotations/__init__.py +30 -0
- spring/orm/pymybatis/annotations/annotations.py +332 -0
- spring/orm/pymybatis/cache/__init__.py +47 -0
- spring/orm/pymybatis/cache/cache.py +371 -0
- spring/orm/pymybatis/cache/redis_cache.py +434 -0
- spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
- spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
- spring/orm/pymybatis/configuration.py +525 -0
- spring/orm/pymybatis/core/__init__.py +10 -0
- spring/orm/pymybatis/core/sql_session.py +1382 -0
- spring/orm/pymybatis/core/sql_session_factory.py +76 -0
- spring/orm/pymybatis/dialect/__init__.py +9 -0
- spring/orm/pymybatis/dialect/dialect.py +445 -0
- spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
- spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
- spring/orm/pymybatis/interceptor/__init__.py +31 -0
- spring/orm/pymybatis/interceptor/interceptor.py +427 -0
- spring/orm/pymybatis/mapper/__init__.py +9 -0
- spring/orm/pymybatis/mapper/mapper.py +540 -0
- spring/orm/pymybatis/metrics/__init__.py +41 -0
- spring/orm/pymybatis/metrics/metrics.py +595 -0
- spring/orm/pymybatis/pool/__init__.py +9 -0
- spring/orm/pymybatis/pool/connection_pool.py +711 -0
- spring/orm/pymybatis/security/__init__.py +19 -0
- spring/orm/pymybatis/security/access_control.py +415 -0
- spring/orm/pymybatis/security/password_encoder.py +293 -0
- spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
- spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
- spring/orm/pymybatis/transaction/__init__.py +9 -0
- spring/orm/pymybatis/transaction/transaction.py +288 -0
- spring/orm/pymybatis/type_handler/__init__.py +37 -0
- spring/orm/pymybatis/type_handler/type_handler.py +473 -0
- spring/orm/pymybatis/version.py +9 -0
- spring/orm/pymybatis/xml_parser/__init__.py +9 -0
- spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
- spring/retry/__init__.py +12 -0
- spring/retry/retry_annotations.py +71 -0
- spring/retry/retry_decorator.py +155 -0
- spring/scheduling/__init__.py +3 -0
- spring/scheduling/scheduler.py +389 -0
- spring/security/__init__.py +39 -0
- spring/security/jwt_utils.py +281 -0
- spring/security/replay_protection.py +206 -0
- spring/security/secret_manager.py +226 -0
- spring/security/security_aop.py +248 -0
- spring/security/security_context.py +172 -0
- spring/test/__init__.py +45 -0
- spring/test/slicing.py +341 -0
- spring/tracing/__init__.py +11 -0
- spring/tracing/skywalking.py +229 -0
- spring/tx/__init__.py +52 -0
- spring/tx/events.py +172 -0
- spring/tx/synchronization.py +143 -0
- spring/utils/__init__.py +5 -0
- spring/utils/banner.py +32 -0
- spring/utils/logger.py +73 -0
- spring/utils/redis_client.py +526 -0
- spring/validation/__init__.py +55 -0
- spring/validation/aop.py +141 -0
- spring/validation/constraints.py +357 -0
- spring/validation/exceptions.py +55 -0
- spring/validation/validator.py +139 -0
- spring/web/__init__.py +12 -0
- spring/web/actuator.py +319 -0
- spring/web/exception_handler.py +61 -0
- spring/web/health.py +399 -0
- spring/web/interceptor.py +91 -0
- spring/web/result.py +44 -0
- spring/web/swagger.py +601 -0
- spring/web/web_context.py +755 -0
- spring/websocket/__init__.py +86 -0
- spring/websocket/annotations.py +169 -0
- spring/websocket/broker.py +238 -0
- spring/websocket/exceptions.py +26 -0
- spring/websocket/handler.py +243 -0
- spring/websocket/router.py +526 -0
- spring/websocket/session.py +216 -0
- springbootai-1.8.0.dist-info/METADATA +2796 -0
- springbootai-1.8.0.dist-info/RECORD +175 -0
- springbootai-1.8.0.dist-info/WHEEL +5 -0
- springbootai-1.8.0.dist-info/entry_points.txt +2 -0
- springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
- springbootai-1.8.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Redis客户端工具类
|
|
3
|
+
提供分布式锁、持久化存储等企业级功能
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from redis.exceptions import RedisError
|
|
12
|
+
except ImportError:
|
|
13
|
+
class RedisError(Exception):
|
|
14
|
+
"""Fallback used when the optional Redis dependency is unavailable."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RedisClient:
|
|
18
|
+
"""Redis客户端封装"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, host: str = 'localhost', port: int = 6379, db: int = 0, password: str = None):
|
|
21
|
+
self._client = None
|
|
22
|
+
self.configure(host=host, port=port, db=db, password=password)
|
|
23
|
+
|
|
24
|
+
def configure(self, host: str, port: int, db: int, password: str = None) -> None:
|
|
25
|
+
self.host = host
|
|
26
|
+
self.port = int(port)
|
|
27
|
+
self.db = int(db)
|
|
28
|
+
self.password = password
|
|
29
|
+
self._client = None
|
|
30
|
+
|
|
31
|
+
def connect(self, strict: bool = False) -> None:
|
|
32
|
+
"""连接Redis"""
|
|
33
|
+
try:
|
|
34
|
+
from redis import Redis
|
|
35
|
+
|
|
36
|
+
self._client = Redis(
|
|
37
|
+
host=self.host,
|
|
38
|
+
port=self.port,
|
|
39
|
+
db=self.db,
|
|
40
|
+
password=self.password,
|
|
41
|
+
decode_responses=True,
|
|
42
|
+
socket_timeout=5,
|
|
43
|
+
socket_connect_timeout=5
|
|
44
|
+
)
|
|
45
|
+
# 测试连接
|
|
46
|
+
self._client.ping()
|
|
47
|
+
except ImportError as exc:
|
|
48
|
+
self._client = None
|
|
49
|
+
if strict:
|
|
50
|
+
raise RuntimeError("Redis已启用但redis依赖未安装") from exc
|
|
51
|
+
except Exception as e:
|
|
52
|
+
self._client = None
|
|
53
|
+
if strict:
|
|
54
|
+
raise ConnectionError(f"无法连接Redis: {e}") from e
|
|
55
|
+
|
|
56
|
+
def get_client(self):
|
|
57
|
+
"""获取Redis客户端"""
|
|
58
|
+
if self._client is None:
|
|
59
|
+
self.connect()
|
|
60
|
+
return self._client
|
|
61
|
+
|
|
62
|
+
# ==================== 分布式锁 ====================
|
|
63
|
+
|
|
64
|
+
def acquire_lock(self, key: str, timeout: int = 10, wait_timeout: int = 5):
|
|
65
|
+
"""
|
|
66
|
+
获取分布式锁
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
key: 锁键
|
|
70
|
+
timeout: 锁过期时间(秒)
|
|
71
|
+
wait_timeout: 等待锁的超时时间(秒)
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
锁标识(用于释放锁),获取失败返回None
|
|
75
|
+
"""
|
|
76
|
+
client = self.get_client()
|
|
77
|
+
if client is None:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
lock_id = str(uuid.uuid4())
|
|
81
|
+
end_time = time.time() + wait_timeout
|
|
82
|
+
|
|
83
|
+
while time.time() < end_time:
|
|
84
|
+
# 使用SET NX EX命令获取锁
|
|
85
|
+
result = client.set(f"lock:{key}", lock_id, nx=True, ex=timeout)
|
|
86
|
+
if result:
|
|
87
|
+
return lock_id
|
|
88
|
+
time.sleep(0.01) # 短暂等待后重试
|
|
89
|
+
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
def release_lock(self, key: str, lock_id: str) -> bool:
|
|
93
|
+
"""
|
|
94
|
+
释放分布式锁
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
key: 锁键
|
|
98
|
+
lock_id: 锁标识
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
是否成功释放
|
|
102
|
+
"""
|
|
103
|
+
client = self.get_client()
|
|
104
|
+
if client is None:
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
# 使用Lua脚本保证原子性释放
|
|
108
|
+
script = """
|
|
109
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
110
|
+
return redis.call("del", KEYS[1])
|
|
111
|
+
else
|
|
112
|
+
return 0
|
|
113
|
+
end
|
|
114
|
+
"""
|
|
115
|
+
result = client.eval(script, 1, f"lock:{key}", lock_id)
|
|
116
|
+
return result == 1
|
|
117
|
+
|
|
118
|
+
# ==================== 持久化存储 ====================
|
|
119
|
+
|
|
120
|
+
def set_value(self, key: str, value: Any, expire: int = None) -> bool:
|
|
121
|
+
"""
|
|
122
|
+
设置值
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
key: 键
|
|
126
|
+
value: 值(支持任意可JSON序列化的类型)
|
|
127
|
+
expire: 过期时间(秒)
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
是否成功
|
|
131
|
+
"""
|
|
132
|
+
client = self.get_client()
|
|
133
|
+
if client is None:
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
if isinstance(value, (str, int, float, bool)):
|
|
138
|
+
result = client.set(key, value, ex=expire)
|
|
139
|
+
else:
|
|
140
|
+
result = client.set(key, json.dumps(value), ex=expire)
|
|
141
|
+
return result is not None
|
|
142
|
+
except (RedisError, TypeError):
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
def get_value(self, key: str) -> Any:
|
|
146
|
+
"""
|
|
147
|
+
获取值
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
key: 键
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
值(自动反序列化)
|
|
154
|
+
"""
|
|
155
|
+
client = self.get_client()
|
|
156
|
+
if client is None:
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
value = client.get(key)
|
|
161
|
+
if value is None:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
# 尝试解析为JSON
|
|
165
|
+
try:
|
|
166
|
+
return json.loads(value)
|
|
167
|
+
except (json.JSONDecodeError, TypeError):
|
|
168
|
+
return value
|
|
169
|
+
except RedisError:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def delete_key(self, key: str) -> bool:
|
|
173
|
+
"""
|
|
174
|
+
删除键
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
key: 键
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
是否成功
|
|
181
|
+
"""
|
|
182
|
+
client = self.get_client()
|
|
183
|
+
if client is None:
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
result = client.delete(key)
|
|
188
|
+
return result > 0
|
|
189
|
+
except RedisError:
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
def exists_key(self, key: str) -> bool:
|
|
193
|
+
"""
|
|
194
|
+
检查键是否存在
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
key: 键
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
是否存在
|
|
201
|
+
"""
|
|
202
|
+
client = self.get_client()
|
|
203
|
+
if client is None:
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
return client.exists(key) > 0
|
|
208
|
+
except RedisError:
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
# ==================== 列表操作 ====================
|
|
212
|
+
|
|
213
|
+
def list_push(self, key: str, value: Any) -> int:
|
|
214
|
+
"""
|
|
215
|
+
向列表尾部添加元素
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
key: 键
|
|
219
|
+
value: 值
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
列表长度
|
|
223
|
+
"""
|
|
224
|
+
client = self.get_client()
|
|
225
|
+
if client is None:
|
|
226
|
+
return 0
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
if not isinstance(value, str):
|
|
230
|
+
value = json.dumps(value)
|
|
231
|
+
return client.rpush(key, value)
|
|
232
|
+
except (RedisError, TypeError):
|
|
233
|
+
return 0
|
|
234
|
+
|
|
235
|
+
def list_range(self, key: str, start: int = 0, end: int = -1) -> list:
|
|
236
|
+
"""
|
|
237
|
+
获取列表指定范围的元素
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
key: 键
|
|
241
|
+
start: 起始索引
|
|
242
|
+
end: 结束索引
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
元素列表
|
|
246
|
+
"""
|
|
247
|
+
client = self.get_client()
|
|
248
|
+
if client is None:
|
|
249
|
+
return []
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
values = client.lrange(key, start, end)
|
|
253
|
+
result = []
|
|
254
|
+
for v in values:
|
|
255
|
+
try:
|
|
256
|
+
result.append(json.loads(v))
|
|
257
|
+
except (json.JSONDecodeError, TypeError):
|
|
258
|
+
result.append(v)
|
|
259
|
+
return result
|
|
260
|
+
except RedisError:
|
|
261
|
+
return []
|
|
262
|
+
|
|
263
|
+
def list_remove_range(self, key: str, start: int, end: int) -> int:
|
|
264
|
+
"""
|
|
265
|
+
删除列表指定范围的元素
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
key: 键
|
|
269
|
+
start: 起始索引
|
|
270
|
+
end: 结束索引
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
删除的元素数量
|
|
274
|
+
"""
|
|
275
|
+
client = self.get_client()
|
|
276
|
+
if client is None:
|
|
277
|
+
return 0
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
# 获取列表长度
|
|
281
|
+
length = client.llen(key)
|
|
282
|
+
if length == 0:
|
|
283
|
+
return 0
|
|
284
|
+
|
|
285
|
+
# 计算需要保留的元素
|
|
286
|
+
result = 0
|
|
287
|
+
# 删除从start到end的元素(通过截断实现)
|
|
288
|
+
if start > 0:
|
|
289
|
+
# 保留前start个元素
|
|
290
|
+
client.ltrim(key, 0, start - 1)
|
|
291
|
+
result = length - start
|
|
292
|
+
elif end < length - 1:
|
|
293
|
+
# 保留从end+1开始的元素
|
|
294
|
+
client.ltrim(key, end + 1, -1)
|
|
295
|
+
result = end + 1
|
|
296
|
+
|
|
297
|
+
return result
|
|
298
|
+
except RedisError:
|
|
299
|
+
return 0
|
|
300
|
+
|
|
301
|
+
def list_length(self, key: str) -> int:
|
|
302
|
+
"""
|
|
303
|
+
获取列表长度
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
key: 键
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
列表长度
|
|
310
|
+
"""
|
|
311
|
+
client = self.get_client()
|
|
312
|
+
if client is None:
|
|
313
|
+
return 0
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
return client.llen(key)
|
|
317
|
+
except RedisError:
|
|
318
|
+
return 0
|
|
319
|
+
|
|
320
|
+
# ==================== 计数器操作 ====================
|
|
321
|
+
|
|
322
|
+
def increment(self, key: str, amount: int = 1) -> int:
|
|
323
|
+
"""
|
|
324
|
+
递增计数器
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
key: 键
|
|
328
|
+
amount: 递增值
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
递增后的值
|
|
332
|
+
"""
|
|
333
|
+
client = self.get_client()
|
|
334
|
+
if client is None:
|
|
335
|
+
return 0
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
return client.incrby(key, amount)
|
|
339
|
+
except RedisError:
|
|
340
|
+
return 0
|
|
341
|
+
|
|
342
|
+
def decrement(self, key: str, amount: int = 1) -> int:
|
|
343
|
+
"""
|
|
344
|
+
递减计数器
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
key: 键
|
|
348
|
+
amount: 递减值
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
递减后的值
|
|
352
|
+
"""
|
|
353
|
+
client = self.get_client()
|
|
354
|
+
if client is None:
|
|
355
|
+
return 0
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
return client.decrby(key, amount)
|
|
359
|
+
except RedisError:
|
|
360
|
+
return 0
|
|
361
|
+
|
|
362
|
+
# ==================== Hash操作 ====================
|
|
363
|
+
|
|
364
|
+
def hash_set(self, key: str, field: str, value: Any) -> bool:
|
|
365
|
+
"""
|
|
366
|
+
设置Hash字段值
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
key: 键
|
|
370
|
+
field: 字段名
|
|
371
|
+
value: 值
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
是否成功
|
|
375
|
+
"""
|
|
376
|
+
client = self.get_client()
|
|
377
|
+
if client is None:
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
if not isinstance(value, str):
|
|
382
|
+
value = json.dumps(value)
|
|
383
|
+
return client.hset(key, field, value) > 0
|
|
384
|
+
except (RedisError, TypeError):
|
|
385
|
+
return False
|
|
386
|
+
|
|
387
|
+
def hash_get(self, key: str, field: str) -> Any:
|
|
388
|
+
"""
|
|
389
|
+
获取Hash字段值
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
key: 键
|
|
393
|
+
field: 字段名
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
值
|
|
397
|
+
"""
|
|
398
|
+
client = self.get_client()
|
|
399
|
+
if client is None:
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
value = client.hget(key, field)
|
|
404
|
+
if value is None:
|
|
405
|
+
return None
|
|
406
|
+
try:
|
|
407
|
+
return json.loads(value)
|
|
408
|
+
except (json.JSONDecodeError, TypeError):
|
|
409
|
+
return value
|
|
410
|
+
except RedisError:
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
def hash_get_all(self, key: str) -> dict:
|
|
414
|
+
"""
|
|
415
|
+
获取Hash所有字段和值
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
key: 键
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
字段-值字典
|
|
422
|
+
"""
|
|
423
|
+
client = self.get_client()
|
|
424
|
+
if client is None:
|
|
425
|
+
return {}
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
result = client.hgetall(key)
|
|
429
|
+
for field, value in list(result.items()):
|
|
430
|
+
try:
|
|
431
|
+
result[field] = json.loads(value)
|
|
432
|
+
except (json.JSONDecodeError, TypeError):
|
|
433
|
+
pass
|
|
434
|
+
return result
|
|
435
|
+
except RedisError:
|
|
436
|
+
return {}
|
|
437
|
+
|
|
438
|
+
def hash_delete(self, key: str, field: str) -> bool:
|
|
439
|
+
"""
|
|
440
|
+
删除Hash字段
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
key: 键
|
|
444
|
+
field: 字段名
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
是否成功
|
|
448
|
+
"""
|
|
449
|
+
client = self.get_client()
|
|
450
|
+
if client is None:
|
|
451
|
+
return False
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
return client.hdel(key, field) > 0
|
|
455
|
+
except RedisError:
|
|
456
|
+
return False
|
|
457
|
+
|
|
458
|
+
# ==================== 集合操作 ====================
|
|
459
|
+
|
|
460
|
+
def set_add(self, key: str, value: Any) -> bool:
|
|
461
|
+
"""
|
|
462
|
+
向集合添加元素
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
key: 键
|
|
466
|
+
value: 值
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
是否成功
|
|
470
|
+
"""
|
|
471
|
+
client = self.get_client()
|
|
472
|
+
if client is None:
|
|
473
|
+
return False
|
|
474
|
+
|
|
475
|
+
try:
|
|
476
|
+
if not isinstance(value, str):
|
|
477
|
+
value = json.dumps(value)
|
|
478
|
+
return client.sadd(key, value) > 0
|
|
479
|
+
except (RedisError, TypeError):
|
|
480
|
+
return False
|
|
481
|
+
|
|
482
|
+
def set_members(self, key: str) -> set:
|
|
483
|
+
"""
|
|
484
|
+
获取集合所有元素
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
key: 键
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
元素集合
|
|
491
|
+
"""
|
|
492
|
+
client = self.get_client()
|
|
493
|
+
if client is None:
|
|
494
|
+
return set()
|
|
495
|
+
|
|
496
|
+
try:
|
|
497
|
+
members = client.smembers(key)
|
|
498
|
+
result = set()
|
|
499
|
+
for m in members:
|
|
500
|
+
try:
|
|
501
|
+
result.add(json.loads(m))
|
|
502
|
+
except (json.JSONDecodeError, TypeError):
|
|
503
|
+
result.add(m)
|
|
504
|
+
return result
|
|
505
|
+
except RedisError:
|
|
506
|
+
return set()
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
# 创建全局Redis客户端实例
|
|
510
|
+
redis_client = RedisClient()
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def init_redis(config: dict) -> None:
|
|
514
|
+
"""
|
|
515
|
+
初始化Redis连接
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
config: Redis配置字典,包含host, port, db, password等
|
|
519
|
+
"""
|
|
520
|
+
redis_client.configure(
|
|
521
|
+
host=config.get('host', 'localhost'),
|
|
522
|
+
port=config.get('port', 6379),
|
|
523
|
+
db=config.get('db', 0),
|
|
524
|
+
password=config.get('password')
|
|
525
|
+
)
|
|
526
|
+
redis_client.connect(strict=True)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""SpringBootAI Bean Validation 模块 —— 字段约束注解 + 验证器 + 方法级 AOP。
|
|
2
|
+
|
|
3
|
+
对齐 Jakarta Bean Validation(Hibernate Validator)的核心能力:
|
|
4
|
+
- 字段级约束(``@NotNull``/``@NotBlank``/``@Size``/``@Min``/``@Max``/``@Pattern``/``@Email`` 等)
|
|
5
|
+
作为字段描述符,复用 ORM ``Column`` / Excel ``ExcelProperty`` 元数据范式。
|
|
6
|
+
- ``BeanValidator`` 反射收集约束并校验对象实例,返回 ``ConstraintViolation`` 列表。
|
|
7
|
+
- ``@BeanValidate`` 方法级 AOP 注解,接入既有 ``comprehensive_aop`` 分发链路,
|
|
8
|
+
受管 Bean 方法调用前自动校验参数对象。
|
|
9
|
+
|
|
10
|
+
模块组成:
|
|
11
|
+
- constraints: 字段约束注解(``Constraint`` 基类 + 14 个内置约束)
|
|
12
|
+
- validator: ``BeanValidator`` 反射校验器
|
|
13
|
+
- aop: ``@BeanValidate`` 方法级注解 + AOP 装饰器
|
|
14
|
+
- exceptions: ``ValidationError`` / ``ConstraintViolation``
|
|
15
|
+
|
|
16
|
+
设计原则:**复用项目既有范式,不重复造轮子**。约束描述符、反射收集、AOP 注册全部对齐
|
|
17
|
+
既有 ORM/Excel/综合 AOP 实现,未引入任何 Spring 风格第三方库。
|
|
18
|
+
|
|
19
|
+
与 Java 的差异(已标注):
|
|
20
|
+
- 仅支持字段级约束(Java 还支持方法参数级/返回值级标量约束);方法级通过 ``@BeanValidate``
|
|
21
|
+
对参数对象整体校验实现。
|
|
22
|
+
- 校验器为无状态静态方法风格,不依赖 IoC 容器即可独立使用。
|
|
23
|
+
"""
|
|
24
|
+
from .exceptions import ConstraintViolation, ValidationError
|
|
25
|
+
from .constraints import (
|
|
26
|
+
Constraint,
|
|
27
|
+
NotNull, NotBlank, NotEmpty,
|
|
28
|
+
Size,
|
|
29
|
+
Min, Max,
|
|
30
|
+
Positive, PositiveOrZero, Negative, NegativeOrZero,
|
|
31
|
+
Pattern, Email,
|
|
32
|
+
AssertTrue, AssertFalse,
|
|
33
|
+
)
|
|
34
|
+
from .validator import BeanValidator
|
|
35
|
+
from .aop import BeanValidate, bean_validate_decorator
|
|
36
|
+
|
|
37
|
+
__version__ = "1.0.0"
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
# 异常
|
|
41
|
+
"ConstraintViolation", "ValidationError",
|
|
42
|
+
# 约束
|
|
43
|
+
"Constraint",
|
|
44
|
+
"NotNull", "NotBlank", "NotEmpty",
|
|
45
|
+
"Size",
|
|
46
|
+
"Min", "Max",
|
|
47
|
+
"Positive", "PositiveOrZero", "Negative", "NegativeOrZero",
|
|
48
|
+
"Pattern", "Email",
|
|
49
|
+
"AssertTrue", "AssertFalse",
|
|
50
|
+
# 验证器
|
|
51
|
+
"BeanValidator",
|
|
52
|
+
# 方法级 AOP
|
|
53
|
+
"BeanValidate", "bean_validate_decorator",
|
|
54
|
+
"__version__",
|
|
55
|
+
]
|
spring/validation/aop.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""SpringBootAI Bean Validation 方法级 AOP 注解 ``@BeanValidate``。
|
|
2
|
+
|
|
3
|
+
把字段约束校验(``spring.validation.validator.BeanValidator``)接入既有 AOP 分发链路:
|
|
4
|
+
``@BeanValidate`` 作为方法级 ``SpringAnnotation``,在 ``comprehensive_aop.ANNOTATION_DECORATORS``
|
|
5
|
+
中注册装饰器,受管 Bean 方法被调用前自动校验指定参数对象。
|
|
6
|
+
|
|
7
|
+
对齐 Jakarta Bean Validation 的方法级校验(``@Validated`` + 约束注解),但本模块只校验
|
|
8
|
+
**参数对象整体**(即对参数值调用 ``BeanValidator.validate_or_raise``),不校验单个标量参数。
|
|
9
|
+
单个标量参数校验仍由既有 ``@Validate`` 切面承担(``comprehensive_aop.validate_decorator``)。
|
|
10
|
+
|
|
11
|
+
用法::
|
|
12
|
+
|
|
13
|
+
from spring.validation import BeanValidate, NotBlank, BeanValidator
|
|
14
|
+
from spring.annotations import Service
|
|
15
|
+
|
|
16
|
+
class UserDto:
|
|
17
|
+
name = NotBlank()
|
|
18
|
+
def __init__(self, name=None): self.name = name
|
|
19
|
+
|
|
20
|
+
@Service
|
|
21
|
+
class UserService:
|
|
22
|
+
@BeanValidate("user") # 校验名为 user 的参数
|
|
23
|
+
def create(self, user: UserDto):
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
@BeanValidate # 不传参:自动校验所有"类型含约束"的参数
|
|
27
|
+
def update(self, user: UserDto, flag: bool):
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
与 Java 的差异:Java 方法级校验需配合 ``MethodValidationPostProcessor`` 代理,且支持
|
|
31
|
+
``@NotNull`` 直接标注在方法参数上;本模块不解析参数上的约束,仅对参数对象做整体校验。
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import functools
|
|
36
|
+
import inspect
|
|
37
|
+
from typing import Any, Callable, List, Optional, Union
|
|
38
|
+
|
|
39
|
+
from spring.annotations.core import SpringAnnotation
|
|
40
|
+
|
|
41
|
+
from .validator import BeanValidator
|
|
42
|
+
from .exceptions import ValidationError
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BeanValidate(SpringAnnotation):
|
|
46
|
+
"""方法级校验注解:调用前自动用 ``BeanValidator`` 校验指定参数对象。
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
value: 指定要校验的参数名(str)或参数名列表(List[str])。
|
|
50
|
+
不传时校验**所有**类型声明含字段约束的参数(自动探测)。
|
|
51
|
+
groups: 校验分组列表(透传给 ``BeanValidator.validate``)。
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
_annotation_type = "aop"
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
value: Union[str, List[str], None] = None,
|
|
59
|
+
groups: Optional[List[type]] = None,
|
|
60
|
+
):
|
|
61
|
+
if isinstance(value, str):
|
|
62
|
+
params: List[str] = [value]
|
|
63
|
+
elif value is None:
|
|
64
|
+
params = []
|
|
65
|
+
else:
|
|
66
|
+
params = list(value)
|
|
67
|
+
super().__init__(value=params, groups=groups or [])
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _param_has_constraints(cls: Any) -> bool:
|
|
71
|
+
"""参数类型是否声明了字段约束(用于 ``@BeanValidate()`` 自动探测)。"""
|
|
72
|
+
if not isinstance(cls, type):
|
|
73
|
+
return False
|
|
74
|
+
try:
|
|
75
|
+
return bool(BeanValidator.get_constraints(cls))
|
|
76
|
+
except Exception:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def bean_validate_decorator(annotation: BeanValidate):
|
|
81
|
+
"""``@BeanValidate`` 的 AOP 装饰器工厂(注册到 ``comprehensive_aop.ANNOTATION_DECORATORS``)。
|
|
82
|
+
|
|
83
|
+
在方法调用前,对指定参数对象执行 ``BeanValidator.validate_or_raise``;
|
|
84
|
+
违反约束时抛出 ``ValidationError``,阻止方法执行。
|
|
85
|
+
"""
|
|
86
|
+
target_params: List[str] = list(annotation.value)
|
|
87
|
+
groups = list(annotation.groups)
|
|
88
|
+
|
|
89
|
+
def decorator(func: Callable) -> Callable:
|
|
90
|
+
sig = inspect.signature(func)
|
|
91
|
+
|
|
92
|
+
def _resolve_targets(args, kwargs) -> List[str]:
|
|
93
|
+
"""决定本次调用要校验的参数名列表。"""
|
|
94
|
+
if target_params:
|
|
95
|
+
# 显式指定的参数名
|
|
96
|
+
return [p for p in target_params]
|
|
97
|
+
# 自动探测:所有类型声明含约束的参数
|
|
98
|
+
targets: List[str] = []
|
|
99
|
+
for pname, param in sig.parameters.items():
|
|
100
|
+
if pname == "self":
|
|
101
|
+
continue
|
|
102
|
+
annotation_cls = param.annotation
|
|
103
|
+
if annotation_cls is inspect.Parameter.empty:
|
|
104
|
+
continue
|
|
105
|
+
if _param_has_constraints(annotation_cls):
|
|
106
|
+
targets.append(pname)
|
|
107
|
+
return targets
|
|
108
|
+
|
|
109
|
+
if inspect.iscoroutinefunction(func):
|
|
110
|
+
@functools.wraps(func)
|
|
111
|
+
async def async_wrapper(*args, **kwargs):
|
|
112
|
+
bound = sig.bind_partial(*args, **kwargs)
|
|
113
|
+
bound.apply_defaults()
|
|
114
|
+
for pname in _resolve_targets(args, kwargs):
|
|
115
|
+
if pname not in bound.arguments:
|
|
116
|
+
continue
|
|
117
|
+
val = bound.arguments[pname]
|
|
118
|
+
if val is None:
|
|
119
|
+
continue
|
|
120
|
+
BeanValidator.validate_or_raise(val, groups=groups)
|
|
121
|
+
return await func(*args, **kwargs)
|
|
122
|
+
return async_wrapper
|
|
123
|
+
|
|
124
|
+
@functools.wraps(func)
|
|
125
|
+
def wrapper(*args, **kwargs):
|
|
126
|
+
bound = sig.bind_partial(*args, **kwargs)
|
|
127
|
+
bound.apply_defaults()
|
|
128
|
+
for pname in _resolve_targets(args, kwargs):
|
|
129
|
+
if pname not in bound.arguments:
|
|
130
|
+
continue
|
|
131
|
+
val = bound.arguments[pname]
|
|
132
|
+
if val is None:
|
|
133
|
+
continue
|
|
134
|
+
BeanValidator.validate_or_raise(val, groups=groups)
|
|
135
|
+
return func(*args, **kwargs)
|
|
136
|
+
return wrapper
|
|
137
|
+
|
|
138
|
+
return decorator
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
__all__ = ["BeanValidate", "bean_validate_decorator", "ValidationError"]
|