diff --git a/backend/services/identity-service/src/api/dto/request/auto-create-account.dto.ts b/backend/services/identity-service/src/api/dto/request/auto-create-account.dto.ts index 0b07911d..89550c18 100644 --- a/backend/services/identity-service/src/api/dto/request/auto-create-account.dto.ts +++ b/backend/services/identity-service/src/api/dto/request/auto-create-account.dto.ts @@ -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; } diff --git a/backend/services/identity-service/src/domain/aggregates/user-account/user-account.aggregate.ts b/backend/services/identity-service/src/domain/aggregates/user-account/user-account.aggregate.ts index 546df145..a3ecc05d 100644 --- a/backend/services/identity-service/src/domain/aggregates/user-account/user-account.aggregate.ts +++ b/backend/services/identity-service/src/domain/aggregates/user-account/user-account.aggregate.ts @@ -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, diff --git a/backend/services/identity-service/src/domain/events/index.ts b/backend/services/identity-service/src/domain/events/index.ts index 7c45aa0f..d5a65bb5 100644 --- a/backend/services/identity-service/src/domain/events/index.ts +++ b/backend/services/identity-service/src/domain/events/index.ts @@ -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位序号 diff --git a/backend/services/identity-service/src/domain/value-objects/referral-code.vo.ts b/backend/services/identity-service/src/domain/value-objects/referral-code.vo.ts index 03626701..a272104d 100644 --- a/backend/services/identity-service/src/domain/value-objects/referral-code.vo.ts +++ b/backend/services/identity-service/src/domain/value-objects/referral-code.vo.ts @@ -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++) { diff --git a/backend/services/planting-service/src/application/services/planting-application.service.ts b/backend/services/planting-service/src/application/services/planting-application.service.ts index c257827a..c0456894 100644 --- a/backend/services/planting-service/src/application/services/planting-application.service.ts +++ b/backend/services/planting-service/src/application/services/planting-application.service.ts @@ -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 }; } diff --git a/backend/services/planting-service/src/infrastructure/external/wallet-service.client.ts b/backend/services/planting-service/src/infrastructure/external/wallet-service.client.ts index 7e073350..ed24f882 100644 --- a/backend/services/planting-service/src/infrastructure/external/wallet-service.client.ts +++ b/backend/services/planting-service/src/infrastructure/external/wallet-service.client.ts @@ -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; + } + } } diff --git a/backend/services/referral-service/src/api/controllers/referral.controller.ts b/backend/services/referral-service/src/api/controllers/referral.controller.ts index 729be4fc..71f8328c 100644 --- a/backend/services/referral-service/src/api/controllers/referral.controller.ts +++ b/backend/services/referral-service/src/api/controllers/referral.controller.ts @@ -192,6 +192,7 @@ export class ReferralController { const command = new CreateReferralRelationshipCommand( BigInt(dto.userId), dto.accountSequence, + dto.referralCode, dto.referrerCode ?? null, dto.inviterAccountSequence ?? null, ); diff --git a/backend/services/referral-service/src/api/dto/referral.dto.ts b/backend/services/referral-service/src/api/dto/referral.dto.ts index 50fa717e..9b1e6745 100644 --- a/backend/services/referral-service/src/api/dto/referral.dto.ts +++ b/backend/services/referral-service/src/api/dto/referral.dto.ts @@ -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) diff --git a/backend/services/referral-service/src/application/commands/create-referral-relationship.command.ts b/backend/services/referral-service/src/application/commands/create-referral-relationship.command.ts index 6f610e8c..d02e2121 100644 --- a/backend/services/referral-service/src/application/commands/create-referral-relationship.command.ts +++ b/backend/services/referral-service/src/application/commands/create-referral-relationship.command.ts @@ -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位序号 ) {} diff --git a/backend/services/referral-service/src/application/event-handlers/user-registered.handler.ts b/backend/services/referral-service/src/application/event-handlers/user-registered.handler.ts index b6c646aa..f4ff3307 100644 --- a/backend/services/referral-service/src/application/event-handlers/user-registered.handler.ts +++ b/backend/services/referral-service/src/application/event-handlers/user-registered.handler.ts @@ -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 查找推荐人 ); diff --git a/backend/services/referral-service/src/application/services/referral.service.ts b/backend/services/referral-service/src/application/services/referral.service.ts index 2e354abf..5f4883ec 100644 --- a/backend/services/referral-service/src/application/services/referral.service.ts +++ b/backend/services/referral-service/src/application/services/referral.service.ts @@ -81,10 +81,11 @@ export class ReferralService { } } - // 创建推荐关系(传入推荐人的推荐码) + // 创建推荐关系(传入用户自己的推荐码和推荐人的推荐码) const relationship = ReferralRelationship.create( command.userId, command.accountSequence, + command.referralCode, // 用户自己的推荐码(由 identity-service 生成) referrerId, referrerReferralCode, // 推荐人的推荐码 parentChain, diff --git a/backend/services/referral-service/src/domain/aggregates/referral-relationship/referral-relationship.aggregate.ts b/backend/services/referral-service/src/domain/aggregates/referral-relationship/referral-relationship.aggregate.ts index d985e827..fb19362f 100644 --- a/backend/services/referral-service/src/domain/aggregates/referral-relationship/referral-relationship.aggregate.ts +++ b/backend/services/referral-service/src/domain/aggregates/referral-relationship/referral-relationship.aggregate.ts @@ -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(); diff --git a/backend/services/wallet-service/prisma/migrations/20241213000000_seed_system_accounts/migration.sql b/backend/services/wallet-service/prisma/migrations/20241213000000_seed_system_accounts/migration.sql index 4a127133..33640fda 100644 --- a/backend/services/wallet-service/prisma/migrations/20241213000000_seed_system_accounts/migration.sql +++ b/backend/services/wallet-service/prisma/migrations/20241213000000_seed_system_accounts/migration.sql @@ -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; diff --git a/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts b/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts index 38803ebc..84879faa 100644 --- a/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts +++ b/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts @@ -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; + } } diff --git a/backend/services/wallet-service/src/application/services/wallet-application.service.ts b/backend/services/wallet-service/src/application/services/wallet-application.service.ts index 300ece1b..7c66644f 100644 --- a/backend/services/wallet-service/src/application/services/wallet-application.service.ts +++ b/backend/services/wallet-service/src/application/services/wallet-application.service.ts @@ -622,28 +622,27 @@ export class WalletApplicationService { ): Promise { // 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 =============== /**