功能:实现 Danding_Points 积分系统插件

- 新增积分系统插件,支持积分查询、签到、转账等核心功能
- 包含对应的测试脚本

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 00:24:00 +08:00
parent ef259bd73c
commit 0fd011fa1e
6 changed files with 715 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
# Danding Points 插件
全局积分/虚拟货币服务层,为其他插件提供统一的积分管理能力。用户在一个插件中获得的积分可以在另一个插件中消费。
本插件不包含任何用户交互命令(无 NoneBot matcher纯 API 服务层,供其他插件直接 import 调用。
## 目录结构
```
danding_points/
├── __init__.py # 插件元数据 & points_api 单例导出
├── config.py # 配置类
├── database.py # SQLite 数据库操作
└── api.py # PointsAPI 核心
```
## 配置选项
`.env` 文件或 NoneBot 配置文件中添加(均带 `DANDING_` 前缀):
| 配置项 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `DANDING_POINTS_DB_FILE` | str | `data/danding_points/points.db` | 数据库文件路径 |
| `DANDING_POINTS_MAX_BALANCE` | int | `0` | 用户积分余额上限,`0` = 无限制 |
| `DANDING_POINTS_MAX_PER_OPERATION` | int | `0` | 单次操作积分上限,`0` = 无限制 |
| `DANDING_POINTS_LOG_RETENTION_DAYS` | int | `365` | 流水日志保留天数 |
## API 接口
所有方法均为 `async`,通过 `points_api` 单例调用。
### `get_balance(user_id: str) -> int`
查询用户积分余额。用户不存在时自动返回 `0`,不会创建账户。
```python
balance = await points_api.get_balance("123456")
```
### `add_points(user_id, amount, source, reason=None) -> Tuple[bool, int]`
为用户增加积分。用户不存在时自动建户。
- `amount`: 正整数,必须 > 0
- `source`: 来源标识,不能为空(用于流水追踪)
- `reason`: 变动原因,可选
- 返回 `(success, new_balance)`,失败时 `success=False`
触发校验:操作上限 → 余额上限。任一校验失败均返回 `(False, 当前余额)`
```python
ok, balance = await points_api.add_points("123456", 100, "gacha", "抽卡奖励")
if ok:
print(f"充值成功,当前余额: {balance}")
```
### `spend_points(user_id, amount, source, reason=None) -> Tuple[bool, int]`
消费用户积分。余额不足时返回 `(False, 当前余额)`
- `amount`: 正整数,必须 > 0
- 流水中 `amount` 记录为负数
```python
ok, balance = await points_api.spend_points("123456", 30, "shop", "购买道具")
if not ok:
print("余额不足")
```
### `set_points(user_id, amount, source, reason=None) -> Tuple[bool, int]`
直接设定用户积分。**绕过** 操作上限和余额上限约束。
- `amount`: 非负整数,必须 >= 0
- 新值等于旧值时不写流水,直接返回 `(True, amount)`
- `total_earned` 仅累加正向差额(设低不影响)
```python
ok, balance = await points_api.set_points("123456", 500, "admin", "管理员调整")
```
### `get_transactions(user_id, limit=20, offset=0) -> List[dict]`
查询用户积分流水记录,按时间倒序。
- `limit`: 1~100超出范围自动裁剪
- `offset`: 分页偏移量,>= 0
返回字段:`id, user_id, amount, balance_after, source, reason, created_at`
```python
txs = await points_api.get_transactions("123456", limit=10, offset=0)
for tx in txs:
print(f"{tx['created_at']} {tx['amount']:+d} 余额:{tx['balance_after']}")
```
### `get_ranking(limit=10, order_by="points") -> List[dict]`
查询积分排行榜。
- `limit`: 1~100
- `order_by`: `"points"`(按余额)或 `"total_earned"`(按累计获得),其他值回退为 `"points"`
- 使用 `RANK()` 窗口函数,同分并列,次级排序按 `user_id` 字母序
返回字段:`rank, user_id, points, total_earned, total_spent`
```python
ranking = await points_api.get_ranking(limit=10, order_by="points")
for r in ranking:
print(f"#{r['rank']} {r['user_id']} {r['points']}分")
```
## 其他插件对接
### 基本用法
在需要积分功能的插件中 import 单例即可:
```python
from nonebot import require
require("danding_points")
from danding_bot.plugins.danding_points import points_api
```
### 示例:抽卡插件发放奖励
```python
# 在 onmyoji_gacha 插件中
from danding_bot.plugins.danding_points import points_api
async def reward_user(user_id: str):
ok, balance = await points_api.add_points(
user_id, 50, "onmyoji_gacha", "SSR 抽到奖励"
)
return balance
```
### 示例:商店插件消费积分
```python
# 在 shop 插件中
from danding_bot.plugins.danding_points import points_api
async def buy_item(user_id: str, cost: int):
ok, balance = await points_api.spend_points(
user_id, cost, "shop", "购买商品"
)
if not ok:
return "积分不足"
return f"购买成功,剩余 {balance} 积分"
```
### 示例:管理员插件调整积分
```python
# 在 danding_api 插件中
from danding_bot.plugins.danding_points import points_api
async def admin_set(user_id: str, amount: int):
ok, balance = await points_api.set_points(
user_id, amount, "danding_api", "管理员手动调整"
)
return balance
```
### source 命名建议
`source` 参数用于标识积分变动来源,建议各插件使用自身插件名作为 source
| 插件 | source 值 |
|------|-----------|
| onmyoji_gacha | `"onmyoji_gacha"` |
| danding_api | `"danding_api"` |
| shop | `"shop"` |
| sign_in | `"sign_in"` |
## 数据库
使用 SQLite数据文件位于 `data/danding_points/points.db`,无需额外配置。
### 表结构
**user_points** — 用户积分账户
| 字段 | 类型 | 说明 |
|------|------|------|
| user_id | TEXT PK | 用户 ID |
| points | INTEGER | 当前余额,>= 0 |
| total_earned | INTEGER | 累计获得 |
| total_spent | INTEGER | 累计消费 |
| created_at | TEXT | 创建时间 |
| updated_at | TEXT | 最后更新时间 |
**point_transactions** — 积分变动流水
| 字段 | 类型 | 说明 |
|------|------|------|
| id | INTEGER PK | 自增 ID |
| user_id | TEXT | 用户 ID |
| amount | INTEGER | 变动数额(消费为负) |
| balance_after | INTEGER | 变动后余额 |
| source | TEXT | 来源标识 |
| reason | TEXT | 变动原因 |
| created_at | TEXT | 创建时间 |