修复动态发布与互动问题
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 }
|
||||
}
|
||||
|
||||
@@ -3,12 +3,25 @@ const cloud = require('wx-server-sdk')
|
||||
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
|
||||
const db = cloud.database()
|
||||
|
||||
// Reading a collection that was never created throws in WeChat 云开发, and this
|
||||
// function reads `follows` before it ever adds to it — so on a fresh database the
|
||||
// first follow would always fail. Create it up-front (no-op if it already exists).
|
||||
async function ensureCollection(name) {
|
||||
try {
|
||||
await db.createCollection(name)
|
||||
} catch (e) {
|
||||
// Already-exists error is expected on every call after the first.
|
||||
}
|
||||
}
|
||||
|
||||
exports.main = async event => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
if (!OPENID) throw new Error('no openid in wx context')
|
||||
const { targetUserId, follow } = event
|
||||
if (!targetUserId) throw new Error('targetUserId is required')
|
||||
|
||||
await ensureCollection('follows')
|
||||
|
||||
const meRes = await db.collection('users').where({ openid: OPENID }).limit(1).get()
|
||||
if (!meRes.data.length) throw new Error('user not found')
|
||||
const meId = meRes.data[0]._id
|
||||
|
||||
@@ -5,17 +5,36 @@ const db = cloud.database()
|
||||
|
||||
exports.main = async event => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
|
||||
// Reading a non-existent collection throws in WeChat 云开发, and this function
|
||||
// starts with a read — so the collection would never get created by the later
|
||||
// add(). Create it up-front (no-op if it already exists) to break that deadlock.
|
||||
try {
|
||||
await db.createCollection('locations')
|
||||
} catch (e) {
|
||||
// -501001 / "already exists" is expected on every call after the first.
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const loc = event.location || null
|
||||
const visible = Boolean(event.visible)
|
||||
const hasCoord = loc && typeof loc.latitude === 'number' && typeof loc.longitude === 'number'
|
||||
|
||||
const data = {
|
||||
openid: OPENID,
|
||||
location: event.location || null,
|
||||
visible: Boolean(event.visible),
|
||||
location: hasCoord ? { latitude: loc.latitude, longitude: loc.longitude } : null,
|
||||
// `nearbyPets` currently filters by distance in memory off `location`, so the
|
||||
// `visible` flag below is what gates discovery. `geo` is kept (Point form) only
|
||||
// to enable a future geoNear migration if the user base outgrows the in-memory
|
||||
// scan — it needs a manually-created geo index to be queried.
|
||||
geo: hasCoord && visible ? db.Geo.Point(loc.longitude, loc.latitude) : null,
|
||||
visible,
|
||||
updatedAt: now
|
||||
}
|
||||
|
||||
const existed = await db.collection('locations').where({ openid: OPENID }).limit(1).get()
|
||||
if (existed.data.length) await db.collection('locations').doc(existed.data[0]._id).update({ data })
|
||||
else await db.collection('locations').add({ data })
|
||||
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,16 @@ function makeHandle(openid) {
|
||||
return `@user_${suffix}`
|
||||
}
|
||||
|
||||
// Default avatar pool to randomise new user avatars
|
||||
const AVATAR_KEYS = [
|
||||
'gradient-avatar-1', 'gradient-avatar-2', 'gradient-avatar-3',
|
||||
'gradient-avatar-4', 'gradient-avatar-5', 'gradient-avatar-6'
|
||||
]
|
||||
// Deterministic gradient swatch (.gradient-avatar-1..6) from the user's openid, so
|
||||
// a user who hasn't set a real avatar always gets the same stable colour.
|
||||
function gradientAvatarKey(seed) {
|
||||
let h = 5381
|
||||
for (let i = 0; i < seed.length; i++) {
|
||||
h = ((h << 5) + h) ^ seed.charCodeAt(i)
|
||||
h >>>= 0
|
||||
}
|
||||
return `gradient-avatar-${(h % 6) + 1}`
|
||||
}
|
||||
|
||||
exports.main = async event => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
@@ -35,7 +40,7 @@ exports.main = async event => {
|
||||
}
|
||||
|
||||
// New user — create with sensible defaults
|
||||
const avatarKey = AVATAR_KEYS[Math.floor(Math.random() * AVATAR_KEYS.length)]
|
||||
const avatarKey = gradientAvatarKey(OPENID)
|
||||
const user = {
|
||||
openid: OPENID,
|
||||
nickname: event.userInfo?.nickname || '宠物达人',
|
||||
@@ -48,7 +53,7 @@ exports.main = async event => {
|
||||
level: 1,
|
||||
bio: '',
|
||||
verified: false,
|
||||
stats: { posts: 0, following: 0, followers: 0, favorites: 0 },
|
||||
stats: { posts: 0, following: 0, followers: 0 },
|
||||
preferences: { notifications: true, nearbyVisible: true },
|
||||
createdAt: now,
|
||||
lastLoginAt: now
|
||||
|
||||
@@ -4,11 +4,25 @@ cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
|
||||
const db = cloud.database()
|
||||
const _ = db.command
|
||||
|
||||
// Reading a collection that was never created throws in WeChat 云开发, and this
|
||||
// function reads `matches` before it ever adds to it — so the collection would
|
||||
// never get created and every "like" would fail. Create it up-front (no-op if it
|
||||
// already exists) to break that deadlock.
|
||||
async function ensureCollection(name) {
|
||||
try {
|
||||
await db.createCollection(name)
|
||||
} catch (e) {
|
||||
// Already-exists error is expected on every call after the first.
|
||||
}
|
||||
}
|
||||
|
||||
exports.main = async event => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
const { petId } = event
|
||||
if (!petId) throw new Error('petId is required')
|
||||
|
||||
await ensureCollection('matches')
|
||||
|
||||
// Get current user and their pets
|
||||
const userRes = await db.collection('users').where({ openid: OPENID }).limit(1).get()
|
||||
if (!userRes.data.length) throw new Error('user not found')
|
||||
|
||||
@@ -40,6 +40,17 @@ async function fetchUsers(ids) {
|
||||
return out
|
||||
}
|
||||
|
||||
async function fetchUsersByOpenids(openids) {
|
||||
if (!openids.length) return []
|
||||
const out = []
|
||||
for (let i = 0; i < openids.length; i += 100) {
|
||||
const chunk = openids.slice(i, i + 100)
|
||||
const res = await db.collection('users').where({ openid: _.in(chunk) }).limit(100).get()
|
||||
out.push(...res.data)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
exports.main = async event => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
if (!OPENID) throw new Error('no openid in wx context')
|
||||
@@ -70,22 +81,46 @@ exports.main = async event => {
|
||||
convRes = { data: [] }
|
||||
}
|
||||
|
||||
const peerOpenids = [
|
||||
...new Set(
|
||||
convRes.data
|
||||
.filter(c => (c.type || 'single') === 'single')
|
||||
.map(c => (c.memberOpenids || []).find(memberOpenid => memberOpenid !== OPENID))
|
||||
.filter(Boolean)
|
||||
)
|
||||
]
|
||||
const peerUsers = await fetchUsersByOpenids(peerOpenids)
|
||||
const peerUserByOpenid = new Map(peerUsers.map(user => [user.openid, user]))
|
||||
|
||||
const conversations = convRes.data.map(c => {
|
||||
const unreadOpenids = c.unreadOpenids || []
|
||||
const unreadCounts = c.unreadCounts || {}
|
||||
const unreadCount = unreadCounts[OPENID] || (unreadOpenids.includes(OPENID) ? 1 : 0)
|
||||
const type = c.type || 'single'
|
||||
const peerOpenid = type === 'single'
|
||||
? (c.memberOpenids || []).find(memberOpenid => memberOpenid !== OPENID)
|
||||
: ''
|
||||
const peer = peerOpenid ? peerUserByOpenid.get(peerOpenid) : null
|
||||
|
||||
return {
|
||||
_id: c._id,
|
||||
type: c.type || 'single',
|
||||
title: c.title || '未知用户',
|
||||
type,
|
||||
title: type === 'single'
|
||||
? peer?.nickname || c.title || '未知用户'
|
||||
: c.title || '系统消息',
|
||||
petName: c.petName || undefined,
|
||||
avatarKey: c.avatarKey || 'gradient-avatar-1',
|
||||
avatarKey: type === 'single'
|
||||
? peer?.avatarKey || c.avatarKey || 'gradient-avatar-1'
|
||||
: c.avatarKey || 'gradient-avatar-1',
|
||||
avatarUrl: type === 'single'
|
||||
? peer?.avatarUrl || c.avatarUrl || ''
|
||||
: c.avatarUrl || '',
|
||||
preview: c.lastMessage?.content || c.preview || '',
|
||||
timeText: formatTimeText(c.updatedAt || c.createdAt),
|
||||
unreadCount,
|
||||
pinned: Boolean(c.pinned),
|
||||
muted: Boolean(c.muted),
|
||||
online: Boolean(c.online)
|
||||
online: type === 'single' ? Boolean(peer?.online || c.online) : Boolean(c.online)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -3,14 +3,148 @@ const cloud = require('wx-server-sdk')
|
||||
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
|
||||
const db = cloud.database()
|
||||
const _ = db.command
|
||||
const SERVER_MESSAGE_LIMIT = 500
|
||||
const CONVERSATIONS_COLLECTION = 'conversations'
|
||||
const MESSAGES_COLLECTION = 'messages'
|
||||
const CHAT_MESSAGE_TEMPLATE_ID = process.env.WQ_MESSAGE_TEMPLATE_ID || process.env.TARO_APP_MESSAGE_TEMPLATE_ID || ''
|
||||
|
||||
function isCollectionMissing(error) {
|
||||
const text = String(error?.errMsg || error?.message || error || '')
|
||||
return (
|
||||
text.includes('collection not exist') ||
|
||||
text.includes('collection not exists') ||
|
||||
text.includes('collection is not exists') ||
|
||||
text.includes('DATABASE_COLLECTION_NOT_EXIST') ||
|
||||
text.includes('-502005')
|
||||
)
|
||||
}
|
||||
|
||||
function isCollectionAlreadyExists(error) {
|
||||
const text = String(error?.errMsg || error?.message || error || '')
|
||||
return text.includes('collection already exists') || text.includes('DATABASE_COLLECTION_ALREADY_EXIST')
|
||||
}
|
||||
|
||||
async function ensureCollection(name) {
|
||||
if (typeof db.createCollection !== 'function') {
|
||||
throw new Error(`${name} collection does not exist`)
|
||||
}
|
||||
|
||||
try {
|
||||
await db.createCollection(name)
|
||||
} catch (error) {
|
||||
if (!isCollectionAlreadyExists(error)) throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function getExistingConversation(openid, toOpenid) {
|
||||
try {
|
||||
return await db.collection(CONVERSATIONS_COLLECTION)
|
||||
.where({ type: 'single', memberOpenids: _.all([openid, toOpenid]) })
|
||||
.limit(1)
|
||||
.get()
|
||||
} catch (error) {
|
||||
if (!isCollectionMissing(error)) throw error
|
||||
await ensureCollection(CONVERSATIONS_COLLECTION)
|
||||
return { data: [] }
|
||||
}
|
||||
}
|
||||
|
||||
async function getConversationById(conversationId) {
|
||||
try {
|
||||
return await db.collection(CONVERSATIONS_COLLECTION).doc(conversationId).get()
|
||||
} catch (error) {
|
||||
if (!isCollectionMissing(error)) throw error
|
||||
return { data: null }
|
||||
}
|
||||
}
|
||||
|
||||
async function addConversation(data) {
|
||||
try {
|
||||
return await db.collection(CONVERSATIONS_COLLECTION).add({ data })
|
||||
} catch (error) {
|
||||
if (!isCollectionMissing(error)) throw error
|
||||
await ensureCollection(CONVERSATIONS_COLLECTION)
|
||||
return db.collection(CONVERSATIONS_COLLECTION).add({ data })
|
||||
}
|
||||
}
|
||||
|
||||
async function addMessage(data) {
|
||||
try {
|
||||
return await db.collection(MESSAGES_COLLECTION).add({ data })
|
||||
} catch (error) {
|
||||
if (!isCollectionMissing(error)) throw error
|
||||
await ensureCollection(MESSAGES_COLLECTION)
|
||||
return db.collection(MESSAGES_COLLECTION).add({ data })
|
||||
}
|
||||
}
|
||||
|
||||
async function trimConversationMessages(conversationId) {
|
||||
for (;;) {
|
||||
const oldMessages = await db.collection(MESSAGES_COLLECTION)
|
||||
.where({ conversationId })
|
||||
.orderBy('createdAt', 'desc')
|
||||
.skip(SERVER_MESSAGE_LIMIT)
|
||||
.limit(100)
|
||||
.get()
|
||||
.catch(() => ({ data: [] }))
|
||||
|
||||
if (!oldMessages.data.length) return
|
||||
|
||||
await Promise.all(
|
||||
oldMessages.data.map(item =>
|
||||
db.collection(MESSAGES_COLLECTION).doc(item._id).remove().catch(() => null)
|
||||
)
|
||||
)
|
||||
|
||||
if (oldMessages.data.length < 100) return
|
||||
}
|
||||
}
|
||||
|
||||
function pad(value) {
|
||||
return String(value).padStart(2, '0')
|
||||
}
|
||||
|
||||
function formatDateTime(ts) {
|
||||
const d = new Date(ts)
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
||||
function templateValue(value, max = 20) {
|
||||
const text = String(value || '').trim()
|
||||
return text.length > max ? `${text.slice(0, max - 1)}…` : text
|
||||
}
|
||||
|
||||
async function sendChatNotifications({ recipients, conversationId, senderName, content, type, createdAt }) {
|
||||
if (!CHAT_MESSAGE_TEMPLATE_ID || !recipients.length || !cloud.openapi?.subscribeMessage?.send) return
|
||||
|
||||
const preview = type === 'image' ? '[图片]' : content
|
||||
await Promise.all(
|
||||
recipients.map(touser =>
|
||||
cloud.openapi.subscribeMessage.send({
|
||||
touser,
|
||||
templateId: CHAT_MESSAGE_TEMPLATE_ID,
|
||||
page: `pages/chat/index?conversationId=${conversationId}`,
|
||||
data: {
|
||||
thing31: { value: templateValue(senderName || '汪友') },
|
||||
thing2: { value: templateValue(preview || '你收到一条新消息') },
|
||||
time3: { value: formatDateTime(createdAt) }
|
||||
}
|
||||
}).catch(error => {
|
||||
console.log('[messageSend] subscribe message skipped', error?.errMsg || error?.message || error)
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
exports.main = async event => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
const content = String(event.content || '').trim()
|
||||
if (!content) throw new Error('content is required')
|
||||
if (!OPENID) throw new Error('no openid in wx context')
|
||||
|
||||
const now = Date.now()
|
||||
let convId = event.conversationId
|
||||
let conversation = null
|
||||
|
||||
// Create a 1:1 conversation if not provided
|
||||
if (!convId) {
|
||||
@@ -19,53 +153,64 @@ exports.main = async event => {
|
||||
if (!toOpenid && event.toUserId) {
|
||||
const toUserById = await db.collection('users').doc(event.toUserId).get().catch(() => null)
|
||||
if (toUserById && toUserById.data) toOpenid = toUserById.data.openid
|
||||
if (!toOpenid) {
|
||||
const toUserByOpenid = await db.collection('users')
|
||||
.where({ openid: event.toUserId })
|
||||
.limit(1)
|
||||
.get()
|
||||
.catch(() => ({ data: [] }))
|
||||
if (toUserByOpenid.data[0]) toOpenid = toUserByOpenid.data[0].openid
|
||||
}
|
||||
}
|
||||
if (!toOpenid) throw new Error('conversationId or toOpenid/toUserId is required')
|
||||
if (!toOpenid) throw new Error('target user not found')
|
||||
|
||||
// Look for existing conversation between these two users
|
||||
const existing = await db.collection('conversations')
|
||||
.where({ type: 'single', memberOpenids: _.all([OPENID, toOpenid]) })
|
||||
.limit(1)
|
||||
.get()
|
||||
const existing = await getExistingConversation(OPENID, toOpenid)
|
||||
|
||||
if (existing.data.length) {
|
||||
convId = existing.data[0]._id
|
||||
conversation = existing.data[0]
|
||||
convId = conversation._id
|
||||
} else {
|
||||
const toUserRes = await db.collection('users').where({ openid: toOpenid }).limit(1).get()
|
||||
const toUser = toUserRes.data[0] || { nickname: '用户', avatarKey: 'gradient-avatar-1' }
|
||||
const newConv = await db.collection('conversations').add({
|
||||
data: {
|
||||
type: 'single',
|
||||
memberOpenids: [OPENID, toOpenid],
|
||||
unreadOpenids: [toOpenid],
|
||||
unreadCounts: { [toOpenid]: 1 },
|
||||
title: toUser.nickname,
|
||||
avatarKey: toUser.avatarKey,
|
||||
lastMessage: { content: content.slice(0, 50), createdAt: now },
|
||||
pinned: false,
|
||||
muted: false,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}
|
||||
})
|
||||
conversation = {
|
||||
type: 'single',
|
||||
memberOpenids: [OPENID, toOpenid],
|
||||
unreadOpenids: [],
|
||||
unreadCounts: {},
|
||||
title: toUser.nickname,
|
||||
avatarKey: toUser.avatarKey,
|
||||
lastMessage: { content: content.slice(0, 50), createdAt: now },
|
||||
pinned: false,
|
||||
muted: false,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}
|
||||
const newConv = await addConversation(conversation)
|
||||
convId = newConv._id
|
||||
}
|
||||
}
|
||||
|
||||
if (!conversation) {
|
||||
const convRes = await getConversationById(convId)
|
||||
conversation = convRes.data
|
||||
}
|
||||
if (!conversation) throw new Error('conversation not found')
|
||||
|
||||
const members = conversation.memberOpenids || []
|
||||
if (!members.includes(OPENID)) throw new Error('permission denied')
|
||||
if ((conversation.type || 'single') !== 'single') throw new Error('unsupported conversation type')
|
||||
|
||||
// Add message to messages collection
|
||||
const msgResult = await db.collection('messages').add({
|
||||
data: {
|
||||
conversationId: convId,
|
||||
fromOpenid: OPENID,
|
||||
content: content.slice(0, 1000),
|
||||
type: event.msgType || 'text',
|
||||
createdAt: now
|
||||
}
|
||||
const msgResult = await addMessage({
|
||||
conversationId: convId,
|
||||
fromOpenid: OPENID,
|
||||
content: content.slice(0, 1000),
|
||||
type: event.msgType || 'text',
|
||||
createdAt: now
|
||||
})
|
||||
|
||||
// Update conversation: last message + unread counts for all recipients
|
||||
const convRes = await db.collection('conversations').doc(convId).get()
|
||||
const members = convRes.data.memberOpenids || []
|
||||
const recipients = members.filter(id => id !== OPENID)
|
||||
|
||||
const updateData = {
|
||||
@@ -78,11 +223,21 @@ exports.main = async event => {
|
||||
})
|
||||
|
||||
// Rebuild unreadOpenids to include all recipients (remove duplicates)
|
||||
const existingUnread = convRes.data.unreadOpenids || []
|
||||
const existingUnread = conversation.unreadOpenids || []
|
||||
const newUnread = [...new Set([...existingUnread, ...recipients])]
|
||||
updateData.unreadOpenids = newUnread
|
||||
|
||||
await db.collection('conversations').doc(convId).update({ data: updateData })
|
||||
await db.collection(CONVERSATIONS_COLLECTION).doc(convId).update({ data: updateData })
|
||||
await trimConversationMessages(convId)
|
||||
const senderRes = await db.collection('users').where({ openid: OPENID }).limit(1).get().catch(() => ({ data: [] }))
|
||||
await sendChatNotifications({
|
||||
recipients,
|
||||
conversationId: convId,
|
||||
senderName: senderRes.data[0]?.nickname || conversation.title || '汪友',
|
||||
content,
|
||||
type: event.msgType || 'text',
|
||||
createdAt: now
|
||||
})
|
||||
|
||||
return { messageId: msgResult._id, conversationId: convId }
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ const cloud = require('wx-server-sdk')
|
||||
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
|
||||
const db = cloud.database()
|
||||
const _ = db.command
|
||||
const SERVER_MESSAGE_LIMIT = 50
|
||||
|
||||
function timeText(ts) {
|
||||
if (!ts) return ''
|
||||
@@ -18,12 +19,30 @@ async function userToPeer(u) {
|
||||
return { id: u._id, name: u.nickname || '用户', avatarKey: u.avatarKey || 'gradient-avatar-1', avatarUrl: u.avatarUrl || '' }
|
||||
}
|
||||
|
||||
function normalizeLimit(value) {
|
||||
const limit = Number(value || SERVER_MESSAGE_LIMIT)
|
||||
if (!Number.isFinite(limit)) return SERVER_MESSAGE_LIMIT
|
||||
return Math.max(1, Math.min(Math.floor(limit), SERVER_MESSAGE_LIMIT))
|
||||
}
|
||||
|
||||
function messageToClient(m, openid) {
|
||||
return {
|
||||
_id: m._id,
|
||||
fromMe: m.fromOpenid === openid,
|
||||
content: m.content,
|
||||
type: m.type || 'text',
|
||||
createdAt: m.createdAt,
|
||||
timeText: timeText(m.createdAt)
|
||||
}
|
||||
}
|
||||
|
||||
exports.main = async event => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
if (!OPENID) throw new Error('no openid in wx context')
|
||||
|
||||
let convId = event.conversationId || ''
|
||||
let peer = null
|
||||
const pageSize = normalizeLimit(event.pageSize || event.limit)
|
||||
|
||||
// Open by target user — resolve peer and find an existing conversation.
|
||||
if (!convId && event.targetUserId) {
|
||||
@@ -56,27 +75,32 @@ exports.main = async event => {
|
||||
}
|
||||
|
||||
let messages = []
|
||||
let nextCursor = ''
|
||||
if (convId) {
|
||||
const query = { conversationId: convId }
|
||||
const cursor = Number(event.cursor || 0)
|
||||
if (Number.isFinite(cursor) && cursor > 0) query.createdAt = _.lt(cursor)
|
||||
|
||||
const res = await db.collection('messages')
|
||||
.where({ conversationId: convId })
|
||||
.where(query)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.limit(50)
|
||||
.limit(pageSize + 1)
|
||||
.get()
|
||||
.catch(() => ({ data: [] }))
|
||||
messages = res.data.reverse().map(m => ({
|
||||
_id: m._id,
|
||||
fromMe: m.fromOpenid === OPENID,
|
||||
content: m.content,
|
||||
type: m.type || 'text',
|
||||
createdAt: m.createdAt,
|
||||
timeText: timeText(m.createdAt)
|
||||
}))
|
||||
const page = res.data.slice(0, pageSize)
|
||||
messages = page.reverse().map(m => messageToClient(m, OPENID))
|
||||
if (res.data.length > pageSize && messages[0]) {
|
||||
nextCursor = String(messages[0].createdAt || '')
|
||||
}
|
||||
|
||||
// Mark this conversation read for me.
|
||||
await db.collection('conversations').doc(convId).update({
|
||||
data: { unreadOpenids: _.pull(OPENID), [`unreadCounts.${OPENID}`]: 0 }
|
||||
}).catch(() => {})
|
||||
// Mark this conversation read for me only when the client asks for it.
|
||||
// Silent polling and loading older history should not write the conversation.
|
||||
if (event.markRead !== false) {
|
||||
await db.collection('conversations').doc(convId).update({
|
||||
data: { unreadOpenids: _.pull(OPENID), [`unreadCounts.${OPENID}`]: 0 }
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
return { conversationId: convId, peer, messages }
|
||||
return { conversationId: convId, peer, messages, nextCursor }
|
||||
}
|
||||
|
||||
@@ -4,108 +4,172 @@ cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
|
||||
const db = cloud.database()
|
||||
const _ = db.command
|
||||
|
||||
async function fetchUsers(ids) {
|
||||
if (!ids.length) return []
|
||||
// Only people within this radius are "nearby". Online = location reported recently.
|
||||
const RADIUS_METERS = 5000
|
||||
const ONLINE_WINDOW_MS = 5 * 60 * 1000
|
||||
// How many visible locations to scan in memory. No geo index is required this way
|
||||
// (geoNear would need a manually-created index in the cloud console); fine for an
|
||||
// early-stage user base. Switch to geoNear on `locations.geo` if this grows large.
|
||||
const MAX_SCAN = 500
|
||||
const MAX_RESULTS = 50
|
||||
|
||||
// In WeChat 云开发, querying a collection that was never created throws
|
||||
// ("collection not exists"). Treat that as an empty result so an unused
|
||||
// collection (e.g. follows/matches before anyone follows/likes) can't break
|
||||
// the whole nearby query.
|
||||
function isMissingCollection(e) {
|
||||
if (!e) return false
|
||||
const msg = String(e.errMsg || e.message || '')
|
||||
return e.errCode === -502005 || /not exist/i.test(msg) || /CollectionNotExists/i.test(msg)
|
||||
}
|
||||
|
||||
async function safeGet(query) {
|
||||
try {
|
||||
return await query.get()
|
||||
} catch (e) {
|
||||
if (isMissingCollection(e)) return { data: [] }
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchByChunks(collection, field, values) {
|
||||
const out = []
|
||||
for (let i = 0; i < ids.length; i += 100) {
|
||||
const chunk = ids.slice(i, i + 100)
|
||||
const res = await db.collection('users').where({ _id: _.in(chunk) }).limit(100).get()
|
||||
for (let i = 0; i < values.length; i += 100) {
|
||||
const chunk = values.slice(i, i + 100)
|
||||
const res = await safeGet(db.collection(collection).where({ [field]: _.in(chunk) }).limit(100))
|
||||
out.push(...res.data)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function hashId(id) {
|
||||
let h = 5381
|
||||
for (let i = 0; i < id.length; i++) {
|
||||
h = ((h << 5) + h) ^ id.charCodeAt(i)
|
||||
h = h >>> 0 // keep as unsigned 32-bit
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// Generate a stable map pin position from pet id (fallback abstract layer)
|
||||
function stablePin(id) {
|
||||
const h = hashId(id)
|
||||
const left = 10 + (h % 70)
|
||||
const top = 15 + ((h >> 8) % 55)
|
||||
return { left, top }
|
||||
}
|
||||
|
||||
// Scatter a pet deterministically within roughly ±0.9km of the user, so on a
|
||||
// real map markers cluster nearby (stable per pet id across requests).
|
||||
function syntheticCoord(id, userLoc) {
|
||||
const h = hashId(id)
|
||||
const dLat = ((h % 1600) - 800) / 100000 // ±0.008° ≈ ±0.9km
|
||||
const dLng = (((h >> 8) % 1600) - 800) / 100000
|
||||
return { latitude: userLoc.latitude + dLat, longitude: userLoc.longitude + dLng }
|
||||
}
|
||||
|
||||
// Rough distance text from coordinate delta (fallback when no real distance)
|
||||
function distanceText(petLoc, userLoc) {
|
||||
if (!petLoc || !userLoc) return null
|
||||
// Great-circle distance in metres between two {latitude, longitude} points.
|
||||
function distanceMeters(a, b) {
|
||||
if (!a || !b || typeof a.latitude !== 'number' || typeof b.latitude !== 'number') return Infinity
|
||||
const R = 6371000
|
||||
const dLat = ((petLoc.latitude - userLoc.latitude) * Math.PI) / 180
|
||||
const dLon = ((petLoc.longitude - userLoc.longitude) * Math.PI) / 180
|
||||
const a =
|
||||
const dLat = ((b.latitude - a.latitude) * Math.PI) / 180
|
||||
const dLon = ((b.longitude - a.longitude) * Math.PI) / 180
|
||||
const s =
|
||||
Math.sin(dLat / 2) ** 2 +
|
||||
Math.cos((userLoc.latitude * Math.PI) / 180) *
|
||||
Math.cos((petLoc.latitude * Math.PI) / 180) *
|
||||
Math.cos((a.latitude * Math.PI) / 180) *
|
||||
Math.cos((b.latitude * Math.PI) / 180) *
|
||||
Math.sin(dLon / 2) ** 2
|
||||
const meters = Math.round(R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)))
|
||||
if (meters < 1000) return `${meters}m`
|
||||
return `${(meters / 1000).toFixed(1)}km`
|
||||
return R * 2 * Math.atan2(Math.sqrt(s), Math.sqrt(1 - s))
|
||||
}
|
||||
|
||||
function distanceText(meters) {
|
||||
if (!isFinite(meters)) return null
|
||||
const m = Math.round(meters)
|
||||
if (m < 1000) return `${m}m`
|
||||
return `${(m / 1000).toFixed(1)}km`
|
||||
}
|
||||
|
||||
exports.main = async event => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
const filter = event.filters?.type || 'all'
|
||||
const userLoc = event.location || null
|
||||
const query = {}
|
||||
if (filter === 'dog' || filter === 'cat') query.species = filter
|
||||
if (filter === 'online') query.online = true
|
||||
|
||||
// No usable origin → nothing to anchor "nearby" to.
|
||||
if (!userLoc || typeof userLoc.latitude !== 'number' || typeof userLoc.longitude !== 'number') {
|
||||
return { list: [] }
|
||||
}
|
||||
|
||||
// Identify the caller and their follow graph (for friend/following badges).
|
||||
let meId = ''
|
||||
let followingSet = new Set()
|
||||
let followerSet = new Set()
|
||||
|
||||
if (OPENID) {
|
||||
const meRes = await db.collection('users').where({ openid: OPENID }).limit(1).get()
|
||||
const meRes = await safeGet(db.collection('users').where({ openid: OPENID }).limit(1))
|
||||
const me = meRes.data[0]
|
||||
if (me) {
|
||||
meId = me._id
|
||||
const [followingRes, followerRes] = await Promise.all([
|
||||
db.collection('follows').where({ followerId: meId }).limit(500).get(),
|
||||
db.collection('follows').where({ followeeId: meId }).limit(500).get()
|
||||
safeGet(db.collection('follows').where({ followerId: meId }).limit(500)),
|
||||
safeGet(db.collection('follows').where({ followeeId: meId }).limit(500))
|
||||
])
|
||||
followingSet = new Set(followingRes.data.map(f => f.followeeId))
|
||||
followerSet = new Set(followerRes.data.map(f => f.followerId))
|
||||
}
|
||||
}
|
||||
|
||||
const res = await db.collection('pets').where(query).limit(50).get()
|
||||
const ownerIds = [...new Set(res.data.map(pet => pet.ownerId).filter(Boolean))]
|
||||
const owners = await fetchUsers(ownerIds)
|
||||
const ownerMap = new Map(owners.map(owner => [owner._id, owner]))
|
||||
|
||||
const list = res.data.map(pet => {
|
||||
const realCoord = pet.location && typeof pet.location.latitude === 'number' ? pet.location : null
|
||||
const coord = realCoord || (userLoc ? syntheticCoord(pet._id, userLoc) : null)
|
||||
const owner = ownerMap.get(pet.ownerId)
|
||||
const isFollowing = followingSet.has(pet.ownerId)
|
||||
const isFollowedBy = followerSet.has(pet.ownerId)
|
||||
return {
|
||||
...pet,
|
||||
ownerName: owner?.nickname || '',
|
||||
isMine: Boolean(meId && pet.ownerId === meId),
|
||||
isFollowing,
|
||||
isFriend: isFollowing && isFollowedBy,
|
||||
latitude: coord ? coord.latitude : undefined,
|
||||
longitude: coord ? coord.longitude : undefined,
|
||||
distanceText: distanceText(coord, userLoc) || pet.distanceText || null,
|
||||
pin: stablePin(pet._id)
|
||||
// 1) Scan visible locations (paginated) and keep those inside the radius.
|
||||
const near = []
|
||||
let scanned = 0
|
||||
for (let skip = 0; skip < MAX_SCAN; skip += 100) {
|
||||
const res = await safeGet(db.collection('locations').where({ visible: true }).skip(skip).limit(100))
|
||||
scanned += res.data.length
|
||||
if (!res.data.length) break
|
||||
for (const rec of res.data) {
|
||||
if (!rec.openid || !rec.location) continue
|
||||
const dist = distanceMeters(userLoc, rec.location)
|
||||
if (dist <= RADIUS_METERS) near.push({ rec, dist })
|
||||
}
|
||||
if (res.data.length < 100) break
|
||||
}
|
||||
near.sort((a, b) => a.dist - b.dist)
|
||||
const top = near.slice(0, MAX_RESULTS)
|
||||
console.log('[nearbyPets] origin=', userLoc, 'visibleScanned=', scanned, 'inRadius=', near.length, 'nearestKm=', near[0] ? (near[0].dist / 1000).toFixed(2) : 'n/a')
|
||||
if (!top.length) return { list: [] }
|
||||
|
||||
const distByOpenid = new Map()
|
||||
const recByOpenid = new Map()
|
||||
const openids = []
|
||||
top.forEach(({ rec, dist }) => {
|
||||
if (recByOpenid.has(rec.openid)) return
|
||||
distByOpenid.set(rec.openid, dist)
|
||||
recByOpenid.set(rec.openid, rec)
|
||||
openids.push(rec.openid)
|
||||
})
|
||||
|
||||
return { list }
|
||||
// 2) Resolve those openids to user records.
|
||||
const users = await fetchByChunks('users', 'openid', openids)
|
||||
const userByOpenid = new Map(users.map(u => [u.openid, u]))
|
||||
const openidByUserId = new Map(users.map(u => [u._id, u.openid]))
|
||||
const ownerIds = users.map(u => u._id)
|
||||
if (!ownerIds.length) return { list: [] }
|
||||
|
||||
// 3) Their pets (optionally narrowed by species).
|
||||
let pets = await fetchByChunks('pets', 'ownerId', ownerIds)
|
||||
if (filter === 'dog' || filter === 'cat') pets = pets.filter(p => p.species === filter)
|
||||
|
||||
// 4) Which of these pets the caller already liked.
|
||||
const likedSet = new Set()
|
||||
const petIds = pets.map(p => p._id).filter(Boolean)
|
||||
if (OPENID && petIds.length) {
|
||||
const liked = await fetchByChunks('matches', 'petId', petIds).then(rows =>
|
||||
rows.filter(m => m.fromOpenid === OPENID)
|
||||
)
|
||||
liked.forEach(m => likedSet.add(m.petId))
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const list = pets
|
||||
.map(pet => {
|
||||
const ownerOpenid = openidByUserId.get(pet.ownerId)
|
||||
const rec = ownerOpenid ? recByOpenid.get(ownerOpenid) : null
|
||||
const coord = rec && rec.location ? rec.location : null
|
||||
const dist = ownerOpenid != null && distByOpenid.has(ownerOpenid) ? distByOpenid.get(ownerOpenid) : Infinity
|
||||
const owner = userByOpenid.get(ownerOpenid)
|
||||
const isFollowing = followingSet.has(pet.ownerId)
|
||||
const isFollowedBy = followerSet.has(pet.ownerId)
|
||||
return {
|
||||
...pet,
|
||||
ownerName: owner?.nickname || '',
|
||||
isMine: Boolean(meId && pet.ownerId === meId),
|
||||
isFollowing,
|
||||
isFriend: isFollowing && isFollowedBy,
|
||||
likedByMe: likedSet.has(pet._id),
|
||||
latitude: coord ? coord.latitude : undefined,
|
||||
longitude: coord ? coord.longitude : undefined,
|
||||
distanceText: distanceText(dist),
|
||||
online: rec ? now - (rec.updatedAt || 0) < ONLINE_WINDOW_MS : false,
|
||||
_dist: dist
|
||||
}
|
||||
})
|
||||
.sort((a, b) => a._dist - b._dist)
|
||||
|
||||
list.forEach(p => delete p._dist)
|
||||
|
||||
const finalList = filter === 'online' ? list.filter(p => p.online) : list
|
||||
console.log('[nearbyPets] nearbyUsers=', openids.length, 'matchedUsers=', users.length, 'pets=', pets.length, 'returned=', finalList.length)
|
||||
return { list: finalList }
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ const cloud = require('wx-server-sdk')
|
||||
|
||||
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
|
||||
const db = cloud.database()
|
||||
const MAX_PETS_PER_USER = 10
|
||||
|
||||
exports.main = async event => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
@@ -20,8 +21,12 @@ exports.main = async event => {
|
||||
return { petId: event.pet._id }
|
||||
}
|
||||
|
||||
const countRes = await db.collection('pets').where({ ownerId: users.data[0]._id }).count()
|
||||
if ((countRes.total || 0) >= MAX_PETS_PER_USER) {
|
||||
throw new Error(`最多只能添加 ${MAX_PETS_PER_USER} 只宠物`)
|
||||
}
|
||||
|
||||
pet.createdAt = Date.now()
|
||||
const result = await db.collection('pets').add({ data: pet })
|
||||
return { petId: result._id }
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ exports.main = async event => {
|
||||
const post = {
|
||||
authorOpenid: OPENID,
|
||||
authorId: user._id,
|
||||
authorSnapshot: { name: user.nickname, avatarKey: user.avatarKey },
|
||||
authorSnapshot: { name: user.nickname, avatarKey: user.avatarKey, avatarUrl: user.avatarUrl || '' },
|
||||
petId: event.petId || '',
|
||||
petSnapshot,
|
||||
content: content.slice(0, 2000),
|
||||
@@ -50,7 +50,7 @@ exports.main = async event => {
|
||||
? { latitude: event.latitude, longitude: event.longitude }
|
||||
: null,
|
||||
visibility: event.visibility || 'public',
|
||||
counts: { likes: 0, comments: 0, shares: 0, favorites: 0 },
|
||||
counts: { likes: 0, comments: 0, shares: 0 },
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}
|
||||
|
||||
66
cloudfunctions/postDelete/index.js
Normal file
66
cloudfunctions/postDelete/index.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const cloud = require('wx-server-sdk')
|
||||
|
||||
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
|
||||
const db = cloud.database()
|
||||
const _ = db.command
|
||||
|
||||
function isCloudFileId(v) {
|
||||
return typeof v === 'string' && v.startsWith('cloud://')
|
||||
}
|
||||
|
||||
function extractMediaId(m) {
|
||||
if (!m) return ''
|
||||
if (typeof m === 'string') return m
|
||||
return m.fileId || m.fileID || m.url || ''
|
||||
}
|
||||
|
||||
// Remove all docs matching `where`, tolerating a collection that was never created.
|
||||
async function safeRemoveWhere(collection, where) {
|
||||
try {
|
||||
await db.collection(collection).where(where).remove()
|
||||
} catch (e) {
|
||||
/* collection missing or nothing to remove */
|
||||
}
|
||||
}
|
||||
|
||||
exports.main = async event => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
if (!OPENID) throw new Error('no openid in wx context')
|
||||
if (!event.postId) throw new Error('postId is required')
|
||||
|
||||
let postRes
|
||||
try {
|
||||
postRes = await db.collection('posts').doc(event.postId).get()
|
||||
} catch (_) {
|
||||
return { ok: true } // already gone
|
||||
}
|
||||
const post = postRes.data
|
||||
if (!post) return { ok: true }
|
||||
if (post.authorOpenid !== OPENID) throw new Error('not allowed')
|
||||
|
||||
await db.collection('posts').doc(event.postId).remove()
|
||||
|
||||
// Decrement the author's post count (never below 0 in practice).
|
||||
if (post.authorId) {
|
||||
await db.collection('users').doc(post.authorId).update({
|
||||
data: { 'stats.posts': _.inc(-1) }
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// Best-effort cleanup of derived data so no orphans linger.
|
||||
await Promise.all([
|
||||
safeRemoveWhere('postLikes', { postId: event.postId }),
|
||||
safeRemoveWhere('postFavorites', { postId: event.postId }),
|
||||
safeRemoveWhere('comments', { postId: event.postId })
|
||||
])
|
||||
|
||||
// Best-effort removal of the post's uploaded media from storage.
|
||||
const fileList = (Array.isArray(post.media) ? post.media : [])
|
||||
.map(extractMediaId)
|
||||
.filter(isCloudFileId)
|
||||
if (fileList.length) {
|
||||
await cloud.deleteCloudFile({ fileList }).catch(() => {})
|
||||
}
|
||||
|
||||
return { ok: true }
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"name": "postFavorite",
|
||||
"name": "postDelete",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"wx-server-sdk": "latest"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,63 +29,116 @@ function formatTimeText(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), liked/favorited state.
|
||||
// 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
|
||||
let favoritedByMe = false
|
||||
if (OPENID) {
|
||||
const [likedRes, favRes] = await Promise.all([
|
||||
safeGet('postLikes', { openid: OPENID, postId }, 1),
|
||||
safeGet('postFavorites', { openid: OPENID, postId }, 1)
|
||||
])
|
||||
const likedRes = await safeGet('postLikes', { openid: OPENID, postId }, 1)
|
||||
likedByMe = likedRes.data.length > 0
|
||||
favoritedByMe = favRes.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: 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 || '', postTempUrlMap)
|
||||
},
|
||||
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: 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,
|
||||
favorites: post.counts?.favorites || 0
|
||||
comments: post.counts?.comments || 0
|
||||
},
|
||||
likedByMe,
|
||||
favoritedByMe
|
||||
likedByMe
|
||||
}
|
||||
|
||||
// Comments, oldest first. Resolve each author's current profile for avatars.
|
||||
const commentsRes = await safeGet('comments', { postId }, 200, { field: 'createdAt', direction: 'asc' })
|
||||
const commentAuthorIds = [...new Set(commentsRes.data.map(c => c.authorId).filter(Boolean))]
|
||||
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)
|
||||
@@ -93,20 +146,34 @@ exports.main = async event => {
|
||||
usersRes.data.forEach(u => authorMap.set(u._id, u))
|
||||
}
|
||||
|
||||
const comments = commentsRes.data.map(c => {
|
||||
const u = authorMap.get(c.authorId)
|
||||
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: c.authorId || '',
|
||||
id: (u && u._id) || c.authorId || '',
|
||||
name: (u && u.nickname) || c.authorSnapshot?.name || '用户',
|
||||
avatarKey: (u && u.avatarKey) || c.authorSnapshot?.avatarKey || 'gradient-avatar-1',
|
||||
avatarUrl: (u && u.avatarUrl) || c.authorSnapshot?.avatarUrl || ''
|
||||
avatarUrl: readableUrl((u && u.avatarUrl) || c.authorSnapshot?.avatarUrl || '', commentTempUrlMap)
|
||||
},
|
||||
content: c.content || '',
|
||||
timeText: formatTimeText(c.createdAt || Date.now())
|
||||
}
|
||||
})
|
||||
|
||||
return { post: mappedPost, comments }
|
||||
return { post: mappedPost, comments, commentsNextCursor }
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
const cloud = require('wx-server-sdk')
|
||||
|
||||
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
|
||||
const db = cloud.database()
|
||||
const _ = db.command
|
||||
|
||||
exports.main = async event => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
const { postId, favorited } = event
|
||||
if (!postId) throw new Error('postId is required')
|
||||
|
||||
const existed = await db.collection('postFavorites').where({ postId, openid: OPENID }).limit(1).get()
|
||||
let delta = 0
|
||||
|
||||
if (favorited && !existed.data.length) {
|
||||
await db.collection('postFavorites').add({ data: { postId, openid: OPENID, createdAt: Date.now() } })
|
||||
delta = 1
|
||||
}
|
||||
|
||||
if (!favorited && existed.data.length) {
|
||||
await db.collection('postFavorites').doc(existed.data[0]._id).remove()
|
||||
delta = -1
|
||||
}
|
||||
|
||||
if (delta) {
|
||||
await db.collection('posts').doc(postId).update({ data: { 'counts.favorites': _.inc(delta) } })
|
||||
}
|
||||
|
||||
const post = await db.collection('posts').doc(postId).get()
|
||||
return { favorited: Boolean(favorited), favorites: post.data.counts?.favorites || 0 }
|
||||
}
|
||||
|
||||
36
cloudfunctions/postUpdate/index.js
Normal file
36
cloudfunctions/postUpdate/index.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const cloud = require('wx-server-sdk')
|
||||
|
||||
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
|
||||
const db = cloud.database()
|
||||
|
||||
const ALLOWED_VISIBILITY = ['public', 'friends', 'private']
|
||||
|
||||
// Lightweight edit: only the post's text content and visibility can change here.
|
||||
exports.main = async event => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
if (!OPENID) throw new Error('no openid in wx context')
|
||||
if (!event.postId) throw new Error('postId is required')
|
||||
|
||||
let postRes
|
||||
try {
|
||||
postRes = await db.collection('posts').doc(event.postId).get()
|
||||
} catch (_) {
|
||||
throw new Error('post not found')
|
||||
}
|
||||
const post = postRes.data
|
||||
if (!post) throw new Error('post not found')
|
||||
if (post.authorOpenid !== OPENID) throw new Error('not allowed')
|
||||
|
||||
const data = { updatedAt: Date.now() }
|
||||
if (typeof event.content === 'string') {
|
||||
const content = event.content.trim()
|
||||
if (!content) throw new Error('content is required')
|
||||
data.content = content.slice(0, 2000)
|
||||
}
|
||||
if (typeof event.visibility === 'string' && ALLOWED_VISIBILITY.includes(event.visibility)) {
|
||||
data.visibility = event.visibility
|
||||
}
|
||||
|
||||
await db.collection('posts').doc(event.postId).update({ data })
|
||||
return { ok: true }
|
||||
}
|
||||
8
cloudfunctions/postUpdate/package.json
Normal file
8
cloudfunctions/postUpdate/package.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "postUpdate",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"wx-server-sdk": "latest"
|
||||
}
|
||||
}
|
||||
@@ -16,13 +16,12 @@ async function safeCount(collection, where) {
|
||||
// Derive live stats from the source collections so the displayed numbers are
|
||||
// always accurate instead of relying on a stored field that can drift.
|
||||
async function computeStats(user) {
|
||||
const [posts, favorites, following, followers] = await Promise.all([
|
||||
const [posts, following, followers] = await Promise.all([
|
||||
safeCount('posts', { authorId: user._id }),
|
||||
safeCount('postFavorites', { openid: user.openid }),
|
||||
safeCount('follows', { followerId: user._id }),
|
||||
safeCount('follows', { followeeId: user._id })
|
||||
])
|
||||
return { posts, following, followers, favorites }
|
||||
return { posts, following, followers }
|
||||
}
|
||||
|
||||
exports.main = async event => {
|
||||
|
||||
@@ -26,11 +26,44 @@ function formatTimeText(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) || ''
|
||||
}
|
||||
|
||||
// List a user's own posts. Defaults to the caller; pass authorId to view
|
||||
// another user (only public posts are returned for others).
|
||||
exports.main = async event => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
if (!OPENID) throw new Error('no openid in wx context')
|
||||
const pageSize = Math.max(1, Math.min(Number(event.pageSize || 20), 50))
|
||||
|
||||
let authorOpenid = OPENID
|
||||
let viewingSelf = true
|
||||
@@ -43,35 +76,59 @@ exports.main = async event => {
|
||||
|
||||
const where = { authorOpenid }
|
||||
if (!viewingSelf) where.visibility = 'public'
|
||||
const cursor = Number(event.cursor || 0)
|
||||
if (Number.isFinite(cursor) && cursor > 0) where.createdAt = _.lt(cursor)
|
||||
|
||||
const res = await db.collection('posts')
|
||||
.where(where)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.limit(50)
|
||||
.limit(pageSize + 1)
|
||||
.get()
|
||||
|
||||
if (!res.data.length) return { list: [] }
|
||||
if (!res.data.length) return { list: [], nextCursor: '' }
|
||||
|
||||
const postIds = res.data.map(p => p._id)
|
||||
const [likedRes, favRes] = await Promise.all([
|
||||
safeGet('postLikes', { openid: OPENID, postId: _.in(postIds) }),
|
||||
safeGet('postFavorites', { openid: OPENID, postId: _.in(postIds) })
|
||||
])
|
||||
const page = res.data.slice(0, pageSize)
|
||||
const nextCursor = res.data.length > pageSize && page[page.length - 1]
|
||||
? String(page[page.length - 1].createdAt || '')
|
||||
: ''
|
||||
|
||||
const authorRes = await db.collection('users').where({ openid: authorOpenid }).limit(1).get().catch(() => null)
|
||||
const authorUser = authorRes && authorRes.data ? authorRes.data[0] : null
|
||||
|
||||
// Real presence: author has a recent, visible location (same signal as 附近).
|
||||
const ONLINE_WINDOW_MS = 5 * 60 * 1000
|
||||
const locRes = await safeGet('locations', { openid: authorOpenid }, 1)
|
||||
const authorLoc = locRes.data[0]
|
||||
const authorOnline = Boolean(
|
||||
authorLoc && authorLoc.visible && authorLoc.updatedAt && Date.now() - authorLoc.updatedAt < ONLINE_WINDOW_MS
|
||||
)
|
||||
const postIds = page.map(p => p._id)
|
||||
const likedRes = await safeGet('postLikes', { openid: OPENID, postId: _.in(postIds) })
|
||||
const likedSet = new Set(likedRes.data.map(l => l.postId))
|
||||
const favSet = new Set(favRes.data.map(f => f.postId))
|
||||
const assetUrls = [
|
||||
authorUser?.avatarUrl || '',
|
||||
...page.flatMap(post => Array.isArray(post.media) ? post.media.map(extractMediaUrl) : [])
|
||||
]
|
||||
const tempUrlMap = await resolveTempUrlMap(assetUrls)
|
||||
|
||||
const list = res.data.map(post => ({
|
||||
const list = page.map(post => ({
|
||||
_id: post._id,
|
||||
author: {
|
||||
id: post.authorId || '',
|
||||
name: post.authorSnapshot?.name || '用户',
|
||||
avatarKey: post.authorSnapshot?.avatarKey || 'gradient-avatar-1'
|
||||
avatarKey: authorUser?.avatarKey || post.authorSnapshot?.avatarKey || 'gradient-avatar-1',
|
||||
avatarUrl: readableUrl(authorUser?.avatarUrl || post.authorSnapshot?.avatarUrl || '', tempUrlMap),
|
||||
online: authorOnline
|
||||
},
|
||||
pet: post.petSnapshot || { name: '', breed: '' },
|
||||
body: post.content || '',
|
||||
topics: post.topics || [],
|
||||
visibility: post.visibility || 'public',
|
||||
media: Array.isArray(post.media)
|
||||
? post.media.map(m => (typeof m === 'string' ? m : m && (m.url || m.fileId))).filter(Boolean)
|
||||
? post.media
|
||||
.map(extractMediaUrl)
|
||||
.map(url => readableUrl(url, tempUrlMap))
|
||||
.filter(Boolean)
|
||||
: [],
|
||||
mediaTone: post.mediaTone || 'pet-1',
|
||||
sticker: post.sticker || undefined,
|
||||
@@ -79,12 +136,10 @@ exports.main = async event => {
|
||||
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)
|
||||
}))
|
||||
|
||||
return { list }
|
||||
return { list, nextCursor }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user