Files
customer-im-client-dev/apps/im_app/lib/data/remote/verify_otp_request.dart
pp-bot b8f1f82ee5
Some checks failed
CI / Lint (push) Has been cancelled
feat(login): 二级密码登录支持(STATUS_SECONDARY_PASSCODE_ERROR #1)
## 问题
旧版 Flutter 项目在 /vcode/check 返回 30164 时展示二级密码输入界面;
新版完全缺失此路径,有二级密码的账号无法登录。

## 改动

### networks_sdk
- `networks_sdk_method_channel_datasource.dart`:executeRequest 的
  generic catch 改为 rethrow,允许 decodeResponse override 抛出
  自定义业务异常(原为 ApiError.unknown 包裹导致数据丢失)

### 数据层
- `errors.dart`:新增 `secondaryPasscodeRequired = 30164`
- `exceptions.dart`(新增):`SecondaryPasscodeRequiredException`
  携带 vcodeToken / recoveryEmail / hint / resetStatus
- `verify_otp_request.dart`:override decodeResponse,拦截 30164,
  从响应 data 提取字段,throw SecondaryPasscodeRequiredException
- `login_request.dart`:新增可选 password 字段 + toJson override
  (条件序列化,null 时不带 password 字段)
- `auth_repository.dart`:新增 loginWithPasscode() 接口
- `auth_repository_impl.dart`:实现 loginWithPasscode()

### 业务层
- `login_usecase.dart`:新增 loginWithSecondaryPasscode()
  (MD5 哈希 passcode → 调 AuthRepository.loginWithPasscode)
- `pubspec.yaml`:新增 crypto: ^3.0.6(用于 MD5)

### UI 层
- `login_state.dart`:新增 LoginStep.secondaryPasscode
  + vcodeToken / passcodeHint / recoveryEmail 字段
- `login_view_model.dart`:verifyAndLogin 捕获 SecondaryPasscodeRequiredException
  跳转步骤 3;新增 verifyPasscode()
- `login_secondary_passcode_step.dart`(新增):密码输入 UI(hint 显示、
  obscured 输入框、错误提示、忘记密码占位)
- `login_page.dart`:switch 路由接入 LoginStep.secondaryPasscode
2026-03-31 15:36:54 +09:00

93 lines
3.1 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:dio/dio.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:networks_sdk/networks_sdk.dart';
import 'package:im_app/core/foundation/api_paths.dart';
import 'package:im_app/core/foundation/errors.dart';
import 'package:im_app/core/foundation/exceptions.dart';
part 'verify_otp_request.g.dart';
/// 校验验证码接口的响应(服务端 data 字段)。
///
/// `token` 是 vcode_token用于后续 login-user 请求换取 access_token。
/// 纯 Dart 类,无需任何注解。`_$VerifyOtpResponseFromJson` 由生成器自动推导生成。
class VerifyOtpResponse {
/// 验证令牌,传给登录接口换取 access_token
final String token;
const VerifyOtpResponse({required this.token});
factory VerifyOtpResponse.fromJson(Map<String, dynamic> json) =>
VerifyOtpResponse(token: json['token'] as String? ?? '');
}
/// # /app/api/auth/vcode/check — 校验手机验证码
///
/// 校验成功后返回 [VerifyOtpResponse.token](即 vcode_token
/// 用于 [LoginRequest] 的 `vcode_token` 字段。
///
/// ## 数据流位置
///
/// ```
/// AuthRepositoryImpl.verifyOtp(countryCode, contact, code)
/// → _client.executeRequest( ★ VerifyOtpRequest ★ ) ← 你在这里
/// → 服务端 POST /app/api/auth/vcode/check
/// → SDK 拆包 {code, message, data} envelope
/// ← ★ VerifyOtpResponse ★ — token 即 vcode_token
/// ```
@ApiRequest(
path: ApiPaths.authVerifyOtp,
method: HttpMethod.post,
responseType: VerifyOtpResponse,
requestType: ApiRequestType.login,
)
class VerifyOtpRequest extends ApiRequestable<VerifyOtpResponse>
with _$VerifyOtpRequestApi {
@JsonKey(name: 'country_code')
final String countryCode;
final String contact;
/// 邮箱(手机号登录传空字符串)
final String email;
/// 用户输入的验证码
final String code;
/// type=1 表示手机号验证
final int type;
VerifyOtpRequest({
required this.countryCode,
required this.contact,
required this.code,
this.email = '',
this.type = 1,
});
/// 拦截二级密码错误码 30164将服务端 data 中的 vcode_token 等字段
/// 包装为 [SecondaryPasscodeRequiredException] 抛出,供上层导航至
/// 二级密码输入界面。其余情况委托给基类处理。
@override
VerifyOtpResponse? decodeResponse(Response response) {
if (response.data is Map<String, dynamic>) {
final json = response.data as Map<String, dynamic>;
final rawCode = json['code'];
final code = rawCode is int
? rawCode
: int.tryParse(rawCode?.toString() ?? '') ?? 0;
if (code == ApiErrorCodes.secondaryPasscodeRequired) {
final data = json['data'] as Map<String, dynamic>?;
throw SecondaryPasscodeRequiredException(
vcodeToken: data?['vcode_token'] as String? ?? '',
recoveryEmail: data?['recovery_email'] as String? ?? '',
hint: data?['hint'] as String? ?? '',
resetStatus: data?['reset_status'] as bool? ?? false,
);
}
}
return super.decodeResponse(response);
}
}