const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() const _ = db.command // 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 < 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 } // 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 = ((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((a.latitude * Math.PI) / 180) * Math.cos((b.latitude * Math.PI) / 180) * Math.sin(dLon / 2) ** 2 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 // 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 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([ 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)) } } // 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) }) // 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 } }