From 00948cffb1e96bcbc2fd48d8409ebebcf88acf23 Mon Sep 17 00:00:00 2001 From: Shu-Ying <1754798088@qq.com> Date: Mon, 20 Oct 2025 23:24:33 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20=E6=96=B0=E5=A2=9EPlayer?= =?UTF-8?q?=E7=B1=BB=20=E6=9B=B4=E6=94=B9=E9=A1=B9=E7=9B=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __init__.py | 23 +- command.py | 149 ++++--- core/activity/activity.py | 108 +++++ core/activity/effect.py | 95 +++++ core/database/database.py | 399 +++++++++++++++++++ {database => core/database}/plant.py | 78 ++-- core/database/user.py | 215 ++++++++++ {database => core/database}/userItem.py | 0 {database => core/database}/userPlant.py | 0 {database => core/database}/userSeed.py | 0 {database => core/database}/userSign.py | 6 +- {database => core/database}/userSoil.py | 4 +- {database => core/database}/userSteal.py | 22 +- core/dbService.py | 43 ++ {farm => core}/farm.py | 80 ++-- {farm => core}/help.py | 6 +- core/player/player.py | 199 ++++++++++ core/player/playerPool.py | 216 ++++++++++ {farm => core}/shop.py | 75 ++-- database/database.py | 143 ------- database/user.py | 479 ----------------------- dbService.py | 45 --- config.py => utils/config.py | 5 +- json.py => utils/json.py | 0 request.py => utils/request.py | 3 +- tool.py => utils/tool.py | 26 +- 26 files changed, 1536 insertions(+), 883 deletions(-) create mode 100644 core/activity/activity.py create mode 100644 core/activity/effect.py create mode 100644 core/database/database.py rename {database => core/database}/plant.py (79%) create mode 100644 core/database/user.py rename {database => core/database}/userItem.py (100%) rename {database => core/database}/userPlant.py (100%) rename {database => core/database}/userSeed.py (100%) rename {database => core/database}/userSign.py (98%) rename {database => core/database}/userSoil.py (99%) rename {database => core/database}/userSteal.py (94%) create mode 100644 core/dbService.py rename {farm => core}/farm.py (94%) rename {farm => core}/help.py (96%) create mode 100644 core/player/player.py create mode 100644 core/player/playerPool.py rename {farm => core}/shop.py (75%) delete mode 100644 database/database.py delete mode 100644 database/user.py delete mode 100644 dbService.py rename config.py => utils/config.py (96%) rename json.py => utils/json.py (100%) rename request.py => utils/request.py (99%) rename tool.py => utils/tool.py (82%) diff --git a/__init__.py b/__init__.py index 0aea104..748525e 100644 --- a/__init__.py +++ b/__init__.py @@ -7,14 +7,15 @@ from zhenxun.services.log import logger from zhenxun.utils.message import MessageUtils from .command import diuse_farm, diuse_register, reclamation -from .database.database import g_pSqlManager -from .dbService import g_pDBService +from .core.database.database import g_pSqlManager +from .core.dbService import g_pDBService +from .core.farm import g_pFarmManager +from .core.help import g_pHelpManager +from .core.player.playerPool import g_pUserPool +from .core.shop import g_pShopManager from .event.event import g_pEventManager -from .farm.farm import g_pFarmManager -from .farm.help import g_pHelpManager -from .farm.shop import g_pShopManager -from .json import g_pJsonManager -from .request import g_pRequestManager +from .utils.json import g_pJsonManager +from .utils.request import g_pRequestManager __plugin_meta__ = PluginMetadata( name="真寻农场", @@ -86,12 +87,13 @@ driver = get_driver() # 构造函数 @driver.on_startup async def start(): - # 初始化数据库 + # 数据库加载 await g_pSqlManager.init() # 初始化读取Json await g_pJsonManager.init() + # 初始化数据库变量 和 加载作物数据库 await g_pDBService.init() # 检查作物文件是否缺失 or 更新 @@ -103,10 +105,11 @@ async def start(): # 析构函数 @driver.on_shutdown async def shutdown(): - await g_pSqlManager.cleanup() - + # 单独卸载 作物数据库 await g_pDBService.cleanup() + await g_pSqlManager.cleanup() + @scheduler.scheduled_job(trigger="cron", hour=4, minute=30, id="signInFile") async def signInFile(): diff --git a/command.py b/command.py index fd6f37d..50d0f66 100644 --- a/command.py +++ b/command.py @@ -24,12 +24,12 @@ from zhenxun.services.log import logger from zhenxun.utils._build_image import BuildImage from zhenxun.utils.message import MessageUtils -from .config import g_bSignStatus, g_sTranslation -from .dbService import g_pDBService -from .farm.farm import g_pFarmManager -from .farm.shop import g_pShopManager -from .json import g_pJsonManager -from .tool import g_pToolManager +from .core.dbService import g_pDBService +from .core.farm import g_pFarmManager +from .core.shop import g_pShopManager +from .utils.config import g_bSignStatus, g_sTranslation +from .utils.json import g_pJsonManager +from .utils.tool import g_pToolManager diuse_register = on_alconna( Alconna("开通农场"), @@ -43,22 +43,19 @@ diuse_register = on_alconna( @diuse_register.handle() async def handle_register(session: Uninfo): uid = str(session.user.id) - user = await g_pDBService.user.getUserInfoByUid(uid) + player = await g_pToolManager.getPlayerByUid(uid) - if user: - await MessageUtils.build_message(g_sTranslation["register"]["repeat"]).send( - reply_to=True - ) + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return try: raw_name = str(session.user.name) safe_name = g_pToolManager.sanitize_username(raw_name) - # 初始化用户信息 - success = await g_pDBService.user.initUserInfoByUid( - uid=uid, name=safe_name, exp=0, point=500 - ) + success = await g_pDBService.user.initUserInfo(uid, safe_name) + + logger.info(f"用户 {uid} 选择的农场名称为: {raw_name} | 过滤后为: {safe_name}") msg = ( g_sTranslation["register"]["success"].format(point=500) @@ -109,8 +106,10 @@ diuse_farm = on_alconna( @diuse_farm.assign("$main") async def _(session: Uninfo): uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return image = await g_pFarmManager.drawFarmByUid(uid) @@ -128,8 +127,10 @@ diuse_farm.shortcut( @diuse_farm.assign("detail") async def _(session: Uninfo): uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return info = await g_pFarmManager.drawDetailFarmByUid(uid) @@ -150,7 +151,13 @@ diuse_farm.shortcut( @diuse_farm.assign("my-point") async def _(session: Uninfo): uid = str(session.user.id) - point = await g_pDBService.user.getUserPointByUid(uid) + player = await g_pToolManager.getPlayerByUid(uid) + + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() + return + + point = player.user["point"] if point < 0: await MessageUtils.build_message(g_sTranslation["basic"]["notFarm"]).send() @@ -172,8 +179,10 @@ diuse_farm.shortcut( @diuse_farm.assign("seed-shop") async def _(session: Uninfo, res: Match[tuple[str, ...]]): uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return if res.result is inspect._empty: @@ -225,8 +234,10 @@ async def _( ) uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return result = await g_pShopManager.buySeed(uid, name.result, num.result) @@ -244,8 +255,10 @@ diuse_farm.shortcut( @diuse_farm.assign("my-seed") async def _(session: Uninfo): uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return result = await g_pFarmManager.getUserSeedByUid(uid) @@ -270,8 +283,10 @@ async def _( ) uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return result = await g_pFarmManager.sowing(uid, name.result, num.result) @@ -289,8 +304,10 @@ diuse_farm.shortcut( @diuse_farm.assign("harvest") async def _(session: Uninfo): uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return result = await g_pFarmManager.harvest(uid) @@ -308,8 +325,10 @@ diuse_farm.shortcut( @diuse_farm.assign("eradicate") async def _(session: Uninfo): uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return result = await g_pFarmManager.eradicate(uid) @@ -327,8 +346,10 @@ diuse_farm.shortcut( @diuse_farm.assign("my-plant") async def _(session: Uninfo): uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return result = await g_pFarmManager.getUserPlantByUid(uid) @@ -346,8 +367,10 @@ diuse_farm.shortcut( @diuse_farm.assign("lock-plant") async def _(session: Uninfo, name: Match[str]): uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return result = await g_pFarmManager.lockUserPlantByUid(uid, name.result, 1) @@ -365,8 +388,10 @@ diuse_farm.shortcut( @diuse_farm.assign("unlock-plant") async def _(session: Uninfo, name: Match[str]): uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return result = await g_pFarmManager.lockUserPlantByUid(uid, name.result, 0) @@ -386,8 +411,10 @@ async def _( session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("num", -1) ): uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return result = await g_pShopManager.sellPlantByUid(uid, name.result, num.result) @@ -405,8 +432,10 @@ reclamation = on_alconna( @reclamation.handle() async def _(session: Uninfo): uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return condition = await g_pFarmManager.reclamationCondition(uid) @@ -441,8 +470,10 @@ diuse_farm.shortcut( @diuse_farm.assign("stealing") async def _(session: Uninfo, target: Match[At]): uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return if not target.available: @@ -451,7 +482,7 @@ async def _(session: Uninfo, target: Match[At]): ) tar = target.result - result = await g_pDBService.user.isUserExist(tar.target) + result = await g_pDBService.user.isRegistered(tar.target) if not result: await MessageUtils.build_message( @@ -479,8 +510,10 @@ async def _(session: Uninfo, num: Query[int] = AlconnaQuery("num", 0)): ) uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return result = await g_pFarmManager.buyPointByUid(uid, num.result) @@ -503,28 +536,23 @@ async def _(session: Uninfo, name: Match[str]): ) uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return - safeName = g_pToolManager.sanitize_username(name.result) - - if safeName == "神秘农夫": - await MessageUtils.build_message(g_sTranslation["changeName"]["error"]).send( - reply_to=True - ) - return - - result = await g_pDBService.user.updateUserNameByUid(uid, safeName) - - if result: - await MessageUtils.build_message(g_sTranslation["changeName"]["success"]).send( - reply_to=True - ) - else: + player = await g_pToolManager.getPlayerByUid(uid) + if player is None: await MessageUtils.build_message(g_sTranslation["changeName"]["error1"]).send( reply_to=True ) + return + + result = await player.updateName(name.result) + await MessageUtils.build_message(g_sTranslation["changeName"][result]).send( + reply_to=True + ) diuse_farm.shortcut( @@ -538,8 +566,10 @@ diuse_farm.shortcut( @diuse_farm.assign("sign-in") async def _(session: Uninfo): uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return # 判断签到是否正常加载 @@ -593,8 +623,6 @@ async def _(session: Uninfo): await MessageUtils.build_message(message).send() - # await MessageUtils.alc_forward_msg([info], session.self_id, BotConfig.self_nickname).send(reply_to=True) - soil_upgrade = on_alconna( Alconna("土地升级", Args["index", int]), @@ -607,8 +635,10 @@ soil_upgrade = on_alconna( @soil_upgrade.handle() async def _(session: Uninfo, index: Query[int] = AlconnaQuery("index", 1)): uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return condition = await g_pFarmManager.soilUpgradeCondition(uid, index.result) @@ -646,8 +676,10 @@ diuse_farm.shortcut( @diuse_farm.assign("admin-up") async def _(session: Uninfo, num: Query[int] = AlconnaQuery("num", 0)): uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return await g_pDBService.userSoil.nextPhase(uid, num.result) @@ -669,8 +701,10 @@ async def _(session: Uninfo, num: Query[int] = AlconnaQuery("num", 0)): ) uid = str(session.user.id) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return result = await g_pFarmManager.pointToVipPointByUid(uid, num.result) @@ -688,13 +722,14 @@ diuse_farm.shortcut( @diuse_farm.assign("my-vipPoint") async def _(session: Uninfo): uid = str(session.user.id) - vipPoint = await g_pDBService.user.getUserVipPointByUid(uid) + player = await g_pToolManager.getPlayerByUid(uid) - if not await g_pToolManager.isRegisteredByUid(uid): + if player is None or await player.isRegistered(): + await g_pToolManager.repeat() return await MessageUtils.build_message( - g_sTranslation["basic"]["vipPoint"].format(vipPoint=vipPoint) + g_sTranslation["basic"]["vipPoint"].format(vipPoint=player.user["vipPoint"]) ).send(reply_to=True) diff --git a/core/activity/activity.py b/core/activity/activity.py new file mode 100644 index 0000000..266745a --- /dev/null +++ b/core/activity/activity.py @@ -0,0 +1,108 @@ +class ActivityManager: + """活动管理器""" + + def __init__(self): + self.activities = {} # 活动ID -> 活动配置 + self.active_activities = set() # 当前活跃的活动ID + self.effect_handlers = {} # 效果类型 -> 处理器 + + # 注册效果处理器 + self._register_handlers() + + def _register_handlers(self): + """注册所有效果处理器""" + self.effect_handlers[EffectType.MULTIPLIER] = MultiplierHandler() + self.effect_handlers[EffectType.FIXED_BONUS] = FixedBonusHandler() + self.effect_handlers[EffectType.BUFF_APPLICATION] = BuffApplicationHandler() + self.effect_handlers[EffectType.QUEST_TRIGGER] = QuestTriggerHandler() + + def load_activities_from_config(self, config_path: str): + """从配置文件加载活动""" + with open(config_path, encoding="utf-8") as f: + config = json.load(f) + + for activity_data in config["activities"]: + activity = Activity(activity_data) + self.activities[activity.id] = activity + print(f"加载活动: {activity.name}") + + def update_activity_status(self): + """更新活动状态(定时调用)""" + now = datetime.now() + + for activity in self.activities.values(): + is_active = activity.start_time <= now <= activity.end_time + + if is_active and activity.id not in self.active_activities: + # 活动开始 + self.active_activities.add(activity.id) + print(f"活动开始: {activity.name}") + + elif not is_active and activity.id in self.active_activities: + # 活动结束 + self.active_activities.remove(activity.id) + print(f"活动结束: {activity.name}") + + def get_active_activities(self, activity_type: ActivityType = None) -> List: + """获取当前活跃的活动""" + active_list = [] + + for activity_id in self.active_activities: + activity = self.activities[activity_id] + if activity_type is None or activity.activity_type == activity_type: + active_list.append(activity) + + return active_list + + def apply_activity_effects( + self, + activity_type: ActivityType, + player: Player, + base_value: int, + context: Dict = None, + ) -> int: + """应用活动效果""" + if context is None: + context = {} + + context["base_value"] = base_value + result = base_value + + # 获取同类型的所有活跃活动 + active_activities = self.get_active_activities(activity_type) + + for activity in active_activities: + print(f"为玩家 {player.player_id} 应用活动: {activity.name}") + + for effect in activity.effects: + handler = self.effect_handlers.get(effect.type) + if handler: + try: + effect_result = handler.execute(effect.params, player, context) + if effect_result is not None: + result = effect_result + context["base_value"] = result # 更新基础值供后续效果使用 + except Exception as e: + print(f"效果执行失败: {effect.type}, 错误: {e}") + + return result + + +class Activity: + """活动类""" + + def __init__(self, data: Dict): + self.id = data["id"] + self.name = data["name"] + self.activity_type = ActivityType(data["activity_type"]) + self.start_time = datetime.strptime(data["start_time"], "%Y-%m-%d %H:%M:%S") + self.end_time = datetime.strptime(data["end_time"], "%Y-%m-%d %H:%M:%S") + self.effects = [Effect(effect_data) for effect_data in data["effects"]] + + +class Effect: + """效果类""" + + def __init__(self, data: Dict): + self.type = EffectType(data["type"]) + self.params = data.get("params", {}) diff --git a/core/activity/effect.py b/core/activity/effect.py new file mode 100644 index 0000000..a4b2a30 --- /dev/null +++ b/core/activity/effect.py @@ -0,0 +1,95 @@ +from enum import Enum +from typing import Any + + +# 活动类型枚举 +class ActivityType(Enum): + PLANTING = "planting" # 种植活动 + HARVESTING = "harvesting" # 收获活动 + FISHING = "fishing" # 钓鱼活动 + COMBAT = "combat" # 战斗活动 + + +# 效果类型枚举 +class EffectType(Enum): + MULTIPLIER = "multiplier" # 倍数加成 + FIXED_BONUS = "fixed_bonus" # 固定加成 + BUFF_APPLICATION = "buff_application" # 施加BUFF + QUEST_TRIGGER = "quest_trigger" # 任务触发 + + +class EffectHandler: + """ + 效果处理器基类 + """ + + def execute(self, params: dict, uid: str, context: dict) -> Any: + raise NotImplementedError + + +class MultiplierHandler(EffectHandler): + """ + 倍数效果处理器 + """ + + def execute(self, params: dict, uid: str, context: dict) -> float: + base_value = context.get("base_value", 0) + multiplier = params.get("value", 1.0) + + # 检查条件 + if self._check_conditions(params.get("conditions", {}), player): + result = base_value * multiplier + print(f"倍数效果: {base_value} × {multiplier} = {result}") + return result + return base_value + + def _check_conditions(self, conditions: dict, player: Player) -> bool: + """检查生效条件""" + # 等级要求 + min_level = conditions.get("min_level", 0) + if player.level < min_level: + return False + + # 需要特定物品 + required_items = conditions.get("required_items", []) + for item in required_items: + if player.inventory.get(item, 0) <= 0: + return False + + return True + + +class FixedBonusHandler(EffectHandler): + """固定加成处理器""" + + def execute(self, params: dict, player: Player, context: dict) -> int: + base_value = context.get("base_value", 0) + bonus = params.get("value", 0) + + result = base_value + bonus + print(f"固定加成: {base_value} + {bonus} = {result}") + return result + + +class BuffApplicationHandler(EffectHandler): + """BUFF应用处理器""" + + def execute(self, params: dict, player: Player, context: dict) -> None: + buff_id = params["buff_id"] + duration = params.get("duration", 3600) # 默认1小时 + properties = params.get("properties", {}) + + player.add_buff(buff_id, duration, properties) + + # 立即应用BUFF效果(如果有) + if "immediate_effect" in params: + self._apply_immediate_effect(params["immediate_effect"], player) + + +class QuestTriggerHandler(EffectHandler): + """任务触发处理器""" + + def execute(self, params: dict, player: Player, context: dict) -> None: + quest_id = params["quest_id"] + print(f"为玩家 {player.player_id} 触发任务: {quest_id}") + # 这里会调用任务系统来分配任务 diff --git a/core/database/database.py b/core/database/database.py new file mode 100644 index 0000000..d0b9934 --- /dev/null +++ b/core/database/database.py @@ -0,0 +1,399 @@ +from contextlib import asynccontextmanager +import os +from pathlib import Path +import re +from typing import Any + +import aiosqlite + +from zhenxun.services.log import logger + +from ...utils.config import g_sDBFilePath, g_sDBPath + + +class CSqlManager: + def __init__(self): + dbPath = Path(g_sDBPath) + if dbPath and not dbPath.exists(): + os.makedirs(dbPath, exist_ok=True) + + @classmethod + async def cleanup(cls): + if hasattr(cls, "m_pDB") and cls.m_pDB: + await cls.m_pDB.close() + + @classmethod + async def init(cls) -> bool: + try: + cls.m_pDB = await aiosqlite.connect(g_sDBFilePath) + cls.m_pDB.row_factory = aiosqlite.Row + return True + except Exception as e: + logger.debug("真寻农场初始化总数据库失败", e=e) + return False + + @classmethod + @asynccontextmanager + async def _transaction(cls): + await cls.m_pDB.execute("BEGIN;") + try: + yield + except: + await cls.m_pDB.execute("ROLLBACK;") + raise + else: + await cls.m_pDB.execute("COMMIT;") + + @classmethod + async def getTableInfo(cls, tableName: str) -> list: + if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", tableName): + raise ValueError(f"Illegal table name: {tableName}") + try: + cursor = await cls.m_pDB.execute(f'PRAGMA table_info("{tableName}")') + rows = await cursor.fetchall() + return [{"name": row[1], "type": row[2]} for row in rows] + except aiosqlite.Error: + return [] + + @classmethod + async def ensureTableSchema(cls, tableName: str, columns: dict) -> bool: + """由AI生成 + 创建表或为已存在表添加缺失字段。 + 返回 True 表示有变更(创建或新增列),False 则无操作 + + Args: + tableName (_type_): 表名 + columns (_type_): 字典 + + Returns: + _type_: _description_ + """ + + info = await cls.getTableInfo(tableName) + existing = {col["name"]: col["type"].upper() for col in info} + desired = {k: v.upper() for k, v in columns.items() if k != "PRIMARY KEY"} + primaryKey = columns.get("PRIMARY KEY", "") + + if not existing: + colsDef = ", ".join(f'"{k}" {v}' for k, v in desired.items()) + if primaryKey: + colsDef += f", PRIMARY KEY {primaryKey}" + await cls.m_pDB.execute(f'CREATE TABLE "{tableName}" ({colsDef});') + return True + + toAdd = [k for k in desired if k not in existing] + toRemove = [k for k in existing if k not in desired] + typeMismatch = [ + k for k in desired if k in existing and existing[k] != desired[k] + ] + + if toAdd and not toRemove and not typeMismatch: + for col in toAdd: + await cls.m_pDB.execute( + f'ALTER TABLE "{tableName}" ADD COLUMN "{col}" {columns[col]}' + ) + return True + + async with cls._transaction(): + tmpTable = f"{tableName}_new" + colsDef = ", ".join(f'"{k}" {v}' for k, v in desired.items()) + if primaryKey: + colsDef += f", PRIMARY KEY {primaryKey}" + await cls.m_pDB.execute(f'CREATE TABLE "{tmpTable}" ({colsDef});') + + commonCols = [k for k in desired if k in existing] + if commonCols: + colsStr = ", ".join(f'"{c}"' for c in commonCols) + + sql = ( + f'INSERT INTO "{tmpTable}" ({colsStr}) ' + f"SELECT {colsStr} " + f'FROM "{tableName}";' + ) + + await cls.m_pDB.execute(sql) + await cls.m_pDB.execute(f'DROP TABLE "{tableName}";') + await cls.m_pDB.execute( + f'ALTER TABLE "{tmpTable}" RENAME TO "{tableName}";' + ) + return True + + @classmethod + async def executeDB(cls, command: str) -> bool: + """执行自定义SQL + + Args: + command (str): SQL语句 + + Returns: + bool: 是否执行成功 + """ + if not command: + return False + + try: + async with cls._transaction(): + await cls.m_pDB.execute(command) + return True + except Exception: + return False + + @classmethod + async def insert(cls, tableName: str, data: dict) -> bool: + """ + 插入数据 + + Args: + tableName: 表名 + data: 要插入的数据字典,键为字段名,值为字段值 + + Returns: + bool: 是否执行成功 + """ + if not data: + return False + + try: + # 构建参数化查询 + columns = ", ".join(f'"{k}"' for k in data.keys()) + placeholders = ", ".join("?" for _ in data.keys()) + values = list(data.values()) + + sql = f'INSERT INTO "{tableName}" ({columns}) VALUES ({placeholders})' + + async with cls._transaction(): + await cls.m_pDB.execute(sql, values) + return True + except Exception as e: + logger.debug("真寻农场插入数据失败!", e=e) + return False + + @classmethod + async def batch_insert(cls, tableName: str, data_list: list) -> bool: + """ + 批量插入数据 + + Args: + tableName: 表名 + data_list: 要插入的数据字典列表 + + Returns: + bool: 是否执行成功 + """ + if not data_list: + return False + + try: + # 使用第一个字典的键作为所有记录的字段 + columns = ", ".join(f'"{k}"' for k in data_list[0].keys()) + placeholders = ", ".join("?" for _ in data_list[0].keys()) + + sql = f'INSERT INTO "{tableName}" ({columns}) VALUES ({placeholders})' + + async with cls._transaction(): + await cls.m_pDB.executemany( + sql, [list(data.values()) for data in data_list] + ) + return True + except Exception as e: + logger.debug("真寻农场批量插入数据失败!", e=e) + return False + + @classmethod + async def select( + cls, + tableName: str, + columns: list[Any] | None = None, + where: dict[str, Any] | None = None, + order_by: str | None = None, + limit: int | None = None, + ) -> list[dict]: + """ + 查询数据 + + Args: + tableName: 表名 + columns: 要查询的字段列表,None表示所有字段 + where: 查询条件字典 + order_by: 排序字段 + limit: 限制返回记录数 + + Returns: + list: 查询结果列表,每个元素是一个字典 + """ + try: + # 构建SELECT部分 + if columns: + select_clause = ", ".join(f'"{col}"' for col in columns) + else: + select_clause = "*" + + sql = f'SELECT {select_clause} FROM "{tableName}"' + + # 构建WHERE部分 + params = [] + if where: + where_conditions = [] + for key, value in where.items(): + if isinstance(value, (list, tuple)): + # 处理IN查询 + placeholders = ", ".join("?" for _ in value) + where_conditions.append(f'"{key}" IN ({placeholders})') + params.extend(value) + else: + where_conditions.append(f'"{key}" = ?') + params.append(value) + + if where_conditions: + sql += " WHERE " + " AND ".join(where_conditions) + + # 构建ORDER BY部分 + if order_by: + sql += f" ORDER BY {order_by}" + + # 构建LIMIT部分 + if limit: + sql += f" LIMIT {limit}" + + cursor = await cls.m_pDB.execute(sql, params) + rows = await cursor.fetchall() + + # 转换为字典列表 + result = [] + for row in rows: + result.append(dict(row)) + + return result + except Exception as e: + logger.debug("真寻农场查询数据失败!", e=e) + return [] + + @classmethod + async def update(cls, tableName: str, data: dict, where: dict) -> bool: + """ + 更新数据 + + Args: + tableName: 表名 + data: 要更新的数据字典 + where: 更新条件字典 + + Returns: + bool: 是否执行成功 + """ + if not data: + return False + + if not where: + return False + + try: + # 构建SET部分 + set_conditions = [] + params = [] + for key, value in data.items(): + set_conditions.append(f'"{key}" = ?') + params.append(value) + + # 构建WHERE部分 + where_conditions = [] + for key, value in where.items(): + where_conditions.append(f'"{key}" = ?') + params.append(value) + + sql = f'UPDATE "{tableName}" SET {", ".join(set_conditions)} WHERE {" AND ".join(where_conditions)}' + + async with cls._transaction(): + cursor = await cls.m_pDB.execute(sql, params) + # 检查是否影响了行 + return cursor.rowcount > 0 + except Exception as e: + logger.debug("真寻农场更新数据失败!", e=e) + return False + + @classmethod + async def delete(cls, tableName: str, where: dict) -> bool: + """ + 删除数据 + + Args: + tableName: 表名 + where: 删除条件字典 + + Returns: + bool: 是否执行成功 + """ + if not where: + return False + + try: + # 构建WHERE部分 + where_conditions = [] + params = [] + for key, value in where.items(): + where_conditions.append(f'"{key}" = ?') + params.append(value) + + sql = f'DELETE FROM "{tableName}" WHERE {" AND ".join(where_conditions)}' + + async with cls._transaction(): + cursor = await cls.m_pDB.execute(sql, params) + # 检查是否影响了行 + return cursor.rowcount > 0 + except Exception as e: + logger.debug("真寻农场删除数据失败!", e=e) + return False + + @classmethod + async def exists(cls, tableName: str, where: dict) -> bool: + """ + 检查记录是否存在 + + Args: + tableName: 表名 + where: 查询条件字典 + + Returns: + bool: 是否存在符合条件的记录 + """ + try: + result = await cls.select(tableName, columns=["1"], where=where, limit=1) + return len(result) > 0 + except Exception as e: + logger.debug("真寻农场检查数据失败!", e=e) + return False + + @classmethod + async def count(cls, tableName: str, where: dict = {}) -> int: + """ + 统计记录数量 + + Args: + tableName: 表名 + where: 查询条件字典 + + Returns: + int: 记录数量 + """ + try: + # 构建WHERE部分 + sql = f'SELECT COUNT(*) as count FROM "{tableName}"' + params = [] + + if where: + where_conditions = [] + for key, value in where.items(): + where_conditions.append(f'"{key}" = ?') + params.append(value) + + sql += " WHERE " + " AND ".join(where_conditions) + + cursor = await cls.m_pDB.execute(sql, params) + row = await cursor.fetchone() + return row["count"] if row else 0 + except Exception as e: + logger.debug("真寻农场统计数据失败!", e=e) + return 0 + + +g_pSqlManager = CSqlManager() diff --git a/database/plant.py b/core/database/plant.py similarity index 79% rename from database/plant.py rename to core/database/plant.py index a9ebc6f..a2ef3f4 100644 --- a/database/plant.py +++ b/core/database/plant.py @@ -6,8 +6,8 @@ import aiosqlite from zhenxun.configs.config import Config from zhenxun.services.log import logger -from ..config import g_bIsDebug, g_sPlantPath, g_sResourcePath -from ..request import g_pRequestManager +from ...utils.config import g_bIsDebug, g_sPlantPath, g_sResourcePath +from ...utils.request import g_pRequestManager class CPlantManager: @@ -17,43 +17,39 @@ class CPlantManager: except FileExistsError: pass - @classmethod - async def cleanup(cls): - if hasattr(cls, "m_pDB") and cls.m_pDB: - await cls.m_pDB.close() + async def cleanup(self): + if hasattr(self, "m_pDB") and self.m_pDB: + await self.m_pDB.close() - @classmethod - async def init(cls) -> bool: + async def init(self) -> bool: try: _ = os.path.exists(g_sPlantPath) if g_bIsDebug: - cls.m_pDB = await aiosqlite.connect( + self.m_pDB = await aiosqlite.connect( str(g_sPlantPath.parent / "plant-test.db") ) else: - cls.m_pDB = await aiosqlite.connect(str(g_sPlantPath)) + self.m_pDB = await aiosqlite.connect(str(g_sPlantPath)) - cls.m_pDB.row_factory = aiosqlite.Row + self.m_pDB.row_factory = aiosqlite.Row return True except Exception as e: logger.warning("初始化植物数据库失败", e=e) return False - @classmethod @asynccontextmanager - async def _transaction(cls): - await cls.m_pDB.execute("BEGIN;") + async def _transaction(self): + await self.m_pDB.execute("BEGIN;") try: yield except: - await cls.m_pDB.execute("ROLLBACK;") + await self.m_pDB.execute("ROLLBACK;") raise else: - await cls.m_pDB.execute("COMMIT;") + await self.m_pDB.execute("COMMIT;") - @classmethod - async def executeDB(cls, command: str) -> bool: + async def executeDB(self, command: str) -> bool: """执行自定义SQL Args: @@ -67,15 +63,14 @@ class CPlantManager: return False try: - async with cls._transaction(): - await cls.m_pDB.execute(command) + async with self._transaction(): + await self.m_pDB.execute(command) return True except Exception as e: logger.warning(f"数据库语句执行出错: {command}", e=e) return False - @classmethod - async def getPlantByName(cls, name: str) -> dict | None: + async def getPlantByName(self, name: str) -> dict | None: """根据作物名称查询记录 Args: @@ -85,7 +80,7 @@ class CPlantManager: dict | None: 返回记录字典,未找到返回None """ try: - async with cls.m_pDB.execute( + async with self.m_pDB.execute( "SELECT * FROM plant WHERE name = ?", (name,) ) as cursor: row = await cursor.fetchone() @@ -94,8 +89,7 @@ class CPlantManager: logger.warning(f"查询作物失败: {name}", e=e) return None - @classmethod - async def getPlantPhaseByName(cls, name: str) -> list[int]: + async def getPlantPhaseByName(self, name: str) -> list[int]: """根据作物名称获取作物各个阶段 Args: @@ -105,7 +99,7 @@ class CPlantManager: list: 阶段数组 """ try: - async with cls.m_pDB.execute( + async with self.m_pDB.execute( "SELECT phase FROM plant WHERE name = ?", (name,) ) as cursor: row = await cursor.fetchone() @@ -130,8 +124,7 @@ class CPlantManager: logger.warning(f"查询作物阶段失败: {name}", e=e) return [] - @classmethod - async def getPlantPhaseNumberByName(cls, name: str) -> int: + async def getPlantPhaseNumberByName(self, name: str) -> int: """根据作物名称获取作物总阶段数 Args: @@ -141,7 +134,7 @@ class CPlantManager: int: 总阶段数 """ try: - async with cls.m_pDB.execute( + async with self.m_pDB.execute( "SELECT phase FROM plant WHERE name = ?", (name,) ) as cursor: row = await cursor.fetchone() @@ -164,8 +157,7 @@ class CPlantManager: logger.warning(f"查询作物阶段失败: {name}", e=e) return -1 - @classmethod - async def getPlantAgainByName(cls, name: str) -> int: + async def getPlantAgainByName(self, name: str) -> int: """根据作物名称获取作物再次成熟时间 Args: @@ -176,7 +168,7 @@ class CPlantManager: """ try: - async with cls.m_pDB.execute( + async with self.m_pDB.execute( "SELECT phase FROM plant WHERE name = ?", (name,) ) as cursor: row = await cursor.fetchone() @@ -193,8 +185,7 @@ class CPlantManager: logger.warning(f"查询作物阶段失败: {name}", e=e) return -1 - @classmethod - async def existsPlant(cls, name: str) -> bool: + async def existsPlant(self, name: str) -> bool: """判断作物是否存在 Args: @@ -204,7 +195,7 @@ class CPlantManager: bool: 存在返回True,否则False """ try: - async with cls.m_pDB.execute( + async with self.m_pDB.execute( "SELECT 1 FROM plant WHERE name = ? LIMIT 1", (name,) ) as cursor: row = await cursor.fetchone() @@ -213,8 +204,7 @@ class CPlantManager: logger.warning(f"检查作物存在性失败: {name}", e=e) return False - @classmethod - async def countPlants(cls, onlyBuy: bool = False) -> int: + async def countPlants(self, onlyBuy: bool = False) -> int: """获取作物总数 Args: @@ -230,18 +220,17 @@ class CPlantManager: else: sql = "SELECT COUNT(*) FROM plant" params: tuple = () - async with cls.m_pDB.execute(sql, params) as cursor: + async with self.m_pDB.execute(sql, params) as cursor: row = await cursor.fetchone() return row[0] if row else 0 except Exception as e: logger.warning(f"统计作物数量失败, onlyBuy={onlyBuy}", e=e) return 0 - @classmethod - async def listPlants(cls) -> list[dict]: + async def listPlants(self) -> list[dict]: """查询所有作物记录""" try: - async with cls.m_pDB.execute( + async with self.m_pDB.execute( "SELECT * FROM plant ORDER BY level" ) as cursor: rows = await cursor.fetchall() @@ -250,8 +239,7 @@ class CPlantManager: logger.warning("查询所有作物失败", e=e) return [] - @classmethod - async def downloadPlant(cls) -> bool: + async def downloadPlant(self) -> bool: """遍历所有作物,下载各阶段图片及icon文件到指定文件夹 Returns: @@ -262,10 +250,10 @@ class CPlantManager: baseUrl = baseUrl.rstrip("/") + ":8998/file" try: - plants = await cls.listPlants() + plants = await self.listPlants() for plant in plants: name = plant["name"] - phaseCount = await cls.getPlantPhaseNumberByName(name) + phaseCount = await self.getPlantPhaseNumberByName(name) saveDir = os.path.join(g_sResourcePath, "plant", name) begin = 0 if plant["general"] == 0 else 1 diff --git a/core/database/user.py b/core/database/user.py new file mode 100644 index 0000000..c07f07a --- /dev/null +++ b/core/database/user.py @@ -0,0 +1,215 @@ +import math + +from ...utils.tool import g_pToolManager +from .database import CSqlManager + + +class CUserDB(CSqlManager): + def __init__(self): + self.currencies: list[str] = ["point", "vipPoint"] + + async def initDB(self): + userInfo = { + "uid": "TEXT PRIMARY KEY", # 用户Uid + "name": "TEXT NOT NULL", # 农场名称 + "exp": "INTEGER DEFAULT 0", # 经验值 + "point": "INTEGER DEFAULT 0", # 金币 + "vipPoint": "INTEGER DEFAULT 0", # 点券 + "soil": "INTEGER DEFAULT 3", # 解锁土地数量 + "stealTime": "TEXT DEFAULT ''", # 偷菜时间字符串 + "stealCount": "INTEGER DEFAULT 0", # 剩余偷菜次数 + } + + await self.ensureTableSchema("user", userInfo) + + async def initUserInfo(self, uid: str, name: str) -> bool: + """初始化用户信息 + + Args: + uid (str): 用户ID + name (str): 农场名称 + + Returns: + bool: 是否成功初始化用户信息 + """ + nowStr = g_pToolManager.dateTime().date().today().strftime("%Y-%m-%d") + + result = await self.insert( + "user", + data={ + "uid": uid, + "name": name, + "exp": 0, + "point": 500, + "soil": 3, + "stealTime": nowStr, + "stealCount": 5, + }, + ) + + return result + + async def getUserInfoByUid(self, uid: str) -> dict: + """根据用户ID获取用户信息 + + Args: + uid (str): 用户ID + + Returns: + dict: 用户信息字典,未找到返回空字典 + """ + if uid == "": + return {} + + records = await self.select("user", where={"uid": uid}) + + return records[0] if records else {} + + async def isRegistered(self, uid: str) -> bool: + """检查用户是否注册农场 + + Args: + uid (str): 用户ID + + Returns: + bool: 是否注册农场 + """ + if uid == "": + return False + + return await self.exists("user", where={"uid": uid}) + + async def updatePoint(self, uid: str, type: str, index: int) -> bool: + """更新货币 + + Args: + uid (str): 用户ID + type (str): 货币类型 point/vipPoint + index (int): 更新后的数量 + + Returns: + bool: 是否成功更新货币 + """ + if type not in self.currencies: + return False + + if index < 0: + index = 0 + + await self.update("user", {type: index}, {"uid": uid}) + + return True + + async def updateExp(self, uid: str, exp: int) -> bool: + """更新经验值 + + Args: + uid (str): 用户ID + exp (int): 更新后的经验值 + + Returns: + bool: 是否成功更新经验值 + """ + if exp < 0: + exp = 0 + + return await self.update("user", {"exp": exp}, {"uid": uid}) + + async def updateName(self, uid: str, name: str) -> str: + """更新农场名称 + + Args: + uid (str): 用户ID + name (str): 农场名称 + + Returns: + bool: 是否成功更新农场名称 + """ + safeName = g_pToolManager.sanitize_username(name) + + if safeName == "神秘农夫": + return "error" + + if await self.update("user", {"name": safeName}, {"uid": uid}): + return "success" + + return "error1" + + async def getUserLevelByUid(self, uid: str) -> tuple[int, int, int]: + """获取用户等级信息 + + Args: + uid (str): 用户Uid + + Returns: + tuple[int, int, int]: 成功返回(当前等级, 升至下级还需经验, 当前等级已获经验) + 失败返回(-1, -1, -1) + """ + if not uid: + return -1, -1, -1 + + records = await self.select("user", where={"uid": uid}, columns=["exp"]) + + if not records: + return -1, -1, -1 + + try: + exp = int(records[0].get("exp", 0)) + except Exception: + exp = 0 + + levelStep = 200 # 每级经验增量 + + discriminant = 1 + 8 * exp / levelStep + level = int((-1 + math.sqrt(discriminant)) // 2) + if level < 0: + level = 0 + + def cumExp(k: int) -> int: + return levelStep * k * (k + 1) // 2 + + totalExpCurrentLevel = cumExp(level) + totalExpNextLevel = cumExp(level + 1) + + currentExp = exp - totalExpCurrentLevel + + return level, totalExpNextLevel, currentExp + + async def getUserSoilByUid(self, uid: str) -> int: + """获取用户解锁土地数量 + + Args: + uid (str): 用户Uid + + Returns: + int: 解锁土地数量,失败返回-1 + """ + if not uid: + return -1 + + records = await self.select("user", where={"uid": uid}, columns=["soil"]) + + if not records: + return -1 + + try: + soil = int(records[0].get("soil", 3)) + except Exception: + soil = 3 + + return soil + + async def updateFieldByUid(self, uid: str, field: str, value) -> bool: + """更新单字段信息 + + Args: + uid (str): 用户Uid + field (str): 字段名称 + + Returns: + bool: 是否成功更新字段 + """ + if not uid or not field: + return False + + return await self.update("user", {field: value}, {"uid": uid}) diff --git a/database/userItem.py b/core/database/userItem.py similarity index 100% rename from database/userItem.py rename to core/database/userItem.py diff --git a/database/userPlant.py b/core/database/userPlant.py similarity index 100% rename from database/userPlant.py rename to core/database/userPlant.py diff --git a/database/userSeed.py b/core/database/userSeed.py similarity index 100% rename from database/userSeed.py rename to core/database/userSeed.py diff --git a/database/userSign.py b/core/database/userSign.py similarity index 98% rename from database/userSign.py rename to core/database/userSign.py index 4046572..f0a25f3 100644 --- a/database/userSign.py +++ b/core/database/userSign.py @@ -5,10 +5,10 @@ import random from zhenxun.services.log import logger from zhenxun.utils._build_image import BuildImage -from ..config import g_bIsDebug +from ...utils.config import g_bIsDebug +from ...utils.json import g_pJsonManager +from ...utils.tool import g_pToolManager from ..dbService import g_pDBService -from ..json import g_pJsonManager -from ..tool import g_pToolManager from .database import CSqlManager diff --git a/database/userSoil.py b/core/database/userSoil.py similarity index 99% rename from database/userSoil.py rename to core/database/userSoil.py index ad16ee6..cf6befb 100644 --- a/database/userSoil.py +++ b/core/database/userSoil.py @@ -2,9 +2,9 @@ import math from zhenxun.services.log import logger -from ..config import g_bIsDebug from ..dbService import g_pDBService -from ..tool import g_pToolManager +from ..utils.config import g_bIsDebug +from ..utils.tool import g_pToolManager from .database import CSqlManager diff --git a/database/userSteal.py b/core/database/userSteal.py similarity index 94% rename from database/userSteal.py rename to core/database/userSteal.py index 57d21e3..474054d 100644 --- a/database/userSteal.py +++ b/core/database/userSteal.py @@ -44,7 +44,7 @@ class CUserStealDB(CSqlManager): return False @classmethod - async def getStealRecordsByUid(cls, uid: str) -> list: + async def getStealRecordsByUid(cls, uid: str) -> dict: """根据用户Uid获取所有偷菜记录 Args: @@ -59,20 +59,16 @@ class CUserStealDB(CSqlManager): 'SELECT soilIndex, stealerUid, stealCount, stealTime FROM "userSteal" WHERE uid=?;', (uid,), ) - rows = await cursor.fetchall() - return [ - { - "uid": uid, - "soilIndex": row[0], - "stealerUid": row[1], - "stealCount": row[2], - "stealTime": row[3], - } - for row in rows - ] + row = await cursor.fetchone() + + if not row: + return {} + + result = dict(row) + return result except Exception as e: logger.warning("获取偷菜记录失败", e=e) - return [] + return {} @classmethod async def getStealRecord(cls, uid: str, soilIndex: int) -> list: diff --git a/core/dbService.py b/core/dbService.py new file mode 100644 index 0000000..0fc6aae --- /dev/null +++ b/core/dbService.py @@ -0,0 +1,43 @@ +class CDBService: + async def init(self): + from .database.plant import CPlantManager + from .database.user import CUserDB + from .database.userItem import CUserItemDB + from .database.userPlant import CUserPlantDB + from .database.userSeed import CUserSeedDB + from .database.userSign import CUserSignDB + from .database.userSoil import CUserSoilDB + from .database.userSteal import CUserStealDB + + self.plant = CPlantManager() + await self.plant.init() + + self.user = CUserDB() + await self.user.initDB() + + self.userSoil = CUserSoilDB() + await self.userSoil.initDB() + + self.userPlant = CUserPlantDB() + await self.userPlant.initDB() + + self.userSeed = CUserSeedDB() + await self.userSeed.initDB() + + self.userItem = CUserItemDB() + await self.userItem.initDB() + + self.userSteal = CUserStealDB() + await self.userSteal.initDB() + + self.userSign = CUserSignDB() + await self.userSign.initDB() + + # 迁移旧数据库 + await self.userSoil.migrateOldFarmData() + + async def cleanup(self): + await self.plant.cleanup() + + +g_pDBService = CDBService() diff --git a/farm/farm.py b/core/farm.py similarity index 94% rename from farm/farm.py rename to core/farm.py index b8adf95..e71eb74 100644 --- a/farm/farm.py +++ b/core/farm.py @@ -9,11 +9,11 @@ from zhenxun.utils.enum import GoldHandle from zhenxun.utils.image_utils import ImageTemplate from zhenxun.utils.platform import PlatformUtils -from ..config import g_bIsDebug, g_iSoilLevelMax, g_sResourcePath, g_sTranslation -from ..dbService import g_pDBService +from ..core.dbService import g_pDBService from ..event.event import g_pEventManager -from ..json import g_pJsonManager -from ..tool import g_pToolManager +from ..utils.config import g_bIsDebug, g_iSoilLevelMax, g_sResourcePath, g_sTranslation +from ..utils.json import g_pJsonManager +from ..utils.tool import g_pToolManager class CFarmManager: @@ -36,20 +36,27 @@ class CFarmManager: return f"你的金币不足或不足承担手续费。当前手续费为{fee}" await UserConsole.reduce_gold( - uid, num, GoldHandle.PLUGIN, "zhenxun_plugin_farm" + uid, + num, + GoldHandle.PLUGIN, # type: ignore + "zhenxun_plugin_farm", ) await UserConsole.reduce_gold( - uid, fee, GoldHandle.PLUGIN, "zhenxun_plugin_farm" - ) # type: ignore + uid, + fee, + GoldHandle.PLUGIN, # type: ignore + "zhenxun_plugin_farm", + ) point = num * pro + player = await g_pToolManager.getPlayerByUid(uid) + if not player: + return g_sTranslation["basic"]["error"] - p = await g_pDBService.user.getUserPointByUid(uid) - number = point + p + p = player.user.get("point", 0) + await player.addPoint("point", point + p) - await g_pDBService.user.updateUserPointByUid(uid, int(number)) - - return f"充值{point}农场币成功,手续费{tax}金币,当前农场币:{number}" + return f"充值{point}农场币成功,手续费{tax}金币,当前农场币:{point + p}" @classmethod async def drawFarmByUid(cls, uid: str) -> bytes: @@ -69,9 +76,11 @@ class CFarmManager: await grass.resize(0, soilSize[0], soilSize[1]) soilPos = g_pJsonManager.m_pSoil["soil"] + player = await g_pToolManager.getPlayerByUid(uid) + if not player: + return img.pic2bytes() - userInfo = await g_pDBService.user.getUserInfoByUid(uid) - soilUnlock = int(userInfo["soil"]) + soilUnlock = int(player.user.get("soil", 3)) x = 0 y = 0 @@ -168,12 +177,12 @@ class CFarmManager: # 用户名 nameImg = await BuildImage.build_text_image( - userInfo["name"], size=24, font_color=(77, 35, 4) + player.user["name"], size=24, font_color=(77, 35, 4) ) await img.paste(nameImg, (300, 92)) # 经验值 - level = await g_pDBService.user.getUserLevelByUid(uid) + level = await player.getUserLevel() beginX = 309 endX = 627 @@ -194,7 +203,7 @@ class CFarmManager: # 金币 pointImg = await BuildImage.build_text_image( - str(userInfo["point"]), size=24, font_color=(253, 253, 253) + str(player.user["point"]), size=24, font_color=(253, 253, 253) ) await img.paste(pointImg, (330, 255)) @@ -220,8 +229,10 @@ class CFarmManager: @classmethod async def drawDetailFarmByUid(cls, uid: str) -> list: info = [] - farm = await cls.drawFarmByUid(uid) + player = await g_pToolManager.getPlayerByUid(uid) + if not player: + return info info.append(BuildImage.open(farm)) @@ -238,7 +249,7 @@ class CFarmManager: ] icon = "" - soilNumber = await g_pDBService.user.getUserSoilByUid(uid) + soilNumber = player.user.get("soil", 3) for i in range(1, soilNumber + 1): soilInfo = await g_pDBService.userSoil.getUserSoil(uid, i) @@ -490,7 +501,10 @@ class CFarmManager: return g_sTranslation["sowing"]["noNum"].format(name=name, num=count) # 获取用户土地数量 - soilNumber = await g_pDBService.user.getUserSoilByUid(uid) + player = await g_pToolManager.getPlayerByUid(uid) + if not player: + return g_sTranslation["basic"]["error"] + soilNumber = player.user.get("soil", 3) # 如果播种数量为 -1,表示播种所有可播种的土地 if num == -1: @@ -546,7 +560,10 @@ class CFarmManager: try: await g_pEventManager.m_beforeHarvest.emit(uid=uid) # type: ignore - soilNumber = await g_pDBService.user.getUserSoilByUid(uid) + player = await g_pToolManager.getPlayerByUid(uid) + if not player: + return g_sTranslation["basic"]["error"] + soilNumber = player.user.get("soil", 3) harvestRecords = [] # 收获日志记录 experience = 0 # 总经验值 @@ -644,8 +661,8 @@ class CFarmManager: ) if experience > 0: - exp = await g_pDBService.user.getUserExpByUid(uid) - await g_pDBService.user.updateUserExpByUid(uid, exp + experience) + exp = player.user.get("exp", 0) + await player.addExp(exp + experience) harvestRecords.append( g_sTranslation["harvest"]["exp"].format( exp=experience, @@ -671,7 +688,10 @@ class CFarmManager: Returns: str: 返回 """ - soilNumber = await g_pDBService.user.getUserSoilByUid(uid) + player = await g_pToolManager.getPlayerByUid(uid) + if not player: + return g_sTranslation["basic"]["error"] + soilNumber = player.user.get("soil", 3) await g_pEventManager.m_beforeEradicate.emit(uid=uid) # type: ignore @@ -715,8 +735,8 @@ class CFarmManager: await g_pEventManager.m_afterEradicate.emit(uid=uid, soilIndex=i) # type: ignore if experience > 0: - exp = await g_pDBService.user.getUserExpByUid(uid) - await g_pDBService.user.updateUserExpByUid(uid, exp + experience) + exp = player.user.get("exp", 0) + await player.addExp(exp + experience) return g_sTranslation["eradicate"]["success"].format(exp=experience) else: @@ -828,10 +848,12 @@ class CFarmManager: str: 返回 """ # 用户信息 - userInfo = await g_pDBService.user.getUserInfoByUid(uid) + player = await g_pToolManager.getPlayerByUid(uid) + if not player: + return g_sTranslation["basic"]["error"] - stealTime = userInfo.get("stealTime", "") - stealCount = int(userInfo["stealCount"]) + stealTime = player.user.get("stealTime", "") + stealCount = int(player.user["stealCount"]) if stealTime == "" or not stealTime: stealTime = g_pToolManager.dateTime().date().today().strftime("%Y-%m-%d") diff --git a/farm/help.py b/core/help.py similarity index 96% rename from farm/help.py rename to core/help.py index 9c0823a..1bf4999 100644 --- a/farm/help.py +++ b/core/help.py @@ -6,12 +6,12 @@ from playwright.async_api import async_playwright from zhenxun.configs.path_config import DATA_PATH from zhenxun.services.log import logger -from ..config import g_sResourcePath +from ..utils.config import g_sResourcePath class CHelpManager: @classmethod - def rendeerHtmlToFile( + def renderHtmlToFile( cls, path: Path | str, context: dict, output: Path | str ) -> None: """ @@ -110,7 +110,7 @@ class CHelpManager: } try: - cls.rendeerHtmlToFile(templatePath, context, outputPath) + cls.renderHtmlToFile(templatePath, context, outputPath) bytes = await cls.screenshotSave(str(outputPath), str(savePath), 1500, 2300) except Exception as e: diff --git a/core/player/player.py b/core/player/player.py new file mode 100644 index 0000000..feb06ca --- /dev/null +++ b/core/player/player.py @@ -0,0 +1,199 @@ +from ..dbService import g_pDBService + + +class CPlayer: + def __init__(self): + self.user = { + "uid": "", # 用户Uid + "name": "", # 农场名称 + "exp": 0, # 经验值 + "point": 0, # 金币 + "vipPoint": 0, # 点券 + "soil": 3, # 解锁土地数量 + "stealTime": "", # 偷菜时间字符串 + "stealCount": 0, # 剩余偷菜次数 + } + + async def init(self, uid: str) -> bool: + self.user["uid"] = uid + return await self.loadFormDB() + + async def loadFormDB(self) -> bool: + uid = self.user.get("uid", "") + + if uid == "": + return False + + self.user = await g_pDBService.user.getUserInfoByUid(uid) + + return True + + async def isRegistered(self) -> bool: + """检查用户是否注册农场 + + Returns: + bool: 是否注册农场 + """ + uid = self.user.get("uid", "") + + if uid == "": + return False + + return await g_pDBService.user.isRegistered(uid) + + async def addPoint(self, type: str, index: int) -> bool: + """增加货币 + + Args: + type (str): 货币类型 point/vipPoint + index (int): 增加的数量 + + Returns: + bool: 是否成功增加货币 + """ + uid = self.user.get("uid", "") + + if uid == "" or type not in g_pDBService.user.currencies: + return False + + if index == 0: + return True + + nowIndex = self.user.get(type, 0) + index + + if nowIndex < 0: + nowIndex = 0 + + if await g_pDBService.user.updatePoint(uid, type, nowIndex): + self.user[type] = nowIndex + return True + + return False + + async def subPoint(self, type: str, index: int) -> bool: + """减少货币 + + Args: + type (str): 货币类型 point/vipPoint + index (int): 减少的数量 + + Returns: + bool: 是否成功减少货币 + """ + uid = self.user.get("uid", "") + + if uid == "" or type not in g_pDBService.user.currencies: + return False + + if index == 0: + return True + + nowIndex = self.user.get(type, 0) - index + + if nowIndex < 0: + nowIndex = 0 + + if await g_pDBService.user.updatePoint(uid, type, nowIndex): + self.user[type] = nowIndex + return True + + return False + + async def addExp(self, exp: int) -> bool: + """增加经验值 + + Args: + exp (int): 增加的经验值 + + Returns: + bool: 是否成功增加经验值 + """ + uid = self.user.get("uid", "") + + if uid == "": + return False + + if exp == 0: + return True + + nowExp = self.user.get("exp", 0) + exp + + if nowExp < 0: + nowExp = 0 + + if await g_pDBService.user.updateExp(uid, nowExp): + self.user["exp"] = nowExp + return True + + return False + + async def subExp(self, exp: int) -> bool: + """减少经验值 + + Args: + exp (int): 减少的经验值 + + Returns: + bool: 是否成功减少经验值 + """ + uid = self.user.get("uid", "") + + if uid == "": + return False + + if exp == 0: + return True + + nowExp = self.user.get("exp", 0) - exp + + if nowExp < 0: + nowExp = 0 + + if await g_pDBService.user.updateExp(uid, nowExp): + self.user["exp"] = nowExp + return True + + return False + + async def updateName(self, name: str) -> str: + """更新农场名称 + + Args: + name (str): 农场名称 + + Returns: + str: success/error/error1 + """ + uid = self.user.get("uid", "") + + if uid == "": + return "error" + + return await g_pDBService.user.updateName(uid, name) + + async def getUserLevel(self) -> tuple[int, int, int]: + """获取用户等级信息 + + Returns: + tuple[int, int, int]: 成功返回(当前等级, 升至下级还需经验, 当前等级已获经验) + 失败返回(-1, -1, -1) + """ + uid = self.user.get("uid", "") + + if uid == "": + return -1, -1, -1 + + return await g_pDBService.user.getUserLevelByUid(uid) + + async def updateField(self, field: str) -> bool: + """更新单字段信息 + + Returns: + bool: 是否成功更新单字段信息 + """ + uid = self.user.get("uid", "") + + if uid == "": + return False + + return await g_pDBService.farm.updateFarmFieldByUid(uid) diff --git a/core/player/playerPool.py b/core/player/playerPool.py new file mode 100644 index 0000000..6dba0c7 --- /dev/null +++ b/core/player/playerPool.py @@ -0,0 +1,216 @@ +import threading +import time +from typing import Any + +from zhenxun.services.log import logger + + +class CPlayerPool: + """ + 用户池管理类 + 管理用户对象的生命周期,支持自动清理超时用户 + """ + + def __init__(self, timeoutSeconds: int = 300, cleanupInterval: int = 3600): + """ + 初始化用户池 + + Args: + timeoutSeconds: 用户超时时间(秒),默认5分钟 + cleanupInterval: 清理间隔(秒),默认1小时 + """ + self._players: dict[str, dict[str, Any]] = {} + self._lock = threading.RLock() + self.timeoutSeconds = timeoutSeconds + self.cleanupInterval = cleanupInterval + + # 启动后台清理线程 + self._cleanupThread = threading.Thread(target=self._cleanupWorker, daemon=True) + self._running = True + self._cleanupThread.start() + + logger.debug( + f"用户池初始化完成,超时时间: {timeoutSeconds}秒, 清理间隔: {cleanupInterval}秒" + ) + + def createUser(self, uid: str, userObj: Any) -> bool: + """ + 创建并管理用户对象 + + Args: + uid: 用户ID + userObj: 用户对象 + + Returns: + bool: 是否创建成功 + """ + with self._lock: + if uid in self._players: + logger.debug(f"用户 {uid} 已存在,正在覆盖") + # 可以选择返回False或者覆盖,这里选择覆盖 + # return False + + self._players[uid] = { + "object": userObj, + "lastActive": time.time(), + "activeCount": 0, + } + logger.debug(f"用户 {uid} 创建并开始管理") + return True + + def getUser(self, uid: str) -> Any | None: + """ + 获取用户对象并刷新活跃时间 + + Args: + uid: 用户ID + + Returns: + Optional[Any]: 用户对象,如果不存在或已超时则返回None + """ + with self._lock: + userData = self._players.get(uid) + + if not userData: + logger.debug(f"用户 {uid} 不存在") + return None + + # 检查是否已超时(防御性检查) + currentTime = time.time() + if currentTime - userData["lastActive"] > self.timeoutSeconds: + logger.debug(f"用户 {uid} 在获取操作期间已超时") + self._removeUser(uid) + return None + + # 刷新活跃时间 + userData["lastActive"] = currentTime + userData["activeCount"] += 1 + + logger.debug(f"用户 {uid} 获取成功,活跃次数: {userData['activeCount']}") + return userData["object"] + + def updateUser(self, uid: str, userObj: Any) -> bool: + """ + 更新用户对象 + + Args: + uid: 用户ID + userObj: 新的用户对象 + + Returns: + bool: 是否更新成功 + """ + with self._lock: + if uid not in self._players: + logger.debug(f"用户 {uid} 不存在,无法更新") + return False + + self._players[uid]["object"] = userObj + self._players[uid]["lastActive"] = time.time() + logger.debug(f"用户 {uid} 更新成功") + return True + + def removeUser(self, uid: str) -> bool: + """ + 主动移除用户 + + Args: + uid: 用户ID + + Returns: + bool: 是否移除成功 + """ + with self._lock: + return self._removeUser(uid) + + def _removeUser(self, uid: str) -> bool: + """内部移除用户方法""" + if uid in self._players: + userData = self._players.pop(uid) + # 如果需要清理资源,可以在这里处理 + if hasattr(userData["object"], "close"): + try: + userData["object"].close() + except Exception as e: + logger.debug(f"关闭用户 {uid} 时出错: {e}") + + logger.debug(f"用户 {uid} 已移除,总活跃次数: {userData['activeCount']}") + return True + return False + + def _cleanupWorker(self): + """后台清理线程的工作函数""" + while self._running: + try: + self._cleanupExpiredUsers() + except Exception as e: + logger.debug(f"清理工作线程出错: {e}") + + # 休眠指定间隔 + time.sleep(self.cleanupInterval) + + def _cleanupExpiredUsers(self): + """清理超时用户""" + currentTime = time.time() + expiredUsers = [] + + # 首先收集过期的用户ID,避免在迭代中修改字典 + with self._lock: + for uid, userData in self._players.items(): + if currentTime - userData["lastActive"] > self.timeoutSeconds: + expiredUsers.append(uid) + + # 移除过期用户 + for uid in expiredUsers: + with self._lock: + # 再次检查,防止在收集和移除之间用户被更新 + if ( + uid in self._players + and currentTime - self._players[uid]["lastActive"] + > self.timeoutSeconds + ): + self._removeUser(uid) + + if expiredUsers: + logger.debug(f"已清理 {len(expiredUsers)} 个过期用户: {expiredUsers}") + + def getActiveUsers(self) -> dict[str, dict[str, Any]]: + """ + 获取当前活跃用户信息 + + Returns: + Dict: 用户信息字典 + """ + with self._lock: + # 返回副本避免外部修改 + return { + uid: { + "lastActive": data["lastActive"], + "activeCount": data["activeCount"], + "timeRemaining": self.timeoutSeconds + - (time.time() - data["lastActive"]), + } + for uid, data in self._players.items() + } + + def userCount(self) -> int: + """获取当前用户数量""" + with self._lock: + return len(self._players) + + def shutdown(self): + """关闭用户池,清理资源""" + self._running = False + if self._cleanupThread.is_alive(): + self._cleanupThread.join(timeout=5) + + # 清理所有用户 + with self._lock: + uids = list(self._players.keys()) + for uid in uids: + self._removeUser(uid) + + logger.debug("用户池关闭完成") + + +g_pUserPool = CPlayerPool() diff --git a/farm/shop.py b/core/shop.py similarity index 75% rename from farm/shop.py rename to core/shop.py index e1a8b00..1f1a318 100644 --- a/farm/shop.py +++ b/core/shop.py @@ -2,8 +2,9 @@ import math from zhenxun.utils.image_utils import ImageTemplate -from ..config import g_sResourcePath, g_sTranslation -from ..dbService import g_pDBService +from ..core.dbService import g_pDBService +from ..utils.config import g_sResourcePath, g_sTranslation +from ..utils.tool import g_pToolManager class CShopManager: @@ -113,48 +114,39 @@ class CShopManager: Returns: str: """ - if num <= 0: return g_sTranslation["buySeed"]["notNum"] + player = await g_pToolManager.getPlayerByUid(uid) plantInfo = await g_pDBService.plant.getPlantByName(name) - if not plantInfo: + if not plantInfo or not player: return g_sTranslation["buySeed"]["error"] - level = await g_pDBService.user.getUserLevelByUid(uid) + level = player.user.get("level", 0) - if level[0] < int(plantInfo["level"]): + if level < int(plantInfo["level"]): return g_sTranslation["buySeed"]["noLevel"] - """ - logger.debug( - f"用户:{uid}购买{name},数量为{num}。用户农场币为{point},购买需要{total}" - ) - """ - if plantInfo["isVip"] == 1: - vipPoint = await g_pDBService.user.getUserVipPointByUid(uid) - total = int(plantInfo["vipBuy"]) * num - if vipPoint < total: - return g_sTranslation["buySeed"]["noVipPoint"] - await g_pDBService.user.updateUserVipPointByUid(uid, vipPoint - total) - else: - point = await g_pDBService.user.getUserPointByUid(uid) - total = int(plantInfo["buy"]) * num - if point < total: - return g_sTranslation["buySeed"]["noPoint"] - await g_pDBService.user.updateUserPointByUid(uid, point - total) + vipSeed = plantInfo.get("isVip", 0) == 1 + currencyType = "vipPoint" if vipSeed else "point" + price = int(plantInfo["vipBuy" if vipSeed else "buy"]) + totalCost = price * num + + currentCurrency = player.user.get(currencyType, 0) + if currentCurrency < totalCost: + return g_sTranslation["buySeed"][f"no{'Vip' if vipSeed else ''}Point"] + + await player.addPoint(currencyType, currentCurrency - totalCost) if not await g_pDBService.userSeed.addUserSeedByUid(uid, name, num): return g_sTranslation["buySeed"]["errorSql"] - if plantInfo["isVip"] == 1: - return g_sTranslation["buySeed"]["vipSuccess"].format( - name=name, total=total, point=vipPoint - total - ) - else: - return g_sTranslation["buySeed"]["success"].format( - name=name, total=total, point=point - total - ) + success_key = "vipSuccess" if vipSeed else "success" + remaining_currency = currentCurrency - totalCost + + return g_sTranslation["buySeed"][success_key].format( + name=name, total=totalCost, point=remaining_currency + ) @classmethod async def sellPlantByUid(cls, uid: str, name: str = "", num: int = 1) -> str: @@ -215,17 +207,18 @@ class CShopManager: totalPoint = totalSold * price - currentPoint = await g_pDBService.user.getUserPointByUid(uid) - await g_pDBService.user.updateUserPointByUid(uid, currentPoint + totalPoint) + player = await g_pToolManager.getPlayerByUid(uid) + if not player: + return g_sTranslation["basic"]["error"] - if name == "": - return g_sTranslation["sellPlant"]["success"].format( - point=totalPoint, num=currentPoint + totalPoint - ) - else: - return g_sTranslation["sellPlant"]["success1"].format( - name=name, point=totalPoint, num=currentPoint + totalPoint - ) + currentPoint = player.user.get("point", 0) + await player.addPoint("point", currentPoint + totalPoint) + + result = "success1" if name == "" else "success" + + return g_sTranslation["sellPlant"][result].format( + point=totalPoint, num=currentPoint + totalPoint + ) g_pShopManager = CShopManager() diff --git a/database/database.py b/database/database.py deleted file mode 100644 index 6b1127f..0000000 --- a/database/database.py +++ /dev/null @@ -1,143 +0,0 @@ -from contextlib import asynccontextmanager -import os -from pathlib import Path -import re - -import aiosqlite - -from zhenxun.services.log import logger - -from ..config import g_sDBFilePath, g_sDBPath - - -class CSqlManager: - def __init__(self): - dbPath = Path(g_sDBPath) - if dbPath and not dbPath.exists(): - os.makedirs(dbPath, exist_ok=True) - - @classmethod - async def cleanup(cls): - if hasattr(cls, "m_pDB") and cls.m_pDB: - await cls.m_pDB.close() - - @classmethod - async def init(cls) -> bool: - try: - cls.m_pDB = await aiosqlite.connect(g_sDBFilePath) - cls.m_pDB.row_factory = aiosqlite.Row - return True - except Exception as e: - logger.warning("初始化总数据库失败", e=e) - return False - - @classmethod - @asynccontextmanager - async def _transaction(cls): - await cls.m_pDB.execute("BEGIN;") - try: - yield - except: - await cls.m_pDB.execute("ROLLBACK;") - raise - else: - await cls.m_pDB.execute("COMMIT;") - - @classmethod - async def getTableInfo(cls, tableName: str) -> list: - if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", tableName): - raise ValueError(f"Illegal table name: {tableName}") - try: - cursor = await cls.m_pDB.execute(f'PRAGMA table_info("{tableName}")') - rows = await cursor.fetchall() - return [{"name": row[1], "type": row[2]} for row in rows] - except aiosqlite.Error: - return [] - - @classmethod - async def ensureTableSchema(cls, tableName: str, columns: dict) -> bool: - """由AI生成 - 创建表或为已存在表添加缺失字段。 - 返回 True 表示有变更(创建或新增列),False 则无操作 - - Args: - tableName (_type_): 表名 - columns (_type_): 字典 - - Returns: - _type_: _description_ - """ - - info = await cls.getTableInfo(tableName) - existing = {col["name"]: col["type"].upper() for col in info} - desired = {k: v.upper() for k, v in columns.items() if k != "PRIMARY KEY"} - primaryKey = columns.get("PRIMARY KEY", "") - - if not existing: - colsDef = ", ".join(f'"{k}" {v}' for k, v in desired.items()) - if primaryKey: - colsDef += f", PRIMARY KEY {primaryKey}" - await cls.m_pDB.execute(f'CREATE TABLE "{tableName}" ({colsDef});') - return True - - toAdd = [k for k in desired if k not in existing] - toRemove = [k for k in existing if k not in desired] - typeMismatch = [ - k for k in desired if k in existing and existing[k] != desired[k] - ] - - if toAdd and not toRemove and not typeMismatch: - for col in toAdd: - await cls.m_pDB.execute( - f'ALTER TABLE "{tableName}" ADD COLUMN "{col}" {columns[col]}' - ) - return True - - async with cls._transaction(): - tmpTable = f"{tableName}_new" - colsDef = ", ".join(f'"{k}" {v}' for k, v in desired.items()) - if primaryKey: - colsDef += f", PRIMARY KEY {primaryKey}" - await cls.m_pDB.execute(f'CREATE TABLE "{tmpTable}" ({colsDef});') - - commonCols = [k for k in desired if k in existing] - if commonCols: - colsStr = ", ".join(f'"{c}"' for c in commonCols) - - sql = ( - f'INSERT INTO "{tmpTable}" ({colsStr}) ' - f"SELECT {colsStr} " - f'FROM "{tableName}";' - ) - - await cls.m_pDB.execute(sql) - await cls.m_pDB.execute(f'DROP TABLE "{tableName}";') - await cls.m_pDB.execute( - f'ALTER TABLE "{tmpTable}" RENAME TO "{tableName}";' - ) - return True - - @classmethod - async def executeDB(cls, command: str) -> bool: - """执行自定义SQL - - Args: - command (str): SQL语句 - - Returns: - bool: 是否执行成功 - """ - if not command: - logger.warning("数据库语句长度为空!") - return False - - try: - async with cls._transaction(): - await cls.m_pDB.execute(command) - return True - except Exception as e: - logger.warning(f"数据库语句执行出错: {command}", e=e) - return False - - -g_pSqlManager = CSqlManager() diff --git a/database/user.py b/database/user.py deleted file mode 100644 index 4fe29da..0000000 --- a/database/user.py +++ /dev/null @@ -1,479 +0,0 @@ -import math - -from zhenxun.services.log import logger - -from ..tool import g_pToolManager -from .database import CSqlManager - - -class CUserDB(CSqlManager): - @classmethod - async def initDB(cls): - """初始化用户表结构,确保user表存在且字段完整""" - userInfo = { - "uid": "TEXT PRIMARY KEY", # 用户Uid - "name": "TEXT NOT NULL", # 农场名称 - "exp": "INTEGER DEFAULT 0", # 经验值 - "point": "INTEGER DEFAULT 0", # 金币 - "vipPoint": "INTEGER DEFAULT 0", # 点券 - "soil": "INTEGER DEFAULT 3", # 解锁土地数量 - "stealTime": "TEXT DEFAULT ''", # 偷菜时间字符串 - "stealCount": "INTEGER DEFAULT 0", # 剩余偷菜次数 - } - await cls.ensureTableSchema("user", userInfo) - - @classmethod - async def initUserInfoByUid( - cls, uid: str, name: str = "", exp: int = 0, point: int = 500 - ) -> bool | str: - """初始化用户信息,包含初始偷菜时间字符串与次数 - - Args: - uid (str): 用户Uid - name (str): 农场名称 - exp (int): 农场经验 - point (int): 农场币 - - Returns: - bool | str: False 表示失败,字符串表示成功信息 - """ - nowStr = g_pToolManager.dateTime().date().today().strftime("%Y-%m-%d") - sql = ( - f"INSERT INTO user (uid, name, exp, point, soil, stealTime, stealCount) " - f"VALUES ({uid}, '{name}', {exp}, {point}, 3, '{nowStr}', 5)" - ) - try: - async with cls._transaction(): - await cls.m_pDB.execute(sql) - return "开通农场成功" - except Exception as e: - logger.warning("initUserInfoByUid 事务执行失败!", e=e) - return False - - @classmethod - async def getAllUsers(cls) -> list[str]: - """获取所有用户UID列表 - - Returns: - list[str]: 用户UID列表 - """ - cursor = await cls.m_pDB.execute("SELECT uid FROM user") - rows = await cursor.fetchall() - return [row[0] for row in rows] - - @classmethod - async def isUserExist(cls, uid: str) -> bool: - """判断用户是否存在 - - Args: - uid (str): 用户Uid - - Returns: - bool: 如果用户存在返回True,否则返回False - """ - if not uid: - return False - try: - async with cls.m_pDB.execute( - "SELECT 1 FROM user WHERE uid = ?", (uid,) - ) as cursor: - row = await cursor.fetchone() - return row is not None - except Exception as e: - logger.warning("isUserExist 查询失败!", e=e) - return False - - @classmethod - async def getUserInfoByUid(cls, uid: str) -> dict: - """获取指定用户完整信息 - - Args: - uid (str): 用户Uid - - Returns: - dict: 包含所有用户字段的字典 - """ - if not uid: - return {} - try: - async with cls.m_pDB.execute( - "SELECT * FROM user WHERE uid = ?", (uid,) - ) as cursor: - row = await cursor.fetchone() - if not row: - return {} - - result = dict(row) - - return result - except Exception as e: - logger.warning("getUserInfoByUid 查询失败!", e=e) - return {} - - @classmethod - async def getUserNameByUid(cls, uid: str) -> str: - """根据用户Uid获取用户名 - - Args: - uid (str): 用户Uid - - Returns: - str: 用户名,失败返回空字符串 - """ - if not uid: - return "" - try: - async with cls.m_pDB.execute( - "SELECT name FROM user WHERE uid = ?", (uid,) - ) as cursor: - row = await cursor.fetchone() - return row["name"] if row else "" - except Exception as e: - logger.warning("getUserNameByUid 查询失败!", e=e) - return "" - - @classmethod - async def updateUserNameByUid(cls, uid: str, name: str) -> bool: - """根据用户Uid更新用户名 - - Args: - uid (str): 用户Uid - name (str): 新用户名 - - Returns: - bool: 是否更新成功 - """ - if not uid or not name: - return False - try: - async with cls._transaction(): - await cls.m_pDB.execute( - "UPDATE user SET name = ? WHERE uid = ?", (name, uid) - ) - return True - except Exception as e: - logger.warning("updateUserNameByUid 事务执行失败!", e=e) - return False - - @classmethod - async def getUserPointByUid(cls, uid: str) -> int: - """获取指定用户农场币 - - Args: - uid (str): 用户Uid - - Returns: - int: 农场币数量,失败返回 -1 - """ - if not uid: - return -1 - try: - async with cls.m_pDB.execute( - "SELECT point FROM user WHERE uid = ?", (uid,) - ) as cursor: - row = await cursor.fetchone() - return int(row[0]) if row and row[0] is not None else -1 - except Exception as e: - logger.warning("getUserPointByUid 查询失败!", e=e) - return -1 - - @classmethod - async def updateUserPointByUid(cls, uid: str, point: int) -> bool: - """根据用户Uid更新农场币数量 - - Args: - uid (str): 用户Uid - point (int): 新农场币数量 - - Returns: - bool: 是否更新成功 - """ - if not uid or point < 0: - logger.warning("updateUserPointByUid 参数校验失败!") - return False - try: - async with cls._transaction(): - await cls.m_pDB.execute( - "UPDATE user SET point = ? WHERE uid = ?", (point, uid) - ) - return True - except Exception as e: - logger.error("updateUserPointByUid 事务执行失败!", e=e) - return False - - @classmethod - async def getUserVipPointByUid(cls, uid: str) -> int: - """获取指定用户点券 - - Args: - uid (str): 用户Uid - - Returns: - int: 点券数量,失败返回 -1 - """ - if not uid: - return -1 - try: - async with cls.m_pDB.execute( - "SELECT vipPoint FROM user WHERE uid = ?", (uid,) - ) as cursor: - row = await cursor.fetchone() - return int(row[0]) if row and row[0] is not None else -1 - except Exception as e: - logger.warning("getUservipPointByUid 查询失败!", e=e) - return -1 - - @classmethod - async def updateUserVipPointByUid(cls, uid: str, vipPoint: int) -> bool: - """根据用户Uid更新点券数量 - - Args: - uid (str): 用户Uid - vipPoint (int): 新点券数量 - - Returns: - bool: 是否更新成功 - """ - if not uid or vipPoint < 0: - logger.warning("updateUservipPointByUid 参数校验失败!") - return False - try: - async with cls._transaction(): - await cls.m_pDB.execute( - "UPDATE user SET vipPoint = ? WHERE uid = ?", (vipPoint, uid) - ) - return True - except Exception as e: - logger.error("updateUservipPointByUid 事务执行失败!", e=e) - return False - - @classmethod - async def getUserExpByUid(cls, uid: str) -> int: - """获取指定用户经验值 - - Args: - uid (str): 用户Uid - - Returns: - int: 经验值,失败返回 -1 - """ - if not uid: - return -1 - try: - async with cls.m_pDB.execute( - "SELECT exp FROM user WHERE uid = ?", (uid,) - ) as cursor: - row = await cursor.fetchone() - return int(row[0]) if row and row[0] is not None else -1 - except Exception as e: - logger.warning("getUserExpByUid 查询失败!", e=e) - return -1 - - @classmethod - async def updateUserExpByUid(cls, uid: str, exp: int) -> bool: - """根据用户Uid更新经验值 - - Args: - uid (str): 用户Uid - exp (int): 新经验值 - - Returns: - bool: 是否更新成功 - """ - if not uid: - return False - try: - async with cls._transaction(): - await cls.m_pDB.execute( - "UPDATE user SET exp = ? WHERE uid = ?", (exp, uid) - ) - return True - except Exception as e: - logger.warning("updateUserExpByUid 事务执行失败!", e=e) - return False - - @classmethod - async def getUserLevelByUid(cls, uid: str) -> tuple[int, int, int]: - """获取用户等级信息 - - Args: - uid (str): 用户Uid - - Returns: - tuple[int, int, int]: 成功返回(当前等级, 升至下级还需经验, 当前等级已获经验) - 失败返回(-1, -1, -1) - """ - if not uid: - return -1, -1, -1 - - try: - async with cls.m_pDB.execute( - "SELECT exp FROM user WHERE uid = ?", (uid,) - ) as cursor: - row = await cursor.fetchone() - if not row or row[0] is None: - return -1, -1, -1 - - expVal = int(row[0]) - levelStep = 200 # 每级经验增量 - - discriminant = 1 + 8 * expVal / levelStep - level = int((-1 + math.sqrt(discriminant)) // 2) - if level < 0: - level = 0 - - def cumExp(k: int) -> int: - return levelStep * k * (k + 1) // 2 - - totalExpCurrentLevel = cumExp(level) - totalExpNextLevel = cumExp(level + 1) - - currentExp = expVal - totalExpCurrentLevel - - return level, totalExpNextLevel, currentExp - - return -1, -1, -1 - except Exception as e: - logger.warning("getUserLevelByUid 查询失败!", e=e) - return -1, -1, -1 - - @classmethod - async def getUserSoilByUid(cls, uid: str) -> int: - """获取解锁土地数量 - - Args: - uid (str): 用户Uid - - Returns: - int: 解锁土地块数,失败返回0 - """ - if not uid: - return 0 - try: - async with cls.m_pDB.execute( - "SELECT soil FROM user WHERE uid = ?", (uid,) - ) as cursor: - row = await cursor.fetchone() - return int(row[0]) if row and row[0] is not None else 0 - except Exception as e: - logger.warning("getUserSoilByUid 查询失败!", e=e) - return 0 - - @classmethod - async def updateUserSoilByUid(cls, uid: str, soil: int) -> bool: - """更新指定用户解锁土地数量 - - Args: - uid (str): 用户Uid - soil (int): 新土地数量 - - Returns: - bool: 更新成功返回True,否则False - """ - if not uid or soil < 0: - return False - try: - async with cls._transaction(): - await cls.m_pDB.execute( - "UPDATE user SET soil = ? WHERE uid = ?", (soil, uid) - ) - return True - except Exception as e: - logger.warning("updateUserSoilByUid 事务执行失败!", e=e) - return False - - @classmethod - async def getStealTimeByUid(cls, uid: str) -> str: - """根据用户Uid获取偷菜时间字符串 - - Args: - uid (str): 用户Uid - - Returns: - str: 偷菜时间字符串,失败返回空字符串 - """ - if not uid: - return "" - try: - async with cls.m_pDB.execute( - "SELECT stealTime FROM user WHERE uid = ?", (uid,) - ) as cursor: - row = await cursor.fetchone() - return row[0] if row and row[0] else "" - except Exception as e: - logger.warning("getStealTimeByUid 查询失败!", e=e) - return "" - - @classmethod - async def updateStealTimeByUid(cls, uid: str, stealTime: str) -> bool: - """根据用户Uid更新偷菜时间字符串 - - Args: - uid (str): 用户Uid - stealTime (str): 新偷菜时间字符串 - - Returns: - bool: 是否更新成功 - """ - if not uid or not stealTime: - logger.warning("updateStealTimeByUid 参数校验失败!") - return False - try: - async with cls._transaction(): - await cls.m_pDB.execute( - "UPDATE user SET stealTime = ? WHERE uid = ?", (stealTime, uid) - ) - return True - except Exception as e: - logger.warning("updateStealTimeByUid 事务执行失败!", e=e) - return False - - @classmethod - async def getStealCountByUid(cls, uid: str) -> int: - """根据用户Uid获取剩余偷菜次数 - - Args: - uid (str): 用户Uid - - Returns: - int: 剩余偷菜次数,失败返回 -1 - """ - if not uid: - return -1 - try: - async with cls.m_pDB.execute( - "SELECT stealCount FROM user WHERE uid = ?", (uid,) - ) as cursor: - row = await cursor.fetchone() - return int(row[0]) if row and row[0] is not None else 0 - except Exception as e: - logger.warning("getStealCountByUid 查询失败!", e=e) - return -1 - - @classmethod - async def updateStealCountByUid( - cls, uid: str, stealTime: str, stealCount: int - ) -> bool: - """根据用户Uid更新剩余偷菜次数 - - Args: - uid (str): 用户Uid - stealTime (str): 偷菜日期 - stealCount (int): 新剩余偷菜次数 - - Returns: - bool: 是否更新成功 - """ - if not uid or stealCount < 0: - logger.warning("updateStealCountByUid 参数校验失败!") - return False - try: - async with cls._transaction(): - await cls.m_pDB.execute( - "UPDATE user SET stealTime = ?, stealCount = ? WHERE uid = ?", - (stealTime, stealCount, uid), - ) - return True - except Exception as e: - logger.warning("updateStealCountByUid 事务执行失败!", e=e) - return False diff --git a/dbService.py b/dbService.py deleted file mode 100644 index d748d96..0000000 --- a/dbService.py +++ /dev/null @@ -1,45 +0,0 @@ -class CDBService: - @classmethod - async def init(cls): - from .database.plant import CPlantManager - from .database.user import CUserDB - from .database.userItem import CUserItemDB - from .database.userPlant import CUserPlantDB - from .database.userSeed import CUserSeedDB - from .database.userSign import CUserSignDB - from .database.userSoil import CUserSoilDB - from .database.userSteal import CUserStealDB - - cls.plant = CPlantManager() - await cls.plant.init() - - cls.user = CUserDB() - await cls.user.initDB() - - cls.userSoil = CUserSoilDB() - await cls.userSoil.initDB() - - cls.userPlant = CUserPlantDB() - await cls.userPlant.initDB() - - cls.userSeed = CUserSeedDB() - await cls.userSeed.initDB() - - cls.userItem = CUserItemDB() - await cls.userItem.initDB() - - cls.userSteal = CUserStealDB() - await cls.userSteal.initDB() - - cls.userSign = CUserSignDB() - await cls.userSign.initDB() - - # 迁移旧数据库 - await cls.userSoil.migrateOldFarmData() - - @classmethod - async def cleanup(cls): - await cls.plant.cleanup() - - -g_pDBService = CDBService() diff --git a/config.py b/utils/config.py similarity index 96% rename from config.py rename to utils/config.py index 091c386..8afdca7 100644 --- a/config.py +++ b/utils/config.py @@ -15,13 +15,13 @@ g_sDBPath = DATA_PATH / "farm_db" g_sDBFilePath = DATA_PATH / "farm_db/farm.db" # 农场资源文件目录 -g_sResourcePath = Path(__file__).resolve().parent / "resource" +g_sResourcePath = Path(__file__).resolve().parent / "../resource" # 农场作物数据库 g_sPlantPath = g_sResourcePath / "db/plant.db" # 农场配置文件目录 -g_sConfigPath = Path(__file__).resolve().parent / "config" +g_sConfigPath = Path(__file__).resolve().parent / "../config" # 农场签到文件路径 g_sSignInPath = g_sConfigPath / "sign_in.json" @@ -35,6 +35,7 @@ g_sTranslation = { "notFarm": "尚未开通农场,快at我发送 开通农场 开通吧 🌱🚜", "point": "你的当前农场币为: {point} 🌾💰", "vipPoint": "你的当前点券为: {vipPoint} 🌾💰", + "error": "❌ 农场功能异常,请稍后再试 💔", }, "register": { "success": "✅ 农场开通成功!\n💼 初始资金:{point}农场币 🥳🎉", diff --git a/json.py b/utils/json.py similarity index 100% rename from json.py rename to utils/json.py diff --git a/request.py b/utils/request.py similarity index 99% rename from request.py rename to utils/request.py index 8773f13..3b468cf 100644 --- a/request.py +++ b/utils/request.py @@ -10,11 +10,12 @@ from rich.progress import ( 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 .dbService import g_pDBService from .tool import g_pToolManager diff --git a/tool.py b/utils/tool.py similarity index 82% rename from tool.py rename to utils/tool.py index 09c0e90..48a4cec 100644 --- a/tool.py +++ b/utils/tool.py @@ -1,25 +1,31 @@ -import os from datetime import datetime +import os from zoneinfo import ZoneInfo from zhenxun.services.log import logger from zhenxun.utils.message import MessageUtils -from .dbService import g_pDBService +from ..core.player.player import CPlayer +from ..core.player.playerPool import g_pUserPool class CToolManager: @classmethod - async def isRegisteredByUid(cls, uid: str) -> bool: - result = await g_pDBService.user.isUserExist(uid) + async def repeat(cls): + await MessageUtils.build_message( + "尚未开通农场,快at我发送 开通农场 开通吧" + ).send() - if not result: - await MessageUtils.build_message( - "尚未开通农场,快at我发送 开通农场 开通吧" - ).send() - return False + @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 True + return player @classmethod def sanitize_username(cls, username: str, max_length: int = 15) -> str: