chore: 补全工程配置,部署云函数,建立数据库索引

- 新增 .gitignore(排除 node_modules、私有配置、编译产物)
- 新增 cloudbaserc.json,绑定云环境 cloud1-d4g697lte499543d8
- project.config.json:补 miniprogramRoot、开启 useCompilerPlugins typescript,修复预览报 app.json 找不到的问题
- tsconfig.json:移除无效 typeRoots,解除 TypeScript 编译阻断
- miniprogram/app.json:移除 glass-easel(需基础库 3.x,兼容性差)
- miniprogram/utils/constants.ts:填入真实云环境 ID
- cloudfunctions/updateLocation:位置字段改用 GeoPoint 以支持地理索引
- cloudfunctions/getNearbyPets:改用 geoNear 聚合管道,支持真实距离排序

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 00:37:33 +08:00
parent c04c890186
commit 24891d9004
8 changed files with 161 additions and 74 deletions

20
.gitignore vendored Normal file
View File

@@ -0,0 +1,20 @@
# 依赖
node_modules/
**/node_modules/
# 云函数锁文件(可选提交,这里统一忽略)
cloudfunctions/**/package-lock.json
# 微信开发者工具私有配置(含 token不入库
project.private.config.json
**/project.private.config.json
# 开发者工具在 srcMiniprogramRoot 内生成的重复配置
miniprogram/project.config.json
# 编译产物
miniprogram/**/*.js.map
# 系统文件
.DS_Store
Thumbs.db

63
cloudbaserc.json Normal file
View File

@@ -0,0 +1,63 @@
{
"$schema": "https://static.cloudbase.net/cli/cloudbaserc.schema.json",
"envId": "cloud1-d4g697lte499543d8",
"functionRoot": "cloudfunctions",
"functions": [
{
"name": "login",
"timeout": 10,
"runtime": "Nodejs20.19",
"handler": "index.main",
"description": "微信登录 & 用户注册"
},
{
"name": "createPost",
"timeout": 15,
"runtime": "Nodejs20.19",
"handler": "index.main",
"description": "发布帖子"
},
{
"name": "getPosts",
"timeout": 10,
"runtime": "Nodejs20.19",
"handler": "index.main",
"description": "获取信息流帖子列表"
},
{
"name": "getPost",
"timeout": 10,
"runtime": "Nodejs20.19",
"handler": "index.main",
"description": "获取单条帖子详情"
},
{
"name": "likePost",
"timeout": 10,
"runtime": "Nodejs20.19",
"handler": "index.main",
"description": "点赞 / 取消点赞"
},
{
"name": "getNearbyPets",
"timeout": 15,
"runtime": "Nodejs20.19",
"handler": "index.main",
"description": "附近宠物地理位置查询"
},
{
"name": "updateLocation",
"timeout": 10,
"runtime": "Nodejs20.19",
"handler": "index.main",
"description": "更新用户地理位置GeoPoint"
},
{
"name": "sendGreeting",
"timeout": 10,
"runtime": "Nodejs20.19",
"handler": "index.main",
"description": "打招呼 / 发起私信"
}
]
}

View File

@@ -3,74 +3,64 @@ cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database() const db = cloud.database()
const _ = db.command const _ = db.command
// 粗略经纬度范围换算1度≈111km
function latDelta(km) { return km / 111 }
function lngDelta(km, lat) { return km / (111 * Math.cos(lat * Math.PI / 180)) }
function calcDistance(lat1, lng1, lat2, lng2) {
const R = 6371
const dLat = (lat2 - lat1) * Math.PI / 180
const dLng = (lng2 - lng1) * Math.PI / 180
const a = Math.sin(dLat / 2) ** 2 +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng / 2) ** 2
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) * 1000 // meters
}
exports.main = async (event, context) => { exports.main = async (event, context) => {
const { OPENID } = cloud.getWXContext() const { OPENID } = cloud.getWXContext()
const { latitude, longitude, radiusKm = 5 } = event const {
latitude,
longitude,
maxDistance = 5000, // 默认 5km单位
page = 0,
pageSize = 20,
} = event
if (!latitude || !longitude) { if (!latitude || !longitude) return { code: -1, message: '缺少位置' }
return { code: -1, message: '缺少位置信息' }
}
// 在线判断60秒内更新过位置的用户为在线
const onlineThreshold = new Date(Date.now() - 60 * 1000).toISOString()
try { try {
const dLat = latDelta(radiusKm) // ✅ geoNear 聚合管道:依赖 users.lastLocation 字段上的 2dsphere 地理位置索引
const dLng = lngDelta(radiusKm, latitude) const { list } = await db.collection('users')
.aggregate()
const usersRes = await db.collection('users') .geoNear({
.where( near: db.Geo.Point(longitude, latitude), // 注意CloudBase GeoPoint 是 (lng, lat) 顺序
_.and([ spherical: true,
{ openid: _.neq(OPENID) }, // 排除自己 distanceField: 'dist', // 在每条记录上附加实际距离(米)
{ 'lastLocation.latitude': _.gt(latitude - dLat).and(_.lt(latitude + dLat)) }, maxDistance,
{ 'lastLocation.longitude': _.gt(longitude - dLng).and(_.lt(longitude + dLng)) }, query: {
{ pets: _.exists(true) }, openid: _.neq(OPENID), // 排除自己
]) 'pets.0': _.exists(true), // 至少有一只宠物
) },
.field({
openid: true, nickName: true, avatarUrl: true,
lastLocation: true, lastSeen: true, isOnline: true,
pets: true, location: true, bio: true,
}) })
.limit(30) .skip(page * pageSize)
.get() .limit(pageSize)
.project({
const nearby = usersRes.data openid: false,
.map(user => {
const userLat = user.lastLocation?.latitude
const userLng = user.lastLocation?.longitude
if (!userLat || !userLng) return null
const distance = calcDistance(latitude, longitude, userLat, userLng)
if (distance > radiusKm * 1000) return null
return {
userId: user.openid,
user,
pet: user.pets?.[0] || null,
distance,
isOnline: user.isOnline && user.lastSeen > onlineThreshold,
location: { latitude: userLat, longitude: userLng },
}
}) })
.filter(Boolean) .end()
.sort((a, b) => a.distance - b.distance)
return { code: 0, data: nearby } // 在线状态5 分钟内活跃
const fiveMin = 5 * 60 * 1000
const now = Date.now()
const result = list.map(u => ({
userId: u._id,
user: u,
pet: u.pets?.[0] || null,
distance: Math.round(u.dist),
distanceText: formatDist(u.dist),
isOnline: u.lastSeen && (now - new Date(u.lastSeen).getTime()) < fiveMin,
location: {
latitude: u.lastLocation?.coordinates?.[1],
longitude: u.lastLocation?.coordinates?.[0],
},
}))
return { code: 0, data: result }
} catch (e) { } catch (e) {
return { code: -1, message: e.message } return { code: -1, message: e.message }
} }
} }
function formatDist(meters) {
if (!meters && meters !== 0) return '未知'
if (meters < 1000) return Math.round(meters) + 'm'
return (meters / 1000).toFixed(1) + 'km'
}

