fix: 统一推荐码生成逻辑 - 由 identity-service 单点生成
重要变更: - identity-service 生成用户推荐码,通过 Kafka 事件传递给 referral-service - referral-service 不再自己生成推荐码,直接使用事件中的推荐码 - 修复两个服务推荐码不一致的问题 涉及服务: - identity-service: 事件 payload 添加 referralCode 字段 - referral-service: 接收并存储 identity-service 生成的推荐码 - wallet-service: 添加区域账户动态创建接口 - planting-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:
parent
ebbf2d971a
commit
98d8bee20d
|
|
@ -33,9 +33,9 @@ export class AutoCreateAccountDto {
|
|||
@IsObject()
|
||||
deviceName?: DeviceNameDto;
|
||||
|
||||
@ApiPropertyOptional({ example: 'ABC123', description: '邀请人推荐码' })
|
||||
@ApiPropertyOptional({ example: 'RWAABC1234', description: '邀请人推荐码 (6-20位大写字母和数字)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Matches(/^[A-Z0-9]{6}$/, { message: '推荐码格式错误' })
|
||||
@Matches(/^[A-Z0-9]{6,20}$/, { message: '推荐码格式错误' })
|
||||
inviterReferralCode?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ export class UserAccount {
|
|||
account.addDomainEvent(new UserAccountAutoCreatedEvent({
|
||||
userId: account.userId.toString(),
|
||||
accountSequence: params.accountSequence.value,
|
||||
referralCode: account._referralCode.value, // 用户的推荐码
|
||||
initialDeviceId: params.initialDeviceId,
|
||||
inviterSequence: params.inviterSequence?.value || null,
|
||||
registeredAt: account._registeredAt,
|
||||
|
|
@ -137,6 +138,7 @@ export class UserAccount {
|
|||
account.addDomainEvent(new UserAccountCreatedEvent({
|
||||
userId: account.userId.toString(),
|
||||
accountSequence: params.accountSequence.value,
|
||||
referralCode: account._referralCode.value, // 用户的推荐码
|
||||
phoneNumber: params.phoneNumber.value,
|
||||
initialDeviceId: params.initialDeviceId,
|
||||
inviterSequence: params.inviterSequence?.value || null,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export class UserAccountAutoCreatedEvent extends DomainEvent {
|
|||
public readonly payload: {
|
||||
userId: string;
|
||||
accountSequence: string; // 格式: D + YYMMDD + 5位序号
|
||||
referralCode: string; // 用户的推荐码(由 identity-service 生成)
|
||||
initialDeviceId: string;
|
||||
inviterSequence: string | null; // 格式: D + YYMMDD + 5位序号
|
||||
registeredAt: Date;
|
||||
|
|
@ -33,6 +34,7 @@ export class UserAccountCreatedEvent extends DomainEvent {
|
|||
public readonly payload: {
|
||||
userId: string;
|
||||
accountSequence: string; // 格式: D + YYMMDD + 5位序号
|
||||
referralCode: string; // 用户的推荐码(由 identity-service 生成)
|
||||
phoneNumber: string;
|
||||
initialDeviceId: string;
|
||||
inviterSequence: string | null; // 格式: D + YYMMDD + 5位序号
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@ import { DomainError } from '@/shared/exceptions/domain.exception';
|
|||
|
||||
export class ReferralCode {
|
||||
constructor(public readonly value: string) {
|
||||
if (!/^[A-Z0-9]{6}$/.test(value)) {
|
||||
// 兼容 referral-service 的推荐码格式 (6-20位大写字母和数字)
|
||||
if (!/^[A-Z0-9]{6,20}$/.test(value)) {
|
||||
throw new DomainError('推荐码格式错误');
|
||||
}
|
||||
}
|
||||
|
||||
static generate(): ReferralCode {
|
||||
// 生成6位随机推荐码(identity-service 本地生成)
|
||||
// 注:referral-service 会生成10位的推荐码,两者都兼容
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let code = '';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ export class PlantingApplicationService {
|
|||
|
||||
/**
|
||||
* 确认省市选择 (5秒后调用)
|
||||
* 同时在wallet-service中创建对应的省市区域账户
|
||||
*/
|
||||
async confirmProvinceCity(
|
||||
orderNo: string,
|
||||
|
|
@ -155,9 +156,33 @@ export class PlantingApplicationService {
|
|||
throw new Error('无权操作此订单');
|
||||
}
|
||||
|
||||
// 获取省市选择信息
|
||||
const selection = order.provinceCitySelection;
|
||||
if (!selection) {
|
||||
throw new Error('请先选择省市');
|
||||
}
|
||||
|
||||
// 确认省市选择
|
||||
order.confirmProvinceCity();
|
||||
await this.orderRepository.save(order);
|
||||
|
||||
// 确保省市区域账户存在(动态创建)
|
||||
try {
|
||||
const result = await this.walletService.ensureRegionAccounts({
|
||||
provinceCode: selection.provinceCode,
|
||||
provinceName: selection.provinceName,
|
||||
cityCode: selection.cityCode,
|
||||
cityName: selection.cityName,
|
||||
});
|
||||
this.logger.log(
|
||||
`区域账户确保完成: 省=${result.provinceAccount.accountSequence}(created=${result.provinceAccount.created}), ` +
|
||||
`市=${result.cityAccount.accountSequence}(created=${result.cityAccount.created})`
|
||||
);
|
||||
} catch (error) {
|
||||
// 区域账户创建失败不影响省市确认流程,只记录日志
|
||||
this.logger.warn(`创建区域账户失败,将在支付时重试: ${error.message}`);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -386,4 +386,50 @@ export class WalletServiceClient {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保区域账户存在
|
||||
* 在用户选择省市确认后调用,动态创建省/市区域账户
|
||||
*/
|
||||
async ensureRegionAccounts(params: {
|
||||
provinceCode: string;
|
||||
provinceName: string;
|
||||
cityCode: string;
|
||||
cityName: string;
|
||||
}): Promise<{
|
||||
provinceAccount: { accountSequence: string; created: boolean };
|
||||
cityAccount: { accountSequence: string; created: boolean };
|
||||
}> {
|
||||
try {
|
||||
return await this.withRetry(
|
||||
`ensureRegionAccounts(${params.provinceCode}, ${params.cityCode})`,
|
||||
async () => {
|
||||
const response = await firstValueFrom(
|
||||
this.httpService.post<{
|
||||
provinceAccount: { accountSequence: string; created: boolean };
|
||||
cityAccount: { accountSequence: string; created: boolean };
|
||||
}>(
|
||||
`${this.baseUrl}/api/v1/wallets/ensure-region-accounts`,
|
||||
params,
|
||||
),
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to ensure region accounts: province=${params.provinceCode}, city=${params.cityCode}`,
|
||||
error,
|
||||
);
|
||||
// 在开发环境模拟成功
|
||||
if (this.configService.get('NODE_ENV') === 'development') {
|
||||
this.logger.warn('Development mode: simulating region accounts creation');
|
||||
return {
|
||||
provinceAccount: { accountSequence: `9${params.provinceCode}`, created: true },
|
||||
cityAccount: { accountSequence: `8${params.cityCode}`, created: true },
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,6 +192,7 @@ export class ReferralController {
|
|||
const command = new CreateReferralRelationshipCommand(
|
||||
BigInt(dto.userId),
|
||||
dto.accountSequence,
|
||||
dto.referralCode,
|
||||
dto.referrerCode ?? null,
|
||||
dto.inviterAccountSequence ?? null,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,12 @@ export class CreateReferralDto {
|
|||
@IsString()
|
||||
accountSequence: string; // 格式: D + YYMMDD + 5位序号
|
||||
|
||||
@ApiPropertyOptional({ description: '推荐码', example: 'RWA123ABC' })
|
||||
@ApiProperty({ description: '用户的推荐码(由 identity-service 生成)', example: 'ABC123' })
|
||||
@IsString()
|
||||
@Length(6, 20)
|
||||
referralCode: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '推荐人的推荐码', example: 'RWA123ABC' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Length(6, 20)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export class CreateReferralRelationshipCommand {
|
|||
constructor(
|
||||
public readonly userId: bigint,
|
||||
public readonly accountSequence: string, // 格式: D + YYMMDD + 5位序号
|
||||
public readonly referralCode: string, // 用户的推荐码(由 identity-service 生成)
|
||||
public readonly referrerCode: string | null = null,
|
||||
public readonly inviterAccountSequence: string | null = null, // 格式: D + YYMMDD + 5位序号
|
||||
) {}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { CreateReferralRelationshipCommand } from '../commands';
|
|||
interface UserAccountCreatedPayload {
|
||||
userId: string;
|
||||
accountSequence: string; // 格式: D + YYMMDD + 5位序号
|
||||
referralCode: string; // 用户的推荐码(由 identity-service 生成)
|
||||
inviterSequence: string | null; // 格式: D + YYMMDD + 5位序号
|
||||
registeredAt: string;
|
||||
// UserAccountCreated 有 phoneNumber, UserAccountAutoCreated 没有
|
||||
|
|
@ -73,6 +74,7 @@ export class UserRegisteredHandler implements OnModuleInit {
|
|||
const command = new CreateReferralRelationshipCommand(
|
||||
userIdFromSequence, // 使用从 accountSequence 提取的数值作为 userId
|
||||
payload.accountSequence, // 完整的 accountSequence 字符串
|
||||
payload.referralCode, // 用户的推荐码(由 identity-service 生成)
|
||||
null, // referrerCode - 不通过推荐码查找
|
||||
payload.inviterSequence, // 通过 accountSequence 查找推荐人
|
||||
);
|
||||
|
|
|
|||
|
|
@ -81,10 +81,11 @@ export class ReferralService {
|
|||
}
|
||||
}
|
||||
|
||||
// 创建推荐关系(传入推荐人的推荐码)
|
||||
// 创建推荐关系(传入用户自己的推荐码和推荐人的推荐码)
|
||||
const relationship = ReferralRelationship.create(
|
||||
command.userId,
|
||||
command.accountSequence,
|
||||
command.referralCode, // 用户自己的推荐码(由 identity-service 生成)
|
||||
referrerId,
|
||||
referrerReferralCode, // 推荐人的推荐码
|
||||
parentChain,
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ export class ReferralRelationship {
|
|||
* 创建新的推荐关系 (用户注册时)
|
||||
* @param userId 新用户ID
|
||||
* @param accountSequence 新用户账户序号
|
||||
* @param userReferralCode 用户自己的推荐码(由 identity-service 生成,直接传入)
|
||||
* @param referrerId 推荐人ID(可选)
|
||||
* @param referrerReferralCode 推荐人的推荐码(可选,如果有推荐人则必须提供)
|
||||
* @param parentReferralChain 推荐人的推荐链
|
||||
|
|
@ -79,13 +80,14 @@ export class ReferralRelationship {
|
|||
static create(
|
||||
userId: bigint,
|
||||
accountSequence: string, // 格式: D + YYMMDD + 5位序号
|
||||
userReferralCode: string, // 用户的推荐码(由 identity-service 生成)
|
||||
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 referralCode = ReferralCode.create(userReferralCode); // 使用传入的推荐码,不再生成
|
||||
const usedReferralCode = referrerReferralCode ? ReferralCode.create(referrerReferralCode) : null;
|
||||
const referralChain = ReferralChain.create(referrerId, parentReferralChain);
|
||||
const now = new Date();
|
||||
|
|
|
|||
|
|
@ -2,13 +2,18 @@
|
|||
-- These accounts receive fixed allocations from each planting order
|
||||
-- Note: Province/City area accounts (9+provinceCode, 8+cityCode) are dynamically created
|
||||
-- when users select their planting location
|
||||
--
|
||||
-- System account user_id convention:
|
||||
-- - Fixed system accounts: -1 to -100 (negative to avoid conflict with real users)
|
||||
-- - Province area accounts: 9 + provinceCode (e.g., 9440000 for Guangdong)
|
||||
-- - City area accounts: 8 + cityCode (e.g., 8440100 for Guangzhou)
|
||||
|
||||
-- S0000000001: 总部社区 (Headquarters Community)
|
||||
-- Receives: 基础费 9 USDT, 无推荐人的分享权益 500 USDT, 无授权的团队权益等
|
||||
INSERT INTO "wallet_accounts" (
|
||||
"account_sequence", "user_id", "status", "created_at", "updated_at"
|
||||
) VALUES (
|
||||
'S0000000001', 1, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
'S0000000001', -1, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
) ON CONFLICT ("account_sequence") DO NOTHING;
|
||||
|
||||
-- S0000000002: 成本费账户 (Cost Fee Account)
|
||||
|
|
@ -16,7 +21,7 @@ INSERT INTO "wallet_accounts" (
|
|||
INSERT INTO "wallet_accounts" (
|
||||
"account_sequence", "user_id", "status", "created_at", "updated_at"
|
||||
) VALUES (
|
||||
'S0000000002', 2, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
'S0000000002', -2, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
) ON CONFLICT ("account_sequence") DO NOTHING;
|
||||
|
||||
-- S0000000003: 运营费账户 (Operation Fee Account)
|
||||
|
|
@ -24,7 +29,7 @@ INSERT INTO "wallet_accounts" (
|
|||
INSERT INTO "wallet_accounts" (
|
||||
"account_sequence", "user_id", "status", "created_at", "updated_at"
|
||||
) VALUES (
|
||||
'S0000000003', 3, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
'S0000000003', -3, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
) ON CONFLICT ("account_sequence") DO NOTHING;
|
||||
|
||||
-- S0000000004: RWAD底池账户 (RWAD Pool Injection Account)
|
||||
|
|
@ -32,5 +37,5 @@ INSERT INTO "wallet_accounts" (
|
|||
INSERT INTO "wallet_accounts" (
|
||||
"account_sequence", "user_id", "status", "created_at", "updated_at"
|
||||
) VALUES (
|
||||
'S0000000004', 4, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
'S0000000004', -4, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
) ON CONFLICT ("account_sequence") DO NOTHING;
|
||||
|
|
|
|||
|
|
@ -147,4 +147,23 @@ export class InternalWalletController {
|
|||
const result = await this.walletService.allocateFunds(command);
|
||||
return { success: result.success };
|
||||
}
|
||||
|
||||
@Post('ensure-region-accounts')
|
||||
@Public()
|
||||
@ApiOperation({ summary: '确保区域账户存在(内部API) - 在选择省市确认后调用' })
|
||||
@ApiResponse({ status: 200, description: '区域账户创建结果' })
|
||||
async ensureRegionAccounts(
|
||||
@Body() dto: { provinceCode: string; provinceName: string; cityCode: string; cityName: string },
|
||||
) {
|
||||
this.logger.log(`确保区域账户存在: 省=${dto.provinceName}(${dto.provinceCode}), 市=${dto.cityName}(${dto.cityCode})`);
|
||||
|
||||
const result = await this.walletService.ensureRegionAccounts({
|
||||
provinceCode: dto.provinceCode,
|
||||
provinceName: dto.provinceName,
|
||||
cityCode: dto.cityCode,
|
||||
cityName: dto.cityName,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -622,28 +622,27 @@ export class WalletApplicationService {
|
|||
): Promise<void> {
|
||||
// targetId 是 accountSequence
|
||||
// - 用户账户: D2512120001
|
||||
// - 固定系统账户: S0000000001
|
||||
// - 省区域账户: 9440000 (9 + provinceCode)
|
||||
// - 市区域账户: 8440100 (8 + cityCode)
|
||||
// - 固定系统账户: S0000000001 (userId: -1 to -100, 由migration seed创建)
|
||||
// - 省区域账户: 9440000 (9 + provinceCode, userId = 9440000)
|
||||
// - 市区域账户: 8440100 (8 + cityCode, userId = 8440100)
|
||||
|
||||
// 为系统账户生成 userId (用于数据库约束)
|
||||
// 省区域: 使用 9 + provinceCode 作为 userId
|
||||
// 市区域: 使用 8 + cityCode 作为 userId
|
||||
let systemUserId = BigInt(0);
|
||||
const targetId = allocation.targetId;
|
||||
if (targetId.startsWith('9')) {
|
||||
// 省区域账户: 9440000 -> userId = 9440000
|
||||
// 省区域账户: 9440000 -> userId = 9440000 (动态创建)
|
||||
systemUserId = BigInt(targetId);
|
||||
} else if (targetId.startsWith('8')) {
|
||||
// 市区域账户: 8440100 -> userId = 8440100
|
||||
// 市区域账户: 8440100 -> userId = 8440100 (动态创建)
|
||||
systemUserId = BigInt(targetId);
|
||||
} else if (targetId.startsWith('S')) {
|
||||
// 固定系统账户: S0000000001 -> userId 从后面的数字提取
|
||||
// 固定系统账户: S0000000001 -> userId = -1 (负数,由seed创建,直接查找)
|
||||
const numPart = targetId.slice(1);
|
||||
systemUserId = BigInt(parseInt(numPart, 10));
|
||||
systemUserId = BigInt(-parseInt(numPart, 10));
|
||||
}
|
||||
|
||||
// 使用 getOrCreate 自动创建不存在的账户(包括省/市区域账户)
|
||||
// 使用 getOrCreate 自动创建不存在的账户(仅用于省/市区域账户的动态创建)
|
||||
// 固定系统账户(S开头)应该已由migration seed创建
|
||||
const wallet = await this.walletRepo.getOrCreate(targetId, systemUserId);
|
||||
if (!wallet) {
|
||||
this.logger.warn(`Failed to get or create wallet for accountSequence ${allocation.targetId}`);
|
||||
|
|
@ -711,6 +710,67 @@ export class WalletApplicationService {
|
|||
// });
|
||||
}
|
||||
|
||||
// =============== Region Accounts ===============
|
||||
|
||||
/**
|
||||
* 确保区域账户存在
|
||||
* 在用户选择省市确认后调用,动态创建省/市区域账户
|
||||
*
|
||||
* 账户序列号规则:
|
||||
* - 省区域账户: 9 + provinceCode (例: 9440000)
|
||||
* - 市区域账户: 8 + cityCode (例: 8440100)
|
||||
*/
|
||||
async ensureRegionAccounts(params: {
|
||||
provinceCode: string;
|
||||
provinceName: string;
|
||||
cityCode: string;
|
||||
cityName: string;
|
||||
}): Promise<{
|
||||
provinceAccount: { accountSequence: string; created: boolean };
|
||||
cityAccount: { accountSequence: string; created: boolean };
|
||||
}> {
|
||||
const { provinceCode, provinceName, cityCode, cityName } = params;
|
||||
|
||||
// 省区域账户: 9 + provinceCode
|
||||
const provinceAccountSequence = `9${provinceCode}`;
|
||||
const provinceUserId = BigInt(provinceAccountSequence);
|
||||
|
||||
// 市区域账户: 8 + cityCode
|
||||
const cityAccountSequence = `8${cityCode}`;
|
||||
const cityUserId = BigInt(cityAccountSequence);
|
||||
|
||||
this.logger.log(`确保区域账户存在: 省=${provinceAccountSequence}, 市=${cityAccountSequence}`);
|
||||
|
||||
// 检查省账户是否已存在
|
||||
let provinceWallet = await this.walletRepo.findByAccountSequence(provinceAccountSequence);
|
||||
let provinceCreated = false;
|
||||
if (!provinceWallet) {
|
||||
provinceWallet = await this.walletRepo.getOrCreate(provinceAccountSequence, provinceUserId);
|
||||
provinceCreated = true;
|
||||
this.logger.log(`创建省区域账户: ${provinceAccountSequence} (${provinceName})`);
|
||||
}
|
||||
|
||||
// 检查市账户是否已存在
|
||||
let cityWallet = await this.walletRepo.findByAccountSequence(cityAccountSequence);
|
||||
let cityCreated = false;
|
||||
if (!cityWallet) {
|
||||
cityWallet = await this.walletRepo.getOrCreate(cityAccountSequence, cityUserId);
|
||||
cityCreated = true;
|
||||
this.logger.log(`创建市区域账户: ${cityAccountSequence} (${cityName})`);
|
||||
}
|
||||
|
||||
return {
|
||||
provinceAccount: {
|
||||
accountSequence: provinceAccountSequence,
|
||||
created: provinceCreated,
|
||||
},
|
||||
cityAccount: {
|
||||
accountSequence: cityAccountSequence,
|
||||
created: cityCreated,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// =============== Withdrawal ===============
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue