mirror of
https://github.com/PaiGramTeam/PamGram.git
synced 2024-11-21 21:58:04 +00:00
✨ Support Pay Log
This commit is contained in:
parent
16dfd5021e
commit
d377c4c241
26
modules/pay_log/error.py
Normal file
26
modules/pay_log/error.py
Normal file
@ -0,0 +1,26 @@
|
||||
class PayLogException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PayLogFileError(PayLogException):
|
||||
pass
|
||||
|
||||
|
||||
class PayLogNotFound(PayLogFileError):
|
||||
pass
|
||||
|
||||
|
||||
class PayLogAccountNotFound(PayLogException):
|
||||
pass
|
||||
|
||||
|
||||
class PayLogAuthkeyException(PayLogException):
|
||||
pass
|
||||
|
||||
|
||||
class PayLogAuthkeyTimeout(PayLogAuthkeyException):
|
||||
pass
|
||||
|
||||
|
||||
class PayLogInvalidAuthkey(PayLogAuthkeyException):
|
||||
pass
|
238
modules/pay_log/log.py
Normal file
238
modules/pay_log/log.py
Normal file
@ -0,0 +1,238 @@
|
||||
import contextlib
|
||||
from pathlib import Path
|
||||
from typing import Tuple, Optional, List, Dict
|
||||
|
||||
import aiofiles
|
||||
from genshin import Client, AuthkeyTimeout, InvalidAuthkey
|
||||
from genshin.models import TransactionKind, BaseTransaction
|
||||
|
||||
from modules.pay_log.error import PayLogAuthkeyTimeout, PayLogInvalidAuthkey, PayLogNotFound
|
||||
from modules.pay_log.models import PayLog as PayLogModel, BaseInfo
|
||||
from utils.const import PROJECT_ROOT
|
||||
|
||||
try:
|
||||
import ujson as jsonlib
|
||||
|
||||
except ImportError:
|
||||
import json as jsonlib
|
||||
|
||||
PAY_LOG_PATH = PROJECT_ROOT.joinpath("data", "apihelper", "pay_log")
|
||||
PAY_LOG_PATH.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
class PayLog:
|
||||
def __init__(self, pay_log_path: Path = PAY_LOG_PATH):
|
||||
self.pay_log_path = pay_log_path
|
||||
|
||||
@staticmethod
|
||||
async def load_json(path):
|
||||
async with aiofiles.open(path, "r", encoding="utf-8") as f:
|
||||
return jsonlib.loads(await f.read())
|
||||
|
||||
@staticmethod
|
||||
async def save_json(path, data: PayLogModel):
|
||||
async with aiofiles.open(path, "w", encoding="utf-8") as f:
|
||||
return await f.write(data.json(ensure_ascii=False, indent=4))
|
||||
|
||||
def get_file_path(
|
||||
self,
|
||||
user_id: str,
|
||||
uid: str,
|
||||
bak: bool = False,
|
||||
):
|
||||
"""获取文件路径
|
||||
:param user_id: 用户 ID
|
||||
:param uid: UID
|
||||
:param bak: 是否为备份文件
|
||||
:return: 文件路径
|
||||
"""
|
||||
return self.pay_log_path / f"{user_id}-{uid}.json{'.bak' if bak else ''}"
|
||||
|
||||
async def load_history_info(
|
||||
self,
|
||||
user_id: str,
|
||||
uid: str,
|
||||
only_status: bool = False,
|
||||
) -> Tuple[Optional[PayLogModel], bool]:
|
||||
"""读取历史记录数据
|
||||
:param user_id: 用户id
|
||||
:param uid: 原神uid
|
||||
:param only_status: 是否只读取状态
|
||||
:return: 抽卡记录数据
|
||||
"""
|
||||
file_path = self.get_file_path(user_id, uid)
|
||||
if only_status:
|
||||
return None, file_path.exists()
|
||||
if not file_path.exists():
|
||||
return PayLogModel(info=BaseInfo(uid=uid), list=[]), False
|
||||
try:
|
||||
return PayLogModel.parse_obj(await self.load_json(file_path)), True
|
||||
except jsonlib.decoder.JSONDecodeError:
|
||||
return PayLogModel(info=BaseInfo(uid=uid), list=[]), False
|
||||
|
||||
async def remove_history_info(
|
||||
self,
|
||||
user_id: str,
|
||||
uid: str,
|
||||
) -> bool:
|
||||
"""删除历史记录数据
|
||||
:param user_id: 用户id
|
||||
:param uid: 原神uid
|
||||
:return: 是否删除成功
|
||||
"""
|
||||
file_path = self.get_file_path(user_id, uid)
|
||||
file_bak_path = self.get_file_path(user_id, uid, bak=True)
|
||||
with contextlib.suppress(Exception):
|
||||
file_bak_path.unlink(missing_ok=True)
|
||||
if file_path.exists():
|
||||
try:
|
||||
file_path.unlink()
|
||||
except PermissionError:
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
async def save_pay_log_info(self, user_id: str, uid: str, info: PayLogModel) -> None:
|
||||
"""保存日志记录数据
|
||||
:param user_id: 用户id
|
||||
:param uid: 原神uid
|
||||
:param info: 记录数据
|
||||
"""
|
||||
save_path = self.pay_log_path / f"{user_id}-{uid}.json"
|
||||
save_path_bak = self.pay_log_path / f"{user_id}-{uid}.json.bak"
|
||||
# 将旧数据备份一次
|
||||
with contextlib.suppress(PermissionError):
|
||||
if save_path.exists():
|
||||
if save_path_bak.exists():
|
||||
save_path_bak.unlink()
|
||||
save_path.rename(save_path.parent / f"{save_path.name}.bak")
|
||||
# 写入数据
|
||||
await self.save_json(save_path, info)
|
||||
|
||||
async def get_log_data(
|
||||
self,
|
||||
user_id: int,
|
||||
client: Client,
|
||||
authkey: str,
|
||||
) -> int:
|
||||
"""使用 authkey 获取历史记录数据,并合并旧数据
|
||||
:param user_id: 用户id
|
||||
:param client: genshin client
|
||||
:param authkey: authkey
|
||||
:return: 更新结果
|
||||
"""
|
||||
new_num = 0
|
||||
pay_log, have_old = await self.load_history_info(str(user_id), str(client.uid))
|
||||
history_ids = [i.id for i in pay_log.list]
|
||||
try:
|
||||
async for data in client.transaction_log(TransactionKind.CRYSTAL, authkey=authkey):
|
||||
if data.id not in history_ids:
|
||||
pay_log.list.append(data)
|
||||
new_num += 1
|
||||
except AuthkeyTimeout as exc:
|
||||
raise PayLogAuthkeyTimeout from exc
|
||||
except InvalidAuthkey as exc:
|
||||
raise PayLogInvalidAuthkey from exc
|
||||
if new_num > 0 or have_old:
|
||||
pay_log.list.sort(key=lambda x: (x.time, x.id), reverse=True)
|
||||
pay_log.info.update_now()
|
||||
await self.save_pay_log_info(str(user_id), str(client.uid), pay_log)
|
||||
return new_num
|
||||
|
||||
@staticmethod
|
||||
async def get_month_data(pay_log: PayLogModel, price_data: List[Dict]) -> Tuple[int, int, List[Dict]]:
|
||||
"""获取月份数据
|
||||
:param pay_log: 日志数据
|
||||
:param price_data: 商品数据
|
||||
:return: 月份数据
|
||||
"""
|
||||
all_amount: int = 0
|
||||
all_pay: int = 0
|
||||
months: List[int] = []
|
||||
month_datas: List[Dict] = []
|
||||
last_month: Optional[Dict] = None
|
||||
month_data: List[Optional[BaseTransaction]] = []
|
||||
for i in pay_log.list:
|
||||
if i.amount <= 0:
|
||||
continue
|
||||
all_amount += i.amount
|
||||
all_pay += i.amount
|
||||
if i.time.month not in months:
|
||||
months.append(i.time.month)
|
||||
if last_month:
|
||||
last_month["amount"] = sum(i.amount for i in month_data)
|
||||
month_data.clear()
|
||||
if len(months) <= 6:
|
||||
last_month = {
|
||||
"month": f"{i.time.month}月",
|
||||
"amount": 0,
|
||||
}
|
||||
month_datas.append(last_month)
|
||||
else:
|
||||
last_month = None
|
||||
for j in price_data:
|
||||
if i.amount in j["price"]:
|
||||
j["count"] += 1
|
||||
j["amount"] += i.amount
|
||||
if i.amount == price_data[0]["price"][0]:
|
||||
all_amount -= i.amount
|
||||
break
|
||||
month_data.append(i)
|
||||
if last_month:
|
||||
last_month["amount"] = sum(i.amount for i in month_data)
|
||||
month_data.clear()
|
||||
if not month_datas:
|
||||
raise PayLogNotFound
|
||||
month_datas.sort(key=lambda k: k["amount"], reverse=True)
|
||||
return all_amount, all_pay, month_datas
|
||||
|
||||
async def get_analysis(self, user_id: int, client: Client):
|
||||
"""获取分析数据
|
||||
:param user_id: 用户id
|
||||
:param client: genshin client
|
||||
:return: 分析数据
|
||||
"""
|
||||
pay_log, status = await self.load_history_info(str(user_id), str(client.uid))
|
||||
if not status:
|
||||
raise PayLogNotFound
|
||||
# 单双倍结晶数
|
||||
price_data = [
|
||||
{
|
||||
"price": price,
|
||||
"count": 0,
|
||||
"amount": 0,
|
||||
}
|
||||
for price in [[680], [300], [8080, 12960], [3880, 6560], [2240, 3960], [1090, 1960], [330, 600], [60, 120]]
|
||||
]
|
||||
price_data_name = ["大月卡", "小月卡", "648", "328", "198", "98", "30", "6"]
|
||||
all_pay, all_amount, month_datas = await PayLog.get_month_data(pay_log, price_data)
|
||||
datas = [
|
||||
{"value": f"¥{all_pay / 10:.0f}", "name": "总消费"},
|
||||
{"value": all_amount, "name": "总结晶"},
|
||||
{"value": f"{month_datas[0]['month']}", "name": "消费最多"},
|
||||
{
|
||||
"value": f"¥{month_datas[0]['amount'] / 10:.0f}",
|
||||
"name": f"{month_datas[0]['month']}消费",
|
||||
},
|
||||
*[
|
||||
{
|
||||
"value": price_data[i]["count"],
|
||||
"name": f"{price_data_name[i]}",
|
||||
}
|
||||
for i in range(len(price_data))
|
||||
],
|
||||
]
|
||||
pie_datas = [
|
||||
{
|
||||
"value": f"{price_data[i]['amount'] / 10:.0f}",
|
||||
"name": f"{price_data_name[i]}",
|
||||
}
|
||||
for i in range(len(price_data))
|
||||
if price_data[i]["count"] > 0
|
||||
]
|
||||
return {
|
||||
"uid": client.uid,
|
||||
"datas": datas,
|
||||
"bar_data": month_datas,
|
||||
"pie_data": pie_datas,
|
||||
}
|
41
modules/pay_log/models.py
Normal file
41
modules/pay_log/models.py
Normal file
@ -0,0 +1,41 @@
|
||||
import datetime
|
||||
|
||||
from typing import Any, List
|
||||
|
||||
from genshin.models import BaseTransaction
|
||||
from pydantic import BaseModel, BaseConfig
|
||||
|
||||
try:
|
||||
import ujson as jsonlib
|
||||
|
||||
except ImportError:
|
||||
import json as jsonlib
|
||||
|
||||
|
||||
class _ModelConfig(BaseConfig):
|
||||
json_dumps = jsonlib.dumps
|
||||
json_loads = jsonlib.loads
|
||||
|
||||
|
||||
class BaseInfo(BaseModel):
|
||||
Config = _ModelConfig
|
||||
uid: str = "0"
|
||||
lang: str = "zh-cn"
|
||||
export_time: str = ""
|
||||
export_timestamp: int = 0
|
||||
export_app: str = "PaimonBot"
|
||||
|
||||
def __init__(self, **data: Any):
|
||||
super().__init__(**data)
|
||||
if not self.export_time:
|
||||
self.update_now()
|
||||
|
||||
def update_now(self):
|
||||
self.export_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
self.export_timestamp = int(datetime.datetime.now().timestamp())
|
||||
|
||||
|
||||
class PayLog(BaseModel):
|
||||
Config = _ModelConfig
|
||||
info: BaseInfo
|
||||
list: List[BaseTransaction]
|
271
plugins/genshin/pay_log.py
Normal file
271
plugins/genshin/pay_log.py
Normal file
@ -0,0 +1,271 @@
|
||||
import genshin
|
||||
from telegram import Update, User, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from telegram.constants import ChatAction
|
||||
from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filters, ConversationHandler
|
||||
from telegram.helpers import create_deep_linked_url
|
||||
|
||||
from core.baseplugin import BasePlugin
|
||||
from core.cookies import CookiesService
|
||||
from core.cookies.error import CookiesNotFoundError
|
||||
from core.plugin import Plugin, handler, conversation
|
||||
from core.template import TemplateService
|
||||
from core.user import UserService
|
||||
from core.user.error import UserNotFoundError
|
||||
from modules.gacha_log.helpers import from_url_get_authkey
|
||||
from modules.pay_log.error import PayLogNotFound, PayLogAccountNotFound, PayLogInvalidAuthkey, PayLogAuthkeyTimeout
|
||||
from modules.pay_log.log import PayLog
|
||||
from utils.bot import get_args
|
||||
from utils.decorators.admins import bot_admins_rights_check
|
||||
from utils.decorators.error import error_callable
|
||||
from utils.decorators.restricts import restricts
|
||||
from utils.genshin import get_authkey_by_stoken
|
||||
from utils.helpers import get_genshin_client
|
||||
from utils.log import logger
|
||||
from utils.models.base import RegionEnum
|
||||
|
||||
INPUT_URL, CONFIRM_DELETE = range(10100, 10102)
|
||||
|
||||
|
||||
class PayLogPlugin(Plugin.Conversation, BasePlugin.Conversation):
|
||||
"""充值记录导入/导出/分析"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
template_service: TemplateService = None,
|
||||
user_service: UserService = None,
|
||||
cookie_service: CookiesService = None,
|
||||
):
|
||||
self.template_service = template_service
|
||||
self.user_service = user_service
|
||||
self.cookie_service = cookie_service
|
||||
self.pay_log = PayLog()
|
||||
|
||||
async def _refresh_user_data(self, user: User, authkey: str = None) -> str:
|
||||
"""刷新用户数据
|
||||
:param user: 用户
|
||||
:param authkey: 认证密钥
|
||||
:return: 返回信息
|
||||
"""
|
||||
try:
|
||||
logger.debug("尝试获取已绑定的原神账号")
|
||||
client = await get_genshin_client(user.id, need_cookie=False)
|
||||
new_num = await self.pay_log.get_log_data(user.id, client, authkey)
|
||||
return "更新完成,本次没有新增数据" if new_num == 0 else f"更新完成,本次共新增{new_num}条抽卡记录"
|
||||
except PayLogNotFound:
|
||||
return "派蒙没有找到你的充值记录,快去氪金吧~"
|
||||
except PayLogAccountNotFound:
|
||||
return "导入失败,可能文件包含的祈愿记录所属 uid 与你当前绑定的 uid 不同"
|
||||
except PayLogInvalidAuthkey:
|
||||
return "更新数据失败,authkey 无效"
|
||||
except PayLogAuthkeyTimeout:
|
||||
return "更新数据失败,authkey 已经过期"
|
||||
except UserNotFoundError:
|
||||
logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id)
|
||||
return "派蒙没有找到您所绑定的账号信息,请先私聊派蒙绑定账号"
|
||||
|
||||
@conversation.entry_point
|
||||
@handler(CommandHandler, command="pay_log_import", filters=filters.ChatType.PRIVATE, block=False)
|
||||
@handler(MessageHandler, filters=filters.Regex("^导入充值记录$") & filters.ChatType.PRIVATE, block=False)
|
||||
@restricts()
|
||||
@error_callable
|
||||
async def command_start(self, update: Update, context: CallbackContext) -> int:
|
||||
message = update.effective_message
|
||||
user = update.effective_user
|
||||
args = get_args(context)
|
||||
logger.info("用户 %s[%s] 导入充值记录命令请求", user.full_name, user.id)
|
||||
authkey = from_url_get_authkey(args[0] if args else "")
|
||||
if not args:
|
||||
try:
|
||||
user_info = await self.user_service.get_user_by_id(user.id)
|
||||
except UserNotFoundError:
|
||||
user_info = None
|
||||
if user_info and user_info.region == RegionEnum.HYPERION:
|
||||
try:
|
||||
cookies = await self.cookie_service.get_cookies(user_info.user_id, user_info.region)
|
||||
except CookiesNotFoundError:
|
||||
cookies = None
|
||||
if cookies and cookies.cookies and "stoken" in cookies.cookies:
|
||||
if stuid := next(
|
||||
(value for key, value in cookies.cookies.items() if key in ["ltuid", "login_uid"]), None
|
||||
):
|
||||
cookies.cookies["stuid"] = stuid
|
||||
client = genshin.Client(
|
||||
cookies=cookies.cookies,
|
||||
game=genshin.types.Game.GENSHIN,
|
||||
region=genshin.Region.CHINESE,
|
||||
lang="zh-cn",
|
||||
uid=user_info.yuanshen_uid,
|
||||
)
|
||||
authkey = await get_authkey_by_stoken(client)
|
||||
if not authkey:
|
||||
await message.reply_text(
|
||||
"<b>开始导入充值历史记录:请通过 https://paimon.moe/wish/import 获取抽卡记录链接后发送给我"
|
||||
"(非 paimon.moe 导出的文件数据)</b>\n\n"
|
||||
"> 在绑定 Cookie 时添加 stoken 可能有特殊效果哦(仅限国服)\n"
|
||||
"<b>注意:导入的数据将会与旧数据进行合并。</b>",
|
||||
parse_mode="html",
|
||||
)
|
||||
return INPUT_URL
|
||||
text = "小派蒙正在从服务器获取数据,请稍后"
|
||||
if not args:
|
||||
text += "\n\n> 由于你绑定的 Cookie 中存在 stoken ,本次通过 stoken 自动刷新数据"
|
||||
reply = await message.reply_text(text)
|
||||
await message.reply_chat_action(ChatAction.TYPING)
|
||||
data = await self._refresh_user_data(user, authkey=authkey)
|
||||
await reply.edit_text(data)
|
||||
return ConversationHandler.END
|
||||
|
||||
@conversation.state(state=INPUT_URL)
|
||||
@handler.message(filters=~filters.COMMAND, block=False)
|
||||
@restricts()
|
||||
@error_callable
|
||||
async def import_data_from_message(self, update: Update, _: CallbackContext) -> int:
|
||||
message = update.effective_message
|
||||
user = update.effective_user
|
||||
if message.document:
|
||||
await self.import_from_file(user, message)
|
||||
return ConversationHandler.END
|
||||
authkey = from_url_get_authkey(message.text)
|
||||
reply = await message.reply_text("小派蒙正在从服务器获取数据,请稍后")
|
||||
await message.reply_chat_action(ChatAction.TYPING)
|
||||
text = await self._refresh_user_data(user, authkey=authkey)
|
||||
await reply.edit_text(text)
|
||||
return ConversationHandler.END
|
||||
|
||||
@conversation.entry_point
|
||||
@handler(CommandHandler, command="pay_log_delete", filters=filters.ChatType.PRIVATE, block=False)
|
||||
@handler(MessageHandler, filters=filters.Regex("^删除充值记录$") & filters.ChatType.PRIVATE, block=False)
|
||||
@restricts()
|
||||
@error_callable
|
||||
async def command_start_delete(self, update: Update, context: CallbackContext) -> int:
|
||||
message = update.effective_message
|
||||
user = update.effective_user
|
||||
logger.info("用户 %s[%s] 删除充值记录命令请求", user.full_name, user.id)
|
||||
try:
|
||||
client = await get_genshin_client(user.id, need_cookie=False)
|
||||
context.chat_data["uid"] = client.uid
|
||||
except UserNotFoundError:
|
||||
logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id)
|
||||
buttons = [[InlineKeyboardButton("点我绑定账号", url=create_deep_linked_url(context.bot.username, "set_uid"))]]
|
||||
if filters.ChatType.GROUPS.filter(message):
|
||||
reply_message = await message.reply_text(
|
||||
"未查询到您所绑定的账号信息,请先私聊派蒙绑定账号", reply_markup=InlineKeyboardMarkup(buttons)
|
||||
)
|
||||
self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id, 30)
|
||||
self._add_delete_message_job(context, message.chat_id, message.message_id, 30)
|
||||
else:
|
||||
await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons))
|
||||
return ConversationHandler.END
|
||||
_, status = await self.pay_log.load_history_info(str(user.id), str(client.uid), only_status=True)
|
||||
if not status:
|
||||
await message.reply_text("你还没有导入充值记录哦~")
|
||||
return ConversationHandler.END
|
||||
await message.reply_text("你确定要删除充值记录吗?(此项操作无法恢复),如果确定请发送 ”确定“,发送其他内容取消")
|
||||
return CONFIRM_DELETE
|
||||
|
||||
@conversation.state(state=CONFIRM_DELETE)
|
||||
@handler.message(filters=filters.TEXT & ~filters.COMMAND, block=False)
|
||||
@restricts()
|
||||
@error_callable
|
||||
async def command_confirm_delete(self, update: Update, context: CallbackContext) -> int:
|
||||
message = update.effective_message
|
||||
user = update.effective_user
|
||||
if message.text == "确定":
|
||||
status = await self.pay_log.remove_history_info(str(user.id), str(context.chat_data["uid"]))
|
||||
await message.reply_text("充值记录已删除" if status else "充值记录删除失败")
|
||||
return ConversationHandler.END
|
||||
await message.reply_text("已取消")
|
||||
return ConversationHandler.END
|
||||
|
||||
@handler(CommandHandler, command="pay_log_force_delete", block=False)
|
||||
@bot_admins_rights_check
|
||||
async def command_pay_log_force_delete(self, update: Update, context: CallbackContext):
|
||||
message = update.effective_message
|
||||
args = get_args(context)
|
||||
if not args:
|
||||
await message.reply_text("请指定用户ID")
|
||||
return
|
||||
try:
|
||||
cid = int(args[0])
|
||||
if cid < 0:
|
||||
raise ValueError("Invalid cid")
|
||||
client = await get_genshin_client(cid, need_cookie=False)
|
||||
_, status = await self.pay_log.load_history_info(str(cid), str(client.uid), only_status=True)
|
||||
if not status:
|
||||
await message.reply_text("该用户还没有导入抽卡记录")
|
||||
return
|
||||
status = await self.pay_log.remove_history_info(str(cid), str(client.uid))
|
||||
await message.reply_text("抽卡记录已强制删除" if status else "抽卡记录删除失败")
|
||||
except PayLogNotFound:
|
||||
await message.reply_text("该用户还没有导入抽卡记录")
|
||||
except UserNotFoundError:
|
||||
await message.reply_text("该用户暂未绑定账号")
|
||||
except (ValueError, IndexError):
|
||||
await message.reply_text("用户ID 不合法")
|
||||
|
||||
@handler(CommandHandler, command="pay_log_export", filters=filters.ChatType.PRIVATE, block=False)
|
||||
@handler(MessageHandler, filters=filters.Regex("^导出充值记录$") & filters.ChatType.PRIVATE, block=False)
|
||||
@restricts()
|
||||
@error_callable
|
||||
async def command_start_export(self, update: Update, context: CallbackContext) -> None:
|
||||
message = update.effective_message
|
||||
user = update.effective_user
|
||||
logger.info("用户 %s[%s] 导出充值记录命令请求", user.full_name, user.id)
|
||||
try:
|
||||
client = await get_genshin_client(user.id, need_cookie=False)
|
||||
await message.reply_chat_action(ChatAction.TYPING)
|
||||
path = self.pay_log.get_file_path(str(user.id), str(client.uid))
|
||||
await message.reply_chat_action(ChatAction.UPLOAD_DOCUMENT)
|
||||
await message.reply_document(document=open(path, "rb+"), caption="充值记录导出文件")
|
||||
except PayLogNotFound:
|
||||
buttons = [
|
||||
[InlineKeyboardButton("点我导入", url=create_deep_linked_url(context.bot.username, "pay_log_import"))]
|
||||
]
|
||||
await message.reply_text("派蒙没有找到你的充值记录,快来私聊派蒙导入吧~", reply_markup=InlineKeyboardMarkup(buttons))
|
||||
except PayLogAccountNotFound:
|
||||
await message.reply_text("导出失败,可能文件包含的祈愿记录所属 uid 与你当前绑定的 uid 不同")
|
||||
except UserNotFoundError:
|
||||
logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id)
|
||||
buttons = [[InlineKeyboardButton("点我绑定账号", url=create_deep_linked_url(context.bot.username, "set_uid"))]]
|
||||
if filters.ChatType.GROUPS.filter(message):
|
||||
reply_message = await message.reply_text(
|
||||
"未查询到您所绑定的账号信息,请先私聊派蒙绑定账号", reply_markup=InlineKeyboardMarkup(buttons)
|
||||
)
|
||||
self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id, 30)
|
||||
self._add_delete_message_job(context, message.chat_id, message.message_id, 30)
|
||||
else:
|
||||
await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons))
|
||||
|
||||
@handler(CommandHandler, command="pay_log", block=False)
|
||||
@handler(MessageHandler, filters=filters.Regex("^充值记录$"), block=False)
|
||||
@restricts()
|
||||
@error_callable
|
||||
async def command_start_analysis(self, update: Update, context: CallbackContext) -> None:
|
||||
message = update.effective_message
|
||||
user = update.effective_user
|
||||
logger.info("用户 %s[%s] 充值记录统计命令请求", user.full_name, user.id)
|
||||
try:
|
||||
client = await get_genshin_client(user.id, need_cookie=False)
|
||||
await message.reply_chat_action(ChatAction.TYPING)
|
||||
data = await self.pay_log.get_analysis(user.id, client)
|
||||
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
|
||||
png_data = await self.template_service.render(
|
||||
"genshin/pay_log/pay_log.html", data, full_page=True, query_selector=".container"
|
||||
)
|
||||
await png_data.reply_photo(message)
|
||||
except PayLogNotFound:
|
||||
buttons = [
|
||||
[InlineKeyboardButton("点我导入", url=create_deep_linked_url(context.bot.username, "pay_log_import"))]
|
||||
]
|
||||
await message.reply_text("派蒙没有找到你的充值记录,快来点击按钮私聊派蒙导入吧~", reply_markup=InlineKeyboardMarkup(buttons))
|
||||
except UserNotFoundError:
|
||||
logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id)
|
||||
buttons = [[InlineKeyboardButton("点我绑定账号", url=create_deep_linked_url(context.bot.username, "set_uid"))]]
|
||||
if filters.ChatType.GROUPS.filter(message):
|
||||
reply_message = await message.reply_text(
|
||||
"未查询到您所绑定的账号信息,请先私聊派蒙绑定账号", reply_markup=InlineKeyboardMarkup(buttons)
|
||||
)
|
||||
self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id, 30)
|
||||
self._add_delete_message_job(context, message.chat_id, message.message_id, 30)
|
||||
else:
|
||||
await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons))
|
@ -13,6 +13,7 @@ from core.sign import SignServices
|
||||
from core.user import UserService
|
||||
from core.user.error import UserNotFoundError
|
||||
from modules.gacha_log.log import GachaLog
|
||||
from modules.pay_log.log import PayLog
|
||||
from utils.bot import get_args, get_chat as get_chat_with_cache
|
||||
from utils.decorators.admins import bot_admins_rights_check
|
||||
from utils.helpers import get_genshin_client
|
||||
@ -31,6 +32,7 @@ class GetChat(Plugin):
|
||||
self.user_service = user_service
|
||||
self.sign_service = sign_service
|
||||
self.gacha_log = GachaLog()
|
||||
self.pay_log = PayLog()
|
||||
|
||||
async def parse_group_chat(self, chat: Chat, admins: List[ChatMember]) -> str:
|
||||
text = f"群 ID:<code>{chat.id}</code>\n群名称:<code>{chat.title}</code>\n"
|
||||
@ -104,6 +106,12 @@ class GetChat(Plugin):
|
||||
text += f"\n - 最后更新:{gacha_log.update_time.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
else:
|
||||
text += "\n抽卡记录:<code>未导入</code>"
|
||||
with contextlib.suppress(Exception):
|
||||
pay_log, status = await self.pay_log.load_history_info(str(chat.id), str(uid))
|
||||
if status:
|
||||
text += f"\n充值记录:" f"\n - {len(pay_log.list)} 条" f"\n - 最后更新:{pay_log.info.export_time}"
|
||||
else:
|
||||
text += "\n充值记录:<code>未导入</code>"
|
||||
return text
|
||||
|
||||
@handler(CommandHandler, command="get_chat", block=False)
|
||||
|
2825
poetry.lock
generated
2825
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
45
resources/genshin/pay_log/echarts.min.js
vendored
Normal file
45
resources/genshin/pay_log/echarts.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
186
resources/genshin/pay_log/example.html
Normal file
186
resources/genshin/pay_log/example.html
Normal file
@ -0,0 +1,186 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" type="text/css" href="pay_log.css" />
|
||||
<link rel="preload" href="../../fonts/tttgbnumber.ttf" as="font">
|
||||
<link rel="preload" href="../gacha_log/img/提纳里.png" as="image">
|
||||
<style>
|
||||
.head_box {
|
||||
background: #fff url(../gacha_log/img/提纳里.png) no-repeat right center;
|
||||
background-size: cover;
|
||||
}
|
||||
</style>
|
||||
<title></title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container" id="container">
|
||||
<div class="head_box">
|
||||
<div class="id_text">ID: 114514</div>
|
||||
<h2 class="day_text">充值统计</h2>
|
||||
<img class="genshin_logo" src="./../../bot/help/background/genshin.png" alt=""/>
|
||||
</div>
|
||||
<div class="data_box">
|
||||
<div class="tab_lable">数据总览</div>
|
||||
<div class="data_line">
|
||||
<div class="data_line_item">
|
||||
<div class="num">114</div>
|
||||
<div class="lable">总消费</div>
|
||||
</div>
|
||||
<div class="data_line_item">
|
||||
<div class="num">514</div>
|
||||
<div class="lable">总结晶</div>
|
||||
</div>
|
||||
<div class="data_line_item">
|
||||
<div class="num">11月</div>
|
||||
<div class="lable">消费最多</div>
|
||||
</div>
|
||||
<div class="data_line_item">
|
||||
<div class="num">520</div>
|
||||
<div class="lable">11月消费</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data_line">
|
||||
<div class="data_line_item">
|
||||
<div class="num">1</div>
|
||||
<div class="lable">大月卡</div>
|
||||
</div>
|
||||
<div class="data_line_item">
|
||||
<div class="num">5</div>
|
||||
<div class="lable">小月卡</div>
|
||||
</div>
|
||||
<div class="data_line_item">
|
||||
<div class="num">3</div>
|
||||
<div class="lable">648</div>
|
||||
</div>
|
||||
<div class="data_line_item">
|
||||
<div class="num">2</div>
|
||||
<div class="lable">328</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data_line">
|
||||
<div class="data_line_item">
|
||||
<div class="num">1</div>
|
||||
<div class="lable">198</div>
|
||||
</div>
|
||||
<div class="data_line_item">
|
||||
<div class="num">2</div>
|
||||
<div class="lable">98</div>
|
||||
</div>
|
||||
<div class="data_line_item">
|
||||
<div class="num">3</div>
|
||||
<div class="lable">30</div>
|
||||
</div>
|
||||
<div class="data_line_item">
|
||||
<div class="num">4</div>
|
||||
<div class="lable">6</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data_box">
|
||||
<div class="tab_lable">月份统计</div>
|
||||
<div id="chartContainer"></div>
|
||||
</div>
|
||||
<div class="data_box">
|
||||
<div class="tab_lable">详细统计</div>
|
||||
<div id="chartContainer2"></div>
|
||||
</div>
|
||||
<div class="logo"> Template By Yunzai-Bot & seven-plugin</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src="echarts.min.js"></script>
|
||||
|
||||
<script>
|
||||
const barData = JSON.parse(`[{"month": "1月", "amount": 1000}]`);
|
||||
const myChart1 = echarts.init(document.querySelector('#chartContainer'), null, { renderer: 'svg' });
|
||||
const xData = barData.map(v => v.month)
|
||||
const yData = barData.map(v => v.amount / 10)
|
||||
// 指定图表的配置项和数据
|
||||
const option = {
|
||||
animation: false,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xData
|
||||
},
|
||||
legend: {
|
||||
x:'left',
|
||||
y:'top',
|
||||
show: true,
|
||||
data: [{ name: '金额' }]
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name:'金额',
|
||||
data: yData,
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
normal: {
|
||||
label: {
|
||||
position: 'top',
|
||||
show: true,
|
||||
textStyle: {
|
||||
color: '#1e1f20',
|
||||
fontSize: 14,
|
||||
fontFamily: "tttgbnumber",
|
||||
}
|
||||
},
|
||||
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [{
|
||||
offset: 0,
|
||||
color: "#1268f3"
|
||||
}, {
|
||||
offset: 0.6,
|
||||
color: "#08a4fa"
|
||||
}, {
|
||||
offset: 1,
|
||||
color: "#01ccfe"
|
||||
}], false)
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
]
|
||||
};
|
||||
myChart1.setOption(option);
|
||||
|
||||
const pieData = JSON.parse(`[{"value": 1, "name": "大月卡"}, {"value": 50, "name": "小月卡"}]`);
|
||||
const myChart2 = echarts.init(document.querySelector('#chartContainer2'), null, { renderer: 'svg' });
|
||||
const option2 = {
|
||||
animation: false,
|
||||
title: {
|
||||
text: '¥114',
|
||||
subtext: '总充值',
|
||||
left: 'right'
|
||||
},
|
||||
legend: {
|
||||
orient: 'horizontal',
|
||||
bottom: 'left'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Access From',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
itemStyle: {
|
||||
normal: {
|
||||
label: {
|
||||
show: true,
|
||||
fontFamily: "tttgbnumber",
|
||||
formatter: '{b}:¥{c} ({d}%)'
|
||||
},
|
||||
labelLine: { show: true }
|
||||
}
|
||||
},
|
||||
data: pieData,
|
||||
}
|
||||
]
|
||||
};
|
||||
myChart2.setOption(option2);
|
||||
</script>
|
||||
|
||||
</html>
|
122
resources/genshin/pay_log/pay_log.css
Normal file
122
resources/genshin/pay_log/pay_log.css
Normal file
@ -0,0 +1,122 @@
|
||||
@font-face {
|
||||
font-family: "tttgbnumber";
|
||||
src: url("../../fonts/tttgbnumber.ttf");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 16px;
|
||||
width: 530px;
|
||||
color: #1e1f20;
|
||||
transform: scale(1.3);
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 530px;
|
||||
padding: 20px 15px 10px 15px;
|
||||
background-color: #f5f6fb;
|
||||
}
|
||||
|
||||
.head_box {
|
||||
border-radius: 15px;
|
||||
font-family: tttgbnumber;
|
||||
padding: 10px 20px;
|
||||
position: relative;
|
||||
box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%);
|
||||
}
|
||||
|
||||
.head_box .id_text {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.head_box .day_text {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.head_box .genshin_logo {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 15px;
|
||||
width: 97px;
|
||||
}
|
||||
|
||||
.base_info {
|
||||
position: relative;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.uid {
|
||||
font-family: tttgbnumber;
|
||||
}
|
||||
|
||||
.data_box {
|
||||
border-radius: 15px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
padding: 20px 15px 5px 15px;
|
||||
background: #fff;
|
||||
box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab_lable {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: -8px;
|
||||
background: #d4b98c;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 15px 0 15px 15px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.data_line {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.data_line_item {
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
/*margin: 0 20px;*/
|
||||
}
|
||||
|
||||
.num {
|
||||
font-family: tttgbnumber;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.data_box .lable {
|
||||
font-size: 14px;
|
||||
color: #7f858a;
|
||||
line-height: 1;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
#chartContainer {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
#chartContainer2 {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 14px;
|
||||
font-family: "tttgbnumber";
|
||||
text-align: center;
|
||||
color: #7994a7;
|
||||
}
|
155
resources/genshin/pay_log/pay_log.html
Normal file
155
resources/genshin/pay_log/pay_log.html
Normal file
@ -0,0 +1,155 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="stylesheet" type="text/css" href="pay_log.css" />
|
||||
<link rel="preload" href="../../fonts/tttgbnumber.ttf" as="font">
|
||||
<link rel="preload" href="../gacha_log/img/提纳里.png" as="image">
|
||||
<style>
|
||||
.head_box {
|
||||
background: #fff url(../gacha_log/img/提纳里.png) no-repeat right center;
|
||||
background-size: cover;
|
||||
}
|
||||
</style>
|
||||
<title></title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container" id="container">
|
||||
<div class="head_box">
|
||||
<div class="id_text">ID: {{ uid }}</div>
|
||||
<h2 class="day_text">充值统计</h2>
|
||||
<img class="genshin_logo" src="./../../bot/help/background/genshin.png" alt=""/>
|
||||
</div>
|
||||
<div class="data_box">
|
||||
<div class="tab_lable">数据总览</div>
|
||||
<div class="data_line">
|
||||
{% for data in datas[:4] %}
|
||||
<div class="data_line_item">
|
||||
<div class="num">{{ data.value }}</div>
|
||||
<div class="lable">{{ data.name }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="data_line">
|
||||
{% for data in datas[4:8] %}
|
||||
<div class="data_line_item">
|
||||
<div class="num">{{ data.value }}</div>
|
||||
<div class="lable">{{ data.name }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="data_line">
|
||||
{% for data in datas[8:] %}
|
||||
<div class="data_line_item">
|
||||
<div class="num">{{ data.value }}</div>
|
||||
<div class="lable">{{ data.name }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="data_box">
|
||||
<div class="tab_lable">月份统计</div>
|
||||
<div id="chartContainer"></div>
|
||||
</div>
|
||||
<div class="data_box">
|
||||
<div class="tab_lable">详细统计</div>
|
||||
<div id="chartContainer2"></div>
|
||||
</div>
|
||||
<div class="logo"> Template By Yunzai-Bot & seven-plugin</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src="echarts.min.js"></script>
|
||||
|
||||
<script>
|
||||
const barData = {{ bar_data | tojson }};
|
||||
const myChart1 = echarts.init(document.querySelector('#chartContainer'), null, {renderer: 'svg'});
|
||||
const xData = barData.map(v => v.month)
|
||||
const yData = barData.map(v => v.amount / 10)
|
||||
const option = {
|
||||
animation: false,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xData
|
||||
},
|
||||
legend: {
|
||||
x:'left',
|
||||
y:'top',
|
||||
show: true,
|
||||
data: [{ name: '金额' }]
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name:'金额',
|
||||
data: yData,
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
normal: {
|
||||
label: {
|
||||
position: 'top',
|
||||
show: true,
|
||||
textStyle: {
|
||||
color: '#1e1f20',
|
||||
fontSize: 14,
|
||||
fontFamily: "tttgbnumber",
|
||||
}
|
||||
},
|
||||
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [{
|
||||
offset: 0,
|
||||
color: "#1268f3"
|
||||
}, {
|
||||
offset: 0.6,
|
||||
color: "#08a4fa"
|
||||
}, {
|
||||
offset: 1,
|
||||
color: "#01ccfe"
|
||||
}], false)
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
]
|
||||
};
|
||||
myChart1.setOption(option);
|
||||
|
||||
const pieData = {{ pie_data | tojson }};
|
||||
const myChart2 = echarts.init(document.querySelector('#chartContainer2'), null, { renderer: 'svg' });
|
||||
const option2 = {
|
||||
animation: false,
|
||||
title: {
|
||||
text: '{{ datas[0].value }}',
|
||||
subtext: '总充值',
|
||||
left: 'right'
|
||||
},
|
||||
legend: {
|
||||
orient: 'horizontal',
|
||||
bottom: 'left'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Access From',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
itemStyle: {
|
||||
normal: {
|
||||
label: {
|
||||
show: true,
|
||||
fontFamily: "tttgbnumber",
|
||||
formatter: '{b}:¥{c} ({d}%)'
|
||||
},
|
||||
labelLine: { show: true }
|
||||
}
|
||||
},
|
||||
data: pieData,
|
||||
}
|
||||
]
|
||||
};
|
||||
myChart2.setOption(option2);
|
||||
</script>
|
||||
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user