From 78d7e0e6371e54782e5bbc937bd0da7de68a947f Mon Sep 17 00:00:00 2001 From: hailin Date: Fri, 26 Dec 2025 03:57:35 -0800 Subject: [PATCH] =?UTF-8?q?feat(mobile-app):=20=E6=B5=81=E6=B0=B4=E6=98=8E?= =?UTF-8?q?=E7=BB=86=E6=94=AF=E6=8C=81=E6=98=BE=E7=A4=BA=E6=9D=83=E7=9B=8A?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=92=8C=E8=AF=A6=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端 wallet-service: getMyLedger API 返回 allocationType 字段 - 前端流水明细: 显示权益类型名称(分享权益、省/市区域权益等) - 新增权益详情弹窗,点击权益记录可查看详细信息 - 兑换页面: "RMB/CNY提现" 改为 "提现" - 我的团队: "暂无下级成员" 改为 "暂无团队成员" - 自助申请授权: 隐藏团队链占用区域提示 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .claude/settings.local.json | 3 +- .../src/api/dto/response/ledger.dto.ts | 3 + .../services/wallet-application.service.ts | 2 + .../lib/core/services/wallet_service.dart | 22 ++ .../pages/authorization_apply_page.dart | 24 +- .../presentation/pages/profile_page.dart | 2 +- .../pages/ledger_detail_page.dart | 246 ++++++++++++++++-- .../presentation/pages/trading_page.dart | 2 +- 8 files changed, 273 insertions(+), 31 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 159674f1..90b16a97 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -423,7 +423,8 @@ "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(mobile-app\\): 使用 appRouterProvider 获取全局路由状态\n\n改用 ref.read\\(appRouterProvider\\) 替代 GoRouter.of\\(context\\),\n确保能正确获取到当前的全局路由路径。\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(mobile-app\\): 首次检查也加入路由判断,避免在KYC页面弹窗\n\n_checkContractsAndKyc\\(\\) 方法之前没有调用 _shouldSkipContractCheck\\(\\),\n导致用户在合同/KYC页面时首次检查仍会弹窗。\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(mobile-app\\): 遍历路由栈检测当前页面,修复push导航检测问题\n\n之前只检查 currentConfiguration.uri.path,对于 push 导航的页面无法正确检测。\n现在遍历整个 matches 路由栈,只要栈中有合同/KYC页面就跳过弹窗。\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", - "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(planting-service\\): 修复跨服务调用使用错误标识符导致的500错误\n\n问题根源:\n- getBalance 调用使用 userId.toString\\(\\) \\(纯数字如 \"14\"\\)\n- wallet-service 按 accountSequence 查找钱包失败后尝试创建新钱包\n- 但 userId 已存在,触发唯一约束冲突导致500错误\n\n修复内容:\n1. planting-application.service.ts:\n - createOrder: getBalance\\(userId.toString\\(\\)\\) → getBalance\\(accountSequence\\)\n - payOrder: getBalance\\(userId.toString\\(\\)\\) → getBalance\\(walletIdentifier\\)\n\n2. payment-compensation.service.ts:\n - 注入 IPlantingOrderRepository 获取订单的 accountSequence\n - handleUnfreeze/handleRetryConfirm 添加 accountSequence 参数\n\n3. wallet-service.client.ts:\n - ensureRegionAccounts 接口添加 provinceTeamAccount/cityTeamAccount 字段\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")" + "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(planting-service\\): 修复跨服务调用使用错误标识符导致的500错误\n\n问题根源:\n- getBalance 调用使用 userId.toString\\(\\) \\(纯数字如 \"14\"\\)\n- wallet-service 按 accountSequence 查找钱包失败后尝试创建新钱包\n- 但 userId 已存在,触发唯一约束冲突导致500错误\n\n修复内容:\n1. planting-application.service.ts:\n - createOrder: getBalance\\(userId.toString\\(\\)\\) → getBalance\\(accountSequence\\)\n - payOrder: getBalance\\(userId.toString\\(\\)\\) → getBalance\\(walletIdentifier\\)\n\n2. payment-compensation.service.ts:\n - 注入 IPlantingOrderRepository 获取订单的 accountSequence\n - handleUnfreeze/handleRetryConfirm 添加 accountSequence 参数\n\n3. wallet-service.client.ts:\n - ensureRegionAccounts 接口添加 provinceTeamAccount/cityTeamAccount 字段\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(mobile-app\\): 流水明细支持显示权益类型和详情\n\n- 后端 wallet-service: getMyLedger API 返回 allocationType 字段\n- 前端流水明细: 显示权益类型名称(分享权益、省/市区域权益等)\n- 新增权益详情弹窗,点击权益记录可查看详细信息\n- 兑换页面: \"RMB/CNY提现\" 改为 \"提现\"\n- 我的团队: \"暂无下级成员\" 改为 \"暂无团队成员\"\n- 自助申请授权: 隐藏团队链占用区域提示\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")" ], "deny": [], "ask": [] diff --git a/backend/services/wallet-service/src/api/dto/response/ledger.dto.ts b/backend/services/wallet-service/src/api/dto/response/ledger.dto.ts index 773a36f0..91e39770 100644 --- a/backend/services/wallet-service/src/api/dto/response/ledger.dto.ts +++ b/backend/services/wallet-service/src/api/dto/response/ledger.dto.ts @@ -25,6 +25,9 @@ export class LedgerEntryResponseDTO { @ApiProperty({ description: '备注', nullable: true }) memo: string | null; + @ApiProperty({ description: '权益分配类型 (SHARE_RIGHT, PROVINCE_AREA_RIGHT, CITY_AREA_RIGHT, COMMUNITY_RIGHT 等)', nullable: true }) + allocationType: string | null; + @ApiProperty({ description: '创建时间' }) createdAt: string; } diff --git a/backend/services/wallet-service/src/application/services/wallet-application.service.ts b/backend/services/wallet-service/src/application/services/wallet-application.service.ts index d846c40e..58a0b0a5 100644 --- a/backend/services/wallet-service/src/application/services/wallet-application.service.ts +++ b/backend/services/wallet-service/src/application/services/wallet-application.service.ts @@ -61,6 +61,7 @@ export interface LedgerEntryDTO { refOrderId: string | null; refTxHash: string | null; memo: string | null; + allocationType: string | null; createdAt: string; } @@ -1730,6 +1731,7 @@ export class WalletApplicationService { refOrderId: entry.refOrderId, refTxHash: entry.refTxHash, memo: entry.memo, + allocationType: (entry.payloadJson as Record)?.allocationType as string ?? null, createdAt: entry.createdAt.toISOString(), })), total: result.total, diff --git a/frontend/mobile-app/lib/core/services/wallet_service.dart b/frontend/mobile-app/lib/core/services/wallet_service.dart index f81d7899..bf0cc004 100644 --- a/frontend/mobile-app/lib/core/services/wallet_service.dart +++ b/frontend/mobile-app/lib/core/services/wallet_service.dart @@ -747,6 +747,7 @@ class LedgerEntry { final String? refOrderId; final String? refTxHash; final String? memo; + final String? allocationType; final DateTime createdAt; LedgerEntry({ @@ -758,6 +759,7 @@ class LedgerEntry { this.refOrderId, this.refTxHash, this.memo, + this.allocationType, required this.createdAt, }); @@ -771,6 +773,7 @@ class LedgerEntry { refOrderId: json['refOrderId'], refTxHash: json['refTxHash'], memo: json['memo'], + allocationType: json['allocationType'], createdAt: json['createdAt'] != null ? DateTime.tryParse(json['createdAt']) ?? DateTime.now() : DateTime.now(), @@ -801,6 +804,25 @@ class LedgerEntry { return nameMap[entryType] ?? entryType; } + /// 获取权益分配类型中文名 + String? get allocationTypeName { + if (allocationType == null) return null; + const nameMap = { + 'SHARE_RIGHT': '分享权益', + 'PROVINCE_AREA_RIGHT': '省区域权益', + 'PROVINCE_TEAM_RIGHT': '省团队权益', + 'CITY_AREA_RIGHT': '市区域权益', + 'CITY_TEAM_RIGHT': '市团队权益', + 'COMMUNITY_RIGHT': '社区权益', + }; + return nameMap[allocationType] ?? allocationType; + } + + /// 获取显示名称(优先显示权益类型,否则显示流水类型) + String get displayName { + return allocationTypeName ?? entryTypeName; + } + /// 是否为收入 bool get isIncome => amount > 0; } diff --git a/frontend/mobile-app/lib/features/authorization/presentation/pages/authorization_apply_page.dart b/frontend/mobile-app/lib/features/authorization/presentation/pages/authorization_apply_page.dart index 8efd3ac3..2e2e9f78 100644 --- a/frontend/mobile-app/lib/features/authorization/presentation/pages/authorization_apply_page.dart +++ b/frontend/mobile-app/lib/features/authorization/presentation/pages/authorization_apply_page.dart @@ -979,18 +979,18 @@ class _AuthorizationApplyPageState : const Color(0xFF745D43), ), ), - // 显示团队链中已占用区域提示 - if (occupiedCount > 0 && !isDisabled) ...[ - const SizedBox(height: 8), - Text( - '团队链中已有 $occupiedCount 个${type == AuthorizationType.cityTeam ? '城市' : '省份'}被申请,请选择其他区域', - style: const TextStyle( - fontSize: 12, - fontFamily: 'Inter', - color: Color(0xFFFF9800), - ), - ), - ], + // 显示团队链中已占用区域提示 (暂时隐藏) + // if (occupiedCount > 0 && !isDisabled) ...[ + // const SizedBox(height: 8), + // Text( + // '团队链中已有 $occupiedCount 个${type == AuthorizationType.cityTeam ? '城市' : '省份'}被申请,请选择其他区域', + // style: const TextStyle( + // fontSize: 12, + // fontFamily: 'Inter', + // color: Color(0xFFFF9800), + // ), + // ), + // ], // 显示互斥限制提示 if (isMutuallyExcluded) ...[ const SizedBox(height: 8), 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 471f4577..166e568c 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 @@ -3402,7 +3402,7 @@ class _ProfilePageState extends ConsumerState { child: Padding( padding: EdgeInsets.all(20), child: Text( - '暂无下级成员', + '暂无团队成员', style: TextStyle( fontSize: 14, fontFamily: 'Inter', diff --git a/frontend/mobile-app/lib/features/trading/presentation/pages/ledger_detail_page.dart b/frontend/mobile-app/lib/features/trading/presentation/pages/ledger_detail_page.dart index 1813200a..68882f0a 100644 --- a/frontend/mobile-app/lib/features/trading/presentation/pages/ledger_detail_page.dart +++ b/frontend/mobile-app/lib/features/trading/presentation/pages/ledger_detail_page.dart @@ -926,11 +926,13 @@ class _LedgerDetailPageState extends ConsumerState /// 构建流水项 Widget _buildLedgerItem(LedgerEntry entry) { final isIncome = entry.isIncome; - // 只有"认种支付"类型可点击查看详情 - final bool isClickable = entry.entryType == 'PLANT_PAYMENT' && entry.refOrderId != null; + // 可点击查看详情的类型:认种支付、权益分配 + final bool isPlantPayment = entry.entryType == 'PLANT_PAYMENT' && entry.refOrderId != null; + final bool isRewardEntry = entry.allocationType != null; + final bool isClickable = isPlantPayment || isRewardEntry; return GestureDetector( - onTap: isClickable ? () => _showTransactionDetail(entry) : null, + onTap: isClickable ? () => _showEntryDetail(entry) : null, child: Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(16), @@ -970,7 +972,7 @@ class _LedgerDetailPageState extends ConsumerState crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - entry.entryTypeName, + entry.displayName, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, @@ -985,18 +987,6 @@ class _LedgerDetailPageState extends ConsumerState color: Color(0x995D4037), ), ), - if (entry.memo != null && entry.memo!.isNotEmpty) ...[ - const SizedBox(height: 2), - Text( - entry.memo!, - style: const TextStyle( - fontSize: 10, - color: Color(0x995D4037), - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], ], ), ), @@ -1050,6 +1040,17 @@ class _LedgerDetailPageState extends ConsumerState ); } + /// 显示流水详情(根据类型显示不同弹窗) + void _showEntryDetail(LedgerEntry entry) { + if (entry.entryType == 'PLANT_PAYMENT' && entry.refOrderId != null) { + // 认种支付 - 显示合同详情 + _showTransactionDetail(entry); + } else if (entry.allocationType != null) { + // 权益分配 - 显示权益详情 + _showRewardDetail(entry); + } + } + /// 显示交易详情弹窗(认种支付) Future _showTransactionDetail(LedgerEntry entry) async { if (entry.refOrderId == null) return; @@ -1066,6 +1067,16 @@ class _LedgerDetailPageState extends ConsumerState ); } + /// 显示权益详情弹窗 + void _showRewardDetail(LedgerEntry entry) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => _RewardDetailSheet(entry: entry), + ); + } + /// 查看合同 PDF Future _viewContractPdf(String orderNo) async { // 显示加载指示器 @@ -1391,3 +1402,206 @@ class _ContractPdfViewerPage extends StatelessWidget { ); } } + +/// 权益详情底部弹窗 +class _RewardDetailSheet extends StatelessWidget { + final LedgerEntry entry; + + const _RewardDetailSheet({required this.entry}); + + @override + Widget build(BuildContext context) { + final isIncome = entry.isIncome; + + // 根据权益类型获取图标 + IconData getRewardIcon() { + switch (entry.allocationType) { + case 'SHARE_RIGHT': + return Icons.share_outlined; + case 'PROVINCE_AREA_RIGHT': + case 'PROVINCE_TEAM_RIGHT': + return Icons.location_city_outlined; + case 'CITY_AREA_RIGHT': + case 'CITY_TEAM_RIGHT': + return Icons.apartment_outlined; + case 'COMMUNITY_RIGHT': + return Icons.groups_outlined; + default: + return Icons.card_giftcard_outlined; + } + } + + // 根据权益类型获取描述 + String getRewardDescription() { + switch (entry.allocationType) { + case 'SHARE_RIGHT': + return '通过分享邀请获得的权益收益'; + case 'PROVINCE_AREA_RIGHT': + return '省级区域销售产生的权益收益'; + case 'PROVINCE_TEAM_RIGHT': + return '省级团队业绩产生的权益收益'; + case 'CITY_AREA_RIGHT': + return '市级区域销售产生的权益收益'; + case 'CITY_TEAM_RIGHT': + return '市级团队业绩产生的权益收益'; + case 'COMMUNITY_RIGHT': + return '社区运营产生的权益收益'; + default: + return '权益分配收益'; + } + } + + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 拖拽指示器 + Container( + margin: const EdgeInsets.only(top: 12), + width: 40, + height: 4, + decoration: BoxDecoration( + color: const Color(0xFFE0E0E0), + borderRadius: BorderRadius.circular(2), + ), + ), + // 标题区域 + Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: isIncome + ? const Color(0x1A4CAF50) + : const Color(0x1AE53935), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + getRewardIcon(), + size: 24, + color: isIncome ? const Color(0xFF4CAF50) : const Color(0xFFE53935), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + entry.displayName, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(height: 4), + Text( + getRewardDescription(), + style: const TextStyle( + fontSize: 12, + color: Color(0x995D4037), + ), + ), + ], + ), + ), + ], + ), + ), + // 分隔线 + Container( + height: 1, + color: const Color(0x1A8B5A2B), + ), + // 详情列表 + Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + _buildDetailRow( + '收益金额', + '${isIncome ? '+' : ''}${_formatAmount(entry.amount)} 绿积分', + color: isIncome ? const Color(0xFF4CAF50) : const Color(0xFFE53935), + ), + _buildDetailRow('权益类型', entry.displayName), + _buildDetailRow('流水类型', entry.entryTypeName), + if (entry.balanceAfter != null) + _buildDetailRow('入账后余额', '${_formatAmount(entry.balanceAfter!)} 绿积分'), + _buildDetailRow('入账时间', _formatDateTime(entry.createdAt)), + if (entry.refOrderId != null && entry.refOrderId!.isNotEmpty) + _buildDetailRow('关联订单', entry.refOrderId!), + ], + ), + ), + // 底部关闭按钮 + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD4AF37), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text('关闭'), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildDetailRow(String label, String value, {Color? color}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 14, + color: Color(0x995D4037), + ), + ), + Flexible( + child: Text( + value, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: color ?? const Color(0xFF5D4037), + ), + textAlign: TextAlign.right, + ), + ), + ], + ), + ); + } + + String _formatAmount(double amount) { + final formatter = NumberFormat('#,##0.00', 'zh_CN'); + return formatter.format(amount); + } + + String _formatDateTime(DateTime date) { + return DateTimeUtils.formatDateTime(date); + } +} diff --git a/frontend/mobile-app/lib/features/trading/presentation/pages/trading_page.dart b/frontend/mobile-app/lib/features/trading/presentation/pages/trading_page.dart index 2a7fdeae..d89c1488 100644 --- a/frontend/mobile-app/lib/features/trading/presentation/pages/trading_page.dart +++ b/frontend/mobile-app/lib/features/trading/presentation/pages/trading_page.dart @@ -436,7 +436,7 @@ class _TradingPageState extends ConsumerState { // const SizedBox(width: 8), // _buildCurrencyChip(SettlementCurrency.og, 'OG'), // const SizedBox(width: 8), - _buildCurrencyChip(SettlementCurrency.usdt, 'RMB/CNY提现'), + _buildCurrencyChip(SettlementCurrency.usdt, '提现'), // const SizedBox(width: 8), // _buildCurrencyChip(SettlementCurrency.dst, 'DST'), ],