Commit Graph

1428 Commits

Author SHA1 Message Date
hailin 28cf0b7769 fix(wallet): settleUserPendingRewards 补创建 REWARD_TO_SETTLEABLE 流水
转换 PENDING→SETTLEABLE 时,为每笔奖励创建带来源信息的
REWARD_TO_SETTLEABLE 流水,解决"分享收益"筛选缺失问题。
统计排除逻辑同步更新,通过 convertedFromPending 标记避免双重计算。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:15:19 -08:00
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 eba125901c fix(wallet): 统计排除 REWARD_SETTLED 避免三重计入
奖励流水三阶段 REWARD_PENDING → REWARD_TO_SETTLEABLE → REWARD_SETTLED
是同一笔钱的状态转换,只在 REWARD_PENDING 阶段计入收入。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 10:07:22 -08:00
hailin b905e8cb23 fix(wallet): 统计概览排除 REWARD_TO_SETTLEABLE 避免重复计入
REWARD_PENDING(入账) 和 REWARD_TO_SETTLEABLE(状态转换) 是同一笔收入
的两个阶段,统计时只应计入 REWARD_PENDING,排除 REWARD_TO_SETTLEABLE。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 10:01:36 -08:00
hailin ecaaf68a27 fix(wallet): settle 时清除 pending_expire_at 避免残留倒计时
settleUserPendingRewards 全量结算后 pendingUsdt=0,
但 pendingExpireAt 未清除导致 API 仍返回过期时间。
与 expirePendingReward/claimRewards 行为保持一致。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 09:52:19 -08:00
hailin d849ca7bc2 fix(wallet): getPendingRewards 只返回 PENDING 状态记录
之前 getPendingRewards 没有传 status 过滤参数,返回了所有状态
(包括 SETTLED 和 EXPIRED)的记录,导致前端误将已结算的预种条目
显示在待领取列表中。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 09:07:31 -08:00
hailin 718e70e61a fix(pre-planting): 修复 settleAfterPrePlanting 响应解包(TransformInterceptor)
wallet-service 的 TransformInterceptor 会将响应包装为 { data: {...} },
需要从 response.data.data 中提取实际数据,与 allocatePrePlantingFunds 一致。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 08:57:45 -08:00
hailin 722c124cc9 feat(pre-planting): 预种购买后自动触发待领取→可结算转换
购买预种份额后,planting-service 调用 wallet-service 新增的内部 API,
将用户标记为 hasPlanted=true 并结算所有 PENDING 奖励为 SETTLED。
纯新增代码,不修改任何现有方法逻辑,两步均幂等,失败不阻塞购买流程。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 08:51:03 -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 1157760d4d fix(pre-planting): 补充社区/省团队/市团队 API 缺失的 treeCount 参数
三个接口调用时未传 treeCount,导致 authorization-service 收到
Number(undefined)=NaN,addMonthlyTrees(NaN) 使字段变为 NaN,
Prisma upsert 报 PrismaClientValidationError。

修复:全部传 treeCount=0(预种不计入月度考核),
省团队补充 provinceCode,市团队补充 cityCode,
同时修正社区/团队接口返回格式为 distributions 数组。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 02:30:35 -08:00
hailin e32658fc5e fix(pre-planting): 修复 authorization-service 响应包装格式解析
authorization-service 全局 TransformInterceptor 将响应包装为
{ success, data: T, timestamp },预种客户端需读取 response.data.data
而非 response.data。此前因解析失败静默 fallback 到系统账户。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 02:22:01 -08:00
hailin a15a4a97b1 fix(docker): 为 planting-service 添加 AUTHORIZATION_SERVICE_URL 环境变量
planting-service 缺少 AUTHORIZATION_SERVICE_URL 配置,
默认回退到 http://localhost:3006,在容器内无法访问 authorization-service,
导致所有授权分配请求失败走 fallback。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 02:15:23 -08:00
hailin d880242807 fix(pre-planting): 修复 authorization-service API 路径错误
预种 client 使用 /internal/authorization/... 但 authorization-service
全局前缀为 api/v1,实际路由是 /api/v1/authorization/...。
路径不对导致所有请求 404 → catch → fallback → 全部进系统账户。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 02:04:09 -08:00
hailin 530aeb2a6f feat(authorization): 新增行政区划代码↔中文名映射表,修复省市公司查询不到的问题
authorization_roles表的regionCode存储格式不一致(有中文名"北京"、"广东省"也有数字代码"110100"),
但查询时统一传入6位数字代码,导致findProvinceCompanyByRegion/findCityCompanyByRegion永远返回null。

