15 Commits
Author SHA1 Message Date
Diuse fce307cc94 🚑 关闭了Debug模式 2025-05-30 11:23:20 +08:00
Diuse db294e6998 🐛 修正作物阶段绘制错误的BUG
🐛 修复偷菜会导致偷自己的BUG
2025-05-30 11:22:32 +08:00
Diuse 1ef506d2fd ✨ 新增每天00:30检查签到文件 2025-05-29 18:20:03 +08:00
Diuse a475d0188b 🐛 修复了农场详述无法正常输出的BUG 2025-05-29 01:27:46 +08:00
Diuse 7dc55bccf1 ✨ 种子商店新增筛选功能 2025-05-28 19:48:12 +08:00
Diuse afa2259ad8 ✨ 添加签到功能
🐛 修复默认使用格林威治时间而不是北京时间的BUG
2025-05-27 18:15:11 +08:00
Diuse c131692b19 🐛 修复迁移旧版数据库失败的BUG 2025-05-26 15:10:12 +08:00
Diuse cca3dc5d4d Merge branch 'Beta' of https://github.com/Shu-Ying/zhenxun_plugin_farm into Beta 2025-05-19 00:58:19 +08:00
Diuse 2fbaf5d54f 🐛 修复当数据库文件不存在时的异常报错 2025-05-19 00:56:17 +08:00
Diuse 6d3eb5d48f 📝 新增农场详述功能 2025-05-12 17:28:33 +08:00
Diuse ff5b446b52 ✨ 新增多阶段作物特性
🐛 修复偷菜BUG
📝 新增部分作物
2025-05-09 18:20:00 +08:00
Diuse 55394e3590 🎨 将plant.json迁移至sqlite本地数据库
📝 新增部分作物
2025-05-08 17:56:45 +08:00
Diuse 0cd9b9d8db 📝 迁移部分代码结构,使文件名和代码功能更加匹配 2025-05-07 17:04:08 +08:00
Diuse 8243a1e1c8 📝 继续完善数据库 2025-04-29 18:11:09 +08:00
Diuse 6b99ec427c 📝 继续对数据库进行优化操作 2025-04-28 19:27:16 +08:00
182 changed files with 3518 additions and 1639 deletions
+3 -1
View File
@@ -1 +1,3 @@
__pycache__
__pycache__
./config/sign_in.json
+63 -11
View File
@@ -1,7 +1,46 @@
<div align="center">
# 真寻农场(zhenxun_plugin_farm)
<p align="center">
<a href="./LICENSE">
<img src="https://img.shields.io/badge/license-GPL3.0-FE7D37" alt="license">
</a>
<a href="https://www.python.org">
<img src="https://img.shields.io/badge/Python-3.10%2B-blue" alt="Python">
</a>
<a href="https://nonebot.dev/">
<img src="https://img.shields.io/badge/Nonebot-2.0.0%2B-black" alt="Nonebot">
</a>
<a href="https://github.com/zhenxun-org/zhenxun_bot">
<img src="https://img.shields.io/badge/zhenxun-0.2.4%2B-%23ECD9D3" alt="Python">
</a>
</p>
<p align="center">
[![tencent-qq](https://img.shields.io/badge/%E7%BE%A4-%E7%9C%9F%E5%AF%BB%E5%86%9C%E5%9C%BA%E6%B5%8B%E8%AF%95-%23FF99CC
)](https://qm.qq.com/q/7hsOD4rOw2)
</p>
你是说可以种地对吧🤔?
</div>
---
## 目录
- [真寻农场(zhenxun\_plugin\_farm)](#真寻农场zhenxun_plugin_farm)
- [目录](#目录)
- [如何安装](#如何安装)
- [使用指令](#使用指令)
- [更新日志(详细):](#更新日志详细)
- [用户方面](#用户方面)
- [代码方面](#代码方面)
- [待办事宜 `Todo` 列表](#待办事宜-todo-列表)
- [关于](#关于)
- [致谢](#致谢)
- [许可证](#许可证)
---
## 如何安装
@@ -9,40 +48,53 @@
方法一(推荐):在小真寻后台的插件商店下载即可<br>
方法二:下载源码放在小真寻`plugin`目录下
---
## 使用指令
| 指令 | 描述 | Tip |
| --- | --- | --- |
| @小真寻 开通农场 | 首次开通农场 | |
| 我的农场 | 你的农场 | |
| 农场详述 | 农场详细信息 | |
| 我的农场币 | 查询农场币 | |
| 种子商店 [页数] | 查看种子商店 | 数量不填默认为1 |
| 种子商店 [筛选关键字] [页数] or [页数] | 查看种子商店 | 当第一个参数为非整数时,会默认进入筛选状态。页数不填默认为1 |
| 购买种子 [种子名称] [数量] | 购买种子 | 数量不填默认为1 |
| 我的种子 | 查询仓库种子 | |
| 播种 [种子名称] [数量] | 播种种子 | 数量不填默认将最大可能播种 |
| 收获 | 收获成熟作物 | |
| 铲除 | 铲除荒废作物 | |
| 我的作物 | | |
| 我的作物 | 你的作物 | |
| 出售作物 [作物名称] [数量] | 从仓库里向系统售卖作物 | 不填写作物名将售卖仓库种全部作物 填作物名不填数量将指定作物全部出售 |
| @美波理 偷菜 | 偷别人的菜 | 每人每天只能偷5次 |
| 购买农场币 | 将真寻金币兑换成农场币 | 兑换比例默认为1:2 手续费默认20% |
| 更改农场名 [新的农场名] | 改名 | 农场名称无法存储特殊字符 |
| 农场签到 | 签到 | 需要注意,该项会从服务器拉取签到数据 |
---
## 更新日志[详细](./log/log.md):
- 感谢[quanquan1014](https://github.com/quanquan1014)对农场名称中包含特殊字符的处理。
- 更正数据库写法,但是会导致V1.0用户的作物和种子丢失
- 完善我的农场图片资源,现在会在左上角显示经验、等级、金币等详细信息了
- 完善出售作物逻辑,现在可以空置作物名称来一键出售全部作物了,也可以选择空置数量来一键出售仓库种指定作物
- 完善播种逻辑,现在可以空置数量来一键播种指定作物了
- 新增更改农场名指令
- 改进对土地开垦条件判断
## 更新日志[(详细)](./log/log.md):
用户方面
---
- 新增种子商店筛选功能(如果没有BUG的话后续我的种子、我的作物等也会加入筛选功能
- 新增签到功能(测试
- 新增农场详述功能,能通过该功能更加详细的观看农场数据
- 修复了迁移旧数据库无法正常迁移的BUG
- 修复了偷菜会导致偷自己的BUG
- 修正了作物阶段绘制不正确的BUG
代码方面
---
- 修正了多阶段作物成长素材计算逻辑
- 修正了作物数据库字段错乱的问题
- 作物新增offset字段,用于以后偏移坐标和大小(尚未启用,该模式有商议
---
## 待办事宜 `Todo` 列表
- [x] 完善我的农场图片,例如左上角显示用户数据
- [ ] 完善升级数据、作物数据、作物图片
- [ ] 签到功能
- [x] 签到功能
- [ ] 添加渔场功能
- [ ] 增加活动、交易行功能
- [ ] 增加交易行总行功能
+24 -4
View File
@@ -1,15 +1,17 @@
from nonebot import get_driver
from nonebot.plugin import PluginMetadata
from nonebot_plugin_apscheduler import scheduler
from zhenxun.configs.utils import Command, PluginExtraData, RegisterConfig
from zhenxun.services.log import logger
from zhenxun.utils.message import MessageUtils
from .command import diuse_farm, diuse_register, reclamation
from .config import g_pJsonManager
from .database import g_pSqlManager
from .database.database import g_pSqlManager
from .dbService import g_pDBService
from .farm.farm import g_pFarmManager
from .farm.shop import g_pShopManager
from .json import g_pJsonManager
from .request import g_pRequestManager
__plugin_meta__ = PluginMetadata(
@@ -21,7 +23,7 @@ __plugin_meta__ = PluginMetadata(
at 开通农场
我的农场
我的农场币
种子商店 [页数]
种子商店 [筛选关键字] [页数] or [页数]
购买种子 [作物/种子名称] [数量]
我的种子
播种 [作物/种子名称] [数量] (数量不填默认将最大可能播种
@@ -33,10 +35,11 @@ __plugin_meta__ = PluginMetadata(
开垦
购买农场币 [数量] 数量为消耗金币的数量
更改农场名 [新农场名]
农场签到
""".strip(),
extra=PluginExtraData(
author="Art_Sakura",
version="1.1",
version="1.4",
commands=[Command(command="我的农场")],
menu_type="群内小游戏",
configs=[
@@ -79,7 +82,24 @@ async def start():
# 初始化读取Json
await g_pJsonManager.init()
await g_pDBService.init()
# 析构函数
@driver.on_shutdown
async def shutdown():
await g_pSqlManager.cleanup()
await g_pDBService.cleanup()
@scheduler.scheduled_job(
trigger="cron",
hour=0,
minute=30,
id="signInFile"
)
async def signInFile():
try:
await g_pJsonManager.initSignInFile()
except:
logger.info("农场签到文件下载失败!")
+158 -35
View File
@@ -1,25 +1,29 @@
import inspect
from nonebot.adapters import Event, MessageTemplate
from nonebot.rule import to_me
from nonebot.typing import T_State
from nonebot_plugin_alconna import (Alconna, AlconnaMatch, AlconnaQuery, Args,
Arparma, At, Match, MultiVar, Option,
Query, Subcommand, on_alconna, store_true)
from nonebot_plugin_uninfo import Uninfo
from nonebot_plugin_waiter import waiter
from zhenxun.configs.config import BotConfig
from zhenxun.services.log import logger
from zhenxun.utils.depends import UserName
from zhenxun.utils.message import MessageUtils
from .database import g_pSqlManager
from .config import g_bSignStatus
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
async def isRegisteredByUid(uid: str) -> bool:
point = await g_pSqlManager.getUserPointByUid(uid)
result = await g_pDBService.user.isUserExist(uid)
if point < 0:
if not result:
await MessageUtils.build_message("尚未开通农场,快at我发送 开通农场 开通吧").send()
return False
@@ -36,19 +40,18 @@ diuse_register = on_alconna(
@diuse_register.handle()
async def handle_register(session: Uninfo):
uid = str(session.user.id)
user = await g_pSqlManager.getUserInfoByUid(uid)
user = await g_pDBService.user.getUserInfoByUid(uid)
if user:
await MessageUtils.build_message("🎉 您已经开通农场啦~").send(reply_to=True)
return
try:
# 获取原始用户名并安全处理
raw_name = str(session.user.name)
safe_name = sanitize_username(raw_name)
# 初始化用户信息
success = await g_pSqlManager.initUserInfoByUid(
success = await g_pDBService.user.initUserInfoByUid(
uid=uid,
name=safe_name,
exp=0,
@@ -122,8 +125,9 @@ diuse_farm = on_alconna(
Alconna(
"我的农场",
Option("--all", action=store_true),
Subcommand("detail", help_text="农场详述"),
Subcommand("my-point", help_text="我的农场币"),
Subcommand("seed-shop", Args["num?", int], help_text="种子商店"),
Subcommand("seed-shop", Args["res?", MultiVar(str)], help_text="种子商店"),
Subcommand("buy-seed", Args["name?", str]["num?", int], help_text="购买种子"),
Subcommand("my-seed", help_text="我的种子"),
Subcommand("sowing", Args["name?", str]["num?", int], help_text="播种"),
@@ -134,7 +138,9 @@ diuse_farm = on_alconna(
Subcommand("stealing", Args["target?", At], help_text="偷菜"),
Subcommand("buy-point", Args["num?", int], help_text="购买农场币"),
#Subcommand("sell-point", Args["num?", int], help_text="转换金币")
Subcommand("change-name", Args["name?", str], help_text="更改农场名")
Subcommand("change-name", Args["name?", str], help_text="更改农场名"),
Subcommand("sign-in", help_text="农场签到"),
Subcommand("admin-up", Args["num?", int], help_text="农场下阶段"),
),
priority=5,
block=True,
@@ -144,12 +150,30 @@ diuse_farm = on_alconna(
async def _(session: Uninfo):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await isRegisteredByUid(uid):
return
image = await g_pFarmManager.drawFarmByUid(uid)
await MessageUtils.build_message(image).send(reply_to=True)
diuse_farm.shortcut(
"农场详述",
command="我的农场",
arguments=["detail"],
prefix=True,
)
@diuse_farm.assign("detail")
async def _(session: Uninfo):
uid = str(session.user.id)
if not await isRegisteredByUid(uid):
return
info = await g_pFarmManager.drawDetailFarmByUid(uid)
await MessageUtils.alc_forward_msg([info], session.self_id, BotConfig.self_nickname).send()
diuse_farm.shortcut(
"我的农场币",
command="我的农场",
@@ -160,7 +184,7 @@ diuse_farm.shortcut(
@diuse_farm.assign("my-point")
async def _(session: Uninfo):
uid = str(session.user.id)
point = await g_pSqlManager.getUserPointByUid(uid)
point = await g_pDBService.user.getUserPointByUid(uid)
if point < 0:
await MessageUtils.build_message("尚未开通农场,快at我发送 开通农场 开通吧").send()
@@ -176,13 +200,35 @@ diuse_farm.shortcut(
)
@diuse_farm.assign("seed-shop")
async def _(session: Uninfo, num: Query[int] = AlconnaQuery("num", 1)):
async def _(session: Uninfo, res: Match[tuple[str, ...]]):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await isRegisteredByUid(uid):
return
image = await g_pShopManager.getSeedShopImage(num.result)
if res.result is inspect._empty:
raw = []
else:
raw = res.result
filterKey: str | int | None = None
page: int = 1
if len(raw) >= 1 and raw[0] is not None:
first = raw[0]
if isinstance(first, str) and first.isdigit():
page = int(first)
else:
filterKey = first
if len(raw) >= 2 and raw[1] is not None and isinstance(raw[1], str) and raw[1].isdigit():
page = int(raw[1])
if filterKey is None:
image = await g_pShopManager.getSeedShopImage(page)
else:
image = await g_pShopManager.getSeedShopImage(filterKey, page)
await MessageUtils.build_message(image).send()
diuse_farm.shortcut(
@@ -193,7 +239,7 @@ diuse_farm.shortcut(
)
@diuse_farm.assign("buy-seed")
async def _(session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("num", 1),):
async def _(session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("num", 1)):
if not name.available:
await MessageUtils.build_message(
"请在指令后跟需要购买的种子名称"
@@ -201,7 +247,7 @@ async def _(session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("n
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await isRegisteredByUid(uid):
return
result = await g_pShopManager.buySeed(uid, name.result, num.result)
@@ -218,7 +264,7 @@ diuse_farm.shortcut(
async def _(session: Uninfo):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await isRegisteredByUid(uid):
return
result = await g_pFarmManager.getUserSeedByUid(uid)
@@ -232,7 +278,7 @@ diuse_farm.shortcut(
)
@diuse_farm.assign("sowing")
async def _(session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("num", -1),):
async def _(session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("num", -1)):
if not name.available:
await MessageUtils.build_message(
"请在指令后跟需要播种的种子名称"
@@ -240,7 +286,7 @@ async def _(session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("n
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await isRegisteredByUid(uid):
return
result = await g_pFarmManager.sowing(uid, name.result, num.result)
@@ -258,7 +304,7 @@ diuse_farm.shortcut(
async def _(session: Uninfo):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await isRegisteredByUid(uid):
return
result = await g_pFarmManager.harvest(uid)
@@ -275,7 +321,7 @@ diuse_farm.shortcut(
async def _(session: Uninfo):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await isRegisteredByUid(uid):
return
result = await g_pFarmManager.eradicate(uid)
@@ -293,7 +339,7 @@ diuse_farm.shortcut(
async def _(session: Uninfo):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await isRegisteredByUid(uid):
return
result = await g_pFarmManager.getUserPlantByUid(uid)
@@ -310,7 +356,7 @@ reclamation = on_alconna(
async def _(session: Uninfo):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await isRegisteredByUid(uid):
return
condition = await g_pFarmManager.reclamationCondition(uid)
@@ -339,10 +385,10 @@ diuse_farm.shortcut(
)
@diuse_farm.assign("sell-plant")
async def _(session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("num", -1),):
async def _(session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("num", -1)):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await isRegisteredByUid(uid):
return
result = await g_pShopManager.sellPlantByUid(uid, name.result, num.result)
@@ -359,16 +405,16 @@ diuse_farm.shortcut(
async def _(session: Uninfo, target: Match[At]):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await isRegisteredByUid(uid):
return
if not target.available:
await MessageUtils.build_message("请在指令后跟需要at的人").finish(reply_to=True)
tar = target.result
point = await g_pSqlManager.getUserPointByUid(tar.target)
result = await g_pDBService.user.isUserExist(tar.target)
if point < 0:
if not result:
await MessageUtils.build_message("目标尚未开通农场,快邀请ta开通吧").send()
return None
@@ -391,7 +437,7 @@ async def _(session: Uninfo, num: Query[int] = AlconnaQuery("num", 0)):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await isRegisteredByUid(uid):
return
result = await g_pFarmManager.buyPointByUid(uid, num.result)
@@ -409,19 +455,96 @@ diuse_farm.shortcut(
async def _(session: Uninfo, name: Match[str]):
if not name.available:
await MessageUtils.build_message(
"请在指令后跟需要更改的用户名"
"请在指令后跟需要更改的农场名"
).finish(reply_to=True)
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await isRegisteredByUid(uid):
return
safeName = sanitize_username(name.result)
result = await g_pSqlManager.updateUserNameByUid(uid, safeName)
if safeName == "神秘农夫":
await MessageUtils.build_message("农场名不支持特殊符号!").send(reply_to=True)
return
result = await g_pDBService.user.updateUserNameByUid(uid, safeName)
if result == True:
await MessageUtils.build_message("更新用户名成功!").send(reply_to=True)
await MessageUtils.build_message("更新农场名成功!").send(reply_to=True)
else:
await MessageUtils.build_message("更新用户名失败!").send(reply_to=True)
await MessageUtils.build_message("更新农场名失败!").send(reply_to=True)
diuse_farm.shortcut(
"农场签到",
command="我的农场",
arguments=["sign-in"],
prefix=True,
)
@diuse_farm.assign("sign-in")
async def _(session: Uninfo):
uid = str(session.user.id)
if not await isRegisteredByUid(uid):
return
#判断签到是否正常加载
if not g_bSignStatus:
await MessageUtils.build_message("签到功能异常!").send()
return
toDay = g_pToolManager.dateTime().date().today()
message = ""
status = await g_pDBService.userSign.sign(uid, toDay.strftime("%Y-%m-%d"))
#如果完成签到
if status == 1 or status == 2:
#获取签到总天数
signDay = await g_pDBService.userSign.getUserSignCountByDate(uid, toDay.strftime("%Y-%m"))
exp, point = await g_pDBService.userSign.getUserSignRewardByDate(uid, toDay.strftime("%Y-%m-%d"))
message += f"签到成功!累计签到天数:{signDay}\n获得经验{exp},获得金币{point}"
reward = g_pJsonManager.m_pSign['continuou'].get(f"{signDay}", None)
if reward:
extraPoint = reward.get('point', 0)
extraExp = reward.get('exp', 0)
plant = reward.get('plant', {})
message += f"\n\n成功领取累计签到奖励:\n额外获得经验{extraExp},额外获得金币{extraPoint}"
vipPoint = reward.get('vipPoint', 0)
if vipPoint > 0:
message += f",额外获得点券{vipPoint}"
if plant:
for key, value in plant.items():
message += f"\n获得{key}种子 * {value}"
else:
message = "签到失败!未知错误"
await MessageUtils.build_message(message).send()
# await MessageUtils.alc_forward_msg([info], session.self_id, BotConfig.self_nickname).send(reply_to=True)
diuse_farm.shortcut(
"农场下阶段(.*?)",
command="我的农场",
arguments=["admin-up"],
prefix=True,
)
@diuse_farm.assign("admin-up")
async def _(session: Uninfo, num: Query[int] = AlconnaQuery("num", 0)):
uid = str(session.user.id)
if not await isRegisteredByUid(uid):
return
await g_pDBService.userSoil.nextPhase(uid, num.result)
+6 -96
View File
@@ -1,106 +1,16 @@
import json
from pathlib import Path
from zhenxun.configs.path_config import DATA_PATH
from zhenxun.services.log import logger
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"
class CJsonManager:
def __init__(self):
self.m_pItem = {}
self.m_pPlant = {}
self.m_pLevel = {}
self.m_pSoil = {}
g_sConfigPath = Path(__file__).resolve().parent / "config"
g_sSignInPath = g_sConfigPath / "sign_in.json"
async def init(self) -> bool:
if not await self.initItem():
return False
if not await self.initPlant():
return False
if not await self.initLevel():
return False
if not await self.initSoil():
return False
return True
async def initItem(self) -> bool:
current_file_path = Path(__file__)
try:
with open(
current_file_path.resolve().parent / "config/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 initPlant(self) -> bool:
current_file_path = Path(__file__)
try:
with open(
current_file_path.resolve().parent / "config/plant.json",
encoding="utf-8",
) as file:
self.m_pPlant = json.load(file)
return True
except FileNotFoundError:
logger.warning("plant.json 打开失败")
return False
except json.JSONDecodeError as e:
logger.warning(f"plant.json JSON格式错误: {e}")
return False
async def initLevel(self) -> bool:
current_file_path = Path(__file__)
try:
with open(
current_file_path.resolve().parent / "config/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:
current_file_path = Path(__file__)
try:
with open(
current_file_path.resolve().parent / "config/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
g_pJsonManager = CJsonManager()
g_bSignStatus = True
-276
View File
@@ -1,276 +0,0 @@
{
"version": 0.01,
"zhuShi":
[
"level: 解锁等级",
"buy: 购买价格",
"limit: 限制等级 0:普通土地 1:红土地 2:黄土地 3:黑土地",
"experience: 收获经验",
"harvest: 收获数量",
"price: 果实售价",
"time: 成熟时间 小时",
"crop: 作物可以收几次",
"again: 再次成熟时间 单位:小时",
"phase: 阶段 目前为 成熟时间 / 阶段 来显示每阶段图片",
"general: 第一阶段是否为通用阶段素材",
"sell: 是否可以上架交易行"
],
"plant":
{
"胡萝卜":
{
"level": 0,
"buy": 163,
"limit": 0,
"experience": 18,
"harvest": 17,
"price": 21,
"time": 13,
"crop": 1,
"again": 0,
"phase": 5,
"general": true,
"sell": false
},
"白萝卜":
{
"level": 0,
"buy": 125,
"limit": 0,
"experience": 15,
"harvest": 16,
"price": 17,
"time": 10,
"crop": 1,
"again": 0,
"phase": 5,
"general": true,
"sell": false
},
"牧草":
{
"level": 0,
"buy": 80,
"limit": 0,
"experience": 10,
"harvest": 25,
"price": 6,
"time": 8,
"crop": 1,
"again": 0,
"phase": 5,
"general": true,
"sell": false
},
"大白菜":
{
"level": 1,
"buy": 168,
"limit": 0,
"experience": 19,
"harvest": 17,
"price": 22,
"time": 14,
"crop": 1,
"again": 0,
"phase": 5,
"general": true,
"sell": false
},
"大蒜":
{
"level": 1,
"buy": 169,
"limit": 0,
"experience": 19,
"harvest": 17,
"price": 22,
"time": 14,
"crop": 1,
"again": 0,
"phase": 6,
"general": true,
"sell": false
},
"水稻":
{
"level": 2,
"buy": 168,
"limit": 0,
"experience": 19,
"harvest": 18,
"price": 21,
"time": 14,
"crop": 1,
"again": 0,
"phase": 5,
"general": false,
"sell": false
},
"小麦":
{
"level": 2,
"buy": 168,
"limit": 0,
"experience": 19,
"harvest": 18,
"price": 21,
"time": 14,
"crop": 1,
"again": 0,
"phase": 6,
"general": true,
"sell": false
},
"玉米":
{
"level": 3,
"buy": 175,
"limit": 0,
"experience": 19,
"harvest": 17,
"price": 23,
"time": 14,
"crop": 1,
"again": 0,
"phase": 6,
"general": true,
"sell": false
},
"油菜":
{
"level": 4,
"buy": 194,
"limit": 0,
"experience": 29,
"harvest": 23,
"price": 24,
"time": 17,
"crop": 1,
"again": 0,
"phase": 5,
"general": true,
"sell": false
},
"生菜":
{
"level": 4,
"buy": 195,
"limit": 0,
"experience": 25,
"harvest": 21,
"price": 24,
"time": 19,
"crop": 1,
"again": 0,
"phase": 6,
"general": true,
"sell": false
},
"小白菜":
{
"level": 4,
"buy": 195,
"limit": 0,
"experience": 15,
"harvest": 18,
"price": 21,
"time": 11,
"crop": 1,
"again": 0,
"phase": 5,
"general": true,
"sell": false
},
"红枣":
{
"level": 5,
"buy": 237,
"limit": 0,
"experience": 21,
"harvest": 20,
"price": 25,
"time": 16,
"crop": 1,
"again": 0,
"phase": 6,
"general": true,
"sell": false
},
"茄子":
{
"level": 5,
"buy": 237,
"limit": 0,
"experience": 21,
"harvest": 20,
"price": 25,
"time": 16,
"crop": 1,
"again": 0,
"phase": 6,
"general": true,
"sell": false
},
"番茄":
{
"level": 6,
"buy": 251,
"limit": 0,
"experience": 22,
"harvest": 21,
"price": 26,
"time": 17,
"crop": 1,
"again": 0,
"phase": 6,
"general": true,
"sell": false
},
"ATTomato":
{
"level": 6,
"buy": 99999,
"limit": 0,
"experience": 22,
"harvest": 21,
"price": 7000,
"time": 17,
"crop": 1,
"again": 0,
"phase": 6,
"general": true,
"sell": false
},
"红玫瑰":
{
"level": 7,
"buy": 251,
"limit": 0,
"experience": 23,
"harvest": 22,
"price": 27,
"time": 18,
"crop": 1,
"again": 0,
"phase": 6,
"general": true,
"sell": false
},
"豌豆":
{
"level": 7,
"buy": 266,
"limit": 0,
"experience": 23,
"harvest": 22,
"price": 27,
"time": 18,
"crop": 1,
"again": 0,
"phase": 6,
"general": true,
"sell": false
}
}
}
+75 -1
View File
@@ -1,4 +1,78 @@
{
"date": "202505",
""
"exp_max": 50,
"exp_min": 5,
"point_max": 2000,
"point_min": 200,
"continuou":
{
"1":
{
"point": 3000,
"exp": 20
},
"3":
{
"point": 5000,
"exp": 25,
"plant":
{
"胡萝卜": 3
}
},
"5":
{
"point": 7000,
"exp": 50,
"plant":
{
"胡萝卜": 3
}
},
"7":
{
"point": 7000,
"exp": 50,
"plant":
{
"胡萝卜": 3
}
},
"10":
{
"point": 7000,
"exp": 50,
"plant":
{
"胡萝卜": 3
}
},
"15":
{
"point": 7000,
"exp": 50,
"plant":
{
"胡萝卜": 3
}
},
"20":
{
"point": 7000,
"exp": 50,
"plant":
{
"胡萝卜": 3
}
},
"25":
{
"point": 7000,
"exp": 50,
"plant":
{
"胡萝卜": 3
}
}
}
}
-909
View File
@@ -1,909 +0,0 @@
import math
import os
import re
from contextlib import asynccontextmanager
from datetime import date, datetime, timedelta
from io import StringIO
from math import e
from typing import Any, Dict, List, Optional
import aiosqlite
from zhenxun.services.log import logger
from .config import g_pJsonManager, g_sDBFilePath, g_sDBPath
class CSqlManager:
def __init__(self):
g_sDBPath.mkdir(parents=True, exist_ok=True)
@classmethod
async def cleanup(cls):
if cls.m_pDB:
await cls.m_pDB.close()
@classmethod
async def init(cls) -> bool:
bIsExist = os.path.exists(g_sDBFilePath)
cls.m_pDB = await aiosqlite.connect(g_sDBFilePath)
cls.m_pDB.row_factory = aiosqlite.Row
await cls.checkDB()
return True
@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)
await cls.m_pDB.execute(
f'INSERT INTO "{tmpTable}" ({colsStr}) SELECT {colsStr} FROM "{tableName}";'
)
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 checkDB(cls) -> bool:
#1. 用户表
userInfo = {
"uid": "INTEGER PRIMARY KEY AUTOINCREMENT",
"name": "TEXT NOT NULL",
"exp": "INTEGER DEFAULT 0",
"point": "INTEGER DEFAULT 0",
"soil": "INTEGER DEFAULT 3",
"stealing": "TEXT DEFAULT NULL"
}
#2. 土地表
userSoilInfo = {
"uid": "INTEGER PRIMARY KEY AUTOINCREMENT",
**{f"soil{i}": "TEXT DEFAULT ''" for i in range(1, 31)}
}
#3. 用户作物明细表
userPlant = {
"uid": "INTEGER NOT NULL",
"plant": "TEXT NOT NULL",
"count": "INTEGER NOT NULL DEFAULT 0",
#建联合主键保证每个品种一行
"PRIMARY KEY": "(uid, plant)"
}
#4. 用户种子明细表
userSeed = {
"uid": "INTEGER NOT NULL",
"seed": "TEXT NOT NULL",
"count": "INTEGER NOT NULL DEFAULT 0",
"PRIMARY KEY": "(uid, seed)"
}
# 5. 用户道具明细表
userItem = {
"uid": "INTEGER NOT NULL",
"item": "TEXT NOT NULL",
"count": "INTEGER NOT NULL DEFAULT 0",
"PRIMARY KEY": "(uid, item)"
}
#建表(或增列)
await cls.ensureTableSchema("user", userInfo)
await cls.ensureTableSchema("soil", userSoilInfo)
await cls.ensureTableSchema("userPlant", userPlant)
await cls.ensureTableSchema("userSeed", userSeed)
await cls.ensureTableSchema("userItem", userItem)
return True
@classmethod
async def executeDB(cls, command: str) -> bool:
"""执行自定义SQL
Args:
command (str): SQL语句
Returns:
bool: 是否执行成功
"""
if len(command) <= 0:
logger.warning("数据库语句长度为空!")
return False
try:
async with cls._transaction():
await cls.m_pDB.execute(command)
return True
except Exception as e:
logger.warning("数据库语句执行出错:" + command)
return False
@classmethod
async def initUserInfoByUid(cls, uid: str, name: str = "", exp: int = 0, point: int = 500):
"""初始化用户信息
Args:
uid (str): 用户Uid
name (str): 农场名称
exp (int): 农场经验
point (int): 农场币
"""
#用户信息
userInfo = f"""
INSERT INTO user (uid, name, exp, point, soil, stealing) VALUES ({uid}, '{name}', {exp}, {point}, 3, '{date.today()}|5')
"""
#用户土地
userSoilInfo = f"""
INSERT INTO soil (uid) VALUES ({uid});
"""
if not await cls.executeDB(userInfo):
return False
if not await cls.executeDB(userSoilInfo):
return False
return "开通农场成功"
@classmethod
async def getUserInfoByUid(cls, uid: str) -> dict:
"""根据用户Uid获取用户信息
Args:
uid (str): 用户Uid
Returns:
list[dict]: 用户信息
"""
if len(uid) <= 0:
return {}
try:
async with cls.m_pDB.execute(
"SELECT * FROM user WHERE uid = ?", (uid,)
) as cursor:
async for row in cursor:
userDict = {
"uid": row[0],
"name": row[1],
"exp": row[2],
"point": row[3],
"soil": row[4],
"stealing": row[5]
}
return userDict
return {}
except Exception as e:
logger.warning(f"getUserInfoByUid查询失败: {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(f"真寻农场getUserNameByUid查询失败: {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(f"真寻农场updateUserNameByUid失败: {e}")
return False
@classmethod
async def getUserPointByUid(cls, uid: str) -> int:
"""根据用户Uid获取用户农场币
Args:
uid (str): 用户Uid
Returns:
int: 用户农场币
"""
if len(uid) <= 0:
return -1
try:
async with cls.m_pDB.execute(f"SELECT point FROM user WHERE uid = {uid}") as cursor:
async for row in cursor:
return int(row[0])
return -1
except Exception as e:
logger.warning(f"getUserPointByUid查询失败: {e}")
return -1
@classmethod
async def updateUserPointByUid(cls, uid: str, point: int) -> int:
"""根据用户Uid修改用户农场币
Args:
uid (str): 用户Uid
point (int): 要更新的新农场币数量(需 ≥ 0)
Returns:
int: 更新后的农场币数量(成功时),-1(失败时)
"""
if len(uid) <= 0:
logger.warning("参数校验失败: uid为空或农场币值无效")
return -1
try:
return await cls.executeDB(f"UPDATE user SET point = {point} WHERE uid = {uid}")
except Exception as e:
logger.error(f"金币更新失败: {e}")
return -1
@classmethod
async def getUserExpByUid(cls, uid: str) -> int:
"""根据用户Uid获取用户经验
Args:
uid (str): 用户Uid
Returns:
int: 用户经验值
"""
if len(uid) <= 0:
return -1
try:
async with cls.m_pDB.execute(f"SELECT exp FROM user WHERE uid = {uid}") as cursor:
async for row in cursor:
return int(row[0])
return -1
except Exception as e:
logger.warning(f"getUserLevelByUid查询失败: {e}")
return -1
@classmethod
async def UpdateUserExpByUid(cls, uid: str, exp: int) -> bool:
"""根据用户Uid刷新用户经验
Args:
uid (str): 用户Uid
Returns:
bool: 是否成功
"""
if len(uid) <= 0:
return False
sql = f"UPDATE user SET exp = '{exp}' WHERE uid = {uid}"
return await cls.executeDB(sql)
@classmethod
async def getUserLevelByUid(cls, uid: str) -> tuple[int, int, int]:
"""根据用户Uid获取用户等级
Args:
uid (str): 用户Uid
Returns:
tuple[int, int, int]: (当前等级, 下级所需经验, 当前等级剩余经验)
"""
if len(uid) <= 0:
return -1, -1, -1
try:
async with cls.m_pDB.execute(f"SELECT exp FROM user WHERE uid = {uid}") as cursor:
async for row in cursor:
exp = int(row[0])
level = exp // 200
nextLevelExp = 200 * (level + 1)
currentLevelExp = level * 200
remainingExp = exp - currentLevelExp
return level, nextLevelExp, remainingExp
return -1, -1, -1
except Exception as e:
logger.warning(f"getUserLevelByUid查询失败: {e}")
return -1, -1, -1
@classmethod
async def getUserSoilByUid(cls, uid: str) -> int:
"""根据用户Uid获取解锁地块
Args:
uid (str): 用户Uid
Returns:
int: 解锁几块地
"""
if len(uid) <= 0:
return 0
async with cls.m_pDB.execute(f"SELECT soil FROM user WHERE uid = {uid}") as cursor:
async for row in cursor:
if not row[0]:
return 0
else:
return int(row[0])
return 0
@classmethod
async def getUserSoilStatusBySoilID(cls, uid: str, soil: str) -> tuple[bool, str]:
"""根据土地块获取用户土地状态
Args:
uid (str): 用户Uid
soil (str): 土地id
Returns:
tuple[bool, str]: [是否可以播种,土地信息]
"""
if len(uid) <= 0:
return False, ""
async with cls.m_pDB.execute(f"SELECT {soil} FROM soil WHERE uid = {uid}") as cursor:
async for row in cursor:
if row[0] == None or len(row[0]) <= 0:
return True, ""
else:
return False, row[0]
return False, ""
@classmethod
async def updateUserSoilStatusByPlantName(cls, uid: str, soil: str,
plant: str = "",
status: int = 0) -> bool:
"""根据种子名称使用户播种
Args:
uid (str): 用户Uid
soil (str): 土地id
plant (str): 种子名称
Returns:
bool: 是否更新成功
"""
if len(uid) <= 0:
return False
if len(plant) <= 0 and status == 4:
s = f",,,{status},"
elif len(plant) <= 0 and status != 4:
s = ""
else:
#获取种子信息 这里能崩我吃
plantInfo = g_pJsonManager.m_pPlant['plant'][plant]
currentTime = datetime.now()
newTime = currentTime + timedelta(hours=int(plantInfo['time']))
#0: 种子名称
#1: 种下时间
#2: 预计成熟时间
#3: 地状态:0:无 1:长草 2:生虫 3:缺水 4:枯萎
#4: 是否被偷 示例:QQ号-偷取数量|QQ号-偷取数量
#5: 土地等级 0:普通 1:红土地 2:黑土地 3:金土地 4:紫晶土地 5:蓝晶土地 6:黑晶土地
s = f"{plant},{int(currentTime.timestamp())},{int(newTime.timestamp())},{status},,"
sql = f"UPDATE soil SET {soil} = '{s}' WHERE uid = {uid}"
return await cls.executeDB(sql)
@classmethod
async def addUserSeedByUid(cls, uid: str, seed: str, count: int = 1) -> bool:
"""根据用户uid添加种子信息
Args:
uid (str): 用户uid
seed (str): 种子名称
count (int): 数量
Returns:
bool: 是否添加成功
"""
try:
async with cls._transaction():
#检查是否已存在该种子
async with cls.m_pDB.execute(
"SELECT count FROM userSeed WHERE uid = ? AND seed = ?",
(uid, seed)
) as cursor:
row = await cursor.fetchone()
if row:
#如果种子已存在,则更新数量
newCount = row[0] + count
await cls.m_pDB.execute(
"UPDATE userSeed SET count = ? WHERE uid = ? AND seed = ?",
(newCount, uid, seed)
)
else:
#如果种子不存在,则插入新记录
newCount = count
await cls.m_pDB.execute(
"INSERT INTO userSeed (uid, seed, count) VALUES (?, ?, ?)",
(uid, seed, count)
)
#如果种子数量为 0,删除记录
if newCount <= 0:
await cls.m_pDB.execute(
"DELETE FROM userSeed WHERE uid = ? AND seed = ?",
(uid, seed)
)
return True
except Exception as e:
logger.warning(f"真寻农场addUserSeedByUid 失败: {e}")
return False
@classmethod
async def getUserSeedByName(cls, uid: str, seed: str) -> Optional[int]:
"""根据种子名称获取种子数量
Args:
uid (str): 用户uid
seed (str): 种子名称
Returns:
Optional[int]: 种子数量
"""
try:
async with cls.m_pDB.execute(
"SELECT count FROM userSeed WHERE uid = ? AND seed = ?",
(uid, seed)
) as cursor:
row = await cursor.fetchone()
return row[0] if row else None
except Exception as e:
logger.warning(f"真寻农场getUserSeedByName 查询失败: {e}")
return None
@classmethod
async def getUserSeedByUid(cls, uid: str) -> dict:
"""根据用户Uid获取仓库全部种子信息
Args:
uid (str): 用户uid
Returns:
dict: 种子信息
"""
cursor = await cls.m_pDB.execute(
"SELECT seed, count FROM userSeed WHERE uid=?",
(uid,)
)
rows = await cursor.fetchall()
return {row["seed"]: row["count"] for row in rows}
@classmethod
async def updateUserSeedByName(cls, uid: str, seed: str, count: int) -> bool:
"""根据种子名称更新种子数量
Args:
uid (str): 用户uid
seed (str): 种子名称
count (int): 种子数量
Returns:
bool: 是否成功
"""
try:
if count <= 0:
return await cls.deleteUserSeedByName(uid, seed)
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE userSeed SET count = ? WHERE uid = ? AND seed = ?",
(count, uid, seed)
)
return True
except Exception as e:
logger.warning(f"真寻农场updateUserSeedByName失败:{e}")
return False
@classmethod
async def deleteUserSeedByName(cls, uid: str, seed: str) -> bool:
"""根据种子名称从种子仓库中删除种子
Args:
uid (str): 用户uid
seed (str): 种子名称
Returns:
bool: 是否成功
"""
try:
async with cls._transaction():
await cls.m_pDB.execute(
"DELETE FROM userSeed WHERE uid = ? AND seed = ?",
(uid, seed)
)
return True
except Exception as e:
logger.warning(f"真寻农场deleteUserSeedByName 删除失败: {e}")
return False
@classmethod
async def addUserPlantByUid(cls, uid: str, plant: str, count: int = 1) -> bool:
"""根据用户uid添加作物信息
Args:
uid (str): 用户uid
plant (str): 作物名称
count (int): 数量
Returns:
bool: 是否添加成功
"""
try:
async with cls._transaction():
#检查是否已存在该作物
async with cls.m_pDB.execute(
"SELECT count FROM userPlant WHERE uid = ? AND plant = ?",
(uid, plant)
) as cursor:
row = await cursor.fetchone()
if row:
#如果作物已存在,则更新数量
new_count = row[0] + count
await cls.m_pDB.execute(
"UPDATE userPlant SET count = ? WHERE uid = ? AND plant = ?",
(new_count, uid, plant)
)
else:
#如果作物不存在,则插入新记录
await cls.m_pDB.execute(
"INSERT INTO userPlant (uid, plant, count) VALUES (?, ?, ?)",
(uid, plant, count)
)
return True
except Exception as e:
logger.warning(f"真寻农场addUserPlantByUid 失败: {e}")
return False
@classmethod
async def getUserPlantByUid(cls, uid: str) -> Dict[str, int]:
"""根据用户uid获取全部作物信息
Args:
uid (str): 用户uid
Returns:
Dict[str, int]: 作物名称和数量
"""
cursor = await cls.m_pDB.execute(
"SELECT plant, count FROM userPlant WHERE uid=?",
(uid,)
)
rows = await cursor.fetchall()
return {row["plant"]: row["count"] for row in rows}
@classmethod
async def getUserPlantByName(cls, uid: str, plant: str) -> Optional[int]:
"""根据作物名称获取用户的作物数量
Args:
uid (str): 用户uid
plant (str): 作物名称
Returns:
Optional[int]: 作物数量
"""
try:
async with cls.m_pDB.execute(
"SELECT count FROM userPlant WHERE uid = ? AND plant = ?",
(uid, plant)
) as cursor:
row = await cursor.fetchone()
return row[0] if row else None
except Exception as e:
logger.warning(f"真寻农场getUserPlantByName 查询失败: {e}")
return None
@classmethod
async def updateUserPlantByName(cls, uid: str, plant: str, count: int) -> bool:
"""更新 userPlant 表中某个作物的数量
Args:
uid (str): 用户uid
plant (str): 作物名称
count (int): 新的作物数量
Returns:
bool: 是否更新成功
"""
try:
if count <= 0:
return await cls.deleteUserPlantByName(uid, plant)
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE userPlant SET count = ? WHERE uid = ? AND plant = ?",
(count, uid, plant)
)
return True
except Exception as e:
logger.warning(f"真寻农场updateUserPlantByName失败:{e}")
return False
@classmethod
async def deleteUserPlantByName(cls, uid: str, plant: str) -> bool:
"""从 userPlant 表中删除某个作物记录
Args:
uid (str): 用户uid
plant (str): 作物名称
Returns:
bool: 是否删除成功
"""
try:
async with cls._transaction():
await cls.m_pDB.execute(
"DELETE FROM userPlant WHERE uid = ? AND plant = ?",
(uid, plant)
)
return True
except Exception as e:
logger.warning(f"真寻农场deleteUserPlantByName 失败: {e}")
return False
@classmethod
async def getUserItemByName(cls, uid: str, item: str) -> Optional[int]:
"""根据道具名称查询某一项数量
Args:
uid (str): 用户uid
item (str): 道具名称
Returns:
Optional[int]: 数量(不存在返回None)
"""
if not uid or not item:
return None
try:
async with cls.m_pDB.execute(
"SELECT count FROM userItem WHERE uid = ? AND item = ?",
(uid, item)
) as cursor:
row = await cursor.fetchone()
return row[0] if row else None
except Exception as e:
logger.warning(f"真寻农场getUserItemByName查询失败: {e}")
return None
@classmethod
async def getUserItemByUid(cls, uid: str) -> dict:
"""根据用户Uid获取全部道具信息
Args:
uid (str): 用户uid
Returns:
dict: {itemName: count, ...}
"""
if not uid:
return {}
try:
cursor = await cls.m_pDB.execute(
"SELECT item, count FROM userItem WHERE uid = ?",
(uid,)
)
rows = await cursor.fetchall()
return {row["item"]: row["count"] for row in rows}
except Exception as e:
logger.warning(f"真寻农场getUserItemByUid查询失败: {e}")
return {}
@classmethod
async def deleteUserItemByName(cls, uid: str, item: str) -> bool:
"""根据道具名删除道具
Args:
uid (str): 用户uid
item (str): 道具名称
Returns:
bool: 是否删除成功
"""
if not uid or not item:
return False
try:
async with cls._transaction():
await cls.m_pDB.execute(
"DELETE FROM userItem WHERE uid = ? AND item = ?",
(uid, item)
)
return True
except Exception as e:
logger.warning(f"真寻农场deleteUserItemByName失败: {e}")
return False
@classmethod
async def updateUserItemByName(cls, uid: str, item: str, count: int) -> bool:
"""根据道具名直接更新道具数量
Args:
uid (str): 用户uid
item (str): 道具名称
count (int): 要更新的新数量
Returns:
bool: 是否更新成功
"""
if not uid or not item:
return False
try:
async with cls._transaction():
if count <= 0:
await cls.m_pDB.execute(
"DELETE FROM userItem WHERE uid = ? AND item = ?",
(uid, item)
)
else:
await cls.m_pDB.execute(
"UPDATE userItem SET count = ? WHERE uid = ? AND item = ?",
(count, uid, item)
)
return True
except Exception as e:
logger.warning(f"真寻农场updateUserItemByName失败: {e}")
return False
@classmethod
async def addUserItemByUid(cls, uid: str, item: str, count: int = 1) -> bool:
"""根据用户uid添加道具信息
Args:
uid (str): 用户uid
item (str): 道具名称
count (int, optional): 数量.Defaults to 1.
Returns:
bool: 是否添加成功
"""
if not uid or not item:
return False
try:
async with cls._transaction():
async with cls.m_pDB.execute(
"SELECT count FROM userItem WHERE uid = ? AND item = ?",
(uid, item)
) as cursor:
row = await cursor.fetchone()
if row:
newCount = row[0] + count
if newCount <= 0:
await cls.m_pDB.execute(
"DELETE FROM userItem WHERE uid = ? AND item = ?",
(uid, item)
)
else:
await cls.m_pDB.execute(
"UPDATE userItem SET count = ? WHERE uid = ? AND item = ?",
(newCount, uid, item)
)
else:
if count > 0:
await cls.m_pDB.execute(
"INSERT INTO userItem (uid, item, count) VALUES (?, ?, ?)",
(uid, item, count)
)
return True
except Exception as e:
logger.warning(f"真寻农场addUserItemByUid失败: {e}")
return False
g_pSqlManager = CSqlManager()
+134
View File
@@ -0,0 +1,134 @@
import math
import os
import re
from contextlib import asynccontextmanager
from pathlib import Path
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)
await cls.m_pDB.execute(
f'INSERT INTO "{tmpTable}" ({colsStr}) SELECT {colsStr} FROM "{tableName}";'
)
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()
+246
View File
@@ -0,0 +1,246 @@
import ast
import os
import re
from contextlib import asynccontextmanager
from unittest import result
import aiosqlite
from zhenxun.services.log import logger
from ..config import g_bIsDebug, g_sPlantPath
class CPlantManager:
def __init__(self):
try:
os.mkdir(g_sPlantPath)
except FileExistsError:
pass
@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:
_ = os.path.exists(g_sPlantPath)
if g_bIsDebug:
cls.m_pDB = await aiosqlite.connect(str(g_sPlantPath.parent / "plant-test.db"))
else:
cls.m_pDB = await aiosqlite.connect(str(g_sPlantPath))
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 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
@classmethod
async def getPlantByName(cls, name: str) -> dict | None:
"""根据作物名称查询记录
Args:
name (str): 作物名称
Returns:
dict | None: 返回记录字典,未找到返回None
"""
try:
async with cls.m_pDB.execute(
"SELECT * FROM plant WHERE name = ?", (name,)
) as cursor:
row = await cursor.fetchone()
return dict(row) if row else None
except Exception as e:
logger.warning(f"查询作物失败: {name}", e=e)
return None
@classmethod
async def getPlantPhaseByName(cls, name: str) -> list[int]:
"""根据作物名称获取作物各个阶段
Args:
name (str): 作物名称
Returns:
list: 阶段数组
"""
try:
async with cls.m_pDB.execute(
"SELECT phase FROM plant WHERE name = ?", (name,)
) as cursor:
row = await cursor.fetchone()
if not row:
return []
phase = row[0].split(',')
seen = set()
result = []
for x in phase:
if x not in seen:
seen.add(int(x))
result.append(int(x))
return result
except Exception as e:
logger.warning(f"查询作物阶段失败: {name}", e=e)
return []
@classmethod
async def getPlantPhaseNumberByName(cls, name: str) -> int:
"""根据作物名称获取作物总阶段数
Args:
name (str): 作物名称
Returns:
int: 总阶段数
"""
try:
async with cls.m_pDB.execute(
"SELECT phase FROM plant WHERE name = ?", (name,)
) as cursor:
row = await cursor.fetchone()
if not row:
return -1
phase = row[0].split(',')
#去重
seen = set()
result = []
for x in phase:
if x not in seen:
seen.add(x)
result.append(x)
return len(result)
except Exception as e:
logger.warning(f"查询作物阶段失败: {name}", e=e)
return -1
@classmethod
async def getPlantAgainByName(cls, name: str) -> int:
"""根据作物名称获取作物再次成熟时间
Args:
name (str): 作物名称
Returns:
int: 再次成熟时间 单位:h
"""
try:
async with cls.m_pDB.execute(
"SELECT phase FROM plant WHERE name = ?", (name,)
) as cursor:
row = await cursor.fetchone()
if not row:
return -1
phase = row[0].split(',')
again = phase[-1] - phase[3] / 60 / 60
return again
except Exception as e:
logger.warning(f"查询作物阶段失败: {name}", e=e)
return -1
@classmethod
async def existsPlant(cls, name: str) -> bool:
"""判断作物是否存在
Args:
name (str): 作物名称
Returns:
bool: 存在返回True,否则False
"""
try:
async with cls.m_pDB.execute(
"SELECT 1 FROM plant WHERE name = ? LIMIT 1", (name,)
) as cursor:
row = await cursor.fetchone()
return True if row else False
except Exception as e:
logger.warning(f"检查作物存在性失败: {name}", e=e)
return False
@classmethod
async def countPlants(cls, onlyBuy: bool = False) -> int:
"""获取作物总数
Args:
onlyBuy (bool): 若为True,仅统计isBuy=1的记录,默认False
Returns:
int: 符合条件的记录数
"""
try:
if onlyBuy:
sql = "SELECT COUNT(*) FROM plant WHERE isBuy = 1"
params: tuple = ()
else:
sql = "SELECT COUNT(*) FROM plant"
params: tuple = ()
async with cls.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]:
"""查询所有作物记录"""
try:
async with cls.m_pDB.execute("SELECT * FROM plant ORDER BY level") as cursor:
rows = await cursor.fetchall()
return [dict(r) for r in rows]
except Exception as e:
logger.warning("查询所有作物失败", e=e)
return []
+475
View File
@@ -0,0 +1,475 @@
import math
from typing import List, Union
from unittest import result
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) -> Union[bool, str]:
"""初始化用户信息,包含初始偷菜时间字符串与次数
Args:
uid (str): 用户Uid
name (str): 农场名称
exp (int): 农场经验
point (int): 农场币
Returns:
Union[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, stealCount: int) -> bool:
"""根据用户Uid更新剩余偷菜次数
Args:
uid (str): 用户Uid
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 stealCount = ? WHERE uid = ?", (stealCount, uid)
)
return True
except Exception as e:
logger.warning("updateStealCountByUid 事务执行失败!", e=e)
return False
+165
View File
@@ -0,0 +1,165 @@
from typing import Optional
from zhenxun.services.log import logger
from .database import CSqlManager
class CUserItemDB(CSqlManager):
@classmethod
async def initDB(cls):
userItem = {
"uid": "TEXT NOT NULL", #用户Uid
"item": "TEXT NOT NULL", #物品名称
"count": "INTEGER NOT NULL DEFAULT 0", #数量
"PRIMARY KEY": "(uid, item)"
}
await cls.ensureTableSchema("userItem", userItem)
@classmethod
async def getUserItemByName(cls, uid: str, item: str) -> Optional[int]:
"""根据道具名称查询某一项数量
Args:
uid (str): 用户uid
item (str): 道具名称
Returns:
Optional[int]: 数量(不存在返回None)
"""
if not uid or not item:
return None
try:
async with cls.m_pDB.execute(
"SELECT count FROM userItem WHERE uid = ? AND item = ?",
(uid, item)
) as cursor:
row = await cursor.fetchone()
return row[0] if row else None
except Exception as e:
logger.warning(f"getUserItemByName查询失败!", e=e)
return None
@classmethod
async def getUserItemByUid(cls, uid: str) -> dict:
"""根据用户Uid获取全部道具信息
Args:
uid (str): 用户uid
Returns:
dict: {itemName: count, ...}
"""
if not uid:
return {}
try:
cursor = await cls.m_pDB.execute(
"SELECT item, count FROM userItem WHERE uid = ?",
(uid,)
)
rows = await cursor.fetchall()
return {row["item"]: row["count"] for row in rows}
except Exception as e:
logger.warning(f"getUserItemByUid查询失败!", e=e)
return {}
@classmethod
async def deleteUserItemByName(cls, uid: str, item: str) -> bool:
"""根据道具名删除道具
Args:
uid (str): 用户uid
item (str): 道具名称
Returns:
bool: 是否删除成功
"""
if not uid or not item:
return False
try:
async with cls._transaction():
await cls.m_pDB.execute(
"DELETE FROM userItem WHERE uid = ? AND item = ?",
(uid, item)
)
return True
except Exception as e:
logger.warning(f"deleteUserItemByName失败!", e=e)
return False
@classmethod
async def updateUserItemByName(cls, uid: str, item: str, count: int) -> bool:
"""根据道具名直接更新道具数量
Args:
uid (str): 用户uid
item (str): 道具名称
count (int): 要更新的新数量
Returns:
bool: 是否更新成功
"""
if not uid or not item:
return False
try:
async with cls._transaction():
if count <= 0:
await cls.m_pDB.execute(
"DELETE FROM userItem WHERE uid = ? AND item = ?",
(uid, item)
)
else:
await cls.m_pDB.execute(
"UPDATE userItem SET count = ? WHERE uid = ? AND item = ?",
(count, uid, item)
)
return True
except Exception as e:
logger.warning(f"updateUserItemByName失败!", e=e)
return False
@classmethod
async def addUserItemByUid(cls, uid: str, item: str, count: int = 1) -> bool:
"""根据用户uid添加道具信息
Args:
uid (str): 用户uid
item (str): 道具名称
count (int, optional): 数量.Defaults to 1.
Returns:
bool: 是否添加成功
"""
if not uid or not item:
return False
try:
async with cls._transaction():
async with cls.m_pDB.execute(
"SELECT count FROM userItem WHERE uid = ? AND item = ?",
(uid, item)
) as cursor:
row = await cursor.fetchone()
if row:
newCount = row[0] + count
if newCount <= 0:
await cls.m_pDB.execute(
"DELETE FROM userItem WHERE uid = ? AND item = ?",
(uid, item)
)
else:
await cls.m_pDB.execute(
"UPDATE userItem SET count = ? WHERE uid = ? AND item = ?",
(newCount, uid, item)
)
else:
if count > 0:
await cls.m_pDB.execute(
"INSERT INTO userItem (uid, item, count) VALUES (?, ?, ?)",
(uid, item, count)
)
return True
except Exception as e:
logger.warning(f"addUserItemByUid失败!", e=e)
return False
+144
View File
@@ -0,0 +1,144 @@
from typing import Dict, Optional
from zhenxun.services.log import logger
from .database import CSqlManager
class CUserPlantDB(CSqlManager):
@classmethod
async def initDB(cls):
userPlant = {
"uid": "TEXT NOT NULL", #用户Uid
"plant": "TEXT NOT NULL", #作物名称
"count": "INTEGER NOT NULL DEFAULT 0", #数量
"PRIMARY KEY": "(uid, plant)"
}
await cls.ensureTableSchema("userPlant", userPlant)
@classmethod
async def addUserPlantByUid(cls, uid: str, plant: str, count: int = 1) -> bool:
"""根据用户uid添加作物信息
Args:
uid (str): 用户uid
plant (str): 作物名称
count (int): 数量
Returns:
bool: 是否添加成功
"""
try:
async with cls._transaction():
#检查是否已存在该作物
async with cls.m_pDB.execute(
"SELECT count FROM userPlant WHERE uid = ? AND plant = ?",
(uid, plant)
) as cursor:
row = await cursor.fetchone()
if row:
#如果作物已存在,则更新数量
new_count = row[0] + count
await cls.m_pDB.execute(
"UPDATE userPlant SET count = ? WHERE uid = ? AND plant = ?",
(new_count, uid, plant)
)
else:
#如果作物不存在,则插入新记录
await cls.m_pDB.execute(
"INSERT INTO userPlant (uid, plant, count) VALUES (?, ?, ?)",
(uid, plant, count)
)
return True
except Exception as e:
logger.warning(f"addUserPlantByUid 失败!", e=e)
return False
@classmethod
async def getUserPlantByUid(cls, uid: str) -> Dict[str, int]:
"""根据用户uid获取全部作物信息
Args:
uid (str): 用户uid
Returns:
Dict[str, int]: 作物名称和数量
"""
cursor = await cls.m_pDB.execute(
"SELECT plant, count FROM userPlant WHERE uid=?",
(uid,)
)
rows = await cursor.fetchall()
return {row["plant"]: row["count"] for row in rows}
@classmethod
async def getUserPlantByName(cls, uid: str, plant: str) -> Optional[int]:
"""根据作物名称获取用户的作物数量
Args:
uid (str): 用户uid
plant (str): 作物名称
Returns:
Optional[int]: 作物数量
"""
try:
async with cls.m_pDB.execute(
"SELECT count FROM userPlant WHERE uid = ? AND plant = ?",
(uid, plant)
) as cursor:
row = await cursor.fetchone()
return row[0] if row else None
except Exception as e:
logger.warning(f"getUserPlantByName 查询失败!", e=e)
return None
@classmethod
async def updateUserPlantByName(cls, uid: str, plant: str, count: int) -> bool:
"""更新 userPlant 表中某个作物的数量
Args:
uid (str): 用户uid
plant (str): 作物名称
count (int): 新的作物数量
Returns:
bool: 是否更新成功
"""
try:
if count <= 0:
return await cls.deleteUserPlantByName(uid, plant)
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE userPlant SET count = ? WHERE uid = ? AND plant = ?",
(count, uid, plant)
)
return True
except Exception as e:
logger.warning(f"updateUserPlantByName失败!", e=e)
return False
@classmethod
async def deleteUserPlantByName(cls, uid: str, plant: str) -> bool:
"""从 userPlant 表中删除某个作物记录
Args:
uid (str): 用户uid
plant (str): 作物名称
Returns:
bool: 是否删除成功
"""
try:
async with cls._transaction():
await cls.m_pDB.execute(
"DELETE FROM userPlant WHERE uid = ? AND plant = ?",
(uid, plant)
)
return True
except Exception as e:
logger.warning(f"deleteUserPlantByName 失败!", e=e)
return False
+221
View File
@@ -0,0 +1,221 @@
from typing import Optional
from zhenxun.services.log import logger
from .database import CSqlManager
class CUserSeedDB(CSqlManager):
@classmethod
async def initDB(cls):
userSeed = {
"uid": "TEXT NOT NULL", #用户Uid
"seed": "TEXT NOT NULL", #种子名称
"count": "INTEGER NOT NULL DEFAULT 0", #数量
"PRIMARY KEY": "(uid, seed)"
}
await cls.ensureTableSchema("userSeed", userSeed)
@classmethod
async def addUserSeedByUid(cls, uid: str, seed: str, count: int = 1) -> bool:
"""根据用户uid添加种子信息(事务版本)
Args:
uid (str): 用户uid
seed (str): 种子名称
count (int): 数量
Returns:
bool: 是否添加成功
"""
try:
async with cls._transaction():
async with cls.m_pDB.execute(
"SELECT count FROM userSeed WHERE uid = ? AND seed = ?",
(uid, seed)
) as cursor:
row = await cursor.fetchone()
if row:
newCount = row[0] + count
await cls.m_pDB.execute(
"UPDATE userSeed SET count = ? WHERE uid = ? AND seed = ?",
(newCount, uid, seed)
)
else:
newCount = count
await cls.m_pDB.execute(
"INSERT INTO userSeed (uid, seed, count) VALUES (?, ?, ?)",
(uid, seed, count)
)
if newCount <= 0:
await cls.m_pDB.execute(
"DELETE FROM userSeed WHERE uid = ? AND seed = ?",
(uid, seed)
)
return True
except Exception as e:
logger.warning(f"addUserSeedByUid 失败!", e=e)
return False
@classmethod
async def _addUserSeedByUid(cls, uid: str, seed: str, count: int = 1) -> bool:
"""根据用户uid添加种子信息(非事务版,复用其他非事务接口)"""
try:
existing = await cls.getUserSeedByName(uid, seed)
newCount = (existing or 0) + count
if existing is not None:
await cls._updateUserSeedByName(uid, seed, newCount)
else:
await cls.m_pDB.execute(
"INSERT INTO userSeed (uid, seed, count) VALUES (?, ?, ?)",
(uid, seed, newCount)
)
if newCount <= 0:
await cls._deleteUserSeedByName(uid, seed)
return True
except Exception as e:
logger.warning(f"_addUserSeedByUid 失败!", e=e)
return False
@classmethod
async def getUserSeedByName(cls, uid: str, seed: str) -> Optional[int]:
"""根据种子名称获取种子数量
Args:
uid (str): 用户uid
seed (str): 种子名称
Returns:
Optional[int]: 种子数量
"""
try:
async with cls.m_pDB.execute(
"SELECT count FROM userSeed WHERE uid = ? AND seed = ?",
(uid, seed)
) as cursor:
row = await cursor.fetchone()
return row[0] if row else None
except Exception as e:
logger.warning(f"getUserSeedByName 查询失败!", e=e)
return None
@classmethod
async def getUserSeedByUid(cls, uid: str) -> dict:
"""根据用户Uid获取仓库全部种子信息
Args:
uid (str): 用户uid
Returns:
dict: 种子信息
"""
cursor = await cls.m_pDB.execute(
"SELECT seed, count FROM userSeed WHERE uid=?",
(uid,)
)
rows = await cursor.fetchall()
return {row["seed"]: row["count"] for row in rows}
@classmethod
async def updateUserSeedByName(cls, uid: str, seed: str, count: int) -> bool:
"""根据种子名称更新种子数量
Args:
uid (str): 用户uid
seed (str): 种子名称
count (int): 种子数量
Returns:
bool: 是否成功
"""
try:
if count <= 0:
return await cls.deleteUserSeedByName(uid, seed)
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE userSeed SET count = ? WHERE uid = ? AND seed = ?",
(count, uid, seed)
)
return True
except Exception as e:
logger.warning(f"updateUserSeedByName失败!", e=e)
return False
@classmethod
async def _updateUserSeedByName(cls, uid: str, seed: str, count: int) -> bool:
"""根据种子名称更新种子数量
Args:
uid (str): 用户uid
seed (str): 种子名称
count (int): 种子数量
Returns:
bool: 是否成功
"""
try:
if count <= 0:
return await cls.deleteUserSeedByName(uid, seed)
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE userSeed SET count = ? WHERE uid = ? AND seed = ?",
(count, uid, seed)
)
return True
except Exception as e:
logger.warning(f"updateUserSeedByName失败!", e=e)
return False
@classmethod
async def deleteUserSeedByName(cls, uid: str, seed: str) -> bool:
"""根据种子名称从种子仓库中删除种子
Args:
uid (str): 用户uid
seed (str): 种子名称
Returns:
bool: 是否成功
"""
try:
async with cls._transaction():
await cls.m_pDB.execute(
"DELETE FROM userSeed WHERE uid = ? AND seed = ?",
(uid, seed)
)
return True
except Exception as e:
logger.warning(f"deleteUserSeedByName 删除失败!", e=e)
return False
@classmethod
async def _deleteUserSeedByName(cls, uid: str, seed: str) -> bool:
"""根据种子名称从种子仓库中删除种子
Args:
uid (str): 用户uid
seed (str): 种子名称
Returns:
bool: 是否成功
"""
try:
await cls.m_pDB.execute(
"DELETE FROM userSeed WHERE uid = ? AND seed = ?",
(uid, seed)
)
return True
except Exception as e:
logger.warning(f"deleteUserSeedByName 删除失败!", e=e)
return False
+262
View File
@@ -0,0 +1,262 @@
import calendar
import random
from datetime import date, datetime, timedelta
from typing import Optional
from zhenxun.services.log import logger
from zhenxun.utils._build_image import BuildImage
from ..config import g_bIsDebug
from ..dbService import g_pDBService
from ..json import g_pJsonManager
from ..tool import g_pToolManager
from .database import CSqlManager
class CUserSignDB(CSqlManager):
@classmethod
async def initDB(cls):
#userSignLog 表结构,每条为一次签到事件
userSignLog = {
"uid": "TEXT NOT NULL", #用户ID
"signDate": "DATE NOT NULL", #签到日期
"isSupplement": "TINYINT NOT NULL DEFAULT 0", #是否补签
"exp": "INT NOT NULL DEFAULT 0", #当天签到经验
"point": "INT NOT NULL DEFAULT 0", #当天签到金币
"createdAt": "DATETIME NOT NULL DEFAULT (datetime(CURRENT_TIMESTAMP, 'localtime'))",#创建时间
"PRIMARY KEY": "(uid, signDate)"
}
#userSignSummary 表结构,每用户一行用于缓存签到状态
userSignSummary = {
"uid": "TEXT PRIMARY KEY NOT NULL", #用户ID
"totalSignDays": "INT NOT NULL DEFAULT 0", #累计签到天数
"currentMonth": "CHAR(7) NOT NULL DEFAULT ''", #当前月份(如2025-05)
"monthSignDays": "INT NOT NULL DEFAULT 0", #本月签到次数
"lastSignDate": "DATE DEFAULT NULL", #上次签到日期
"continuousDays": "INT NOT NULL DEFAULT 0", #连续签到天数
"supplementCount": "INT NOT NULL DEFAULT 0", #补签次数
"updatedAt": "DATETIME NOT NULL DEFAULT (datetime(CURRENT_TIMESTAMP, 'localtime'))"#更新时间
}
await cls.ensureTableSchema("userSignLog", userSignLog)
await cls.ensureTableSchema("userSignSummary", userSignSummary)
@classmethod
async def getUserSignRewardByDate(cls, uid: str, date: str) -> tuple[int, int]:
"""根据指定日期获取用户签到随机奖励
Args:
uid (str): 用户Uid
date (str): 用户签到日期 示例:2025-05-27
Returns:
tuple[int, int]: 经验、金币
"""
try:
async with cls._transaction():
async with cls.m_pDB.execute(
"SELECT exp, point FROM userSignLog WHERE uid=? AND signDate=?",
(uid, date)
) as cursor:
row = await cursor.fetchone()
if row is None:
return 0, 0
exp = row['exp']
point = row['point']
return exp, point
except Exception as e:
logger.warning("获取用户签到数据失败", e=e)
return 0, 0
@classmethod
async def getUserSignCountByDate(cls, uid: str, monthStr: str) -> int:
"""根据日期查询用户签到总天数
Args:
uid (str): 用户Uid
monthStr (str): 需要查询的日期 示例: 2025-05
Returns:
int: 查询月总签到天数
"""
try:
sql = "SELECT COUNT(*) FROM userSignLog WHERE uid=? AND signDate LIKE ?"
param = f"{monthStr}-%"
async with cls.m_pDB.execute(sql, (uid, param)) as cursor:
row = await cursor.fetchone()
return row[0] if row else 0
except Exception as e:
logger.warning("统计用户月签到次数失败", e=e)
return 0
@classmethod
async def hasSigned(cls, uid: str, signDate: str) -> bool:
"""判断指定日期是否已签到
Args:
uid (int): 用户ID
signDate (str): 日期字符串 'YYYY-MM-DD'
Returns:
bool: True=已签到,False=未签到
"""
try:
sql = "SELECT 1 FROM userSignLog WHERE uid=? AND signDate=? LIMIT 1"
async with cls.m_pDB.execute(sql, (uid, signDate)) as cursor:
row = await cursor.fetchone()
return row is not None
except Exception as e:
logger.warning("查询是否已签到失败", e=e)
return False
@classmethod
async def sign(cls, uid: str, signDate: str = '') -> int:
"""签到
Args:
uid (int): 用户ID
signDate (str): 日期字符串 'YYYY-MM-DD' 不传默认当前系统日期
Returns:
bool: 0: 签到失败 1: 签到成功 2: 重复签到
"""
try:
if not signDate:
signDate = g_pToolManager.dateTime().date().today().strftime("%Y-%m-%d")
if await cls.hasSigned(uid, signDate):
return 2
todayStr = g_pToolManager.dateTime().date().today().strftime("%Y-%m-%d")
isSupplement = 0 if signDate == todayStr else 1
expMax, expMin, pointMax, pointMin = [
g_pJsonManager.m_pSign.get(key, default)
for key, default in (
("exp_max", 50),
("exp_min", 5),
("point_max", 2000),
("point_min", 200),
)
]
exp = random.randint(expMin, expMax)
point = random.randint(pointMin, pointMax)
vipPoint = 0
async with cls._transaction():
await cls.m_pDB.execute(
"INSERT INTO userSignLog (uid, signDate, isSupplement, exp, point) VALUES (?, ?, ?, ?, ?)",
(uid, signDate, isSupplement, exp, point)
)
cursor = await cls.m_pDB.execute("SELECT * FROM userSignSummary WHERE uid=?", (uid,))
row = await cursor.fetchone()
currentMonth = signDate[:7]
if row:
monthSignDays = row['monthSignDays'] + 1 if row['currentMonth'] == currentMonth else 1
totalSignDays = row['totalSignDays']
lastDate = row['lastSignDate']
prevDate = (g_pToolManager.dateTime().strptime(signDate, "%Y-%m-%d") - timedelta(days=1)).strftime("%Y-%m-%d")
continuousDays = row['continuousDays'] + 1 if lastDate == prevDate else 1
supplementCount = row['supplementCount'] + 1 if isSupplement else row['supplementCount']
await cls.m_pDB.execute(
"""
UPDATE userSignSummary
SET totalSignDays=totalSignDays+1,
currentMonth=?,
monthSignDays=?,
lastSignDate=?,
continuousDays=?,
supplementCount=?
WHERE uid=?
""",
(currentMonth, monthSignDays, signDate, continuousDays, supplementCount, uid)
)
else:
totalSignDays = 1
await cls.m_pDB.execute(
"""
INSERT INTO userSignSummary
(uid, totalSignDays, currentMonth, monthSignDays, lastSignDate, continuousDays, supplementCount)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(uid, 1, currentMonth, 1, signDate, 1, 1 if isSupplement else 0)
)
#计算累签奖励
reward = g_pJsonManager.m_pSign['continuou'].get(f"{totalSignDays}", None)
if reward:
point += reward.get('point', 0)
exp += reward.get('exp', 0)
vipPoint = reward.get('vipPoint', 0)
plant = reward.get('plant', {})
if plant:
for key, value in plant.items():
await g_pDBService.userSeed.addUserSeedByUid(uid, key, value)
if g_bIsDebug:
exp += 9999
#向数据库更新
currentExp = await g_pDBService.user.getUserExpByUid(uid)
await g_pDBService.user.updateUserExpByUid(uid, currentExp + exp)
currentPoint = await g_pDBService.user.getUserPointByUid(uid)
await g_pDBService.user.updateUserPointByUid(uid, currentPoint + point)
if vipPoint > 0:
currentVipPoint = await g_pDBService.user.getUserVipPointByUid(uid)
await g_pDBService.user.updateUserVipPointByUid(uid, currentVipPoint + vipPoint)
return 1
except Exception as e:
logger.warning("执行签到失败", e=e)
return 0
@classmethod
async def drawSignCalendarImage(cls, uid: str, year: int, month: int):
#绘制签到图,自动提取数据库中该用户该月的签到天数
cellSize = 80
padding = 40
titleHeight = 80
cols = 7
rows = 6
width = cellSize * cols + padding * 2
height = cellSize * rows + padding * 2 + titleHeight
img = BuildImage(width, height, color=(255, 255, 255))
await img.text((padding, 20), f"{year}年{month}月签到表", font_size=36)
firstWeekday, totalDays = calendar.monthrange(year, month)
monthStr = f"{year:04d}-{month:02d}"
try:
sql = "SELECT signDate FROM userSignLog WHERE uid=? AND signDate LIKE ?"
async with cls.m_pDB.execute(sql, (uid, f"{monthStr}-%")) as cursor:
rows = await cursor.fetchall()
signedDays = set(int(r[0][-2:]) for r in rows if r[0][-2:].isdigit())
except Exception as e:
logger.warning("绘制签到图时数据库查询失败", e=e)
signedDays = set()
for day in range(1, totalDays + 1):
index = day + firstWeekday - 1
row = index // cols
col = index % cols
x1 = padding + col * cellSize
y1 = padding + titleHeight + row * cellSize
x2 = x1 + cellSize - 10
y2 = y1 + cellSize - 10
color = (112, 196, 112) if day in signedDays else (220, 220, 220)
await img.rectangle((x1, y1, x2, y2), fill=color, outline="black", width=2)
await img.text((x1 + 10, y1 + 10), str(day), font_size=24)
return img
+453
View File
@@ -0,0 +1,453 @@
from typing import Optional
from zhenxun.services.log import logger
from ..config import g_bIsDebug
from ..dbService import g_pDBService
from ..json import g_pJsonManager
from ..tool import g_pToolManager
from .database import CSqlManager
class CUserSoilDB(CSqlManager):
@classmethod
async def initDB(cls):
userSoil = {
"uid": "TEXT NOT NULL",
"soilIndex": "INTEGER NOT NULL", #地块索引从1开始
"plantName": "TEXT DEFAULT ''", #作物名称
"plantTime": "INTEGER DEFAULT 0", #播种时间
"matureTime": "INTEGER DEFAULT 0", #成熟时间
"soilLevel": "INTEGER DEFAULT 0", #土地等级 0=普通地,1=红土地,2=黑土地,3=金土地
"wiltStatus": "INTEGER DEFAULT 0", #枯萎状态 0=未枯萎,1=枯萎
"fertilizerStatus": "INTEGER DEFAULT 0",#施肥状态 0=未施肥,1=施肥 2=增肥
"bugStatus": "INTEGER DEFAULT 0", #虫害状态 0=无虫害,1=有虫害
"weedStatus": "INTEGER DEFAULT 0", #杂草状态 0=无杂草,1=有杂草
"waterStatus": "INTEGER DEFAULT 0", #缺水状态 0=不缺水,1=缺水
"harvestCount": "INTEGER DEFAULT 0", #收获次数
"PRIMARY KEY": "(uid, soilIndex)",
}
await cls.ensureTableSchema("userSoil", userSoil)
@classmethod
async def nextPhase(cls, uid: str, soilIndex: int):
"""将指定地块的作物进入下个阶段
Args:
soilIndex (int): 地块索引 从1开始
"""
if not g_bIsDebug:
return
soilInfo = await cls.getUserSoil(uid, soilIndex)
if not soilInfo:
return
plantInfo = await g_pDBService.plant.getPlantByName(soilInfo['plantName'])
if not plantInfo:
return
currentTime = g_pToolManager.dateTime().now().timestamp()
phaseList = await g_pDBService.plant.getPlantPhaseByName(soilInfo['plantName'])
if currentTime >= soilInfo['matureTime']:
return
currentStage = 0
elapsedTime = currentTime - soilInfo['plantTime']
for idx, thr in enumerate(phaseList, start=1):
if elapsedTime < thr:
currentStage = idx
break
t = int(soilInfo['plantTime']) - phaseList[currentStage]
s = int(soilInfo['matureTime']) - phaseList[currentStage]
await cls.updateUserSoilFields(uid, soilIndex,
{
"plantTime": t,
"matureTime": s
})
logger.debug(f"当前阶段{currentStage}, 阶段时间{phaseList[currentStage]}, 播种时间{t}, 收获时间{s}")
@classmethod
async def getUserFarmByUid(cls, uid: str) -> dict:
"""获取指定用户的旧农场数据
Args:
uid (str): 用户ID
Returns:
dict: 包含字段名-值的字典; 若无数据则返回空字典
"""
cursor = await cls.m_pDB.execute("SELECT * FROM soil WHERE uid = ?", (uid,))
row = await cursor.fetchone()
if not row:
return {}
columns = [description[0] for description in cursor.description]
return dict(zip(columns, row))
@classmethod
async def migrateOldFarmData(cls) -> bool:
"""迁移旧土地数据到新表 userSoil 并删除旧表
Returns:
bool: 如果旧表不存在则返回 False,否则迁移并删除后返回 True
"""
#检查旧表是否存在
cursor = await cls.m_pDB.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='soil'"
)
if not await cursor.fetchone():
return False
async with cls._transaction():
users = await g_pDBService.user.getAllUsers()
for uid in users:
farmInfo = await cls.getUserFarmByUid(uid)
for i in range(1, 31):
key = f"soil{i}"
data = farmInfo.get(key)
if not data:
continue
parts = data.split(",")
if len(parts) < 3:
continue
name = parts[0]
pt = int(parts[1])
mt = int(parts[2])
await cls.m_pDB.execute(
"""
INSERT INTO userSoil
(uid,soilIndex,plantName,plantTime,matureTime,harvestCount)
VALUES (?,?,?,?,?,?)
""",
(uid, i, name, pt, mt, 0),
)
await cls.m_pDB.execute("DROP TABLE soil")
logger.info("数据库迁移完毕!")
return True
@classmethod
async def insertUserSoil(cls, soilInfo: dict):
"""插入一条新的 userSoil 记录
Args:
soilInfo (dict): 新土地数据
Returns:
None
"""
async with cls._transaction():
await cls.m_pDB.execute(
"""
INSERT INTO userSoil
(uid, soilIndex, plantName, plantTime, matureTime,
soilLevel, wiltStatus, fertilizerStatus, bugStatus,
weedStatus, waterStatus, harvestCount)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
""",
(
soilInfo["uid"],
soilInfo["soilIndex"],
soilInfo.get("plantName", ""),
soilInfo.get("plantTime", 0),
soilInfo.get("matureTime", 0),
soilInfo.get("soilLevel", 0),
soilInfo.get("wiltStatus", 0),
soilInfo.get("fertilizerStatus", 0),
soilInfo.get("bugStatus", 0),
soilInfo.get("weedStatus", 0),
soilInfo.get("waterStatus", 0),
soilInfo.get("harvestCount", 0),
),
)
@classmethod
async def _insertUserSoil(cls, soilInfo: dict):
"""插入一条新的 userSoil 记录
Args:
soilInfo (dict): 新土地数据
Returns:
None
"""
await cls.m_pDB.execute(
"""
INSERT INTO userSoil
(uid, soilIndex, plantName, plantTime, matureTime,
soilLevel, wiltStatus, fertilizerStatus, bugStatus,
weedStatus, waterStatus, harvestCount)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
""",
(
soilInfo["uid"],
soilInfo["soilIndex"],
soilInfo.get("plantName", ""),
soilInfo.get("plantTime", 0),
soilInfo.get("matureTime", 0),
soilInfo.get("soilLevel", 0),
soilInfo.get("wiltStatus", 0),
soilInfo.get("fertilizerStatus", 0),
soilInfo.get("bugStatus", 0),
soilInfo.get("weedStatus", 0),
soilInfo.get("waterStatus", 0),
soilInfo.get("harvestCount", 0),
),
)
@classmethod
async def getUserSoil(cls, uid: str, soilIndex: int) -> Optional[dict]:
"""获取指定用户某块土地的详细信息
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
Returns:
Optional[dict]: 记录存在返回字段-值字典,否则返回 None
"""
async with cls._transaction():
cursor = await cls.m_pDB.execute(
"SELECT * FROM userSoil WHERE uid = ? AND soilIndex = ?",
(uid, soilIndex),
)
row = await cursor.fetchone()
if not row:
return None
columns = [description[0] for description in cursor.description]
return dict(zip(columns, row))
@classmethod
async def _getUserSoil(cls, uid: str, soilIndex: int) -> Optional[dict]:
"""获取指定用户某块土地的详细信息
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
Returns:
Optional[dict]: 记录存在返回字段-值字典,否则返回 None
"""
cursor = await cls.m_pDB.execute(
"SELECT * FROM userSoil WHERE uid = ? AND soilIndex = ?",
(uid, soilIndex),
)
row = await cursor.fetchone()
if not row:
return None
columns = [description[0] for description in cursor.description]
return dict(zip(columns, row))
@classmethod
async def updateUserSoil(cls, uid: str, soilIndex: int, field: str, value):
"""更新指定用户土地的单个字段
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
field (str): 需更新的字段名
value: 新值
Returns:
None
"""
async with cls._transaction():
await cls.m_pDB.execute(
f"UPDATE userSoil SET {field} = ? WHERE uid = ? AND soilIndex = ?",
(value, uid, soilIndex),
)
@classmethod
async def _updateUserSoil(cls, uid: str, soilIndex: int, field: str, value):
"""更新指定用户土地的单个字段
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
field (str): 需更新的字段名
value: 新值
Returns:
None
"""
await cls.m_pDB.execute(
f"UPDATE userSoil SET {field} = ? WHERE uid = ? AND soilIndex = ?",
(value, uid, soilIndex),
)
@classmethod
async def updateUserSoilFields(cls, uid: str, soilIndex: int, updates: dict) -> bool:
"""批量更新指定用户土地的多个字段
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
updates (dict): 字段-新值的字典
Returns:
bool: 如果无可更新字段则返回 False,否则更新成功返回 True
"""
#允许更新的列白名单
allowedFields = {
"plantName", "plantTime", "matureTime", "soilLevel",
"wiltStatus", "fertilizerStatus", "bugStatus",
"weedStatus", "waterStatus", "harvestCount"
}
setClauses = []
values = []
for field, value in updates.items():
if field not in allowedFields:
continue
setClauses.append(f'"{field}" = ?')
values.append(value)
if not setClauses:
return False
values.extend([uid, soilIndex])
sql = f'UPDATE userSoil SET {", ".join(setClauses)} WHERE uid = ? AND soilIndex = ?'
try:
async with cls._transaction():
await cls.m_pDB.execute(sql, tuple(values))
return True
except Exception as e:
logger.error(f"批量更新土地字段失败: {e}")
return False
@classmethod
async def deleteUserSoil(cls, uid: str, soilIndex: int):
"""删除指定用户的土地记录
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
Returns:
None
"""
async with cls._transaction():
await cls.m_pDB.execute(
"DELETE FROM userSoil WHERE uid = ? AND soilIndex = ?", (uid, soilIndex)
)
@classmethod
async def _deleteUserSoil(cls, uid: str, soilIndex: int):
"""删除指定用户的土地记录
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
Returns:
None
"""
await cls.m_pDB.execute(
"DELETE FROM userSoil WHERE uid = ? AND soilIndex = ?", (uid, soilIndex)
)
@classmethod
async def isSoilPlanted(cls, uid: str, soilIndex: int) -> bool:
"""判断指定用户的指定土地是否已种植
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
Returns:
bool: 如果 plantName 不为空且 plantTime 大于 0,则视为已种植,返回 True;否则 False
"""
soilInfo = await cls.getUserSoil(uid, soilIndex)
if not soilInfo:
return False
return bool(soilInfo.get("plantName")) and soilInfo.get("plantTime", 0) > 0
@classmethod
async def sowingByPlantName(cls, uid: str, soilIndex: int, plantName: str) -> bool:
"""播种指定作物到用户土地区
Args:
uid (str): 用户ID
soilIndex (int): 土地区索引
plantName (str): 植物名
Returns:
bool: 播种成功返回 True,否则返回 False
"""
#校验土地区是否已种植
soilRecord = await cls.getUserSoil(uid, soilIndex)
if soilRecord and soilRecord.get("plantName"):
return False
#获取植物配置
plantCfg = await g_pDBService.plant.getPlantByName(plantName)
if not plantCfg:
logger.error(f"未知植物: {plantName}")
return False
nowTs = int(g_pToolManager.dateTime().now().timestamp())
matureTs = nowTs + int(plantCfg.get("time", 0)) * 3600
try:
async with cls._transaction():
prev = soilRecord or {}
await cls._deleteUserSoil(uid, soilIndex)
await cls._insertUserSoil(
{
"uid": uid,
"soilIndex": soilIndex,
"plantName": plantName,
"plantTime": nowTs,
"matureTime": matureTs,
"soilLevel": prev.get("soilLevel", 0),
"wiltStatus": prev.get("wiltStatus", 0),
"fertilizerStatus": prev.get("fertilizerStatus", 0),
"bugStatus": prev.get("bugStatus", 0),
"weedStatus": prev.get("weedStatus", 0),
"waterStatus": prev.get("waterStatus", 0),
"harvestCount": 0
}
)
return True
except Exception as e:
logger.error(f"播种失败!", e=e)
return False
@classmethod
async def getUserSoilStatus(cls, uid: str, soilIndex: int) -> str:
status = []
soilInfo = await g_pDBService.userSoil.getUserSoil(uid, soilIndex)
if not soilInfo:
return ""
if soilInfo.get("wiltStatus", 0) == 1:
return "枯萎"
if soilInfo.get("fertilizerStatus", 0) == 1:
status.append("施肥")
elif soilInfo.get("fertilizerStatus", 0) == 2:
status.append("增肥")
if soilInfo.get("bugStatus", 0) == 1:
status.append("虫害")
if soilInfo.get("weedStatus", 0) == 1:
status.append("杂草")
if soilInfo.get("waterStatus", 0) == 1:
status.append("缺水")
return ",".join(status)
+222
View File
@@ -0,0 +1,222 @@
from zhenxun.services.log import logger
from .database import CSqlManager
class CUserStealDB(CSqlManager):
@classmethod
async def initDB(cls):
userSteal = {
"uid": "TEXT NOT NULL", #被偷用户Uid
"soilIndex": "INTEGER NOT NULL", #被偷的地块索引 从1开始
"stealerUid": "TEXT NOT NULL", #偷菜用户Uid
"stealCount": "INTEGER NOT NULL", #被偷数量
"stealTime": "INTEGER NOT NULL", #被偷时间
"PRIMARY KEY": "(uid, soilIndex, stealerUid)"
}
await cls.ensureTableSchema("userSteal", userSteal)
@classmethod
async def addStealRecord(cls, uid: str, soilIndex: int, stealerUid: str, stealCount: int, stealTime: int) -> bool:
"""添加偷菜记录
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
stealerUid (str): 偷菜用户Uid
stealCount (int): 被偷数量
stealTime (int): 被偷时间(时间戳)
Returns:
bool: 操作是否成功
"""
try:
async with cls._transaction():
await cls.m_pDB.execute(
'INSERT INTO "userSteal"(uid, soilIndex, stealerUid, stealCount, stealTime) VALUES(?, ?, ?, ?, ?);',
(uid, soilIndex, stealerUid, stealCount, stealTime)
)
return True
except Exception as e:
logger.warning("添加偷菜记录失败", e=e)
return False
@classmethod
async def getStealRecordsByUid(cls, uid: str) -> list:
"""根据用户Uid获取所有偷菜记录
Args:
uid (str): 被偷用户Uid
Returns:
list: 偷菜记录字典列表,每条包含 soilIndex, stealerUid, stealCount, stealTime
"""
try:
async with cls._transaction():
cursor = await cls.m_pDB.execute(
'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
]
except Exception as e:
logger.warning("获取偷菜记录失败", e=e)
return []
@classmethod
async def getStealRecord(cls, uid: str, soilIndex: int) -> list:
"""获取指定地块的所有偷菜记录
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
Returns:
list: 偷菜记录字典列表,每条包含 stealerUid, stealCount, stealTime
"""
try:
async with cls._transaction():
cursor = await cls.m_pDB.execute(
'SELECT stealerUid, stealCount, stealTime FROM "userSteal" WHERE uid=? AND soilIndex=?;',
(uid, soilIndex)
)
rows = await cursor.fetchall()
return [
{
"uid": uid,
"soilIndex": soilIndex,
"stealerUid": row[0],
"stealCount": row[1],
"stealTime": row[2]
}
for row in rows
]
except Exception as e:
logger.warning("获取单地块偷菜记录失败", e=e)
return []
@classmethod
async def getTotalStolenCount(cls, uid: str, soilIndex: int) -> int:
"""计算指定地块被偷的总数量(所有用户偷取数量之和)
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
Returns:
int: 被偷的总数量,如果无记录则返回 0
"""
try:
async with cls._transaction():
cursor = await cls.m_pDB.execute(
'SELECT SUM(stealCount) FROM "userSteal" WHERE uid=? AND soilIndex=?;',
(uid, soilIndex)
)
row = await cursor.fetchone()
return row[0] or 0 # type: ignore
except Exception as e:
logger.warning("计算总偷菜数量失败", e=e)
return 0
@classmethod
async def getStealerCount(cls, uid: str, soilIndex: int) -> int:
"""计算指定地块被多少人偷过(不同偷菜用户数量)
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
Returns:
int: 偷菜者总数,如果无记录则返回 0
"""
try:
async with cls._transaction():
cursor = await cls.m_pDB.execute(
'SELECT COUNT(DISTINCT stealerUid) FROM "userSteal" WHERE uid=? AND soilIndex=?;',
(uid, soilIndex)
)
row = await cursor.fetchone()
return row[0] or 0 # type: ignore
except Exception as e:
logger.warning("计算偷菜者数量失败", e=e)
return 0
@classmethod
async def hasStealed(cls, uid: str, soilIndex: int, stealerUid: str) -> bool:
"""判断指定用户是否曾偷取过该地块
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
stealerUid (str): 偷菜用户Uid
Returns:
bool: 若存在记录返回 True,否则返回 False
"""
try:
async with cls._transaction():
cursor = await cls.m_pDB.execute(
'SELECT 1 FROM "userSteal" WHERE uid=? AND soilIndex=? AND stealerUid=? LIMIT 1;',
(uid, soilIndex, stealerUid)
)
row = await cursor.fetchone()
return bool(row)
except Exception as e:
logger.warning("检查偷菜记录失败", e=e)
return False
@classmethod
async def updateStealRecord(cls, uid: str, soilIndex: int, stealerUid: str, stealCount: int, stealTime: int) -> bool:
"""更新偷菜记录的数量和时间
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
stealerUid (str): 偷菜用户Uid
stealCount (int): 新的偷菜数量
stealTime (int): 新的偷菜时间(时间戳)
Returns:
bool: 操作是否成功
"""
try:
async with cls._transaction():
await cls.m_pDB.execute(
'UPDATE "userSteal" SET stealCount=?, stealTime=? WHERE uid=? AND soilIndex=? AND stealerUid=?;',
(stealCount, stealTime, uid, soilIndex, stealerUid)
)
return True
except Exception as e:
logger.warning("更新偷菜记录失败", e=e)
return False
@classmethod
async def deleteStealRecord(cls, uid: str, soilIndex: int) -> bool:
"""删除指定偷菜记录(只需被偷用户Uid和地块索引)
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
Returns:
bool: 删除是否成功
"""
try:
async with cls._transaction():
await cls.m_pDB.execute(
'DELETE FROM "userSteal" WHERE uid=? AND soilIndex=?;',
(uid, soilIndex)
)
return True
except Exception as e:
logger.warning("删除偷菜记录失败", e=e)
return False
+47
View File
@@ -0,0 +1,47 @@
from typing import Optional
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()
+103
View File
@@ -0,0 +1,103 @@
import asyncio
import time
from zhenxun.services.log import logger
class Signal:
def __init__(self):
self._slots = [] #绑定的槽函数列表
self._onceSlots = [] #只触发一次的槽函数列表
def connect(self, slot, priority=0):
if callable(slot) and not any(s[0] == slot for s in self._slots):
self._slots.append((slot, priority))
self._slots.sort(key=lambda x: -x[1])
def connectOnce(self, slot, priority=0):
if callable(slot) and not any(s[0] == slot for s in self._onceSlots):
self._onceSlots.append((slot, priority))
self._onceSlots.sort(key=lambda x: -x[1])
def disconnect(self, slot):
self._slots = [s for s in self._slots if s[0] != slot]
self._onceSlots = [s for s in self._onceSlots if s[0] != slot]
async def emit(self, *args, **kwargs):
slots = list(self._slots)
onceSlots = list(self._onceSlots)
self._onceSlots.clear()
for slot, _ in slots + onceSlots:
startTime = time.time()
try:
if asyncio.iscoroutinefunction(slot):
await slot(*args, **kwargs)
else:
slot(*args, **kwargs)
duration = (time.time() - startTime) * 1000
logger.debug(f"事件槽 {slot.__name__} 执行完成,耗时 {duration:.2f} ms")
except Exception as e:
logger.warning(f"事件槽 {slot.__name__} 触发异常: {e}")
class FarmEventManager:
def __init__(self):
self.m_beforePlant = Signal()
"""播种前信号
Args:
uid (str): 用户Uid
name (str): 播种种子名称
num (int): 播种数量
"""
self.m_afterPlant = Signal()
"""播种后信号 每块地播种都会触发该信号
Args:
uid (str): 用户Uid
name (str): 播种种子名称
soilIndex (int): 播种地块索引 从1开始
"""
self.m_beforeHarvest = Signal()
"""收获前信号
Args:
uid (str): 用户Uid
"""
self.m_afterHarvest = Signal()
"""收获后信号 每块地收获都会触发该信号
Args:
uid (str): 用户Uid
name (str): 收获作物名称
num (int): 收获数量
soilIndex (int): 收获地块索引 从1开始
"""
self.m_beforeEradicate = Signal()
"""铲除前信号
Args:
uid (str): 用户Uid
"""
self.m_afterEradicate = Signal()
"""铲除后信号 每块地铲除都会触发该信号
Args:
uid (str): 用户Uid
soilIndex (index): 铲除地块索引 从1开始
"""
self.m_beforeExpand = Signal()
self.m_afterExpand = Signal()
self.m_beforeSteal = Signal()
self.m_afterSteal = Signal()
g_pEventManager = FarmEventManager()
+343 -217
View File
@@ -1,12 +1,8 @@
import asyncio
import math
import random
from datetime import date, datetime
from io import StringIO
from typing import Dict, List, Tuple
from numpy import number
from zhenxun.configs.config import Config
from zhenxun.models.user_console import UserConsole
from zhenxun.services.log import logger
@@ -15,8 +11,11 @@ from zhenxun.utils.enum import GoldHandle
from zhenxun.utils.image_utils import ImageTemplate
from zhenxun.utils.platform import PlatformUtils
from ..config import g_pJsonManager, g_sResourcePath
from ..database import g_pSqlManager
from ..config import g_bIsDebug, g_sResourcePath
from ..dbService import g_pDBService
from ..event.event import g_pEventManager
from ..json import g_pJsonManager
from ..tool import g_pToolManager
class CFarmManager:
@@ -38,15 +37,15 @@ class CFarmManager:
if user.gold < deduction:
return f"你的金币不足或不足承担手续费。当前手续费为{fee}"
await UserConsole.reduce_gold(uid, num, GoldHandle.PLUGIN , 'zhenxun_plugin_farm')
await UserConsole.reduce_gold(uid, fee, GoldHandle.PLUGIN , 'zhenxun_plugin_farm')
await UserConsole.reduce_gold(uid, num, GoldHandle.PLUGIN , 'zhenxun_plugin_farm') # type: ignore
await UserConsole.reduce_gold(uid, fee, GoldHandle.PLUGIN , 'zhenxun_plugin_farm') # type: ignore
point = num * pro
p = await g_pSqlManager.getUserPointByUid(uid)
p = await g_pDBService.user.getUserPointByUid(uid)
number = point + p
await g_pSqlManager.updateUserPointByUid(uid, int(number))
await g_pDBService.user.updateUserPointByUid(uid, int(number))
return f"充值{point}农场币成功,手续费{tax}金币,当前农场币:{number}"
@@ -73,7 +72,7 @@ class CFarmManager:
soilPos = g_pJsonManager.m_pSoil['soil']
userInfo = await g_pSqlManager.getUserInfoByUid(uid)
userInfo = await g_pDBService.user.getUserInfoByUid(uid)
soilUnlock = int(userInfo['soil'])
x = 0
@@ -89,11 +88,11 @@ class CFarmManager:
if index < soilUnlock:
await img.paste(soil, (x, y))
isPlant, plant, isRipe= await cls.drawSoilPlant(uid, f"soil{str(index + 1)}")
isPlant, plant, isRipe, offsetX, offsetY = await cls.drawSoilPlant(uid, index + 1)
if isPlant:
await img.paste(plant, (x + soilSize[0] // 2 - plant.width // 2,
y + soilSize[1] // 2 - plant.height // 2))
await img.paste(plant, (x + soilSize[0] // 2 - plant.width // 2 + offsetX,
y + soilSize[1] // 2 - plant.height // 2 + offsetY))
#1700 275
#首次添加可收获图片
@@ -134,7 +133,7 @@ class CFarmManager:
await img.paste(nameImg, (300, 92))
#经验值
level = await g_pSqlManager.getUserLevelByUid(uid)
level = await g_pDBService.user.getUserLevelByUid(uid)
beginX = 309
endX = 627
@@ -171,59 +170,163 @@ class CFarmManager:
return img.pic2bytes()
@classmethod
async def drawSoilPlant(cls, uid: str, soilid: str) -> tuple[bool, BuildImage, bool]:
async def drawDetailFarmByUid(cls, uid: str) -> list:
info = []
farm = await cls.drawFarmByUid(uid)
info.append(BuildImage.open(farm))
dataList = []
columnName = [
"-",
"土地ID",
"作物名称",
"成熟时间",
"土地状态",
"被偷数量",
"剩余产出",
]
icon = ""
soilNumber = await g_pDBService.user.getUserSoilByUid(uid)
for i in range(1, soilNumber + 1):
soilInfo = await g_pDBService.userSoil.getUserSoil(uid, i)
if soilInfo:
if soilInfo["soilLevel"] == 1:
iconPath = g_sResourcePath / f"soil/TODO.png"
else:
iconPath = g_sResourcePath / f"soil/普通土地.png"
if iconPath.exists():
icon = (iconPath, 33, 33)
plantName = soilInfo.get("plantName", "-")
if plantName == "-":
matureTime = "-"
soilStatus = "-"
totalNumber = "-"
plantNumber = "-"
else:
matureTime = g_pToolManager.dateTime().fromtimestamp(int(soilInfo.get("matureTime", 0))).strftime("%Y-%m-%d %H:%M:%S")
soilStatus = await g_pDBService.userSoil.getUserSoilStatus(uid, i)
totalNumber = await g_pDBService.userSteal.getTotalStolenCount(uid, i)
planInfo = await g_pDBService.plant.getPlantByName(plantName)
if not planInfo:
plantNumber = f"None"
else:
plantNumber = f"{planInfo['harvest'] - totalNumber}"
dataList.append(
[
icon,
i,
plantName,
matureTime,
soilStatus,
totalNumber,
plantNumber,
])
if len(dataList) >= 15:
result = await ImageTemplate.table_page(
"土地详细信息",
"",
columnName,
dataList,
)
info.append(result.copy())
dataList.clear()
if i >= soilNumber:
result = await ImageTemplate.table_page(
"土地详细信息",
"",
columnName,
dataList,
)
info.append(result.copy())
dataList.clear()
return info
@classmethod
async def drawSoilPlant(cls, uid: str, soilIndex: int) -> tuple[bool, BuildImage, bool, int, int]:
"""绘制植物资源
Args:
uid (str): 用户Uid
soilid (str): 土地id
soilIndex (int): 土地索引 从1开始
Returns:
tuple[bool, BuildImage]: [绘制是否成功,资源图片, 是否成熟]
"""
plant = None
soilStatus, soilInfo = await g_pSqlManager.getUserSoilStatusBySoilID(uid, soilid)
soilInfo = await g_pDBService.userSoil.getUserSoil(uid, soilIndex)
if soilStatus == True:
return False, None, False
if not soilInfo:
return False, None, False, 0, 0 #type: ignore
#是否枯萎
if int(soilInfo.get("wiltStatus", 0)) == 1:
plant = BuildImage(background = g_sResourcePath / f"plant/basic/9.png")
await plant.resize(0, 150, 212)
return True, plant, False, 0, 0
#获取作物详细信息
plantInfo = await g_pDBService.plant.getPlantByName(soilInfo['plantName'])
if not plantInfo:
logger.error(f"绘制植物资源失败: {soilInfo['plantName']}")
return False, None, False, 0, 0 #type: ignore
offsetX = plantInfo.get('officX', 0)
offsetY = plantInfo.get('officY', 0)
offsetW = plantInfo.get('officW', 0)
offsetH = plantInfo.get('officH', 0)
currentTime = g_pToolManager.dateTime().now().timestamp()
phaseList = await g_pDBService.plant.getPlantPhaseByName(soilInfo['plantName'])
#如果当前时间大于成熟时间 说明作物成熟
if currentTime >= soilInfo['matureTime']:
plant = BuildImage(background = g_sResourcePath / f"plant/{soilInfo['plantName']}/{len(phaseList)}.png")
return True, plant, True, offsetX, offsetY
else:
soilInfo = soilInfo.split(',')
#如果是多阶段作物 且没有成熟 #早期思路 多阶段作物 直接是倒数第二阶段图片
# if soilInfo['harvestCount'] >= 1:
# plant = BuildImage(background = g_sResourcePath / f"plant/{soilInfo['plantName']}/{plantInfo['phase'] - 1s}.png")
if int(soilInfo[3]) == 4:
plant = BuildImage(background = g_sResourcePath / f"plant/basic/9.png")
await plant.resize(0, 150, 212)
return True, plant, False
# return True, plant, False, offsetX, offsetY
plantInfo = g_pJsonManager.m_pPlant['plant'][soilInfo[0]]
#如果没有成熟 则根据当前阶段进行绘制
currentStage = 0
elapsedTime = currentTime - soilInfo['plantTime']
currentTime = datetime.now()
matureTime = datetime.fromtimestamp(int(soilInfo[2]))
for idx, thr in enumerate(phaseList, start=1):
if elapsedTime < thr:
currentStage = idx
break
if currentTime >= matureTime:
phase = int(plantInfo['phase'])
plant = BuildImage(background = g_sResourcePath / f"plant/{soilInfo[0]}/{phase - 1}.png")
return True, plant, True
else:
plantedTime = datetime.fromtimestamp(int(soilInfo[1]))
elapsedTime = currentTime - plantedTime
elapsedHour = elapsedTime.total_seconds() / 60 / 60
currentStage = int(elapsedHour / (plantInfo['time'] / (plantInfo['phase'] - 1)))
if currentStage <= 0:
if plantInfo['general'] == False:
plant = BuildImage(background = g_sResourcePath / f"plant/{soilInfo[0]}/0.png")
else:
plant = BuildImage(background = g_sResourcePath / f"plant/basic/0.png")
await plant.resize(0, 35, 58)
if currentStage <= 0:
if plantInfo['general'] == False:
plant = BuildImage(background = g_sResourcePath / f"plant/{soilInfo['plantName']}/0.png")
else:
plant = BuildImage(background = g_sResourcePath / f"plant/{soilInfo[0]}/{currentStage}.png")
plant = BuildImage(background = g_sResourcePath / f"plant/basic/0.png")
return True, plant, False
await plant.resize(0, 35 + offsetW, 58 + offsetH)
else:
plant = BuildImage(background = g_sResourcePath / f"plant/{soilInfo['plantName']}/{currentStage}.png")
return True, plant, False, offsetX, offsetY
@classmethod
async def getUserSeedByUid(cls, uid: str) -> bytes:
@@ -237,12 +340,11 @@ class CFarmManager:
"收获数量",
"成熟时间(小时)",
"收获次数",
"再次成熟时间(小时)",
"是否可以上架交易行"
]
# 从数据库获取结构化数据
seedRecords = await g_pSqlManager.getUserSeedByUid(uid) or {}
#从数据库获取结构化数据
seedRecords = await g_pDBService.userSeed.getUserSeedByUid(uid) or {}
if not seedRecords:
result = await ImageTemplate.table_page(
@@ -255,10 +357,13 @@ class CFarmManager:
for seedName, count in seedRecords.items():
try:
plantInfo = g_pJsonManager.m_pPlant['plant'][seedName]
plantInfo = await g_pDBService.plant.getPlantByName(seedName)
if not plantInfo:
continue
iconPath = g_sResourcePath / f"plant/{seedName}/icon.png"
icon = (iconPath, 33, 33) if iconPath.exists() else ""
sellable = "可以" if plantInfo['again'] else "不可以"
sellable = "可以" if plantInfo['sell'] else "不可以"
dataList.append([
icon,
@@ -268,7 +373,6 @@ class CFarmManager:
plantInfo['harvest'],
plantInfo['time'],
plantInfo['crop'],
plantInfo['again'],
sellable
])
except KeyError:
@@ -295,54 +399,57 @@ class CFarmManager:
str: 返回结果
"""
try:
# 获取用户的种子数量
count = await g_pSqlManager.getUserSeedByName(uid, name)
#获取用户的种子数量
count = await g_pDBService.userSeed.getUserSeedByName(uid, name)
if count is None:
count = 0 # 如果返回 None,则视为没有种子
count = 0 #如果返回 None,则视为没有种子
if count <= 0:
return f"没有在你的仓库发现{name}种子,快去买点吧!"
# 如果播种数量超过仓库种子数量
#如果播种数量超过仓库种子数量
if count < num and num != -1:
return f"仓库中的{name}种子数量不足,当前剩余{count}个种子"
# 获取用户土地数量
soilNumber = await g_pSqlManager.getUserSoilByUid(uid)
#获取用户土地数量
soilNumber = await g_pDBService.user.getUserSoilByUid(uid)
# 如果播种数量为 -1,表示播种所有可播种的土地
#如果播种数量为 -1,表示播种所有可播种的土地
if num == -1:
num = count
# 记录是否成功播种
#发送播种前信号
await g_pEventManager.m_beforePlant.emit(uid=uid, name=name, num=num)
#记录是否成功播种
successCount = 0
for i in range(1, soilNumber + 1):
if count > 0 and num > 0:
soilName = f"soil{i}"
success, message = await g_pSqlManager.getUserSoilStatusBySoilID(uid, soilName)
success = await g_pDBService.userSoil.sowingByPlantName(uid, i, name)
if success:
# 更新种子数量
#更新种子数量
num -= 1
count -= 1
# 记录种子消耗数量
#记录种子消耗数量
successCount += 1
# 更新数据库
await g_pSqlManager.updateUserSoilStatusByPlantName(uid, soilName, name)
#发送播种后信号
await g_pEventManager.m_afterPlant.emit(uid=uid, name=name, soilIndex=i)
# 确保用户仓库数量更新
#确保用户仓库数量更新
if successCount > 0:
await g_pSqlManager.updateUserSeedByName(uid, name, count)
await g_pDBService.userSeed.updateUserSeedByName(uid, name, count)
# 根据播种结果给出反馈
#根据播种结果给出反馈
if num == 0:
return f"播种{name}成功!仓库剩余{count}个种子"
else:
return f"播种数量超出开垦土地数量,已将可播种土地成功播种{name}!仓库剩余{count}个种子"
except Exception as e:
logger.warning(f"播种操作失败: {e}")
logger.warning(f"播种操作失败!", e=e)
return "播种失败,请稍后重试!"
@classmethod
@@ -355,61 +462,85 @@ class CFarmManager:
Returns:
str: 返回
"""
try:
await g_pEventManager.m_beforeHarvest.emit(uid=uid)
isStealingPlant = 0
soilUnlock = await g_pSqlManager.getUserSoilByUid(uid)
soilNumber = await g_pDBService.user.getUserSoilByUid(uid)
soilNames = [f"soil{i + 1}" for i in range(soilUnlock)]
soilStatuses = await asyncio.gather(*[
g_pSqlManager.getUserSoilStatusBySoilID(uid, name)
for name in soilNames
])
harvestRecords = [] #收获日志记录
experience = 0 #总经验值
harvestCount = 0 #成功收获数量
harvestRecords: List[str] = []
experience = 0
for i in range(1, soilNumber + 1):
#如果没有种植
if not await g_pDBService.userSoil.isSoilPlanted(uid, i):
continue
for (soil_name, (status, info)) in zip(soilNames, soilStatuses):
if len(info) <= 0:
continue
soilInfo = await g_pDBService.userSoil.getUserSoil(uid, i)
if not soilInfo:
continue
soilInfo = info.split(',')
if int(soilInfo[3]) == 4:
continue
#如果是枯萎状态
if soilInfo.get("wiltStatus", 1) == 1:
continue
plantId = soilInfo[0]
plantInfo = g_pJsonManager.m_pPlant['plant'][plantId]
plantInfo = await g_pDBService.plant.getPlantByName(soilInfo['plantName'])
if not plantInfo:
continue
currentTime = datetime.now()
matureTime = datetime.fromtimestamp(int(soilInfo[2]))
currentTime = g_pToolManager.dateTime().now()
matureTime = g_pToolManager.dateTime().fromtimestamp(int(soilInfo['matureTime']))
if currentTime >= matureTime:
number = plantInfo['harvest']
if currentTime >= matureTime:
number = plantInfo['harvest']
#判断该土地作物是否被透过
if len(soilInfo[4]) > 0:
stealingStatus = soilInfo[4].split('|')
for isUser in stealingStatus:
user = isUser.split('-')
number -= int(user[1])
#处理偷菜扣除数量
stealNum = await g_pDBService.userSteal.getTotalStolenCount(uid, i)
isStealingPlant += 1
number -= stealNum
experience += plantInfo['experience']
harvestRecords.append(f"收获作物:{plantId},数量为:{number},经验为:{plantInfo['experience']}")
if number <= 0:
continue
#更新数据库操作
await g_pSqlManager.addUserPlantByUid(uid, plantId, number)
await g_pSqlManager.updateUserSoilStatusByPlantName(uid, soil_name, "", 4)
harvestCount += 1
experience += plantInfo['experience']
if experience > 0:
harvestRecords.append(f"\t累计获得经验:{experience}")
exp = await g_pSqlManager.getUserExpByUid(uid)
await g_pSqlManager.UpdateUserExpByUid(uid, exp + experience)
harvestRecords.append(f"收获作物:{soilInfo['plantName']},数量为:{number},经验为:{plantInfo['experience']}")
if isStealingPlant <= 0:
return "没有可收获的作物哦~ 不要试图拔苗助长"
else:
return "\n".join(harvestRecords)
await g_pDBService.userPlant.addUserPlantByUid(uid, soilInfo['plantName'], number)
#如果到达收获次数上限
if soilInfo['harvestCount'] + 1 >= plantInfo['crop']:
await g_pDBService.userSoil.updateUserSoil(uid, i, "wiltStatus", 1)
else:
phase = await g_pDBService.plant.getPlantPhaseByName(soilInfo['plantName'])
ts, hc = int(currentTime.timestamp()), soilInfo["harvestCount"] + 1
p1, p2, *rest = phase
await g_pDBService.userSoil.updateUserSoilFields(uid, i,
{
"harvestCount": hc,
"plantTime": ts - p1 - p2,
"matureTime": ts + p2 + sum(rest),
}
)
await g_pEventManager.m_afterHarvest.emit(uid=uid, name=soilInfo['plantName'], num=number, soilIndex=i)
if experience > 0:
exp = await g_pDBService.user.getUserExpByUid(uid)
await g_pDBService.user.updateUserExpByUid(uid, exp + experience)
harvestRecords.append(f"\t累计获得经验:{experience}")
if harvestCount <= 0:
return "没有可收获的作物哦~ 不要试图拔苗助长"
else:
return "\n".join(harvestRecords)
except Exception as e:
logger.warning(f"收获操作失败!", e=e)
return "收获失败,请稍后重试!"
@classmethod
async def eradicate(cls, uid: str) -> str:
@@ -421,28 +552,40 @@ class CFarmManager:
Returns:
str: 返回
"""
soilNumber = await g_pDBService.user.getUserSoilByUid(uid)
soilUnlock = await g_pSqlManager.getUserSoilByUid(uid)
soilNames = [f"soil{i + 1}" for i in range(soilUnlock)]
soilStatuses = await asyncio.gather(*[
g_pSqlManager.getUserSoilStatusBySoilID(uid, name)
for name in soilNames
])
await g_pEventManager.m_beforeEradicate.emit(uid=uid)
experience = 0
for (soil_name, (status, info)) in zip(soilNames, soilStatuses):
if info:
soilInfo = info.split(',')
if int(soilInfo[3]) == 4:
experience += 3
for i in range(1, soilNumber + 1):
#如果没有种植
if not await g_pDBService.userSoil.isSoilPlanted(uid, i):
continue
#批量更新数据库操作
await g_pSqlManager.updateUserSoilStatusByPlantName(uid, soil_name, "", 0)
soilInfo = await g_pDBService.userSoil.getUserSoil(uid, i)
if not soilInfo:
continue
#如果不是枯萎状态
if soilInfo.get("wiltStatus", 0) == 0:
continue
experience += 3
if g_bIsDebug:
experience += 999
#批量更新数据库操作
await g_pDBService.userSoil.deleteUserSoil(uid, i)
#铲除作物会将偷菜记录清空
await g_pDBService.userSteal.deleteStealRecord(uid, i)
await g_pEventManager.m_afterEradicate.emit(uid=uid, soilIndex=i)
if experience > 0:
exp = await g_pSqlManager.getUserExpByUid(uid)
await g_pSqlManager.UpdateUserExpByUid(uid, exp + experience)
exp = await g_pDBService.user.getUserExpByUid(uid)
await g_pDBService.user.updateUserExpByUid(uid, exp + experience)
return f"成功铲除荒废作物,累计获得经验:{experience}"
else:
@@ -468,7 +611,7 @@ class CFarmManager:
"是否可以上架交易行"
]
plant = await g_pSqlManager.getUserPlantByUid(uid)
plant = await g_pDBService.userPlant.getUserPlantByUid(uid)
if plant is None:
result = await ImageTemplate.table_page(
@@ -481,13 +624,16 @@ class CFarmManager:
sell = ""
for name, count in plant.items():
plantInfo = g_pJsonManager.m_pPlant['plant'][name]
plantInfo = await g_pDBService.plant.getPlantByName(name)
if not plantInfo:
continue
icon = ""
icon_path = g_sResourcePath / f"plant/{name}/icon.png"
if icon_path.exists():
icon = (icon_path, 33, 33)
if plantInfo['again'] == True:
if plantInfo['sell'] == True:
sell = "可以"
else:
sell = "不可以"
@@ -525,121 +671,104 @@ class CFarmManager:
Returns:
str: 返回
"""
#用户信息
userInfo = await g_pSqlManager.getUserInfoByUid(uid)
#用户可偷次数
userStealing = userInfo["stealing"].split('|')
userInfo = await g_pDBService.user.getUserInfoByUid(uid)
if userStealing[0] == '':
userStealing[0] = date.today().strftime('%Y-%m-%d')
userStealing.append(5)
elif date.fromisoformat(userStealing[0]) != date.today():
userStealing[0] = date.today().strftime('%Y-%m-%d')
userStealing[1] = 5
stealTime = userInfo.get('stealTime', "")
stealCount = int(userInfo['stealCount'])
if int(userStealing[1]) <= 0:
if stealTime == "" or not stealTime:
stealTime = g_pToolManager.dateTime().date().today().strftime('%Y-%m-%d')
stealCount = 5
elif g_pToolManager.dateTime().date().fromisoformat(stealTime) != g_pToolManager.dateTime().date().today():
stealTime = g_pToolManager.dateTime().date().today().strftime('%Y-%m-%d')
stealCount = 5
if stealCount <= 0:
return "你今天可偷次数到达上限啦,手下留情吧"
#获取用户解锁地块数量
soilUnlockNum = int(userInfo["soil"])
plant = {}
#根据解锁土地,获取每块土地状态信息
soilNames = [f"soil{i + 1}" for i in range(soilUnlockNum)]
soilStatuses = await asyncio.gather(*[
g_pSqlManager.getUserSoilStatusBySoilID(target, name)
for name in soilNames
])
isStealing = False
soilNumber = await g_pDBService.user.getUserSoilByUid(target)
harvestRecords: List[str] = []
isStealingNumber = 0
isStealingPlant = 0
for (soilName, (status, info)) in zip(soilNames, soilStatuses):
isStealing = False
if not info:
for i in range(1, soilNumber + 1):
#如果没有种植
if not await g_pDBService.userSoil.isSoilPlanted(target, i):
continue
soilInfo = info.split(',')
if int(soilInfo[3]) == 4:
soilInfo = await g_pDBService.userSoil.getUserSoil(target, i)
if not soilInfo:
continue
#如果是枯萎状态
if soilInfo.get("wiltStatus", 1) == 1:
continue
#作物ID
plantId = soilInfo[0]
#作物信息
plantInfo = g_pJsonManager.m_pPlant['plant'][plantId]
plantInfo = await g_pDBService.plant.getPlantByName(soilInfo['plantName'])
if not plantInfo:
continue
currentTime = datetime.now()
matureTime = datetime.fromtimestamp(int(soilInfo[2]))
currentTime = g_pToolManager.dateTime().now()
matureTime = g_pToolManager.dateTime().fromtimestamp(int(soilInfo['matureTime']))
#偷窃状态
stealingStatus: list[str] = []
#偷窃数量
stealingNumber = 0
if currentTime >= matureTime:
if soilInfo[4]:
#先获取用户是否偷过该土地
stealingStatus = soilInfo[4].split('|')
for isUser in stealingStatus:
user = isUser.split('-')
if user[0] == uid:
isStealing = True
isStealingNumber += 1
break
stealingNumber -= int(user[1])
#如果偷过,则跳过该土地
if isStealing:
if await g_pDBService.userSteal.hasStealed(target, i, uid):
continue
stealingNumber += plantInfo['harvest']
stealingNumber = plantInfo['harvest'] - await g_pDBService.userSteal.getTotalStolenCount(target, i)
randomNumber = random.choice([1, 2])
randomNumber = min(randomNumber, stealingNumber)
logger.info(f"{randomNumber}")
if randomNumber > 0:
await g_pSqlManager.addUserPlantByUid(uid, plantId, randomNumber)
await g_pDBService.userPlant.addUserPlantByUid(uid, soilInfo['plantName'], randomNumber)
harvestRecords.append(f"成功偷到作物:{plantId},数量为:{randomNumber}")
stealingStatus.append(f"{uid}-{randomNumber}")
harvestRecords.append(f"成功偷到作物:{soilInfo['plantName']},数量为:{randomNumber}")
isStealingPlant += 1
#如果将作物偷完,就直接更新状态 并记录用户偷取过
if plantInfo['harvest'] - randomNumber + stealingNumber == 0:
sql = f"UPDATE soil SET {soilName} = ',,,4,{'|'.join(stealingStatus)}' WHERE uid = '{target}'"
else:
sql = f"UPDATE soil SET {soilName} = '{soilInfo[0]},{soilInfo[1]},{soilInfo[2]},{soilInfo[3]},{'|'.join(stealingStatus)}' WHERE uid = '{target}'"
#如果作物 是最后一阶段作物且偷完 则直接枯萎
if soilInfo['harvestCount'] + 1 >= plantInfo['crop']:
await g_pDBService.userSoil.updateUserSoil(target, i, "wiltStatus", 1)
else:
phase = await g_pDBService.plant.getPlantPhaseByName(soilInfo['plantName'])
await g_pSqlManager.executeDB(sql)
ts, hc = int(currentTime.timestamp()), soilInfo["harvestCount"] + 1
p1, p2, *rest = phase
await g_pDBService.userSoil.updateUserSoilFields(uid, i,
{
"harvestCount": hc,
"plantTime": ts - p1 - p2,
"matureTime": ts + p2 + sum(rest),
}
)
await g_pDBService.userSteal.addStealRecord(target, i, uid, randomNumber, int(g_pToolManager.dateTime().now().timestamp()))
else:
await g_pDBService.userSteal.addStealRecord(target, i, uid, randomNumber, int(g_pToolManager.dateTime().now().timestamp()))
if isStealingPlant <= 0 and isStealingNumber <= 0:
return "目标没有作物可以被偷"
elif isStealingPlant <= 0 and isStealingNumber > 0:
return "你已经偷过目标啦,请手下留情"
else:
userStealing[1] = int(userStealing[1]) - 1
# 转换所有元素为字符串
userStealing_str = [str(item) for item in userStealing]
stealCount -= 1
sql = f"UPDATE user SET stealing = '{userStealing[0]}|{userStealing[1]}' WHERE uid = {uid}"
#更新用户每日偷取次数
await g_pSqlManager.executeDB(sql)
await g_pDBService.user.updateStealCountByUid(uid, stealCount)
return "\n".join(harvestRecords)
@classmethod
async def reclamationCondition(cls, uid: str) -> str:
userInfo = await g_pSqlManager.getUserInfoByUid(uid)
rec = g_pJsonManager.m_pLevel["reclamation"]
userInfo = await g_pDBService.user.getUserInfoByUid(uid)
rec = g_pJsonManager.m_pLevel['reclamation']
try:
if userInfo['soil'] >= 30:
@@ -653,20 +782,20 @@ class CFarmManager:
str = ""
if len(item) == 0:
str = f"下次升级所需条件:等级:{level},农场币:{point}"
str = f"下次开垦所需条件:等级:{level},农场币:{point}"
else:
str = f"下次升级所需条件:等级:{level},农场币:{point},物品:{item}"
str = f"下次开垦所需条件:等级:{level},农场币:{point},物品:{item}"
return str
except Exception as e:
return "获取升级土地条件失败!"
return "获取开垦土地条件失败!"
@classmethod
async def reclamation(cls, uid: str) -> str:
userInfo = await g_pSqlManager.getUserInfoByUid(uid)
level = await g_pSqlManager.getUserLevelByUid(uid)
userInfo = await g_pDBService.user.getUserInfoByUid(uid)
level = await g_pDBService.user.getUserLevelByUid(uid)
rec = g_pJsonManager.m_pLevel["reclamation"]
rec = g_pJsonManager.m_pLevel['reclamation']
try:
if userInfo['soil'] >= 30:
@@ -678,18 +807,15 @@ class CFarmManager:
point = rec['point']
item = rec['item']
logger.info(f"{level[0]}")
if level[0] < levelFileter:
return f"当前用户等级{level[0]},升级所需等级为{levelFileter}"
if userInfo['point'] < point:
return f"当前用户农场币不足,升级所需农场币为{point}"
#TODO 缺少判断需要的Item
sql = f"UPDATE user SET point = '{userInfo['point'] - point}', soil = '{userInfo['soil'] + 1}' WHERE uid = {uid}"
await g_pSqlManager.executeDB(sql)
#TODO 缺少判断消耗的item
await g_pDBService.user.updateUserPointByUid(uid, userInfo['point'] - point)
await g_pDBService.user.updateUserSoilByUid(uid, userInfo['soil'] + 1)
return "开垦土地成功!"
except Exception as e:
+91 -57
View File
@@ -1,26 +1,40 @@
import math
from re import I
import plistlib
from itertools import islice
from zhenxun.services.log import logger
from zhenxun.utils._build_image import BuildImage
from zhenxun.utils.image_utils import ImageTemplate
from ..config import g_pJsonManager, g_sResourcePath
from ..database import g_pSqlManager
from ..config import g_sResourcePath
from ..dbService import g_pDBService
from ..json import g_pJsonManager
class CShopManager:
@classmethod
async def getSeedShopImage(cls, num: int = 1) -> bytes:
async def getSeedShopImage(cls, filterKey: str|int = 1, num: int = 1) -> bytes:
"""获取商店页面
Args:
filterKey (str|int):
- 字符串: 根据关键字筛选种子名称
- 整数: 翻至对应页(无筛选)
num (int, optional): 当 filterKey 为字符串时,用于指定页码。Defaults to 1.
Returns:
bytes: 返回商店图片bytes
"""
# 解析参数:区分筛选关键字和页码
filterStr = None
if isinstance(filterKey, int):
page = filterKey
else:
filterStr = filterKey
page = num
data_list = []
column_name = [
# 表头定义
columnName = [
"-",
"种子名称",
"种子单价",
@@ -30,55 +44,65 @@ class CShopManager:
"收获数量",
"成熟时间(小时)",
"收获次数",
"再次成熟时间(小时)",
"是否可以上架交易行"
]
sell = ""
plants = list(g_pJsonManager.m_pPlant['plant'].items())
start = (num - 1) * 15
maxItems = min(len(plants) - start, 15)
items = plants[start:start + maxItems]
# 查询所有可购买作物,并根据筛选关键字过滤
plants = await g_pDBService.plant.listPlants()
filteredPlants = []
for plant in plants:
# 跳过未解锁购买的种子
if plant['isBuy'] == 0:
continue
# 字符串筛选
if filterStr and filterStr not in plant['name']:
continue
filteredPlants.append(plant)
for key, plant in items:
# 计算分页
totalCount = len(filteredPlants)
pageCount = math.ceil(totalCount / 15) if totalCount else 1
startIndex = (page - 1) * 15
pageItems = filteredPlants[startIndex: startIndex + 15]
# 构建数据行
dataList = []
for plant in pageItems:
# 图标处理
icon = ""
icon_path = g_sResourcePath / f"plant/{key}/icon.png"
if icon_path.exists():
icon = (icon_path, 33, 33)
iconPath = g_sResourcePath / f"plant/{plant['name']}/icon.png"
if iconPath.exists():
icon = (iconPath, 33, 33)
if plant['again'] == True:
sell = "可以"
else:
sell = "不可以"
# 交易行标记
sell = "可以" if plant['sell'] else "不可以"
data_list.append(
[
icon,
key,
plant['buy'],
plant['level'],
plant['price'],
plant['experience'],
plant['harvest'],
plant['time'],
plant['crop'],
plant['again'],
sell
]
)
dataList.append([
icon,
plant['name'], # 种子名称
plant['buy'], # 种子单价
plant['level'], # 解锁等级
plant['price'], # 果实单价
plant['experience'], # 收获经验
plant['harvest'], # 收获数量
plant['time'], # 成熟时间(小时)
plant['crop'], # 收获次数
sell # 是否可上架交易行
])
count = math.ceil(len(g_pJsonManager.m_pPlant['plant']) / 15)
title = f"种子商店 页数: {num}/{count}"
# 页码标题
title = f"种子商店 页数: {page}/{pageCount}"
# 渲染表格并返回图片bytes
result = await ImageTemplate.table_page(
title,
"购买示例:@小真寻 购买种子 大白菜 5",
column_name,
data_list,
columnName,
dataList,
)
return result.pic2bytes()
@classmethod
async def buySeed(cls, uid: str, name: str, num: int = 1) -> str:
"""购买种子
@@ -95,19 +119,16 @@ class CShopManager:
if num <= 0:
return "请输入购买数量!"
plantInfo = None
try:
plantInfo = g_pJsonManager.m_pPlant['plant'][name]
except Exception as e:
plantInfo = await g_pDBService.plant.getPlantByName(name)
if not plantInfo:
return "购买出错!请检查需购买的种子名称!"
level = await g_pSqlManager.getUserLevelByUid(uid)
level = await g_pDBService.user.getUserLevelByUid(uid)
if level[0] < int(plantInfo['level']):
return "你的等级不够哦,努努力吧"
point = await g_pSqlManager.getUserPointByUid(uid)
point = await g_pDBService.user.getUserPointByUid(uid)
total = int(plantInfo['buy']) * num
logger.debug(f"用户:{uid}购买{name},数量为{num}。用户农场币为{point},购买需要{total}")
@@ -115,9 +136,9 @@ class CShopManager:
if point < total:
return "你的农场币不够哦~ 快速速氪金吧!"
else:
await g_pSqlManager.updateUserPointByUid(uid, point - total)
await g_pDBService.user.updateUserPointByUid(uid, point - total)
if await g_pSqlManager.addUserSeedByUid(uid, name, num) == False:
if not await g_pDBService.userSeed.addUserSeedByUid(uid, name, num):
return "购买失败,执行数据库错误!"
return f"成功购买{name},花费{total}农场币, 剩余{point - total}农场币"
@@ -135,7 +156,7 @@ class CShopManager:
if not isinstance(name, str) or name.strip() == "":
name = ""
plant = await g_pSqlManager.getUserPlantByUid(uid)
plant = await g_pDBService.userPlant.getUserPlantByUid(uid)
if not plant:
return "你仓库没有可以出售的作物"
@@ -145,9 +166,12 @@ class CShopManager:
if name == "":
for plantName, count in plant.items():
plantInfo = g_pJsonManager.m_pPlant['plant'][plantName]
plantInfo = await g_pDBService.plant.getPlantByName(plantName)
if not plantInfo:
continue
point += plantInfo['price'] * count
await g_pSqlManager.updateUserPlantByName(uid, plantName, 0)
await g_pDBService.userPlant.updateUserPlantByName(uid, plantName, 0)
else:
if name not in plant:
return f"出售作物{name}出错:仓库中不存在该作物"
@@ -155,12 +179,22 @@ class CShopManager:
sellAmount = available if isAll else min(available, num)
if sellAmount <= 0:
return f"出售作物{name}出错:数量不足"
await g_pSqlManager.updateUserPlantByName(uid, name, available - sellAmount)
await g_pDBService.userPlant.updateUserPlantByName(uid, name, available - sellAmount)
totalSold = sellAmount
totalPoint = point if name == "" else totalSold * g_pJsonManager.m_pPlant['plant'][name]['price']
currentPoint = await g_pSqlManager.getUserPointByUid(uid)
await g_pSqlManager.updateUserPointByUid(uid, currentPoint + totalPoint)
if name == "":
totalPoint = point
else:
plantInfo = await g_pDBService.plant.getPlantByName(name)
if not plantInfo:
price = 0
else:
price = plantInfo['price']
totalPoint = totalSold * price
currentPoint = await g_pDBService.user.getUserPointByUid(uid)
await g_pDBService.user.updateUserPointByUid(uid, currentPoint + totalPoint)
if name == "":
return f"成功出售所有作物,获得农场币:{totalPoint},当前农场币:{currentPoint + totalPoint}"
+101
View File
@@ -0,0 +1,101 @@
import json
from pathlib import Path
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:
return await self.initSign()
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()
+45 -3
View File
@@ -1,15 +1,57 @@
# 真寻农场更新日志
## V1.1
## V1.4
用户方面
---
- 新增种子商店筛选功能(如果没有BUG的话后续我的种子、我的作物等也会加入筛选功能
- 新增签到功能(测试
- 新增农场详述功能,能通过该功能更加详细的观看农场数据
- 修复了迁移旧数据库无法正常迁移的BUG
- 修复了偷菜会导致偷自己的BUG
- 修正了作物阶段绘制不正确的BUG
- 感谢[quanquan1014](https://github.com/quanquan1014)对农场名称中包含特殊字符的处理。
- 更正数据库写法,但是会导致V1.0用户的作物和种子丢失
代码方面
---
- 修正了多阶段作物成长素材计算逻辑
- 修正了作物数据库字段错乱的问题
- 作物新增offset字段,用于以后偏移坐标和大小(尚未启用,该模式有商议
## V1.3
用户方面
---
- 新增部分作物
- 修复一个严重BUG, 该BUG会导致无法重复偷别人菜的问题
代码方面
---
- 将作物数据从plant.json迁移至resource/db/plant.db中,并将操作json改为操作数据库
## V1.2
用户方面
---
- 修复重大BUG,该BUG会导致用户经验等级计算异常
- 修正种子价格,现在不会以单价的形式购买种子了,而是新增的种子价格
代码方面
---
- 彻底重构数据库,但是不会像V1.1一样导致用户数据丢失
- 迁移部分代码结构,使文件名和代码功能更加匹配
- 加入事件机制,采用类似Qt信号槽机制
## V1.1
用户方面
---
- 完善我的农场图片资源,现在会在左上角显示经验、等级、金币等详细信息了
- 完善出售作物逻辑,现在可以空置作物名称来一键出售全部作物了,也可以选择空置数量来一键出售仓库种指定作物
- 完善播种逻辑,现在可以空置数量来一键播种指定作物了
- 新增更改农场名指令
- 改进对土地开垦条件判断
代码方面
---
- 感谢[quanquan1014](https://github.com/quanquan1014)对农场名称中包含特殊字符的处理。
- 更正数据库写法,但是会导致V1.0用户的作物和种子丢失
## V1.0
世界的起源。
+98 -29
View File
@@ -1,28 +1,49 @@
import json
import os
from datetime import datetime
import httpx
from zhenxun.configs.config import Config
from zhenxun.services.log import logger
from .config import 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) -> bool:
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=10.0) as client:
response = await client.get(url)
requestArgs: dict = {"headers": headers}
if params:
requestArgs["params"] = params
if jsonData:
requestArgs["json"] = jsonData
response = await client.request("GET", url, **requestArgs)
if response.status_code == 200:
fullPath = os.path.join(savePath, fileName)
os.makedirs(os.path.dirname(fullPath), exist_ok=True)
@@ -32,68 +53,116 @@ class CRequestManager:
else:
logger.warning(f"文件下载失败: HTTP {response.status_code} {response.text}")
return False
except Exception as e:
logger.warning(f"下载文件异常: {e}")
return False
@classmethod
async def post(cls, endpoint: str, name: str = "", jsonData: dict = None, formData: dict = None) -> dict:
"""发送POST请求到指定接口,供其他方法统一调用
async def post(cls, endpoint: str, name: str = "", jsonData: dict = {}) -> dict:
"""发送POST请求到指定接口,统一调用,仅支持JSON格式数据
Args:
endpoint (str): 请求的接口路径
name (str, optional): 操作名称用于日志记录
jsonData (dict, optional): 以JSON格式发送的数据
formData (dict, optional): 以表单格式发送的数据
jsonData (dict): 以JSON格式发送的数据
Raises:
ValueError: 当jsonData和formData都未提供时抛出
ValueError: 当jsonData未提供时抛出
Returns:
dict: 返回请求结果的JSON数据
"""
if jsonData is None and formData is None:
raise ValueError("post请求必须提供jsonData或formData其中之一")
baseUrl = Config.get_config("zhenxun_plugin_farm", "服务地址")
url = f"{baseUrl.rstrip('/')}/{endpoint.lstrip('/')}"
headers = {"token": cls.m_sTokens}
try:
async with httpx.AsyncClient(timeout=5.0) as client:
if jsonData is not None:
response = await client.post(url, json=jsonData)
else:
response = await client.post(url, data=formData)
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}")
logger.warning(f"{name}请求失败: HTTP {response.status_code} {response.text}")
return {}
except httpx.RequestError as e:
logger.warning(f"真寻农场{name}请求异常: {e}")
logger.warning(f"{name}请求异常", e=e)
return {}
except Exception as e:
logger.warning(f"真寻农场{name}处理异常: {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('/')}/{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 sign(cls, uid: str) -> str:
a = await cls.post("http://diuse.work:9099/testPost", jsonData={"level":3})
async def initSignInFile(cls) -> bool:
if os.path.exists(g_sSignInPath):
try:
with open(g_sSignInPath, "r", encoding="utf-8") as f:
content = f.read()
sign = json.loads(content)
result = ""
date = sign.get("date", "")
yearMonth = g_pToolManager.dateTime().now().strftime("%Y%m")
type = int(a["type"])
if type == 1:
result = f"签到成功 type = 1"
elif type == 2:
result = f"签到成功 type = 2"
if date == yearMonth:
logger.debug("真寻农场签到文件检查完毕")
return True
else:
logger.warning("真寻农场签到文件检查失败, 即将下载")
return await cls.downloadSignInFile()
except json.JSONDecodeError as e:
logger.warning(f"真寻农场签到文件格式错误, 即将下载")
return await cls.downloadSignInFile()
else:
result = f"签到成功 type = {type}"
return await cls.downloadSignInFile()
return result
@classmethod
async def downloadSignInFile(cls) -> 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")
await cls.download(url, path, "signTemp.json", jsonData={'date':yearMonth})
g_pToolManager.renameFile(f"{path}/signTemp.json", "sign_in.json")
return True
except Exception as e:
logger.error("下载签到文件失败", e=e)
return False
g_pRequestManager = CRequestManager()
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Some files were not shown because too many files have changed in this diff Show More