Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
S
smart_hotel_app
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
张宏
smart_hotel_app
Commits
76a57f12
Commit
76a57f12
authored
Jun 07, 2026
by
张宏
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
。
parent
9d09840c
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
1831 additions
and
5 deletions
+1831
-5
.gitignore
.gitignore
+5
-5
对接方案.md
对接方案.md
+1222
-0
测试方案.md
测试方案.md
+604
-0
No files found.
.gitignore
View file @
76a57f12
...
...
@@ -52,7 +52,7 @@ app.*.map.json
ai_work/
## 文档相关
测试方案.md
对接方案.md
技术方案.md
上手帮助.md
\ No newline at end of file
# 测试方案.md
# 对接方案.md
# 技术方案.md
# 上手帮助.md
\ No newline at end of file
对接方案.md
0 → 100644
View file @
76a57f12
# 智慧酒店 App 接口对接方案
# 智慧酒店 App 接口对接方案
## 1. 对接原则
### 1.1 核心原则
-
**View 层不变**
:所有 UI 代码已完成,对接时只修改 Cubit 及以下层
-
**统一调用链**
:Cubit → Service → Repository → DioRequest,不允许 Cubit 直接调用 Repository
-
**BO 模型统一**
:所有数据模型定义在
`models/bo/`
,State 中的内联模型需迁移
-
**错误统一处理**
:Service 层处理业务错误,网络错误由拦截器自动处理
-
**loading/error 状态**
:每个 State 必须包含
`isLoading`
和
`error`
字段
### 1.2 代码模板
#### Repository 模板
```
dart
// lib/repositories/xxx_repository.dart
import
'../http/response_model.dart'
;
import
'../http/dio_request.dart'
;
import
'../models/bo/xxx_bo.dart'
;
class
XxxRepository
{
// GET 请求 - 返回列表
Future
<
ResponseModel
<
List
<
XxxBO
>>>
getList
()
{
return
DioRequest
.
instance
.
get
<
List
<
XxxBO
>>(
'/api/xxx'
,
fromJsonT:
(
data
)
{
final
list
=
data
as
List
<
dynamic
>;
return
list
.
map
((
e
)
=>
XxxBO
.
fromJson
(
e
as
Map
<
String
,
dynamic
>)).
toList
();
},
);
}
// GET 请求 - 返回单个对象
Future
<
ResponseModel
<
XxxBO
>>
getDetail
(
String
id
)
{
return
DioRequest
.
instance
.
get
<
XxxBO
>(
'/api/xxx/
$id
'
,
fromJsonT:
(
data
)
=>
XxxBO
.
fromJson
(
data
as
Map
<
String
,
dynamic
>),
);
}
// POST 请求
Future
<
ResponseModel
>
create
(
Map
<
String
,
dynamic
>
params
)
{
return
DioRequest
.
instance
.
post
(
'/api/xxx'
,
data:
params
);
}
// PUT 请求
Future
<
ResponseModel
>
update
(
String
id
,
Map
<
String
,
dynamic
>
params
)
{
return
DioRequest
.
instance
.
put
(
'/api/xxx/
$id
'
,
data:
params
);
}
}
```
#### Service 模板
```
dart
// lib/services/xxx_service.dart
import
'../repositories/xxx_repository.dart'
;
import
'../models/bo/xxx_bo.dart'
;
class
XxxService
{
final
XxxRepository
_repository
;
XxxService
({
required
XxxRepository
repository
})
:
_repository
=
repository
;
Future
<
List
<
XxxBO
>>
getList
()
async
{
final
result
=
await
_repository
.
getList
();
if
(
result
.
success
&&
result
.
data
!=
null
)
{
return
result
.
data
!;
}
throw
Exception
(
result
.
msg
);
}
Future
<
XxxBO
>
getDetail
(
String
id
)
async
{
final
result
=
await
_repository
.
getDetail
(
id
);
if
(
result
.
success
&&
result
.
data
!=
null
)
{
return
result
.
data
!;
}
throw
Exception
(
result
.
msg
);
}
}
```
#### Cubit 改造模板(从 Mock 到真实 API)
```
dart
// 改造前(Mock)
class
XxxCubit
extends
Cubit
<
XxxState
>
{
XxxCubit
()
:
super
(
const
XxxState
())
{
_initMockData
();
// 硬编码数据
}
void
_initMockData
()
{
/* emit mock data */
}
}
// 改造后(真实 API)
class
XxxCubit
extends
Cubit
<
XxxState
>
{
final
XxxService
_service
;
XxxCubit
({
required
XxxService
service
})
:
_service
=
service
,
super
(
const
XxxState
())
{
loadData
();
}
Future
<
void
>
loadData
()
async
{
emit
(
state
.
copyWith
(
isLoading:
true
,
error:
null
));
try
{
final
data
=
await
_service
.
getList
();
emit
(
state
.
copyWith
(
isLoading:
false
,
dataList:
data
));
}
catch
(
e
)
{
emit
(
state
.
copyWith
(
isLoading:
false
,
error:
e
.
toString
()));
}
}
}
```
---
## 2. BO 模型定义
### 2.1 文件清单
按模块组织,每个文件包含该模块所有 BO 类:
| 文件 | 包含的 BO 类 | 对应原 State 文件中的类 |
|------|-------------|----------------------|
|
`alarm_bo.dart`
|
`AlarmItemBO`
,
`AlarmDetailBO`
,
`TemperatureSpotBO`
|
`AlarmItem`
,
`AlarmInfo`
|
|
`device_bo.dart`
|
`DeviceInfoBO`
,
`EquipmentItemBO`
|
`DeviceInfo`
,
`EquipmentItem`
|
|
`inspection_bo.dart`
|
`InspectionDeviceBO`
,
`InspectionRecordBO`
,
`InspectionItemBO`
|
`InspectionDevice`
,
`InspectionRecord`
等 |
|
`topology_bo.dart`
|
`TopologyNodeBO`
|
`TopologyNode`
|
|
`energy_bo.dart`
|
`EnergyOverviewBO`
,
`ZoneDataBO`
,
`HourlyDataBO`
|
`ZoneData`
等 |
|
`room_bo.dart`
|
`RoomInfoBO`
,
`RoomDeviceBO`
,
`RoomDeviceStatusBO`
|
`RoomInfo`
,
`RoomDevice`
,
`RoomDeviceStatus`
|
|
`rule_bo.dart`
|
`RuleInfoBO`
|
`RuleInfo`
|
### 2.2 详细定义
#### alarm_bo.dart
```
dart
import
'package:equatable/equatable.dart'
;
class
AlarmItemBO
extends
Equatable
{
final
String
id
;
final
String
title
;
final
String
deviceInfo
;
final
String
alarmTime
;
final
String
processTime
;
final
String
status
;
// "pending" | "processed" | "falseAlarm"
final
String
level
;
// "高警" | "中警" | "低警"
final
String
urgency
;
// "紧急" | "一般"
const
AlarmItemBO
({
required
this
.
id
,
required
this
.
title
,
required
this
.
deviceInfo
,
required
this
.
alarmTime
,
required
this
.
processTime
,
required
this
.
status
,
required
this
.
level
,
required
this
.
urgency
,
});
factory
AlarmItemBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
AlarmItemBO
(
id:
json
[
'id'
]
as
String
,
title:
json
[
'title'
]
as
String
,
deviceInfo:
json
[
'deviceInfo'
]
as
String
?
??
''
,
alarmTime:
json
[
'alarmTime'
]
as
String
?
??
''
,
processTime:
json
[
'processTime'
]
as
String
?
??
''
,
status:
json
[
'status'
]
as
String
?
??
'pending'
,
level:
json
[
'level'
]
as
String
?
??
''
,
urgency:
json
[
'urgency'
]
as
String
?
??
''
,
);
}
@override
List
<
Object
?>
get
props
=>
[
id
,
title
,
deviceInfo
,
alarmTime
,
processTime
,
status
,
level
,
urgency
];
}
class
AlarmDetailBO
extends
Equatable
{
final
String
alarmType
;
final
String
deviceInfo
;
final
String
alarmLevel
;
final
String
urgency
;
final
String
currentTemp
;
final
String
voltage
;
final
String
current
;
final
String
triggerTime
;
final
String
threshold
;
final
String
duration
;
final
String
peak
;
final
String
linkAction
;
final
String
date
;
final
List
<
TemperatureSpotBO
>
temperatureSpots
;
const
AlarmDetailBO
({
required
this
.
alarmType
,
required
this
.
deviceInfo
,
required
this
.
alarmLevel
,
required
this
.
urgency
,
required
this
.
currentTemp
,
required
this
.
voltage
,
required
this
.
current
,
required
this
.
triggerTime
,
required
this
.
threshold
,
required
this
.
duration
,
required
this
.
peak
,
required
this
.
linkAction
,
required
this
.
date
,
required
this
.
temperatureSpots
,
});
factory
AlarmDetailBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
AlarmDetailBO
(
alarmType:
json
[
'alarmType'
]
as
String
?
??
''
,
deviceInfo:
json
[
'deviceInfo'
]
as
String
?
??
''
,
alarmLevel:
json
[
'alarmLevel'
]
as
String
?
??
''
,
urgency:
json
[
'urgency'
]
as
String
?
??
''
,
currentTemp:
json
[
'currentTemp'
]
as
String
?
??
''
,
voltage:
json
[
'voltage'
]
as
String
?
??
''
,
current:
json
[
'current'
]
as
String
?
??
''
,
triggerTime:
json
[
'triggerTime'
]
as
String
?
??
''
,
threshold:
json
[
'threshold'
]
as
String
?
??
''
,
duration:
json
[
'duration'
]
as
String
?
??
''
,
peak:
json
[
'peak'
]
as
String
?
??
''
,
linkAction:
json
[
'linkAction'
]
as
String
?
??
''
,
date:
json
[
'date'
]
as
String
?
??
''
,
temperatureSpots:
(
json
[
'temperatureSpots'
]
as
List
<
dynamic
>?)
?.
map
((
e
)
=>
TemperatureSpotBO
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
.
toList
()
??
[],
);
}
@override
List
<
Object
?>
get
props
=>
[
alarmType
,
deviceInfo
,
alarmLevel
,
urgency
,
currentTemp
,
voltage
,
current
,
triggerTime
,
threshold
,
duration
,
peak
,
linkAction
,
date
,
temperatureSpots
];
}
class
TemperatureSpotBO
extends
Equatable
{
final
double
x
;
final
double
y
;
const
TemperatureSpotBO
({
required
this
.
x
,
required
this
.
y
});
factory
TemperatureSpotBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
TemperatureSpotBO
(
x:
(
json
[
'x'
]
as
num
).
toDouble
(),
y:
(
json
[
'y'
]
as
num
).
toDouble
(),
);
}
@override
List
<
Object
?>
get
props
=>
[
x
,
y
];
}
```
#### device_bo.dart
```
dart
import
'package:equatable/equatable.dart'
;
class
DeviceInfoBO
extends
Equatable
{
final
String
deviceName
;
final
String
deviceId
;
final
String
location
;
final
String
status
;
final
String
voltage
;
final
String
current
;
final
String
power
;
final
String
temperature
;
final
List
<
double
>
powerData
;
final
List
<
double
>
tempData
;
const
DeviceInfoBO
({
required
this
.
deviceName
,
required
this
.
deviceId
,
required
this
.
location
,
required
this
.
status
,
required
this
.
voltage
,
required
this
.
current
,
required
this
.
power
,
required
this
.
temperature
,
required
this
.
powerData
,
required
this
.
tempData
,
});
factory
DeviceInfoBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
DeviceInfoBO
(
deviceName:
json
[
'deviceName'
]
as
String
?
??
''
,
deviceId:
json
[
'deviceId'
]
as
String
?
??
''
,
location:
json
[
'location'
]
as
String
?
??
''
,
status:
json
[
'status'
]
as
String
?
??
''
,
voltage:
json
[
'voltage'
]
as
String
?
??
''
,
current:
json
[
'current'
]
as
String
?
??
''
,
power:
json
[
'power'
]
as
String
?
??
''
,
temperature:
json
[
'temperature'
]
as
String
?
??
''
,
powerData:
(
json
[
'powerData'
]
as
List
<
dynamic
>?)?.
map
((
e
)
=>
(
e
as
num
).
toDouble
()).
toList
()
??
[],
tempData:
(
json
[
'tempData'
]
as
List
<
dynamic
>?)?.
map
((
e
)
=>
(
e
as
num
).
toDouble
()).
toList
()
??
[],
);
}
@override
List
<
Object
?>
get
props
=>
[
deviceName
,
deviceId
,
location
,
status
,
voltage
,
current
,
power
,
temperature
,
powerData
,
tempData
];
}
class
EquipmentItemBO
extends
Equatable
{
final
String
title
;
final
String
subtitle
;
final
String
status
;
final
String
statusColor
;
// hex color string, e.g. "#1ABC9C"
const
EquipmentItemBO
({
required
this
.
title
,
required
this
.
subtitle
,
required
this
.
status
,
required
this
.
statusColor
,
});
factory
EquipmentItemBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
EquipmentItemBO
(
title:
json
[
'title'
]
as
String
?
??
''
,
subtitle:
json
[
'subtitle'
]
as
String
?
??
''
,
status:
json
[
'status'
]
as
String
?
??
''
,
statusColor:
json
[
'statusColor'
]
as
String
?
??
'#1ABC9C'
,
);
}
@override
List
<
Object
?>
get
props
=>
[
title
,
subtitle
,
status
,
statusColor
];
}
```
#### inspection_bo.dart
```
dart
import
'package:equatable/equatable.dart'
;
class
InspectionDeviceBO
extends
Equatable
{
final
String
id
;
final
String
name
;
final
String
type
;
final
String
location
;
final
String
status
;
final
String
time
;
final
String
statusColor
;
const
InspectionDeviceBO
({
required
this
.
id
,
required
this
.
name
,
required
this
.
type
,
required
this
.
location
,
required
this
.
status
,
required
this
.
time
,
required
this
.
statusColor
,
});
factory
InspectionDeviceBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
InspectionDeviceBO
(
id:
json
[
'id'
]
as
String
?
??
''
,
name:
json
[
'name'
]
as
String
?
??
''
,
type:
json
[
'type'
]
as
String
?
??
''
,
location:
json
[
'location'
]
as
String
?
??
''
,
status:
json
[
'status'
]
as
String
?
??
''
,
time:
json
[
'time'
]
as
String
?
??
''
,
statusColor:
json
[
'statusColor'
]
as
String
?
??
'#1ABC9C'
,
);
}
@override
List
<
Object
?>
get
props
=>
[
id
,
name
,
type
,
location
,
status
,
time
,
statusColor
];
}
class
InspectionRecordBO
extends
Equatable
{
final
String
deviceName
;
final
String
inspectorName
;
final
String
inspectionTime
;
final
String
result
;
final
String
remark
;
final
List
<
InspectionItemBO
>
items
;
const
InspectionRecordBO
({
required
this
.
deviceName
,
required
this
.
inspectorName
,
required
this
.
inspectionTime
,
required
this
.
result
,
required
this
.
remark
,
required
this
.
items
,
});
factory
InspectionRecordBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
InspectionRecordBO
(
deviceName:
json
[
'deviceName'
]
as
String
?
??
''
,
inspectorName:
json
[
'inspectorName'
]
as
String
?
??
''
,
inspectionTime:
json
[
'inspectionTime'
]
as
String
?
??
''
,
result:
json
[
'result'
]
as
String
?
??
''
,
remark:
json
[
'remark'
]
as
String
?
??
''
,
items:
(
json
[
'items'
]
as
List
<
dynamic
>?)
?.
map
((
e
)
=>
InspectionItemBO
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
.
toList
()
??
[],
);
}
@override
List
<
Object
?>
get
props
=>
[
deviceName
,
inspectorName
,
inspectionTime
,
result
,
remark
,
items
];
}
class
InspectionItemBO
extends
Equatable
{
final
String
name
;
final
String
status
;
final
String
value
;
const
InspectionItemBO
({
required
this
.
name
,
required
this
.
status
,
required
this
.
value
,
});
factory
InspectionItemBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
InspectionItemBO
(
name:
json
[
'name'
]
as
String
?
??
''
,
status:
json
[
'status'
]
as
String
?
??
''
,
value:
json
[
'value'
]
as
String
?
??
''
,
);
}
@override
List
<
Object
?>
get
props
=>
[
name
,
status
,
value
];
}
```
#### topology_bo.dart
```
dart
import
'package:equatable/equatable.dart'
;
class
TopologyNodeBO
extends
Equatable
{
final
String
id
;
final
String
name
;
final
String
status
;
final
List
<
TopologyNodeBO
>
children
;
final
double
x
;
final
double
y
;
const
TopologyNodeBO
({
required
this
.
id
,
required
this
.
name
,
required
this
.
status
,
required
this
.
children
,
required
this
.
x
,
required
this
.
y
,
});
factory
TopologyNodeBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
TopologyNodeBO
(
id:
json
[
'id'
]
as
String
?
??
''
,
name:
json
[
'name'
]
as
String
?
??
''
,
status:
json
[
'status'
]
as
String
?
??
''
,
children:
(
json
[
'children'
]
as
List
<
dynamic
>?)
?.
map
((
e
)
=>
TopologyNodeBO
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
.
toList
()
??
[],
x:
(
json
[
'x'
]
as
num
?)?.
toDouble
()
??
0
,
y:
(
json
[
'y'
]
as
num
?)?.
toDouble
()
??
0
,
);
}
@override
List
<
Object
?>
get
props
=>
[
id
,
name
,
status
,
children
,
x
,
y
];
}
```
#### energy_bo.dart
```
dart
import
'package:equatable/equatable.dart'
;
class
EnergyOverviewBO
extends
Equatable
{
final
double
todayEnergy
;
final
double
todayEnergyChange
;
final
double
monthEnergy
;
final
double
monthEnergyChange
;
final
double
monthBill
;
final
List
<
double
>
hourlyData
;
final
List
<
ZoneDataBO
>
zoneData
;
const
EnergyOverviewBO
({
required
this
.
todayEnergy
,
required
this
.
todayEnergyChange
,
required
this
.
monthEnergy
,
required
this
.
monthEnergyChange
,
required
this
.
monthBill
,
required
this
.
hourlyData
,
required
this
.
zoneData
,
});
factory
EnergyOverviewBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
EnergyOverviewBO
(
todayEnergy:
(
json
[
'todayEnergy'
]
as
num
?)?.
toDouble
()
??
0
,
todayEnergyChange:
(
json
[
'todayEnergyChange'
]
as
num
?)?.
toDouble
()
??
0
,
monthEnergy:
(
json
[
'monthEnergy'
]
as
num
?)?.
toDouble
()
??
0
,
monthEnergyChange:
(
json
[
'monthEnergyChange'
]
as
num
?)?.
toDouble
()
??
0
,
monthBill:
(
json
[
'monthBill'
]
as
num
?)?.
toDouble
()
??
0
,
hourlyData:
(
json
[
'hourlyData'
]
as
List
<
dynamic
>?)?.
map
((
e
)
=>
(
e
as
num
).
toDouble
()).
toList
()
??
[],
zoneData:
(
json
[
'zoneData'
]
as
List
<
dynamic
>?)
?.
map
((
e
)
=>
ZoneDataBO
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
.
toList
()
??
[],
);
}
@override
List
<
Object
?>
get
props
=>
[
todayEnergy
,
todayEnergyChange
,
monthEnergy
,
monthEnergyChange
,
monthBill
,
hourlyData
,
zoneData
];
}
class
ZoneDataBO
extends
Equatable
{
final
String
name
;
final
double
percentage
;
final
double
energy
;
const
ZoneDataBO
({
required
this
.
name
,
required
this
.
percentage
,
required
this
.
energy
});
factory
ZoneDataBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
ZoneDataBO
(
name:
json
[
'name'
]
as
String
?
??
''
,
percentage:
(
json
[
'percentage'
]
as
num
?)?.
toDouble
()
??
0
,
energy:
(
json
[
'energy'
]
as
num
?)?.
toDouble
()
??
0
,
);
}
@override
List
<
Object
?>
get
props
=>
[
name
,
percentage
,
energy
];
}
```
#### room_bo.dart
```
dart
import
'package:equatable/equatable.dart'
;
class
RoomBO
extends
Equatable
{
final
String
roomNumber
;
final
String
status
;
// "vacant" | "occupied" | "reserved" | "warning" | "temperature"
final
String
statusText
;
final
RoomDeviceStatusBO
deviceStatus
;
const
RoomBO
({
required
this
.
roomNumber
,
required
this
.
status
,
required
this
.
statusText
,
required
this
.
deviceStatus
,
});
factory
RoomBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
RoomBO
(
roomNumber:
json
[
'roomNumber'
]
as
String
?
??
''
,
status:
json
[
'status'
]
as
String
?
??
'vacant'
,
statusText:
json
[
'statusText'
]
as
String
?
??
''
,
deviceStatus:
RoomDeviceStatusBO
.
fromJson
(
json
[
'deviceStatus'
]
as
Map
<
String
,
dynamic
>?
??
{}),
);
}
@override
List
<
Object
?>
get
props
=>
[
roomNumber
,
status
,
statusText
,
deviceStatus
];
}
class
RoomDeviceStatusBO
extends
Equatable
{
final
bool
hasPerson
;
final
bool
powerOn
;
final
bool
wifiNormal
;
final
bool
acOn
;
final
String
acTemperature
;
final
bool
lightOn
;
final
bool
tvOn
;
final
bool
curtainOn
;
const
RoomDeviceStatusBO
({
this
.
hasPerson
=
true
,
this
.
powerOn
=
true
,
this
.
wifiNormal
=
true
,
this
.
acOn
=
false
,
this
.
acTemperature
=
'24'
,
this
.
lightOn
=
false
,
this
.
tvOn
=
false
,
this
.
curtainOn
=
false
,
});
factory
RoomDeviceStatusBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
RoomDeviceStatusBO
(
hasPerson:
json
[
'hasPerson'
]
as
bool
?
??
true
,
powerOn:
json
[
'powerOn'
]
as
bool
?
??
true
,
wifiNormal:
json
[
'wifiNormal'
]
as
bool
?
??
true
,
acOn:
json
[
'acOn'
]
as
bool
?
??
false
,
acTemperature:
json
[
'acTemperature'
]
as
String
?
??
'24'
,
lightOn:
json
[
'lightOn'
]
as
bool
?
??
false
,
tvOn:
json
[
'tvOn'
]
as
bool
?
??
false
,
curtainOn:
json
[
'curtainOn'
]
as
bool
?
??
false
,
);
}
@override
List
<
Object
?>
get
props
=>
[
hasPerson
,
powerOn
,
wifiNormal
,
acOn
,
acTemperature
,
lightOn
,
tvOn
,
curtainOn
];
}
class
RoomDeviceBO
extends
Equatable
{
final
String
name
;
final
String
iconType
;
// "power" | "ac" | "light" | "tv" | "curtain" | "wifi"
final
bool
isOn
;
final
String
?
statusText
;
const
RoomDeviceBO
({
required
this
.
name
,
required
this
.
iconType
,
this
.
isOn
=
false
,
this
.
statusText
,
});
factory
RoomDeviceBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
RoomDeviceBO
(
name:
json
[
'name'
]
as
String
?
??
''
,
iconType:
json
[
'iconType'
]
as
String
?
??
'power'
,
isOn:
json
[
'isOn'
]
as
bool
?
??
false
,
statusText:
json
[
'statusText'
]
as
String
?,
);
}
@override
List
<
Object
?>
get
props
=>
[
name
,
iconType
,
isOn
,
statusText
];
}
```
#### rule_bo.dart
```
dart
import
'package:equatable/equatable.dart'
;
class
RuleBO
extends
Equatable
{
final
String
id
;
final
String
name
;
final
String
description
;
final
bool
enabled
;
final
String
triggerCondition
;
final
String
action
;
final
String
iconType
;
// "thermostat" | "power" | "schedule" | ...
const
RuleBO
({
required
this
.
id
,
required
this
.
name
,
required
this
.
description
,
required
this
.
enabled
,
required
this
.
triggerCondition
,
required
this
.
action
,
required
this
.
iconType
,
});
factory
RuleBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
RuleBO
(
id:
json
[
'id'
]
as
String
?
??
''
,
name:
json
[
'name'
]
as
String
?
??
''
,
description:
json
[
'description'
]
as
String
?
??
''
,
enabled:
json
[
'enabled'
]
as
bool
?
??
false
,
triggerCondition:
json
[
'triggerCondition'
]
as
String
?
??
''
,
action:
json
[
'action'
]
as
String
?
??
''
,
iconType:
json
[
'iconType'
]
as
String
?
??
'power'
,
);
}
@override
List
<
Object
?>
get
props
=>
[
id
,
name
,
description
,
enabled
,
triggerCondition
,
action
,
iconType
];
}
```
---
## 3. API 接口清单
### 3.1 接口总览
| 序号 | 方法 | 路径 | 说明 | 所属模块 |
|------|------|------|------|---------|
| 1 | POST |
`/api/login`
| 登录 | 认证 |
| 2 | GET |
`/api/user/profile`
| 获取用户信息 | 个人中心 |
| 3 | POST |
`/api/logout`
| 退出登录 | 认证 |
| 4 | GET |
`/api/alarms`
| 告警列表 | 告警 |
| 5 | GET |
`/api/alarms/{id}`
| 告警详情 | 告警 |
| 6 | GET |
`/api/home/overview`
| 告警首页概览 | 告警 |
| 7 | GET |
`/api/devices/{id}`
| 设备详情 | 告警 |
| 8 | GET |
`/api/inspections`
| 巡检任务列表 | 巡检 |
| 9 | GET |
`/api/inspections/{id}/devices`
| 巡检设备列表 | 巡检 |
| 10 | GET |
`/api/inspections/devices/{id}/detail`
| 巡检设备详情 | 巡检 |
| 11 | GET |
`/api/inspections/devices/{id}/history`
| 巡检历史 | 巡检 |
| 12 | GET |
`/api/inspections/{id}/topology`
| 巡检拓扑 | 巡检 |
| 13 | GET |
`/api/service/overview`
| 管理首页概览 | 管理 |
| 14 | GET |
`/api/service/rooms`
| 房间列表 | 管理 |
| 15 | GET |
`/api/service/rooms/{roomNumber}`
| 房间详情 | 管理 |
| 16 | POST |
`/api/service/rooms/{roomNumber}/devices/control`
| 设备控制 | 管理 |
| 17 | GET |
`/api/reports/overview`
| 能耗总览 | 能耗 |
| 18 | GET |
`/api/reports/devices`
| 设备能耗列表 | 能耗 |
| 19 | GET |
`/api/reports/devices/{id}`
| 设备能耗详情 | 能耗 |
| 20 | GET |
`/api/reports/energy`
| 区域能耗 | 能耗 |
| 21 | GET |
`/api/reports/rooms`
| 房间能耗报告 | 能耗 |
| 22 | GET |
`/api/reports/rules`
| 规则列表 | 能耗 |
| 23 | POST |
`/api/reports/rules/{id}/toggle`
| 规则开关 | 能耗 |
### 3.2 接口详细定义
#### 认证模块
**POST /api/login**
```
请求体:
{
"username": "string",
"password": "string"
}
响应 data:
{
"token": "string",
"userInfo": {
"username": "string",
"phone": "string"
}
}
```
**GET /api/user/profile**
```
响应 data:
{
"phone": "string",
"welcomeText": "string"
}
```
#### 告警模块
**GET /api/home/overview**
- 告警首页概览
```
响应 data:
{
"highAlarmCount": 1,
"mediumAlarmCount": 1,
"onlineDeviceCount": 48,
"temperatureAlarm": {
"title": "温度异常告警",
"location": "客房301·A-301智能空开",
"currentTemperature": "85",
"currentVoltage": "220",
"currentCurrent": "3.2",
"waitTime": "2h37m",
"alarmLevel": "紧急"
},
"voltageAlarm": {
"title": "电压波动告警",
"location": "会议室2F-01",
"voltageValue": "248",
"normalVoltage": "235",
"waitTime": "1h19m",
"alarmLevel": "中警"
},
"equipmentList": [
{
"title": "客房305空开",
"subtitle": "38°C·220V·520W",
"status": "正常",
"statusColor": "#1ABC9C"
}
]
}
```
**GET /api/alarms**
- 告警列表
```
查询参数:
?status=pending|processed|falseAlarm (可选,默认全部)
响应 data:
[
{
"id": "string",
"title": "string",
"deviceInfo": "string",
"alarmTime": "string",
"processTime": "string",
"status": "pending|processed|falseAlarm",
"level": "高警|中警|低警",
"urgency": "紧急|一般"
}
]
```
**GET /api/alarms/{id}**
- 告警详情
```
响应 data:
{
"alarmType": "string",
"deviceInfo": "string",
"alarmLevel": "string",
"urgency": "string",
"currentTemp": "string",
"voltage": "string",
"current": "string",
"triggerTime": "string",
"threshold": "string",
"duration": "string",
"peak": "string",
"linkAction": "string",
"date": "string",
"temperatureSpots": [
{ "x": 0, "y": 40 },
{ "x": 4, "y": 40 }
]
}
```
**GET /api/devices/{id}**
- 设备详情
```
响应 data:
{
"deviceName": "string",
"deviceId": "string",
"location": "string",
"status": "string",
"voltage": "string",
"current": "string",
"power": "string",
"temperature": "string",
"powerData": [100, 200, 150],
"tempData": [30, 35, 40]
}
```
#### 巡检模块
**GET /api/inspections**
- 巡检任务列表
```
响应 data: 与页面现有 inspection 模块数据一致
```
**GET /api/inspections/{id}/devices**
- 巡检设备列表
```
查询参数:
?status=total|on|off|offline|alarm|fault
?rooms=101,102
响应 data:
[
{
"id": "string",
"name": "string",
"type": "string",
"location": "string",
"status": "string",
"time": "string",
"statusColor": "string"
}
]
```
**GET /api/inspections/devices/{id}/detail**
- 巡检设备详情
```
响应 data:
{
"deviceId": "string",
"deviceName": "string",
"deviceType": "string",
"deviceStatus": "string",
"deviceModel": "string",
"installLocation": "string",
"area": "string",
"responsiblePerson": "string",
"lastInspectionTime": "string",
"inspectionHistory": [...],
"inspectionItems": [...]
}
```
**GET /api/inspections/devices/{id}/history**
- 巡检历史
```
响应 data:
{
"totalCount": 20,
"passCount": 15,
"warningCount": 3,
"failCount": 2,
"records": [
{
"deviceName": "string",
"inspectorName": "string",
"inspectionTime": "string",
"result": "string",
"remark": "string",
"items": [
{ "name": "string", "status": "string", "value": "string" }
]
}
]
}
```
**GET /api/inspections/{id}/topology**
- 巡检拓扑
```
响应 data:
{
"deviceName": "string",
"deviceModel": "string",
"deviceCount": 10,
"connectionCount": 11,
"topologyNodes": [
{
"id": "string",
"name": "string",
"status": "string",
"x": 0.0,
"y": 0.0,
"children": [...]
}
]
}
```
#### 管理模块
**GET /api/service/overview**
- 管理首页概览
```
响应 data:
{
"pendingCount": 2,
"occupiedRooms": 45,
"vacantRooms": 7,
"floors": ["1楼", "2楼", "3楼", "4楼", "5楼", "6楼"]
}
```
**GET /api/service/rooms**
- 房间列表
```
查询参数:
?floor=1楼
?type=occupied|vacant (可选)
?search=302 (可选)
响应 data:
[
{
"roomNumber": "302",
"status": "occupied",
"statusText": "入住中",
"deviceStatus": {
"hasPerson": true,
"powerOn": true,
"wifiNormal": true,
"acOn": true,
"acTemperature": "24",
"lightOn": false,
"tvOn": false,
"curtainOn": false
}
}
]
```
**GET /api/service/rooms/{roomNumber}**
- 房间详情
```
响应 data:
{
"roomNumber": "302",
"roomStatus": "入住中",
"statusTags": ["hasPerson", "powerOn", "wifiNormal"],
"roomDevices": [
{ "name": "空调", "iconType": "ac", "isOn": true, "statusText": "制冷 24°C" }
],
"mainPowerOn": true,
"controlDevices": [
{ "name": "空调", "iconType": "ac", "isOn": true, "detailText": "制冷 24°C" },
{ "name": "灯光", "iconType": "light", "isOn": false, "detailText": null }
]
}
```
**POST /api/service/rooms/{roomNumber}/devices/control**
- 设备控制
```
请求体:
{
"deviceName": "空调",
"action": "toggle|setTemperature|setSpeed|setMode",
"value": "string|number"
}
响应 data:
{
"success": true
}
```
#### 能耗模块
**GET /api/reports/overview**
- 能耗总览
```
响应 data:
{
"reportHeader": {
"title": "string",
"subtitle": "string"
},
"reportMetrics": {
"powerUsage": { "value": 1284, "unit": "kWh", "change": -8, "changeText": "↓ 8%", "progress": 0.6 },
"pendingAlerts": { "value": 3, "unit": "条", "change": null, "changeText": "需关注", "progress": 0.3 }
},
"weeklyPowerUsage": {
"unit": "kWh",
"dateRange": "string",
"data": [
{ "day": "一", "value": 120 }
]
},
"roomStatusDistribution": {
"totalRooms": 52,
"statuses": [
{ "name": "入住中", "count": 45, "color": "#3B82F6" }
]
}
}
```
**GET /api/reports/devices**
- 设备能耗列表
```
查询参数:
?type=all|smartBreaker|guestControl|network
响应 data:
[
{
"id": "string",
"name": "string",
"room": "string",
"type": "smartBreaker|guestControl|network",
"online": true,
"status": "string",
"lastUpdate": "string"
}
]
```
**GET /api/reports/devices/{id}**
- 设备能耗详情
```
响应 data: 与设备能耗详情页字段一致
```
**GET /api/reports/energy**
- 区域能耗
```
响应 data:
{
"todayEnergy": 1284,
"todayEnergyChange": -8,
"monthEnergy": 38420,
"monthEnergyChange": -5,
"monthBill": 6982,
"hourlyData": [45, 38, 32, ...],
"zoneData": [
{ "name": "客房区域", "percentage": 68, "energy": 873 }
]
}
```
**GET /api/reports/rooms**
- 房间能耗报告
```
查询参数:
?floor=1
响应 data:
{
"floors": ["1楼", "2楼", "3楼"],
"selectedFloor": "1楼",
"rooms": [
{
"roomNumber": "101",
"status": "occupied",
"statusText": "入住中",
"deviceStatus": { ... }
}
]
}
```
**GET /api/reports/rules**
- 规则列表
```
响应 data:
[
{
"id": "string",
"name": "string",
"description": "string",
"enabled": true,
"triggerCondition": "string",
"action": "string",
"iconType": "string"
}
]
```
**POST /api/reports/rules/{id}/toggle**
- 规则开关
```
请求体:
{
"enabled": true
}
响应 data:
{
"success": true
}
```
---
## 4. 需要创建的文件清单
### 4.1 BO 模型文件(7 个)
| 文件 | 路径 |
|------|------|
|
`alarm_bo.dart`
|
`lib/models/bo/alarm_bo.dart`
|
|
`device_bo.dart`
|
`lib/models/bo/device_bo.dart`
|
|
`inspection_bo.dart`
|
`lib/models/bo/inspection_bo.dart`
|
|
`topology_bo.dart`
|
`lib/models/bo/topology_bo.dart`
|
|
`energy_bo.dart`
|
`lib/models/bo/energy_bo.dart`
|
|
`room_bo.dart`
|
`lib/models/bo/room_bo.dart`
|
|
`rule_bo.dart`
|
`lib/models/bo/rule_bo.dart`
|
### 4.2 Repository 文件(按模块,约 10 个)
| 文件 | 对应接口 |
|------|---------|
|
`alarm_repository.dart`
| #4, #5, #6 |
|
`device_repository.dart`
| #7 |
|
`inspection_repository.dart`
| #8, #9, #10, #11, #12 |
|
`service_repository.dart`
| #13, #14, #15, #16 |
|
`report_repository.dart`
| #17, #18, #19, #20, #21, #22, #23 |
|
`user_repository.dart`
| #2, #3 |
### 4.3 Service 文件(按模块,约 10 个)
与 Repository 一一对应,封装业务逻辑。
### 4.4 需要修改的 Cubit 文件(15 个)
| Cubit 文件 | 改造内容 |
|-----------|---------|
|
`home_index_cubit.dart`
| 构造函数注入
`HomeService`
,
`initData()`
改为调用 API |
|
`abnormal_list_cubit.dart`
| 注入
`AlarmService`
,加载告警列表 |
|
`abnormal_cubit.dart`
| 注入
`AlarmService`
,通过路由参数加载告警详情 |
|
`device_cubit.dart`
| 注入
`DeviceService`
,加载设备详情 |
|
`inspection_cubit.dart`
| 注入
`InspectionService`
,加载巡检任务 |
|
`inspection_device_cubit.dart`
| 注入
`InspectionService`
,加载设备巡检详情 |
|
`inspection_history_cubit.dart`
| 注入
`InspectionService`
,加载巡检历史 |
|
`inspection_topology_cubit.dart`
| 注入
`InspectionService`
,加载拓扑数据 |
|
`service_index_cubit.dart`
| 注入
`ServiceRoomService`
,加载管理首页 |
|
`service_room_cubit.dart`
| 注入
`ServiceRoomService`
,加载房间详情 |
|
`service_device_cubit.dart`
| 注入
`ServiceRoomService`
,设备控制 |
|
`report_index_cubit.dart`
| 注入
`ReportService`
,加载能耗总览 |
|
`device_list_cubit.dart`
| 注入
`ReportService`
,加载设备能耗列表 |
|
`energy_cubit.dart`
| 注入
`ReportService`
,加载区域能耗 |
|
`room_report_cubit.dart`
| 注入
`ReportService`
,加载房间能耗 |
|
`rule_management_cubit.dart`
| 注入
`ReportService`
,加载/切换规则 |
|
`profile_cubit.dart`
| 注入
`UserService`
,加载用户信息 |
---
## 5. AI 自动对接指南
### 5.1 AI 可以自动完成的操作
1.
**创建 BO 模型文件**
:根据本文档第 2 节的 BO 定义,直接复制代码到对应文件
2.
**创建 Repository 文件**
:根据本文档第 3 节的接口定义,按模板生成
3.
**创建 Service 文件**
:根据 Repository 方法,按模板生成
4.
**修改 Cubit 文件**
:将
`initData()`
mock 替换为 Service 调用,添加
`isLoading/error`
状态管理
5.
**修改 View 文件**
:在
`BlocProvider`
中注入 Service 依赖(从
`create: (_) => XxxCubit()`
改为
`create: (_) => XxxCubit(service: XxxService(...))`
)
### 5.2 需要人工确认/介入的操作
| 事项 | 原因 | 介入方式 |
|------|------|---------|
|
**API 地址确认**
|
`constants.dart`
中的
`baseUrl`
需要替换为真实后端地址 | 人工修改配置文件 |
|
**接口路径确认**
| 本文档定义的接口路径(如
`/api/alarms`
)是约定,实际后端路径可能不同 | 对照后端 Swagger/YApi 文档核对 |
|
**字段名映射**
| JSON 字段名可能与本文档定义不一致(如后端用
`device_info`
而非
`deviceInfo`
) | 对照后端文档调整
`fromJson`
|
|
**数据类型差异**
| 数值类型可能需要调整(int/double/String) | 对照后端文档调整 BO 字段类型 |
|
**鉴权调试**
| Token 传递、401 处理逻辑是否正确 | 登录后测试接口调用 |
|
**分页逻辑**
| 当前页面未设计分页,如果后端数据量大需要补充分页 | 评估是否需要加载更多/下拉刷新 |
|
**错误文案**
| 后端返回的错误信息可能需要前端映射 | 检查
`msg`
字段是否适合直接展示 |
### 5.3 对接执行顺序(建议)
```
第一阶段:基础设施
1. 创建所有 BO 模型文件
2. 创建所有 Repository 文件
3. 创建所有 Service 文件
第二阶段:逐模块改造
4. 改造认证模块(已在 main.dart 中注入,无需修改)
5. 改造告警模块(4 个 Cubit)
6. 改造巡检模块(4 个 Cubit)
7. 改造管理模块(3 个 Cubit)
8. 改造能耗模块(5 个 Cubit)
9. 改造个人中心(1 个 Cubit)
第三阶段:联调验证
10. 修改 main.dart 注入全局 Service(如需要)
11. 逐页面运行验证
```
---
## 6. 注意事项
1.
**Cubit 构造函数变更会破坏 View 层**
:所有
`BlocProvider(create: (_) => XxxCubit())`
需要改为
`BlocProvider(create: (_) => XxxCubit(service: xxxService))`
,AI 需同步修改 View 文件。
2.
**State 中不需要 `isLoading/error` 的页面**
:部分页面(如
`abnormal_cubit.dart`
)的 State 没有
`isLoading`
字段,需要先添加,否则 loading 状态无法管理。
3.
**路由参数传递**
:某些页面通过路由参数获取 id(如
`AbnormalDetailRoute(id: xxx)`
),Cubit 需要通过构造函数接收该 id 以调用 API。
4.
**`Equatable` 缺失**
:部分 State 类(如
`AbnormalListState`
、
`DeviceState`
)没有
`extends Equatable`
,需要补充。
\ No newline at end of file
测试方案.md
0 → 100644
View file @
76a57f12
# 智慧酒店 App 自动化测试方案
# 智慧酒店 App 自动化测试方案
## 1. 测试策略总览
### 1.1 测试金字塔
```
┌──────────┐
│ 集成测试 │ ← 少量,覆盖核心流程(登录→首页→详情)
├──────────┤
│ Widget测试│ ← 中等,每个页面至少 1 个冒烟测试
├──────────┤
│ 单元测试 │ ← 大量,Cubit/Service/Repository/BO 全覆盖
└──────────┘
```
### 1.2 测试范围
| 测试类型 | 覆盖目标 | 工具 | 优先级 |
|---------|---------|------|--------|
| 单元测试 - BO |
`fromJson`
/
`toJson`
正确性 |
`flutter_test`
| 高 |
| 单元测试 - Repository | API 调用正确、参数传递 |
`flutter_test`
+
`mockito`
| 高 |
| 单元测试 - Service | 业务逻辑正确、异常处理 |
`flutter_test`
+
`mockito`
| 高 |
| 单元测试 - Cubit | 状态流转正确 |
`flutter_bloc_test`
+
`mockito`
| 高 |
| Widget 测试 | 页面渲染、交互行为 |
`flutter_test`
| 中 |
| 集成测试 | 端到端流程 |
`integration_test`
| 低 |
### 1.3 依赖配置
在
`pubspec.yaml`
的
`dev_dependencies`
中添加:
```
yaml
dev_dependencies
:
flutter_test
:
sdk
:
flutter
bloc_test
:
^9.1.7
mockito
:
^5.4.4
build_runner
:
^2.4.8
integration_test
:
sdk
:
flutter
```
运行
`flutter pub get`
安装。
---
## 2. 测试目录结构
```
test/
├── unit/ # 单元测试
│ ├── models/ # BO 模型测试
│ │ ├── alarm_bo_test.dart
│ │ ├── device_bo_test.dart
│ │ ├── inspection_bo_test.dart
│ │ ├── topology_bo_test.dart
│ │ ├── energy_bo_test.dart
│ │ ├── room_bo_test.dart
│ │ └── rule_bo_test.dart
│ ├── repositories/ # Repository 测试
│ │ ├── alarm_repository_test.dart
│ │ ├── device_repository_test.dart
│ │ ├── inspection_repository_test.dart
│ │ ├── service_repository_test.dart
│ │ └── report_repository_test.dart
│ ├── services/ # Service 测试
│ │ ├── alarm_service_test.dart
│ │ ├── device_service_test.dart
│ │ ├── inspection_service_test.dart
│ │ ├── service_room_service_test.dart
│ │ └── report_service_test.dart
│ └── cubits/ # Cubit 测试
│ ├── home_index_cubit_test.dart
│ ├── abnormal_list_cubit_test.dart
│ ├── abnormal_cubit_test.dart
│ ├── device_cubit_test.dart
│ ├── inspection_cubit_test.dart
│ ├── inspection_device_cubit_test.dart
│ ├── inspection_history_cubit_test.dart
│ ├── inspection_topology_cubit_test.dart
│ ├── service_index_cubit_test.dart
│ ├── service_room_cubit_test.dart
│ ├── service_device_cubit_test.dart
│ ├── report_index_cubit_test.dart
│ ├── device_list_cubit_test.dart
│ ├── energy_cubit_test.dart
│ ├── room_report_cubit_test.dart
│ ├── rule_management_cubit_test.dart
│ └── profile_cubit_test.dart
├── widget/ # Widget 测试
│ ├── login_view_test.dart
│ ├── home_index_view_test.dart
│ ├── abnormal_list_view_test.dart
│ └── ... # 每个页面一个
└── integration/ # 集成测试
└── app_test.dart
```
---
## 3. 单元测试规范
### 3.1 BO 模型测试
**目的**
:验证 JSON 反序列化正确性
```
dart
// test/unit/models/alarm_bo_test.dart
import
'package:flutter_test/flutter_test.dart'
;
import
'package:smart_hotel_app/models/bo/alarm_bo.dart'
;
void
main
(
)
{
group
(
'AlarmItemBO'
,
()
{
test
(
'fromJson should parse correctly'
,
()
{
final
json
=
{
'id'
:
'1'
,
'title'
:
'温度异常告警'
,
'deviceInfo'
:
'A-301智能空开'
,
'alarmTime'
:
'2026-05-09 14:23:18'
,
'processTime'
:
'2026-05-09 15:00:00'
,
'status'
:
'pending'
,
'level'
:
'高警'
,
'urgency'
:
'紧急'
,
};
final
bo
=
AlarmItemBO
.
fromJson
(
json
);
expect
(
bo
.
id
,
'1'
);
expect
(
bo
.
title
,
'温度异常告警'
);
expect
(
bo
.
deviceInfo
,
'A-301智能空开'
);
expect
(
bo
.
status
,
'pending'
);
expect
(
bo
.
level
,
'高警'
);
});
test
(
'fromJson should handle missing fields'
,
()
{
final
json
=
<
String
,
dynamic
>{};
final
bo
=
AlarmItemBO
.
fromJson
(
json
);
expect
(
bo
.
id
,
''
);
expect
(
bo
.
title
,
''
);
expect
(
bo
.
status
,
'pending'
);
// 默认值
});
test
(
'fromJson should handle null list fields'
,
()
{
final
json
=
{
'temperatureSpots'
:
null
,
'alarmType'
:
'test'
,
'deviceInfo'
:
'test'
,
// ... 其他必填字段
};
// 验证不抛异常
expect
(()
=>
AlarmDetailBO
.
fromJson
(
json
),
returnsNormally
);
});
});
}
```
**AI 可自动化**
:是。给定 JSON 示例和 BO 定义,AI 可自动生成所有字段的断言。
### 3.2 Repository 测试
**目的**
:验证 API 调用参数正确、fromJsonT 回调正确
```
dart
// test/unit/repositories/alarm_repository_test.dart
import
'package:flutter_test/flutter_test.dart'
;
import
'package:mockito/mockito.dart'
;
import
'package:mockito/annotations.dart'
;
import
'package:dio/dio.dart'
;
import
'package:smart_hotel_app/http/dio_request.dart'
;
import
'package:smart_hotel_app/http/response_model.dart'
;
import
'package:smart_hotel_app/repositories/alarm_repository.dart'
;
import
'package:smart_hotel_app/models/bo/alarm_bo.dart'
;
// 生成 Mock 类
// 运行: flutter pub run build_runner build
@GenerateMocks
([
DioRequest
])
import
'alarm_repository_test.mocks.dart'
;
void
main
(
)
{
late
MockDioRequest
mockDio
;
late
AlarmRepository
repository
;
setUp
(()
{
mockDio
=
MockDioRequest
();
repository
=
AlarmRepository
();
});
group
(
'getAlarms'
,
()
{
test
(
'should return list of AlarmItemBO on success'
,
()
async
{
// 注意:由于 DioRequest 是单例,实际测试时需要重构为可注入
// 或者使用 mock HTTP 拦截器
// 见下文 7.1 节
});
});
}
```
> **重要**:当前 `DioRequest` 是单例模式,Repository 直接调用 `DioRequest.instance`,无法 mock。建议在对接时同时将 Repository 改为接收 `DioRequest` 实例(依赖注入),或者使用 Dio 的 `HttpClientAdapter` 进行 mock。
### 3.3 Service 测试
**目的**
:验证业务逻辑正确、异常处理
```
dart
// test/unit/services/alarm_service_test.dart
import
'package:flutter_test/flutter_test.dart'
;
import
'package:mockito/mockito.dart'
;
import
'package:mockito/annotations.dart'
;
import
'package:smart_hotel_app/services/alarm_service.dart'
;
import
'package:smart_hotel_app/repositories/alarm_repository.dart'
;
import
'package:smart_hotel_app/models/bo/alarm_bo.dart'
;
import
'package:smart_hotel_app/http/response_model.dart'
;
@GenerateMocks
([
AlarmRepository
])
import
'alarm_service_test.mocks.dart'
;
void
main
(
)
{
late
MockAlarmRepository
mockRepository
;
late
AlarmService
service
;
setUp
(()
{
mockRepository
=
MockAlarmRepository
();
service
=
AlarmService
(
repository:
mockRepository
);
});
group
(
'getAlarms'
,
()
{
final
mockAlarms
=
[
AlarmItemBO
(
id:
'1'
,
title:
'告警1'
,
deviceInfo:
'设备A'
,
alarmTime:
''
,
processTime:
''
,
status:
'pending'
,
level:
'高警'
,
urgency:
'紧急'
,
),
];
test
(
'should return alarms on success'
,
()
async
{
when
(
mockRepository
.
getAlarms
()).
thenAnswer
(
(
_
)
async
=>
ResponseModel
<
List
<
AlarmItemBO
>>(
code:
200
,
msg:
'success'
,
data:
mockAlarms
,
uuid:
'test'
,
success:
true
,
timestamp:
0
,
),
);
final
result
=
await
service
.
getAlarms
();
expect
(
result
,
mockAlarms
);
verify
(
mockRepository
.
getAlarms
()).
called
(
1
);
});
test
(
'should throw on failure'
,
()
async
{
when
(
mockRepository
.
getAlarms
()).
thenAnswer
(
(
_
)
async
=>
ResponseModel
<
List
<
AlarmItemBO
>>(
code:
500
,
msg:
'服务器错误'
,
data:
null
,
uuid:
'test'
,
success:
false
,
timestamp:
0
,
),
);
expect
(()
=>
service
.
getAlarms
(),
throwsException
);
});
});
}
```
**AI 可自动化**
:是。Service 测试模式固定,AI 可按模板批量生成。
### 3.4 Cubit 测试(使用 bloc_test)
**目的**
:验证状态流转正确
```
dart
// test/unit/cubits/home_index_cubit_test.dart
import
'package:flutter_test/flutter_test.dart'
;
import
'package:bloc_test/bloc_test.dart'
;
import
'package:mockito/mockito.dart'
;
import
'package:mockito/annotations.dart'
;
import
'package:smart_hotel_app/views/home/index/cubit/home_index_cubit.dart'
;
import
'package:smart_hotel_app/views/home/index/cubit/home_index_state.dart'
;
import
'package:smart_hotel_app/services/home_service.dart'
;
@GenerateMocks
([
HomeService
])
import
'home_index_cubit_test.mocks.dart'
;
void
main
(
)
{
late
MockHomeService
mockService
;
setUp
(()
{
mockService
=
MockHomeService
();
});
group
(
'HomeIndexCubit'
,
()
{
blocTest
<
HomeIndexCubit
,
HomeIndexState
>(
'should emit [loading, loaded] when loadData succeeds'
,
build:
()
=>
HomeIndexCubit
(
service:
mockService
),
act:
(
cubit
)
{
// loadData() 在构造函数中自动调用
// 或手动调用 cubit.loadData()
},
expect:
()
=>
[
isA
<
HomeIndexState
>().
having
((
s
)
=>
s
.
isLoading
,
'isLoading'
,
true
),
isA
<
HomeIndexState
>()
.
having
((
s
)
=>
s
.
isLoading
,
'isLoading'
,
false
)
.
having
((
s
)
=>
s
.
highAlarmCount
,
'highAlarmCount'
,
isPositive
),
],
);
blocTest
<
HomeIndexCubit
,
HomeIndexState
>(
'should emit [loading, error] when loadData fails'
,
build:
()
{
when
(
mockService
.
getOverview
()).
thenThrow
(
Exception
(
'网络错误'
));
return
HomeIndexCubit
(
service:
mockService
);
},
expect:
()
=>
[
isA
<
HomeIndexState
>().
having
((
s
)
=>
s
.
isLoading
,
'isLoading'
,
true
),
isA
<
HomeIndexState
>()
.
having
((
s
)
=>
s
.
isLoading
,
'isLoading'
,
false
)
.
having
((
s
)
=>
s
.
error
,
'error'
,
isNotNull
),
],
);
});
}
```
**AI 可自动化**
:是。每个 Cubit 的测试模式相同(loading → success/error),AI 可批量生成。
---
## 4. Widget 测试规范
### 4.1 冒烟测试(每个页面至少 1 个)
**目的**
:验证页面能正常渲染,不抛异常
```
dart
// test/widget/home_index_view_test.dart
import
'package:flutter_test/flutter_test.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:smart_hotel_app/views/home/index/index_view.dart'
;
import
'package:smart_hotel_app/views/home/index/cubit/home_index_cubit.dart'
;
import
'package:smart_hotel_app/views/home/index/cubit/home_index_state.dart'
;
void
main
(
)
{
testWidgets
(
'HomeView should render without error'
,
(
tester
)
async
{
await
tester
.
pumpWidget
(
MaterialApp
(
home:
BlocProvider
<
HomeIndexCubit
>(
create:
(
_
)
=>
HomeIndexCubit
(),
child:
const
HomeView
(),
),
),
);
// 等待初始化完成
await
tester
.
pumpAndSettle
();
// 验证页面关键元素存在
expect
(
find
.
byType
(
HomeView
),
findsOneWidget
);
});
}
```
**AI 可自动化**
:是。每个页面一个冒烟测试,模式固定。
### 4.2 交互测试(关键页面)
```
dart
testWidgets
(
'tapping alarm item should navigate to detail'
,
(
tester
)
async
{
// 需要 mock 路由
await
tester
.
pumpWidget
(
MaterialApp
(
home:
BlocProvider
<
AbnormalListCubit
>(
create:
(
_
)
=>
AbnormalListCubit
(
service:
mockService
),
child:
const
AbnormalListView
(),
),
),
);
await
tester
.
pumpAndSettle
();
await
tester
.
tap
(
find
.
text
(
'温度异常告警'
));
await
tester
.
pumpAndSettle
();
// 验证导航到详情页
expect
(
find
.
byType
(
AbnormalDetailView
),
findsOneWidget
);
});
```
**AI 可自动化**
:需要人工提供交互路径描述,AI 可生成测试代码。
---
## 5. 集成测试
### 5.1 核心流程测试
```
dart
// test/integration/app_test.dart
import
'package:flutter_test/flutter_test.dart'
;
import
'package:integration_test/integration_test.dart'
;
void
main
(
)
{
IntegrationTestWidgetsFlutterBinding
.
ensureInitialized
();
testWidgets
(
'full login flow'
,
(
tester
)
async
{
// 启动 App
await
tester
.
pumpWidget
(
const
MyApp
());
await
tester
.
pumpAndSettle
();
// 验证在登录页
expect
(
find
.
text
(
'登录'
),
findsOneWidget
);
// 输入用户名密码
await
tester
.
enterText
(
find
.
byType
(
TextFormField
).
first
,
'admin'
);
await
tester
.
enterText
(
find
.
byType
(
TextFormField
).
last
,
'123456'
);
await
tester
.
tap
(
find
.
text
(
'登录'
));
await
tester
.
pumpAndSettle
();
// 验证跳转到首页
expect
(
find
.
text
(
'告警'
),
findsOneWidget
);
});
}
```
**AI 可自动化**
:需要人工描述流程步骤,AI 可生成代码。
**但集成测试需要在真机/模拟器上运行,AI 无法自动执行验证。**
---
## 6. AI 自动化能力评估
### 6.1 AI 可完全自动化
| 测试类型 | 自动化程度 | 说明 |
|---------|-----------|------|
| BO 模型
`fromJson`
测试 | 100% | 给定 JSON 样本和 BO 定义,AI 可生成全字段断言 |
| BO 模型
`null`
安全测试 | 100% | 验证缺失字段/空列表不抛异常 |
| Service 测试(成功/失败) | 100% | 模式固定,mock Repository 返回成功/失败 |
| Cubit 状态流转测试 | 100% | 使用
`blocTest`
,模式固定 |
| Widget 冒烟测试 | 100% | 每个页面验证能渲染,模式固定 |
### 6.2 AI 需要人工辅助
| 测试类型 | 需人工介入 | 原因 |
|---------|-----------|------|
| Repository 测试 | 需要 mock DioRequest 方案 | 当前 DioRequest 是单例,需先改造为可注入 |
| Widget 交互测试 | 需要描述交互路径 | AI 不知道哪个按钮做什么操作 |
| 集成测试 | 需要描述完整流程 | AI 不知道业务操作步骤 |
| 测试验证 | 需要人工执行 | AI 无法在真机上运行测试 |
### 6.3 需要人工执行的操作
| 操作 | 原因 |
|------|------|
|
`flutter test`
运行 | AI 无法执行终端命令并观察结果 |
|
`flutter run`
真机验证 | 需要在真机上观察 UI 渲染 |
| 测试失败后的排查 | 需要分析失败原因,可能是测试代码问题或业务代码 bug |
| 覆盖率报告分析 | 需要判断哪些分支未覆盖 |
| mock 数据维护 | 接口变更时需要更新 mock JSON |
---
## 7. 关键注意事项
### 7.1 DioRequest 单例导致 Repository 不可测试
**问题**
:
`DioRequest`
是单例,Repository 直接调用
`DioRequest.instance`
,无法注入 mock。
**解决方案**
(在对按时一并改造):
```
dart
// 方案 A:Repository 接收 DioRequest 实例(推荐)
class
AlarmRepository
{
final
DioRequest
_dio
;
AlarmRepository
({
DioRequest
?
dio
})
:
_dio
=
dio
??
DioRequest
.
instance
;
Future
<
ResponseModel
<
List
<
AlarmItemBO
>>>
getAlarms
()
{
return
_dio
.
get
<
List
<
AlarmItemBO
>>(
'/api/alarms'
,
...);
}
}
// 方案 B:使用 Dio HttpClientAdapter mock
// 通过 Dio.httpClientAdapter 注入 mock HTTP 客户端
```
**建议在对按时统一采用方案 A**
,确保所有 Repository 可测试。
### 7.2 State 缺少 isLoading/error 字段
部分页面 State 没有
`isLoading`
和
`error`
字段(如
`AbnormalState`
、
`DeviceState`
),需要在对按时一并添加:
```
dart
class
AbnormalState
extends
Equatable
{
final
AlarmDetailBO
?
alarmInfo
;
final
bool
isLoading
;
final
String
?
error
;
// ... copyWith, props
}
```
### 7.3 测试执行命令
```
bash
# 运行所有单元测试
flutter
test test
/unit/
# 运行指定模块测试
flutter
test test
/unit/cubits/
# 运行 widget 测试
flutter
test test
/widget/
# 运行集成测试
flutter
test
integration_test/
# 生成覆盖率报告
flutter
test
--coverage
genhtml coverage/lcov.info
-o
coverage/html
# 生成 mock 类
flutter pub run build_runner build
--delete-conflicting-outputs
```
---
## 8. 测试执行建议
### 8.1 分阶段执行
```
阶段 1:BO 模型测试(对接前)
- 创建 BO 文件后立即运行,验证 fromJson 正确
- 不需要 mock,可独立运行
阶段 2:Service + Repository 测试(对接中)
- 创建 Service/Repository 后运行
- 需要 mock Repository
阶段 3:Cubit 测试(对接中)
- 改造 Cubit 后运行
- 需要 mock Service
阶段 4:Widget 冒烟测试(对接后)
- 每个页面改造完成后运行
- 验证 UI 仍能正常渲染
阶段 5:集成测试(联调后)
- 在真机/模拟器上运行
- 需要后端服务可用
```
### 8.2 CI/CD 集成
```
yaml
# .github/workflows/test.yml 示例
name
:
Test
on
:
[
push
,
pull_request
]
jobs
:
test
:
runs-on
:
ubuntu-latest
steps
:
-
uses
:
actions/checkout@v3
-
uses
:
subosito/flutter-action@v2
-
run
:
flutter pub get
-
run
:
flutter test --coverage
-
run
:
flutter test integration_test/
```
---
## 9. AI 批量生成测试代码提示词模板
如果让 AI 批量生成测试,可以使用以下提示词模板:
```
请根据以下测试方案,为 [模块名] 模块生成完整的测试代码:
1. BO 模型测试:[BO文件名],JSON 示例为 [JSON示例]
2. Service 测试:[Service类名],方法返回 [BO类型]
3. Cubit 测试:[Cubit类名],状态包含 [State字段列表]
4. Widget 冒烟测试:[View类名]
要求:
- 使用 mockito 生成 mock 类
- 覆盖成功和失败两种情况
- 每个测试文件包含完整 import
- 遵循现有的测试命名规范
```
---
## 10. 总结
| 环节 | AI 可自动化 | 需人工 | 备注 |
|------|:----------:|:-----:|------|
| 生成测试代码 | 是 | - | 按模板批量生成 |
| 运行测试 | - | 是 | 终端执行
`flutter test`
|
| 分析测试结果 | - | 是 | 判断失败原因 |
| 修复测试失败 | 是(简单) | 是(复杂) | 简单断言错误 AI 可修 |
| 更新 mock 数据 | 是 | 是 | 接口变更时同步更新 |
| 真机验证 | - | 是 | 必须在真机上运行 |
| 覆盖率报告 | - | 是 | 人工判断哪些分支需补测 |
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment