完善移动端错误提示中文化
This commit is contained in:
@@ -1,7 +1,78 @@
|
||||
// 功能描述:封装用户端 HTTP 请求,并将服务端错误转换为安全、可读的中文提示。
|
||||
// 版本:1.1.0
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
const Map<int, String> _apiErrorMessages = {
|
||||
404: '记录不存在',
|
||||
1001: '请求标识缺失',
|
||||
1002: '登录信息缺失,请重新登录',
|
||||
1003: '接口密钥缺失',
|
||||
1004: '请求头参数缺失',
|
||||
1101: '暂无数据',
|
||||
1102: '请求解析失败',
|
||||
1103: '请填写必填参数',
|
||||
1104: '无权执行此操作',
|
||||
1105: '服务端数据处理失败',
|
||||
1106: '服务端数据处理失败',
|
||||
1107: '服务内部错误,请稍后重试',
|
||||
1108: '密码错误',
|
||||
1110: '账号已禁用',
|
||||
1111: '当前记录已停用',
|
||||
1112: '记录不存在',
|
||||
1301: '登录状态已失效,请重新登录',
|
||||
1302: '登录状态已失效,请重新登录',
|
||||
1303: '登录状态已失效,请重新登录',
|
||||
1304: '登录状态已失效,请重新登录',
|
||||
1305: '登录状态已失效,请重新登录',
|
||||
1306: '登录状态已失效,请重新登录',
|
||||
1307: '登录状态已变更,请重新登录',
|
||||
1308: '登录已过期,请重新登录',
|
||||
1309: '登录状态已失效,请重新登录',
|
||||
1310: '登录状态已失效,请重新登录',
|
||||
1311: '登录状态已失效,请重新登录',
|
||||
1312: '登录状态已失效,请重新登录',
|
||||
1313: '登录失败,请稍后重试',
|
||||
1314: '登录状态已失效,请重新登录',
|
||||
1501: '服务暂时不可用,请稍后重试',
|
||||
1502: '服务暂时不可用,请稍后重试',
|
||||
1503: '服务暂时不可用,请稍后重试',
|
||||
1504: '服务暂时不可用,请稍后重试',
|
||||
1505: '服务暂时不可用,请稍后重试',
|
||||
1506: '服务暂时不可用,请稍后重试',
|
||||
1507: '服务暂时不可用,请稍后重试',
|
||||
1702: '操作已取消',
|
||||
1703: '操作失败,请稍后重试',
|
||||
1704: '请求参数错误',
|
||||
1705: '请求超时,请稍后重试',
|
||||
1706: '记录已存在',
|
||||
1707: '无权执行此操作',
|
||||
1708: '系统繁忙,请稍后重试',
|
||||
1709: '当前状态不支持此操作',
|
||||
1710: '操作已中止',
|
||||
1711: '输入内容超出允许范围',
|
||||
1712: '当前功能暂不支持',
|
||||
1713: '服务暂时不可用,请稍后重试',
|
||||
1714: '服务数据异常,请稍后重试',
|
||||
1715: '请先登录',
|
||||
};
|
||||
|
||||
final RegExp _chineseCharacterPattern = RegExp(r'[\u3400-\u9fff]');
|
||||
|
||||
/// 将服务端错误转换为中文用户提示。
|
||||
///
|
||||
/// [code] 是稳定错误码,[message] 是服务端原始消息;返回值不会透传未知英文技术信息。
|
||||
String localizeApiErrorMessage(int code, String? message) {
|
||||
final mappedMessage = _apiErrorMessages[code];
|
||||
if (mappedMessage != null) return mappedMessage;
|
||||
|
||||
final originalMessage = message?.trim() ?? '';
|
||||
if (_chineseCharacterPattern.hasMatch(originalMessage)) return originalMessage;
|
||||
return '操作失败,请稍后重试';
|
||||
}
|
||||
|
||||
/// 表示客户端可识别并可安全展示的接口异常。
|
||||
class ApiException implements Exception {
|
||||
const ApiException(this.code, this.message);
|
||||
|
||||
@@ -12,6 +83,7 @@ class ApiException implements Exception {
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
/// 负责用户端统一 HTTP 请求、鉴权头和响应解析。
|
||||
class ApiClient {
|
||||
ApiClient(
|
||||
this._tokenProvider, {
|
||||
@@ -59,18 +131,39 @@ class ApiClient {
|
||||
request.headers['content-type'] = 'application/json; charset=UTF-8';
|
||||
request.body = jsonEncode(body);
|
||||
}
|
||||
final streamed = await _client.send(request);
|
||||
final response = await http.Response.fromStream(streamed);
|
||||
final response = await _sendRequest(request);
|
||||
return _decode(response);
|
||||
}
|
||||
|
||||
/// 发送请求并统一处理网络连接异常。
|
||||
Future<http.Response> _sendRequest(http.BaseRequest request) async {
|
||||
try {
|
||||
return await http.Response.fromStream(await _client.send(request));
|
||||
} catch (_) {
|
||||
throw const ApiException(500, '网络连接失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析统一响应结构,并按错误码生成中文提示。
|
||||
Object? _decode(http.Response response) {
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throw ApiException(response.statusCode, '网络请求失败(${response.statusCode})');
|
||||
}
|
||||
final decoded = jsonDecode(response.body);
|
||||
Object? decoded;
|
||||
try {
|
||||
decoded = jsonDecode(response.body);
|
||||
} on FormatException {
|
||||
throw const ApiException(500, '服务端响应格式错误');
|
||||
}
|
||||
if (decoded is! Map<String, Object?>) {
|
||||
throw const ApiException(500, '服务端响应格式错误');
|
||||
}
|
||||
final code = (decoded['code'] as num?)?.toInt() ?? 500;
|
||||
if (code != 0) {
|
||||
throw ApiException(code, decoded['message'] as String? ?? '操作失败');
|
||||
throw ApiException(
|
||||
code,
|
||||
localizeApiErrorMessage(code, decoded['message'] as String?),
|
||||
);
|
||||
}
|
||||
return decoded['details'];
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
// 功能描述:提供用户端手机号密码登录界面及本地输入校验。
|
||||
// 版本:1.1.0
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../app/dependencies.dart';
|
||||
import '../../../data/services/api_client.dart';
|
||||
|
||||
/// 用户端手机号密码登录页面。
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({required this.session, super.key});
|
||||
|
||||
@@ -12,11 +16,15 @@ class LoginPage extends StatefulWidget {
|
||||
State<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
/// 管理登录输入、提交状态和错误提示。
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
static final RegExp _phonePattern = RegExp(r'^1[3-9]\d{9}$');
|
||||
|
||||
final _phone = TextEditingController();
|
||||
final _password = TextEditingController();
|
||||
bool _submitting = false;
|
||||
String? _error;
|
||||
String? _phoneError;
|
||||
bool _obscurePassword = true;
|
||||
|
||||
@override
|
||||
@@ -26,15 +34,30 @@ class _LoginPageState extends State<LoginPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 校验手机号并提交登录请求。
|
||||
Future<void> _login() async {
|
||||
final phone = _phone.text.trim();
|
||||
if (!_phonePattern.hasMatch(phone)) {
|
||||
setState(() {
|
||||
_phoneError = '请输入正确的11位手机号';
|
||||
_error = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_submitting = true;
|
||||
_phoneError = null;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
await widget.session.login(phone: _phone.text.trim(), password: _password.text);
|
||||
await widget.session.login(phone: phone, password: _password.text);
|
||||
} catch (error) {
|
||||
if (mounted) setState(() => _error = error.toString());
|
||||
// 登录接口使用统一密码错误码,页面不区分账号是否存在,避免泄露账号状态。
|
||||
final message = error is ApiException
|
||||
? (error.code == 1108 ? '手机号或密码错误' : error.toString())
|
||||
: '登录失败,请稍后重试';
|
||||
if (mounted) setState(() => _error = message);
|
||||
} finally {
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
}
|
||||
@@ -86,9 +109,13 @@ class _LoginPageState extends State<LoginPage> {
|
||||
controller: _phone,
|
||||
keyboardType: TextInputType.phone,
|
||||
autofillHints: const [AutofillHints.telephoneNumber],
|
||||
decoration: const InputDecoration(
|
||||
onChanged: (_) {
|
||||
if (_phoneError != null) setState(() => _phoneError = null);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: '手机号',
|
||||
prefixIcon: Icon(Icons.phone_outlined),
|
||||
prefixIcon: const Icon(Icons.phone_outlined),
|
||||
errorText: _phoneError,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// 功能描述:验证用户端登录页字段、手机号校验和接口错误中文化。
|
||||
// 版本:1.1.0
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:user_app/app/dependencies.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/data/services/secure_session_store.dart';
|
||||
import 'package:user_app/ui/features/auth/login_page.dart';
|
||||
|
||||
/// 验证用户端登录界面的关键交互。
|
||||
void main() {
|
||||
testWidgets('login page exposes phone and password fields', (tester) async {
|
||||
final session = UserSession(SecureSessionStore());
|
||||
@@ -13,4 +17,23 @@ void main() {
|
||||
expect(find.byType(TextField), findsNWidgets(2));
|
||||
expect(find.text('安全登录'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('手机号格式错误显示输入框中文提示', (tester) async {
|
||||
final session = UserSession(SecureSessionStore());
|
||||
await tester.pumpWidget(MaterialApp(home: LoginPage(session: session)));
|
||||
|
||||
await tester.enterText(find.byType(TextField).first, '+8613800138000');
|
||||
await tester.tap(find.text('安全登录'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('请输入正确的11位手机号'), findsOneWidget);
|
||||
expect(find.text('Invalid Argument'), findsNothing);
|
||||
});
|
||||
|
||||
test('接口错误优先按错误码中文化并屏蔽未知英文', () {
|
||||
expect(localizeApiErrorMessage(1704, 'Invalid Argument'), '请求参数错误');
|
||||
expect(localizeApiErrorMessage(1108, 'Password Incorrect'), '密码错误');
|
||||
expect(localizeApiErrorMessage(9999, '业务处理失败'), '业务处理失败');
|
||||
expect(localizeApiErrorMessage(9999, 'Unexpected Error'), '操作失败,请稍后重试');
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user