diff --git a/backend/services/authorization-service/prisma/migrations/20241210000001_add_account_sequence/migration.sql b/backend/services/authorization-service/prisma/migrations/20241210000001_add_account_sequence/migration.sql new file mode 100644 index 00000000..c706d79a --- /dev/null +++ b/backend/services/authorization-service/prisma/migrations/20241210000001_add_account_sequence/migration.sql @@ -0,0 +1,44 @@ +-- Step 1: Add account_sequence columns to all tables first +ALTER TABLE "authorization_roles" ADD COLUMN "account_sequence" BIGINT; +ALTER TABLE "monthly_assessments" ADD COLUMN "account_sequence" BIGINT; +ALTER TABLE "monthly_bypasses" ADD COLUMN "account_sequence" BIGINT; +ALTER TABLE "stickman_rankings" ADD COLUMN "account_sequence" BIGINT; + +-- Step 2: Backfill account_sequence from existing user_id (which is still String at this point) +UPDATE "authorization_roles" SET "account_sequence" = CAST("user_id" AS BIGINT) WHERE "account_sequence" IS NULL; +UPDATE "monthly_assessments" SET "account_sequence" = CAST("user_id" AS BIGINT) WHERE "account_sequence" IS NULL; +UPDATE "monthly_bypasses" SET "account_sequence" = CAST("user_id" AS BIGINT) WHERE "account_sequence" IS NULL; +UPDATE "stickman_rankings" SET "account_sequence" = CAST("user_id" AS BIGINT) WHERE "account_sequence" IS NULL; + +-- Step 3: Make account_sequence NOT NULL +ALTER TABLE "authorization_roles" ALTER COLUMN "account_sequence" SET NOT NULL; +ALTER TABLE "monthly_assessments" ALTER COLUMN "account_sequence" SET NOT NULL; +ALTER TABLE "monthly_bypasses" ALTER COLUMN "account_sequence" SET NOT NULL; +ALTER TABLE "stickman_rankings" ALTER COLUMN "account_sequence" SET NOT NULL; + +-- Step 4: Drop existing unique constraints that reference user_id +DROP INDEX IF EXISTS "authorization_roles_user_id_role_type_region_code_key"; + +-- Step 5: Change user_id column type from String to BigInt +ALTER TABLE "authorization_roles" ALTER COLUMN "user_id" TYPE BIGINT USING "user_id"::BIGINT; +ALTER TABLE "monthly_assessments" ALTER COLUMN "user_id" TYPE BIGINT USING "user_id"::BIGINT; +ALTER TABLE "monthly_bypasses" ALTER COLUMN "user_id" TYPE BIGINT USING "user_id"::BIGINT; +ALTER TABLE "stickman_rankings" ALTER COLUMN "user_id" TYPE BIGINT USING "user_id"::BIGINT; + +-- Step 6: Change authorized_by, revoked_by, bypassed_by, grantedBy, approverXId to BigInt +ALTER TABLE "authorization_roles" ALTER COLUMN "authorized_by" TYPE BIGINT USING "authorized_by"::BIGINT; +ALTER TABLE "authorization_roles" ALTER COLUMN "revoked_by" TYPE BIGINT USING "revoked_by"::BIGINT; +ALTER TABLE "monthly_assessments" ALTER COLUMN "bypassed_by" TYPE BIGINT USING "bypassed_by"::BIGINT; +ALTER TABLE "monthly_bypasses" ALTER COLUMN "granted_by" TYPE BIGINT USING "granted_by"::BIGINT; +ALTER TABLE "monthly_bypasses" ALTER COLUMN "approver1_id" TYPE BIGINT USING "approver1_id"::BIGINT; +ALTER TABLE "monthly_bypasses" ALTER COLUMN "approver2_id" TYPE BIGINT USING "approver2_id"::BIGINT; +ALTER TABLE "monthly_bypasses" ALTER COLUMN "approver3_id" TYPE BIGINT USING "approver3_id"::BIGINT; + +-- Step 7: Create new unique constraint using account_sequence +CREATE UNIQUE INDEX "authorization_roles_account_sequence_role_type_region_code_key" ON "authorization_roles"("account_sequence", "role_type", "region_code"); + +-- Step 8: Create indexes for account_sequence +CREATE INDEX "idx_authorization_roles_account_sequence" ON "authorization_roles"("account_sequence"); +CREATE INDEX "idx_monthly_assessments_account_sequence_month" ON "monthly_assessments"("account_sequence", "assessment_month"); +CREATE INDEX "idx_monthly_bypasses_account_sequence_month" ON "monthly_bypasses"("account_sequence", "bypass_month"); +CREATE INDEX "idx_stickman_rankings_account_sequence_month" ON "stickman_rankings"("account_sequence", "current_month"); diff --git a/backend/services/authorization-service/src/api/controllers/admin-authorization.controller.ts b/backend/services/authorization-service/src/api/controllers/admin-authorization.controller.ts index 188c66dc..d72b4a1e 100644 --- a/backend/services/authorization-service/src/api/controllers/admin-authorization.controller.ts +++ b/backend/services/authorization-service/src/api/controllers/admin-authorization.controller.ts @@ -18,14 +18,16 @@ export class AdminAuthorizationController { @ApiOperation({ summary: '授权正式省公司(管理员)' }) @ApiResponse({ status: 201, description: '授权成功' }) async grantProvinceCompany( - @CurrentUser() user: { userId: string }, + @CurrentUser() user: { userId: string; accountSequence: number }, @Body() dto: GrantProvinceCompanyDto, ): Promise<{ message: string }> { const command = new GrantProvinceCompanyCommand( dto.userId, + dto.accountSequence, dto.provinceCode, dto.provinceName, user.userId, + user.accountSequence, ) await this.applicationService.grantProvinceCompany(command) return { message: '正式省公司授权成功' } @@ -36,14 +38,16 @@ export class AdminAuthorizationController { @ApiOperation({ summary: '授权正式市公司(管理员)' }) @ApiResponse({ status: 201, description: '授权成功' }) async grantCityCompany( - @CurrentUser() user: { userId: string }, + @CurrentUser() user: { userId: string; accountSequence: number }, @Body() dto: GrantCityCompanyDto, ): Promise<{ message: string }> { const command = new GrantCityCompanyCommand( dto.userId, + dto.accountSequence, dto.cityCode, dto.cityName, user.userId, + user.accountSequence, ) await this.applicationService.grantCityCompany(command) return { message: '正式市公司授权成功' } diff --git a/backend/services/authorization-service/src/api/dto/request/grant-city-company.dto.ts b/backend/services/authorization-service/src/api/dto/request/grant-city-company.dto.ts index e4f04085..b886471b 100644 --- a/backend/services/authorization-service/src/api/dto/request/grant-city-company.dto.ts +++ b/backend/services/authorization-service/src/api/dto/request/grant-city-company.dto.ts @@ -1,4 +1,4 @@ -import { IsString, IsNotEmpty, MaxLength } from 'class-validator' +import { IsString, IsNotEmpty, MaxLength, IsNumber } from 'class-validator' import { ApiProperty } from '@nestjs/swagger' export class GrantCityCompanyDto { @@ -7,6 +7,11 @@ export class GrantCityCompanyDto { @IsNotEmpty({ message: '用户ID不能为空' }) userId: string + @ApiProperty({ description: '账户序列号' }) + @IsNumber() + @IsNotEmpty({ message: '账户序列号不能为空' }) + accountSequence: number + @ApiProperty({ description: '城市代码', example: '430100' }) @IsString() @IsNotEmpty({ message: '城市代码不能为空' }) diff --git a/backend/services/authorization-service/src/api/dto/request/grant-province-company.dto.ts b/backend/services/authorization-service/src/api/dto/request/grant-province-company.dto.ts index 9425a213..d6a33b03 100644 --- a/backend/services/authorization-service/src/api/dto/request/grant-province-company.dto.ts +++ b/backend/services/authorization-service/src/api/dto/request/grant-province-company.dto.ts @@ -1,4 +1,4 @@ -import { IsString, IsNotEmpty, MaxLength } from 'class-validator' +import { IsString, IsNotEmpty, MaxLength, IsNumber } from 'class-validator' import { ApiProperty } from '@nestjs/swagger' export class GrantProvinceCompanyDto { @@ -7,6 +7,11 @@ export class GrantProvinceCompanyDto { @IsNotEmpty({ message: '用户ID不能为空' }) userId: string + @ApiProperty({ description: '账户序列号' }) + @IsNumber() + @IsNotEmpty({ message: '账户序列号不能为空' }) + accountSequence: number + @ApiProperty({ description: '省份代码', example: '430000' }) @IsString() @IsNotEmpty({ message: '省份代码不能为空' }) diff --git a/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.spec.ts b/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.spec.ts index d2ae2662..53290fda 100644 --- a/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.spec.ts +++ b/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.spec.ts @@ -7,7 +7,7 @@ describe('AuthorizationRole Aggregate', () => { describe('createCommunityAuth', () => { it('should create community authorization', () => { const auth = AuthorizationRole.createCommunityAuth({ - userId: UserId.create('user-1'), + userId: UserId.create('user-1', BigInt(1)), communityName: '量子社区', }) @@ -24,7 +24,7 @@ describe('AuthorizationRole Aggregate', () => { describe('createAuthProvinceCompany', () => { it('should create auth province company authorization', () => { const auth = AuthorizationRole.createAuthProvinceCompany({ - userId: UserId.create('user-1'), + userId: UserId.create('user-1', BigInt(1)), provinceCode: '430000', provinceName: '湖南省', }) @@ -42,7 +42,7 @@ describe('AuthorizationRole Aggregate', () => { describe('createAuthCityCompany', () => { it('should create auth city company authorization', () => { const auth = AuthorizationRole.createAuthCityCompany({ - userId: UserId.create('user-1'), + userId: UserId.create('user-1', BigInt(1)), cityCode: '430100', cityName: '长沙市', }) @@ -57,9 +57,9 @@ describe('AuthorizationRole Aggregate', () => { describe('createProvinceCompany', () => { it('should create official province company with active benefits', () => { - const adminId = AdminUserId.create('admin-1') + const adminId = AdminUserId.create('admin-1', BigInt(101)) const auth = AuthorizationRole.createProvinceCompany({ - userId: UserId.create('user-1'), + userId: UserId.create('user-1', BigInt(1)), provinceCode: '430000', provinceName: '湖南省', adminId, @@ -76,7 +76,7 @@ describe('AuthorizationRole Aggregate', () => { describe('activateBenefit', () => { it('should activate benefit and emit event', () => { const auth = AuthorizationRole.createCommunityAuth({ - userId: UserId.create('user-1'), + userId: UserId.create('user-1', BigInt(1)), communityName: '量子社区', }) auth.clearDomainEvents() @@ -92,10 +92,10 @@ describe('AuthorizationRole Aggregate', () => { it('should throw error if already active', () => { const auth = AuthorizationRole.createProvinceCompany({ - userId: UserId.create('user-1'), + userId: UserId.create('user-1', BigInt(1)), provinceCode: '430000', provinceName: '湖南省', - adminId: AdminUserId.create('admin-1'), + adminId: AdminUserId.create('admin-1', BigInt(101)), }) expect(() => auth.activateBenefit()).toThrow(DomainError) @@ -105,10 +105,10 @@ describe('AuthorizationRole Aggregate', () => { describe('deactivateBenefit', () => { it('should deactivate benefit and reset month index', () => { const auth = AuthorizationRole.createProvinceCompany({ - userId: UserId.create('user-1'), + userId: UserId.create('user-1', BigInt(1)), provinceCode: '430000', provinceName: '湖南省', - adminId: AdminUserId.create('admin-1'), + adminId: AdminUserId.create('admin-1', BigInt(101)), }) auth.clearDomainEvents() @@ -124,14 +124,14 @@ describe('AuthorizationRole Aggregate', () => { describe('revoke', () => { it('should revoke authorization', () => { const auth = AuthorizationRole.createProvinceCompany({ - userId: UserId.create('user-1'), + userId: UserId.create('user-1', BigInt(1)), provinceCode: '430000', provinceName: '湖南省', - adminId: AdminUserId.create('admin-1'), + adminId: AdminUserId.create('admin-1', BigInt(101)), }) auth.clearDomainEvents() - auth.revoke(AdminUserId.create('admin-2'), '违规操作') + auth.revoke(AdminUserId.create('admin-2', BigInt(102)), '违规操作') expect(auth.status).toBe(AuthorizationStatus.REVOKED) expect(auth.benefitActive).toBe(false) @@ -142,14 +142,14 @@ describe('AuthorizationRole Aggregate', () => { it('should throw error if already revoked', () => { const auth = AuthorizationRole.createProvinceCompany({ - userId: UserId.create('user-1'), + userId: UserId.create('user-1', BigInt(1)), provinceCode: '430000', provinceName: '湖南省', - adminId: AdminUserId.create('admin-1'), + adminId: AdminUserId.create('admin-1', BigInt(101)), }) - auth.revoke(AdminUserId.create('admin-2'), '违规操作') + auth.revoke(AdminUserId.create('admin-2', BigInt(102)), '违规操作') - expect(() => auth.revoke(AdminUserId.create('admin-3'), '再次撤销')).toThrow( + expect(() => auth.revoke(AdminUserId.create('admin-3', BigInt(103)), '再次撤销')).toThrow( DomainError, ) }) @@ -158,7 +158,7 @@ describe('AuthorizationRole Aggregate', () => { describe('exemptLocalPercentageCheck', () => { it('should exempt from percentage check', () => { const auth = AuthorizationRole.createAuthProvinceCompany({ - userId: UserId.create('user-1'), + userId: UserId.create('user-1', BigInt(1)), provinceCode: '430000', provinceName: '湖南省', }) @@ -166,7 +166,7 @@ describe('AuthorizationRole Aggregate', () => { expect(auth.exemptFromPercentageCheck).toBe(false) expect(auth.needsLocalPercentageCheck()).toBe(true) - auth.exemptLocalPercentageCheck(AdminUserId.create('admin-1')) + auth.exemptLocalPercentageCheck(AdminUserId.create('admin-1', BigInt(101))) expect(auth.exemptFromPercentageCheck).toBe(true) expect(auth.needsLocalPercentageCheck()).toBe(false) @@ -176,7 +176,7 @@ describe('AuthorizationRole Aggregate', () => { describe('incrementMonthIndex', () => { it('should increment month index', () => { const auth = AuthorizationRole.createCommunityAuth({ - userId: UserId.create('user-1'), + userId: UserId.create('user-1', BigInt(1)), communityName: '量子社区', }) auth.activateBenefit() diff --git a/backend/services/authorization-service/src/domain/services/assessment-calculator.service.ts b/backend/services/authorization-service/src/domain/services/assessment-calculator.service.ts index 12aefb0d..b5186204 100644 --- a/backend/services/authorization-service/src/domain/services/assessment-calculator.service.ts +++ b/backend/services/authorization-service/src/domain/services/assessment-calculator.service.ts @@ -6,6 +6,7 @@ import { IMonthlyAssessmentRepository, IAuthorizationRoleRepository } from '@/do export interface TeamStatistics { userId: string + accountSequence: bigint totalTeamPlantingCount: number getProvinceTeamCount(provinceCode: string): number getCityTeamCount(cityCode: string): number @@ -13,6 +14,7 @@ export interface TeamStatistics { export interface ITeamStatisticsRepository { findByUserId(userId: string): Promise + findByAccountSequence(accountSequence: bigint): Promise } export class AssessmentCalculatorService { diff --git a/backend/services/authorization-service/src/infrastructure/kafka/event-consumer.controller.ts b/backend/services/authorization-service/src/infrastructure/kafka/event-consumer.controller.ts index d3c3308e..15627fbf 100644 --- a/backend/services/authorization-service/src/infrastructure/kafka/event-consumer.controller.ts +++ b/backend/services/authorization-service/src/infrastructure/kafka/event-consumer.controller.ts @@ -141,7 +141,7 @@ export class EventConsumerController { // 2. 获取用户所有授权 const authorizations = await this.authorizationRepository.findByUserId( - UserId.create(userId), + UserId.create(userId, teamStats.accountSequence), ) if (authorizations.length === 0) { diff --git a/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts b/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts index f1ea450c..ab133826 100644 --- a/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts +++ b/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts @@ -76,7 +76,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi ): Promise { const record = await this.prisma.authorizationRole.findFirst({ where: { - userId: userId.value, + userId: BigInt(userId.value), roleType: roleType, }, }) @@ -90,7 +90,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi ): Promise { const record = await this.prisma.authorizationRole.findFirst({ where: { - userId: userId.value, + userId: BigInt(userId.value), roleType: roleType, regionCode: regionCode.value, }, @@ -113,7 +113,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi async findByUserId(userId: UserId): Promise { const records = await this.prisma.authorizationRole.findMany({ - where: { userId: userId.value }, + where: { userId: BigInt(userId.value) }, orderBy: { createdAt: 'desc' }, }) return records.map((record) => this.toDomain(record)) @@ -156,7 +156,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi async findPendingByUserId(userId: UserId): Promise { const records = await this.prisma.authorizationRole.findMany({ where: { - userId: userId.value, + userId: BigInt(userId.value), status: AuthorizationStatus.PENDING, }, }) diff --git a/backend/services/authorization-service/src/infrastructure/persistence/repositories/monthly-assessment.repository.impl.ts b/backend/services/authorization-service/src/infrastructure/persistence/repositories/monthly-assessment.repository.impl.ts index c712fe02..e9b5cb75 100644 --- a/backend/services/authorization-service/src/infrastructure/persistence/repositories/monthly-assessment.repository.impl.ts +++ b/backend/services/authorization-service/src/infrastructure/persistence/repositories/monthly-assessment.repository.impl.ts @@ -148,7 +148,7 @@ export class MonthlyAssessmentRepositoryImpl implements IMonthlyAssessmentReposi async findByUserAndMonth(userId: UserId, month: Month): Promise { const records = await this.prisma.monthlyAssessment.findMany({ where: { - userId: userId.value, + userId: BigInt(userId.value), assessmentMonth: month.value, }, }) diff --git a/backend/services/authorization-service/src/shared/decorators/current-user.decorator.ts b/backend/services/authorization-service/src/shared/decorators/current-user.decorator.ts index 8e8ac369..a5770a16 100644 --- a/backend/services/authorization-service/src/shared/decorators/current-user.decorator.ts +++ b/backend/services/authorization-service/src/shared/decorators/current-user.decorator.ts @@ -2,6 +2,7 @@ import { createParamDecorator, ExecutionContext } from '@nestjs/common' export interface CurrentUserData { userId: string + accountSequence?: number walletAddress?: string roles?: string[] } diff --git a/backend/services/authorization-service/test/domain-services.integration-spec.ts b/backend/services/authorization-service/test/domain-services.integration-spec.ts index 6d19d1a7..b7589165 100644 --- a/backend/services/authorization-service/test/domain-services.integration-spec.ts +++ b/backend/services/authorization-service/test/domain-services.integration-spec.ts @@ -27,6 +27,8 @@ describe('Domain Services Integration Tests', () => { findPendingByUserId: jest.fn(), findByStatus: jest.fn(), delete: jest.fn(), + findByAccountSequenceAndRoleType: jest.fn(), + findByAccountSequence: jest.fn(), } const mockMonthlyAssessmentRepository: jest.Mocked = { @@ -52,6 +54,7 @@ describe('Domain Services Integration Tests', () => { // Mock team statistics repository const mockTeamStatisticsRepository: jest.Mocked = { findByUserId: jest.fn(), + findByAccountSequence: jest.fn(), } beforeAll(async () => { @@ -93,7 +96,7 @@ describe('Domain Services Integration Tests', () => { describe('AuthorizationValidatorService', () => { describe('validateAuthorizationRequest', () => { it('should return success when no conflicts in referral chain', async () => { - const userId = UserId.create('user-123') + const userId = UserId.create('user-123', BigInt(123)) const roleType = RoleType.AUTH_PROVINCE_COMPANY const regionCode = RegionCode.create('430000') @@ -112,7 +115,7 @@ describe('Domain Services Integration Tests', () => { }) it('should return failure when user already has province authorization', async () => { - const userId = UserId.create('user-123') + const userId = UserId.create('user-123', BigInt(123)) const roleType = RoleType.AUTH_PROVINCE_COMPANY const regionCode = RegionCode.create('430000') @@ -136,11 +139,11 @@ describe('Domain Services Integration Tests', () => { }) it('should return failure when ancestor has same region authorization', async () => { - const userId = UserId.create('user-123') + const userId = UserId.create('user-123', BigInt(123)) const roleType = RoleType.AUTH_PROVINCE_COMPANY const regionCode = RegionCode.create('430000') - const ancestorUserId = UserId.create('ancestor-user') + const ancestorUserId = UserId.create('ancestor-user', BigInt(999)) mockAuthorizationRoleRepository.findByUserIdAndRoleType.mockResolvedValue(null) mockReferralRepository.findByUserId.mockResolvedValue({ parentId: 'ancestor-user' }) @@ -177,18 +180,18 @@ describe('Domain Services Integration Tests', () => { // Mock two authorizations const auth1 = AuthorizationRole.createAuthProvinceCompany({ - userId: UserId.create('user-1'), + userId: UserId.create('user-1', BigInt(1)), provinceCode: '430000', provinceName: '湖南省', }) - auth1.authorize(UserId.create('admin')) + auth1.authorize(UserId.create('admin', BigInt(100))) const auth2 = AuthorizationRole.createAuthProvinceCompany({ - userId: UserId.create('user-2'), + userId: UserId.create('user-2', BigInt(2)), provinceCode: '430000', provinceName: '湖南省', }) - auth2.authorize(UserId.create('admin')) + auth2.authorize(UserId.create('admin', BigInt(100))) mockAuthorizationRoleRepository.findActiveByRoleTypeAndRegion.mockResolvedValue([auth1, auth2]) mockMonthlyAssessmentRepository.findByAuthorizationAndMonth.mockResolvedValue(null) @@ -197,12 +200,14 @@ describe('Domain Services Integration Tests', () => { mockTeamStatisticsRepository.findByUserId .mockResolvedValueOnce({ userId: 'user-1', + accountSequence: BigInt(1), totalTeamPlantingCount: 200, getProvinceTeamCount: () => 70, getCityTeamCount: () => 0, }) .mockResolvedValueOnce({ userId: 'user-2', + accountSequence: BigInt(2), totalTeamPlantingCount: 100, getProvinceTeamCount: () => 35, getCityTeamCount: () => 0, diff --git a/backend/services/referral-service/test/domain/aggregates/referral-relationship.aggregate.spec.ts b/backend/services/referral-service/test/domain/aggregates/referral-relationship.aggregate.spec.ts index d01ea10f..5d7c3e3e 100644 --- a/backend/services/referral-service/test/domain/aggregates/referral-relationship.aggregate.spec.ts +++ b/backend/services/referral-service/test/domain/aggregates/referral-relationship.aggregate.spec.ts @@ -4,9 +4,10 @@ import { ReferralRelationshipCreatedEvent } from '../../../src/domain/events'; describe('ReferralRelationship Aggregate', () => { describe('create', () => { it('should create referral relationship without referrer', () => { - const relationship = ReferralRelationship.create(100n, null); + const relationship = ReferralRelationship.create(100n, 12345678, null); expect(relationship.userId).toBe(100n); + expect(relationship.accountSequence).toBe(12345678); expect(relationship.referrerId).toBeNull(); expect(relationship.referralCode).toMatch(/^RWA/); expect(relationship.referralChain).toEqual([]); @@ -14,15 +15,16 @@ describe('ReferralRelationship Aggregate', () => { it('should create referral relationship with referrer', () => { const parentChain = [200n, 300n]; - const relationship = ReferralRelationship.create(100n, 50n, parentChain); + const relationship = ReferralRelationship.create(100n, 12345678, 50n, parentChain); expect(relationship.userId).toBe(100n); + expect(relationship.accountSequence).toBe(12345678); expect(relationship.referrerId).toBe(50n); expect(relationship.referralChain).toEqual([50n, 200n, 300n]); }); it('should emit ReferralRelationshipCreatedEvent', () => { - const relationship = ReferralRelationship.create(100n, 50n); + const relationship = ReferralRelationship.create(100n, 12345678, 50n); expect(relationship.domainEvents.length).toBe(1); expect(relationship.domainEvents[0]).toBeInstanceOf(ReferralRelationshipCreatedEvent); @@ -38,6 +40,7 @@ describe('ReferralRelationship Aggregate', () => { const props = { id: 1n, userId: 100n, + accountSequence: 12345678, referrerId: 50n, referralCode: 'RWATEST123', referralChain: [50n, 200n], @@ -49,6 +52,7 @@ describe('ReferralRelationship Aggregate', () => { expect(relationship.id).toBe(1n); expect(relationship.userId).toBe(100n); + expect(relationship.accountSequence).toBe(12345678); expect(relationship.referrerId).toBe(50n); expect(relationship.referralCode).toBe('RWATEST123'); expect(relationship.referralChain).toEqual([50n, 200n]); @@ -57,19 +61,19 @@ describe('ReferralRelationship Aggregate', () => { describe('getDirectReferrer', () => { it('should return direct referrer', () => { - const relationship = ReferralRelationship.create(100n, 50n, [200n]); + const relationship = ReferralRelationship.create(100n, 12345678, 50n, [200n]); expect(relationship.getDirectReferrer()).toBe(50n); }); it('should return null when no referrer', () => { - const relationship = ReferralRelationship.create(100n, null); + const relationship = ReferralRelationship.create(100n, 12345678, null); expect(relationship.getDirectReferrer()).toBeNull(); }); }); describe('getReferrerAtLevel', () => { it('should return referrer at specific level', () => { - const relationship = ReferralRelationship.create(100n, 50n, [200n, 300n]); + const relationship = ReferralRelationship.create(100n, 12345678, 50n, [200n, 300n]); expect(relationship.getReferrerAtLevel(0)).toBe(50n); expect(relationship.getReferrerAtLevel(1)).toBe(200n); @@ -79,21 +83,21 @@ describe('ReferralRelationship Aggregate', () => { describe('getAllAncestorIds', () => { it('should return all ancestor IDs', () => { - const relationship = ReferralRelationship.create(100n, 50n, [200n, 300n]); + const relationship = ReferralRelationship.create(100n, 12345678, 50n, [200n, 300n]); expect(relationship.getAllAncestorIds()).toEqual([50n, 200n, 300n]); }); }); describe('getChainDepth', () => { it('should return chain depth', () => { - const relationship = ReferralRelationship.create(100n, 50n, [200n, 300n]); + const relationship = ReferralRelationship.create(100n, 12345678, 50n, [200n, 300n]); expect(relationship.getChainDepth()).toBe(3); }); }); describe('clearDomainEvents', () => { it('should clear domain events', () => { - const relationship = ReferralRelationship.create(100n, 50n); + const relationship = ReferralRelationship.create(100n, 12345678, 50n); expect(relationship.domainEvents.length).toBe(1); relationship.clearDomainEvents(); @@ -103,10 +107,11 @@ describe('ReferralRelationship Aggregate', () => { describe('toPersistence', () => { it('should convert to persistence format', () => { - const relationship = ReferralRelationship.create(100n, 50n, [200n]); + const relationship = ReferralRelationship.create(100n, 12345678, 50n, [200n]); const data = relationship.toPersistence(); expect(data.userId).toBe(100n); + expect(data.accountSequence).toBe(12345678); expect(data.referrerId).toBe(50n); expect(data.referralCode).toMatch(/^RWA/); expect(data.referralChain).toEqual([50n, 200n]); diff --git a/backend/services/referral-service/test/integration/repositories/referral-relationship.repository.integration.spec.ts b/backend/services/referral-service/test/integration/repositories/referral-relationship.repository.integration.spec.ts index cf461928..87872b26 100644 --- a/backend/services/referral-service/test/integration/repositories/referral-relationship.repository.integration.spec.ts +++ b/backend/services/referral-service/test/integration/repositories/referral-relationship.repository.integration.spec.ts @@ -30,48 +30,52 @@ describe('ReferralRelationshipRepository (Integration)', () => { describe('save', () => { it('should save a new referral relationship', async () => { - const relationship = ReferralRelationship.create(100n, null); + const relationship = ReferralRelationship.create(100n, 12345678, null); const saved = await repository.save(relationship); expect(saved).toBeDefined(); expect(saved.userId).toBe(100n); + expect(saved.accountSequence).toBe(12345678); expect(saved.referralCode).toBeDefined(); expect(saved.referralChain).toEqual([]); }); it('should save referral relationship with referrer', async () => { // First create the referrer - const referrer = ReferralRelationship.create(50n, null); + const referrer = ReferralRelationship.create(50n, 11111111, null); await repository.save(referrer); // Then create with referrer - const relationship = ReferralRelationship.create(100n, 50n, []); + const relationship = ReferralRelationship.create(100n, 12345678, 50n, []); const saved = await repository.save(relationship); expect(saved.userId).toBe(100n); + expect(saved.accountSequence).toBe(12345678); expect(saved.referrerId).toBe(50n); expect(saved.referralChain).toContain(50n); }); it('should update existing referral relationship', async () => { - const relationship = ReferralRelationship.create(100n, null); + const relationship = ReferralRelationship.create(100n, 12345678, null); await repository.save(relationship); // Save again should update const updated = await repository.save(relationship); expect(updated.userId).toBe(100n); + expect(updated.accountSequence).toBe(12345678); }); }); describe('findByUserId', () => { it('should find relationship by user ID', async () => { - const relationship = ReferralRelationship.create(100n, null); + const relationship = ReferralRelationship.create(100n, 12345678, null); await repository.save(relationship); const found = await repository.findByUserId(100n); expect(found).toBeDefined(); expect(found!.userId).toBe(100n); + expect(found!.accountSequence).toBe(12345678); }); it('should return null for non-existent user', async () => { @@ -82,13 +86,14 @@ describe('ReferralRelationshipRepository (Integration)', () => { describe('findByReferralCode', () => { it('should find relationship by referral code', async () => { - const relationship = ReferralRelationship.create(100n, null); + const relationship = ReferralRelationship.create(100n, 12345678, null); const saved = await repository.save(relationship); const found = await repository.findByReferralCode(saved.referralCode); expect(found).toBeDefined(); expect(found!.userId).toBe(100n); + expect(found!.accountSequence).toBe(12345678); }); it('should return null for non-existent code', async () => { @@ -100,12 +105,12 @@ describe('ReferralRelationshipRepository (Integration)', () => { describe('findDirectReferrals', () => { it('should find all direct referrals', async () => { // Create referrer - const referrer = ReferralRelationship.create(50n, null); + const referrer = ReferralRelationship.create(50n, 11111111, null); await repository.save(referrer); // Create direct referrals - const ref1 = ReferralRelationship.create(100n, 50n, []); - const ref2 = ReferralRelationship.create(101n, 50n, []); + const ref1 = ReferralRelationship.create(100n, 12345678, 50n, []); + const ref2 = ReferralRelationship.create(101n, 12345679, 50n, []); await repository.save(ref1); await repository.save(ref2); @@ -117,7 +122,7 @@ describe('ReferralRelationshipRepository (Integration)', () => { }); it('should return empty array for user with no referrals', async () => { - const referrer = ReferralRelationship.create(50n, null); + const referrer = ReferralRelationship.create(50n, 11111111, null); await repository.save(referrer); const directReferrals = await repository.findDirectReferrals(50n); @@ -127,7 +132,7 @@ describe('ReferralRelationshipRepository (Integration)', () => { describe('existsByReferralCode', () => { it('should return true for existing code', async () => { - const relationship = ReferralRelationship.create(100n, null); + const relationship = ReferralRelationship.create(100n, 12345678, null); const saved = await repository.save(relationship); const exists = await repository.existsByReferralCode(saved.referralCode); @@ -142,7 +147,7 @@ describe('ReferralRelationshipRepository (Integration)', () => { describe('existsByUserId', () => { it('should return true for existing user', async () => { - const relationship = ReferralRelationship.create(100n, null); + const relationship = ReferralRelationship.create(100n, 12345678, null); await repository.save(relationship); const exists = await repository.existsByUserId(100n); @@ -158,7 +163,7 @@ describe('ReferralRelationshipRepository (Integration)', () => { describe('getReferralChain', () => { it('should return referral chain', async () => { const parentChain = [200n, 300n]; - const relationship = ReferralRelationship.create(100n, 50n, parentChain); + const relationship = ReferralRelationship.create(100n, 12345678, 50n, parentChain); await repository.save(relationship); const chain = await repository.getReferralChain(100n); diff --git a/backend/services/referral-service/test/integration/services/referral.service.integration.spec.ts b/backend/services/referral-service/test/integration/services/referral.service.integration.spec.ts index bf006f26..e462f6c3 100644 --- a/backend/services/referral-service/test/integration/services/referral.service.integration.spec.ts +++ b/backend/services/referral-service/test/integration/services/referral.service.integration.spec.ts @@ -6,7 +6,7 @@ import { ReferralChainService, ReferralRelationship, } from '../../../src/domain'; -import { EventPublisherService, LeaderboardCacheService } from '../../../src/infrastructure'; +import { EventPublisherService } from '../../../src/infrastructure'; import { CreateReferralRelationshipCommand } from '../../../src/application/commands'; import { GetUserReferralInfoQuery, GetDirectReferralsQuery } from '../../../src/application/queries'; @@ -91,10 +91,6 @@ class MockEventPublisher { async publishEvent() {} } -class MockLeaderboardCache { - async getUserRank() { return 1; } - async updateScore() {} -} describe('ReferralService (Integration)', () => { let service: ReferralService; @@ -121,10 +117,6 @@ describe('ReferralService (Integration)', () => { provide: EventPublisherService, useClass: MockEventPublisher, }, - { - provide: LeaderboardCacheService, - useClass: MockLeaderboardCache, - }, ], }).compile(); @@ -133,7 +125,7 @@ describe('ReferralService (Integration)', () => { describe('createReferralRelationship', () => { it('should create referral relationship without referrer', async () => { - const command = new CreateReferralRelationshipCommand(100n, null); + const command = new CreateReferralRelationshipCommand(100n, 12345678, null); const result = await service.createReferralRelationship(command); expect(result).toBeDefined(); @@ -143,11 +135,11 @@ describe('ReferralService (Integration)', () => { it('should create referral relationship with valid referrer code', async () => { // First create a referrer - const referrerCommand = new CreateReferralRelationshipCommand(50n, null); + const referrerCommand = new CreateReferralRelationshipCommand(50n, 11111111, null); const referrerResult = await service.createReferralRelationship(referrerCommand); // Then create with referrer code - const command = new CreateReferralRelationshipCommand(100n, referrerResult.referralCode); + const command = new CreateReferralRelationshipCommand(100n, 12345678, referrerResult.referralCode); const result = await service.createReferralRelationship(command); expect(result).toBeDefined(); @@ -155,14 +147,14 @@ describe('ReferralService (Integration)', () => { }); it('should throw error if user already has referral relationship', async () => { - const command = new CreateReferralRelationshipCommand(100n, null); + const command = new CreateReferralRelationshipCommand(100n, 12345678, null); await service.createReferralRelationship(command); await expect(service.createReferralRelationship(command)).rejects.toThrow('用户已存在推荐关系'); }); it('should throw error for invalid referral code', async () => { - const command = new CreateReferralRelationshipCommand(100n, 'INVALID_CODE'); + const command = new CreateReferralRelationshipCommand(100n, 12345678, 'INVALID_CODE'); await expect(service.createReferralRelationship(command)).rejects.toThrow('推荐码不存在'); }); @@ -171,10 +163,10 @@ describe('ReferralService (Integration)', () => { describe('getUserReferralInfo', () => { it('should return user referral info', async () => { // Create user first - const createCommand = new CreateReferralRelationshipCommand(100n, null); + const createCommand = new CreateReferralRelationshipCommand(100n, 12345678, null); await service.createReferralRelationship(createCommand); - const query = new GetUserReferralInfoQuery(100n); + const query = new GetUserReferralInfoQuery(12345678); const result = await service.getUserReferralInfo(query); expect(result).toBeDefined(); @@ -184,7 +176,7 @@ describe('ReferralService (Integration)', () => { }); it('should throw error for non-existent user', async () => { - const query = new GetUserReferralInfoQuery(999n); + const query = new GetUserReferralInfoQuery(99999999); await expect(service.getUserReferralInfo(query)).rejects.toThrow('用户推荐关系不存在'); }); @@ -193,12 +185,12 @@ describe('ReferralService (Integration)', () => { describe('getDirectReferrals', () => { it('should return direct referrals list', async () => { // Create referrer - const referrerCommand = new CreateReferralRelationshipCommand(50n, null); + const referrerCommand = new CreateReferralRelationshipCommand(50n, 11111111, null); const referrerResult = await service.createReferralRelationship(referrerCommand); // Create direct referrals - await service.createReferralRelationship(new CreateReferralRelationshipCommand(100n, referrerResult.referralCode)); - await service.createReferralRelationship(new CreateReferralRelationshipCommand(101n, referrerResult.referralCode)); + await service.createReferralRelationship(new CreateReferralRelationshipCommand(100n, 12345678, referrerResult.referralCode)); + await service.createReferralRelationship(new CreateReferralRelationshipCommand(101n, 12345679, referrerResult.referralCode)); const query = new GetDirectReferralsQuery(50n); const result = await service.getDirectReferrals(query); @@ -210,7 +202,7 @@ describe('ReferralService (Integration)', () => { describe('validateReferralCode', () => { it('should return true for valid code', async () => { - const createCommand = new CreateReferralRelationshipCommand(100n, null); + const createCommand = new CreateReferralRelationshipCommand(100n, 12345678, null); const { referralCode } = await service.createReferralRelationship(createCommand); const isValid = await service.validateReferralCode(referralCode); diff --git a/backend/services/reward-service/prisma/migrations/20241210000001_add_account_sequence/migration.sql b/backend/services/reward-service/prisma/migrations/20241210000001_add_account_sequence/migration.sql new file mode 100644 index 00000000..648baa9c --- /dev/null +++ b/backend/services/reward-service/prisma/migrations/20241210000001_add_account_sequence/migration.sql @@ -0,0 +1,29 @@ +-- Add account_sequence column to reward_ledger_entries +ALTER TABLE "reward_ledger_entries" ADD COLUMN "account_sequence" BIGINT; + +-- Add indexes for account_sequence on reward_ledger_entries +CREATE INDEX "idx_account_status" ON "reward_ledger_entries"("account_sequence", "reward_status"); +CREATE INDEX "idx_account_created" ON "reward_ledger_entries"("account_sequence", "created_at" DESC); + +-- Add account_sequence column to reward_summaries +ALTER TABLE "reward_summaries" ADD COLUMN "account_sequence" BIGINT; + +-- Add unique constraint and index for account_sequence on reward_summaries +CREATE UNIQUE INDEX "reward_summaries_account_sequence_key" ON "reward_summaries"("account_sequence"); +CREATE INDEX "idx_summary_account" ON "reward_summaries"("account_sequence"); + +-- Add account_sequence column to settlement_records +ALTER TABLE "settlement_records" ADD COLUMN "account_sequence" BIGINT; + +-- Add index for account_sequence on settlement_records +CREATE INDEX "idx_settlement_account" ON "settlement_records"("account_sequence"); + +-- Backfill: set account_sequence = user_id for existing records +UPDATE "reward_ledger_entries" SET "account_sequence" = "user_id" WHERE "account_sequence" IS NULL; +UPDATE "reward_summaries" SET "account_sequence" = "user_id" WHERE "account_sequence" IS NULL; +UPDATE "settlement_records" SET "account_sequence" = "user_id" WHERE "account_sequence" IS NULL; + +-- Make account_sequence NOT NULL after backfill +ALTER TABLE "reward_ledger_entries" ALTER COLUMN "account_sequence" SET NOT NULL; +ALTER TABLE "reward_summaries" ALTER COLUMN "account_sequence" SET NOT NULL; +ALTER TABLE "settlement_records" ALTER COLUMN "account_sequence" SET NOT NULL; diff --git a/backend/services/reward-service/src/application/services/reward-application.service.ts b/backend/services/reward-service/src/application/services/reward-application.service.ts index 4bacd6ee..6e67a143 100644 --- a/backend/services/reward-service/src/application/services/reward-application.service.ts +++ b/backend/services/reward-service/src/application/services/reward-application.service.ts @@ -53,7 +53,8 @@ export class RewardApplicationService { const userIds = [...new Set(rewards.map(r => r.userId))]; for (const userId of userIds) { const userRewards = rewards.filter(r => r.userId === userId); - const summary = await this.rewardSummaryRepository.getOrCreate(userId); + const accountSequence = userRewards[0].accountSequence; + const summary = await this.rewardSummaryRepository.getOrCreate(userId, accountSequence); for (const reward of userRewards) { if (reward.isPending) { @@ -89,7 +90,8 @@ export class RewardApplicationService { this.logger.log(`Claiming pending rewards for user ${userId}`); const pendingRewards = await this.rewardLedgerEntryRepository.findPendingByUserId(userId); - const summary = await this.rewardSummaryRepository.getOrCreate(userId); + // 使用 userId 作为 accountSequence (系统账户场景) + const summary = await this.rewardSummaryRepository.getOrCreate(userId, userId); let claimedCount = 0; let totalUsdtClaimed = 0; @@ -222,7 +224,9 @@ export class RewardApplicationService { // 更新每个用户的汇总数据 for (const [userId, rewards] of userRewardsMap) { - const summary = await this.rewardSummaryRepository.getOrCreate(BigInt(userId)); + const userIdBigInt = BigInt(userId); + // 使用 userId 作为 accountSequence (过期任务中无法获取真实 accountSequence) + const summary = await this.rewardSummaryRepository.getOrCreate(userIdBigInt, userIdBigInt); for (const reward of rewards) { await this.rewardLedgerEntryRepository.save(reward); @@ -236,7 +240,7 @@ export class RewardApplicationService { // 将过期奖励转入总部社区 if (expiredRewards.length > 0) { - const hqSummary = await this.rewardSummaryRepository.getOrCreate(HEADQUARTERS_COMMUNITY_USER_ID); + const hqSummary = await this.rewardSummaryRepository.getOrCreate(HEADQUARTERS_COMMUNITY_USER_ID, HEADQUARTERS_COMMUNITY_USER_ID); const totalHqUsdt = expiredRewards.reduce((sum, r) => sum + r.usdtAmount.amount, 0); const totalHqHashpower = expiredRewards.reduce((sum, r) => sum + r.hashpowerAmount.value, 0); hqSummary.addSettleable(Money.USDT(totalHqUsdt), Hashpower.create(totalHqHashpower)); diff --git a/backend/services/reward-service/src/domain/aggregates/reward-ledger-entry/reward-ledger-entry.spec.ts b/backend/services/reward-service/src/domain/aggregates/reward-ledger-entry/reward-ledger-entry.spec.ts index 137e0766..ac8dbb46 100644 --- a/backend/services/reward-service/src/domain/aggregates/reward-ledger-entry/reward-ledger-entry.spec.ts +++ b/backend/services/reward-service/src/domain/aggregates/reward-ledger-entry/reward-ledger-entry.spec.ts @@ -7,12 +7,13 @@ import { Hashpower } from '../../value-objects/hashpower.vo'; describe('RewardLedgerEntry', () => { const createRewardSource = () => - RewardSource.create(RightType.SHARE_RIGHT, BigInt(1), BigInt(2)); + RewardSource.create(RightType.SHARE_RIGHT, 'ORDER001', BigInt(2)); describe('createPending', () => { it('should create a pending reward with 24h expiration', () => { const entry = RewardLedgerEntry.createPending({ userId: BigInt(100), + accountSequence: BigInt(100), rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -31,6 +32,7 @@ describe('RewardLedgerEntry', () => { const before = Date.now(); const entry = RewardLedgerEntry.createPending({ userId: BigInt(100), + accountSequence: BigInt(100), rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -49,6 +51,7 @@ describe('RewardLedgerEntry', () => { it('should create a settleable reward without expiration', () => { const entry = RewardLedgerEntry.createSettleable({ userId: BigInt(100), + accountSequence: BigInt(100), rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.create(5), @@ -66,6 +69,7 @@ describe('RewardLedgerEntry', () => { it('should transition pending to settleable', () => { const entry = RewardLedgerEntry.createPending({ userId: BigInt(100), + accountSequence: BigInt(100), rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -84,6 +88,7 @@ describe('RewardLedgerEntry', () => { it('should throw error when not pending', () => { const entry = RewardLedgerEntry.createSettleable({ userId: BigInt(100), + accountSequence: BigInt(100), rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -97,6 +102,7 @@ describe('RewardLedgerEntry', () => { it('should transition pending to expired', () => { const entry = RewardLedgerEntry.createPending({ userId: BigInt(100), + accountSequence: BigInt(100), rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -114,6 +120,7 @@ describe('RewardLedgerEntry', () => { it('should throw error when not pending', () => { const entry = RewardLedgerEntry.createSettleable({ userId: BigInt(100), + accountSequence: BigInt(100), rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -127,6 +134,7 @@ describe('RewardLedgerEntry', () => { it('should transition settleable to settled', () => { const entry = RewardLedgerEntry.createSettleable({ userId: BigInt(100), + accountSequence: BigInt(100), rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -144,6 +152,7 @@ describe('RewardLedgerEntry', () => { it('should throw error when not settleable', () => { const entry = RewardLedgerEntry.createPending({ userId: BigInt(100), + accountSequence: BigInt(100), rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -157,6 +166,7 @@ describe('RewardLedgerEntry', () => { it('should return remaining time for pending rewards', () => { const entry = RewardLedgerEntry.createPending({ userId: BigInt(100), + accountSequence: BigInt(100), rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -172,6 +182,7 @@ describe('RewardLedgerEntry', () => { it('should return 0 for settleable rewards', () => { const entry = RewardLedgerEntry.createSettleable({ userId: BigInt(100), + accountSequence: BigInt(100), rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -186,6 +197,7 @@ describe('RewardLedgerEntry', () => { const data = { id: BigInt(1), userId: BigInt(100), + accountSequence: BigInt(100), rewardSource: createRewardSource(), usdtAmount: 500, hashpowerAmount: 5, diff --git a/backend/services/reward-service/src/domain/aggregates/reward-summary/reward-summary.spec.ts b/backend/services/reward-service/src/domain/aggregates/reward-summary/reward-summary.spec.ts index 03db5a40..c6cd6719 100644 --- a/backend/services/reward-service/src/domain/aggregates/reward-summary/reward-summary.spec.ts +++ b/backend/services/reward-service/src/domain/aggregates/reward-summary/reward-summary.spec.ts @@ -5,9 +5,10 @@ import { Hashpower } from '../../value-objects/hashpower.vo'; describe('RewardSummary', () => { describe('create', () => { it('should create a new summary with zero values', () => { - const summary = RewardSummary.create(BigInt(100)); + const summary = RewardSummary.create(BigInt(100), BigInt(100)); expect(summary.userId).toBe(BigInt(100)); + expect(summary.accountSequence).toBe(BigInt(100)); expect(summary.pendingUsdt.amount).toBe(0); expect(summary.settleableUsdt.amount).toBe(0); expect(summary.settledTotalUsdt.amount).toBe(0); @@ -17,7 +18,7 @@ describe('RewardSummary', () => { describe('addPending', () => { it('should add pending rewards and update expire time', () => { - const summary = RewardSummary.create(BigInt(100)); + const summary = RewardSummary.create(BigInt(100), BigInt(100)); const expireAt = new Date(Date.now() + 24 * 60 * 60 * 1000); summary.addPending(Money.USDT(500), Hashpower.create(5), expireAt); @@ -28,7 +29,7 @@ describe('RewardSummary', () => { }); it('should keep earliest expire time', () => { - const summary = RewardSummary.create(BigInt(100)); + const summary = RewardSummary.create(BigInt(100), BigInt(100)); const earlyExpire = new Date(Date.now() + 12 * 60 * 60 * 1000); const lateExpire = new Date(Date.now() + 24 * 60 * 60 * 1000); @@ -42,7 +43,7 @@ describe('RewardSummary', () => { describe('movePendingToSettleable', () => { it('should move amounts from pending to settleable', () => { - const summary = RewardSummary.create(BigInt(100)); + const summary = RewardSummary.create(BigInt(100), BigInt(100)); const expireAt = new Date(Date.now() + 24 * 60 * 60 * 1000); summary.addPending(Money.USDT(500), Hashpower.create(5), expireAt); @@ -56,7 +57,7 @@ describe('RewardSummary', () => { }); it('should partially move pending amounts', () => { - const summary = RewardSummary.create(BigInt(100)); + const summary = RewardSummary.create(BigInt(100), BigInt(100)); const expireAt = new Date(Date.now() + 24 * 60 * 60 * 1000); summary.addPending(Money.USDT(500), Hashpower.create(5), expireAt); @@ -71,7 +72,7 @@ describe('RewardSummary', () => { describe('movePendingToExpired', () => { it('should move amounts from pending to expired', () => { - const summary = RewardSummary.create(BigInt(100)); + const summary = RewardSummary.create(BigInt(100), BigInt(100)); const expireAt = new Date(Date.now() + 24 * 60 * 60 * 1000); summary.addPending(Money.USDT(500), Hashpower.create(5), expireAt); @@ -85,7 +86,7 @@ describe('RewardSummary', () => { describe('addSettleable', () => { it('should add directly to settleable', () => { - const summary = RewardSummary.create(BigInt(100)); + const summary = RewardSummary.create(BigInt(100), BigInt(100)); summary.addSettleable(Money.USDT(1000), Hashpower.create(10)); @@ -96,7 +97,7 @@ describe('RewardSummary', () => { describe('settle', () => { it('should move settleable to settled total', () => { - const summary = RewardSummary.create(BigInt(100)); + const summary = RewardSummary.create(BigInt(100), BigInt(100)); summary.addSettleable(Money.USDT(1000), Hashpower.create(10)); summary.settle(Money.USDT(1000), Hashpower.create(10)); @@ -107,7 +108,7 @@ describe('RewardSummary', () => { }); it('should accumulate settled totals', () => { - const summary = RewardSummary.create(BigInt(100)); + const summary = RewardSummary.create(BigInt(100), BigInt(100)); summary.addSettleable(Money.USDT(1000), Hashpower.create(10)); summary.settle(Money.USDT(500), Hashpower.create(5)); @@ -124,6 +125,7 @@ describe('RewardSummary', () => { const data = { id: BigInt(1), userId: BigInt(100), + accountSequence: BigInt(100), pendingUsdt: 500, pendingHashpower: 5, pendingExpireAt: new Date(), diff --git a/backend/services/reward-service/src/domain/repositories/reward-summary.repository.interface.ts b/backend/services/reward-service/src/domain/repositories/reward-summary.repository.interface.ts index 36ac75ae..810d1a29 100644 --- a/backend/services/reward-service/src/domain/repositories/reward-summary.repository.interface.ts +++ b/backend/services/reward-service/src/domain/repositories/reward-summary.repository.interface.ts @@ -4,7 +4,7 @@ export interface IRewardSummaryRepository { save(summary: RewardSummary): Promise; findByUserId(userId: bigint): Promise; findByAccountSequence(accountSequence: bigint): Promise; - getOrCreate(userId: bigint): Promise; + getOrCreate(userId: bigint, accountSequence: bigint): Promise; getOrCreateByAccountSequence(accountSequence: bigint): Promise; findByUserIds(userIds: bigint[]): Promise>; findTopSettleableUsers(limit: number): Promise; diff --git a/backend/services/reward-service/src/domain/services/reward-calculation.service.ts b/backend/services/reward-service/src/domain/services/reward-calculation.service.ts index c501d929..829de27e 100644 --- a/backend/services/reward-service/src/domain/services/reward-calculation.service.ts +++ b/backend/services/reward-service/src/domain/services/reward-calculation.service.ts @@ -179,6 +179,7 @@ export class RewardCalculationService { return RewardLedgerEntry.createSettleable({ userId: COST_FEE_ACCOUNT_ID, + accountSequence: COST_FEE_ACCOUNT_ID, rewardSource, usdtAmount, hashpowerAmount: hashpower, @@ -207,6 +208,7 @@ export class RewardCalculationService { return RewardLedgerEntry.createSettleable({ userId: OPERATION_FEE_ACCOUNT_ID, + accountSequence: OPERATION_FEE_ACCOUNT_ID, rewardSource, usdtAmount, hashpowerAmount: hashpower, @@ -235,6 +237,7 @@ export class RewardCalculationService { return RewardLedgerEntry.createSettleable({ userId: HEADQUARTERS_COMMUNITY_USER_ID, + accountSequence: HEADQUARTERS_COMMUNITY_USER_ID, rewardSource, usdtAmount, hashpowerAmount: hashpower, @@ -263,6 +266,7 @@ export class RewardCalculationService { return RewardLedgerEntry.createSettleable({ userId: RWAD_POOL_ACCOUNT_ID, + accountSequence: RWAD_POOL_ACCOUNT_ID, rewardSource, usdtAmount, hashpowerAmount: hashpower, @@ -302,6 +306,7 @@ export class RewardCalculationService { // 推荐人已认种,直接可结算 return [RewardLedgerEntry.createSettleable({ userId: directReferrer.userId, + accountSequence: directReferrer.userId, rewardSource, usdtAmount, hashpowerAmount: hashpower, @@ -311,6 +316,7 @@ export class RewardCalculationService { // 推荐人未认种,进入待领取(24h倒计时) return [RewardLedgerEntry.createPending({ userId: directReferrer.userId, + accountSequence: directReferrer.userId, rewardSource, usdtAmount, hashpowerAmount: hashpower, @@ -321,6 +327,7 @@ export class RewardCalculationService { // 无推荐人,进总部社区 return [RewardLedgerEntry.createSettleable({ userId: HEADQUARTERS_COMMUNITY_USER_ID, + accountSequence: HEADQUARTERS_COMMUNITY_USER_ID, rewardSource, usdtAmount, hashpowerAmount: hashpower, @@ -357,6 +364,7 @@ export class RewardCalculationService { if (nearestProvince) { return RewardLedgerEntry.createSettleable({ userId: nearestProvince, + accountSequence: nearestProvince, rewardSource, usdtAmount, hashpowerAmount: hashpower, @@ -365,6 +373,7 @@ export class RewardCalculationService { } else { return RewardLedgerEntry.createSettleable({ userId: HEADQUARTERS_COMMUNITY_USER_ID, + accountSequence: HEADQUARTERS_COMMUNITY_USER_ID, rewardSource, usdtAmount, hashpowerAmount: hashpower, @@ -397,6 +406,7 @@ export class RewardCalculationService { return RewardLedgerEntry.createSettleable({ userId: systemProvinceAccountId, + accountSequence: systemProvinceAccountId, rewardSource, usdtAmount, hashpowerAmount: hashpower, @@ -432,6 +442,7 @@ export class RewardCalculationService { if (nearestCity) { return RewardLedgerEntry.createSettleable({ userId: nearestCity, + accountSequence: nearestCity, rewardSource, usdtAmount, hashpowerAmount: hashpower, @@ -440,6 +451,7 @@ export class RewardCalculationService { } else { return RewardLedgerEntry.createSettleable({ userId: HEADQUARTERS_COMMUNITY_USER_ID, + accountSequence: HEADQUARTERS_COMMUNITY_USER_ID, rewardSource, usdtAmount, hashpowerAmount: hashpower, @@ -472,6 +484,7 @@ export class RewardCalculationService { return RewardLedgerEntry.createSettleable({ userId: systemCityAccountId, + accountSequence: systemCityAccountId, rewardSource, usdtAmount, hashpowerAmount: hashpower, @@ -503,6 +516,7 @@ export class RewardCalculationService { if (nearestCommunity) { return RewardLedgerEntry.createSettleable({ userId: nearestCommunity, + accountSequence: nearestCommunity, rewardSource, usdtAmount, hashpowerAmount: hashpower, @@ -511,6 +525,7 @@ export class RewardCalculationService { } else { return RewardLedgerEntry.createSettleable({ userId: HEADQUARTERS_COMMUNITY_USER_ID, + accountSequence: HEADQUARTERS_COMMUNITY_USER_ID, rewardSource, usdtAmount, hashpowerAmount: hashpower, diff --git a/backend/services/reward-service/src/infrastructure/persistence/repositories/reward-summary.repository.impl.ts b/backend/services/reward-service/src/infrastructure/persistence/repositories/reward-summary.repository.impl.ts index c9167897..236b1cdb 100644 --- a/backend/services/reward-service/src/infrastructure/persistence/repositories/reward-summary.repository.impl.ts +++ b/backend/services/reward-service/src/infrastructure/persistence/repositories/reward-summary.repository.impl.ts @@ -53,13 +53,13 @@ export class RewardSummaryRepositoryImpl implements IRewardSummaryRepository { return raw ? RewardSummaryMapper.toDomain(raw) : null; } - async getOrCreate(userId: bigint): Promise { + async getOrCreate(userId: bigint, accountSequence: bigint): Promise { const existing = await this.findByUserId(userId); if (existing) { return existing; } - const newSummary = RewardSummary.create(userId); + const newSummary = RewardSummary.create(userId, accountSequence); await this.save(newSummary); return newSummary; } diff --git a/backend/services/reward-service/test/integration/reward-application.service.spec.ts b/backend/services/reward-service/test/integration/reward-application.service.spec.ts index a4e1238a..edafafac 100644 --- a/backend/services/reward-service/test/integration/reward-application.service.spec.ts +++ b/backend/services/reward-service/test/integration/reward-application.service.spec.ts @@ -87,7 +87,7 @@ describe('RewardApplicationService (Integration)', () => { describe('distributeRewards', () => { it('should distribute rewards and update summaries', async () => { const params = { - sourceOrderId: BigInt(1), + sourceOrderNo: 'ORDER001', sourceUserId: BigInt(100), treeCount: 10, provinceCode: '440000', @@ -96,13 +96,14 @@ describe('RewardApplicationService (Integration)', () => { const mockReward = RewardLedgerEntry.createPending({ userId: BigInt(200), - rewardSource: RewardSource.create(RightType.SHARE_RIGHT, BigInt(1), BigInt(100)), + accountSequence: BigInt(100), + rewardSource: RewardSource.create(RightType.SHARE_RIGHT, 'ORDER001', BigInt(100)), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), memo: 'Test', }); - const mockSummary = RewardSummary.create(BigInt(200)); + const mockSummary = RewardSummary.create(BigInt(200), BigInt(100)); mockCalculationService.calculateRewards.mockResolvedValue([mockReward]); mockSummaryRepository.getOrCreate.mockResolvedValue(mockSummary); @@ -122,13 +123,14 @@ describe('RewardApplicationService (Integration)', () => { const pendingReward = RewardLedgerEntry.createPending({ userId, - rewardSource: RewardSource.create(RightType.SHARE_RIGHT, BigInt(1), BigInt(50)), + accountSequence: BigInt(100), + rewardSource: RewardSource.create(RightType.SHARE_RIGHT, 'ORDER001', BigInt(50)), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), memo: 'Test reward', }); - const mockSummary = RewardSummary.create(userId); + const mockSummary = RewardSummary.create(userId, BigInt(100)); mockSummary.addPending( Money.USDT(500), Hashpower.zero(), @@ -152,7 +154,8 @@ describe('RewardApplicationService (Integration)', () => { const expiredReward = RewardLedgerEntry.reconstitute({ id: BigInt(1), userId, - rewardSource: RewardSource.create(RightType.SHARE_RIGHT, BigInt(1), BigInt(50)), + accountSequence: BigInt(100), + rewardSource: RewardSource.create(RightType.SHARE_RIGHT, 'ORDER001', BigInt(50)), usdtAmount: 500, hashpowerAmount: 0, rewardStatus: RewardStatus.PENDING, @@ -164,7 +167,7 @@ describe('RewardApplicationService (Integration)', () => { memo: 'Expired reward', }); - const mockSummary = RewardSummary.create(userId); + const mockSummary = RewardSummary.create(userId, BigInt(100)); mockLedgerRepository.findPendingByUserId.mockResolvedValue([expiredReward]); mockSummaryRepository.getOrCreate.mockResolvedValue(mockSummary); @@ -178,12 +181,13 @@ describe('RewardApplicationService (Integration)', () => { describe('settleRewards', () => { it('should settle rewards and call wallet service', async () => { - const userId = BigInt(100); + const accountSequence = BigInt(100); const settleableReward = RewardLedgerEntry.reconstitute({ id: BigInt(1), - userId, - rewardSource: RewardSource.create(RightType.SHARE_RIGHT, BigInt(1), BigInt(50)), + userId: BigInt(100), + accountSequence: BigInt(100), + rewardSource: RewardSource.create(RightType.SHARE_RIGHT, 'ORDER001', BigInt(50)), usdtAmount: 500, hashpowerAmount: 0, rewardStatus: RewardStatus.SETTLEABLE, @@ -195,7 +199,7 @@ describe('RewardApplicationService (Integration)', () => { memo: 'Test', }); - const mockSummary = RewardSummary.create(userId); + const mockSummary = RewardSummary.create(BigInt(100), BigInt(100)); mockSummary.addSettleable(Money.USDT(500), Hashpower.zero()); mockLedgerRepository.findSettleableByUserId.mockResolvedValue([settleableReward]); @@ -207,7 +211,7 @@ describe('RewardApplicationService (Integration)', () => { }); const result = await service.settleRewards({ - userId, + accountSequence, settleCurrency: 'BNB', }); @@ -216,19 +220,19 @@ describe('RewardApplicationService (Integration)', () => { expect(result.receivedAmount).toBe(0.25); expect(result.settleCurrency).toBe('BNB'); expect(mockWalletService.executeSwap).toHaveBeenCalledWith({ - userId, + userId: BigInt(100), usdtAmount: 500, targetCurrency: 'BNB', }); }); it('should return error when no settleable rewards', async () => { - const userId = BigInt(100); + const accountSequence = BigInt(100); mockLedgerRepository.findSettleableByUserId.mockResolvedValue([]); const result = await service.settleRewards({ - userId, + accountSequence, settleCurrency: 'BNB', }); @@ -237,12 +241,13 @@ describe('RewardApplicationService (Integration)', () => { }); it('should handle wallet service failure', async () => { - const userId = BigInt(100); + const accountSequence = BigInt(100); const settleableReward = RewardLedgerEntry.reconstitute({ id: BigInt(1), - userId, - rewardSource: RewardSource.create(RightType.SHARE_RIGHT, BigInt(1), BigInt(50)), + userId: BigInt(100), + accountSequence: BigInt(100), + rewardSource: RewardSource.create(RightType.SHARE_RIGHT, 'ORDER001', BigInt(50)), usdtAmount: 500, hashpowerAmount: 0, rewardStatus: RewardStatus.SETTLEABLE, @@ -254,7 +259,7 @@ describe('RewardApplicationService (Integration)', () => { memo: 'Test', }); - const mockSummary = RewardSummary.create(userId); + const mockSummary = RewardSummary.create(BigInt(100), BigInt(100)); mockSummary.addSettleable(Money.USDT(500), Hashpower.zero()); mockLedgerRepository.findSettleableByUserId.mockResolvedValue([settleableReward]); @@ -265,7 +270,7 @@ describe('RewardApplicationService (Integration)', () => { }); const result = await service.settleRewards({ - userId, + accountSequence, settleCurrency: 'BNB', }); @@ -277,7 +282,7 @@ describe('RewardApplicationService (Integration)', () => { describe('getRewardSummary', () => { it('should return reward summary for user', async () => { const userId = BigInt(100); - const mockSummary = RewardSummary.create(userId); + const mockSummary = RewardSummary.create(userId, BigInt(100)); mockSummaryRepository.findByUserId.mockResolvedValue(mockSummary); @@ -307,7 +312,8 @@ describe('RewardApplicationService (Integration)', () => { const mockReward = RewardLedgerEntry.reconstitute({ id: BigInt(1), userId, - rewardSource: RewardSource.create(RightType.SHARE_RIGHT, BigInt(1), BigInt(50)), + accountSequence: BigInt(100), + rewardSource: RewardSource.create(RightType.SHARE_RIGHT, 'ORDER001', BigInt(50)), usdtAmount: 500, hashpowerAmount: 0, rewardStatus: RewardStatus.PENDING, @@ -336,7 +342,8 @@ describe('RewardApplicationService (Integration)', () => { const pendingReward = RewardLedgerEntry.createPending({ userId, - rewardSource: RewardSource.create(RightType.SHARE_RIGHT, BigInt(1), BigInt(50)), + accountSequence: BigInt(100), + rewardSource: RewardSource.create(RightType.SHARE_RIGHT, 'ORDER001', BigInt(50)), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), memo: 'Test', diff --git a/backend/services/reward-service/test/integration/reward-calculation.service.spec.ts b/backend/services/reward-service/test/integration/reward-calculation.service.spec.ts index 14e02e2b..3270b69f 100644 --- a/backend/services/reward-service/test/integration/reward-calculation.service.spec.ts +++ b/backend/services/reward-service/test/integration/reward-calculation.service.spec.ts @@ -41,7 +41,7 @@ describe('RewardCalculationService (Integration)', () => { describe('calculateRewards', () => { const baseParams = { - sourceOrderId: BigInt(1), + sourceOrderNo: 'ORDER001', sourceUserId: BigInt(100), treeCount: 10, provinceCode: '440000',