Files
Pawer/cloudfunctions/postDetail/index.js

180 lines
6.4 KiB
JavaScript

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 }
}