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';
|
} from '@/shared/guards/jwt-auth.guard';
|
||||||
import {
|
import {
|
||||||
AutoCreateAccountCommand,
|
AutoCreateAccountCommand,
|
||||||
|
RegisterByPhoneCommand,
|
||||||
RecoverByMnemonicCommand,
|
RecoverByMnemonicCommand,
|
||||||
RecoverByPhoneCommand,
|
RecoverByPhoneCommand,
|
||||||
AutoLoginCommand,
|
AutoLoginCommand,
|
||||||
|
|
@ -50,6 +51,7 @@ import {
|
||||||
} from '@/application/commands';
|
} from '@/application/commands';
|
||||||
import {
|
import {
|
||||||
AutoCreateAccountDto,
|
AutoCreateAccountDto,
|
||||||
|
RegisterByPhoneDto,
|
||||||
RecoverByMnemonicDto,
|
RecoverByMnemonicDto,
|
||||||
RecoverByPhoneDto,
|
RecoverByPhoneDto,
|
||||||
AutoLoginDto,
|
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()
|
@Public()
|
||||||
@Post('recover-by-mnemonic')
|
@Post('recover-by-mnemonic')
|
||||||
@ApiOperation({ summary: '用序列号+助记词恢复账户' })
|
@ApiOperation({ summary: '用序列号+助记词恢复账户' })
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './auto-create-account.dto';
|
export * from './auto-create-account.dto';
|
||||||
|
export * from './register-by-phone.dto';
|
||||||
export * from './recover-by-mnemonic.dto';
|
export * from './recover-by-mnemonic.dto';
|
||||||
export * from './recover-by-phone.dto';
|
export * from './recover-by-phone.dto';
|
||||||
export * from './bind-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 {
|
export class RecoverByMnemonicCommand {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly accountSequence: string, // 格式: D + YYMMDD + 5位序号
|
public readonly accountSequence: string, // 格式: D + YYMMDD + 5位序号
|
||||||
|
|
|
||||||
|
|
@ -1477,11 +1477,12 @@ class AccountService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 手机号注册
|
/// 手机号注册 (旧接口,不推荐使用)
|
||||||
///
|
///
|
||||||
/// [phoneNumber] - 手机号
|
/// [phoneNumber] - 手机号
|
||||||
/// [smsCode] - 6位短信验证码
|
/// [smsCode] - 6位短信验证码
|
||||||
/// [inviterReferralCode] - 邀请人推荐码(可选)
|
/// [inviterReferralCode] - 邀请人推荐码(可选)
|
||||||
|
@Deprecated('使用 registerByPhoneWithPassword 替代')
|
||||||
Future<PhoneAuthResponse> registerByPhone({
|
Future<PhoneAuthResponse> registerByPhone({
|
||||||
required String phoneNumber,
|
required String phoneNumber,
|
||||||
required String smsCode,
|
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] - 手机号
|
/// [phoneNumber] - 手机号
|
||||||
|
|
|
||||||
|
|
@ -3,29 +3,38 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../../../core/di/injection_container.dart';
|
import '../../../../core/di/injection_container.dart';
|
||||||
|
import '../../../../core/services/multi_account_service.dart';
|
||||||
import '../../../../routes/route_paths.dart';
|
import '../../../../routes/route_paths.dart';
|
||||||
|
|
||||||
/// 设置登录密码页面参数
|
/// 设置登录密码页面参数
|
||||||
class SetPasswordParams {
|
class SetPasswordParams {
|
||||||
final String userSerialNum;
|
final String? userSerialNum; // 如果有则表示已创建账号(旧流程),无则表示需要创建账号(新流程)
|
||||||
final String? inviterReferralCode;
|
final String? inviterReferralCode;
|
||||||
|
final String? phoneNumber; // 新流程需要
|
||||||
|
final String? smsCode; // 新流程需要
|
||||||
|
|
||||||
SetPasswordParams({
|
SetPasswordParams({
|
||||||
required this.userSerialNum,
|
this.userSerialNum,
|
||||||
this.inviterReferralCode,
|
this.inviterReferralCode,
|
||||||
|
this.phoneNumber,
|
||||||
|
this.smsCode,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 设置登录密码页面
|
/// 设置登录密码页面
|
||||||
/// 用户完成短信验证后,设置登录密码
|
/// 用户完成短信验证后,设置登录密码
|
||||||
class SetPasswordPage extends ConsumerStatefulWidget {
|
class SetPasswordPage extends ConsumerStatefulWidget {
|
||||||
final String userSerialNum;
|
final String? userSerialNum; // 旧流程:已创建账号
|
||||||
final String? inviterReferralCode;
|
final String? inviterReferralCode;
|
||||||
|
final String? phoneNumber; // 新流程:手机号
|
||||||
|
final String? smsCode; // 新流程:验证码
|
||||||
|
|
||||||
const SetPasswordPage({
|
const SetPasswordPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.userSerialNum,
|
this.userSerialNum,
|
||||||
this.inviterReferralCode,
|
this.inviterReferralCode,
|
||||||
|
this.phoneNumber,
|
||||||
|
this.smsCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -45,7 +54,7 @@ class _SetPasswordPageState extends ConsumerState<SetPasswordPage> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
debugPrint('[SetPasswordPage] initState - userSerialNum: ${widget.userSerialNum}');
|
debugPrint('[SetPasswordPage] initState - userSerialNum: ${widget.userSerialNum}, phoneNumber: ${widget.phoneNumber != null ? "***" : "null"}');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -99,12 +108,40 @@ class _SetPasswordPageState extends ConsumerState<SetPasswordPage> {
|
||||||
final accountService = ref.read(accountServiceProvider);
|
final accountService = ref.read(accountServiceProvider);
|
||||||
final password = _passwordController.text;
|
final password = _passwordController.text;
|
||||||
|
|
||||||
debugPrint('[SetPasswordPage] 开始设置密码...');
|
// 判断是新流程还是旧流程
|
||||||
|
if (widget.phoneNumber != null && widget.smsCode != null) {
|
||||||
|
// 新流程:使用 register-by-phone API 一步完成注册
|
||||||
|
debugPrint('[SetPasswordPage] 使用新流程: register-by-phone');
|
||||||
|
|
||||||
// 调用设置密码 API
|
final response = await accountService.registerByPhoneWithPassword(
|
||||||
await accountService.setLoginPassword(password);
|
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;
|
if (!mounted) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../../../core/di/injection_container.dart';
|
import '../../../../core/di/injection_container.dart';
|
||||||
import '../../../../core/services/account_service.dart';
|
import '../../../../core/services/account_service.dart';
|
||||||
import '../../../../core/services/multi_account_service.dart';
|
|
||||||
import '../../../../routes/route_paths.dart';
|
import '../../../../routes/route_paths.dart';
|
||||||
import 'set_password_page.dart';
|
import 'set_password_page.dart';
|
||||||
|
|
||||||
|
|
@ -153,49 +152,21 @@ class _SmsVerifyPageState extends ConsumerState<SmsVerifyPage> {
|
||||||
final accountService = ref.read(accountServiceProvider);
|
final accountService = ref.read(accountServiceProvider);
|
||||||
|
|
||||||
if (widget.type == SmsCodeType.register) {
|
if (widget.type == SmsCodeType.register) {
|
||||||
// 注册流程:先验证短信验证码,然后调用 auto-create 创建账号
|
// 注册流程:直接跳转到设置密码页面,由密码页面一起调用 register-by-phone API
|
||||||
debugPrint('[SmsVerifyPage] 开始验证短信验证码...');
|
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}');
|
|
||||||
|
|
||||||
if (!mounted) return;
|
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(
|
context.go(
|
||||||
RoutePaths.setPassword,
|
RoutePaths.setPassword,
|
||||||
extra: SetPasswordParams(
|
extra: SetPasswordParams(
|
||||||
userSerialNum: response.userSerialNum,
|
phoneNumber: widget.phoneNumber,
|
||||||
|
smsCode: code,
|
||||||
inviterReferralCode: widget.inviterReferralCode,
|
inviterReferralCode: widget.inviterReferralCode,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
return; // 提前返回,不需要等待
|
||||||
} else if (widget.type == SmsCodeType.login) {
|
} else if (widget.type == SmsCodeType.login) {
|
||||||
// 登录
|
// 登录
|
||||||
debugPrint('[SmsVerifyPage] 开始登录...');
|
debugPrint('[SmsVerifyPage] 开始登录...');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue