HarmonyOS APP开发:LBS营销与地理围栏推送

举报
Jack20 发表于 2026/06/22 14:10:39 2026/06/22
【摘要】 HarmonyOS APP开发:LBS营销与地理围栏推送核心要点:本文深入讲解HarmonyOS平台LBS营销与地理围栏推送的完整技术实现,涵盖地理围栏创建与监听、围栏事件触发机制、精准推送策略、营销活动管理、用户画像匹配、推送效果分析等核心能力,构建一个完整的LBS营销推送系统。项目说明核心KitLocation Kit、Notification Kit、Push Kit、Backgro...

HarmonyOS APP开发:LBS营销与地理围栏推送

核心要点:本文深入讲解HarmonyOS平台LBS营销与地理围栏推送的完整技术实现,涵盖地理围栏创建与监听、围栏事件触发机制、精准推送策略、营销活动管理、用户画像匹配、推送效果分析等核心能力,构建一个完整的LBS营销推送系统。

项目 说明
核心Kit Location Kit、Notification Kit、Push Kit、Background Tasks Kit
难度等级 ⭐⭐⭐⭐⭐

一、背景与动机

1.1 LBS营销的商业价值

LBS(Location-Based Service)营销是连接线下商业与移动用户的核心纽带。当用户走进商场、路过门店、抵达景区时,精准推送相关优惠和活动信息,实现"在对的时间、对的地点、推给对的人"。

LBS营销的核心商业价值:

  • 到店转化:用户进入商圈范围即推送优惠券,到店转化率提升3-5倍
  • 场景触发:基于位置的场景化营销,用户接受度远高于随机推送
  • 竞品拦截:用户进入竞品门店附近时推送更有吸引力的优惠
  • 数据沉淀:用户到店行为数据反哺用户画像,优化营销策略
  • O2O闭环:线上推送→线下到店→消费转化→数据回流

1.2 地理围栏推送的技术挑战

地理围栏(Geofence)是LBS营销的核心技术,但实现高质量的围栏推送面临多重挑战:

  • 围栏精度:室内外场景精度差异大,GPS在室内可能偏移数十米
  • 电量消耗:持续监听围栏事件是耗电大户,需平衡精度与功耗
  • 推送时机:进入围栏立即推送还是延迟推送?如何避免骚扰?
  • 围栏数量:一个城市可能有数千个围栏,如何高效管理?
  • 并发处理:高峰期大量用户同时触发围栏,服务端如何应对?
  • 合规要求:位置数据采集和推送需满足隐私法规要求

1.3 本文目标

构建一个完整的「LBS营销与地理围栏推送」系统,实现以下核心功能:

  1. 地理围栏的创建、管理与监听
  2. 围栏事件触发与精准推送
  3. 营销活动管理后台
  4. 用户画像匹配与推送策略
  5. 推送效果分析与数据看板

二、核心原理

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 geofence fill:#45B7D1,stroke:#2C3E50,color:#fff,font-weight:bold
    classDef push fill:#96CEB4,stroke:#2C3E50,color:#fff,font-weight:bold
    classDef analytics fill:#DDA0DD,stroke:#2C3E50,color:#fff,font-weight:bold

    A[用户APP]:::client --> B[Location Kit]:::client
    B --> C[位置采集]:::geofence
    C --> D[围栏匹配引擎]:::geofence

    E[营销管理后台]:::server --> F[围栏配置]:::geofence
    E --> G[活动管理]:::server
    E --> H[用户画像]:::server

    F --> I[围栏数据库]:::geofence
    I --> D

    D --> J{触发事件}:::geofence
    J --> K[进入围栏 ENTER]:::geofence
    J --> L[停留超时 DWELL]:::geofence
    J --> M[离开围栏 EXIT]:::geofence

    K --> N[推送决策引擎]:::push
    L --> N
    M --> N

    H --> N
    N --> O[推送频率控制]:::push
    N --> P[内容个性化]:::push
    N --> Q[推送通道]:::push

    Q --> R[Notification Kit]:::client
    Q --> S[Push Kit]:::server

    R --> T[用户接收]:::client
    S --> T

    T --> U[行为埋点]:::analytics
    U --> V[效果分析]:::analytics
    V --> W[数据看板]:::analytics
    W --> E

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

2.2 地理围栏核心概念

地理围栏(Geofence) 是一个虚拟的地理边界,当设备进入、离开或在其中停留时触发事件。

HarmonyOS Location Kit支持的围栏类型:

围栏类型 说明 典型场景
圆形围栏 以某点为圆心、指定半径的圆形区域 门店周边、商圈范围
多边形围栏 由多个顶点围成的多边形区域 商场内部、景区范围
停留围栏 在指定区域内停留超过阈值时间 用户在店内停留5分钟触发

围栏事件类型

┌─────────────────────────────────────────────┐
│              地理围栏事件时序                  │
│                                              │
│   用户位置:  ──○────○──●●●●●──○────○──→     │
│              外部  边界  内部停留  边界  外部  │
│                                              │
│   触发事件:       ↑    ↑↑↑↑↑    ↑            │
│                 ENTER  DWELL    EXIT                        (5min)                 │
└─────────────────────────────────────────────┘

2.3 推送决策引擎

推送决策引擎是LBS营销的核心大脑,它决定"推不推、推什么、怎么推":

flowchart LR
    classDef input fill:#4ECDC4,stroke:#2C3E50,color:#fff,font-weight:bold
    classDef decision fill:#45B7D1,stroke:#2C3E50,color:#fff,font-weight:bold
    classDef output fill:#96CEB4,stroke:#2C3E50,color:#fff,font-weight:bold
    classDef block fill:#FF6B6B,stroke:#2C3E50,color:#fff,font-weight:bold

    A[围栏触发事件]:::input --> B{用户画像匹配?}:::decision
    B -->|不匹配| C[❌ 不推送]:::block
    B -->|匹配| D{频率限制?}:::decision

    D -->|超频| E[⏸️ 延迟推送]:::decision
    D -->|未超频| F{时间段允许?}:::decision

    E --> G[加入延迟队列]:::decision
    G --> F

    F -->|不允许| H[🌙 暂存次日推送]:::output
    F -->|允许| I{停留时长?}:::decision

    I -->|刚进入| J[🎯 即时推送]:::output
    I -->|停留超时| K[🔥 深度推送]:::output

    style A fill:#FFE66D,stroke:#2C3E50,color:#2C3E50
    style B fill:#FFE66D,stroke:#2C3E50,color:#2C3E50
    style D fill:#FFE66D,stroke:#2C3E50,color:#2C3E50
    style F fill:#FFE66D,stroke:#2C3E50,color:#2C3E50
    style I fill:#FFE66D,stroke:#2C3E50,color:#2C3E50

2.4 推送频率控制算法

为防止推送骚扰,采用滑动窗口+令牌桶双重频率控制:

  • 滑动窗口:同一用户24小时内最多接收N条推送
  • 令牌桶:同一围栏对同一用户每小时最多触发1次推送
  • 冷却期:用户关闭推送后48小时内不再推送

三、代码实战

3.1 地理围栏管理服务

