cangku/backend/src/index.js

872 lines
31 KiB
JavaScript
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.

// 后端服务入口
const path = require('path');
require('dotenv').config();
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
const express = require('express');
const cors = require('cors');
const { CubejsServer } = require('@cubejs-backend/server');
const mysql = require('mysql2/promise');
const installRoutes = require('./api/install');
const authMiddleware = require('./middleware/auth');
const { createPageDataCache } = require('./middleware/pageDataCache');
const adminRoutes = require('./api/admin');
const planningUploadRoutes = require('./api/planningUpload');
const planningMayProductPublicRoutes = require('./api/planningMayProductPublic');
const planningSelectionFissionExternalRoutes = require('./api/planningSelectionFissionExternal');
const presaleExternalRoutes = require('./api/presaleExternal');
const delistExternalRoutes = require('./api/delistExternal');
const campaignCloudRoutes = require('./api/campaignCloud');
const desktopClientRoutes = require('./api/desktopClient');
const desktopPresenceRoutes = require('./api/desktopPresence');
const desktopExecutionTaskRoutes = require('./api/desktopExecutionTasks');
const alibaba1688ProductExternalRoutes = require('./api/alibaba1688ProductExternal');
const taobaoDetailPlusRoutes = require('./api/taobaoDetailPlus');
const thirdPartySsoRoutes = require('./api/thirdPartySso');
const thirdPartySsoAdminRoutes = require('./api/thirdPartySsoAdmin');
const channelRoutes = require('./api/channel');
const analysisRoutes = require('./api/analysis');
const productCodeBoardRoutes = require('./api/productCodeBoard');
const dashboardRoutes = require('./api/dashboard');
const database = require('./config/database');
const cloudDb = require('./config/cloudDatabase');
const jianSunDb = require('./config/jianSunDatabase');
const redis = require('./config/redis');
const { startScheduler, manualRefresh } = require('./jobs/dailyRefresh');
const { startStockSelectionSchedulers } = require('./jobs/stockSelectionSyncJob');
const { startTaobaoProductEmbeddingSchedulers } = require('./jobs/taobaoProductEmbeddingSyncJob');
const { startPddSkuDimensionSnapshotSchedulers } = require('./jobs/pddSkuDimensionSnapshotJob');
const { startPlanningMayProductSnapshotSchedulers } = require('./jobs/planningMayProductSnapshotJob');
const { startPlanningDingWorkflowSyncSchedulers } = require('./jobs/planningDingWorkflowSyncJob');
const {
startAlibaba1688ImageSearchExpiryScheduler,
} = require('./jobs/alibaba1688ImageSearchExpiryJob');
const { DEFAULT_SITE_CONFIG, getInstallStatus } = require('./services/installService');
const { ensureLocalSchema } = require('./services/localSchema');
const {
prewarmChannelHotData,
prewarmChannelIdDefaultSnapshots,
prewarmChannelIdDashboardDefaultSnapshot,
prewarmChannelStockMonitorDashboardDefaultSnapshot,
} = require('./services/channelAnalysis');
const { prewarmBrandAnalysisDefault } = require('./services/brandAnalysis');
const { runHeavyDatabaseTask } = require('./services/databaseLoadGuard');
const { warmSelectionFissionReadSchema } = require('./services/planningSelectionFissionService');
const {
getPublicSystemConfigCache,
getStalePublicSystemConfigCache,
setPublicSystemConfigCache,
} = require('./services/publicSystemConfigCache');
const { filterPublicSystemConfigs } = require('./services/systemConfigVisibility');
const {
shouldRequireImageSearchCredentials,
verifyImageSearchCredentials,
} = require('./services/imageSearchCredentialGuard');
const PLANNING_PAGE_REDIS_TTL_SECONDS = Math.max(
Number(process.env.PLANNING_PAGE_RESPONSE_CACHE_REDIS_TTL_SECONDS) || 23 * 60 * 60,
60
);
const BRAND_ANALYSIS_PREWARM_DELAY_MS = Math.max(
Number(process.env.BRAND_ANALYSIS_PREWARM_DELAY_MS) || 20 * 1000,
5 * 1000
);
const PUBLIC_SYSTEM_CONFIG_CACHE_TTL_MS = Math.max(
Number(process.env.PUBLIC_SYSTEM_CONFIG_CACHE_TTL_SECONDS) || 300,
30
) * 1000;
const PUBLIC_SYSTEM_CONFIG_QUERY_TIMEOUT_MS = Math.max(
Number(process.env.PUBLIC_SYSTEM_CONFIG_QUERY_TIMEOUT_MS) || 1800,
500
);
const CUBE_SCHEDULED_REFRESH_TIMER = String(process.env.CUBEJS_SCHEDULED_REFRESH_TIMER || '')
.trim()
.toLowerCase() === 'true';
function getBuildIdentity() {
return {
sourceGitSha: String(process.env.SOURCE_GIT_SHA || '').trim() || null,
sourceGitRef: String(process.env.SOURCE_GIT_REF || '').trim() || null,
};
}
const CHANNEL_STOCK_MONITOR_RESILIENCE_PREWARM_DELAY_MS = Math.max(
Number(process.env.CHANNEL_STOCK_MONITOR_RESILIENCE_PREWARM_DELAY_MS) || 90000,
10000
);
const CHANNEL_ID_RESILIENCE_PREWARM_DELAY_MS = Math.max(
Number(process.env.CHANNEL_ID_RESILIENCE_PREWARM_DELAY_MS) || 30000,
10000
);
function shouldCachePlanningUploadResponse(body = {}) {
if (!body || body.error || body.success === false) {
return false;
}
const payload = body.data || body;
if (payload?.pending || payload?.cache?.backgroundRefresh) {
return false;
}
return true;
}
function shouldHandlePlanningUploadPageCache(req) {
// 企划页首页会聚合 MAY 快照统计;MAY 商品页本身也依赖最新成功批次。
// MAY 渠道洞察也依赖最新店铺/覆盖快照,外层整页缓存会把旧列口径整天顶住。
// 榜单数据分析现在也依赖实时机会/竞争诊断结果,外层整页缓存会把旧字段结构整天顶住。
// 抖音/小红书市场页支持实时批次切换,整页缓存会把旧批次和旧字段结构顶住。
// 这些接口必须直接读实体表/轻缓存,不能再被 23 小时整页 Redis 顶住。
if (
req.path === '/'
|| req.path.startsWith('/table/may_product_data')
|| req.path.startsWith('/analysis/may-channel-insight')
|| req.path.startsWith('/selection-fission')
) {
return false;
}
return true;
}
const analysisPageCache = createPageDataCache({
namespace: 'analysis',
redisTtlSeconds: 300,
});
const channelPageCache = createPageDataCache({
namespace: 'channel',
redisTtlSeconds: 300,
// ID 分析内部已有按口径版本控制的缓存;导出接口返回 CSV,不能走整页 JSON 缓存。
shouldHandleRequest: (req) => !['/id-analysis', '/export'].includes(req.path)
&& !req.path.startsWith('/export-jobs'),
});
const planningUploadPageCache = createPageDataCache({
namespace: 'planning-upload',
version: 'v27-bypass-may-snapshot-pages-20260423',
redisTtlSeconds: PLANNING_PAGE_REDIS_TTL_SECONDS,
shouldHandleRequest: shouldHandlePlanningUploadPageCache,
shouldCacheBody: shouldCachePlanningUploadResponse,
});
let serverMode = 'install';
function isStartupPrewarmEnabled() {
return String(process.env.ENABLE_STARTUP_PREWARM || '').toLowerCase() === 'true';
}
function runGuardedPrewarm(label, handler) {
return runHeavyDatabaseTask(`prewarm:${label}`, handler);
}
function withTimeout(promise, timeoutMs, fallback) {
let timer = null;
return Promise.race([
promise.finally(() => {
if (timer) {
clearTimeout(timer);
}
}),
new Promise((resolve) => {
timer = setTimeout(() => resolve(fallback), timeoutMs);
if (typeof timer.unref === 'function') {
timer.unref();
}
}),
]);
}
function hasCompleteCloudEnvConfig() {
return cloudDb.hasCompleteEnvConfig();
}
function getCloudEnvConfig() {
const config = cloudDb.getEnvBusinessDatabaseConfig();
return {
driver: config.driver || 'mssql',
host: config.host,
port: config.port || 1433,
database: config.database,
user: config.user,
password: config.password,
ssl: Boolean(config.ssl),
};
}
function getBootstrapCloudConfig() {
return {
driver: 'mysql',
host: process.env.LOCAL_DB_HOST,
port: parseInt(process.env.LOCAL_DB_PORT || '3306', 10),
database: process.env.LOCAL_DB_NAME,
user: process.env.LOCAL_DB_USER,
password: process.env.LOCAL_DB_PASSWORD,
ssl: false,
bootstrap: true,
};
}
// =====================
// 从数据库获取云数据库配置
// =====================
async function getCloudDbConfig() {
let localPool;
try {
// 先连接本地数据库读取配置
localPool = mysql.createPool({
host: process.env.LOCAL_DB_HOST,
port: parseInt(process.env.LOCAL_DB_PORT || '3306'),
user: process.env.LOCAL_DB_USER,
password: process.env.LOCAL_DB_PASSWORD,
database: process.env.LOCAL_DB_NAME,
charset: 'utf8mb4',
waitForConnections: true,
connectionLimit: 2,
maxIdle: 2,
idleTimeout: 60000,
queueLimit: 10,
connectTimeout: 5000,
enableKeepAlive: true,
keepAliveInitialDelay: 0,
});
const [rows] = await localPool.query(
'SELECT host, port, database_name, username, password, `ssl`, driver, use_env_config FROM database_configs WHERE is_default = 1 AND status = ? LIMIT 1',
['active']
);
if (rows.length > 0) {
const config = rows[0];
if (config.use_env_config === 1 || config.use_env_config === true) {
if (hasCompleteCloudEnvConfig()) {
console.log('📌 使用 .env 业务数据库配置(CLOUD_DB_*)');
return getCloudEnvConfig();
}
console.warn('⚠️ 已开启 .env 业务数据库配置,但 CLOUD_DB_* 不完整,继续使用 database_configs 表配置');
}
console.log('📌 使用数据库配置(来自 database_configs 表)');
return {
driver: config.driver || 'mysql',
host: config.host,
port: config.port || 3306,
database: config.database_name,
user: config.username,
password: config.password,
ssl: config.ssl === 1,
};
}
} catch (error) {
console.warn('⚠️ 无法从数据库读取配置:', error.message);
} finally {
if (localPool) {
await localPool.end().catch((error) => {
console.warn('⚠️ 关闭系统库临时连接池失败:', error.message);
});
}
}
// 回退到环境变量配置
if (hasCompleteCloudEnvConfig()) {
console.log('📌 使用环境变量配置(CLOUD_DB_*)');
return getCloudEnvConfig();
}
console.log('⚙️ 业务数据库尚未配置,已进入系统初始化模式');
return getBootstrapCloudConfig();
}
function applyCommonMiddleware(app) {
app.set('trust proxy', 1);
app.use(cors());
app.use(express.json({
limit: '15mb',
verify: (req, _res, buffer) => {
req.rawBody = buffer.toString('utf8');
},
}));
}
function registerPublicSystemConfigRoute(app, fallbackConfig = DEFAULT_SITE_CONFIG) {
app.get('/api/admin/system-config', async (_req, res) => {
const now = Date.now();
const cachedConfig = getPublicSystemConfigCache(now);
if (cachedConfig) {
return res.json(cachedConfig);
}
try {
const configs = await Promise.race([
database.local.query('SELECT config_key, config_value, config_type FROM system_config'),
new Promise((_, reject) => {
setTimeout(() => reject(new Error('public system config query timeout')), PUBLIC_SYSTEM_CONFIG_QUERY_TIMEOUT_MS);
}),
]);
const result = {};
filterPublicSystemConfigs(configs).forEach((config) => {
if (config.config_type === 'boolean') {
result[config.config_key] = config.config_value === 'true' || config.config_value === '1';
} else if (config.config_type === 'number') {
result[config.config_key] = Number(config.config_value);
} else {
result[config.config_key] = config.config_value;
}
});
setPublicSystemConfigCache(result, PUBLIC_SYSTEM_CONFIG_CACHE_TTL_MS, now);
res.json(result);
} catch (error) {
const staleConfig = getStalePublicSystemConfigCache();
if (staleConfig) {
return res.json(staleConfig);
}
if (serverMode === 'install' || serverMode === 'database_unavailable') {
return res.json({
site_name: fallbackConfig.siteName,
site_description: fallbackConfig.siteDescription,
logo: '',
});
}
console.warn('获取系统配置超时或失败,返回兜底配置:', error.message);
return res.json({
site_name: fallbackConfig.siteName,
site_description: fallbackConfig.siteDescription,
logo: '',
});
}
});
}
function registerInstallModeRoutes(app, installStatus) {
const unavailableHandler = (_req, res) => {
res.status(503).json({ error: '系统尚未完成安装,请先访问 /install 完成初始化' });
};
app.get('/health', async (_req, res) => {
const status = await getInstallStatus();
res.json({
status: 'install_required',
timestamp: new Date().toISOString(),
build: getBuildIdentity(),
install: status,
});
});
app.use('/api/install', installRoutes);
registerPublicSystemConfigRoute(app);
app.get('/api/auth/providers', (_req, res) => {
res.json({
authBaseUrl: '',
dingtalkEnabled: false,
googleEnabled: false,
installRequired: true,
});
});
app.get('/api/auth/dingtalk/login', unavailableHandler);
app.get('/api/auth/google/login', unavailableHandler);
app.post('/api/auth/login', unavailableHandler);
app.post('/api/auth/callback', unavailableHandler);
app.get('/api/auth/verify', (_req, res) => {
res.status(401).json({ valid: false, installRequired: true });
});
app.use('/api/admin', (_req, res) => {
res.status(503).json({
error: '系统尚未完成安装,请先访问 /install 初始化系统数据库并创建管理员账号',
});
});
console.log('='.repeat(50));
console.log('🛠️ 系统已进入首次安装模式');
console.log(`📡 服务地址: http://localhost:${process.env.PORT || 4000}`);
console.log(`🧭 安装入口: http://localhost:${process.env.PORT || 4000}/install`);
console.log(`📦 安装状态: ${installStatus.systemDatabase.reason}`);
console.log('='.repeat(50));
}
async function startInstallServer(installStatus) {
serverMode = 'install';
const app = express();
applyCommonMiddleware(app);
registerInstallModeRoutes(app, installStatus);
const port = parseInt(process.env.PORT || '4000', 10);
await new Promise((resolve) => {
app.listen(port, resolve);
});
}
function registerDatabaseUnavailableRoutes(app, installStatus) {
const unavailableHandler = (_req, res) => {
res.status(503).json({
error: '系统数据库暂时无法连接,请检查 MySQL 网络、白名单或安全组后重启服务',
databaseUnavailable: true,
installRequired: false,
});
};
app.get('/health', async (_req, res) => {
const status = await getInstallStatus();
res.status(503).json({
status: 'database_unavailable',
timestamp: new Date().toISOString(),
build: getBuildIdentity(),
install: status,
});
});
app.use('/api/install', installRoutes);
registerPublicSystemConfigRoute(app);
app.get('/api/auth/providers', (_req, res) => {
res.json({
authBaseUrl: '',
dingtalkEnabled: false,
googleEnabled: false,
installRequired: false,
databaseUnavailable: true,
});
});
app.get('/api/auth/verify', (_req, res) => {
res.status(503).json({ valid: false, databaseUnavailable: true, installRequired: false });
});
app.post('/api/auth/login', unavailableHandler);
app.post('/api/auth/callback', unavailableHandler);
app.get('/api/auth/dingtalk/login', unavailableHandler);
app.get('/api/auth/google/login', unavailableHandler);
app.use('/api', unavailableHandler);
console.log('='.repeat(50));
console.log('🚧 系统数据库暂时无法连接,已进入降级模式');
console.log(`📡 服务地址: http://localhost:${process.env.PORT || 4000}`);
console.log(`📦 数据库状态: ${installStatus.systemDatabase.reason}`);
console.log('💡 这不是首次安装,请检查 MySQL 白名单/安全组/网络后重启服务');
console.log('='.repeat(50));
}
async function startDatabaseUnavailableServer(installStatus) {
serverMode = 'database_unavailable';
const app = express();
applyCommonMiddleware(app);
registerDatabaseUnavailableRoutes(app, installStatus);
const port = parseInt(process.env.PORT || '4000', 10);
await new Promise((resolve) => {
app.listen(port, resolve);
});
}
async function createFullModeApp(cloudConfig) {
const dbType = cloudConfig.driver === 'mssql' ? 'mssql' : 'mysql';
console.log(`📊 数据库类型: ${dbType}`);
const port = parseInt(process.env.PORT || '4000', 10);
if (String(process.env.DISABLE_CUBEJS || '').trim().toLowerCase() === 'true') {
console.warn('⏸️ 已通过 DISABLE_CUBEJS=true 跳过 Cube.js,仅启动业务 API 服务');
const app = express();
await new Promise((resolve) => {
app.listen(port, resolve);
});
return {
app,
cubeMode: 'disabled',
cubeError: 'DISABLE_CUBEJS=true',
};
}
try {
const cubejsServer = new CubejsServer({
schemaPath: 'model',
dbType: dbType,
logger: (msg, params) => {
console.log(`[Cube.js] ${msg}`, params !== undefined ? JSON.stringify(params) : '');
},
driverFactory: () => {
// 根据 driver 类型选择驱动
if (cloudConfig.driver === 'mssql') {
const MSSQLDriver = require('@cubejs-backend/mssql-driver');
return new MSSQLDriver({
server: cloudConfig.host,
port: cloudConfig.port,
database: cloudConfig.database,
user: cloudConfig.user,
password: cloudConfig.password,
options: {
encrypt: cloudConfig.ssl,
trustServerCertificate: true,
},
});
}
// 默认使用 MySQL
const MySQLDriver = require('@cubejs-backend/mysql-driver');
return new MySQLDriver({
database: cloudConfig.database,
host: cloudConfig.host,
port: cloudConfig.port,
user: cloudConfig.user,
password: cloudConfig.password,
ssl: cloudConfig.ssl,
});
},
apiSecret: process.env.CUBEJS_API_SECRET,
scheduledRefreshTimer: CUBE_SCHEDULED_REFRESH_TIMER,
jwt: {
key: process.env.CUBEJS_SECRET,
algorithms: ['HS256'],
},
});
const { app } = await cubejsServer.listen();
return {
app,
cubeMode: 'cube',
cubeError: null,
};
} catch (error) {
console.warn('⚠️ Cube.js 启动失败,已降级为业务 API 服务:', error.message);
const app = express();
await new Promise((resolve) => {
app.listen(port, resolve);
});
return {
app,
cubeMode: 'express-fallback',
cubeError: error.message,
};
}
}
// =====================
// 启动服务
// =====================
async function startServer(options = {}) {
const envSkipLocalSchema = String(process.env.SKIP_LOCAL_SCHEMA_CHECK || '').trim().toLowerCase() === 'true';
const { skipLocalSchema = envSkipLocalSchema } = options;
serverMode = 'full';
if (skipLocalSchema) {
console.warn('⚠️ 系统数据库暂时不可用,已跳过本次本地 schema 检查,继续启动业务 API');
} else {
await ensureLocalSchema();
}
if (shouldRequireImageSearchCredentials()) {
const credentialStatus = await verifyImageSearchCredentials();
console.log(`✅ 图搜凭据守卫通过:公司=${credentialStatus.company.source}, 1688=${credentialStatus.alibaba1688.source}`);
}
// 获取云数据库配置
const cloudConfig = await getCloudDbConfig();
if (cloudConfig.bootstrap) {
console.log(`📊 初始化模式占位数据源: ${cloudConfig.host}:${cloudConfig.port}/${cloudConfig.database}`);
} else {
console.log(`📊 云数据库: ${cloudConfig.host}:${cloudConfig.port}/${cloudConfig.database}`);
}
// 启动 Cube.js 服务器;如 Cube Store 不可用,降级为普通业务 API 服务,避免整站不可用。
const { app: cubejsApp, cubeMode, cubeError } = await createFullModeApp(cloudConfig);
applyCommonMiddleware(cubejsApp);
// =====================
// 健康检查 (多数据源)
// =====================
cubejsApp.get('/health', async (_req, res) => {
const [systemTest, businessTest, redisTest] = await Promise.all([
withTimeout(database.local.test(), 5000, { success: false, message: '系统数据库检测超时' }),
withTimeout(cloudDb.test(), 5000, { success: false, message: '业务数据库检测超时' }),
withTimeout(redis.test(), 3000, { success: false, message: 'Redis 检测超时' }),
]);
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
build: getBuildIdentity(),
databases: {
system: systemTest,
local: systemTest,
cloud: businessTest,
},
services: {
redis: redisTest,
cube: {
success: cubeMode === 'cube',
mode: cubeMode,
message: cubeMode === 'cube' ? 'Cube.js 服务正常' : `Cube.js 已降级: ${cubeError || '未知错误'}`,
},
},
});
});
// =====================
// 认证 API
// =====================
cubejsApp.get('/api/auth/providers', require('./api/auth').getProviders);
cubejsApp.get('/api/auth/dingtalk/login', require('./api/auth').startDingTalkLogin);
cubejsApp.get('/api/auth/google/login', require('./api/auth').startGoogleLogin);
cubejsApp.post('/api/auth/login', require('./api/auth').login);
cubejsApp.post('/api/auth/local-preview', require('./api/auth').localPreview);
cubejsApp.post('/api/auth/callback', require('./api/auth').callback);
cubejsApp.get('/api/auth/verify', require('./api/auth').verify);
// =====================
// 公开访问的 API(无需认证)
// =====================
cubejsApp.use('/api/install', installRoutes);
cubejsApp.use('/api/sso', thirdPartySsoRoutes);
cubejsApp.use('/api/desktop-client', desktopClientRoutes);
cubejsApp.use('/api/public/taobao-detail-plus', taobaoDetailPlusRoutes);
cubejsApp.use('/api/public/planning-may-product', planningMayProductPublicRoutes);
cubejsApp.use('/api/external/selection-fission/presale', presaleExternalRoutes);
cubejsApp.use('/api/external/selection-fission/delist', delistExternalRoutes);
cubejsApp.use('/api/campaign', campaignCloudRoutes);
cubejsApp.use('/api/external/selection-fission/1688-products', alibaba1688ProductExternalRoutes);
cubejsApp.use('/api/external/selection-fission', planningSelectionFissionExternalRoutes);
cubejsApp.use('/api/external/presale', presaleExternalRoutes);
// 系统配置的 GET 接口公开访问(登录页需要获取网站名称和 Logo)
registerPublicSystemConfigRoute(cubejsApp);
// =====================
// 需要认证的 API
// =====================
cubejsApp.use('/api/admin/sso-clients', authMiddleware, authMiddleware.requireRole('admin'), thirdPartySsoAdminRoutes);
cubejsApp.use('/api/admin', authMiddleware, adminRoutes);
cubejsApp.use('/api/desktop-presence', authMiddleware, desktopPresenceRoutes);
cubejsApp.use('/api/desktop-execution', desktopExecutionTaskRoutes);
cubejsApp.use('/api/product-code-board', authMiddleware, productCodeBoardRoutes);
cubejsApp.use('/api/dashboard', authMiddleware, dashboardRoutes);
cubejsApp.use('/api/planning-upload', authMiddleware, planningUploadPageCache, planningUploadRoutes);
cubejsApp.use('/api/channel', authMiddleware, channelPageCache, channelRoutes);
// =====================
// 数据分析 API(公开访问,可根据需要添加认证)
// =====================
cubejsApp.use('/api/analysis', analysisPageCache, analysisRoutes);
// =====================
// 数据刷新 API(需要认证)
// =====================
cubejsApp.post('/api/admin/refresh', authMiddleware, async (req, res) => {
try {
const { date } = req.body;
console.log(`[API] 手动触发数据刷新: ${date || '昨天'}`);
const result = await manualRefresh(date);
res.json({ success: true, ...result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// =====================
// 重新加载云数据库配置 API
// =====================
cubejsApp.post('/api/admin/database/reload', authMiddleware, async (req, res) => {
try {
console.log('🔄 正在重新加载云数据库配置...');
res.json({
success: true,
message: '配置已保存,需要重启服务才能生效。请执行: pm2 restart maixiang'
});
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
console.log('='.repeat(50));
console.log('🚀 后端服务已启动');
console.log(`📡 服务地址: http://localhost:4000`);
console.log(cubeMode === 'cube'
? '📊 Cube.js API: http://localhost:4000/cubejs-api'
: '📊 Cube.js API: 已降级不可用,业务 API 继续运行');
// 测试数据库连接
console.log('\n📦 数据库连接测试:');
const [systemTest, businessTest, redisTest] = await Promise.all([
withTimeout(database.local.test(), 5000, { message: '系统数据库检测超时' }),
withTimeout(cloudDb.test(), 5000, { message: '业务数据库检测超时' }),
withTimeout(redis.test(), 3000, { message: 'Redis 检测超时' }),
]);
console.log(` [系统] ${systemTest.message}`);
console.log(` [业务] ${businessTest.message}`);
console.log(` [缓存] ${redisTest.message}`);
console.log('='.repeat(50));
if (String(process.env.ENABLE_BACKGROUND_JOBS || 'true').toLowerCase() === 'false') {
console.log('⏸️ 后台定时任务和预热已关闭,业务请求按需处理');
return;
}
startScheduler();
startStockSelectionSchedulers();
startTaobaoProductEmbeddingSchedulers();
startPddSkuDimensionSnapshotSchedulers();
startPlanningMayProductSnapshotSchedulers();
startPlanningDingWorkflowSyncSchedulers();
startAlibaba1688ImageSearchExpiryScheduler();
// Keep the first selection-pool page request from paying the remote schema
// inspection round-trip over the planning database tunnel.
setTimeout(() => {
warmSelectionFissionReadSchema()
.then(() => console.log('[SelectionFission] 列表读取结构预热完成'))
.catch((error) => console.warn('[SelectionFission] 列表读取结构预热失败:', error.message));
}, 1000);
if (isStartupPrewarmEnabled()) {
setTimeout(() => {
if (typeof prewarmChannelHotData === 'function') {
runGuardedPrewarm('channel-hot-data', () => prewarmChannelHotData({ skipCoverage: true })).catch((error) => {
console.warn('⚠️ 渠道默认查询预热失败:', error.message);
});
}
}, 8000);
setTimeout(() => {
if (typeof analysisRoutes.prewarmHotAnalysisPages === 'function') {
runGuardedPrewarm('analysis-hot-pages', () => analysisRoutes.prewarmHotAnalysisPages()).catch((error) => {
console.warn('⚠️ 分析页默认查询预热失败:', error.message);
});
}
}, 10000);
if (typeof analysisRoutes.prewarmHotAnalysisPages === 'function') {
const analysisPrewarmTimer = setInterval(() => {
runGuardedPrewarm('analysis-hot-pages:interval', () => analysisRoutes.prewarmHotAnalysisPages()).catch((error) => {
console.warn('⚠️ 分析页默认查询定时预热失败:', error.message);
});
}, 10 * 60 * 1000);
if (typeof analysisPrewarmTimer.unref === 'function') {
analysisPrewarmTimer.unref();
}
}
} else {
console.log('⏸️ 启动预热已关闭,可通过 ENABLE_STARTUP_PREWARM=true 开启');
}
if (!isStartupPrewarmEnabled() && typeof prewarmChannelStockMonitorDashboardDefaultSnapshot === 'function') {
if (typeof prewarmChannelIdDefaultSnapshots === 'function') {
setTimeout(() => {
runGuardedPrewarm(
'channel-id-analysis-resilience',
() => prewarmChannelIdDefaultSnapshots({ skipCoverage: true })
).catch((error) => {
console.warn('⚠️ ID数据分析韧性快照预热失败:', error.message);
});
}, CHANNEL_ID_RESILIENCE_PREWARM_DELAY_MS);
}
if (typeof prewarmChannelIdDashboardDefaultSnapshot === 'function') {
setTimeout(() => {
runGuardedPrewarm(
'channel-id-dashboard-resilience',
() => prewarmChannelIdDashboardDefaultSnapshot({ skipCoverage: true })
).catch((error) => {
console.warn('⚠️ ID数据分析仪表盘韧性快照预热失败:', error.message);
});
}, CHANNEL_ID_RESILIENCE_PREWARM_DELAY_MS + 15000);
}
setTimeout(() => {
runGuardedPrewarm(
'channel-stock-monitor-resilience',
() => prewarmChannelStockMonitorDashboardDefaultSnapshot({ skipCoverage: true })
).catch((error) => {
console.warn('⚠️ 库存销售影响仪表盘韧性快照预热失败:', error.message);
});
}, CHANNEL_STOCK_MONITOR_RESILIENCE_PREWARM_DELAY_MS);
}
if (String(process.env.ENABLE_BRAND_ANALYSIS_PREWARM || 'true').toLowerCase() !== 'false'
&& typeof prewarmBrandAnalysisDefault === 'function') {
setTimeout(() => {
runGuardedPrewarm('brand-analysis-default', () => prewarmBrandAnalysisDefault())
.then((result) => {
console.log(
`[BrandAnalysis] default prewarm completed total=${result.total || 0} elapsedMs=${result.elapsedMs || 0}`
);
})
.catch((error) => {
console.warn('⚠️ 品牌维度默认真实数据预热失败:', error.message);
});
}, BRAND_ANALYSIS_PREWARM_DELAY_MS);
}
const cron = require('node-cron');
if (typeof prewarmChannelHotData === 'function') {
cron.schedule('0 1 * * *', async () => {
console.log('🧊 凌晨 1 点执行渠道 23 小时缓存预热...');
await runGuardedPrewarm(
'channel-hot-data:daily',
() => prewarmChannelHotData({ forceRefresh: true, skipCoverage: true })
);
}, {
timezone: 'Asia/Shanghai',
});
console.log('[Scheduler] 渠道 23 小时缓存预热任务已注册: 每天01:00执行');
}
if (process.env.NODE_ENV === 'production') {
const alertChecker = require('./services/alertChecker');
cron.schedule('*/5 * * * *', async () => {
console.log('🔔 执行预警检查...');
await alertChecker.checkAllRules();
});
}
}
async function bootstrap() {
const installStatus = await getInstallStatus();
if (installStatus.required) {
await startInstallServer(installStatus);
return;
}
if (installStatus.databaseUnavailable) {
console.warn(`⚠️ 系统数据库暂时不可用,不进入降级拦截模式: ${installStatus.systemDatabase?.reason || 'unknown'}`);
await startServer({ skipLocalSchema: true });
return;
}
await startServer();
}
bootstrap().catch((error) => {
console.error('❌ 服务启动失败:', error);
process.exit(1);
});
async function shutdownGracefully() {
await Promise.allSettled([
cloudDb.close(),
jianSunDb.close(),
database.close(),
redis.close(),
]);
}
// 优雅关闭
process.on('SIGTERM', async () => {
console.log('正在关闭服务...');
await shutdownGracefully();
process.exit(0);
});
process.on('SIGINT', async () => {
console.log('正在关闭服务...');
await shutdownGracefully();
process.exit(0);
});