test_points.md 15 KB

routers/points.py 接口测试文档

文件功能概述

该文件提供积分系统相关的接口,包括:

  • 查询用户积分余额
  • 消费积分下载文件(每次扣除 10 积分)
  • 管理员为用户添加积分
  • 查询积分消费记录

所有接口均需要 Token 认证。积分系统同时支持本地用户(User 表,通过 user_id 查找)和外部用户(UserData 表,通过 account 查找)。

路由前缀:/apiv1(以 routers/__init__.py 中注册为准)

常量:REQUIRED_POINTS = 10(每次下载消耗的积分数)


接口列表


1. GET /apiv1/points/balance — 获取用户积分余额

功能说明: 查询当前登录用户的积分余额。优先查询本地用户表 User,若未找到则查询外部用户表 UserData

是否需要认证:

请求方式: GET

请求参数:

业务逻辑:

  1. request.state.user 获取用户信息
  2. 通过 user_info.user_id + is_deleted == 0 查询 User
  3. 若本地用户存在,返回 user.points(None 时默认为 0)
  4. 若本地用户不存在,通过 user_info.account 查询 UserData 表的 accountID 字段
  5. 若外部用户也不存在,返回 404

注意: 该接口手动管理数据库会话(SessionLocal() + try/finally/db.close()),未使用 FastAPI 的 Depends(get_db)

测试用例:

用例 1:本地用户查询余额

// 请求
GET /apiv1/points/balance
token: <本地用户有效Token>

// 预期响应 (HTTP 200)
{
  "statusCode": 200,
  "msg": "success",
  "data": {
    "points": 100
  }
}

用例 2:外部用户查询余额

// 请求
GET /apiv1/points/balance
token: <外部用户有效Token>

// 预期响应 (HTTP 200)
// 本地 User 表无此用户,回退到 UserData 表查询
{
  "statusCode": 200,
  "msg": "success",
  "data": {
    "points": 50
  }
}

用例 3:用户不存在(本地和外部均未找到)

// 请求
GET /apiv1/points/balance
token: <Token对应的用户在两个表中均不存在>

// 预期响应 (HTTP 200, 业务码 404)
{
  "statusCode": 404,
  "msg": "未找到用户数据"
}

用例 4:积分字段为 null(返回 0)

// 请求
GET /apiv1/points/balance
token: <用户积分字段为null的Token>

// 预期响应 (HTTP 200)
{
  "statusCode": 200,
  "msg": "success",
  "data": {
    "points": 0
  }
}

用例 5:未认证

// 请求
GET /apiv1/points/balance

// 预期响应 (HTTP 401)
{
  "statusCode": 401,
  "msg": "未认证"
}

用例 6:数据库异常

// 预期响应 (HTTP 200, 业务码 500)
{
  "statusCode": 500,
  "msg": "获取积分余额失败: <具体错误信息>"
}

2. POST /apiv1/points/consume — 消费积分下载文件

功能说明: 每次消耗 10 积分进行文件下载。扣减积分并记录消费日志。同样支持本地用户和外部用户。

是否需要认证:

请求方式: POST

请求体(JSON): | 字段 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| | file_name | string | 否 | "" | 下载的文件名 | | file_url | string | 否 | "" | 下载的文件 URL |

注意:该接口直接使用 request.json() 解析请求体,未使用 Pydantic 模型。

业务逻辑:

  1. 解析 JSON 请求体,失败返回 400
  2. 查询用户(优先本地 → 外部)
  3. 检查积分是否 >= 10
  4. 扣减 10 积分,更新数据库
  5. 插入 PointsConsumptionLog 记录(含 user_id、file_name、file_url、points_consumed、balance_after、created_at、updated_at)
  6. 提交事务

测试用例:

用例 1:本地用户正常消费

// 请求
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
  }
}

用例 2:外部用户正常消费

// 请求
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
  }
}

用例 3:积分不足(本地用户)

// 请求
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
  }
}

用例 4:积分不足(外部用户)

// 请求
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
  }
}

用例 5:用户不存在(本地和外部均无)

// 请求
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": "未找到用户数据"
}

用例 6:JSON 解析失败

// 请求
POST /apiv1/points/consume
token: <有效Token>
Content-Type: application/json

invalid json body

// 预期响应 (HTTP 200, 业务码 400)
{
  "statusCode": 400,
  "msg": "JSON解析失败"
}

用例 7:不传 file_name 和 file_url(使用默认空字符串)

// 请求
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 会被记录为空字符串。

用例 8:未认证

// 请求
POST /apiv1/points/consume

// 预期响应 (HTTP 401)
{
  "statusCode": 401,
  "msg": "未认证"
}

用例 9:数据库异常(事务回滚)

// 预期响应 (HTTP 200, 业务码 500)
{
  "statusCode": 500,
  "msg": "消费积分失败: <具体错误信息>"
}

注意:异常时代码会执行 db.rollback() 回滚事务。


3. POST /apiv1/points/add — 增加积分(仅管理员)

功能说明: 管理员为指定用户增加积分。需要管理员权限(role == "admin")。

是否需要认证:

是否需要管理员权限: 是(user_info.role != "admin" 时返回 403)

请求方式: POST

