HarmonyOS APP开发:位置分享与实时位置同步

举报
Jack20 发表于 2026/06/22 14:08:24 2026/06/22
【摘要】 HarmonyOS APP开发:位置分享与实时位置同步核心要点:本文深入讲解HarmonyOS平台位置分享与实时位置同步的完整技术实现,涵盖位置数据序列化、短链生成与分享、WebSocket实时同步、地图多人位置展示、隐私控制与权限管理等核心能力,构建一个完整的实时位置共享应用。项目说明核心KitLocation Kit、Map Kit、Network Kit、Notification K...

HarmonyOS APP开发:位置分享与实时位置同步

核心要点:本文深入讲解HarmonyOS平台位置分享与实时位置同步的完整技术实现,涵盖位置数据序列化、短链生成与分享、WebSocket实时同步、地图多人位置展示、隐私控制与权限管理等核心能力,构建一个完整的实时位置共享应用。

项目 说明
核心Kit Location Kit、Map Kit、Network Kit、Notification Kit
难度等级 ⭐⭐⭐⭐☆
官方文档 Network Kit开发指南 · Location Kit开发指南

一、背景与动机

1.1 位置分享的应用场景

位置分享是LBS社交的核心能力之一,其应用场景广泛而深入:

  • 家庭守护:家长实时查看孩子位置,老人外出安全监护
  • 好友聚会:共享实时位置,方便集合碰面
  • 出行拼车:司机与乘客互相查看位置,预估到达时间
  • 户外运动:团队成员互相查看位置,保障安全
  • 物流配送:实时追踪配送员位置,提升用户体验

1.2 实时位置同步的技术挑战

位置分享看似简单——“把我的位置发给别人”——但实现高质量的实时同步面临多重挑战:

  • 实时性:位置变化后需在秒级内同步到所有关注者
  • 网络适应:弱网环境下保证数据最终到达
  • 电量优化:持续上报位置是耗电大户
  • 隐私控制:用户需精确控制谁可以看到自己的位置
  • 并发处理:多人同时在线时的服务端压力

1.3 本文目标

构建一个完整的「位置共享」应用,实现以下核心功能:

  1. 生成位置分享链接,支持系统分享
  2. WebSocket实时位置同步
  3. 地图上展示多个用户的实时位置
  4. 隐私控制:时间限制、精度控制、随时停止

二、核心原理

2.1 实时位置同步架构

flowchart TB
    classDef client fill:#4ECDC4,stroke:#2C3E50,color:#fff,font-weight:bold
    classDef server fill:#FF6B6B,stroke:#2C3E50,color:#fff,font-weight:bold
    classDef protocol fill:#45B7D1,stroke:#2C3E50,color:#fff,font-weight:bold
    classDef data fill:#96CEB4,stroke:#2C3E50,color:#fff,font-weight:bold

    A[分享者APP]:::client --> B[Location Kit]:::client
    B --> C[位置数据采集]:::data
    C --> D[WebSocket连接]:::protocol
    D --> E[位置同步服务器]:::server

    E --> F[房间管理]:::server
    E --> G[权限校验]:::server
    E --> H[消息广播]:::server

    H --> I[WebSocket推送]:::protocol
    I --> J[关注者APP 1]:::client
    I --> K[关注者APP 2]:::client
    I --> L[关注者APP N]:::client

    J --> M[Map Kit渲染]:::data
    K --> N[Map Kit渲染]:::data
    L --> O[Map Kit渲染]:::data

    P[短链服务]:::server --> Q[分享链接]:::data
    Q --> R[系统分享面板]:::client

    style A fill:#FFE66D,stroke:#2C3E50,color:#2C3E50
    style E fill:#FFE66D,stroke:#2C3E50,color:#2C3E50

2.2 通信协议设计

WebSocket消息格式

// 消息类型枚举
enum MessageType {
  JOIN_ROOM = 'join_room',           // 加入房间
  LEAVE_ROOM = 'leave_room',         // 离开房间
  LOCATION_UPDATE = 'location_update', // 位置更新
  MEMBER_JOIN = 'member_join',       // 成员加入通知
  MEMBER_LEAVE = 'member_leave',     // 成员离开通知
  ROOM_CLOSE = 'room_close',         // 房间关闭
  ERROR = 'error'                    // 错误消息
}

// 通用消息结构
interface WsMessage {
  type: MessageType;
  roomId: string;
  userId: string;
  timestamp: number;
  payload: Record<string, Object>;
}

位置同步流程

sequenceDiagram
    participant A as 分享者
    participant S as 同步服务器
    participant B as 关注者1
    participant C as 关注者2

    A->>S: JOIN_ROOM {roomId, userId}
    S-->>A: MEMBER_JOIN {members: []}
    A->>S: LOCATION_UPDATE {lat, lng, accuracy}
    S-->>B: LOCATION_UPDATE {userId: A, lat, lng}
    S-->>C: LOCATION_UPDATE {userId: A, lat, lng}

    B->>S: JOIN_ROOM {roomId, userId}
    S-->>A: MEMBER_JOIN {userId: B}
    S-->>C: MEMBER_JOIN {userId: B}

    A->>S: LOCATION_UPDATE {lat, lng}
    S-->>B: LOCATION_UPDATE {userId: A, lat, lng}
    S-->>C: LOCATION_UPDATE {userId: A, lat, lng}

    A->>S: LEAVE_ROOM
    S-->>B: MEMBER_LEAVE {userId: A}
    S-->>C: MEMBER_LEAVE {userId: A}

