// 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 _curl( String method, String url, { String? body, Map? 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 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 testGet(Map 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 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 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; createdOrderId = data['id']?.toString(); createdOrderNo = data['orderNo']?.toString(); print(' ✅ 创建成功! id=$createdOrderId, orderNo=$createdOrderNo'); } else { print(' ❌ 创建失败: ${result['message']}'); } } catch (e) { print(' ❌ 请求失败: $e'); } } Future 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 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 _parseResult(String body) { try { return jsonDecode(body) as Map; } catch (e) { return {'code': -1, 'message': 'JSON解析失败', 'raw': body}; } } void _printResult(String body) { try { final json = jsonDecode(body) as Map; 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'); } } }