新增群积分通货膨胀策略
1. 新增群聊插件积分真实消耗统计,区分成员持有积分与 SYSTEM 沉淀积分。 2. 为 plugin_points_cost 注解接入按群动态倍率,积分消耗偏低时自动提高实际扣费。 3. 优化积分不足与扣费成功提示,展示基础价、实际价与当前通胀倍率。
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import functools
|
||||
import math
|
||||
import time
|
||||
import traceback
|
||||
import asyncio
|
||||
@@ -12,6 +13,138 @@ from utils.robot_cmd.robot_command import Feature, PermissionStatus, GroupBotMan
|
||||
from wechat_ipad import WechatAPIClient
|
||||
|
||||
|
||||
def _read_points_inflation_config(config: Dict[str, Any], key: str, default: Any) -> Any:
|
||||
"""
|
||||
兼容读取通胀配置。
|
||||
|
||||
之所以做这一层兼容,是因为插件 TOML 里常见两种写法:
|
||||
1. `lookback_hours = 72`
|
||||
2. `lookback-hours = 72`
|
||||
统一兼容后,后续新增配置就不用担心命名风格差异。
|
||||
"""
|
||||
if not isinstance(config, dict):
|
||||
return default
|
||||
return config.get(key, config.get(key.replace("_", "-"), default))
|
||||
|
||||
|
||||
def _resolve_points_cost_profile(plugin_instance: Any, message: Dict[str, Any], base_points: int) -> Dict[str, Any]:
|
||||
"""
|
||||
解析本次积分消费的实际扣费画像。
|
||||
|
||||
默认情况下,注解的扣费是固定值;这里加入群聊级“通货膨胀”策略后,
|
||||
会根据群里当前积分存量和最近插件消耗效率,动态算出本次实际扣费。
|
||||
|
||||
返回值说明:
|
||||
- `base_points`: 注解原始价格
|
||||
- `actual_points`: 本次实际扣费
|
||||
- `multiplier`: 通胀倍率
|
||||
- `inflated`: 是否触发了通胀涨价
|
||||
"""
|
||||
roomid = message.get("roomid", "")
|
||||
profile = {
|
||||
"base_points": int(base_points),
|
||||
"actual_points": int(base_points),
|
||||
"multiplier": 1.0,
|
||||
"inflated": False,
|
||||
"lookback_hours": 0,
|
||||
"spend_ratio": 0.0,
|
||||
"active_points_total": 0,
|
||||
"plugin_spend_points": 0,
|
||||
"plugin_spend_count": 0,
|
||||
}
|
||||
|
||||
# 用户需求明确指向“群里积分消耗不掉了”,因此这里只对群聊启用通胀。
|
||||
if not roomid:
|
||||
return profile
|
||||
|
||||
plugin_config = getattr(plugin_instance, "_config", {}) or {}
|
||||
inflation_config = plugin_config.get("PointsCostInflation", {}) or {}
|
||||
if not _read_points_inflation_config(inflation_config, "enabled", True):
|
||||
return profile
|
||||
|
||||
# 下面这些默认值尽量保守:
|
||||
# 1. 只有群积分存量和群成员数达到一定规模,才说明积分开始“堆积”;
|
||||
# 2. 目标消耗比、步进倍率、最大倍率都做了封顶,避免涨价过于激进。
|
||||
lookback_hours = max(1, int(_read_points_inflation_config(inflation_config, "lookback_hours", 72) or 72))
|
||||
min_group_users = max(1, int(_read_points_inflation_config(inflation_config, "min_group_users", 5) or 5))
|
||||
min_group_points = max(0, int(_read_points_inflation_config(inflation_config, "min_group_points", 1000) or 1000))
|
||||
target_spend_ratio = float(_read_points_inflation_config(inflation_config, "target_spend_ratio", 0.08) or 0.08)
|
||||
ratio_per_step = float(_read_points_inflation_config(inflation_config, "ratio_per_step", 0.02) or 0.02)
|
||||
multiplier_step = float(_read_points_inflation_config(inflation_config, "multiplier_step", 0.5) or 0.5)
|
||||
max_multiplier = float(_read_points_inflation_config(inflation_config, "max_multiplier", 3.0) or 3.0)
|
||||
|
||||
try:
|
||||
db_manager = DBConnectionManager.get_instance()
|
||||
points_db = PointsDBOperator(db_manager)
|
||||
stats = points_db.get_group_plugin_consumption_stats(roomid, lookback_hours)
|
||||
|
||||
profile["lookback_hours"] = lookback_hours
|
||||
profile["spend_ratio"] = float(stats.get("plugin_spend_ratio") or 0.0)
|
||||
profile["active_points_total"] = int(stats.get("active_points_total") or 0)
|
||||
profile["plugin_spend_points"] = int(stats.get("plugin_spend_points") or 0)
|
||||
profile["plugin_spend_count"] = int(stats.get("plugin_spend_count") or 0)
|
||||
|
||||
# 群体规模太小,或者群里本身没什么积分时,没必要触发通胀。
|
||||
if int(stats.get("total_users") or 0) < min_group_users:
|
||||
return profile
|
||||
if profile["active_points_total"] < min_group_points:
|
||||
return profile
|
||||
if target_spend_ratio <= 0:
|
||||
return profile
|
||||
|
||||
spend_ratio = profile["spend_ratio"]
|
||||
if spend_ratio >= target_spend_ratio:
|
||||
return profile
|
||||
|
||||
# 根据“离目标消耗比差多少”来提升倍率。
|
||||
# 例子:
|
||||
# - 目标 8%,当前只有 1%,说明群积分沉淀较严重;
|
||||
# - 每差 2% 提高 0.5 倍,直到达到封顶倍率。
|
||||
safe_ratio_per_step = max(ratio_per_step, 0.0001)
|
||||
gap_ratio = max(0.0, target_spend_ratio - spend_ratio)
|
||||
step_count = max(1, math.ceil(gap_ratio / safe_ratio_per_step))
|
||||
multiplier = min(max_multiplier, 1.0 + step_count * multiplier_step)
|
||||
actual_points = max(int(base_points), math.ceil(int(base_points) * multiplier))
|
||||
|
||||
profile["actual_points"] = actual_points
|
||||
profile["multiplier"] = round(multiplier, 2)
|
||||
profile["inflated"] = actual_points > int(base_points)
|
||||
|
||||
if profile["inflated"]:
|
||||
plugin_name = getattr(plugin_instance, "name", "未知插件")
|
||||
logger.info(
|
||||
f"群 {roomid} 触发积分通胀: plugin={plugin_name}, "
|
||||
f"base={base_points}, actual={actual_points}, "
|
||||
f"ratio={spend_ratio:.4f}, multiplier={profile['multiplier']:.2f}"
|
||||
)
|
||||
return profile
|
||||
except Exception as e:
|
||||
# 通胀策略属于增强能力,不能因为统计异常影响原有功能可用性。
|
||||
logger.warning(f"解析积分通胀倍率失败,回退固定扣费: {e}")
|
||||
logger.warning(traceback.format_exc())
|
||||
return profile
|
||||
|
||||
|
||||
def _build_points_cost_text(profile: Dict[str, Any]) -> str:
|
||||
"""
|
||||
生成扣费说明文案。
|
||||
|
||||
统一文案可以让“积分不足提示”和“扣费成功提示”保持一致,
|
||||
用户更容易理解为什么这次扣费不是固定值。
|
||||
"""
|
||||
if not profile.get("inflated"):
|
||||
return f"💰消费 {profile['actual_points']} 积分"
|
||||
|
||||
multiplier = float(profile.get("multiplier") or 1.0)
|
||||
ratio = float(profile.get("spend_ratio") or 0.0) * 100
|
||||
return (
|
||||
f"💰消费 {profile['actual_points']} 积分\n"
|
||||
f"📈 群通胀倍率: x{multiplier:.2f}\n"
|
||||
f"🧾 基础价: {profile['base_points']} | 实际价: {profile['actual_points']}\n"
|
||||
f"📉 近{profile['lookback_hours']}小时插件消耗占比: {ratio:.2f}%"
|
||||
)
|
||||
|
||||
|
||||
def points_reward_decorator(points_calculator: Union[int, Callable], source_type: str = "other",
|
||||
description: str = None, feature_key: str = None):
|
||||
"""积分奖励装饰器
|
||||
@@ -183,7 +316,7 @@ def plugin_points_cost(points: int, description: str = None, feature_key: str =
|
||||
"""插件积分消费装饰器
|
||||
|
||||
Args:
|
||||
points: 消费积分数量
|
||||
points: 基础消费积分数量
|
||||
description: 积分消费描述
|
||||
feature_key: 功能权限键名
|
||||
|
||||
@@ -224,14 +357,24 @@ def plugin_points_cost(points: int, description: str = None, feature_key: str =
|
||||
|
||||
plugin_name = self.name if hasattr(self, 'name') else "未知插件"
|
||||
logger.info(f"PointsCost.{plugin_name}")
|
||||
cost_profile = _resolve_points_cost_profile(self, message, points)
|
||||
actual_cost_points = int(cost_profile["actual_points"])
|
||||
|
||||
user_points = points_db.get_user_points(sender, roomid)
|
||||
if user_points["total_points"] < points:
|
||||
if user_points["total_points"] < actual_cost_points:
|
||||
# 积分不足
|
||||
shortage = actual_cost_points - user_points["total_points"]
|
||||
inflation_tip = ""
|
||||
if cost_profile["inflated"]:
|
||||
inflation_tip = (
|
||||
f"\n📈 当前群通胀倍率: x{cost_profile['multiplier']:.2f}"
|
||||
f"\n🧾 基础价: {cost_profile['base_points']} | 实际价: {actual_cost_points}"
|
||||
)
|
||||
await bot.send_at_message((roomid if roomid else sender),
|
||||
f"❌ 积分不足\n"
|
||||
f"🪙 先参与积分活动[签到]赚取吧!\n"
|
||||
f"💰 有: {user_points['total_points']} | 需: {points} |差: {points - user_points['total_points']} ",
|
||||
f"💰 有: {user_points['total_points']} | 需: {actual_cost_points} | 差: {shortage}"
|
||||
f"{inflation_tip}",
|
||||
[sender]
|
||||
)
|
||||
logger.info(f"用户 {sender} 积分不足,无法使用功能")
|
||||
@@ -243,20 +386,20 @@ def plugin_points_cost(points: int, description: str = None, feature_key: str =
|
||||
# 如果原始方法执行成功,扣除积分
|
||||
if success:
|
||||
deduct_success, deduct_result = points_db.deduct_points(
|
||||
sender, roomid, points, PointSource.PLUGIN,
|
||||
sender, roomid, actual_cost_points, PointSource.PLUGIN,
|
||||
description or f"使用 {plugin_name} 功能"
|
||||
)
|
||||
|
||||
if deduct_success:
|
||||
logger.info(f"用户 {sender} 使用 {plugin_name} 功能扣除 {points} 积分")
|
||||
logger.info(f"用户 {sender} 使用 {plugin_name} 功能扣除 {actual_cost_points} 积分")
|
||||
|
||||
# 添加对 response 的类型检查
|
||||
if isinstance(response, str) and "积分" not in response:
|
||||
response += f"\n\n💰 已消费 {points} 积分"
|
||||
response += f"\n\n{_build_points_cost_text(cost_profile)}"
|
||||
|
||||
client_msg_id, create_time, new_msg_id = await bot.send_at_message(
|
||||
(roomid if roomid else sender),
|
||||
f"💰消费 {points} 积分", [sender]
|
||||
_build_points_cost_text(cost_profile), [sender]
|
||||
)
|
||||
revoke.add_message_to_revoke(roomid, client_msg_id, create_time, new_msg_id, 5)
|
||||
|
||||
@@ -300,14 +443,23 @@ def plugin_points_cost(points: int, description: str = None, feature_key: str =
|
||||
|
||||
plugin_name = self.name if hasattr(self, 'name') else "未知插件"
|
||||
logger.info(f"PointsCost.{plugin_name}")
|
||||
cost_profile = _resolve_points_cost_profile(self, message, points)
|
||||
actual_cost_points = int(cost_profile["actual_points"])
|
||||
|
||||
user_points = points_db.get_user_points(sender, roomid)
|
||||
if user_points["total_points"] < points:
|
||||
if user_points["total_points"] < actual_cost_points:
|
||||
# 积分不足
|
||||
inflation_tip = ""
|
||||
if cost_profile["inflated"]:
|
||||
inflation_tip = (
|
||||
f"\n📈 当前群通胀倍率: x{cost_profile['multiplier']:.2f}"
|
||||
f"\n🧾 基础价: {cost_profile['base_points']} | 实际价: {actual_cost_points}"
|
||||
)
|
||||
self.message_util.send_text(
|
||||
f"❌ 积分不足\n无法使用 {plugin_name} 功能\n"
|
||||
f"🪙 先参与积分活动[签到,答题/t]赚取吧!\n"
|
||||
f"💰 有: {user_points['total_points']} | 需: {points} |差: {points - user_points['total_points']} ",
|
||||
f"💰 有: {user_points['total_points']} | 需: {actual_cost_points} "
|
||||
f"| 差: {actual_cost_points - user_points['total_points']}{inflation_tip}",
|
||||
(roomid if roomid else sender), sender
|
||||
)
|
||||
logger.info(f"用户 {sender} 积分不足,无法使用功能")
|
||||
@@ -319,18 +471,18 @@ def plugin_points_cost(points: int, description: str = None, feature_key: str =
|
||||
# 如果原始方法执行成功,扣除积分
|
||||
if success:
|
||||
deduct_success, deduct_result = points_db.deduct_points(
|
||||
sender, roomid, points, PointSource.PLUGIN,
|
||||
sender, roomid, actual_cost_points, PointSource.PLUGIN,
|
||||
description or f"使用 {plugin_name} 功能"
|
||||
)
|
||||
|
||||
if deduct_success:
|
||||
logger.info(f"用户 {sender} 使用 {plugin_name} 功能扣除 {points} 积分")
|
||||
logger.info(f"用户 {sender} 使用 {plugin_name} 功能扣除 {actual_cost_points} 积分")
|
||||
|
||||
# 添加对 response 的类型检查
|
||||
if isinstance(response, str) and "积分" not in response:
|
||||
response += f"\n\n💰 已消费 {points} 积分"
|
||||
response += f"\n\n{_build_points_cost_text(cost_profile)}"
|
||||
self.message_util.send_text(
|
||||
f"💰消费 {points} 积分",
|
||||
_build_points_cost_text(cost_profile),
|
||||
(roomid if roomid else sender), sender
|
||||
)
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user