2.3 隐私控制模型

控制维度 选项 说明
时长限制 15分钟/1小时/8小时/自定义 到期自动停止分享
精度控制 精确位置/城市级/仅省份 降低位置精度保护隐私
可见范围 指定用户/群组/公开链接 控制谁能查看位置
即时停止 随时一键停止 用户完全掌控

三、代码实战

3.1 位置数据模型与隐私处理

// LocationShareModels.ets

/**
 * 位置分享配置
 */
export interface ShareConfig {
  roomId: string;            // 分享房间ID
  duration: number;          // 分享时长(分钟)
  precision: LocationPrecision; // 位置精度
  shareLink: string;         // 分享链接
  expireTime: number;        // 过期时间戳
}

/**
 * 位置精度枚举
 */
export enum LocationPrecision {
  EXACT = 'exact',           // 精确位置(~10m)
  CITY = 'city',             // 城市级(~5km)
  PROVINCE = 'province'      // 省份级(~50km)
}

/**
 * 用户位置数据
 */
export interface UserLocation {
  userId: string;            // 用户ID
  nickname: string;          // 昵称
  avatar: string;            // 头像URL
  latitude: number;          // 纬度
  longitude: number;         // 经度
  accuracy: number;          // 精度
  speed: number;             // 速度
  timestamp: number;         // 时间戳
  batteryLevel: number;      // 电量
  isSharing: boolean;        // 是否正在分享
}

/**
 * 隐私处理器
 * 根据精度设置对位置数据进行模糊化处理
 */
export class PrivacyProcessor {
  /**
   * 应用隐私精度处理
   */
  static applyPrecision(
    latitude: number,
    longitude: number,
    precision: LocationPrecision
  ): { latitude: number; longitude: number } {
    switch (precision) {
      case LocationPrecision.EXACT:
        // 精确位置,不做处理
        return { latitude, longitude };

      case LocationPrecision.CITY:
        // 城市级:保留2位小数(约1km精度)
        return {
          latitude: Math.round(latitude * 100) / 100,
          longitude: Math.round(longitude * 100) / 100
        };

      case LocationPrecision.PROVINCE:
        // 省份级:保留1位小数(约10km精度)
        return {
          latitude: Math.round(latitude * 10) / 10,
          longitude: Math.round(longitude * 10) / 10
        };

      default:
        return { latitude, longitude };
    }
  }

  /**
   * 生成随机偏移(差分隐私)
   * 在精确位置上添加随机偏移,防止位置推断
   */
  static addNoise(
    latitude: number,
    longitude: number,
    noiseRadius: number = 0.001
  ): { latitude: number; longitude: number } {
    const angle = Math.random() * 2 * Math.PI;
    const distance = Math.random() * noiseRadius;

    return {
      latitude: latitude + distance * Math.cos(angle),
      longitude: longitude + distance * Math.sin(angle)
    };
  }
}

3.2 WebSocket实时同步服务

// WebSocketSyncService.ets
import { webSocket } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { UserLocation, ShareConfig, LocationPrecision, PrivacyProcessor } from './LocationShareModels';

/**
 * WebSocket连接状态
 */
export enum WsState {
  DISCONNECTED = 'disconnected',
  CONNECTING = 'connecting',
  CONNECTED = 'connected',
  RECONNECTING = 'reconnecting'
}

/**
 * 同步回调接口
 */
export interface SyncCallback {
  onConnected?: () => void;
  onDisconnected?: (reason: string) => void;
  onMemberJoin?: (location: UserLocation) => void;
  onMemberLeave?: (userId: string) => void;
  onLocationUpdate?: (location: UserLocation) => void;
  onError?: (error: Error) => void;
}

/**
 * WebSocket实时位置同步服务
 * 负责与服务器建立WebSocket连接,实现位置数据的实时双向同步
 */
export class WebSocketSyncService {
  private static instance: WebSocketSyncService;
  private ws: webSocket.WebSocket | null = null;
  private state: WsState = WsState.DISCONNECTED;
  private callback: SyncCallback | null = null;
  private reconnectTimer: number = -1;
  private heartbeatTimer: number = -1;
  private currentRoomId: string = '';
  private currentUserId: string = '';
  private serverUrl: string = 'wss://location-sync.example.com/ws';
  private reconnectAttempts: number = 0;
  private maxReconnectAttempts: number = 5;
  private precision: LocationPrecision = LocationPrecision.EXACT;

  private constructor() {}

  static getInstance(): WebSocketSyncService {
    if (!WebSocketSyncService.instance) {
      WebSocketSyncService.instance = new WebSocketSyncService();
    }
    return WebSocketSyncService.instance;
  }

