Files
abot/db/points_db.py
liuwei 955c2f2797 为积分通胀策略增加Redis日缓存
1. 新增群积分通胀统计的Redis缓存键与按天过期策略,减少重复聚合查询。

2. 新增带缓存的群插件积分消耗统计方法,Redis异常时自动回退实时查询。

3. 调整积分消耗注解优先读取缓存版统计,降低高频群聊场景下的数据库压力。
2026-04-27 13:40:27 +08:00

860 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
积分系统数据库操作类
"""
import json
from loguru import logger
from datetime import datetime, timedelta
from enum import Enum
from typing import Dict, List, Optional, Tuple, Any
from db.base import BaseDBOperator
from db.connection import DBConnectionManager
class PointSource(Enum):
"""积分来源枚举"""
CHECKIN = "checkin" # 签到
GAME = "game" # 游戏
ADMIN = "admin" # 管理员操作
TRADE = "trade" # 积分交易
PLUGIN = "plugin" # 插件使用
OTHER = "other" # 其他
class PointsDBOperator(BaseDBOperator):
"""积分系统数据库操作类"""
def __init__(self, db_manager=None):
"""初始化积分数据库操作类"""
super().__init__(db_manager or DBConnectionManager.get_instance())
self.LOG = logger
# 确保数据库表存在,后续不需要处理了。
# self._ensure_tables_exist()
def _ensure_tables_exist(self):
"""确保积分相关的数据库表存在"""
try:
# 创建用户积分表
self.execute_update("""
CREATE TABLE IF NOT EXISTS t_user_points (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
user_id VARCHAR(100) NOT NULL,
group_id VARCHAR(100) NOT NULL,
total_points INTEGER DEFAULT 0,
checkin_points INTEGER DEFAULT 0,
game_points INTEGER DEFAULT 0,
other_points INTEGER DEFAULT 0,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE(user_id, group_id)
) ENGINE=InnoDB CHARACTER SET utf8mb4;
""")
# 创建积分交易记录表
self.execute_update("""
CREATE TABLE IF NOT EXISTS t_point_transactions (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
user_id VARCHAR(100) NOT NULL,
group_id VARCHAR(100) NOT NULL,
transaction_type VARCHAR(20) NOT NULL,
points INTEGER NOT NULL,
source VARCHAR(50) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB CHARACTER SET utf8mb4;
""")
# 创建功能插件积分配置表
self.execute_update("""
CREATE TABLE IF NOT EXISTS t_plugin_point_config (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
plugin_name VARCHAR(100) NOT NULL,
points_required INTEGER DEFAULT 0,
is_enabled BOOLEAN DEFAULT TRUE,
description TEXT,
UNIQUE(plugin_name)
) ENGINE=InnoDB CHARACTER SET utf8mb4;
""")
# 创建关禁闭记录表
self.execute_update("""
CREATE TABLE IF NOT EXISTS t_prison_records (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
user_id VARCHAR(100) NOT NULL,
group_id VARCHAR(100) NOT NULL,
start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
end_time TIMESTAMP NOT NULL,
reason VARCHAR(255),
status TINYINT DEFAULT 1 COMMENT '1:在押 0:已释放',
bailout_user_id VARCHAR(100),
bailout_time TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `idx_user_group` (user_id, group_id, status)
) ENGINE=InnoDB CHARACTER SET utf8mb4;
""")
self.LOG.info("积分系统数据库表检查/创建完成")
except Exception as e:
self.LOG.error(f"创建积分系统数据库表失败: {e}")
raise
def _build_group_plugin_consumption_cache_key(self, group_id: str, lookback_hours: int) -> str:
"""
构造群聊插件积分消耗统计的 Redis 缓存键。
这里按“天维度”缓存,原因有两点:
1. 用户明确希望不要每次都实时计算;
2. 通胀策略的目标是压缩群积分长期沉淀,对秒级实时性要求不高,
因此按天缓存可以显著减少数据库聚合压力,同时保持策略方向稳定。
Key 里额外带上 `lookback_hours`,是为了避免后续不同窗口参数互相污染缓存。
"""
stat_day = datetime.now().strftime("%Y%m%d")
normalized_group_id = str(group_id or "private")
normalized_hours = max(1, int(lookback_hours or 72))
return f"bot:points:inflation:group_stats:{normalized_group_id}:{stat_day}:{normalized_hours}"
@staticmethod
def _seconds_until_next_day() -> int:
"""
计算距离次日零点还有多少秒。
缓存采用“自然日切换”:
- 当天第一次命中后,后续同一天直接复用;
- 到了第二天自动失效,重新基于新一天的数据重算。
"""
now = datetime.now()
tomorrow = (now + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
return max(60, int((tomorrow - now).total_seconds()))
def get_user_points(self, user_id: str, group_id: str) -> Dict:
"""
获取用户积分信息
Args:
user_id: 用户ID
group_id: 群组ID
Returns:
包含用户积分信息的字典
"""
try:
# 先尝试从新表获取
result = self.execute_query("""
SELECT * FROM t_user_points
WHERE user_id = %s AND group_id = %s
""", (user_id, group_id), fetch_one=True)
if result:
return result
# 如果新表没有数据,尝试从旧表迁移数据
return self._migrate_user_points(user_id, group_id)
except Exception as e:
self.LOG.error(f"获取用户积分失败: {e}")
return {
"user_id": user_id,
"group_id": group_id,
"total_points": 0,
"checkin_points": 0,
"game_points": 0,
"other_points": 0
}
def _migrate_user_points(self, user_id: str, group_id: str) -> Dict:
"""
从旧表迁移用户积分数据到新表
Args:
user_id: 用户ID
group_id: 群组ID
Returns:
包含用户积分信息的字典
"""
result = {
"user_id": user_id,
"group_id": group_id,
"total_points": 0,
"checkin_points": 0,
"game_points": 0,
"other_points": 0
}
try:
# 查询签到积分
sign_result = self.execute_query("""
SELECT points FROM t_sign_record
WHERE wx_id = %s AND group_id = %s
""", (user_id, group_id), fetch_one=True)
# 查询游戏积分
game_result = self.execute_query("""
SELECT points FROM t_encyclopedia_players
WHERE player_id = %s AND group_id = %s
""", (user_id, group_id), fetch_one=True)
# 合并积分
checkin_points = sign_result["points"] if sign_result else 0
game_points = game_result["points"] if game_result else 0
total_points = checkin_points + game_points
# 更新结果
result["checkin_points"] = checkin_points
result["game_points"] = game_points
result["total_points"] = total_points
# 插入到新表
self.execute_update("""
INSERT INTO t_user_points
(user_id, group_id, total_points, checkin_points, game_points, other_points)
VALUES (%s, %s, %s, %s, %s, %s)
""", (user_id, group_id, total_points, checkin_points, game_points, 0))
# 获取插入后的完整记录
return self.execute_query("""
SELECT * FROM t_user_points
WHERE user_id = %s AND group_id = %s
""", (user_id, group_id), fetch_one=True) or result
except Exception as e:
self.LOG.error(f"迁移用户积分失败: {e}")
return result
def add_points(self, user_id: str, group_id: str, points: int,
source: PointSource, description: str = None,
user_name: str = None) -> Tuple[bool, Dict]:
"""
增加用户积分
Args:
user_id: 用户ID
group_id: 群组ID
points: 积分数量
source: 积分来源
description: 描述
user_name: 用户名称
Returns:
(成功标志, 用户积分信息)
"""
if points <= 0:
return False, {"error": "积分必须为正数"}
try:
# 检查用户是否存在
user_exists = self.execute_query("""
SELECT * FROM t_user_points
WHERE user_id = %s AND group_id = %s
""", (user_id, group_id), fetch_one=True)
if not user_exists:
# 如果用户不存在,先迁移或创建用户
self._migrate_user_points(user_id, group_id)
# 更新积分
source_field = f"{source.value}_points" if source.value in ["checkin", "game"] else "other_points"
self.execute_update(f"""
UPDATE t_user_points
SET total_points = total_points + %s,
{source_field} = {source_field} + %s
WHERE user_id = %s AND group_id = %s
""", (points, points, user_id, group_id))
# 记录交易
self.execute_update("""
INSERT INTO t_point_transactions
(user_id, group_id, transaction_type, points, source, description)
VALUES (%s, %s, %s, %s, %s, %s)
""", (user_id, group_id, "earn", points, source.value, description))
# 同时更新旧表,保持兼容
if source == PointSource.CHECKIN:
self.execute_update("""
UPDATE t_sign_record
SET points = points + %s
WHERE wx_id = %s AND group_id = %s
""", (points, user_id, group_id))
elif source == PointSource.GAME:
self.execute_update("""
UPDATE t_encyclopedia_players
SET points = points + %s
WHERE player_id = %s AND group_id = %s
""", (points, user_id, group_id))
# 获取更新后的积分信息
updated_points = self.execute_query("""
SELECT * FROM t_user_points
WHERE user_id = %s AND group_id = %s
""", (user_id, group_id), fetch_one=True)
return True, updated_points
except Exception as e:
self.LOG.error(f"增加用户积分失败: {e}")
return False, {"error": str(e)}
def deduct_points(self, user_id: str, group_id: str, points: int,
source: PointSource, description: str = None) -> Tuple[bool, Dict]:
"""
扣除用户积分
Args:
user_id: 用户ID
group_id: 群组ID
points: 积分数量
source: 积分来源
description: 描述
Returns:
(成功标志, 用户积分信息)
"""
if points <= 0:
return False, {"error": "积分必须为正数"}
try:
# 检查用户是否存在及积分是否足够
user_points = self.execute_query("""
SELECT * FROM t_user_points
WHERE user_id = %s AND group_id = %s
""", (user_id, group_id), fetch_one=True)
if not user_points:
# 如果用户不存在,先迁移或创建用户
user_points = self._migrate_user_points(user_id, group_id)
if user_points["total_points"] < points:
return False, {"error": "积分不足", "current_points": user_points["total_points"]}
# 更新积分
self.execute_update("""
UPDATE t_user_points
SET total_points = total_points - %s
WHERE user_id = %s AND group_id = %s
""", (points, user_id, group_id))
# 记录交易
self.execute_update("""
INSERT INTO t_point_transactions
(user_id, group_id, transaction_type, points, source, description)
VALUES (%s, %s, %s, %s, %s, %s)
""", (user_id, group_id, "spend", -points, source.value, description))
# 获取更新后的积分信息
updated_points = self.execute_query("""
SELECT * FROM t_user_points
WHERE user_id = %s AND group_id = %s
""", (user_id, group_id), fetch_one=True)
return True, updated_points
except Exception as e:
self.LOG.error(f"扣除用户积分失败: {e}")
return False, {"error": str(e)}
def transfer_points(
self,
from_user_id: str,
to_user_id: str,
group_id: str,
points: int,
description: str = None,
from_user_name: str = None,
to_user_name: str = None,
) -> Tuple[bool, Dict]:
"""
转移积分从一个用户到另一个用户
Args:
from_user_id: 转出用户ID
to_user_id: 转入用户ID
group_id: 群组ID
points: 积分数量
description: 描述
from_user_name: 转出用户展示名(可选,优先用于流水描述)
to_user_name: 转入用户展示名(可选,优先用于流水描述)
Returns:
(成功标志, 结果信息)
"""
if points <= 0:
return False, {"error": "积分必须为正数"}
try:
# 流水描述统一使用“昵称优先ID兜底”策略
# 1. 业务层可显式传入昵称;
# 2. 未传时回落到 user_id确保兼容旧调用方。
from_display_name = str(from_user_name or from_user_id)
to_display_name = str(to_user_name or to_user_id)
# 先扣除转出用户积分
success, result = self.deduct_points(
from_user_id, group_id, points,
PointSource.TRADE, f"转账给用户 {to_display_name}: {description}"
)
if not success:
return False, result
# 再增加转入用户积分
success, to_result = self.add_points(
to_user_id, group_id, points,
PointSource.TRADE, f"收到用户 {from_display_name} 的转账: {description}"
)
if not success:
# 如果增加失败,回滚扣除操作
self.add_points(
from_user_id, group_id, points,
PointSource.TRADE, f"转账失败退回: {description}",
)
return False, to_result
return True, {
"from_user": result,
"to_user": to_result
}
except Exception as e:
self.LOG.error(f"转移用户积分失败: {e}")
return False, {"error": str(e)}
def get_user_transactions(self, user_id: str, group_id: str, limit: int = 10) -> List[Dict]:
"""
获取用户积分交易记录
Args:
user_id: 用户ID
group_id: 群组ID
limit: 记录数量限制
Returns:
交易记录列表
"""
try:
return self.execute_query("""
SELECT * FROM t_point_transactions
WHERE user_id = %s AND group_id = %s
ORDER BY created_at DESC
LIMIT %s
""", (user_id, group_id, limit))
except Exception as e:
self.LOG.error(f"获取用户交易记录失败: {e}")
return []
def get_points_ranking(self, group_id: str, limit: int = 10) -> List[Dict]:
"""
获取群组积分排行榜
Args:
group_id: 群组ID
limit: 记录数量限制
Returns:
排行榜列表
"""
try:
return self.execute_query("""
SELECT user_id, total_points, checkin_points, game_points, other_points
FROM t_user_points
WHERE group_id = %s
AND user_id !='SYSTEM'
ORDER BY total_points DESC
LIMIT %s
""", (group_id, limit))
except Exception as e:
self.LOG.error(f"获取积分排行榜失败: {e}")
return []
def get_plugin_config(self, plugin_name: str) -> Optional[Dict]:
"""
获取插件积分配置
Args:
plugin_name: 插件名称
Returns:
插件积分配置
"""
try:
return self.execute_query("""
SELECT * FROM t_plugin_point_config
WHERE plugin_name = %s
""", (plugin_name,), fetch_one=True)
except Exception as e:
self.LOG.error(f"获取插件积分配置失败: {e}")
return None
def set_plugin_config(self, plugin_name: str, points_required: int,
is_enabled: bool = True, description: str = None) -> bool:
"""
设置插件积分配置
Args:
plugin_name: 插件名称
points_required: 所需积分
is_enabled: 是否启用
description: 描述
Returns:
是否成功
"""
try:
self.execute_update("""
INSERT INTO t_plugin_point_config
(plugin_name, points_required, is_enabled, description)
VALUES (%s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
points_required = VALUES(points_required),
is_enabled = VALUES(is_enabled),
description = VALUES(description)
""", (plugin_name, points_required, is_enabled, description))
return True
except Exception as e:
self.LOG.error(f"设置插件积分配置失败: {e}")
return False
def get_all_plugin_configs(self) -> List[Dict]:
"""
获取所有插件积分配置
Returns:
所有插件积分配置
"""
try:
return self.execute_query("SELECT * FROM t_plugin_point_config")
except Exception as e:
self.LOG.error(f"获取所有插件积分配置失败: {e}")
return []
def check_plugin_points(self, user_id: str, group_id: str, plugin_name: str) -> Tuple[bool, Dict]:
"""
检查用户是否有足够积分使用插件
Args:
user_id: 用户ID
group_id: 群组ID
plugin_name: 插件名称
Returns:
(是否有足够积分, 结果信息)
"""
try:
# 获取插件积分配置
plugin_config = self.get_plugin_config(plugin_name)
# 如果插件未配置或未启用积分限制,直接返回成功
if not plugin_config or not plugin_config["is_enabled"]:
return True, {"message": "插件未配置积分限制"}
# 获取用户积分
user_points = self.get_user_points(user_id, group_id)
# 检查积分是否足够
if user_points["total_points"] < plugin_config["points_required"]:
return False, {
"error": "积分不足",
"current_points": user_points["total_points"],
"required_points": plugin_config["points_required"]
}
return True, {
"message": "积分充足",
"current_points": user_points["total_points"],
"required_points": plugin_config["points_required"]
}
except Exception as e:
self.LOG.error(f"检查插件积分失败: {e}")
return True, {"error": str(e)} # 出错时默认允许使用
def use_plugin(self, user_id: str, group_id: str, plugin_name: str) -> Tuple[bool, Dict]:
"""
使用插件并扣除积分
Args:
user_id: 用户ID
group_id: 群组ID
plugin_name: 插件名称
Returns:
(是否成功, 结果信息)
"""
try:
# 先检查积分是否足够
can_use, result = self.check_plugin_points(user_id, group_id, plugin_name)
if not can_use:
return False, result
# 如果插件未配置积分或不需要扣除积分,直接返回成功
if "required_points" not in result or result["required_points"] <= 0:
return True, {"message": "无需扣除积分"}
# 扣除积分
return self.deduct_points(
user_id, group_id, result["required_points"],
PointSource.PLUGIN, f"使用插件: {plugin_name}"
)
except Exception as e:
self.LOG.error(f"使用插件扣除积分失败: {e}")
return False, {"error": str(e)}
def get_user_points_stats(self, group_id: str) -> Dict[str, Any]:
"""
获取群组积分统计信息
Args:
group_id: 群组ID
Returns:
统计信息
"""
stats = {
"total_users": 0,
"total_points": 0,
"avg_points": 0,
"max_points": 0,
"min_points": 0,
"checkin_points_total": 0,
"game_points_total": 0,
"other_points_total": 0
}
try:
result = self.execute_query("""
SELECT
COUNT(*) as total_users,
SUM(total_points) as total_points,
AVG(total_points) as avg_points,
MAX(total_points) as max_points,
MIN(total_points) as min_points,
SUM(checkin_points) as checkin_points_total,
SUM(game_points) as game_points_total,
SUM(other_points) as other_points_total
FROM t_user_points
WHERE group_id = %s
""", (group_id,), fetch_one=True)
if result:
stats.update({k: v or 0 for k, v in result.items()})
return stats
except Exception as e:
self.LOG.error(f"获取群组积分统计信息失败: {e}")
return stats
def get_group_plugin_consumption_stats(self, group_id: str, lookback_hours: int = 72) -> Dict[str, Any]:
"""
获取群聊插件积分消耗统计信息。
这里专门为“积分通货膨胀”策略提供数据支撑,只统计真正会销毁积分总量的插件扣费:
1. 用户之间转账、打劫、保释这类行为,本质上只是积分在用户之间流转,不算真实消耗;
2. `plugin` 来源的 `spend` 流水,才代表群积分池被真正消耗掉了;
3. 因此通胀策略应基于“当前群积分存量”与“最近一段时间插件真实消耗量”的关系来判断。
Args:
group_id: 群ID
lookback_hours: 统计最近多少小时的插件消耗
Returns:
包含群积分存量、系统沉淀积分、最近插件消耗量等字段的统计字典
"""
normalized_hours = max(1, int(lookback_hours or 72))
stats = {
"group_id": group_id,
"lookback_hours": normalized_hours,
"total_users": 0,
"active_points_total": 0,
"system_points_total": 0,
"plugin_spend_points": 0,
"plugin_spend_count": 0,
"plugin_spend_ratio": 0.0,
}
if not group_id:
return stats
try:
# 用户积分存量统计中要把 SYSTEM 单独拆出来:
# SYSTEM 账户通常用于承接抽水、保释金等沉淀积分,
# 这些积分已经不在普通成员手里流通,所以需要单独记录,避免干扰真实“群成员持有积分”。
points_result = self.execute_query(
"""
SELECT
SUM(CASE WHEN user_id <> 'SYSTEM' THEN 1 ELSE 0 END) AS total_users,
SUM(CASE WHEN user_id <> 'SYSTEM' THEN total_points ELSE 0 END) AS active_points_total,
SUM(CASE WHEN user_id = 'SYSTEM' THEN total_points ELSE 0 END) AS system_points_total
FROM t_user_points
WHERE group_id = %s
""",
(group_id,),
fetch_one=True,
) or {}
# 最近插件消耗采用时间窗口统计。
# 只看 plugin 来源的 spend能更准确反映“带注解功能”对积分池的真实消耗效率。
time_boundary = datetime.now() - timedelta(hours=normalized_hours)
spend_result = self.execute_query(
"""
SELECT
SUM(CASE WHEN transaction_type = 'spend' AND source = %s THEN ABS(points) ELSE 0 END)
AS plugin_spend_points,
SUM(CASE WHEN transaction_type = 'spend' AND source = %s THEN 1 ELSE 0 END)
AS plugin_spend_count
FROM t_point_transactions
WHERE group_id = %s
AND created_at >= %s
""",
(PointSource.PLUGIN.value, PointSource.PLUGIN.value, group_id, time_boundary),
fetch_one=True,
) or {}
stats["total_users"] = int(points_result.get("total_users") or 0)
stats["active_points_total"] = int(points_result.get("active_points_total") or 0)
stats["system_points_total"] = int(points_result.get("system_points_total") or 0)
stats["plugin_spend_points"] = int(spend_result.get("plugin_spend_points") or 0)
stats["plugin_spend_count"] = int(spend_result.get("plugin_spend_count") or 0)
active_points_total = stats["active_points_total"]
if active_points_total > 0:
stats["plugin_spend_ratio"] = round(
stats["plugin_spend_points"] / active_points_total,
6,
)
return stats
except Exception as e:
self.LOG.error(f"获取群聊插件积分消耗统计失败: {e}")
return stats
def get_group_plugin_consumption_stats_cached(self, group_id: str, lookback_hours: int = 72) -> Dict[str, Any]:
"""
获取带 Redis 日缓存的群聊插件积分消耗统计。
设计目标:
1. 避免每次插件扣费前都扫描积分表和流水表;
2. 使用“群 + 日期 + 回看窗口”的 Redis 键,把同一天的同群统计复用起来;
3. Redis 不可用时自动降级为实时查询,保证功能可用性优先。
Args:
group_id: 群ID
lookback_hours: 统计最近多少小时的插件消耗
Returns:
群积分通胀计算所需的统计字典
"""
normalized_hours = max(1, int(lookback_hours or 72))
if not group_id:
return self.get_group_plugin_consumption_stats(group_id, normalized_hours)
cache_key = self._build_group_plugin_consumption_cache_key(group_id, normalized_hours)
try:
# 先读 Redis 日缓存。
# 同一天同一群通常会多次触发同类功能,这一步能把热点 SQL 压掉。
with self.db_manager.get_redis_connection() as redis_client:
cached_payload = redis_client.get(cache_key)
if cached_payload:
if isinstance(cached_payload, bytes):
cached_payload = cached_payload.decode("utf-8")
cached_stats = json.loads(cached_payload)
if isinstance(cached_stats, dict):
return cached_stats
except Exception as e:
# 缓存失败不影响主流程,直接回退实时统计。
self.LOG.warning(f"读取群积分通胀缓存失败,回退实时查询: {e}")
stats = self.get_group_plugin_consumption_stats(group_id, normalized_hours)
try:
with self.db_manager.get_redis_connection() as redis_client:
# 额外多保留 5 分钟缓冲,避免 00:00 刚过时多个请求同时击穿。
redis_client.set(
cache_key,
json.dumps(stats, ensure_ascii=False),
ex=self._seconds_until_next_day() + 300,
)
except Exception as e:
self.LOG.warning(f"写入群积分通胀缓存失败: {e}")
return stats
def imprison_user(self, user_id: str, group_id: str, hours: int = 24, reason: str = None) -> bool:
"""关押用户
Args:
user_id: 用户ID
group_id: 群组ID
hours: 关押时长(小时)
reason: 关押原因
"""
try:
end_time = datetime.now() + timedelta(hours=hours)
self.execute_update("""
INSERT INTO t_prison_records (user_id, group_id, end_time, reason)
VALUES (%s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
start_time=CURRENT_TIMESTAMP, end_time=%s, reason=%s, status=1
""", (user_id, group_id, end_time, reason, end_time, reason))
return True
except Exception as e:
self.LOG.error(f"关押用户失败: {e}")
return False
def check_prison_status(self, user_id: str, group_id: str) -> Optional[Dict]:
"""检查用户是否在押
Returns:
None: 不在押
Dict: 在押信息
"""
try:
records = self.execute_query("""
SELECT * FROM t_prison_records
WHERE user_id = %s AND group_id = %s AND status = 1
AND end_time > CURRENT_TIMESTAMP
LIMIT 1
""", (user_id, group_id))
return records[0] if records else None
except Exception as e:
self.LOG.error(f"检查用户在押状态失败: {e}")
return None
def bailout_user(
self,
prisoner_id: str,
bailout_user_id: str,
group_id: str,
prisoner_name: str = None,
bailout_user_name: str = None,
) -> Tuple[bool, str]:
"""保释用户
Returns:
(bool, str): (是否成功, 错误信息)
"""
try:
# 检查是否在押
prison_record = self.check_prison_status(prisoner_id, group_id)
if not prison_record:
return False, "该用户未被关押"
# 扣除保释金
success, result = self.transfer_points(
bailout_user_id,
"SYSTEM",
group_id,
30,
f"{str(prisoner_name or prisoner_id)}支付保释金",
from_user_name=bailout_user_name,
to_user_name="系统",
)
if not success:
return False, result.get("error", "保释失败")
# 释放用户
self.execute_update("""
UPDATE t_prison_records
SET status = 0, bailout_user_id = %s, bailout_time = CURRENT_TIMESTAMP
WHERE user_id = %s AND group_id = %s AND status = 1
""", (bailout_user_id, prisoner_id, group_id))
return True, "保释成功"
except Exception as e:
self.LOG.error(f"保释用户失败: {e}")
return False, f"保释失败: {str(e)}"