feat(payment-password): 添加支付密码功能(全栈)

后端(identity-service):
- Prisma schema 新增 paymentPasswordHash 字段及手动迁移脚本
- user-application.service 添加 4 个方法:isPaymentPasswordSet / setPaymentPassword /
  changePaymentPassword / verifyPaymentPassword(纯新增,不修改已有逻辑)
- user-account.controller 新增 4 个端点:
  GET /user/payment-password-status
  POST /user/set-payment-password
  POST /user/change-payment-password
  POST /user/verify-payment-password → { valid: bool }

前端(mobile-app):
- account_service 新增 4 个方法对应后端 4 个接口
- 新建 change_payment_password_page.dart:6 位数字支付密码设置/修改页面
- 路由:RoutePaths / RouteNames / AppRouter 注册 /security/payment-password
- profile_page:'修改登录密码' 改为 '修改密码',下方新增 '支付密码' 入口
- PasswordVerifyDialog:新增 title / subtitle / hint 可选参数,支持登录/支付双模式
- planting_location_page:认种确认改为验证支付密码(verifyPaymentPassword)
- pre_planting_purchase_page:预种确认后追加支付密码验证步骤

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-05 06:18:40 -08:00
parent d91ff7b83a
commit cad7ebe832
13 changed files with 831 additions and 21 deletions

View File

@ -0,0 +1,2 @@
-- AlterTable: 新增支付密码哈希字段
ALTER TABLE "user_accounts" ADD COLUMN "payment_password_hash" VARCHAR(100);

View File

