Commit Graph

1892 Commits

Author SHA1 Message Date
hailin 761fc4369c fix(auth): docker-compose SMS 变量名与 .env 对齐,补充 SMS_ENABLED 注入
之前用 SMS_ACCESS_KEY_ID 等旧名,但 .env 用 ALIYUN_ 前缀,
导致容器拿到空值且 SMS_ENABLED 未注入,短信发送一直走 disabled 分支。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 20:33:14 -07:00
hailin 41cd442ce7 fix(auth): 修正 response.body 可能为 undefined 的 TS 类型错误 2026-03-10 20:27:28 -07:00
hailin a2313734b4 feat(auth): 实现阿里云短信发送
- 安装 @alicloud/dysmsapi20170525 SDK
- 实现 sendSmsToProvider,使用 ALIYUN_ACCESS_KEY_ID/SECRET/
  SMS_SIGN_NAME/SMS_TEMPLATE_CODE 环境变量(.env 已配置)
- SMS_ENABLED=false 时仅打日志,不发送(开发环境降级)
- 发送失败时抛出 BadRequestException,不静默吞掉错误

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 20:21:22 -07:00
hailin 936c3e89c1 fix(auth): SMS controller DTO 缺少 class-validator 装饰器导致发送失败
全局 ValidationPipe 开启了 whitelist + forbidNonWhitelisted,
SendSmsDto/VerifySmsDto 没有装饰器导致 phone/type 被当成非法属性拒绝。
补齐 @IsString/@IsNotEmpty/@Matches/@IsEnum 装饰器。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 20:04:49 -07:00
hailin 149cf7ea77 fix(auth): 修复找回密码流程的三个 bug
Backend:
- password.service.ts: resetPassword 补齐验证码暴力破解防护,
  输错时累计 attempts,超过 5 次拒绝,与 loginBySms 逻辑一致

Frontend (forgot_password_page.dart):
- 发送验证码错误消息改为提取真实 message,不再显示原始异常类名
- 重置密码错误消息同上处理
- 新增 _sendingSms 标志,发送请求期间禁用按钮,防止重复发短信

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 19:56:34 -07:00
hailin 761dcb1115 fix(auth): resetPassword 解除锁定 + 登录错误提示优化
Backend:
- password.service.ts: resetPassword 成功后调用 user.unlock(),
  清除 loginFailCount 和 lockedUntil,避免用户改密后仍无法登录

Frontend:
- api_client.dart: 401 响应提取后端真实错误消息,不再丢弃
- auth_remote_datasource.dart: loginWithPassword 直接 rethrow
  已知异常类型,避免二次包装导致消息格式混乱
- login_page.dart: 登录失败按错误类型分类提示:
  · 账户锁定 → AlertDialog + "找回密码"按钮
  · 还有尝试机会 → SnackBar(橙色) + "找回密码"Action
  · 其他错误 → 普通 SnackBar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 19:46:06 -07:00
hailin ff82bddbc6 fix(mining-admin): 审计日志 keyword 搜索、管理员列、详情中文化
1. keyword 搜索实际生效:按 resourceId 和管理员用户名模糊匹配
2. LOGIN_FAILED 无账号时管理员列显示尝试的用户名(而非 unknown)
3. 详情列对 LOGIN_FAILED reason 做中文映射

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 01:27:31 -07:00
hailin a72be1bbf3 fix(mining-admin): 修正 migration SQL 列名为驼峰式 adminId
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 00:49:20 -07:00
hailin ad1c889848 feat(mining-admin): 审计日志记录登录失败事件
- audit_logs.adminId 改为可选字段,支持用户名不存在时的失败记录
- 登录失败时记录 LOGIN_FAILED 操作,resourceId 存储尝试的用户名,newValue 记录失败原因
- 前端审计日志页新增 LOGIN_FAILED 标签(红色)及筛选选项
- 新增 migration: 20260310_audit_log_nullable_admin

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 00:42:57 -07:00
hailin 991bc77db8 fix(mining-admin): Dockerfile builder 阶段补充 python3/make/g++ 以支持 bcrypt 编译
缓存失效后 npm ci 会从源码编译 bcrypt,但 node:20-alpine 的 builder 阶段
缺少 python3/make/g++,导致构建失败。runner 阶段已有这些工具,
现在 builder 阶段同步补上,确保镜像可稳定重建。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 22:37:47 -07:00
hailin 510a890b33 fix(mining-admin): 修复审计日志 IP 地址记录为内网 IP 的问题,补全全操作的 IP 审计
【根本原因】
- main.ts 缺少 `app.set('trust proxy', 1)`,Express 在 Nginx/Kong 代理后 req.ip
  返回的是代理服务器内网 IP(127.0.0.1),而非真实客户端 IP。
