bugfix:修复notice事件扩散问题 (#2132)

* bugfix:修复notice事件扩散问题

* 优化并发调度

* bugfix:修复签到样式

* bugfix:功能调用统计修复

* bugfix:修复私聊时功能调用统计显示已退群问题

* 提高插件适配兼容性

* 优化发送队列

* 修改权限检查设计

* 继续修改权限检查设计

* 完善权限检查设计

* 优化sqlite配置

* 优化数据库初始化

* 代码整理,无用代码清理

* bugfix:修复启动时数据库校验问题

* bugfix:修复预算裁剪过于激进问题
This commit is contained in:
Copaan
2026-05-28 22:57:28 +08:00
committed by GitHub
parent 12fc5663fb
commit 5596497947
57 changed files with 7557 additions and 1522 deletions
+113
View File
@@ -0,0 +1,113 @@
from __future__ import annotations
import asyncio
from collections import deque
import contextlib
import time
from typing import TYPE_CHECKING
from zhenxun.services.log import logger
if TYPE_CHECKING:
from .bot_message_store import BotMessageStore
LOG_COMMAND = "BotMessageStore"
_BUFFER_MAX_RETAIN = 20_000
_FLUSH_TRIGGER_SIZE = 64
_FLUSH_BATCH_SIZE = 500
_FLUSH_INTERVAL_SECONDS = 5.0
_DROP_LOG_INTERVAL_SECONDS = 10.0
_buffer: deque[BotMessageStore] = deque()
_buffer_lock = asyncio.Lock()
_flush_lock = asyncio.Lock()
_flush_task: asyncio.Task[None] | None = None
_dropped = 0
_last_drop_log_at = 0.0
def _ensure_flush_task() -> None:
global _flush_task
if _flush_task is not None and not _flush_task.done():
return
try:
loop = asyncio.get_running_loop()
except RuntimeError:
return
_flush_task = loop.create_task(_flush_loop())
def _record_drop() -> None:
global _dropped, _last_drop_log_at
_dropped += 1
now = time.monotonic()
if now - _last_drop_log_at < _DROP_LOG_INTERVAL_SECONDS:
return
_last_drop_log_at = now
logger.warning(
f"bot_message_store buffer full, dropped {_dropped} records, "
f"backlog={len(_buffer)}",
LOG_COMMAND,
)
async def _flush_loop() -> None:
while True:
await asyncio.sleep(_FLUSH_INTERVAL_SECONDS)
try:
await flush_bot_message_store_buffer("定时")
except asyncio.CancelledError:
raise
except Exception as exc:
logger.warning("定时批量写入 Bot 发送记录失败", LOG_COMMAND, e=exc)
async def append_bot_message_store_record(record: BotMessageStore) -> None:
_ensure_flush_task()
async with _buffer_lock:
if len(_buffer) >= _BUFFER_MAX_RETAIN:
_buffer.popleft()
_record_drop()
_buffer.append(record)
should_flush = len(_buffer) >= _FLUSH_TRIGGER_SIZE and not _flush_lock.locked()
if should_flush:
await flush_bot_message_store_buffer("缓冲区触发")
async def flush_bot_message_store_buffer(reason: str) -> int:
from .bot_message_store import BotMessageStore
async with _flush_lock:
written = 0
while True:
batch: list[BotMessageStore] = []
async with _buffer_lock:
while _buffer and len(batch) < _FLUSH_BATCH_SIZE:
batch.append(_buffer.popleft())
if not batch:
break
try:
await BotMessageStore.bulk_create(batch, batch_size=_FLUSH_BATCH_SIZE)
except Exception as exc:
async with _buffer_lock:
retain_count = max(_BUFFER_MAX_RETAIN - len(_buffer), 0)
for record in reversed(batch[-retain_count:]):
_buffer.appendleft(record)
logger.error(f"{reason}批量写入 Bot 发送记录失败", LOG_COMMAND, e=exc)
return written
written += len(batch)
if written:
logger.debug(f"{reason}批量写入 Bot 发送记录 {written} 条", LOG_COMMAND)
return written
async def stop_bot_message_store_buffer() -> int:
global _flush_task
task = _flush_task
_flush_task = None
if task is not None:
task.cancel()
with contextlib.suppress(BaseException):
await task
return await flush_bot_message_store_buffer("关闭")
+49
View File
@@ -0,0 +1,49 @@
from typing import ClassVar
from tortoise import fields
from zhenxun.services.db_context import Model
class AuthDecisionLog(Model):
id = fields.IntField(pk=True, generated=True, auto_increment=True)
"""自增id"""
bot_id = fields.CharField(255, null=True, description="Bot ID")
"""Bot ID"""
platform = fields.CharField(64, null=True, description="平台")
"""平台"""
group_id = fields.CharField(255, null=True, description="群组id")
"""群组id"""
user_id = fields.CharField(255, null=True, description="用户id")
"""用户id"""
module = fields.CharField(255, null=True, description="插件模块")
"""插件模块"""
effect = fields.CharField(32, description="决策结果")
"""决策结果 allow/deny/skip/defer/error"""
reason = fields.CharField(255, null=True, description="原因")
"""原因"""
shadow_effect = fields.CharField(32, null=True, description="影子决策结果")
"""影子决策结果"""
shadow_reason = fields.CharField(255, null=True, description="影子决策原因")
"""影子决策原因"""
side_effect_state = fields.TextField(null=True, description="副作用状态")
"""副作用状态 JSON 摘要"""
latency_ms = fields.FloatField(default=0, description="耗时毫秒")
"""耗时毫秒"""
overloaded = fields.BooleanField(default=False, description="是否过载")
"""是否过载"""
create_time = fields.DatetimeField(auto_now_add=True, description="创建时间")
"""创建时间"""
class Meta: # pyright: ignore [reportIncompatibleVariableOverride]
table = "auth_decision_log"
table_description = "权限决策追加审计日志"
indexes: ClassVar = [
("create_time",),
("module", "create_time"),
("effect", "create_time"),
]
@classmethod
async def _run_script(cls):
return []
+1 -5
View File
@@ -229,8 +229,4 @@ class BanConsole(Model):
@classmethod
async def _run_script(cls):
return [
"CREATE INDEX idx_ban_console_user_id ON ban_console(user_id);",
"CREATE INDEX idx_ban_console_group_id ON ban_console(group_id);",
"ALTER TABLE ban_console ADD COLUMN ban_reason TEXT DEFAULT NULL;",
]
return []
+6 -29
View File
@@ -2,9 +2,9 @@ from typing import Literal, overload
from tortoise import fields
from zhenxun.configs.config import BotConfig
from zhenxun.services.cache.runtime_cache import BotMemoryCache
from zhenxun.services.db_context import Model
from zhenxun.services.db_context.schema_ops import AlterColumnType, RenameColumn
from zhenxun.utils.enum import CacheType
@@ -470,32 +470,9 @@ class BotConsole(Model):
@classmethod
async def _run_script(cls):
db_type = (BotConfig.get_sql_type() or "").lower()
scripts = [
"ALTER TABLE bot_console RENAME COLUMN block_plugin TO block_plugins;",
"ALTER TABLE bot_console RENAME COLUMN block_task TO block_tasks;",
"ALTER TABLE bot_console ADD available_plugins text default '';",
"ALTER TABLE bot_console ADD available_tasks text default '';",
return [
RenameColumn("bot_console", "block_plugin", "block_plugins"),
RenameColumn("bot_console", "block_task", "block_tasks"),
AlterColumnType("bot_console", "block_plugins", "TEXT"),
AlterColumnType("bot_console", "block_tasks", "TEXT"),
]
if "postgres" in db_type:
scripts.extend(
[
"ALTER TABLE bot_console ALTER COLUMN block_plugins TYPE TEXT;",
"ALTER TABLE bot_console ALTER COLUMN block_tasks TYPE TEXT;",
"ALTER TABLE bot_console ALTER COLUMN available_plugins TYPE TEXT;",
"ALTER TABLE bot_console ALTER COLUMN available_tasks TYPE TEXT;",
]
)
elif "mysql" in db_type:
scripts.extend(
[
"ALTER TABLE bot_console MODIFY COLUMN block_plugins TEXT;",
"ALTER TABLE bot_console MODIFY COLUMN block_tasks TEXT;",
"ALTER TABLE bot_console MODIFY COLUMN available_plugins TEXT;",
"ALTER TABLE bot_console MODIFY COLUMN available_tasks TEXT;",
]
)
return scripts
+26
View File
@@ -3,6 +3,8 @@ from tortoise import fields
from zhenxun.services.db_context import Model
from zhenxun.utils.enum import BotSentType
from ._bot_message_buffer import append_bot_message_store_record
class BotMessageStore(Model):
id = fields.IntField(pk=True, generated=True, auto_increment=True)
@@ -27,3 +29,27 @@ class BotMessageStore(Model):
class Meta: # pyright: ignore [reportIncompatibleVariableOverride]
table = "bot_message_store"
table_description = "Bot发送消息列表"
@classmethod
async def append_buffered(
cls,
*,
bot_id: str | None = None,
user_id: str | None = None,
group_id: str | None = None,
sent_type: BotSentType,
text: str | None = None,
plain_text: str | None = None,
platform: str | None = None,
) -> None:
await append_bot_message_store_record(
cls(
bot_id=bot_id,
user_id=user_id,
group_id=group_id,
sent_type=sent_type,
text=text,
plain_text=plain_text,
platform=platform,
)
)
+64 -35
View File
@@ -1,11 +1,12 @@
from datetime import datetime, timedelta
from typing import ClassVar, Literal
from typing import Any, ClassVar, Literal
from typing_extensions import Self
from tortoise import fields
from tortoise.functions import Count
from tortoise.expressions import Q
from zhenxun.services.db_context import Model
from zhenxun.services.db_context.schema_ops import AlterColumnType, RenameColumn
class ChatHistory(Model):
@@ -35,6 +36,41 @@ class ChatHistory(Model):
("user_id", "group_id"),
]
@classmethod
def _platform_from_scope(cls, platform_scope: str | None) -> str | None:
"""Map the new fine-grained scope back to the legacy platform column."""
if not platform_scope:
return None
scope = str(platform_scope).lower()
if scope in {"qq", "qq_client", "qq_api"} or scope.startswith("qq_"):
return "qq"
if "onebot" in scope:
return "qq"
return scope
@classmethod
def scoped_query(cls, platform_scope: str | None = None, **filters: Any):
"""Return a chat-history query compatible with platform_scope callers.
chat_history currently stores the coarse legacy ``platform`` column rather
than a dedicated ``platform_scope`` column, so this method intentionally
stays as a thin compatibility shim.
"""
query = cls.filter(**filters)
if not platform_scope:
return query
if "platform" in filters or any(k.startswith("platform__") for k in filters):
return query
platform = cls._platform_from_scope(platform_scope)
if not platform:
return query
if platform == "qq" and str(platform_scope).lower() in {"qq", "qq_client"}:
return query.filter(
Q(platform=platform) | Q(platform__isnull=True) | Q(platform="")
)
return query.filter(platform=platform)
@classmethod
async def get_group_msg_rank(
cls,
@@ -42,7 +78,7 @@ class ChatHistory(Model):
limit: int = 10,
order: str = "DESC",
date_scope: tuple[datetime, datetime] | None = None,
) -> list[Self]:
) -> list[tuple[str, int]]:
"""获取排行数据
参数:
@@ -51,18 +87,9 @@ class ChatHistory(Model):
order: 排序类型,desc,des
date_scope: 日期范围
"""
o = "-" if order == "DESC" else ""
query = cls.filter(group_id=gid) if gid else cls
if date_scope:
filter_scope = (date_scope[0].isoformat(" "), date_scope[1].isoformat(" "))
query = query.filter(create_time__range=filter_scope)
return list(
await query.annotate(count=Count("user_id"))
.order_by(f"{o}count")
.group_by("user_id")
.limit(limit)
.values_list("user_id", "count")
) # type: ignore
from zhenxun.services.hot_query_cache import get_chat_history_rank_cached
return await get_chat_history_rank_cached(cls, gid, limit, order, date_scope)
@classmethod
async def get_group_first_msg_datetime(
@@ -73,22 +100,21 @@ class ChatHistory(Model):
参数:
group_id: 群组id
"""
if group_id:
message = (
await cls.filter(group_id=group_id).order_by("create_time").first()
)
else:
message = await cls.all().order_by("create_time").first()
return message.create_time if message else None
from zhenxun.services.hot_query_cache import (
get_chat_history_first_msg_datetime_cached,
)
return await get_chat_history_first_msg_datetime_cached(cls, group_id)
@classmethod
async def get_message(
cls,
uid: str,
gid: str,
uid: str | None,
gid: str | None,
type_: Literal["user", "group"],
msg_type: Literal["private", "group"] | None = None,
days: int | tuple[datetime, datetime] | None = None,
platform_scope: str | None = None,
) -> list[Self]:
"""获取消息查询query
@@ -98,15 +124,16 @@ class ChatHistory(Model):
type_: 类型,私聊或群聊
msg_type: 消息类型,用户或群聊
days: 限制日期
platform_scope: 兼容细粒度平台作用域
"""
if type_ == "user":
query = cls.filter(user_id=uid)
query = cls.scoped_query(platform_scope=platform_scope, user_id=uid)
if msg_type == "private":
query = query.filter(group_id__isnull=True)
elif msg_type == "group":
query = query.filter(group_id__not_isnull=True)
else:
query = cls.filter(group_id=gid)
query = cls.scoped_query(platform_scope=platform_scope, group_id=gid)
if uid:
query = query.filter(user_id=uid)
if days:
@@ -128,13 +155,15 @@ class ChatHistory(Model):
# 允许 plain_text 为空
"alter table chat_history alter plain_text drop not null;",
# 将user_id改为user_id
"ALTER TABLE chat_history RENAME COLUMN user_qq TO user_id;",
"ALTER TABLE chat_history "
"ALTER COLUMN user_id TYPE character varying(255);",
"ALTER TABLE chat_history "
"ALTER COLUMN group_id TYPE character varying(255);",
# 添加bot_id字段
"ALTER TABLE chat_history ADD bot_id VARCHAR(255);",
"ALTER TABLE chat_history ALTER COLUMN bot_id TYPE character varying(255);",
"ALTER TABLE chat_history ADD COLUMN platform character varying(255);",
RenameColumn("chat_history", "user_qq", "user_id"),
AlterColumnType(
"chat_history",
"user_id",
{"postgres": "character varying(255)", "mysql": "VARCHAR(255)"},
),
AlterColumnType(
"chat_history",
"group_id",
{"postgres": "character varying(255)", "mysql": "VARCHAR(255)"},
),
]
+1 -4
View File
@@ -8,7 +8,6 @@ from zhenxun.configs.config import BotConfig
from zhenxun.models.group_console import GroupConsole
from zhenxun.services.db_context import Model
from zhenxun.services.log import logger
from zhenxun.utils.common_utils import SqlUtils
from zhenxun.utils.enum import RequestHandleType, RequestType
from zhenxun.utils.exception import NotFoundError
from zhenxun.utils.manager.bot_profile_manager import BotProfileManager
@@ -173,6 +172,4 @@ class FgRequest(Model):
@classmethod
async def _run_script(cls):
return [
SqlUtils.add_column("fg_request", "message_ids", "character varying(255)")
]
return []
+2 -1
View File
@@ -1,6 +1,7 @@
from tortoise import fields
from zhenxun.services.db_context import Model
from zhenxun.services.db_context.schema_ops import DropColumn
class FriendUser(Model):
@@ -30,4 +31,4 @@ class FriendUser(Model):
@classmethod
def _run_script(cls):
return ["ALTER TABLE friend_users DROP COLUMN nickname;"]
return [DropColumn("friend_users", "nickname")]
+2 -6
View File
@@ -4,6 +4,7 @@ import uuid
from tortoise import fields
from zhenxun.services.db_context import Model
from zhenxun.services.db_context.schema_ops import DropColumn
class GoodsInfo(Model):
@@ -158,11 +159,6 @@ class GoodsInfo(Model):
@classmethod
async def _run_script(cls):
return [
"ALTER TABLE goods_info ADD uuid VARCHAR(255);",
"ALTER TABLE goods_info ADD daily_limit Integer DEFAULT 0;",
"ALTER TABLE goods_info ADD is_passive boolean DEFAULT False;",
"ALTER TABLE goods_info ADD icon VARCHAR(255);",
# 删除 daily_purchase_limit 字段
"ALTER TABLE goods_info DROP daily_purchase_limit;",
"ALTER TABLE goods_info ADD partition VARCHAR(255);",
DropColumn("goods_info", "daily_purchase_limit"),
]
+9 -45
View File
@@ -4,13 +4,13 @@ from typing_extensions import Self
from tortoise import fields
from tortoise.backends.base.client import BaseDBAsyncClient
from zhenxun.configs.config import BotConfig
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.models.task_info import TaskInfo
from zhenxun.services.cache import CacheRoot
from zhenxun.services.cache.runtime_cache import GroupMemoryCache
from zhenxun.services.data_access import DataAccess
from zhenxun.services.db_context import Model
from zhenxun.services.db_context.schema_ops import AlterColumnType, CreateIndex
from zhenxun.utils.enum import CacheType, DbLockType, PluginType
if TYPE_CHECKING:
@@ -516,49 +516,13 @@ class GroupConsole(Model):
@classmethod
def _run_script(cls):
db_type = (BotConfig.get_sql_type() or "").lower()
scripts = [
"ALTER TABLE group_console ADD superuser_block_plugin"
" Text NOT NULL DEFAULT '';",
"ALTER TABLE group_console ADD superuser_block_task"
" Text NOT NULL DEFAULT '';",
"CREATE INDEX idx_group_console_group_id ON group_console(group_id);",
(
"CREATE INDEX idx_group_console_group_null_channel ON "
"group_console(group_id) WHERE channel_id IS NULL;"
return [
CreateIndex(
"group_console",
("group_id",),
name="idx_group_console_group_null_channel",
where="channel_id IS NULL",
),
AlterColumnType("group_console", "block_plugin", "TEXT"),
AlterColumnType("group_console", "block_task", "TEXT"),
]
if "postgres" in db_type:
scripts.extend(
[
("ALTER TABLE group_console ALTER COLUMN block_plugin TYPE TEXT;"),
(
"ALTER TABLE group_console ALTER COLUMN "
"superuser_block_plugin TYPE TEXT;"
),
("ALTER TABLE group_console ALTER COLUMN block_task TYPE TEXT;"),
(
"ALTER TABLE group_console ALTER COLUMN "
"superuser_block_task TYPE TEXT;"
),
]
)
elif "mysql" in db_type:
scripts.extend(
[
("ALTER TABLE group_console MODIFY COLUMN block_plugin TEXT;"),
(
"ALTER TABLE group_console MODIFY COLUMN "
"superuser_block_plugin TEXT;"
),
("ALTER TABLE group_console MODIFY COLUMN block_task TEXT;"),
(
"ALTER TABLE group_console MODIFY COLUMN "
"superuser_block_task TEXT;"
),
]
)
return scripts
+22 -58
View File
@@ -2,8 +2,8 @@ from typing import ClassVar
from tortoise import fields
from zhenxun.configs.config import BotConfig
from zhenxun.services.db_context import Model
from zhenxun.services.db_context.schema_ops import AlterColumnType, DropColumn
class GroupInfoUser(Model):
@@ -35,9 +35,9 @@ class GroupInfoUser(Model):
参数:
group_id: 群号
"""
return set(
await cls.filter(group_id=group_id).values_list("user_id", flat=True)
) # type: ignore
from zhenxun.services.hot_query_cache import get_group_user_ids
return await get_group_user_ids(group_id)
@classmethod
async def get_user_all_group(cls, user_id: str) -> list[str]:
@@ -46,60 +46,24 @@ class GroupInfoUser(Model):
参数:
user_id: 用户id
"""
return list(
await cls.filter(user_id=user_id).values_list("group_id", flat=True)
) # type: ignore
from zhenxun.services.hot_query_cache import get_user_group_ids
return await get_user_group_ids(user_id)
@classmethod
async def _run_script(cls):
db_type = (BotConfig.get_sql_type() or "").lower()
scripts = ["ALTER TABLE group_info_users DROP COLUMN nickname;"]
if "postgres" in db_type:
scripts.extend(
[
(
"ALTER TABLE group_info_users ADD COLUMN IF NOT EXISTS "
"platform character varying(255);"
),
(
"ALTER TABLE group_info_users ALTER COLUMN user_id "
"TYPE character varying(255) USING user_id::character varying;"
),
(
"ALTER TABLE group_info_users ALTER COLUMN group_id "
"TYPE character varying(255) USING group_id::character varying;"
),
(
"ALTER TABLE group_info_users ALTER COLUMN platform "
"TYPE character varying(255) USING platform::character varying;"
),
]
)
elif "mysql" in db_type:
scripts.extend(
[
(
"ALTER TABLE group_info_users ADD COLUMN "
"platform VARCHAR(255) NULL;"
),
(
"ALTER TABLE group_info_users MODIFY COLUMN "
"user_id VARCHAR(255) NOT NULL;"
),
(
"ALTER TABLE group_info_users MODIFY COLUMN "
"group_id VARCHAR(255) NOT NULL;"
),
(
"ALTER TABLE group_info_users MODIFY COLUMN "
"platform VARCHAR(255) NULL;"
),
]
)
elif "sqlite" in db_type:
scripts.append(
"ALTER TABLE group_info_users ADD COLUMN platform VARCHAR(255);"
)
return scripts
return [
DropColumn("group_info_users", "nickname"),
AlterColumnType(
"group_info_users",
"user_id",
{"postgres": "character varying(255)", "mysql": "VARCHAR(255)"},
nullable=False,
),
AlterColumnType(
"group_info_users",
"group_id",
{"postgres": "character varying(255)", "mysql": "VARCHAR(255)"},
nullable=False,
),
]
+12 -4
View File
@@ -2,6 +2,7 @@ from tortoise import fields
from zhenxun.services.cache.runtime_cache import LevelUserMemoryCache
from zhenxun.services.db_context import Model
from zhenxun.services.db_context.schema_ops import AlterColumnType, RenameColumn
from zhenxun.utils.enum import CacheType
@@ -151,9 +152,16 @@ class LevelUser(Model):
async def _run_script(cls):
return [
# 将user_id改为user_id
"ALTER TABLE level_users RENAME COLUMN user_qq TO user_id;",
"ALTER TABLE level_users ALTER COLUMN user_id TYPE character varying(255);",
RenameColumn("level_users", "user_qq", "user_id"),
AlterColumnType(
"level_users",
"user_id",
{"postgres": "character varying(255)", "mysql": "VARCHAR(255)"},
),
# 将user_id字段类型改为character varying(255)
"ALTER TABLE level_users "
"ALTER COLUMN group_id TYPE character varying(255);",
AlterColumnType(
"level_users",
"group_id",
{"postgres": "character varying(255)", "mysql": "VARCHAR(255)"},
),
]
+1 -9
View File
@@ -206,12 +206,4 @@ class PluginInfo(Model):
@classmethod
async def _run_script(cls):
return [
"ALTER TABLE plugin_info ADD COLUMN parent character varying(255);",
"ALTER TABLE plugin_info ADD COLUMN is_show boolean DEFAULT true;",
"ALTER TABLE plugin_info ADD COLUMN ignore_prompt boolean DEFAULT false;",
"ALTER TABLE plugin_info ADD COLUMN impression float DEFAULT 0;",
"CREATE INDEX idx_plugin_info_module ON plugin_info(module);",
"ALTER TABLE plugin_info ADD COLUMN ignore_statistics"
" boolean DEFAULT false;",
]
return []
@@ -0,0 +1,39 @@
from typing import ClassVar
from tortoise import fields
from zhenxun.services.db_context import Model
class RuntimeBackpressureLog(Model):
id = fields.IntField(pk=True, generated=True, auto_increment=True)
"""自增id"""
scope_key = fields.CharField(255, null=True, description="作用域")
"""作用域"""
reason = fields.CharField(255, null=True, description="原因")
"""原因"""
lane = fields.CharField(64, null=True, description="调度通道")
"""调度通道"""
action = fields.CharField(64, description="处理动作")
"""处理动作 execute/skip/defer/signal"""
queue_size = fields.IntField(default=0, description="队列长度")
"""队列长度"""
active_count = fields.IntField(default=0, description="活跃数量")
"""活跃数量"""
duration_ms = fields.FloatField(default=0, description="持续耗时毫秒")
"""持续耗时毫秒"""
create_time = fields.DatetimeField(auto_now_add=True, description="创建时间")
"""创建时间"""
class Meta: # pyright: ignore [reportIncompatibleVariableOverride]
table = "runtime_backpressure_log"
table_description = "运行时背压追加审计日志"
indexes: ClassVar = [
("create_time",),
("scope_key", "create_time"),
("lane", "create_time"),
]
@classmethod
async def _run_script(cls):
return []
+12 -4
View File
@@ -3,6 +3,7 @@ from typing import ClassVar
from tortoise import fields
from zhenxun.services.db_context import Model
from zhenxun.services.db_context.schema_ops import AlterColumnType, RenameColumn
class Statistics(Model):
@@ -32,9 +33,16 @@ class Statistics(Model):
@classmethod
async def _run_script(cls):
return [
"ALTER TABLE statistics RENAME COLUMN user_qq TO user_id;",
RenameColumn("statistics", "user_qq", "user_id"),
# 将user_qq改为user_id
"ALTER TABLE statistics ALTER COLUMN user_id TYPE character varying(255);",
"ALTER TABLE statistics ALTER COLUMN group_id TYPE character varying(255);",
"ALTER TABLE statistics ADD bot_id Text DEFAULT '';",
AlterColumnType(
"statistics",
"user_id",
{"postgres": "character varying(255)", "mysql": "VARCHAR(255)"},
),
AlterColumnType(
"statistics",
"group_id",
{"postgres": "character varying(255)", "mysql": "VARCHAR(255)"},
),
]
+1 -5
View File
@@ -101,8 +101,4 @@ class TaskInfo(Model):
@classmethod
async def _run_script(cls):
return [
"ALTER TABLE task_info ADD default_status boolean DEFAULT true;",
"ALTER TABLE task_info ADD load_status boolean DEFAULT false;",
# 默认状态
]
return []
+84 -4
View File
@@ -1,8 +1,11 @@
import asyncio
from dataclasses import dataclass
from typing import ClassVar
from tortoise import fields
from tortoise.exceptions import IntegrityError
from tortoise.expressions import F
from tortoise.transactions import in_transaction
from zhenxun.models.goods_info import GoodsInfo
from zhenxun.services.buffered_writers import append_user_gold_log
@@ -11,6 +14,41 @@ from zhenxun.utils.enum import CacheType, GoldHandle
from zhenxun.utils.exception import GoodsNotFound, InsufficientGold
@dataclass(slots=True)
class GoldReservation:
user_id: str
gold: int
handle: GoldHandle
plugin_module: str
platform: str | None = None
committed: bool = False
released: bool = False
async def commit(self) -> None:
if self.committed or self.released:
return
await append_user_gold_log(
user_id=self.user_id,
gold=self.gold,
handle=self.handle,
source=self.plugin_module,
)
self.committed = True
async def release(self) -> None:
if self.released or self.committed:
return
self.released = True
async with in_transaction() as connection:
updated = (
await UserConsole.filter(user_id=self.user_id)
.using_db(connection)
.update(gold=F("gold") + self.gold)
)
if updated:
await UserConsole.invalidate_user_cache(self.user_id)
class UserConsole(Model):
id = fields.IntField(pk=True, generated=True, auto_increment=True)
"""自增id"""
@@ -150,6 +188,51 @@ class UserConsole(Model):
user_id=user_id, gold=gold, handle=handle, source=plugin_module
)
@classmethod
async def reserve_gold(
cls,
user_id: str,
gold: int,
handle: GoldHandle,
plugin_module: str,
platform: str | None = None,
) -> GoldReservation:
"""预扣金币;插件最终未执行时可 release 补偿。"""
async with in_transaction() as connection:
user = await cls.filter(user_id=user_id).using_db(connection).get_or_none()
if user is None:
try:
user = await cls.create(
using_db=connection,
user_id=user_id,
platform=platform,
uid=await cls.get_new_uid(),
)
except IntegrityError:
user = await cls.filter(user_id=user_id).using_db(connection).get()
if user.gold < gold:
raise InsufficientGold()
updated = (
await cls.filter(user_id=user_id, gold__gte=gold)
.using_db(connection)
.update(gold=F("gold") - gold)
)
if not updated:
raise InsufficientGold()
return GoldReservation(
user_id=user_id,
gold=gold,
handle=handle,
plugin_module=plugin_module,
platform=platform,
)
@classmethod
async def invalidate_user_cache(cls, user_id: str) -> None:
from zhenxun.services.cache import CacheRoot
await CacheRoot.invalidate_cache(CacheType.USERS, user_id)
@classmethod
async def add_props(
cls, user_id: str, goods_uuid: str, num: int = 1, platform: str | None = None
@@ -223,7 +306,4 @@ class UserConsole(Model):
@classmethod
async def _run_script(cls):
return [
"CREATE INDEX idx_user_console_user_id ON user_console(user_id);",
"CREATE INDEX idx_user_console_uid ON user_console(uid);",
]
return []