  /**
   * 设置回调
   */
  setCallback(callback: SyncCallback): void {
    this.callback = callback;
  }

  /**
   * 设置服务器地址
   */
  setServerUrl(url: string): void {
    this.serverUrl = url;
  }

  /**
   * 设置位置精度
   */
  setPrecision(precision: LocationPrecision): void {
    this.precision = precision;
  }

  /**
   * 连接WebSocket服务器
   */
  async connect(userId: string): Promise<void> {
    if (this.state === WsState.CONNECTED || this.state === WsState.CONNECTING) {
      console.warn('[WsSync] 已连接或正在连接');
      return;
    }

    this.currentUserId = userId;
    this.state = WsState.CONNECTING;

    try {
      this.ws = webSocket.createWebSocket();

      // 注册事件回调
      this.ws.on('open', () => {
        this.onOpen();
      });

      this.ws.on('message', (err: Error, data: webSocket.Result) => {
        this.onMessage(data);
      });

      this.ws.on('close', (err: Error, data: webSocket.CloseResult) => {
        this.onClose(data);
      });

      this.ws.on('error', (err: Error) => {
        this.onError(err);
      });

      // 建立连接
      await this.ws.connect(this.serverUrl);
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[WsSync] 连接失败: ${err.code} - ${err.message}`);
      this.state = WsState.DISCONNECTED;
      this.callback?.onError?.(new Error(`连接失败: ${err.message}`));
      this.scheduleReconnect();
    }
  }

  /**
   * 断开连接
   */
  async disconnect(): Promise<void> {
    this.stopHeartbeat();
    this.stopReconnect();

    if (this.ws) {
      try {
        // 发送离开房间消息
        if (this.currentRoomId) {
          this.sendMessage({
            type: 'leave_room',
            roomId: this.currentRoomId,
            userId: this.currentUserId,
            timestamp: Date.now(),
            payload: {}
          });
        }

        await this.ws.close();
      } catch (error) {
        console.error('[WsSync] 断开连接失败:', JSON.stringify(error));
      }

      this.ws = null;
    }

    this.state = WsState.DISCONNECTED;
    this.currentRoomId = '';
  }

  /**
   * 加入分享房间
   */
  joinRoom(roomId: string): void {
    this.currentRoomId = roomId;
    this.sendMessage({
      type: 'join_room',
      roomId: roomId,
      userId: this.currentUserId,
      timestamp: Date.now(),
      payload: {}
    });
  }

  /**
   * 发送位置更新
   */
  sendLocationUpdate(location: UserLocation): void {
    if (this.state !== WsState.CONNECTED) return;

    // 应用隐私精度处理
    const processed = PrivacyProcessor.applyPrecision(
      location.latitude,
      location.longitude,
      this.precision
    );

    this.sendMessage({
      type: 'location_update',
      roomId: this.currentRoomId,
      userId: this.currentUserId,
      timestamp: Date.now(),
      payload: {
        latitude: processed.latitude,
        longitude: processed.longitude,
        accuracy: location.accuracy,
        speed: location.speed,
        batteryLevel: location.batteryLevel
      }
    });
  }

  /**
   * 连接成功回调
   */
  private onOpen(): void {
    console.info('[WsSync] WebSocket连接成功');
    this.state = WsState.CONNECTED;
    this.reconnectAttempts = 0;
    this.callback?.onConnected?.();
    this.startHeartbeat();
  }

  /**
   * 消息接收回调
   */
  private onMessage(data: webSocket.Result): void {
    try {
      const message = JSON.parse(typeof data === 'string' ? data : data.toString());
      this.handleMessage(message);
    } catch (error) {
      console.error('[WsSync] 消息解析失败:', JSON.stringify(error));
    }
  }

  /**
   * 处理接收到的消息
   */
  private handleMessage(message: Record<string, Object>): void {
    const type = message.type as string;

    switch (type) {
      case 'member_join': {
        const location: UserLocation = {
          userId: message.userId as string,
          nickname: (message.payload as Record<string, Object>).nickname as string ?? '未知用户',
          avatar: (message.payload as Record<string, Object>).avatar as string ?? '',
          latitude: (message.payload as Record<string, Object>).latitude as number ?? 0,
          longitude: (message.payload as Record<string, Object>).longitude as number ?? 0,
          accuracy: 0,
          speed: 0,
          timestamp: Date.now(),
          batteryLevel: 0,
          isSharing: true
        };
        this.callback?.onMemberJoin?.(location);
        break;
      }

      case 'member_leave': {
        const userId = message.userId as string;
        this.callback?.onMemberLeave?.(userId);
        break;
      }

      case 'location_update': {
        const payload = message.payload as Record<string, Object>;
        const location: UserLocation = {
          userId: message.userId as string,
          nickname: '',
          avatar: '',
          latitude: payload.latitude as number,
          longitude: payload.longitude as number,
          accuracy: payload.accuracy as number ?? 0,
          speed: payload.speed as number ?? 0,
          timestamp: message.timestamp as number,
          batteryLevel: payload.batteryLevel as number ?? 0,
          isSharing: true
        };
        this.callback?.onLocationUpdate?.(location);
        break;
      }

      case 'room_close': {
        this.callback?.onDisconnected?.('房间已关闭');
        break;
      }

      case 'error': {
        const errorMsg = (message.payload as Record<string, Object>).message as string ?? '未知错误';
        this.callback?.onError?.(new Error(errorMsg));
        break;
      }
    }
  }

  /**
   * 连接关闭回调
   */
  private onClose(data: webSocket.CloseResult): void {
    console.info(`[WsSync] 连接关闭: code=${data.code}, reason=${data.reason}`);
    this.state = WsState.DISCONNECTED;
    this.stopHeartbeat();
    this.callback?.onDisconnected?.(data.reason);

    // 非主动关闭时尝试重连
    if (data.code !== 1000) {
      this.scheduleReconnect();
    }
  }

  /**
   * 连接错误回调
   */
  private onError(err: Error): void {
    console.error(`[WsSync] 连接错误: ${err.message}`);
    this.callback?.onError?.(err);
  }

  /**
   * 发送消息
   */
  private sendMessage(message: Record<string, Object>): void {
    if (!this.ws || this.state !== WsState.CONNECTED) return;

    try {
      this.ws.send(JSON.stringify(message));
    } catch (error) {
      console.error('[WsSync] 发送消息失败:', JSON.stringify(error));
    }
  }

  /**
   * 启动心跳
   */
  private startHeartbeat(): void {
    this.stopHeartbeat();
    this.heartbeatTimer = setInterval(() => {
      this.sendMessage({
        type: 'ping',
        roomId: this.currentRoomId,
        userId: this.currentUserId,
        timestamp: Date.now(),
        payload: {}
      });
    }, 30000) as unknown as number; // 每30秒发送一次心跳
  }

  /**
   * 停止心跳
   */
  private stopHeartbeat(): void {
    if (this.heartbeatTimer !== -1) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = -1;
    }
  }

  /**
   * 调度重连
   */
  private scheduleReconnect(): void {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.warn('[WsSync] 已达最大重连次数');
      this.callback?.onError?.(new Error('连接失败,请检查网络'));
      return;
    }

    // 指数退避重连
    const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
    this.reconnectAttempts++;

    console.info(`[WsSync] ${delay / 1000}秒后尝试第${this.reconnectAttempts}次重连`);

    this.reconnectTimer = setTimeout(() => {
      this.connect(this.currentUserId);
    }, delay) as unknown as number;
  }

  /**
   * 停止重连
   */
  private stopReconnect(): void {
    if (this.reconnectTimer !== -1) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = -1;
    }
    this.reconnectAttempts = 0;
  }

  /**
   * 获取当前连接状态
   */
  getState(): WsState {
    return this.state;
  }
}

3.3 分享链接生成服务

// ShareLinkService.ets
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { ShareConfig, LocationPrecision } from './LocationShareModels';

/**
 * 分享链接生成服务
 * 负责创建分享房间、生成短链、管理分享生命周期
 */
export class ShareLinkService {
  private static instance: ShareLinkService;
  private baseUrl: string = 'https://location-share.example.com/api';
  private activeShares: Map<string, ShareConfig> = new Map();

  private constructor() {}

  static getInstance(): ShareLinkService {
    if (!ShareLinkService.instance) {
      ShareLinkService.instance = new ShareLinkService();
    }
    return ShareLinkService.instance;
  }

  /**
   * 创建位置分享
   * @param userId 用户ID
   * @param duration 分享时长(分钟)
   * @param precision 位置精度
   */
  async createShare(
    userId: string,
    duration: number = 60,
    precision: LocationPrecision = LocationPrecision.EXACT
  ): Promise<ShareConfig> {
    try {
      const httpRequest = http.createHttp();

      const response = await httpRequest.request(
        `${this.baseUrl}/share/create`,
        {
          method: http.RequestMethod.POST,
          header: { 'Content-Type': 'application/json' },
          extraData: JSON.stringify({
            userId: userId,
            duration: duration,
            precision: precision
          })
        }
      );

      if (response.responseCode === 200) {
        const result = JSON.parse(response.result as string);
        const config: ShareConfig = {
          roomId: result.roomId,
          duration: duration,
          precision: precision,
          shareLink: result.shareLink,
          expireTime: Date.now() + duration * 60 * 1000
        };

        this.activeShares.set(config.roomId, config);
        return config;
      }

      throw new Error(`创建分享失败: ${response.responseCode}`);
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[ShareLink] 创建失败: ${err.message}`);
      throw error;
    }
  }

  /**
   * 停止位置分享
   */
  async stopShare(roomId: string): Promise<void> {
    try {
      const httpRequest = http.createHttp();

      await httpRequest.request(
        `${this.baseUrl}/share/stop`,
        {
          method: http.RequestMethod.POST,
          header: { 'Content-Type': 'application/json' },
          extraData: JSON.stringify({ roomId: roomId })
        }
      );

      this.activeShares.delete(roomId);
    } catch (error) {
      console.error('[ShareLink] 停止分享失败:', JSON.stringify(error));
    }
  }

  /**
   * 获取活跃分享配置
   */
  getActiveShare(roomId: string): ShareConfig | undefined {
    return this.activeShares.get(roomId);
  }

  /**
   * 检查分享是否过期
   */
  isShareExpired(roomId: string): boolean {
    const config = this.activeShares.get(roomId);
    if (!config) return true;
    return Date.now() > config.expireTime;
  }

  /**
   * 调用系统分享面板
   */
  async shareToSystem(link: string, title: string = '我的实时位置'): Promise<void> {
    try {
      // 使用HarmonyOS系统分享能力
      const shareData = {
        title: title,
        text: `我在分享我的实时位置,点击查看:${link}`,
        uri: link
      };

      // 调用系统分享API
      console.info(`[ShareLink] 系统分享: ${JSON.stringify(shareData)}`);
    } catch (error) {
      console.error('[ShareLink] 系统分享失败:', JSON.stringify(error));
    }
  }
}

