353 lines
9.9 KiB
Dart
353 lines
9.9 KiB
Dart
// 2026-04-17 | test_kitchen_api.dart | 点餐助手API接口测试 | 验证kitchen.php CRUD + SSE
|
||
// 运行: dart run scripts/test_kitchen_api.dart
|
||
|
||
import 'dart:convert';
|
||
import 'dart:io';
|
||
|
||
const String baseUrl = 'https://eat.wktyl.com/api/kitchen';
|
||
|
||
String? createdOrderId;
|
||
String? createdOrderNo;
|
||
|
||
int _curlCounter = 0;
|
||
|
||
Future<String> _curl(
|
||
String method,
|
||
String url, {
|
||
String? body,
|
||
Map<String, String>? headers,
|
||
}) async {
|
||
_curlCounter++;
|
||
final tmpFile = File(
|
||
'${Directory.systemTemp.path}/kitchen_test_$_curlCounter.json',
|
||
);
|
||
final args = [
|
||
'-sS',
|
||
'-X',
|
||
method,
|
||
'--max-time',
|
||
'15',
|
||
'--compressed',
|
||
'-o',
|
||
tmpFile.path,
|
||
];
|
||
if (headers != null) {
|
||
headers.forEach((k, v) {
|
||
args.addAll(['-H', '$k: $v']);
|
||
});
|
||
}
|
||
if (body != null) {
|
||
args.addAll([
|
||
'-H',
|
||
'Content-Type: application/json; charset=utf-8',
|
||
'-d',
|
||
body,
|
||
]);
|
||
}
|
||
args.add(url);
|
||
|
||
final result = await Process.run('curl.exe', args);
|
||
final stderr = (result.stderr as String).trim();
|
||
if (!tmpFile.existsSync()) {
|
||
throw Exception('curl无输出: $stderr');
|
||
}
|
||
final bytes = tmpFile.readAsBytesSync();
|
||
try {
|
||
tmpFile.deleteSync();
|
||
} catch (_) {}
|
||
if (bytes.isEmpty && stderr.isNotEmpty) {
|
||
throw Exception('curl error: $stderr');
|
||
}
|
||
final output = utf8.decode(bytes, allowMalformed: true);
|
||
return _extractJson(output);
|
||
}
|
||
|
||
String _extractJson(String raw) {
|
||
var s = raw.trim();
|
||
final jsonStart = s.indexOf('{');
|
||
if (jsonStart > 0) {
|
||
s = s.substring(jsonStart);
|
||
}
|
||
final jsonEnd = s.lastIndexOf('}');
|
||
if (jsonEnd >= 0 && jsonEnd < s.length - 1) {
|
||
s = s.substring(0, jsonEnd + 1);
|
||
}
|
||
return s;
|
||
}
|
||
|
||
Future<void> main() async {
|
||
print('╔══════════════════════════════════════════════════╗');
|
||
print('║ 🍽️ 点餐助手 API 接口测试 ║');
|
||
print('║ 目标: $baseUrl ║');
|
||
print('╚══════════════════════════════════════════════════╝\n');
|
||
|
||
print('━━━ 1. 接口首页 (index) ━━━');
|
||
await testGet({'act': 'index'});
|
||
|
||
print('\n━━━ 2. CORS预检 (OPTIONS) ━━━');
|
||
await testOptions();
|
||
|
||
print('\n━━━ 3. 创建点单 (POST JSON body) ━━━');
|
||
await testCreateOrder();
|
||
|
||
if (createdOrderId == null) {
|
||
print('\n❌ 创建点单失败,后续测试跳过');
|
||
return;
|
||
}
|
||
|
||
print('\n━━━ 4. 获取点单 (GET ?act=get&id=xxx) ━━━');
|
||
await testGet({'act': 'get', 'id': createdOrderId!});
|
||
|
||
print('\n━━━ 5. 更新点单 (POST ?act=update) ━━━');
|
||
await testUpdateOrder();
|
||
|
||
print('\n━━━ 6. 点单列表 (GET ?act=list) ━━━');
|
||
await testGet({'act': 'list', 'page': '1', 'limit': '5'});
|
||
|
||
print('\n━━━ 7. 统计信息 (GET ?act=stats) ━━━');
|
||
await testGet({'act': 'stats'});
|
||
|
||
print('\n━━━ 8. SSE连接测试 ━━━');
|
||
await testSSE();
|
||
|
||
print('\n━━━ 9. 清理过期数据 (GET ?act=cleanup&days=999) ━━━');
|
||
await testGet({'act': 'cleanup', 'days': '999'});
|
||
|
||
print('\n━━━ 10. 删除点单 (GET ?act=delete&id=xxx) ━━━');
|
||
await testGet({'act': 'delete', 'id': createdOrderId!});
|
||
|
||
print('\n━━━ 11. 确认删除 - 再次获取 ━━━');
|
||
await testGet({'act': 'get', 'id': createdOrderId!});
|
||
|
||
print('\n╔══════════════════════════════════════════════════╗');
|
||
print('║ ✅ 全部测试完成 ║');
|
||
print('╚══════════════════════════════════════════════════╝');
|
||
}
|
||
|
||
Future<void> testGet(Map<String, String> params) async {
|
||
final qs = params.entries
|
||
.map((e) => '${e.key}=${Uri.encodeComponent(e.value)}')
|
||
.join('&');
|
||
final url = '$baseUrl/kitchen.php?$qs';
|
||
print(' GET $url');
|
||
try {
|
||
final output = await _curl(
|
||
'GET',
|
||
url,
|
||
headers: {'Accept': 'application/json'},
|
||
);
|
||
_printResult(output);
|
||
} catch (e) {
|
||
print(' ❌ 请求失败: $e');
|
||
}
|
||
}
|
||
|
||
Future<void> testOptions() async {
|
||
final url = '$baseUrl/kitchen.php';
|
||
print(' OPTIONS $url');
|
||
try {
|
||
final args = [
|
||
'-sS',
|
||
'-X',
|
||
'OPTIONS',
|
||
'-o',
|
||
'NUL',
|
||
'-w',
|
||
'%{http_code}\\n%header{Access-Control-Allow-Origin}\\n%header{Access-Control-Allow-Methods}',
|
||
'--max-time',
|
||
'15',
|
||
url,
|
||
];
|
||
final result = await Process.run('curl.exe', args);
|
||
final output = (result.stdout as String).trim();
|
||
final lines = output.split('\n');
|
||
final statusCode = lines.isNotEmpty ? lines[0].trim() : '???';
|
||
print(' 状态码: $statusCode');
|
||
if (lines.length > 1) print(' Allow-Origin: ${lines[1].trim()}');
|
||
if (lines.length > 2) print(' Allow-Methods: ${lines[2].trim()}');
|
||
if (statusCode == '204') {
|
||
print(' ✅ CORS预检通过');
|
||
} else {
|
||
print(' ⚠️ 预期204,实际$statusCode');
|
||
}
|
||
} catch (e) {
|
||
print(' ❌ 请求失败: $e');
|
||
}
|
||
}
|
||
|
||
Future<void> testCreateOrder() async {
|
||
final orderData = {
|
||
'type': 0,
|
||
'status': 1,
|
||
'tableNo': 'A1',
|
||
'note': '接口测试订单,可删除',
|
||
'items': [
|
||
{
|
||
'id': 'item_1',
|
||
'name': '红烧肉',
|
||
'source': 0,
|
||
'quantity': 1,
|
||
'price': 38.0,
|
||
'ingredients': '五花肉、酱油、冰糖',
|
||
'note': null,
|
||
},
|
||
{
|
||
'id': 'item_2',
|
||
'name': '番茄炒蛋',
|
||
'source': 2,
|
||
'quantity': 2,
|
||
'price': 18.0,
|
||
'ingredients': null,
|
||
'note': '少盐',
|
||
},
|
||
],
|
||
};
|
||
|
||
final url = '$baseUrl/kitchen.php?act=create';
|
||
print(' POST $url');
|
||
try {
|
||
final output = await _curl(
|
||
'POST',
|
||
url,
|
||
body: jsonEncode(orderData),
|
||
headers: {'Accept': 'application/json'},
|
||
);
|
||
print(' Raw: ${output.length > 500 ? output.substring(0, 500) : output}');
|
||
final result = _parseResult(output);
|
||
if (result['code'] == 200 && result['data'] != null) {
|
||
final data = result['data'] as Map<String, dynamic>;
|
||
createdOrderId = data['id']?.toString();
|
||
createdOrderNo = data['orderNo']?.toString();
|
||
print(' ✅ 创建成功! id=$createdOrderId, orderNo=$createdOrderNo');
|
||
} else {
|
||
print(' ❌ 创建失败: ${result['message']}');
|
||
}
|
||
} catch (e) {
|
||
print(' ❌ 请求失败: $e');
|
||
}
|
||
}
|
||
|
||
Future<void> testUpdateOrder() async {
|
||
final updateData = {
|
||
'id': createdOrderId,
|
||
'status': 2,
|
||
'note': '接口测试 - 已更新',
|
||
'items': [
|
||
{
|
||
'id': 'item_1',
|
||
'name': '红烧肉',
|
||
'source': 0,
|
||
'quantity': 2,
|
||
'price': 38.0,
|
||
'ingredients': '五花肉、酱油、冰糖',
|
||
'note': '多放糖',
|
||
},
|
||
],
|
||
};
|
||
|
||
final url = '$baseUrl/kitchen.php?act=update';
|
||
print(' POST $url');
|
||
try {
|
||
final output = await _curl(
|
||
'POST',
|
||
url,
|
||
body: jsonEncode(updateData),
|
||
headers: {'Accept': 'application/json'},
|
||
);
|
||
_printResult(output);
|
||
} catch (e) {
|
||
print(' ❌ 请求失败: $e');
|
||
}
|
||
}
|
||
|
||
Future<void> testSSE() async {
|
||
final url = '$baseUrl/kitchen_sse.php?order_id=${createdOrderId ?? "test"}';
|
||
print(' GET (SSE) $url');
|
||
try {
|
||
_curlCounter++;
|
||
final tmpFile = File(
|
||
'${Directory.systemTemp.path}/kitchen_sse_test_$_curlCounter.txt',
|
||
);
|
||
final args = [
|
||
'-sS',
|
||
'-N',
|
||
'--max-time',
|
||
'6',
|
||
'--compressed',
|
||
'-o',
|
||
tmpFile.path,
|
||
'-H',
|
||
'Accept: text/event-stream',
|
||
'-H',
|
||
'Cache-Control: no-cache',
|
||
url,
|
||
];
|
||
await Process.run('curl.exe', args);
|
||
if (!tmpFile.existsSync()) {
|
||
print(' ⚠️ SSE无输出');
|
||
return;
|
||
}
|
||
final bytes = tmpFile.readAsBytesSync();
|
||
try {
|
||
tmpFile.deleteSync();
|
||
} catch (_) {}
|
||
final output = utf8.decode(bytes, allowMalformed: true).trim();
|
||
if (output.isEmpty) {
|
||
print(' ⚠️ SSE无输出');
|
||
return;
|
||
}
|
||
final lines = output.split('\n').where((l) => l.trim().isNotEmpty).toList();
|
||
print(' 收到 ${lines.length} 行SSE数据:');
|
||
for (var i = 0; i < lines.length && i < 10; i++) {
|
||
print(' 📡 ${lines[i]}');
|
||
}
|
||
final hasConnected = lines.any((l) => l.contains('connected'));
|
||
final hasHeartbeat = lines.any((l) => l.contains('heartbeat'));
|
||
final hasOrderUpdate = lines.any((l) => l.contains('order_update'));
|
||
if (hasConnected) print(' ✅ SSE连接事件已收到');
|
||
if (hasHeartbeat) print(' ✅ SSE心跳事件已收到');
|
||
if (hasOrderUpdate) print(' ✅ SSE订单更新事件已收到');
|
||
if (!hasConnected && !hasHeartbeat) print(' ⚠️ 未收到标准SSE事件');
|
||
} catch (e) {
|
||
print(' ❌ SSE请求失败: $e');
|
||
}
|
||
}
|
||
|
||
Map<String, dynamic> _parseResult(String body) {
|
||
try {
|
||
return jsonDecode(body) as Map<String, dynamic>;
|
||
} catch (e) {
|
||
return {'code': -1, 'message': 'JSON解析失败', 'raw': body};
|
||
}
|
||
}
|
||
|
||
void _printResult(String body) {
|
||
try {
|
||
final json = jsonDecode(body) as Map<String, dynamic>;
|
||
final code = json['code'];
|
||
final message = json['message'];
|
||
if (code == 200) {
|
||
print(' ✅ code: $code | $message');
|
||
final data = json['data'];
|
||
if (data != null) {
|
||
final dataStr = jsonEncode(data);
|
||
if (dataStr.length > 300) {
|
||
print(' data: ${dataStr.substring(0, 300)}...');
|
||
} else {
|
||
print(' data: $dataStr');
|
||
}
|
||
}
|
||
} else {
|
||
print(' ⚠️ code: $code | $message');
|
||
final data = json['data'];
|
||
if (data != null) print(' data: ${jsonEncode(data)}');
|
||
}
|
||
} catch (e) {
|
||
if (body.length > 300) {
|
||
print(' ${body.substring(0, 300)}...');
|
||
} else {
|
||
print(' $body');
|
||
}
|
||
}
|
||
}
|