fix(tenant): use TenantContextModule.forRoot() for global tenant context

All services were providing TenantContextService directly without
making it global, causing DI resolution failures in child modules.
Now using TenantContextModule.forRoot() which exports TenantContextService
globally so all repositories can access it.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-26 04:31:56 -08:00
parent 89d9645c02
commit 7ff701369b
8 changed files with 112 additions and 92 deletions

13
.builder.Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM node:20-alpine
# 安装 pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
# 设置工作目录
WORKDIR /app
# 设置 pnpm store 目录
ENV PNPM_HOME=/root/.local/share/pnpm
ENV PATH=$PNPM_HOME:$PATH
CMD ["sh"]

View File

@ -28,7 +28,21 @@
"Bash(cmd /c \"dir %USERPROFILE%\\\\.ssh\")",
"Bash(git fetch:*)",
"Bash(TEST_USER_ID=\"a1b2c3d4-e5f6-7890-abcd-ef1234567890\":*)",
"Bash(git reset:*)"
"Bash(git reset:*)",
"Bash(ls \"c:\\\\Users\\\\dong\\\\Desktop\\\\iConsulting\\\\packages\\\\services\\\\evolution-service\\\\src\\\\infrastructure\\\\database\\\\entities\"\" 2>/dev/null || echo \"Directory empty or not exists \")",
"Bash(done)",
"Bash(for service in knowledge-service conversation-service user-service payment-service evolution-service file-service)",
"Bash(do echo \"=== $service ===\" find \"/c/Users/dong/Desktop/iConsulting/packages/services/$service/src\" -type d)",
"Bash(pnpm build)",
"Bash(find:*)",
"Bash(grep:*)",
"Bash(timeout:*)",
"Bash(git restore:*)",
"Bash(pnpm build:*)",
"Bash(pnpm --filter @iconsulting/shared build:*)",
"Bash(pnpm --filter @iconsulting/user-service build:*)",
"Bash(pnpm --filter @iconsulting/conversation-service build:*)",
"Bash(pnpm --filter @iconsulting/payment-service build:*)"
]
}
}

View File

