智慧社区鸿蒙解决方案:门禁、物业、安防一体化架构与实践
当我们谈论智慧社区时,我们在谈论什么?是刷脸开门的那一秒便捷,是物业报修从“打电话”到“点一下”的效率提升,还是安防系统从“事后追查”到“事前预警”的质变?在鸿蒙生态下,这些都不再是孤立的场景——门禁、物业、安防正在走向深度融合,设备与设备、服务与服务正在编织成一张“信任网络”。2025年,碧桂园服务与华为联合开发的鸿蒙版“凤凰会”正式上线,成为首批完成HarmonyOS 5深度适配的物业应用,覆
智慧社区鸿蒙解决方案:门禁、物业、安防一体化架构与实践
前言
当我们谈论智慧社区时,我们在谈论什么?是刷脸开门的那一秒便捷,是物业报修从“打电话”到“点一下”的效率提升,还是安防系统从“事后追查”到“事前预警”的质变?在鸿蒙生态下,这些都不再是孤立的场景——门禁、物业、安防正在走向深度融合,设备与设备、服务与服务正在编织成一张“信任网络”。
2025年,碧桂园服务与华为联合开发的鸿蒙版“凤凰会”正式上线,成为首批完成HarmonyOS 5深度适配的物业应用,覆盖超400万华为设备用户。同年,湖北联投在武汉智博会上展示了基于鸿蒙的全屋智能与社区设备深度互联方案:业主离家时空调自动关闭、窗帘合拢,安防系统启动巡逻模式;突发火情时,烟雾传感器触发报警,电梯迫降、门禁开启,并推送逃生路线至手机。这些不再是科幻电影的场景,而是鸿蒙赋能智慧社区的今天。
本文将从一个完整的智慧社区解决方案出发,深入剖析三大核心模块的技术实现:
- 多模态识别门禁系统:人脸/指纹/NFC的融合认证与活体防伪
- 物业报修原子化服务:从卡片到跨设备流转的轻量化设计
- 访客通行权限跨设备流转:分布式安全框架下的信任传递
全文包含完整的ArkTS代码示例、权限配置说明、分布式调试技巧,以及行业最佳实践。无论你是鸿蒙开发者、智慧社区从业者,还是对分布式技术感兴趣的爱好者,希望这篇文章能为你提供有价值的参考。
第一部分:多模态识别门禁系统——从“识别”到“防伪”的进化
1.1 场景描述:无感通行的终极体验
清晨7:30,业主李先生拎着早餐走向单元门。无需刷卡、无需掏手机,门禁摄像头在0.3秒内完成人脸识别,同时红外传感器检测活体特征,系统确认是“真人+本人”后自动开门。电梯已同步下行至1层,并点亮李先生所住的18层——这一切在他到达电梯厅前已完成。
这是智慧社区门禁的理想形态,也是鸿蒙多模态识别能力的典型应用。
1.2 技术架构:多模态融合认证的设计思路
传统的单一生物识别存在明显短板:人脸识别易被照片/视频攻击,指纹识别可能因手指脱皮或潮湿而失败,NFC卡片存在丢失风险。鸿蒙的多模态融合认证方案通过组合多种认证方式,在安全性和便捷性之间取得平衡。
门禁系统的整体架构分为三层:
- 设备层:包含带3D结构光摄像头的门禁机、指纹采集模组、NFC读卡器、蓝牙通信模块
- 认证层:负责多模态特征提取、活体检测、本地/云端认证
- 服务层:与物业系统对接,记录通行日志、同步黑白名单
认证策略矩阵:
| 场景 | 认证组合 | 安全等级 | 适用时段 |
|---|---|---|---|
| 日常通行 | 人脸(3D)+活体检测 | ★★★☆☆ | 白天 |
| 夜间通行 | 人脸(3D)+指纹 | ★★★★☆ | 22:00-6:00 |
| 访客通行 | 动态二维码+NFC | ★★★☆☆ | 预约时段 |
| 快递/外卖 | 身份证+人脸比对 | ★★★★★ | 临时授权 |
1.3 核心代码实现
1.3.1 权限声明(module.json5)
在鸿蒙应用中,使用生物识别需要声明相应权限。根据最新的权限分级机制,生物识别属于“AI敏感权限”,需要显式声明。
{
"module": {
"name": "entry",
"type": "entry",
"srcEntry": "./ets/entryability/EntryAbility.ts",
"description": "$string:entry_desc",
"mainElement": "EntryAbility",
"deviceTypes": ["phone", "tablet", "2in1"],
"requestPermissions": [
{
"name": "ohos.permission.DISTRIBUTED_DATASYNC",
"reason": "$string:distributed_data_sync_reason",
"usedScene": {
"abilities": ["SmartDoorAbility", "VisitorAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.CAMERA",
"reason": "$string:camera_permission_reason",
"usedScene": {
"abilities": ["SmartDoorAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.MANAGE_FINGERPRINT",
"reason": "$string:biometric_permission_reason",
"usedScene": {
"abilities": ["SmartDoorAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.FACE_AUTH",
"reason": "$string:face_auth_reason",
"usedScene": {
"abilities": ["SmartDoorAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.NFC_TAG",
"reason": "$string:nfc_permission_reason",
"usedScene": {
"abilities": ["SmartDoorAbility"],
"when": "inuse"
}
}
]
}
}
1.3.2 多模态门禁组件(SmartDoorPanel.ets)
借鉴社区开发实践,我们设计一个集成了人脸、指纹、NFC三种认证方式的门禁面板组件。
// SmartDoorPanel.ets
import { doorControl } from '@ohos.doorControl';
import { faceAuth } from '@ohos.faceAuth';
import { fingerprintAuth } from '@ohos.userIAM.userAuth';
import { nfcController } from '@ohos.nfc.controller';
import { businessError } from '@ohos.base';
import { commonEvent } from '@ohos.commonEvent';
// 定义门禁状态枚举
enum DoorStatus {
LOCKED = 'locked',
UNLOCKING = 'unlocking',
UNLOCKED = 'unlocked',
ERROR = 'error'
}
// 定义认证类型枚举
enum AuthType {
FACE = 'face',
FINGERPRINT = 'fingerprint',
NFC = 'nfc',
MULTI_MODAL = 'multi_modal'
}
// 定义认证结果接口
interface AuthResult {
success: boolean;
type: AuthType;
userId?: string;
confidence?: number;
message?: string;
timestamp: number;
}
@Entry
@Component
struct SmartDoorPanel {
// 状态变量
@State doorStatus: DoorStatus = DoorStatus.LOCKED;
@State lastAuthType: AuthType = AuthType.FACE;
@State authMessage: string = '等待认证...';
@State isNightMode: boolean = false;
@State recognizedUserId: string = '';
// 控制器引用
private doorController: doorControl.DoorController = new doorControl.DoorController();
private faceAuthHelper: faceAuth.FaceAuthHelper = new faceAuth.FaceAuthHelper();
// 定时器ID(用于模拟解锁后自动闭锁)
private timerId: number = -1;
aboutToAppear() {
// 初始化检测环境光强,判断是否开启夜间模式(夜间要求更高安全等级)
this.detectEnvironment();
// 订阅门禁状态变更事件(跨设备)
commonEvent.subscribe('door_status_changed', (err, data) => {
if (data?.parameters?.status) {
this.doorStatus = data.parameters.status;
}
});
// 启动NFC监听(用于刷卡开门)
this.startNfcListening();
}
aboutToDisappear() {
if (this.timerId > 0) {
clearTimeout(this.timerId);
}
commonEvent.unsubscribe('door_status_changed');
nfcController.stopReading();
}
/**
* 检测环境光强,决定是否启用夜间安全模式
*/
private detectEnvironment() {
try {
const lightIntensity = getLightIntensity(); // 假设的API,实际需调用传感器
this.isNightMode = lightIntensity < 10; // 光照强度低于10lux视为夜间
console.info(`环境检测完成,夜间模式: ${this.isNightMode}`);
} catch (err) {
console.error('环境检测失败,默认日间模式');
this.isNightMode = false;
}
}
/**
* 启动NFC监听
*/
private startNfcListening() {
try {
nfcController.startReading({
onTagDiscovered: (tagInfo) => {
console.info('检测到NFC标签');
// 解析NFC卡片信息
const cardId = this.parseNfcCardId(tagInfo);
if (cardId) {
this.authenticateByNfc(cardId);
}
}
});
} catch (err) {
console.error('NFC监听启动失败:' + JSON.stringify(err));
}
}
/**
* 解析NFC卡片ID
*/
private parseNfcCardId(tagInfo: nfcController.TagInfo): string | null {
// 实际解析逻辑依赖具体卡片格式
// 这里简化处理,假设第一个技术类型是NfcA,从中读取ID
try {
const nfcA = tagInfo.techList.find(t => t.includes('NfcA'));
if (nfcA) {
// 读取卡片的UID
const uid = nfcController.getUid();
return uid;
}
return null;
} catch (err) {
console.error('NFC解析失败:' + JSON.stringify(err));
return null;
}
}
/**
* 人脸识别认证
*/
async authenticateByFace(): Promise<AuthResult> {
this.authMessage = '正在进行人脸识别...';
try {
// 构建认证参数:夜间模式要求更高安全等级
const authParam = {
challenge: 'door_access_' + Date.now(),
authType: faceAuth.FaceAuthType.FACE_3D,
level: this.isNightMode ?
faceAuth.SecurityLevel.SECURITY_LEVEL_4 :
faceAuth.SecurityLevel.SECURITY_LEVEL_3,
enableLiveness: true // 启用活体检测
};
const result = await this.faceAuthHelper.auth(authParam);
if (result.code === 0) {
// 识别成功
const userId = result.userId || 'unknown';
// 检查用户是否有门禁权限
const hasPermission = await this.checkDoorPermission(userId);
if (hasPermission) {
return {
success: true,
type: AuthType.FACE,
userId: userId,
confidence: result.confidence,
timestamp: Date.now()
};
} else {
return {
success: false,
type: AuthType.FACE,
message: '无门禁权限',
timestamp: Date.now()
};
}
} else {
return {
success: false,
type: AuthType.FACE,
message: `识别失败,错误码:${result.code}`,
timestamp: Date.now()
};
}
} catch (err) {
const businessErr = err as businessError.BusinessError;
return {
success: false,
type: AuthType.FACE,
message: `认证异常:${businessErr.message}`,
timestamp: Date.now()
};
}
}
/**
* 指纹识别认证
*/
async authenticateByFingerprint(): Promise<AuthResult> {
this.authMessage = '请按压指纹传感器...';
try {
// 鸿蒙指纹认证API
const auth = new fingerprintAuth.FingerprintAuth();
const result = await auth.execute({
description: '门禁指纹验证',
disableDeviceCredential: true // 禁用备选密码
});
if (result.code === fingerprintAuth.ResultCode.SUCCESS) {
// 指纹识别成功
const fingerId = result.fingerprintId; // 指纹ID
const userId = await this.mapFingerprintToUser(fingerId);
if (userId) {
const hasPermission = await this.checkDoorPermission(userId);
if (hasPermission) {
return {
success: true,
type: AuthType.FINGERPRINT,
userId: userId,
timestamp: Date.now()
};
}
}
return {
success: false,
type: AuthType.FINGERPRINT,
message: '无门禁权限',
timestamp: Date.now()
};
} else {
return {
success: false,
type: AuthType.FINGERPRINT,
message: `指纹验证失败,错误码:${result.code}`,
timestamp: Date.now()
};
}
} catch (err) {
return {
success: false,
type: AuthType.FINGERPRINT,
message: `指纹认证异常:${JSON.stringify(err)}`,
timestamp: Date.now()
};
}
}
/**
* NFC卡片认证
*/
async authenticateByNfc(cardId: string): Promise<AuthResult> {
this.authMessage = '正在读取卡片信息...';
try {
// 根据卡片ID查询绑定的用户
const userId = await this.mapCardToUser(cardId);
if (!userId) {
return {
success: false,
type: AuthType.NFC,
message: '未绑定用户',
timestamp: Date.now()
};
}
const hasPermission = await this.checkDoorPermission(userId);
if (hasPermission) {
return {
success: true,
type: AuthType.NFC,
userId: userId,
timestamp: Date.now()
};
} else {
return {
success: false,
type: AuthType.NFC,
message: '无门禁权限',
timestamp: Date.now()
};
}
} catch (err) {
return {
success: false,
type: AuthType.NFC,
message: `卡片认证异常:${JSON.stringify(err)}`,
timestamp: Date.now()
};
}
}
/**
* 多模态融合认证(人脸+指纹)
* 在夜间或高安全需求场景使用
*/
async authenticateByMultiModal(): Promise<AuthResult> {
this.authMessage = '正在进行双重认证...';
try {
// 并行执行两种认证
const [faceResult, fingerResult] = await Promise.allSettled([
this.authenticateByFace(),
this.authenticateByFingerprint()
]);
// 判断两种认证是否都成功
const faceSuccess = faceResult.status === 'fulfilled' && faceResult.value.success;
const fingerSuccess = fingerResult.status === 'fulfilled' && fingerResult.value.success;
if (faceSuccess && fingerSuccess) {
const faceValue = (faceResult as PromiseFulfilledResult<AuthResult>).value;
return {
success: true,
type: AuthType.MULTI_MODAL,
userId: faceValue.userId,
confidence: faceValue.confidence,
timestamp: Date.now()
};
} else {
return {
success: false,
type: AuthType.MULTI_MODAL,
message: '双重认证未通过',
timestamp: Date.now()
};
}
} catch (err) {
return {
success: false,
type: AuthType.MULTI_MODAL,
message: `多重认证异常:${JSON.stringify(err)}`,
timestamp: Date.now()
};
}
}
/**
* 检查用户门禁权限(模拟云端调用)
*/
private async checkDoorPermission(userId: string): Promise<boolean> {
try {
// 实际开发中应调用云函数或数据库
// 这里简化处理,假设用户ID以'resident_'开头的有权限
return userId.startsWith('resident_');
} catch (err) {
console.error('权限检查失败:' + JSON.stringify(err));
return false;
}
}
/**
* 指纹ID映射到用户ID(模拟)
*/
private async mapFingerprintToUser(fingerId: string): Promise<string | null> {
// 实际应查询数据库
return 'resident_1001';
}
/**
* 卡片ID映射到用户ID(模拟)
*/
private async mapCardToUser(cardId: string): Promise<string | null> {
// 实际应查询数据库
if (cardId === 'A1B2C3D4') {
return 'resident_1001';
}
return null;
}
/**
* 执行开锁操作
*/
private async performUnlock(authResult: AuthResult) {
if (this.doorStatus !== DoorStatus.LOCKED) {
console.info('门已开启或正在操作中');
return;
}
this.doorStatus = DoorStatus.UNLOCKING;
this.lastAuthType = authResult.type;
this.recognizedUserId = authResult.userId || '';
try {
// 调用门禁控制API开锁
await this.doorController.unlock('main_gate', {
userId: authResult.userId,
authType: authResult.type,
timestamp: authResult.timestamp
});
this.doorStatus = DoorStatus.UNLOCKED;
this.authMessage = '门已开启';
// 记录开锁日志(分布式)
this.logAccessRecord(authResult);
// 5秒后自动闭锁(模拟)
this.timerId = setTimeout(() => {
this.doorStatus = DoorStatus.LOCKED;
this.authMessage = '等待认证...';
this.timerId = -1;
// 广播门禁状态
commonEvent.publish('door_status_changed', {
parameters: { status: DoorStatus.LOCKED }
});
}, 5000);
} catch (err) {
this.doorStatus = DoorStatus.ERROR;
this.authMessage = '开锁失败:' + JSON.stringify(err);
console.error('开锁失败:' + JSON.stringify(err));
}
}
/**
* 记录门禁访问日志(跨设备同步)
*/
private async logAccessRecord(authResult: AuthResult) {
try {
// 使用分布式数据服务同步日志
const kvStore = await distributedKVStore.getKVStore('access_logs');
const logEntry = {
key: 'log_' + authResult.timestamp,
value: {
userId: authResult.userId,
authType: authResult.type,
success: authResult.success,
timestamp: authResult.timestamp,
deviceId: getDeviceId()
}
};
await kvStore.put(logEntry.key, logEntry.value);
// 如果门禁日志需要上传物业系统,可调用云函数
cloudFunction('syncAccessLog', logEntry.value);
} catch (err) {
console.error('日志记录失败:' + JSON.stringify(err));
}
}
/**
* 统一认证入口:根据模式选择认证方式
*/
async startAuthentication() {
if (this.doorStatus !== DoorStatus.LOCKED) {
return;
}
let result: AuthResult | null = null;
// 根据当前模式选择认证策略
if (this.isNightMode) {
// 夜间模式:多模态认证
result = await this.authenticateByMultiModal();
} else {
// 日间模式:优先人脸识别,失败则尝试指纹
result = await this.authenticateByFace();
if (!result || !result.success) {
// 人脸识别失败,提示可尝试指纹
this.authMessage = '人脸识别失败,请尝试指纹';
// 这里可以显示指纹按钮让用户手动触发
}
}
if (result && result.success) {
await this.performUnlock(result);
} else {
this.authMessage = result?.message || '认证失败,请重试';
}
}
build() {
Column() {
// 门禁状态指示区
Row() {
Circle()
.width(12)
.height(12)
.fill(this.doorStatus === DoorStatus.LOCKED ? '#FF4444' :
this.doorStatus === DoorStatus.UNLOCKED ? '#44FF44' : '#FFAA00')
Text(this.doorStatus === DoorStatus.LOCKED ? '门禁已闭锁' :
this.doorStatus === DoorStatus.UNLOCKING ? '门禁正在开启...' :
this.doorStatus === DoorStatus.UNLOCKED ? '门禁已开启' : '异常状态')
.fontSize(16)
.margin({ left: 8 })
}
.padding(16)
.width('100%')
.backgroundColor('#F5F5F5')
// 当前认证信息
Column() {
Text(this.authMessage)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.margin({ top: 20, bottom: 10 })
if (this.recognizedUserId) {
Text(`欢迎您,业主 ${this.recognizedUserId}`)
.fontSize(14)
.fontColor('#666')
}
if (this.isNightMode) {
Text('🌙 夜间安全模式已开启')
.fontSize(12)
.fontColor('#FFAA00')
.margin({ top: 8 })
}
}
.width('100%')
.height(120)
.justifyContent(FlexAlign.Center)
// 主认证按钮
Button(this.doorStatus === DoorStatus.LOCKED ? '点击认证开门' : '门已开启')
.width(200)
.height(200)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.backgroundColor(this.doorStatus === DoorStatus.LOCKED ? '#0077FF' : '#CCCCCC')
.enabled(this.doorStatus === DoorStatus.LOCKED)
.onClick(() => {
this.startAuthentication();
})
.margin({ top: 20, bottom: 20 })
// 辅助认证选项
Row({ space: 20 }) {
Button('指纹')
.width(80)
.height(40)
.fontSize(14)
.backgroundColor('#4CAF50')
.enabled(this.doorStatus === DoorStatus.LOCKED)
.onClick(() => {
this.authenticateByFingerprint().then(result => {
if (result.success) this.performUnlock(result);
else this.authMessage = result.message || '指纹认证失败';
});
})
Button('NFC')
.width(80)
.height(40)
.fontSize(14)
.backgroundColor('#9C27B0')
.enabled(this.doorStatus === DoorStatus.LOCKED)
.onClick(() => {
// 提示用户刷卡
this.authMessage = '请将门禁卡靠近感应区';
})
}
.margin({ bottom: 20 })
// 最近通行记录(简化版)
Column() {
Text('最近通行记录')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
.padding({ left: 16, bottom: 8 })
// 此处可添加LazyForEach展示最近记录
Row() {
Text('今日 08:30')
Text('人脸识别').margin({ left: 20 })
Text('1801室').margin({ left: 20 })
}
.width('100%')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.backgroundColor('#F9F9F9')
}
}
.width('100%')
.height('100%')
.backgroundColor(Color.White)
}
}
1.4 活体检测技术深度解析
在生物识别中,防伪能力至关重要。鸿蒙的生物特征认证框架深度集成了活体检测技术:
-
指纹活体检测:结合电容传感器检测皮肤电容值变化,判断是否为活体手指。假冒指纹(如硅胶指膜)无法通过电容检测。
-
面部活体检测:通过3D结构光+红外摄像头,识别真人与照片/视频的差异。关键点包括:
- 眼球反光动态检测
- 微表情分析(如眨眼、嘴角微动)
- 深度信息验证(防止平面照片攻击)
代码中启用活体检测的方式:
const authParam = {
enableLiveness: true, // 启用活体检测
livenessLevel: LivenessLevel.HIGH // 高安全等级
};
1.5 门禁系统的分布式特性
鸿蒙门禁系统的独特价值在于“分布式”:
-
多设备协同:门禁机、室内屏、手机、手表可形成协同网络。当业主靠近单元门时,手机和手表同时收到推送,即使手机没电,手表仍可开门。
-
状态同步:门禁状态(开/关/异常)在所有家庭成员设备上实时同步。
-
跨设备日志:开门记录自动同步到家庭所有成员手机,方便老人小孩出入管理。
第二部分:物业报修原子化服务设计——从“应用”到“服务”的降维
2.1 场景描述:报修,不应该是一个App
传统的物业报修流程:打开物业App → 找到报修入口 → 填写表单 → 上传图片 → 提交 → 等待接单。这个流程有三个问题:入口深、操作重、用完即走却必须安装App。
鸿蒙的原子化服务解决了这个问题。原子化服务是一种无需安装、即用即走、跨端流转的轻量服务形态。对于报修这种高频但轻量的场景,原子化服务是最佳载体。
2.2 原子化服务架构设计
服务卡片 + 云端函数 + 分布式通知的组合,构成了完整的报修服务体系:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 服务卡片 │ │ 云函数/后端 │ │ 物业端 │
│ │ │ │ │ │
│ · 一键报修 │────▶│ · 工单生成 │────▶│ · 接单 │
│ · 进度查看 │◀────│ · 派单逻辑 │◀────│ · 维修完成 │
│ · 评价反馈 │ │ · 状态同步 │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└────────────────────────┼────────────────────────┘
│
┌───────▼───────┐
│ 分布式通知 │
│ · 手机提醒 │
│ · 手表振动 │
│ · 电视弹窗 │
└───────────────┘
2.3 核心代码实现
2.3.1 报修服务卡片(RepairCard.ets)
服务卡片是原子化服务的入口,用户无需打开App即可完成报修。
// RepairCard.ets
import { widgetManager } from '@ohos.widgetManager';
import { cloud } from '@ohos.cloud';
import { notification } from '@ohos.notification';
import { distributedDevice } from '@ohos.distributedDevice';
// 定义报修工单接口
interface RepairOrder {
id: string;
type: 'electric' | 'plumbing' | 'appliance' | 'other';
description: string;
images?: string[];
address: string;
contactName: string;
contactPhone: string;
status: 'pending' | 'accepted' | 'processing' | 'completed' | 'cancelled';
createTime: number;
acceptTime?: number;
completeTime?: number;
workerName?: string;
workerPhone?: string;
rating?: number;
}
// 定义报修参数接口
interface RepairParams {
type: RepairOrder['type'];
description: string;
images?: string[];
}
@Entry
@Component
struct RepairServiceWidget {
@State recentOrders: RepairOrder[] = [];
@State isSubmitting: boolean = false;
@State submitResult: string = '';
aboutToAppear() {
// 加载最近的报修记录
this.loadRecentOrders();
// 订阅报修状态变更(分布式)
distributedDevice.on('repair_status_change', (data) => {
if (data?.orderId) {
this.refreshOrder(data.orderId);
}
});
}
/**
* 加载最近的报修记录
*/
private async loadRecentOrders() {
try {
// 从云数据库获取最近5条记录
const result = await cloud.database().collection('repair_orders')
.orderBy('createTime', 'desc')
.limit(5)
.get();
this.recentOrders = result.data as RepairOrder[];
} catch (err) {
console.error('加载报修记录失败:' + JSON.stringify(err));
}
}
/**
* 刷新单条报修记录
*/
private async refreshOrder(orderId: string) {
try {
const result = await cloud.database().collection('repair_orders')
.doc(orderId)
.get();
if (result.data) {
const index = this.recentOrders.findIndex(o => o.id === orderId);
if (index >= 0) {
this.recentOrders[index] = result.data as RepairOrder;
} else {
this.recentOrders.unshift(result.data as RepairOrder);
}
}
} catch (err) {
console.error('刷新报修记录失败:' + JSON.stringify(err));
}
}
/**
* 提交报修工单
*/
async submitRepair(params: RepairParams) {
if (this.isSubmitting) return;
this.isSubmitting = true;
this.submitResult = '';
try {
// 获取当前用户信息
const userInfo = await cloud.auth().getUserInfo();
// 获取默认地址(假设从用户配置中获取)
const userProfile = await cloud.database().collection('user_profiles')
.doc(userInfo.uid)
.get();
const defaultAddress = userProfile.data?.address || '未设置地址';
// 构建工单数据
const orderData: Partial<RepairOrder> = {
type: params.type,
description: params.description,
images: params.images || [],
address: defaultAddress,
contactName: userProfile.data?.name || userInfo.uid,
contactPhone: userProfile.data?.phone || '',
status: 'pending',
createTime: Date.now()
};
// 调用云函数创建工单
const result = await cloud.function('createRepairOrder')
.call(orderData);
if (result.success) {
this.submitResult = '报修提交成功!工单号:' + result.orderId;
// 发送本地通知
this.sendLocalNotification('报修已提交', `工单${result.orderId}已提交,请等待接单`);
// 刷新列表
this.loadRecentOrders();
// 如果当前设备是手机,同步状态到手表(如果有)
this.syncToWearable(result.orderId);
} else {
this.submitResult = '提交失败:' + result.message;
}
} catch (err) {
this.submitResult = '提交异常:' + JSON.stringify(err);
} finally {
this.isSubmitting = false;
}
}
/**
* 发送本地通知
*/
private async sendLocalNotification(title: string, content: string) {
try {
const notificationRequest = {
id: Date.now(),
content: {
contentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
title: title,
text: content
}
};
await notification.publish(notificationRequest);
} catch (err) {
console.error('发送通知失败:' + JSON.stringify(err));
}
}
/**
* 同步报修状态到可穿戴设备
*/
private async syncToWearable(orderId: string) {
try {
// 获取可信设备列表
const devices = await distributedDevice.getTrustedDevices();
// 找到手表类设备
const wearables = devices.filter(d =>
d.type === 'wearable' && d.status === 'online'
);
for (const device of wearables) {
await distributedDevice.sync(device.id, 'repair_sync', {
orderId: orderId,
action: 'new_order'
});
}
} catch (err) {
console.error('同步到穿戴设备失败:' + JSON.stringify(err));
}
}
build() {
Column() {
// 卡片标题
Row() {
Image($r('app.media.repair_icon'))
.width(24)
.height(24)
Text('物业报修')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ left: 8 })
Blank()
Text('查看更多')
.fontSize(12)
.fontColor('#0077FF')
.onClick(() => {
// 跳转到完整应用
this.jumpToFullApp();
})
}
.width('100%')
.padding(12)
// 快速报修入口(卡片核心功能)
Row({ space: 10 }) {
this.quickRepairButton('💡', '电路', 'electric')
this.quickRepairButton('🚰', '水路', 'plumbing')
this.quickRepairButton('❄️', '家电', 'appliance')
this.quickRepairButton('🔧', '其他', 'other')
}
.padding({ left: 12, right: 12, bottom: 12 })
.width('100%')
// 提交状态提示
if (this.submitResult) {
Text(this.submitResult)
.fontSize(12)
.fontColor(this.submitResult.includes('成功') ? '#4CAF50' : '#FF4444')
.padding({ left: 12, right: 12, bottom: 8 })
}
// 最近报修列表
if (this.recentOrders.length > 0) {
Column() {
Text('最近报修')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.width('100%')
.padding({ left: 12, bottom: 6 })
ForEach(this.recentOrders, (order: RepairOrder) => {
this.repairItem(order)
})
}
.padding({ bottom: 12 })
} else {
Text('暂无报修记录')
.fontSize(12)
.fontColor('#999')
.height(60)
.width('100%')
.textAlign(TextAlign.Center)
}
}
.width('100%')
.height('100%')
.backgroundColor(Color.White)
.borderRadius(12)
}
@Builder
quickRepairButton(emoji: string, label: string, type: string) {
Column() {
Text(emoji)
.fontSize(20)
Text(label)
.fontSize(10)
.margin({ top: 4 })
}
.padding(8)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.onClick(() => {
// 弹出报修详情输入框(简化版,实际开发可弹出自定义对话框)
this.showRepairDialog(type);
})
}
@Builder
repairItem(order: RepairOrder) {
Row() {
// 类型图标
Text(this.getTypeEmoji(order.type))
.fontSize(16)
.width(30)
// 描述和状态
Column() {
Text(order.description.length > 12 ?
order.description.substring(0, 12) + '...' : order.description)
.fontSize(12)
.fontWeight(FontWeight.Medium)
Row() {
Text(this.getStatusText(order.status))
.fontSize(10)
.fontColor(this.getStatusColor(order.status))
Text(this.formatTime(order.createTime))
.fontSize(10)
.fontColor('#999')
.margin({ left: 8 })
}
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
// 查看详情箭头
Text('>')
.fontSize(14)
.fontColor('#999')
.margin({ right: 8 })
}
.width('100%')
.padding({ left: 12, right: 8, top: 6, bottom: 6 })
.onClick(() => {
// 跳转到详情页
this.openOrderDetail(order.id);
})
}
private getTypeEmoji(type: string): string {
const map: Record<string, string> = {
'electric': '💡',
'plumbing': '🚰',
'appliance': '❄️',
'other': '🔧'
};
return map[type] || '📋';
}
private getStatusText(status: string): string {
const map: Record<string, string> = {
'pending': '待接单',
'accepted': '已接单',
'processing': '维修中',
'completed': '已完成',
'cancelled': '已取消'
};
return map[status] || status;
}
private getStatusColor(status: string): string {
const map: Record<string, string> = {
'pending': '#FFAA00',
'accepted': '#4CAF50',
'processing': '#2196F3',
'completed': '#9C27B0',
'cancelled': '#999'
};
return map[status] || '#000';
}
private formatTime(timestamp: number): string {
const date = new Date(timestamp);
const now = new Date();
const diff = now.getTime() - timestamp;
if (diff < 60 * 1000) {
return '刚刚';
} else if (diff < 60 * 60 * 1000) {
return Math.floor(diff / (60 * 1000)) + '分钟前';
} else if (diff < 24 * 60 * 60 * 1000) {
return Math.floor(diff / (60 * 60 * 1000)) + '小时前';
} else {
return `${date.getMonth() + 1}/${date.getDate()}`;
}
}
private async showRepairDialog(type: string) {
// 在实际应用中,这里应该弹出一个自定义对话框让用户输入描述
// 由于卡片环境限制,这里简化为直接跳转到完整应用并传参
this.jumpToFullAppWithParams({ action: 'new_repair', type: type });
}
private jumpToFullApp() {
widgetManager.startAbility({
bundleName: 'com.smartcommunity.app',
abilityName: 'RepairMainAbility'
});
}
private jumpToFullAppWithParams(params: Record<string, any>) {
widgetManager.startAbility({
bundleName: 'com.smartcommunity.app',
abilityName: 'RepairMainAbility',
parameters: params
});
}
private openOrderDetail(orderId: string) {
widgetManager.startAbility({
bundleName: 'com.smartcommunity.app',
abilityName: 'RepairDetailAbility',
parameters: { orderId: orderId }
});
}
}
2.3.2 报修表单组件(RepairForm.ets)
当用户从卡片进入完整应用时,展示完整的报修表单。
// RepairForm.ets
import { cloud } from '@ohos.cloud';
import { picker } from '@ohos.file.picker';
import { image } from '@ohos.multimedia.image';
@Entry
@Component
struct RepairFormPage {
@State type: 'electric' | 'plumbing' | 'appliance' | 'other' = 'other';
@State description: string = '';
@State images: string[] = [];
@State contactName: string = '';
@State contactPhone: string = '';
@State address: string = '';
@State isSubmitting: boolean = false;
@State submitStatus: string = '';
aboutToAppear() {
// 从路由参数中获取初始值
const params = router.getParams() as Record<string, any>;
if (params?.type) {
this.type = params.type;
}
// 加载用户默认信息
this.loadUserProfile();
}
private async loadUserProfile() {
try {
const userInfo = await cloud.auth().getUserInfo();
const result = await cloud.database().collection('user_profiles')
.doc(userInfo.uid)
.get();
if (result.data) {
this.contactName = result.data.name || '';
this.contactPhone = result.data.phone || '';
this.address = result.data.address || '';
}
} catch (err) {
console.error('加载用户信息失败:' + JSON.stringify(err));
}
}
/**
* 选择图片
*/
private async selectImages() {
try {
const photoPicker = new picker.PhotoViewPicker();
const result = await photoPicker.select({
maxSelectNumber: 3 - this.images.length // 最多3张
});
if (result.photoUris) {
this.images = [...this.images, ...result.photoUris];
}
} catch (err) {
console.error('选择图片失败:' + JSON.stringify(err));
}
}
/**
* 移除图片
*/
private removeImage(index: number) {
this.images = this.images.filter((_, i) => i !== index);
}
/**
* 提交表单
*/
private async submitForm() {
// 表单验证
if (!this.description) {
this.submitStatus = '请填写问题描述';
return;
}
if (!this.contactPhone) {
this.submitStatus = '请填写联系电话';
return;
}
if (!this.address) {
this.submitStatus = '请填写地址';
return;
}
this.isSubmitting = true;
this.submitStatus = '提交中...';
try {
// 上传图片到云存储(如有)
const uploadedImageUrls: string[] = [];
for (const uri of this.images) {
const uploadResult = await cloud.storage().upload(uri);
if (uploadResult.url) {
uploadedImageUrls.push(uploadResult.url);
}
}
// 提交工单
const result = await cloud.function('createRepairOrder').call({
type: this.type,
description: this.description,
images: uploadedImageUrls,
contactName: this.contactName,
contactPhone: this.contactPhone,
address: this.address
});
if (result.success) {
this.submitStatus = '提交成功!';
// 提交成功后,返回上一页并携带成功信息
router.back({
params: { submitSuccess: true, orderId: result.orderId }
});
} else {
this.submitStatus = '提交失败:' + result.message;
}
} catch (err) {
this.submitStatus = '提交异常:' + JSON.stringify(err);
} finally {
this.isSubmitting = false;
}
}
build() {
Column() {
// 标题栏
Row() {
Image($r('app.media.back'))
.width(24)
.height(24)
.onClick(() => router.back())
Text('提交报修')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ left: 16 })
}
.width('100%')
.padding(12)
.backgroundColor(Color.White)
Scroll() {
Column({ space: 16 }) {
// 报修类型
Column() {
Text('报修类型')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.width('100%')
Row({ space: 10 }) {
this.typeButton('electric', '💡 电路')
this.typeButton('plumbing', '🚰 水路')
this.typeButton('appliance', '❄️ 家电')
this.typeButton('other', '🔧 其他')
}
.width('100%')
.margin({ top: 8 })
}
.padding({ left: 16, right: 16 })
// 问题描述
Column() {
Text('问题描述')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.width('100%')
TextArea({
placeholder: '请详细描述故障情况...',
text: this.description,
onChange: (value) => { this.description = value; }
})
.height(100)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.margin({ top: 8 })
}
.padding({ left: 16, right: 16 })
// 图片上传
Column() {
Row() {
Text('图片上传')
.fontSize(14)
.fontWeight(FontWeight.Medium)
Text('(最多3张)')
.fontSize(12)
.fontColor('#999')
.margin({ left: 4 })
Blank()
Text(`${this.images.length}/3`)
.fontSize(12)
.fontColor('#999')
}
.width('100%')
if (this.images.length > 0) {
Scroll() {
Row({ space: 8 }) {
ForEach(this.images, (uri: string, index: number) => {
Stack() {
Image(uri)
.width(80)
.height(80)
.borderRadius(8)
.objectFit(ImageFit.Cover)
Text('✕')
.fontSize(12)
.fontColor(Color.White)
.backgroundColor('#66000000')
.width(20)
.height(20)
.textAlign(TextAlign.Center)
.borderRadius(10)
.position({ x: 60, y: 0 })
.onClick(() => this.removeImage(index))
}
})
if (this.images.length < 3) {
Column() {
Text('+')
.fontSize(24)
.fontColor('#999')
Text('上传')
.fontSize(10)
.fontColor('#999')
}
.width(80)
.height(80)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.justifyContent(FlexAlign.Center)
.onClick(() => this.selectImages())
}
}
.padding({ top: 8, bottom: 8 })
}
.scrollable(ScrollDirection.Horizontal)
.height(100)
} else {
Column() {
Text('+ 点击上传图片')
.fontSize(14)
.fontColor('#999')
}
.width('100%')
.height(80)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.justifyContent(FlexAlign.Center)
.onClick(() => this.selectImages())
.margin({ top: 8 })
}
}
.padding({ left: 16, right: 16 })
// 联系人
Column() {
Text('联系人')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.width('100%')
TextInput({
placeholder: '姓名',
text: this.contactName,
onChange: (value) => { this.contactName = value; }
})
.backgroundColor('#F5F5F5')
.borderRadius(8)
.margin({ top: 8 })
}
.padding({ left: 16, right: 16 })
// 联系电话
Column() {
Text('联系电话')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.width('100%')
TextInput({
placeholder: '手机号码',
text: this.contactPhone,
onChange: (value) => { this.contactPhone = value; },
type: InputType.PhoneNumber
})
.backgroundColor('#F5F5F5')
.borderRadius(8)
.margin({ top: 8 })
}
.padding({ left: 16, right: 16 })
// 地址
Column() {
Text('地址')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.width('100%')
TextInput({
placeholder: '如:3栋202室',
text: this.address,
onChange: (value) => { this.address = value; }
})
.backgroundColor('#F5F5F5')
.borderRadius(8)
.margin({ top: 8 })
}
.padding({ left: 16, right: 16 })
// 提交状态提示
if (this.submitStatus) {
Text(this.submitStatus)
.fontSize(14)
.fontColor(this.submitStatus.includes('成功') ? '#4CAF50' : '#FF4444')
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 8 })
}
// 提交按钮
Button(this.isSubmitting ? '提交中...' : '提交报修')
.width('90%')
.height(44)
.backgroundColor('#0077FF')
.enabled(!this.isSubmitting && !!this.description && !!this.contactPhone && !!this.address)
.onClick(() => this.submitForm())
.margin({ top: 16, bottom: 32 })
}
.padding({ top: 16, bottom: 16 })
}
.backgroundColor('#F5F5F5')
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Builder
typeButton(type: string, label: string) {
Column() {
Text(label)
.fontSize(14)
.fontColor(this.type === type ? '#0077FF' : '#333')
}
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.backgroundColor(this.type === type ? '#E6F0FF' : '#F5F5F5')
.borderRadius(16)
.onClick(() => {
this.type = type as any;
})
}
}
2.4 报修服务的分布式通知设计
报修服务的独特价值在于“跨设备提醒”。当维修工单状态变化时,系统需要通知业主、维修工、物业管家三方,且通知要出现在最合适的设备上。
通知分发策略:
| 角色 | 设备类型 | 通知方式 | 场景 |
|---|---|---|---|
| 业主 | 手机 | 系统通知+卡片更新 | 工单提交/接单/完成 |
| 业主 | 手表 | 振动提醒 | 维修工即将到达 |
| 维修工 | 手机 | 语音播报 | 新工单分配 |
| 维修工 | 平板 | 工单列表刷新 | 查看所有任务 |
| 物业管家 | 电视/大屏 | 弹窗提醒 | 超时未接单预警 |
// 分布式通知服务示例
class DistributedNotificationService {
/**
* 发送报修状态更新通知
*/
static async sendRepairNotification(order: RepairOrder, action: string) {
// 根据角色分发通知
const participants = await this.getOrderParticipants(order.id);
for (const participant of participants) {
const devices = await this.getUserOnlineDevices(participant.userId);
// 根据角色选择最优设备
const targetDevice = this.selectBestDevice(devices, participant.role, action);
if (targetDevice) {
await this.sendToDevice(targetDevice, {
title: this.getNotificationTitle(order, action, participant.role),
content: this.getNotificationContent(order, action, participant.role),
orderId: order.id,
action: action,
priority: this.getNotificationPriority(action)
});
}
}
// 特殊场景:超时未接单,推送物业大屏
if (action === 'timeout') {
await this.sendToPropertyScreen(order);
}
}
/**
* 选择最适合接收通知的设备
*/
private static selectBestDevice(devices: DeviceInfo[], role: string, action: string): DeviceInfo | null {
if (devices.length === 0) return null;
// 业主:优先手机,次选手表
if (role === 'owner') {
const phone = devices.find(d => d.type === 'phone');
if (phone) return phone;
return devices.find(d => d.type === 'wearable') || devices[0];
}
// 维修工:优先手表(振动提醒),次选手机
if (role === 'worker' && action === 'new_order') {
const watch = devices.find(d => d.type === 'wearable');
if (watch) return watch;
}
// 默认返回第一个在线设备
return devices[0];
}
/**
* 发送到物业大屏
*/
private static async sendToPropertyScreen(order: RepairOrder) {
// 通过分布式软总线发现物业大屏设备
const screens = await distributedDevice.findDevicesByType('tv');
for (const screen of screens) {
await distributedDevice.sync(screen.id, 'property_alert', {
type: 'timeout_warning',
orderId: order.id,
description: order.description,
createTime: order.createTime
});
}
}
}
2.5 报修服务的行业实践
碧桂园服务的鸿蒙版“凤凰会”聚焦报事报修、物业缴费等核心功能,确保业主在高频使用场景中获得稳定、流畅的操作体验。这一案例验证了报修服务作为原子化服务的可行性——用户不需要下载完整App,通过服务卡片即可完成报修全流程。
第三部分:访客通行权限跨设备流转——分布式安全的实战应用
3.1 场景描述:从“开门”到“开权限”
下午3点,业主王先生接到快递员电话。他正在公司开会,无法亲自下楼。打开手机上的社区App,点击“访客通行”,生成一个有效期为30分钟的临时二维码,通过微信发送给快递员。快递员到达单元门时,扫码进入,同时电梯自动下行至1层。
30分钟后,二维码失效。整个过程,王先生的隐私得到了保护——快递员不知道他的房号,只知道“3栋”。
这是访客通行的基础场景。在鸿蒙分布式能力加持下,这个场景可以更智能:
- 场景一:王先生在手机上生成访客码,权限自动同步到他家的智能门锁、单元门禁、电梯——访客只需扫码一次,即可通行所有关卡。
- 场景二:访客到达时,王先生的手表和手机同时收到推送,他可以在手表上一键开门,无需掏出手机。
- 场景三:临时授权的权限在访客离开后自动回收,所有设备的门禁状态同步更新。
3.2 分布式安全的核心原理:从“门禁”到“信任网络”
传统安全是“以设备为中心”,每个门禁独立验证。鸿蒙分布式安全是“以用户+设备集合为中心”。访客通行权限的跨设备流转,本质上是构建了一个“信任网络”:
三大核心要素 :
- 设备认证:设备想加入分布式家庭,先得证明自己不是坏人。单元门禁、电梯、智能门锁之间建立信任关系。
- 用户身份:同样的设备,不同用户权限不同。业主可以授权访客,但访客不能授权他人。
- 访问控制:核心是“你是谁→你在哪个设备→你想访问什么→能不能给你”。在鸿蒙里,访问控制基于权限声明+能力访问+Token验证一起工作。
3.3 核心代码实现
3.3.1 访客通行权限生成与管理(VisitorAccess.ets)
// VisitorAccess.ets
import { cloud } from '@ohos.cloud';
import { distributedDevice } from '@ohos.distributedDevice';
import { cryptoFramework } from '@ohos.security.cryptoFramework';
// 访客通行证接口
interface VisitorPass {
id: string; // 通行证ID
ownerId: string; // 业主ID
visitorName: string; // 访客姓名
visitorPhone?: string; // 访客电话
qrCode: string; // 二维码内容
token: string; // 安全令牌
startTime: number; // 生效时间
endTime: number; // 失效时间
maxUses: number; // 最大使用次数
usedCount: number; // 已使用次数
devices: string[]; // 可通行设备列表
status: 'active' | 'expired' | 'revoked';
}
@Entry
@Component
struct VisitorAccessPage {
@State visitorName: string = '';
@State visitorPhone: string = '';
@State accessDuration: number = 30; // 默认30分钟
@State accessCount: number = 1; // 默认单次
@State activePasses: VisitorPass[] = [];
@State qrCodeValue: string = '';
@State showQRCode: boolean = false;
@State currentPass: VisitorPass | null = null;
@State generateLoading: boolean = false;
aboutToAppear() {
// 加载活跃的访客通行证
this.loadActivePasses();
}
/**
* 加载活跃的访客通行证
*/
private async loadActivePasses() {
try {
const userInfo = await cloud.auth().getUserInfo();
const result = await cloud.database().collection('visitor_passes')
.where({
ownerId: userInfo.uid,
status: 'active',
endTime: { $gt: Date.now() }
})
.orderBy('endTime', 'asc')
.get();
this.activePasses = result.data as VisitorPass[];
} catch (err) {
console.error('加载访客通行证失败:' + JSON.stringify(err));
}
}
/**
* 生成访客通行证
*/
private async generateVisitorPass() {
if (!this.visitorName) {
AlertDialog.show({ message: '请输入访客姓名' });
return;
}
this.generateLoading = true;
try {
const userInfo = await cloud.auth().getUserInfo();
// 生成唯一ID
const passId = 'pass_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// 生成QR码内容(包含通行证ID和加密信息)
const qrContent = `visitor:${passId}:${Date.now()}`;
// 生成安全令牌(使用HMAC-SHA256)
const secretKey = await this.getSecretKey();
const token = await this.generateToken(qrContent, secretKey);
// 计算起止时间
const startTime = Date.now();
const endTime = startTime + this.accessDuration * 60 * 1000;
// 获取社区设备列表(哪些设备需要授权)
const communityDevices = await this.getCommunityDevices();
// 构建通行证对象
const pass: VisitorPass = {
id: passId,
ownerId: userInfo.uid,
visitorName: this.visitorName,
visitorPhone: this.visitorPhone || undefined,
qrCode: qrContent,
token: token,
startTime: startTime,
endTime: endTime,
maxUses: this.accessCount,
usedCount: 0,
devices: communityDevices,
status: 'active'
};
// 保存到云端
await cloud.database().collection('visitor_passes').add(pass);
// 同步权限到所有设备(分布式授权)
await this.syncPermissionsToDevices(pass);
// 更新UI
this.currentPass = pass;
this.qrCodeValue = JSON.stringify({
id: passId,
t: token.substring(0, 8) // 仅用于显示
});
this.showQRCode = true;
// 刷新列表
this.loadActivePasses();
} catch (err) {
AlertDialog.show({
message: '生成失败:' + JSON.stringify(err)
});
} finally {
this.generateLoading = false;
}
}
/**
* 获取密钥(实际应从安全存储中获取)
*/
private async getSecretKey(): Promise<cryptoFramework.KeyPair> {
// 实际开发中应从HUKS获取或生成密钥
// 这里简化处理
return {} as cryptoFramework.KeyPair;
}
/**
* 生成HMAC令牌
*/
private async generateToken(data: string, key: cryptoFramework.KeyPair): Promise<string> {
try {
const mac = cryptoFramework.createMac('HmacSHA256');
await mac.init(key);
await mac.update({ data: new Uint8Array(Buffer.from(data)) });
const result = await mac.doFinal();
return Buffer.from(result).toString('hex');
} catch (err) {
console.error('生成令牌失败:' + JSON.stringify(err));
return 'token_' + Date.now(); // 降级方案
}
}
/**
* 获取社区设备列表
*/
private async getCommunityDevices(): Promise<string[]> {
try {
// 从云端获取业主绑定的社区设备
const userInfo = await cloud.auth().getUserInfo();
const result = await cloud.database().collection('user_devices')
.where({ userId: userInfo.uid })
.get();
return result.data.map((d: any) => d.deviceId);
} catch (err) {
console.error('获取设备列表失败:' + JSON.stringify(err));
return ['main_gate', 'building_3_gate', 'elevator_1']; // 默认设备
}
}
/**
* 同步权限到所有设备(分布式授权核心)
*/
private async syncPermissionsToDevices(pass: VisitorPass) {
try {
// 获取所有需要同步的设备(门禁、电梯等)
const devices = pass.devices;
// 为每个设备创建授权记录
for (const deviceId of devices) {
// 检查设备是否在线
const deviceInfo = await distributedDevice.getDeviceInfo(deviceId);
if (deviceInfo && deviceInfo.status === 'online') {
// 设备在线,直接推送授权
await distributedDevice.sync(deviceId, 'visitor_auth', {
passId: pass.id,
token: pass.token,
startTime: pass.startTime,
endTime: pass.endTime,
maxUses: pass.maxUses,
visitorName: pass.visitorName
});
console.info(`权限已同步到设备:${deviceId}`);
} else {
// 设备离线,将授权记录保存在云端,设备上线后拉取
await cloud.database().collection('device_pending_auths').add({
deviceId: deviceId,
passId: pass.id,
authData: {
token: pass.token,
startTime: pass.startTime,
endTime: pass.endTime,
maxUses: pass.maxUses
},
status: 'pending',
createTime: Date.now()
});
console.info(`设备离线,授权待同步:${deviceId}`);
}
}
// 记录授权操作日志
await cloud.function('logAuthOperation').call({
type: 'visitor_grant',
passId: pass.id,
devices: devices,
timestamp: Date.now()
});
} catch (err) {
console.error('同步权限失败:' + JSON.stringify(err));
throw err;
}
}
/**
* 撤销访客通行证
*/
private async revokePass(passId: string) {
try {
// 更新云端状态
await cloud.database().collection('visitor_passes')
.doc(passId)
.update({ status: 'revoked' });
// 获取该通行证关联的设备
const pass = this.activePasses.find(p => p.id === passId);
if (pass) {
// 向所有设备发送撤销指令
for (const deviceId of pass.devices) {
await distributedDevice.sync(deviceId, 'visitor_revoke', {
passId: passId,
token: pass.token
});
}
}
// 刷新列表
this.loadActivePasses();
// 如果当前显示的二维码被撤销,隐藏
if (this.currentPass?.id === passId) {
this.showQRCode = false;
this.currentPass = null;
}
AlertDialog.show({ message: '已撤销通行权限' });
} catch (err) {
AlertDialog.show({ message: '撤销失败:' + JSON.stringify(err) });
}
}
build() {
Column() {
// 标题栏
Row() {
Image($r('app.media.back'))
.width(24)
.height(24)
.onClick(() => router.back())
Text('访客通行')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ left: 16 })
}
.width('100%')
.padding(12)
.backgroundColor(Color.White)
Scroll() {
Column({ space: 16 }) {
// 生成新通行证表单
Column() {
Text('生成临时通行证')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
// 访客姓名
Column() {
Text('访客姓名')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.width('100%')
TextInput({
placeholder: '请输入访客姓名',
text: this.visitorName,
onChange: (value) => { this.visitorName = value; }
})
.backgroundColor('#F5F5F5')
.borderRadius(8)
.margin({ top: 4 })
}
.margin({ top: 12 })
// 访客电话(可选)
Column() {
Text('访客电话(可选)')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.width('100%')
TextInput({
placeholder: '请输入访客电话',
text: this.visitorPhone,
onChange: (value) => { this.visitorPhone = value; },
type: InputType.PhoneNumber
})
.backgroundColor('#F5F5F5')
.borderRadius(8)
.margin({ top: 4 })
}
.margin({ top: 8 })
// 有效时长
Column() {
Text('有效时长')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.width('100%')
Row({ space: 10 }) {
this.durationButton(15, '15分钟')
this.durationButton(30, '30分钟')
this.durationButton(60, '1小时')
this.durationButton(120, '2小时')
}
.width('100%')
.margin({ top: 8 })
}
.margin({ top: 8 })
// 可使用次数
Column() {
Text('可使用次数')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.width('100%')
Row({ space: 10 }) {
this.countButton(1, '单次')
this.countButton(3, '3次')
this.countButton(10, '10次')
}
.width('100%')
.margin({ top: 8 })
}
.margin({ top: 8 })
// 生成按钮
Button(this.generateLoading ? '生成中...' : '生成通行证')
.width('100%')
.height(44)
.backgroundColor('#0077FF')
.enabled(!this.generateLoading && !!this.visitorName)
.onClick(() => this.generateVisitorPass())
.margin({ top: 16 })
}
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ horizontal: 16, top: 16 })
// 生成的二维码展示
if (this.showQRCode && this.currentPass) {
Column() {
Text('通行二维码')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
// 此处实际应使用QRCode组件
Column() {
Text('二维码模拟')
.fontSize(12)
.fontColor('#666')
Text(this.qrCodeValue)
.fontSize(10)
.fontColor('#999')
.margin({ top: 4 })
Text(`有效期至:${new Date(this.currentPass.endTime).toLocaleString()}`)
.fontSize(12)
.margin({ top: 8 })
Text(`剩余次数:${this.currentPass.maxUses - this.currentPass.usedCount}/${this.currentPass.maxUses}`)
.fontSize(12)
}
.width(200)
.height(200)
.backgroundColor('#F5F5F5')
.justifyContent(FlexAlign.Center)
.borderRadius(12)
.margin({ top: 12 })
Row({ space: 16 }) {
Button('分享二维码')
.backgroundColor('#4CAF50')
.onClick(() => {
// 调用分享接口
})
Button('撤销权限')
.backgroundColor('#FF4444')
.onClick(() => {
this.revokePass(this.currentPass!.id);
})
}
.margin({ top: 12 })
}
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ horizontal: 16, top: 16 })
}
// 活跃的通行证列表
if (this.activePasses.length > 0) {
Column() {
Text('当前有效的通行证')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
ForEach(this.activePasses, (pass: VisitorPass) => {
this.passItem(pass)
})
}
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ horizontal: 16, top: 16, bottom: 16 })
}
}
}
.backgroundColor('#F5F5F5')
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Builder
durationButton(minutes: number, label: string) {
Column() {
Text(label)
.fontSize(14)
.fontColor(this.accessDuration === minutes ? '#0077FF' : '#333')
}
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.backgroundColor(this.accessDuration === minutes ? '#E6F0FF' : '#F5F5F5')
.borderRadius(16)
.onClick(() => {
this.accessDuration = minutes;
})
}
@Builder
countButton(count: number, label: string) {
Column() {
Text(label)
.fontSize(14)
.fontColor(this.accessCount === count ? '#0077FF' : '#333')
}
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.backgroundColor(this.accessCount === count ? '#E6F0FF' : '#F5F5F5')
.borderRadius(16)
.onClick(() => {
this.accessCount = count;
})
}
@Builder
passItem(pass: VisitorPass) {
Row() {
Column() {
Text(pass.visitorName)
.fontSize(14)
.fontWeight(FontWeight.Medium)
Text(`有效期至:${new Date(pass.endTime).toLocaleString()}`)
.fontSize(10)
.fontColor('#999')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text(`${pass.usedCount}/${pass.maxUses}`)
.fontSize(12)
.margin({ right: 12 })
Button('撤销')
.fontSize(12)
.height(28)
.backgroundColor('#FF4444')
.onClick(() => {
this.revokePass(pass.id);
})
}
.width('100%')
.padding({ top: 8, bottom: 8 })
}
}
3.3.2 门禁端验证组件(GateVerifier.ets)
当访客在门禁上扫码时,门禁端需要验证通行证的有效性。
// GateVerifier.ets
import { cloud } from '@ohos.cloud';
import { distributedDevice } from '@ohos.distributedDevice';
import { cryptoFramework } from '@ohos.security.cryptoFramework';
// 门禁验证器
class GateVerifier {
private deviceId: string;
private secretKey: cryptoFramework.KeyPair | null = null;
constructor(deviceId: string) {
this.deviceId = deviceId;
this.initKey();
}
private async initKey() {
// 初始化密钥(实际应从HUKS获取)
try {
// 简化处理
} catch (err) {
console.error('初始化密钥失败:' + JSON.stringify(err));
}
}
/**
* 验证访客二维码
* @param qrData 二维码数据
*/
public async verifyQRCode(qrData: string): Promise<{
valid: boolean;
passId?: string;
visitorName?: string;
message?: string;
}> {
try {
// 解析二维码
let qrObj: { id: string; t: string };
try {
qrObj = JSON.parse(qrData);
} catch {
// 如果不是JSON格式,尝试直接解析
const parts = qrData.split(':');
if (parts.length >= 2) {
qrObj = { id: parts[1], t: '' };
} else {
throw new Error('无效的二维码格式');
}
}
const passId = qrObj.id;
// 1. 首先检查本地缓存
const localPass = await this.getLocalPass(passId);
if (localPass) {
const validation = this.validatePass(localPass);
if (validation.valid) {
// 记录使用次数
await this.recordUsage(passId, this.deviceId);
return {
valid: true,
passId: passId,
visitorName: localPass.visitorName
};
} else {
return validation;
}
}
// 2. 本地无缓存或已过期,查询云端
const remotePass = await this.getRemotePass(passId);
if (!remotePass) {
return { valid: false, message: '通行证不存在' };
}
// 验证令牌(可选)
if (qrObj.t) {
const tokenValid = await this.verifyToken(passId, qrObj.t, remotePass);
if (!tokenValid) {
return { valid: false, message: '令牌验证失败' };
}
}
// 验证有效期和次数
const validation = this.validatePass(remotePass);
if (validation.valid) {
// 缓存到本地(分布式同步)
await this.cacheLocalPass(remotePass);
// 记录使用
await this.recordUsage(passId, this.deviceId);
return {
valid: true,
passId: passId,
visitorName: remotePass.visitorName
};
} else {
return validation;
}
} catch (err) {
console.error('验证失败:' + JSON.stringify(err));
return { valid: false, message: '验证异常' };
}
}
/**
* 获取本地缓存的通行证
*/
private async getLocalPass(passId: string): Promise<any> {
try {
const kvStore = await distributedKVStore.getKVStore('visitor_passes');
const value = await kvStore.get(passId);
return value ? JSON.parse(value) : null;
} catch {
return null;
}
}
/**
* 缓存通行证到本地
*/
private async cacheLocalPass(pass: any) {
try {
const kvStore = await distributedKVStore.getKVStore('visitor_passes');
await kvStore.put(pass.id, JSON.stringify(pass));
// 设置自动过期(根据pass.endTime)
setTimeout(() => {
kvStore.delete(pass.id);
}, Math.max(0, pass.endTime - Date.now()));
} catch (err) {
console.error('缓存失败:' + JSON.stringify(err));
}
}
/**
* 从云端获取通行证
*/
private async getRemotePass(passId: string): Promise<any> {
try {
const result = await cloud.database().collection('visitor_passes')
.doc(passId)
.get();
return result.data;
} catch (err) {
console.error('云端查询失败:' + JSON.stringify(err));
return null;
}
}
/**
* 验证令牌
*/
private async verifyToken(passId: string, tokenPart: string, pass: any): Promise<boolean> {
// 实际应使用完整令牌验证
// 简化处理:检查令牌前缀是否匹配
return pass.token && pass.token.startsWith(tokenPart);
}
/**
* 验证通行证有效性
*/
private validatePass(pass: any): { valid: boolean; message?: string } {
const now = Date.now();
if (pass.status !== 'active') {
return { valid: false, message: '通行证已失效' };
}
if (now < pass.startTime) {
return { valid: false, message: '通行证尚未生效' };
}
if (now > pass.endTime) {
return { valid: false, message: '通行证已过期' };
}
if (pass.usedCount >= pass.maxUses) {
return { valid: false, message: '使用次数已达上限' };
}
return { valid: true };
}
/**
* 记录使用次数
*/
private async recordUsage(passId: string, deviceId: string) {
try {
// 更新云端使用次数
await cloud.function('incrementPassUsage').call({
passId: passId,
deviceId: deviceId,
timestamp: Date.now()
});
// 同步更新本地缓存
const localPass = await this.getLocalPass(passId);
if (localPass) {
localPass.usedCount += 1;
await this.cacheLocalPass(localPass);
}
// 广播使用事件(其他设备可同步状态)
await distributedDevice.broadcast('visitor_usage', {
passId: passId,
deviceId: deviceId,
timestamp: Date.now()
});
} catch (err) {
console.error('记录使用失败:' + JSON.stringify(err));
}
}
/**
* 开锁
*/
public async unlockDoor(doorId: string, passId: string) {
try {
// 调用门禁控制API
const doorControl = new doorControl.DoorController();
await doorControl.unlock(doorId, {
reason: 'visitor',
passId: passId,
deviceId: this.deviceId
});
console.info(`门禁 ${doorId} 已开启,通行证:${passId}`);
// 通知业主访客已进入
await this.notifyOwner(passId, doorId);
} catch (err) {
console.error('开锁失败:' + JSON.stringify(err));
throw err;
}
}
/**
* 通知业主
*/
private async notifyOwner(passId: string, doorId: string) {
try {
const pass = await this.getRemotePass(passId);
if (pass?.ownerId) {
// 获取业主在线设备
const devices = await distributedDevice.getUserOnlineDevices(pass.ownerId);
// 优先推送到手表
const watch = devices.find(d => d.type === 'wearable');
if (watch) {
await distributedDevice.sync(watch.id, 'visitor_alert', {
type: 'visitor_entered',
visitorName: pass.visitorName,
doorId: doorId,
time: Date.now()
});
} else {
// 否则推送到手机
await distributedDevice.sync(devices[0]?.id, 'visitor_alert', {
type: 'visitor_entered',
visitorName: pass.visitorName,
doorId: doorId,
time: Date.now()
});
}
}
} catch (err) {
console.error('通知业主失败:' + JSON.stringify(err));
}
}
}
3.4 分布式访问控制的关键技术
3.4.1 设备认证与信任建立
在鸿蒙中,设备想加入分布式网络,必须先通过认证。认证方式包括:
- PIN码认证:用户手动输入配对码
- 碰一碰认证:NFC触碰自动配对
- 云账号认证:基于同一华为账号自动信任
// 设备认证示例
import deviceManager from '@ohos.distributedHardware.deviceManager';
let dm = deviceManager.createDeviceManager("com.smartcommunity");
// 认证远端设备
dm.authenticateDevice(deviceId, {
authType: 1, // 1 = PIN码认证
extraInfo: "Smart Community Device"
});
3.4.2 基于Token的跨设备授权
跨设备访问必须基于Token,而不是基于进程ID/UID,因为已经跨设备了。Token大幅减少了传统分布式授权的复杂度。
// 携带Token调用远端Ability
let want = {
bundleName: "com.smartcommunity.door",
abilityName: "DoorAbility",
deviceId: remoteDeviceId,
parameters: {
action: "unlock",
passId: passId,
authToken: token // 必须携带用户令牌
}
};
FeatureAbility.startAbility(want);
3.4.3 权限分级管理
鸿蒙Next引入权限分级3.0机制,权限分为四个等级:
| 等级 | 说明 | 示例 |
|---|---|---|
| NORMAL | 基础权限 | 网络状态访问 |
| LIMITED | 受限权限 | 日历读写 |
| SIGNATURE | 系统级权限 | 修改系统设置 |
| AI_SENSITIVE | AI敏感权限 | 生物识别 |
访客通行涉及DISTRIBUTED_DATASYNC等分布式权限,属于受限权限,需要在config.json中声明并说明使用原因。
3.5 行业实践:分布式安全的温度
华为云社区一篇文章指出:“单设备时代,安全是‘防护’;分布式时代,安全是‘关系’”。访客通行权限的跨设备流转,正是这种理念的体现——安全不再是挡住用户,而是保护用户的体验,让协作更简单,让安全更自然。
第四部分:AI安防与老人关怀——智慧社区的延伸
智慧社区不仅仅是门禁和报修,安防和康养同样是核心场景。在2025年武汉智博会上,湖北联投展示的AI智慧生活场景让人印象深刻。
4.1 AI全域安防
场景一:电动车入梯预警
当电动车被推入电梯,AI摄像头自动识别,电梯内警报响起,同时电梯暂停运行,直到电动车退出。系统还会将违规记录推送给物业和车主。
场景二:高空抛物追溯
AI平台通过存储视频进行大模型算法分析,提取抛物特征点,实现精准追溯。从“事后追责”变为“事前预防”。
场景三:AI寻人寻物
当业主物品遗失或老人小孩走失,AI平台通过视频分析,提取特征信息,实现精准找人找物。
4.2 老人关怀与主动健康管理
智能监测:在联投燕语光年乐龄样板间,配有紧急呼叫与双控开关,可升降飘窗拓展休闲场景,卫生间配有智能报警器,全方位守护长者日常活动安全。
主动预警:当老人跌倒时,3秒内救援电话就会打出。系统不再是被动的“照护”,而是主动的“健康管理”。
4.3 与门禁/访客系统的联动
AI安防与门禁系统可以深度联动:
- 当独居老人连续24小时未出门且未检测到室内活动时,系统自动通知物业管家上门查看
- 当老人出门后长时间未归,系统向子女手机推送提醒
- 当老人刷卡开门时,如果是在非惯常时间(如凌晨),系统向紧急联系人发送通知
第五部分:性能优化与安全最佳实践
5.1 权限请求的四大原则
根据鸿蒙权限管理最佳实践,权限请求应遵循以下原则:
-
最小化原则:仅请求当前场景必需的权限,避免预先请求未使用的权限。
// 错误示例:一次性请求所有可能用到的权限 const permissions = ["ohos.permission.CAMERA", "ohos.permission.READ_CALENDAR", "ohos.permission.MICROPHONE"]; // 正确做法:按需请求,仅在需要时请求 // 门禁页面仅请求生物识别权限 -
透明性原则:自定义权限请求对话框,清晰说明权限用途。
function showCameraPermissionDialog() { AlertDialog.show({ title: "需要摄像头权限", message: "用于人脸识别开门,我们不会在未经您同意的情况下录制视频", confirmText: "允许", cancelText: "暂不允许" }); } -
时机恰当原则:在用户即将使用相关功能时请求权限,而非应用启动时。
-
降级处理原则:用户拒绝权限后,提供降级方案(如指纹代替人脸)。
5.2 渲染性能优化
在社区应用中,列表渲染(如报修记录、访客记录)可能成为性能瓶颈。使用LazyForEach实现按需加载:
// 使用LazyForEach优化长列表
LazyForEach(this.visitorPassDataSource, (pass) => {
VisitorPassItem({ pass: pass })
}, pass => pass.id)
5.3 功耗优化
社区应用常驻后台时,需注意功耗优化:
// 按需启用位置服务
geofence.enable().then(() => {
// 获取地理围栏信息后立即关闭
geofence.disable();
});
// 使用WorkScheduler管理后台任务
const workInfo = {
workName: "syncVisitorPasses",
workId: 1001,
bundleName: "com.smartcommunity",
triggerCondition: {
timeInterval: 30 * 60 * 1000 // 每30分钟同步一次
}
};
workScheduler.startWork(workInfo);
5.4 数据安全
- 端到端加密:门禁控制指令必须加密传输
- 本地加密:敏感数据(如访客令牌)存储在HUKS中
- 防重放攻击:每次认证使用不同的challenge值
第六部分:未来展望——鸿蒙智慧社区的演进方向
6.1 无感认证的普及
未来的智慧社区将实现“无感通行”——无需主动认证,系统通过行为生物特征(步态、握持习惯)自动识别用户。同时结合环境感知(常用WiFi、蓝牙设备),自动判定可信环境,减少认证弹窗。
6.2 AI与大模型的深度集成
随着AI模型调用权限的标准化,社区服务将更加智能化:
- AI管家主动提醒快递取件、物业缴费
- 根据生活习惯自动调节公共区域照明、空调
- 异常行为预判(如深夜独行、徘徊)触发主动安防
6.3 跨社区互联
当鸿蒙生态覆盖足够多的社区后,可以实现“跨社区信任”——业主在本社区认证后,可临时授权访问附近社区的公共设施(如健身房、游泳池),形成更大的信任网络。
结语
从多模态门禁的活体检测,到报修服务的原子化卡片,再到访客权限的跨设备流转,鸿蒙为智慧社区带来了全新的可能性。这不仅是技术的升级,更是体验的变革——设备不再是孤岛,服务不再是负担,安全不再是障碍。
碧桂园服务用45天完成鸿蒙版凤凰会开发的故事告诉我们:在鸿蒙生态下,智慧社区的落地比想象中更快。联投长江天元的AI全场景打通证明:这不是简单的设备堆砌,而是户内户外全场景的深度融合。
作为开发者,我们既要理解分布式权限与Token的严谨性,也要理解“分布式世界里安全的温度”——安全不是挡住用户,而是保护用户的体验,让科技真正温暖生活。
附录:完整项目结构
smart_community/
├── entry/
│ ├── src/
│ │ ├── main/
│ │ │ ├── ets/
│ │ │ │ ├── MainAbility/
│ │ │ │ │ └── MainAbility.ts
│ │ │ │ ├── pages/
│ │ │ │ │ ├── Index.ets # 首页
│ │ │ │ │ ├── SmartDoorPanel.ets # 智能门禁
│ │ │ │ │ ├── RepairFormPage.ets # 报修表单
│ │ │ │ │ ├── VisitorAccessPage.ets # 访客通行
│ │ │ │ │ └── GateVerifier.ets # 门禁验证器
│ │ │ │ ├── common/
│ │ │ │ │ ├── constants/
│ │ │ │ │ └── utils/
│ │ │ │ └── models/
│ │ │ │ ├── DoorModels.ets
│ │ │ │ ├── RepairModels.ets
│ │ │ │ └── VisitorModels.ets
│ │ │ ├── resources/
│ │ │ └── module.json5
│ │ └── ohosTest/
├── feature/
│ ├── repair_card/ # 报修服务卡片
│ └── visitor_widget/ # 访客管理卡片
├── cloud/ # 云函数
│ ├── createRepairOrder/
│ ├── incrementPassUsage/
│ └── syncAccessLog/
└── README.md
更多推荐




所有评论(0)