@ -17,7 +17,8 @@ model UserAccount {
email String? @unique @db.VarChar(100) // 绑定的邮箱地址
emailVerified Boolean @default(false) @map("email_verified") // 邮箱是否验证
emailVerifiedAt DateTime? @map("email_verified_at") // 邮箱验证时间
passwordHash String? @map("password_hash") @db.VarChar(100) // bcrypt 哈希密码
passwordHash String? @map("password_hash") @db.VarChar(100) // bcrypt 哈希登录密码
paymentPasswordHash String? @map("payment_password_hash") @db.VarChar(100) // bcrypt 哈希支付密码
nickname String @db.VarChar(100)
avatarUrl String? @map("avatar_url") @db.Text

View File

@ -756,6 +756,77 @@ export class UserAccountController {
return { valid };
}
// ============ 支付密码相关 ============
@Get('payment-password-status')
@ApiBearerAuth()
@ApiOperation({
summary: '查询支付密码状态',
description: '查询当前用户是否已设置支付密码',
})
@ApiResponse({ status: 200, description: '{ isSet: boolean }' })
async getPaymentPasswordStatus(@CurrentUser() user: CurrentUserData) {
const isSet = await this.userService.isPaymentPasswordSet(user.userId);
return { isSet };
}
@Post('set-payment-password')
@ApiBearerAuth()
@ApiOperation({
summary: '设置支付密码',
description: '首次设置支付密码(无需旧密码)',
})
@ApiResponse({ status: 200, description: '设置成功' })
@ApiResponse({ status: 400, description: '支付密码已设置' })
async setPaymentPassword(
@CurrentUser() user: CurrentUserData,
@Body() body: { password: string },
) {
await this.userService.setPaymentPassword({
userId: user.userId,
password: body.password,
});
return { message: '支付密码设置成功' };
}
@Post('change-payment-password')
@ApiBearerAuth()
@ApiOperation({
summary: '修改支付密码',
description: '验证旧支付密码后修改为新支付密码',
})
@ApiResponse({ status: 200, description: '修改成功' })
@ApiResponse({ status: 400, description: '旧支付密码错误' })
async changePaymentPassword(
@CurrentUser() user: CurrentUserData,
@Body() body: { oldPassword: string; newPassword: string },
) {
await this.userService.changePaymentPassword({
userId: user.userId,
oldPassword: body.oldPassword,
newPassword: body.newPassword,
});
return { message: '支付密码修改成功' };
}
@Post('verify-payment-password')
@ApiBearerAuth()
@ApiOperation({
summary: '验证支付密码',
description: '验证用户的支付密码,用于认种/预种等支付操作二次验证',
})
@ApiResponse({ status: 200, description: '{ valid: boolean }' })
async verifyPaymentPassword(
@CurrentUser() user: CurrentUserData,
@Body() body: { password: string },
) {
const valid = await this.userService.verifyPaymentPassword(
user.userId,
body.password,
);
return { valid };
}
@Get('users/resolve-address/:accountSequence')
@ApiBearerAuth()
@ApiOperation({

View File

@ -2856,6 +2856,106 @@ export class UserApplicationService {
return isValid;
}
// ============ 支付密码相关 ============
/**
*
*/
async isPaymentPasswordSet(userId: string): Promise<boolean> {
const user = await this.prisma.userAccount.findUnique({
where: { userId: BigInt(userId) },
select: { paymentPasswordHash: true },
});
return !!user?.paymentPasswordHash;
}
/**
*
*/
async setPaymentPassword(command: {
userId: string;
password: string;
}): Promise<void> {
this.logger.log(`Setting payment password for user: ${command.userId}`);
const user = await this.prisma.userAccount.findUnique({
where: { userId: BigInt(command.userId) },
select: { paymentPasswordHash: true },
});
if (user?.paymentPasswordHash) {
throw new ApplicationError('支付密码已设置,请使用修改支付密码功能');
}
const bcrypt = await import('bcrypt');
const paymentPasswordHash = await bcrypt.hash(command.password, 10);
await this.prisma.userAccount.update({
where: { userId: BigInt(command.userId) },
data: { paymentPasswordHash },
});
this.logger.log(`Payment password set for user: ${command.userId}`);
}
/**
*
*/
async changePaymentPassword(command: {
userId: string;
oldPassword: string;
newPassword: string;
}): Promise<void> {
this.logger.log(`Changing payment password for user: ${command.userId}`);
const user = await this.prisma.userAccount.findUnique({
where: { userId: BigInt(command.userId) },
select: { paymentPasswordHash: true },
});
if (!user?.paymentPasswordHash) {
throw new ApplicationError('尚未设置支付密码,请先设置');
}
const bcrypt = await import('bcrypt');
const isOldValid = await bcrypt.compare(command.oldPassword, user.paymentPasswordHash);
if (!isOldValid) {
throw new ApplicationError('旧支付密码错误');
}
const paymentPasswordHash = await bcrypt.hash(command.newPassword, 10);
await this.prisma.userAccount.update({
where: { userId: BigInt(command.userId) },
data: { paymentPasswordHash },
});
this.logger.log(`Payment password changed for user: ${command.userId}`);
}
/**
*
*
* @returns
*/
async verifyPaymentPassword(userId: string, password: string): Promise<boolean> {
this.logger.log(`Verifying payment password for user: ${userId}`);
const user = await this.prisma.userAccount.findUnique({
where: { userId: BigInt(userId) },
select: { paymentPasswordHash: true },
});
if (!user?.paymentPasswordHash) {
throw new ApplicationError('请先设置支付密码');
}
const bcrypt = await import('bcrypt');
const isValid = await bcrypt.compare(password, user.paymentPasswordHash);
this.logger.log(`Payment password verification result for user ${userId}: ${isValid}`);
return isValid;
}
/**
*
*/

View File

@ -2124,6 +2124,93 @@ class AccountService {
}
}
// ============ ============
/// (GET /user/payment-password-status)
///
/// true false
Future<bool> isPaymentPasswordSet() async {
debugPrint('$_tag isPaymentPasswordSet() - 查询支付密码状态');
try {
final response = await _apiClient.get('/user/payment-password-status');
final isSet = response.data['isSet'] == true;
debugPrint('$_tag isPaymentPasswordSet() - 结果: $isSet');
return isSet;
} catch (e) {
debugPrint('$_tag isPaymentPasswordSet() - 异常: $e');
rethrow;
}
}
/// (POST /user/set-payment-password)
///
/// [password] 6
Future<void> setPaymentPassword(String password) async {
debugPrint('$_tag setPaymentPassword() - 开始设置支付密码');
try {
await _apiClient.post(
'/user/set-payment-password',
data: {'password': password},
);
debugPrint('$_tag setPaymentPassword() - 支付密码设置成功');
} on ApiException catch (e) {
debugPrint('$_tag setPaymentPassword() - API 异常: $e');
rethrow;
} catch (e) {
debugPrint('$_tag setPaymentPassword() - 未知异常: $e');
throw ApiException('设置支付密码失败: $e');
}
}
/// (POST /user/change-payment-password)
///
/// [oldPassword]
/// [newPassword]
Future<void> changePaymentPassword({
required String oldPassword,
required String newPassword,
}) async {
debugPrint('$_tag changePaymentPassword() - 开始修改支付密码');
try {
await _apiClient.post(
'/user/change-payment-password',
data: {'oldPassword': oldPassword, 'newPassword': newPassword},
);
debugPrint('$_tag changePaymentPassword() - 支付密码修改成功');
} on ApiException catch (e) {
debugPrint('$_tag changePaymentPassword() - API 异常: $e');
rethrow;
} catch (e) {
debugPrint('$_tag changePaymentPassword() - 未知异常: $e');
throw ApiException('修改支付密码失败: $e');
}
}
/// (POST /user/verify-payment-password)
///
/// { "valid": true/false }
/// - true
/// - false
/// -
Future<bool> verifyPaymentPassword(String password) async {
debugPrint('$_tag verifyPaymentPassword() - 开始验证支付密码');
try {
final response = await _apiClient.post(
'/user/verify-payment-password',
data: {'password': password},
);
final valid = response.data['valid'] == true;
debugPrint('$_tag verifyPaymentPassword() - 验证结果: $valid');
return valid;
} on ApiException catch (e) {
debugPrint('$_tag verifyPaymentPassword() - 验证异常: $e');
rethrow;
} catch (e) {
debugPrint('$_tag verifyPaymentPassword() - 未知异常: $e');
rethrow;
}
}
// ============ ============
///

View File

@ -243,17 +243,20 @@ class _PlantingLocationPageState extends ConsumerState<PlantingLocationPage> {
}
}
///
/// [2026-03-05]
///
/// [PlantingConfirmDialog] [_submitPlanting]
/// [PasswordVerifyDialog] [_submitPlanting]
/// [PasswordVerifyDialog] [_submitPlanting]
///
Future<void> _verifyPasswordThenSubmit() async {
final verified = await PasswordVerifyDialog.show(
context: context,
title: '验证支付密码',
subtitle: '为保障资金安全请输入您的6位支付密码以确认认种',
hint: '请输入支付密码',
onVerify: (password) async {
final accountService = ref.read(accountServiceProvider);
return await accountService.verifyLoginPassword(password);
return await accountService.verifyPaymentPassword(password);
},
);
if (verified == true) {

View File

@ -7,28 +7,45 @@ import 'package:flutter/material.dart';
/// /
typedef PasswordVerifyCallback = Future<bool> Function(String password);
///
///
///
/// [PlantingConfirmDialog] "确认认种"
///
///
/// - /
/// - /
///
///
/// ```dart
/// //
/// final verified = await PasswordVerifyDialog.show(
/// context: context,
/// onVerify: (password) => accountService.verifyLoginPassword(password),
/// title: '验证支付密码',
/// subtitle: '请输入6位支付密码以确认认种',
/// hint: '请输入支付密码',
/// onVerify: (password) => accountService.verifyPaymentPassword(password),
/// );
/// if (verified == true) _submitPlanting();
/// ```
///
/// POST /user/verify-password { valid: bool }
/// POST /user/verify-payment-password { valid: bool }
class PasswordVerifyDialog extends StatefulWidget {
/// accountService.verifyLoginPassword
///
final PasswordVerifyCallback onVerify;
/// "验证登录密码"
final String title;
/// "为保障账户安全,请输入您的登录密码以继续"
final String subtitle;
/// hint"请输入登录密码"
final String hint;
const PasswordVerifyDialog({
super.key,
required this.onVerify,
this.title = '验证登录密码',
this.subtitle = '为保障账户安全,请输入您的登录密码以继续',
this.hint = '请输入登录密码',
});
///
@ -36,12 +53,20 @@ class PasswordVerifyDialog extends StatefulWidget {
static Future<bool?> show({
required BuildContext context,
required PasswordVerifyCallback onVerify,
String title = '验证登录密码',
String subtitle = '为保障账户安全,请输入您的登录密码以继续',
String hint = '请输入登录密码',
}) {
return showDialog<bool>(
context: context,
barrierDismissible: false,
barrierColor: const Color(0x80000000),
builder: (context) => PasswordVerifyDialog(onVerify: onVerify),
builder: (context) => PasswordVerifyDialog(
onVerify: onVerify,
title: title,
subtitle: subtitle,
hint: hint,
),
);
}
@ -73,7 +98,7 @@ class _PasswordVerifyDialogState extends State<PasswordVerifyDialog> {
Future<void> _handleConfirm() async {
final password = _passwordController.text.trim();
if (password.isEmpty) {
setState(() => _errorMessage = '请输入登录密码');
setState(() => _errorMessage = '请输入${widget.hint.replaceAll('请输入', '')}');
return;
}
@ -205,9 +230,9 @@ class _PasswordVerifyDialogState extends State<PasswordVerifyDialog> {
///
Widget _buildTitle() {
return const Text(
'验证登录密码',
style: TextStyle(
return Text(
widget.title,
style: const TextStyle(
fontSize: 20,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
@ -221,9 +246,9 @@ class _PasswordVerifyDialogState extends State<PasswordVerifyDialog> {
///
Widget _buildSubtitle() {
return const Text(
'为保障账户安全,请输入您的登录密码以继续认种',
style: TextStyle(
return Text(
widget.subtitle,
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
height: 1.5,
@ -267,7 +292,7 @@ class _PasswordVerifyDialogState extends State<PasswordVerifyDialog> {
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
border: InputBorder.none,
hintText: '请输入登录密码',
hintText: widget.hint,
hintStyle: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',

View File

@ -6,6 +6,7 @@ import '../../../../core/di/injection_container.dart';
import '../../../../core/services/pre_planting_service.dart';
import '../../../../core/services/tree_pricing_service.dart';
import '../../../../routes/route_paths.dart';
import '../../../planting/presentation/widgets/password_verify_dialog.dart'; // [2026-03-05]
// ============================================
// [2026-02-17]
@ -344,6 +345,19 @@ class _PrePlantingPurchasePageState
final confirmed = await _showConfirmDialog();
if (confirmed != true || !mounted) return;
// [2026-03-05]
final verified = await PasswordVerifyDialog.show(
context: context,
title: '验证支付密码',
subtitle: '为保障资金安全请输入您的6位支付密码以确认预种',
hint: '请输入支付密码',
onVerify: (password) async {
final accountService = ref.read(accountServiceProvider);
return await accountService.verifyPaymentPassword(password);
},
);
if (verified != true || !mounted) return;
setState(() => _isPurchasing = true);
try {

View File

@ -1342,11 +1342,16 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
context.push(RoutePaths.googleAuth);
}
///
///
void _goToChangePassword() {
context.push(RoutePaths.changePassword);
}
/// / [2026-03-05]
void _goToPaymentPassword() {
context.push(RoutePaths.paymentPassword);
}
///
void _goToBindEmail() {
context.push(RoutePaths.bindEmail);
@ -4206,9 +4211,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
// ),
_buildSettingItem(
icon: Icons.lock,
title: '修改登录密码',
title: '修改密码',
onTap: _goToChangePassword,
),
_buildSettingItem(
icon: Icons.payment,
title: '支付密码',
onTap: _goToPaymentPassword,
),
_buildSettingItem(
icon: Icons.email,
title: '绑定邮箱',

View File

@ -0,0 +1,487 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/di/injection_container.dart';
/// /
///
/// -
/// -
///
/// 6
class ChangePaymentPasswordPage extends ConsumerStatefulWidget {
const ChangePaymentPasswordPage({super.key});
@override
ConsumerState<ChangePaymentPasswordPage> createState() =>
_ChangePaymentPasswordPageState();
}
class _ChangePaymentPasswordPageState
extends ConsumerState<ChangePaymentPasswordPage> {
final TextEditingController _oldPasswordController = TextEditingController();
final TextEditingController _newPasswordController = TextEditingController();
final TextEditingController _confirmPasswordController =
TextEditingController();
bool _showOldPassword = false;
bool _showNewPassword = false;
bool _showConfirmPassword = false;
bool _isSubmitting = false;
bool _hasPaymentPassword = false;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadStatus();
}
@override
void dispose() {
_oldPasswordController.dispose();
_newPasswordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
Future<void> _loadStatus() async {
setState(() => _isLoading = true);
try {
final accountService = ref.read(accountServiceProvider);
final hasPassword = await accountService.isPaymentPasswordSet();
if (mounted) {
setState(() {
_hasPaymentPassword = hasPassword;
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_hasPaymentPassword = false;
_isLoading = false;
});
}
}
}
/// 6
String? _validatePassword(String password) {
if (password.length != 6) return '支付密码必须为6位数字';
if (!RegExp(r'^\d{6}$').hasMatch(password)) return '支付密码只能包含数字';
return null;
}
Future<void> _submit() async {
final oldPassword = _oldPasswordController.text;
final newPassword = _newPasswordController.text;
final confirmPassword = _confirmPasswordController.text;
if (_hasPaymentPassword && oldPassword.isEmpty) {
_showError('请输入当前支付密码');
return;
}
if (newPassword.isEmpty) {
_showError('请输入新支付密码');
return;
}
final formatError = _validatePassword(newPassword);
if (formatError != null) {
_showError(formatError);
return;
}
if (confirmPassword != newPassword) {
_showError('两次输入的支付密码不一致');
return;
}
setState(() => _isSubmitting = true);
try {
final accountService = ref.read(accountServiceProvider);
if (_hasPaymentPassword) {
await accountService.changePaymentPassword(
oldPassword: oldPassword,
newPassword: newPassword,
);
} else {
await accountService.setPaymentPassword(newPassword);
}
if (mounted) {
setState(() => _isSubmitting = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_hasPaymentPassword ? '支付密码修改成功' : '支付密码设置成功'),
backgroundColor: const Color(0xFF4CAF50),
),
);
context.pop(true);
}
} catch (e) {
if (mounted) {
setState(() => _isSubmitting = false);
_showError(
'操作失败: ${e.toString().replaceAll('Exception: ', '')}');
}
}
}
void _showError(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.red),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFFFF5E6), Color(0xFFFFE4B5)],
),
),
child: SafeArea(
child: Column(
children: [
_buildAppBar(),
Expanded(
child: _isLoading
? const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Color(0xFFD4AF37)),
),
)
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildStatusCard(),
const SizedBox(height: 24),
_buildPasswordRequirements(),
const SizedBox(height: 24),
if (_hasPaymentPassword) ...[
_buildPasswordInput(
controller: _oldPasswordController,
label: '当前支付密码',
hint: '请输入当前6位支付密码',
showPassword: _showOldPassword,
onToggleVisibility: () => setState(
() => _showOldPassword = !_showOldPassword),
),
const SizedBox(height: 16),
],
_buildPasswordInput(
controller: _newPasswordController,
label: '新支付密码',
hint: '请输入6位数字支付密码',
showPassword: _showNewPassword,
onToggleVisibility: () => setState(
() => _showNewPassword = !_showNewPassword),
),
const SizedBox(height: 16),
_buildPasswordInput(
controller: _confirmPasswordController,
label: '确认支付密码',
hint: '请再次输入6位支付密码',
showPassword: _showConfirmPassword,
onToggleVisibility: () => setState(() =>
_showConfirmPassword = !_showConfirmPassword),
),
const SizedBox(height: 32),
_buildSubmitButton(),
],
),
),
),
],
),
),
),
);
}
Widget _buildAppBar() {
return Container(
height: 64,
padding: const EdgeInsets.only(top: 16, left: 16, right: 16),
child: Row(
children: [
GestureDetector(
onTap: () => context.pop(),
child: Container(
width: 48,
height: 48,
alignment: Alignment.center,
child: const Icon(Icons.arrow_back,
size: 24, color: Color(0xFF5D4037)),
),
),
Expanded(
child: Text(
_hasPaymentPassword ? '修改支付密码' : '设置支付密码',
style: const TextStyle(
fontSize: 18,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.25,
letterSpacing: -0.27,
color: Color(0xFF5D4037),
),
textAlign: TextAlign.center,
),
),
const SizedBox(width: 48),
],
),
);
}
Widget _buildStatusCard() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0x80FFFFFF),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0x33D4AF37), width: 1),
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: _hasPaymentPassword
? const Color(0xFF4CAF50).withValues(alpha: 0.1)
: const Color(0xFFFF9800).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(24),
),
child: Icon(
_hasPaymentPassword ? Icons.payment : Icons.payment_outlined,
color: _hasPaymentPassword
? const Color(0xFF4CAF50)
: const Color(0xFFFF9800),
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_hasPaymentPassword ? '已设置支付密码' : '未设置支付密码',
style: TextStyle(
fontSize: 18,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: _hasPaymentPassword
? const Color(0xFF4CAF50)
: const Color(0xFFFF9800),
),
),
const SizedBox(height: 4),
Text(
_hasPaymentPassword
? '认种、预种等支付操作需要验证支付密码'
: '设置后,认种和预种时需输入支付密码确认',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
color: Color(0xFF745D43),
),
),
],
),
),
],
),
);
}
Widget _buildPasswordRequirements() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFFF5E6),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0x33D4AF37), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.info_outline, size: 20, color: Color(0xFF8B5A2B)),
SizedBox(width: 8),
Text(
'支付密码要求',
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFF5D4037),
),
),
],
),
const SizedBox(height: 12),
_buildRequirementItem('必须为 6 位纯数字'),
_buildRequirementItem('与登录密码相互独立'),
_buildRequirementItem('用于认种、预种等支付场景的二次验证'),
],
),
);
}
Widget _buildRequirementItem(String text) {
return Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
children: [
const Icon(Icons.check_circle_outline,
size: 16, color: Color(0xFFD4AF37)),
const SizedBox(width: 8),
Expanded(
child: Text(
text,
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
color: Color(0xFF745D43),
),
),
),
],
),
);
}
Widget _buildPasswordInput({
required TextEditingController controller,
required String label,
required String hint,
required bool showPassword,
required VoidCallback onToggleVisibility,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
color: Color(0xFF5D4037),
),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
height: 54,
decoration: BoxDecoration(
color: const Color(0x80FFFFFF),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0x80FFFFFF), width: 1),
boxShadow: const [
BoxShadow(
color: Color(0x0D000000),
blurRadius: 2,
offset: Offset(0, 1),
),
],
),
child: TextField(
controller: controller,
obscureText: !showPassword,
keyboardType: TextInputType.number,
maxLength: 6,
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
height: 1.19,
color: Color(0xFF5D4037),
),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
border: InputBorder.none,
counterText: '',
hintText: hint,
hintStyle: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
height: 1.19,
color: Color(0x995D4037),
),
suffixIcon: GestureDetector(
onTap: onToggleVisibility,
child: Icon(
showPassword ? Icons.visibility_off : Icons.visibility,
color: const Color(0xFF8B5A2B),
size: 20,
),
),
),
),
),
],
);
}
Widget _buildSubmitButton() {
return GestureDetector(
onTap: _isSubmitting ? null : _submit,
child: Container(
width: double.infinity,
height: 56,
decoration: BoxDecoration(
color: _isSubmitting
? const Color(0xFFD4AF37).withValues(alpha: 0.5)
: const Color(0xFFD4AF37),
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Color(0x4DD4AF37),
blurRadius: 14,
offset: Offset(0, 4),
),
],
),
child: Center(
child: _isSubmitting
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(
_hasPaymentPassword ? '修改支付密码' : '设置支付密码',
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
letterSpacing: 0.24,
color: Colors.white,
),
),
),
),
);
}
}

