Files
2026-03-30 02:35:31 +08:00

526 lines
9.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 诗词答题 API 文档
## 基础信息
| 项目 | 说明 |
|------|------|
| 基础URL | `/api.php` |
| 返回格式 | JSON |
| 编码 | UTF-8 |
| 请求方式 | **GET / POST 都支持** |
### 请求方式说明
| 接口 | 推荐方式 | 原因 |
|------|----------|------|
| 获取题目 (question) | **GET** | 读取操作,简单可缓存 |
| 下一题 (next) | **GET** | 读取操作 |
| 获取新题 (fetch) | **GET** | 读取操作 |
| 提交答案 (answer) | **POST** | 提交操作,更规范 |
| 获取提示 (hint) | **GET** | 读取操作 |
| 题目列表 (list) | **GET** | 读取操作 |
| 刷新缓存 (refresh) | **GET** | 管理操作 |
| 状态统计 (stats) | **GET** | 读取操作 |
## 通用返回格式
```json
{
"code": 0,
"msg": "",
"data": { ... }
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| code | int | 状态码0=成功,其他=错误 |
| msg | string | 提示信息 |
| data | object | 返回数据 |
---
## 接口列表
### 1. 获取题目
**请求**
```
GET /api.php?action=question&id=0
```
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| action | string | 否 | 默认为 question |
| id | int | 否 | 题目ID默认0 |
**返回**
```json
{
"code": 0,
"msg": "",
"data": {
"id": 0,
"total": 10,
"question": "欲把西湖比西子,\"________\"",
"author": "苏轼",
"type": "江南",
"grade": "小学",
"dynasty": "宋朝",
"options": [
{"index": 1, "content": "山色空蒙雨亦奇"},
{"index": 2, "content": "淡妆浓抹总相宜"},
{"index": 3, "content": "门前流水尚能西"},
{"index": 4, "content": "拄杖无时夜叩门"}
]
}
}
```
---
### 2. 获取下一题(自动进度)
**请求**
```
GET /api.php?action=next
```
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| action | string | 是 | 固定为 next |
**说明**
- **无需传 id 参数**,系统自动记住当前进度(使用 Session
- 每次刷新自动跳到下一题
- 如果已经是最后一题,自动回到第 0 题(循环)
**使用方式**
| 刷新次数 | 返回的 id |
|----------|-----------|
| 第 1 次 | 1 |
| 第 2 次 | 2 |
| 第 3 次 | 3 |
| ... | ... |
| 第 62 次 | 0 (循环) |
**返回**
```json
{
"code": 0,
"msg": "",
"data": {
"id": 1,
"total": 62,
"question": "人生得意须尽欢,\"__________\"",
"author": "李白",
"type": "豪放",
"grade": "高中",
"dynasty": "唐朝",
"options": [...],
"prev_id": 0
}
}
```
---
### 3. 刷新获取新题(推荐)
**请求**
```
GET /api.php?action=fetch
```
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| action | string | 是 | 固定为 fetch |
**说明**
- **每次刷新都从百度 API 获取新题目**
- 新题目自动写入本地缓存(去重)
- 返回本次获取的随机一题
- API 失败时自动降级使用本地缓存
**返回**
```json
{
"code": 0,
"msg": "",
"data": {
"id": null,
"total": 65,
"question": "人生得意须尽欢,\"__________\"",
"author": "李白",
"type": "豪放",
"grade": "高中",
"dynasty": "唐朝",
"options": [...],
"from_cache": false,
"new_questions": 3
}
}
```
**额外返回字段**
| 字段 | 类型 | 说明 |
|------|------|------|
| from_cache | bool | 是否来自本地缓存 |
| new_questions | int | 本次新增题目数量 |
---
### 4. 提交答案
**请求方式 1GET简单**
```
GET /api.php?action=answer&id=0&answer=2
```
**请求方式 2POST推荐**
```
POST /api.php
Content-Type: application/x-www-form-urlencoded
action=answer&id=0&answer=2
```
或使用 JSON
```
POST /api.php
Content-Type: application/json
{
"action": "answer",
"id": 0,
"answer": "2"
}
```
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| action | string | 是 | 固定为 answer |
| id | int | 是 | 题目ID |
| answer | string | 是 | 答案序号(1-4) |
**返回**
```json
{
"code": 0,
"msg": "",
"data": {
"id": 0,
"correct": true,
"your_answer": "2",
"correct_answer": "2",
"next_id": 1,
"has_next": true
}
}
```
---
### 5. 获取提示
**请求**
```
GET /api.php?action=hint&id=0
```
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| action | string | 是 | 固定为 hint |
| id | int | 是 | 题目ID |
**返回**
```json
{
"code": 0,
"msg": "",
"data": {
"id": 0,
"hint": "这是首描写江南的诗,你在小学学过它。",
"author": "苏轼",
"dynasty": "宋朝"
}
}
```
---
### 6. 获取题目列表
**请求**
```
GET /api.php?action=list
```
**返回**
```json
{
"code": 0,
"msg": "",
"data": {
"total": 10,
"list": [
{
"id": 0,
"question": "欲把西湖比西子,\"________\"...",
"author": "苏轼",
"dynasty": "宋朝"
}
]
}
}
```
---
### 7. 刷新缓存
**请求**
```
GET /api.php?action=refresh
```
**说明**
- 强制从百度 API 获取最新题目
- 合并到本地缓存(去重)
**返回**
```json
{
"code": 0,
"msg": "",
"data": {
"refreshed": true,
"total": 15
}
}
```
---
## 缓存机制
| 配置 | 值 | 说明 |
|------|------|------|
| 缓存文件 | `data/questions.json` | 本地JSON文件 |
| 过期时间 | **永不过期** | 永久保存 |
| 去重方式 | 按 `question_content` | 相同题目不重复存储 |
| 降级策略 | API失败自动使用本地缓存 | 原始URL失效也能使用 |
| API 限频 | 5秒内只请求一次 | 防止并发请求百度 |
### 缓存文件结构
```json
{
"updated": "2024-01-01 12:00:00",
"count": 10,
"questions": [...]
}
```
---
## 性能优化
### 已实现优化
| 优化项 | 说明 |
|--------|------|
| 静态变量缓存 | 同一请求内多次读取只加载一次文件 |
| API 请求锁 | 5秒内多人请求只发一次 API |
| 原子写入 | 先写临时文件再 rename防止数据损坏 |
| 去重优化 | 只在写入时去重,读取时不处理 |
| 超时控制 | API 超时 5 秒,连接超时 3 秒 |
### 性能预估
| 场景 | 响应时间 |
|------|----------|
| 读取本地缓存 | ~2-5ms |
| API 请求成功 | ~200-500ms |
| API 限频降级 | ~1ms直接读缓存 |
| 100人并发 | 无压力(读缓存为主) |
### 查看状态
```
GET /api.php?action=stats
```
返回:
```json
{
"code": 0,
"data": {
"total_questions": 149,
"cache_file_size": "40.5 KB",
"last_updated": "2026-03-29 05:04:04",
"memory_usage": "256 KB"
}
}
```
---
## 错误码
| code | 说明 |
|------|------|
| 0 | 成功 |
| 400 | 参数错误 |
| 404 | 题目不存在 |
---
## 使用示例
### JavaScript
```javascript
// 获取第一题
fetch('/api.php?action=question&id=0')
.then(r => r.json())
.then(d => console.log(d.data.question));
// 获取下一题(自动进度,无需传 id
fetch('/api.php?action=next')
.then(r => r.json())
.then(d => console.log(d.data.id, d.data.question));
// 提交答案
fetch('/api.php?action=answer&id=0&answer=2')
.then(r => r.json())
.then(d => {
if(d.data.correct) {
console.log('回答正确!');
}
});
```
### Flutter/Dart
```dart
import 'package:dio/dio.dart';
final dio = Dio();
// 获取题目
Future<Map> getQuestion(int id) async {
final res = await dio.get('/api.php', queryParameters: {
'action': 'question',
'id': id,
});
return res.data['data'];
}
// 获取下一题(自动进度,无需传 id
Future<Map> getNextQuestion() async {
final res = await dio.get('/api.php', queryParameters: {
'action': 'next',
});
return res.data['data'];
}
// 提交答案POST 方式)
Future<bool> checkAnswer(int id, String answer) async {
final res = await dio.post('/api.php', data: {
'action': 'answer',
'id': id,
'answer': answer,
});
return res.data['data']['correct'];
}
```
---
## App 集成建议
### 方案 1App 自己管理进度(推荐)
使用 `?action=question` 接口App 完全控制进度:
```dart
class QuizApi {
int _currentId = 0;
int _total = 0;
Future<Map> getQuestion() async {
final res = await dio.get('/api.php', queryParameters: {
'action': 'question',
'id': _currentId,
});
final data = res.data['data'];
_total = data['total'] ?? 0;
return data;
}
Future<Map> submitAnswer(String answer) async {
final res = await dio.post('/api.php', data: {
'action': 'answer',
'id': _currentId,
'answer': answer,
});
return res.data['data'];
}
void nextQuestion() {
_currentId++;
if (_currentId >= _total) {
_currentId = 0;
}
}
int get currentId => _currentId;
}
```
**优点**
- 不依赖 SessionApp 完全控制进度
- 可以随时跳转任意题目
- 适合多端同步
---
### 方案 2使用自动进度
使用 `?action=next` 接口API 自动管理:
```dart
Future<Map> getNextQuestion() async {
final res = await dio.get('/api.php', queryParameters: {
'action': 'next',
});
return res.data['data'];
}
```
**优点**
- 简单,无需管理 ID
- 自动循环
**缺点**
- 依赖 Session需要保持 cookie
- 无法自由跳转题目
---
### 方案 3不断获取新题
使用 `?action=fetch` 接口,每次都从百度获取新题:
```dart
Future<Map> getNewQuestion() async {
final res = await dio.get('/api.php', queryParameters: {
'action': 'fetch',
});
return res.data['data'];
}
```
**适用场景**
- 想要不断扩展题库
- 用户每次刷新都可能看到新题