Commit Graph

918 Commits

Author SHA1 Message Date
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
hailin edc81cc55d fix(trading+mining-app): 修复"全网兑换销毁量"显示为0的问题
根因:前端"全网兑换销毁量"读取的是circulationPool(流通池积分股),
但实际应该显示burn_records中source_type=SELL_BURN的销毁总量。
这是两个不同的概念:circulationPool是卖出交易进入流通的积分股,
而兑换销毁量是卖出时被销毁(进入黑洞)的积分股。

修复:
- 后端: BlackHoleRepository添加getTotalSellBurned()聚合查询
- 后端: asset.service.ts市场概览API新增totalSellBurned字段
- 前端: MarketOverview实体/Model新增totalSellBurned字段
- 前端: trading_page销毁明细弹窗改用totalSellBurned显示

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 09:52:10 -08:00
hailin acf55b26a7 feat(pricing): 预种每份价格从 3171 调整为 3566 绿积分
分配规则:按 reward-service RIGHT_AMOUNTS(15831 整棵树)各项 /5 取整,
余额全归总部社区(HQ_BASE_FEE)。5 份合成一棵树 = 17830。

10 类分配金额变更:
- COST_FEE:       576 (不变, floor(2880/5))
- OPERATION_FEE:  420 (不变, floor(2100/5))
- HQ_BASE_FEE:    29.4 → 427 (3566 - 3139, 吸收全部余额)
- RWAD_POOL:      1152 (不变, floor(5760/5))
- SHARE_RIGHT:    720 (不变, floor(3600/5))
- PROVINCE_AREA:  21.6 → 21 (floor(108/5))
- PROVINCE_TEAM:  28.8 → 28 (floor(144/5))
- CITY_AREA:      50.4 → 50 (floor(252/5))
- CITY_TEAM:      57.6 → 57 (floor(288/5))
- COMMUNITY:      115.2 → 115 (floor(576/5))
- 合计: 3171 → 3566 ✓

涉及服务:planting-service, admin-service, contribution-service
涉及前端:admin-web, mobile-app (Flutter)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 08:02:17 -08:00
hailin ed6b48562a feat(pricing): 认种树动态定价涨价系统(总部运营成本压力涨价)
基础价 15831 USDT/棵不变,新增 HQ_PRICE_SUPPLEMENT 加价项全额归总部(S0000000001)。
支持手动调价+自动周期涨价,所有变更可审计,移动端动态展示价格及涨价预告。

- admin-service: TreePricingConfig/ChangeLog 表 + Service + Controller + 定时任务
- planting-service: 正式认种和预种订单快照 priceSupplement,动态价格校验
- reward-service: HQ_PRICE_SUPPLEMENT 分配类型,涨价金额直接入总部账户
- admin-web: Settings 页面新增定价配置区间(手动调价/自动涨价/变更历史)
- mobile-app: TreePricingService + 动态价格加载 + 涨价预告展示

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 03:02:56 -08:00
hailin 577979bc83 chore: 同步线上构建版本号,避免服务器编译时版本号回退
- mining-app: 1.0.0+1 → 1.0.0+58(与线上已发布版本对齐)
- mobile-app: 2.0.0+1 → 2.0.0+357(与线上已发布版本对齐)

构建脚本会在此基础上自动递增,下次构建将从 59/358 开始。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 00:29:29 -08:00
hailin a68fe5e999 feat(mining-app): 兑换页面"流通池"改名及"已销毁量"点击查看销毁明细
1. 将市场数据卡片中的"流通池"标签改名为"已分配积分股"
   - 展示值不变,仍为 totalMined(全网已挖矿产出总量)

2. "已销毁量"新增点击交互,点击弹出"销毁明细"对话框:
   - 系统总销毁量(blackHoleAmount):当前页面已展示的黑洞销毁总量
   - 全网兑换销毁量(circulationPool):卖出交易实际产生的流通池金额
   - 标签旁显示 info_outline 图标提示可点击

