feat(mobile-app): 流水明细支持显示权益类型和详情

- 后端 wallet-service: getMyLedger API 返回 allocationType 字段
- 前端流水明细: 显示权益类型名称(分享权益、省/市区域权益等)
- 新增权益详情弹窗,点击权益记录可查看详细信息
- 兑换页面: "RMB/CNY提现" 改为 "提现"
- 我的团队: "暂无下级成员" 改为 "暂无团队成员"
- 自助申请授权: 隐藏团队链占用区域提示

🤖 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-26 03:57:35 -08:00
parent f7e2f7f6f2
commit 78d7e0e637
8 changed files with 273 additions and 31 deletions

View File

@ -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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\nEOF\n\\)\")"
],
"deny": [],
"ask": []

View File

@ -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;
}

View File

@ -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<string, unknown>)?.allocationType as string ?? null,
createdAt: entry.createdAt.toISOString(),
})),
total: result.total,

View File

@ -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;
}

View File

@ -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),

View File

@ -3402,7 +3402,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
child: Padding(
padding: EdgeInsets.all(20),
child: Text(
'暂无下级成员',
'暂无团队成员',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',

View File

@ -926,11 +926,13 @@ class _LedgerDetailPageState extends ConsumerState<LedgerDetailPage>
///
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<LedgerDetailPage>
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<LedgerDetailPage>
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<LedgerDetailPage>
);
}
///
void _showEntryDetail(LedgerEntry entry) {
if (entry.entryType == 'PLANT_PAYMENT' && entry.refOrderId != null) {
// -
_showTransactionDetail(entry);
} else if (entry.allocationType != null) {
// -
_showRewardDetail(entry);
}
}
///
Future<void> _showTransactionDetail(LedgerEntry entry) async {
if (entry.refOrderId == null) return;
@ -1066,6 +1067,16 @@ class _LedgerDetailPageState extends ConsumerState<LedgerDetailPage>
);
}
///
void _showRewardDetail(LedgerEntry entry) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => _RewardDetailSheet(entry: entry),
);
}
/// PDF
Future<void> _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);
}
}

View File

@ -436,7 +436,7 @@ class _TradingPageState extends ConsumerState<TradingPage> {
// 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'),
],