This commit is contained in:
2026-06-18 14:33:50 +08:00
commit 0e43ccec72
248 changed files with 30329 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
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()
if (!event.conversationId) throw new Error('conversationId is required')
await db.collection('conversations').doc(event.conversationId).update({
data: {
unreadOpenids: _.pull(OPENID),
[`unreadCounts.${OPENID}`]: 0
}
})
return { unreadCount: 0 }
}

View File

@@ -0,0 +1,9 @@
{
"name": "conversationRead",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,13 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
exports.main = async () => {
const { OPENID } = cloud.getWXContext()
if (!OPENID) throw new Error('no openid in wx context')
const res = await db.collection('drafts').where({ openid: OPENID }).limit(1).get()
if (!res.data.length) return { draft: null }
const record = res.data[0]
return { draft: record.draft || null, updatedAt: record.updatedAt }
}

View File

@@ -0,0 +1,8 @@
{
"name": "draftGet",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,24 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
exports.main = async event => {
const { OPENID } = cloud.getWXContext()
const now = Date.now()
const data = {
openid: OPENID,
draft: event.draft || {},
updatedAt: now
}
const existed = await db.collection('drafts').where({ openid: OPENID }).limit(1).get()
if (existed.data.length) {
await db.collection('drafts').doc(existed.data[0]._id).update({ data })
return { draftId: existed.data[0]._id, updatedAt: now }
}
const result = await db.collection('drafts').add({ data })
return { draftId: result._id, updatedAt: now }
}

View File

@@ -0,0 +1,9 @@
{
"name": "draftSave",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,105 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const _ = db.command
// Query a collection that may not exist yet (created lazily on first write).
// Returns an empty result instead of throwing "collection not exists".
async function safeGet(collection, where, limit = 200) {
try {
return await db.collection(collection).where(where).limit(limit).get()
} catch (e) {
return { data: [] }
}
}
function formatTimeText(ts) {
const diff = Date.now() - ts
const mins = Math.floor(diff / 60000)
if (mins < 1) return '刚刚'
if (mins < 60) return `${mins} 分钟前`
const hours = Math.floor(mins / 60)
if (hours < 24) return `${hours} 小时前`
const days = Math.floor(hours / 24)
if (days === 1) return '昨天'
if (days < 7) return `${days} 天前`
const d = new Date(ts)
return `${d.getMonth() + 1}${d.getDate()}`
}
exports.main = async event => {
const { OPENID } = cloud.getWXContext()
const pageSize = Math.min(event.pageSize || 20, 50)
const query = { visibility: 'public' }
if (event.topic) query.topics = event.topic
// Follow channel: only posts from users I follow (plus my own).
if (event.channel === 'follow') {
if (!OPENID) return { list: [], nextCursor: '' }
const meRes = await db.collection('users').where({ openid: OPENID }).limit(1).get()
if (!meRes.data.length) return { list: [], nextCursor: '' }
const meId = meRes.data[0]._id
const followRes = await safeGet('follows', { followerId: meId }, 500)
const authorOpenids = followRes.data.map(f => f.followeeOpenid).filter(Boolean)
authorOpenids.push(OPENID)
query.authorOpenid = _.in(authorOpenids)
query.visibility = _.in(['public', 'friends'])
}
if (event.cursor) {
query.createdAt = _.lt(Number(event.cursor))
}
const res = await db.collection('posts')
.where(query)
.orderBy('createdAt', 'desc')
.limit(pageSize)
.get()
if (!res.data.length) return { list: [], nextCursor: '' }
const postIds = res.data.map(p => p._id)
// Batch-check liked/favorited 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) })
])
likedSet = new Set(likedRes.data.map(l => l.postId))
favSet = new Set(favRes.data.map(f => f.postId))
}
const list = res.data.map(post => ({
_id: post._id,
author: {
id: post.authorId || '',
name: post.authorSnapshot?.name || '用户',
avatarKey: post.authorSnapshot?.avatarKey || 'gradient-avatar-1'
},
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)
: [],
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
},
likedByMe: likedSet.has(post._id),
favoritedByMe: favSet.has(post._id)
}))
const last = res.data[res.data.length - 1]
return { list, nextCursor: last ? String(last.createdAt) : '' }
}

