diff --git a/lib/common/models/mq/mq_enum_models.dart b/lib/common/models/mq/mq_enum_models.dart index 4399989..ff011c8 100644 --- a/lib/common/models/mq/mq_enum_models.dart +++ b/lib/common/models/mq/mq_enum_models.dart @@ -10,6 +10,9 @@ enum MqtActionEnum implements BaseEnum { PRINT("60", "打印变化"), FACE("70", "人脸变化"), TAKE_FOOD_BROADCAST("80", "定餐广播"), + + /// 指定终端在业务安全窗口上传当前日志文件。 + PULL_TERMINAL_LOG("90", "拉取终端当前日志"), ; @override diff --git a/lib/common/utils/app/app_mq_tool.dart b/lib/common/utils/app/app_mq_tool.dart index aa94f62..147cf66 100644 --- a/lib/common/utils/app/app_mq_tool.dart +++ b/lib/common/utils/app/app_mq_tool.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:common_utils/common_utils.dart'; @@ -129,6 +130,15 @@ class AppMqTool { logger.e("|$_tag|定餐广播执行错误orgStr|$orgStr|error|$e|stack|$stack|"); } return; + case MqtActionEnum.PULL_TERMINAL_LOG: + mqUpdateEventBus.fire(MqtActionEnum.PULL_TERMINAL_LOG); + // MQTT 回调不能等待文件扫描和上传,任务会在支付/识别结束后的安全窗口异步执行。 + unawaited(SyncCheckTool.instance.trigger( + ScheduledTaskType.terminalLogUpload, + currentLogOnly: true, + triggerType: ScheduledTaskTriggerType.mqtt, + )); + return; case MqtActionEnum.FACE: return; // final msg = MqttInvoiceMsg.fromJson(json.decode(orgStr)); @@ -152,8 +162,6 @@ class AppMqTool { // ); // mqUpdateEventBus.fire(MqtActionEnum.FACE); // break; - default: - break; } } diff --git a/lib/common/utils/app/control/app_control.dart b/lib/common/utils/app/control/app_control.dart index 5e2beb9..591841d 100644 --- a/lib/common/utils/app/control/app_control.dart +++ b/lib/common/utils/app/control/app_control.dart @@ -40,6 +40,7 @@ class AppControl extends GetxController { AppNetworkTool.closeListenNetwork(); TtsManage.dispose(); SyncCheckTool.instance.dispose(); + TerminalHeartbeatTool.stop(); } Future initAsync() async { @@ -57,6 +58,8 @@ class AppControl extends GetxController { await TtsManage.initialize(); await Future.delayed(const Duration(milliseconds: 500)); SyncCheckTool.instance.startAll(); + // 服务端按 180 秒心跳阈值判定设备在线,启动后必须开启周期上报,未绑定设备时上报内部会安全跳过。 + TerminalHeartbeatTool.start(); Get.offAllNamed(RouteNames.login); } } diff --git a/lib/common/utils/manage/mq_manager.dart b/lib/common/utils/manage/mq_manager.dart index 71c0745..7a491db 100644 --- a/lib/common/utils/manage/mq_manager.dart +++ b/lib/common/utils/manage/mq_manager.dart @@ -50,13 +50,33 @@ class MqManager { MqManager.mqttClient = null; } MqManager.state = MqttConnectionState.disconnected; + connectionFailureReason = null; + } + + /// 打印实际用于连接的参数,便于与外部MQTT工具比对定位notAuthorized;密码只展示长度与首尾几位。 + static void _logConnectParams( + String scene, + SysSetting config, { + required String clientId, + required String host, + required int port, + }) { + final password = config.mqPassword ?? ''; + final maskedPassword = password.length < 6 + ? "*(len:${password.length})" + : "${password.substring(0, 3)}***(len:${password.length})${password.substring(password.length - 3)}"; + logger.i( + "|$_tag|连接参数|scene:$scene|host:$host|port:$port|clientId:$clientId" + "|username:${config.mqUsername}|password:$maskedPassword" + "|tls:${port == 8883}|protocol:MQTT/3.1.1|", + ); } static Future initMqtt() async { // 获取配置 final config = await validate(); - if (ObjectUtil.isEmpty(config!)) return; - final port = int.tryParse(config.mqSocketPort!); + if (ObjectUtil.isEmpty(config)) return; + final port = int.tryParse(config!.mqSocketPort!); if (ObjectUtil.isEmpty(port)) return; final client = MqttServerClient.withPort( config.mqSocketAddress!, @@ -87,6 +107,13 @@ class MqManager { MqttConnectMessage().withProtocolName("MQTT").withProtocolVersion(4); client.connectionMessage = connMessage; + _logConnectParams( + "初始化", + config, + clientId: client.clientIdentifier, + host: client.server, + port: port, + ); try { final state = await client.connect(config.mqUsername, config.mqPassword); if (ObjectUtil.isNotEmpty(state)) { @@ -96,6 +123,7 @@ class MqManager { } } } catch (e) { + connectionFailureReason = "mqtt连接失败:$e"; logger.e("|$_tag|mqtt连接失败error:|$e|"); } } @@ -103,21 +131,61 @@ class MqManager { static Future refresh() async { final config = await validate(); if (ObjectUtil.isEmpty(config)) { + connectionFailureReason = "mq参数校验不通过,本地配置缺少连接必要字段"; logger.e("|$_tag||刷新mq|错误mq参数校验不通过"); return; } + final port = int.tryParse(config!.mqSocketPort!); + if (ObjectUtil.isEmpty(port)) { + connectionFailureReason = "mq端口非法:${config.mqSocketPort}"; + logger.e("|$_tag||刷新mq|错误mq端口非法|port:${config.mqSocketPort}|"); + return; + } if (ObjectUtil.isEmpty(mqttClient)) { await initMqtt(); - } else if (MqManager.state != MqttConnectionState.connected) { - final res = await mqttClient!.connect( - config!.mqUsername, - config.mqPassword, - ); - if (res != null && res.state != MqManager.state) { - MqManager.state = res.state; - } - } else { + } else if (MqManager.state == MqttConnectionState.connected) { logger.e("|$_tag||刷新mq|错误mq已连接"); + } else { + final client = mqttClient!; + // 旧client实例的clientId与地址在创建时已固化,配置变化后继续复用会拿旧clientId配新账密,被broker以notAuthorized拒绝。 + final stale = + client.clientIdentifier != config.mqClientId || + client.server != config.mqSocketAddress || + client.port != port; + if (stale) { + logger.w( + "|$_tag||刷新mq|mq连接参数已变化,重建client" + "|old:${client.server}:${client.port}/${client.clientIdentifier}" + "|new:${config.mqSocketAddress}:$port/${config.mqClientId}|", + ); + await closeMqtt(); + await initMqtt(); + return; + } + _logConnectParams( + "重连", + config, + clientId: client.clientIdentifier, + host: client.server, + port: client.port, + ); + try { + final res = await client.connect( + config.mqUsername, + config.mqPassword, + ); + if (res != null && res.state != MqManager.state) { + MqManager.state = res.state; + } + if (MqManager.state == MqttConnectionState.connected) { + connectionFailureReason = null; + } else { + connectionFailureReason = "mqtt连接异常,当前状态:${MqManager.state}"; + } + } catch (e) { + connectionFailureReason = "mqtt连接失败:$e"; + rethrow; + } } } @@ -135,6 +203,7 @@ class MqManager { static void _onConnected() async { logger.w("|$_tag|mqtt连接成功"); + connectionFailureReason = null; final config = await AppSettingTool.getSysSetting(); if (ObjectUtil.isNotEmpty(config)) { config.mqSyncTime = DateTime.now(); diff --git a/lib/common/widgets/toast/toast_widget.dart b/lib/common/widgets/toast/toast_widget.dart index db5bc9c..b25a015 100644 --- a/lib/common/widgets/toast/toast_widget.dart +++ b/lib/common/widgets/toast/toast_widget.dart @@ -17,7 +17,12 @@ class Toast { color: Colors.black, ), SizedBox(width: 10.w), - Text(msg, style: theme.typography.xl.copyWith(color: Colors.black)), + Flexible( + child: Text( + msg, + style: theme.typography.xl.copyWith(color: Colors.black), + ), + ), ]; return Container( padding: EdgeInsets.symmetric(horizontal: 30.w, vertical: 15.h), @@ -44,7 +49,12 @@ class Toast { color: Colors.white, ), SizedBox(width: 10.w), - Text(msg, style: theme.typography.xl.copyWith(color: Colors.white)), + Flexible( + child: Text( + msg, + style: theme.typography.xl.copyWith(color: Colors.white), + ), + ), ]; return Container( padding: EdgeInsets.symmetric(horizontal: 30.w, vertical: 15.h),