// GeofenceManager.ets — 地理围栏创建、管理与监听服务
import { geoLocationManager } from '@kit.LocationKit'
import { BusinessError } from '@kit.BasicServicesKit'

// 围栏类型枚举
export enum GeofenceType {
  CIRCLE = 'circle',       // 圆形围栏
  POLYGON = 'polygon'      // 多边形围栏
}

// 围栏事件类型
export enum GeofenceEventType {
  ENTER = 'enter',         // 进入围栏
  EXIT = 'exit',           // 离开围栏
  DWELL = 'dwell'          // 停留超时
}

// 围栏配置
export interface GeofenceConfig {
  id: string                         // 围栏唯一ID
  name: string                       // 围栏名称
  type: GeofenceType                 // 围栏类型
  latitude: number                   // 圆心纬度(圆形围栏)
  longitude: number                  // 圆心经度(圆形围栏)
  radius: number                     // 半径(米,圆形围栏)
  vertices?: Array<{ lat: number, lon: number }>  // 顶点(多边形围栏)
  dwellTimeout?: number              // 停留超时(毫秒),默认300000=5分钟
  expireTime?: number                // 过期时间戳,0=永不过期
  marketingId?: string               // 关联营销活动ID
  priority: number                   // 优先级(1-10)
}

// 围栏触发事件
export interface GeofenceEvent {
  geofenceId: string                 // 围栏ID
  eventType: GeofenceEventType       // 事件类型
  timestamp: number                  // 触发时间戳
  latitude: number                   // 触发时纬度
  longitude: number                  // 触发时经度
  accuracy: number                   // 定位精度(米)
  dwellDuration?: number             // 停留时长(毫秒,仅DWELL事件)
}

// 围栏请求结果
export interface GeofenceRequest {
  requestId: string
  geofences: Array<geoLocationManager.Geofence>
  intend: Want
}

export class GeofenceManagerService {
  private static instance: GeofenceManagerService
  private activeFences: Map<string, GeofenceConfig> = new Map()
  private eventCallbacks: Map<string, Array<(event: GeofenceEvent) => void>> = new Map()
  private geofenceClient: geoLocationManager.LocatorProxy | null = null

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

  /**
   * 初始化围栏客户端
   */
  init(): void {
    try {
      this.geofenceClient = geoLocationManager.createLocatorProxy(
        geoLocationManager.LocationRequestPriority.FIRST_FIX
      )
      console.info('[地理围栏] 客户端初始化成功')
    } catch (error) {
      const err = error as BusinessError
      console.error(`[地理围栏] 初始化失败: ${err.code} - ${err.message}`)
    }
  }

  /**
   * 添加圆形地理围栏
   * @param config 围栏配置
   * @returns 是否添加成功
   */
  addCircleGeofence(config: GeofenceConfig): boolean {
    if (config.type !== GeofenceType.CIRCLE) {
      console.error('[地理围栏] 配置类型与围栏类型不匹配')
      return false
    }

    try {
      // 构建HarmonyOS地理围栏请求
      const geofence: geoLocationManager.Geofence = {
        latitude: config.latitude,
        longitude: config.longitude,
        radius: config.radius,
        expiration: config.expireTime ?? 0,
        dwellDelayTime: config.dwellTimeout ?? 300000,  // 默认停留5分钟
        notificationItem: {
          title: `📍 ${config.name}`,
          text: '您已进入营销区域',
        }
      }

      // 创建围栏请求
      const request: geoLocationManager.GeofenceRequest = {
        priority: geoLocationManager.LocationRequestPriority.ACCURACY,
        scenario: geoLocationManager.LocationRequestScenario.UNSET,
        geofences: [geofence],
      }

      // 注册围栏监听
      geoLocationManager.on('geofenceStatusChange', request,
        (geofenceEvent: geoLocationManager.GeofenceResult) => {
          this.handleGeofenceEvent(geofenceEvent, config)
        }
      )

      // 缓存围栏配置
      this.activeFences.set(config.id, config)
      console.info(`[地理围栏] 圆形围栏已添加: ${config.name}, 半径${config.radius}m`)
      return true
    } catch (error) {
      const err = error as BusinessError
      console.error(`[地理围栏] 添加失败: ${err.code} - ${err.message}`)
      return false
    }
  }

  /**
   * 批量添加围栏(营销场景通常需要大量围栏)
   * @param configs 围栏配置数组
   * @returns 成功添加的数量
   */
  addBatchGeofences(configs: GeofenceConfig[]): number {
    let successCount = 0
    // HarmonyOS单次请求最多支持100个围栏
    const batchSize = 100

    for (let i = 0; i < configs.length; i += batchSize) {
      const batch = configs.slice(i, i + batchSize)
      const geofences: geoLocationManager.Geofence[] = batch.map(config => ({
        latitude: config.latitude,
        longitude: config.longitude,
        radius: config.radius,
        expiration: config.expireTime ?? 0,
        dwellDelayTime: config.dwellTimeout ?? 300000,
        notificationItem: {
          title: `📍 ${config.name}`,
          text: '您已进入营销区域',
        }
      }))

      try {
        const request: geoLocationManager.GeofenceRequest = {
          priority: geoLocationManager.LocationRequestPriority.ACCURACY,
          scenario: geoLocationManager.LocationRequestScenario.UNSET,
          geofences,
        }

        geoLocationManager.on('geofenceStatusChange', request,
          (geofenceEvent: geoLocationManager.GeofenceResult) => {
            // 找到对应的围栏配置
            const matchedConfig = batch.find(c =>
              Math.abs(c.latitude - geofenceEvent.location?.latitude ?? 0) < 0.001 &&
              Math.abs(c.longitude - geofenceEvent.location?.longitude ?? 0) < 0.001
            )
            if (matchedConfig) {
              this.handleGeofenceEvent(geofenceEvent, matchedConfig)
            }
          }
        )

        batch.forEach(config => this.activeFences.set(config.id, config))
        successCount += batch.length
      } catch (error) {
        console.error(`[地理围栏] 批量添加失败: ${error}`)
      }
    }

    console.info(`[地理围栏] 批量添加完成: ${successCount}/${configs.length}`)
    return successCount
  }

  /**
   * 处理围栏触发事件
   */
  private handleGeofenceEvent(
    geofenceEvent: geoLocationManager.GeofenceResult,
    config: GeofenceConfig
  ): void {
    // 映射HarmonyOS围栏事件类型
    let eventType: GeofenceEventType
    switch (geofenceEvent.transitionStatus) {
      case geoLocationManager.GeofenceTransitionStatus.ENTER:
        eventType = GeofenceEventType.ENTER
        break
      case geoLocationManager.GeofenceTransitionStatus.EXIT:
        eventType = GeofenceEventType.EXIT
        break
      case geoLocationManager.GeofenceTransitionStatus.DWELL:
        eventType = GeofenceEventType.DWELL
        break
      default:
        return
    }

    const event: GeofenceEvent = {
      geofenceId: config.id,
      eventType,
      timestamp: Date.now(),
      latitude: geofenceEvent.location?.latitude ?? 0,
      longitude: geofenceEvent.location?.longitude ?? 0,
      accuracy: geofenceEvent.location?.accuracy ?? 0,
      dwellDuration: eventType === GeofenceEventType.DWELL
        ? config.dwellTimeout : undefined
    }

    console.info(`[地理围栏] 事件触发: ${config.name} - ${eventType}`)

    // 通知所有监听回调
    const callbacks = this.eventCallbacks.get(config.id) ?? []
    callbacks.forEach(cb => cb(event))

    // 通知全局回调
    const globalCallbacks = this.eventCallbacks.get('*') ?? []
    globalCallbacks.forEach(cb => cb(event))
  }

