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:
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal 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
63
cloudbaserc.json
Normal 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": "打招呼 / 发起私信"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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": {}
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user