新增群积分通货膨胀策略

1. 新增群聊插件积分真实消耗统计,区分成员持有积分与 SYSTEM 沉淀积分。

2. 为 plugin_points_cost 注解接入按群动态倍率,积分消耗偏低时自动提高实际扣费。

3. 优化积分不足与扣费成功提示,展示基础价、实际价与当前通胀倍率。
This commit is contained in:
liuwei
2026-04-27 13:35:13 +08:00
parent 59db63937d
commit 66ac0a7e89
2 changed files with 249 additions and 13 deletions

View File

@@ -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: