@sophiacyl/dsh-plugin-waves 0.0.0-stage → 0.1.0-beta.1

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 (140) hide show
  1. package/DSH-GO-VERIFICATION.md +57 -0
  2. package/ICON.png +0 -0
  3. package/LICENSE +674 -0
  4. package/README.md +251 -2
  5. package/cordis.patch.yml +8 -0
  6. package/install.ps1 +0 -0
  7. package/install.sh +209 -0
  8. package/lib/client.js +1511 -0
  9. package/lib/host/aliashint.js +70 -0
  10. package/lib/host/attachments.js +25 -0
  11. package/lib/host/command.js +34 -0
  12. package/lib/host/consts.js +16 -0
  13. package/lib/host/dataguide.js +35 -0
  14. package/lib/host/imagebridge.js +41 -0
  15. package/lib/host/native-result.js +91 -0
  16. package/lib/host/native.js +191 -0
  17. package/lib/host/proxy.js +59 -0
  18. package/lib/host/results.js +67 -0
  19. package/lib/host/service.js +263 -0
  20. package/lib/host/tool.js +185 -0
  21. package/lib/host/troubleshoot.js +31 -0
  22. package/lib/host/worldbook.js +80 -0
  23. package/lib/index.js +74 -0
  24. package/locale/en.json +6 -0
  25. package/locale/zh.json +6 -0
  26. package/package.json +136 -4
  27. package/service/ICON.png +0 -0
  28. package/service/ai/AI_DATA.md +221 -0
  29. package/service/knowledge/additional/manual.jsonl +5 -0
  30. package/service/knowledge/wiki/baike.jsonl +1231 -0
  31. package/service/knowledge/wiki/chengjiu.jsonl +36 -0
  32. package/service/knowledge/wiki/gonglve.jsonl +383 -0
  33. package/service/knowledge/wiki/huodong.jsonl +427 -0
  34. package/service/knowledge/wiki/renwu.jsonl +412 -0
  35. package/service/knowledge/wiki/tujian.jsonl +2889 -0
  36. package/service/knowledge/wiki/zixun.jsonl +47 -0
  37. package/service/plugins/__init__.py +0 -0
  38. package/service/pyproject.toml +40 -0
  39. package/service/rover/__init__.py +0 -0
  40. package/service/rover/ai_core/__init__.py +0 -0
  41. package/service/rover/ai_core/models.py +50 -0
  42. package/service/rover/ai_core/rag/__init__.py +5 -0
  43. package/service/rover/ai_core/rag/knowledge.py +23 -0
  44. package/service/rover/ai_core/register.py +86 -0
  45. package/service/rover/ai_core/resource.py +6 -0
  46. package/service/rover/ai_core/skills/__init__.py +0 -0
  47. package/service/rover/ai_core/skills/operations.py +8 -0
  48. package/service/rover/api.py +720 -0
  49. package/service/rover/api_render.py +161 -0
  50. package/service/rover/app_life.py +154 -0
  51. package/service/rover/aps.py +47 -0
  52. package/service/rover/avatar.py +85 -0
  53. package/service/rover/bot.py +133 -0
  54. package/service/rover/builtin.py +152 -0
  55. package/service/rover/cards/__init__.py +26 -0
  56. package/service/rover/cards/_shared.py +129 -0
  57. package/service/rover/cards/char_list.py +258 -0
  58. package/service/rover/cards/char_panel.py +1353 -0
  59. package/service/rover/cards/echo_list.py +206 -0
  60. package/service/rover/cards/matrix_roles.py +25 -0
  61. package/service/rover/cards/rank_boards.py +714 -0
  62. package/service/rover/cards/rank_total.py +664 -0
  63. package/service/rover/compat.py +215 -0
  64. package/service/rover/config.py +163 -0
  65. package/service/rover/custom_panel.py +220 -0
  66. package/service/rover/data_store.py +31 -0
  67. package/service/rover/gss.py +19 -0
  68. package/service/rover/handler.py +191 -0
  69. package/service/rover/help/__init__.py +0 -0
  70. package/service/rover/help/draw_new_plugin_help.py +332 -0
  71. package/service/rover/help/model.py +16 -0
  72. package/service/rover/help/utils.py +7 -0
  73. package/service/rover/hooks.py +420 -0
  74. package/service/rover/html_actions.py +236 -0
  75. package/service/rover/i18n.py +19 -0
  76. package/service/rover/knowledge.py +235 -0
  77. package/service/rover/logger.py +102 -0
  78. package/service/rover/main.py +33 -0
  79. package/service/rover/media.py +222 -0
  80. package/service/rover/message_models.py +23 -0
  81. package/service/rover/models.py +92 -0
  82. package/service/rover/native_media.py +64 -0
  83. package/service/rover/ocr.py +79 -0
  84. package/service/rover/pool.py +43 -0
  85. package/service/rover/restart.py +36 -0
  86. package/service/rover/segment.py +143 -0
  87. package/service/rover/server.py +131 -0
  88. package/service/rover/status/__init__.py +0 -0
  89. package/service/rover/status/models.py +10 -0
  90. package/service/rover/status/plugin_status.py +17 -0
  91. package/service/rover/subscribe.py +148 -0
  92. package/service/rover/sv.py +403 -0
  93. package/service/rover/templates/char_list.html +182 -0
  94. package/service/rover/templates/char_panel.html +465 -0
  95. package/service/rover/templates/echo_list.html +221 -0
  96. package/service/rover/templates/rank_all_total.html +193 -0
  97. package/service/rover/templates/rank_board.html +107 -0
  98. package/service/rover/templates/rank_level_total.html +57 -0
  99. package/service/rover/templates/rank_matrix_total.html +66 -0
  100. package/service/rover/templates/rank_phantom_total.html +180 -0
  101. package/service/rover/templates/rank_slash_total.html +38 -0
  102. package/service/rover/trigger.py +98 -0
  103. package/service/rover/utils/__init__.py +0 -0
  104. package/service/rover/utils/boardcast/__init__.py +0 -0
  105. package/service/rover/utils/boardcast/models.py +15 -0
  106. package/service/rover/utils/cookie_manager/__init__.py +0 -0
  107. package/service/rover/utils/cookie_manager/qrlogin.py +24 -0
  108. package/service/rover/utils/database/__init__.py +0 -0
  109. package/service/rover/utils/database/base_models.py +1771 -0
  110. package/service/rover/utils/database/models.py +80 -0
  111. package/service/rover/utils/database/startup.py +60 -0
  112. package/service/rover/utils/default_bg/bg.jpg +0 -0
  113. package/service/rover/utils/download_resource/__init__.py +0 -0
  114. package/service/rover/utils/download_resource/download_core.py +164 -0
  115. package/service/rover/utils/download_resource/download_file.py +46 -0
  116. package/service/rover/utils/fonts/__init__.py +0 -0
  117. package/service/rover/utils/fonts/fonts.py +33 -0
  118. package/service/rover/utils/image/__init__.py +0 -0
  119. package/service/rover/utils/image/convert.py +51 -0
  120. package/service/rover/utils/image/image_tools.py +30 -0
  121. package/service/rover/utils/image/utils.py +70 -0
  122. package/service/rover/utils/plugins_config/__init__.py +0 -0
  123. package/service/rover/utils/plugins_config/gs_config.py +506 -0
  124. package/service/rover/utils/plugins_config/models.py +182 -0
  125. package/service/rover/web_app.py +3 -0
  126. package/service/rover/webconsole/__init__.py +0 -0
  127. package/service/rover/webconsole/mount_app.py +26 -0
  128. package/service/tools/migrate.py +351 -0
  129. package/service/tools/regression.py +87 -0
  130. package/service/tools/smoke.py +63 -0
  131. package/service/tools/update_plugins.py +212 -0
  132. package/service/tools/verify_actions.py +104 -0
  133. package/service/tools/verify_card.py +82 -0
  134. package/service/tools/wwdata.py +665 -0
  135. package/tests/compat.test.js +181 -0
  136. package/tests/imagebridge.test.js +58 -0
  137. package/tests/media.test.js +68 -0
  138. package/tests/native.test.js +127 -0
  139. package/tests/runtime.test.js +69 -0
  140. package/tests/service.test.js +48 -0
