feat: 添加 register-by-phone API 实现手机号一步注册
- 后端: 添加 POST /user/register-by-phone 接口 - 验证短信验证码、创建账户、绑定手机号、设置密码、触发钱包生成 - 添加 RegisterByPhoneCommand 和 RegisterByPhoneDto - 前端: 修改注册流程使用新 API - SmsVerifyPage 直接跳转到密码页面传递验证码 - SetPasswordPage 调用 registerByPhoneWithPassword 一步完成 - AccountService 添加 registerByPhoneWithPassword 方法 修复手机号注册流程中手机号和密码未正确保存的问题 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
86d3a3f6a2
commit
2897a0c74c
|
|
@ -30,6 +30,7 @@ import {
|
|||
} from '@/shared/guards/jwt-auth.guard';
|
||||
import {
|
||||
AutoCreateAccountCommand,
|
||||
RegisterByPhoneCommand,
|
||||
RecoverByMnemonicCommand,
|
||||
RecoverByPhoneCommand,
|
||||
AutoLoginCommand,
|
||||
|
|
@ -50,6 +51,7 @@ import {
|
|||
} from '@/application/commands';
|
||||
import {
|
||||
AutoCreateAccountDto,
|
||||
RegisterByPhoneDto,
|
||||
RecoverByMnemonicDto,
|
||||
RecoverByPhoneDto,
|
||||
AutoLoginDto,
|
||||
|
|
@ -102,6 +104,26 @@ export class UserAccountController {
|
|||
);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('register-by-phone')
|
||||
@ApiOperation({
|
||||
summary: '手机号注册(验证码+密码一步完成)',
|
||||
description: '验证短信验证码,创建账户,绑定手机号,设置密码,触发钱包生成',
|
||||
})
|
||||
@ApiResponse({ status: 200, type: AutoCreateAccountResponseDto })
|
||||
async registerByPhone(@Body() dto: RegisterByPhoneDto) {
|
||||
return this.userService.registerByPhone(
|
||||
new RegisterByPhoneCommand(
|
||||
dto.phoneNumber,
|
||||
dto.smsCode,
|
||||
dto.password,
|
||||
dto.deviceId,
|
||||
dto.deviceName,
|
||||
dto.inviterReferralCode,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('recover-by-mnemonic')
|
||||
@ApiOperation({ summary: '用序列号+助记词恢复账户' })
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export * from './auto-create-account.dto';
|
||||
export * from './register-by-phone.dto';
|
||||
export * from './recover-by-mnemonic.dto';
|
||||
export * from './recover-by-phone.dto';
|
||||
export * from './bind-phone.dto';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsNotEmpty,
|
||||
Matches,
|
||||
IsObject,
|
||||
Length,
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { DeviceNameDto } from './auto-create-account.dto';
|
||||
|
||||
export class RegisterByPhoneDto {
|
||||
@ApiProperty({
|
||||
example: '13800138000',
|
||||
description: '手机号',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Matches(/^1[3-9]\d{9}$/, { message: '手机号格式错误' })
|
||||
phoneNumber: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '123456',
|
||||
description: '短信验证码 (6位数字)',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(6, 6, { message: '验证码必须是6位' })
|
||||
smsCode: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Password123',
|
||||
description: '登录密码 (6-20位)',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MinLength(6, { message: '密码至少6位' })
|
||||
password: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '550e8400-e29b-41d4-a716-446655440000',
|
||||
description: '设备唯一标识',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
deviceId: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: '设备信息 (JSON 对象)',
|
||||
example: { model: 'iPhone 15 Pro', platform: 'ios', osVersion: '17.2' },
|
||||
})
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
deviceName?: DeviceNameDto;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: 'RWAABC1234',
|
||||
description: '邀请人推荐码 (6-20位大写字母和数字)',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Matches(/^[A-Z0-9]{6,20}$/, { message: '推荐码格式错误' })
|
||||
inviterReferralCode?: string;
|
||||
}
|
||||
|
|
@ -16,6 +16,17 @@ export class AutoCreateAccountCommand {
|
|||
) {}
|
||||
}
|
||||
|
||||
export class RegisterByPhoneCommand {
|
||||
constructor(
|
||||
public readonly phoneNumber: string,
|
||||
public readonly smsCode: string,
|
||||
public readonly password: string,
|
||||
public readonly deviceId: string,
|
||||
public readonly deviceName?: DeviceNameInput,
|
||||
public readonly inviterReferralCode?: string,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class RecoverByMnemonicCommand {
|
||||
constructor(
|
||||
public readonly accountSequence: string, // 格式: D + YYMMDD + 5位序号
|
||||
|
|
|
|||
|
|
@ -1477,11 +1477,12 @@ class AccountService {
|
|||
}
|
||||
}
|
||||
|
||||
/// 手机号注册
|
||||
/// 手机号注册 (旧接口,不推荐使用)
|
||||
///
|
||||
/// [phoneNumber] - 手机号
|
||||
/// [smsCode] - 6位短信验证码
|
||||
/// [inviterReferralCode] - 邀请人推荐码(可选)
|
||||
@Deprecated('使用 registerByPhoneWithPassword 替代')
|
||||
Future<PhoneAuthResponse> registerByPhone({
|
||||
required String phoneNumber,
|
||||
required String smsCode,
|
||||
|
|
@ -1543,6 +1544,79 @@ class AccountService {
|
|||
}
|
||||
}
|
||||
|
||||
/// 手机号注册 (新接口:验证码+密码一步完成)
|
||||
///
|
||||
/// [phoneNumber] - 手机号
|
||||
/// [smsCode] - 6位短信验证码
|
||||
/// [password] - 登录密码
|
||||
/// [inviterReferralCode] - 邀请人推荐码(可选)
|
||||
Future<CreateAccountResponse> registerByPhoneWithPassword({
|
||||
required String phoneNumber,
|
||||
required String smsCode,
|
||||
required String password,
|
||||
String? inviterReferralCode,
|
||||
}) async {
|
||||
debugPrint('$_tag registerByPhoneWithPassword() - 开始手机号注册');
|
||||
debugPrint('$_tag registerByPhoneWithPassword() - 手机号: ${_maskPhoneNumber(phoneNumber)}');
|
||||
debugPrint('$_tag registerByPhoneWithPassword() - 邀请码: ${inviterReferralCode ?? "无"}');
|
||||
|
||||
try {
|
||||
// 获取设备ID
|
||||
final deviceId = await getDeviceId();
|
||||
debugPrint('$_tag registerByPhoneWithPassword() - 获取设备ID成功');
|
||||
|
||||
// 获取设备硬件信息
|
||||
final deviceInfo = await getDeviceHardwareInfo();
|
||||
debugPrint('$_tag registerByPhoneWithPassword() - 获取设备硬件信息成功');
|
||||
|
||||
// 调用 API
|
||||
debugPrint('$_tag registerByPhoneWithPassword() - 调用 POST /user/register-by-phone');
|
||||
final response = await _apiClient.post(
|
||||
'/user/register-by-phone',
|
||||
data: {
|
||||
'phoneNumber': phoneNumber,
|
||||
'smsCode': smsCode,
|
||||
'password': password,
|
||||
'deviceId': deviceId,
|
||||
'deviceName': deviceInfo.toJson(),
|
||||
if (inviterReferralCode != null) 'inviterReferralCode': inviterReferralCode,
|
||||
},
|
||||
);
|
||||
debugPrint('$_tag registerByPhoneWithPassword() - API 响应状态码: ${response.statusCode}');
|
||||
|
||||
if (response.data == null) {
|
||||
debugPrint('$_tag registerByPhoneWithPassword() - 错误: API 返回空响应');
|
||||
throw const ApiException('注册失败: 空响应');
|
||||
}
|
||||
|
||||
debugPrint('$_tag registerByPhoneWithPassword() - 解析响应数据');
|
||||
final responseData = response.data as Map<String, dynamic>;
|
||||
final data = responseData['data'] as Map<String, dynamic>;
|
||||
final result = CreateAccountResponse.fromJson(data);
|
||||
debugPrint('$_tag registerByPhoneWithPassword() - 解析成功: $result');
|
||||
|
||||
// 保存账号数据
|
||||
debugPrint('$_tag registerByPhoneWithPassword() - 保存账号数据');
|
||||
await _saveAccountData(result, deviceId);
|
||||
|
||||
// 保存手机号
|
||||
await _secureStorage.write(key: StorageKeys.phoneNumber, value: phoneNumber);
|
||||
|
||||
// 标记密码已设置
|
||||
await _secureStorage.write(key: StorageKeys.isPasswordSet, value: 'true');
|
||||
|
||||
debugPrint('$_tag registerByPhoneWithPassword() - 手机号注册完成');
|
||||
return result;
|
||||
} on ApiException catch (e) {
|
||||
debugPrint('$_tag registerByPhoneWithPassword() - API 异常: $e');
|
||||
rethrow;
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('$_tag registerByPhoneWithPassword() - 未知异常: $e');
|
||||
debugPrint('$_tag registerByPhoneWithPassword() - 堆栈: $stackTrace');
|
||||
throw ApiException('注册失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 手机号登录
|
||||
///
|
||||
/// [phoneNumber] - 手机号
|
||||
|
|
|
|||
|
|
@ -3,29 +3,38 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
import '../../../../core/services/multi_account_service.dart';
|
||||
import '../../../../routes/route_paths.dart';
|
||||
|
||||
/// 设置登录密码页面参数
|
||||
class SetPasswordParams {
|
||||
final String userSerialNum;
|
||||
final String? userSerialNum; // 如果有则表示已创建账号(旧流程),无则表示需要创建账号(新流程)
|
||||
final String? inviterReferralCode;
|
||||
final String? phoneNumber; // 新流程需要
|
||||
final String? smsCode; // 新流程需要
|
||||
|
||||
SetPasswordParams({
|
||||
required this.userSerialNum,
|
||||
this.userSerialNum,
|
||||
this.inviterReferralCode,
|
||||
this.phoneNumber,
|
||||
this.smsCode,
|
||||
});
|
||||
}
|
||||
|
||||
/// 设置登录密码页面
|
||||
/// 用户完成短信验证后,设置登录密码
|
||||
class SetPasswordPage extends ConsumerStatefulWidget {
|
||||
final String userSerialNum;
|
||||
final String? userSerialNum; // 旧流程:已创建账号
|
||||
final String? inviterReferralCode;
|
||||
final String? phoneNumber; // 新流程:手机号
|
||||
final String? smsCode; // 新流程:验证码
|
||||
|
||||
const SetPasswordPage({
|
||||
super.key,
|
||||
required this.userSerialNum,
|
||||
this.userSerialNum,
|
||||
this.inviterReferralCode,
|
||||
this.phoneNumber,
|
||||
this.smsCode,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -45,7 +54,7 @@ class _SetPasswordPageState extends ConsumerState<SetPasswordPage> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
debugPrint('[SetPasswordPage] initState - userSerialNum: ${widget.userSerialNum}');
|
||||
debugPrint('[SetPasswordPage] initState - userSerialNum: ${widget.userSerialNum}, phoneNumber: ${widget.phoneNumber != null ? "***" : "null"}');
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -99,12 +108,40 @@ class _SetPasswordPageState extends ConsumerState<SetPasswordPage> {
|
|||
final accountService = ref.read(accountServiceProvider);
|
||||
final password = _passwordController.text;
|
||||
|
||||
debugPrint('[SetPasswordPage] 开始设置密码...');
|
||||
// 判断是新流程还是旧流程
|
||||
if (widget.phoneNumber != null && widget.smsCode != null) {
|
||||
// 新流程:使用 register-by-phone API 一步完成注册
|
||||
debugPrint('[SetPasswordPage] 使用新流程: register-by-phone');
|
||||
|
||||
// 调用设置密码 API
|
||||
await accountService.setLoginPassword(password);
|
||||
final response = await accountService.registerByPhoneWithPassword(
|
||||
phoneNumber: widget.phoneNumber!,
|
||||
smsCode: widget.smsCode!,
|
||||
password: password,
|
||||
inviterReferralCode: widget.inviterReferralCode,
|
||||
);
|
||||
|
||||
debugPrint('[SetPasswordPage] 密码设置成功');
|
||||
debugPrint('[SetPasswordPage] 注册成功: ${response.userSerialNum}');
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// 将账号添加到多账号列表
|
||||
final multiAccountService = ref.read(multiAccountServiceProvider);
|
||||
await multiAccountService.addAccount(
|
||||
AccountSummary(
|
||||
userSerialNum: response.userSerialNum,
|
||||
username: response.username,
|
||||
avatarSvg: response.avatarSvg,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
await multiAccountService.setCurrentAccountId(response.userSerialNum);
|
||||
debugPrint('[SetPasswordPage] 已添加到多账号列表');
|
||||
} else {
|
||||
// 旧流程:单独设置密码
|
||||
debugPrint('[SetPasswordPage] 使用旧流程: set-password');
|
||||
await accountService.setLoginPassword(password);
|
||||
debugPrint('[SetPasswordPage] 密码设置成功');
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|||
import 'package:go_router/go_router.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
import '../../../../core/services/account_service.dart';
|
||||
import '../../../../core/services/multi_account_service.dart';
|
||||
import '../../../../routes/route_paths.dart';
|
||||
import 'set_password_page.dart';
|
||||
|
||||
|
|
@ -153,49 +152,21 @@ class _SmsVerifyPageState extends ConsumerState<SmsVerifyPage> {
|
|||
final accountService = ref.read(accountServiceProvider);
|
||||
|
||||
if (widget.type == SmsCodeType.register) {
|
||||
// 注册流程:先验证短信验证码,然后调用 auto-create 创建账号
|
||||
debugPrint('[SmsVerifyPage] 开始验证短信验证码...');
|
||||
|
||||
// 验证短信验证码
|
||||
await accountService.verifySmsCode(
|
||||
phoneNumber: widget.phoneNumber,
|
||||
smsCode: code,
|
||||
type: SmsCodeType.register,
|
||||
);
|
||||
debugPrint('[SmsVerifyPage] 短信验证码验证成功');
|
||||
|
||||
// 调用 auto-create 创建账号
|
||||
debugPrint('[SmsVerifyPage] 开始调用 auto-create 创建账号...');
|
||||
final response = await accountService.createAccount(
|
||||
inviterReferralCode: widget.inviterReferralCode,
|
||||
);
|
||||
debugPrint('[SmsVerifyPage] 账号创建成功: ${response.userSerialNum}');
|
||||
// 注册流程:直接跳转到设置密码页面,由密码页面一起调用 register-by-phone API
|
||||
debugPrint('[SmsVerifyPage] 验证码输入完成,跳转到设置密码页面');
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// 将账号添加到多账号列表
|
||||
final multiAccountService = ref.read(multiAccountServiceProvider);
|
||||
await multiAccountService.addAccount(
|
||||
AccountSummary(
|
||||
userSerialNum: response.userSerialNum,
|
||||
username: response.username,
|
||||
avatarSvg: response.avatarSvg,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
await multiAccountService.setCurrentAccountId(response.userSerialNum);
|
||||
debugPrint('[SmsVerifyPage] 已添加到多账号列表');
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// 跳转到设置密码页面
|
||||
// 跳转到设置密码页面,传递手机号和验证码
|
||||
context.go(
|
||||
RoutePaths.setPassword,
|
||||
extra: SetPasswordParams(
|
||||
userSerialNum: response.userSerialNum,
|
||||
phoneNumber: widget.phoneNumber,
|
||||
smsCode: code,
|
||||
inviterReferralCode: widget.inviterReferralCode,
|
||||
),
|
||||
);
|
||||
return; // 提前返回,不需要等待
|
||||
} else if (widget.type == SmsCodeType.login) {
|
||||
// 登录
|
||||
debugPrint('[SmsVerifyPage] 开始登录...');
|
||||
|
|
|
|||
Loading…
Reference in New Issue