killapp/Assets/Scripts/Managers/DataManager.cs
“虞渠成” a95b257b0d feat: 新增OTA升级相关功能与设备列表缓存优化
1.  新增设备信息页OTA升级进度UI与本地持久化传输状态
2.  实现WiFi模式OTA非全屏进度订阅与重启等待逻辑
3.  新增设备列表本地缓存与加载、清空功能
4.  优化蓝牙管理:移除自动弹窗,改为提示用户手动开启蓝牙
5.  新增网络状态监听与设备在线状态刷新逻辑
6.  新增多语言文案与蓝牙未开启提示资源
7.  优化网络请求处理,新增静默错误提示参数
2026-08-27 13:11:55 +08:00

537 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using Kill.Network;
using Kill.Managers.ResponseModels;
using Kill.UI.Components;
using UnityEngine;
namespace Kill.Managers
{
public class DataManager : MonoBehaviour
{
public string selectedDeviceMac="";
public static DataManager Instance { get; private set; }
public DeviceInfo selectedDevice;
public List<DeviceInfo> OwnedDevices = new List<DeviceInfo>();
public List<DeviceInfo> SharedDevices = new List<DeviceInfo>();
public bool isOwner;
public bool hasWifi;
public bool hasBluetooth;
public FingerprintListResponseData fingerprintListData = new FingerprintListResponseData();
public List<FingerprintUserData> fingerprintUserDatas = new List<FingerprintUserData>();
public List<ScheduleTaskData> scheduleTaskDatas = new List<ScheduleTaskData>();
private void Awake()
{
Instance = this;
}
public async Task Init()
{
InitUser();
GetSelectedDeviceMac();
}
public string token = "";
public UserInfo userInfo = new UserInfo();
public void SetToken(string token,UserInfo userInfo)
{
this.token = token;
this.userInfo = userInfo;
string userData =JsonUtility.ToJson(userInfo);
Debug.Log(userData);
PlayerPrefs.SetString("token", token);
PlayerPrefs.SetString("userData", userData);
NetworkCtrl.Instance.RemoveGlobalHeader("token");
NetworkCtrl.Instance.AddGlobalHeader("token", token);
}
public void SavaSelectedDeviceMac(string mac)
{
selectedDeviceMac=mac;
PlayerPrefs.SetString("selectedDeviceMac", selectedDeviceMac);
}
public void GetSelectedDeviceMac()
{
selectedDeviceMac=PlayerPrefs.GetString("selectedDeviceMac", "");
}
#region 使
private const string OWNED_DEVICES_KEY = "ownedDevicesCache";
private const string SHARED_DEVICES_KEY = "sharedDevicesCache";
/// <summary>
/// 将当前 OwnedDevices/SharedDevices 持久化到本地(按当前登录用户隔离)
/// </summary>
public void SaveDeviceLists()
{
try
{
string userId = userInfo?.id ?? "";
string ownedJson = JsonUtility.ToJson(OwnedDevices != null ? new DeviceInfoListWrapper(OwnedDevices) : new DeviceInfoListWrapper());
string sharedJson = JsonUtility.ToJson(SharedDevices != null ? new DeviceInfoListWrapper(SharedDevices) : new DeviceInfoListWrapper());
PlayerPrefs.SetString(OWNED_DEVICES_KEY + "_" + userId, ownedJson);
PlayerPrefs.SetString(SHARED_DEVICES_KEY + "_" + userId, sharedJson);
PlayerPrefs.Save();
}
catch (Exception ex)
{
Debug.LogError($"[DataManager] 保存设备列表缓存失败: {ex.Message}");
}
}
/// <summary>
/// 从本地恢复 OwnedDevices/SharedDevices返回是否成功恢复
/// </summary>
public bool LoadDeviceLists()
{
try
{
string userId = userInfo?.id ?? "";
string ownedJson = PlayerPrefs.GetString(OWNED_DEVICES_KEY + "_" + userId, "");
string sharedJson = PlayerPrefs.GetString(SHARED_DEVICES_KEY + "_" + userId, "");
OwnedDevices = string.IsNullOrEmpty(ownedJson) ? new List<DeviceInfo>() : JsonUtility.FromJson<DeviceInfoListWrapper>(ownedJson)?.ToList() ?? new List<DeviceInfo>();
SharedDevices = string.IsNullOrEmpty(sharedJson) ? new List<DeviceInfo>() : JsonUtility.FromJson<DeviceInfoListWrapper>(sharedJson)?.ToList() ?? new List<DeviceInfo>();
return OwnedDevices.Count > 0 || SharedDevices.Count > 0;
}
catch (Exception ex)
{
Debug.LogError($"[DataManager] 读取设备列表缓存失败: {ex.Message}");
OwnedDevices = new List<DeviceInfo>();
SharedDevices = new List<DeviceInfo>();
return false;
}
}
/// <summary>
/// 清空本地缓存的设备列表(登出时调用)
/// </summary>
public void ClearDeviceLists()
{
string userId = userInfo?.id ?? "";
PlayerPrefs.DeleteKey(OWNED_DEVICES_KEY + "_" + userId);
PlayerPrefs.DeleteKey(SHARED_DEVICES_KEY + "_" + userId);
PlayerPrefs.Save();
OwnedDevices = new List<DeviceInfo>();
SharedDevices = new List<DeviceInfo>();
}
[Serializable]
private class DeviceInfoListWrapper
{
public List<DeviceInfo> items = new List<DeviceInfo>();
public DeviceInfoListWrapper() { }
public DeviceInfoListWrapper(List<DeviceInfo> source)
{
if (source != null) items.AddRange(source);
}
public List<DeviceInfo> ToList() => new List<DeviceInfo>(items);
}
#endregion
public void InitUser()
{
token = PlayerPrefs.GetString("token", "");
string userData = PlayerPrefs.GetString("userData", "");
userInfo = JsonUtility.FromJson<UserInfo>(userData);
NetworkCtrl.Instance.RemoveGlobalHeader("token");
NetworkCtrl.Instance.AddGlobalHeader("token",token);
Debug.Log(token);
}
public void ClearInfo()
{
token = "";
userInfo = new UserInfo();
PlayerPrefs.DeleteKey("token");
PlayerPrefs.DeleteKey("userData");
}
public async Task<bool> TokenLogin()
{
LoadingUI.Show();
try
{
// 获取用户详情
var response = await NetworkCtrl.Instance.Post<LoginResponse>("/api/v1/auth/token-login");
LoadingUI.Hide();
if(response.Data.code==200)
{
SetToken(response.Data.data.token,response.Data.data.user);
return true;
}
else
{
ClearInfo();
return false;
}
}
catch (Exception ex)
{
LoadingUI.Hide();
Debug.LogError($"TokenLogin 异常: {ex.Message}");
return false;
}
}
public DeviceConfig deviceConfig;
// 从服务器获取的原始配置快照,用于比对变化字段
private DeviceConfig _originalDeviceConfig;
#region
/// <summary>
/// 获取设备配置
/// </summary>
public async Task GetDeviceConfig(string deviceSn)
{
var response = await NetworkCtrl.Instance.Get<DeviceConfigResponse>(
"/api/v1/device/config",
new Dictionary<string, string> { { "device_sn", deviceSn } }
);
DeviceConfig result = null;
ResponseCodeHandler.HandleResponse(response,
onSuccess: (data) =>
{
result = data.data;
deviceConfig = result;
// 保存原始快照,用于后续差异对比
_originalDeviceConfig = result?.Clone();
},
onError: (code, msg) =>
{
Debug.LogError($"[DataManager] 获取设备配置失败: {msg}");
},
silentErrorTip: true
);
}
/// <summary>
/// 修改设备配置(只提交与原始配置相比发生变化的字段)
/// </summary>
/// <param name="deviceSn">设备 MAC/序列号</param>
/// <param name="config">当前设备配置</param>
/// <param name="original">原始配置快照,传 null 时会提交所有非空字段</param>
public async Task<bool> UpdateDeviceConfig(string deviceSn, DeviceConfig config, DeviceConfig original = null)
{
if (config == null)
{
Debug.LogError("[DataManager] UpdateDeviceConfig: config 为 null");
return false;
}
config.device_sn = deviceSn;
var changedFields = config.GetChangedFields(original);
// device_sn 必须始终携带
changedFields["device_sn"] = deviceSn;
if (changedFields.Count == 0)
{
Debug.Log("[DataManager] UpdateDeviceConfig: 没有字段变化,跳过同步");
return true;
}
var response = await NetworkCtrl.Instance.Put<NoDataResponse>("/api/v1/device/config", changedFields);
bool success = false;
ResponseCodeHandler.HandleResponse(response,
onSuccess: (data) =>
{
success = true;
},
onError: (code, msg) =>
{
Debug.LogError($"[DataManager] 修改设备配置失败: {msg}");
},
silentErrorTip: true
);
return success;
}
/// <summary>
/// 构建设备配置JSON只包含非null字段
/// </summary>
private string BuildDeviceConfigJson(DeviceConfig config)
{
var sb = new System.Text.StringBuilder();
sb.Append("{");
AppendField(sb, "device_sn", config.device_sn);
AppendField(sb, "language", config.language);
AppendField(sb, "is_locked", config.is_locked);
AppendField(sb, "fov_angle", config.fov_angle);
AppendField(sb, "detection_distance", config.detection_distance);
AppendField(sb, "aim_distance", config.aim_distance);
AppendField(sb, "work_mode", config.work_mode);
AppendField(sb, "fill_light_enable", config.fill_light_enable);
AppendField(sb, "fill_light_type", config.fill_light_type);
AppendField(sb, "fill_light_intensity", config.fill_light_intensity);
AppendField(sb, "laser_visible_enable", config.laser_visible_enable);
AppendField(sb, "visual_detect_enable", config.visual_detect_enable);
AppendField(sb, "visual_sensitivity", config.visual_sensitivity);
AppendField(sb, "radar_enable", config.radar_enable);
AppendField(sb, "radar_sensitivity", config.radar_sensitivity);
AppendField(sb, "radar_safe_distance", config.radar_safe_distance);
AppendField(sb, "rgb_enable", config.rgb_enable);
AppendField(sb, "rgb_red", config.rgb_red);
AppendField(sb, "rgb_green", config.rgb_green);
AppendField(sb, "rgb_blue", config.rgb_blue);
AppendField(sb, "rgb_effect", config.rgb_effect);
AppendField(sb, "lcd_auto_brightness", config.lcd_auto_brightness);
AppendField(sb, "lcd_brightness", config.lcd_brightness);
AppendField(sb, "lcd_sleep_enable", config.lcd_sleep_enable);
AppendField(sb, "lcd_sleep_time", config.lcd_sleep_time);
AppendField(sb, "wifi_enable", config.wifi_enable);
AppendField(sb, "wifi_ssid", config.wifi_ssid);
AppendField(sb, "wifi_password", config.wifi_password);
AppendField(sb, "video_record_enable", config.video_record_enable);
AppendField(sb, "record_duration", config.record_duration);
AppendField(sb, "sound_enable", config.sound_enable);
AppendField(sb, "sound_type", config.sound_type);
AppendField(sb, "volume", config.volume);
// 移除末尾逗号
if (sb[sb.Length - 1] == ',')
sb.Length--;
sb.Append("}");
return sb.ToString();
}
private void AppendField(System.Text.StringBuilder sb, string key, object value)
{
if (value == null) return;
sb.Append("\"");
sb.Append(key);
sb.Append("\":");
if (value is string s)
{
sb.Append("\"");
sb.Append(s);
sb.Append("\"");
}
else if (value is bool b)
{
sb.Append(b ? "true" : "false");
}
else
{
sb.Append(value.ToString());
}
sb.Append(",");
}
#endregion
/// <summary>
/// 同步设备配置到服务器WiFi在线时调用由各设置页面Sync方法调用
/// 只提交与原始配置相比发生变化的字段
/// </summary>
public async void SyncDeviceConfigToServer()
{
if (!hasWifi) return;
bool success = await UpdateDeviceConfig(selectedDevice.ble_mac, deviceConfig, _originalDeviceConfig);
if (success)
{
// 同步成功后刷新原始快照
_originalDeviceConfig = deviceConfig?.Clone();
}
}
/// <summary>
/// 更新定时任务到服务器
/// </summary>
public async Task<bool> UpdateScheduleTasksToServer(string deviceSn, List<ScheduleTaskData> tasks)
{
var requestData = new ScheduleTasksUploadRequest
{
device_sn = deviceSn,
tasks = tasks
};
var response = await NetworkCtrl.Instance.Put<NoDataResponse>(
"/api/v1/device/schedule-tasks", requestData);
bool success = false;
ResponseCodeHandler.HandleResponse(response,
onSuccess: (data) =>
{
success = true;
Debug.Log($"[DataManager] 定时任务已同步到服务器,共 {tasks.Count} 条");
},
onError: (code, msg) =>
{
Debug.LogError($"[DataManager] 同步定时任务失败: {msg}");
}
);
return success;
}
/// <summary>
/// 更新指纹列表到服务器
/// </summary>
public async Task<bool> UpdateFingerprintsToServer(string deviceSn, List<FingerprintUserData> fingerprints)
{
var requestData = new FingerprintsUploadRequest
{
device_sn = deviceSn,
fingerprints = fingerprints
};
var response = await NetworkCtrl.Instance.Put<NoDataResponse>(
"/api/v1/device/fingerprints", requestData);
bool success = false;
ResponseCodeHandler.HandleResponse(response,
onSuccess: (data) =>
{
success = true;
Debug.Log($"[DataManager] 指纹列表已同步到服务器,共 {fingerprints.Count} 条");
},
onError: (code, msg) =>
{
Debug.LogError($"[DataManager] 同步指纹列表失败: {msg}");
}
);
return success;
}
/// <summary>
/// 通过WiFi获取指纹列表
/// </summary>
public async Task GetFingerprints()
{
var response = await NetworkCtrl.Instance.Get<FingerprintListResponse>(
"/api/v1/device/fingerprints",
new Dictionary<string, string> { { "device_sn", selectedDevice.ble_mac } }
);
ResponseCodeHandler.HandleResponse(response,
onSuccess: (data) =>
{
fingerprintUserDatas.Clear();
if (data.data?.list != null)
{
fingerprintUserDatas.AddRange(data.data.list);
}
Debug.Log($"[DataManager] 获取到 {fingerprintUserDatas.Count} 条指纹记录");
},
onError: (code, msg) =>
{
Debug.LogError($"[DataManager] 获取指纹列表失败: {msg}");
}
);
}
/// <summary>
/// 通过WiFi获取定时任务列表
/// </summary>
public async Task GetScheduleTasks(string deviceSn)
{
var response = await NetworkCtrl.Instance.Get<ScheduleTaskListDataResponse>(
"/api/v1/device/schedule-tasks",
new Dictionary<string, string> { { "device_sn", deviceSn } }
);
ResponseCodeHandler.HandleResponse(response,
onSuccess: (data) =>
{
scheduleTaskDatas.Clear();
Debug.Log($"[DataManager] 定时任务API返回: total={data.data?.total}, list count={data.data?.list?.Count ?? 0}");
if (data.data?.list != null)
{
scheduleTaskDatas.AddRange(data.data.list);
foreach (var d in scheduleTaskDatas)
{
Debug.Log($"[DataManager] 任务 ID={d.task_id}, enabled={d.enabled}, start={d.start_hour}:{d.start_minute:D2}, end={d.end_hour}:{d.end_minute:D2}, mode={d.mode}, repeat={d.repeat_mask}");
}
}
Debug.Log($"[DataManager] 获取到 {scheduleTaskDatas.Count} 条定时任务");
},
onError: (code, msg) =>
{
Debug.LogError($"[DataManager] 获取定时任务列表失败: {msg}");
}
);
}
}
/// <summary>
/// 指纹用户数据(从服务器获取)
/// </summary>
[System.Serializable]
public class FingerprintUserData
{
public string username;
public int fingerprint_id;
public bool has_fingerprint;
}
}
// 响应模型
namespace Kill.Managers.ResponseModels
{
[System.Serializable]
public class FingerprintListResponse
{
public int code;
public string message;
public FingerprintListResponseData data;
}
[System.Serializable]
public class FingerprintListResponseData
{
public int total;
public int pages;
public int limit;
public int page;
public List<FingerprintUserData> list;
}
[System.Serializable]
public class ScheduleTaskListDataResponse
{
public int code;
public string message;
public ScheduleTaskListData data;
}
[System.Serializable]
public class ScheduleTaskListData
{
public int total;
public int pages;
public int limit;
public int page;
public List<ScheduleTaskData> list;
}
[System.Serializable]
public class ScheduleTaskData
{
public byte task_id;
public bool enabled;
public byte start_hour;
public byte start_minute;
public byte end_hour;
public byte end_minute;
public string mode;
public byte repeat_mask;
}
[System.Serializable]
public class ScheduleTasksUploadRequest
{
public string device_sn;
public List<ScheduleTaskData> tasks;
}
[System.Serializable]
public class FingerprintsUploadRequest
{
public string device_sn;
public List<FingerprintUserData> fingerprints;
}
}