Commit 76a57f12 authored by 张宏's avatar 张宏

parent 9d09840c
......@@ -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
# 智慧酒店 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
# 智慧酒店 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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment