diff --git a/packages/services/conversation-service/src/conversation/conversation.controller.ts b/packages/services/conversation-service/src/adapters/inbound/conversation.controller.ts similarity index 91% rename from packages/services/conversation-service/src/conversation/conversation.controller.ts rename to packages/services/conversation-service/src/adapters/inbound/conversation.controller.ts index 010d52d..8081fdf 100644 --- a/packages/services/conversation-service/src/conversation/conversation.controller.ts +++ b/packages/services/conversation-service/src/adapters/inbound/conversation.controller.ts @@ -9,20 +9,8 @@ import { HttpCode, HttpStatus, } from '@nestjs/common'; -import { IsOptional, IsString, IsNotEmpty } from 'class-validator'; -import { ConversationService } from './conversation.service'; - -class CreateConversationDto { - @IsOptional() - @IsString() - title?: string; -} - -class SendMessageDto { - @IsNotEmpty() - @IsString() - content: string; -} +import { ConversationService } from '../../application/services/conversation.service'; +import { CreateConversationDto, SendMessageDto } from '../../application/dtos/conversation.dto'; @Controller('conversations') export class ConversationController { diff --git a/packages/services/conversation-service/src/conversation/conversation.gateway.ts b/packages/services/conversation-service/src/adapters/inbound/conversation.gateway.ts similarity index 93% rename from packages/services/conversation-service/src/conversation/conversation.gateway.ts rename to packages/services/conversation-service/src/adapters/inbound/conversation.gateway.ts index c56349e..b662e15 100644 --- a/packages/services/conversation-service/src/conversation/conversation.gateway.ts +++ b/packages/services/conversation-service/src/adapters/inbound/conversation.gateway.ts @@ -8,17 +8,7 @@ import { MessageBody, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; -import { ConversationService } from './conversation.service'; - -interface FileAttachment { - id: string; - originalName: string; - mimeType: string; - type: 'image' | 'document' | 'audio' | 'video' | 'other'; - size: number; - downloadUrl?: string; - thumbnailUrl?: string; -} +import { ConversationService, FileAttachment } from '../../application/services/conversation.service'; interface SendMessagePayload { conversationId: string; diff --git a/packages/services/conversation-service/src/adapters/inbound/index.ts b/packages/services/conversation-service/src/adapters/inbound/index.ts new file mode 100644 index 0000000..bc19b49 --- /dev/null +++ b/packages/services/conversation-service/src/adapters/inbound/index.ts @@ -0,0 +1,3 @@ +export * from './conversation.controller'; +export * from './conversation.gateway'; +export * from './internal.controller'; diff --git a/packages/services/conversation-service/src/conversation/internal.controller.ts b/packages/services/conversation-service/src/adapters/inbound/internal.controller.ts similarity index 85% rename from packages/services/conversation-service/src/conversation/internal.controller.ts rename to packages/services/conversation-service/src/adapters/inbound/internal.controller.ts index 388dc00..75c767b 100644 --- a/packages/services/conversation-service/src/conversation/internal.controller.ts +++ b/packages/services/conversation-service/src/adapters/inbound/internal.controller.ts @@ -1,8 +1,8 @@ import { Controller, Get, Query, Param } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, MoreThan, LessThan } from 'typeorm'; -import { ConversationEntity } from '../domain/entities/conversation.entity'; -import { MessageEntity } from '../domain/entities/message.entity'; +import { Repository } from 'typeorm'; +import { ConversationORM } from '../../infrastructure/database/postgres/entities/conversation.orm'; +import { MessageORM } from '../../infrastructure/database/postgres/entities/message.orm'; /** * 内部 API - 供其他微服务调用 @@ -11,10 +11,10 @@ import { MessageEntity } from '../domain/entities/message.entity'; @Controller('internal/conversations') export class InternalConversationController { constructor( - @InjectRepository(ConversationEntity) - private conversationRepo: Repository, - @InjectRepository(MessageEntity) - private messageRepo: Repository, + @InjectRepository(ConversationORM) + private conversationRepo: Repository, + @InjectRepository(MessageORM) + private messageRepo: Repository, ) {} /** diff --git a/packages/services/conversation-service/src/infrastructure/database/postgres/conversation-postgres.repository.ts b/packages/services/conversation-service/src/adapters/outbound/persistence/conversation-postgres.repository.ts similarity index 97% rename from packages/services/conversation-service/src/infrastructure/database/postgres/conversation-postgres.repository.ts rename to packages/services/conversation-service/src/adapters/outbound/persistence/conversation-postgres.repository.ts index 4e1e7cd..748bdac 100644 --- a/packages/services/conversation-service/src/infrastructure/database/postgres/conversation-postgres.repository.ts +++ b/packages/services/conversation-service/src/adapters/outbound/persistence/conversation-postgres.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, MoreThan, LessThan } from 'typeorm'; -import { ConversationORM } from './entities/conversation.orm'; +import { Repository } from 'typeorm'; +import { ConversationORM } from '../../../infrastructure/database/postgres/entities/conversation.orm'; import { IConversationRepository } from '../../../domain/repositories/conversation.repository.interface'; import { ConversationEntity, diff --git a/packages/services/conversation-service/src/adapters/outbound/persistence/index.ts b/packages/services/conversation-service/src/adapters/outbound/persistence/index.ts new file mode 100644 index 0000000..a9161e1 --- /dev/null +++ b/packages/services/conversation-service/src/adapters/outbound/persistence/index.ts @@ -0,0 +1,3 @@ +export * from './conversation-postgres.repository'; +export * from './message-postgres.repository'; +export * from './token-usage-postgres.repository'; diff --git a/packages/services/conversation-service/src/infrastructure/database/postgres/message-postgres.repository.ts b/packages/services/conversation-service/src/adapters/outbound/persistence/message-postgres.repository.ts similarity index 95% rename from packages/services/conversation-service/src/infrastructure/database/postgres/message-postgres.repository.ts rename to packages/services/conversation-service/src/adapters/outbound/persistence/message-postgres.repository.ts index 256a18c..1c03ae5 100644 --- a/packages/services/conversation-service/src/infrastructure/database/postgres/message-postgres.repository.ts +++ b/packages/services/conversation-service/src/adapters/outbound/persistence/message-postgres.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { MessageORM } from './entities/message.orm'; +import { MessageORM } from '../../../infrastructure/database/postgres/entities/message.orm'; import { IMessageRepository } from '../../../domain/repositories/message.repository.interface'; import { MessageEntity } from '../../../domain/entities/message.entity'; diff --git a/packages/services/conversation-service/src/infrastructure/database/postgres/token-usage-postgres.repository.ts b/packages/services/conversation-service/src/adapters/outbound/persistence/token-usage-postgres.repository.ts similarity index 97% rename from packages/services/conversation-service/src/infrastructure/database/postgres/token-usage-postgres.repository.ts rename to packages/services/conversation-service/src/adapters/outbound/persistence/token-usage-postgres.repository.ts index 432bb1d..a6604a0 100644 --- a/packages/services/conversation-service/src/infrastructure/database/postgres/token-usage-postgres.repository.ts +++ b/packages/services/conversation-service/src/adapters/outbound/persistence/token-usage-postgres.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { TokenUsageORM } from './entities/token-usage.orm'; +import { TokenUsageORM } from '../../../infrastructure/database/postgres/entities/token-usage.orm'; import { ITokenUsageRepository } from '../../../domain/repositories/token-usage.repository.interface'; import { TokenUsageEntity } from '../../../domain/entities/token-usage.entity'; diff --git a/packages/services/conversation-service/src/application/dtos/conversation.dto.ts b/packages/services/conversation-service/src/application/dtos/conversation.dto.ts new file mode 100644 index 0000000..de91308 --- /dev/null +++ b/packages/services/conversation-service/src/application/dtos/conversation.dto.ts @@ -0,0 +1,30 @@ +import { IsOptional, IsString, IsNotEmpty, IsArray, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class CreateConversationDto { + @IsOptional() + @IsString() + title?: string; +} + +export class FileAttachmentDto { + id: string; + originalName: string; + mimeType: string; + type: 'image' | 'document' | 'audio' | 'video' | 'other'; + size: number; + downloadUrl?: string; + thumbnailUrl?: string; +} + +export class SendMessageDto { + @IsNotEmpty() + @IsString() + content: string; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => FileAttachmentDto) + attachments?: FileAttachmentDto[]; +} diff --git a/packages/services/conversation-service/src/application/dtos/index.ts b/packages/services/conversation-service/src/application/dtos/index.ts new file mode 100644 index 0000000..1c71148 --- /dev/null +++ b/packages/services/conversation-service/src/application/dtos/index.ts @@ -0,0 +1 @@ +export * from './conversation.dto'; diff --git a/packages/services/conversation-service/src/conversation/conversation.service.ts b/packages/services/conversation-service/src/application/services/conversation.service.ts similarity index 84% rename from packages/services/conversation-service/src/conversation/conversation.service.ts rename to packages/services/conversation-service/src/application/services/conversation.service.ts index c1c8082..d391843 100644 --- a/packages/services/conversation-service/src/conversation/conversation.service.ts +++ b/packages/services/conversation-service/src/application/services/conversation.service.ts @@ -3,27 +3,27 @@ import { v4 as uuidv4 } from 'uuid'; import { ConversationEntity, ConversationStatus, -} from '../domain/entities/conversation.entity'; +} from '../../domain/entities/conversation.entity'; import { MessageEntity, MessageRole, MessageType, -} from '../domain/entities/message.entity'; +} from '../../domain/entities/message.entity'; import { IConversationRepository, CONVERSATION_REPOSITORY, -} from '../domain/repositories/conversation.repository.interface'; +} from '../../domain/repositories/conversation.repository.interface'; import { IMessageRepository, MESSAGE_REPOSITORY, -} from '../domain/repositories/message.repository.interface'; +} from '../../domain/repositories/message.repository.interface'; import { ClaudeAgentServiceV2, ConversationContext, StreamChunk, -} from '../infrastructure/claude/claude-agent-v2.service'; +} from '../../infrastructure/claude/claude-agent-v2.service'; -export interface CreateConversationDto { +export interface CreateConversationParams { userId: string; title?: string; } @@ -38,7 +38,7 @@ export interface FileAttachment { thumbnailUrl?: string; } -export interface SendMessageDto { +export interface SendMessageParams { conversationId: string; userId: string; content: string; @@ -58,11 +58,11 @@ export class ConversationService { /** * Create a new conversation */ - async createConversation(dto: CreateConversationDto): Promise { + async createConversation(params: CreateConversationParams): Promise { const conversation = ConversationEntity.create({ id: uuidv4(), - userId: dto.userId, - title: dto.title || '新对话', + userId: params.userId, + title: params.title || '新对话', }); return this.conversationRepo.save(conversation); @@ -107,34 +107,34 @@ export class ConversationService { /** * Send a message and get streaming response */ - async *sendMessage(dto: SendMessageDto): AsyncGenerator { + async *sendMessage(params: SendMessageParams): AsyncGenerator { // Verify conversation exists and belongs to user - const conversation = await this.getConversation(dto.conversationId, dto.userId); + const conversation = await this.getConversation(params.conversationId, params.userId); if (!conversation.isActive()) { throw new Error('Conversation is not active'); } // Save user message with attachments if present - const hasAttachments = dto.attachments && dto.attachments.length > 0; + const hasAttachments = params.attachments && params.attachments.length > 0; const userMessage = MessageEntity.create({ id: uuidv4(), - conversationId: dto.conversationId, + conversationId: params.conversationId, role: MessageRole.USER, type: hasAttachments ? MessageType.TEXT_WITH_ATTACHMENTS : MessageType.TEXT, - content: dto.content, - metadata: hasAttachments ? { attachments: dto.attachments } : undefined, + content: params.content, + metadata: hasAttachments ? { attachments: params.attachments } : undefined, }); await this.messageRepo.save(userMessage); // Get previous messages for context - const previousMessages = await this.messageRepo.findByConversationId(dto.conversationId); + const previousMessages = await this.messageRepo.findByConversationId(params.conversationId); const recentMessages = previousMessages.slice(-20); // Last 20 messages for context // Build context with support for multimodal messages and consulting state (V2) const context: ConversationContext = { - userId: dto.userId, - conversationId: dto.conversationId, + userId: params.userId, + conversationId: params.conversationId, previousMessages: recentMessages.map((m) => { const msg: { role: 'user' | 'assistant'; content: string; attachments?: FileAttachment[] } = { role: m.role as 'user' | 'assistant', @@ -158,9 +158,9 @@ export class ConversationService { // Stream response from Claude (with attachments for multimodal support) for await (const chunk of this.claudeAgentService.sendMessage( - dto.content, + params.content, context, - dto.attachments, + params.attachments, )) { if (chunk.type === 'text' && chunk.content) { fullResponse += chunk.content; @@ -194,7 +194,7 @@ export class ConversationService { // Save assistant response const assistantMessage = MessageEntity.create({ id: uuidv4(), - conversationId: dto.conversationId, + conversationId: params.conversationId, role: MessageRole.ASSISTANT, type: MessageType.TEXT, content: fullResponse, @@ -204,7 +204,7 @@ export class ConversationService { // Update conversation title if first message if (conversation.messageCount === 0) { - const title = await this.generateTitle(dto.content); + const title = await this.generateTitle(params.content); conversation.title = title; await this.conversationRepo.update(conversation); } diff --git a/packages/services/conversation-service/src/application/services/index.ts b/packages/services/conversation-service/src/application/services/index.ts new file mode 100644 index 0000000..1b443fe --- /dev/null +++ b/packages/services/conversation-service/src/application/services/index.ts @@ -0,0 +1 @@ +export * from './conversation.service'; diff --git a/packages/services/conversation-service/src/conversation/conversation.module.ts b/packages/services/conversation-service/src/conversation/conversation.module.ts index 4aefdb5..5aeeda5 100644 --- a/packages/services/conversation-service/src/conversation/conversation.module.ts +++ b/packages/services/conversation-service/src/conversation/conversation.module.ts @@ -3,16 +3,16 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ConversationORM } from '../infrastructure/database/postgres/entities/conversation.orm'; import { MessageORM } from '../infrastructure/database/postgres/entities/message.orm'; import { TokenUsageORM } from '../infrastructure/database/postgres/entities/token-usage.orm'; -import { ConversationPostgresRepository } from '../infrastructure/database/postgres/conversation-postgres.repository'; -import { MessagePostgresRepository } from '../infrastructure/database/postgres/message-postgres.repository'; -import { TokenUsagePostgresRepository } from '../infrastructure/database/postgres/token-usage-postgres.repository'; +import { ConversationPostgresRepository } from '../adapters/outbound/persistence/conversation-postgres.repository'; +import { MessagePostgresRepository } from '../adapters/outbound/persistence/message-postgres.repository'; +import { TokenUsagePostgresRepository } from '../adapters/outbound/persistence/token-usage-postgres.repository'; import { CONVERSATION_REPOSITORY } from '../domain/repositories/conversation.repository.interface'; import { MESSAGE_REPOSITORY } from '../domain/repositories/message.repository.interface'; import { TOKEN_USAGE_REPOSITORY } from '../domain/repositories/token-usage.repository.interface'; -import { ConversationService } from './conversation.service'; -import { ConversationController } from './conversation.controller'; -import { InternalConversationController } from './internal.controller'; -import { ConversationGateway } from './conversation.gateway'; +import { ConversationService } from '../application/services/conversation.service'; +import { ConversationController } from '../adapters/inbound/conversation.controller'; +import { InternalConversationController } from '../adapters/inbound/internal.controller'; +import { ConversationGateway } from '../adapters/inbound/conversation.gateway'; @Module({ imports: [TypeOrmModule.forFeature([ConversationORM, MessageORM, TokenUsageORM])], diff --git a/packages/services/conversation-service/src/infrastructure/claude/claude.module.ts b/packages/services/conversation-service/src/infrastructure/claude/claude.module.ts index 96ef1a4..09c8f20 100644 --- a/packages/services/conversation-service/src/infrastructure/claude/claude.module.ts +++ b/packages/services/conversation-service/src/infrastructure/claude/claude.module.ts @@ -6,7 +6,7 @@ import { ClaudeAgentServiceV2 } from './claude-agent-v2.service'; import { ImmigrationToolsService } from './tools/immigration-tools.service'; import { TokenUsageService } from './token-usage.service'; import { StrategyEngineService } from './strategy/strategy-engine.service'; -import { TokenUsageEntity } from '../../domain/entities/token-usage.entity'; +import { TokenUsageORM } from '../database/postgres/entities/token-usage.orm'; import { KnowledgeModule } from '../knowledge/knowledge.module'; @Global() @@ -14,7 +14,7 @@ import { KnowledgeModule } from '../knowledge/knowledge.module'; imports: [ ConfigModule, KnowledgeModule, - TypeOrmModule.forFeature([TokenUsageEntity]), + TypeOrmModule.forFeature([TokenUsageORM]), ], providers: [ ClaudeAgentService, diff --git a/packages/services/conversation-service/src/infrastructure/claude/token-usage.service.ts b/packages/services/conversation-service/src/infrastructure/claude/token-usage.service.ts index 9a487dc..f7f8d26 100644 --- a/packages/services/conversation-service/src/infrastructure/claude/token-usage.service.ts +++ b/packages/services/conversation-service/src/infrastructure/claude/token-usage.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, Between, MoreThanOrEqual } from 'typeorm'; -import { TokenUsageEntity } from '../../domain/entities/token-usage.entity'; +import { TokenUsageORM } from '../database/postgres/entities/token-usage.orm'; /** * Claude API 定价 (截至 2024年) @@ -65,8 +65,8 @@ export interface UsageStats { @Injectable() export class TokenUsageService { constructor( - @InjectRepository(TokenUsageEntity) - private tokenUsageRepository: Repository, + @InjectRepository(TokenUsageORM) + private tokenUsageRepository: Repository, ) {} /** @@ -95,7 +95,7 @@ export class TokenUsageService { /** * 记录一次 API 调用的 token 使用量 */ - async recordUsage(input: TokenUsageInput): Promise { + async recordUsage(input: TokenUsageInput): Promise { const cacheCreationTokens = input.cacheCreationTokens || 0; const cacheReadTokens = input.cacheReadTokens || 0; const totalTokens = input.inputTokens + input.outputTokens; @@ -203,7 +203,7 @@ export class TokenUsageService { }); // 按日期分组 - const byDate = new Map(); + const byDate = new Map(); for (const record of records) { const date = record.createdAt.toISOString().split('T')[0]; if (!byDate.has(date)) { @@ -256,7 +256,7 @@ export class TokenUsageService { /** * 计算统计数据 */ - private calculateStats(records: TokenUsageEntity[]): UsageStats { + private calculateStats(records: TokenUsageORM[]): UsageStats { if (records.length === 0) { return { totalRequests: 0, diff --git a/packages/services/conversation-service/src/infrastructure/database/postgres/index.ts b/packages/services/conversation-service/src/infrastructure/database/postgres/index.ts index 82f13ef..697510e 100644 --- a/packages/services/conversation-service/src/infrastructure/database/postgres/index.ts +++ b/packages/services/conversation-service/src/infrastructure/database/postgres/index.ts @@ -1,4 +1 @@ export * from './entities'; -export * from './conversation-postgres.repository'; -export * from './message-postgres.repository'; -export * from './token-usage-postgres.repository'; diff --git a/packages/services/evolution-service/src/admin/admin.controller.ts b/packages/services/evolution-service/src/adapters/inbound/admin.controller.ts similarity index 91% rename from packages/services/evolution-service/src/admin/admin.controller.ts rename to packages/services/evolution-service/src/adapters/inbound/admin.controller.ts index c78eb09..6e8aa19 100644 --- a/packages/services/evolution-service/src/admin/admin.controller.ts +++ b/packages/services/evolution-service/src/adapters/inbound/admin.controller.ts @@ -12,42 +12,16 @@ import { UnauthorizedException, ForbiddenException, } from '@nestjs/common'; -import { AdminService, AdminRole, LoginResult } from './admin.service'; - -// ========== DTOs ========== - -class LoginDto { - username: string; - password: string; -} - -class CreateAdminDto { - username: string; - password: string; - name: string; - email?: string; - phone?: string; - role: AdminRole; -} - -class UpdateAdminDto { - name?: string; - email?: string; - phone?: string; - role?: AdminRole; - isActive?: boolean; -} - -class ChangePasswordDto { - oldPassword: string; - newPassword: string; -} - -class ResetPasswordDto { - newPassword: string; -} - -// ========== Controller ========== +import { AdminService } from '../../application/services/admin.service'; +import { AdminRole } from '../../domain/value-objects/admin-role.enum'; +import { + LoginDto, + CreateAdminDto, + UpdateAdminDto, + ChangePasswordDto, + ResetPasswordDto, + LoginResult, +} from '../../application/dtos/admin.dto'; @Controller('admin') export class AdminController { diff --git a/packages/services/evolution-service/src/evolution/evolution.controller.ts b/packages/services/evolution-service/src/adapters/inbound/evolution.controller.ts similarity index 80% rename from packages/services/evolution-service/src/evolution/evolution.controller.ts rename to packages/services/evolution-service/src/adapters/inbound/evolution.controller.ts index 8cb54e0..862b0c7 100644 --- a/packages/services/evolution-service/src/evolution/evolution.controller.ts +++ b/packages/services/evolution-service/src/adapters/inbound/evolution.controller.ts @@ -3,21 +3,11 @@ import { Get, Post, Body, - Query, HttpCode, HttpStatus, } from '@nestjs/common'; -import { EvolutionService } from './evolution.service'; - -// ========== DTOs ========== - -class RunEvolutionTaskDto { - hoursBack?: number; - limit?: number; - minMessageCount?: number; -} - -// ========== Controller ========== +import { EvolutionService } from '../../application/services/evolution.service'; +import { RunEvolutionTaskDto } from '../../application/dtos/evolution.dto'; @Controller('evolution') export class EvolutionController { diff --git a/packages/services/evolution-service/src/adapters/inbound/index.ts b/packages/services/evolution-service/src/adapters/inbound/index.ts new file mode 100644 index 0000000..7687bbc --- /dev/null +++ b/packages/services/evolution-service/src/adapters/inbound/index.ts @@ -0,0 +1,2 @@ +export * from './evolution.controller'; +export * from './admin.controller'; diff --git a/packages/services/evolution-service/src/infrastructure/clients/conversation.client.ts b/packages/services/evolution-service/src/adapters/outbound/clients/conversation.client.ts similarity index 100% rename from packages/services/evolution-service/src/infrastructure/clients/conversation.client.ts rename to packages/services/evolution-service/src/adapters/outbound/clients/conversation.client.ts diff --git a/packages/services/evolution-service/src/adapters/outbound/clients/index.ts b/packages/services/evolution-service/src/adapters/outbound/clients/index.ts new file mode 100644 index 0000000..1e39868 --- /dev/null +++ b/packages/services/evolution-service/src/adapters/outbound/clients/index.ts @@ -0,0 +1,2 @@ +export * from './conversation.client'; +export * from './knowledge.client'; diff --git a/packages/services/evolution-service/src/infrastructure/clients/knowledge.client.ts b/packages/services/evolution-service/src/adapters/outbound/clients/knowledge.client.ts similarity index 100% rename from packages/services/evolution-service/src/infrastructure/clients/knowledge.client.ts rename to packages/services/evolution-service/src/adapters/outbound/clients/knowledge.client.ts diff --git a/packages/services/evolution-service/src/adapters/outbound/persistence/admin-postgres.repository.ts b/packages/services/evolution-service/src/adapters/outbound/persistence/admin-postgres.repository.ts new file mode 100644 index 0000000..ef212d7 --- /dev/null +++ b/packages/services/evolution-service/src/adapters/outbound/persistence/admin-postgres.repository.ts @@ -0,0 +1,124 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { IAdminRepository } from '../../../domain/repositories/admin.repository.interface'; +import { AdminEntity } from '../../../domain/entities/admin.entity'; +import { AdminRole } from '../../../domain/value-objects/admin-role.enum'; +import { AdminORM } from '../../../infrastructure/database/postgres/entities/admin.orm'; + +@Injectable() +export class AdminPostgresRepository implements IAdminRepository { + constructor( + @InjectRepository(AdminORM) + private adminRepo: Repository, + ) {} + + async save(admin: AdminEntity): Promise { + const orm = this.toORM(admin); + await this.adminRepo.save(orm); + } + + async findById(id: string): Promise { + const orm = await this.adminRepo.findOne({ where: { id } }); + return orm ? this.toEntity(orm) : null; + } + + async findByUsername(username: string): Promise { + const orm = await this.adminRepo.findOne({ where: { username } }); + return orm ? this.toEntity(orm) : null; + } + + async findAll(options?: { + role?: AdminRole; + isActive?: boolean; + limit?: number; + offset?: number; + }): Promise { + const query = this.adminRepo.createQueryBuilder('admin'); + + if (options?.role) { + query.andWhere('admin.role = :role', { role: options.role }); + } + + if (options?.isActive !== undefined) { + query.andWhere('admin.isActive = :active', { active: options.isActive }); + } + + query.orderBy('admin.createdAt', 'DESC'); + + if (options?.limit) { + query.take(options.limit); + } + + if (options?.offset) { + query.skip(options.offset); + } + + const orms = await query.getMany(); + return orms.map(orm => this.toEntity(orm)); + } + + async count(options?: { + role?: AdminRole; + isActive?: boolean; + }): Promise { + const query = this.adminRepo.createQueryBuilder('admin'); + + if (options?.role) { + query.andWhere('admin.role = :role', { role: options.role }); + } + + if (options?.isActive !== undefined) { + query.andWhere('admin.isActive = :active', { active: options.isActive }); + } + + return query.getCount(); + } + + async update(admin: AdminEntity): Promise { + const orm = this.toORM(admin); + await this.adminRepo.save(orm); + } + + async delete(id: string): Promise { + await this.adminRepo.delete(id); + } + + private toORM(entity: AdminEntity): AdminORM { + const orm = new AdminORM(); + orm.id = entity.id; + orm.username = entity.username; + orm.passwordHash = entity.passwordHash; + orm.name = entity.name; + orm.email = entity.email; + orm.phone = entity.phone; + orm.role = entity.role; + orm.permissions = entity.permissions; + orm.avatar = entity.avatar; + orm.lastLoginAt = entity.lastLoginAt; + orm.lastLoginIp = entity.lastLoginIp; + orm.isActive = entity.isActive; + orm.createdAt = entity.createdAt; + orm.updatedAt = entity.updatedAt; + return orm; + } + + private toEntity(orm: AdminORM): AdminEntity { + return AdminEntity.fromPersistence({ + id: orm.id, + username: orm.username, + passwordHash: orm.passwordHash, + name: orm.name, + email: orm.email, + phone: orm.phone, + role: orm.role as AdminRole, + permissions: orm.permissions, + avatar: orm.avatar, + lastLoginAt: orm.lastLoginAt, + lastLoginIp: orm.lastLoginIp, + isActive: orm.isActive, + createdAt: orm.createdAt, + updatedAt: orm.updatedAt, + }); + } +} diff --git a/packages/services/evolution-service/src/adapters/outbound/persistence/index.ts b/packages/services/evolution-service/src/adapters/outbound/persistence/index.ts new file mode 100644 index 0000000..b078170 --- /dev/null +++ b/packages/services/evolution-service/src/adapters/outbound/persistence/index.ts @@ -0,0 +1 @@ +export * from './admin-postgres.repository'; diff --git a/packages/services/evolution-service/src/admin/admin.module.ts b/packages/services/evolution-service/src/admin/admin.module.ts index 73bf56c..4422716 100644 --- a/packages/services/evolution-service/src/admin/admin.module.ts +++ b/packages/services/evolution-service/src/admin/admin.module.ts @@ -1,15 +1,23 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { AdminController } from './admin.controller'; -import { AdminService } from './admin.service'; -import { AdminORM } from '../infrastructure/database/entities/admin.orm'; +import { AdminController } from '../adapters/inbound/admin.controller'; +import { AdminService } from '../application/services/admin.service'; +import { AdminPostgresRepository } from '../adapters/outbound/persistence/admin-postgres.repository'; +import { AdminORM } from '../infrastructure/database/postgres/entities/admin.orm'; +import { ADMIN_REPOSITORY } from '../domain/repositories/admin.repository.interface'; @Module({ imports: [ TypeOrmModule.forFeature([AdminORM]), ], controllers: [AdminController], - providers: [AdminService], + providers: [ + AdminService, + { + provide: ADMIN_REPOSITORY, + useClass: AdminPostgresRepository, + }, + ], exports: [AdminService], }) export class AdminModule {} diff --git a/packages/services/evolution-service/src/application/dtos/admin.dto.ts b/packages/services/evolution-service/src/application/dtos/admin.dto.ts new file mode 100644 index 0000000..83deafc --- /dev/null +++ b/packages/services/evolution-service/src/application/dtos/admin.dto.ts @@ -0,0 +1,44 @@ +import { AdminRole } from '../../domain/value-objects/admin-role.enum'; + +export class LoginDto { + username: string; + password: string; +} + +export class CreateAdminDto { + username: string; + password: string; + name: string; + email?: string; + phone?: string; + role: AdminRole; +} + +export class UpdateAdminDto { + name?: string; + email?: string; + phone?: string; + role?: AdminRole; + isActive?: boolean; +} + +export class ChangePasswordDto { + oldPassword: string; + newPassword: string; +} + +export class ResetPasswordDto { + newPassword: string; +} + +export interface LoginResult { + admin: { + id: string; + username: string; + name: string; + role: string; + permissions: string[]; + }; + token: string; + expiresIn: number; +} diff --git a/packages/services/evolution-service/src/application/dtos/evolution.dto.ts b/packages/services/evolution-service/src/application/dtos/evolution.dto.ts new file mode 100644 index 0000000..3d85f5e --- /dev/null +++ b/packages/services/evolution-service/src/application/dtos/evolution.dto.ts @@ -0,0 +1,14 @@ +export class RunEvolutionTaskDto { + hoursBack?: number; + limit?: number; + minMessageCount?: number; +} + +export interface EvolutionTaskResult { + taskId: string; + status: 'success' | 'partial' | 'failed'; + conversationsAnalyzed: number; + experiencesExtracted: number; + knowledgeGapsFound: number; + errors: string[]; +} diff --git a/packages/services/evolution-service/src/application/dtos/index.ts b/packages/services/evolution-service/src/application/dtos/index.ts new file mode 100644 index 0000000..3ce0366 --- /dev/null +++ b/packages/services/evolution-service/src/application/dtos/index.ts @@ -0,0 +1,2 @@ +export * from './evolution.dto'; +export * from './admin.dto'; diff --git a/packages/services/evolution-service/src/admin/admin.service.ts b/packages/services/evolution-service/src/application/services/admin.service.ts similarity index 57% rename from packages/services/evolution-service/src/admin/admin.service.ts rename to packages/services/evolution-service/src/application/services/admin.service.ts index fe9e436..3195dee 100644 --- a/packages/services/evolution-service/src/admin/admin.service.ts +++ b/packages/services/evolution-service/src/application/services/admin.service.ts @@ -1,68 +1,11 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { Injectable, Inject } from '@nestjs/common'; import * as bcrypt from 'bcrypt'; import * as jwt from 'jsonwebtoken'; import { ConfigService } from '@nestjs/config'; -import { AdminORM } from '../infrastructure/database/entities/admin.orm'; -import { v4 as uuidv4 } from 'uuid'; - -/** - * 管理员角色 - */ -export enum AdminRole { - SUPER_ADMIN = 'SUPER_ADMIN', - ADMIN = 'ADMIN', - OPERATOR = 'OPERATOR', - VIEWER = 'VIEWER', -} - -/** - * 角色权限映射 - */ -const ROLE_PERMISSIONS: Record = { - [AdminRole.SUPER_ADMIN]: ['*'], - [AdminRole.ADMIN]: [ - 'knowledge:*', - 'experience:*', - 'user:read', - 'conversation:read', - 'statistics:*', - 'admin:read', - ], - [AdminRole.OPERATOR]: [ - 'knowledge:read', - 'knowledge:create', - 'knowledge:update', - 'experience:read', - 'experience:approve', - 'user:read', - 'conversation:read', - 'statistics:read', - ], - [AdminRole.VIEWER]: [ - 'knowledge:read', - 'experience:read', - 'user:read', - 'conversation:read', - 'statistics:read', - ], -}; - -/** - * 登录结果 - */ -export interface LoginResult { - admin: { - id: string; - username: string; - name: string; - role: string; - permissions: string[]; - }; - token: string; - expiresIn: number; -} +import { AdminEntity } from '../../domain/entities/admin.entity'; +import { AdminRole, ROLE_PERMISSIONS } from '../../domain/value-objects/admin-role.enum'; +import { IAdminRepository, ADMIN_REPOSITORY } from '../../domain/repositories/admin.repository.interface'; +import { LoginResult } from '../dtos/admin.dto'; /** * 管理员服务 @@ -73,8 +16,8 @@ export class AdminService { private readonly jwtExpiresIn: number = 24 * 60 * 60; // 24小时 constructor( - @InjectRepository(AdminORM) - private adminRepo: Repository, + @Inject(ADMIN_REPOSITORY) + private adminRepo: IAdminRepository, private configService: ConfigService, ) { this.jwtSecret = this.configService.get('JWT_SECRET') || 'iconsulting-secret-key'; @@ -84,7 +27,7 @@ export class AdminService { * 管理员登录 */ async login(username: string, password: string, ip?: string): Promise { - const admin = await this.adminRepo.findOne({ where: { username } }); + const admin = await this.adminRepo.findByUsername(username); if (!admin || !admin.isActive) { throw new Error('用户名或密码错误'); @@ -96,9 +39,8 @@ export class AdminService { } // 更新登录信息 - admin.lastLoginAt = new Date(); - admin.lastLoginIp = ip; - await this.adminRepo.save(admin); + admin.recordLogin(ip); + await this.adminRepo.update(admin); // 生成Token const token = jwt.sign( @@ -112,7 +54,7 @@ export class AdminService { ); // 获取权限 - const permissions = this.getPermissions(admin.role as AdminRole, admin.permissions); + const permissions = admin.getPermissions(); return { admin: { @@ -146,12 +88,12 @@ export class AdminService { role: string; }; - const admin = await this.adminRepo.findOne({ where: { id: decoded.sub } }); + const admin = await this.adminRepo.findById(decoded.sub); if (!admin || !admin.isActive) { return { valid: false }; } - const permissions = this.getPermissions(admin.role as AdminRole, admin.permissions); + const permissions = admin.getPermissions(); return { valid: true, @@ -177,11 +119,9 @@ export class AdminService { email?: string; phone?: string; role: AdminRole; - }): Promise { + }): Promise { // 检查用户名是否存在 - const existing = await this.adminRepo.findOne({ - where: { username: params.username }, - }); + const existing = await this.adminRepo.findByUsername(params.username); if (existing) { throw new Error('用户名已存在'); } @@ -189,16 +129,13 @@ export class AdminService { // 加密密码 const passwordHash = await bcrypt.hash(params.password, 10); - const admin = this.adminRepo.create({ - id: uuidv4(), + const admin = AdminEntity.create({ username: params.username, passwordHash, name: params.name, email: params.email, phone: params.phone, role: params.role, - permissions: ROLE_PERMISSIONS[params.role], - isActive: true, }); await this.adminRepo.save(admin); @@ -215,28 +152,24 @@ export class AdminService { page?: number; pageSize?: number; }): Promise<{ - items: AdminORM[]; + items: AdminEntity[]; total: number; }> { const page = options?.page || 1; const pageSize = options?.pageSize || 20; - const query = this.adminRepo.createQueryBuilder('admin'); - - if (options?.role) { - query.andWhere('admin.role = :role', { role: options.role }); - } - - if (options?.isActive !== undefined) { - query.andWhere('admin.isActive = :active', { active: options.isActive }); - } - - query.orderBy('admin.createdAt', 'DESC'); - - const [items, total] = await query - .skip((page - 1) * pageSize) - .take(pageSize) - .getManyAndCount(); + const [items, total] = await Promise.all([ + this.adminRepo.findAll({ + role: options?.role, + isActive: options?.isActive, + limit: pageSize, + offset: (page - 1) * pageSize, + }), + this.adminRepo.count({ + role: options?.role, + isActive: options?.isActive, + }), + ]); return { items, total }; } @@ -253,8 +186,8 @@ export class AdminService { role?: AdminRole; isActive?: boolean; }, - ): Promise { - const admin = await this.adminRepo.findOne({ where: { id: adminId } }); + ): Promise { + const admin = await this.adminRepo.findById(adminId); if (!admin) { throw new Error('管理员不存在'); } @@ -262,13 +195,10 @@ export class AdminService { if (params.name) admin.name = params.name; if (params.email !== undefined) admin.email = params.email; if (params.phone !== undefined) admin.phone = params.phone; - if (params.role) { - admin.role = params.role; - admin.permissions = ROLE_PERMISSIONS[params.role]; - } + if (params.role) admin.updateRole(params.role); if (params.isActive !== undefined) admin.isActive = params.isActive; - await this.adminRepo.save(admin); + await this.adminRepo.update(admin); return admin; } @@ -281,7 +211,7 @@ export class AdminService { oldPassword: string, newPassword: string, ): Promise { - const admin = await this.adminRepo.findOne({ where: { id: adminId } }); + const admin = await this.adminRepo.findById(adminId); if (!admin) { throw new Error('管理员不存在'); } @@ -292,20 +222,20 @@ export class AdminService { } admin.passwordHash = await bcrypt.hash(newPassword, 10); - await this.adminRepo.save(admin); + await this.adminRepo.update(admin); } /** * 重置密码(超管功能) */ async resetPassword(adminId: string, newPassword: string): Promise { - const admin = await this.adminRepo.findOne({ where: { id: adminId } }); + const admin = await this.adminRepo.findById(adminId); if (!admin) { throw new Error('管理员不存在'); } admin.passwordHash = await bcrypt.hash(newPassword, 10); - await this.adminRepo.save(admin); + await this.adminRepo.update(admin); } /** @@ -323,22 +253,11 @@ export class AdminService { } // 通配符匹配 (如 knowledge:* 匹配 knowledge:read) - const [resource, action] = requiredPermission.split(':'); + const [resource] = requiredPermission.split(':'); if (adminPermissions.includes(`${resource}:*`)) { return true; } return false; } - - /** - * 获取管理员权限列表 - */ - private getPermissions(role: AdminRole, customPermissions?: string[]): string[] { - const rolePermissions = ROLE_PERMISSIONS[role] || []; - if (customPermissions && customPermissions.length > 0) { - return [...new Set([...rolePermissions, ...customPermissions])]; - } - return rolePermissions; - } } diff --git a/packages/services/evolution-service/src/evolution/evolution.service.ts b/packages/services/evolution-service/src/application/services/evolution.service.ts similarity index 93% rename from packages/services/evolution-service/src/evolution/evolution.service.ts rename to packages/services/evolution-service/src/application/services/evolution.service.ts index 3496cbf..36299c3 100644 --- a/packages/services/evolution-service/src/evolution/evolution.service.ts +++ b/packages/services/evolution-service/src/application/services/evolution.service.ts @@ -1,20 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { ExperienceExtractorService } from '../infrastructure/claude/experience-extractor.service'; -import { ConversationClient } from '../infrastructure/clients/conversation.client'; -import { KnowledgeClient } from '../infrastructure/clients/knowledge.client'; +import { ExperienceExtractorService } from '../../infrastructure/claude/experience-extractor.service'; +import { ConversationClient } from '../../adapters/outbound/clients/conversation.client'; +import { KnowledgeClient } from '../../adapters/outbound/clients/knowledge.client'; import { v4 as uuidv4 } from 'uuid'; - -/** - * 进化任务结果 - */ -export interface EvolutionTaskResult { - taskId: string; - status: 'success' | 'partial' | 'failed'; - conversationsAnalyzed: number; - experiencesExtracted: number; - knowledgeGapsFound: number; - errors: string[]; -} +import { EvolutionTaskResult } from '../dtos/evolution.dto'; /** * 进化服务 diff --git a/packages/services/evolution-service/src/application/services/index.ts b/packages/services/evolution-service/src/application/services/index.ts new file mode 100644 index 0000000..fa89a77 --- /dev/null +++ b/packages/services/evolution-service/src/application/services/index.ts @@ -0,0 +1,2 @@ +export * from './evolution.service'; +export * from './admin.service'; diff --git a/packages/services/evolution-service/src/domain/entities/admin.entity.ts b/packages/services/evolution-service/src/domain/entities/admin.entity.ts new file mode 100644 index 0000000..e7ddd94 --- /dev/null +++ b/packages/services/evolution-service/src/domain/entities/admin.entity.ts @@ -0,0 +1,132 @@ +import { v4 as uuidv4 } from 'uuid'; +import { AdminRole, ROLE_PERMISSIONS } from '../value-objects/admin-role.enum'; + +export interface AdminProps { + id: string; + username: string; + passwordHash: string; + name: string; + email?: string; + phone?: string; + role: AdminRole; + permissions: string[]; + avatar?: string; + lastLoginAt?: Date; + lastLoginIp?: string; + isActive: boolean; + createdAt: Date; + updatedAt: Date; +} + +export class AdminEntity { + private constructor(private props: AdminProps) {} + + // Getters + get id(): string { return this.props.id; } + get username(): string { return this.props.username; } + get passwordHash(): string { return this.props.passwordHash; } + get name(): string { return this.props.name; } + get email(): string | undefined { return this.props.email; } + get phone(): string | undefined { return this.props.phone; } + get role(): AdminRole { return this.props.role; } + get permissions(): string[] { return this.props.permissions; } + get avatar(): string | undefined { return this.props.avatar; } + get lastLoginAt(): Date | undefined { return this.props.lastLoginAt; } + get lastLoginIp(): string | undefined { return this.props.lastLoginIp; } + get isActive(): boolean { return this.props.isActive; } + get createdAt(): Date { return this.props.createdAt; } + get updatedAt(): Date { return this.props.updatedAt; } + + // Setters + set name(value: string) { this.props.name = value; this.props.updatedAt = new Date(); } + set email(value: string | undefined) { this.props.email = value; this.props.updatedAt = new Date(); } + set phone(value: string | undefined) { this.props.phone = value; this.props.updatedAt = new Date(); } + set isActive(value: boolean) { this.props.isActive = value; this.props.updatedAt = new Date(); } + set passwordHash(value: string) { this.props.passwordHash = value; this.props.updatedAt = new Date(); } + + /** + * 创建新管理员 + */ + static create(params: { + username: string; + passwordHash: string; + name: string; + email?: string; + phone?: string; + role: AdminRole; + }): AdminEntity { + const now = new Date(); + return new AdminEntity({ + id: uuidv4(), + username: params.username, + passwordHash: params.passwordHash, + name: params.name, + email: params.email, + phone: params.phone, + role: params.role, + permissions: ROLE_PERMISSIONS[params.role], + isActive: true, + createdAt: now, + updatedAt: now, + }); + } + + /** + * 从持久化数据恢复实体 + */ + static fromPersistence(props: AdminProps): AdminEntity { + return new AdminEntity(props); + } + + /** + * 更新角色 + */ + updateRole(role: AdminRole): void { + this.props.role = role; + this.props.permissions = ROLE_PERMISSIONS[role]; + this.props.updatedAt = new Date(); + } + + /** + * 记录登录 + */ + recordLogin(ip?: string): void { + this.props.lastLoginAt = new Date(); + this.props.lastLoginIp = ip; + this.props.updatedAt = new Date(); + } + + /** + * 获取完整权限列表 + */ + getPermissions(customPermissions?: string[]): string[] { + const rolePermissions = ROLE_PERMISSIONS[this.props.role] || []; + if (customPermissions && customPermissions.length > 0) { + return [...new Set([...rolePermissions, ...customPermissions])]; + } + return rolePermissions; + } + + /** + * 检查是否有指定权限 + */ + hasPermission(requiredPermission: string): boolean { + // 超管拥有所有权限 + if (this.props.permissions.includes('*')) { + return true; + } + + // 完全匹配 + if (this.props.permissions.includes(requiredPermission)) { + return true; + } + + // 通配符匹配 (如 knowledge:* 匹配 knowledge:read) + const [resource] = requiredPermission.split(':'); + if (this.props.permissions.includes(`${resource}:*`)) { + return true; + } + + return false; + } +} diff --git a/packages/services/evolution-service/src/domain/repositories/admin.repository.interface.ts b/packages/services/evolution-service/src/domain/repositories/admin.repository.interface.ts new file mode 100644 index 0000000..0bc756f --- /dev/null +++ b/packages/services/evolution-service/src/domain/repositories/admin.repository.interface.ts @@ -0,0 +1,22 @@ +import { AdminEntity } from '../entities/admin.entity'; +import { AdminRole } from '../value-objects/admin-role.enum'; + +export interface IAdminRepository { + save(admin: AdminEntity): Promise; + findById(id: string): Promise; + findByUsername(username: string): Promise; + findAll(options?: { + role?: AdminRole; + isActive?: boolean; + limit?: number; + offset?: number; + }): Promise; + count(options?: { + role?: AdminRole; + isActive?: boolean; + }): Promise; + update(admin: AdminEntity): Promise; + delete(id: string): Promise; +} + +export const ADMIN_REPOSITORY = Symbol('IAdminRepository'); diff --git a/packages/services/evolution-service/src/domain/repositories/index.ts b/packages/services/evolution-service/src/domain/repositories/index.ts new file mode 100644 index 0000000..acb4fa3 --- /dev/null +++ b/packages/services/evolution-service/src/domain/repositories/index.ts @@ -0,0 +1 @@ +export * from './admin.repository.interface'; diff --git a/packages/services/evolution-service/src/domain/value-objects/admin-role.enum.ts b/packages/services/evolution-service/src/domain/value-objects/admin-role.enum.ts new file mode 100644 index 0000000..70ff8fe --- /dev/null +++ b/packages/services/evolution-service/src/domain/value-objects/admin-role.enum.ts @@ -0,0 +1,41 @@ +/** + * 管理员角色 + */ +export enum AdminRole { + SUPER_ADMIN = 'SUPER_ADMIN', + ADMIN = 'ADMIN', + OPERATOR = 'OPERATOR', + VIEWER = 'VIEWER', +} + +/** + * 角色权限映射 + */ +export const ROLE_PERMISSIONS: Record = { + [AdminRole.SUPER_ADMIN]: ['*'], + [AdminRole.ADMIN]: [ + 'knowledge:*', + 'experience:*', + 'user:read', + 'conversation:read', + 'statistics:*', + 'admin:read', + ], + [AdminRole.OPERATOR]: [ + 'knowledge:read', + 'knowledge:create', + 'knowledge:update', + 'experience:read', + 'experience:approve', + 'user:read', + 'conversation:read', + 'statistics:read', + ], + [AdminRole.VIEWER]: [ + 'knowledge:read', + 'experience:read', + 'user:read', + 'conversation:read', + 'statistics:read', + ], +}; diff --git a/packages/services/evolution-service/src/evolution/evolution.module.ts b/packages/services/evolution-service/src/evolution/evolution.module.ts index b70f31f..c484504 100644 --- a/packages/services/evolution-service/src/evolution/evolution.module.ts +++ b/packages/services/evolution-service/src/evolution/evolution.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common'; -import { EvolutionController } from './evolution.controller'; -import { EvolutionService } from './evolution.service'; +import { EvolutionController } from '../adapters/inbound/evolution.controller'; +import { EvolutionService } from '../application/services/evolution.service'; import { ExperienceExtractorService } from '../infrastructure/claude/experience-extractor.service'; -import { ConversationClient } from '../infrastructure/clients/conversation.client'; -import { KnowledgeClient } from '../infrastructure/clients/knowledge.client'; +import { ConversationClient } from '../adapters/outbound/clients/conversation.client'; +import { KnowledgeClient } from '../adapters/outbound/clients/knowledge.client'; @Module({ imports: [], diff --git a/packages/services/evolution-service/src/infrastructure/database/entities/admin.orm.ts b/packages/services/evolution-service/src/infrastructure/database/postgres/entities/admin.orm.ts similarity index 92% rename from packages/services/evolution-service/src/infrastructure/database/entities/admin.orm.ts rename to packages/services/evolution-service/src/infrastructure/database/postgres/entities/admin.orm.ts index d40c835..7d098f1 100644 --- a/packages/services/evolution-service/src/infrastructure/database/entities/admin.orm.ts +++ b/packages/services/evolution-service/src/infrastructure/database/postgres/entities/admin.orm.ts @@ -21,10 +21,10 @@ export class AdminORM { name: string; @Column({ length: 255, nullable: true }) - email: string; + email?: string; @Column({ length: 20, nullable: true }) - phone: string; + phone?: string; @Column({ length: 20, default: 'OPERATOR' }) role: string; @@ -33,10 +33,10 @@ export class AdminORM { permissions: string[]; @Column({ length: 500, nullable: true }) - avatar: string; + avatar?: string; @Column({ name: 'last_login_at', nullable: true }) - lastLoginAt: Date; + lastLoginAt?: Date; @Column({ name: 'last_login_ip', length: 50, nullable: true }) lastLoginIp?: string; diff --git a/packages/services/file-service/src/file/file.controller.ts b/packages/services/file-service/src/adapters/inbound/file.controller.ts similarity index 95% rename from packages/services/file-service/src/file/file.controller.ts rename to packages/services/file-service/src/adapters/inbound/file.controller.ts index c492df9..3a2459d 100644 --- a/packages/services/file-service/src/file/file.controller.ts +++ b/packages/services/file-service/src/adapters/inbound/file.controller.ts @@ -14,13 +14,13 @@ import { HttpStatus, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; -import { FileService } from './file.service'; +import { FileService } from '../../application/services/file.service'; import { UploadFileDto, PresignedUrlDto, FileResponseDto, PresignedUrlResponseDto, -} from './dto/upload-file.dto'; +} from '../../application/dtos/upload-file.dto'; @Controller('files') export class FileController { diff --git a/packages/services/file-service/src/adapters/inbound/index.ts b/packages/services/file-service/src/adapters/inbound/index.ts new file mode 100644 index 0000000..c28de2a --- /dev/null +++ b/packages/services/file-service/src/adapters/inbound/index.ts @@ -0,0 +1 @@ +export * from './file.controller'; diff --git a/packages/services/file-service/src/infrastructure/database/postgres/file-postgres.repository.ts b/packages/services/file-service/src/adapters/outbound/persistence/file-postgres.repository.ts similarity index 97% rename from packages/services/file-service/src/infrastructure/database/postgres/file-postgres.repository.ts rename to packages/services/file-service/src/adapters/outbound/persistence/file-postgres.repository.ts index cd24f07..96e4e3c 100644 --- a/packages/services/file-service/src/infrastructure/database/postgres/file-postgres.repository.ts +++ b/packages/services/file-service/src/adapters/outbound/persistence/file-postgres.repository.ts @@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { IFileRepository } from '../../../domain/repositories/file.repository.interface'; import { FileEntity, FileStatus } from '../../../domain/entities/file.entity'; -import { FileORM } from './entities/file.orm'; +import { FileORM } from '../../../infrastructure/database/postgres/entities/file.orm'; @Injectable() export class FilePostgresRepository implements IFileRepository { diff --git a/packages/services/file-service/src/adapters/outbound/persistence/index.ts b/packages/services/file-service/src/adapters/outbound/persistence/index.ts new file mode 100644 index 0000000..a01e1fb --- /dev/null +++ b/packages/services/file-service/src/adapters/outbound/persistence/index.ts @@ -0,0 +1 @@ +export * from './file-postgres.repository'; diff --git a/packages/services/file-service/src/adapters/outbound/storage/index.ts b/packages/services/file-service/src/adapters/outbound/storage/index.ts new file mode 100644 index 0000000..13b9b25 --- /dev/null +++ b/packages/services/file-service/src/adapters/outbound/storage/index.ts @@ -0,0 +1 @@ +export * from './minio-storage.adapter'; diff --git a/packages/services/file-service/src/minio/minio.service.ts b/packages/services/file-service/src/adapters/outbound/storage/minio-storage.adapter.ts similarity index 96% rename from packages/services/file-service/src/minio/minio.service.ts rename to packages/services/file-service/src/adapters/outbound/storage/minio-storage.adapter.ts index 7053425..7827e99 100644 --- a/packages/services/file-service/src/minio/minio.service.ts +++ b/packages/services/file-service/src/adapters/outbound/storage/minio-storage.adapter.ts @@ -3,8 +3,8 @@ import { ConfigService } from '@nestjs/config'; import * as Minio from 'minio'; @Injectable() -export class MinioService implements OnModuleInit { - private readonly logger = new Logger(MinioService.name); +export class MinioStorageAdapter implements OnModuleInit { + private readonly logger = new Logger(MinioStorageAdapter.name); private client: Minio.Client; private bucketName: string; diff --git a/packages/services/file-service/src/app.module.ts b/packages/services/file-service/src/app.module.ts index 45ed978..de077cc 100644 --- a/packages/services/file-service/src/app.module.ts +++ b/packages/services/file-service/src/app.module.ts @@ -3,7 +3,6 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { HealthModule } from './health/health.module'; import { FileModule } from './file/file.module'; -import { MinioModule } from './minio/minio.module'; @Module({ imports: [ @@ -33,9 +32,6 @@ import { MinioModule } from './minio/minio.module'; // Health check HealthModule, - // MinIO storage - MinioModule, - // 功能模块 FileModule, ], diff --git a/packages/services/file-service/src/application/dtos/index.ts b/packages/services/file-service/src/application/dtos/index.ts new file mode 100644 index 0000000..f49c3fe --- /dev/null +++ b/packages/services/file-service/src/application/dtos/index.ts @@ -0,0 +1 @@ +export * from './upload-file.dto'; diff --git a/packages/services/file-service/src/file/dto/upload-file.dto.ts b/packages/services/file-service/src/application/dtos/upload-file.dto.ts similarity index 100% rename from packages/services/file-service/src/file/dto/upload-file.dto.ts rename to packages/services/file-service/src/application/dtos/upload-file.dto.ts diff --git a/packages/services/file-service/src/file/file.service.ts b/packages/services/file-service/src/application/services/file.service.ts similarity index 90% rename from packages/services/file-service/src/file/file.service.ts rename to packages/services/file-service/src/application/services/file.service.ts index 4317436..7ae1b18 100644 --- a/packages/services/file-service/src/file/file.service.ts +++ b/packages/services/file-service/src/application/services/file.service.ts @@ -7,14 +7,14 @@ import { } from '@nestjs/common'; import { v4 as uuidv4 } from 'uuid'; import * as mimeTypes from 'mime-types'; -import { FileEntity, FileType, FileStatus } from '../domain/entities/file.entity'; -import { IFileRepository, FILE_REPOSITORY } from '../domain/repositories/file.repository.interface'; -import { MinioService } from '../minio/minio.service'; +import { FileEntity, FileType, FileStatus } from '../../domain/entities/file.entity'; +import { IFileRepository, FILE_REPOSITORY } from '../../domain/repositories/file.repository.interface'; +import { MinioStorageAdapter } from '../../adapters/outbound/storage/minio-storage.adapter'; import { FileResponseDto, PresignedUrlDto, PresignedUrlResponseDto, -} from './dto/upload-file.dto'; +} from '../dtos/upload-file.dto'; // 允许的文件类型 const ALLOWED_IMAGE_TYPES = [ @@ -45,7 +45,7 @@ export class FileService { constructor( @Inject(FILE_REPOSITORY) private readonly fileRepo: IFileRepository, - private readonly minioService: MinioService, + private readonly storageAdapter: MinioStorageAdapter, ) {} /** @@ -87,7 +87,7 @@ export class FileService { // 获取预签名 URL (有效期 1 小时) const expiresIn = 3600; - const uploadUrl = await this.minioService.getPresignedPutUrl( + const uploadUrl = await this.storageAdapter.getPresignedPutUrl( objectName, expiresIn, ); @@ -162,7 +162,7 @@ export class FileService { const objectName = `uploads/${fileType}s/${datePath}/${userId}/${fileId}.${extension}`; // 上传到 MinIO - await this.minioService.uploadFile(objectName, buffer, mimetype, { + await this.storageAdapter.uploadFile(objectName, buffer, mimetype, { 'x-amz-meta-original-name': encodeURIComponent(originalname), 'x-amz-meta-user-id': userId, }); @@ -218,7 +218,7 @@ export class FileService { throw new NotFoundException('File not found'); } - return this.minioService.getPresignedUrl(file.storagePath, 3600); + return this.storageAdapter.getPresignedUrl(file.storagePath, 3600); } /** @@ -287,13 +287,13 @@ export class FileService { }; if (file.isReady()) { - dto.downloadUrl = await this.minioService.getPresignedUrl( + dto.downloadUrl = await this.storageAdapter.getPresignedUrl( file.storagePath, 3600, ); if (file.thumbnailPath) { - dto.thumbnailUrl = await this.minioService.getPresignedUrl( + dto.thumbnailUrl = await this.storageAdapter.getPresignedUrl( file.thumbnailPath, 3600, ); diff --git a/packages/services/file-service/src/application/services/index.ts b/packages/services/file-service/src/application/services/index.ts new file mode 100644 index 0000000..3c44127 --- /dev/null +++ b/packages/services/file-service/src/application/services/index.ts @@ -0,0 +1 @@ +export * from './file.service'; diff --git a/packages/services/file-service/src/file/file.module.ts b/packages/services/file-service/src/file/file.module.ts index 317442e..cd8d476 100644 --- a/packages/services/file-service/src/file/file.module.ts +++ b/packages/services/file-service/src/file/file.module.ts @@ -2,10 +2,11 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { MulterModule } from '@nestjs/platform-express'; import { FileORM } from '../infrastructure/database/postgres/entities/file.orm'; -import { FilePostgresRepository } from '../infrastructure/database/postgres/file-postgres.repository'; +import { FilePostgresRepository } from '../adapters/outbound/persistence/file-postgres.repository'; import { FILE_REPOSITORY } from '../domain/repositories/file.repository.interface'; -import { FileController } from './file.controller'; -import { FileService } from './file.service'; +import { FileController } from '../adapters/inbound/file.controller'; +import { FileService } from '../application/services/file.service'; +import { MinioStorageAdapter } from '../adapters/outbound/storage/minio-storage.adapter'; @Module({ imports: [ @@ -19,6 +20,7 @@ import { FileService } from './file.service'; controllers: [FileController], providers: [ FileService, + MinioStorageAdapter, { provide: FILE_REPOSITORY, useClass: FilePostgresRepository, diff --git a/packages/services/file-service/src/minio/minio.module.ts b/packages/services/file-service/src/minio/minio.module.ts deleted file mode 100644 index 2bb45ca..0000000 --- a/packages/services/file-service/src/minio/minio.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module, Global } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { MinioService } from './minio.service'; - -@Global() -@Module({ - imports: [ConfigModule], - providers: [MinioService], - exports: [MinioService], -}) -export class MinioModule {} diff --git a/packages/services/knowledge-service/src/adapters/inbound/index.ts b/packages/services/knowledge-service/src/adapters/inbound/index.ts new file mode 100644 index 0000000..590d39f --- /dev/null +++ b/packages/services/knowledge-service/src/adapters/inbound/index.ts @@ -0,0 +1,3 @@ +export * from './knowledge.controller'; +export * from './memory.controller'; +export * from './internal-memory.controller'; diff --git a/packages/services/knowledge-service/src/memory/internal.controller.ts b/packages/services/knowledge-service/src/adapters/inbound/internal-memory.controller.ts similarity index 94% rename from packages/services/knowledge-service/src/memory/internal.controller.ts rename to packages/services/knowledge-service/src/adapters/inbound/internal-memory.controller.ts index 2959894..98b1f5f 100644 --- a/packages/services/knowledge-service/src/memory/internal.controller.ts +++ b/packages/services/knowledge-service/src/adapters/inbound/internal-memory.controller.ts @@ -1,6 +1,6 @@ import { Controller, Get, Post, Body, Query } from '@nestjs/common'; -import { MemoryService } from './memory.service'; -import { ExperienceType } from '../domain/entities/system-experience.entity'; +import { MemoryService } from '../../application/services/memory.service'; +import { ExperienceType } from '../../domain/entities/system-experience.entity'; /** * 内部 API 控制器 diff --git a/packages/services/knowledge-service/src/knowledge/knowledge.controller.ts b/packages/services/knowledge-service/src/adapters/inbound/knowledge.controller.ts similarity index 82% rename from packages/services/knowledge-service/src/knowledge/knowledge.controller.ts rename to packages/services/knowledge-service/src/adapters/inbound/knowledge.controller.ts index baea774..97d75b4 100644 --- a/packages/services/knowledge-service/src/knowledge/knowledge.controller.ts +++ b/packages/services/knowledge-service/src/adapters/inbound/knowledge.controller.ts @@ -10,56 +10,17 @@ import { HttpCode, HttpStatus, } from '@nestjs/common'; -import { KnowledgeService } from './knowledge.service'; -import { RAGService } from '../application/services/rag.service'; -import { KnowledgeSource } from '../domain/entities/knowledge-article.entity'; - -// ========== DTOs ========== - -class CreateArticleDto { - title: string; - content: string; - category: string; - tags?: string[]; - sourceUrl?: string; - autoPublish?: boolean; -} - -class UpdateArticleDto { - title?: string; - content?: string; - category?: string; - tags?: string[]; -} - -class SearchArticlesDto { - query: string; - category?: string; - useVector?: boolean; -} - -class RetrieveKnowledgeDto { - query: string; - userId?: string; - category?: string; - includeMemories?: boolean; - includeExperiences?: boolean; -} - -class ImportArticlesDto { - articles: Array<{ - title: string; - content: string; - category: string; - tags?: string[]; - }>; -} - -class FeedbackDto { - helpful: boolean; -} - -// ========== Controller ========== +import { KnowledgeService } from '../../application/services/knowledge.service'; +import { RAGService } from '../../application/services/rag.service'; +import { KnowledgeSource } from '../../domain/entities/knowledge-article.entity'; +import { + CreateArticleDto, + UpdateArticleDto, + SearchArticlesDto, + RetrieveKnowledgeDto, + ImportArticlesDto, + FeedbackDto, +} from '../../application/dtos/knowledge.dto'; @Controller('knowledge') export class KnowledgeController { diff --git a/packages/services/knowledge-service/src/memory/memory.controller.ts b/packages/services/knowledge-service/src/adapters/inbound/memory.controller.ts similarity index 87% rename from packages/services/knowledge-service/src/memory/memory.controller.ts rename to packages/services/knowledge-service/src/adapters/inbound/memory.controller.ts index 8d2bd6d..74b2f2e 100644 --- a/packages/services/knowledge-service/src/memory/memory.controller.ts +++ b/packages/services/knowledge-service/src/adapters/inbound/memory.controller.ts @@ -10,41 +10,15 @@ import { HttpCode, HttpStatus, } from '@nestjs/common'; -import { MemoryService } from './memory.service'; -import { MemoryType } from '../domain/entities/user-memory.entity'; -import { ExperienceType } from '../domain/entities/system-experience.entity'; - -// ========== DTOs ========== - -class SaveMemoryDto { - userId: string; - memoryType: MemoryType; - content: string; - importance?: number; - sourceConversationId?: string; - relatedCategory?: string; -} - -class SearchMemoryDto { - userId: string; - query: string; - limit?: number; -} - -class ExtractExperienceDto { - experienceType: ExperienceType; - content: string; - scenario: string; - relatedCategory?: string; - sourceConversationId: string; - confidence?: number; -} - -class FeedbackDto { - positive: boolean; -} - -// ========== Controller ========== +import { MemoryService } from '../../application/services/memory.service'; +import { MemoryType } from '../../domain/entities/user-memory.entity'; +import { ExperienceType } from '../../domain/entities/system-experience.entity'; +import { + SaveMemoryDto, + SearchMemoryDto, + ExtractExperienceDto, + MemoryFeedbackDto, +} from '../../application/dtos/memory.dto'; @Controller('memory') export class MemoryController { @@ -263,7 +237,7 @@ export class MemoryController { @Post('experience/:id/feedback') async recordExperienceFeedback( @Param('id') id: string, - @Body() dto: FeedbackDto, + @Body() dto: MemoryFeedbackDto, ) { await this.memoryService.recordExperienceFeedback(id, dto.positive); return { diff --git a/packages/services/knowledge-service/src/adapters/outbound/persistence/index.ts b/packages/services/knowledge-service/src/adapters/outbound/persistence/index.ts new file mode 100644 index 0000000..72233f6 --- /dev/null +++ b/packages/services/knowledge-service/src/adapters/outbound/persistence/index.ts @@ -0,0 +1,2 @@ +export * from './knowledge-postgres.repository'; +export * from './memory-postgres.repository'; diff --git a/packages/services/knowledge-service/src/infrastructure/database/postgres/knowledge-postgres.repository.ts b/packages/services/knowledge-service/src/adapters/outbound/persistence/knowledge-postgres.repository.ts similarity index 97% rename from packages/services/knowledge-service/src/infrastructure/database/postgres/knowledge-postgres.repository.ts rename to packages/services/knowledge-service/src/adapters/outbound/persistence/knowledge-postgres.repository.ts index b0b2cd5..a33a7fc 100644 --- a/packages/services/knowledge-service/src/infrastructure/database/postgres/knowledge-postgres.repository.ts +++ b/packages/services/knowledge-service/src/adapters/outbound/persistence/knowledge-postgres.repository.ts @@ -4,8 +4,8 @@ import { Repository, ILike } from 'typeorm'; import { IKnowledgeRepository } from '../../../domain/repositories/knowledge.repository.interface'; import { KnowledgeArticleEntity, KnowledgeSource } from '../../../domain/entities/knowledge-article.entity'; import { KnowledgeChunkEntity, ChunkType } from '../../../domain/entities/knowledge-chunk.entity'; -import { KnowledgeArticleORM } from './entities/knowledge-article.orm'; -import { KnowledgeChunkORM } from './entities/knowledge-chunk.orm'; +import { KnowledgeArticleORM } from '../../../infrastructure/database/postgres/entities/knowledge-article.orm'; +import { KnowledgeChunkORM } from '../../../infrastructure/database/postgres/entities/knowledge-chunk.orm'; @Injectable() export class KnowledgePostgresRepository implements IKnowledgeRepository { diff --git a/packages/services/knowledge-service/src/infrastructure/database/postgres/memory-postgres.repository.ts b/packages/services/knowledge-service/src/adapters/outbound/persistence/memory-postgres.repository.ts similarity index 98% rename from packages/services/knowledge-service/src/infrastructure/database/postgres/memory-postgres.repository.ts rename to packages/services/knowledge-service/src/adapters/outbound/persistence/memory-postgres.repository.ts index 70e2abf..9ca8628 100644 --- a/packages/services/knowledge-service/src/infrastructure/database/postgres/memory-postgres.repository.ts +++ b/packages/services/knowledge-service/src/adapters/outbound/persistence/memory-postgres.repository.ts @@ -11,8 +11,8 @@ import { ExperienceType, VerificationStatus, } from '../../../domain/entities/system-experience.entity'; -import { UserMemoryORM } from './entities/user-memory.orm'; -import { SystemExperienceORM } from './entities/system-experience.orm'; +import { UserMemoryORM } from '../../../infrastructure/database/postgres/entities/user-memory.orm'; +import { SystemExperienceORM } from '../../../infrastructure/database/postgres/entities/system-experience.orm'; @Injectable() export class UserMemoryPostgresRepository implements IUserMemoryRepository { diff --git a/packages/services/knowledge-service/src/application/dtos/index.ts b/packages/services/knowledge-service/src/application/dtos/index.ts new file mode 100644 index 0000000..c091de9 --- /dev/null +++ b/packages/services/knowledge-service/src/application/dtos/index.ts @@ -0,0 +1,2 @@ +export * from './knowledge.dto'; +export * from './memory.dto'; diff --git a/packages/services/knowledge-service/src/application/dtos/knowledge.dto.ts b/packages/services/knowledge-service/src/application/dtos/knowledge.dto.ts new file mode 100644 index 0000000..e86795f --- /dev/null +++ b/packages/services/knowledge-service/src/application/dtos/knowledge.dto.ts @@ -0,0 +1,44 @@ +import { KnowledgeSource } from '../../domain/entities/knowledge-article.entity'; + +export class CreateArticleDto { + title: string; + content: string; + category: string; + tags?: string[]; + sourceUrl?: string; + autoPublish?: boolean; +} + +export class UpdateArticleDto { + title?: string; + content?: string; + category?: string; + tags?: string[]; +} + +export class SearchArticlesDto { + query: string; + category?: string; + useVector?: boolean; +} + +export class RetrieveKnowledgeDto { + query: string; + userId?: string; + category?: string; + includeMemories?: boolean; + includeExperiences?: boolean; +} + +export class ImportArticlesDto { + articles: Array<{ + title: string; + content: string; + category: string; + tags?: string[]; + }>; +} + +export class FeedbackDto { + helpful: boolean; +} diff --git a/packages/services/knowledge-service/src/application/dtos/memory.dto.ts b/packages/services/knowledge-service/src/application/dtos/memory.dto.ts new file mode 100644 index 0000000..f75efd7 --- /dev/null +++ b/packages/services/knowledge-service/src/application/dtos/memory.dto.ts @@ -0,0 +1,30 @@ +import { MemoryType } from '../../domain/entities/user-memory.entity'; +import { ExperienceType } from '../../domain/entities/system-experience.entity'; + +export class SaveMemoryDto { + userId: string; + memoryType: MemoryType; + content: string; + importance?: number; + sourceConversationId?: string; + relatedCategory?: string; +} + +export class SearchMemoryDto { + userId: string; + query: string; + limit?: number; +} + +export class ExtractExperienceDto { + experienceType: ExperienceType; + content: string; + scenario: string; + relatedCategory?: string; + sourceConversationId: string; + confidence?: number; +} + +export class MemoryFeedbackDto { + positive: boolean; +} diff --git a/packages/services/knowledge-service/src/application/services/index.ts b/packages/services/knowledge-service/src/application/services/index.ts new file mode 100644 index 0000000..16a479e --- /dev/null +++ b/packages/services/knowledge-service/src/application/services/index.ts @@ -0,0 +1,4 @@ +export * from './knowledge.service'; +export * from './memory.service'; +export * from './rag.service'; +export * from './chunking.service'; diff --git a/packages/services/knowledge-service/src/knowledge/knowledge.service.ts b/packages/services/knowledge-service/src/application/services/knowledge.service.ts similarity index 96% rename from packages/services/knowledge-service/src/knowledge/knowledge.service.ts rename to packages/services/knowledge-service/src/application/services/knowledge.service.ts index d3032b5..0814ba1 100644 --- a/packages/services/knowledge-service/src/knowledge/knowledge.service.ts +++ b/packages/services/knowledge-service/src/application/services/knowledge.service.ts @@ -1,14 +1,14 @@ import { Injectable, Inject } from '@nestjs/common'; -import { EmbeddingService } from '../infrastructure/embedding/embedding.service'; -import { ChunkingService } from '../application/services/chunking.service'; +import { EmbeddingService } from '../../infrastructure/embedding/embedding.service'; +import { ChunkingService } from './chunking.service'; import { IKnowledgeRepository, KNOWLEDGE_REPOSITORY, -} from '../domain/repositories/knowledge.repository.interface'; +} from '../../domain/repositories/knowledge.repository.interface'; import { KnowledgeArticleEntity, KnowledgeSource, -} from '../domain/entities/knowledge-article.entity'; +} from '../../domain/entities/knowledge-article.entity'; /** * 知识管理服务 diff --git a/packages/services/knowledge-service/src/memory/memory.service.ts b/packages/services/knowledge-service/src/application/services/memory.service.ts similarity index 95% rename from packages/services/knowledge-service/src/memory/memory.service.ts rename to packages/services/knowledge-service/src/application/services/memory.service.ts index d189043..dcefc2b 100644 --- a/packages/services/knowledge-service/src/memory/memory.service.ts +++ b/packages/services/knowledge-service/src/application/services/memory.service.ts @@ -1,18 +1,18 @@ import { Injectable, Inject } from '@nestjs/common'; -import { EmbeddingService } from '../infrastructure/embedding/embedding.service'; -import { Neo4jService } from '../infrastructure/database/neo4j/neo4j.service'; +import { EmbeddingService } from '../../infrastructure/embedding/embedding.service'; +import { Neo4jService } from '../../infrastructure/database/neo4j/neo4j.service'; import { IUserMemoryRepository, ISystemExperienceRepository, USER_MEMORY_REPOSITORY, SYSTEM_EXPERIENCE_REPOSITORY, -} from '../domain/repositories/memory.repository.interface'; -import { UserMemoryEntity, MemoryType } from '../domain/entities/user-memory.entity'; +} from '../../domain/repositories/memory.repository.interface'; +import { UserMemoryEntity, MemoryType } from '../../domain/entities/user-memory.entity'; import { SystemExperienceEntity, ExperienceType, VerificationStatus, -} from '../domain/entities/system-experience.entity'; +} from '../../domain/entities/system-experience.entity'; /** * 记忆管理服务 diff --git a/packages/services/knowledge-service/src/knowledge/knowledge.module.ts b/packages/services/knowledge-service/src/knowledge/knowledge.module.ts index 1afe1bd..88c39ed 100644 --- a/packages/services/knowledge-service/src/knowledge/knowledge.module.ts +++ b/packages/services/knowledge-service/src/knowledge/knowledge.module.ts @@ -1,15 +1,15 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { KnowledgeController } from './knowledge.controller'; -import { KnowledgeService } from './knowledge.service'; +import { KnowledgeController } from '../adapters/inbound/knowledge.controller'; +import { KnowledgeService } from '../application/services/knowledge.service'; import { RAGService } from '../application/services/rag.service'; import { ChunkingService } from '../application/services/chunking.service'; import { EmbeddingService } from '../infrastructure/embedding/embedding.service'; -import { KnowledgePostgresRepository } from '../infrastructure/database/postgres/knowledge-postgres.repository'; +import { KnowledgePostgresRepository } from '../adapters/outbound/persistence/knowledge-postgres.repository'; import { UserMemoryPostgresRepository, SystemExperiencePostgresRepository, -} from '../infrastructure/database/postgres/memory-postgres.repository'; +} from '../adapters/outbound/persistence/memory-postgres.repository'; import { KnowledgeArticleORM } from '../infrastructure/database/postgres/entities/knowledge-article.orm'; import { KnowledgeChunkORM } from '../infrastructure/database/postgres/entities/knowledge-chunk.orm'; import { UserMemoryORM } from '../infrastructure/database/postgres/entities/user-memory.orm'; diff --git a/packages/services/knowledge-service/src/memory/memory.module.ts b/packages/services/knowledge-service/src/memory/memory.module.ts index 321f497..bef3283 100644 --- a/packages/services/knowledge-service/src/memory/memory.module.ts +++ b/packages/services/knowledge-service/src/memory/memory.module.ts @@ -1,14 +1,14 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { MemoryController } from './memory.controller'; -import { InternalMemoryController } from './internal.controller'; -import { MemoryService } from './memory.service'; +import { MemoryController } from '../adapters/inbound/memory.controller'; +import { InternalMemoryController } from '../adapters/inbound/internal-memory.controller'; +import { MemoryService } from '../application/services/memory.service'; import { EmbeddingService } from '../infrastructure/embedding/embedding.service'; import { Neo4jService } from '../infrastructure/database/neo4j/neo4j.service'; import { UserMemoryPostgresRepository, SystemExperiencePostgresRepository, -} from '../infrastructure/database/postgres/memory-postgres.repository'; +} from '../adapters/outbound/persistence/memory-postgres.repository'; import { UserMemoryORM } from '../infrastructure/database/postgres/entities/user-memory.orm'; import { SystemExperienceORM } from '../infrastructure/database/postgres/entities/system-experience.orm'; import { diff --git a/packages/services/payment-service/src/adapters/inbound/index.ts b/packages/services/payment-service/src/adapters/inbound/index.ts new file mode 100644 index 0000000..fb83645 --- /dev/null +++ b/packages/services/payment-service/src/adapters/inbound/index.ts @@ -0,0 +1,2 @@ +export * from './order.controller'; +export * from './payment.controller'; diff --git a/packages/services/payment-service/src/order/order.controller.ts b/packages/services/payment-service/src/adapters/inbound/order.controller.ts similarity index 88% rename from packages/services/payment-service/src/order/order.controller.ts rename to packages/services/payment-service/src/adapters/inbound/order.controller.ts index 6692dcf..352d0ea 100644 --- a/packages/services/payment-service/src/order/order.controller.ts +++ b/packages/services/payment-service/src/adapters/inbound/order.controller.ts @@ -8,14 +8,8 @@ import { HttpCode, HttpStatus, } from '@nestjs/common'; -import { OrderService } from './order.service'; -import { ServiceType } from '../domain/entities/order.entity'; - -class CreateOrderDto { - serviceType: ServiceType; - serviceCategory?: string; - conversationId?: string; -} +import { OrderService } from '../../application/services/order.service'; +import { CreateOrderDto } from '../../application/dtos/order.dto'; @Controller('orders') export class OrderController { diff --git a/packages/services/payment-service/src/payment/payment.controller.ts b/packages/services/payment-service/src/adapters/inbound/payment.controller.ts similarity index 91% rename from packages/services/payment-service/src/payment/payment.controller.ts rename to packages/services/payment-service/src/adapters/inbound/payment.controller.ts index 2e144fc..85a2a9b 100644 --- a/packages/services/payment-service/src/payment/payment.controller.ts +++ b/packages/services/payment-service/src/adapters/inbound/payment.controller.ts @@ -10,13 +10,9 @@ import { Req, } from '@nestjs/common'; import { Request } from 'express'; -import { PaymentService } from './payment.service'; -import { PaymentMethod } from '../domain/entities/payment.entity'; - -class CreatePaymentDto { - orderId: string; - method: PaymentMethod; -} +import { PaymentService } from '../../application/services/payment.service'; +import { CreatePaymentDto } from '../../application/dtos/payment.dto'; +import { PaymentMethod } from '../../domain/entities/payment.entity'; @Controller('payments') export class PaymentController { diff --git a/packages/services/payment-service/src/payment/adapters/alipay.adapter.ts b/packages/services/payment-service/src/adapters/outbound/payment-methods/alipay.adapter.ts similarity index 97% rename from packages/services/payment-service/src/payment/adapters/alipay.adapter.ts rename to packages/services/payment-service/src/adapters/outbound/payment-methods/alipay.adapter.ts index c84e82d..f69443e 100644 --- a/packages/services/payment-service/src/payment/adapters/alipay.adapter.ts +++ b/packages/services/payment-service/src/adapters/outbound/payment-methods/alipay.adapter.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as QRCode from 'qrcode'; -import { OrderEntity } from '../../domain/entities/order.entity'; +import { OrderEntity } from '../../../domain/entities/order.entity'; export interface AlipayPaymentResult { qrCodeUrl: string; diff --git a/packages/services/payment-service/src/adapters/outbound/payment-methods/index.ts b/packages/services/payment-service/src/adapters/outbound/payment-methods/index.ts new file mode 100644 index 0000000..52f4c2c --- /dev/null +++ b/packages/services/payment-service/src/adapters/outbound/payment-methods/index.ts @@ -0,0 +1,3 @@ +export * from './alipay.adapter'; +export * from './wechat-pay.adapter'; +export * from './stripe.adapter'; diff --git a/packages/services/payment-service/src/payment/adapters/stripe.adapter.ts b/packages/services/payment-service/src/adapters/outbound/payment-methods/stripe.adapter.ts similarity index 97% rename from packages/services/payment-service/src/payment/adapters/stripe.adapter.ts rename to packages/services/payment-service/src/adapters/outbound/payment-methods/stripe.adapter.ts index 3379616..472291e 100644 --- a/packages/services/payment-service/src/payment/adapters/stripe.adapter.ts +++ b/packages/services/payment-service/src/adapters/outbound/payment-methods/stripe.adapter.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { OrderEntity } from '../../domain/entities/order.entity'; +import { OrderEntity } from '../../../domain/entities/order.entity'; export interface StripePaymentResult { paymentUrl: string; diff --git a/packages/services/payment-service/src/payment/adapters/wechat-pay.adapter.ts b/packages/services/payment-service/src/adapters/outbound/payment-methods/wechat-pay.adapter.ts similarity index 97% rename from packages/services/payment-service/src/payment/adapters/wechat-pay.adapter.ts rename to packages/services/payment-service/src/adapters/outbound/payment-methods/wechat-pay.adapter.ts index 20ec613..9795bff 100644 --- a/packages/services/payment-service/src/payment/adapters/wechat-pay.adapter.ts +++ b/packages/services/payment-service/src/adapters/outbound/payment-methods/wechat-pay.adapter.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as QRCode from 'qrcode'; -import { OrderEntity } from '../../domain/entities/order.entity'; +import { OrderEntity } from '../../../domain/entities/order.entity'; export interface WechatPaymentResult { qrCodeUrl: string; diff --git a/packages/services/payment-service/src/adapters/outbound/persistence/index.ts b/packages/services/payment-service/src/adapters/outbound/persistence/index.ts new file mode 100644 index 0000000..3ffbae8 --- /dev/null +++ b/packages/services/payment-service/src/adapters/outbound/persistence/index.ts @@ -0,0 +1,2 @@ +export * from './order-postgres.repository'; +export * from './payment-postgres.repository'; diff --git a/packages/services/payment-service/src/infrastructure/database/postgres/order-postgres.repository.ts b/packages/services/payment-service/src/adapters/outbound/persistence/order-postgres.repository.ts similarity index 96% rename from packages/services/payment-service/src/infrastructure/database/postgres/order-postgres.repository.ts rename to packages/services/payment-service/src/adapters/outbound/persistence/order-postgres.repository.ts index 8b12bc4..ee64ac6 100644 --- a/packages/services/payment-service/src/infrastructure/database/postgres/order-postgres.repository.ts +++ b/packages/services/payment-service/src/adapters/outbound/persistence/order-postgres.repository.ts @@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { IOrderRepository } from '../../../domain/repositories/order.repository.interface'; import { OrderEntity } from '../../../domain/entities/order.entity'; -import { OrderORM } from './entities/order.orm'; +import { OrderORM } from '../../../infrastructure/database/postgres/entities/order.orm'; @Injectable() export class OrderPostgresRepository implements IOrderRepository { diff --git a/packages/services/payment-service/src/infrastructure/database/postgres/payment-postgres.repository.ts b/packages/services/payment-service/src/adapters/outbound/persistence/payment-postgres.repository.ts similarity index 96% rename from packages/services/payment-service/src/infrastructure/database/postgres/payment-postgres.repository.ts rename to packages/services/payment-service/src/adapters/outbound/persistence/payment-postgres.repository.ts index 6a6f5ba..7350f6c 100644 --- a/packages/services/payment-service/src/infrastructure/database/postgres/payment-postgres.repository.ts +++ b/packages/services/payment-service/src/adapters/outbound/persistence/payment-postgres.repository.ts @@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { IPaymentRepository } from '../../../domain/repositories/payment.repository.interface'; import { PaymentEntity, PaymentStatus } from '../../../domain/entities/payment.entity'; -import { PaymentORM } from './entities/payment.orm'; +import { PaymentORM } from '../../../infrastructure/database/postgres/entities/payment.orm'; @Injectable() export class PaymentPostgresRepository implements IPaymentRepository { diff --git a/packages/services/payment-service/src/application/dtos/index.ts b/packages/services/payment-service/src/application/dtos/index.ts new file mode 100644 index 0000000..a1d945f --- /dev/null +++ b/packages/services/payment-service/src/application/dtos/index.ts @@ -0,0 +1,2 @@ +export * from './order.dto'; +export * from './payment.dto'; diff --git a/packages/services/payment-service/src/application/dtos/order.dto.ts b/packages/services/payment-service/src/application/dtos/order.dto.ts new file mode 100644 index 0000000..bbbe1ca --- /dev/null +++ b/packages/services/payment-service/src/application/dtos/order.dto.ts @@ -0,0 +1,16 @@ +import { IsString, IsNotEmpty, IsOptional, IsEnum } from 'class-validator'; +import { ServiceType } from '../../domain/entities/order.entity'; + +export class CreateOrderDto { + @IsEnum(ServiceType) + @IsNotEmpty() + serviceType: ServiceType; + + @IsString() + @IsOptional() + serviceCategory?: string; + + @IsString() + @IsOptional() + conversationId?: string; +} diff --git a/packages/services/payment-service/src/application/dtos/payment.dto.ts b/packages/services/payment-service/src/application/dtos/payment.dto.ts new file mode 100644 index 0000000..64152f1 --- /dev/null +++ b/packages/services/payment-service/src/application/dtos/payment.dto.ts @@ -0,0 +1,12 @@ +import { IsString, IsNotEmpty, IsEnum } from 'class-validator'; +import { PaymentMethod } from '../../domain/entities/payment.entity'; + +export class CreatePaymentDto { + @IsString() + @IsNotEmpty() + orderId: string; + + @IsEnum(PaymentMethod) + @IsNotEmpty() + method: PaymentMethod; +} diff --git a/packages/services/payment-service/src/application/services/index.ts b/packages/services/payment-service/src/application/services/index.ts new file mode 100644 index 0000000..14c92b4 --- /dev/null +++ b/packages/services/payment-service/src/application/services/index.ts @@ -0,0 +1,2 @@ +export * from './order.service'; +export * from './payment.service'; diff --git a/packages/services/payment-service/src/order/order.service.ts b/packages/services/payment-service/src/application/services/order.service.ts similarity index 80% rename from packages/services/payment-service/src/order/order.service.ts rename to packages/services/payment-service/src/application/services/order.service.ts index 40d8b04..37caed9 100644 --- a/packages/services/payment-service/src/order/order.service.ts +++ b/packages/services/payment-service/src/application/services/order.service.ts @@ -1,6 +1,6 @@ import { Injectable, Inject, NotFoundException, BadRequestException } from '@nestjs/common'; -import { OrderEntity, OrderStatus, ServiceType } from '../domain/entities/order.entity'; -import { IOrderRepository, ORDER_REPOSITORY } from '../domain/repositories/order.repository.interface'; +import { OrderEntity, OrderStatus, ServiceType } from '../../domain/entities/order.entity'; +import { IOrderRepository, ORDER_REPOSITORY } from '../../domain/repositories/order.repository.interface'; // Default pricing const SERVICE_PRICING: Record> = { @@ -14,7 +14,7 @@ const SERVICE_PRICING: Record> = { }, }; -export interface CreateOrderDto { +export interface CreateOrderParams { userId: string; serviceType: ServiceType; serviceCategory?: string; @@ -28,15 +28,15 @@ export class OrderService { private readonly orderRepo: IOrderRepository, ) {} - async createOrder(dto: CreateOrderDto): Promise { + async createOrder(params: CreateOrderParams): Promise { // Get price based on service type and category - const price = this.getPrice(dto.serviceType, dto.serviceCategory); + const price = this.getPrice(params.serviceType, params.serviceCategory); const order = OrderEntity.create({ - userId: dto.userId, - serviceType: dto.serviceType, - serviceCategory: dto.serviceCategory, - conversationId: dto.conversationId, + userId: params.userId, + serviceType: params.serviceType, + serviceCategory: params.serviceCategory, + conversationId: params.conversationId, amount: price, currency: 'CNY', }); diff --git a/packages/services/payment-service/src/payment/payment.service.ts b/packages/services/payment-service/src/application/services/payment.service.ts similarity index 86% rename from packages/services/payment-service/src/payment/payment.service.ts rename to packages/services/payment-service/src/application/services/payment.service.ts index 0b1da37..7dc8536 100644 --- a/packages/services/payment-service/src/payment/payment.service.ts +++ b/packages/services/payment-service/src/application/services/payment.service.ts @@ -1,13 +1,13 @@ import { Injectable, Inject, NotFoundException, BadRequestException } from '@nestjs/common'; -import { PaymentEntity, PaymentMethod, PaymentStatus } from '../domain/entities/payment.entity'; -import { OrderStatus } from '../domain/entities/order.entity'; -import { IPaymentRepository, PAYMENT_REPOSITORY } from '../domain/repositories/payment.repository.interface'; -import { OrderService } from '../order/order.service'; -import { AlipayAdapter } from './adapters/alipay.adapter'; -import { WechatPayAdapter } from './adapters/wechat-pay.adapter'; -import { StripeAdapter } from './adapters/stripe.adapter'; +import { PaymentEntity, PaymentMethod, PaymentStatus } from '../../domain/entities/payment.entity'; +import { OrderStatus } from '../../domain/entities/order.entity'; +import { IPaymentRepository, PAYMENT_REPOSITORY } from '../../domain/repositories/payment.repository.interface'; +import { OrderService } from './order.service'; +import { AlipayAdapter } from '../../adapters/outbound/payment-methods/alipay.adapter'; +import { WechatPayAdapter } from '../../adapters/outbound/payment-methods/wechat-pay.adapter'; +import { StripeAdapter } from '../../adapters/outbound/payment-methods/stripe.adapter'; -export interface CreatePaymentDto { +export interface CreatePaymentParams { orderId: string; method: PaymentMethod; } @@ -33,15 +33,15 @@ export class PaymentService { private readonly stripeAdapter: StripeAdapter, ) {} - async createPayment(dto: CreatePaymentDto): Promise { - const order = await this.orderService.findById(dto.orderId); + async createPayment(params: CreatePaymentParams): Promise { + const order = await this.orderService.findById(params.orderId); if (!order.canBePaid()) { throw new BadRequestException('Cannot create payment for this order'); } // Check for existing pending payment - const existingPayment = await this.paymentRepo.findPendingByOrderId(dto.orderId); + const existingPayment = await this.paymentRepo.findPendingByOrderId(params.orderId); if (existingPayment && !existingPayment.isExpired()) { return { @@ -59,7 +59,7 @@ export class PaymentService { let qrCodeUrl: string | undefined; let paymentUrl: string | undefined; - switch (dto.method) { + switch (params.method) { case PaymentMethod.ALIPAY: const alipayResult = await this.alipayAdapter.createPayment(order); qrCodeUrl = alipayResult.qrCodeUrl; @@ -82,7 +82,7 @@ export class PaymentService { // Create payment entity const payment = PaymentEntity.create({ orderId: order.id, - method: dto.method, + method: params.method, amount: order.amount, currency: order.currency, qrCodeUrl, diff --git a/packages/services/payment-service/src/order/order.module.ts b/packages/services/payment-service/src/order/order.module.ts index bf86029..ee8c1a3 100644 --- a/packages/services/payment-service/src/order/order.module.ts +++ b/packages/services/payment-service/src/order/order.module.ts @@ -1,10 +1,10 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { OrderORM } from '../infrastructure/database/postgres/entities/order.orm'; -import { OrderPostgresRepository } from '../infrastructure/database/postgres/order-postgres.repository'; +import { OrderPostgresRepository } from '../adapters/outbound/persistence/order-postgres.repository'; import { ORDER_REPOSITORY } from '../domain/repositories/order.repository.interface'; -import { OrderService } from './order.service'; -import { OrderController } from './order.controller'; +import { OrderService } from '../application/services/order.service'; +import { OrderController } from '../adapters/inbound/order.controller'; @Module({ imports: [TypeOrmModule.forFeature([OrderORM])], diff --git a/packages/services/payment-service/src/payment/payment.module.ts b/packages/services/payment-service/src/payment/payment.module.ts index afb6379..15be464 100644 --- a/packages/services/payment-service/src/payment/payment.module.ts +++ b/packages/services/payment-service/src/payment/payment.module.ts @@ -1,14 +1,14 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { PaymentORM } from '../infrastructure/database/postgres/entities/payment.orm'; -import { PaymentPostgresRepository } from '../infrastructure/database/postgres/payment-postgres.repository'; +import { PaymentPostgresRepository } from '../adapters/outbound/persistence/payment-postgres.repository'; import { PAYMENT_REPOSITORY } from '../domain/repositories/payment.repository.interface'; import { OrderModule } from '../order/order.module'; -import { PaymentService } from './payment.service'; -import { PaymentController } from './payment.controller'; -import { AlipayAdapter } from './adapters/alipay.adapter'; -import { WechatPayAdapter } from './adapters/wechat-pay.adapter'; -import { StripeAdapter } from './adapters/stripe.adapter'; +import { PaymentService } from '../application/services/payment.service'; +import { PaymentController } from '../adapters/inbound/payment.controller'; +import { AlipayAdapter } from '../adapters/outbound/payment-methods/alipay.adapter'; +import { WechatPayAdapter } from '../adapters/outbound/payment-methods/wechat-pay.adapter'; +import { StripeAdapter } from '../adapters/outbound/payment-methods/stripe.adapter'; @Module({ imports: [ diff --git a/packages/services/user-service/src/auth/auth.controller.ts b/packages/services/user-service/src/adapters/inbound/auth.controller.ts similarity index 60% rename from packages/services/user-service/src/auth/auth.controller.ts rename to packages/services/user-service/src/adapters/inbound/auth.controller.ts index 68dbd47..03fbbc2 100644 --- a/packages/services/user-service/src/auth/auth.controller.ts +++ b/packages/services/user-service/src/adapters/inbound/auth.controller.ts @@ -6,45 +6,18 @@ import { HttpCode, HttpStatus, } from '@nestjs/common'; -import { IsString, IsNotEmpty, IsOptional } from 'class-validator'; -import { AuthService } from './auth.service'; - -class CreateAnonymousDto { - @IsOptional() - @IsString() - fingerprint?: string; -} - -class SendCodeDto { - @IsNotEmpty() - @IsString() - phone: string; -} - -class VerifyCodeDto { - @IsNotEmpty() - @IsString() - phone: string; - - @IsNotEmpty() - @IsString() - code: string; -} - -class RefreshTokenDto { - @IsNotEmpty() - @IsString() - token: string; -} +import { AuthService } from '../../application/services/auth.service'; +import { + CreateAnonymousDto, + SendCodeDto, + VerifyCodeDto, + RefreshTokenDto, +} from '../../application/dtos/auth.dto'; @Controller('auth') export class AuthController { - constructor(private authService: AuthService) {} + constructor(private readonly authService: AuthService) {} - /** - * Create anonymous session - * POST /api/v1/auth/anonymous - */ @Post('anonymous') @HttpCode(HttpStatus.OK) async createAnonymousSession(@Body() dto: CreateAnonymousDto) { @@ -55,10 +28,6 @@ export class AuthController { }; } - /** - * Send verification code - * POST /api/v1/auth/send-code - */ @Post('send-code') @HttpCode(HttpStatus.OK) async sendVerificationCode(@Body() dto: SendCodeDto) { @@ -69,10 +38,6 @@ export class AuthController { }; } - /** - * Verify code and login - * POST /api/v1/auth/verify-phone - */ @Post('verify-phone') @HttpCode(HttpStatus.OK) async verifyPhone( @@ -90,10 +55,6 @@ export class AuthController { }; } - /** - * Refresh token - * POST /api/v1/auth/refresh - */ @Post('refresh') @HttpCode(HttpStatus.OK) async refreshToken(@Body() dto: RefreshTokenDto) { @@ -104,10 +65,6 @@ export class AuthController { }; } - /** - * Logout (client-side action, just acknowledge) - * POST /api/v1/auth/logout - */ @Post('logout') @HttpCode(HttpStatus.OK) async logout() { diff --git a/packages/services/user-service/src/adapters/inbound/index.ts b/packages/services/user-service/src/adapters/inbound/index.ts new file mode 100644 index 0000000..99ddf78 --- /dev/null +++ b/packages/services/user-service/src/adapters/inbound/index.ts @@ -0,0 +1,2 @@ +export * from './user.controller'; +export * from './auth.controller'; diff --git a/packages/services/user-service/src/user/user.controller.ts b/packages/services/user-service/src/adapters/inbound/user.controller.ts similarity index 85% rename from packages/services/user-service/src/user/user.controller.ts rename to packages/services/user-service/src/adapters/inbound/user.controller.ts index f8333dd..545b6e8 100644 --- a/packages/services/user-service/src/user/user.controller.ts +++ b/packages/services/user-service/src/adapters/inbound/user.controller.ts @@ -5,18 +5,13 @@ import { Body, Param, Headers, - UseGuards, } from '@nestjs/common'; -import { UserService } from './user.service'; - -class UpdateProfileDto { - nickname?: string; - avatar?: string; -} +import { UserService } from '../../application/services/user.service'; +import { UpdateProfileDto } from '../../application/dtos/user.dto'; @Controller('users') export class UserController { - constructor(private userService: UserService) {} + constructor(private readonly userService: UserService) {} @Get('me') async getCurrentUser(@Headers('x-user-id') userId: string) { diff --git a/packages/services/user-service/src/adapters/outbound/persistence/index.ts b/packages/services/user-service/src/adapters/outbound/persistence/index.ts new file mode 100644 index 0000000..349c41c --- /dev/null +++ b/packages/services/user-service/src/adapters/outbound/persistence/index.ts @@ -0,0 +1,2 @@ +export * from './user-postgres.repository'; +export * from './verification-code-postgres.repository'; diff --git a/packages/services/user-service/src/infrastructure/database/postgres/user-postgres.repository.ts b/packages/services/user-service/src/adapters/outbound/persistence/user-postgres.repository.ts similarity index 90% rename from packages/services/user-service/src/infrastructure/database/postgres/user-postgres.repository.ts rename to packages/services/user-service/src/adapters/outbound/persistence/user-postgres.repository.ts index 83bf9d6..8124d36 100644 --- a/packages/services/user-service/src/infrastructure/database/postgres/user-postgres.repository.ts +++ b/packages/services/user-service/src/adapters/outbound/persistence/user-postgres.repository.ts @@ -3,11 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { IUserRepository } from '../../../domain/repositories/user.repository.interface'; import { UserEntity } from '../../../domain/entities/user.entity'; -import { UserORM } from './entities/user.orm'; +import { UserORM } from '../../../infrastructure/database/postgres/entities/user.orm'; -/** - * PostgreSQL implementation of IUserRepository - */ @Injectable() export class UserPostgresRepository implements IUserRepository { constructor( @@ -40,9 +37,6 @@ export class UserPostgresRepository implements IUserRepository { await this.repo.update(userId, { lastActiveAt: new Date() }); } - /** - * Convert domain entity to ORM entity - */ private toORM(entity: UserEntity): UserORM { const orm = new UserORM(); orm.id = entity.id; @@ -57,9 +51,6 @@ export class UserPostgresRepository implements IUserRepository { return orm; } - /** - * Convert ORM entity to domain entity - */ private toEntity(orm: UserORM): UserEntity { return UserEntity.fromPersistence({ id: orm.id, diff --git a/packages/services/user-service/src/infrastructure/database/postgres/verification-code-postgres.repository.ts b/packages/services/user-service/src/adapters/outbound/persistence/verification-code-postgres.repository.ts similarity index 89% rename from packages/services/user-service/src/infrastructure/database/postgres/verification-code-postgres.repository.ts rename to packages/services/user-service/src/adapters/outbound/persistence/verification-code-postgres.repository.ts index b1aecb6..d205213 100644 --- a/packages/services/user-service/src/infrastructure/database/postgres/verification-code-postgres.repository.ts +++ b/packages/services/user-service/src/adapters/outbound/persistence/verification-code-postgres.repository.ts @@ -3,11 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository, MoreThan } from 'typeorm'; import { IVerificationCodeRepository } from '../../../domain/repositories/verification-code.repository.interface'; import { VerificationCodeEntity } from '../../../domain/entities/verification-code.entity'; -import { VerificationCodeORM } from './entities/verification-code.orm'; +import { VerificationCodeORM } from '../../../infrastructure/database/postgres/entities/verification-code.orm'; -/** - * PostgreSQL implementation of IVerificationCodeRepository - */ @Injectable() export class VerificationCodePostgresRepository implements IVerificationCodeRepository { constructor( @@ -48,9 +45,6 @@ export class VerificationCodePostgresRepository implements IVerificationCodeRepo await this.repo.update(id, { isUsed: true }); } - /** - * Convert domain entity to ORM entity - */ private toORM(entity: VerificationCodeEntity): VerificationCodeORM { const orm = new VerificationCodeORM(); orm.id = entity.id; @@ -62,9 +56,6 @@ export class VerificationCodePostgresRepository implements IVerificationCodeRepo return orm; } - /** - * Convert ORM entity to domain entity - */ private toEntity(orm: VerificationCodeORM): VerificationCodeEntity { return VerificationCodeEntity.fromPersistence({ id: orm.id, diff --git a/packages/services/user-service/src/application/dtos/auth.dto.ts b/packages/services/user-service/src/application/dtos/auth.dto.ts new file mode 100644 index 0000000..c2527b5 --- /dev/null +++ b/packages/services/user-service/src/application/dtos/auth.dto.ts @@ -0,0 +1,29 @@ +import { IsString, IsNotEmpty, IsOptional } from 'class-validator'; + +export class CreateAnonymousDto { + @IsOptional() + @IsString() + fingerprint?: string; +} + +export class SendCodeDto { + @IsNotEmpty() + @IsString() + phone: string; +} + +export class VerifyCodeDto { + @IsNotEmpty() + @IsString() + phone: string; + + @IsNotEmpty() + @IsString() + code: string; +} + +export class RefreshTokenDto { + @IsNotEmpty() + @IsString() + token: string; +} diff --git a/packages/services/user-service/src/application/dtos/index.ts b/packages/services/user-service/src/application/dtos/index.ts new file mode 100644 index 0000000..b7fb6f7 --- /dev/null +++ b/packages/services/user-service/src/application/dtos/index.ts @@ -0,0 +1,2 @@ +export * from './user.dto'; +export * from './auth.dto'; diff --git a/packages/services/user-service/src/application/dtos/user.dto.ts b/packages/services/user-service/src/application/dtos/user.dto.ts new file mode 100644 index 0000000..63a4484 --- /dev/null +++ b/packages/services/user-service/src/application/dtos/user.dto.ts @@ -0,0 +1,14 @@ +export class UpdateProfileDto { + nickname?: string; + avatar?: string; +} + +export class UserResponseDto { + id: string; + type: string; + phone?: string | null; + nickname?: string | null; + avatar?: string | null; + createdAt: Date; + lastActiveAt?: Date; +} diff --git a/packages/services/user-service/src/auth/auth.service.ts b/packages/services/user-service/src/application/services/auth.service.ts similarity index 76% rename from packages/services/user-service/src/auth/auth.service.ts rename to packages/services/user-service/src/application/services/auth.service.ts index 2a78195..ecea3e8 100644 --- a/packages/services/user-service/src/auth/auth.service.ts +++ b/packages/services/user-service/src/application/services/auth.service.ts @@ -1,11 +1,11 @@ import { Injectable, Inject, UnauthorizedException, BadRequestException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; -import { VerificationCodeEntity } from '../domain/entities/verification-code.entity'; +import { VerificationCodeEntity } from '../../domain/entities/verification-code.entity'; import { IVerificationCodeRepository, VERIFICATION_CODE_REPOSITORY, -} from '../domain/repositories/verification-code.repository.interface'; -import { UserService } from '../user/user.service'; +} from '../../domain/repositories/verification-code.repository.interface'; +import { UserService } from './user.service'; @Injectable() export class AuthService { @@ -16,9 +16,6 @@ export class AuthService { private readonly jwtService: JwtService, ) {} - /** - * Create anonymous session - */ async createAnonymousSession(fingerprint: string) { if (!fingerprint) { throw new BadRequestException('Fingerprint is required'); @@ -34,70 +31,51 @@ export class AuthService { return { userId: user.id, token, - expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), }; } - /** - * Send verification code (SMS) - */ async sendVerificationCode(phone: string) { - // Validate phone format if (!this.isValidPhone(phone)) { throw new BadRequestException('Invalid phone number'); } - // Check rate limit (max 5 codes per phone per hour) const recentCodes = await this.verificationCodeRepo.countRecentByPhone(phone, 1); if (recentCodes >= 5) { throw new BadRequestException('Too many verification codes requested'); } - // Create verification code using domain entity const verificationCode = VerificationCodeEntity.create(phone); - - // Save to database await this.verificationCodeRepo.save(verificationCode); - // TODO: Actually send SMS via Aliyun SMS or other provider - // For development, just log the code console.log(`[DEV] Verification code for ${phone}: ${verificationCode.code}`); return { sent: true, - expiresIn: 300, // 5 minutes in seconds + expiresIn: 300, }; } - /** - * Verify code and login - */ async verifyAndLogin(phone: string, code: string, userId?: string) { - // Find valid verification code const verificationCode = await this.verificationCodeRepo.findValidCode(phone, code); if (!verificationCode) { throw new UnauthorizedException('Invalid or expired verification code'); } - // Mark code as used await this.verificationCodeRepo.markAsUsed(verificationCode.id); - // Get or create user let user; if (userId) { - // Upgrade anonymous user to registered user = await this.userService.upgradeToRegistered(userId, phone); } else { - // Find existing user by phone or create new user = await this.userService.findByPhone(phone); if (!user) { throw new BadRequestException('User not found. Please provide userId to upgrade.'); } } - // Generate token const token = this.jwtService.sign({ sub: user.id, type: user.type, @@ -115,9 +93,6 @@ export class AuthService { }; } - /** - * Refresh token - */ async refreshToken(oldToken: string) { try { const payload = this.jwtService.verify(oldToken, { @@ -140,12 +115,7 @@ export class AuthService { } } - /** - * Validate phone format - */ private isValidPhone(phone: string): boolean { - // Chinese phone: 1xx xxxx xxxx - // HK phone: xxxx xxxx const cleanPhone = phone.replace(/[\s-]/g, ''); return /^1[3-9]\d{9}$/.test(cleanPhone) || /^[2-9]\d{7}$/.test(cleanPhone); } diff --git a/packages/services/user-service/src/application/services/index.ts b/packages/services/user-service/src/application/services/index.ts new file mode 100644 index 0000000..d99a6a5 --- /dev/null +++ b/packages/services/user-service/src/application/services/index.ts @@ -0,0 +1,2 @@ +export * from './user.service'; +export * from './auth.service'; diff --git a/packages/services/user-service/src/user/user.service.ts b/packages/services/user-service/src/application/services/user.service.ts similarity index 80% rename from packages/services/user-service/src/user/user.service.ts rename to packages/services/user-service/src/application/services/user.service.ts index addba24..a5763c6 100644 --- a/packages/services/user-service/src/user/user.service.ts +++ b/packages/services/user-service/src/application/services/user.service.ts @@ -1,9 +1,9 @@ import { Injectable, Inject, NotFoundException } from '@nestjs/common'; -import { UserEntity, UserType } from '../domain/entities/user.entity'; +import { UserEntity } from '../../domain/entities/user.entity'; import { IUserRepository, USER_REPOSITORY, -} from '../domain/repositories/user.repository.interface'; +} from '../../domain/repositories/user.repository.interface'; @Injectable() export class UserService { @@ -13,16 +13,13 @@ export class UserService { ) {} async createAnonymousUser(fingerprint: string): Promise { - // Check if user with this fingerprint already exists const existingUser = await this.userRepo.findByFingerprint(fingerprint); if (existingUser) { - // Update last active time and return existing user existingUser.updateLastActive(); return this.userRepo.save(existingUser); } - // Create new anonymous user const user = UserEntity.createAnonymous(fingerprint); return this.userRepo.save(user); } @@ -45,13 +42,9 @@ export class UserService { return this.userRepo.findByPhone(phone); } - async upgradeToRegistered( - userId: string, - phone: string, - ): Promise { + async upgradeToRegistered(userId: string, phone: string): Promise { const user = await this.findById(userId); - // Check if phone is already registered const existingPhoneUser = await this.findByPhone(phone); if (existingPhoneUser && existingPhoneUser.id !== userId) { throw new Error('Phone number already registered'); diff --git a/packages/services/user-service/src/auth/auth.module.ts b/packages/services/user-service/src/auth/auth.module.ts index 64890b5..4988dfc 100644 --- a/packages/services/user-service/src/auth/auth.module.ts +++ b/packages/services/user-service/src/auth/auth.module.ts @@ -1,11 +1,11 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { VerificationCodeORM } from '../infrastructure/database/postgres/entities/verification-code.orm'; -import { VerificationCodePostgresRepository } from '../infrastructure/database/postgres/verification-code-postgres.repository'; +import { VerificationCodePostgresRepository } from '../adapters/outbound/persistence/verification-code-postgres.repository'; import { VERIFICATION_CODE_REPOSITORY } from '../domain/repositories/verification-code.repository.interface'; import { UserModule } from '../user/user.module'; -import { AuthService } from './auth.service'; -import { AuthController } from './auth.controller'; +import { AuthService } from '../application/services/auth.service'; +import { AuthController } from '../adapters/inbound/auth.controller'; @Module({ imports: [ diff --git a/packages/services/user-service/src/user/user.module.ts b/packages/services/user-service/src/user/user.module.ts index 547a898..c3546bb 100644 --- a/packages/services/user-service/src/user/user.module.ts +++ b/packages/services/user-service/src/user/user.module.ts @@ -1,10 +1,10 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UserORM } from '../infrastructure/database/postgres/entities/user.orm'; -import { UserPostgresRepository } from '../infrastructure/database/postgres/user-postgres.repository'; +import { UserPostgresRepository } from '../adapters/outbound/persistence/user-postgres.repository'; import { USER_REPOSITORY } from '../domain/repositories/user.repository.interface'; -import { UserService } from './user.service'; -import { UserController } from './user.controller'; +import { UserService } from '../application/services/user.service'; +import { UserController } from '../adapters/inbound/user.controller'; @Module({ imports: [TypeOrmModule.forFeature([UserORM])], diff --git a/turbo.json b/turbo.json index 56f2678..653ad94 100644 --- a/turbo.json +++ b/turbo.json @@ -1,7 +1,7 @@ { "$schema": "https://turbo.build/schema.json", "globalDependencies": [".env"], - "pipeline": { + "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**", "build/**"]