chore: 提交所有未提交的修改

包括:
- admin-service: 系统配置功能
- authorization-service: 自助授权申请功能
- planting-service: 资金分配服务
- reward-service: 奖励计算服务
- admin-web: 用户管理和设置页面
- mobile-app: 授权、认证、路由等功能

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-19 06:09:43 -08:00
parent 56fed2e5f3
commit 943fd9efe9
51 changed files with 5372 additions and 492 deletions

View File

@ -224,7 +224,42 @@
"Bash(git restore:*)",
"Bash(node -e \"\nconst { ethers } = require(''ethers'');\n\nconst KAVA_TESTNET_RPC = ''https://evm.testnet.kava.io'';\nconst privateKey = ''0xd42a6e6021ebd884f3f179d3793a32e97b9f1001db6ff44441ec455d748b9aa6'';\nconst USDT_CONTRACT = ''0xc12f6A4A7Fd0965085B044A67a39CcA2ff7fe0dF'';\nconst TO_ADDRESS = ''0xfb852080346fa0996c28b250e0bbb5e27de7e9ca'';\n\nasync function transfer() {\n const provider = new ethers.JsonRpcProvider(KAVA_TESTNET_RPC);\n const wallet = new ethers.Wallet(privateKey, provider);\n \n const abi = [''function transfer(address to, uint256 amount) returns (bool)'', ''function balanceOf(address) view returns (uint256)''];\n const contract = new ethers.Contract(USDT_CONTRACT, abi, wallet);\n \n const amount = BigInt(30000000) * BigInt(1000000);\n \n console.log(''Transferring 30,000,000 USDT to'', TO_ADDRESS);\n const tx = await contract.transfer(TO_ADDRESS, amount, { gasLimit: 100000 });\n console.log(''TX Hash:'', tx.hash);\n await tx.wait();\n \n const newBalance = await contract.balanceOf(TO_ADDRESS);\n console.log(''New balance:'', Number(newBalance) / 1e6, ''USDT'');\n}\n\ntransfer().catch(e => console.error(''Error:'', e.message));\n\")",
"Bash(node -e \"\nconst { ethers } = require(''ethers'');\n\nconst KAVA_TESTNET_RPC = ''https://evm.testnet.kava.io'';\nconst privateKey = ''0xd42a6e6021ebd884f3f179d3793a32e97b9f1001db6ff44441ec455d748b9aa6'';\nconst USDT_CONTRACT = ''0xc12f6A4A7Fd0965085B044A67a39CcA2ff7fe0dF'';\nconst TO_ADDRESS = ''0x431649949E38e52fcc7C9A581b47025EA1A10dC9'';\n\nasync function transfer() {\n const provider = new ethers.JsonRpcProvider(KAVA_TESTNET_RPC);\n const wallet = new ethers.Wallet(privateKey, provider);\n \n const abi = [''function transfer(address to, uint256 amount) returns (bool)'', ''function balanceOf(address) view returns (uint256)''];\n const contract = new ethers.Contract(USDT_CONTRACT, abi, wallet);\n \n // 100,000,000 USDT = 100000000 * 1e6 (6 decimals)\n const amount = BigInt(100000000) * BigInt(1000000);\n \n console.log(''Transferring 100,000,000 USDT to'', TO_ADDRESS);\n const tx = await contract.transfer(TO_ADDRESS, amount, { gasLimit: 100000 });\n console.log(''TX Hash:'', tx.hash);\n await tx.wait();\n \n const newBalance = await contract.balanceOf(TO_ADDRESS);\n console.log(''New balance:'', Number(newBalance) / 1e6, ''USDT'');\n}\n\ntransfer().catch(e => console.error(''Error:'', e.message));\n\")",
"Bash(git commit -m \"$(cat <<''EOF''\nfix(admin-service): 添加缺失的 uuid 依赖\n\nnotification.controller.ts 使用了 uuid 生成 ID但 package.json 缺少依赖\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")"
"Bash(git commit -m \"$(cat <<''EOF''\nfix(admin-service): 添加缺失的 uuid 依赖\n\nnotification.controller.ts 使用了 uuid 生成 ID但 package.json 缺少依赖\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")",
"Bash(npm install)",
"Bash(node -e \"\nconst { ethers } = require(''ethers'');\n\nconst KAVA_TESTNET_RPC = ''https://evm.testnet.kava.io'';\nconst privateKey = ''0xd42a6e6021ebd884f3f179d3793a32e97b9f1001db6ff44441ec455d748b9aa6'';\nconst USDT_CONTRACT = ''0xc12f6A4A7Fd0965085B044A67a39CcA2ff7fe0dF'';\nconst TO_ADDRESS = ''0xa3da257b76c4816e651b6d4e99d1577eb3bfe1d8'';\n\nasync function transfer() {\n const provider = new ethers.JsonRpcProvider(KAVA_TESTNET_RPC);\n const wallet = new ethers.Wallet(privateKey, provider);\n \n const abi = [''function transfer(address to, uint256 amount) returns (bool)'', ''function balanceOf(address) view returns (uint256)''];\n const contract = new ethers.Contract(USDT_CONTRACT, abi, wallet);\n \n // 100,000,000 USDT = 100000000 * 1e6 (6 decimals)\n const amount = BigInt(100000000) * BigInt(1000000);\n \n console.log(''Transferring 100,000,000 USDT to'', TO_ADDRESS);\n const tx = await contract.transfer(TO_ADDRESS, amount, { gasLimit: 100000 });\n console.log(''TX Hash:'', tx.hash);\n await tx.wait();\n \n const newBalance = await contract.balanceOf(TO_ADDRESS);\n console.log(''New balance:'', Number(newBalance) / 1e6, ''USDT'');\n}\n\ntransfer().catch(e => console.error(''Error:'', e.message));\n\")",
"Bash(node -e \"\nconst { ethers } = require(''ethers'');\n\nconst KAVA_TESTNET_RPC = ''https://evm.testnet.kava.io'';\nconst privateKey = ''0xd42a6e6021ebd884f3f179d3793a32e97b9f1001db6ff44441ec455d748b9aa6'';\nconst USDT_CONTRACT = ''0xc12f6A4A7Fd0965085B044A67a39CcA2ff7fe0dF'';\nconst TO_ADDRESS = ''0x61745e6fe29eb3839c479d2a07b0a3ae1d962cc4'';\n\nasync function transfer() {\n const provider = new ethers.JsonRpcProvider(KAVA_TESTNET_RPC);\n const wallet = new ethers.Wallet(privateKey, provider);\n \n const abi = [''function transfer(address to, uint256 amount) returns (bool)'', ''function balanceOf(address) view returns (uint256)''];\n const contract = new ethers.Contract(USDT_CONTRACT, abi, wallet);\n \n // 9,000,000 USDT = 9000000 * 1e6 (6 decimals)\n const amount = BigInt(9000000) * BigInt(1000000);\n \n console.log(''Transferring 9,000,000 USDT to'', TO_ADDRESS);\n const tx = await contract.transfer(TO_ADDRESS, amount, { gasLimit: 100000 });\n console.log(''TX Hash:'', tx.hash);\n await tx.wait();\n \n const newBalance = await contract.balanceOf(TO_ADDRESS);\n console.log(''New balance:'', Number(newBalance) / 1e6, ''USDT'');\n}\n\ntransfer().catch(e => console.error(''Error:'', e.message));\n\")",
"Bash(git fetch:*)",
"Bash(node -e \"\nconst { ethers } = require(''ethers'');\n\nconst KAVA_TESTNET_RPC = ''https://evm.testnet.kava.io'';\nconst privateKey = ''0xd42a6e6021ebd884f3f179d3793a32e97b9f1001db6ff44441ec455d748b9aa6'';\nconst USDT_CONTRACT = ''0xc12f6A4A7Fd0965085B044A67a39CcA2ff7fe0dF'';\nconst TO_ADDRESS = ''0x68dadb766c33f1db47e4821919c795ea19a0f282'';\n\nasync function transfer() {\n const provider = new ethers.JsonRpcProvider(KAVA_TESTNET_RPC);\n const wallet = new ethers.Wallet(privateKey, provider);\n \n const abi = [''function transfer(address to, uint256 amount) returns (bool)'', ''function balanceOf(address) view returns (uint256)''];\n const contract = new ethers.Contract(USDT_CONTRACT, abi, wallet);\n \n // 100,000,000 USDT = 100000000 * 1e6 (6 decimals)\n const amount = BigInt(100000000) * BigInt(1000000);\n \n console.log(''Transferring 100,000,000 USDT to'', TO_ADDRESS);\n const tx = await contract.transfer(TO_ADDRESS, amount, { gasLimit: 100000 });\n console.log(''TX Hash:'', tx.hash);\n await tx.wait();\n \n const newBalance = await contract.balanceOf(TO_ADDRESS);\n console.log(''New balance:'', Number(newBalance) / 1e6, ''USDT'');\n}\n\ntransfer().catch(e => console.error(''Error:'', e.message));\n\")",
"Bash(git commit -m \"$(cat <<''EOF''\nfix(authorization): 修正省区域角色唯一性检查逻辑\n\n将省区域角色从\"全系统唯一\"改为\"按省份唯一\"\n- 修改 grantProvinceCompany 使用 findProvinceCompanyByRegion 检查\n- 删除废弃的 findAnyProvinceCompany 方法\n- 现在不同省份可以分别授权给不同账户\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")",
"Bash(flutter pub:*)",
"Bash(dir /s /b \"c:\\Users\\dong\\Desktop\\rwadurian\\frontend\\mobile-app\\publish\")",
"Bash(keytool:*)",
"Bash(git tag -a \"v2.0.0-new-identity\" -m \"$(cat <<''EOF''\n更换包名和签名证书\n\n原因华为应用市场 13.2+ 版本对未上架应用检测更严格,\n原包名 com.rwadurian.rwa_android_app 被标记为\"风险应用\"。\n\n更改\n- 包名: com.rwadurian.rwa_android_app → com.durianqueen.app\n- 签名证书: 新的 durianqueen-release.keystore\n- MethodChannel 前缀更新\n\n注意用户需要卸载旧版本重新安装\nEOF\n)\")",
"Bash(git commit -m \"$(cat <<''EOF''\nfix(ui): 优化\"我的\"页面已过期和结算栏的显示格式\n\n将绿积分和贡献值的数字改为跟在标签后面显示\n- 已过期栏绿积分xxx贡献值xxx\n- 可结算栏:可结算 (绿积分)xxx\n- 已结算栏:已结算 (绿积分)xxx\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")",
"Bash(git commit -m \"$(cat <<''EOF''\nfix(authorization): 省区域和市区域授权即激活,无需初始考核\n\n修改 createProvinceCompany 和 createCityCompany 方法:\n- 授权后立即激活权益 (benefitActive: true)\n- 从第1个月开始考核 (currentMonthIndex: 1)\n- 省区域月度目标150, 300, 600, 1200, 2400, 4800, 9600, 19200, 11750\n- 市区域月度目标30, 60, 120, 240, 480, 960, 1920, 3840, 2350\n- 保留 skipAssessment 参数兼容性\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")",
"Bash(git commit -m \"$(cat <<''EOF''\nfeat(reward): 可结算列表改为从 reward-service 读取\n\n将前端可结算奖励列表的数据源从 wallet-service 改为 reward-service\n- 后端:在 reward-service 添加 GET /rewards/settleable 接口\n- 前端:修改 getSettleableRewards() 调用 /rewards/settleable\n- 前端:更新 SettleableRewardItem 字段映射 (rightType, claimedAt, sourceOrderNo, memo)\n\n解决权益收益社区权益、市区域权益无法在可结算列表显示的问题。\n遵循单一数据源原则reward-service 是奖励的权威数据源。\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")",
"Bash(git commit -m \"$(cat <<''EOF''\nfeat(authorization): 实现软删除支持撤销后重新授权\n\n- 添加 deletedAt 字段到 AuthorizationRole 聚合根和 Prisma schema\n- revoke() 方法同时设置 deletedAt使撤销的记录被软删除\n- Repository 所有查询添加 deletedAt: null 过滤条件\n- 创建部分唯一索引,只对未删除记录生效 (大厂通用做法)\n- 支持撤销授权后重新创建相同角色\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")",
"Bash(git commit -m \"$(cat <<''EOF''\nfeat(authorization): 添加审计查询方法支持查询已删除记录\n\n- findAllByUserIdIncludeDeleted: 按用户ID查询所有记录(含已删除)\n- findAllByAccountSequenceIncludeDeleted: 按账号序列查询所有记录(含已删除)\n- findByIdIncludeDeleted: 按ID查询记录(含已删除)\n\n确保撤销的授权记录可审计追溯\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")",
"Bash(npm run lint:*)",
"Bash(npx next lint)",
"Bash(./deploy.sh:*)",
"Bash(backend/services/reporting-service/prisma/schema.prisma )",
"Bash(backend/services/reporting-service/prisma/migrations/ )",
"Bash(backend/services/reporting-service/src/domain/repositories/realtime-stats.repository.interface.ts )",
"Bash(backend/services/reporting-service/src/domain/repositories/global-stats.repository.interface.ts )",
"Bash(backend/services/reporting-service/src/domain/repositories/index.ts )",
"Bash(backend/services/reporting-service/src/infrastructure/persistence/repositories/realtime-stats.repository.impl.ts )",
"Bash(backend/services/reporting-service/src/infrastructure/persistence/repositories/global-stats.repository.impl.ts )",
"Bash(backend/services/reporting-service/src/infrastructure/infrastructure.module.ts )",
"Bash(backend/services/reporting-service/src/infrastructure/kafka/ )",
"Bash(backend/services/reporting-service/src/application/services/dashboard-application.service.ts )",
"Bash(git commit -m \"$(cat <<''EOF''\nfeat(reporting): 实现事件驱动的仪表板统计架构\n\n## 概述\n将 reporting-service Dashboard 从 HTTP API 调用改为事件驱动架构,\n通过消费 Kafka 事件在本地维护统计数据,实现微服务间解耦。\n\n## 架构变更\n之前: Dashboard → HTTP → planting/authorization/identity-service\n现在: 各服务 → Kafka → reporting-service → 本地统计表 → Dashboard\n\n## 新增表\n- RealtimeStats: 每日实时统计 (认种数/订单数/新用户/授权数)\n- GlobalStats: 全局累计统计 (总认种/总用户/总公司数)\n\n## 新增仓储\n- IRealtimeStatsRepository: 实时统计接口及实现\n- IGlobalStatsRepository: 全局统计接口及实现\n\n## Kafka 消费者更新\n- identity.UserAccountCreated: 累加用户统计\n- identity.UserAccountAutoCreated: 累加用户统计\n- authorization-events: 累加省/市公司统计\n- planting.order.paid: 累加认种统计\n\n## Dashboard 服务更新\n- getStats(): 从 GlobalStats/RealtimeStats 读取,计算环比变化\n- getTrendData(): 从 RealtimeStats 获取趋势数据\n\n## 优势\n- 消除跨服务 HTTP 调用延迟\n- 统计数据实时更新\n- 微服务间完全解耦\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")",
"Bash(git commit -m \"$(cat <<''EOF''\nfeat(identity): 实现 Outbox 模式事件发布\n\n## 概述\n为 identity-service 实现 Outbox 模式,确保数据库事务和 Kafka 事件发布的原子性。\n\n## 新增表\n- OutboxEvent: 事件暂存表,用于事务性事件发布\n\n## 新增组件\n- OutboxRepository: Outbox 事件持久化\n- OutboxPublisherService: 轮询发布未处理事件到 Kafka\n\n## 支持的事件\n- identity.UserAccountCreated: 用户注册事件\n- identity.UserAccountAutoCreated: 自动创建用户事件\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")",
"Bash(git commit -m \"$(cat <<''EOF''\nfeat(authorization): 实现 Outbox 模式事件发布\n\n## 概述\n为 authorization-service 实现 Outbox 模式,确保数据库事务和 Kafka 事件发布的原子性。\n\n## 新增表\n- OutboxEvent: 事件暂存表,用于事务性事件发布\n\n## 新增组件\n- OutboxRepository: Outbox 事件持久化\n- OutboxPublisherService: 轮询发布未处理事件到 Kafka\n\n## 支持的事件\n- authorization-events: 授权角色创建/更新事件(省公司、市公司授权)\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")",
"Bash(git commit -m \"$(cat <<''EOF''\nfeat(reporting): 实现 Dashboard API 完整功能\n\n## 概述\n为 reporting-service 实现完整的 Dashboard API 端点,支持统计卡片、趋势图表、\n区域分布和最近活动等功能。\n\n## API 端点\n- GET /dashboard/stats: 获取统计卡片数据\n- GET /dashboard/charts: 获取趋势图表数据 (支持 7d/30d/90d 周期)\n- GET /dashboard/region: 获取区域分布数据\n- GET /dashboard/activities: 获取最近活动列表\n\n## 新增 DTO\n- DashboardStatsResponseDto: 统计卡片响应\n- DashboardTrendResponseDto: 趋势数据响应\n- DashboardRegionResponseDto: 区域分布响应\n- DashboardActivitiesResponseDto: 活动列表响应\n\n## Repository 层\n- IDashboardStatsSnapshotRepository: 统计快照接口\n- IDashboardTrendDataRepository: 趋势数据接口\n- ISystemActivityRepository: 系统活动接口\n\n## External Clients (已弃用)\n- AuthorizationServiceClient: 授权服务客户端\n- IdentityServiceClient: 身份服务客户端\n注已改为事件驱动架构这些客户端仅作为备用\n\n<><6E> Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")",
"Bash(git commit -m \"$(cat <<''EOF''\nfeat(admin-web): 实现 Dashboard 页面真实 API 接入\n\n## 概述\n将 admin-web Dashboard 页面从模拟数据改为真实 API 调用,\n使用 React Query 实现数据获取、缓存和自动刷新。\n\n## 新增文件\n- dashboardService.ts: Dashboard API 服务封装\n- useDashboard.ts: React Query hooks\n- dashboard.types.ts: Dashboard 类型定义\n\n## API 接入\n- /dashboard/stats: 统计卡片(总认种量、总用户数、省/市公司数)\n- /dashboard/charts: 趋势图表(支持 7d/30d/90d 周期切换)\n- /dashboard/region: 区域分布\n- /dashboard/activities: 最近活动\n\n## UI 优化\n- 添加加载骨架屏\n- 添加错误重试机制\n- 添加空数据提示\n- 优化图表周期切换交互\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")",
"Bash(npx next:*)",
"Bash(npx prisma validate:*)"
],
"deny": [],
"ask": []

View File