- Nginx 已正确设置 X-Forwarded-For / X-Real-IP,但后端从未读取这些 header。

【修复内容】

1. main.ts
   - 新增 `app.set('trust proxy', 1)`,使 Express 信任 Nginx 第一跳的
     X-Forwarded-For,req.ip 从此返回真实客户端 IP。

2. shared/utils/get-client-ip.ts(新建工具函数)
   - 优先读取 X-Forwarded-For(取逗号分隔的第一个 IP,支持多跳代理)
   - 其次读取 X-Real-IP
   - 兜底使用 req.ip
   - 全服务统一使用此函数,避免各处重复逻辑。

3. auth.controller.ts / auth.service.ts
   - LOGIN:将 req.ip 改为 getClientIp(req)(已记录 IP,修正来源)
   - LOGOUT:之前完全不记录 IP/UA,现在补全传入并存入审计日志。

4. config.service.ts / config.controller.ts
   - setConfig / deleteConfig 新增 ipAddress / userAgent 可选参数。
   - 新增 recordAuditLog 通用方法,供 Controller 记录任意审计事件。
   - 所有写操作(setTransferEnabled、setP2pTransferFee、setConfig、
     deleteConfig)均传入真实 IP 和 UA。
   - activateMining / deactivateMining 之前完全无审计日志,
     现补录 ACTIVATE / DEACTIVATE 类型的 MINING 审计条目。

5. capability-admin.service.ts / capability.controller.ts
   - setCapability / setCapabilities / writeAuditLog 均新增
     ipAddress / userAgent 参数,Controller 层传入真实 IP。

6. pre-planting-restriction.service.ts / controller
   - unlockRestriction 新增 ipAddress / userAgent 参数并写入审计日志。

7. manual-mining.service.ts / controller
   - execute 新增 ipAddress / userAgent 参数并写入审计日志。

8. batch-mining.service.ts / controller
   - execute 新增 ipAddress / userAgent 参数并写入审计日志
     (upload-execute 和 execute 两个入口均已更新)。

【影响范围】
- 仅 mining-admin-service,无数据库 Schema 变更(ipAddress/userAgent 字段已存在)。
- 所有现有接口签名向后兼容(新增参数均为可选)。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 22:32:29 -07:00
hailin 92c71b5e97 fix(presence): 在线判定窗口从 3 分钟调整为 5 分钟(行业标准)
心跳间隔 60s 不变,窗口从 180s → 300s(5x 容差),
对网络抖动、Android ROM 限制、WiFi/4G 切换更友好。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 21:48:09 -08:00
hailin c9217a85a9 fix(presence-service): JWT guard 用 sub 兼容旧 token 的 userId/accountSequence
旧 token 只有 sub 字段,新 token 有 userId/accountSequence。
用 payload.userId ?? payload.sub 兼容两种格式,
确保旧 token 心跳能正确写入在线状态。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 21:24:12 -08:00
hailin a01355aecc fix(presence-service): JWT guard 向后兼容无 type 字段的旧 token
新 token 含 type:'access',旧 token 无 type 字段。
改为:只有 type 字段存在且不为 'access' 时才拒绝,
避免已登录用户因旧 token 格式导致心跳永久 401。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 21:16:01 -08:00
hailin 330f8c7681 fix(auth-service): JWT access token 加入 type/userId/accountSequence 字段
presence-service 的 JwtAuthGuard 校验 payload.type === 'access',
但之前 token 只有 sub/phone/source,导致心跳接口一直 401。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 21:09:24 -08:00
hailin 893513ad78 feat(presence): 添加设备档案表,实现事件流水+设备快照分离
大厂标准架构(Amplitude/Mixpanel):
- analytics_event_log: 事件流水(append-only,每条事件一行)
- analytics_device_profile: 设备快照(每台设备一行,upsert 更新)

