diff --git a/backend/services/identity-service/prisma/migrations/20260305000000_add_payment_password_hash/migration.sql b/backend/services/identity-service/prisma/migrations/20260305000000_add_payment_password_hash/migration.sql new file mode 100644 index 00000000..8851f3f4 --- /dev/null +++ b/backend/services/identity-service/prisma/migrations/20260305000000_add_payment_password_hash/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable: 新增支付密码哈希字段 +ALTER TABLE "user_accounts" ADD COLUMN "payment_password_hash" VARCHAR(100); diff --git a/backend/services/identity-service/prisma/schema.prisma b/backend/services/identity-service/prisma/schema.prisma index 6369b28f..5b43ba9d 100644 --- a/backend/services/identity-service/prisma/schema.prisma +++ b/backend/services/identity-service/prisma/schema.prisma @@ -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 diff --git a/backend/services/identity-service/src/api/controllers/user-account.controller.ts b/backend/services/identity-service/src/api/controllers/user-account.controller.ts index 8daae251..432777f9 100644 --- a/backend/services/identity-service/src/api/controllers/user-account.controller.ts +++ b/backend/services/identity-service/src/api/controllers/user-account.controller.ts @@ -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({ diff --git a/backend/services/identity-service/src/application/services/user-application.service.ts b/backend/services/identity-service/src/application/services/user-application.service.ts index 129b2f65..5fdefe4b 100644 --- a/backend/services/identity-service/src/application/services/user-application.service.ts +++ b/backend/services/identity-service/src/application/services/user-application.service.ts @@ -2856,6 +2856,106 @@ export class UserApplicationService { return isValid; } + // ============ 支付密码相关 ============ + + /** + * 查询是否已设置支付密码 + */ + async isPaymentPasswordSet(userId: string): Promise { + 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 { + 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 { + 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 { + 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; + } + /** * 遮蔽手机号 */ diff --git a/frontend/mobile-app/lib/core/services/account_service.dart b/frontend/mobile-app/lib/core/services/account_service.dart index 00771270..50367a21 100644 --- a/frontend/mobile-app/lib/core/services/account_service.dart +++ b/frontend/mobile-app/lib/core/services/account_service.dart @@ -2124,6 +2124,93 @@ class AccountService { } } + // ============ 支付密码相关 ============ + + /// 查询是否已设置支付密码 (GET /user/payment-password-status) + /// + /// 返回 true 表示已设置,false 表示未设置 + Future 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 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 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 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; + } + } + // ============ 邮箱绑定相关 ============ /// 获取邮箱绑定状态 diff --git a/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart b/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart index fec8343f..9efeffae 100644 --- a/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart +++ b/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart @@ -243,17 +243,20 @@ class _PlantingLocationPageState extends ConsumerState { } } - /// 验证登录密码后再提交认种 + /// 验证支付密码后再提交认种 [2026-03-05] /// /// 插入在 [PlantingConfirmDialog] 确认与 [_submitPlanting] 之间: - /// 弹出 [PasswordVerifyDialog] → 密码正确才调用 [_submitPlanting], + /// 弹出 [PasswordVerifyDialog](支付密码模式)→ 密码正确才调用 [_submitPlanting], /// 取消或验证失败则停留在当前页,不触发任何提交逻辑。 Future _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) { diff --git a/frontend/mobile-app/lib/features/planting/presentation/widgets/password_verify_dialog.dart b/frontend/mobile-app/lib/features/planting/presentation/widgets/password_verify_dialog.dart index 2df6569c..9a8bb5a8 100644 --- a/frontend/mobile-app/lib/features/planting/presentation/widgets/password_verify_dialog.dart +++ b/frontend/mobile-app/lib/features/planting/presentation/widgets/password_verify_dialog.dart @@ -7,28 +7,45 @@ import 'package:flutter/material.dart'; /// 抛出异常表示网络/系统错误(由弹窗捕获并展示提示) typedef PasswordVerifyCallback = Future 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 show({ required BuildContext context, required PasswordVerifyCallback onVerify, + String title = '验证登录密码', + String subtitle = '为保障账户安全,请输入您的登录密码以继续', + String hint = '请输入登录密码', }) { return showDialog( 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 { Future _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 { /// 构建标题 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 { /// 构建说明文字 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 { decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), border: InputBorder.none, - hintText: '请输入登录密码', + hintText: widget.hint, hintStyle: const TextStyle( fontSize: 16, fontFamily: 'Inter', diff --git a/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_purchase_page.dart b/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_purchase_page.dart index f1ae714a..a6fbdecf 100644 --- a/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_purchase_page.dart +++ b/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_purchase_page.dart @@ -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 { diff --git a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart index 7ef885b5..f70277e7 100644 --- a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart +++ b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart @@ -1342,11 +1342,16 @@ class _ProfilePageState extends ConsumerState { 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 { // ), _buildSettingItem( icon: Icons.lock, - title: '修改登录密码', + title: '修改密码', onTap: _goToChangePassword, ), + _buildSettingItem( + icon: Icons.payment, + title: '支付密码', + onTap: _goToPaymentPassword, + ), _buildSettingItem( icon: Icons.email, title: '绑定邮箱', diff --git a/frontend/mobile-app/lib/features/security/presentation/pages/change_payment_password_page.dart b/frontend/mobile-app/lib/features/security/presentation/pages/change_payment_password_page.dart new file mode 100644 index 00000000..5ec834dd --- /dev/null +++ b/frontend/mobile-app/lib/features/security/presentation/pages/change_payment_password_page.dart @@ -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 createState() => + _ChangePaymentPasswordPageState(); +} + +class _ChangePaymentPasswordPageState + extends ConsumerState { + 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 _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 _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(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(Colors.white), + ), + ) + : Text( + _hasPaymentPassword ? '修改支付密码' : '设置支付密码', + style: const TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + height: 1.5, + letterSpacing: 0.24, + color: Colors.white, + ), + ), + ), + ), + ); + } +} diff --git a/frontend/mobile-app/lib/routes/app_router.dart b/frontend/mobile-app/lib/routes/app_router.dart index 3bc7e4ef..3ad4afa7 100644 --- a/frontend/mobile-app/lib/routes/app_router.dart +++ b/frontend/mobile-app/lib/routes/app_router.dart @@ -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((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, diff --git a/frontend/mobile-app/lib/routes/route_names.dart b/frontend/mobile-app/lib/routes/route_names.dart index d1517ffa..a518effe 100644 --- a/frontend/mobile-app/lib/routes/route_names.dart +++ b/frontend/mobile-app/lib/routes/route_names.dart @@ -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'; diff --git a/frontend/mobile-app/lib/routes/route_paths.dart b/frontend/mobile-app/lib/routes/route_paths.dart index 942f5af1..7d2ae885 100644 --- a/frontend/mobile-app/lib/routes/route_paths.dart +++ b/frontend/mobile-app/lib/routes/route_paths.dart @@ -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';