Files
WXProgram/miniprogram/services/locationTrackingService.ts
2025-10-19 23:38:54 +08:00

561 lines
19 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 位置追踪服务 - 处理用户签到后的位置追踪和状态管理
import { LocationData } from '../types';
import apiService from './apiService';
import userService from './userService';
import mapService from './mapService';
/**
* 在线用户信息接口
*/
export interface OnlineUserInfo {
userId: number;
name: string;
avatarUrl: string;
role: string; // 用户角色:'admin' | 'delivery_person'
lastLocation: LocationData;
lastUpdateTime: number;
status: 'online' | 'offline' | 'timeout';
}
/**
* 位置追踪服务类
* 提供用户签到后的位置追踪、状态管理和位置分发功能
*/
class LocationTrackingService {
// 在线用户列表仅通过服务器WebSocket推送更新
private onlineUsers: Map<number, OnlineUserInfo>;
// 位置更新定时器(仅用于向服务器发送位置信息)
private locationUpdateTimer: number | null;
// 用户状态变化回调函数(单一方法赋值)
private userStatusCallback: ((userId: number, status: 'online' | 'offline' | 'timeout') => void) | null;
/**
* 位置更新回调函数类型
*/
private locationUpdateCallback: ((onlineUsers: OnlineUserInfo[]) => void) | null;
// 当前用户是否已签到
private isSignedIn: boolean;
// 取消订阅回调函数
private unsubscribeCallback: (() => void) | null = null;
/**
* 构造函数
*/
constructor() {
this.onlineUsers = new Map();
this.locationUpdateTimer = null;
this.userStatusCallback = null;
this.locationUpdateCallback = null;
this.isSignedIn = false;
this.unsubscribeCallback = null;
}
/**
* 启动位置追踪服务WebSocket连接
*/
public async startTracking(): Promise<void> {
const userInfo = userService.getGlobalUserInfo();
if (!userInfo || !userInfo.id) {
throw new Error('用户未登录,无法启动位置追踪');
}
this.isSignedIn = true;
// 设置实时位置订阅初始化WebSocket连接
await this.setupRealTimeSubscription();
// 启动位置更新定时器每30秒向服务器发送位置信息
this.startLocationUpdateTimer();
console.log('位置追踪服务已启动');
}
/**
* 初始化位置追踪服务
*/
public async initialize(): Promise<void> {
}
/**
* 停止位置追踪服务WebSocket断开时调用
*/
public async stopTracking(): Promise<void> {
const userInfo = userService.getGlobalUserInfo();
if (!userInfo || !userInfo.id) {
return;
}
this.isSignedIn = false;
// 停止位置更新定时器
this.stopLocationUpdateTimer();
// 关闭WebSocket连接
try {
await this.teardownRealTimeSubscription();
console.log('调试信息 - WebSocket连接已关闭');
} catch (error) {
console.warn('调试信息 - 关闭WebSocket连接失败:', error);
}
// 清空在线用户列表WebSocket断开时清空所有数据
this.onlineUsers.clear();
// 通知所有位置更新回调,清空地图标记点
this.notifyLocationUpdateCallbacks();
console.log('位置追踪服务已停止,在线用户列表已清空');
}
/**
* 向服务器发送位置更新(仅发送,不更新本地缓存)
*/
public async updateUserLocation(location: { longitude: number; latitude: number }): Promise<void> {
const userInfo = userService.getGlobalUserInfo();
if (!userInfo || !userInfo.id || !this.isSignedIn) {
console.warn('[位置追踪服务] 用户未登录或未签到,无法更新位置');
return;
}
console.log('[位置追踪服务] 向服务器发送位置信息:');
console.log(`- 用户ID: ${userInfo.id}`);
console.log(`- 经度: ${location.longitude}`);
console.log(`- 纬度: ${location.latitude}`);
// 通过WebSocket发送位置更新给服务器
await apiService.sendLocationUpdate(userInfo.id, location.latitude, location.longitude);
console.log('[位置追踪服务] 位置信息已发送到服务器');
}
/**
* 获取所有在线用户信息
*/
public getOnlineUsers(): OnlineUserInfo[] {
return Array.from(this.onlineUsers.values()).filter(user =>
user.status === 'online'
);
}
/**
* 订阅用户状态变化(单一方法赋值)
* @param callback 用户状态变化回调函数
* @returns 取消订阅函数
*/
public subscribeToUserStatusChanges(callback: (userId: number, status: 'online' | 'offline' | 'timeout') => void): () => void {
console.log('📝 [LocationTrackingService] 注册用户状态变化回调函数');
// 直接赋值,覆盖之前的回调
this.userStatusCallback = callback;
console.log('✅ [LocationTrackingService] 用户状态变化回调注册完成');
// 返回取消订阅函数
return () => {
if (this.userStatusCallback === callback) {
this.userStatusCallback = null;
console.log('✅ [LocationTrackingService] 用户状态变化回调已取消');
}
};
}
/**
* 检查用户是否在线
*/
public isUserOnline(userId: number): boolean {
const userInfo = this.onlineUsers.get(userId);
return userInfo ? userInfo.status === 'online' : false;
}
// ===== 私有方法 =====
/**
* 启动位置更新定时器
*/
private startLocationUpdateTimer(): void {
this.stopLocationUpdateTimer();
this.locationUpdateTimer = setInterval(async () => {
if (this.isSignedIn) {
try {
const location = await this.getCurrentLocation();
await this.updateUserLocation(location);
} catch (error) {
console.error('自动位置更新失败:', error);
}
}
}, 30000); // 每30秒更新一次
}
/**
* 获取当前位置
*/
public async getCurrentLocation(): Promise<LocationData> {
const userInfo = userService.getGlobalUserInfo();
if (!userInfo || !userInfo.id) {
throw new Error('用户未登录');
}
try {
// 使用mapService获取真实位置保持与项目中其他地方的一致性
const location = await mapService.getLocation();
console.log('[位置追踪服务] 获取真实位置成功:', location);
return {
userId: userInfo.id,
longitude: location.longitude,
latitude: location.latitude,
timestamp: Date.now()
};
} catch (error) {
console.error('[位置追踪服务] 获取真实位置失败,使用默认位置:', error);
// 定位失败时返回默认位置作为降级方案
return {
userId: userInfo.id,
longitude: 102.833722,
latitude: 24.880095,
timestamp: Date.now()
};
}
}
/**
* 设置实时位置订阅
*/
private async setupRealTimeSubscription(): Promise<void> {
// 获取用户信息
const userInfo = userService.getGlobalUserInfo();
if (!userInfo || !userInfo.id) {
console.warn('用户未登录,无法设置实时位置订阅');
return;
}
// 缓存用户信息(已移除)
// 真实环境初始化WebSocket连接服务器会自动处理订阅逻辑
try {
// 初始化WebSocket连接如果尚未连接
await apiService.initLocationWebSocket();
// 等待WebSocket连接建立最多等待5秒
await new Promise<void>((resolve, reject) => {
let attempts = 0;
const maxAttempts = 50; // 5秒超时
const checkConnection = () => {
if (apiService.isWebSocketConnected()) {
resolve();
} else if (attempts < maxAttempts) {
attempts++;
setTimeout(checkConnection, 100);
} else {
reject(new Error('WebSocket连接建立超时'));
}
};
checkConnection();
});
// 服务器会自动处理订阅逻辑,无需发送订阅消息
console.log('WebSocket连接已建立服务器将自动处理位置订阅逻辑');
} catch (error) {
console.error('设置实时位置订阅失败:', error);
}
}
/**
* 关闭实时位置订阅
*/
private teardownRealTimeSubscription(): void {
// 真实环境关闭WebSocket连接服务器会自动处理取消订阅逻辑
try {
// 服务器会自动处理取消订阅逻辑,无需发送取消订阅消息
console.log('关闭WebSocket连接服务器将自动处理取消订阅逻辑');
// 移除位置更新回调
if (this.unsubscribeCallback) {
this.unsubscribeCallback();
this.unsubscribeCallback = null;
}
// 关闭WebSocket连接
apiService.closeLocationWebSocket();
} catch (error) {
console.error('关闭实时位置订阅失败:', error);
}
}
/**
* 停止位置更新定时器
*/
private stopLocationUpdateTimer(): void {
if (this.locationUpdateTimer) {
clearInterval(this.locationUpdateTimer);
this.locationUpdateTimer = null;
}
}
/**
* 通知位置更新回调
*/
private notifyLocationUpdateCallbacks(): void {
const onlineUsers = this.getOnlineUsers();
console.log('🔔 [LocationTrackingService] 开始通知位置更新回调');
console.log('👥 [LocationTrackingService] 在线用户数量:', onlineUsers.length);
// 触发位置更新回调
if (this.locationUpdateCallback) {
try {
console.log('🔄 [LocationTrackingService] 执行位置更新回调');
this.locationUpdateCallback(onlineUsers);
console.log('✅ [LocationTrackingService] 位置更新回调执行成功');
} catch (error) {
console.error('❌ [LocationTrackingService] 位置更新回调执行失败:', error);
}
} else {
console.log('⚠️ [LocationTrackingService] 没有注册位置更新回调');
}
console.log('🏁 [LocationTrackingService] 位置更新回调通知完成');
}
/**
* 手动触发位置更新(公共方法,可从外部调用)
* 用于手动刷新地图上的用户标记
*/
public triggerLocationUpdate(): void {
console.log('🔔 [LocationTrackingService] 手动触发位置更新');
this.notifyLocationUpdateCallbacks();
}
/**
* 订阅位置更新用于locationModule注册回调
* @param callback 位置更新回调函数
* @returns 取消订阅函数
*/
public subscribeToLocationUpdates(callback: (onlineUsers: OnlineUserInfo[]) => void): () => void {
console.log('📝 [LocationTrackingService] 注册位置更新回调函数');
// 直接赋值,覆盖之前的回调
this.locationUpdateCallback = callback;
console.log('✅ [LocationTrackingService] 位置更新回调注册完成');
// 返回取消订阅函数
return () => {
if (this.locationUpdateCallback === callback) {
this.locationUpdateCallback = null;
console.log('✅ [LocationTrackingService] 位置更新回调已取消');
}
};
}
/**
* 通知用户状态变化回调
*/
private notifyUserStatusChange(userId: number, status: 'online' | 'offline' | 'timeout'): void {
console.log(`🔔 [LocationTrackingService] 开始通知用户状态变化回调: 用户 ${userId} 状态变为 ${status}`);
if (this.userStatusCallback) {
try {
console.log('🔄 [LocationTrackingService] 执行用户状态变化回调');
this.userStatusCallback(userId, status);
console.log('✅ [LocationTrackingService] 用户状态变化回调执行成功');
} catch (error) {
console.error('❌ [LocationTrackingService] 用户状态变化回调执行失败:', error);
}
} else {
console.log('⚠️ [LocationTrackingService] 没有注册用户状态变化回调');
}
console.log('🏁 [LocationTrackingService] 用户状态变化回调通知完成');
}
/**
* 处理WebSocket消息
* @param message 服务器下发的消息
*/
public handleWebSocketMessage(message: any){
console.log(`📡 收到WebSocket消息类型: ${message.type}`, message);
switch (message.type) {
case 'onlineUserList':
// 处理在线用户列表消息(服务器发送当前在线用户列表)
console.log('👥 处理在线用户列表,用户数量:', message.users ? message.users.length : 0);
if (message.users && Array.isArray(message.users)) {
this.triggerOnlineUserListCallbacks({
type: 'onlineUserList',
users: message.users.map((user: any) => ({
userId: user.userId,
name: user.name,
role: user.role,
userStatus: user.userStatus,
lastUpdateTime: user.lastUpdateTime,
latitude: user.locationData?.latitude || user.latitude,
longitude: user.locationData?.longitude || user.longitude,
timestamp: user.locationData?.timestamp || user.timestamp
}))
});
}else {
console.warn('❌ onlineUserList消息格式错误users字段不存在或不是数组');
}
break;
case 'userLocationList':
// 处理用户位置列表消息服务器每30秒发送所有在线用户的位置列表
console.log('👥 处理用户位置列表,用户数量:', message.users ? message.users.length : 0);
// 确保用户列表存在且是数组
if (message.users && Array.isArray(message.users)) {
// 转换用户数据格式,确保与位置追踪服务兼容
const formattedUsers = message.users.map((user: any) => {
// 支持多种数据格式locationData字段或直接字段
const locationData = user.locationData || user;
return {
userId: user.userId || locationData.userId,
name: user.name || user.userName || `用户${user.userId || locationData.userId}`,
role: user.role || 'employee',
userStatus: user.userStatus !== false, // 转换为布尔值
lastUpdateTime: user.lastUpdateTime || locationData.timestamp || Date.now(),
latitude: locationData.latitude,
longitude: locationData.longitude,
timestamp: locationData.timestamp || user.timestamp || Date.now()
};
});
console.log('📊 转换后的用户位置数据:', formattedUsers);
this.triggerOnlineUserListCallbacks({
type: 'userLocationList',
users: formattedUsers
});
} else {
console.warn('❌ userLocationList消息格式错误users字段不存在或不是数组');
}
break;
default:
console.warn('收到未知类型的WebSocket消息:', message);
break;
}
}
/**
* 触发在线用户列表回调函数
* @param userList 在线用户列表数据
*/
private triggerOnlineUserListCallbacks(userList: any): void {
console.log('🔔 [apiService] 开始触发在线用户列表回调,消息类型:', userList.type);
console.log('📊 [apiService] 回调数据内容:', JSON.stringify(userList, null, 2));
try {
console.log('🔄 [apiService] 执行在线用户列表回调函数');
// 传递正确的数据格式只传递users数组而不是整个消息对象
this.handleOnlineUserList(userList.users);
console.log('✅ [apiService] 在线用户列表回调函数执行成功');
} catch (error) {
console.error('❌ [apiService] 在线用户列表回调函数执行失败:', error);
}
console.log('🏁 [apiService] 在线用户列表回调触发完成');
}
/**
* 处理在线用户列表
* @param userList 服务器下发的在线用户列表
*/
private handleOnlineUserList(userList: any[]): void {
try {
// 验证数据格式
if (!Array.isArray(userList)) {
console.warn('[LocationTrackingService] 无效的在线用户列表数据格式');
return;
}
console.log(`[LocationTrackingService] 开始处理在线用户列表,用户数量: ${userList.length}`);
// 获取当前本地用户列表的userId集合
const currentUserIds = new Set(this.onlineUsers.keys());
// 处理服务器下发的用户列表
const newUserIds = new Set();
userList.forEach(user => {
if (user && user.userId && user.latitude !== undefined && user.longitude !== undefined) {
newUserIds.add(user.userId);
// 提取位置数据(支持多种数据格式)
const latitude = user.latitude;
const longitude = user.longitude;
const timestamp = user.timestamp || user.lastUpdateTime || Date.now();
// 更新或添加用户信息
this.onlineUsers.set(user.userId, {
userId: user.userId,
name: user.name || user.userName || `用户${user.userId}`,
avatarUrl: '/images/user-avatar.png',
role: user.role || 'employee',
lastLocation: {
userId: user.userId,
longitude: longitude,
latitude: latitude,
timestamp: timestamp
},
lastUpdateTime: timestamp,
status: user.userStatus === false ? 'offline' : 'online'
});
console.log(`[LocationTrackingService] 更新用户 ${user.userId} 位置: (${latitude}, ${longitude})`);
// 如果用户是新上线的,触发用户状态回调
if (!currentUserIds.has(user.userId)) {
this.notifyUserStatusChange(user.userId, 'online');
console.log(`[LocationTrackingService] 新用户上线: ${user.userId}`);
}
} else {
console.warn(`[LocationTrackingService] 跳过无效用户数据:`, user);
}
});
// 删除本地列表中不在服务器列表中的用户
let removedCount = 0;
currentUserIds.forEach(userId => {
if (!newUserIds.has(userId)) {
// 删除离线用户
this.onlineUsers.delete(userId);
removedCount++;
// 触发用户状态回调(离线)
this.notifyUserStatusChange(userId, 'offline');
console.log(`[LocationTrackingService] 用户离线: ${userId}`);
}
});
console.log(`[LocationTrackingService] 在线用户列表更新完成: 新增 ${newUserIds.size - (currentUserIds.size - removedCount)} 个用户,删除 ${removedCount} 个用户`);
// 通知位置模块更新地图标记点(无论是否有变化都通知,确保地图标记点同步)
this.notifyLocationUpdateCallbacks();
console.log('[LocationTrackingService] 已通知位置模块更新地图标记点');
} catch (error) {
console.error('[LocationTrackingService] 处理在线用户列表失败:', error);
}
}
}
/**
* 位置追踪服务单例实例
* 导出供应用程序全局使用
*/
export default new LocationTrackingService();