@@ -0,0 +1,1771 @@
1
+ import json
2
+ import asyncio
3
+ import sqlite3
4
+ from typing import (
5
+ Any,
6
+ Dict,
7
+ List,
8
+ Type,
9
+ TypeVar,
10
+ Callable,
11
+ Optional,
12
+ Sequence,
13
+ Awaitable,
14
+ )
15
+ from functools import wraps
16
+ from typing_extensions import ParamSpec, Concatenate
17
+
18
+ from sqlmodel import Field, SQLModel, col, and_, delete, select, update
19
+ from sqlalchemy import exc, text, event, create_engine
20
+ from sqlalchemy.exc import OperationalError
21
+ from sqlalchemy.pool import NullPool
22
+ from sqlalchemy.engine import Engine
23
+ from sqlalchemy.ext.asyncio import (
24
+ AsyncSession,
25
+ async_sessionmaker, # type: ignore
26
+ create_async_engine,
27
+ )
28
+ from sqlalchemy.orm.attributes import InstrumentedAttribute
29
+ from sqlalchemy.sql.expression import func, null, true
30
+
31
+ from rover.logger import logger
32
+ from rover.data_store import get_res_path
33
+
34
+ T_BaseModel = TypeVar("T_BaseModel", bound="BaseModel")
35
+ T_BaseIDModel = TypeVar("T_BaseIDModel", bound="BaseIDModel")
36
+ T_User = TypeVar("T_User", bound="User")
37
+ T_Bind = TypeVar("T_Bind", bound="Bind")
38
+ T_Push = TypeVar("T_Push", bound="Push")
39
+ T_Cache = TypeVar("T_Cache", bound="Cache")
40
+ T = TypeVar("T")
41
+ P = ParamSpec("P")
42
+ R = TypeVar("R")
43
+
44
+
45
+ DB_CONFIG_DEFAULT: Dict[str, Any] = {
46
+ "db_type": "SQLite",
47
+ "db_driver": "aiomysql",
48
+ "db_custom_url": "",
49
+ "db_host": "localhost",
50
+ "db_port": "3306",
51
+ "db_user": "root",
52
+ "db_password": "root",
53
+ "db_name": "WavesData",
54
+ "db_pool_size": 5,
55
+ "db_echo": False,
56
+ "db_pool_recycle": 1500,
57
+ }
58
+
59
+
60
+ def _load_db_config() -> Dict[str, Any]:
61
+ """读取 configs/database_config.json, 缺失项回落默认值。"""
62
+ conf = dict(DB_CONFIG_DEFAULT)
63
+ path = get_res_path("configs") / "database_config.json"
64
+ if not path.exists():
65
+ return conf
66
+
67
+ try:
68
+ raw = json.loads(path.read_text(encoding="utf-8"))
69
+ except Exception as e:
70
+ logger.warning(f"[数据库] 配置读取失败, 使用默认配置: {e}")
71
+ return conf
72
+
73
+ if not isinstance(raw, dict):
74
+ return conf
75
+
76
+ for key in DB_CONFIG_DEFAULT:
77
+ if key not in raw:
78
+ continue
79
+ value = raw[key]
80
+ # 配置文件既可能是裸值, 也可能是 {"data": 值} 的配置项结构
81
+ if isinstance(value, dict):
82
+ value = value.get("data", conf[key])
83
+ if value is not None:
84
+ conf[key] = value
85
+ return conf
86
+
87
+
88
+ _db_config_data = _load_db_config()
89
+
90
+ db_host: str = _db_config_data["db_host"]
91
+ db_port: str = _db_config_data["db_port"]
92
+ db_user: str = _db_config_data["db_user"]
93
+ db_password: str = _db_config_data["db_password"]
94
+ db_name: str = _db_config_data["db_name"]
95
+
96
+ db_pool_size: Optional[int] = _db_config_data["db_pool_size"]
97
+ db_echo: bool = _db_config_data["db_echo"]
98
+ db_pool_recycle: int = _db_config_data["db_pool_recycle"]
99
+
100
+ db_custom_url = _db_config_data["db_custom_url"]
101
+ db_type: str = _db_config_data["db_type"]
102
+
103
+ db_driver: str = _db_config_data["db_driver"]
104
+
105
+ _db_type = db_type.lower()
106
+ db_config = {
107
+ "pool_recycle": db_pool_recycle,
108
+ "echo": db_echo,
109
+ }
110
+
111
+ DB_PATH = get_res_path() / "WavesData.db"
112
+
113
+ sync_url, engine, finally_url = "", "", ""
114
+ async_maker: async_sessionmaker[AsyncSession] = None # type: ignore
115
+ server_engine = None
116
+ _db_init_lock = asyncio.Lock()
117
+ _db_initialized = False
118
+ sqlite_semaphore = None
119
+
120
+ if _db_type == "sqlite":
121
+ sync_url = "sqlite:///"
122
+ base_url = "sqlite+aiosqlite:///"
123
+ db_url = str(DB_PATH)
124
+ # del db_config['pool_size']
125
+ elif _db_type == "mysql":
126
+ sync_url = "mysql+pymysql://"
127
+ base_url = f"mysql+{db_driver}://"
128
+ db_hp = f"{db_host}:{db_port}" if db_port else db_host
129
+ db_url = f"{db_user}:{db_password}@{db_hp}/"
130
+ elif _db_type == "postgresql":
131
+ sync_url = "postgresql+psycopg://"
132
+ base_url = "postgresql+asyncpg://"
133
+ db_hp = f"{db_host}:{db_port}" if db_port else db_host
134
+ db_url = f"{db_user}:{db_password}@{db_hp}/"
135
+ elif _db_type == "自定义":
136
+ base_url = ""
137
+ db_url = db_custom_url
138
+ else:
139
+ base_url = db_type
140
+ db_url = db_custom_url
141
+
142
+
143
+ async def init_database():
144
+ global _db_initialized, engine, finally_url, async_maker, sqlite_semaphore
145
+
146
+ if _db_initialized:
147
+ return
148
+
149
+ async with _db_init_lock:
150
+ if _db_initialized:
151
+ return
152
+
153
+ logger.info("[数据库] 开始初始化...")
154
+
155
+ try:
156
+ if _db_type == "sqlite":
157
+ # sqlite 显式 NullPool, 并发上限由下方 semaphore 控制
158
+ db_config.update(
159
+ {
160
+ "connect_args": {"check_same_thread": False},
161
+ "poolclass": NullPool,
162
+ }
163
+ )
164
+ engine = create_async_engine(f"{base_url}{db_url}", **db_config)
165
+ finally_url = f"{base_url}{db_url}"
166
+
167
+ @event.listens_for(engine.sync_engine, "connect")
168
+ def set_sqlite_pragma(dbapi_connection: sqlite3.Connection, connection_record):
169
+ cursor = dbapi_connection.cursor()
170
+ cursor.execute("PRAGMA journal_mode=WAL")
171
+ cursor.execute("PRAGMA synchronous=NORMAL")
172
+ cursor.execute("PRAGMA busy_timeout=15000")
173
+ cursor.close()
174
+
175
+ # NullPool 下信号量是唯一并发闸门
176
+ sqlite_semaphore = asyncio.Semaphore(8)
177
+ else:
178
+ db_config.update(
179
+ {
180
+ "pool_size": db_pool_size,
181
+ "max_overflow": 10,
182
+ "pool_timeout": 30,
183
+ "isolation_level": "AUTOCOMMIT",
184
+ }
185
+ )
186
+ server_engine: Engine | None = None
187
+ try:
188
+ if _db_type == "mysql":
189
+ server_engine = create_engine(f"{sync_url}{db_url}", **db_config)
190
+
191
+ with server_engine.connect() as conn:
192
+ t1 = f"CREATE DATABASE IF NOT EXISTS {db_name} "
193
+ t2 = "CHARACTER SET utf8mb4 COLLATE "
194
+ t3 = "utf8mb4_unicode_ci"
195
+ conn.execute(text(t1 + t2 + t3))
196
+ logger.success(f"[MySQL] 数据库 {db_name} 创建成功或已存在!")
197
+ elif _db_type == "postgresql":
198
+ server_engine = create_engine(f"{sync_url}{db_url}", **db_config)
199
+ try:
200
+ with server_engine.connect() as conn:
201
+ t = f"CREATE DATABASE {db_name} WITH ENCODING "
202
+ t2 = "'UTF8' LC_COLLATE 'en_US.UTF-8' LC_CTYPE "
203
+ t3 = "'en_US.UTF-8' TEMPLATE template0"
204
+ conn.execute(text(t + t2 + t3))
205
+ except exc.ProgrammingError as e:
206
+ if "already exists" in str(e) or "已经存在" in str(e):
207
+ pass
208
+ else:
209
+ raise
210
+ logger.success(f"[PostgreSQL] 数据库 {db_name} 创建成功或已存在!")
211
+ finally:
212
+ if server_engine:
213
+ server_engine.dispose()
214
+ logger.info("[数据库] 临时数据库连接已释放!")
215
+
216
+ # db_config['poolclass'] = NullPool
217
+ finally_url = f"{base_url}{db_url}{db_name}"
218
+ engine = create_async_engine(finally_url, **db_config)
219
+
220
+ async_maker = async_sessionmaker(
221
+ engine,
222
+ expire_on_commit=False,
223
+ close_resets_only=False,
224
+ class_=AsyncSession,
225
+ )
226
+
227
+ _db_initialized = True
228
+ except Exception as e: # noqa: E722
229
+ logger.exception(f"[数据库] 连接失败: {e}")
230
+ raise ValueError(f"[数据库] [{base_url}] 连接失败, 请检查配置文件!")
231
+
232
+
233
+ def _is_pool_timeout(err: BaseException) -> bool:
234
+ """连接池耗尽 / checkout 超时:重试只会让等待雪崩,必须立即失败。"""
235
+ if isinstance(err, (TimeoutError, asyncio.TimeoutError)):
236
+ return True
237
+ # 兜底按文案识别 QueuePool 报错
238
+ msg = str(err)
239
+ return "QueuePool limit" in msg or "connection timed out" in msg
240
+
241
+
242
+ def _is_transient_db_error(err: BaseException) -> bool:
243
+ """仅对 SQLite 锁竞争 / 瞬时 I/O 做有限重试。"""
244
+ if not isinstance(err, OperationalError):
245
+ return False
246
+ msg = str(err).lower()
247
+ if "unable to open database file" in msg:
248
+ return False
249
+ return "database is locked" in msg or "database table is locked" in msg or "busy" in msg or "disk i/o error" in msg
250
+
251
+
252
+ def with_session(
253
+ func: Callable[Concatenate[Any, AsyncSession, P], Awaitable[R]],
254
+ ) -> Callable[Concatenate[Any, P], Awaitable[R]]:
255
+ @wraps(func)
256
+ async def wrapper(self, *args: P.args, **kwargs: P.kwargs):
257
+ max_retries = 3
258
+ last_err: BaseException | None = None
259
+ for attempt in range(max_retries):
260
+ try:
261
+ if sqlite_semaphore:
262
+ async with sqlite_semaphore:
263
+ async with async_maker() as session:
264
+ data = await func(self, session, *args, **kwargs)
265
+ await session.commit()
266
+ return data
267
+ else:
268
+ async with async_maker() as session:
269
+ data = await func(self, session, *args, **kwargs)
270
+ await session.commit()
271
+ return data
272
+ except Exception as e:
273
+ last_err = e
274
+ if _is_pool_timeout(e):
275
+ logger.error(f"[数据库] 连接池耗尽/超时, 停止重试: {e}")
276
+ raise
277
+ if isinstance(e, OperationalError) and "unable to open database file" in str(e):
278
+ logger.error("[数据库] 数据库无法打开, 停止重试")
279
+ raise
280
+ if _is_transient_db_error(e) and attempt < max_retries - 1:
281
+ logger.warning(f"[数据库] 第 {attempt + 1} 次重试失败: {e}")
282
+ await asyncio.sleep(0.5 * (2**attempt))
283
+ continue
284
+ # 业务异常 / 不可恢复:直接抛,禁止静默 return None
285
+ if attempt >= max_retries - 1 and _is_transient_db_error(e):
286
+ logger.error(
287
+ f"[数据库] 重试耗尽仍失败: {e}",
288
+ exc_info=True,
289
+ )
290
+ raise
291
+
292
+ # 理论上到不了这里;兜底保证不返回 None
293
+ if last_err is not None:
294
+ raise last_err
295
+ raise RuntimeError("[数据库] with_session 未知失败")
296
+
297
+ return wrapper # type: ignore
298
+
299
+
300
+ # https://github.com/tiangolo/sqlmodel/issues/264
301
+ class BaseIDModel(SQLModel):
302
+ id: int = Field(default=None, primary_key=True, title="序号")
303
+
304
+ @classmethod
305
+ @with_session
306
+ async def get_distinct_list(
307
+ cls,
308
+ session: AsyncSession,
309
+ column: InstrumentedAttribute[T],
310
+ ):
311
+ result = await session.execute(select(column).distinct())
312
+ r = result.all()
313
+ return r
314
+
315
+ @classmethod
316
+ @with_session
317
+ async def batch_insert_data(
318
+ cls,
319
+ session: AsyncSession,
320
+ datas: Sequence["BaseIDModel"],
321
+ ):
322
+ session.add_all(datas)
323
+
324
+ @classmethod
325
+ @with_session
326
+ async def batch_insert_data_with_update(
327
+ cls,
328
+ session: AsyncSession,
329
+ datas: Sequence["BaseIDModel"],
330
+ update_key: List[str],
331
+ index_elements: List[str],
332
+ ):
333
+ """
334
+ MySQL需要预先定义约束条件!!
335
+ """
336
+ if not datas:
337
+ return
338
+
339
+ values_to_insert = [data.model_dump() for data in datas]
340
+ if _db_type == "sqlite":
341
+ from sqlalchemy.dialects.sqlite import insert
342
+
343
+ stmt = insert(cls)
344
+ update_stmt = stmt.on_conflict_do_update(
345
+ index_elements=index_elements,
346
+ set_={k: stmt.excluded[k] for k in update_key},
347
+ )
348
+ elif _db_type == "postgresql":
349
+ from sqlalchemy.dialects.postgresql import insert
350
+
351
+ stmt = insert(cls)
352
+ update_stmt = stmt.on_conflict_do_update(
353
+ index_elements=index_elements,
354
+ set_={k: stmt.excluded[k] for k in update_key},
355
+ )
356
+ elif _db_type == "mysql":
357
+ from sqlalchemy.dialects.mysql import insert
358
+
359
+ stmt = insert(cls)
360
+ update_dict = {col: getattr(stmt.inserted, col) for col in update_key}
361
+ update_stmt = stmt.on_duplicate_key_update(**update_dict)
362
+ else:
363
+ raise ValueError(f"[数据库] 不支持 {_db_type} 数据库!")
364
+
365
+ await session.execute(update_stmt, values_to_insert)
366
+
367
+ @classmethod
368
+ @with_session
369
+ async def update_data_by_data(
370
+ cls,
371
+ session: AsyncSession,
372
+ select_data: Dict,
373
+ update_data: Dict,
374
+ ) -> int:
375
+ """📝简单介绍:
376
+
377
+ 基类的数据更新方法
378
+
379
+ 🌱参数:
380
+
381
+ 🔹select_data (`Dict`):
382
+ 寻找数据条件, 例如`{"user_id": `event.bot_id`}`
383
+
384
+ 🔹`update_data (`Dict`)`:
385
+ 要更新的数据
386
+
387
+ 🚀使用范例:
388
+
389
+ `await WavesUser.update_data_by_data(`
390
+ `select_data={"user_id": `event.bot_id`}, `
391
+ `update_data={"bot_id": 'onebot', "uid": '22'}`
392
+ `)`
393
+
394
+ ✅返回值:
395
+
396
+ 🔸`int`: 成功为0, 失败为-1(未找到数据则无法更新)
397
+ """
398
+ sql = update(cls)
399
+ for k, v in select_data.items():
400
+ sql = sql.where(getattr(cls, k) == v)
401
+
402
+ if update_data:
403
+ query = sql.values(**update_data)
404
+ query.execution_options(synchronize_session="fetch")
405
+ await session.execute(query)
406
+ return 0
407
+ return -1
408
+
409
+ @classmethod
410
+ def get_gameid_name(cls, game_name: Optional[str] = None) -> str:
411
+ """📝简单介绍:
412
+
413
+ 快速获取uid的列名
414
+
415
+ 🌱参数:
416
+
417
+ 🔹game_name (`Optional[str]`, 默认是 `None`):
418
+ 假设传入`None`会返回`uid`,而传入`sr`会返回`sr_uid`
419
+ 特殊的, 传入`gs`也会返回`uid`!
420
+
421
+ 注意: 仅接受「模型上真实存在的游戏 UID 列」对应短名,
422
+ 否则在校验阶段抛出 ``ValueError``。
423
+
424
+ 🚀使用范例:
425
+
426
+ `await WavesUser.get_gameid_name('sr')`
427
+
428
+ ✅返回值:
429
+
430
+ 🔸`str`: 游戏uid对应列名,默认为`uid`
431
+ """
432
+ if not game_name or game_name == "gs":
433
+ col_name = "uid"
434
+ else:
435
+ col_name = f"{game_name}_uid"
436
+
437
+ # 入口统一校验:所有 getattr(cls, get_gameid_name(...)) 依赖此保证
438
+ if not hasattr(cls, col_name):
439
+ raise ValueError(
440
+ f"{cls.__name__} 不存在字段 {col_name!r} "
441
+ f"(game_name={game_name!r}); "
442
+ f"game_name 只能是模型上真实存在的游戏 UID 列短名"
443
+ )
444
+ return col_name
445
+
446
+ @classmethod
447
+ @with_session
448
+ async def full_insert_data(cls, session: AsyncSession, **data) -> int:
449
+ """📝简单介绍:
450
+
451
+ 数据库基类基础插入数据方法
452
+
453
+ 🌱参数:
454
+
455
+ 🔹`**data`
456
+ 插入的数据, 入参列名等于数据即可
457
+
458
+ 🚀使用范例:
459
+
460
+ `await WavesUser.full_insert_data(uid='123',cookie='233', ...)`
461
+
462
+ ✅返回值:
463
+
464
+ 🔸`int`: 恒为0
465
+ """
466
+ session.add(cls(**data))
467
+ return 0
468
+
469
+ @classmethod
470
+ @with_session
471
+ async def delete_row(
472
+ cls: Type[T_BaseIDModel],
473
+ session: AsyncSession,
474
+ **data,
475
+ ) -> int:
476
+ """
477
+ ✅返回值:
478
+
479
+ 🔸`int`: 如为1则删除成功,否则删除失败(数据不存在)
480
+ """
481
+ row_data = await cls.select_rows(**data)
482
+ logger.trace(f"[数据库] 即将删除{row_data}")
483
+ if row_data:
484
+ for row in row_data:
485
+ await session.delete(row)
486
+ return 1
487
+ else:
488
+ return 0
489
+
490
+ @classmethod
491
+ @with_session
492
+ async def select_rows(
493
+ cls: Type[T_BaseIDModel],
494
+ session: AsyncSession,
495
+ distinct: bool = False,
496
+ **data,
497
+ ):
498
+ """📝简单介绍:
499
+
500
+ 数据库基类基础选择数据方法
501
+
502
+ 🌱参数:
503
+
504
+ 🔹`**data`
505
+ 选择的数据, 入参列名等于数据即可
506
+
507
+ 🚀使用范例:
508
+
509
+ `await WavesUser.base_select_data(uid='100740568')`
510
+
511
+ ✅返回值:
512
+
513
+ 🔸`Optional[List[T_BaseIDModel]]`: 选中全部符合条件的数据,或者为`None`
514
+ """
515
+ stmt = select(cls)
516
+ for k, v in data.items():
517
+ stmt = stmt.where(getattr(cls, k) == v)
518
+ if distinct:
519
+ stmt = stmt.distinct()
520
+ result = await session.execute(stmt)
521
+ data = result.scalars().all()
522
+ logger.trace(f"[数据库] 选择 {data}")
523
+ return data
524
+
525
+ @classmethod
526
+ async def base_select_data(cls: Type[T_BaseIDModel], **data) -> Optional[T_BaseIDModel]:
527
+ """📝简单介绍:
528
+
529
+ 数据库基类基础选择数据方法
530
+
531
+ 🌱参数:
532
+
533
+ 🔹`**data`
534
+ 选择的数据, 入参列名等于数据即可
535
+
536
+ 🚀使用范例:
537
+
538
+ `await WavesUser.base_select_data(uid='100740568')`
539
+
540
+ ✅返回值:
541
+
542
+ 🔸`Optional[T_BaseIDModel]`: 选中符合条件的第一个数据,或者为`None`
543
+ """
544
+ data = await cls.select_rows(**data)
545
+ return data[0] if data else None
546
+
547
+ @classmethod
548
+ async def data_exist(cls, **data) -> bool:
549
+ """📝简单介绍:
550
+
551
+ 数据库基类基础判定数据是否存在的方法
552
+
553
+
554
+ 🚀使用范例:
555
+
556
+ `await WavesUser.data_exist(uid='100740568')`
557
+
558
+ ✅返回值:
559
+
560
+ 🔸`bool`: 存在为`True`
561
+ """
562
+ return bool(await cls.base_select_data(**data))
563
+
564
+
565
+ class BaseBotIDModel(BaseIDModel):
566
+ bot_id: str = Field(title="平台")
567
+
568
+ @classmethod
569
+ @with_session
570
+ async def update_data_by_uid_without_bot_id(
571
+ cls,
572
+ session: AsyncSession,
573
+ uid: str,
574
+ game_name: Optional[str] = None,
575
+ **data,
576
+ ) -> int:
577
+ """📝简单介绍:
578
+
579
+ 基类方法,通过传入uid查找并更新数据,无需bot_id
580
+
581
+ 🌱参数:
582
+
583
+ 🔹uid (`str`):
584
+ 根据该入参寻找相应数据
585
+
586
+ 🔹game_name (`Optional[str]`, 默认是 `None`):
587
+ 根据该入参寻找相应列名
588
+
589
+ 🚀使用范例:
590
+
591
+ `await WavesUser.update_data_by_uid_without_bot_id(uid, cookie='2')`
592
+
593
+ ✅返回值:
594
+
595
+ 🔸`int`: 成功为`0`, 失败为`-1`
596
+ """
597
+ sql = update(cls).where(
598
+ getattr(cls, cls.get_gameid_name(game_name)) == uid,
599
+ )
600
+ if data is not None:
601
+ query = sql.values(**data)
602
+ query.execution_options(synchronize_session="fetch")
603
+ await session.execute(query)
604
+ return 0
605
+ return -1
606
+
607
+ @classmethod
608
+ @with_session
609
+ async def update_data_by_xx(
610
+ cls,
611
+ session: AsyncSession,
612
+ by: Dict[str, Any],
613
+ **data,
614
+ ) -> int:
615
+ """📝简单介绍:
616
+
617
+ 基类方法,通过传入`by`和`**data`查找并更新数据
618
+
619
+ 🌱参数:
620
+
621
+ 🔹by (`Dict[str, Any]`)
622
+ 根据该入参寻找相应数据
623
+
624
+ 🔹**data
625
+ 根据该入参修改数据
626
+
627
+ 🚀使用范例:
628
+
629
+ `await WavesUser.update_data_by_xx({'uid': '233'}, cookie=ck)`
630
+
631
+ ✅返回值:
632
+
633
+ 🔸`int`: 成功为`0`, 失败为`-1`
634
+ """
635
+ sql = update(cls)
636
+ for i in by:
637
+ sql = sql.where(getattr(cls, i) == by[i])
638
+ if data is not None:
639
+ query = sql.values(**data)
640
+ query.execution_options(synchronize_session="fetch")
641
+ await session.execute(query)
642
+ return 0
643
+ return -1
644
+
645
+ @classmethod
646
+ @with_session
647
+ async def update_data_by_uid(
648
+ cls,
649
+ session: AsyncSession,
650
+ uid: str,
651
+ bot_id: Optional[str] = None,
652
+ game_name: Optional[str] = None,
653
+ **data,
654
+ ) -> int:
655
+ """📝简单介绍:
656
+
657
+ 基类方法,通过传入`uid`和`bot_id`查找并更新数据
658
+
659
+ 🌱参数:
660
+
661
+ 🔹uid (`str`)
662
+ 根据该入参寻找相应数据
663
+
664
+ 🔹bot_id (`str`)
665
+ 根据该入参寻找相应数据
666
+
667
+ 🔹game_name (`Optional[str]`, 默认是 `None`)
668
+ 根据该入参修改寻找列名
669
+
670
+ 🔹**data
671
+ 根据该入参修改数据
672
+
673
+ 🚀使用范例:
674
+
675
+ `await WavesUser.update_data_by_uid(uid, 'onebot', cookie='2')`
676
+
677
+ ✅返回值:
678
+
679
+ 🔸`int`: 成功为`0`, 失败为`-1`
680
+ """
681
+ uid_name = cls.get_gameid_name(game_name)
682
+ if not await cls.data_exist(**{uid_name: uid}):
683
+ data[uid_name] = uid
684
+ return await cls.full_insert_data(bot_id=bot_id, **data)
685
+
686
+ sql = update(cls).where(and_(getattr(cls, uid_name) == uid))
687
+
688
+ if bot_id is not None:
689
+ sql = sql.where(and_(cls.bot_id == bot_id))
690
+
691
+ if data is not None:
692
+ query = sql.values(**data)
693
+ query.execution_options(synchronize_session="fetch")
694
+ await session.execute(query)
695
+ return 0
696
+ return -1
697
+
698
+ @classmethod
699
+ @with_session
700
+ async def get_all_data(
701
+ cls: Type[T_BaseIDModel],
702
+ session: AsyncSession,
703
+ ):
704
+ rdata = await session.execute(select(cls))
705
+ data = rdata.scalars().all()
706
+ return data
707
+
708
+
709
+ class BaseModel(BaseBotIDModel):
710
+ user_id: str = Field(title="账号")
711
+
712
+ ################################
713
+ # 基本的增删改查 #
714
+ ################################
715
+
716
+ @classmethod
717
+ @with_session
718
+ async def select_data_list(
719
+ cls: Type[T_BaseModel],
720
+ session: AsyncSession,
721
+ user_id: str,
722
+ bot_id: Optional[str] = None,
723
+ ):
724
+ """📝简单介绍:
725
+
726
+ 基类的数据选择方法
727
+
728
+ 🌱参数:
729
+
730
+ 🔹user_id (`str`):
731
+ 传入的用户id, 例如QQ号, 一般直接取`event.user_id`
732
+
733
+ 🔹bot_id (`Optional[str]`, 默认是 `None`):
734
+ 传入的bot_id, 例如`onebot`, 一般直接取`event.bot_id`
735
+
736
+ 🚀使用范例:
737
+
738
+ `await WavesUser.select_data(user_id='444888', bot_id='onebot')`
739
+
740
+ ✅返回值:
741
+
742
+ 🔸`Optional[Sequence[T_BaseModel]]`: 选中符合条件的全部数据,不存在则为`None`
743
+ """
744
+ if bot_id is None:
745
+ sql = select(cls).where(cls.user_id == user_id)
746
+ else:
747
+ sql = select(cls).where(and_(cls.user_id == user_id, cls.bot_id == bot_id))
748
+ result = await session.execute(sql)
749
+ data = result.scalars().all()
750
+ return data if data else None
751
+
752
+ @classmethod
753
+ async def select_data(
754
+ cls: Type[T_BaseModel],
755
+ user_id: str,
756
+ bot_id: Optional[str] = None,
757
+ ) -> Optional[T_BaseModel]:
758
+ """📝简单介绍:
759
+
760
+ 基类的数据选择方法
761
+
762
+ 🌱参数:
763
+
764
+ 🔹user_id (`str`):
765
+ 传入的用户id, 例如QQ号, 一般直接取`event.user_id`
766
+
767
+ 🔹bot_id (`Optional[str]`, 默认是 `None`):
768
+ 传入的bot_id, 例如`onebot`, 一般直接取`event.bot_id`
769
+
770
+ 🚀使用范例:
771
+
772
+ `await WavesUser.select_data(user_id='444888', bot_id='onebot')`
773
+
774
+ ✅返回值:
775
+
776
+ 🔸`Optional[T_BaseModel]`: 选中符合条件的第一个数据,不存在则为`None`
777
+ """
778
+ data = await cls.select_data_list(user_id, bot_id)
779
+ return data[0] if data else None
780
+
781
+ @classmethod
782
+ @with_session
783
+ async def insert_data(
784
+ cls: Type[T_BaseModel],
785
+ session: AsyncSession,
786
+ user_id: str,
787
+ bot_id: str,
788
+ **data,
789
+ ) -> int:
790
+ """📝简单介绍:
791
+
792
+ 基类的数据插入方法
793
+
794
+ 🌱参数:
795
+
796
+ 🔹user_id (`str`):
797
+ 传入的用户id, 例如QQ号, 一般直接取`event.user_id`
798
+
799
+ 🔹bot_id (`str`):
800
+ 传入的bot_id, 例如`onebot`, 一般直接取`event.bot_id`
801
+
802
+ 🔹`**data`:
803
+ 要插入的数据
804
+
805
+ 🚀使用范例:
806
+
807
+ `await WavesUser.insert_data(user_id='4', bot_id='onebot', uid='22')`
808
+
809
+ ✅返回值:
810
+
811
+ 🔸`int`: 恒为0
812
+ """
813
+ cond = {"user_id": user_id, "bot_id": bot_id}
814
+ if "mys_id" in data:
815
+ cond["mys_id"] = data["mys_id"]
816
+ elif "uid" in data:
817
+ cond["uid"] = data["uid"]
818
+ if await cls.data_exist(**cond):
819
+ await cls.update_data(user_id, bot_id, **data)
820
+ else:
821
+ session.add(cls(user_id=user_id, bot_id=bot_id, **data))
822
+ return 0
823
+
824
+ @classmethod
825
+ @with_session
826
+ async def delete_data(
827
+ cls: Type[T_BaseModel],
828
+ session: AsyncSession,
829
+ user_id: str,
830
+ bot_id: str,
831
+ **data,
832
+ ) -> int:
833
+ """📝简单介绍:
834
+
835
+ 基类的数据删除方法
836
+
837
+ 🌱参数:
838
+
839
+ 🔹user_id (`str`):
840
+ 传入的用户id, 例如QQ号, 一般直接取`event.user_id`
841
+
842
+ 🔹bot_id (`str`):
843
+ 传入的bot_id, 例如`onebot`, 一般直接取`event.bot_id`
844
+
845
+ 🚀使用范例:
846
+
847
+ `await WavesUser.delete_data(user_id='4', bot_id='onebot', uid='22')`
848
+
849
+ ✅返回值:
850
+
851
+ 🔸`int`: 恒为0
852
+ """
853
+ await session.delete(cls(user_id=user_id, bot_id=bot_id, **data))
854
+ return 0
855
+
856
+ @classmethod
857
+ @with_session
858
+ async def update_data(
859
+ cls: Type[T_BaseModel],
860
+ session: AsyncSession,
861
+ user_id: str,
862
+ bot_id: str,
863
+ **data,
864
+ ) -> int:
865
+ """📝简单介绍:
866
+
867
+ 基类的数据更新方法
868
+
869
+ 🌱参数:
870
+
871
+ 🔹user_id (`str`):
872
+ 传入的用户id, 例如QQ号, 一般直接取`event.user_id`
873
+
874
+ 🔹bot_id (`str`):
875
+ 传入的bot_id, 例如`onebot`, 一般直接取`event.bot_id`
876
+
877
+ 🔹`**data`:
878
+ 要更新的数据
879
+
880
+ 🚀使用范例:
881
+
882
+ `await WavesUser.update_data(user_id='4', bot_id='onebot', uid='22')`
883
+
884
+ ✅返回值:
885
+
886
+ 🔸`int`: 成功为0, 失败为-1(未找到数据则无法更新)
887
+ """
888
+ sql = update(cls).where(and_(cls.user_id == user_id, cls.bot_id == bot_id))
889
+ if data is not None:
890
+ query = sql.values(**data)
891
+ query.execution_options(synchronize_session="fetch")
892
+ await session.execute(query)
893
+ return 0
894
+ return -1
895
+
896
+
897
+ class Bind(BaseModel):
898
+ group_id: Optional[str] = Field(title="群号")
899
+
900
+ ################################
901
+ # 额外的扩展方法 #
902
+ ################################
903
+ @classmethod
904
+ async def get_uid_list_by_game(
905
+ cls: Type[T_Bind],
906
+ user_id: str,
907
+ bot_id: str,
908
+ game_name: Optional[str] = None,
909
+ ) -> Optional[List[str]]:
910
+ """📝简单介绍:
911
+
912
+ 基础`Bind`类的扩展方法, 根据传入的`bot_id`和`user_id`拿到绑定的uid列表
913
+
914
+ 🌱参数:
915
+
916
+ 🔹user_id (`str`):
917
+ 传入的用户id, 例如QQ号, 一般直接取`event.user_id`
918
+
919
+ 🔹bot_id (`str`):
920
+ 传入的bot_id, 例如`onebot`, 一般直接取`event.bot_id`
921
+
922
+ 🔹game_name (`Optional[str]`, 默认是 `None`):
923
+ 根据该入参寻找相应列名
924
+
925
+ 🚀使用范例:
926
+
927
+ `await WavesUser.get_uid_list_by_game(user_id='4', bot_id='onebot')`
928
+
929
+ ✅返回值:
930
+
931
+ 🔸`Optional[List[str]]`: 如果有数据则为uid的列表,无则为`None`
932
+ """
933
+ result = await cls.select_data(user_id, bot_id)
934
+ if result is None:
935
+ return None
936
+
937
+ uid = getattr(result, cls.get_gameid_name(game_name))
938
+ if uid is None:
939
+ return None
940
+ else:
941
+ uid_list = uid.split("_")
942
+
943
+ if uid_list:
944
+ return uid_list
945
+ else:
946
+ return None
947
+
948
+ @classmethod
949
+ async def get_uid_by_game(
950
+ cls: Type[T_Bind],
951
+ user_id: str,
952
+ bot_id: str,
953
+ game_name: Optional[str] = None,
954
+ ) -> Optional[str]:
955
+ """📝简单介绍:
956
+
957
+ 基础`Bind`类的扩展方法, 根据传入的`bot_id`和`user_id`拿到单个绑定的uid
958
+
959
+ 🌱参数:
960
+
961
+ 🔹user_id (`str`):
962
+ 传入的用户id, 例如QQ号, 一般直接取`event.user_id`
963
+
964
+ 🔹bot_id (`str`):
965
+ 传入的bot_id, 例如`onebot`, 一般直接取`event.bot_id`
966
+
967
+ 🔹game_name (`Optional[str]`, 默认是 `None`):
968
+ 根据该入参寻找相应列名
969
+
970
+ 🚀使用范例:
971
+
972
+ `await WavesUser.get_uid_by_game(user_id='4', bot_id='onebot')`
973
+
974
+ ✅返回值:
975
+
976
+ 🔸`Optional[str]`: 如果有绑定数据则返回当前绑定uid, 没有则为`None`
977
+ """
978
+ result = await cls.get_uid_list_by_game(user_id, bot_id, game_name)
979
+ if result is None or not result:
980
+ return None
981
+ return result[0]
982
+
983
+ @classmethod
984
+ async def bind_exists(
985
+ cls: Type[T_Bind],
986
+ user_id: str,
987
+ bot_id: str,
988
+ ) -> bool:
989
+ """
990
+ 查询当前user_id是否已有绑定数据
991
+ """
992
+ return bool(await cls.select_data(user_id, bot_id))
993
+
994
+ @classmethod
995
+ async def insert_uid(
996
+ cls: Type[T_Bind],
997
+ user_id: str,
998
+ bot_id: str,
999
+ uid: str,
1000
+ group_id: Optional[str] = None,
1001
+ lenth_limit: Optional[int] = None,
1002
+ is_digit: Optional[bool] = True,
1003
+ game_name: Optional[str] = None,
1004
+ ) -> int:
1005
+ """📝简单介绍:
1006
+
1007
+ 基础`Bind`类的扩展方法, 为给定的`user_id`和`bot_id`插入一条uid绑定数据
1008
+
1009
+ 可支持多uid的绑定, 如果绑定多个uid, 则数据库中uid列将会用`_`分割符相连接
1010
+
1011
+ 可以使用`cls.get_uid_list_by_game()`方法获取相应多绑定uid列表
1012
+
1013
+ 或者使用`cls.get_uid_by_game()`方法获得当前绑定uid(单个)
1014
+
1015
+ 🌱参数:
1016
+
1017
+ 🔹user_id (`str`):
1018
+ 传入的用户id, 例如QQ号, 一般直接取`event.user_id`
1019
+
1020
+ 🔹bot_id (`str`):
1021
+ 传入的bot_id, 例如`onebot`, 一般直接取`event.bot_id`
1022
+
1023
+ 🔹uid (`str`):
1024
+ 将要插入的uid数据
1025
+
1026
+ 🔹group_id (`Optional[str]`, 默认是 `None`):
1027
+ 将要插入的群组数据,为绑定uid提供群组绑定
1028
+
1029
+ 🔹lenth_limit (`Optional[int]`, 默认是 `None`):
1030
+ 如果有传该参数, 当uid位数不等于该参数、或uid位数为0的时候, 返回`-1`
1031
+
1032
+ 🔹is_digit (`Optional[bool]`, 默认是 `True`):
1033
+ 如果有传该参数, 当uid不为全数字的时候, 返回`-3`
1034
+
1035
+ 🔹game_name (`Optional[str]`, 默认是 `None`):
1036
+ 根据该入参寻找相应列名
1037
+
1038
+ 🚀使用范例:
1039
+
1040
+ `await WavesBind.insert_uid(qid, ev.bot_id, uid, ev.group_id, 9)`
1041
+
1042
+ ✅返回值:
1043
+
1044
+ 🔸`int`: 如果该UID已绑定, 则返回`-2`, 成功则为`0`, 合法校验失败为`-3`或`-1`
1045
+ """
1046
+ result = await cls.get_uid_list_by_game(user_id, bot_id, game_name)
1047
+
1048
+ result = [i for i in result if i] if result else None
1049
+
1050
+ if lenth_limit:
1051
+ if len(uid) != lenth_limit:
1052
+ return -1
1053
+
1054
+ if is_digit:
1055
+ if not uid.isdigit():
1056
+ return -3
1057
+ if not uid:
1058
+ return -1
1059
+
1060
+ if result is None and not await cls.bind_exists(user_id, bot_id):
1061
+ return await cls.insert_data(
1062
+ user_id,
1063
+ bot_id,
1064
+ **{cls.get_gameid_name(game_name): uid, "group_id": group_id},
1065
+ )
1066
+ elif result is None:
1067
+ new_uid = uid
1068
+ elif uid in result:
1069
+ return -2
1070
+ else:
1071
+ result.append(uid)
1072
+ new_uid = "_".join(result)
1073
+ await cls.update_data(
1074
+ user_id,
1075
+ bot_id,
1076
+ **{cls.get_gameid_name(game_name): new_uid},
1077
+ )
1078
+ return 0
1079
+
1080
+ @classmethod
1081
+ async def delete_uid(
1082
+ cls: Type[T_Bind],
1083
+ user_id: str,
1084
+ bot_id: str,
1085
+ uid: str,
1086
+ game_name: Optional[str] = None,
1087
+ ) -> int:
1088
+ """📝简单介绍:
1089
+
1090
+ 基础`Bind`类的扩展方法, 根据给定的`user_id`和`bot_id`和`uid`删除一个uid
1091
+
1092
+ 该方法不会删除行,如果只有一个uid会置空,如果同时绑定多个uid只会删除其中一个
1093
+
1094
+ 🌱参数:
1095
+
1096
+ 🔹user_id (`str`):
1097
+ 传入的用户id, 例如QQ号, 一般直接取`event.user_id`
1098
+
1099
+ 🔹bot_id (`str`):
1100
+ 传入的bot_id, 例如`onebot`, 一般直接取`event.bot_id`
1101
+
1102
+ 🔹uid (`str`):
1103
+ 将要删除的uid数据
1104
+
1105
+ 🔹game_name (`Optional[str]`, 默认是 `None`):
1106
+ 根据该入参寻找相应列名
1107
+
1108
+ 🚀使用范例:
1109
+
1110
+ `await WavesBind.delete_uid(qid, ev.bot_id, uid)`
1111
+
1112
+ ✅返回值:
1113
+
1114
+ 🔸`int`: 失败为`-1`, 成功为`0`
1115
+ """
1116
+ result = await cls.get_uid_list_by_game(user_id, bot_id, game_name)
1117
+ if result is None:
1118
+ return -1
1119
+
1120
+ if uid not in result:
1121
+ return -1
1122
+
1123
+ result.remove(uid)
1124
+
1125
+ result = [i for i in result if i] if result else []
1126
+ new_uid = "_".join(result)
1127
+
1128
+ if not new_uid:
1129
+ new_uid = None
1130
+
1131
+ await cls.update_data(
1132
+ user_id,
1133
+ bot_id,
1134
+ **{cls.get_gameid_name(game_name): new_uid},
1135
+ )
1136
+ return 0
1137
+
1138
+ @classmethod
1139
+ @with_session
1140
+ async def get_all_uid_list_by_game(
1141
+ cls: Type[T_Bind],
1142
+ session: AsyncSession,
1143
+ bot_id: str,
1144
+ game_name: Optional[str] = None,
1145
+ ) -> List[str]:
1146
+ """📝简单介绍:
1147
+
1148
+ 基础`Bind`类的扩展方法, 根据给定的`bot_id`获取全部user绑定的uid列表
1149
+
1150
+ 🌱参数:
1151
+
1152
+ 🔹bot_id (`str`):
1153
+ 传入的bot_id, 例如`onebot`, 一般直接取`event.bot_id`
1154
+
1155
+ 🔹game_name (`Optional[str]`, 默认是 `None`):
1156
+ 根据该入参寻找相应列名
1157
+
1158
+ 🚀使用范例:
1159
+
1160
+ `await WavesBind.get_all_uid_list_by_game(ev.bot_id)`
1161
+
1162
+ ✅返回值:
1163
+
1164
+ 🔸`List[str]`: 一个uid的列表, 如果没有任何用户的绑定信息将返回`[]`
1165
+ """
1166
+ sql = select(cls).where(cls.bot_id == bot_id)
1167
+ result = await session.execute(sql)
1168
+ data = result.scalars().all()
1169
+ uid_list: List[str] = []
1170
+ for item in data:
1171
+ uid = getattr(item, cls.get_gameid_name(game_name))
1172
+ if uid is not None and uid:
1173
+ game_uid_list: List[str] = uid.split("_")
1174
+ uid_list.extend(game_uid_list)
1175
+ return uid_list
1176
+
1177
+ @classmethod
1178
+ async def switch_uid_by_game(
1179
+ cls: Type[T_Bind],
1180
+ user_id: str,
1181
+ bot_id: str,
1182
+ uid: Optional[str] = None,
1183
+ game_name: Optional[str] = None,
1184
+ ) -> int:
1185
+ """📝简单介绍:
1186
+
1187
+ 基础`Bind`类的扩展方法, 根据给定的`bot_id`和`user_id`定位数据,并切换当前uid
1188
+
1189
+ 如果不传uid参数则默认切换下个uid
1190
+
1191
+ 🌱参数:
1192
+
1193
+ 🔹user_id (`str`):
1194
+ 传入的用户id, 例如QQ号, 一般直接取`event.user_id`
1195
+
1196
+ 🔹bot_id (`str`):
1197
+ 传入的bot_id, 例如`onebot`, 一般直接取`event.bot_id`
1198
+
1199
+ 🔹uid (`Optional[str]`, 默认是 `None`):
1200
+ 将要切换的uid数据, 可以不传
1201
+
1202
+ 🔹game_name (`Optional[str]`, 默认是 `None`):
1203
+ 根据该入参寻找相应列名
1204
+
1205
+ 🚀使用范例:
1206
+
1207
+ `await WavesBind.switch_uid_by_game(qid, ev.bot_id, uid)`
1208
+
1209
+ ✅返回值:
1210
+
1211
+ 🔸`int`:
1212
+
1213
+ 成功返回`0`
1214
+
1215
+ 如果不存在绑定记录,则返回`-1`
1216
+
1217
+ 如果传了UID但是不存在绑定列表,则返回`-2`
1218
+
1219
+ 如果绑定UID列表不足2个,返回`-3`
1220
+ """
1221
+ uid_list = await cls.get_uid_list_by_game(user_id, bot_id, game_name)
1222
+ if not uid_list:
1223
+ return -1
1224
+ elif len(uid_list) <= 1:
1225
+ return -3
1226
+ elif uid is None or not uid:
1227
+ uid = uid_list[1]
1228
+ old_uid = uid_list[0]
1229
+ uid_list.remove(uid)
1230
+ uid_list.remove(old_uid)
1231
+ uid_list.insert(0, uid)
1232
+ uid_list.append(old_uid)
1233
+ elif uid not in uid_list:
1234
+ return -2
1235
+ else:
1236
+ uid_list.remove(uid)
1237
+ uid_list.insert(0, uid)
1238
+ await cls.update_data(
1239
+ user_id,
1240
+ bot_id,
1241
+ **{cls.get_gameid_name(game_name): "_".join(uid_list)},
1242
+ )
1243
+ return 0
1244
+
1245
+ @classmethod
1246
+ async def get_bind_group_list(cls: Type[T_Bind], user_id: str, bot_id: str) -> List[str]:
1247
+ """获取传入`user_id`和`bot_id`对应的绑定群列表"""
1248
+ data: Optional["Bind"] = await cls.select_data(user_id, bot_id)
1249
+ return data.group_id.split("_") if data and data.group_id else []
1250
+
1251
+ @classmethod
1252
+ async def get_bind_group(cls: Type[T_Bind], user_id: str, bot_id: str) -> Optional[str]:
1253
+ """获取传入`user_id`和`bot_id`对应的绑定群(如多个则返回第一个)"""
1254
+ data = await cls.get_bind_group_list(user_id, bot_id)
1255
+ return data[0] if data else None
1256
+
1257
+ @classmethod
1258
+ @with_session
1259
+ async def get_group_all_uid(cls: Type[T_Bind], session: AsyncSession, group_id: str):
1260
+ """根据传入`group_id`获取该群号下所有绑定`uid`列表"""
1261
+ result = await session.scalars(select(cls).where(col(cls.group_id).contains(group_id)))
1262
+ data = result.all()
1263
+ return data[0] if data else None
1264
+
1265
+
1266
+ class User(BaseModel):
1267
+ cookie: str = Field(default=None, title="Cookie")
1268
+ stoken: Optional[str] = Field(default=None, title="Stoken")
1269
+ status: Optional[str] = Field(default=None, title="状态")
1270
+ push_switch: str = Field(default="off", title="全局推送开关")
1271
+ sign_switch: str = Field(default="off", title="自动签到")
1272
+
1273
+ @classmethod
1274
+ @with_session
1275
+ async def select_data_by_uid(
1276
+ cls: Type[T_User],
1277
+ session: AsyncSession,
1278
+ uid: str,
1279
+ game_name: Optional[str] = None,
1280
+ ):
1281
+ """📝简单介绍:
1282
+
1283
+ 基础`User`类的数据选择方法
1284
+
1285
+ 🌱参数:
1286
+
1287
+ 🔹uid (`str`):
1288
+ 传入的用户uid, 一般是该游戏的用户唯一识别id
1289
+
1290
+ 🚀使用范例:
1291
+
1292
+ `await WavesUser.select_data_by_uid(uid='100740568')`
1293
+
1294
+ ✅返回值:
1295
+
1296
+ 🔸`Optional[T_BaseModel]`: 选中符合条件的第一个数据,不存在则为`None`
1297
+ """
1298
+ result = await session.execute(
1299
+ select(cls).where(
1300
+ getattr(cls, cls.get_gameid_name(game_name)) == uid,
1301
+ )
1302
+ )
1303
+ data = result.scalars().all()
1304
+ return data[0] if data else None
1305
+
1306
+ @classmethod
1307
+ @with_session
1308
+ async def get_user_all_data_by_user_id(cls: Type[T_User], session: AsyncSession, user_id: str):
1309
+ """📝简单介绍:
1310
+
1311
+ 基础`User`类的数据选择方法, 获取该`user_id`绑定的全部数据实例
1312
+
1313
+ 🌱参数:
1314
+
1315
+ 🔹user_id (`str`):
1316
+ 传入的用户id, 例如QQ号, 一般直接取`event.user_id`
1317
+
1318
+ 🚀使用范例:
1319
+
1320
+ `await WavesUser.get_user_all_data_by_user_id(user_id='2333')`
1321
+
1322
+ ✅返回值:
1323
+
1324
+ 🔸`Optional[T_BaseModel]`: 选中符合条件的数据列表,不存在则为`None`
1325
+ """
1326
+ result = await session.execute(select(cls).where(cls.user_id == user_id))
1327
+ data = result.scalars().all()
1328
+ return data if data else None
1329
+
1330
+ @classmethod
1331
+ async def get_user_attr(
1332
+ cls: Type[T_User],
1333
+ user_id: str,
1334
+ bot_id: str,
1335
+ attr: str,
1336
+ ) -> Optional[Any]:
1337
+ """📝简单介绍:
1338
+
1339
+ 根据传入的`user_id`和`bot_id`选择数据实例,然后返回数据的某个属性的值
1340
+
1341
+ 🌱参数:
1342
+
1343
+ 🔹user_id (`str`):
1344
+ 传入的用户id, 例如QQ号, 一般直接取`event.user_id`
1345
+
1346
+ 🔹bot_id (`str`):
1347
+ 传入的bot_id, 例如`onebot`, 一般直接取`event.bot_id`
1348
+
1349
+ 🔹attr (`str`):
1350
+ 想要获取的该数据的属性
1351
+
1352
+ 🚀使用范例:
1353
+
1354
+ `await cls.get_user_attr(user_id, bot_id, 'cookie')`
1355
+
1356
+ ✅返回值:
1357
+
1358
+ 🔸`Optional[Any]`: 可能是任何值,如果没获取到数据则为`None`
1359
+ """
1360
+ result = await cls.select_data(user_id, bot_id)
1361
+ return getattr(result, attr) if result else None
1362
+
1363
+ @classmethod
1364
+ async def get_user_attr_by_uid(
1365
+ cls: Type[T_User],
1366
+ uid: str,
1367
+ attr: str,
1368
+ game_name: Optional[str] = None,
1369
+ ) -> Optional[Any]:
1370
+ """根据传入的`uid`选择数据实例,然后返回数据的`attr`属性的值
1371
+
1372
+ 如果没获取到数据则为`None`
1373
+ """
1374
+ result = await cls.select_data_by_uid(uid, game_name)
1375
+ return getattr(result, attr) if result else None
1376
+
1377
+ @classmethod
1378
+ async def get_user_attr_by_user_id(
1379
+ cls: Type[T_User],
1380
+ user_id: str,
1381
+ attr: str,
1382
+ ) -> Optional[Any]:
1383
+ """根据传入的`user_id`选择数据实例,然后返回数据的`attr`属性的值
1384
+
1385
+ 如果没获取到数据则为`None`
1386
+ """
1387
+ result = await cls.select_data(user_id)
1388
+ return getattr(result, attr) if result else None
1389
+
1390
+ @classmethod
1391
+ @with_session
1392
+ async def mark_invalid(cls: Type[T_User], session: AsyncSession, cookie: str, mark: str):
1393
+ """令一个cookie所对应数据的`status`值为传入的mark
1394
+
1395
+ 例如:mark值可以是`error`, 标记该Cookie已失效
1396
+ """
1397
+ sql = update(cls).where(and_(cls.cookie == cookie, true())).values(status=mark)
1398
+ await session.execute(sql)
1399
+ return True
1400
+
1401
+ @classmethod
1402
+ async def get_user_cookie_by_uid(cls: Type[T_User], uid: str, game_name: Optional[str] = None) -> Optional[str]:
1403
+ """根据传入的`uid`选择数据实例,然后返回该数据的`cookie`值
1404
+
1405
+ 如果没获取到数据则为`None`
1406
+ """
1407
+ return await cls.get_user_attr_by_uid(uid, "cookie", game_name)
1408
+
1409
+ @classmethod
1410
+ async def get_user_cookie_by_user_id(cls: Type[T_User], user_id: str, bot_id: str) -> Optional[str]:
1411
+ """根据传入的`user_id`选择数据实例,然后返回该数据的`cookie`值
1412
+
1413
+ 如果没获取到数据则为`None`
1414
+ """
1415
+ return await cls.get_user_attr(user_id, bot_id, "cookie")
1416
+
1417
+ @classmethod
1418
+ async def get_user_stoken_by_uid(cls: Type[T_User], uid: str, game_name: Optional[str] = None) -> Optional[str]:
1419
+ """根据传入的`uid`选择数据实例,然后返回该数据的`stoken`值
1420
+
1421
+ 如果没获取到数据则为`None`
1422
+ """
1423
+ return await cls.get_user_attr_by_uid(uid, "stoken", game_name)
1424
+
1425
+ @classmethod
1426
+ async def get_user_stoken_by_user_id(cls: Type[T_User], user_id: str, bot_id: str) -> Optional[str]:
1427
+ """根据传入的`user_id`选择数据实例,然后返回该数据的`stoken`值
1428
+
1429
+ 如果没获取到数据则为`None`
1430
+ """
1431
+ return await cls.get_user_attr(user_id, bot_id, "stoken")
1432
+
1433
+ @classmethod
1434
+ async def cookie_validate(cls: Type[T_User], uid: str, game_name: Optional[str] = None) -> bool:
1435
+ """根据传入的`uid`选择数据实例, 校验数据中的`cookie`是否有效
1436
+
1437
+ 方法是判断数据中的`status`是否为空值, 如果没有异常标记, 则为`True`
1438
+ """
1439
+ data = await cls.get_user_attr_by_uid(uid, "status", game_name)
1440
+ if not data:
1441
+ return True
1442
+ else:
1443
+ return False
1444
+
1445
+ @classmethod
1446
+ @with_session
1447
+ async def get_switch_open_list(cls: Type[T_User], session: AsyncSession, switch_name: str) -> List[T_User]:
1448
+ """📝简单介绍:
1449
+
1450
+ 根据表定义的结构, 根据传入的`switch_name`, 寻找表数据中的该列
1451
+
1452
+ 如果不存在该列,则选中`cls.push_switch`该列
1453
+
1454
+ 对所有数据中,该列满足`!= 'off'`的数据,标记为有效数据
1455
+
1456
+ 返回有效数据的列表
1457
+
1458
+ 🌱参数:
1459
+
1460
+ 🔹switch_name (`str`):
1461
+ 寻找表数据列名
1462
+
1463
+ 🚀使用范例:
1464
+
1465
+ `await WavesUser.get_switch_open_list('sign_switch')`
1466
+
1467
+ ✅返回值:
1468
+
1469
+ 🔸`List[T_User]`: 有效数据的列表, 如没有则为`[]`
1470
+ """
1471
+ _switch = getattr(cls, switch_name, cls.push_switch)
1472
+ sql = select(cls).filter(and_(_switch != "off", true()))
1473
+ data = await session.execute(sql)
1474
+ data_list = data.scalars().all()
1475
+ return [user for user in data_list]
1476
+
1477
+ @classmethod
1478
+ @with_session
1479
+ async def get_all_user(
1480
+ cls: Type[T_User],
1481
+ session: AsyncSession,
1482
+ without_error: bool = True,
1483
+ ) -> Sequence[T_User]:
1484
+ """📝简单介绍:
1485
+
1486
+ 基础`User`类的扩展方法, 获取到全部的数据列表
1487
+
1488
+ 不一定会返回该表中所有的数据, 返回的数据中`cookie`必定存在值
1489
+
1490
+ 🌱参数:
1491
+
1492
+ 🔹without_error (`bool`, 默认是 `True`):
1493
+ 如果为`True`, 则会排除返回列表中存在`status != None`的数据
1494
+
1495
+ 🚀使用范例:
1496
+
1497
+ `await WavesUser.get_all_user()`
1498
+
1499
+ ✅返回值:
1500
+
1501
+ 🔸`List[T_User]`: 有效数据的列表, 如没有则为`[]`
1502
+ """
1503
+ if without_error:
1504
+ sql = select(cls).where(cls.status == null(), cls.cookie != null(), cls.cookie != "")
1505
+ else:
1506
+ sql = select(cls).where(cls.cookie != null(), cls.cookie != "")
1507
+ result = await session.execute(sql)
1508
+ data = result.scalars().all()
1509
+ return data
1510
+
1511
+ @classmethod
1512
+ async def get_all_cookie(cls) -> List[str]:
1513
+ """获得表数据中全部的`cookie`列表"""
1514
+ data = await cls.get_all_user()
1515
+ return [_u.cookie for _u in data if _u.cookie]
1516
+
1517
+ @classmethod
1518
+ async def get_all_stoken(cls) -> List[str]:
1519
+ """获得表数据中全部的`stoken`列表"""
1520
+ data = await cls.get_all_user()
1521
+ return [_u.stoken for _u in data if _u.stoken]
1522
+
1523
+ @classmethod
1524
+ async def get_all_error_cookie(cls) -> List[str]:
1525
+ """获得表数据中,`status != None`情况下的所有`cookie`列表
1526
+
1527
+ 也就是全部失效CK的列表
1528
+ """
1529
+ data = await cls.get_all_user()
1530
+ return [_u.cookie for _u in data if _u.cookie and _u.status]
1531
+
1532
+ @classmethod
1533
+ async def get_all_push_user_list(cls: Type[T_User]) -> List[T_User]:
1534
+ """获得表数据中全部的`push_switch != off`的数据列表"""
1535
+ data = await cls.get_all_user()
1536
+ return [user for user in data if user.push_switch != "off"]
1537
+
1538
+ @classmethod
1539
+ async def get_all_sign_user_list(cls: Type[T_User]) -> List[T_User]:
1540
+ """获得表数据中全部的`sign_switch!= off`的数据列表"""
1541
+ data = await cls.get_all_user()
1542
+ return [user for user in data if user.sign_switch != "off"]
1543
+
1544
+ @classmethod
1545
+ async def get_push_user_list(
1546
+ cls: Type[T_User],
1547
+ push_title: Optional[str] = None,
1548
+ ) -> List[T_User]:
1549
+ """获得表数据中全部的`{push_title}_push_switch!= off`的数据列表"""
1550
+ if push_title is None:
1551
+ return await cls.get_all_push_user_list()
1552
+ data = await cls.get_all_user()
1553
+ return [user for user in data if getattr(user, f"{push_title}_push_switch") != "off"]
1554
+
1555
+ @classmethod
1556
+ async def get_sign_user_list(
1557
+ cls: Type[T_User],
1558
+ sign_title: Optional[str] = None,
1559
+ ) -> List[T_User]:
1560
+ """获得表数据中全部的`{sign_title}_sign_switch!= off`的数据列表"""
1561
+ if sign_title is None:
1562
+ return await cls.get_all_sign_user_list()
1563
+ data = await cls.get_all_user()
1564
+ return [user for user in data if getattr(user, f"{sign_title}_sign_switch") != "off"]
1565
+
1566
+ @classmethod
1567
+ async def user_exists(cls, uid: str, game_name: Optional[str] = None) -> bool:
1568
+ """根据传入`uid`,判定数据是否存在"""
1569
+ data = await cls.select_data_by_uid(uid, game_name)
1570
+ return True if data else False
1571
+
1572
+ @classmethod
1573
+ @with_session
1574
+ async def get_random_cookie(
1575
+ cls: Type[T_User],
1576
+ session: AsyncSession,
1577
+ uid: str,
1578
+ cache_model: Optional[Type["Cache"]] = None,
1579
+ condition: Optional[Dict[str, str]] = None,
1580
+ game_name: Optional[str] = None,
1581
+ ) -> Optional[str]:
1582
+ """📝简单介绍:
1583
+
1584
+ 基础`User`类的扩展方法, 返回一个随机的cookie
1585
+
1586
+ 如果该uid存在绑定cookie, 则返回他绑定的cookie
1587
+
1588
+ 如果传入了`cache_model`并且该uid存在cache_model定义的表数据中,返回该cookie
1589
+
1590
+ 如果定义了condition, 则选取随机cookie时遵照此规则
1591
+
1592
+ 🌱参数:
1593
+
1594
+ 🔹uid (`str`):
1595
+ 传入的用户uid, 一般是该游戏的用户唯一识别id
1596
+
1597
+ 🔹cache_model (`Optional[Type["Cache"]]`, 默认是 `None`):
1598
+ 继承基础`Cache`缓存表的模型, 例如`WavesCache`
1599
+
1600
+ 🔹condition (`Optional[Dict[str, str]]`, 默认是 `None`):
1601
+ 字典结构, 寻找表中符合`key == value`条件的值
1602
+
1603
+ 🔹game_name (`Optional[str]`, 默认是 `None`):
1604
+ 根据该入参寻找相应列名
1605
+
1606
+ 🚀使用范例:
1607
+
1608
+ `await WavesUser.get_random_cookie(
1609
+ uid, WavesCache, {'region': server}, game_name='sr'
1610
+ )`
1611
+
1612
+ ✅返回值:
1613
+
1614
+ 🔸`Optional[str]`: 如找到符合条件的cookie则返回,没有则为`None`
1615
+ """
1616
+ # 有绑定自己CK 并且该CK有效的前提下,优先使用自己CK
1617
+ if await cls.user_exists(uid, game_name) and await cls.cookie_validate(uid, game_name):
1618
+ return await cls.get_user_cookie_by_uid(uid, game_name)
1619
+
1620
+ # 自动刷新缓存
1621
+ # await self.delete_error_cache()
1622
+ # 获得缓存库Ck
1623
+ if cache_model is not None:
1624
+ cache_data = await cache_model.select_cache_cookie(uid, game_name)
1625
+ if cache_data is not None:
1626
+ return cache_data
1627
+
1628
+ # 随机取CK
1629
+ if condition:
1630
+ for i in condition:
1631
+ sql = select(cls).where(getattr(cls, i) == condition[i]).order_by(func.random())
1632
+ data = await session.execute(sql)
1633
+ user_list = data.scalars().all()
1634
+ break
1635
+ else:
1636
+ user_list = await cls.get_all_user()
1637
+ else:
1638
+ user_list = await cls.get_all_user()
1639
+
1640
+ for user in user_list:
1641
+ if not user.status and user.cookie:
1642
+ if cache_model:
1643
+ # 进入缓存
1644
+ await cache_model.insert_cache_data(
1645
+ user.cookie,
1646
+ **{cls.get_gameid_name(game_name): uid},
1647
+ )
1648
+ return user.cookie
1649
+ continue
1650
+ else:
1651
+ return None
1652
+
1653
+ @classmethod
1654
+ @with_session
1655
+ async def delete_user_data_by_uid(cls, session: AsyncSession, uid: str, game_name: Optional[str] = None) -> bool:
1656
+ """根据给定的`uid`获取数据后, 删除整行数据
1657
+
1658
+ 如果该数据存在, 删除后返回`True`, 不存在, 则返回`False`
1659
+ """
1660
+ if await cls.user_exists(uid, game_name):
1661
+ sql = delete(cls).where(getattr(cls, cls.get_gameid_name(game_name)) == uid)
1662
+ await session.execute(sql)
1663
+ return True
1664
+ return False
1665
+
1666
+
1667
+ class Cache(BaseIDModel):
1668
+ cookie: str = Field(default=None, title="Cookie")
1669
+
1670
+ @classmethod
1671
+ @with_session
1672
+ async def select_cache_cookie(
1673
+ cls: Type[T_Cache],
1674
+ session: AsyncSession,
1675
+ uid: str,
1676
+ game_name: Optional[str],
1677
+ ) -> Optional[str]:
1678
+ """根据给定的`uid`获取表中存在缓存的`cookie`并返回"""
1679
+ sql = select(cls).where(getattr(cls, cls.get_gameid_name(game_name)) == uid)
1680
+ result = await session.execute(sql)
1681
+ data = result.scalars().all()
1682
+ return data[0].cookie if len(data) >= 1 else None
1683
+
1684
+ @classmethod
1685
+ @with_session
1686
+ async def delete_error_cache(cls: Type[T_Cache], session: AsyncSession, user: Type["User"]) -> bool:
1687
+ """根据给定的`user`模型中, 查找该模型所有数据的status
1688
+
1689
+ 若`status != None`, 则代表该数据cookie有问题
1690
+
1691
+ 查找`Cache`表中该cookie对应数据行,并删除
1692
+
1693
+ 恒返回`True`
1694
+ """
1695
+ data = await user.get_all_error_cookie()
1696
+ for cookie in data:
1697
+ sql = delete(cls).where(and_(cls.cookie == cookie, true()))
1698
+ await session.execute(sql)
1699
+ return True
1700
+
1701
+ @classmethod
1702
+ @with_session
1703
+ async def delete_all_cache(cls, session: AsyncSession, user: Type["User"]) -> bool:
1704
+ """删除整个表的数据
1705
+
1706
+ 根据给定的`user`模型中, 查找该模型所有数据的status
1707
+
1708
+ 若`status == limit30`, 则代表该数据cookie限制可能已回复
1709
+
1710
+ 清除`User`表中该类cookie的status, 令其重新为`None`
1711
+ """
1712
+ sql = (
1713
+ update(user)
1714
+ .where(and_(user.status == "limit30"))
1715
+ .values(status=None)
1716
+ .execution_options(synchronize_session="fetch")
1717
+ )
1718
+ empty_sql = delete(cls)
1719
+ await session.execute(sql)
1720
+ await session.execute(empty_sql)
1721
+ return True
1722
+
1723
+ @classmethod
1724
+ @with_session
1725
+ async def refresh_cache(cls, session: AsyncSession, uid: str, game_name: Optional[str] = None) -> bool:
1726
+ """删除指定`uid`的数据行"""
1727
+ await session.execute(delete(cls).where(getattr(cls, cls.get_gameid_name(game_name)) == uid))
1728
+ return True
1729
+
1730
+ @classmethod
1731
+ @with_session
1732
+ async def insert_cache_data(cls, session: AsyncSession, cookie: str, **data) -> bool:
1733
+ """新增指定`cookie`的数据行, `**data`为数据"""
1734
+ new_data = cls(cookie=cookie, **data)
1735
+ session.add(new_data)
1736
+ return True
1737
+
1738
+
1739
+ class Push(BaseBotIDModel):
1740
+ @classmethod
1741
+ @with_session
1742
+ async def select_data_by_uid(
1743
+ cls: Type[T_Push],
1744
+ session: AsyncSession,
1745
+ uid: str,
1746
+ game_name: Optional[str] = None,
1747
+ ) -> Optional[T_Push]:
1748
+ """📝简单介绍:
1749
+
1750
+ 基础`Push`类的数据选择方法
1751
+
1752
+ 🌱参数:
1753
+
1754
+ 🔹uid (`str`):
1755
+ 传入的用户uid, 一般是该游戏的用户唯一识别id
1756
+
1757
+ 🚀使用范例:
1758
+
1759
+ `await WavesPush.select_data_by_uid(uid='100740568')`
1760
+
1761
+ ✅返回值:
1762
+
1763
+ 🔸`Optional[T_BaseModel]`: 选中符合条件的第一个数据,不存在则为`None`
1764
+ """
1765
+ result = await session.execute(
1766
+ select(cls).where(
1767
+ getattr(cls, cls.get_gameid_name(game_name)) == uid,
1768
+ )
1769
+ )
1770
+ data = result.scalars().all()
1771
+ return data[0] if data else None