3.4 位置分享主页面

// LocationSharePage.ets
import { map, mapCommon } from '@kit.MapKit';
import { geoLocationManager } from '@kit.LocationKit';
import { UserLocation, LocationPrecision, ShareConfig } from '../service/LocationShareModels';
import { WebSocketSyncService, WsState } from '../service/WebSocketSyncService';
import { ShareLinkService } from '../service/ShareLinkService';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct LocationSharePage {
  // 分享状态
  @State isSharing: boolean = false;
  @State shareConfig: ShareConfig | null = null;
  @State shareDuration: number = 60;           // 默认1小时
  @State sharePrecision: LocationPrecision = LocationPrecision.EXACT;
  @State remainingTime: string = '--:--';

  // 成员位置
  @State members: UserLocation[] = [];
  @State myLocation: UserLocation | null = null;

  // 连接状态
  @State wsState: WsState = WsState.DISCONNECTED;

  // 地图
  @State mapController: map.MapComponentController | null = null;
  private memberMarkers: Map<string, map.Marker> = new Map();

  // 服务
  private syncService = WebSocketSyncService.getInstance();
  private shareLinkService = ShareLinkService.getInstance();
  private userId: string = `user_${Date.now()}`;
  private locationTimer: number = -1;
  private countdownTimer: number = -1;

  aboutToAppear(): void {
    this.initSyncService();
    this.startLocationTracking();
  }

  aboutToDisappear(): void {
    this.stopLocationTracking();
    this.stopCountdown();
    if (this.isSharing) {
      this.stopSharing();
    }
  }

  /**
   * 初始化同步服务回调
   */
  initSyncService(): void {
    this.syncService.setCallback({
      onConnected: () => {
        this.wsState = WsState.CONNECTED;
        promptAction.showToast({ message: '已连接到位置同步服务' });
      },
      onDisconnected: (reason: string) => {
        this.wsState = WsState.DISCONNECTED;
        if (this.isSharing) {
          promptAction.showToast({ message: `连接断开: ${reason}` });
        }
      },
      onMemberJoin: (location: UserLocation) => {
        this.members = [...this.members, location];
        this.updateMemberMarker(location);
        promptAction.showToast({ message: `${location.nickname} 加入了位置共享` });
      },
      onMemberLeave: (userId: string) => {
        this.members = this.members.filter(m => m.userId !== userId);
        this.removeMemberMarker(userId);
      },
      onLocationUpdate: (location: UserLocation) => {
        const index = this.members.findIndex(m => m.userId === location.userId);
        if (index >= 0) {
          // 更新已有成员位置
          const updated = [...this.members];
          updated[index] = { ...updated[index], ...location };
          this.members = updated;
        } else {
          // 新成员
          this.members = [...this.members, location];
        }
        this.updateMemberMarker(location);
      },
      onError: (error: Error) => {
        promptAction.showToast({ message: error.message });
      }
    });
  }

  /**
   * 开始位置追踪
   */
  startLocationTracking(): void {
    this.locationTimer = setInterval(async () => {
      try {
        const requestInfo: geoLocationManager.SingleLocationRequest = {
          latitude: this.myLocation?.latitude ?? 39.9042,
          longitude: this.myLocation?.longitude ?? 116.4074,
          timeout: 5000
        };

        const location = await geoLocationManager.getCurrentLocation(requestInfo);

        this.myLocation = {
          userId: this.userId,
          nickname: '我',
          avatar: '',
          latitude: location.latitude,
          longitude: location.longitude,
          accuracy: location.accuracy,
          speed: location.speed ?? 0,
          timestamp: Date.now(),
          batteryLevel: 0,
          isSharing: this.isSharing
        };

        // 如果正在分享,发送位置更新
        if (this.isSharing && this.wsState === WsState.CONNECTED) {
          this.syncService.sendLocationUpdate(this.myLocation);
        }
      } catch (error) {
        console.error('[LocationShare] 定位失败:', JSON.stringify(error));
      }
    }, 3000) as unknown as number; // 每3秒更新一次位置
  }

  /**
   * 停止位置追踪
   */
  stopLocationTracking(): void {
    if (this.locationTimer !== -1) {
      clearInterval(this.locationTimer);
      this.locationTimer = -1;
    }
  }

  /**
   * 开始位置分享
   */
  async startSharing(): Promise<void> {
    try {
      // 创建分享
      this.shareConfig = await this.shareLinkService.createShare(
        this.userId,
        this.shareDuration,
        this.sharePrecision
      );

      // 连接WebSocket
      this.syncService.setPrecision(this.sharePrecision);
      await this.syncService.connect(this.userId);

      // 加入房间
      this.syncService.joinRoom(this.shareConfig.roomId);

      this.isSharing = true;
      this.startCountdown();

      promptAction.showToast({ message: '位置分享已开启' });
    } catch (error) {
      promptAction.showToast({ message: '开启分享失败' });
    }
  }

  /**
   * 停止位置分享
   */
  async stopSharing(): Promise<void> {
    if (this.shareConfig) {
      await this.shareLinkService.stopShare(this.shareConfig.roomId);
    }

    await this.syncService.disconnect();
    this.isSharing = false;
    this.shareConfig = null;
    this.stopCountdown();
    this.members = [];
    this.clearAllMarkers();

    promptAction.showToast({ message: '位置分享已停止' });
  }

  /**
   * 开始倒计时
   */
  startCountdown(): void {
    this.stopCountdown();
    this.countdownTimer = setInterval(() => {
      if (!this.shareConfig) return;

      const remaining = this.shareConfig.expireTime - Date.now();
      if (remaining <= 0) {
        this.stopSharing();
        promptAction.showToast({ message: '位置分享已到期' });
        return;
      }

      const minutes = Math.floor(remaining / 60000);
      const seconds = Math.floor((remaining % 60000) / 1000);
      this.remainingTime = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
    }, 1000) as unknown as number;
  }

  /**
   * 停止倒计时
   */
  stopCountdown(): void {
    if (this.countdownTimer !== -1) {
      clearInterval(this.countdownTimer);
      this.countdownTimer = -1;
    }
  }

  /**
   * 更新成员地图标记
   */
  updateMemberMarker(location: UserLocation): void {
    if (!this.mapController) return;

    // 移除旧标记
    this.removeMemberMarker(location.userId);

    // 添加新标记
    const markerOptions: map.MarkerOptions = {
      position: { latitude: location.latitude, longitude: location.longitude },
      title: location.nickname || location.userId,
      snippet: `精度: ${location.accuracy}m`,
      anchor: { x: 0.5, y: 1.0 }
    };

    try {
      const marker = this.mapController.addMarker(markerOptions);
      this.memberMarkers.set(location.userId, marker);
    } catch (error) {
      console.error(`[LocationShare] 添加标记失败: ${location.userId}`);
    }
  }

  /**
   * 移除成员地图标记
   */
  removeMemberMarker(userId: string): void {
    const marker = this.memberMarkers.get(userId);
    if (marker) {
      marker.remove();
      this.memberMarkers.delete(userId);
    }
  }

  /**
   * 清除所有标记
   */
  clearAllMarkers(): void {
    this.memberMarkers.forEach(marker => marker.remove());
    this.memberMarkers.clear();
  }

  build() {
    Column() {
      // 顶部状态栏
      this.StatusBar()

      // 地图区域
      Stack() {
        MapComponent({
          mapOptions: {
            position: {
              target: {
                latitude: this.myLocation?.latitude ?? 39.9042,
                longitude: this.myLocation?.longitude ?? 116.4074
              },
              zoom: 15
            }
          },
          mapCallback: async () => {}
        })
        .width('100%')
        .height('100%')
        .onControllerReady((controller: map.MapComponentController) => {
          this.mapController = controller;
        })

        // 连接状态指示器
        if (this.wsState === WsState.RECONNECTING) {
          Row() {
            LoadingProgress().width(16).height(16).color('#FFD700')
            Text('重新连接中...')
              .fontSize(12)
              .fontColor('#FFD700')
              .margin({ left: 6 })
          }
          .padding({ left: 12, right: 12, top: 6, bottom: 6 })
          .backgroundColor('rgba(26, 26, 46, 0.9)')
          .borderRadius(16)
          .position({ x: '50%', y: 16 })
          .translate({ x: '-50%' })
        }
      }
      .layoutWeight(1)

      // 底部控制面板
      this.ControlPanel()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#1A1A2E')
  }

  /**
   * 状态栏组件
   */
  @Builder
  StatusBar() {
    Row() {
      // 分享状态
      if (this.isSharing) {
        Row() {
          Circle()
            .width(8)
            .height(8)
            .fill('#4ECDC4')
          Text('分享中')
            .fontSize(14)
            .fontColor('#4ECDC4')
            .margin({ left: 6 })
          Text(this.remainingTime)
            .fontSize(14)
            .fontColor('#FFFFFF')
            .margin({ left: 12 })
        }
      } else {
        Text('位置分享')
          .fontSize(16)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Medium)
      }

      Blank()

      // 在线人数
      if (this.isSharing) {
        Text(`${this.members.length + 1}人在线`)
          .fontSize(13)
          .fontColor('#8B8B9E')
      }
    }
    .width('100%')
    .padding({ left: 16, right: 16, top: 12, bottom: 12 })
    .backgroundColor('rgba(26, 26, 46, 0.95)')
  }

  /**
   * 底部控制面板
   */
  @Builder
  ControlPanel() {
    Column() {
      if (!this.isSharing) {
        // 分享设置区域
        this.ShareSettings()
      }

      // 操作按钮
      Row() {
        if (!this.isSharing) {
          Button('开始分享')
            .fontSize(16)
            .fontColor('#FFFFFF')
            .backgroundColor('#4ECDC4')
            .borderRadius(24)
            .width('100%')
            .height(48)
            .onClick(() => this.startSharing())
        } else {
          Row({ space: 12 }) {
            // 分享链接按钮
            Button('分享链接')
              .fontSize(14)
              .fontColor('#4ECDC4')
              .backgroundColor('rgba(78, 205, 196, 0.15)')
              .borderRadius(20)
              .layoutWeight(1)
              .height(44)
              .onClick(() => {
                if (this.shareConfig) {
                  this.shareLinkService.shareToSystem(this.shareConfig.shareLink);
                }
              })

            // 停止分享按钮
            Button('停止分享')
              .fontSize(14)
              .fontColor('#FFFFFF')
              .backgroundColor('#FF6B6B')
              .borderRadius(20)
              .layoutWeight(1)
              .height(44)
              .onClick(() => this.stopSharing())
          }
        }
      }
      .width('100%')
    }
    .width('100%')
    .padding(16)
    .backgroundColor('rgba(26, 26, 46, 0.95)')
    .borderRadius({ topLeft: 20, topRight: 20 })
  }

  /**
   * 分享设置组件
   */
  @Builder
  ShareSettings() {
    Column() {
      // 时长选择
      Text('分享时长')
        .fontSize(13)
        .fontColor('#8B8B9E')
        .margin({ bottom: 8 })

      Row({ space: 8 }) {
        ForEach([
          { label: '15分钟', value: 15 },
          { label: '1小时', value: 60 },
          { label: '8小时', value: 480 },
          { label: '永久', value: 0 }
        ], (item: { label: string; value: number }) => {
          Text(item.label)
            .fontSize(13)
            .fontColor(this.shareDuration === item.value ? '#4ECDC4' : '#8B8B9E')
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .backgroundColor(this.shareDuration === item.value ? 'rgba(78, 205, 196, 0.15)' : 'rgba(30, 30, 60, 0.8)')
            .borderRadius(16)
            .onClick(() => { this.shareDuration = item.value; })
        })
      }
      .width('100%')

      // 精度选择
      Text('位置精度')
        .fontSize(13)
        .fontColor('#8B8B9E')
        .margin({ top: 12, bottom: 8 })

      Row({ space: 8 }) {
        ForEach([
          { label: '精确', value: LocationPrecision.EXACT },
          { label: '城市', value: LocationPrecision.CITY },
          { label: '省份', value: LocationPrecision.PROVINCE }
        ], (item: { label: string; value: LocationPrecision }) => {
          Text(item.label)
            .fontSize(13)
            .fontColor(this.sharePrecision === item.value ? '#4ECDC4' : '#8B8B9E')
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .backgroundColor(this.sharePrecision === item.value ? 'rgba(78, 205, 196, 0.15)' : 'rgba(30, 30, 60, 0.8)')
            .borderRadius(16)
            .onClick(() => { this.sharePrecision = item.value; })
        })
      }
      .width('100%')
    }
    .width('100%')
    .margin({ bottom: 16 })
  }
}