@ -0,0 +1,18 @@
-- CreateTable
CREATE TABLE "system_configs" (
"id" TEXT NOT NULL,
"key" VARCHAR(100) NOT NULL,
"value" TEXT NOT NULL,
"description" VARCHAR(255),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"updated_by" TEXT,
CONSTRAINT "system_configs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "system_configs_key_key" ON "system_configs"("key");
-- CreateIndex
CREATE INDEX "system_configs_key_idx" ON "system_configs"("key");

View File

@ -1,192 +1,210 @@
// =============================================================================
// Admin Service - Prisma Schema
// =============================================================================
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// =============================================================================
// App Version Management
// =============================================================================
model AppVersion {
id String @id @default(uuid())
platform Platform
versionCode Int // Android: versionCode, iOS: CFBundleVersion
versionName String // 用户可见版本号,如 "1.2.3"
buildNumber String // 构建号
downloadUrl String // APK/IPA 下载地址
fileSize BigInt // 文件大小(字节)
fileSha256 String // 文件 SHA-256 校验和
minOsVersion String? // 最低操作系统版本要求
changelog String // 更新日志
isForceUpdate Boolean @default(false) // 是否强制更新
isEnabled Boolean @default(true) // 是否启用
releaseDate DateTime? // 发布日期
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy String // 创建人ID
updatedBy String? // 更新人ID
@@index([platform, isEnabled])
@@index([platform, versionCode])
@@map("app_versions")
}
enum Platform {
ANDROID
IOS
}
// =============================================================================
// Notification System (通知系统)
// =============================================================================
/// 系统通知 - 管理员发布的公告/通知
model Notification {
id String @id @default(uuid())
title String // 通知标题
content String // 通知内容
type NotificationType // 通知类型
priority NotificationPriority @default(NORMAL) // 优先级
targetType TargetType @default(ALL) // 目标用户类型
imageUrl String? // 可选的图片URL
linkUrl String? // 可选的跳转链接
isEnabled Boolean @default(true) // 是否启用
publishedAt DateTime? // 发布时间null表示草稿
expiresAt DateTime? // 过期时间null表示永不过期
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy String // 创建人ID
// 用户已读记录
readRecords NotificationRead[]
@@index([isEnabled, publishedAt])
@@index([type])
@@map("notifications")
}
/// 用户已读记录
model NotificationRead {
id String @id @default(uuid())
notificationId String
userSerialNum String // 用户序列号
readAt DateTime @default(now())
notification Notification @relation(fields: [notificationId], references: [id], onDelete: Cascade)
@@unique([notificationId, userSerialNum])
@@index([userSerialNum])
@@map("notification_reads")
}
/// 通知类型
enum NotificationType {
SYSTEM // 系统通知
ACTIVITY // 活动通知
REWARD // 收益通知
UPGRADE // 升级通知
ANNOUNCEMENT // 公告
}
/// 通知优先级
enum NotificationPriority {
LOW
NORMAL
HIGH
URGENT
}
/// 目标用户类型
enum TargetType {
ALL // 所有用户
NEW_USER // 新用户
VIP // VIP用户
}
// =============================================================================
// User Query View (用户查询视图 - 通过 Kafka 事件同步)
// =============================================================================
/// 用户查询视图 - 本地物化视图,通过消费 Kafka 事件同步维护
/// 用于 admin-web 用户管理页面的查询,避免跨服务 HTTP 调用
model UserQueryView {
userId BigInt @id @map("user_id")
accountSequence String @unique @map("account_sequence") @db.VarChar(12)
// 基本信息 (来自 identity-service 事件)
nickname String? @db.VarChar(100)
avatarUrl String? @map("avatar_url") @db.Text
phoneNumberMasked String? @map("phone_number_masked") @db.VarChar(20) // 脱敏: 138****8888
// 推荐关系
inviterSequence String? @map("inviter_sequence") @db.VarChar(12)
// KYC 状态
kycStatus String @default("NOT_VERIFIED") @map("kyc_status") @db.VarChar(20)
// 认种统计 (来自 planting-service 事件)
personalAdoptionCount Int @default(0) @map("personal_adoption_count")
teamAddressCount Int @default(0) @map("team_address_count")
teamAdoptionCount Int @default(0) @map("team_adoption_count")
// 授权统计 (来自 authorization-service 事件)
provinceAdoptionCount Int @default(0) @map("province_adoption_count")
cityAdoptionCount Int @default(0) @map("city_adoption_count")
// 排名
leaderboardRank Int? @map("leaderboard_rank")
// 状态
status String @default("ACTIVE") @db.VarChar(20)
isOnline Boolean @default(false) @map("is_online")
// 时间戳
registeredAt DateTime @map("registered_at")
lastActiveAt DateTime? @map("last_active_at")
syncedAt DateTime @default(now()) @map("synced_at")
@@index([accountSequence])
@@index([nickname])
@@index([status])
@@index([registeredAt])
@@index([personalAdoptionCount])
@@index([inviterSequence])
@@map("user_query_view")
}
// =============================================================================
// Kafka Event Tracking (事件消费追踪)
// =============================================================================
/// 事件消费位置追踪 - 用于幂等性和断点续传
model EventConsumerOffset {
id BigInt @id @default(autoincrement())
consumerGroup String @map("consumer_group") @db.VarChar(100)
topic String @db.VarChar(100)
partition Int
offset BigInt
updatedAt DateTime @default(now()) @map("updated_at")
@@unique([consumerGroup, topic, partition])
@@map("event_consumer_offsets")
}
/// 已处理事件记录 - 用于幂等性检查
model ProcessedEvent {
id BigInt @id @default(autoincrement())
eventId String @unique @map("event_id") @db.VarChar(100)
eventType String @map("event_type") @db.VarChar(50)
processedAt DateTime @default(now()) @map("processed_at")
@@index([eventType])
@@index([processedAt])
@@map("processed_events")
}
// =============================================================================
// Admin Service - Prisma Schema
// =============================================================================
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// =============================================================================
// App Version Management
// =============================================================================
model AppVersion {
id String @id @default(uuid())
platform Platform
versionCode Int // Android: versionCode, iOS: CFBundleVersion
versionName String // 用户可见版本号,如 "1.2.3"
buildNumber String // 构建号
downloadUrl String // APK/IPA 下载地址
fileSize BigInt // 文件大小(字节)
fileSha256 String // 文件 SHA-256 校验和
minOsVersion String? // 最低操作系统版本要求
changelog String // 更新日志
isForceUpdate Boolean @default(false) // 是否强制更新
isEnabled Boolean @default(true) // 是否启用
releaseDate DateTime? // 发布日期
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy String // 创建人ID
updatedBy String? // 更新人ID
@@index([platform, isEnabled])
@@index([platform, versionCode])
@@map("app_versions")
}
enum Platform {
ANDROID
IOS
}
// =============================================================================
// Notification System (通知系统)
// =============================================================================
/// 系统通知 - 管理员发布的公告/通知
model Notification {
id String @id @default(uuid())
title String // 通知标题
content String // 通知内容
type NotificationType // 通知类型
priority NotificationPriority @default(NORMAL) // 优先级
targetType TargetType @default(ALL) // 目标用户类型
imageUrl String? // 可选的图片URL
linkUrl String? // 可选的跳转链接
isEnabled Boolean @default(true) // 是否启用
publishedAt DateTime? // 发布时间null表示草稿
expiresAt DateTime? // 过期时间null表示永不过期
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy String // 创建人ID
// 用户已读记录
readRecords NotificationRead[]
@@index([isEnabled, publishedAt])
@@index([type])
@@map("notifications")
}
/// 用户已读记录
model NotificationRead {
id String @id @default(uuid())
notificationId String
userSerialNum String // 用户序列号
readAt DateTime @default(now())
notification Notification @relation(fields: [notificationId], references: [id], onDelete: Cascade)
@@unique([notificationId, userSerialNum])
@@index([userSerialNum])
@@map("notification_reads")
}
/// 通知类型
enum NotificationType {
SYSTEM // 系统通知
ACTIVITY // 活动通知
REWARD // 收益通知
UPGRADE // 升级通知
ANNOUNCEMENT // 公告
}
/// 通知优先级
enum NotificationPriority {
LOW
NORMAL
HIGH
URGENT
}
/// 目标用户类型
enum TargetType {
ALL // 所有用户
NEW_USER // 新用户
VIP // VIP用户
}
// =============================================================================
// User Query View (用户查询视图 - 通过 Kafka 事件同步)
// =============================================================================
/// 用户查询视图 - 本地物化视图,通过消费 Kafka 事件同步维护
/// 用于 admin-web 用户管理页面的查询,避免跨服务 HTTP 调用
model UserQueryView {
userId BigInt @id @map("user_id")
accountSequence String @unique @map("account_sequence") @db.VarChar(12)
// 基本信息 (来自 identity-service 事件)
nickname String? @db.VarChar(100)
avatarUrl String? @map("avatar_url") @db.Text
phoneNumberMasked String? @map("phone_number_masked") @db.VarChar(20) // 脱敏: 138****8888
// 推荐关系
inviterSequence String? @map("inviter_sequence") @db.VarChar(12)
// KYC 状态
kycStatus String @default("NOT_VERIFIED") @map("kyc_status") @db.VarChar(20)
// 认种统计 (来自 planting-service 事件)
personalAdoptionCount Int @default(0) @map("personal_adoption_count")
teamAddressCount Int @default(0) @map("team_address_count")
teamAdoptionCount Int @default(0) @map("team_adoption_count")
// 授权统计 (来自 authorization-service 事件)
provinceAdoptionCount Int @default(0) @map("province_adoption_count")
cityAdoptionCount Int @default(0) @map("city_adoption_count")
// 排名
leaderboardRank Int? @map("leaderboard_rank")
// 状态
status String @default("ACTIVE") @db.VarChar(20)
isOnline Boolean @default(false) @map("is_online")
// 时间戳
registeredAt DateTime @map("registered_at")
lastActiveAt DateTime? @map("last_active_at")
syncedAt DateTime @default(now()) @map("synced_at")
@@index([accountSequence])
@@index([nickname])
@@index([status])
@@index([registeredAt])
@@index([personalAdoptionCount])
@@index([inviterSequence])
@@map("user_query_view")
}
// =============================================================================
// Kafka Event Tracking (事件消费追踪)
// =============================================================================
/// 事件消费位置追踪 - 用于幂等性和断点续传
model EventConsumerOffset {
id BigInt @id @default(autoincrement())
consumerGroup String @map("consumer_group") @db.VarChar(100)
topic String @db.VarChar(100)
partition Int
offset BigInt
updatedAt DateTime @default(now()) @map("updated_at")
@@unique([consumerGroup, topic, partition])
@@map("event_consumer_offsets")
}
/// 已处理事件记录 - 用于幂等性检查
model ProcessedEvent {
id BigInt @id @default(autoincrement())
eventId String @unique @map("event_id") @db.VarChar(100)
eventType String @map("event_type") @db.VarChar(50)
processedAt DateTime @default(now()) @map("processed_at")
@@index([eventType])
@@index([processedAt])
@@map("processed_events")
}
// =============================================================================
// System Config (系统配置)
// =============================================================================
/// 系统配置 - 键值对存储,用于存储各种系统设置
model SystemConfig {
id String @id @default(uuid())
key String @unique @db.VarChar(100) // 配置键
value String @db.Text // 配置值 (JSON 格式)
description String? @db.VarChar(255) // 配置描述
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
updatedBy String? @map("updated_by") // 更新人ID
@@index([key])
@@map("system_configs")
}

View File

@ -0,0 +1,202 @@
import {
Controller,
Get,
Put,
Body,
Param,
HttpCode,
HttpStatus,
Inject,
Query,
} from '@nestjs/common';
import {
SYSTEM_CONFIG_REPOSITORY,
ISystemConfigRepository,
} from '../../domain/repositories/system-config.repository';
/**
* -
*/
export const CONFIG_KEYS = {
// 是否允许未认种用户查看各省认种热度
ALLOW_NON_ADOPTER_VIEW_HEAT: 'display.allowNonAdopterViewHeat',
// 热度展示方式: 'count' | 'level'
HEAT_DISPLAY_MODE: 'display.heatDisplayMode',
} as const;
/**
* DTO
*/
interface SystemConfigResponseDto {
key: string;
value: string;
description?: string;
updatedAt: Date;
}
/**
* DTO
*/
interface DisplaySettingsResponseDto {
allowNonAdopterViewHeat: boolean;
heatDisplayMode: 'count' | 'level';
}
/**
* DTO
*/
interface UpdateDisplaySettingsDto {
allowNonAdopterViewHeat?: boolean;
heatDisplayMode?: 'count' | 'level';
}
/**
*
*/
@Controller('admin/system-config')
export class AdminSystemConfigController {
constructor(
@Inject(SYSTEM_CONFIG_REPOSITORY)
private readonly configRepo: ISystemConfigRepository,
) {}
/**
*
*/
@Get()
async getAll(): Promise<SystemConfigResponseDto[]> {
const configs = await this.configRepo.findAll();
return configs.map((c) => ({
key: c.key,
value: c.value,
description: c.description,
updatedAt: c.updatedAt,
}));
}
/**
*
*/
@Get(':key')
async getByKey(@Param('key') key: string): Promise<SystemConfigResponseDto | null> {
const config = await this.configRepo.findByKey(key);
if (!config) {
return null;
}
return {
key: config.key,
value: config.value,
description: config.description,
updatedAt: config.updatedAt,
};
}
/**
*
*/
@Get('display/settings')
async getDisplaySettings(): Promise<DisplaySettingsResponseDto> {
const configs = await this.configRepo.findByKeys([
CONFIG_KEYS.ALLOW_NON_ADOPTER_VIEW_HEAT,
CONFIG_KEYS.HEAT_DISPLAY_MODE,
]);
const configMap = new Map(configs.map((c) => [c.key, c.value]));
return {
allowNonAdopterViewHeat:
configMap.get(CONFIG_KEYS.ALLOW_NON_ADOPTER_VIEW_HEAT) === 'true',
heatDisplayMode:
(configMap.get(CONFIG_KEYS.HEAT_DISPLAY_MODE) as 'count' | 'level') || 'count',
};
}
/**
*
*/
@Put('display/settings')
@HttpCode(HttpStatus.OK)
async updateDisplaySettings(
@Body() dto: UpdateDisplaySettingsDto,
): Promise<DisplaySettingsResponseDto> {
const updates: Array<{ key: string; value: string; description?: string }> = [];
if (dto.allowNonAdopterViewHeat !== undefined) {
updates.push({
key: CONFIG_KEYS.ALLOW_NON_ADOPTER_VIEW_HEAT,
value: String(dto.allowNonAdopterViewHeat),
description: '是否允许未认种用户查看各省认种热度',
});
}
if (dto.heatDisplayMode !== undefined) {
updates.push({
key: CONFIG_KEYS.HEAT_DISPLAY_MODE,
value: dto.heatDisplayMode,
description: '热度展示方式: count=显示具体数量, level=仅显示热度等级',
});
}
if (updates.length > 0) {
await this.configRepo.batchUpsert(updates, 'admin');
}
// 返回更新后的配置
return this.getDisplaySettings();
}
/**
*
*/
@Put(':key')
@HttpCode(HttpStatus.OK)
async updateByKey(
@Param('key') key: string,
@Body() body: { value: string; description?: string },
): Promise<SystemConfigResponseDto> {
const config = await this.configRepo.upsert(
key,
body.value,
body.description,
'admin',
);
return {
key: config.key,
value: config.value,
description: config.description,
updatedAt: config.updatedAt,
};
}
}
/**
* /
* mobile-app
*/
@Controller('api/v1/system-config')
export class PublicSystemConfigController {
constructor(
@Inject(SYSTEM_CONFIG_REPOSITORY)
private readonly configRepo: ISystemConfigRepository,
) {}
/**
*
*/
@Get('display/settings')
async getDisplaySettings(): Promise<DisplaySettingsResponseDto> {
const configs = await this.configRepo.findByKeys([
CONFIG_KEYS.ALLOW_NON_ADOPTER_VIEW_HEAT,
CONFIG_KEYS.HEAT_DISPLAY_MODE,
]);
const configMap = new Map(configs.map((c) => [c.key, c.value]));
return {
allowNonAdopterViewHeat:
configMap.get(CONFIG_KEYS.ALLOW_NON_ADOPTER_VIEW_HEAT) === 'true',
heatDisplayMode:
(configMap.get(CONFIG_KEYS.HEAT_DISPLAY_MODE) as 'count' | 'level') || 'count',
};
}
}

View File

@ -148,6 +148,7 @@ export class UserController {
accountSequence: item.accountSequence,
avatar: item.avatarUrl,
nickname: item.nickname,
phoneNumberMasked: item.phoneNumberMasked,
personalAdoptions: item.personalAdoptionCount,
teamAddresses: item.teamAddressCount,
teamAdoptions: item.teamAdoptionCount,
@ -171,7 +172,6 @@ export class UserController {
return {
...listItem,
phoneNumberMasked: item.phoneNumberMasked,
kycStatus: item.kycStatus,
registeredAt: item.registeredAt.toISOString(),
lastActiveAt: item.lastActiveAt?.toISOString() || null,

View File

@ -6,6 +6,7 @@ export class UserListItemDto {
accountSequence!: string;
avatar!: string | null;
nickname!: string | null;
phoneNumberMasked!: string | null;
personalAdoptions!: number;
teamAddresses!: number;
teamAdoptions!: number;
@ -38,7 +39,6 @@ export class UserListResponseDto {
* DTO
*/
export class UserDetailDto extends UserListItemDto {
phoneNumberMasked!: string | null;
kycStatus!: string;
registeredAt!: string;
lastActiveAt!: string | null;

View File

@ -31,6 +31,10 @@ import { UserQueryRepositoryImpl } from './infrastructure/persistence/repositori
import { USER_QUERY_REPOSITORY } from './domain/repositories/user-query.repository';
import { UserController } from './api/controllers/user.controller';
import { UserEventConsumerService } from './infrastructure/kafka/user-event-consumer.service';
// System Config imports
import { SystemConfigRepositoryImpl } from './infrastructure/persistence/repositories/system-config.repository.impl';
import { SYSTEM_CONFIG_REPOSITORY } from './domain/repositories/system-config.repository';
import { AdminSystemConfigController, PublicSystemConfigController } from './api/controllers/system-config.controller';
@Module({
imports: [
@ -52,6 +56,8 @@ import { UserEventConsumerService } from './infrastructure/kafka/user-event-cons
AdminNotificationController,
MobileNotificationController,
UserController,
AdminSystemConfigController,
PublicSystemConfigController,
],
providers: [
PrismaService,
@ -84,6 +90,11 @@ import { UserEventConsumerService } from './infrastructure/kafka/user-event-cons
useClass: UserQueryRepositoryImpl,
},
UserEventConsumerService,
// System Config
{
provide: SYSTEM_CONFIG_REPOSITORY,
useClass: SystemConfigRepositoryImpl,
},
],
})
export class AppModule {}

View File

@ -0,0 +1,58 @@
/**
*
*/
export const SYSTEM_CONFIG_REPOSITORY = Symbol('SYSTEM_CONFIG_REPOSITORY');
export interface SystemConfigEntity {
id: string;
key: string;
value: string;
description?: string;
createdAt: Date;
updatedAt: Date;
updatedBy?: string;
}
export interface ISystemConfigRepository {
/**
*
*/
findByKey(key: string): Promise<SystemConfigEntity | null>;
/**
*
*/
findByKeys(keys: string[]): Promise<SystemConfigEntity[]>;
/**
*
*/
findAll(): Promise<SystemConfigEntity[]>;
/**
*
*/
upsert(
key: string,
value: string,
description?: string,
updatedBy?: string,
): Promise<SystemConfigEntity>;
/**
*
*/
batchUpsert(
configs: Array<{
key: string;
value: string;
description?: string;
}>,
updatedBy?: string,
): Promise<SystemConfigEntity[]>;
/**
*
*/
delete(key: string): Promise<void>;
}

View File

@ -0,0 +1,99 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import {
ISystemConfigRepository,
SystemConfigEntity,
} from '../../../domain/repositories/system-config.repository';
@Injectable()
export class SystemConfigRepositoryImpl implements ISystemConfigRepository {
constructor(private readonly prisma: PrismaService) {}
async findByKey(key: string): Promise<SystemConfigEntity | null> {
const config = await this.prisma.systemConfig.findUnique({
where: { key },
});
return config;
}
async findByKeys(keys: string[]): Promise<SystemConfigEntity[]> {
const configs = await this.prisma.systemConfig.findMany({
where: { key: { in: keys } },
});
return configs;
}
async findAll(): Promise<SystemConfigEntity[]> {
const configs = await this.prisma.systemConfig.findMany({
orderBy: { key: 'asc' },
});
return configs;
}
async upsert(
key: string,
value: string,
description?: string,
updatedBy?: string,
): Promise<SystemConfigEntity> {
const config = await this.prisma.systemConfig.upsert({
where: { key },
create: {
key,
value,
description,
updatedBy,
},
update: {
value,
description,
updatedBy,
},
});
return config;
}
async batchUpsert(
configs: Array<{
key: string;
value: string;
description?: string;
}>,
updatedBy?: string,
): Promise<SystemConfigEntity[]> {
const results: SystemConfigEntity[] = [];
// 使用事务确保批量操作的原子性
await this.prisma.$transaction(async (tx) => {
for (const config of configs) {
const result = await tx.systemConfig.upsert({
where: { key: config.key },
create: {
key: config.key,
value: config.value,
description: config.description,
updatedBy,
},
update: {
value: config.value,
description: config.description,
updatedBy,
},
});
results.push(result);
}
});
return results;
}
async delete(key: string): Promise<void> {
await this.prisma.systemConfig.delete({
where: { key },
});
}
}

View File

@ -26,6 +26,7 @@ import {
RevokeAuthorizationCommand,
GrantMonthlyBypassCommand,
ExemptLocalPercentageCheckCommand,
SelfApplyAuthorizationCommand,
} from '@/application/commands'
import {
ApplyCommunityAuthDto,
@ -33,6 +34,10 @@ import {
ApplyAuthCityDto,
RevokeAuthorizationDto,
GrantMonthlyBypassDto,
SelfApplyAuthorizationDto,
SelfApplyAuthorizationResponseDto,
UserAuthorizationStatusResponseDto,
SelfApplyAuthorizationType,
} from '@/api/dto/request'
import {
AuthorizationResponse,
@ -122,11 +127,12 @@ export class AuthorizationController {
@ApiQuery({ name: 'regionCode', description: '区域代码' })
@ApiResponse({ status: 200, type: [StickmanRankingResponse] })
async getStickmanRanking(
@CurrentUser() user: { userId: string; accountSequence: string },
@Query('month') month: string,
@Query('roleType') roleType: RoleType,
@Query('regionCode') regionCode: string,
): Promise<StickmanRankingResponse[]> {
return await this.applicationService.getStickmanRanking(month, roleType, regionCode)
return await this.applicationService.getStickmanRanking(month, roleType, regionCode, user.userId)
}
@Delete(':id')
@ -169,4 +175,44 @@ export class AuthorizationController {
const command = new ExemptLocalPercentageCheckCommand(id, user.accountSequence)
await this.applicationService.exemptLocalPercentageCheck(command)
}
// ============================================
// 自助申请授权相关接口
// ============================================
@Get('self-apply/status')
@ApiOperation({
summary: '获取用户授权申请状态',
description: '获取用户是否已认种、认种棵数、已有授权和待审核申请',
})
@ApiResponse({ status: 200, type: UserAuthorizationStatusResponseDto })
async getUserAuthorizationStatus(
@CurrentUser() user: { userId: string; accountSequence: string },
): Promise<UserAuthorizationStatusResponseDto> {
return await this.applicationService.getUserAuthorizationStatus(user.accountSequence)
}
@Post('self-apply')
@ApiOperation({
summary: '自助申请授权',
description: '用户自助申请社区、市团队或省团队授权(需先认种并上传办公室照片)',
})
@ApiResponse({ status: 201, type: SelfApplyAuthorizationResponseDto })
async selfApplyAuthorization(
@CurrentUser() user: { userId: string; accountSequence: string },
@Body() dto: SelfApplyAuthorizationDto,
): Promise<SelfApplyAuthorizationResponseDto> {
const command = new SelfApplyAuthorizationCommand(
user.userId,
user.accountSequence,
dto.type,
dto.officePhotoUrls,
dto.communityName,
dto.provinceCode,
dto.provinceName,
dto.cityCode,
dto.cityName,
)
return await this.applicationService.selfApplyAuthorization(command)
}
}

View File

@ -8,3 +8,4 @@ export * from './grant-auth-province-company.dto'
export * from './grant-auth-city-company.dto'
export * from './revoke-authorization.dto'
export * from './grant-monthly-bypass.dto'
export * from './self-apply-authorization.dto'

View File

@ -0,0 +1,121 @@
import {
IsString,
IsNotEmpty,
MaxLength,
IsEnum,
IsArray,
ArrayMinSize,
ArrayMaxSize,
IsUrl,
IsOptional,
} from 'class-validator'
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
/**
*
*
*
*/
export enum SelfApplyAuthorizationType {
COMMUNITY = 'COMMUNITY', // 社区
CITY_TEAM = 'CITY_TEAM', // 市团队
PROVINCE_TEAM = 'PROVINCE_TEAM', // 省团队
}
/**
* DTO
*/
export class SelfApplyAuthorizationDto {
@ApiProperty({
description: '申请类型',
enum: SelfApplyAuthorizationType,
example: SelfApplyAuthorizationType.COMMUNITY,
})
@IsEnum(SelfApplyAuthorizationType, { message: '无效的申请类型' })
type: SelfApplyAuthorizationType
@ApiPropertyOptional({ description: '社区名称(申请社区时必填)', example: '量子社区' })
@IsOptional()
@IsString()
@MaxLength(50, { message: '社区名称最大50字符' })
communityName?: string
@ApiPropertyOptional({ description: '省份代码(申请省团队时必填)', example: '440000' })
@IsOptional()
@IsString()
provinceCode?: string
@ApiPropertyOptional({ description: '省份名称(申请省团队时必填)', example: '广东省' })
@IsOptional()
@IsString()
@MaxLength(50)
provinceName?: string
@ApiPropertyOptional({ description: '城市代码(申请市团队时必填)', example: '440100' })
@IsOptional()
@IsString()
cityCode?: string
@ApiPropertyOptional({ description: '城市名称(申请市团队时必填)', example: '广州市' })
@IsOptional()
@IsString()
@MaxLength(50)
cityName?: string
@ApiProperty({
description: '办公室照片URL列表至少2张最多6张',
example: [
'https://storage.example.com/office1.jpg',
'https://storage.example.com/office2.jpg',
],
})
@IsArray()
@ArrayMinSize(2, { message: '请至少上传2张办公室照片' })
@ArrayMaxSize(6, { message: '最多上传6张办公室照片' })
@IsUrl({}, { each: true, message: '照片URL格式不正确' })
officePhotoUrls: string[]
}
/**
* DTO
*/
export class SelfApplyAuthorizationResponseDto {
@ApiProperty({ description: '申请ID' })
applicationId: string
@ApiProperty({ description: '申请状态', enum: ['PENDING', 'APPROVED', 'REJECTED'] })
status: 'PENDING' | 'APPROVED' | 'REJECTED'
@ApiProperty({ description: '申请类型' })
type: SelfApplyAuthorizationType
@ApiProperty({ description: '申请时间' })
appliedAt: Date
@ApiPropertyOptional({ description: '审核时间' })
reviewedAt?: Date
@ApiPropertyOptional({ description: '审核备注' })
reviewNote?: string
}
/**
* DTO
*/
export class UserAuthorizationStatusResponseDto {
@ApiProperty({ description: '是否已认种' })
hasPlanted: boolean
@ApiProperty({ description: '认种棵数' })
plantedCount: number
@ApiProperty({ description: '已拥有的授权类型列表' })
existingAuthorizations: string[]
@ApiProperty({ description: '待审核的申请列表' })
pendingApplications: {
applicationId: string
type: SelfApplyAuthorizationType
appliedAt: Date
}[]
}

View File

@ -75,6 +75,9 @@ export class ApplyAuthorizationResponse {
}
export class StickmanRankingResponse {
@ApiProperty({ description: '唯一标识授权ID' })
id: string
@ApiProperty({ description: '用户ID' })
userId: string
@ -87,12 +90,21 @@ export class StickmanRankingResponse {
@ApiProperty({ description: '区域代码' })
regionCode: string
@ApiPropertyOptional({ description: '昵称' })
nickname?: string
@ApiProperty({ description: '昵称' })
nickname: string
@ApiPropertyOptional({ description: '头像URL' })
avatarUrl?: string
@ApiProperty({ description: '完成数量(累计完成)' })
completedCount: number
@ApiProperty({ description: '本月可结算收益USDT' })
monthlyEarnings: number
@ApiProperty({ description: '是否是当前用户' })
isCurrentUser: boolean
@ApiProperty({ description: '排名' })
ranking: number

View File

@ -20,7 +20,7 @@ import {
import { RedisModule } from '@/infrastructure/redis/redis.module'
import { KafkaModule } from '@/infrastructure/kafka/kafka.module'
import { EventConsumerController } from '@/infrastructure/kafka/event-consumer.controller'
import { ReferralServiceClient } from '@/infrastructure/external/referral-service.client'
import { ReferralServiceClient, IdentityServiceClient } from '@/infrastructure/external'
// Application
import { AuthorizationApplicationService, REFERRAL_REPOSITORY, TEAM_STATISTICS_REPOSITORY } from '@/application/services'
@ -88,6 +88,7 @@ const MockReferralRepository = {
// External Service Clients (replaces mock)
ReferralServiceClient,
IdentityServiceClient,
{
provide: TEAM_STATISTICS_REPOSITORY,
useExisting: ReferralServiceClient,

View File

@ -9,3 +9,4 @@ export * from './grant-auth-city-company.command'
export * from './revoke-authorization.command'
export * from './grant-monthly-bypass.command'
export * from './exempt-percentage-check.command'
export * from './self-apply-authorization.command'

View File

@ -0,0 +1,18 @@
import { SelfApplyAuthorizationType } from '@/api/dto/request/self-apply-authorization.dto'
/**
*
*/
export class SelfApplyAuthorizationCommand {
constructor(
public readonly userId: string,
public readonly accountSequence: string,
public readonly type: SelfApplyAuthorizationType,
public readonly officePhotoUrls: string[],
public readonly communityName?: string,
public readonly provinceCode?: string,
public readonly provinceName?: string,
public readonly cityCode?: string,
public readonly cityName?: string,
) {}
}

View File

@ -21,12 +21,16 @@ export interface AuthorizationDTO {
}
export interface StickmanRankingDTO {
id: string // 授权ID用于前端唯一标识
userId: string
authorizationId: string
roleType: RoleType
regionCode: string
nickname?: string
avatarUrl?: string
nickname: string // 用户昵称
avatarUrl?: string // 头像URL
completedCount: number // 完成数量(累计完成)
monthlyEarnings: number // 本月可结算收益USDT
isCurrentUser: boolean // 是否是当前请求用户
ranking: number
isFirstPlace: boolean
cumulativeCompleted: number

View File

@ -22,7 +22,7 @@ import {
TeamStatistics,
} from '@/domain/services'
import { EventPublisherService } from '@/infrastructure/kafka'
import { ReferralServiceClient } from '@/infrastructure/external'
import { ReferralServiceClient, IdentityServiceClient } from '@/infrastructure/external'
import { ApplicationError, NotFoundError } from '@/shared/exceptions'
import {
ApplyCommunityAuthCommand,
@ -39,7 +39,13 @@ import {
RevokeAuthorizationCommand,
GrantMonthlyBypassCommand,
ExemptLocalPercentageCheckCommand,
SelfApplyAuthorizationCommand,
} from '@/application/commands'
import {
SelfApplyAuthorizationResponseDto,
UserAuthorizationStatusResponseDto,
SelfApplyAuthorizationType,
} from '@/api/dto/request/self-apply-authorization.dto'
import { AuthorizationDTO, StickmanRankingDTO, CommunityHierarchyDTO } from '@/application/dto'
export const REFERRAL_REPOSITORY = Symbol('IReferralRepository')
@ -61,6 +67,7 @@ export class AuthorizationApplicationService {
private readonly statsRepository: ITeamStatisticsRepository,
private readonly eventPublisher: EventPublisherService,
private readonly referralServiceClient: ReferralServiceClient,
private readonly identityServiceClient: IdentityServiceClient,
) {}
/**
@ -569,6 +576,7 @@ export class AuthorizationApplicationService {
month: string,
roleType: RoleType,
regionCode: string,
currentUserId?: string,
): Promise<StickmanRankingDTO[]> {
const assessments = await this.assessmentRepository.findRankingsByMonthAndRegion(
Month.create(month),
@ -576,15 +584,30 @@ export class AuthorizationApplicationService {
RegionCode.create(regionCode),
)
if (assessments.length === 0) {
return []
}
// 批量获取用户信息
const userIds = assessments.map(a => a.userId.value)
const userInfoMap = await this.identityServiceClient.batchGetUserInfo(userIds)
const rankings: StickmanRankingDTO[] = []
const finalTarget = LadderTargetRule.getFinalTarget(roleType)
for (const assessment of assessments) {
const userInfo = userInfoMap.get(assessment.userId.value)
rankings.push({
id: assessment.authorizationId.value,
userId: assessment.userId.value,
authorizationId: assessment.authorizationId.value,
roleType: assessment.roleType,
regionCode: assessment.regionCode.value,
nickname: userInfo?.nickname || `用户${assessment.userId.accountSequence.slice(-4)}`,
avatarUrl: userInfo?.avatarUrl,
completedCount: assessment.cumulativeCompleted,
monthlyEarnings: 0, // TODO: 从奖励服务获取本月可结算收益
isCurrentUser: currentUserId ? assessment.userId.value === currentUserId : false,
ranking: assessment.rankingInRegion || 0,
isFirstPlace: assessment.isFirstPlace,
cumulativeCompleted: assessment.cumulativeCompleted,
@ -597,6 +620,9 @@ export class AuthorizationApplicationService {
})
}
// 按完成数量降序排序
rankings.sort((a, b) => b.completedCount - a.completedCount)
return rankings
}
@ -2734,4 +2760,247 @@ export class AuthorizationApplicationService {
limit,
)
}
// ============================================
// 自助申请授权相关方法
// ============================================
/**
*
*/
async getUserAuthorizationStatus(
accountSequence: string,
): Promise<UserAuthorizationStatusResponseDto> {
this.logger.log(`[getUserAuthorizationStatus] accountSequence=${accountSequence}`)
// 1. 获取用户认种数据
const teamStats = await this.statsRepository.findByAccountSequence(accountSequence)
const hasPlanted = (teamStats?.personalPlantingCount || 0) > 0
const plantedCount = teamStats?.personalPlantingCount || 0
// 2. 获取用户已有的授权
const authorizations = await this.authorizationRepository.findByAccountSequence(accountSequence)
const existingAuthorizations = authorizations
.filter(auth => auth.status !== AuthorizationStatus.REVOKED)
.map(auth => this.mapRoleTypeToDisplayName(auth.roleType))
// 3. 获取待审核的申请(暂时返回空,待实现申请审核功能)
const pendingApplications: {
applicationId: string
type: SelfApplyAuthorizationType
appliedAt: Date
}[] = []
return {
hasPlanted,
plantedCount,
existingAuthorizations,
pendingApplications,
}
}
/**
*
*/
async selfApplyAuthorization(
command: SelfApplyAuthorizationCommand,
): Promise<SelfApplyAuthorizationResponseDto> {
this.logger.log(
`[selfApplyAuthorization] userId=${command.userId}, type=${command.type}, ` +
`photos=${command.officePhotoUrls.length}`,
)
// 1. 验证用户已认种
const teamStats = await this.statsRepository.findByAccountSequence(command.accountSequence)
const personalPlantingCount = teamStats?.personalPlantingCount || 0
if (personalPlantingCount <= 0) {
throw new ApplicationError('申请授权需要先完成认种')
}
// 2. 验证照片数量
if (command.officePhotoUrls.length < 2) {
throw new ApplicationError('请至少上传2张办公室照片')
}
if (command.officePhotoUrls.length > 6) {
throw new ApplicationError('最多上传6张办公室照片')
}
// 3. 根据申请类型处理
let result: SelfApplyAuthorizationResponseDto
switch (command.type) {
case SelfApplyAuthorizationType.COMMUNITY:
if (!command.communityName) {
throw new ApplicationError('申请社区授权需要提供社区名称')
}
result = await this.processCommunityApplication(command)
break
case SelfApplyAuthorizationType.CITY_TEAM:
if (!command.cityCode || !command.cityName) {
throw new ApplicationError('申请市团队授权需要提供城市信息')
}
result = await this.processCityTeamApplication(command)
break
case SelfApplyAuthorizationType.PROVINCE_TEAM:
if (!command.provinceCode || !command.provinceName) {
throw new ApplicationError('申请省团队授权需要提供省份信息')
}
result = await this.processProvinceTeamApplication(command)
break
default:
throw new ApplicationError('不支持的授权类型')
}
return result
}
/**
*
*/
private async processCommunityApplication(
command: SelfApplyAuthorizationCommand,
): Promise<SelfApplyAuthorizationResponseDto> {
// 检查是否已有社区授权
const existing = await this.authorizationRepository.findByAccountSequenceAndRoleType(
command.accountSequence,
RoleType.COMMUNITY,
)
if (existing && existing.status !== AuthorizationStatus.REVOKED) {
throw new ApplicationError('您已拥有社区授权')
}
// 直接创建授权(自助申请的社区授权直接生效)
const userId = UserId.create(command.userId, command.accountSequence)
const authorization = AuthorizationRole.createCommunityAuth({
userId,
communityName: command.communityName!,
})
// 检查初始考核
const teamStats = await this.statsRepository.findByAccountSequence(command.accountSequence)
const subordinateTreeCount = teamStats?.subordinateTeamPlantingCount || 0
if (subordinateTreeCount >= authorization.getInitialTarget()) {
authorization.activateBenefit()
}
await this.authorizationRepository.save(authorization)
await this.eventPublisher.publishAll(authorization.domainEvents)
authorization.clearDomainEvents()
return {
applicationId: authorization.id,
status: 'APPROVED',
type: SelfApplyAuthorizationType.COMMUNITY,
appliedAt: new Date(),
reviewedAt: new Date(),
reviewNote: '自助申请自动审核通过',
}
}
/**
*
*/
private async processCityTeamApplication(
command: SelfApplyAuthorizationCommand,
): Promise<SelfApplyAuthorizationResponseDto> {
// 检查是否已有该市的授权市公司
const existing = await this.authorizationRepository.findByRegionCodeAndRoleType(
RegionCode.create(command.cityCode!),
RoleType.AUTH_CITY_COMPANY,
)
if (existing && existing.status !== AuthorizationStatus.REVOKED) {
throw new ApplicationError('该市已有授权市公司')
}
// 创建授权市公司授权
const userId = UserId.create(command.userId, command.accountSequence)
const regionCode = RegionCode.create(command.cityCode!)
const authorization = AuthorizationRole.createAuthCityCompany({
userId,
regionCode,
cityName: command.cityName!,
})
// 检查初始考核100棵
const teamStats = await this.statsRepository.findByAccountSequence(command.accountSequence)
const subordinateTreeCount = teamStats?.subordinateTeamPlantingCount || 0
if (subordinateTreeCount >= authorization.getInitialTarget()) {
authorization.activateBenefit()
}
await this.authorizationRepository.save(authorization)
await this.eventPublisher.publishAll(authorization.domainEvents)
authorization.clearDomainEvents()
return {
applicationId: authorization.id,
status: 'APPROVED',
type: SelfApplyAuthorizationType.CITY_TEAM,
appliedAt: new Date(),
reviewedAt: new Date(),
reviewNote: '自助申请自动审核通过',
}
}
/**
*
*/
private async processProvinceTeamApplication(
command: SelfApplyAuthorizationCommand,
): Promise<SelfApplyAuthorizationResponseDto> {
// 检查是否已有该省的授权省公司
const existing = await this.authorizationRepository.findByRegionCodeAndRoleType(
RegionCode.create(command.provinceCode!),
RoleType.AUTH_PROVINCE_COMPANY,
)
if (existing && existing.status !== AuthorizationStatus.REVOKED) {
throw new ApplicationError('该省已有授权省公司')
}
// 创建授权省公司授权
const userId = UserId.create(command.userId, command.accountSequence)
const regionCode = RegionCode.create(command.provinceCode!)
const authorization = AuthorizationRole.createAuthProvinceCompany({
userId,
regionCode,
provinceName: command.provinceName!,
})
// 检查初始考核500棵
const teamStats = await this.statsRepository.findByAccountSequence(command.accountSequence)
const subordinateTreeCount = teamStats?.subordinateTeamPlantingCount || 0
if (subordinateTreeCount >= authorization.getInitialTarget()) {
authorization.activateBenefit()
}
await this.authorizationRepository.save(authorization)
await this.eventPublisher.publishAll(authorization.domainEvents)
authorization.clearDomainEvents()
return {
applicationId: authorization.id,
status: 'APPROVED',
type: SelfApplyAuthorizationType.PROVINCE_TEAM,
appliedAt: new Date(),
reviewedAt: new Date(),
reviewNote: '自助申请自动审核通过',
}
}
/**
* RoleType
*/
private mapRoleTypeToDisplayName(roleType: RoleType): string {
const mapping: Record<RoleType, string> = {
[RoleType.COMMUNITY]: '社区',
[RoleType.AUTH_CITY_COMPANY]: '市团队',
[RoleType.AUTH_PROVINCE_COMPANY]: '省团队',
[RoleType.CITY_COMPANY]: '市区域',
[RoleType.PROVINCE_COMPANY]: '省区域',
}
return mapping[roleType] || roleType
}
}

View File

@ -307,7 +307,7 @@ export class SystemAccountApplicationService implements OnModuleInit {
allocations.push({
targetType: 'COST_ACCOUNT',
targetSystemAccountId: costAccount.id,
amount: new Decimal(400).mul(treeCount),
amount: new Decimal(2800).mul(treeCount),
})
}
@ -315,7 +315,7 @@ export class SystemAccountApplicationService implements OnModuleInit {
allocations.push({
targetType: 'OPERATION_ACCOUNT',
targetSystemAccountId: operationAccount.id,
amount: new Decimal(300).mul(treeCount),
amount: new Decimal(2100).mul(treeCount),
})
}
@ -323,7 +323,7 @@ export class SystemAccountApplicationService implements OnModuleInit {
allocations.push({
targetType: 'HQ_COMMUNITY',
targetSystemAccountId: hqCommunityAccount.id,
amount: new Decimal(9).mul(treeCount),
amount: new Decimal(203).mul(treeCount),
})
}
@ -331,27 +331,27 @@ export class SystemAccountApplicationService implements OnModuleInit {
allocations.push({
targetType: 'RWAD_POOL',
targetSystemAccountId: rwadPoolAccount.id,
amount: new Decimal(800).mul(treeCount),
amount: new Decimal(5760).mul(treeCount),
})
}
// 2. 直接推荐人(500 USDT
// 2. 直接推荐人(3600 USDT
if (params.referrerId) {
allocations.push({
targetType: 'DIRECT_REFERRER',
targetUserId: params.referrerId,
amount: new Decimal(500).mul(treeCount),
amount: new Decimal(3600).mul(treeCount),
})
} else if (operationAccount) {
// 无推荐人,归入运营账户
allocations.push({
targetType: 'OPERATION_ACCOUNT',
targetSystemAccountId: operationAccount.id,
amount: new Decimal(500).mul(treeCount),
amount: new Decimal(3600).mul(treeCount),
})
}
// 3. 省公司权益15 区域 + 20 团队)
// 3. 省公司权益108 区域 + 144 团队)
// TODO: 从 authorization-service 查询省公司授权状态
// 暂时归入系统省账户
const systemProvince = await this.systemAccountRepository.getOrCreate(
@ -362,15 +362,15 @@ export class SystemAccountApplicationService implements OnModuleInit {
allocations.push({
targetType: 'PROVINCE_REGION',
targetSystemAccountId: systemProvince.id,
amount: new Decimal(15).mul(treeCount),
amount: new Decimal(108).mul(treeCount),
})
allocations.push({
targetType: 'PROVINCE_TEAM',
targetSystemAccountId: systemProvince.id,
amount: new Decimal(20).mul(treeCount),
amount: new Decimal(144).mul(treeCount),
})
// 4. 市公司权益(35 区域 + 40 团队)
// 4. 市公司权益(252 区域 + 288 团队)
// TODO: 从 authorization-service 查询市公司授权状态
// 暂时归入系统市账户
const systemCity = await this.systemAccountRepository.getOrCreate(
@ -381,22 +381,22 @@ export class SystemAccountApplicationService implements OnModuleInit {
allocations.push({
targetType: 'CITY_REGION',
targetSystemAccountId: systemCity.id,
amount: new Decimal(35).mul(treeCount),
amount: new Decimal(252).mul(treeCount),
})
allocations.push({
targetType: 'CITY_TEAM',
targetSystemAccountId: systemCity.id,
amount: new Decimal(40).mul(treeCount),
amount: new Decimal(288).mul(treeCount),
})
// 5. 社区权益(80 USDT
// 5. 社区权益(576 USDT
// TODO: 从 authorization-service 查询社区授权状态
// 暂时归入运营账户
if (operationAccount) {
allocations.push({
targetType: 'COMMUNITY',
targetSystemAccountId: operationAccount.id,
amount: new Decimal(80).mul(treeCount),
amount: new Decimal(576).mul(treeCount),
})
}

View File

@ -0,0 +1,99 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios, { AxiosInstance } from 'axios';
/**
*
*/
export interface UserInfo {
userId: string;
accountSequence: string;
nickname: string;
avatarUrl?: string;
}
/**
* Identity Service HTTP
* identity-service
*/
@Injectable()
export class IdentityServiceClient implements OnModuleInit {
private readonly logger = new Logger(IdentityServiceClient.name);
private httpClient: AxiosInstance;
private readonly baseUrl: string;
private readonly enabled: boolean;
constructor(private readonly configService: ConfigService) {
this.baseUrl = this.configService.get<string>('IDENTITY_SERVICE_URL') || 'http://identity-service:3001';
this.enabled = this.configService.get<boolean>('IDENTITY_SERVICE_ENABLED') !== false;
}
onModuleInit() {
this.httpClient = axios.create({
baseURL: this.baseUrl,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
this.logger.log(`[INIT] IdentityServiceClient initialized: ${this.baseUrl}, enabled: ${this.enabled}`);
}
/**
*
*/
async batchGetUserInfo(userIds: string[]): Promise<Map<string, UserInfo>> {
const result = new Map<string, UserInfo>();
if (!this.enabled || userIds.length === 0) {
return result;
}
try {
this.logger.debug(`[HTTP] POST /internal/users/batch - ${userIds.length} users`);
const response = await this.httpClient.post<UserInfo[]>(
`/api/v1/internal/users/batch`,
{ userIds },
);
if (response.data && Array.isArray(response.data)) {
for (const user of response.data) {
result.set(user.userId, user);
}
}
this.logger.debug(`[HTTP] Got ${result.size} users info`);
} catch (error) {
this.logger.error(`[HTTP] Failed to batch get user info:`, error);
}
return result;
}
/**
*
*/
async getUserInfo(userId: string): Promise<UserInfo | null> {
if (!this.enabled) {
return null;
}
try {
this.logger.debug(`[HTTP] GET /internal/users/${userId}`);
const response = await this.httpClient.get<UserInfo>(
`/api/v1/internal/users/${userId}`,
);
if (response.data) {
return response.data;
}
} catch (error) {
this.logger.error(`[HTTP] Failed to get user info for ${userId}:`, error);
}
return null;
}
}

View File

@ -1 +1,2 @@
export * from './referral-service.client';
export * from './identity-service.client';

View File

@ -64,7 +64,7 @@ export class FundAllocationDomainService {
throw new Error('订单未选择省市,无法计算资金分配');
}
// 1. 成本账户: 400 USDT/棵
// 1. 成本账户: 2800 USDT/棵
allocations.push(
new FundAllocation(
FundAllocationTargetType.COST_ACCOUNT,
@ -74,7 +74,7 @@ export class FundAllocationDomainService {
),
);
// 2. 运营账户: 300 USDT/棵
// 2. 运营账户: 2100 USDT/棵
allocations.push(
new FundAllocation(
FundAllocationTargetType.OPERATION_ACCOUNT,
@ -84,7 +84,7 @@ export class FundAllocationDomainService {
),
);
// 3. 总部社区: 9 USDT/棵
// 3. 总部社区: 203 USDT/棵
allocations.push(
new FundAllocation(
FundAllocationTargetType.HEADQUARTERS_COMMUNITY,
@ -95,7 +95,7 @@ export class FundAllocationDomainService {
),
);
// 4. 分享权益: 500 USDT/棵 (分配给推荐链)
// 4. 分享权益: 3600 USDT/棵 (分配给推荐链)
// 无推荐人时进入分享权益池 SYSTEM_SHARE_RIGHT_POOL
allocations.push(
new FundAllocation(
@ -109,7 +109,7 @@ export class FundAllocationDomainService {
),
);
// 5. 省区域权益: 15 USDT/棵
// 5. 省区域权益: 108 USDT/棵
allocations.push(
new FundAllocation(
FundAllocationTargetType.PROVINCE_AREA_RIGHTS,
@ -119,7 +119,7 @@ export class FundAllocationDomainService {
),
);
// 6. 省团队权益: 20 USDT/棵
// 6. 省团队权益: 144 USDT/棵
allocations.push(
new FundAllocation(
FundAllocationTargetType.PROVINCE_TEAM_RIGHTS,
@ -129,7 +129,7 @@ export class FundAllocationDomainService {
),
);
// 7. 市区域权益: 35 USDT/棵
// 7. 市区域权益: 252 USDT/棵
allocations.push(
new FundAllocation(
FundAllocationTargetType.CITY_AREA_RIGHTS,
@ -139,7 +139,7 @@ export class FundAllocationDomainService {
),
);
// 8. 市团队权益: 40 USDT/棵
// 8. 市团队权益: 288 USDT/棵
allocations.push(
new FundAllocation(
FundAllocationTargetType.CITY_TEAM_RIGHTS,
@ -149,7 +149,7 @@ export class FundAllocationDomainService {
),
);
// 9. 社区权益: 80 USDT/棵
// 9. 社区权益: 576 USDT/棵
allocations.push(
new FundAllocation(
FundAllocationTargetType.COMMUNITY_RIGHTS,
@ -159,7 +159,7 @@ export class FundAllocationDomainService {
),
);
// 10. RWAD底池: 800 USDT/棵
// 10. RWAD底池: 5760 USDT/棵
allocations.push(
new FundAllocation(
FundAllocationTargetType.RWAD_POOL,
@ -170,7 +170,7 @@ export class FundAllocationDomainService {
// 验证总额
const total = allocations.reduce((sum, a) => sum + a.amount, 0);
const expected = 2199 * treeCount;
const expected = 15831 * treeCount;
if (Math.abs(total - expected) > 0.01) {
throw new Error(`资金分配计算错误: 总额 ${total} != ${expected}`);
}
@ -201,7 +201,7 @@ export class FundAllocationDomainService {
throw new Error('订单未选择省市,无法计算资金分配');
}
// 1. 成本账户: 400 USDT/棵
// 1. 成本账户: 2800 USDT/棵
allocations.push(
new FundAllocation(
FundAllocationTargetType.COST_ACCOUNT,
@ -210,7 +210,7 @@ export class FundAllocationDomainService {
),
);
// 2. 运营账户: 300 USDT/棵
// 2. 运营账户: 2100 USDT/棵
allocations.push(
new FundAllocation(
FundAllocationTargetType.OPERATION_ACCOUNT,
@ -219,7 +219,7 @@ export class FundAllocationDomainService {
),
);
// 3. 总部社区: 9 USDT/棵
// 3. 总部社区: 203 USDT/棵
allocations.push(
new FundAllocation(
FundAllocationTargetType.HEADQUARTERS_COMMUNITY,
@ -228,7 +228,7 @@ export class FundAllocationDomainService {
),
);
// 4. 分享权益 (直推奖励): 500 USDT/棵
// 4. 分享权益 (直推奖励): 3600 USDT/棵
// 仅直推,无多级;无推荐人归入分享权益池
const referralTarget = context.directReferrerId
? `USER:${context.directReferrerId}`
@ -245,7 +245,7 @@ export class FundAllocationDomainService {
),
);
// 5. 省区域权益: 15 USDT/棵 + 1%算力(正式省公司才有算力)
// 5. 省区域权益: 108 USDT/棵 + 1%算力(正式省公司才有算力)
const provinceAreaTarget = context.provinceAreaRecipient.type === 'USER'
? `USER:${context.provinceAreaRecipient.id}`
: `SYSTEM:SYSTEM_PROVINCE:${selection.provinceCode}`;
@ -261,7 +261,7 @@ export class FundAllocationDomainService {
),
);
// 6. 省团队权益: 20 USDT/棵(授权省公司)
// 6. 省团队权益: 144 USDT/棵(授权省公司)
const provinceTeamTarget = context.provinceTeamRecipient.type === 'USER'
? `USER:${context.provinceTeamRecipient.id}`
: `SYSTEM:SYSTEM_PROVINCE:${selection.provinceCode}`;
@ -274,7 +274,7 @@ export class FundAllocationDomainService {
),
);
// 7. 市区域权益: 35 USDT/棵 + 2%算力(正式市公司才有算力)
// 7. 市区域权益: 252 USDT/棵 + 2%算力(正式市公司才有算力)
const cityAreaTarget = context.cityAreaRecipient.type === 'USER'
? `USER:${context.cityAreaRecipient.id}`
: `SYSTEM:SYSTEM_CITY:${selection.cityCode}`;
@ -290,7 +290,7 @@ export class FundAllocationDomainService {
),
);
// 8. 市团队权益: 40 USDT/棵(授权市公司)
// 8. 市团队权益: 288 USDT/棵(授权市公司)
const cityTeamTarget = context.cityTeamRecipient.type === 'USER'
? `USER:${context.cityTeamRecipient.id}`
: `SYSTEM:SYSTEM_CITY:${selection.cityCode}`;
@ -303,7 +303,7 @@ export class FundAllocationDomainService {
),
);
// 9. 社区权益: 80 USDT/棵
// 9. 社区权益: 576 USDT/棵
const communityTarget = context.communityRecipient && context.communityRecipient.type === 'USER'
? `USER:${context.communityRecipient.id}`
: 'SYSTEM:OPERATION_ACCOUNT';
@ -315,7 +315,7 @@ export class FundAllocationDomainService {
),
);
// 10. RWAD底池: 800 USDT/棵
// 10. RWAD底池: 5760 USDT/棵
allocations.push(
new FundAllocation(
FundAllocationTargetType.RWAD_POOL,
@ -327,7 +327,7 @@ export class FundAllocationDomainService {
// 验证总额
const total = allocations.reduce((sum, a) => sum + a.amount, 0);
const expected = 2199 * treeCount;
const expected = 15831 * treeCount;
if (Math.abs(total - expected) > 0.01) {
throw new Error(`资金分配计算错误: 总额 ${total} != ${expected}`);
}

View File

@ -1,33 +1,33 @@
export enum FundAllocationTargetType {
COST_ACCOUNT = 'COST_ACCOUNT', // 400 USDT - 成本账户
OPERATION_ACCOUNT = 'OPERATION_ACCOUNT', // 300 USDT - 运营账户
HEADQUARTERS_COMMUNITY = 'HEADQUARTERS_COMMUNITY', // 9 USDT - 总部社区
REFERRAL_RIGHTS = 'REFERRAL_RIGHTS', // 500 USDT - 分享权益
PROVINCE_AREA_RIGHTS = 'PROVINCE_AREA_RIGHTS', // 15 USDT - 省区域权益
PROVINCE_TEAM_RIGHTS = 'PROVINCE_TEAM_RIGHTS', // 20 USDT - 省团队权益
CITY_AREA_RIGHTS = 'CITY_AREA_RIGHTS', // 35 USDT - 市区域权益
CITY_TEAM_RIGHTS = 'CITY_TEAM_RIGHTS', // 40 USDT - 市团队权益
COMMUNITY_RIGHTS = 'COMMUNITY_RIGHTS', // 80 USDT - 社区权益
RWAD_POOL = 'RWAD_POOL', // 800 USDT - RWAD底池
COST_ACCOUNT = 'COST_ACCOUNT', // 2800 USDT - 成本账户
OPERATION_ACCOUNT = 'OPERATION_ACCOUNT', // 2100 USDT - 运营账户
HEADQUARTERS_COMMUNITY = 'HEADQUARTERS_COMMUNITY', // 203 USDT - 总部社区
REFERRAL_RIGHTS = 'REFERRAL_RIGHTS', // 3600 USDT - 分享权益
PROVINCE_AREA_RIGHTS = 'PROVINCE_AREA_RIGHTS', // 108 USDT - 省区域权益
PROVINCE_TEAM_RIGHTS = 'PROVINCE_TEAM_RIGHTS', // 144 USDT - 省团队权益
CITY_AREA_RIGHTS = 'CITY_AREA_RIGHTS', // 252 USDT - 市区域权益
CITY_TEAM_RIGHTS = 'CITY_TEAM_RIGHTS', // 288 USDT - 市团队权益
COMMUNITY_RIGHTS = 'COMMUNITY_RIGHTS', // 576 USDT - 社区权益
RWAD_POOL = 'RWAD_POOL', // 5760 USDT - RWAD底池
}
// 每棵树的资金分配规则 (总计 2199 USDT)
// 每棵树的资金分配规则 (总计 15831 USDT)
export const FUND_ALLOCATION_AMOUNTS: Record<FundAllocationTargetType, number> =
{
[FundAllocationTargetType.COST_ACCOUNT]: 400,
[FundAllocationTargetType.OPERATION_ACCOUNT]: 300,
[FundAllocationTargetType.HEADQUARTERS_COMMUNITY]: 9,
[FundAllocationTargetType.REFERRAL_RIGHTS]: 500,
[FundAllocationTargetType.PROVINCE_AREA_RIGHTS]: 15,
[FundAllocationTargetType.PROVINCE_TEAM_RIGHTS]: 20,
[FundAllocationTargetType.CITY_AREA_RIGHTS]: 35,
[FundAllocationTargetType.CITY_TEAM_RIGHTS]: 40,
[FundAllocationTargetType.COMMUNITY_RIGHTS]: 80,
[FundAllocationTargetType.RWAD_POOL]: 800,
[FundAllocationTargetType.COST_ACCOUNT]: 2800,
[FundAllocationTargetType.OPERATION_ACCOUNT]: 2100,
[FundAllocationTargetType.HEADQUARTERS_COMMUNITY]: 203,
[FundAllocationTargetType.REFERRAL_RIGHTS]: 3600,
[FundAllocationTargetType.PROVINCE_AREA_RIGHTS]: 108,
[FundAllocationTargetType.PROVINCE_TEAM_RIGHTS]: 144,
[FundAllocationTargetType.CITY_AREA_RIGHTS]: 252,
[FundAllocationTargetType.CITY_TEAM_RIGHTS]: 288,
[FundAllocationTargetType.COMMUNITY_RIGHTS]: 576,
[FundAllocationTargetType.RWAD_POOL]: 5760,
};
// 每棵树价格
export const PRICE_PER_TREE = 2199;
export const PRICE_PER_TREE = 15831;
// 验证总额
const TOTAL = Object.values(FUND_ALLOCATION_AMOUNTS).reduce((a, b) => a + b, 0);

View File

@ -85,7 +85,7 @@ export class RewardCalculationService {
/**
*
* 2199 USDT = 400 + 300 + 9 + 800 + 500 + 15 + 20 + 35 + 40 + 80
* 15831 USDT = 2800 + 2100 + 203 + 5760 + 3600 + 108 + 144 + 252 + 288 + 576
*/
async calculateRewards(params: {
sourceOrderNo: string; // 订单号是字符串格式
@ -103,10 +103,10 @@ export class RewardCalculationService {
const rewards: RewardLedgerEntry[] = [];
// ============================================
// 系统费用类 (709 USDT)
// 系统费用类 (10863 USDT)
// ============================================
// 1. 成本费 (400 USDT)
// 1. 成本费 (2800 USDT)
const costFeeReward = this.calculateCostFee(
params.sourceOrderNo,
params.sourceUserId,
@ -114,7 +114,7 @@ export class RewardCalculationService {
);
rewards.push(costFeeReward);
// 2. 运营费 (300 USDT)
// 2. 运营费 (2100 USDT)
const operationFeeReward = this.calculateOperationFee(
params.sourceOrderNo,
params.sourceUserId,
@ -122,7 +122,7 @@ export class RewardCalculationService {
);
rewards.push(operationFeeReward);
// 3. 总部社区基础费 (9 USDT)
// 3. 总部社区基础费 (203 USDT)
const headquartersBaseFeeReward = this.calculateHeadquartersBaseFee(
params.sourceOrderNo,
params.sourceUserId,
@ -130,7 +130,7 @@ export class RewardCalculationService {
);
rewards.push(headquartersBaseFeeReward);
// 4. RWAD底池注入 (800 USDT)
// 4. RWAD底池注入 (5760 USDT)
const rwadPoolReward = this.calculateRwadPoolInjection(
params.sourceOrderNo,
params.sourceUserId,
@ -139,10 +139,10 @@ export class RewardCalculationService {
rewards.push(rwadPoolReward);
// ============================================
// 用户权益类 (690 USDT + 算力)
// 用户权益类 (4968 USDT + 算力)
// ============================================
// 5. 分享权益 (500 USDT)
// 5. 分享权益 (3600 USDT)
const shareRewards = await this.calculateShareRights(
params.sourceOrderNo,
params.sourceUserId,
@ -151,7 +151,7 @@ export class RewardCalculationService {
);
rewards.push(...shareRewards);
// 6. 省团队权益 (20 USDT) - 可能返回多条记录(考核分配)
// 6. 省团队权益 (144 USDT) - 可能返回多条记录(考核分配)
const provinceTeamRewards = await this.calculateProvinceTeamRight(
params.sourceOrderNo,
params.sourceUserId,
@ -161,7 +161,7 @@ export class RewardCalculationService {
);
rewards.push(...provinceTeamRewards);
// 7. 省区域权益 (15 USDT + 1%算力) - 可能返回多条记录(考核分配)
// 7. 省区域权益 (108 USDT + 1%算力) - 可能返回多条记录(考核分配)
const provinceAreaRewards = await this.calculateProvinceAreaRight(
params.sourceOrderNo,
params.sourceUserId,
@ -170,7 +170,7 @@ export class RewardCalculationService {
);
rewards.push(...provinceAreaRewards);
// 8. 市团队权益 (40 USDT) - 可能返回多条记录(考核分配)
// 8. 市团队权益 (288 USDT) - 可能返回多条记录(考核分配)
const cityTeamRewards = await this.calculateCityTeamRight(
params.sourceOrderNo,
params.sourceUserId,
@ -180,7 +180,7 @@ export class RewardCalculationService {
);
rewards.push(...cityTeamRewards);
// 9. 市区域权益 (35 USDT + 2%算力) - 可能返回多条记录(考核分配)
// 9. 市区域权益 (252 USDT + 2%算力) - 可能返回多条记录(考核分配)
const cityAreaRewards = await this.calculateCityAreaRight(
params.sourceOrderNo,
params.sourceUserId,
@ -189,7 +189,7 @@ export class RewardCalculationService {
);
rewards.push(...cityAreaRewards);
// 10. 社区权益 (80 USDT) - 可能返回多条记录(考核分配)
// 10. 社区权益 (576 USDT) - 可能返回多条记录(考核分配)
const communityRewards = await this.calculateCommunityRight(
params.sourceOrderNo,
params.sourceUserId,
@ -210,7 +210,7 @@ export class RewardCalculationService {
// ============================================
/**
* (400 USDT)
* (2800 USDT)
*
*/
private calculateCostFee(
@ -239,7 +239,7 @@ export class RewardCalculationService {
}
/**
* (300 USDT)
* (2100 USDT)
*
*/
private calculateOperationFee(
@ -268,7 +268,7 @@ export class RewardCalculationService {
}
/**
* (9 USDT)
* (203 USDT)
*
*/
private calculateHeadquartersBaseFee(
@ -297,7 +297,7 @@ export class RewardCalculationService {
}
/**
* RWAD底池注入 (800 USDT)
* RWAD底池注入 (5760 USDT)
* RWAD 1
*/
private calculateRwadPoolInjection(
@ -321,7 +321,7 @@ export class RewardCalculationService {
rewardSource,
usdtAmount,
hashpowerAmount: hashpower,
memo: `RWAD底池注入来自用户${sourceUserId}的认种,${treeCount}棵树,800U注入1号底池`,
memo: `RWAD底池注入来自用户${sourceUserId}的认种,${treeCount}棵树,5760U注入1号底池`,
});
}
@ -330,7 +330,7 @@ export class RewardCalculationService {
// ============================================
/**
* (500 USDT)
* (3600 USDT)
*/
private async calculateShareRights(
sourceOrderNo: string,
@ -400,7 +400,7 @@ export class RewardCalculationService {
}
/**
* (20 USDT)
* (144 USDT)
* 500
*/
private async calculateProvinceTeamRight(
@ -456,7 +456,7 @@ export class RewardCalculationService {
}
/**
* (15 USDT + 1%)
* (108 USDT + 1%)
* 50000
* -
* -
@ -512,7 +512,7 @@ export class RewardCalculationService {
}
/**
* (40 USDT)
* (288 USDT)
* 100
*/
private async calculateCityTeamRight(
@ -568,7 +568,7 @@ export class RewardCalculationService {
}
/**
* (35 USDT + 2%)
* (252 USDT + 2%)
* 10000
* -
* -
@ -624,7 +624,7 @@ export class RewardCalculationService {
}
/**
* (80 USDT)
* (576 USDT)
*
* -
* - /

View File

@ -1,34 +1,34 @@
export enum RightType {
// === 系统费用类 ===
COST_FEE = 'COST_FEE', // 成本费 400U
OPERATION_FEE = 'OPERATION_FEE', // 运营费 300U
HEADQUARTERS_BASE_FEE = 'HEADQUARTERS_BASE_FEE', // 总部社区基础费 9U
RWAD_POOL_INJECTION = 'RWAD_POOL_INJECTION', // RWAD底池注入 800U
COST_FEE = 'COST_FEE', // 成本费 2800U
OPERATION_FEE = 'OPERATION_FEE', // 运营费 2100U
HEADQUARTERS_BASE_FEE = 'HEADQUARTERS_BASE_FEE', // 总部社区基础费 203U
RWAD_POOL_INJECTION = 'RWAD_POOL_INJECTION', // RWAD底池注入 5760U
// === 用户权益类 ===
SHARE_RIGHT = 'SHARE_RIGHT', // 分享权益 500U
PROVINCE_AREA_RIGHT = 'PROVINCE_AREA_RIGHT', // 省区域权益 15U + 1%算力
PROVINCE_TEAM_RIGHT = 'PROVINCE_TEAM_RIGHT', // 省团队权益 20U
CITY_AREA_RIGHT = 'CITY_AREA_RIGHT', // 市区域权益 35U + 2%算力
CITY_TEAM_RIGHT = 'CITY_TEAM_RIGHT', // 市团队权益 40U
COMMUNITY_RIGHT = 'COMMUNITY_RIGHT', // 社区权益 80U
SHARE_RIGHT = 'SHARE_RIGHT', // 分享权益 3600U
PROVINCE_AREA_RIGHT = 'PROVINCE_AREA_RIGHT', // 省区域权益 108U + 1%算力
PROVINCE_TEAM_RIGHT = 'PROVINCE_TEAM_RIGHT', // 省团队权益 144U
CITY_AREA_RIGHT = 'CITY_AREA_RIGHT', // 市区域权益 252U + 2%算力
CITY_TEAM_RIGHT = 'CITY_TEAM_RIGHT', // 市团队权益 288U
COMMUNITY_RIGHT = 'COMMUNITY_RIGHT', // 社区权益 576U
}
// 权益金额配置
export const RIGHT_AMOUNTS: Record<RightType, { usdt: number; hashpowerPercent: number }> = {
// 系统费用类
[RightType.COST_FEE]: { usdt: 400, hashpowerPercent: 0 },
[RightType.OPERATION_FEE]: { usdt: 300, hashpowerPercent: 0 },
[RightType.HEADQUARTERS_BASE_FEE]: { usdt: 9, hashpowerPercent: 0 },
[RightType.RWAD_POOL_INJECTION]: { usdt: 800, hashpowerPercent: 0 },
[RightType.COST_FEE]: { usdt: 2800, hashpowerPercent: 0 },
[RightType.OPERATION_FEE]: { usdt: 2100, hashpowerPercent: 0 },
[RightType.HEADQUARTERS_BASE_FEE]: { usdt: 203, hashpowerPercent: 0 },
[RightType.RWAD_POOL_INJECTION]: { usdt: 5760, hashpowerPercent: 0 },
// 用户权益类
[RightType.SHARE_RIGHT]: { usdt: 500, hashpowerPercent: 0 },
[RightType.PROVINCE_AREA_RIGHT]: { usdt: 15, hashpowerPercent: 1 },
[RightType.PROVINCE_TEAM_RIGHT]: { usdt: 20, hashpowerPercent: 0 },
[RightType.CITY_AREA_RIGHT]: { usdt: 35, hashpowerPercent: 2 },
[RightType.CITY_TEAM_RIGHT]: { usdt: 40, hashpowerPercent: 0 },
[RightType.COMMUNITY_RIGHT]: { usdt: 80, hashpowerPercent: 0 },
[RightType.SHARE_RIGHT]: { usdt: 3600, hashpowerPercent: 0 },
[RightType.PROVINCE_AREA_RIGHT]: { usdt: 108, hashpowerPercent: 1 },
[RightType.PROVINCE_TEAM_RIGHT]: { usdt: 144, hashpowerPercent: 0 },
[RightType.CITY_AREA_RIGHT]: { usdt: 252, hashpowerPercent: 2 },
[RightType.CITY_TEAM_RIGHT]: { usdt: 288, hashpowerPercent: 0 },
[RightType.COMMUNITY_RIGHT]: { usdt: 576, hashpowerPercent: 0 },
};
// 总金额验证: 400 + 300 + 9 + 800 + 500 + 15 + 20 + 35 + 40 + 80 = 2199 USDT
// 总金额验证: 2800 + 2100 + 203 + 5760 + 3600 + 108 + 144 + 252 + 288 + 576 = 15831 USDT

View File

@ -1,9 +1,10 @@
'use client';
import { useState } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { PageContainer } from '@/components/layout';
import { cn } from '@/utils/helpers';
import styles from './settings.module.scss';
import { systemConfigService, type DisplaySettings } from '@/services/systemConfigService';
/**
*
@ -84,6 +85,9 @@ export default function SettingsPage() {
// 前端展示设置
const [allowNonAdopterView, setAllowNonAdopterView] = useState(false);
const [heatDisplayMode, setHeatDisplayMode] = useState<'count' | 'level'>('count');
const [displaySettingsLoading, setDisplaySettingsLoading] = useState(false);
const [displaySettingsSaving, setDisplaySettingsSaving] = useState(false);
const [displaySettingsError, setDisplaySettingsError] = useState<string | null>(null);
// 后台账号与安全
const [approvalCount, setApprovalCount] = useState('3');
@ -91,6 +95,45 @@ export default function SettingsPage() {
const [logDate, setLogDate] = useState('');
const [logSearch, setLogSearch] = useState('');
// 加载前端展示设置
const loadDisplaySettings = useCallback(async () => {
setDisplaySettingsLoading(true);
setDisplaySettingsError(null);
try {
const settings = await systemConfigService.getDisplaySettings();
setAllowNonAdopterView(settings.allowNonAdopterViewHeat);
setHeatDisplayMode(settings.heatDisplayMode);
} catch (error) {
console.error('Failed to load display settings:', error);
setDisplaySettingsError('加载展示设置失败');
} finally {
setDisplaySettingsLoading(false);
}
}, []);
// 保存前端展示设置
const saveDisplaySettings = useCallback(async () => {
setDisplaySettingsSaving(true);
setDisplaySettingsError(null);
try {
await systemConfigService.updateDisplaySettings({
allowNonAdopterViewHeat: allowNonAdopterView,
heatDisplayMode: heatDisplayMode,
});
alert('展示设置保存成功');
} catch (error) {
console.error('Failed to save display settings:', error);
setDisplaySettingsError('保存展示设置失败');
} finally {
setDisplaySettingsSaving(false);
}
}, [allowNonAdopterView, heatDisplayMode]);
// 组件挂载时加载展示设置
useEffect(() => {
loadDisplaySettings();
}, [loadDisplaySettings]);
// 切换货币选择
const toggleCurrency = (currency: string) => {
if (settlementCurrencies.includes(currency)) {
@ -364,8 +407,26 @@ export default function SettingsPage() {
<section className={styles.settings__section}>
<div className={styles.settings__sectionHeader}>
<h2 className={styles.settings__sectionTitle}></h2>
<button className={styles.settings__resetBtn}></button>
<div className={styles.settings__sectionActions}>
<button
className={styles.settings__resetBtn}
onClick={loadDisplaySettings}
disabled={displaySettingsLoading}
>
{displaySettingsLoading ? '加载中...' : '刷新'}
</button>
<button
className={styles.settings__saveBtn}
onClick={saveDisplaySettings}
disabled={displaySettingsSaving || displaySettingsLoading}
>
{displaySettingsSaving ? '保存中...' : '保存展示设置'}
</button>
</div>
</div>
{displaySettingsError && (
<div className={styles.settings__error}>{displaySettingsError}</div>
)}
<div className={styles.settings__content}>
<div className={styles.settings__toggleRow}>
<span className={styles.settings__toggleLabel}></span>

View File

@ -95,6 +95,28 @@
&:hover {
text-decoration: underline;
}
&:disabled {
color: #94a3b8;
cursor: not-allowed;
}
}
.settings__sectionActions {
display: flex;
align-items: center;
gap: 12px;
}
.settings__error {
align-self: stretch;
padding: 12px;
border-radius: 6px;
background-color: #fef2f2;
border: 1px solid #fecaca;
color: #dc2626;
font-size: 14px;
line-height: 20px;
}
/* 设置内容区域 */

View File

@ -13,7 +13,7 @@ import styles from './users.module.scss';
// 骨架屏组件
const TableRowSkeleton = () => (
<div className={styles.users__tableRow}>
{Array.from({ length: 12 }).map((_, i) => (
{Array.from({ length: 13 }).map((_, i) => (
<div key={i} className={styles.users__tableCellSkeleton}>
<div className={styles.users__skeleton} />
</div>
@ -350,6 +350,9 @@ export default function UsersPage() {
<div className={cn(styles.users__tableHeaderCell, styles['users__tableHeaderCell--nickname'])}>
<b></b>
</div>
<div className={cn(styles.users__tableHeaderCell, styles['users__tableHeaderCell--phone'])}>
<b></b>
</div>
<div className={cn(styles.users__tableHeaderCell, styles['users__tableHeaderCell--adoptions'])}>
<b></b>
</div>
@ -437,6 +440,11 @@ export default function UsersPage() {
{user.nickname || '-'}
</div>
{/* 手机号 */}
<div className={cn(styles.users__tableCell, styles['users__tableCell--phone'])}>
{user.phoneNumberMasked || '-'}
</div>
{/* 账户认种量 */}
<div className={cn(styles.users__tableCell, styles['users__tableCell--adoptions'])}>
{formatNumber(user.personalAdoptions)}

View File

@ -253,6 +253,11 @@
flex-shrink: 0;
}
&--phone {
width: 100px;
flex-shrink: 0;
}
&--adoptions {
width: 70px;
flex-shrink: 0;
@ -358,6 +363,13 @@
font-weight: 500;
}
&--phone {
width: 100px;
flex-shrink: 0;
font-family: 'Consolas', monospace;
color: #6b7280;
}
&--adoptions {
width: 70px;
flex-shrink: 0;

View File

@ -0,0 +1,71 @@
/**
*
* API调用
*/
import apiClient from '@/infrastructure/api/client';
import { API_ENDPOINTS } from '@/infrastructure/api/endpoints';
/** 前端展示设置 */
export interface DisplaySettings {
/** 是否允许未认种用户查看各省认种热度 */
allowNonAdopterViewHeat: boolean;
/** 热度展示方式: 'count' 显示具体数量, 'level' 仅显示热度等级 */
heatDisplayMode: 'count' | 'level';
}
/** 更新前端展示设置请求 */
export interface UpdateDisplaySettingsRequest {
allowNonAdopterViewHeat?: boolean;
heatDisplayMode?: 'count' | 'level';
}
/** 系统配置项 */
export interface SystemConfigItem {
key: string;
value: string;
description?: string;
updatedAt: string;
}
/**
*
*/
export const systemConfigService = {
/**
*
*/
async getAllConfigs(): Promise<SystemConfigItem[]> {
return apiClient.get(API_ENDPOINTS.SYSTEM_CONFIG.ALL);
},
/**
*
*/
async getDisplaySettings(): Promise<DisplaySettings> {
return apiClient.get(API_ENDPOINTS.SYSTEM_CONFIG.DISPLAY_SETTINGS);
},
/**
*
*/
async updateDisplaySettings(settings: UpdateDisplaySettingsRequest): Promise<DisplaySettings> {
return apiClient.put(API_ENDPOINTS.SYSTEM_CONFIG.DISPLAY_SETTINGS, settings);
},
/**
*
*/
async getConfigByKey(key: string): Promise<SystemConfigItem | null> {
return apiClient.get(API_ENDPOINTS.SYSTEM_CONFIG.BY_KEY(key));
},
/**
*
*/
async updateConfigByKey(key: string, value: string, description?: string): Promise<SystemConfigItem> {
return apiClient.put(API_ENDPOINTS.SYSTEM_CONFIG.BY_KEY(key), { value, description });
},
};
export default systemConfigService;

View File

@ -13,6 +13,7 @@ export interface UserListItem {
accountSequence: string;
avatar: string | null;
nickname: string | null;
phoneNumberMasked: string | null;
personalAdoptions: number;
teamAddresses: number;
teamAdoptions: number;
@ -32,7 +33,6 @@ export interface UserListItem {
/** 用户详情 */
export interface UserDetail extends UserListItem {
phoneNumberMasked: string | null;
kycStatus: string;
registeredAt: string;
lastActiveAt: string | null;

View File

@ -0,0 +1,286 @@
{
"v": "5.7.4",
"fr": 30,
"ip": 0,
"op": 30,
"w": 100,
"h": 120,
"nm": "Stickman Running",
"ddd": 0,
"assets": [],
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "Head",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100 },
"r": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [50, 20, 0] },
"a": { "a": 0, "k": [0, 0, 0] },
"s": { "a": 0, "k": [100, 100, 100] }
},
"ao": 0,
"shapes": [
{
"ty": "el",
"s": { "a": 0, "k": [20, 20] },
"p": { "a": 0, "k": [0, 0] },
"nm": "Head Circle"
},
{
"ty": "st",
"c": { "a": 0, "k": [0.83, 0.69, 0.22, 1] },
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 3 },
"lc": 2,
"lj": 2
},
{
"ty": "fl",
"c": { "a": 0, "k": [0.83, 0.69, 0.22, 1] },
"o": { "a": 0, "k": 100 }
}
],
"ip": 0,
"op": 30,
"st": 0
},
{
"ddd": 0,
"ind": 2,
"ty": 4,
"nm": "Body",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100 },
"r": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [50, 50, 0] },
"a": { "a": 0, "k": [0, 0, 0] },
"s": { "a": 0, "k": [100, 100, 100] }
},
"ao": 0,
"shapes": [
{
"ty": "sh",
"ks": {
"a": 0,
"k": {
"i": [[0, 0], [0, 0]],
"o": [[0, 0], [0, 0]],
"v": [[0, -20], [0, 15]],
"c": false
}
},
"nm": "Body Line"
},
{
"ty": "st",
"c": { "a": 0, "k": [0.83, 0.69, 0.22, 1] },
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 3 },
"lc": 2,
"lj": 2
}
],
"ip": 0,
"op": 30,
"st": 0
},
{
"ddd": 0,
"ind": 3,
"ty": 4,
"nm": "Left Arm",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100 },
"r": {
"a": 1,
"k": [
{ "i": { "x": [0.5], "y": [1] }, "o": { "x": [0.5], "y": [0] }, "t": 0, "s": [-30] },
{ "i": { "x": [0.5], "y": [1] }, "o": { "x": [0.5], "y": [0] }, "t": 15, "s": [30] },
{ "t": 30, "s": [-30] }
]
},
"p": { "a": 0, "k": [50, 38, 0] },
"a": { "a": 0, "k": [0, 0, 0] },
"s": { "a": 0, "k": [100, 100, 100] }
},
"ao": 0,
"shapes": [
{
"ty": "sh",
"ks": {
"a": 0,
"k": {
"i": [[0, 0], [0, 0]],
"o": [[0, 0], [0, 0]],
"v": [[0, 0], [-15, 15]],
"c": false
}
},
"nm": "Left Arm Line"
},
{
"ty": "st",
"c": { "a": 0, "k": [0.83, 0.69, 0.22, 1] },
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 3 },
"lc": 2,
"lj": 2
}
],
"ip": 0,
"op": 30,
"st": 0
},
{
"ddd": 0,
"ind": 4,
"ty": 4,
"nm": "Right Arm",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100 },
"r": {
"a": 1,
"k": [
{ "i": { "x": [0.5], "y": [1] }, "o": { "x": [0.5], "y": [0] }, "t": 0, "s": [30] },
{ "i": { "x": [0.5], "y": [1] }, "o": { "x": [0.5], "y": [0] }, "t": 15, "s": [-30] },
{ "t": 30, "s": [30] }
]
},
"p": { "a": 0, "k": [50, 38, 0] },
"a": { "a": 0, "k": [0, 0, 0] },
"s": { "a": 0, "k": [100, 100, 100] }
},
"ao": 0,
"shapes": [
{
"ty": "sh",
"ks": {
"a": 0,
"k": {
"i": [[0, 0], [0, 0]],
"o": [[0, 0], [0, 0]],
"v": [[0, 0], [15, 15]],
"c": false
}
},
"nm": "Right Arm Line"
},
{
"ty": "st",
"c": { "a": 0, "k": [0.83, 0.69, 0.22, 1] },
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 3 },
"lc": 2,
"lj": 2
}
],
"ip": 0,
"op": 30,
"st": 0
},
{
"ddd": 0,
"ind": 5,
"ty": 4,
"nm": "Left Leg",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100 },
"r": {
"a": 1,
"k": [
{ "i": { "x": [0.5], "y": [1] }, "o": { "x": [0.5], "y": [0] }, "t": 0, "s": [30] },
{ "i": { "x": [0.5], "y": [1] }, "o": { "x": [0.5], "y": [0] }, "t": 15, "s": [-30] },
{ "t": 30, "s": [30] }
]
},
"p": { "a": 0, "k": [50, 65, 0] },
"a": { "a": 0, "k": [0, 0, 0] },
"s": { "a": 0, "k": [100, 100, 100] }
},
"ao": 0,
"shapes": [
{
"ty": "sh",
"ks": {
"a": 0,
"k": {
"i": [[0, 0], [0, 0]],
"o": [[0, 0], [0, 0]],
"v": [[0, 0], [-10, 30]],
"c": false
}
},
"nm": "Left Leg Line"
},
{
"ty": "st",
"c": { "a": 0, "k": [0.83, 0.69, 0.22, 1] },
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 3 },
"lc": 2,
"lj": 2
}
],
"ip": 0,
"op": 30,
"st": 0
},
{
"ddd": 0,
"ind": 6,
"ty": 4,
"nm": "Right Leg",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100 },
"r": {
"a": 1,
"k": [
{ "i": { "x": [0.5], "y": [1] }, "o": { "x": [0.5], "y": [0] }, "t": 0, "s": [-30] },
{ "i": { "x": [0.5], "y": [1] }, "o": { "x": [0.5], "y": [0] }, "t": 15, "s": [30] },
{ "t": 30, "s": [-30] }
]
},
"p": { "a": 0, "k": [50, 65, 0] },
"a": { "a": 0, "k": [0, 0, 0] },
"s": { "a": 0, "k": [100, 100, 100] }
},
"ao": 0,
"shapes": [
{
"ty": "sh",
"ks": {
"a": 0,
"k": {
"i": [[0, 0], [0, 0]],
"o": [[0, 0], [0, 0]],
"v": [[0, 0], [10, 30]],
"c": false
}
},
"nm": "Right Leg Line"
},
{
"ty": "st",
"c": { "a": 0, "k": [0.83, 0.69, 0.22, 1] },
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 3 },
"lc": 2,
"lj": 2
}
],
"ip": 0,
"op": 30,
"st": 0
}
],
"markers": []
}

View File

@ -79,10 +79,15 @@ class ApiEndpoints {
static const String authorizations = '/authorizations';
static const String myAuthorizations = '$authorizations/my'; //
static const String myCommunityHierarchy = '$authorizations/my/community-hierarchy'; //
static const String stickmanRanking = '$authorizations/ranking/stickman'; //
// Telemetry (-> Reporting Service)
static const String telemetry = '/telemetry';
static const String telemetrySession = '$telemetry/session';
static const String telemetryHeartbeat = '$telemetry/heartbeat';
static const String telemetryEvents = '$telemetry/events';
// System Config (-> Admin Service)
static const String systemConfig = '/system-config';
static const String displaySettings = '$systemConfig/display/settings';
}

View File

@ -11,6 +11,7 @@ import '../services/wallet_service.dart';
import '../services/planting_service.dart';
import '../services/reward_service.dart';
import '../services/notification_service.dart';
import '../services/system_config_service.dart';
// Storage Providers
final secureStorageProvider = Provider<SecureStorage>((ref) {
@ -86,6 +87,12 @@ final notificationServiceProvider = Provider<NotificationService>((ref) {
return NotificationService(apiClient: apiClient);
});
// System Config Service Provider ( admin-service)
final systemConfigServiceProvider = Provider<SystemConfigService>((ref) {
final apiClient = ref.watch(apiClientProvider);
return SystemConfigService(apiClient: apiClient);
});
// Override provider with initialized instance
ProviderContainer createProviderContainer(LocalStorage localStorage) {
return ProviderContainer(

View File

@ -179,6 +179,43 @@ String _maskAddress(String? address) {
return '${address.substring(0, 6)}...${address.substring(address.length - 4)}';
}
///
enum SmsCodeType {
register, //
login, //
bind, //
recover, //
}
/// ()
class PhoneAuthResponse {
final String userId;
final String accountSequence; //
final String accessToken;
final String refreshToken;
PhoneAuthResponse({
required this.userId,
required this.accountSequence,
required this.accessToken,
required this.refreshToken,
});
factory PhoneAuthResponse.fromJson(Map<String, dynamic> json) {
debugPrint('[AccountService] 解析 PhoneAuthResponse: ${json.keys.toList()}');
return PhoneAuthResponse(
userId: json['userId'] as String,
accountSequence: json['accountSequence']?.toString() ?? '',
accessToken: json['accessToken'] as String,
refreshToken: json['refreshToken'] as String,
);
}
@override
String toString() =>
'PhoneAuthResponse(userId: $userId, accountSequence: $accountSequence)';
}
///
class RecoverAccountResponse {
final String userId;
@ -1252,4 +1289,327 @@ class AccountService {
return null;
}
}
// ==================== / ====================
/// /
///
/// [phoneNumber] -
/// [smsCode] - 6
/// [type] -
Future<void> verifySmsCode({
required String phoneNumber,
required String smsCode,
required SmsCodeType type,
}) async {
debugPrint('$_tag verifySmsCode() - 验证短信验证码');
debugPrint('$_tag verifySmsCode() - 手机号: ${_maskPhoneNumber(phoneNumber)}, 类型: ${type.name}');
try {
final typeStr = type.name.toUpperCase();
debugPrint('$_tag verifySmsCode() - 调用 POST /user/verify-sms-code');
final response = await _apiClient.post(
'/user/verify-sms-code',
data: {
'phoneNumber': phoneNumber,
'smsCode': smsCode,
'type': typeStr,
},
);
debugPrint('$_tag verifySmsCode() - API 响应状态码: ${response.statusCode}');
//
await _secureStorage.write(
key: StorageKeys.phoneNumber,
value: phoneNumber,
);
debugPrint('$_tag verifySmsCode() - 短信验证码验证成功');
} on ApiException catch (e) {
debugPrint('$_tag verifySmsCode() - API 异常: $e');
rethrow;
} catch (e, stackTrace) {
debugPrint('$_tag verifySmsCode() - 未知异常: $e');
debugPrint('$_tag verifySmsCode() - 堆栈: $stackTrace');
throw ApiException('验证码验证失败: $e');
}
}
///
///
/// [phoneNumber] - 111
/// [type] -
Future<void> sendSmsCode(String phoneNumber, SmsCodeType type) async {
debugPrint('$_tag sendSmsCode() - 发送短信验证码');
debugPrint('$_tag sendSmsCode() - 手机号: ${_maskPhoneNumber(phoneNumber)}, 类型: ${type.name}');
try {
//
final typeStr = type.name.toUpperCase();
debugPrint('$_tag sendSmsCode() - 调用 POST /user/send-sms-code');
final response = await _apiClient.post(
'/user/send-sms-code',
data: {
'phoneNumber': phoneNumber,
'type': typeStr,
},
);
debugPrint('$_tag sendSmsCode() - API 响应状态码: ${response.statusCode}');
debugPrint('$_tag sendSmsCode() - 短信验证码已发送');
} on ApiException catch (e) {
debugPrint('$_tag sendSmsCode() - API 异常: $e');
rethrow;
} catch (e, stackTrace) {
debugPrint('$_tag sendSmsCode() - 未知异常: $e');
debugPrint('$_tag sendSmsCode() - 堆栈: $stackTrace');
throw ApiException('发送验证码失败: $e');
}
}
///
///
/// [phoneNumber] -
/// [smsCode] - 6
/// [inviterReferralCode] -
Future<PhoneAuthResponse> registerByPhone({
required String phoneNumber,
required String smsCode,
String? inviterReferralCode,
}) async {
debugPrint('$_tag registerByPhone() - 开始手机号注册');
debugPrint('$_tag registerByPhone() - 手机号: ${_maskPhoneNumber(phoneNumber)}');
debugPrint('$_tag registerByPhone() - 邀请码: ${inviterReferralCode ?? ""}');
try {
// ID
final deviceId = await getDeviceId();
debugPrint('$_tag registerByPhone() - 获取设备ID成功');
//
final deviceInfo = await getDeviceHardwareInfo();
debugPrint('$_tag registerByPhone() - 获取设备硬件信息成功');
final deviceName = '${deviceInfo.brand ?? ''} ${deviceInfo.model ?? ''}'.trim();
// API
debugPrint('$_tag registerByPhone() - 调用 POST /user/register');
final response = await _apiClient.post(
'/user/register',
data: {
'phoneNumber': phoneNumber,
'smsCode': smsCode,
'deviceId': deviceId,
'deviceName': deviceName.isEmpty ? null : deviceName,
if (inviterReferralCode != null) 'inviterReferralCode': inviterReferralCode,
},
);
debugPrint('$_tag registerByPhone() - API 响应状态码: ${response.statusCode}');
if (response.data == null) {
debugPrint('$_tag registerByPhone() - 错误: API 返回空响应');
throw const ApiException('注册失败: 空响应');
}
debugPrint('$_tag registerByPhone() - 解析响应数据');
final responseData = response.data as Map<String, dynamic>;
final data = responseData['data'] as Map<String, dynamic>;
final result = PhoneAuthResponse.fromJson(data);
debugPrint('$_tag registerByPhone() - 解析成功: $result');
//
debugPrint('$_tag registerByPhone() - 保存账号数据');
await _savePhoneAuthData(result, deviceId, phoneNumber);
debugPrint('$_tag registerByPhone() - 手机号注册完成');
return result;
} on ApiException catch (e) {
debugPrint('$_tag registerByPhone() - API 异常: $e');
rethrow;
} catch (e, stackTrace) {
debugPrint('$_tag registerByPhone() - 未知异常: $e');
debugPrint('$_tag registerByPhone() - 堆栈: $stackTrace');
throw ApiException('注册失败: $e');
}
}
///
///
/// [phoneNumber] -
/// [smsCode] - 6
Future<PhoneAuthResponse> loginByPhone({
required String phoneNumber,
required String smsCode,
}) async {
debugPrint('$_tag loginByPhone() - 开始手机号登录');
debugPrint('$_tag loginByPhone() - 手机号: ${_maskPhoneNumber(phoneNumber)}');
try {
// ID
final deviceId = await getDeviceId();
debugPrint('$_tag loginByPhone() - 获取设备ID成功');
// API
debugPrint('$_tag loginByPhone() - 调用 POST /user/login');
final response = await _apiClient.post(
'/user/login',
data: {
'phoneNumber': phoneNumber,
'smsCode': smsCode,
'deviceId': deviceId,
},
);
debugPrint('$_tag loginByPhone() - API 响应状态码: ${response.statusCode}');
if (response.data == null) {
debugPrint('$_tag loginByPhone() - 错误: API 返回空响应');
throw const ApiException('登录失败: 空响应');
}
debugPrint('$_tag loginByPhone() - 解析响应数据');
final responseData = response.data as Map<String, dynamic>;
final data = responseData['data'] as Map<String, dynamic>;
final result = PhoneAuthResponse.fromJson(data);
debugPrint('$_tag loginByPhone() - 解析成功: $result');
//
debugPrint('$_tag loginByPhone() - 保存账号数据');
await _savePhoneAuthData(result, deviceId, phoneNumber);
debugPrint('$_tag loginByPhone() - 手机号登录完成');
return result;
} on ApiException catch (e) {
debugPrint('$_tag loginByPhone() - API 异常: $e');
rethrow;
} catch (e, stackTrace) {
debugPrint('$_tag loginByPhone() - 未知异常: $e');
debugPrint('$_tag loginByPhone() - 堆栈: $stackTrace');
throw ApiException('登录失败: $e');
}
}
///
Future<void> _savePhoneAuthData(
PhoneAuthResponse response,
String deviceId,
String phoneNumber,
) async {
debugPrint('$_tag _savePhoneAuthData() - 开始保存手机号认证数据');
//
debugPrint('$_tag _savePhoneAuthData() - 保存 userSerialNum: ${response.accountSequence}');
await _secureStorage.write(
key: StorageKeys.userSerialNum,
value: response.accountSequence,
);
// Token
debugPrint('$_tag _savePhoneAuthData() - 保存 accessToken (长度: ${response.accessToken.length})');
await _secureStorage.write(
key: StorageKeys.accessToken,
value: response.accessToken,
);
debugPrint('$_tag _savePhoneAuthData() - 保存 refreshToken (长度: ${response.refreshToken.length})');
await _secureStorage.write(
key: StorageKeys.refreshToken,
value: response.refreshToken,
);
// ID
debugPrint('$_tag _savePhoneAuthData() - 保存 deviceId: ${_maskAddress(deviceId)}');
await _secureStorage.write(
key: StorageKeys.deviceId,
value: deviceId,
);
//
debugPrint('$_tag _savePhoneAuthData() - 保存手机号: ${_maskPhoneNumber(phoneNumber)}');
await _secureStorage.write(
key: StorageKeys.phoneNumber,
value: phoneNumber,
);
//
debugPrint('$_tag _savePhoneAuthData() - 标记账号已创建');
await _secureStorage.write(
key: StorageKeys.isAccountCreated,
value: 'true',
);
// ID
if (TelemetryService().isInitialized) {
TelemetryService().setUserId(response.accountSequence);
debugPrint('$_tag _savePhoneAuthData() - 设置TelemetryService userId: ${response.accountSequence}');
}
// Sentry
if (SentryService().isInitialized) {
SentryService().setUser(userId: response.accountSequence);
debugPrint('$_tag _savePhoneAuthData() - 设置SentryService userId: ${response.accountSequence}');
}
debugPrint('$_tag _savePhoneAuthData() - 手机号认证数据保存完成');
}
///
Future<String?> getPhoneNumber() async {
debugPrint('$_tag getPhoneNumber() - 获取手机号');
final result = await _secureStorage.read(key: StorageKeys.phoneNumber);
if (result != null) {
debugPrint('$_tag getPhoneNumber() - 结果: ${_maskPhoneNumber(result)}');
} else {
debugPrint('$_tag getPhoneNumber() - 未找到手机号');
}
return result;
}
///
///
/// [password] - 6-20
Future<void> setLoginPassword(String password) async {
debugPrint('$_tag setLoginPassword() - 开始设置登录密码');
try {
// API
debugPrint('$_tag setLoginPassword() - 调用 POST /user/set-password');
final response = await _apiClient.post(
'/user/set-password',
data: {
'password': password,
},
);
debugPrint('$_tag setLoginPassword() - API 响应状态码: ${response.statusCode}');
//
await _secureStorage.write(
key: StorageKeys.isPasswordSet,
value: 'true',
);
debugPrint('$_tag setLoginPassword() - 登录密码设置完成');
} on ApiException catch (e) {
debugPrint('$_tag setLoginPassword() - API 异常: $e');
rethrow;
} catch (e, stackTrace) {
debugPrint('$_tag setLoginPassword() - 未知异常: $e');
debugPrint('$_tag setLoginPassword() - 堆栈: $stackTrace');
throw ApiException('设置密码失败: $e');
}
}
///
Future<bool> isPasswordSet() async {
debugPrint('$_tag isPasswordSet() - 检查是否已设置密码');
final isSet = await _secureStorage.read(key: StorageKeys.isPasswordSet);
final result = isSet == 'true';
debugPrint('$_tag isPasswordSet() - 结果: $result');
return result;
}
}
///
String _maskPhoneNumber(String phoneNumber) {
if (phoneNumber.length < 7) return phoneNumber;
return '${phoneNumber.substring(0, 3)}****${phoneNumber.substring(phoneNumber.length - 4)}';
}

View File

@ -312,11 +312,42 @@ class CommunityHierarchy {
}
}
///
class StickmanRankingResponse {
final String id;
final String nickname;
final String? avatarUrl;
final int completedCount;
final double monthlyEarnings;
final bool isCurrentUser;
StickmanRankingResponse({
required this.id,
required this.nickname,
this.avatarUrl,
required this.completedCount,
required this.monthlyEarnings,
required this.isCurrentUser,
});
factory StickmanRankingResponse.fromJson(Map<String, dynamic> json) {
return StickmanRankingResponse(
id: json['id']?.toString() ?? '',
nickname: json['nickname'] ?? '',
avatarUrl: json['avatarUrl'],
completedCount: json['completedCount'] ?? 0,
monthlyEarnings: (json['monthlyEarnings'] ?? 0).toDouble(),
isCurrentUser: json['isCurrentUser'] ?? false,
);
}
}
///
///
/// :
/// -
/// - //
/// -
class AuthorizationService {
final ApiClient _apiClient;
@ -401,4 +432,52 @@ class AuthorizationService {
rethrow;
}
}
///
///
/// /
/// [month] : YYYY-MM
/// [roleType] : AUTH_PROVINCE_COMPANY AUTH_CITY_COMPANY
/// [regionCode] /
Future<List<StickmanRankingResponse>> getStickmanRanking({
required String month,
required String roleType,
required String regionCode,
}) async {
try {
debugPrint('获取火柴人排名: month=$month, roleType=$roleType, regionCode=$regionCode');
final response = await _apiClient.get(
ApiEndpoints.stickmanRanking,
queryParameters: {
'month': month,
'roleType': roleType,
'regionCode': regionCode,
},
);
if (response.statusCode == 200) {
final responseData = response.data;
List<dynamic>? dataList;
if (responseData is Map<String, dynamic>) {
dataList = responseData['data'] as List<dynamic>?;
} else if (responseData is List) {
dataList = responseData;
}
if (dataList != null) {
final rankings = dataList
.map((e) => StickmanRankingResponse.fromJson(e as Map<String, dynamic>))
.toList();
debugPrint('火柴人排名获取成功: ${rankings.length}');
return rankings;
}
return [];
}
throw Exception('获取火柴人排名失败');
} catch (e) {
debugPrint('获取火柴人排名失败: $e');
rethrow;
}
}
}

View File

@ -0,0 +1,121 @@
import 'package:flutter/foundation.dart';
import '../network/api_client.dart';
///
enum HeatDisplayMode {
///
count,
/// //
level,
}
///
class DisplaySettings {
///
final bool allowNonAdopterViewHeat;
///
final HeatDisplayMode heatDisplayMode;
DisplaySettings({
required this.allowNonAdopterViewHeat,
required this.heatDisplayMode,
});
factory DisplaySettings.fromJson(Map<String, dynamic> json) {
return DisplaySettings(
allowNonAdopterViewHeat: json['allowNonAdopterViewHeat'] ?? false,
heatDisplayMode: _parseHeatDisplayMode(json['heatDisplayMode']),
);
}
static HeatDisplayMode _parseHeatDisplayMode(String? mode) {
switch (mode) {
case 'level':
return HeatDisplayMode.level;
case 'count':
default:
return HeatDisplayMode.count;
}
}
///
factory DisplaySettings.defaultSettings() {
return DisplaySettings(
allowNonAdopterViewHeat: false,
heatDisplayMode: HeatDisplayMode.count,
);
}
}
///
class SystemConfigService {
final ApiClient _apiClient;
///
DisplaySettings? _cachedDisplaySettings;
///
DateTime? _lastFetchTime;
/// 5
static const Duration _cacheExpiration = Duration(minutes: 5);
SystemConfigService({required ApiClient apiClient}) : _apiClient = apiClient;
///
///
/// [forceRefresh] 使
Future<DisplaySettings> getDisplaySettings({bool forceRefresh = false}) async {
//
if (!forceRefresh && _cachedDisplaySettings != null && _lastFetchTime != null) {
final cacheAge = DateTime.now().difference(_lastFetchTime!);
if (cacheAge < _cacheExpiration) {
return _cachedDisplaySettings!;
}
}
try {
final response = await _apiClient.get(
'/admin-service/api/v1/system-config/display/settings',
);
_cachedDisplaySettings = DisplaySettings.fromJson(response.data);
_lastFetchTime = DateTime.now();
debugPrint('[SystemConfigService] 获取展示设置成功: allowNonAdopterViewHeat=${_cachedDisplaySettings!.allowNonAdopterViewHeat}, heatDisplayMode=${_cachedDisplaySettings!.heatDisplayMode}');
return _cachedDisplaySettings!;
} catch (e) {
debugPrint('[SystemConfigService] 获取展示设置失败: $e');
//
return _cachedDisplaySettings ?? DisplaySettings.defaultSettings();
}
}
///
void clearCache() {
_cachedDisplaySettings = null;
_lastFetchTime = null;
}
///
///
/// [hasAdopted]
Future<bool> canViewProvinceHeat({required bool hasAdopted}) async {
if (hasAdopted) {
//
return true;
}
final settings = await getDisplaySettings();
return settings.allowNonAdopterViewHeat;
}
///
Future<HeatDisplayMode> getHeatDisplayMode() async {
final settings = await getDisplaySettings();
return settings.heatDisplayMode;
}
}

View File

@ -14,6 +14,8 @@ class StorageKeys {
static const String inviterSequence = 'inviter_sequence'; //
static const String inviterReferralCode = 'inviter_referral_code'; //
static const String isAccountCreated = 'is_account_created'; //
static const String phoneNumber = 'phone_number'; //
static const String isPasswordSet = 'is_password_set'; //
///
///

View File

@ -9,6 +9,7 @@ import '../../../../core/di/injection_container.dart';
import '../../../../core/storage/storage_keys.dart';
import '../../../../routes/route_paths.dart';
import '../providers/auth_provider.dart';
import 'phone_register_page.dart';
///
class GuidePageData {
@ -340,22 +341,52 @@ class _WelcomePageContentState extends ConsumerState<_WelcomePageContent> {
context.push(RoutePaths.importMnemonic);
}
///
///
Future<void> _goToPhoneRegister() async {
debugPrint('[GuidePage] _goToPhoneRegister - 跳转到手机号注册页面');
//
String? inviterCode;
if (_hasReferrer && _referralCodeController.text.trim().isNotEmpty) {
inviterCode = _referralCodeController.text.trim();
final secureStorage = ref.read(secureStorageProvider);
await secureStorage.write(
key: StorageKeys.inviterReferralCode,
value: inviterCode,
);
debugPrint('[GuidePage] 保存邀请人推荐码: $inviterCode');
}
if (!mounted) return;
context.push(
RoutePaths.phoneRegister,
extra: PhoneRegisterParams(inviterReferralCode: inviterCode),
);
}
/// ()
Future<void> _saveReferralCodeAndProceed() async {
if (!_canProceed) return;
//
String? inviterCode;
if (_hasReferrer && _referralCodeController.text.trim().isNotEmpty) {
inviterCode = _referralCodeController.text.trim();
final secureStorage = ref.read(secureStorageProvider);
await secureStorage.write(
key: StorageKeys.inviterReferralCode,
value: _referralCodeController.text.trim(),
value: inviterCode,
);
debugPrint('[GuidePage] 保存邀请人推荐码: ${_referralCodeController.text.trim()}');
debugPrint('[GuidePage] 保存邀请人推荐码: $inviterCode');
}
//
widget.onNext();
if (!mounted) return;
//
context.push(
RoutePaths.phoneRegister,
extra: PhoneRegisterParams(inviterReferralCode: inviterCode),
);
}
///
@ -725,6 +756,42 @@ class _WelcomePageContentState extends ConsumerState<_WelcomePageContent> {
],
),
SizedBox(height: 24.h),
//
GestureDetector(
onTap: _goToPhoneRegister,
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 14.h),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: Colors.white.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.phone_android,
size: 20.sp,
color: Colors.white,
),
SizedBox(width: 8.w),
Text(
'使用手机号注册',
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
],
),
),
),
SizedBox(height: 12.h),
//
GestureDetector(
onTap: _importMnemonic,

View File

@ -0,0 +1,325 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/di/injection_container.dart';
import '../../../../core/services/account_service.dart';
import '../../../../routes/route_paths.dart';
///
class PhoneRegisterParams {
final String? inviterReferralCode; //
PhoneRegisterParams({this.inviterReferralCode});
}
///
///
class PhoneRegisterPage extends ConsumerStatefulWidget {
final String? inviterReferralCode;
const PhoneRegisterPage({
super.key,
this.inviterReferralCode,
});
@override
ConsumerState<PhoneRegisterPage> createState() => _PhoneRegisterPageState();
}
class _PhoneRegisterPageState extends ConsumerState<PhoneRegisterPage> {
final TextEditingController _phoneController = TextEditingController();
final FocusNode _phoneFocusNode = FocusNode();
bool _isSending = false;
String? _errorMessage;
@override
void initState() {
super.initState();
debugPrint('[PhoneRegisterPage] initState - inviterReferralCode: ${widget.inviterReferralCode}');
_phoneController.addListener(_onPhoneChanged);
}
@override
void dispose() {
_phoneController.removeListener(_onPhoneChanged);
_phoneController.dispose();
_phoneFocusNode.dispose();
super.dispose();
}
void _onPhoneChanged() {
//
if (_errorMessage != null) {
setState(() {
_errorMessage = null;
});
}
}
///
bool _isValidPhoneNumber(String phone) {
// 13-911
final regex = RegExp(r'^1[3-9]\d{9}$');
return regex.hasMatch(phone);
}
///
Future<void> _sendSmsCode() async {
final phone = _phoneController.text.trim();
//
if (!_isValidPhoneNumber(phone)) {
setState(() {
_errorMessage = '请输入正确的手机号';
});
return;
}
setState(() {
_isSending = true;
_errorMessage = null;
});
try {
final accountService = ref.read(accountServiceProvider);
//
debugPrint('[PhoneRegisterPage] 发送验证码...');
await accountService.sendSmsCode(phone, SmsCodeType.register);
debugPrint('[PhoneRegisterPage] 验证码已发送');
if (!mounted) return;
//
context.push(
RoutePaths.smsVerify,
extra: SmsVerifyParams(
phoneNumber: phone,
type: SmsCodeType.register,
inviterReferralCode: widget.inviterReferralCode,
),
);
} catch (e) {
debugPrint('[PhoneRegisterPage] 发送验证码失败: $e');
if (mounted) {
setState(() {
_errorMessage = e.toString().replaceAll('Exception: ', '');
});
}
} finally {
if (mounted) {
setState(() {
_isSending = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Color(0xFF333333)),
onPressed: () => context.pop(),
),
title: Text(
'手机号注册',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: const Color(0xFF333333),
),
),
centerTitle: true,
),
body: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 32.h),
//
Text(
'输入手机号',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w700,
color: const Color(0xFF333333),
),
),
SizedBox(height: 8.h),
Text(
'我们将向您的手机发送验证码',
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF999999),
),
),
SizedBox(height: 32.h),
//
_buildPhoneInput(),
//
if (_errorMessage != null) ...[
SizedBox(height: 8.h),
Text(
_errorMessage!,
style: TextStyle(
fontSize: 12.sp,
color: Colors.red,
),
),
],
const Spacer(),
//
_buildNextButton(),
SizedBox(height: 32.h),
],
),
),
),
),
);
}
Widget _buildPhoneInput() {
return Container(
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: _errorMessage != null
? Colors.red.withValues(alpha: 0.5)
: Colors.transparent,
),
),
child: Row(
children: [
// /
Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: const Color(0xFFE0E0E0),
width: 1.w,
),
),
),
child: Text(
'+86',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: const Color(0xFF333333),
),
),
),
//
Expanded(
child: TextField(
controller: _phoneController,
focusNode: _phoneFocusNode,
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(11),
],
decoration: InputDecoration(
hintText: '请输入手机号',
hintStyle: TextStyle(
fontSize: 16.sp,
color: const Color(0xFFBBBBBB),
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 16.h,
),
),
style: TextStyle(
fontSize: 16.sp,
color: const Color(0xFF333333),
),
),
),
//
if (_phoneController.text.isNotEmpty)
GestureDetector(
onTap: () {
_phoneController.clear();
setState(() {});
},
child: Padding(
padding: EdgeInsets.only(right: 16.w),
child: Icon(
Icons.cancel,
size: 20.sp,
color: const Color(0xFFBBBBBB),
),
),
),
],
),
);
}
Widget _buildNextButton() {
final isValid = _isValidPhoneNumber(_phoneController.text.trim());
return GestureDetector(
onTap: isValid && !_isSending ? _sendSmsCode : null,
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 16.h),
decoration: BoxDecoration(
color: isValid && !_isSending
? const Color(0xFF2E7D32)
: const Color(0xFFE0E0E0),
borderRadius: BorderRadius.circular(12.r),
),
child: Center(
child: _isSending
? SizedBox(
width: 24.sp,
height: 24.sp,
child: const CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(
'获取验证码',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isValid ? Colors.white : const Color(0xFF999999),
),
),
),
),
);
}
}
///
class SmsVerifyParams {
final String phoneNumber;
final SmsCodeType type;
final String? inviterReferralCode;
SmsVerifyParams({
required this.phoneNumber,
required this.type,
this.inviterReferralCode,
});
}

View File

@ -0,0 +1,360 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/di/injection_container.dart';
import '../../../../routes/route_paths.dart';
///
class SetPasswordParams {
final String userSerialNum;
final String? inviterReferralCode;
SetPasswordParams({
required this.userSerialNum,
this.inviterReferralCode,
});
}
///
///
class SetPasswordPage extends ConsumerStatefulWidget {
final String userSerialNum;
final String? inviterReferralCode;
const SetPasswordPage({
super.key,
required this.userSerialNum,
this.inviterReferralCode,
});
@override
ConsumerState<SetPasswordPage> createState() => _SetPasswordPageState();
}
class _SetPasswordPageState extends ConsumerState<SetPasswordPage> {
final _formKey = GlobalKey<FormState>();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
bool _isPasswordVisible = false;
bool _isConfirmPasswordVisible = false;
bool _isSubmitting = false;
String? _errorMessage;
@override
void initState() {
super.initState();
debugPrint('[SetPasswordPage] initState - userSerialNum: ${widget.userSerialNum}');
}
@override
void dispose() {
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
///
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return '请输入密码';
}
if (value.length < 6) {
return '密码长度至少6位';
}
if (value.length > 20) {
return '密码长度不能超过20位';
}
//
if (!RegExp(r'^(?=.*[A-Za-z])(?=.*\d).+$').hasMatch(value)) {
return '密码需包含字母和数字';
}
return null;
}
///
String? _validateConfirmPassword(String? value) {
if (value == null || value.isEmpty) {
return '请再次输入密码';
}
if (value != _passwordController.text) {
return '两次输入的密码不一致';
}
return null;
}
///
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isSubmitting = true;
_errorMessage = null;
});
try {
final accountService = ref.read(accountServiceProvider);
final password = _passwordController.text;
debugPrint('[SetPasswordPage] 开始设置密码...');
// API
await accountService.setLoginPassword(password);
debugPrint('[SetPasswordPage] 密码设置成功');
if (!mounted) return;
//
context.go(RoutePaths.mining);
} catch (e) {
debugPrint('[SetPasswordPage] 设置密码失败: $e');
if (mounted) {
setState(() {
_errorMessage = e.toString().replaceAll('Exception: ', '');
});
}
} finally {
if (mounted) {
setState(() {
_isSubmitting = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Color(0xFF333333)),
onPressed: () => context.pop(),
),
title: Text(
'设置登录密码',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: const Color(0xFF333333),
),
),
centerTitle: true,
),
body: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: SafeArea(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 32.h),
//
Text(
'创建登录密码',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w700,
color: const Color(0xFF333333),
),
),
SizedBox(height: 8.h),
Text(
'密码需包含6-20位字母和数字',
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF999999),
),
),
SizedBox(height: 32.h),
//
_buildPasswordField(
controller: _passwordController,
label: '登录密码',
hint: '请输入密码',
isPasswordVisible: _isPasswordVisible,
onToggleVisibility: () {
setState(() {
_isPasswordVisible = !_isPasswordVisible;
});
},
validator: _validatePassword,
),
SizedBox(height: 16.h),
//
_buildPasswordField(
controller: _confirmPasswordController,
label: '确认密码',
hint: '请再次输入密码',
isPasswordVisible: _isConfirmPasswordVisible,
onToggleVisibility: () {
setState(() {
_isConfirmPasswordVisible = !_isConfirmPasswordVisible;
});
},
validator: _validateConfirmPassword,
),
//
if (_errorMessage != null) ...[
SizedBox(height: 16.h),
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: Colors.red.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
children: [
Icon(
Icons.error_outline,
color: Colors.red,
size: 20.sp,
),
SizedBox(width: 8.w),
Expanded(
child: Text(
_errorMessage!,
style: TextStyle(
fontSize: 14.sp,
color: Colors.red,
),
),
),
],
),
),
],
SizedBox(height: 40.h),
//
GestureDetector(
onTap: _isSubmitting ? null : _submit,
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 16.h),
decoration: BoxDecoration(
color: _isSubmitting
? const Color(0xFFCCCCCC)
: const Color(0xFF2E7D32),
borderRadius: BorderRadius.circular(12.r),
),
child: _isSubmitting
? Center(
child: SizedBox(
width: 24.sp,
height: 24.sp,
child: const CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
),
)
: Text(
'完成注册',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),
textAlign: TextAlign.center,
),
),
),
SizedBox(height: 32.h),
],
),
),
),
),
),
);
}
Widget _buildPasswordField({
required TextEditingController controller,
required String label,
required String hint,
required bool isPasswordVisible,
required VoidCallback onToggleVisibility,
required String? Function(String?) validator,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: const Color(0xFF333333),
),
),
SizedBox(height: 8.h),
TextFormField(
controller: controller,
obscureText: !isPasswordVisible,
validator: validator,
decoration: InputDecoration(
hintText: hint,
hintStyle: TextStyle(
fontSize: 16.sp,
color: const Color(0xFF999999),
),
filled: true,
fillColor: const Color(0xFFF5F5F5),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: const BorderSide(
color: Color(0xFF2E7D32),
width: 2,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: const BorderSide(
color: Colors.red,
width: 1,
),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: const BorderSide(
color: Colors.red,
width: 2,
),
),
contentPadding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 16.h,
),
suffixIcon: IconButton(
icon: Icon(
isPasswordVisible ? Icons.visibility_off : Icons.visibility,
color: const Color(0xFF999999),
),
onPressed: onToggleVisibility,
),
),
style: TextStyle(
fontSize: 16.sp,
color: const Color(0xFF333333),
),
),
],
);
}
}