  /**
   * 注册围栏事件回调
   * @param geofenceId 围栏ID,'*'表示监听所有围栏
   * @param callback 事件回调
   */
  onGeofenceEvent(
    geofenceId: string,
    callback: (event: GeofenceEvent) => void
  ): void {
    const callbacks = this.eventCallbacks.get(geofenceId) ?? []
    callbacks.push(callback)
    this.eventCallbacks.set(geofenceId, callbacks)
  }

  /**
   * 移除围栏
   */
  removeGeofence(geofenceId: string): void {
    this.activeFences.delete(geofenceId)
    this.eventCallbacks.delete(geofenceId)
    console.info(`[地理围栏] 已移除: ${geofenceId}`)
  }

  /**
   * 获取所有活跃围栏
   */
  getActiveFences(): GeofenceConfig[] {
    return Array.from(this.activeFences.values())
  }

  /**
   * 清除所有围栏
   */
  clearAllFences(): void {
    this.activeFences.clear()
    this.eventCallbacks.clear()
    console.info('[地理围栏] 已清除所有围栏')
  }
}

3.2 推送决策与频率控制引擎

// PushDecisionEngine.ets — 推送决策与频率控制引擎
import { notificationManager } from '@kit.NotificationKit'
import { http } from '@kit.NetworkKit'
import { GeofenceEvent, GeofenceEventType } from './GeofenceManager'

// 推送消息
export interface PushMessage {
  id: string                    // 消息唯一ID
  title: string                 // 推送标题
  body: string                  // 推送内容
  imageUrl?: string             // 大图URL
  deepLink?: string             // 点击跳转链接
  marketingId: string           // 营销活动ID
  geofenceId: string            // 关联围栏ID
  eventType: GeofenceEventType  // 触发事件类型
  priority: 'high' | 'normal' | 'low'  // 推送优先级
  expireTime: number            // 推送过期时间
  tags: string[]                // 用户标签匹配
}

// 推送策略
export interface PushStrategy {
  maxDailyPushes: number        // 每日最大推送数
  minIntervalMs: number         // 同一围栏最小推送间隔(毫秒)
  quietHoursStart: number       // 免打扰开始(小时,如22)
  quietHoursEnd: number         // 免打扰结束(小时,如8)
  dwellOnlyMode: boolean        // 仅停留触发模式(更精准)
  userConsentRequired: boolean  // 是否需要用户同意
}

// 推送记录
interface PushRecord {
  geofenceId: string
  timestamp: number
  marketingId: string
  userAction: 'received' | 'clicked' | 'dismissed' | 'converted'
}

// 用户画像(简化版)
interface UserProfile {
  userId: string
  ageGroup: string
  gender: string
  interests: string[]
  spendLevel: 'low' | 'medium' | 'high'
  lastVisitTime: number
  totalVisits: number
  pushPreference: 'aggressive' | 'moderate' | 'minimal'
}

export class PushDecisionEngine {
  private static instance: PushDecisionEngine
  private readonly BASE_URL = 'https://lbs-marketing.example.com/api'

  // 推送策略(默认值)
  private strategy: PushStrategy = {
    maxDailyPushes: 5,
    minIntervalMs: 3600000,     // 1小时
    quietHoursStart: 22,
    quietHoursEnd: 8,
    dwellOnlyMode: false,
    userConsentRequired: true
  }

  // 推送记录(本地缓存)
  private pushHistory: PushRecord[] = []
  private dailyPushCount: number = 0
  private lastPushDate: string = ''

  // 用户画像
  private userProfile: UserProfile | null = null

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

  /**
   * 处理围栏事件,决策是否推送
   * @param event 围栏事件
   * @returns 推送消息(如果决策推送),null表示不推送
   */
  async processGeofenceEvent(event: GeofenceEvent): Promise<PushMessage | null> {
    // 步骤1:检查免打扰时段
    if (this.isQuietHours()) {
      console.info('[推送引擎] 当前为免打扰时段,跳过推送')
      return null
    }

    // 步骤2:检查每日推送上限
    this.resetDailyCountIfNeeded()
    if (this.dailyPushCount >= this.strategy.maxDailyPushes) {
      console.info('[推送引擎] 今日推送已达上限')
      return null
    }

    // 步骤3:检查围栏推送间隔
    const lastPushToThisFence = this.pushHistory
      .filter(r => r.geofenceId === event.geofenceId)
      .sort((a, b) => b.timestamp - a.timestamp)[0]

    if (lastPushToThisFence) {
      const elapsed = Date.now() - lastPushToThisFence.timestamp
      if (elapsed < this.strategy.minIntervalMs) {
        const remaining = Math.ceil((this.strategy.minIntervalMs - elapsed) / 60000)
        console.info(`[推送引擎] 围栏冷却中,还需${remaining}分钟`)
        return null
      }
    }

    // 步骤4:停留模式检查
    if (this.strategy.dwellOnlyMode && event.eventType !== GeofenceEventType.DWELL) {
      console.info('[推送引擎] 仅停留模式,忽略非停留事件')
      return null
    }

    // 步骤5:用户画像匹配
    const matchedMessage = await this.matchUserAndFetchMessage(event)
    if (!matchedMessage) {
      console.info('[推送引擎] 无匹配的营销消息')
      return null
    }

    // 步骤6:用户偏好过滤
    if (this.userProfile?.pushPreference === 'minimal' && event.eventType !== GeofenceEventType.DWELL) {
      return null
    }

    // 决策通过,返回推送消息
    return matchedMessage
  }

  /**
   * 执行推送
   * @param message 推送消息
   */
  async executePush(message: PushMessage): Promise<boolean> {
    try {
      // 构建HarmonyOS通知
      const notificationRequest: notificationManager.NotificationRequest = {
        id: parseInt(message.id.slice(-8), 16) % 2147483647,
        content: {
          notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
          normal: {
            title: message.title,
            text: message.body,
          }
        },
        actionButtons: [
          { title: '立即查看', wantAgent: message.deepLink } as notificationManager.ActionButton
        ],
        deliveryTime: Date.now(),
        showDeliveryTime: true,
      }

      // 如果有大图,使用多行文本通知
      if (message.imageUrl) {
        notificationRequest.content = {
          notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_LONG_TEXT,
          longText: {
            title: message.title,
            text: message.body,
            longText: message.body,
            briefText: message.title,
            expandedTitle: message.title,
          }
        }
      }

      // 发布通知
      await notificationManager.publish(notificationRequest)

      // 记录推送历史
      this.pushHistory.push({
        geofenceId: message.geofenceId,
        timestamp: Date.now(),
        marketingId: message.marketingId,
        userAction: 'received'
      })
      this.dailyPushCount++

      // 上报推送事件到服务端
      this.reportPushEvent(message, 'delivered')

      console.info(`[推送引擎] 推送成功: ${message.title}`)
      return true
    } catch (error) {
      console.error(`[推送引擎] 推送失败: ${error}`)
      return false
    }
  }

