46361ec713
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>
99 lines
4.1 KiB
TypeScript
99 lines
4.1 KiB
TypeScript
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();
|