fix(iam): 修复 TS 编译错误、ESM 依赖注入与身份头读取

P1 端到端验证中发现 IAM 服务存在 14 个 TS 编译错误与运行时 DI 失败:

- 移除 typeorm/ioredis/kafkajs 依赖(IAM 用 Drizzle)
- health.controller.ts 改用 db.execute(sql SELECT 1) 校验连接
- lifecycle.service.ts 简化为只关闭 Drizzle 连接池
- Drizzle API 修正:r.roles -> r.iam_roles,.in() -> inArray()
- ESM 模式下 DI 必须显式 @Inject(IamRepository)(参考 classes 黄金模板)
- iam.controller.ts 直接读 req.headers[x-user-id],不依赖未注册的 AuthMiddleware
- health.module.ts 补 .js 后缀
- package.json 补 @types/express

验证:register -> JWT -> Gateway /iam/me 200 -> /classes CRUD 200
This commit is contained in:
SpecialX
2026-07-09 00:25:37 +08:00
parent 68ddff1065
commit f658571726
10 changed files with 142 additions and 110 deletions

View File

@@ -1,7 +1,8 @@
import { Controller, Get, HttpException, HttpStatus } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { Controller, Get, HttpException, HttpStatus } from "@nestjs/common";
import { sql } from "drizzle-orm";
import { getDb } from "../../config/database.js";
const SERVICE_NAME = 'iam';
const SERVICE_NAME = "iam";
/**
* 健康检查端点。
@@ -14,33 +15,37 @@ const SERVICE_NAME = 'iam';
*/
@Controller()
export class HealthController {
constructor(private readonly dataSource: DataSource) {}
@Get('healthz')
@Get("healthz")
liveness(): { status: string; service: string; timestamp: string } {
return {
status: 'ok',
status: "ok",
service: SERVICE_NAME,
timestamp: new Date().toISOString(),
};
}
@Get('readyz')
async readiness(): Promise<{ status: string; service: string; timestamp: string }> {
@Get("readyz")
async readiness(): Promise<{
status: string;
service: string;
timestamp: string;
}> {
try {
await this.dataSource.query('SELECT 1');
const db = getDb();
await db.execute(sql`SELECT 1`);
return {
status: 'ok',
status: "ok",
service: SERVICE_NAME,
timestamp: new Date().toISOString(),
};
} catch (error) {
throw new HttpException(
{
status: 'error',
status: "error",
service: SERVICE_NAME,
timestamp: new Date().toISOString(),
error: error instanceof Error ? error.message : 'database unreachable',
error:
error instanceof Error ? error.message : "database unreachable",
},
HttpStatus.SERVICE_UNAVAILABLE,
);

View File

@@ -1,5 +1,5 @@
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
import { Module } from "@nestjs/common";
import { HealthController } from "./health.controller.js";
/**
* 健康检查模块。

View File

@@ -1,9 +1,12 @@
import { Inject, Injectable, Logger, OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
import { DataSource } from 'typeorm';
import type { Redis } from 'ioredis';
import type { Producer } from 'kafkajs';
import {
Injectable,
Logger,
OnApplicationShutdown,
OnModuleInit,
} from "@nestjs/common";
import { closeDb } from "../../config/database.js";
const SERVICE_NAME = 'iam';
const SERVICE_NAME = "iam";
/**
* 优雅停机服务。
@@ -12,52 +15,31 @@ const SERVICE_NAME = 'iam';
* 触发SIGTERM / SIGINTNestJS 会依次调用 OnApplicationShutdown 钩子。
* K8s 配置 `terminationGracePeriodSeconds=60` 给予足够时间清理。
*
* 关闭顺序Kafka producer → Redis → DataSource
* 先停外部消息生产避免新事件,再关缓存,最后关 DB
*
* 集成说明(不修改 app.module.ts仅在 README 注释说明):
* - 在 `app.module.ts` 的 providers 中加入 `LifecycleService`。
* - 在 `main.ts` 中 `app.listen` 之前调用 `app.enableShutdownHooks()`。
*
* 依赖注入 token 约定(需与各服务 provider 注册一致):
* - DataSource由 `TypeOrmModule.forRoot()` 提供。
* - 'REDIS_CLIENT':需在对应模块注册 `{ provide: 'REDIS_CLIENT', useFactory: ... }`。
* - 'KAFKA_PRODUCER':需在对应模块注册 `{ provide: 'KAFKA_PRODUCER', useFactory: ... }`。
* IAM 服务仅使用 Drizzle ORMMySQL无 Kafka / Redis 依赖
* 关闭时仅需关闭数据库连接池
*/
@Injectable()
export class LifecycleService implements OnModuleInit, OnApplicationShutdown {
private readonly logger = new Logger(LifecycleService.name);
constructor(
private readonly dataSource: DataSource,
@Inject('REDIS_CLIENT') private readonly redis: Redis,
@Inject('KAFKA_PRODUCER') private readonly kafkaProducer: Producer,
) {}
onModuleInit(): void {
this.logger.log(`service ${SERVICE_NAME} module initialized`);
}
async onApplicationShutdown(signal?: string): Promise<void> {
this.logger.log(
`service ${SERVICE_NAME} shutting down (signal=${signal ?? 'unknown'})`,
`service ${SERVICE_NAME} shutting down (signal=${signal ?? "unknown"})`,
);
await this.safeDisconnect('kafka producer', () => this.kafkaProducer.disconnect());
await this.safeDisconnect('redis', () => this.redis.quit());
await this.safeDisconnect('datasource', () => this.dataSource.destroy());
try {
await closeDb();
this.logger.log("database connection closed");
} catch (error) {
this.logger.error(
`database close failed: ${error instanceof Error ? error.message : String(error)}`,
);
}
this.logger.log(`service ${SERVICE_NAME} shutdown complete`);
}
private async safeDisconnect(name: string, fn: () => Promise<unknown>): Promise<void> {
try {
await fn();
this.logger.log(`${name} closed`);
} catch (error) {
this.logger.error(
`${name} close failed: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}