修复动态发布与互动问题

This commit is contained in:
2026-06-23 18:43:03 +08:00
parent 702b578e1e
commit cda8685956
151 changed files with 4993 additions and 914 deletions

View File

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