585 lines
16 KiB
Dart
585 lines
16 KiB
Dart
/// 时间: 2026-03-29
|
||
/// 功能: 投票API服务
|
||
/// 介绍: 专门处理投票相关的API请求,包括获取投票列表、投票详情、提交投票等功能
|
||
/// 最新变化: 添加Cookie管理器支持PHP Session认证
|
||
|
||
import 'dart:convert';
|
||
import 'dart:io' as io show Platform;
|
||
import 'package:dio/dio.dart';
|
||
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
|
||
import 'package:cookie_jar/cookie_jar.dart';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:platform_info/platform_info.dart';
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
|
||
class VoteApi {
|
||
static const String _baseUrl = 'https://poe.vogov.cn/toupiao/';
|
||
static const String _voteEndpoint = 'vote_api.php';
|
||
static const String _userEndpoint = 'tapi.php';
|
||
static const Duration _timeout = Duration(seconds: 30);
|
||
|
||
static final BaseOptions _options = BaseOptions(
|
||
baseUrl: _baseUrl,
|
||
connectTimeout: _timeout,
|
||
receiveTimeout: _timeout,
|
||
sendTimeout: _timeout,
|
||
headers: {
|
||
'Content-Type': 'application/json; charset=UTF-8',
|
||
'Accept': 'application/json',
|
||
'User-Agent': 'Poes-Flutter/1.0.0',
|
||
},
|
||
);
|
||
|
||
static final CookieJar _cookieJar = CookieJar();
|
||
static final Dio _dio = Dio(_options)
|
||
..interceptors.add(CookieManager(_cookieJar));
|
||
|
||
static void _debugLog(String message) {
|
||
if (kDebugMode) {
|
||
print('VoteApi: $message');
|
||
}
|
||
}
|
||
|
||
static Future<Map<String, dynamic>> _get(
|
||
String path, {
|
||
Map<String, dynamic>? queryParameters,
|
||
}) async {
|
||
try {
|
||
final url = '$_baseUrl$path';
|
||
_debugLog('GET $url');
|
||
if (queryParameters != null) {
|
||
_debugLog('查询参数: $queryParameters');
|
||
}
|
||
|
||
final response = await _dio.get(url, queryParameters: queryParameters);
|
||
|
||
_debugLog('响应: ${response.data}');
|
||
return response.data as Map<String, dynamic>;
|
||
} on DioException catch (e) {
|
||
_debugLog('Dio异常: ${e.type} - ${e.message}');
|
||
throw Exception('请求失败: ${e.message}');
|
||
} catch (e) {
|
||
_debugLog('未知异常: $e');
|
||
throw Exception('请求失败: $e');
|
||
}
|
||
}
|
||
|
||
static Future<Map<String, dynamic>> _post(
|
||
String path, {
|
||
Map<String, dynamic>? data,
|
||
}) async {
|
||
try {
|
||
final url = '$_baseUrl$path';
|
||
_debugLog('POST $url');
|
||
if (data != null) {
|
||
_debugLog('请求数据: $data');
|
||
}
|
||
|
||
final response = await _dio.post(url, data: data);
|
||
|
||
_debugLog('响应: ${response.data}');
|
||
return response.data as Map<String, dynamic>;
|
||
} on DioException catch (e) {
|
||
_debugLog('Dio异常: ${e.type} - ${e.message}');
|
||
throw Exception('请求失败: ${e.message}');
|
||
} catch (e) {
|
||
_debugLog('未知异常: $e');
|
||
throw Exception('请求失败: $e');
|
||
}
|
||
}
|
||
|
||
/// 获取用户登录状态
|
||
static Future<bool> isLoggedIn() async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
return prefs.getBool('vote_logged_in') ?? false;
|
||
}
|
||
|
||
/// 获取用户信息
|
||
static Future<Map<String, dynamic>?> getUserInfo() async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
final userData = prefs.getString('vote_user');
|
||
if (userData != null) {
|
||
try {
|
||
return jsonDecode(userData) as Map<String, dynamic>;
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// 保存用户登录状态
|
||
static Future<void> saveUserLogin(Map<String, dynamic> userData) async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
await prefs.setBool('vote_logged_in', true);
|
||
await prefs.setString('vote_user', jsonEncode(userData));
|
||
}
|
||
|
||
/// 清除用户登录状态
|
||
static Future<void> clearUserLogin() async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
await prefs.remove('vote_logged_in');
|
||
await prefs.remove('vote_user');
|
||
}
|
||
|
||
/// 用户登录
|
||
static Future<VoteApiResponse> login({
|
||
required String username,
|
||
required String password,
|
||
}) async {
|
||
final jsonData = await _post(
|
||
_userEndpoint,
|
||
data: {'act': 'login', 'username': username, 'password': password},
|
||
);
|
||
|
||
if (jsonData['code'] != 0) {
|
||
throw Exception(jsonData['msg'] ?? '登录失败');
|
||
}
|
||
|
||
final userData = jsonData['data'];
|
||
if (userData != null) {
|
||
await saveUserLogin(userData);
|
||
}
|
||
|
||
return VoteApiResponse.fromJson(jsonData);
|
||
}
|
||
|
||
/// 用户注册
|
||
static Future<VoteApiResponse> register({
|
||
required String username,
|
||
required String password,
|
||
String? userIdentifier,
|
||
}) async {
|
||
final data = <String, dynamic>{
|
||
'act': 'register',
|
||
'username': username,
|
||
'password': password,
|
||
};
|
||
if (userIdentifier != null) {
|
||
data['user_identifier'] = userIdentifier;
|
||
}
|
||
|
||
final jsonData = await _post(_userEndpoint, data: data);
|
||
|
||
if (jsonData['code'] != 0) {
|
||
throw Exception(jsonData['msg'] ?? '注册失败');
|
||
}
|
||
|
||
return VoteApiResponse.fromJson(jsonData);
|
||
}
|
||
|
||
/// 获取投票列表
|
||
static Future<VoteListResponse> getVoteList({
|
||
int page = 1,
|
||
int pageSize = 10,
|
||
int status = 1,
|
||
int type = -1,
|
||
}) async {
|
||
final jsonData = await _get(
|
||
_voteEndpoint,
|
||
queryParameters: {
|
||
'act': 'getVoteList',
|
||
'page': page.toString(),
|
||
'pageSize': pageSize.toString(),
|
||
'status': status.toString(),
|
||
'type': type.toString(),
|
||
},
|
||
);
|
||
|
||
if (jsonData['code'] != 0) {
|
||
throw Exception(jsonData['msg'] ?? '获取投票列表失败');
|
||
}
|
||
|
||
return VoteListResponse.fromJson(jsonData);
|
||
}
|
||
|
||
/// 获取投票详情
|
||
static Future<VoteDetailResponse> getVoteDetail(int id) async {
|
||
final jsonData = await _get(
|
||
_voteEndpoint,
|
||
queryParameters: {'act': 'getVoteDetail', 'id': id.toString()},
|
||
);
|
||
|
||
if (jsonData['code'] != 0) {
|
||
throw Exception(jsonData['msg'] ?? '获取投票详情失败');
|
||
}
|
||
|
||
return VoteDetailResponse.fromJson(jsonData);
|
||
}
|
||
|
||
/// 提交投票
|
||
static Future<VoteApiResponse> submitVote({
|
||
required int topicId,
|
||
required List<int> options,
|
||
}) async {
|
||
final jsonData = await _post(
|
||
_voteEndpoint,
|
||
data: {'act': 'submitVote', 'topic_id': topicId, 'options': options},
|
||
);
|
||
|
||
if (jsonData['code'] != 0) {
|
||
throw Exception(jsonData['msg'] ?? '提交投票失败');
|
||
}
|
||
|
||
return VoteApiResponse.fromJson(jsonData);
|
||
}
|
||
|
||
/// 获取投票结果
|
||
static Future<VoteResultResponse> getVoteResult(int id) async {
|
||
final jsonData = await _get(
|
||
_voteEndpoint,
|
||
queryParameters: {'act': 'getVoteResult', 'id': id.toString()},
|
||
);
|
||
|
||
if (jsonData['code'] != 0) {
|
||
throw Exception(jsonData['msg'] ?? '获取投票结果失败');
|
||
}
|
||
|
||
return VoteResultResponse.fromJson(jsonData);
|
||
}
|
||
|
||
/// 获取用户投票记录
|
||
static Future<VoteListResponse> getUserVotes({
|
||
int page = 1,
|
||
int pageSize = 10,
|
||
}) async {
|
||
final jsonData = await _get(
|
||
_voteEndpoint,
|
||
queryParameters: {
|
||
'act': 'getUserVotes',
|
||
'page': page.toString(),
|
||
'pageSize': pageSize.toString(),
|
||
},
|
||
);
|
||
|
||
if (jsonData['code'] != 0) {
|
||
throw Exception(jsonData['msg'] ?? '获取用户投票记录失败');
|
||
}
|
||
|
||
return VoteListResponse.fromJson(jsonData);
|
||
}
|
||
|
||
/// 获取设备类型作为user_identifier
|
||
static Future<String> getDeviceType() async {
|
||
final platform = Platform.instance;
|
||
|
||
bool isHarmonyOS = false;
|
||
String platformName = 'Unknown';
|
||
|
||
try {
|
||
final String osName = io.Platform.operatingSystem;
|
||
final String osVersion = io.Platform.operatingSystemVersion.toLowerCase();
|
||
|
||
if (osName == 'ohos' ||
|
||
osName == 'harmonyos' ||
|
||
osName == 'openharmony') {
|
||
platformName = 'HarmonyOS';
|
||
isHarmonyOS = true;
|
||
} else if (io.Platform.isAndroid) {
|
||
platformName = 'Android';
|
||
if (osVersion.contains('harmony') ||
|
||
osVersion.contains('ohos') ||
|
||
osVersion.contains('openharmony')) {
|
||
platformName = 'HarmonyOS';
|
||
isHarmonyOS = true;
|
||
}
|
||
} else if (io.Platform.isIOS) {
|
||
platformName = 'iOS';
|
||
} else if (io.Platform.isMacOS) {
|
||
platformName = 'macOS';
|
||
} else if (io.Platform.isWindows) {
|
||
platformName = 'Windows';
|
||
} else if (io.Platform.isLinux) {
|
||
platformName = 'Linux';
|
||
} else if (io.Platform.isFuchsia) {
|
||
platformName = 'Fuchsia';
|
||
} else {
|
||
platformName = osName[0].toUpperCase() + osName.substring(1);
|
||
}
|
||
} catch (e) {
|
||
platformName = switch (platform.operatingSystem) {
|
||
const OperatingSystem.android() => 'Android',
|
||
const OperatingSystem.fuchsia() => 'Fuchsia',
|
||
const OperatingSystem.iOS() => 'iOS',
|
||
const OperatingSystem.linux() => 'Linux',
|
||
const OperatingSystem.macOS() => 'macOS',
|
||
const OperatingSystem.windows() => 'Windows',
|
||
const OperatingSystem.unknown() || _ => 'Unknown',
|
||
};
|
||
}
|
||
|
||
String deviceType = 'Unknown';
|
||
if (isHarmonyOS) {
|
||
final String osName = io.Platform.operatingSystem;
|
||
if (osName == 'ohos') {
|
||
deviceType = 'OHOS';
|
||
} else if (osName == 'harmonyos') {
|
||
deviceType = 'HarmonyOS';
|
||
} else if (osName == 'openharmony') {
|
||
deviceType = 'OpenHarmony';
|
||
} else {
|
||
deviceType = 'HarmonyOS';
|
||
}
|
||
} else {
|
||
deviceType =
|
||
platform.when<String?>(
|
||
mobile: () => 'Mobile',
|
||
desktop: () => 'Desktop',
|
||
js: () => 'Web',
|
||
orElse: () => null,
|
||
) ??
|
||
'Unknown';
|
||
}
|
||
|
||
return '$platformName-$deviceType-Flutter';
|
||
}
|
||
}
|
||
|
||
/// 基础API响应
|
||
class VoteApiResponse {
|
||
final int code;
|
||
final String message;
|
||
final dynamic data;
|
||
|
||
VoteApiResponse({required this.code, required this.message, this.data});
|
||
|
||
factory VoteApiResponse.fromJson(Map<String, dynamic> json) {
|
||
return VoteApiResponse(
|
||
code: json['code'] ?? 0,
|
||
message: json['msg'] ?? '',
|
||
data: json['data'],
|
||
);
|
||
}
|
||
|
||
bool get isSuccess => code == 0;
|
||
}
|
||
|
||
/// 投票列表响应
|
||
class VoteListResponse {
|
||
final List<VoteItem> list;
|
||
final VotePagination pagination;
|
||
|
||
VoteListResponse({required this.list, required this.pagination});
|
||
|
||
factory VoteListResponse.fromJson(Map<String, dynamic> json) {
|
||
final data = json['data'] as Map<String, dynamic>? ?? {};
|
||
final listRaw = data['list'] as List? ?? [];
|
||
final list = listRaw.map((item) => VoteItem.fromJson(item)).toList();
|
||
final pagination = VotePagination.fromJson(
|
||
data['pagination'] as Map<String, dynamic>? ?? {},
|
||
);
|
||
return VoteListResponse(list: list, pagination: pagination);
|
||
}
|
||
}
|
||
|
||
/// 投票项
|
||
class VoteItem {
|
||
final int id;
|
||
final String title;
|
||
final String? idesc;
|
||
final int itype;
|
||
final int maxtime;
|
||
final int status;
|
||
final int iview;
|
||
final String addtime;
|
||
final String statime;
|
||
final String endtime;
|
||
final String? statusText;
|
||
final String? statusClass;
|
||
|
||
VoteItem({
|
||
required this.id,
|
||
required this.title,
|
||
this.idesc,
|
||
required this.itype,
|
||
required this.maxtime,
|
||
required this.status,
|
||
required this.iview,
|
||
required this.addtime,
|
||
required this.statime,
|
||
required this.endtime,
|
||
this.statusText,
|
||
this.statusClass,
|
||
});
|
||
|
||
factory VoteItem.fromJson(Map<String, dynamic> json) {
|
||
return VoteItem(
|
||
id: int.tryParse(json['id'].toString()) ?? 0,
|
||
title: json['title']?.toString() ?? '',
|
||
idesc: json['idesc']?.toString(),
|
||
itype: int.tryParse(json['itype'].toString()) ?? 0,
|
||
maxtime: int.tryParse(json['maxtime'].toString()) ?? 0,
|
||
status: int.tryParse(json['status'].toString()) ?? 0,
|
||
iview: int.tryParse(json['iview'].toString()) ?? 0,
|
||
addtime: json['addtime']?.toString() ?? '',
|
||
statime: json['statime']?.toString() ?? '',
|
||
endtime: json['endtime']?.toString() ?? '',
|
||
statusText: json['status_text']?.toString(),
|
||
statusClass: json['status_class']?.toString(),
|
||
);
|
||
}
|
||
|
||
bool get isSingleChoice => itype == 0;
|
||
bool get isMultipleChoice => itype == 1;
|
||
bool get isActive => status == 1;
|
||
}
|
||
|
||
/// 分页信息
|
||
class VotePagination {
|
||
final int total;
|
||
final int page;
|
||
final int pageSize;
|
||
final int totalPage;
|
||
final int offset;
|
||
|
||
VotePagination({
|
||
required this.total,
|
||
required this.page,
|
||
required this.pageSize,
|
||
required this.totalPage,
|
||
required this.offset,
|
||
});
|
||
|
||
factory VotePagination.fromJson(Map<String, dynamic> json) {
|
||
return VotePagination(
|
||
total: int.tryParse(json['total'].toString()) ?? 0,
|
||
page: int.tryParse(json['page'].toString()) ?? 1,
|
||
pageSize: int.tryParse(json['pageSize'].toString()) ?? 10,
|
||
totalPage: int.tryParse(json['totalPage'].toString()) ?? 1,
|
||
offset: int.tryParse(json['offset'].toString()) ?? 0,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 投票详情响应
|
||
class VoteDetailResponse {
|
||
final VoteItem vote;
|
||
final List<VoteOption> options;
|
||
final bool hasVoted;
|
||
final List<int> userVotes;
|
||
final bool canVote;
|
||
|
||
VoteDetailResponse({
|
||
required this.vote,
|
||
required this.options,
|
||
required this.hasVoted,
|
||
required this.userVotes,
|
||
required this.canVote,
|
||
});
|
||
|
||
factory VoteDetailResponse.fromJson(Map<String, dynamic> json) {
|
||
final data = json['data'] as Map<String, dynamic>? ?? {};
|
||
final voteRaw = data['vote'] as Map<String, dynamic>? ?? {};
|
||
final optionsRaw = data['options'] as List? ?? [];
|
||
final userVotesRaw = data['userVotes'] as List? ?? [];
|
||
|
||
return VoteDetailResponse(
|
||
vote: VoteItem.fromJson(voteRaw),
|
||
options: optionsRaw.map((item) => VoteOption.fromJson(item)).toList(),
|
||
hasVoted: data['hasVoted'] as bool? ?? false,
|
||
userVotes: userVotesRaw
|
||
.map((id) => int.tryParse(id.toString()) ?? 0)
|
||
.where((id) => id > 0)
|
||
.toList(),
|
||
canVote: data['canVote'] as bool? ?? true,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 投票选项
|
||
class VoteOption {
|
||
final int id;
|
||
final int topicId;
|
||
final String name;
|
||
final String? idesc;
|
||
final String? imgs;
|
||
final int sort;
|
||
|
||
VoteOption({
|
||
required this.id,
|
||
required this.topicId,
|
||
required this.name,
|
||
this.idesc,
|
||
this.imgs,
|
||
required this.sort,
|
||
});
|
||
|
||
factory VoteOption.fromJson(Map<String, dynamic> json) {
|
||
return VoteOption(
|
||
id: int.tryParse(json['id'].toString()) ?? 0,
|
||
topicId: int.tryParse(json['topic_id'].toString()) ?? 0,
|
||
name: json['name']?.toString() ?? '',
|
||
idesc: json['idesc']?.toString(),
|
||
imgs: json['imgs']?.toString(),
|
||
sort: int.tryParse(json['sort'].toString()) ?? 0,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 投票结果响应
|
||
class VoteResultResponse {
|
||
final VoteItem vote;
|
||
final List<VoteResultOption> options;
|
||
final int totalVotes;
|
||
final bool hasVoted;
|
||
final List<int> userVotes;
|
||
|
||
VoteResultResponse({
|
||
required this.vote,
|
||
required this.options,
|
||
required this.totalVotes,
|
||
required this.hasVoted,
|
||
required this.userVotes,
|
||
});
|
||
|
||
factory VoteResultResponse.fromJson(Map<String, dynamic> json) {
|
||
final data = json['data'] as Map<String, dynamic>? ?? {};
|
||
final voteRaw = data['vote'] as Map<String, dynamic>? ?? {};
|
||
final optionsRaw = data['options'] as List? ?? [];
|
||
final userVotesRaw = data['userVotes'] as List? ?? [];
|
||
|
||
return VoteResultResponse(
|
||
vote: VoteItem.fromJson(voteRaw),
|
||
options: optionsRaw
|
||
.map((item) => VoteResultOption.fromJson(item))
|
||
.toList(),
|
||
totalVotes: int.tryParse(data['totalVotes'].toString()) ?? 0,
|
||
hasVoted: data['hasVoted'] as bool? ?? false,
|
||
userVotes: userVotesRaw
|
||
.map((id) => int.tryParse(id.toString()) ?? 0)
|
||
.where((id) => id > 0)
|
||
.toList(),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 投票结果选项
|
||
class VoteResultOption {
|
||
final int id;
|
||
final String name;
|
||
final String? idesc;
|
||
final String? imgs;
|
||
final int count;
|
||
final int percentage;
|
||
|
||
VoteResultOption({
|
||
required this.id,
|
||
required this.name,
|
||
this.idesc,
|
||
this.imgs,
|
||
required this.count,
|
||
required this.percentage,
|
||
});
|
||
|
||
factory VoteResultOption.fromJson(Map<String, dynamic> json) {
|
||
return VoteResultOption(
|
||
id: int.tryParse(json['id'].toString()) ?? 0,
|
||
name: json['name']?.toString() ?? '',
|
||
idesc: json['idesc']?.toString(),
|
||
imgs: json['imgs']?.toString(),
|
||
count: int.tryParse(json['count'].toString()) ?? 0,
|
||
percentage: int.tryParse(json['percentage'].toString()) ?? 0,
|
||
);
|
||
}
|
||
}
|