Merge branch 'feature/my' of http://1.95.137.212:57001/APP/FlutterApp into feature/my

This commit is contained in:
mmc
2026-03-06 17:20:30 +08:00
11 changed files with 376 additions and 34 deletions

View File

@@ -11,6 +11,7 @@ import 'package:maibu_satabot_v2/features/devices/domain/usecases/select_work_re
import 'package:maibu_satabot_v2/features/devices/domain/usecases/unbind_device_usecase.dart';
import 'package:maibu_satabot_v2/features/devices/domain/usecases/update_devicename_usecase.dart';
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubit.dart';
import 'package:maibu_satabot_v2/features/devices/services/path_planning_service.dart';
import 'package:maibu_satabot_v2/features/my/presentation/bloc/my_cubit.dart';
import 'package:maibu_satabot_v2/features/my/repository/my_repository.dart';
import 'package:maibu_satabot_v2/features/my/repository/my_repository_impl.dart';
@@ -71,9 +72,14 @@ Future<void> init() async {
sl.registerLazySingleton(
() => TcpClient(sl<UserStorage>(), getUserDeviceUseCase: sl<GetUserDeviceUseCase>(), switchDeviceUseCase: sl<SwitchDeviceUseCase>()),
);
sl.registerLazySingleton(() => PathPlanningService());
/// 1.1.3 NetMessageDispatcher:消息调度器,并将 TcpClient 注入给它
sl.registerLazySingleton(() => NetMessageDispatcher(sl()));
sl.registerLazySingleton(() => NetMessageDispatcher(
sl<TcpClient>(),
sl<RoutePlanningRepository>(),
sl<PathPlanningService>(),
));
/// 1.2 --- 本地存储 (LocalStorage) ---
/// 1.2.1 SharedPreferences:本地存储库
@@ -164,7 +170,7 @@ Future<void> init() async {
/// 5. 状态管理 (Cubit/Bloc)
sl.registerLazySingleton(() => AppUserCubit()); // AuthCubit 依赖它,必须先注册
sl.registerLazySingleton(() => GetDeviceLocationUseCase(sl()));
sl.registerLazySingleton(() => DevicesCubit(sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(),sl(),sl()));
sl.registerLazySingleton(() => DevicesCubit(sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(),sl(),sl(),sl()));
sl.registerFactory(() => RemoteControlCubit(sl()));
/* // 工厂模式(留存,每次获取新实例)

View File

@@ -7,9 +7,9 @@ class EnvConfig {
static String get sentryDsn {
// 根据环境切换 DSN(去 Sentry 后台创建两个项目:Test 和 Prod)
if (environment == 'prod') {
return 'https://your_prod_dsn@sentry.io/123';
return 'https://d5fdda78335793935593efa3f4ed11bb@o4510984107261952.ingest.de.sentry.io/4510984527937616';
}
return 'https://your_test_dsn@sentry.io/456';
return 'https://1a72051f8ac6c21cde96ded18a318217@o4510984107261952.ingest.de.sentry.io/4510984468824144';
}
static bool get isProduction => environment == 'prod';

View File

@@ -10,17 +10,26 @@ import 'package:intl/intl.dart';
class SentryLoggerImpl implements ILoggerService {
File? _localLogFile;
//调试模式开关
bool _isDebugModeEnabled = false;
@override
Future<void> init() async {
// await SentryFlutter.init((options) {
// options.dsn = EnvConfig.sentryDsn;
// options.environment = EnvConfig.environment;
// // 生产环境只采样 20% 的性能数据,测试环境全开
// options.tracesSampleRate = EnvConfig.isProduction ? 0.2 : 1.0;
// });
await _collectDeviceInfo();
_isDebugModeEnabled = false;
await SentryFlutter.init((options) {
options.dsn = EnvConfig.sentryDsn;
options.environment = EnvConfig.environment;
// 生产环境只采样 20% 的性能数据,测试环境全开
options.tracesSampleRate = EnvConfig.isProduction ? 0.2 : 1.0;
// 动态设置 Sentry 接收的日志级别
// 如果开启调试模式,接收 DEBUG;否则生产环境只接收 INFO 以上
options.debug = _isDebugModeEnabled; // 开启 SDK 内部调试
});
await _collectDeviceInfo();
await _prepareLocalFile();
}
@@ -93,6 +102,54 @@ class SentryLoggerImpl implements ILoggerService {
// 本地写入失败不应崩溃
}
}
@override
void logWithLevel(
String message, {
String level = 'INFO', // 'DEBUG', 'INFO', 'WARNING', 'ERROR'
Map<String, dynamic>? data,
}) {
final timestamp = DateTime.now().toIso8601String();
final logEntry = "[$timestamp] [$level] $message ${data ?? ''}\n";
// --- 策略判断 ---
// 1. 开发环境:全部允许
// 2. 生产环境:
// - 如果是 DEBUG 级别:只有 _isDebugModeEnabled 为 true 时才允许
// - 其他级别:始终允许
bool shouldProcess = !EnvConfig.isProduction ||
level != 'DEBUG' ||
_isDebugModeEnabled;
if (shouldProcess) {
// 写入本地文件
_writeToLocalFile(logEntry);
// 上报 Sentry
SentryLevel sentryLevel;
switch (level) {
case 'DEBUG': sentryLevel = SentryLevel.debug; break;
case 'WARNING': sentryLevel = SentryLevel.warning; break;
case 'ERROR': sentryLevel = SentryLevel.error; break;
default: sentryLevel = SentryLevel.info;
}
// 使用 addBreadcrumb 记录轨迹
Sentry.addBreadcrumb(Breadcrumb(
message: message,
level: sentryLevel,
data: data,
));
// 如果是 ERROR,额外捕获为一个事件
if (level == 'ERROR') {
Sentry.captureMessage(message, level: sentryLevel);
}
}
if (kDebugMode) {
debugPrint(logEntry);
}
}
// 增加一个获取本地日志文件路径的方法,方便你以后做“导出日志”功能
Future<String?> getLocalLogPath() async {

View File

@@ -6,6 +6,11 @@ abstract class ILoggerService {
bool isWarning = false,
Map<String, dynamic>? data,
});
void logWithLevel(
String message, {
String level = 'INFO', // 'DEBUG', 'INFO', 'WARNING', 'ERROR'
Map<String, dynamic>? data,
});
void addBreadcrumb(String message, {String? category});
void setUser(String userId);
}

View File

@@ -9,8 +9,8 @@ class DioClient {
final dio = Dio(
BaseOptions(
baseUrl: HttpApiConsts.baseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
connectTimeout: const Duration(seconds: 12),
receiveTimeout: const Duration(seconds: 12),
headers: {'Content-Type': 'application/json'},
),
);

View File

@@ -1,12 +1,20 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:maibu_satabot_v2/features/devices/data/models/device_add_path_point_model.dart';
import '../../features/devices/domain/repositories/route_planning_repository.dart';
import '../../features/devices/services/path_planning_service.dart';
import 'protocol_decoder.dart';
import 'tcp/tcp_client.dart';
// import '../../features/auth/data/models/user_model.dart';
class NetMessageDispatcher {
final TcpClient tcpClient;
NetMessageDispatcher(this.tcpClient);
final PathPlanningService _pathPlanningService;
final RoutePlanningRepository routePlanningRepository;
NetMessageDispatcher(this.tcpClient, this.routePlanningRepository, this._pathPlanningService);
/// 过滤特定指令的流
@@ -36,6 +44,85 @@ class NetMessageDispatcher {
return onCommand(cmd).map((p) => jsonDecode(utf8.decode(p.payload)));
}
//监听路径规划的开始作业的指令的应答
// if (data[0] == (byte) 0xAB && data[1] == (byte) 0xAA && data[2] == (byte) 0x01) {
// System.out.println("=====下位机回复成功=====" + data[0] + " " + data[1] + " " + data[2] + " " + data[3] + " " +
// data[4] + " " + data[5] + " " + data[6] + " " + data[7] + " " + data[8] + " " + data[9]);
// if (data[5] == (byte) 0x01) {
// Point point = new Point();
// //发送下一个指令
// }
/// 监听路径规划的开始作业指令的应答 (0x01 指令)
/// 协议格式:AB AA 01 [DATA] CRC(2) AA AB
/// 验证头部:data[0]=0xAB, data[1]=0xAA, data[2]=0x01
/// 检查状态位:data[5] == 0x01 表示回复成功
Stream<RawPacket> onPathPlanningResponse() {
debugPrint('[Dispatcher] 开始监听路径规划指令应答 (CMD: 0x01)');
return onCommand(0x01).where((packet) {
// 构造完整数据包用于验证
final fullData = <int>[
0xAB, 0xAA, 0x01,
...packet.payload,
0x00, 0x00, // CRC 占位
0xAA, 0xAB
];
// 验证头部协议
if (fullData[0] == 0xAB &&
fullData[1] == 0xAA &&
fullData[2] == 0x01) {
debugPrint('[Dispatcher] 下位机回复成功 - 完整数据包:${fullData.join(" ")}');
// 检查状态位 (索引 5 对应 payload 的第 2 个字节)
if (fullData.length > 5 && fullData[5] == 0x01) {
debugPrint('✅ [Dispatcher] 状态位验证通过:0x01 - 可以发送下一个指令');
///取全局的路径规划发送实体 queue
/// 发送下一个指令 移除上一条数据
var queue = _pathPlanningService.getQueue();
//去除第一条数据
queue.removeFirst();
_pathPlanningService.updateQueue(queue as List<DeviceAddPathPointModel>);
routePlanningRepository.startRoutePlanning(queue);
return true;
} else {
debugPrint('⚠️ [Dispatcher] 状态位异常:${fullData.length > 5 ? fullData[5].toRadixString(16) : "N/A"}');
}
} else {
debugPrint('❌ [Dispatcher] 协议头验证失败');
}
return false;
});
}
/// 监听特定状态的回复(可复用)
/// [commandCode] 指令码
/// [statusIndex] 状态字节在 payload 中的索引
/// [expectedStatus] 期望的状态值
Stream<RawPacket> onStatusResponse({
required int commandCode,
required int statusIndex,
required int expectedStatus,
}) {
debugPrint('🔍 [Dispatcher] 监听指令应答 CMD: 0x${commandCode.toRadixString(16)}, 期望状态:0x${expectedStatus.toRadixString(16)}');
return onCommand(commandCode).where((packet) {
if (packet.payload.length > statusIndex) {
final status = packet.payload[statusIndex];
if (status == expectedStatus) {
debugPrint('✅ [Dispatcher] 状态验证通过 - CMD: 0x${commandCode.toRadixString(16)}, 状态:0x${status.toRadixString(16)}');
return true;
} else {
debugPrint('⚠️ [Dispatcher] 状态不匹配 - 期望:0x${expectedStatus.toRadixString(16)}, 实际:0x${status.toRadixString(16)}');
}
} else {
debugPrint('❌ [Dispatcher] Payload 长度不足 - 索引:$statusIndex, 实际长度:${packet.payload.length}');
}
return false;
});
}
}

View File

@@ -28,7 +28,7 @@ class AuthTcpDatasourceImpl implements AuthTcpDatasource {
@override
Future<void> sendAuthPacket() async {
if (!_tcpClient.isConnected) {
throw Exception('TCP 未连接,无法发送认证包');
throw Exception('TCP 未连接,无法发送认证包sendAuthPacket');
}
final user = await _userStorage.getUser();
@@ -40,7 +40,7 @@ class AuthTcpDatasourceImpl implements AuthTcpDatasource {
final token = user.token!;
final authString = '$username:app:$token';
debugPrint('🔑 [AuthTcp] 准备发送认证包:$authString');
debugPrint('🔑 sendAuthPacket[AuthTcp] 准备发送认证包:$authString');
final authBytes = utf8.encode(authString);
@@ -57,7 +57,7 @@ class AuthTcpDatasourceImpl implements AuthTcpDatasource {
..addByte(0xAB);
_tcpClient.sendRawBytes(builder.takeBytes()); // 假设 TcpClient 增加了此方法,或用 socket.add
debugPrint('✅ [AuthTcp] 认证包已发送');
debugPrint('✅ sendAuthPacket[AuthTcp] 认证包已发送');
// 获取用户设备列表
try {
@@ -70,36 +70,36 @@ class AuthTcpDatasourceImpl implements AuthTcpDatasource {
await eitherResult.fold(
(failure) {
// 处理失败:打印日志或抛出异常
debugPrint('❌ [AuthTcp] 获取设备列表失败:$failure');
debugPrint('❌ sendAuthPacket [AuthTcp] 获取设备列表失败:$failure');
throw Exception('获取设备列表失败:$failure');
},
(devices) async {
// 处理成功:devices 现在是真正的 List<DeviceEntity>
if (devices.isEmpty) {
debugPrint('⚠️ [AuthTcp] 当前用户无可用设备,跳过切换步骤');
debugPrint('⚠️ sendAuthPacket[AuthTcp] 当前用户无可用设备,跳过切换步骤');
return;
}
// 取第一个设备
final DeviceEntity targetDevice = devices.first;
debugPrint('📱 [AuthTcp] 准备切换至默认设备:${targetDevice.deviceName}');
debugPrint('📱 sendAuthPacket[AuthTcp] 准备切换至默认设备:${targetDevice.deviceName}');
// 切换设备 (同样,如果 switchDeviceUseCase 也返回 Either,也需要 fold 处理)
final switchResult = await switchDeviceUseCase.deviceRepository.switchDevice("app",targetDevice.deviceName);
await switchResult.fold(
(failure) {
debugPrint('❌ [AuthTcp] 切换设备失败:$failure');
debugPrint('❌ sendAuthPacket[AuthTcp] 切换设备失败:$failure');
throw Exception('切换设备失败:$failure');
},
(success) {
debugPrint('✅ [AuthTcp] 设备切换成功,服务端应开始推送数据');
debugPrint('✅ sendAuthPacket[AuthTcp] 设备切换成功,服务端应开始推送数据');
},
);
},
);
} catch (e) {
debugPrint('❌ [AuthTcp] 设备订阅流程异常:$e');
debugPrint('❌ sendAuthPacket[AuthTcp] 设备订阅流程异常:$e');
rethrow;
}
}

View File

@@ -1,11 +1,14 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart';
import 'package:maibu_satabot_v2/core/domain/entities/user_entity.dart';
import 'package:maibu_satabot_v2/core/network/net_message_dispatcher.dart';
import '../../../../core/app/app_user_cubit.dart';
import '../../../../core/infrastructure/logging/sentry_logger_impl.dart';
import '../../../../core/logging/i_logger_service.dart';
import '../../../../core/network/tcp/tcp_client.dart';
import '../../../../core/storage/user_storage.dart';
import '../../../devices/domain/usecases/get_user_device_usecase.dart';
@@ -34,16 +37,26 @@ class AuthCubit extends Cubit<AuthState> {
/// App 启动时检查本地缓存
Future<void> appStarted() async {
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
// print("App 启动时检查本地缓存");
final user = await storage.getUser();
logger.logWithLevel(
'启动时检查本地缓存',
level: 'INFO',
data: {'user': user, 'data': user}
);
if (user != null) {
// 1. 建立 TCP 连接 (远程控制)
// print("App 启动时检查本地缓存user != null) ");
// 1. print("App 启动时检查本地缓存user != null) ");
// print("App 启动时检查本地缓存 tcp.connect) ");
tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
//await _authTcpDatasource.sendAuthPacket();//包括发送认证包和获取列表和切换函数
// await _authTcpDatasource.sendAuthPacket();//包括发送认证包和获取列表和切换函数
// 2. 同步全局 App 状态
appCubit.setAuth(user);
// 3. 进入已登录状态
emit(AuthAuthenticated(user));
} else {
// print("App 启动时检查本地缓存user=null) ");
emit(AuthUnauthenticated());
}
}

View File

@@ -1,10 +1,12 @@
import 'package:dio/dio.dart';
import 'package:flutter/widgets.dart';
import 'package:get_it/get_it.dart';
import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart';
import 'package:maibu_satabot_v2/core/network/tcp/tcp_client.dart';
import 'package:maibu_satabot_v2/features/devices/data/datasources/device_http_datasource.dart';
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart';
import '../../../../../core/infrastructure/logging/sentry_logger_impl.dart';
import '../../../../../core/logging/i_logger_service.dart';
import '../../../../../core/storage/user_storage.dart';
import '../../../domain/entities/device_location_entity.dart';
@@ -24,14 +26,43 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
@override
Future<int> bindDevice(String deviceId, String deviceAlias) async {
var response = await dio.post(HttpApiConsts.bindDevice, data: {'deviceId': deviceId, 'deviceAlias': deviceAlias});
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
logger.logWithLevel('用户点击bindDevice方法开始触发', level: 'INFO');
logger.logWithLevel(
'用户点击bindDevice方法API 请求开始',
level: 'INFO',
data: {'url': 'https://serviceri.satabot.com/iot/device/bindDevice'}
);
var response = await dio.post(
HttpApiConsts.bindDevice,
data: {'deviceId': deviceId, 'deviceAlias': deviceAlias},
);
logger.logWithLevel(
'请求详细参数',
level: 'DEBUG',
data: {
'url': 'https://serviceri.satabot.com/iot/device/bindDevice',
'deviceId': deviceId,
'deviceAlias': deviceAlias
}
);
if (response.statusCode != 200) {
logger.logWithLevel(
'API 响应失败',
level: 'ERROR',
data: {'statusCode': response.statusCode, 'data': response.data}
);
throw Exception('网络请求失败:${response.statusCode}');
}
final responseData = response.data;
if (responseData['code'] != 200 || responseData['data'] != true) {
logger.logWithLevel(
'API 响应失败',
level: 'ERROR',
data: {'statusCode': response.statusCode, 'data': response.data}
);
throw Exception(responseData['msg'] ?? '业务异常');
}
@@ -76,7 +107,15 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
@override
Future<int> switchDevice(String platform, String deviceId) async {
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
final token = await _getToken();
print('用户点击switchDevice方法开始触发: $token');
logger.logWithLevel('用户点击switchDevice方法开始触发', level: 'DEBUG');
logger.logWithLevel(
'用户点击switchDevice方法API 请求开始',
level: 'INFO',
data: {'url': 'https://serviceri.satabot.com/iot/device/switchDevice'}
);
var response = await dio.post(
HttpApiConsts.switchDevice,
data: {'platform': platform, 'deviceId': deviceId},
@@ -87,16 +126,38 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
},
),
);
logger.logWithLevel(
'请求详细参数',
level: 'DEBUG',
data: {
'url': 'https://serviceri.satabot.com/iot/device/switchDevice',
'platform': platform,
'deviceId': deviceId
}
);
if (response.statusCode != 200) {
logger.logWithLevel(
'API 请求失败',
level: 'ERROR',
data: {'statusCode': response.statusCode, 'data': response.data}
);
throw Exception('网络请求失败:${response.statusCode}');
}
final responseData = response.data;
logger.logWithLevel(
'API 响应成功',
level: 'INFO',
data: {'statusCode': response.statusCode, 'data': response.data}
);
if (responseData['code'] != 200 || responseData['data'] != true) {
logger.logWithLevel(
'API 响应失败',
level: 'ERROR',
data: {'statusCode': response.statusCode, 'data': response.data}
);
throw Exception(responseData['msg'] ?? '业务异常');
}
return 1;
}
@@ -135,9 +196,19 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
@override
Future<dynamic> getDeviceLocation(String deviceame) async {
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
// TODO: implement getDeviceLocation
try {
// 发起 GET 请求
logger.logWithLevel('用户点击getDeviceLocation方法开始触发', level: 'INFO');
logger.logWithLevel(
'请求详细参数',
level: 'DEBUG',
data: {
'url': 'https://serviceri.satabot.com/iot/device/userDevice',
'tenantName': deviceame
}
);
final response = await dio.get(
'https://serviceri.satabot.com/iot/device/userDevice',
queryParameters: {'tenantName': deviceame},
@@ -152,17 +223,35 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
// 检查响应状态码
if (response.statusCode == 200) {
final data = response.data;
logger.logWithLevel(
'API 响应接收',
level: 'DEBUG',
data: {'statusCode': response.statusCode, 'data': response.data}
);
if (data['code'] == 200) {
//return data['data']; // 返回设备数据
final locationData = data['data']; // 假设数据结构,测试时候如有不妥就修改结果的实例化
return DeviceLocationEntity(deviceName: locationData['deviceName'], latitude: locationData['latitude'], longitude: locationData['longitude']);
logger.logWithLevel(
'成功获取设备位置',
level: 'INFO',
data: {'lat': locationData['latitude'], 'lng': locationData['longitude']}
);
return DeviceLocationEntity(
deviceName: locationData['deviceName'],
latitude: locationData['latitude'],
longitude: locationData['longitude'],
);
} else {
logger.logWithLevel(data['msg'] ?? '业务异常', level: 'ERROR');
throw Exception(data['msg'] ?? '业务异常');
}
} else {
logger.logWithLevel('网络请求失败: ${response.statusCode}', level: 'ERROR');
throw Exception('网络请求失败: ${response.statusCode}');
}
} catch (e) {
logger.logWithLevel('获取设备位置失败: $e', level: 'ERROR');
logger.captureException(e, stackTrace: e);
throw Exception('获取设备位置失败: $e');
}

View File

@@ -20,6 +20,7 @@ import '../../domain/usecases/delete_work_record_usecase.dart';
import '../../domain/usecases/get_device_location_usecase.dart';
import '../../domain/usecases/get_work_record_usecase.dart';
import '../../domain/usecases/route_planning_usecase.dart';
import '../../services/path_planning_service.dart';
import 'device_status_bloc.dart';
import 'device_status_event.dart';
import 'devices_state.dart';
@@ -39,6 +40,9 @@ class DevicesCubit extends Cubit<DevicesState> {
final RoutePlanningUseCase _routePlanningUseCase; //
final DeviceStatusBloc _deviceStatusBloc; // 🔥 新增字段
final TcpClient _tcpClient;
// 🔥 关键:注入全局服务
final PathPlanningService _pathPlanningService;
DevicesCubit(
this.repository,
this._getUserDeviceUseCase,
@@ -52,9 +56,8 @@ class DevicesCubit extends Cubit<DevicesState> {
this._generatePathUseCase,
this._routePlanningUseCase,
this._bindDeviceUseCase,
this._deviceStatusBloc, this._tcpClient,
this._deviceStatusBloc,
this._tcpClient, this._pathPlanningService,
) : super(const DevicesState());
Future<void> unbindDevice(String deviceId, String deviceName) async {
@@ -346,14 +349,21 @@ class DevicesCubit extends Cubit<DevicesState> {
// 开始路径规划
Future<void> startRoutePlanning(Queue<DeviceAddPathPointModel> locationQueue) async {
// 清空全局 Service 中的队列
// _routePlanningUseCase.clearLocationQueue();
_pathPlanningService.updateQueue(locationQueue as List<DeviceAddPathPointModel>);
// 开始路径规划
emit(state.copyWith(isLoading: true));
final result = await _routePlanningUseCase.startRoutePlanning(locationQueue);
//传入全局的Service 中路径规划队列
Queue<DeviceAddPathPointModel> queue = _pathPlanningService.getQueue() ;
final result = await _routePlanningUseCase.startRoutePlanning(queue);
result.fold(
(failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message ?? '路径规划启动失败')),
(_) => emit(state.copyWith(isLoading: false, errorMessage: '')),
);
}
// 暂停
Future<void> pauseRoutePlanning() async {
emit(state.copyWith(isLoading: true));

View File

@@ -0,0 +1,75 @@
// lib/features/devices/domain/services/path_planning_service.dart
import 'dart:collection';
import 'package:flutter/cupertino.dart';
import '../data/models/device_add_path_point_model.dart';
class PathPlanningService {
static final PathPlanningService _instance = PathPlanningService._internal();
factory PathPlanningService() => _instance;
PathPlanningService._internal();
// 🔥 核心:全局唯一的可变队列
final Queue<DeviceAddPathPointModel> locationQueue = Queue();
bool isRunning = false;
int currentIndex = 0;
int totalCount = 0;
/// 🔥 存:生成路径后立即存储
void updateQueue(List<DeviceAddPathPointModel> locations) {
if (locations.isEmpty) {
debugPrint('⚠️ [PathPlanningService] 尝试更新空队列');
return;
}
locationQueue.clear();
locationQueue.addAll(locations);
currentIndex = 0;
totalCount = locations.length;
debugPrint('✅ [PathPlanningService] 队列已更新,共 $totalCount 个点');
}
/// 🔥 取:发送时取出第一个点
DeviceAddPathPointModel? removeFirst() {
if (locationQueue.isEmpty) {
debugPrint('⚠️ [PathPlanningService] 队列为空');
return null;
}
final point = locationQueue.removeFirst();
currentIndex++;
debugPrint(
'📍 [PathPlanningService] 取出第 $currentIndex/$totalCount 个点:'
'${point.latitude}, ${point.longitude}',
);
return point;
}
/// 🔥 获取进度
int get remainingCount => locationQueue.length;
int get completedCount => currentIndex;
double get progress => totalCount == 0 ? 0.0 : currentIndex / totalCount;
bool get isEmpty => locationQueue.isEmpty;
bool get isNotEmpty => locationQueue.isNotEmpty;
/// 🔥 清空(作业完成或停止时)
void clear() {
locationQueue.clear();
isRunning = false;
currentIndex = 0;
totalCount = 0;
debugPrint('🧹 [PathPlanningService] 队列已清空');
}
int get length => locationQueue.length;
/// 获取当前队列(供 Cubit 使用)
Queue<DeviceAddPathPointModel> getQueue() {
return Queue.of(locationQueue);
}
}