Compare commits

...

37 Commits
v1.1 ... main

Author SHA1 Message Date
术樱
ac495ad6e9
Merge pull request #11 from Mualamx/Beta
 添加数据库字段(是否为点券作物,点券作物单价);实现点券购买种子;我的种子查询页面添加点券单价。
2025-10-12 20:23:55 +08:00
Mualamx
f62008209f 🎨 '农场币换点券'改为'点券兑换',新增兑换点券阈值赠送规则。 2025-10-12 06:07:16 +08:00
Mualamx
34d4f698dd 添加数据库字段(是否为点券作物,点券作物单价);实现点券购买种子;我的种子查询页面添加点券单价。 2025-10-12 05:15:24 +08:00
Mualamx
4ab21b7f7b 🔥 删除后门 2025-10-12 03:35:14 +08:00
Mualamx
eb937dd309 实现农场币换点券、查看当前用户点券,偷偷加后门改农场币。 2025-10-10 13:32:45 +08:00
Mualamx
4841ceb871 Merge branch 'mx' into Beta 2025-10-10 10:59:04 +08:00
Mualamx
01fceaa4fe 实现农场币换点券、查看当前用户点券,偷偷加后门改农场币。 2025-10-10 10:58:46 +08:00
3d42c1d283 🐛 修复签到无法正确获取作物的BUG 2025-08-06 17:51:26 +08:00
aa2c5811de 🚑 修复新字段影响旧用户导致无法收获作物的BUG 2025-06-30 04:22:51 +08:00
69ddbe4669 🚑 修复无法正常铲除荒废作物的BUG 2025-06-30 03:43:44 +08:00
564d9858f5 🚑 修正了铲除作物会导致土地等级失效的BUG 2025-06-30 01:07:57 +08:00
ba06fd8967 🐛 修正部分指令忽视指令前缀的BUG 2025-06-29 23:35:38 +08:00
e48bdf03da 🚑 修复土地等级绘制出错的BUG 2025-06-29 22:38:51 +08:00
f4e192ff27 🐛 修复铲除荒废作物会清空地块的BUG 2025-06-29 21:31:09 +08:00
c6f24bc3c2 新增在线查漏补缺功能 2025-06-29 01:45:24 +08:00
ca6a414ba4 update .gitignore 2025-06-06 11:06:11 +08:00
ac13ab9280 🎨 将文本内容同一更新
🐛 修复偷菜无法正常计算的BUG
2025-06-06 10:56:22 +08:00
ba2ecf19fc 🚑 修复当作物阶段计算下标错误的BUG 2025-06-03 16:24:04 +08:00
776b408c9c Merge branch 'Beta' of https://github.com/Shu-Ying/zhenxun_plugin_farm into Beta 2025-06-03 15:32:27 +08:00
d42f0aa433 🐛 修复了迁移旧数据库时因枯萎作物报错的BUG 2025-06-03 15:32:14 +08:00
04a2cc7b4d 🐛 修复了作物无法正常绘制种子状态的BUG 2025-05-30 16:07:34 +08:00
475865f1e9 📝 更新了README文件 2025-05-30 12:19:05 +08:00
fce307cc94 🚑 关闭了Debug模式 2025-05-30 11:23:20 +08:00
db294e6998 🐛 修正作物阶段绘制错误的BUG
🐛 修复偷菜会导致偷自己的BUG
2025-05-30 11:22:32 +08:00
1ef506d2fd 新增每天00:30检查签到文件 2025-05-29 18:20:03 +08:00
a475d0188b 🐛 修复了农场详述无法正常输出的BUG 2025-05-29 01:27:46 +08:00
7dc55bccf1 种子商店新增筛选功能 2025-05-28 19:48:12 +08:00
afa2259ad8 添加签到功能
🐛 修复默认使用格林威治时间而不是北京时间的BUG
2025-05-27 18:15:11 +08:00
c131692b19 🐛 修复迁移旧版数据库失败的BUG 2025-05-26 15:10:12 +08:00
cca3dc5d4d Merge branch 'Beta' of https://github.com/Shu-Ying/zhenxun_plugin_farm into Beta 2025-05-19 00:58:19 +08:00
2fbaf5d54f 🐛 修复当数据库文件不存在时的异常报错 2025-05-19 00:56:17 +08:00
6d3eb5d48f 📝 新增农场详述功能 2025-05-12 17:28:33 +08:00
ff5b446b52 新增多阶段作物特性
🐛 修复偷菜BUG
📝 新增部分作物
2025-05-09 18:20:00 +08:00
55394e3590 🎨 将plant.json迁移至sqlite本地数据库
📝 新增部分作物
2025-05-08 17:56:45 +08:00
0cd9b9d8db 📝 迁移部分代码结构,使文件名和代码功能更加匹配 2025-05-07 17:04:08 +08:00
8243a1e1c8 📝 继续完善数据库 2025-04-29 18:11:09 +08:00
6b99ec427c 📝 继续对数据库进行优化操作 2025-04-28 19:27:16 +08:00
623 changed files with 5335 additions and 1864 deletions

197
.gitignore vendored
View File

@ -1 +1,196 @@
__pycache__
/config/sign_in.json
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the enitre vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore

View File

