From d0487c4a7e2daa205ed9aeedc67d091f0007f27b Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 9 Dec 2025 01:57:39 -0800 Subject: [PATCH] feat(profile): integrate referral and authorization APIs for profile page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- backend/api-gateway/kong.yml | 39 +- .../services/authorization-service/.gitignore | 3 +- .../authorization-service/package-lock.json | 8 + .../authorization-service/package.json | 1 + .../00000000000000_init/migration.sql | 371 ++++++++++++++++++ .../src/shared/strategies/jwt.strategy.ts | 14 +- .../lib/core/constants/api_endpoints.dart | 6 + .../lib/core/di/injection_container.dart | 7 + .../core/services/authorization_service.dart | 245 ++++++++++++ .../lib/core/services/referral_service.dart | 156 +++++++- .../presentation/pages/profile_page.dart | 102 ++++- 11 files changed, 924 insertions(+), 28 deletions(-) create mode 100644 backend/services/authorization-service/prisma/migrations/00000000000000_init/migration.sql create mode 100644 frontend/mobile-app/lib/core/services/authorization_service.dart diff --git a/backend/api-gateway/kong.yml b/backend/api-gateway/kong.yml index 047d0a40..78967b9e 100644 --- a/backend/api-gateway/kong.yml +++ b/backend/api-gateway/kong.yml @@ -32,6 +32,10 @@ services: paths: - /api/v1/auth strip_path: false + - name: identity-me + paths: + - /api/v1/me + strip_path: false - name: identity-user paths: - /api/v1/user @@ -90,9 +94,21 @@ services: url: http://192.168.1.111:3004 routes: - name: referral-api + paths: + - /api/v1/referral + strip_path: false + - name: referral-referrals paths: - /api/v1/referrals 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 - 奖励服务 @@ -151,9 +167,11 @@ services: routes: - name: authorization-api paths: - - /api/v1/authorization - - /api/v1/permissions - - /api/v1/roles + - /api/v1/authorizations + strip_path: false + - name: authorization-admin + paths: + - /api/v1/admin/authorizations strip_path: false # --------------------------------------------------------------------------- @@ -182,6 +200,21 @@ services: - /api/v1/presence 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 - 全局插件配置 # ============================================================================= diff --git a/backend/services/authorization-service/.gitignore b/backend/services/authorization-service/.gitignore index 07f1bf1b..10f8fb06 100644 --- a/backend/services/authorization-service/.gitignore +++ b/backend/services/authorization-service/.gitignore @@ -43,5 +43,4 @@ lerna-debug.log* !.env.example # Prisma -prisma/migrations/* -!prisma/migrations/.gitkeep +prisma/migrations/**/migration_lock.toml diff --git a/backend/services/authorization-service/package-lock.json b/backend/services/authorization-service/package-lock.json index d3f3a5c2..b9931e09 100644 --- a/backend/services/authorization-service/package-lock.json +++ b/backend/services/authorization-service/package-lock.json @@ -21,6 +21,7 @@ "@prisma/client": "^5.7.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "decimal.js": "^10.4.3", "ioredis": "^5.3.2", "kafkajs": "^2.2.4", "passport-jwt": "^4.0.1", @@ -246,6 +247,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@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": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", diff --git a/backend/services/authorization-service/package.json b/backend/services/authorization-service/package.json index 761d8b1a..7d406f3c 100644 --- a/backend/services/authorization-service/package.json +++ b/backend/services/authorization-service/package.json @@ -43,6 +43,7 @@ "@prisma/client": "^5.7.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "decimal.js": "^10.4.3", "ioredis": "^5.3.2", "kafkajs": "^2.2.4", "passport-jwt": "^4.0.1", diff --git a/backend/services/authorization-service/prisma/migrations/00000000000000_init/migration.sql b/backend/services/authorization-service/prisma/migrations/00000000000000_init/migration.sql new file mode 100644 index 00000000..9f6a44ea --- /dev/null +++ b/backend/services/authorization-service/prisma/migrations/00000000000000_init/migration.sql @@ -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; + diff --git a/backend/services/authorization-service/src/shared/strategies/jwt.strategy.ts b/backend/services/authorization-service/src/shared/strategies/jwt.strategy.ts index b3274004..6dfc09c7 100644 --- a/backend/services/authorization-service/src/shared/strategies/jwt.strategy.ts +++ b/backend/services/authorization-service/src/shared/strategies/jwt.strategy.ts @@ -4,7 +4,13 @@ import { ExtractJwt, Strategy } from 'passport-jwt' import { ConfigService } from '@nestjs/config' 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 roles?: string[] iat?: number @@ -22,8 +28,12 @@ export class JwtStrategy extends PassportStrategy(Strategy) { } async validate(payload: JwtPayload) { + // Support both 'userId' (from identity-service) and 'sub' (legacy) + const userId = payload.userId || payload.sub return { - userId: payload.sub, + userId, + accountSequence: payload.accountSequence, + deviceId: payload.deviceId, walletAddress: payload.walletAddress, roles: payload.roles, } diff --git a/frontend/mobile-app/lib/core/constants/api_endpoints.dart b/frontend/mobile-app/lib/core/constants/api_endpoints.dart index d8c13e60..b05eee17 100644 --- a/frontend/mobile-app/lib/core/constants/api_endpoints.dart +++ b/frontend/mobile-app/lib/core/constants/api_endpoints.dart @@ -67,11 +67,17 @@ class ApiEndpoints { // Community & Referral (-> Referral Service) static const String community = '$apiPrefix/community'; 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 earnings = '$community/earnings'; static const String claimEarnings = '$community/claim'; 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) static const String telemetry = '$apiPrefix/telemetry'; static const String telemetrySession = '$telemetry/session'; diff --git a/frontend/mobile-app/lib/core/di/injection_container.dart b/frontend/mobile-app/lib/core/di/injection_container.dart index e639c0ed..24363e12 100644 --- a/frontend/mobile-app/lib/core/di/injection_container.dart +++ b/frontend/mobile-app/lib/core/di/injection_container.dart @@ -4,6 +4,7 @@ import '../storage/local_storage.dart'; import '../network/api_client.dart'; import '../services/account_service.dart'; import '../services/referral_service.dart'; +import '../services/authorization_service.dart'; import '../services/deposit_service.dart'; // Storage Providers @@ -37,6 +38,12 @@ final referralServiceProvider = Provider((ref) { return ReferralService(apiClient: apiClient); }); +// Authorization Service Provider +final authorizationServiceProvider = Provider((ref) { + final apiClient = ref.watch(apiClientProvider); + return AuthorizationService(apiClient: apiClient); +}); + // Deposit Service Provider final depositServiceProvider = Provider((ref) { final apiClient = ref.watch(apiClientProvider); diff --git a/frontend/mobile-app/lib/core/services/authorization_service.dart b/frontend/mobile-app/lib/core/services/authorization_service.dart new file mode 100644 index 00000000..730c9258 --- /dev/null +++ b/frontend/mobile-app/lib/core/services/authorization_service.dart @@ -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 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 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 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> 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)) + .toList(); + debugPrint('授权列表获取成功: ${authorizations.length} 个授权'); + return authorizations; + } + return []; + } + + throw Exception('获取授权列表失败'); + } catch (e) { + debugPrint('获取授权列表失败: $e'); + rethrow; + } + } + + /// 获取用户授权汇总信息 + /// + /// 方便前端快速访问社区/省公司/市公司信息 + Future getMyAuthorizationSummary() async { + final authorizations = await getMyAuthorizations(); + return UserAuthorizationSummary.fromList(authorizations); + } +} diff --git a/frontend/mobile-app/lib/core/services/referral_service.dart b/frontend/mobile-app/lib/core/services/referral_service.dart index 01c152d1..43ef6f5d 100644 --- a/frontend/mobile-app/lib/core/services/referral_service.dart +++ b/frontend/mobile-app/lib/core/services/referral_service.dart @@ -1,5 +1,102 @@ import 'package:flutter/foundation.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 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 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 referrals; + final int total; + final bool hasMore; + + DirectReferralsResponse({ + required this.referrals, + required this.total, + required this.hasMore, + }); + + factory DirectReferralsResponse.fromJson(Map json) { + return DirectReferralsResponse( + referrals: (json['referrals'] as List? ?? []) + .map((e) => DirectReferralInfo.fromJson(e)) + .toList(), + total: json['total'] ?? 0, + hasMore: json['hasMore'] ?? false, + ); + } +} /// 推荐链接响应 class ReferralLinkResponse { @@ -7,6 +104,7 @@ class ReferralLinkResponse { final String referralCode; final String shortUrl; final String fullUrl; + final String downloadUrl; // APK 下载链接 final String? channel; final String? campaignId; final DateTime createdAt; @@ -16,6 +114,7 @@ class ReferralLinkResponse { required this.referralCode, required this.shortUrl, required this.fullUrl, + required this.downloadUrl, this.channel, this.campaignId, required this.createdAt, @@ -27,6 +126,7 @@ class ReferralLinkResponse { referralCode: json['referralCode'] ?? '', shortUrl: json['shortUrl'] ?? '', fullUrl: json['fullUrl'] ?? '', + downloadUrl: json['downloadUrl'] ?? 'https://s3.szaiai.com/rwadurian/app-release.apk', channel: json['channel'], campaignId: json['campaignId'], createdAt: json['createdAt'] != null @@ -104,6 +204,7 @@ class WalletAddress { /// 处理推荐链接相关功能: /// - 获取用户信息和默认推荐链接 /// - 生成渠道专属推荐链接 (短链) +/// - 获取推荐关系信息 class ReferralService { final ApiClient _apiClient; @@ -111,11 +212,11 @@ class ReferralService { /// 获取当前用户信息 (包含默认推荐链接) /// - /// 调用 GET /me + /// 调用 GET /me (identity-service) Future getMe() async { try { debugPrint('获取用户信息...'); - final response = await _apiClient.get('/me'); + final response = await _apiClient.get(ApiEndpoints.me); if (response.statusCode == 200) { final data = response.data as Map; @@ -130,6 +231,57 @@ class ReferralService { } } + /// 获取当前用户推荐信息 + /// + /// 调用 GET /referral/me (referral-service) + Future getMyReferralInfo() async { + try { + debugPrint('获取推荐信息...'); + final response = await _apiClient.get(ApiEndpoints.referralMe); + + if (response.statusCode == 200) { + final data = response.data as Map; + debugPrint('推荐信息获取成功: directReferralCount=${data['directReferralCount']}'); + return ReferralInfoResponse.fromJson(data); + } + + throw Exception('获取推荐信息失败'); + } catch (e) { + debugPrint('获取推荐信息失败: $e'); + rethrow; + } + } + + /// 获取直推列表 + /// + /// 调用 GET /referral/me/direct-referrals (referral-service) + Future 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; + debugPrint('直推列表获取成功: total=${data['total']}'); + return DirectReferralsResponse.fromJson(data); + } + + throw Exception('获取直推列表失败'); + } catch (e) { + debugPrint('获取直推列表失败: $e'); + rethrow; + } + } + /// 生成推荐链接 (短链) /// /// 调用 POST /referrals/links diff --git a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart index cd7253cf..de29788b 100644 --- a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart +++ b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.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:device_info_plus/device_info_plus.dart'; import '../../../../core/di/injection_container.dart'; +import '../../../../core/services/referral_service.dart'; import '../../../../routes/route_paths.dart'; import '../../../../routes/app_router.dart'; -import 'dart:async'; /// 个人中心页面 - 显示用户信息、社区数据、收益和设置 /// 包含用户资料、推荐信息、社区考核、收益领取等功能 @@ -29,23 +30,25 @@ class _ProfilePageState extends ConsumerState { String? _localAvatarPath; // 本地头像文件路径 String _referrerSerial = '--'; // 推荐人序列号(从API获取) String _referralCode = '--'; // 我的推荐码 - final String _community = '星空社区'; - final String _parentCommunity = '银河社区'; - final String _childCommunity = '星辰社区'; - final String _cityCompany = '深圳公司'; - final String _provinceCompany = '广东公司'; - final String _province = '广东'; - // 团队数据 - final int _teamUsers = 1204; - final int _teamPlanting = 8930; + // 授权数据(从 authorization-service 获取) + String _community = '--'; + String _parentCommunity = '--'; + String _childCommunity = '--'; + String _cityCompany = '--'; + String _provinceCompany = '--'; + String _province = '--'; - // 直推数据 - final List> _referrals = [ - {'serial': '87654321', 'personal': 15, 'team': 120}, - {'serial': '87654322', 'personal': 10, 'team': 85}, - {'serial': '87654323', 'personal': 25, 'team': 250}, - ]; + // 团队数据(从 referral-service 获取) + int _directReferralCount = 0; + int _totalTeamCount = 0; + int _personalPlantingCount = 0; + int _teamPlantingCount = 0; + double _leaderboardScore = 0; + int? _leaderboardRank; + + // 直推数据(从 referral-service 获取) + List> _referrals = []; // 社区考核数据 final int _communityLevel = 3; @@ -80,6 +83,9 @@ class _ProfilePageState extends ConsumerState { _checkLocalAvatarSync(); _loadUserData(); _loadAppInfo(); + // 加载推荐和授权数据 + _loadReferralData(); + _loadAuthorizationData(); } /// 加载应用信息 @@ -200,6 +206,64 @@ class _ProfilePageState extends ConsumerState { } } + /// 加载推荐数据 (from referral-service) + Future _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 _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 _downloadAndCacheAvatar(String url) async { final accountService = ref.read(accountServiceProvider); @@ -1100,7 +1164,7 @@ class _ProfilePageState extends ConsumerState { child: Column( children: [ const Text( - '团队注册用户', + '直推人数', style: TextStyle( fontSize: 14, fontFamily: 'Inter', @@ -1112,7 +1176,7 @@ class _ProfilePageState extends ConsumerState { ), const SizedBox(height: 8), Text( - _formatInt(_teamUsers), + _formatInt(_directReferralCount), style: const TextStyle( fontSize: 24, fontFamily: 'Inter', @@ -1153,7 +1217,7 @@ class _ProfilePageState extends ConsumerState { ), const SizedBox(height: 8), Text( - _formatInt(_teamPlanting), + _formatInt(_totalTeamCount), style: const TextStyle( fontSize: 24, fontFamily: 'Inter',