From b1e95b370c3a41af1a670766ac8ed1bc4dfdcf40 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Fri, 23 Oct 2026 13:05:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(app):=20=E6=8C=89=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E7=A0=81=E5=B1=95=E7=A4=BA=E4=B8=AD=E8=8B=B1=E5=8D=B0=E5=B0=BC?= =?UTF-8?q?=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/formatting/error_text.dart | 31 ++++++++-- lib/l10n/app_en.arb | 15 +++++ lib/l10n/app_id.arb | 15 +++++ lib/l10n/app_localizations.dart | 90 ++++++++++++++++++++++++++++ lib/l10n/app_localizations_en.dart | 46 ++++++++++++++ lib/l10n/app_localizations_id.dart | 46 ++++++++++++++ lib/l10n/app_localizations_zh.dart | 45 ++++++++++++++ lib/l10n/app_zh.arb | 15 +++++ lib/pages/auth/phone_auth_sheet.dart | 5 +- test/core/error_text_test.dart | 29 +++++++++ 10 files changed, 331 insertions(+), 6 deletions(-) create mode 100644 test/core/error_text_test.dart diff --git a/lib/core/formatting/error_text.dart b/lib/core/formatting/error_text.dart index 8b1876d..ad29a63 100644 --- a/lib/core/formatting/error_text.dart +++ b/lib/core/formatting/error_text.dart @@ -2,14 +2,37 @@ import '../../l10n/app_localizations.dart'; -const int _serverErrorCode = 500; - -/// 用户端不展示内部故障文案。500 与空提示改成可理解的说明,业务提示原样保留。 +/// 按后端 `ErrorCode` 取当前语言文案。未收录的业务码回退服务端 message。 String userFacingApiMessage(AppLocalizations l10n, int code, String message) { - if (code >= _serverErrorCode || message.isEmpty) return l10n.authServerError; + final localized = _apiMessage(l10n, code); + if (localized != null) return localized; + if (message.isEmpty) return l10n.authServerError; return message; } +/// 已定义错误码。500 用服务繁忙,不展示「系统异常」。 +String? _apiMessage(AppLocalizations l10n, int code) { + return switch (code) { + 400 => l10n.apiErrorBadRequest, + 401 => l10n.apiErrorUnauthorized, + 403 => l10n.apiErrorForbidden, + 404 => l10n.apiErrorNotFound, + 429 => l10n.apiErrorTooManyRequests, + 500 => l10n.authServerError, + 100000 => l10n.apiErrorLoginFailed, + 100001 => l10n.apiErrorAccountDisabled, + 100002 => l10n.apiErrorCoinNotEnough, + 100003 => l10n.apiErrorEpisodeUnavailable, + 100004 => l10n.apiErrorEpisodeUnlocked, + 100005 => l10n.apiErrorOrderInvalid, + 100006 => l10n.apiErrorVipExpired, + 200001 => l10n.apiErrorDataExists, + 200002 => l10n.apiErrorDataNotFound, + 200003 => l10n.apiErrorNotAllowed, + _ => null, + }; +} + /// logic 里的错误在两种情况下是"哨兵值":本地网络异常(读不到后端提示语)与 /// 我们自己包装的失败。这里统一翻译成当前语言的文案;服务端给的中文提示原样展示。 String errorText(AppLocalizations l10n, String? code, String fallback) { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1156300..2adaab3 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -438,6 +438,21 @@ "authPasswordWeak": "Password needs both letters and numbers", "authNetworkError": "Network error, please try again", "authServerError": "The service is busy. Please try again shortly.", + "apiErrorBadRequest": "Invalid request", + "apiErrorUnauthorized": "Sign in to continue", + "apiErrorForbidden": "You don't have permission", + "apiErrorNotFound": "Not found", + "apiErrorTooManyRequests": "Too many requests. Please try again later.", + "apiErrorLoginFailed": "Incorrect account or password", + "apiErrorAccountDisabled": "This account has been disabled", + "apiErrorCoinNotEnough": "Not enough coins", + "apiErrorEpisodeUnavailable": "This episode is unavailable", + "apiErrorEpisodeUnlocked": "This episode is already unlocked", + "apiErrorOrderInvalid": "This order is already paid or closed", + "apiErrorVipExpired": "Membership has expired", + "apiErrorDataExists": "This record already exists", + "apiErrorDataNotFound": "Not found", + "apiErrorNotAllowed": "This action isn't available right now", "authSwitchToSignUp": "New here? Create an account", "authSwitchToSignIn": "Already have an account? Sign in", "authGateTitle": "Sign in to continue", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 4e28505..359a0f4 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -412,6 +412,21 @@ "authPasswordWeak": "Kata sandi perlu huruf dan angka", "authNetworkError": "Jaringan bermasalah, coba lagi", "authServerError": "Layanan sedang sibuk. Coba lagi sebentar.", + "apiErrorBadRequest": "Permintaan tidak valid", + "apiErrorUnauthorized": "Masuk untuk melanjutkan", + "apiErrorForbidden": "Anda tidak memiliki izin", + "apiErrorNotFound": "Tidak ditemukan", + "apiErrorTooManyRequests": "Terlalu banyak permintaan. Coba lagi nanti.", + "apiErrorLoginFailed": "Akun atau kata sandi salah", + "apiErrorAccountDisabled": "Akun ini telah dinonaktifkan", + "apiErrorCoinNotEnough": "Koin tidak cukup", + "apiErrorEpisodeUnavailable": "Episode ini tidak tersedia", + "apiErrorEpisodeUnlocked": "Episode ini sudah terbuka", + "apiErrorOrderInvalid": "Pesanan ini sudah dibayar atau ditutup", + "apiErrorVipExpired": "Keanggotaan telah berakhir", + "apiErrorDataExists": "Data sudah ada", + "apiErrorDataNotFound": "Data tidak ditemukan", + "apiErrorNotAllowed": "Tindakan ini tidak tersedia saat ini", "authSwitchToSignUp": "Baru di sini? Buat akun", "authSwitchToSignIn": "Sudah punya akun? Masuk", "authGateTitle": "Masuk untuk melanjutkan", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index f364161..3d3e44a 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1906,6 +1906,96 @@ abstract class AppLocalizations { /// **'The service is busy. Please try again shortly.'** String get authServerError; + /// No description provided for @apiErrorBadRequest. + /// + /// In en, this message translates to: + /// **'Invalid request'** + String get apiErrorBadRequest; + + /// No description provided for @apiErrorUnauthorized. + /// + /// In en, this message translates to: + /// **'Sign in to continue'** + String get apiErrorUnauthorized; + + /// No description provided for @apiErrorForbidden. + /// + /// In en, this message translates to: + /// **'You don\'t have permission'** + String get apiErrorForbidden; + + /// No description provided for @apiErrorNotFound. + /// + /// In en, this message translates to: + /// **'Not found'** + String get apiErrorNotFound; + + /// No description provided for @apiErrorTooManyRequests. + /// + /// In en, this message translates to: + /// **'Too many requests. Please try again later.'** + String get apiErrorTooManyRequests; + + /// No description provided for @apiErrorLoginFailed. + /// + /// In en, this message translates to: + /// **'Incorrect account or password'** + String get apiErrorLoginFailed; + + /// No description provided for @apiErrorAccountDisabled. + /// + /// In en, this message translates to: + /// **'This account has been disabled'** + String get apiErrorAccountDisabled; + + /// No description provided for @apiErrorCoinNotEnough. + /// + /// In en, this message translates to: + /// **'Not enough coins'** + String get apiErrorCoinNotEnough; + + /// No description provided for @apiErrorEpisodeUnavailable. + /// + /// In en, this message translates to: + /// **'This episode is unavailable'** + String get apiErrorEpisodeUnavailable; + + /// No description provided for @apiErrorEpisodeUnlocked. + /// + /// In en, this message translates to: + /// **'This episode is already unlocked'** + String get apiErrorEpisodeUnlocked; + + /// No description provided for @apiErrorOrderInvalid. + /// + /// In en, this message translates to: + /// **'This order is already paid or closed'** + String get apiErrorOrderInvalid; + + /// No description provided for @apiErrorVipExpired. + /// + /// In en, this message translates to: + /// **'Membership has expired'** + String get apiErrorVipExpired; + + /// No description provided for @apiErrorDataExists. + /// + /// In en, this message translates to: + /// **'This record already exists'** + String get apiErrorDataExists; + + /// No description provided for @apiErrorDataNotFound. + /// + /// In en, this message translates to: + /// **'Not found'** + String get apiErrorDataNotFound; + + /// No description provided for @apiErrorNotAllowed. + /// + /// In en, this message translates to: + /// **'This action isn\'t available right now'** + String get apiErrorNotAllowed; + /// No description provided for @authSwitchToSignUp. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 8a1fe40..51e1345 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -968,6 +968,52 @@ class AppLocalizationsEn extends AppLocalizations { String get authServerError => 'The service is busy. Please try again shortly.'; + @override + String get apiErrorBadRequest => 'Invalid request'; + + @override + String get apiErrorUnauthorized => 'Sign in to continue'; + + @override + String get apiErrorForbidden => 'You don\'t have permission'; + + @override + String get apiErrorNotFound => 'Not found'; + + @override + String get apiErrorTooManyRequests => + 'Too many requests. Please try again later.'; + + @override + String get apiErrorLoginFailed => 'Incorrect account or password'; + + @override + String get apiErrorAccountDisabled => 'This account has been disabled'; + + @override + String get apiErrorCoinNotEnough => 'Not enough coins'; + + @override + String get apiErrorEpisodeUnavailable => 'This episode is unavailable'; + + @override + String get apiErrorEpisodeUnlocked => 'This episode is already unlocked'; + + @override + String get apiErrorOrderInvalid => 'This order is already paid or closed'; + + @override + String get apiErrorVipExpired => 'Membership has expired'; + + @override + String get apiErrorDataExists => 'This record already exists'; + + @override + String get apiErrorDataNotFound => 'Not found'; + + @override + String get apiErrorNotAllowed => 'This action isn\'t available right now'; + @override String get authSwitchToSignUp => 'New here? Create an account'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 088b6fb..25b53d9 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -971,6 +971,52 @@ class AppLocalizationsId extends AppLocalizations { @override String get authServerError => 'Layanan sedang sibuk. Coba lagi sebentar.'; + @override + String get apiErrorBadRequest => 'Permintaan tidak valid'; + + @override + String get apiErrorUnauthorized => 'Masuk untuk melanjutkan'; + + @override + String get apiErrorForbidden => 'Anda tidak memiliki izin'; + + @override + String get apiErrorNotFound => 'Tidak ditemukan'; + + @override + String get apiErrorTooManyRequests => + 'Terlalu banyak permintaan. Coba lagi nanti.'; + + @override + String get apiErrorLoginFailed => 'Akun atau kata sandi salah'; + + @override + String get apiErrorAccountDisabled => 'Akun ini telah dinonaktifkan'; + + @override + String get apiErrorCoinNotEnough => 'Koin tidak cukup'; + + @override + String get apiErrorEpisodeUnavailable => 'Episode ini tidak tersedia'; + + @override + String get apiErrorEpisodeUnlocked => 'Episode ini sudah terbuka'; + + @override + String get apiErrorOrderInvalid => 'Pesanan ini sudah dibayar atau ditutup'; + + @override + String get apiErrorVipExpired => 'Keanggotaan telah berakhir'; + + @override + String get apiErrorDataExists => 'Data sudah ada'; + + @override + String get apiErrorDataNotFound => 'Data tidak ditemukan'; + + @override + String get apiErrorNotAllowed => 'Tindakan ini tidak tersedia saat ini'; + @override String get authSwitchToSignUp => 'Baru di sini? Buat akun'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 08006ac..4e82207 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -955,6 +955,51 @@ class AppLocalizationsZh extends AppLocalizations { @override String get authServerError => '服务繁忙,请稍后再试'; + @override + String get apiErrorBadRequest => '参数校验失败'; + + @override + String get apiErrorUnauthorized => '未登录或登录已过期'; + + @override + String get apiErrorForbidden => '无权限'; + + @override + String get apiErrorNotFound => '资源不存在'; + + @override + String get apiErrorTooManyRequests => '请求过于频繁,请稍后再试'; + + @override + String get apiErrorLoginFailed => '账号或密码错误'; + + @override + String get apiErrorAccountDisabled => '账号已停用'; + + @override + String get apiErrorCoinNotEnough => '金币余额不足'; + + @override + String get apiErrorEpisodeUnavailable => '剧集未发布或不存在'; + + @override + String get apiErrorEpisodeUnlocked => '剧集已解锁'; + + @override + String get apiErrorOrderInvalid => '订单已支付或已关闭'; + + @override + String get apiErrorVipExpired => '会员已过期'; + + @override + String get apiErrorDataExists => '数据已存在'; + + @override + String get apiErrorDataNotFound => '数据不存在'; + + @override + String get apiErrorNotAllowed => '当前状态不允许该操作'; + @override String get authSwitchToSignUp => '还没有账号?去注册'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 1b02daa..ee4d692 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -374,6 +374,21 @@ "authPasswordWeak": "密码需同时包含字母和数字", "authNetworkError": "网络异常,请稍后重试", "authServerError": "服务繁忙,请稍后再试", + "apiErrorBadRequest": "参数校验失败", + "apiErrorUnauthorized": "未登录或登录已过期", + "apiErrorForbidden": "无权限", + "apiErrorNotFound": "资源不存在", + "apiErrorTooManyRequests": "请求过于频繁,请稍后再试", + "apiErrorLoginFailed": "账号或密码错误", + "apiErrorAccountDisabled": "账号已停用", + "apiErrorCoinNotEnough": "金币余额不足", + "apiErrorEpisodeUnavailable": "剧集未发布或不存在", + "apiErrorEpisodeUnlocked": "剧集已解锁", + "apiErrorOrderInvalid": "订单已支付或已关闭", + "apiErrorVipExpired": "会员已过期", + "apiErrorDataExists": "数据已存在", + "apiErrorDataNotFound": "数据不存在", + "apiErrorNotAllowed": "当前状态不允许该操作", "authSwitchToSignUp": "还没有账号?去注册", "authSwitchToSignIn": "已有账号?去登录", "authGateTitle": "登录后可继续", diff --git a/lib/pages/auth/phone_auth_sheet.dart b/lib/pages/auth/phone_auth_sheet.dart index 9fe651b..9b96588 100644 --- a/lib/pages/auth/phone_auth_sheet.dart +++ b/lib/pages/auth/phone_auth_sheet.dart @@ -8,6 +8,7 @@ import 'package:provider/provider.dart'; import '../../core/data/remote/api_client.dart'; import '../../core/data/services/auth_controller.dart'; +import '../../core/formatting/error_text.dart'; import '../../core/platform/haptics.dart'; import '../../core/platform/keyboard_inset.dart'; import '../../core/theme/app_colors.dart'; @@ -180,7 +181,7 @@ class _PhoneAuthSheetState extends State { ).showSnackBar(SnackBar(content: Text(l10n.authCodeSent))); _codeFocus.requestFocus(); } on ApiException catch (error) { - failureMessage = error.message; + failureMessage = userFacingApiMessage(l10n, error.code, error.message); } catch (_) { failureMessage = l10n.authNetworkError; } finally { @@ -228,7 +229,7 @@ class _PhoneAuthSheetState extends State { } if (mounted) Navigator.of(context).pop(true); } on ApiException catch (error) { - failureMessage = error.message; + failureMessage = userFacingApiMessage(l10n, error.code, error.message); } catch (_) { failureMessage = l10n.authNetworkError; } finally { diff --git a/test/core/error_text_test.dart b/test/core/error_text_test.dart new file mode 100644 index 0000000..52083a6 --- /dev/null +++ b/test/core/error_text_test.dart @@ -0,0 +1,29 @@ +// Author: fruit.com.cn + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fruitshorts/core/formatting/error_text.dart'; +import 'package:fruitshorts/l10n/app_localizations.dart'; + +void main() { + test('should_keep_login_failure_message_when_code_is_business', () async { + final l10n = await AppLocalizations.delegate.load(const Locale('zh')); + + expect( + userFacingApiMessage(l10n, 100000, '账号或密码错误'), + '账号或密码错误', + ); + expect(userFacingApiMessage(l10n, 500, '系统异常'), l10n.authServerError); + + final english = await AppLocalizations.delegate.load(const Locale('en')); + expect( + userFacingApiMessage(english, 100000, '账号或密码错误'), + 'Incorrect account or password', + ); + final indonesian = await AppLocalizations.delegate.load(const Locale('id')); + expect( + userFacingApiMessage(indonesian, 100000, '账号或密码错误'), + 'Akun atau kata sandi salah', + ); + }); +}