设备分布查询从 O(events) 降为 O(devices):
- SELECT COUNT(*), device_brand FROM analytics_device_profile GROUP BY device_brand
  不再需要 COUNT(DISTINCT install_id) 扫描全量事件表

ON CONFLICT (install_id) DO UPDATE:
- COALESCE 保留已有字段(不被 NULL 覆盖)
- last_seen_at 每次上报更新
- event_count 累加(可用于活跃度分析)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 19:31:33 -08:00
hailin 2c312a850b fix(2.0): presence-service-2 使用 APP_PORT 指定监听端口
presence-service 的 main.ts 读取 APP_PORT 而非 PORT,
添加 APP_PORT: 3027 确保服务监听在正确端口。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 18:53:28 -08:00
hailin b06f1272e2 feat(deploy): 注册 presence-service-2 到 deploy-mining.sh
- MINING_SERVICES 追加 presence-service-2 (port 3027)
- SERVICE_ALIASES 追加 presence / presence2 快捷名
- MINING_DATABASES 追加 rwa_mining_presence
- SERVICE_DB / SERVICE_PORTS 映射表同步更新

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 18:46:27 -08:00
hailin 5eb4afa2f9 feat(2.0): 为 2.0 部署独立的 presence-service-2
- 在 docker-compose.2.0.yml 添加 presence-service-2 (port 3027)
- 使用独立数据库 rwa_mining_presence,隔离 2.0 用户 DAU/在线数据
- Redis DB 17(2.0 其他服务已用 8,10,12,13,14,15,16)
- postgres-2 POSTGRES_MULTIPLE_DATABASES 追加 rwa_mining_presence
- 复用 presence-service 代码,与 auth-service 共享 JWT_SECRET

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 18:39:44 -08:00
hailin 6bca65e434 feat(telemetry): 设备字段提升为顶层结构化列(Amplitude 风格)
将 device_brand/device_model/device_os/app_version/locale 从 JSONB properties
提升为 analytics_event_log 表的独立列,并建立索引,支持亿级数据量下的高效
按设备维度查询和分组统计。

前端 (mining-app + mobile-app):
- toServerJson() 从 properties 中提取设备字段,以顶层字段发送给服务端
- 本地存储格式不变(properties 仍保留设备字段,便于离线队列完整性)

后端 (presence-service):
- Prisma schema: EventLog 新增 deviceBrand/deviceModel/deviceOs/appVersion/locale 列
- Migration: ALTER TABLE 添加 5 列 + 2 个索引
- DTO/Command: EventItemDto 接收顶层设备字段
- Entity: EventLog 新增 5 个字段及 getter
- Mapper: toDomain/toPersistence 映射新字段
- Handler: toEventLog 从 DTO 读取设备字段;SessionStartedEvent 优先使用顶层字段

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 09:50:48 -08:00
hailin 9c84be72bc chore(presence-service): 更新 package-lock.json 以包含 @nestjs/jwt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 08:59:34 -08:00
hailin e8d9cb72a9 feat(presence-service): 修复鉴权——用户JWT验证心跳,管理员JWT查询在线/DAU数据
- 添加 @nestjs/jwt 依赖,AppModule 注册全局 JwtModule
- 重写 JwtAuthGuard:使用 JwtService.verifyAsync 解析用户 token (type=access)
- 新建 AdminGuard:验证管理员 token (type=admin),与 identity-service 共享 JWT_SECRET
- heartbeat 接口:保持 JwtAuthGuard(用户 JWT)
- online-count / online-history / dau:改用 AdminGuard(管理员 JWT)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 08:52:39 -08:00
hailin a1d284b6b5 fix(password): 统一登录密码和支付密码状态查询为服务端 API
问题:
- isPasswordSet() 读 SecureStorage,重装/换设备后丢失 → 页面误判未设密码
- isPaymentPasswordSet() 读 response.data['isSet'],但实际格式为
  { success, data: { isSet } },取到 null → 始终返回 false

