web端修复
This commit is contained in:
545
lib/services/API使用文档.md
Normal file
545
lib/services/API使用文档.md
Normal file
@@ -0,0 +1,545 @@
|
||||
# 诗词收录系统 - 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**: 添加检测按钮和提交前确认
|
||||
Reference in New Issue
Block a user