该文件提供积分系统相关的接口,包括:
所有接口均需要 Token 认证。积分系统同时支持本地用户(User 表,通过 user_id 查找)和外部用户(UserData 表,通过 account 查找)。
路由前缀:/apiv1(以 routers/__init__.py 中注册为准)
常量:REQUIRED_POINTS = 10(每次下载消耗的积分数)
/apiv1/points/balance — 获取用户积分余额功能说明: 查询当前登录用户的积分余额。优先查询本地用户表 User,若未找到则查询外部用户表 UserData。
是否需要认证: 是
请求方式: GET
请求参数: 无
业务逻辑:
request.state.user 获取用户信息user_info.user_id + is_deleted == 0 查询 User 表user.points(None 时默认为 0)user_info.account 查询 UserData 表的 accountID 字段注意: 该接口手动管理数据库会话(SessionLocal() + try/finally/db.close()),未使用 FastAPI 的 Depends(get_db)。
测试用例:
// 请求
GET /apiv1/points/balance
token: <本地用户有效Token>
// 预期响应 (HTTP 200)
{
"statusCode": 200,
"msg": "success",
"data": {
"points": 100
}
}
// 请求
GET /apiv1/points/balance
token: <外部用户有效Token>
// 预期响应 (HTTP 200)
// 本地 User 表无此用户,回退到 UserData 表查询
{
"statusCode": 200,
"msg": "success",
"data": {
"points": 50
}
}
// 请求
GET /apiv1/points/balance
token: <Token对应的用户在两个表中均不存在>
// 预期响应 (HTTP 200, 业务码 404)
{
"statusCode": 404,
"msg": "未找到用户数据"
}
// 请求
GET /apiv1/points/balance
token: <用户积分字段为null的Token>
// 预期响应 (HTTP 200)
{
"statusCode": 200,
"msg": "success",
"data": {
"points": 0
}
}
// 请求
GET /apiv1/points/balance
// 预期响应 (HTTP 401)
{
"statusCode": 401,
"msg": "未认证"
}
// 预期响应 (HTTP 200, 业务码 500)
{
"statusCode": 500,
"msg": "获取积分余额失败: <具体错误信息>"
}
/apiv1/points/consume — 消费积分下载文件功能说明: 每次消耗 10 积分进行文件下载。扣减积分并记录消费日志。同样支持本地用户和外部用户。
是否需要认证: 是
请求方式: POST
请求体(JSON): | 字段 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| | file_name | string | 否 | "" | 下载的文件名 | | file_url | string | 否 | "" | 下载的文件 URL |
注意:该接口直接使用
request.json()解析请求体,未使用 Pydantic 模型。
业务逻辑:
PointsConsumptionLog 记录(含 user_id、file_name、file_url、points_consumed、balance_after、created_at、updated_at)测试用例:
// 请求
POST /apiv1/points/consume
token: <本地用户Token, 积分>=10>
Content-Type: application/json
{
"file_name": "隧道施工规范.pdf",
"file_url": "https://oss.example.com/files/tunnel_spec.pdf"
}
// 预期响应 (HTTP 200)
{
"statusCode": 200,
"msg": "success",
"data": {
"new_balance": 90,
"points_consumed": 10
}
}
// 请求
POST /apiv1/points/consume
token: <外部用户Token, 积分>=10>
Content-Type: application/json
{
"file_name": "桥梁设计手册.pdf",
"file_url": "https://oss.example.com/files/bridge_manual.pdf"
}
// 预期响应 (HTTP 200)
{
"statusCode": 200,
"msg": "success",
"data": {
"new_balance": 40,
"points_consumed": 10
}
}
// 请求
POST /apiv1/points/consume
token: <本地用户Token, 积分=5>
Content-Type: application/json
{
"file_name": "测试文件.pdf",
"file_url": "https://oss.example.com/files/test.pdf"
}
// 预期响应 (HTTP 200, 业务码 400)
{
"statusCode": 400,
"msg": "积分不足,下载需要10积分",
"data": {
"current_points": 5,
"required_points": 10
}
}
// 请求
POST /apiv1/points/consume
token: <外部用户Token, 积分=3>
Content-Type: application/json
{
"file_name": "测试文件.pdf",
"file_url": "https://oss.example.com/files/test.pdf"
}
// 预期响应 (HTTP 200, 业务码 400)
{
"statusCode": 400,
"msg": "积分不足,下载需要10积分",
"data": {
"current_points": 3,
"required_points": 10
}
}
// 请求
POST /apiv1/points/consume
token: <Token对应用户在两个表中均不存在>
Content-Type: application/json
{
"file_name": "文件.pdf",
"file_url": "https://example.com/file.pdf"
}
// 预期响应 (HTTP 200, 业务码 404)
{
"statusCode": 404,
"msg": "未找到用户数据"
}
// 请求
POST /apiv1/points/consume
token: <有效Token>
Content-Type: application/json
invalid json body
// 预期响应 (HTTP 200, 业务码 400)
{
"statusCode": 400,
"msg": "JSON解析失败"
}
// 请求
POST /apiv1/points/consume
token: <有效Token, 积分>=10>
Content-Type: application/json
{}
// 预期响应 (HTTP 200)
{
"statusCode": 200,
"msg": "success",
"data": {
"new_balance": 90,
"points_consumed": 10
}
}
file_name 和 file_url 会被记录为空字符串。
// 请求
POST /apiv1/points/consume
// 预期响应 (HTTP 401)
{
"statusCode": 401,
"msg": "未认证"
}
// 预期响应 (HTTP 200, 业务码 500)
{
"statusCode": 500,
"msg": "消费积分失败: <具体错误信息>"
}
注意:异常时代码会执行
db.rollback()回滚事务。
/apiv1/points/add — 增加积分(仅管理员)功能说明: 管理员为指定用户增加积分。需要管理员权限(role == "admin")。
是否需要认证: 是
是否需要管理员权限: 是(user_info.role != "admin" 时返回 403)
请求方式: POST
请求体(JSON): | 字段 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| | user_id | int/string | 是 | — | 目标用户 ID | | points | int | 否 | 0 | 增加的积分数(必须 > 0) | | reason | string | 否 | "" | 添加原因(仅记录日志) |
业务逻辑:
user_id 存在且 points > 0user_id 查询本地 User 表 → 外部 UserData 表logger.info 记录操作日志测试用例:
// 请求
POST /apiv1/points/add
token: <管理员Token>
Content-Type: application/json
{
"user_id": 5,
"points": 50,
"reason": "活动奖励"
}
// 预期响应 (HTTP 200)
{
"statusCode": 200,
"msg": "积分添加成功",
"data": {
"new_balance": 150
}
}
// 请求
POST /apiv1/points/add
token: <管理员Token>
Content-Type: application/json
{
"user_id": 10,
"points": 30,
"reason": "补偿积分"
}
// 预期响应 (HTTP 200)
// 本地 User 表未找到 id=10,回退到 UserData 表
{
"statusCode": 200,
"msg": "积分添加成功",
"data": {
"new_balance": 80
}
}
// 请求
POST /apiv1/points/add
token: <普通用户Token>
Content-Type: application/json
{
"user_id": 5,
"points": 50,
"reason": "测试"
}
// 预期响应 (HTTP 403)
{
"statusCode": 403,
"msg": "权限不足"
}
// 请求
POST /apiv1/points/add
token: <管理员Token>
Content-Type: application/json
{
"user_id": 99999,
"points": 50,
"reason": "测试"
}
// 预期响应 (HTTP 200, 业务码 404)
{
"statusCode": 404,
"msg": "用户不存在"
}
// 请求
POST /apiv1/points/add
token: <管理员Token>
Content-Type: application/json
{
"user_id": 5,
"points": 0,
"reason": "测试"
}
// 预期响应 (HTTP 200, 业务码 400)
{
"statusCode": 400,
"msg": "参数错误"
}
// 请求
POST /apiv1/points/add
token: <管理员Token>
Content-Type: application/json
{
"points": 50,
"reason": "测试"
}
// 预期响应 (HTTP 200, 业务码 400)
{
"statusCode": 400,
"msg": "参数错误"
}
// 请求
POST /apiv1/points/add
token: <管理员Token>
Content-Type: application/json
invalid json
// 预期响应 (HTTP 200, 业务码 400)
{
"statusCode": 400,
"msg": "JSON解析失败"
}
// 请求
POST /apiv1/points/add
// 预期响应 (HTTP 401)
{
"statusCode": 401,
"msg": "未认证"
}
// 预期响应 (HTTP 200, 业务码 500)
{
"statusCode": 500,
"msg": "添加积分失败: <具体错误信息>"
}
/apiv1/points/logs 或 /apiv1/points/history — 获取积分消费记录功能说明: 查询当前用户的积分消费记录,支持分页。两个路由路径(/logs 和 /history)指向同一个处理函数。
是否需要认证: 是
请求方式: GET
请求参数(Query): | 字段 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| | page | int | 否 | 1 | 页码 | | pageSize | int | 否 | 10 | 每页条数 |
业务逻辑:
str(user.id),外部用户使用 user_info.accountuser_identifier 查询 PointsConsumptionLog 表id 降序排列(最新的在前)成功响应字段: | 字段 | 类型 | 说明 | |------|------|------| | data.list | array | 消费记录列表 | | data.list[].id | int | 记录 ID | | data.list[].file_name | string | 文件名 | | data.list[].file_url | string | 文件 URL | | data.list[].points_consumed | int | 消耗积分数 | | data.list[].balance_after | int | 消费后余额 | | data.list[].created_at | int | 创建时间(Unix 时间戳) | | data.total | int | 总记录数 | | data.page | int | 当前页码 | | data.pageSize | int | 每页条数 |
测试用例:
// 请求
GET /apiv1/points/logs
token: <有效Token>
// 预期响应 (HTTP 200)
{
"statusCode": 200,
"msg": "success",
"data": {
"list": [
{
"id": 10,
"file_name": "隧道施工规范.pdf",
"file_url": "https://oss.example.com/files/tunnel_spec.pdf",
"points_consumed": 10,
"balance_after": 90,
"created_at": 1700000000
},
{
"id": 9,
"file_name": "桥梁设计手册.pdf",
"file_url": "https://oss.example.com/files/bridge_manual.pdf",
"points_consumed": 10,
"balance_after": 100,
"created_at": 1699999000
}
],
"total": 15,
"page": 1,
"pageSize": 10
}
}
// 请求
GET /apiv1/points/history?page=1&pageSize=5
token: <有效Token>
// 预期响应 (HTTP 200)
// 与 /points/logs 返回格式完全一致
{
"statusCode": 200,
"msg": "success",
"data": {
"list": [...],
"total": 15,
"page": 1,
"pageSize": 5
}
}
// 请求
GET /apiv1/points/logs?page=2&pageSize=5
token: <有效Token>
// 预期响应 (HTTP 200)
{
"statusCode": 200,
"msg": "success",
"data": {
"list": [
// ... 第 6-10 条记录
],
"total": 15,
"page": 2,
"pageSize": 5
}
}
// 请求
GET /apiv1/points/logs
token: <从未消费积分的用户Token>
// 预期响应 (HTTP 200)
{
"statusCode": 200,
"msg": "success",
"data": {
"list": [],
"total": 0,
"page": 1,
"pageSize": 10
}
}
// 请求
GET /apiv1/points/logs
// 预期响应 (HTTP 401)
{
"statusCode": 401,
"msg": "未认证"
}
// 预期响应 (HTTP 200, 业务码 500)
{
"statusCode": 500,
"msg": "获取消费记录失败: <具体错误信息>"
}
| 依赖项 | 说明 |
|---|---|
database.SessionLocal |
SQLAlchemy 数据库会话工厂(手动管理,非 Depends 注入) |
models.total.User |
本地用户模型(字段:id, points, is_deleted 等) |
models.user_data.UserData |
外部用户模型(字段:id, accountID, points 等) |
models.points.PointsConsumptionLog |
积分消费日志模型(字段:id, user_id, file_name, file_url, points_consumed, balance_after, created_at, updated_at) |
utils.logger.logger |
日志记录器 |
request.state.user |
从中间件注入的用户信息(含 user_id, account, role, username 等) |
User 表)和外部用户(UserData 表),查询顺序为先本地后外部。SessionLocal() 手动创建数据库会话,而非 FastAPI 推荐的 Depends(get_db) 依赖注入方式。每个接口都有 try/except/finally 结构确保 db.close() 被调用。consume 和 add 接口在异常时执行 db.rollback(),确保积分扣减和日志插入的原子性。PointsConsumptionLog.user_id 存储的是 str(user.id)(整数转字符串),外部用户存储的是 user_info.account(字符串)。查询记录时也按此逻辑匹配。/points/logs 和 /points/history 是同一接口的两个路由路径。add 接口的权限控制: 通过 user_info.role != "admin" 判断,返回 HTTP 403 状态码(其他接口的认证失败返回 HTTP 401)。consume 和 add 接口未使用 Pydantic 模型, 直接通过 request.json() 解析请求体,需要手动处理 JSON 解析失败的情况。add 接口不记录 PointsConsumptionLog, 仅通过 logger.info 记录操作日志到文件/控制台。