修复:
- 后端新增 GET /user/password-status 接口(isLoginPasswordSet 方法)
- 前端 isPasswordSet() 改为调用服务端 API
- 两个方法均使用正确的 response.data['data']['isSet'] 解包

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 08:14:24 -08:00
hailin 3aa2856770 fix(payment-password): 修复 isActive 字段名错误(应为 status !== ACTIVE)
Prisma schema 中 UserAccount 无 isActive 字段,实际为 status VARCHAR(20)。
两处 select 改为 { phoneNumber, status },检查改为 status !== 'ACTIVE'。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 07:05:42 -08:00
hailin 6f912b1232 feat(payment-password): 添加忘记支付密码功能(全栈)
后端(identity-service,纯新增):
- user-application.service 添加 2 个方法:
  sendResetPaymentPasswordSmsCode(userId) —— 发送验证码到已绑定手机(Redis key 独立)
  resetPaymentPassword(userId, smsCode, newPassword) —— 验证码校验 + 格式校验 + bcrypt 更新
- user-account.controller 新增 2 个端点(均为 @ApiBearerAuth):
  POST /user/send-reset-payment-password-sms
  POST /user/reset-payment-password

前端(mobile-app,纯新增):
- account_service 新增 sendResetPaymentPasswordSmsCode / resetPaymentPassword 两个方法
- 新建 reset_payment_password_page.dart:验证码 + 新6位PIN,重置成功后自动返回
- 路由:RoutePaths / RouteNames / AppRouter 各新增 resetPaymentPassword 条目
- change_payment_password_page:旧密码输入框下方添加「忘记支付密码?」入口链接

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 07:04:20 -08:00
hailin cad7ebe832 feat(payment-password): 添加支付密码功能(全栈)
后端(identity-service):
- Prisma schema 新增 paymentPasswordHash 字段及手动迁移脚本
- user-application.service 添加 4 个方法:isPaymentPasswordSet / setPaymentPassword /
  changePaymentPassword / verifyPaymentPassword(纯新增,不修改已有逻辑)
- user-account.controller 新增 4 个端点:
  GET /user/payment-password-status
  POST /user/set-payment-password
  POST /user/change-payment-password
  POST /user/verify-payment-password → { valid: bool }

前端(mobile-app):
- account_service 新增 4 个方法对应后端 4 个接口
- 新建 change_payment_password_page.dart:6 位数字支付密码设置/修改页面
- 路由:RoutePaths / RouteNames / AppRouter 注册 /security/payment-password
- profile_page:'修改登录密码' 改为 '修改密码',下方新增 '支付密码' 入口
- PasswordVerifyDialog:新增 title / subtitle / hint 可选参数,支持登录/支付双模式
- planting_location_page:认种确认改为验证支付密码(verifyPaymentPassword)
- pre_planting_purchase_page:预种确认后追加支付密码验证步骤

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 06:18:40 -08:00
hailin b67dfa0f4c fix(planting-service+app): 合并列表补充 totalPortions,前端显示实际份数而非订单数
- getMerges 批量查来源订单 portionCount,按合并分组求和后返回 totalPortions
- 预种明细合并卡片改用 totalPortions 显示份数,fallback 才用订单数

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 08:58:45 -08:00
hailin 1f5bb62805 fix(contribution-service): 合并完成后写入 synced_adoptions 记录以解除卖出限制
swapContributionForMerge 在事务内新增步骤 9f:
- 写入 synced_adoptions(original_adoption_id = 20B + mergeId)
- 供 SellRestrictionService.isRestricted 的 has_real_tree 判断使用
- upsert 保证幂等,contributionDistributed 由调用方置 true

