'use strict'; // MKH 成品补货预警推送 —— 配置 + 推送状态(本地库 system_config) // // ── 配置键 mkh_replenish_alert_config ───────────────────────────── // { enabled, recipientAlias, scope } // · recipientAlias 为空时回退顺序:环境变量 → 统一通知中心默认收件人 // · scope: 'warning'(缺货 + 3 天内预警,默认)| 'outOfStock'(仅缺货) // // ── 状态键 mkh_replenish_alert_state ────────────────────────────── // { version, notified: { [skuId]: { level, firstNotifiedAt, lastNotifiedAt, ... } }, lastRunAt } // · 用于实现「只提醒新出现的预警」:已在 notified 里的商品不再重复推送; // 商品恢复到正常范围后从 notified 移除,下次再预警会重新提醒。 const database = require('../config/database'); const { getYydNotificationCenterRuntimeConfig, toAdminConfig: toNotificationCenterAdminConfig, } = require('./yydNotificationCenterSettings'); const CONFIG_KEY = 'mkh_replenish_alert_config'; const STATE_KEY = 'mkh_replenish_alert_state'; const CONFIG_CACHE_TTL_MS = 30 * 1000; const SCOPES = Object.freeze({ WARNING: 'warning', OUT_OF_STOCK: 'outOfStock' }); const SCOPE_OPTIONS = Object.freeze([ { value: SCOPES.WARNING, label: '缺货 + 3天内预警' }, { value: SCOPES.OUT_OF_STOCK, label: '仅缺货' }, ]); let cachedConfig = null; let cachedAt = 0; function normalizeText(value, maxLength = 200) { return String(value ?? '').trim().slice(0, maxLength); } function safeJsonParse(value, fallback = {}) { if (value && typeof value === 'object') return value; try { const parsed = JSON.parse(String(value || '')); return parsed && typeof parsed === 'object' ? parsed : fallback; } catch (_error) { return fallback; } } function normalizeScope(value) { const text = normalizeText(value, 40).toLowerCase(); if (!text) return SCOPES.WARNING; if (text === 'outofstock' || text === 'out_of_stock' || text === '缺货') return SCOPES.OUT_OF_STOCK; if (text === 'warning' || text === 'warn' || text === '预警' || text === 'all') return SCOPES.WARNING; return SCOPES.WARNING; } function normalizeFlag(value, fallback = true) { if (value === undefined || value === null || value === '') return fallback; if (typeof value === 'boolean') return value; const text = String(value).trim().toLowerCase(); if (['1', 'true', 'yes', 'on'].includes(text)) return true; if (['0', 'false', 'no', 'off'].includes(text)) return false; return fallback; } async function readSystemConfigValue(key) { const rows = await database.local.query( 'SELECT config_value AS configValue FROM system_config WHERE config_key = ? LIMIT 1', [key] ); return rows?.[0]?.configValue; } async function upsertSystemConfigValue(key, value, configType = 'json') { const payload = typeof value === 'string' ? value : JSON.stringify(value); const rows = await database.local.query( 'SELECT id FROM system_config WHERE config_key = ? LIMIT 1', [key] ); if (rows?.[0]) { await database.local.query( 'UPDATE system_config SET config_value = ?, config_type = ? WHERE config_key = ?', [payload, configType, key] ); } else { await database.local.insert('system_config', { config_key: key, config_value: payload, config_type: configType, }); } return payload; } // ── 配置 ──────────────────────────────────────────────────────── async function getMkhReplenishAlertRuntimeConfig({ forceRefresh = false } = {}) { if (!forceRefresh && cachedConfig && Date.now() - cachedAt < CONFIG_CACHE_TTL_MS) { return { ...cachedConfig }; } let stored = {}; try { stored = safeJsonParse(await readSystemConfigValue(CONFIG_KEY), {}); } catch (error) { console.warn('读取补货预警推送配置失败,使用默认值:', error.message); } cachedConfig = { enabled: normalizeFlag(stored.enabled, true), recipientAlias: normalizeText(stored.recipientAlias || stored.recipient_alias, 50), scope: normalizeScope(stored.scope), recipientAliasFallback: normalizeText(process.env.MKH_REPLENISH_ALERT_RECIPIENT_ALIAS, 50), }; cachedAt = Date.now(); return { ...cachedConfig }; } /** * 解析实际收件人 alias:配置填写值 → 环境变量 → 统一通知中心默认收件人。 */ async function resolveMkhReplenishAlertRecipient(options = {}) { const config = options.config || await getMkhReplenishAlertRuntimeConfig(); if (config.recipientAlias) return { alias: config.recipientAlias, source: 'config' }; if (config.recipientAliasFallback) return { alias: config.recipientAliasFallback, source: 'environment' }; let center = {}; try { center = await getYydNotificationCenterRuntimeConfig({ forceRefresh: options.forceRefresh === true }); } catch (_error) { center = {}; } const alias = normalizeText(center?.recipientAlias, 50); return { alias, source: alias ? 'notification_center' : 'none' }; } async function getMkhReplenishAlertAdminConfig() { const config = await getMkhReplenishAlertRuntimeConfig({ forceRefresh: true }); const recipient = await resolveMkhReplenishAlertRecipient({ config }); let center = {}; try { center = await getYydNotificationCenterRuntimeConfig(); } catch (_error) { center = {}; } const centerAdmin = toNotificationCenterAdminConfig(center); const state = await loadMkhReplenishAlertState(); return { enabled: config.enabled, scope: config.scope, recipientAlias: config.recipientAlias, effectiveRecipientAlias: recipient.alias || '', recipientSource: recipient.source, notificationCenterConfigured: centerAdmin.configured, notificationCenterBaseUrl: centerAdmin.baseUrl, notificationCenter: { configured: centerAdmin.configured, baseUrl: centerAdmin.baseUrl, projectCode: centerAdmin.projectCode, projectSecretConfigured: centerAdmin.projectSecretConfigured, projectSecretPreview: centerAdmin.projectSecretPreview, recipientAlias: centerAdmin.recipientAlias, }, notifiedCount: Object.keys(state.notified || {}).length, lastRunAt: state.lastRunAt || null, scopeOptions: SCOPE_OPTIONS.map((item) => ({ ...item })), }; } async function saveMkhReplenishAlertConfig(input = {}) { const current = await getMkhReplenishAlertRuntimeConfig({ forceRefresh: true }); const rawAlias = input.recipientAlias ?? input.recipient_alias; const next = { enabled: normalizeFlag(input.enabled, current.enabled), recipientAlias: normalizeText(rawAlias === undefined ? current.recipientAlias : rawAlias, 50), scope: normalizeScope(input.scope || current.scope), }; await upsertSystemConfigValue(CONFIG_KEY, next); cachedConfig = { ...next, recipientAliasFallback: current.recipientAliasFallback }; cachedAt = Date.now(); return getMkhReplenishAlertAdminConfig(); } // ── 推送状态 ──────────────────────────────────────────────────── async function loadMkhReplenishAlertState() { let parsed = {}; try { parsed = safeJsonParse(await readSystemConfigValue(STATE_KEY), {}); } catch (error) { console.warn('读取补货预警推送状态失败,按空状态处理:', error.message); } const notified = parsed.notified && typeof parsed.notified === 'object' && !Array.isArray(parsed.notified) ? parsed.notified : {}; return { version: 1, notified, lastRunAt: normalizeText(parsed.lastRunAt, 40) || null, }; } async function saveMkhReplenishAlertState(state = {}) { const value = { version: 1, notified: state.notified && typeof state.notified === 'object' ? state.notified : {}, lastRunAt: normalizeText(state.lastRunAt, 40) || null, }; await upsertSystemConfigValue(STATE_KEY, value); return value; } /** 清空「已推送」记录,让下一次检查重新按新增口径判断(便于测试)。 */ async function resetMkhReplenishAlertState() { return saveMkhReplenishAlertState({ notified: {}, lastRunAt: null }); } module.exports = { CONFIG_KEY, STATE_KEY, SCOPES, SCOPE_OPTIONS, getMkhReplenishAlertAdminConfig, getMkhReplenishAlertRuntimeConfig, loadMkhReplenishAlertState, normalizeFlag, normalizeScope, resetMkhReplenishAlertState, resolveMkhReplenishAlertRecipient, saveMkhReplenishAlertConfig, saveMkhReplenishAlertState, };