3. 重构 _buildMarketDataItem,拆分出 _buildMarketDataItemContent:
   - _buildMarketDataItem:包含 Expanded 包裹,供普通数据项使用
   - _buildMarketDataItemContent:纯内容组件,支持 showTapHint 参数
   - 避免 Row > GestureDetector > Expanded 的布局冲突

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:55:37 -08:00
hailin eda39b982d fix(mobile-app): 修复切换窗口期 NotificationBadge 混账号请求问题
问题(通过日志发现):
账号切换时存在一个 storage 空窗期(旧数据已清除、新数据尚未恢复完成)。
在此期间,NotificationBadgeNotifier 的 30s 定时器恰好触发,导致:
- _loadUnreadCount() 从 authProvider 内存读到旧账号 userSerialNum
- HTTP interceptor 从 storage 读到已恢复的新账号 accessToken
- 发出混账号请求:?userSerialNum=旧账号 + Authorization: Bearer 新账号token
日志证据:
  _restoreAccountData() 执行期间出现
  GET /notifications/unread-count?userSerialNum=D26022600000
  Authorization: Bearer [D26022600001的token]

修复:

1. notification_badge_provider.dart
   新增 stopAutoRefresh() 公开方法,取消 30s 定时器而不 dispose,
   Provider invalidate 重建后会自动重启定时器。

2. account_switch_page.dart - _switchToAccount
   在 onBeforeRestore 中补加:
     ref.read(notificationBadgeProvider.notifier).stopAutoRefresh()
   确保切换空窗期内 notificationBadge 定时器不触发。

   同时移除 UI 层冗余的 saveCurrentAccountData() 调用——
   switchToAccount() 内部已有此步骤,无需重复。

   日志步骤从 [1/6]...[6/6] 更新为 [1/5]...[5/5],
   并在 onBeforeRestore 注释中说明停止各定时器的原因。

切换空窗期现在所有定时器均已停止:
  ✓ walletStatusProvider (60s)
  ✓ pendingActionPollingService (4s)
  ✓ notificationBadgeProvider (30s)  ← 本次新增
  ✓ TelemetryUploader (30s)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 09:09:37 -08:00
hailin 825c8a32e4 chore(mobile-app): 补全多账号切换流程的关键日志,便于验证与排查
切换流程中的每一步现在都有清晰的日志输出,方便通过 adb logcat
或 flutter logs 验证切换行为是否符合预期。

account_switch_page.dart - 三条路径全部覆盖:

_switchToAccount(切换账号):
  [1/6] 保存当前账号数据
  [2/6] 调用 switchToAccount()
  [3/6] onBeforeRestore - 停止 walletStatus/pendingAction 轮询、暂停遥测
  [4/6] invalidate authProvider / walletStatusProvider / notificationBadgeProvider
  [5/6] 恢复遥测上传
  [6/6] 导航到 ranking 页面

_addNewAccount(添加新账号):
  [1/5] 保存当前账号数据
  [2/5] 停止定时器
  [3/5] 调用 logoutCurrentAccount()
  [4/5] invalidate 三个 Provider
  [5/5] 导航到 guide 页面

_deleteAccount(删除账号):
  删除前打印目标账号,删除后打印剩余账号数

profile_page.dart - _performLogout(退出登录):
  [1/4] 停止 walletStatus/pendingAction 轮询
  [2/4] 调用 logoutCurrentAccount()
  [3/4] invalidate 三个 Provider
  [4/4] 导航到 guide 页面

每条关键操作完成后打印 ✓ 确认符号,便于逐步验证。
每条路径用 ========== 分隔符标识开始/完成,日志易于 grep 过滤。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 09:04:14 -08:00
hailin e02bcf418c fix(mobile-app): 升级弹窗禁止点击外部或返回键关闭,必须通过按钮操作
问题:
检测到新版本弹出升级对话框时,用户点击弹窗外部区域或按系统返回键
即可直接关闭弹窗,绕过升级提示,导致用户可能永远不会注意到更新。

修复:
对所有 3 个升级弹窗统一加两层防护:
- barrierDismissible: false — 禁止点击弹窗外部区域关闭
- PopScope(canPop: false) — 禁止系统返回键关闭

涉及弹窗:
1. self_hosted_updater.dart - _showSelfHostedUpdateDialog(自建APK更新)
2. self_hosted_updater.dart - _showMarketUpdateDialog(应用市场引导更新)
3. update_service.dart - _checkGooglePlayUpdate(Google Play 更新)

