修复动态发布与互动问题
This commit is contained in:
@@ -28,9 +28,104 @@ function formatTimeText(ts) {
|
||||
return `${d.getMonth() + 1}月${d.getDate()}日`
|
||||
}
|
||||
|
||||
function normalizeKeyword(keyword) {
|
||||
return String(keyword || '').trim().toLowerCase()
|
||||
}
|
||||
|
||||
function postMatchesKeyword(post, keyword) {
|
||||
if (!keyword) return true
|
||||
|
||||
return [
|
||||
post.content,
|
||||
post.locationText,
|
||||
post.authorSnapshot && post.authorSnapshot.name,
|
||||
post.petSnapshot && post.petSnapshot.name,
|
||||
post.petSnapshot && post.petSnapshot.breed,
|
||||
...(Array.isArray(post.topics) ? post.topics : [])
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(keyword)
|
||||
}
|
||||
|
||||
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) || ''
|
||||
}
|
||||
|
||||
async function queryPosts(query, limit, cursor) {
|
||||
const nextQuery = { ...query }
|
||||
if (cursor) nextQuery.createdAt = _.lt(Number(cursor))
|
||||
|
||||
return db.collection('posts')
|
||||
.where(nextQuery)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.limit(limit)
|
||||
.get()
|
||||
}
|
||||
|
||||
async function loadTopicOptions(query) {
|
||||
try {
|
||||
const topicQuery = { ...query }
|
||||
delete topicQuery.topics
|
||||
delete topicQuery.createdAt
|
||||
|
||||
const res = await db.collection('posts')
|
||||
.where(topicQuery)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.limit(100)
|
||||
.get()
|
||||
|
||||
const counts = new Map()
|
||||
res.data.forEach(post => {
|
||||
const postTopics = post.topics || []
|
||||
postTopics.forEach(topic => {
|
||||
if (!topic) return
|
||||
counts.set(topic, (counts.get(topic) || 0) + 1)
|
||||
})
|
||||
})
|
||||
|
||||
return [...counts.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 12)
|
||||
.map(([topic]) => topic)
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
exports.main = async event => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
const pageSize = Math.min(event.pageSize || 20, 50)
|
||||
const keyword = normalizeKeyword(event.keyword)
|
||||
const query = { visibility: 'public' }
|
||||
|
||||
if (event.topic) query.topics = event.topic
|
||||
@@ -48,35 +143,45 @@ exports.main = async event => {
|
||||
query.visibility = _.in(['public', 'friends'])
|
||||
}
|
||||
|
||||
if (event.cursor) {
|
||||
query.createdAt = _.lt(Number(event.cursor))
|
||||
const topics = event.cursor ? [] : await loadTopicOptions(query)
|
||||
const fetchLimit = keyword ? 50 : pageSize
|
||||
const matchedPosts = []
|
||||
let scanCursor = event.cursor || ''
|
||||
let nextCursor = ''
|
||||
|
||||
while (matchedPosts.length < pageSize) {
|
||||
const res = await queryPosts(query, fetchLimit, scanCursor)
|
||||
if (!res.data.length) break
|
||||
|
||||
for (const post of res.data) {
|
||||
scanCursor = String(post.createdAt || '')
|
||||
if (!postMatchesKeyword(post, keyword)) continue
|
||||
|
||||
matchedPosts.push(post)
|
||||
if (matchedPosts.length >= pageSize) {
|
||||
nextCursor = scanCursor
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedPosts.length >= pageSize || res.data.length < fetchLimit) break
|
||||
scanCursor = String(res.data[res.data.length - 1].createdAt || '')
|
||||
}
|
||||
|
||||
const res = await db.collection('posts')
|
||||
.where(query)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.limit(pageSize)
|
||||
.get()
|
||||
if (!matchedPosts.length) return { list: [], nextCursor: '', topics }
|
||||
|
||||
if (!res.data.length) return { list: [], nextCursor: '' }
|
||||
const postIds = matchedPosts.map(p => p._id)
|
||||
|
||||
const postIds = res.data.map(p => p._id)
|
||||
|
||||
// Batch-check liked/favorited status — skip if no valid OPENID (non-WeChat context)
|
||||
// Batch-check liked status, skip if no valid OPENID (non-WeChat context).
|
||||
let likedSet = new Set()
|
||||
let favSet = new Set()
|
||||
if (OPENID) {
|
||||
const [likedRes, favRes] = await Promise.all([
|
||||
safeGet('postLikes', { openid: OPENID, postId: _.in(postIds) }),
|
||||
safeGet('postFavorites', { openid: OPENID, postId: _.in(postIds) })
|
||||
])
|
||||
const likedRes = await safeGet('postLikes', { openid: OPENID, postId: _.in(postIds) })
|
||||
likedSet = new Set(likedRes.data.map(l => l.postId))
|
||||
favSet = new Set(favRes.data.map(f => f.postId))
|
||||
}
|
||||
|
||||
// Resolve authors' current profile (real avatar image) so feed avatars load
|
||||
// and stay fresh, even for posts whose snapshot predates avatar upload.
|
||||
const authorIds = [...new Set(res.data.map(p => p.authorId).filter(Boolean))]
|
||||
const authorIds = [...new Set(matchedPosts.map(p => p.authorId).filter(Boolean))]
|
||||
const authorMap = new Map()
|
||||
for (let i = 0; i < authorIds.length; i += 100) {
|
||||
const chunk = authorIds.slice(i, i + 100)
|
||||
@@ -84,36 +189,70 @@ exports.main = async event => {
|
||||
usersRes.data.forEach(u => authorMap.set(u._id, u))
|
||||
}
|
||||
|
||||
const list = res.data.map(post => {
|
||||
const author = authorMap.get(post.authorId)
|
||||
const authorOpenids = [...new Set(matchedPosts.map(p => p.authorOpenid).filter(Boolean))]
|
||||
const authorOpenidMap = new Map()
|
||||
for (let i = 0; i < authorOpenids.length; i += 100) {
|
||||
const chunk = authorOpenids.slice(i, i + 100)
|
||||
const usersRes = await safeGet('users', { openid: _.in(chunk) }, 100)
|
||||
usersRes.data.forEach(u => authorOpenidMap.set(u.openid, u))
|
||||
}
|
||||
const findAuthor = post => authorMap.get(post.authorId) || authorOpenidMap.get(post.authorOpenid)
|
||||
|
||||
// Real presence per author: a recent, visible location (same signal as 附近).
|
||||
const ONLINE_WINDOW_MS = 5 * 60 * 1000
|
||||
const onlineNow = Date.now()
|
||||
const onlineSet = new Set()
|
||||
for (let i = 0; i < authorOpenids.length; i += 100) {
|
||||
const chunk = authorOpenids.slice(i, i + 100)
|
||||
const locRes = await safeGet('locations', { openid: _.in(chunk) }, 100)
|
||||
locRes.data.forEach(loc => {
|
||||
if (loc.visible && loc.updatedAt && onlineNow - loc.updatedAt < ONLINE_WINDOW_MS) onlineSet.add(loc.openid)
|
||||
})
|
||||
}
|
||||
|
||||
const assetUrls = []
|
||||
matchedPosts.forEach(post => {
|
||||
const author = findAuthor(post)
|
||||
assetUrls.push((author && author.avatarUrl) || post.authorSnapshot?.avatarUrl || '')
|
||||
if (Array.isArray(post.media)) {
|
||||
post.media.forEach(media => assetUrls.push(extractMediaUrl(media)))
|
||||
}
|
||||
})
|
||||
const tempUrlMap = await resolveTempUrlMap(assetUrls)
|
||||
|
||||
const list = matchedPosts.map(post => {
|
||||
const author = findAuthor(post)
|
||||
const media = Array.isArray(post.media)
|
||||
? post.media
|
||||
.map(extractMediaUrl)
|
||||
.map(url => readableUrl(url, tempUrlMap))
|
||||
.filter(Boolean)
|
||||
: []
|
||||
|
||||
return {
|
||||
_id: post._id,
|
||||
author: {
|
||||
id: post.authorId || '',
|
||||
id: (author && author._id) || post.authorId || '',
|
||||
name: (author && author.nickname) || post.authorSnapshot?.name || '用户',
|
||||
avatarKey: (author && author.avatarKey) || post.authorSnapshot?.avatarKey || 'gradient-avatar-1',
|
||||
avatarUrl: (author && author.avatarUrl) || post.authorSnapshot?.avatarUrl || ''
|
||||
avatarUrl: readableUrl((author && author.avatarUrl) || post.authorSnapshot?.avatarUrl || '', tempUrlMap),
|
||||
online: onlineSet.has(post.authorOpenid)
|
||||
},
|
||||
pet: post.petSnapshot || { name: '', breed: '' },
|
||||
body: post.content || '',
|
||||
topics: post.topics || [],
|
||||
media: Array.isArray(post.media)
|
||||
? post.media.map(m => (typeof m === 'string' ? m : m && (m.url || m.fileId))).filter(Boolean)
|
||||
: [],
|
||||
media,
|
||||
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,
|
||||
favorites: post.counts?.favorites || 0
|
||||
comments: post.counts?.comments || 0
|
||||
},
|
||||
likedByMe: likedSet.has(post._id),
|
||||
favoritedByMe: favSet.has(post._id)
|
||||
likedByMe: likedSet.has(post._id)
|
||||
}
|
||||
})
|
||||
|
||||
const last = res.data[res.data.length - 1]
|
||||
return { list, nextCursor: last ? String(last.createdAt) : '' }
|
||||
return { list, nextCursor, topics }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user