[Enhance] PTT record & log DB cleaner

- Add support for recording users' potential each day
- Add a log database cleaner tool
- A small change: `/user/<user_id> PUT` API can ban user now.
This commit is contained in:
Lost-MSth
2023-05-03 00:37:41 +08:00
parent 880b66a995
commit bd74d96250
9 changed files with 226 additions and 9 deletions

View File

@@ -35,6 +35,16 @@ def logdb_execute_func(sql, *args, **kwargs):
c.execute(sql, *args, **kwargs) c.execute(sql, *args, **kwargs)
def logdb_execute_many_func(sql, *args, **kwargs):
with Connect(Constant.SQLITE_LOG_DATABASE_PATH) as c:
c.executemany(sql, *args, **kwargs)
def logdb_execute(sql: str, *args, **kwargs): def logdb_execute(sql: str, *args, **kwargs):
'''异步执行SQL日志库写入注意不会直接返回结果''' '''异步执行SQL日志库写入注意不会直接返回结果'''
return BGTask(logdb_execute_func, sql, *args, **kwargs) return BGTask(logdb_execute_func, sql, *args, **kwargs)
def logdb_execute_many(sql: str, *args, **kwargs):
'''异步批量执行SQL日志库写入注意不会直接返回结果'''
return BGTask(logdb_execute_many_func, sql, *args, **kwargs)

View File

@@ -1,6 +1,7 @@
from .config_manager import Config from .config_manager import Config
ARCAEA_SERVER_VERSION = 'v2.11.1.3' ARCAEA_SERVER_VERSION = 'v2.11.1.3'
ARCAEA_LOG_DATBASE_VERSION = 'v1.1'
class Constant: class Constant:
@@ -101,4 +102,6 @@ class Constant:
DATABASE_MIGRATE_TABLES = ['user', 'friend', 'best_score', 'recent30', 'user_world', 'item', 'user_item', 'purchase', 'purchase_item', 'user_save', DATABASE_MIGRATE_TABLES = ['user', 'friend', 'best_score', 'recent30', 'user_world', 'item', 'user_item', 'purchase', 'purchase_item', 'user_save',
'login', 'present', 'user_present', 'present_item', 'redeem', 'user_redeem', 'redeem_item', 'api_login', 'chart', 'user_course', 'user_char', 'user_role'] 'login', 'present', 'user_present', 'present_item', 'redeem', 'user_redeem', 'redeem_item', 'api_login', 'chart', 'user_course', 'user_char', 'user_role']
LOG_DATABASE_MIGRATE_TABLES = ['cache', 'user_score', 'user_rating']
UPDATE_WITH_NEW_CHARACTER_DATA = Config.UPDATE_WITH_NEW_CHARACTER_DATA UPDATE_WITH_NEW_CHARACTER_DATA = Config.UPDATE_WITH_NEW_CHARACTER_DATA

View File

