#连接库初步完成

This commit is contained in:
CC
2026-01-27 18:44:49 +08:00
parent 8c39f0daf8
commit 949743972c
82 changed files with 1187 additions and 650 deletions

View File

@@ -1,16 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../domain/entities/user_entity.dart';
import 'app_user_state.dart';
class AppUserCubit extends Cubit<AppUserState> {
AppUserCubit() : super(const AppUserState());
void setAuth(UserEntity user) {
emit(state.copyWith(user));
}
void clearAuth() {
emit(const AppUserState());
}
}

View File

@@ -1,20 +0,0 @@
import 'package:equatable/equatable.dart';
import '../domain/entities/user_entity.dart';
class AppUserState extends Equatable {
final UserEntity? user; // 直接存实体,包含 userId, username,token 等所有信息
const AppUserState({this.user});
// 辅助属性:判断是否登录
bool get isLoggedIn => user != null;
AppUserState copyWith(UserEntity? user) {
return AppUserState(user: user ?? this.user);
}
// 必须重写 props,UI 才会只在数据真正变化时刷新
@override
List<Object?> get props => [user];
}

View File

@@ -1,4 +1,5 @@
class TCPConsts {
static const String TCP_IP = "1.95.137.212";
// static const String TCP_IP = "1.95.137.212";
static const String TCP_IP = "192.210.172.250";
static const int TCP_PORT = 9001;
}

View File

