endstone-qqsync-plugin 0.0.7__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 (73) hide show
  1. endstone_qqsync_plugin/__init__.py +3 -0
  2. endstone_qqsync_plugin/core/__init__.py +18 -0
  3. endstone_qqsync_plugin/core/config_manager.py +224 -0
  4. endstone_qqsync_plugin/core/data_manager.py +511 -0
  5. endstone_qqsync_plugin/core/event_handlers.py +552 -0
  6. endstone_qqsync_plugin/core/permission_manager.py +331 -0
  7. endstone_qqsync_plugin/core/verification_manager.py +718 -0
  8. endstone_qqsync_plugin/lib/websockets/__init__.py +236 -0
  9. endstone_qqsync_plugin/lib/websockets/__main__.py +5 -0
  10. endstone_qqsync_plugin/lib/websockets/asyncio/__init__.py +0 -0
  11. endstone_qqsync_plugin/lib/websockets/asyncio/async_timeout.py +282 -0
  12. endstone_qqsync_plugin/lib/websockets/asyncio/client.py +820 -0
  13. endstone_qqsync_plugin/lib/websockets/asyncio/compatibility.py +30 -0
  14. endstone_qqsync_plugin/lib/websockets/asyncio/connection.py +1237 -0
  15. endstone_qqsync_plugin/lib/websockets/asyncio/messages.py +314 -0
  16. endstone_qqsync_plugin/lib/websockets/asyncio/router.py +198 -0
  17. endstone_qqsync_plugin/lib/websockets/asyncio/server.py +981 -0
  18. endstone_qqsync_plugin/lib/websockets/auth.py +18 -0
  19. endstone_qqsync_plugin/lib/websockets/cli.py +178 -0
  20. endstone_qqsync_plugin/lib/websockets/client.py +389 -0
  21. endstone_qqsync_plugin/lib/websockets/connection.py +12 -0
  22. endstone_qqsync_plugin/lib/websockets/datastructures.py +187 -0
  23. endstone_qqsync_plugin/lib/websockets/exceptions.py +473 -0
  24. endstone_qqsync_plugin/lib/websockets/extensions/__init__.py +4 -0
  25. endstone_qqsync_plugin/lib/websockets/extensions/base.py +123 -0
  26. endstone_qqsync_plugin/lib/websockets/extensions/permessage_deflate.py +697 -0
  27. endstone_qqsync_plugin/lib/websockets/frames.py +430 -0
  28. endstone_qqsync_plugin/lib/websockets/headers.py +586 -0
  29. endstone_qqsync_plugin/lib/websockets/http.py +20 -0
  30. endstone_qqsync_plugin/lib/websockets/http11.py +427 -0
  31. endstone_qqsync_plugin/lib/websockets/imports.py +100 -0
  32. endstone_qqsync_plugin/lib/websockets/legacy/__init__.py +11 -0
  33. endstone_qqsync_plugin/lib/websockets/legacy/auth.py +190 -0
  34. endstone_qqsync_plugin/lib/websockets/legacy/client.py +705 -0
  35. endstone_qqsync_plugin/lib/websockets/legacy/exceptions.py +71 -0
  36. endstone_qqsync_plugin/lib/websockets/legacy/framing.py +225 -0
  37. endstone_qqsync_plugin/lib/websockets/legacy/handshake.py +158 -0
  38. endstone_qqsync_plugin/lib/websockets/legacy/http.py +201 -0
  39. endstone_qqsync_plugin/lib/websockets/legacy/protocol.py +1641 -0
  40. endstone_qqsync_plugin/lib/websockets/legacy/server.py +1191 -0
  41. endstone_qqsync_plugin/lib/websockets/protocol.py +758 -0
  42. endstone_qqsync_plugin/lib/websockets/py.typed +0 -0
  43. endstone_qqsync_plugin/lib/websockets/server.py +587 -0
  44. endstone_qqsync_plugin/lib/websockets/speedups.c +222 -0
  45. endstone_qqsync_plugin/lib/websockets/speedups.pyi +1 -0
  46. endstone_qqsync_plugin/lib/websockets/streams.py +151 -0
  47. endstone_qqsync_plugin/lib/websockets/sync/__init__.py +0 -0
  48. endstone_qqsync_plugin/lib/websockets/sync/client.py +648 -0
  49. endstone_qqsync_plugin/lib/websockets/sync/connection.py +1072 -0
  50. endstone_qqsync_plugin/lib/websockets/sync/messages.py +345 -0
  51. endstone_qqsync_plugin/lib/websockets/sync/router.py +192 -0
  52. endstone_qqsync_plugin/lib/websockets/sync/server.py +763 -0
  53. endstone_qqsync_plugin/lib/websockets/sync/utils.py +45 -0
  54. endstone_qqsync_plugin/lib/websockets/typing.py +74 -0
  55. endstone_qqsync_plugin/lib/websockets/uri.py +225 -0
  56. endstone_qqsync_plugin/lib/websockets/utils.py +51 -0
  57. endstone_qqsync_plugin/lib/websockets/version.py +92 -0
  58. endstone_qqsync_plugin/qqsync_plugin.py +356 -0
  59. endstone_qqsync_plugin/ui/__init__.py +7 -0
  60. endstone_qqsync_plugin/ui/forms.py +403 -0
  61. endstone_qqsync_plugin/utils/__init__.py +27 -0
  62. endstone_qqsync_plugin/utils/helpers.py +45 -0
  63. endstone_qqsync_plugin/utils/imports.py +31 -0
  64. endstone_qqsync_plugin/utils/info.py +262 -0
  65. endstone_qqsync_plugin/utils/message_utils.py +273 -0
  66. endstone_qqsync_plugin/utils/time_utils.py +347 -0
  67. endstone_qqsync_plugin/websocket/__init__.py +18 -0
  68. endstone_qqsync_plugin/websocket/client.py +244 -0
  69. endstone_qqsync_plugin/websocket/handlers.py +1117 -0
  70. endstone_qqsync_plugin-0.0.7.dist-info/METADATA +6 -0
  71. endstone_qqsync_plugin-0.0.7.dist-info/RECORD +73 -0
  72. endstone_qqsync_plugin-0.0.7.dist-info/WHEEL +5 -0
  73. endstone_qqsync_plugin-0.0.7.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,511 @@
