feat(api): 开发平台、门店相关代码

This commit is contained in:
R524809
2026-09-03 16:04:44 +08:00
parent 35fcf8776e
commit ada02e9135
40 changed files with 7014 additions and 5807 deletions
+38 -45
View File
@@ -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;
});
}