Initial commit: Flutter 无书应用项目

This commit is contained in:
Developer
2026-03-30 02:35:31 +08:00
commit 9175ff9905
566 changed files with 103261 additions and 0 deletions

503
ht/p1/API文档.md Normal file
View File

@@ -0,0 +1,503 @@
# 古诗文答题系统 API 文档
## 文件信息
- **文件路径**: `p1/api.php`
- **功能描述**: 古诗文题目获取与答题系统
- **编码格式**: UTF-8
- **响应格式**: JSON
## 安全认证
### Key 认证机制
API 采用动态 Key 认证,防止接口被滥用。
#### 认证原理
1. **客户端生成 Key**: 使用 `SHA256(时间戳 + 密钥)` 生成
2. **服务端验证**: 验证 Key 是否在有效期内(默认 300 秒)
3. **Key 更新**: 每次成功请求后,服务端返回新的 `new_key`
#### 密钥配置
- **前端**: `api.js` 中的 `SECRET_KEY` 变量
- **后端**: `api.php` 中的 `SECRET_KEY` 常量
- **默认密钥**: `tzgsc_2026_secret_key`
#### Key 生成算法
##### PHP
```php
<?php
define('SECRET_KEY', 'tzgsc_2026_secret_key');
function generate_key() {
$timestamp = time();
$data = $timestamp . SECRET_KEY;
return hash('sha256', $data);
}
// 使用示例
$key = generate_key();
$url = "api.php?id=1&key=" . urlencode($key);
```
##### JavaScript (Node.js)
```javascript
const crypto = require('crypto');
const SECRET_KEY = 'tzgsc_2026_secret_key';
function generateKey() {
const timestamp = Math.floor(Date.now() / 1000);
const data = timestamp + SECRET_KEY;
return crypto.createHash('sha256').update(data).digest('hex');
}
// 使用示例
const key = generateKey();
const url = `api.php?id=1&key=${encodeURIComponent(key)}`;
```
##### Python
```python
import time
import hashlib
import urllib.parse
SECRET_KEY = 'tzgsc_2026_secret_key'
def generate_key():
timestamp = int(time.time())
data = str(timestamp) + SECRET_KEY
return hashlib.sha256(data.encode()).hexdigest()
# 使用示例
key = generate_key()
url = f"api.php?id=1&key={urllib.parse.quote(key)}"
```
##### Java
```java
import java.security.MessageDigest;
import java.time.Instant;
public class KeyGenerator {
private static final String SECRET_KEY = "tzgsc_2026_secret_key";
public static String generateKey() throws Exception {
long timestamp = Instant.now().getEpochSecond();
String data = timestamp + SECRET_KEY;
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(data.getBytes("UTF-8"));
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
}
// 使用示例
public static void main(String[] args) throws Exception {
String key = generateKey();
String url = "api.php?id=1&key=" + java.net.URLEncoder.encode(key, "UTF-8");
System.out.println(url);
}
}
```
##### Go
```go
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"net/url"
"time"
)
const SECRET_KEY = "tzgsc_2026_secret_key"
func generateKey() string {
timestamp := time.Now().Unix()
data := fmt.Sprintf("%d%s", timestamp, SECRET_KEY)
hash := sha256.Sum256([]byte(data))
return hex.EncodeToString(hash[:])
}
// 使用示例
func main() {
key := generateKey()
encodedKey := url.QueryEscape(key)
apiUrl := fmt.Sprintf("api.php?id=1&key=%s", encodedKey)
fmt.Println(apiUrl)
}
```
##### C#
```csharp
using System;
using System.Security.Cryptography;
using System.Text;
using System.Web;
public class KeyGenerator
{
private const string SECRET_KEY = "tzgsc_2026_secret_key";
public static string GenerateKey()
{
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
string data = timestamp + SECRET_KEY;
using (SHA256 sha256 = SHA256.Create())
{
byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(data));
StringBuilder builder = new StringBuilder();
foreach (byte b in hash)
{
builder.Append(b.ToString("x2"));
}
return builder.ToString();
}
}
// 使用示例
public static void Main()
{
string key = GenerateKey();
string url = $"api.php?id=1&key={HttpUtility.UrlEncode(key)}";
Console.WriteLine(url);
}
}
```
##### Ruby
```ruby
require 'digest'
require 'cgi'
SECRET_KEY = 'tzgsc_2026_secret_key'
def generate_key
timestamp = Time.now.to_i
data = "#{timestamp}#{SECRET_KEY}"
Digest::SHA256.hexdigest(data)
end
# 使用示例
key = generate_key
url = "api.php?id=1&key=#{CGI.escape(key)}"
puts url
```
##### Rust
```rust
use sha2::{Sha256, Digest};
use std::time::{SystemTime, UNIX_EPOCH};
const SECRET_KEY: &str = "tzgsc_2026_secret_key";
fn generate_key() -> String {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let data = format!("{}{}", timestamp, SECRET_KEY);
let mut hasher = Sha256::new();
hasher.update(data.as_bytes());
let result = hasher.finalize();
hex::encode(result)
}
// 使用示例
fn main() {
let key = generate_key();
let encoded_key = urlencoding::encode(&key);
let url = format!("api.php?id=1&key={}", encoded_key);
println!("{}", url);
}
```
### 请求限制
| 类型 | 限制 | 说明 |
|------|------|------|
| 无 Key 请求 | 10 次/分钟 | 基于 IP 地址限制 |
| 有 Key 请求 | 无限制 | 验证通过后无频率限制 |
#### 限制配置
```php
define('KEY_EXPIRE_TIME', 300); // Key 有效期(秒)
define('MAX_REQUESTS_NO_KEY', 10); // 无 Key 最大请求次数
define('RATE_LIMIT_WINDOW', 60); // 限流时间窗口(秒)
```
## 接口信息
### 接口概述
该接口用于获取古诗文题目、验证答案、获取提示信息,支持 GET 和 POST 请求方式。
### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | string/int | 是 | 题目唯一标识符 |
| msg | string | 否 | 操作类型:答案序号/提示 |
| key | string | 否 | 认证密钥(推荐携带) |
### 请求示例
#### 1. 获取题目
```
GET p1/api.php?id=1&key=xxx
```
POST 请求:
```
POST p1/api.php
Content-Type: application/json
{
"id": "1",
"key": "xxx"
}
```
#### 2. 提交答案
```
GET p1/api.php?id=1&msg=1&key=xxx
```
POST 请求:
```
POST p1/api.php
Content-Type: application/json
{
"id": "1",
"msg": "1",
"key": "xxx"
}
```
#### 3. 获取提示
```
GET p1/api.php?id=1&msg=提示&key=xxx
```
POST 请求:
```
POST p1/api.php
Content-Type: application/json
{
"id": "1",
"msg": "提示",
"key": "xxx"
}
```
### 响应信息
#### 获取题目成功响应
```json
{
"success": true,
"new_key": "新的认证密钥",
"data": {
"id": "1",
"question": "题目内容",
"options": [
{
"num": 1,
"text": "选项1"
},
{
"num": 2,
"text": "选项2"
},
{
"num": 3,
"text": "选项3"
},
{
"num": 4,
"text": "选项4"
}
],
"author": "作者",
"type": "描写类型",
"grade": "学习阶段",
"dynasty": "年代"
}
}
```
#### 答案正确响应
```json
{
"success": true,
"type": "correct",
"message": "恭喜你,回答正确。请继续下一题",
"next_question": {
"id": "2",
"question": "下一题题目",
"options": [
...
],
"author": "作者",
"type": "描写类型",
"grade": "学习阶段",
"dynasty": "年代"
}
}
```
#### 答案错误响应
```json
{
"success": true,
"type": "wrong",
"message": "抱歉,答案不对哦。你可以回复提示获取该题的部分信息哦。"
}
```
#### 提示响应
```json
{
"success": true,
"type": "hint",
"message": "这是首描写[描写类型]的诗,你在[学习阶段]学过它。"
}
```
#### 获取失败响应
```json
{
"success": false,
"message": "抱歉,获取出现错误。"
}
```
#### 错误参数响应
```json
{
"success": false,
"message": "缺少必要参数: id"
}
```
#### 不支持的请求方法
```json
{
"success": false,
"message": "不支持的请求方法"
}
```
#### 请求频率限制
```json
{
"success": false,
"message": "请求过于频繁,请稍后再试"
}
```
HTTP 状态码: 429
## 核心功能说明
### 1. 题目数据来源
- 从百度汉语 API 获取题目数据
- API 地址: `https://hanyu.baidu.com/hanyu/ajax/pingce_data`
### 2. 数据存储
- 题目数据存储在 `data/tzgsc/` 目录
- 文件格式: `.json`
- 文件命名: `{id}.json`
- JSON 缓存: `data/tzgsc.json`
### 3. 数据格式
题目数据存储格式 (JSON):
```json
{
"id": "1",
"question": "题目内容",
"options": [
{"num": 1, "text": "选项1"},
{"num": 2, "text": "选项2"},
{"num": 3, "text": "选项3"},
{"num": 4, "text": "选项4"}
],
"author": "作者",
"type": "描写类型",
"grade": "学习阶段",
"dynasty": "年代",
"correct_answer": 1
}
```
### 4. 核心函数
#### get_question($id)
获取并返回题目
- **参数**: $id - 题目 ID
- **返回**: 包含题目信息的数组
#### submit_answer($id, $msg)
提交答案并返回结果
- **参数**:
- $id - 题目 ID
- $msg - 答案序号或"提示"
- **返回**: 包含答题结果的数组
#### cj($data)
采集并缓存题目数据
- **参数**: $data - API 返回的原始数据
- **功能**: 解析题目数据,去重后保存到 JSON 文件
#### get_curl($url, $post, $referer, $cookie, $header, $ua, $nobaody)
发送 HTTP 请求
- **参数**:
- $url - 请求地址
- $post - POST 数据
- $referer - 来源地址
- $cookie - Cookie
- $header - 是否返回头部
- $ua - User-Agent
- $nobaody - 是否不返回内容
- **功能**: 使用 cURL 发送 HTTP 请求,支持 SSL 和 GZIP 压缩
#### replace_unicode_escape_sequence($match)
Unicode 转义序列转换
- **参数**: $match - 匹配到的 Unicode 序列
- **返回**: UTF-8 编码的字符串
- **功能**: 将 Unicode 转义序列转换为 UTF-8 字符
## 目录结构
```
p1/
├── api.php # API 接口文件
├── index.php # 前端页面(服务端生成初始 Key
├── api.js # 前端交互脚本
└── data/
├── tzgsc.json # 题目缓存文件
├── rate_limit/ # 请求限制记录目录
└── tzgsc/
└── [id].json # 题目数据文件
```
## 注意事项
1. 确保 `data/tzgsc/` 目录具有写入权限
2. API 依赖百度汉语接口,需确保网络连接正常
3. 题目数据会缓存到本地,避免重复请求
4. 答案验证时会读取本地保存的题目数据文件
5. 支持 Unicode 编码处理和 GZIP 压缩响应
6. 支持 GET 和 POST 两种请求方式
7. 所有响应均为 JSON 格式

