# Danding Points 插件

全局积分/虚拟货币服务层，为其他插件提供统一的积分管理能力。用户在一个插件中获得的积分可以在另一个插件中消费。

本插件不包含任何用户交互命令（无 NoneBot matcher），纯 API 服务层，供其他插件直接 import 调用。

## 目录结构

```
danding_points/
├── __init__.py     # 插件元数据 & points_api 单例导出
├── config.py       # 配置类
└── api.py          # PointsAPI 核心
```

## 配置选项

在 `.env` 文件或 NoneBot 配置文件中添加（均带 `DANDING_` 前缀）：

| 配置项 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `DANDING_POINTS_API_HOST` | str | `https://api.danding.vip/bot/points` | xapi 积分 API 地址 |
| `DANDING_BOT_USER` | str | `1424473282` | xapi Bot 鉴权用户 |
| `DANDING_BOT_TOKEN` | str | 空 | xapi Bot 鉴权 Token，未设置时读取 `DANDING_API_TOKEN` / `BOT_TOKEN` |

积分余额上限与单次操作上限由 xapi `BOT_POINTS_MAX_BALANCE` / `BOT_POINTS_MAX_PER_OPERATION` 控制，nonebot 本地不再持有阈值。

## 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 签到 | `"gacha_sign"` |
| group_horse_racing | `"horse_race"` |
| 管理调整 | `"admin"` |

## 数据库

本插件不再写入本地 SQLite。积分账户与流水由 xapi MySQL `bot_user_points` / `bot_point_transactions` 承载，nonebot 只通过 `/bot/points/*` HTTP API 读写。
