fix: 修复多个服务的 TypeScript 编译错误

- admin-service: 添加 kafkajs 依赖,修复 SystemConfigEntity null vs undefined 类型
- authorization-service: 修复 selfPlantingCount 属性名,修复 AuthorizationRole factory 参数
- identity-service: 修复测试文件 accountSequence 类型(number -> string)
- admin-web: 在 authSlice 中添加 refreshToken 支持

🤖 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-19 06:29:49 -08:00
parent 943fd9efe9
commit f20643599e
8 changed files with 105 additions and 57 deletions

View File

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

View File

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

View File

@ -30,7 +30,7 @@ export const CONFIG_KEYS = {
interface SystemConfigResponseDto {
key: string;
value: string;
description?: string;
description: string | null;
updatedAt: Date;
}

View File

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

View File

@ -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<SelfApplyAuthorizationResponseDto> {
// 检查是否已有该市的授权市公司
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<SelfApplyAuthorizationResponseDto> {
// 检查是否已有该省的授权省公司
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(),

View File

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

View File

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

View File

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