flutter 文件上傳組件和大文件分片上傳

文件分片上傳

資料

https://www.cnblogs.com/caijinglong/p/11558389.html

使用分段上傳來上傳和復制對象 - Amazon Simple Storage Service

因為公司使用的是亞馬遜的s3桶? 下面是查閱資料獲得的

亞馬遜s3桶的文件上傳分片

分段上分為三個步驟:開始上傳、上傳對象分段,以及在上傳所有分段后完成分段上傳。在收到完成分段上傳請求后,Amazon S3 會利用上傳的分段創建對象,然后您可以像在您的存儲桶中訪問任何其他對象一樣訪問該對象。

您可以列出所有正在執行的分段上傳,或者獲取為特定分段上傳操作上傳的分段列表。以上每個操作都在本節中進行了說明。

分段上傳開始

當您發送請求以開始分段上傳時,Amazon S3 將返回具有上傳 ID 的響應,此 ID 是分段上傳的唯一標識符。無論您何時上傳分段、列出分段、完成上傳或停止上傳,您都必須包括此上傳 ID。如果您想要提供描述已上傳的對象的任何元數據,必須在請求中提供它以開始分段上傳。

分段上傳

上傳分段時,除了指定上傳 ID,還必須指定分段編號。您可以選擇 1 和 10000 之間的任意分段編號。分段編號在您正在上傳的對象中唯一地識別分段及其位置。您選擇的分段編號不必是連續序列(例如,它可以是 1、5 和 14)。如果您使用之前上傳的分段的同一分段編號上傳新分段,則之前上傳的分段將被覆蓋。

無論您何時上傳分段,Amazon S3 都將在其響應中返回實體標簽 (ETag)?標頭。對于每個分段上傳,您必須記錄分段編號和 ETag 值。所有對象分段上傳的 ETag 值將保持不變,但將為每個分段分配不同的分段號。您必須在隨后的請求中包括這些值以完成分段上傳。

分段上傳完成

完成分段上傳時,Amazon S3 通過按升序的分段編號規范化分段來創建對象。如果在開始分段上傳請求中提供了任何對象元數據,則 Amazon S3 會將該元數據與對象相關聯。成功完成請求后,分段將不再存在。

完成分段上傳請求必須包括上傳 ID 以及分段編號和相應的 ETag 值的列表。Amazon S3 響應包括可唯一地識別組合對象數據的 ETag。此 ETag 無需成為對象數據的 MD5 哈希。

文件分片基本原理

前端使用插件獲取到本地選擇的文件,判斷文件的大小,超過設置的限制數,就進行大文件分片上傳邏輯

第一步

進行文件分片 下面這個方法 返回的是大文件分片后的開始索引和結束索引

 List<List<int>> sliceFileIntoChunks(int fileSize, int sliceMinSize, int sliceMaxCount) {List<List<int>> slices = [];int start = 0;while (start < fileSize) {int end = start + sliceMinSize;if (end > fileSize || slices.length + 1 >= sliceMaxCount) {end = fileSize;}slices.add([start, end]);start = end;}return slices;}

第二步

將分片信息 生成新的配置對象 配置對象會導出 分片的json

List<SliceChunkItem> config = await utils.getJsonFromSplitFileIntoChunks();
// 切片項
class SliceChunkItem {// 切片所在文件的位置final int start;final int end;final int partNumber;String uploadId = "";String tag = "";String checksum = "";List<int> fileBytes = [];MultipartFile? multipartFile;SliceChunkItem({required this.start,required this.end,required this.partNumber,});setUploadId(id) {uploadId = id;}setTag(id) {tag = id;}Map<String, dynamic> toJson() {return {"partNumber": partNumber,"tag": tag,"checksum": checksum,};}List<int> toClunk() {return fileBytes;}
}

第三步

接口發送,分為三個接口,第一個為初始化接口、第二個為分片上傳接口、第三個為文件合成接口(因為需求原因 希望存的是一個完整的文件,且不做分片下載功能)

第一個接口 傳遞了文件名 和加密的類型 因為是亞馬遜?hashMethod= SHA1

String fileName = file.path.split('/').last;
  final aa = await multipartUploadInit(fileName: fileName, checksumType: FileUtils.hashMethod);

第二個接口 因為需要并發去發分片?

分片使用FormData進行存 其他信息也加在FormData中

static multipartUpload({required FormData formData,}) async {final res = await dio.post(Url.multipartUpload,data: formData,options: Options(method: "post",contentType: "multipart/form-data",sendTimeout: const Duration(days: 5),receiveTimeout: const Duration(days: 5),),);return res.data;}
  // 同時對分片進行并發Future sendItems({required List<SliceChunkItem> config,required int concurrentLimit,required Function(SliceChunkItem) callback,}) async {return await Future.wait(config.map((item) async {// 判斷是否需要取消請求if (cancelCompleter.isCompleted) {throw 'Requests are cancelled';}// 控制并發數量while (count >= concurrentLimit) {await Future.delayed(const Duration(milliseconds: 100));}count++;try {await callback(item);} finally {count--;}}));}

