Files
dukang/server/dukang-api/src/main.ts
T
jacy 46361ec713 feat(v3.4.16): partner UX, package pricing, CS gate, HQ list polish
Stop auto ST from validation_error; show package prices with right-aligned layout; partner store list status/filter and onboard CS QR gate; HQ store table truncation/fixed actions; expose CS config in wechat_mini settings; bind local servers for LAN.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-09 16:00:45 +08:00

99 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import './load-env';
import { NestFactory } from '@nestjs/core';
import { loadAppConfig } from '@dukang/shared-types';
import { NestExpressApplication } from '@nestjs/platform-express';
import { ValidationPipe } from '@nestjs/common';
import { json, urlencoded, type NextFunction, type Request, type Response } from 'express';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
import { LoggingInterceptor } from './common/logging/logging.interceptor';
import { preloadSystemConfigEnv } from './common/system-config/system-config.env';
import { AlertService } from './common/alert/alert.service';
import { initSentryIfConfigured } from './integrations/sentry/sentry.bootstrap';
import { parseCourierCallbackBody } from './integrations/courier/xiaofeixia/xiaofeixia-callback-body';
function isCourierTrackCallbackUrl(url?: string): boolean {
if (!url) return false;
const path = url.split('?')[0] ?? '';
return (
(path.includes('/callbacks/courier/') && path.endsWith('/track')) ||
path.endsWith('/callbacks/delivery/track')
);
}
/** 小飞侠回调:无论 Content-Type,先吃下 rawBody 再尽力解析(含 multipart/form-data */
function courierTrackRawBodyMiddleware(req: Request, _res: Response, next: NextFunction) {
if (req.method !== 'POST' || !isCourierTrackCallbackUrl(req.originalUrl || req.url)) {
return next();
}
const chunks: Buffer[] = [];
req.on('data', (chunk: Buffer | string) => {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
});
req.on('error', (err) => next(err));
req.on('end', () => {
const buf = Buffer.concat(chunks);
const rawText = buf.toString('utf8');
(req as Request & { rawBody?: Buffer }).rawBody = buf;
req.body = parseCourierCallbackBody(rawText, String(req.headers['content-type'] || ''));
next();
});
}
async function bootstrap() {
const preloaded = await preloadSystemConfigEnv().catch((e) => {
console.warn('[config] system_config preload skipped:', e instanceof Error ? e.message : e);
return 0;
});
if (preloaded > 0) {
console.log(`[config] loaded ${preloaded} keys from system_config`);
}
initSentryIfConfigured();
const app = await NestFactory.create<NestExpressApplication>(AppModule, { bodyParser: false });
app.setGlobalPrefix('api/v1');
app.set('trust proxy', true);
app.enableCors({ origin: true, credentials: true });
// 小飞侠路由回调:优先捕获 rawBody(Content-Type 异常时也能入第三方日志)
app.use(courierTrackRawBodyMiddleware);
// 微信支付等需 rawBody;其它 JSON / form 请求走常规解析(跳过已由上面吃掉 body 的小飞侠回调)
app.use(
json({
type: (req) =>
!isCourierTrackCallbackUrl((req as Request).originalUrl || req.url) &&
Boolean(req.headers['content-type']?.includes('json')),
verify: (req, _res, buf) => {
if (
req.url?.includes('/callbacks/wechat/pay') ||
req.url?.includes('/callbacks/wechat/refund') ||
req.url?.includes('/callbacks/wechat/message')
) {
(req as { rawBody?: Buffer }).rawBody = buf;
}
},
}),
);
app.use(
urlencoded({
extended: true,
type: (req) =>
!isCourierTrackCallbackUrl((req as Request).originalUrl || req.url) &&
Boolean(req.headers['content-type']?.includes('urlencoded')),
}),
);
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
app.useGlobalFilters(new HttpExceptionFilter(app.get(AlertService)));
app.useGlobalInterceptors(new ResponseInterceptor(), app.get(LoggingInterceptor));
const port = Number(process.env.PORT || 3010);
const host = process.env.HOST || '0.0.0.0';
const cfg = loadAppConfig();
const smsMode = cfg.mockSms ? 'MOCK' : 'ALIYUN';
console.log(`[config] NODE_ENV=${process.env.NODE_ENV} MOCK_SMS=${cfg.mockSms} SMS=${smsMode}`);
await app.listen(port, host);
console.log(`dukang-api listening on http://${host}:${port}/api/v1`);
}
bootstrap();