20 KiB
20 KiB
CLR After Sales 售后服务SDK
模块概述
clr_after_sales 是 OneApp 售后服务模块群中的核心服务SDK,负责封装售后服务的业务逻辑、API接口调用和数据模型定义。该模块为售后服务应用层提供统一的服务接口和数据处理能力。
基本信息
- 模块名称: clr_after_sales
- 版本: 0.0.1
- 描述: 售后服务核心SDK
- Flutter 版本: >=1.17.0
- Dart 版本: >=3.0.0 <4.0.0
功能特性
核心功能
-
API接口封装
- 售后服务API统一封装
- 网络请求统一处理
- 错误处理和重试机制
- 数据缓存和同步
-
业务逻辑封装
- 预约业务逻辑
- 服务流程管理
- 支付结算逻辑
- 数据验证规则
-
数据模型管理
- 统一数据模型定义
- JSON序列化支持
- 数据转换和映射
- 模型验证机制
-
状态管理
- 业务状态统一管理
- 事件驱动更新
- 状态持久化
- 状态同步机制
技术架构
目录结构
lib/
├── clr_after_sales.dart # 模块入口文件
├── src/ # 源代码目录
│ ├── services/ # 业务服务
│ ├── repositories/ # 数据仓库
│ ├── models/ # 数据模型
│ ├── enums/ # 枚举定义
│ ├── exceptions/ # 异常定义
│ ├── utils/ # 工具类
│ └── constants/ # 常量定义
├── test/ # 测试文件
└── generated/ # 代码生成文件
依赖关系
基础框架依赖
basic_network: ^0.2.3+4- 网络通信框架common_utils: ^2.1.0- 通用工具库
内部依赖 (dependency_overrides)
ui_basic- 基础UI组件(本地路径)basic_platform- 平台适配(本地路径)basic_utils- 基础工具(本地路径)basic_uis- 基础UI集合(本地路径)base_mvvm- MVVM架构(本地路径)
核心模块分析
1. 业务服务 (src/services/)
预约服务
class AppointmentService {
final AppointmentRepository _repository;
final NetworkService _networkService;
AppointmentService(this._repository, this._networkService);
/// 创建预约
Future<Result<Appointment>> createAppointment(
AppointmentCreateRequest request,
) async {
try {
// 1. 验证请求数据
final validationResult = _validateCreateRequest(request);
if (validationResult.isFailure) {
return Result.failure(validationResult.error);
}
// 2. 检查时间可用性
final availability = await checkTimeAvailability(
request.storeId,
request.appointmentTime,
);
if (!availability.isAvailable) {
return Result.failure(
AppointmentException('预约时间不可用'),
);
}
// 3. 调用API创建预约
final apiResponse = await _networkService.post(
'/appointments',
data: request.toJson(),
);
// 4. 解析响应数据
final appointment = Appointment.fromJson(apiResponse.data);
// 5. 缓存到本地
await _repository.saveAppointment(appointment);
return Result.success(appointment);
} catch (e) {
return Result.failure(AppointmentException(e.toString()));
}
}
/// 获取用户预约列表
Future<Result<List<Appointment>>> getUserAppointments({
String? status,
int page = 1,
int pageSize = 20,
}) async {
try {
// 1. 先从缓存获取
final cachedAppointments = await _repository.getCachedAppointments(
status: status,
page: page,
pageSize: pageSize,
);
// 2. 如果缓存存在且未过期,直接返回
if (cachedAppointments.isNotEmpty && !_isCacheExpired()) {
return Result.success(cachedAppointments);
}
// 3. 从网络获取最新数据
final apiResponse = await _networkService.get(
'/appointments',
queryParameters: {
'status': status,
'page': page,
'pageSize': pageSize,
},
);
// 4. 解析并缓存数据
final appointments = (apiResponse.data['items'] as List)
.map((json) => Appointment.fromJson(json))
.toList();
await _repository.cacheAppointments(appointments);
return Result.success(appointments);
} catch (e) {
return Result.failure(AppointmentException(e.toString()));
}
}
/// 取消预约
Future<Result<bool>> cancelAppointment(String appointmentId) async {
try {
await _networkService.put(
'/appointments/$appointmentId/cancel',
);
// 更新本地缓存
await _repository.updateAppointmentStatus(
appointmentId,
AppointmentStatus.cancelled,
);
return Result.success(true);
} catch (e) {
return Result.failure(AppointmentException(e.toString()));
}
}
}
服务进度服务
class ServiceProgressService {
final ServiceProgressRepository _repository;
final NetworkService _networkService;
ServiceProgressService(this._repository, this._networkService);
/// 获取服务进度
Future<Result<ServiceProgress>> getServiceProgress(
String appointmentId,
) async {
try {
final apiResponse = await _networkService.get(
'/appointments/$appointmentId/progress',
);
final progress = ServiceProgress.fromJson(apiResponse.data);
// 缓存进度数据
await _repository.saveProgress(progress);
return Result.success(progress);
} catch (e) {
return Result.failure(ServiceException(e.toString()));
}
}
/// 更新服务步骤
Future<Result<ServiceStep>> updateServiceStep(
String appointmentId,
String stepId,
ServiceStepUpdate update,
) async {
try {
final apiResponse = await _networkService.put(
'/appointments/$appointmentId/steps/$stepId',
data: update.toJson(),
);
final updatedStep = ServiceStep.fromJson(apiResponse.data);
// 更新本地缓存
await _repository.updateStep(appointmentId, updatedStep);
return Result.success(updatedStep);
} catch (e) {
return Result.failure(ServiceException(e.toString()));
}
}
/// 上传服务照片
Future<Result<ServicePhoto>> uploadServicePhoto(
String appointmentId,
String stepId,
File photoFile,
) async {
try {
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(photoFile.path),
'appointmentId': appointmentId,
'stepId': stepId,
});
final apiResponse = await _networkService.post(
'/service-photos/upload',
data: formData,
);
final photo = ServicePhoto.fromJson(apiResponse.data);
return Result.success(photo);
} catch (e) {
return Result.failure(ServiceException(e.toString()));
}
}
}
2. 数据仓库 (src/repositories/)
预约数据仓库
abstract class AppointmentRepository {
Future<void> saveAppointment(Appointment appointment);
Future<List<Appointment>> getCachedAppointments({
String? status,
int page = 1,
int pageSize = 20,
});
Future<void> cacheAppointments(List<Appointment> appointments);
Future<void> updateAppointmentStatus(String id, AppointmentStatus status);
Future<void> clearCache();
}
class AppointmentRepositoryImpl implements AppointmentRepository {
final LocalStorage _localStorage;
final DatabaseHelper _databaseHelper;
AppointmentRepositoryImpl(this._localStorage, this._databaseHelper);
@override
Future<void> saveAppointment(Appointment appointment) async {
try {
// 保存到数据库
await _databaseHelper.insertAppointment(appointment);
// 更新缓存
final cacheKey = 'appointment_${appointment.id}';
await _localStorage.setString(cacheKey, jsonEncode(appointment.toJson()));
} catch (e) {
throw RepositoryException('保存预约失败: $e');
}
}
@override
Future<List<Appointment>> getCachedAppointments({
String? status,
int page = 1,
int pageSize = 20,
}) async {
try {
return await _databaseHelper.getAppointments(
status: status,
offset: (page - 1) * pageSize,
limit: pageSize,
);
} catch (e) {
throw RepositoryException('获取缓存预约失败: $e');
}
}
@override
Future<void> updateAppointmentStatus(String id, AppointmentStatus status) async {
try {
await _databaseHelper.updateAppointmentStatus(id, status);
// 更新内存缓存
final cacheKey = 'appointment_$id';
final cachedData = await _localStorage.getString(cacheKey);
if (cachedData != null) {
final appointment = Appointment.fromJson(jsonDecode(cachedData));
final updatedAppointment = appointment.copyWith(status: status);
await _localStorage.setString(
cacheKey,
jsonEncode(updatedAppointment.toJson()),
);
}
} catch (e) {
throw RepositoryException('更新预约状态失败: $e');
}
}
}
3. 数据模型 (src/models/)
预约模型
@freezed
class Appointment with _$Appointment {
const factory Appointment({
required String id,
required String userId,
required String storeId,
required ServiceType serviceType,
required DateTime appointmentTime,
required AppointmentStatus status,
required VehicleInfo vehicleInfo,
String? description,
List<String>? attachments,
@JsonKey(name: 'created_at') DateTime? createdAt,
@JsonKey(name: 'updated_at') DateTime? updatedAt,
Store? store,
List<ServiceItem>? serviceItems,
PaymentInfo? paymentInfo,
}) = _Appointment;
factory Appointment.fromJson(Map<String, dynamic> json) =>
_$AppointmentFromJson(json);
}
@freezed
class AppointmentCreateRequest with _$AppointmentCreateRequest {
const factory AppointmentCreateRequest({
required String storeId,
required ServiceType serviceType,
required DateTime appointmentTime,
required VehicleInfo vehicleInfo,
String? description,
List<String>? attachments,
List<String>? serviceItemIds,
}) = _AppointmentCreateRequest;
factory AppointmentCreateRequest.fromJson(Map<String, dynamic> json) =>
_$AppointmentCreateRequestFromJson(json);
}
服务进度模型
@freezed
class ServiceProgress with _$ServiceProgress {
const factory ServiceProgress({
required String appointmentId,
required ServiceStatus status,
required List<ServiceStep> steps,
required double progressPercentage,
@JsonKey(name: 'estimated_completion') DateTime? estimatedCompletion,
@JsonKey(name: 'actual_completion') DateTime? actualCompletion,
Technician? assignedTechnician,
List<ServicePhoto>? photos,
String? currentStepNote,
}) = _ServiceProgress;
factory ServiceProgress.fromJson(Map<String, dynamic> json) =>
_$ServiceProgressFromJson(json);
}
@freezed
class ServiceStep with _$ServiceStep {
const factory ServiceStep({
required String id,
required String name,
required String description,
required ServiceStepStatus status,
required int orderIndex,
@JsonKey(name: 'started_at') DateTime? startedAt,
@JsonKey(name: 'completed_at') DateTime? completedAt,
String? note,
List<String>? photoIds,
Duration? estimatedDuration,
Duration? actualDuration,
}) = _ServiceStep;
factory ServiceStep.fromJson(Map<String, dynamic> json) =>
_$ServiceStepFromJson(json);
}
4. 枚举定义 (src/enums/)
enum AppointmentStatus {
@JsonValue('pending')
pending,
@JsonValue('confirmed')
confirmed,
@JsonValue('inProgress')
inProgress,
@JsonValue('completed')
completed,
@JsonValue('cancelled')
cancelled,
@JsonValue('noShow')
noShow,
}
enum ServiceType {
@JsonValue('maintenance')
maintenance,
@JsonValue('repair')
repair,
@JsonValue('inspection')
inspection,
@JsonValue('bodywork')
bodywork,
@JsonValue('insurance')
insurance,
@JsonValue('emergency')
emergency,
@JsonValue('recall')
recall,
}
enum ServiceStatus {
@JsonValue('waiting')
waiting,
@JsonValue('inProgress')
inProgress,
@JsonValue('completed')
completed,
@JsonValue('paused')
paused,
@JsonValue('cancelled')
cancelled,
}
enum ServiceStepStatus {
@JsonValue('pending')
pending,
@JsonValue('inProgress')
inProgress,
@JsonValue('completed')
completed,
@JsonValue('skipped')
skipped,
}
5. 异常定义 (src/exceptions/)
abstract class AfterSalesException implements Exception {
final String message;
final String? code;
final dynamic details;
const AfterSalesException(this.message, {this.code, this.details});
@override
String toString() => 'AfterSalesException: $message';
}
class AppointmentException extends AfterSalesException {
const AppointmentException(String message, {String? code, dynamic details})
: super(message, code: code, details: details);
}
class ServiceException extends AfterSalesException {
const ServiceException(String message, {String? code, dynamic details})
: super(message, code: code, details: details);
}
class PaymentException extends AfterSalesException {
const PaymentException(String message, {String? code, dynamic details})
: super(message, code: code, details: details);
}
class NetworkException extends AfterSalesException {
const NetworkException(String message, {String? code, dynamic details})
: super(message, code: code, details: details);
}
class RepositoryException extends AfterSalesException {
const RepositoryException(String message, {String? code, dynamic details})
: super(message, code: code, details: details);
}
6. 结果封装 (src/utils/result.dart)
@freezed
class Result<T> with _$Result<T> {
const factory Result.success(T data) = Success<T>;
const factory Result.failure(Exception error) = Failure<T>;
bool get isSuccess => this is Success<T>;
bool get isFailure => this is Failure<T>;
T? get data => mapOrNull(success: (success) => success.data);
Exception? get error => mapOrNull(failure: (failure) => failure.error);
}
extension ResultExtensions<T> on Result<T> {
R fold<R>(
R Function(T data) onSuccess,
R Function(Exception error) onFailure,
) {
return when(
success: onSuccess,
failure: onFailure,
);
}
Future<Result<R>> mapAsync<R>(
Future<R> Function(T data) mapper,
) async {
return fold(
(data) async {
try {
final result = await mapper(data);
return Result.success(result);
} catch (e) {
return Result.failure(Exception(e.toString()));
}
},
(error) => Future.value(Result.failure(error)),
);
}
}
使用示例
基础使用
class AfterSalesExample {
final AppointmentService _appointmentService;
final ServiceProgressService _progressService;
AfterSalesExample(this._appointmentService, this._progressService);
Future<void> createAppointmentExample() async {
final request = AppointmentCreateRequest(
storeId: 'store_123',
serviceType: ServiceType.maintenance,
appointmentTime: DateTime.now().add(Duration(days: 1)),
vehicleInfo: VehicleInfo(
vin: 'WVWAA71K08W201030',
licensePlate: '京A12345',
model: 'ID.4 CROZZ',
),
description: '常规保养',
);
final result = await _appointmentService.createAppointment(request);
result.fold(
(appointment) {
print('预约创建成功: ${appointment.id}');
},
(error) {
print('预约创建失败: ${error.toString()}');
},
);
}
Future<void> trackServiceProgressExample(String appointmentId) async {
final result = await _progressService.getServiceProgress(appointmentId);
result.fold(
(progress) {
print('服务进度: ${progress.progressPercentage}%');
print('当前步骤: ${progress.steps.where((s) => s.status == ServiceStepStatus.inProgress).first.name}');
},
(error) {
print('获取进度失败: ${error.toString()}');
},
);
}
}
依赖注入配置
class AfterSalesDI {
static void setupDependencies(GetIt locator) {
// 注册仓库
locator.registerLazySingleton<AppointmentRepository>(
() => AppointmentRepositoryImpl(
locator<LocalStorage>(),
locator<DatabaseHelper>(),
),
);
locator.registerLazySingleton<ServiceProgressRepository>(
() => ServiceProgressRepositoryImpl(
locator<LocalStorage>(),
locator<DatabaseHelper>(),
),
);
// 注册服务
locator.registerLazySingleton<AppointmentService>(
() => AppointmentService(
locator<AppointmentRepository>(),
locator<NetworkService>(),
),
);
locator.registerLazySingleton<ServiceProgressService>(
() => ServiceProgressService(
locator<ServiceProgressRepository>(),
locator<NetworkService>(),
),
);
}
}
测试策略
单元测试
void main() {
group('AppointmentService', () {
late AppointmentService appointmentService;
late MockAppointmentRepository mockRepository;
late MockNetworkService mockNetworkService;
setUp(() {
mockRepository = MockAppointmentRepository();
mockNetworkService = MockNetworkService();
appointmentService = AppointmentService(mockRepository, mockNetworkService);
});
test('should create appointment successfully', () async {
// Arrange
final request = AppointmentCreateRequest(
storeId: 'store_123',
serviceType: ServiceType.maintenance,
appointmentTime: DateTime.now().add(Duration(days: 1)),
vehicleInfo: VehicleInfo(vin: 'TEST123'),
);
final expectedAppointment = Appointment(
id: 'appointment_123',
userId: 'user_123',
storeId: request.storeId,
serviceType: request.serviceType,
appointmentTime: request.appointmentTime,
status: AppointmentStatus.pending,
vehicleInfo: request.vehicleInfo,
);
when(mockNetworkService.post('/appointments', data: any))
.thenAnswer((_) async => ApiResponse(data: expectedAppointment.toJson()));
// Act
final result = await appointmentService.createAppointment(request);
// Assert
expect(result.isSuccess, true);
expect(result.data?.id, 'appointment_123');
verify(mockRepository.saveAppointment(any)).called(1);
});
});
}
最佳实践
API设计原则
- 统一响应格式: 所有API使用统一的响应格式
- 错误处理: 完善的错误码和错误信息
- 数据验证: 严格的输入数据验证
- 幂等性: 关键操作支持幂等性
缓存策略
- 多级缓存: 内存缓存 + 本地存储 + 网络
- 缓存失效: 基于时间和事件的缓存失效
- 数据一致性: 保证缓存与服务器数据一致
- 离线支持: 关键数据支持离线访问
总结
clr_after_sales 模块作为售后服务的核心SDK,通过完善的架构设计和标准化的接口封装,为售后服务应用提供了稳定可靠的技术支撑。模块采用了领域驱动设计思想,具有良好的可测试性和可维护性,能够支撑复杂的售后业务场景。