🎨 新增Player类 更改项目文件结构

This commit is contained in:
2025-10-20 23:24:33 +08:00
parent 7bda14ea81
commit 00948cffb1
26 changed files with 1536 additions and 883 deletions
+134
View File
@@ -0,0 +1,134 @@
from pathlib import Path
from zhenxun.configs.path_config import DATA_PATH
# 签到状态
g_bSignStatus = True
# 是否处于Debug模式
g_bIsDebug = False
# 数据库文件目录
g_sDBPath = DATA_PATH / "farm_db"
# 数据库文件路径
g_sDBFilePath = DATA_PATH / "farm_db/farm.db"
# 农场资源文件目录
g_sResourcePath = Path(__file__).resolve().parent / "../resource"
# 农场作物数据库
g_sPlantPath = g_sResourcePath / "db/plant.db"
# 农场配置文件目录
g_sConfigPath = Path(__file__).resolve().parent / "../config"
# 农场签到文件路径
g_sSignInPath = g_sConfigPath / "sign_in.json"
# 土地等级上限
g_iSoilLevelMax = 3
# 农场同一文本
g_sTranslation = {
"basic": {
"notFarm": "尚未开通农场,快at我发送 开通农场 开通吧 🌱🚜",
"point": "你的当前农场币为: {point} 🌾💰",
"vipPoint": "你的当前点券为: {vipPoint} 🌾💰",
"error": "❌ 农场功能异常,请稍后再试 💔",
},
"register": {
"success": "✅ 农场开通成功!\n💼 初始资金:{point}农场币 🥳🎉",
"repeat": "🎉 您已经开通农场啦~ 😄",
"error": "⚠️ 开通失败,请稍后再试 💔",
},
"buySeed": {
"notSeed": "🌱 请在指令后跟需要购买的种子名称",
"notNum": "❗️ 请输入购买数量!",
"noLevel": "🔒 你的等级不够哦,努努力吧 💪",
"noPoint": "💰 你的农场币不够哦~ 快速速氪金吧!💸",
"noVipPoint": "💰 你的点券不够哦~ 快速速氪金吧!💸",
"success": "✅ 成功购买{name},花费{total}农场币,剩余{point}农场币 🌾",
"vipSuccess": "✅ 成功购买{name},花费{total}点券,剩余{point}点券 🌾",
"errorSql": "❌ 购买失败,执行数据库错误!🛑",
"error": "❌ 购买出错!请检查需购买的种子名称!🔍",
},
"sowing": {
"notSeed": "🌱 请在指令后跟需要播种的种子名称",
"noSeed": "❌ 没有在你的仓库发现{name}种子,快去买点吧!🛒",
"noNum": "⚠️ 播种失败!仓库中的{name}种子数量不足,当前剩余{num}个种子 🍂",
"success": "✅ 播种{name}成功!仓库剩余{num}个种子 🌱",
"success2": "✅ 播种数量超出开垦土地数量,已将可播种土地成功播种{name}!仓库剩余{num}个种子 🌾",
"error": "❌ 播种失败,请稍后重试!⏳",
},
"harvest": {
"append": "🌾 收获作物:{name},数量为:{num},经验为:{exp} ✨",
"exp": "✨ 累计获得经验:{exp} 📈",
"no": "🤷‍♂️ 没有可收获的作物哦~ 不要试图拔苗助长 🚫",
"error": "❌ 收获失败,请稍后重试!⏳",
},
"eradicate": {
"success": "🗑️ 成功铲除荒废作物,累计获得经验:{exp} ✨",
"error": "❌ 没有可以铲除的作物 🚜",
},
"reclamation": {
"confirm": "⚠️ 回复“是”将执行开垦 ⛏️",
"timeOut": "⏰ 等待开垦回复超时,请重试",
"perfect": "🌟 你已经开垦了全部土地 🎉",
"next": "🔜 下次开垦所需条件:等级:{level},农场币:{num} 💰",
"next2": "🔜 下次开垦所需条件:等级:{level},农场币:{num},物品:{item} 📦",
"nextLevel": "📈 当前用户等级{level},升级所需等级为{next} ⏳",
"noNum": "💰 当前用户农场币不足,升级所需农场币为{num} 💸",
"success": "✅ 开垦土地成功!🌱",
"error": "❌ 获取开垦土地条件失败!",
"error1": "❌ 执行开垦失败!",
"error2": "❌ 未知错误{e}💥",
},
"sellPlant": {
"no": "🤷‍♀️ 你仓库没有可以出售的作物 🌾",
"success": "💰 成功出售所有作物,获得农场币:{point},当前农场币:{num} 🎉",
"success1": "💰 成功出售{name},获得农场币:{point},当前农场币:{num} 🥳",
"error": "❌ 出售作物{name}出错:仓库中不存在该作物 🚫",
"error1": "❌ 出售作物{name}出错:数量不足 ⚠️",
},
"stealing": {
"noTarget": "🎯 请在指令后跟需要at的人 👤",
"targetNotFarm": "🚜 目标尚未开通农场,快邀请ta开通吧 😉",
"max": "❌ 你今天可偷次数到达上限啦,手下留情吧 🙏",
"info": "🤫 成功偷到作物:{name},数量为:{num} 🍒",
"noPlant": "🌱 目标没有作物可以被偷 🌾",
"repeat": "🚫 你已经偷过目标啦,请手下留情 🙏",
},
"changeName": {
"noName": "✏️ 请在指令后跟需要更改的农场名",
"success": "✅ 更新农场名成功!🎉",
"error": "❌ 农场名不支持特殊符号!🚫",
"error1": "❌ 更新农场名失败!💔",
},
"signIn": {
"success": "📝 签到成功!累计签到天数:{day}\n🎁 获得经验{exp},获得金币{num} 💰",
"grandTotal": "\n🎉 成功领取累计签到奖励:\n✨ 额外获得经验{exp},额外获得金币{num} 🥳",
"grandTotal1": ",额外获得点券{num} 🎫",
"grandTotal2": "\n🌱 获得{name}种子 * {num} 🌟",
"error": "❗️ 签到功能异常!",
"error1": "❌ 签到失败!未知错误 💔",
},
"soilInfo": {
"success": "土地成功升级至{name},效果为:{text}",
"timeOut": "等待土地升级回复超时,请重试",
"error": "土地信息尚未查询到",
"error1": "该土地已经升至满级啦~",
"red": "增产+10%",
"black": "增产+20% 时间-20%",
"gold": "增产+28% 经验+28% 时间-20%",
"amethyst": "增产+30% 经验+30% 时间-25% 幸运+1%",
"aquamarine": "增产+32% 经验+32% 时间-28% 幸运+1%",
"blackcrystal": "增产+32% 经验+40% 时间-28% 幸运+2%",
},
"lockPlant": {
"noPlant": "很抱歉,你的作物仓库中没有该作物",
"lockPlant": "加锁成功,现在{name}不会被一键售卖了",
"unlockPlant": "解锁成功,现在{name}会被一键售卖了",
"error": "未知错误",
},
}
+105
View File
@@ -0,0 +1,105 @@
import json
from zhenxun.services.log import logger
from . import config
from .request import g_pRequestManager
class CJsonManager:
def __init__(self):
self.m_pItem = {}
self.m_pLevel = {}
self.m_pSoil = {}
self.m_pSign = {}
async def init(self) -> bool:
if not await self.initItem():
return False
if not await self.initLevel():
return False
if not await self.initSoil():
return False
return await self.initSignInFile()
async def initItem(self) -> bool:
try:
with open(
config.g_sConfigPath / "item.json",
encoding="utf-8",
) as file:
self.m_pItem = json.load(file)
return True
except FileNotFoundError:
logger.warning("item.json 打开失败")
return False
except json.JSONDecodeError as e:
logger.warning(f"item.json JSON格式错误: {e}")
return False
async def initLevel(self) -> bool:
try:
with open(
config.g_sConfigPath / "level.json",
encoding="utf-8",
) as file:
self.m_pLevel = json.load(file)
return True
except FileNotFoundError:
logger.warning("level.json 打开失败")
return False
except json.JSONDecodeError as e:
logger.warning(f"level.json JSON格式错误: {e}")
return False
async def initSoil(self) -> bool:
try:
with open(
config.g_sConfigPath / "soil.json",
encoding="utf-8",
) as file:
self.m_pSoil = json.load(file)
return True
except FileNotFoundError:
logger.warning("soil.json 打开失败")
return False
except json.JSONDecodeError as e:
logger.warning(f"soil.json JSON格式错误: {e}")
return False
async def initSignInFile(self) -> bool:
if not await g_pRequestManager.initSignInFile():
config.g_bSignStatus = False
return False
else:
result = await self.initSign()
config.g_bSignStatus = result
return result
async def initSign(self) -> bool:
try:
with open(
config.g_sSignInPath,
encoding="utf-8",
) as file:
self.m_pSign = json.load(file)
return True
except FileNotFoundError:
logger.warning("sign_in.json 打开失败")
return False
except json.JSONDecodeError as e:
logger.warning(f"sign_in.json JSON格式错误: {e}")
return False
g_pJsonManager = CJsonManager()
+293
View File
@@ -0,0 +1,293 @@
import json
import os
import httpx
from rich.progress import (
BarColumn,
DownloadColumn,
Progress,
TextColumn,
TimeRemainingColumn,
TransferSpeedColumn,
)
from zhenxun.configs.config import Config
from zhenxun.services.log import logger
from ..core.dbService import g_pDBService
from .config import g_sPlantPath, g_sSignInPath
from .tool import g_pToolManager
class CRequestManager:
m_sTokens = "xZ%?z5LtWV7H:0-Xnwp+bNRNQ-jbfrxG"
@classmethod
async def download(
cls,
url: str,
savePath: str,
fileName: str,
params: dict | None = None,
jsonData: dict | None = None,
) -> bool:
"""下载文件到指定路径并覆盖已存在的文件
Args:
url (str): 文件的下载链接
savePath (str): 保存文件夹路径
fileName (str): 保存后的文件名
params (dict | None): 可选的 URL 查询参数
jsonData (dict | None): 可选的 JSON 请求体
Returns:
bool: 是否下载成功
"""
headers = {"token": cls.m_sTokens}
try:
async with httpx.AsyncClient(timeout=30.0) as client:
requestArgs: dict = {"headers": headers}
if params:
requestArgs["params"] = params
if jsonData:
requestArgs["json"] = jsonData
response = await client.request(
"GET", url, **requestArgs, follow_redirects=True
)
if response.status_code != 200:
logger.warning(
f"文件下载失败: HTTP {response.status_code} {response.text}"
)
return False
totalLength = int(response.headers.get("Content-Length", 0))
fullPath = os.path.join(savePath, fileName)
os.makedirs(os.path.dirname(fullPath), exist_ok=True)
with Progress(
TextColumn("[progress.description]{task.description}"),
BarColumn(),
DownloadColumn(),
TransferSpeedColumn(),
TimeRemainingColumn(),
transient=True,
) as progress:
task = progress.add_task(
f"[green]【真寻农场】正在下载 {fileName}", total=totalLength
)
with open(fullPath, "wb") as f:
async for chunk in response.aiter_bytes(chunk_size=1024):
f.write(chunk)
progress.advance(task, len(chunk))
return True
except Exception as e:
logger.warning(f"下载文件异常: {e}")
return False
@classmethod
async def post(cls, endpoint: str, name: str = "", jsonData: dict = {}) -> dict:
"""发送POST请求到指定接口,统一调用,仅支持JSON格式数据
Args:
endpoint (str): 请求的接口路径
name (str, optional): 操作名称用于日志记录
jsonData (dict): 以JSON格式发送的数据
Raises:
ValueError: 当jsonData未提供时抛出
Returns:
dict: 返回请求结果的JSON数据
"""
baseUrl = Config.get_config("zhenxun_plugin_farm", "服务地址")
url = f"{baseUrl.rstrip('/')}:8998/{endpoint.lstrip('/')}"
headers = {"token": cls.m_sTokens}
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.post(url, json=jsonData, headers=headers)
if response.status_code == 200:
return response.json()
else:
logger.warning(
f"{name}请求失败: HTTP {response.status_code} {response.text}"
)
return {}
except httpx.RequestError as e:
logger.warning(f"{name}请求异常", e=e)
return {}
except Exception as e:
logger.warning(f"{name}处理异常", e=e)
return {}
@classmethod
async def get(cls, endpoint: str, name: str = "") -> dict:
"""发送GET请求到指定接口,统一调用,仅支持无体的查询
Args:
endpoint (str): 请求的接口路径
name (str, optional): 操作名称用于日志记录
Returns:
dict: 返回请求结果的JSON数据
"""
baseUrl = Config.get_config("zhenxun_plugin_farm", "服务地址")
url = f"{baseUrl.rstrip('/')}:8998/{endpoint.lstrip('/')}"
headers = {"token": cls.m_sTokens}
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(url, headers=headers)
if response.status_code == 200:
return response.json()
else:
logger.warning(
f"{name}请求失败: HTTP {response.status_code} {response.text}"
)
return {}
except httpx.RequestError as e:
logger.warning(f"{name}请求异常", e=e)
return {}
except Exception as e:
logger.warning(f"{name}处理异常", e=e)
return {}
@classmethod
async def initSignInFile(cls) -> bool:
if os.path.exists(g_sSignInPath):
try:
with open(g_sSignInPath, encoding="utf-8") as f:
content = f.read()
sign = json.loads(content)
date = sign.get("date", "")
yearMonth = g_pToolManager.dateTime().now().strftime("%Y%m")
if date == yearMonth:
logger.debug("真寻农场签到文件检查完毕")
return True
else:
logger.warning("真寻农场签到文件检查失败, 即将下载")
return await cls.downloadSignInFile()
except json.JSONDecodeError:
logger.warning("真寻农场签到文件格式错误, 即将下载")
return await cls.downloadSignInFile()
else:
return await cls.downloadSignInFile()
@classmethod
async def downloadSignInFile(cls) -> bool:
"""下载签到文件,并重命名为 sign_in.json
Returns:
bool: 是否下载成功
"""
try:
baseUrl = Config.get_config("zhenxun_plugin_farm", "服务地址")
url = f"{baseUrl.rstrip('/')}:8998/sign_in"
path = str(g_sSignInPath.parent.resolve(strict=False))
yearMonth = g_pToolManager.dateTime().now().strftime("%Y%m")
# 下载为 signTemp.json
success = await cls.download(
url=url,
savePath=path,
fileName="signTemp.json",
jsonData={"date": yearMonth},
)
if not success:
return False
# 重命名为 sign_in.json
g_pToolManager.renameFile(f"{path}/signTemp.json", "sign_in.json")
return True
except Exception as e:
logger.error("下载签到文件失败", e=e)
return False
@classmethod
async def initPlantDBFile(cls) -> bool:
"""检查本地 plant.db 版本,如远程版本更新则重新下载
Returns:
bool: 是否为最新版或成功更新
"""
versionPath = os.path.join(os.path.dirname(g_sPlantPath), "version.json")
try:
with open(versionPath, encoding="utf-8") as f:
localVersion = json.load(f).get("version", 0)
except Exception as e:
logger.warning(f"读取本地版本失败,默认版本为0: {e}")
localVersion = 0
remoteInfo = await cls.get("plant_version", name="版本检查")
remoteVersion = remoteInfo.get("version")
if remoteVersion is None:
logger.warning("获取远程版本失败")
return False
if float(remoteVersion) <= float(localVersion):
logger.debug("plant.db 已为最新版本")
return True
logger.warning(
f"发现新版本 plant.db(远程: {remoteVersion} / 本地: {localVersion}),开始更新..."
)
# 先断开数据库连接
await g_pDBService.cleanup()
return await cls.downloadPlantDBFile(remoteVersion)
@classmethod
async def downloadPlantDBFile(cls, remoteVersion: float) -> bool:
"""下载最新版 plant.db 并更新本地 version.json
Args:
remoteVersion (float): 远程版本号
Returns:
bool: 是否下载并更新成功
"""
baseUrl = Config.get_config("zhenxun_plugin_farm", "服务地址")
savePath = os.path.dirname(g_sPlantPath)
success = await cls.download(
url=f"{baseUrl.rstrip('/')}:8998/file/plant.db",
savePath=savePath,
fileName="plantTemp.db",
)
if not success:
return False
# 重命名为 sign_in.json
g_pToolManager.renameFile(f"{savePath}/plantTemp.db", "plant.db")
versionPath = os.path.join(savePath, "version.json")
try:
with open(versionPath, "w", encoding="utf-8") as f:
json.dump({"version": remoteVersion}, f)
logger.debug("版本文件已更新")
except Exception as e:
logger.warning(f"写入版本文件失败: {e}")
return False
await g_pDBService.plant.init()
await g_pDBService.plant.downloadPlant()
return True
g_pRequestManager = CRequestManager()
+111
View File
@@ -0,0 +1,111 @@
from datetime import datetime
import os
from zoneinfo import ZoneInfo
from zhenxun.services.log import logger
from zhenxun.utils.message import MessageUtils
from ..core.player.player import CPlayer
from ..core.player.playerPool import g_pUserPool
class CToolManager:
@classmethod
async def repeat(cls):
await MessageUtils.build_message(
"尚未开通农场,快at我发送 开通农场 开通吧"
).send()
@classmethod
async def getPlayerByUid(cls, uid: str) -> CPlayer | None:
player = g_pUserPool.getUser(uid)
if player is None:
player = CPlayer()
if not await player.init(uid):
return None
g_pUserPool.createUser(uid, player)
return player
@classmethod
def sanitize_username(cls, username: str, max_length: int = 15) -> str:
"""
安全处理用户名
功能:
1. 移除首尾空白
2. 过滤危险字符
3. 转义单引号
4. 处理空值
5. 限制长度
"""
# 处理空值
if not username:
return "神秘农夫"
# 基础清洗
cleaned = username.strip()
# 允许的字符白名单(可自定义扩展)
# fmt: off
safe_chars = {
"_", "-", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")",
"+", "=", ".", ",", "~", "·", " ",
"a","b","c","d","e","f","g","h","i","j","k","l","m",
"n","o","p","q","r","s","t","u","v","w","x","y","z",
"A","B","C","D","E","F","G","H","I","J","K","L","M",
"N","O","P","Q","R","S","T","U","V","W","X","Y","Z",
"0","1","2","3","4","5","6","7","8","9",
}
# fmt: on
# 添加常用中文字符(Unicode范围)
safe_chars.update(chr(c) for c in range(0x4E00, 0x9FFF + 1))
# 过滤危险字符
filtered = [
c if c in safe_chars or 0x4E00 <= ord(c) <= 0x9FFF else "" for c in cleaned
]
# 合并处理结果
safe_str = "".join(filtered)
# 转义单引号(双重保障)
escaped = safe_str.replace("'", "''")
# 处理空结果
if not escaped:
return "神秘农夫"
# 长度限制
return escaped[:max_length]
@classmethod
def renameFile(cls, currentFilePath: str, newFileName: str) -> bool:
"""重命名文件,如果目标文件名已存在则先删除再重命名
Args:
currentFilePath (str): 当前文件的完整路径
newFileName (str): 重命名后的文件名
Returns:
bool: 重命名成功返回 True,否则返回 False
"""
try:
dirPath = os.path.dirname(currentFilePath)
newFilePath = os.path.join(dirPath, newFileName)
if os.path.exists(newFilePath):
os.remove(newFilePath)
os.rename(currentFilePath, newFilePath)
return True
except Exception as e:
logger.warning(f"文件重命名失败: {e}")
return False
@classmethod
def dateTime(cls) -> datetime:
tz = ZoneInfo("Asia/Shanghai")
return datetime.now(tz)
g_pToolManager = CToolManager()