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

186 lines
5.3 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 ApprovalTaskItem {
const ApprovalTaskItem({
required this.id,
required this.name,
required this.createdAt,
required this.leaveRequest,
});
final String id;
final String name;
final DateTime createdAt;
final ApprovalLeaveRequest leaveRequest;
factory ApprovalTaskItem.fromJson(Map<String, Object?> json) =>
ApprovalTaskItem(
id: json['id']! as String,
name: json['name']! as String,
createdAt: DateTime.parse(json['createdAt']! as String),
leaveRequest: ApprovalLeaveRequest.fromJson(
Map<String, Object?>.from(json['leaveRequest']! as Map),
),
);
}
class ApprovalLeaveRequest {
const ApprovalLeaveRequest({
required this.id,
required this.type,
required this.startsAt,
required this.endsAt,
required this.reason,
required this.status,
required this.version,
});
final String id;
final String type;
final DateTime startsAt;
final DateTime endsAt;
final String reason;
final String status;
final int version;
factory ApprovalLeaveRequest.fromJson(Map<String, Object?> json) =>
ApprovalLeaveRequest(
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,
);
}
class ApprovalTaskException implements Exception {
const ApprovalTaskException(this.message);
final String message;
@override
String toString() => message;
}
class ApprovalTaskRepository {
ApprovalTaskRepository({
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<ApprovalTaskItem>> list() async {
final response = await _client.get(
Uri.parse('$baseUrl/approval-tasks'),
headers: _headers,
);
_requireSuccess(response);
return (jsonDecode(response.body) as List)
.map(
(item) =>
ApprovalTaskItem.fromJson(Map<String, Object?>.from(item as Map)),
)
.toList();
}
Future<void> decide({
required ApprovalTaskItem task,
required bool approved,
String? comment,
}) async {
final action = approved ? 'approve' : 'reject';
final payload = jsonEncode({
'version': task.leaveRequest.version,
if (comment != null && comment.trim().isNotEmpty)
'comment': comment.trim(),
});
final preferences = await SharedPreferences.getInstance();
final storageKey = 'approval.pending.${task.id}.$action';
final existing = preferences.getString(storageKey);
final pending = existing == null ? null : _decodePending(existing);
final idempotencyKey = pending?.payload == payload
? pending!.key
: _newKey(action);
await preferences.setString(
storageKey,
jsonEncode({'key': idempotencyKey, 'payload': payload}),
);
late http.Response response;
try {
response = await _client.post(
Uri.parse('$baseUrl/approval-tasks/${task.id}/$action'),
headers: {
..._headers,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: payload,
);
} catch (_) {
throw const ApprovalTaskException('网络不可用,审批请求已保存,可安全重试');
}
if (response.statusCode < 200 || response.statusCode >= 300) {
if (response.statusCode < 500 && response.statusCode != 401) {
await preferences.remove(storageKey);
}
_requireSuccess(response);
}
await preferences.remove(storageKey);
}
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 ApprovalTaskException(message ?? '待办请求失败(${response.statusCode}');
}
_PendingApproval? _decodePending(String encoded) {
try {
final json = jsonDecode(encoded) as Map<String, Object?>;
return _PendingApproval(
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 'approval-$action-${DateTime.now().microsecondsSinceEpoch}-$entropy';
}
}
class _PendingApproval {
const _PendingApproval(this.key, this.payload);
final String key;
final String payload;
}