ops-console v1.0.2 (versionCode 8) 源码入库
This commit is contained in:
612
lib/services/device_service.dart
Normal file
612
lib/services/device_service.dart
Normal file
@@ -0,0 +1,612 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/alert.dart';
|
||||
import '../models/device.dart';
|
||||
import '../models/org_node.dart';
|
||||
import '../utils/constants.dart';
|
||||
import '../utils/session_org_scope.dart';
|
||||
import '../utils/store_local_cache.dart';
|
||||
import 'org_service.dart';
|
||||
import 'api_client.dart';
|
||||
|
||||
/// 设备与告警数据服务:对接真实「爱视后台」/api/device/* 接口。
|
||||
///
|
||||
/// - 设备列表:GET /device/info/list(响应三种形态均兼容,见 [_extractList]);
|
||||
/// - 设备详情:GET /device/info/info/{id};
|
||||
/// - 设备指令:POST /device/command/sendByMac | sendByOrg(service 已实现,
|
||||
/// 下发 UI 属下一轮迭代);
|
||||
/// - 告警:真实后端无 alerts 端点,real 模式下由设备列表推导
|
||||
/// (offline→critical、warning→warning);mock 模式使用示例告警。
|
||||
/// - 设备分组:GET/POST /device/group/*;
|
||||
/// - 激活码:POST/GET /device/activationCode/*;
|
||||
/// - 门店配置:GET/POST /device/storeConfig/*。
|
||||
class DeviceService {
|
||||
DeviceService({Dio? dio}) : _dio = dio ?? ApiClient.instance.dio;
|
||||
|
||||
final Dio _dio;
|
||||
|
||||
/// 获取设备总数量(后端分页响应中的 totalCount)。
|
||||
///
|
||||
/// GET /device/info/list?page=1&limit=1
|
||||
Future<int> fetchDeviceTotalCount() async {
|
||||
final response = await _dio.get<dynamic>(
|
||||
'/device/info/list',
|
||||
queryParameters: {'page': 1, 'limit': 1},
|
||||
);
|
||||
final data = response.data;
|
||||
if (data is Map<String, dynamic>) {
|
||||
final total = data['totalCount'];
|
||||
if (total is int) return total;
|
||||
if (total is String) return int.tryParse(total) ?? 0;
|
||||
// 部分后端把 total 放到 page.totalCount。
|
||||
final page = data['page'];
|
||||
if (page is Map<String, dynamic>) {
|
||||
final pageTotal = page['totalCount'];
|
||||
if (pageTotal is int) return pageTotal;
|
||||
if (pageTotal is String) return int.tryParse(pageTotal) ?? 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// 按状态统计设备数量(O(1):limit=1 仅取 totalCount,不下拉全量)。
|
||||
///
|
||||
/// [status] 取值:'online' / 'offline' / 'warning';传 null 表示全部。
|
||||
/// GET /device/info/list?page=1&limit=1&status=xxx
|
||||
Future<int> fetchDeviceCountByStatus({String? status}) async {
|
||||
final query = <String, dynamic>{'page': 1, 'limit': 1};
|
||||
if (status != null) query['status'] = status;
|
||||
final response = await _dio.get<dynamic>(
|
||||
'/device/info/list',
|
||||
queryParameters: query,
|
||||
);
|
||||
final data = response.data;
|
||||
if (data is Map<String, dynamic>) {
|
||||
final total = data['totalCount'];
|
||||
if (total is int) return total;
|
||||
if (total is String) return int.tryParse(total) ?? 0;
|
||||
final page = data['page'];
|
||||
if (page is Map<String, dynamic>) {
|
||||
final pageTotal = page['totalCount'];
|
||||
if (pageTotal is int) return pageTotal;
|
||||
if (pageTotal is String) return int.tryParse(pageTotal) ?? 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// 获取设备列表。
|
||||
Future<List<Device>> fetchDevices({
|
||||
int page = 1,
|
||||
int limit = AppConstants.deviceListPageSize,
|
||||
String? status,
|
||||
String? mac,
|
||||
String? deviceSn,
|
||||
String? deviceUid,
|
||||
String? groupId,
|
||||
String? storeOrgId,
|
||||
}) async {
|
||||
final response = await _dio.get<dynamic>(
|
||||
'/device/info/list',
|
||||
queryParameters: {
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
if (status != null) 'status': status,
|
||||
if (mac != null) 'mac': mac,
|
||||
if (deviceSn != null) 'deviceSn': deviceSn,
|
||||
if (deviceUid != null) 'deviceUid': deviceUid,
|
||||
if (groupId != null) 'groupId': groupId,
|
||||
if (storeOrgId != null) 'storeOrgId': storeOrgId,
|
||||
},
|
||||
);
|
||||
final list = _extractList(response.data);
|
||||
return list
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(Device.fromJson)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// 按多个门店(组织节点)批量拉取并合并设备列表。
|
||||
///
|
||||
/// 后端 `/device/info/list` 仅接受单个 `storeOrgId`,故对每个门店 orgId 并发
|
||||
/// 请求 [fetchDevices],再把结果合并、按 [Device.id] 去重(同一设备不会因归属
|
||||
/// 多个选中组织而重复出现)。
|
||||
///
|
||||
/// [storeOrgIds] 为空时直接走 [fetchDevices](等效于「全部门店」),保持原行为。
|
||||
Future<List<Device>> fetchDevicesByOrgSet(
|
||||
Set<String> storeOrgIds, {
|
||||
String? status,
|
||||
String? mac,
|
||||
String? deviceSn,
|
||||
String? deviceUid,
|
||||
String? groupId,
|
||||
}) async {
|
||||
if (storeOrgIds.isEmpty) {
|
||||
return fetchDevices(
|
||||
status: status,
|
||||
mac: mac,
|
||||
deviceSn: deviceSn,
|
||||
deviceUid: deviceUid,
|
||||
groupId: groupId,
|
||||
);
|
||||
}
|
||||
final lists = await Future.wait(
|
||||
storeOrgIds.map(
|
||||
(id) => _fetchDevicesForStore(
|
||||
id,
|
||||
status: status,
|
||||
mac: mac,
|
||||
deviceSn: deviceSn,
|
||||
deviceUid: deviceUid,
|
||||
groupId: groupId,
|
||||
),
|
||||
),
|
||||
);
|
||||
final map = <String, Device>{};
|
||||
for (final d in lists.expand((x) => x)) {
|
||||
map[d.id] = d;
|
||||
}
|
||||
return map.values.toList();
|
||||
}
|
||||
|
||||
/// 按单个门店 orgId 拉取设备列表(构造查询参数 + 解析)。
|
||||
///
|
||||
/// 抽取自 [fetchDevices] 的单店请求逻辑,供 [fetchDevicesByOrgSet] 复用,
|
||||
/// 避免多门店合并时重复拼装 queryParameters。
|
||||
Future<List<Device>> _fetchDevicesForStore(
|
||||
String storeOrgId, {
|
||||
String? status,
|
||||
String? mac,
|
||||
String? deviceSn,
|
||||
String? deviceUid,
|
||||
String? groupId,
|
||||
}) async {
|
||||
final response = await _dio.get<dynamic>(
|
||||
'/device/info/list',
|
||||
queryParameters: {
|
||||
'page': 1,
|
||||
'limit': AppConstants.deviceListPageSize,
|
||||
if (status != null) 'status': status,
|
||||
if (mac != null) 'mac': mac,
|
||||
if (deviceSn != null) 'deviceSn': deviceSn,
|
||||
if (deviceUid != null) 'deviceUid': deviceUid,
|
||||
if (groupId != null) 'groupId': groupId,
|
||||
'storeOrgId': storeOrgId,
|
||||
},
|
||||
);
|
||||
final list = _extractList(response.data);
|
||||
return list
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(Device.fromJson)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// 按 ID 获取设备详情;不存在时抛出 [DeviceNotFoundException]。
|
||||
///
|
||||
/// 注意:详情接口 /device/info/info/{id} 返回的 `status` / `lastSyncTime`
|
||||
/// 可能滞后于列表接口。Web 端设备列表走 /device/info/list 并显示最新在线状态,
|
||||
/// 因此本方法在拿到详情后,再用 [Device.mac] 到列表接口取一次最新状态并合并,
|
||||
/// 使手机端详情页与 Web 端列表/心跳保持一致。
|
||||
Future<Device> fetchDeviceById(String id) async {
|
||||
try {
|
||||
final response =
|
||||
await _dio.get<dynamic>('/device/info/info/$id');
|
||||
final data = response.data;
|
||||
if (data is! Map<String, dynamic>) {
|
||||
throw DeviceNotFoundException('未找到设备:$id');
|
||||
}
|
||||
final detail = Device.fromJson(data);
|
||||
return _mergeWithFreshListStatus(detail);
|
||||
} on DioException catch (e) {
|
||||
final error = e.error;
|
||||
if (error is ApiException) {
|
||||
throw DeviceNotFoundException('${error.message}($id)');
|
||||
}
|
||||
throw DeviceNotFoundException('设备详情加载失败:$id');
|
||||
}
|
||||
}
|
||||
|
||||
/// 用 [detail.mac] 从列表接口拉取最新状态并合并。
|
||||
///
|
||||
/// 列表接口 /device/info/list 与 Web 端同源,状态/心跳刷新更及时;
|
||||
/// 合并时以列表的 [status] 与 [lastSyncTime] 覆盖详情,其余字段保留详情。
|
||||
Future<Device> _mergeWithFreshListStatus(Device detail) async {
|
||||
if (detail.mac.isEmpty) return detail;
|
||||
try {
|
||||
final freshList = await fetchDevices(mac: detail.mac, limit: 1);
|
||||
if (freshList.isEmpty) return detail;
|
||||
final fresh = freshList.first;
|
||||
return detail.copyWith(
|
||||
status: fresh.status,
|
||||
lastSyncTime: fresh.lastSyncTime ?? detail.lastSyncTime,
|
||||
);
|
||||
} on Object {
|
||||
// 列表接口失败时不阻断详情展示,保持原样。
|
||||
return detail;
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新设备信息(修改分组、房号等)。
|
||||
///
|
||||
/// POST /device/info/update,body 至少含 `id` 与需修改字段。
|
||||
Future<void> updateDevice(Map<String, dynamic> data) async {
|
||||
await _dio.post<dynamic>('/device/info/update', data: data);
|
||||
}
|
||||
|
||||
/// 新增设备。
|
||||
///
|
||||
/// POST /device/info/save,必填字段参考 Web 端:groupId、mac。
|
||||
Future<void> saveDeviceInfo(Map<String, dynamic> data) async {
|
||||
await _dio.post<dynamic>('/device/info/save', data: data);
|
||||
}
|
||||
|
||||
/// 删除单个设备。
|
||||
///
|
||||
/// 后端 `POST /device/info/delete` 接收的是**裸 JSON 数组** `[id]`,与 Web 端
|
||||
/// 设备列表批量删除保持一致(Web 调用 `Ao(t)` → `$.adornData(t,!1)` →
|
||||
/// `JSON.stringify([id,...])`)。
|
||||
/// 旧实现误传 `{'ids': [id]}`(对象),后端无法解析请求体 → 返回「未知异常」。
|
||||
/// id 在可转为数字时按数字发送(复刻 Web 端 `Yo()` 的 `Number(id)` 行为),
|
||||
/// 否则保留原字符串,保证数字/字符串两种主键都能正确命中。
|
||||
Future<void> deleteDevice(String id) async {
|
||||
final idValue = int.tryParse(id) ?? id;
|
||||
await _dio.post<dynamic>('/device/info/delete', data: [idValue]);
|
||||
}
|
||||
|
||||
/// 按 MAC 查询设备(最多返回 1 条,用于判断设备是否已存在)。
|
||||
Future<Device?> fetchDeviceByMac(String mac) async {
|
||||
if (mac.isEmpty) return null;
|
||||
final devices = await fetchDevices(mac: mac, limit: 1);
|
||||
return devices.isNotEmpty ? devices.first : null;
|
||||
}
|
||||
|
||||
/// 扫码激活/绑定设备:把设备真正写入后端。
|
||||
///
|
||||
/// 逻辑:
|
||||
/// 1. 先按 MAC 查询设备是否已存在;
|
||||
/// 2. 存在则走 `/device/info/update` 更新归属(门店/分组/房号);
|
||||
/// 3. 不存在则走 `/device/info/save` 新增设备。
|
||||
///
|
||||
/// 这样手机端扫码激活后,Web 端设备列表能立即看到该设备。
|
||||
Future<void> activateDeviceByScan({
|
||||
required String mac,
|
||||
required String storeId,
|
||||
String? groupId,
|
||||
String? roomNo,
|
||||
}) async {
|
||||
final existing = await fetchDeviceByMac(mac);
|
||||
if (existing != null && existing.id.isNotEmpty) {
|
||||
final data = <String, dynamic>{
|
||||
'id': existing.id,
|
||||
'mac': mac,
|
||||
};
|
||||
if (groupId != null && groupId.isNotEmpty) data['groupId'] = groupId;
|
||||
if (roomNo != null && roomNo.isNotEmpty) data['roomId'] = roomNo;
|
||||
await updateDevice(data);
|
||||
return;
|
||||
}
|
||||
|
||||
final data = <String, dynamic>{
|
||||
'mac': mac,
|
||||
// 门店在后端设备表中通常映射为 orgId / storeOrgId;同时携带两个别名,
|
||||
// 兼容不同后端版本。
|
||||
'orgId': storeId,
|
||||
'storeOrgId': storeId,
|
||||
};
|
||||
if (groupId != null && groupId.isNotEmpty) data['groupId'] = groupId;
|
||||
if (roomNo != null && roomNo.isNotEmpty) data['roomId'] = roomNo;
|
||||
await saveDeviceInfo(data);
|
||||
}
|
||||
|
||||
/// 按 MAC 下发设备指令。
|
||||
///
|
||||
/// [command] 取值见 [DeviceCommand];[data] 为指令参数(可为空)。
|
||||
Future<void> sendCommandByMac({
|
||||
required String mac,
|
||||
required String command,
|
||||
Map<String, dynamic>? data,
|
||||
}) async {
|
||||
await _dio.post<dynamic>(
|
||||
'/device/command/sendByMac',
|
||||
data: {'mac': mac, 'command': command, 'data': data ?? <String, dynamic>{}},
|
||||
);
|
||||
}
|
||||
|
||||
/// 按组织下发设备指令(批量)。
|
||||
Future<void> sendCommandByOrg({
|
||||
required String orgId,
|
||||
required String command,
|
||||
Map<String, dynamic>? data,
|
||||
}) async {
|
||||
await _dio.post<dynamic>(
|
||||
'/device/command/sendByOrg',
|
||||
data: {
|
||||
'orgId': orgId,
|
||||
'command': command,
|
||||
'data': data ?? <String, dynamic>{},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取告警列表。
|
||||
///
|
||||
/// 真实后端无 alerts 端点:real 模式下由设备列表推导——
|
||||
/// 仅为异常设备生成告警(offline→critical、warning→warning);
|
||||
/// 在线设备的级别映射为 info(见 [alertLevelFor]),但不产生告警条目。
|
||||
/// mock 模式下返回内置示例告警。
|
||||
Future<List<Alert>> fetchAlerts() async {
|
||||
if (AppConstants.useMock) {
|
||||
final response = await _dio.get<dynamic>('/alerts');
|
||||
final data = response.data;
|
||||
if (data is List) {
|
||||
return data
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(Alert.fromJson)
|
||||
.toList();
|
||||
}
|
||||
return <Alert>[];
|
||||
}
|
||||
|
||||
final devices = await fetchDevices(limit: AppConstants.deviceListPageSize);
|
||||
// 受限账号:仅保留其组织子树内门店所属设备的告警,避免泄露跨组织设备告警。
|
||||
final scope = SessionOrgScope.instance;
|
||||
List<Device> scopedDevices = devices;
|
||||
if (scope.enabled) {
|
||||
final ids = await OrgService().scopedOrgIdSet();
|
||||
if (ids.isNotEmpty) {
|
||||
scopedDevices = devices.where((d) => ids.contains(d.storeId)).toList();
|
||||
}
|
||||
}
|
||||
final alerts = <Alert>[];
|
||||
for (final device in scopedDevices) {
|
||||
if (device.status == DeviceStatus.online) {
|
||||
continue;
|
||||
}
|
||||
alerts.add(_deriveAlert(device));
|
||||
}
|
||||
// 按级别严重程度排序:critical 在前。
|
||||
alerts.sort((a, b) => a.level.index.compareTo(b.level.index));
|
||||
return alerts;
|
||||
}
|
||||
|
||||
/// 设备状态 → 告警级别映射(offline→critical、warning→warning、online→info)。
|
||||
static AlertLevel alertLevelFor(DeviceStatus status) {
|
||||
switch (status) {
|
||||
case DeviceStatus.offline:
|
||||
return AlertLevel.critical;
|
||||
case DeviceStatus.warning:
|
||||
return AlertLevel.warning;
|
||||
case DeviceStatus.online:
|
||||
return AlertLevel.info;
|
||||
}
|
||||
}
|
||||
|
||||
/// 由异常设备推导告警条目。
|
||||
static Alert _deriveAlert(Device device) {
|
||||
final offline = device.status == DeviceStatus.offline;
|
||||
return Alert(
|
||||
id: 'derived-${device.id}',
|
||||
deviceId: device.id,
|
||||
deviceName: device.name,
|
||||
title: offline ? '设备离线' : '设备指标异常',
|
||||
message: offline
|
||||
? '设备 ${device.mac} 最近无心跳上报,请检查电源与网络连接。'
|
||||
: '设备 ${device.mac} 上报状态为「告警」,请登录后台查看详情。',
|
||||
level: alertLevelFor(device.status),
|
||||
occurredAt: device.lastSyncTime ?? DateTime.now(),
|
||||
acknowledged: false,
|
||||
);
|
||||
}
|
||||
|
||||
// ================ 设备分组管理 ================
|
||||
|
||||
/// 获取设备分组列表(分页,按门店筛选)。
|
||||
///
|
||||
/// GET /device/group/list?page=&limit=&orgId=&storeId=
|
||||
///
|
||||
/// 注意:Web 端该接口使用 `orgId` 作为门店过滤参数,移动端同时携带
|
||||
/// `orgId` 与 `storeId`,避免后端因参数名差异而忽略门店过滤,导致
|
||||
/// 返回全部门店分组。
|
||||
Future<List<OrgNode>> getDeviceGroupList({
|
||||
int page = 1,
|
||||
int limit = 20,
|
||||
required String storeId,
|
||||
}) async {
|
||||
final response = await _dio.get<dynamic>(
|
||||
'/device/group/list',
|
||||
queryParameters: {
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
'orgId': storeId,
|
||||
'storeId': storeId,
|
||||
},
|
||||
);
|
||||
final list = _extractList(response.data);
|
||||
return list
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(OrgNode.fromJson)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// 获取设备分组详情。
|
||||
///
|
||||
/// GET /device/group/info/{id}
|
||||
Future<OrgNode> getDeviceGroupInfo(String id) async {
|
||||
final response = await _dio.get<dynamic>('/device/group/info/$id');
|
||||
final data = response.data;
|
||||
if (data is! Map<String, dynamic>) {
|
||||
throw DeviceNotFoundException('设备分组信息加载失败:$id');
|
||||
}
|
||||
return OrgNode.fromJson(data);
|
||||
}
|
||||
|
||||
/// 新增设备分组。
|
||||
///
|
||||
/// POST /device/group/save,body 为分组数据。
|
||||
Future<void> saveDeviceGroup(Map<String, dynamic> data) async {
|
||||
await _dio.post<dynamic>('/device/group/save', data: data);
|
||||
}
|
||||
|
||||
/// 修改设备分组。
|
||||
///
|
||||
/// POST /device/group/update,body 为分组数据(含 id)。
|
||||
Future<void> updateDeviceGroup(Map<String, dynamic> data) async {
|
||||
await _dio.post<dynamic>('/device/group/update', data: data);
|
||||
}
|
||||
|
||||
/// 批量删除设备分组。
|
||||
///
|
||||
/// POST /device/group/delete,body 为 `{ids: [...]}`。
|
||||
Future<void> deleteDeviceGroups(List<String> ids) async {
|
||||
await _dio.post<dynamic>(
|
||||
'/device/group/delete',
|
||||
data: {'ids': ids},
|
||||
);
|
||||
}
|
||||
|
||||
// ================ 激活码管理 ================
|
||||
|
||||
/// 生成门店激活码。
|
||||
///
|
||||
/// POST /device/activationCode/generate,body 为 `{orgId, maxActivateCount}`。
|
||||
Future<void> generateActivationCode(
|
||||
String orgId,
|
||||
int maxActivateCount,
|
||||
) async {
|
||||
await _dio.post<dynamic>(
|
||||
'/device/activationCode/generate',
|
||||
data: {
|
||||
'orgId': orgId,
|
||||
'maxActivateCount': maxActivateCount,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取激活码信息。
|
||||
///
|
||||
/// GET /device/activationCode/info/{orgId}
|
||||
Future<Map<String, dynamic>> getActivationCodeInfo(String orgId) async {
|
||||
final response =
|
||||
await _dio.get<dynamic>('/device/activationCode/info/$orgId');
|
||||
final data = response.data;
|
||||
if (data is! Map<String, dynamic>) {
|
||||
throw DeviceNotFoundException('激活码信息加载失败:$orgId');
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/// 重置激活码。
|
||||
///
|
||||
/// POST /device/activationCode/reset/{orgId}
|
||||
Future<void> resetActivationCode(String orgId) async {
|
||||
await _dio.post<dynamic>('/device/activationCode/reset/$orgId');
|
||||
}
|
||||
|
||||
// ================ 门店配置 ================
|
||||
|
||||
/// 获取门店配置。
|
||||
///
|
||||
/// GET /device/storeConfig/info/{orgId}(与 Web 后台门店配置弹窗同源,
|
||||
/// 见 screen-front src/api/device.ts `getStoreConfig`)。
|
||||
Future<Map<String, dynamic>> getStoreConfig(String orgId) async {
|
||||
final response =
|
||||
await _dio.get<dynamic>('/device/storeConfig/info/$orgId');
|
||||
final data = response.data;
|
||||
if (data is! Map<String, dynamic>) {
|
||||
throw DeviceNotFoundException('门店配置加载失败:$orgId');
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/// 新增或修改门店配置。
|
||||
///
|
||||
/// POST /device/storeConfig/createOrUpdate(与 Web 后台 `createOrUpdateStoreConfig`
|
||||
/// 严格一致;早期误写 /saveOrUpdate 会导致后端路由不匹配、保存失效)。
|
||||
Future<void> createOrUpdateStoreConfig(Map<String, dynamic> data) async {
|
||||
await _dio.post<dynamic>(
|
||||
'/device/storeConfig/createOrUpdate',
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
|
||||
/// 重置门店配置密码。
|
||||
///
|
||||
/// POST /device/storeConfig/resetPassword/{orgId}
|
||||
Future<void> resetStoreConfigPassword(String orgId) async {
|
||||
await _dio.post<dynamic>(
|
||||
'/device/storeConfig/resetPassword/$orgId',
|
||||
);
|
||||
}
|
||||
|
||||
// ================ 扫码绑定(前端占位) ================
|
||||
|
||||
/// 扫码绑定设备到门店/分组。
|
||||
///
|
||||
/// 当前后端无扫码绑定接口,前端占位:本地记录绑定关系并写回设备归属,
|
||||
/// 不发起真实请求。
|
||||
///
|
||||
/// [scanPayload] 为扫码解析出的设备标识(mac/sn/二维码内容);
|
||||
/// [storeId]/[storeName] 为目标门店;[groupId] 为分组(可空)。
|
||||
///
|
||||
/// TODO(后端): POST /device/bind {scanPayload, storeId, groupId},
|
||||
/// 由后台按 scanPayload 定位设备并写归属。
|
||||
Future<void> bindDeviceByScan(
|
||||
String scanPayload,
|
||||
String storeId,
|
||||
String storeName,
|
||||
String? groupId,
|
||||
) async {
|
||||
// 占位:把绑定关系写入本地缓存(key = deviceId,此处以 scanPayload 作为标识)。
|
||||
await StoreLocalCache.instance.saveDeviceBinding(
|
||||
scanPayload,
|
||||
storeId,
|
||||
storeName,
|
||||
groupId,
|
||||
);
|
||||
// 占位:本地 updateDevice 写 groupId/storeName,便于详情页回显。
|
||||
try {
|
||||
await updateDevice({
|
||||
'id': storeId,
|
||||
'groupId': groupId ?? '',
|
||||
'storeName': storeName,
|
||||
});
|
||||
} on Object {
|
||||
// 真实后台可能没有该设备/接口,占位阶段忽略失败。
|
||||
}
|
||||
// TODO(后端): POST /device/bind {scanPayload, storeId, groupId}。
|
||||
debugPrint('[DeviceService.bindDeviceByScan] 占位未上报: '
|
||||
'payload=$scanPayload storeId=$storeId');
|
||||
}
|
||||
|
||||
// ================ 工具方法 ================
|
||||
|
||||
/// 兼容设备列表响应的三种形态:
|
||||
/// `{list: [...], totalCount}` / `{page: {list, totalCount}}` / 直接数组。
|
||||
static List<dynamic> _extractList(dynamic data) {
|
||||
if (data is List) {
|
||||
return data;
|
||||
}
|
||||
if (data is Map<String, dynamic>) {
|
||||
final list = data['list'];
|
||||
if (list is List) {
|
||||
return list;
|
||||
}
|
||||
final page = data['page'];
|
||||
if (page is Map<String, dynamic> && page['list'] is List) {
|
||||
return page['list'] as List;
|
||||
}
|
||||
}
|
||||
return const <dynamic>[];
|
||||
}
|
||||
}
|
||||
|
||||
/// 设备不存在异常。
|
||||
class DeviceNotFoundException implements Exception {
|
||||
const DeviceNotFoundException(this.message);
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
Reference in New Issue
Block a user