fix: code compliance audit and fix across all services
Some checks failed
CI / quality-ts (push) Failing after 48s
CI / quality-go (push) Failing after 4s
CI / quality-proto (push) Failing after 2s
CI / deploy (push) Has been skipped

NestJS (6 services): implement @RequirePermission decorator with
SetMetadata+Reflector, register APP_GUARD globally, fix as assertions
to type guards, add explicit return types, fix import type for express,
fix /metrics implicit any, replace native Error with ApplicationError,
remove typeorm remnants, register LifecycleService.

teacher-bff: add logger, ApplicationError, GlobalErrorFilter, forward
real userId to downstream, log downstream failures, migrate health
controller to shared/health.

Go (2 services): interface to any, doc comments, CORS dev whitelist,
JWT secret fail-fast, push-gateway internal API auth, metrics and
readyz endpoints, remove dead code.

Python (2 services): lifespan return type, dev_mode to bool, data-ana
APIRouter, ai POST body model, ClickHouse async wrapping.
This commit is contained in:
SpecialX
2026-07-09 17:28:27 +08:00
parent b53a486c6e
commit 0a71b02e04
93 changed files with 5775 additions and 608 deletions

View File

@@ -0,0 +1,118 @@
export type ErrorType =
| "validation"
| "not_found"
| "permission_denied"
| "unauthorized"
| "conflict"
| "business"
| "database"
| "bad_gateway"
| "internal";
export interface ErrorDetails {
[key: string]: unknown;
}
export abstract class ApplicationError extends Error {
abstract readonly type: ErrorType;
abstract readonly statusCode: number;
readonly code: string;
readonly details?: ErrorDetails;
traceId?: string;
constructor(message: string, code: string, details?: ErrorDetails) {
super(message);
this.name = this.constructor.name;
this.code = code;
this.details = details;
}
toJSON(): Record<string, unknown> {
return {
success: false,
error: {
code: this.code,
message: this.message,
details: this.details,
traceId: this.traceId,
},
};
}
}
export class ValidationError extends ApplicationError {
readonly type = "validation" as const;
readonly statusCode = 400;
constructor(message: string, details?: ErrorDetails) {
super(message, "TEACHER_BFF_VALIDATION_ERROR", details);
}
}
export class NotFoundError extends ApplicationError {
readonly type = "not_found" as const;
readonly statusCode = 404;
constructor(resource: string, id: string) {
super(`${resource} not found: ${id}`, "TEACHER_BFF_NOT_FOUND", {
resource,
id,
});
}
}
export class PermissionDeniedError extends ApplicationError {
readonly type = "permission_denied" as const;
readonly statusCode = 403;
constructor(permission: string) {
super(`Permission denied: ${permission}`, "TEACHER_BFF_PERMISSION_DENIED", {
permission,
});
}
}
export class UnauthorizedError extends ApplicationError {
readonly type = "unauthorized" as const;
readonly statusCode = 401;
constructor(message: string, details?: ErrorDetails) {
super(message, "TEACHER_BFF_UNAUTHORIZED", details);
}
}
export class ConflictError extends ApplicationError {
readonly type = "conflict" as const;
readonly statusCode = 409;
constructor(message: string, details?: ErrorDetails) {
super(message, "TEACHER_BFF_CONFLICT", details);
}
}
export class BusinessError extends ApplicationError {
readonly type = "business" as const;
readonly statusCode = 422;
constructor(message: string, details?: ErrorDetails) {
super(message, "TEACHER_BFF_BUSINESS_ERROR", details);
}
}
export class DatabaseError extends ApplicationError {
readonly type = "database" as const;
readonly statusCode = 500;
constructor(message: string, details?: ErrorDetails) {
super(message, "TEACHER_BFF_DATABASE_ERROR", details);
}
}
export class BadGatewayError extends ApplicationError {
readonly type = "bad_gateway" as const;
readonly statusCode = 502;
constructor(message: string, details?: ErrorDetails) {
super(message, "TEACHER_BFF_BAD_GATEWAY", details);
}
}
export class InternalError extends ApplicationError {
readonly type = "internal" as const;
readonly statusCode = 500;
constructor(message: string, details?: ErrorDetails) {
super(message, "TEACHER_BFF_INTERNAL_ERROR", details);
}
}

View File

@@ -0,0 +1,87 @@
import {
Catch,
ExceptionFilter,
ArgumentsHost,
HttpException,
Logger,
} from "@nestjs/common";
import type { Request, Response } from "express";
import { ZodError } from "zod";
import { ApplicationError } from "./application-error.js";
@Catch()
export class GlobalErrorFilter implements ExceptionFilter {
private readonly logger = new Logger(GlobalErrorFilter.name);
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const traceIdHeader = request.headers["x-request-id"];
const traceId =
typeof traceIdHeader === "string" ? traceIdHeader : "unknown";
let statusCode = 500;
let body: Record<string, unknown>;
if (exception instanceof ApplicationError) {
exception.traceId = traceId;
statusCode = exception.statusCode;
body = exception.toJSON();
} else if (exception instanceof ZodError) {
statusCode = 400;
body = {
success: false,
error: {
code: "TEACHER_BFF_VALIDATION_ERROR",
message: "Validation failed",
details: exception.flatten(),
traceId,
},
};
} else if (exception instanceof HttpException) {
statusCode = exception.getStatus();
const res = exception.getResponse();
const message = this.extractHttpMessage(res, exception);
body = {
success: false,
error: {
code: "HTTP_ERROR",
message,
traceId,
},
};
} else {
this.logger.error(
`Unhandled exception: ${exception}`,
exception instanceof Error ? exception.stack : undefined,
);
body = {
success: false,
error: {
code: "INTERNAL_ERROR",
message: "An unexpected error occurred",
traceId,
},
};
}
response.status(statusCode).json(body);
}
private extractHttpMessage(
res: string | object,
exception: HttpException,
): string {
if (typeof res === "string") {
return res;
}
if (res && typeof res === "object" && "message" in res) {
// 从 HttpException 响应体收窄类型NestJS 约定包含 message 字段)
const msg = (res as { message: unknown }).message;
return typeof msg === "string" ? msg : exception.message;
}
return exception.message;
}
}

View File

@@ -0,0 +1,32 @@
import { Controller, Get } from "@nestjs/common";
const SERVICE_NAME = "teacher-bff";
/**
* 健康检查端点。
*
* - GET /healthzliveness仅返回进程存活。
* - GET /readyzreadinessBFF 不直接访问 DB可直接返回 ok。
*
* 不需要鉴权,必须在路由白名单中放行。
*/
@Controller()
export class HealthController {
@Get("healthz")
liveness(): { status: string; service: string; timestamp: string } {
return {
status: "ok",
service: SERVICE_NAME,
timestamp: new Date().toISOString(),
};
}
@Get("readyz")
readiness(): { status: string; service: string; timestamp: string } {
return {
status: "ok",
service: SERVICE_NAME,
timestamp: new Date().toISOString(),
};
}
}

View File

@@ -0,0 +1,7 @@
import { Module } from "@nestjs/common";
import { HealthController } from "./health.controller.js";
@Module({
controllers: [HealthController],
})
export class HealthModule {}

View File

@@ -0,0 +1,19 @@
import pino from "pino";
import { env } from "../../config/env.js";
export const logger = pino({
level: env.LOG_LEVEL,
base: {
service: "teacher-bff",
version: "0.1.0",
},
transport:
env.NODE_ENV === "development"
? {
target: "pino-pretty",
options: { colorize: true },
}
: undefined,
});
export type Logger = typeof logger;