Bug: 合并已完成(contribution_records 正确),但 isRestricted 始终
返回 true,因为 synced_adoptions 中没有 20B+ 记录(virtualAdoption
仅用于计算,未持久化)。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 08:25:15 -08:00
hailin 94792f56ea fix(contribution-service): 预种算力除数从 5 改为 10,冻结阈值同步更新
- PRE_PLANTING_PORTION_DIVISOR: 5 → 10(每份算力 = 1棵树的 1/10)
- 冻结条件:totalPortions < 5 → < 10
- 解冻条件:totalPortions >= 5 → >= 10
- 同步更新所有相关注释和日志文案

注:2026-03-02 前已分配的份额算力记录(按 1/5 计算)不追溯修正。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 08:04:42 -08:00
hailin 979ba379c1 fix(text): 预种合并份数从5份改为10份
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 06:18:12 -08:00
hailin 867d4853ae fix(trading-service): 补充 CONTRIBUTION_SERVICE_URL 环境变量
trading-service 调用 contribution-service 做预种卖出限制检查时,
因缺少 CONTRIBUTION_SERVICE_URL 导致 fallback 到 localhost:3020,
在 Docker 网络中无法连通,造成 fail-open 放行所有卖出。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 06:09:47 -08:00
hailin ee734fb7b9 feat(trading+app): 预种卖出限制 — 前端 UI 禁用 + 后端查询端点
trading-service:
- asset.controller.ts: 新增 GET /asset/sell-restriction,供 mobile app 查询当前用户限制状态
- application.module.ts: 导出 TradingSellRestrictionService

mining-app:
- api_endpoints.dart: 新增 sellRestriction 端点常量
- trading_remote_datasource.dart: 新增 getSellRestriction()(fail-open)
- trading_repository.dart/impl: 新增接口与实现
- trading_providers.dart: 新增 sellRestrictionProvider(2分钟缓存,fail-open)
- trading_page.dart: 卖出限制时显示红色提示文字并禁用"确认交易"按钮
- c2c_publish_page.dart: 发布卖出广告时显示红色提示文字并禁用发布按钮

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 05:51:56 -08:00
hailin ac3adfc90a feat(pre-planting): 新增预种积分股卖出限制(方案B纯新增)
限制仅有预种份数(未合并成棵)的用户卖出积分股,
直到用户完成首次预种合并后方可卖出。

=== 改动范围(全部 2.0 系统,纯新增)===

contribution-service:
- prisma/pre-planting/schema.prisma: 新增 PrePlantingSellRestrictionOverride 模型
- migrations/20260304000000: 对应建表 SQL
- src/pre-planting/application/services/sell-restriction.service.ts: 核心判断逻辑
  isRestricted = has_pre_planting_marker AND !has_real_tree AND !admin_override
- src/api/controllers/pre-planting-restriction.controller.ts: 暴露内部接口
  GET  /api/v2/pre-planting/sell-restriction/:accountSequence (@Public)
  POST /api/v2/pre-planting/sell-restriction/:accountSequence/unlock (@Public)
- src/api/api.module.ts: 注册新 controller 和 SellRestrictionService

trading-service:
- src/application/services/sell-restriction.service.ts: HTTP + Redis 缓存(TTL 60s)
  fail-open:contribution-service 不可用时允许卖出,保障业务连续性
- src/application/services/order.service.ts: 卖单前增加限制检查(4行)
- src/application/application.module.ts: 注册 TradingSellRestrictionService

mining-admin-service:
- src/application/services/pre-planting-restriction.service.ts: 代理接口 + 审计日志
  每次管理员解除操作均写入 AuditLog,保证严格可追溯性
- src/api/controllers/pre-planting-restriction.controller.ts:
  GET  /pre-planting-restriction/:accountSequence
  POST /pre-planting-restriction/:accountSequence/unlock
- api.module.ts / application.module.ts: 注册新服务和接口

mining-admin-web:
- users.api.ts: 新增 getPrePlantingRestriction / unlockPrePlantingRestriction
- use-users.ts: 新增 usePrePlantingRestriction / useUnlockPrePlantingRestriction hooks
- users/[accountSequence]/page.tsx: 受限时在基本信息卡显示红色警告 + 解除按钮

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 05:04:57 -08:00
hailin 8fcfec9b65 fix(contribution): backfill 强制重算 unlock status,修复预种用户层级卡在5级的问题
问题根因:直推用户先买预种导致 directReferralAdoptedCount 已累加到正确值(如5),
但 markAsAdopted() 随后被调用时硬编码 level=5/bonus=1,覆盖了正确的解锁状态。
之后 backfill 因 count 未变(5>5=false)永远不触发重算,level 永久卡死。