344
ht/p1/api.js Normal file
View File

@@ -0,0 +1,344 @@
(function() {
'use strict';
var state = {
currentId: 1,
correctCount: 0,
totalCount: 0,
selectedOption: null,
isLoading: false,
answered: false,
currentKey: window.INITIAL_KEY || null
};
var elements = {
questionCard: document.getElementById('questionCard'),
navigation: document.getElementById('navigation'),
details: document.getElementById('details'),
loading: document.getElementById('loading'),
correctCount: document.getElementById('correctCount'),
totalCount: document.getElementById('totalCount'),
feedback: document.getElementById('feedback'),
feedbackIcon: document.getElementById('feedbackIcon'),
feedbackText: document.getElementById('feedbackText'),
overlay: document.getElementById('overlay'),
detailAuthor: document.getElementById('detailAuthor'),
detailDynasty: document.getElementById('detailDynasty'),
detailType: document.getElementById('detailType'),
detailGrade: document.getElementById('detailGrade')
};
function getUrlParam(name) {
var urlParams = new URLSearchParams(window.location.search);
return urlParams.get(name);
}
function setUrlParam(name, value) {
var url = new URL(window.location.href);
url.searchParams.set(name, value);
window.history.replaceState({}, '', url);
}
function fetchData(url, callback, errorCallback) {
if (!state.currentKey) {
errorCallback('未初始化 Key');
return;
}
var separator = url.indexOf('?') !== -1 ? '&' : '?';
var fullUrl = url + separator + 'key=' + encodeURIComponent(state.currentKey);
var xhr = new XMLHttpRequest();
xhr.open('GET', fullUrl, true);
xhr.setRequestHeader('Accept', 'application/json');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
try {
var response = JSON.parse(xhr.responseText);
if (response.new_key) {
state.currentKey = response.new_key;
}
callback(response);
} catch (e) {
errorCallback('解析响应失败');
}
} else if (xhr.status === 429) {
errorCallback('请求过于频繁,请稍后再试');
} else {
errorCallback('请求失败,状态码: ' + xhr.status);
}
}
};
xhr.onerror = function() {
errorCallback('网络错误');
};
xhr.send();
}
function fetchQuestion(id) {
if (state.isLoading) return;
state.isLoading = true;
state.answered = false;
state.selectedOption = null;
showLoading();
fetchData('api.php?id=' + id, function(response) {
state.isLoading = false;
if (response.success) {
renderQuestion(response.data);
updateNavigation(id);
updateDetails(response.data);
} else {
showError(response.message || '获取题目失败');
}
}, function(error) {
state.isLoading = false;
showError(error);
});
}
function submitAnswer(id, answer) {
if (state.isLoading || state.answered) return;
state.isLoading = true;
fetchData('api.php?id=' + id + '&msg=' + answer, function(response) {
state.isLoading = false;
if (response.success) {
state.totalCount++;
updateStats();
if (response.type === 'correct') {
state.correctCount++;
updateStats();
showFeedback('correct', '🎉 ' + response.message);
markOption(answer, 'correct');
setTimeout(function() {
hideFeedback();
if (response.next_question) {
state.currentId++;
setUrlParam('id', state.currentId);
renderQuestion(response.next_question);
updateNavigation(state.currentId);
updateDetails(response.next_question);
} else {
state.currentId++;
setUrlParam('id', state.currentId);
fetchQuestion(state.currentId);
}
}, 1500);
} else if (response.type === 'wrong') {
showFeedback('wrong', '😢 ' + response.message);
markOption(answer, 'wrong');
setTimeout(function() {
hideFeedback();
clearOptionMark(answer);
}, 2000);
} else if (response.type === 'hint') {
showHintMessage(response.message);
}
} else {
showFeedback('wrong', response.message || '提交失败');
setTimeout(hideFeedback, 2000);
}
}, function(error) {
state.isLoading = false;
showFeedback('wrong', error);
setTimeout(hideFeedback, 2000);
});
}
function getHint(id) {
if (state.isLoading) return;
state.isLoading = true;
fetchData('api.php?id=' + id + '&msg=提示', function(response) {
state.isLoading = false;
if (response.success && response.type === 'hint') {
showHintMessage(response.message);
} else {
showFeedback('wrong', response.message || '获取提示失败');
setTimeout(hideFeedback, 2000);
}
}, function(error) {
state.isLoading = false;
showFeedback('wrong', error);
setTimeout(hideFeedback, 2000);
});
}
function showLoading() {
elements.questionCard.innerHTML = '<div class="loading">加载中</div>';
elements.details.style.display = 'none';
}
function showError(message) {
elements.questionCard.innerHTML =
'<div class="error-message">' +
'<p>❌ ' + message + '</p>' +
'<button class="retry-btn" onclick="PoetryQuiz.retry()">🔄 重试</button>' +
'</div>';
elements.details.style.display = 'none';
}
function renderQuestion(data) {
var html = '<span class="question-number">第 ' + data.id + ' 题</span>';
html += '<div class="question">' + escapeHtml(data.question) + '</div>';
html += '<div class="options">';
data.options.forEach(function(option) {
html += '<button class="option-btn" data-num="' + option.num + '" onclick="PoetryQuiz.selectOption(' + option.num + ')">';
html += '<span class="option-num">' + option.num + '</span>';
html += '<span class="option-text">' + escapeHtml(option.text) + '</span>';
html += '</button>';
});
html += '</div>';
html += '<button class="hint-btn" onclick="PoetryQuiz.getHint()">';
html += '💡 获取提示';
html += '</button>';
html += '<div class="hint-message" id="hintMessage"></div>';
elements.questionCard.innerHTML = html;
}
function updateNavigation(currentId) {
var html = '<button class="nav-btn random" onclick="PoetryQuiz.goToRandomQuestion()">🎲 随机</button>';
var start = Math.max(1, currentId - 4);
var end = currentId + 5;
for (var i = start; i <= end; i++) {
var activeClass = i === currentId ? ' active' : '';
html += '<button class="nav-btn' + activeClass + '" onclick="PoetryQuiz.goToQuestion(' + i + ')">' + i + '</button>';
}
elements.navigation.innerHTML = html;
}
function updateDetails(data) {
elements.detailAuthor.textContent = data.author || '-';
elements.detailDynasty.textContent = data.dynasty || '-';
elements.detailType.textContent = data.type || '-';
elements.detailGrade.textContent = data.grade || '-';
elements.details.style.display = 'grid';
}
function updateStats() {
elements.correctCount.textContent = state.correctCount;
elements.totalCount.textContent = state.totalCount;
}
function selectOption(num) {
if (state.answered || state.isLoading) return;
var buttons = document.querySelectorAll('.option-btn');
buttons.forEach(function(btn) {
btn.classList.remove('selected');
});
var selectedBtn = document.querySelector('.option-btn[data-num="' + num + '"]');
if (selectedBtn) {
selectedBtn.classList.add('selected');
}
state.selectedOption = num;
submitAnswer(state.currentId, num);
}
function markOption(num, type) {
var btn = document.querySelector('.option-btn[data-num="' + num + '"]');
if (btn) {
btn.classList.add(type);
}
}
function clearOptionMark(num) {
var btn = document.querySelector('.option-btn[data-num="' + num + '"]');
if (btn) {
btn.classList.remove('wrong', 'selected');
}
}
function showFeedback(type, message) {
elements.feedback.className = 'feedback show ' + type;
elements.feedbackIcon.textContent = type === 'correct' ? '🎉' : '😢';
elements.feedbackText.textContent = message.replace(/^[🎉😢]\s*/, '');
elements.overlay.classList.add('show');
}
function hideFeedback() {
elements.feedback.classList.remove('show');
elements.overlay.classList.remove('show');
}
function showHintMessage(message) {
var hintEl = document.getElementById('hintMessage');
if (hintEl) {
hintEl.textContent = '💡 ' + message;
hintEl.classList.add('show');
}
}
function goToQuestion(id) {
if (state.isLoading) return;
state.currentId = id;
state.answered = false;
state.selectedOption = null;
setUrlParam('id', id);
fetchQuestion(id);
}
function goToRandomQuestion() {
if (state.isLoading) return;
var randomId = Math.floor(Math.random() * 500) + 1;
goToQuestion(randomId);
}
function retry() {
fetchQuestion(state.currentId);
}
function escapeHtml(text) {
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function init() {
var idParam = getUrlParam('id');
if (idParam) {
state.currentId = parseInt(idParam, 10) || 1;
} else {
state.currentId = Math.floor(Math.random() * 500) + 1;
}
fetchQuestion(state.currentId);
}
window.PoetryQuiz = {
selectOption: selectOption,
getHint: function() { getHint(state.currentId); },
goToQuestion: goToQuestion,
goToRandomQuestion: goToRandomQuestion,
retry: retry
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

375
ht/p1/api.php Normal file
View File

@@ -0,0 +1,375 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
define('SECRET_KEY', 'tzgsc_2026_secret_key');
define('KEY_EXPIRE_TIME', 300);
define('MAX_REQUESTS_NO_KEY', 10);
define('RATE_LIMIT_WINDOW', 60);
function replace_unicode_escape_sequence($match) {
return mb_convert_encoding(pack('H*', $match[1]), 'UTF-8', 'UCS-2BE');
}
function send_json_response($data, $status_code = 200) {
http_response_code($status_code);
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
function error_response($message, $status_code = 400) {
send_json_response([
'success' => false,
'message' => $message
], $status_code);
}
function generate_new_key($timestamp = null) {
if ($timestamp === null) {
$timestamp = time();
}
$data = $timestamp . SECRET_KEY;
return hash('sha256', $data);
}
function verify_key($key, &$new_key = null) {
if (empty($key)) {
return false;
}
$current_time = time();
for ($i = 0; $i <= KEY_EXPIRE_TIME; $i++) {
$expected_key = generate_new_key($current_time - $i);
if (hash_equals($expected_key, $key)) {
$new_key = generate_new_key();
return true;
}
}
return false;
}
function get_client_ip() {
$ip = '';
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
$ip = $_SERVER['REMOTE_ADDR'];
}
return trim($ip);
}
function check_rate_limit($ip) {
$limit_file = "data/rate_limit/" . md5($ip) . ".json";
if (!is_dir("data/rate_limit")) {
mkdir("data/rate_limit", 0755, true);
}
$current_time = time();
$requests = [];
if (file_exists($limit_file)) {
$requests = json_decode(file_get_contents($limit_file), true) ?: [];
}
$requests = array_filter($requests, function($time) use ($current_time) {
return ($current_time - $time) < RATE_LIMIT_WINDOW;
});
return count($requests);
}
function record_request($ip) {
$limit_file = "data/rate_limit/" . md5($ip) . ".json";
if (!is_dir("data/rate_limit")) {
mkdir("data/rate_limit", 0755, true);
}
$current_time = time();
$requests = [];
if (file_exists($limit_file)) {
$requests = json_decode(file_get_contents($limit_file), true) ?: [];
}
$requests[] = $current_time;
$requests = array_filter($requests, function($time) use ($current_time) {
return ($current_time - $time) < RATE_LIMIT_WINDOW;
});
file_put_contents($limit_file, json_encode(array_values($requests)));
}
function get_key_param() {
if (isset($_GET['key'])) {
return $_GET['key'];
}
if (isset($_POST['key'])) {
return $_POST['key'];
}
$input = json_decode(file_get_contents('php://input'), true);
if (isset($input['key'])) {
return $input['key'];
}
return null;
}
$method = $_SERVER['REQUEST_METHOD'];
$key = get_key_param();
$ip = get_client_ip();
$new_key = null;
if (!verify_key($key, $new_key)) {
$request_count = check_rate_limit($ip);
if ($request_count >= MAX_REQUESTS_NO_KEY) {
error_response('请求过于频繁,请稍后再试', 429);
}
record_request($ip);
}
if ($method === 'GET') {
$id = isset($_GET['id']) ? $_GET['id'] : '';
$msg = isset($_GET['msg']) ? $_GET['msg'] : '';
if (empty($id)) {
error_response('缺少必要参数: id');
}
if (empty($msg)) {
$result = get_question($id);
if ($new_key) {
$result['new_key'] = $new_key;
}
send_json_response($result);
} else {
$result = submit_answer($id, $msg);
if ($new_key) {
$result['new_key'] = $new_key;
}
send_json_response($result);
}
} elseif ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
$id = isset($input['id']) ? $input['id'] : '';
$msg = isset($input['msg']) ? $input['msg'] : '';
if (empty($id)) {
error_response('缺少必要参数: id');
}
if (empty($msg)) {
$result = get_question($id);
if ($new_key) {
$result['new_key'] = $new_key;
}
send_json_response($result);
} else {
$result = submit_answer($id, $msg);
if ($new_key) {
$result['new_key'] = $new_key;
}
send_json_response($result);
}
} else {
error_response('不支持的请求方法', 405);
}
function get_question($id) {
$data = get_curl("https://hanyu.baidu.com/hanyu/ajax/pingce_data");
$data = preg_replace_callback('/\\\\u([0-9a-f]{4})/i', 'replace_unicode_escape_sequence', $data);
$cj = cj($data);
$s = preg_match_all('/{"question_content":"(.*?)","type":{"person":"(.*?)","type":"(.*?)","grade":"(.*?)","dynasty":"(.*?)"},"option_answers":\[(.*?)\]}/', $data, $t);
if ($s == 0) {
return [
'success' => false,
'message' => '抱歉,获取出现错误。'
];
}
$tm = $t[1][0];
$z = $t[2][0];
$l = $t[3][0];
$n = $t[4][0];
$nd = $t[5][0];
preg_match_all('/{"answer_content":"(.*?)","is_standard_answer":(.*?)}/', $t[6][0], $d);
$options = [];
$correct_answer = null;
for ($i = 0; $i < 4; $i++) {
$d1 = $d[1][$i];
$p = $d[2][$i];
$option_num = $i + 1;
$options[] = [
'num' => $option_num,
'text' => $d1
];
if ($p == "1") {
$correct_answer = $option_num;
}
}
$question_data = [
'id' => $id,
'question' => $tm,
'options' => $options,
'author' => $z,
'type' => $l,
'grade' => $n,
'dynasty' => $nd,
'correct_answer' => $correct_answer
];
file_put_contents("data/tzgsc/" . $id . ".json", json_encode($question_data, JSON_UNESCAPED_UNICODE));
return [
'success' => true,
'data' => [
'id' => $id,
'question' => $tm,
'options' => $options,
'author' => $z,
'type' => $l,
'grade' => $n,
'dynasty' => $nd
]
];
}
function submit_answer($id, $msg) {
$data_file = "data/tzgsc/" . $id . ".json";
if (!file_exists($data_file)) {
return [
'success' => false,
'message' => '题目数据不存在,请先获取题目'
];
}
$question_data = json_decode(file_get_contents($data_file), true);
if ($msg === "提示") {
return [
'success' => true,
'type' => 'hint',
'message' => '这是首描写' . $question_data['type'] . '的诗,你在' . $question_data['grade'] . '学过它。'
];
}
if ($msg == $question_data['correct_answer']) {
$next_id = $id + 1;
$next_question = get_question($next_id);
return [
'success' => true,
'type' => 'correct',
'message' => '恭喜你,回答正确。请继续下一题',
'next_question' => $next_question['success'] ? $next_question['data'] : null
];
} else {
return [
'success' => true,
'type' => 'wrong',
'message' => '抱歉,答案不对哦。你可以回复提示获取该题的部分信息哦。'
];
}
}
function cj($data) {
if (!$data) return;
$s = preg_match_all('/{"question_content":"(.*?)","type":(.*?)}]}/', $data, $d);
if ($s == 0) return;
$json_file = "data/tzgsc.json";
$existing = [];
if (file_exists($json_file)) {
$content = file_get_contents($json_file);
$existing = json_decode($content, true);
if (!is_array($existing)) {
$existing = [];
}
}
$existing_questions = [];
foreach ($existing as $item) {
if (isset($item['question_content'])) {
$existing_questions[$item['question_content']] = true;
}
}
for ($i = 0; $i < $s; $i++) {
$d1 = $d[1][$i];
$d2 = $d[2][$i];
if (!isset($existing_questions[$d1])) {
$new_item = [
'question_content' => $d1,
'type' => json_decode('{' . $d2, true)
];
if ($new_item['type']) {
$existing[] = $new_item;
$existing_questions[$d1] = true;
}
}
}
file_put_contents($json_file, json_encode($existing, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
}
function get_curl($url, $post = 0, $referer = 1, $cookie = 0, $header = 0, $ua = 0, $nobaody = 0) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$httpheader[] = "Accept:application/json";
$httpheader[] = "Accept-Encoding:gzip,deflate,sdch";
$httpheader[] = "Accept-Language:zh-CN,zh;q=0.8";
$httpheader[] = "Connection:close";
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
if ($post) {
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
}
if ($header) {
curl_setopt($ch, CURLOPT_HEADER, TRUE);
}
if ($cookie) {
curl_setopt($ch, CURLOPT_DICTAPP_MID, $cookie);
}
if ($referer) {
if ($referer == 1) {
curl_setopt($ch, CURLOPT_REFERER, 'http://m.qzone.com/infocenter?g_f=');
} else {
curl_setopt($ch, CURLOPT_REFERER, $referer);
}
}
if ($ua) {
curl_setopt($ch, CURLOPT_USERAGENT, $ua);
} else {
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Linux; U; Android 4.4.1; zh-cn) AppleWebKit/533.1 (KHTML, like Gecko)Version/4.0 MQQBrowser/5.5 Mobile Safari/533.1');
}
if ($nobaody) {
curl_setopt($ch, CURLOPT_NOBODY, 1);
}
curl_setopt($ch, CURLOPT_ENCODING, "gzip");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$ret = curl_exec($ch);
curl_close($ch);
return $ret;
}
?>

5456
ht/p1/data/tzgsc.json Normal file

File diff suppressed because it is too large Load Diff

74
ht/p1/index.php Normal file
View File

@@ -0,0 +1,74 @@
<?php
define('SECRET_KEY', 'tzgsc_2026_secret_key');
function generate_initial_key() {
$timestamp = time();
$data = $timestamp . SECRET_KEY;
return hash('sha256', $data);
}
$initial_key = generate_initial_key();
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>古诗文答题</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<header class="header">
<h1>📜 古诗文答题</h1>
<div class="stats">
<span class="correct">✓ 答对: <strong id="correctCount">0</strong></span>
<span>📝 总题: <strong id="totalCount">0</strong></span>
</div>
</header>
<nav class="navigation" id="navigation">
</nav>
<main class="card" id="questionCard">
<div class="loading" id="loading">加载中</div>
</main>
<section class="details" id="details" style="display: none;">
<div class="detail-item">
<div class="detail-label">作者</div>
<div class="detail-value" id="detailAuthor">-</div>
</div>
<div class="detail-item">
<div class="detail-label">朝代</div>
<div class="detail-value" id="detailDynasty">-</div>
</div>
<div class="detail-item">
<div class="detail-label">类型</div>
<div class="detail-value" id="detailType">-</div>
</div>
<div class="detail-item">
<div class="detail-label">年级</div>
<div class="detail-value" id="detailGrade">-</div>
</div>
</section>
<footer class="footer">
<a href="mailto:developer@example.com?subject=古诗文答题接口咨询" class="contact-btn">
📧 联系开发者获取接口
</a>
</footer>
</div>
<div class="overlay" id="overlay"></div>
<div class="feedback" id="feedback">
<div class="feedback-icon" id="feedbackIcon"></div>
<div class="feedback-text" id="feedbackText"></div>
</div>
<script>
window.INITIAL_KEY = '<?php echo $initial_key; ?>';
</script>
<script src="api.js"></script>
</body>
</html>

469
ht/p1/style.css Normal file
View File

@@ -0,0 +1,469 @@
:root {
--bg-primary: #f5f0e6;
--bg-card: #fffef9;
--text-primary: #5d4e37;
--text-secondary: #8b7355;
--accent: #c94c4c;
--border: #d4c5a9;
--success: #4a7c59;
--error: #c94c4c;
--shadow: rgba(93, 78, 55, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
padding: 20px;
background-image:
radial-gradient(circle at 20% 80%, rgba(201, 76, 76, 0.05) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(74, 124, 89, 0.05) 0%, transparent 50%);
}
.container {
max-width: 600px;
margin: 0 auto;
}
.header {
text-align: center;
padding: 20px 0;
margin-bottom: 20px;
}
.header h1 {
font-family: 'KaiTi', 'STKaiti', '楷体', serif;
font-size: 28px;
font-weight: normal;
color: var(--text-primary);
margin-bottom: 8px;
letter-spacing: 4px;
}
.stats {
display: flex;
justify-content: center;
gap: 20px;
font-size: 14px;
color: var(--text-secondary);
}
.stats span {
display: flex;
align-items: center;
gap: 4px;
}
.stats .correct {
color: var(--success);
}
.navigation {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 20px;
padding: 12px;
background: var(--bg-card);
border-radius: 12px;
border: 1px solid var(--border);
}
.nav-btn {
width: 36px;
height: 36px;
border: 1px solid var(--border);
background: var(--bg-card);
color: var(--text-secondary);
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.nav-btn:hover {
background: var(--bg-primary);
border-color: var(--accent);
color: var(--accent);
}
.nav-btn.active {
background: var(--accent);
color: white;
border-color: var(--accent);
}
.card {
background: var(--bg-card);
border-radius: 16px;
border: 1px solid var(--border);
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 4px 20px var(--shadow);
position: relative;
overflow: hidden;
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--accent), var(--success));
}
.question-number {
position: absolute;
top: 20px;
right: 20px;
font-size: 12px;
color: var(--text-secondary);
background: var(--bg-primary);
padding: 4px 12px;
border-radius: 20px;
}
.question {
font-family: 'KaiTi', 'STKaiti', '楷体', serif;
font-size: 22px;
line-height: 1.8;
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: linear-gradient(135deg, rgba(212, 197, 169, 0.2), transparent);
border-radius: 12px;
border-left: 3px solid var(--accent);
}
.options {
display: flex;
flex-direction: column;
gap: 12px;
}
.option-btn {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
background: var(--bg-card);
border: 2px solid var(--border);
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
text-align: left;
font-size: 16px;
color: var(--text-primary);
}
.option-btn:hover {
border-color: var(--accent);
transform: translateX(4px);
box-shadow: 0 2px 10px var(--shadow);
}
.option-btn.selected {
border-color: var(--accent);
background: rgba(201, 76, 76, 0.05);
}
.option-btn.correct {
border-color: var(--success);
background: rgba(74, 124, 89, 0.1);
}
.option-btn.wrong {
border-color: var(--error);
background: rgba(201, 76, 76, 0.1);
}
.option-num {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-primary);
border-radius: 6px;
font-weight: bold;
font-size: 14px;
color: var(--text-secondary);
flex-shrink: 0;
}
.option-btn:hover .option-num {
background: var(--accent);
color: white;
}
.option-btn.selected .option-num {
background: var(--accent);
color: white;
}
.option-btn.correct .option-num {
background: var(--success);
color: white;
}
.option-btn.wrong .option-num {
background: var(--error);
color: white;
}
.hint-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 20px;
padding: 12px;
background: transparent;
border: 1px dashed var(--border);
border-radius: 12px;
cursor: pointer;
color: var(--text-secondary);
font-size: 14px;
transition: all 0.3s ease;
width: 100%;
}
.hint-btn:hover {
border-color: var(--accent);
color: var(--accent);
background: rgba(201, 76, 76, 0.05);
}
.details {
background: var(--bg-card);
border-radius: 12px;
border: 1px solid var(--border);
padding: 20px;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.detail-item {
text-align: center;
padding: 12px;
background: var(--bg-primary);
border-radius: 8px;
}
.detail-label {
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 4px;
}
.detail-value {
font-size: 16px;
color: var(--text-primary);
font-weight: 500;
}
.feedback {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.8);
background: var(--bg-card);
border-radius: 16px;
padding: 30px 50px;
text-align: center;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.feedback.show {
opacity: 1;
visibility: visible;
transform: translate(-50%, -50%) scale(1);
}
.feedback.correct {
border: 2px solid var(--success);
}
.feedback.wrong {
border: 2px solid var(--error);
}
.feedback-icon {
font-size: 48px;
margin-bottom: 12px;
}
.feedback-text {
font-size: 18px;
color: var(--text-primary);
}
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 999;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.overlay.show {
opacity: 1;
visibility: visible;
}
.hint-message {
background: var(--bg-card);
border-radius: 12px;
border: 1px solid var(--accent);
padding: 16px;
margin-top: 16px;
font-size: 14px;
color: var(--text-primary);
line-height: 1.6;
display: none;
}
.hint-message.show {
display: block;
animation: fadeIn 0.3s ease;
}
.loading {
text-align: center;
padding: 40px;
color: var(--text-secondary);
}
.loading::after {
content: '';
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-left: 10px;
vertical-align: middle;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.error-message {
text-align: center;
padding: 20px;
color: var(--error);
}
.retry-btn {
display: inline-flex;
align-items: center;
gap: 8px;
margin-top: 12px;
padding: 10px 20px;
background: var(--accent);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.retry-btn:hover {
opacity: 0.9;
transform: translateY(-2px);
}
.nav-btn.random {
background: linear-gradient(135deg, var(--accent), #e67e22);
color: white;
border: none;
min-width: 60px;
}
.nav-btn.random:hover {
transform: scale(1.05);
box-shadow: 0 2px 10px rgba(201, 76, 76, 0.3);
}
.footer {
text-align: center;
padding: 20px;
margin-top: 20px;
}
.contact-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
background: linear-gradient(135deg, var(--accent), #e67e22);
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
text-decoration: none;
}
.contact-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(201, 76, 76, 0.4);
}
@media (max-width: 480px) {
body {
padding: 12px;
}
.header h1 {
font-size: 24px;
}
.card {
padding: 20px;
}
.question {
font-size: 18px;
padding: 16px;
}
.details {
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.detail-item {
padding: 10px;
}
}

97
ht/p1/syx.php Normal file
View File

@@ -0,0 +1,97 @@
<?php
header("Content-Type:text/html;charset=UTF-8");
function replace_unicode_escape_sequence($match){
return mb_convert_encoding(pack('H*', $match[1]), 'UTF-8', 'UCS-2BE');}
if($_GET["msg"]==""){
echo get($_GET["id"]);
}else{
$data=file_get_contents("data/tzgsc/".$_GET["id"].".txt");
preg_match_all('/tm(.*?)z(.*?)l(.*?)n(.*?)nian(.*?)d(.*?)d/',$data,$t);
if($_GET["msg"]==$t[6][0]){
echo "恭喜你,回答正确。\n请继续下一题\n\n";
exit(get($id));}else{
if($_GET["msg"]=="提示"){
exit("这是首描写".$t[3][0]."的诗,你在".$t[4][0]."学过它。");}
exit("抱歉,答案不对哦。\n你可以回复提示获取该题的部分信息哦。");}
}
//http://poe.pmmu.cn/tzgsc.php?msg=1
function get($id){
$data=get_curl("https://hanyu.baidu.com/hanyu/ajax/pingce_data");
$data = preg_replace_callback('/\\\\u([0-9a-f]{4})/i', 'replace_unicode_escape_sequence', $data);
//exit($data);
$cj=cj($data);
$s = preg_match_all('/{"question_content":"(.*?)","type":{"person":"(.*?)","type":"(.*?)","grade":"(.*?)","dynasty":"(.*?)"},"option_answers":\[(.*?)\]}/',$data,$t);
if($s==0){exit("抱歉,获取出现错误。");}
$tm=$t[1][0];//题目
$z=$t[2][0];//作者
$l=$t[3][0];//描写类型
$n=$t[4][0];//学习阶段
$nd=$t[5][0];//年代
echo $id."如题:".$tm."\n请从下面四个选项中选择一个你认为对的来回答。\n\n";
preg_match_all('/{"answer_content":"(.*?)","is_standard_answer":(.*?)}/',$t[6][0],$d);
$ps="tm".$tm."z".$z."l".$l."n".$n."nian".$nd;
for( $i = 0 ; $i < 4 ; $i ++ ){
$d1=$d[1][$i];
$p=$d[2][$i];
if($p=="1"){file_put_contents("data/tzgsc/".$id.".txt",$ps."d".($i+1)."d");
}
echo ($i+1)."".$d1."\n";
}
}
function cj($data){
if(!$data)exit("");//无数据返回
$s=preg_match_all('/{"question_content":"(.*?)","type":(.*?)}]}/',$data,$d);
$json="data/tzgsc/tzgsc.json";
@$pd = file_get_contents($json);
for( $i = 0 ; $i < $s ; $i ++ ){
$d1=$d[1][$i];
$d2=$d[2][$i];
$a='{"question_content":"'.$d1.'","type":'.$d2.'}]}';
$p = explode($d1,$pd);
if(count($p)>1){
//存在不写入
}else{
file_put_contents($json, $a, FILE_APPEND | LOCK_EX);//追加写入,防止同时写入。
//不存在,继续写入。
}
}}
function get_curl($url,$post=0,$referer=1,$cookie=0,$header=0,$ua=0,$nobaody=0){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,$url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$httpheader[] = "Accept:application/json";
$httpheader[] = "Accept-Encoding:gzip,deflate,sdch";
$httpheader[] = "Accept-Language:zh-CN,zh;q=0.8";
$httpheader[] = "Connection:close";
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
if($post){
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);}
if($header){
curl_setopt($ch, CURLOPT_HEADER, TRUE);}
if($cookie){
curl_setopt($ch, CURLOPT_DICTAPP_MID, $cookie);}
if($referer){
if($referer==1){
curl_setopt($ch, CURLOPT_REFERER, 'http://m.qzone.com/infocenter?g_f=');
}else{
curl_setopt($ch, CURLOPT_REFERER, $referer);}}
if($ua){
curl_setopt($ch, CURLOPT_USERAGENT,$ua);
}else{
curl_setopt($ch, CURLOPT_USERAGENT,'Mozilla/5.0 (Linux; U; Android 4.4.1; zh-cn) AppleWebKit/533.1 (KHTML, like Gecko)Version/4.0 MQQBrowser/5.5 Mobile Safari/533.1');}
if($nobaody){
curl_setopt($ch, CURLOPT_NOBODY,1);}
curl_setopt($ch, CURLOPT_ENCODING, "gzip");
curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
$ret = curl_exec($ch);
curl_close($ch);
return $ret;}
?>