HarmonyOS APP开发:LBS营销与地理围栏推送
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营销与地理围栏推送」系统,实现以下核心功能:
- 地理围栏的创建、管理与监听
- 围栏事件触发与精准推送
- 营销活动管理后台
- 用户画像匹配与推送策略
- 推送效果分析与数据看板
二、核心原理
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 关键设计决策
- 围栏半径:室外100-150m、室内200m、商圈500m,需考虑GPS精度偏差
- 推送频率:每日上限5条、围栏冷却1小时、免打扰22:00-08:00
- 停留触发:默认5分钟停留才触发深度推送,避免路过误触发
- 动态加载:只加载用户5km范围内的围栏,移动2km后重新加载
- 双通道推送:本地Notification Kit + 远程Push Kit,确保送达率
6.3 扩展方向
- A/B测试:同一围栏不同推送文案,自动选择CTR最高的版本
- 智能出价:根据用户价值动态调整优惠力度
- 归因分析:推送→到店→购买的完整归因链路
- 竞品围栏:在竞品门店附近设置围栏,推送差异化优惠
- LBS广告平台:将围栏能力开放给第三方广告主
- AR营销:围栏触发后展示AR互动内容,提升参与度
📌 一句话总结:LBS营销的核心是地理围栏精准触发+推送决策引擎智能过滤+频率控制防止骚扰三驾马车,技术实现的关键是在商业转化与用户体验之间找到最佳平衡点。
- 点赞
- 收藏
- 关注作者
评论(0)