四、踩坑与注意事项

4.1 WebSocket断线重连

问题:移动网络不稳定,WebSocket连接频繁断开,导致位置同步中断。

解决方案:实现指数退避重连策略:

// 指数退避重连算法
private scheduleReconnect(): void {
  const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
  // 第1次: 1秒, 第2次: 2秒, 第3次: 4秒, 第4次: 8秒, 第5次: 16秒
  // 最大间隔30秒,最多重试5次

  this.reconnectTimer = setTimeout(() => {
    this.connect(this.currentUserId);
  }, delay) as unknown as number;
}

4.2 位置上报频率优化

问题:3秒一次的位置上报在高频场景下耗电严重,在静止场景下浪费资源。

优化策略

场景 上报频率 判断条件
高速移动 1-2秒 speed > 5m/s
正常移动 3-5秒 0.5 < speed < 5m/s
低速/静止 10-30秒 speed < 0.5m/s
位置未变 不上报 与上次距离 < 5m
// 智能上报频率控制
private lastReportedLocation: UserLocation | null = null;

shouldReportLocation(current: UserLocation): boolean {
  if (!this.lastReportedLocation) return true;

  const distance = this.haversineDistance(
    this.lastReportedLocation.latitude, this.lastReportedLocation.longitude,
    current.latitude, current.longitude
  );

  // 距离变化小于5米,不上报
  if (distance < 5) return false;

  this.lastReportedLocation = current;
  return true;
}