View File

@@ -0,0 +1,9 @@
{
"name": "feedList",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,63 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const _ = db.command
async function fetchUsers(ids) {
if (!ids.length) return []
const out = []
// _.in supports up to ~100 values; chunk to be safe
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()
out.push(...res.data)
}
return out
}
exports.main = async event => {
const { OPENID } = cloud.getWXContext()
if (!OPENID) throw new Error('no openid in wx context')
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
const type = event.type || 'friends'
const safeFollows = where =>
db.collection('follows').where(where).limit(500).get().catch(() => ({ data: [] }))
const [followingRes, followerRes] = await Promise.all([
safeFollows({ followerId: meId }),
safeFollows({ followeeId: meId })
])
const followingIds = followingRes.data.map(f => f.followeeId)
const followerIds = followerRes.data.map(f => f.followerId)
const followingSet = new Set(followingIds)
const followerSet = new Set(followerIds)
const friendIds = followingIds.filter(id => followerSet.has(id))
let ids = friendIds
if (type === 'following') ids = followingIds
else if (type === 'followers') ids = followerIds
const users = await fetchUsers(ids)
const list = users.map(u => ({
id: u._id,
name: u.nickname || '用户',
handle: u.handle || '',
avatarKey: u.avatarKey || 'gradient-avatar-1',
avatarUrl: u.avatarUrl || '',
bio: u.bio || '',
isFollowing: followingSet.has(u._id),
isFriend: followingSet.has(u._id) && followerSet.has(u._id)
}))
return {
list,
counts: {
following: followingIds.length,
followers: followerIds.length,
friends: friendIds.length
}
}
}

View File

@@ -0,0 +1,8 @@
{
"name": "followList",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,47 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
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')
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
if (meId === targetUserId) throw new Error('cannot follow yourself')
const targetRes = await db.collection('users').doc(targetUserId).get().catch(() => null)
const target = targetRes && targetRes.data
if (!target) throw new Error('target user not found')
const existing = await db.collection('follows')
.where({ followerId: meId, followeeId: targetUserId })
.limit(1)
.get()
if (follow && !existing.data.length) {
await db.collection('follows').add({
data: {
followerId: meId,
followerOpenid: OPENID,
followeeId: targetUserId,
followeeOpenid: target.openid,
createdAt: Date.now()
}
})
} else if (!follow && existing.data.length) {
await db.collection('follows').doc(existing.data[0]._id).remove()
}
// A friendship (汪友) is a mutual follow.
const back = await db.collection('follows')
.where({ followerId: targetUserId, followeeId: meId })
.limit(1)
.get()
return { following: Boolean(follow), mutual: Boolean(follow) && back.data.length > 0 }
}

View File

