Files
wushu/ht/API使用文档.md
2026-03-30 07:32:12 +08:00

14 KiB
Raw Blame History

诗词收录系统 - API 使用文档

概述

本文档描述诗词收录系统的 API 接口使用方法。

基础信息

  • API 地址: app/apply.php
  • 请求方式: GET/POST
  • 返回格式: JSON
  • 字符编码: UTF-8

API 接口

1. 获取分类列表

获取所有可用的诗词分类。

接口地址: app/apply.php?api=categories

请求方式: GET

请求参数: 无

返回示例:

{
  "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/apply.php",
    "categories_count": 3
  }
}

2. 检查诗词名称是否存在

检查指定的诗词名称是否已存在于数据库中(支持相似度检查)。

接口地址: app/apply.php?api=check-name

请求方式: POST

请求参数:

参数名 类型 必填 说明
name string 诗词名称/参考语句
threshold int 相似度阈值0-100默认 80

请求示例:

const formData = new FormData();
formData.append('name', '盈盈一水间,脉脉不得语');
formData.append('threshold', 80);

const response = await fetch('app/apply.php?api=check-name', {
  method: 'POST',
  body: formData
});

const data = await response.json();

返回示例:

无相似内容:

{
  "ok": true,
  "exists": false,
  "similar_count": 0,
  "max_similarity": 0,
  "threshold": 80
}

发现相似内容:

{
  "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. 提交诗词收录申请

提交诗词收录申请到数据库。

接口地址: app/apply.php?api=submit

请求方式: POST

请求参数:

参数名 类型 必填 说明
name string 诗词名称/参考语句
catename string 分类名称
url string 诗人和标题
keywords string 关键词,多个用逗号分隔
introduce string 诗词介绍
img string 平台/配图,默认值: 'default'
captcha string 人机验证码
threshold int 相似度阈值0-100默认 80

请求示例:

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('app/apply.php?api=submit', {
  method: 'POST',
  body: formData
});

const data = await response.json();

返回示例:

成功:

{
  "ok": true,
  "message": "✅ 提交成功!等待审核",
  "debug": {
    "input_data": {...},
    "insert_result": true,
    "last_insert_id": "123"
  }
}

失败:

{
  "ok": false,
  "error": "该诗词已存在!",
  "debug": {...}
}

返回字段说明:

字段名 类型 说明
ok boolean 请求是否成功
message string 成功消息(仅成功时返回)
error string 错误消息(仅失败时返回)
debug object 调试信息

在 App 中的使用方法

Android (Kotlin)

// 获取分类
suspend fun getCategories(): List<Category> {
    val response = OkHttpClient().newCall(
        Request.Builder()
            .url("https://your-domain.com/app/apply.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/app/apply.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/app/apply.php?api=submit")
            .post(formBody)
            .build()
    ).execute()
    
    val json = JSONObject(response.body?.string())
    return json.getBoolean("ok")
}

iOS (Swift)

// 获取分类
func getCategories(completion: @escaping ([String]?, Error?) -> Void) {
    guard let url = URL(string: "https://your-domain.com/app/apply.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/app/apply.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/app/apply.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)

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/app/apply.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/app/apply.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/app/apply.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: 添加检测按钮和提交前确认