初始化提交
This commit is contained in:
commit
2bc94fc6b7
162
.gitignore
vendored
Normal file
162
.gitignore
vendored
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
# ---> Python
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# 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/
|
||||||
|
|
8
README.md
Normal file
8
README.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# tools-ui
|
||||||
|
|
||||||
|
图形化界面版本的小工具
|
||||||
|
|
||||||
|
$env:PLAYWRIGHT_BROWSERS_PATH="0"
|
||||||
|
|
||||||
|
|
||||||
|
pyinstaller -F -w -i .\src\ui\icon\icon.ico .\src\app.py --hidden-import plyer.platforms.win.notification --add-data "./src/ui/icon;ui/icon/" --add-data "./src/ui/style.qss;ui/"
|
13
requirements.txt
Normal file
13
requirements.txt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
aiohttp==3.9.1
|
||||||
|
loguru==0.7.2
|
||||||
|
pika==1.3.2
|
||||||
|
playwright==1.40.0
|
||||||
|
pyperclip==1.8.2
|
||||||
|
PyQt6==6.6.1
|
||||||
|
PyQt6_sip==13.6.0
|
||||||
|
python_dateutil==2.8.2
|
||||||
|
QDarkStyle==3.2.3
|
||||||
|
Requests==2.31.0
|
||||||
|
schedule==1.2.1
|
||||||
|
SQLAlchemy==2.0.25
|
||||||
|
typing_extensions==4.9.0
|
13
src/__init__.py
Normal file
13
src/__init__.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from loguru import logger
|
||||||
|
from qtpy import QtCore
|
||||||
|
|
||||||
|
from src.core.util import resource_path
|
||||||
|
|
||||||
|
# logger.remove()
|
||||||
|
# logger.add(sys.stderr, level="INFO")
|
||||||
|
logger.add("{time:YYYY-MM}/{time:YYYY-MM-DD}.log", rotation="00:00", level="DEBUG", retention='1 day',
|
||||||
|
encoding='utf-8')
|
||||||
|
|
||||||
|
QtCore.QDir.addSearchPath('icons', resource_path('ui/icon/'))
|
||||||
|
|
||||||
|
__version__ = '0.0.1'
|
12
src/app.py
Normal file
12
src/app.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import qdarkstyle
|
||||||
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
|
||||||
|
from src.ui.app import Application
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app = QApplication([])
|
||||||
|
# 应用 qdarkstyle
|
||||||
|
app.setStyleSheet(qdarkstyle.load_stylesheet(qt_api='pyqt6'))
|
||||||
|
main_win = Application()
|
||||||
|
main_win.show()
|
||||||
|
app.exec()
|
6
src/config.ini
Normal file
6
src/config.ini
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[Credentials]
|
||||||
|
username = zayac
|
||||||
|
password = 123456
|
||||||
|
|
||||||
|
[Minimum]
|
||||||
|
minimum = True
|
0
src/core/__init__.py
Normal file
0
src/core/__init__.py
Normal file
93
src/core/api_request.py
Normal file
93
src/core/api_request.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import requests
|
||||||
|
from aiohttp import ClientError
|
||||||
|
|
||||||
|
from src import logger
|
||||||
|
from src.core.constant import BOT_TOKEN, CHAT_ID
|
||||||
|
from src.core.login import login
|
||||||
|
from src.core.message_client import send_message
|
||||||
|
from src.entity.account import Account
|
||||||
|
from src.entity.api import ApiResponse
|
||||||
|
|
||||||
|
|
||||||
|
def post(url: str, headers: dict, params: dict) -> ApiResponse[Any]:
|
||||||
|
try:
|
||||||
|
logger.debug(f"url:{url}")
|
||||||
|
logger.debug(f"headers:{headers}")
|
||||||
|
logger.debug(f"params:{params}")
|
||||||
|
|
||||||
|
response = requests.post(url=url, headers=headers, json=params)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
logger.debug(f'res:{response.json()}')
|
||||||
|
return ApiResponse(**response.json())
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logger.error(f"HTTP error occurred: {e}")
|
||||||
|
raise ClientError("HTTP error occurred")
|
||||||
|
|
||||||
|
|
||||||
|
async def async_post(url: str, headers: dict, params: dict) -> ApiResponse[Any]:
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
response = await session.post(url=url, headers=headers, json=params)
|
||||||
|
response.raise_for_status()
|
||||||
|
res = await response.json()
|
||||||
|
return ApiResponse(**res)
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
logger.error(f"HTTP error occurred: {e}")
|
||||||
|
raise ClientError("HTTP error occurred")
|
||||||
|
|
||||||
|
|
||||||
|
def account_post(url: str, account: Account, params: dict) -> ApiResponse[Any]:
|
||||||
|
for _ in range(3):
|
||||||
|
try:
|
||||||
|
if account.headers is None:
|
||||||
|
account = login(account)
|
||||||
|
api_res = post(url=account.url + url, headers=account.headers, params=params)
|
||||||
|
if api_res.status_code == 6000:
|
||||||
|
return api_res
|
||||||
|
elif api_res.status_code == 6008:
|
||||||
|
logger.error(api_res.message)
|
||||||
|
else:
|
||||||
|
logger.error(api_res.message)
|
||||||
|
logger.info('Retrying login')
|
||||||
|
account = login(account)
|
||||||
|
except ClientError as e:
|
||||||
|
logger.error(f"Client error occurred: {e}")
|
||||||
|
except TimeoutError as e:
|
||||||
|
logger.error(f"")
|
||||||
|
send_message(BOT_TOKEN, CHAT_ID, f'{account.url}:加载超时,请检查是否后台更换了链接')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unknown error: {e}")
|
||||||
|
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
send_message(BOT_TOKEN, CHAT_ID, f'{account.url}: Retry limit exceeded, please check the code')
|
||||||
|
logger.error(f"{account.url}: Retry limit exceeded, please check the code")
|
||||||
|
|
||||||
|
|
||||||
|
async def async_account_post(url: str, account: Account, params: dict) -> ApiResponse[Any]:
|
||||||
|
for _ in range(3):
|
||||||
|
try:
|
||||||
|
api_res = await async_post(url=account.url + url, headers=account.headers, params=params)
|
||||||
|
if api_res.status_code == 6000:
|
||||||
|
return api_res
|
||||||
|
else:
|
||||||
|
logger.error(api_res.message)
|
||||||
|
logger.info('Retrying login')
|
||||||
|
account = await asyncio.get_running_loop().run_in_executor(None, login, account)
|
||||||
|
except ClientError as e:
|
||||||
|
logger.error(f"Client error occurred: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unknown error: {e}")
|
||||||
|
|
||||||
|
# Add a delay before retrying
|
||||||
|
await asyncio.sleep(10)
|
||||||
|
|
||||||
|
send_message(BOT_TOKEN, CHAT_ID, f'{account.url}: Retry limit exceeded, please check the code')
|
||||||
|
logger.error("Retry limit exceeded, please check the code")
|
||||||
|
# You can raise a custom exception here or return an error status code
|
21
src/core/constant.py
Normal file
21
src/core/constant.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
BANNER_URL = '/agent/api/v1/front/banner'
|
||||||
|
|
||||||
|
VISUAL_LIST_URL = '/agent/api/v1/front/getAgentDataVisualList'
|
||||||
|
|
||||||
|
MEMBER_LIST_URL = '/agent/api/v1/member/list'
|
||||||
|
|
||||||
|
MEMBER_DETAIL_URL = '/agent/api/v1/member/detail'
|
||||||
|
|
||||||
|
PAY_RECORD_URL = '/agent/api/v1/payRecords/list'
|
||||||
|
|
||||||
|
PAY_RECORD_LIST_URL = '/agent/api/v1/member/payRecordList'
|
||||||
|
|
||||||
|
FINANCE_URL = '/agent/api/v1/finance/excel/total'
|
||||||
|
|
||||||
|
BOT_TOKEN = '6013830443:AAGzq1Tgtr_ZejU7bv0mab14xOwi0_64d0w'
|
||||||
|
# 工作号id
|
||||||
|
CHAT_ID = '6054562838'
|
||||||
|
# 冲!群组id
|
||||||
|
GROUP_ID = '-1002122455730'
|
||||||
|
# 报数群组id
|
||||||
|
COUNT_GROUP_ID = '-4062683798'
|
130
src/core/login.py
Normal file
130
src/core/login.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from playwright.sync_api import Position, TimeoutError, sync_playwright
|
||||||
|
|
||||||
|
from src import logger
|
||||||
|
from src.entity.account import Account
|
||||||
|
from src.entity.database import db
|
||||||
|
from src.ui import global_signals
|
||||||
|
|
||||||
|
|
||||||
|
def on_request(request, account: Account):
|
||||||
|
if 'banner' in request.url:
|
||||||
|
headers = request.headers
|
||||||
|
account.headers = headers
|
||||||
|
logger.info(f'Headers for account {account.name}: {headers}')
|
||||||
|
persistence(account, headers)
|
||||||
|
# 通知app数据更新了
|
||||||
|
global_signals.user_data_updated.emit()
|
||||||
|
|
||||||
|
|
||||||
|
def login(account: Account) -> Account:
|
||||||
|
logger.info(f'Starting login for account: {account.name}')
|
||||||
|
try:
|
||||||
|
with sync_playwright() as playwright:
|
||||||
|
account = perform_login(playwright, account)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Error during login for account {account.name}: {e}', exc_info=True)
|
||||||
|
handle_login_failure(account)
|
||||||
|
return account
|
||||||
|
|
||||||
|
|
||||||
|
def perform_login(playwright, account: Account) -> Account:
|
||||||
|
browser = playwright.chromium.launch(headless=False)
|
||||||
|
context = browser.new_context()
|
||||||
|
page = context.new_page()
|
||||||
|
page.goto(account.url)
|
||||||
|
fill_login_form(page, account)
|
||||||
|
if handle_captcha(page):
|
||||||
|
account.headers = capture_request_headers(page, account)
|
||||||
|
logger.info('登录成功')
|
||||||
|
else:
|
||||||
|
logger.error('登录失败或验证码处理失败')
|
||||||
|
close_resources(page, context, browser)
|
||||||
|
return account
|
||||||
|
|
||||||
|
|
||||||
|
def fill_login_form(page, account: Account):
|
||||||
|
username_input = page.get_by_placeholder('用户名')
|
||||||
|
password_input = page.get_by_placeholder('密码')
|
||||||
|
username_input.click()
|
||||||
|
username_input.fill(account.username)
|
||||||
|
password_input.click()
|
||||||
|
password_input.fill(account.password)
|
||||||
|
page.locator("div").filter(has_text=re.compile(r"^登录$")).get_by_role("button").click()
|
||||||
|
logger.info(f'{account.name}登录ing...........')
|
||||||
|
|
||||||
|
|
||||||
|
def handle_captcha(page) -> bool:
|
||||||
|
try:
|
||||||
|
validate_code = page.wait_for_selector('.geetest_box', state='visible')
|
||||||
|
time.sleep(1)
|
||||||
|
return process_validate_code(validate_code)
|
||||||
|
except TimeoutError:
|
||||||
|
logger.error('超时了')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def process_validate_code(validate_code):
|
||||||
|
validate_code_buffer = validate_code.screenshot()
|
||||||
|
img = base64.b64encode(validate_code_buffer).decode('utf-8')
|
||||||
|
res = base64_api(img=img)
|
||||||
|
if '|' in res:
|
||||||
|
click_captcha_positions(validate_code, res)
|
||||||
|
validate_code.query_selector('.geetest_submit').click()
|
||||||
|
validate_code.wait_for_element_state('hidden')
|
||||||
|
logger.debug('验证码点击成功')
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(res)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def click_captcha_positions(validate_code, positions_str):
|
||||||
|
for part in positions_str.split('|'):
|
||||||
|
x, y = part.split(',')
|
||||||
|
validate_code.click(position=Position(x=int(x), y=int(y)))
|
||||||
|
time.sleep(.5)
|
||||||
|
|
||||||
|
|
||||||
|
def capture_request_headers(page, account: Account):
|
||||||
|
page.on('request', lambda request: on_request(request, account))
|
||||||
|
page.wait_for_url(f'{account.url}/app/home?showWelcome=false')
|
||||||
|
return account.headers
|
||||||
|
|
||||||
|
|
||||||
|
def close_resources(page, context, browser):
|
||||||
|
page.close()
|
||||||
|
context.close()
|
||||||
|
browser.close()
|
||||||
|
|
||||||
|
|
||||||
|
def handle_login_failure(account: Account):
|
||||||
|
# 处理登录失败的情况
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def base64_api(uname='luffy230505', pwd='qwer12345', img='', typeid=20):
|
||||||
|
logger.info('Calling base64_api')
|
||||||
|
data = {"username": uname, "password": pwd, "typeid": typeid, "image": img,
|
||||||
|
'softid': '8d13df0efe074035b54ee9c2bef85106'}
|
||||||
|
result = json.loads(requests.post("http://api.ttshitu.com/predict", json=data).text)
|
||||||
|
if result['success']:
|
||||||
|
return result["data"]["result"]
|
||||||
|
else:
|
||||||
|
return result["message"]
|
||||||
|
|
||||||
|
|
||||||
|
def persistence(account: Account, headers: dict):
|
||||||
|
logger.info(f'Persisting headers for account {account.name}')
|
||||||
|
with db.Session() as session:
|
||||||
|
db_account = session.query(Account).filter(Account.username == account.username,
|
||||||
|
Account.password == account.password).one()
|
||||||
|
db_account.headers = headers
|
||||||
|
db_account.x_api_token = headers['x-api-token']
|
||||||
|
session.commit()
|
||||||
|
logger.info(f'Headers persisted for account {account.name}')
|
34
src/core/message_client.py
Normal file
34
src/core/message_client.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import pika
|
||||||
|
|
||||||
|
|
||||||
|
class MessageClient:
|
||||||
|
def __init__(self):
|
||||||
|
self.credentials = pika.PlainCredentials('bot', 'xiaomi123')
|
||||||
|
self.connection = pika.BlockingConnection(
|
||||||
|
pika.ConnectionParameters('164.155.224.131', 5672, '/', credentials=self.credentials))
|
||||||
|
self.channel = self.connection.channel()
|
||||||
|
self.channel.queue_declare(queue='message_queue')
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def send_message(self, bot_token, target_id, message):
|
||||||
|
self.channel.basic_publish(exchange='', routing_key='message_queue', body=f'{bot_token}|{target_id}|{message}')
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
def send_message(bot_token, target_id, message):
|
||||||
|
with MessageClient() as client:
|
||||||
|
client.send_message(bot_token, target_id, escape_markdown(message))
|
||||||
|
|
||||||
|
|
||||||
|
def escape_markdown(text):
|
||||||
|
escape_chars = ['_', '[', ']', '(', ')', '~', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']
|
||||||
|
for char in escape_chars:
|
||||||
|
text = text.replace(char, '\\' + char)
|
||||||
|
return text
|
58
src/core/message_server.py
Normal file
58
src/core/message_server.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
import pika
|
||||||
|
import requests
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
|
def _send_message_to_user(bot_token, target_id, message):
|
||||||
|
base_url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
|
||||||
|
params = {
|
||||||
|
"chat_id": target_id,
|
||||||
|
"text": message,
|
||||||
|
"parse_mode": "MarkdownV2"
|
||||||
|
}
|
||||||
|
max_retry = 3
|
||||||
|
retry_count = 0
|
||||||
|
while retry_count < max_retry:
|
||||||
|
response = requests.post(base_url, params=params)
|
||||||
|
if response.status_code == 200:
|
||||||
|
logger.debug(f'消息发送成功:{message}')
|
||||||
|
return # 如果发送成功,立即返回
|
||||||
|
else:
|
||||||
|
logger.debug('消息发送失败,重试中...')
|
||||||
|
logger.error(response.text)
|
||||||
|
time.sleep(10)
|
||||||
|
retry_count += 1
|
||||||
|
|
||||||
|
logger.debug('消息发送失败')
|
||||||
|
|
||||||
|
|
||||||
|
# 你的消息发送逻辑
|
||||||
|
|
||||||
|
class MessageServer:
|
||||||
|
def __init__(self):
|
||||||
|
self.credentials = pika.PlainCredentials('bot', 'xiaomi123')
|
||||||
|
self.connection = pika.BlockingConnection(
|
||||||
|
pika.ConnectionParameters('164.155.224.131', 5672, '/', credentials=self.credentials))
|
||||||
|
self.channel = self.connection.channel()
|
||||||
|
self.channel.queue_declare(queue='message_queue')
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
try:
|
||||||
|
def callback(ch, method, properties, body):
|
||||||
|
logger.info(body.decode())
|
||||||
|
bot_token, target_id, message = body.decode().split('|')
|
||||||
|
_send_message_to_user(bot_token, target_id, message)
|
||||||
|
|
||||||
|
self.channel.basic_consume(queue='message_queue', on_message_callback=callback, auto_ack=True)
|
||||||
|
self.channel.start_consuming()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
server = MessageServer()
|
||||||
|
server.start()
|
62
src/core/salary.py
Normal file
62
src/core/salary.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from src.core.util import (get_first_day_by_str, get_first_day_of_last_month,
|
||||||
|
get_last_day_of_last_month)
|
||||||
|
from src.entity.database import db
|
||||||
|
from src.entity.finance import get_finance
|
||||||
|
from src.entity.user import User
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_commission(profit, employee_type, target_completion):
|
||||||
|
# 定义提成点位
|
||||||
|
commission_rates = {
|
||||||
|
"1": [(0, 0.03), (100001, 0.08), (300001, 0.10), (500001, 0.11), (700001, 0.12), (1000001, 0.13)],
|
||||||
|
"2": [(0, 0.02), (100001, 0.05), (300001, 0.07), (500001, 0.08), (700001, 0.09), (1000001, 0.10)],
|
||||||
|
"3": [(0, 0.02), (100001, 0.03), (300001, 0.04), (500001, 0.05), (700001, 0.06), (1000001, 0.07)]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 根据负盈利选择正确的提成点位
|
||||||
|
rates = commission_rates[employee_type]
|
||||||
|
rate = 0
|
||||||
|
for r in rates:
|
||||||
|
if profit >= r[0]:
|
||||||
|
rate = r[1]
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
# 计算提成
|
||||||
|
commission = profit * target_completion * Decimal(str(rate))
|
||||||
|
return commission
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_salary(employee_type, agent_profit):
|
||||||
|
if employee_type == "3":
|
||||||
|
base_salary = 30000
|
||||||
|
else:
|
||||||
|
base_salary = 12000
|
||||||
|
total_salary = 0
|
||||||
|
for profit in agent_profit:
|
||||||
|
total_salary += calculate_commission(profit, employee_type, 1)
|
||||||
|
if total_salary > 70000:
|
||||||
|
base_salary = 0
|
||||||
|
return total_salary + base_salary
|
||||||
|
|
||||||
|
|
||||||
|
def get_salary(user: User, date: str):
|
||||||
|
profits = []
|
||||||
|
start_date = get_first_day_by_str(date)
|
||||||
|
for account in user.accounts:
|
||||||
|
finance = get_finance(account, start_date, date)
|
||||||
|
print(f'{finance.name}: {finance.netProfit}')
|
||||||
|
profits.append(int(float(finance.netProfit)))
|
||||||
|
return f'方式一:{calculate_salary("1", profits)}\n方式二:{calculate_salary("2", profits)}\n方式三:{calculate_salary("3", profits)}'
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_month_salary(user: User):
|
||||||
|
profits = []
|
||||||
|
for account in user.accounts:
|
||||||
|
finance = get_finance(account, get_first_day_of_last_month(), get_last_day_of_last_month())
|
||||||
|
print(f'{finance.name}: {finance.netProfit}')
|
||||||
|
profits.append(int(float(finance.netProfit)))
|
||||||
|
return f'方式一:{calculate_salary("1", profits)}\n方式二:{calculate_salary("2", profits)}\n方式三:{calculate_salary("3", profits)}'
|
||||||
|
|
91
src/core/util.py
Normal file
91
src/core/util.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, time, timedelta
|
||||||
|
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
|
||||||
|
def get_curr_month():
|
||||||
|
now = datetime.now()
|
||||||
|
if now.time() < time(13, 0):
|
||||||
|
now -= timedelta(days=1)
|
||||||
|
return now.strftime("%Y-%m")
|
||||||
|
|
||||||
|
|
||||||
|
def get_curr_day():
|
||||||
|
return datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
|
def get_one_day_before():
|
||||||
|
return (datetime.now().date() - timedelta(days=1)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
|
def get_first_day_month():
|
||||||
|
current_date = datetime.now().date()
|
||||||
|
first_day_of_month = current_date.replace(day=1)
|
||||||
|
return first_day_of_month.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
|
# 计算两个时间差值 返回值秒
|
||||||
|
def get_difference(time_str1, time: datetime) -> float:
|
||||||
|
time1 = datetime.strptime(time_str1, '%Y-%m-%d %H:%M:%S')
|
||||||
|
time_difference = time - time1
|
||||||
|
return abs(time_difference.total_seconds())
|
||||||
|
|
||||||
|
|
||||||
|
def resource_path(relative_path):
|
||||||
|
""" 获取资源的绝对路径。用于访问打包后的资源文件。 """
|
||||||
|
if hasattr(sys, '_MEIPASS'):
|
||||||
|
# 如果程序被打包,则使用临时目录
|
||||||
|
base_path = sys._MEIPASS
|
||||||
|
else:
|
||||||
|
# 如果程序未被打包,则使用当前目录
|
||||||
|
base_path = os.path.abspath(".")
|
||||||
|
|
||||||
|
return os.path.join(base_path, relative_path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_day_of_last_month():
|
||||||
|
last_day_of_last_month = datetime.now().replace(day=1) - relativedelta(days=1)
|
||||||
|
return last_day_of_last_month.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
|
def get_first_day_of_last_month():
|
||||||
|
last_day_of_last_month = datetime.now().replace(day=1) - relativedelta(days=1)
|
||||||
|
first_day_of_last_month = last_day_of_last_month.replace(day=1)
|
||||||
|
return first_day_of_last_month.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
|
def get_first_day_by_str(date_str):
|
||||||
|
# 解析日期字符串
|
||||||
|
date = datetime.strptime(date_str, "%Y-%m-%d")
|
||||||
|
|
||||||
|
# 获取该月的第一天
|
||||||
|
first_day = date.replace(day=1)
|
||||||
|
|
||||||
|
return first_day.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
|
def convert_data(data):
|
||||||
|
"""
|
||||||
|
尝试将数据转换为最合适的类型。
|
||||||
|
"""
|
||||||
|
# 尝试转换为浮点数
|
||||||
|
try:
|
||||||
|
float_data = float(data)
|
||||||
|
# 如果转换后的数据和原始数据相同,则尝试转换为整数
|
||||||
|
if float_data.is_integer():
|
||||||
|
return int(float_data)
|
||||||
|
return float_data
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 尝试转换为日期时间
|
||||||
|
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d", "%H:%M:%S"):
|
||||||
|
try:
|
||||||
|
return datetime.strptime(data, fmt)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 如果所有转换都失败,保留为字符串
|
||||||
|
return data
|
64
src/core/喜报.py
Normal file
64
src/core/喜报.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import time
|
||||||
|
from concurrent.futures.thread import ThreadPoolExecutor
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
from util import get_curr_day
|
||||||
|
|
||||||
|
from src.core.constant import BOT_TOKEN, GROUP_ID
|
||||||
|
from src.core.message_client import send_message
|
||||||
|
from src.entity.account import Account
|
||||||
|
from src.entity.banner_info import BannerInfo, get_banner_info
|
||||||
|
from src.entity.database import db
|
||||||
|
from src.entity.member import get_today_new_member_list
|
||||||
|
from src.entity.pay_record import get_latest_deposit_user
|
||||||
|
from src.entity.user import User
|
||||||
|
|
||||||
|
|
||||||
|
def query_banner_info(account: Account):
|
||||||
|
last_banner_info = get_banner_info(account)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
date = get_curr_day()
|
||||||
|
banner_info = get_banner_info(account)
|
||||||
|
logger.debug(f'{account.name}请求成功:{banner_info}')
|
||||||
|
logger.info(
|
||||||
|
f'{time.strftime("%Y-%m-%d %H:%M:%S")} {account.name}:注册:{banner_info.registerMembers},首存:{banner_info.firstDepositNum},负盈利:{banner_info.netWinLose},有效:{banner_info.effectiveNew},活跃:{banner_info.activeMembers}')
|
||||||
|
if banner_info.registerMembers > last_banner_info.registerMembers:
|
||||||
|
register_count = banner_info.registerMembers - last_banner_info.registerMembers
|
||||||
|
logger.debug(f'新注册用户数为 {register_count}')
|
||||||
|
members = get_today_new_member_list(account, register_count)
|
||||||
|
if members is not None:
|
||||||
|
names = ','.join([f'`{member.name}`' for member in members])
|
||||||
|
else:
|
||||||
|
names = 'unknown'
|
||||||
|
msg = f'👏 {account.name} 注册:{register_count} 用户: {names} 总数: {banner_info.registerMembers}'
|
||||||
|
send_message(BOT_TOKEN, GROUP_ID, msg)
|
||||||
|
logger.info(f'发送的消息: {msg}')
|
||||||
|
|
||||||
|
last_banner_info = banner_info
|
||||||
|
|
||||||
|
if banner_info.firstDepositNum > last_banner_info.firstDepositNum:
|
||||||
|
count = banner_info.firstDepositNum - last_banner_info.firstDepositNum
|
||||||
|
member_details_list = get_latest_deposit_user(account, count)
|
||||||
|
msg = '\n'.join(
|
||||||
|
[f"用户: `{member_detail.name}`, 首存金额: *{member_detail.deposit}*" for member_detail in
|
||||||
|
member_details_list])
|
||||||
|
send_message(BOT_TOKEN, GROUP_ID,
|
||||||
|
f'🎉 {account.name} 首存:{count} {msg} 总数:*{banner_info.firstDepositNum}*')
|
||||||
|
logger.info(f'发送的消息: {msg}')
|
||||||
|
|
||||||
|
last_banner_info = banner_info
|
||||||
|
time.sleep(60)
|
||||||
|
except Exception as e:
|
||||||
|
send_message(BOT_TOKEN, GROUP_ID, str(e))
|
||||||
|
logger.exception(f'发生未知错误:{e} ')
|
||||||
|
time.sleep(10)
|
||||||
|
return query_banner_info(account)
|
||||||
|
|
||||||
|
|
||||||
|
def get_banner_info_by_user(user: User) -> List[BannerInfo]:
|
||||||
|
with ThreadPoolExecutor(max_workers=len(user.accounts)) as executor:
|
||||||
|
futures = [executor.submit(get_banner_info, account) for account in user.accounts]
|
||||||
|
return [future.result() for future in futures]
|
||||||
|
|
44
src/core/定时任务.py
Normal file
44
src/core/定时任务.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
import schedule
|
||||||
|
from loguru import logger
|
||||||
|
from 报数 import get_net_win, text_count_by_user
|
||||||
|
from 查询存款失败用户 import get_pay_failed_by_user
|
||||||
|
|
||||||
|
from src.core.constant import BOT_TOKEN, COUNT_GROUP_ID
|
||||||
|
from src.core.message_client import send_message
|
||||||
|
from src.entity.user import get_user_by_username_and_password
|
||||||
|
|
||||||
|
|
||||||
|
def job_count(username, password):
|
||||||
|
logger.info(f'Running job_count for username: {username}')
|
||||||
|
user = get_user_by_username_and_password(username, password)
|
||||||
|
send_message(BOT_TOKEN, COUNT_GROUP_ID, text_count_by_user(user))
|
||||||
|
logger.info(f'Finished job_count for username: {username}')
|
||||||
|
|
||||||
|
|
||||||
|
def query_failed_deposit(username, password):
|
||||||
|
logger.info(f'Running query_failed_deposit for username: {username}')
|
||||||
|
user = get_user_by_username_and_password(username, password)
|
||||||
|
send_message(BOT_TOKEN, COUNT_GROUP_ID, get_pay_failed_by_user(user))
|
||||||
|
logger.info(f'Finished query_failed_deposit for username: {username}')
|
||||||
|
|
||||||
|
|
||||||
|
def query_net_win(username, password) -> None:
|
||||||
|
logger.info(f'Running query_net_win for username: {username}')
|
||||||
|
user = get_user_by_username_and_password(username, password)
|
||||||
|
send_message(BOT_TOKEN, COUNT_GROUP_ID, get_net_win(user))
|
||||||
|
logger.info(f'Finished query_net_win for username: {username}')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logger.info('Starting scheduled tasks')
|
||||||
|
times = ['10:50', '14:40', '17:40', '20:40', '23:59']
|
||||||
|
for time_str in times:
|
||||||
|
schedule.every().day.at(time_str).do(job_count, 'zayac', '123456')
|
||||||
|
schedule.every().day.at(time_str).do(query_net_win, 'zayac', '123456')
|
||||||
|
schedule.every().day.at('23:59').do(query_failed_deposit, 'zayac', '123456')
|
||||||
|
while True:
|
||||||
|
schedule.run_pending()
|
||||||
|
time.sleep(1)
|
||||||
|
logger.info('Running scheduled tasks')
|
53
src/core/报数.py
Normal file
53
src/core/报数.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import pyperclip
|
||||||
|
|
||||||
|
from src import logger
|
||||||
|
from src.core import util
|
||||||
|
from src.entity.database import db
|
||||||
|
from src.entity.finance import Finance, get_finance
|
||||||
|
from src.entity.user import User
|
||||||
|
from src.entity.visual_list import VisualInfo, get_curr_data, get_visual_list
|
||||||
|
|
||||||
|
|
||||||
|
def get_statics(account, date=util.get_curr_day()) -> VisualInfo:
|
||||||
|
params = {"monthDate": util.get_curr_month()}
|
||||||
|
data = get_visual_list(account, params)
|
||||||
|
# 合并列表并创建日期到数据的映射
|
||||||
|
date_map = {item.staticsDate: item for item in data.curData + data.lastData}
|
||||||
|
|
||||||
|
# 直接通过日期获取数据
|
||||||
|
return date_map.get(date)
|
||||||
|
|
||||||
|
|
||||||
|
def count_by_user(user: User, date: str):
|
||||||
|
accounts = user.accounts
|
||||||
|
with ThreadPoolExecutor(max_workers=len(accounts)) as t:
|
||||||
|
futures = [t.submit(get_statics, account, date) for account in accounts]
|
||||||
|
return [future.result() for future in futures]
|
||||||
|
|
||||||
|
|
||||||
|
def text_count_by_user(user: User, date: str) -> str:
|
||||||
|
visual_list = count_by_user(user, date)
|
||||||
|
text = '\n\n'.join(
|
||||||
|
f'{result.agentName}\n注册:{result.isNew}\n首存:{result.firstCount}\n日活:{int(result.countBets)}\n流水:{int(result.bets)}'
|
||||||
|
for result in visual_list
|
||||||
|
)
|
||||||
|
logger.info(f'Generated text: {text}')
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def get_finances_by_user(user: User, date) -> List[Finance]:
|
||||||
|
accounts = user.accounts
|
||||||
|
start_date = util.get_first_day_by_str(date)
|
||||||
|
with ThreadPoolExecutor(max_workers=len(accounts)) as t:
|
||||||
|
futures = [t.submit(get_finance, account, start_date, date) for account in accounts]
|
||||||
|
return [future.result() for future in futures]
|
||||||
|
|
||||||
|
|
||||||
|
def get_net_win(user: User, date: str) -> str:
|
||||||
|
finances = get_finances_by_user(user, date)
|
||||||
|
finance_strings = [f"{finance.name}: {finance.netProfit}" for finance in finances]
|
||||||
|
logger.info(f'Finance strings: {finance_strings}')
|
||||||
|
return '\n'.join(finance_strings)
|
82
src/core/查询存款失败用户.py
Normal file
82
src/core/查询存款失败用户.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from src.core import api_request
|
||||||
|
from src.core.constant import PAY_RECORD_LIST_URL
|
||||||
|
from src.core.util import get_first_day_month
|
||||||
|
from src.entity.account import Account
|
||||||
|
from src.entity.member import MemberList, get_member_list
|
||||||
|
from src.entity.user import User
|
||||||
|
|
||||||
|
|
||||||
|
def get_pay_record_list(account: Account, date: str) -> Dict[str, List[str]]:
|
||||||
|
logger.info(f'Getting pay record list for account: {account.name} and date: {date}')
|
||||||
|
_names = {'name': account.name, 'names': []}
|
||||||
|
params = {
|
||||||
|
"pageNum": 1,
|
||||||
|
"pageSize": 100,
|
||||||
|
"registerSort": 1,
|
||||||
|
"drawSort": -1,
|
||||||
|
"depositSort": -1,
|
||||||
|
"lastLoginTimeSort": -1,
|
||||||
|
"name": "",
|
||||||
|
"minPay": None,
|
||||||
|
"maxPay": None,
|
||||||
|
"startDate": get_first_day_month(),
|
||||||
|
"registerStartDate": date,
|
||||||
|
"endDate": date,
|
||||||
|
"registerEndDate": date,
|
||||||
|
"firstPayStartTime": "",
|
||||||
|
"firstPayEndTime": "",
|
||||||
|
"isBet": "0",
|
||||||
|
"tagsFlag": "1"
|
||||||
|
}
|
||||||
|
member_list = get_member_list(account, params)
|
||||||
|
if member_list is not None and len(member_list) > 0:
|
||||||
|
with ThreadPoolExecutor(max_workers=len(member_list)) as executor:
|
||||||
|
futures = [executor.submit(get_pay_record, account, member, date) for member in member_list]
|
||||||
|
for future in futures:
|
||||||
|
result = future.result()
|
||||||
|
if result:
|
||||||
|
_names['names'].append(result)
|
||||||
|
logger.info(f'Finished getting pay record list for account: {account.name} and date: {date}')
|
||||||
|
return _names
|
||||||
|
|
||||||
|
|
||||||
|
def get_pay_record(account: Account, member: MemberList, date: str) -> Optional[str]:
|
||||||
|
logger.info(f'Getting pay record for account: {account.name}, member: {member.name}, and date: {date}')
|
||||||
|
params = {
|
||||||
|
"pageNum": 1,
|
||||||
|
"pageSize": 15,
|
||||||
|
"id": member.id,
|
||||||
|
"startDate": get_first_day_month(),
|
||||||
|
"endDate": date
|
||||||
|
}
|
||||||
|
res = api_request.account_post(PAY_RECORD_LIST_URL, account=account, params=params)
|
||||||
|
if int(res.data['orderAmountTotal']) > 0 and int(res.data['scoreAmountTotal']) == 0:
|
||||||
|
return member.name
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_pay_failed_by_user(user: User, date: str) -> Optional[str]:
|
||||||
|
logger.info(f'Getting pay failed by user: {user.username}')
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=len(user.accounts)) as executor:
|
||||||
|
futures = [executor.submit(get_pay_record_list, account, date) for account in user.accounts]
|
||||||
|
|
||||||
|
# 使用列表推导式构建结果字符串
|
||||||
|
text_lines = [
|
||||||
|
"{}\n{}".format(res['name'], '\n'.join(res['names']))
|
||||||
|
for future in futures if (res := future.result())['names']
|
||||||
|
]
|
||||||
|
|
||||||
|
text = '\n'.join(text_lines)
|
||||||
|
|
||||||
|
if not text:
|
||||||
|
logger.info('无存款失败用户')
|
||||||
|
return '无存款失败用户'
|
||||||
|
|
||||||
|
logger.info(text)
|
||||||
|
return text
|
3
src/entity/__init__.py
Normal file
3
src/entity/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from src import logger
|
||||||
|
|
||||||
|
from . import account, user
|
34
src/entity/account.py
Normal file
34
src/entity/account.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from sqlalchemy import JSON as Sql_JSON
|
||||||
|
from sqlalchemy import Enum as Sql_Enum
|
||||||
|
from sqlalchemy import ForeignKey, String
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from src.entity.database import db
|
||||||
|
|
||||||
|
|
||||||
|
# 账号类型枚举
|
||||||
|
class AccountType(Enum):
|
||||||
|
ky = 0
|
||||||
|
hth = 1
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Account(db.Base):
|
||||||
|
__tablename__ = 'ky_account'
|
||||||
|
|
||||||
|
id: Mapped[db.int_pk]
|
||||||
|
username: Mapped[db.str_required_unique] = mapped_column(comment='账号')
|
||||||
|
password: Mapped[db.str_required] = mapped_column(comment='密码')
|
||||||
|
type: Mapped[AccountType] = mapped_column(Sql_Enum(AccountType), default=AccountType.ky, nullable=False,
|
||||||
|
comment='类型 ky hth')
|
||||||
|
name: Mapped[str] = mapped_column(String(64), nullable=True, comment='别名')
|
||||||
|
url: Mapped[str] = mapped_column(String(128), nullable=False, comment='url')
|
||||||
|
x_api_token: Mapped[str] = mapped_column(String(64), nullable=True, comment='x-api-token')
|
||||||
|
headers: Mapped[json] = mapped_column(Sql_JSON, nullable=True, comment='headers')
|
||||||
|
user_id: Mapped[int] = mapped_column(ForeignKey('ky_user.id'),
|
||||||
|
nullable=False, comment='关联的用户id')
|
||||||
|
user: Mapped['user.User'] = relationship('user.User', back_populates='accounts')
|
12
src/entity/api.py
Normal file
12
src/entity/api.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Generic, TypeVar
|
||||||
|
|
||||||
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ApiResponse(Generic[T]):
|
||||||
|
data: T
|
||||||
|
message: str
|
||||||
|
status_code: int
|
||||||
|
|
32
src/entity/banner_info.py
Normal file
32
src/entity/banner_info.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from src.core.api_request import account_post
|
||||||
|
from src.core.constant import BANNER_URL
|
||||||
|
from src.entity.account import Account
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BannerInfo:
|
||||||
|
registerMembers: int
|
||||||
|
firstDepositNum: int
|
||||||
|
netWinLose: float
|
||||||
|
totalMembers: int
|
||||||
|
activeMembers: int
|
||||||
|
commissionLevel: float
|
||||||
|
depositIncrease: float
|
||||||
|
effectiveNew: int
|
||||||
|
profit: float
|
||||||
|
registerIncrease: float
|
||||||
|
agentCode: str
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
if value is not None:
|
||||||
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
|
||||||
|
def get_banner_info(account: Account) -> BannerInfo:
|
||||||
|
api_response = account_post(url=BANNER_URL, account=account, params={})
|
||||||
|
banner_info = BannerInfo(**api_response.data)
|
||||||
|
banner_info.agentCode = account.username
|
||||||
|
return banner_info
|
62
src/entity/database.py
Normal file
62
src/entity/database.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
"""
|
||||||
|
初始化数据的脚本
|
||||||
|
"""
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import Integer, String
|
||||||
|
from sqlalchemy.orm import declarative_base, mapped_column, sessionmaker
|
||||||
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
int_pk = Annotated[int, mapped_column(Integer, primary_key=True)]
|
||||||
|
str_required_unique = Annotated[str, mapped_column(String(64), unique=True, nullable=False)]
|
||||||
|
str_required = Annotated[str, mapped_column(String(64), nullable=False)]
|
||||||
|
|
||||||
|
def __init__(self, db_url):
|
||||||
|
self.engine = sa.create_engine(db_url, echo=False, future=True, pool_size=10, pool_recycle=3600)
|
||||||
|
self.Session = sessionmaker(bind=self.engine)
|
||||||
|
self.Base = declarative_base()
|
||||||
|
|
||||||
|
def initialize_data(self):
|
||||||
|
from src.entity.account import Account, AccountType
|
||||||
|
from src.entity.user import User
|
||||||
|
session = self.Session()
|
||||||
|
|
||||||
|
user = User(username='zayac', password='123456', name='蓝胖', email='stupidzayac@gmail')
|
||||||
|
account = [Account(username='ky3tg107032', password='tg666888', type=AccountType.ky,
|
||||||
|
headers={'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120"',
|
||||||
|
'x-api-token': 'DL_21f490b93c4fc4db0faf5d0458b6bee2',
|
||||||
|
'x-api-xxx': '7306dfacdddf5be53c9be3588d9669b9a0d65d0e2fc51e4dd61aa551b5eead68',
|
||||||
|
'x-api-version': '1.0.0', 'sec-ch-ua-mobile': '?0',
|
||||||
|
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'x-api-uuid': '22958E8F-C36B-4C81-AF3F-02A7E6A4DCF2',
|
||||||
|
'referer': 'https://www.arv5e7.com:6001/app/home?showWelcome=false',
|
||||||
|
'x-api-client': 'agent_web',
|
||||||
|
'sec-ch-ua-platform': '"Windows"'},
|
||||||
|
url='https://www.arv5e7.com:6001',
|
||||||
|
name='ky32线',
|
||||||
|
x_api_token='DL_21f490b93c4fc4db0faf5d0458b6bee2',
|
||||||
|
user=user),
|
||||||
|
Account(username='htg51120', password='tg666888', type=AccountType.hth,
|
||||||
|
headers={'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120"',
|
||||||
|
'x-api-token': 'DL_f55c9588cf34b7e81413bc23f0c9d114',
|
||||||
|
'x-api-xxx': '40ef612c97c7673f32c7f1367ed5349519350adc2638f2245a53d39527e50f2c',
|
||||||
|
'x-api-version': '1.0.0', 'sec-ch-ua-mobile': '?0',
|
||||||
|
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'x-api-uuid': '2140DC1C-4343-4AF2-9B20-814457DBC3EC',
|
||||||
|
'referer': 'https://www.oy9eux.com:9514/app/home?showWelcome=false',
|
||||||
|
'x-api-client': 'agent_web', 'sec-ch-ua-platform': '"Windows"'},
|
||||||
|
url='https://www.oy9eux.com:9514',
|
||||||
|
name='hth20线',
|
||||||
|
x_api_token='DL_f55c9588cf34b7e81413bc23f0c9d114',
|
||||||
|
user=user
|
||||||
|
)]
|
||||||
|
self.Base.metadata.create_all(self.engine)
|
||||||
|
session.add(user)
|
||||||
|
session.add_all(account)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
db = Database('mysql+mysqlconnector://ky_tools:hMXWFxRstbkaCDDr@164.155.224.131:13306/ky_tools')
|
47
src/entity/finance.py
Normal file
47
src/entity/finance.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from src.core import util
|
||||||
|
from src.core.api_request import account_post
|
||||||
|
from src.core.constant import FINANCE_URL
|
||||||
|
from src.entity.account import Account
|
||||||
|
|
||||||
|
'''
|
||||||
|
财务报表
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Finance(object):
|
||||||
|
profit: Decimal
|
||||||
|
promo: Decimal
|
||||||
|
thirdPartySpend: Decimal
|
||||||
|
netProfit: Decimal
|
||||||
|
deposit: Decimal
|
||||||
|
draw: Decimal
|
||||||
|
rebate: Decimal
|
||||||
|
adjust: Decimal
|
||||||
|
netAmount: Decimal
|
||||||
|
betAmount: Decimal
|
||||||
|
handlingFee: Decimal
|
||||||
|
partnershipProfit: Decimal
|
||||||
|
name: str
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
if value is not None:
|
||||||
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
|
||||||
|
def get_finance(account: Account, start_date=util.get_first_day_month(), end_date=util.get_curr_day()) -> Finance:
|
||||||
|
"""
|
||||||
|
:rtype: Finance
|
||||||
|
"""
|
||||||
|
api_response = account_post(url=FINANCE_URL, account=account, params={
|
||||||
|
"startDate": start_date,
|
||||||
|
"endDate": end_date,
|
||||||
|
"topId": 0
|
||||||
|
})
|
||||||
|
finance = Finance(**api_response.data)
|
||||||
|
finance.name = account.name
|
||||||
|
return finance
|
150
src/entity/member.py
Normal file
150
src/entity/member.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from src.core import api_request
|
||||||
|
from src.core.constant import MEMBER_DETAIL_URL, MEMBER_LIST_URL
|
||||||
|
from src.core.util import get_curr_day, get_first_day_month
|
||||||
|
from src.entity.account import Account
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BaseMember:
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
realName: str
|
||||||
|
registerDate: str
|
||||||
|
deposit: str
|
||||||
|
draw: str
|
||||||
|
profit: str
|
||||||
|
lastLoginTime: str
|
||||||
|
active: int
|
||||||
|
promo: str
|
||||||
|
rebate: str
|
||||||
|
riskAdjust: str
|
||||||
|
netAmount: str
|
||||||
|
betAmount: str
|
||||||
|
changeLog: str
|
||||||
|
venueProfitList: list
|
||||||
|
isChange: int
|
||||||
|
vipGrade: int
|
||||||
|
vipGradeStr: str
|
||||||
|
firstPayAt: str
|
||||||
|
remark: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MemberList(BaseMember):
|
||||||
|
venueNetAmountList: str
|
||||||
|
tagsIds: str
|
||||||
|
tagsInfo: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MemberDetail(BaseMember):
|
||||||
|
venueNetAmountList: list
|
||||||
|
tags: str
|
||||||
|
|
||||||
|
def get_first_pay_datetime(self) -> datetime:
|
||||||
|
return datetime.strptime(self.firstPayAt, "%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
|
def get_member_list(account: Account, params: dict) -> List[MemberList]:
|
||||||
|
api_response = api_request.account_post(MEMBER_LIST_URL, account=account, params=params)
|
||||||
|
data_list = api_response.data.get('list', [])
|
||||||
|
return [MemberList(**item) for item in data_list] if data_list else []
|
||||||
|
|
||||||
|
|
||||||
|
def get_today_new_member_list(account: Account, count: int) -> List[MemberList]:
|
||||||
|
date = get_curr_day()
|
||||||
|
params = {
|
||||||
|
"pageNum": 1,
|
||||||
|
"pageSize": count,
|
||||||
|
"registerSort": 1,
|
||||||
|
"drawSort": -1,
|
||||||
|
"depositSort": -1,
|
||||||
|
"lastLoginTimeSort": -1,
|
||||||
|
"name": "",
|
||||||
|
"minPay": None,
|
||||||
|
"maxPay": None,
|
||||||
|
"startDate": date,
|
||||||
|
"registerStartDate": date,
|
||||||
|
"endDate": date,
|
||||||
|
"registerEndDate": date,
|
||||||
|
"firstPayStartTime": "",
|
||||||
|
"firstPayEndTime": "",
|
||||||
|
"isBet": "",
|
||||||
|
"tagsFlag": "1"
|
||||||
|
}
|
||||||
|
members = get_member_list(account=account, params=params)
|
||||||
|
return members
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_member_list(account: Account, params: dict) -> List[MemberList]:
|
||||||
|
api_res = await api_request.async_account_post(MEMBER_LIST_URL, account=account, params=params)
|
||||||
|
data_list = api_res.data.get('list', [])
|
||||||
|
return [MemberList(**item) for item in data_list] if data_list else []
|
||||||
|
|
||||||
|
|
||||||
|
# 根据用户名查询用户详情
|
||||||
|
def get_member_by_name(account: Account, name: str) -> MemberDetail:
|
||||||
|
params = {
|
||||||
|
"pageNum": 1,
|
||||||
|
"pageSize": 1,
|
||||||
|
"registerSort": 1,
|
||||||
|
"drawSort": -1,
|
||||||
|
"depositSort": -1,
|
||||||
|
"lastLoginTimeSort": -1,
|
||||||
|
"name": name,
|
||||||
|
"minPay": None,
|
||||||
|
"maxPay": None,
|
||||||
|
"startDate": get_first_day_month(),
|
||||||
|
"registerStartDate": '',
|
||||||
|
"endDate": get_curr_day(),
|
||||||
|
"registerEndDate": '',
|
||||||
|
"firstPayStartTime": "",
|
||||||
|
"firstPayEndTime": "",
|
||||||
|
"isBet": "",
|
||||||
|
"tagsFlag": "1"
|
||||||
|
}
|
||||||
|
member = get_member_list(account, params)[0]
|
||||||
|
params = {
|
||||||
|
'startDate': get_first_day_month(),
|
||||||
|
'endDate': get_curr_day(),
|
||||||
|
'id': member.id
|
||||||
|
}
|
||||||
|
api_response = api_request.account_post(url=MEMBER_DETAIL_URL, account=account, params=params)
|
||||||
|
return MemberDetail(**api_response.data)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_member_detail_by_name(account: Account, name: str) -> MemberDetail:
|
||||||
|
params = {
|
||||||
|
"pageNum": 1,
|
||||||
|
"pageSize": 1,
|
||||||
|
"registerSort": 1,
|
||||||
|
"drawSort": -1,
|
||||||
|
"depositSort": -1,
|
||||||
|
"lastLoginTimeSort": -1,
|
||||||
|
"name": name,
|
||||||
|
"minPay": None,
|
||||||
|
"maxPay": None,
|
||||||
|
"startDate": get_first_day_month(),
|
||||||
|
"registerStartDate": '',
|
||||||
|
"endDate": get_curr_day(),
|
||||||
|
"registerEndDate": '',
|
||||||
|
"firstPayStartTime": "",
|
||||||
|
"firstPayEndTime": "",
|
||||||
|
"isBet": "",
|
||||||
|
"tagsFlag": "1"
|
||||||
|
}
|
||||||
|
member_list = await async_get_member_list(account, params)
|
||||||
|
member = member_list[0]
|
||||||
|
params = {
|
||||||
|
'startDate': get_first_day_month(),
|
||||||
|
'endDate': get_curr_day(),
|
||||||
|
'id': member.id
|
||||||
|
}
|
||||||
|
|
||||||
|
api_response = await api_request.async_account_post(url=MEMBER_DETAIL_URL, account=account,
|
||||||
|
params=params)
|
||||||
|
return MemberDetail(**api_response.data)
|
135
src/entity/pay_record.py
Normal file
135
src/entity/pay_record.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import asyncio
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from src import logger
|
||||||
|
from src.core import api_request, util
|
||||||
|
from src.core.constant import PAY_RECORD_URL
|
||||||
|
from src.core.util import get_curr_day
|
||||||
|
from src.entity.account import Account
|
||||||
|
from src.entity.member import (async_get_member_detail_by_name,
|
||||||
|
get_member_by_name)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PayRecord(object):
|
||||||
|
billNo: str
|
||||||
|
name: str
|
||||||
|
agentName: str
|
||||||
|
orderAmount: str
|
||||||
|
rebateAmount: str
|
||||||
|
flowRatio: str
|
||||||
|
payType: int
|
||||||
|
payName: str
|
||||||
|
recipientAccount: str
|
||||||
|
createdAt: str
|
||||||
|
payStatus: int
|
||||||
|
payStatusName: str
|
||||||
|
whetherGetCard: str
|
||||||
|
topId: int
|
||||||
|
scoreAmount: str
|
||||||
|
|
||||||
|
|
||||||
|
# 根据用户查询最新存款信息
|
||||||
|
def get_pay_record_list(account: Account):
|
||||||
|
logger.info(f'Getting pay record list for account: {account.name}')
|
||||||
|
# 获取当前成功存款的用户
|
||||||
|
params = {
|
||||||
|
"memberName": "",
|
||||||
|
"payState": 2,
|
||||||
|
"isRest": False,
|
||||||
|
"pageNum": 1,
|
||||||
|
"pageSize": 100,
|
||||||
|
"startDate": get_curr_day(),
|
||||||
|
"endDate": get_curr_day()
|
||||||
|
}
|
||||||
|
api_response = api_request.account_post(PAY_RECORD_URL, account, params)
|
||||||
|
logger.info(f'Finished getting pay record list for account: {account.name}')
|
||||||
|
return [PayRecord(**item) for item in api_response.data['list']]
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_deposit_user(account: Account, count: int):
|
||||||
|
logger.info(f'Getting latest deposit user for account: {account.name} and count: {count}')
|
||||||
|
pay_record_list = get_pay_record_list(account)
|
||||||
|
# 提取所有用户名
|
||||||
|
names = []
|
||||||
|
seen = set()
|
||||||
|
now = datetime.now()
|
||||||
|
for item in pay_record_list:
|
||||||
|
name = item.name
|
||||||
|
if name not in seen and util.get_difference(item.createdAt, now) < 7200:
|
||||||
|
names.append(name)
|
||||||
|
seen.add(name)
|
||||||
|
logger.debug(f"获取到{len(names)}个成功存款人数")
|
||||||
|
# 开启多线程 根据用户名查询所有数据
|
||||||
|
with ThreadPoolExecutor(max_workers=len(names)) as executor:
|
||||||
|
futures = [executor.submit(get_member_by_name, account, name) for name in names]
|
||||||
|
try:
|
||||||
|
results = [future.result() for future in futures]
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f'查询失败:{e}')
|
||||||
|
sorted_members = sorted(results, key=lambda member_detail: member_detail.get_first_pay_datetime(), reverse=True)
|
||||||
|
# 截取前n个
|
||||||
|
details = sorted_members[:count]
|
||||||
|
record_dict = {record.createdAt: record.scoreAmount for record in pay_record_list}
|
||||||
|
member_details = []
|
||||||
|
for detail in details:
|
||||||
|
if detail.firstPayAt in record_dict:
|
||||||
|
detail.deposit = record_dict[detail.firstPayAt]
|
||||||
|
member_details.append(detail)
|
||||||
|
logger.info(f'Finished getting latest deposit user for account: {account.name} and count: {count}')
|
||||||
|
return member_details
|
||||||
|
|
||||||
|
|
||||||
|
# 根据用户查询最新存款信息
|
||||||
|
async def async_get_pay_record_list(account: Account):
|
||||||
|
logger.info(f'Async getting pay record list for account: {account.name}')
|
||||||
|
today = get_curr_day()
|
||||||
|
params = {
|
||||||
|
"memberName": "",
|
||||||
|
"payState": 2,
|
||||||
|
"isRest": False,
|
||||||
|
"pageNum": 1,
|
||||||
|
"pageSize": 100,
|
||||||
|
"startDate": today,
|
||||||
|
"endDate": today
|
||||||
|
}
|
||||||
|
res = await api_request.async_account_post(PAY_RECORD_URL, account, params)
|
||||||
|
logger.info(f'Finished async getting pay record list for account: {account.name}')
|
||||||
|
return [PayRecord(**item) for item in res.data['list']]
|
||||||
|
|
||||||
|
|
||||||
|
# 获取最新存款用户
|
||||||
|
async def async_get_latest_deposit_user(account: Account, count: int):
|
||||||
|
logger.info(f'Async getting latest deposit user for account: {account.name} and count: {count}')
|
||||||
|
pay_record_list = await async_get_pay_record_list(account)
|
||||||
|
# 提取所有用户名
|
||||||
|
names = []
|
||||||
|
seen = set()
|
||||||
|
now = datetime.now()
|
||||||
|
for item in pay_record_list:
|
||||||
|
name = item.name
|
||||||
|
# 存款订单有效期一般两个小时左右,所以默认只查询两个小时以内的订单即可
|
||||||
|
if name not in seen and util.get_difference(item.createdAt, now) < 7200:
|
||||||
|
names.append(name)
|
||||||
|
seen.add(name)
|
||||||
|
|
||||||
|
logger.debug(f"获取到{len(names)}个成功存款人数")
|
||||||
|
tasks = []
|
||||||
|
for name in names:
|
||||||
|
task = asyncio.create_task(async_get_member_detail_by_name(account, name))
|
||||||
|
tasks.append(task)
|
||||||
|
|
||||||
|
results = await asyncio.gather(*tasks)
|
||||||
|
# logger.info(f'查询成功:{results}')
|
||||||
|
sorted_members = sorted(results, key=lambda member_detail: member_detail.get_first_pay_datetime(), reverse=True)
|
||||||
|
# logger.debug(f'首存金额:{[member_detail.deposit for member_detail in sorted_members]}')
|
||||||
|
# 因为会员列表的存款是统计用户当月的所有金额 所以有延迟 所以需要将订单中的金额赋值给存款金额 然后再返回
|
||||||
|
details = sorted_members[:count]
|
||||||
|
record_dict = {record.createdAt: record.scoreAmount for record in pay_record_list}
|
||||||
|
for detail in details:
|
||||||
|
if detail.firstPayAt in record_dict:
|
||||||
|
detail.deposit = record_dict[detail.firstPayAt]
|
||||||
|
logger.info(f'Finished async getting latest deposit user for account: {account.name} and count: {count}')
|
||||||
|
return details
|
34
src/entity/user.py
Normal file
34
src/entity/user.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from sqlalchemy import String
|
||||||
|
from sqlalchemy.dialects.mssql import TINYINT
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from src.entity import account
|
||||||
|
from src.entity.database import db
|
||||||
|
|
||||||
|
|
||||||
|
# 用户
|
||||||
|
@dataclass
|
||||||
|
class User(db.Base):
|
||||||
|
__tablename__ = 'ky_user'
|
||||||
|
|
||||||
|
id: Mapped[db.int_pk] = mapped_column(comment="唯一键")
|
||||||
|
username: Mapped[db.str_required_unique] = mapped_column(comment="账号")
|
||||||
|
password: Mapped[db.str_required] = mapped_column(comment="密码")
|
||||||
|
name: Mapped[str] = mapped_column(String(32), nullable=True, comment="别名")
|
||||||
|
email: Mapped[str] = mapped_column(String(32), nullable=False, comment="邮箱")
|
||||||
|
bot_token: Mapped[str] = mapped_column(String(64), nullable=True, comment="飞机机器人id")
|
||||||
|
group_id: Mapped[str] = mapped_column(String(32), nullable=True, comment="消息群组id")
|
||||||
|
chat_id: Mapped[str] = mapped_column(String(32), nullable=True, comment="消息个人id")
|
||||||
|
telegram_ids: Mapped[str] = mapped_column(String(128), nullable=True, comment="telegram_ids")
|
||||||
|
status: Mapped[int] = mapped_column(TINYINT, nullable=False, comment='账号状态,0禁用,1启用', default=0)
|
||||||
|
accounts: Mapped[List['account.Account']] = relationship('account.Account', back_populates='user', lazy=False)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_by_username_and_password(username: str, password: str) -> User:
|
||||||
|
with db.Session() as session:
|
||||||
|
user = session.query(User).filter(
|
||||||
|
User.username == username and User.password == password and User.status == 1).one()
|
||||||
|
return user
|
85
src/entity/visual_list.py
Normal file
85
src/entity/visual_list.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from src.core import api_request
|
||||||
|
from src.core.constant import VISUAL_LIST_URL
|
||||||
|
from src.entity.account import Account
|
||||||
|
|
||||||
|
|
||||||
|
# 视图列表对象 对应界面上的图表
|
||||||
|
@dataclass
|
||||||
|
class VisualInfo:
|
||||||
|
staticsDate: str
|
||||||
|
agentId: int
|
||||||
|
agentType: int
|
||||||
|
agentCode: str
|
||||||
|
agentName: str
|
||||||
|
isNew: int
|
||||||
|
firstDeposit: float
|
||||||
|
deposit: float
|
||||||
|
depositPromo: float
|
||||||
|
draw: int
|
||||||
|
promo: float
|
||||||
|
promoDividend: float
|
||||||
|
rebate: float
|
||||||
|
adjust: float
|
||||||
|
riskAdjust: float
|
||||||
|
bets: float
|
||||||
|
profit: float
|
||||||
|
allBets: float
|
||||||
|
firstCount: int
|
||||||
|
countDeposit: int
|
||||||
|
countDraw: int
|
||||||
|
countBets: int
|
||||||
|
createdAt: str
|
||||||
|
updatedAt: str
|
||||||
|
oldDeposit: float
|
||||||
|
oldDepositCount: int
|
||||||
|
newDeposit: float
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_dict(data):
|
||||||
|
return VisualInfo(**data)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VisualList:
|
||||||
|
curData: list[VisualInfo]
|
||||||
|
lastData: list[VisualInfo]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_json(json_str):
|
||||||
|
data = json.loads(json_str)
|
||||||
|
cur_data = [VisualInfo.from_dict(item) for item in data['curData']]
|
||||||
|
last_data = [VisualInfo.from_dict(item) for item in data['lastData']]
|
||||||
|
return VisualList(curData=cur_data, lastData=last_data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def dict_to_visual_list(data):
|
||||||
|
cur_data = [VisualInfo(**item) for item in data['curData']]
|
||||||
|
last_data = [VisualInfo(**item) for item in data['lastData']]
|
||||||
|
return VisualList(curData=cur_data, lastData=last_data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_visual_list(account: Account, params: dict) -> VisualList:
|
||||||
|
res = api_request.account_post(url=VISUAL_LIST_URL, account=account,
|
||||||
|
params=params)
|
||||||
|
return VisualList.dict_to_visual_list(res.data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_curr_data(account: Account, params: dict) -> list[VisualInfo]:
|
||||||
|
res = api_request.account_post(url=VISUAL_LIST_URL, account=account,
|
||||||
|
params=params)
|
||||||
|
return [VisualInfo(**item) for item in res.data['curData']]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_visual_list(account: Account, params: dict) -> VisualList:
|
||||||
|
res = await api_request.async_account_post(url=VISUAL_LIST_URL, account=account,
|
||||||
|
params=params)
|
||||||
|
return VisualList(**res.data)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_curr_data(account: Account, params: dict) -> list[VisualInfo]:
|
||||||
|
res = await api_request.async_account_post(url=VISUAL_LIST_URL, account=account,
|
||||||
|
params=params)
|
||||||
|
return [VisualInfo(**item) for item in res.data['curData']]
|
8
src/ui/__init__.py
Normal file
8
src/ui/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from PyQt6.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
|
class GlobalSignals(QObject):
|
||||||
|
user_data_updated = pyqtSignal()
|
||||||
|
|
||||||
|
|
||||||
|
# 实例化全局信号
|
||||||
|
global_signals = GlobalSignals()
|
557
src/ui/app.py
Normal file
557
src/ui/app.py
Normal file
@ -0,0 +1,557 @@
|
|||||||
|
import configparser
|
||||||
|
import os
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
from PyQt6.QtCore import QDate, QDateTime, Qt, QThreadPool, QTime, QTimer
|
||||||
|
from PyQt6.QtGui import QColor, QIcon, QAction
|
||||||
|
from PyQt6.QtWidgets import (QApplication, QCheckBox, QDateEdit, QHBoxLayout,
|
||||||
|
QHeaderView, QMainWindow, QPushButton,
|
||||||
|
QSizePolicy, QTableWidget, QTableWidgetItem,
|
||||||
|
QTabWidget, QTextEdit, QVBoxLayout, QWidget, QMessageBox, QSystemTrayIcon, QMenu)
|
||||||
|
|
||||||
|
from src import resource_path
|
||||||
|
from src.core.message_client import send_message
|
||||||
|
from src.core.util import convert_data
|
||||||
|
from src.entity.member import get_today_new_member_list
|
||||||
|
from src.entity.pay_record import get_latest_deposit_user
|
||||||
|
from src.entity.user import get_user_by_username_and_password
|
||||||
|
from src.ui import global_signals
|
||||||
|
from src.ui.data_query import ButtonTask, ReportTask
|
||||||
|
from src.ui.title_bar import CustomTitleBar
|
||||||
|
|
||||||
|
|
||||||
|
class Application(QMainWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.tables = {}
|
||||||
|
self.is_dragging = False
|
||||||
|
self.drag_position = None
|
||||||
|
self.is_resizing = False
|
||||||
|
self.resize_direction = None
|
||||||
|
self.toaster_notify_enabled = True
|
||||||
|
self.telegram_notify_enabled = True
|
||||||
|
self.initialize_application()
|
||||||
|
|
||||||
|
def initialize_application(self):
|
||||||
|
# 1. 加载配置文件
|
||||||
|
self.load_config()
|
||||||
|
|
||||||
|
# 2. 设置线程池
|
||||||
|
self.thread_pool = QThreadPool()
|
||||||
|
|
||||||
|
# 3. 设置 UI
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
# 4. 初始化系统托盘图标
|
||||||
|
self.init_tray_icon()
|
||||||
|
|
||||||
|
# 5. 初始化表格数据
|
||||||
|
self.init_table_data()
|
||||||
|
|
||||||
|
# 6. 设置日期更新和报告定时器
|
||||||
|
self.setup_date_update_timer()
|
||||||
|
self.setup_report_timer()
|
||||||
|
# 7. 全局信号处理,更新用户信息
|
||||||
|
global_signals.user_data_updated.connect(self.refresh_user_data)
|
||||||
|
# 8. 消息通知对象
|
||||||
|
self.chat_id = self.user.group_id if self.user.group_id else self.user.chat_id
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
self.apply_stylesheet()
|
||||||
|
self.set_window_properties()
|
||||||
|
self.create_central_widget()
|
||||||
|
self.setup_layouts()
|
||||||
|
|
||||||
|
def set_window_properties(self):
|
||||||
|
self.resize(600, 400)
|
||||||
|
self.setWindowTitle("zayac的小工具")
|
||||||
|
self.setWindowIcon(QIcon("icons:icon.png"))
|
||||||
|
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
|
||||||
|
|
||||||
|
def create_central_widget(self):
|
||||||
|
self.central_widget = QWidget()
|
||||||
|
self.setCentralWidget(self.central_widget)
|
||||||
|
self.main_layout = QVBoxLayout(self.central_widget)
|
||||||
|
|
||||||
|
def setup_layouts(self):
|
||||||
|
self.setup_top_panel()
|
||||||
|
self.setup_middle_panel()
|
||||||
|
self.setup_bottom_panel()
|
||||||
|
self.customTitleBar = CustomTitleBar(self)
|
||||||
|
self.setMenuWidget(self.customTitleBar)
|
||||||
|
|
||||||
|
def init_tray_icon(self):
|
||||||
|
self.tray_icon = QSystemTrayIcon(QIcon("icons:icon.png"), self)
|
||||||
|
tray_menu = QMenu()
|
||||||
|
exit_action = QAction("退出", self)
|
||||||
|
exit_action.triggered.connect(self.exit_application)
|
||||||
|
tray_menu.addAction(exit_action)
|
||||||
|
self.tray_icon.setContextMenu(tray_menu)
|
||||||
|
self.tray_icon.activated.connect(self.tray_icon_clicked)
|
||||||
|
self.tray_icon.show()
|
||||||
|
|
||||||
|
def init_data_and_timers(self):
|
||||||
|
# 初始化数据
|
||||||
|
self.init_table_data()
|
||||||
|
|
||||||
|
# 设置定时器
|
||||||
|
self.setup_date_update_timer()
|
||||||
|
self.setup_report_timer()
|
||||||
|
|
||||||
|
def load_config(self):
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config_file = 'config.ini'
|
||||||
|
|
||||||
|
if not os.path.exists(config_file):
|
||||||
|
QMessageBox.warning(None, "警告", "用户信息获取失败!")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
config.read(config_file)
|
||||||
|
username = config.get('Credentials', 'username')
|
||||||
|
password = config.get('Credentials', 'password')
|
||||||
|
minimum = config.get('Minimum', 'minimum')
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.user = get_user_by_username_and_password(username, password)
|
||||||
|
self.minimum = minimum
|
||||||
|
|
||||||
|
def init_table_data(self):
|
||||||
|
# 初始化表格数据
|
||||||
|
# 例如,加载账户数据并更新表格
|
||||||
|
for account in self.user.accounts:
|
||||||
|
data = self.query_initial_data(account)
|
||||||
|
self.update_table(account.username, data)
|
||||||
|
|
||||||
|
def query_initial_data(self, account):
|
||||||
|
# 实际实现应该根据您的业务逻辑来定义
|
||||||
|
return ReportTask.query_data_for_account(account)
|
||||||
|
|
||||||
|
def setup_report_timer(self):
|
||||||
|
self.report_timer = QTimer(self)
|
||||||
|
self.report_timer.timeout.connect(self.update_reports)
|
||||||
|
self.report_timer.start(60000) # 每60秒触发一次
|
||||||
|
|
||||||
|
def update_reports(self):
|
||||||
|
for account in self.user.accounts:
|
||||||
|
report_task = ReportTask(account)
|
||||||
|
report_task.signals.table_updated.connect(self.update_table)
|
||||||
|
self.thread_pool.start(report_task)
|
||||||
|
|
||||||
|
def apply_stylesheet(self):
|
||||||
|
style_sheet = self.load_stylesheet(resource_path('ui/style.qss'))
|
||||||
|
self.setStyleSheet(style_sheet)
|
||||||
|
|
||||||
|
def refresh_user_data(self):
|
||||||
|
# 刷新用户数据的逻辑
|
||||||
|
self.user = get_user_by_username_and_password(self.username, self.password) # 重新加载用户
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load_stylesheet(file_path):
|
||||||
|
try:
|
||||||
|
with open(file_path, "r", encoding='utf-8') as file:
|
||||||
|
return file.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"无法找到样式文件: {file_path}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_account_by_account_username(self, username: str):
|
||||||
|
for account in self.user.accounts:
|
||||||
|
if account.username == username:
|
||||||
|
return account
|
||||||
|
|
||||||
|
def setup_top_panel(self):
|
||||||
|
try:
|
||||||
|
self.top_panel = QHBoxLayout()
|
||||||
|
self.add_buttons_to_top_panel()
|
||||||
|
self.add_date_picker_to_top_panel()
|
||||||
|
self.add_checkboxes_to_top_panel()
|
||||||
|
self.setup_report_button()
|
||||||
|
self.main_layout.addLayout(self.top_panel)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error setting up top panel: {e}")
|
||||||
|
|
||||||
|
def add_date_picker_to_top_panel(self):
|
||||||
|
# 创建日期选择器
|
||||||
|
self.dateEdit = QDateEdit()
|
||||||
|
self.dateEdit.setCalendarPopup(True)
|
||||||
|
self.dateEdit.setDate(QDate.currentDate())
|
||||||
|
|
||||||
|
# 设置日期范围
|
||||||
|
today = QDate.currentDate()
|
||||||
|
first_day_last_month = QDate(today.year(), today.month(), 1).addMonths(-1)
|
||||||
|
self.dateEdit.setMinimumDate(first_day_last_month)
|
||||||
|
self.dateEdit.setMaximumDate(today)
|
||||||
|
|
||||||
|
# 将日期选择器添加到顶部面板
|
||||||
|
self.top_panel.addWidget(self.dateEdit)
|
||||||
|
|
||||||
|
def setup_date_update_timer(self):
|
||||||
|
# 创建一个新的定时器
|
||||||
|
self.date_update_timer = QTimer(self)
|
||||||
|
# 设置定时器触发的槽函数
|
||||||
|
self.date_update_timer.timeout.connect(self.update_date_edit)
|
||||||
|
# 启动定时器
|
||||||
|
self.start_date_update_timer()
|
||||||
|
|
||||||
|
def start_date_update_timer(self):
|
||||||
|
now = QDateTime.currentDateTime()
|
||||||
|
next_midnight = QDateTime(now.date().addDays(1), QTime(0, 0))
|
||||||
|
interval = now.msecsTo(next_midnight)
|
||||||
|
self.date_update_timer.start(interval if interval > 0 else 86400000) # 86400000ms = 24小时
|
||||||
|
|
||||||
|
def update_date_edit(self):
|
||||||
|
# 更新日期选择器的日期为当前日期
|
||||||
|
self.dateEdit.setDate(QDate.currentDate())
|
||||||
|
# 更新日期范围
|
||||||
|
self.update_date_range()
|
||||||
|
print(self.dateEdit.date())
|
||||||
|
# 设置定时器每24小时触发一次
|
||||||
|
self.date_update_timer.start(86400000)
|
||||||
|
|
||||||
|
def update_date_range(self):
|
||||||
|
today = QDate.currentDate()
|
||||||
|
first_day_last_month = QDate(today.year(), today.month(), 1).addMonths(-1)
|
||||||
|
self.dateEdit.setMinimumDate(first_day_last_month)
|
||||||
|
self.dateEdit.setMaximumDate(today)
|
||||||
|
|
||||||
|
def setup_report_button(self):
|
||||||
|
self.report_button = QPushButton("停止喜报")
|
||||||
|
self.report_button.setCheckable(True)
|
||||||
|
self.report_button.setChecked(True) # 默认设置为选中状态
|
||||||
|
self.report_button.setObjectName("Warning")
|
||||||
|
self.report_button.clicked.connect(self.on_report_clicked)
|
||||||
|
self.top_panel.addWidget(self.report_button)
|
||||||
|
|
||||||
|
def add_buttons_to_top_panel(self):
|
||||||
|
for name, style in self.get_buttons_info():
|
||||||
|
self.create_and_add_button(name, style)
|
||||||
|
|
||||||
|
def get_buttons_info(self):
|
||||||
|
return [
|
||||||
|
("报数", "Primary"),
|
||||||
|
("存款失败用户", "Danger"),
|
||||||
|
("负盈利", "Success"),
|
||||||
|
("薪资", "Light"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def create_and_add_button(self, name, style):
|
||||||
|
button = QPushButton(name)
|
||||||
|
button.setObjectName(style)
|
||||||
|
button.clicked.connect(lambda _, n=name: self.query_data(n))
|
||||||
|
self.top_panel.addWidget(button)
|
||||||
|
|
||||||
|
def add_checkboxes_to_top_panel(self):
|
||||||
|
# 创建垂直布局来放置复选框
|
||||||
|
checkbox_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# 添加复选框
|
||||||
|
self.system_notification_checkbox = QCheckBox("系统通知")
|
||||||
|
self.telegram_notification_checkbox = QCheckBox("飞机通知")
|
||||||
|
self.system_notification_checkbox.setChecked(True)
|
||||||
|
self.telegram_notification_checkbox.setChecked(True)
|
||||||
|
self.system_notification_checkbox.stateChanged.connect(self.toggle_system_notification)
|
||||||
|
self.telegram_notification_checkbox.stateChanged.connect(self.toggle_telegram_notification)
|
||||||
|
checkbox_layout.addWidget(self.system_notification_checkbox)
|
||||||
|
checkbox_layout.addWidget(self.telegram_notification_checkbox)
|
||||||
|
|
||||||
|
# 将复选框布局添加到顶部面板
|
||||||
|
self.top_panel.addLayout(checkbox_layout)
|
||||||
|
|
||||||
|
self.main_layout.addLayout(self.top_panel)
|
||||||
|
|
||||||
|
def toggle_system_notification(self, state):
|
||||||
|
self.toaster_notify_enabled = state == Qt.CheckState.Checked
|
||||||
|
|
||||||
|
def toggle_telegram_notification(self, state):
|
||||||
|
self.telegram_notify_enabled = state == Qt.CheckState.Checked
|
||||||
|
|
||||||
|
def query_data(self, btn_name):
|
||||||
|
# 获取日期控件的当前值
|
||||||
|
selected_date = self.dateEdit.date()
|
||||||
|
|
||||||
|
# 转换为所需的格式
|
||||||
|
selected_date_str = selected_date.toString("yyyy-MM-dd")
|
||||||
|
# 在文本框中显示查询中的消息
|
||||||
|
self.txt.append(f"正在查询{selected_date_str}的{btn_name},请等待...\n")
|
||||||
|
|
||||||
|
self.start_data_query(btn_name, selected_date_str)
|
||||||
|
|
||||||
|
def start_data_query(self, query_type, selected_date_str):
|
||||||
|
task = ButtonTask(query_type, selected_date_str, self.user)
|
||||||
|
task.signals.query_completed.connect(self.display_query_result)
|
||||||
|
self.thread_pool.start(task)
|
||||||
|
|
||||||
|
def display_query_result(self, result, auto_clipboard, need_notify):
|
||||||
|
if auto_clipboard:
|
||||||
|
copy_to_clipboard(result)
|
||||||
|
if need_notify and '无' not in result:
|
||||||
|
self.tray_icon.showMessage("", "自动复制成功", QSystemTrayIcon.MessageIcon.Information, 500)
|
||||||
|
self.txt.append(result)
|
||||||
|
|
||||||
|
def send_notification(self, emoji, account_name, title, count, results, total):
|
||||||
|
msg = f'{emoji} {account_name} {title}:{count} {results} 总数:*{total}*'
|
||||||
|
if self.toaster_notify_enabled:
|
||||||
|
self.tray_icon.showMessage(f"{title}通知", msg, QSystemTrayIcon.MessageIcon.Information, 2000)
|
||||||
|
if self.telegram_notify_enabled:
|
||||||
|
send_message(self.user.bot_token, self.chat_id, msg)
|
||||||
|
|
||||||
|
def on_report_clicked(self):
|
||||||
|
try:
|
||||||
|
if self.report_button.isChecked():
|
||||||
|
self.report_timer.start(60000) # 启动定时器
|
||||||
|
self.report_button.setText("停止喜报") # 更改按钮文本
|
||||||
|
# 更改按钮样式为 "Warning"
|
||||||
|
self.report_button.setObjectName("Warning")
|
||||||
|
else:
|
||||||
|
self.report_timer.stop() # 停止定时器
|
||||||
|
self.report_button.setText("启动喜报") # 恢复按钮文本
|
||||||
|
self.report_button.setObjectName("Success")
|
||||||
|
# 重新应用样式来更新按钮外观
|
||||||
|
self.apply_stylesheet()
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(e)
|
||||||
|
|
||||||
|
def update_table(self, account_username, data):
|
||||||
|
try:
|
||||||
|
table = self.tables.get(account_username)
|
||||||
|
if not table:
|
||||||
|
return
|
||||||
|
self.ensure_table_row_limit(table)
|
||||||
|
self.insert_data_in_table(table, data, account_username)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating table for {account_username}: {e}")
|
||||||
|
|
||||||
|
def ensure_table_row_limit(self, table, row_limit=20):
|
||||||
|
if table.rowCount() >= row_limit:
|
||||||
|
table.removeRow(0)
|
||||||
|
|
||||||
|
def insert_data_in_table(self, table, data, account_username):
|
||||||
|
# 获取当前的行数
|
||||||
|
row_count = table.rowCount()
|
||||||
|
table.insertRow(row_count)
|
||||||
|
notifications = []
|
||||||
|
|
||||||
|
for col, cell_data in enumerate(data):
|
||||||
|
cell = self.create_table_cell(cell_data, table, col)
|
||||||
|
# 注意这里我们使用 row_count 而不是 0
|
||||||
|
table.setItem(row_count, col, cell)
|
||||||
|
self.handle_data_change(table, cell_data, col, account_username, notifications)
|
||||||
|
|
||||||
|
self.send_all_notifications(notifications)
|
||||||
|
|
||||||
|
def create_table_cell(self, cell_data, table, col):
|
||||||
|
cell = QTableWidgetItem(str(cell_data))
|
||||||
|
cell.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
return cell
|
||||||
|
|
||||||
|
def handle_data_change(self, table, cell_data, col, account_username, notifications):
|
||||||
|
if table.rowCount() > 1 and col != 0:
|
||||||
|
old_data_str = table.item(1, col).text()
|
||||||
|
old_data = convert_data(old_data_str)
|
||||||
|
new_data = convert_data(cell_data)
|
||||||
|
|
||||||
|
if old_data != new_data:
|
||||||
|
count_change = new_data - old_data
|
||||||
|
self.update_cell_color(table, col, count_change)
|
||||||
|
self.generate_notifications(account_username, col, cell_data, count_change, notifications)
|
||||||
|
|
||||||
|
def update_cell_color(self, table, col, count_change):
|
||||||
|
# 更新单元格颜色
|
||||||
|
cell = table.item(0, col)
|
||||||
|
if count_change > 0:
|
||||||
|
cell.setForeground(QColor(Qt.GlobalColor.green))
|
||||||
|
elif count_change < 0:
|
||||||
|
cell.setForeground(QColor(Qt.GlobalColor.red))
|
||||||
|
|
||||||
|
def generate_notifications(self, account_username, col, cell_data, count_change, notifications):
|
||||||
|
# 生成通知
|
||||||
|
account = self.get_account_by_account_username(account_username)
|
||||||
|
if count_change > 0:
|
||||||
|
if col == 1: # 第1列是注册用户数量
|
||||||
|
reg_results = ','.join(
|
||||||
|
[f'`{member.name}`' for member in get_today_new_member_list(account, count_change)])
|
||||||
|
notifications.append(
|
||||||
|
('👏', account.name, '注册', count_change, f'用户: {reg_results}', str(cell_data)))
|
||||||
|
elif col == 2: # 第2列是首存用户数量
|
||||||
|
deposit_results = '\n'.join(
|
||||||
|
[f"用户: `{member.name}`, 首存金额: *{member.deposit}*" for member in
|
||||||
|
get_latest_deposit_user(account, count_change)])
|
||||||
|
notifications.append(
|
||||||
|
('🎉', account.name, '首存', count_change, deposit_results, str(cell_data)))
|
||||||
|
|
||||||
|
def send_all_notifications(self, notifications):
|
||||||
|
for notification in notifications:
|
||||||
|
self.send_notification(*notification)
|
||||||
|
|
||||||
|
def addToggleButton(self, text, style):
|
||||||
|
toggleButton = QPushButton(text)
|
||||||
|
toggleButton.setCheckable(True)
|
||||||
|
toggleButton.setStyleSheet(style)
|
||||||
|
self.top_panel.addWidget(toggleButton)
|
||||||
|
|
||||||
|
def setup_middle_panel(self):
|
||||||
|
# 底部面板,包括文本框
|
||||||
|
self.bottom_panel = QVBoxLayout()
|
||||||
|
self.txt = QTextEdit()
|
||||||
|
self.bottom_panel.addWidget(self.txt)
|
||||||
|
self.main_layout.addLayout(self.bottom_panel)
|
||||||
|
|
||||||
|
def setup_bottom_panel(self):
|
||||||
|
# 中间面板,包括笔记本(标签页)
|
||||||
|
self.middle_panel = QVBoxLayout()
|
||||||
|
self.notebook = QTabWidget()
|
||||||
|
self.middle_panel.addWidget(self.notebook)
|
||||||
|
self.add_tabs(self.notebook)
|
||||||
|
self.main_layout.addLayout(self.middle_panel)
|
||||||
|
|
||||||
|
def add_tabs(self, notebook):
|
||||||
|
column_headers = ["时间", "注册", "首存", "负盈利", "有效", "活跃"]
|
||||||
|
for account in self.user.accounts:
|
||||||
|
# 创建 Tab 和布局
|
||||||
|
tab = QWidget()
|
||||||
|
tab_layout = QVBoxLayout(tab)
|
||||||
|
notebook.addTab(tab, account.name)
|
||||||
|
|
||||||
|
# 创建表格并设置列数和列标题
|
||||||
|
table = QTableWidget()
|
||||||
|
table.setColumnCount(len(column_headers))
|
||||||
|
table.setHorizontalHeaderLabels(column_headers)
|
||||||
|
|
||||||
|
# 禁用表格的编辑功能
|
||||||
|
table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||||
|
|
||||||
|
# 设置列宽
|
||||||
|
header = table.horizontalHeader()
|
||||||
|
|
||||||
|
# 设置所有列为自适应宽度
|
||||||
|
for i in range(len(column_headers)):
|
||||||
|
header.setSectionResizeMode(i, QHeaderView.ResizeMode.Stretch)
|
||||||
|
|
||||||
|
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
|
||||||
|
# 将表格的大小调整策略设置为填充整个 Tab
|
||||||
|
table.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding))
|
||||||
|
|
||||||
|
# 将表格添加到布局中
|
||||||
|
tab_layout.addWidget(table)
|
||||||
|
|
||||||
|
# 保存表格引用以便稍后更新
|
||||||
|
self.tables[account.username] = table
|
||||||
|
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
self.drag_position = event.globalPosition().toPoint()
|
||||||
|
self.resize_direction = self.get_resize_direction(event.pos())
|
||||||
|
if self.resize_direction:
|
||||||
|
self.is_resizing = True
|
||||||
|
elif self.is_draggable_area(event.pos()):
|
||||||
|
self.is_dragging = True
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
|
def is_draggable_area(self, pos):
|
||||||
|
# 将点转换为标题栏的局部坐标
|
||||||
|
title_bar_pos = self.customTitleBar.mapFromParent(pos)
|
||||||
|
|
||||||
|
# 检查点是否在标题栏内
|
||||||
|
return self.customTitleBar.rect().contains(title_bar_pos)
|
||||||
|
|
||||||
|
# 重置鼠标样式
|
||||||
|
def reset_cursor_style(self):
|
||||||
|
if not (self.is_dragging or self.is_resizing):
|
||||||
|
self.setCursor(Qt.CursorShape.ArrowCursor)
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, event):
|
||||||
|
direction = self.get_resize_direction(event.pos())
|
||||||
|
# 更新鼠标样式
|
||||||
|
if direction == "left" or direction == "right":
|
||||||
|
self.setCursor(Qt.CursorShape.SizeHorCursor)
|
||||||
|
elif direction == "top" or direction == "bottom":
|
||||||
|
self.setCursor(Qt.CursorShape.SizeVerCursor)
|
||||||
|
elif direction in ["top-left", "bottom-right"]:
|
||||||
|
self.setCursor(Qt.CursorShape.SizeFDiagCursor)
|
||||||
|
elif direction in ["top-right", "bottom-left"]:
|
||||||
|
self.setCursor(Qt.CursorShape.SizeBDiagCursor)
|
||||||
|
else:
|
||||||
|
self.reset_cursor_style()
|
||||||
|
# 处理窗口拖动
|
||||||
|
if self.is_dragging:
|
||||||
|
self.move(self.pos() + (event.globalPosition().toPoint() - self.drag_position))
|
||||||
|
self.drag_position = event.globalPosition().toPoint()
|
||||||
|
|
||||||
|
# 处理窗口调整大小
|
||||||
|
elif self.is_resizing:
|
||||||
|
self.resize_window(event.globalPosition().toPoint())
|
||||||
|
|
||||||
|
super().mouseMoveEvent(event)
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event):
|
||||||
|
self.is_dragging = False
|
||||||
|
self.is_resizing = False
|
||||||
|
super().mouseReleaseEvent(event)
|
||||||
|
|
||||||
|
def get_resize_direction(self, pos):
|
||||||
|
border_width = 10 # 边缘感应区的宽度
|
||||||
|
rect = self.rect()
|
||||||
|
left, right, top, bottom = rect.left(), rect.right(), rect.top(), rect.bottom()
|
||||||
|
|
||||||
|
if pos.x() < left + border_width and pos.y() < top + border_width:
|
||||||
|
return "top-left"
|
||||||
|
if pos.x() > right - border_width and pos.y() < top + border_width:
|
||||||
|
return "top-right"
|
||||||
|
if pos.x() < left + border_width and pos.y() > bottom - border_width:
|
||||||
|
return "bottom-left"
|
||||||
|
if pos.x() > right - border_width and pos.y() > bottom - border_width:
|
||||||
|
return "bottom-right"
|
||||||
|
if pos.x() < left + border_width:
|
||||||
|
return "left"
|
||||||
|
if pos.x() > right - border_width:
|
||||||
|
return "right"
|
||||||
|
if pos.y() < top + border_width:
|
||||||
|
return "top"
|
||||||
|
if pos.y() > bottom - border_width:
|
||||||
|
return "bottom"
|
||||||
|
return None
|
||||||
|
|
||||||
|
def resize_window(self, current_pos):
|
||||||
|
if not self.resize_direction:
|
||||||
|
return
|
||||||
|
|
||||||
|
delta = current_pos - self.drag_position
|
||||||
|
rect = self.geometry()
|
||||||
|
|
||||||
|
if "left" in self.resize_direction:
|
||||||
|
rect.setLeft(rect.left() + delta.x())
|
||||||
|
if "right" in self.resize_direction:
|
||||||
|
rect.setRight(rect.right() + delta.x())
|
||||||
|
if "top" in self.resize_direction:
|
||||||
|
rect.setTop(rect.top() + delta.y())
|
||||||
|
if "bottom" in self.resize_direction:
|
||||||
|
rect.setBottom(rect.bottom() + delta.y())
|
||||||
|
|
||||||
|
self.setGeometry(rect)
|
||||||
|
self.drag_position = current_pos
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
if self.minimum:
|
||||||
|
event.ignore()
|
||||||
|
self.hide()
|
||||||
|
else:
|
||||||
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
def exit_application(self):
|
||||||
|
self.tray_icon.hide() # 隐藏托盘图标
|
||||||
|
QApplication.quit()
|
||||||
|
|
||||||
|
def tray_icon_clicked(self, reason):
|
||||||
|
if reason == QSystemTrayIcon.ActivationReason.Trigger:
|
||||||
|
if self.isVisible():
|
||||||
|
self.hide()
|
||||||
|
else:
|
||||||
|
self.showNormal()
|
||||||
|
|
||||||
|
|
||||||
|
def copy_to_clipboard(text):
|
||||||
|
clipboard = QApplication.clipboard()
|
||||||
|
clipboard.setText(text)
|
62
src/ui/data_query.py
Normal file
62
src/ui/data_query.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
from PyQt6.QtCore import QObject, QRunnable, QThread, pyqtSignal
|
||||||
|
|
||||||
|
from src.core.salary import get_salary
|
||||||
|
from src.core.报数 import get_net_win, text_count_by_user
|
||||||
|
from src.core.查询存款失败用户 import get_pay_failed_by_user
|
||||||
|
from src.entity.banner_info import get_banner_info
|
||||||
|
|
||||||
|
|
||||||
|
class TaskSignals(QObject):
|
||||||
|
query_completed = pyqtSignal(str, bool, bool)
|
||||||
|
table_updated = pyqtSignal(str, list)
|
||||||
|
|
||||||
|
|
||||||
|
class ReportTask(QRunnable):
|
||||||
|
def __init__(self, account):
|
||||||
|
super().__init__()
|
||||||
|
self.account = account
|
||||||
|
self.signals = TaskSignals()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
data = self.query_data_for_account(self.account)
|
||||||
|
self.signals.table_updated.emit(self.account.username, data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def query_data_for_account(account):
|
||||||
|
time_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
|
||||||
|
banner_info = get_banner_info(account)
|
||||||
|
# 返回数据列表
|
||||||
|
return [time_str, banner_info.registerMembers, banner_info.firstDepositNum, banner_info.netWinLose,
|
||||||
|
banner_info.effectiveNew, banner_info.activeMembers]
|
||||||
|
|
||||||
|
|
||||||
|
class ButtonTask(QRunnable):
|
||||||
|
def __init__(self, query_type, selected_date_str, user):
|
||||||
|
super().__init__()
|
||||||
|
self.query_type = query_type
|
||||||
|
self.selected_date_str = selected_date_str
|
||||||
|
self.user = user
|
||||||
|
self.signals = TaskSignals()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
result = ''
|
||||||
|
auto_clipboard = False
|
||||||
|
notify = False
|
||||||
|
if self.query_type == '报数':
|
||||||
|
result = text_count_by_user(self.user, self.selected_date_str)
|
||||||
|
auto_clipboard = True
|
||||||
|
notify = True
|
||||||
|
elif self.query_type == '存款失败用户':
|
||||||
|
result = get_pay_failed_by_user(self.user, self.selected_date_str)
|
||||||
|
if '无' not in result:
|
||||||
|
auto_clipboard = True
|
||||||
|
notify = True
|
||||||
|
elif self.query_type == '负盈利':
|
||||||
|
result = get_net_win(self.user, self.selected_date_str)
|
||||||
|
elif self.query_type == '薪资':
|
||||||
|
result = get_salary(self.user, self.selected_date_str)
|
||||||
|
|
||||||
|
# 数据查询完成,发出信号
|
||||||
|
self.signals.query_completed.emit(result, auto_clipboard, notify)
|
1
src/ui/icon/close.svg
Normal file
1
src/ui/icon/close.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1705040984076" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6674" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M568.277333 512l283.008-327.637333c4.736-5.418667 0.725333-13.696-6.570666-13.696H758.613333a17.792 17.792 0 0 0-13.226666 5.973333L512 446.890667 278.613333 176.64A17.450667 17.450667 0 0 0 265.344 170.666667H179.285333c-7.296 0-11.306667 8.277333-6.570666 13.696L455.722667 512l-283.008 327.594667c-4.736 5.461333-0.725333 13.738667 6.570666 13.738666H265.386667a17.792 17.792 0 0 0 13.226666-5.973333L512 577.109333l233.386667 270.250667c3.242667 3.754667 8.106667 5.973333 13.269333 5.973333h86.058667c7.338667 0 11.306667-8.277333 6.570666-13.738666L568.277333 512z" fill="#dfe1e2" p-id="6675"></path></svg>
|
After Width: | Height: | Size: 945 B |
BIN
src/ui/icon/icon.ico
Normal file
BIN
src/ui/icon/icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
BIN
src/ui/icon/icon.png
Normal file
BIN
src/ui/icon/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 546 KiB |
1
src/ui/icon/max.svg
Normal file
1
src/ui/icon/max.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1705041340268" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7442" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M358.058667 128H156.970667A28.970667 28.970667 0 0 0 128 157.013333v202.837334c0 7.978667 6.528 14.506667 14.506667 14.506666h43.434666a14.506667 14.506667 0 0 0 14.506667-14.506666V200.448h157.610667a14.506667 14.506667 0 0 0 14.506666-14.506667V142.506667a14.506667 14.506667 0 0 0-14.506666-14.506667zM881.493333 649.642667h-43.434666a14.506667 14.506667 0 0 0-14.506667 14.506666v159.402667h-157.610667a14.506667 14.506667 0 0 0-14.506666 14.506667v43.434666c0 7.978667 6.570667 14.506667 14.506666 14.506667h201.088c16 0 28.970667-12.928 28.970667-29.013333v-202.837334a14.506667 14.506667 0 0 0-14.506667-14.506666zM358.058667 823.552H200.448v-159.402667a14.506667 14.506667 0 0 0-14.506667-14.506666H142.506667a14.506667 14.506667 0 0 0-14.506667 14.506666v202.88c0 16 12.970667 28.970667 29.013333 28.970667h201.045334a14.506667 14.506667 0 0 0 14.506666-14.506667v-43.434666a14.506667 14.506667 0 0 0-14.506666-14.506667zM866.986667 128h-201.088a14.506667 14.506667 0 0 0-14.506667 14.506667v43.434666c0 7.978667 6.570667 14.506667 14.506667 14.506667h157.610666v159.402667c0 7.978667 6.528 14.506667 14.506667 14.506666h43.434667a14.506667 14.506667 0 0 0 14.506666-14.506666V156.970667A28.928 28.928 0 0 0 866.986667 128z" fill="#dfe1e2" p-id="7443"></path></svg>
|
After Width: | Height: | Size: 1.6 KiB |
1
src/ui/icon/min.svg
Normal file
1
src/ui/icon/min.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1705041096742" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7186" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M851.2 214.186667l-41.386667-41.386667a7.381333 7.381333 0 0 0-10.368 0L654.933333 317.397333l-50.176-50.176a7.253333 7.253333 0 0 0-12.373333 4.266667l-21.589333 173.525333a7.338667 7.338667 0 0 0 8.192 8.149334l173.568-21.546667c6.058667-0.725333 8.533333-8.106667 4.309333-12.373333L706.688 369.066667l144.597333-144.64a7.338667 7.338667 0 0 0-0.085333-10.24z m-406.186667 356.608l-173.568 21.589333a7.338667 7.338667 0 0 0-4.309333 12.373333l50.176 50.176-144.512 144.512a7.381333 7.381333 0 0 0 0 10.368l41.386667 41.386667a7.381333 7.381333 0 0 0 10.368 0l144.597333-144.64 50.176 50.218667a7.253333 7.253333 0 0 0 12.373333-4.309334l21.461334-173.482666a7.253333 7.253333 0 0 0-8.106667-8.192z" fill="#dfe1e2" p-id="7187"></path></svg>
|
After Width: | Height: | Size: 1.0 KiB |
1
src/ui/icon/top.svg
Normal file
1
src/ui/icon/top.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1705040893836" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6460" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M927.701333 375.978667l-279.637333-279.637334A37.546667 37.546667 0 0 0 621.397333 85.333333a37.546667 37.546667 0 0 0-26.666666 11.008L411.904 279.296a366.506667 366.506667 0 0 0-41.770667-2.304 374.016 374.016 0 0 0-234.325333 82.048 37.717333 37.717333 0 0 0-3.072 56.064l206.208 206.208-244.48 244.224a17.92 17.92 0 0 0-5.205333 11.093333l-3.84 42.24a18.090667 18.090667 0 0 0 18.048 19.754667c0.554667 0 1.109333 0 1.706666-0.128l42.24-3.84a17.92 17.92 0 0 0 11.093334-5.248l244.437333-244.437333 206.208 206.208a37.546667 37.546667 0 0 0 26.666667 11.008 37.546667 37.546667 0 0 0 29.397333-14.08 374.826667 374.826667 0 0 0 79.658667-276.224l182.826666-182.826667a37.589333 37.589333 0 0 0 0-53.077333z m-240.725333 178.346666l-27.776 27.818667 4.266667 39.04a294.997333 294.997333 0 0 1-34.474667 174.677333L228.309333 394.922667a293.546667 293.546667 0 0 1 174.634667-34.517334l39.04 4.309334 27.818667-27.776 151.722666-151.722667 217.301334 217.301333-151.850667 151.850667z" fill="#dfe1e2" p-id="6461"></path></svg>
|
After Width: | Height: | Size: 1.3 KiB |
94
src/ui/style.qss
Normal file
94
src/ui/style.qss
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
QPushButton {
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
transition: background-color 0.3s, border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 采用更柔和的颜色 */
|
||||||
|
/* Primary 风格的按钮 */
|
||||||
|
QPushButton#Primary {
|
||||||
|
background-color: #0056b3; /* 暗淡蓝色 */
|
||||||
|
}
|
||||||
|
QPushButton#Primary:hover {
|
||||||
|
background-color: #0044a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Secondary 风格的按钮 */
|
||||||
|
QPushButton#Secondary {
|
||||||
|
background-color: #383d41; /* 暗灰色 */
|
||||||
|
}
|
||||||
|
QPushButton#Secondary:hover {
|
||||||
|
background-color: #303438;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success 风格的按钮 */
|
||||||
|
QPushButton#Success {
|
||||||
|
background-color: #155724; /* 暗绿色 */
|
||||||
|
}
|
||||||
|
QPushButton#Success:hover {
|
||||||
|
background-color: #0f4b1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Info 风格的按钮 */
|
||||||
|
QPushButton#Info {
|
||||||
|
background-color: #0c5460; /* 暗青色 */
|
||||||
|
}
|
||||||
|
QPushButton#Info:hover {
|
||||||
|
background-color: #0a4a56;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Warning 风格的按钮 */
|
||||||
|
QPushButton#Warning {
|
||||||
|
background-color: #856404; /* 暗黄色 */
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
QPushButton#Warning:hover {
|
||||||
|
background-color: #755c03;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Danger 风格的按钮 */
|
||||||
|
QPushButton#Danger {
|
||||||
|
background-color: #721c24; /* 暗红色 */
|
||||||
|
}
|
||||||
|
QPushButton#Danger:hover {
|
||||||
|
background-color: #621b21;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light 风格的按钮 */
|
||||||
|
QPushButton#Light {
|
||||||
|
background-color: #818182; /* 浅灰色 */
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
QPushButton#Light:hover {
|
||||||
|
background-color: #737475;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark 风格的按钮 */
|
||||||
|
QPushButton#Dark {
|
||||||
|
background-color: #1b1c1e; /* 接近黑色 */
|
||||||
|
}
|
||||||
|
QPushButton#Dark:hover {
|
||||||
|
background-color: #1a1b1d;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
QToolButton {
|
||||||
|
background-color: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
QToolButton:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 20) !important;
|
||||||
|
border: 1px solid #555 !important; /* 鼠标悬停时的边框 */
|
||||||
|
}
|
||||||
|
QToolButton:pressed, QToolButton:checked {
|
||||||
|
background-color: rgba(255, 255, 255, 40) !important;
|
||||||
|
}
|
||||||
|
QToolButton#stayOnTopButton:checked {
|
||||||
|
border: 1px solid #555 !important; /* 置顶时的边框 */
|
||||||
|
}
|
||||||
|
QToolButton#closeButton:hover {
|
||||||
|
background-color: red !important; /* 鼠标悬停在关闭按钮上时的背景色 */
|
||||||
|
}
|
94
src/ui/title_bar.py
Normal file
94
src/ui/title_bar.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
from PyQt6.QtGui import QFont, QIcon, QPixmap
|
||||||
|
from PyQt6.QtWidgets import QHBoxLayout, QLabel, QToolButton, QWidget
|
||||||
|
|
||||||
|
|
||||||
|
class CustomTitleBar(QWidget):
|
||||||
|
def __init__(self, parent):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.parent = parent
|
||||||
|
self.setup_ui()
|
||||||
|
self.mousePressed = False
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
self.layout = QHBoxLayout()
|
||||||
|
self.layout.setContentsMargins(0, 0, 0, 0) # 设置布局的边距为0
|
||||||
|
self.layout.setSpacing(0) # 设置按钮之间的间距为0
|
||||||
|
|
||||||
|
# 创建图标 QLabel
|
||||||
|
self.iconLabel = QLabel(self)
|
||||||
|
self.iconLabel.setFixedSize(32, 32) # 设置图标大小
|
||||||
|
iconPixmap = QPixmap('icons:icon.png').scaled(24, 24, Qt.AspectRatioMode.KeepAspectRatio)
|
||||||
|
self.iconLabel.setPixmap(iconPixmap)
|
||||||
|
self.layout.addWidget(self.iconLabel) # 添加图标到布局
|
||||||
|
|
||||||
|
# 创建标题 QLabel
|
||||||
|
font = QFont()
|
||||||
|
font.setPointSize(11)
|
||||||
|
self.titleLabel = QLabel("zayac的小工具")
|
||||||
|
self.titleLabel.setFont(font)
|
||||||
|
self.titleLabel.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||||
|
self.layout.addWidget(self.titleLabel, 1)
|
||||||
|
|
||||||
|
# 使用图标按钮并应用样式
|
||||||
|
self.stayOnTopButton = QToolButton()
|
||||||
|
self.stayOnTopButton.setIcon(QIcon('icons:top.svg'))
|
||||||
|
self.stayOnTopButton.setFixedSize(32, 32) # 设置按钮的固定大小
|
||||||
|
self.stayOnTopButton.setCheckable(True)
|
||||||
|
self.stayOnTopButton.clicked.connect(self.toggle_stay_on_top)
|
||||||
|
self.layout.addWidget(self.stayOnTopButton)
|
||||||
|
|
||||||
|
# 设置其他按钮
|
||||||
|
self.minimizeButton = QToolButton()
|
||||||
|
self.minimizeButton.setIcon(QIcon('icons:min.svg'))
|
||||||
|
self.minimizeButton.setFixedSize(32, 32)
|
||||||
|
self.minimizeButton.clicked.connect(self.parent.showMinimized)
|
||||||
|
self.layout.addWidget(self.minimizeButton)
|
||||||
|
|
||||||
|
self.maximizeButton = QToolButton()
|
||||||
|
self.maximizeButton.setIcon(QIcon('icons:max.svg'))
|
||||||
|
self.maximizeButton.setFixedSize(32, 32)
|
||||||
|
self.maximizeButton.clicked.connect(self.toggle_maximize)
|
||||||
|
self.layout.addWidget(self.maximizeButton)
|
||||||
|
|
||||||
|
self.closeButton = QToolButton()
|
||||||
|
self.closeButton.setIcon(QIcon('icons:close.svg'))
|
||||||
|
self.closeButton.setFixedSize(32, 32)
|
||||||
|
self.closeButton.clicked.connect(self.parent.close)
|
||||||
|
self.closeButton.setObjectName("closeButton")
|
||||||
|
self.layout.addWidget(self.closeButton)
|
||||||
|
|
||||||
|
# 在创建按钮后立即设置样式表
|
||||||
|
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
self.mousePressed = True
|
||||||
|
self.mousePos = event.globalPosition().toPoint()
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, event):
|
||||||
|
if self.mousePressed:
|
||||||
|
self.parent.move(self.parent.pos() + (event.globalPosition().toPoint() - self.mousePos))
|
||||||
|
self.mousePos = event.globalPosition().toPoint()
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event):
|
||||||
|
self.mousePressed = False
|
||||||
|
|
||||||
|
def toggle_stay_on_top(self):
|
||||||
|
if self.stayOnTopButton.isChecked():
|
||||||
|
self.parent.setWindowFlags(self.parent.windowFlags() | Qt.WindowType.WindowStaysOnTopHint)
|
||||||
|
else:
|
||||||
|
self.parent.setWindowFlags(self.parent.windowFlags() & ~Qt.WindowType.WindowStaysOnTopHint)
|
||||||
|
self.parent.show() # 重新显示窗口以应用新的窗口标志
|
||||||
|
|
||||||
|
def enterEvent(self, event):
|
||||||
|
self.parent.reset_cursor_style() # 通知主窗口重置鼠标样式
|
||||||
|
|
||||||
|
def leaveEvent(self, event):
|
||||||
|
self.parent.reset_cursor_style() # 通知主窗口重置鼠标样式
|
||||||
|
|
||||||
|
def toggle_maximize(self):
|
||||||
|
if self.parent.isMaximized():
|
||||||
|
self.parent.showNormal()
|
||||||
|
else:
|
||||||
|
self.parent.showMaximized()
|
Loading…
Reference in New Issue
Block a user