新增 RegionCodeResolver 静态映射表(34个省+333个地级市),
将精确匹配 regionCode = provinceCode 改为多形式匹配 regionCode IN (所有可能形式)。
此修复同时影响正常认种和预种的省区域/市区域/省团队/市团队权益分配。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 01:41:48 -08:00
hailin b9ddda2532 fix(pre-planting): 修复省市代码格式不一致导致授权分配失败
预种DTO接收2位省代码(如"44")和4位市代码(如"4401"),
但authorization-service需要6位标准格式(如"440000"/"440100")。
- resolveAllocations中新增padEnd(6,'0')标准化转换
- fallback系统账户生成从padStart改为padEnd(右补零)
- 正常认种不受影响(SelectProvinceCityDto直接接收6位格式)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 01:23:55 -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 f32748c1d5 fix(pre-planting): 修复推荐链 API 调用的 URL 路径和返回格式解析
1. URL: /referrals/:id/chain → /referral/chain/:id(与正常认种对齐)
2. 返回格式: 正确解析 { ancestors: [{accountSequence, hasPlanted}] }
   之前错误期望 { directReferrer: {...} },导致有推荐人也被当成无推荐人

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 21:39:07 -08:00
hailin 545e897c1f fix(pre-planting): 预种省/市区域 API 传 treeCount=0,不计入考核
预种 1 份 = 1/5 棵树,如果将 portionCount 作为 treeCount 传给
authorization-service,会导致省/市公司月度考核进度被多算 5 倍。

修正:传 treeCount=0,预种阶段不累计考核棵数。
等 5 份合成 1 棵完整树后,由合成流程负责累计 1 棵的考核进度。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 20:57:27 -08:00
hailin 1c71cda2ec fix(pre-planting): 修复省/市区域权益分配的三个 bug
问题:
1. 省/市区域 API 调用缺少必需的 treeCount 参数,导致 authorization-service
   报错,每次都走 fallback 路径
2. fallback 路径没有对省/市代码做 padStart(6,'0') 补位,
   生成了错误的账户ID(如 944 而非 9440000,84401 而非 8440100)
3. API 返回格式解析错误:authorization-service 返回
   { distributions: [{accountSequence, ...}] },但预种客户端错误地期望
   { accountSequence: string },导致取到 undefined

修复:
- getProvinceAreaDistribution / getCityAreaDistribution 新增 portionCount 参数
- 正确解析 distributions 数组,优先取非系统账户(省/市公司)
- fallback 中使用 padStart(6,'0') 确保 7 位标准账户 ID 格式
- resolveAllocations 调用时传入 portionCount

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 20:53: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 90fad63fed fix(wallet): 优化流水memo避免正常认种来源信息重复
正常认种的 reward-service memo 已含"来自用户Dxxx的认种",
增加 hasSourceInfo 检查,包含"来自"时不再重复拼接来源账户。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 20:10:09 -08:00
hailin 299c82fc4f fix(wallet): 流水备注增加来源用户AccountSequence和订单号
所有分配类型(系统账户/区域账户/用户钱包/社区权益)的流水 memo
从原来的纯英文标识改为:中文描述 | 来源: 账户序列号 (订单号)
例: [预种] 预种成本费 | 来源: D26022600000 (PPLMM6670DO9VETGK)

修改方法:
- 新增 buildAllocationMemo() 统一从 metadata 中提取 memo/sourceAccountSequence
- 替换 allocateToSystemAccount/allocateToUserWallet/allocateCommunityRight/allocateToRegionAccount 中的 memo 生成
- 兼容无 metadata 的历史调用(回退到 allocationType 英文标识)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 20:08:52 -08:00
hailin 19fca05a81 fix(pre-planting): 预种权益分配metadata与正常认种对齐 + 删除retry-rewards
1. executeAllocations() metadata 修复:
   - 原:仅传 { source: 'PRE_PLANTING' },wallet流水缺失所有业务信息
   - 现:传完整 metadata(rightType, sourceOrderNo, sourceAccountSequence,
     treeCount, provinceCode, cityCode, memo),与正常认种 reward-service 一致
   - wallet-service 的 prePlantingPrefix() 通过 metadata.source 添加[预种]前缀