View File

@ -0,0 +1,446 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/di/injection_container.dart';
import '../../../../core/services/account_service.dart';
import '../../../../core/services/multi_account_service.dart';
import '../../../../routes/route_paths.dart';
import 'set_password_page.dart';
///
/// 6/
class SmsVerifyPage extends ConsumerStatefulWidget {
final String phoneNumber;
final SmsCodeType type;
final String? inviterReferralCode;
const SmsVerifyPage({
super.key,
required this.phoneNumber,
required this.type,
this.inviterReferralCode,
});
@override
ConsumerState<SmsVerifyPage> createState() => _SmsVerifyPageState();
}
class _SmsVerifyPageState extends ConsumerState<SmsVerifyPage> {
final List<TextEditingController> _controllers = List.generate(
6,
(_) => TextEditingController(),
);
final List<FocusNode> _focusNodes = List.generate(6, (_) => FocusNode());
bool _isVerifying = false;
String? _errorMessage;
//
int _countdown = 60;
Timer? _countdownTimer;
bool _canResend = false;
@override
void initState() {
super.initState();
debugPrint('[SmsVerifyPage] initState - phoneNumber: ${_maskPhoneNumber(widget.phoneNumber)}, type: ${widget.type}');
_startCountdown();
//
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNodes[0].requestFocus();
});
}
@override
void dispose() {
_countdownTimer?.cancel();
for (final controller in _controllers) {
controller.dispose();
}
for (final focusNode in _focusNodes) {
focusNode.dispose();
}
super.dispose();
}
void _startCountdown() {
_countdown = 60;
_canResend = false;
_countdownTimer?.cancel();
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_countdown > 0) {
setState(() {
_countdown--;
});
} else {
setState(() {
_canResend = true;
});
timer.cancel();
}
});
}
String _getVerificationCode() {
return _controllers.map((c) => c.text).join();
}
///
Future<void> _resendCode() async {
if (!_canResend) return;
try {
final accountService = ref.read(accountServiceProvider);
await accountService.sendSmsCode(widget.phoneNumber, widget.type);
if (mounted) {
_startCountdown();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'验证码已发送',
style: TextStyle(fontSize: 14.sp),
),
backgroundColor: const Color(0xFF2E7D32),
behavior: SnackBarBehavior.floating,
margin: EdgeInsets.all(16.w),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
);
}
} catch (e) {
debugPrint('[SmsVerifyPage] 重新发送验证码失败: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'发送失败: ${e.toString().replaceAll('Exception: ', '')}',
style: TextStyle(fontSize: 14.sp),
),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
margin: EdgeInsets.all(16.w),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
);
}
}
}
///
Future<void> _verifyCode() async {
final code = _getVerificationCode();
if (code.length != 6) {
setState(() {
_errorMessage = '请输入完整的验证码';
});
return;
}
setState(() {
_isVerifying = true;
_errorMessage = null;
});
try {
final accountService = ref.read(accountServiceProvider);
if (widget.type == SmsCodeType.register) {
// auto-create
debugPrint('[SmsVerifyPage] 开始验证短信验证码...');
//
await accountService.verifySmsCode(
phoneNumber: widget.phoneNumber,
smsCode: code,
type: SmsCodeType.register,
);
debugPrint('[SmsVerifyPage] 短信验证码验证成功');
// auto-create
debugPrint('[SmsVerifyPage] 开始调用 auto-create 创建账号...');
final response = await accountService.createAccount(
inviterReferralCode: widget.inviterReferralCode,
);
debugPrint('[SmsVerifyPage] 账号创建成功: ${response.userSerialNum}');
if (!mounted) return;
//
final multiAccountService = ref.read(multiAccountServiceProvider);
await multiAccountService.addAccount(
AccountSummary(
userSerialNum: response.userSerialNum,
username: response.username,
avatarSvg: response.avatarSvg,
createdAt: DateTime.now(),
),
);
await multiAccountService.setCurrentAccountId(response.userSerialNum);
debugPrint('[SmsVerifyPage] 已添加到多账号列表');
if (!mounted) return;
//
context.go(
RoutePaths.setPassword,
extra: SetPasswordParams(
userSerialNum: response.userSerialNum,
inviterReferralCode: widget.inviterReferralCode,
),
);
} else if (widget.type == SmsCodeType.login) {
//
debugPrint('[SmsVerifyPage] 开始登录...');
final response = await accountService.loginByPhone(
phoneNumber: widget.phoneNumber,
smsCode: code,
);
debugPrint('[SmsVerifyPage] 登录成功: ${response.accountSequence}');
if (!mounted) return;
//
context.go(RoutePaths.mining);
}
} catch (e) {
debugPrint('[SmsVerifyPage] 验证失败: $e');
if (mounted) {
setState(() {
_errorMessage = e.toString().replaceAll('Exception: ', '');
});
//
for (final controller in _controllers) {
controller.clear();
}
_focusNodes[0].requestFocus();
}
} finally {
if (mounted) {
setState(() {
_isVerifying = false;
});
}
}
}
void _onCodeChanged(int index, String value) {
//
if (_errorMessage != null) {
setState(() {
_errorMessage = null;
});
}
if (value.isNotEmpty) {
//
if (index < 5) {
_focusNodes[index + 1].requestFocus();
} else {
//
_focusNodes[index].unfocus();
_verifyCode();
}
}
}
void _onKeyEvent(int index, KeyEvent event) {
if (event is KeyDownEvent) {
if (event.logicalKey == LogicalKeyboardKey.backspace) {
if (_controllers[index].text.isEmpty && index > 0) {
//
_controllers[index - 1].clear();
_focusNodes[index - 1].requestFocus();
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Color(0xFF333333)),
onPressed: () => context.pop(),
),
title: Text(
widget.type == SmsCodeType.register ? '手机号注册' : '手机号登录',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: const Color(0xFF333333),
),
),
centerTitle: true,
),
body: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 32.h),
//
Text(
'输入验证码',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w700,
color: const Color(0xFF333333),
),
),
SizedBox(height: 8.h),
Text(
'验证码已发送至 ${_formatPhoneNumber(widget.phoneNumber)}',
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF999999),
),
),
SizedBox(height: 32.h),
//
_buildCodeInputs(),
//
if (_errorMessage != null) ...[
SizedBox(height: 16.h),
Text(
_errorMessage!,
style: TextStyle(
fontSize: 14.sp,
color: Colors.red,
),
),
],
SizedBox(height: 24.h),
//
_buildResendButton(),
const Spacer(),
//
if (_isVerifying)
Center(
child: Column(
children: [
SizedBox(
width: 32.sp,
height: 32.sp,
child: const CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF2E7D32)),
),
),
SizedBox(height: 12.h),
Text(
widget.type == SmsCodeType.register ? '正在注册...' : '正在登录...',
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF666666),
),
),
],
),
),
SizedBox(height: 32.h),
],
),
),
),
),
);
}
Widget _buildCodeInputs() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(6, (index) {
return SizedBox(
width: 48.w,
height: 56.h,
child: KeyboardListener(
focusNode: FocusNode(),
onKeyEvent: (event) => _onKeyEvent(index, event),
child: TextField(
controller: _controllers[index],
focusNode: _focusNodes[index],
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
maxLength: 1,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
decoration: InputDecoration(
counterText: '',
filled: true,
fillColor: const Color(0xFFF5F5F5),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: const BorderSide(
color: Color(0xFF2E7D32),
width: 2,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: const BorderSide(
color: Colors.red,
width: 1,
),
),
),
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w700,
color: const Color(0xFF333333),
),
onChanged: (value) => _onCodeChanged(index, value),
),
),
);
}),
);
}
Widget _buildResendButton() {
return Center(
child: GestureDetector(
onTap: _canResend ? _resendCode : null,
child: Text(
_canResend ? '重新发送验证码' : '$_countdown秒后重新发送',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: _canResend
? const Color(0xFF2E7D32)
: const Color(0xFF999999),
),
),
),
);
}
String _formatPhoneNumber(String phone) {
if (phone.length != 11) return phone;
return '${phone.substring(0, 3)} **** ${phone.substring(7)}';
}
String _maskPhoneNumber(String phone) {
if (phone.length < 7) return phone;
return '${phone.substring(0, 3)}****${phone.substring(phone.length - 4)}';
}
}

