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
hailin
7c95d1d425
fix(admin-service): admin config 端点也返回 agreementText
...
planting-service 调用的是 admin 控制器的 getConfig()(非 public 控制器),
因为 public 控制器有双重 api/v1 前缀。确保 admin getConfig 也包含协议文本。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 22:01:03 -08:00
hailin
5131728835
fix(planting-service): 添加 ADMIN_SERVICE_URL 环境变量
...
planting-service 的 PrePlantingPublicController 需要调用 admin-service
获取预种配置(含协议文本),但 docker-compose 中缺少 ADMIN_SERVICE_URL
环境变量,导致默认使用 localhost:3010 连接失败。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:58:42 -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
92054e776e
fix(contribution): 复制预种Prisma生成客户端到dist目录修复运行时MODULE_NOT_FOUND
...
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 20:35:26 -08:00
hailin
30a2f739cb
fix(contribution): Dockerfile添加预种Prisma Client生成和migration
...
- builder和runner阶段均添加 prisma generate --schema=prisma/pre-planting/schema.prisma
- start.sh添加预种migration部署步骤
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 20:33:04 -08:00
hailin
8d7fd68509
fix(admin): 预种开关DTO添加class-validator装饰器,修复400错误
...
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 20:26:42 -08:00
hailin
37a5610d74
feat(admin): 实现预种管理页面完整API端点
...
planting-service: InternalPrePlantingController 新增4个管理员查询端点
- GET /internal/pre-planting/admin/orders (分页订单列表)
- GET /internal/pre-planting/admin/positions (分页持仓列表)
- GET /internal/pre-planting/admin/merges (分页合并记录)
- GET /internal/pre-planting/admin/stats (统计汇总)
admin-service: HTTP代理层新增5个端点
- PUT config/toggle (开关切换)
- GET orders/positions/merges/stats (代理转发到planting-service)
- 新建 PrePlantingProxyService (复用ContractService的axios代理模式)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 20:21:31 -08:00
hailin
63a169abb0
fix(cdc): deploy-mining.sh 添加预种CDC connector管理
...
更新 CDC_POSTGRES_CONNECTORS 数组和所有 case 映射(resnapshot、
full-reset topic清理、Step9重注册),确保2.0部署脚本能正确
管理预种CDC connector的生命周期。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 18:58:49 -08:00
hailin
f270b7cc27
fix(cdc): 添加3171预种计划Debezium CDC connector配置
...
预种CDC消费端(contribution-service)代码已就绪,但缺少Debezium
connector配置,导致pre_planting_orders和pre_planting_positions
表变更无法捕获到Kafka,算力无法同步。
新增:
- pre-planting-connector.json: 监听rwa_planting库的pre_planting_*表
独立slot/publication/topic前缀(cdc.pre-planting)
- register-connectors.sh: 注册pre-planting-postgres-connector
- deploy.sh: infra-status显示所有1.0 connector状态
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 18:56:42 -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
83ba9b7d54
fix(admin-service): 定价DTO添加class-validator装饰器,修复400错误
...
与auth-service支付密码DTO同样的问题:ValidationPipe的
forbidNonWhitelisted:true 导致无装饰器的DTO属性被拒绝。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 10:50:45 -08:00
hailin
81ea35b712
fix(admin-service): 修复预种价格计算公式,3566不是15831/5
...
totalPortionPrice 之前用 Math.floor(totalPrice/5) = 3166,但预种
价格 3566 是各权益项 floor(amount/5) 之和 + 总部吸收余额,不是
简单的整棵价格除以5。
修正为: BASE_PORTION_PRICE(3566) + floor(currentSupplement/5)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 10:48:07 -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
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
74f061cfeb
fix(auth-service): 修复支付密码DTO缺少class-validator装饰器导致请求被ValidationPipe拒绝
...
根因:ValidationPipe配置了whitelist+forbidNonWhitelisted,但DTO类的属性
没有任何class-validator装饰器,导致所有请求体属性被当作非白名单属性直接
返回Bad Request,请求根本未到达业务逻辑层。
修复:为SetTradePasswordDto、ChangeTradePasswordDto、VerifyTradePasswordDto
添加@IsString()和@IsNotEmpty()装饰器。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 09:39:14 -08:00
hailin
16da1d20f0
fix(auth): 修复设置支付密码时报错的问题
...
支付密码是6位纯数字,但 setTradePassword 调用了 Password.create()
走了登录密码的格式验证(要求≥8位+字母+数字),导致必然抛出异常。
新增 Password.createWithoutValidation() 方法,仅做 bcrypt hash
不走格式验证。支付密码的格式验证由 trade-password.service.ts 独立处理。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 09:01:33 -08:00
hailin
4a1bf3aafe
fix(trading): 修复已分配积分股显示为0的问题
...
mining-service 返回格式为 { success: true, data: { totalDistributed: "..." } }
但 getTotalMinedFromMiningService() 直接取 result.totalDistributed,
应该取 result.data.totalDistributed。
同时兼容两种格式,优先取 result.data.totalDistributed。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 08:36:38 -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
5adcd023e6
fix(pricing): 修复 priceSupplement 在 Kafka 事件链中丢失的问题
...
## 问题描述
认种树动态定价涨价功能 (ed6b48562 ) 在 planting-service 的资金分配
中正确计算了 HQ_PRICE_SUPPLEMENT,但 priceSupplement 字段未随
Kafka 事件传递到 reward-service,导致 reward-service 的
calculateHqPriceSupplement 永远收到 priceSupplement=0,涨价部分
的总部奖励分配不会执行。
## 事件链路径 (修复前 → 修复后)
planting-service (contract-signing.service.ts)
↓ contract.signed / contract.expired
↓ 修复前: 不含 priceSupplement ❌
↓ 修复后: 携带 order.priceSupplement ✅
referral-service (contract-signing.handler.ts)
↓ planting.order.paid / planting.order.expired
↓ 修复前: 不含 priceSupplement ❌
↓ 修复后: 透传 eventData.priceSupplement || 0 ✅
reward-service (event-consumer.controller.ts)
↓ calculateHqPriceSupplement(priceSupplement)
↓ 修复前: 始终为 0,不分配 ❌
↓ 修复后: 收到实际值,正确分配给 S0000000001 ✅
## 修改文件
1. planting-service/src/infrastructure/kafka/event-publisher.service.ts
- ContractSigningEventData 接口新增 priceSupplement?: number 字段
2. planting-service/src/application/services/contract-signing.service.ts
- signContract(): publishContractSigned 时传递 order.priceSupplement
- handleExpiredTasks(): publishContractExpired 时传递 order.priceSupplement
3. referral-service/src/application/event-handlers/contract-signing.handler.ts
- ContractSigningEvent 接口新增 priceSupplement?: number 字段
- publishOrderPaidEvent(): 透传 priceSupplement 到 planting.order.paid
- publishOrderExpiredEvent(): 透传 priceSupplement 到 planting.order.expired
## 向后兼容
- priceSupplement 为可选字段 (?: number),默认 fallback 为 0
- 已存在的订单 priceSupplement=0,不影响现有分配逻辑
- reward-service event-consumer 已有 || 0 fallback 保护
## 验证方法
1. 设置 supplement > 0 后创建认种订单
2. 签署合同后检查 reward-service 日志是否有 HQ_PRICE_SUPPLEMENT 分配记录
3. 检查总部账户 S0000000001 是否收到 priceSupplement * treeCount 的入账
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 07:13:01 -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
023d71ac33
feat(authorization): 提高省团队权益初始激活门槛至3000棵
...
省团队(AUTH_PROVINCE_COMPANY)初始激活门槛: 500棵 → 3000棵
当前各角色初始激活门槛汇总:
- 社区(COMMUNITY): 100棵(上次已调整: 10→100)
- 市团队(AUTH_CITY_COMPANY): 500棵(上次已调整: 100→500)
- 省团队(AUTH_PROVINCE_COMPANY): 3000棵(本次调整: 500→3000)
- 市区域(CITY_COMPANY): 10000棵(未变)
- 省区域(PROVINCE_COMPANY): 50000棵(未变)
变更说明:
- 仅调整初始激活门槛,月度阶梯考核目标不变
- 已激活用户不受影响(祖父条款),仅对新申请的授权生效
- 数据库已有记录的 initialTargetTreeCount 保持旧值不变
- 需在服务器重新部署 authorization-service 后生效
修改文件:
- assessment-config.vo.ts: 核心门槛配置 500→3000
- authorization-application.service.ts: AUTH_PROVINCE_TARGET 常量及注释同步
- authorization.dto.ts: DTO 注释同步
- authorization.response.ts: ApiProperty 注释同步
- DEVELOPMENT_GUIDE.md: 文档说明同步
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:44:02 -08:00
hailin
eacdfddff8
feat(authorization): 提高社区和市团队权益初始激活门槛
...
社区初始激活门槛: 10棵 → 100棵
市团队初始激活门槛: 100棵 → 500棵
变更说明:
- 仅调整初始激活门槛,月度考核目标不变(社区仍为10棵/月,市团队仍为100棵/月)
- 已激活用户不受影响(祖父条款),仅对新申请的授权生效
- 激活逻辑通过 AssessmentConfig 工厂方法动态获取门槛值,无需修改业务代码
- 数据库已有记录的 initialTargetTreeCount 保持旧值不变
修改文件:
- assessment-config.vo.ts: 核心门槛配置
- authorization-application.service.ts: 同步注释
- authorization-role.aggregate.spec.ts: 同步测试断言
- DEVELOPMENT_GUIDE.md: 同步文档描述
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 22:55:22 -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
27db2a5aa2
fix(snapshot): 修复备份进行中被删除导致容器崩溃的问题
...
1. checkDone 中 statSync 加 try-catch,文件被删时 reject 而非 uncaught crash
2. 删除 API 禁止删除 RUNNING 状态的任务,返回 409 Conflict
3. compose 补充 restart: unless-stopped,防止异常退出后服务不可用
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 01:30:44 -08:00
hailin
716b37041e
fix(snapshot): PG_HOST 改用容器名 rwa-postgres 避免跨网络 DNS 冲突
...
snapshot-service 同时在 rwa-network 和 mpc-network 中,
服务名 "postgres" 在两个网络都存在导致解析到 mpc-postgres。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 04:02:01 -08:00
hailin
b6fb421316
feat(snapshot): 新增 MPC PostgreSQL 备份目标
...
MPC 系统使用独立的 PostgreSQL 实例 (mpc-postgres),之前不在备份范围内。
新增 MPC_POSTGRES handler,通过 MPC_PG_* 环境变量连接,snapshot-service
加入 mpc-network 实现跨 compose 网络访问。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 03:12:16 -08:00
hailin
08161c64d4
fix(snapshot): API 响应补上 progressMsg 字段
...
toSnapshotResponse 手动映射字段时遗漏了 progressMsg,
导致前端轮询拿不到 MB 进度消息。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 02:11:42 -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
9cbc0ba580
fix(snapshot): DATABASE_URL 改绝对路径,修复 SQLite 每次重建丢数据
...
Prisma 的 file:./data/snapshot.db 相对于 schema.prisma 所在目录
(/app/prisma/),实际创建在 /app/prisma/data/snapshot.db,不在
volume 挂载的 /app/data/ 下,导致容器重建后数据丢失。
改为 file:/app/data/snapshot.db 绝对路径,确保 SQLite 存入
volume 挂载目录,数据跨容器持久化。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 01:48:24 -08:00
hailin
7b7bfcac93
fix(snapshot): onModuleInit 增加扫描临时目录清理孤儿文件
...
SQLite 可能因重建丢失任务记录,导致 onModuleInit 仅靠查数据库
无法清理遗留的临时目录。新增逻辑:启动时扫描临时目录所有子目录,
若数据库中不存在对应任务则直接删除。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 01:45:24 -08:00
hailin
669a8a7248
fix(snapshot): 进度写 DB 改为每 2 秒一次,避免前端长时间显示 0%
...
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 01:40:32 -08:00
hailin
f14f685ea9
fix(snapshot): PG 进度改字节百分比 + Redis 备份防崩溃 + 启动清理遗留任务
...
- postgres-backup: 用 pg_database_size 做分母,统计 stdout 字节数算进度(与 pv 方案一致)
- redis-backup: BGSAVE 后先 copyFileSync 到临时目录再打包,防止 tar-stream Size mismatch 崩溃
- orchestrator: onModuleInit 清理遗留 RUNNING 任务,标记 FAILED 并删除临时文件
- docker-compose: 临时文件改挂宿主机 /tmp 目录,方便手动清理
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 01:30:52 -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
9a88fb473a
fix(snapshot): Dockerfile 安装 postgresql-client-16 匹配目标 PG 版本
...
bookworm 默认 postgresql-client 是 15,目标数据库是 PG16
添加 PGDG 源安装 postgresql-client-16
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 23:20:45 -08:00
hailin
470dc1ccd0
fix(snapshot): PostgreSQL 备份从 pg_basebackup 改为 pg_dumpall
...
pg_basebackup -D - -Ft -z 在 PG15 中不支持同时 WAL streaming
改用 pg_dumpall | gzip 逻辑备份,更轻量且不需要 replication 权限
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 23:18:52 -08:00
hailin
a4689d5e8b
fix(snapshot): 修复 listSnapshots 分页参数 NaN 问题
...
NestJS @Query() 返回 string,需手动 parseInt 再传给 Prisma
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 23:15:26 -08:00
hailin
b8b4305ea5
fix(snapshot): Dockerfile 添加 ca-certificates 修复 mc 下载 SSL 错误
...
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 23:11:07 -08:00