Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,8 @@
"files.trimTrailingWhitespace": true, // 保存时移除行尾空白
"files.insertFinalNewline": true, // 文件末尾自动插入换行符
"files.encoding": "utf8", // 文件编码:UTF-8
"files.eol": "\n" // 行结束符:Unix 风格(\n)
"files.eol": "\n", // 行结束符:Unix 风格(\n)

// ===== NPM 配置 =====
"npm.exclude": "**/dist" // 排除 dist 目录下的 package.json
}
17 changes: 9 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ RUN pnpm prune --prod --ignore-scripts
FROM node:22-slim

# 安装依赖和 OpenSSL (运行时 Prisma Client 可能需要)
RUN apt-get update -y && apt-get install -y openssl curl && rm -rf /var/lib/apt/lists/* && npm install -g pnpm
RUN apt-get update -y && apt-get install -y openssl curl && rm -rf /var/lib/apt/lists/*

# 设置工作目录
WORKDIR /app
Expand All @@ -59,23 +59,24 @@ COPY --from=builder /app/node_modules ./node_modules
# 从构建阶段复制构建输出
COPY --from=builder /app/dist ./dist

# 复制 package.json (用于识别项目信息)
COPY package.json ./

# 构建参数
ARG GIT_COMMIT=unknown
ARG APP_VERSION=0.0.0
ARG APP_VERSION
ARG APP_NAME
ARG NODE_ENV=production
ARG GIT_COMMIT=unknown
ARG PORT=3000

# 环境变量
ENV APP_VERSION=$APP_VERSION
ENV APP_NAME=$APP_NAME
ENV GIT_COMMIT=$GIT_COMMIT
ENV NODE_ENV=$NODE_ENV
ENV npm_package_version=$APP_VERSION
ENV PORT=$PORT

EXPOSE ${PORT}

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD curl --fail http://localhost:${PORT}/health
# 启动应用
CMD ["node", "dist/src/main.js"]
CMD ["node", "dist/src/main"]
9 changes: 1 addition & 8 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,11 @@ export default [
...tsPlugin.configs.recommended.rules,

// TypeScript 特定规则
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'@typescript-eslint/no-explicit-any': 'off', // 允许显式 any
'@typescript-eslint/explicit-module-boundary-types': 'off', // 灵活

// 风格规则
// 'no-console': 'warn', // 生产环境应清理 console
'no-console': 'warn', // 生产环境应清理 console
'no-debugger': 'error', // debugger 不能提交
'prefer-const': 'error', // 优先 const
'no-var': 'error', // 禁用 var
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "nestjs-demo-basic",
"private": true,
"version": "0.5.2",
"version": "0.5.3",
"description": "一个使用 NestJS 构建的示例项目",
"license": "MIT",
"type": "module",
Expand All @@ -28,7 +28,6 @@
"pnpm": ">=8.0.0"
},
"dependencies": {
"@nestjs/cli": "^11.0.16",
"@nestjs/common": "^11.1.13",
"@nestjs/config": "^4.0.3",
"@nestjs/core": "^11.1.13",
Expand All @@ -46,15 +45,14 @@
"pg": "^8.18.0",
"pino": "^10.3.1",
"pino-http": "^11.0.0",
"prisma": "^7.3.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"tsc-alias": "^1.8.16",
"ulid": "^3.0.2",
"zod": "^4.3.6"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@nestjs/cli": "^11.0.16",
"@nestjs/testing": "^11.1.13",
"@types/compression": "^1.8.1",
"@types/express": "^5.0.6",
Expand All @@ -73,8 +71,10 @@
"lint-staged": "^16.2.7",
"pino-pretty": "^13.1.3",
"prettier": "^3.8.1",
"prisma": "^7.3.0",
"supertest": "^7.2.2",
"ts-jest": "^29.4.6",
"tsc-alias": "^1.8.16",
"typescript": "^5.9.3"
},
"lint-staged": {
Expand Down
6 changes: 3 additions & 3 deletions src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service.js';
import { AppService } from '@/app.service.js';
import { Body, Post, HttpStatus, HttpException } from '@nestjs/common';
import { BusinessException } from './common/exceptions/business.exception.js';
import { LoginDto } from './app.dto.js';
import { BusinessException } from '@/common/exceptions/business.exception.js';
import { LoginDto } from '@/app.dto.js';
import { Logger } from '@/common/logger.service.js';
import { PinoLogger } from 'nestjs-pino';
import { DatabaseService } from '@/common/database.service.js';
Expand Down
54 changes: 9 additions & 45 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController, TestController } from './app.controller.js';
import { AppService } from './app.service.js';
import { DatabaseService } from './common/database.service.js';
import { AppController, TestController } from '@/app.controller.js';
import { AppService } from '@/app.service.js';
import { DatabaseService } from '@/common/database.service.js';
import { APP_PIPE, APP_INTERCEPTOR, APP_FILTER, APP_GUARD } from '@nestjs/core';
import { ZodValidationPipe, ZodSerializerInterceptor } from 'nestjs-zod';
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter.js';
import { AllExceptionsFilter } from '@/common/filters/all-exceptions.filter.js';
import {
PerformanceInterceptor,
RequestContextInterceptor,
ResponseFormatInterceptor,
TimeoutInterceptor,
} from './common/interceptors/index.js';
} from '@/common/interceptors/index.js';
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { LoggerModule } from 'nestjs-pino';
import pino from 'pino';
import { IS_DEV, IS_PROD } from './utils/constants.js';
import { IS_DEV, IS_PROD, APP_NAME } from '@/utils/constants.js';
import { Logger } from '@/common/logger.service.js';
import { RequestPreprocessingMiddleware } from './common/middleware/request-preprocessing.middleware.js';
import { RequestPreprocessingMiddleware } from '@/common/middleware/request-preprocessing.middleware.js';
import { z } from 'zod/v4';

@Module({
Expand Down Expand Up @@ -184,7 +184,7 @@ import { z } from 'zod/v4';
LoggerModule.forRoot({
pinoHttp: [
{
name: process.env.npm_package_name,
name: APP_NAME,
level: process.env.LOG_LEVEL || (!IS_PROD ? 'trace' : 'info'),
// prettier-ignore
transport:
Expand All @@ -194,41 +194,12 @@ import { z } from 'zod/v4';
sync: true,
colorize: true,
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss.l',
messageFormat: '{if req.method}[{req.method}]({req.url}){end} {if context}{context} - {end}{msg}',
// messageFormat: '{if req.method}[{req.method}]({req.url}){end} {if context}{context} - {end}{msg}',
},
} : undefined,
serializers: {
err: () => undefined, // 错误堆栈交由 exceptions.filter 处理,避免重复记录
// 请求序列化
// req: (req) => {
// return {
// id: req.id,
// method: req.method,
// url: req.url,
// query: req.query,
// params: req.params,
// // 只记录部分关键 headers
// headers: {
// 'user-agent': req.headers['user-agent'],
// 'content-type': req.headers['content-type'],
// authorization: req.headers['authorization'],
// },
// remoteAddress: req.remoteAddress,
// remotePort: req.remotePort,
// };
// },
req: () => undefined, // 请求信息交由 performance.interceptor 处理,避免重复记录
// 响应序列化
// res: (res) => {
// return {
// statusCode: res.statusCode,
// // 只记录关键响应头
// headers: {
// 'content-type': res.headers['content-type'],
// 'content-length': res.headers['content-length'],
// },
// };
// },
},
// prettier-ignore
// 全局隐藏敏感信息
Expand All @@ -245,13 +216,6 @@ import { z } from 'zod/v4';
mkdir: true,
}),
],
// 排除的日志记录路径和方法
// exclude: [
// { path: '/hello', method: RequestMethod.ALL },
// { path: '/health', method: RequestMethod.ALL },
// { path: '/logger/*', method: RequestMethod.ALL },
// { path: '/perf-test/*', method: RequestMethod.ALL },
// ],
}),
],
controllers: [AppController, TestController],
Expand Down
6 changes: 3 additions & 3 deletions src/app.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Injectable } from '@nestjs/common';
import { uptime } from 'node:process';
import { DatabaseService } from './common/database.service.js';
import { DatabaseService } from '@/common/database.service.js';
import { ConfigService } from '@nestjs/config';
import { Logger } from '@/common/logger.service.js';
import { APP_VERSION } from '@/utils/constants.js';

@Injectable()
export class AppService {
Expand All @@ -21,9 +22,8 @@ export class AppService {
const databaseHealth = await this.checkDatabaseHealth();
return {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: uptime(),
version: this.configService.get('npm_package_version', 'N/A'),
version: APP_VERSION,
gitCommit: this.configService.get('GIT_COMMIT', 'N/A'),
components: {
database: {
Expand Down
2 changes: 1 addition & 1 deletion src/common/filters/all-exceptions.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { PrismaClientKnownRequestError } from '@root/prisma/generated/internal/p
import { ZodValidationException, ZodSerializationException } from 'nestjs-zod';
import { ZodError } from 'zod/v4';
import { ThrottlerException } from '@nestjs/throttler';
import { Logger } from '../logger.service.js';
import { Logger } from '@/common/logger.service.js';

interface Request extends originRequest {
user?: any;
Expand Down
2 changes: 1 addition & 1 deletion src/common/interceptors/request-context.interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { RequestContextService } from '../request-context.service.js';
import { RequestContextService } from '@/common/request-context.service.js';
import { Request } from '@/common/middleware/request-preprocessing.middleware.js';

/**
Expand Down
3 changes: 3 additions & 0 deletions src/common/logger.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export class Logger extends NestLogger {
};
let formatData = ConsoleFormatter.format(level, payload, context);
if (IS_DEV) formatData = JSON.parse(formatData);
/* eslint-disable no-console */
switch (level) {
case 'verbose':
console.debug(formatData);
Expand All @@ -66,6 +67,7 @@ export class Logger extends NestLogger {
console.error(formatData);
break;
}
/* eslint-enable no-console */
const expectionStack = err.stack ?? 'No stack trace available';
const selfExpectionPayload = {
context: 'Logger',
Expand All @@ -82,6 +84,7 @@ export class Logger extends NestLogger {
`Internal error\n${expectionStack}`
);
if (IS_DEV) selfExceptionData = JSON.parse(selfExceptionData);
// eslint-disable-next-line no-console
console.error(selfExceptionData);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/common/middleware/request-preprocessing.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class RequestPreprocessingMiddleware implements NestMiddleware {
const reqId = req.headers['x-request-id'] ?? ulid();
req.id = typeof reqId === 'string' ? reqId : reqId[0];
res.setHeader('X-Request-Id', req.id);
req.version = APP_VERSION ?? 'unknown';
req.version = APP_VERSION;
next();
}
}
15 changes: 5 additions & 10 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js';
import { AppModule } from '@/app.module.js';

import figlet from 'figlet';
import { atlas } from 'gradient-string';

import compression from 'compression';
import express from 'express';

import { Logger as pinoLogger } from 'nestjs-pino';
import { Logger } from '@/common/logger.service.js';

import helmet from 'helmet';

import { APP_VERSION } from '@/utils/constants.js';

async function bootstrap() {
const app = await NestFactory.create(AppModule, { bufferLogs: true });
app.useLogger(app.get(pinoLogger));
Expand Down Expand Up @@ -48,9 +49,6 @@ async function bootstrap() {
maxAge: 86400,
});

app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

app.use(compression({ threshold: 1024 }));

const port = parseInt(process.env.PORT ?? '3000');
Expand Down Expand Up @@ -84,13 +82,10 @@ bootstrap()
font: 'Slant',
horizontalLayout: 'fitted',
});
process.stdout.write(
atlas.multiline(
startupBanner + `\nv${process.env.npm_package_version || '0.0.0'} | by FOV-RGT\n\n`
)
);
process.stdout.write(atlas.multiline(startupBanner + `\nv${APP_VERSION} | by FOV-RGT\n\n`));
})
.catch((err) => {
// eslint-disable-next-line no-console
console.error('Bootstrap failed:', err);
process.exit(1);
});
7 changes: 6 additions & 1 deletion src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import 'dotenv/config';
import _package_info from '@root/package.json' with { type: 'json' };

