diff --git a/backend/services/admin-service/package-lock.json b/backend/services/admin-service/package-lock.json index e95f2f59..4aaef6b8 100644 --- a/backend/services/admin-service/package-lock.json +++ b/backend/services/admin-service/package-lock.json @@ -24,6 +24,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "ioredis": "^5.3.2", + "kafkajs": "^2.2.4", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", @@ -7116,6 +7117,15 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/kafkajs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz", + "integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/backend/services/admin-service/package.json b/backend/services/admin-service/package.json index ab718bbe..6b7b85c2 100644 --- a/backend/services/admin-service/package.json +++ b/backend/services/admin-service/package.json @@ -44,6 +44,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "ioredis": "^5.3.2", + "kafkajs": "^2.2.4", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", @@ -51,7 +52,6 @@ "uuid": "^9.0.1" }, "devDependencies": { - "@types/uuid": "^9.0.7", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", @@ -61,6 +61,7 @@ "@types/passport-jwt": "^3.0.13", "@types/supertest": "^6.0.0", "@types/unzipper": "^0.10.11", + "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.42.0", diff --git a/backend/services/admin-service/src/api/controllers/system-config.controller.ts b/backend/services/admin-service/src/api/controllers/system-config.controller.ts index 501ed92d..560781b7 100644 --- a/backend/services/admin-service/src/api/controllers/system-config.controller.ts +++ b/backend/services/admin-service/src/api/controllers/system-config.controller.ts @@ -30,7 +30,7 @@ export const CONFIG_KEYS = { interface SystemConfigResponseDto { key: string; value: string; - description?: string; + description: string | null; updatedAt: Date; } diff --git a/backend/services/admin-service/src/domain/repositories/system-config.repository.ts b/backend/services/admin-service/src/domain/repositories/system-config.repository.ts index 0a670084..b76be4d8 100644 --- a/backend/services/admin-service/src/domain/repositories/system-config.repository.ts +++ b/backend/services/admin-service/src/domain/repositories/system-config.repository.ts @@ -7,10 +7,10 @@ export interface SystemConfigEntity { id: string; key: string; value: string; - description?: string; + description: string | null; createdAt: Date; updatedAt: Date; - updatedBy?: string; + updatedBy: string | null; } export interface ISystemConfigRepository { diff --git a/backend/services/authorization-service/src/application/services/authorization-application.service.ts b/backend/services/authorization-service/src/application/services/authorization-application.service.ts index 2b279b34..5e8e770b 100644 --- a/backend/services/authorization-service/src/application/services/authorization-application.service.ts +++ b/backend/services/authorization-service/src/application/services/authorization-application.service.ts @@ -2775,8 +2775,8 @@ export class AuthorizationApplicationService { // 1. 获取用户认种数据 const teamStats = await this.statsRepository.findByAccountSequence(accountSequence) - const hasPlanted = (teamStats?.personalPlantingCount || 0) > 0 - const plantedCount = teamStats?.personalPlantingCount || 0 + const hasPlanted = (teamStats?.selfPlantingCount || 0) > 0 + const plantedCount = teamStats?.selfPlantingCount || 0 // 2. 获取用户已有的授权 const authorizations = await this.authorizationRepository.findByAccountSequence(accountSequence) @@ -2812,8 +2812,8 @@ export class AuthorizationApplicationService { // 1. 验证用户已认种 const teamStats = await this.statsRepository.findByAccountSequence(command.accountSequence) - const personalPlantingCount = teamStats?.personalPlantingCount || 0 - if (personalPlantingCount <= 0) { + const selfPlantingCount = teamStats?.selfPlantingCount || 0 + if (selfPlantingCount <= 0) { throw new ApplicationError('申请授权需要先完成认种') } @@ -2891,7 +2891,7 @@ export class AuthorizationApplicationService { authorization.clearDomainEvents() return { - applicationId: authorization.id, + applicationId: authorization.authorizationId.value, status: 'APPROVED', type: SelfApplyAuthorizationType.COMMUNITY, appliedAt: new Date(), @@ -2907,20 +2907,19 @@ export class AuthorizationApplicationService { command: SelfApplyAuthorizationCommand, ): Promise { // 检查是否已有该市的授权市公司 - const existing = await this.authorizationRepository.findByRegionCodeAndRoleType( - RegionCode.create(command.cityCode!), + const existingList = await this.authorizationRepository.findActiveByRoleTypeAndRegion( RoleType.AUTH_CITY_COMPANY, + RegionCode.create(command.cityCode!), ) - if (existing && existing.status !== AuthorizationStatus.REVOKED) { + if (existingList.length > 0) { throw new ApplicationError('该市已有授权市公司') } // 创建授权市公司授权 const userId = UserId.create(command.userId, command.accountSequence) - const regionCode = RegionCode.create(command.cityCode!) const authorization = AuthorizationRole.createAuthCityCompany({ userId, - regionCode, + cityCode: command.cityCode!, cityName: command.cityName!, }) @@ -2936,7 +2935,7 @@ export class AuthorizationApplicationService { authorization.clearDomainEvents() return { - applicationId: authorization.id, + applicationId: authorization.authorizationId.value, status: 'APPROVED', type: SelfApplyAuthorizationType.CITY_TEAM, appliedAt: new Date(), @@ -2952,20 +2951,19 @@ export class AuthorizationApplicationService { command: SelfApplyAuthorizationCommand, ): Promise { // 检查是否已有该省的授权省公司 - const existing = await this.authorizationRepository.findByRegionCodeAndRoleType( - RegionCode.create(command.provinceCode!), + const existingList = await this.authorizationRepository.findActiveByRoleTypeAndRegion( RoleType.AUTH_PROVINCE_COMPANY, + RegionCode.create(command.provinceCode!), ) - if (existing && existing.status !== AuthorizationStatus.REVOKED) { + if (existingList.length > 0) { throw new ApplicationError('该省已有授权省公司') } // 创建授权省公司授权 const userId = UserId.create(command.userId, command.accountSequence) - const regionCode = RegionCode.create(command.provinceCode!) const authorization = AuthorizationRole.createAuthProvinceCompany({ userId, - regionCode, + provinceCode: command.provinceCode!, provinceName: command.provinceName!, }) @@ -2981,7 +2979,7 @@ export class AuthorizationApplicationService { authorization.clearDomainEvents() return { - applicationId: authorization.id, + applicationId: authorization.authorizationId.value, status: 'APPROVED', type: SelfApplyAuthorizationType.PROVINCE_TEAM, appliedAt: new Date(), diff --git a/backend/services/identity-service/src/application/services/user-application.service.referral.spec.ts b/backend/services/identity-service/src/application/services/user-application.service.referral.spec.ts index f119d91b..cb8700f7 100644 --- a/backend/services/identity-service/src/application/services/user-application.service.referral.spec.ts +++ b/backend/services/identity-service/src/application/services/user-application.service.referral.spec.ts @@ -7,12 +7,16 @@ import { AccountSequence, ReferralCode, UserId, AccountStatus, KYCStatus, Device import { ConfigService } from '@nestjs/config'; import { ValidateReferralCodeQuery, GetReferralStatsQuery, GenerateReferralLinkCommand } from '@/application/commands'; import { ApplicationError } from '@/shared/exceptions/domain.exception'; -import { AccountSequenceGeneratorService, UserValidatorService, WalletGeneratorService } from '@/domain/services'; +import { AccountSequenceGeneratorService, UserValidatorService } from '@/domain/services'; import { TokenService } from './token.service'; import { RedisService } from '@/infrastructure/redis/redis.service'; import { SmsService } from '@/infrastructure/external/sms/sms.service'; import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service'; import { MpcWalletService } from '@/infrastructure/external/mpc'; +import { BlockchainClientService } from '@/infrastructure/external/blockchain/blockchain-client.service'; +import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service'; +import { BlockchainWalletHandler } from '@/application/event-handlers/blockchain-wallet.handler'; +import { MpcKeygenCompletedHandler } from '@/application/event-handlers/mpc-keygen-completed.handler'; describe('UserApplicationService - Referral APIs', () => { let service: UserApplicationService; @@ -21,12 +25,12 @@ describe('UserApplicationService - Referral APIs', () => { // Helper function to create a test account using UserAccount.reconstruct const createMockAccount = (params: { userId?: string; - accountSequence?: number; + accountSequence?: string; referralCode?: string; nickname?: string; avatarUrl?: string | null; isActive?: boolean; - inviterSequence?: number | null; + inviterSequence?: string | null; registeredAt?: Date; } = {}): UserAccount => { const devices = [ @@ -35,7 +39,7 @@ describe('UserApplicationService - Referral APIs', () => { return UserAccount.reconstruct({ userId: params.userId || '123456789', - accountSequence: params.accountSequence || 1, + accountSequence: params.accountSequence || 'D2412190001', devices, phoneNumber: '13800138000', nickname: params.nickname || '用户1', @@ -90,15 +94,15 @@ describe('UserApplicationService - Referral APIs', () => { }; const mockAccountSequenceGeneratorService = { - getNext: jest.fn().mockResolvedValue(AccountSequence.create(1)), + getNext: jest.fn().mockResolvedValue(AccountSequence.create('D2412190001')), }; const mockUserValidatorService = { validateUniquePhone: jest.fn(), }; - const mockWalletGeneratorService = { - generateWallets: jest.fn(), + const mockBlockchainClientService = { + getBalance: jest.fn(), }; const mockTokenService = { @@ -127,6 +131,18 @@ describe('UserApplicationService - Referral APIs', () => { generateMpcWallet: jest.fn(), }; + const mockPrismaService = { + userAccount: { findUnique: jest.fn() }, + }; + + const mockBlockchainWalletHandler = { + handle: jest.fn(), + }; + + const mockMpcKeygenCompletedHandler = { + handle: jest.fn(), + }; + const module: TestingModule = await Test.createTestingModule({ providers: [ UserApplicationService, @@ -151,8 +167,8 @@ describe('UserApplicationService - Referral APIs', () => { useValue: mockUserValidatorService, }, { - provide: WalletGeneratorService, - useValue: mockWalletGeneratorService, + provide: BlockchainClientService, + useValue: mockBlockchainClientService, }, { provide: TokenService, @@ -174,6 +190,18 @@ describe('UserApplicationService - Referral APIs', () => { provide: MpcWalletService, useValue: mockMpcWalletService, }, + { + provide: PrismaService, + useValue: mockPrismaService, + }, + { + provide: BlockchainWalletHandler, + useValue: mockBlockchainWalletHandler, + }, + { + provide: MpcKeygenCompletedHandler, + useValue: mockMpcKeygenCompletedHandler, + }, ], }).compile(); @@ -189,7 +217,7 @@ describe('UserApplicationService - Referral APIs', () => { it('should return current user info with referral code and link', async () => { const mockAccount = createMockAccount({ userId: '123456789', - accountSequence: 1, + accountSequence: 'D2412190001', nickname: '测试用户', referralCode: 'ABC123', }); @@ -200,7 +228,7 @@ describe('UserApplicationService - Referral APIs', () => { expect(result).toEqual({ userId: '123456789', - accountSequence: 1, + accountSequence: 'D2412190001', phoneNumber: '138****8000', // masked nickname: '测试用户', avatarUrl: null, @@ -228,7 +256,7 @@ describe('UserApplicationService - Referral APIs', () => { describe('validateReferralCode', () => { it('should return valid=true for existing active referral code', async () => { const mockInviter = createMockAccount({ - accountSequence: 100, + accountSequence: 'D2412190100', nickname: '邀请人', avatarUrl: 'https://example.com/avatar.jpg', referralCode: 'INVTE1', @@ -245,7 +273,7 @@ describe('UserApplicationService - Referral APIs', () => { valid: true, referralCode: 'INVTE1', inviterInfo: { - accountSequence: 100, + accountSequence: 'D2412190100', nickname: '邀请人', avatarUrl: 'https://example.com/avatar.jpg', }, @@ -406,33 +434,33 @@ describe('UserApplicationService - Referral APIs', () => { it('should return referral stats with direct and indirect invites', async () => { const mockAccount = createMockAccount({ userId: '123456789', - accountSequence: 1, + accountSequence: 'D2412190001', referralCode: 'ABC123', }); // Direct invites (invited by user 1) const directInvite1 = createMockAccount({ userId: '200000001', - accountSequence: 2, + accountSequence: 'D2412190002', nickname: '直接邀请1', - inviterSequence: 1, + inviterSequence: 'D2412190001', registeredAt: new Date(), }); const directInvite2 = createMockAccount({ userId: '200000002', - accountSequence: 3, + accountSequence: 'D2412190003', nickname: '直接邀请2', - inviterSequence: 1, + inviterSequence: 'D2412190001', registeredAt: new Date(), }); // Indirect invite (invited by user 2, who was invited by user 1) const indirectInvite1 = createMockAccount({ userId: '300000001', - accountSequence: 4, + accountSequence: 'D2412190004', nickname: '间接邀请1', - inviterSequence: 2, + inviterSequence: 'D2412190002', registeredAt: new Date(), }); @@ -456,7 +484,7 @@ describe('UserApplicationService - Referral APIs', () => { thisMonthInvites: expect.any(Number), recentInvites: expect.arrayContaining([ expect.objectContaining({ - accountSequence: expect.any(Number), + accountSequence: expect.any(String), nickname: expect.any(String), level: expect.any(Number), // 1 for direct, 2 for indirect }), @@ -469,7 +497,7 @@ describe('UserApplicationService - Referral APIs', () => { it('should return empty stats when no invites', async () => { const mockAccount = createMockAccount({ userId: '123456789', - accountSequence: 1, + accountSequence: 'D2412190001', referralCode: 'ABC123', }); @@ -495,29 +523,29 @@ describe('UserApplicationService - Referral APIs', () => { it('should correctly calculate time-based stats', async () => { const mockAccount = createMockAccount({ userId: '123456789', - accountSequence: 1, + accountSequence: 'D2412190001', referralCode: 'ABC123', }); const now = new Date(); const todayInvite = createMockAccount({ - accountSequence: 2, + accountSequence: 'D2412190002', nickname: '今日邀请', - inviterSequence: 1, + inviterSequence: 'D2412190001', registeredAt: now, }); const yesterdayInvite = createMockAccount({ - accountSequence: 3, + accountSequence: 'D2412190003', nickname: '昨日邀请', - inviterSequence: 1, + inviterSequence: 'D2412190001', registeredAt: new Date(now.getTime() - 24 * 60 * 60 * 1000), // yesterday }); const lastMonthInvite = createMockAccount({ - accountSequence: 4, + accountSequence: 'D2412190004', nickname: '上月邀请', - inviterSequence: 1, + inviterSequence: 'D2412190001', registeredAt: new Date(now.getFullYear(), now.getMonth() - 1, 15), }); @@ -546,22 +574,22 @@ describe('UserApplicationService - Referral APIs', () => { it('should sort recent invites by registration date (newest first)', async () => { const mockAccount = createMockAccount({ userId: '123456789', - accountSequence: 1, + accountSequence: 'D2412190001', referralCode: 'ABC123', }); const now = new Date(); const oldInvite = createMockAccount({ - accountSequence: 2, + accountSequence: 'D2412190002', nickname: '旧邀请', - inviterSequence: 1, + inviterSequence: 'D2412190001', registeredAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000), // 7 days ago }); const newInvite = createMockAccount({ - accountSequence: 3, + accountSequence: 'D2412190003', nickname: '新邀请', - inviterSequence: 1, + inviterSequence: 'D2412190001', registeredAt: now, }); diff --git a/backend/services/identity-service/test/app.e2e-spec.ts b/backend/services/identity-service/test/app.e2e-spec.ts index 321449d9..9e59fc84 100644 --- a/backend/services/identity-service/test/app.e2e-spec.ts +++ b/backend/services/identity-service/test/app.e2e-spec.ts @@ -65,10 +65,17 @@ describe('Identity Service E2E Tests', () => { // 初始化账户序列号生成器 await prisma.accountSequenceGenerator.deleteMany(); + const today = new Date(); + const year = String(today.getFullYear()).slice(-2); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const day = String(today.getDate()).padStart(2, '0'); + const dateKey = `${year}${month}${day}`; + await prisma.accountSequenceGenerator.create({ data: { id: 1, - currentSequence: BigInt(100000), + dateKey: dateKey, + currentSequence: 0, }, }); }); diff --git a/frontend/admin-web/src/store/redux/slices/authSlice.ts b/frontend/admin-web/src/store/redux/slices/authSlice.ts index 372353d3..4ab72395 100644 --- a/frontend/admin-web/src/store/redux/slices/authSlice.ts +++ b/frontend/admin-web/src/store/redux/slices/authSlice.ts @@ -4,6 +4,7 @@ import type { User } from '@/types/user.types'; interface AuthState { user: User | null; token: string | null; + refreshToken: string | null; isAuthenticated: boolean; permissions: string[]; loading: boolean; @@ -12,6 +13,7 @@ interface AuthState { const initialState: AuthState = { user: null, token: null, + refreshToken: null, isAuthenticated: false, permissions: [], loading: true, @@ -23,10 +25,11 @@ const authSlice = createSlice({ reducers: { setCredentials: ( state, - action: PayloadAction<{ user: User; token: string; permissions?: string[] }> + action: PayloadAction<{ user: User; token: string; refreshToken?: string; permissions?: string[] }> ) => { state.user = action.payload.user; state.token = action.payload.token; + state.refreshToken = action.payload.refreshToken || null; state.permissions = action.payload.permissions || []; state.isAuthenticated = true; state.loading = false; @@ -34,6 +37,7 @@ const authSlice = createSlice({ logout: (state) => { state.user = null; state.token = null; + state.refreshToken = null; state.permissions = []; state.isAuthenticated = false; state.loading = false;