Skip to content

Commit 21b8e11

Browse files
committed
feat(phi-plugin): 优化存档解析性能并添加惰性加载功能
- 重构 PhigrosUser 类的 buildRecord 方法,使用 asyncio.gather 并发读取和解密存档文件,提升性能 - 添加 LevelRecordInfo 类的惰性加载功能,优化内存使用 - 更新 SaveInfoSummary 和 SaveInfoGameFile 类,将日期字段改为 datetime 类型 - 移除 default.html 中的版权信息显示位置,改为在页面底部显示
1 parent fc9b74c commit 21b8e11

8 files changed

Lines changed: 158 additions & 91 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
>
3131
> 当前 `Save -> gameRecord: dict[str, list["LevelRecordInfo | None"]]``LevelRecordInfo` 补全存在严重的性能问题,正在着手解决但希望渺茫
3232
33+
## 🏃‍♀️ 我跑路了
34+
35+
由于无法解决性能问题,我跑路了,等有大佬能解决了再说,不写了不写了(˙꒫˙`)
36+
3337

3438
## 长期寻求合作者共同开发
3539

phi-plugin/apps/session.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,7 @@ async def _(bot, session: Uninfo, sstk: Match[str]):
9999
f"二维码剩余时间:{QRCodetimeout}",
100100
await getQRcode.getQRcode(qrcode),
101101
],
102-
False,
103-
recallTime,
102+
recallTime=recallTime,
104103
)
105104
else:
106105
qrCodeMsg = await send.sendWithAt(

phi-plugin/lib/PhigrosUser.py

Lines changed: 39 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import io
23
import re
34
from typing import Any
@@ -89,70 +90,54 @@ async def getSaveInfo(self) -> SaveInfo:
8990
raise ValueError("设置saveUrl失败") from e
9091
return self.saveInfo[0]
9192

92-
async def buildRecord(self) -> bool:
93+
async def buildRecord(self):
9394
"""
94-
返回未绑定的信息数组,没有则为false
95-
96-
(注: 看实际逻辑感觉是构建记录,然后不会有返回,失败直接抛出)
97-
98-
:return: 是否成功
95+
构建记录
9996
"""
10097
if not hasattr(self, "saveUrl"):
10198
await self.getSaveInfo()
10299

103100
if self.saveInfo[0].summary.saveVersion == "1":
104101
raise ValueError("存档版本过低,请更新Phigros!")
105102

106-
if getattr(self, "saveUrl", None):
107-
# 从saveurl获取存档zip
108-
try:
109-
response = await AsyncHttpx.get(self.saveUrl)
110-
if not response:
111-
raise ValueError("获取存档失败")
112-
savezip = readZip(response.content)
113-
114-
# 插件存档版本
115-
self.Recordver = 1.0
116-
117-
# 获取 gameProgress
118-
file = ByteReader(await savezip.file("gameProgress"))
119-
file.getByte()
120-
self.gameProgress = GameProgress(
121-
await SaveManager.decrypt(file.getAllByte())
122-
)
123-
124-
# 获取 gameuser
125-
file = ByteReader(await savezip.file("user"))
126-
file.getByte()
127-
self.gameuser = GameUser(await SaveManager.decrypt(file.getAllByte()))
103+
save_url = getattr(self, "saveUrl", None)
104+
if not save_url:
105+
logger.info("获取存档链接失败!", "phi-plugin")
106+
raise RuntimeError("获取存档链接失败!")
128107

129-
# 获取 gamesetting
130-
file = ByteReader(await savezip.file("settings"))
108+
try:
109+
response = await AsyncHttpx.get(save_url)
110+
if not response:
111+
raise ValueError("获取存档失败")
112+
savezip = readZip(response.content)
113+
self.Recordver = 1.0
114+
115+
# 并发读取和解密各文件
116+
async def read_and_decrypt(filename):
117+
file = ByteReader(await savezip.file(filename))
131118
file.getByte()
132-
self.gamesettings = GameSettings(
133-
await SaveManager.decrypt(file.getAllByte())
119+
return await SaveManager.decrypt(file.getAllByte())
120+
121+
files = ["gameProgress", "user", "settings", "gameRecord"]
122+
results = await asyncio.gather(*(read_and_decrypt(f) for f in files))
123+
124+
self.gameProgress = GameProgress(results[0])
125+
self.gameuser = GameUser(results[1])
126+
self.gamesettings = GameSettings(results[2])
127+
128+
# gameRecord版本校验
129+
file = ByteReader(await savezip.file("gameRecord"))
130+
if file.getByte() != GameRecord.version:
131+
self.gameRecord = {}
132+
logger.info(
133+
"[PhigrosUser]版本号已更新,请更新PhigrosLibrary。",
134+
"phi-plugin",
134135
)
136+
raise ValueError("版本号已更新")
135137

136-
# 获取gameRecord
137-
file = ByteReader(await savezip.file("gameRecord"))
138-
if file.getByte() != GameRecord.version:
139-
self.gameRecord = {}
140-
logger.info(
141-
"[PhigrosUser]版本号已更新,请更新PhigrosLibrary。",
142-
"phi-plugin",
143-
)
144-
raise ValueError("版本号已更新")
145-
146-
record = GameRecord(await SaveManager.decrypt(file.getAllByte()))
147-
self.gameRecord = record.Record
148-
return True
149-
150-
except Exception as e:
151-
if isinstance(e, ValueError):
152-
raise e
153-
logger.error("解压zip文件失败", "phi-plugin", e=e)
154-
raise RuntimeError("解压zip文件失败") from e
138+
record = GameRecord(results[3])
139+
self.gameRecord = record.Record
155140

156-
else:
157-
logger.info("获取存档链接失败!", "phi-plugin")
158-
raise RuntimeError("获取存档链接失败!")
141+
except Exception as e:
142+
logger.error("解压zip文件失败", "phi-plugin", e=e)
143+
raise RuntimeError("解压zip文件失败") from e

phi-plugin/lib/SaveManager.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from datetime import datetime
2-
31
from zhenxun.services.log import logger
42
from zhenxun.utils.http_utils import AsyncHttpx
53

@@ -78,9 +76,7 @@ async def saveCheck(session: str) -> list[dict]:
7876
for item in array:
7977
item["summary"] = to_dict(Summary(item["summary"]))
8078
item.update(await SaveManager.getPlayerId(session))
81-
date = datetime.fromisoformat(item["updatedAt"].replace("Z", "+00:00"))
82-
item["updatedAt"] = date.strftime("%Y %b.%d %H:%M:%S")
83-
if item.get("gameFile"):
79+
if "gameFile" in item:
8480
item["PlayerId"] = item["nickname"]
8581
results.append(item)
8682
return results

phi-plugin/model/cls/LevelRecordInfo.py

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from datetime import datetime
22
from pathlib import Path
3-
from typing import Literal
3+
from typing import Any, Literal
44

5-
from pydantic import BaseModel
5+
from pydantic import BaseModel, Field
66

77
from ...utils import Rating
88
from ..constNum import LevelItem
@@ -40,13 +40,104 @@ class LevelRecordInfo(BaseModel):
4040
"""定数"""
4141
rks: float = 0.0
4242
"""等效RKS"""
43-
suggest: str = ""
43+
### 不允许序列化的字段
44+
suggest: str = Field(default="", exclude=True)
4445
"""推分建议"""
45-
num: int = 0
46+
num: int = Field(default=0, exclude=True)
4647
"""是 Best 几"""
47-
date: datetime = datetime.now()
48+
date: datetime = Field(default=datetime.now(), exclude=True)
4849
"""更新时间(iso)"""
4950

51+
# 私有字段
52+
lazy_song_id: str = Field(default="", exclude=True)
53+
lazy_index: int = Field(default=0, exclude=True)
54+
lazy_raw_data: dict[str, Any] | None = Field(default=None, exclude=True)
55+
is_lazy: bool = Field(default=False, exclude=True)
56+
initialized: bool = Field(default=True, exclude=True) # 默认为已初始化
57+
58+
@classmethod
59+
def lazy_init(cls, data: dict, id: str, index: int) -> "LevelRecordInfo":
60+
"""
61+
惰性初始化方法,只设置基本字段
62+
"""
63+
# 检查数据是否包含完整字段,如果包含则直接初始化
64+
if cls._is_data_complete(data):
65+
# 数据已经完整,直接创建实例而不调用 init
66+
instance = cls(**data)
67+
# 标记为已初始化
68+
instance.initialized = True
69+
instance.is_lazy = False
70+
return instance
71+
# 数据不完整,使用惰性加载
72+
instance = cls(
73+
fc=data.get("fc", False),
74+
score=data.get("score", 0),
75+
acc=data.get("acc", 0.0),
76+
)
77+
instance.lazy_raw_data = data
78+
instance.lazy_song_id = id
79+
instance.lazy_index = index
80+
instance.is_lazy = True
81+
instance.initialized = False
82+
return instance
83+
84+
@classmethod
85+
def _is_data_complete(cls, data: dict) -> bool:
86+
"""
87+
检查传入数据是否包含完整字段
88+
"""
89+
# 定义完整字段列表
90+
complete_fields = [
91+
"song",
92+
"rank",
93+
"difficulty",
94+
"rks",
95+
"Rating",
96+
"illustration",
97+
]
98+
99+
# 检查是否包含完整字段
100+
return all(field in data for field in complete_fields)
101+
102+
def _ensure_initialized(self):
103+
"""
104+
确保完全初始化
105+
"""
106+
if not self.initialized and self.is_lazy and self.lazy_raw_data is not None:
107+
# 完整初始化
108+
full_instance = LevelRecordInfo.init(
109+
self.lazy_raw_data, self.lazy_song_id, self.lazy_index
110+
)
111+
# 复制所有字段
112+
for field_name in LevelRecordInfo.model_fields:
113+
setattr(self, field_name, getattr(full_instance, field_name))
114+
self.initialized = True
115+
self.is_lazy = False
116+
117+
# 重写 __getattribute__ 实现惰性加载
118+
def __getattribute__(self, name: str):
119+
# 避免无限递归,对特殊属性直接返回
120+
if name in {
121+
"initialized",
122+
"is_lazy",
123+
"lazy_raw_data",
124+
"lazy_song_id",
125+
"lazy_index",
126+
}:
127+
return object.__getattribute__(self, name)
128+
129+
# 检查是否是模型字段且需要初始化
130+
if name in {
131+
"song",
132+
"rank",
133+
"difficulty",
134+
"rks",
135+
"Rating",
136+
"illustration",
137+
} and not object.__getattribute__(self, "initialized"):
138+
object.__getattribute__(self, "_ensure_initialized")()
139+
return object.__getattribute__(self, name)
140+
50141
@classmethod
51142
def init(cls, data: dict, id: str, rank: int | str) -> "LevelRecordInfo":
52143
"""

phi-plugin/model/cls/common.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ class statsRecord(BaseModel):
6969

7070

7171
class SaveInfoSummary(BaseModel):
72-
updatedAt: str
73-
"""插件获取存档时间 2023 Oct.06 11:46:33"""
72+
updatedAt: datetime
73+
"""插件获取存档时间"""
7474
saveVersion: int
7575
"""存档版本"""
7676
challengeModeRank: int
@@ -88,13 +88,19 @@ class SaveInfoSummary(BaseModel):
8888
phi: tuple[float, float, float, float]
8989
"""AP曲目数量"""
9090

91+
@field_validator("updatedAt", mode="before")
92+
@classmethod
93+
def parse_updatedAt(cls, value: Any) -> datetime:
94+
"""自动将字符串转换为datetime对象"""
95+
return Date(value)
96+
9197

9298
class SaveInfoGameFile(BaseModel):
9399
__type: str # type: ignore
94100
"""文件类型"""
95101
bucket: str
96102
"""存档bucket"""
97-
createdAt: str
103+
createdAt: datetime
98104
"""存档创建时间 2023-10-05T07:41:24.503Z"""
99105
key: str
100106
"""gamesaves/{32}/.save"""
@@ -108,7 +114,7 @@ class SaveInfoGameFile(BaseModel):
108114
"""存档id length:24"""
109115
provider: str
110116
"""provider"""
111-
updatedAt: str
117+
updatedAt: datetime
112118
"""存档更新时间 2023-10-05T07:41:24.503Z"""
113119
url: str
114120
"""https://rak3ffdi.tds1.tapfiles.cn/gamesaves/{32}/.save"""
@@ -120,12 +126,6 @@ class DateField(BaseModel):
120126
iso: datetime
121127
"""iso格式日期"""
122128

123-
@field_validator("iso")
124-
@classmethod
125-
def parse_iso(cls, value: Any) -> datetime:
126-
"""自动将字符串转换为datetime对象"""
127-
return Date(value)
128-
129129

130130
class ACLValue(BaseModel):
131131
read: bool = True
@@ -135,8 +135,8 @@ class ACLValue(BaseModel):
135135

136136

137137
class SaveInfo(BaseModel):
138-
createdAt: str
139-
"""账户创建时间 2022-09-03T10:21:48.613Z"""
138+
createdAt: datetime
139+
"""账户创建时间"""
140140
gameFile: SaveInfoGameFile
141141
"""gameFile 子信息"""
142142
modifiedAt: DateField
@@ -163,8 +163,8 @@ class SaveInfo(BaseModel):
163163
"""短ID"""
164164
username: str
165165
"""用户名"""
166-
updatedAt: str
167-
"""存档上传时间 2023 Oct.06 11:46:33"""
166+
updatedAt: datetime
167+
"""存档上传时间"""
168168
user: dict
169169
"""用户信息"""
170170
PlayerId: str
@@ -241,7 +241,7 @@ class Save(BaseModel):
241241
def parse_game_record(cls, v):
242242
return {
243243
song_id: [
244-
LevelRecordInfo.init(record, song_id, index)
244+
LevelRecordInfo.lazy_init(record, song_id, index)
245245
for index, record in enumerate(records)
246246
]
247247
for song_id, records in v.items()

phi-plugin/model/cls/models.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,15 @@
22
from typing import Any
33
from typing_extensions import Self
44

5-
from nonebot.compat import field_validator
65
from pydantic import BaseModel
76

8-
from ...utils import Date, to_dict
7+
from ...utils import to_dict
98

109

1110
class RecordModel(BaseModel):
1211
date: datetime
1312
value: Any
1413

15-
@field_validator("date")
16-
@classmethod
17-
def parse_iso(cls, value: Any) -> datetime:
18-
"""自动将字符串转换为datetime对象"""
19-
return Date(value)
20-
2114

2215
class LevelData(BaseModel):
2316
fc: bool = False

phi-plugin/resources/html/common/layout/default.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,6 @@
106106
window.onresize = adjustFontSize;
107107
</script>
108108

109-
<div class="copyright">{{ sys.copyright }}</div>
110-
111109
{% if theme == "snow" %}
112110
<div class="snow-box"></div>
113111
<script>
@@ -124,5 +122,6 @@
124122
themeStar();
125123
</script>
126124
{% endif %}
125+
<div class="copyright">{{ sys.copyright }}</div>
127126
</body>
128127
</html>

0 commit comments

Comments
 (0)