@ -1,7 +1,56 @@
<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://github.com/astral-sh/ruff">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json" alt="ruff">
</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-列表)
- [关于](#关于)
- [致谢](#致谢)
- [许可证](#许可证)
---
## 农场界面
![农场界面](./resource/1.png)
---
## 如何安装
@ -9,40 +58,52 @@
方法一(推荐):在小真寻后台的插件商店下载即可<br>
方法二:下载源码放在小真寻`plugin`目录下
---
## 使用指令
| 指令 | 描述 | Tip |
| --- | --- | --- |
| @小真寻 开通农场 | 首次开通农场 | |
| 我的农场 | 你的农场 | |
| 农场详述 | 农场详细信息 | |
| 我的农场币 | 查询农场币 | |
| 种子商店 [页数] | 查看种子商店 | 数量不填默认为1 |
| 种子商店 [筛选关键字] [页数] or [页数] | 查看种子商店 | 当第一个参数为非整数时,会默认进入筛选状态。页数不填默认为1 |
| 购买种子 [种子名称] [数量] | 购买种子 | 数量不填默认为1 |
| 我的种子 | 查询仓库种子 | |
| 播种 [种子名称] [数量] | 播种种子 | 数量不填默认将最大可能播种 |
| 收获 | 收获成熟作物 | |
| 铲除 | 铲除荒废作物 | |
| 我的作物 | | |
| 我的作物 | 你的作物 | |
| 出售作物 [作物名称] [数量] | 从仓库里向系统售卖作物 | 不填写作物名将售卖仓库种全部作物 填作物名不填数量将指定作物全部出售 |
| @美波理 偷菜 | 偷别人的菜 | 每人每天只能偷5次 |
| 偷菜 @美波理 | 偷别人的菜 | 每人每天只能偷5次 |
| 购买农场币 | 将真寻金币兑换成农场币 | 兑换比例默认为1:2 手续费默认20% |
| 更改农场名 [新的农场名] | 改名 | 农场名称无法存储特殊字符 |
| 农场签到 | 签到 | 需要注意,该项会从服务器拉取签到数据 |
| 土地升级 [地块ID] | 将土地升级,带来收益提升 | 如果土地升级时,土地有播种作物,那么将直接成熟 |
---
## 更新日志[详细](./log/log.md)
- 感谢[quanquan1014](https://github.com/quanquan1014)对农场名称中包含特殊字符的处理。
- 更正数据库写法但是会导致V1.0用户的作物和种子丢失
- 完善我的农场图片资源,现在会在左上角显示经验、等级、金币等详细信息了
- 完善出售作物逻辑,现在可以空置作物名称来一键出售全部作物了,也可以选择空置数量来一键出售仓库种指定作物
- 完善播种逻辑,现在可以空置数量来一键播种指定作物了
- 新增更改农场名指令
- 改进对土地开垦条件判断
## 更新日志[(详细)](./log/log.md)
用户方面
---
- 新增土地升级指令,快来将土地升级至红土地、黑土地、金土地吧
- 新增查漏补缺作物资源功能,自动更新最新作物和作物资源文件
- 定时更新签到文件、作物资源从00:30调整至04:30
- 修正了部分土地资源错误的情况
- 修正了部分文本信息错误的情况
代码方面
---
- 修正部分事件连接机制
- 修正网络请求端口
## 待办事宜 `Todo` 列表
- [x] 完善我的农场图片,例如左上角显示用户数据
- [ ] 完善升级数据、作物数据、作物图片
- [ ] 签到功能
- [x] 签到功能
- [x] 在线更新作物信息
- [ ] 添加渔场功能
- [ ] 增加活动、交易行功能
- [ ] 增加交易行总行功能

View File

@ -1,15 +1,18 @@
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 .event.event import g_pEventManager
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(
@ -20,8 +23,9 @@ __plugin_meta__ = PluginMetadata(
指令
at 开通农场
我的农场
农场详述
我的农场币
种子商店 [页数]
种子商店 [筛选关键字] [页数] or [页数]
购买种子 [作物/种子名称] [数量]
我的种子
播种 [作物/种子名称] [数量] (数量不填默认将最大可能播种
@ -33,10 +37,12 @@ __plugin_meta__ = PluginMetadata(
开垦
购买农场币 [数量] 数量为消耗金币的数量
更改农场名 [新农场名]
农场签到
土地升级 [地块ID]通过农场详述获取
""".strip(),
extra=PluginExtraData(
author="Art_Sakura",
version="1.1",
version="1.5.2",
commands=[Command(command="我的农场")],
menu_type="群内小游戏",
configs=[
@ -52,6 +58,12 @@ __plugin_meta__ = PluginMetadata(
help="金币兑换农场币的倍数 默认值为: 2倍",
default_value="2",
),
RegisterConfig(
key="点券兑换倍数",
value="2000",
help="农场币兑换点券的倍数 比例为2000:1",
default_value="2000",
),
RegisterConfig(
key="手续费",
value="0.2",
@ -63,8 +75,8 @@ __plugin_meta__ = PluginMetadata(
value="http://diuse.work",
help="签到、交易行、活动等服务器地址",
default_value="http://diuse.work",
)
]
),
],
).to_dict(),
)
driver = get_driver()
@ -79,7 +91,24 @@ async def start():
# 初始化读取Json
await g_pJsonManager.init()
await g_pDBService.init()
# 检查作物文件是否缺失 or 更新
await g_pRequestManager.initPlantDBFile()
# 析构函数
@driver.on_shutdown
async def shutdown():
await g_pSqlManager.cleanup()
await g_pDBService.cleanup()
@scheduler.scheduled_job(trigger="cron", hour=4, minute=30, id="signInFile")
async def signInFile():
try:
await g_pJsonManager.initSignInFile()
await g_pRequestManager.initPlantDBFile()
except Exception as e:
logger.error("农场定时检查出错", e=e)

View File

@ -1,155 +1,141 @@
from nonebot.adapters import Event, MessageTemplate
import inspect
from nonebot.adapters import Event
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_alconna import (
Alconna,
AlconnaQuery,
Args,
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, g_sTranslation
from .dbService import g_pDBService
from .farm.farm import g_pFarmManager
from .farm.shop import g_pShopManager
async def isRegisteredByUid(uid: str) -> bool:
point = await g_pSqlManager.getUserPointByUid(uid)
if point < 0:
await MessageUtils.build_message("尚未开通农场快at我发送 开通农场 开通吧").send()
return False
return True
from .json import g_pJsonManager
from .tool import g_pToolManager
diuse_register = on_alconna(
Alconna("开通农场"),
priority=5,
rule=to_me(),
block=True,
use_cmd_start=True,
)
@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)
await MessageUtils.build_message(g_sTranslation["register"]["repeat"]).send(
reply_to=True
)
return
try:
# 获取原始用户名并安全处理
raw_name = str(session.user.name)
safe_name = sanitize_username(raw_name)
safe_name = g_pToolManager.sanitize_username(raw_name)
# 初始化用户信息
success = await g_pSqlManager.initUserInfoByUid(
uid=uid,
name=safe_name,
exp=0,
point=500
success = await g_pDBService.user.initUserInfoByUid(
uid=uid, name=safe_name, exp=0, point=500
)
msg = (
"✅ 农场开通成功!\n💼 初始资金500农场币"
g_sTranslation["register"]["success"].format(point=500)
if success
else "⚠️ 开通失败,请稍后再试"
else g_sTranslation["register"]["error"]
)
logger.info(f"用户注册 {'成功' if success else '失败'}{uid}")
except Exception as e:
msg = "⚠️ 系统繁忙,请稍后再试"
logger.error(f"注册异常 | UID:{uid} | 错误:{str(e)}")
msg = g_sTranslation["register"]["error"]
logger.error(f"注册异常 | UID:{uid} | 错误:{e}")
await MessageUtils.build_message(msg).send(reply_to=True)
def sanitize_username(username: str, max_length: int = 15) -> str:
"""
安全处理用户名
功能
1. 移除首尾空白
2. 过滤危险字符
3. 转义单引号
4. 处理空值
5. 限制长度
"""
# 处理空值
if not username:
return "神秘农夫"
# 基础清洗
cleaned = username.strip()
# 允许的字符白名单(可自定义扩展)
safe_chars = {
'_', '-', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')',
'+', '=', '.', ',', '~', '·', ' ',
'a','b','c','d','e','f','g','h','i','j','k','l','m',
'n','o','p','q','r','s','t','u','v','w','x','y','z',
'A','B','C','D','E','F','G','H','I','J','K','L','M',
'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
'0','1','2','3','4','5','6','7','8','9',
}
# 添加常用中文字符Unicode范围
safe_chars.update(chr(c) for c in range(0x4E00, 0x9FFF+1))
# 过滤危险字符
filtered = [
c if c in safe_chars or 0x4E00 <= ord(c) <= 0x9FFF
else ''
for c in cleaned
]
# 合并处理结果
safe_str = ''.join(filtered)
# 转义单引号(双重保障)
escaped = safe_str.replace("'", "''")
# 处理空结果
if not escaped:
return "神秘农夫"
# 长度限制
return escaped[:max_length]
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="播种"),
Subcommand("harvest", help_text="收获"),
Subcommand("eradicate", help_text="铲除"),
Subcommand("my-plant", help_text="我的作物"),
Subcommand("lock-plant", Args["name?", str], help_text="作物加锁"),
Subcommand("unlock-plant", Args["name?", str], help_text="作物解锁"),
Subcommand("sell-plant", Args["name?", str]["num?", int], help_text="出售作物"),
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("sell-point", Args["num?", int], help_text="转换金币")
Subcommand("change-name", Args["name?", str], help_text="更改农场名"),
Subcommand("sign-in", help_text="农场签到"),
Subcommand("admin-up", Args["num?", int], help_text="农场下阶段"),
Subcommand("point-to-vipPoint", Args["num?", int], help_text="点券兑换"),
Subcommand("my-vipPoint", help_text="我的点券"),
),
priority=5,
block=True,
use_cmd_start=True,
)
@diuse_farm.assign("$main")
async def _(session: Uninfo):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await g_pToolManager.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 g_pToolManager.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="我的农场",
@ -157,16 +143,20 @@ diuse_farm.shortcut(
prefix=True,
)
@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()
await MessageUtils.build_message(g_sTranslation["basic"]["notFarm"]).send()
return False
await MessageUtils.build_message(f"你的当前农场币为: {point}").send(reply_to=True)
await MessageUtils.build_message(
g_sTranslation["basic"]["point"].format(point=point)
).send(reply_to=True)
diuse_farm.shortcut(
"种子商店(.*?)",
@ -175,16 +165,45 @@ diuse_farm.shortcut(
prefix=True,
)
@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 g_pToolManager.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(
"购买种子(?P<name>.*?)",
command="我的农场",
@ -192,21 +211,25 @@ diuse_farm.shortcut(
prefix=True,
)
@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(
"请在指令后跟需要购买的种子名称"
).finish(reply_to=True)
await MessageUtils.build_message(g_sTranslation["buySeed"]["notSeed"]).finish(
reply_to=True
)
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await g_pToolManager.isRegisteredByUid(uid):
return
result = await g_pShopManager.buySeed(uid, name.result, num.result)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"我的种子",
command="我的农场",
@ -214,16 +237,18 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("my-seed")
async def _(session: Uninfo):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await g_pToolManager.isRegisteredByUid(uid):
return
result = await g_pFarmManager.getUserSeedByUid(uid)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"播种(?P<name>.*?)",
command="我的农场",
@ -231,16 +256,19 @@ diuse_farm.shortcut(
prefix=True,
)
@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(
"请在指令后跟需要播种的种子名称"
).finish(reply_to=True)
await MessageUtils.build_message(g_sTranslation["sowing"]["notSeed"]).finish(
reply_to=True
)
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await g_pToolManager.isRegisteredByUid(uid):
return
result = await g_pFarmManager.sowing(uid, name.result, num.result)
@ -254,16 +282,18 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("harvest")
async def _(session: Uninfo):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await g_pToolManager.isRegisteredByUid(uid):
return
result = await g_pFarmManager.harvest(uid)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"铲除",
command="我的农场",
@ -271,11 +301,12 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("eradicate")
async def _(session: Uninfo):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await g_pToolManager.isRegisteredByUid(uid):
return
result = await g_pFarmManager.eradicate(uid)
@ -289,47 +320,55 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("my-plant")
async def _(session: Uninfo):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await g_pToolManager.isRegisteredByUid(uid):
return
result = await g_pFarmManager.getUserPlantByUid(uid)
await MessageUtils.build_message(result).send(reply_to=True)
reclamation = on_alconna(
Alconna("开垦"),
priority=5,
block=True,
diuse_farm.shortcut(
"作物加锁(?P<name>)",
command="我的农场",
arguments=["lock-plant", "{name}"],
prefix=True,
)
@reclamation.handle()
async def _(session: Uninfo):
@diuse_farm.assign("lock-plant")
async def _(session: Uninfo, name: Match[str]):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await g_pToolManager.isRegisteredByUid(uid):
return
condition = await g_pFarmManager.reclamationCondition(uid)
condition += "\n 回复是将执行开垦"
await MessageUtils.build_message(condition).send(reply_to=True)
result = await g_pFarmManager.lockUserPlantByUid(uid, name.result, 1)
await MessageUtils.build_message(result).send(reply_to=True)
@waiter(waits=["message"], keep_session=True)
async def check(event: Event):
return event.get_plaintext()
resp = await check.wait(timeout=60)
if resp is None:
await MessageUtils.build_message("等待超时").send(reply_to=True)
return
if not resp == "":
diuse_farm.shortcut(
"作物解锁(?P<name>)",
command="我的农场",
arguments=["unlock-plant", "{name}"],
prefix=True,
)
@diuse_farm.assign("unlock-plant")
async def _(session: Uninfo, name: Match[str]):
uid = str(session.user.id)
if not await g_pToolManager.isRegisteredByUid(uid):
return
res = await g_pFarmManager.reclamation(uid)
await MessageUtils.build_message(res).send(reply_to=True)
result = await g_pFarmManager.lockUserPlantByUid(uid, name.result, 0)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"出售作物(?P<name>.*?)",
@ -338,16 +377,56 @@ diuse_farm.shortcut(
prefix=True,
)
@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 g_pToolManager.isRegisteredByUid(uid):
return
result = await g_pShopManager.sellPlantByUid(uid, name.result, num.result)
await MessageUtils.build_message(result).send(reply_to=True)
reclamation = on_alconna(
Alconna("开垦"),
priority=5,
block=True,
use_cmd_start=True,
)
@reclamation.handle()
async def _(session: Uninfo):
uid = str(session.user.id)
if not await g_pToolManager.isRegisteredByUid(uid):
return
condition = await g_pFarmManager.reclamationCondition(uid)
condition += f"\n{g_sTranslation['reclamation']['confirm']}"
await MessageUtils.build_message(condition).send(reply_to=True)
@waiter(waits=["message"], keep_session=True)
async def check(event: Event):
return event.get_plaintext()
resp = await check.wait(timeout=60)
if resp is None:
await MessageUtils.build_message(g_sTranslation["reclamation"]["timeOut"]).send(
reply_to=True
)
return
if not resp == "":
return
res = await g_pFarmManager.reclamation(uid)
await MessageUtils.build_message(res).send(reply_to=True)
diuse_farm.shortcut(
"偷菜",
command="我的农场",
@ -355,26 +434,32 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("stealing")
async def _(session: Uninfo, target: Match[At]):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await g_pToolManager.isRegisteredByUid(uid):
return
if not target.available:
await MessageUtils.build_message("请在指令后跟需要at的人").finish(reply_to=True)
await MessageUtils.build_message(g_sTranslation["stealing"]["noTarget"]).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:
await MessageUtils.build_message("目标尚未开通农场快邀请ta开通吧").send()
if not result:
await MessageUtils.build_message(
g_sTranslation["stealing"]["targetNotFarm"]
).send()
return None
result = await g_pFarmManager.stealing(uid, tar.target)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"购买农场币(.*?)",
command="我的农场",
@ -382,16 +467,17 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("buy-point")
async def _(session: Uninfo, num: Query[int] = AlconnaQuery("num", 0)):
if num.result <= 0:
await MessageUtils.build_message(
"请在指令后跟需要购买农场币的数量"
).finish(reply_to=True)
await MessageUtils.build_message("请在指令后跟需要购买农场币的数量").finish(
reply_to=True
)
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await g_pToolManager.isRegisteredByUid(uid):
return
result = await g_pFarmManager.buyPointByUid(uid, num.result)
@ -405,23 +491,205 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("change-name")
async def _(session: Uninfo, name: Match[str]):
if not name.available:
await MessageUtils.build_message(
"请在指令后跟需要更改的用户名"
).finish(reply_to=True)
await MessageUtils.build_message(g_sTranslation["changeName"]["noName"]).finish(
reply_to=True
)
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
if not await g_pToolManager.isRegisteredByUid(uid):
return
safeName = sanitize_username(name.result)
safeName = g_pToolManager.sanitize_username(name.result)
result = await g_pSqlManager.updateUserNameByUid(uid, safeName)
if safeName == "神秘农夫":
await MessageUtils.build_message(g_sTranslation["changeName"]["error"]).send(
reply_to=True
)
return
if result == True:
await MessageUtils.build_message("更新用户名成功!").send(reply_to=True)
result = await g_pDBService.user.updateUserNameByUid(uid, safeName)
if result:
await MessageUtils.build_message(g_sTranslation["changeName"]["success"]).send(
reply_to=True
)
else:
await MessageUtils.build_message("更新用户名失败!").send(reply_to=True)
await MessageUtils.build_message(g_sTranslation["changeName"]["error1"]).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 g_pToolManager.isRegisteredByUid(uid):
return
# 判断签到是否正常加载
if not g_bSignStatus:
await MessageUtils.build_message(g_sTranslation["signIn"]["error"]).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 += g_sTranslation["signIn"]["success"].format(
day=signDay, exp=exp, num=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 += g_sTranslation["signIn"]["grandTotal"].format(
exp=extraExp, num=extraPoint
)
vipPoint = reward.get("vipPoint", 0)
if vipPoint > 0:
message += g_sTranslation["signIn"]["grandTotal1"].format(num=vipPoint)
if plant:
for key, value in plant.items():
message += g_sTranslation["signIn"]["grandTotal2"].format(
name=key, num=value
)
else:
message = g_sTranslation["signIn"]["error1"]
await MessageUtils.build_message(message).send()
# await MessageUtils.alc_forward_msg([info], session.self_id, BotConfig.self_nickname).send(reply_to=True)
soil_upgrade = on_alconna(
Alconna("土地升级", Args["index", int]),
priority=5,
block=True,
use_cmd_start=True,
)
@soil_upgrade.handle()
async def _(session: Uninfo, index: Query[int] = AlconnaQuery("index", 1)):
uid = str(session.user.id)
if not await g_pToolManager.isRegisteredByUid(uid):
return
condition = await g_pFarmManager.soilUpgradeCondition(uid, index.result)
await MessageUtils.build_message(condition).send(reply_to=True)
if not condition.startswith("将土地升级至:"):
return
@waiter(waits=["message"], keep_session=True)
async def check(event: Event):
return event.get_plaintext()
resp = await check.wait(timeout=60)
if resp is None:
await MessageUtils.build_message(g_sTranslation["soilInfo"]["timeOut"]).send(
reply_to=True
)
return
if not resp == "":
return
res = await g_pFarmManager.soilUpgrade(uid, index.result)
await MessageUtils.build_message(res).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 g_pToolManager.isRegisteredByUid(uid):
return
await g_pDBService.userSoil.nextPhase(uid, num.result)
diuse_farm.shortcut(
"点券兑换(.*?)",
command="我的农场",
arguments=["point-to-vipPoint"],
prefix=True,
)
@diuse_farm.assign("point-to-vipPoint")
async def _(session: Uninfo, num: Query[int] = AlconnaQuery("num", 0)):
if num.result <= 0:
await MessageUtils.build_message("请在指令后跟需要购买点券的数量").finish(
reply_to=True
)
uid = str(session.user.id)
if not await g_pToolManager.isRegisteredByUid(uid):
return
result = await g_pFarmManager.pointToVipPointByUid(uid, num.result)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"我的点券",
command="我的农场",
arguments=["my-vipPoint"],
prefix=True,
)
@diuse_farm.assign("my-vipPoint")
async def _(session: Uninfo):
uid = str(session.user.id)
vipPoint = await g_pDBService.user.getUserVipPointByUid(uid)
if not await g_pToolManager.isRegisteredByUid(uid):
return
await MessageUtils.build_message(
g_sTranslation["basic"]["vipPoint"].format(vipPoint=vipPoint)
).send(reply_to=True)

213
config.py
View File

@ -1,106 +1,133 @@
import json
from pathlib import Path
from zhenxun.configs.path_config import DATA_PATH
from zhenxun.services.log import logger
# 签到状态
g_bSignStatus = True
# 是否处于Debug模式
g_bIsDebug = False
# 数据库文件目录
g_sDBPath = DATA_PATH / "farm_db"
# 数据库文件路径
g_sDBFilePath = DATA_PATH / "farm_db/farm.db"
# 农场资源文件目录
g_sResourcePath = Path(__file__).resolve().parent / "resource"
class CJsonManager:
def __init__(self):
self.m_pItem = {}
self.m_pPlant = {}
self.m_pLevel = {}
self.m_pSoil = {}
# 农场作物数据库
g_sPlantPath = g_sResourcePath / "db/plant.db"
async def init(self) -> bool:
if not await self.initItem():
return False
# 农场配置文件目录
g_sConfigPath = Path(__file__).resolve().parent / "config"
if not await self.initPlant():
return False
# 农场签到文件路径
g_sSignInPath = g_sConfigPath / "sign_in.json"
if not await self.initLevel():
return False
# 土地等级上限
g_iSoilLevelMax = 3
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_sTranslation = {
"basic": {
"notFarm": "尚未开通农场快at我发送 开通农场 开通吧 🌱🚜",
"point": "你的当前农场币为: {point} 🌾💰",
"vipPoint": "你的当前点券为: {vipPoint} 🌾💰",
},
"register": {
"success": "✅ 农场开通成功!\n💼 初始资金:{point}农场币 🥳🎉",
"repeat": "🎉 您已经开通农场啦~ 😄",
"error": "⚠️ 开通失败,请稍后再试 💔",
},
"buySeed": {
"notSeed": "🌱 请在指令后跟需要购买的种子名称",
"notNum": "❗️ 请输入购买数量!",
"noLevel": "🔒 你的等级不够哦,努努力吧 💪",
"noPoint": "💰 你的农场币不够哦~ 快速速氪金吧!💸",
"noVipPoint": "💰 你的点券不够哦~ 快速速氪金吧!💸",
"success": "✅ 成功购买{name},花费{total}农场币,剩余{point}农场币 🌾",
"vipSuccess": "✅ 成功购买{name},花费{total}点券,剩余{point}点券 🌾",
"errorSql": "❌ 购买失败,执行数据库错误!🛑",
"error": "❌ 购买出错!请检查需购买的种子名称!🔍",
},
"sowing": {
"notSeed": "🌱 请在指令后跟需要播种的种子名称",
"noSeed": "❌ 没有在你的仓库发现{name}种子,快去买点吧!🛒",
"noNum": "⚠️ 播种失败!仓库中的{name}种子数量不足,当前剩余{num}个种子 🍂",
"success": "✅ 播种{name}成功!仓库剩余{num}个种子 🌱",
"success2": "✅ 播种数量超出开垦土地数量,已将可播种土地成功播种{name}!仓库剩余{num}个种子 🌾",
"error": "❌ 播种失败,请稍后重试!⏳",
},
"harvest": {
"append": "🌾 收获作物:{name},数量为:{num},经验为:{exp}",
"exp": "✨ 累计获得经验:{exp} 📈",
"no": "🤷‍♂️ 没有可收获的作物哦~ 不要试图拔苗助长 🚫",
"error": "❌ 收获失败,请稍后重试!⏳",
},
"eradicate": {
"success": "🗑️ 成功铲除荒废作物,累计获得经验:{exp}",
"error": "❌ 没有可以铲除的作物 🚜",
},
"reclamation": {
"confirm": "⚠️ 回复“是”将执行开垦 ⛏️",
"timeOut": "⏰ 等待开垦回复超时,请重试",
"perfect": "🌟 你已经开垦了全部土地 🎉",
"next": "🔜 下次开垦所需条件:等级:{level},农场币:{num} 💰",
"next2": "🔜 下次开垦所需条件:等级:{level},农场币:{num},物品:{item} 📦",
"nextLevel": "📈 当前用户等级{level},升级所需等级为{next}",
"noNum": "💰 当前用户农场币不足,升级所需农场币为{num} 💸",
"success": "✅ 开垦土地成功!🌱",
"error": "❌ 获取开垦土地条件失败!",
"error1": "❌ 执行开垦失败!",
"error2": "❌ 未知错误{e}💥",
},
"sellPlant": {
"no": "🤷‍♀️ 你仓库没有可以出售的作物 🌾",
"success": "💰 成功出售所有作物,获得农场币:{point},当前农场币:{num} 🎉",
"success1": "💰 成功出售{name},获得农场币:{point},当前农场币:{num} 🥳",
"error": "❌ 出售作物{name}出错:仓库中不存在该作物 🚫",
"error1": "❌ 出售作物{name}出错:数量不足 ⚠️",
},
"stealing": {
"noTarget": "🎯 请在指令后跟需要at的人 👤",
"targetNotFarm": "🚜 目标尚未开通农场快邀请ta开通吧 😉",
"max": "❌ 你今天可偷次数到达上限啦,手下留情吧 🙏",
"info": "🤫 成功偷到作物:{name},数量为:{num} 🍒",
"noPlant": "🌱 目标没有作物可以被偷 🌾",
"repeat": "🚫 你已经偷过目标啦,请手下留情 🙏",
},
"changeName": {
"noName": "✏️ 请在指令后跟需要更改的农场名",
"success": "✅ 更新农场名成功!🎉",
"error": "❌ 农场名不支持特殊符号!🚫",
"error1": "❌ 更新农场名失败!💔",
},
"signIn": {
"success": "📝 签到成功!累计签到天数:{day}\n🎁 获得经验{exp},获得金币{num} 💰",
"grandTotal": "\n🎉 成功领取累计签到奖励:\n✨ 额外获得经验{exp},额外获得金币{num} 🥳",
"grandTotal1": ",额外获得点券{num} 🎫",
"grandTotal2": "\n🌱 获得{name}种子 * {num} 🌟",
"error": "❗️ 签到功能异常!",
"error1": "❌ 签到失败!未知错误 💔",
},
"soilInfo": {
"success": "土地成功升级至{name},效果为:{text}",
"timeOut": "等待土地升级回复超时,请重试",
"error": "土地信息尚未查询到",
"error1": "该土地已经升至满级啦~",
"red": "增产+10%",
"black": "增产+20% 时间-20%",
"gold": "增产+28% 经验+28% 时间-20%",
"amethyst": "增产+30% 经验+30% 时间-25% 幸运+1%",
"aquamarine": "增产+32% 经验+32% 时间-28% 幸运+1%",
"blackcrystal": "增产+32% 经验+40% 时间-28% 幸运+2%",
},
"lockPlant": {
"noPlant": "很抱歉,你的作物仓库中没有该作物",
"lockPlant": "加锁成功,现在{name}不会被一键售卖了",
"unlockPlant": "解锁成功,现在{name}会被一键售卖了",
"error": "未知错误",
},
}

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
}
}
}

View File

@ -1,4 +0,0 @@
{
"date": "202505",
""
}

View File

@ -153,5 +153,104 @@
"x": 1451,
"y": 1072
}
},
"upgrade":
{
"red": [
{ "level": 28, "point": 200000, "vipPoint": 0, "item":{}},
{ "level": 29, "point": 220000, "vipPoint": 0, "item":{}},
{ "level": 30, "point": 240000, "vipPoint": 0, "item":{}},
{ "level": 31, "point": 260000, "vipPoint": 0, "item":{}},
{ "level": 32, "point": 290000, "vipPoint": 0, "item":{}},
{ "level": 33, "point": 320000, "vipPoint": 0, "item":{}},
{ "level": 34, "point": 350000, "vipPoint": 0, "item":{}},
{ "level": 35, "point": 380000, "vipPoint": 0, "item":{}},
{ "level": 36, "point": 410000, "vipPoint": 0, "item":{}},
{ "level": 37, "point": 440000, "vipPoint": 0, "item":{}},
{ "level": 38, "point": 480000, "vipPoint": 0, "item":{}},
{ "level": 39, "point": 520000, "vipPoint": 0, "item":{}},
{ "level": 40, "point": 560000, "vipPoint": 0, "item":{}},
{ "level": 41, "point": 600000, "vipPoint": 0, "item":{}},
{ "level": 42, "point": 650000, "vipPoint": 0, "item":{}},
{ "level": 43, "point": 700000, "vipPoint": 0, "item":{}},
{ "level": 44, "point": 770000, "vipPoint": 0, "item":{}},
{ "level": 45, "point": 900000, "vipPoint": 0, "item":{}},
{ "level": 47, "point": 1500000, "vipPoint": 0, "item":{}},
{ "level": 49, "point": 2000000, "vipPoint": 0, "item":{}},
{ "level": 53, "point": 4000000, "vipPoint": 0, "item":{}},
{ "level": 53, "point": 4000000, "vipPoint": 0, "item":{}},
{ "level": 55, "point": 5500000, "vipPoint": 0, "item":{}},
{ "level": 57, "point": 6800000, "vipPoint": 0, "item":{}},
{ "level": 60, "point": 10000000, "vipPoint": 0, "item":{}},
{ "level": 64, "point": 15000000, "vipPoint": 0, "item":{}},
{ "level": 68, "point": 20000000, "vipPoint": 0, "item":{}},
{ "level": 73, "point": 30000000, "vipPoint": 0, "item":{}},
{ "level": 78, "point": 50000000, "vipPoint": 0, "item":{}},
{ "level": 83, "point": 80000000, "vipPoint": 0, "item":{}}
],
"black": [
{ "level": 40, "point": 500000, "vipPoint": 0, "item":{}},
{ "level": 41, "point": 600000, "vipPoint": 0, "item":{}},
{ "level": 42, "point": 700000, "vipPoint": 0, "item":{}},
{ "level": 43, "point": 800000, "vipPoint": 0, "item":{}},
{ "level": 44, "point": 900000, "vipPoint": 0, "item":{}},
{ "level": 45, "point": 1000000, "vipPoint": 0, "item":{}},
{ "level": 46, "point": 2000000, "vipPoint": 0, "item":{}},
{ "level": 47, "point": 2200000, "vipPoint": 0, "item":{}},
{ "level": 48, "point": 2400000, "vipPoint": 0, "item":{}},
{ "level": 49, "point": 2600000, "vipPoint": 0, "item":{}},
{ "level": 50, "point": 2800000, "vipPoint": 0, "item":{}},
{ "level": 51, "point": 3000000, "vipPoint": 0, "item":{}},
{ "level": 52, "point": 4000000, "vipPoint": 0, "item":{}},
{ "level": 53, "point": 4200000, "vipPoint": 0, "item":{}},
{ "level": 54, "point": 4400000, "vipPoint": 0, "item":{}},
{ "level": 55, "point": 4600000, "vipPoint": 0, "item":{}},
{ "level": 56, "point": 4800000, "vipPoint": 0, "item":{}},
{ "level": 57, "point": 5000000, "vipPoint": 0, "item":{}},
{ "level": 59, "point": 6000000, "vipPoint": 0, "item":{}},
{ "level": 61, "point": 6200000, "vipPoint": 0, "item":{}},
{ "level": 65, "point": 6600000, "vipPoint": 0, "item":{}},
{ "level": 65, "point": 6600000, "vipPoint": 0, "item":{}},
{ "level": 67, "point": 6800000, "vipPoint": 0, "item":{}},
{ "level": 69, "point": 7000000, "vipPoint": 0, "item":{}},
{ "level": 72, "point": 10000000, "vipPoint": 0, "item":{}},
{ "level": 76, "point": 16000000, "vipPoint": 0, "item":{}},
{ "level": 80, "point": 27000000, "vipPoint": 0, "item":{}},
{ "level": 85, "point": 43000000, "vipPoint": 0, "item":{}},
{ "level": 90, "point": 65000000, "vipPoint": 0, "item":{}},
{ "level": 95, "point": 93000000, "vipPoint": 0, "item":{}}
],
"gold": [
{ "level": 58, "point": 4000000, "vipPoint": 0, "item":{}},
{ "level": 59, "point": 4200000, "vipPoint": 0, "item":{}},
{ "level": 60, "point": 4400000, "vipPoint": 0, "item":{}},
{ "level": 61, "point": 4600000, "vipPoint": 0, "item":{}},
{ "level": 62, "point": 4800000, "vipPoint": 0, "item":{}},
{ "level": 63, "point": 5000000, "vipPoint": 0, "item":{}},
{ "level": 64, "point": 5200000, "vipPoint": 0, "item":{}},
{ "level": 65, "point": 5400000, "vipPoint": 0, "item":{}},
{ "level": 66, "point": 5600000, "vipPoint": 0, "item":{}},
{ "level": 67, "point": 5800000, "vipPoint": 0, "item":{}},
{ "level": 68, "point": 6000000, "vipPoint": 0, "item":{}},
{ "level": 69, "point": 6200000, "vipPoint": 0, "item":{}},
{ "level": 70, "point": 6600000, "vipPoint": 0, "item":{}},
{ "level": 71, "point": 7000000, "vipPoint": 0, "item":{}},
{ "level": 72, "point": 7400000, "vipPoint": 0, "item":{}},
{ "level": 73, "point": 7800000, "vipPoint": 0, "item":{}},
{ "level": 74, "point": 8200000, "vipPoint": 0, "item":{}},
{ "level": 75, "point": 8600000, "vipPoint": 0, "item":{}},
{ "level": 77, "point": 9500000, "vipPoint": 0, "item":{}},
{ "level": 79, "point": 10400000, "vipPoint": 0, "item":{}},
{ "level": 83, "point": 12200000, "vipPoint": 0, "item":{}},
{ "level": 83, "point": 12200000, "vipPoint": 0, "item":{}},
{ "level": 85, "point": 13100000, "vipPoint": 0, "item":{}},
{ "level": 87, "point": 14000000, "vipPoint": 0, "item":{}},
{ "level": 90, "point": 16000000, "vipPoint": 0, "item":{}},
{ "level": 94, "point": 20000000, "vipPoint": 0, "item":{}},
{ "level": 98, "point": 28000000, "vipPoint": 0, "item":{}},
{ "level": 103, "point": 44000000, "vipPoint": 0, "item":{}},
{ "level": 108, "point": 76000000, "vipPoint": 0, "item":{}},
{ "level": 113, "point": 1080000000, "vipPoint": 0, "item":{}}
]
}
}

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()

143
database/database.py Normal file
View File

@ -0,0 +1,143 @@
from contextlib import asynccontextmanager
import os
from pathlib import Path
import re
import aiosqlite
from zhenxun.services.log import logger
from ..config import g_sDBFilePath, g_sDBPath
class CSqlManager:
def __init__(self):
dbPath = Path(g_sDBPath)
if dbPath and not dbPath.exists():
os.makedirs(dbPath, exist_ok=True)
@classmethod
async def cleanup(cls):
if hasattr(cls, "m_pDB") and cls.m_pDB:
await cls.m_pDB.close()
@classmethod
async def init(cls) -> bool:
try:
cls.m_pDB = await aiosqlite.connect(g_sDBFilePath)
cls.m_pDB.row_factory = aiosqlite.Row
return True
except Exception as e:
logger.warning("初始化总数据库失败", e=e)
return False
@classmethod
@asynccontextmanager
async def _transaction(cls):
await cls.m_pDB.execute("BEGIN;")
try:
yield
except:
await cls.m_pDB.execute("ROLLBACK;")
raise
else:
await cls.m_pDB.execute("COMMIT;")
@classmethod
async def getTableInfo(cls, tableName: str) -> list:
if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", tableName):
raise ValueError(f"Illegal table name: {tableName}")
try:
cursor = await cls.m_pDB.execute(f'PRAGMA table_info("{tableName}")')
rows = await cursor.fetchall()
return [{"name": row[1], "type": row[2]} for row in rows]
except aiosqlite.Error:
return []
@classmethod
async def ensureTableSchema(cls, tableName: str, columns: dict) -> bool:
"""由AI生成
创建表或为已存在表添加缺失字段
返回 True 表示有变更创建或新增列False 则无操作
Args:
tableName (_type_): 表名
columns (_type_): 字典
Returns:
_type_: _description_
"""
info = await cls.getTableInfo(tableName)
existing = {col["name"]: col["type"].upper() for col in info}
desired = {k: v.upper() for k, v in columns.items() if k != "PRIMARY KEY"}
primaryKey = columns.get("PRIMARY KEY", "")
if not existing:
colsDef = ", ".join(f'"{k}" {v}' for k, v in desired.items())
if primaryKey:
colsDef += f", PRIMARY KEY {primaryKey}"
await cls.m_pDB.execute(f'CREATE TABLE "{tableName}" ({colsDef});')
return True
toAdd = [k for k in desired if k not in existing]
toRemove = [k for k in existing if k not in desired]
typeMismatch = [
k for k in desired if k in existing and existing[k] != desired[k]
]
if toAdd and not toRemove and not typeMismatch:
for col in toAdd:
await cls.m_pDB.execute(
f'ALTER TABLE "{tableName}" ADD COLUMN "{col}" {columns[col]}'
)
return True
async with cls._transaction():
tmpTable = f"{tableName}_new"
colsDef = ", ".join(f'"{k}" {v}' for k, v in desired.items())
if primaryKey:
colsDef += f", PRIMARY KEY {primaryKey}"
await cls.m_pDB.execute(f'CREATE TABLE "{tmpTable}" ({colsDef});')
commonCols = [k for k in desired if k in existing]
if commonCols:
colsStr = ", ".join(f'"{c}"' for c in commonCols)
sql = (
f'INSERT INTO "{tmpTable}" ({colsStr}) '
f"SELECT {colsStr} "
f'FROM "{tableName}";'
)
await cls.m_pDB.execute(sql)
await cls.m_pDB.execute(f'DROP TABLE "{tableName}";')
await cls.m_pDB.execute(
f'ALTER TABLE "{tmpTable}" RENAME TO "{tableName}";'
)
return True
@classmethod
async def executeDB(cls, command: str) -> bool:
"""执行自定义SQL
Args:
command (str): SQL语句
Returns:
bool: 是否执行成功
"""
if not command:
logger.warning("数据库语句长度为空!")
return False
try:
async with cls._transaction():
await cls.m_pDB.execute(command)
return True
except Exception as e:
logger.warning(f"数据库语句执行出错: {command}", e=e)
return False
g_pSqlManager = CSqlManager()

293
database/plant.py Normal file
View File

@ -0,0 +1,293 @@
from contextlib import asynccontextmanager
import os
import aiosqlite
from zhenxun.configs.config import Config
from zhenxun.services.log import logger
from ..config import g_bIsDebug, g_sPlantPath, g_sResourcePath
from ..request import g_pRequestManager
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:
num = int(x)
if num not in seen:
seen.add(num)
result.append(num)
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 []
@classmethod
async def downloadPlant(cls) -> bool:
"""遍历所有作物下载各阶段图片及icon文件到指定文件夹
Returns:
bool: 全部下载完成返回True如有失败返回False
"""
success = True
baseUrl = Config.get_config("zhenxun_plugin_farm", "服务地址")
baseUrl = baseUrl.rstrip("/") + ":8998/file"
try:
plants = await cls.listPlants()
for plant in plants:
name = plant["name"]
phaseCount = await cls.getPlantPhaseNumberByName(name)
saveDir = os.path.join(g_sResourcePath, "plant", name)
begin = 0 if plant["general"] == 0 else 1
for idx in range(begin, phaseCount + 1):
fileName = f"{idx}.png"
fullPath = os.path.join(saveDir, fileName)
if os.path.exists(fullPath):
continue
url = f"{baseUrl}/{name}/{idx}.png"
if not await g_pRequestManager.download(url, saveDir, f"{idx}.png"):
success = False
iconName = "icon.png"
iconPath = os.path.join(saveDir, iconName)
if not os.path.exists(iconPath):
iconUrl = f"{baseUrl}/{name}/{iconName}"
if not await g_pRequestManager.download(iconUrl, saveDir, iconName):
success = False
return success
except Exception as e:
logger.warning(f"下载作物资源异常: {e}")
return False

479
database/user.py Normal file
View File

@ -0,0 +1,479 @@
import math
from zhenxun.services.log import logger
from ..tool import g_pToolManager
from .database import CSqlManager
class CUserDB(CSqlManager):
@classmethod
async def initDB(cls):
"""初始化用户表结构确保user表存在且字段完整"""
userInfo = {
"uid": "TEXT PRIMARY KEY", # 用户Uid
"name": "TEXT NOT NULL", # 农场名称
"exp": "INTEGER DEFAULT 0", # 经验值
"point": "INTEGER DEFAULT 0", # 金币
"vipPoint": "INTEGER DEFAULT 0", # 点券
"soil": "INTEGER DEFAULT 3", # 解锁土地数量
"stealTime": "TEXT DEFAULT ''", # 偷菜时间字符串
"stealCount": "INTEGER DEFAULT 0", # 剩余偷菜次数
}
await cls.ensureTableSchema("user", userInfo)
@classmethod
async def initUserInfoByUid(
cls, uid: str, name: str = "", exp: int = 0, point: int = 500
) -> bool | str:
"""初始化用户信息,包含初始偷菜时间字符串与次数
Args:
uid (str): 用户Uid
name (str): 农场名称
exp (int): 农场经验
point (int): 农场币
Returns:
bool | str: False 表示失败字符串表示成功信息
"""
nowStr = g_pToolManager.dateTime().date().today().strftime("%Y-%m-%d")
sql = (
f"INSERT INTO user (uid, name, exp, point, soil, stealTime, stealCount) "
f"VALUES ({uid}, '{name}', {exp}, {point}, 3, '{nowStr}', 5)"
)
try:
async with cls._transaction():
await cls.m_pDB.execute(sql)
return "开通农场成功"
except Exception as e:
logger.warning("initUserInfoByUid 事务执行失败!", e=e)
return False
@classmethod
async def getAllUsers(cls) -> list[str]:
"""获取所有用户UID列表
Returns:
list[str]: 用户UID列表
"""
cursor = await cls.m_pDB.execute("SELECT uid FROM user")
rows = await cursor.fetchall()
return [row[0] for row in rows]
@classmethod
async def isUserExist(cls, uid: str) -> bool:
"""判断用户是否存在
Args:
uid (str): 用户Uid
Returns:
bool: 如果用户存在返回True否则返回False
"""
if not uid:
return False
try:
async with cls.m_pDB.execute(
"SELECT 1 FROM user WHERE uid = ?", (uid,)
) as cursor:
row = await cursor.fetchone()
return row is not None
except Exception as e:
logger.warning("isUserExist 查询失败!", e=e)
return False
@classmethod
async def getUserInfoByUid(cls, uid: str) -> dict:
"""获取指定用户完整信息
Args:
uid (str): 用户Uid
Returns:
dict: 包含所有用户字段的字典
"""
if not uid:
return {}
try:
async with cls.m_pDB.execute(
"SELECT * FROM user WHERE uid = ?", (uid,)
) as cursor:
row = await cursor.fetchone()
if not row:
return {}
result = dict(row)
return result
except Exception as e:
logger.warning("getUserInfoByUid 查询失败!", e=e)
return {}
@classmethod
async def getUserNameByUid(cls, uid: str) -> str:
"""根据用户Uid获取用户名
Args:
uid (str): 用户Uid
Returns:
str: 用户名失败返回空字符串
"""
if not uid:
return ""
try:
async with cls.m_pDB.execute(
"SELECT name FROM user WHERE uid = ?", (uid,)
) as cursor:
row = await cursor.fetchone()
return row["name"] if row else ""
except Exception as e:
logger.warning("getUserNameByUid 查询失败!", e=e)
return ""
@classmethod
async def updateUserNameByUid(cls, uid: str, name: str) -> bool:
"""根据用户Uid更新用户名
Args:
uid (str): 用户Uid
name (str): 新用户名
Returns:
bool: 是否更新成功
"""
if not uid or not name:
return False
try:
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE user SET name = ? WHERE uid = ?", (name, uid)
)
return True
except Exception as e:
logger.warning("updateUserNameByUid 事务执行失败!", e=e)
return False
@classmethod
async def getUserPointByUid(cls, uid: str) -> int:
"""获取指定用户农场币
Args:
uid (str): 用户Uid
Returns:
int: 农场币数量失败返回 -1
"""
if not uid:
return -1
try:
async with cls.m_pDB.execute(
"SELECT point FROM user WHERE uid = ?", (uid,)
) as cursor:
row = await cursor.fetchone()
return int(row[0]) if row and row[0] is not None else -1
except Exception as e:
logger.warning("getUserPointByUid 查询失败!", e=e)
return -1
@classmethod
async def updateUserPointByUid(cls, uid: str, point: int) -> bool:
"""根据用户Uid更新农场币数量
Args:
uid (str): 用户Uid
point (int): 新农场币数量
Returns:
bool: 是否更新成功
"""
if not uid or point < 0:
logger.warning("updateUserPointByUid 参数校验失败!")
return False
try:
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE user SET point = ? WHERE uid = ?", (point, uid)
)
return True
except Exception as e:
logger.error("updateUserPointByUid 事务执行失败!", e=e)
return False
@classmethod
async def getUserVipPointByUid(cls, uid: str) -> int:
"""获取指定用户点券
Args:
uid (str): 用户Uid
Returns:
int: 点券数量失败返回 -1
"""
if not uid:
return -1
try:
async with cls.m_pDB.execute(
"SELECT vipPoint FROM user WHERE uid = ?", (uid,)
) as cursor:
row = await cursor.fetchone()
return int(row[0]) if row and row[0] is not None else -1
except Exception as e:
logger.warning("getUservipPointByUid 查询失败!", e=e)
return -1
@classmethod
async def updateUserVipPointByUid(cls, uid: str, vipPoint: int) -> bool:
"""根据用户Uid更新点券数量
Args:
uid (str): 用户Uid
vipPoint (int): 新点券数量
Returns:
bool: 是否更新成功
"""
if not uid or vipPoint < 0:
logger.warning("updateUservipPointByUid 参数校验失败!")
return False
try:
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE user SET vipPoint = ? WHERE uid = ?", (vipPoint, uid)
)
return True
except Exception as e:
logger.error("updateUservipPointByUid 事务执行失败!", e=e)
return False
@classmethod
async def getUserExpByUid(cls, uid: str) -> int:
"""获取指定用户经验值
Args:
uid (str): 用户Uid
Returns:
int: 经验值失败返回 -1
"""
if not uid:
return -1
try:
async with cls.m_pDB.execute(
"SELECT exp FROM user WHERE uid = ?", (uid,)
) as cursor:
row = await cursor.fetchone()
return int(row[0]) if row and row[0] is not None else -1
except Exception as e:
logger.warning("getUserExpByUid 查询失败!", e=e)
return -1
@classmethod
async def updateUserExpByUid(cls, uid: str, exp: int) -> bool:
"""根据用户Uid更新经验值
Args:
uid (str): 用户Uid
exp (int): 新经验值
Returns:
bool: 是否更新成功
"""
if not uid:
return False
try:
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE user SET exp = ? WHERE uid = ?", (exp, uid)
)
return True
except Exception as e:
logger.warning("updateUserExpByUid 事务执行失败!", e=e)
return False
@classmethod
async def getUserLevelByUid(cls, uid: str) -> tuple[int, int, int]:
"""获取用户等级信息
Args:
uid (str): 用户Uid
Returns:
tuple[int, int, int]: 成功返回(当前等级, 升至下级还需经验, 当前等级已获经验)
失败返回(-1, -1, -1)
"""
if not uid:
return -1, -1, -1
try:
async with cls.m_pDB.execute(
"SELECT exp FROM user WHERE uid = ?", (uid,)
) as cursor:
row = await cursor.fetchone()
if not row or row[0] is None:
return -1, -1, -1
expVal = int(row[0])
levelStep = 200 # 每级经验增量
discriminant = 1 + 8 * expVal / levelStep
level = int((-1 + math.sqrt(discriminant)) // 2)
if level < 0:
level = 0
def cumExp(k: int) -> int:
return levelStep * k * (k + 1) // 2
totalExpCurrentLevel = cumExp(level)
totalExpNextLevel = cumExp(level + 1)
currentExp = expVal - totalExpCurrentLevel
return level, totalExpNextLevel, currentExp
return -1, -1, -1
except Exception as e:
logger.warning("getUserLevelByUid 查询失败!", e=e)
return -1, -1, -1
@classmethod
async def getUserSoilByUid(cls, uid: str) -> int:
"""获取解锁土地数量
Args:
uid (str): 用户Uid
Returns:
int: 解锁土地块数失败返回0
"""
if not uid:
return 0
try:
async with cls.m_pDB.execute(
"SELECT soil FROM user WHERE uid = ?", (uid,)
) as cursor:
row = await cursor.fetchone()
return int(row[0]) if row and row[0] is not None else 0
except Exception as e:
logger.warning("getUserSoilByUid 查询失败!", e=e)
return 0
@classmethod
async def updateUserSoilByUid(cls, uid: str, soil: int) -> bool:
"""更新指定用户解锁土地数量
Args:
uid (str): 用户Uid
soil (int): 新土地数量
Returns:
bool: 更新成功返回True否则False
"""
if not uid or soil < 0:
return False
try:
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE user SET soil = ? WHERE uid = ?", (soil, uid)
)
return True
except Exception as e:
logger.warning("updateUserSoilByUid 事务执行失败!", e=e)
return False
@classmethod
async def getStealTimeByUid(cls, uid: str) -> str:
"""根据用户Uid获取偷菜时间字符串
Args:
uid (str): 用户Uid
Returns:
str: 偷菜时间字符串失败返回空字符串
"""
if not uid:
return ""
try:
async with cls.m_pDB.execute(
"SELECT stealTime FROM user WHERE uid = ?", (uid,)
) as cursor:
row = await cursor.fetchone()
return row[0] if row and row[0] else ""
except Exception as e:
logger.warning("getStealTimeByUid 查询失败!", e=e)
return ""
@classmethod
async def updateStealTimeByUid(cls, uid: str, stealTime: str) -> bool:
"""根据用户Uid更新偷菜时间字符串
Args:
uid (str): 用户Uid
stealTime (str): 新偷菜时间字符串
Returns:
bool: 是否更新成功
"""
if not uid or not stealTime:
logger.warning("updateStealTimeByUid 参数校验失败!")
return False
try:
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE user SET stealTime = ? WHERE uid = ?", (stealTime, uid)
)
return True
except Exception as e:
logger.warning("updateStealTimeByUid 事务执行失败!", e=e)
return False
@classmethod
async def getStealCountByUid(cls, uid: str) -> int:
"""根据用户Uid获取剩余偷菜次数
Args:
uid (str): 用户Uid
Returns:
int: 剩余偷菜次数失败返回 -1
"""
if not uid:
return -1
try:
async with cls.m_pDB.execute(
"SELECT stealCount FROM user WHERE uid = ?", (uid,)
) as cursor:
row = await cursor.fetchone()
return int(row[0]) if row and row[0] is not None else 0
except Exception as e:
logger.warning("getStealCountByUid 查询失败!", e=e)
return -1
@classmethod
async def updateStealCountByUid(
cls, uid: str, stealTime: str, stealCount: int
) -> bool:
"""根据用户Uid更新剩余偷菜次数
Args:
uid (str): 用户Uid
stealTime (str): 偷菜日期
stealCount (int): 新剩余偷菜次数
Returns:
bool: 是否更新成功
"""
if not uid or stealCount < 0:
logger.warning("updateStealCountByUid 参数校验失败!")
return False
try:
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE user SET stealTime = ?, stealCount = ? WHERE uid = ?",
(stealTime, stealCount, uid),
)
return True
except Exception as e:
logger.warning("updateStealCountByUid 事务执行失败!", e=e)
return False

158
database/userItem.py Normal file
View File

@ -0,0 +1,158 @@
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) -> int | None:
"""根据道具名称查询某一项数量
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("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("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("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("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("addUserItemByUid失败", e=e)
return False

205
database/userPlant.py Normal file
View File

@ -0,0 +1,205 @@
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", # 数量
"isLock": "INTEGER NOT NULL DEFAULT 0", # 是否上锁 0=没有非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("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) -> int | None:
"""根据作物名称获取用户的作物数量
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("getUserPlantByName 查询失败!", e=e)
return None
@classmethod
async def checkUserPlantByName(cls, uid: str, plant: str) -> bool:
"""根据作物名称判断用户作物仓库是否存在该作物
Args:
uid (str): 用户uid
plant (str): 作物名称
Returns:
bool: 是否存在
"""
try:
async with cls.m_pDB.execute(
"SELECT * FROM userPlant WHERE uid = ? AND plant = ?", (uid, plant)
) as cursor:
row = await cursor.fetchone()
return True if row else False
except Exception as e:
logger.warning("checkUserPlantByName 查询失败!", e=e)
return False
@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("updateUserPlantByName 失败!", e=e)
return False
@classmethod
async def lockUserPlantByName(cls, uid: str, plant: str, lock: int) -> bool:
"""给作物加锁,防止一键售卖
Args:
uid (str): 用户uid
plant (str): 作物名称
lock (int): 0为解锁非0均为加锁
Returns:
bool: 是否加锁成功
"""
try:
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE userPlant SET isLock = ? WHERE uid = ? AND plant = ?",
(lock, uid, plant),
)
return True
except Exception as e:
logger.warning("lockUserPlantByName 失败!", e=e)
return False
@classmethod
async def checkPlantLockByName(cls, uid: str, plant: str) -> bool:
"""根据作物名称判断是否加锁
Args:
uid (str): 用户uid
plant (str): 作物名称
Returns:
bool: 是否加锁
"""
try:
async with cls.m_pDB.execute(
"SELECT isLock FROM userPlant WHERE uid = ? AND plant = ?", (uid, plant)
) as cursor:
row = await cursor.fetchone()
return row[0] > 0 if row else False
except Exception as e:
logger.warning("checkUserPlantByName 查询失败!", 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("deleteUserPlantByName 失败!", e=e)
return False

212
database/userSeed.py Normal file
View File

@ -0,0 +1,212 @@
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("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("_addUserSeedByUid 失败!", e=e)
return False
@classmethod
async def getUserSeedByName(cls, uid: str, seed: str) -> int | None:
"""根据种子名称获取种子数量
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("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("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("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("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("deleteUserSeedByName 删除失败!", e=e)
return False

290
database/userSign.py Normal file
View File

@ -0,0 +1,290 @@
import calendar
from datetime import timedelta
import random
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'))", # 创建时间 # noqa: E501
"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'))", # 更新时间 # noqa: E501
}
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
)
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:
monthSignDays = 1
await cls.m_pDB.execute(
"""
INSERT INTO userSignSummary
(uid, totalSignDays, currentMonth, monthSignDays, lastSignDate, continuousDays, supplementCount)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
uid,
1,
currentMonth,
monthSignDays,
signDate,
1,
1 if isSupplement else 0,
),
)
# 计算累签奖励
reward = g_pJsonManager.m_pSign["continuou"].get(f"{monthSignDays}", 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 = {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

591
database/userSoil.py Normal file
View File

@ -0,0 +1,591 @@
import math
from zhenxun.services.log import logger
from ..config import g_bIsDebug
from ..dbService import g_pDBService
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", # 收获次数
"isSoilPlanted": "INTEGER DEFAULT NULL", # 是否种植作物 0=没有1=有
"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
elapsedTime = currentTime - soilInfo["plantTime"]
currentStage = currentStage = sum(1 for thr in phaseList if elapsedTime >= thr)
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 matureNow(cls, uid: str, soilIndex: int):
"""将指定地块的作物直接成熟
Args:
uid (str): 用户ID
soilIndex (int): 地块索引从1开始
"""
# 与 nextPhase 不同:无需调试模式检查,允许在任何模式下调用
soilInfo = await cls.getUserSoil(uid, soilIndex)
if not soilInfo:
return
plantName = soilInfo.get("plantName")
if not plantName:
return
plantInfo = await g_pDBService.plant.getPlantByName(plantName)
if not plantInfo:
return
currentTime = int(g_pToolManager.dateTime().now().timestamp())
# 如果当前时间已经超过或等于成熟时间,则作物已成熟或可收获
if currentTime >= soilInfo["matureTime"]:
return
# 将作物成熟时间直接更新为当前时间,实现立即成熟
await cls.updateUserSoilFields(uid, soilIndex, {"matureTime": currentTime})
@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
if data == ",,,4,":
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, isSoilPlanted)
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),
soilInfo.get("isSoilPlanted", 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, isSoilPlanted)
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),
soilInfo.get("isSoilPlanted", 0),
),
)
@classmethod
async def getUserSoil(cls, uid: str, soilIndex: int) -> dict:
"""获取指定用户某块土地的详细信息
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
Returns:
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 {}
columns = [description[0] for description in cursor.description]
return dict(zip(columns, row))
@classmethod
async def _getUserSoil(cls, uid: str, soilIndex: int) -> dict | None:
"""获取指定用户某块土地的详细信息
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
Returns:
dict | None: 记录存在返回字段-值字典否则返回 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 countSoilByLevel(cls, uid: str, soilLevel: int) -> int:
"""统计指定用户在指定土地等级的土地数量
Args:
uid (str): 用户ID
soilLevel (int): 土地等级
Returns:
int: 符合条件的土地数量
"""
async with cls._transaction():
cursor = await cls.m_pDB.execute(
"SELECT COUNT(*) FROM userSoil WHERE uid = ? AND soilLevel = ?",
(uid, soilLevel),
)
row = await cursor.fetchone()
return row[0] if row else 0
@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",
"isSoilPlanted",
}
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 sowingByPlantName(cls, uid: str, soilIndex: int, plantName: str) -> bool:
"""播种指定作物到用户土地区
Args:
uid (str): 用户ID
soilIndex (int): 土地区索引
plantName (str): 植物名
Returns:
bool: 播种成功返回 True否则返回 False
"""
# 校验土地区是否已种植
soilInfo = await cls.getUserSoil(uid, soilIndex)
if soilInfo and soilInfo.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())
time = int(plantCfg.get("time", 0))
percent = await cls.getSoilLevelTime(soilInfo.get("soilLevel", 0))
# 处理土地等级带来的时间缩短
time = math.floor(time * (100 + percent) // 100)
matureTs = nowTs + time * 3600
try:
async with cls._transaction():
prev = soilInfo 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": 0,
"fertilizerStatus": 0,
"bugStatus": 0,
"weedStatus": 0,
"waterStatus": 0,
"harvestCount": 0,
"isSoilPlanted": 1,
}
)
return True
except Exception as e:
logger.error("播种失败!", 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)
@classmethod
async def getSoilLevel(cls, level: int) -> str:
"""获取土地等级英文文本
Args:
level (int): 土地等级
Returns:
str:
"""
if level == 1:
return "red"
elif level == 2:
return "black"
elif level == 3:
return "gold"
return "default"
@classmethod
async def getSoilLevelText(cls, level: int) -> str:
"""获取土地等级中文文本
Args:
level (int): 土地等级
Returns:
str:
"""
if level == 1:
return "红土地"
elif level == 2:
return "黑土地"
elif level == 3:
return "金土地"
return "草土地"
@classmethod
async def getSoilLevelHarvestNumber(cls, level: int) -> int:
"""获取土地等级收获数量增加比例
Args:
level (int): 土地等级
Returns:
int:
"""
if level == 2:
return 20
elif level == 3:
return 28
return 10
@classmethod
async def getSoilLevelHarvestExp(cls, level: int) -> int:
"""获取土地等级收获经验增加比例
Args:
level (int): 土地等级
Returns:
int:
"""
if level == 3:
return 28
return 0
@classmethod
async def getSoilLevelTime(cls, level: int) -> int:
"""获取土地等级播种减少时间消耗
Args:
level (int): 土地等级
Returns:
int:
"""
if level == 2:
return 20
elif level == 3:
return 20
return 0

226
database/userSteal.py Normal file
View File

@ -0,0 +1,226 @@
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

45
dbService.py Normal file
View File

@ -0,0 +1,45 @@
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()

123
event/event.py Normal file
View File

@ -0,0 +1,123 @@
import inspect
import time
from zhenxun.services.log import logger
class Signal:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
bound = instance.__dict__.get(self.name)
if bound is None:
bound = _SignalBound()
instance.__dict__[self.name] = bound
return bound
class _SignalBound:
def __init__(self):
self._slots = []
self._onceSlots = []
def connect(self, func=None, *, priority=0):
if func is None:
return lambda f: self.connect(f, priority=priority)
if callable(func) and not any(s[0] == func for s in self._slots):
self._slots.append((func, priority))
self._slots.sort(key=lambda x: -x[1])
return func
def connect_once(self, func=None, *, priority=0):
if func is None:
return lambda f: self.connect_once(f, priority=priority)
if callable(func) and not any(s[0] == func for s in self._onceSlots):
self._onceSlots.append((func, priority))
self._onceSlots.sort(key=lambda x: -x[1])
return func
def disconnect(self, func):
self._slots = [s for s in self._slots if s[0] != func]
self._onceSlots = [s for s in self._onceSlots if s[0] != func]
async def emit(self, *args, **kwargs):
slots = list(self._slots)
onceSlots = list(self._onceSlots)
self._onceSlots.clear()
for slot, _ in slots + onceSlots:
start = time.time()
try:
if inspect.iscoroutinefunction(slot):
await slot(*args, **kwargs)
else:
slot(*args, **kwargs)
logger.debug(
f"【真寻农场】事件槽 {slot.__name__} 执行完成,耗时 {(time.time() - start) * 1000:.2f} ms"
)
except Exception as e:
logger.warning(f"事件槽 {slot.__name__} 触发异常: {e}")
class FarmEventManager:
m_beforePlant = Signal()
"""播种前信号
Args:
uid (str): 用户Uid
name (str): 播种种子名称
num (int): 播种数量
"""
m_afterPlant = Signal()
"""播种后信号 每块地播种都会触发该信号
Args:
uid (str): 用户Uid
name (str): 播种种子名称
soilIndex (int): 播种地块索引 从1开始
"""
m_beforeHarvest = Signal()
"""收获前信号
Args:
uid (str): 用户Uid
"""
m_afterHarvest = Signal()
"""收获后信号 每块地收获都会触发该信号
Args:
uid (str): 用户Uid
name (str): 收获作物名称
num (int): 收获数量
soilIndex (int): 收获地块索引 从1开始
"""
m_beforeEradicate = Signal()
"""铲除前信号
Args:
uid (str): 用户Uid
"""
m_afterEradicate = Signal()
"""铲除后信号 每块地铲除都会触发该信号
Args:
uid (str): 用户Uid
soilIndex (index): 铲除地块索引 从1开始
"""
m_beforeExpand = Signal()
m_afterExpand = Signal()
m_beforeSteal = Signal()
m_afterSteal = Signal()
m_dit = Signal()
g_pEventManager = FarmEventManager()

File diff suppressed because it is too large Load Diff

117
farm/help.py Normal file
View File

@ -0,0 +1,117 @@
from pathlib import Path
from jinja2 import Template
from playwright.async_api import async_playwright
from zhenxun.configs.path_config import DATA_PATH
from zhenxun.services.log import logger
from ..config import g_sResourcePath
def rendeerHtmlToFile(path: Path | str, context: dict, output: Path | str) -> None:
"""
使用 Jinja2 渲染 HTML 模板并保存到指定文件会自动创建父目录
Args:
path (str): 模板 HTML 路径
context (dict): 用于渲染的上下文字典
output (str): 输出 HTML 文件路径
"""
templatePath = str(path)
outputPath = str(output)
templateStr = Path(templatePath).read_text(encoding="utf-8")
template = Template(templateStr)
rendered = template.render(**context)
# 自动创建目录
Path(outputPath).parent.mkdir(parents=True, exist_ok=True)
Path(outputPath).write_text(rendered, encoding="utf-8")
async def screenshotHtmlToBytes(path: str) -> bytes:
"""
使用 Playwright 截图本地 HTML 文件并返回 PNG 图片字节数据
Args:
path (str): 本地 HTML 文件路径
Returns:
bytes: PNG 图片的原始字节内容
"""
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()
file_url = Path(path).resolve().as_uri()
await page.goto(file_url)
image_bytes = await page.screenshot(full_page=True)
await browser.close()
return image_bytes
async def screenshotSave(path: str, save: str) -> None:
"""
使用 Playwright 渲染本地 HTML 并将截图保存到指定路径
Args:
path (str): HTML 文件路径
save (str): PNG 保存路径 output/image.png
"""
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()
file_url = Path(path).resolve().as_uri()
await page.goto(file_url)
# 确保保存目录存在
Path(save).parent.mkdir(parents=True, exist_ok=True)
# 截图并保存到本地文件
await page.screenshot(path=save, full_page=True)
await browser.close()
async def createHelpImage() -> bool:
templatePath = g_sResourcePath / "html/help.html"
outputPath = DATA_PATH / "farm_res/html/help.html"
context = {
"title": "功能指令总览",
"data": [
{
"command": "开通农场",
"description": "首次进入游戏开通农场",
"tip": "",
},
{
"command": "购买种子",
"description": "从商店中购买可用种子",
"tip": "",
},
{"command": "播种", "description": "将种子种入土地中", "tip": "先开垦土地"},
{
"command": "收获",
"description": "收获成熟作物获得收益",
"tip": "",
},
{
"command": "偷菜",
"description": "从好友农场中偷取成熟作物",
"tip": "1",
},
],
}
try:
rendeerHtmlToFile(templatePath, context, outputPath)
image_bytes = await screenshot_html_to_bytes(html_output_path)
except Exception as e:
logger.warning("绘制农场帮助菜单失败", e=e)
return False
return True

View File

@ -1,82 +1,104 @@
import math
from re import I
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, g_sTranslation
from ..dbService import g_pDBService
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 = [
"-",
"种子名称",
"种子单价",
"农场币",
"点券",
"解锁等级",
"果实单价",
"收获经验",
"收获数量",
"成熟时间(小时)",
"收获次数",
"再次成熟时间(小时)",
"是否可以上架交易行"
"是否可以上架交易行",
]
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(
dataList.append(
[
icon,
key,
plant['buy'],
plant['level'],
plant['price'],
plant['experience'],
plant['harvest'],
plant['time'],
plant['crop'],
plant['again'],
sell
plant["name"], # 种子名称
plant["buy"], # 农场币种子单价
plant["vipBuy"], # 点券种子单价
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
@ -93,34 +115,46 @@ class CShopManager:
"""
if num <= 0:
return "请输入购买数量!"
return g_sTranslation["buySeed"]["notNum"]
plantInfo = None
plantInfo = await g_pDBService.plant.getPlantByName(name)
if not plantInfo:
return g_sTranslation["buySeed"]["error"]
try:
plantInfo = g_pJsonManager.m_pPlant['plant'][name]
except Exception as e:
return "购买出错!请检查需购买的种子名称!"
level = await g_pDBService.user.getUserLevelByUid(uid)
level = await g_pSqlManager.getUserLevelByUid(uid)
if level[0] < int(plantInfo["level"]):
return g_sTranslation["buySeed"]["noLevel"]
if level[0] < int(plantInfo['level']):
return "你的等级不够哦,努努力吧"
point = await g_pSqlManager.getUserPointByUid(uid)
total = int(plantInfo['buy']) * num
logger.debug(f"用户:{uid}购买{name},数量为{num}。用户农场币为{point},购买需要{total}")
if point < total:
return "你的农场币不够哦~ 快速速氪金吧!"
"""
logger.debug(
f"用户:{uid}购买{name},数量为{num}。用户农场币为{point},购买需要{total}"
)
"""
if plantInfo["isVip"] == 1:
vipPoint = await g_pDBService.user.getUserVipPointByUid(uid)
total = int(plantInfo["vipBuy"]) * num
if vipPoint < total:
return g_sTranslation["buySeed"]["noVipPoint"]
await g_pDBService.user.updateUserVipPointByUid(uid, vipPoint - total)
else:
await g_pSqlManager.updateUserPointByUid(uid, point - total)
point = await g_pDBService.user.getUserPointByUid(uid)
total = int(plantInfo["buy"]) * num
if point < total:
return g_sTranslation["buySeed"]["noPoint"]
await g_pDBService.user.updateUserPointByUid(uid, point - total)
if await g_pSqlManager.addUserSeedByUid(uid, name, num) == False:
return "购买失败,执行数据库错误!"
if not await g_pDBService.userSeed.addUserSeedByUid(uid, name, num):
return g_sTranslation["buySeed"]["errorSql"]
return f"成功购买{name},花费{total}农场币, 剩余{point - total}农场币"
if plantInfo["isVip"] == 1:
return g_sTranslation["buySeed"]["vipSuccess"].format(
name=name, total=total, point=vipPoint - total
)
else:
return g_sTranslation["buySeed"]["success"].format(
name=name, total=total, point=point - total
)
@classmethod
async def sellPlantByUid(cls, uid: str, name: str = "", num: int = 1) -> str:
@ -135,36 +169,63 @@ 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 "你仓库没有可以出售的作物"
return g_sTranslation["sellPlant"]["no"]
point = 0
totalSold = 0
isAll = (num == -1)
isAll = num == -1
if name == "":
for plantName, count in plant.items():
plantInfo = g_pJsonManager.m_pPlant['plant'][plantName]
point += plantInfo['price'] * count
await g_pSqlManager.updateUserPlantByName(uid, plantName, 0)
isLock = await g_pDBService.userPlant.checkPlantLockByName(
uid, plantName
)
if isLock:
continue
plantInfo = await g_pDBService.plant.getPlantByName(plantName)
if not plantInfo:
continue
point += plantInfo["price"] * count
await g_pDBService.userPlant.updateUserPlantByName(uid, plantName, 0)
else:
if name not in plant:
return f"出售作物{name}出错:仓库中不存在该作物"
return g_sTranslation["sellPlant"]["error"].format(name=name)
available = plant[name]
sellAmount = available if isAll else min(available, num)
if sellAmount <= 0:
return f"出售作物{name}出错:数量不足"
await g_pSqlManager.updateUserPlantByName(uid, name, available - sellAmount)
return g_sTranslation["sellPlant"]["error1"].format(name=name)
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}"
return g_sTranslation["sellPlant"]["success"].format(
point=totalPoint, num=currentPoint + totalPoint
)
else:
return f"成功出售{name},获得农场币:{totalPoint},当前农场币:{currentPoint + totalPoint}"
return g_sTranslation["sellPlant"]["success1"].format(
name=name, point=totalPoint, num=currentPoint + totalPoint
)
g_pShopManager = CShopManager()

105
json.py Normal file
View File

@ -0,0 +1,105 @@
import json
from zhenxun.services.log import logger
from . import config
from .request import g_pRequestManager
class CJsonManager:
def __init__(self):
self.m_pItem = {}
self.m_pLevel = {}
self.m_pSoil = {}
self.m_pSign = {}
async def init(self) -> bool:
if not await self.initItem():
return False
if not await self.initLevel():
return False
if not await self.initSoil():
return False
return await self.initSignInFile()
async def initItem(self) -> bool:
try:
with open(
config.g_sConfigPath / "item.json",
encoding="utf-8",
) as file:
self.m_pItem = json.load(file)
return True
except FileNotFoundError:
logger.warning("item.json 打开失败")
return False
except json.JSONDecodeError as e:
logger.warning(f"item.json JSON格式错误: {e}")
return False
async def initLevel(self) -> bool:
try:
with open(
config.g_sConfigPath / "level.json",
encoding="utf-8",
) as file:
self.m_pLevel = json.load(file)
return True
except FileNotFoundError:
logger.warning("level.json 打开失败")
return False
except json.JSONDecodeError as e:
logger.warning(f"level.json JSON格式错误: {e}")
return False
async def initSoil(self) -> bool:
try:
with open(
config.g_sConfigPath / "soil.json",
encoding="utf-8",
) as file:
self.m_pSoil = json.load(file)
return True
except FileNotFoundError:
logger.warning("soil.json 打开失败")
return False
except json.JSONDecodeError as e:
logger.warning(f"soil.json JSON格式错误: {e}")
return False
async def initSignInFile(self) -> bool:
if not await g_pRequestManager.initSignInFile():
config.g_bSignStatus = False
return False
else:
result = await self.initSign()
config.g_bSignStatus = result
return result
async def initSign(self) -> bool:
try:
with open(
config.g_sSignInPath,
encoding="utf-8",
) as file:
self.m_pSign = json.load(file)
return True
except FileNotFoundError:
logger.warning("sign_in.json 打开失败")
return False
except json.JSONDecodeError as e:
logger.warning(f"sign_in.json JSON格式错误: {e}")
return False
g_pJsonManager = CJsonManager()

View File

@ -1,15 +1,71 @@
# 真寻农场更新日志
## V1.1
## V1.5
用户方面
---
- 新增土地升级指令,快来将土地升级至红土地、黑土地、金土地吧
- 新增查漏补缺作物资源功能,自动更新最新作物和作物资源文件
- 定时更新签到文件、作物资源从00:30调整至04:30
- 修正了部分土地资源错误的情况
- 修正了部分文本信息错误的情况
- 感谢[quanquan1014](https://github.com/quanquan1014)对农场名称中包含特殊字符的处理。
- 更正数据库写法但是会导致V1.0用户的作物和种子丢失
代码方面
---
- 修正部分事件连接机制
- 修正网络请求端口
## V1.4
用户方面
---
- 新增种子商店筛选功能如果没有BUG的话后续我的种子、我的作物等也会加入筛选功能
- 新增签到功能(测试
- 新增农场详述功能,能通过该功能更加详细的观看农场数据
- 修复了迁移旧数据库无法正常迁移的BUG
- 修复了偷菜会导致偷自己的BUG
- 修正了作物阶段绘制不正确的BUG
代码方面
---
- 修正了多阶段作物成长素材计算逻辑
- 修正了作物数据库字段错乱的问题
- 作物新增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
世界的起源。

View File

@ -1,99 +1,292 @@
import json
import os
import httpx
from rich.progress import (
BarColumn,
DownloadColumn,
Progress,
TextColumn,
TimeRemainingColumn,
TransferSpeedColumn,
)
from zhenxun.configs.config import Config
from zhenxun.services.log import logger
from .config import g_sPlantPath, g_sSignInPath
from .dbService import g_pDBService
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)
if response.status_code == 200:
fullPath = os.path.join(savePath, fileName)
os.makedirs(os.path.dirname(fullPath), exist_ok=True)
with open(fullPath, "wb") as f:
f.write(response.content)
return True
else:
logger.warning(f"文件下载失败: HTTP {response.status_code} {response.text}")
async with httpx.AsyncClient(timeout=30.0) as client:
requestArgs: dict = {"headers": headers}
if params:
requestArgs["params"] = params
if jsonData:
requestArgs["json"] = jsonData
response = await client.request(
"GET", url, **requestArgs, follow_redirects=True
)
if response.status_code != 200:
logger.warning(
f"文件下载失败: HTTP {response.status_code} {response.text}"
)
return False
totalLength = int(response.headers.get("Content-Length", 0))
fullPath = os.path.join(savePath, fileName)
os.makedirs(os.path.dirname(fullPath), exist_ok=True)
with Progress(
TextColumn("[progress.description]{task.description}"),
BarColumn(),
DownloadColumn(),
TransferSpeedColumn(),
TimeRemainingColumn(),
transient=True,
) as progress:
task = progress.add_task(
f"[green]【真寻农场】正在下载 {fileName}", total=totalLength
)
with open(fullPath, "wb") as f:
async for chunk in response.aiter_bytes(chunk_size=1024):
f.write(chunk)
progress.advance(task, len(chunk))
return True
except Exception as e:
logger.warning(f"下载文件异常: {e}")
return False
@classmethod
async def post(cls, endpoint: str, name: str = "", jsonData: dict = 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('/')}"
url = f"{baseUrl.rstrip('/')}:8998/{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('/')}:8998/{endpoint.lstrip('/')}"
headers = {"token": cls.m_sTokens}
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(url, headers=headers)
if response.status_code == 200:
return response.json()
else:
logger.warning(
f"{name}请求失败: HTTP {response.status_code} {response.text}"
)
return {}
except httpx.RequestError as e:
logger.warning(f"{name}请求异常", e=e)
return {}
except Exception as e:
logger.warning(f"{name}处理异常", e=e)
return {}
@classmethod
async def 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, 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:
logger.warning("真寻农场签到文件格式错误, 即将下载")
return await cls.downloadSignInFile()
else:
result = f"签到成功 type = {type}"
return await cls.downloadSignInFile()
return result
@classmethod
async def downloadSignInFile(cls) -> bool:
"""下载签到文件,并重命名为 sign_in.json
Returns:
bool: 是否下载成功
"""
try:
baseUrl = Config.get_config("zhenxun_plugin_farm", "服务地址")
url = f"{baseUrl.rstrip('/')}:8998/sign_in"
path = str(g_sSignInPath.parent.resolve(strict=False))
yearMonth = g_pToolManager.dateTime().now().strftime("%Y%m")
# 下载为 signTemp.json
success = await cls.download(
url=url,
savePath=path,
fileName="signTemp.json",
jsonData={"date": yearMonth},
)
if not success:
return False
# 重命名为 sign_in.json
g_pToolManager.renameFile(f"{path}/signTemp.json", "sign_in.json")
return True
except Exception as e:
logger.error("下载签到文件失败", e=e)
return False
@classmethod
async def initPlantDBFile(cls) -> bool:
"""检查本地 plant.db 版本,如远程版本更新则重新下载
Returns:
bool: 是否为最新版或成功更新
"""
versionPath = os.path.join(os.path.dirname(g_sPlantPath), "version.json")
try:
with open(versionPath, encoding="utf-8") as f:
localVersion = json.load(f).get("version", 0)
except Exception as e:
logger.warning(f"读取本地版本失败默认版本为0: {e}")
localVersion = 0
remoteInfo = await cls.get("plant_version", name="版本检查")
remoteVersion = remoteInfo.get("version")
if remoteVersion is None:
logger.warning("获取远程版本失败")
return False
if float(remoteVersion) <= float(localVersion):
logger.debug("plant.db 已为最新版本")
return True
logger.warning(
f"发现新版本 plant.db远程: {remoteVersion} / 本地: {localVersion}),开始更新..."
)
# 先断开数据库连接
await g_pDBService.cleanup()
return await cls.downloadPlantDBFile(remoteVersion)
@classmethod
async def downloadPlantDBFile(cls, remoteVersion: float) -> bool:
"""下载最新版 plant.db 并更新本地 version.json
Args:
remoteVersion (float): 远程版本号
Returns:
bool: 是否下载并更新成功
"""
baseUrl = Config.get_config("zhenxun_plugin_farm", "服务地址")
savePath = os.path.dirname(g_sPlantPath)
success = await cls.download(
url=f"{baseUrl.rstrip('/')}:8998/file/plant.db",
savePath=savePath,
fileName="plantTemp.db",
)
if not success:
return False
# 重命名为 sign_in.json
g_pToolManager.renameFile(f"{savePath}/plantTemp.db", "plant.db")
versionPath = os.path.join(savePath, "version.json")
try:
with open(versionPath, "w", encoding="utf-8") as f:
json.dump({"version": remoteVersion}, f)
logger.debug("版本文件已更新")
except Exception as e:
logger.warning(f"写入版本文件失败: {e}")
return False
await g_pDBService.plant.init()
await g_pDBService.plant.downloadPlant()
return True
g_pRequestManager = CRequestManager()

BIN
resource/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

BIN
resource/db/plant-test.db Normal file

Binary file not shown.

BIN
resource/db/plant.db Normal file

Binary file not shown.

1
resource/db/version.json Normal file
View File

@ -0,0 +1 @@
{"version": 0.41}

75
resource/html/help.html Normal file
View File

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
<style>
body {
background-color: #ffe4e9;
font-family: "Microsoft YaHei", sans-serif;
margin: 0;
padding: 0;
}
.container {
display: flex;
justify-content: center;
padding: 40px 20px;
}
.content-box {
background-color: #fff0f5;
border-radius: 24px;
padding: 30px;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
width: 900px;
}
.title {
text-align: center;
font-size: 32px;
font-weight: bold;
margin-bottom: 40px;
}
table {
width: 100%;
border-collapse: collapse;
}
th {
background-color: #ffb6c1;
font-weight: bold;
padding: 12px;
text-align: center;
}
td {
padding: 12px;
text-align: center;
}
tr:not(:last-child) {
border-bottom: 1px solid #ddd;
}
</style>
</head>
<body>
<div class="container">
<div class="content-box">
<div class="title">{{ title }}</div>
<table>
<thead>
<tr>
<th>指令</th>
<th>描述</th>
<th>Tip</th>
</tr>
</thead>
<tbody>
{% for entry in data %}
<tr>
<td>{{ entry.command }}</td>
<td>{{ entry.description }}</td>
<td>{{ entry.tip }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
resource/plant/三七/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
resource/plant/三七/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
resource/plant/三七/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
resource/plant/三七/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
resource/plant/三七/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
resource/plant/丝瓜/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
resource/plant/丝瓜/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
resource/plant/丝瓜/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
resource/plant/丝瓜/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
resource/plant/丝瓜/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 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: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 841 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
resource/plant/冬瓜/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
resource/plant/冬瓜/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
resource/plant/冬瓜/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
resource/plant/冬瓜/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
resource/plant/冬瓜/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
resource/plant/南瓜/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
resource/plant/南瓜/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
resource/plant/南瓜/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
resource/plant/南瓜/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
resource/plant/南瓜/5.png Normal file

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: 795 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

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