flutter架 构项目基础

This commit is contained in:
崔宝新
2026-09-04 15:22:20 +08:00
parent 70967229d0
commit 0c5aee59c0
265 changed files with 35484 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:new_learn/core/api/auth/model.dart';
import 'package:new_learn/core/models/user_model.dart';
import 'package:new_learn/core/network/dio_client.dart';
import 'package:new_learn/core/storage/user_storage.dart';
/// ============================================================
/// 【UserApi】用户相关接口(对齐 Vue 端 api/user/index.ts)
///
/// 分层职责(像 Vue 项目把接口收敛到 api 目录一样):
/// Page(UI) → UserApi(业务方法) → DioClient(Dio 封装) → 服务器
///
/// 一、已实现:
/// login → POST /api/v1/User/Login
/// getCurrentUserExtension → GET /api/v1/User/GetCurrentUserExtension
/// (登录后拿完整用户信息:昵称/头像/班级/等级等)
/// getCurrentUser → 统一入口(本地缓存优先)
/// updateUserAvatar → 更改用户头像
///
/// 二、后续继续写(对齐 Vue,用到再加):
/// checkStudyPackStatus / setChangeUserOnline
/// getUserPointLevel / UpdateGuideStatus / UpdateReportStatus
///
/// 三、高频坑:
/// 1. 登录密码 MD5 加密(对齐 Vue stringToMd5),提交前在 Api 层完成;
/// 2. getCurrentUserExtension 必须带 Token 才有效 ——
/// 登录成功已把 Token 写进 DioClient.accessToken,自动带上;
/// 3. 不要在这里 catch ApiException —— 交给页面统一处理展示。
/// ============================================================
class UserApi {
// ==================== 登录 ====================
/// 登录 → POST /api/v1/User/Login
/// 密码规则对齐 Vue 端 stringToMd5:MD5 加密后再提交
static Future<LoginResponse> login({
required String account,
required String password,
}) async {
final request = LoginRequest(
account: account.trim(),
password: _md5(password.trim()),
);
final data = await DioClient.instance.post(
'/api/v1/User/Login',
data: request.toJson(),
);
return LoginResponse.fromJson(data as Map<String, dynamic>);
}
/// 登录后拉取完整用户信息 → GET /api/v1/User/GetCurrentUserExtension
static Future<CurrentUserExtension> getCurrentUserExtension() async {
final data = await DioClient.instance.get(
'/api/v1/User/GetCurrentUserExtension',
);
return CurrentUserExtension.fromJson(data as Map<String, dynamic>);
}
/// 获取当前用户(统一入口,缓存优先):
/// 1. 有本地缓存 → 直接返回(不费流量不转圈);
/// 2. 无缓存或 forceRefresh → 调 GetCurrentUserExtension 拉最新
/// (Token 由 Dio 拦截器自动带进 header)并写回缓存;
/// 3. 接口失败 → 回退返回本地缓存,缓存也没有才抛异常。
/// 页面展示用户信息一律走这里,别直接调 getCurrentUserExtension。
static Future<CurrentUserExtension> getCurrentUser({
bool forceRefresh = false,
}) async {
final cached = await UserStorage.instance.readUserInfo();
if (!forceRefresh && cached != null) return cached;
try {
final ext = await getCurrentUserExtension();
await UserStorage.instance.saveUserInfo(ext);
return ext;
} catch (_) {
if (cached != null) return cached; // 网络失败回退本地缓存
rethrow;
}
}
/// 更改用户头像 → POST /api/v1/User/UpdateUserAvatar
///
/// 对齐 UniApp:头像先传 OSS,拿到 url 后再把 {avatar: url} 提交给业务接口。
static Future<void> updateUserAvatar(String avatar) async {
await DioClient.instance.post(
'/api/v1/User/UpdateUserAvatar',
data: {'avatar': avatar},
);
}
/// MD5 加密(UniApp stringToMd5 的等价实现)
static String _md5(String text) {
return md5.convert(utf8.encode(text)).toString();
}
}
+85
View File
@@ -0,0 +1,85 @@
/// ============================================================
/// 【auth 模块模型】登录相关实体 —— 对照 Vue 项目 src/api/login.ts
///
/// LoginRequest → 登录请求体(toJson 提交给服务器)
/// LoginResponse → 登录响应(token + 用户信息)
/// UserInfo → 用户信息子模型(info 字段)
///
/// 高频坑:
/// 1. fromJson 全部用 ?? 兜底,字段缺失/类型漂移不会闪崩;
/// 2. 数字用 (json['x'] as num?)?.toInt() 防 int/double 不一致;
/// 3. 兼容业务响应壳 {code, msg, data: {token, info}}。
/// ============================================================
library;
class LoginRequest {
const LoginRequest({required this.account, required this.password});
final String account;
final String password;
Map<String, dynamic> toJson() {
return {
'account': account,
'password': password,
};
}
}
class LoginResponse {
const LoginResponse({required this.token, required this.info});
final String token;
final UserInfo info;
factory LoginResponse.fromJson(Map<String, dynamic> json) {
// 兼容常见包装结构:后端可能直接返回 {token, info},
// 也可能包一层 {code, msg, data: {token, info}}(业务响应通用壳)。
// data 存在就取 data,否则直接用 json,两种都能解析到。
final payload = (json['data'] as Map<String, dynamic>?) ?? json;
return LoginResponse(
token: payload['token'] as String? ?? '',
info: UserInfo.fromJson(
(payload['info'] as Map<String, dynamic>?) ?? const {},
),
);
}
}
/// 用户信息(对齐 Vue 端 getUserInfo 的字段,未知的用 ?? 兜底)
class UserInfo {
const UserInfo({
this.userId = 0,
this.userName = '',
this.account = '',
this.avatar = '',
this.phone = '',
this.isShowReport = false,
});
final int userId;
final String userName;
final String account;
final String avatar;
final String phone;
final bool isShowReport;
factory UserInfo.fromJson(Map<String, dynamic> json) {
return UserInfo(
// userId 可能是字符串(后端统一风格),不能直接 as num
userId: _toInt(json['userId']),
userName: json['userName'] as String? ?? '',
account: json['account'] as String? ?? '',
avatar: json['avatar'] as String? ?? '',
phone: json['phone'] as String? ?? '',
isShowReport: json['isShowReport'] as bool? ?? false,
);
}
}
/// 兼容"字符串数字"(后端 userId 返回 "820466383020136")
int _toInt(dynamic v) {
if (v is num) return v.toInt();
if (v is String) return int.tryParse(v) ?? 0;
return 0;
}