用户必须通过弹窗内按钮操作:
- 非强制更新:点击「稍后」/「暂时不更新」关闭,或点击「立即更新」开始更新
- 强制更新:只有「立即更新」按钮,无法跳过

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 08:01:52 -08:00
hailin 8f8a9230d0 fix(mobile-app): 修复多账号切换数据串号问题,完善存储隔离与状态重置
问题:
多账号切换时,前一个账号的推荐码、种植省市、缓存数据等会串到下一个账号,
同时定时器(钱包轮询、通知刷新、遥测上传)未正确停止/重启,
导致旧账号的 API 请求混入新账号上下文。

修复内容:

1. StorageKeys 补充种植省市常量(storage_keys.dart)
   - 新增 plantingProvinceName/Code、plantingCityName/Code 4个常量
   - 将硬编码的 key 统一收口,确保隔离列表引用一致

2. MultiAccountService 补全隔离列表(multi_account_service.dart)
   - _accountSecureKeys 新增 inviterReferralCode(修复邀请码串号)
   - _accountLocalKeys 新增 4个种植省市key + cachedAppAssets +
     cachedCustomerServiceContacts(修复种植/缓存数据串号)
   - switchToAccount() 新增 onBeforeRestore 回调参数,用于在
     storage清空后、恢复新数据前 停止定时器

3. AccountSwitchPage 三层状态重置(account_switch_page.dart)
   - _switchToAccount: onBeforeRestore 内停止 walletStatus 轮询、
     pendingAction 轮询、telemetry 上传;返回后 invalidate
     authProvider/walletStatusProvider/notificationBadgeProvider,
     最后 resumeAfterLogin 恢复遥测
   - _addNewAccount: 退出前停止定时器,退出后 invalidate Provider

4. ProfilePage 退出登录补全清理(profile_page.dart)
   - _performLogout: 退出前停止 walletStatus/pendingAction 轮询,
     退出后 invalidate 三个 Provider

5. 页面 key 统一引用 StorageKeys 常量
   - planting_location_page.dart 和 authorization_apply_page.dart
     将硬编码 key 替换为 StorageKeys.plantingXxx 常量

关键时序(switchToAccount 内部):
  save → clear → onBeforeRestore(停timer) → restore → 返回
  → invalidate Provider(此时storage已恢复新数据)→ resume telemetry → navigate

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 06:13:05 -08:00
hailin a5a69645b4 fix(trading): 流通池改为显示已挖矿产出总量,解决显示为0的问题
前端"流通池"原来读取 circulationPool(交易流通池),因系统尚无卖出交易故为 0。
现改为读取 totalMined(全网已挖矿产出的积分股总量 = 用户已挖 + 系统已挖)。

后端新增 getTotalMinedFromMiningService() 方法,调用 mining-service 的
GET /mining/progress 接口获取 totalDistributed。

注意:价格公式中的 circulationPool 保持不变,仍用交易流通池参与计算。
新增的 totalMined 字段仅用于前端展示。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 02:54:26 -08:00
hailin 18675f083c fix(admin-web): 滚动区域改到 nav 内部,避免裁掉 toggle 按钮
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 06:55:32 -08:00
hailin 31d3eabcf8 fix(admin-web): 侧边栏菜单项过多时支持滚动,底部不再被截断
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 06:46:15 -08:00
hailin e32ef9b9ff fix(frontend): snapshot.types 补充 MPC_POSTGRES 类型和标签
后端新增了 MPC_POSTGRES 备份目标,前端 BackupTarget 类型和
BACKUP_TARGET_LABELS 缺少对应项导致复选框无法渲染。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 04:19:57 -08:00
hailin 26e55a649f feat(snapshot): 进度显示增加文件大小信息,完成项显示 "完成 (29.4 MB)"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 02:59:07 -08:00
hailin ff28615fc3 fix(admin-web): Dockerfile 构建时传入 SNAPSHOT_SERVICE_URL build arg
Next.js rewrites 在构建阶段烘焙,运行时环境变量不生效

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 02:53:03 -08:00
hailin de361e24f6 fix(infra): 网关 nginx 添加 snapshot-api 代理 + admin-web 配置 SNAPSHOT_SERVICE_URL
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 02:44:46 -08:00
hailin 30f1355bb4 fix(admin-web): snapshots 页面补上 PageContainer 包裹,恢复侧边栏
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 02:27:43 -08:00
hailin 8855491637 feat(snapshot): 进度精度升级 — Float百分比 + MB消息存DB
- schema: progress Int→Float,新增 progressMsg 字段
- PG handler: 百分比保留2位小数(toFixed(2)),不再 Math.floor
- orchestrator: 每2秒写DB时同时写 progressMsg (含MB信息)
- 前端: 百分比显示 toFixed(1),message 优先读 progressMsg

