Merge pull request #11 from Mualamx/Beta

 添加数据库字段(是否为点券作物,点券作物单价);实现点券购买种子;我的种子查询页面添加点券单价。
This commit is contained in:
术樱 2025-10-12 20:23:55 +08:00 committed by GitHub
commit ac495ad6e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 509 additions and 42 deletions

View File

@ -58,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",

View File

@ -85,6 +85,8 @@ 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="购买农场币"),
@ -92,6 +94,8 @@ diuse_farm = on_alconna(
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,
@ -328,6 +332,65 @@ async def _(session: Uninfo):
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"作物加锁(?P<name>)",
command="我的农场",
arguments=["lock-plant", "{name}"],
prefix=True,
)
@diuse_farm.assign("lock-plant")
async def _(session: Uninfo, name: Match[str]):
uid = str(session.user.id)
if not await g_pToolManager.isRegisteredByUid(uid):
return
result = await g_pFarmManager.lockUserPlantByUid(uid, name.result, 1)
await MessageUtils.build_message(result).send(reply_to=True)
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
result = await g_pFarmManager.lockUserPlantByUid(uid, name.result, 0)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"出售作物(?P<name>.*?)",
command="我的农场",
arguments=["sell-plant", "{name}"],
prefix=True,
)
@diuse_farm.assign("sell-plant")
async def _(
session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("num", -1)
):
uid = str(session.user.id)
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,
@ -364,27 +427,6 @@ async def _(session: Uninfo):
await MessageUtils.build_message(res).send(reply_to=True)
diuse_farm.shortcut(
"出售作物(?P<name>.*?)",
command="我的农场",
arguments=["sell-plant", "{name}"],
prefix=True,
)
@diuse_farm.assign("sell-plant")
async def _(
session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("num", -1)
):
uid = str(session.user.id)
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)
diuse_farm.shortcut(
"偷菜",
command="我的农场",
@ -606,3 +648,48 @@ async def _(session: Uninfo, num: Query[int] = AlconnaQuery("num", 0)):
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)

View File

