const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() const _ = db.command // Query a collection that may not exist yet (created lazily on first write). async function safeGet(collection, where, limit = 200, orderBy) { try { let query = db.collection(collection).where(where) if (orderBy) query = query.orderBy(orderBy.field, orderBy.direction) return await query.limit(limit).get() } catch (e) { return { data: [] } } } function formatTimeText(ts) { const diff = Date.now() - ts const mins = Math.floor(diff / 60000) if (mins < 1) return '刚刚' if (mins < 60) return `${mins} 分钟前` const hours = Math.floor(mins / 60) if (hours < 24) return `${hours} 小时前` const days = Math.floor(hours / 24) if (days === 1) return '昨天' if (days < 7) return `${days} 天前` const d = new Date(ts) return `${d.getMonth() + 1}月${d.getDate()}日` } function isCloudFileId(value) { return typeof value === 'string' && value.startsWith('cloud://') } function extractMediaUrl(media) { if (!media) return '' if (typeof media === 'string') return media return media.url || media.fileId || media.fileID || '' } async function resolveTempUrlMap(urls) { const fileIds = [...new Set(urls.filter(isCloudFileId))] if (!fileIds.length) return new Map() try { const result = await cloud.getTempFileURL({ fileList: fileIds }) const fileList = Array.isArray(result.fileList) ? result.fileList : [] return new Map(fileList.map(file => [ file.fileID || file.fileId, file.tempFileURL || file.tempFileUrl || '' ]).filter(([fileId, tempUrl]) => fileId && tempUrl)) } catch (e) { return new Map() } } function readableUrl(url, tempUrlMap) { if (!url) return '' if (!isCloudFileId(url)) return url return tempUrlMap.get(url) || '' } exports.main = async event => { const { OPENID } = cloud.getWXContext() const postId = String(event.postId || '').trim() if (!postId) throw new Error('postId is required') const commentPageSize = Math.max(1, Math.min(Number(event.commentPageSize || 50), 100)) const postRes = await db.collection('posts').doc(postId).get().catch(() => null) if (!postRes || !postRes.data) return { post: null, comments: [] } const post = postRes.data // Resolve the author's current profile (fresh avatar) and liked state. let author = null if (post.authorId) { const authorRes = await safeGet('users', { _id: post.authorId }, 1) author = authorRes.data[0] || null } if (!author && post.authorOpenid) { const authorRes = await safeGet('users', { openid: post.authorOpenid }, 1) author = authorRes.data[0] || null } let likedByMe = false if (OPENID) { const likedRes = await safeGet('postLikes', { openid: OPENID, postId }, 1) likedByMe = likedRes.data.length > 0 } const postAssetUrls = [ (author && author.avatarUrl) || post.authorSnapshot?.avatarUrl || '', ...(Array.isArray(post.media) ? post.media.map(extractMediaUrl) : []) ] const postTempUrlMap = await resolveTempUrlMap(postAssetUrls) const postMedia = Array.isArray(post.media) ? post.media .map(extractMediaUrl) .map(url => readableUrl(url, postTempUrlMap)) .filter(Boolean) : [] const mappedPost = { _id: post._id, author: { id: (author && author._id) || post.authorId || '', name: (author && author.nickname) || post.authorSnapshot?.name || '用户', avatarKey: (author && author.avatarKey) || post.authorSnapshot?.avatarKey || 'gradient-avatar-1', avatarUrl: readableUrl((author && author.avatarUrl) || post.authorSnapshot?.avatarUrl || '', postTempUrlMap) }, pet: post.petSnapshot || { name: '', breed: '' }, body: post.content || '', topics: post.topics || [], media: postMedia, mediaTone: post.mediaTone || 'pet-1', sticker: post.sticker || undefined, locationText: post.locationText || '', timeText: formatTimeText(post.createdAt || Date.now()), counts: { likes: post.counts?.likes || 0, comments: post.counts?.comments || 0 }, likedByMe } // Comments, oldest first. Resolve each author's current profile for avatars. const commentWhere = { postId } const commentCursor = Number(event.commentCursor || 0) if (Number.isFinite(commentCursor) && commentCursor > 0) commentWhere.createdAt = _.gt(commentCursor) const commentsRes = await safeGet( 'comments', commentWhere, commentPageSize + 1, { field: 'createdAt', direction: 'asc' } ) const commentPage = commentsRes.data.slice(0, commentPageSize) const commentsNextCursor = commentsRes.data.length > commentPageSize && commentPage[commentPage.length - 1] ? String(commentPage[commentPage.length - 1].createdAt || '') : '' const commentAuthorIds = [...new Set(commentPage.map(c => c.authorId).filter(Boolean))] const authorMap = new Map() for (let i = 0; i < commentAuthorIds.length; i += 100) { const chunk = commentAuthorIds.slice(i, i + 100) const usersRes = await safeGet('users', { _id: _.in(chunk) }, 100) usersRes.data.forEach(u => authorMap.set(u._id, u)) } const commentAuthorOpenids = [...new Set(commentPage.map(c => c.openid).filter(Boolean))] const authorOpenidMap = new Map() for (let i = 0; i < commentAuthorOpenids.length; i += 100) { const chunk = commentAuthorOpenids.slice(i, i + 100) const usersRes = await safeGet('users', { openid: _.in(chunk) }, 100) usersRes.data.forEach(u => authorOpenidMap.set(u.openid, u)) } const findCommentAuthor = comment => authorMap.get(comment.authorId) || authorOpenidMap.get(comment.openid) const commentTempUrlMap = await resolveTempUrlMap(commentPage.map(c => { const u = findCommentAuthor(c) return (u && u.avatarUrl) || c.authorSnapshot?.avatarUrl || '' })) const comments = commentPage.map(c => { const u = findCommentAuthor(c) return { _id: c._id, author: { id: (u && u._id) || c.authorId || '', name: (u && u.nickname) || c.authorSnapshot?.name || '用户', avatarKey: (u && u.avatarKey) || c.authorSnapshot?.avatarKey || 'gradient-avatar-1', avatarUrl: readableUrl((u && u.avatarUrl) || c.authorSnapshot?.avatarUrl || '', commentTempUrlMap) }, content: c.content || '', timeText: formatTimeText(c.createdAt || Date.now()) } }) return { post: mappedPost, comments, commentsNextCursor } }