@@ -0,0 +1,8 @@
{
"name": "followToggle",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,21 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
exports.main = async event => {
const { OPENID } = cloud.getWXContext()
const now = Date.now()
const data = {
openid: OPENID,
location: event.location || null,
visible: Boolean(event.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 }
}

View File

@@ -0,0 +1,9 @@
{
"name": "locationUpdate",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,59 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
// Generate a unique handle from openid
function makeHandle(openid) {
const suffix = openid.slice(-6).toLowerCase()
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'
]
exports.main = async event => {
const { OPENID } = cloud.getWXContext()
if (!OPENID) throw new Error('no openid in wx context')
const now = Date.now()
const existed = await db.collection('users').where({ openid: OPENID }).limit(1).get()
if (existed.data.length) {
const user = existed.data[0]
await db.collection('users').doc(user._id).update({
data: { lastLoginAt: now }
})
return {
openid: OPENID,
user: { ...user, lastLoginAt: now },
isNew: false
}
}
// New user — create with sensible defaults
const avatarKey = AVATAR_KEYS[Math.floor(Math.random() * AVATAR_KEYS.length)]
const user = {
openid: OPENID,
nickname: event.userInfo?.nickname || '宠物达人',
handle: makeHandle(OPENID),
avatarKey,
avatarUrl: '',
profileCompleted: false,
location: '',
yearsWithPets: 0,
level: 1,
bio: '',
verified: false,
stats: { posts: 0, following: 0, followers: 0, favorites: 0 },
preferences: { notifications: true, nearbyVisible: true },
createdAt: now,
lastLoginAt: now
}
const result = await db.collection('users').add({ data: user })
return { openid: OPENID, user: { _id: result._id, ...user }, isNew: true }
}

View File

@@ -0,0 +1,9 @@
{
"name": "login",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,46 @@
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 { petId } = event
if (!petId) throw new Error('petId is required')
// 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')
const userId = userRes.data[0]._id
// Record this like (idempotent)
const existed = await db.collection('matches').where({ fromOpenid: OPENID, petId }).limit(1).get()
if (!existed.data.length) {
await db.collection('matches').add({ data: { fromOpenid: OPENID, userId, petId, createdAt: Date.now() } })
}
// Check mutual match: does the target pet's owner like any of current user's pets?
let matched = false
try {
const targetPetRes = await db.collection('pets').doc(petId).get()
const targetOwnerId = targetPetRes.data?.ownerId
if (targetOwnerId && targetOwnerId !== userId) {
const targetOwnerRes = await db.collection('users').doc(targetOwnerId).get()
const targetOpenid = targetOwnerRes.data?.openid
if (targetOpenid) {
const myPetsRes = await db.collection('pets').where({ ownerId: userId }).limit(20).get()
const myPetIds = myPetsRes.data.map(p => p._id)
if (myPetIds.length) {
const reverseRes = await db.collection('matches')
.where({ fromOpenid: targetOpenid, petId: _.in(myPetIds) })
.limit(1)
.get()
matched = reverseRes.data.length > 0
}
}
}
} catch (_) {}
return { matched }
}

View File

@@ -0,0 +1,9 @@
{
"name": "matchLike",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,101 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const _ = db.command
function formatTimeText(ts) {
if (!ts) return ''
const diff = Date.now() - ts
const mins = Math.floor(diff / 60000)
if (mins < 1) return '现在'
if (mins < 60) return `${mins} 分钟前`
const hours = Math.floor(mins / 60)
if (hours < 24) return `${hours} 小时前`
const days = Math.floor(hours / 24)
if (days === 1) return '昨天'
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
if (days < 7) return weekdays[new Date(ts).getDay()]
const d = new Date(ts)
return `${d.getMonth() + 1}/${d.getDate()}`
}
// Query a collection that may not exist yet (created lazily on first write).
async function safeGet(collection, where, limit = 200) {
try {
return await db.collection(collection).where(where).limit(limit).get()
} catch (e) {
return { data: [] }
}
}
async function fetchUsers(ids) {
if (!ids.length) return []
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()
out.push(...res.data)
}
return out
}
exports.main = async event => {
const { OPENID } = cloud.getWXContext()
if (!OPENID) throw new Error('no openid in wx context')
const tab = event.tab || 'all'
const query = { memberOpenids: OPENID }
if (tab === 'groups') query.type = 'group'
if (tab === 'unread') query.unreadOpenids = OPENID
const meRes = await db.collection('users').where({ openid: OPENID }).limit(1).get()
const meId = meRes.data[0] && meRes.data[0]._id
// Active row = my 汪友 (mutual follows)
let friendUsers = []
if (meId) {
const [followingRes, followerRes] = await Promise.all([
safeGet('follows', { followerId: meId }, 500),
safeGet('follows', { followeeId: meId }, 500)
])
const followerSet = new Set(followerRes.data.map(f => f.followerId))
const friendIds = followingRes.data.map(f => f.followeeId).filter(id => followerSet.has(id))
friendUsers = await fetchUsers(friendIds)
}
let convRes
try {
convRes = await db.collection('conversations').where(query).orderBy('updatedAt', 'desc').limit(50).get()
} catch (e) {
convRes = { data: [] }
}
const conversations = convRes.data.map(c => {
const unreadOpenids = c.unreadOpenids || []
const unreadCounts = c.unreadCounts || {}
const unreadCount = unreadCounts[OPENID] || (unreadOpenids.includes(OPENID) ? 1 : 0)
return {
_id: c._id,
type: c.type || 'single',
title: c.title || '未知用户',
petName: c.petName || undefined,
avatarKey: c.avatarKey || 'gradient-avatar-1',
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)
}
})
const activeUsers = friendUsers.map(u => ({
id: u._id,
name: u.nickname || '汪友',
avatarKey: u.avatarKey || 'gradient-avatar-1',
avatarUrl: u.avatarUrl || '',
live: Boolean(u.online)
}))
return { activeUsers, conversations }
}

View File

@@ -0,0 +1,9 @@
{
"name": "messageList",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,88 @@
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 content = String(event.content || '').trim()
if (!content) throw new Error('content is required')
const now = Date.now()
let convId = event.conversationId
// Create a 1:1 conversation if not provided
if (!convId) {
let { toOpenid } = event
// Allow opening by user id without exposing openids to the client.
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) throw new Error('conversationId or toOpenid/toUserId is required')
// Look for existing conversation between these two users
const existing = await db.collection('conversations')
.where({ type: 'single', memberOpenids: _.all([OPENID, toOpenid]) })
.limit(1)
.get()
if (existing.data.length) {
convId = existing.data[0]._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
}
})
convId = newConv._id
}
}
// 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
}
})
// 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 = {
lastMessage: { content: content.slice(0, 50), createdAt: now },
updatedAt: now
}
// Add recipients to unreadOpenids and increment their unread counts
recipients.forEach(r => {
updateData[`unreadCounts.${r}`] = _.inc(1)
})
// Rebuild unreadOpenids to include all recipients (remove duplicates)
const existingUnread = convRes.data.unreadOpenids || []
const newUnread = [...new Set([...existingUnread, ...recipients])]
updateData.unreadOpenids = newUnread
await db.collection('conversations').doc(convId).update({ data: updateData })
return { messageId: msgResult._id, conversationId: convId }
}

