feat(mindspace): add achievements showcase with paginated management
Memind CI / Test, build, and release guards (push) Successful in 3m28s
Memind CI / Test, build, and release guards (push) Successful in 3m28s
Ship the M成果 page with list/delete UX, fix deletePage for incomplete page records, route /space/achievements, and add M成果 under the WeChat M空间 menu. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+73
-8
@@ -135,6 +135,12 @@ function pageResponse(row, { includeContent = true, h5Root = null, env = process
|
||||
status: row.status,
|
||||
visibility: row.visibility,
|
||||
publicationAccessMode: row.pub_access_mode ?? null,
|
||||
publicationId: row.pub_id ?? null,
|
||||
publicationStatus: row.pub_status ?? null,
|
||||
viewCount: asNumber(row.total_view_count ?? row.pub_view_count ?? 0),
|
||||
clickCount: null,
|
||||
statsSource: asNumber(row.total_view_count ?? row.pub_view_count ?? 0) > 0 ? 'publication' : 'none',
|
||||
publishedAt: row.pub_published_at != null ? asNumber(row.pub_published_at) : null,
|
||||
publicationUrl: workspacePublicUrl ?? row.pub_public_url ?? null,
|
||||
workspaceRelativePath: workspaceRelativePath ?? null,
|
||||
workspacePublicUrl,
|
||||
@@ -727,21 +733,42 @@ export function createPageService(pool, options = {}) {
|
||||
clauses.push(`c.category_code = ?`);
|
||||
params.push(filters.categoryCode);
|
||||
}
|
||||
if (Number.isFinite(filters.createdAfter)) {
|
||||
clauses.push(`p.created_at >= ?`);
|
||||
params.push(filters.createdAfter);
|
||||
}
|
||||
if (Number.isFinite(filters.createdBefore)) {
|
||||
clauses.push(`p.created_at < ?`);
|
||||
params.push(filters.createdBefore);
|
||||
}
|
||||
const where = clauses.join(' AND ');
|
||||
const { limit, offset } = normalizeListPageFilters(filters);
|
||||
const queryParams = [userId, ...params];
|
||||
const baseFrom = `FROM h5_page_records p
|
||||
JOIN h5_space_categories c ON c.id = p.category_id AND c.user_id = p.user_id
|
||||
LEFT JOIN h5_page_versions pv ON pv.id = p.current_version_id
|
||||
LEFT JOIN h5_publish_records pr ON pr.id = p.current_publish_id AND pr.status = 'online'`;
|
||||
LEFT JOIN (
|
||||
SELECT page_id,
|
||||
SUM(view_count) AS total_view_count,
|
||||
MAX(published_at) AS last_published_at,
|
||||
SUBSTRING_INDEX(GROUP_CONCAT(id ORDER BY COALESCE(published_at, 0) DESC, updated_at DESC), ',', 1) AS latest_publish_id
|
||||
FROM h5_publish_records
|
||||
WHERE user_id = ?
|
||||
GROUP BY page_id
|
||||
) pub_stats ON pub_stats.page_id = p.id
|
||||
LEFT JOIN h5_publish_records pr ON pr.id = COALESCE(p.current_publish_id, pub_stats.latest_publish_id)`;
|
||||
const [[countRows], [rows]] = await Promise.all([
|
||||
pool.query(`SELECT COUNT(*) AS total ${baseFrom} WHERE ${where}`, params),
|
||||
pool.query(`SELECT COUNT(*) AS total ${baseFrom} WHERE ${where}`, queryParams),
|
||||
pool.query(
|
||||
`SELECT p.*, c.category_code, pv.version_no, pr.access_mode AS pub_access_mode, pr.public_url AS pub_public_url
|
||||
`SELECT p.*, c.category_code, pv.version_no,
|
||||
pr.id AS pub_id, pr.access_mode AS pub_access_mode, pr.public_url AS pub_public_url,
|
||||
pr.view_count AS pub_view_count, pr.published_at AS pub_published_at, pr.status AS pub_status,
|
||||
COALESCE(pub_stats.total_view_count, 0) AS total_view_count
|
||||
${baseFrom}
|
||||
WHERE ${where}
|
||||
ORDER BY p.created_at DESC, p.updated_at DESC
|
||||
LIMIT ? OFFSET ?`,
|
||||
[...params, limit, offset],
|
||||
[...queryParams, limit, offset],
|
||||
),
|
||||
]);
|
||||
const total = asNumber(countRows[0]?.total);
|
||||
@@ -754,6 +781,28 @@ export function createPageService(pool, options = {}) {
|
||||
};
|
||||
};
|
||||
|
||||
const listPageCreatedDateBuckets = async (userId, { limit = 3 } = {}) => {
|
||||
const safeLimit = Math.min(Math.max(Number(limit) || 3, 1), 31);
|
||||
const [rows] = await pool.query(
|
||||
`SELECT DATE(FROM_UNIXTIME(p.created_at / 1000)) AS date_key,
|
||||
MIN(p.created_at) AS min_created_at,
|
||||
MAX(p.created_at) AS max_created_at,
|
||||
COUNT(*) AS page_count
|
||||
FROM h5_page_records p
|
||||
WHERE p.user_id = ? AND p.status <> 'deleted'
|
||||
GROUP BY date_key
|
||||
ORDER BY date_key DESC
|
||||
LIMIT ?`,
|
||||
[userId, safeLimit],
|
||||
);
|
||||
return rows.map((row) => ({
|
||||
dateKey: String(row.date_key),
|
||||
minCreatedAt: asNumber(row.min_created_at),
|
||||
maxCreatedAt: asNumber(row.max_created_at),
|
||||
pageCount: asNumber(row.page_count),
|
||||
}));
|
||||
};
|
||||
|
||||
async function getPage(userId, pageId) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT p.*, c.category_code, pv.version_no, av.storage_key, pv.source_snapshot_json
|
||||
@@ -1241,13 +1290,27 @@ export function createPageService(pool, options = {}) {
|
||||
|
||||
const deletePage = async (userId, pageId, options = {}) => {
|
||||
const removeFromPlaza = Boolean(options.removeFromPlaza);
|
||||
const page = await getPage(userId, pageId);
|
||||
const [metaRows] = await pool.query(
|
||||
`SELECT p.id, p.title, p.page_type, p.space_id, p.source_asset_id
|
||||
FROM h5_page_records p
|
||||
WHERE p.id = ? AND p.user_id = ? AND p.status <> 'deleted'
|
||||
LIMIT 1`,
|
||||
[pageId, userId],
|
||||
);
|
||||
const pageMeta = metaRows[0];
|
||||
if (!pageMeta) throw pageError('页面不存在', 'page_not_found');
|
||||
|
||||
const pageDetail = await getPage(userId, pageId).catch(() => null);
|
||||
const contentFormat =
|
||||
pageDetail?.contentFormat ?? (pageMeta.page_type === 'html' ? 'html' : 'markdown');
|
||||
const pageContent = pageDetail?.content ?? '';
|
||||
|
||||
let workspaceHtmlRelativePath = null;
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT pv.source_snapshot_json
|
||||
FROM h5_page_records p
|
||||
JOIN h5_page_versions pv ON pv.id = p.current_version_id
|
||||
LEFT JOIN h5_page_versions pv ON pv.id = p.current_version_id
|
||||
WHERE p.id = ? AND p.user_id = ? AND p.status <> 'deleted'
|
||||
LIMIT 1`,
|
||||
[pageId, userId],
|
||||
@@ -1258,7 +1321,7 @@ export function createPageService(pool, options = {}) {
|
||||
workspaceHtmlRelativePath = null;
|
||||
}
|
||||
const embeddedAssetIds =
|
||||
page.contentFormat === 'html' ? extractAssetIdsFromHtml(page.content ?? '') : [];
|
||||
contentFormat === 'html' ? extractAssetIdsFromHtml(pageContent) : [];
|
||||
|
||||
const conn = await pool.getConnection();
|
||||
let result;
|
||||
@@ -1333,7 +1396,7 @@ export function createPageService(pool, options = {}) {
|
||||
const purgeResult = await purgeWorkspacePageArtifacts({
|
||||
publishDir: workspacePublishDir,
|
||||
htmlRelativePath: workspaceHtmlRelativePath,
|
||||
html: page.content ?? '',
|
||||
html: pageContent,
|
||||
}).catch((error) => {
|
||||
console.warn('[MindSpace] workspace purge failed:', error?.message ?? error);
|
||||
return { removed: [], skipped: [] };
|
||||
@@ -1494,6 +1557,7 @@ export function createPageService(pool, options = {}) {
|
||||
redactPage,
|
||||
createRedactedCopy: redactPage,
|
||||
listPages,
|
||||
listPageCreatedDateBuckets,
|
||||
findPageBySourceAsset,
|
||||
findPageBySourceMessage,
|
||||
findPageByRelativePath,
|
||||
@@ -1602,6 +1666,7 @@ export const pageInternals = {
|
||||
normalizePageInput,
|
||||
normalizeListPageFilters,
|
||||
parseJsonColumn,
|
||||
pageResponse,
|
||||
renderContent,
|
||||
renderPreviewHtml,
|
||||
renderHtmlPreview,
|
||||
|
||||
Reference in New Issue
Block a user