refactor(services): implement 4-layer Clean Architecture for all backend services
Refactored all 6 backend services to 4-layer Clean Architecture pattern
following knowledge-service as reference implementation.
## Architecture Pattern (4-Layer)
```
src/
├── domain/ # Pure business entities and interfaces
│ ├── entities/ # Domain entities (no ORM decorators)
│ ├── repositories/ # Repository interfaces + Symbol tokens
│ └── value-objects/ # Enums and value types
├── application/
│ ├── dtos/ # Data transfer objects
│ └── services/ # Application services (use case orchestration)
├── adapters/
│ ├── inbound/ # Controllers, gateways (API endpoints)
│ └── outbound/
│ ├── persistence/ # Repository implementations
│ ├── clients/ # External service clients
│ └── storage/ # File storage adapters
└── infrastructure/
└── database/postgres/
└── entities/ # ORM entities with decorators
```
## Services Refactored
### user-service
- adapters/inbound: AuthController, UserController
- adapters/outbound/persistence: UserPostgresRepository, VerificationCodePostgresRepository
- application/services: AuthService, UserService
- application/dtos: AuthDto, UserDto
### payment-service
- adapters/inbound: OrderController, PaymentController
- adapters/outbound/persistence: OrderPostgresRepository, PaymentPostgresRepository
- adapters/outbound/payment-methods: AlipayAdapter, WechatPayAdapter, StripeAdapter
- application/services: OrderService, PaymentService
- application/dtos: OrderDto, PaymentDto
### file-service
- adapters/inbound: FileController
- adapters/outbound/persistence: FilePostgresRepository
- adapters/outbound/storage: MinioStorageAdapter
- application/services: FileService
- application/dtos: UploadFileDto
### conversation-service
- adapters/inbound: ConversationController, InternalController, ConversationGateway
- adapters/outbound/persistence: ConversationPostgresRepository, MessagePostgresRepository, TokenUsagePostgresRepository
- application/services: ConversationService
- application/dtos: ConversationDto
### knowledge-service
- adapters/inbound: KnowledgeController, MemoryController, InternalMemoryController
- adapters/outbound/persistence: KnowledgePostgresRepository, MemoryPostgresRepository
- application/services: KnowledgeService, MemoryService
- application/dtos: KnowledgeDto, MemoryDto
### evolution-service
- domain/entities: AdminEntity
- domain/repositories: IAdminRepository (Symbol-based DI)
- domain/value-objects: AdminRole enum
- adapters/inbound: AdminController, EvolutionController
- adapters/outbound/persistence: AdminPostgresRepository
- adapters/outbound/clients: ConversationClient, KnowledgeClient
- application/services: AdminService, EvolutionService
- application/dtos: AdminDto, EvolutionDto
- infrastructure/database/postgres/entities: AdminORM
## Key Improvements
- Symbol-based dependency injection for repository interfaces
- ORM entities separated from domain entities
- Consistent 4-layer structure across all services
- DTOs for API contracts
- Clear separation: domain logic vs infrastructure concerns
## Configuration
- Updated turbo.json: renamed "pipeline" to "tasks" for Turbo 2.0+
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9e1dca25f2
commit
afd707d15f
|
|
@ -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 {
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from './conversation.controller';
|
||||
export * from './conversation.gateway';
|
||||
export * from './internal.controller';
|
||||
|
|
@ -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<ConversationEntity>,
|
||||
@InjectRepository(MessageEntity)
|
||||
private messageRepo: Repository<MessageEntity>,
|
||||
@InjectRepository(ConversationORM)
|
||||
private conversationRepo: Repository<ConversationORM>,
|
||||
@InjectRepository(MessageORM)
|
||||
private messageRepo: Repository<MessageORM>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
|
@ -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,
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from './conversation-postgres.repository';
|
||||
export * from './message-postgres.repository';
|
||||
export * from './token-usage-postgres.repository';
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
@ -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[];
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './conversation.dto';
|
||||
|
|
@ -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<ConversationEntity> {
|
||||
async createConversation(params: CreateConversationParams): Promise<ConversationEntity> {
|
||||
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<StreamChunk> {
|
||||
async *sendMessage(params: SendMessageParams): AsyncGenerator<StreamChunk> {
|
||||
// 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);
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './conversation.service';
|
||||
|
|
@ -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])],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<TokenUsageEntity>,
|
||||
@InjectRepository(TokenUsageORM)
|
||||
private tokenUsageRepository: Repository<TokenUsageORM>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
|
@ -95,7 +95,7 @@ export class TokenUsageService {
|
|||
/**
|
||||
* 记录一次 API 调用的 token 使用量
|
||||
*/
|
||||
async recordUsage(input: TokenUsageInput): Promise<TokenUsageEntity> {
|
||||
async recordUsage(input: TokenUsageInput): Promise<TokenUsageORM> {
|
||||
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<string, TokenUsageEntity[]>();
|
||||
const byDate = new Map<string, TokenUsageORM[]>();
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -1,4 +1 @@
|
|||
export * from './entities';
|
||||
export * from './conversation-postgres.repository';
|
||||
export * from './message-postgres.repository';
|
||||
export * from './token-usage-postgres.repository';
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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 {
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './evolution.controller';
|
||||
export * from './admin.controller';
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './conversation.client';
|
||||
export * from './knowledge.client';
|
||||
|
|
@ -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<AdminORM>,
|
||||
) {}
|
||||
|
||||
async save(admin: AdminEntity): Promise<void> {
|
||||
const orm = this.toORM(admin);
|
||||
await this.adminRepo.save(orm);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<AdminEntity | null> {
|
||||
const orm = await this.adminRepo.findOne({ where: { id } });
|
||||
return orm ? this.toEntity(orm) : null;
|
||||
}
|
||||
|
||||
async findByUsername(username: string): Promise<AdminEntity | null> {
|
||||
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<AdminEntity[]> {
|
||||
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<number> {
|
||||
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<void> {
|
||||
const orm = this.toORM(admin);
|
||||
await this.adminRepo.save(orm);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './admin-postgres.repository';
|
||||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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[];
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './evolution.dto';
|
||||
export * from './admin.dto';
|
||||
|
|
@ -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, string[]> = {
|
||||
[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<AdminORM>,
|
||||
@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<LoginResult> {
|
||||
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<AdminORM> {
|
||||
}): Promise<AdminEntity> {
|
||||
// 检查用户名是否存在
|
||||
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<AdminORM> {
|
||||
const admin = await this.adminRepo.findOne({ where: { id: adminId } });
|
||||
): Promise<AdminEntity> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
||||
/**
|
||||
* 进化服务
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './evolution.service';
|
||||
export * from './admin.service';
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<void>;
|
||||
findById(id: string): Promise<AdminEntity | null>;
|
||||
findByUsername(username: string): Promise<AdminEntity | null>;
|
||||
findAll(options?: {
|
||||
role?: AdminRole;
|
||||
isActive?: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}): Promise<AdminEntity[]>;
|
||||
count(options?: {
|
||||
role?: AdminRole;
|
||||
isActive?: boolean;
|
||||
}): Promise<number>;
|
||||
update(admin: AdminEntity): Promise<void>;
|
||||
delete(id: string): Promise<void>;
|
||||
}
|
||||
|
||||
export const ADMIN_REPOSITORY = Symbol('IAdminRepository');
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './admin.repository.interface';
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* 管理员角色
|
||||
*/
|
||||
export enum AdminRole {
|
||||
SUPER_ADMIN = 'SUPER_ADMIN',
|
||||
ADMIN = 'ADMIN',
|
||||
OPERATOR = 'OPERATOR',
|
||||
VIEWER = 'VIEWER',
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色权限映射
|
||||
*/
|
||||
export const ROLE_PERMISSIONS: Record<AdminRole, string[]> = {
|
||||
[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',
|
||||
],
|
||||
};
|
||||
|
|
@ -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: [],
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 {
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './file.controller';
|
||||
|
|
@ -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 {
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './file-postgres.repository';
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './minio-storage.adapter';
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export * from './upload-file.dto';
|
||||
|
|
@ -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,
|
||||
);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './file.service';
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from './knowledge.controller';
|
||||
export * from './memory.controller';
|
||||
export * from './internal-memory.controller';
|
||||
|
|
@ -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 控制器
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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 {
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './knowledge-postgres.repository';
|
||||
export * from './memory-postgres.repository';
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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 {
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './knowledge.dto';
|
||||
export * from './memory.dto';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export * from './knowledge.service';
|
||||
export * from './memory.service';
|
||||
export * from './rag.service';
|
||||
export * from './chunking.service';
|
||||
|
|
@ -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';
|
||||
|
||||
/**
|
||||
* 知识管理服务
|
||||
|
|
@ -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';
|
||||
|
||||
/**
|
||||
* 记忆管理服务
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './order.controller';
|
||||
export * from './payment.controller';
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from './alipay.adapter';
|
||||
export * from './wechat-pay.adapter';
|
||||
export * from './stripe.adapter';
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './order-postgres.repository';
|
||||
export * from './payment-postgres.repository';
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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 {
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './order.dto';
|
||||
export * from './payment.dto';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './order.service';
|
||||
export * from './payment.service';
|
||||
|
|
@ -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<string, Record<string, number>> = {
|
||||
|
|
@ -14,7 +14,7 @@ const SERVICE_PRICING: Record<string, Record<string, number>> = {
|
|||
},
|
||||
};
|
||||
|
||||
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<OrderEntity> {
|
||||
async createOrder(params: CreateOrderParams): Promise<OrderEntity> {
|
||||
// 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',
|
||||
});
|
||||
|
|
@ -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<PaymentResult> {
|
||||
const order = await this.orderService.findById(dto.orderId);
|
||||
async createPayment(params: CreatePaymentParams): Promise<PaymentResult> {
|
||||
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,
|
||||
|
|
@ -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])],
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './user.controller';
|
||||
export * from './auth.controller';
|
||||
|
|
@ -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) {
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './user-postgres.repository';
|
||||
export * from './verification-code-postgres.repository';
|
||||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './user.dto';
|
||||
export * from './auth.dto';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './user.service';
|
||||
export * from './auth.service';
|
||||
|
|
@ -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<UserEntity> {
|
||||
// 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<UserEntity> {
|
||||
async upgradeToRegistered(userId: string, phone: string): Promise<UserEntity> {
|
||||
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');
|
||||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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])],
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"globalDependencies": [".env"],
|
||||
"pipeline": {
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**", ".next/**", "build/**"]
|
||||
|
|
|
|||
Loading…
Reference in New Issue