View File

@@ -13,9 +13,10 @@ exports.main = async (event, context) => {
.where({ openid: OPENID }) .where({ openid: OPENID })
.update({ .update({
data: { data: {
lastLocation: { latitude, longitude }, // ✅ 改用 GeoPoint才能用地理位置索引做距离查询
isOnline: true, lastLocation: db.Geo.Point(longitude, latitude), // 注意Geo.Point 是 (lng, lat) 顺序
lastSeen: db.serverDate(), lastSeen: db.serverDate(),
isOnline: true,
}, },
}) })
return { code: 0, data: null } return { code: 0, data: null }

View File

@@ -41,7 +41,6 @@
"requiredPrivateInfos": ["getLocation", "chooseLocation"], "requiredPrivateInfos": ["getLocation", "chooseLocation"],
"sitemapLocation": "sitemap.json", "sitemapLocation": "sitemap.json",
"style": "v2", "style": "v2",
"componentFramework": "glass-easel",
"lazyCodeLoading": "requiredComponents", "lazyCodeLoading": "requiredComponents",
"networkTimeout": { "networkTimeout": {
"request": 10000, "request": 10000,

View File

@@ -1,4 +1,4 @@
export const CLOUD_ENV = 'YOUR_CLOUD_ENV_ID' export const CLOUD_ENV = 'cloud1-d4g697lte499543d8'
export const COLORS = { export const COLORS = {
pink: '#ff4f91', pink: '#ff4f91',

View File

@@ -1,12 +1,19 @@
{ {
"appid": "YOUR_APPID_HERE", "appid": "wx8b033e454024200b",
"compileType": "miniprogram", "compileType": "miniprogram",
"libVersion": "2.25.0", "libVersion": "3.16.1",
"packOptions": { "packOptions": {
"ignore": [ "ignore": [
{ "type": "regexp", "value": "\\.eslintrc\\.js" }, {
{ "type": "regexp", "value": "\\.gitignore" } "value": "\\.eslintrc\\.js",
] "type": "regexp"
},
{
"value": "\\.gitignore",
"type": "regexp"
}
],
"include": []
}, },
"setting": { "setting": {
"coverView": true, "coverView": true,
@@ -31,19 +38,26 @@
}, },
"enableEngineNative": false, "enableEngineNative": false,
"bundle": false, "bundle": false,
"useIsolateContext": true,
"userConfirmedBundleSwitch": false, "userConfirmedBundleSwitch": false,
"packNpmManually": false, "packNpmManually": false,
"packNpmRelationList": [], "packNpmRelationList": [],
"minifyWXSS": true, "minifyWXSS": true,
"disableUseStrict": false, "disableUseStrict": false,
"ignoreUploadUnusedFiles": true, "ignoreUploadUnusedFiles": true,
"uploadWithSourceMap": true "compileWorklet": false,
"minifyWXML": true,
"localPlugins": false,
"useCompilerPlugins": ["typescript"],
"condition": false,
"swc": false,
"disableSWC": true
}, },
"condition": {}, "condition": {},
"editorSetting": { "editorSetting": {
"tabIndent": "insertSpaces", "tabIndent": "insertSpaces",
"tabSize": 2 "tabSize": 2
}, },
"srcMiniprogramRoot": "miniprogram/" "miniprogramRoot": "miniprogram/",
} "srcMiniprogramRoot": "miniprogram/",
"simulatorPluginLibVersion": {}
}

View File

@@ -18,8 +18,8 @@
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["miniprogram/*"] "@/*": ["miniprogram/*"]
}, }
"typeRoots": ["./typings"]
}, },
"include": ["miniprogram/**/*.ts"], "include": ["miniprogram/**/*.ts"],
"exclude": ["node_modules", "miniprogram/**/*.d.ts"] "exclude": ["node_modules", "miniprogram/**/*.d.ts"]