已完成用户APP首期功能开发

交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
This commit is contained in:
czl231
2026-09-13 00:57:32 +08:00
parent a0a4bb1218
commit 3ef33b531d
793 changed files with 40959 additions and 1204 deletions

View File

@@ -57,6 +57,10 @@ const Map<int, String> _apiErrorMessages = {
1713: '服务暂时不可用,请稍后重试',
1714: '服务数据异常,请稍后重试',
1715: '请先登录',
2410: '短信验证码暂未开放',
2511: '最多可创建20个设备分组',
2512: '该分组名称已存在',
2513: '该新增请求已处理,请关闭表单并刷新分组列表',
};
final RegExp _chineseCharacterPattern = RegExp(r'[\u3400-\u9fff]');
@@ -112,13 +116,23 @@ class ApiClient {
final http.Client _client;
final UnauthorizedCallback? onUnauthorized;
/// 捕获当前会话而不暴露令牌,供跨页面事件拒绝前一个账号的迟到响应。
bool Function() captureSessionGuard() {
final token = _tokenProvider();
return () => token == _tokenProvider();
}
Future<Object?> get(String path, {bool authenticated = true}) =>
_send('GET', path, authenticated: authenticated);
/// 读取需要鉴权的二进制资源;资源不存在时返回空值。
Future<Uint8List?> getBytes(String path) async {
Future<Uint8List?> getBytes(
String path, {
String failureMessage = '头像加载失败',
String accept = 'image/jpeg, image/png',
}) async {
final request = http.Request('GET', Uri.parse('$baseUrl$path'));
request.headers['accept'] = 'image/jpeg, image/png';
request.headers['accept'] = accept;
final token = _tokenProvider();
if (token.isNotEmpty) request.headers['authorization'] = token;
final response = await _sendRequest(request);
@@ -131,7 +145,7 @@ class ApiClient {
}
if (response.statusCode == 404) return null;
if (response.statusCode < 200 || response.statusCode >= 300) {
throw ApiException(response.statusCode, '头像加载失败');
throw ApiException(response.statusCode, failureMessage);
}
return response.bodyBytes.isEmpty ? null : response.bodyBytes;
}
@@ -144,6 +158,24 @@ class ApiClient {
Future<Object?> put(String path, {Map<String, Object?>? body}) => _send('PUT', path, body: body);
/// 上传头像二进制,沿用统一鉴权、错误解析和会话失效处理。
Future<Object?> uploadAvatar(Uint8List bytes, String filename) async {
return uploadImage('/upload/avatar', bytes, filename);
}
/// 仅由仓储传入固定受控端点,沿用统一鉴权与图片体积约束。
Future<Object?> uploadImage(String path, Uint8List bytes, String filename) async {
if (bytes.isEmpty || bytes.length > 2 * 1024 * 1024) {
throw const ApiException(422, '请选择不超过 2MB 的 JPG 或 PNG 图片');
}
final request = http.MultipartRequest('POST', Uri.parse('$baseUrl$path'));
final token = _tokenProvider();
request.headers['accept'] = 'application/json';
if (token.isNotEmpty) request.headers['authorization'] = token;
request.files.add(http.MultipartFile.fromBytes('file', bytes, filename: filename));
return _decode(await _sendRequest(request), authenticated: true, requestToken: token);
}
Future<Object?> delete(String path, {Map<String, Object?>? body}) =>
_send('DELETE', path, body: body);