@@ -7,11 +7,12 @@ from time import time
from traceback import format_exc from traceback import format_exc
from core.config_manager import Config from core.config_manager import Config
from core.constant import ARCAEA_SERVER_VERSION from core.constant import ARCAEA_LOG_DATBASE_VERSION, ARCAEA_SERVER_VERSION
from core.course import Course from core.course import Course
from core.download import DownloadList from core.download import DownloadList
from core.purchase import Purchase from core.purchase import Purchase
from core.sql import Connect, DatabaseMigrator, MemoryDatabase from core.sql import (Connect, DatabaseMigrator, LogDatabaseMigrator,
MemoryDatabase)
from core.user import UserRegister from core.user import UserRegister
from core.util import try_rename from core.util import try_rename
@@ -208,6 +209,29 @@ class FileChecker:
self.logger.error( self.logger.error(
f'Failed to new the file {Config.SQLITE_LOG_DATABASE_PATH}') f'Failed to new the file {Config.SQLITE_LOG_DATABASE_PATH}')
return False return False
else:
# 检查更新
with Connect(Config.SQLITE_LOG_DATABASE_PATH) as c:
try:
x = c.execute(
'''select value from cache where key="version"''').fetchone()
except:
x = None
if not x or x[0] != ARCAEA_LOG_DATBASE_VERSION:
self.logger.warning(
f'Maybe the file `{Config.SQLITE_LOG_DATABASE_PATH}` is an old version.')
try:
self.logger.info(
f'Try to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
self.update_log_database()
self.logger.info(
f'Success to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
except Exception as e:
self.logger.error(format_exc())
self.logger.error(
f'Failed to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
return False
if not self.check_file(Config.SQLITE_DATABASE_PATH): if not self.check_file(Config.SQLITE_DATABASE_PATH):
# 新建数据库 # 新建数据库
try: try:
@@ -275,6 +299,12 @@ class FileChecker:
DatabaseMigrator(old_path, new_path).update_database() DatabaseMigrator(old_path, new_path).update_database()
os.remove(old_path) os.remove(old_path)
@staticmethod
def update_log_database(old_path: str = Config.SQLITE_LOG_DATABASE_PATH) -> None:
'''直接更新日志数据库'''
if os.path.isfile(old_path):
LogDatabaseMigrator(old_path).update_database()
def check_song_file(self) -> bool: def check_song_file(self) -> bool:
'''检查song有关文件并初始化缓存''' '''检查song有关文件并初始化缓存'''
f = self.check_folder(Config.SONG_FILE_FOLDER_PATH) f = self.check_folder(Config.SONG_FILE_FOLDER_PATH)

View File

@@ -2,14 +2,15 @@ from base64 import b64encode
from os import urandom from os import urandom
from time import time from time import time
from .bgtask import logdb_execute from .bgtask import BGTask, logdb_execute
from .config_manager import Config
from .constant import Constant from .constant import Constant
from .course import CoursePlay from .course import CoursePlay
from .error import NoData, StaminaNotEnough from .error import NoData, StaminaNotEnough
from .item import ItemCore from .item import ItemCore
from .song import Chart from .song import Chart
from .sql import Query, Sql from .sql import Connect, Query, Sql
from .util import md5 from .util import get_today_timestamp, md5
from .world import WorldPlay from .world import WorldPlay
@@ -431,6 +432,18 @@ class UserPlay(UserScore):
logdb_execute('''insert into user_score values(?,?,?,?,?,?,?,?,?,?,?,?,?)''', (self.user.user_id, self.song.song_id, self.song.difficulty, self.time_played, logdb_execute('''insert into user_score values(?,?,?,?,?,?,?,?,?,?,?,?,?)''', (self.user.user_id, self.song.song_id, self.song.difficulty, self.time_played,
self.score, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count, self.health, self.modifier, self.clear_type, self.rating)) self.score, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count, self.health, self.modifier, self.clear_type, self.rating))
def record_rating_ptt(self, user_rating_ptt: float) -> None:
'''向log数据库记录用户ptt变化'''
today_timestamp = get_today_timestamp()
with Connect(Config.SQLITE_LOG_DATABASE_PATH) as c2:
old_ptt = c2.execute('''select rating_ptt from user_rating where user_id=? and time=?''', (
self.user.user_id, today_timestamp)).fetchone()
old_ptt = 0 if old_ptt is None else old_ptt[0]
if old_ptt != user_rating_ptt:
c2.execute('''insert or replace into user_rating values(?,?,?)''',
(self.user.user_id, today_timestamp, user_rating_ptt))
def upload_score(self) -> None: def upload_score(self) -> None:
'''上传分数包括user的recent更新best更新recent30更新世界模式计算''' '''上传分数包括user的recent更新best更新recent30更新世界模式计算'''
self.get_play_state() self.get_play_state()
@@ -474,10 +487,11 @@ class UserPlay(UserScore):
self.update_recent30() self.update_recent30()
# 总PTT更新 # 总PTT更新
self.user.rating_ptt = int(self.ptt.value * 100) user_rating_ptt = self.ptt.value
self.user.rating_ptt = int(user_rating_ptt * 100)
BGTask(self.record_rating_ptt, user_rating_ptt) # 记录总PTT变换
self.c.execute('''update user set rating_ptt = :a where user_id = :b''', { self.c.execute('''update user set rating_ptt = :a where user_id = :b''', {
'a': self.user.rating_ptt, 'b': self.user.user_id}) 'a': self.user.rating_ptt, 'b': self.user.user_id})
# TODO: PTT log
# 世界模式判断 # 世界模式判断
if self.is_world_mode: if self.is_world_mode:

View File

@@ -1,8 +1,10 @@
import os
import sqlite3 import sqlite3
import traceback import traceback
from atexit import register from atexit import register
from .constant import Constant from .config_manager import Config
from .constant import ARCAEA_LOG_DATBASE_VERSION, Constant
from .error import ArcError, InputError from .error import ArcError, InputError
@@ -404,6 +406,31 @@ class DatabaseMigrator:
self.update_user_char_full(c2) # 更新user_char_full self.update_user_char_full(c2) # 更新user_char_full
class LogDatabaseMigrator:
def __init__(self, c1_path: str = Config.SQLITE_LOG_DATABASE_PATH) -> None:
self.c1_path = c1_path
# self.c2_path = c2_path
self.init_folder_path = Config.DATABASE_INIT_PATH
self.c = None
@property
def sql_path(self) -> str:
return os.path.join(self.init_folder_path, 'log_tables.sql')
def table_update(self) -> None:
'''直接更新数据库结构'''
with open(self.sql_path, 'r') as f:
self.c.executescript(f.read())
self.c.execute(
'''insert or replace into cache values("version", :a, -1);''', {'a': ARCAEA_LOG_DATBASE_VERSION})
def update_database(self) -> None:
with Connect(self.c1_path) as c:
self.c = c
self.table_update()
class MemoryDatabase: class MemoryDatabase:
conn = sqlite3.connect('file:arc_tmp?mode=memory&cache=shared', uri=True) conn = sqlite3.connect('file:arc_tmp?mode=memory&cache=shared', uri=True)

View File

@@ -763,7 +763,7 @@ class UserChanger(UserInfo, UserRegister):
if columns is not None: if columns is not None:
d = {} d = {}
for column in columns: for column in columns:
if column == 'password': if column == 'password' and self.password != '':
d[column] = self.hash_pwd d[column] = self.hash_pwd
else: else:
d[column] = self.__dict__[column] d[column] = self.__dict__[column]

View File

@@ -1,5 +1,7 @@
import hashlib import hashlib
import os import os
from datetime import date
from time import mktime
def md5(code: str) -> str: def md5(code: str) -> str:
@@ -37,3 +39,8 @@ def try_rename(path: str, new_path: str) -> str:
os.rename(path, final_path) os.rename(path, final_path)
return final_path return final_path
def get_today_timestamp():
'''相对于本机本地时间的今天0点的时间戳'''
return int(mktime(date.today().timetuple()))

View File

@@ -17,6 +17,11 @@ clear_type int,
rating real, rating real,
primary key(user_id, song_id, difficulty, time_played) primary key(user_id, song_id, difficulty, time_played)
); );
create table if not exists user_rating(user_id int,
time int,
rating_ptt real,
primary key(user_id, time)
);
create index if not exists user_score_1 on user_score (song_id, difficulty); create index if not exists user_score_1 on user_score (song_id, difficulty);
create index if not exists user_score_2 on user_score (time_played); create index if not exists user_score_2 on user_score (time_played);

121
tools/clean_logdb.py Normal file
View File

@@ -0,0 +1,121 @@
import os
import sqlite3
from datetime import datetime
from time import mktime
LOG_DATABASE_PATH = './arcaea_log.db'
class Connect():
# 数据库连接类,上下文管理
def __init__(self, file_path=LOG_DATABASE_PATH):
"""
数据库连接
接受:文件路径
返回sqlite3连接操作对象
"""
self.file_path = file_path
def __enter__(self):
self.conn = sqlite3.connect(self.file_path)
self.c = self.conn.cursor()
return self.c
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
if exc_type is not None:
print(f'exc_type: {exc_type}')
print(f'exc_val: {exc_val}')
print(f'exc_tb: {exc_tb}')
if self.conn:
self.conn.rollback()
if self.conn:
self.conn.commit()
self.conn.close()
return True
def clean_by_time(table_name: str, time_colume_name: str, colume_count: int):
with Connect() as c:
today = datetime.now()
print(f'The time now is {today}.')
day = input(
'Please input the number of days the data before which you want to delete: ')
try:
day = int(day)
except ValueError:
print('Invalid input!')
return
time = mktime(today.timetuple()) - day * 24 * 60 * 60
delete_count = c.execute(
f'select count(*) from {table_name} where {time_colume_name} < ?', (time,)).fetchone()[0]
all_count = c.execute(
f'select count(*) from {table_name}').fetchone()[0]
print(
f'Before {day} days, there are {delete_count} records to be deleted, {all_count} records in total.')
flag = input('Are you sure to delete these records? (y/n) ')
if flag == 'y' or flag == 'Y':
if delete_count >= 1000000:
print(
'It will cost a long time to delete these records, please wait patiently...')
print('Deleting...')
c.execute('PRAGMA cache_size = 32768')
c.execute('PRAGMA synchronous = OFF')
c.execute('PRAGMA temp_store = MEMORY')
if delete_count / all_count >= 0.8:
data = c.execute(
f'select * from {table_name} where {time_colume_name} > ?', (time,)).fetchall()
c.execute(f'delete from {table_name}')
c.executemany(
f'insert into {table_name} values ({",".join(["?"]*colume_count)})', data)
else:
c.execute(
f'delete from {table_name} where {time_colume_name} < ?', (time,))
c.execute('PRAGMA temp_store = DEFAULT')
print('Delete successfully!')
else:
print('Delete canceled!')
def vacuum():
print('This operation will release unused space in the database file.')
print('It will cost a long time to release unused space if the database file is so large.')
flag = input('Are you sure to release unused space? (y/n) ')
if flag == 'y' or flag == 'Y':
with Connect() as c:
print('Releasing unused space...')
c.execute('vacuum')
print('Release unused space successfully!')
else:
print('Release unused space canceled!')
def main():
if not os.path.exists(LOG_DATABASE_PATH):
print('The database file `arcaea_log.db` does not exist!')
print('-- Arcaea Server Log Database Cleaner --')
print('Note: It is more recommended to delete the database file directly.')
while True:
print('-' * 40)
print('1. clean `user_score` table')
print('2. clean `user_rating` table')
print('3. release unused space (`vacuum` command)')
print('0. exit')
choice = input('Please input your choice: ')
if choice == '1':
clean_by_time('user_score', 'time_played', 13)
elif choice == '2':
clean_by_time('user_rating', 'time', 3)
elif choice == '3':
vacuum()
elif choice == '0':
break
else:
print('Invalid choice!')
if __name__ == '__main__':
main()
input('Press `Enter` key to exit.')