  /**
   * 用户画像匹配与消息获取
   */
  private async matchUserAndFetchMessage(event: GeofenceEvent): Promise<PushMessage | null> {
    const request = http.createHttp()
    try {
      const response = await request.request(
        `${this.BASE_URL}/push/match`,
        {
          method: http.RequestMethod.POST,
          header: { 'Content-Type': 'application/json' },
          extraData: JSON.stringify({
            geofenceId: event.geofenceId,
            eventType: event.eventType,
            latitude: event.latitude,
            longitude: event.longitude,
            dwellDuration: event.dwellDuration,
            userProfile: this.userProfile,
            timestamp: event.timestamp
          })
        }
      )

      if (response.responseCode === 200) {
        const result = JSON.parse(response.result as string)
        return result.data as PushMessage
      }
      return null
    } finally {
      request.destroy()
    }
  }

  /**
   * 检查是否在免打扰时段
   */
  private isQuietHours(): boolean {
    const hour = new Date().getHours()
    const { quietHoursStart, quietHoursEnd } = this.strategy
    if (quietHoursStart > quietHoursEnd) {
      // 跨午夜:如22:00-08:00
      return hour >= quietHoursStart || hour < quietHoursEnd
    }
    return hour >= quietHoursStart && hour < quietHoursEnd
  }

  /**
   * 每日推送计数重置
   */
  private resetDailyCountIfNeeded(): void {
    const today = new Date().toISOString().slice(0, 10)
    if (this.lastPushDate !== today) {
      this.dailyPushCount = 0
      this.lastPushDate = today
      // 清理7天前的推送记录
      const sevenDaysAgo = Date.now() - 7 * 24 * 3600000
      this.pushHistory = this.pushHistory.filter(r => r.timestamp > sevenDaysAgo)
    }
  }

  /**
   * 更新推送策略
   */
  updateStrategy(strategy: Partial<PushStrategy>): void {
    this.strategy = { ...this.strategy, ...strategy }
  }

  /**
   * 更新用户画像
   */
  updateUserProfile(profile: UserProfile): void {
    this.userProfile = profile
  }

  /**
   * 记录用户推送行为(点击/关闭/转化)
   */
  recordUserAction(messageId: string, action: PushRecord['userAction']): void {
    const record = this.pushHistory.find(r =>
      r.marketingId === messageId || r.geofenceId === messageId
    )
    if (record) {
      record.userAction = action
      this.reportPushEvent(null, action)
    }
  }

  /**
   * 上报推送事件到服务端
   */
  private async reportPushEvent(
    message: PushMessage | null,
    action: string
  ): Promise<void> {
    const request = http.createHttp()
    try {
      await request.request(`${this.BASE_URL}/push/report`, {
        method: http.RequestMethod.POST,
        header: { 'Content-Type': 'application/json' },
        extraData: JSON.stringify({
          messageId: message?.id,
          marketingId: message?.marketingId,
          geofenceId: message?.geofenceId,
          action,
          timestamp: Date.now()
        })
      })
    } finally {
      request.destroy()
    }
  }

  /**
   * 获取推送统计
   */
  getPushStats(): {
    dailyCount: number
    totalCount: number
    clickRate: number
    conversionRate: number
  } {
    const total = this.pushHistory.length
    const clicked = this.pushHistory.filter(r => r.userAction === 'clicked').length
    const converted = this.pushHistory.filter(r => r.userAction === 'converted').length

    return {
      dailyCount: this.dailyPushCount,
      totalCount: total,
      clickRate: total > 0 ? clicked / total : 0,
      conversionRate: total > 0 ? converted / total : 0
    }
  }
}

3.3 营销活动管理

// MarketingCampaignService.ets — 营销活动管理服务
import { http } from '@kit.NetworkKit'
import { GeofenceConfig, GeofenceType, GeofenceManagerService } from './GeofenceManager'
import { PushDecisionEngine, PushStrategy } from './PushDecisionEngine'

// 营销活动状态
export enum CampaignStatus {
  DRAFT = 'draft',
  ACTIVE = 'active',
  PAUSED = 'paused',
  COMPLETED = 'completed',
  EXPIRED = 'expired'
}

// 营销活动
export interface MarketingCampaign {
  id: string
  name: string                       // 活动名称
  description: string                // 活动描述
  status: CampaignStatus             // 活动状态
  startTime: number                  // 开始时间
  endTime: number                    // 结束时间
  budget: number                     // 预算(分)
  spent: number                      // 已花费(分)

  // 围栏配置
  geofences: GeofenceConfig[]        // 关联围栏列表

  // 推送内容模板
  pushTemplates: PushTemplate[]      // 推送模板

  // 目标用户
  targetUserTags: string[]           // 目标用户标签
  targetAgeRange?: [number, number]  // 目标年龄段
  targetSpendLevel?: string[]        // 目标消费水平

  // 推送策略
  pushStrategy: PushStrategy

  // 效果数据
  metrics: CampaignMetrics
}

// 推送模板
export interface PushTemplate {
  id: string
  eventType: string                  // 触发事件类型
  title: string                      // 标题模板
  body: string                       // 正文模板
  deepLink: string                   // 跳转链接
  priority: 'high' | 'normal' | 'low'
}

// 活动效果指标
export interface CampaignMetrics {
  impressions: number                // 曝光次数
  clicks: number                     // 点击次数
  conversions: number                // 转化次数
  revenue: number                    // 产生收入(分)
  ctr: number                        // 点击率
  cvr: number                        // 转化率
  roi: number                        // 投资回报率
}

export class MarketingCampaignService {
  private static instance: MarketingCampaignService
  private readonly BASE_URL = 'https://lbs-marketing.example.com/api/campaigns'
  private campaigns: Map<string, MarketingCampaign> = new Map()
  private geofenceManager = GeofenceManagerService.getInstance()
  private pushEngine = PushDecisionEngine.getInstance()

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

  /**
   * 创建营销活动
   * @param campaign 活动配置
   */
  async createCampaign(campaign: MarketingCampaign): Promise<boolean> {
    try {
      // 1. 上传活动到服务端
      const request = http.createHttp()
      const response = await request.request(`${this.BASE_URL}/create`, {
        method: http.RequestMethod.POST,
        header: { 'Content-Type': 'application/json' },
        extraData: JSON.stringify(campaign)
      })
      request.destroy()

      if (response.responseCode !== 200) {
        console.error('[营销活动] 创建失败')
        return false
      }

      // 2. 缓存活动
      this.campaigns.set(campaign.id, campaign)

      // 3. 如果活动是激活状态,立即注册围栏
      if (campaign.status === CampaignStatus.ACTIVE) {
        await this.activateCampaign(campaign.id)
      }

      console.info(`[营销活动] 创建成功: ${campaign.name}`)
      return true
    } catch (error) {
      console.error(`[营销活动] 创建异常: ${error}`)
      return false
    }
  }