2. SHARE_RIGHT PENDING 机制说明(无代码变更):
   - 预种侧只确定收款人,全部标记 SETTLED 发给 wallet-service
   - wallet-service.allocateToUserWallet() 内部根据收款方 hasPlanted 判断:
     已种→SETTLEABLE / 未种→PENDING(24h过期归总部)
   - 与正常认种走同一套 wallet-service 代码

3. 删除无用的 retry-rewards 端点及其 WalletServiceClient 依赖

不涉及历史数据修改,不影响正常认种流程。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 19:27:16 -08:00
hailin d9c238702e feat(pre-planting): 预种权益流水备注添加[预种]前缀
wallet-service 新增 prePlantingPrefix 私有方法,
当 FundAllocationItem.metadata.source === 'PRE_PLANTING' 时,
在流水 memo 中添加 "[预种] " 前缀,使用户可区分预种与普通认种权益。

仅影响 pre-planting 新增的分配流水,不修改任何普通认种 memo。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 12:34:01 -08:00
hailin 1d0e4352df fix(pre-planting): 修复预种权益资金分配字段名错误(allocationType 丢失)
原 executeAllocations 使用了错误的 FundAllocationItem 字段名:
- targetAccountId → 应为 targetId
- 缺少 allocationType 字段
- targetType 错误地使用了 rightType 值而非 'USER'|'SYSTEM'

导致所有预种订单的 SHARE_RIGHT/COMMUNITY_RIGHT 等权益分配静默失败,
资金未能分配到推荐人/社区/省市账户,同时流水明细中也不显示预种记录。

修复内容:
1. WalletServiceClient 新增 allocatePrePlantingFunds 方法(使用正确格式)
2. executeAllocations 改用新方法,正确设置 targetType/targetId/allocationType
3. InternalPrePlantingController 新增 POST /admin/retry-rewards 历史数据修复端点

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 12:29:03 -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 1431c89684 fix(pre-planting): Decimal -> Number in totalAmount reduce 2026-02-28 11:01:37 -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 b17bf82443 feat(pre-planting): 新增 GET /merges/:mergeNo 合并详情接口
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 10:07:53 -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 20b8d41212 feat(pre-planting): 支持省市名称存储,参照正常认种处理方式
- PurchasePrePlantingDto 添加可选字段 provinceName/cityName,
  与 SelectProvinceCityDto 保持一致,解决 NestJS forbidNonWhitelisted 400 错误
- pre_planting_positions 表新增 province_name/city_name 列(迁移)
- PrePlantingPosition aggregate 增加 provinceName/cityName 字段
- addPortions() 接受并存储省市名称
- getPosition() 返回 provinceName/cityName 供续购时显示

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 07:48:47 -08:00
hailin 5aa17b05c5 fix(pre-planting): 代码审查修复 2 处小问题
1. handler: 删除冗余三元表达式(两边相同),改用 new Date(raw) 直接解析
2. service: swapContributionForMerge 增加源订单数量不足时的 warn 日志(不阻断执行)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 07:28:30 -08:00
hailin 4c6fd424b5 feat(pre-planting): 合成树后算力切换(预种 5 份合同签署触发)
当用户购买满5份预种后合成1棵树并签署合同时,自动执行算力切换:
1. 作废5份份额的算力记录(is_expired=true,remark 标注合成原因,已挖积分不受影响)
2. 从认种人账户扣减旧个人算力(保持账户余额准确)
3. 以1棵完整树的算力单价创建新算力记录(remark 标注来源订单)
4. 写入 pre_planting_synced_merges 幂等标记