View File

@ -27,6 +27,7 @@ import '../features/planting/presentation/pages/planting_quantity_page.dart';
import '../features/planting/presentation/pages/planting_location_page.dart';
import '../features/security/presentation/pages/google_auth_page.dart';
import '../features/security/presentation/pages/change_password_page.dart';
import '../features/security/presentation/pages/change_payment_password_page.dart'; // [2026-03-05]
import '../features/security/presentation/pages/bind_email_page.dart';
import '../features/authorization/presentation/pages/authorization_apply_page.dart';
import '../features/withdraw/presentation/pages/withdraw_usdt_page.dart';
@ -341,6 +342,13 @@ final appRouterProvider = Provider<GoRouter>((ref) {
builder: (context, state) => const ChangePasswordPage(),
),
// Payment Password Page (/) [2026-03-05]
GoRoute(
path: RoutePaths.paymentPassword,
name: RouteNames.paymentPassword,
builder: (context, state) => const ChangePaymentPasswordPage(),
),
// Bind Email Page ()
GoRoute(
path: RoutePaths.bindEmail,

View File

@ -35,6 +35,7 @@ class RouteNames {
static const plantingLocation = 'planting-location';
static const googleAuth = 'google-auth';
static const changePassword = 'change-password';
static const paymentPassword = 'payment-password'; // [2026-03-05]
static const bindEmail = 'bind-email';
static const authorizationApply = 'authorization-apply';
static const transactionHistory = 'transaction-history';

View File

@ -35,6 +35,7 @@ class RoutePaths {
static const plantingLocation = '/planting/location';
static const googleAuth = '/security/google-auth';
static const changePassword = '/security/password';
static const paymentPassword = '/security/payment-password'; // [2026-03-05] /
static const bindEmail = '/security/email';
static const authorizationApply = '/authorization/apply';
static const transactionHistory = '/trading/history';