feat: add Flutter mobile clients and staff delivery API
This commit is contained in:
149
apps/user_app/lib/data/repositories/client_repository.dart
Normal file
149
apps/user_app/lib/data/repositories/client_repository.dart
Normal file
@@ -0,0 +1,149 @@
|
||||
import '../../domain/models/client_models.dart';
|
||||
import '../services/api_client.dart';
|
||||
|
||||
class ClientRepository {
|
||||
ClientRepository(this._api);
|
||||
|
||||
static const root = '/heqi/client/v1/user';
|
||||
final ApiClient _api;
|
||||
|
||||
Future<List<ClientRecord>> contents() async {
|
||||
final values = jsonList(await _api.get('$root/public/contents', authenticated: false));
|
||||
return values
|
||||
.map(
|
||||
(value) => ClientRecord(
|
||||
identity: value['identity'] as String? ?? '',
|
||||
title: value['title'] as String? ?? '安全公告',
|
||||
subtitle: value['summary'] as String? ?? value['content_type'] as String? ?? '',
|
||||
raw: value,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<ClientRecord>> products() async {
|
||||
final values = jsonList(await _api.get('$root/public/products', authenticated: false));
|
||||
return values
|
||||
.map(
|
||||
(value) => ClientRecord(
|
||||
identity: value['identity'] as String? ?? '',
|
||||
title: value['name'] as String? ?? '商品',
|
||||
subtitle:
|
||||
'${moneyText((value['price_amount'] as num?)?.toInt() ?? 0)} · 库存 ${(value['stock_quantity'] as num?)?.toInt() ?? 0}',
|
||||
raw: value,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<UserProfile> profile() async =>
|
||||
UserProfile.fromJson(jsonMap(await _api.get('$root/auth/profile')));
|
||||
|
||||
Future<WalletSummary> wallet() async =>
|
||||
WalletSummary.fromJson(jsonMap(await _api.get('$root/wallet')));
|
||||
|
||||
Future<List<ClientRecord>> addresses() =>
|
||||
_records('$root/addresses', titleKeys: const ['address'], subtitleKeys: const ['is_default']);
|
||||
|
||||
Future<List<ClientRecord>> shopOrders() => _records(
|
||||
'$root/shop/orders',
|
||||
titleKeys: const ['order_no'],
|
||||
subtitleKeys: const ['payable_amount', 'logistics_company'],
|
||||
statusKey: 'order_status',
|
||||
);
|
||||
|
||||
Future<List<ClientRecord>> gasOrders() => _records(
|
||||
'$root/gas/orders',
|
||||
titleKeys: const ['order_no'],
|
||||
subtitleKeys: const ['address'],
|
||||
statusKey: 'order_status',
|
||||
);
|
||||
|
||||
Future<List<ClientRecord>> contracts() => _records(
|
||||
'$root/gas/contracts',
|
||||
titleKeys: const ['contract_no'],
|
||||
subtitleKeys: const ['title'],
|
||||
statusKey: 'contract_status',
|
||||
);
|
||||
|
||||
Future<List<ClientRecord>> tickets() => _records(
|
||||
'$root/tickets',
|
||||
titleKeys: const ['ticket_no'],
|
||||
subtitleKeys: const ['description', 'category'],
|
||||
statusKey: 'ticket_status',
|
||||
);
|
||||
|
||||
Future<List<ClientRecord>> walletRecords() => _records(
|
||||
'$root/wallet/records',
|
||||
titleKeys: const ['trade_type', 'record_no'],
|
||||
subtitleKeys: const ['amount', 'direction'],
|
||||
);
|
||||
|
||||
Future<Map<String, Object?>?> serviceRelation() async {
|
||||
final value = await _api.get('$root/service-relation');
|
||||
if (value == null || value == '') return null;
|
||||
return jsonMap(value);
|
||||
}
|
||||
|
||||
Future<void> addAddress(String address, {bool isDefault = false}) async {
|
||||
await _api.post('$root/addresses', body: {'address': address, 'is_default': isDefault});
|
||||
}
|
||||
|
||||
Future<void> createTicket({
|
||||
required String requestNo,
|
||||
required String category,
|
||||
required String description,
|
||||
}) async {
|
||||
await _api.post(
|
||||
'$root/tickets',
|
||||
body: {'request_no': requestNo, 'category': category, 'description': description},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> createShopOrder({
|
||||
required String requestNo,
|
||||
required String productIdentity,
|
||||
required String addressIdentity,
|
||||
required String contactName,
|
||||
required String contactPhone,
|
||||
}) async {
|
||||
await _api.post(
|
||||
'$root/shop/orders',
|
||||
body: {
|
||||
'request_no': requestNo,
|
||||
'address_identity': addressIdentity,
|
||||
'contact_name': contactName,
|
||||
'contact_phone': contactPhone,
|
||||
'items': [
|
||||
{'product_identity': productIdentity, 'quantity': 1},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ClientRecord>> _records(
|
||||
String path, {
|
||||
required List<String> titleKeys,
|
||||
required List<String> subtitleKeys,
|
||||
String? statusKey,
|
||||
}) async {
|
||||
final values = jsonList(await _api.get(path));
|
||||
return values.map((value) {
|
||||
String pick(List<String> keys) {
|
||||
for (final key in keys) {
|
||||
final item = value[key];
|
||||
if (item != null && item.toString().isNotEmpty) return item.toString();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
return ClientRecord(
|
||||
identity: value['identity'] as String? ?? '',
|
||||
title: pick(titleKeys),
|
||||
subtitle: pick(subtitleKeys),
|
||||
status: statusKey == null ? null : (value[statusKey] as num?)?.toInt(),
|
||||
raw: value,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
89
apps/user_app/lib/data/services/api_client.dart
Normal file
89
apps/user_app/lib/data/services/api_client.dart
Normal file
@@ -0,0 +1,89 @@
|
||||
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<Object?> delete(String path, {Map<String, Object?>? body}) =>
|
||||
_send('DELETE', path, body: body);
|
||||
|
||||
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) {
|
||||
final token = _tokenProvider();
|
||||
if (token.isNotEmpty) request.headers[HttpHeaders.authorizationHeader] = token;
|
||||
}
|
||||
if (body != null) {
|
||||
request.headers[HttpHeaders.contentTypeHeader] = 'application/json; charset=UTF-8';
|
||||
request.body = jsonEncode(body);
|
||||
}
|
||||
final streamed = await _client.send(request);
|
||||
final response = await http.Response.fromStream(streamed);
|
||||
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);
|
||||
}
|
||||
15
apps/user_app/lib/data/services/secure_session_store.dart
Normal file
15
apps/user_app/lib/data/services/secure_session_store.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
class SecureSessionStore {
|
||||
SecureSessionStore({FlutterSecureStorage? storage})
|
||||
: _storage = storage ?? const FlutterSecureStorage();
|
||||
|
||||
static const _tokenKey = 'user_app_access_token';
|
||||
final FlutterSecureStorage _storage;
|
||||
|
||||
Future<String?> readToken() => _storage.read(key: _tokenKey);
|
||||
|
||||
Future<void> writeToken(String token) => _storage.write(key: _tokenKey, value: token);
|
||||
|
||||
Future<void> clear() => _storage.delete(key: _tokenKey);
|
||||
}
|
||||
Reference in New Issue
Block a user