Files
wushu/lib/services/document/API使用文档.md
2026-04-01 04:45:33 +08:00

546 lines
14 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 使用文档
## 概述
本文档描述诗词收录系统的 API 接口使用方法。
## 基础信息
- **API 地址**: `api.php`
- **请求方式**: GET/POST
- **返回格式**: JSON
- **字符编码**: UTF-8
## API 接口
### 1. 获取分类列表
获取所有可用的诗词分类。
**接口地址**: `api.php?api=categories`
**请求方式**: GET
**请求参数**: 无
**返回示例**:
```json
{
"ok": true,
"categories": [
{
"id": "1",
"sid": "1",
"icon": "fa-paper-plane",
"catename": "诗词句",
"alias": null,
"create_time": "2026-03-12 04:17:50",
"update_time": "2026-03-13 02:12:54"
}
],
"debug": {
"current_dir": "/www/wwwroot/yy.vogov.cn/api/app",
"categories_count": 3
}
}
```
---
### 2. 检查诗词名称是否存在
检查指定的诗词名称是否已存在于数据库中(支持相似度检查)。
**接口地址**: `api.php?api=check-name`
**请求方式**: POST
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| name | string | 是 | 诗词名称/参考语句 |
| threshold | int | 否 | 相似度阈值0-100默认 80 |
**请求示例**:
```javascript
const formData = new FormData();
formData.append('name', '盈盈一水间,脉脉不得语');
formData.append('threshold', 80);
const response = await fetch('api.php?api=check-name', {
method: 'POST',
body: formData
});
const data = await response.json();
```
**返回示例**:
**无相似内容**:
```json
{
"ok": true,
"exists": false,
"similar_count": 0,
"max_similarity": 0,
"threshold": 80
}
```
**发现相似内容**:
```json
{
"ok": true,
"exists": true,
"similar_count": 2,
"max_similarity": 95,
"threshold": 80
}
```
**返回字段说明**:
| 字段名 | 类型 | 说明 |
|--------|------|------|
| ok | boolean | 请求是否成功 |
| exists | boolean | 是否存在相似内容true=存在false=不存在 |
| similar_count | int | 相似内容条数 |
| max_similarity | float | 最高相似度百分比0-100 |
| threshold | int | 使用的相似度阈值 |
---
### 3. 提交诗词收录申请
提交诗词收录申请到数据库。
**接口地址**: `api.php?api=submit`
**请求方式**: POST
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| name | string | 是 | 诗词名称/参考语句 |
| catename | string | 是 | 分类名称 |
| url | string | 是 | 诗人和标题 |
| keywords | string | 是 | 关键词,多个用逗号分隔 |
| introduce | string | 是 | 诗词介绍 |
| img | string | 否 | 平台/配图,默认值: 'default' |
| captcha | string | 是 | 人机验证码 |
| threshold | int | 否 | 相似度阈值0-100默认 80 |
**请求示例**:
```javascript
const formData = new FormData();
formData.append('name', '盈盈一水间,脉脉不得语');
formData.append('catename', '诗词句');
formData.append('url', '古诗十九首');
formData.append('keywords', '爱情,古诗,离别');
formData.append('introduce', '《迢迢牵牛星》是产生于汉代的一首文人五言诗...');
formData.append('img', 'iOS Swift');
formData.append('captcha', '1234');
formData.append('threshold', 80);
const response = await fetch('api.php?api=submit', {
method: 'POST',
body: formData
});
const data = await response.json();
```
**返回示例**:
**成功**:
```json
{
"ok": true,
"message": "✅ 提交成功!等待审核",
"debug": {
"input_data": {...},
"insert_result": true,
"last_insert_id": "123"
}
}
```
**失败**:
```json
{
"ok": false,
"error": "该诗词已存在!",
"debug": {...}
}
```
**返回字段说明**:
| 字段名 | 类型 | 说明 |
|--------|------|------|
| ok | boolean | 请求是否成功 |
| message | string | 成功消息(仅成功时返回) |
| error | string | 错误消息(仅失败时返回) |
| debug | object | 调试信息 |
---
## 在 App 中的使用方法
### Android (Kotlin)
```kotlin
// 获取分类
suspend fun getCategories(): List<Category> {
val response = OkHttpClient().newCall(
Request.Builder()
.url("https://your-domain.com/api.php?api=categories")
.build()
).execute()
val json = JSONObject(response.body?.string())
val categoriesArray = json.getJSONArray("categories")
val categories = mutableListOf<Category>()
for (i in 0 until categoriesArray.length()) {
val cat = categoriesArray.getJSONObject(i)
categories.add(Category(cat.getString("catename")))
}
return categories
}
// 检查名称
suspend fun checkName(name: String, threshold: Int = 80): CheckResult {
val formBody = FormBody.Builder()
.add("name", name)
.add("threshold", threshold.toString())
.build()
val response = OkHttpClient().newCall(
Request.Builder()
.url("https://your-domain.com/api.php?api=check-name")
.post(formBody)
.build()
).execute()
val json = JSONObject(response.body?.string())
return CheckResult(
exists = json.getBoolean("exists"),
similarCount = json.getInt("similar_count"),
maxSimilarity = json.getDouble("max_similarity"),
threshold = json.getInt("threshold")
)
}
// 提交收录
suspend fun submitPoem(data: PoemData): Boolean {
val formBody = FormBody.Builder()
.add("name", data.name)
.add("catename", data.catename)
.add("url", data.url)
.add("keywords", data.keywords)
.add("introduce", data.introduce)
.add("img", data.img ?: "default")
.add("captcha", data.captcha)
.add("threshold", data.threshold?.toString() ?: "80")
.build()
val response = OkHttpClient().newCall(
Request.Builder()
.url("https://your-domain.com/api.php?api=submit")
.post(formBody)
.build()
).execute()
val json = JSONObject(response.body?.string())
return json.getBoolean("ok")
}
```
### iOS (Swift)
```swift
//
func getCategories(completion: @escaping ([String]?, Error?) -> Void) {
guard let url = URL(string: "https://your-domain.com/api.php?api=categories") else {
completion(nil, NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]))
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(nil, error)
return
}
guard let data = data else {
completion(nil, NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data"]))
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let categories = json["categories"] as? [[String: Any]] {
let categoryNames = categories.compactMap { $0["catename"] as? String }
completion(categoryNames, nil)
}
} catch {
completion(nil, error)
}
}.resume()
}
//
func checkName(name: String, threshold: Int = 80, completion: @escaping (CheckResult?, Error?) -> Void) {
guard let apiUrl = URL(string: "https://your-domain.com/api.php?api=check-name") else {
completion(nil, NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]))
return
}
var request = URLRequest(url: apiUrl)
request.httpMethod = "POST"
let parameters = [
"name": name,
"threshold": "\(threshold)"
]
request.httpBody = parameters.percentEncoded()
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(nil, error)
return
}
guard let data = data else {
completion(nil, NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data"]))
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let exists = json["exists"] as? Bool,
let similarCount = json["similar_count"] as? Int,
let maxSimilarity = json["max_similarity"] as? Double,
let threshold = json["threshold"] as? Int {
let result = CheckResult(
exists: exists,
similarCount: similarCount,
maxSimilarity: maxSimilarity,
threshold: threshold
)
completion(result, nil)
}
} catch {
completion(nil, error)
}
}.resume()
}
//
func submitPoem(name: String, catename: String, url: String, keywords: String, introduce: String, img: String?, captcha: String, threshold: Int = 80, completion: @escaping (Bool, String?) -> Void) {
guard let apiUrl = URL(string: "https://your-domain.com/api.php?api=submit") else {
completion(false, "Invalid URL")
return
}
var request = URLRequest(url: apiUrl)
request.httpMethod = "POST"
let parameters = [
"name": name,
"catename": catename,
"url": url,
"keywords": keywords,
"introduce": introduce,
"img": img ?? "default",
"captcha": captcha,
"threshold": "\(threshold)"
]
request.httpBody = parameters.percentEncoded()
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(false, error.localizedDescription)
return
}
guard let data = data else {
completion(false, "No data")
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let ok = json["ok"] as? Bool {
let message = json["message"] as? String ?? json["error"] as? String
completion(ok, message)
}
} catch {
completion(false, error.localizedDescription)
}
}.resume()
}
extension Dictionary {
func percentEncoded() -> Data? {
return map { key, value in
let escapedKey = "\(key)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
let escapedValue = "\(value)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
return escapedKey + "=" + escapedValue
}
.joined(separator: "&")
.data(using: .utf8)
}
}
extension CharacterSet {
static let urlQueryValueAllowed: CharacterSet = {
let generalDelimitersToEncode = ":#[]@"
let subDelimitersToEncode = "!$&'()*+,;="
var allowed = CharacterSet.urlQueryAllowed
allowed.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
return allowed
}()
}
```
### Flutter (Dart)
```dart
import 'package:http/http.dart' as http;
import 'dart:convert';
// 获取分类
Future<List<String>> getCategories() async {
final response = await http.get(
Uri.parse('https://your-domain.com/api.php?api=categories'),
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
final List<dynamic> categories = data['categories'];
return categories.map((cat) => cat['catename'] as String).toList();
} else {
throw Exception('Failed to load categories');
}
}
// 检查名称
Future<CheckResult> checkName({
required String name,
int threshold = 80,
}) async {
final response = await http.post(
Uri.parse('https://your-domain.com/api.php?api=check-name'),
body: {
'name': name,
'threshold': threshold.toString(),
},
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
return CheckResult(
exists: data['exists'] as bool,
similarCount: data['similar_count'] as int,
maxSimilarity: (data['max_similarity'] as num).toDouble(),
threshold: data['threshold'] as int,
);
} else {
throw Exception('Failed to check name');
}
}
// 提交收录
Future<bool> submitPoem({
required String name,
required String catename,
required String url,
required String keywords,
required String introduce,
String? img,
required String captcha,
int threshold = 80,
}) async {
final response = await http.post(
Uri.parse('https://your-domain.com/api.php?api=submit'),
body: {
'name': name,
'catename': catename,
'url': url,
'keywords': keywords,
'introduce': introduce,
'img': img ?? 'default',
'captcha': captcha,
'threshold': threshold.toString(),
},
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
return data['ok'] as bool;
} else {
throw Exception('Failed to submit');
}
}
```
---
## 错误码说明
| 错误信息 | 说明 |
|----------|------|
| 缺少必填字段xxx | 必填字段未填写 |
| 该诗词已存在! | 诗词名称已在数据库中,或相似度超过阈值 |
| ❌ 数据库写入失败:无法插入数据 | 数据库插入失败 |
| 验证码错误,请重新输入 | 人机验证码错误 |
| 提交过于频繁,请稍后再试 | 频率限制1分钟内只能提交3次 |
---
## 相似度说明
系统使用 **Levenshtein 距离算法** 计算文本相似度:
1. **文本清理**:自动去除标点符号和空格后比较
2. **阈值设置**0-100%,默认 80%
3. **判断规则**:相似度 ≥ 阈值 则认为是重复内容
**示例**
- "盈盈一水间,脉脉不得语"
- "盈盈一水间,脉脉不得语。"(相似度约 95%
- "盈盈一水间,脉脉不得"(相似度约 85%
---
## 注意事项
1. **字符编码**: 所有请求和响应都使用 UTF-8 编码
2. **人机验证**: 提交接口必须提供正确的验证码
3. **频率限制**: 同一 IP 1分钟内最多提交 3 次
4. **相似度检查**: check-name 和 submit 接口都会进行相似度检查
5. **数据安全**: 所有用户输入都会经过安全处理
6. **调试信息**: API 返回包含 debug 字段,方便开发调试,生产环境可忽略
---
## 更新日志
- **v1.0.12**: 添加相似度验证功能,支持可配置阈值
- **v1.0.11**: 修改验证表为 pre_site
- **v1.0.10**: 添加人机验证功能和频率限制
- **v1.0.9**: 添加结果Modal对话框
- **v1.0.8**: 添加检测按钮和提交前确认