feat(account): 实现多账号管理功能

- 新增 MultiAccountService 处理多账号存储和切换
- 新增 AccountSwitchPage 账号切换页面
- 修改 storage_keys.dart 添加多账号相关键和前缀方法
- 修改 ProfilePage 切换账号按钮跳转到账号切换页面
- 修改退出登录逻辑,保留账号数据只清除会话状态
- 新账号创建时自动添加到账号列表
- 支持旧单账号数据自动迁移到多账号架构
- 支持删除账号(带确认对话框)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-15 02:57:13 -08:00
parent 04644ea3f8
commit f3a475a6f4
9 changed files with 788 additions and 18 deletions

View File

@ -3,6 +3,7 @@ import '../storage/secure_storage.dart';
import '../storage/local_storage.dart';
import '../network/api_client.dart';
import '../services/account_service.dart';
import '../services/multi_account_service.dart';
import '../services/referral_service.dart';
import '../services/authorization_service.dart';
import '../services/deposit_service.dart';
@ -36,6 +37,12 @@ final accountServiceProvider = Provider<AccountService>((ref) {
);
});
// Multi Account Service Provider
final multiAccountServiceProvider = Provider<MultiAccountService>((ref) {
final secureStorage = ref.watch(secureStorageProvider);
return MultiAccountService(secureStorage);
});
// Referral Service Provider
final referralServiceProvider = Provider<ReferralService>((ref) {
final apiClient = ref.watch(apiClientProvider);

View File

@ -0,0 +1,349 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../storage/secure_storage.dart';
import '../storage/storage_keys.dart';
///
class AccountSummary {
final String userSerialNum; //
final String username; //
final String? avatarSvg; // SVG
final String? avatarUrl; // URL
final DateTime createdAt; //
AccountSummary({
required this.userSerialNum,
required this.username,
this.avatarSvg,
this.avatarUrl,
required this.createdAt,
});
Map<String, dynamic> toJson() => {
'userSerialNum': userSerialNum,
'username': username,
'avatarSvg': avatarSvg,
'avatarUrl': avatarUrl,
'createdAt': createdAt.toIso8601String(),
};
factory AccountSummary.fromJson(Map<String, dynamic> json) {
return AccountSummary(
userSerialNum: json['userSerialNum'] as String,
username: json['username'] as String,
avatarSvg: json['avatarSvg'] as String?,
avatarUrl: json['avatarUrl'] as String?,
createdAt: DateTime.parse(json['createdAt'] as String),
);
}
@override
String toString() => 'AccountSummary(userSerialNum: $userSerialNum, username: $username)';
}
///
///
class MultiAccountService {
static const String _tag = '[MultiAccountService]';
final SecureStorage _secureStorage;
MultiAccountService(this._secureStorage);
///
Future<List<AccountSummary>> getAccountList() async {
debugPrint('$_tag getAccountList() - 获取账号列表');
try {
final jsonStr = await _secureStorage.read(key: StorageKeys.accountList);
if (jsonStr == null || jsonStr.isEmpty) {
debugPrint('$_tag getAccountList() - 账号列表为空');
return [];
}
final List<dynamic> jsonList = jsonDecode(jsonStr);
final accounts = jsonList
.map((e) => AccountSummary.fromJson(e as Map<String, dynamic>))
.toList();
debugPrint('$_tag getAccountList() - 找到 ${accounts.length} 个账号');
return accounts;
} catch (e) {
debugPrint('$_tag getAccountList() - 解析失败: $e');
return [];
}
}
///
Future<void> _saveAccountList(List<AccountSummary> accounts) async {
final jsonStr = jsonEncode(accounts.map((e) => e.toJson()).toList());
await _secureStorage.write(key: StorageKeys.accountList, value: jsonStr);
debugPrint('$_tag _saveAccountList() - 保存 ${accounts.length} 个账号');
}
///
Future<void> addAccount(AccountSummary account) async {
debugPrint('$_tag addAccount() - 添加账号: ${account.userSerialNum}');
final accounts = await getAccountList();
//
final existingIndex = accounts.indexWhere(
(a) => a.userSerialNum == account.userSerialNum,
);
if (existingIndex >= 0) {
//
accounts[existingIndex] = account;
debugPrint('$_tag addAccount() - 更新现有账号');
} else {
//
accounts.add(account);
debugPrint('$_tag addAccount() - 添加新账号');
}
await _saveAccountList(accounts);
}
///
Future<void> removeAccount(String userSerialNum) async {
debugPrint('$_tag removeAccount() - 移除账号: $userSerialNum');
final accounts = await getAccountList();
accounts.removeWhere((a) => a.userSerialNum == userSerialNum);
await _saveAccountList(accounts);
}
/// ID
Future<String?> getCurrentAccountId() async {
return await _secureStorage.read(key: StorageKeys.currentAccountId);
}
/// ID
Future<void> setCurrentAccountId(String? accountId) async {
if (accountId == null) {
await _secureStorage.delete(key: StorageKeys.currentAccountId);
} else {
await _secureStorage.write(key: StorageKeys.currentAccountId, value: accountId);
}
debugPrint('$_tag setCurrentAccountId() - 设置为: $accountId');
}
///
/// true
Future<bool> switchToAccount(String userSerialNum) async {
debugPrint('$_tag switchToAccount() - 切换到账号: $userSerialNum');
//
final accounts = await getAccountList();
final account = accounts.where((a) => a.userSerialNum == userSerialNum).firstOrNull;
if (account == null) {
debugPrint('$_tag switchToAccount() - 账号不存在');
return false;
}
//
await _restoreAccountData(userSerialNum);
//
await setCurrentAccountId(userSerialNum);
debugPrint('$_tag switchToAccount() - 切换成功');
return true;
}
///
Future<void> saveCurrentAccountData() async {
final currentId = await getCurrentAccountId();
if (currentId == null) {
debugPrint('$_tag saveCurrentAccountData() - 无当前账号');
return;
}
debugPrint('$_tag saveCurrentAccountData() - 保存账号数据: $currentId');
//
final keysToSave = [
StorageKeys.userSerialNum,
StorageKeys.username,
StorageKeys.avatarSvg,
StorageKeys.avatarUrl,
StorageKeys.referralCode,
StorageKeys.inviterSequence,
StorageKeys.isAccountCreated,
StorageKeys.walletAddressBsc,
StorageKeys.walletAddressKava,
StorageKeys.walletAddressDst,
StorageKeys.mnemonic,
StorageKeys.isWalletReady,
StorageKeys.isMnemonicBackedUp,
StorageKeys.accessToken,
StorageKeys.refreshToken,
];
for (final key in keysToSave) {
final value = await _secureStorage.read(key: key);
if (value != null) {
final prefixedKey = StorageKeys.withAccountPrefix(currentId, key);
await _secureStorage.write(key: prefixedKey, value: value);
}
}
debugPrint('$_tag saveCurrentAccountData() - 保存完成');
}
///
Future<void> _restoreAccountData(String accountId) async {
debugPrint('$_tag _restoreAccountData() - 恢复账号数据: $accountId');
//
final keysToRestore = [
StorageKeys.userSerialNum,
StorageKeys.username,
StorageKeys.avatarSvg,
StorageKeys.avatarUrl,
StorageKeys.referralCode,
StorageKeys.inviterSequence,
StorageKeys.isAccountCreated,
StorageKeys.walletAddressBsc,
StorageKeys.walletAddressKava,
StorageKeys.walletAddressDst,
StorageKeys.mnemonic,
StorageKeys.isWalletReady,
StorageKeys.isMnemonicBackedUp,
StorageKeys.accessToken,
StorageKeys.refreshToken,
];
for (final key in keysToRestore) {
final prefixedKey = StorageKeys.withAccountPrefix(accountId, key);
final value = await _secureStorage.read(key: prefixedKey);
if (value != null) {
await _secureStorage.write(key: key, value: value);
} else {
//
await _secureStorage.delete(key: key);
}
}
debugPrint('$_tag _restoreAccountData() - 恢复完成');
}
/// 退
///
Future<void> logoutCurrentAccount() async {
debugPrint('$_tag logoutCurrentAccount() - 退出当前账号');
//
await saveCurrentAccountData();
// Token
await _secureStorage.delete(key: StorageKeys.accessToken);
await _secureStorage.delete(key: StorageKeys.refreshToken);
//
await setCurrentAccountId(null);
//
await _secureStorage.delete(key: StorageKeys.userSerialNum);
await _secureStorage.delete(key: StorageKeys.username);
await _secureStorage.delete(key: StorageKeys.isAccountCreated);
debugPrint('$_tag logoutCurrentAccount() - 退出完成');
}
///
Future<void> deleteAccount(String userSerialNum) async {
debugPrint('$_tag deleteAccount() - 删除账号: $userSerialNum');
//
await removeAccount(userSerialNum);
//
final keysToDelete = [
StorageKeys.userSerialNum,
StorageKeys.username,
StorageKeys.avatarSvg,
StorageKeys.avatarUrl,
StorageKeys.referralCode,
StorageKeys.inviterSequence,
StorageKeys.isAccountCreated,
StorageKeys.walletAddressBsc,
StorageKeys.walletAddressKava,
StorageKeys.walletAddressDst,
StorageKeys.mnemonic,
StorageKeys.isWalletReady,
StorageKeys.isMnemonicBackedUp,
StorageKeys.accessToken,
StorageKeys.refreshToken,
];
for (final key in keysToDelete) {
final prefixedKey = StorageKeys.withAccountPrefix(userSerialNum, key);
await _secureStorage.delete(key: prefixedKey);
}
//
final currentId = await getCurrentAccountId();
if (currentId == userSerialNum) {
await logoutCurrentAccount();
}
debugPrint('$_tag deleteAccount() - 删除完成');
}
///
///
Future<void> migrateFromSingleAccount() async {
debugPrint('$_tag migrateFromSingleAccount() - 检查是否需要迁移');
//
final accounts = await getAccountList();
if (accounts.isNotEmpty) {
debugPrint('$_tag migrateFromSingleAccount() - 已有账号列表,无需迁移');
return;
}
//
final userSerialNum = await _secureStorage.read(key: StorageKeys.userSerialNum);
final isAccountCreated = await _secureStorage.read(key: StorageKeys.isAccountCreated);
if (userSerialNum == null || isAccountCreated != 'true') {
debugPrint('$_tag migrateFromSingleAccount() - 无旧账号数据');
return;
}
//
final username = await _secureStorage.read(key: StorageKeys.username) ?? '未知用户';
final avatarSvg = await _secureStorage.read(key: StorageKeys.avatarSvg);
final avatarUrl = await _secureStorage.read(key: StorageKeys.avatarUrl);
//
final account = AccountSummary(
userSerialNum: userSerialNum,
username: username,
avatarSvg: avatarSvg,
avatarUrl: avatarUrl,
createdAt: DateTime.now(), // 使
);
//
await addAccount(account);
//
await setCurrentAccountId(userSerialNum);
//
await saveCurrentAccountData();
debugPrint('$_tag migrateFromSingleAccount() - 迁移完成: $userSerialNum');
}
///
Future<bool> hasAnyAccount() async {
final accounts = await getAccountList();
return accounts.isNotEmpty;
}
///
Future<int> getAccountCount() async {
final accounts = await getAccountList();
return accounts.length;
}
}

View File

@ -1,6 +1,10 @@
class StorageKeys {
StorageKeys._();
// ===== =====
static const String accountList = 'account_list'; // (JSON )
static const String currentAccountId = 'current_account_id'; // ID (userSerialNum)
//
static const String userSerialNum = 'user_serial_num'; //
static const String username = 'username'; //
@ -11,6 +15,12 @@ class StorageKeys {
static const String inviterReferralCode = 'inviter_referral_code'; //
static const String isAccountCreated = 'is_account_created'; //
///
///
static String withAccountPrefix(String accountId, String key) {
return 'account_${accountId}_$key';
}
//
static const String walletAddressBsc = 'wallet_address_bsc';
static const String walletAddressKava = 'wallet_address_kava';

View File

@ -0,0 +1,392 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.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 AccountSwitchPage extends ConsumerStatefulWidget {
const AccountSwitchPage({super.key});
@override
ConsumerState<AccountSwitchPage> createState() => _AccountSwitchPageState();
}
class _AccountSwitchPageState extends ConsumerState<AccountSwitchPage> {
List<AccountSummary> _accounts = [];
String? _currentAccountId;
bool _isLoading = true;
bool _isSwitching = false;
@override
void initState() {
super.initState();
_loadAccounts();
}
Future<void> _loadAccounts() async {
setState(() => _isLoading = true);
final multiAccountService = ref.read(multiAccountServiceProvider);
//
await multiAccountService.migrateFromSingleAccount();
final accounts = await multiAccountService.getAccountList();
final currentId = await multiAccountService.getCurrentAccountId();
if (mounted) {
setState(() {
_accounts = accounts;
_currentAccountId = currentId;
_isLoading = false;
});
}
}
Future<void> _switchToAccount(AccountSummary account) async {
if (account.userSerialNum == _currentAccountId) {
//
context.pop();
return;
}
setState(() => _isSwitching = true);
try {
final multiAccountService = ref.read(multiAccountServiceProvider);
//
await multiAccountService.saveCurrentAccountData();
//
final success = await multiAccountService.switchToAccount(account.userSerialNum);
if (success && mounted) {
//
context.go(RoutePaths.ranking);
} else if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('切换账号失败')),
);
}
} catch (e) {
debugPrint('[AccountSwitchPage] 切换账号失败: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('切换账号失败: $e')),
);
}
} finally {
if (mounted) {
setState(() => _isSwitching = false);
}
}
}
Future<void> _addNewAccount() async {
//
final multiAccountService = ref.read(multiAccountServiceProvider);
await multiAccountService.saveCurrentAccountData();
// 退
await multiAccountService.logoutCurrentAccount();
//
if (mounted) {
context.go(RoutePaths.guide);
}
}
Future<void> _deleteAccount(AccountSummary account) async {
//
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('删除账号'),
content: Text(
'确定要删除账号 "${account.username}" 吗?\n\n'
'删除后该账号的所有本地数据将被清除,'
'如需恢复请使用助记词重新导入。',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFFE57373),
),
child: const Text('删除'),
),
],
),
);
if (confirmed != true) return;
try {
final multiAccountService = ref.read(multiAccountServiceProvider);
await multiAccountService.deleteAccount(account.userSerialNum);
//
await _loadAccounts();
//
if (_accounts.isEmpty && mounted) {
context.go(RoutePaths.guide);
}
} catch (e) {
debugPrint('[AccountSwitchPage] 删除账号失败: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('删除账号失败: $e')),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFFFFBF5),
appBar: AppBar(
backgroundColor: const Color(0xFFFFFBF5),
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Color(0xFF5D4037)),
onPressed: () => context.pop(),
),
title: const Text(
'切换账号',
style: TextStyle(
fontSize: 18,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFF5D4037),
),
),
centerTitle: true,
),
body: _isLoading
? const Center(
child: CircularProgressIndicator(
color: Color(0xFFD4AF37),
),
)
: _isSwitching
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: Color(0xFFD4AF37),
),
SizedBox(height: 16),
Text(
'正在切换账号...',
style: TextStyle(
fontSize: 14,
color: Color(0xFF5D4037),
),
),
],
),
)
: ListView(
padding: const EdgeInsets.all(16),
children: [
//
const Padding(
padding: EdgeInsets.only(bottom: 12),
child: Text(
'已登录的账号',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
color: Color(0x995D4037),
),
),
),
//
..._accounts.map((account) => _buildAccountItem(account)),
const SizedBox(height: 16),
//
_buildAddAccountButton(),
],
),
);
}
Widget _buildAccountItem(AccountSummary account) {
final isCurrent = account.userSerialNum == _currentAccountId;
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: isCurrent ? const Color(0xFFD4AF37).withValues(alpha: 0.1) : const Color(0xFFFFF5E6),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isCurrent ? const Color(0xFFD4AF37) : const Color(0x33D4AF37),
width: isCurrent ? 2 : 1,
),
),
child: InkWell(
onTap: () => _switchToAccount(account),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
//
_buildAvatar(account),
const SizedBox(width: 12),
//
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
account.username,
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFF5D4037),
),
),
if (isCurrent) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color(0xFFD4AF37),
borderRadius: BorderRadius.circular(10),
),
child: const Text(
'当前',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
],
],
),
const SizedBox(height: 4),
Text(
account.userSerialNum,
style: const TextStyle(
fontSize: 12,
fontFamily: 'Inter',
color: Color(0x995D4037),
),
),
],
),
),
//
if (!isCurrent)
IconButton(
icon: const Icon(
Icons.delete_outline,
color: Color(0xFFE57373),
size: 20,
),
onPressed: () => _deleteAccount(account),
),
//
if (isCurrent)
const Icon(
Icons.check_circle,
color: Color(0xFFD4AF37),
size: 24,
),
],
),
),
),
);
}
Widget _buildAvatar(AccountSummary account) {
return Container(
width: 48,
height: 48,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
color: const Color(0xFFE8D5B7),
),
clipBehavior: Clip.antiAlias,
child: account.avatarSvg != null
? SvgPicture.string(
account.avatarSvg!,
fit: BoxFit.cover,
)
: const Icon(
Icons.person,
size: 28,
color: Color(0xFF8B5A2B),
),
);
}
Widget _buildAddAccountButton() {
return GestureDetector(
onTap: _addNewAccount,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFFF5E6),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
style: BorderStyle.solid,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: const Color(0xFFD4AF37).withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(16),
),
child: const Icon(
Icons.add,
size: 20,
color: Color(0xFFD4AF37),
),
),
const SizedBox(width: 12),
const Text(
'添加新账号',
style: TextStyle(
fontSize: 15,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
color: Color(0xFF5D4037),
),
),
],
),
),
);
}
}