@ -2,7 +2,7 @@ import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/c
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
TenantContextService,
TenantContextModule,
TenantContextMiddleware,
SimpleTenantFinder,
DEFAULT_TENANT_ID,
@ -19,6 +19,17 @@ import { HealthModule } from './health/health.module';
envFilePath: ['.env.local', '.env'],
}),
// Tenant context module (global)
TenantContextModule.forRoot({
tenantFinder: SimpleTenantFinder,
middlewareOptions: {
allowDefaultTenant: true,
defaultTenantId: DEFAULT_TENANT_ID,
excludePaths: ['/health', '/health/*'],
},
isGlobal: true,
}),
// Database
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
@ -53,27 +64,15 @@ import { HealthModule } from './health/health.module';
ConversationModule,
ClaudeModule,
],
providers: [TenantContextService, SimpleTenantFinder],
})
export class AppModule implements NestModule {
constructor(
private readonly tenantContext: TenantContextService,
private readonly tenantFinder: SimpleTenantFinder,
private readonly tenantMiddleware: TenantContextMiddleware,
) {}
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))
.apply((req: any, res: any, next: any) => this.tenantMiddleware.use(req, res, next))
.exclude(
{ path: 'health', method: RequestMethod.GET },
{ path: 'health/(.*)', method: RequestMethod.ALL },

View File

@ -3,7 +3,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ScheduleModule } from '@nestjs/schedule';
import {
TenantContextService,
TenantContextModule,
TenantContextMiddleware,
DEFAULT_TENANT_ID,
} from '@iconsulting/shared';
@ -25,6 +25,17 @@ import { TenantFinderService } from './infrastructure/tenant/tenant-finder.servi
// 定时任务模块
ScheduleModule.forRoot(),
// Tenant context module (global) - uses custom TenantFinderService
TenantContextModule.forRoot({
tenantFinder: TenantFinderService,
middlewareOptions: {
allowDefaultTenant: true,
defaultTenantId: DEFAULT_TENANT_ID,
excludePaths: ['/health', '/health/*', '/super-admin/*'],
},
isGlobal: true,
}),
// 数据库连接
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
@ -54,28 +65,15 @@ import { TenantFinderService } from './infrastructure/tenant/tenant-finder.servi
AdminModule,
AnalyticsModule,
],
providers: [TenantContextService],
})
export class AppModule implements NestModule {
constructor(
private readonly tenantContext: TenantContextService,
private readonly tenantFinder: TenantFinderService,
private readonly tenantMiddleware: TenantContextMiddleware,
) {}
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))
.apply((req: any, res: any, next: any) => this.tenantMiddleware.use(req, res, next))
.exclude(
{ path: 'health', method: RequestMethod.GET },
{ path: 'health/(.*)', method: RequestMethod.ALL },

View File

@ -2,7 +2,7 @@ import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/c
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
TenantContextService,
TenantContextModule,
TenantContextMiddleware,
SimpleTenantFinder,
DEFAULT_TENANT_ID,
@ -18,6 +18,17 @@ import { FileModule } from './file/file.module';
envFilePath: ['.env.local', '.env'],
}),
// Tenant context module (global)
TenantContextModule.forRoot({
tenantFinder: SimpleTenantFinder,
middlewareOptions: {
allowDefaultTenant: true,
defaultTenantId: DEFAULT_TENANT_ID,
excludePaths: ['/health', '/health/*'],
},
isGlobal: true,
}),
// 数据库连接
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
@ -41,27 +52,15 @@ import { FileModule } from './file/file.module';
// 功能模块
FileModule,
],
providers: [TenantContextService, SimpleTenantFinder],
})
export class AppModule implements NestModule {
constructor(
private readonly tenantContext: TenantContextService,
private readonly tenantFinder: SimpleTenantFinder,
private readonly tenantMiddleware: TenantContextMiddleware,
) {}
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))
.apply((req: any, res: any, next: any) => this.tenantMiddleware.use(req, res, next))
.exclude(
{ path: 'health', method: RequestMethod.GET },
{ path: 'health/(.*)', method: RequestMethod.ALL },

View File

@ -2,7 +2,7 @@ import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/c
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
TenantContextService,
TenantContextModule,
TenantContextMiddleware,
SimpleTenantFinder,
DEFAULT_TENANT_ID,
@ -19,6 +19,17 @@ import { HealthModule } from './health/health.module';
envFilePath: ['.env.local', '.env'],
}),
// Tenant context module (global)
TenantContextModule.forRoot({
tenantFinder: SimpleTenantFinder,
middlewareOptions: {
allowDefaultTenant: true,
defaultTenantId: DEFAULT_TENANT_ID,
excludePaths: ['/health', '/health/*'],
},
isGlobal: true,
}),
// 数据库连接
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
@ -44,27 +55,15 @@ import { HealthModule } from './health/health.module';
KnowledgeModule,
MemoryModule,
],
providers: [TenantContextService, SimpleTenantFinder],
})
export class AppModule implements NestModule {
constructor(
private readonly tenantContext: TenantContextService,
private readonly tenantFinder: SimpleTenantFinder,
private readonly tenantMiddleware: TenantContextMiddleware,
) {}
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))
.apply((req: any, res: any, next: any) => this.tenantMiddleware.use(req, res, next))
.exclude(
{ path: 'health', method: RequestMethod.GET },
{ path: 'health/(.*)', method: RequestMethod.ALL },

View File

@ -2,7 +2,7 @@ import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/c
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
TenantContextService,
TenantContextModule,
TenantContextMiddleware,
SimpleTenantFinder,
DEFAULT_TENANT_ID,
@ -18,6 +18,17 @@ import { HealthModule } from './health/health.module';
envFilePath: ['.env.local', '.env'],
}),
// Tenant context module (global)
TenantContextModule.forRoot({
tenantFinder: SimpleTenantFinder,
middlewareOptions: {
allowDefaultTenant: true,
defaultTenantId: DEFAULT_TENANT_ID,
excludePaths: ['/health', '/health/*'],
},
isGlobal: true,
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
@ -56,27 +67,15 @@ import { HealthModule } from './health/health.module';
OrderModule,
PaymentModule,
],
providers: [TenantContextService, SimpleTenantFinder],
})
export class AppModule implements NestModule {
constructor(
private readonly tenantContext: TenantContextService,
private readonly tenantFinder: SimpleTenantFinder,
private readonly tenantMiddleware: TenantContextMiddleware,
) {}
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))
.apply((req: any, res: any, next: any) => this.tenantMiddleware.use(req, res, next))
.exclude(
{ path: 'health', method: RequestMethod.GET },
{ path: 'health/(.*)', method: RequestMethod.ALL },

View File

@ -3,7 +3,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JwtModule } from '@nestjs/jwt';
import {
TenantContextService,
TenantContextModule,
TenantContextMiddleware,
SimpleTenantFinder,
DEFAULT_TENANT_ID,
@ -19,6 +19,17 @@ import { HealthModule } from './health/health.module';
envFilePath: ['.env.local', '.env'],
}),
// Tenant context module (global)
TenantContextModule.forRoot({
tenantFinder: SimpleTenantFinder,
middlewareOptions: {
allowDefaultTenant: true,
defaultTenantId: DEFAULT_TENANT_ID,
excludePaths: ['/health', '/health/*'],
},
isGlobal: true,
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
@ -52,27 +63,15 @@ import { HealthModule } from './health/health.module';
UserModule,
AuthModule,
],
providers: [TenantContextService, SimpleTenantFinder],
})
export class AppModule implements NestModule {
constructor(
private readonly tenantContext: TenantContextService,
private readonly tenantFinder: SimpleTenantFinder,
private readonly tenantMiddleware: TenantContextMiddleware,
) {}
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))
.apply((req: any, res: any, next: any) => this.tenantMiddleware.use(req, res, next))
.exclude(
{ path: 'health', method: RequestMethod.GET },
{ path: 'health/(.*)', method: RequestMethod.ALL },