ver1.00.00

bindQR
fix
This commit is contained in:
spasolreisa
2026-04-19 23:31:52 +08:00
parent c5c3f7c8f5
commit b985cd1f9e
4 changed files with 166 additions and 200 deletions

View File

@@ -3,6 +3,7 @@ import 'package:unionapp/pages/music/music_page.dart';
import '../../service/recommendation_helper.dart'; import '../../service/recommendation_helper.dart';
import '../../service/song_service.dart'; import '../../service/song_service.dart';
import '../../tool/gradientText.dart'; import '../../tool/gradientText.dart';
import '../score/updateScorePage.dart';
import '../user/userpage.dart'; import '../user/userpage.dart';
import '../scorelist.dart'; import '../scorelist.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -182,7 +183,7 @@ class HomePage extends StatelessWidget {
const SizedBox(width: 4), const SizedBox(width: 4),
Expanded( Expanded(
flex: 6, flex: 6,
child: _buildQuickActionButtons(), // 快捷按钮组件 child: _buildQuickActionButtons(context), // 快捷按钮组件
), ),
const SizedBox(width: 10), // 左右间距 const SizedBox(width: 10), // 左右间距
], ],
@@ -195,7 +196,7 @@ class HomePage extends StatelessWidget {
); );
} }
// 右侧快捷按钮区域(你可以自由修改图标、文字、点击事件) // 右侧快捷按钮区域(你可以自由修改图标、文字、点击事件)
Widget _buildQuickActionButtons() { Widget _buildQuickActionButtons(BuildContext context) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -225,7 +226,12 @@ class HomePage extends StatelessWidget {
end: Alignment.bottomRight, end: Alignment.bottomRight,
), ),
color: Colors.pinkAccent.shade100, color: Colors.pinkAccent.shade100,
onTap: () {}, onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const BindAccountPage()),
);
},
), ),
_quickActionItem( _quickActionItem(
icon: Icons.stars_outlined, icon: Icons.stars_outlined,

View File

@@ -7,29 +7,29 @@ import 'package:provider/provider.dart';
import '../../providers/user_provider.dart'; import '../../providers/user_provider.dart';
class UpdateScorePage extends StatefulWidget { class BindAccountPage extends StatefulWidget {
const UpdateScorePage({super.key}); const BindAccountPage({super.key});
@override @override
State<UpdateScorePage> createState() => _UpdateScorePageState(); State<BindAccountPage> createState() => _BindAccountPageState();
} }
class _UpdateScorePageState extends State<UpdateScorePage> { class _BindAccountPageState extends State<BindAccountPage> {
final TextEditingController _segaIdController = TextEditingController(); final TextEditingController _qrContentController = TextEditingController();
final ImagePicker _picker = ImagePicker(); final ImagePicker _picker = ImagePicker();
bool _isProcessing = false; bool _isProcessing = false;
String? _statusMessage; String? _statusMessage;
File? _selectedImageFile; // 用于预览 File? _selectedImageFile; // 用户选择的原始二维码截图
// 条码扫描器 (支持 QR Code, Data Matrix, Aztec 等) // 条码扫描器
final BarcodeScanner _barcodeScanner = BarcodeScanner( final BarcodeScanner _barcodeScanner = BarcodeScanner(
formats: [BarcodeFormat.qrCode, BarcodeFormat.dataMatrix, BarcodeFormat.aztec], formats: [BarcodeFormat.qrCode],
); );
@override @override
void dispose() { void dispose() {
_segaIdController.dispose(); _qrContentController.dispose();
_barcodeScanner.close(); _barcodeScanner.close();
super.dispose(); super.dispose();
} }
@@ -46,7 +46,6 @@ class _UpdateScorePageState extends State<UpdateScorePage> {
_selectedImageFile = File(pickedFile.path); _selectedImageFile = File(pickedFile.path);
}); });
// 执行二维码识别
final inputImage = InputImage.fromFilePath(pickedFile.path); final inputImage = InputImage.fromFilePath(pickedFile.path);
final List<Barcode> barcodes = await _barcodeScanner.processImage(inputImage); final List<Barcode> barcodes = await _barcodeScanner.processImage(inputImage);
@@ -54,23 +53,15 @@ class _UpdateScorePageState extends State<UpdateScorePage> {
_isProcessing = false; _isProcessing = false;
}); });
if (barcodes.isNotEmpty) { if (barcodes.isNotEmpty && barcodes.first.rawValue != null) {
// 取第一个识别到的二维码内容 final String code = barcodes.first.rawValue!;
final String code = barcodes.first.rawValue ?? "";
if (code.isNotEmpty) {
setState(() { setState(() {
_segaIdController.text = code; _qrContentController.text = code;
_statusMessage = "识别成功: $code"; _statusMessage = "识别成功";
}); });
} else { } else {
setState(() { setState(() {
_statusMessage = "二维码内容为空"; _statusMessage = "未识别到二维码,请手动输入";
});
}
} else {
setState(() {
_statusMessage = "未在图中发现二维码,请手动输入";
}); });
} }
@@ -82,13 +73,13 @@ class _UpdateScorePageState extends State<UpdateScorePage> {
} }
} }
// 2. 执行隐写上传 // 2. 执行隐写上传 (触发后端绑定/登录逻辑)
Future<void> _handleUpload() async { Future<void> _handleBind() async {
final segaId = _segaIdController.text.trim(); final qrContent = _qrContentController.text.trim();
if (segaId.isEmpty) { if (qrContent.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("请输入或扫描二维码"), backgroundColor: Colors.orange), const SnackBar(content: Text("请输入或扫描二维码内容"), backgroundColor: Colors.orange),
); );
return; return;
} }
@@ -97,58 +88,54 @@ class _UpdateScorePageState extends State<UpdateScorePage> {
setState(() { setState(() {
_isProcessing = true; _isProcessing = true;
_statusMessage = "正在准备图片..."; _statusMessage = "正在处理图片...";
}); });
try { try {
// 策略:优先使用用户选择的截图作为载体。
// 如果用户没选图,或者选图后删除了,我们可以下载默认背景图作为载体。
if (_selectedImageFile != null) {
imageBytes = await _selectedImageFile!.readAsBytes();
} else {
// 下载默认背景图作为隐写载体
final httpClient = HttpClient(); final httpClient = HttpClient();
final request = await httpClient.getUrl(Uri.parse('https://union.godserver.cn/assets/jpeg/20180621142015_5FmGZ-wYXkyL4y.jpeg')); final request = await httpClient.getUrl(Uri.parse('https://union.godserver.cn/assets/jpeg/20180621142015_5FmGZ-wYXkyL4y.jpeg'));
final response = await request.close(); final response = await request.close();
// 辅助函数:将 HttpClientResponse 转为 Uint8List
final bytes = <int>[]; final bytes = <int>[];
await for (final chunk in response) { await for (final chunk in response) {
bytes.addAll(chunk); bytes.addAll(chunk);
} }
imageBytes = Uint8List.fromList(bytes); imageBytes = Uint8List.fromList(bytes);
}
if (imageBytes == null || imageBytes.isEmpty) { if (imageBytes == null || imageBytes.isEmpty) throw Exception("图片数据无效");
throw Exception("图片数据无效");
}
setState(() { setState(() {
_statusMessage = "正在隐写并上传..."; _statusMessage = "正在提交绑定请求...";
}); });
// 调用 Provider 上传 // 调用 Provider
// 注意:这里 segaId 参数其实传的是 QR Code 的内容
final userProvider = context.read<UserProvider>(); final userProvider = context.read<UserProvider>();
final result = await userProvider.uploadStegImage(imageBytes, segaId: segaId); final result = await userProvider.uploadStegImage(imageBytes, segaId: qrContent);
setState(() { setState(() {
_isProcessing = false; _isProcessing = false;
_statusMessage = "上传成功!";
}); });
// 处理后端返回
if (mounted) { if (mounted) {
if (result['code'] == 200) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("上传成功: ${result['msg']}"), backgroundColor: Colors.green), SnackBar(content: Text(" ${result['msg']}"), backgroundColor: Colors.green, duration: const Duration(seconds: 5)),
); );
// 可选:清空表单 // 绑定成功后,通常建议刷新用户信息
// _segaIdController.clear(); await userProvider.initUser();
// _selectedImageFile = null; } else {
// 后端返回 500 或其他错误
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("${result['msg']}"), backgroundColor: Colors.red, duration: const Duration(seconds: 5)),
);
}
} }
} catch (e) { } catch (e) {
setState(() { setState(() {
_isProcessing = false; _isProcessing = false;
_statusMessage = "上传失败: $e"; _statusMessage = "网络或处理错误: $e";
}); });
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@@ -161,10 +148,15 @@ class _UpdateScorePageState extends State<UpdateScorePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(
title: const Text("绑定 / 刷新 Sega 账号"),
backgroundColor: Colors.black.withOpacity(0.5),
elevation: 0,
),
body: Stack( body: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
// 1. 背景图 // 背景图
Image.network( Image.network(
'https://union.godserver.cn/assets/jpeg/20180621142015_5FmGZ-wYXkyL4y.jpeg', 'https://union.godserver.cn/assets/jpeg/20180621142015_5FmGZ-wYXkyL4y.jpeg',
fit: BoxFit.cover, fit: BoxFit.cover,
@@ -172,15 +164,13 @@ class _UpdateScorePageState extends State<UpdateScorePage> {
if (loadingProgress == null) return child; if (loadingProgress == null) return child;
return Container(color: Colors.black); return Container(color: Colors.black);
}, },
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) => Container(color: Colors.grey[900]),
return Container(color: Colors.grey[900]);
},
), ),
// 2. 深色遮罩 // 遮罩
Container(color: Colors.black.withOpacity(0.7)), Container(color: Colors.black.withOpacity(0.75)),
// 3. 主体内容 // 内容
SafeArea( SafeArea(
child: Center( child: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -188,37 +178,39 @@ class _UpdateScorePageState extends State<UpdateScorePage> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// 标题 const Icon(Icons.qr_code_scanner, size: 60, color: Colors.white70),
const SizedBox(height: 16),
const Text( const Text(
"更新成绩 / 二维码", "请提供 Sega 登录二维码",
style: TextStyle( style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold),
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
), ),
const SizedBox(height: 8),
const Text(
"支持识别截图或手动输入二维码内容\n用于绑定账号或刷新登录状态",
style: TextStyle(fontSize: 14, color: Colors.white60),
textAlign: TextAlign.center,
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
// 卡片容器 // 输入卡片
Container( Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15), color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withOpacity(0.2)), border: Border.all(color: Colors.white.withOpacity(0.2)),
), ),
child: Column( child: Column(
children: [ children: [
// 输入框
TextField( TextField(
controller: _segaIdController, controller: _qrContentController,
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
maxLines: 10,
decoration: InputDecoration( decoration: InputDecoration(
labelText: "Sega ID / 二维码内容", labelText: "二维码内容 (QR Data)",
labelStyle: const TextStyle(color: Colors.white70), labelStyle: const TextStyle(color: Colors.white70),
hintText: "手动输入或点击下方按钮识别", hintText: "例如: SGWCMAID...",
hintStyle: const TextStyle(color: Colors.white38), hintStyle: const TextStyle(color: Colors.white38),
prefixIcon: const Icon(Icons.qr_code, color: Colors.white70),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.white38), borderSide: const BorderSide(color: Colors.white38),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@@ -232,60 +224,42 @@ class _UpdateScorePageState extends State<UpdateScorePage> {
const SizedBox(height: 20), const SizedBox(height: 20),
// 图片预览区域 (如果有) // 预览
if (_selectedImageFile != null) if (_selectedImageFile != null)
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Image.file( child: Image.file(_selectedImageFile!, height: 120, fit: BoxFit.contain),
_selectedImageFile!,
height: 150,
fit: BoxFit.contain,
),
), ),
if (_selectedImageFile != null) const SizedBox(height: 15), if (_selectedImageFile != null) const SizedBox(height: 15),
// 操作按钮组 // 按钮组
Row( Row(
children: [ children: [
// 识别二维码按钮
Expanded( Expanded(
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: _isProcessing ? null : _pickImageAndScan, onPressed: _isProcessing ? null : _pickImageAndScan,
icon: const Icon(Icons.camera_alt), icon: const Icon(Icons.image_search),
label: const Text("识别截图"), label: const Text("识别截图"),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent, backgroundColor: Colors.blueAccent,
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
), ),
), ),
), ),
), const SizedBox(width: 12),
const SizedBox(width: 10),
// 上传按钮
Expanded( Expanded(
flex: 1,
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: _isProcessing ? null : _handleUpload, onPressed: _isProcessing ? null : _handleBind,
icon: _isProcessing icon: _isProcessing
? const SizedBox( ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
width: 16, height: 16, : const Icon(Icons.login),
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), label: Text(_isProcessing ? "处理中" : "绑定/刷新"),
)
: const Icon(Icons.cloud_upload),
label: Text(_isProcessing ? "处理中..." : "上传"),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.green, backgroundColor: Colors.green,
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
), ),
), ),
), ),
@@ -295,17 +269,14 @@ class _UpdateScorePageState extends State<UpdateScorePage> {
), ),
), ),
// 状态提示 // 状态消息
if (_statusMessage != null) if (_statusMessage != null)
Padding( Padding(
padding: const EdgeInsets.only(top: 20), padding: const EdgeInsets.only(top: 20),
child: Text( child: Text(
_statusMessage!, _statusMessage!,
style: TextStyle( style: TextStyle(
color: _statusMessage!.contains("失败") || _statusMessage!.contains("错误") color: _statusMessage!.contains("失败") || _statusMessage!.contains("错误") ? Colors.redAccent : Colors.white70,
? Colors.redAccent
: Colors.white70,
fontSize: 14,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),

View File

@@ -1,114 +1,104 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:image/image.dart' as img; import 'package:image/image.dart' as img;
import '../tool/encryption_util.dart';
class SteganographyUtil { class SteganographyUtil {
static const String MAGIC_HEADER = 'SGWCMAID'; static const String MAGIC_HEADER = 'SGWCMAID';
/// 图片隐写:将数据藏进图片 R 通道最低位 /// 图片隐写:写入数据
static Uint8List hideDataInImage(Uint8List imageBytes, String secretData) { static Uint8List hideDataInImage(Uint8List imageBytes, String secretData) {
if (!secretData.startsWith(MAGIC_HEADER)) { // 1. 解码图片
throw Exception('数据格式不正确,必须以 $MAGIC_HEADER 开头');
}
// 1. 加密 + 压缩
final encryptedString = EncryptionUtil.encryptAndCompress(secretData);
// 2. 转为字节并添加 \0 结束符
final dataBytes = utf8.encode(encryptedString);
final payload = Uint8List(dataBytes.length + 1);
payload.setRange(0, dataBytes.length, dataBytes);
payload[dataBytes.length] = 0; // \0
// 3. 解码图片
final image = img.decodeImage(imageBytes); final image = img.decodeImage(imageBytes);
if (image == null) throw Exception('图片解码失败'); if (image == null) throw Exception('图片解码失败');
// 4. 获取像素数据 // 2. 准备数据:必须以 MAGIC_HEADER 开头,并以 \0 结尾
final imageData = image.data; final dataBytes = utf8.encode(secretData);
if (imageData == null) throw Exception('图片数据为空'); final payload = Uint8List(dataBytes.length + 1);
payload.setRange(0, dataBytes.length, dataBytes);
payload[dataBytes.length] = 0; // 终止符
// 【关键修复】ByteBuffer 必须转换为 Uint8List 才能进行 [] 操作和获取 length // 检查容量
// 注意asUint8List() 返回的是原 buffer 的视图,修改它会直接修改 image 数据 if (payload.length * 8 > image.width * image.height) {
final Uint8List pixels = imageData.buffer.asUint8List(); throw Exception('图片像素不足以容纳数据');
// 检查容量 (R通道只有 width * height 个字节可用)
// pixels.length 是总字节数 (width * height * 4)
final maxBits = pixels.length ~/ 4;
if (payload.length * 8 > maxBits) {
throw Exception('图片容量不足!需要 ${payload.length * 8} bits图片仅提供 ${maxBits} bits');
} }
// 5. 执行 LSB 隐写 int payloadIndex = 0;
_writeLsbRChannel(pixels, payload); int bitIndex = 7; // MSB First (最高位优先)
// 6. 导出 PNG // ✅ 修复:必须严格按照 Java 的 (y, x) 顺序遍历,确保坐标对齐
return img.encodePng(image); bool finished = false;
for (int y = 0; y < image.height; y++) {
for (int x = 0; x < image.width; x++) {
if (payloadIndex >= payload.length) {
finished = true;
break;
} }
/// 从图片中提取数据 // 获取当前坐标的像素
final pixel = image.getPixel(x, y);
// 取出当前字节的特定位
int currentByte = payload[payloadIndex];
int bit = (currentByte >> bitIndex) & 1;
// ✅ 修复:通过 red 分量操作屏蔽不同图片格式RGBA/BGRA的底层差异
int r = pixel.r.toInt();
int g = pixel.g.toInt();
int b = pixel.b.toInt();
int a = pixel.a.toInt();
// 修改 R 通道最低位
r = (r & 0xFE) | bit;
// 写回像素(保持其他通道不变)
image.setPixel(x, y, img.ColorRgba8(r, g, b, a));
bitIndex--;
if (bitIndex < 0) {
bitIndex = 7;
payloadIndex++;
}
}
if (finished) break;
}
// ✅ 必须导出为 PNG防止 JPEG 压缩破坏 LSB 位
return Uint8List.fromList(img.encodePng(image));
}
/// 提取数据(本地调试用,逻辑需与写入完全对称)
static String? extractDataFromImage(Uint8List imageBytes) { static String? extractDataFromImage(Uint8List imageBytes) {
final image = img.decodeImage(imageBytes); final image = img.decodeImage(imageBytes);
if (image == null) return null; if (image == null) return null;
final imageData = image.data; List<int> extractedBytes = [];
if (imageData == null) return null; int currentByte = 0;
// 【关键修复】转换为 Uint8List
final Uint8List pixels = imageData.buffer.asUint8List();
final List<int> extractedBytes = [];
int currentByteValue = 0;
int bitCount = 0; int bitCount = 0;
// 遍历像素 (RGBA, 步长 4) outer:
for (int i = 0; i < pixels.length; i += 4) { for (int y = 0; y < image.height; y++) {
// 只取 R 通道 (index i) 的最低位 for (int x = 0; x < image.width; x++) {
int bit = pixels[i] & 1; final pixel = image.getPixel(x, y);
int bit = pixel.r.toInt() & 1;
currentByteValue = (currentByteValue << 1) | bit; // MSB First 组装字节
currentByte = (currentByte << 1) | bit;
bitCount++; bitCount++;
if (bitCount == 8) { if (bitCount == 8) {
if (currentByteValue == 0) { if (currentByte == 0) break outer; // 结束符
break; // 遇到 \0 停止 extractedBytes.add(currentByte);
} currentByte = 0;
extractedBytes.add(currentByteValue);
currentByteValue = 0;
bitCount = 0; bitCount = 0;
} }
} }
}
if (extractedBytes.isEmpty) return null; if (extractedBytes.isEmpty) return null;
try { try {
return utf8.decode(extractedBytes); return utf8.decode(extractedBytes);
} catch (e) { } catch (e) {
return null; return null;
} }
} }
/// 核心写入:仅修改 R 通道
static void _writeLsbRChannel(Uint8List pixels, Uint8List payload) {
int payloadIndex = 0;
int bitIndex = 0; // 0-7
for (int i = 0; i < pixels.length; i += 4) {
if (payloadIndex >= payload.length) break;
int currentByte = payload[payloadIndex];
// 取当前字节的第 bitIndex 位 (从高位到低位)
int bit = (currentByte >> (7 - bitIndex)) & 1;
// 修改 R 通道: 清除最低位,写入新位
pixels[i] = (pixels[i] & 0xFE) | bit;
bitIndex++;
if (bitIndex > 7) {
bitIndex = 0;
payloadIndex++;
}
}
}
} }

View File

@@ -350,7 +350,8 @@ class UserService {
}) async { }) async {
try { try {
// 1. 拼接隐写数据SGWCMAID 开头) // 1. 拼接隐写数据SGWCMAID 开头)
final secretData = 'SGWCMAID$segaId'; final secretData = '$segaId';
print(secretData);
// 2. 执行隐写 + 加密 // 2. 执行隐写 + 加密
// 使用 Uint8List.fromList 确保类型兼容,避免 typed_data_patch 错误 // 使用 Uint8List.fromList 确保类型兼容,避免 typed_data_patch 错误
@@ -378,14 +379,11 @@ class UserService {
// 5. 【核心上传】隐写图片 // 5. 【核心上传】隐写图片
final res = await _dio.post( final res = await _dio.post(
'$baseUrl/api/sega/steg', '$baseUrl/api/sega/phone',
data: formData, data: formData,
options: Options( options: Options(
headers: { headers: {
"Authorization": token, "Authorization": token,},
// Dio 会自动处理 multipart/form-data 的 boundary通常不需要手动指定 Content-Type
// 但为了严格对齐 JS如果后端强校验可以保留不过 Dio 推荐让它在 FormData 时自动设置
},
), ),
); );
@@ -405,6 +403,7 @@ class UserService {
options: Options(headers: {"Authorization": token}), options: Options(headers: {"Authorization": token}),
); );
print(res.data);
// 返回最终结果 // 返回最终结果
if (res.data is Map) { if (res.data is Map) {
return Map<String, dynamic>.from(res.data); return Map<String, dynamic>.from(res.data);