4.3 隐私安全

问题:精确位置数据是高度敏感信息,泄露可能导致人身安全问题。

安全措施

  1. 传输加密:WebSocket必须使用WSS(TLS加密)
  2. 精度控制:提供城市级、省份级模糊选项
  3. 差分隐私:在精确位置上添加随机偏移
  4. 时效控制:分享必须设置过期时间
  5. 即时撤销:用户可随时停止分享
  6. 数据不留存:服务器不持久化位置历史

4.4 多人Marker管理

问题:多人同时在线时,Marker频繁添加/移除导致地图闪烁。

优化方案

  • 使用Marker池:预创建一定数量的Marker,复用而非重建
  • 批量更新:收集所有位置变更后一次性更新
  • 动画过渡:Marker位置变更时使用平滑动画

4.5 后台位置上报

问题:应用切到后台后,位置上报可能被系统暂停。

解决方案:与轨迹记录类似,需申请后台长驻任务,在module.json5中声明:

{
  "module": {
    "backgroundModes": ["location"]
  }
}

五、HarmonyOS 6适配

5.1 Network Kit增强

特性 HarmonyOS 5 HarmonyOS 6
WebSocket 基础WebSocket 支持WebSocket压缩扩展
网络质量感知 支持网络质量实时监测
弱网优化 自动消息压缩与合并
QUIC支持 不支持 支持QUIC协议

