优化和路径规划新适配

This commit is contained in:
2026-07-11 16:46:53 +08:00
parent 2124f3bc02
commit 1a4d0aedfd
43 changed files with 2454 additions and 685 deletions

View File

@@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:get_it/get_it.dart';
import '../../../../core/consts/http_api_consts.dart';
import '../../../../core/logging/i_logger_service.dart';
import '../models/device_task_model.dart';
@@ -50,7 +51,7 @@ class DeviceTaskDatasourceImpl implements DeviceTaskDatasource {
int pageSize = 99999999,
}) async {
try {
final url = 'http://1.95.137.212:59015/iot/deviceTask/deviceTaskPool';
final url = '${HttpApiConsts.baseUrl}/iot/deviceTask/deviceTaskPool';
final response = await dio.get(
url,
queryParameters: {
@@ -89,7 +90,7 @@ class DeviceTaskDatasourceImpl implements DeviceTaskDatasource {
required int siteId,
}) async {
try {
final url = 'http://1.95.137.212:59015/iot/deviceTask/cancelTask';
final url = '${HttpApiConsts.baseUrl}/iot/deviceTask/cancelTask';
final response = await dio.post(
url,
data: {
@@ -124,7 +125,7 @@ class DeviceTaskDatasourceImpl implements DeviceTaskDatasource {
required int siteId,
}) async {
try {
final url = 'http://1.95.137.212:59015/iot/deviceTask/pauseTask';
final url = '${HttpApiConsts.baseUrl}/iot/deviceTask/pauseTask';
final response = await dio.post(
url,
data: {
@@ -159,7 +160,7 @@ class DeviceTaskDatasourceImpl implements DeviceTaskDatasource {
required int siteId,
}) async {
try {
final url = 'http://1.95.137.212:59015/iot/deviceTask/recoveryTask';
final url = '${HttpApiConsts.baseUrl}/iot/deviceTask/recoveryTask';
final response = await dio.post(
url,
data: {

View File

@@ -34,7 +34,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
logger.logWithLevel(
'用户点击bindDevice方法API 请求开始',
level: 'INFO',
data: {'url': 'https://serviceri.satabot.com/iot/device/bindDevice'},
data: {'url': 'http://1.95.137.212:8081/iot/device/bindDevice'},
);
var response = await dio.post(
HttpApiConsts.bindDevice,
@@ -44,7 +44,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
'请求详细参数',
level: 'DEBUG',
data: {
'url': 'https://serviceri.satabot.com/iot/device/bindDevice',
'url': 'http://1.95.137.212:8081/iot/device/bindDevice',
'deviceId': deviceId,
'deviceAlias': deviceAlias,
},
@@ -122,7 +122,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
logger.logWithLevel(
'用户点击switchDevice方法API 请求开始',
level: 'INFO',
data: {'url': 'https://serviceri.satabot.com/iot/device/switchDevice'},
data: {'url': 'http://1.95.137.212:8081/iot/device/switchDevice'},
);
var response = await dio.post(
HttpApiConsts.switchDevice,
@@ -138,7 +138,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
'请求详细参数',
level: 'DEBUG',
data: {
'url': 'https://serviceri.satabot.com/iot/device/switchDevice',
'url': 'http://1.95.137.212:8081/iot/device/switchDevice',
'platform': platform,
'deviceId': deviceId,
},
@@ -219,12 +219,12 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
'请求详细参数',
level: 'DEBUG',
data: {
'url': 'https://serviceri.satabot.com/iot/device/userDevice',
'url': 'http://1.95.137.212:8081/iot/device/userDevice',
'tenantName': deviceame,
},
);
final response = await dio.get(
'https://serviceri.satabot.com/iot/device/userDevice',
'http://1.95.137.212:8081/iot/device/userDevice',
queryParameters: {'tenantName': deviceame},
// options: Options(
// headers: {

View File

@@ -21,66 +21,83 @@ class PathHttpDatasourceImpl implements PathHttpDatasource {
@override
Future<List<DeviceAddPathPointModel>> generatePathRaw({required Map<String, dynamic> body}) async {
try {
_logger.logWithLevel( '请求体类型:${body.runtimeType}',level: 'INFO');
_logger.logWithLevel( '璇锋眰浣撶被鍨嬶細${body.runtimeType}',level: 'INFO');
// 2. 格式化打印JSON(带缩进,清晰展示嵌套结构)
// 2. 鏍煎紡鍖栨墦鍗癑SON锛堝甫缂╄繘锛屾竻鏅板睍绀哄祵濂楃粨鏋勶級
final jsonString = const JsonEncoder.withIndent(' ').convert(body);
final _jsonString = jsonEncode(body);
// print('完整JSON请求体:\n$jsonString');
_logger.logWithLevel( '请求体:$_jsonString',level: 'INFO');
// print('瀹屾暣JSON璇锋眰浣擄細\n$jsonString');
_logger.logWithLevel( '璇锋眰浣擄細$_jsonString',level: 'INFO');
// 3. 可选:单独打印holes的JSON(重点关注)
// 3. 鍙€夛細鍗曠嫭鎵撳嵃holes鐨凧SON锛堥噸鐐瑰叧娉級
//final holesJson = body['holes'] as List;
//print('=== 单独打印holes的JSON ===');
//print('=== 鍗曠嫭鎵撳嵃holes鐨凧SON ===');
//if (holesJson.isEmpty) {
// print('holes 为空数组');
// print('holes 涓虹┖鏁扮粍');
//} else {
// for (int i = 0; i < holesJson.length; i++) {
// print('第${i + 1}组hole:\n${const JsonEncoder.withIndent(' ').convert(holesJson[i])}');
// print('绗?{i + 1}缁刪ole锛歕n${const JsonEncoder.withIndent(' ').convert(holesJson[i])}');
// }
//}
} catch (e) {
// 防止JSON序列化失败导致崩溃
//print('打印请求体失败:$e');
_logger.logWithLevel( '打印请求体失败:$e',level: 'ERROR');
// 降级打印原始body(虽然格式乱,但能看基础数据)
// print('原始body数据:$body');
_logger.logWithLevel( '原始body数据:$body',level: 'ERROR');
// 闃叉JSON搴忓垪鍖栧け璐ュ鑷村穿婧?
//print('鎵撳嵃璇锋眰浣撳け璐ワ細$e');
_logger.logWithLevel( '鎵撳嵃璇锋眰浣撳け璐ワ細$e',level: 'ERROR');
// 闄嶇骇鎵撳嵃鍘熷body锛堣櫧鐒舵牸寮忎贡锛屼絾鑳界湅鍩虹鏁版嵁锛?
// print('鍘熷body鏁版嵁锛?body');
_logger.logWithLevel( '鍘熷body鏁版嵁锛?body',level: 'ERROR');
}
final token = (await _userStorage.getUser())?.token;
if (token == null) throw Exception('Token missing');
final requestBody = jsonEncode(body);
// print('🔍 [路径规划请求] ====== 完整请求体 ======');
// print(requestBody);
//print('🔍 [路径规划请求] ====== 请求体结束 ======');
final response = await http.post(
Uri.parse('https://servicepathplan.satabot.com/api/path'),
headers: {'Content-Type': 'application/json', 'Authorization': token},
body: jsonEncode(body),
headers: {'Content'
'-Type': 'application/json', 'Authorization': token},
body: requestBody,
);
// 🔥 完整打印响应体
//printFullString(formatJson(jsonEncode(response.body)));
//print('🔄 [路径规划API] 响应状态码: ${response.statusCode}');
// print('🔄 [路径规划API] 响应体长度: ${response.body.length}');
//print('🔄 [路径规划API] 响应体前500字符: ${response.body.length > 500 ? response.body.substring(0, 500) : response.body}');
try {
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
if (decoded['success'] == true) {
final pathJson = decoded['data']?['path'];
// print('路径数据: $pathJson'); // 调试输出路径数据
_logger.logWithLevel( '路径数据: $pathJson',level: 'INFO');
if (pathJson == null) {
throw Exception('API returned null for "path"');
}
// print('🔄 [路径规划API] 解析后 keys: ${decoded.keys.toList()}');
//print('🔄 [路径规划API] code=${decoded['code']}, success=${decoded['success']}, msg=${decoded['msg']}, hasData=${decoded.containsKey('data')}');
// 🔥 对齐 Web 端:优先判断 data.path 是否存在,兼容 code==200 和 success==true 两种格式
final dataObj = decoded['data'];
final pathJson = dataObj is Map ? dataObj['path'] : null;
if (pathJson != null) {
/ print('✅ [路径规划API] 路径数据点数: ${pathJson is List ? pathJson.length : '非列表'}');
if (pathJson is! List) {
throw Exception('Expected "path" to be a List, but got ${pathJson.runtimeType}');
}
final List<dynamic> jsonData = pathJson;
return jsonData.map((item) => DeviceAddPathPointModel.fromJson(item)).toList();
} else {
final apiCode = decoded['code'] ?? -1;
final msg = decoded['msg'] ?? '未知错误';
throw Exception('API 错误 [$apiCode]: $msg');
}
// 检查 success 字段(旧格式兼容)
if (decoded['success'] == true) {
if (pathJson == null) {
throw Exception('API returned null for "path"');
}
}
// 兼容 code==200 格式
final apiCode = decoded['code'];
if (apiCode != null && apiCode.toString() == '200') {
throw Exception('API code=200 但 data.path 为空');
}
final msg = decoded['message'] ?? decoded['msg'] ?? '未知错误';
throw Exception('API 错误 [${decoded['code'] ?? -1}]: $msg');
} catch (e) {
throw Exception('Network error: $e');
}
}
}

View File

@@ -4,7 +4,7 @@ class ReferencePoint {
ReferencePoint({required this.lat, required this.lon});
Map<String, dynamic> toJson() => {'lat': lat, 'lng': lon};
Map<String, dynamic> toJson() => {'lat': lat, 'lon': lon};
}
class OuterBoundary {
@@ -86,5 +86,5 @@ class Position {
return Position(lat: lat, lon: lon);
}
Map<String, dynamic> toJson() => {'lat': lat, 'lng': lon};
Map<String, dynamic> toJson() => {'lat': lat, 'lon': lon};
}

View File

@@ -11,7 +11,7 @@ class WorkRecordEntity {
final int userId;
final int id;
final String workName;
final WorkRecordJsonData? jsonData;
final dynamic jsonData; // 支持 WorkRecordJsonData(JSON接口)或 String(XML接口原始JSON,避免二次序列化丢失数据)
final String? imgUrl;
WorkRecordEntity({
@@ -43,11 +43,8 @@ class WorkRecordEntity {
userId: int.tryParse(xmlData['userId']?.toString() ?? '0') ?? 0,
id: int.tryParse(xmlData['id']?.toString() ?? '0') ?? 0,
workName: xmlData['workName'] as String? ?? '',
jsonData: xmlData['jsonData'] != null
? WorkRecordJsonData.fromXml(
xmlData['jsonData'] as Map<String, dynamic>,
)
: null,
// 🔥 核心修复:_parseJsonDataNode 现在返回 JSON 字符串,直接存储,避免 Map→jsonEncode 丢失路径数据
jsonData: xmlData['jsonData'],
imgUrl: xmlData['imgUrl'] as String?,
);
}

View File

@@ -35,7 +35,7 @@ class DeviceHostrityWorkRepositoryImpl implements DeviceHostrityWorkRepositoryRe
_logger.logWithLevel('用户信息获取成功,Token: $token', level: 'info');
final response = await client.get(
'https://serviceri.satabot.com/iot/device/getDeviceRunStatistics?deviceId=$deviceId',
'http://1.95.137.212:8081/iot/device/getDeviceRunStatistics?deviceId=$deviceId',
options: Options(headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer $token'}),
);

View File

@@ -2,6 +2,7 @@ import 'dart:math';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:xml/xml.dart';
import '../../../../core/di/injection.dart';
@@ -16,7 +17,7 @@ class PathRepositoryImpl implements PathRepository {
final PathHttpDatasource _datasource;
PathRepositoryImpl({required PathHttpDatasource datasource})
: _datasource = datasource;
// 生成路径
// 鐢熸垚璺<EFBFBD>緞
@override
Future<List<DeviceAddPathPointModel>> generatePath({
required ReferencePoint reference,
@@ -31,22 +32,22 @@ class PathRepositoryImpl implements PathRepository {
'outer': outer.toJson(),
'holes': holes != null && holes.isNotEmpty
? holes.values.first
.toJson() // 取第一个值并序列化,去掉外层 Map
: {},
.toJson() // 鍙栫<EFBFBD>涓€涓<EFBFBD>€煎苟搴忓垪鍖栵紝鍘绘帀澶栧眰 Map
: [],
'workType': workType,
};
return await _datasource.generatePathRaw(body: body);
}
// 保存工作记录
// 淇濆瓨宸ヤ綔璁板綍
@override
Future<Map<String, dynamic>> saveWorkRecord({
required String workName,
required String userId,
required String jsonData,
}) async {
final url = Uri.parse('https://serviceri.satabot.com/iot/workRecord/add');
final url = Uri.parse('http://1.95.137.212:8081/iot/workRecord/add');
final headers = {'Content-Type': 'application/json'};
final body = jsonEncode({
'workName': workName,
@@ -73,7 +74,7 @@ class PathRepositoryImpl implements PathRepository {
}) async {
final timestamp = DateTime.now().millisecondsSinceEpoch;
final url = Uri.parse(
'https://serviceri.satabot.com/iot/workRecord/selectByUserId',
'http://1.95.137.212:8081/iot/workRecord/selectByUserId',
).replace(queryParameters: {'userId': userId, '_t': timestamp.toString()});
try {
@@ -102,7 +103,7 @@ class PathRepositoryImpl implements PathRepository {
final timestamp = DateTime.now().millisecondsSinceEpoch;
final url =
Uri.parse(
'https://serviceri.satabot.com/iot/workRecord/deleteByWorkName',
'http://1.95.137.212:8081/iot/workRecord/deleteByWorkName',
).replace(
queryParameters: {'workName': workName, '_t': timestamp.toString()},
);
@@ -112,7 +113,7 @@ class PathRepositoryImpl implements PathRepository {
final data = jsonDecode(response.body) as Map<String, dynamic>;
if (response.statusCode == 200 && data['code'] == 200) {
return data; // 返回 {"code": 200, "msg": "删除成功"}
return data; // 杈斿洖 {"code": 200, "msg": "鍒犻櫎鎴愬姛"}
} else {
throw Exception(
'Delete failed: ${data['msg'] ?? response.reasonPhrase}',
@@ -123,7 +124,7 @@ class PathRepositoryImpl implements PathRepository {
}
}
/// 选择工作记录
/// 选择工作记录(根据workName)
@override
Future<List<Map<String, dynamic>>> selectWorkRecordByName({
required String workName,
@@ -131,41 +132,54 @@ class PathRepositoryImpl implements PathRepository {
final timestamp = DateTime.now().millisecondsSinceEpoch;
final url =
Uri.parse(
'https://serviceri.satabot.com/iot/workRecord/selectByWorkName',
'http://1.95.137.212:8081/iot/workRecord/selectByWorkName',
).replace(
queryParameters: {'workName': workName, '_t': timestamp.toString()},
);
try {
final response = await http.get(url);
final response = await http.get(url).timeout(const Duration(seconds: 15));
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
if (data['code'] == 200 && data.containsKey('data')) {
final dynamic rawData = data['data'];
// 🔧 关键修复:安全转换为 List
List<Map<String, dynamic>> records;
if (rawData is List) {
records = rawData.map((e) {
if (e is Map) {
return Map<String, dynamic>.from(e);
}
throw Exception('List item is not a Map: ${e.runtimeType}');
}).toList();
} else if (rawData is Map) {
records = [Map<String, dynamic>.from(rawData)];
} else {
throw Exception(
'Unexpected data type for "data": ${rawData.runtimeType}',
);
// 判断响应格式:XML 还是 JSON
final trimmed = response.body.trim();
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
// JSON 格式(原有逻辑)
Map<String, dynamic> data;
try {
data = jsonDecode(response.body) as Map<String, dynamic>;
} catch (formatError) {
print('[selectWorkRecordByName] JSON解析失败: $formatError');
throw Exception('JSON解析失败: $formatError');
}
return records;
if (data['code'] == 200 && data.containsKey('data')) {
final dynamic rawData = data['data'];
List<Map<String, dynamic>> records;
if (rawData is List) {
records = rawData.map((e) {
if (e is Map) {
return Map<String, dynamic>.from(e);
}
throw Exception('List item is not a Map: ${e.runtimeType}');
}).toList();
} else if (rawData is Map) {
records = [Map<String, dynamic>.from(rawData)];
} else {
throw Exception(
'Unexpected data type for "data": ${rawData.runtimeType}',
);
}
return records;
} else {
throw Exception('API error: ${data['msg'] ?? 'Unknown'}');
}
} else {
throw Exception('API error: ${data['msg'] ?? 'Unknown'}');
// XML 格式:使用 XML 解析逻辑
print('[selectByWorkName] 检测到 XML 格式响应');
return _parseXmlSelectByNameResponse(response.body);
}
} else {
throw Exception(
@@ -177,7 +191,75 @@ class PathRepositoryImpl implements PathRepository {
}
}
/// 根据场站ID查询工作记录列表(XML格式)
/// 解析 selectByWorkName 的 XML 响应
/// 返回 List<Map<String, dynamic>>,将 jsonData 中的 path/outer 提取到顶层
Future<List<Map<String, dynamic>>> _parseXmlSelectByNameResponse(String body) async {
print('[selectByWorkName XML] 原始响应前500字符: ${body.length > 500 ? body.substring(0, 500) : body}');
final codeMatch = RegExp(r'<\w*:?code[^>]*>(\d+)</\w*:?code>').firstMatch(body);
final code = codeMatch?.group(1);
print('[selectByWorkName XML] code: $code');
if (code != '200') {
final msgMatch = RegExp(r'<\w*:?msg[^>]*>(.*?)</\w*:?msg>').firstMatch(body);
throw Exception('API error: ${msgMatch?.group(1) ?? 'Unknown'}');
}
final dataRegex = RegExp(r'<\w*:?data[^>]*>([\s\S]*?)</\w*:?data>');
final dataMatches = dataRegex.allMatches(body);
print('[selectByWorkName XML] 找到 data 节点数量: ${dataMatches.length}');
final List<Map<String, dynamic>> records = [];
for (final match in dataMatches) {
final dataContent = match.group(1)!;
print('[selectByWorkName XML] data内容前300字符: ${dataContent.length > 300 ? dataContent.substring(0, 300) : dataContent}');
try {
final wrappedXml = '<root>$dataContent</root>';
final document = XmlDocument.parse(wrappedXml);
final recordElement = document.rootElement;
final recordData = _parseXmlRecord(recordElement);
print('[selectByWorkName XML] recordData keys: ${recordData.keys.toList()}');
print('[selectByWorkName XML] jsonData 类型: ${recordData['jsonData']?.runtimeType}');
// 将 jsonData 中的 path/outer 提取到顶层,供 UI 直接使用
if (recordData['jsonData'] is String) {
try {
final jsonDataMap = jsonDecode(recordData['jsonData'] as String) as Map<String, dynamic>;
print('[selectByWorkName XML] jsonData 解析后 keys: ${jsonDataMap.keys.toList()}');
print('[selectByWorkName XML] path 类型: ${jsonDataMap['path']?.runtimeType}, outer 类型: ${jsonDataMap['outer']?.runtimeType}');
if (jsonDataMap.containsKey('path')) {
recordData['path'] = jsonDataMap['path'];
}
if (jsonDataMap.containsKey('outer')) {
recordData['outer'] = jsonDataMap['outer'];
}
if (jsonDataMap.containsKey('planModel')) {
recordData['planModel'] = jsonDataMap['planModel'];
}
print('[selectByWorkName XML] 提取后 recordData path类型: ${recordData['path']?.runtimeType}, outer类型: ${recordData['outer']?.runtimeType}');
} catch (e) {
print('[selectByWorkName XML] jsonData 解析失败: $e');
}
} else {
print('[selectByWorkName XML] jsonData 不是 String 类型! 值: ${recordData['jsonData']}');
}
records.add(recordData);
} catch (e) {
print('[selectByWorkName XML] 解析单个 data 节点失败: $e');
}
}
print('[selectByWorkName XML] 最终解析记录数: ${records.length}');
if (records.isNotEmpty) {
final first = records.first;
print('[selectByWorkName XML] 第一条记录 path长度: ${(first['path'] as List?)?.length ?? "null"}');
print('[selectByWorkName XML] 第一条记录 outer长度: ${(first['outer'] as List?)?.length ?? "null"}');
}
return records;
}
/// 鏍规嵁鍦虹珯ID鏌ヨ<E98F8C>宸ヤ綔璁板綍鍒楄〃锛圶ML鏍煎紡锛?
@override
Future<List<WorkRecordEntity>> getWorkRecordsBySiteId({
required int siteId,
@@ -185,7 +267,7 @@ class PathRepositoryImpl implements PathRepository {
final timestamp = DateTime.now().millisecondsSinceEpoch;
final url =
Uri.parse(
'http://1.95.137.212:59015/iot/workRecord/selectBySiteId',
'http://1.95.137.212:8081/iot/workRecord/selectBySiteId',
).replace(
queryParameters: {
'siteId': siteId.toString(),
@@ -198,24 +280,27 @@ class PathRepositoryImpl implements PathRepository {
url,
headers: {'Accept': 'application/xml, text/xml, */*'},
);
/// print('[XML接口] 响应状态码: ${response.statusCode}');
////print('[XML接口] 响应内容长度: ${response.body.length}');
///print('[XML接口] Content-Type: ${response.headers['content-type']}');
/// print('[XML鎺ュ彛] 鍝嶅簲鐘舵€佺爜: ${response.statusCode}');
////print('[XML鎺ュ彛] 鍝嶅簲鍐呭<E98D90>闀垮害: ${response.body.length}');
///print('[XML鎺ュ彛] Content-Type: ${response.headers['content-type']}');
if (response.statusCode == 200) {
// 打印前200字符确认格式
// 鎵撳嵃鍓?00瀛楃<E7809B>纭<EFBFBD><E7BAAD>鏍煎紡
final preview = response.body.length > 200
? response.body.substring(0, 200)
: response.body;
/// print('[XML接口] 响应开头: $preview');
// 🔥 调试:打印原始 API 响应
// print('[selectBySiteId 原始响应] 长度=${response.body.length}');
// print('[selectBySiteId 原始响应] 前1000字符: ${response.body.length > 1000 ? response.body.substring(0, 1000) : response.body}');
/// print('[XML鎺ュ彛] 鍝嶅簲寮€澶? $preview');
// 判断是JSON还是XML格式
// 鍒ゆ柇鏄疛SON杩樻槸XML鏍煎紡
final trimmed = response.body.trim();
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
///print('[XML接口] 检测到JSON格式,使用JSON解析');
///print('[XML鎺ュ彛] 妫€娴嬪埌JSON鏍煎紡锛屼娇鐢↗SON瑙f瀽');
return _parseJsonResponse(response.body);
} else {
///print('[XML接口] 检测到XML格式,使用XML解析');
///print('[XML鎺ュ彛] 妫€娴嬪埌XML鏍煎紡锛屼娇鐢╔ML瑙f瀽');
return _parseXmlResponse(response.body);
}
} else {
@@ -224,12 +309,12 @@ class PathRepositoryImpl implements PathRepository {
);
}
} catch (e) {
/// print('[XML接口] 错误: $e');
/// print('[XML鎺ュ彛] 閿欒<E996BF>: $e');
throw Exception('Network error in getWorkRecordsBySiteId: $e');
}
}
/// 解析JSON格式响应
/// 瑙f瀽JSON鏍煎紡鍝嶅簲
Future<List<WorkRecordEntity>> _parseJsonResponse(String body) async {
final data = jsonDecode(body) as Map<String, dynamic>;
@@ -252,18 +337,18 @@ class PathRepositoryImpl implements PathRepository {
records.add(WorkRecordEntity.fromJson(recordsData));
}
/// print('[XML接口] 最终解析记录数: ${records.length}');
/// print('[XML鎺ュ彛] 鏈€缁堣В鏋愯<E98F8B>褰曟暟: ${records.length}');
return records;
}
/// 解析XML格式响应
/// 瑙f瀽XML鏍煎紡鍝嶅簲
Future<List<WorkRecordEntity>> _parseXmlResponse(String body) async {
// 用更宽松的正则检查响应码(支持命名空间前缀)
// 鐢ㄦ洿瀹芥澗鐨勬<EFBFBD>鍒欐<EFBFBD>鏌ュ搷搴旂爜锛堟敮鎸佸懡鍚嶇┖闂村墠缂€锛?
final codeMatch = RegExp(
r'<\w*:?code[^>]*>(\d+)</\w*:?code>',
).firstMatch(body);
final code = codeMatch?.group(1);
print('[XML接口] code: $code');
print('[XML鎺ュ彛] code: $code');
if (code != '200') {
final msgMatch = RegExp(
@@ -272,84 +357,104 @@ class PathRepositoryImpl implements PathRepository {
throw Exception('API error: ${msgMatch?.group(1) ?? 'Unknown'}');
}
// 使用正则表达式提取所有<data>...</data>节点
// 使用非贪婪匹配,确保每个data节点独立提取
final dataRegex = RegExp(r'<data>([\s\S]*?)</data>');
// 浣跨敤姝e垯琛ㄨ揪寮忔彁鍙栨墍鏈?data>...</data>鑺傜偣
// 浣跨敤闈炶椽濠<EFBFBD>尮閰嶏紝纭<EFBFBD>繚姣忎釜data鑺傜偣鐙<EFBFBD>鎻愬彇
final dataRegex = RegExp(r'<\w*:?data[^>]*>([\s\S]*?)</\w*:?data>');
final dataMatches = dataRegex.allMatches(body);
///print('[XML接口] 找到data节点数量: ${dataMatches.length}');
///print('[XML鎺ュ彛] 鎵惧埌data鑺傜偣鏁伴噺: ${dataMatches.length}');
// 解析所有工作记录
// 瑙f瀽鎵€鏈夊伐浣滆<EFBFBD>褰?
final List<WorkRecordEntity> records = [];
for (int i = 0; i < dataMatches.length; i++) {
final match = dataMatches.elementAt(i);
final dataContent = match.group(1)!; // 获取<data>和</data>之间的内容
final dataContent = match.group(1)!; // 鑾峰彇<data>鍜?/data>涔嬮棿鐨勫唴瀹?
try {
// 将提取的内容包装成完整XML进行解析
// 灏嗘彁鍙栫殑鍐呭<EFBFBD>鍖呰<EFBFBD>鎴愬畬鏁碭ML杩涜<EFBFBD>瑙f瀽
final wrappedXml = '<root>$dataContent</root>';
final document = XmlDocument.parse(wrappedXml);
final recordElement = document.rootElement;
final recordData = _parseXmlRecord(recordElement);
final _jdVal = recordData['jsonData'];
// print('[selectBySiteId XML] data[$i] workName=${recordData['workName']}, jsonData类型=${_jdVal?.runtimeType}, jsonData长度=${_jdVal is String ? _jdVal.length : "N/A"}');
// if (_jdVal is String && _jdVal.isNotEmpty) {
// try {
// final _jdDecoded = jsonDecode(_jdVal);
// if (_jdDecoded is Map) {
// print('[selectBySiteId XML] data[$i] jsonData.path类型=${_jdDecoded['path']?.runtimeType}, outer类型=${_jdDecoded['outer']?.runtimeType}');
// if (_jdDecoded['path'] is List) print('[selectBySiteId XML] data[$i] path长度=${(_jdDecoded['path'] as List).length}');
// }
// } catch (_) {}
// }
/// print(
/// '[XML接口] data[$i] 解析结果: workName=${recordData['workName']}, id=${recordData['id']}, imgUrl=${recordData['imgUrl']}',
/// '[XML鎺ュ彛] data[$i] 瑙f瀽缁撴灉: workName=${recordData['workName']}, id=${recordData['id']}, imgUrl=${recordData['imgUrl']}',
/// );
records.add(WorkRecordEntity.fromXml(recordData));
} catch (e) {
print('[XML接口] 解析单个data节点失败: $e');
print('[XML鎺ュ彛] 瑙f瀽鍗曚釜data鑺傜偣澶辫触: $e');
}
}
///print('[XML接口] 最终解析记录数: ${records.length}');
///print('[XML鎺ュ彛] 鏈€缁堣В鏋愯<E98F8B>褰曟暟: ${records.length}');
return records;
}
/// 解析XML工作记录节点
/// 瑙f瀽XML宸ヤ綔璁板綍鑺傜偣
Map<String, dynamic> _parseXmlRecord(XmlElement recordElement) {
final Map<String, dynamic> result = {};
//// print('[XML解析] 开始解析节点,子元素数量: ${recordElement.childElements.length}');
//// print('[XML瑙f瀽] 寮€濮嬭В鏋愯妭鐐癸紝瀛愬厓绱犳暟閲? ${recordElement.childElements.length}');
for (final child in recordElement.childElements) {
final tagName = child.name.local;
final innerText = child.innerText.trim();
// print(
// '[XML解析] 标签: $tagName, 值: ${innerText.length > 50 ? innerText.substring(0, 50) + '...' : innerText}',
// '[XML瑙f瀽] 鏍囩<E98F8D>: $tagName, 鍊? ${innerText.length > 50 ? innerText.substring(0, 50) + '...' : innerText}',
// );
// 特殊处理jsonData节点(包含嵌套结构)
// 鐗规畩澶勭悊jsonData鑺傜偣锛堝寘鍚<EFBFBD>祵濂楃粨鏋勶級
if (tagName == 'jsonData') {
result['jsonData'] = _parseJsonDataNode(child);
} else {
// 普通节点直接取值
// 鏅<EFBFBD>€氳妭鐐圭洿鎺ュ彇鍊?
result[tagName] = innerText;
}
}
//print('[XML解析] 解析完成,结果keys: ${result.keys.toList()}');
//print('[XML瑙f瀽] 瑙f瀽瀹屾垚锛岀粨鏋渒eys: ${result.keys.toList()}');
return result;
}
/// 解析jsonData节点
Map<String, dynamic> _parseJsonDataNode(XmlElement jsonDataElement) {
/// 返回 JSON 字符串,避免 Map → jsonEncode 的二次序列化导致数据丢失
String _parseJsonDataNode(XmlElement jsonDataElement) {
final Map<String, dynamic> result = {};
// // 🔥 调试:打印 jsonData 节点的原始 XML 内容
// final rawXml = jsonDataElement.outerXml;
// print('[jsonData 原始XML] 长度=${rawXml.length}, 前500字符: ${rawXml.length > 500 ? rawXml.substring(0, 500) : rawXml}');
// print('[jsonData 子元素数量] ${jsonDataElement.childElements.length}');
// for (final child in jsonDataElement.childElements) {
// print('[jsonData 子元素] tag=${child.name.local}, innerText长度=${child.innerText.trim().length}');
// }
for (final child in jsonDataElement.childElements) {
final tagName = child.name.local;
if (tagName == 'path' || tagName == 'outer') {
// path和outer可能包含嵌套的path/outer节点
result[tagName] = _parseCoordinateList(child, tagName);
} else {
// 普通字段(name, img, planModel等)
result[tagName] = child.innerText.trim();
}
}
return result;
final encoded = jsonEncode(result);
// print('[jsonData 解析结果] keys=${result.keys.toList()}, 长度=${encoded.length}');
return encoded;
}
/// 解析坐标列表(path或outer)
@@ -365,7 +470,7 @@ class PathRepositoryImpl implements PathRepository {
.toList();
if (nestedElements.isNotEmpty) {
// 有嵌套结构:outer > outer > {lat, lng}
// 嵌套结构:outer > outer > {lat, lng}
for (final nestedElement in nestedElements) {
final latElement = nestedElement.getElement('lat');
final lngElement = nestedElement.getElement('lng');
@@ -378,55 +483,106 @@ class PathRepositoryImpl implements PathRepository {
}
}
} else {
// 直接包含lat/lng节点
final latElement = element.getElement('lat');
final lngElement = element.getElement('lng');
// 🔥 核心修复:提取所有同级 lat/lng 对,而不是只取第一个
// 支持格式:<path><lat>30.1</lat><lng>120.1</lng><lat>30.2</lat><lng>120.2</lng></path>
final allLatElements = element.childElements
.where((e) => e.name.local == 'lat')
.toList();
final allLngElements = element.childElements
.where((e) => e.name.local == 'lng')
.toList();
if (latElement != null && lngElement != null) {
coordinates.add({
'lat': double.tryParse(latElement.innerText.trim()) ?? 0.0,
'lng': double.tryParse(lngElement.innerText.trim()) ?? 0.0,
});
final count = allLatElements.length < allLngElements.length
? allLatElements.length
: allLngElements.length;
for (int i = 0; i < count; i++) {
final lat = double.tryParse(allLatElements[i].innerText.trim()) ?? 0.0;
final lng = double.tryParse(allLngElements[i].innerText.trim()) ?? 0.0;
coordinates.add({'lat': lat, 'lng': lng});
}
}
return coordinates;
}
/// 创建设备任务(通过接口执行作业)
/// 接口地址: http://1.95.137.212:59015/iot/deviceTask/createDeviceTask
/// 入参: {"deviceId":"...","routeId":76,"siteId":22}
/// 鍒涘缓璁惧<EFBFBD>浠诲姟锛堥€氳繃鎺ュ彛鎵ц<EFBFBD>浣滀笟锛?
/// 鎺ュ彛鍦板潃: http://1.95.137.212:8081/iot/deviceTask/createDeviceTask
/// 鍏ュ弬: {"deviceId":"...","routeId":76,"siteId":22,"orgId":5}
/// 杩斿洖: 鍒涘缓鎴愬姛鐨勪换鍔<E68DA2>D
@override
Future<void> createDeviceTask({
Future<int> createDeviceTask({
required String deviceId,
required int routeId,
required int siteId,
required int orgId,
}) async {
final url = Uri.parse('http://1.95.137.212:59015/iot/deviceTask/createDeviceTask');
final body = jsonEncode({
final dio = sl<Dio>();
final body = {
'deviceId': deviceId,
'routeId': routeId,
'siteId': siteId,
});
'orgId': orgId,
};
print('📤 [创建设备任务] 请求参数: $body');
print('[创建设备任务] 请求参数: ${jsonEncode(body)}');
try {
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: body,
final response = await dio.post(
'http://1.95.137.212:8081/iot/deviceTask/createDeviceTask',
data: body,
);
print('📥 [创建设备任务] 响应状态码: ${response.statusCode}');
print('📥 [创建设备任务] 响应内容: ${response.body}');
print('[创建设备任务] 响应状态码: ${response.statusCode}');
print('[创建设备任务] 响应原始数据: ${response.data}');
if (response.statusCode != 200) {
throw Exception('创建设备任务失败: HTTP ${response.statusCode}');
}
// 解析响应,提取 taskId
final data = response.data as Map<String, dynamic>;
final code = data['code'];
print('[创建设备任务] code字段值: $code, 类型: ${code.runtimeType}');
// 兼容 int 和 String 类型的 code
final codeValue = code is int ? code : int.tryParse(code?.toString() ?? '');
if (codeValue == 200) {
final taskData = data['data'];
print('[创建设备任务] data字段值: $taskData, 类型: ${taskData?.runtimeType}');
if (taskData is int) {
print('[创建设备任务] 任务ID(int): $taskData');
return taskData;
} else if (taskData is String) {
final taskId = int.tryParse(taskData);
if (taskId != null) {
print('[创建设备任务] 任务ID(String->int): $taskId');
return taskId;
}
} else if (taskData is Map<String, dynamic>) {
final taskId = taskData['id'] ?? taskData['taskId'];
print('[创建设备任务] data是Map, id=$taskId, 类型=${taskId?.runtimeType}');
if (taskId is int) {
print('[创建设备任务] 任务ID(Map.id): $taskId');
return taskId;
} else if (taskId is String) {
final parsedId = int.tryParse(taskId);
if (parsedId != null) {
print('[创建设备任务] 任务ID(Map.id->int): $parsedId');
return parsedId;
}
}
}
// code==200 但无法解析taskId,可能是新格式,打印完整响应方便调试
print('[创建设备任务] WARNING: code=200但无法解析taskId, 完整响应: ${response.data}');
throw Exception('任务已创建但无法解析taskId, 完整响应: ${response.data}');
} else {
throw Exception('创建设备任务失败: ${data['msg'] ?? response.data}');
}
} catch (e) {
print('❌ [创建设备任务] 错误: $e');
print('[创建设备任务] 错误: $e');
throw Exception('创建设备任务失败: $e');
}
}

View File

@@ -84,14 +84,14 @@ class PathPlanner {
PathPlanner(this.tcpClient);
// 🔥 已弃用:路径点逐个下发已改为服务端通过 createDeviceTask HTTP 接口处理,
// APP 不再通过 TCP 逐个发送路径点,避免与服务器重复发送导致下位机收到双份数据。
void sendNextLocation() {
_logger.log("[sendNextLocation] 已弃用 - 路径点下发由服务端 HTTP 接口负责");
return;
/* ========== 以下为原有 TCP 逐个发点逻辑,已注释保留 ==========
_logger.log("[发送sendNextLocation方法指令]");
// if (isStart && locationQueue.isEmpty) {
// isStart = false;
// print("[track]");
// tcpClient.disconnect();
// return;
// }
// 关键检查:暂停或停止时不再发送
if (_isPaused) {
_logger.log("[暂停状态],停止发送指令");
@@ -101,11 +101,9 @@ class PathPlanner {
if (_isStopped || (isStart && locationQueue.isEmpty)) {
isStart = false;
if (_isStopped) {
//print("⏹️ 已停止作业,清空队列");
_logger.log("[路径点发送完毕] 停止");
locationQueue.clear(); // 清空剩余队列
} else {
//print("[路径点发送完毕]");
_logger.log("[路径点发送完毕]");
}
_isStopped = false; // 重置停止标志
@@ -121,10 +119,10 @@ class PathPlanner {
speed: 1000,
);
//print("====Lat:${entity.latitude} ====Lng:${entity.longitude}");
_logger.log("[发送指令]");
tcpClient.sendPathPoint(routePlanSendEntity);
isStart = true;
========== 原有 TCP 逐个发点逻辑结束 ========== */
}
/// 开始
@@ -186,36 +184,30 @@ class PathPlanner {
}
}
/// 暂停
/// 暂停(已切换为 HTTP 接口 pauseTask,TCP 方式已弃用)
void pauseRPWork() {
//print("⏸️ 暂停作业,设置_isPaused = true");
_logger.log("[暂停作业,设置_isPaused = true]");
_logger.log("[pauseRPWork] 已弃用 - 暂停由 HTTP pauseTask 接口负责");
_isPaused = true;
/* ========== 以下为原有 TCP 发送暂停指令,已注释保留 ==========
var entity = new RoutePlanSendEntity(commandType: 0x01, pointCounts: 2, targetLatitude: 0, targetLongitude: 0, speed: 0);
// print("📡 发送暂停指令到设备...");
_logger.log("[发送暂停指令到设备...]");
tcpClient.sendDeviceStateChange(entity);
========== 原有 TCP 暂停指令结束 ========== */
}
/// 恢复
/// 恢复(已切换为 HTTP 接口 recoveryTask,TCP 方式已弃用)
void resumeRPWork() {
//print("▶️ 恢复作业,设置_isPaused = false");
_logger.log("[恢复作业,设置_isPaused = false]");
_logger.log("[resumeRPWork] 已弃用 - 恢复由 HTTP recoveryTask 接口负责");
_isPaused = false;
/* ========== 以下为原有 TCP 发送恢复指令,已注释保留 ==========
var entity = new RoutePlanSendEntity(commandType: 0x01, pointCounts: 3, targetLatitude: 0, targetLongitude: 0, speed: 0);
// print("📡 发送恢复指令到设备...");
_logger.log("[发送恢复指令到设备...]");
tcpClient.sendDeviceStateChange(entity);
// 恢复后继续发送下一个点
// if (locationQueue.isNotEmpty) {
// print("📍 恢复发送下一个路径点");
// sendNextLocation();
// }
if (locationQueue.isNotEmpty) {
//print("📍 恢复发送下一个路径点");
_logger.log("[恢复发送下一个路径点]");
if (PathPlanningConfig.useAckHandshake) {
sendNextLocationWithAck(sl<NetMessageDispatcher>());
@@ -223,52 +215,53 @@ class PathPlanner {
sendNextLocation();
}
}
========== 原有 TCP 恢复指令结束 ========== */
}
/// 停止
/// 停止(已切换为 HTTP 接口 cancelTask,TCP 方式已弃用)
void stopRoutePlanning() {
// print("⏹️ 停止作业,设置_isStopped = true");
_logger.log("[停止作业,设置_isStopped = true]");
_logger.log("[stopRoutePlanning] 已弃用 - 停止由 HTTP cancelTask 接口负责");
_isStopped = true;
_isPaused = false; // 清除暂停状态
// 🔥 核心修复:清空队列
// print("🗑️ 清空待发送队列,剩余 ${locationQueue.length} 个点");
_logger.log("[清空待发送队列,剩余 ${locationQueue.length} 个点]");
_isPaused = false;
locationQueue.clear();
final devicesCubit = sl<DevicesCubit>();
devicesCubit.setArrivedLocation(0.0, 0.0);
/* ========== 以下为原有 TCP 发送停止指令,已注释保留 ==========
var entity = new RoutePlanSendEntity(commandType: 0x01, pointCounts: 0, targetLatitude: 0, targetLongitude: 0, speed: 0);
// print("📡 发送停止指令到设备...");
_logger.log("[发送停止指令到设备...] 按下停止按钮");
tcpClient.sendDeviceStateChange(entity);
// 发送完成工作播报指令
//sendWorkCompleteBroadcast();
========== 原有 TCP 停止指令结束 ========== */
}
//发送完成工作播报指令
// 发送完成工作播报指令(已切换为 HTTP 接口,TCP 方式已弃用)
void sendWorkCompleteBroadcast() {
_logger.log("[sendWorkCompleteBroadcast] 已弃用 - 由服务端 HTTP 接口负责");
/* ========== 以下为原有 TCP 发送完成播报指令,已注释保留 ==========
var entity = new RoutePlanSendEntity(commandType: 0x01, pointCounts: 0xFF, targetLatitude: 0, targetLongitude: 0, speed: 0);
// print("发送完成工作播报指令...");
_logger.log("[发送完成工作播报指令...]");
tcpClient.sendDeviceStateChange(entity);
========== 原有 TCP 完成播报指令结束 ========== */
}
// 🔥 新增:ACK 握手机制的发送方法 (新模式)
// 🔥 已弃用:路径点逐个下发已改为服务端通过 createDeviceTask HTTP 接口处理,
// APP 不再通过 TCP 逐个发送路径点,避免与服务器重复发送导致下位机收到双份数据。
void sendNextLocationWithAck(NetMessageDispatcher dispatcher) {
// print("[在发送指令 sendNextLocationWithAck 方法中 - ACK 模式]");
_logger.log("[sendNextLocationWithAck] 已弃用 - 路径点下发由服务端 HTTP 接口负责");
return;
/* ========== 以下为原有 TCP 逐个发点逻辑(ACK 模式),已注释保留 ==========
_logger.log("[在发送指令 sendNextLocationWithAck 方法中 - ACK 模式]");
// 🔥 关键检查:如果正在等待 ACK,不要重复发送
// 关键检查:如果正在等待 ACK,不要重复发送
if (_waitingForAck) {
//print("⏳ 正在等待 ACK 确认,跳过发送");
_logger.log("[正在等待 ACK 确认,跳过发送]");
return;
}
// 🔥 关键检查:暂停或停止时不再发送
// 关键检查:暂停或停止时不再发送
if (_isPaused) {
//print("⏸️ 当前处于暂停状态,停止发送指令");
_logger.log("[暂停状态]");
return;
}
@@ -276,7 +269,6 @@ class PathPlanner {
if (_isStopped || (isStart && locationQueue.isEmpty)) {
isStart = false;
if (_isStopped) {
// print("️ 已停止作业,清空队列");
_logger.log("[已停止作业,清空队列]");
locationQueue.clear(); // 清空剩余队列
} else {
@@ -289,26 +281,25 @@ class PathPlanner {
final entity = locationQueue.removeFirst(); // 类型:DeviceAddPathPointModel
// 🔥 核心改进:使用递增的点编号,确保精准匹配
// 核心改进:使用递增的点编号,确保精准匹配
final pointIndex = _currentPointIndex++;
final routePlanSendEntity = RoutePlanSendEntity(
commandType: 0x01,
pointCounts: 1, // 🔥 使用点编号,而不是固定的 1
pointCounts: 1,
targetLatitude: entity.latitude,
targetLongitude: entity.longitude,
speed: 1000,
);
//print("====Lat:${entity.latitude} ====Lng:${entity.longitude}");
_logger.log("[发送路径点] Lat:${entity.latitude} Lng:${entity.longitude}");
tcpClient.sendPathPoint(routePlanSendEntity);
isStart = true;
// 🔥 关键:标记为正在等待 ACK
// 标记为正在等待 ACK
_waitingForAck = true;
dispatcher.setExpectedPointIndex(pointIndex); // 🔥 通知 Dispatcher
//sprint(" 已锁定发送,等待 ACK 确认 (0x02)...");
dispatcher.setExpectedPointIndex(pointIndex); // 通知 Dispatcher
_logger.log("[已锁定发送,等待 ACK 确认 (0x02)...]");
========== 原有 TCP 逐个发点逻辑结束 ========== */
}
}