效果: 113GB库每次轮询进度条和MB数都有变化,不再卡在整数百分比

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 01:57:47 -08:00
hailin 38efa891b8 fix(snapshot): Dockerfile 添加 SNAPSHOT_SERVICE_URL build ARG
Next.js rewrites 在 build 时烘焙到 routes-manifest.json,
运行时环境变量无法覆盖。需要通过 Docker build ARG 在构建时传入。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 00:43:33 -08:00
hailin ee94f1420d fix(snapshot): 前端 API 改走 Next.js rewrites 代理 + WebSocket 改 REST 轮询
- snapshot.api.ts: 从直连 localhost:port 改为 /api/snapshots/* 走 Next.js 代理
- next.config: 两个前端都添加 /api/snapshots/:path* → snapshot-service 代理规则
- docker-compose.2.0-snapshot.yml: overlay 中追加 mining-admin-web 的 SNAPSHOT_SERVICE_URL
- useSnapshotWebSocket → useSnapshotPolling: 2秒轮询 GET /snapshots/:id 获取进度
- 移除 socket.io-client 依赖(Next.js standalone 不支持 WebSocket proxy)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 00:37:41 -08:00
hailin ef2f0f67bf chore: 更新前端 package-lock.json 同步 socket.io-client 依赖
添加 socket.io-client 到 package.json 后未同步 lock 文件,
导致 Docker 构建时 npm ci 报 EUSAGE 错误。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 21:56:28 -08:00
hailin cf07eb03be feat(snapshot): 数据快照备份服务全量实现(纯新增,零侵入)
一套代码两处部署的在线备份编排服务,为 1.0 认种分配系统和 2.0 算力挖矿系统
分别提供 PostgreSQL / Redis / Kafka / ZooKeeper / MinIO / Uploads 的在线备份能力。
管理员在 admin-web / mining-admin-web 中选择备份目标和存储方式,点击备份后系统
串行执行各组件备份,通过 Socket.IO WebSocket 实时推送进度到前端。

## 后端 snapshot-service(NestJS 10 + Prisma 5 + SQLite)

架构: DDD 四层(api / application / domain / infrastructure)

- api 层:
  · SnapshotController — REST API(创建/查询/删除/下载)含 Range/206 断点续传
  · SnapshotGateway — Socket.IO WebSocket 实时推送 5 类事件
  · HealthController — 健康检查
  · CreateSnapshotDto — class-validator 验证
  · toSnapshotResponse — BigInt→string 序列化

- application 层:
  · SnapshotOrchestratorService — 核心编排引擎
    - startSnapshot() 异步启动,不阻塞 HTTP
    - 按 PG→Redis→Kafka→ZK→MinIO→Uploads 顺序串行执行
    - 单目标失败不中断整体任务
    - MinIO 模式: 备份完上传到 MinIO 后删除本地临时文件
    - LOCAL 模式: 保留在服务器临时目录供下载
    - @Cron(EVERY_HOUR) 自动清理过期本地备份(默认 72h)
    - runningTaskId 防止并发执行

- domain 层:
  · BackupTarget 枚举(6 种目标)+ BACKUP_TARGET_ORDER 执行顺序
  · SnapshotStatus 枚举(PENDING/RUNNING/COMPLETED/FAILED)
  · StorageType 枚举(MINIO/LOCAL)
  · BackupHandler 接口 + BACKUP_HANDLER_TOKEN

- infrastructure 层:
  · 6 个备份 Handler(均实现 BackupHandler 接口):
    - PostgresBackupHandler: pg_basebackup 通过网络流式备份,解析 stderr 进度
    - RedisBackupHandler: BGSAVE + LASTSAVE 轮询 + 打包 dump.rdb/AOF
    - KafkaBackupHandler: archiver 打包数据卷,按字节计算进度
    - ZookeeperBackupHandler: archiver 打包 data/ + log/
    - MinioBackupHandler: SDK 列举并下载所有桶(排除备份桶)后打包
    - UploadsBackupHandler: archiver 打包上传文件目录
  · 2 个存储适配器:
    - MinioStorageAdapter: fPutObject 上传 / removeObjects 批量删除
    - LocalStorageAdapter: 本地临时目录管理 + 过期清理
  · PrismaService (SQLite) + SnapshotRepository (完整 CRUD)
  · BACKUP_HANDLER_TOKEN 工厂: 根据 AVAILABLE_TARGETS 环境变量过滤可用 handler

- Prisma Schema (SQLite):
  · SnapshotTask: 主表,targets 存 JSON 字符串,totalSize 用 BigInt
  · SnapshotDetail: 明细表,每个目标一行,@@index([taskId])
  · onDelete: Cascade 级联删除

- Dockerfile: 多阶段构建,生产镜像安装 postgresql-client + mc (MinIO CLI)
  SQLite 使用 prisma db push 而非 migrate deploy

- 部署端口: 1.0 系统 = 3099,2.0 系统 = 3199

## Docker Compose overlay(纯新增,不修改现有 docker-compose)

- docker-compose.snapshot.yml (1.0):
  · 挂载 redis_data/kafka_data/zookeeper_data/zookeeper_log/admin_uploads_data 只读卷
  · AVAILABLE_TARGETS=POSTGRES,REDIS,KAFKA,ZOOKEEPER,MINIO,UPLOADS
  · 依赖 postgres + redis 健康检查

- docker-compose.2.0-snapshot.yml (2.0 standalone):
  · 挂载 redis_2_data/mining-admin-uploads/trading-uploads 只读卷
  · AVAILABLE_TARGETS=POSTGRES,REDIS,UPLOADS
  · 依赖 postgres-2 + redis-2 健康检查

## 前端 admin-web(Next.js 15 + SCSS)

- 新增 /snapshots 页面: 创建备份表单 + 实时进度条 + 历史列表 + 下载/删除
- 新增 useSnapshotWebSocket hook: Socket.IO 连接 + 5 类事件监听
- 新增 snapshot.api.ts: 独立 fetch(不走通用 apiClient,snapshot 服务独立端口)
- 新增 snapshot.types.ts: 共享类型定义
- 新增 page.module.scss: 表单/进度条/表格样式
- 修改 Sidebar.tsx: 添加「数据快照」菜单项
- package.json: 添加 socket.io-client 依赖

## 前端 mining-admin-web(Next.js 14 + Tailwind CSS)

- 新增 /snapshots 页面: 同 admin-web 功能,Tailwind CSS 风格
- 新增 useSnapshotWebSocket hook
- 新增 snapshot.api.ts + snapshot.types.ts
- 修改 sidebar.tsx: 添加「数据快照」菜单项 + HardDrive 图标
- package.json: 添加 socket.io-client 依赖

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 21:53:09 -08:00
hailin e690a55c8e style(transfer): 树转让 3 页面从暗夜主题改为 App 标准浅色棕金主题
问题:树转让的 transfer_list_page、transfer_initiate_page、
transfer_detail_page 三个页面使用了深色暗夜主题(#1A1A2E 背景 +
#16213E 卡片),与 App 其余 40+ 功能页面的浅色棕金主题不一致。

修改内容(3 个文件,纯样式重写,零业务逻辑变更):

1. 页面背景:#1A1A2E → 渐变 #FFF7E6 → #EAE0C8(与 planting_quantity_page 一致)
2. 卡片背景:#16213E → #99FFFFFF 半透明白 + boxShadow(与认种页一致)
3. AppBar:深色背景白字 → 透明背景 + 金色返回键(#D4AF37) + 棕色标题(#5D4037)
4. 正文文字:Colors.white/white70 → #5D4037 棕色 / #745D43 次级棕色
5. 输入框:#16213E 填充 → #99FFFFFF 填充 + #EAE0C8 边框
6. 按钮:ElevatedButton → GestureDetector+Container(高度 56,与全局一致)
7. 分割线:Colors.white24 → #EAE0C8
8. Tab 栏:暗色系 → 半透明白卡片容器 + 金色指示器
9. Saga 时间线未完成节点:white 20% → #EAE0C8 暖色
10. 对话框:系统默认 → #FFF7E6 背景 + 棕色文字

样式参考基准:planting_quantity_page.dart(现有认种数量页)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 20:25:25 -08:00
hailin b3a3652f21 feat(transfer): 树转让功能全量实现(纯新增,零侵入)
实现已认种果树所有权在用户间转让的完整功能。采用方案一:
独立 transfer-service 微服务 + Saga 编排器模式。

=== 架构设计 ===
- Saga 编排器 8 步正向流程:卖方确认 → 冻结资金 → 锁定树 →
  变更所有权 → 调整算力 → 更新统计 → 结算资金 → 完成
- 补偿回滚:任一步骤失败自动反向补偿(解冻资金 → 解锁树)
- 13 种状态:PENDING → SELLER_CONFIRMED → PAYMENT_FROZEN →
  TREES_LOCKED → OWNERSHIP_TRANSFERRED → CONTRIBUTION_ADJUSTED →
  STATS_UPDATED → PAYMENT_SETTLED → COMPLETED / CANCELLED /
  FAILED / ROLLING_BACK / ROLLED_BACK

=== Phase 1-2: transfer-service(独立微服务) ===
新建文件:
- Prisma Schema:transfer_orders + transfer_status_logs + outbox_events
- Domain:TransferOrder 聚合根 + TransferFeeService(5% 手续费)
- Application:TransferApplicationService + SagaOrchestratorService
- Infrastructure:Kafka 事件消费/生产 + Outbox Pattern
- API:TransferController(用户端)+ AdminTransferController(管理端)
- External Clients:wallet/planting/identity-service HTTP 客户端
- Docker + 环境配置

=== Phase 3: 现有微服务扩展(纯追加) ===
planting-service:
- Prisma schema 追加 transferLockId 可空字段
- InternalTransferController:锁定/解锁/执行 3 个新端点
- Kafka handlers:transfer-lock/execute/rollback 事件处理
- main.ts 追加 Kafka consumer group 配置

referral-service:
- PlantingTransferredHandler:处理转让后团队统计更新
- TeamStatisticsAggregate 追加 handleTransfer() 方法
- TeamStatisticsRepository 追加 adjustForTransfer() 方法
- ProvinceCityDistribution 追加 transferTrees() 方法

contribution-service:
- TransferOwnershipHandler:处理所有权变更事件
- TransferAdjustmentService:算力调整(879 行核心逻辑)
- Prisma schema 追加 transferOrderId 可空字段
- ContributionAccount 追加 applyTransferAdjustment() 方法

=== Phase 4A: wallet-service(3 个新内部端点) ===
新建文件:
- FreezeForTransferDto / UnfreezeForTransferDto / SettleTransferDto
- FreezeForTransferCommand / UnfreezeForTransferCommand / SettleTransferPaymentCommand
- InternalTransferWalletController(POST freeze/unfreeze/settle-transfer)

修改文件:
- wallet-application.service.ts 追加 3 组方法(+437 行):
  freezeForTransfer / unfreezeForTransfer / settleTransferPayment
  (乐观锁 + 3 次重试 + Prisma $transaction + 幂等检查)
- 结算操作:单事务内更新 3 个钱包(买方扣减 + 卖方入账 + 手续费归集)

=== Phase 4B: admin-web(转让管理页面) ===
新建文件:
- transferService.ts:API 调用服务 + 完整类型定义
- useTransfers.ts:React Query hooks(list/detail/stats/forceCancel)
- /transfers/page.tsx:列表页(统计卡片 + 搜索筛选 + 分页 + 13 种状态 badge)
- /transfers/[transferOrderNo]/page.tsx:详情页(Saga 时间线 + 状态日志 + 强制取消)
- transfers.module.scss:完整样式

修改文件:
- endpoints.ts 追加 TRANSFERS 端点配置
- Sidebar.tsx 追加「转让管理」菜单项
- hooks/index.ts 追加 useTransfers 导出

=== Phase 4C: mobile-app(转让 UI) ===
新建文件:
- transfer_service.dart:Flutter API 服务 + Model(TransferOrder/Detail/StatusLog)
- transfer_list_page.dart:转让记录列表(全部/转出/转入 Tab + 下拉刷新)
- transfer_detail_page.dart:转让详情(Saga 时间线 + 确认/取消操作)
- transfer_initiate_page.dart:发起转让表单(手续费自动计算)

修改文件:
- injection_container.dart 追加 transferServiceProvider
- route_paths.dart + route_names.dart 追加 3 个路由
- app_router.dart 追加 3 个 GoRoute
- profile_page.dart 追加「发起转让」+「转让记录」按钮行

=== 基础设施 ===
- docker-compose.yml 追加 transfer-service 容器配置
- deploy.sh 追加 transfer-service 部署
- init-databases.sh 追加 transfer_db 数据库初始化

=== 纯新增原则 ===
所有变更均为追加式修改,不修改任何现有业务逻辑:
- 新增 nullable 字段(不影响现有数据)
- 新增 enum 值(不影响现有枚举使用)
- 新增 providers/controllers(不影响现有依赖注入)
- 新增页面/路由(不影响现有页面行为)

回滚方式:删除 transfer-service 目录 + 移除各服务中带 [2026-02-19] 标记的代码

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 03:44:02 -08:00
hailin 765a4f41d3 feat(pre-planting): Admin Web 预种管理页面 + Sidebar 入口
[2026-02-17] Admin Web 预种计划管理页面完整实现

新增文件:
- (dashboard)/pre-planting/page.tsx: 预种管理页面
  - 预种开关控制卡片(开启/关闭 + 状态徽章)
  - 四格统计卡片(总订单、总份数、总金额、合成树数)
  - Tab 切换:预种订单 / 用户持仓 / 合并记录
  - 订单表格:订单号、用户、份数、金额、状态标签、时间
  - 持仓表格:用户、累计份数、待合并进度、合成树数、省市
  - 合并表格:合并号、用户、树数、来源订单、合同状态、挖矿状态
  - 搜索过滤、刷新、加载/错误/空状态处理
- pre-planting.module.scss: 页面样式
  - 开关状态卡片(渐变背景,开/关不同主题色)
  - 统计网格(4列响应式)
  - Tab、表格、状态标签样式

修改文件:
- Sidebar.tsx: 新增"预种管理"菜单项(数据统计与系统维护之间)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 05:54:21 -08:00
hailin 63ae7662a4 feat(pre-planting): Admin Web 预种计划 Service 层
[2026-02-17] Admin Web 预种管理 API 服务 + React Query Hooks

新增文件:
- endpoints.ts: PRE_PLANTING 端点组(config/toggle/orders/positions/merges/stats)
- prePlantingService.ts: 预种管理服务(开关配置、订单/持仓/合并查询、统计汇总)
  - 完整 TypeScript 类型定义(Config, Stats, Order, Position, Merge)
  - 分页列表请求/响应类型
- usePrePlanting.ts: React Query hooks
  - Query key factory(层级化参数化)
  - usePrePlantingConfig/Stats/Orders/Positions/Merges
  - useTogglePrePlantingConfig(mutation + 自动刷新)
- hooks/index.ts: 导出新 hooks

所有端点走 admin-service 的 PrePlantingAdminModule
与现有认种管理完全独立

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 05:48:58 -08:00
hailin 03f5c4af28 feat(pre-planting): Profile 页面添加预种计划入口
[2026-02-17] 在 Profile 页面认种按钮下方新增预种计划入口

新增内容(纯新增,不改现有逻辑):
- _goToPrePlantingPurchase():跳转预种购买页
- _goToPrePlantingPosition():跳转预种持仓页
- _buildPrePlantingButtons():两个并排按钮
  - 左侧「预种购买」:金色主题,跳转购买页
  - 右侧「预种持仓」:棕色主题,跳转持仓页

布局位置:认种按钮正下方,主内容卡片上方
现有功能零影响,仅新增 3 个方法 + 1 处布局插入

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 05:44:48 -08:00
hailin d248f92443 feat(pre-planting): Mobile App 预种合并详情页完整实现
[2026-02-17] 预种合并详情页面 (pre_planting_merge_detail_page.dart)

完整功能:
- 合并信息卡片:合并号、合并时间、份数→树数、总价值、省市
- 合同签署状态卡片:待签署/已签署/已过期,含签署时间
- 挖矿状态卡片:已开启/未开启,含开启时间
- 来源订单列表:编号圆标 + 订单号 + 金额,逐条展示 5 笔订单
- 签约确认弹窗:列出签约后解锁的权限(交易/提现/授权/挖矿)
- 底部签约按钮:仅待签署状态显示,含加载状态
- 签约成功后自动刷新页面状态

UI 风格与全局一致:渐变背景、金色主色调、卡片容器

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 05:43:17 -08:00