Files
AIOA/mobile/lib/features/requests/data/leave_request_repository.dart
T
2026-07-18 19:20:07 +08:00

214 lines
6.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:convert';
import 'dart:math';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
class LeaveRequestItem {
const LeaveRequestItem({
required this.id,
required this.type,
required this.startsAt,
required this.endsAt,
required this.reason,
required this.status,
required this.version,
required this.createdAt,
required this.updatedAt,
});
final String id;
final String type;
final DateTime startsAt;
final DateTime endsAt;
final String reason;
final String status;
final int version;
final DateTime createdAt;
final DateTime updatedAt;
factory LeaveRequestItem.fromJson(Map<String, Object?> json) =>
LeaveRequestItem(
id: json['id']! as String,
type: json['type']! as String,
startsAt: DateTime.parse(json['startsAt']! as String),
endsAt: DateTime.parse(json['endsAt']! as String),
reason: json['reason']! as String,
status: json['status']! as String,
version: json['version']! as int,
createdAt: DateTime.parse(json['createdAt']! as String),
updatedAt: DateTime.parse(json['updatedAt']! as String),
);
}
class LeaveTimelineEvent {
const LeaveTimelineEvent({
required this.id,
required this.eventType,
required this.fromStatus,
required this.toStatus,
required this.occurredAt,
});
final String id;
final String eventType;
final String fromStatus;
final String toStatus;
final DateTime occurredAt;
factory LeaveTimelineEvent.fromJson(Map<String, Object?> json) =>
LeaveTimelineEvent(
id: json['id']! as String,
eventType: json['eventType']! as String,
fromStatus: json['fromStatus']! as String,
toStatus: json['toStatus']! as String,
occurredAt: DateTime.parse(json['occurredAt']! as String),
);
}
class LeaveRequestException implements Exception {
const LeaveRequestException(this.message);
final String message;
@override
String toString() => message;
}
class LeaveRequestRepository {
LeaveRequestRepository({
http.Client? client,
this.baseUrl = const String.fromEnvironment(
'AIOA_API_BASE_URL',
defaultValue: 'http://127.0.0.1:8080/api/v1',
),
this.accessToken = const String.fromEnvironment('AIOA_ACCESS_TOKEN'),
}) : _client = client ?? http.Client();
final http.Client _client;
final String baseUrl;
final String accessToken;
Future<List<LeaveRequestItem>> list() async {
final response = await _client.get(
Uri.parse('$baseUrl/leave-requests'),
headers: _headers,
);
_requireSuccess(response);
return (jsonDecode(response.body) as List)
.map(
(item) =>
LeaveRequestItem.fromJson(Map<String, Object?>.from(item as Map)),
)
.toList();
}
Future<LeaveRequestItem> get(String id) async {
final response = await _client.get(
Uri.parse('$baseUrl/leave-requests/$id'),
headers: _headers,
);
_requireSuccess(response);
return LeaveRequestItem.fromJson(
jsonDecode(response.body) as Map<String, Object?>,
);
}
Future<List<LeaveTimelineEvent>> timeline(String id) async {
final response = await _client.get(
Uri.parse('$baseUrl/leave-requests/$id/timeline'),
headers: _headers,
);
_requireSuccess(response);
return (jsonDecode(response.body) as List)
.map(
(item) => LeaveTimelineEvent.fromJson(
Map<String, Object?>.from(item as Map),
),
)
.toList();
}
Future<LeaveRequestItem> transition(
LeaveRequestItem request,
String action,
) async {
final payload = jsonEncode({'version': request.version});
final preferences = await SharedPreferences.getInstance();
final storageKey = 'leave-transition.${request.id}.$action';
final existing = preferences.getString(storageKey);
final pending = existing == null ? null : _decodePending(existing);
final key = pending?.payload == payload ? pending!.key : _newKey(action);
await preferences.setString(
storageKey,
jsonEncode({'key': key, 'payload': payload}),
);
late http.Response response;
try {
response = await _client.post(
Uri.parse('$baseUrl/leave-requests/${request.id}/$action'),
headers: {
..._headers,
'Content-Type': 'application/json',
'Idempotency-Key': key,
},
body: payload,
);
} catch (_) {
throw const LeaveRequestException('网络不可用,操作已保存,可安全重试');
}
if (response.statusCode < 200 || response.statusCode >= 300) {
if (response.statusCode < 500 && response.statusCode != 401) {
await preferences.remove(storageKey);
}
_requireSuccess(response);
}
await preferences.remove(storageKey);
return LeaveRequestItem.fromJson(
jsonDecode(response.body) as Map<String, Object?>,
);
}
Map<String, String> get _headers => {
'Accept': 'application/json',
if (accessToken.isNotEmpty) 'Authorization': 'Bearer $accessToken',
};
void _requireSuccess(http.Response response) {
if (response.statusCode >= 200 && response.statusCode < 300) return;
String? message;
try {
message =
(jsonDecode(response.body) as Map<String, Object?>)['detail']
as String?;
} catch (_) {}
throw LeaveRequestException(message ?? '申请请求失败(${response.statusCode}');
}
_PendingTransition? _decodePending(String value) {
try {
final json = jsonDecode(value) as Map<String, Object?>;
return _PendingTransition(
json['key']! as String,
json['payload']! as String,
);
} catch (_) {
return null;
}
}
String _newKey(String action) {
final random = Random.secure();
final entropy = List.generate(
12,
(_) => random.nextInt(256).toRadixString(16).padLeft(2, '0'),
).join();
return 'leave-$action-${DateTime.now().microsecondsSinceEpoch}-$entropy';
}
}
class _PendingTransition {
const _PendingTransition(this.key, this.payload);
final String key;
final String payload;
}