feat(profile): integrate referral and authorization APIs for profile page
- Add Kong routes for identity-service /me, referral-service, and authorization-service - Create AuthorizationService in Flutter for fetching user authorizations - Extend ReferralService with getMyReferralInfo() and getDirectReferrals() methods - Update profile_page.dart to display real team stats from APIs - Fix authorization-service JWT strategy to accept identity-service token format - Add decimal.js dependency to authorization-service - Add prisma migration file for authorization-service 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
15f617c9a9
commit
d0487c4a7e
|
|
@ -32,6 +32,10 @@ services:
|
||||||
paths:
|
paths:
|
||||||
- /api/v1/auth
|
- /api/v1/auth
|
||||||
strip_path: false
|
strip_path: false
|
||||||
|
- name: identity-me
|
||||||
|
paths:
|
||||||
|
- /api/v1/me
|
||||||
|
strip_path: false
|
||||||
- name: identity-user
|
- name: identity-user
|
||||||
paths:
|
paths:
|
||||||
- /api/v1/user
|
- /api/v1/user
|
||||||
|
|
@ -90,9 +94,21 @@ services:
|
||||||
url: http://192.168.1.111:3004
|
url: http://192.168.1.111:3004
|
||||||
routes:
|
routes:
|
||||||
- name: referral-api
|
- name: referral-api
|
||||||
|
paths:
|
||||||
|
- /api/v1/referral
|
||||||
|
strip_path: false
|
||||||
|
- name: referral-referrals
|
||||||
paths:
|
paths:
|
||||||
- /api/v1/referrals
|
- /api/v1/referrals
|
||||||
strip_path: false
|
strip_path: false
|
||||||
|
- name: referral-leaderboard
|
||||||
|
paths:
|
||||||
|
- /api/v1/leaderboard
|
||||||
|
strip_path: false
|
||||||
|
- name: referral-team-statistics
|
||||||
|
paths:
|
||||||
|
- /api/v1/team-statistics
|
||||||
|
strip_path: false
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Reward Service - 奖励服务
|
# Reward Service - 奖励服务
|
||||||
|
|
@ -151,9 +167,11 @@ services:
|
||||||
routes:
|
routes:
|
||||||
- name: authorization-api
|
- name: authorization-api
|
||||||
paths:
|
paths:
|
||||||
- /api/v1/authorization
|
- /api/v1/authorizations
|
||||||
- /api/v1/permissions
|
strip_path: false
|
||||||
- /api/v1/roles
|
- name: authorization-admin
|
||||||
|
paths:
|
||||||
|
- /api/v1/admin/authorizations
|
||||||
strip_path: false
|
strip_path: false
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -182,6 +200,21 @@ services:
|
||||||
- /api/v1/presence
|
- /api/v1/presence
|
||||||
strip_path: false
|
strip_path: false
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Blockchain Service - 区块链服务
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
- name: blockchain-service
|
||||||
|
url: http://192.168.1.111:3012
|
||||||
|
routes:
|
||||||
|
- name: blockchain-deposit
|
||||||
|
paths:
|
||||||
|
- /api/v1/deposit
|
||||||
|
strip_path: false
|
||||||
|
- name: blockchain-balance
|
||||||
|
paths:
|
||||||
|
- /api/v1/balance
|
||||||
|
strip_path: false
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Plugins - 全局插件配置
|
# Plugins - 全局插件配置
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -43,5 +43,4 @@ lerna-debug.log*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
# Prisma
|
# Prisma
|
||||||
prisma/migrations/*
|
prisma/migrations/**/migration_lock.toml
|
||||||
!prisma/migrations/.gitkeep
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
"@prisma/client": "^5.7.0",
|
"@prisma/client": "^5.7.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
|
"decimal.js": "^10.4.3",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"kafkajs": "^2.2.4",
|
"kafkajs": "^2.2.4",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
|
|
@ -246,6 +247,7 @@
|
||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
|
|
@ -4272,6 +4274,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decimal.js": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||||
|
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/dedent": {
|
"node_modules/dedent": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@
|
||||||
"@prisma/client": "^5.7.0",
|
"@prisma/client": "^5.7.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
|
"decimal.js": "^10.4.3",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"kafkajs": "^2.2.4",
|
"kafkajs": "^2.2.4",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,371 @@
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "RoleType" AS ENUM ('COMMUNITY', 'AUTH_PROVINCE_COMPANY', 'PROVINCE_COMPANY', 'AUTH_CITY_COMPANY', 'CITY_COMPANY');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "AuthorizationStatus" AS ENUM ('PENDING', 'AUTHORIZED', 'REVOKED');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "AssessmentResult" AS ENUM ('NOT_ASSESSED', 'PASS', 'FAIL', 'BYPASSED');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "MonthlyTargetType" AS ENUM ('NONE', 'FIXED', 'LADDER');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "RestrictionType" AS ENUM ('ACCOUNT_LIMIT', 'TOTAL_LIMIT');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "ApprovalStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "OperationType" AS ENUM ('GRANT_AUTHORIZATION', 'REVOKE_AUTHORIZATION', 'GRANT_BYPASS', 'EXEMPT_PERCENTAGE', 'MODIFY_CONFIG');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "RegionType" AS ENUM ('PROVINCE', 'CITY');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "SystemAccountType" AS ENUM ('COST_ACCOUNT', 'OPERATION_ACCOUNT', 'HQ_COMMUNITY', 'RWAD_POOL_PENDING', 'SYSTEM_PROVINCE', 'SYSTEM_CITY');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "SystemLedgerEntryType" AS ENUM ('PLANTING_ALLOCATION', 'REWARD_EXPIRED', 'TRANSFER_OUT', 'TRANSFER_IN', 'WITHDRAWAL', 'ADJUSTMENT');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "authorization_roles" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"user_id" TEXT NOT NULL,
|
||||||
|
"role_type" "RoleType" NOT NULL,
|
||||||
|
"region_code" TEXT NOT NULL,
|
||||||
|
"region_name" TEXT NOT NULL,
|
||||||
|
"status" "AuthorizationStatus" NOT NULL DEFAULT 'PENDING',
|
||||||
|
"display_title" TEXT NOT NULL,
|
||||||
|
"authorized_at" TIMESTAMP(3),
|
||||||
|
"authorized_by" TEXT,
|
||||||
|
"revoked_at" TIMESTAMP(3),
|
||||||
|
"revoked_by" TEXT,
|
||||||
|
"revoke_reason" TEXT,
|
||||||
|
"initial_target_tree_count" INTEGER NOT NULL,
|
||||||
|
"monthly_target_type" "MonthlyTargetType" NOT NULL,
|
||||||
|
"require_local_percentage" DECIMAL(5,2) NOT NULL DEFAULT 5.0,
|
||||||
|
"exempt_from_percentage_check" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"benefit_active" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"benefit_activated_at" TIMESTAMP(3),
|
||||||
|
"benefit_deactivated_at" TIMESTAMP(3),
|
||||||
|
"current_month_index" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "authorization_roles_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "monthly_assessments" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"authorization_id" TEXT NOT NULL,
|
||||||
|
"user_id" TEXT NOT NULL,
|
||||||
|
"role_type" "RoleType" NOT NULL,
|
||||||
|
"region_code" TEXT NOT NULL,
|
||||||
|
"assessment_month" TEXT NOT NULL,
|
||||||
|
"month_index" INTEGER NOT NULL,
|
||||||
|
"monthly_target" INTEGER NOT NULL,
|
||||||
|
"cumulative_target" INTEGER NOT NULL,
|
||||||
|
"monthly_completed" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"cumulative_completed" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"completed_at" TIMESTAMP(3),
|
||||||
|
"local_team_count" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"total_team_count" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"local_percentage" DECIMAL(5,2) NOT NULL DEFAULT 0,
|
||||||
|
"local_percentage_pass" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"exceed_ratio" DECIMAL(10,4) NOT NULL DEFAULT 0,
|
||||||
|
"result" "AssessmentResult" NOT NULL DEFAULT 'NOT_ASSESSED',
|
||||||
|
"ranking_in_region" INTEGER,
|
||||||
|
"is_first_place" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"is_bypassed" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"bypassed_by" TEXT,
|
||||||
|
"bypassed_at" TIMESTAMP(3),
|
||||||
|
"assessed_at" TIMESTAMP(3),
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "monthly_assessments_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "monthly_bypasses" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"authorization_id" TEXT NOT NULL,
|
||||||
|
"user_id" TEXT NOT NULL,
|
||||||
|
"role_type" "RoleType" NOT NULL,
|
||||||
|
"bypass_month" TEXT NOT NULL,
|
||||||
|
"granted_by" TEXT NOT NULL,
|
||||||
|
"granted_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
"reason" TEXT,
|
||||||
|
"approver1_id" TEXT NOT NULL,
|
||||||
|
"approver1_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
"approver2_id" TEXT,
|
||||||
|
"approver2_at" TIMESTAMP(3),
|
||||||
|
"approver3_id" TEXT,
|
||||||
|
"approver3_at" TIMESTAMP(3),
|
||||||
|
"approval_status" "ApprovalStatus" NOT NULL DEFAULT 'PENDING',
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "monthly_bypasses_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ladder_target_configs" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"role_type" "RoleType" NOT NULL,
|
||||||
|
"month_index" INTEGER NOT NULL,
|
||||||
|
"monthly_target" INTEGER NOT NULL,
|
||||||
|
"cumulative_target" INTEGER NOT NULL,
|
||||||
|
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ladder_target_configs_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "planting_restrictions" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"restriction_type" "RestrictionType" NOT NULL,
|
||||||
|
"account_limit_days" INTEGER,
|
||||||
|
"account_limit_count" INTEGER,
|
||||||
|
"total_limit_days" INTEGER,
|
||||||
|
"total_limit_count" INTEGER,
|
||||||
|
"current_total_count" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"start_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
"end_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"created_by" TEXT NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "planting_restrictions_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "admin_approvals" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"operation_type" "OperationType" NOT NULL,
|
||||||
|
"target_id" TEXT NOT NULL,
|
||||||
|
"target_type" TEXT NOT NULL,
|
||||||
|
"request_data" JSONB NOT NULL,
|
||||||
|
"status" "ApprovalStatus" NOT NULL DEFAULT 'PENDING',
|
||||||
|
"requester_id" TEXT NOT NULL,
|
||||||
|
"approver1_id" TEXT,
|
||||||
|
"approver1_at" TIMESTAMP(3),
|
||||||
|
"approver2_id" TEXT,
|
||||||
|
"approver2_at" TIMESTAMP(3),
|
||||||
|
"approver3_id" TEXT,
|
||||||
|
"approver3_at" TIMESTAMP(3),
|
||||||
|
"completed_at" TIMESTAMP(3),
|
||||||
|
"rejected_by" TEXT,
|
||||||
|
"rejected_at" TIMESTAMP(3),
|
||||||
|
"reject_reason" TEXT,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "admin_approvals_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "authorization_audit_logs" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"operation_type" TEXT NOT NULL,
|
||||||
|
"target_user_id" TEXT NOT NULL,
|
||||||
|
"target_role_type" "RoleType",
|
||||||
|
"target_region_code" TEXT,
|
||||||
|
"operator_id" TEXT NOT NULL,
|
||||||
|
"operator_role" TEXT NOT NULL,
|
||||||
|
"before_state" JSONB,
|
||||||
|
"after_state" JSONB,
|
||||||
|
"ip_address" TEXT,
|
||||||
|
"user_agent" TEXT,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "authorization_audit_logs_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "region_heat_maps" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"region_code" TEXT NOT NULL,
|
||||||
|
"region_name" TEXT NOT NULL,
|
||||||
|
"region_type" "RegionType" NOT NULL,
|
||||||
|
"total_plantings" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"monthly_plantings" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"weekly_plantings" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"daily_plantings" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"active_users" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"auth_company_count" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"heat_score" DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "region_heat_maps_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "stickman_rankings" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"user_id" TEXT NOT NULL,
|
||||||
|
"authorization_id" TEXT NOT NULL,
|
||||||
|
"role_type" "RoleType" NOT NULL,
|
||||||
|
"region_code" TEXT NOT NULL,
|
||||||
|
"region_name" TEXT NOT NULL,
|
||||||
|
"nickname" TEXT NOT NULL,
|
||||||
|
"avatar_url" TEXT,
|
||||||
|
"current_month" TEXT NOT NULL,
|
||||||
|
"cumulative_completed" INTEGER NOT NULL,
|
||||||
|
"cumulative_target" INTEGER NOT NULL,
|
||||||
|
"progress_percentage" DECIMAL(5,2) NOT NULL,
|
||||||
|
"exceed_ratio" DECIMAL(10,4) NOT NULL,
|
||||||
|
"ranking" INTEGER NOT NULL,
|
||||||
|
"is_first_place" BOOLEAN NOT NULL,
|
||||||
|
"monthly_reward_usdt" DECIMAL(18,2) NOT NULL,
|
||||||
|
"monthly_reward_rwad" DECIMAL(18,8) NOT NULL,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "stickman_rankings_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "authorization_configs" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"config_key" TEXT NOT NULL,
|
||||||
|
"config_value" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "authorization_configs_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "system_accounts" (
|
||||||
|
"account_id" BIGSERIAL NOT NULL,
|
||||||
|
"account_type" "SystemAccountType" NOT NULL,
|
||||||
|
"region_code" VARCHAR(10),
|
||||||
|
"region_name" VARCHAR(50),
|
||||||
|
"wallet_address" VARCHAR(42),
|
||||||
|
"mpc_public_key" VARCHAR(130),
|
||||||
|
"usdt_balance" DECIMAL(20,8) NOT NULL DEFAULT 0,
|
||||||
|
"hashpower" DECIMAL(20,8) NOT NULL DEFAULT 0,
|
||||||
|
"total_received" DECIMAL(20,8) NOT NULL DEFAULT 0,
|
||||||
|
"total_transferred" DECIMAL(20,8) NOT NULL DEFAULT 0,
|
||||||
|
"status" VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "system_accounts_pkey" PRIMARY KEY ("account_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "system_account_ledgers" (
|
||||||
|
"ledger_id" BIGSERIAL NOT NULL,
|
||||||
|
"account_id" BIGINT NOT NULL,
|
||||||
|
"entry_type" "SystemLedgerEntryType" NOT NULL,
|
||||||
|
"amount" DECIMAL(20,8) NOT NULL,
|
||||||
|
"balance_after" DECIMAL(20,8) NOT NULL,
|
||||||
|
"source_order_id" BIGINT,
|
||||||
|
"source_reward_id" BIGINT,
|
||||||
|
"tx_hash" VARCHAR(66),
|
||||||
|
"memo" VARCHAR(500),
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "system_account_ledgers_pkey" PRIMARY KEY ("ledger_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "authorization_roles_user_id_idx" ON "authorization_roles"("user_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "authorization_roles_role_type_region_code_idx" ON "authorization_roles"("role_type", "region_code");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "authorization_roles_status_idx" ON "authorization_roles"("status");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "authorization_roles_role_type_status_idx" ON "authorization_roles"("role_type", "status");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "authorization_roles_user_id_role_type_region_code_key" ON "authorization_roles"("user_id", "role_type", "region_code");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "monthly_assessments_user_id_assessment_month_idx" ON "monthly_assessments"("user_id", "assessment_month");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "monthly_assessments_role_type_region_code_assessment_month_idx" ON "monthly_assessments"("role_type", "region_code", "assessment_month");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "monthly_assessments_assessment_month_result_idx" ON "monthly_assessments"("assessment_month", "result");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "monthly_assessments_assessment_month_role_type_exceed_ratio_idx" ON "monthly_assessments"("assessment_month", "role_type", "exceed_ratio" DESC);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "monthly_assessments_authorization_id_assessment_month_key" ON "monthly_assessments"("authorization_id", "assessment_month");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "monthly_bypasses_user_id_bypass_month_idx" ON "monthly_bypasses"("user_id", "bypass_month");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "monthly_bypasses_authorization_id_bypass_month_key" ON "monthly_bypasses"("authorization_id", "bypass_month");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ladder_target_configs_role_type_month_index_key" ON "ladder_target_configs"("role_type", "month_index");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "admin_approvals_status_idx" ON "admin_approvals"("status");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "admin_approvals_target_id_target_type_idx" ON "admin_approvals"("target_id", "target_type");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "authorization_audit_logs_target_user_id_idx" ON "authorization_audit_logs"("target_user_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "authorization_audit_logs_operator_id_idx" ON "authorization_audit_logs"("operator_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "authorization_audit_logs_created_at_idx" ON "authorization_audit_logs"("created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "region_heat_maps_region_code_region_type_key" ON "region_heat_maps"("region_code", "region_type");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "stickman_rankings_role_type_region_code_current_month_idx" ON "stickman_rankings"("role_type", "region_code", "current_month");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "stickman_rankings_authorization_id_current_month_key" ON "stickman_rankings"("authorization_id", "current_month");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "authorization_configs_config_key_key" ON "authorization_configs"("config_key");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "idx_system_account_type" ON "system_accounts"("account_type");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "idx_system_wallet_address" ON "system_accounts"("wallet_address");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "system_accounts_account_type_region_code_key" ON "system_accounts"("account_type", "region_code");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "idx_system_ledger_account_created" ON "system_account_ledgers"("account_id", "created_at" DESC);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "idx_system_ledger_source_order" ON "system_account_ledgers"("source_order_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "idx_system_ledger_tx_hash" ON "system_account_ledgers"("tx_hash");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "monthly_assessments" ADD CONSTRAINT "monthly_assessments_authorization_id_fkey" FOREIGN KEY ("authorization_id") REFERENCES "authorization_roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "monthly_bypasses" ADD CONSTRAINT "monthly_bypasses_authorization_id_fkey" FOREIGN KEY ("authorization_id") REFERENCES "authorization_roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "system_account_ledgers" ADD CONSTRAINT "system_account_ledgers_account_id_fkey" FOREIGN KEY ("account_id") REFERENCES "system_accounts"("account_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
|
@ -4,7 +4,13 @@ import { ExtractJwt, Strategy } from 'passport-jwt'
|
||||||
import { ConfigService } from '@nestjs/config'
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
|
||||||
export interface JwtPayload {
|
export interface JwtPayload {
|
||||||
sub: string
|
// Identity-service uses 'userId' field
|
||||||
|
userId: string
|
||||||
|
accountSequence?: number
|
||||||
|
deviceId?: string
|
||||||
|
type?: string
|
||||||
|
// Legacy support for 'sub' field
|
||||||
|
sub?: string
|
||||||
walletAddress?: string
|
walletAddress?: string
|
||||||
roles?: string[]
|
roles?: string[]
|
||||||
iat?: number
|
iat?: number
|
||||||
|
|
@ -22,8 +28,12 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async validate(payload: JwtPayload) {
|
async validate(payload: JwtPayload) {
|
||||||
|
// Support both 'userId' (from identity-service) and 'sub' (legacy)
|
||||||
|
const userId = payload.userId || payload.sub
|
||||||
return {
|
return {
|
||||||
userId: payload.sub,
|
userId,
|
||||||
|
accountSequence: payload.accountSequence,
|
||||||
|
deviceId: payload.deviceId,
|
||||||
walletAddress: payload.walletAddress,
|
walletAddress: payload.walletAddress,
|
||||||
roles: payload.roles,
|
roles: payload.roles,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,11 +67,17 @@ class ApiEndpoints {
|
||||||
// Community & Referral (-> Referral Service)
|
// Community & Referral (-> Referral Service)
|
||||||
static const String community = '$apiPrefix/community';
|
static const String community = '$apiPrefix/community';
|
||||||
static const String referral = '$apiPrefix/referral';
|
static const String referral = '$apiPrefix/referral';
|
||||||
|
static const String referralMe = '$referral/me'; // 获取当前用户推荐信息
|
||||||
|
static const String referralDirects = '$referral/me/direct-referrals'; // 获取直推列表
|
||||||
static const String referralList = '$community/referrals';
|
static const String referralList = '$community/referrals';
|
||||||
static const String earnings = '$community/earnings';
|
static const String earnings = '$community/earnings';
|
||||||
static const String claimEarnings = '$community/claim';
|
static const String claimEarnings = '$community/claim';
|
||||||
static const String generateReferralLink = '$referral/generate-link';
|
static const String generateReferralLink = '$referral/generate-link';
|
||||||
|
|
||||||
|
// Authorization (-> Authorization Service)
|
||||||
|
static const String authorizations = '$apiPrefix/authorizations';
|
||||||
|
static const String myAuthorizations = '$authorizations/my'; // 获取我的授权列表
|
||||||
|
|
||||||
// Telemetry (-> Reporting Service)
|
// Telemetry (-> Reporting Service)
|
||||||
static const String telemetry = '$apiPrefix/telemetry';
|
static const String telemetry = '$apiPrefix/telemetry';
|
||||||
static const String telemetrySession = '$telemetry/session';
|
static const String telemetrySession = '$telemetry/session';
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import '../storage/local_storage.dart';
|
||||||
import '../network/api_client.dart';
|
import '../network/api_client.dart';
|
||||||
import '../services/account_service.dart';
|
import '../services/account_service.dart';
|
||||||
import '../services/referral_service.dart';
|
import '../services/referral_service.dart';
|
||||||
|
import '../services/authorization_service.dart';
|
||||||
import '../services/deposit_service.dart';
|
import '../services/deposit_service.dart';
|
||||||
|
|
||||||
// Storage Providers
|
// Storage Providers
|
||||||
|
|
@ -37,6 +38,12 @@ final referralServiceProvider = Provider<ReferralService>((ref) {
|
||||||
return ReferralService(apiClient: apiClient);
|
return ReferralService(apiClient: apiClient);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Authorization Service Provider
|
||||||
|
final authorizationServiceProvider = Provider<AuthorizationService>((ref) {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
return AuthorizationService(apiClient: apiClient);
|
||||||
|
});
|
||||||
|
|
||||||
// Deposit Service Provider
|
// Deposit Service Provider
|
||||||
final depositServiceProvider = Provider<DepositService>((ref) {
|
final depositServiceProvider = Provider<DepositService>((ref) {
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,245 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import '../network/api_client.dart';
|
||||||
|
import '../constants/api_endpoints.dart';
|
||||||
|
|
||||||
|
/// 角色类型
|
||||||
|
enum RoleType {
|
||||||
|
community,
|
||||||
|
provinceCompany,
|
||||||
|
cityCompany,
|
||||||
|
}
|
||||||
|
|
||||||
|
extension RoleTypeExtension on RoleType {
|
||||||
|
String get value {
|
||||||
|
switch (this) {
|
||||||
|
case RoleType.community:
|
||||||
|
return 'COMMUNITY';
|
||||||
|
case RoleType.provinceCompany:
|
||||||
|
return 'PROVINCE_COMPANY';
|
||||||
|
case RoleType.cityCompany:
|
||||||
|
return 'CITY_COMPANY';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get displayName {
|
||||||
|
switch (this) {
|
||||||
|
case RoleType.community:
|
||||||
|
return '社区';
|
||||||
|
case RoleType.provinceCompany:
|
||||||
|
return '省公司';
|
||||||
|
case RoleType.cityCompany:
|
||||||
|
return '市公司';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static RoleType? fromString(String? value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
switch (value.toUpperCase()) {
|
||||||
|
case 'COMMUNITY':
|
||||||
|
return RoleType.community;
|
||||||
|
case 'PROVINCE_COMPANY':
|
||||||
|
return RoleType.provinceCompany;
|
||||||
|
case 'CITY_COMPANY':
|
||||||
|
return RoleType.cityCompany;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 授权状态
|
||||||
|
enum AuthorizationStatus {
|
||||||
|
pending,
|
||||||
|
active,
|
||||||
|
suspended,
|
||||||
|
revoked,
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AuthorizationStatusExtension on AuthorizationStatus {
|
||||||
|
String get value {
|
||||||
|
switch (this) {
|
||||||
|
case AuthorizationStatus.pending:
|
||||||
|
return 'PENDING';
|
||||||
|
case AuthorizationStatus.active:
|
||||||
|
return 'ACTIVE';
|
||||||
|
case AuthorizationStatus.suspended:
|
||||||
|
return 'SUSPENDED';
|
||||||
|
case AuthorizationStatus.revoked:
|
||||||
|
return 'REVOKED';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static AuthorizationStatus fromString(String? value) {
|
||||||
|
switch (value?.toUpperCase()) {
|
||||||
|
case 'PENDING':
|
||||||
|
return AuthorizationStatus.pending;
|
||||||
|
case 'ACTIVE':
|
||||||
|
return AuthorizationStatus.active;
|
||||||
|
case 'SUSPENDED':
|
||||||
|
return AuthorizationStatus.suspended;
|
||||||
|
case 'REVOKED':
|
||||||
|
return AuthorizationStatus.revoked;
|
||||||
|
default:
|
||||||
|
return AuthorizationStatus.pending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 授权信息响应
|
||||||
|
class AuthorizationResponse {
|
||||||
|
final String authorizationId;
|
||||||
|
final String userId;
|
||||||
|
final RoleType roleType;
|
||||||
|
final String regionCode;
|
||||||
|
final String regionName;
|
||||||
|
final AuthorizationStatus status;
|
||||||
|
final String displayTitle;
|
||||||
|
final bool benefitActive;
|
||||||
|
final int currentMonthIndex;
|
||||||
|
final double requireLocalPercentage;
|
||||||
|
final bool exemptFromPercentageCheck;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
|
||||||
|
AuthorizationResponse({
|
||||||
|
required this.authorizationId,
|
||||||
|
required this.userId,
|
||||||
|
required this.roleType,
|
||||||
|
required this.regionCode,
|
||||||
|
required this.regionName,
|
||||||
|
required this.status,
|
||||||
|
required this.displayTitle,
|
||||||
|
required this.benefitActive,
|
||||||
|
required this.currentMonthIndex,
|
||||||
|
required this.requireLocalPercentage,
|
||||||
|
required this.exemptFromPercentageCheck,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory AuthorizationResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return AuthorizationResponse(
|
||||||
|
authorizationId: json['authorizationId']?.toString() ?? '',
|
||||||
|
userId: json['userId']?.toString() ?? '',
|
||||||
|
roleType: RoleTypeExtension.fromString(json['roleType']) ?? RoleType.community,
|
||||||
|
regionCode: json['regionCode'] ?? '',
|
||||||
|
regionName: json['regionName'] ?? '',
|
||||||
|
status: AuthorizationStatusExtension.fromString(json['status']),
|
||||||
|
displayTitle: json['displayTitle'] ?? '',
|
||||||
|
benefitActive: json['benefitActive'] ?? false,
|
||||||
|
currentMonthIndex: json['currentMonthIndex'] ?? 0,
|
||||||
|
requireLocalPercentage: (json['requireLocalPercentage'] ?? 0).toDouble(),
|
||||||
|
exemptFromPercentageCheck: json['exemptFromPercentageCheck'] ?? false,
|
||||||
|
createdAt: json['createdAt'] != null
|
||||||
|
? DateTime.parse(json['createdAt'])
|
||||||
|
: DateTime.now(),
|
||||||
|
updatedAt: json['updatedAt'] != null
|
||||||
|
? DateTime.parse(json['updatedAt'])
|
||||||
|
: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 用户授权汇总信息
|
||||||
|
class UserAuthorizationSummary {
|
||||||
|
/// 社区授权 (如果有)
|
||||||
|
final AuthorizationResponse? community;
|
||||||
|
/// 省公司授权 (如果有)
|
||||||
|
final AuthorizationResponse? provinceCompany;
|
||||||
|
/// 市公司授权 (如果有)
|
||||||
|
final AuthorizationResponse? cityCompany;
|
||||||
|
/// 所有授权列表
|
||||||
|
final List<AuthorizationResponse> allAuthorizations;
|
||||||
|
|
||||||
|
UserAuthorizationSummary({
|
||||||
|
this.community,
|
||||||
|
this.provinceCompany,
|
||||||
|
this.cityCompany,
|
||||||
|
required this.allAuthorizations,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 是否有任何授权
|
||||||
|
bool get hasAnyAuthorization => allAuthorizations.isNotEmpty;
|
||||||
|
|
||||||
|
/// 社区名称
|
||||||
|
String? get communityName => community?.displayTitle;
|
||||||
|
|
||||||
|
/// 省公司名称
|
||||||
|
String? get provinceCompanyName => provinceCompany?.displayTitle;
|
||||||
|
|
||||||
|
/// 市公司名称
|
||||||
|
String? get cityCompanyName => cityCompany?.displayTitle;
|
||||||
|
|
||||||
|
factory UserAuthorizationSummary.fromList(List<AuthorizationResponse> authorizations) {
|
||||||
|
AuthorizationResponse? community;
|
||||||
|
AuthorizationResponse? provinceCompany;
|
||||||
|
AuthorizationResponse? cityCompany;
|
||||||
|
|
||||||
|
for (final auth in authorizations) {
|
||||||
|
switch (auth.roleType) {
|
||||||
|
case RoleType.community:
|
||||||
|
community = auth;
|
||||||
|
break;
|
||||||
|
case RoleType.provinceCompany:
|
||||||
|
provinceCompany = auth;
|
||||||
|
break;
|
||||||
|
case RoleType.cityCompany:
|
||||||
|
cityCompany = auth;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return UserAuthorizationSummary(
|
||||||
|
community: community,
|
||||||
|
provinceCompany: provinceCompany,
|
||||||
|
cityCompany: cityCompany,
|
||||||
|
allAuthorizations: authorizations,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 授权服务
|
||||||
|
///
|
||||||
|
/// 处理用户授权相关功能:
|
||||||
|
/// - 获取用户授权列表
|
||||||
|
/// - 社区/省公司/市公司授权信息
|
||||||
|
class AuthorizationService {
|
||||||
|
final ApiClient _apiClient;
|
||||||
|
|
||||||
|
AuthorizationService({required ApiClient apiClient}) : _apiClient = apiClient;
|
||||||
|
|
||||||
|
/// 获取我的授权列表
|
||||||
|
///
|
||||||
|
/// 调用 GET /authorizations/my (authorization-service)
|
||||||
|
Future<List<AuthorizationResponse>> getMyAuthorizations() async {
|
||||||
|
try {
|
||||||
|
debugPrint('获取授权列表...');
|
||||||
|
final response = await _apiClient.get(ApiEndpoints.myAuthorizations);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = response.data;
|
||||||
|
if (data is List) {
|
||||||
|
final authorizations = data
|
||||||
|
.map((e) => AuthorizationResponse.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
debugPrint('授权列表获取成功: ${authorizations.length} 个授权');
|
||||||
|
return authorizations;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception('获取授权列表失败');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('获取授权列表失败: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取用户授权汇总信息
|
||||||
|
///
|
||||||
|
/// 方便前端快速访问社区/省公司/市公司信息
|
||||||
|
Future<UserAuthorizationSummary> getMyAuthorizationSummary() async {
|
||||||
|
final authorizations = await getMyAuthorizations();
|
||||||
|
return UserAuthorizationSummary.fromList(authorizations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,102 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import '../network/api_client.dart';
|
import '../network/api_client.dart';
|
||||||
|
import '../constants/api_endpoints.dart';
|
||||||
|
|
||||||
|
/// 推荐信息响应 (来自 referral-service)
|
||||||
|
class ReferralInfoResponse {
|
||||||
|
final String userId;
|
||||||
|
final String referralCode;
|
||||||
|
final String? referrerId;
|
||||||
|
final int referralChainDepth;
|
||||||
|
final int directReferralCount;
|
||||||
|
final int totalTeamCount;
|
||||||
|
final int personalPlantingCount;
|
||||||
|
final int teamPlantingCount;
|
||||||
|
final double leaderboardScore;
|
||||||
|
final int? leaderboardRank;
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
ReferralInfoResponse({
|
||||||
|
required this.userId,
|
||||||
|
required this.referralCode,
|
||||||
|
this.referrerId,
|
||||||
|
required this.referralChainDepth,
|
||||||
|
required this.directReferralCount,
|
||||||
|
required this.totalTeamCount,
|
||||||
|
required this.personalPlantingCount,
|
||||||
|
required this.teamPlantingCount,
|
||||||
|
required this.leaderboardScore,
|
||||||
|
this.leaderboardRank,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ReferralInfoResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return ReferralInfoResponse(
|
||||||
|
userId: json['userId']?.toString() ?? '',
|
||||||
|
referralCode: json['referralCode'] ?? '',
|
||||||
|
referrerId: json['referrerId']?.toString(),
|
||||||
|
referralChainDepth: json['referralChainDepth'] ?? 0,
|
||||||
|
directReferralCount: json['directReferralCount'] ?? 0,
|
||||||
|
totalTeamCount: json['totalTeamCount'] ?? 0,
|
||||||
|
personalPlantingCount: json['personalPlantingCount'] ?? 0,
|
||||||
|
teamPlantingCount: json['teamPlantingCount'] ?? 0,
|
||||||
|
leaderboardScore: (json['leaderboardScore'] ?? 0).toDouble(),
|
||||||
|
leaderboardRank: json['leaderboardRank'],
|
||||||
|
createdAt: json['createdAt'] != null
|
||||||
|
? DateTime.parse(json['createdAt'])
|
||||||
|
: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 直推成员信息
|
||||||
|
class DirectReferralInfo {
|
||||||
|
final String userId;
|
||||||
|
final String referralCode;
|
||||||
|
final int teamCount;
|
||||||
|
final DateTime joinedAt;
|
||||||
|
|
||||||
|
DirectReferralInfo({
|
||||||
|
required this.userId,
|
||||||
|
required this.referralCode,
|
||||||
|
required this.teamCount,
|
||||||
|
required this.joinedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory DirectReferralInfo.fromJson(Map<String, dynamic> json) {
|
||||||
|
return DirectReferralInfo(
|
||||||
|
userId: json['userId']?.toString() ?? '',
|
||||||
|
referralCode: json['referralCode'] ?? '',
|
||||||
|
teamCount: json['teamCount'] ?? 0,
|
||||||
|
joinedAt: json['joinedAt'] != null
|
||||||
|
? DateTime.parse(json['joinedAt'])
|
||||||
|
: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 直推列表响应
|
||||||
|
class DirectReferralsResponse {
|
||||||
|
final List<DirectReferralInfo> referrals;
|
||||||
|
final int total;
|
||||||
|
final bool hasMore;
|
||||||
|
|
||||||
|
DirectReferralsResponse({
|
||||||
|
required this.referrals,
|
||||||
|
required this.total,
|
||||||
|
required this.hasMore,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory DirectReferralsResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return DirectReferralsResponse(
|
||||||
|
referrals: (json['referrals'] as List? ?? [])
|
||||||
|
.map((e) => DirectReferralInfo.fromJson(e))
|
||||||
|
.toList(),
|
||||||
|
total: json['total'] ?? 0,
|
||||||
|
hasMore: json['hasMore'] ?? false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 推荐链接响应
|
/// 推荐链接响应
|
||||||
class ReferralLinkResponse {
|
class ReferralLinkResponse {
|
||||||
|
|
@ -7,6 +104,7 @@ class ReferralLinkResponse {
|
||||||
final String referralCode;
|
final String referralCode;
|
||||||
final String shortUrl;
|
final String shortUrl;
|
||||||
final String fullUrl;
|
final String fullUrl;
|
||||||
|
final String downloadUrl; // APK 下载链接
|
||||||
final String? channel;
|
final String? channel;
|
||||||
final String? campaignId;
|
final String? campaignId;
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
|
|
@ -16,6 +114,7 @@ class ReferralLinkResponse {
|
||||||
required this.referralCode,
|
required this.referralCode,
|
||||||
required this.shortUrl,
|
required this.shortUrl,
|
||||||
required this.fullUrl,
|
required this.fullUrl,
|
||||||
|
required this.downloadUrl,
|
||||||
this.channel,
|
this.channel,
|
||||||
this.campaignId,
|
this.campaignId,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
|
|
@ -27,6 +126,7 @@ class ReferralLinkResponse {
|
||||||
referralCode: json['referralCode'] ?? '',
|
referralCode: json['referralCode'] ?? '',
|
||||||
shortUrl: json['shortUrl'] ?? '',
|
shortUrl: json['shortUrl'] ?? '',
|
||||||
fullUrl: json['fullUrl'] ?? '',
|
fullUrl: json['fullUrl'] ?? '',
|
||||||
|
downloadUrl: json['downloadUrl'] ?? 'https://s3.szaiai.com/rwadurian/app-release.apk',
|
||||||
channel: json['channel'],
|
channel: json['channel'],
|
||||||
campaignId: json['campaignId'],
|
campaignId: json['campaignId'],
|
||||||
createdAt: json['createdAt'] != null
|
createdAt: json['createdAt'] != null
|
||||||
|
|
@ -104,6 +204,7 @@ class WalletAddress {
|
||||||
/// 处理推荐链接相关功能:
|
/// 处理推荐链接相关功能:
|
||||||
/// - 获取用户信息和默认推荐链接
|
/// - 获取用户信息和默认推荐链接
|
||||||
/// - 生成渠道专属推荐链接 (短链)
|
/// - 生成渠道专属推荐链接 (短链)
|
||||||
|
/// - 获取推荐关系信息
|
||||||
class ReferralService {
|
class ReferralService {
|
||||||
final ApiClient _apiClient;
|
final ApiClient _apiClient;
|
||||||
|
|
||||||
|
|
@ -111,11 +212,11 @@ class ReferralService {
|
||||||
|
|
||||||
/// 获取当前用户信息 (包含默认推荐链接)
|
/// 获取当前用户信息 (包含默认推荐链接)
|
||||||
///
|
///
|
||||||
/// 调用 GET /me
|
/// 调用 GET /me (identity-service)
|
||||||
Future<MeResponse> getMe() async {
|
Future<MeResponse> getMe() async {
|
||||||
try {
|
try {
|
||||||
debugPrint('获取用户信息...');
|
debugPrint('获取用户信息...');
|
||||||
final response = await _apiClient.get('/me');
|
final response = await _apiClient.get(ApiEndpoints.me);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final data = response.data as Map<String, dynamic>;
|
final data = response.data as Map<String, dynamic>;
|
||||||
|
|
@ -130,6 +231,57 @@ class ReferralService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取当前用户推荐信息
|
||||||
|
///
|
||||||
|
/// 调用 GET /referral/me (referral-service)
|
||||||
|
Future<ReferralInfoResponse> getMyReferralInfo() async {
|
||||||
|
try {
|
||||||
|
debugPrint('获取推荐信息...');
|
||||||
|
final response = await _apiClient.get(ApiEndpoints.referralMe);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = response.data as Map<String, dynamic>;
|
||||||
|
debugPrint('推荐信息获取成功: directReferralCount=${data['directReferralCount']}');
|
||||||
|
return ReferralInfoResponse.fromJson(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception('获取推荐信息失败');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('获取推荐信息失败: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取直推列表
|
||||||
|
///
|
||||||
|
/// 调用 GET /referral/me/direct-referrals (referral-service)
|
||||||
|
Future<DirectReferralsResponse> getDirectReferrals({
|
||||||
|
int limit = 50,
|
||||||
|
int offset = 0,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
debugPrint('获取直推列表...');
|
||||||
|
final response = await _apiClient.get(
|
||||||
|
ApiEndpoints.referralDirects,
|
||||||
|
queryParameters: {
|
||||||
|
'limit': limit,
|
||||||
|
'offset': offset,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = response.data as Map<String, dynamic>;
|
||||||
|
debugPrint('直推列表获取成功: total=${data['total']}');
|
||||||
|
return DirectReferralsResponse.fromJson(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception('获取直推列表失败');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('获取直推列表失败: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 生成推荐链接 (短链)
|
/// 生成推荐链接 (短链)
|
||||||
///
|
///
|
||||||
/// 调用 POST /referrals/links
|
/// 调用 POST /referrals/links
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
@ -7,9 +8,9 @@ import 'package:go_router/go_router.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import '../../../../core/di/injection_container.dart';
|
import '../../../../core/di/injection_container.dart';
|
||||||
|
import '../../../../core/services/referral_service.dart';
|
||||||
import '../../../../routes/route_paths.dart';
|
import '../../../../routes/route_paths.dart';
|
||||||
import '../../../../routes/app_router.dart';
|
import '../../../../routes/app_router.dart';
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
/// 个人中心页面 - 显示用户信息、社区数据、收益和设置
|
/// 个人中心页面 - 显示用户信息、社区数据、收益和设置
|
||||||
/// 包含用户资料、推荐信息、社区考核、收益领取等功能
|
/// 包含用户资料、推荐信息、社区考核、收益领取等功能
|
||||||
|
|
@ -29,23 +30,25 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
String? _localAvatarPath; // 本地头像文件路径
|
String? _localAvatarPath; // 本地头像文件路径
|
||||||
String _referrerSerial = '--'; // 推荐人序列号(从API获取)
|
String _referrerSerial = '--'; // 推荐人序列号(从API获取)
|
||||||
String _referralCode = '--'; // 我的推荐码
|
String _referralCode = '--'; // 我的推荐码
|
||||||
final String _community = '星空社区';
|
|
||||||
final String _parentCommunity = '银河社区';
|
|
||||||
final String _childCommunity = '星辰社区';
|
|
||||||
final String _cityCompany = '深圳公司';
|
|
||||||
final String _provinceCompany = '广东公司';
|
|
||||||
final String _province = '广东';
|
|
||||||
|
|
||||||
// 团队数据
|
// 授权数据(从 authorization-service 获取)
|
||||||
final int _teamUsers = 1204;
|
String _community = '--';
|
||||||
final int _teamPlanting = 8930;
|
String _parentCommunity = '--';
|
||||||
|
String _childCommunity = '--';
|
||||||
|
String _cityCompany = '--';
|
||||||
|
String _provinceCompany = '--';
|
||||||
|
String _province = '--';
|
||||||
|
|
||||||
// 直推数据
|
// 团队数据(从 referral-service 获取)
|
||||||
final List<Map<String, dynamic>> _referrals = [
|
int _directReferralCount = 0;
|
||||||
{'serial': '87654321', 'personal': 15, 'team': 120},
|
int _totalTeamCount = 0;
|
||||||
{'serial': '87654322', 'personal': 10, 'team': 85},
|
int _personalPlantingCount = 0;
|
||||||
{'serial': '87654323', 'personal': 25, 'team': 250},
|
int _teamPlantingCount = 0;
|
||||||
];
|
double _leaderboardScore = 0;
|
||||||
|
int? _leaderboardRank;
|
||||||
|
|
||||||
|
// 直推数据(从 referral-service 获取)
|
||||||
|
List<Map<String, dynamic>> _referrals = [];
|
||||||
|
|
||||||
// 社区考核数据
|
// 社区考核数据
|
||||||
final int _communityLevel = 3;
|
final int _communityLevel = 3;
|
||||||
|
|
@ -80,6 +83,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
_checkLocalAvatarSync();
|
_checkLocalAvatarSync();
|
||||||
_loadUserData();
|
_loadUserData();
|
||||||
_loadAppInfo();
|
_loadAppInfo();
|
||||||
|
// 加载推荐和授权数据
|
||||||
|
_loadReferralData();
|
||||||
|
_loadAuthorizationData();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 加载应用信息
|
/// 加载应用信息
|
||||||
|
|
@ -200,6 +206,64 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 加载推荐数据 (from referral-service)
|
||||||
|
Future<void> _loadReferralData() async {
|
||||||
|
try {
|
||||||
|
final referralService = ref.read(referralServiceProvider);
|
||||||
|
|
||||||
|
// 并行加载推荐信息和直推列表
|
||||||
|
final results = await Future.wait([
|
||||||
|
referralService.getMyReferralInfo(),
|
||||||
|
referralService.getDirectReferrals(limit: 10),
|
||||||
|
]);
|
||||||
|
|
||||||
|
final referralInfo = results[0] as ReferralInfoResponse;
|
||||||
|
final directReferrals = results[1] as DirectReferralsResponse;
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_directReferralCount = referralInfo.directReferralCount;
|
||||||
|
_totalTeamCount = referralInfo.totalTeamCount;
|
||||||
|
_personalPlantingCount = referralInfo.personalPlantingCount;
|
||||||
|
_teamPlantingCount = referralInfo.teamPlantingCount;
|
||||||
|
_leaderboardScore = referralInfo.leaderboardScore;
|
||||||
|
_leaderboardRank = referralInfo.leaderboardRank;
|
||||||
|
|
||||||
|
// 转换直推列表格式
|
||||||
|
_referrals = directReferrals.referrals.map((r) => {
|
||||||
|
'serial': r.userId,
|
||||||
|
'personal': 0, // API暂未返回个人认种量
|
||||||
|
'team': r.teamCount,
|
||||||
|
}).toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[ProfilePage] 加载推荐数据失败: $e');
|
||||||
|
// 失败时保持默认数据
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载授权数据 (from authorization-service)
|
||||||
|
Future<void> _loadAuthorizationData() async {
|
||||||
|
try {
|
||||||
|
final authorizationService = ref.read(authorizationServiceProvider);
|
||||||
|
final summary = await authorizationService.getMyAuthorizationSummary();
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_community = summary.communityName ?? '--';
|
||||||
|
_cityCompany = summary.cityCompanyName ?? '--';
|
||||||
|
_provinceCompany = summary.provinceCompanyName ?? '--';
|
||||||
|
// 上级社区和下级社区暂时无法从当前API获取
|
||||||
|
// 后续需要扩展API或从推荐链中计算
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[ProfilePage] 加载授权数据失败: $e');
|
||||||
|
// 失败时保持默认数据
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 后台下载并缓存头像
|
/// 后台下载并缓存头像
|
||||||
Future<void> _downloadAndCacheAvatar(String url) async {
|
Future<void> _downloadAndCacheAvatar(String url) async {
|
||||||
final accountService = ref.read(accountServiceProvider);
|
final accountService = ref.read(accountServiceProvider);
|
||||||
|
|
@ -1100,7 +1164,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'团队注册用户',
|
'直推人数',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
|
|
@ -1112,7 +1176,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
_formatInt(_teamUsers),
|
_formatInt(_directReferralCount),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
|
|
@ -1153,7 +1217,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
_formatInt(_teamPlanting),
|
_formatInt(_totalTeamCount),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue