fix(referral-service): 从领域层修复 usedReferralCode 存储错误

问题:
- 创建推荐关系时,used_referral_code 字段错误地存储了用户自己的推荐码
- 而不是用户使用的推荐人的推荐码

根本修复(从领域层):
1. ReferralRelationshipProps: 添加 usedReferralCode 字段
2. ReferralRelationship.create(): 新增 referrerReferralCode 参数
3. ReferralRelationship: 添加 _usedReferralCode 私有属性和 getter
4. toPersistence(): 输出 usedReferralCode
5. reconstitute(): 正确重建 usedReferralCode

调用链路修复:
- referral.service.ts: 在查询推荐人时获取其 referralCode,
  并在调用 ReferralRelationship.create() 时传入
- repository: 直接使用 data.usedReferralCode 而不是额外查询

测试更新:
- 更新单元测试和集成测试以支持新的 create() 签名
- 新增 usedReferralCode 相关测试用例

🤖 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-12 19:49:11 -08:00
parent e557b00c24
commit ad82e3ee44
6 changed files with 100 additions and 42 deletions

View File

@ -43,6 +43,7 @@ export class ReferralService {
}
let referrerId: bigint | null = null;
let referrerReferralCode: string | null = null; // 推荐人的推荐码
let parentChain: bigint[] = [];
// 优先通过推荐码查找推荐人
@ -52,6 +53,7 @@ export class ReferralService {
throw new NotFoundException('推荐码不存在');
}
referrerId = referrer.userId;
referrerReferralCode = referrer.referralCode; // 获取推荐人的推荐码
parentChain = referrer.referralChain;
// 验证推荐链
@ -64,12 +66,14 @@ export class ReferralService {
const referrer = await this.referralRepo.findByAccountSequence(command.inviterAccountSequence);
if (referrer) {
referrerId = referrer.userId;
referrerReferralCode = referrer.referralCode; // 获取推荐人的推荐码
parentChain = referrer.referralChain;
// 验证推荐链
if (!this.referralChainService.validateChain(parentChain, command.userId)) {
this.logger.warn(`Invalid referral chain for user ${command.userId}, ignoring inviter`);
referrerId = null;
referrerReferralCode = null;
parentChain = [];
}
} else {
@ -77,11 +81,12 @@ export class ReferralService {
}
}
// 创建推荐关系
// 创建推荐关系(传入推荐人的推荐码)
const relationship = ReferralRelationship.create(
command.userId,
command.accountSequence,
referrerId,
referrerReferralCode, // 推荐人的推荐码
parentChain,
);
const saved = await this.referralRepo.save(relationship);

View File

@ -6,7 +6,8 @@ export interface ReferralRelationshipProps {
userId: bigint;
accountSequence: string; // 格式: D + YYMMDD + 5位序号
referrerId: bigint | null;
referralCode: string;
referralCode: string; // 用户自己的推荐码
usedReferralCode: string | null; // 用户使用的推荐码(推荐人的推荐码)
referralChain: bigint[];
createdAt: Date;
updatedAt: Date;
@ -29,6 +30,7 @@ export class ReferralRelationship {
private readonly _accountSequence: string, // 格式: D + YYMMDD + 5位序号
private readonly _referrerId: UserId | null,
private readonly _referralCode: ReferralCode,
private readonly _usedReferralCode: ReferralCode | null, // 用户使用的推荐码(推荐人的推荐码)
private readonly _referralChain: ReferralChain,
private readonly _createdAt: Date,
private _updatedAt: Date,
@ -50,6 +52,9 @@ export class ReferralRelationship {
get referralCode(): string {
return this._referralCode.value;
}
get usedReferralCode(): string | null {
return this._usedReferralCode?.value ?? null;
}
get referralChain(): bigint[] {
return this._referralChain.toArray();
}
@ -65,16 +70,23 @@ export class ReferralRelationship {
/**
* ()
* @param userId ID
* @param accountSequence
* @param referrerId ID
* @param referrerReferralCode
* @param parentReferralChain
*/
static create(
userId: bigint,
accountSequence: string, // 格式: D + YYMMDD + 5位序号
referrerId: bigint | null,
referrerReferralCode: string | null, // 推荐人的推荐码
parentReferralChain: bigint[] = [],
): ReferralRelationship {
const userIdVo = UserId.create(userId);
const referrerIdVo = referrerId ? UserId.create(referrerId) : null;
const referralCode = ReferralCode.generate(userId);
const usedReferralCode = referrerReferralCode ? ReferralCode.create(referrerReferralCode) : null;
const referralChain = ReferralChain.create(referrerId, parentReferralChain);
const now = new Date();
@ -84,6 +96,7 @@ export class ReferralRelationship {
accountSequence,
referrerIdVo,
referralCode,
usedReferralCode,
referralChain,
now,
now,
@ -112,6 +125,7 @@ export class ReferralRelationship {
props.accountSequence,
props.referrerId ? UserId.create(props.referrerId) : null,
ReferralCode.create(props.referralCode),
props.usedReferralCode ? ReferralCode.create(props.usedReferralCode) : null,
ReferralChain.fromArray(props.referralChain),
props.createdAt,
props.updatedAt,
@ -163,6 +177,7 @@ export class ReferralRelationship {
accountSequence: this._accountSequence,
referrerId: this._referrerId?.value ?? null,
referralCode: this._referralCode.value,
usedReferralCode: this._usedReferralCode?.value ?? null,
referralChain: this._referralChain.toArray(),
createdAt: this._createdAt,
updatedAt: this._updatedAt,

View File

@ -27,7 +27,7 @@ export class ReferralRelationshipRepository implements IReferralRelationshipRepo
accountSequence: data.accountSequence,
referrerId: data.referrerId,
myReferralCode: data.referralCode,
usedReferralCode: data.referrerId ? data.referralCode : null,
usedReferralCode: data.usedReferralCode, // 从领域模型获取推荐人的推荐码
ancestorPath: data.referralChain,
depth: data.referralChain.length,
rootUserId: data.referralChain.length > 0 ? data.referralChain[data.referralChain.length - 1] : null,
@ -103,6 +103,7 @@ export class ReferralRelationshipRepository implements IReferralRelationshipRepo
accountSequence: string; // 格式: D + YYMMDD + 5位序号
referrerId: bigint | null;
myReferralCode: string;
usedReferralCode: string | null; // 用户使用的推荐码(推荐人的推荐码)
ancestorPath: bigint[];
createdAt: Date;
updatedAt: Date;
@ -113,6 +114,7 @@ export class ReferralRelationshipRepository implements IReferralRelationshipRepo
accountSequence: record.accountSequence,
referrerId: record.referrerId,
referralCode: record.myReferralCode,
usedReferralCode: record.usedReferralCode,
referralChain: record.ancestorPath,
createdAt: record.createdAt,
updatedAt: record.updatedAt,

View File

@ -4,27 +4,29 @@ import { ReferralRelationshipCreatedEvent } from '../../../src/domain/events';
describe('ReferralRelationship Aggregate', () => {
describe('create', () => {
it('should create referral relationship without referrer', () => {
const relationship = ReferralRelationship.create(100n, 12345678, null);
const relationship = ReferralRelationship.create(100n, 'D2512120001', null, null);
expect(relationship.userId).toBe(100n);
expect(relationship.accountSequence).toBe(12345678);
expect(relationship.accountSequence).toBe('D2512120001');
expect(relationship.referrerId).toBeNull();
expect(relationship.usedReferralCode).toBeNull();
expect(relationship.referralCode).toMatch(/^RWA/);
expect(relationship.referralChain).toEqual([]);
});
it('should create referral relationship with referrer', () => {
const parentChain = [200n, 300n];
const relationship = ReferralRelationship.create(100n, 12345678, 50n, parentChain);
const relationship = ReferralRelationship.create(100n, 'D2512120001', 50n, 'RWAREF12345', parentChain);
expect(relationship.userId).toBe(100n);
expect(relationship.accountSequence).toBe(12345678);
expect(relationship.accountSequence).toBe('D2512120001');
expect(relationship.referrerId).toBe(50n);
expect(relationship.usedReferralCode).toBe('RWAREF12345');
expect(relationship.referralChain).toEqual([50n, 200n, 300n]);
});
it('should emit ReferralRelationshipCreatedEvent', () => {
const relationship = ReferralRelationship.create(100n, 12345678, 50n);
const relationship = ReferralRelationship.create(100n, 'D2512120001', 50n, 'RWAREF12345');
expect(relationship.domainEvents.length).toBe(1);
expect(relationship.domainEvents[0]).toBeInstanceOf(ReferralRelationshipCreatedEvent);
@ -40,9 +42,10 @@ describe('ReferralRelationship Aggregate', () => {
const props = {
id: 1n,
userId: 100n,
accountSequence: 12345678,
accountSequence: 'D2512120001',
referrerId: 50n,
referralCode: 'RWATEST123',
usedReferralCode: 'RWAREF12345',
referralChain: [50n, 200n],
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-02'),
@ -52,28 +55,48 @@ describe('ReferralRelationship Aggregate', () => {
expect(relationship.id).toBe(1n);
expect(relationship.userId).toBe(100n);
expect(relationship.accountSequence).toBe(12345678);
expect(relationship.accountSequence).toBe('D2512120001');
expect(relationship.referrerId).toBe(50n);
expect(relationship.referralCode).toBe('RWATEST123');
expect(relationship.usedReferralCode).toBe('RWAREF12345');
expect(relationship.referralChain).toEqual([50n, 200n]);
});
it('should reconstitute without used referral code', () => {
const props = {
id: 1n,
userId: 100n,
accountSequence: 'D2512120001',
referrerId: null,
referralCode: 'RWATEST123',
usedReferralCode: null,
referralChain: [],
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-02'),
};
const relationship = ReferralRelationship.reconstitute(props);
expect(relationship.referrerId).toBeNull();
expect(relationship.usedReferralCode).toBeNull();
});
});
describe('getDirectReferrer', () => {
it('should return direct referrer', () => {
const relationship = ReferralRelationship.create(100n, 12345678, 50n, [200n]);
const relationship = ReferralRelationship.create(100n, 'D2512120001', 50n, 'RWAREF12345', [200n]);
expect(relationship.getDirectReferrer()).toBe(50n);
});
it('should return null when no referrer', () => {
const relationship = ReferralRelationship.create(100n, 12345678, null);
const relationship = ReferralRelationship.create(100n, 'D2512120001', null, null);
expect(relationship.getDirectReferrer()).toBeNull();
});
});
describe('getReferrerAtLevel', () => {
it('should return referrer at specific level', () => {
const relationship = ReferralRelationship.create(100n, 12345678, 50n, [200n, 300n]);
const relationship = ReferralRelationship.create(100n, 'D2512120001', 50n, 'RWAREF12345', [200n, 300n]);
expect(relationship.getReferrerAtLevel(0)).toBe(50n);
expect(relationship.getReferrerAtLevel(1)).toBe(200n);
@ -83,21 +106,21 @@ describe('ReferralRelationship Aggregate', () => {
describe('getAllAncestorIds', () => {
it('should return all ancestor IDs', () => {
const relationship = ReferralRelationship.create(100n, 12345678, 50n, [200n, 300n]);
const relationship = ReferralRelationship.create(100n, 'D2512120001', 50n, 'RWAREF12345', [200n, 300n]);
expect(relationship.getAllAncestorIds()).toEqual([50n, 200n, 300n]);
});
});
describe('getChainDepth', () => {
it('should return chain depth', () => {
const relationship = ReferralRelationship.create(100n, 12345678, 50n, [200n, 300n]);
const relationship = ReferralRelationship.create(100n, 'D2512120001', 50n, 'RWAREF12345', [200n, 300n]);
expect(relationship.getChainDepth()).toBe(3);
});
});
describe('clearDomainEvents', () => {
it('should clear domain events', () => {
const relationship = ReferralRelationship.create(100n, 12345678, 50n);
const relationship = ReferralRelationship.create(100n, 'D2512120001', 50n, 'RWAREF12345');
expect(relationship.domainEvents.length).toBe(1);
relationship.clearDomainEvents();
@ -106,15 +129,24 @@ describe('ReferralRelationship Aggregate', () => {
});
describe('toPersistence', () => {
it('should convert to persistence format', () => {
const relationship = ReferralRelationship.create(100n, 12345678, 50n, [200n]);
it('should convert to persistence format with referrer', () => {
const relationship = ReferralRelationship.create(100n, 'D2512120001', 50n, 'RWAREF12345', [200n]);
const data = relationship.toPersistence();
expect(data.userId).toBe(100n);
expect(data.accountSequence).toBe(12345678);
expect(data.accountSequence).toBe('D2512120001');
expect(data.referrerId).toBe(50n);
expect(data.referralCode).toMatch(/^RWA/);
expect(data.usedReferralCode).toBe('RWAREF12345');
expect(data.referralChain).toEqual([50n, 200n]);
});
it('should convert to persistence format without referrer', () => {
const relationship = ReferralRelationship.create(100n, 'D2512120001', null, null);
const data = relationship.toPersistence();
expect(data.referrerId).toBeNull();
expect(data.usedReferralCode).toBeNull();
});
});
});

View File

@ -6,6 +6,7 @@
type MockReferralRelationship = {
id: bigint;
userId: bigint;
accountSequence: string; // 格式: D + YYMMDD + 5位序号
referrerId: bigint | null;
rootUserId: bigint | null;
myReferralCode: string;
@ -65,6 +66,7 @@ export class MockPrismaService {
const created: MockReferralRelationship = {
id: this.idCounter++,
userId: args.where.userId,
accountSequence: args.create.accountSequence ?? `D${Date.now()}`,
referrerId: args.create.referrerId ?? null,
rootUserId: args.create.rootUserId ?? null,
myReferralCode: args.create.myReferralCode!,

View File

@ -29,53 +29,55 @@ describe('ReferralRelationshipRepository (Integration)', () => {
});
describe('save', () => {
it('should save a new referral relationship', async () => {
const relationship = ReferralRelationship.create(100n, 12345678, null);
it('should save a new referral relationship without referrer', async () => {
const relationship = ReferralRelationship.create(100n, 'D2512120001', null, null);
const saved = await repository.save(relationship);
expect(saved).toBeDefined();
expect(saved.userId).toBe(100n);
expect(saved.accountSequence).toBe(12345678);
expect(saved.accountSequence).toBe('D2512120001');
expect(saved.referralCode).toBeDefined();
expect(saved.usedReferralCode).toBeNull();
expect(saved.referralChain).toEqual([]);
});
it('should save referral relationship with referrer', async () => {
// First create the referrer
const referrer = ReferralRelationship.create(50n, 11111111, null);
await repository.save(referrer);
const referrer = ReferralRelationship.create(50n, 'D2512110001', null, null);
const savedReferrer = await repository.save(referrer);
// Then create with referrer
const relationship = ReferralRelationship.create(100n, 12345678, 50n, []);
// Then create with referrer, passing the referrer's referral code
const relationship = ReferralRelationship.create(100n, 'D2512120001', 50n, savedReferrer.referralCode, []);
const saved = await repository.save(relationship);
expect(saved.userId).toBe(100n);
expect(saved.accountSequence).toBe(12345678);
expect(saved.accountSequence).toBe('D2512120001');
expect(saved.referrerId).toBe(50n);
expect(saved.usedReferralCode).toBe(savedReferrer.referralCode);
expect(saved.referralChain).toContain(50n);
});
it('should update existing referral relationship', async () => {
const relationship = ReferralRelationship.create(100n, 12345678, null);
const relationship = ReferralRelationship.create(100n, 'D2512120001', null, 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);
expect(updated.accountSequence).toBe('D2512120001');
});
});
describe('findByUserId', () => {
it('should find relationship by user ID', async () => {
const relationship = ReferralRelationship.create(100n, 12345678, null);
const relationship = ReferralRelationship.create(100n, 'D2512120001', null, null);
await repository.save(relationship);
const found = await repository.findByUserId(100n);
expect(found).toBeDefined();
expect(found!.userId).toBe(100n);
expect(found!.accountSequence).toBe(12345678);
expect(found!.accountSequence).toBe('D2512120001');
});
it('should return null for non-existent user', async () => {
@ -86,14 +88,14 @@ describe('ReferralRelationshipRepository (Integration)', () => {
describe('findByReferralCode', () => {
it('should find relationship by referral code', async () => {
const relationship = ReferralRelationship.create(100n, 12345678, null);
const relationship = ReferralRelationship.create(100n, 'D2512120001', null, 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);
expect(found!.accountSequence).toBe('D2512120001');
});
it('should return null for non-existent code', async () => {
@ -105,12 +107,12 @@ describe('ReferralRelationshipRepository (Integration)', () => {
describe('findDirectReferrals', () => {
it('should find all direct referrals', async () => {
// Create referrer
const referrer = ReferralRelationship.create(50n, 11111111, null);
await repository.save(referrer);
const referrer = ReferralRelationship.create(50n, 'D2512110001', null, null);
const savedReferrer = await repository.save(referrer);
// Create direct referrals
const ref1 = ReferralRelationship.create(100n, 12345678, 50n, []);
const ref2 = ReferralRelationship.create(101n, 12345679, 50n, []);
// Create direct referrals with referrer's code
const ref1 = ReferralRelationship.create(100n, 'D2512120001', 50n, savedReferrer.referralCode, []);
const ref2 = ReferralRelationship.create(101n, 'D2512120002', 50n, savedReferrer.referralCode, []);
await repository.save(ref1);
await repository.save(ref2);
@ -122,7 +124,7 @@ describe('ReferralRelationshipRepository (Integration)', () => {
});
it('should return empty array for user with no referrals', async () => {
const referrer = ReferralRelationship.create(50n, 11111111, null);
const referrer = ReferralRelationship.create(50n, 'D2512110001', null, null);
await repository.save(referrer);
const directReferrals = await repository.findDirectReferrals(50n);
@ -132,7 +134,7 @@ describe('ReferralRelationshipRepository (Integration)', () => {
describe('existsByReferralCode', () => {
it('should return true for existing code', async () => {
const relationship = ReferralRelationship.create(100n, 12345678, null);
const relationship = ReferralRelationship.create(100n, 'D2512120001', null, null);
const saved = await repository.save(relationship);
const exists = await repository.existsByReferralCode(saved.referralCode);
@ -147,7 +149,7 @@ describe('ReferralRelationshipRepository (Integration)', () => {
describe('existsByUserId', () => {
it('should return true for existing user', async () => {
const relationship = ReferralRelationship.create(100n, 12345678, null);
const relationship = ReferralRelationship.create(100n, 'D2512120001', null, null);
await repository.save(relationship);
const exists = await repository.existsByUserId(100n);
@ -163,7 +165,7 @@ describe('ReferralRelationshipRepository (Integration)', () => {
describe('getReferralChain', () => {
it('should return referral chain', async () => {
const parentChain = [200n, 300n];
const relationship = ReferralRelationship.create(100n, 12345678, 50n, parentChain);
const relationship = ReferralRelationship.create(100n, 'D2512120001', 50n, 'RWAREF12345', parentChain);
await repository.save(relationship);
const chain = await repository.getReferralChain(100n);