View File

@ -6,6 +6,7 @@ import '../../../../routes/route_paths.dart';
import '../../../../routes/app_router.dart';
import '../../../../core/di/injection_container.dart';
import '../../../../core/storage/storage_keys.dart';
import '../../../../core/services/multi_account_service.dart';
/// -
///
@ -130,6 +131,19 @@ class _OnboardingPageState extends ConsumerState<OnboardingPage> {
debugPrint('[OnboardingPage] _createAccount - 已清除临时邀请人推荐码');
}
//
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('[OnboardingPage] _createAccount - 已添加到多账号列表');
if (!mounted) {
debugPrint('[OnboardingPage] _createAccount - Widget已卸载忽略响应');
return;

View File

@ -2867,21 +2867,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
);
}
///
/// -
void _onSwitchAccount() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('切换账号'),
content: const Text('多账号管理功能即将上线,敬请期待。'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('确定'),
),
],
),
);
context.push(RoutePaths.accountSwitch);
}
/// 退
@ -2890,7 +2878,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
context: context,
builder: (context) => AlertDialog(
title: const Text('退出登录'),
content: const Text('确定要退出当前账号吗?\n\n退出后需要重新导入助记词才能恢复账号。'),
content: const Text('确定要退出当前账号吗?\n\n退出后账号数据会保留在本地,可在账号切换页面重新登录'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
@ -2914,9 +2902,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
/// 退
Future<void> _performLogout() async {
try {
final accountService = ref.read(accountServiceProvider);
//
await accountService.logout();
final multiAccountService = ref.read(multiAccountServiceProvider);
// 退
await multiAccountService.logoutCurrentAccount();
//
if (mounted) {
context.go(RoutePaths.guide);

View File

@ -25,6 +25,7 @@ import '../features/security/presentation/pages/bind_email_page.dart';
import '../features/withdraw/presentation/pages/withdraw_usdt_page.dart';
import '../features/withdraw/presentation/pages/withdraw_confirm_page.dart';
import '../features/notification/presentation/pages/notification_inbox_page.dart';
import '../features/account/presentation/pages/account_switch_page.dart';
import 'route_paths.dart';
import 'route_names.dart';
@ -187,6 +188,13 @@ final appRouterProvider = Provider<GoRouter>((ref) {
builder: (context, state) => const NotificationInboxPage(),
),
// Account Switch Page ()
GoRoute(
path: RoutePaths.accountSwitch,
name: RouteNames.accountSwitch,
builder: (context, state) => const AccountSwitchPage(),
),
// Share Page ()
GoRoute(
path: RoutePaths.share,

View File

@ -23,6 +23,7 @@ class RouteNames {
static const referralList = 'referral-list';
static const earningsDetail = 'earnings-detail';
static const notifications = 'notifications';
static const accountSwitch = 'account-switch';
static const deposit = 'deposit';
static const depositUsdt = 'deposit-usdt';
static const plantingQuantity = 'planting-quantity';

View File

@ -23,6 +23,7 @@ class RoutePaths {
static const referralList = '/profile/referrals';
static const earningsDetail = '/profile/earnings';
static const notifications = '/notifications';
static const accountSwitch = '/account/switch';
static const deposit = '/deposit';
static const depositUsdt = '/deposit/usdt';
static const plantingQuantity = '/planting/quantity';