const { isRetiredSystemConfigKey, pruneRetiredSidebarConfig } = require('../services/retiredFeatures'); // 管理员 API const express = require('express'); const router = express.Router(); const bcrypt = require('bcryptjs'); const crypto = require('crypto'); const dayjs = require('dayjs'); const fs = require('fs'); const multer = require('multer'); const os = require('os'); const path = require('path'); const { v4: uuidv4 } = require('uuid'); const { requireRole } = require('../middleware/auth'); const database = require('../config/database'); const cloudDb = require('../config/cloudDatabase'); const { dailyRefreshJob } = require('../jobs/dailyRefresh'); const { manualJxStockSelectionSync } = require('../jobs/jxStockSelectionSyncJob'); const { getAuthConfig, saveAuthConfig } = require('../services/authConfig'); const { ensureObjectStorageSchema, getDefaultS3Config, normalizeEndpoint, normalizeProtocol, normalizePublicDomain, normalizeStorageClass, testObjectStorageConnection, uploadBase64ImageToDefaultS3, uploadFileToDefaultS3, } = require('../services/objectStorage'); const { compareVersions, getLatestDesktopRelease, listDesktopReleases, normalizeArchitecture, normalizePlatform, publishDesktopRelease, validateVersion, } = require('../services/desktopReleaseService'); const { listOnlineDesktopUsers } = require('../services/desktopPresenceService'); const { getSystemDbConfig, saveSystemDbConfig, testSystemDbConnection, } = require('../services/systemDbConfig'); const { getEffectiveRedisConfig, getRedisConfig, getRedisRuntimeProfile, getRedisRuntimeProfiles, getStoredRedisConfigs, saveRedisConfig, setRedisRuntimeProfile, testRedisConnection, } = require('../services/redisConfig'); const { getWorkOrderConfig, saveWorkOrderConfig, testWorkOrderConnection, } = require('../services/workOrderIntegration'); const redisRuntime = require('../config/redis'); const { getPageDataCacheStats, resetPageDataCacheStats, } = require('../middleware/pageDataCache'); const { getSetupStatus, markSetupCompleted, } = require('../services/setupWizard'); const { syncDingTalkUsers } = require('../services/socialAuth'); const { SIDEBAR_PAGES, getDepartmentPagePermissions, getSidebarRolePermissions, getUserPagePermissions, saveDepartmentPagePermissions, saveUserPagePermissions, } = require('../services/pageAccess'); const { getStatsSnapshot, resetStats } = require('../services/dataPageCache'); const { getHeavyDatabaseTaskStats } = require('../services/databaseLoadGuard'); const { batchUpsertAiImageFeatures } = require('../services/aiImageFeatureLibrary'); const { buildProductCodeImportTemplateBuffer, buildProductCodeBatchImportTemplateBuffer, importProductCodeWorkbook, deleteAllProductCodeItems, deleteProductCodeItemsByCodes, } = require('../services/productCodeBoard'); const { clearPublicSystemConfigCache } = require('../services/publicSystemConfigCache'); const { filterPublicSystemConfigs, isProtectedSystemConfigKey, } = require('../services/systemConfigVisibility'); const { createSelectionFissionApiKey, deleteSelectionFissionApiKey, listSelectionFissionApiKeys, updateSelectionFissionApiKey, } = require('../services/selectionFissionApiKeyService'); const { createExternalAutomationApp, deleteExternalAutomationApp, listExternalAutomationApps, updateExternalAutomationApp, } = require('../services/externalAutomationAppService'); const { getSelectionFissionAiAdminSettings, saveSelectionFissionAiConfig, testSelectionFissionAiConfig, } = require('../services/selectionFissionAiSettings'); const { testSelectionImageGenerationConfig } = require('../services/selectionImageGenerationService'); const { getProductTagConfig, saveProductTagConfig, } = require('../services/productTagConfigService'); const backfillJobs = new Map(); const productCodeImportUpload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 20 * 1024 * 1024, }, fileFilter: (_req, file, callback) => { const lowerName = String(file.originalname || '').toLowerCase(); if (lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls')) { callback(null, true); return; } callback(new Error('仅支持上传 Excel 文件(.xlsx / .xls)')); }, }); const desktopReleaseTempDir = path.join(os.tmpdir(), 'maixiang-desktop-releases'); fs.mkdirSync(desktopReleaseTempDir, { recursive: true }); const desktopReleaseUpload = multer({ dest: desktopReleaseTempDir, limits: { fileSize: 300 * 1024 * 1024, files: 1, }, fileFilter: (_req, file, callback) => { if (/\.(exe|dmg)$/i.test(String(file.originalname || ''))) { callback(null, true); return; } callback(new Error('仅支持上传 Windows(.exe)或 macOS(.dmg)安装包')); }, }); const uploadDesktopReleaseInstaller = (req, res, next) => { desktopReleaseUpload.single('installer')(req, res, (error) => { if (!error) return next(); if (error.code === 'LIMIT_FILE_SIZE') { return res.status(413).json({ success: false, error: '安装包不能超过 300 MB' }); } return res.status(400).json({ success: false, error: error.message || '安装包上传失败' }); }); }; async function sha256File(filePath) { const hash = crypto.createHash('sha256'); await new Promise((resolve, reject) => { const stream = fs.createReadStream(filePath); stream.on('data', (chunk) => hash.update(chunk)); stream.on('error', reject); stream.on('end', resolve); }); return hash.digest('hex'); } const runBackfill = async (job) => { try { const totalStepsPerDay = 21; job.totalTasks = job.totalDays * totalStepsPerDay; job.completedTasks = 0; job.remainingTasks = job.totalTasks; for (let i = 0; i < job.totalDays; i++) { const date = dayjs(job.startDate).add(i, 'day').format('YYYY-MM-DD'); job.currentDate = date; await dailyRefreshJob(date, (step) => { job.currentTask = step.name; job.currentStep = step.currentStep; job.totalStepsPerDay = step.totalSteps; job.completedTasks = i * totalStepsPerDay + step.currentStep; job.remainingTasks = Math.max(0, job.totalTasks - job.completedTasks); }); job.completedDays = i + 1; job.remainingDays = job.totalDays - job.completedDays; } job.status = 'success'; job.finishedAt = new Date().toISOString(); } catch (error) { job.status = 'failed'; job.error = error.message; job.finishedAt = new Date().toISOString(); } }; router.use(requireRole('admin')); router.get('/desktop-presence', async (_req, res) => { try { const data = await listOnlineDesktopUsers(); res.json({ success: true, data }); } catch (error) { console.error('获取桌面客户端在线用户失败:', error); res.status(500).json({ success: false, error: '获取桌面客户端在线用户失败' }); } }); router.get('/desktop-releases', async (req, res) => { try { const releases = await listDesktopReleases(req.query.limit); res.json({ success: true, data: releases.map((release) => ({ ...release, downloadUrl: `/api/desktop-client/releases/${release.id}/download`, })), }); } catch (error) { console.error('获取桌面客户端发布记录失败:', error); res.status(500).json({ success: false, error: '获取桌面客户端发布记录失败' }); } }); router.post('/desktop-releases', uploadDesktopReleaseInstaller, async (req, res) => { const tempFilePath = req.file?.path; try { if (!req.file) { return res.status(400).json({ success: false, error: '请选择需要发布的桌面客户端安装包' }); } const version = validateVersion(req.body?.version); const originalName = String(req.file.originalname || ''); const platform = normalizePlatform(req.body?.platform, originalName); const architecture = normalizeArchitecture(req.body?.architecture, originalName, platform); const expectedExtension = platform === 'darwin' ? '.dmg' : '.exe'; if (!originalName.toLowerCase().endsWith(expectedExtension)) { return res.status(400).json({ success: false, error: platform === 'darwin' ? 'macOS 安装包必须为 .dmg 格式' : 'Windows 安装包必须为 .exe 格式', }); } const latest = await getLatestDesktopRelease(platform, architecture); if (latest && compareVersions(version, latest.version) < 0) { return res.status(409).json({ success: false, error: `发布版本 v${version} 低于当前平台最新版本 v${latest.version}`, }); } const fileName = platform === 'darwin' ? `pdd-auto-sale-${version}-${architecture}.dmg` : `pdd-auto-sale-${version}-setup.exe`; const objectKey = `desktop-client/${platform}/${version}/${Date.now()}-${fileName}`; const mimeType = platform === 'darwin' ? 'application/x-apple-diskimage' : 'application/vnd.microsoft.portable-executable'; const [sha256, uploadResult] = await Promise.all([ sha256File(tempFilePath), uploadFileToDefaultS3(tempFilePath, { key: objectKey, mimeType, }), ]); const release = await publishDesktopRelease({ version, platform, architecture, releaseNotes: String(req.body?.releaseNotes || '').trim(), objectKey: uploadResult.key, fileName, fileSize: req.file.size, sha256, publishedByUserId: Number(req.user?.sub || req.user?.id || 0) || null, publishedByName: req.user?.name || req.user?.username || req.user?.email || '', }); res.status(201).json({ success: true, message: `${platform === 'darwin' ? 'macOS' : 'Windows'} 客户端 v${version} 已发布`, data: { ...release, downloadUrl: `/api/desktop-client/releases/${release.id}/download`, }, }); } catch (error) { console.error('发布桌面客户端失败:', error); const status = /版本号格式/.test(error.message) ? 400 : 500; res.status(status).json({ success: false, error: error.message || '发布桌面客户端失败' }); } finally { if (tempFilePath) { await fs.promises.unlink(tempFilePath).catch(() => {}); } } }); router.get('/cache-performance', async (_req, res) => { const dataPayloadStats = getStatsSnapshot(); res.json({ success: true, data: { ...dataPayloadStats, pageResponseCache: getPageDataCacheStats(), heavyDatabaseTasks: getHeavyDatabaseTaskStats(), }, }); }); router.post('/cache-performance/reset', async (_req, res) => { resetStats(); resetPageDataCacheStats(); const dataPayloadStats = getStatsSnapshot(); res.json({ success: true, message: '缓存性能统计已重置', data: { ...dataPayloadStats, pageResponseCache: getPageDataCacheStats(), heavyDatabaseTasks: getHeavyDatabaseTaskStats(), }, }); }); router.get('/product-code-board/import-template', async (_req, res) => { try { const buffer = buildProductCodeImportTemplateBuffer(); const fileName = encodeURIComponent('测款数据导入模板.xlsx'); res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${fileName}`); res.send(buffer); } catch (error) { console.error('生成测款导入模板失败:', error); res.status(500).json({ error: '生成测款导入模板失败' }); } }); router.get('/product-code-board/batch-template', async (_req, res) => { try { const buffer = buildProductCodeBatchImportTemplateBuffer(); const fileName = encodeURIComponent('测款批量新增模板.xlsx'); res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${fileName}`); res.send(buffer); } catch (error) { console.error('生成测款批量新增模板失败:', error); res.status(500).json({ error: '生成测款批量新增模板失败' }); } }); router.post('/product-code-board/import', (req, res, next) => { productCodeImportUpload.single('file')(req, res, (error) => { if (error) { return res.status(400).json({ success: false, error: error.message || '上传文件失败' }); } return next(); }); }, async (req, res) => { try { if (!req.file?.buffer) { return res.status(400).json({ success: false, error: '请上传测款数据 Excel 文件' }); } const data = await importProductCodeWorkbook(req.file.buffer, req.user || {}); res.json({ success: true, message: '测款数据导入成功', data, }); } catch (error) { console.error('导入测款数据失败:', error); res.status(error.status || error.statusCode || 500).json({ success: false, error: error.message || '导入测款数据失败', details: error.details || null, }); } }); async function handleDeleteProductCodeItems(_req, res) { try { const data = await deleteAllProductCodeItems(); res.json({ success: true, message: `已删除 ${data.deletedRows} 条测款数据,清理 ${data.deletedStyleRows || 0} 条款式记录`, data, }); } catch (error) { console.error('删除测款数据失败:', error); res.status(error.status || error.statusCode || 500).json({ success: false, error: error.message || '删除测款数据失败', }); } } async function handleDeleteSelectedProductCodeItems(req, res) { try { const data = await deleteProductCodeItemsByCodes(req.body?.productCodes); const missingText = data.notFoundCodes?.length ? `,未找到 ${data.notFoundCodes.length} 个编码` : ''; res.json({ success: true, message: `已删除 ${data.deletedRows} 条测款数据,清理 ${data.deletedStyleRows || 0} 条款式记录${missingText}`, data, }); } catch (error) { console.error('按编码删除测款数据失败:', error); res.status(error.status || error.statusCode || 500).json({ success: false, error: error.message || '按编码删除测款数据失败', }); } } router.post('/product-code-board/items/delete-all', handleDeleteProductCodeItems); router.post('/product-code-board/items/delete-selected', handleDeleteSelectedProductCodeItems); router.delete('/product-code-board/items', handleDeleteProductCodeItems); // ==================== 首次安装向导 ==================== router.get('/setup/status', async (req, res) => { try { const status = await getSetupStatus(req.user.sub); res.json(status); } catch (error) { console.error('获取首次安装状态失败:', error); res.status(500).json({ error: '获取首次安装状态失败' }); } }); router.post('/setup/password', async (req, res) => { try { const { currentPassword, newPassword } = req.body || {}; if (!currentPassword || !newPassword) { return res.status(400).json({ error: '请填写当前密码和新密码' }); } if (String(newPassword).length < 8) { return res.status(400).json({ error: '新密码长度不能少于 8 位' }); } const users = await database.local.query( 'SELECT id, password_hash FROM users WHERE id = ? LIMIT 1', [req.user.sub] ); const currentUser = users[0]; if (!currentUser) { return res.status(404).json({ error: '当前账号不存在' }); } const matched = await bcrypt.compare(currentPassword, currentUser.password_hash || ''); if (!matched) { return res.status(400).json({ error: '当前密码不正确' }); } const sameAsCurrent = await bcrypt.compare(newPassword, currentUser.password_hash || ''); if (sameAsCurrent) { return res.status(400).json({ error: '新密码不能与当前密码相同' }); } const passwordHash = await bcrypt.hash(newPassword, 10); await database.local.update('users', currentUser.id, { password_hash: passwordHash }); const status = await getSetupStatus(req.user.sub); res.json({ message: '管理员密码已更新', setup: status, }); } catch (error) { console.error('更新管理员密码失败:', error); res.status(500).json({ error: '更新管理员密码失败' }); } }); router.post('/setup/complete', async (req, res) => { try { const status = await markSetupCompleted(req.user.sub); res.json({ message: '首次安装向导已完成,请重启后端服务后重新登录', needRestart: true, ...status, }); } catch (error) { console.error('完成首次安装向导失败:', error); res.status(400).json({ error: error.message || '完成首次安装向导失败' }); } }); // ==================== 京喜现货选品同步 ==================== router.post('/jx/stock-selection/sync', async (_req, res) => { try { const result = await manualJxStockSelectionSync(); res.json({ success: true, data: result, }); } catch (error) { console.error('京喜现货选品同步失败:', error); res.status(500).json({ success: false, error: error.message || '京喜现货选品同步失败' }); } }); // ==================== 用户管理 ==================== async function getManagedUser(userId) { const users = await database.local.query( 'SELECT id, username, name, role, status FROM users WHERE id = ? LIMIT 1', [userId] ); return users[0] || null; } async function countOtherActiveAdmins(userId) { const result = await database.local.query( "SELECT COUNT(*) AS total FROM users WHERE role = 'admin' AND status = 'active' AND id <> ?", [userId] ); return result[0]?.total || 0; } async function ensureAdminRoleChangeAllowed(req, targetUser, nextRole) { if (!targetUser) { return { allowed: false, status: 404, error: '用户不存在' }; } if (targetUser.role !== 'admin' || nextRole === 'admin') { return { allowed: true }; } if (String(req.user.sub) === String(targetUser.id)) { return { allowed: false, status: 400, error: '不能取消当前登录账号的管理员权限' }; } const otherAdminCount = await countOtherActiveAdmins(targetUser.id); if (otherAdminCount === 0) { return { allowed: false, status: 400, error: '至少保留一位启用状态的管理员' }; } return { allowed: true }; } // 获取用户列表 router.get('/users', async (req, res) => { try { const { page = 1, size = 20, keyword = '' } = req.query; const offset = (page - 1) * size; let sql = ` SELECT u.id, u.username, u.name, u.email, u.role, u.status, u.department_id, u.created_at, u.auth_source AS authSource, u.avatar_url AS avatarUrl, u.mobile, u.job_title AS jobTitle, u.dingtalk_user_id AS dingtalkUserId, u.dingtalk_union_id AS dingtalkUnionId, u.google_sub AS googleSub, u.department_names AS departmentNames, u.last_login_at AS lastLoginAt, d.name AS departmentName, d.source AS departmentSource FROM users u LEFT JOIN departments d ON u.department_id = d.id `; const params = []; if (keyword) { sql += ` WHERE u.username LIKE ? OR u.name LIKE ? OR u.email LIKE ? OR COALESCE(u.mobile, '') LIKE ? OR COALESCE(u.department_names, '') LIKE ? OR COALESCE(d.name, '') LIKE ? `; params.push(`%${keyword}%`, `%${keyword}%`, `%${keyword}%`, `%${keyword}%`, `%${keyword}%`, `%${keyword}%`); } sql += ' ORDER BY u.created_at DESC LIMIT ? OFFSET ?'; params.push(parseInt(size), parseInt(offset)); const users = await database.local.query(sql, params); // 获取总数 let countSql = ` SELECT COUNT(*) as total FROM users u LEFT JOIN departments d ON u.department_id = d.id `; if (keyword) { countSql += ` WHERE u.username LIKE ? OR u.name LIKE ? OR u.email LIKE ? OR COALESCE(u.mobile, '') LIKE ? OR COALESCE(u.department_names, '') LIKE ? OR COALESCE(d.name, '') LIKE ? `; } const countResult = await database.local.query( countSql, keyword ? [`%${keyword}%`, `%${keyword}%`, `%${keyword}%`, `%${keyword}%`, `%${keyword}%`, `%${keyword}%`] : [] ); const total = countResult[0]?.total || 0; res.json({ data: users, total, page: parseInt(page), size: parseInt(size), }); } catch (error) { console.error('获取用户列表失败:', error); res.status(500).json({ error: '获取用户列表失败' }); } }); // 获取单个用户 router.get('/users/:id', async (req, res) => { try { const users = await database.local.query( `SELECT u.id, u.username, u.name, u.email, u.role, u.status, u.department_id, u.created_at, u.auth_source AS authSource, u.avatar_url AS avatarUrl, u.mobile, u.job_title AS jobTitle, u.dingtalk_user_id AS dingtalkUserId, u.dingtalk_union_id AS dingtalkUnionId, u.google_sub AS googleSub, u.department_names AS departmentNames, u.last_login_at AS lastLoginAt, d.name AS departmentName FROM users u LEFT JOIN departments d ON u.department_id = d.id WHERE u.id = ?`, [req.params.id] ); if (users.length === 0) { return res.status(404).json({ error: '用户不存在' }); } res.json(users[0]); } catch (error) { console.error('获取用户失败:', error); res.status(500).json({ error: '获取用户失败' }); } }); router.get('/users/:id/page-permissions', async (req, res) => { try { const data = await getUserPagePermissions(req.params.id); if (!data) { return res.status(404).json({ error: '用户不存在' }); } res.json(data); } catch (error) { console.error('获取用户页面权限失败:', error); res.status(500).json({ error: '获取用户页面权限失败' }); } }); router.put('/users/:id/page-permissions', async (req, res) => { try { const targetUser = await getManagedUser(req.params.id); if (!targetUser) { return res.status(404).json({ error: '用户不存在' }); } const permissions = Array.isArray(req.body?.permissions) ? req.body.permissions : []; const result = await saveUserPagePermissions(req.params.id, permissions); res.json({ message: '用户可见页面已更新', user: result.user, permissions: result.permissions, }); } catch (error) { console.error('更新用户页面权限失败:', error); res.status(500).json({ error: '更新用户页面权限失败' }); } }); // 创建用户 router.post('/users', async (req, res) => { try { const { username, name, email, password, departmentId, role, mobile, jobTitle } = req.body; // 检查用户名是否存在 const existing = await database.local.query('SELECT id FROM users WHERE username = ?', [username]); if (existing.length > 0) { return res.status(400).json({ error: '用户名已存在' }); } const existingEmail = await database.local.query('SELECT id FROM users WHERE email = ?', [email]); if (existingEmail.length > 0) { return res.status(400).json({ error: '邮箱已存在' }); } const hashedPassword = await bcrypt.hash(password, 10); const result = await database.local.insert('users', { username, password_hash: hashedPassword, name, email, department_id: departmentId || null, role: role || 'user', status: 'active', auth_source: 'local', mobile: mobile || null, job_title: jobTitle || null, }); res.status(201).json({ id: result.insertId, username, name, email, role: role || 'user', status: 'active', authSource: 'local', mobile: mobile || null, jobTitle: jobTitle || null, }); } catch (error) { console.error('创建用户失败:', error); res.status(500).json({ error: '创建用户失败' }); } }); // 更新用户 router.put('/users/:id', async (req, res) => { try { const { name, email, departmentId, role, status, mobile, jobTitle } = req.body; const targetUser = await getManagedUser(req.params.id); if (!targetUser) { return res.status(404).json({ error: '用户不存在' }); } const existingEmail = await database.local.query( 'SELECT id FROM users WHERE email = ? AND id <> ? LIMIT 1', [email, req.params.id] ); if (existingEmail.length > 0) { return res.status(400).json({ error: '邮箱已存在' }); } const roleCheck = await ensureAdminRoleChangeAllowed(req, targetUser, role); if (!roleCheck.allowed) { return res.status(roleCheck.status).json({ error: roleCheck.error }); } await database.local.update('users', req.params.id, { name, email, department_id: departmentId || null, role, status, mobile: mobile || null, job_title: jobTitle || null, }); res.json({ id: req.params.id, name, email, role, status, mobile: mobile || null, jobTitle: jobTitle || null }); } catch (error) { console.error('更新用户失败:', error); res.status(500).json({ error: '更新用户失败' }); } }); router.patch('/users/:id/admin-role', async (req, res) => { try { const { isAdmin } = req.body || {}; if (typeof isAdmin !== 'boolean') { return res.status(400).json({ error: '请提供是否授予管理员权限' }); } const targetUser = await getManagedUser(req.params.id); if (!targetUser) { return res.status(404).json({ error: '用户不存在' }); } const nextRole = isAdmin ? 'admin' : 'user'; const roleCheck = await ensureAdminRoleChangeAllowed(req, targetUser, nextRole); if (!roleCheck.allowed) { return res.status(roleCheck.status).json({ error: roleCheck.error }); } if (targetUser.role !== nextRole) { await database.local.update('users', req.params.id, { role: nextRole, }); } res.json({ id: Number(req.params.id), role: nextRole, message: isAdmin ? '已授予管理员权限' : '已取消管理员权限', }); } catch (error) { console.error('更新管理员权限失败:', error); res.status(500).json({ error: '更新管理员权限失败' }); } }); // 删除用户 router.delete('/users/:id', async (req, res) => { try { const targetUser = await getManagedUser(req.params.id); if (!targetUser) { return res.status(404).json({ error: '用户不存在' }); } if (String(req.user.sub) === String(targetUser.id)) { return res.status(400).json({ error: '不能删除当前登录账号' }); } const roleCheck = await ensureAdminRoleChangeAllowed(req, targetUser, 'user'); if (!roleCheck.allowed) { return res.status(roleCheck.status).json({ error: roleCheck.error }); } await database.local.query('DELETE FROM user_page_permissions WHERE user_id = ?', [req.params.id]); await database.local.query('DELETE FROM users WHERE id = ?', [req.params.id]); res.status(204).send(); } catch (error) { console.error('删除用户失败:', error); res.status(500).json({ error: '删除用户失败' }); } }); // 重置密码 router.post('/users/:id/reset-password', async (req, res) => { try { const { newPassword } = req.body; const hashedPassword = await bcrypt.hash(newPassword, 10); await database.local.update('users', req.params.id, { password_hash: hashedPassword }); res.json({ message: '密码已重置' }); } catch (error) { console.error('重置密码失败:', error); res.status(500).json({ error: '重置密码失败' }); } }); // ==================== // Backfill Jobs // ==================== router.post('/backfill/start', async (req, res) => { try { const { startDate, endDate } = req.body || {}; if (!startDate || !endDate) { return res.status(400).json({ success: false, error: 'Missing startDate or endDate' }); } const start = dayjs(startDate); const end = dayjs(endDate); if (!start.isValid() || !end.isValid() || end.isBefore(start)) { return res.status(400).json({ success: false, error: 'Invalid date range' }); } const hasRunning = Array.from(backfillJobs.values()).some(job => job.status === 'running'); if (hasRunning) { return res.status(400).json({ success: false, error: 'Another backfill job is running' }); } const jobId = uuidv4(); const totalDays = end.diff(start, 'day') + 1; const job = { id: jobId, startDate: start.format('YYYY-MM-DD'), endDate: end.format('YYYY-MM-DD'), totalDays, completedDays: 0, currentDate: null, currentTask: 'Waiting', currentStep: 0, totalStepsPerDay: 21, totalTasks: totalDays * 21, completedTasks: 0, remainingTasks: totalDays * 21, remainingDays: totalDays, status: 'running', error: null, startedAt: new Date().toISOString(), finishedAt: null, }; backfillJobs.set(jobId, job); setImmediate(() => runBackfill(job)); return res.json({ success: true, jobId, job }); } catch (error) { return res.status(500).json({ success: false, error: error.message }); } }); router.get('/backfill/status', async (req, res) => { try { const { jobId } = req.query || {}; if (!jobId) { return res.status(400).json({ success: false, error: 'Missing jobId' }); } const job = backfillJobs.get(jobId); if (!job) { return res.status(404).json({ success: false, error: 'Job not found or expired' }); } const percent = job.totalDays > 0 ? Number(((job.completedDays / job.totalDays) * 100).toFixed(1)) : 0; return res.json({ success: true, job: { ...job, percent, }, }); } catch (error) { return res.status(500).json({ success: false, error: error.message }); } }); router.get('/departments', async (req, res) => { try { const departments = await database.local.query( 'SELECT id, name, code, parent_id as parentId, sort, status, created_at, source, external_id as externalId FROM departments ORDER BY sort ASC, id ASC' ); res.json(departments); } catch (error) { console.error('获取部门列表失败:', error); res.status(500).json({ error: '获取部门列表失败' }); } }); router.get('/departments/:id/page-permissions', async (req, res) => { try { const department = await database.local.query('SELECT id, name FROM departments WHERE id = ? LIMIT 1', [req.params.id]); if (department.length === 0) { return res.status(404).json({ error: '部门不存在' }); } const permissions = await getDepartmentPagePermissions(req.params.id); res.json({ department: department[0], permissions, }); } catch (error) { console.error('获取部门页面权限失败:', error); res.status(500).json({ error: '获取部门页面权限失败' }); } }); router.put('/departments/:id/page-permissions', async (req, res) => { try { const department = await database.local.query('SELECT id, name FROM departments WHERE id = ? LIMIT 1', [req.params.id]); if (department.length === 0) { return res.status(404).json({ error: '部门不存在' }); } const permissions = Array.isArray(req.body?.permissions) ? req.body.permissions : []; const result = await saveDepartmentPagePermissions(req.params.id, permissions); res.json({ message: '部门功能页面权限已更新', department: department[0], permissions: result, }); } catch (error) { console.error('更新部门页面权限失败:', error); res.status(500).json({ error: '更新部门页面权限失败' }); } }); // 创建部门 router.post('/departments', async (req, res) => { try { const { name, code, parentId, sort } = req.body; const result = await database.local.insert('departments', { name, code, parent_id: parentId || null, sort: sort || 0, status: 'active', source: 'local', external_id: null, }); res.status(201).json({ id: result.insertId, name, code, parentId, sort, status: 'active' }); } catch (error) { console.error('创建部门失败:', error); res.status(500).json({ error: '创建部门失败' }); } }); // 更新部门 router.put('/departments/:id', async (req, res) => { try { const { name, code, parentId, sort, status } = req.body; await database.local.update('departments', req.params.id, { name, code, parent_id: parentId || null, sort, status, }); res.json({ id: req.params.id, name, code, parentId, sort, status }); } catch (error) { console.error('更新部门失败:', error); res.status(500).json({ error: '更新部门失败' }); } }); // 删除部门 router.delete('/departments/:id', async (req, res) => { try { // 检查是否有子部门 const children = await database.local.query( 'SELECT id FROM departments WHERE parent_id = ?', [req.params.id] ); if (children.length > 0) { return res.status(400).json({ error: '请先删除子部门' }); } await database.local.query('DELETE FROM departments WHERE id = ?', [req.params.id]); res.status(204).send(); } catch (error) { console.error('删除部门失败:', error); res.status(500).json({ error: '删除部门失败' }); } }); // ==================== 权限管理 ==================== router.get('/auth-config', async (_req, res) => { try { const config = await getAuthConfig(); res.json(config); } catch (error) { console.error('获取认证配置失败:', error); res.status(500).json({ error: '获取认证配置失败' }); } }); router.put('/auth-config', async (req, res) => { try { const config = await saveAuthConfig(req.body || {}); res.json(config); } catch (error) { console.error('保存认证配置失败:', error); res.status(500).json({ error: '保存认证配置失败' }); } }); router.post('/auth-config/sync-dingtalk', async (_req, res) => { try { const result = await syncDingTalkUsers(); res.json({ message: '钉钉用户同步完成', ...result, }); } catch (error) { console.error('同步钉钉用户失败:', error); res.status(500).json({ error: error.message || '同步钉钉用户失败' }); } }); router.get('/sidebar-pages', async (_req, res) => { res.json(SIDEBAR_PAGES); }); // 获取权限列表 router.get('/permissions', async (req, res) => { try { const permissions = await getSidebarRolePermissions(); res.json(permissions); } catch (error) { console.error('获取权限列表失败:', error); res.status(500).json({ error: '获取权限列表失败' }); } }); // 更新权限 router.put('/permissions/:page/:role', async (req, res) => { try { const { page, role } = req.params; if (!SIDEBAR_PAGES.some((item) => item.pageKey === page) || !['admin', 'analyst', 'user', 'guest'].includes(role)) { return res.status(400).json({ error: '无效的页面或角色' }); } const { allowed } = req.body; // 检查是否存在 const existing = await database.local.query( 'SELECT id FROM permissions WHERE page_key = ? AND role = ?', [page, role] ); if (existing.length > 0) { await database.local.query( 'UPDATE permissions SET allowed = ? WHERE page_key = ? AND role = ?', [allowed ? 1 : 0, page, role] ); } else { await database.local.insert('permissions', { page_key: page, page_name: page, role, allowed: allowed ? 1 : 0, }); } res.json({ message: '权限已更新' }); } catch (error) { console.error('更新权限失败:', error); res.status(500).json({ error: '更新权限失败' }); } }); // ==================== 系统配置 ==================== // 获取系统配置 router.get('/system-config', async (req, res) => { try { const configs = await database.local.query('SELECT config_key, config_value, config_type FROM system_config'); const result = {}; filterPublicSystemConfigs(configs).forEach(c => { if (c.config_type === 'boolean') { result[c.config_key] = c.config_value === 'true' || c.config_value === '1'; } else if (c.config_type === 'number') { result[c.config_key] = Number(c.config_value); } else { result[c.config_key] = c.config_key === 'sidebar_navigation' ? pruneRetiredSidebarConfig(c.config_value) : c.config_value; } }); res.json(result); } catch (error) { console.error('获取系统配置失败:', error); res.status(500).json({ error: '获取系统配置失败' }); } }); // 更新系统配置 router.put('/system-config', async (req, res) => { try { const updates = { ...req.body }; if (Object.keys(updates).some(isRetiredSystemConfigKey)) { return res.status(400).json({ error: '该功能已移除,不能保存旧设置' }); } if (updates.sidebar_navigation !== undefined) { const clean = pruneRetiredSidebarConfig(updates.sidebar_navigation); updates.sidebar_navigation = typeof clean === 'string' ? clean : JSON.stringify(clean); } const protectedKeys = Object.keys(updates || {}).filter(isProtectedSystemConfigKey); if (protectedKeys.length > 0) { return res.status(400).json({ error: '专用服务密钥必须通过对应的安全配置接口更新' }); } for (const [key, value] of Object.entries(updates)) { const configType = typeof value === 'boolean' ? 'boolean' : typeof value === 'number' ? 'number' : 'string'; const configValue = String(value); // 检查是否存在 const existing = await database.local.query('SELECT id FROM system_config WHERE config_key = ?', [key]); if (existing.length > 0) { await database.local.query( 'UPDATE system_config SET config_value = ?, config_type = ? WHERE config_key = ?', [configValue, configType, key] ); } else { await database.local.insert('system_config', { config_key: key, config_value: configValue, config_type: configType, }); } } clearPublicSystemConfigCache(); res.json(updates); } catch (error) { console.error('更新系统配置失败:', error); res.status(500).json({ error: '更新系统配置失败' }); } }); // ==================== 第三方 API Key ==================== router.get('/external-automation-apps', async (_req, res) => { try { const data = await listExternalAutomationApps(); res.setHeader('Cache-Control', 'no-store'); res.json({ success: true, data }); } catch (error) { handleApiKeyError(res, error, '获取自动上架 App 链接失败'); } }); router.post('/external-automation-apps', async (req, res) => { try { const data = await createExternalAutomationApp(req.body || {}, req.user || {}); res.status(201).json({ success: true, message: 'App 链接已创建', data }); } catch (error) { handleApiKeyError(res, error, '创建自动上架 App 链接失败'); } }); router.put('/external-automation-apps/:id', async (req, res) => { try { const data = await updateExternalAutomationApp(req.params.id, req.body || {}, req.user || {}); res.json({ success: true, message: 'App 链接已更新', data }); } catch (error) { handleApiKeyError(res, error, '更新自动上架 App 链接失败'); } }); router.delete('/external-automation-apps/:id', async (req, res) => { try { const data = await deleteExternalAutomationApp(req.params.id); res.json({ success: true, message: 'App 链接已删除', data }); } catch (error) { handleApiKeyError(res, error, '删除自动上架 App 链接失败'); } }); function handleApiKeyError(res, error, fallbackMessage) { console.error(`[AdminAPIKey] ${fallbackMessage}:`, error); res.status(error.statusCode || error.status || 500).json({ success: false, error: error.message || fallbackMessage, }); } router.get('/api-keys', async (_req, res) => { try { const rows = await listSelectionFissionApiKeys(); res.setHeader('Cache-Control', 'no-store'); res.json({ success: true, data: rows }); } catch (error) { handleApiKeyError(res, error, '获取 API Key 列表失败'); } }); router.post('/api-keys', async (req, res) => { try { const data = await createSelectionFissionApiKey(req.body || {}, req.user || {}); res.setHeader('Cache-Control', 'no-store'); res.status(201).json({ success: true, message: 'API Key 已创建,请立即保存完整 Key', data, }); } catch (error) { handleApiKeyError(res, error, '创建 API Key 失败'); } }); router.put('/api-keys/:id', async (req, res) => { try { const data = await updateSelectionFissionApiKey(req.params.id, req.body || {}, req.user || {}); res.setHeader('Cache-Control', 'no-store'); res.json({ success: true, message: data.plainApiKey ? 'API Key 已更新并完成轮换' : 'API Key 已更新', data, }); } catch (error) { handleApiKeyError(res, error, '更新 API Key 失败'); } }); router.delete('/api-keys/:id', async (req, res) => { try { const data = await deleteSelectionFissionApiKey(req.params.id); res.json({ success: true, message: 'API Key 已删除', data, }); } catch (error) { handleApiKeyError(res, error, '删除 API Key 失败'); } }); // ==================== 选品 AI 分析模型 ==================== router.get('/selection-fission-ai-settings', async (_req, res) => { try { res.json(await getSelectionFissionAiAdminSettings()); } catch (error) { console.error('获取选品AI分析模型配置失败:', error); res.status(error.statusCode || error.status || 500).json({ error: error.message || '获取选品AI分析模型配置失败', }); } }); router.put('/selection-fission-ai-settings', async (req, res) => { try { const config = await saveSelectionFissionAiConfig(req.body || {}); res.json({ success: true, data: config, message: '选品AI分析模型配置已保存' }); } catch (error) { console.error('保存选品AI分析模型配置失败:', error); res.status(error.statusCode || error.status || 500).json({ error: error.message || '保存选品AI分析模型配置失败', }); } }); router.post('/selection-fission-ai-settings/test', async (_req, res) => { try { const result = await testSelectionFissionAiConfig(); res.json({ success: true, data: result }); } catch (error) { console.error('测试选品AI分析模型失败:', error); res.status(error.statusCode || error.status || 500).json({ success: false, error: error.message || '测试选品AI分析模型失败', code: error.code || undefined, }); } }); router.post('/selection-fission-ai-settings/image-generation/test', async (_req, res) => { try { const result = await testSelectionImageGenerationConfig(); res.json({ success: true, data: result }); } catch (error) { console.error('测试选品生图服务失败:', error); res.status(error.statusCode || error.status || 500).json({ success: false, error: error.message || '测试选品生图服务失败', code: error.code || undefined, }); } }); // ==================== 以图搜图服务 ==================== // ==================== 商品标签配置(规模标签 / 利润标签 / SABC 映射) ==================== router.get('/product-tag-config', async (_req, res) => { try { res.json({ success: true, data: await getProductTagConfig() }); } catch (error) { console.error('获取商品标签配置失败:', error); res.status(error.statusCode || error.status || 500).json({ success: false, error: error.message || '获取商品标签配置失败', }); } }); router.put('/product-tag-config', async (req, res) => { try { const result = await saveProductTagConfig(req.body || {}); const config = await getProductTagConfig(); res.json({ success: true, message: '商品标签配置已保存到系统数据库', data: { ...config, saved: result }, }); } catch (error) { console.error('保存商品标签配置失败:', error); // 校验类错误回 400,方便前端直接展示原因 const status = /行|重复|不能为空|取值非法|必须小于/.test(error.message || '') ? 400 : (error.statusCode || error.status || 500); res.status(status).json({ success: false, error: error.message || '保存商品标签配置失败', }); } }); // ==================== 1688 专用以图搜图 ==================== // 上传 Logo router.post('/system-config/logo', async (req, res) => { try { const { logo } = req.body; let nextLogo = logo; const s3Config = await getDefaultS3Config(true); const hasObjectStorage = s3Config && s3Config.endpoint && s3Config.bucket && s3Config.accessKeyId && s3Config.secretAccessKey; if (typeof logo === 'string' && logo.startsWith('data:image/') && hasObjectStorage) { const uploadResult = await uploadBase64ImageToDefaultS3(logo, 'logos'); nextLogo = uploadResult.url; } const existing = await database.local.query('SELECT id FROM system_config WHERE config_key = ?', ['logo']); if (existing.length > 0) { await database.local.query('UPDATE system_config SET config_value = ? WHERE config_key = ?', [nextLogo, 'logo']); } else { await database.local.insert('system_config', { config_key: 'logo', config_value: nextLogo, config_type: 'string' }); } clearPublicSystemConfigCache(); res.json({ logo: nextLogo }); } catch (error) { console.error('上传 Logo 失败:', error); res.status(500).json({ error: '上传 Logo 失败' }); } }); // ==================== 数据库配置 ==================== router.get('/system-database', async (_req, res) => { try { res.json(getSystemDbConfig()); } catch (error) { console.error('获取系统数据库配置失败:', error); res.status(500).json({ error: '获取系统数据库配置失败' }); } }); router.put('/system-database', async (req, res) => { try { const { driver = 'mysql', host, port, database: dbName, username, password } = req.body || {}; const config = await saveSystemDbConfig({ driver, host, port: Number(port || 3306), database: dbName, username, password, }); res.json({ message: '系统数据库配置已保存,请重启服务使配置生效', needRestart: true, ...config, }); } catch (error) { console.error('保存系统数据库配置失败:', error); res.status(500).json({ error: '保存系统数据库配置失败' }); } }); router.post('/system-database/test', async (req, res) => { try { const { host, port, database: dbName, username, password } = req.body || {}; const startTime = Date.now(); await testSystemDbConnection({ host, port: Number(port || 3306), database: dbName, username, password, }); res.json({ success: true, message: '系统数据库连接成功', responseTime: Date.now() - startTime, }); } catch (error) { console.error('测试系统数据库连接失败:', error); res.status(400).json({ success: false, message: `连接失败: ${error.message}`, }); } }); router.get('/redis', async (_req, res) => { try { const [storedConfig, effectiveConfig, storedProfiles] = await Promise.all([ getRedisConfig(), getEffectiveRedisConfig(), getStoredRedisConfigs(), ]); res.json({ ...storedConfig, storedProfiles, runtimeProfile: getRedisRuntimeProfile(), runtimeProfiles: getRedisRuntimeProfiles(), effective: effectiveConfig, }); } catch (error) { console.error('获取 Redis 配置失败:', error); res.status(500).json({ error: '获取 Redis 配置失败' }); } }); router.post('/redis/runtime-profile', async (req, res) => { try { const { profile } = req.body || {}; const config = await setRedisRuntimeProfile(profile || 'stored'); await redisRuntime.reload(); res.json({ message: config.enabled ? `Redis 已切换到${config.profileName || config.source || '自动选择 Redis'}` : 'Redis 运行态已关闭', needRestart: false, runtimeProfile: getRedisRuntimeProfile(), effective: config, }); } catch (error) { console.error('切换 Redis 运行态失败:', error); res.status(400).json({ error: error.message || '切换 Redis 运行态失败' }); } }); router.put('/redis', async (req, res) => { try { const { host, port, password, db, profileKey, profile } = req.body || {}; const config = await saveRedisConfig({ profileKey: profileKey || profile, host: typeof host === 'string' ? host.trim() : '', port: Number(port || 6379), password, db: Number(db || 0), }); await redisRuntime.reload(); res.json({ message: config.enabled ? `${config.profileName || 'Redis'}配置已保存到系统库并立即生效` : `${config.profileName || 'Redis'}已在系统库中关闭并立即生效`, needRestart: false, ...config, }); } catch (error) { console.error('保存 Redis 配置失败:', error); res.status(500).json({ error: '保存 Redis 配置失败' }); } }); router.post('/redis/test', async (req, res) => { try { const { host, port, password, db, profileKey, profile } = req.body || {}; const startTime = Date.now(); await testRedisConnection({ profileKey: profileKey || profile, host: typeof host === 'string' ? host.trim() : '', port: Number(port || 6379), password, db: Number(db || 0), }); res.json({ success: true, message: 'Redis 连接成功', responseTime: Date.now() - startTime, }); } catch (error) { console.error('测试 Redis 连接失败:', error); res.status(400).json({ success: false, message: `连接失败: ${error.message}`, }); } }); // ==================== 工单通知系统配置 ==================== router.get('/work-order-config', async (_req, res) => { try { const config = await getWorkOrderConfig(); res.json(config); } catch (error) { console.error('获取工单系统配置失败:', error); res.status(500).json({ error: '获取工单系统配置失败' }); } }); router.put('/work-order-config', async (req, res) => { try { const config = await saveWorkOrderConfig(req.body || {}); res.json({ message: '工单系统配置已保存', needRestart: false, ...config, }); } catch (error) { console.error('保存工单系统配置失败:', error); res.status(400).json({ error: error.message || '保存工单系统配置失败' }); } }); router.post('/work-order-config/test', async (req, res) => { try { const startTime = Date.now(); const result = await testWorkOrderConnection(req.body || {}); res.json({ ...result, responseTime: Date.now() - startTime, }); } catch (error) { console.error('测试工单系统连接失败:', error); res.status(400).json({ success: false, message: `连接失败: ${error.message}`, }); } }); const DEFAULT_DATABASE_CONFIG_NAME = 'cloud_database'; const CLOUD_DATABASE_PUBLIC_CONFIG_NAME = 'cloud_database_public'; const PLANNING_DATABASE_CONFIG_NAME = 'planning_database'; const AI_BUYER_SHOW_DATABASE_CONFIG_NAME = 'ai_buyer_show_database'; const PROTECTED_DATABASE_CONFIG_NAMES = new Set([ DEFAULT_DATABASE_CONFIG_NAME, CLOUD_DATABASE_PUBLIC_CONFIG_NAME, PLANNING_DATABASE_CONFIG_NAME, AI_BUYER_SHOW_DATABASE_CONFIG_NAME, ]); function normalizeDatabaseConfigName(rawName) { const configName = String(rawName || '').trim(); if (!configName) { return DEFAULT_DATABASE_CONFIG_NAME; } if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{1,99}$/.test(configName)) { const error = new Error('数据库配置标识只能使用 2-100 位英文、数字、下划线或短横线'); error.statusCode = 400; throw error; } return configName; } function getDefaultPortByDriver(driver) { if (driver === 'mssql') return 1433; if (driver === 'postgres') return 5432; return 3306; } async function getStoredDatabaseConfig(configName) { const normalizedName = normalizeDatabaseConfigName(configName); let configs = await database.local.query( 'SELECT id, name, driver, host, port, database_name as `database`, username, `ssl`, use_env_config, is_default, status FROM database_configs WHERE name = ? LIMIT 1', [normalizedName] ); // 兼容旧数据:默认业务数据库历史上只靠 is_default 标记,没有固定 name。 if (configs.length === 0 && normalizedName === DEFAULT_DATABASE_CONFIG_NAME) { configs = await database.local.query( 'SELECT id, name, driver, host, port, database_name as `database`, username, `ssl`, use_env_config, is_default, status FROM database_configs WHERE is_default = 1 LIMIT 1' ); } return configs[0] || null; } async function getStoredDatabaseCredentials(configName) { const normalizedName = normalizeDatabaseConfigName(configName); const configs = await database.local.query( 'SELECT driver, host, port, database_name as `database`, username, password, `ssl` FROM database_configs WHERE name = ? LIMIT 1', [normalizedName] ); return configs[0] || null; } function getBusinessEnvDatabasePayload(configName) { const normalizedName = normalizeDatabaseConfigName(configName); if (![DEFAULT_DATABASE_CONFIG_NAME, CLOUD_DATABASE_PUBLIC_CONFIG_NAME].includes(normalizedName)) { return {}; } const summary = cloudDb.getEnvBusinessDatabaseSummary( process.env, normalizedName === CLOUD_DATABASE_PUBLIC_CONFIG_NAME ? 'public' : 'default' ); return { envConfigAvailable: summary.available, envConfig: summary, }; } // 获取全部业务及扩展数据库配置(不返回密码) router.get('/databases', async (_req, res) => { try { const configs = await database.local.query( 'SELECT id, name, driver, host, port, database_name as `database`, username, `ssl`, use_env_config, is_default, status, updated_at FROM database_configs ORDER BY is_default DESC, id ASC' ); res.json(configs.map((config) => ({ ...config, ssl: config.ssl === 1 || config.ssl === true, useEnvConfig: config.use_env_config === 1 || config.use_env_config === true, protected: PROTECTED_DATABASE_CONFIG_NAMES.has(config.name) || Boolean(config.is_default), use_env_config: undefined, }))); } catch (error) { console.error('获取数据库配置列表失败:', error); res.status(500).json({ error: '获取数据库配置列表失败' }); } }); router.delete('/databases/:id', async (req, res) => { try { const id = Number(req.params.id); if (!Number.isInteger(id) || id <= 0) { return res.status(400).json({ error: '数据库配置 ID 无效' }); } const configs = await database.local.query( 'SELECT id, name, is_default FROM database_configs WHERE id = ? LIMIT 1', [id] ); const config = configs[0]; if (!config) { return res.status(404).json({ error: '数据库配置不存在' }); } if (config.is_default === 1 || PROTECTED_DATABASE_CONFIG_NAMES.has(config.name)) { return res.status(400).json({ error: '核心数据库配置不能删除' }); } await database.local.query('DELETE FROM database_configs WHERE id = ?', [id]); res.json({ message: '数据库配置已删除' }); } catch (error) { console.error('删除数据库配置失败:', error); res.status(500).json({ error: '删除数据库配置失败' }); } }); // 获取单个数据库配置 router.get('/database', async (req, res) => { try { const configName = normalizeDatabaseConfigName(req.query.name); const config = await getStoredDatabaseConfig(configName); if (config) { // 转换 ssl 为布尔值 config.ssl = config.ssl === 1; config.useEnvConfig = config.use_env_config === 1 || config.use_env_config === true; delete config.use_env_config; config.name = config.name || configName; res.json({ ...config, ...getBusinessEnvDatabasePayload(configName), }); } else { res.json({ name: configName, driver: 'mysql', port: 3306, ssl: false, useEnvConfig: false, ...getBusinessEnvDatabasePayload(configName), }); } } catch (error) { console.error('获取数据库配置失败:', error); res.status(error.statusCode || 500).json({ error: error.statusCode ? error.message : '获取数据库配置失败' }); } }); // 更新数据库配置 router.put('/database', async (req, res) => { try { const { name, driver, host, port, database: dbName, username, password, ssl, useEnvConfig, status } = req.body; const configName = normalizeDatabaseConfigName(name); const isDefaultConfig = configName === DEFAULT_DATABASE_CONFIG_NAME; const isBusinessConfig = configName === DEFAULT_DATABASE_CONFIG_NAME || configName === CLOUD_DATABASE_PUBLIC_CONFIG_NAME; const existingConfig = await getStoredDatabaseConfig(configName); const envProfile = configName === CLOUD_DATABASE_PUBLIC_CONFIG_NAME ? 'public' : 'default'; const envSummary = cloudDb.getEnvBusinessDatabaseSummary(process.env, envProfile); if (isBusinessConfig && useEnvConfig && !envSummary.available) { return res.status(400).json({ error: configName === CLOUD_DATABASE_PUBLIC_CONFIG_NAME ? '当前 .env 公网业务数据库配置不完整,请先配置 CLOUD_DB_PUBLIC_HOST / CLOUD_DB_PUBLIC_NAME / CLOUD_DB_PUBLIC_USER' : '当前 .env 业务数据库配置不完整,请先配置 CLOUD_DB_HOST / CLOUD_DB_NAME / CLOUD_DB_USER', }); } // 根据数据库类型设置默认端口 const resolvedDriver = driver || existingConfig?.driver || (useEnvConfig ? envSummary.driver : 'mysql'); const defaultPort = getDefaultPortByDriver(resolvedDriver); const configData = { name: configName, driver: resolvedDriver, host: host || existingConfig?.host || (useEnvConfig ? envSummary.host : ''), port: port || existingConfig?.port || (useEnvConfig ? envSummary.port : defaultPort), database_name: dbName || existingConfig?.database || (useEnvConfig ? envSummary.database : ''), username: username || existingConfig?.username || (useEnvConfig ? envSummary.username : ''), ssl: ssl ? 1 : 0, use_env_config: isBusinessConfig && useEnvConfig ? 1 : 0, is_default: isDefaultConfig ? 1 : 0, status: status === 'inactive' && !isDefaultConfig ? 'inactive' : 'active', }; // 只有密码不为空时才更新密码 if (password) { configData.password = password; } if (existingConfig) { await database.local.update('database_configs', existingConfig.id, configData); } else { configData.password = password || ''; await database.local.insert('database_configs', configData); } if (isDefaultConfig) { await database.local.query( 'UPDATE database_configs SET is_default = 0 WHERE name != ? AND is_default = 1', [configName] ); } if (isBusinessConfig) { await cloudDb.close(); } res.json({ message: '配置已保存,业务数据库连接已重新加载', needRestart: false, name: configName, ...configData }); } catch (error) { console.error('更新数据库配置失败:', error); res.status(error.statusCode || 500).json({ error: error.statusCode ? error.message : '更新数据库配置失败' }); } }); // 测试数据库连接 router.post('/database/test', async (req, res) => { let { name, driver, host, port, database: dbName, username, password, ssl, useEnvConfig } = req.body; try { const startTime = Date.now(); const configName = normalizeDatabaseConfigName(name); const envProfile = configName === CLOUD_DATABASE_PUBLIC_CONFIG_NAME ? 'public' : 'default'; if (useEnvConfig) { const envConfig = cloudDb.getEnvBusinessDatabaseConfig(process.env, envProfile); if (!cloudDb.hasCompleteEnvConfig(process.env, envProfile)) { return res.status(400).json({ success: false, error: configName === CLOUD_DATABASE_PUBLIC_CONFIG_NAME ? '当前 .env 公网业务数据库配置不完整,请先配置 CLOUD_DB_PUBLIC_HOST / CLOUD_DB_PUBLIC_NAME / CLOUD_DB_PUBLIC_USER' : '当前 .env 业务数据库配置不完整,请先配置 CLOUD_DB_HOST / CLOUD_DB_NAME / CLOUD_DB_USER', message: configName === CLOUD_DATABASE_PUBLIC_CONFIG_NAME ? '当前 .env 公网业务数据库配置不完整,请先配置 CLOUD_DB_PUBLIC_HOST / CLOUD_DB_PUBLIC_NAME / CLOUD_DB_PUBLIC_USER' : '当前 .env 业务数据库配置不完整,请先配置 CLOUD_DB_HOST / CLOUD_DB_NAME / CLOUD_DB_USER', }); } driver = envConfig.driver || 'mssql'; host = envConfig.host; port = envConfig.port; dbName = envConfig.database; username = envConfig.user; password = envConfig.password; ssl = envConfig.ssl; } else { const storedConfig = await getStoredDatabaseCredentials(configName); driver = driver || storedConfig?.driver; host = host || storedConfig?.host; port = port || storedConfig?.port; dbName = dbName || storedConfig?.database; username = username || storedConfig?.username; password = password || storedConfig?.password; ssl = typeof ssl === 'boolean' ? ssl : Boolean(storedConfig?.ssl); } if (!host || !dbName || !username || !password) { return res.status(400).json({ success: false, error: '请完整填写数据库连接信息' }); } if (driver === 'mssql') { // SQL Server 连接测试 const sql = require('mssql'); const config = { server: host, port: port || 1433, database: dbName, user: username, password, options: { encrypt: ssl || false, trustServerCertificate: true, }, connectionTimeout: 10000, }; const pool = await sql.connect(config); await pool.request().query('SELECT 1 as test'); await pool.close(); } else { // MySQL 连接测试 const mysql = require('mysql2/promise'); const connection = await mysql.createConnection({ host, port: port || 3306, user: username, password, database: dbName, connectTimeout: 10000, ssl: ssl ? { rejectUnauthorized: false } : false, }); await connection.ping(); await connection.end(); } const responseTime = Date.now() - startTime; res.json({ success: true, message: '数据库连接成功', responseTime, }); } catch (error) { console.error('数据库连接测试失败:', error); res.status(400).json({ success: false, error: `连接失败: ${error.message}`, message: `连接失败: ${error.message}`, }); } }); // ==================== S3 配置 ==================== // 获取 S3 配置 router.get('/s3', async (req, res) => { try { const config = await getDefaultS3Config(true); res.setHeader('Cache-Control', 'no-store'); if (config) { const { secretAccessKey, ...publicConfig } = config; res.json({ ...publicConfig, hasSecret: Boolean(secretAccessKey), }); } else { res.json({ provider: 'minio', region: 'auto', protocol: 'https', storageClass: 'STANDARD', hasSecret: false, }); } } catch (error) { console.error('获取 S3 配置失败:', error); res.status(500).json({ error: '获取 S3 配置失败' }); } }); // 更新 S3 配置 router.put('/s3', async (req, res) => { try { await ensureObjectStorageSchema(); const { provider, endpoint, region, zone, protocol, storageClass, bucket, accessKeyId, secretAccessKey, customDomain, } = req.body || {}; const normalizedProvider = String(provider || '').trim().toLowerCase(); const normalizedProtocol = normalizeProtocol(protocol, endpoint); const normalizedEndpoint = normalizeEndpoint(endpoint, normalizedProtocol); const normalizedBucket = String(bucket || '').trim(); const normalizedAccessKeyId = String(accessKeyId || '').trim(); const existing = await getDefaultS3Config(true); const nextSecret = String(secretAccessKey || '').trim() || existing?.secretAccessKey || ''; if (!normalizedProvider || !normalizedEndpoint || !normalizedBucket || !normalizedAccessKeyId || !nextSecret) { return res.status(400).json({ error: '请完整填写提供商、Endpoint、Bucket、Access Key 和 Secret Key', }); } const configData = { provider: normalizedProvider, endpoint: normalizedEndpoint, region: String(region || 'auto').trim() || 'auto', zone: String(zone || '').trim() || null, protocol: normalizedProtocol, storage_class: normalizeStorageClass(storageClass), bucket: normalizedBucket, access_key_id: normalizedAccessKeyId, secret_access_key: nextSecret, custom_domain: normalizePublicDomain(customDomain, normalizedProtocol) || null, is_default: 1, }; if (existing) { await database.local.update('s3_configs', existing.id, configData); } else { await database.local.insert('s3_configs', configData); } res.setHeader('Cache-Control', 'no-store'); res.json({ message: '对象存储配置已更新', data: { provider: configData.provider, endpoint: configData.endpoint, region: configData.region, zone: configData.zone, protocol: configData.protocol, storageClass: configData.storage_class, bucket: configData.bucket, accessKeyId: configData.access_key_id, customDomain: configData.custom_domain, hasSecret: true, }, }); } catch (error) { console.error('更新 S3 配置失败:', error); res.status(500).json({ error: '更新 S3 配置失败' }); } }); // 测试 S3 连接 router.post('/s3/test', async (req, res) => { try { const input = req.body || {}; const existing = await getDefaultS3Config(true); const config = { provider: input.provider || existing?.provider, endpoint: input.endpoint || existing?.endpoint, region: input.region || existing?.region || 'auto', zone: input.zone || existing?.zone || '', protocol: input.protocol || existing?.protocol, storageClass: input.storageClass || existing?.storageClass || 'STANDARD', bucket: input.bucket || existing?.bucket, accessKeyId: input.accessKeyId || existing?.accessKeyId, secretAccessKey: input.secretAccessKey || existing?.secretAccessKey, }; const result = await testObjectStorageConnection(config); res.json({ success: true, message: '对象存储连接成功', data: result, }); } catch (error) { console.error('S3 连接测试失败:', error); res.status(400).json({ success: false, message: `连接失败: ${error.message}`, }); } }); // ==================== KPI目标值配置 ==================== // 获取所有页面列表(用于下拉选择) router.get('/kpi-targets/pages', async (req, res) => { try { const pages = await database.local.query( 'SELECT DISTINCT page_code as pageCode FROM sys_kpi_targets ORDER BY page_code' ); // 页面名称映射 const pageNames = { customerService: '客服分析', warehouse: '仓库分析', logistics: '快递分析', supplier: '供应商分析', procurement: '采购分析', productDesign: '开发分析', region: '地区分析', summary: '监控大厅', }; const result = pages.map(p => ({ code: p.pageCode, name: pageNames[p.pageCode] || p.pageCode, })); res.json({ success: true, data: result }); } catch (error) { console.error('获取页面列表失败:', error); res.status(500).json({ success: false, error: '获取页面列表失败' }); } }); // 获取指定页面的KPI目标值 router.get('/kpi-targets/:pageCode', async (req, res) => { try { const { pageCode } = req.params; const targets = await database.local.query( `SELECT id, page_code as pageCode, kpi_code as kpiCode, kpi_name as kpiName, unit, direction, target_good as targetGood, target_pass as targetPass, target_warn as targetWarn, is_enabled as isEnabled, sort_order as sortOrder, remark FROM sys_kpi_targets WHERE page_code = ? AND is_enabled = 1 ORDER BY sort_order`, [pageCode] ); res.json({ success: true, data: targets }); } catch (error) { console.error('获取KPI目标值失败:', error); res.status(500).json({ success: false, error: '获取KPI目标值失败' }); } }); // 获取所有KPI目标值(管理界面用) router.get('/kpi-targets', async (req, res) => { try { const targets = await database.local.query( `SELECT id, page_code as pageCode, kpi_code as kpiCode, kpi_name as kpiName, unit, direction, target_good as targetGood, target_pass as targetPass, target_warn as targetWarn, is_enabled as isEnabled, sort_order as sortOrder, remark FROM sys_kpi_targets ORDER BY page_code, sort_order` ); res.json({ success: true, data: targets }); } catch (error) { console.error('获取所有KPI目标值失败:', error); res.status(500).json({ success: false, error: '获取所有KPI目标值失败' }); } }); // 更新KPI目标值 router.put('/kpi-targets/:id', async (req, res) => { try { const { id } = req.params; const { kpiName, unit, direction, targetGood, targetPass, targetWarn, isEnabled, remark } = req.body; await database.local.query( `UPDATE sys_kpi_targets SET kpi_name = ?, unit = ?, direction = ?, target_good = ?, target_pass = ?, target_warn = ?, is_enabled = ?, remark = ?, updated_at = NOW() WHERE id = ?`, [kpiName, unit, direction, targetGood, targetPass, targetWarn, isEnabled ? 1 : 0, remark || '', id] ); res.json({ success: true, message: '更新成功' }); } catch (error) { console.error('更新KPI目标值失败:', error); res.status(500).json({ success: false, error: '更新KPI目标值失败' }); } }); // 新增KPI指标 router.post('/kpi-targets', async (req, res) => { try { const { pageCode, kpiCode, kpiName, unit, direction, targetGood, targetPass, targetWarn, sortOrder } = req.body; const result = await database.local.insert('sys_kpi_targets', { page_code: pageCode, kpi_code: kpiCode, kpi_name: kpiName, unit: unit || '', direction: direction || 'lower', target_good: targetGood, target_pass: targetPass, target_warn: targetWarn, sort_order: sortOrder || 0, is_enabled: 1, }); res.status(201).json({ success: true, id: result.insertId, message: '新增成功' }); } catch (error) { console.error('新增KPI指标失败:', error); res.status(500).json({ success: false, error: '新增KPI指标失败' }); } }); // 删除KPI指标 router.delete('/kpi-targets/:id', async (req, res) => { try { const { id } = req.params; await database.local.query('DELETE FROM sys_kpi_targets WHERE id = ?', [id]); res.json({ success: true, message: '删除成功' }); } catch (error) { console.error('删除KPI指标失败:', error); res.status(500).json({ success: false, error: '删除KPI指标失败' }); } }); router.post('/ai-image-features/upsert', async (req, res) => { try { const records = Array.isArray(req.body?.records) ? req.body.records : (req.body ? [req.body] : []); if (records.length === 0) { return res.status(400).json({ error: 'records 不能为空' }); } const results = await batchUpsertAiImageFeatures(records); res.json({ success: true, message: `已写入系统库,共 ${results.length} 条`, count: results.length, data: results, }); } catch (error) { console.error('写入 AI 图片标签与向量失败:', error); res.status(500).json({ error: error.message || '写入 AI 图片标签与向量失败' }); } }); module.exports = router;