使用

    await utils.sendItems(config: config,concurrentLimit: 5,callback: (item) async {item.setUploadId(uploadId);"[分片上傳] bb 開始上傳 partNumber ${item.partNumber} ".w();var fileBytes = await utils.getRange(item.start, item.end);item.checksum = utils.calculateSHA1FormList(fileBytes);// 直接傳遞數組fileBytes 給dio 會導致內存崩潰formData = FormData.fromMap({'file': MultipartFile.fromBytes(fileBytes, filename: "11"),'partNumber': item.partNumber,'checksum': item.checksum,'uploadId': item.uploadId,});final b = await multipartUpload(formData: formData);finalUploadSliceCount++;onSendProgress?.call(finalUploadSliceCount, config.length);"[分片上傳] bb 結束上傳 partNumber ${item.partNumber} $b".w();String tag = b["data"]["tag"];item.setTag(tag);});

第三個接口

  final cc = await multipartUploadComplete(// checksum: checksum,uploadId: uploadId,partList: config.map((e) => e.toJson()).toList(),);

大文件加載到內存問題

1、讀大文件的時候

去拿文件的句柄 然后通過移動獲取不同的文件段

file.openRead();

2、讀取后存儲問題

如果將獲取的分片數據直接存到一個類里面,這樣的操作會導致內存被撐爆,

必須發送接口的時候再進行 文件指針方式進行文件數據讀取 然后發送接口后直接釋放

所有分片上傳的接口 做了 讀取大文件分片數據的邏輯操作

3、對大文件分片 進行哈希計算

錯誤代碼示范

 // ShA 1 進行文件哈希Future<String> calculateSHA1() async {if (await file.exists()) {List<int> contents = await file.readAsBytes();Digest sha1Result = sha1.convert(contents);return sha1Result.toString();} else {throw const FileSystemException('File not found');}}

修改后的代碼 (對內存基本沒影響)

 Future<String> calculateSHA1() async {if (await file.exists()) {Digest value = await sha1.bind(file.openRead()).first;return value.toString();} else {throw const FileSystemException('File not found');}}

出現的問題 (就是內存被撐爆的原因)

E/DartVM (24105): Exhausted heap space, trying to allocate 67108872 bytes. E/flutter (24105): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Out of Memory

出現問題的地方 (分片完成后 直接加載每個分片到內存中了 所有導致內容崩潰)

如下面代碼 將獲得的分片數據 存在了內存中 ,如果文件過大 就會被撐爆

Future<List<SliceChunkItem>> getJsonFromSplitFileIntoChunks() async {List<SliceChunkItem> sliceChunkList = [];// int i = 0;int partNumber = 1;// List<int> chunks = splitFileIntoChunks();// for (var v in chunks) {//   sliceChunkList.add(//     SliceChunkItem(//       start: i,//       end: v + i,//       fileBytes: await getRange(i, v + i),//       partNumber: partNumber,//     ),//   );//   i = v;//   partNumber++;// }List<List<int>> chunks =sliceFileIntoChunks(file.lengthSync(), sliceMinSize, sliceMaxCount);"[分片上傳] ${chunks.length}".w();for (List<int> v in chunks) {sliceChunkList.add(SliceChunkItem(start: v[0],end: v[1],fileBytes: await getRange(v[0], v[1]),partNumber: partNumber,),);partNumber++;}return sliceChunkList;}

修改如下

發送的時候 再去進行獲取?fileBytes 和 checksum;

完整代碼如下

大文件上傳工具類

import 'dart:async';
import 'dart:convert';
import 'dart:io';import 'package:LS/common/extension/custom_ext.dart';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';class MyCompleter<T> {MyCompleter();Completer<T> completer = Completer();Future<T> get future => completer.future;void reply(T result) {if (!completer.isCompleted) {completer.complete(result);}}
}class FileUtils {File file;// 文件哈希String hash = "";static String hashMethod = "SHA1";// 每個切片最小的大小int sliceMinSize = 1024 * 1024 * 10; // 10MB// 最大的切片數量int sliceMaxCount = 10000;// 限制并發數量的計數器int count = 0;// 用于取消請求的Completerfinal cancelCompleter = Completer<void>();FileUtils(this.file) {// 后端改動不需要l// // 默認后臺進行文件求哈希// backstageCalculateSHA1();}// 讀取文件的某個范圍返回Future<List<int>> getRange(int start, int end) async {if (start < 0) {throw RangeError.range(start, 0, file.lengthSync());}if (end > file.lengthSync()) {throw RangeError.range(end, 0, file.lengthSync());}final c = MyCompleter<List<int>>();List<int> result = [];file.openRead(start, end).listen((data) {result.addAll(data);}).onDone(() {c.reply(result);});return c.future;}Stream<List<int>> getRangeStream(int start, int end) {if (start < 0) {throw RangeError.range(start, 0, file.lengthSync());}if (end > file.lengthSync()) {throw RangeError.range(end, 0, file.lengthSync());}return file.openRead(start, end);}// 讀取文件的前n個字節返回List<int> splitFileIntoChunks() {final size = file.lengthSync();int chunkSize = size ~/ sliceMaxCount;chunkSize = chunkSize < sliceMinSize ? sliceMinSize : chunkSize;List<int> chunkSizes = [];int currentPosition = 0;while (currentPosition < size) {int remainingSize = size - currentPosition;int currentChunkSize =remainingSize > chunkSize ? chunkSize : remainingSize;chunkSizes.add(currentChunkSize);currentPosition += currentChunkSize;}return chunkSizes;}List<List<int>> sliceFileIntoChunks(int fileSize, int sliceMinSize, int sliceMaxCount) {List<List<int>> slices = [];int start = 0;while (start < fileSize) {int end = start + sliceMinSize;if (end > fileSize || slices.length + 1 >= sliceMaxCount) {end = fileSize;}slices.add([start, end]);start = end;}return slices;}Future<List<SliceChunkItem>> getJsonFromSplitFileIntoChunks() async {List<SliceChunkItem> sliceChunkList = [];int partNumber = 1;List<List<int>> chunks =sliceFileIntoChunks(file.lengthSync(), sliceMinSize, sliceMaxCount);"[分片上傳] 當前分片 ${chunks.length}".w();for (List<int> v in chunks) {sliceChunkList.add(SliceChunkItem(start: v[0],end: v[1],partNumber: partNumber,),);partNumber++;}return sliceChunkList;}// 將M 單位轉為基本單位字節static int mToSize(int m) {return 1024 * 1024 * m;}// ShA 1 進行文件哈希Future<String> calculateSHA1(Stream<List<int>> stream) async {Digest digest = await sha1.bind(stream).first;return base64.encode(digest.bytes);}// 將數組數據重新組合成文件////// 測試使用 將分片合成一個文件 寫到本地// String appDocDir = (await getDownloadsDirectory())?.path ?? "";// String filePath = '$appDocDir/new.zip';// await FileUtils.mergeChunksIntoFile(//     config.map((e) => e.toClunk()).toList(), filePath);static Future<void> mergeChunksIntoFile(List<List<int>> chunks, String outputPath) async {File outputFile = File(outputPath);outputFile.createSync();IOSink output = outputFile.openWrite(mode: FileMode.writeOnlyAppend);"將數組數據重新組合成文件 a".w();for (List<int> chunk in chunks) {output.add(chunk);"將數組數據重新組合成文件 b".w();}"將數組數據重新組合成文件 c".w();await output.close();}String calculateSHA1FormList(List<int> data) {Digest digest = sha1.convert(data);return base64.encode(digest.bytes);}static String staticCalculateSHA1FormList(List<int> data) {Digest digest = sha1.convert(data);return base64.encode(digest.bytes);}// 后臺進行對文件的哈希// backstageCalculateSHA1() async {//   hash = await calculateSHA1();//   return hash;// }// 同時對分片進行并發Future sendItems({required List<SliceChunkItem> config,required int concurrentLimit,required Function(SliceChunkItem) callback,}) async {return await Future.wait(config.map((item) async {// 判斷是否需要取消請求if (cancelCompleter.isCompleted) {throw 'Requests are cancelled';}// 控制并發數量while (count >= concurrentLimit) {await Future.delayed(const Duration(milliseconds: 100));}count++;try {await callback(item);} finally {count--;}}));}// 取消所有請求cancelSendItems() {if (!cancelCompleter.isCompleted) {cancelCompleter.complete();}count = 0;"[切片上傳] 取消并發成功".w();}
}// 切片項
class SliceChunkItem {// 切片所在文件的位置final int start;final int end;final int partNumber;String uploadId = "";String tag = "";String checksum = "";List<int> fileBytes = [];MultipartFile? multipartFile;SliceChunkItem({required this.start,required this.end,required this.partNumber,});setUploadId(id) {uploadId = id;}setTag(id) {tag = id;}Map<String, dynamic> toJson() {return {"partNumber": partNumber,"tag": tag,"checksum": checksum,};}List<int> toClunk() {return fileBytes;}
}

分片上傳總接口

  // 文件分片上傳static uploadSliceFile(String path, {ProgressCallback? onSendProgress,required Function(FileUtils) getFileUtils,}) async {File file = File(path);String fileName = file.path.split('/').last;final utils = FileUtils(file);getFileUtils.call(utils);List<SliceChunkItem> config = await utils.getJsonFromSplitFileIntoChunks();final aa = await multipartUploadInit(fileName: fileName, checksumType: FileUtils.hashMethod);String uploadId = aa['data']['uploadId'];"[分片上傳] aa $aa".w();int finalUploadSliceCount = 0;FormData formData;// for (SliceChunkItem item in config) {//   item.setUploadId(uploadId);//   "[分片上傳] bb 開始上傳 partNumber ${item.partNumber} ".w();//   var fileBytes = await utils.getRange(item.start, item.end);//   item.checksum = utils.calculateSHA1FormList(fileBytes);//   // 直接傳遞數組fileBytes 給dio 會導致內存崩潰//   formData = FormData.fromMap({//     'file': MultipartFile.fromBytes(fileBytes, filename: "11"),//     'partNumber': item.partNumber,//     'checksum': item.checksum,//     'uploadId': item.uploadId,//   });//   final b = await multipartUpload(formData: formData);//   finalUploadSliceCount++;//   onSendProgress?.call(finalUploadSliceCount, config.length);//   "[分片上傳] bb 結束上傳 partNumber ${item.partNumber} $b".w();//   String tag = b["data"]["tag"];//   item.setTag(tag);// }await utils.sendItems(config: config,concurrentLimit: 5,callback: (item) async {item.setUploadId(uploadId);"[分片上傳] bb 開始上傳 partNumber ${item.partNumber} ".w();var fileBytes = await utils.getRange(item.start, item.end);item.checksum = utils.calculateSHA1FormList(fileBytes);// 直接傳遞數組fileBytes 給dio 會導致內存崩潰formData = FormData.fromMap({'file': MultipartFile.fromBytes(fileBytes, filename: "11"),'partNumber': item.partNumber,'checksum': item.checksum,'uploadId': item.uploadId,});final b = await multipartUpload(formData: formData);finalUploadSliceCount++;onSendProgress?.call(finalUploadSliceCount, config.length);"[分片上傳] bb 結束上傳 partNumber ${item.partNumber} $b".w();String tag = b["data"]["tag"];item.setTag(tag);});// 廢棄 后端不需要整體文件的hash// String checksum = utils.hash;// if (checksum.isEmpty) {//   checksum = await utils.backstageCalculateSHA1();// }final cc = await multipartUploadComplete(// checksum: checksum,uploadId: uploadId,partList: config.map((e) => e.toJson()).toList(),);// String filePath = cc["data"]["file_path"];"[分片上傳] cc $cc".w();return cc;}

文件上傳組件代碼

import 'dart:io';import 'package:LS/common/index.dart';
import 'package:LS/gen/assets.gen.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';enum UploadType {image,file,
}class UploadWidget extends StatefulWidget {final UploadType type;final Function(String, int)? onSuccess;final Function(int, List, UploadType)? onDelete;final Function()? onPickAssets;final List<String>? allowedExtensions;final int? limit;const UploadWidget({super.key,this.type = UploadType.image,this.onSuccess,this.onDelete,this.limit,this.onPickAssets,this.allowedExtensions,});@overrideState<UploadWidget> createState() => _UploadWidgetState();
}class _UploadWidgetState extends State<UploadWidget> {// 這兩個是上傳圖片的時候存的List<Uint8List> webImageList = [];List<File> appImageList = [];// 這兩個是上傳文件的時候存的 文件上傳只有 接口上傳的時候 有不同所有只要一個List<FilePickerResult> filesList = [];FilePickerResult? files;Widget get curContain {switch (widget.type) {case UploadType.image:return uploadImage();case UploadType.file:return uploadFile();}}onPreview(String url) {if (url.isNotEmpty) {Get.to(() => ImagePreviewPage(imageUrl: url,),);}}//   選擇圖片pickImages() async {Uint8List? webImage;File? appImage;XFile? image = await AppToast.getLostData();if (image != null) {if (kIsWeb) {webImage = await image.readAsBytes();} else {appImage = File(image.path);}}if (webImage != null) {webImageList.add(webImage);}if (appImage != null) {appImageList.add(appImage);}widget.onPickAssets?.call();setState(() {});}bool isValidExtension(FilePickerResult files) {return files.files.every((file) {String extension = (file.extension ?? "").toLowerCase();return (widget.allowedExtensions ??['jpg','png','doc','xls','pdf','ppt','docx','xlsx','pptx']).contains(extension);});}// 選擇文件pickFiles() async {files = await AppToast.getLostFileData(allowMultiple: false,allowedExtensions: (widget.allowedExtensions ??['jpg', 'png', 'doc', 'xls', 'pdf', 'ppt', 'docx', 'xlsx', 'pptx']),);if (files != null) {if (!isValidExtension(files as FilePickerResult)) {AppToast.show("請選擇正確的文件格式");return;}filesList.add(files!);widget.onPickAssets?.call();setState(() {});}}// 上傳圖片Widget uploadImage() {return SizedBox(width: double.infinity,child: GridView.builder(physics: const NeverScrollableScrollPhysics(),shrinkWrap: true,gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4,crossAxisSpacing: 10,mainAxisSpacing: 10,childAspectRatio: 1,),// +1 是為了添加圖片按鈕itemCount: widget.limit != null &&widget.limit! <=(kIsWeb ? webImageList.length : appImageList.length)? (kIsWeb ? webImageList.length : appImageList.length): (kIsWeb ? webImageList.length : appImageList.length) + 1,itemBuilder: (c, i) {if (kIsWeb) {if (i >= webImageList.length) {return addContainer(onTap: pickImages);}return UploadingImageWidget(webImage: webImageList[i],onDelete: () => onImageDelete(i),onSuccess: (url) => onSuccess(i, url),onPreview: onPreview,);} else {if (i >= appImageList.length) {return addContainer(onTap: pickImages);}return UploadingImageWidget(image: appImageList[i],onDelete: () => onImageDelete(i),onSuccess: (url) => onSuccess(i, url),onPreview: onPreview,);}},),);}// 上傳文件Widget uploadFile() {return SizedBox(width: double.infinity,child: GridView.builder(physics: const NeverScrollableScrollPhysics(),shrinkWrap: true,gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4,crossAxisSpacing: 10,mainAxisSpacing: 10,childAspectRatio: 1,),// +1 是為了添加圖片按鈕itemCount: widget.limit != null && widget.limit! <= filesList.length? filesList.length: (filesList.length + 1),itemBuilder: (c, i) {if (i >= filesList.length) {return addContainer(onTap: pickFiles);}return UploadingFileWidget(files: filesList[i],index: i,onDelete: () => onFileDelete(i),onSuccess: (url) => onSuccess(i, url),onPreview: onPreview,);},),);}onImageDelete(int i) {if (kIsWeb) {webImageList.removeAt(i);widget.onDelete?.call(i, webImageList, UploadType.image);} else {appImageList.removeAt(i);widget.onDelete?.call(i, appImageList, UploadType.image);}setState(() {});}onFileDelete(int i) {filesList.removeAt(i);widget.onDelete?.call(i, filesList, UploadType.file);setState(() {});}onSuccess(int i, String url) {widget.onSuccess?.call(url, i);}// 已上傳完成的容器Widget hasUploadContainer() {return Container(width: 75.w,height: 75.w,decoration: BoxDecoration(color: HexColor("#F2F4F7"),borderRadius: BorderRadius.circular(5.r),),);}// 點擊添加Widget addContainer({Function? onTap,}) {return InkWell(child: Container(width: 75.w,height: 75.w,decoration: BoxDecoration(color: HexColor("#F2F4F7"),borderRadius: BorderRadius.circular(5.r),image: DecorationImage(image: Assets.images.uploadAdd.provider(),),),alignment: Alignment.center,),onTap: () {onTap?.call();},);}@overrideWidget build(BuildContext context) {return curContain;}
}// 上傳圖片 上傳中的組件
class UploadingImageWidget extends StatefulWidget {final File? image;final Uint8List? webImage;final Function(String)? onSuccess;final Function()? onFail;final Function()? onDelete;final Function(String)? onPreview;const UploadingImageWidget({super.key,this.image,this.webImage,this.onSuccess,this.onFail,this.onDelete,this.onPreview,});@overrideState<UploadingImageWidget> createState() => _UploadingImageWidgetState();
}class _UploadingImageWidgetState extends State<UploadingImageWidget> {// 是否上傳失敗bool isUploadFail = false;double cruProgress = 0.0;String httpPath = "";// 正在上傳中bool isUploading = false;@overridevoid initState() {super.initState();initUpload();}// 立即進行上傳initUpload() {try {kIsWeb ? webUpload() : appUpload();} catch (e) {widget.onFail?.call();setState(() {isUploadFail = true;});}}onTapDelete() {widget.onDelete?.call();}webUpload() async {setState(() {isUploading = true;});final res = await Api.uploadFileListInt(widget.webImage as Uint8List,name: "img",onSendProgress: (count, total) {setState(() {cruProgress = count / total;});},);setState(() {isUploading = false;});httpPath = res["data"] ?? "";widget.onSuccess?.call(httpPath);}appUpload() async {setState(() {isUploading = true;});final res = await Api.uploadFile(widget.image!.path,onSendProgress: (count, total) {setState(() {cruProgress = count / total;});},);setState(() {isUploading = false;});httpPath = res["data"] ?? "";widget.onSuccess?.call(httpPath);}@overrideWidget build(BuildContext context) {return AnimatedContainer(duration: const Duration(seconds: 1),width: double.infinity,height: double.infinity,decoration: BoxDecoration(color: HexColor("#F2F4F7"),borderRadius: BorderRadius.circular(5.r),),child: isUploadFail? failUploadContainer(): (cruProgress == 1.0 && !isUploading? finallyUploadContainer(): beforeUploadContainer()),);}// 上傳之前的容器Stack beforeUploadContainer() {return Stack(clipBehavior: Clip.none,children: [if (widget.webImage != null)Container(width: double.infinity,height: double.infinity,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: Image.memory(widget.webImage as Uint8List,width: double.infinity,height: double.infinity,),),if (widget.image != null)Container(width: double.infinity,height: double.infinity,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: Image.file(widget.image as File,width: double.infinity,height: double.infinity,fit: BoxFit.cover,),),Positioned.fill(child: Opacity(opacity: 0.6,child: Container(width: double.infinity,height: double.infinity,decoration: BoxDecoration(color: HexColor("#000000"),borderRadius: BorderRadius.circular(5.r),),),),),Positioned(top: -4.w,right: -4.w,child: InkWell(onTap: onTapDelete,child: Container(width: 18.w,height: 18.w,decoration: const BoxDecoration(shape: BoxShape.circle,color: Colors.white,),child: Assets.images.uploadClose.image(width: 18.w),),),),Positioned.fill(child: Container(width: double.infinity,height: double.infinity,alignment: Alignment.center,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: SizedBox(width: 40.w,child: LineProgressWidget(cruProgress: cruProgress,minHeight: 5.h,color: HexColor("#CCCCCC"),showText: false,valueColor: AlwaysStoppedAnimation<Color>(HexColor("#F9DE4A")),),),),),],);}// 完成上傳的容器Stack finallyUploadContainer() {return Stack(clipBehavior: Clip.none,children: [if (widget.webImage != null)InkWell(onTap: () => widget.onPreview?.call(httpPath),child: Container(width: double.infinity,height: double.infinity,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: Image.memory(widget.webImage as Uint8List,width: double.infinity,height: double.infinity,),),),if (widget.image != null)InkWell(onTap: () => widget.onPreview?.call(httpPath),child: Container(width: double.infinity,height: double.infinity,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: Image.file(widget.image as File,width: double.infinity,height: double.infinity,fit: BoxFit.cover,),),),Positioned(top: -4.w,right: -4.w,child: InkWell(onTap: onTapDelete,child: Container(width: 18.w,height: 18.w,decoration: const BoxDecoration(shape: BoxShape.circle,color: Colors.white,),child: Assets.images.uploadClose.image(width: 18.w),),),),],);}// 上傳失敗重新上傳failReUpload() async {cruProgress = 0.0;isUploadFail = false;setState(() {});initUpload();}// 失敗上傳的容器Stack failUploadContainer() {return Stack(clipBehavior: Clip.none,children: [if (widget.webImage != null)Container(width: double.infinity,height: double.infinity,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: Image.memory(widget.webImage as Uint8List,width: double.infinity,height: double.infinity,),),if (widget.image != null)Container(width: double.infinity,height: double.infinity,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: Image.file(widget.image as File,width: double.infinity,height: double.infinity,fit: BoxFit.cover,),),Positioned.fill(child: Container(width: double.infinity,height: double.infinity,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),color: HexColor("#000000").withOpacity(0.6),),alignment: Alignment.center,child: InkWell(onTap: failReUpload,child: SizedBox(width: 20.w,height: 20.w,child: Assets.images.uploadReload.image(width: 20.w),),),),),Positioned(top: -4.w,right: -4.w,child: InkWell(onTap: onTapDelete,child: Container(width: 18.w,height: 18.w,decoration: const BoxDecoration(shape: BoxShape.circle,color: Colors.white,),child: Assets.images.uploadClose.image(width: 18.w),),),),],);}
}// 上傳文件 上傳中的組件
class UploadingFileWidget extends StatefulWidget {final int index;final FilePickerResult? files;final Function(String)? onSuccess;final Function()? onFail;final Function()? onDelete;final Function(String)? onPreview;const UploadingFileWidget({super.key,this.files,required this.index,this.onSuccess,this.onFail,this.onDelete,this.onPreview,});@overrideState<UploadingFileWidget> createState() => _UploadingFileWidgetState();
}class _UploadingFileWidgetState extends State<UploadingFileWidget> {// 是否上傳失敗bool isUploadFail = false;// 正在上傳中bool isUploading = false;double cruProgress = 0.0;String httpPath = "";Function? cancelSendItems;@overridevoid initState() {super.initState();initUpload();}// 立即進行上傳initUpload() {try {kIsWeb ? webUpload() : appUpload();} catch (e) {widget.onFail?.call();setState(() {isUploadFail = true;});}}onTapDelete() {widget.onDelete?.call();if (cancelSendItems != null) {cancelSendItems!.call();}}webUpload() async {PlatformFile curFile = widget.files!.files.first;setState(() {isUploading = true;});final res = await Api.uploadFilePlatformFile(curFile,onSendProgress: (count, total) {setState(() {cruProgress = count / total;});},);httpPath = res["data"] ?? "";setState(() {isUploading = false;});widget.onSuccess?.call(httpPath);}appUpload() async {var path = widget.files!.paths.first;int size = widget.files?.files.first.size ?? 0;if (size > FileUtils.mToSize(20)) {appSliceUpload();return;}setState(() {isUploading = true;});final res = await Api.uploadFile(path as String,onSendProgress: (count, total) async {// await Future.delayed(const Duration(seconds: 1));setState(() {cruProgress = count / total;});},);setState(() {isUploading = false;});httpPath = res["data"] ?? "";widget.onSuccess?.call(httpPath);}// 大文件切片上傳appSliceUpload() async {var path = widget.files!.paths.first;setState(() {isUploading = true;});final res = await Api.uploadSliceFile(path as String,onSendProgress: (count, total) async {// await Future.delayed(const Duration(seconds: 1));setState(() {cruProgress = count / total;});},getFileUtils: (utils) {"[分片上傳] 獲取 utils $utils".w();cancelSendItems = () => utils.cancelSendItems();},);setState(() {isUploading = false;});httpPath = res["data"] ?? "";widget.onSuccess?.call(httpPath);}@overrideWidget build(BuildContext context) {return AnimatedContainer(duration: const Duration(seconds: 1),width: double.infinity,height: double.infinity,decoration: BoxDecoration(color: HexColor("#F2F4F7"),borderRadius: BorderRadius.circular(5.r),),child: isUploadFail? failUploadContainer(): (cruProgress == 1.0 && !isUploading? finallyUploadContainer(): beforeUploadContainer()),);}// 上傳之前的容器Stack beforeUploadContainer() {return Stack(clipBehavior: Clip.none,children: [if (widget.files?.files.first != null)fileInfoContainer(widget.files?.files.first),Positioned.fill(child: Opacity(opacity: 0.6,child: Container(width: double.infinity,height: double.infinity,decoration: BoxDecoration(color: HexColor("#000000"),borderRadius: BorderRadius.circular(5.r),),),),),Positioned.fill(child: Container(width: double.infinity,height: double.infinity,alignment: Alignment.center,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: SizedBox(width: 40.w,child: LineProgressWidget(cruProgress: cruProgress,minHeight: 5.h,color: HexColor("#CCCCCC"),showText: false,valueColor: AlwaysStoppedAnimation<Color>(HexColor("#F9DE4A")),),),),),Positioned(top: -4.w,right: -4.w,child: InkWell(onTap: onTapDelete,child: Container(width: 18.w,height: 18.w,decoration: const BoxDecoration(shape: BoxShape.circle,color: Colors.white,),child: Assets.images.uploadClose.image(width: 18.w),),),),],);}// 文件信息展示容器Widget fileInfoContainer(PlatformFile? file) {String fileName = file?.name ?? "";if (Utils.isImageFile(fileName)) {if (kIsWeb) {Uint8List webImageFile = file?.bytes as Uint8List;return InkWell(onTap: () => widget.onPreview?.call(httpPath),child: Container(width: double.infinity,height: double.infinity,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: Image.memory(webImageFile,width: double.infinity,height: double.infinity,),),);} else {File imageFile = File(file?.path ?? "");return InkWell(onTap: () => widget.onPreview?.call(httpPath),child: Container(width: double.infinity,height: double.infinity,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: Image.file(imageFile,width: double.infinity,height: double.infinity,fit: BoxFit.cover,),),);}}return Container(width: double.infinity,height: double.infinity,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),alignment: Alignment.center,child: Column(mainAxisSize: MainAxisSize.min,crossAxisAlignment: CrossAxisAlignment.center,mainAxisAlignment: MainAxisAlignment.center,children: [Assets.images.uploadFileIcon.image(width: 20.w),SizedBox(height: 5.h),Text(fileName,style: TextStyle(fontFamily: Font.pingFang,fontWeight: FontWeight.w500,fontSize: 12.sp,color: HexColor("#1A1A1A"),height: 1.1,),textAlign: TextAlign.center,overflow: TextOverflow.ellipsis,maxLines: 3,),],),);}// 完成上傳的容器Stack finallyUploadContainer() {return Stack(clipBehavior: Clip.none,children: [if (widget.files?.files.first != null)fileInfoContainer(widget.files?.files.first),Positioned(top: -4.w,right: -4.w,child: InkWell(onTap: onTapDelete,child: Container(width: 18.w,height: 18.w,decoration: const BoxDecoration(shape: BoxShape.circle,color: Colors.white,),child: Assets.images.uploadClose.image(width: 18.w),),),),],);}// 上傳失敗重新上傳failReUpload() async {cruProgress = 0.0;isUploadFail = false;setState(() {});initUpload();}// 失敗上傳的容器Stack failUploadContainer() {return Stack(clipBehavior: Clip.none,children: [if (widget.files?.files.first != null)fileInfoContainer(widget.files?.files.first),Positioned.fill(child: Container(width: double.infinity,height: double.infinity,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),color: HexColor("#000000").withOpacity(0.6),),alignment: Alignment.center,child: InkWell(onTap: failReUpload,child: SizedBox(width: 20.w,height: 20.w,child: Assets.images.uploadReload.image(width: 20.w),),),),),Positioned(top: -4.w,right: -4.w,child: InkWell(onTap: onTapDelete,child: Container(width: 18.w,height: 18.w,decoration: const BoxDecoration(shape: BoxShape.circle,color: Colors.white,),child: Assets.images.uploadClose.image(width: 18.w),),),),],);}
}// 已上傳圖片或文件展示
class HasUploadShowWidget extends StatelessWidget {final List<String?> urls;final Function(int)? onDelete;final bool showDelete;final Function(String)? onFileTap;final TextDirection textDirection;const HasUploadShowWidget({super.key,required this.urls,this.onDelete,this.showDelete = true,this.onFileTap,this.textDirection = TextDirection.ltr,});onTapDelete(int idx) {onDelete?.call(idx);}onPreview(String url) {if (url.isNotEmpty) {Get.to(() => ImagePreviewPage(imageUrl: url,),);}}@overrideWidget build(BuildContext context) {return SizedBox(width: double.infinity,child: Directionality(textDirection: textDirection,child: GridView.builder(shrinkWrap: true,itemCount: urls.length,physics: const NeverScrollableScrollPhysics(),gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4,crossAxisSpacing: 10,mainAxisSpacing: 10,childAspectRatio: 1,),itemBuilder: (c, i) {if (Utils.isImageFile(urls[i] ?? "")) {return Stack(clipBehavior: Clip.none,children: [InkWell(onTap: () => onPreview(urls[i] ?? ""),child: Container(width: 75.w,height: 75.w,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: urls[i] != null? CachedNetworkImage(width: 75.w,height: 75.w,imageUrl: urls[i] ?? "",fit: BoxFit.cover,): null,),),if (showDelete)Positioned(top: -4.w,right: -4.w,child: InkWell(onTap: () => onTapDelete(i),child: Container(width: 18.w,height: 18.w,decoration: const BoxDecoration(shape: BoxShape.circle,color: Colors.white,),child: Assets.images.uploadClose.image(width: 18.w),),),),],);}return Stack(clipBehavior: Clip.none,children: [InkWell(onTap: () => onFileTap?.call(urls[i] ?? ""),child: Container(width: 75.w,height: 75.w,clipBehavior: Clip.none,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),color: HexColor("#F2F4F7"),),alignment: Alignment.center,child: Column(crossAxisAlignment: CrossAxisAlignment.center,mainAxisAlignment: MainAxisAlignment.center,children: [Assets.images.uploadFileIcon.image(width: 20.w),SizedBox(height: 5.h),Container(constraints: BoxConstraints(maxWidth: 75.w),child: Text(Utils.getFileNameFromUrl((urls[i] ?? "")),style: TextStyle(fontFamily: Font.pingFang,fontWeight: FontWeight.w500,fontSize: 12.sp,color: HexColor("#1A1A1A"),height: 1.1,),textAlign: TextAlign.center,overflow: TextOverflow.ellipsis,maxLines: 2,),),],),),),if (showDelete)Positioned(top: -4.w,right: -4.w,child: InkWell(onTap: () => onTapDelete(i),child: Container(width: 18.w,height: 18.w,decoration: const BoxDecoration(shape: BoxShape.circle,color: Colors.white,),child: Assets.images.uploadClose.image(width: 18.w),),),),],);}),),);}
}

