# 诗词收录系统 - 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 { 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() 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> 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 categories = data['categories']; return categories.map((cat) => cat['catename'] as String).toList(); } else { throw Exception('Failed to load categories'); } } // 检查名称 Future 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 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**: 添加检测按钮和提交前确认