Initial commit: Memind H5 portal with MindSpace, Plaza, and agent jobs.
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+150
@@ -0,0 +1,150 @@
|
||||
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'),
|
||||
});
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user