import crypto from 'node:crypto'; function hashIp(ip) { return crypto.createHash('sha256').update(String(ip ?? 'unknown')).digest('hex').slice(0, 32); } function normalizeUtm(value, maxLen = 100) { return String(value ?? '') .trim() .slice(0, maxLen); } function normalizeEventType(value) { const type = String(value ?? '').trim(); if (type === 'landing' || type === 'signup') return type; throw Object.assign(new Error('不支持的 attribution 事件类型'), { code: 'invalid_input' }); } export const seoInternals = { normalizeUtm, normalizeEventType, buildPostPublicUrl, }; export function buildPostPublicUrl(postId, siteBase = process.env.PLAZA_PUBLIC_BASE ?? 'https://plaza.tkmind.cn') { const base = String(siteBase).replace(/\/$/, ''); return `${base}/plaza/p/${postId}`; } export function createPlazaSeoService( pool, { idFactory = () => crypto.randomUUID(), siteBase = process.env.PLAZA_PUBLIC_BASE ?? 'https://plaza.tkmind.cn', baiduToken = process.env.PLAZA_BAIDU_PUSH_TOKEN ?? '', baiduSite = process.env.PLAZA_BAIDU_SITE ?? 'plaza.tkmind.cn', } = {}, ) { const listSitemapData = async ({ postLimit = 1000, userLimit = 500 } = {}) => { const [categoryRows] = await pool.query( `SELECT slug, name FROM plaza_categories WHERE is_active = 1 ORDER BY sort_order ASC`, ); const [postRows] = await pool.query( `SELECT id, updated_at, published_at FROM plaza_posts WHERE status = 'published' ORDER BY published_at DESC LIMIT ?`, [Math.min(Math.max(Number(postLimit) || 1000, 1), 5000)], ); const [userRows] = await pool.query( `SELECT u.slug, u.username, MAX(pp.published_at) AS last_post_at FROM h5_users u JOIN plaza_posts pp ON pp.user_id = u.id AND pp.status = 'published' GROUP BY u.id, u.slug, u.username ORDER BY last_post_at DESC LIMIT ?`, [Math.min(Math.max(Number(userLimit) || 500, 1), 2000)], ); return { categories: categoryRows.map((row) => ({ slug: row.slug, name: row.name, })), posts: postRows.map((row) => ({ id: row.id, updated_at: new Date(Number(row.updated_at ?? row.published_at)).toISOString(), published_at: new Date(Number(row.published_at)).toISOString(), })), users: userRows.map((row) => ({ slug: row.slug || row.username, last_post_at: row.last_post_at ? new Date(Number(row.last_post_at)).toISOString() : new Date().toISOString(), })), }; }; const recordAttribution = async ( { event_type: eventTypeInput, utm_source: utmSource, utm_medium: utmMedium, utm_campaign: utmCampaign, ref_id: refId, user_id: userId = null, }, ip = 'unknown', ) => { const eventType = normalizeEventType(eventTypeInput); const source = normalizeUtm(utmSource); if (!source) throw Object.assign(new Error('utm_source 不能为空'), { code: 'invalid_input' }); const id = idFactory(); const now = Date.now(); await pool.query( `INSERT INTO plaza_attribution_events (id, event_type, utm_source, utm_medium, utm_campaign, ref_id, user_id, ip_hash, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ id, eventType, source, normalizeUtm(utmMedium), normalizeUtm(utmCampaign), normalizeUtm(refId, 200), userId, hashIp(ip), now, ], ); return { id, event_type: eventType }; }; const pingBaidu = async (urls) => { const list = Array.isArray(urls) ? urls.filter(Boolean) : [urls].filter(Boolean); if (!baiduToken || list.length === 0) { return { pushed: false, reason: 'disabled_or_empty' }; } const endpoint = `http://data.zz.baidu.com/urls?site=${encodeURIComponent(baiduSite)}&token=${encodeURIComponent(baiduToken)}`; const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: list.join('\n'), signal: AbortSignal.timeout(5_000), }); const payload = await response.json().catch(() => ({})); if (!response.ok) { throw Object.assign(new Error('百度推送失败'), { code: 'baidu_push_failed', details: payload }); } return { pushed: true, result: payload }; }; const notifyPostPublished = async (postId) => { const url = buildPostPublicUrl(postId, siteBase); try { return await pingBaidu([url]); } catch (error) { console.error('Plaza Baidu push failed:', error.message); return { pushed: false, reason: error.message }; } }; return { listSitemapData, recordAttribution, pingBaidu, notifyPostPublished, }; }