修复:updateAccountUnlockStatus 改用 setDirectReferralAdoptedCount() 替代
incrementDirectReferralAdoptedCount 循环,无论 count 是否变化都强制调用
updateUnlockStatus() 重算 unlockedLevelDepth 和 unlockedBonusTiers。

同时为 getDirectReferralAdoptedCount 补充注释,说明常规认种和预种均按人头计。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 23:16:47 -08:00
hailin 728728bee3 fix(admin-service): 个人/团队认种数量改为统计棵数而非订单数
【问题】
getUserList 及推荐树节点中「个人认种」「团队认种」显示的是订单条数
(_count: { id }),而非实际认种棵数。一笔订单可认种多棵,导致多棵
合并下单的用户数量严重偏低。

【修复】
将以下方法中的所有 count(orders) 改为 sum(treeCount):
- getPersonalAdoptionCount   — 用户详情页个人认种数
- getTeamStats               — 用户详情页团队认种数
- getBatchUserStats          — 用户列表批量统计(个人/团队/省/市认种数)
- getAncestors               — 推荐树祖先节点认种数
- getDirectReferrals         — 推荐树直推节点认种数

【影响范围】
仅影响 admin-web 管理后台的展示数据,不涉及业务逻辑和数据存储。
省市认种百分比计算基准同步修正(teamAdoptions 也改为棵数),
比例结果不变,但基数更准确。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 22:29:32 -08:00
hailin 3f4b22b013 docs(contribution): add detailed comments for backfill task and findAccountsWithIncompleteUnlock
Explain the starvation root cause, unlock rules, pre-planting user scenario,
and future scalability considerations (cursor-based pagination).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 21:58:52 -08:00
hailin 2565fa8259 fix(contribution): process all incomplete-unlock accounts in backfill (remove 100-limit)
Previous 100-account batch caused starvation: accounts at positions 101+
(including recent pre-planting users) were never processed. Remove limit
to process all eligible accounts in each 10-minute run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 21:55:46 -08:00
hailin 551723fe82 fix(contribution): remove redundant snapshotDate from GetBatchRatiosRequest query DTO
The date is already read from URL path param @Param('date'), not query string.
Having it as required in the query DTO caused 400 Bad Request on ratios endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 09:06:53 -08:00
hailin fb4e52c0de fix(contribution): add @Public() to getBatchRatios endpoint for service-to-service calls
mining-service calls this endpoint without JWT token during DailySnapshot
full sync, causing 401 Unauthorized. Mark it as public since it's internal data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 08:35:55 -08:00
hailin a392f708a7 fix(mining): 修复 DailySnapshot API 路径 v1→v2 + deploy-mining.sh 默认 standalone
- fetchContributionRatios URL 从 /api/v1/ 改为 /api/v2/ 与 contribution-service 的 globalPrefix 匹配
- deploy-mining.sh 默认部署模式从 shared 改为 standalone

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 08:27:57 -08:00
hailin 2b2e1efc7a fix(mining): 修复挖矿分配并发覆盖贡献值同步的 Lost Update 问题
挖矿分配每秒运行的 save() 无条件写回所有字段(含 totalContribution),
导致贡献值同步刚更新的正确值被立即覆盖回旧值。
同时修复 DailySnapshot 全量同步一直 synced 0 accounts 的安全网失效问题。

- repository save() 增加 skipContributionUpdate 选项
- 挖矿分配路径传入 skipContributionUpdate: true
- contribution-service DailySnapshot 事件 payload 补全字段
- mining-service 适配字段名差异并修复 API 解析 bug

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 07:24:52 -08:00
hailin 532be9a561 fix(pre-planting): 合并详情显示实际份数和金额,不再硬编码
问题:合并详情页"合并份数"显示订单条数(7)而非实际份数(10),
"总价值"硬编码 订单数×1887,每笔订单金额也硬编码 1,887。

