feat(multi-tenant): apply tenant middleware and refactor repositories
- Apply TenantContextMiddleware to all 6 services - Add SimpleTenantFinder for services without direct tenant DB access - Add TenantFinderService for evolution-service with database access - Refactor 8 repositories to extend BaseTenantRepository: - user-postgres.repository.ts - verification-code-postgres.repository.ts - conversation-postgres.repository.ts - message-postgres.repository.ts - token-usage-postgres.repository.ts - file-postgres.repository.ts - order-postgres.repository.ts - payment-postgres.repository.ts - Add @iconsulting/shared dependency to evolution-service and knowledge-service - Configure middleware to exclude health and super-admin paths Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
422069be68
commit
1df5854825
|
|
@ -1,6 +1,7 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { BaseTenantRepository, TenantContextService } from '@iconsulting/shared';
|
||||
import { ConversationORM } from '../../../infrastructure/database/postgres/entities/conversation.orm';
|
||||
import { IConversationRepository } from '../../../domain/repositories/conversation.repository.interface';
|
||||
import {
|
||||
|
|
@ -9,20 +10,26 @@ import {
|
|||
} from '../../../domain/entities/conversation.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ConversationPostgresRepository implements IConversationRepository {
|
||||
export class ConversationPostgresRepository
|
||||
extends BaseTenantRepository<ConversationEntity, ConversationORM>
|
||||
implements IConversationRepository
|
||||
{
|
||||
constructor(
|
||||
@InjectRepository(ConversationORM)
|
||||
private readonly repo: Repository<ConversationORM>,
|
||||
) {}
|
||||
@InjectRepository(ConversationORM) repo: Repository<ConversationORM>,
|
||||
tenantContext: TenantContextService,
|
||||
) {
|
||||
super(repo, tenantContext);
|
||||
}
|
||||
|
||||
async save(conversation: ConversationEntity): Promise<ConversationEntity> {
|
||||
const orm = this.toORM(conversation);
|
||||
orm.tenantId = this.getTenantId();
|
||||
const saved = await this.repo.save(orm);
|
||||
return this.toEntity(saved);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<ConversationEntity | null> {
|
||||
const orm = await this.repo.findOne({ where: { id } });
|
||||
const orm = await this.findOneWithTenant({ id } as any);
|
||||
return orm ? this.toEntity(orm) : null;
|
||||
}
|
||||
|
||||
|
|
@ -30,9 +37,8 @@ export class ConversationPostgresRepository implements IConversationRepository {
|
|||
userId: string,
|
||||
options?: { status?: ConversationStatusType; limit?: number },
|
||||
): Promise<ConversationEntity[]> {
|
||||
const queryBuilder = this.repo
|
||||
.createQueryBuilder('conversation')
|
||||
.where('conversation.user_id = :userId', { userId });
|
||||
const queryBuilder = this.createTenantQueryBuilder('conversation')
|
||||
.andWhere('conversation.user_id = :userId', { userId });
|
||||
|
||||
if (options?.status) {
|
||||
queryBuilder.andWhere('conversation.status = :status', { status: options.status });
|
||||
|
|
@ -53,7 +59,7 @@ export class ConversationPostgresRepository implements IConversationRepository {
|
|||
hoursBack?: number;
|
||||
minMessageCount?: number;
|
||||
}): Promise<ConversationEntity[]> {
|
||||
const queryBuilder = this.repo.createQueryBuilder('conversation');
|
||||
const queryBuilder = this.createTenantQueryBuilder('conversation');
|
||||
|
||||
if (options.status) {
|
||||
queryBuilder.andWhere('conversation.status = :status', { status: options.status });
|
||||
|
|
@ -77,12 +83,13 @@ export class ConversationPostgresRepository implements IConversationRepository {
|
|||
|
||||
async update(conversation: ConversationEntity): Promise<ConversationEntity> {
|
||||
const orm = this.toORM(conversation);
|
||||
orm.tenantId = this.getTenantId();
|
||||
const updated = await this.repo.save(orm);
|
||||
return this.toEntity(updated);
|
||||
}
|
||||
|
||||
async count(options?: { status?: ConversationStatusType; daysBack?: number }): Promise<number> {
|
||||
const queryBuilder = this.repo.createQueryBuilder('conversation');
|
||||
const queryBuilder = this.createTenantQueryBuilder('conversation');
|
||||
|
||||
if (options?.status) {
|
||||
queryBuilder.andWhere('conversation.status = :status', { status: options.status });
|
||||
|
|
@ -100,6 +107,7 @@ export class ConversationPostgresRepository implements IConversationRepository {
|
|||
private toORM(entity: ConversationEntity): ConversationORM {
|
||||
const orm = new ConversationORM();
|
||||
orm.id = entity.id;
|
||||
orm.tenantId = this.getTenantId();
|
||||
orm.userId = entity.userId;
|
||||
orm.status = entity.status;
|
||||
orm.title = entity.title;
|
||||
|
|
|
|||
|
|
@ -1,43 +1,53 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { BaseTenantRepository, TenantContextService } from '@iconsulting/shared';
|
||||
import { MessageORM } from '../../../infrastructure/database/postgres/entities/message.orm';
|
||||
import { IMessageRepository } from '../../../domain/repositories/message.repository.interface';
|
||||
import { MessageEntity } from '../../../domain/entities/message.entity';
|
||||
|
||||
@Injectable()
|
||||
export class MessagePostgresRepository implements IMessageRepository {
|
||||
export class MessagePostgresRepository
|
||||
extends BaseTenantRepository<MessageEntity, MessageORM>
|
||||
implements IMessageRepository
|
||||
{
|
||||
constructor(
|
||||
@InjectRepository(MessageORM)
|
||||
private readonly repo: Repository<MessageORM>,
|
||||
) {}
|
||||
@InjectRepository(MessageORM) repo: Repository<MessageORM>,
|
||||
tenantContext: TenantContextService,
|
||||
) {
|
||||
super(repo, tenantContext);
|
||||
}
|
||||
|
||||
async save(message: MessageEntity): Promise<MessageEntity> {
|
||||
const orm = this.toORM(message);
|
||||
orm.tenantId = this.getTenantId();
|
||||
const saved = await this.repo.save(orm);
|
||||
return this.toEntity(saved);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<MessageEntity | null> {
|
||||
const orm = await this.repo.findOne({ where: { id } });
|
||||
const orm = await this.findOneWithTenant({ id } as any);
|
||||
return orm ? this.toEntity(orm) : null;
|
||||
}
|
||||
|
||||
async findByConversationId(conversationId: string): Promise<MessageEntity[]> {
|
||||
const orms = await this.repo.find({
|
||||
where: { conversationId },
|
||||
order: { createdAt: 'ASC' },
|
||||
});
|
||||
const orms = await this.createTenantQueryBuilder('message')
|
||||
.andWhere('message.conversation_id = :conversationId', { conversationId })
|
||||
.orderBy('message.created_at', 'ASC')
|
||||
.getMany();
|
||||
return orms.map((orm) => this.toEntity(orm));
|
||||
}
|
||||
|
||||
async countByConversationId(conversationId: string): Promise<number> {
|
||||
return this.repo.count({ where: { conversationId } });
|
||||
return this.createTenantQueryBuilder('message')
|
||||
.andWhere('message.conversation_id = :conversationId', { conversationId })
|
||||
.getCount();
|
||||
}
|
||||
|
||||
private toORM(entity: MessageEntity): MessageORM {
|
||||
const orm = new MessageORM();
|
||||
orm.id = entity.id;
|
||||
orm.tenantId = this.getTenantId();
|
||||
orm.conversationId = entity.conversationId;
|
||||
orm.role = entity.role;
|
||||
orm.type = entity.type;
|
||||
|
|
|
|||
|
|
@ -1,35 +1,41 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { BaseTenantRepository, TenantContextService } from '@iconsulting/shared';
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class TokenUsagePostgresRepository implements ITokenUsageRepository {
|
||||
export class TokenUsagePostgresRepository
|
||||
extends BaseTenantRepository<TokenUsageEntity, TokenUsageORM>
|
||||
implements ITokenUsageRepository
|
||||
{
|
||||
constructor(
|
||||
@InjectRepository(TokenUsageORM)
|
||||
private readonly repo: Repository<TokenUsageORM>,
|
||||
) {}
|
||||
@InjectRepository(TokenUsageORM) repo: Repository<TokenUsageORM>,
|
||||
tenantContext: TenantContextService,
|
||||
) {
|
||||
super(repo, tenantContext);
|
||||
}
|
||||
|
||||
async save(tokenUsage: TokenUsageEntity): Promise<TokenUsageEntity> {
|
||||
const orm = this.toORM(tokenUsage);
|
||||
orm.tenantId = this.getTenantId();
|
||||
const saved = await this.repo.save(orm);
|
||||
return this.toEntity(saved);
|
||||
}
|
||||
|
||||
async findByConversationId(conversationId: string): Promise<TokenUsageEntity[]> {
|
||||
const orms = await this.repo.find({
|
||||
where: { conversationId },
|
||||
order: { createdAt: 'ASC' },
|
||||
});
|
||||
const orms = await this.createTenantQueryBuilder('token_usage')
|
||||
.andWhere('token_usage.conversation_id = :conversationId', { conversationId })
|
||||
.orderBy('token_usage.created_at', 'ASC')
|
||||
.getMany();
|
||||
return orms.map((orm) => this.toEntity(orm));
|
||||
}
|
||||
|
||||
async findByUserId(userId: string, options?: { limit?: number }): Promise<TokenUsageEntity[]> {
|
||||
const queryBuilder = this.repo
|
||||
.createQueryBuilder('token_usage')
|
||||
.where('token_usage.user_id = :userId', { userId })
|
||||
const queryBuilder = this.createTenantQueryBuilder('token_usage')
|
||||
.andWhere('token_usage.user_id = :userId', { userId })
|
||||
.orderBy('token_usage.created_at', 'DESC');
|
||||
|
||||
if (options?.limit) {
|
||||
|
|
@ -45,12 +51,11 @@ export class TokenUsagePostgresRepository implements ITokenUsageRepository {
|
|||
totalOutputTokens: number;
|
||||
totalCost: number;
|
||||
}> {
|
||||
const result = await this.repo
|
||||
.createQueryBuilder('token_usage')
|
||||
const result = await this.createTenantQueryBuilder('token_usage')
|
||||
.select('SUM(token_usage.input_tokens)', 'totalInputTokens')
|
||||
.addSelect('SUM(token_usage.output_tokens)', 'totalOutputTokens')
|
||||
.addSelect('SUM(token_usage.estimated_cost)', 'totalCost')
|
||||
.where('token_usage.conversation_id = :conversationId', { conversationId })
|
||||
.andWhere('token_usage.conversation_id = :conversationId', { conversationId })
|
||||
.getRawOne();
|
||||
|
||||
return {
|
||||
|
|
@ -65,12 +70,11 @@ export class TokenUsagePostgresRepository implements ITokenUsageRepository {
|
|||
totalOutputTokens: number;
|
||||
totalCost: number;
|
||||
}> {
|
||||
const result = await this.repo
|
||||
.createQueryBuilder('token_usage')
|
||||
const result = await this.createTenantQueryBuilder('token_usage')
|
||||
.select('SUM(token_usage.input_tokens)', 'totalInputTokens')
|
||||
.addSelect('SUM(token_usage.output_tokens)', 'totalOutputTokens')
|
||||
.addSelect('SUM(token_usage.estimated_cost)', 'totalCost')
|
||||
.where('token_usage.user_id = :userId', { userId })
|
||||
.andWhere('token_usage.user_id = :userId', { userId })
|
||||
.getRawOne();
|
||||
|
||||
return {
|
||||
|
|
@ -83,6 +87,7 @@ export class TokenUsagePostgresRepository implements ITokenUsageRepository {
|
|||
private toORM(entity: TokenUsageEntity): TokenUsageORM {
|
||||
const orm = new TokenUsageORM();
|
||||
orm.id = entity.id;
|
||||
orm.tenantId = this.getTenantId();
|
||||
orm.userId = entity.userId;
|
||||
orm.conversationId = entity.conversationId;
|
||||
orm.messageId = entity.messageId;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import {
|
||||
TenantContextService,
|
||||
TenantContextMiddleware,
|
||||
SimpleTenantFinder,
|
||||
DEFAULT_TENANT_ID,
|
||||
} from '@iconsulting/shared';
|
||||
import { ConversationModule } from './conversation/conversation.module';
|
||||
import { ClaudeModule } from './infrastructure/claude/claude.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
|
|
@ -47,5 +53,31 @@ import { HealthModule } from './health/health.module';
|
|||
ConversationModule,
|
||||
ClaudeModule,
|
||||
],
|
||||
providers: [TenantContextService, SimpleTenantFinder],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule implements NestModule {
|
||||
constructor(
|
||||
private readonly tenantContext: TenantContextService,
|
||||
private readonly tenantFinder: SimpleTenantFinder,
|
||||
) {}
|
||||
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
const tenantMiddleware = new TenantContextMiddleware(
|
||||
this.tenantContext,
|
||||
this.tenantFinder,
|
||||
{
|
||||
allowDefaultTenant: true,
|
||||
defaultTenantId: DEFAULT_TENANT_ID,
|
||||
excludePaths: ['/health', '/health/*'],
|
||||
},
|
||||
);
|
||||
|
||||
consumer
|
||||
.apply((req: any, res: any, next: any) => tenantMiddleware.use(req, res, next))
|
||||
.exclude(
|
||||
{ path: 'health', method: RequestMethod.GET },
|
||||
{ path: 'health/(.*)', method: RequestMethod.ALL },
|
||||
)
|
||||
.forRoutes('*');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"test:cov": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconsulting/shared": "workspace:*",
|
||||
"@anthropic-ai/sdk": "^0.52.0",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.2.0",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import {
|
||||
TenantContextService,
|
||||
TenantContextMiddleware,
|
||||
DEFAULT_TENANT_ID,
|
||||
} from '@iconsulting/shared';
|
||||
import { EvolutionModule } from './evolution/evolution.module';
|
||||
import { AdminModule } from './admin/admin.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { AnalyticsModule } from './analytics/analytics.module';
|
||||
import { TenantModule } from './infrastructure/tenant/tenant.module';
|
||||
import { TenantFinderService } from './infrastructure/tenant/tenant-finder.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -36,6 +43,9 @@ import { AnalyticsModule } from './analytics/analytics.module';
|
|||
}),
|
||||
}),
|
||||
|
||||
// 租户模块
|
||||
TenantModule,
|
||||
|
||||
// Health check
|
||||
HealthModule,
|
||||
|
||||
|
|
@ -44,5 +54,34 @@ import { AnalyticsModule } from './analytics/analytics.module';
|
|||
AdminModule,
|
||||
AnalyticsModule,
|
||||
],
|
||||
providers: [TenantContextService],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule implements NestModule {
|
||||
constructor(
|
||||
private readonly tenantContext: TenantContextService,
|
||||
private readonly tenantFinder: TenantFinderService,
|
||||
) {}
|
||||
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
// 创建租户中间件
|
||||
const tenantMiddleware = new TenantContextMiddleware(
|
||||
this.tenantContext,
|
||||
this.tenantFinder,
|
||||
{
|
||||
allowDefaultTenant: true,
|
||||
defaultTenantId: DEFAULT_TENANT_ID,
|
||||
excludePaths: ['/health', '/health/*', '/super-admin/*'],
|
||||
},
|
||||
);
|
||||
|
||||
consumer
|
||||
.apply((req: any, res: any, next: any) => tenantMiddleware.use(req, res, next))
|
||||
.exclude(
|
||||
{ path: 'health', method: RequestMethod.GET },
|
||||
{ path: 'health/(.*)', method: RequestMethod.ALL },
|
||||
{ path: 'super-admin', method: RequestMethod.ALL },
|
||||
{ path: 'super-admin/(.*)', method: RequestMethod.ALL },
|
||||
)
|
||||
.forRoutes('*');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ITenantFinder, TenantContext, TenantStatus, TenantPlan } from '@iconsulting/shared';
|
||||
import { TenantORM } from '../database/postgres/entities/tenant.orm';
|
||||
|
||||
/**
|
||||
* 租户查找器实现 (evolution-service)
|
||||
* 从数据库直接查询租户信息
|
||||
*/
|
||||
@Injectable()
|
||||
export class TenantFinderService implements ITenantFinder {
|
||||
// 内存缓存
|
||||
private cache = new Map<string, { tenant: TenantContext; expiresAt: number }>();
|
||||
private slugCache = new Map<string, string>(); // slug -> id 映射
|
||||
private readonly cacheTtlMs = 5 * 60 * 1000; // 5 分钟
|
||||
|
||||
constructor(
|
||||
@InjectRepository(TenantORM)
|
||||
private readonly tenantRepo: Repository<TenantORM>,
|
||||
) {}
|
||||
|
||||
async findById(id: string): Promise<TenantContext | null> {
|
||||
// 检查缓存
|
||||
const cached = this.cache.get(id);
|
||||
if (cached && cached.expiresAt > Date.now()) {
|
||||
return cached.tenant;
|
||||
}
|
||||
|
||||
// 从数据库查询
|
||||
const orm = await this.tenantRepo.findOne({ where: { id } });
|
||||
if (!orm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tenant = this.toContext(orm);
|
||||
this.cache.set(id, { tenant, expiresAt: Date.now() + this.cacheTtlMs });
|
||||
this.slugCache.set(orm.slug, orm.id);
|
||||
|
||||
return tenant;
|
||||
}
|
||||
|
||||
async findBySlug(slug: string): Promise<TenantContext | null> {
|
||||
// 检查 slug 缓存
|
||||
const cachedId = this.slugCache.get(slug);
|
||||
if (cachedId) {
|
||||
return this.findById(cachedId);
|
||||
}
|
||||
|
||||
// 从数据库查询
|
||||
const orm = await this.tenantRepo.findOne({ where: { slug } });
|
||||
if (!orm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tenant = this.toContext(orm);
|
||||
this.cache.set(orm.id, { tenant, expiresAt: Date.now() + this.cacheTtlMs });
|
||||
this.slugCache.set(slug, orm.id);
|
||||
|
||||
return tenant;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使缓存失效
|
||||
*/
|
||||
invalidate(tenantId: string): void {
|
||||
const cached = this.cache.get(tenantId);
|
||||
if (cached) {
|
||||
// 清除 slug 缓存
|
||||
for (const [slug, id] of this.slugCache.entries()) {
|
||||
if (id === tenantId) {
|
||||
this.slugCache.delete(slug);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.cache.delete(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有缓存
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.cache.clear();
|
||||
this.slugCache.clear();
|
||||
}
|
||||
|
||||
private toContext(orm: TenantORM): TenantContext {
|
||||
return {
|
||||
id: orm.id,
|
||||
slug: orm.slug,
|
||||
name: orm.name,
|
||||
status: orm.status as TenantStatus,
|
||||
plan: orm.plan as TenantPlan,
|
||||
config: orm.config || {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { TenantORM } from '../database/postgres/entities/tenant.orm';
|
||||
import { TenantFinderService } from './tenant-finder.service';
|
||||
import { TENANT_FINDER } from '@iconsulting/shared';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([TenantORM])],
|
||||
providers: [
|
||||
TenantFinderService,
|
||||
{
|
||||
provide: TENANT_FINDER,
|
||||
useExisting: TenantFinderService,
|
||||
},
|
||||
],
|
||||
exports: [TenantFinderService, TENANT_FINDER],
|
||||
})
|
||||
export class TenantModule {}
|
||||
|
|
@ -1,30 +1,37 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { BaseTenantRepository, TenantContextService } from '@iconsulting/shared';
|
||||
import { IFileRepository } from '../../../domain/repositories/file.repository.interface';
|
||||
import { FileEntity, FileStatus } from '../../../domain/entities/file.entity';
|
||||
import { FileORM } from '../../../infrastructure/database/postgres/entities/file.orm';
|
||||
|
||||
@Injectable()
|
||||
export class FilePostgresRepository implements IFileRepository {
|
||||
export class FilePostgresRepository
|
||||
extends BaseTenantRepository<FileEntity, FileORM>
|
||||
implements IFileRepository
|
||||
{
|
||||
constructor(
|
||||
@InjectRepository(FileORM)
|
||||
private readonly repo: Repository<FileORM>,
|
||||
) {}
|
||||
@InjectRepository(FileORM) repo: Repository<FileORM>,
|
||||
tenantContext: TenantContextService,
|
||||
) {
|
||||
super(repo, tenantContext);
|
||||
}
|
||||
|
||||
async save(file: FileEntity): Promise<FileEntity> {
|
||||
const orm = this.toORM(file);
|
||||
orm.tenantId = this.getTenantId();
|
||||
const saved = await this.repo.save(orm);
|
||||
return this.toEntity(saved);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<FileEntity | null> {
|
||||
const orm = await this.repo.findOne({ where: { id } });
|
||||
const orm = await this.findOneWithTenant({ id } as any);
|
||||
return orm ? this.toEntity(orm) : null;
|
||||
}
|
||||
|
||||
async findByIdAndUser(id: string, userId: string): Promise<FileEntity | null> {
|
||||
const orm = await this.repo.findOne({ where: { id, userId } });
|
||||
const orm = await this.findOneWithTenant({ id, userId } as any);
|
||||
return orm ? this.toEntity(orm) : null;
|
||||
}
|
||||
|
||||
|
|
@ -33,7 +40,7 @@ export class FilePostgresRepository implements IFileRepository {
|
|||
userId: string,
|
||||
status: FileStatus,
|
||||
): Promise<FileEntity | null> {
|
||||
const orm = await this.repo.findOne({ where: { id, userId, status } });
|
||||
const orm = await this.findOneWithTenant({ id, userId, status } as any);
|
||||
return orm ? this.toEntity(orm) : null;
|
||||
}
|
||||
|
||||
|
|
@ -42,20 +49,21 @@ export class FilePostgresRepository implements IFileRepository {
|
|||
status: FileStatus,
|
||||
conversationId?: string,
|
||||
): Promise<FileEntity[]> {
|
||||
const where: Record<string, unknown> = { userId, status };
|
||||
const queryBuilder = this.createTenantQueryBuilder('file')
|
||||
.andWhere('file.user_id = :userId', { userId })
|
||||
.andWhere('file.status = :status', { status });
|
||||
|
||||
if (conversationId) {
|
||||
where.conversationId = conversationId;
|
||||
queryBuilder.andWhere('file.conversation_id = :conversationId', { conversationId });
|
||||
}
|
||||
|
||||
const orms = await this.repo.find({
|
||||
where,
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
const orms = await queryBuilder.orderBy('file.created_at', 'DESC').getMany();
|
||||
return orms.map((orm) => this.toEntity(orm));
|
||||
}
|
||||
|
||||
async update(file: FileEntity): Promise<FileEntity> {
|
||||
const orm = this.toORM(file);
|
||||
orm.tenantId = this.getTenantId();
|
||||
const saved = await this.repo.save(orm);
|
||||
return this.toEntity(saved);
|
||||
}
|
||||
|
|
@ -63,6 +71,7 @@ export class FilePostgresRepository implements IFileRepository {
|
|||
private toORM(entity: FileEntity): FileORM {
|
||||
const orm = new FileORM();
|
||||
orm.id = entity.id;
|
||||
orm.tenantId = this.getTenantId();
|
||||
orm.userId = entity.userId;
|
||||
orm.conversationId = entity.conversationId;
|
||||
orm.originalName = entity.originalName;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import {
|
||||
TenantContextService,
|
||||
TenantContextMiddleware,
|
||||
SimpleTenantFinder,
|
||||
DEFAULT_TENANT_ID,
|
||||
} from '@iconsulting/shared';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { FileModule } from './file/file.module';
|
||||
|
||||
|
|
@ -35,5 +41,31 @@ import { FileModule } from './file/file.module';
|
|||
// 功能模块
|
||||
FileModule,
|
||||
],
|
||||
providers: [TenantContextService, SimpleTenantFinder],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule implements NestModule {
|
||||
constructor(
|
||||
private readonly tenantContext: TenantContextService,
|
||||
private readonly tenantFinder: SimpleTenantFinder,
|
||||
) {}
|
||||
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
const tenantMiddleware = new TenantContextMiddleware(
|
||||
this.tenantContext,
|
||||
this.tenantFinder,
|
||||
{
|
||||
allowDefaultTenant: true,
|
||||
defaultTenantId: DEFAULT_TENANT_ID,
|
||||
excludePaths: ['/health', '/health/*'],
|
||||
},
|
||||
);
|
||||
|
||||
consumer
|
||||
.apply((req: any, res: any, next: any) => tenantMiddleware.use(req, res, next))
|
||||
.exclude(
|
||||
{ path: 'health', method: RequestMethod.GET },
|
||||
{ path: 'health/(.*)', method: RequestMethod.ALL },
|
||||
)
|
||||
.forRoutes('*');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"test:cov": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconsulting/shared": "workspace:*",
|
||||
"@anthropic-ai/sdk": "^0.52.0",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.2.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import {
|
||||
TenantContextService,
|
||||
TenantContextMiddleware,
|
||||
SimpleTenantFinder,
|
||||
DEFAULT_TENANT_ID,
|
||||
} from '@iconsulting/shared';
|
||||
import { KnowledgeModule } from './knowledge/knowledge.module';
|
||||
import { MemoryModule } from './memory/memory.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
|
|
@ -38,5 +44,31 @@ import { HealthModule } from './health/health.module';
|
|||
KnowledgeModule,
|
||||
MemoryModule,
|
||||
],
|
||||
providers: [TenantContextService, SimpleTenantFinder],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule implements NestModule {
|
||||
constructor(
|
||||
private readonly tenantContext: TenantContextService,
|
||||
private readonly tenantFinder: SimpleTenantFinder,
|
||||
) {}
|
||||
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
const tenantMiddleware = new TenantContextMiddleware(
|
||||
this.tenantContext,
|
||||
this.tenantFinder,
|
||||
{
|
||||
allowDefaultTenant: true,
|
||||
defaultTenantId: DEFAULT_TENANT_ID,
|
||||
excludePaths: ['/health', '/health/*'],
|
||||
},
|
||||
);
|
||||
|
||||
consumer
|
||||
.apply((req: any, res: any, next: any) => tenantMiddleware.use(req, res, next))
|
||||
.exclude(
|
||||
{ path: 'health', method: RequestMethod.GET },
|
||||
{ path: 'health/(.*)', method: RequestMethod.ALL },
|
||||
)
|
||||
.forRoutes('*');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,46 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { BaseTenantRepository, TenantContextService } from '@iconsulting/shared';
|
||||
import { IOrderRepository } from '../../../domain/repositories/order.repository.interface';
|
||||
import { OrderEntity } from '../../../domain/entities/order.entity';
|
||||
import { OrderORM } from '../../../infrastructure/database/postgres/entities/order.orm';
|
||||
|
||||
@Injectable()
|
||||
export class OrderPostgresRepository implements IOrderRepository {
|
||||
export class OrderPostgresRepository
|
||||
extends BaseTenantRepository<OrderEntity, OrderORM>
|
||||
implements IOrderRepository
|
||||
{
|
||||
constructor(
|
||||
@InjectRepository(OrderORM)
|
||||
private readonly repo: Repository<OrderORM>,
|
||||
) {}
|
||||
@InjectRepository(OrderORM) repo: Repository<OrderORM>,
|
||||
tenantContext: TenantContextService,
|
||||
) {
|
||||
super(repo, tenantContext);
|
||||
}
|
||||
|
||||
async save(order: OrderEntity): Promise<OrderEntity> {
|
||||
const orm = this.toORM(order);
|
||||
orm.tenantId = this.getTenantId();
|
||||
const saved = await this.repo.save(orm);
|
||||
return this.toEntity(saved);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<OrderEntity | null> {
|
||||
const orm = await this.repo.findOne({ where: { id } });
|
||||
const orm = await this.findOneWithTenant({ id } as any);
|
||||
return orm ? this.toEntity(orm) : null;
|
||||
}
|
||||
|
||||
async findByUserId(userId: string): Promise<OrderEntity[]> {
|
||||
const orms = await this.repo.find({
|
||||
where: { userId },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
const orms = await this.createTenantQueryBuilder('order')
|
||||
.andWhere('order.user_id = :userId', { userId })
|
||||
.orderBy('order.created_at', 'DESC')
|
||||
.getMany();
|
||||
return orms.map((orm) => this.toEntity(orm));
|
||||
}
|
||||
|
||||
async update(order: OrderEntity): Promise<OrderEntity> {
|
||||
const orm = this.toORM(order);
|
||||
orm.tenantId = this.getTenantId();
|
||||
const saved = await this.repo.save(orm);
|
||||
return this.toEntity(saved);
|
||||
}
|
||||
|
|
@ -40,6 +48,7 @@ export class OrderPostgresRepository implements IOrderRepository {
|
|||
private toORM(entity: OrderEntity): OrderORM {
|
||||
const orm = new OrderORM();
|
||||
orm.id = entity.id;
|
||||
orm.tenantId = this.getTenantId();
|
||||
orm.userId = entity.userId;
|
||||
orm.conversationId = entity.conversationId;
|
||||
orm.serviceType = entity.serviceType;
|
||||
|
|
|
|||
|
|
@ -1,44 +1,48 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { BaseTenantRepository, TenantContextService } from '@iconsulting/shared';
|
||||
import { IPaymentRepository } from '../../../domain/repositories/payment.repository.interface';
|
||||
import { PaymentEntity, PaymentStatus } from '../../../domain/entities/payment.entity';
|
||||
import { PaymentORM } from '../../../infrastructure/database/postgres/entities/payment.orm';
|
||||
|
||||
@Injectable()
|
||||
export class PaymentPostgresRepository implements IPaymentRepository {
|
||||
export class PaymentPostgresRepository
|
||||
extends BaseTenantRepository<PaymentEntity, PaymentORM>
|
||||
implements IPaymentRepository
|
||||
{
|
||||
constructor(
|
||||
@InjectRepository(PaymentORM)
|
||||
private readonly repo: Repository<PaymentORM>,
|
||||
) {}
|
||||
@InjectRepository(PaymentORM) repo: Repository<PaymentORM>,
|
||||
tenantContext: TenantContextService,
|
||||
) {
|
||||
super(repo, tenantContext);
|
||||
}
|
||||
|
||||
async save(payment: PaymentEntity): Promise<PaymentEntity> {
|
||||
const orm = this.toORM(payment);
|
||||
orm.tenantId = this.getTenantId();
|
||||
const saved = await this.repo.save(orm);
|
||||
return this.toEntity(saved);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<PaymentEntity | null> {
|
||||
const orm = await this.repo.findOne({ where: { id } });
|
||||
const orm = await this.findOneWithTenant({ id } as any);
|
||||
return orm ? this.toEntity(orm) : null;
|
||||
}
|
||||
|
||||
async findPendingByOrderId(orderId: string): Promise<PaymentEntity | null> {
|
||||
const orm = await this.repo.findOne({
|
||||
where: { orderId, status: PaymentStatus.PENDING },
|
||||
});
|
||||
const orm = await this.findOneWithTenant({ orderId, status: PaymentStatus.PENDING } as any);
|
||||
return orm ? this.toEntity(orm) : null;
|
||||
}
|
||||
|
||||
async findByTransactionId(transactionId: string): Promise<PaymentEntity | null> {
|
||||
const orm = await this.repo.findOne({
|
||||
where: { transactionId },
|
||||
});
|
||||
const orm = await this.findOneWithTenant({ transactionId } as any);
|
||||
return orm ? this.toEntity(orm) : null;
|
||||
}
|
||||
|
||||
async update(payment: PaymentEntity): Promise<PaymentEntity> {
|
||||
const orm = this.toORM(payment);
|
||||
orm.tenantId = this.getTenantId();
|
||||
const saved = await this.repo.save(orm);
|
||||
return this.toEntity(saved);
|
||||
}
|
||||
|
|
@ -46,6 +50,7 @@ export class PaymentPostgresRepository implements IPaymentRepository {
|
|||
private toORM(entity: PaymentEntity): PaymentORM {
|
||||
const orm = new PaymentORM();
|
||||
orm.id = entity.id;
|
||||
orm.tenantId = this.getTenantId();
|
||||
orm.orderId = entity.orderId;
|
||||
orm.method = entity.method;
|
||||
orm.amount = entity.amount;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import {
|
||||
TenantContextService,
|
||||
TenantContextMiddleware,
|
||||
SimpleTenantFinder,
|
||||
DEFAULT_TENANT_ID,
|
||||
} from '@iconsulting/shared';
|
||||
import { PaymentModule } from './payment/payment.module';
|
||||
import { OrderModule } from './order/order.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
|
|
@ -50,5 +56,31 @@ import { HealthModule } from './health/health.module';
|
|||
OrderModule,
|
||||
PaymentModule,
|
||||
],
|
||||
providers: [TenantContextService, SimpleTenantFinder],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule implements NestModule {
|
||||
constructor(
|
||||
private readonly tenantContext: TenantContextService,
|
||||
private readonly tenantFinder: SimpleTenantFinder,
|
||||
) {}
|
||||
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
const tenantMiddleware = new TenantContextMiddleware(
|
||||
this.tenantContext,
|
||||
this.tenantFinder,
|
||||
{
|
||||
allowDefaultTenant: true,
|
||||
defaultTenantId: DEFAULT_TENANT_ID,
|
||||
excludePaths: ['/health', '/health/*'],
|
||||
},
|
||||
);
|
||||
|
||||
consumer
|
||||
.apply((req: any, res: any, next: any) => tenantMiddleware.use(req, res, next))
|
||||
.exclude(
|
||||
{ path: 'health', method: RequestMethod.GET },
|
||||
{ path: 'health/(.*)', method: RequestMethod.ALL },
|
||||
)
|
||||
.forRoutes('*');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, Like, FindOptionsWhere } from 'typeorm';
|
||||
import { BaseTenantRepository, TenantContextService } from '@iconsulting/shared';
|
||||
import {
|
||||
IUserRepository,
|
||||
UserQueryOptions,
|
||||
|
|
@ -10,35 +11,41 @@ import { UserEntity, UserType } from '../../../domain/entities/user.entity';
|
|||
import { UserORM } from '../../../infrastructure/database/postgres/entities/user.orm';
|
||||
|
||||
@Injectable()
|
||||
export class UserPostgresRepository implements IUserRepository {
|
||||
export class UserPostgresRepository
|
||||
extends BaseTenantRepository<UserEntity, UserORM>
|
||||
implements IUserRepository
|
||||
{
|
||||
constructor(
|
||||
@InjectRepository(UserORM)
|
||||
private readonly repo: Repository<UserORM>,
|
||||
) {}
|
||||
@InjectRepository(UserORM) repo: Repository<UserORM>,
|
||||
tenantContext: TenantContextService,
|
||||
) {
|
||||
super(repo, tenantContext);
|
||||
}
|
||||
|
||||
async save(user: UserEntity): Promise<UserEntity> {
|
||||
const orm = this.toORM(user);
|
||||
orm.tenantId = this.getTenantId();
|
||||
const saved = await this.repo.save(orm);
|
||||
return this.toEntity(saved);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<UserEntity | null> {
|
||||
const orm = await this.repo.findOne({ where: { id } });
|
||||
const orm = await this.findOneWithTenant({ id } as any);
|
||||
return orm ? this.toEntity(orm) : null;
|
||||
}
|
||||
|
||||
async findByPhone(phone: string): Promise<UserEntity | null> {
|
||||
const orm = await this.repo.findOne({ where: { phone } });
|
||||
const orm = await this.findOneWithTenant({ phone } as any);
|
||||
return orm ? this.toEntity(orm) : null;
|
||||
}
|
||||
|
||||
async findByFingerprint(fingerprint: string): Promise<UserEntity | null> {
|
||||
const orm = await this.repo.findOne({ where: { fingerprint } });
|
||||
const orm = await this.findOneWithTenant({ fingerprint } as any);
|
||||
return orm ? this.toEntity(orm) : null;
|
||||
}
|
||||
|
||||
async updateLastActive(userId: string): Promise<void> {
|
||||
await this.repo.update(userId, { lastActiveAt: new Date() });
|
||||
await this.updateWithTenant(userId, { lastActiveAt: new Date() } as any);
|
||||
}
|
||||
|
||||
async findAll(options: UserQueryOptions): Promise<PaginatedUsers> {
|
||||
|
|
@ -46,25 +53,22 @@ export class UserPostgresRepository implements IUserRepository {
|
|||
const pageSize = options.pageSize || 20;
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
const where: FindOptionsWhere<UserORM> = {};
|
||||
const queryBuilder = this.createTenantQueryBuilder('user');
|
||||
|
||||
if (options.type) {
|
||||
where.type = options.type;
|
||||
queryBuilder.andWhere('user.type = :type', { type: options.type });
|
||||
}
|
||||
if (options.phone) {
|
||||
where.phone = Like(`%${options.phone}%`);
|
||||
queryBuilder.andWhere('user.phone LIKE :phone', { phone: `%${options.phone}%` });
|
||||
}
|
||||
if (options.nickname) {
|
||||
where.nickname = Like(`%${options.nickname}%`);
|
||||
queryBuilder.andWhere('user.nickname LIKE :nickname', { nickname: `%${options.nickname}%` });
|
||||
}
|
||||
|
||||
const [items, total] = await this.repo.findAndCount({
|
||||
where,
|
||||
order: {
|
||||
[options.sortBy || 'createdAt']: options.sortOrder || 'DESC',
|
||||
},
|
||||
skip,
|
||||
take: pageSize,
|
||||
});
|
||||
queryBuilder.orderBy(`user.${options.sortBy || 'createdAt'}`, options.sortOrder || 'DESC');
|
||||
queryBuilder.skip(skip).take(pageSize);
|
||||
|
||||
const [items, total] = await queryBuilder.getManyAndCount();
|
||||
|
||||
return {
|
||||
items: items.map((orm) => this.toEntity(orm)),
|
||||
|
|
@ -76,8 +80,7 @@ export class UserPostgresRepository implements IUserRepository {
|
|||
}
|
||||
|
||||
async countByType(): Promise<Record<UserType, number>> {
|
||||
const result = await this.repo
|
||||
.createQueryBuilder('user')
|
||||
const result = await this.createTenantQueryBuilder('user')
|
||||
.select('user.type', 'type')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.groupBy('user.type')
|
||||
|
|
@ -96,14 +99,13 @@ export class UserPostgresRepository implements IUserRepository {
|
|||
}
|
||||
|
||||
async search(keyword: string, limit = 10): Promise<UserEntity[]> {
|
||||
const items = await this.repo.find({
|
||||
where: [
|
||||
{ phone: Like(`%${keyword}%`) },
|
||||
{ nickname: Like(`%${keyword}%`) },
|
||||
],
|
||||
take: limit,
|
||||
order: { lastActiveAt: 'DESC' },
|
||||
});
|
||||
const items = await this.createTenantQueryBuilder('user')
|
||||
.andWhere('(user.phone LIKE :keyword OR user.nickname LIKE :keyword)', {
|
||||
keyword: `%${keyword}%`,
|
||||
})
|
||||
.orderBy('user.lastActiveAt', 'DESC')
|
||||
.take(limit)
|
||||
.getMany();
|
||||
|
||||
return items.map((orm) => this.toEntity(orm));
|
||||
}
|
||||
|
|
@ -111,6 +113,7 @@ export class UserPostgresRepository implements IUserRepository {
|
|||
private toORM(entity: UserEntity): UserORM {
|
||||
const orm = new UserORM();
|
||||
orm.id = entity.id;
|
||||
orm.tenantId = this.getTenantId();
|
||||
orm.type = entity.type;
|
||||
orm.fingerprint = entity.fingerprint;
|
||||
orm.phone = entity.phone;
|
||||
|
|
|
|||
|
|
@ -1,53 +1,57 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, MoreThan } from 'typeorm';
|
||||
import { BaseTenantRepository, TenantContextService } from '@iconsulting/shared';
|
||||
import { IVerificationCodeRepository } from '../../../domain/repositories/verification-code.repository.interface';
|
||||
import { VerificationCodeEntity } from '../../../domain/entities/verification-code.entity';
|
||||
import { VerificationCodeORM } from '../../../infrastructure/database/postgres/entities/verification-code.orm';
|
||||
|
||||
@Injectable()
|
||||
export class VerificationCodePostgresRepository implements IVerificationCodeRepository {
|
||||
export class VerificationCodePostgresRepository
|
||||
extends BaseTenantRepository<VerificationCodeEntity, VerificationCodeORM>
|
||||
implements IVerificationCodeRepository
|
||||
{
|
||||
constructor(
|
||||
@InjectRepository(VerificationCodeORM)
|
||||
private readonly repo: Repository<VerificationCodeORM>,
|
||||
) {}
|
||||
@InjectRepository(VerificationCodeORM) repo: Repository<VerificationCodeORM>,
|
||||
tenantContext: TenantContextService,
|
||||
) {
|
||||
super(repo, tenantContext);
|
||||
}
|
||||
|
||||
async save(code: VerificationCodeEntity): Promise<VerificationCodeEntity> {
|
||||
const orm = this.toORM(code);
|
||||
orm.tenantId = this.getTenantId();
|
||||
const saved = await this.repo.save(orm);
|
||||
return this.toEntity(saved);
|
||||
}
|
||||
|
||||
async findValidCode(phone: string, code: string): Promise<VerificationCodeEntity | null> {
|
||||
const orm = await this.repo.findOne({
|
||||
where: {
|
||||
phone,
|
||||
code,
|
||||
isUsed: false,
|
||||
expiresAt: MoreThan(new Date()),
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
const orm = await this.createTenantQueryBuilder('vc')
|
||||
.andWhere('vc.phone = :phone', { phone })
|
||||
.andWhere('vc.code = :code', { code })
|
||||
.andWhere('vc.is_used = :isUsed', { isUsed: false })
|
||||
.andWhere('vc.expires_at > :now', { now: new Date() })
|
||||
.orderBy('vc.created_at', 'DESC')
|
||||
.getOne();
|
||||
return orm ? this.toEntity(orm) : null;
|
||||
}
|
||||
|
||||
async countRecentByPhone(phone: string, hoursBack: number): Promise<number> {
|
||||
const since = new Date(Date.now() - hoursBack * 60 * 60 * 1000);
|
||||
return this.repo.count({
|
||||
where: {
|
||||
phone,
|
||||
createdAt: MoreThan(since),
|
||||
},
|
||||
});
|
||||
return this.createTenantQueryBuilder('vc')
|
||||
.andWhere('vc.phone = :phone', { phone })
|
||||
.andWhere('vc.created_at > :since', { since })
|
||||
.getCount();
|
||||
}
|
||||
|
||||
async markAsUsed(id: string): Promise<void> {
|
||||
await this.repo.update(id, { isUsed: true });
|
||||
await this.updateWithTenant(id, { isUsed: true } as any);
|
||||
}
|
||||
|
||||
private toORM(entity: VerificationCodeEntity): VerificationCodeORM {
|
||||
const orm = new VerificationCodeORM();
|
||||
orm.id = entity.id;
|
||||
orm.tenantId = this.getTenantId();
|
||||
orm.phone = entity.phone;
|
||||
orm.code = entity.code;
|
||||
orm.expiresAt = entity.expiresAt;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import {
|
||||
TenantContextService,
|
||||
TenantContextMiddleware,
|
||||
SimpleTenantFinder,
|
||||
DEFAULT_TENANT_ID,
|
||||
} from '@iconsulting/shared';
|
||||
import { UserModule } from './user/user.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
|
|
@ -46,5 +52,31 @@ import { HealthModule } from './health/health.module';
|
|||
UserModule,
|
||||
AuthModule,
|
||||
],
|
||||
providers: [TenantContextService, SimpleTenantFinder],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule implements NestModule {
|
||||
constructor(
|
||||
private readonly tenantContext: TenantContextService,
|
||||
private readonly tenantFinder: SimpleTenantFinder,
|
||||
) {}
|
||||
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
const tenantMiddleware = new TenantContextMiddleware(
|
||||
this.tenantContext,
|
||||
this.tenantFinder,
|
||||
{
|
||||
allowDefaultTenant: true,
|
||||
defaultTenantId: DEFAULT_TENANT_ID,
|
||||
excludePaths: ['/health', '/health/*'],
|
||||
},
|
||||
);
|
||||
|
||||
consumer
|
||||
.apply((req: any, res: any, next: any) => tenantMiddleware.use(req, res, next))
|
||||
.exclude(
|
||||
{ path: 'health', method: RequestMethod.GET },
|
||||
{ path: 'health/(.*)', method: RequestMethod.ALL },
|
||||
)
|
||||
.forRoutes('*');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ export { TenantContextService } from './tenant-context.service.js';
|
|||
export { TenantContextMiddleware, TENANT_FINDER, createTenantMiddleware } from './tenant-context.middleware.js';
|
||||
export type { ITenantFinder, TenantMiddlewareOptions } from './tenant-context.middleware.js';
|
||||
|
||||
// Tenant Finder (Simple implementation)
|
||||
export { SimpleTenantFinder } from './simple-tenant-finder.js';
|
||||
|
||||
// Guards
|
||||
export { TenantGuard } from './tenant.guard.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ITenantFinder } from './tenant-context.middleware.js';
|
||||
import { TenantContext, TenantStatus, TenantPlan, DEFAULT_TENANT_ID } from './tenant.types.js';
|
||||
|
||||
/**
|
||||
* 简单租户查找器
|
||||
* 用于内部服务(由网关验证租户后调用)
|
||||
* 生产环境应使用 HTTP 客户端调用 evolution-service 验证
|
||||
*/
|
||||
@Injectable()
|
||||
export class SimpleTenantFinder implements ITenantFinder {
|
||||
// 租户缓存 (简单实现,生产环境应使用 Redis)
|
||||
private cache = new Map<string, { tenant: TenantContext; expiresAt: number }>();
|
||||
private readonly cacheTtlMs = 5 * 60 * 1000; // 5 分钟缓存
|
||||
|
||||
async findById(id: string): Promise<TenantContext | null> {
|
||||
// 检查缓存
|
||||
const cached = this.cache.get(id);
|
||||
if (cached && cached.expiresAt > Date.now()) {
|
||||
return cached.tenant;
|
||||
}
|
||||
|
||||
// 默认租户 (向后兼容)
|
||||
if (id === DEFAULT_TENANT_ID) {
|
||||
const tenant: TenantContext = {
|
||||
id: DEFAULT_TENANT_ID,
|
||||
slug: 'default',
|
||||
name: 'Default Tenant',
|
||||
status: TenantStatus.ACTIVE,
|
||||
plan: TenantPlan.ENTERPRISE,
|
||||
config: {},
|
||||
};
|
||||
this.cache.set(id, { tenant, expiresAt: Date.now() + this.cacheTtlMs });
|
||||
return tenant;
|
||||
}
|
||||
|
||||
// 对于其他租户,信任传入的 ID(由网关验证)
|
||||
// 生产环境应调用 evolution-service 验证
|
||||
const tenant: TenantContext = {
|
||||
id,
|
||||
slug: id.substring(0, 8),
|
||||
name: `Tenant ${id.substring(0, 8)}`,
|
||||
status: TenantStatus.ACTIVE,
|
||||
plan: TenantPlan.STANDARD,
|
||||
config: {},
|
||||
};
|
||||
this.cache.set(id, { tenant, expiresAt: Date.now() + this.cacheTtlMs });
|
||||
return tenant;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存中移除指定租户
|
||||
*/
|
||||
invalidate(tenantId: string): void {
|
||||
this.cache.delete(tenantId);
|
||||
}
|
||||
}
|
||||
|
|
@ -205,6 +205,9 @@ importers:
|
|||
'@anthropic-ai/sdk':
|
||||
specifier: ^0.52.0
|
||||
version: 0.52.0
|
||||
'@iconsulting/shared':
|
||||
specifier: workspace:*
|
||||
version: link:../../shared
|
||||
'@nestjs/common':
|
||||
specifier: ^10.0.0
|
||||
version: 10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
|
|
@ -381,6 +384,9 @@ importers:
|
|||
'@anthropic-ai/sdk':
|
||||
specifier: ^0.52.0
|
||||
version: 0.52.0
|
||||
'@iconsulting/shared':
|
||||
specifier: workspace:*
|
||||
version: link:../../shared
|
||||
'@nestjs/common':
|
||||
specifier: ^10.0.0
|
||||
version: 10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
|
|
|
|||
Loading…
Reference in New Issue