  /**
   * 激活营销活动——注册所有围栏并启动推送
   * @param campaignId 活动ID
   */
  async activateCampaign(campaignId: string): Promise<boolean> {
    const campaign = this.campaigns.get(campaignId)
    if (!campaign) {
      console.error('[营销活动] 活动不存在')
      return false
    }

    // 注册所有围栏
    let fenceCount = 0
    for (const fence of campaign.geofences) {
      // 关联营销活动ID
      fence.marketingId = campaignId

      const success = this.geofenceManager.addCircleGeofence(fence)
      if (success) {
        fenceCount++
        // 注册围栏事件回调
        this.geofenceManager.onGeofenceEvent(fence.id, async (event) => {
          // 通过推送决策引擎处理
          const message = await this.pushEngine.processGeofenceEvent(event)
          if (message) {
            // 匹配推送模板
            const template = campaign.pushTemplates.find(
              t => t.eventType === event.eventType
            )
            if (template) {
              message.title = template.title
              message.body = template.body
              message.deepLink = template.deepLink
              message.marketingId = campaignId
            }
            await this.pushEngine.executePush(message)
          }
        })
      }
    }

    // 更新推送策略
    this.pushEngine.updateStrategy(campaign.pushStrategy)

    // 更新活动状态
    campaign.status = CampaignStatus.ACTIVE

    console.info(`[营销活动] 已激活: ${campaign.name}, 注册${fenceCount}个围栏`)
    return true
  }

  /**
   * 暂停营销活动
   */
  pauseCampaign(campaignId: string): void {
    const campaign = this.campaigns.get(campaignId)
    if (campaign) {
      campaign.status = CampaignStatus.PAUSED
      // 移除所有关联围栏
      campaign.geofences.forEach(fence => {
        this.geofenceManager.removeGeofence(fence.id)
      })
      console.info(`[营销活动] 已暂停: ${campaign.name}`)
    }
  }

  /**
   * 获取活动效果数据
   */
  async fetchCampaignMetrics(campaignId: string): Promise<CampaignMetrics | null> {
    const request = http.createHttp()
    try {
      const response = await request.request(
        `${this.BASE_URL}/${campaignId}/metrics`,
        { method: http.RequestMethod.GET }
      )
      if (response.responseCode === 200) {
        const result = JSON.parse(response.result as string)
        const metrics = result.data as CampaignMetrics

        // 更新本地缓存
        const campaign = this.campaigns.get(campaignId)
        if (campaign) {
          campaign.metrics = metrics
        }
        return metrics
      }
      return null
    } finally {
      request.destroy()
    }
  }

  /**
   * 获取所有活动列表
   */
  getCampaigns(): MarketingCampaign[] {
    return Array.from(this.campaigns.values())
  }

  /**
   * 获取活跃活动
   */
  getActiveCampaigns(): MarketingCampaign[] {
    return Array.from(this.campaigns.values())
      .filter(c => c.status === CampaignStatus.ACTIVE)
  }
}

3.4 LBS营销主界面与数据看板

// LbsMarketingPage.ets — LBS营销主界面
// 包含围栏管理、活动管理、推送设置、效果看板
import { GeofenceManagerService, GeofenceConfig, GeofenceType, GeofenceEvent, GeofenceEventType } from './GeofenceManager'
import { PushDecisionEngine, PushStrategy } from './PushDecisionEngine'
import { MarketingCampaignService, MarketingCampaign, CampaignStatus, CampaignMetrics } from './MarketingCampaignService'

@Entry
@Component
struct LbsMarketingPage {
  @State activeTab: number = 0
  @State campaigns: MarketingCampaign[] = []
  @State activeFences: GeofenceConfig[] = []
  @State recentEvents: GeofenceEvent[] = []
  @State pushStats: { dailyCount: number, totalCount: number, clickRate: number, conversionRate: number } =
    { dailyCount: 0, totalCount: 0, clickRate: 0, conversionRate: 0 }
  @State showCreateSheet: boolean = false
  @State isLoading: boolean = false

  private geofenceManager = GeofenceManagerService.getInstance()
  private pushEngine = PushDecisionEngine.getInstance()
  private campaignService = MarketingCampaignService.getInstance()

  aboutToAppear(): void {
    this.geofenceManager.init()
    this.loadData()

    // 监听所有围栏事件
    this.geofenceManager.onGeofenceEvent('*', (event: GeofenceEvent) => {
      this.recentEvents.unshift(event)
      if (this.recentEvents.length > 50) {
        this.recentEvents = this.recentEvents.slice(0, 50)
      }
    })
  }

  loadData(): void {
    this.campaigns = this.campaignService.getCampaigns()
    this.activeFences = this.geofenceManager.getActiveFences()
    this.pushStats = this.pushEngine.getPushStats()
  }

