diff --git a/pwuniapp/common/api.js b/pwuniapp/common/api.js index c052cd9..99336aa 100644 --- a/pwuniapp/common/api.js +++ b/pwuniapp/common/api.js @@ -249,6 +249,59 @@ export async function getCompanions(keyword) { return []; } +/** + * 即时匹配在线大神:服务端按真实注册、审核通过、10 分钟心跳在线过滤; + * 传入定位时按 5km 距离过滤并升序返回(只返回距离,不返回精确坐标)。 + */ +export async function matchCompanions(options = {}) { + try { + const qs = []; + const lat = Number(options.lat); + const lng = Number(options.lng); + if (Number.isFinite(lat) && Number.isFinite(lng) && !(lat === 0 && lng === 0)) { + qs.push(`lat=${lat}`, `lng=${lng}`); + } + qs.push(`radius=${Math.min(50, Math.max(1, Number(options.radius) || 5))}`); + qs.push(`limit=${Math.min(20, Math.max(1, Number(options.limit) || 10))}`); + const res = await http.get(`/companions/match?${qs.join('&')}`); + const list = Array.isArray(res) ? res : (res && Array.isArray(res.list) ? res.list : []); + return list.map((c) => ({ + id: String(c.id), + userId: Number(c.userId) || 0, + nickname: c.nickname || '', + avatar: c.avatar || '', + gender: c.gender === '男' ? 'male' : (c.gender === '女' ? 'female' : ''), + age: Number(c.age) || 0, + game: c.track === 'OFFLINE_MEAL' ? '美食陪吃' + : (c.track === 'OFFLINE_SING' ? '同城陪唱' + : (c.track === 'OFFLINE_DRINK' ? '下午茶陪喝' : (c.track || ''))), + track: c.track || '', + rankTag: c.rankTag || '', + price: Number(c.price) || 0, + rating: Number(c.rating) || 0, + orderCount: Number(c.orderCount) || 0, + isOnline: Boolean(c.isOnline), + distanceKm: c.distanceKm === null || c.distanceKm === undefined ? null : Number(c.distanceKm), + distanceText: c.distanceText || '同城', + })); + } catch (err) { + console.warn('[API] 即时匹配大神异常:', err.message); + return []; + } +} + +/** 大神接单心跳:刷新在线状态;定位失败时仍上报活跃时间 */ +export async function sendCompanionHeartbeat({ lat, lng } = {}) { + const payload = {}; + const nLat = Number(lat); + const nLng = Number(lng); + if (Number.isFinite(nLat) && Number.isFinite(nLng) && !(nLat === 0 && nLng === 0)) { + payload.lat = nLat; + payload.lng = nLng; + } + return http.post('/companions/heartbeat', payload); +} + /** * 获取单个大神详情 (真实从 MySQL 读取) */ diff --git a/pwuniapp/common/expert-heartbeat.js b/pwuniapp/common/expert-heartbeat.js new file mode 100644 index 0000000..29a32f9 --- /dev/null +++ b/pwuniapp/common/expert-heartbeat.js @@ -0,0 +1,61 @@ +// 大神接单心跳:工作台/接单大厅/雷达页在前台时每 60 秒上报一次 +// 服务端以 last_active_at 判断是否真实在线(10 分钟无心跳自动移出匹配池) +import { getUserLocation } from './location.js'; +import { sendCompanionHeartbeat, normalizeUserId } from './api.js'; + +let timer = null; +let inFlight = false; +let lastSentAt = 0; +let coords = null; +let coordsAt = 0; + +const COORD_TTL_MS = 5 * 60 * 1000; + +async function resolveCoords() { + if (coords && Date.now() - coordsAt < COORD_TTL_MS) return coords; + try { + const pos = await getUserLocation(); + coords = { lat: pos.latitude, lng: pos.longitude }; + coordsAt = Date.now(); + return coords; + } catch (_) { + // 用户拒绝定位时仍上报活跃时间,只是没有距离信息 + return coords; + } +} + +export async function sendHeartbeat(force = false) { + if (inFlight) return; + if (!force && Date.now() - lastSentAt < 45 * 1000) return; + if (!normalizeUserId()) return; + inFlight = true; + try { + const pos = await resolveCoords(); + await sendCompanionHeartbeat(pos || {}); + lastSentAt = Date.now(); + } catch (_) { + } finally { + inFlight = false; + } +} + +export function startHeartbeat(intervalMs = 60 * 1000) { + if (timer) return; + void sendHeartbeat(true); + timer = setInterval(() => { + void sendHeartbeat(); + }, Math.max(30 * 1000, Number(intervalMs) || 60 * 1000)); + if (timer && typeof timer.unref === 'function') timer.unref(); +} + +export function stopHeartbeat() { + if (timer) { + clearInterval(timer); + timer = null; + } +} + +export function resetHeartbeatLocation() { + coords = null; + coordsAt = 0; +} diff --git a/pwuniapp/pages/expert/hall.vue b/pwuniapp/pages/expert/hall.vue index 776733c..8d3dddd 100644 --- a/pwuniapp/pages/expert/hall.vue +++ b/pwuniapp/pages/expert/hall.vue @@ -253,6 +253,7 @@ import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js'; import { isExpertCertified } from '@/common/role.js'; import { claimOrder, getOrdersPool, getExpertStats, updateCompanionProfile, getUserProfile, getActivities, normalizeUserId } from '@/common/api.js'; import { getUserLocation } from '@/common/location.js'; +import { startHeartbeat, stopHeartbeat } from '@/common/expert-heartbeat.js'; import { getNavBarInfo } from '@/common/nav.js'; const currentThemeKey = ref(getCurrentThemeKey()); @@ -436,15 +437,19 @@ onShow(() => { uni.hideTabBar({ animation: false, fail: () => {} }); loadHallData(); startHallPolling(); + if (isOnline.value) startHeartbeat(); + else stopHeartbeat(); }); onHide(() => { stopHallPolling(); + stopHeartbeat(); }); onUnmounted(() => { uni.$off('theme-change', onThemeChanged); stopHallPolling(); + stopHeartbeat(); }); @@ -482,6 +487,11 @@ const toggleOnlineStatus = async () => { status: isOnline.value ? '接单中' : '休息中' }); } catch (_) {} + if (isOnline.value) { + startHeartbeat(); + } else { + stopHeartbeat(); + } uni.showToast({ title: isOnline.value ? '已开启在线接单' : '已设置为打烊休息', icon: isOnline.value ? 'success' : 'none', diff --git a/pwuniapp/pages/expert/radar.vue b/pwuniapp/pages/expert/radar.vue index 94c7a8d..f3de3b3 100644 --- a/pwuniapp/pages/expert/radar.vue +++ b/pwuniapp/pages/expert/radar.vue @@ -191,6 +191,7 @@ import { isExpertCertified } from '@/common/role.js'; import { getNavBarInfo } from '@/common/nav.js'; import { getOrdersPool, claimOrder, reverseGeocode } from '@/common/api.js'; import { getUserLocation } from '@/common/location.js'; +import { startHeartbeat, stopHeartbeat } from '@/common/expert-heartbeat.js'; const navInfo = ref(getNavBarInfo()); const navStyle = computed(() => ({ @@ -350,15 +351,19 @@ onShow(() => { uni.hideTabBar({ animation: false, fail: () => {} }); refreshLocation(); startPolling(); + if (isRadarActive.value) startHeartbeat(); + else stopHeartbeat(); }); onHide(() => { stopPolling(); + stopHeartbeat(); }); onUnmounted(() => { uni.$off('theme-change', onThemeChanged); stopPolling(); + stopHeartbeat(); }); @@ -389,8 +394,10 @@ const toggleRadarSwitch = (e) => { isRadarActive.value = e.detail.value; if (isRadarActive.value) { startPolling(); + startHeartbeat(); } else { stopPolling(); + stopHeartbeat(); } uni.showToast({ title: isRadarActive.value ? '已开启 5km 智能巡航接单' : '已挂起智能巡航', diff --git a/pwuniapp/pages/expert/workspace.vue b/pwuniapp/pages/expert/workspace.vue index c8715ae..ccb3b51 100644 --- a/pwuniapp/pages/expert/workspace.vue +++ b/pwuniapp/pages/expert/workspace.vue @@ -464,7 +464,7 @@