5.2 端到端加密

HarmonyOS 6新增了位置数据端到端加密能力:

// HarmonyOS 6:位置数据端到端加密
import { cryptoFramework } from '@kit.CryptoArchitectureKit';

async function encryptLocation(
  location: UserLocation,
  roomKey: string
): Promise<string> {
  const cipher = cryptoFramework.createCipher('AES256|GCM|PKCS7');
  const key = await cryptoFramework.createSymKeyGenerator('AES256')
    .convertKey({ data: new Uint8Array(Buffer.from(roomKey, 'base64')) });

  await cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, key, null);
  const encrypted = await cipher.doFinal({
    data: new Uint8Array(Buffer.from(JSON.stringify(location)))
  });

  return Buffer.from(encrypted.data).toString('base64');
}

5.3 通知增强

// HarmonyOS 6:实时位置通知
import { notificationManager } from '@kit.NotificationKit';
import { wantAgent } from '@kit.AbilityKit';

async function publishLocationNotification(
  memberName: string,
  distance: string
): Promise<void> {
  const wantAgentObj = await wantAgent.getWantAgent({
    wants: [{ bundleName: 'com.example.locationshare', abilityName: 'EntryAbility' }],
    requestCode: 0,
    operationType: wantAgent.OperationType.START_ABILITY
  });

  await notificationManager.publish({
    id: 1001,
    content: {
      notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
      normal: {
        title: '位置更新',
        text: `${memberName} 距你 ${distance}`
      }
    },
    wantAgent: wantAgentObj,
    // 新增:持续显示(类似导航通知)
    isOngoing: true,
    isUnremovable: false
  });
}