View File

@@ -0,0 +1,8 @@
{
"name": "messageSend",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,82 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const _ = db.command
function timeText(ts) {
if (!ts) return ''
const d = new Date(ts)
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
const sameDay = new Date().toDateString() === d.toDateString()
if (sameDay) return `${hh}:${mm}`
return `${d.getMonth() + 1}/${d.getDate()} ${hh}:${mm}`
}
async function userToPeer(u) {
return { id: u._id, name: u.nickname || '用户', avatarKey: u.avatarKey || 'gradient-avatar-1', avatarUrl: u.avatarUrl || '' }
}
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
// Open by target user — resolve peer and find an existing conversation.
if (!convId && event.targetUserId) {
const tRes = await db.collection('users').doc(event.targetUserId).get().catch(() => null)
const t = tRes && tRes.data
if (t) {
peer = await userToPeer(t)
const ex = await db.collection('conversations')
.where({ type: 'single', memberOpenids: _.all([OPENID, t.openid]) })
.limit(1)
.get()
.catch(() => ({ data: [] }))
if (ex.data.length) convId = ex.data[0]._id
}
}
// Open by conversation — resolve the other member as peer.
if (convId && !peer) {
const c = await db.collection('conversations').doc(convId).get().catch(() => null)
if (c && c.data) {
const otherOpenid = (c.data.memberOpenids || []).find(o => o !== OPENID)
if (otherOpenid) {
const oRes = await db.collection('users').where({ openid: otherOpenid }).limit(1).get()
if (oRes.data[0]) peer = await userToPeer(oRes.data[0])
}
if (!peer) {
peer = { id: '', name: c.data.title || '对话', avatarKey: c.data.avatarKey || 'gradient-avatar-1', avatarUrl: '' }
}
}
}
let messages = []
if (convId) {
const res = await db.collection('messages')
.where({ conversationId: convId })
.orderBy('createdAt', 'desc')
.limit(50)
.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)
}))
// Mark this conversation read for me.
await db.collection('conversations').doc(convId).update({
data: { unreadOpenids: _.pull(OPENID), [`unreadCounts.${OPENID}`]: 0 }
}).catch(() => {})
}
return { conversationId: convId, peer, messages }
}

View File