View File

@ -0,0 +1,898 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
///
enum AuthorizationType {
community, //
cityTeam, //
provinceTeam, //
}
///
extension AuthorizationTypeExtension on AuthorizationType {
String get displayName {
switch (this) {
case AuthorizationType.community:
return '社区';
case AuthorizationType.cityTeam:
return '市团队';
case AuthorizationType.provinceTeam:
return '省团队';
}
}
String get description {
switch (this) {
case AuthorizationType.community:
return '社区权益 - 576 USDT/棵';
case AuthorizationType.cityTeam:
return '市团队权益 - 288 USDT/棵';
case AuthorizationType.provinceTeam:
return '省团队权益 - 144 USDT/棵';
}
}
}
///
class AuthorizationApplyPage extends ConsumerStatefulWidget {
const AuthorizationApplyPage({super.key});
@override
ConsumerState<AuthorizationApplyPage> createState() =>
_AuthorizationApplyPageState();
}
class _AuthorizationApplyPageState
extends ConsumerState<AuthorizationApplyPage> {
///
AuthorizationType? _selectedType;
///
final List<File> _officePhotos = [];
///
final ImagePicker _picker = ImagePicker();
///
bool _isSubmitting = false;
///
bool _isLoading = true;
///
bool _hasPlanted = false;
///
int _plantedCount = 0;
///
List<String> _existingAuthorizations = [];
@override
void initState() {
super.initState();
_loadUserStatus();
}
///
Future<void> _loadUserStatus() async {
setState(() {
_isLoading = true;
});
try {
// TODO: API获取用户认种状态和已有授权
// final authService = ref.read(authorizationServiceProvider);
// final status = await authService.getUserAuthorizationStatus();
//
await Future.delayed(const Duration(milliseconds: 500));
if (mounted) {
setState(() {
_hasPlanted = true; //
_plantedCount = 10; // 10
_existingAuthorizations = []; //
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
});
_showErrorSnackBar('加载状态失败: ${e.toString()}');
}
}
}
///
void _goBack() {
context.pop();
}
///
Future<void> _pickPhoto() async {
if (_officePhotos.length >= 6) {
_showErrorSnackBar('最多上传6张照片');
return;
}
try {
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery,
maxWidth: 1920,
maxHeight: 1920,
imageQuality: 85,
);
if (image != null && mounted) {
setState(() {
_officePhotos.add(File(image.path));
});
}
} catch (e) {
_showErrorSnackBar('选择图片失败');
}
}
///
Future<void> _takePhoto() async {
if (_officePhotos.length >= 6) {
_showErrorSnackBar('最多上传6张照片');
return;
}
try {
final XFile? image = await _picker.pickImage(
source: ImageSource.camera,
maxWidth: 1920,
maxHeight: 1920,
imageQuality: 85,
);
if (image != null && mounted) {
setState(() {
_officePhotos.add(File(image.path));
});
}
} catch (e) {
_showErrorSnackBar('拍照失败');
}
}
///
void _removePhoto(int index) {
setState(() {
_officePhotos.removeAt(index);
});
}
///
Future<void> _submitApplication() async {
//
if (_selectedType == null) {
_showErrorSnackBar('请选择要申请的授权类型');
return;
}
if (!_hasPlanted) {
_showErrorSnackBar('您还未认种,请先进行认种');
return;
}
if (_officePhotos.isEmpty) {
_showErrorSnackBar('请上传办公室照片');
return;
}
if (_officePhotos.length < 2) {
_showErrorSnackBar('请至少上传2张办公室照片');
return;
}
//
final typeName = _selectedType!.displayName;
if (_existingAuthorizations.contains(typeName)) {
_showErrorSnackBar('您已拥有${typeName}授权');
return;
}
setState(() {
_isSubmitting = true;
});
try {
// TODO: API提交申请
// final authService = ref.read(authorizationServiceProvider);
// await authService.submitAuthorizationApplication(
// type: _selectedType!,
// officePhotos: _officePhotos,
// );
//
await Future.delayed(const Duration(seconds: 2));
if (mounted) {
setState(() {
_isSubmitting = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('申请已提交,请等待审核'),
backgroundColor: Color(0xFF4CAF50),
),
);
context.pop(true);
}
} catch (e) {
if (mounted) {
setState(() {
_isSubmitting = false;
});
_showErrorSnackBar('提交失败: ${e.toString()}');
}
}
}
///
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFFFF5E6),
Color(0xFFFFE4B5),
],
),
),
child: SafeArea(
child: Column(
children: [
//
_buildAppBar(),
//
Expanded(
child: _isLoading
? const Center(
child: CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
),
)
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
_buildInfoCard(),
const SizedBox(height: 24),
//
_buildUserStatusCard(),
const SizedBox(height: 24),
//
_buildAuthorizationTypes(),
const SizedBox(height: 24),
//
_buildNonSelfApplyInfo(),
const SizedBox(height: 24),
//
if (_selectedType != null) ...[
_buildPhotoUpload(),
const SizedBox(height: 32),
],
//
_buildSubmitButton(),
],
),
),
),
],
),
),
),
);
}
///
Widget _buildAppBar() {
return Container(
height: 64,
padding: const EdgeInsets.only(top: 16, left: 16, right: 16),
child: Row(
children: [
//
GestureDetector(
onTap: _goBack,
child: Container(
width: 48,
height: 48,
alignment: Alignment.center,
child: const Icon(
Icons.arrow_back,
size: 24,
color: Color(0xFF5D4037),
),
),
),
//
const Expanded(
child: Text(
'自助申请授权',
style: TextStyle(
fontSize: 18,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.25,
letterSpacing: -0.27,
color: Color(0xFF5D4037),
),
textAlign: TextAlign.center,
),
),
//
const SizedBox(width: 48),
],
),
);
}
///
Widget _buildInfoCard() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFFF5E6),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: const [
Icon(
Icons.info_outline,
size: 20,
color: Color(0xFFD4AF37),
),
SizedBox(width: 8),
Text(
'申请说明',
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFF5D4037),
),
),
],
),
const SizedBox(height: 12),
const Text(
'1. 申请账户必须已完成认种\n'
'2. 需提供办公室照片至少2张\n'
'3. 社区、市团队、省团队支持自助申请\n'
'4. 提交后等待审核,审核通过后生效',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
height: 1.6,
color: Color(0xFF745D43),
),
),
],
),
);
}
///
Widget _buildUserStatusCard() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0x80FFFFFF),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: _hasPlanted
? const Color(0xFF4CAF50).withValues(alpha: 0.1)
: const Color(0xFFFF9800).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(24),
),
child: Icon(
_hasPlanted ? Icons.check_circle : Icons.warning,
color:
_hasPlanted ? const Color(0xFF4CAF50) : const Color(0xFFFF9800),
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_hasPlanted ? '已认种' : '未认种',
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: _hasPlanted
? const Color(0xFF4CAF50)
: const Color(0xFFFF9800),
),
),
const SizedBox(height: 4),
Text(
_hasPlanted ? '累计认种 $_plantedCount' : '请先完成认种后再申请授权',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
color: Color(0xFF745D43),
),
),
],
),
),
],
),
);
}
///
Widget _buildAuthorizationTypes() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'选择申请类型',
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
color: Color(0xFF5D4037),
),
),
const SizedBox(height: 12),
...AuthorizationType.values.map((type) => _buildTypeItem(type)),
],
);
}
///
Widget _buildTypeItem(AuthorizationType type) {
final isSelected = _selectedType == type;
final isAlreadyHas = _existingAuthorizations.contains(type.displayName);
return GestureDetector(
onTap: isAlreadyHas
? null
: () {
setState(() {
_selectedType = type;
});
},
child: Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isAlreadyHas
? const Color(0xFFE0E0E0)
: isSelected
? const Color(0xFFD4AF37).withValues(alpha: 0.1)
: const Color(0x80FFFFFF),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected
? const Color(0xFFD4AF37)
: const Color(0x33D4AF37),
width: isSelected ? 2 : 1,
),
),
child: Row(
children: [
//
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: isAlreadyHas
? const Color(0xFF9E9E9E)
: isSelected
? const Color(0xFFD4AF37)
: const Color(0xFF8B5A2B),
width: 2,
),
color: isSelected ? const Color(0xFFD4AF37) : Colors.transparent,
),
child: isSelected
? const Icon(
Icons.check,
size: 16,
color: Colors.white,
)
: null,
),
const SizedBox(width: 16),
//
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
type.displayName,
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: isAlreadyHas
? const Color(0xFF9E9E9E)
: const Color(0xFF5D4037),
),
),
const SizedBox(height: 4),
Text(
type.description,
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
color: isAlreadyHas
? const Color(0xFFBDBDBD)
: const Color(0xFF745D43),
),
),
],
),
),
//
if (isAlreadyHas)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF9E9E9E),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'已拥有',
style: TextStyle(
fontSize: 12,
fontFamily: 'Inter',
color: Colors.white,
),
),
),
],
),
),
);
}
///
Widget _buildNonSelfApplyInfo() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFEFEFEF),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: const [
Icon(
Icons.block,
size: 20,
color: Color(0xFF9E9E9E),
),
SizedBox(width: 8),
Text(
'不支持自助申请',
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFF757575),
),
),
],
),
const SizedBox(height: 12),
const Text(
'省区域和市区域授权不支持自助申请,获得方式:\n\n'
'1. 自动升级\n'
' - 某省团队账号伞下第一个累计达到5万棵的账户\n'
' 自动获得该省区域授权\n'
' - 某市团队账号伞下第一个累计达到1万棵的账户\n'
' 自动获得该市区域授权\n\n'
'2. 后台手动授权',
style: TextStyle(
fontSize: 13,
fontFamily: 'Inter',
height: 1.6,
color: Color(0xFF757575),
),
),
],
),
);
}
///
Widget _buildPhotoUpload() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: const [
Text(
'办公室照片',
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
color: Color(0xFF5D4037),
),
),
SizedBox(width: 4),
Text(
'*',
style: TextStyle(
fontSize: 16,
color: Colors.red,
),
),
],
),
const SizedBox(height: 4),
const Text(
'请上传办公室照片至少2张最多6张',
style: TextStyle(
fontSize: 12,
fontFamily: 'Inter',
color: Color(0xFF745D43),
),
),
const SizedBox(height: 12),
//
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1,
),
itemCount: _officePhotos.length + 1,
itemBuilder: (context, index) {
if (index == _officePhotos.length) {
//
return _buildAddPhotoButton();
}
//
return _buildPhotoItem(index);
},
),
],
);
}
///
Widget _buildAddPhotoButton() {
if (_officePhotos.length >= 6) {
return const SizedBox.shrink();
}
return GestureDetector(
onTap: () => _showPhotoSourceDialog(),
child: Container(
decoration: BoxDecoration(
color: const Color(0x80FFFFFF),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
style: BorderStyle.solid,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(
Icons.add_a_photo,
size: 32,
color: Color(0xFFD4AF37),
),
SizedBox(height: 4),
Text(
'添加照片',
style: TextStyle(
fontSize: 12,
fontFamily: 'Inter',
color: Color(0xFF745D43),
),
),
],
),
),
);
}
///
void _showPhotoSourceDialog() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.photo_library, color: Color(0xFFD4AF37)),
title: const Text('从相册选择'),
onTap: () {
Navigator.pop(context);
_pickPhoto();
},
),
ListTile(
leading: const Icon(Icons.camera_alt, color: Color(0xFFD4AF37)),
title: const Text('拍照'),
onTap: () {
Navigator.pop(context);
_takePhoto();
},
),
ListTile(
leading: const Icon(Icons.close, color: Color(0xFF757575)),
title: const Text('取消'),
onTap: () => Navigator.pop(context),
),
],
),
),
);
}
///
Widget _buildPhotoItem(int index) {
return Stack(
children: [
//
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: FileImage(_officePhotos[index]),
fit: BoxFit.cover,
),
),
),
//
Positioned(
top: 4,
right: 4,
child: GestureDetector(
onTap: () => _removePhoto(index),
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.5),
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
size: 16,
color: Colors.white,
),
),
),
),
],
);
}
///
Widget _buildSubmitButton() {
final canSubmit = _hasPlanted &&
_selectedType != null &&
_officePhotos.length >= 2 &&
!_isSubmitting;
return GestureDetector(
onTap: canSubmit ? _submitApplication : null,
child: Container(
width: double.infinity,
height: 56,
decoration: BoxDecoration(
color: canSubmit
? const Color(0xFFD4AF37)
: const Color(0xFFD4AF37).withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(12),
boxShadow: canSubmit
? const [
BoxShadow(
color: Color(0x4DD4AF37),
blurRadius: 14,
offset: Offset(0, 4),
),
]
: null,
),
child: Center(
child: _isSubmitting
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Text(
'提交申请',
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
letterSpacing: 0.24,
color: Colors.white,
),
),
),
),
);
}
}