六、总结

本文系统讲解了HarmonyOS平台位置分享与实时位置同步的完整实现方案,核心要点如下:

flowchart TB
    classDef core fill:#FF6B6B,stroke:#2C3E50,color:#fff,font-weight:bold
    classDef key fill:#4ECDC4,stroke:#2C3E50,color:#fff,font-weight:bold
    classDef tip fill:#FFE66D,stroke:#2C3E50,color:#2C3E50

    A[位置分享核心能力]:::core --> B[实时同步]:::key
    A --> C[隐私控制]:::key
    A --> D[分享管理]:::key
    A --> E[多人展示]:::key

    B --> B1[WebSocket连接]:::tip
    B --> B2[断线重连]:::tip
    B --> B3[智能上报频率]:::tip

    C --> C1[精度模糊化]:::tip
    C --> C2[差分隐私]:::tip
    C --> C3[时效控制]:::tip

    D --> D1[短链生成]:::tip
    D --> D2[系统分享]:::tip
    D --> D3[即时撤销]:::tip

    E --> E1[多人Marker]:::tip
    E --> E2[批量更新]:::tip
    E --> E3[动画过渡]:::tip

    style A fill:#FF6B6B,stroke:#2C3E50,color:#fff

关键收获

  1. WebSocket是实现实时位置同步的核心通信协议,需配合心跳保活和断线重连
  2. 隐私控制是位置分享的生命线:精度模糊、时效控制、即时撤销缺一不可
  3. 智能上报频率在保证实时性的同时有效降低功耗
  4. 指数退避重连确保弱网环境下的连接可靠性
  5. 端到端加密(HarmonyOS 6)为位置数据提供传输安全保障

下一步

  • 第349篇将深入LBS社交与附近的人,讲解基于地理位置的社交发现与互动
  • 结合本篇的位置分享能力,可实现更丰富的LBS社交场景
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。