3318c04490
Speed up the quick-plaza path with session snapshot reads, publish timeouts, and a modal fix so parent re-renders no longer abort in-flight requests. Co-authored-by: Cursor <cursoragent@cursor.com>
152 lines
4.7 KiB
JavaScript
152 lines
4.7 KiB
JavaScript
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,
|
|
};
|
|
}
|