修复:
后端 getMergeDetail:
  - 新增 sourceOrders[] 含每笔订单的 portionCount + totalAmount
  - 新增 totalPortions(总份数)和 totalAmount(总金额)

前端 PrePlantingMerge model:
  - 新增 MergeSourceOrder 类
  - 新增 sourceOrders/totalPortions/totalAmount 字段

前端合并详情页:
  - "合并份数"用 totalPortions 替代 sourceOrderNos.length
  - "总价值"用 totalAmount 替代硬编码计算
  - 来源订单列表显示每笔实际金额和份数(多份时显示"N份"前缀)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 02:58:34 -08:00
hailin a8e06e2eda fix(pre-planting): 合并逻辑改为按份数累计,支持多份订单合并
问题:用户一笔订单可购买多份(portionCount>1),但 performMerge
按订单条数校验是否够 10 条,导致 6 笔订单共 10 份却报错
"不足 10 笔已支付订单进行合并"。

修复:
- performMerge: 遍历 PAID 订单累加 portionCount 直到凑满 10 份
- findPaidOrdersByUserId: 去掉 limit 参数,获取所有 PAID 订单
- PrePlantingMerge.create: 去掉 sourceOrderNos.length === 10 校验
  改为 length > 0(份数校验已在 performMerge 完成)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 02:44:35 -08:00
hailin d7f7d7082d fix(pricing): 预种定价API常量同步 — 3566→1887, /5→/10, 正式认种15831不变
admin-service tree-pricing.service.ts:
  - BASE_PORTION_PRICE: 3566 → 1887
  - supplement 除数: /5 → /PORTIONS_PER_TREE(10)
  - BASE_PRICE 保持 15831(正式认种价格不变)
  - 移除 updateSupplement 中重复声明的 BASE_PRICE 局部变量

planting-service tree-pricing-admin.client.ts:
  - fallback basePortionPrice/totalPortionPrice: 3566 → 1887

mobile-app tree_pricing_service.dart:
  - 修正上次commit误改的 basePrice/totalPrice fallback: 18870 → 15831
  - basePortionPrice/totalPortionPrice 保持 1887

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:56:21 -08:00
hailin a801a46e76 fix(admin): 修复授权照片代理未解包全局响应拦截器的问题
authorization-service 全局拦截器将响应包装为 {success, data, timestamp},
代理服务需要从 response.data.data 取实际数据,而非 response.data。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 22:50:52 -08:00
hailin 1621b75a47 feat(admin): 引荐关系树节点增加个人/团队预种份数展示
后端:
- ReferralNodeDto 新增 selfPrePlantingPortions, teamPrePlantingPortions
- user-detail.controller: getReferralTree 中并行调用
  ReferralProxyService 批量获取所有节点的预种统计
  (当前用户用 getPrePlantingStats,祖先+下级用 batchGetPrePlantingStats)

前端:
- ReferralNode 类型新增两个预种字段
- 引荐关系树节点(祖先链 + 递归展开节点)在"本人认种/团队认种"
  下方新增一行"个人预种: X份 / 团队预种: Y份"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 19:16:02 -08:00
hailin eb425b0f92 fix(referral): 团队预种总量排除自己的预种份数
TeamStatistics.teamPrePlantingPortions 包含用户自身的预种量,
但"团队预种"应只统计伞下成员。返回时减去 selfPrePlantingPortions。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 19:03:08 -08:00
hailin 5e05e336f7 fix(mining-admin): add @map for targetType column in Notification schema
Missing @map("target_type") caused Prisma to look for camelCase column
name instead of the snake_case column created in migration SQL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 08:55:00 -08:00
hailin e68b5aa3d9 feat(mining-admin): add Prisma migration for notification tables
Create notifications, notification_reads, notification_user_targets
tables with indexes and unique constraints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 08:48:17 -08:00
hailin 5ee94b3672 fix(notifications): NotificationPriority 类型从 string 改为 Prisma 枚举
修复后端编译错误: priority 字段类型应使用 @prisma/client 的
NotificationPriority 枚举而非 string。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 08:38:25 -08:00