Compare commits

...

22 Commits
v1.4 ... 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
492 changed files with 2546 additions and 954 deletions

197
.gitignore vendored
View File

@ -1,3 +1,196 @@
__pycache__
/config/sign_in.json
./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

@ -8,6 +8,9 @@
<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>
@ -31,6 +34,7 @@
## 目录
- [真寻农场(zhenxun\_plugin\_farm)](#真寻农场zhenxun_plugin_farm)
- [目录](#目录)
- [农场界面](#农场界面)
- [如何安装](#如何安装)
- [使用指令](#使用指令)
- [更新日志(详细)](#更新日志详细)
@ -43,6 +47,12 @@
---
## 农场界面
![农场界面](./resource/1.png)
---
## 如何安装
方法一(推荐):在小真寻后台的插件商店下载即可<br>
@ -66,35 +76,34 @@
| 铲除 | 铲除荒废作物 | |
| 我的作物 | 你的作物 | |
| 出售作物 [作物名称] [数量] | 从仓库里向系统售卖作物 | 不填写作物名将售卖仓库种全部作物 填作物名不填数量将指定作物全部出售 |
| @美波理 偷菜 | 偷别人的菜 | 每人每天只能偷5次 |
| 偷菜 @美波理 | 偷别人的菜 | 每人每天只能偷5次 |
| 购买农场币 | 将真寻金币兑换成农场币 | 兑换比例默认为1:2 手续费默认20% |
| 更改农场名 [新的农场名] | 改名 | 农场名称无法存储特殊字符 |
| 农场签到 | 签到 | 需要注意,该项会从服务器拉取签到数据 |
| 土地升级 [地块ID] | 将土地升级,带来收益提升 | 如果土地升级时,土地有播种作物,那么将直接成熟 |
---
## 更新日志[(详细)](./log/log.md)
用户方面
---
- 新增种子商店筛选功能如果没有BUG的话后续我的种子、我的作物等也会加入筛选功能
- 新增签到功能(测试
- 新增农场详述功能,能通过该功能更加详细的观看农场数据
- 修复了迁移旧数据库无法正常迁移的BUG
- 修复了偷菜会导致偷自己的BUG
- 修正了作物阶段绘制不正确的BUG
- 新增土地升级指令,快来将土地升级至红土地、黑土地、金土地吧
- 新增查漏补缺作物资源功能,自动更新最新作物和作物资源文件
- 定时更新签到文件、作物资源从00:30调整至04:30
- 修正了部分土地资源错误的情况
- 修正了部分文本信息错误的情况
代码方面
---
- 修正了多阶段作物成长素材计算逻辑
- 修正了作物数据库字段错乱的问题
- 作物新增offset字段用于以后偏移坐标和大小(尚未启用,该模式有商议
---
- 修正部分事件连接机制
- 修正网络请求端口
## 待办事宜 `Todo` 列表
- [x] 完善我的农场图片,例如左上角显示用户数据
- [ ] 完善升级数据、作物数据、作物图片
- [x] 签到功能
- [x] 在线更新作物信息
- [ ] 添加渔场功能
- [ ] 增加活动、交易行功能
- [ ] 增加交易行总行功能

View File

@ -9,6 +9,7 @@ from zhenxun.utils.message import MessageUtils
from .command import diuse_farm, diuse_register, reclamation
from .database.database import g_pSqlManager
from .dbService import g_pDBService
from .event.event import g_pEventManager
from .farm.farm import g_pFarmManager
from .farm.shop import g_pShopManager
from .json import g_pJsonManager
@ -22,6 +23,7 @@ __plugin_meta__ = PluginMetadata(
指令
at 开通农场
我的农场
农场详述
我的农场币
种子商店 [筛选关键字] [页数] or [页数]
购买种子 [作物/种子名称] [数量]
@ -36,10 +38,11 @@ __plugin_meta__ = PluginMetadata(
购买农场币 [数量] 数量为消耗金币的数量
更改农场名 [新农场名]
农场签到
土地升级 [地块ID]通过农场详述获取
""".strip(),
extra=PluginExtraData(
author="Art_Sakura",
version="1.4",
version="1.5.2",
commands=[Command(command="我的农场")],
menu_type="群内小游戏",
configs=[
@ -55,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",
@ -66,8 +75,8 @@ __plugin_meta__ = PluginMetadata(
value="http://diuse.work",
help="签到、交易行、活动等服务器地址",
default_value="http://diuse.work",
)
]
),
],
).to_dict(),
)
driver = get_driver()
@ -84,6 +93,10 @@ async def start():
await g_pDBService.init()
# 检查作物文件是否缺失 or 更新
await g_pRequestManager.initPlantDBFile()
# 析构函数
@driver.on_shutdown
async def shutdown():
@ -92,14 +105,10 @@ async def shutdown():
await g_pDBService.cleanup()
@scheduler.scheduled_job(
trigger="cron",
hour=0,
minute=30,
id="signInFile"
)
@scheduler.scheduled_job(trigger="cron", hour=4, minute=30, id="signInFile")
async def signInFile():
try:
await g_pJsonManager.initSignInFile()
except:
logger.info("农场签到文件下载失败!")
await g_pRequestManager.initPlantDBFile()
except Exception as e:
logger.error("农场定时检查出错", e=e)

View File

@ -1,10 +1,20 @@
import inspect
from nonebot.adapters import Event, MessageTemplate
from nonebot.adapters import Event
from nonebot.rule import to_me
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
@ -12,114 +22,55 @@ from zhenxun.configs.config import BotConfig
from zhenxun.services.log import logger
from zhenxun.utils.message import MessageUtils
from .config import g_bSignStatus
from .config import g_bSignStatus, g_sTranslation
from .dbService import g_pDBService
from .farm.farm import g_pFarmManager
from .farm.shop import g_pShopManager
from .json import g_pJsonManager
from .tool import g_pToolManager
async def isRegisteredByUid(uid: str) -> bool:
result = await g_pDBService.user.isUserExist(uid)
if not result:
await MessageUtils.build_message("尚未开通农场快at我发送 开通农场 开通吧").send()
return False
return True
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_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_pDBService.user.initUserInfoByUid(
uid=uid,
name=safe_name,
exp=0,
point=500
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(
@ -134,28 +85,35 @@ diuse_farm = on_alconna(
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("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 not await isRegisteredByUid(uid):
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="我的农场",
@ -163,16 +121,20 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("detail")
async def _(session: Uninfo):
uid = str(session.user.id)
if not await isRegisteredByUid(uid):
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()
await MessageUtils.alc_forward_msg(
[info], session.self_id, BotConfig.self_nickname
).send()
diuse_farm.shortcut(
"我的农场币",
@ -181,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_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(
"种子商店(.*?)",
@ -199,11 +165,12 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("seed-shop")
async def _(session: Uninfo, res: Match[tuple[str, ...]]):
uid = str(session.user.id)
if not await isRegisteredByUid(uid):
if not await g_pToolManager.isRegisteredByUid(uid):
return
if res.result is inspect._empty:
@ -221,7 +188,12 @@ async def _(session: Uninfo, res: Match[tuple[str, ...]]):
else:
filterKey = first
if len(raw) >= 2 and raw[1] is not None and isinstance(raw[1], str) and raw[1].isdigit():
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:
@ -231,6 +203,7 @@ async def _(session: Uninfo, res: Match[tuple[str, ...]]):
await MessageUtils.build_message(image).send()
diuse_farm.shortcut(
"购买种子(?P<name>.*?)",
command="我的农场",
@ -238,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 not await isRegisteredByUid(uid):
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="我的农场",
@ -260,16 +237,18 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("my-seed")
async def _(session: Uninfo):
uid = str(session.user.id)
if not await isRegisteredByUid(uid):
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="我的农场",
@ -277,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 not await isRegisteredByUid(uid):
if not await g_pToolManager.isRegisteredByUid(uid):
return
result = await g_pFarmManager.sowing(uid, name.result, num.result)
@ -300,16 +282,18 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("harvest")
async def _(session: Uninfo):
uid = str(session.user.id)
if not await isRegisteredByUid(uid):
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="我的农场",
@ -317,11 +301,12 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("eradicate")
async def _(session: Uninfo):
uid = str(session.user.id)
if not await isRegisteredByUid(uid):
if not await g_pToolManager.isRegisteredByUid(uid):
return
result = await g_pFarmManager.eradicate(uid)
@ -335,47 +320,55 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("my-plant")
async def _(session: Uninfo):
uid = str(session.user.id)
if not await isRegisteredByUid(uid):
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 not await isRegisteredByUid(uid):
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>.*?)",
@ -384,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 not await isRegisteredByUid(uid):
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="我的农场",
@ -401,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 not await isRegisteredByUid(uid):
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
result = await g_pDBService.user.isUserExist(tar.target)
if not result:
await MessageUtils.build_message("目标尚未开通农场快邀请ta开通吧").send()
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="我的农场",
@ -428,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 not await isRegisteredByUid(uid):
if not await g_pToolManager.isRegisteredByUid(uid):
return
result = await g_pFarmManager.buyPointByUid(uid, num.result)
@ -451,30 +491,38 @@ 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 not await isRegisteredByUid(uid):
if not await g_pToolManager.isRegisteredByUid(uid):
return
safeName = sanitize_username(name.result)
safeName = g_pToolManager.sanitize_username(name.result)
if safeName == "神秘农夫":
await MessageUtils.build_message("农场名不支持特殊符号!").send(reply_to=True)
await MessageUtils.build_message(g_sTranslation["changeName"]["error"]).send(
reply_to=True
)
return
result = await g_pDBService.user.updateUserNameByUid(uid, safeName)
if result == True:
await MessageUtils.build_message("更新农场名成功!").send(reply_to=True)
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(
"农场签到",
@ -483,16 +531,17 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("sign-in")
async def _(session: Uninfo):
uid = str(session.user.id)
if not await isRegisteredByUid(uid):
if not await g_pToolManager.isRegisteredByUid(uid):
return
#判断签到是否正常加载
# 判断签到是否正常加载
if not g_bSignStatus:
await MessageUtils.build_message("签到功能异常!").send()
await MessageUtils.build_message(g_sTranslation["signIn"]["error"]).send()
return
@ -500,39 +549,89 @@ async def _(session: Uninfo):
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"))
# 获取签到总天数
signDay = await g_pDBService.userSign.getUserSignCountByDate(
uid, toDay.strftime("%Y-%m")
)
exp, point = await g_pDBService.userSign.getUserSignRewardByDate(
uid, toDay.strftime("%Y-%m-%d")
)
message += f"签到成功!累计签到天数:{signDay}\n获得经验{exp},获得金币{point}"
message += g_sTranslation["signIn"]["success"].format(
day=signDay, exp=exp, num=point
)
reward = g_pJsonManager.m_pSign['continuou'].get(f"{signDay}", None)
reward = g_pJsonManager.m_pSign["continuou"].get(f"{signDay}", None)
if reward:
extraPoint = reward.get('point', 0)
extraExp = reward.get('exp', 0)
extraPoint = reward.get("point", 0)
extraExp = reward.get("exp", 0)
plant = reward.get('plant', {})
plant = reward.get("plant", {})
message += f"\n\n成功领取累计签到奖励:\n额外获得经验{extraExp},额外获得金币{extraPoint}"
message += g_sTranslation["signIn"]["grandTotal"].format(
exp=extraExp, num=extraPoint
)
vipPoint = reward.get('vipPoint', 0)
vipPoint = reward.get("vipPoint", 0)
if vipPoint > 0:
message += f",额外获得点券{vipPoint}"
message += g_sTranslation["signIn"]["grandTotal1"].format(num=vipPoint)
if plant:
for key, value in plant.items():
message += f"\n获得{key}种子 * {value}"
message += g_sTranslation["signIn"]["grandTotal2"].format(
name=key, num=value
)
else:
message = "签到失败!未知错误"
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="我的农场",
@ -540,11 +639,57 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("admin-up")
async def _(session: Uninfo, num: Query[int] = AlconnaQuery("num", 0)):
uid = str(session.user.id)
if not await isRegisteredByUid(uid):
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)

119
config.py
View File

@ -2,15 +2,132 @@ from pathlib import Path
from zhenxun.configs.path_config import DATA_PATH
# 签到状态
g_bSignStatus = True
# 是否处于Debug模式
g_bIsDebug = False
# 数据库文件目录
g_sDBPath = DATA_PATH / "farm_db"
# 数据库文件路径
g_sDBFilePath = DATA_PATH / "farm_db/farm.db"
# 农场资源文件目录
g_sResourcePath = Path(__file__).resolve().parent / "resource"
# 农场作物数据库
g_sPlantPath = g_sResourcePath / "db/plant.db"
# 农场配置文件目录
g_sConfigPath = Path(__file__).resolve().parent / "config"
# 农场签到文件路径
g_sSignInPath = g_sConfigPath / "sign_in.json"
g_bSignStatus = True
# 土地等级上限
g_iSoilLevelMax = 3
# 农场同一文本
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,78 +0,0 @@
{
"date": "202505",
"exp_max": 50,
"exp_min": 5,
"point_max": 2000,
"point_min": 200,
"continuou":
{
"1":
{
"point": 3000,
"exp": 20
},
"3":
{
"point": 5000,
"exp": 25,
"plant":
{
"胡萝卜": 3
}
},
"5":
{
"point": 7000,
"exp": 50,
"plant":
{
"胡萝卜": 3
}
},
"7":
{
"point": 7000,
"exp": 50,
"plant":
{
"胡萝卜": 3
}
},
"10":
{
"point": 7000,
"exp": 50,
"plant":
{
"胡萝卜": 3
}
},
"15":
{
"point": 7000,
"exp": 50,
"plant":
{
"胡萝卜": 3
}
},
"20":
{
"point": 7000,
"exp": 50,
"plant":
{
"胡萝卜": 3
}
},
"25":
{
"point": 7000,
"exp": 50,
"plant":
{
"胡萝卜": 3
}
}
}
}

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,8 +1,7 @@
import math
import os
import re
from contextlib import asynccontextmanager
import os
from pathlib import Path
import re
import aiosqlite
@ -13,9 +12,9 @@ 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)
dbPath = Path(g_sDBPath)
if dbPath and not dbPath.exists():
os.makedirs(dbPath, exist_ok=True)
@classmethod
async def cleanup(cls):
@ -46,7 +45,7 @@ class CSqlManager:
@classmethod
async def getTableInfo(cls, tableName: str) -> list:
if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', tableName):
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}")')
@ -70,7 +69,7 @@ class CSqlManager:
"""
info = await cls.getTableInfo(tableName)
existing = {col['name']: col['type'].upper() for col in info}
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", "")
@ -83,7 +82,9 @@ class CSqlManager:
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]]
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:
@ -102,11 +103,18 @@ class CSqlManager:
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}";'
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}";')
await cls.m_pDB.execute(
f'ALTER TABLE "{tmpTable}" RENAME TO "{tableName}";'
)
return True
@classmethod
@ -131,4 +139,5 @@ class CSqlManager:
logger.warning(f"数据库语句执行出错: {command}", e=e)
return False
g_pSqlManager = CSqlManager()

View File

@ -1,14 +1,13 @@
import ast
import os
import re
from contextlib import asynccontextmanager
from unittest import result
import os
import aiosqlite
from zhenxun.configs.config import Config
from zhenxun.services.log import logger
from ..config import g_bIsDebug, g_sPlantPath
from ..config import g_bIsDebug, g_sPlantPath, g_sResourcePath
from ..request import g_pRequestManager
class CPlantManager:
@ -29,7 +28,9 @@ class CPlantManager:
_ = os.path.exists(g_sPlantPath)
if g_bIsDebug:
cls.m_pDB = await aiosqlite.connect(str(g_sPlantPath.parent / "plant-test.db"))
cls.m_pDB = await aiosqlite.connect(
str(g_sPlantPath.parent / "plant-test.db")
)
else:
cls.m_pDB = await aiosqlite.connect(str(g_sPlantPath))
@ -73,7 +74,6 @@ class CPlantManager:
logger.warning(f"数据库语句执行出错: {command}", e=e)
return False
@classmethod
async def getPlantByName(cls, name: str) -> dict | None:
"""根据作物名称查询记录
@ -113,14 +113,17 @@ class CPlantManager:
if not row:
return []
phase = row[0].split(',')
phase = row[0].split(",")
seen = set()
result = []
for x in phase:
if x not in seen:
seen.add(int(x))
result.append(int(x))
num = int(x)
if num not in seen:
seen.add(num)
result.append(num)
return result
except Exception as e:
@ -146,9 +149,9 @@ class CPlantManager:
if not row:
return -1
phase = row[0].split(',')
phase = row[0].split(",")
#去重
# 去重
seen = set()
result = []
for x in phase:
@ -181,7 +184,7 @@ class CPlantManager:
if not row:
return -1
phase = row[0].split(',')
phase = row[0].split(",")
again = phase[-1] - phase[3] / 60 / 60
return again
@ -238,9 +241,53 @@ class CPlantManager:
async def listPlants(cls) -> list[dict]:
"""查询所有作物记录"""
try:
async with cls.m_pDB.execute("SELECT * FROM plant ORDER BY level") as cursor:
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

View File

@ -1,6 +1,4 @@
import math
from typing import List, Union
from unittest import result
from zhenxun.services.log import logger
@ -13,19 +11,21 @@ class CUserDB(CSqlManager):
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" #剩余偷菜次数
"uid": "TEXT PRIMARY KEY", # 用户Uid
"name": "TEXT NOT NULL", # 农场名称
"exp": "INTEGER DEFAULT 0", # 经验值
"point": "INTEGER DEFAULT 0", # 金币
"vipPoint": "INTEGER DEFAULT 0", # 点券
"soil": "INTEGER DEFAULT 3", # 解锁土地数量
"stealTime": "TEXT DEFAULT ''", # 偷菜时间字符串
"stealCount": "INTEGER DEFAULT 0", # 剩余偷菜次数
}
await cls.ensureTableSchema("user", userInfo)
@classmethod
async def initUserInfoByUid(cls, uid: str, name: str = "", exp: int = 0, point: int = 500) -> Union[bool, str]:
async def initUserInfoByUid(
cls, uid: str, name: str = "", exp: int = 0, point: int = 500
) -> bool | str:
"""初始化用户信息,包含初始偷菜时间字符串与次数
Args:
@ -35,9 +35,9 @@ class CUserDB(CSqlManager):
point (int): 农场币
Returns:
Union[bool, str]: False 表示失败字符串表示成功信息
bool | str: False 表示失败字符串表示成功信息
"""
nowStr = g_pToolManager.dateTime().date().today().strftime('%Y-%m-%d')
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)"
@ -51,11 +51,11 @@ class CUserDB(CSqlManager):
return False
@classmethod
async def getAllUsers(cls) -> List[str]:
async def getAllUsers(cls) -> list[str]:
"""获取所有用户UID列表
Returns:
List[str]: 用户UID列表
list[str]: 用户UID列表
"""
cursor = await cls.m_pDB.execute("SELECT uid FROM user")
rows = await cursor.fetchall()
@ -225,11 +225,11 @@ class CUserDB(CSqlManager):
@classmethod
async def updateUserVipPointByUid(cls, uid: str, vipPoint: int) -> bool:
"""根据用户Uid更新农场币数量
"""根据用户Uid更新点券数量
Args:
uid (str): 用户Uid
vipPoint (int): 农场币数量
vipPoint (int): 点券数量
Returns:
bool: 是否更新成功
@ -300,7 +300,8 @@ class CUserDB(CSqlManager):
uid (str): 用户Uid
Returns:
tuple[int, int, int]: (当前等级, 升至下级还需经验, 当前等级已获经验)失败返回(-1, -1, -1)
tuple[int, int, int]: 成功返回(当前等级, 升至下级还需经验, 当前等级已获经验)
失败返回(-1, -1, -1)
"""
if not uid:
return -1, -1, -1
@ -395,8 +396,7 @@ class CUserDB(CSqlManager):
return ""
try:
async with cls.m_pDB.execute(
"SELECT stealTime FROM user WHERE uid = ?", (uid,
)
"SELECT stealTime FROM user WHERE uid = ?", (uid,)
) as cursor:
row = await cursor.fetchone()
return row[0] if row and row[0] else ""
@ -451,11 +451,14 @@ class CUserDB(CSqlManager):
return -1
@classmethod
async def updateStealCountByUid(cls, uid: str, stealCount: int) -> bool:
async def updateStealCountByUid(
cls, uid: str, stealTime: str, stealCount: int
) -> bool:
"""根据用户Uid更新剩余偷菜次数
Args:
uid (str): 用户Uid
stealTime (str): 偷菜日期
stealCount (int): 新剩余偷菜次数
Returns:
@ -467,7 +470,8 @@ class CUserDB(CSqlManager):
try:
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE user SET stealCount = ? WHERE uid = ?", (stealCount, uid)
"UPDATE user SET stealTime = ?, stealCount = ? WHERE uid = ?",
(stealTime, stealCount, uid),
)
return True
except Exception as e:

View File

@ -1,5 +1,3 @@
from typing import Optional
from zhenxun.services.log import logger
from .database import CSqlManager
@ -9,16 +7,16 @@ 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)"
"uid": "TEXT NOT NULL", # 用户Uid
"item": "TEXT NOT NULL", # 物品名称
"count": "INTEGER NOT NULL DEFAULT 0", # 数量
"PRIMARY KEY": "(uid, item)",
}
await cls.ensureTableSchema("userItem", userItem)
@classmethod
async def getUserItemByName(cls, uid: str, item: str) -> Optional[int]:
async def getUserItemByName(cls, uid: str, item: str) -> int | None:
"""根据道具名称查询某一项数量
Args:
@ -32,13 +30,12 @@ class CUserItemDB(CSqlManager):
return None
try:
async with cls.m_pDB.execute(
"SELECT count FROM userItem WHERE uid = ? AND item = ?",
(uid, item)
"SELECT count FROM userItem WHERE uid = ? AND item = ?", (uid, item)
) as cursor:
row = await cursor.fetchone()
return row[0] if row else None
except Exception as e:
logger.warning(f"getUserItemByName查询失败", e=e)
logger.warning("getUserItemByName查询失败", e=e)
return None
@classmethod
@ -55,13 +52,12 @@ class CUserItemDB(CSqlManager):
return {}
try:
cursor = await cls.m_pDB.execute(
"SELECT item, count FROM userItem WHERE uid = ?",
(uid,)
"SELECT item, count FROM userItem WHERE uid = ?", (uid,)
)
rows = await cursor.fetchall()
return {row["item"]: row["count"] for row in rows}
except Exception as e:
logger.warning(f"getUserItemByUid查询失败", e=e)
logger.warning("getUserItemByUid查询失败", e=e)
return {}
@classmethod
@ -80,12 +76,11 @@ class CUserItemDB(CSqlManager):
try:
async with cls._transaction():
await cls.m_pDB.execute(
"DELETE FROM userItem WHERE uid = ? AND item = ?",
(uid, item)
"DELETE FROM userItem WHERE uid = ? AND item = ?", (uid, item)
)
return True
except Exception as e:
logger.warning(f"deleteUserItemByName失败", e=e)
logger.warning("deleteUserItemByName失败", e=e)
return False
@classmethod
@ -106,17 +101,16 @@ class CUserItemDB(CSqlManager):
async with cls._transaction():
if count <= 0:
await cls.m_pDB.execute(
"DELETE FROM userItem WHERE uid = ? AND item = ?",
(uid, item)
"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)
(count, uid, item),
)
return True
except Exception as e:
logger.warning(f"updateUserItemByName失败", e=e)
logger.warning("updateUserItemByName失败", e=e)
return False
@classmethod
@ -136,8 +130,7 @@ class CUserItemDB(CSqlManager):
try:
async with cls._transaction():
async with cls.m_pDB.execute(
"SELECT count FROM userItem WHERE uid = ? AND item = ?",
(uid, item)
"SELECT count FROM userItem WHERE uid = ? AND item = ?", (uid, item)
) as cursor:
row = await cursor.fetchone()
@ -146,20 +139,20 @@ class CUserItemDB(CSqlManager):
if newCount <= 0:
await cls.m_pDB.execute(
"DELETE FROM userItem WHERE uid = ? AND item = ?",
(uid, item)
(uid, item),
)
else:
await cls.m_pDB.execute(
"UPDATE userItem SET count = ? WHERE uid = ? AND item = ?",
(newCount, uid, item)
(newCount, uid, item),
)
else:
if count > 0:
await cls.m_pDB.execute(
"INSERT INTO userItem (uid, item, count) VALUES (?, ?, ?)",
(uid, item, count)
(uid, item, count),
)
return True
except Exception as e:
logger.warning(f"addUserItemByUid失败", e=e)
logger.warning("addUserItemByUid失败", e=e)
return False

View File

@ -1,5 +1,3 @@
from typing import Dict, Optional
from zhenxun.services.log import logger
from .database import CSqlManager
@ -9,10 +7,11 @@ class CUserPlantDB(CSqlManager):
@classmethod
async def initDB(cls):
userPlant = {
"uid": "TEXT NOT NULL", #用户Uid
"plant": "TEXT NOT NULL", #作物名称
"count": "INTEGER NOT NULL DEFAULT 0", #数量
"PRIMARY KEY": "(uid, plant)"
"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)
@ -31,33 +30,33 @@ class CUserPlantDB(CSqlManager):
"""
try:
async with cls._transaction():
#检查是否已存在该作物
# 检查是否已存在该作物
async with cls.m_pDB.execute(
"SELECT count FROM userPlant WHERE uid = ? AND plant = ?",
(uid, 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)
(new_count, uid, plant),
)
else:
#如果作物不存在,则插入新记录
# 如果作物不存在,则插入新记录
await cls.m_pDB.execute(
"INSERT INTO userPlant (uid, plant, count) VALUES (?, ?, ?)",
(uid, plant, count)
(uid, plant, count),
)
return True
except Exception as e:
logger.warning(f"addUserPlantByUid 失败!", e=e)
logger.warning("addUserPlantByUid 失败!", e=e)
return False
@classmethod
async def getUserPlantByUid(cls, uid: str) -> Dict[str, int]:
async def getUserPlantByUid(cls, uid: str) -> dict[str, int]:
"""根据用户uid获取全部作物信息
Args:
@ -67,14 +66,13 @@ class CUserPlantDB(CSqlManager):
Dict[str, int]: 作物名称和数量
"""
cursor = await cls.m_pDB.execute(
"SELECT plant, count FROM userPlant WHERE uid=?",
(uid,)
"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]:
async def getUserPlantByName(cls, uid: str, plant: str) -> int | None:
"""根据作物名称获取用户的作物数量
Args:
@ -86,15 +84,35 @@ class CUserPlantDB(CSqlManager):
"""
try:
async with cls.m_pDB.execute(
"SELECT count FROM userPlant WHERE uid = ? AND plant = ?",
(uid, plant)
"SELECT count FROM userPlant WHERE uid = ? AND plant = ?", (uid, plant)
) as cursor:
row = await cursor.fetchone()
return row[0] if row else None
except Exception as e:
logger.warning(f"getUserPlantByName 查询失败!", e=e)
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 表中某个作物的数量
@ -114,11 +132,55 @@ class CUserPlantDB(CSqlManager):
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE userPlant SET count = ? WHERE uid = ? AND plant = ?",
(count, uid, plant)
(count, uid, plant),
)
return True
except Exception as e:
logger.warning(f"updateUserPlantByName失败", e=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
@ -135,10 +197,9 @@ class CUserPlantDB(CSqlManager):
try:
async with cls._transaction():
await cls.m_pDB.execute(
"DELETE FROM userPlant WHERE uid = ? AND plant = ?",
(uid, plant)
"DELETE FROM userPlant WHERE uid = ? AND plant = ?", (uid, plant)
)
return True
except Exception as e:
logger.warning(f"deleteUserPlantByName 失败!", e=e)
logger.warning("deleteUserPlantByName 失败!", e=e)
return False

View File

@ -1,5 +1,3 @@
from typing import Optional
from zhenxun.services.log import logger
from .database import CSqlManager
@ -9,10 +7,10 @@ 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)"
"uid": "TEXT NOT NULL", # 用户Uid
"seed": "TEXT NOT NULL", # 种子名称
"count": "INTEGER NOT NULL DEFAULT 0", # 数量
"PRIMARY KEY": "(uid, seed)",
}
await cls.ensureTableSchema("userSeed", userSeed)
@ -32,8 +30,7 @@ class CUserSeedDB(CSqlManager):
try:
async with cls._transaction():
async with cls.m_pDB.execute(
"SELECT count FROM userSeed WHERE uid = ? AND seed = ?",
(uid, seed)
"SELECT count FROM userSeed WHERE uid = ? AND seed = ?", (uid, seed)
) as cursor:
row = await cursor.fetchone()
@ -41,23 +38,22 @@ class CUserSeedDB(CSqlManager):
newCount = row[0] + count
await cls.m_pDB.execute(
"UPDATE userSeed SET count = ? WHERE uid = ? AND seed = ?",
(newCount, uid, seed)
(newCount, uid, seed),
)
else:
newCount = count
await cls.m_pDB.execute(
"INSERT INTO userSeed (uid, seed, count) VALUES (?, ?, ?)",
(uid, seed, count)
(uid, seed, count),
)
if newCount <= 0:
await cls.m_pDB.execute(
"DELETE FROM userSeed WHERE uid = ? AND seed = ?",
(uid, seed)
"DELETE FROM userSeed WHERE uid = ? AND seed = ?", (uid, seed)
)
return True
except Exception as e:
logger.warning(f"addUserSeedByUid 失败!", e=e)
logger.warning("addUserSeedByUid 失败!", e=e)
return False
@classmethod
@ -72,7 +68,7 @@ class CUserSeedDB(CSqlManager):
else:
await cls.m_pDB.execute(
"INSERT INTO userSeed (uid, seed, count) VALUES (?, ?, ?)",
(uid, seed, newCount)
(uid, seed, newCount),
)
if newCount <= 0:
@ -80,12 +76,11 @@ class CUserSeedDB(CSqlManager):
return True
except Exception as e:
logger.warning(f"_addUserSeedByUid 失败!", e=e)
logger.warning("_addUserSeedByUid 失败!", e=e)
return False
@classmethod
async def getUserSeedByName(cls, uid: str, seed: str) -> Optional[int]:
async def getUserSeedByName(cls, uid: str, seed: str) -> int | None:
"""根据种子名称获取种子数量
Args:
@ -98,13 +93,12 @@ class CUserSeedDB(CSqlManager):
try:
async with cls.m_pDB.execute(
"SELECT count FROM userSeed WHERE uid = ? AND seed = ?",
(uid, seed)
"SELECT count FROM userSeed WHERE uid = ? AND seed = ?", (uid, seed)
) as cursor:
row = await cursor.fetchone()
return row[0] if row else None
except Exception as e:
logger.warning(f"getUserSeedByName 查询失败!", e=e)
logger.warning("getUserSeedByName 查询失败!", e=e)
return None
@classmethod
@ -119,8 +113,7 @@ class CUserSeedDB(CSqlManager):
"""
cursor = await cls.m_pDB.execute(
"SELECT seed, count FROM userSeed WHERE uid=?",
(uid,)
"SELECT seed, count FROM userSeed WHERE uid=?", (uid,)
)
rows = await cursor.fetchall()
return {row["seed"]: row["count"] for row in rows}
@ -144,11 +137,11 @@ class CUserSeedDB(CSqlManager):
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE userSeed SET count = ? WHERE uid = ? AND seed = ?",
(count, uid, seed)
(count, uid, seed),
)
return True
except Exception as e:
logger.warning(f"updateUserSeedByName失败", e=e)
logger.warning("updateUserSeedByName失败", e=e)
return False
@classmethod
@ -170,11 +163,11 @@ class CUserSeedDB(CSqlManager):
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE userSeed SET count = ? WHERE uid = ? AND seed = ?",
(count, uid, seed)
(count, uid, seed),
)
return True
except Exception as e:
logger.warning(f"updateUserSeedByName失败", e=e)
logger.warning("updateUserSeedByName失败", e=e)
return False
@classmethod
@ -191,12 +184,11 @@ class CUserSeedDB(CSqlManager):
try:
async with cls._transaction():
await cls.m_pDB.execute(
"DELETE FROM userSeed WHERE uid = ? AND seed = ?",
(uid, seed)
"DELETE FROM userSeed WHERE uid = ? AND seed = ?", (uid, seed)
)
return True
except Exception as e:
logger.warning(f"deleteUserSeedByName 删除失败!", e=e)
logger.warning("deleteUserSeedByName 删除失败!", e=e)
return False
@classmethod
@ -212,10 +204,9 @@ class CUserSeedDB(CSqlManager):
"""
try:
await cls.m_pDB.execute(
"DELETE FROM userSeed WHERE uid = ? AND seed = ?",
(uid, seed)
"DELETE FROM userSeed WHERE uid = ? AND seed = ?", (uid, seed)
)
return True
except Exception as e:
logger.warning(f"deleteUserSeedByName 删除失败!", e=e)
logger.warning("deleteUserSeedByName 删除失败!", e=e)
return False

View File

@ -1,7 +1,6 @@
import calendar
from datetime import timedelta
import random
from datetime import date, datetime, timedelta
from typing import Optional
from zhenxun.services.log import logger
from zhenxun.utils._build_image import BuildImage
@ -16,27 +15,27 @@ from .database import CSqlManager
class CUserSignDB(CSqlManager):
@classmethod
async def initDB(cls):
#userSignLog 表结构,每条为一次签到事件
# userSignLog 表结构,每条为一次签到事件
userSignLog = {
"uid": "TEXT NOT NULL", #用户ID
"signDate": "DATE NOT NULL", #签到日期
"isSupplement": "TINYINT NOT NULL DEFAULT 0", #是否补签
"exp": "INT NOT NULL DEFAULT 0", #当天签到经验
"point": "INT NOT NULL DEFAULT 0", #当天签到金币
"createdAt": "DATETIME NOT NULL DEFAULT (datetime(CURRENT_TIMESTAMP, 'localtime'))",#创建时间
"PRIMARY KEY": "(uid, signDate)"
"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 表结构,每用户一行用于缓存签到状态
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'))"#更新时间
"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)
@ -57,15 +56,15 @@ class CUserSignDB(CSqlManager):
async with cls._transaction():
async with cls.m_pDB.execute(
"SELECT exp, point FROM userSignLog WHERE uid=? AND signDate=?",
(uid, date)
(uid, date),
) as cursor:
row = await cursor.fetchone()
if row is None:
return 0, 0
exp = row['exp']
point = row['point']
exp = row["exp"]
point = row["point"]
return exp, point
except Exception as e:
@ -114,7 +113,7 @@ class CUserSignDB(CSqlManager):
return False
@classmethod
async def sign(cls, uid: str, signDate: str = '') -> int:
async def sign(cls, uid: str, signDate: str = "") -> int:
"""签到
Args:
@ -151,20 +150,34 @@ class CUserSignDB(CSqlManager):
async with cls._transaction():
await cls.m_pDB.execute(
"INSERT INTO userSignLog (uid, signDate, isSupplement, exp, point) VALUES (?, ?, ?, ?, ?)",
(uid, signDate, isSupplement, exp, point)
(uid, signDate, isSupplement, exp, point),
)
cursor = await cls.m_pDB.execute("SELECT * FROM userSignSummary WHERE uid=?", (uid,))
cursor = await cls.m_pDB.execute(
"SELECT * FROM userSignSummary WHERE uid=?", (uid,)
)
row = await cursor.fetchone()
currentMonth = signDate[:7]
if row:
monthSignDays = row['monthSignDays'] + 1 if row['currentMonth'] == currentMonth else 1
totalSignDays = row['totalSignDays']
lastDate = row['lastSignDate']
prevDate = (g_pToolManager.dateTime().strptime(signDate, "%Y-%m-%d") - timedelta(days=1)).strftime("%Y-%m-%d")
continuousDays = row['continuousDays'] + 1 if lastDate == prevDate else 1
supplementCount = row['supplementCount'] + 1 if isSupplement else row['supplementCount']
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
@ -176,29 +189,42 @@ class CUserSignDB(CSqlManager):
supplementCount=?
WHERE uid=?
""",
(currentMonth, monthSignDays, signDate, continuousDays, supplementCount, uid)
(
currentMonth,
monthSignDays,
signDate,
continuousDays,
supplementCount,
uid,
),
)
else:
totalSignDays = 1
monthSignDays = 1
await cls.m_pDB.execute(
"""
INSERT INTO userSignSummary
(uid, totalSignDays, currentMonth, monthSignDays, lastSignDate, continuousDays, supplementCount)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(uid, 1, currentMonth, 1, signDate, 1, 1 if isSupplement else 0)
(
uid,
1,
currentMonth,
monthSignDays,
signDate,
1,
1 if isSupplement else 0,
),
)
#计算累签奖励
reward = g_pJsonManager.m_pSign['continuou'].get(f"{totalSignDays}", None)
# 计算累签奖励
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', {})
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)
@ -206,7 +232,7 @@ class CUserSignDB(CSqlManager):
if g_bIsDebug:
exp += 9999
#向数据库更新
# 向数据库更新
currentExp = await g_pDBService.user.getUserExpByUid(uid)
await g_pDBService.user.updateUserExpByUid(uid, currentExp + exp)
@ -215,7 +241,9 @@ class CUserSignDB(CSqlManager):
if vipPoint > 0:
currentVipPoint = await g_pDBService.user.getUserVipPointByUid(uid)
await g_pDBService.user.updateUserVipPointByUid(uid, currentVipPoint + vipPoint)
await g_pDBService.user.updateUserVipPointByUid(
uid, currentVipPoint + vipPoint
)
return 1
except Exception as e:
@ -224,7 +252,7 @@ class CUserSignDB(CSqlManager):
@classmethod
async def drawSignCalendarImage(cls, uid: str, year: int, month: int):
#绘制签到图,自动提取数据库中该用户该月的签到天数
# 绘制签到图,自动提取数据库中该用户该月的签到天数
cellSize = 80
padding = 40
titleHeight = 80
@ -242,7 +270,7 @@ class CUserSignDB(CSqlManager):
sql = "SELECT signDate FROM userSignLog WHERE uid=? AND signDate LIKE ?"
async with cls.m_pDB.execute(sql, (uid, f"{monthStr}-%")) as cursor:
rows = await cursor.fetchall()
signedDays = set(int(r[0][-2:]) for r in rows if r[0][-2:].isdigit())
signedDays = {int(r[0][-2:]) for r in rows if r[0][-2:].isdigit()}
except Exception as e:
logger.warning("绘制签到图时数据库查询失败", e=e)
signedDays = set()

View File

@ -1,10 +1,9 @@
from typing import Optional
import math
from zhenxun.services.log import logger
from ..config import g_bIsDebug
from ..dbService import g_pDBService
from ..json import g_pJsonManager
from ..tool import g_pToolManager
from .database import CSqlManager
@ -14,17 +13,18 @@ class CUserSoilDB(CSqlManager):
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", #收获次数
"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)",
}
@ -45,35 +45,59 @@ class CUserSoilDB(CSqlManager):
if not soilInfo:
return
plantInfo = await g_pDBService.plant.getPlantByName(soilInfo['plantName'])
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'])
phaseList = await g_pDBService.plant.getPlantPhaseByName(soilInfo["plantName"])
if currentTime >= soilInfo['matureTime']:
if currentTime >= soilInfo["matureTime"]:
return
currentStage = 0
elapsedTime = currentTime - soilInfo['plantTime']
elapsedTime = currentTime - soilInfo["plantTime"]
currentStage = currentStage = sum(1 for thr in phaseList if elapsedTime >= thr)
for idx, thr in enumerate(phaseList, start=1):
if elapsedTime < thr:
currentStage = idx
break
t = int(soilInfo["plantTime"]) - phaseList[currentStage]
s = int(soilInfo["matureTime"]) - phaseList[currentStage]
t = int(soilInfo['plantTime']) - phaseList[currentStage]
s = int(soilInfo['matureTime']) - phaseList[currentStage]
await cls.updateUserSoilFields(
uid, soilIndex, {"plantTime": t, "matureTime": s}
)
await cls.updateUserSoilFields(uid, soilIndex,
{
"plantTime": t,
"matureTime": s
})
logger.debug(
f"当前阶段{currentStage}, 阶段时间{phaseList[currentStage]}, 播种时间{t}, 收获时间{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:
@ -100,7 +124,7 @@ class CUserSoilDB(CSqlManager):
Returns:
bool: 如果旧表不存在则返回 False否则迁移并删除后返回 True
"""
#检查旧表是否存在
# 检查旧表是否存在
cursor = await cls.m_pDB.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='soil'"
)
@ -117,9 +141,14 @@ class CUserSoilDB(CSqlManager):
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])
@ -154,8 +183,8 @@ class CUserSoilDB(CSqlManager):
INSERT INTO userSoil
(uid, soilIndex, plantName, plantTime, matureTime,
soilLevel, wiltStatus, fertilizerStatus, bugStatus,
weedStatus, waterStatus, harvestCount)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
weedStatus, waterStatus, harvestCount, isSoilPlanted)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
""",
(
soilInfo["uid"],
@ -170,6 +199,7 @@ class CUserSoilDB(CSqlManager):
soilInfo.get("weedStatus", 0),
soilInfo.get("waterStatus", 0),
soilInfo.get("harvestCount", 0),
soilInfo.get("isSoilPlanted", 0),
),
)
@ -184,31 +214,32 @@ class CUserSoilDB(CSqlManager):
None
"""
await cls.m_pDB.execute(
"""
"""
INSERT INTO userSoil
(uid, soilIndex, plantName, plantTime, matureTime,
soilLevel, wiltStatus, fertilizerStatus, bugStatus,
weedStatus, waterStatus, harvestCount)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
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["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) -> Optional[dict]:
async def getUserSoil(cls, uid: str, soilIndex: int) -> dict:
"""获取指定用户某块土地的详细信息
Args:
@ -216,7 +247,7 @@ class CUserSoilDB(CSqlManager):
soilIndex (int): 土地索引
Returns:
Optional[dict]: 记录存在返回字段-值字典否则返回 None
dict: 记录存在返回字段-值字典否则返回 None
"""
async with cls._transaction():
cursor = await cls.m_pDB.execute(
@ -225,12 +256,12 @@ class CUserSoilDB(CSqlManager):
)
row = await cursor.fetchone()
if not row:
return None
return {}
columns = [description[0] for description in cursor.description]
return dict(zip(columns, row))
@classmethod
async def _getUserSoil(cls, uid: str, soilIndex: int) -> Optional[dict]:
async def _getUserSoil(cls, uid: str, soilIndex: int) -> dict | None:
"""获取指定用户某块土地的详细信息
Args:
@ -238,7 +269,7 @@ class CUserSoilDB(CSqlManager):
soilIndex (int): 土地索引
Returns:
Optional[dict]: 记录存在返回字段-值字典否则返回 None
dict | None: 记录存在返回字段-值字典否则返回 None
"""
cursor = await cls.m_pDB.execute(
"SELECT * FROM userSoil WHERE uid = ? AND soilIndex = ?",
@ -250,6 +281,25 @@ class CUserSoilDB(CSqlManager):
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):
"""更新指定用户土地的单个字段
@ -288,7 +338,9 @@ class CUserSoilDB(CSqlManager):
)
@classmethod
async def updateUserSoilFields(cls, uid: str, soilIndex: int, updates: dict) -> bool:
async def updateUserSoilFields(
cls, uid: str, soilIndex: int, updates: dict
) -> bool:
"""批量更新指定用户土地的多个字段
Args:
@ -299,11 +351,19 @@ class CUserSoilDB(CSqlManager):
Returns:
bool: 如果无可更新字段则返回 False否则更新成功返回 True
"""
#允许更新的列白名单
# 允许更新的列白名单
allowedFields = {
"plantName", "plantTime", "matureTime", "soilLevel",
"wiltStatus", "fertilizerStatus", "bugStatus",
"weedStatus", "waterStatus", "harvestCount"
"plantName",
"plantTime",
"matureTime",
"soilLevel",
"wiltStatus",
"fertilizerStatus",
"bugStatus",
"weedStatus",
"waterStatus",
"harvestCount",
"isSoilPlanted",
}
setClauses = []
values = []
@ -316,7 +376,7 @@ class CUserSoilDB(CSqlManager):
return False
values.extend([uid, soilIndex])
sql = f'UPDATE userSoil SET {", ".join(setClauses)} WHERE uid = ? AND soilIndex = ?'
sql = f"UPDATE userSoil SET {', '.join(setClauses)} WHERE uid = ? AND soilIndex = ?"
try:
async with cls._transaction():
@ -357,23 +417,6 @@ class CUserSoilDB(CSqlManager):
"DELETE FROM userSoil WHERE uid = ? AND soilIndex = ?", (uid, soilIndex)
)
@classmethod
async def isSoilPlanted(cls, uid: str, soilIndex: int) -> bool:
"""判断指定用户的指定土地是否已种植
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
Returns:
bool: 如果 plantName 不为空且 plantTime 大于 0则视为已种植返回 True否则 False
"""
soilInfo = await cls.getUserSoil(uid, soilIndex)
if not soilInfo:
return False
return bool(soilInfo.get("plantName")) and soilInfo.get("plantTime", 0) > 0
@classmethod
async def sowingByPlantName(cls, uid: str, soilIndex: int, plantName: str) -> bool:
"""播种指定作物到用户土地区
@ -386,23 +429,30 @@ class CUserSoilDB(CSqlManager):
Returns:
bool: 播种成功返回 True否则返回 False
"""
#校验土地区是否已种植
soilRecord = await cls.getUserSoil(uid, soilIndex)
if soilRecord and soilRecord.get("plantName"):
# 校验土地区是否已种植
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())
matureTs = nowTs + int(plantCfg.get("time", 0)) * 3600
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 = soilRecord or {}
prev = soilInfo or {}
await cls._deleteUserSoil(uid, soilIndex)
await cls._insertUserSoil(
{
@ -412,17 +462,18 @@ class CUserSoilDB(CSqlManager):
"plantTime": nowTs,
"matureTime": matureTs,
"soilLevel": prev.get("soilLevel", 0),
"wiltStatus": prev.get("wiltStatus", 0),
"fertilizerStatus": prev.get("fertilizerStatus", 0),
"bugStatus": prev.get("bugStatus", 0),
"weedStatus": prev.get("weedStatus", 0),
"waterStatus": prev.get("waterStatus", 0),
"harvestCount": 0
"wiltStatus": 0,
"fertilizerStatus": 0,
"bugStatus": 0,
"weedStatus": 0,
"waterStatus": 0,
"harvestCount": 0,
"isSoilPlanted": 1,
}
)
return True
except Exception as e:
logger.error(f"播种失败!", e=e)
logger.error("播种失败!", e=e)
return False
@classmethod
@ -451,3 +502,90 @@ class CUserSoilDB(CSqlManager):
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

View File

@ -7,17 +7,19 @@ 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)"
"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:
async def addStealRecord(
cls, uid: str, soilIndex: int, stealerUid: str, stealCount: int, stealTime: int
) -> bool:
"""添加偷菜记录
Args:
@ -34,7 +36,7 @@ class CUserStealDB(CSqlManager):
async with cls._transaction():
await cls.m_pDB.execute(
'INSERT INTO "userSteal"(uid, soilIndex, stealerUid, stealCount, stealTime) VALUES(?, ?, ?, ?, ?);',
(uid, soilIndex, stealerUid, stealCount, stealTime)
(uid, soilIndex, stealerUid, stealCount, stealTime),
)
return True
except Exception as e:
@ -55,7 +57,7 @@ class CUserStealDB(CSqlManager):
async with cls._transaction():
cursor = await cls.m_pDB.execute(
'SELECT soilIndex, stealerUid, stealCount, stealTime FROM "userSteal" WHERE uid=?;',
(uid,)
(uid,),
)
rows = await cursor.fetchall()
return [
@ -64,7 +66,7 @@ class CUserStealDB(CSqlManager):
"soilIndex": row[0],
"stealerUid": row[1],
"stealCount": row[2],
"stealTime": row[3]
"stealTime": row[3],
}
for row in rows
]
@ -87,7 +89,7 @@ class CUserStealDB(CSqlManager):
async with cls._transaction():
cursor = await cls.m_pDB.execute(
'SELECT stealerUid, stealCount, stealTime FROM "userSteal" WHERE uid=? AND soilIndex=?;',
(uid, soilIndex)
(uid, soilIndex),
)
rows = await cursor.fetchall()
return [
@ -96,7 +98,7 @@ class CUserStealDB(CSqlManager):
"soilIndex": soilIndex,
"stealerUid": row[0],
"stealCount": row[1],
"stealTime": row[2]
"stealTime": row[2],
}
for row in rows
]
@ -119,10 +121,10 @@ class CUserStealDB(CSqlManager):
async with cls._transaction():
cursor = await cls.m_pDB.execute(
'SELECT SUM(stealCount) FROM "userSteal" WHERE uid=? AND soilIndex=?;',
(uid, soilIndex)
(uid, soilIndex),
)
row = await cursor.fetchone()
return row[0] or 0 # type: ignore
return row[0] or 0 # type: ignore
except Exception as e:
logger.warning("计算总偷菜数量失败", e=e)
return 0
@ -142,10 +144,10 @@ class CUserStealDB(CSqlManager):
async with cls._transaction():
cursor = await cls.m_pDB.execute(
'SELECT COUNT(DISTINCT stealerUid) FROM "userSteal" WHERE uid=? AND soilIndex=?;',
(uid, soilIndex)
(uid, soilIndex),
)
row = await cursor.fetchone()
return row[0] or 0 # type: ignore
return row[0] or 0 # type: ignore
except Exception as e:
logger.warning("计算偷菜者数量失败", e=e)
return 0
@ -166,7 +168,7 @@ class CUserStealDB(CSqlManager):
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)
(uid, soilIndex, stealerUid),
)
row = await cursor.fetchone()
return bool(row)
@ -175,7 +177,9 @@ class CUserStealDB(CSqlManager):
return False
@classmethod
async def updateStealRecord(cls, uid: str, soilIndex: int, stealerUid: str, stealCount: int, stealTime: int) -> bool:
async def updateStealRecord(
cls, uid: str, soilIndex: int, stealerUid: str, stealCount: int, stealTime: int
) -> bool:
"""更新偷菜记录的数量和时间
Args:
@ -192,7 +196,7 @@ class CUserStealDB(CSqlManager):
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)
(stealCount, stealTime, uid, soilIndex, stealerUid),
)
return True
except Exception as e:
@ -214,7 +218,7 @@ class CUserStealDB(CSqlManager):
async with cls._transaction():
await cls.m_pDB.execute(
'DELETE FROM "userSteal" WHERE uid=? AND soilIndex=?;',
(uid, soilIndex)
(uid, soilIndex),
)
return True
except Exception as e:

View File

@ -1,6 +1,3 @@
from typing import Optional
class CDBService:
@classmethod
async def init(cls):
@ -37,11 +34,12 @@ class CDBService:
cls.userSign = CUserSignDB()
await cls.userSign.initDB()
#迁移旧数据库
# 迁移旧数据库
await cls.userSoil.migrateOldFarmData()
@classmethod
async def cleanup(cls):
await cls.plant.cleanup()
g_pDBService = CDBService()

View File

@ -1,103 +1,123 @@
import asyncio
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 = [] #只触发一次的槽函数列表
self._slots = []
self._onceSlots = []
def connect(self, slot, priority=0):
if callable(slot) and not any(s[0] == slot for s in self._slots):
self._slots.append((slot, priority))
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 connectOnce(self, slot, priority=0):
if callable(slot) and not any(s[0] == slot for s in self._onceSlots):
self._onceSlots.append((slot, priority))
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, slot):
self._slots = [s for s in self._slots if s[0] != slot]
self._onceSlots = [s for s in self._onceSlots if s[0] != slot]
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:
startTime = time.time()
start = time.time()
try:
if asyncio.iscoroutinefunction(slot):
if inspect.iscoroutinefunction(slot):
await slot(*args, **kwargs)
else:
slot(*args, **kwargs)
duration = (time.time() - startTime) * 1000
logger.debug(f"事件槽 {slot.__name__} 执行完成,耗时 {duration:.2f} ms")
logger.debug(
f"【真寻农场】事件槽 {slot.__name__} 执行完成,耗时 {(time.time() - start) * 1000:.2f} ms"
)
except Exception as e:
logger.warning(f"事件槽 {slot.__name__} 触发异常: {e}")
class FarmEventManager:
def __init__(self):
m_beforePlant = Signal()
"""播种前信号
self.m_beforePlant = Signal()
"""播种前信号
Args:
uid (str): 用户Uid
name (str): 播种种子名称
num (int): 播种数量
"""
Args:
uid (str): 用户Uid
name (str): 播种种子名称
num (int): 播种数量
"""
m_afterPlant = Signal()
"""播种后信号 每块地播种都会触发该信号
self.m_afterPlant = Signal()
"""播种后信号 每块地播种都会触发该信号
Args:
uid (str): 用户Uid
name (str): 播种种子名称
soilIndex (int): 播种地块索引 从1开始
"""
Args:
uid (str): 用户Uid
name (str): 播种种子名称
soilIndex (int): 播种地块索引 从1开始
"""
m_beforeHarvest = Signal()
"""收获前信号
self.m_beforeHarvest = Signal()
"""收获前信号
Args:
uid (str): 用户Uid
"""
Args:
uid (str): 用户Uid
"""
m_afterHarvest = Signal()
"""收获后信号 每块地收获都会触发该信号
self.m_afterHarvest = Signal()
"""收获后信号 每块地收获都会触发该信号
Args:
uid (str): 用户Uid
name (str): 收获作物名称
num (int): 收获数量
soilIndex (int): 收获地块索引 从1开始
"""
Args:
uid (str): 用户Uid
name (str): 收获作物名称
num (int): 收获数量
soilIndex (int): 收获地块索引 从1开始
"""
m_beforeEradicate = Signal()
"""铲除前信号
self.m_beforeEradicate = Signal()
"""铲除前信号
Args:
uid (str): 用户Uid
"""
Args:
uid (str): 用户Uid
"""
m_afterEradicate = Signal()
"""铲除后信号 每块地铲除都会触发该信号
self.m_afterEradicate = Signal()
"""铲除后信号 每块地铲除都会触发该信号
Args:
uid (str): 用户Uid
soilIndex (index): 铲除地块索引 从1开始
"""
Args:
uid (str): 用户Uid
soilIndex (index): 铲除地块索引 从1开始
"""
m_beforeExpand = Signal()
m_afterExpand = Signal()
m_beforeSteal = Signal()
m_afterSteal = Signal()
m_dit = Signal()
self.m_beforeExpand = Signal()
self.m_afterExpand = Signal()
self.m_beforeSteal = Signal()
self.m_afterSteal = 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,19 +1,14 @@
import math
import plistlib
from itertools import islice
from zhenxun.services.log import logger
from zhenxun.utils._build_image import BuildImage
from zhenxun.utils.image_utils import ImageTemplate
from ..config import g_sResourcePath
from ..config import g_sResourcePath, g_sTranslation
from ..dbService import g_pDBService
from ..json import g_pJsonManager
class CShopManager:
@classmethod
async def getSeedShopImage(cls, filterKey: str|int = 1, num: int = 1) -> bytes:
async def getSeedShopImage(cls, filterKey: str | int = 1, num: int = 1) -> bytes:
"""获取商店页面
Args:
@ -37,14 +32,15 @@ class CShopManager:
columnName = [
"-",
"种子名称",
"种子单价",
"农场币",
"点券",
"解锁等级",
"果实单价",
"收获经验",
"收获数量",
"成熟时间(小时)",
"收获次数",
"是否可以上架交易行"
"是否可以上架交易行",
]
# 查询所有可购买作物,并根据筛选关键字过滤
@ -52,10 +48,10 @@ class CShopManager:
filteredPlants = []
for plant in plants:
# 跳过未解锁购买的种子
if plant['isBuy'] == 0:
if plant["isBuy"] == 0:
continue
# 字符串筛选
if filterStr and filterStr not in plant['name']:
if filterStr and filterStr not in plant["name"]:
continue
filteredPlants.append(plant)
@ -63,7 +59,7 @@ class CShopManager:
totalCount = len(filteredPlants)
pageCount = math.ceil(totalCount / 15) if totalCount else 1
startIndex = (page - 1) * 15
pageItems = filteredPlants[startIndex: startIndex + 15]
pageItems = filteredPlants[startIndex : startIndex + 15]
# 构建数据行
dataList = []
@ -75,20 +71,23 @@ class CShopManager:
icon = (iconPath, 33, 33)
# 交易行标记
sell = "可以" if plant['sell'] else "不可以"
sell = "可以" if plant["sell"] else "不可以"
dataList.append([
icon,
plant['name'], # 种子名称
plant['buy'], # 种子单价
plant['level'], # 解锁等级
plant['price'], # 果实单价
plant['experience'], # 收获经验
plant['harvest'], # 收获数量
plant['time'], # 成熟时间(小时)
plant['crop'], # 收获次数
sell # 是否可上架交易行
])
dataList.append(
[
icon,
plant["name"], # 种子名称
plant["buy"], # 农场币种子单价
plant["vipBuy"], # 点券种子单价
plant["level"], # 解锁等级
plant["price"], # 果实单价
plant["experience"], # 收获经验
plant["harvest"], # 收获数量
plant["time"], # 成熟时间(小时)
plant["crop"], # 收获次数
sell, # 是否可上架交易行
]
)
# 页码标题
title = f"种子商店 页数: {page}/{pageCount}"
@ -102,7 +101,6 @@ class CShopManager:
)
return result.pic2bytes()
@classmethod
async def buySeed(cls, uid: str, name: str, num: int = 1) -> str:
"""购买种子
@ -117,31 +115,46 @@ class CShopManager:
"""
if num <= 0:
return "请输入购买数量!"
return g_sTranslation["buySeed"]["notNum"]
plantInfo = await g_pDBService.plant.getPlantByName(name)
if not plantInfo:
return "购买出错!请检查需购买的种子名称!"
return g_sTranslation["buySeed"]["error"]
level = await g_pDBService.user.getUserLevelByUid(uid)
if level[0] < int(plantInfo['level']):
return "你的等级不够哦,努努力吧"
if level[0] < int(plantInfo["level"]):
return g_sTranslation["buySeed"]["noLevel"]
point = await g_pDBService.user.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:
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 not await g_pDBService.userSeed.addUserSeedByUid(uid, name, num):
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:
@ -158,28 +171,37 @@ class CShopManager:
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():
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
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_pDBService.userPlant.updateUserPlantByName(uid, name, available - sellAmount)
return g_sTranslation["sellPlant"]["error1"].format(name=name)
await g_pDBService.userPlant.updateUserPlantByName(
uid, name, available - sellAmount
)
totalSold = sellAmount
if name == "":
@ -189,7 +211,7 @@ class CShopManager:
if not plantInfo:
price = 0
else:
price = plantInfo['price']
price = plantInfo["price"]
totalPoint = totalSold * price
@ -197,8 +219,13 @@ class CShopManager:
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()

View File

@ -1,5 +1,4 @@
import json
from pathlib import Path
from zhenxun.services.log import logger
@ -80,7 +79,11 @@ class CJsonManager:
return False
else:
return await self.initSign()
result = await self.initSign()
config.g_bSignStatus = result
return result
async def initSign(self) -> bool:
try:
@ -98,4 +101,5 @@ class CJsonManager:
logger.warning(f"sign_in.json JSON格式错误: {e}")
return False
g_pJsonManager = CJsonManager()

View File

@ -1,5 +1,19 @@
# 真寻农场更新日志
## V1.5
用户方面
---
- 新增土地升级指令,快来将土地升级至红土地、黑土地、金土地吧
- 新增查漏补缺作物资源功能,自动更新最新作物和作物资源文件
- 定时更新签到文件、作物资源从00:30调整至04:30
- 修正了部分土地资源错误的情况
- 修正了部分文本信息错误的情况
代码方面
---
- 修正部分事件连接机制
- 修正网络请求端口
## V1.4
用户方面
---

View File

@ -1,13 +1,20 @@
import json
import os
from datetime import datetime
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_sSignInPath
from .config import g_sPlantPath, g_sSignInPath
from .dbService import g_pDBService
from .tool import g_pToolManager
@ -15,12 +22,14 @@ class CRequestManager:
m_sTokens = "xZ%?z5LtWV7H:0-Xnwp+bNRNQ-jbfrxG"
@classmethod
async def download(cls,
url: str,
savePath: str,
fileName: str,
params: dict | None = None,
jsonData: dict | None = None) -> bool:
async def download(
cls,
url: str,
savePath: str,
fileName: str,
params: dict | None = None,
jsonData: dict | None = None,
) -> bool:
"""下载文件到指定路径并覆盖已存在的文件
Args:
@ -29,31 +38,53 @@ class CRequestManager:
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:
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)
response = await client.request(
"GET", url, **requestArgs, follow_redirects=True
)
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}")
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
@ -74,7 +105,7 @@ class CRequestManager:
dict: 返回请求结果的JSON数据
"""
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:
@ -84,7 +115,9 @@ class CRequestManager:
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=e)
@ -105,7 +138,7 @@ class CRequestManager:
dict: 返回请求结果的JSON数据
"""
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:
@ -115,7 +148,9 @@ class CRequestManager:
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=e)
@ -128,7 +163,7 @@ class CRequestManager:
async def initSignInFile(cls) -> bool:
if os.path.exists(g_sSignInPath):
try:
with open(g_sSignInPath, "r", encoding="utf-8") as f:
with open(g_sSignInPath, encoding="utf-8") as f:
content = f.read()
sign = json.loads(content)
@ -141,28 +176,117 @@ class CRequestManager:
else:
logger.warning("真寻农场签到文件检查失败, 即将下载")
return await cls.downloadSignInFile()
except json.JSONDecodeError as e:
logger.warning(f"真寻农场签到文件格式错误, 即将下载")
except json.JSONDecodeError:
logger.warning("真寻农场签到文件格式错误, 即将下载")
return await cls.downloadSignInFile()
else:
return await cls.downloadSignInFile()
@classmethod
async def downloadSignInFile(cls) -> bool:
"""下载签到文件,并重命名为 sign_in.json
Returns:
bool: 是否下载成功
"""
try:
baseUrl = Config.get_config("zhenxun_plugin_farm", "服务地址")
url = f"{baseUrl.rstrip('/')}:8998/sign_in"
path = str(g_sSignInPath.parent.resolve(strict=False))
yearMonth = g_pToolManager.dateTime().now().strftime("%Y%m")
await cls.download(url, path, "signTemp.json", jsonData={'date':yearMonth})
g_pToolManager.renameFile(f"{path}/signTemp.json", "sign_in.json")
# 下载为 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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

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