@ -34,6 +34,7 @@ g_sTranslation = {
"basic": {
"notFarm": "尚未开通农场快at我发送 开通农场 开通吧 🌱🚜",
"point": "你的当前农场币为: {point} 🌾💰",
"vipPoint": "你的当前点券为: {vipPoint} 🌾💰",
},
"register": {
"success": "✅ 农场开通成功!\n💼 初始资金:{point}农场币 🥳🎉",
@ -45,7 +46,9 @@ g_sTranslation = {
"notNum": "❗️ 请输入购买数量!",
"noLevel": "🔒 你的等级不够哦,努努力吧 💪",
"noPoint": "💰 你的农场币不够哦~ 快速速氪金吧!💸",
"noVipPoint": "💰 你的点券不够哦~ 快速速氪金吧!💸",
"success": "✅ 成功购买{name},花费{total}农场币,剩余{point}农场币 🌾",
"vipSuccess": "✅ 成功购买{name},花费{total}点券,剩余{point}点券 🌾",
"errorSql": "❌ 购买失败,执行数据库错误!🛑",
"error": "❌ 购买出错!请检查需购买的种子名称!🔍",
},
@ -121,4 +124,10 @@ g_sTranslation = {
"aquamarine": "增产+32% 经验+32% 时间-28% 幸运+1%",
"blackcrystal": "增产+32% 经验+40% 时间-28% 幸运+2%",
},
"lockPlant": {
"noPlant": "很抱歉,你的作物仓库中没有该作物",
"lockPlant": "加锁成功,现在{name}不会被一键售卖了",
"unlockPlant": "解锁成功,现在{name}会被一键售卖了",
"error": "未知错误",
},
}

View File

@ -10,6 +10,7 @@ class CUserPlantDB(CSqlManager):
"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)",
}
@ -91,6 +92,27 @@ class CUserPlantDB(CSqlManager):
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,7 +136,51 @@ class CUserPlantDB(CSqlManager):
)
return True
except Exception as e:
logger.warning("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

View File

@ -165,7 +165,6 @@ class CUserSignDB(CSqlManager):
if row["currentMonth"] == currentMonth
else 1
)
totalSignDays = row["totalSignDays"]
lastDate = row["lastSignDate"]
prevDate = (
g_pToolManager.dateTime().strptime(signDate, "%Y-%m-%d")
@ -200,7 +199,7 @@ class CUserSignDB(CSqlManager):
),
)
else:
totalSignDays = 1
monthSignDays = 1
await cls.m_pDB.execute(
"""
INSERT INTO userSignSummary
@ -211,7 +210,7 @@ class CUserSignDB(CSqlManager):
uid,
1,
currentMonth,
1,
monthSignDays,
signDate,
1,
1 if isSupplement else 0,
@ -219,15 +218,13 @@ class CUserSignDB(CSqlManager):
)
# 计算累签奖励
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", {})
if plant:
for key, value in plant.items():
await g_pDBService.userSeed.addUserSeedByUid(uid, key, value)

View File

@ -24,7 +24,7 @@ class CUserSoilDB(CSqlManager):
"weedStatus": "INTEGER DEFAULT 0", # 杂草状态 0=无杂草1=有杂草
"waterStatus": "INTEGER DEFAULT 0", # 缺水状态 0=不缺水1=缺水
"harvestCount": "INTEGER DEFAULT 0", # 收获次数
"isSoilPlanted": "INTEGER DEFAULT NULL", # 是否种植作物
"isSoilPlanted": "INTEGER DEFAULT NULL", # 是否种植作物 0=没有1=有
"PRIMARY KEY": "(uid, soilIndex)",
}

View File

@ -159,6 +159,7 @@ class CFarmManager:
if image:
avatar = BuildImage(background=image)
await avatar.resize(0, 140, 150)
await img.paste(avatar, (125, 85))
# 头像框
@ -732,7 +733,15 @@ class CFarmManager:
bytes: 返回图片
"""
data_list = []
column_name = ["-", "作物名称", "数量", "单价", "总价", "是否可以上架交易行"]
column_name = [
"-",
"作物名称",
"数量",
"单价",
"总价",
"是否上锁",
"是否可以上架交易行",
]
plant = await g_pDBService.userPlant.getUserPlantByUid(uid)
@ -763,7 +772,15 @@ class CFarmManager:
number = int(count) * plantInfo["price"]
data_list.append([icon, name, count, plantInfo["price"], number, sell])
isLock = await g_pDBService.userPlant.checkPlantLockByName(uid, name)
if isLock:
lock = "上锁"
else:
lock = "未上锁"
data_list.append(
[icon, name, count, plantInfo["price"], number, lock, sell]
)
result = await ImageTemplate.table_page(
"作物仓库",
@ -774,6 +791,31 @@ class CFarmManager:
return result.pic2bytes()
@classmethod
async def lockUserPlantByUid(cls, uid: str, name: str, lock: int) -> str:
"""加/解 锁用户作物
Args:
uid (str): 用户uid
name (str): 作物名称
lock (int): 0=解锁 非0=加锁
Returns:
str: 返回
"""
# 先判断该用户仓库是否有该作物
if not await g_pDBService.userPlant.checkUserPlantByName(uid, name):
return g_sTranslation["lockPlant"]["noPlant"]
# 更改加锁状态
if await g_pDBService.userPlant.lockUserPlantByName(uid, name, lock):
if lock == 0:
return g_sTranslation["lockPlant"]["unlockPlant"].format(name=name)
else:
return g_sTranslation["lockPlant"]["lockPlant"].format(name=name)
else:
return g_sTranslation["lockPlant"]["error"]
@classmethod
async def stealing(cls, uid: str, target: str) -> str:
"""偷菜
@ -1106,5 +1148,54 @@ class CFarmManager:
text=g_sTranslation["soilInfo"][soilLevelText],
)
@classmethod
async def pointToVipPointByUid(cls, uid: str, num: int) -> str:
"""点券兑换
num:用户传参,即将兑换的点券
pro:兑换倍数;兑换倍数乘以num即为需要消耗的农场币
Args:
uid (str): 用户Uid
num (int): 兑换点券数量
Returns:
str: 返回结果
兑换比例在配置文件中配置
目前配置文件中默认是20倍
100点券需要20000农场币
赠送点券规则
小于2000点券0
2000-5000点券100
5000-50000点券280
大于50000点券3000
"""
if num < 100:
return "点券兑换数量必须大于等于100"
pro = int(Config.get_config("zhenxun_plugin_farm", "点券兑换倍数"))
pro *= num
point = await g_pDBService.user.getUserPointByUid(uid)
if point < pro:
return f"你的农场币不足,当前农场币为{point},兑换还需要{pro - point}农场币"
p = await g_pDBService.user.getUserVipPointByUid(uid)
giftPoints: int
if num < 2000:
giftPoints = 0
elif num < 5000:
giftPoints = 100
elif num < 50000:
giftPoints = 280
else:
giftPoints = 3000
number = num + p + giftPoints
await g_pDBService.user.updateUserVipPointByUid(uid, int(number))
point -= pro
await g_pDBService.user.updateUserPointByUid(uid, int(point))
return f"兑换{num}点券成功,当前点券:{number},赠送点券:{giftPoints},当前农场币:{point}"
g_pFarmManager = CFarmManager()

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,6 +1,5 @@
import math
from zhenxun.services.log import logger
from zhenxun.utils.image_utils import ImageTemplate
from ..config import g_sResourcePath, g_sTranslation
@ -33,7 +32,8 @@ class CShopManager:
columnName = [
"-",
"种子名称",
"种子单价",
"农场币",
"点券",
"解锁等级",
"果实单价",
"收获经验",
@ -77,7 +77,8 @@ class CShopManager:
[
icon,
plant["name"], # 种子名称
plant["buy"], # 种子单价
plant["buy"], # 农场币种子单价
plant["vipBuy"], # 点券种子单价
plant["level"], # 解锁等级
plant["price"], # 果实单价
plant["experience"], # 收获经验
@ -125,21 +126,32 @@ class CShopManager:
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 g_sTranslation["buySeed"]["noPoint"]
"""
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 g_sTranslation["buySeed"]["errorSql"]
if not await g_pDBService.userSeed.addUserSeedByUid(uid, name, num):
return g_sTranslation["buySeed"]["errorSql"]
if plantInfo["isVip"] == 1:
return g_sTranslation["buySeed"]["vipSuccess"].format(
name=name, total=total, point=vipPoint - total
)
else:
return g_sTranslation["buySeed"]["success"].format(
name=name, total=total, point=point - total
)
@ -167,6 +179,13 @@ class CShopManager:
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

Binary file not shown.

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>