  build() {
    Navigation() {
      Column() {
        // 顶部概览卡片
        this.OverviewCard()

        // 标签页
        Tabs({ index: $$this.activeTab }) {
          TabContent() { this.CampaignTab() }.tabBar('🎯 活动')
          TabContent() { this.GeofenceTab() }.tabBar('📍 围栏')
          TabContent() { this.EventTab() }.tabBar('⚡ 事件')
          TabContent() { this.AnalyticsTab() }.tabBar('📊 看板')
        }
        .width('100%')
        .layoutWeight(1)
        .barMode(BarMode.Fixed)
        .barBackgroundColor('#161B22')
        .barInactiveColor('#8B949E')
        .barActiveColor('#58A6FF')
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#0D1117')
    }
    .title('LBS营销中心')
    .titleMode(NavigationTitleMode.Mini)
    .backgroundColor('#0D1117')
  }

  // ===== 顶部概览 =====
  @Builder
  OverviewCard() {
    Row() {
      // 活跃活动数
      Column() {
        Text(`${this.campaigns.filter(c => c.status === CampaignStatus.ACTIVE).length}`)
          .fontSize(28)
          .fontColor('#3FB950')
          .fontWeight(FontWeight.Bold)
        Text('活跃活动')
          .fontSize(11)
          .fontColor('#8B949E')
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Center)

      // 围栏数
      Column() {
        Text(`${this.activeFences.length}`)
          .fontSize(28)
          .fontColor('#58A6FF')
          .fontWeight(FontWeight.Bold)
        Text('围栏数')
          .fontSize(11)
          .fontColor('#8B949E')
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Center)

      // 今日推送
      Column() {
        Text(`${this.pushStats.dailyCount}`)
          .fontSize(28)
          .fontColor('#F0883E')
          .fontWeight(FontWeight.Bold)
        Text('今日推送')
          .fontSize(11)
          .fontColor('#8B949E')
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Center)

      // 点击率
      Column() {
        Text(`${(this.pushStats.clickRate * 100).toFixed(1)}%`)
          .fontSize(28)
          .fontColor('#DDA0DD')
          .fontWeight(FontWeight.Bold)
        Text('点击率')
          .fontSize(11)
          .fontColor('#8B949E')
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .padding(16)
    .margin({ top: 8, left: 16, right: 16 })
    .backgroundColor('#161B22')
    .borderRadius(12)
    .border({ width: 1, color: '#30363D' })
  }

  // ===== 活动管理标签 =====
  @Builder
  CampaignTab() {
    Column() {
      // 创建活动按钮
      Button('+ 创建营销活动')
        .width('90%')
        .height(44)
        .fontSize(15)
        .fontColor('#FFFFFF')
        .backgroundColor('#238636')
        .borderRadius(10)
        .margin({ top: 16, bottom: 12 })
        .onClick(() => { this.showCreateSheet = true })

      // 活动列表
      List({ space: 10 }) {
        ForEach(this.campaigns, (campaign: MarketingCampaign) => {
          ListItem() {
            this.CampaignCard(campaign)
          }
        }, (campaign: MarketingCampaign) => campaign.id)

        if (this.campaigns.length === 0) {
          ListItem() {
            Column() {
              Text('📋')
                .fontSize(48)
              Text('暂无营销活动')
                .fontSize(14)
                .fontColor('#8B949E')
                .margin({ top: 8 })
              Text('点击上方按钮创建第一个活动')
                .fontSize(12)
                .fontColor('#484F58')
                .margin({ top: 4 })
            }
            .width('100%')
            .padding(40)
            .justifyContent(FlexAlign.Center)
          }
        }
      }
      .width('100%')
      .layoutWeight(1)
      .padding({ left: 16, right: 16 })
    }
    .width('100%')
    .height('100%')
  }

  // ===== 活动卡片 =====
  @Builder
  CampaignCard(campaign: MarketingCampaign) {
    Column() {
      Row() {
        Text(campaign.name)
          .fontSize(16)
          .fontColor('#E6EDF3')
          .fontWeight(FontWeight.Medium)
          .layoutWeight(1)

        // 状态标签
        Text(this.getStatusLabel(campaign.status))
          .fontSize(11)
          .fontColor(this.getStatusColor(campaign.status))
          .backgroundColor(this.getStatusBgColor(campaign.status))
          .borderRadius(4)
          .padding({ left: 6, right: 6, top: 2, bottom: 2 })
      }

      Text(campaign.description)
        .fontSize(12)
        .fontColor('#8B949E')
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
        .margin({ top: 6 })

      // 效果指标
      Row() {
        Text(`围栏: ${campaign.geofences.length}`)
          .fontSize(11)
          .fontColor('#58A6FF')
        Text(`曝光: ${campaign.metrics.impressions}`)
          .fontSize(11)
          .fontColor('#8B949E')
          .margin({ left: 12 })
        Text(`点击: ${campaign.metrics.clicks}`)
          .fontSize(11)
          .fontColor('#8B949E')
          .margin({ left: 12 })
        Text(`CTR: ${(campaign.metrics.ctr * 100).toFixed(1)}%`)
          .fontSize(11)
          .fontColor('#F0883E')
          .margin({ left: 12 })
      }
      .margin({ top: 8 })

      // 操作按钮
      Row() {
        if (campaign.status === CampaignStatus.ACTIVE) {
          Button('暂停')
            .fontSize(12)
            .fontColor('#F0883E')
            .backgroundColor('#1AF0883E')
            .borderRadius(6)
            .height(28)
            .onClick(() => {
              this.campaignService.pauseCampaign(campaign.id)
              this.loadData()
            })
        } else if (campaign.status === CampaignStatus.PAUSED) {
          Button('恢复')
            .fontSize(12)
            .fontColor('#3FB950')
            .backgroundColor('#1A3FB950')
            .borderRadius(6)
            .height(28)
            .onClick(() => {
              this.campaignService.activateCampaign(campaign.id)
              this.loadData()
            })
        }

        Button('查看详情')
          .fontSize(12)
          .fontColor('#58A6FF')
          .backgroundColor('#1A58A6FF')
          .borderRadius(6)
          .height(28)
          .margin({ left: 8 })
      }
      .margin({ top: 10 })
    }
    .width('100%')
    .padding(14)
    .backgroundColor('#161B22')
    .borderRadius(10)
    .border({ width: 1, color: '#30363D' })
  }

  // ===== 围栏管理标签 =====
  @Builder
  GeofenceTab() {
    List({ space: 10 }) {
      ForEach(this.activeFences, (fence: GeofenceConfig) => {
        ListItem() {
          Row() {
            Column() {
              Text(fence.name)
                .fontSize(14)
                .fontColor('#E6EDF3')
                .fontWeight(FontWeight.Medium)
              Text(`${fence.latitude.toFixed(4)}, ${fence.longitude.toFixed(4)}`)
                .fontSize(11)
                .fontColor('#8B949E')
                .fontFamily('monospace')
                .margin({ top: 2 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)

            Text(`${fence.radius}m`)
              .fontSize(14)
              .fontColor('#F0883E')
              .fontWeight(FontWeight.Medium)

            Button('删除')
              .fontSize(11)
              .fontColor('#F85149')
              .backgroundColor('#1AF85149')
              .borderRadius(6)
              .height(26)
              .margin({ left: 8 })
              .onClick(() => {
                this.geofenceManager.removeGeofence(fence.id)
                this.loadData()
              })
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#161B22')
          .borderRadius(8)
          .border({ width: 1, color: '#30363D' })
        }
      }, (fence: GeofenceConfig) => fence.id)

      if (this.activeFences.length === 0) {
        ListItem() {
          Column() {
            Text('📍')
              .fontSize(48)
            Text('暂无活跃围栏')
              .fontSize(14)
              .fontColor('#8B949E')
              .margin({ top: 8 })
          }
          .width('100%')
          .padding(40)
        }
      }
    }
    .width('100%')
    .layoutWeight(1)
    .padding({ left: 16, right: 16, top: 12 })
  }

  // ===== 事件流标签 =====
  @Builder
  EventTab() {
    List({ space: 8 }) {
      ForEach(this.recentEvents, (event: GeofenceEvent) => {
        ListItem() {
          Row() {
            Text(this.getEventIcon(event.eventType))
              .fontSize(20)

            Column() {
              Text(`${event.geofenceId} - ${event.eventType}`)
                .fontSize(13)
                .fontColor('#E6EDF3')
              Text(this.formatTime(event.timestamp))
                .fontSize(11)
                .fontColor('#8B949E')
                .margin({ top: 2 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            .margin({ left: 8 })

            Text(`±${event.accuracy.toFixed(0)}m`)
              .fontSize(11)
              .fontColor('#484F58')
          }
          .width('100%')
          .padding(10)
          .backgroundColor('#161B22')
          .borderRadius(8)
        }
      }, (event: GeofenceEvent, index: number) => `${event.geofenceId}_${index}`)

      if (this.recentEvents.length === 0) {
        ListItem() {
          Column() {
            Text('⚡')
              .fontSize(48)
            Text('等待围栏事件...')
              .fontSize(14)
              .fontColor('#8B949E')
              .margin({ top: 8 })
          }
          .width('100%')
          .padding(40)
        }
      }
    }
    .width('100%')
    .layoutWeight(1)
    .padding({ left: 16, right: 16, top: 12 })
  }

  // ===== 数据看板标签 =====
  @Builder
  AnalyticsTab() {
    Scroll() {
      Column() {
        // 推送效果总览
        Text('推送效果总览')
          .fontSize(16)
          .fontColor('#E6EDF3')
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 12 })

        // 效果指标网格
        Row() {
          this.MetricCard('总推送', `${this.pushStats.totalCount}`, '#58A6FF')
          this.MetricCard('今日推送', `${this.pushStats.dailyCount}`, '#3FB950')
          this.MetricCard('点击率', `${(this.pushStats.clickRate * 100).toFixed(1)}%`, '#F0883E')
          this.MetricCard('转化率', `${(this.pushStats.conversionRate * 100).toFixed(1)}%`, '#DDA0DD')
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        // 推送策略设置
        Text('推送策略')
          .fontSize(16)
          .fontColor('#E6EDF3')
          .fontWeight(FontWeight.Bold)
          .margin({ top: 24, bottom: 12 })

        this.StrategySettings()

        // 免打扰时段提示
        Row() {
          Text('🌙')
            .fontSize(16)
          Text('免打扰时段: 22:00 - 08:00')
            .fontSize(13)
            .fontColor('#8B949E')
            .margin({ left: 6 })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#161B22')
        .borderRadius(8)
        .margin({ top: 12 })
      }
      .padding(16)
    }
    .width('100%')
    .layoutWeight(1)
  }

  // ===== 效果指标小卡片 =====
  @Builder
  MetricCard(label: string, value: string, color: string) {
    Column() {
      Text(value)
        .fontSize(20)
        .fontColor(color)
        .fontWeight(FontWeight.Bold)
      Text(label)
        .fontSize(11)
        .fontColor('#8B949E')
        .margin({ top: 4 })
    }
    .width('23%')
    .padding(12)
    .backgroundColor('#161B22')
    .borderRadius(8)
    .border({ width: 1, color: '#30363D' })
    .alignItems(HorizontalAlign.Center)
  }

  // ===== 推送策略设置 =====
  @Builder
  StrategySettings() {
    Column() {
      Row() {
        Text('每日推送上限')
          .fontSize(13)
          .fontColor('#E6EDF3')
        Blank()
        Text('5条')
          .fontSize(13)
          .fontColor('#58A6FF')
      }
      .width('100%')
      .padding({ top: 8, bottom: 8 })

      Divider().color('#30363D')

      Row() {
        Text('围栏冷却时间')
          .fontSize(13)
          .fontColor('#E6EDF3')
        Blank()
        Text('1小时')
          .fontSize(13)
          .fontColor('#58A6FF')
      }
      .width('100%')
      .padding({ top: 8, bottom: 8 })

      Divider().color('#30363D')

      Row() {
        Text('仅停留触发')
          .fontSize(13)
          .fontColor('#E6EDF3')
        Blank()
        Toggle({ type: ToggleType.Switch, isOn: false })
          .selectedColor('#238636')
          .onChange((isOn: boolean) => {
            this.pushEngine.updateStrategy({ dwellOnlyMode: isOn })
          })
      }
      .width('100%')
      .padding({ top: 8, bottom: 8 })
    }
    .width('100%')
    .padding({ left: 14, right: 14 })
    .backgroundColor('#161B22')
    .borderRadius(8)
    .border({ width: 1, color: '#30363D' })
  }

  // ===== 工具方法 =====
  private getStatusLabel(status: CampaignStatus): string {
    const map: Record<CampaignStatus, string> = {
      [CampaignStatus.DRAFT]: '草稿',
      [CampaignStatus.ACTIVE]: '进行中',
      [CampaignStatus.PAUSED]: '已暂停',
      [CampaignStatus.COMPLETED]: '已完成',
      [CampaignStatus.EXPIRED]: '已过期'
    }
    return map[status] ?? '未知'
  }

  private getStatusColor(status: CampaignStatus): string {
    const map: Record<CampaignStatus, string> = {
      [CampaignStatus.DRAFT]: '#8B949E',
      [CampaignStatus.ACTIVE]: '#3FB950',
      [CampaignStatus.PAUSED]: '#F0883E',
      [CampaignStatus.COMPLETED]: '#58A6FF',
      [CampaignStatus.EXPIRED]: '#484F58'
    }
    return map[status] ?? '#8B949E'
  }

  private getStatusBgColor(status: CampaignStatus): string {
    const map: Record<CampaignStatus, string> = {
      [CampaignStatus.DRAFT]: '#1A8B949E',
      [CampaignStatus.ACTIVE]: '#1A3FB950',
      [CampaignStatus.PAUSED]: '#1AF0883E',
      [CampaignStatus.COMPLETED]: '#1A58A6FF',
      [CampaignStatus.EXPIRED]: '#1A484F58'
    }
    return map[status] ?? '#1A8B949E'
  }

  private getEventIcon(type: GeofenceEventType): string {
    const map: Record<GeofenceEventType, string> = {
      [GeofenceEventType.ENTER]: '🟢',
      [GeofenceEventType.EXIT]: '🔴',
      [GeofenceEventType.DWELL]: '🟡'
    }
    return map[type] ?? '⚪'
  }

  private formatTime(timestamp: number): string {
    const date = new Date(timestamp)
    return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`
  }
}

四、踩坑与注意事项

4.1 地理围栏精度问题

问题:GPS在室内精度可能偏差50-200米,导致围栏误触发或漏触发。

解决方案

// 围栏半径建议值(考虑定位误差)
function getRecommendedRadius(scenario: string): number {
  switch (scenario) {
    case 'indoor_mall':     // 室内商场
      return 200            // GPS室内偏差大,围栏需更大
    case 'outdoor_store':   // 室外门店
      return 100            // GPS室外较准,围栏可小
    case 'commercial_area': // 商圈
      return 500            // 大范围覆盖
    case 'scenic_spot':     // 景区
      return 300
    default:
      return 150
  }
}

// 精度过滤:定位精度低于阈值时忽略围栏事件
function shouldTrustLocation(accuracy: number, fenceRadius: number): boolean {
  // 如果定位精度误差大于围栏半径的50%,不可信
  return accuracy < fenceRadius * 0.5
}

4.2 围栏数量限制

问题:HarmonyOS单次围栏请求最多支持100个围栏,城市级营销可能需要数千个。

解决方案:采用动态围栏加载策略:

// 动态围栏加载:只加载用户当前位置附近的围栏
async function loadNearbyFences(
  latitude: number,
  longitude: number,
  radius: number = 5000  // 加载5km范围内的围栏
): Promise<GeofenceConfig[]> {
  // 1. 从服务端获取附近围栏列表
  const fences = await fetchFencesFromServer(latitude, longitude, radius)

  // 2. 清除旧围栏,注册新围栏
  geofenceManager.clearAllFences()
  geofenceManager.addBatchGeofences(fences)

  // 3. 用户移动超过2km时重新加载
  // 在位置变化回调中判断
  return fences
}

4.3 推送骚扰防护

问题:用户频繁进出围栏(如沿街行走),导致短时间内收到大量推送。

解决方案

策略 说明 参数
频率限制 同一围栏1小时内只推1次 minIntervalMs: 3600000
每日上限 每天最多5条营销推送 maxDailyPushes: 5
免打扰 22:00-08:00不推送 quietHoursStart/End
停留优先 仅停留5分钟以上才推送 dwellOnlyMode: true
用户控制 用户可设置推送偏好 pushPreference
冷却期 关闭推送后48小时不再推 cooldownMs: 172800000

4.4 后台围栏监听保活

问题:APP进入后台后,围栏监听可能被系统回收。

解决方案

// 申请长时任务保活
import { backgroundTaskManager } from '@kit.BackgroundTasksKit'
import { wantAgent, WantAgent } from '@kit.AbilityKit'

async function requestBackgroundTask(): Promise<void> {
  // 创建WantAgent用于点击通知回到APP
  const wantAgentInfo: wantAgent.WantAgentInfo = {
    wants: [{ bundleName: 'com.example.lbs', abilityName: 'MainAbility' }],
    requestCode: 0,
    operationType: wantAgent.OperationType.START_ABILITY,
    wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
  }
  const agent: WantAgent = await wantAgent.getWantAgent(wantAgentInfo)

  // 申请位置类型长时任务
  backgroundTaskManager.requestEnableBackgroundTask({
    bgMode: backgroundTaskManager.BackgroundMode.LOCATION,
    wantAgent: agent
  })
}

4.5 合规要点

法规要求 实现方式
明示同意 首次使用弹窗说明位置用途,用户确认后才开启围栏监听
最小必要 围栏半径不超过业务必要范围
目的限定 位置数据仅用于营销推送,不用于其他目的
退出机制 提供一键关闭"营销推送"开关
数据留存 位置数据留存不超过30天,推送记录不超过90天
用户知情 推送消息中标注"基于位置推送",用户可查看触发围栏

五、HarmonyOS 6适配

5.1 高精度室内围栏

HarmonyOS 6增强了室内定位能力,支持蓝牙信标和Wi-Fi RTT定位:

// HarmonyOS 6:室内高精度围栏
// 结合蓝牙信标实现商场内店铺级围栏
import { geoLocationManager } from '@kit.LocationKit'

// 室内围栏配置(结合蓝牙信标)
const indoorGeofence: geoLocationManager.Geofence = {
  latitude: 39.9042,
  longitude: 116.4074,
  radius: 20,  // 室内可缩小到20米级
  expiration: 0,
  dwellDelayTime: 180000,  // 室内停留3分钟触发
  notificationItem: {
    title: '📍 您已进入XX店铺',
    text: '专属优惠已送达!'
  }
}

// 使用高精度定位场景
const indoorRequest: geoLocationManager.GeofenceRequest = {
  priority: geoLocationManager.LocationRequestPriority.ACCURACY,
  scenario: geoLocationManager.LocationRequestScenario.NAVIGATION,
  geofences: [indoorGeofence]
}

5.2 智能推送时机优化

HarmonyOS 6的AI能力可优化推送时机:

// HarmonyOS 6:AI推送时机优化
// 基于用户行为模式预测最佳推送时机

interface UserBehaviorPattern {
  usualWakeTime: number      // 通常起床时间
  usualSleepTime: number     // 通常睡觉时间
  activeHours: number[]      // 活跃时段
  shoppingPreference: string // 购物偏好时段
}

// 预测最佳推送时机
function predictOptimalPushTime(
  pattern: UserBehaviorPattern,
  geofenceEventType: string
): { shouldPushNow: boolean, suggestedDelay: number } {
  const hour = new Date().getHours()

  // 如果在活跃时段且非免打扰时段,立即推送
  if (pattern.activeHours.includes(hour) &&
      hour >= 8 && hour < 22) {
    return { shouldPushNow: true, suggestedDelay: 0 }
  }

  // 如果在非活跃时段,延迟到下一个活跃时段
  const nextActiveHour = pattern.activeHours.find(h => h > hour)
  if (nextActiveHour !== undefined) {
    const delayMs = (nextActiveHour - hour) * 3600000
    return { shouldPushNow: false, suggestedDelay: delayMs }
  }

  // 默认延迟到次日活跃时段
  return { shouldPushNow: false, suggestedDelay: (24 - hour + pattern.activeHours[0]) * 3600000 }
}

5.3 跨设备营销协同

HarmonyOS 6的分布式能力,实现跨设备营销推送:

// HarmonyOS 6:跨设备营销协同
// 手机触发围栏 → 手表震动提醒 → 平板展示详情

import { distributedDeviceManager } from '@kit.DistributedServiceKit'

// 围栏触发后,根据设备类型分发不同内容
async function distributeMarketingContent(
  event: GeofenceEvent,
  message: PushMessage
): Promise<void> {
  const deviceManager = distributedDeviceManager.createDeviceManager('com.example.lbs')
  const devices = deviceManager.getAvailableDeviceListSync()

  for (const device of devices) {
    switch (device.deviceType) {
      case distributedDeviceManager.DeviceType.WATCH:
        // 手表:简洁震动提醒
        await sendToWatch(device.deviceId, {
          type: 'vibration',
          message: `${message.title} - 点击查看`
        })
        break

      case distributedDeviceManager.DeviceType.TABLET:
        // 平板:富媒体详情展示
        await sendToTablet(device.deviceId, {
          type: 'rich_card',
          title: message.title,
          body: message.body,
          imageUrl: message.imageUrl,
          deepLink: message.deepLink
        })
        break

      default:
        // 手机:标准通知推送
        await this.pushEngine.executePush(message)
    }
  }
}

六、总结

6.1 技术架构回顾

本文构建了一个完整的LBS营销与地理围栏推送系统,核心技术栈如下:

层级 技术 作用
围栏引擎 Location Kit Geofence 创建、监听、触发地理围栏
推送决策 频率控制+画像匹配 决定推不推、推什么、怎么推
消息推送 Notification Kit + Push Kit 本地通知与远程推送双通道
活动管理 营销活动CRUD 创建、激活、暂停、效果追踪
效果分析 行为埋点+数据看板 CTR、CVR、ROI实时监控
后台保活 Background Tasks Kit 保证后台围栏监听持续运行

6.2 关键设计决策

  1. 围栏半径:室外100-150m、室内200m、商圈500m,需考虑GPS精度偏差
  2. 推送频率:每日上限5条、围栏冷却1小时、免打扰22:00-08:00
  3. 停留触发:默认5分钟停留才触发深度推送,避免路过误触发
  4. 动态加载:只加载用户5km范围内的围栏,移动2km后重新加载
  5. 双通道推送:本地Notification Kit + 远程Push Kit,确保送达率

6.3 扩展方向

  • A/B测试:同一围栏不同推送文案,自动选择CTR最高的版本
  • 智能出价:根据用户价值动态调整优惠力度
  • 归因分析:推送→到店→购买的完整归因链路
  • 竞品围栏:在竞品门店附近设置围栏,推送差异化优惠
  • LBS广告平台:将围栏能力开放给第三方广告主
  • AR营销:围栏触发后展示AR互动内容,提升参与度

📌 一句话总结:LBS营销的核心是地理围栏精准触发+推送决策引擎智能过滤+频率控制防止骚扰三驾马车,技术实现的关键是在商业转化与用户体验之间找到最佳平衡点。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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