feat: add Flutter mobile clients and staff delivery API
This commit is contained in:
93
apps/service_app/lib/data/services/api_client.dart
Normal file
93
apps/service_app/lib/data/services/api_client.dart
Normal file
@@ -0,0 +1,93 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class ApiException implements Exception {
|
||||
const ApiException(this.code, this.message);
|
||||
|
||||
final int code;
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
ApiClient(this._tokenProvider, {http.Client? client, String? baseUrl})
|
||||
: _client = client ?? http.Client(),
|
||||
baseUrl =
|
||||
baseUrl ??
|
||||
const String.fromEnvironment(
|
||||
'API_BASE_URL',
|
||||
defaultValue: 'http://10.0.2.2:12426',
|
||||
);
|
||||
|
||||
final String baseUrl;
|
||||
final String Function() _tokenProvider;
|
||||
final http.Client _client;
|
||||
|
||||
Future<Object?> get(String path, {bool authenticated = true}) =>
|
||||
_send('GET', path, authenticated: authenticated);
|
||||
|
||||
Future<Object?> post(
|
||||
String path, {
|
||||
Map<String, Object?>? body,
|
||||
bool authenticated = true,
|
||||
}) => _send('POST', path, body: body, authenticated: authenticated);
|
||||
|
||||
Future<Object?> put(String path, {Map<String, Object?>? body}) => _send('PUT', path, body: body);
|
||||
|
||||
Future<String> upload(String filePath, {String contentType = 'image/jpeg'}) async {
|
||||
final request = http.MultipartRequest('POST', Uri.parse('$baseUrl/upload/file'));
|
||||
request.headers[HttpHeaders.authorizationHeader] = _tokenProvider();
|
||||
request.fields['declared_content_type'] = contentType;
|
||||
request.files.add(await http.MultipartFile.fromPath('file', filePath));
|
||||
final response = await http.Response.fromStream(await request.send());
|
||||
final details = _decode(response);
|
||||
return jsonMap(details)['uri'] as String? ?? '';
|
||||
}
|
||||
|
||||
Future<Object?> _send(
|
||||
String method,
|
||||
String path, {
|
||||
Map<String, Object?>? body,
|
||||
bool authenticated = true,
|
||||
}) async {
|
||||
final request = http.Request(method, Uri.parse('$baseUrl$path'));
|
||||
request.headers[HttpHeaders.acceptHeader] = 'application/json';
|
||||
if (authenticated && _tokenProvider().isNotEmpty) {
|
||||
request.headers[HttpHeaders.authorizationHeader] = _tokenProvider();
|
||||
}
|
||||
if (body != null) {
|
||||
request.headers[HttpHeaders.contentTypeHeader] = 'application/json; charset=UTF-8';
|
||||
request.body = jsonEncode(body);
|
||||
}
|
||||
final response = await http.Response.fromStream(await _client.send(request));
|
||||
return _decode(response);
|
||||
}
|
||||
|
||||
Object? _decode(http.Response response) {
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throw ApiException(response.statusCode, '网络请求失败(${response.statusCode})');
|
||||
}
|
||||
final decoded = jsonDecode(response.body);
|
||||
if (decoded is! Map<String, Object?>) {
|
||||
throw const ApiException(500, '服务端响应格式错误');
|
||||
}
|
||||
final code = (decoded['code'] as num?)?.toInt() ?? 500;
|
||||
if (code != 0) throw ApiException(code, decoded['message'] as String? ?? '操作失败');
|
||||
return decoded['details'];
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object?> jsonMap(Object? value) {
|
||||
if (value is Map<String, Object?>) return value;
|
||||
if (value is Map) return value.map((key, item) => MapEntry(key.toString(), item));
|
||||
throw const ApiException(500, '服务端数据格式错误');
|
||||
}
|
||||
|
||||
List<Map<String, Object?>> jsonList(Object? value) {
|
||||
if (value is! List) return const [];
|
||||
return value.map<Map<String, Object?>>(jsonMap).toList(growable: false);
|
||||
}
|
||||
42
apps/service_app/lib/data/services/location_service.dart
Normal file
42
apps/service_app/lib/data/services/location_service.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
class LocationPoint {
|
||||
const LocationPoint({
|
||||
required this.longitude,
|
||||
required this.latitude,
|
||||
required this.accuracy,
|
||||
required this.occurredAt,
|
||||
});
|
||||
|
||||
final String longitude;
|
||||
final String latitude;
|
||||
final String accuracy;
|
||||
final DateTime occurredAt;
|
||||
}
|
||||
|
||||
abstract interface class LocationService {
|
||||
Future<LocationPoint> current();
|
||||
}
|
||||
|
||||
class GeolocatorLocationService implements LocationService {
|
||||
@override
|
||||
Future<LocationPoint> current() async {
|
||||
if (!await Geolocator.isLocationServiceEnabled()) {
|
||||
throw StateError('请先开启系统定位服务');
|
||||
}
|
||||
var permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
}
|
||||
if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) {
|
||||
throw StateError('定位权限未授权,无法完成该在线动作');
|
||||
}
|
||||
final position = await Geolocator.getCurrentPosition();
|
||||
return LocationPoint(
|
||||
longitude: position.longitude.toStringAsFixed(7),
|
||||
latitude: position.latitude.toStringAsFixed(7),
|
||||
accuracy: position.accuracy.toStringAsFixed(1),
|
||||
occurredAt: position.timestamp,
|
||||
);
|
||||
}
|
||||
}
|
||||
12
apps/service_app/lib/data/services/secure_session_store.dart
Normal file
12
apps/service_app/lib/data/services/secure_session_store.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
class SecureSessionStore {
|
||||
SecureSessionStore({FlutterSecureStorage? storage})
|
||||
: _storage = storage ?? const FlutterSecureStorage();
|
||||
|
||||
final FlutterSecureStorage _storage;
|
||||
|
||||
Future<String?> read(String key) => _storage.read(key: key);
|
||||
Future<void> write(String key, String value) => _storage.write(key: key, value: value);
|
||||
Future<void> delete(String key) => _storage.delete(key: key);
|
||||
}
|
||||
Reference in New Issue
Block a user