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:
parent
cf7230457f
commit
5d671bf5ec
|
|
@ -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 }>;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 - 需要后续实现
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ export interface IReferralRelationshipRepository {
|
|||
*/
|
||||
findByUserId(userId: bigint): Promise<ReferralRelationship | null>;
|
||||
|
||||
/**
|
||||
* 根据账户序列号查找 (用于跨服务关联)
|
||||
*/
|
||||
findByAccountSequence(accountSequence: number): Promise<ReferralRelationship | null>;
|
||||
|
||||
/**
|
||||
* 根据推荐码查找
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'; // 账号是否已创建
|
||||
|
||||
// 钱包信息
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
// 失败时保持现有数据,不影响页面显示
|
||||
}
|
||||
}
|
||||
|
||||
/// 后台下载并缓存头像
|
||||
|
|
|
|||
Loading…
Reference in New Issue