== 实现方式 ==
- 触发节点:Debezium CDC on pre_planting_merges.mining_enabled_at(null → 非null)
- 新增 Debezium table:public.pre_planting_merges
- 新增 Kafka topic 订阅:cdc.pre-planting.public.pre_planting_merges
- 新增 handler:PrePlantingMergeSyncedHandler(解析 CDC 事件)
- 新增 service 方法:swapContributionForMerge(核心算力切换逻辑)
- 新增常量:PRE_PLANTING_MERGE_SOURCE_ID_OFFSET = 20B(区别于份额的 10B 偏移)
- 新增 DB 表:pre_planting_synced_merges(幂等标记,migration 已包含)

== 幂等保证 ==
- CDC 层:processedCdcEvent 表(sourceTopic + offset 唯一)
- 业务层:contribution_records WHERE sourceAdoptionId=20B+mergeId 存在性检查
- 标记层:pre_planting_synced_merges(best-effort,事务提交后写入)

== 对现有系统的影响 ==
- 零修改现有 contribution 调度器 / freeze scheduler
- 团队分润账户净效果≈0(旧5份=1棵树,切换后金额一致)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 07:22:09 -08:00
hailin f4c9535e12 feat(capability): 补齐全部后端 API 能力拦截
## 背景
审计发现 13 项用户能力中,部分后端 API 端点缺少 @RequireCapability
拦截,用户可绕过前端 UI 限制直接调用 API。本次逐服务补齐。

## Phase 1: 高优先级 — 操作端点

### auth-service
- POST /auth/password/change → @RequireCapability('PROFILE_EDIT')
  修改登录密码需要 PROFILE_EDIT 能力
- POST /auth/trade-password/set → @RequireCapability('PROFILE_EDIT')
  设置交易密码需要 PROFILE_EDIT 能力
- POST /auth/trade-password/change → @RequireCapability('PROFILE_EDIT')
  修改交易密码需要 PROFILE_EDIT 能力
- POST /auth/trade-password/verify → @RequireCapability('TRADING')
  验证交易密码是交易前置步骤,需要 TRADING 能力

### trading-service
- POST /c2c/orders/:orderNo/cancel → @RequireCapability('C2C')
  C2C 取消订单是唯一缺失 C2C 能力检查的操作端点

## Phase 2: 低优先级 — 查看端点

### trading-service
- GET /trading/orders → VIEW_RECORDS (用户订单列表)
- GET /trading/trades → VIEW_RECORDS (成交记录)
- GET /transfers/history → VIEW_RECORDS (划转历史)
- GET /p2p/transfers/:accountSequence → VIEW_RECORDS (P2P转账历史)
- GET /c2c/orders/my → VIEW_RECORDS (我的C2C订单)

### contribution-service
- GET /contribution/accounts/:accountSequence/active → VIEW_ASSET
- GET /contribution/accounts/:accountSequence/planting-ledger → VIEW_RECORDS

## 能力覆盖总览 (补齐后)
| 能力 | 端点数 | 状态 |
|------|--------|------|
| LOGIN | 全局 |  JwtAuthGuard 拦截 |
| TRADING | 3 |  createOrder, cancelOrder, verifyTradePassword |
| C2C | 6 |  create, take, cancel, confirmPayment, confirmReceived, uploadProof |
| TRANSFER_IN | 1 |  transferIn |
| TRANSFER_OUT | 1 |  transferOut |
| P2P_SEND | 1 |  transfer |
| KYC | 1 |  submitKyc |
| PROFILE_EDIT | 3 |  changePassword, setTradePassword, changeTradePassword |
| VIEW_ASSET | 2 |  getMyAsset, getActiveContribution |
| VIEW_TEAM | 2 |  getMyTeamInfo, getDirectReferrals |
| VIEW_RECORDS | 6 |  各服务历史记录端点 |
| P2P_RECEIVE | 0 | 仅前端展示控制(无后端操作端点) |
| MINING_CLAIM | 0 | mining-service 需后续重构(@Public 类级别) |

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 05:22:37 -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 a7f2008bc2 feat(pre-planting): 添加算力补偿调度器,修复 transfer_order_no schema 一致性
问题:CDC 后置回调失败(如迁移未就绪)后,pre_planting_synced_orders 记录
status=PAID 但 contributionDistributed=false,没有机制重新触发算力计算。