View File

@ -0,0 +1,585 @@
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
///
class StickmanRankingData {
final String id;
final String nickname;
final String? avatarUrl;
final int completedCount; //
final int targetCount; // (: 50000, : 10000)
final double monthlyEarnings; //
final bool isCurrentUser; //
StickmanRankingData({
required this.id,
required this.nickname,
this.avatarUrl,
required this.completedCount,
required this.targetCount,
required this.monthlyEarnings,
this.isCurrentUser = false,
});
/// (0.0 - 1.0)
double get progress => (completedCount / targetCount).clamp(0.0, 1.0);
}
///
enum AuthorizationRoleType {
province, // (: 5)
city, // (: 1)
}
///
class StickmanRaceWidget extends StatefulWidget {
final List<StickmanRankingData> rankings;
final AuthorizationRoleType roleType;
final String regionName; // /
const StickmanRaceWidget({
super.key,
required this.rankings,
required this.roleType,
required this.regionName,
});
@override
State<StickmanRaceWidget> createState() => _StickmanRaceWidgetState();
}
class _StickmanRaceWidgetState extends State<StickmanRaceWidget>
with TickerProviderStateMixin {
late AnimationController _bounceController;
@override
void initState() {
super.initState();
//
_bounceController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
)..repeat(reverse: true);
}
@override
void dispose() {
_bounceController.dispose();
super.dispose();
}
///
int get targetCount =>
widget.roleType == AuthorizationRoleType.province ? 50000 : 10000;
///
String get targetText =>
widget.roleType == AuthorizationRoleType.province ? '5万' : '1万';
@override
Widget build(BuildContext context) {
if (widget.rankings.isEmpty) {
return const SizedBox.shrink();
}
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0x80FFFFFF),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
boxShadow: const [
BoxShadow(
color: Color(0x1A000000),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
_buildHeader(),
const SizedBox(height: 16),
//
_buildRaceTrack(),
const SizedBox(height: 12),
//
_buildProgressBar(),
const SizedBox(height: 16),
//
_buildRankingList(),
],
),
);
}
///
Widget _buildHeader() {
final roleText =
widget.roleType == AuthorizationRoleType.province ? '省公司' : '市公司';
return Row(
children: [
Container(
width: 4,
height: 20,
decoration: BoxDecoration(
color: const Color(0xFFD4AF37),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text(
'${widget.regionName}$roleText排名',
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFF5D4037),
),
),
const Spacer(),
Text(
'目标: $targetText棵',
style: const TextStyle(
fontSize: 12,
fontFamily: 'Inter',
color: Color(0xFF8B5A2B),
),
),
],
);
}
///
Widget _buildRaceTrack() {
//
final sortedRankings = List<StickmanRankingData>.from(widget.rankings)
..sort((a, b) => b.completedCount.compareTo(a.completedCount));
return SizedBox(
height: 160,
child: Stack(
children: [
// 线
Positioned.fill(
child: CustomPaint(
painter: _TrackPainter(
trackCount: sortedRankings.length,
),
),
),
//
Positioned(
right: 8,
top: 0,
bottom: 40,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(
Icons.flag,
color: Colors.red,
size: 32,
),
SizedBox(height: 4),
Text(
'终点',
style: TextStyle(
fontSize: 10,
fontFamily: 'Inter',
color: Color(0xFF8B5A2B),
),
),
],
),
),
//
...sortedRankings.asMap().entries.map((entry) {
final index = entry.key;
final data = entry.value;
return _buildStickman(data, index, sortedRankings.length);
}),
],
),
);
}
///
Widget _buildStickman(StickmanRankingData data, int rank, int total) {
// ()
final horizontalProgress = data.progress;
// ()
final trackHeight = 120.0 / total;
final verticalPosition = rank * trackHeight + 10;
return AnimatedBuilder(
animation: _bounceController,
builder: (context, child) {
//
final bounce = _bounceController.value * 3;
return Positioned(
left: 20 + (MediaQuery.of(context).size.width - 120) * horizontalProgress,
top: verticalPosition - bounce,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
//
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: data.isCurrentUser
? const Color(0xFFD4AF37)
: const Color(0xFF8B5A2B),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'${_formatNumber(data.completedCount)}',
style: const TextStyle(
fontSize: 9,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
const SizedBox(height: 2),
//
SizedBox(
width: 40,
height: 50,
child: Lottie.asset(
'assets/lottie/stickman_running.json',
fit: BoxFit.contain,
repeat: true,
),
),
//
Container(
constraints: const BoxConstraints(maxWidth: 60),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: data.isCurrentUser
? const Color(0xFFD4AF37).withValues(alpha: 0.2)
: Colors.white.withValues(alpha: 0.8),
borderRadius: BorderRadius.circular(4),
border: data.isCurrentUser
? Border.all(color: const Color(0xFFD4AF37), width: 1)
: null,
),
child: Text(
data.nickname,
style: TextStyle(
fontSize: 9,
fontFamily: 'Inter',
fontWeight:
data.isCurrentUser ? FontWeight.w600 : FontWeight.w400,
color: data.isCurrentUser
? const Color(0xFFD4AF37)
: const Color(0xFF5D4037),
),
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
],
),
);
},
);
}
///
Widget _buildProgressBar() {
//
final maxProgress = widget.rankings.isEmpty
? 0.0
: widget.rankings
.map((r) => r.progress)
.reduce((a, b) => a > b ? a : b);
return Column(
children: [
//
Container(
height: 8,
decoration: BoxDecoration(
color: const Color(0xFFE0E0E0),
borderRadius: BorderRadius.circular(4),
),
child: Stack(
children: [
//
FractionallySizedBox(
widthFactor: maxProgress,
child: Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFFD4AF37), Color(0xFFF5D799)],
),
borderRadius: BorderRadius.circular(4),
),
),
),
],
),
),
const SizedBox(height: 4),
//
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'0',
style: TextStyle(
fontSize: 10,
fontFamily: 'Inter',
color: Color(0xFF8B5A2B),
),
),
Text(
targetText,
style: const TextStyle(
fontSize: 10,
fontFamily: 'Inter',
color: Color(0xFF8B5A2B),
),
),
],
),
],
);
}
///
Widget _buildRankingList() {
//
final sortedRankings = List<StickmanRankingData>.from(widget.rankings)
..sort((a, b) => b.completedCount.compareTo(a.completedCount));
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'排名详情',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
color: Color(0xFF5D4037),
),
),
const SizedBox(height: 8),
...sortedRankings.asMap().entries.map((entry) {
final rank = entry.key + 1;
final data = entry.value;
return _buildRankingItem(rank, data);
}),
],
);
}
///
Widget _buildRankingItem(int rank, StickmanRankingData data) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: data.isCurrentUser
? const Color(0xFFD4AF37).withValues(alpha: 0.1)
: Colors.white.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
border: data.isCurrentUser
? Border.all(color: const Color(0xFFD4AF37), width: 1)
: null,
),
child: Row(
children: [
//
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: _getRankColor(rank),
shape: BoxShape.circle,
),
child: Center(
child: Text(
'$rank',
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
),
),
const SizedBox(width: 12),
//
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
data.nickname,
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: data.isCurrentUser
? FontWeight.w600
: FontWeight.w500,
color: const Color(0xFF5D4037),
),
),
if (data.isCurrentUser) ...[
const SizedBox(width: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 1,
),
decoration: BoxDecoration(
color: const Color(0xFFD4AF37),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'',
style: TextStyle(
fontSize: 9,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
],
],
),
const SizedBox(height: 2),
Text(
'本月收益: ${_formatCurrency(data.monthlyEarnings)} USDT',
style: const TextStyle(
fontSize: 11,
fontFamily: 'Inter',
color: Color(0xFF8B5A2B),
),
),
],
),
),
//
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${_formatNumber(data.completedCount)}',
style: const TextStyle(
fontSize: 16,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
color: Color(0xFFD4AF37),
),
),
const SizedBox(height: 2),
Text(
'${(data.progress * 100).toStringAsFixed(1)}%',
style: const TextStyle(
fontSize: 11,
fontFamily: 'Inter',
color: Color(0xFF8B5A2B),
),
),
],
),
],
),
);
}
///
Color _getRankColor(int rank) {
switch (rank) {
case 1:
return const Color(0xFFFFD700); //
case 2:
return const Color(0xFFC0C0C0); //
case 3:
return const Color(0xFFCD7F32); //
default:
return const Color(0xFF8B5A2B);
}
}
///
String _formatNumber(int number) {
if (number >= 10000) {
return '${(number / 10000).toStringAsFixed(1)}';
}
return number.toString();
}
///
String _formatCurrency(double amount) {
if (amount >= 10000) {
return '${(amount / 10000).toStringAsFixed(2)}';
}
return amount.toStringAsFixed(2);
}
}
///
class _TrackPainter extends CustomPainter {
final int trackCount;
_TrackPainter({required this.trackCount});
@override
void paint(Canvas canvas, Size size) {
final dashedPaint = Paint()
..color = const Color(0xFFBDBDBD)
..strokeWidth = 1
..style = PaintingStyle.stroke;
final trackHeight = (size.height - 40) / trackCount;
for (int i = 0; i < trackCount; i++) {
final y = 20 + i * trackHeight + trackHeight / 2;
// 线
double startX = 0;
const dashWidth = 10.0;
const dashSpace = 5.0;
while (startX < size.width) {
canvas.drawLine(
Offset(startX, y),
Offset(startX + dashWidth, y),
dashedPaint,
);
startX += dashWidth + dashSpace;
}
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@ -5,15 +5,9 @@ import 'package:go_router/go_router.dart';
import 'package:qr_flutter/qr_flutter.dart';
import '../../../../core/di/injection_container.dart';
///
enum NetworkType {
kava,
bsc,
dst,
}
/// USDT
/// KAVA BSC
/// IDaccountSequence
/// accountSequence
class DepositUsdtPage extends ConsumerStatefulWidget {
const DepositUsdtPage({super.key});
@ -22,60 +16,47 @@ class DepositUsdtPage extends ConsumerStatefulWidget {
}
class _DepositUsdtPageState extends ConsumerState<DepositUsdtPage> {
///
NetworkType _selectedNetwork = NetworkType.kava;
///
bool _isLoading = true;
/// USDT
String _balance = '0.00';
/// KAVA
String? _kavaAddress;
/// BSC
String? _bscAddress;
/// DST
String? _dstAddress;
///
String? _accountSequence;
@override
void initState() {
super.initState();
_loadWalletData();
_loadData();
}
///
String? _errorMessage;
///
Future<void> _loadWalletData() async {
///
Future<void> _loadData() async {
try {
setState(() {
_isLoading = true;
_errorMessage = null;
});
final depositService = ref.read(depositServiceProvider);
//
final accountService = ref.read(accountServiceProvider);
final userSerialNum = await accountService.getUserSerialNum();
// ()
final addressResponse = await depositService.getDepositAddresses();
if (!addressResponse.isValid) {
//
if (userSerialNum == null || userSerialNum.isEmpty) {
if (mounted) {
setState(() {
_errorMessage = addressResponse.message ?? '充值账户异常,请联系客服';
_errorMessage = '无法获取用户信息,请重新登录';
_isLoading = false;
});
}
return;
}
_kavaAddress = addressResponse.kavaAddress;
_bscAddress = addressResponse.bscAddress;
_dstAddress = addressResponse.dstAddress;
_accountSequence = userSerialNum;
// wallet-service
try {
@ -98,7 +79,7 @@ class _DepositUsdtPageState extends ConsumerState<DepositUsdtPage> {
}
}
} catch (e) {
debugPrint('加载钱包数据失败: $e');
debugPrint('加载数据失败: $e');
if (mounted) {
setState(() {
_errorMessage = '加载失败,请重试';
@ -108,35 +89,19 @@ class _DepositUsdtPageState extends ConsumerState<DepositUsdtPage> {
}
}
///
String get _currentAddress {
switch (_selectedNetwork) {
case NetworkType.kava:
return _kavaAddress ?? '';
case NetworkType.bsc:
return _bscAddress ?? '';
case NetworkType.dst:
return _dstAddress ?? '';
}
}
///
void _switchNetwork(NetworkType network) {
if (_selectedNetwork != network) {
setState(() {
_selectedNetwork = network;
});
}
/// ID
String get _depositAddress {
return _accountSequence ?? '';
}
///
void _copyAddress() {
if (_currentAddress.isEmpty) return;
if (_depositAddress.isEmpty) return;
Clipboard.setData(ClipboardData(text: _currentAddress));
Clipboard.setData(ClipboardData(text: _depositAddress));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('充值地址已复制'),
content: Text('充值ID已复制'),
backgroundColor: Color(0xFFD4AF37),
duration: Duration(seconds: 2),
),
@ -192,9 +157,7 @@ class _DepositUsdtPageState extends ConsumerState<DepositUsdtPage> {
children: [
//
_buildBalanceSection(),
//
_buildNetworkSwitch(),
const SizedBox(height: 8),
const SizedBox(height: 16),
//
_buildQrCodeCard(),
//
@ -274,87 +237,6 @@ class _DepositUsdtPageState extends ConsumerState<DepositUsdtPage> {
);
}
///
Widget _buildNetworkSwitch() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(4),
child: Row(
children: [
// KAVA
Expanded(
child: _buildNetworkButton(
label: 'KAVA',
network: NetworkType.kava,
isSelected: _selectedNetwork == NetworkType.kava,
),
),
// BSC
// Expanded(
// child: _buildNetworkButton(
// label: 'BSC',
// network: NetworkType.bsc,
// isSelected: _selectedNetwork == NetworkType.bsc,
// ),
// ),
// DST
// Expanded(
// child: _buildNetworkButton(
// label: 'DST',
// network: NetworkType.dst,
// isSelected: _selectedNetwork == NetworkType.dst,
// ),
// ),
],
),
),
);
}
///
Widget _buildNetworkButton({
required String label,
required NetworkType network,
required bool isSelected,
}) {
return GestureDetector(
onTap: () => _switchNetwork(network),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 9.5, horizontal: 12),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFFD4AF37) : Colors.transparent,
borderRadius: BorderRadius.circular(8),
boxShadow: isSelected
? const [
BoxShadow(
color: Color(0x66D4AF37),
blurRadius: 3,
offset: Offset(0, 1),
),
]
: null,
),
child: Center(
child: Text(
label,
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
height: 1.5,
color: isSelected ? Colors.white : const Color(0xFF745D43),
),
),
),
),
);
}
///
Widget _buildQrCodeCard() {
return Container(
@ -446,7 +328,7 @@ class _DepositUsdtPageState extends ConsumerState<DepositUsdtPage> {
}
//
if (_currentAddress.isEmpty) {
if (_depositAddress.isEmpty) {
return Container(
width: 192,
height: 192,
@ -456,7 +338,7 @@ class _DepositUsdtPageState extends ConsumerState<DepositUsdtPage> {
),
child: const Center(
child: Text(
'暂无充值地址',
'暂无充值ID',
style: TextStyle(
fontSize: 14,
color: Color(0xFF5D4037),
@ -483,7 +365,7 @@ class _DepositUsdtPageState extends ConsumerState<DepositUsdtPage> {
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: QrImageView(
data: _currentAddress,
data: _depositAddress,
version: QrVersions.auto,
size: 192,
backgroundColor: Colors.white,
@ -505,9 +387,9 @@ class _DepositUsdtPageState extends ConsumerState<DepositUsdtPage> {
Widget _buildAddressInfo() {
return Column(
children: [
//
// ID标
const Text(
'充值地址',
'充值ID',
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',
@ -517,16 +399,18 @@ class _DepositUsdtPageState extends ConsumerState<DepositUsdtPage> {
),
),
const SizedBox(height: 12),
//
// ID文
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
_isLoading ? '加载中...' : _currentAddress,
_isLoading ? '加载中...' : _depositAddress,
style: TextStyle(
fontSize: 14,
fontSize: 20,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
height: 1.5,
color: const Color(0xFF5D4037).withValues(alpha: 0.8),
letterSpacing: 1.5,
color: const Color(0xFF5D4037).withValues(alpha: 0.9),
),
textAlign: TextAlign.center,
),
@ -545,7 +429,7 @@ class _DepositUsdtPageState extends ConsumerState<DepositUsdtPage> {
mainAxisSize: MainAxisSize.min,
children: [
Text(
'复制地址',
'复制ID',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
@ -577,7 +461,7 @@ class _DepositUsdtPageState extends ConsumerState<DepositUsdtPage> {
return Container(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 48),
child: Text(
'仅支持 绿积分,错充将无法追回',
'请使用此充值ID进行充值',
style: TextStyle(
fontSize: 12,
fontFamily: 'Inter',

View File

@ -19,6 +19,7 @@ import '../../../../routes/app_router.dart';
import '../../../auth/presentation/providers/auth_provider.dart';
import '../widgets/team_tree_widget.dart';
import '../widgets/stacked_cards_widget.dart';
import '../../../authorization/presentation/widgets/stickman_race_widget.dart';
/// -
///
@ -103,6 +104,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
int _provinceCompanyMonthlyTarget = 500;
int _provinceCompanyMonthIndex = 0;
// /
List<StickmanRankingData> _provinceRankings = [];
List<StickmanRankingData> _cityRankings = [];
String _provinceRegionName = ''; //
String _cityRegionName = ''; //
bool _isLoadingStickmanRanking = false;
bool _hasLoadedStickmanRanking = false;
// reward-service
double _pendingUsdt = 0.0;
double _pendingPower = 0.0;
@ -513,6 +522,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_hasProvinceCompanyAuth = false;
}
});
//
_loadStickmanRankingData();
}
} catch (e, stackTrace) {
debugPrint('[ProfilePage] 加载授权数据失败: $e');
@ -533,6 +545,106 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
}
}
///
Future<void> _loadStickmanRankingData() async {
//
if (!_hasAuthProvinceCompanyAuth && !_hasAuthCityCompanyAuth) {
return;
}
if (_isLoadingStickmanRanking) return;
setState(() {
_isLoadingStickmanRanking = true;
});
try {
debugPrint('[ProfilePage] 开始加载火柴人排名数据...');
final authorizationService = ref.read(authorizationServiceProvider);
//
final now = DateTime.now();
final month = '${now.year}-${now.month.toString().padLeft(2, '0')}';
//
if (_hasAuthProvinceCompanyAuth && _authProvinceCompany != '--') {
try {
// "XX省"
final provinceName = _authProvinceCompany;
_provinceRegionName = provinceName;
final rankings = await authorizationService.getStickmanRanking(
month: month,
roleType: 'AUTH_PROVINCE_COMPANY',
regionCode: provinceName,
);
if (mounted) {
setState(() {
_provinceRankings = rankings.map((r) => StickmanRankingData(
id: r.id,
nickname: r.nickname,
avatarUrl: r.avatarUrl,
completedCount: r.completedCount,
targetCount: 50000, // 5
monthlyEarnings: r.monthlyEarnings,
isCurrentUser: r.isCurrentUser,
)).toList();
});
}
debugPrint('[ProfilePage] 省公司排名加载成功: ${_provinceRankings.length}');
} catch (e) {
debugPrint('[ProfilePage] 加载省公司排名失败: $e');
}
}
//
if (_hasAuthCityCompanyAuth && _authCityCompany != '--') {
try {
final cityName = _authCityCompany;
_cityRegionName = cityName;
final rankings = await authorizationService.getStickmanRanking(
month: month,
roleType: 'AUTH_CITY_COMPANY',
regionCode: cityName,
);
if (mounted) {
setState(() {
_cityRankings = rankings.map((r) => StickmanRankingData(
id: r.id,
nickname: r.nickname,
avatarUrl: r.avatarUrl,
completedCount: r.completedCount,
targetCount: 10000, // 1
monthlyEarnings: r.monthlyEarnings,
isCurrentUser: r.isCurrentUser,
)).toList();
});
}
debugPrint('[ProfilePage] 市公司排名加载成功: ${_cityRankings.length}');
} catch (e) {
debugPrint('[ProfilePage] 加载市公司排名失败: $e');
}
}
if (mounted) {
setState(() {
_isLoadingStickmanRanking = false;
_hasLoadedStickmanRanking = true;
});
}
} catch (e) {
debugPrint('[ProfilePage] 加载火柴人排名失败: $e');
if (mounted) {
setState(() {
_isLoadingStickmanRanking = false;
});
}
}
}
///
Future<void> _loadUnreadNotificationCount() async {
try {
@ -972,6 +1084,11 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
context.push(RoutePaths.bindEmail);
}
///
void _goToAuthorizationApply() {
context.push(RoutePaths.authorizationApply);
}
///
Future<void> _goToEditProfile() async {
final result = await context.push<bool>(RoutePaths.editProfile);
@ -1113,6 +1230,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
//
_buildUserHeader(),
const SizedBox(height: 16),
// /
_buildStickmanRaceSection(),
//
VisibilityDetector(
key: const Key('referral_section'),
@ -1292,6 +1411,71 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
);
}
///
Widget _buildStickmanRaceSection() {
//
if (!_hasAuthProvinceCompanyAuth && !_hasAuthCityCompanyAuth) {
return const SizedBox.shrink();
}
//
if (_isLoadingStickmanRanking && !_hasLoadedStickmanRanking) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0x80FFFFFF),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
),
child: const Center(
child: Column(
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
),
SizedBox(height: 8),
Text(
'加载排名数据...',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
color: Color(0xFF8B5A2B),
),
),
],
),
),
);
}
return Column(
children: [
//
if (_hasAuthProvinceCompanyAuth && _provinceRankings.isNotEmpty) ...[
StickmanRaceWidget(
rankings: _provinceRankings,
roleType: AuthorizationRoleType.province,
regionName: _provinceRegionName,
),
const SizedBox(height: 16),
],
//
if (_hasAuthCityCompanyAuth && _cityRankings.isNotEmpty) ...[
StickmanRaceWidget(
rankings: _cityRankings,
roleType: AuthorizationRoleType.city,
regionName: _cityRegionName,
),
const SizedBox(height: 16),
],
],
);
}
/// URLSVG
Widget _buildAvatarContent() {
debugPrint('[ProfilePage] _buildAvatarContent() - 开始构建头像');
@ -3530,11 +3714,12 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
),
child: Column(
children: [
_buildSettingItem(
icon: Icons.verified_user,
title: '谷歌验证器',
onTap: _goToGoogleAuth,
),
//
// _buildSettingItem(
// icon: Icons.verified_user,
// title: '谷歌验证器',
// onTap: _goToGoogleAuth,
// ),
_buildSettingItem(
icon: Icons.lock,
title: '修改登录密码',
@ -3545,6 +3730,11 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
title: '绑定邮箱',
onTap: _goToBindEmail,
),
_buildSettingItem(
icon: Icons.verified,
title: '自助申请授权',
onTap: _goToAuthorizationApply,
),
],
),
);

View File

@ -407,18 +407,19 @@ class _TradingPageState extends ConsumerState<TradingPage> {
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
const Text(
'结算的币种',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w700,
height: 1.5,
letterSpacing: 0.21,
color: Color(0xCC8B5A2B),
),
),
const SizedBox(height: 12),
// "结算的币种" label
// const Text(
// '结算的币种',
// style: TextStyle(
// fontSize: 14,
// fontFamily: 'Inter',
// fontWeight: FontWeight.w700,
// height: 1.5,
// letterSpacing: 0.21,
// color: Color(0xCC8B5A2B),
// ),
// ),
// const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
@ -432,7 +433,7 @@ class _TradingPageState extends ConsumerState<TradingPage> {
// const SizedBox(width: 8),
// _buildCurrencyChip(SettlementCurrency.og, 'OG'),
// const SizedBox(width: 8),
_buildCurrencyChip(SettlementCurrency.usdt, 'RMB/CNY'),
_buildCurrencyChip(SettlementCurrency.usdt, 'RMB/CNY提现'),
// const SizedBox(width: 8),
// _buildCurrencyChip(SettlementCurrency.dst, 'DST'),
],
@ -583,7 +584,7 @@ class _TradingPageState extends ConsumerState<TradingPage> {
),
const SizedBox(width: 8),
Text(
'提取',
'划转',
style: TextStyle(
fontSize: 16,
fontFamily: 'Inter',

View File

@ -137,7 +137,7 @@ class _WithdrawUsdtPageState extends ConsumerState<WithdrawUsdtPage> {
//
if (!_isValidAddress(address)) {
_showErrorSnackBar('请输入有效的钱包地址');
_showErrorSnackBar('请输入有效的充值用户ID');
return;
}
@ -176,8 +176,16 @@ class _WithdrawUsdtPageState extends ConsumerState<WithdrawUsdtPage> {
///
bool _isValidAddress(String address) {
//
// KAVA BSC 0x 42
//
// 1. accountSequence D + (6) + (5)12 D25121400005
// 2.
// accountSequence
if (_isAccountSequence(address)) {
return true;
}
//
if (_selectedNetwork == WithdrawNetwork.kava) {
// KAVA kava1... 0x...
return address.startsWith('kava1') ||
@ -188,6 +196,16 @@ class _WithdrawUsdtPageState extends ConsumerState<WithdrawUsdtPage> {
}
}
/// accountSequence
/// D + (6) + (5)12 D25121400005
bool _isAccountSequence(String address) {
if (address.length != 12) return false;
if (!address.startsWith('D')) return false;
// 11
final numericPart = address.substring(1);
return RegExp(r'^\d{11}$').hasMatch(numericPart);
}
///
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
@ -579,7 +597,7 @@ class _WithdrawUsdtPageState extends ConsumerState<WithdrawUsdtPage> {
decoration: InputDecoration(
contentPadding: const EdgeInsets.all(16),
border: InputBorder.none,
hintText: '请输入接收绿积分的地址',
hintText: '请输入充值用户ID',
hintStyle: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
@ -836,7 +854,7 @@ class _WithdrawUsdtPageState extends ConsumerState<WithdrawUsdtPage> {
const SizedBox(height: 12),
_buildNoticeItem('请确保接收地址正确,错误地址将导致资产丢失'),
_buildNoticeItem('请选择正确的网络,不同网络之间不可互转'),
_buildNoticeItem('提取需要进行谷歌验证器验证'),
_buildNoticeItem('提取需要进行手机短信验证'),
_buildNoticeItem('提取通常在 1-30 分钟内到账'),
],
),
@ -1013,7 +1031,7 @@ class _AddressQrScannerPageState extends State<_AddressQrScannerPage> {
onPressed: () => Navigator.of(context).pop(),
),
title: Text(
'扫描钱包地址',
'扫描充值ID',
style: TextStyle(
fontSize: 18.sp,
color: Colors.white,
@ -1047,7 +1065,7 @@ class _AddressQrScannerPageState extends State<_AddressQrScannerPage> {
child: Column(
children: [
Text(
'钱包地址二维码放入框内',
'充值ID二维码放入框内',
style: TextStyle(
fontSize: 14.sp,
color: Colors.white70,

View File

@ -9,6 +9,9 @@ import '../features/auth/presentation/pages/backup_mnemonic_page.dart';
import '../features/auth/presentation/pages/verify_mnemonic_page.dart';
import '../features/auth/presentation/pages/wallet_created_page.dart';
import '../features/auth/presentation/pages/import_mnemonic_page.dart';
import '../features/auth/presentation/pages/phone_register_page.dart';
import '../features/auth/presentation/pages/sms_verify_page.dart';
import '../features/auth/presentation/pages/set_password_page.dart';
import '../features/home/presentation/pages/home_shell_page.dart';
import '../features/ranking/presentation/pages/ranking_page.dart';
import '../features/mining/presentation/pages/mining_page.dart';
@ -23,6 +26,7 @@ import '../features/planting/presentation/pages/planting_location_page.dart';
import '../features/security/presentation/pages/google_auth_page.dart';
import '../features/security/presentation/pages/change_password_page.dart';
import '../features/security/presentation/pages/bind_email_page.dart';
import '../features/authorization/presentation/pages/authorization_apply_page.dart';
import '../features/withdraw/presentation/pages/withdraw_usdt_page.dart';
import '../features/withdraw/presentation/pages/withdraw_confirm_page.dart';
import '../features/notification/presentation/pages/notification_inbox_page.dart';
@ -129,7 +133,46 @@ final appRouterProvider = Provider<GoRouter>((ref) {
builder: (context, state) => const ImportMnemonicPage(),
),
// Backup Mnemonic ( - API )
// Phone Register ()
GoRoute(
path: RoutePaths.phoneRegister,
name: RouteNames.phoneRegister,
builder: (context, state) {
final params = state.extra as PhoneRegisterParams?;
return PhoneRegisterPage(
inviterReferralCode: params?.inviterReferralCode,
);
},
),
// SMS Verify ()
GoRoute(
path: RoutePaths.smsVerify,
name: RouteNames.smsVerify,
builder: (context, state) {
final params = state.extra as SmsVerifyParams;
return SmsVerifyPage(
phoneNumber: params.phoneNumber,
type: params.type,
inviterReferralCode: params.inviterReferralCode,
);
},
),
// Set Password ()
GoRoute(
path: RoutePaths.setPassword,
name: RouteNames.setPassword,
builder: (context, state) {
final params = state.extra as SetPasswordParams;
return SetPasswordPage(
userSerialNum: params.userSerialNum,
inviterReferralCode: params.inviterReferralCode,
);
},
),
// Backup Mnemonic ( - API )
GoRoute(
path: RoutePaths.backupMnemonic,
name: RouteNames.backupMnemonic,
@ -258,6 +301,13 @@ final appRouterProvider = Provider<GoRouter>((ref) {
builder: (context, state) => const BindEmailPage(),
),
// Authorization Apply Page ()
GoRoute(
path: RoutePaths.authorizationApply,
name: RouteNames.authorizationApply,
builder: (context, state) => const AuthorizationApplyPage(),
),
// Ledger Detail Page ()
GoRoute(
path: RoutePaths.ledgerDetail,

View File

@ -11,6 +11,9 @@ class RouteNames {
static const walletCreated = 'wallet-created';
static const importWallet = 'import-wallet';
static const importMnemonic = 'import-mnemonic';
static const phoneRegister = 'phone-register';
static const smsVerify = 'sms-verify';
static const setPassword = 'set-password';
// Main tabs
static const ranking = 'ranking';
@ -31,6 +34,7 @@ class RouteNames {
static const googleAuth = 'google-auth';
static const changePassword = 'change-password';
static const bindEmail = 'bind-email';
static const authorizationApply = 'authorization-apply';
static const transactionHistory = 'transaction-history';
static const ledgerDetail = 'ledger-detail';
static const withdrawUsdt = 'withdraw-usdt';

View File

@ -11,6 +11,9 @@ class RoutePaths {
static const walletCreated = '/auth/wallet-created';
static const importWallet = '/auth/import';
static const importMnemonic = '/auth/import-mnemonic';
static const phoneRegister = '/auth/phone-register';
static const smsVerify = '/auth/sms-verify';
static const setPassword = '/auth/set-password';
// Main tabs
static const ranking = '/ranking';
@ -31,6 +34,7 @@ class RoutePaths {
static const googleAuth = '/security/google-auth';
static const changePassword = '/security/password';
static const bindEmail = '/security/email';
static const authorizationApply = '/authorization/apply';
static const transactionHistory = '/trading/history';
static const ledgerDetail = '/trading/ledger';
static const withdrawUsdt = '/withdraw/usdt';