@@ -1,8 +1,9 @@
import '../../features/devices/domain/entities/device_entity.dart';
abstract class BaseModel {
BaseModel.fromJson(Map<String, dynamic> json);
abstract class BaseModel<E> {
/// 将 Model 转换为对应的 Domain Entity
E toEntity();
/// 将 Model 转换为 JSON(用于持久化或日志)
Map<String, dynamic> toJson();
toEntity();
BaseModel.fromEntity(DeviceEntity entity);
}

View File

@@ -1,8 +1,10 @@
import 'package:dio/dio.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart';
import 'package:maibu_satabot_v2/core/infrastructure/logging/sentry_logger_impl.dart';
import 'package:maibu_satabot_v2/core/logging/i_logger_service.dart';
import 'package:maibu_satabot_v2/core/network/tcp/tcp_connection.dart';
import 'package:maibu_satabot_v2/features/devices/data/datasources/impl/device_http_datasource_impl.dart';
import 'package:maibu_satabot_v2/features/devices/domain/repositories/device_repository.dart';
import 'package:maibu_satabot_v2/features/devices/domain/usecases/get_user_device_usecase.dart';
@@ -12,7 +14,7 @@ import 'package:maibu_satabot_v2/features/remote_control/domain/repositories/rem
import 'package:maibu_satabot_v2/features/remote_control/domain/usecase/diff_steer_usecase.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../features/auth/data/datasources/auth_http_datasource.dart';
import '../../features/auth/data/datasources/i_auth_http_datasource.dart';
import '../../features/auth/data/datasources/impl/auth_http_datasource_impl.dart';
import '../../features/auth/data/repositories/auth_repository_impl.dart';
import '../../features/auth/domain/repositories/auth_repository.dart';
@@ -20,16 +22,15 @@ import '../../features/auth/domain/usecases/login_usecase.dart';
import '../../features/auth/presentation/bloc/auth_cubit.dart';
import '../../features/auth/presentation/bloc/login_bloc.dart';
import '../../features/auth/presentation/bloc/login_cubit.dart';
import '../../features/devices/data/datasources/device_http_datasource.dart';
import '../../features/connectivity/presentation/bloc/connectivity_cubit.dart';
import '../../features/devices/data/datasources/i_device_http_datasource.dart';
import '../../features/devices/data/repositories/device_repository_impl.dart';
import '../../features/remote_control/presentation/bloc/remote_control_cubit.dart';
import '../app/app_user_cubit.dart';
import '../network/dio_client.dart';
import '../network/net_message_dispatcher.dart';
import '../network/tcp/tcp_client.dart';
import '../../features/auth/presentation/bloc/app_user_cubit.dart';
import '../network/http/dio_client.dart';
import '../router/app_router.dart';
import '../storage/impl/user_storage_impl.dart';
import '../storage/user_storage.dart';
import '../../features/auth/data/datasources/impl/auth_local_datasource_impl.dart';
import '../../features/auth/data/datasources/i_auth_local_datasource.dart';
final sl = GetIt.instance;
@@ -40,10 +41,10 @@ Future<void> init() async {
sl.registerLazySingleton<Dio>(() => DioClient.create());
/// 1.1.2 TcpClient:TCP客户端
sl.registerLazySingleton(() => TcpClient());
sl.registerLazySingleton(() => TcpConnection(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT));
/// 1.1.3 NetMessageDispatcher:消息调度器,并将 TcpClient 注入给它
sl.registerLazySingleton(() => NetMessageDispatcher(sl()));
sl.registerLazySingleton(() => ConnectivityCubit());
/// 1.2 --- 本地存储 (LocalStorage) ---
/// 1.2.1 SharedPreferences:本地存储库
@@ -51,19 +52,19 @@ Future<void> init() async {
sl.registerLazySingleton<SharedPreferences>(() => sharedPrefs);
/// 1.2.2 UserStorage:用户存储库
sl.registerLazySingleton<UserStorage>(
sl.registerLazySingleton<IAuthLocalDataSource>(
// 必须先注册上面本地存储库的实例
() => UserStorageImpl(sl<SharedPreferences>()),
() => AuthLocalDatasourceImpl(sl<SharedPreferences>()),
);
/// 1.3 Log工具Sentry
sl.registerLazySingleton<ILoggerService>(() => SentryLoggerImpl());
/// 2. 数据源 (DataSource)
sl.registerLazySingleton<AuthHttpDataSource>(
sl.registerLazySingleton<IAuthHttpDataSource>(
() => AuthHttpDataSourceImpl(sl()),
);
sl.registerLazySingleton<DeviceHttpDatasource>(
sl.registerLazySingleton<IDeviceHttpDatasource>(
() => DeviceHttpDatasourceImpl(sl()),
);
@@ -76,7 +77,10 @@ Future<void> init() async {
);
sl.registerLazySingleton<DeviceRepository>(() => DeviceRepositoryImpl(sl()));
sl.registerLazySingleton<RemoteControlRepository>(
() => RemoteControlRepositoryImpl(sl(), sl()),
() => RemoteControlRepositoryImpl(
sl<ConnectivityCubit>(),
sl<DiffSteerUseCase>(),
),
);
/// 4. 用例 (UseCase)
@@ -85,18 +89,17 @@ Future<void> init() async {
sl.registerLazySingleton(() => DiffSteerUseCase());
/// 5. 状态管理 (Cubit/Bloc)
sl.registerLazySingleton(() => AppUserCubit()); // AuthCubit 依赖它,必须先注册
sl.registerLazySingleton(() => AppUserCubit(sl())); // AuthCubit 依赖它,必须先注册
sl.registerLazySingleton(() => DevicesCubit(sl(), sl()));
sl.registerFactory(() => RemoteControlCubit(sl()));
/// 6. 认证 (Auth)
// --- 关键修改点 1: AuthCubit 必须在 GoRouter 之前注册,并传入参数 ---
sl.registerLazySingleton(
() => AuthCubit(
sl<UserStorage>(),
sl<TcpClient>(),
sl<AppUserCubit>(),
sl<NetMessageDispatcher>(),
() => AuthCubit(
storage: sl<IAuthLocalDataSource>(), // 对应构造函数第一个参数
appCubit: sl<AppUserCubit>(), // 对应第二个参数
connectivityCubit: sl<ConnectivityCubit>(), // 对应第三个参数
),
);

View File

@@ -1,32 +0,0 @@
import 'package:equatable/equatable.dart';
class UserEntity extends Equatable {
final String userId;
final String username;
final String nickname;
final String token;
final String? avatar;
final String? email;
final String? phone;
UserEntity({
required this.userId,
required this.username,
required this.nickname,
required this.token,
this.avatar,
this.email,
this.phone,
});
@override
List<Object?> get props => [
userId,
username,
nickname,
avatar,
token,
email,
phone,
];
}

View File

@@ -2,3 +2,7 @@ class Failure {
final String message;
Failure(this.message);
}
class NetworkFailure extends Failure {
NetworkFailure(super.message);
}

View File

@@ -11,26 +11,31 @@ import 'package:intl/intl.dart';
class SentryLoggerImpl implements ILoggerService {
File? _localLogFile;
/// --- 临时控制开关 ---
/// true: 上传到 Sentry + 本地存储
/// false: 只进行本地存储 + 控制台打印
bool get _isRemoteEnabled => 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();
// 只有在开启远程上传时才初始化 Sentry SDK
if (_isRemoteEnabled) {
await SentryFlutter.init((options) {
options.dsn = EnvConfig.sentryDsn;
options.environment = EnvConfig.environment;
options.tracesSampleRate = EnvConfig.isProduction ? 0.2 : 1.0;
});
await _collectDeviceInfo();
}
// 无论是否上传服务器,始终准备本地日志文件
await _prepareLocalFile();
}
// 准备本地文件路径
Future<void> _prepareLocalFile() async {
try {
final directory = await getApplicationDocumentsDirectory();
// 按天生成文件名,例如: 2024-05-20_logs.txt
final fileName =
"${DateFormat('yyyy-MM-dd').format(DateTime.now())}_logs.txt";
final fileName = "${DateFormat('yyyy-MM-dd').format(DateTime.now())}_logs.txt";
_localLogFile = File('${directory.path}/$fileName');
if (!await _localLogFile!.exists()) {
@@ -42,6 +47,8 @@ class SentryLoggerImpl implements ILoggerService {
}
Future<void> _collectDeviceInfo() async {
if (!_isRemoteEnabled) return;
final deviceInfo = DeviceInfoPlugin();
Sentry.configureScope((scope) async {
if (Platform.isAndroid) {
@@ -58,28 +65,45 @@ class SentryLoggerImpl implements ILoggerService {
@override
void captureException(dynamic exception, {dynamic stackTrace}) {
Sentry.captureException(exception, stackTrace: stackTrace);
final timestamp = DateTime.now().toIso8601String();
final logEntry = "[$timestamp] [ERROR] $exception\n$stackTrace\n";
// 1. 只有开启时上传
if (_isRemoteEnabled) {
Sentry.captureException(exception, stackTrace: stackTrace);
}
// 2. 始终写入本地
_writeToLocalFile(logEntry);
// 3. 始终控制台打印
if (kDebugMode) {
debugPrint(logEntry);
}
}
@override
void log(
String message, {
bool isWarning = false,
Map<String, dynamic>? data,
}) {
String message, {
bool isWarning = false,
Map<String, dynamic>? data,
}) {
final timestamp = DateTime.now().toIso8601String();
final level = isWarning ? 'WARNING' : 'INFO';
final logEntry = "[$timestamp] [$level] $message ${data ?? ''}\n";
// 1.发送日志到 Sentry
Sentry.captureMessage(
message,
level: isWarning ? SentryLevel.warning : SentryLevel.info,
);
// 1. 只有开启时上传到 Sentry
if (_isRemoteEnabled) {
Sentry.captureMessage(
message,
level: isWarning ? SentryLevel.warning : SentryLevel.info,
);
}
// 2.写入本地文件
// 2. 始终写入本地文件
_writeToLocalFile(logEntry);
// 3. 始终打印到控制台
if (kDebugMode) {
debugPrint(logEntry);
}
@@ -87,25 +111,29 @@ class SentryLoggerImpl implements ILoggerService {
Future<void> _writeToLocalFile(String entry) async {
try {
// 使用追加模式写入
await _localLogFile?.writeAsString(entry, mode: FileMode.append);
} catch (e) {
// 本地写入失败不应崩溃
// 这里的错误不再抛出,避免日志系统本身崩溃导致主业务中断
}
}
// 增加一个获取本地日志文件路径的方法,方便你以后做“导出日志”功能
Future<String?> getLocalLogPath() async {
return _localLogFile?.path;
}
@override
void addBreadcrumb(String message, {String? category}) {
Sentry.addBreadcrumb(Breadcrumb(message: message, category: category));
// 面包屑主要用于 Sentry 报错时的上下文,如果关闭了远程,记录本地即可(可选)
if (_isRemoteEnabled) {
Sentry.addBreadcrumb(Breadcrumb(message: message, category: category));
}
_writeToLocalFile("[BREADCRUMB] [$category] $message\n");
}
@override
void setUser(String userId) {
Sentry.configureScope((scope) => scope.setUser(SentryUser(id: userId)));
if (_isRemoteEnabled) {
Sentry.configureScope((scope) => scope.setUser(SentryUser(id: userId)));
}
}
}
}

View File

@@ -0,0 +1,14 @@
import 'dart:async';
import 'package:fpdart/fpdart.dart';
import '../../error/failure.dart';
enum ConnectionStatus { connected, disconnected, connecting }
abstract class IConnection {
Stream<List<int>> get rawStream;
Stream<ConnectionStatus> get statusStream;
Future<Either<Failure, Unit>> connect();
Future<void> disconnect();
Future<Either<Failure, Unit>> send(List<int> data);
}

View File

@@ -0,0 +1,3 @@
class BleClient{
}

View File

@@ -0,0 +1 @@
enum ControlMode { TCP, BLE, LOCAL, NONE, OTHER }

View File

@@ -1,8 +1,8 @@
import 'package:dio/dio.dart';
import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart';
import '../app/app_user_cubit.dart';
import '../di/injection.dart';
import '../../../features/auth/presentation/bloc/app_user_cubit.dart';
import '../../di/injection.dart';
class DioClient {
static Dio create() {

View File

@@ -0,0 +1,17 @@
class ConnectionConfig {
final String address; // IP 地址或蓝牙 MAC/UUID
final int? port; // TCP 专用端口
ConnectionConfig({
required this.address,
this.port,
});
// 方便快捷创建 TCP 配置
factory ConnectionConfig.tcp(String ip, int port) =>
ConnectionConfig(address: ip, port: port);
// 方便快捷创建 BLE 配置
factory ConnectionConfig.ble(String mac) =>
ConnectionConfig(address: mac);
}

View File

@@ -1,26 +0,0 @@
import 'dart:convert';
import 'protocol_decoder.dart';
import 'tcp/tcp_client.dart';
// 注意:这里需要导入你具体的 UserModel
// import '../../features/auth/data/models/user_model.dart';
class NetMessageDispatcher {
final TcpClient tcpClient;
NetMessageDispatcher(this.tcpClient);
/// 过滤特定指令的流
Stream<RawPacket> onCommand(int commandCode) {
return tcpClient.packetStream.where((p) => p.command == commandCode);
}
/// 示例解析方法:将 0x02 指令解析为 String
Stream<String> onStringMessage() {
return onCommand(0x02).map((p) => utf8.decode(p.payload));
}
/// 示例解析方法:将 0x12 指令解析为 JSON 并转为模型
Stream<Map<String, dynamic>> onJsonMessage(int cmd) {
return onCommand(cmd).map((p) => jsonDecode(utf8.decode(p.payload)));
}
}

View File

@@ -1,59 +0,0 @@
import 'dart:typed_data';
class RawPacket {
final int command; // 第三位指令位
final Uint8List payload; // 数据位
RawPacket({required this.command, required this.payload});
}
class ProtocolDecoder {
static const head = [0xAB, 0xAA];
static const tail = [0xAA, 0xAB];
final List<int> _buffer = [];
List<RawPacket> decode(List<int> newData) {
_buffer.addAll(newData);
List<RawPacket> packets = [];
while (_buffer.length >= 5) {
// 最小包长度:头2+指令1+尾2 = 5,加1位数据=6,但是心跳包是AB AA FF AA AB
// 1. 找头
int headIdx = -1;
for (int i = 0; i < _buffer.length - 1; i++) {
if (_buffer[i] == head[0] && _buffer[i + 1] == head[1]) {
headIdx = i;
break;
}
}
if (headIdx == -1) {
_buffer.clear(); // 没头,全是垃圾数据
break;
}
// 2. 找尾
int tailIdx = -1;
for (int i = headIdx + 2; i < _buffer.length - 1; i++) {
if (_buffer[i] == tail[0] && _buffer[i + 1] == tail[1]) {
tailIdx = i;
break;
}
}
if (tailIdx != -1) {
// 3. 提取
int cmd = _buffer[headIdx + 2];
Uint8List payload = Uint8List.fromList(
_buffer.sublist(headIdx + 3, tailIdx),
);
packets.add(RawPacket(command: cmd, payload: payload));
// 4. 移除已处理部分
_buffer.removeRange(0, tailIdx + 2);
} else {
break; // 还没传完
}
}
return packets;
}
}

View File

@@ -1,66 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/cupertino.dart';
import 'package:maibu_satabot_v2/core/network/protocol_decoder.dart';
class TcpClient {
Socket? _socket;
final _decoder = ProtocolDecoder();
// 使用 StreamController 将原始数据流暴露给外部,以便多个 DataSource 监听
final _controller = StreamController<RawPacket>.broadcast();
Stream<RawPacket> get packetStream => _controller.stream;
// 通过参数配置,不硬编码
Future<void> connect({required String host, required int port}) async {
try {
_socket = await Socket.connect(
host,
port,
timeout: const Duration(seconds: 5),
);
_socket!.listen(
(data) {
var packets = _decoder.decode(data);
for (var packet in packets) {
_controller.add(packet);
}
},
onDone: disconnect,
onError: (e) => disconnect(),
);
} catch (e) {
rethrow; // 向上抛出连接异常
}
}
void send(Map<String, dynamic> data) {
if (_socket == null) throw Exception("Socket not connected");
_socket!.write(jsonEncode(data));
}
void sendRaw(int cmd, List<int> payload) {
if (_socket == null) return;
final builder = BytesBuilder()
..addByte(0xAB)
..addByte(0xAA)
..addByte(cmd)
..add(payload)
..add([0x00, 0x00]) // CRC
..addByte(0xAA)
..addByte(0xAB);
_socket!.add(builder.takeBytes());
}
void disconnect() {
_socket?.destroy();
_socket = null;
_controller.close();
debugPrint("TCP Disconnected");
}
}

View File

@@ -0,0 +1,53 @@
import 'dart:io';
import 'dart:async';
import 'package:fpdart/fpdart.dart';
import '../../error/failure.dart';
import '../base/i_connection.dart';
class TcpConnection implements IConnection {
final String host;
final int port;
Socket? _socket;
final _statusController = StreamController<ConnectionStatus>.broadcast();
TcpConnection({required this.host, required this.port});
@override
Stream<List<int>> get rawStream => _socket ?? const Stream.empty();
@override
Stream<ConnectionStatus> get statusStream => _statusController.stream;
@override
Future<Either<Failure, Unit>> connect() async {
try {
_statusController.add(ConnectionStatus.connecting);
_socket = await Socket.connect(host, port, timeout: const Duration(seconds: 5));
_statusController.add(ConnectionStatus.connected);
_socket!.done.then((_) => _statusController.add(ConnectionStatus.disconnected));
return right(unit);
} catch (e) {
_statusController.add(ConnectionStatus.disconnected);
return left(NetworkFailure(e.toString()));
}
}
@override
Future<Either<Failure, Unit>> send(List<int> data) async {
if (_socket == null) return left(NetworkFailure("Socket未连接"));
try {
_socket!.add(data);
await _socket!.flush(); // 立即刷出
return right(unit);
} catch (e) {
return left(NetworkFailure(e.toString()));
}
}
@override
Future<void> disconnect() async {
await _socket?.close();
_socket = null;
_statusController.add(ConnectionStatus.disconnected);
}
}

View File

@@ -1,28 +0,0 @@
import 'dart:typed_data';
/// 极简化的 Codec:只负责中间那段变化的“负载数据”
class MachineProtocolCodec {
/// 仅封装 0x00 指令的数据部分 (Byte 3 到 Byte 10,共 8 字节)
static Uint8List encodeRemoteControlPayload({
required int left,
required int right,
int lift = 0,
int mower = 0,
int ignition = 0,
int emergency = 0,
}) {
final bd = ByteData(8); // 只分配 8 字节
int pos = 0;
bd.setInt16(pos, left, Endian.little);
pos += 2;
bd.setInt16(pos, right, Endian.little);
pos += 2;
bd.setUint8(pos++, lift);
bd.setUint8(pos++, mower);
bd.setUint8(pos++, ignition);
bd.setUint8(pos++, emergency);
return bd.buffer.asUint8List();
}
}

View File

@@ -0,0 +1,12 @@
import 'package:equatable/equatable.dart';
class PacketEntity extends Equatable{
final int cmdType;
final List<int> payload;
final List<int>? crc; // 心跳包可能为 null
const PacketEntity({required this.cmdType, required this.payload, this.crc});
@override
List<Object?> get props => [cmdType, payload, crc];
}

View File

@@ -0,0 +1,83 @@
import 'dart:async';
import 'dart:typed_data'; // 引入 Uint8List
import 'protocol_constants.dart';
import 'packet_entity.dart';
class PacketParserTransformer extends StreamTransformerBase<Uint8List, PacketEntity> {
@override
Stream<PacketEntity> bind(Stream<Uint8List> stream) {
final List<int> buffer = [];
final StreamController<PacketEntity> controller = StreamController<PacketEntity>();
stream.listen(
(data) {
// 将接收到的数据追加到 buffer
buffer.addAll(data);
while (buffer.length >= 5) { // 最小包长(心跳) AB AA FF AA AB
// 找 Header
int headIdx = -1;
for (int i = 0; i < buffer.length - 1; i++) {
if (buffer[i] == ProtocolConstants.header1 &&
buffer[i + 1] == ProtocolConstants.header2) {
headIdx = i;
break;
}
}
if (headIdx == -1) {
// 如果没有找到包头,丢弃已接收的数据,继续等待数据
buffer.clear(); // 直接清空缓冲区,可以避免部分无效数据的积累
break;
}
if (headIdx > 0) {
// 如果包头不是从缓冲区的开头开始,移除头部无效数据
buffer.removeRange(0, headIdx);
}
// 找 Footer
int footIdx = -1;
for (int i = 2; i < buffer.length - 1; i++) {
if (buffer[i] == ProtocolConstants.endFlag1 &&
buffer[i + 1] == ProtocolConstants.endFlag2) {
footIdx = i;
break;
}
}
if (footIdx != -1) {
final packetBytes = buffer.sublist(0, footIdx + 2); // 处理数据包
final cmd = packetBytes[2]; // 获取命令字节
if (cmd == ProtocolConstants.cmdHeartbeat) {
controller.add(PacketEntity(cmdType: cmd, payload: [])); // 心跳包没有负载
} else if (packetBytes.length >= 7) {
// 带 CRC 的包
final payload = packetBytes.sublist(3, packetBytes.length - 4);
final crc = packetBytes.sublist(packetBytes.length - 4, packetBytes.length - 2);
controller.add(PacketEntity(cmdType: cmd, payload: payload, crc: crc)); // 解析命令和负载
}
// 完成当前数据包的解析,移除已处理的包
buffer.removeRange(0, footIdx + 2);
} else {
break; // 数据不完整,等待更多数据
}
}
},
onError: (error) {
print("Stream error: $error");
controller.addError(error); // 如果发生错误,报告错误
},
onDone: () {
print("Stream closed.");
controller.close(); // 流关闭时,关闭控制器
},
);
return controller.stream;
}
}

View File

@@ -1,4 +1,4 @@
class MachineProtocolConstants {
class ProtocolConstants {
// 固定字节
static const int header1 = 0xAB;
static const int header2 = 0xAA;

View File

@@ -1,41 +0,0 @@
import 'dart:convert';
import 'package:maibu_satabot_v2/core/domain/entities/user_entity.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../features/auth/data/models/user_model.dart';
import '../user_storage.dart';
class UserStorageImpl implements UserStorage {
final SharedPreferences _prefs;
static const _userKey = 'auth_user';
UserStorageImpl(this._prefs);
@override
Future<void> saveUser(UserEntity user) async {
// 1. 将对象转为 Map -> 2. 将 Map 转为 JSON 字符串
final model = UserModel.fromEntity(user);
await _prefs.setString(_userKey, json.encode(model.toJson()));
}
@override
Future<UserEntity?> getUser() async {
final jsonString = _prefs.getString(_userKey);
if (jsonString == null) return null;
// 将 JSON 转回 Model,再转成 Entity 返回给业务层
final model = UserModel.fromJson(json.decode(jsonString));
return model.toEntity();
}
@override
Future<void> deleteUser() async {
await _prefs.remove(_userKey);
}
@override
Future<bool> hasUser() async {
return _prefs.containsKey(_userKey);
}
}

View File

@@ -1,8 +0,0 @@
import 'package:maibu_satabot_v2/core/domain/entities/user_entity.dart';
abstract class UserStorage {
Future<void> saveUser(UserEntity user);
Future<UserEntity?> getUser();
Future<void> deleteUser();
Future<bool> hasUser();
}