使用 文件上傳 (如果上傳的是圖片 顯示的也是圖片樣式)

UploadWidget(type: UploadType.file,limit: 5 - controller.lastFiles.length,onPickAssets: () {controller.curUploadCount++;},onDelete: (i, list, t) {controller.curUploadCount--;controller.state.files.removeAt(i);controller.update();},onSuccess: (url, i) {controller.curUploadCount--;controller.state.files.add(url);controller.update();},)

圖片上傳

UploadWidget(type: UploadType.image,limit: 9,onPickAssets: () {curUploadCount++;setState(() {});},onDelete: (i, list, t) {curUploadCount--;print("文件 -- $curUploadCount");imagesFiles.removeAt(i);setState(() {});},onSuccess: (url, i) {curUploadCount--;// controller.state.files.add(url);imagesFiles.add(url);setState(() {});},),

支持撤銷文件上傳

斷點續傳功能和秒傳

在原有的基礎上改動如下

所有上傳接口都加上文件的MD5 值,分片的幾個接口將uploadId改為整個文件的MD5

新增MD5的方法 依舊使用crypto 庫

crypto | Dart PackageImplementations of SHA, MD5, and HMAC cryptographic functions.icon-default.png?t=N7T8https://pub-web.flutter-io.cn/packages/crypto

 static staticCalculateMD5(File file) async {if (!file.existsSync()) {print('File "$file" does not exist.');return;}Digest digest = await md5.bind(file.openRead()).first;return digest.toString();}

?app 使用

  FormData formData = FormData.fromMap({'file': await MultipartFile.fromFile(path),'md5': await FileUtils.staticCalculateMD5(File(path)),});

web端使用

static staticCalculateMD5Stream(List<int> input) async {Digest digest = md5.convert(input);return digest.toString();}
FormData formData = FormData.fromMap({'file': multipartFile,'md5': await FileUtils.staticCalculateMD5Stream(fileBytes),});

分片接口改動如下

1、初始化分片接口

新增參數?md5 文件的MD5? 這樣就可以驗證文件是否上傳過,如果上傳直接返回文件地址

未上傳完 會返回已上傳的分片 然后對求出未上傳的分片進行繼續上傳?

邏輯修改如下

    String filePath = aa['data']['filePath'] ?? "";List<SliceChunkItem> sendSliceConfig = config;int finalUploadSliceCount = 0;// 說明是上傳過的文件 直接秒傳if (filePath.isNotEmpty) {onSendProgress?.call(1, 1);return aa;} else {List partList = (aa['data']['partList'] ?? []);List<SliceChunkItem> filterPartList = partList.map<SliceChunkItem>((e) => SliceChunkItem.fromJson(e)).toList();for (var it in filterPartList) {int idx = config.indexWhere((e) => e.partNumber == it.partNumber);if (idx != -1) {config[idx] = it;}}sendSliceConfig =config.where((e) => !(e.end == 0 && e.start == 0)).toList();if (sendSliceConfig.isNotEmpty) {finalUploadSliceCount = filterPartList.length;onSendProgress?.call(finalUploadSliceCount, config.length);}}

2、分片的接口 新增了文件的MD5

?完成接口一樣的新增md5

最后最新的分片上傳接口邏輯如下

?

// 文件分片上傳static uploadSliceFile(String path, {ProgressCallback? onSendProgress,required Function(FileUtils) getFileUtils,}) async {File file = File(path);String fileName = file.path.split('/').last;final utils = FileUtils(file);getFileUtils.call(utils);List<SliceChunkItem> config = await utils.getJsonFromSplitFileIntoChunks();String curMd5Code = utils.hash;if (curMd5Code.isEmpty) {curMd5Code = await FileUtils.staticCalculateMD5(file);}final aa = await multipartUploadInit(fileName: fileName,checksumType: FileUtils.hashMethod,md5: curMd5Code,);String filePath = aa['data']['filePath'] ?? "";List<SliceChunkItem> sendSliceConfig = config;int finalUploadSliceCount = 0;// 說明是上傳過的文件 直接秒傳if (filePath.isNotEmpty) {onSendProgress?.call(1, 1);return aa;} else {List partList = (aa['data']['partList'] ?? []);List<SliceChunkItem> filterPartList = partList.map<SliceChunkItem>((e) => SliceChunkItem.fromJson(e)).toList();for (var it in filterPartList) {int idx = config.indexWhere((e) => e.partNumber == it.partNumber);if (idx != -1) {config[idx] = it;}}sendSliceConfig =config.where((e) => !(e.end == 0 && e.start == 0)).toList();if (sendSliceConfig.isNotEmpty) {finalUploadSliceCount = filterPartList.length;onSendProgress?.call(finalUploadSliceCount, config.length);}}// String uploadId = aa['data']['uploadId'];"[分片上傳] aa $aa".w();FormData formData;await utils.sendItems(config: sendSliceConfig,concurrentLimit: 4,callback: (item) async {// item.setUploadId(uploadId);"[分片上傳] bb 開始上傳 partNumber ${item.partNumber} ".w();var fileBytes = await utils.getRange(item.start ?? 0, item.end ?? 0);item.checksum = utils.calculateSHA1FormList(fileBytes);// 直接傳遞數組fileBytes 給dio 會導致內存崩潰formData = FormData.fromMap({'file': MultipartFile.fromBytes(fileBytes, filename: "11"),'partNumber': item.partNumber,'checksum': item.checksum,'md5': curMd5Code,});final b = await multipartUpload(formData: formData);finalUploadSliceCount++;onSendProgress?.call(finalUploadSliceCount, config.length);"[分片上傳] bb 結束上傳 partNumber ${item.partNumber} $b".w();String tag = b["data"]["tag"];item.setTag(tag);});final cc = await multipartUploadComplete(// checksum: checksum,// uploadId: uploadId,md5: curMd5Code,partList: config.map((e) => e.toJson()).toList(),);// String filePath = cc["data"]["file_path"];"[分片上傳] cc $cc".w();return cc;}

上傳組件發現刪除存在bug 修復如下

UploadingFileWidget 組件使用key: ValueKey('image_$i'),進行標識

GridView.builder(physics: const NeverScrollableScrollPhysics(),shrinkWrap: true,gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4,crossAxisSpacing: 10,mainAxisSpacing: 10,childAspectRatio: 1,),// +1 是為了添加圖片按鈕itemCount: widget.limit != null && widget.limit! <= filesList.length? filesList.length: (filesList.length + 1),itemBuilder: (c, i) {if (i >= filesList.length) {return addContainer(onTap: pickFiles);}return UploadingFileWidget(files: filesList[i],index: i,onDelete: () => onFileDelete(i),onSuccess: (url) => onSuccess(i, url),onPreview: onPreview,key: ValueKey('image_$i'),);},),

在Flutter中,Key可以是任何類型的對象,但最常用的是 GlobalKey、ValueKey 和 ObjectKey。

  1. GlobalKey: 用于在整個應用程序中唯一標識一個Widget。當你需要在程序的不同部分引用同一個組件時,可以使用GlobalKey。比如,如果你需要通過GlobalKey在一個頁面中的某個組件,可以使用GlobalKey。

  2. ValueKey: 用于基于值的比較,可以根據給定的值來標識Widget,在列表、集合或父子關系中非常有用。比如,如果你有一個具有一組項目的列表,并且需要標識這些項目,可以使用ValueKey,并以項目的唯一標識符作為值。

  3. ObjectKey: 與ValueKey類似,可以根據對象的身份來標識Widget。如果你需要根據對象的身份來標識Widget,而不是基于對象的值,可以使用ObjectKey。

除了這些內置的Key類型,你也可以創建自定義的Key類,只要它們遵循Key的方法和屬性即可。

在你的情況下,你可以使用GlobalKey來保證每個UploadingImageWidget都有唯一的標識,這樣就可以避免狀態混淆的問題。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/696147.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/696147.shtml
英文地址,請注明出處:http://en.pswp.cn/news/696147.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

CSP-J 2023 T3 一元二次方程

文章目錄 題目題目背景題目描述輸入格式輸出格式樣例 #1樣例輸入 #1樣例輸出 #1 提示 題目傳送門題解思路總代碼 提交結果尾聲 題目 題目背景 眾所周知&#xff0c;對一元二次方程 a x 2 b x c 0 , ( a ≠ 0 ) ax ^ 2 bx c 0, (a \neq 0) ax2bxc0,(a0)&#xff0c;可…

STM32G030C8T6:定時器1ms中斷(以64MHz外部晶振為例)

本專欄記錄STM32開發各個功能的詳細過程&#xff0c;方便自己后續查看&#xff0c;當然也供正在入門STM32單片機的兄弟們參考&#xff1b; 本小節的目標是&#xff0c;系統主頻64 MHZ,采用高速外部晶振&#xff0c;通過定時器3 每秒中斷控制 PB9 引腳輸出高低電平&#xff0c;從…

20240222作業

完善對話框&#xff0c;點擊登錄對話框&#xff0c;如果賬號和密碼匹配&#xff0c;則彈出信息對話框&#xff0c;給出提示“登錄成功"&#xff0c;提供一個Ok按鈕&#xff0c;用戶點擊OK后&#xff0c;關閉登錄界面&#xff0c;跳轉到其他界面 如果賬號和密碼不匹配&…

Java基礎-注解

注解 注解是用來干什么的它有什么作用注解的常見分類內置注解Override注解定義 Deprecated注解定義 SuppressWarnings注解定義 元注解Target注解定義ElementType Retention&&RetentionTarget注解定義RetentionPolicy Documented注解定義 Inherited注解定義用法 Repeata…

低代碼開發:推動互聯網企業數字化轉型的關鍵因素

聯網行業作為我國數字經濟發展的核心驅動力&#xff0c;在推動國家數字化轉型中扮演著至關重要的角色。與其他傳統行業相比&#xff0c;互聯網企業面臨更加緊迫的數字化轉型需求&#xff0c;因為它們需要不斷適應快速變化的市場環境和技術趨勢。 然而&#xff0c;由于互聯網企業…

深入理解APDU協議與Java開發

1. 什么是APDU&#xff1f; APDU&#xff0c;即應用協議數據單元&#xff08;Application Protocol Data Unit&#xff09;&#xff0c;是一種在智能卡與卡片讀卡器之間進行通信的協議。APDU定義了在交互中傳輸的數據格式和規則&#xff0c;允許讀卡器發送指令并接收響應。 2…

MFC 皮膚庫配置

1.創建MFC 對話框 2.添加皮膚資源 添加資源 添加頭文件 關閉SDL檢測 添加靜態庫文件 修改字符集 添加頭文件 將皮膚中的ssk文件加載到初始化實例中 > 運行即可

springboot 的 websocket 里面使用 @Autowired 注入 service 或 bean 時,報空指針異常

直接上解決方案&#xff1a; 在你的WebSocketServer服務器中 public static MessageService messageService; //要注入的類// 注入的時候&#xff0c;給類的 service 注入Autowiredpublic void setChatService(MessageService messageService) {WebSocketServer.messageSer…

【寸鐵的刷題筆記】樹、dfs、bfs、回溯、遞歸(一)

【寸鐵的刷題筆記】樹、dfs、bfs、回溯、遞歸(一) 大家好 我是寸鐵&#x1f44a; 總結了一篇刷題關于樹、dfs、bfs、回溯、遞歸的文章? 喜歡的小伙伴可以點點關注 &#x1f49d; 105. 從前序與中序遍歷序列構造二叉樹 模擬分析圖 代碼實現 /*** Definition for a binary tre…

HarmonyOS—添加/刪除Module

Module是應用/服務的基本功能單元&#xff0c;包含了源代碼、資源文件、第三方庫及應用/服務配置文件&#xff0c;每一個Module都可以獨立進行編譯和運行。一個HarmonyOS應用/服務通常會包含一個或多個Module&#xff0c;因此&#xff0c;可以在工程中創建多個Module&#xff0…

如何利用內網穿透工具在企業微信開發者中心實現本地接口服務回調

文章目錄 1. Windows安裝Cpolar2. 創建Cpolar域名3. 創建企業微信應用4. 定義回調本地接口5. 回調和可信域名接口校驗6. 設置固定Cpolar域名7. 使用固定域名校驗 企業微信開發者在應用的開發測試階段&#xff0c;應用服務通常是部署在開發環境&#xff0c;在有數據回調的開發場…

SQL查詢每個類別價格前3的數據

SELECTproduct_id,category,price FROM (SELECTproduct_id,category,price,ROW_NUMBER() OVER (PARTITION BY category ORDER BY price) AS rankFROMyour_products_table ) AS ranked_products WHERErank < 3;DENSE_RANK() 和 ROW_NUMBER() 是窗口函數&#xff08;Window Fu…

前端知識復習

1.symbol類型 Symbol 是 ECMAScript 6 中引入的一種新的基本數據類型&#xff0c;它表示獨一無二的值。Symbol 值是通過 Symbol() 函數創建的。 Symbol 值具有以下特點&#xff1a; 獨一無二性&#xff08;唯一性&#xff09;&#xff1a;每個通過 Symbol() 函數創建的 Symb…

十三:集合

提示&#xff1a;文章寫完后&#xff0c;目錄可以自動生成&#xff0c;如何生成可參考右邊的幫助文檔 文章目錄 01、Java 集合框架概述1.1、集合框架與數組的對比及概述1.2、集合框架涉及到的API 02、Collection接口方法2.1、Collection接口中的常用方法12.2、Collection接口中…

在idea中配置Tomcat

1.在idea中點擊右上角 2.點擊Edit Configurations,點擊加號 3.向下拉找到Tomcat Server下的Local,點一下 點擊Configure 找到tomcat文件路徑,選擇apache-tomcat-8.5.63(8.5.63是我的版本號) 選擇好路徑后點ok就配置好了 總步驟:

Vue 圖片輪播第三方庫 Vue-awesome-swiper介紹及簡單例子

vue-awesome-swiper 是一個基于 Swiper 的 Vue 輪播圖組件&#xff0c;Swiper 是一個流行的移動端觸摸滑動插件。它為 Vue.js 應用提供了一套豐富的輪播組件&#xff0c;支持多種布局和功能&#xff0c;如自動播放、無限循環、觸摸滑動等。 安裝 首先&#xff…

代碼隨想錄算法訓練營第一天

● 今日學習的文章鏈接和視頻鏈接 ● 自己看到題目的第一想法 1. 704二分法&#xff1a; 方法一&#xff1a; 整個數組是 左閉右閉區間 [ ] left指針指向數組開始下標&#xff0c; right 指針指向數組最后下表nums.size()-1, mid為 (leftright) /2循環條件 left<rightnu…

打開stable diffusion webui時,提示缺少clip或clip安裝不上的解決方案(windows下的操作)

1.問題描述 打開stable diffusion webui時&#xff0c;提示缺少clip或clip安裝不上 2.解決方案 原因&#xff1a;stable diffusion webui環境中的clip其實是open_clip&#xff0c;不能用pip install clip安裝解決方法是直接到github下載 open_clip 代碼到本地&#xff0c;并…

linux環境ssh-rsa進行簽名\權限\登錄\原理(免密登錄)

linux環境ssh-rsa進行簽名權限登錄(免密登錄) SSH原理與運用什么是SSH?SSH的使用場景ssh-rsa獲取xshell環境登錄獲取ssh-rsa使用ssh-rsa登錄SHA系列SHA-1、SHA-256和RSA的區別RSA原理數論基礎RSA機制RSA數學密鑰生成公式RSA數學加密理論RSA數學簽名公式

小折疊也能成為主力機,全新小折疊旗艦華為Pocket 2正式發布

2024年2月22日&#xff0c;華為在三亞舉辦華為Pocket 2時尚盛典&#xff0c;正式發布其全新小折疊旗艦華為Pocket 2。一直以來&#xff0c;華為致力于萃取各界藝術靈感&#xff0c;不斷探尋科技美學的可能性&#xff0c;華為Pocket系列更是秉承將奢雅美學與尖端科技融為一體的理…