Support Pay Log

This commit is contained in:
omg-xtao 2023-01-07 16:01:31 +08:00 committed by GitHub
parent 16dfd5021e
commit d377c4c241
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 2508 additions and 1409 deletions

26
modules/pay_log/error.py Normal file
View 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
View 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
View 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
View 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))

View File

@ -13,6 +13,7 @@ from core.sign import SignServices
from core.user import UserService from core.user import UserService
from core.user.error import UserNotFoundError from core.user.error import UserNotFoundError
from modules.gacha_log.log import GachaLog 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.bot import get_args, get_chat as get_chat_with_cache
from utils.decorators.admins import bot_admins_rights_check from utils.decorators.admins import bot_admins_rights_check
from utils.helpers import get_genshin_client from utils.helpers import get_genshin_client
@ -31,6 +32,7 @@ class GetChat(Plugin):
self.user_service = user_service self.user_service = user_service
self.sign_service = sign_service self.sign_service = sign_service
self.gacha_log = GachaLog() self.gacha_log = GachaLog()
self.pay_log = PayLog()
async def parse_group_chat(self, chat: Chat, admins: List[ChatMember]) -> str: 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" 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')}" text += f"\n - 最后更新:{gacha_log.update_time.strftime('%Y-%m-%d %H:%M:%S')}"
else: else:
text += "\n抽卡记录:<code>未导入</code>" 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 return text
@handler(CommandHandler, command="get_chat", block=False) @handler(CommandHandler, command="get_chat", block=False)

2825
poetry.lock generated

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

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

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

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