export const DEFAULT_PORT = Number(process.env.PORT);

export const APP_VERSION = process.env.npm_package_version ?? '0.0.0';
export const PACKAGE_INFO = _package_info;

export const APP_VERSION = process.env.APP_VERSION || PACKAGE_INFO.version || 'unknown';

export const APP_NAME = process.env.APP_NAME || PACKAGE_INFO.name || 'unknown';

export const IS_DEV = process.env.NODE_ENV === 'development';

Expand Down
3 changes: 2 additions & 1 deletion src/utils/helpers/console-formatter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os from 'os';
import { APP_NAME } from '@/utils/constants.js';

type Message = string | Error | Record<string, any>;

Expand All @@ -18,7 +19,7 @@ export class ConsoleFormatter {
time: Date.now(),
pid: process.pid,
hostname: os.hostname(),
name: process.env.npm_package_name,
name: APP_NAME,
...this.formatMsgAndCtx(message, context),
});
}
Expand Down
8 changes: 3 additions & 5 deletions test/unit/app.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AppController } from '@/app.controller';
import { AppService } from '@/app.service';
import { DatabaseService } from '@/common/database.service';
import { AppController } from '@/app.controller.js';
import { AppService } from '@/app.service.js';
import { DatabaseService } from '@/common/database.service.js';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { PinoLogger } from 'nestjs-pino';
Expand Down Expand Up @@ -52,7 +52,5 @@ describe('AppController (unit)', () => {
it('getHealth should return status ok and timestamp', async () => {
const res: any = await controller.getHealth();
expect(res).toHaveProperty('status', 'ok');
expect(res).toHaveProperty('timestamp');
expect(new Date(res.timestamp).toString()).not.toContain('Invalid');
});
});
Loading