fix: add accountSequence to all services and fix compilation errors

- reward-service: add accountSequence to aggregates, services, tests
- authorization-service: fix UserId/AdminUserId to accept accountSequence, add findByAccountSequence to repositories
- referral-service: fix test files for accountSequence changes
- Add migration files for reward-service and authorization-service

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-10 14:14:06 -08:00
parent 034fb53674
commit 300fe211c8
23 changed files with 260 additions and 123 deletions

View File

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

View File

@ -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: '正式市公司授权成功' }

View File

@ -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: '城市代码不能为空' })

View File

@ -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: '省份代码不能为空' })

View File

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

View File

@ -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<TeamStatistics | null>
findByAccountSequence(accountSequence: bigint): Promise<TeamStatistics | null>
}
export class AssessmentCalculatorService {

View File

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

View File

@ -76,7 +76,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
): Promise<AuthorizationRole | null> {
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<AuthorizationRole | null> {
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<AuthorizationRole[]> {
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<AuthorizationRole[]> {
const records = await this.prisma.authorizationRole.findMany({
where: {
userId: userId.value,
userId: BigInt(userId.value),
status: AuthorizationStatus.PENDING,
},
})

View File

@ -148,7 +148,7 @@ export class MonthlyAssessmentRepositoryImpl implements IMonthlyAssessmentReposi
async findByUserAndMonth(userId: UserId, month: Month): Promise<MonthlyAssessment[]> {
const records = await this.prisma.monthlyAssessment.findMany({
where: {
userId: userId.value,
userId: BigInt(userId.value),
assessmentMonth: month.value,
},
})

View File

@ -2,6 +2,7 @@ import { createParamDecorator, ExecutionContext } from '@nestjs/common'
export interface CurrentUserData {
userId: string
accountSequence?: number
walletAddress?: string
roles?: string[]
}

View File

@ -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<IMonthlyAssessmentRepository> = {
@ -52,6 +54,7 @@ describe('Domain Services Integration Tests', () => {
// Mock team statistics repository
const mockTeamStatisticsRepository: jest.Mocked<ITeamStatisticsRepository> = {
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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ export interface IRewardSummaryRepository {
save(summary: RewardSummary): Promise<void>;
findByUserId(userId: bigint): Promise<RewardSummary | null>;
findByAccountSequence(accountSequence: bigint): Promise<RewardSummary | null>;
getOrCreate(userId: bigint): Promise<RewardSummary>;
getOrCreate(userId: bigint, accountSequence: bigint): Promise<RewardSummary>;
getOrCreateByAccountSequence(accountSequence: bigint): Promise<RewardSummary>;
findByUserIds(userIds: bigint[]): Promise<Map<string, RewardSummary>>;
findTopSettleableUsers(limit: number): Promise<RewardSummary[]>;

View File

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

View File

@ -53,13 +53,13 @@ export class RewardSummaryRepositoryImpl implements IRewardSummaryRepository {
return raw ? RewardSummaryMapper.toDomain(raw) : null;
}
async getOrCreate(userId: bigint): Promise<RewardSummary> {
async getOrCreate(userId: bigint, accountSequence: bigint): Promise<RewardSummary> {
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;
}

View File

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

View File

@ -41,7 +41,7 @@ describe('RewardCalculationService (Integration)', () => {
describe('calculateRewards', () => {
const baseParams = {
sourceOrderId: BigInt(1),
sourceOrderNo: 'ORDER001',
sourceUserId: BigInt(100),
treeCount: 10,
provinceCode: '440000',