refactor: 更新URL哈希处理逻辑 feat: 添加聊天消息存储支持 docs: 更新API控制器基类文档 chore: 删除无用脚本文件 fix: 修复分类模型返回类型问题 feat: 添加回执登录功能 build: 更新依赖项配置 style: 统一HTML模板中的哈希ID引用格式 ci: 添加部署和检查脚本
600 lines
19 KiB
Dart
600 lines
19 KiB
Dart
// ============================================================
|
||
// 闲言APP — UI增强相关API接口验证脚本
|
||
// 创建时间: 2026-04-30
|
||
// 更新时间: 2026-04-30
|
||
// 作用: 验证成就/打卡/查重/金币/公开主页/文章/仪表盘/统计等接口
|
||
// 上次更新: 初始创建,覆盖10个页面所需的全部API
|
||
// ============================================================
|
||
|
||
import 'dart:convert';
|
||
import 'package:dio/dio.dart';
|
||
import 'package:crypto/crypto.dart';
|
||
|
||
const String baseUrl = 'https://tools.wktyl.com';
|
||
const String receiptSecret = 'Xy7kP9mL2qR4wS8v';
|
||
const String testAccount = 'apitest_user';
|
||
const String testPassword = '123456';
|
||
|
||
String? authToken;
|
||
int passCount = 0;
|
||
int failCount = 0;
|
||
final List<String> results = [];
|
||
|
||
final dio = Dio(
|
||
BaseOptions(
|
||
baseUrl: baseUrl,
|
||
connectTimeout: const Duration(seconds: 30),
|
||
receiveTimeout: const Duration(seconds: 30),
|
||
headers: {'Accept': 'application/json'},
|
||
),
|
||
);
|
||
|
||
void setToken(String? t) {
|
||
authToken = t;
|
||
if (t != null && t.isNotEmpty) {
|
||
dio.options.headers['token'] = t;
|
||
} else {
|
||
dio.options.headers.remove('token');
|
||
}
|
||
}
|
||
|
||
Map<String, String> generateReceipt(String action, String payloadStr) {
|
||
final payloadDigest = sha256
|
||
.convert(utf8.encode(payloadStr))
|
||
.toString()
|
||
.substring(0, 16);
|
||
final data = {
|
||
'action': action,
|
||
'payload': payloadDigest,
|
||
'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||
'nonce': List.generate(8, (_) => DateTime.now().microsecondsSinceEpoch.toRadixString(16)).join().substring(0, 8),
|
||
};
|
||
final receipt = base64Encode(utf8.encode(jsonEncode(data)));
|
||
final sig = Hmac(sha256, utf8.encode(receiptSecret))
|
||
.convert(utf8.encode(receipt))
|
||
.toString();
|
||
return {'receipt': receipt, 'sig': sig};
|
||
}
|
||
|
||
Future<Map<String, dynamic>> post(String path, {Map<String, dynamic>? data}) async {
|
||
final resp = await dio.post<Map<String, dynamic>>(path, data: data);
|
||
return resp.data ?? {};
|
||
}
|
||
|
||
Future<Map<String, dynamic>> get(String path, {Map<String, dynamic>? params}) async {
|
||
final resp = await dio.get<Map<String, dynamic>>(path, queryParameters: params);
|
||
return resp.data ?? {};
|
||
}
|
||
|
||
void log(String name, bool passed, [String detail = '']) {
|
||
final icon = passed ? '✅' : '❌';
|
||
final msg = '$icon $name${detail.isNotEmpty ? ' — $detail' : ''}';
|
||
results.add(msg);
|
||
print(msg);
|
||
if (passed) passCount++; else failCount++;
|
||
}
|
||
|
||
// ============================================================
|
||
// 登录获取Token
|
||
// ============================================================
|
||
Future<void> testLogin() async {
|
||
print('\n🔑 登录获取Token');
|
||
try {
|
||
final resp = await post('/api/user_security/login', data: {
|
||
'account': testAccount,
|
||
'password': testPassword,
|
||
});
|
||
final code = resp['code'] as int? ?? 0;
|
||
if (code == 1) {
|
||
final data = resp['data'] as Map<String, dynamic>?;
|
||
final userinfo = data?['userinfo'] as Map<String, dynamic>?;
|
||
final token = userinfo?['token'] as String? ?? data?['token'] as String? ?? '';
|
||
setToken(token);
|
||
log('登录', token.isNotEmpty, 'token=${token.substring(0, 20)}...');
|
||
} else {
|
||
log('登录', false, 'code=$code, msg=${resp['msg']}');
|
||
}
|
||
} catch (e) {
|
||
log('登录', false, e.toString());
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 模块6: 成就与激励体系
|
||
// ============================================================
|
||
Future<void> testAchievementAPIs() async {
|
||
print('\n🏆 成就与激励体系');
|
||
|
||
// 成就列表
|
||
try {
|
||
final resp = await get('/api/achievement/list', params: {'type': 'signin'});
|
||
final code = resp['code'] as int? ?? 0;
|
||
final data = resp['data'] as List?;
|
||
log('成就列表(signin)', code == 1, 'count=${data?.length ?? 0}');
|
||
} catch (e) {
|
||
log('成就列表', false, e.toString());
|
||
}
|
||
|
||
// 我的成就
|
||
try {
|
||
final resp = await get('/api/achievement/my');
|
||
final code = resp['code'] as int? ?? 0;
|
||
final data = resp['data'] as Map<String, dynamic>?;
|
||
log('我的成就', code == 1, 'achieved=${data?['achieved']?.length}, unachieved=${data?['unachieved']?.length}');
|
||
} catch (e) {
|
||
log('我的成就', false, e.toString());
|
||
}
|
||
|
||
// 学习打卡
|
||
try {
|
||
final resp = await post('/api/achievement/checkin', data: {'type': 'poetry'});
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('学习打卡(poetry)', code == 1 || (resp['msg'] as String?)?.contains('已打卡') == true,
|
||
'code=$code, msg=${resp['msg']}');
|
||
} catch (e) {
|
||
log('学习打卡', false, e.toString());
|
||
}
|
||
|
||
// 每日签到
|
||
try {
|
||
final resp = await post('/api/user_center/signin');
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('每日签到', code == 1 || (resp['msg'] as String?)?.contains('已签到') == true,
|
||
'code=$code, msg=${resp['msg']}');
|
||
} catch (e) {
|
||
log('每日签到', false, e.toString());
|
||
}
|
||
|
||
// 签到日历
|
||
try {
|
||
final resp = await get('/api/user_center/signin_calendar', params: {'month': '2026-04'});
|
||
final code = resp['code'] as int? ?? 0;
|
||
final data = resp['data'] as Map<String, dynamic>?;
|
||
log('签到日历', code == 1, 'keys=${data?.keys.toList()}');
|
||
} catch (e) {
|
||
log('签到日历', false, e.toString());
|
||
}
|
||
|
||
// 补签
|
||
try {
|
||
final r = generateReceipt('signin_makeup', '2026-04-28');
|
||
final resp = await post('/api/user_center/signin_makeup', data: {
|
||
'date': '2026-04-28',
|
||
'receipt': r['receipt'],
|
||
'sig': r['sig'],
|
||
});
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('补签', code == 1 || (resp['msg'] as String?)?.contains('已签到') == true,
|
||
'code=$code, msg=${resp['msg']}');
|
||
} catch (e) {
|
||
log('补签', false, e.toString());
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 模块13: 内容查重
|
||
// ============================================================
|
||
Future<void> testCheckAPIs() async {
|
||
print('\n🔍 内容查重');
|
||
|
||
// 数据源列表
|
||
try {
|
||
final resp = await get('/api/check/sources');
|
||
final code = resp['code'] as int? ?? 0;
|
||
final data = resp['data'] as List?;
|
||
log('数据源列表', code == 1, 'count=${data?.length ?? 0}');
|
||
} catch (e) {
|
||
log('数据源列表', false, e.toString());
|
||
}
|
||
|
||
// 精确查重
|
||
try {
|
||
final resp = await post('/api/check/exact', data: {
|
||
'text': '君不见黄河之水天上来',
|
||
'type': 'poetry',
|
||
});
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('精确查重', code == 1, 'matches=${(resp['data'] as Map?)?['matches']}');
|
||
} catch (e) {
|
||
log('精确查重', false, e.toString());
|
||
}
|
||
|
||
// 模糊查重
|
||
try {
|
||
final resp = await post('/api/check/fuzzy', data: {
|
||
'text': '黄河之水天上来奔流到海不复回',
|
||
});
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('模糊查重', code == 1, 'code=$code');
|
||
} catch (e) {
|
||
log('模糊查重', false, e.toString());
|
||
}
|
||
|
||
// 相似度查重
|
||
try {
|
||
final resp = await post('/api/check/similar', data: {
|
||
'text': '床前明月光疑是地上霜',
|
||
'limit': 5,
|
||
});
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('相似度查重', code == 1, 'code=$code');
|
||
} catch (e) {
|
||
log('相似度查重', false, e.toString());
|
||
}
|
||
|
||
// 综合报告
|
||
try {
|
||
final resp = await post('/api/check/report', data: {
|
||
'text': '白日依山尽黄河入海流',
|
||
});
|
||
final code = resp['code'] as int? ?? 0;
|
||
final data = resp['data'] as Map<String, dynamic>?;
|
||
log('综合报告', code == 1,
|
||
'risk=${data?['risk_level']}, score=${data?['risk_score']}, max_sim=${data?['max_similarity']}');
|
||
} catch (e) {
|
||
log('综合报告', false, e.toString());
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 金币记录
|
||
// ============================================================
|
||
Future<void> testCoinAPIs() async {
|
||
print('\n💰 金币记录');
|
||
|
||
try {
|
||
final resp = await get('/api/user_center/coin', params: {'page': 1, 'limit': 10});
|
||
final code = resp['code'] as int? ?? 0;
|
||
final data = resp['data'] as Map<String, dynamic>?;
|
||
final list = data?['list'] as List?;
|
||
log('金币记录', code == 1, 'count=${list?.length ?? 0}');
|
||
} catch (e) {
|
||
log('金币记录', false, e.toString());
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 公开主页
|
||
// ============================================================
|
||
Future<void> testPublicProfileAPIs() async {
|
||
print('\n👤 公开主页');
|
||
|
||
try {
|
||
final resp = await get('/api/user_center/public_profile', params: {'uid': 39});
|
||
final code = resp['code'] as int? ?? 0;
|
||
final data = resp['data'] as Map<String, dynamic>?;
|
||
log('公开主页', code == 1,
|
||
'name=${data?['display_name']}, title=${data?['title']}, score=${data?['score']}');
|
||
} catch (e) {
|
||
log('公开主页', false, e.toString());
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 文章模块
|
||
// ============================================================
|
||
Future<void> testArticleAPIs() async {
|
||
print('\n📝 文章模块');
|
||
|
||
// 文章列表
|
||
try {
|
||
final resp = await get('/api/article/list', params: {'page': 1, 'sort': 'newst'});
|
||
final code = resp['code'] as int? ?? 0;
|
||
final data = resp['data'] as Map<String, dynamic>?;
|
||
final list = data?['list'] as List?;
|
||
log('文章列表', code == 1, 'count=${list?.length ?? 0}');
|
||
} catch (e) {
|
||
log('文章列表', false, e.toString());
|
||
}
|
||
|
||
// 文章详情
|
||
try {
|
||
final resp = await get('/api/article/detail', params: {'id': 1});
|
||
final code = resp['code'] as int? ?? 0;
|
||
final data = resp['data'] as Map<String, dynamic>?;
|
||
log('文章详情', code == 1, 'title=${data?['title']}');
|
||
} catch (e) {
|
||
log('文章详情', false, e.toString());
|
||
}
|
||
|
||
// 我的文章
|
||
try {
|
||
final resp = await get('/api/article/mine', params: {'page': 1});
|
||
final code = resp['code'] as int? ?? 0;
|
||
final data = resp['data'] as Map<String, dynamic>?;
|
||
final list = data?['list'] as List?;
|
||
log('我的文章', code == 1, 'count=${list?.length ?? 0}');
|
||
} catch (e) {
|
||
log('我的文章', false, e.toString());
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 仪表盘 + 学习中心
|
||
// ============================================================
|
||
Future<void> testDashboardAPIs() async {
|
||
print('\n📚 学习中心');
|
||
|
||
// 仪表盘
|
||
try {
|
||
final resp = await get('/api/user_center/dashboard');
|
||
final code = resp['code'] as int? ?? 0;
|
||
final data = resp['data'] as Map<String, dynamic>?;
|
||
log('仪表盘', code == 1,
|
||
'score=${data?['score']}, signin=${data?['signin_days']}, fav=${data?['favorite_count']}, note=${data?['note_count']}');
|
||
} catch (e) {
|
||
log('仪表盘', false, e.toString());
|
||
}
|
||
|
||
// 学习统计 - overview
|
||
try {
|
||
final resp = await get('/api/user_center/stats', params: {'type': 'overview'});
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('学习统计(overview)', code == 1, 'code=$code');
|
||
} catch (e) {
|
||
log('学习统计(overview)', false, e.toString());
|
||
}
|
||
|
||
// 学习统计 - category
|
||
try {
|
||
final resp = await get('/api/user_center/stats', params: {'type': 'category'});
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('学习统计(category)', code == 1, 'code=$code');
|
||
} catch (e) {
|
||
log('学习统计(category)', false, e.toString());
|
||
}
|
||
|
||
// 学习统计 - trend
|
||
try {
|
||
final resp = await get('/api/user_center/stats', params: {'type': 'trend'});
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('学习统计(trend)', code == 1, 'code=$code');
|
||
} catch (e) {
|
||
log('学习统计(trend)', false, e.toString());
|
||
}
|
||
|
||
// 热力图
|
||
try {
|
||
final resp = await get('/api/user_center/heatmap', params: {'year': 2026});
|
||
final code = resp['code'] as int? ?? 0;
|
||
final data = resp['data'] as Map<String, dynamic>?;
|
||
log('热力图', code == 1, 'keys=${data?.keys.take(5).toList()}');
|
||
} catch (e) {
|
||
log('热力图', false, e.toString());
|
||
}
|
||
|
||
// 每日推荐
|
||
try {
|
||
final resp = await get('/api/daily/recommend');
|
||
final code = resp['code'] as int? ?? 0;
|
||
final data = resp['data'] as Map<String, dynamic>?;
|
||
log('每日推荐', code == 1,
|
||
'poetry=${(data?['poetry'] as Map?)?['title']}, wisdom=${(data?['wisdom'] as Map?)?['author']}');
|
||
} catch (e) {
|
||
log('每日推荐', false, e.toString());
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 统计模块
|
||
// ============================================================
|
||
Future<void> testStatisticsAPIs() async {
|
||
print('\n📊 数据统计');
|
||
|
||
// 站点总览
|
||
try {
|
||
final resp = await get('/api/webapi/stats_overview');
|
||
final code = resp['code'] as int? ?? 0;
|
||
final data = resp['data'] as Map<String, dynamic>?;
|
||
log('站点总览', code == 1,
|
||
'users=${data?['total_users']}, articles=${data?['total_articles']}');
|
||
} catch (e) {
|
||
log('站点总览', false, e.toString());
|
||
}
|
||
|
||
// 热门工具
|
||
try {
|
||
final resp = await get('/api/webapi/stats_hot_tools', params: {'limit': 10});
|
||
final code = resp['code'] as int? ?? 0;
|
||
final data = resp['data'] as List?;
|
||
log('热门工具', code == 1, 'count=${data?.length ?? 0}');
|
||
} catch (e) {
|
||
log('热门工具', false, e.toString());
|
||
}
|
||
|
||
// 热门文章
|
||
try {
|
||
final resp = await get('/api/webapi/stats_hot_articles', params: {'limit': 10});
|
||
final code = resp['code'] as int? ?? 0;
|
||
final data = resp['data'] as List?;
|
||
log('热门文章', code == 1, 'count=${data?.length ?? 0}');
|
||
} catch (e) {
|
||
log('热门文章', false, e.toString());
|
||
}
|
||
|
||
// 内容统计
|
||
try {
|
||
final resp = await get('/api/webapi/stats_content');
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('内容统计', code == 1, 'code=$code');
|
||
} catch (e) {
|
||
log('内容统计', false, e.toString());
|
||
}
|
||
|
||
// 用户活跃
|
||
try {
|
||
final resp = await get('/api/webapi/stats_user_activity');
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('用户活跃', code == 1, 'code=$code');
|
||
} catch (e) {
|
||
log('用户活跃', false, e.toString());
|
||
}
|
||
|
||
// 趋势数据7天
|
||
try {
|
||
final resp = await get('/api/webapi/stats_trend', params: {'days': 7});
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('趋势数据(7天)', code == 1, 'code=$code');
|
||
} catch (e) {
|
||
log('趋势数据(7天)', false, e.toString());
|
||
}
|
||
|
||
// 趋势数据30天
|
||
try {
|
||
final resp = await get('/api/webapi/stats_trend', params: {'days': 30});
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('趋势数据(30天)', code == 1, 'code=$code');
|
||
} catch (e) {
|
||
log('趋势数据(30天)', false, e.toString());
|
||
}
|
||
|
||
// API调用统计
|
||
try {
|
||
final resp = await get('/api/webapi/stats_api_usage', params: {'days': 7});
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('API调用统计', code == 1, 'code=$code');
|
||
} catch (e) {
|
||
log('API调用统计', false, e.toString());
|
||
}
|
||
|
||
// 站点统计 - overview
|
||
try {
|
||
final resp = await get('/api/statistics/overview');
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('站点统计(overview)', code == 1, 'code=$code');
|
||
} catch (e) {
|
||
log('站点统计(overview)', false, e.toString());
|
||
}
|
||
|
||
// 签到统计
|
||
try {
|
||
final resp = await get('/api/statistics/signins');
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('签到统计', code == 1, 'code=$code');
|
||
} catch (e) {
|
||
log('签到统计', false, e.toString());
|
||
}
|
||
|
||
// 金币统计
|
||
try {
|
||
final resp = await get('/api/statistics/coins');
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('金币统计', code == 1, 'code=$code');
|
||
} catch (e) {
|
||
log('金币统计', false, e.toString());
|
||
}
|
||
|
||
// 笔记统计
|
||
try {
|
||
final resp = await get('/api/statistics/notes');
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('笔记统计', code == 1, 'code=$code');
|
||
} catch (e) {
|
||
log('笔记统计', false, e.toString());
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// Feed互动
|
||
// ============================================================
|
||
Future<void> testFeedInteractionAPIs() async {
|
||
print('\n💬 Feed互动');
|
||
|
||
// 收藏列表
|
||
try {
|
||
final resp = await get('/api/feed/favorites', params: {'limit': 5});
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('收藏列表', code == 1, 'code=$code');
|
||
} catch (e) {
|
||
log('收藏列表', false, e.toString());
|
||
}
|
||
|
||
// 点赞列表
|
||
try {
|
||
final resp = await get('/api/feed/likes', params: {'limit': 5});
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('点赞列表', code == 1, 'code=$code');
|
||
} catch (e) {
|
||
log('点赞列表', false, e.toString());
|
||
}
|
||
|
||
// 浏览历史
|
||
try {
|
||
final resp = await get('/api/feed/history', params: {'limit': 5});
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('浏览历史', code == 1, 'code=$code');
|
||
} catch (e) {
|
||
log('浏览历史', false, e.toString());
|
||
}
|
||
|
||
// 稍后读
|
||
try {
|
||
final resp = await get('/api/feed/readlater', params: {'limit': 5});
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('稍后读列表', code == 1, 'code=$code');
|
||
} catch (e) {
|
||
log('稍后读列表', false, e.toString());
|
||
}
|
||
|
||
// 互动操作 - like
|
||
try {
|
||
final resp = await post('/api/feed/action', data: {
|
||
'action': 'like',
|
||
'feed_type': 'poetry',
|
||
'feed_id': 1,
|
||
});
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('互动-点赞', code == 1, 'code=$code, msg=${resp['msg']}');
|
||
} catch (e) {
|
||
log('互动-点赞', false, e.toString());
|
||
}
|
||
|
||
// 互动操作 - counts
|
||
try {
|
||
final resp = await post('/api/feed/action', data: {
|
||
'action': 'counts',
|
||
'feed_type': 'poetry',
|
||
'feed_id': 1,
|
||
});
|
||
final code = resp['code'] as int? ?? 0;
|
||
log('互动-统计', code == 1, 'code=$code');
|
||
} catch (e) {
|
||
log('互动-统计', false, e.toString());
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 主函数
|
||
// ============================================================
|
||
Future<void> main() async {
|
||
print('╔══════════════════════════════════════════╗');
|
||
print('║ 闲言APP — UI增强API接口验证 ║');
|
||
print('║ 覆盖: 成就/打卡/查重/金币/主页/文章/ ║');
|
||
print('║ 仪表盘/统计/Feed互动 ║');
|
||
print('╚══════════════════════════════════════════╝');
|
||
|
||
await testLogin();
|
||
|
||
if (authToken == null || authToken!.isEmpty) {
|
||
print('\n❌ 登录失败,终止测试');
|
||
return;
|
||
}
|
||
|
||
await testAchievementAPIs();
|
||
await testCheckAPIs();
|
||
await testCoinAPIs();
|
||
await testPublicProfileAPIs();
|
||
await testArticleAPIs();
|
||
await testDashboardAPIs();
|
||
await testStatisticsAPIs();
|
||
await testFeedInteractionAPIs();
|
||
|
||
print('\n╔══════════════════════════════════════════╗');
|
||
print('║ 测试结果汇总 ║');
|
||
print('╚══════════════════════════════════════════╝');
|
||
print('✅ 通过: $passCount');
|
||
print('❌ 失败: $failCount');
|
||
print('📊 总计: ${passCount + failCount}');
|
||
print('📈 通过率: ${((passCount / (passCount + failCount)) * 100).toStringAsFixed(1)}%');
|
||
}
|