修复:
1. 新增 PrePlantingContributionScheduler(每 5 分钟):
   - 扫描未分配算力的 PAID 预种订单
   - 调用 processUndistributedOrders() 补偿分配
   - Redis 分布式锁防并发
2. 注册到 PrePlantingCdcModule 的 providers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 03:08:06 -08:00
hailin b747555927 fix(contribution-service): 补充缺失的 transfer_order_no 迁移文件
schema.prisma 中 ContributionRecord / SystemContributionRecord /
UnallocatedContribution 三个模型均新增了 transferOrderNo 字段,
但历史上只有 0001_init 一个迁移文件,导致生产数据库中缺少该列。

新增迁移 20260228000001_add_transfer_order_no:
- ALTER TABLE contribution_records ADD COLUMN transfer_order_no
- ALTER TABLE system_contribution_records ADD COLUMN transfer_order_no
- ALTER TABLE unallocated_contributions ADD COLUMN transfer_order_no
- 对应 3 个索引(与 schema @@index 一致)
- 使用 IF NOT EXISTS 保证幂等性

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 03:00:40 -08:00
hailin 390e5ccb19 fix(pre-planting): 用 orderNo 替代 BigInt 自增 ID 作为 CDC 关联键
问题:Debezium CDC 事件中 Prisma @map("order_id") 字段以 DB 列名
order_id 发送,而代码访问 data.id 导致 undefined → BigInt 转换失败。

修复方案(遵循"用 orderNo 业务键关联"原则):
- pre-planting-order-synced.handler.ts:
  * PrePlantingOrderSyncResult 改为 { orderNo: string }
  * handleCreateOrSnapshot/handleUpdate 均用 order_no 字段
  * syncToTrackingTable upsert where 改为 { orderNo }
  * ensureAdoptionMarker 入参从 orderId bigint 改为 orderNo string
    - markerAdoptionId = PRE_PLANTING_SOURCE_ID_OFFSET + hash(orderNo)
  * isAlreadyDistributed 改为 findUnique({ where: { orderNo } })
  * calculateAfterCommit 传 result.orderNo
- pre-planting-contribution.service.ts:
  * calculateForPrePlantingOrder 入参从 bigint 改为 string(orderNo)
  * findUnique({ where: { orderNo } }) 查询,用存储的 originalOrderId 计算偏移
  * 所有日志/update 中 originalOrderId 替换为 orderNo
  * processUndistributedOrders 改为传 order.orderNo,orderBy 改为 createdAt
- schema.prisma:orderNo 字段新增 @unique 约束
- migration SQL:CREATE UNIQUE INDEX on order_no 列

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 02:31:04 -08:00
hailin 560674f2e9 fix(pre-planting): 无推荐关系用户购买预种时 404 导致整笔交易失败
问题:PrePlantingReferralClient.getReferralChain() 生产环境遇到 404
(用户无推荐人)时直接 throw error,导致整个购买事务回滚,
无推荐关系的用户(测试账号、直接注册用户)完全无法购买预种份额。

修复:AxiosError status === 404 时返回 { directReferrer: null },
与"直接注册、无推荐人"的正常业务场景对齐,不阻断购买流程。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 02:13:29 -08:00
hailin 21fc55fb01 fix(capability): auth-service CapabilityGuard 类型修复 string → Capability
isCapabilityEnabled 参数需要 Capability 枚举类型,添加 as Capability 类型断言

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 22:29:20 -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 2684a81383 fix(pre-planting): getEligibility 返回 canPurchase 字段修复"待开启"问题
后端 getEligibility() 原先只返回 hasPrePlanting/canTrade 等内部字段,
缺少前端购买页期望的 canPurchase/maxAdditional/message 字段。
由于 json['canPurchase'] ?? false 默认为 false,导致购买页始终显示"待开启"。

修复:getEligibility() 现在先查询 admin config 的 isActive 状态,
结合用户持仓计算出 canPurchase/maxAdditional/message,同时保留原有字段。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 23:36:08 -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 20a73a8d43 feat(planting-service): 添加 transfer_locked_count 列的数据库迁移
Prisma schema 中已定义 transferLockedCount 字段但缺少对应 migration,
导致运行时 PrismaClientKnownRequestError:
  The column planting_orders.transfer_locked_count does not exist

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 22:57:15 -08:00