# 积分系统 Spec
## Why
Xiuno BBS 用户表已有 `credits`/`golds`/`rmbs` 三个积数字段,但缺少统一的服务层管理、变更日志、防刷机制和 API 接口。需要一个健壮的积分系统来支持多积分类型的增减操作、审计日志、防刷限制和插件扩展。
## What Changes
- 新建 `bbs_credits_log` 积分日志表(含 type, change, reason, ip, time)
- 新建 `lib/CreditsService.php` 统一积分服务类(add/sub/get/log/checkNegative,行锁+事务,禁止负分)
- 新建 `api/v1/credits.php` REST API 路由(GET 查询 / POST 增加扣减),需 Bearer token 鉴权
- 新增插件钩子:`credits_before_change` / `credits_after_change`
- 新增防刷机制:同一 reason + 用户每日限制次数(可配置)
- 新建 `cron/clean_credits_log.php` 日志清理定时任务(保留最近90天)
- 更新 `UpgradeService.php` 添加积分系统升级步骤
- 更新 `api/v1/bootstrap.php` 注册 credits 路由
- 更新 `conf/conf.default.php` 添加积分相关配置项
- 更新 `update.md` 记录变更
## Impact
- Affected code: `lib/CreditsService.php`(新增), `api/v1/credits.php`(新增), `api/v1/bootstrap.php`(修改), `lib/UpgradeService.php`(修改), `conf/conf.default.php`(修改), `cron/clean_credits_log.php`(新增)
- 数据库: 新建 `bbs_credits_log` 表,用户表已有 credits/golds/rmbs 字段无需修改
- 兼容性: 不覆盖 Xiuno 原有积分函数,使用新的 CreditsService 类
## ADDED Requirements
### Requirement: 积分日志表
系统 SHALL 创建 `bbs_credits_log` 表,包含字段:logid(AI主键), uid, type(积分类型: credits/golds/rmbs), change(变动值,正负), balance(变动后余额), reason(变动原因), ip, create_date。索引:(uid, create_date), (uid, reason, create_date)。
#### Scenario: 记录积分变动
- **WHEN** 用户积分发生变动
- **THEN** 系统在 credits_log 表中插入一条记录,包含变动类型、变动值、变动后余额、原因、IP 和时间
### Requirement: CreditsService 统一服务类
系统 SHALL 提供 `CreditsService` 类,包含方法:`add($uid, $type, $amount, $reason)`、`sub($uid, $type, $amount, $reason)`、`get($uid, $type)`、`log($uid, $page, $pagesize)`、`checkNegative($uid, $type, $amount)`。所有写操作使用行锁(SELECT FOR UPDATE)+ 事务,禁止余额为负。
#### Scenario: 增加积分
- **WHEN** 调用 `CreditsService::add()` 增加积分
- **THEN** 使用行锁读取当前余额,增加指定金额,写入日志,提交事务
#### Scenario: 扣减积分防止负分
- **WHEN** 调用 `CreditsService::sub()` 扣减积分且余额不足
- **THEN** 事务回滚,抛出异常或返回错误,余额不变
#### Scenario: 并发安全
- **WHEN** 同一用户同时发生多笔积分变动
- **THEN** 行锁确保串行执行,余额计算正确
### Requirement: 积分 API 路由
系统 SHALL 提供 REST API 端点:
- `GET /api/v1/credits` — 查询当前用户积分余额(需 Bearer token)
- `GET /api/v1/credits/log` — 查询当前用户积分日志(分页)
- `POST /api/v1/credits/add` — 增加积分(管理员或指定 reason 允许的操作)
- `POST /api/v1/credits/sub` — 扣减积分(管理员或指定 reason 允许的操作)
所有端点需 Bearer token 鉴权,普通用户只能操作自己的积分。
#### Scenario: 普通用户查询自己的积分
- **WHEN** 已登录用户 GET /api/v1/credits
- **THEN** 返回该用户所有积分类型的余额
#### Scenario: 普通用户操作他人积分被拒绝
- **WHEN** 普通用户 POST /api/v1/credits/add 指定 uid 不是自己
- **THEN** 返回 403 权限不足错误
#### Scenario: 管理员操作任意用户积分
- **WHEN** 管理员 POST /api/v1/credits/add 指定任意 uid
- **THEN** 成功增加积分
### Requirement: 插件钩子
系统 SHALL 在积分变动前后提供钩子:
- `credits_before_change` — 变动前触发,可阻止操作或修改变动值
- `credits_after_change` — 变动后触发,用于通知、统计等
#### Scenario: 钩子阻止积分变动
- **WHEN** credits_before_change 钩子返回 false
- **THEN** 积分变动被阻止,事务回滚
#### Scenario: 钩子修改变动值
- **WHEN** credits_before_change 钩子修改 amount 值
- **THEN** 使用修改后的 amount 执行积分变动
### Requirement: 防刷机制
系统 SHALL 对同一 reason + uid 的每日操作次数进行限制,次数阈值通过配置设定(`credits_daily_limit`)。超出限制时拒绝操作。
#### Scenario: 防刷限制生效
- **WHEN** 同一用户同一天对同一 reason 的操作次数超过配置限制
- **THEN** 返回错误,拒绝本次积分变动
#### Scenario: 防刷限制未超
- **WHEN** 同一用户同一天对同一 reason 的操作次数未超过限制
- **THEN** 正常执行积分变动
### Requirement: 日志清理定时任务
系统 SHALL 提供 `cron/clean_credits_log.php` 脚本,删除 90 天前的积分日志,保留天数可通过配置调整(`credits_log_retention_days`)。
#### Scenario: 清理过期日志
- **WHEN** 运行清理脚本
- **THEN** 删除 create_date 早于保留天数之前的所有日志记录
### Requirement: 升级脚本
系统 SHALL 在 UpgradeService 中添加积分系统升级步骤,创建 credits_log 表并添加积分相关配置项。
#### Scenario: 从旧版升级
- **WHEN** 执行升级流程
- **THEN** 自动创建 credits_log 表,添加 credits_daily_limit 和 credits_log_retention_days 配置项
## MODIFIED Requirements
### Requirement: API 路由注册
修改 `api/v1/bootstrap.php`,在路由分发中新增 credits 资源的路由指向 `credits.php`,在端点列表中添加 `credits`。
### Requirement: 系统配置
修改后台设置,新增配置项:
- `credits_daily_limit` — 每日防刷限制次数(默认 10)
- `credits_log_retention_days` — 日志保留天数(默认 90)
- `credits_types` — 启用的积分类型列表(默认 `['credits', 'golds', 'rmbs']`)
Xiuno BBS 重构记录贴(五)积分系统
复制成功