hailin
fda79304c6
Revert "fix(ledger): 分享收益筛选支持多类型(REWARD_TO_SETTLEABLE + REWARD_PENDING)"
...
This reverts commit d223671db7 .
2026-03-01 11:07:19 -08:00
hailin
d223671db7
fix(ledger): 分享收益筛选支持多类型(REWARD_TO_SETTLEABLE + REWARD_PENDING)
...
后端 entryType 筛选支持逗号分隔多值,前端"分享收益"同时查询两种类型,
解决未认种时收到的 REWARD_PENDING 分享权益在筛选中丢失的问题。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:02:08 -08:00
hailin
e4a2a0e37a
feat(ledger): 流水明细显示来源用户ID + 统计兼容历史批量转换数据
...
- wallet_service.dart: LedgerEntry 新增 sourceAccountFromMemo 从 memo 提取来源用户
- ledger_detail_page.dart: 流水列表项显示"来自 Dxxx"金色文字
- ledger_detail_page.dart: 权益详情弹窗添加来源用户行和备注行
- wallet-application.service.ts: 统计/趋势保留 memo 兼容历史批量转换记录
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 10:41:11 -08:00
hailin
d876dd1591
fix(wallet): 区分直接入账和批量转换的 REWARD_TO_SETTLEABLE
...
新增 REWARD_PENDING_CONVERTED 类型用于批量转换(待领取→可结算),
REWARD_TO_SETTLEABLE 保留给直接入账(hasPlanted=true时的新收入)。
统计排除:REWARD_PENDING_CONVERTED + REWARD_SETTLED(状态转换)
统计计入:REWARD_PENDING + REWARD_TO_SETTLEABLE(首次入账)
已迁移7条历史数据的 entry_type。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 10:30:47 -08:00
hailin
31e6f9e15a
fix(mobile): 预种可结算列表以wallet实际金额为准,避免已结算条目残留
...
planting-service 的分配记录不跟踪 wallet 端的结算状态,
原来的反向排除法(排除PENDING+EXPIRED)无法处理:
1. 已结算到余额(SETTLED→余额)的条目
2. hasPlanted=true 后直接进可结算、不经 pending_rewards 的条目
改为以 walletInfo.rewards.settleableUsdt 为权威来源:
- settleableUsdt=0 时直接跳过(已全部结算到余额)
- settleableUsdt>0 时按金额截断,确保展示总额不超过实际可结算
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 10:19:31 -08:00
hailin
8e52535dd9
feat(mobile): 预种明细显示来源用户ID + 修复空待领取倒计时
...
1. 预种待领取/可结算明细的 memo 中追加来源用户(如"来自D26022600016的预种")
2. 修复 pendingUsdt=0 时倒计时仍然显示的问题(pending_expire_at 未清除时兜底)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 09:48:34 -08:00
hailin
b13d873f64
feat(mobile): 流水明细中 REWARD_PENDING 标注"已转可结算"
...
用户购买预种后 hasPlanted=true,所有 PENDING 奖励转为 SETTLED,
此时流水中历史的 REWARD_PENDING 条目追加"(已转可结算)"标注,
避免用户误以为还有未领取的奖励。仅在 pendingUsdt=0 时显示。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 09:15:18 -08:00
hailin
e9b9896317
fix(mobile): 修复预种可结算列表与金额不一致
...
问题:pre-planting/my-rewards 返回所有分配记录不区分状态,
导致 PENDING 状态的预种奖励也被错误地显示在可结算列表中,
而可结算金额(从 wallet-service 取值)正确为 0,造成列表和金额不一致。
修复:在合并预种可结算列表时,排除正在 PENDING 和 EXPIRED 状态的条目。
通过 wallet/pending-rewards 和 wallet/expired-rewards 获取实际状态,
用 sourceOrderId 交叉比对,只保留真正可结算的预种条目。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 08:24:03 -08:00
hailin
4996c1d110
feat(mobile): profile页待领取/可结算/已过期列表统一显示预种数据
...
变更概要:
- wallet_service.dart: 新增 WalletPendingRewardItem 模型和 getWalletPendingRewards() 方法
调用 GET /wallet/pending-rewards 获取 wallet-service 的待领取奖励列表
- profile_page.dart: 合并预种待领取奖励到列表中
从 wallet-service 待领取列表中筛选 PPL 前缀的预种条目,转换为 PendingRewardItem
与 reward-service 的正常认种待领取统一展示
- profile_page.dart: 已过期列表标记预种条目
wallet-service GET /wallet/expired-rewards 已包含预种过期记录,
渲染时通过 sourceOrderId.startsWith('PPL') 动态添加 [预种] 前缀
- profile_page.dart: 所有汇总金额统一从 wallet-service 取值
_pendingUsdt / _expiredUsdt / _remainingSeconds 改为从 walletInfo.rewards 读取,
wallet_accounts 包含正常认种 + 预种,是唯一的 source of truth
技术说明:
- 后端零改动,仅前端变更(零风险)
- 预种条目通过订单号 PPL 前缀与正常认种区分,避免重复显示
- 所有预种条目在卡片上显示 [预种] 前缀,方便用户区分来源
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 07:52:02 -08:00
hailin
27cd72fe01
feat(pre-planting): 预种可结算收益结算 + 前端可结算金额修正
...
背景:
预种奖励通过 planting-service → wallet-service allocateFunds 链路
直接写入 wallet_accounts.settleable_usdt,不经过 reward-service。
因此 reward-service 的一键结算(settleToBalance)无法覆盖预种部分,
且 reward-service 的 summary.settleableUsdt 不包含预种金额。
改动:
1. wallet-service 新增 POST /wallet/settle-pre-planting 端点
- 将 wallet 中剩余的 settleable_usdt 转入 available 余额
- settleable_usdt=0 时幂等跳过,不创建空流水
- 流水备注标注 [预种],payloadJson.source='pre-planting'
2. mobile-app 兑换页(trading_page):
- 可结算金额改为从 wallet-service 的 wallet.rewards.settleableUsdt 取值
(包含正常认种 + 预种的可结算部分,是唯一的 source of truth)
- 一键结算流程改为两步串行:
先调 reward-service settleToBalance(正常认种,不动现有逻辑),
再调 wallet-service settle-pre-planting(预种部分,纯增量)
3. mobile-app 我的页(profile_page):
- 并行加载新增 walletService.getMyWallet() 调用
- _settleableUsdt 改为从 wallet.rewards.settleableUsdt 取值
不涉及的系统:
- reward-service:零改动
- planting-service:零改动
- wallet-service 现有结算逻辑:零改动
- admin-web:零改动
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 07:33:53 -08:00
hailin
05e590ef04
fix(pre-planting): 修复可结算收益重复计算
...
pre-planting getMyRewards API 错误地将所有分配记录金额算作
settleableUsdt(包括 PENDING 状态的待领取奖励)。
预种奖励的 PENDING/SETTLEABLE 状态由 wallet-service 管理,
reward-service 的 getMyRewardSummary 已包含预种可结算部分,
不应重复累加。
修复:
- 后端 getMyRewards 返回 settleableUsdt: 0
- 前端"我"页面和"兑换"页面不再额外加预种 settleableUsdt
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 03:34:22 -08:00
hailin
d1be7173be
fix(admin-web): 引荐树节点点击导航到用户详情页
...
setTreeRootUser 只更新树不更新页面顶部信息,导致数据不同步。
改为 router.push 导航,页面整体刷新保持一致。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 03:13:42 -08:00
hailin
cf07712a8c
fix(mobile): 兑换页可结算收益聚合正常认种+预种金额
...
之前只显示正常认种的 settleableUsdt,未包含预种收益,
导致有预种收益但显示"暂无可结算收益"。
现在并行获取两者并累加,与"我"页面保持一致。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 03:11:13 -08:00
hailin
5392c47e47
fix(admin-web): 引荐关系树节点点击切换为该节点为根显示
...
点击节点后以该节点为根重新加载树,显示其祖先链和一级下线。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 03:00:45 -08:00
hailin
e715fd2504
fix(admin-web): 引荐关系树节点点击跳转到用户详情页
...
之前点击只切换树根节点,不导航。改为 router.push 跳转。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 02:55:56 -08:00
hailin
292c6518ba
fix(admin-web): 隐藏钱包流水表格的类型和关联订单列
...
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 02:49:59 -08:00
hailin
0ac131a3b7
fix(admin-web): 钱包流水表格备注列显示不全
...
各列均为 flex:1 等宽导致备注内容被 ellipsis 截断,
调整各列 flex 比例并允许备注列换行显示完整文本。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 02:41:31 -08:00
hailin
3be7b47678
feat(pre-planting+mobile): 预种奖励在"我"页面展示
...
## 问题
预种奖励直接走 planting-service → wallet-service,绕过 reward-service,
导致前端"我"页面读 reward-service 的 summary/settleable 数据时看不到预种奖励。
## 方案
前端同时读 reward-service(正常认种)和 planting-service(预种),合并展示。
## 后端(planting-service)
- PrePlantingRewardEntryRepository: 新增 findByRecipientAccountSequence() 方法,
按收款方账户查询预种奖励记录(注入 PrismaService 替代事务 client)
- PrePlantingController: 新增 GET /pre-planting/my-rewards 端点,
返回当前用户作为收款方收到的预种奖励汇总+明细列表
格式与 reward-service 的 settleable 对齐(id, rightType, usdtAmount, sourceOrderNo 等)
## 前端(Flutter mobile-app)
- PrePlantingService: 新增 getMyRewards() 方法 + PrePlantingMyRewards/PrePlantingRewardItem 数据类
- profile_page.dart: 并行调用 prePlantingService.getMyRewards(),
将预种奖励转为 SettleableRewardItem 合并到可结算列表,
summary.settleableUsdt 也加上预种金额
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:03:53 -08:00
hailin
6a659ca718
feat(admin-web): 省/市区域账户添加点击查看明细功能
...
RegionAccountsSection 新增:
- 每行添加"查看明细"按钮,点击展开该账户分类账流水
- 明细表包含 时间/类型/金额/余额/来源账户/来源备注/备注 7列
- 复用 getAllLedger API 的 provinceAccountsLedger/cityAccountsLedger 数据
- 行点击和按钮点击均可展开/收起
- 新增 clickableRow/selectedRow CSS 样式
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 20:37:12 -08:00
hailin
bf50810830
feat(wallet+admin-web): 系统账户流水增加来源用户账户和来源备注列
...
问题:系统账户(S0000000001等)、省/市区域/团队账户的流水明细
只显示 allocationType 英文标识,无法追溯是哪个用户的认种产生的。
解决方案:从 wallet_ledger_entries.payload_json.metadata 中提取
sourceAccountSequence 和 memo 字段,通过 API 返回给前端展示。
后端 wallet-service 改动:
- LedgerEntryDTO 新增 sourceAccountSequence / sourceMemo 两个可选字段
- 新增 extractPayloadInfo() 辅助函数统一从 payloadJson 提取信息
- 替换所有 5 处 LedgerEntryDTO 映射,使用 extractPayloadInfo()
- 向后兼容:旧记录无 metadata 时返回 null,不影响已有功能
前端 admin-web 改动:
- LedgerEntryDTO 类型新增 sourceAccountSequence / sourceMemo 字段
- 固定账户明细表格和分类账明细表格增加"来源账户"和"来源备注"列
- 新增 .sourceAccount 样式(等宽字体显示账户序列号)
数据来源说明:
- 正常认种:reward-service 传入 metadata 含完整中文 memo 和 sourceAccountSequence
- 预种:planting-service 传入 metadata 含 sourceAccountSequence 和中文 memo
- 历史记录(2026-01-04前):metadata 可能为空,显示为"-"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 20:22:40 -08:00
hailin
05aacc0d5b
fix(admin-web): 系统账户流水备注列显示完整内容
...
移除 .memo 的 max-width/overflow/ellipsis/nowrap 截断样式,
改为 word-break: break-word 自动换行,确保备注全文可见。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 19:55:45 -08:00
hailin
62bbbca609
Revert "fix(ledger): REWARD_EXPIRED条目显示权益类型+已过期标签"
...
This reverts commit 4bd40970d0 .
2026-02-28 12:04:07 -08:00
hailin
4bd40970d0
fix(ledger): REWARD_EXPIRED条目显示权益类型+已过期标签
...
- 奖励过期条目显示具体权益名(分享权益/省团队权益等)+ 红色"已过期"标签
- 图标改为灰色 timer_off,金额文字改为灰色,背景微灰
- 与正常权益收入条目有明显视觉区分
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 12:01:36 -08:00
hailin
724fb08be4
fix(contribution): 发布过期份额同步事件 + 管理后台/挖矿app状态显示
...
- contribution-service: swapContributionForMerge 作废旧份额记录后,
立即发布 ContributionRecordSynced outbox 事件(isExpired=true),
mining-admin-service 收到后 upsert syncedContributionRecord
- mining-admin-web: 算力记录状态列改为绿色"有效"/红色"无效"
- mining-app: 贡献值记录卡片始终显示有效/无效状态标签
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 11:35:55 -08:00
hailin
a904c8bd42
feat(ledger): 预种份额在流水明细中显示合并合同下载按钮
...
- 移除硬编码"预种无合同"逻辑
- PPL 份额点击详情时,查找是否有对应的已签署合并记录
- 有签署合同则显示查看/下载按钮,调用预种合并合同 PDF 接口
- 同时新增 _viewMergeContractPdf / _downloadMergeContractPdf 方法
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 11:19:27 -08:00
hailin
cd73b2dec4
fix(pre-planting): 签署合同前检查实名认证 + 修正合同金额
...
- getMergeContractPdf: KYC 为 null 时返回 400,不允许查看合同
- getMergeContractPdf: 从源订单汇总实际绿积分金额,CNY = 绿积分 × 1.1
- Flutter: KYC 错误时显示专用提示 + "去完成实名认证" 按钮
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 10:59:26 -08:00
hailin
b1e5e6b29f
feat(pre-planting): 合并合同走完整签署流程(PDF展示+手写签名)
...
- planting-service: 新增 GET /merges/:mergeNo/contract-pdf 接口,复用现有 PDF 模板
- planting-service: PrePlantingApplicationService 注入 PdfGeneratorService/IdentityServiceClient
- pre_planting_service.dart: 新增 downloadMergeContractPdf,signMergeContract 简化返回值
- 新建 PrePlantingMergeSigningPage:PDF展示→滚动到底→确认法律效力→手写签名→提交
- pending_contracts_page: 合并卡片点击跳签名页(prePlantingMergeSigning)
- pre_planting_merge_detail_page: 签署按钮跳签名页,移除直接调用逻辑
- 新增路由 /pre-planting/merge-signing/:mergeNo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 10:35:22 -08:00
hailin
2ad1936126
fix(pre-planting): 合并详情页 USDT 改为绿积分
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 10:17:12 -08:00
hailin
7bad0a8935
fix(pre-planting): 修复编译错误(getMerges→getMyMerges、RoutePaths 缺失导入、Future.wait 类型)
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 10:00:17 -08:00
hailin
b9b23c36d7
feat(pre-planting): 合并后走正常签合同流程,购买第5份直接跳合并详情页
...
- pre_planting_service: CreatePrePlantingOrderResponse 增加 merged/mergeNo 字段
- pre_planting_purchase_page: 购买成功若触发合并,直接跳转合并详情签合同
- contract_check_service: 注入 PrePlantingService,checkAll 增加预种待签合并检查
- pending_contracts_page: 同时展示普通合同和预种合并待签卡片,复用现有签合同弹窗流程
- injection_container: contractCheckServiceProvider 注入 prePlantingService
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 09:51:21 -08:00
hailin
26dcd1d2de
fix(pre-planting): 修复购买省市名称存储及多项购买失败问题
...
== 问题修复 ==
1. 购买失败:NestJS 返回数组 message 导致 Flutter 类型转换错误
- 症状:List<dynamic> is not a subtype of String
- 原因:ValidationPipe 校验失败时 message 字段为 List<String>(每条字段错误一条),
Flutter _handleDioError 直接用 data['message'] 作为 String 参数导致运行时崩溃
- 修复:api_client.dart 中对 rawMsg 判断是否 List,若是则 join(', ')
2. 续购省市为空导致 400 校验失败
- 症状:续购时后端返回 "provinceCode should not be empty"
- 原因:购买页面续购分支未传入省市,导致 provinceCode/cityCode 为 null
- 修复:pre_planting_purchase_page.dart 中续购时使用 _position?.provinceCode
3. 购买请求携带 provinceName/cityName 被后端 forbidNonWhitelisted 拒绝
- 症状:400 "property provinceName should not exist"
- 原因:前端发送名称字段,但 PurchasePrePlantingDto 未声明这些字段
- 修复:在 DTO 中添加 @IsOptional() 的 provinceName / cityName 字段
== 功能新增 ==
4. 预种持仓表新增省市名称存储(参照正式认种的处理方式)
- 迁移:20260228000000_add_province_city_name_to_position
- Prisma schema:PrePlantingPosition 新增 provinceName / cityName 可空字段
- 聚合根:addPortions() 接受可选 provinceName/cityName,首购时写入,续购忽略
- Repository:save/toDomain 同步处理名称字段
- Application Service:purchasePortion 透传名称,getPosition 返回名称
- Controller:purchase 端点透传 dto.provinceName / dto.cityName
5. 预种合并时算力精确回滚(contribution-service)
- 新增 9a-team 步骤:事务内查询即将作废的 TEAM_LEVEL/TEAM_BONUS 算力记录
- 新增 9c-team 步骤:按账户聚合后精确 decrement 上游推荐人的各档位 pending 和 effective
- 目的:确保旧份额算力精确回滚,避免新树算力 9d 叠加后造成双倍计入
== UI 优化 ==
- 购买页面将 "USDT" 改为 "绿积分"(单价、总价、成功提示)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 08:02:14 -08:00
hailin
eea38b2b86
fix(pre-planting): 购买页面和弹窗中 USDT 改为绿积分
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 07:20:51 -08:00
hailin
9b6effe63d
debug(pre-planting): 添加购买流程详细日志以排查 List cast 错误
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 07:19:35 -08:00
hailin
606d3c0b22
fix(pre-planting): 修复购买失败时 List<dynamic> 类型转换错误和续购省市缺失
...
1. api_client.dart: NestJS validation error 返回 message 为数组时,
用 join(', ') 转为字符串,避免直接传给 ApiException(String) 崩溃
2. pre_planting_purchase_page.dart: 续购时传 _position 中已保存的
provinceCode/cityCode,满足后端 DTO 必填校验
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 06:56:49 -08:00
hailin
2d7b02aa96
fix(pre-planting): 修复预种页面 5 个 UI 问题(纯前端,零后端改动)
...
=== 问题 1:流水明细合同按钮 500 错误 ===
文件: ledger_detail_page.dart
原因: 预种订单(PPL 前缀)无合同,但流水详情弹窗显示「查看合同/下载合同」按钮
修复: _showTransactionDetail 检测 refOrderId.startsWith('PPL'),
预种订单传 showContractButtons: false,弹窗不渲染合同按钮区
=== 问题 2:流水备注显示英文 ===
文件: ledger_detail_page.dart
原因: 备注字段存储的是 'Plant payment (from frozen)'(后端写入,不改后端)
修复: _TransactionDetailSheet 展示备注时,若订单号以 PPL 开头则显示「预种」
=== 问题 3:预种明细订单金额单位错误 ===
文件: pre_planting_position_page.dart
修复: '${order.totalAmount.toInt()} USDT' → '${order.totalAmount.toInt()} 绿积分'
=== 问题 4:省市显示数字代码(如 44 · 4401)===
文件: pre_planting_position_page.dart / pre_planting_purchase_page.dart
原因: provinceName/cityName 为 null 时回退显示 provinceCode/cityCode
修复:
- position 页:条件改为 provinceName != null && cityName != null,无中文名则不显示省市行
- purchase 页:加载时不再 fallback 到代码;锁定显示无名称时显示「已锁定」;
购买确认弹窗省市行无名称时显示「-」
=== 问题 5:「合并进度」改为「合成进度」===
文件: pre_planting_position_page.dart / pre_planting_purchase_page.dart
修复: 两处 Text('合并进度') → Text('合成进度')
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 06:25:38 -08:00
hailin
97f8b7339f
fix(auth): LOGIN 能力禁用后强制下线已登录用户
...
## 问题
管理员在后台禁用用户的 LOGIN 能力后,该用户仍然可以正常使用 mining-app。
原因是 LOGIN 检查只在登录/刷新 token 时执行,已持有有效 JWT(7天有效期)
的用户不会被影响,直到 token 过期才会被拦截。
## 修复
### 后端 - JwtAuthGuard (auth-service)
- 在 JWT 验证通过后,增加 LOGIN 能力实时检查
- 调用 CapabilityService.isCapabilityEnabled() 查询 Redis 缓存
- LOGIN 被禁用时返回 403 ForbiddenException("您的账户已被限制登录")
- 采用 fail-open 策略:Redis/DB 查询失败时放行,不影响正常用户
- 每次认证请求多一次 Redis GET(<1ms),对当前用户规模无性能影响
### 前端 - mining-app API Client
- 新增 onLoginDisabled 全局回调(类似现有的 onUnauthorized)
- Dio 拦截器检测 403 响应中包含"限制登录"关键词时触发回调
- 回调执行:清除用户状态 + 跳转登录页(与 401 处理一致)
## 影响范围
- 所有使用 @UseGuards(JwtAuthGuard) 的端点都会实时检查 LOGIN 能力
- 管理员禁用 LOGIN 后,用户下一次 API 请求即被拦截并强制下线
- 不影响公开端点(登录、注册等不经过 JwtAuthGuard 的接口)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 05:01:37 -08:00
hailin
fe9a30df85
feat(mining-app): 接入 capabilitiesProvider 实现 UI 层能力适配
...
Phase 5 补充:在 mining-app Flutter 前端关键页面接入 capability 检查,
被禁用的功能按钮置灰并显示 SnackBar 提示,后端 403 拦截作为兜底。
修改的页面及对应能力:
- send_shares_page: P2P_SEND → "确认发送"按钮
- receive_shares_page: P2P_RECEIVE → 顶部限制提示横幅
- c2c_market_page: C2C → "发布"按钮 + "接单"弹窗入口
- c2c_publish_page: C2C → "发布买入/卖出"按钮
- trading_page: TRADING → "确认交易"按钮
- edit_profile_page: PROFILE_EDIT → "保存"操作
- team_page: VIEW_TEAM → 页面数据加载前检查
设计原则:
- 不阻断页面浏览,只阻断操作
- fail-open: 能力获取失败时默认全部开启
- 禁用时点击按钮弹出 SnackBar 提示"您的XX功能已被限制"
- 沿用现有 disabled 按钮样式(.withOpacity(0.4))
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 01:36:24 -08:00
hailin
55cfc96464
feat(capability): 实现用户能力权限控制系统(Capability-based Permission)
...
借鉴 Stripe Capability 模型,实现 13 项细粒度用户功能权限控制:
LOGIN, TRADING, C2C, TRANSFER_IN/OUT, P2P_SEND/RECEIVE,
MINING_CLAIM, KYC, PROFILE_EDIT, VIEW_ASSET/TEAM/RECORDS
## 架构设计
- auth-service 为能力数据唯一写入点(DB + Redis DB14 缓存)
- 下游服务通过独立 ioredis 客户端直连 Redis DB14 检查能力(~1ms)
- 默认全部开启(fail-open):无缓存/Redis 故障 = 允许通行
- Guard 执行顺序:JwtAuthGuard → CapabilityGuard
## Phase 1: auth-service 核心
- Prisma Schema: UserCapability + CapabilityLog 两张表
- Domain: Capability 枚举, CapabilityMap 类型, Repository 接口
- Infrastructure: PrismaCapabilityRepository(含 $transaction 原子操作)
- Application: CapabilityService(Redis 缓存优先 → DB fallback → 写回 Redis TTL 1h)
- Scheduler: 每 60 秒扫描到期限制自动恢复(Redis 分布式锁防重复)
- API: GET /auth/user/capabilities (JWT), Internal CRUD API (服务间)
- 登录/refreshToken 均增加 LOGIN 能力检查
## Phase 2: 下游 CapabilityGuard
- trading-service: 14 个端点标注(TRADING/C2C/TRANSFER/P2P_SEND/VIEW_ASSET)
- contribution-service: 3 个端点标注(VIEW_RECORDS/VIEW_TEAM)
- mining-service: Guard 注册 + JwtAuthGuard accountSequence 兼容修复
- auth-service: KYC 端点标注(controller 级别 UseGuards)
## Phase 3: mining-admin-service
- CapabilityAdminService: 代理 auth-service internal API + 本地 AuditLog
- CapabilityController: Admin CRUD + 批量设置 + 变更日志查询
## Phase 4: mining-admin-web
- capability-management.tsx: 分组 Switch 开关 + 禁用 Dialog(原因+到期时间)+ 变更日志分页
- React Query hooks: useCapabilities/useSetCapability/useCapabilityLogs
- 用户详情页新增"权限管理"Tab
## Phase 5: mining-app (Flutter)
- CapabilityMap 数据模型 + ForbiddenException 异常类
- api_client.dart: 403 响应适配 ExceptionFilter 包装格式
- capabilitiesProvider: 登录后获取能力列表,fail-open 降级
## 审计修复
- CRITICAL: users.api.ts capability 方法移入 usersApi 对象内部
- P0: Flutter 403 解析路径适配 {error:{code,message}} 实际格式
- P0: 批量接口 operatorId 提升到 body 顶层匹配 auth-service 契约
- P1: mining-service JwtAuthGuard accountSequence fallback payload.sub
- P1: refreshCache 加 try/catch 防止 Redis 故障导致 500
- P1: processExpiredRestrictions 改用 upsertWithLog 事务方法
- P1: C2C upload-proof 补加 @RequireCapability('C2C')
- HIGH: internal.controller.ts 新增 capability 枚举校验
- HIGH: admin capability.controller.ts adminId fallback + query params 类型修复
- MEDIUM: setCapability 改用 $transaction 保证 upsert+log 原子性
## 部署注意
- 需运行: cd auth-service && npx prisma migrate dev --name add_user_capabilities
- 需配置: mining-admin-service .env AUTH_SERVICE_URL=http://auth-service:3010
## 待后续处理(P2)
- P2P_RECEIVE 需在业务逻辑层检查(收款方无主动请求)
- MINING_CLAIM/PROFILE_EDIT 待对应端点实现后标注
- getCapabilities 返回 Map 转 Array 丢失 reason/expiresAt 详细字段
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 22:19:56 -08:00
hailin
1d1c60e2a2
feat(notification): 新增强制阅读弹窗功能(管理员可配置 requiresForceRead)
...
## 功能概述
在不影响任何现有业务的前提下,新增"强制阅读弹窗"功能:
- 管理员创建通知时可勾选「需要强制弹窗阅读」
- App 冷启动进入主页 或 从后台切回前台时自动触发检查
- 存在未读且标记 requiresForceRead=true 的通知时,依次逐条弹窗
- 用户无法通过点击背景或返回键关闭弹窗(强制阅读)
- 最后一条通知弹窗底部显示 checkbox「我已经阅读并知晓」
- 未勾选时"确定"按钮置灰禁用
- 勾选后"确定"变为金色可点击,点击后所有弹窗消失
- 全部看完后仅对已展示的强制阅读通知按 ID 逐一标记已读
(不影响普通未读通知的 badge 计数)
## 涉及改动
### 后端 admin-service
- `prisma/schema.prisma`
- Notification 模型新增字段 `requiresForceRead Boolean @default(false)`
- `prisma/migrations/20260227100000_add_requires_force_read_to_notifications/migration.sql`
- 手动创建 SQL migration(本地无 DATABASE_URL 环境)
- 部署时需在服务器执行 `npx prisma migrate deploy`
- `src/domain/entities/notification.entity.ts`
- 实体类构造器新增 `requiresForceRead`
- create() / update() 方法均支持该字段,默认值 false
- `src/infrastructure/persistence/mappers/notification.mapper.ts`
- toDomain() 从 Prisma 记录读取 requiresForceRead
- toPersistence() 写入 requiresForceRead
- `src/api/dto/request/notification.dto.ts`
- CreateNotificationDto / UpdateNotificationDto 各新增可选字段 requiresForceRead
- `src/api/dto/response/notification.dto.ts`
- NotificationResponseDto(管理端)新增 requiresForceRead
- UserNotificationResponseDto(移动端)新增 requiresForceRead
- `src/api/controllers/notification.controller.ts`
- create() / update() 透传 requiresForceRead 到 entity
### 前端 admin-web
- `src/services/notificationService.ts`
- NotificationItem / CreateNotificationRequest / UpdateNotificationRequest 新增 requiresForceRead
- `src/app/(dashboard)/notifications/page.tsx`
- 通知列表:requiresForceRead=true 时显示红色「强制阅读」标签
- 创建/编辑表单:新增 checkbox「需要强制弹窗阅读」及说明文字
- form state / submit payload / edit 初始化均包含 requiresForceRead
### 移动端 mobile-app
- `lib/core/services/notification_service.dart`
- NotificationItem 新增字段 requiresForceRead(默认 false,fromJson 安全读取)
- `lib/features/notification/presentation/pages/notification_inbox_page.dart`
- markAsRead / markAllAsRead 重建 NotificationItem 时保留 requiresForceRead
- `lib/features/notification/presentation/widgets/force_read_notification_dialog.dart`(新建)
- 单条强制阅读弹窗组件
- 顶部显示通知类型图标 + 进度「1/3」
- 可滚动内容区展示完整通知
- 非最后条:「下一条 ▶」按钮(始终可点)
- 最后一条:checkbox + 「确定」(勾选后才可点)
- barrierDismissible: false + PopScope(canPop: false),无法逃出
- `lib/features/home/presentation/pages/home_shell_page.dart`
- 新增状态:_isShowingForceReadDialog(实例,防重入)
_lastForceReadDialogShownAt(静态,60秒冷却)
- 新增方法 _checkAndShowForceReadDialog():
Guard 1: 防重入锁
Guard 2: 60秒冷却(防回前台闪弹)
Guard 3: 检查用户已登录
Guard 4: 检查无其他弹窗在显示
弹窗期间同时设置 _isShowingDialog=true,阻止后台合同/KYC检查并发
全部看完后仅标记 forceReadList 中的通知为已读,再 refresh() 刷新 badge
- initState addPostFrameCallback 中新增调用
- didChangeAppLifecycleState resumed 分支中新增调用
- resetContractCheckState() 中重置 _lastForceReadDialogShownAt(账号切换隔离)
## 安全与兼容性
- API 调用失败时静默返回,不阻断用户进入 App
- 仅对 requiresForceRead=true 的通知弹窗,普通通知完全不受影响
- 与现有合同弹窗、KYC弹窗、维护弹窗、更新弹窗无冲突
- 静态冷却变量在账号切换时重置,避免新账号被旧账号冷却影响
- badge 准确:仅标记已展示的强制通知,不动其他未读通知计数
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 19:33:51 -08:00
hailin
ef68b7b9c0
fix(pre-planting): 预种开关关闭时隐藏"我的"页面预种按钮
...
- profile_page: 加载预种配置,根据 isActive 控制预种按钮显隐
- 开关关闭时不显示「预种购买」「预种明细」按钮
- 默认不显示(_isPrePlantingActive = false),加载成功后更新
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 23:11:53 -08:00
hailin
e328c75fc1
fix(pre-planting): 修复前后端 API 路径不匹配导致预种页面打不开
...
后端 PrePlantingController 缺少 eligibility 端点,前端请求 404 导致
Future.wait 整体失败,页面显示"加载数据失败"。
修复:
1. 后端: 在 PrePlantingController 添加 GET eligibility 端点
2. 前端: createOrder 路径从 /orders 改为 /purchase(匹配后端)
3. 前端: signMergeContract 路径从 /merges/:no/sign 改为 /sign-contract
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 23:07:47 -08:00
hailin
c0ac63d40a
feat(pre-planting): 重命名预种持仓→预种明细 + 购买协议弹窗
...
- mobile-app: "预种持仓"按钮和页面标题改为"预种明细"
- admin-service: 新增预种协议文本 API (GET/PUT agreement),存储于 system_configs 表
- admin-service: 公开 config API 响应增加 agreementText 字段
- planting-service: 新建 PrePlantingPublicController (无需 JWT),暴露 GET /pre-planting/config
- admin-web: 预种管理页面新增协议文本编辑器(textarea + 保存按钮)
- mobile-app: 购买流程增加协议弹窗,用户需勾选同意后才能继续
- mobile-app: 协议文本优先使用后台配置,未配置时使用默认文本
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:14:24 -08:00
hailin
8bafb0a8d4
fix(mobile-app): 增加切换账号全局防护,彻底解决切换期间自动退出登录
...
根因:切换账号时 saveCurrentAccountData() 耗时 ~7 秒,期间定时器仍在发 API 请求,
clear 阶段 token 被删除后 in-flight 请求收到 401 → 触发 tokenExpired →
logoutCurrentAccount() 把刚恢复的新账号数据全部擦除。
修复(两层防护):
1. 全局锁 isSwitchingAccount:MultiAccountService 在 switchToAccount 整个过程中
设为 true,app.dart _handleTokenExpired 检测到该标志直接 return,不执行 logout
2. 定时器提前停止:将定时器停止从 onBeforeRestore(save 之后)移到 switchToAccount
调用之前,确保 save 期间无新 API 请求
3. try/finally 保证标志位必定清除,异常情况不会锁死后续 tokenExpired 事件
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:41:45 -08:00
hailin
a5cc3fdc5b
fix(mobile-app): 修复切换账号时 token 过期导致自动退出登录
...
根因:switchToAccount() 流程中,定时器在 _clearCurrentAccountData()
之后才停止。clear 阶段会删除 token,但此时定时器仍在运行,
in-flight 的 API 请求收到 401 → 触发 _handleTokenExpired()
→ 调用 logoutCurrentAccount() 把正在恢复的新账号数据全部清掉
→ 用户被自动踢到登录页面。
修复:将 onBeforeRestore 回调(停止定时器)移到 _clearCurrentAccountData()
之前执行,确保所有 API 请求停止后再清除 token。
修改前: save → clear(删token) → 停定时器 → restore
修改后: save → 停定时器 → clear(删token) → restore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:21:57 -08:00
hailin
cfc03fe523
fix(pricing): 手动调价支持负数降价对冲
...
之前手动调价只允许非负整数,无法用负数对冲降价。
前端 (admin-web settings/page.tsx):
- 移除 input min="0" 限制,允许输入负数
- 验证改为:只校验 isNaN 和总价不低于 0(15831 + amount >= 0)
- 文案:"加价金额" → "调价金额",placeholder 改为"正数涨价,负数降价"
- 实时预览条件从 amount >= 0 改为总价 >= 0
- 提示文案更新为"正数涨价,负数降价对冲"
后端 (admin-service tree-pricing.service.ts):
- 移除 newSupplement < 0 的硬性拒绝
- 改为校验 BASE_PRICE(15831) + newSupplement >= 0,防止总价为负
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 11:58:11 -08:00
hailin
12004d1c2e
fix(mobile-app): 修复账号切换后自动退出登录的问题
...
根因:ref.invalidate(authProvider) 销毁旧 AuthNotifier 后,新实例的构造函数
仅设置 AuthState(status: AuthStatus.initial),从不自动调用 checkAuthStatus()
从 SecureStorage 重新加载认证数据。导致 auth 状态停留在 initial(未认证),
依赖 auth 状态的组件误判为"未登录",触发页面跳转到登录页。
修复:
- account_switch_page: invalidate 后立即调用 loadAuthState() 从 storage
读取新账号数据,确保 auth 状态为 authenticated 后再导航
- account_switch_page: 切换后重置 ApiClient 的 tokenExpired 标记,防止
旧会话的 401 状态阻塞新账号的请求
- app.dart: _handleTokenExpired() 增加醒目日志和调用栈打印,便于排查
切换期间是否有 token 过期事件被误触发
切换流程更新为 6 步:
[1/6] switchToAccount() - 保存旧账号、清空、恢复新账号 storage
[2/6] onBeforeRestore - 停止所有定时器
[3/6] invalidate Provider - 销毁旧 Provider 实例
[4/6] loadAuthState() - 从 storage 加载新账号 auth 状态 ← 新增关键步骤
[5/6] 恢复遥测上传
[6/6] 导航到 ranking 页面
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 11:39:31 -08:00
hailin
3a307b5db7
fix(planting): 修复认种页面动态定价不生效 + 添加涨价倒计时
...
- 修复 admin-service PublicTreePricingController 路由双重前缀问题
(@Controller('api/v1/tree-pricing') → @Controller('tree-pricing'))
- Kong 网关新增 /api/v1/tree-pricing 路由到 admin-service
- mobile-app 认种页面添加涨价倒计时功能:
显示"距下次涨价还有 X天 X小时 X分钟"及涨价后价格
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 10:22:27 -08:00
hailin
19f350b8e3
fix(mining-app): 全网兑换销毁量改为保留8位小数
...
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 09:54:53 -08:00
hailin
c293309bdf
fix(mining-app): 全网兑换销毁量保留4位小数
...
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 09:53:47 -08:00
hailin
a39af93063
fix(mining-app): 全网兑换销毁量改用circulationPool+保留小数格式
...
根据业务需求,"全网兑换销毁量"应显示流通池总数量(全网卖出量),
而非SELL_BURN销毁总量。同时将formatIntWithCommas改为formatWithCommas
以保留小数位,避免小数值被截断显示为0。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 09:53:14 -08:00