feat(referral): integrate referral system with identity-service and mobile-app

## Backend Changes

### referral-service
- Add accountSequence field to ReferralRelationship aggregate for cross-service user identification
- Add findByAccountSequence() method to repository interface and implementation
- Update CreateReferralRelationshipCommand to accept accountSequence and inviterAccountSequence
- Modify ReferralService to support looking up inviter by accountSequence
- Update event handler to listen to identity.UserAccountAutoCreated and identity.UserAccountCreated topics
- Add initial database migration with all tables including accountSequence field
- Update DTO and controller to support new parameters

### identity-service
- Add inviterSequence field to MeResult interface
- Update getMe() method to return inviterSequence from user account
- Update MeResponseDto to include inviterSequence field

## Frontend Changes (mobile-app)

### API & Storage
- Add /me endpoint constant in api_endpoints.dart
- Add inviterSequence key in storage_keys.dart
- Add MeResponse and WalletAddressInfo classes in account_service.dart
- Add getMe() method to fetch complete user info including inviter
- Add getInviterSequence() method to retrieve from local storage

### Profile Page
- Update profile_page.dart to load referrer info from API
- Add _loadMeData() method to call getMe() API
- Display inviterSequence (referrer serial number) dynamically

## Flow Summary
1. User creates account with optional inviterReferralCode
2. identity-service validates and saves inviterSequence
3. identity-service publishes UserAccountAutoCreated/UserAccountCreated event
4. referral-service listens and creates referral relationship using inviterAccountSequence
5. Mobile app calls GET /me to display inviter info in profile page

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-08 22:37:06 -08:00
parent cf7230457f
commit 5d671bf5ec
17 changed files with 445 additions and 23 deletions

View File

@ -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 }>;

View File

@ -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;

View File

@ -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,

View File

@ -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;

View File

@ -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")

View File

@ -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 - 需要后续实现
};
}
}

View File

@ -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 {

View File

@ -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,
) {}
}

View File

@ -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<string, unknown>): Promise<void> {
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,
);
}
}
}

View File

@ -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 };
}

View File

@ -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(),

View File

@ -14,6 +14,11 @@ export interface IReferralRelationshipRepository {
*/
findByUserId(userId: bigint): Promise<ReferralRelationship | null>;
/**
* ()
*/
findByAccountSequence(accountSequence: number): Promise<ReferralRelationship | null>;
/**
*
*/

View File

@ -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<ReferralRelationship | null> {
const record = await this.prisma.referralRelationship.findUnique({
where: { accountSequence },
});
if (!record) return null;
return ReferralRelationship.reconstitute(this.mapToProps(record));
}
async findByReferralCode(code: string): Promise<ReferralRelationship | null> {
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,

View File

@ -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';

View File

@ -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<WalletAddressInfo> 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<String, dynamic> 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<dynamic>?)
?.map((e) => WalletAddressInfo.fromJson(e as Map<String, dynamic>))
.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<String, dynamic> json) {
return WalletAddressInfo(
chainType: json['chainType'] as String,
address: json['address'] as String,
);
}
}
///
///
///
@ -408,6 +478,54 @@ class AccountService {
}
}
/// (GET /me)
///
///
Future<MeResponse> 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<String, dynamic>;
final data = responseData['data'] as Map<String, dynamic>;
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<int?> getInviterSequence() async {
final value = await _secureStorage.read(key: StorageKeys.inviterSequence);
return value != null ? int.tryParse(value) : null;
}
///
Future<void> _saveAccountData(
CreateAccountResponse response,

View File

@ -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'; //
//

View File

@ -18,13 +18,14 @@ class ProfilePage extends ConsumerStatefulWidget {
}
class _ProfilePageState extends ConsumerState<ProfilePage> {
//
// /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<ProfilePage> {
Future<void> _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<ProfilePage> {
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<ProfilePage> {
_avatarSvg = avatarSvg;
_avatarUrl = avatarUrl;
_localAvatarPath = localAvatarPath;
_referralCode = referralCode ?? '--';
});
// URL但没有本地缓存
@ -116,6 +120,33 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_downloadAndCacheAvatar(avatarUrl);
}
}
// API获取完整用户信息
_loadMeData();
}
/// API加载完整用户信息
Future<void> _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');
//
}
}
///