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 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 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() 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.from(item as Map)), ) .toList(); } Future 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, ); } Future> 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.from(item as Map), ), ) .toList(); } Future 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, ); } Map 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)['detail'] as String?; } catch (_) {} throw LeaveRequestException(message ?? '申请请求失败(${response.statusCode})'); } _PendingTransition? _decodePending(String value) { try { final json = jsonDecode(value) as Map; 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; }