@@ -0,0 +1,8 @@
{
"name": "messageThread",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,70 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
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
const R = 6371000
const dLat = ((petLoc.latitude - userLoc.latitude) * Math.PI) / 180
const dLon = ((petLoc.longitude - userLoc.longitude) * Math.PI) / 180
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos((userLoc.latitude * Math.PI) / 180) *
Math.cos((petLoc.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`
}
exports.main = async event => {
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
const res = await db.collection('pets').where(query).limit(50).get()
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)
return {
...pet,
latitude: coord ? coord.latitude : undefined,
longitude: coord ? coord.longitude : undefined,
distanceText: distanceText(coord, userLoc) || pet.distanceText || null,
pin: stablePin(pet._id)
}
})
return { list }
}

View File

@@ -0,0 +1,9 @@
{
"name": "nearbyPets",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,26 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
exports.main = async event => {
const { OPENID } = cloud.getWXContext()
if (!OPENID) throw new Error('no openid in wx context')
if (!event.petId) throw new Error('petId is required')
const users = await db.collection('users').where({ openid: OPENID }).limit(1).get()
if (!users.data.length) throw new Error('user not found')
const ownerId = users.data[0]._id
let pet
try {
pet = await db.collection('pets').doc(event.petId).get()
} catch (_) {
return { ok: true } // already gone
}
if (!pet.data) return { ok: true }
if (pet.data.ownerId !== ownerId) throw new Error('not allowed')
await db.collection('pets').doc(event.petId).remove()
return { ok: true }
}

View File

@@ -0,0 +1,8 @@
{
"name": "petDelete",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,17 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
exports.main = async event => {
const { OPENID } = cloud.getWXContext()
let ownerId = event.ownerId
if (!ownerId) {
const user = await db.collection('users').where({ openid: OPENID }).limit(1).get()
ownerId = user.data[0]?._id
}
const res = ownerId ? await db.collection('pets').where({ ownerId }).limit(20).get() : { data: [] }
return { list: res.data }
}

View File

@@ -0,0 +1,9 @@
{
"name": "petList",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,27 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
exports.main = async event => {
const { OPENID } = cloud.getWXContext()
const users = await db.collection('users').where({ openid: OPENID }).limit(1).get()
if (!users.data.length) throw new Error('user not found')
const pet = {
...(event.pet || {}),
ownerId: users.data[0]._id,
updatedAt: Date.now()
}
delete pet._id
if (event.pet?._id) {
await db.collection('pets').doc(event.pet._id).update({ data: pet })
return { petId: event.pet._id }
}
pet.createdAt = Date.now()
const result = await db.collection('pets').add({ data: pet })
return { petId: result._id }
}

View File

@@ -0,0 +1,9 @@
{
"name": "petSave",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,65 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const _ = db.command
const MEDIA_TONES = ['pet-1', 'pet-2', 'pet-3']
const PET_TONES = { dog: 'gold', cat: 'purple', other: 'green' }
exports.main = async event => {
const { OPENID } = cloud.getWXContext()
const now = Date.now()
const content = String(event.content || '').trim()
if (!content) throw new Error('content is required')
// Get author info first (required for snapshot)
const userRes = await db.collection('users').where({ openid: OPENID }).limit(1).get()
if (!userRes.data.length) throw new Error('user not found')
const user = userRes.data[0]
// Get pet snapshot if petId provided
let petSnapshot = null
if (event.petId) {
try {
const petRes = await db.collection('pets').doc(event.petId).get()
if (petRes.data) {
const p = petRes.data
petSnapshot = { name: p.name, breed: p.breed, tone: PET_TONES[p.species] || 'green' }
}
} catch (_) {}
}
// Derive mediaTone from media count so similar posts look varied
const mediaCount = Array.isArray(event.media) ? event.media.length : 0
const mediaTone = event.mediaTone || MEDIA_TONES[(mediaCount + now) % 3]
const post = {
authorOpenid: OPENID,
authorId: user._id,
authorSnapshot: { name: user.nickname, avatarKey: user.avatarKey },
petId: event.petId || '',
petSnapshot,
content: content.slice(0, 2000),
topics: Array.isArray(event.topics) ? event.topics : [],
media: Array.isArray(event.media) ? event.media : [],
mediaTone,
sticker: event.sticker || null,
locationText: event.locationText || '',
location: (typeof event.latitude === 'number' && typeof event.longitude === 'number')
? { latitude: event.latitude, longitude: event.longitude }
: null,
visibility: event.visibility || 'public',
counts: { likes: 0, comments: 0, shares: 0, favorites: 0 },
createdAt: now,
updatedAt: now
}
const result = await db.collection('posts').add({ data: post })
await db.collection('users').doc(user._id).update({
data: { 'stats.posts': _.inc(1) }
})
return { postId: result._id }
}

View File

@@ -0,0 +1,9 @@
{
"name": "postCreate",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,32 @@
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 }
}

View File

@@ -0,0 +1,9 @@
{
"name": "postFavorite",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,32 @@
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, liked } = event
if (!postId) throw new Error('postId is required')
const existed = await db.collection('postLikes').where({ postId, openid: OPENID }).limit(1).get()
let delta = 0
if (liked && !existed.data.length) {
await db.collection('postLikes').add({ data: { postId, openid: OPENID, createdAt: Date.now() } })
delta = 1
}
if (!liked && existed.data.length) {
await db.collection('postLikes').doc(existed.data[0]._id).remove()
delta = -1
}
if (delta) {
await db.collection('posts').doc(postId).update({ data: { 'counts.likes': _.inc(delta) } })
}
const post = await db.collection('posts').doc(postId).get()
return { liked: Boolean(liked), likes: post.data.counts?.likes || 0 }
}

View File

@@ -0,0 +1,9 @@
{
"name": "postLike",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,40 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
// Count a collection safely — returns 0 if the collection does not exist yet.
async function safeCount(collection, where) {
try {
const res = await db.collection(collection).where(where).count()
return res.total || 0
} catch (e) {
return 0
}
}
// 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([
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 }
}
exports.main = async event => {
const { OPENID } = cloud.getWXContext()
if (!event.userId && !OPENID) throw new Error('no openid in wx context')
const query = event.userId ? { _id: event.userId } : { openid: OPENID }
const users = await db.collection('users').where(query).limit(1).get()
const user = users.data[0] || null
if (!user) return { user: null, pets: [] }
const pets = await db.collection('pets').where({ ownerId: user._id }).limit(20).get()
user.stats = await computeStats(user)
return { user, pets: pets.data }
}

View File

@@ -0,0 +1,9 @@
{
"name": "profileGet",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,28 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
exports.main = async event => {
const { OPENID } = cloud.getWXContext()
if (!OPENID) throw new Error('no openid in wx context')
const patch = event.patch || {}
delete patch.openid
delete patch._id
const users = await db.collection('users').where({ openid: OPENID }).limit(1).get()
if (!users.data.length) throw new Error('user not found')
// Setting a real avatar means the user has completed WeChat profile authorization
if (patch.avatarUrl) patch.profileCompleted = true
await db.collection('users').doc(users.data[0]._id).update({
data: {
...patch,
updatedAt: Date.now()
}
})
const user = await db.collection('users').doc(users.data[0]._id).get()
return { user: user.data }
}

View File

@@ -0,0 +1,9 @@
{
"name": "profileUpdate",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -0,0 +1,46 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const _ = db.command
// Discover other users to follow. Optional keyword filters by nickname.
exports.main = async event => {
const { OPENID } = cloud.getWXContext()
if (!OPENID) throw new Error('no openid in wx context')
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
const keyword = String(event.keyword || '').trim()
const where = { _id: _.neq(meId) }
if (keyword) where.nickname = db.RegExp({ regexp: keyword, options: 'i' })
const usersRes = await db.collection('users')
.where(where)
.orderBy('lastLoginAt', 'desc')
.limit(30)
.get()
const safeFollows = where2 =>
db.collection('follows').where(where2).limit(500).get().catch(() => ({ data: [] }))
const [followingRes, followerRes] = await Promise.all([
safeFollows({ followerId: meId }),
safeFollows({ followeeId: meId })
])
const followingSet = new Set(followingRes.data.map(f => f.followeeId))
const followerSet = new Set(followerRes.data.map(f => f.followerId))
const list = usersRes.data.map(u => ({
id: u._id,
name: u.nickname || '用户',
handle: u.handle || '',
avatarKey: u.avatarKey || 'gradient-avatar-1',
avatarUrl: u.avatarUrl || '',
bio: u.bio || '',
isFollowing: followingSet.has(u._id),
isFriend: followingSet.has(u._id) && followerSet.has(u._id)
}))
return { list }
}

View File

@@ -0,0 +1,8 @@
{
"name": "userList",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}