1
+ """
2
+ 数据管理模块
3
+ 负责QQ绑定数据的存储、查询和管理
4
+ """
5
+
6
+ import json
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Dict, List, Any, Optional
10
+ from ..utils.time_utils import TimeUtils
11
+
12
+
13
+ class DataManager:
14
+ """数据管理器"""
15
+
16
+ def __init__(self, data_folder: Path, logger):
17
+ self.data_folder = data_folder
18
+ self.logger = logger
19
+ self.binding_file = data_folder / "data.json"
20
+ self._binding_data: Dict[str, Any] = {}
21
+ self._auto_save_enabled = True
22
+ self._init_binding_data()
23
+
24
+ def _init_binding_data(self):
25
+ """初始化QQ绑定数据文件"""
26
+ # 如果绑定数据文件不存在,创建空数据
27
+ if not self.binding_file.exists():
28
+ self.binding_file.parent.mkdir(parents=True, exist_ok=True)
29
+ with open(self.binding_file, 'w', encoding='utf-8') as f:
30
+ json.dump({}, f, indent=2, ensure_ascii=False)
31
+ self.logger.info(f"已创建QQ绑定数据文件: {self.binding_file}")
32
+
33
+ # 读取绑定数据
34
+ try:
35
+ with open(self.binding_file, 'r', encoding='utf-8') as f:
36
+ self._binding_data = json.load(f)
37
+ except Exception as e:
38
+ self.logger.error(f"读取QQ绑定数据失败: {e}")
39
+ self._binding_data = {}
40
+
41
+ # 更新旧数据结构兼容性
42
+ self._update_data_structure()
43
+
44
+ from endstone import ColorFormat
45
+ self.logger.info(f"{ColorFormat.AQUA}QQ绑定数据已加载,已绑定玩家: {len(self._binding_data)}{ColorFormat.RESET}")
46
+
47
+ def _update_data_structure(self):
48
+ """更新数据结构以保持兼容性"""
49
+ data_updated = False
50
+ invalid_bindings = []
51
+
52
+ for player_name, data in self._binding_data.items():
53
+ # 添加缺失的字段
54
+ required_fields = {
55
+ "total_playtime": 0,
56
+ "last_join_time": None,
57
+ "last_quit_time": None,
58
+ "session_count": 0
59
+ }
60
+
61
+ for field, default_value in required_fields.items():
62
+ if field not in data:
63
+ data[field] = default_value
64
+ data_updated = True
65
+
66
+ # 检查并清理无效的QQ绑定
67
+ qq_number = data.get("qq", "")
68
+ bind_time = data.get("bind_time", 0)
69
+
70
+ # 只清理那些QQ为空且没有绑定时间的记录
71
+ if (not qq_number or not qq_number.strip()) and not bind_time:
72
+ invalid_bindings.append(player_name)
73
+ self.logger.warning(f"发现无效绑定:玩家 {player_name} 的QQ号为空且无绑定历史,将被清理")
74
+
75
+ # 清理无效绑定
76
+ for player_name in invalid_bindings:
77
+ del self._binding_data[player_name]
78
+ data_updated = True
79
+
80
+ if invalid_bindings:
81
+ self.logger.info(f"已清理 {len(invalid_bindings)} 个无效的QQ绑定")
82
+
83
+ if data_updated:
84
+ self.save_data()
85
+ if invalid_bindings:
86
+ self.logger.info("已更新绑定数据结构并清理无效绑定")
87
+ else:
88
+ self.logger.info("已更新绑定数据结构以支持在线时间统计")
89
+
90
+ def save_data(self):
91
+ """保存QQ绑定数据到文件"""
92
+ try:
93
+ # 创建临时文件,避免写入过程中的数据损坏
94
+ temp_file = self.binding_file.with_suffix('.tmp')
95
+
96
+ with open(temp_file, 'w', encoding='utf-8') as f:
97
+ json.dump(self._binding_data, f, indent=2, ensure_ascii=False)
98
+
99
+ # 原子性替换文件
100
+ temp_file.replace(self.binding_file)
101
+
102
+ except Exception as e:
103
+ self.logger.error(f"保存QQ绑定数据失败: {e}")
104
+ # 如果临时文件存在,清理它
105
+ temp_file = self.binding_file.with_suffix('.tmp')
106
+ if temp_file.exists():
107
+ try:
108
+ temp_file.unlink()
109
+ except:
110
+ pass
111
+
112
+ def trigger_save(self, reason: str = "数据变更"):
113
+ """触发数据保存"""
114
+ if self._auto_save_enabled:
115
+ self.save_data()
116
+ self.logger.info(f"数据保存: {reason}")
117
+
118
+ # 玩家绑定相关方法
119
+ def is_player_bound(self, player_name: str, player_xuid: str = None) -> bool:
120
+ """检查玩家是否已绑定QQ"""
121
+ # 如果提供了XUID,优先通过XUID查找
122
+ if player_xuid:
123
+ player_data = self._get_player_by_xuid(player_xuid)
124
+ if player_data:
125
+ qq_number = player_data.get("qq", "")
126
+ return bool(qq_number and qq_number.strip())
127
+ else:
128
+ # 通过XUID没有找到,继续用玩家名查找(向后兼容)
129
+ if player_name in self._binding_data:
130
+ qq_number = self._binding_data[player_name].get("qq", "")
131
+ return bool(qq_number and qq_number.strip())
132
+ return False
133
+
134
+ # 仅基于玩家名的检查
135
+ if player_name not in self._binding_data:
136
+ return False
137
+
138
+ qq_number = self._binding_data[player_name].get("qq", "")
139
+ return bool(qq_number and qq_number.strip())
140
+
141
+ def _get_player_by_xuid(self, xuid: str) -> Dict[str, Any]:
142
+ """根据XUID获取玩家绑定信息"""
143
+ for name, data in self._binding_data.items():
144
+ if data.get("xuid") == xuid:
145
+ return data
146
+ return {}
147
+
148
+ def get_player_qq(self, player_name: str) -> str:
149
+ """获取玩家绑定的QQ号"""
150
+ return self._binding_data.get(player_name, {}).get("qq", "")
151
+
152
+ def get_qq_player(self, qq_number: str) -> str:
153
+ """根据QQ号获取绑定的玩家名"""
154
+ for name, data in self._binding_data.items():
155
+ current_qq = data.get("qq", "")
156
+ if current_qq and current_qq.strip() == qq_number:
157
+ return name
158
+ return ""
159
+
160
+ def get_qq_player_history(self, qq_number: str) -> str:
161
+ """根据QQ号获取历史绑定的玩家名(包括已解绑的)"""
162
+ for name, data in self._binding_data.items():
163
+ # 首先检查当前绑定的QQ号
164
+ if data.get("qq") == qq_number:
165
+ return name
166
+ # 检查原QQ号(用于被解绑或封禁的玩家历史查询)
167
+ if data.get("original_qq") == qq_number:
168
+ return name
169
+ return ""
170
+
171
+ def get_player_by_xuid(self, xuid: str) -> Dict[str, Any]:
172
+ """根据XUID获取玩家绑定信息"""
173
+ return self._get_player_by_xuid(xuid)
174
+
175
+ def bind_player_qq(self, player_name: str, player_xuid: str, qq_number: str) -> bool:
176
+ """绑定玩家QQ"""
177
+ # 验证参数
178
+ if not qq_number or not qq_number.strip():
179
+ self.logger.error(f"尝试绑定空QQ号给玩家 {player_name},操作被拒绝")
180
+ return False
181
+
182
+ qq_clean = qq_number.strip()
183
+ if not qq_clean.isdigit() or len(qq_clean) < 5 or len(qq_clean) > 11:
184
+ self.logger.error(f"尝试绑定无效QQ号 {qq_clean} 给玩家 {player_name},QQ号必须是5-11位数字")
185
+ return False
186
+
187
+ if not player_name or not player_name.strip():
188
+ self.logger.error(f"尝试绑定QQ {qq_clean} 给空玩家名,操作被拒绝")
189
+ return False
190
+
191
+ # 检查是否已有该玩家的数据
192
+ if player_name in self._binding_data:
193
+ # 保留现有的游戏数据,更新绑定信息
194
+ player_data = self._binding_data[player_name]
195
+ old_qq = player_data.get("qq", "")
196
+
197
+ # 更新绑定信息
198
+ player_data["qq"] = qq_clean
199
+ player_data["xuid"] = player_xuid
200
+
201
+ if old_qq:
202
+ # 重新绑定
203
+ player_data["rebind_time"] = int(TimeUtils.get_timestamp())
204
+ player_data["previous_qq"] = old_qq
205
+ self.logger.info(f"玩家 {player_name} 重新绑定QQ: {old_qq} → {qq_clean}")
206
+ else:
207
+ # 首次绑定或解绑后重新绑定
208
+ if "unbind_time" in player_data:
209
+ player_data["rebind_time"] = int(TimeUtils.get_timestamp())
210
+ self.logger.info(f"玩家 {player_name} 解绑后重新绑定QQ: {qq_clean}")
211
+ else:
212
+ player_data["bind_time"] = int(TimeUtils.get_timestamp())
213
+ self.logger.info(f"玩家 {player_name} 首次绑定QQ: {qq_clean}")
214
+ else:
215
+ # 全新的玩家数据
216
+ self._binding_data[player_name] = {
217
+ "name": player_name,
218
+ "xuid": player_xuid,
219
+ "qq": qq_clean,
220
+ "bind_time": int(TimeUtils.get_timestamp()),
221
+ "total_playtime": 0,
222
+ "last_join_time": None,
223
+ "last_quit_time": None,
224
+ "session_count": 0
225
+ }
226
+ self.logger.info(f"玩家 {player_name} 已绑定QQ: {qq_clean}")
227
+
228
+ self.trigger_save(f"绑定QQ: {player_name} → {qq_clean}")
229
+ return True
230
+
231
+ def unbind_player_qq(self, player_name: str, admin_name: str = "system") -> bool:
232
+ """解绑玩家QQ(保留游戏数据)"""
233
+ if player_name not in self._binding_data:
234
+ return False
235
+
236
+ player_data = self._binding_data[player_name]
237
+ original_qq = player_data.get("qq", "")
238
+
239
+ if not original_qq or not original_qq.strip():
240
+ return False
241
+
242
+ # 保留所有游戏数据,只清空QQ相关信息
243
+ player_data["qq"] = ""
244
+ player_data["unbind_time"] = int(TimeUtils.get_timestamp())
245
+ player_data["unbind_by"] = admin_name
246
+ player_data["original_qq"] = original_qq
247
+
248
+ self.trigger_save(f"解绑QQ: {player_name} (原QQ: {original_qq})")
249
+ self.logger.info(f"玩家 {player_name} 的QQ绑定已被 {admin_name} 解除 (原QQ: {original_qq}),游戏数据已保留")
250
+ return True
251
+
252
+ def update_player_name(self, old_name: str, new_name: str, xuid: str) -> bool:
253
+ """更新玩家名称(处理改名情况)"""
254
+ if old_name in self._binding_data:
255
+ # 保存原有数据
256
+ player_data = self._binding_data[old_name].copy()
257
+ # 更新名称
258
+ player_data["name"] = new_name
259
+ player_data["last_name_update"] = int(TimeUtils.get_timestamp())
260
+
261
+ # 删除旧记录,添加新记录
262
+ del self._binding_data[old_name]
263
+ self._binding_data[new_name] = player_data
264
+
265
+ self.trigger_save(f"玩家改名: {old_name} → {new_name}")
266
+ self.logger.info(f"玩家改名: {old_name} → {new_name} (XUID: {xuid})")
267
+ return True
268
+ return False
269
+
270
+ # 游戏统计相关方法
271
+ def update_player_join(self, player_name: str, player_xuid: str = None):
272
+ """更新玩家加入时间"""
273
+ if player_name not in self._binding_data:
274
+ return
275
+
276
+ # 检查玩家是否已绑定QQ
277
+ if not self._binding_data[player_name].get("qq", "").strip():
278
+ return
279
+
280
+ current_time = int(TimeUtils.get_timestamp())
281
+ self._binding_data[player_name]["last_join_time"] = current_time
282
+ self._binding_data[player_name]["session_count"] = self._binding_data[player_name].get("session_count", 0) + 1
283
+
284
+ # 更新XUID(如果提供了新的XUID)
285
+ if player_xuid and not self._binding_data[player_name].get("xuid"):
286
+ self._binding_data[player_name]["xuid"] = player_xuid
287
+
288
+ self.trigger_save(f"玩家加入: {player_name}")
289
+
290
+ def update_player_quit(self, player_name: str):
291
+ """更新玩家离开时间和总在线时间"""
292
+ if player_name not in self._binding_data:
293
+ return
294
+
295
+ # 检查玩家是否已绑定QQ
296
+ if not self._binding_data[player_name].get("qq", "").strip():
297
+ return
298
+
299
+ current_time = int(TimeUtils.get_timestamp())
300
+ last_join = self._binding_data[player_name].get("last_join_time")
301
+
302
+ if last_join:
303
+ # 计算本次会话时间
304
+ session_time = current_time - last_join
305
+ if session_time > 0:
306
+ self._binding_data[player_name]["total_playtime"] = self._binding_data[player_name].get("total_playtime", 0) + session_time
307
+
308
+ self._binding_data[player_name]["last_quit_time"] = current_time
309
+ self.trigger_save(f"玩家离开: {player_name}")
310
+
311
+ def get_player_playtime_info(self, player_name: str, online_players: List[Any]) -> Dict[str, Any]:
312
+ """获取玩家在线时间信息"""
313
+ if player_name not in self._binding_data:
314
+ return {}
315
+
316
+ data = self._binding_data[player_name]
317
+
318
+ # 检查玩家是否已绑定QQ
319
+ if not data.get("qq", "").strip():
320
+ return {}
321
+
322
+ current_time = int(TimeUtils.get_timestamp())
323
+ total_playtime = data.get("total_playtime", 0)
324
+ last_join = data.get("last_join_time")
325
+
326
+ # 如果玩家当前在线,加上当前会话时间
327
+ is_online = False
328
+ current_session_time = 0
329
+ if last_join:
330
+ # 检查玩家是否在线
331
+ for player in online_players:
332
+ if player.name == player_name:
333
+ is_online = True
334
+ current_session_time = current_time - last_join
335
+ break
336
+
337
+ total_with_current = total_playtime + (current_session_time if is_online else 0)
338
+
339
+ return {
340
+ "total_playtime": total_with_current,
341
+ "session_count": data.get("session_count", 0),
342
+ "last_join_time": last_join,
343
+ "last_quit_time": data.get("last_quit_time"),
344
+ "is_online": is_online,
345
+ "current_session_time": current_session_time if is_online else 0,
346
+ "bind_time": data.get("bind_time")
347
+ }
348
+
349
+ # 封禁相关方法
350
+ def is_player_banned(self, player_name: str) -> bool:
351
+ """检查玩家是否被封禁"""
352
+ if player_name not in self._binding_data:
353
+ return False
354
+ return self._binding_data[player_name].get("is_banned", False)
355
+
356
+ def ban_player(self, player_name: str, admin_name: str = "system", reason: str = "") -> bool:
357
+ """封禁玩家"""
358
+ # 确保玩家数据存在
359
+ if player_name not in self._binding_data:
360
+ self._binding_data[player_name] = {
361
+ "name": player_name,
362
+ "xuid": "",
363
+ "qq": "",
364
+ "total_playtime": 0,
365
+ "last_join_time": None,
366
+ "last_quit_time": None,
367
+ "session_count": 0
368
+ }
369
+
370
+ # 设置封禁状态
371
+ player_data = self._binding_data[player_name]
372
+ player_data["is_banned"] = True
373
+ player_data["ban_time"] = int(TimeUtils.get_timestamp())
374
+ player_data["ban_by"] = admin_name
375
+ player_data["ban_reason"] = reason or "管理员封禁"
376
+
377
+ # 如果玩家已绑定QQ,解除绑定
378
+ if player_data.get("qq"):
379
+ original_qq = player_data["qq"]
380
+ player_data["qq"] = ""
381
+ player_data["unbind_time"] = int(TimeUtils.get_timestamp())
382
+ player_data["unbind_by"] = admin_name
383
+ player_data["unbind_reason"] = "封禁时自动解绑"
384
+ player_data["original_qq"] = original_qq
385
+ self.logger.info(f"玩家 {player_name} 被封禁时自动解除QQ绑定 (原QQ: {original_qq})")
386
+
387
+ self.trigger_save(f"封禁玩家: {player_name} (原因: {reason or '管理员封禁'})")
388
+ self.logger.info(f"玩家 {player_name} 已被 {admin_name} 封禁,原因:{reason or '管理员封禁'}")
389
+ return True
390
+
391
+ def unban_player(self, player_name: str, admin_name: str = "system") -> bool:
392
+ """解封玩家"""
393
+ if player_name not in self._binding_data:
394
+ return False
395
+
396
+ player_data = self._binding_data[player_name]
397
+ if not player_data.get("is_banned", False):
398
+ return False
399
+
400
+ # 解除封禁
401
+ player_data["is_banned"] = False
402
+ player_data["unban_time"] = int(TimeUtils.get_timestamp())
403
+ player_data["unban_by"] = admin_name
404
+
405
+ self.trigger_save(f"解封玩家: {player_name}")
406
+ self.logger.info(f"玩家 {player_name} 已被 {admin_name} 解封")
407
+ return True
408
+
409
+ def get_banned_players(self) -> List[Dict[str, Any]]:
410
+ """获取所有被封禁的玩家列表"""
411
+ banned_players = [
412
+ {
413
+ "name": player_name,
414
+ "ban_time": data.get("ban_time"),
415
+ "ban_by": data.get("ban_by", "unknown"),
416
+ "ban_reason": data.get("ban_reason", "无原因")
417
+ }
418
+ for player_name, data in self._binding_data.items()
419
+ if data.get("is_banned", False)
420
+ ]
421
+ return banned_players
422
+
423
+ def get_player_binding_history(self, player_name: str) -> Dict[str, Any]:
424
+ """获取玩家绑定历史信息"""
425
+ if player_name not in self._binding_data:
426
+ return {}
427
+
428
+ data = self._binding_data[player_name]
429
+
430
+ history = {
431
+ "current_qq": data.get("qq", ""),
432
+ "is_bound": bool(data.get("qq", "").strip()),
433
+ "bind_time": data.get("bind_time"),
434
+ "unbind_time": data.get("unbind_time"),
435
+ "rebind_time": data.get("rebind_time"),
436
+ "unbind_by": data.get("unbind_by"),
437
+ "original_qq": data.get("original_qq"),
438
+ "previous_qq": data.get("previous_qq"),
439
+ "total_playtime": data.get("total_playtime", 0),
440
+ "session_count": data.get("session_count", 0),
441
+ }
442
+
443
+ # 计算绑定状态
444
+ if history["is_bound"]:
445
+ if history["rebind_time"]:
446
+ history["status"] = "重新绑定"
447
+ else:
448
+ history["status"] = "已绑定"
449
+ else:
450
+ if history["unbind_time"]:
451
+ history["status"] = "已解绑"
452
+ else:
453
+ history["status"] = "从未绑定"
454
+
455
+ return history
456
+
457
+ def get_complete_player_binding_status(self, player_name: str, player_xuid: str) -> Dict[str, Any]:
458
+ """获取玩家完整的绑定状态信息"""
459
+ result = {
460
+ "is_bound": False,
461
+ "qq_number": "",
462
+ "binding_source": "",
463
+ "data_consistent": True,
464
+ "issues": []
465
+ }
466
+
467
+ # 检查基于玩家名的绑定
468
+ name_bound = False
469
+ name_qq = ""
470
+ if player_name in self._binding_data:
471
+ name_qq = self._binding_data[player_name].get("qq", "")
472
+ name_bound = bool(name_qq and name_qq.strip())
473
+
474
+ # 检查基于XUID的绑定
475
+ xuid_data = self._get_player_by_xuid(player_xuid)
476
+ xuid_bound = False
477
+ xuid_qq = ""
478
+ if xuid_data:
479
+ xuid_qq = xuid_data.get("qq", "")
480
+ xuid_bound = bool(xuid_qq and xuid_qq.strip())
481
+
482
+ # 分析绑定状态
483
+ if name_bound and xuid_bound:
484
+ if name_qq == xuid_qq:
485
+ result["is_bound"] = True
486
+ result["qq_number"] = name_qq
487
+ result["binding_source"] = "both"
488
+ else:
489
+ result["is_bound"] = False
490
+ result["data_consistent"] = False
491
+ result["issues"].append(f"QQ号不一致: 玩家名对应{name_qq}, XUID对应{xuid_qq}")
492
+ elif name_bound and not xuid_bound:
493
+ result["is_bound"] = name_bound
494
+ result["qq_number"] = name_qq
495
+ result["binding_source"] = "name"
496
+ result["issues"].append("仅玩家名有绑定记录,XUID无对应数据")
497
+ elif not name_bound and xuid_bound:
498
+ result["is_bound"] = xuid_bound
499
+ result["qq_number"] = xuid_qq
500
+ result["binding_source"] = "xuid"
501
+ result["issues"].append("仅XUID有绑定记录,当前玩家名无对应数据")
502
+ else:
503
+ result["is_bound"] = False
504
+ result["binding_source"] = "none"
505
+
506
+ return result
507
+
508
+ @property
509
+ def binding_data(self) -> Dict[str, Any]:
510
+ """获取完整绑定数据的副本"""
511
+ return self._binding_data.copy()