feat(api): 开发平台、门店相关代码
This commit is contained in:
+38
-45
@@ -1,6 +1,7 @@
|
||||
import 'reflect-metadata';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
@@ -16,31 +17,10 @@ type RequestWithId = Request & {
|
||||
requestId?: string;
|
||||
};
|
||||
|
||||
function openSwaggerInDevelopment(config: AppConfig, logger: AppLogger): void {
|
||||
if (
|
||||
config.NODE_ENV !== 'development' ||
|
||||
!config.swaggerEnabled ||
|
||||
process.platform !== 'darwin'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const swaggerUrl = `http://localhost:${config.API_PORT}/api-docs`;
|
||||
// 只在本机开发时打开浏览器,生产环境和非 macOS 主机不执行外部进程。
|
||||
execFile('open', [swaggerUrl], (error) => {
|
||||
if (error) {
|
||||
logger.warn('swagger_auto_open_failed', {
|
||||
error: error.message,
|
||||
url: swaggerUrl,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
export async function createApplication() {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
||||
bodyParser: false,
|
||||
// Keep Nest's bootstrap errors visible until AppLogger is installed below.
|
||||
// 自定义日志器安装前保留 Nest 启动日志,避免配置错误没有可见诊断。
|
||||
logger: ['error', 'warn', 'log'],
|
||||
});
|
||||
const config = app.get<AppConfig>(APP_CONFIG);
|
||||
@@ -48,7 +28,7 @@ async function bootstrap() {
|
||||
app.useLogger(logger);
|
||||
|
||||
if (config.trustedProxyCidrs.length > 0) {
|
||||
// Never trust forwarded headers unless the deployment names its proxy networks explicitly.
|
||||
// 仅在部署明确声明代理网段时信任转发头,避免伪造客户端 IP。
|
||||
app.getHttpAdapter()
|
||||
.getInstance()
|
||||
.set('trust proxy', config.trustedProxyCidrs);
|
||||
@@ -68,6 +48,7 @@ async function bootstrap() {
|
||||
],
|
||||
exposedHeaders: ['X-Request-Id'],
|
||||
});
|
||||
// Helmet、body 限制、请求 ID 与全局错误过滤器必须在业务路由前注册。
|
||||
app.use(helmet({ contentSecurityPolicy: false }));
|
||||
app.useBodyParser('json', { limit: config.API_BODY_LIMIT_BYTES });
|
||||
app.useBodyParser('urlencoded', {
|
||||
@@ -79,6 +60,7 @@ async function bootstrap() {
|
||||
const typedRequest = request;
|
||||
const typedResponse = response;
|
||||
const suppliedId = typedRequest.headers['x-request-id'];
|
||||
// 合法调用方提供的 request ID 可贯穿上游链路;其他情况由服务端生成。
|
||||
const requestId =
|
||||
typeof suppliedId === 'string' &&
|
||||
/^[A-Za-z0-9_-]{8,128}$/.test(suppliedId)
|
||||
@@ -104,6 +86,7 @@ async function bootstrap() {
|
||||
app.enableShutdownHooks();
|
||||
|
||||
if (config.swaggerEnabled) {
|
||||
// Swagger 仅描述运行时已注册路由;冻结接口契约仍以 docs/api 为准。
|
||||
const swaggerConfig = new DocumentBuilder()
|
||||
.setTitle('Atlas Seller Kit API')
|
||||
.setDescription('Atlas 卖家工作台 HTTP API 文档')
|
||||
@@ -122,30 +105,40 @@ async function bootstrap() {
|
||||
});
|
||||
}
|
||||
|
||||
return { app, config, logger };
|
||||
}
|
||||
|
||||
export async function bootstrap() {
|
||||
const { app, config, logger } = await createApplication();
|
||||
await app.listen(config.API_PORT);
|
||||
logger.log('api_started', {
|
||||
port: config.API_PORT,
|
||||
swagger_enabled: config.swaggerEnabled,
|
||||
});
|
||||
openSwaggerInDevelopment(config, logger);
|
||||
}
|
||||
|
||||
void bootstrap().catch((error: unknown) => {
|
||||
const details =
|
||||
error instanceof Error
|
||||
? {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
}
|
||||
: error;
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'error',
|
||||
message: 'api_startup_failed',
|
||||
error: details,
|
||||
}),
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
// 测试复用应用工厂时不自动监听固定端口,只有直接运行入口才启动服务。
|
||||
if (
|
||||
process.argv[1] &&
|
||||
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
||||
) {
|
||||
void bootstrap().catch((error: unknown) => {
|
||||
const details =
|
||||
error instanceof Error
|
||||
? {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
}
|
||||
: error;
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'error',
|
||||
message: 'api_startup_failed',
|
||||
error: details,
|
||||
}),
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user