修复动态发布与互动问题
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user