6a2a9d2c87
Backend: - Add user_feedback table with RLS policy - Create feedback submission API (multipart/form-data) - Implement xlsx report generation with embedded images - Add scheduled email delivery via Feishu SMTP - Create HTML email templates (daily_report, no_feedback) Frontend: - Add feedback screen with type selection and image picker - Support anonymous submission via skipAuth flag - Collect device info and app version Protocol: - Document feedback API contract and error codes - Update http-error-codes.md with FEEDBACK_* codes
86 lines
2.3 KiB
Dart
86 lines
2.3 KiB
Dart
import 'package:dio/dio.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
|
|
import '../../../../core/logging/logger.dart';
|
|
import '../../../../core/network/api_problem.dart';
|
|
import '../../../../data/network/api_client.dart';
|
|
import '../models/feedback.dart';
|
|
|
|
class FeedbackApi {
|
|
FeedbackApi({required ApiClient apiClient}) : _apiClient = apiClient;
|
|
|
|
final ApiClient _apiClient;
|
|
final Logger _logger = getLogger('features.settings.feedback_api');
|
|
|
|
Future<void> submitFeedback({
|
|
required FeedbackType type,
|
|
required String content,
|
|
required DeviceInfo deviceInfo,
|
|
required String appVersion,
|
|
required String osVersion,
|
|
required List<XFile> images,
|
|
required bool isAnonymous,
|
|
}) async {
|
|
final typeName = switch (type) {
|
|
FeedbackType.bug => 'bug',
|
|
FeedbackType.suggestion => 'suggestion',
|
|
FeedbackType.other => 'other',
|
|
};
|
|
|
|
final formData = FormData.fromMap({
|
|
'feedback_type': typeName,
|
|
'content': content,
|
|
'device_info':
|
|
'{"platform":"${deviceInfo.platform}","model":"${deviceInfo.model}"}',
|
|
'app_version': appVersion,
|
|
'os_version': osVersion,
|
|
});
|
|
|
|
for (final image in images) {
|
|
formData.files.add(
|
|
MapEntry(
|
|
'images',
|
|
await MultipartFile.fromFile(image.path, filename: image.name),
|
|
),
|
|
);
|
|
}
|
|
|
|
final options = isAnonymous
|
|
? Options(extra: {'skipAuth': true})
|
|
: Options();
|
|
|
|
try {
|
|
await _apiClient.rawDio.post<void>(
|
|
'/api/v1/feedback',
|
|
data: formData,
|
|
options: options,
|
|
);
|
|
} on DioException catch (error, stackTrace) {
|
|
_logger.error(
|
|
message: 'Submit feedback failed',
|
|
error: error,
|
|
stackTrace: stackTrace,
|
|
);
|
|
throw _mapProblem(error);
|
|
}
|
|
}
|
|
|
|
ApiProblem _mapProblem(DioException error) {
|
|
final status = error.response?.statusCode ?? 500;
|
|
final data = error.response?.data;
|
|
if (data is Map<String, dynamic>) {
|
|
return ApiProblem(
|
|
status: status,
|
|
title: (data['title'] as String?) ?? 'Request failed',
|
|
detail: (data['detail'] as String?) ?? '',
|
|
code: data['code'] as String?,
|
|
);
|
|
}
|
|
return ApiProblem(
|
|
status: status,
|
|
title: 'Network error',
|
|
detail: error.message ?? 'Request failed',
|
|
);
|
|
}
|
|
}
|