请求体(JSON): | 字段 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| | user_id | int/string | 是 | — | 目标用户 ID | | points | int | 否 | 0 | 增加的积分数(必须 > 0) | | reason | string | 否 | "" | 添加原因(仅记录日志) |

业务逻辑:

  1. 检查管理员权限
  2. 解析 JSON,验证 user_id 存在且 points > 0
  3. 通过 user_id 查询本地 User 表 → 外部 UserData
  4. 增加用户积分并提交
  5. 通过 logger.info 记录操作日志

测试用例:

用例 1:管理员为本地用户增加积分

// 请求
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
  }
}

用例 2:管理员为外部用户增加积分

// 请求
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
  }
}

用例 3:非管理员调用

// 请求
POST /apiv1/points/add
token: <普通用户Token>
Content-Type: application/json

{
  "user_id": 5,
  "points": 50,
  "reason": "测试"
}

// 预期响应 (HTTP 403)
{
  "statusCode": 403,
  "msg": "权限不足"
}

用例 4:user_id 不存在

// 请求
POST /apiv1/points/add
token: <管理员Token>
Content-Type: application/json

{
  "user_id": 99999,
  "points": 50,
  "reason": "测试"
}

// 预期响应 (HTTP 200, 业务码 404)
{
  "statusCode": 404,
  "msg": "用户不存在"
}

用例 5:参数错误 — points <= 0

// 请求
POST /apiv1/points/add
token: <管理员Token>
Content-Type: application/json

{
  "user_id": 5,
  "points": 0,
  "reason": "测试"
}

// 预期响应 (HTTP 200, 业务码 400)
{
  "statusCode": 400,
  "msg": "参数错误"
}

用例 6:参数错误 — 缺少 user_id

// 请求
POST /apiv1/points/add
token: <管理员Token>
Content-Type: application/json

{
  "points": 50,
  "reason": "测试"
}

// 预期响应 (HTTP 200, 业务码 400)
{
  "statusCode": 400,
  "msg": "参数错误"
}

用例 7:JSON 解析失败

// 请求
POST /apiv1/points/add
token: <管理员Token>
Content-Type: application/json

invalid json

// 预期响应 (HTTP 200, 业务码 400)
{
  "statusCode": 400,
  "msg": "JSON解析失败"
}

用例 8:未认证

// 请求
POST /apiv1/points/add

// 预期响应 (HTTP 401)
{
  "statusCode": 401,
  "msg": "未认证"
}

用例 9:数据库异常

// 预期响应 (HTTP 200, 业务码 500)
{
  "statusCode": 500,
  "msg": "添加积分失败: <具体错误信息>"
}

4. GET /apiv1/points/logs/apiv1/points/history — 获取积分消费记录

功能说明: 查询当前用户的积分消费记录,支持分页。两个路由路径(/logs/history)指向同一个处理函数。

是否需要认证:

请求方式: GET

请求参数(Query): | 字段 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| | page | int | 否 | 1 | 页码 | | pageSize | int | 否 | 10 | 每页条数 |

业务逻辑:

  1. 确定用户标识:本地用户使用 str(user.id),外部用户使用 user_info.account
  2. 通过 user_identifier 查询 PointsConsumptionLog
  3. id 降序排列(最新的在前)
  4. 分页返回

成功响应字段: | 字段 | 类型 | 说明 | |------|------|------| | 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 | 每页条数 |

测试用例:

用例 1:正常查询(默认分页)

// 请求
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
  }
}

用例 2:使用 /history 路由别名

// 请求
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
  }
}

用例 3:分页 — 第二页

// 请求
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
  }
}

用例 4:无消费记录

// 请求
GET /apiv1/points/logs
token: <从未消费积分的用户Token>

// 预期响应 (HTTP 200)
{
  "statusCode": 200,
  "msg": "success",
  "data": {
    "list": [],
    "total": 0,
    "page": 1,
    "pageSize": 10
  }
}

用例 5:未认证

// 请求
GET /apiv1/points/logs

// 预期响应 (HTTP 401)
{
  "statusCode": 401,
  "msg": "未认证"
}

用例 6:数据库异常

// 预期响应 (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 等)

代码备注

  1. 双用户体系: 所有接口都同时支持本地用户(User 表)和外部用户(UserData 表),查询顺序为先本地后外部。
  2. 手动会话管理: 所有接口使用 SessionLocal() 手动创建数据库会话,而非 FastAPI 推荐的 Depends(get_db) 依赖注入方式。每个接口都有 try/except/finally 结构确保 db.close() 被调用。
  3. 消费接口的事务性: consumeadd 接口在异常时执行 db.rollback(),确保积分扣减和日志插入的原子性。
  4. 积分记录的用户标识差异: 本地用户的 PointsConsumptionLog.user_id 存储的是 str(user.id)(整数转字符串),外部用户存储的是 user_info.account(字符串)。查询记录时也按此逻辑匹配。
  5. /points/logs/points/history 是同一接口的两个路由路径。
  6. add 接口的权限控制: 通过 user_info.role != "admin" 判断,返回 HTTP 403 状态码(其他接口的认证失败返回 HTTP 401)。
  7. consumeadd 接口未使用 Pydantic 模型, 直接通过 request.json() 解析请求体,需要手动处理 JSON 解析失败的情况。
  8. add 接口不记录 PointsConsumptionLog 仅通过 logger.info 记录操作日志到文件/控制台。