diff --git a/backend/services/identity-service/src/api/dto/index.ts b/backend/services/identity-service/src/api/dto/index.ts index e04aa25e..2e521c34 100644 --- a/backend/services/identity-service/src/api/dto/index.ts +++ b/backend/services/identity-service/src/api/dto/index.ts @@ -234,6 +234,9 @@ export class MeResponseDto { @ApiProperty({ description: '完整推荐链接' }) referralLink: string; + @ApiProperty({ description: '推荐人序列号', nullable: true }) + inviterSequence: number | null; + @ApiProperty({ description: '钱包地址列表' }) walletAddresses: Array<{ chainType: string; address: string }>; diff --git a/backend/services/identity-service/src/application/commands/index.ts b/backend/services/identity-service/src/application/commands/index.ts index 7a26e44a..dcaa8697 100644 --- a/backend/services/identity-service/src/application/commands/index.ts +++ b/backend/services/identity-service/src/application/commands/index.ts @@ -289,6 +289,7 @@ export interface MeResult { avatarUrl: string | null; referralCode: string; referralLink: string; // 完整推荐链接 + inviterSequence: number | null; // 推荐人序列号 walletAddresses: Array<{ chainType: string; address: string }>; kycStatus: string; status: string; diff --git a/backend/services/identity-service/src/application/services/user-application.service.ts b/backend/services/identity-service/src/application/services/user-application.service.ts index b605f5f0..14f46846 100644 --- a/backend/services/identity-service/src/application/services/user-application.service.ts +++ b/backend/services/identity-service/src/application/services/user-application.service.ts @@ -506,6 +506,7 @@ export class UserApplicationService { avatarUrl: account.avatarUrl, referralCode: account.referralCode.value, referralLink, + inviterSequence: account.inviterSequence?.value || null, walletAddresses: account.getAllWalletAddresses().map((wa) => ({ chainType: wa.chainType, address: wa.address, diff --git a/backend/services/referral-service/prisma/migrations/00000000000000_init/migration.sql b/backend/services/referral-service/prisma/migrations/00000000000000_init/migration.sql new file mode 100644 index 00000000..88e2002d --- /dev/null +++ b/backend/services/referral-service/prisma/migrations/00000000000000_init/migration.sql @@ -0,0 +1,139 @@ +-- CreateTable: referral_relationships +CREATE TABLE "referral_relationships" ( + "relationship_id" BIGSERIAL NOT NULL, + "user_id" BIGINT NOT NULL, + "account_sequence" INTEGER NOT NULL, + "referrer_id" BIGINT, + "root_user_id" BIGINT, + "my_referral_code" VARCHAR(20) NOT NULL, + "used_referral_code" VARCHAR(20), + "ancestor_path" BIGINT[] NOT NULL DEFAULT '{}', + "depth" INTEGER NOT NULL DEFAULT 0, + "direct_referral_count" INTEGER NOT NULL DEFAULT 0, + "active_direct_count" INTEGER NOT NULL DEFAULT 0, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "referral_relationships_pkey" PRIMARY KEY ("relationship_id") +); + +-- CreateTable: team_statistics +CREATE TABLE "team_statistics" ( + "statistics_id" BIGSERIAL NOT NULL, + "user_id" BIGINT NOT NULL, + "direct_referral_count" INTEGER NOT NULL DEFAULT 0, + "total_team_count" INTEGER NOT NULL DEFAULT 0, + "self_planting_count" INTEGER NOT NULL DEFAULT 0, + "self_planting_amount" DECIMAL(20,8) NOT NULL DEFAULT 0, + "direct_planting_count" INTEGER NOT NULL DEFAULT 0, + "total_team_planting_count" INTEGER NOT NULL DEFAULT 0, + "total_team_planting_amount" DECIMAL(20,8) NOT NULL DEFAULT 0, + "direct_team_planting_data" JSONB NOT NULL DEFAULT '[]', + "max_single_team_planting_count" INTEGER NOT NULL DEFAULT 0, + "effective_planting_count_for_ranking" INTEGER NOT NULL DEFAULT 0, + "own_province_team_count" INTEGER NOT NULL DEFAULT 0, + "own_city_team_count" INTEGER NOT NULL DEFAULT 0, + "province_team_percentage" DECIMAL(5,2) NOT NULL DEFAULT 0, + "city_team_percentage" DECIMAL(5,2) NOT NULL DEFAULT 0, + "province_city_distribution" JSONB NOT NULL DEFAULT '{}', + "last_calc_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "team_statistics_pkey" PRIMARY KEY ("statistics_id") +); + +-- CreateTable: direct_referrals +CREATE TABLE "direct_referrals" ( + "direct_referral_id" BIGSERIAL NOT NULL, + "referrer_id" BIGINT NOT NULL, + "referral_id" BIGINT NOT NULL, + "referral_sequence" BIGINT NOT NULL, + "referral_nickname" VARCHAR(100), + "referral_avatar" VARCHAR(255), + "personal_planting_count" INTEGER NOT NULL DEFAULT 0, + "team_planting_count" INTEGER NOT NULL DEFAULT 0, + "has_planted" BOOLEAN NOT NULL DEFAULT false, + "first_planted_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "direct_referrals_pkey" PRIMARY KEY ("direct_referral_id") +); + +-- CreateTable: team_province_city_details +CREATE TABLE "team_province_city_details" ( + "detail_id" BIGSERIAL NOT NULL, + "user_id" BIGINT NOT NULL, + "province_code" VARCHAR(10) NOT NULL, + "city_code" VARCHAR(10) NOT NULL, + "team_planting_count" INTEGER NOT NULL DEFAULT 0, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "team_province_city_details_pkey" PRIMARY KEY ("detail_id") +); + +-- CreateTable: referral_events +CREATE TABLE "referral_events" ( + "event_id" BIGSERIAL NOT NULL, + "event_type" VARCHAR(50) NOT NULL, + "aggregate_id" VARCHAR(100) NOT NULL, + "aggregate_type" VARCHAR(50) NOT NULL, + "event_data" JSONB NOT NULL, + "user_id" BIGINT, + "occurred_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "version" INTEGER NOT NULL DEFAULT 1, + + CONSTRAINT "referral_events_pkey" PRIMARY KEY ("event_id") +); + +-- CreateIndex: referral_relationships unique constraints +CREATE UNIQUE INDEX "referral_relationships_user_id_key" ON "referral_relationships"("user_id"); +CREATE UNIQUE INDEX "referral_relationships_account_sequence_key" ON "referral_relationships"("account_sequence"); +CREATE UNIQUE INDEX "referral_relationships_my_referral_code_key" ON "referral_relationships"("my_referral_code"); + +-- CreateIndex: referral_relationships indexes +CREATE INDEX "idx_referrer" ON "referral_relationships"("referrer_id"); +CREATE INDEX "idx_account_sequence" ON "referral_relationships"("account_sequence"); +CREATE INDEX "idx_my_referral_code" ON "referral_relationships"("my_referral_code"); +CREATE INDEX "idx_used_referral_code" ON "referral_relationships"("used_referral_code"); +CREATE INDEX "idx_root_user" ON "referral_relationships"("root_user_id"); +CREATE INDEX "idx_depth" ON "referral_relationships"("depth"); +CREATE INDEX "idx_referral_created" ON "referral_relationships"("created_at"); + +-- CreateIndex: team_statistics unique constraints +CREATE UNIQUE INDEX "team_statistics_user_id_key" ON "team_statistics"("user_id"); + +-- CreateIndex: team_statistics indexes +CREATE INDEX "idx_leaderboard_score" ON "team_statistics"("effective_planting_count_for_ranking" DESC); +CREATE INDEX "idx_team_planting" ON "team_statistics"("total_team_planting_count" DESC); +CREATE INDEX "idx_self_planting" ON "team_statistics"("self_planting_count"); + +-- CreateIndex: direct_referrals unique constraints +CREATE UNIQUE INDEX "uk_referrer_referral" ON "direct_referrals"("referrer_id", "referral_id"); + +-- CreateIndex: direct_referrals indexes +CREATE INDEX "idx_direct_referrer" ON "direct_referrals"("referrer_id"); +CREATE INDEX "idx_direct_referral" ON "direct_referrals"("referral_id"); +CREATE INDEX "idx_has_planted" ON "direct_referrals"("has_planted"); +CREATE INDEX "idx_direct_team_planting" ON "direct_referrals"("team_planting_count" DESC); + +-- CreateIndex: team_province_city_details unique constraints +CREATE UNIQUE INDEX "uk_user_province_city" ON "team_province_city_details"("user_id", "province_code", "city_code"); + +-- CreateIndex: team_province_city_details indexes +CREATE INDEX "idx_detail_user" ON "team_province_city_details"("user_id"); +CREATE INDEX "idx_detail_province" ON "team_province_city_details"("province_code"); +CREATE INDEX "idx_detail_city" ON "team_province_city_details"("city_code"); + +-- CreateIndex: referral_events indexes +CREATE INDEX "idx_event_aggregate" ON "referral_events"("aggregate_type", "aggregate_id"); +CREATE INDEX "idx_event_type" ON "referral_events"("event_type"); +CREATE INDEX "idx_event_user" ON "referral_events"("user_id"); +CREATE INDEX "idx_event_occurred" ON "referral_events"("occurred_at"); + +-- AddForeignKey: referral_relationships self-reference +ALTER TABLE "referral_relationships" ADD CONSTRAINT "referral_relationships_referrer_id_fkey" FOREIGN KEY ("referrer_id") REFERENCES "referral_relationships"("user_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey: team_statistics to referral_relationships +ALTER TABLE "team_statistics" ADD CONSTRAINT "team_statistics_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "referral_relationships"("user_id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend/services/referral-service/prisma/schema.prisma b/backend/services/referral-service/prisma/schema.prisma index fc809c27..1ac3e7c9 100644 --- a/backend/services/referral-service/prisma/schema.prisma +++ b/backend/services/referral-service/prisma/schema.prisma @@ -14,6 +14,7 @@ datasource db { model ReferralRelationship { id BigInt @id @default(autoincrement()) @map("relationship_id") userId BigInt @unique @map("user_id") + accountSequence Int @unique @map("account_sequence") // 8位账户序列号,用于跨服务关联 // 推荐人信息 referrerId BigInt? @map("referrer_id") // 直接推荐人 (null = 无推荐人/根节点) @@ -43,6 +44,7 @@ model ReferralRelationship { @@map("referral_relationships") @@index([referrerId], name: "idx_referrer") + @@index([accountSequence], name: "idx_account_sequence") @@index([myReferralCode], name: "idx_my_referral_code") @@index([usedReferralCode], name: "idx_used_referral_code") @@index([rootUserId], name: "idx_root_user") diff --git a/backend/services/referral-service/src/api/controllers/referral.controller.ts b/backend/services/referral-service/src/api/controllers/referral.controller.ts index 30e796cb..5e585f38 100644 --- a/backend/services/referral-service/src/api/controllers/referral.controller.ts +++ b/backend/services/referral-service/src/api/controllers/referral.controller.ts @@ -90,7 +90,9 @@ export class ReferralController { ): Promise<{ referralCode: string }> { const command = new CreateReferralRelationshipCommand( BigInt(dto.userId), + dto.accountSequence, dto.referrerCode ?? null, + dto.inviterAccountSequence ?? null, ); return this.referralService.createReferralRelationship(command); } @@ -106,3 +108,38 @@ export class ReferralController { return this.referralService.getUserReferralInfo(query); } } + +/** + * 内部API控制器 - 供其他微服务调用 + * 不需要JWT认证 + */ +@ApiTags('Internal Referral API') +@Controller('referrals') +export class InternalReferralController { + constructor(private readonly referralService: ReferralService) {} + + @Get(':userId/context') + @ApiOperation({ summary: '获取用户推荐上下文信息(内部API)' }) + @ApiParam({ name: 'userId', description: '用户ID' }) + @ApiResponse({ status: 200, description: '推荐上下文' }) + async getReferralContext( + @Param('userId') userId: string, + @Query('provinceCode') provinceCode: string, + @Query('cityCode') cityCode: string, + ) { + // 获取用户的推荐链 + const query = new GetUserReferralInfoQuery(BigInt(userId)); + const referralInfo = await this.referralService.getUserReferralInfo(query); + + // 返回推荐上下文信息 + // 目前返回基础信息,后续可以扩展省市授权等信息 + return { + userId, + referralChain: referralInfo.referrerId ? [referralInfo.referrerId] : [], + referrerId: referralInfo.referrerId, + nearestProvinceAuth: null, // 省代账户ID - 需要后续实现 + nearestCityAuth: null, // 市代账户ID - 需要后续实现 + nearestCommunity: null, // 社区账户ID - 需要后续实现 + }; + } +} diff --git a/backend/services/referral-service/src/api/dto/referral.dto.ts b/backend/services/referral-service/src/api/dto/referral.dto.ts index 831d4bec..78ce5614 100644 --- a/backend/services/referral-service/src/api/dto/referral.dto.ts +++ b/backend/services/referral-service/src/api/dto/referral.dto.ts @@ -14,11 +14,20 @@ export class CreateReferralDto { @IsString() userId: string; + @ApiProperty({ description: '账户序列号 (8位)', example: 10000001 }) + @IsInt() + accountSequence: number; + @ApiPropertyOptional({ description: '推荐码', example: 'RWA123ABC' }) @IsOptional() @IsString() @Length(6, 20) referrerCode?: string; + + @ApiPropertyOptional({ description: '邀请人账户序列号', example: 10000001 }) + @IsOptional() + @IsInt() + inviterAccountSequence?: number; } export class GetDirectReferralsDto { diff --git a/backend/services/referral-service/src/application/commands/create-referral-relationship.command.ts b/backend/services/referral-service/src/application/commands/create-referral-relationship.command.ts index edd96dd2..8eb76dcf 100644 --- a/backend/services/referral-service/src/application/commands/create-referral-relationship.command.ts +++ b/backend/services/referral-service/src/application/commands/create-referral-relationship.command.ts @@ -1,6 +1,8 @@ export class CreateReferralRelationshipCommand { constructor( public readonly userId: bigint, - public readonly referrerCode: string | null, + public readonly accountSequence: number, + public readonly referrerCode: string | null = null, + public readonly inviterAccountSequence: number | null = null, ) {} } diff --git a/backend/services/referral-service/src/application/event-handlers/user-registered.handler.ts b/backend/services/referral-service/src/application/event-handlers/user-registered.handler.ts index 0fdb26b3..8a63ef03 100644 --- a/backend/services/referral-service/src/application/event-handlers/user-registered.handler.ts +++ b/backend/services/referral-service/src/application/event-handlers/user-registered.handler.ts @@ -3,17 +3,32 @@ import { KafkaService } from '../../infrastructure'; import { ReferralService } from '../services'; import { CreateReferralRelationshipCommand } from '../commands'; -interface UserRegisteredEvent { - eventName: string; - data: { - userId: string; - referralCode?: string; - }; +/** + * identity-service 发布的账户创建事件结构 + */ +interface UserAccountCreatedPayload { + userId: string; + accountSequence: number; + inviterSequence: number | null; + registeredAt: string; + // UserAccountCreated 有 phoneNumber, UserAccountAutoCreated 没有 + phoneNumber?: string; + initialDeviceId?: string; +} + +interface IdentityEvent { + eventId: string; + eventType: string; + occurredAt: string; + payload: UserAccountCreatedPayload; } /** * 用户注册事件处理器 - * 监听 identity-service 发出的用户注册事件 + * 监听 identity-service 发出的用户创建事件 + * 支持两种创建方式: + * - identity.UserAccountAutoCreated: 免密快捷创建 + * - identity.UserAccountCreated: 手机号密码创建 */ @Injectable() export class UserRegisteredHandler implements OnModuleInit { @@ -26,32 +41,45 @@ export class UserRegisteredHandler implements OnModuleInit { async onModuleInit() { await this.kafkaService.subscribe( - 'referral-service-user-registered', - ['identity.user.registered'], + 'referral-service-user-account-created', + ['identity.UserAccountAutoCreated', 'identity.UserAccountCreated'], this.handleMessage.bind(this), ); - this.logger.log('Subscribed to user.registered events'); + this.logger.log('Subscribed to identity.UserAccountAutoCreated and identity.UserAccountCreated events'); } private async handleMessage(topic: string, message: Record): Promise { - const event = message as unknown as UserRegisteredEvent; + const event = message as unknown as IdentityEvent; - if (event.eventName !== 'user.registered') { + // 验证事件类型 + if (event.eventType !== 'UserAccountAutoCreated' && event.eventType !== 'UserAccountCreated') { + this.logger.debug(`Ignoring event type: ${event.eventType}`); return; } + const payload = event.payload; + try { + this.logger.log( + `Processing ${event.eventType} event: userId=${payload.userId}, accountSequence=${payload.accountSequence}, inviterSequence=${payload.inviterSequence}`, + ); + const command = new CreateReferralRelationshipCommand( - BigInt(event.data.userId), - event.data.referralCode ?? null, + BigInt(payload.userId), + payload.accountSequence, + null, // referrerCode - 不通过推荐码查找 + payload.inviterSequence, // 通过 accountSequence 查找推荐人 ); const result = await this.referralService.createReferralRelationship(command); this.logger.log( - `Created referral relationship for user ${event.data.userId}, code: ${result.referralCode}`, + `Created referral relationship for user ${payload.userId} (seq: ${payload.accountSequence}), code: ${result.referralCode}, inviter: ${payload.inviterSequence ?? 'none'}`, ); } catch (error) { - this.logger.error(`Failed to create referral relationship for user ${event.data.userId}:`, error); + this.logger.error( + `Failed to create referral relationship for user ${payload.userId} (seq: ${payload.accountSequence}):`, + error, + ); } } } diff --git a/backend/services/referral-service/src/application/services/referral.service.ts b/backend/services/referral-service/src/application/services/referral.service.ts index 6fdfd612..02969b3c 100644 --- a/backend/services/referral-service/src/application/services/referral.service.ts +++ b/backend/services/referral-service/src/application/services/referral.service.ts @@ -32,6 +32,7 @@ export class ReferralService { /** * 创建推荐关系 (用户注册时调用) + * 支持通过推荐码或inviterAccountSequence查找推荐人 */ async createReferralRelationship( command: CreateReferralRelationshipCommand, @@ -45,7 +46,7 @@ export class ReferralService { let referrerId: bigint | null = null; let parentChain: bigint[] = []; - // 如果有推荐码,查找推荐人 + // 优先通过推荐码查找推荐人 if (command.referrerCode) { const referrer = await this.referralRepo.findByReferralCode(command.referrerCode); if (!referrer) { @@ -59,9 +60,31 @@ export class ReferralService { throw new BadRequestException('无效的推荐关系'); } } + // 如果没有推荐码,尝试通过inviterAccountSequence查找 + else if (command.inviterAccountSequence) { + const referrer = await this.referralRepo.findByAccountSequence(command.inviterAccountSequence); + if (referrer) { + referrerId = referrer.userId; + parentChain = referrer.referralChain; + + // 验证推荐链 + if (!this.referralChainService.validateChain(parentChain, command.userId)) { + this.logger.warn(`Invalid referral chain for user ${command.userId}, ignoring inviter`); + referrerId = null; + parentChain = []; + } + } else { + this.logger.warn(`Inviter with accountSequence ${command.inviterAccountSequence} not found, creating without referrer`); + } + } // 创建推荐关系 - const relationship = ReferralRelationship.create(command.userId, referrerId, parentChain); + const relationship = ReferralRelationship.create( + command.userId, + command.accountSequence, + referrerId, + parentChain, + ); const saved = await this.referralRepo.save(relationship); // 创建团队统计记录 @@ -80,7 +103,7 @@ export class ReferralService { await this.eventPublisher.publishDomainEvents(saved.domainEvents); saved.clearDomainEvents(); - this.logger.log(`Created referral relationship for user ${command.userId}`); + this.logger.log(`Created referral relationship for user ${command.userId}, accountSequence: ${command.accountSequence}`); return { referralCode: saved.referralCode }; } diff --git a/backend/services/referral-service/src/domain/aggregates/referral-relationship/referral-relationship.aggregate.ts b/backend/services/referral-service/src/domain/aggregates/referral-relationship/referral-relationship.aggregate.ts index 3a484b4b..b256963d 100644 --- a/backend/services/referral-service/src/domain/aggregates/referral-relationship/referral-relationship.aggregate.ts +++ b/backend/services/referral-service/src/domain/aggregates/referral-relationship/referral-relationship.aggregate.ts @@ -4,6 +4,7 @@ import { DomainEvent, ReferralRelationshipCreatedEvent } from '../../events'; export interface ReferralRelationshipProps { id: bigint; userId: bigint; + accountSequence: number; // 8位账户序列号,用于跨服务关联 referrerId: bigint | null; referralCode: string; referralChain: bigint[]; @@ -25,6 +26,7 @@ export class ReferralRelationship { private constructor( private readonly _id: bigint, private readonly _userId: UserId, + private readonly _accountSequence: number, private readonly _referrerId: UserId | null, private readonly _referralCode: ReferralCode, private readonly _referralChain: ReferralChain, @@ -39,6 +41,9 @@ export class ReferralRelationship { get userId(): bigint { return this._userId.value; } + get accountSequence(): number { + return this._accountSequence; + } get referrerId(): bigint | null { return this._referrerId?.value ?? null; } @@ -63,6 +68,7 @@ export class ReferralRelationship { */ static create( userId: bigint, + accountSequence: number, referrerId: bigint | null, parentReferralChain: bigint[] = [], ): ReferralRelationship { @@ -75,6 +81,7 @@ export class ReferralRelationship { const relationship = new ReferralRelationship( 0n, // ID will be assigned by database userIdVo, + accountSequence, referrerIdVo, referralCode, referralChain, @@ -102,6 +109,7 @@ export class ReferralRelationship { return new ReferralRelationship( props.id, UserId.create(props.userId), + props.accountSequence, props.referrerId ? UserId.create(props.referrerId) : null, ReferralCode.create(props.referralCode), ReferralChain.fromArray(props.referralChain), @@ -152,6 +160,7 @@ export class ReferralRelationship { return { id: this._id, userId: this._userId.value, + accountSequence: this._accountSequence, referrerId: this._referrerId?.value ?? null, referralCode: this._referralCode.value, referralChain: this._referralChain.toArray(), diff --git a/backend/services/referral-service/src/domain/repositories/referral-relationship.repository.interface.ts b/backend/services/referral-service/src/domain/repositories/referral-relationship.repository.interface.ts index 83929f5a..02de568f 100644 --- a/backend/services/referral-service/src/domain/repositories/referral-relationship.repository.interface.ts +++ b/backend/services/referral-service/src/domain/repositories/referral-relationship.repository.interface.ts @@ -14,6 +14,11 @@ export interface IReferralRelationshipRepository { */ findByUserId(userId: bigint): Promise; + /** + * 根据账户序列号查找 (用于跨服务关联) + */ + findByAccountSequence(accountSequence: number): Promise; + /** * 根据推荐码查找 */ diff --git a/backend/services/referral-service/src/infrastructure/repositories/referral-relationship.repository.ts b/backend/services/referral-service/src/infrastructure/repositories/referral-relationship.repository.ts index e1615491..6b32f48a 100644 --- a/backend/services/referral-service/src/infrastructure/repositories/referral-relationship.repository.ts +++ b/backend/services/referral-service/src/infrastructure/repositories/referral-relationship.repository.ts @@ -24,6 +24,7 @@ export class ReferralRelationshipRepository implements IReferralRelationshipRepo }, create: { userId: data.userId, + accountSequence: data.accountSequence, referrerId: data.referrerId, myReferralCode: data.referralCode, usedReferralCode: data.referrerId ? data.referralCode : null, @@ -45,6 +46,15 @@ export class ReferralRelationshipRepository implements IReferralRelationshipRepo return ReferralRelationship.reconstitute(this.mapToProps(record)); } + async findByAccountSequence(accountSequence: number): Promise { + const record = await this.prisma.referralRelationship.findUnique({ + where: { accountSequence }, + }); + + if (!record) return null; + return ReferralRelationship.reconstitute(this.mapToProps(record)); + } + async findByReferralCode(code: string): Promise { const record = await this.prisma.referralRelationship.findUnique({ where: { myReferralCode: code }, @@ -90,6 +100,7 @@ export class ReferralRelationshipRepository implements IReferralRelationshipRepo private mapToProps(record: { id: bigint; userId: bigint; + accountSequence: number; referrerId: bigint | null; myReferralCode: string; ancestorPath: bigint[]; @@ -99,6 +110,7 @@ export class ReferralRelationshipRepository implements IReferralRelationshipRepo return { id: record.id, userId: record.userId, + accountSequence: record.accountSequence, referrerId: record.referrerId, referralCode: record.myReferralCode, referralChain: record.ancestorPath, diff --git a/frontend/mobile-app/lib/core/constants/api_endpoints.dart b/frontend/mobile-app/lib/core/constants/api_endpoints.dart index f5606c46..d8c13e60 100644 --- a/frontend/mobile-app/lib/core/constants/api_endpoints.dart +++ b/frontend/mobile-app/lib/core/constants/api_endpoints.dart @@ -20,6 +20,7 @@ class ApiEndpoints { static const String user = '$apiPrefix/user'; static const String autoCreate = '$user/auto-create'; static const String userWallet = '$user/wallet'; // 获取钱包状态 + static const String me = '$apiPrefix/me'; // 获取当前用户完整信息 static const String profile = '$user/profile'; static const String updateProfile = '$user/profile/update'; static const String updateAvatar = '$user/avatar'; diff --git a/frontend/mobile-app/lib/core/services/account_service.dart b/frontend/mobile-app/lib/core/services/account_service.dart index 3a11de8c..beb4ea7f 100644 --- a/frontend/mobile-app/lib/core/services/account_service.dart +++ b/frontend/mobile-app/lib/core/services/account_service.dart @@ -215,6 +215,76 @@ class RecoverAccountResponse { 'RecoverAccountResponse(userSerialNum: $userSerialNum, username: $username, referralCode: $referralCode)'; } +/// 当前用户信息响应 (GET /me) +class MeResponse { + final String userId; + final int accountSequence; + final String? phoneNumber; + final String nickname; + final String? avatarUrl; + final String referralCode; + final String referralLink; + final int? inviterSequence; // 推荐人序列号 + final List walletAddresses; + final String kycStatus; + final String status; + final DateTime registeredAt; + + MeResponse({ + required this.userId, + required this.accountSequence, + this.phoneNumber, + required this.nickname, + this.avatarUrl, + required this.referralCode, + required this.referralLink, + this.inviterSequence, + required this.walletAddresses, + required this.kycStatus, + required this.status, + required this.registeredAt, + }); + + factory MeResponse.fromJson(Map json) { + return MeResponse( + userId: json['userId'] as String, + accountSequence: json['accountSequence'] as int, + phoneNumber: json['phoneNumber'] as String?, + nickname: json['nickname'] as String, + avatarUrl: json['avatarUrl'] as String?, + referralCode: json['referralCode'] as String, + referralLink: json['referralLink'] as String, + inviterSequence: json['inviterSequence'] as int?, + walletAddresses: (json['walletAddresses'] as List?) + ?.map((e) => WalletAddressInfo.fromJson(e as Map)) + .toList() ?? + [], + kycStatus: json['kycStatus'] as String, + status: json['status'] as String, + registeredAt: DateTime.parse(json['registeredAt'] as String), + ); + } + + @override + String toString() => + 'MeResponse(accountSequence: $accountSequence, nickname: $nickname, inviterSequence: $inviterSequence)'; +} + +/// 钱包地址信息 +class WalletAddressInfo { + final String chainType; + final String address; + + WalletAddressInfo({required this.chainType, required this.address}); + + factory WalletAddressInfo.fromJson(Map json) { + return WalletAddressInfo( + chainType: json['chainType'] as String, + address: json['address'] as String, + ); + } +} + /// 账号服务 /// /// 处理账号创建、钱包获取等功能 @@ -408,6 +478,54 @@ class AccountService { } } + /// 获取当前用户完整信息 (GET /me) + /// + /// 返回用户的所有信息,包括推荐人序列号 + Future getMe() async { + debugPrint('$_tag getMe() - 开始获取用户信息'); + + try { + debugPrint('$_tag getMe() - 调用 GET /me'); + final response = await _apiClient.get('/me'); + debugPrint('$_tag getMe() - API 响应状态码: ${response.statusCode}'); + + if (response.data == null) { + debugPrint('$_tag getMe() - 错误: API 返回空响应'); + throw const ApiException('获取用户信息失败: 空响应'); + } + + debugPrint('$_tag getMe() - 解析响应数据'); + final responseData = response.data as Map; + final data = responseData['data'] as Map; + final result = MeResponse.fromJson(data); + debugPrint('$_tag getMe() - 解析成功: $result'); + + // 保存推荐人序列号到本地存储 + if (result.inviterSequence != null) { + await _secureStorage.write( + key: StorageKeys.inviterSequence, + value: result.inviterSequence.toString(), + ); + debugPrint('$_tag getMe() - 保存 inviterSequence: ${result.inviterSequence}'); + } + + return result; + } on ApiException catch (e) { + debugPrint('$_tag getMe() - API 异常: $e'); + rethrow; + } catch (e, stackTrace) { + debugPrint('$_tag getMe() - 未知异常: $e'); + debugPrint('$_tag getMe() - 堆栈: $stackTrace'); + throw ApiException('获取用户信息失败: $e'); + } + } + + /// 获取推荐人序列号 (从本地存储) + Future getInviterSequence() async { + final value = await _secureStorage.read(key: StorageKeys.inviterSequence); + return value != null ? int.tryParse(value) : null; + } + /// 保存账号数据 Future _saveAccountData( CreateAccountResponse response, diff --git a/frontend/mobile-app/lib/core/storage/storage_keys.dart b/frontend/mobile-app/lib/core/storage/storage_keys.dart index 48447678..44fb5a85 100644 --- a/frontend/mobile-app/lib/core/storage/storage_keys.dart +++ b/frontend/mobile-app/lib/core/storage/storage_keys.dart @@ -7,6 +7,7 @@ class StorageKeys { static const String avatarSvg = 'avatar_svg'; // 随机 SVG 头像(初始生成) static const String avatarUrl = 'avatar_url'; // 用户上传的头像URL static const String referralCode = 'referral_code'; // 推荐码 + static const String inviterSequence = 'inviter_sequence'; // 推荐人序列号 static const String isAccountCreated = 'is_account_created'; // 账号是否已创建 // 钱包信息 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 4a56b262..a8eaf00c 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 @@ -18,13 +18,14 @@ class ProfilePage extends ConsumerStatefulWidget { } class _ProfilePageState extends ConsumerState { - // 用户数据(从存储加载) + // 用户数据(从存储/API加载) String _nickname = '加载中...'; String _serialNumber = '--'; String? _avatarSvg; String? _avatarUrl; String? _localAvatarPath; // 本地头像文件路径 - final String _referrerSerial = '87654321'; + String _referrerSerial = '--'; // 推荐人序列号(从API获取) + String _referralCode = '--'; // 我的推荐码 final String _community = '星空社区'; final String _parentCommunity = '银河社区'; final String _childCommunity = '星辰社区'; @@ -87,13 +88,14 @@ class _ProfilePageState extends ConsumerState { Future _loadUserData() async { final accountService = ref.read(accountServiceProvider); - // 并行加载所有数据 + // 并行加载本地存储数据 final results = await Future.wait([ accountService.getUsername(), accountService.getUserSerialNum(), accountService.getAvatarSvg(), accountService.getAvatarUrl(), accountService.getLocalAvatarPath(), + accountService.getReferralCode(), ]); final username = results[0] as String?; @@ -101,6 +103,7 @@ class _ProfilePageState extends ConsumerState { final avatarSvg = results[2] as String?; final avatarUrl = results[3] as String?; final localAvatarPath = results[4] as String?; + final referralCode = results[5] as String?; if (mounted) { setState(() { @@ -109,6 +112,7 @@ class _ProfilePageState extends ConsumerState { _avatarSvg = avatarSvg; _avatarUrl = avatarUrl; _localAvatarPath = localAvatarPath; + _referralCode = referralCode ?? '--'; }); // 如果有远程URL但没有本地缓存,后台下载并缓存 @@ -116,6 +120,33 @@ class _ProfilePageState extends ConsumerState { _downloadAndCacheAvatar(avatarUrl); } } + + // 从API获取完整用户信息(包括推荐人) + _loadMeData(); + } + + /// 从API加载完整用户信息 + Future _loadMeData() async { + try { + final accountService = ref.read(accountServiceProvider); + final meData = await accountService.getMe(); + + if (mounted) { + setState(() { + _referrerSerial = meData.inviterSequence?.toString() ?? '无'; + // 如果API返回了更新的数据,也更新本地显示 + if (meData.nickname.isNotEmpty) { + _nickname = meData.nickname; + } + if (meData.referralCode.isNotEmpty) { + _referralCode = meData.referralCode; + } + }); + } + } catch (e) { + debugPrint('[ProfilePage] 加载用户信息失败: $e'); + // 失败时保持现有数据,不影响页面显示 + } } /// 后台下载并缓存头像