const express = require('express'); const XLSX = require('xlsx'); const database = require('../config/database'); const { getCompanySkusBySpu, fetchFreightProfile, getSelectionFissionProfitPricingContext, getSelectionFissionProductCodeMeta, saveSelectionFissionProfitPricingTable, } = require('../services/planningSelectionFissionService'); const router = express.Router(); const HISTORY_TABLE = 'pricing_calculator_history'; // 运费/售后画像查询设定超时,避免整页等待。 const FREIGHT_PROFILE_TIMEOUT_MS = Math.max( Number.parseInt(process.env.PRICING_CALC_FREIGHT_TIMEOUT_MS || '30000', 10) || 30000, 5000 ); const MAX_HISTORY_ROWS = 500; const MAX_PAYLOAD_BYTES = 1024 * 1024; const CALCULATOR_TYPES = new Set(['company', 'alibaba1688']); let historyTableReady = null; async function ensureHistoryTable() { if (!historyTableReady) { historyTableReady = (async () => { // 先查表是否存在:生产/隧道账号通常只有 SELECT/INSERT/UPDATE/DELETE, // MariaDB 对 CREATE TABLE IF NOT EXISTS 也会校验 CREATE 权限,存在时直接跳过。 const existing = await database.local.query( `SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ? LIMIT 1`, [HISTORY_TABLE] ); if (existing.length > 0) { return; } await database.local.query( `CREATE TABLE IF NOT EXISTS ${HISTORY_TABLE} ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', user_id BIGINT NOT NULL COMMENT '测算人用户ID', user_name VARCHAR(100) NULL COMMENT '测算人显示名', spu_code VARCHAR(160) NOT NULL COMMENT '公司SPU', brand VARCHAR(100) NULL COMMENT '品牌名称', note VARCHAR(500) NULL COMMENT '备注', row_count INT NOT NULL DEFAULT 0 COMMENT '测算行数', payload_json LONGTEXT NOT NULL COMMENT '完整测算快照 JSON', created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', INDEX idx_pricing_calc_history_user_created (user_id, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='上架测算历史记录'` ); })(); } try { await historyTableReady; } catch (error) { // 建表失败后允许下次请求重试 historyTableReady = null; throw error; } } function sendError(res, error, fallbackMessage) { const status = error.status || error.statusCode || 500; res.status(status).json({ success: false, error: error.message || fallbackMessage, }); } function getRequestUser(req) { const user = req.user || {}; return { id: Number(user.sub || user.id || 0) || null, name: user.name || user.username || '', isAdmin: user.role === 'admin', }; } function requireUserId(req, res) { const { id } = getRequestUser(req); if (!id) { res.status(401).json({ success: false, error: '当前登录状态无效,请重新登录' }); return null; } return id; } const normalizeText = (value, maxLength) => String(value ?? '').trim().slice(0, maxLength); const normalizeCalculatorType = (value) => ( CALCULATOR_TYPES.has(value) ? value : 'company' ); function sanitizeHistoryProduct(input) { const product = input && typeof input === 'object' ? input : {}; const dimensions = (Array.isArray(product.skuDimensions) ? product.skuDimensions : []) .slice(0, 4) .map((dimension) => ({ name: normalizeText(dimension?.name, 120), values: (Array.isArray(dimension?.values) ? dimension.values : []) .slice(0, 500) .map((value) => normalizeText(value, 500)) .filter(Boolean), })) .filter((dimension) => dimension.name); return { productId: normalizeText(product.productId, 80), title: normalizeText(product.title, 500), mainImage: normalizeText(product.mainImage, 2000), sourceUrl: normalizeText(product.sourceUrl, 2000), storeName: normalizeText(product.storeName, 300), category: normalizeText(product.category, 500), skuDimensions: dimensions, }; } function mapSkuRow(row) { const qgyf = Number(row?.raw?.qgyf); return { skuCode: row.skuCode || '', status: row.saleStatus || '', stock: row.stock ?? null, stockWarehouses: Array.isArray(row.stockWarehouses) ? row.stockWarehouses : [], name: row.skuName || row.productName || '', image: row.skuImageUrl || '', size: row.size || '', color: row.colorName || '', cost: row.cost ?? null, minimumPrice: row.minimumPrice ?? null, salePrice: row?.raw?.sale_price ?? row?.raw?.salePrice ?? null, // qgyf = 聚水潭接口返回的全国平均运费(按条码) freight: Number.isFinite(qgyf) ? qgyf : null, supplier: row.supplierName || '', category: row.categoryName || '', lengthCm: row.lengthCm ?? row?.raw?.length_cm ?? row?.raw?.length ?? null, widthCm: row.widthCm ?? row?.raw?.width_cm ?? row?.raw?.width ?? null, heightCm: row.heightCm ?? row?.raw?.height_cm ?? row?.raw?.height ?? null, weight: row.weight ?? row?.raw?.weight ?? row?.raw?.gross_weight ?? null, }; } async function fetchFreightProfileBestEffort(spuCode, skuRows, user) { const emptyProfile = { nationalFreight: null, zone1Freight: null, zone2Freight: null, zone3Freight: null, zone4Freight: null, aftersaleRatio: null, minimumPricesBySku: {}, raw: null, }; try { const result = await Promise.race([ fetchFreightProfile({ spuCode, skuRows }, user), new Promise((resolve) => { setTimeout(() => resolve({ ...emptyProfile, warning: '运费/售后信息查询超时,可手动填写运费' }), FREIGHT_PROFILE_TIMEOUT_MS); }), ]); return result && typeof result === 'object' ? { ...emptyProfile, ...result } : emptyProfile; } catch (error) { return { ...emptyProfile, warning: error.message || '运费/售后信息查询失败' }; } } // 拉取 SPU 下全部条码(含销售、退市、停产)+ 运费/售后画像(尽力而为) // 运费由聚水潭接口 qgyf 字段按条码直出, // 默认跳过;如需售后占比/最低限价画像,可设 PRICING_CALC_FREIGHT_PROFILE=on 重新开启。 const FREIGHT_PROFILE_ENABLED = String(process.env.PRICING_CALC_FREIGHT_PROFILE || 'off').toLowerCase() === 'on'; function getSelectionSkuCodeSet(selectionContext) { if (!selectionContext) return null; const selectedSkuCodes = Array.isArray(selectionContext.skuSceneImages?.selectedSkuCodes) ? selectionContext.skuSceneImages.selectedSkuCodes : []; const fallbackSkuCodes = Array.isArray(selectionContext.rows) ? selectionContext.rows.map((row) => row.skuCode) : []; return new Set( (selectedSkuCodes.length > 0 ? selectedSkuCodes : fallbackSkuCodes) .map((skuCode) => normalizeText(skuCode, 160).toUpperCase()) .filter(Boolean) ); } function filterSkuRowsForSelectionContext(skuRows, selectionContext) { const rows = Array.isArray(skuRows) ? skuRows : []; const selectedSkuCodes = getSelectionSkuCodeSet(selectionContext); if (!selectedSkuCodes) return rows; return rows.filter((row) => selectedSkuCodes.has( normalizeText(row?.skuCode || row?.sku_code || row?.barcode, 160).toUpperCase() )); } router.get('/product-code/meta', async (req, res) => { const userId = requireUserId(req, res); if (!userId) return; try { res.json({ success: true, data: await getSelectionFissionProductCodeMeta() }); } catch (error) { sendError(res, error, '公司SKU编码配置加载失败'); } }); router.get('/spu/:spuCode', async (req, res) => { const spuCode = normalizeText(req.params.spuCode, 160).toUpperCase(); if (!spuCode) { return res.status(400).json({ success: false, error: '请输入公司 SPU' }); } try { const planId = Number.parseInt(req.query.planId, 10) || 0; const itemId = Number.parseInt(req.query.itemId, 10) || 0; if (Boolean(planId) !== Boolean(itemId)) { return res.status(400).json({ success: false, error: '选品定价上下文缺少方案ID或SPU任务ID' }); } const skuResult = await getCompanySkusBySpu(spuCode, { includeInactive: true }); const allSkuRows = Array.from(skuResult.rows || []); if (allSkuRows.length === 0) { return res.status(404).json({ success: false, error: `未查询到 SPU「${spuCode}」的条码,请确认 SPU 是否正确`, }); } let selectionContext = null; if (planId && itemId) { selectionContext = await getSelectionFissionProfitPricingContext(planId, itemId); if (selectionContext.spuCode.toUpperCase() !== spuCode) { return res.status(409).json({ success: false, error: `该选品任务绑定的是SPU ${selectionContext.spuCode}` }); } } const skuRows = filterSkuRowsForSelectionContext(allSkuRows, selectionContext); if (selectionContext && skuRows.length === 0) { return res.status(409).json({ success: false, error: '选品任务已选 SKU 不在当前 SPU 条码列表中,请返回 SKU 节点重新确认', }); } const freightProfile = FREIGHT_PROFILE_ENABLED ? await fetchFreightProfileBestEffort(spuCode, skuRows, req.user) : { nationalFreight: null, aftersaleRatio: null, minimumPricesBySku: {}, warning: null }; const warnings = [...(skuResult.warnings || [])]; if (freightProfile.warning) { warnings.push(freightProfile.warning); } const savedRowsBySku = new Map((selectionContext?.rows || []).map((row) => [ String(row.skuCode || '').toUpperCase(), row, ])); res.json({ success: true, data: { spu: spuCode, source: skuResult.source, warnings, freightProfile: { nationalFreight: freightProfile.nationalFreight ?? null, zone1Freight: freightProfile.zone1Freight ?? null, zone2Freight: freightProfile.zone2Freight ?? null, zone3Freight: freightProfile.zone3Freight ?? null, zone4Freight: freightProfile.zone4Freight ?? null, aftersaleRatio: freightProfile.aftersaleRatio ?? null, minimumPricesBySku: freightProfile.minimumPricesBySku || {}, }, selectionContext: selectionContext ? { planId: selectionContext.planId, itemId: selectionContext.itemId, taskCode: selectionContext.taskCode, schemeCode: selectionContext.schemeCode, listedItemId: selectionContext.listedItemId, pricingTableSavedAt: selectionContext.pricingTableSavedAt, pricingTableSavedBy: selectionContext.pricingTableSavedBy, pricingTableVersion: selectionContext.pricingTableVersion, workflowProfile: selectionContext.workflowProfile, productCodeStatus: selectionContext.productCodeStatus, productCodeStyleCode: selectionContext.productCodeStyleCode, productCodeConfig: selectionContext.productCodeConfig, productCodeErrorMessage: selectionContext.productCodeErrorMessage, productCodeRequestedAt: selectionContext.productCodeRequestedAt, productCodeCompletedAt: selectionContext.productCodeCompletedAt, } : null, rows: skuRows.map((skuRow) => { const mapped = mapSkuRow(skuRow); const saved = savedRowsBySku.get(String(mapped.skuCode || '').toUpperCase()); return { ...mapped, savedPricing: saved ? { price: saved.finalPrice, groupPrice: saved.groupPrice, installFee: saved.installationFee, coupon: saved.discountAmount, activityDiscount: saved.activityDiscount, shipping: saved.freightNational, commission: saved.platformCommissionRatio, promoRate: saved.promotionRatio, adFee: saved.marketingAdRatio, seeding: saved.farmingRatio, afterSale: saved.aftersaleRatio, insurance: saved.insuranceRatio, brandFee: saved.brandUsageRatio, targetMargin: saved.operatingProfitRatio, originalSkuName: saved.originalSkuName, selectedSkuName: saved.selectedSkuName, internalProductCode: saved.internalProductCode, productCodeProductName: saved.productCodeProductName, productCodeColorSpec: saved.productCodeColorSpec, } : null, }; }), }, }); } catch (error) { sendError(res, error, 'SPU 条码查询失败,请稍后重试'); } }); // 保存一条测算历史(完整行快照,含输入与公式结果) router.post('/history', async (req, res) => { const userId = requireUserId(req, res); if (!userId) return; const body = req.body || {}; const spuCode = normalizeText(body.spuCode, 160).toUpperCase(); const calculatorType = normalizeCalculatorType(body.calculatorType); const rows = Array.isArray(body.rows) ? body.rows : []; if (!spuCode) { return res.status(400).json({ success: false, error: '缺少 SPU 编码' }); } if (rows.length === 0) { return res.status(400).json({ success: false, error: '测算行为空,无法保存' }); } if (rows.length > MAX_HISTORY_ROWS) { return res.status(400).json({ success: false, error: `单条历史最多保存 ${MAX_HISTORY_ROWS} 行` }); } const payload = { version: 4, calculatorType, spuCode, isBlackLabelShop: body.isBlackLabelShop === true, brand: normalizeText(body.brand, 100), note: normalizeText(body.note, 500), savedAt: new Date().toISOString(), summary: body.summary && typeof body.summary === 'object' ? body.summary : null, freightProfile: body.freightProfile && typeof body.freightProfile === 'object' ? body.freightProfile : null, product: calculatorType === 'alibaba1688' ? sanitizeHistoryProduct(body.product) : null, rows, }; const payloadJson = JSON.stringify(payload); if (Buffer.byteLength(payloadJson, 'utf8') > MAX_PAYLOAD_BYTES) { return res.status(400).json({ success: false, error: '测算快照过大,无法保存' }); } try { let selectionPricing = null; const planId = Number.parseInt(body.selectionContext?.planId || body.planId, 10) || 0; const itemId = Number.parseInt(body.selectionContext?.itemId || body.itemId, 10) || 0; if (Boolean(planId) !== Boolean(itemId)) { return res.status(400).json({ success: false, error: '选品定价上下文缺少方案ID或SPU任务ID' }); } if (planId && itemId) { // 测算页会展示实时接口中的全部条码;自动上架保存用户保留的任务 SKU 子集。 const pricingContext = await getSelectionFissionProfitPricingContext(planId, itemId); const listingSkuCodes = new Set((pricingContext.rows || []).map((row) => ( normalizeText(row.skuCode, 160).toUpperCase() ))); const listingRows = rows.filter((row) => listingSkuCodes.has( normalizeText(row.skuCode || row.sku || row.barcode, 160).toUpperCase() )); selectionPricing = await saveSelectionFissionProfitPricingTable( planId, itemId, { spuCode, rows: listingRows, note: payload.note, productCodeConfig: body.productCodeConfig, }, req.user, { pricingContext } ); } let insertId = null; if (!selectionPricing) { await ensureHistoryTable(); const inserted = await database.local.insert(HISTORY_TABLE, { user_id: userId, user_name: getRequestUser(req).name || null, spu_code: spuCode, brand: payload.brand || null, note: payload.note || null, row_count: rows.length, payload_json: payloadJson, }); insertId = inserted.insertId; } res.json({ success: true, data: { id: insertId, spuCode, rowCount: rows.length, selectionPricing: selectionPricing ? { planId: selectionPricing.planId, itemId: selectionPricing.itemId, taskCode: selectionPricing.taskCode, pricingTableSavedAt: selectionPricing.pricingTableSavedAt, pricingTableVersion: selectionPricing.pricingTableVersion, productCodeStatus: selectionPricing.productCodeStatus, productCodeStyleCode: selectionPricing.productCodeStyleCode, productCodeConfig: selectionPricing.productCodeConfig, productCodeCompletedAt: selectionPricing.productCodeCompletedAt, productCodes: (selectionPricing.rows || []).map((row) => ({ skuCode: row.skuCode, originalSkuName: row.originalSkuName, selectedSkuName: row.selectedSkuName, internalProductCode: row.internalProductCode, productCodeProductName: row.productCodeProductName, })), } : null, }, }); } catch (error) { sendError(res, error, '保存测算记录失败'); } }); // 历史列表(管理员可见全部,普通用户仅本人,不含快照正文) router.get('/history', async (req, res) => { const userId = requireUserId(req, res); if (!userId) return; const { isAdmin } = getRequestUser(req); const page = Math.max(Number.parseInt(req.query.page, 10) || 1, 1); const pageSize = Math.min(Math.max(Number.parseInt(req.query.pageSize, 10) || 20, 1), 100); const spuCode = normalizeText(req.query.spuCode, 160).toUpperCase(); const calculatorType = normalizeText(req.query.calculatorType, 30); const offset = (page - 1) * pageSize; try { await ensureHistoryTable(); const where = []; const params = []; if (!isAdmin) { where.push('user_id = ?'); params.push(userId); } if (spuCode) { where.push('spu_code = ?'); params.push(spuCode); } if (calculatorType === 'alibaba1688') { where.push('payload_json LIKE ?'); params.push('%"calculatorType":"alibaba1688"%'); } else if (calculatorType === 'company') { // 兼容 version 1-3 的公司品历史,它们还没有 calculatorType 字段。 where.push('payload_json NOT LIKE ?'); params.push('%"calculatorType":"alibaba1688"%'); } const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''; const [countRows, list] = await Promise.all([ database.local.query(`SELECT COUNT(*) AS total FROM ${HISTORY_TABLE} ${whereSql}`, params), database.local.query( `SELECT id, user_id, user_name, spu_code, brand, note, row_count, created_at, updated_at FROM ${HISTORY_TABLE} ${whereSql} ORDER BY id DESC LIMIT ${pageSize} OFFSET ${offset}`, params ), ]); res.json({ success: true, data: { list: list.map((row) => ({ id: row.id, userName: row.user_name || '', spuCode: row.spu_code, brand: row.brand || '', note: row.note || '', rowCount: row.row_count, calculatorType: calculatorType === 'alibaba1688' ? 'alibaba1688' : 'company', createdAt: row.created_at, updatedAt: row.updated_at, })), total: Number(countRows[0]?.total || 0), page, pageSize, }, }); } catch (error) { sendError(res, error, '查询测算历史失败'); } }); async function loadOwnHistoryRow(req, res) { const userId = requireUserId(req, res); if (!userId) return null; const { isAdmin } = getRequestUser(req); const id = Number.parseInt(req.params.id, 10); if (!Number.isFinite(id) || id <= 0) { res.status(400).json({ success: false, error: '无效的历史记录 ID' }); return null; } await ensureHistoryTable(); // 服务端强制数据边界:管理员可见全部,普通用户的他人记录表现为不存在 const rows = isAdmin ? await database.local.query(`SELECT * FROM ${HISTORY_TABLE} WHERE id = ? LIMIT 1`, [id]) : await database.local.query( `SELECT * FROM ${HISTORY_TABLE} WHERE id = ? AND user_id = ? LIMIT 1`, [id, userId] ); if (rows.length === 0) { res.status(404).json({ success: false, error: '测算记录不存在或已被删除' }); return null; } return rows[0]; } // 历史详情(仅本人) router.get('/history/:id', async (req, res) => { try { const row = await loadOwnHistoryRow(req, res); if (!row) return; let payload = null; try { payload = JSON.parse(row.payload_json); } catch { payload = null; } res.json({ success: true, data: { id: row.id, userName: row.user_name || '', spuCode: row.spu_code, brand: row.brand || '', note: row.note || '', rowCount: row.row_count, createdAt: row.created_at, updatedAt: row.updated_at, payload, }, }); } catch (error) { sendError(res, error, '查询测算记录失败'); } }); // 删除历史(本人,或管理员可管理全部) router.delete('/history/:id', async (req, res) => { try { const row = await loadOwnHistoryRow(req, res); if (!row) return; await database.local.query( `DELETE FROM ${HISTORY_TABLE} WHERE id = ? AND user_id = ?`, [row.id, row.user_id] ); res.json({ success: true, data: { id: row.id } }); } catch (error) { sendError(res, error, '删除测算记录失败'); } }); // 导出测算表:按前端给定的列顺序(与页面显示一致)与行数据生成 xlsx 下载 const MAX_EXPORT_ROWS = 2000; const MAX_EXPORT_COLUMNS = 40; router.post('/export', async (req, res) => { const userId = requireUserId(req, res); if (!userId) return; const body = req.body || {}; const spuCode = normalizeText(body.spu, 160).toUpperCase() || 'SPU'; const columns = Array.isArray(body.columns) ? body.columns : []; const rows = Array.isArray(body.rows) ? body.rows : []; const validColumns = columns .filter((col) => col && typeof col === 'object') .slice(0, MAX_EXPORT_COLUMNS) .map((col) => ({ key: normalizeText(col.key, 60), label: normalizeText(col.label, 60) || normalizeText(col.key, 60), percent: col.percent === true, })) .filter((col) => col.key); if (validColumns.length === 0) { return res.status(400).json({ success: false, error: '缺少导出列配置' }); } if (rows.length === 0) { return res.status(400).json({ success: false, error: '导出数据为空' }); } if (rows.length > MAX_EXPORT_ROWS) { return res.status(400).json({ success: false, error: `单次最多导出 ${MAX_EXPORT_ROWS} 行` }); } try { const aoa = [validColumns.map((col) => col.label)]; rows.forEach((row) => { aoa.push(validColumns.map((col) => { const value = row && typeof row === 'object' ? row[col.key] : null; return value === null || value === undefined ? '' : value; })); }); const sheet = XLSX.utils.aoa_to_sheet(aoa); // 百分比列:保持数值,设置百分比数字格式(0.06 在 Excel 显示为 6.00%) validColumns.forEach((col, colIndex) => { if (!col.percent) return; for (let rowIndex = 1; rowIndex < aoa.length; rowIndex += 1) { const addr = XLSX.utils.encode_cell({ c: colIndex, r: rowIndex }); const cell = sheet[addr]; if (cell && typeof cell.v === 'number') { cell.z = '0.00%'; } } }); sheet['!cols'] = validColumns.map((col) => ({ wch: Math.max(12, String(col.label).length * 2 + 6) })); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, sheet, '上架测算'); const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }); const stamp = new Date(); const pad = (n) => String(n).padStart(2, '0'); const filename = `上架测算_${spuCode}_${stamp.getFullYear()}${pad(stamp.getMonth() + 1)}${pad(stamp.getDate())}-${pad(stamp.getHours())}${pad(stamp.getMinutes())}.xlsx`; res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`); res.send(buffer); } catch (error) { res.status(500).json({ success: false, error: `导出失败:${error.message}` }); } }); // ===== 用户个性化默认值(每运营维护自己的配置,无配置时前端用系统默认) ===== const PREFS_TABLE = 'pricing_calculator_user_prefs'; let prefsTableReady = null; async function ensurePrefsTable() { if (!prefsTableReady) { prefsTableReady = (async () => { const existing = await database.local.query( `SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ? LIMIT 1`, [PREFS_TABLE] ); if (existing.length > 0) { return; } await database.local.query( `CREATE TABLE IF NOT EXISTS ${PREFS_TABLE} ( user_id BIGINT PRIMARY KEY COMMENT '用户ID', prefs_json VARCHAR(2000) NOT NULL COMMENT '个性化默认值 JSON', created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='上架测算用户个性化默认值'` ); })(); } try { await prefsTableReady; } catch (error) { prefsTableReady = null; throw error; } } // 允许的偏好字段与取值范围(targetMargin 为小数,0.15 = 15%) const PREF_FIELD_RANGES = { coupon: { min: 0, max: 100000 }, activityDiscount: { min: 0.01, max: 1 }, installFee: { min: 0, max: 100000 }, targetMargin: { min: -1, max: 1 }, }; function sanitizePrefs(input) { const src = input && typeof input === 'object' ? input : {}; const prefs = {}; Object.entries(PREF_FIELD_RANGES).forEach(([key, range]) => { const num = Number(src[key]); if (Number.isFinite(num)) { prefs[key] = Math.min(range.max, Math.max(range.min, num)); } }); return prefs; } const PREFS_PROFILE_KEY = '_profiles'; const PREFS_PROFILES = new Set(['company', 'alibaba1688']); function normalizePrefsProfile(value) { return PREFS_PROFILES.has(value) ? value : 'company'; } function parsePrefsStorage(value) { try { const parsed = typeof value === 'string' ? JSON.parse(value || 'null') : value; return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}; } catch { return {}; } } function readPrefsProfile(storageValue, profile = 'company') { const storage = parsePrefsStorage(storageValue); const normalizedProfile = normalizePrefsProfile(profile); const source = normalizedProfile === 'company' ? storage : storage[PREFS_PROFILE_KEY]?.[normalizedProfile]; const prefs = sanitizePrefs(source); return Object.keys(prefs).length > 0 ? prefs : null; } function writePrefsProfile(storageValue, profile, prefs) { const storage = parsePrefsStorage(storageValue); const normalizedProfile = normalizePrefsProfile(profile); if (normalizedProfile === 'company') { return { ...storage, ...prefs }; } return { ...storage, [PREFS_PROFILE_KEY]: { ...(storage[PREFS_PROFILE_KEY] && typeof storage[PREFS_PROFILE_KEY] === 'object' ? storage[PREFS_PROFILE_KEY] : {}), [normalizedProfile]: prefs, }, }; } function createPrefsPatch(profile, prefs) { const normalizedProfile = normalizePrefsProfile(profile); return normalizedProfile === 'company' ? prefs : { [PREFS_PROFILE_KEY]: { [normalizedProfile]: prefs } }; } async function persistPrefsProfile(userId, profile, prefs) { const patch = JSON.stringify(createPrefsPatch(profile, prefs)); try { // JSON_MERGE_PATCH 在数据库内原子合并,保留另一测算类型的配置,且只需一次网络往返。 await database.local.query( `INSERT INTO ${PREFS_TABLE} (user_id, prefs_json) VALUES (?, ?) ON DUPLICATE KEY UPDATE prefs_json = JSON_MERGE_PATCH( IF(JSON_VALID(prefs_json), prefs_json, '{}'), VALUES(prefs_json) )`, [userId, patch] ); } catch (error) { const canFallback = ['ER_SP_DOES_NOT_EXIST', 'ER_PARSE_ERROR', 'ER_NOT_SUPPORTED_YET'] .includes(error.code); if (!canFallback) throw error; // 兼容不支持 JSON_MERGE_PATCH 的旧版 MySQL / MariaDB。 const existingRows = await database.local.query( `SELECT prefs_json FROM ${PREFS_TABLE} WHERE user_id = ? LIMIT 1`, [userId] ); const storage = writePrefsProfile(existingRows[0]?.prefs_json, profile, prefs); await database.local.query( `INSERT INTO ${PREFS_TABLE} (user_id, prefs_json) VALUES (?, ?) ON DUPLICATE KEY UPDATE prefs_json = VALUES(prefs_json)`, [userId, JSON.stringify(storage)] ); } } router.get('/prefs', async (req, res) => { const userId = requireUserId(req, res); if (!userId) return; try { await ensurePrefsTable(); const rows = await database.local.query( `SELECT prefs_json FROM ${PREFS_TABLE} WHERE user_id = ? LIMIT 1`, [userId] ); const profile = normalizePrefsProfile(req.query.profile); const prefs = rows.length > 0 ? readPrefsProfile(rows[0].prefs_json, profile) : null; res.json({ success: true, data: { prefs } }); } catch (error) { sendError(res, error, '读取个性化配置失败'); } }); router.put('/prefs', async (req, res) => { const userId = requireUserId(req, res); if (!userId) return; const prefs = sanitizePrefs(req.body?.prefs ?? req.body); const profile = normalizePrefsProfile(req.body?.profile); if (Object.keys(prefs).length === 0) { return res.status(400).json({ success: false, error: '缺少有效的配置项' }); } try { await ensurePrefsTable(); await persistPrefsProfile(userId, profile, prefs); res.json({ success: true, data: { prefs } }); } catch (error) { sendError(res, error, '保存个性化配置失败'); } }); module.exports = router; module.exports._test = { filterSkuRowsForSelectionContext, createPrefsPatch, getSelectionSkuCodeSet, normalizeCalculatorType, readPrefsProfile, sanitizeHistoryProduct, sanitizePrefs, writePrefsProfile, };